summaryrefslogtreecommitdiffstats
path: root/src/network
diff options
context:
space:
mode:
Diffstat (limited to 'src/network')
-rw-r--r--src/network/fuzz-netdev-parser.c28
-rw-r--r--src/network/fuzz-netdev-parser.options2
-rw-r--r--src/network/fuzz-network-parser.c28
-rw-r--r--src/network/fuzz-network-parser.options2
-rw-r--r--src/network/generator/main.c211
-rw-r--r--src/network/generator/network-generator.c1444
-rw-r--r--src/network/generator/network-generator.h115
-rw-r--r--src/network/generator/test-network-generator.c459
-rw-r--r--src/network/meson.build323
-rw-r--r--src/network/netdev/bareudp.c86
-rw-r--r--src/network/netdev/bareudp.h34
-rw-r--r--src/network/netdev/batadv.c216
-rw-r--r--src/network/netdev/batadv.h47
-rw-r--r--src/network/netdev/bond.c457
-rw-r--r--src/network/netdev/bond.h60
-rw-r--r--src/network/netdev/bridge.c290
-rw-r--r--src/network/netdev/bridge.h46
-rw-r--r--src/network/netdev/dummy.c13
-rw-r--r--src/network/netdev/dummy.h11
-rw-r--r--src/network/netdev/fou-tunnel.c285
-rw-r--r--src/network/netdev/fou-tunnel.h42
-rw-r--r--src/network/netdev/geneve.c299
-rw-r--r--src/network/netdev/geneve.h52
-rw-r--r--src/network/netdev/ifb.c14
-rw-r--r--src/network/netdev/ifb.h13
-rw-r--r--src/network/netdev/ipoib.c161
-rw-r--r--src/network/netdev/ipoib.h30
-rw-r--r--src/network/netdev/ipvlan.c92
-rw-r--r--src/network/netdev/ipvlan.h25
-rw-r--r--src/network/netdev/l2tp-tunnel.c880
-rw-r--r--src/network/netdev/l2tp-tunnel.h80
-rw-r--r--src/network/netdev/macsec.c1233
-rw-r--r--src/network/netdev/macsec.h87
-rw-r--r--src/network/netdev/macvlan.c170
-rw-r--r--src/network/netdev/macvlan.h25
-rw-r--r--src/network/netdev/netdev-gperf.gperf270
-rw-r--r--src/network/netdev/netdev-util.c100
-rw-r--r--src/network/netdev/netdev-util.h27
-rw-r--r--src/network/netdev/netdev.c950
-rw-r--r--src/network/netdev/netdev.h262
-rw-r--r--src/network/netdev/netdevsim.c13
-rw-r--r--src/network/netdev/netdevsim.h13
-rw-r--r--src/network/netdev/nlmon.c25
-rw-r--r--src/network/netdev/nlmon.h14
-rw-r--r--src/network/netdev/tunnel.c1318
-rw-r--r--src/network/netdev/tunnel.h138
-rw-r--r--src/network/netdev/tuntap.c261
-rw-r--r--src/network/netdev/tuntap.h26
-rw-r--r--src/network/netdev/vcan.c12
-rw-r--r--src/network/netdev/vcan.h17
-rw-r--r--src/network/netdev/veth.c98
-rw-r--r--src/network/netdev/veth.h16
-rw-r--r--src/network/netdev/vlan.c234
-rw-r--r--src/network/netdev/vlan.h27
-rw-r--r--src/network/netdev/vrf.c35
-rw-r--r--src/network/netdev/vrf.h15
-rw-r--r--src/network/netdev/vxcan.c74
-rw-r--r--src/network/netdev/vxcan.h16
-rw-r--r--src/network/netdev/vxlan.c468
-rw-r--r--src/network/netdev/vxlan.h76
-rw-r--r--src/network/netdev/wireguard.c1183
-rw-r--r--src/network/netdev/wireguard.h84
-rw-r--r--src/network/netdev/wlan.c258
-rw-r--r--src/network/netdev/wlan.h22
-rw-r--r--src/network/netdev/xfrm.c55
-rw-r--r--src/network/netdev/xfrm.h14
-rw-r--r--src/network/networkctl.c2965
-rw-r--r--src/network/networkd-address-generation.c440
-rw-r--r--src/network/networkd-address-generation.h14
-rw-r--r--src/network/networkd-address-label.c298
-rw-r--r--src/network/networkd-address-label.h30
-rw-r--r--src/network/networkd-address-pool.c175
-rw-r--r--src/network/networkd-address-pool.h17
-rw-r--r--src/network/networkd-address.c2182
-rw-r--r--src/network/networkd-address.h135
-rw-r--r--src/network/networkd-bridge-fdb.c535
-rw-r--r--src/network/networkd-bridge-fdb.h54
-rw-r--r--src/network/networkd-bridge-mdb.c365
-rw-r--r--src/network/networkd-bridge-mdb.h29
-rw-r--r--src/network/networkd-bridge-vlan.c249
-rw-r--r--src/network/networkd-bridge-vlan.h31
-rw-r--r--src/network/networkd-can.c336
-rw-r--r--src/network/networkd-can.h18
-rw-r--r--src/network/networkd-conf.c38
-rw-r--r--src/network/networkd-conf.h14
-rw-r--r--src/network/networkd-dhcp-common.c1306
-rw-r--r--src/network/networkd-dhcp-common.h107
-rw-r--r--src/network/networkd-dhcp-prefix-delegation.c1298
-rw-r--r--src/network/networkd-dhcp-prefix-delegation.h25
-rw-r--r--src/network/networkd-dhcp-server-bus.c117
-rw-r--r--src/network/networkd-dhcp-server-bus.h11
-rw-r--r--src/network/networkd-dhcp-server-static-lease.c210
-rw-r--r--src/network/networkd-dhcp-server-static-lease.h26
-rw-r--r--src/network/networkd-dhcp-server.c736
-rw-r--r--src/network/networkd-dhcp-server.h16
-rw-r--r--src/network/networkd-dhcp4.c1784
-rw-r--r--src/network/networkd-dhcp4.h34
-rw-r--r--src/network/networkd-dhcp6.c859
-rw-r--r--src/network/networkd-dhcp6.h32
-rw-r--r--src/network/networkd-gperf.gperf35
-rw-r--r--src/network/networkd-ipv4acd.c327
-rw-r--r--src/network/networkd-ipv4acd.h13
-rw-r--r--src/network/networkd-ipv4ll.c320
-rw-r--r--src/network/networkd-ipv4ll.h16
-rw-r--r--src/network/networkd-ipv6-proxy-ndp.c180
-rw-r--r--src/network/networkd-ipv6-proxy-ndp.h13
-rw-r--r--src/network/networkd-ipv6ll.c247
-rw-r--r--src/network/networkd-ipv6ll.h37
-rw-r--r--src/network/networkd-json.c1422
-rw-r--r--src/network/networkd-json.h10
-rw-r--r--src/network/networkd-link-bus.c908
-rw-r--r--src/network/networkd-link-bus.h39
-rw-r--r--src/network/networkd-link.c2736
-rw-r--r--src/network/networkd-link.h245
-rw-r--r--src/network/networkd-lldp-rx.c173
-rw-r--r--src/network/networkd-lldp-rx.h22
-rw-r--r--src/network/networkd-lldp-tx.c137
-rw-r--r--src/network/networkd-lldp-tx.h10
-rw-r--r--src/network/networkd-manager-bus.c428
-rw-r--r--src/network/networkd-manager-bus.h12
-rw-r--r--src/network/networkd-manager.c1075
-rw-r--r--src/network/networkd-manager.h117
-rw-r--r--src/network/networkd-ndisc.c1211
-rw-r--r--src/network/networkd-ndisc.h49
-rw-r--r--src/network/networkd-neighbor.c683
-rw-r--r--src/network/networkd-neighbor.h44
-rw-r--r--src/network/networkd-netlabel.c128
-rw-r--r--src/network/networkd-netlabel.h5
-rw-r--r--src/network/networkd-network-bus.c141
-rw-r--r--src/network/networkd-network-bus.h13
-rw-r--r--src/network/networkd-network-gperf.gperf591
-rw-r--r--src/network/networkd-network.c1335
-rw-r--r--src/network/networkd-network.h400
-rw-r--r--src/network/networkd-nexthop.c1406
-rw-r--r--src/network/networkd-nexthop.h59
-rw-r--r--src/network/networkd-queue.c308
-rw-r--r--src/network/networkd-queue.h139
-rw-r--r--src/network/networkd-radv.c1339
-rw-r--r--src/network/networkd-radv.h86
-rw-r--r--src/network/networkd-route-util.c567
-rw-r--r--src/network/networkd-route-util.h49
-rw-r--r--src/network/networkd-route.c2996
-rw-r--r--src/network/networkd-route.h130
-rw-r--r--src/network/networkd-routing-policy-rule.c1756
-rw-r--r--src/network/networkd-routing-policy-rule.h90
-rw-r--r--src/network/networkd-setlink.c1299
-rw-r--r--src/network/networkd-setlink.h29
-rw-r--r--src/network/networkd-speed-meter.c111
-rw-r--r--src/network/networkd-speed-meter.h12
-rw-r--r--src/network/networkd-sriov.c360
-rw-r--r--src/network/networkd-sriov.h17
-rw-r--r--src/network/networkd-state-file.c730
-rw-r--r--src/network/networkd-state-file.h12
-rw-r--r--src/network/networkd-sysctl.c350
-rw-r--r--src/network/networkd-sysctl.h25
-rw-r--r--src/network/networkd-util.c256
-rw-r--r--src/network/networkd-util.h168
-rw-r--r--src/network/networkd-wifi.c300
-rw-r--r--src/network/networkd-wifi.h9
-rw-r--r--src/network/networkd-wiphy.c500
-rw-r--r--src/network/networkd-wiphy.h71
-rw-r--r--src/network/networkd.c121
-rw-r--r--src/network/networkd.conf28
-rw-r--r--src/network/org.freedesktop.network1.conf27
-rw-r--r--src/network/org.freedesktop.network1.policy186
-rw-r--r--src/network/org.freedesktop.network1.service14
-rw-r--r--src/network/systemd-networkd.pkla7
-rw-r--r--src/network/systemd-networkd.rules13
-rw-r--r--src/network/tc/cake.c613
-rw-r--r--src/network/tc/cake.h79
-rw-r--r--src/network/tc/codel.c252
-rw-r--r--src/network/tc/codel.h24
-rw-r--r--src/network/tc/drr.c108
-rw-r--r--src/network/tc/drr.h23
-rw-r--r--src/network/tc/ets.c342
-rw-r--r--src/network/tc/ets.h25
-rw-r--r--src/network/tc/fifo.c183
-rw-r--r--src/network/tc/fifo.h25
-rw-r--r--src/network/tc/fq-codel.c351
-rw-r--r--src/network/tc/fq-codel.h28
-rw-r--r--src/network/tc/fq-pie.c102
-rw-r--r--src/network/tc/fq-pie.h17
-rw-r--r--src/network/tc/fq.c415
-rw-r--r--src/network/tc/fq.h29
-rw-r--r--src/network/tc/gred.c193
-rw-r--r--src/network/tc/gred.h20
-rw-r--r--src/network/tc/hhf.c97
-rw-r--r--src/network/tc/hhf.h17
-rw-r--r--src/network/tc/htb.c487
-rw-r--r--src/network/tc/htb.h39
-rw-r--r--src/network/tc/netem.c227
-rw-r--r--src/network/tc/netem.h25
-rw-r--r--src/network/tc/pie.c96
-rw-r--r--src/network/tc/pie.h17
-rw-r--r--src/network/tc/qdisc.c687
-rw-r--r--src/network/tc/qdisc.h110
-rw-r--r--src/network/tc/qfq.c177
-rw-r--r--src/network/tc/qfq.h26
-rw-r--r--src/network/tc/sfb.c107
-rw-r--r--src/network/tc/sfb.h17
-rw-r--r--src/network/tc/sfq.c91
-rw-r--r--src/network/tc/sfq.h18
-rw-r--r--src/network/tc/tbf.c343
-rw-r--r--src/network/tc/tbf.h26
-rw-r--r--src/network/tc/tc-util.c133
-rw-r--r--src/network/tc/tc-util.h14
-rw-r--r--src/network/tc/tc.c41
-rw-r--r--src/network/tc/tc.h6
-rw-r--r--src/network/tc/tclass.c605
-rw-r--r--src/network/tc/tclass.h76
-rw-r--r--src/network/tc/teql.c97
-rw-r--r--src/network/tc/teql.h16
-rw-r--r--src/network/test-network-tables.c51
-rw-r--r--src/network/test-network.c294
-rw-r--r--src/network/test-networkd-address.c25
-rw-r--r--src/network/test-networkd-conf.c251
-rw-r--r--src/network/test-networkd-util.c19
-rw-r--r--src/network/wait-online/link.c179
-rw-r--r--src/network/wait-online/link.h33
-rw-r--r--src/network/wait-online/manager.c417
-rw-r--r--src/network/wait-online/manager.h44
-rw-r--r--src/network/wait-online/wait-online.c237
222 files changed, 62633 insertions, 0 deletions
diff --git a/src/network/fuzz-netdev-parser.c b/src/network/fuzz-netdev-parser.c
new file mode 100644
index 0000000..77e87e9
--- /dev/null
+++ b/src/network/fuzz-netdev-parser.c
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fd-util.h"
+#include "fs-util.h"
+#include "fuzz.h"
+#include "networkd-manager.h"
+#include "tmpfile-util.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ _cleanup_(manager_freep) Manager *manager = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(unlink_tempfilep) char netdev_config[] = "/tmp/fuzz-networkd.XXXXXX";
+
+ if (outside_size_range(size, 0, 65536))
+ return 0;
+
+ if (!getenv("SYSTEMD_LOG_LEVEL"))
+ log_set_max_level(LOG_CRIT);
+
+ assert_se(fmkostemp_safe(netdev_config, "r+", &f) == 0);
+ if (size != 0)
+ assert_se(fwrite(data, size, 1, f) == 1);
+
+ fflush(f);
+ assert_se(manager_new(&manager, /* test_mode = */ true) >= 0);
+ (void) netdev_load_one(manager, netdev_config);
+ return 0;
+}
diff --git a/src/network/fuzz-netdev-parser.options b/src/network/fuzz-netdev-parser.options
new file mode 100644
index 0000000..678d526
--- /dev/null
+++ b/src/network/fuzz-netdev-parser.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65536
diff --git a/src/network/fuzz-network-parser.c b/src/network/fuzz-network-parser.c
new file mode 100644
index 0000000..0c9d6d2
--- /dev/null
+++ b/src/network/fuzz-network-parser.c
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fd-util.h"
+#include "fs-util.h"
+#include "fuzz.h"
+#include "networkd-manager.h"
+#include "tmpfile-util.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ _cleanup_(manager_freep) Manager *manager = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(unlink_tempfilep) char network_config[] = "/tmp/fuzz-networkd.XXXXXX";
+
+ if (outside_size_range(size, 0, 65536))
+ return 0;
+
+ if (!getenv("SYSTEMD_LOG_LEVEL"))
+ log_set_max_level(LOG_CRIT);
+
+ assert_se(fmkostemp_safe(network_config, "r+", &f) == 0);
+ if (size != 0)
+ assert_se(fwrite(data, size, 1, f) == 1);
+
+ fflush(f);
+ assert_se(manager_new(&manager, /* test_mode = */ true) >= 0);
+ (void) network_load_one(manager, &manager->networks, network_config);
+ return 0;
+}
diff --git a/src/network/fuzz-network-parser.options b/src/network/fuzz-network-parser.options
new file mode 100644
index 0000000..678d526
--- /dev/null
+++ b/src/network/fuzz-network-parser.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65536
diff --git a/src/network/generator/main.c b/src/network/generator/main.c
new file mode 100644
index 0000000..7ff977a
--- /dev/null
+++ b/src/network/generator/main.c
@@ -0,0 +1,211 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <sys/stat.h>
+
+#include "fd-util.h"
+#include "generator.h"
+#include "macro.h"
+#include "main-func.h"
+#include "mkdir.h"
+#include "network-generator.h"
+#include "path-util.h"
+#include "proc-cmdline.h"
+
+#define NETWORKD_UNIT_DIRECTORY "/run/systemd/network"
+
+static const char *arg_root = NULL;
+
+static int network_save(Network *network, const char *dest_dir) {
+ _cleanup_free_ char *filename = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(network);
+
+ r = asprintf(&filename, "%s-%s.network",
+ isempty(network->ifname) ? "91" : "90",
+ isempty(network->ifname) ? "default" : network->ifname);
+ if (r < 0)
+ return log_oom();
+
+ r = generator_open_unit_file(dest_dir, "kernel command line", filename, &f);
+ if (r < 0)
+ return r;
+
+ network_dump(network, f);
+
+ return 0;
+}
+
+static int netdev_save(NetDev *netdev, const char *dest_dir) {
+ _cleanup_free_ char *filename = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(netdev);
+
+ r = asprintf(&filename, "90-%s.netdev",
+ netdev->ifname);
+ if (r < 0)
+ return log_oom();
+
+ r = generator_open_unit_file(dest_dir, "kernel command line", filename, &f);
+ if (r < 0)
+ return r;
+
+ netdev_dump(netdev, f);
+
+ return 0;
+}
+
+static int link_save(Link *link, const char *dest_dir) {
+ _cleanup_free_ char *filename = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(link);
+
+ filename = strjoin(!isempty(link->ifname) ? "90" :
+ !hw_addr_is_null(&link->mac) ? "91" : "92",
+ "-", link->filename, ".link");
+ if (!filename)
+ return log_oom();
+
+ r = generator_open_unit_file(dest_dir, "kernel command line", filename, &f);
+ if (r < 0)
+ return r;
+
+ link_dump(link, f);
+
+ return 0;
+}
+
+static int context_save(Context *context) {
+ Network *network;
+ NetDev *netdev;
+ Link *link;
+ int k, r;
+ const char *p;
+
+ p = prefix_roota(arg_root, NETWORKD_UNIT_DIRECTORY);
+
+ r = mkdir_p(p, 0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create directory " NETWORKD_UNIT_DIRECTORY ": %m");
+
+ HASHMAP_FOREACH(network, context->networks_by_name) {
+ k = network_save(network, p);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ HASHMAP_FOREACH(netdev, context->netdevs_by_name) {
+ k = netdev_save(netdev, p);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ HASHMAP_FOREACH(link, context->links_by_filename) {
+ k = link_save(link, p);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int help(void) {
+ printf("%s [OPTIONS...] [-- KERNEL_CMDLINE]\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --root=PATH Operate on an alternate filesystem root\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_ROOT,
+ };
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "root", required_argument, NULL, ARG_ROOT },
+ {},
+ };
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help();
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_ROOT:
+ arg_root = optarg;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(context_clear) Context context = {};
+ int r;
+
+ log_setup();
+
+ umask(0022);
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (optind >= argc) {
+ r = proc_cmdline_parse(parse_cmdline_item, &context, 0);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to parse kernel command line: %m");
+ } else {
+ for (int i = optind; i < argc; i++) {
+ _cleanup_free_ char *word = NULL;
+ char *value;
+
+ word = strdup(argv[i]);
+ if (!word)
+ return log_oom();
+
+ value = strchr(word, '=');
+ if (value)
+ *(value++) = 0;
+
+ r = parse_cmdline_item(word, value, &context);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to parse command line \"%s%s%s\": %m",
+ word, value ? "=" : "", strempty(value));
+ }
+ }
+
+ r = context_merge_networks(&context);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to merge multiple command line options: %m");
+
+ return context_save(&context);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/network/generator/network-generator.c b/src/network/generator/network-generator.c
new file mode 100644
index 0000000..3e1ce5a
--- /dev/null
+++ b/src/network/generator/network-generator.c
@@ -0,0 +1,1444 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "hostname-util.h"
+#include "log.h"
+#include "macro.h"
+#include "netif-naming-scheme.h"
+#include "network-generator.h"
+#include "parse-util.h"
+#include "proc-cmdline.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+/*
+ # .network
+ ip={dhcp|on|any|dhcp6|auto6|either6|link6}
+ ip=<interface>:{dhcp|on|any|dhcp6|auto6|link6}[:[<mtu>][:<macaddr>]]
+ ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|link6|ibft}[:[<mtu>][:<macaddr>]]
+ ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|link6|ibft}[:[<dns1>][:<dns2>]]
+ rd.route=<net>/<netmask>:<gateway>[:<interface>]
+ nameserver=<IP> [nameserver=<IP> ...]
+ rd.peerdns=0
+
+ # .link
+ ifname=<interface>:<MAC>
+ net.ifname-policy=policy1[,policy2,...][,<MAC>] # This is an original rule, not supported by other tools.
+
+ # .netdev
+ vlan=<vlanname>:<phydevice>
+ bond=<bondname>[:<bondslaves>:[:<options>[:<mtu>]]]
+ team=<teammaster>:<teamslaves> # not supported
+ bridge=<bridgename>:<ethnames>
+
+ # ignored
+ bootdev=<interface>
+ BOOTIF=<MAC>
+ rd.bootif=0
+ biosdevname=0
+ rd.neednet=1
+*/
+
+static const char * const dracut_dhcp_type_table[_DHCP_TYPE_MAX] = {
+ [DHCP_TYPE_NONE] = "none",
+ [DHCP_TYPE_OFF] = "off",
+ [DHCP_TYPE_ON] = "on",
+ [DHCP_TYPE_ANY] = "any",
+ [DHCP_TYPE_DHCP4] = "dhcp",
+ [DHCP_TYPE_DHCP6] = "dhcp6",
+ [DHCP_TYPE_AUTO6] = "auto6",
+ [DHCP_TYPE_EITHER6] = "either6",
+ [DHCP_TYPE_IBFT] = "ibft",
+ [DHCP_TYPE_LINK6] = "link6",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(dracut_dhcp_type, DHCPType);
+
+static const char * const networkd_dhcp_type_table[_DHCP_TYPE_MAX] = {
+ [DHCP_TYPE_NONE] = "no",
+ [DHCP_TYPE_OFF] = "no",
+ [DHCP_TYPE_ON] = "yes",
+ [DHCP_TYPE_ANY] = "yes",
+ [DHCP_TYPE_DHCP4] = "ipv4",
+ [DHCP_TYPE_DHCP6] = "ipv6",
+ [DHCP_TYPE_AUTO6] = "no", /* TODO: enable other setting? */
+ [DHCP_TYPE_EITHER6] = "ipv6", /* TODO: enable other setting? */
+ [DHCP_TYPE_IBFT] = "no",
+ [DHCP_TYPE_LINK6] = "no",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(networkd_dhcp_type, DHCPType);
+
+static Address *address_free(Address *address) {
+ if (!address)
+ return NULL;
+
+ if (address->network)
+ LIST_REMOVE(addresses, address->network->addresses, address);
+
+ return mfree(address);
+}
+
+static int address_new(Network *network, int family, unsigned char prefixlen,
+ union in_addr_union *addr, union in_addr_union *peer, Address **ret) {
+ Address *address;
+
+ assert(network);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(addr);
+
+ address = new(Address, 1);
+ if (!address)
+ return -ENOMEM;
+
+ *address = (Address) {
+ .family = family,
+ .prefixlen = prefixlen,
+ .address = *addr,
+ .peer = peer ? *peer : IN_ADDR_NULL,
+ };
+
+ LIST_PREPEND(addresses, network->addresses, address);
+
+ address->network = network;
+
+ if (ret)
+ *ret = address;
+ return 0;
+}
+
+static Route *route_free(Route *route) {
+ if (!route)
+ return NULL;
+
+ if (route->network)
+ LIST_REMOVE(routes, route->network->routes, route);
+
+ return mfree(route);
+}
+
+static int route_new(Network *network, int family, unsigned char prefixlen,
+ union in_addr_union *dest, union in_addr_union *gateway, Route **ret) {
+ Route *route;
+
+ assert(network);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(dest || gateway);
+
+ route = new(Route, 1);
+ if (!route)
+ return -ENOMEM;
+
+ *route = (Route) {
+ .family = family,
+ .prefixlen = prefixlen,
+ .dest = dest ? *dest : IN_ADDR_NULL,
+ .gateway = gateway ? *gateway : IN_ADDR_NULL,
+ };
+
+ LIST_PREPEND(routes, network->routes, route);
+
+ route->network = network;
+
+ if (ret)
+ *ret = route;
+ return 0;
+}
+
+static Network *network_free(Network *network) {
+ Address *address;
+ Route *route;
+
+ if (!network)
+ return NULL;
+
+ free(network->ifname);
+ free(network->hostname);
+ strv_free(network->dns);
+ free(network->vlan);
+ free(network->bridge);
+ free(network->bond);
+
+ while ((address = network->addresses))
+ address_free(address);
+
+ while ((route = network->routes))
+ route_free(route);
+
+ return mfree(network);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Network*, network_free);
+
+static int network_new(Context *context, const char *name, Network **ret) {
+ _cleanup_(network_freep) Network *network = NULL;
+ _cleanup_free_ char *ifname = NULL;
+ int r;
+
+ assert(context);
+
+ if (!isempty(name) && !ifname_valid(name))
+ return -EINVAL;
+
+ ifname = strdup(name);
+ if (!ifname)
+ return -ENOMEM;
+
+ network = new(Network, 1);
+ if (!network)
+ return -ENOMEM;
+
+ *network = (Network) {
+ .ifname = TAKE_PTR(ifname),
+ .dhcp_type = _DHCP_TYPE_INVALID,
+ .dhcp_use_dns = -1,
+ };
+
+ r = hashmap_ensure_put(&context->networks_by_name, &string_hash_ops, network->ifname, network);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = network;
+
+ TAKE_PTR(network);
+ return 0;
+}
+
+Network *network_get(Context *context, const char *ifname) {
+ return hashmap_get(context->networks_by_name, ifname);
+}
+
+static NetDev *netdev_free(NetDev *netdev) {
+ if (!netdev)
+ return NULL;
+
+ free(netdev->ifname);
+ free(netdev->kind);
+ return mfree(netdev);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(NetDev*, netdev_free);
+
+static int netdev_new(Context *context, const char *_kind, const char *_ifname, NetDev **ret) {
+ _cleanup_(netdev_freep) NetDev *netdev = NULL;
+ _cleanup_free_ char *kind = NULL, *ifname = NULL;
+ int r;
+
+ assert(context);
+ assert(_kind);
+
+ if (!ifname_valid(_ifname))
+ return -EINVAL;
+
+ kind = strdup(_kind);
+ if (!kind)
+ return -ENOMEM;
+
+ ifname = strdup(_ifname);
+ if (!ifname)
+ return -ENOMEM;
+
+ netdev = new(NetDev, 1);
+ if (!netdev)
+ return -ENOMEM;
+
+ *netdev = (NetDev) {
+ .kind = TAKE_PTR(kind),
+ .ifname = TAKE_PTR(ifname),
+ };
+
+ r = hashmap_ensure_put(&context->netdevs_by_name, &string_hash_ops, netdev->ifname, netdev);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = netdev;
+
+ TAKE_PTR(netdev);
+ return 0;
+}
+
+NetDev *netdev_get(Context *context, const char *ifname) {
+ return hashmap_get(context->netdevs_by_name, ifname);
+}
+
+static Link *link_free(Link *link) {
+ if (!link)
+ return NULL;
+
+ free(link->filename);
+ free(link->ifname);
+ strv_free(link->policies);
+ strv_free(link->alt_policies);
+ return mfree(link);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free);
+
+static int link_new(
+ Context *context,
+ const char *name,
+ const struct hw_addr_data *mac,
+ Link **ret) {
+
+ _cleanup_(link_freep) Link *link = NULL;
+ _cleanup_free_ char *ifname = NULL, *filename = NULL;
+ int r;
+
+ assert(context);
+ assert(mac);
+
+ if (name) {
+ if (!ifname_valid(name))
+ return -EINVAL;
+
+ ifname = strdup(name);
+ if (!ifname)
+ return -ENOMEM;
+
+ filename = strdup(name);
+ if (!filename)
+ return -ENOMEM;
+ }
+
+ if (!filename) {
+ filename = strdup(hw_addr_is_null(mac) ? "default" :
+ HW_ADDR_TO_STR_FULL(mac, HW_ADDR_TO_STRING_NO_COLON));
+ if (!filename)
+ return -ENOMEM;
+ }
+
+ link = new(Link, 1);
+ if (!link)
+ return -ENOMEM;
+
+ *link = (Link) {
+ .filename = TAKE_PTR(filename),
+ .ifname = TAKE_PTR(ifname),
+ .mac = *mac,
+ };
+
+ r = hashmap_ensure_put(&context->links_by_filename, &string_hash_ops, link->filename, link);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = link;
+
+ TAKE_PTR(link);
+ return 0;
+}
+
+Link *link_get(Context *context, const char *filename) {
+ assert(context);
+ assert(filename);
+ return hashmap_get(context->links_by_filename, filename);
+}
+
+static int network_set_dhcp_type(Context *context, const char *ifname, const char *dhcp_type) {
+ Network *network;
+ DHCPType t;
+ int r;
+
+ assert(context);
+ assert(ifname);
+ assert(dhcp_type);
+
+ t = dracut_dhcp_type_from_string(dhcp_type);
+ if (t < 0)
+ return t;
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ network->dhcp_type = t;
+ return 0;
+}
+
+static int network_set_hostname(Context *context, const char *ifname, const char *hostname) {
+ Network *network;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return free_and_strdup(&network->hostname, hostname);
+}
+
+static int network_set_mtu(Context *context, const char *ifname, const char *mtu) {
+ Network *network;
+
+ assert(context);
+ assert(ifname);
+
+ if (isempty(mtu))
+ return 0;
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return parse_mtu(AF_UNSPEC, mtu, &network->mtu);
+}
+
+static int network_set_mac_address(Context *context, const char *ifname, const char *mac) {
+ Network *network;
+
+ assert(context);
+ assert(ifname);
+ assert(mac);
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return parse_ether_addr(mac, &network->mac);
+}
+
+static int network_set_address(Context *context, const char *ifname, int family, unsigned char prefixlen,
+ union in_addr_union *addr, union in_addr_union *peer) {
+ Network *network;
+
+ assert(context);
+ assert(ifname);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(addr);
+
+ if (!in_addr_is_set(family, addr))
+ return 0;
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return address_new(network, family, prefixlen, addr, peer, NULL);
+}
+
+static int network_set_route(Context *context, const char *ifname, int family, unsigned char prefixlen,
+ union in_addr_union *dest, union in_addr_union *gateway) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+
+ if (!(dest && in_addr_is_set(family, dest)) &&
+ !(gateway && in_addr_is_set(family, gateway)))
+ return 0;
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return route_new(network, family, prefixlen, dest, gateway, NULL);
+}
+
+static int network_set_dns(Context *context, const char *ifname, int family, const char *dns) {
+ union in_addr_union a;
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+ assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(dns);
+
+ if (family == AF_UNSPEC)
+ r = in_addr_from_string_auto(dns, &family, &a);
+ else
+ r = in_addr_from_string(family, dns, &a);
+ if (r < 0)
+ return r;
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return strv_extend(&network->dns, dns);
+}
+
+static int network_set_dhcp_use_dns(Context *context, const char *ifname, bool value) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ network->dhcp_use_dns = value;
+
+ return 0;
+}
+
+static int network_set_vlan(Context *context, const char *ifname, const char *value) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return free_and_strdup(&network->vlan, value);
+}
+
+static int network_set_bridge(Context *context, const char *ifname, const char *value) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return free_and_strdup(&network->bridge, value);
+}
+
+static int network_set_bond(Context *context, const char *ifname, const char *value) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return free_and_strdup(&network->bond, value);
+}
+
+static int parse_cmdline_ip_mtu_mac(Context *context, const char *ifname, const char *value) {
+ const char *mtu, *p;
+ int r;
+
+ assert(context);
+ assert(ifname);
+ assert(value);
+
+ /* [<mtu>][:<macaddr>] */
+
+ p = strchr(value, ':');
+ if (!p)
+ mtu = value;
+ else
+ mtu = strndupa_safe(value, p - value);
+
+ r = network_set_mtu(context, ifname, mtu);
+ if (r < 0)
+ return r;
+
+ if (!p || isempty(p + 1))
+ return 0;
+
+ r = network_set_mac_address(context, ifname, p + 1);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int parse_ip_address_one(int family, const char **value, union in_addr_union *ret) {
+ const char *p, *q, *buf;
+ int r;
+
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(value);
+ assert(ret);
+
+ p = ASSERT_PTR(*value);
+
+ if (p[0] == ':') {
+ *value = p + 1;
+ return 0;
+ }
+
+ if (family == AF_INET6) {
+ if (p[0] != '[')
+ return -EINVAL;
+
+ q = strchr(p + 1, ']');
+ if (!q)
+ return -EINVAL;
+
+ if (q[1] != ':')
+ return -EINVAL;
+
+ buf = strndupa_safe(p + 1, q - p - 1);
+ p = q + 2;
+ } else {
+ q = strchr(p, ':');
+ if (!q)
+ return -EINVAL;
+
+ buf = strndupa_safe(p, q - p);
+ p = q + 1;
+ }
+
+ r = in_addr_from_string(family, buf, ret);
+ if (r < 0)
+ return r;
+
+ *value = p;
+ return 1;
+}
+
+static int parse_netmask_or_prefixlen(int family, const char **value, unsigned char *ret) {
+ union in_addr_union netmask;
+ const char *p, *q;
+ int r;
+
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(value);
+ assert(*value);
+ assert(ret);
+
+ r = parse_ip_address_one(family, value, &netmask);
+ if (r > 0) {
+ if (family == AF_INET6)
+ /* TODO: Not supported yet. */
+ return -EINVAL;
+
+ *ret = in4_addr_netmask_to_prefixlen(&netmask.in);
+ } else if (r == 0)
+ *ret = family == AF_INET6 ? 128 : 32;
+ else {
+ p = strchr(*value, ':');
+ if (!p)
+ return -EINVAL;
+
+ q = strndupa_safe(*value, p - *value);
+ r = safe_atou8(q, ret);
+ if (r < 0)
+ return r;
+
+ *value = p + 1;
+ }
+
+ return 0;
+}
+
+static int parse_ip_dns_address_one(Context *context, const char *ifname, const char **value) {
+ const char *p, *q, *buf;
+ int r, family;
+
+ assert(context);
+ assert(ifname);
+ assert(value);
+
+ p = ASSERT_PTR(*value);
+
+ if (isempty(p))
+ return 0;
+
+ if (p[0] == '[') {
+ q = strchr(p + 1, ']');
+ if (!q)
+ return -EINVAL;
+ if (!IN_SET(q[1], ':', '\0'))
+ return -EINVAL;
+
+ buf = strndupa_safe(p + 1, q - p - 1);
+ p = q + 1;
+ family = AF_INET6;
+ } else {
+ q = strchr(p, ':');
+ if (!q)
+ buf = *value;
+ else
+ buf = strndupa_safe(*value, q - *value);
+
+ p += strlen(buf);
+ family = AF_INET;
+ }
+
+ r = network_set_dns(context, ifname, family, buf);
+ if (r < 0)
+ return r;
+
+ *value = p;
+ return 0;
+}
+
+static int parse_cmdline_ip_address(Context *context, int family, const char *value) {
+ union in_addr_union addr = {}, peer = {}, gateway = {};
+ const char *hostname = NULL, *ifname, *dhcp_type, *p;
+ unsigned char prefixlen;
+ int r;
+
+ assert(context);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(value);
+
+ /* ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft|link6}[:[<mtu>][:<macaddr>]]
+ * ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft|link6}[:[<dns1>][:<dns2>]] */
+
+ r = parse_ip_address_one(family, &value, &addr);
+ if (r < 0)
+ return r;
+ r = parse_ip_address_one(family, &value, &peer);
+ if (r < 0)
+ return r;
+ r = parse_ip_address_one(family, &value, &gateway);
+ if (r < 0)
+ return r;
+ r = parse_netmask_or_prefixlen(family, &value, &prefixlen);
+ if (r < 0)
+ return r;
+
+ /* hostname */
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ if (p != value) {
+ hostname = strndupa_safe(value, p - value);
+ if (!hostname_is_valid(hostname, 0))
+ return -EINVAL;
+ }
+
+ value = p + 1;
+
+ /* ifname */
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ ifname = strndupa_safe(value, p - value);
+
+ value = p + 1;
+
+ /* dhcp_type */
+ p = strchr(value, ':');
+ if (!p)
+ dhcp_type = value;
+ else
+ dhcp_type = strndupa_safe(value, p - value);
+
+ r = network_set_dhcp_type(context, ifname, dhcp_type);
+ if (r < 0)
+ return r;
+
+ /* set values */
+ r = network_set_hostname(context, ifname, hostname);
+ if (r < 0)
+ return r;
+
+ r = network_set_address(context, ifname, family, prefixlen, &addr, &peer);
+ if (r < 0)
+ return r;
+
+ r = network_set_route(context, ifname, family, 0, NULL, &gateway);
+ if (r < 0)
+ return r;
+
+ if (!p)
+ return 0;
+
+ /* First, try [<mtu>][:<macaddr>] */
+ r = parse_cmdline_ip_mtu_mac(context, ifname, p + 1);
+ if (r >= 0)
+ return 0;
+
+ /* Next, try [<dns1>][:<dns2>] */
+ value = p + 1;
+ r = parse_ip_dns_address_one(context, ifname, &value);
+ if (r < 0)
+ return r;
+
+ value += *value == ':';
+ r = parse_ip_dns_address_one(context, ifname, &value);
+ if (r < 0)
+ return r;
+
+ /* refuse unexpected trailing strings */
+ if (!isempty(value))
+ return -EINVAL;
+
+ return 0;
+}
+
+static int parse_cmdline_ip_interface(Context *context, const char *value) {
+ const char *ifname, *dhcp_type, *p;
+ int r;
+
+ assert(context);
+ assert(value);
+
+ /* ip=<interface>:{dhcp|on|any|dhcp6|auto6|link6}[:[<mtu>][:<macaddr>]] */
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ ifname = strndupa_safe(value, p - value);
+
+ value = p + 1;
+ p = strchr(value, ':');
+ if (!p)
+ dhcp_type = value;
+ else
+ dhcp_type = strndupa_safe(value, p - value);
+
+ r = network_set_dhcp_type(context, ifname, dhcp_type);
+ if (r < 0)
+ return r;
+
+ if (!p)
+ return 0;
+
+ return parse_cmdline_ip_mtu_mac(context, ifname, p + 1);
+}
+
+static int parse_cmdline_ip(Context *context, const char *key, const char *value) {
+ const char *p;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ /* ip={dhcp|on|any|dhcp6|auto6|either6|link6} */
+ return network_set_dhcp_type(context, "", value);
+
+ if (value[0] == '[')
+ return parse_cmdline_ip_address(context, AF_INET6, value);
+
+ r = parse_cmdline_ip_address(context, AF_INET, value);
+ if (r < 0)
+ return parse_cmdline_ip_interface(context, value);
+
+ return 0;
+}
+
+static int parse_cmdline_rd_route(Context *context, const char *key, const char *value) {
+ union in_addr_union addr = {}, gateway = {};
+ unsigned char prefixlen;
+ const char *buf, *p;
+ int family, r;
+
+ assert(context);
+ assert(key);
+
+ /* rd.route=<net>/<netmask>:<gateway>[:<interface>] */
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ if (value[0] == '[') {
+ p = strchr(value, ']');
+ if (!p)
+ return -EINVAL;
+
+ if (p[1] != ':')
+ return -EINVAL;
+
+ buf = strndupa_safe(value + 1, p - value - 1);
+ value = p + 2;
+ family = AF_INET6;
+ } else {
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ buf = strndupa_safe(value, p - value);
+ value = p + 1;
+ family = AF_INET;
+ }
+
+ r = in_addr_prefix_from_string(buf, family, &addr, &prefixlen);
+ if (r < 0)
+ return r;
+
+ p = strchr(value, ':');
+ if (!p)
+ value = strjoina(value, ":");
+
+ r = parse_ip_address_one(family, &value, &gateway);
+ if (r < 0)
+ return r;
+
+ return network_set_route(context, value, family, prefixlen, &addr, &gateway);
+}
+
+static int parse_cmdline_nameserver(Context *context, const char *key, const char *value) {
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ return network_set_dns(context, "", AF_UNSPEC, value);
+}
+
+static int parse_cmdline_rd_peerdns(Context *context, const char *key, const char *value) {
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return network_set_dhcp_use_dns(context, "", true);
+
+ r = parse_boolean(value);
+ if (r < 0)
+ return r;
+
+ return network_set_dhcp_use_dns(context, "", r);
+}
+
+static int parse_cmdline_vlan(Context *context, const char *key, const char *value) {
+ const char *name, *p;
+ NetDev *netdev;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa_safe(value, p - value);
+
+ netdev = netdev_get(context, name);
+ if (!netdev) {
+ r = netdev_new(context, "vlan", name, &netdev);
+ if (r < 0)
+ return r;
+ }
+
+ return network_set_vlan(context, p + 1, name);
+}
+
+static int parse_cmdline_bridge(Context *context, const char *key, const char *value) {
+ const char *name, *p;
+ NetDev *netdev;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa_safe(value, p - value);
+
+ netdev = netdev_get(context, name);
+ if (!netdev) {
+ r = netdev_new(context, "bridge", name, &netdev);
+ if (r < 0)
+ return r;
+ }
+
+ p++;
+ if (isempty(p))
+ return -EINVAL;
+
+ for (;;) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&p, &word, ",", 0);
+ if (r <= 0)
+ return r;
+
+ r = network_set_bridge(context, word, name);
+ if (r < 0)
+ return r;
+ }
+}
+
+static int parse_cmdline_bond(Context *context, const char *key, const char *value) {
+ const char *name, *slaves, *p;
+ NetDev *netdev;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa_safe(value, p - value);
+
+ netdev = netdev_get(context, name);
+ if (!netdev) {
+ r = netdev_new(context, "bond", name, &netdev);
+ if (r < 0)
+ return r;
+ }
+
+ value = p + 1;
+ p = strchr(value, ':');
+ if (!p)
+ slaves = value;
+ else
+ slaves = strndupa_safe(value, p - value);
+
+ if (isempty(slaves))
+ return -EINVAL;
+
+ for (const char *q = slaves; ; ) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&q, &word, ",", 0);
+ if (r == 0)
+ break;
+ if (r < 0)
+ return r;
+
+ r = network_set_bond(context, word, name);
+ if (r < 0)
+ return r;
+ }
+
+ if (!p)
+ return 0;
+
+ value = p + 1;
+ p = strchr(value, ':');
+ if (!p)
+ /* TODO: set bonding options */
+ return 0;
+
+ return parse_mtu(AF_UNSPEC, p + 1, &netdev->mtu);
+}
+
+static int parse_cmdline_ifname(Context *context, const char *key, const char *value) {
+ struct hw_addr_data mac;
+ const char *name, *p;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ /* ifname=<interface>:<MAC> */
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa_safe(value, p - value);
+
+ r = parse_hw_addr(p + 1, &mac);
+ if (r < 0)
+ return r;
+
+ return link_new(context, name, &mac, NULL);
+}
+
+static int parse_cmdline_ifname_policy(Context *context, const char *key, const char *value) {
+ _cleanup_strv_free_ char **policies = NULL, **alt_policies = NULL;
+ struct hw_addr_data mac = HW_ADDR_NULL;
+ Link *link;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ /* net.ifname-policy=policy1[,policy2,...][,<MAC>] */
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ for (const char *q = value; ; ) {
+ _cleanup_free_ char *word = NULL;
+ NamePolicy p;
+
+ r = extract_first_word(&q, &word, ",", 0);
+ if (r == 0)
+ break;
+ if (r < 0)
+ return r;
+
+ p = name_policy_from_string(word);
+ if (p < 0) {
+ r = parse_hw_addr(word, &mac);
+ if (r < 0)
+ return r;
+
+ if (hw_addr_is_null(&mac))
+ return -EINVAL;
+
+ if (!isempty(q))
+ return -EINVAL;
+
+ break;
+ }
+
+ if (alternative_names_policy_from_string(word) >= 0) {
+ r = strv_extend(&alt_policies, word);
+ if (r < 0)
+ return r;
+ }
+
+ r = strv_consume(&policies, TAKE_PTR(word));
+ if (r < 0)
+ return r;
+ }
+
+ if (strv_isempty(policies))
+ return -EINVAL;
+
+ r = link_new(context, NULL, &mac, &link);
+ if (r < 0)
+ return r;
+
+ link->policies = TAKE_PTR(policies);
+ link->alt_policies = TAKE_PTR(alt_policies);
+ return 0;
+}
+
+int parse_cmdline_item(const char *key, const char *value, void *data) {
+ Context *context = ASSERT_PTR(data);
+
+ assert(key);
+
+ if (streq(key, "ip"))
+ return parse_cmdline_ip(context, key, value);
+ if (streq(key, "rd.route"))
+ return parse_cmdline_rd_route(context, key, value);
+ if (streq(key, "nameserver"))
+ return parse_cmdline_nameserver(context, key, value);
+ if (streq(key, "rd.peerdns"))
+ return parse_cmdline_rd_peerdns(context, key, value);
+ if (streq(key, "vlan"))
+ return parse_cmdline_vlan(context, key, value);
+ if (streq(key, "bridge"))
+ return parse_cmdline_bridge(context, key, value);
+ if (streq(key, "bond"))
+ return parse_cmdline_bond(context, key, value);
+ if (streq(key, "ifname"))
+ return parse_cmdline_ifname(context, key, value);
+ if (streq(key, "net.ifname-policy"))
+ return parse_cmdline_ifname_policy(context, key, value);
+
+ return 0;
+}
+
+int context_merge_networks(Context *context) {
+ Network *all, *network;
+ int r;
+
+ assert(context);
+
+ /* Copy settings about the following options
+ rd.route=<net>/<netmask>:<gateway>[:<interface>]
+ nameserver=<IP> [nameserver=<IP> ...]
+ rd.peerdns=0 */
+
+ all = network_get(context, "");
+ if (!all)
+ return 0;
+
+ if (hashmap_size(context->networks_by_name) <= 1)
+ return 0;
+
+ HASHMAP_FOREACH(network, context->networks_by_name) {
+ if (network == all)
+ continue;
+
+ network->dhcp_use_dns = all->dhcp_use_dns;
+
+ r = strv_extend_strv(&network->dns, all->dns, false);
+ if (r < 0)
+ return r;
+
+ LIST_FOREACH(routes, route, all->routes) {
+ r = route_new(network, route->family, route->prefixlen, &route->dest, &route->gateway, NULL);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ assert_se(hashmap_remove(context->networks_by_name, "") == all);
+ network_free(all);
+ return 0;
+}
+
+void context_clear(Context *context) {
+ if (!context)
+ return;
+
+ hashmap_free_with_destructor(context->networks_by_name, network_free);
+ hashmap_free_with_destructor(context->netdevs_by_name, netdev_free);
+ hashmap_free_with_destructor(context->links_by_filename, link_free);
+}
+
+static int address_dump(Address *address, FILE *f) {
+ assert(address);
+ assert(f);
+
+ fprintf(f,
+ "\n[Address]\n"
+ "Address=%s\n",
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->address, address->prefixlen));
+ if (in_addr_is_set(address->family, &address->peer))
+ fprintf(f, "Peer=%s\n",
+ IN_ADDR_TO_STRING(address->family, &address->peer));
+ return 0;
+}
+
+static int route_dump(Route *route, FILE *f) {
+ assert(route);
+ assert(f);
+
+ fputs("\n[Route]\n", f);
+ if (in_addr_is_set(route->family, &route->dest))
+ fprintf(f, "Destination=%s\n",
+ IN_ADDR_PREFIX_TO_STRING(route->family, &route->dest, route->prefixlen));
+ if (in_addr_is_set(route->family, &route->gateway))
+ fprintf(f, "Gateway=%s\n",
+ IN_ADDR_TO_STRING(route->family, &route->gateway));
+
+ return 0;
+}
+
+void network_dump(Network *network, FILE *f) {
+ const char *dhcp;
+
+ assert(network);
+ assert(f);
+
+ fputs("[Match]\n", f);
+
+ if (isempty(network->ifname))
+ /* If the interface name is not specified, then let's make the .network file match the all
+ * physical interfaces. */
+ fputs("Kind=!*\n"
+ "Type=!loopback\n", f);
+ else
+ fprintf(f, "Name=%s\n", network->ifname);
+
+ fputs("\n[Link]\n", f);
+
+ if (!ether_addr_is_null(&network->mac))
+ fprintf(f, "MACAddress=%s\n", ETHER_ADDR_TO_STR(&network->mac));
+ if (network->mtu > 0)
+ fprintf(f, "MTUBytes=%" PRIu32 "\n", network->mtu);
+
+ fputs("\n[Network]\n", f);
+
+ dhcp = networkd_dhcp_type_to_string(network->dhcp_type);
+ if (dhcp)
+ fprintf(f, "DHCP=%s\n", dhcp);
+
+ if (!strv_isempty(network->dns))
+ STRV_FOREACH(dns, network->dns)
+ fprintf(f, "DNS=%s\n", *dns);
+
+ if (network->vlan)
+ fprintf(f, "VLAN=%s\n", network->vlan);
+
+ if (network->bridge)
+ fprintf(f, "Bridge=%s\n", network->bridge);
+
+ if (network->bond)
+ fprintf(f, "Bond=%s\n", network->bond);
+
+ fputs("\n[DHCP]\n", f);
+
+ if (!isempty(network->hostname))
+ fprintf(f, "Hostname=%s\n", network->hostname);
+
+ if (network->dhcp_use_dns >= 0)
+ fprintf(f, "UseDNS=%s\n", yes_no(network->dhcp_use_dns));
+
+ LIST_FOREACH(addresses, address, network->addresses)
+ (void) address_dump(address, f);
+
+ LIST_FOREACH(routes, route, network->routes)
+ (void) route_dump(route, f);
+}
+
+void netdev_dump(NetDev *netdev, FILE *f) {
+ assert(netdev);
+ assert(f);
+
+ fprintf(f,
+ "[NetDev]\n"
+ "Kind=%s\n"
+ "Name=%s\n",
+ netdev->kind,
+ netdev->ifname);
+
+ if (netdev->mtu > 0)
+ fprintf(f, "MTUBytes=%" PRIu32 "\n", netdev->mtu);
+}
+
+void link_dump(Link *link, FILE *f) {
+ assert(link);
+ assert(f);
+
+ fputs("[Match]\n", f);
+
+ if (!hw_addr_is_null(&link->mac))
+ fprintf(f, "MACAddress=%s\n", HW_ADDR_TO_STR(&link->mac));
+ else
+ fputs("OriginalName=*\n", f);
+
+ fputs("\n[Link]\n", f);
+
+ if (!isempty(link->ifname))
+ fprintf(f, "Name=%s\n", link->ifname);
+
+ if (!strv_isempty(link->policies)) {
+ fputs("NamePolicy=", f);
+ fputstrv(f, link->policies, " ", NULL);
+ fputc('\n', f);
+ }
+
+ if (!strv_isempty(link->alt_policies)) {
+ fputs("AlternativeNamesPolicy=", f);
+ fputstrv(f, link->alt_policies, " ", NULL);
+ fputc('\n', f);
+ }
+}
+
+int network_format(Network *network, char **ret) {
+ _cleanup_free_ char *s = NULL;
+ size_t sz = 0;
+ int r;
+
+ assert(network);
+ assert(ret);
+
+ {
+ _cleanup_fclose_ FILE *f = NULL;
+
+ f = open_memstream_unlocked(&s, &sz);
+ if (!f)
+ return -ENOMEM;
+
+ network_dump(network, f);
+
+ /* Add terminating 0, so that the output buffer is a valid string. */
+ fputc('\0', f);
+
+ r = fflush_and_check(f);
+ }
+ if (r < 0)
+ return r;
+
+ assert(s);
+ *ret = TAKE_PTR(s);
+ assert(sz > 0);
+ return (int) sz - 1;
+}
+
+int netdev_format(NetDev *netdev, char **ret) {
+ _cleanup_free_ char *s = NULL;
+ size_t sz = 0;
+ int r;
+
+ assert(netdev);
+ assert(ret);
+
+ {
+ _cleanup_fclose_ FILE *f = NULL;
+
+ f = open_memstream_unlocked(&s, &sz);
+ if (!f)
+ return -ENOMEM;
+
+ netdev_dump(netdev, f);
+
+ /* Add terminating 0, so that the output buffer is a valid string. */
+ fputc('\0', f);
+
+ r = fflush_and_check(f);
+ }
+ if (r < 0)
+ return r;
+
+ assert(s);
+ *ret = TAKE_PTR(s);
+ assert(sz > 0);
+ return (int) sz - 1;
+}
+
+int link_format(Link *link, char **ret) {
+ _cleanup_free_ char *s = NULL;
+ size_t sz = 0;
+ int r;
+
+ assert(link);
+ assert(ret);
+
+ {
+ _cleanup_fclose_ FILE *f = NULL;
+
+ f = open_memstream_unlocked(&s, &sz);
+ if (!f)
+ return -ENOMEM;
+
+ link_dump(link, f);
+
+ /* Add terminating 0, so that the output buffer is a valid string. */
+ fputc('\0', f);
+
+ r = fflush_and_check(f);
+ }
+ if (r < 0)
+ return r;
+
+ assert(s);
+ *ret = TAKE_PTR(s);
+ assert(sz > 0);
+ return (int) sz - 1;
+}
diff --git a/src/network/generator/network-generator.h b/src/network/generator/network-generator.h
new file mode 100644
index 0000000..0e0da2a
--- /dev/null
+++ b/src/network/generator/network-generator.h
@@ -0,0 +1,115 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdio.h>
+
+#include "ether-addr-util.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "list.h"
+
+typedef enum DHCPType {
+ DHCP_TYPE_NONE,
+ DHCP_TYPE_OFF,
+ DHCP_TYPE_ON,
+ DHCP_TYPE_ANY,
+ DHCP_TYPE_DHCP4,
+ DHCP_TYPE_DHCP6,
+ DHCP_TYPE_AUTO6,
+ DHCP_TYPE_EITHER6,
+ DHCP_TYPE_IBFT,
+ DHCP_TYPE_LINK6,
+ _DHCP_TYPE_MAX,
+ _DHCP_TYPE_INVALID = -EINVAL,
+} DHCPType;
+
+typedef struct Address Address;
+typedef struct Link Link;
+typedef struct NetDev NetDev;
+typedef struct Network Network;
+typedef struct Route Route;
+typedef struct Context Context;
+
+struct Address {
+ Network *network;
+
+ union in_addr_union address, peer;
+ unsigned char prefixlen;
+ int family;
+
+ LIST_FIELDS(Address, addresses);
+};
+
+struct Route {
+ Network *network;
+
+ union in_addr_union dest, gateway;
+ unsigned char prefixlen;
+ int family;
+
+ LIST_FIELDS(Route, routes);
+};
+
+struct Network {
+ /* [Match] */
+ char *ifname;
+
+ /* [Link] */
+ struct ether_addr mac;
+ uint32_t mtu;
+
+ /* [Network] */
+ DHCPType dhcp_type;
+ char **dns;
+ char *vlan;
+ char *bridge;
+ char *bond;
+
+ /* [DHCP] */
+ char *hostname;
+ int dhcp_use_dns;
+
+ LIST_HEAD(Address, addresses);
+ LIST_HEAD(Route, routes);
+};
+
+struct NetDev {
+ /* [NetDev] */
+ char *ifname;
+ char *kind;
+ uint32_t mtu;
+};
+
+struct Link {
+ char *filename;
+
+ /* [Match] */
+ struct hw_addr_data mac;
+
+ /* [Link] */
+ char *ifname;
+ char **policies;
+ char **alt_policies;
+};
+
+typedef struct Context {
+ Hashmap *networks_by_name;
+ Hashmap *netdevs_by_name;
+ Hashmap *links_by_filename;
+} Context;
+
+int parse_cmdline_item(const char *key, const char *value, void *data);
+int context_merge_networks(Context *context);
+void context_clear(Context *context);
+
+Network *network_get(Context *context, const char *ifname);
+void network_dump(Network *network, FILE *f);
+int network_format(Network *network, char **ret);
+
+NetDev *netdev_get(Context *context, const char *ifname);
+void netdev_dump(NetDev *netdev, FILE *f);
+int netdev_format(NetDev *netdev, char **ret);
+
+Link *link_get(Context *context, const char *filename);
+void link_dump(Link *link, FILE *f);
+int link_format(Link *link, char **ret);
diff --git a/src/network/generator/test-network-generator.c b/src/network/generator/test-network-generator.c
new file mode 100644
index 0000000..318b9e9
--- /dev/null
+++ b/src/network/generator/test-network-generator.c
@@ -0,0 +1,459 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "macro.h"
+#include "network-generator.h"
+#include "string-util.h"
+
+static void test_network_one(const char *ifname, const char *key, const char *value, const char *expected) {
+ _cleanup_(context_clear) Context context = {};
+ _cleanup_free_ char *output = NULL;
+ Network *network;
+
+ printf("# %s=%s\n", key, value);
+ assert_se(parse_cmdline_item(key, value, &context) >= 0);
+ assert_se(network = network_get(&context, ifname));
+ assert_se(network_format(network, &output) >= 0);
+ puts(output);
+ assert_se(streq(output, expected));
+}
+
+static void test_network_two(const char *ifname,
+ const char *key1, const char *value1,
+ const char *key2, const char *value2,
+ const char *expected) {
+ _cleanup_(context_clear) Context context = {};
+ _cleanup_free_ char *output = NULL;
+ Network *network;
+
+ printf("# %s=%s\n", key1, value1);
+ printf("# %s=%s\n", key2, value2);
+ assert_se(parse_cmdline_item(key1, value1, &context) >= 0);
+ assert_se(parse_cmdline_item(key2, value2, &context) >= 0);
+ assert_se(context_merge_networks(&context) >= 0);
+ assert_se(network = network_get(&context, ifname));
+ assert_se(network_format(network, &output) >= 0);
+ puts(output);
+ assert_se(streq(output, expected));
+}
+
+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;
+ NetDev *netdev;
+
+ printf("# %s=%s\n", key, value);
+ assert_se(parse_cmdline_item(key, value, &context) >= 0);
+ assert_se(netdev = netdev_get(&context, ifname));
+ assert_se(netdev_format(netdev, &output) >= 0);
+ puts(output);
+ assert_se(streq(output, expected));
+}
+
+static void test_link_one(const char *filename, const char *key, const char *value, const char *expected) {
+ _cleanup_(context_clear) Context context = {};
+ _cleanup_free_ char *output = NULL;
+ Link *link;
+
+ printf("# %s=%s\n", key, value);
+ assert_se(parse_cmdline_item(key, value, &context) >= 0);
+ assert_se(link = link_get(&context, filename));
+ assert_se(link_format(link, &output) >= 0);
+ puts(output);
+ assert_se(streq(output, expected));
+}
+
+int main(int argc, char *argv[]) {
+ test_network_one("", "ip", "dhcp6",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=ipv6\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "ip", "eth0:dhcp",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=ipv4\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "ip", "eth0:dhcp:1530",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "MTUBytes=1530\n"
+ "\n[Network]\n"
+ "DHCP=ipv4\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "ip", "eth0:dhcp:1530:00:11:22:33:44:55",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "MACAddress=00:11:22:33:44:55\n"
+ "MTUBytes=1530\n"
+ "\n[Network]\n"
+ "DHCP=ipv4\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10::192.168.0.1:255.255.255.0:hogehoge:eth0:on",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:1530",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "MTUBytes=1530\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:1530:00:11:22:33:44:55",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "MACAddress=00:11:22:33:44:55\n"
+ "MTUBytes=1530\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "[2001:1234:56:8f63::10]::[2001:1234:56:8f63::1]:64:hogehoge:eth0:on",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=2001:1234:56:8f63::10/64\n"
+ "\n[Route]\n"
+ "Gateway=2001:1234:56:8f63::1\n"
+ );
+
+ test_network_one("eth0", "ip", "[2001:1234:56:8f63::10]:[2001:1234:56:8f63::2]:[2001:1234:56:8f63::1]:64:hogehoge:eth0:on",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=2001:1234:56:8f63::10/64\n"
+ "Peer=2001:1234:56:8f63::2\n"
+ "\n[Route]\n"
+ "Gateway=2001:1234:56:8f63::1\n"
+ );
+
+ test_network_one("", "rd.route", "10.1.2.3/16:10.0.2.3",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "\n[Route]\n"
+ "Destination=10.1.2.3/16\n"
+ "Gateway=10.0.2.3\n"
+ );
+
+ test_network_one("eth0", "rd.route", "10.1.2.3/16:10.0.2.3:eth0",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "\n[Route]\n"
+ "Destination=10.1.2.3/16\n"
+ "Gateway=10.0.2.3\n"
+ );
+
+ test_network_one("", "nameserver", "10.1.2.3",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DNS=10.1.2.3\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("", "rd.peerdns", "0",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "UseDNS=no\n"
+ );
+
+ test_network_one("", "rd.peerdns", "1",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "UseDNS=yes\n"
+ );
+
+ test_network_one("eth0", "vlan", "vlan99:eth0",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "VLAN=vlan99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "bridge", "bridge99:eth0,eth1",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "Bridge=bridge99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth1", "bridge", "bridge99:eth0,eth1",
+ "[Match]\n"
+ "Name=eth1\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "Bridge=bridge99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "bond", "bond99:eth0,eth1",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "Bond=bond99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth1", "bond", "bond99:eth0,eth1::1530",
+ "[Match]\n"
+ "Name=eth1\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "Bond=bond99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_netdev_one("bond99", "bond", "bond99:eth0,eth1::1530",
+ "[NetDev]\n"
+ "Kind=bond\n"
+ "Name=bond99\n"
+ "MTUBytes=1530\n"
+ );
+
+ test_link_one("hogehoge", "ifname", "hogehoge:00:11:22:33:44:55",
+ "[Match]\n"
+ "MACAddress=00:11:22:33:44:55\n"
+ "\n[Link]\n"
+ "Name=hogehoge\n"
+ );
+
+ test_link_one("001122334455", "net.ifname-policy", "keep,kernel,database,onboard,slot,path,mac,00:11:22:33:44:55",
+ "[Match]\n"
+ "MACAddress=00:11:22:33:44:55\n"
+ "\n[Link]\n"
+ "NamePolicy=keep kernel database onboard slot path mac\n"
+ "AlternativeNamesPolicy=database onboard slot path mac\n"
+ );
+
+ test_link_one("default", "net.ifname-policy", "keep,kernel,database,onboard,slot,path,mac",
+ "[Match]\n"
+ "OriginalName=*\n"
+ "\n[Link]\n"
+ "NamePolicy=keep kernel database onboard slot path mac\n"
+ "AlternativeNamesPolicy=database onboard slot path mac\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "rd.route", "10.1.2.3/16:10.0.2.3",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Destination=10.1.2.3/16\n"
+ "Gateway=10.0.2.3\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on",
+ "nameserver", "10.1.2.3",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.1.2.3\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "nameserver", "10.1.2.3",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "DNS=10.1.2.3\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "rd.peerdns", "1",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "UseDNS=yes\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "bridge", "bridge99:eth0,eth1",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "Bridge=bridge99\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ return 0;
+}
diff --git a/src/network/meson.build b/src/network/meson.build
new file mode 100644
index 0000000..b09c18d
--- /dev/null
+++ b/src/network/meson.build
@@ -0,0 +1,323 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+sources = files(
+ 'netdev/bareudp.c',
+ 'netdev/bareudp.h',
+ 'netdev/batadv.c',
+ 'netdev/batadv.h',
+ 'netdev/bond.c',
+ 'netdev/bond.h',
+ 'netdev/bridge.c',
+ 'netdev/bridge.h',
+ 'netdev/dummy.c',
+ 'netdev/dummy.h',
+ 'netdev/fou-tunnel.c',
+ 'netdev/fou-tunnel.h',
+ 'netdev/geneve.c',
+ 'netdev/geneve.h',
+ 'netdev/ifb.c',
+ 'netdev/ifb.h',
+ 'netdev/ipoib.c',
+ 'netdev/ipoib.h',
+ 'netdev/ipvlan.c',
+ 'netdev/ipvlan.h',
+ 'netdev/l2tp-tunnel.c',
+ 'netdev/l2tp-tunnel.h',
+ 'netdev/macsec.c',
+ 'netdev/macsec.h',
+ 'netdev/macvlan.c',
+ 'netdev/macvlan.h',
+ 'netdev/netdev-util.c',
+ 'netdev/netdev-util.h',
+ 'netdev/netdev.c',
+ 'netdev/netdev.h',
+ 'netdev/netdevsim.c',
+ 'netdev/netdevsim.h',
+ 'netdev/nlmon.c',
+ 'netdev/nlmon.h',
+ 'netdev/tunnel.c',
+ 'netdev/tunnel.h',
+ 'netdev/tuntap.c',
+ 'netdev/tuntap.h',
+ 'netdev/vcan.c',
+ 'netdev/vcan.h',
+ 'netdev/veth.c',
+ 'netdev/veth.h',
+ 'netdev/vlan.c',
+ 'netdev/vlan.h',
+ 'netdev/vrf.c',
+ 'netdev/vrf.h',
+ 'netdev/vxcan.c',
+ 'netdev/vxcan.h',
+ 'netdev/vxlan.c',
+ 'netdev/vxlan.h',
+ 'netdev/wireguard.c',
+ 'netdev/wireguard.h',
+ 'netdev/wlan.c',
+ 'netdev/wlan.h',
+ 'netdev/xfrm.c',
+ 'netdev/xfrm.h',
+ 'networkd-address-generation.c',
+ 'networkd-address-generation.h',
+ 'networkd-address-label.c',
+ 'networkd-address-label.h',
+ 'networkd-address-pool.c',
+ 'networkd-address-pool.h',
+ 'networkd-address.c',
+ 'networkd-address.h',
+ 'networkd-bridge-fdb.c',
+ 'networkd-bridge-fdb.h',
+ 'networkd-bridge-mdb.c',
+ 'networkd-bridge-mdb.h',
+ 'networkd-bridge-vlan.c',
+ 'networkd-bridge-vlan.h',
+ 'networkd-can.c',
+ 'networkd-can.h',
+ 'networkd-conf.c',
+ 'networkd-conf.h',
+ 'networkd-dhcp-common.c',
+ 'networkd-dhcp-common.h',
+ 'networkd-dhcp-prefix-delegation.c',
+ 'networkd-dhcp-prefix-delegation.h',
+ 'networkd-dhcp-server-bus.c',
+ 'networkd-dhcp-server-bus.h',
+ 'networkd-dhcp-server-static-lease.c',
+ 'networkd-dhcp-server-static-lease.h',
+ 'networkd-dhcp-server.c',
+ 'networkd-dhcp-server.h',
+ 'networkd-dhcp4.c',
+ 'networkd-dhcp4.h',
+ 'networkd-dhcp6.c',
+ 'networkd-dhcp6.h',
+ 'networkd-ipv4acd.c',
+ 'networkd-ipv4acd.h',
+ 'networkd-ipv4ll.c',
+ 'networkd-ipv4ll.h',
+ 'networkd-ipv6-proxy-ndp.c',
+ 'networkd-ipv6-proxy-ndp.h',
+ 'networkd-ipv6ll.c',
+ 'networkd-ipv6ll.h',
+ 'networkd-json.c',
+ 'networkd-json.h',
+ 'networkd-link-bus.c',
+ 'networkd-link-bus.h',
+ 'networkd-link.c',
+ 'networkd-link.h',
+ 'networkd-lldp-rx.c',
+ 'networkd-lldp-rx.h',
+ 'networkd-lldp-tx.c',
+ 'networkd-lldp-tx.h',
+ 'networkd-manager-bus.c',
+ 'networkd-manager-bus.h',
+ 'networkd-manager.c',
+ 'networkd-manager.h',
+ 'networkd-ndisc.c',
+ 'networkd-ndisc.h',
+ 'networkd-neighbor.c',
+ 'networkd-neighbor.h',
+ 'networkd-netlabel.c',
+ 'networkd-netlabel.h',
+ 'networkd-network-bus.c',
+ 'networkd-network-bus.h',
+ 'networkd-network.c',
+ 'networkd-network.h',
+ 'networkd-nexthop.c',
+ 'networkd-nexthop.h',
+ 'networkd-queue.c',
+ 'networkd-queue.h',
+ 'networkd-radv.c',
+ 'networkd-radv.h',
+ 'networkd-route-util.c',
+ 'networkd-route-util.h',
+ 'networkd-route.c',
+ 'networkd-route.h',
+ 'networkd-routing-policy-rule.c',
+ 'networkd-routing-policy-rule.h',
+ 'networkd-setlink.c',
+ 'networkd-setlink.h',
+ 'networkd-speed-meter.c',
+ 'networkd-speed-meter.h',
+ 'networkd-sriov.c',
+ 'networkd-sriov.h',
+ 'networkd-state-file.c',
+ 'networkd-state-file.h',
+ 'networkd-sysctl.c',
+ 'networkd-sysctl.h',
+ 'networkd-util.c',
+ 'networkd-util.h',
+ 'networkd-wifi.c',
+ 'networkd-wifi.h',
+ 'networkd-wiphy.c',
+ 'networkd-wiphy.h',
+ 'tc/cake.c',
+ 'tc/cake.h',
+ 'tc/codel.c',
+ 'tc/codel.h',
+ 'tc/drr.c',
+ 'tc/drr.h',
+ 'tc/ets.c',
+ 'tc/ets.h',
+ 'tc/fifo.c',
+ 'tc/fifo.h',
+ 'tc/fq-codel.c',
+ 'tc/fq-codel.h',
+ 'tc/fq-pie.c',
+ 'tc/fq-pie.h',
+ 'tc/fq.c',
+ 'tc/fq.h',
+ 'tc/gred.c',
+ 'tc/gred.h',
+ 'tc/hhf.c',
+ 'tc/hhf.h',
+ 'tc/htb.c',
+ 'tc/htb.h',
+ 'tc/netem.c',
+ 'tc/netem.h',
+ 'tc/pie.c',
+ 'tc/pie.h',
+ 'tc/qdisc.c',
+ 'tc/qdisc.h',
+ 'tc/qfq.c',
+ 'tc/qfq.h',
+ 'tc/sfb.c',
+ 'tc/sfb.h',
+ 'tc/sfq.c',
+ 'tc/sfq.h',
+ 'tc/tbf.c',
+ 'tc/tbf.h',
+ 'tc/tc-util.c',
+ 'tc/tc-util.h',
+ 'tc/tc.c',
+ 'tc/tc.h',
+ 'tc/tclass.c',
+ 'tc/tclass.h',
+ 'tc/teql.c',
+ 'tc/teql.h',
+)
+
+systemd_networkd_sources = files('networkd.c')
+
+systemd_networkd_wait_online_sources = files(
+ 'wait-online/link.c',
+ 'wait-online/link.h',
+ 'wait-online/manager.c',
+ 'wait-online/manager.h',
+ 'wait-online/wait-online.c',
+)
+
+networkctl_sources = files('networkctl.c')
+
+network_generator_sources = files(
+ 'generator/main.c',
+ 'generator/network-generator.c',
+ 'generator/network-generator.h',
+)
+
+sources += custom_target(
+ 'networkd-gperf.c',
+ input : 'networkd-gperf.gperf',
+ output : 'networkd-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+sources += custom_target(
+ 'networkd-network-gperf.c',
+ input : 'networkd-network-gperf.gperf',
+ output : 'networkd-network-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+sources += custom_target(
+ 'netdev-gperf.c',
+ input : 'netdev/netdev-gperf.gperf',
+ output : 'netdev-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+if get_option('link-networkd-shared')
+ networkd_link_with = [libshared]
+else
+ networkd_link_with = [libsystemd_static,
+ libshared_static,
+ libbasic_gcrypt]
+endif
+
+network_includes = [libsystemd_network_includes, include_directories(['.', 'netdev', 'tc'])]
+
+libnetworkd_core = static_library(
+ 'networkd-core',
+ sources,
+ include_directories : network_includes,
+ link_with : networkd_link_with,
+ build_by_default : false)
+
+if conf.get('ENABLE_NETWORKD') == 1
+ install_data('org.freedesktop.network1.conf',
+ install_dir : dbuspolicydir)
+ install_data('org.freedesktop.network1.service',
+ install_dir : dbussystemservicedir)
+ install_data('org.freedesktop.network1.policy',
+ install_dir : polkitpolicydir)
+ if install_polkit
+ install_data('systemd-networkd.rules',
+ install_dir : polkitrulesdir)
+ endif
+ if install_polkit_pkla
+ install_data('systemd-networkd.pkla',
+ install_dir : polkitpkladir)
+ endif
+
+ if install_sysconfdir_samples
+ install_data('networkd.conf',
+ install_dir : pkgsysconfdir)
+ endif
+endif
+
+fuzzers += [
+ [files('fuzz-netdev-parser.c'),
+ [libnetworkd_core,
+ libsystemd_network,
+ networkd_link_with],
+ [threads],
+ network_includes],
+
+ [files('fuzz-network-parser.c'),
+ [libnetworkd_core,
+ libsystemd_network,
+ networkd_link_with],
+ [threads],
+ network_includes],
+]
+
+tests += [
+ [files('test-networkd-address.c'),
+ [libnetworkd_core,
+ libsystemd_network],
+ [libatomic],
+ network_includes],
+
+ [files('test-networkd-conf.c'),
+ [libnetworkd_core,
+ libsystemd_network],
+ [libatomic],
+ network_includes],
+
+ [files('test-networkd-util.c'),
+ [libnetworkd_core,
+ libsystemd_network],
+ [],
+ network_includes],
+
+ [files('test-network.c'),
+ [libnetworkd_core,
+ libsystemd_network],
+ [threads],
+ network_includes],
+
+ [files('test-network-tables.c'),
+ [libnetworkd_core,
+ libsystemd_network],
+ [threads],
+ network_includes],
+
+ [files('generator/test-network-generator.c',
+ 'generator/network-generator.c',
+ 'generator/network-generator.h')],
+]
diff --git a/src/network/netdev/bareudp.c b/src/network/netdev/bareudp.c
new file mode 100644
index 0000000..24d3afb
--- /dev/null
+++ b/src/network/netdev/bareudp.c
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "bareudp.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "string-table.h"
+
+static const char* const bare_udp_protocol_table[_BARE_UDP_PROTOCOL_MAX] = {
+ [BARE_UDP_PROTOCOL_IPV4] = "ipv4",
+ [BARE_UDP_PROTOCOL_IPV6] = "ipv6",
+ [BARE_UDP_PROTOCOL_MPLS_UC] = "mpls-uc",
+ [BARE_UDP_PROTOCOL_MPLS_MC] = "mpls-mc",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(bare_udp_protocol, BareUDPProtocol);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bare_udp_iftype, bare_udp_protocol, BareUDPProtocol,
+ "Failed to parse EtherType=");
+
+static int netdev_bare_udp_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ BareUDP *u;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ u = BAREUDP(netdev);
+
+ assert(u);
+
+ r = sd_netlink_message_append_u16(m, IFLA_BAREUDP_ETHERTYPE, htobe16(u->iftype));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_BAREUDP_PORT, htobe16(u->dest_port));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int netdev_bare_udp_verify(NetDev *netdev, const char *filename) {
+ BareUDP *u;
+
+ assert(netdev);
+ assert(filename);
+
+ u = BAREUDP(netdev);
+
+ assert(u);
+
+ if (u->dest_port == 0)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: BareUDP DesinationPort= is not set. Ignoring.", filename);
+
+ if (u->iftype == _BARE_UDP_PROTOCOL_INVALID)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: BareUDP EtherType= is not set. Ignoring.", filename);
+
+ return 0;
+}
+
+static void bare_udp_init(NetDev *netdev) {
+ BareUDP *u;
+
+ assert(netdev);
+
+ u = BAREUDP(netdev);
+
+ assert(u);
+
+ u->iftype = _BARE_UDP_PROTOCOL_INVALID;
+}
+
+const NetDevVTable bare_udp_vtable = {
+ .object_size = sizeof(BareUDP),
+ .sections = NETDEV_COMMON_SECTIONS "BareUDP\0",
+ .init = bare_udp_init,
+ .config_verify = netdev_bare_udp_verify,
+ .fill_message_create = netdev_bare_udp_fill_message_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_NONE,
+};
diff --git a/src/network/netdev/bareudp.h b/src/network/netdev/bareudp.h
new file mode 100644
index 0000000..8d8863c
--- /dev/null
+++ b/src/network/netdev/bareudp.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+typedef struct BareUDP BareUDP;
+
+#include <linux/if_ether.h>
+
+#include "conf-parser.h"
+#include "netdev.h"
+
+typedef enum BareUDPProtocol {
+ BARE_UDP_PROTOCOL_IPV4 = ETH_P_IP,
+ BARE_UDP_PROTOCOL_IPV6 = ETH_P_IPV6,
+ BARE_UDP_PROTOCOL_MPLS_UC = ETH_P_MPLS_UC,
+ BARE_UDP_PROTOCOL_MPLS_MC = ETH_P_MPLS_MC,
+ _BARE_UDP_PROTOCOL_MAX,
+ _BARE_UDP_PROTOCOL_INVALID = -EINVAL,
+} BareUDPProtocol;
+
+struct BareUDP {
+ NetDev meta;
+
+ BareUDPProtocol iftype;
+ uint16_t dest_port;
+};
+
+DEFINE_NETDEV_CAST(BAREUDP, BareUDP);
+extern const NetDevVTable bare_udp_vtable;
+
+const char *bare_udp_protocol_to_string(BareUDPProtocol d) _const_;
+BareUDPProtocol bare_udp_protocol_from_string(const char *d) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_bare_udp_iftype);
diff --git a/src/network/netdev/batadv.c b/src/network/netdev/batadv.c
new file mode 100644
index 0000000..7e97619
--- /dev/null
+++ b/src/network/netdev/batadv.c
@@ -0,0 +1,216 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <inttypes.h>
+#include <netinet/in.h>
+#include <linux/genetlink.h>
+#include <linux/if_arp.h>
+
+#include "batadv.h"
+#include "fileio.h"
+#include "netlink-util.h"
+#include "network-internal.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "stdio-util.h"
+#include "string-table.h"
+#include "string-util.h"
+
+static void batadv_init(NetDev *n) {
+ BatmanAdvanced *b;
+
+ b = BATADV(n);
+
+ /* Set defaults */
+ b->aggregation = true;
+ b->gateway_bandwidth_down = 10000;
+ b->gateway_bandwidth_up = 2000;
+ b->bridge_loop_avoidance = true;
+ b->distributed_arp_table = true;
+ b->fragmentation = true;
+ b->hop_penalty = 15;
+ b->originator_interval = 1000;
+ b->routing_algorithm = BATADV_ROUTING_ALGORITHM_BATMAN_V;
+}
+
+static const char* const batadv_gateway_mode_table[_BATADV_GATEWAY_MODE_MAX] = {
+ [BATADV_GATEWAY_MODE_OFF] = "off",
+ [BATADV_GATEWAY_MODE_CLIENT] = "client",
+ [BATADV_GATEWAY_MODE_SERVER] = "server",
+};
+
+static const char* const batadv_routing_algorithm_table[_BATADV_ROUTING_ALGORITHM_MAX] = {
+ [BATADV_ROUTING_ALGORITHM_BATMAN_V] = "batman-v",
+ [BATADV_ROUTING_ALGORITHM_BATMAN_IV] = "batman-iv",
+};
+
+static const char* const batadv_routing_algorithm_kernel_table[_BATADV_ROUTING_ALGORITHM_MAX] = {
+ [BATADV_ROUTING_ALGORITHM_BATMAN_V] = "BATMAN_V",
+ [BATADV_ROUTING_ALGORITHM_BATMAN_IV] = "BATMAN_IV",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(batadv_gateway_mode, BatadvGatewayModes);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_batadv_gateway_mode, batadv_gateway_mode, BatadvGatewayModes,
+ "Failed to parse GatewayMode=");
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(batadv_routing_algorithm, BatadvRoutingAlgorithm);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_batadv_routing_algorithm, batadv_routing_algorithm, BatadvRoutingAlgorithm,
+ "Failed to parse RoutingAlgorithm=");
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(batadv_routing_algorithm_kernel, BatadvRoutingAlgorithm);
+
+int config_parse_badadv_bandwidth (
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint64_t k;
+ uint32_t *bandwidth = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = parse_size(rvalue, 1000, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (k/1000/100 > UINT32_MAX)
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "The value of '%s=', is outside of 0...429496729500000 range: %s",
+ lvalue, rvalue);
+
+ *bandwidth = k/1000/100;
+
+ return 0;
+}
+
+/* callback for batman netdev's parameter set */
+static int netdev_batman_set_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "BATADV parameters could not be set: %m");
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "BATADV parameters set success");
+
+ return 1;
+}
+
+static int netdev_batadv_post_create_message(NetDev *netdev, sd_netlink_message *message) {
+ BatmanAdvanced *b;
+ int r;
+
+ assert_se(b = BATADV(netdev));
+
+ r = sd_netlink_message_append_u32(message, BATADV_ATTR_MESH_IFINDEX, netdev->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(message, BATADV_ATTR_GW_MODE, b->gateway_mode);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(message, BATADV_ATTR_AGGREGATED_OGMS_ENABLED, b->aggregation);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(message, BATADV_ATTR_BRIDGE_LOOP_AVOIDANCE_ENABLED, b->bridge_loop_avoidance);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(message, BATADV_ATTR_DISTRIBUTED_ARP_TABLE_ENABLED, b->distributed_arp_table);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(message, BATADV_ATTR_FRAGMENTATION_ENABLED, b->fragmentation);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(message, BATADV_ATTR_HOP_PENALTY, b->hop_penalty);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(message, BATADV_ATTR_ORIG_INTERVAL, DIV_ROUND_UP(b->originator_interval, USEC_PER_MSEC));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(message, BATADV_ATTR_GW_BANDWIDTH_DOWN, b->gateway_bandwidth_down);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(message, BATADV_ATTR_GW_BANDWIDTH_UP, b->gateway_bandwidth_up);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int netdev_batadv_post_create(NetDev *netdev, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL;
+ int r;
+
+ assert(netdev);
+
+ r = sd_genl_message_new(netdev->manager->genl, BATADV_NL_NAME, BATADV_CMD_SET_MESH, &message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not allocate netlink message: %m");
+
+ r = netdev_batadv_post_create_message(netdev, message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not create netlink message: %m");
+
+ r = netlink_call_async(netdev->manager->genl, NULL, message, netdev_batman_set_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not send netlink message: %m");
+
+ netdev_ref(netdev);
+
+ return r;
+}
+
+static int netdev_batadv_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ BatmanAdvanced *b;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ b = BATADV(netdev);
+ assert(b);
+
+ r = sd_netlink_message_append_string(m, IFLA_BATADV_ALGO_NAME, batadv_routing_algorithm_kernel_to_string(b->routing_algorithm));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+const NetDevVTable batadv_vtable = {
+ .object_size = sizeof(BatmanAdvanced),
+ .init = batadv_init,
+ .sections = NETDEV_COMMON_SECTIONS "BatmanAdvanced\0",
+ .fill_message_create = netdev_batadv_fill_message_create,
+ .post_create = netdev_batadv_post_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/batadv.h b/src/network/netdev/batadv.h
new file mode 100644
index 0000000..f1f9b46
--- /dev/null
+++ b/src/network/netdev/batadv.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#pragma once
+
+#include <linux/batman_adv.h>
+
+#include "conf-parser.h"
+#include "netdev.h"
+
+#define BATADV_GENL_NAME "batadv"
+
+typedef enum BatadvGatewayModes {
+ BATADV_GATEWAY_MODE_OFF = BATADV_GW_MODE_OFF,
+ BATADV_GATEWAY_MODE_CLIENT = BATADV_GW_MODE_CLIENT,
+ BATADV_GATEWAY_MODE_SERVER = BATADV_GW_MODE_SERVER,
+ _BATADV_GATEWAY_MODE_MAX,
+ _BATADV_GATEWAY_MODE_INVALID = -EINVAL,
+} BatadvGatewayModes;
+
+typedef enum BatadvRoutingAlgorithm {
+ BATADV_ROUTING_ALGORITHM_BATMAN_V,
+ BATADV_ROUTING_ALGORITHM_BATMAN_IV,
+ _BATADV_ROUTING_ALGORITHM_MAX,
+ _BATADV_ROUTING_ALGORITHM_INVALID = -EINVAL,
+} BatadvRoutingAlgorithm;
+
+typedef struct Batadv {
+ NetDev meta;
+
+ BatadvGatewayModes gateway_mode;
+ uint32_t gateway_bandwidth_down;
+ uint32_t gateway_bandwidth_up;
+ uint8_t hop_penalty;
+ BatadvRoutingAlgorithm routing_algorithm;
+ usec_t originator_interval;
+ bool aggregation;
+ bool bridge_loop_avoidance;
+ bool distributed_arp_table;
+ bool fragmentation;
+} BatmanAdvanced;
+
+DEFINE_NETDEV_CAST(BATADV, BatmanAdvanced);
+extern const NetDevVTable batadv_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_batadv_gateway_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_batadv_routing_algorithm);
+CONFIG_PARSER_PROTOTYPE(config_parse_badadv_bandwidth);
diff --git a/src/network/netdev/bond.c b/src/network/netdev/bond.c
new file mode 100644
index 0000000..601bff0
--- /dev/null
+++ b/src/network/netdev/bond.c
@@ -0,0 +1,457 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "alloc-util.h"
+#include "bond.h"
+#include "bond-util.h"
+#include "conf-parser.h"
+#include "ether-addr-util.h"
+#include "extract-word.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "string-table.h"
+
+/*
+ * Number of seconds between instances where the bonding
+ * driver sends learning packets to each slaves peer switch
+ */
+#define LEARNING_PACKETS_INTERVAL_MIN_SEC (1 * USEC_PER_SEC)
+#define LEARNING_PACKETS_INTERVAL_MAX_SEC (0x7fffffff * USEC_PER_SEC)
+
+/* Number of IGMP membership reports to be issued after
+ * a failover event.
+ */
+#define RESEND_IGMP_MIN 0
+#define RESEND_IGMP_MAX 255
+#define RESEND_IGMP_DEFAULT 1
+
+/*
+ * Number of packets to transmit through a slave before
+ * moving to the next one.
+ */
+#define PACKETS_PER_SLAVE_MIN 0
+#define PACKETS_PER_SLAVE_MAX 65535
+#define PACKETS_PER_SLAVE_DEFAULT 1
+
+/*
+ * Number of peer notifications (gratuitous ARPs and
+ * unsolicited IPv6 Neighbor Advertisements) to be issued after a
+ * failover event.
+ */
+#define GRATUITOUS_ARP_MIN 0
+#define GRATUITOUS_ARP_MAX 255
+#define GRATUITOUS_ARP_DEFAULT 1
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_mode, bond_mode, BondMode, "Failed to parse bond mode");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_xmit_hash_policy,
+ bond_xmit_hash_policy,
+ BondXmitHashPolicy,
+ "Failed to parse bond transmit hash policy");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_lacp_rate, bond_lacp_rate, BondLacpRate, "Failed to parse bond lacp rate");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_ad_select, bond_ad_select, BondAdSelect, "Failed to parse bond AD select");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_fail_over_mac, bond_fail_over_mac, BondFailOverMac, "Failed to parse bond fail over MAC");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_arp_validate, bond_arp_validate, BondArpValidate, "Failed to parse bond arp validate");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_arp_all_targets, bond_arp_all_targets, BondArpAllTargets, "Failed to parse bond Arp all targets");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_primary_reselect, bond_primary_reselect, BondPrimaryReselect, "Failed to parse bond primary reselect");
+
+static int netdev_bond_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ Bond *b;
+ int r;
+
+ assert(netdev);
+ assert(!link);
+ assert(m);
+
+ b = BOND(netdev);
+
+ assert(b);
+
+ if (b->mode != _NETDEV_BOND_MODE_INVALID) {
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_MODE, b->mode);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->xmit_hash_policy != _NETDEV_BOND_XMIT_HASH_POLICY_INVALID) {
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_XMIT_HASH_POLICY, b->xmit_hash_policy);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->lacp_rate != _NETDEV_BOND_LACP_RATE_INVALID &&
+ b->mode == NETDEV_BOND_MODE_802_3AD) {
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_AD_LACP_RATE, b->lacp_rate);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->miimon != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_MIIMON, b->miimon / USEC_PER_MSEC);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->downdelay != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_DOWNDELAY, b->downdelay / USEC_PER_MSEC);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->updelay != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_UPDELAY, b->updelay / USEC_PER_MSEC);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->arp_interval != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_ARP_INTERVAL, b->arp_interval / USEC_PER_MSEC);
+ if (r < 0)
+ return r;
+
+ if (b->lp_interval >= LEARNING_PACKETS_INTERVAL_MIN_SEC &&
+ b->lp_interval <= LEARNING_PACKETS_INTERVAL_MAX_SEC) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_LP_INTERVAL, b->lp_interval / USEC_PER_SEC);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ if (b->ad_select != _NETDEV_BOND_AD_SELECT_INVALID &&
+ b->mode == NETDEV_BOND_MODE_802_3AD) {
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_AD_SELECT, b->ad_select);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->fail_over_mac != _NETDEV_BOND_FAIL_OVER_MAC_INVALID &&
+ b->mode == NETDEV_BOND_MODE_ACTIVE_BACKUP) {
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_FAIL_OVER_MAC, b->fail_over_mac);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->arp_validate != _NETDEV_BOND_ARP_VALIDATE_INVALID) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_ARP_VALIDATE, b->arp_validate);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->arp_all_targets != _NETDEV_BOND_ARP_ALL_TARGETS_INVALID) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_ARP_ALL_TARGETS, b->arp_all_targets);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->primary_reselect != _NETDEV_BOND_PRIMARY_RESELECT_INVALID) {
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_PRIMARY_RESELECT, b->primary_reselect);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->resend_igmp <= RESEND_IGMP_MAX) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_RESEND_IGMP, b->resend_igmp);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->packets_per_slave <= PACKETS_PER_SLAVE_MAX &&
+ b->mode == NETDEV_BOND_MODE_BALANCE_RR) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_PACKETS_PER_SLAVE, b->packets_per_slave);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->num_grat_arp <= GRATUITOUS_ARP_MAX) {
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_NUM_PEER_NOTIF, b->num_grat_arp);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->min_links != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_MIN_LINKS, b->min_links);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->ad_actor_sys_prio != 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_BOND_AD_ACTOR_SYS_PRIO, b->ad_actor_sys_prio);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->ad_user_port_key != 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_BOND_AD_USER_PORT_KEY, b->ad_user_port_key);
+ if (r < 0)
+ return r;
+ }
+
+ if (!ether_addr_is_null(&b->ad_actor_system)) {
+ r = sd_netlink_message_append_ether_addr(m, IFLA_BOND_AD_ACTOR_SYSTEM, &b->ad_actor_system);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_ALL_SLAVES_ACTIVE, b->all_slaves_active);
+ if (r < 0)
+ return r;
+
+ if (b->tlb_dynamic_lb >= 0) {
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_TLB_DYNAMIC_LB, b->tlb_dynamic_lb);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->arp_interval > 0 && !ordered_set_isempty(b->arp_ip_targets)) {
+ void *val;
+ int n = 0;
+
+ r = sd_netlink_message_open_container(m, IFLA_BOND_ARP_IP_TARGET);
+ if (r < 0)
+ return r;
+
+ ORDERED_SET_FOREACH(val, b->arp_ip_targets) {
+ r = sd_netlink_message_append_u32(m, n++, PTR_TO_UINT32(val));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int config_parse_arp_ip_target_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Bond *b = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ b->arp_ip_targets = ordered_set_free(b->arp_ip_targets);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *n = NULL;
+ union in_addr_union ip;
+
+ r = extract_first_word(&p, &n, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse Bond ARP IP target address, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ r = in_addr_from_string(AF_INET, n, &ip);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Bond ARP IP target address is invalid, ignoring assignment: %s", n);
+ continue;
+ }
+
+ if (ordered_set_size(b->arp_ip_targets) >= NETDEV_BOND_ARP_TARGETS_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Too many ARP IP targets are specified. The maximum number is %d. Ignoring assignment: %s",
+ NETDEV_BOND_ARP_TARGETS_MAX, n);
+ continue;
+ }
+
+ r = ordered_set_ensure_put(&b->arp_ip_targets, NULL, UINT32_TO_PTR(ip.in.s_addr));
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r == -EEXIST)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Bond ARP IP target address is duplicated, ignoring assignment: %s", n);
+ if (r < 0)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store bond ARP IP target address '%s', ignoring assignment: %m", n);
+ }
+}
+
+int config_parse_ad_actor_sys_prio(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+ Bond *b = userdata;
+ uint16_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou16(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse actor system priority '%s', ignoring: %m", rvalue);
+ return 0;
+ }
+
+ if (v == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse actor system priority '%s'. Range is [1,65535], ignoring.",
+ rvalue);
+ return 0;
+ }
+
+ b->ad_actor_sys_prio = v;
+
+ return 0;
+}
+
+int config_parse_ad_user_port_key(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+ Bond *b = userdata;
+ uint16_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou16(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse user port key '%s', ignoring: %m", rvalue);
+ return 0;
+ }
+
+ if (v > 1023) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse user port key '%s'. Range is [0…1023], ignoring.", rvalue);
+ return 0;
+ }
+
+ b->ad_user_port_key = v;
+
+ return 0;
+}
+
+int config_parse_ad_actor_system(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+ Bond *b = userdata;
+ struct ether_addr n;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = parse_ether_addr(rvalue, &n);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Not a valid MAC address %s. Ignoring assignment: %m",
+ rvalue);
+ return 0;
+ }
+ if (ether_addr_is_null(&n) || (n.ether_addr_octet[0] & 0x01)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Not an appropriate MAC address %s, cannot be null or multicast. Ignoring assignment.",
+ rvalue);
+ return 0;
+ }
+
+ b->ad_actor_system = n;
+
+ return 0;
+}
+
+static void bond_done(NetDev *netdev) {
+ Bond *b;
+
+ assert(netdev);
+ b = BOND(netdev);
+ assert(b);
+
+ ordered_set_free(b->arp_ip_targets);
+}
+
+static void bond_init(NetDev *netdev) {
+ Bond *b;
+
+ assert(netdev);
+
+ b = BOND(netdev);
+
+ assert(b);
+
+ b->mode = _NETDEV_BOND_MODE_INVALID;
+ b->xmit_hash_policy = _NETDEV_BOND_XMIT_HASH_POLICY_INVALID;
+ b->lacp_rate = _NETDEV_BOND_LACP_RATE_INVALID;
+ b->ad_select = _NETDEV_BOND_AD_SELECT_INVALID;
+ b->fail_over_mac = _NETDEV_BOND_FAIL_OVER_MAC_INVALID;
+ b->arp_validate = _NETDEV_BOND_ARP_VALIDATE_INVALID;
+ b->arp_all_targets = _NETDEV_BOND_ARP_ALL_TARGETS_INVALID;
+ b->primary_reselect = _NETDEV_BOND_PRIMARY_RESELECT_INVALID;
+
+ b->all_slaves_active = false;
+ b->tlb_dynamic_lb = -1;
+
+ b->resend_igmp = RESEND_IGMP_DEFAULT;
+ b->packets_per_slave = PACKETS_PER_SLAVE_DEFAULT;
+ b->num_grat_arp = GRATUITOUS_ARP_DEFAULT;
+ b->lp_interval = LEARNING_PACKETS_INTERVAL_MIN_SEC;
+}
+
+const NetDevVTable bond_vtable = {
+ .object_size = sizeof(Bond),
+ .init = bond_init,
+ .done = bond_done,
+ .sections = NETDEV_COMMON_SECTIONS "Bond\0",
+ .fill_message_create = netdev_bond_fill_message_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/bond.h b/src/network/netdev/bond.h
new file mode 100644
index 0000000..e4b0a0d
--- /dev/null
+++ b/src/network/netdev/bond.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+#include <linux/if_bonding.h>
+
+#include "bond-util.h"
+#include "macro.h"
+#include "netdev.h"
+#include "ordered-set.h"
+
+typedef struct Bond {
+ NetDev meta;
+
+ BondMode mode;
+ BondXmitHashPolicy xmit_hash_policy;
+ BondLacpRate lacp_rate;
+ BondAdSelect ad_select;
+ BondFailOverMac fail_over_mac;
+ BondArpValidate arp_validate;
+ BondArpAllTargets arp_all_targets;
+ BondPrimaryReselect primary_reselect;
+
+ int tlb_dynamic_lb;
+
+ bool all_slaves_active;
+
+ unsigned resend_igmp;
+ unsigned packets_per_slave;
+ unsigned num_grat_arp;
+ unsigned min_links;
+
+ uint16_t ad_actor_sys_prio;
+ uint16_t ad_user_port_key;
+ struct ether_addr ad_actor_system;
+
+ usec_t miimon;
+ usec_t updelay;
+ usec_t downdelay;
+ usec_t arp_interval;
+ usec_t lp_interval;
+
+ OrderedSet *arp_ip_targets;
+} Bond;
+
+DEFINE_NETDEV_CAST(BOND, Bond);
+extern const NetDevVTable bond_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_bond_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_bond_xmit_hash_policy);
+CONFIG_PARSER_PROTOTYPE(config_parse_bond_lacp_rate);
+CONFIG_PARSER_PROTOTYPE(config_parse_bond_ad_select);
+CONFIG_PARSER_PROTOTYPE(config_parse_bond_fail_over_mac);
+CONFIG_PARSER_PROTOTYPE(config_parse_bond_arp_validate);
+CONFIG_PARSER_PROTOTYPE(config_parse_bond_arp_all_targets);
+CONFIG_PARSER_PROTOTYPE(config_parse_bond_primary_reselect);
+CONFIG_PARSER_PROTOTYPE(config_parse_arp_ip_target_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_ad_actor_sys_prio);
+CONFIG_PARSER_PROTOTYPE(config_parse_ad_user_port_key);
+CONFIG_PARSER_PROTOTYPE(config_parse_ad_actor_system);
diff --git a/src/network/netdev/bridge.c b/src/network/netdev/bridge.c
new file mode 100644
index 0000000..b65c3b4
--- /dev/null
+++ b/src/network/netdev/bridge.c
@@ -0,0 +1,290 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+#include <linux/if_bridge.h>
+
+#include "bridge.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "string-table.h"
+#include "vlan-util.h"
+
+assert_cc((int) MULTICAST_ROUTER_NONE == (int) MDB_RTR_TYPE_DISABLED);
+assert_cc((int) MULTICAST_ROUTER_TEMPORARY_QUERY == (int) MDB_RTR_TYPE_TEMP_QUERY);
+assert_cc((int) MULTICAST_ROUTER_PERMANENT == (int) MDB_RTR_TYPE_PERM);
+assert_cc((int) MULTICAST_ROUTER_TEMPORARY == (int) MDB_RTR_TYPE_TEMP);
+
+static const char* const multicast_router_table[_MULTICAST_ROUTER_MAX] = {
+ [MULTICAST_ROUTER_NONE] = "no",
+ [MULTICAST_ROUTER_TEMPORARY_QUERY] = "query",
+ [MULTICAST_ROUTER_PERMANENT] = "permanent",
+ [MULTICAST_ROUTER_TEMPORARY] = "temporary",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(multicast_router, MulticastRouter, _MULTICAST_ROUTER_INVALID);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_multicast_router, multicast_router, MulticastRouter,
+ "Failed to parse bridge multicast router setting");
+
+/* callback for bridge netdev's parameter set */
+static int netdev_bridge_set_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "Bridge parameters could not be set: %m");
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "Bridge parameters set success");
+
+ return 1;
+}
+
+static int netdev_bridge_post_create_message(NetDev *netdev, sd_netlink_message *req) {
+ Bridge *b;
+ int r;
+
+ assert_se(b = BRIDGE(netdev));
+
+ r = sd_netlink_message_open_container(req, IFLA_LINKINFO);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container_union(req, IFLA_INFO_DATA, netdev_kind_to_string(netdev->kind));
+ if (r < 0)
+ return r;
+
+ /* convert to jiffes */
+ if (b->forward_delay != USEC_INFINITY) {
+ r = sd_netlink_message_append_u32(req, IFLA_BR_FORWARD_DELAY, usec_to_jiffies(b->forward_delay));
+ if (r < 0)
+ return r;
+ }
+
+ if (b->hello_time > 0) {
+ r = sd_netlink_message_append_u32(req, IFLA_BR_HELLO_TIME, usec_to_jiffies(b->hello_time));
+ if (r < 0)
+ return r;
+ }
+
+ if (b->max_age > 0) {
+ r = sd_netlink_message_append_u32(req, IFLA_BR_MAX_AGE, usec_to_jiffies(b->max_age));
+ if (r < 0)
+ return r;
+ }
+
+ if (b->ageing_time != USEC_INFINITY) {
+ r = sd_netlink_message_append_u32(req, IFLA_BR_AGEING_TIME, usec_to_jiffies(b->ageing_time));
+ if (r < 0)
+ return r;
+ }
+
+ if (b->priority > 0) {
+ r = sd_netlink_message_append_u16(req, IFLA_BR_PRIORITY, b->priority);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->group_fwd_mask > 0) {
+ r = sd_netlink_message_append_u16(req, IFLA_BR_GROUP_FWD_MASK, b->group_fwd_mask);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->default_pvid != VLANID_INVALID) {
+ r = sd_netlink_message_append_u16(req, IFLA_BR_VLAN_DEFAULT_PVID, b->default_pvid);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->mcast_querier >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_QUERIER, b->mcast_querier);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->mcast_snooping >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_SNOOPING, b->mcast_snooping);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->vlan_filtering >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BR_VLAN_FILTERING, b->vlan_filtering);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->vlan_protocol >= 0) {
+ r = sd_netlink_message_append_u16(req, IFLA_BR_VLAN_PROTOCOL, htobe16(b->vlan_protocol));
+ if (r < 0)
+ return r;
+ }
+
+ if (b->stp >= 0) {
+ r = sd_netlink_message_append_u32(req, IFLA_BR_STP_STATE, b->stp);
+ if (r < 0)
+ return r;
+ }
+
+ if (b->igmp_version > 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_IGMP_VERSION, b->igmp_version);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int netdev_bridge_post_create(NetDev *netdev, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(netdev);
+
+ r = sd_rtnl_message_new_link(netdev->manager->rtnl, &req, RTM_NEWLINK, netdev->ifindex);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not allocate netlink message: %m");
+
+ r = sd_netlink_message_set_flags(req, NLM_F_REQUEST | NLM_F_ACK);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set netlink message flags: %m");
+
+ r = netdev_bridge_post_create_message(netdev, req);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not create netlink message: %m");
+
+ r = netlink_call_async(netdev->manager->rtnl, NULL, req, netdev_bridge_set_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not send netlink message: %m");
+
+ netdev_ref(netdev);
+
+ return r;
+}
+
+int config_parse_bridge_igmp_version(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Bridge *b = userdata;
+ uint8_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ b->igmp_version = 0; /* 0 means unset. */
+ return 0;
+ }
+
+ r = safe_atou8(rvalue, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse bridge's multicast IGMP version number '%s', ignoring assignment: %m",
+ rvalue);
+ return 0;
+ }
+ if (!IN_SET(u, 2, 3)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid bridge's multicast IGMP version number '%s', ignoring assignment.", rvalue);
+ return 0;
+ }
+
+ b->igmp_version = u;
+
+ return 0;
+}
+
+int config_parse_bridge_port_priority(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint16_t i;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ /* This is used in networkd-network-gperf.gperf. */
+
+ r = safe_atou16(rvalue, &i);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse bridge port priority, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (i > LINK_BRIDGE_PORT_PRIORITY_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Bridge port priority is larger than maximum %u, ignoring: %s",
+ LINK_BRIDGE_PORT_PRIORITY_MAX, rvalue);
+ return 0;
+ }
+
+ *((uint16_t *)data) = i;
+
+ return 0;
+}
+
+static void bridge_init(NetDev *n) {
+ Bridge *b;
+
+ b = BRIDGE(n);
+
+ assert(b);
+
+ b->mcast_querier = -1;
+ b->mcast_snooping = -1;
+ b->vlan_filtering = -1;
+ b->vlan_protocol = -1;
+ b->stp = -1;
+ b->default_pvid = VLANID_INVALID;
+ b->forward_delay = USEC_INFINITY;
+ b->ageing_time = USEC_INFINITY;
+}
+
+const NetDevVTable bridge_vtable = {
+ .object_size = sizeof(Bridge),
+ .init = bridge_init,
+ .sections = NETDEV_COMMON_SECTIONS "Bridge\0",
+ .post_create = netdev_bridge_post_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/bridge.h b/src/network/netdev/bridge.h
new file mode 100644
index 0000000..72dd3e4
--- /dev/null
+++ b/src/network/netdev/bridge.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "netdev.h"
+
+#define LINK_BRIDGE_PORT_PRIORITY_INVALID 128U
+#define LINK_BRIDGE_PORT_PRIORITY_MAX 63U
+
+typedef struct Bridge {
+ NetDev meta;
+
+ int mcast_querier;
+ int mcast_snooping;
+ int vlan_filtering;
+ int vlan_protocol;
+ int stp;
+ uint16_t priority;
+ uint16_t group_fwd_mask;
+ uint16_t default_pvid;
+ uint8_t igmp_version;
+
+ usec_t forward_delay;
+ usec_t hello_time;
+ usec_t max_age;
+ usec_t ageing_time;
+} Bridge;
+
+typedef enum MulticastRouter {
+ MULTICAST_ROUTER_NONE,
+ MULTICAST_ROUTER_TEMPORARY_QUERY,
+ MULTICAST_ROUTER_PERMANENT,
+ MULTICAST_ROUTER_TEMPORARY,
+ _MULTICAST_ROUTER_MAX,
+ _MULTICAST_ROUTER_INVALID = -EINVAL,
+} MulticastRouter;
+
+DEFINE_NETDEV_CAST(BRIDGE, Bridge);
+extern const NetDevVTable bridge_vtable;
+
+const char* multicast_router_to_string(MulticastRouter i) _const_;
+MulticastRouter multicast_router_from_string(const char *s) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_multicast_router);
+CONFIG_PARSER_PROTOTYPE(config_parse_bridge_igmp_version);
+CONFIG_PARSER_PROTOTYPE(config_parse_bridge_port_priority);
diff --git a/src/network/netdev/dummy.c b/src/network/netdev/dummy.c
new file mode 100644
index 0000000..00df1d2
--- /dev/null
+++ b/src/network/netdev/dummy.c
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/if_arp.h>
+
+#include "dummy.h"
+
+const NetDevVTable dummy_vtable = {
+ .object_size = sizeof(Dummy),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/dummy.h b/src/network/netdev/dummy.h
new file mode 100644
index 0000000..eafdf4b
--- /dev/null
+++ b/src/network/netdev/dummy.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "netdev.h"
+
+typedef struct Dummy {
+ NetDev meta;
+} Dummy;
+
+DEFINE_NETDEV_CAST(DUMMY, Dummy);
+extern const NetDevVTable dummy_vtable;
diff --git a/src/network/netdev/fou-tunnel.c b/src/network/netdev/fou-tunnel.c
new file mode 100644
index 0000000..acd609f
--- /dev/null
+++ b/src/network/netdev/fou-tunnel.c
@@ -0,0 +1,285 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/fou.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/ip.h>
+
+#include "conf-parser.h"
+#include "fou-tunnel.h"
+#include "ip-protocol-list.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "util.h"
+
+static const char* const fou_encap_type_table[_NETDEV_FOO_OVER_UDP_ENCAP_MAX] = {
+ [NETDEV_FOO_OVER_UDP_ENCAP_DIRECT] = "FooOverUDP",
+ [NETDEV_FOO_OVER_UDP_ENCAP_GUE] = "GenericUDPEncapsulation",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(fou_encap_type, FooOverUDPEncapType);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_fou_encap_type, fou_encap_type, FooOverUDPEncapType,
+ "Failed to parse Encapsulation=");
+
+static int netdev_fill_fou_tunnel_message(NetDev *netdev, sd_netlink_message *m) {
+ FouTunnel *t;
+ uint8_t encap_type;
+ int r;
+
+ assert_se(t = FOU(netdev));
+
+ r = sd_netlink_message_append_u16(m, FOU_ATTR_PORT, htobe16(t->port));
+ if (r < 0)
+ return r;
+
+ if (IN_SET(t->peer_family, AF_INET, AF_INET6)) {
+ r = sd_netlink_message_append_u16(m, FOU_ATTR_PEER_PORT, htobe16(t->peer_port));
+ if (r < 0)
+ return r;
+ }
+
+ switch (t->fou_encap_type) {
+ case NETDEV_FOO_OVER_UDP_ENCAP_DIRECT:
+ encap_type = FOU_ENCAP_DIRECT;
+ break;
+ case NETDEV_FOO_OVER_UDP_ENCAP_GUE:
+ encap_type = FOU_ENCAP_GUE;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ r = sd_netlink_message_append_u8(m, FOU_ATTR_TYPE, encap_type);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, FOU_ATTR_AF, AF_INET);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, FOU_ATTR_IPPROTO, t->fou_protocol);
+ if (r < 0)
+ return r;
+
+ if (t->local_family == AF_INET) {
+ r = sd_netlink_message_append_in_addr(m, FOU_ATTR_LOCAL_V4, &t->local.in);
+ if (r < 0)
+ return r;
+ } else if (t->local_family == AF_INET6) {
+ r = sd_netlink_message_append_in6_addr(m, FOU_ATTR_LOCAL_V6, &t->local.in6);
+ if (r < 0)
+ return r;
+ }
+
+ if (t->peer_family == AF_INET) {
+ r = sd_netlink_message_append_in_addr(m, FOU_ATTR_PEER_V4, &t->peer.in);
+ if (r < 0)
+ return r;
+ } else if (t->peer_family == AF_INET6){
+ r = sd_netlink_message_append_in6_addr(m, FOU_ATTR_PEER_V6, &t->peer.in6);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int netdev_create_fou_tunnel_message(NetDev *netdev, sd_netlink_message **ret) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+
+ r = sd_genl_message_new(netdev->manager->genl, FOU_GENL_NAME, FOU_CMD_ADD, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not allocate netlink message: %m");
+
+ r = netdev_fill_fou_tunnel_message(netdev, m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not create netlink message: %m");
+
+ *ret = TAKE_PTR(m);
+ return 0;
+}
+
+static int fou_tunnel_create_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
+ int r;
+
+ assert(netdev);
+ assert(netdev->state != _NETDEV_STATE_INVALID);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EEXIST)
+ log_netdev_info(netdev, "netdev exists, using existing without changing its parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "netdev could not be created: %m");
+ netdev_enter_failed(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "FooOverUDP tunnel is created");
+ return 1;
+}
+
+static int netdev_fou_tunnel_create(NetDev *netdev) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+ assert(FOU(netdev));
+
+ r = netdev_create_fou_tunnel_message(netdev, &m);
+ if (r < 0)
+ return r;
+
+ r = netlink_call_async(netdev->manager->genl, NULL, m, fou_tunnel_create_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create FooOverUDP tunnel: %m");
+
+ netdev_ref(netdev);
+ return 0;
+}
+
+int config_parse_ip_protocol(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint8_t *ret = ASSERT_PTR(data);
+ unsigned protocol;
+ /* linux/fou.h defines the netlink field as one byte, so we need to reject protocols numbers that
+ * don't fit in one byte. */
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = parse_ip_protocol(rvalue);
+ if (r >= 0)
+ protocol = r;
+ else {
+ r = safe_atou(rvalue, &protocol);
+ if (r < 0)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse IP protocol '%s' for FooOverUDP tunnel, "
+ "ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (protocol > UINT8_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "IP protocol '%s' for FooOverUDP tunnel out of range, "
+ "ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ *ret = protocol;
+ return 0;
+}
+
+int config_parse_fou_tunnel_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ union in_addr_union *addr = ASSERT_PTR(data);
+ FouTunnel *t = userdata;
+ int r, *f;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (streq(lvalue, "Local"))
+ f = &t->local_family;
+ else
+ f = &t->peer_family;
+
+ r = in_addr_from_string_auto(rvalue, f, addr);
+ if (r < 0)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "FooOverUDP tunnel '%s' address is invalid, ignoring assignment: %s",
+ lvalue, rvalue);
+
+ return 0;
+}
+
+static int netdev_fou_tunnel_verify(NetDev *netdev, const char *filename) {
+ FouTunnel *t;
+
+ assert(netdev);
+ assert(filename);
+
+ t = FOU(netdev);
+
+ assert(t);
+
+ switch (t->fou_encap_type) {
+ case NETDEV_FOO_OVER_UDP_ENCAP_DIRECT:
+ if (t->fou_protocol <= 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "FooOverUDP protocol not configured in %s. Rejecting configuration.",
+ filename);
+ break;
+ case NETDEV_FOO_OVER_UDP_ENCAP_GUE:
+ if (t->fou_protocol > 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "FooOverUDP GUE can't be set with protocol configured in %s. Rejecting configuration.",
+ filename);
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ if (t->peer_family == AF_UNSPEC && t->peer_port > 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "FooOverUDP peer port is set but peer address not configured in %s. Rejecting configuration.",
+ filename);
+ else if (t->peer_family != AF_UNSPEC && t->peer_port == 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "FooOverUDP peer port not set but peer address is configured in %s. Rejecting configuration.",
+ filename);
+ return 0;
+}
+
+static void fou_tunnel_init(NetDev *netdev) {
+ FouTunnel *t;
+
+ assert(netdev);
+
+ t = FOU(netdev);
+
+ assert(t);
+
+ t->fou_encap_type = NETDEV_FOO_OVER_UDP_ENCAP_DIRECT;
+}
+
+const NetDevVTable foutnl_vtable = {
+ .object_size = sizeof(FouTunnel),
+ .init = fou_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "FooOverUDP\0",
+ .create = netdev_fou_tunnel_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = netdev_fou_tunnel_verify,
+};
diff --git a/src/network/netdev/fou-tunnel.h b/src/network/netdev/fou-tunnel.h
new file mode 100644
index 0000000..576d82e
--- /dev/null
+++ b/src/network/netdev/fou-tunnel.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+#include <linux/fou.h>
+
+#include "in-addr-util.h"
+#include "netdev.h"
+
+typedef enum FooOverUDPEncapType {
+ NETDEV_FOO_OVER_UDP_ENCAP_UNSPEC = FOU_ENCAP_UNSPEC,
+ NETDEV_FOO_OVER_UDP_ENCAP_DIRECT = FOU_ENCAP_DIRECT,
+ NETDEV_FOO_OVER_UDP_ENCAP_GUE = FOU_ENCAP_GUE,
+ _NETDEV_FOO_OVER_UDP_ENCAP_MAX,
+ _NETDEV_FOO_OVER_UDP_ENCAP_INVALID = -EINVAL,
+} FooOverUDPEncapType;
+
+typedef struct FouTunnel {
+ NetDev meta;
+
+ uint8_t fou_protocol;
+
+ uint16_t port;
+ uint16_t peer_port;
+
+ int local_family;
+ int peer_family;
+
+ FooOverUDPEncapType fou_encap_type;
+ union in_addr_union local;
+ union in_addr_union peer;
+} FouTunnel;
+
+DEFINE_NETDEV_CAST(FOU, FouTunnel);
+extern const NetDevVTable foutnl_vtable;
+
+const char *fou_encap_type_to_string(FooOverUDPEncapType d) _const_;
+FooOverUDPEncapType fou_encap_type_from_string(const char *d) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_fou_encap_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_ip_protocol);
+CONFIG_PARSER_PROTOTYPE(config_parse_fou_tunnel_address);
diff --git a/src/network/netdev/geneve.c b/src/network/netdev/geneve.c
new file mode 100644
index 0000000..777a32d
--- /dev/null
+++ b/src/network/netdev/geneve.c
@@ -0,0 +1,299 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "extract-word.h"
+#include "geneve.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+#define GENEVE_FLOW_LABEL_MAX_MASK 0xFFFFFU
+#define DEFAULT_GENEVE_DESTINATION_PORT 6081
+
+static const char* const geneve_df_table[_NETDEV_GENEVE_DF_MAX] = {
+ [NETDEV_GENEVE_DF_NO] = "no",
+ [NETDEV_GENEVE_DF_YES] = "yes",
+ [NETDEV_GENEVE_DF_INHERIT] = "inherit",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(geneve_df, GeneveDF, NETDEV_GENEVE_DF_YES);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_geneve_df, geneve_df, GeneveDF, "Failed to parse Geneve IPDoNotFragment= setting");
+
+static int netdev_geneve_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ Geneve *v;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ v = GENEVE(netdev);
+
+ if (v->id <= GENEVE_VID_MAX) {
+ r = sd_netlink_message_append_u32(m, IFLA_GENEVE_ID, v->id);
+ if (r < 0)
+ return r;
+ }
+
+ if (in_addr_is_set(v->remote_family, &v->remote)) {
+ if (v->remote_family == AF_INET)
+ r = sd_netlink_message_append_in_addr(m, IFLA_GENEVE_REMOTE, &v->remote.in);
+ else
+ r = sd_netlink_message_append_in6_addr(m, IFLA_GENEVE_REMOTE6, &v->remote.in6);
+ if (r < 0)
+ return r;
+ }
+
+ if (v->inherit) {
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TTL_INHERIT, 1);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TTL, v->ttl);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TOS, v->tos);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_CSUM, v->udpcsum);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_TX, v->udp6zerocsumtx);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_RX, v->udp6zerocsumrx);
+ if (r < 0)
+ return r;
+
+ if (v->dest_port != DEFAULT_GENEVE_DESTINATION_PORT) {
+ r = sd_netlink_message_append_u16(m, IFLA_GENEVE_PORT, htobe16(v->dest_port));
+ if (r < 0)
+ return r;
+ }
+
+ if (v->flow_label > 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_GENEVE_LABEL, htobe32(v->flow_label));
+ if (r < 0)
+ return r;
+ }
+
+ if (v->geneve_df != _NETDEV_GENEVE_DF_INVALID) {
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_DF, v->geneve_df);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int config_parse_geneve_vni(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Geneve *v = userdata;
+ uint32_t f;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou32(rvalue, &f);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse Geneve VNI '%s'.", rvalue);
+ return 0;
+ }
+
+ if (f > GENEVE_VID_MAX){
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Geneve VNI out is of range '%s'.", rvalue);
+ return 0;
+ }
+
+ v->id = f;
+
+ return 0;
+}
+
+int config_parse_geneve_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Geneve *v = userdata;
+ union in_addr_union *addr = data, buffer;
+ int r, f;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = in_addr_from_string_auto(rvalue, &f, &buffer);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "geneve '%s' address is invalid, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ r = in_addr_is_multicast(f, &buffer);
+ if (r > 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "geneve invalid multicast '%s' address, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ v->remote_family = f;
+ *addr = buffer;
+
+ return 0;
+}
+
+int config_parse_geneve_flow_label(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Geneve *v = userdata;
+ uint32_t f;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou32(rvalue, &f);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse Geneve flow label '%s'.", rvalue);
+ return 0;
+ }
+
+ if (f & ~GENEVE_FLOW_LABEL_MAX_MASK) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Geneve flow label '%s' not valid. Flow label range should be [0-1048575].", rvalue);
+ return 0;
+ }
+
+ v->flow_label = f;
+
+ return 0;
+}
+
+int config_parse_geneve_ttl(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Geneve *v = userdata;
+ unsigned f;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(rvalue, "inherit"))
+ v->inherit = true;
+ else {
+ r = safe_atou(rvalue, &f);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse Geneve TTL '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (f > 255) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid Geneve TTL '%s'. TTL must be <= 255. Ignoring assignment.", rvalue);
+ return 0;
+ }
+
+ v->ttl = f;
+ }
+
+ return 0;
+}
+
+static int netdev_geneve_verify(NetDev *netdev, const char *filename) {
+ Geneve *v = GENEVE(netdev);
+
+ assert(netdev);
+ assert(v);
+ assert(filename);
+
+ if (v->id > GENEVE_VID_MAX)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: Geneve without valid VNI (or Virtual Network Identifier) configured. Ignoring.",
+ filename);
+
+ return 0;
+}
+
+static void geneve_init(NetDev *netdev) {
+ Geneve *v;
+
+ assert(netdev);
+
+ v = GENEVE(netdev);
+
+ assert(v);
+
+ v->id = GENEVE_VID_MAX + 1;
+ v->geneve_df = _NETDEV_GENEVE_DF_INVALID;
+ v->dest_port = DEFAULT_GENEVE_DESTINATION_PORT;
+ v->udpcsum = false;
+ v->udp6zerocsumtx = false;
+ v->udp6zerocsumrx = false;
+}
+
+const NetDevVTable geneve_vtable = {
+ .object_size = sizeof(Geneve),
+ .init = geneve_init,
+ .sections = NETDEV_COMMON_SECTIONS "GENEVE\0",
+ .fill_message_create = netdev_geneve_fill_message_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = netdev_geneve_verify,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/geneve.h b/src/network/netdev/geneve.h
new file mode 100644
index 0000000..1f0f15c
--- /dev/null
+++ b/src/network/netdev/geneve.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct Geneve Geneve;
+
+#include "in-addr-util.h"
+#include "netdev.h"
+#include "networkd-network.h"
+
+#define GENEVE_VID_MAX (1u << 24) - 1
+
+typedef enum GeneveDF {
+ NETDEV_GENEVE_DF_NO = GENEVE_DF_UNSET,
+ NETDEV_GENEVE_DF_YES = GENEVE_DF_SET,
+ NETDEV_GENEVE_DF_INHERIT = GENEVE_DF_INHERIT,
+ _NETDEV_GENEVE_DF_MAX,
+ _NETDEV_GENEVE_DF_INVALID = -EINVAL,
+} GeneveDF;
+
+struct Geneve {
+ NetDev meta;
+
+ uint32_t id;
+ uint32_t flow_label;
+
+ int remote_family;
+
+ uint8_t tos;
+ uint8_t ttl;
+
+ uint16_t dest_port;
+
+ bool udpcsum;
+ bool udp6zerocsumtx;
+ bool udp6zerocsumrx;
+ bool inherit;
+
+ GeneveDF geneve_df;
+ union in_addr_union remote;
+};
+
+DEFINE_NETDEV_CAST(GENEVE, Geneve);
+extern const NetDevVTable geneve_vtable;
+
+const char *geneve_df_to_string(GeneveDF d) _const_;
+GeneveDF geneve_df_from_string(const char *d) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_geneve_vni);
+CONFIG_PARSER_PROTOTYPE(config_parse_geneve_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_geneve_flow_label);
+CONFIG_PARSER_PROTOTYPE(config_parse_geneve_df);
+CONFIG_PARSER_PROTOTYPE(config_parse_geneve_ttl);
diff --git a/src/network/netdev/ifb.c b/src/network/netdev/ifb.c
new file mode 100644
index 0000000..d7ff44c
--- /dev/null
+++ b/src/network/netdev/ifb.c
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/if_arp.h>
+
+#include "ifb.h"
+
+const NetDevVTable ifb_vtable = {
+ .object_size = sizeof(IntermediateFunctionalBlock),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/ifb.h b/src/network/netdev/ifb.h
new file mode 100644
index 0000000..badfb4a
--- /dev/null
+++ b/src/network/netdev/ifb.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#pragma once
+
+#include "netdev.h"
+
+typedef struct IntermediateFunctionalBlock {
+ NetDev meta;
+} IntermediateFunctionalBlock;
+
+DEFINE_NETDEV_CAST(IFB, IntermediateFunctionalBlock);
+extern const NetDevVTable ifb_vtable;
diff --git a/src/network/netdev/ipoib.c b/src/network/netdev/ipoib.c
new file mode 100644
index 0000000..5dd9286
--- /dev/null
+++ b/src/network/netdev/ipoib.c
@@ -0,0 +1,161 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/if_arp.h>
+#include <linux/if_link.h>
+
+#include "ipoib.h"
+#include "networkd-network.h"
+#include "parse-util.h"
+#include "string-table.h"
+
+assert_cc((int) IP_OVER_INFINIBAND_MODE_DATAGRAM == (int) IPOIB_MODE_DATAGRAM);
+assert_cc((int) IP_OVER_INFINIBAND_MODE_CONNECTED == (int) IPOIB_MODE_CONNECTED);
+
+static void netdev_ipoib_init(NetDev *netdev) {
+ IPoIB *ipoib;
+
+ assert(netdev);
+
+ ipoib = IPOIB(netdev);
+
+ assert(ipoib);
+
+ ipoib->mode = _IP_OVER_INFINIBAND_MODE_INVALID;
+ ipoib->umcast = -1;
+}
+
+static int netdev_ipoib_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ IPoIB *ipoib;
+ int r;
+
+ assert(netdev);
+ assert(link);
+ assert(m);
+
+ ipoib = IPOIB(netdev);
+
+ assert(ipoib);
+
+ if (ipoib->pkey > 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_IPOIB_PKEY, ipoib->pkey);
+ if (r < 0)
+ return r;
+ }
+
+ if (ipoib->mode >= 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_IPOIB_MODE, ipoib->mode);
+ if (r < 0)
+ return r;
+ }
+
+ if (ipoib->umcast >= 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_IPOIB_UMCAST, ipoib->umcast);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int ipoib_set_netlink_message(Link *link, sd_netlink_message *m) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(m);
+
+ r = sd_netlink_message_set_flags(m, NLM_F_REQUEST | NLM_F_ACK);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, link->kind);
+ if (r < 0)
+ return r;
+
+ if (link->network->ipoib_mode >= 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_IPOIB_MODE, link->network->ipoib_mode);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->ipoib_umcast >= 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_IPOIB_UMCAST, link->network->ipoib_umcast);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static const char * const ipoib_mode_table[_IP_OVER_INFINIBAND_MODE_MAX] = {
+ [IP_OVER_INFINIBAND_MODE_DATAGRAM] = "datagram",
+ [IP_OVER_INFINIBAND_MODE_CONNECTED] = "connected",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(ipoib_mode, IPoIBMode);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_ipoib_mode, ipoib_mode, IPoIBMode, "Failed to parse IPoIB mode");
+
+int config_parse_ipoib_pkey(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint16_t u, *pkey = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *pkey = 0; /* 0 means unset. */
+ return 0;
+ }
+
+ r = safe_atou16(rvalue, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse IPoIB pkey '%s', ignoring assignment: %m",
+ rvalue);
+ return 0;
+ }
+ if (IN_SET(u, 0, 0x8000)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "IPoIB pkey cannot be 0 nor 0x8000, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ *pkey = u;
+ return 0;
+}
+
+
+const NetDevVTable ipoib_vtable = {
+ .object_size = sizeof(IPoIB),
+ .sections = NETDEV_COMMON_SECTIONS "IPoIB\0",
+ .init = netdev_ipoib_init,
+ .fill_message_create = netdev_ipoib_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .iftype = ARPHRD_INFINIBAND,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/ipoib.h b/src/network/netdev/ipoib.h
new file mode 100644
index 0000000..415d3b1
--- /dev/null
+++ b/src/network/netdev/ipoib.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <errno.h>
+
+#include "conf-parser.h"
+#include "netdev.h"
+
+typedef enum IPoIBMode {
+ IP_OVER_INFINIBAND_MODE_DATAGRAM,
+ IP_OVER_INFINIBAND_MODE_CONNECTED,
+ _IP_OVER_INFINIBAND_MODE_MAX,
+ _IP_OVER_INFINIBAND_MODE_INVALID = -EINVAL,
+} IPoIBMode;
+
+typedef struct IPoIB {
+ NetDev meta;
+
+ uint16_t pkey;
+ IPoIBMode mode;
+ int umcast;
+} IPoIB;
+
+DEFINE_NETDEV_CAST(IPOIB, IPoIB);
+extern const NetDevVTable ipoib_vtable;
+
+int ipoib_set_netlink_message(Link *link, sd_netlink_message *m);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipoib_pkey);
+CONFIG_PARSER_PROTOTYPE(config_parse_ipoib_mode);
diff --git a/src/network/netdev/ipvlan.c b/src/network/netdev/ipvlan.c
new file mode 100644
index 0000000..058eade
--- /dev/null
+++ b/src/network/netdev/ipvlan.c
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "conf-parser.h"
+#include "ipvlan.h"
+#include "ipvlan-util.h"
+#include "networkd-link.h"
+#include "string-util.h"
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_ipvlan_mode, ipvlan_mode, IPVlanMode, "Failed to parse ipvlan mode");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_ipvlan_flags, ipvlan_flags, IPVlanFlags, "Failed to parse ipvlan flags");
+
+static int netdev_ipvlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *req) {
+ IPVlan *m;
+ int r;
+
+ assert(netdev);
+ assert(link);
+ assert(netdev->ifname);
+
+ if (netdev->kind == NETDEV_KIND_IPVLAN)
+ m = IPVLAN(netdev);
+ else
+ m = IPVTAP(netdev);
+
+ assert(m);
+
+ if (m->mode != _NETDEV_IPVLAN_MODE_INVALID) {
+ r = sd_netlink_message_append_u16(req, IFLA_IPVLAN_MODE, m->mode);
+ if (r < 0)
+ return r;
+ }
+
+ if (m->flags != _NETDEV_IPVLAN_FLAGS_INVALID) {
+ r = sd_netlink_message_append_u16(req, IFLA_IPVLAN_FLAGS, m->flags);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static void ipvlan_init(NetDev *n) {
+ IPVlan *m;
+
+ assert(n);
+
+ if (n->kind == NETDEV_KIND_IPVLAN)
+ m = IPVLAN(n);
+ else
+ m = IPVTAP(n);
+
+ assert(m);
+
+ m->mode = _NETDEV_IPVLAN_MODE_INVALID;
+ m->flags = _NETDEV_IPVLAN_FLAGS_INVALID;
+}
+
+const NetDevVTable ipvlan_vtable = {
+ .object_size = sizeof(IPVlan),
+ .init = ipvlan_init,
+ .sections = NETDEV_COMMON_SECTIONS "IPVLAN\0",
+ .fill_message_create = netdev_ipvlan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
+
+const NetDevVTable ipvtap_vtable = {
+ .object_size = sizeof(IPVlan),
+ .init = ipvlan_init,
+ .sections = NETDEV_COMMON_SECTIONS "IPVTAP\0",
+ .fill_message_create = netdev_ipvlan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
+
+IPVlanMode link_get_ipvlan_mode(Link *link) {
+ IPVlan *ipvlan;
+
+ assert(link);
+
+ ipvlan = IPVLAN(link->netdev);
+ if (!ipvlan)
+ return _NETDEV_IPVLAN_MODE_INVALID;
+
+ return ipvlan->mode;
+}
diff --git a/src/network/netdev/ipvlan.h b/src/network/netdev/ipvlan.h
new file mode 100644
index 0000000..633b0bd
--- /dev/null
+++ b/src/network/netdev/ipvlan.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+#include <linux/if_link.h>
+
+#include "ipvlan-util.h"
+#include "netdev.h"
+
+typedef struct IPVlan {
+ NetDev meta;
+
+ IPVlanMode mode;
+ IPVlanFlags flags;
+} IPVlan;
+
+DEFINE_NETDEV_CAST(IPVLAN, IPVlan);
+DEFINE_NETDEV_CAST(IPVTAP, IPVlan);
+extern const NetDevVTable ipvlan_vtable;
+extern const NetDevVTable ipvtap_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipvlan_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_ipvlan_flags);
+
+IPVlanMode link_get_ipvlan_mode(Link *link);
diff --git a/src/network/netdev/l2tp-tunnel.c b/src/network/netdev/l2tp-tunnel.c
new file mode 100644
index 0000000..fd2783e
--- /dev/null
+++ b/src/network/netdev/l2tp-tunnel.c
@@ -0,0 +1,880 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/l2tp.h>
+#include <linux/genetlink.h>
+
+#include "conf-parser.h"
+#include "hashmap.h"
+#include "l2tp-tunnel.h"
+#include "netlink-util.h"
+#include "networkd-address.h"
+#include "networkd-manager.h"
+#include "networkd-route-util.h"
+#include "parse-util.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "util.h"
+
+static const char* const l2tp_l2spec_type_table[_NETDEV_L2TP_L2SPECTYPE_MAX] = {
+ [NETDEV_L2TP_L2SPECTYPE_NONE] = "none",
+ [NETDEV_L2TP_L2SPECTYPE_DEFAULT] = "default",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(l2tp_l2spec_type, L2tpL2specType);
+
+static const char* const l2tp_encap_type_table[_NETDEV_L2TP_ENCAPTYPE_MAX] = {
+ [NETDEV_L2TP_ENCAPTYPE_UDP] = "udp",
+ [NETDEV_L2TP_ENCAPTYPE_IP] = "ip",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(l2tp_encap_type, L2tpEncapType);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_l2tp_encap_type, l2tp_encap_type, L2tpEncapType, "Failed to parse L2TP Encapsulation Type");
+
+static const char* const l2tp_local_address_type_table[_NETDEV_L2TP_LOCAL_ADDRESS_MAX] = {
+ [NETDEV_L2TP_LOCAL_ADDRESS_AUTO] = "auto",
+ [NETDEV_L2TP_LOCAL_ADDRESS_STATIC] = "static",
+ [NETDEV_L2TP_LOCAL_ADDRESS_DYNAMIC] = "dynamic",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(l2tp_local_address_type, L2tpLocalAddressType);
+
+static L2tpSession* l2tp_session_free(L2tpSession *s) {
+ if (!s)
+ return NULL;
+
+ if (s->tunnel && s->section)
+ ordered_hashmap_remove(s->tunnel->sessions_by_section, s->section);
+
+ config_section_free(s->section);
+ free(s->name);
+ return mfree(s);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(L2tpSession, l2tp_session_free);
+
+static int l2tp_session_new_static(L2tpTunnel *t, const char *filename, unsigned section_line, L2tpSession **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(l2tp_session_freep) L2tpSession *s = NULL;
+ int r;
+
+ assert(t);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ s = ordered_hashmap_get(t->sessions_by_section, n);
+ if (s) {
+ *ret = TAKE_PTR(s);
+ return 0;
+ }
+
+ s = new(L2tpSession, 1);
+ if (!s)
+ return -ENOMEM;
+
+ *s = (L2tpSession) {
+ .l2tp_l2spec_type = NETDEV_L2TP_L2SPECTYPE_DEFAULT,
+ .tunnel = t,
+ .section = TAKE_PTR(n),
+ };
+
+ r = ordered_hashmap_ensure_put(&t->sessions_by_section, &config_section_hash_ops, s->section, s);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(s);
+ return 0;
+}
+
+static int netdev_l2tp_create_message_tunnel(NetDev *netdev, union in_addr_union *local_address, sd_netlink_message **ret) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ uint16_t encap_type;
+ L2tpTunnel *t;
+ int r;
+
+ assert(netdev);
+ assert(local_address);
+ assert_se(t = L2TP(netdev));
+
+ r = sd_genl_message_new(netdev->manager->genl, L2TP_GENL_NAME, L2TP_CMD_TUNNEL_CREATE, &m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_CONN_ID, t->tunnel_id);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_CONN_ID, t->peer_tunnel_id);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, L2TP_ATTR_PROTO_VERSION, 3);
+ if (r < 0)
+ return r;
+
+ switch (t->l2tp_encap_type) {
+ case NETDEV_L2TP_ENCAPTYPE_IP:
+ encap_type = L2TP_ENCAPTYPE_IP;
+ break;
+ case NETDEV_L2TP_ENCAPTYPE_UDP:
+ default:
+ encap_type = L2TP_ENCAPTYPE_UDP;
+ break;
+ }
+
+ r = sd_netlink_message_append_u16(m, L2TP_ATTR_ENCAP_TYPE, encap_type);
+ if (r < 0)
+ return r;
+
+ if (t->family == AF_INET) {
+ r = sd_netlink_message_append_in_addr(m, L2TP_ATTR_IP_SADDR, &local_address->in);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in_addr(m, L2TP_ATTR_IP_DADDR, &t->remote.in);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_netlink_message_append_in6_addr(m, L2TP_ATTR_IP6_SADDR, &local_address->in6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in6_addr(m, L2TP_ATTR_IP6_DADDR, &t->remote.in6);
+ if (r < 0)
+ return r;
+ }
+
+ if (encap_type == L2TP_ENCAPTYPE_UDP) {
+ r = sd_netlink_message_append_u16(m, L2TP_ATTR_UDP_SPORT, t->l2tp_udp_sport);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, L2TP_ATTR_UDP_DPORT, t->l2tp_udp_dport);
+ if (r < 0)
+ return r;
+
+ if (t->udp_csum) {
+ r = sd_netlink_message_append_u8(m, L2TP_ATTR_UDP_CSUM, t->udp_csum);
+ if (r < 0)
+ return r;
+ }
+
+ if (t->udp6_csum_tx) {
+ r = sd_netlink_message_append_flag(m, L2TP_ATTR_UDP_ZERO_CSUM6_TX);
+ if (r < 0)
+ return r;
+ }
+
+ if (t->udp6_csum_rx) {
+ r = sd_netlink_message_append_flag(m, L2TP_ATTR_UDP_ZERO_CSUM6_RX);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ *ret = TAKE_PTR(m);
+
+ return 0;
+}
+
+static int netdev_l2tp_create_message_session(NetDev *netdev, L2tpSession *session, sd_netlink_message **ret) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ uint16_t l2_spec_len;
+ uint8_t l2_spec_type;
+ int r;
+
+ assert(netdev);
+ assert(session);
+ assert(session->tunnel);
+
+ r = sd_genl_message_new(netdev->manager->genl, L2TP_GENL_NAME, L2TP_CMD_SESSION_CREATE, &m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_CONN_ID, session->tunnel->tunnel_id);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_CONN_ID, session->tunnel->peer_tunnel_id);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_SESSION_ID, session->session_id);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_SESSION_ID, session->peer_session_id);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, L2TP_ATTR_PW_TYPE, L2TP_PWTYPE_ETH);
+ if (r < 0)
+ return r;
+
+ switch (session->l2tp_l2spec_type) {
+ case NETDEV_L2TP_L2SPECTYPE_NONE:
+ l2_spec_type = L2TP_L2SPECTYPE_NONE;
+ l2_spec_len = 0;
+ break;
+ case NETDEV_L2TP_L2SPECTYPE_DEFAULT:
+ default:
+ l2_spec_type = L2TP_L2SPECTYPE_DEFAULT;
+ l2_spec_len = 4;
+ break;
+ }
+
+ r = sd_netlink_message_append_u8(m, L2TP_ATTR_L2SPEC_TYPE, l2_spec_type);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, L2TP_ATTR_L2SPEC_LEN, l2_spec_len);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_string(m, L2TP_ATTR_IFNAME, session->name);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(m);
+
+ return 0;
+}
+
+static int link_get_l2tp_local_address(Link *link, L2tpTunnel *t, union in_addr_union *ret) {
+ Address *a;
+
+ assert(link);
+ assert(t);
+
+ SET_FOREACH(a, link->addresses) {
+ if (!address_is_ready(a))
+ continue;
+
+ if (a->family != t->family)
+ continue;
+
+ if (in_addr_is_set(a->family, &a->in_addr_peer))
+ continue;
+
+ if (t->local_address_type == NETDEV_L2TP_LOCAL_ADDRESS_STATIC &&
+ !FLAGS_SET(a->flags, IFA_F_PERMANENT))
+ continue;
+
+ if (t->local_address_type == NETDEV_L2TP_LOCAL_ADDRESS_DYNAMIC &&
+ FLAGS_SET(a->flags, IFA_F_PERMANENT))
+ continue;
+
+ if (ret)
+ *ret = a->in_addr;
+ }
+
+ return -ENOENT;
+}
+
+static int l2tp_get_local_address(NetDev *netdev, union in_addr_union *ret) {
+ Link *link = NULL;
+ L2tpTunnel *t;
+ Address *a = NULL;
+ int r;
+
+ assert(netdev);
+ assert(netdev->manager);
+ assert_se(t = L2TP(netdev));
+
+ if (t->local_ifname) {
+ r = link_get_by_name(netdev->manager, t->local_ifname, &link);
+ if (r < 0)
+ return r;
+
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false))
+ return -EBUSY;
+ }
+
+ if (netdev->manager->manage_foreign_routes) {
+ /* First, check if the remote address is accessible. */
+ if (link)
+ r = link_address_is_reachable(link, t->family, &t->remote, &t->local, &a);
+ else
+ r = manager_address_is_reachable(netdev->manager, t->family, &t->remote, &t->local, &a);
+ if (r < 0)
+ return r;
+ }
+
+ if (in_addr_is_set(t->family, &t->local)) {
+ /* local address is explicitly specified. */
+
+ if (!a) {
+ if (link)
+ r = link_get_address(link, t->family, &t->local, 0, &a);
+ else
+ r = manager_get_address(netdev->manager, t->family, &t->local, 0, &a);
+ if (r < 0)
+ return r;
+
+ if (!address_is_ready(a))
+ return -EBUSY;
+ }
+
+ if (ret)
+ *ret = a->in_addr;
+
+ return 0;
+ }
+
+ if (a) {
+ if (t->local_address_type == NETDEV_L2TP_LOCAL_ADDRESS_STATIC &&
+ !FLAGS_SET(a->flags, IFA_F_PERMANENT))
+ return -EINVAL;
+
+ if (t->local_address_type == NETDEV_L2TP_LOCAL_ADDRESS_DYNAMIC &&
+ FLAGS_SET(a->flags, IFA_F_PERMANENT))
+ return -EINVAL;
+
+ if (ret)
+ *ret = a->in_addr;
+
+ return 0;
+ }
+
+ if (link)
+ return link_get_l2tp_local_address(link, t, ret);
+
+ HASHMAP_FOREACH(link, netdev->manager->links_by_index) {
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false))
+ continue;
+
+ if (link_get_l2tp_local_address(link, t, ret) >= 0)
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static void l2tp_session_destroy_callback(L2tpSession *session) {
+ if (!session)
+ return;
+
+ netdev_unref(NETDEV(session->tunnel));
+}
+
+static int l2tp_create_session_handler(sd_netlink *rtnl, sd_netlink_message *m, L2tpSession *session) {
+ NetDev *netdev;
+ int r;
+
+ assert(session);
+ assert(session->tunnel);
+
+ netdev = NETDEV(session->tunnel);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EEXIST)
+ log_netdev_info(netdev, "L2TP session %s exists, using existing without changing its parameters",
+ session->name);
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "L2TP session %s could not be created: %m", session->name);
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "L2TP session %s created", session->name);
+ return 1;
+}
+
+static int l2tp_create_session(NetDev *netdev, L2tpSession *session) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *n = NULL;
+ int r;
+
+ r = netdev_l2tp_create_message_session(netdev, session, &n);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m");
+
+ r = netlink_call_async(netdev->manager->genl, NULL, n, l2tp_create_session_handler,
+ l2tp_session_destroy_callback, session);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create L2TP session %s: %m", session->name);
+
+ netdev_ref(netdev);
+ return 0;
+}
+
+static int l2tp_create_tunnel_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
+ L2tpSession *session;
+ L2tpTunnel *t;
+ int r;
+
+ assert(netdev);
+ assert(netdev->state != _NETDEV_STATE_INVALID);
+
+ t = L2TP(netdev);
+
+ assert(t);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EEXIST)
+ log_netdev_info(netdev, "netdev exists, using existing without changing its parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "netdev could not be created: %m");
+ netdev_enter_failed(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "L2TP tunnel is created");
+
+ ORDERED_HASHMAP_FOREACH(session, t->sessions_by_section)
+ (void) l2tp_create_session(netdev, session);
+
+ return 1;
+}
+
+static int l2tp_create_tunnel(NetDev *netdev) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ union in_addr_union local_address;
+ L2tpTunnel *t;
+ int r;
+
+ assert(netdev);
+ assert_se(t = L2TP(netdev));
+
+ r = l2tp_get_local_address(netdev, &local_address);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not find local address.");
+
+ if (t->local_address_type >= 0 && DEBUG_LOGGING)
+ log_netdev_debug(netdev, "Local address %s acquired.",
+ IN_ADDR_TO_STRING(t->family, &local_address));
+
+ r = netdev_l2tp_create_message_tunnel(netdev, &local_address, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m");
+
+ r = netlink_call_async(netdev->manager->genl, NULL, m, l2tp_create_tunnel_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create L2TP tunnel: %m");
+
+ netdev_ref(netdev);
+
+ return 0;
+}
+
+static int netdev_l2tp_is_ready_to_create(NetDev *netdev, Link *link) {
+ return l2tp_get_local_address(netdev, NULL) >= 0;
+}
+
+int config_parse_l2tp_tunnel_local_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *addr_or_type = NULL, *ifname = NULL;
+ L2tpLocalAddressType type;
+ L2tpTunnel *t = ASSERT_PTR(userdata);
+ const char *p = ASSERT_PTR(rvalue);
+ union in_addr_union a;
+ int r, f;
+
+ assert(filename);
+ assert(lvalue);
+
+ if (isempty(rvalue)) {
+ t->local_ifname = mfree(t->local_ifname);
+ t->local_address_type = NETDEV_L2TP_LOCAL_ADDRESS_AUTO;
+ t->local = IN_ADDR_NULL;
+
+ if (!in_addr_is_set(t->family, &t->remote))
+ /* If Remote= is not specified yet, then also clear family. */
+ t->family = AF_UNSPEC;
+
+ return 0;
+ }
+
+ r = extract_first_word(&p, &addr_or_type, "@", 0);
+ if (r < 0)
+ return log_oom();
+ if (r == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid L2TP Tunnel address specified in %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ if (!isempty(p)) {
+ if (!ifname_valid_full(p, IFNAME_VALID_ALTERNATIVE)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid interface name specified in %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ ifname = strdup(p);
+ if (!ifname)
+ return log_oom();
+ }
+
+ type = l2tp_local_address_type_from_string(addr_or_type);
+ if (type >= 0) {
+ free_and_replace(t->local_ifname, ifname);
+ t->local_address_type = type;
+ t->local = IN_ADDR_NULL;
+
+ if (!in_addr_is_set(t->family, &t->remote))
+ /* If Remote= is not specified yet, then also clear family. */
+ t->family = AF_UNSPEC;
+
+ return 0;
+ }
+
+ r = in_addr_from_string_auto(addr_or_type, &f, &a);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid L2TP Tunnel local address \"%s\" specified, ignoring assignment: %s", addr_or_type, rvalue);
+ return 0;
+ }
+
+ if (in_addr_is_null(f, &a)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "L2TP Tunnel local address cannot be null, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (t->family != AF_UNSPEC && t->family != f) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Address family does not match the previous assignment, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ t->family = f;
+ t->local = a;
+ free_and_replace(t->local_ifname, ifname);
+ t->local_address_type = _NETDEV_L2TP_LOCAL_ADDRESS_INVALID;
+ return 0;
+}
+
+int config_parse_l2tp_tunnel_remote_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ L2tpTunnel *t = ASSERT_PTR(userdata);
+ union in_addr_union a;
+ int r, f;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ t->remote = IN_ADDR_NULL;
+
+ if (!in_addr_is_set(t->family, &t->local))
+ /* If Local= is not specified yet, then also clear family. */
+ t->family = AF_UNSPEC;
+
+ return 0;
+ }
+
+ r = in_addr_from_string_auto(rvalue, &f, &a);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid L2TP Tunnel remote address specified, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (in_addr_is_null(f, &a)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "L2TP Tunnel remote address cannot be null, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (t->family != AF_UNSPEC && t->family != f) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Address family does not match the previous assignment, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ t->family = f;
+ t->remote = a;
+ return 0;
+}
+
+int config_parse_l2tp_tunnel_id(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint32_t *id = data, k;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou32(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse L2TP tunnel id. Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (k == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid L2TP tunnel id. Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ *id = k;
+
+ return 0;
+}
+
+int config_parse_l2tp_session_id(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(l2tp_session_free_or_set_invalidp) L2tpSession *session = NULL;
+ L2tpTunnel *t = userdata;
+ uint32_t k;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = l2tp_session_new_static(t, filename, section_line, &session);
+ if (r < 0)
+ return log_oom();
+
+ r = safe_atou32(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse L2TP session id. Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (k == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid L2TP session id. Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "SessionId"))
+ session->session_id = k;
+ else
+ session->peer_session_id = k;
+
+ session = NULL;
+ return 0;
+}
+
+int config_parse_l2tp_session_l2spec(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(l2tp_session_free_or_set_invalidp) L2tpSession *session = NULL;
+ L2tpTunnel *t = userdata;
+ L2tpL2specType spec;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = l2tp_session_new_static(t, filename, section_line, &session);
+ if (r < 0)
+ return log_oom();
+
+ spec = l2tp_l2spec_type_from_string(rvalue);
+ if (spec < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, spec,
+ "Failed to parse layer2 specific header type. Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ session->l2tp_l2spec_type = spec;
+
+ session = NULL;
+ return 0;
+}
+
+int config_parse_l2tp_session_name(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(l2tp_session_free_or_set_invalidp) L2tpSession *session = NULL;
+ L2tpTunnel *t = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = l2tp_session_new_static(t, filename, section_line, &session);
+ if (r < 0)
+ return log_oom();
+
+ if (!ifname_valid(rvalue)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse L2TP tunnel session name. Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ r = free_and_strdup(&session->name, rvalue);
+ if (r < 0)
+ return log_oom();
+
+ session = NULL;
+ return 0;
+}
+
+static void l2tp_tunnel_init(NetDev *netdev) {
+ L2tpTunnel *t;
+
+ assert(netdev);
+
+ t = L2TP(netdev);
+
+ assert(t);
+
+ t->l2tp_encap_type = NETDEV_L2TP_ENCAPTYPE_UDP;
+ t->udp6_csum_rx = true;
+ t->udp6_csum_tx = true;
+}
+
+static int l2tp_session_verify(L2tpSession *session) {
+ NetDev *netdev;
+
+ assert(session);
+ assert(session->tunnel);
+
+ netdev = NETDEV(session->tunnel);
+
+ if (section_is_invalid(session->section))
+ return -EINVAL;
+
+ if (!session->name)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: L2TP session without name configured. "
+ "Ignoring [L2TPSession] section from line %u",
+ session->section->filename, session->section->line);
+
+ if (session->session_id == 0 || session->peer_session_id == 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: L2TP session without session IDs configured. "
+ "Ignoring [L2TPSession] section from line %u",
+ session->section->filename, session->section->line);
+
+ return 0;
+}
+
+static int netdev_l2tp_tunnel_verify(NetDev *netdev, const char *filename) {
+ L2tpTunnel *t;
+ L2tpSession *session;
+
+ assert(netdev);
+ assert(filename);
+
+ t = L2TP(netdev);
+
+ assert(t);
+
+ if (!IN_SET(t->family, AF_INET, AF_INET6))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: L2TP tunnel with invalid address family configured. Ignoring",
+ filename);
+
+ if (!in_addr_is_set(t->family, &t->remote))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: L2TP tunnel without a remote address configured. Ignoring",
+ filename);
+
+ if (t->tunnel_id == 0 || t->peer_tunnel_id == 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: L2TP tunnel without tunnel IDs configured. Ignoring",
+ filename);
+
+ ORDERED_HASHMAP_FOREACH(session, t->sessions_by_section)
+ if (l2tp_session_verify(session) < 0)
+ l2tp_session_free(session);
+
+ return 0;
+}
+
+static void l2tp_tunnel_done(NetDev *netdev) {
+ L2tpTunnel *t;
+
+ assert(netdev);
+
+ t = L2TP(netdev);
+
+ assert(t);
+
+ ordered_hashmap_free_with_destructor(t->sessions_by_section, l2tp_session_free);
+ free(t->local_ifname);
+}
+
+const NetDevVTable l2tptnl_vtable = {
+ .object_size = sizeof(L2tpTunnel),
+ .init = l2tp_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "L2TP\0L2TPSession\0",
+ .create = l2tp_create_tunnel,
+ .done = l2tp_tunnel_done,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .is_ready_to_create = netdev_l2tp_is_ready_to_create,
+ .config_verify = netdev_l2tp_tunnel_verify,
+};
diff --git a/src/network/netdev/l2tp-tunnel.h b/src/network/netdev/l2tp-tunnel.h
new file mode 100644
index 0000000..6028b35
--- /dev/null
+++ b/src/network/netdev/l2tp-tunnel.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+#include <linux/l2tp.h>
+
+#include "in-addr-util.h"
+#include "netdev.h"
+#include "networkd-util.h"
+
+typedef enum L2tpL2specType {
+ NETDEV_L2TP_L2SPECTYPE_NONE = L2TP_L2SPECTYPE_NONE,
+ NETDEV_L2TP_L2SPECTYPE_DEFAULT = L2TP_L2SPECTYPE_DEFAULT,
+ _NETDEV_L2TP_L2SPECTYPE_MAX,
+ _NETDEV_L2TP_L2SPECTYPE_INVALID = -EINVAL,
+} L2tpL2specType;
+
+typedef enum L2tpEncapType {
+ NETDEV_L2TP_ENCAPTYPE_UDP = L2TP_ENCAPTYPE_UDP,
+ NETDEV_L2TP_ENCAPTYPE_IP = L2TP_ENCAPTYPE_IP,
+ _NETDEV_L2TP_ENCAPTYPE_MAX,
+ _NETDEV_L2TP_ENCAPTYPE_INVALID = -EINVAL,
+} L2tpEncapType;
+
+typedef enum L2tpLocalAddressType {
+ NETDEV_L2TP_LOCAL_ADDRESS_AUTO,
+ NETDEV_L2TP_LOCAL_ADDRESS_STATIC,
+ NETDEV_L2TP_LOCAL_ADDRESS_DYNAMIC,
+ _NETDEV_L2TP_LOCAL_ADDRESS_MAX,
+ _NETDEV_L2TP_LOCAL_ADDRESS_INVALID = -EINVAL,
+} L2tpLocalAddressType;
+
+typedef struct L2tpTunnel L2tpTunnel;
+
+typedef struct L2tpSession {
+ L2tpTunnel *tunnel;
+ ConfigSection *section;
+
+ char *name;
+
+ uint32_t session_id;
+ uint32_t peer_session_id;
+ L2tpL2specType l2tp_l2spec_type;
+} L2tpSession;
+
+struct L2tpTunnel {
+ NetDev meta;
+
+ uint16_t l2tp_udp_sport;
+ uint16_t l2tp_udp_dport;
+
+ uint32_t tunnel_id;
+ uint32_t peer_tunnel_id;
+
+ int family;
+
+ bool udp_csum;
+ bool udp6_csum_rx;
+ bool udp6_csum_tx;
+
+ char *local_ifname;
+ L2tpLocalAddressType local_address_type;
+ union in_addr_union local;
+ union in_addr_union remote;
+
+ L2tpEncapType l2tp_encap_type;
+
+ OrderedHashmap *sessions_by_section;
+};
+
+DEFINE_NETDEV_CAST(L2TP, L2tpTunnel);
+extern const NetDevVTable l2tptnl_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_tunnel_local_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_tunnel_remote_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_tunnel_id);
+CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_encap_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_session_l2spec);
+CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_session_id);
+CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_session_name);
diff --git a/src/network/netdev/macsec.c b/src/network/netdev/macsec.c
new file mode 100644
index 0000000..1e3f6a9
--- /dev/null
+++ b/src/network/netdev/macsec.c
@@ -0,0 +1,1233 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+#include <linux/if_ether.h>
+#include <linux/if_macsec.h>
+#include <linux/genetlink.h>
+
+#include "conf-parser.h"
+#include "fileio.h"
+#include "hashmap.h"
+#include "hexdecoct.h"
+#include "macsec.h"
+#include "memory-util.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "parse-helpers.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "util.h"
+
+static void security_association_clear(SecurityAssociation *sa) {
+ if (!sa)
+ return;
+
+ explicit_bzero_safe(sa->key, sa->key_len);
+ free(sa->key);
+ free(sa->key_file);
+}
+
+static void security_association_init(SecurityAssociation *sa) {
+ assert(sa);
+
+ sa->activate = -1;
+ sa->use_for_encoding = -1;
+}
+
+static ReceiveAssociation* macsec_receive_association_free(ReceiveAssociation *c) {
+ if (!c)
+ return NULL;
+
+ if (c->macsec && c->section)
+ ordered_hashmap_remove(c->macsec->receive_associations_by_section, c->section);
+
+ config_section_free(c->section);
+ security_association_clear(&c->sa);
+
+ return mfree(c);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(ReceiveAssociation, macsec_receive_association_free);
+
+static int macsec_receive_association_new_static(MACsec *s, const char *filename, unsigned section_line, ReceiveAssociation **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(macsec_receive_association_freep) ReceiveAssociation *c = NULL;
+ int r;
+
+ assert(s);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ c = ordered_hashmap_get(s->receive_associations_by_section, n);
+ if (c) {
+ *ret = TAKE_PTR(c);
+ return 0;
+ }
+
+ c = new(ReceiveAssociation, 1);
+ if (!c)
+ return -ENOMEM;
+
+ *c = (ReceiveAssociation) {
+ .macsec = s,
+ .section = TAKE_PTR(n),
+ };
+
+ security_association_init(&c->sa);
+
+ r = ordered_hashmap_ensure_put(&s->receive_associations_by_section, &config_section_hash_ops, c->section, c);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(c);
+
+ return 0;
+}
+
+static ReceiveChannel* macsec_receive_channel_free(ReceiveChannel *c) {
+ if (!c)
+ return NULL;
+
+ if (c->macsec) {
+ if (c->sci.as_uint64 > 0)
+ ordered_hashmap_remove_value(c->macsec->receive_channels, &c->sci.as_uint64, c);
+
+ if (c->section)
+ ordered_hashmap_remove(c->macsec->receive_channels_by_section, c->section);
+ }
+
+ config_section_free(c->section);
+
+ return mfree(c);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(ReceiveChannel, macsec_receive_channel_free);
+
+static int macsec_receive_channel_new(MACsec *s, uint64_t sci, ReceiveChannel **ret) {
+ ReceiveChannel *c;
+
+ assert(s);
+
+ c = new(ReceiveChannel, 1);
+ if (!c)
+ return -ENOMEM;
+
+ *c = (ReceiveChannel) {
+ .macsec = s,
+ .sci.as_uint64 = sci,
+ };
+
+ *ret = c;
+ return 0;
+}
+
+static int macsec_receive_channel_new_static(MACsec *s, const char *filename, unsigned section_line, ReceiveChannel **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(macsec_receive_channel_freep) ReceiveChannel *c = NULL;
+ int r;
+
+ assert(s);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ c = ordered_hashmap_get(s->receive_channels_by_section, n);
+ if (c) {
+ *ret = TAKE_PTR(c);
+ return 0;
+ }
+
+ r = macsec_receive_channel_new(s, 0, &c);
+ if (r < 0)
+ return r;
+
+ c->section = TAKE_PTR(n);
+
+ r = ordered_hashmap_ensure_put(&s->receive_channels_by_section, &config_section_hash_ops, c->section, c);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(c);
+
+ return 0;
+}
+
+static TransmitAssociation* macsec_transmit_association_free(TransmitAssociation *a) {
+ if (!a)
+ return NULL;
+
+ if (a->macsec && a->section)
+ ordered_hashmap_remove(a->macsec->transmit_associations_by_section, a->section);
+
+ config_section_free(a->section);
+ security_association_clear(&a->sa);
+
+ return mfree(a);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(TransmitAssociation, macsec_transmit_association_free);
+
+static int macsec_transmit_association_new_static(MACsec *s, const char *filename, unsigned section_line, TransmitAssociation **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(macsec_transmit_association_freep) TransmitAssociation *a = NULL;
+ int r;
+
+ assert(s);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ a = ordered_hashmap_get(s->transmit_associations_by_section, n);
+ if (a) {
+ *ret = TAKE_PTR(a);
+ return 0;
+ }
+
+ a = new(TransmitAssociation, 1);
+ if (!a)
+ return -ENOMEM;
+
+ *a = (TransmitAssociation) {
+ .macsec = s,
+ .section = TAKE_PTR(n),
+ };
+
+ security_association_init(&a->sa);
+
+ r = ordered_hashmap_ensure_put(&s->transmit_associations_by_section, &config_section_hash_ops, a->section, a);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(a);
+
+ return 0;
+}
+
+static int netdev_macsec_create_message(NetDev *netdev, int command, sd_netlink_message **ret) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+ assert(netdev->ifindex > 0);
+
+ r = sd_genl_message_new(netdev->manager->genl, MACSEC_GENL_NAME, command, &m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, MACSEC_ATTR_IFINDEX, netdev->ifindex);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(m);
+
+ return 0;
+}
+
+static int netdev_macsec_fill_message_sci(NetDev *netdev, MACsecSCI *sci, sd_netlink_message *m) {
+ int r;
+
+ assert(netdev);
+ assert(m);
+ assert(sci);
+
+ r = sd_netlink_message_open_container(m, MACSEC_ATTR_RXSC_CONFIG);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u64(m, MACSEC_RXSC_ATTR_SCI, sci->as_uint64);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int netdev_macsec_fill_message_sa(NetDev *netdev, SecurityAssociation *a, sd_netlink_message *m) {
+ int r;
+
+ assert(netdev);
+ assert(a);
+ assert(m);
+
+ r = sd_netlink_message_open_container(m, MACSEC_ATTR_SA_CONFIG);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, MACSEC_SA_ATTR_AN, a->association_number);
+ if (r < 0)
+ return r;
+
+ if (a->packet_number > 0) {
+ r = sd_netlink_message_append_u32(m, MACSEC_SA_ATTR_PN, a->packet_number);
+ if (r < 0)
+ return r;
+ }
+
+ if (a->key_len > 0) {
+ r = sd_netlink_message_append_data(m, MACSEC_SA_ATTR_KEYID, a->key_id, MACSEC_KEYID_LEN);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(m, MACSEC_SA_ATTR_KEY, a->key, a->key_len);
+ if (r < 0)
+ return r;
+ }
+
+ if (a->activate >= 0) {
+ r = sd_netlink_message_append_u8(m, MACSEC_SA_ATTR_ACTIVE, a->activate);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int macsec_receive_association_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
+ int r;
+
+ assert(netdev);
+ assert(netdev->state != _NETDEV_STATE_INVALID);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EEXIST)
+ log_netdev_info(netdev,
+ "MACsec receive secure association exists, using it without changing parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r,
+ "Failed to add receive secure association: %m");
+ netdev_enter_failed(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "Receive secure association is configured");
+
+ return 1;
+}
+
+static int netdev_macsec_configure_receive_association(NetDev *netdev, ReceiveAssociation *a) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+ assert(a);
+
+ r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_RXSA, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m");
+
+ r = netdev_macsec_fill_message_sa(netdev, &a->sa, m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m");
+
+ r = netdev_macsec_fill_message_sci(netdev, &a->sci, m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m");
+
+ r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_receive_association_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to configure receive secure association: %m");
+
+ netdev_ref(netdev);
+
+ return 0;
+}
+
+static int macsec_receive_channel_handler(sd_netlink *rtnl, sd_netlink_message *m, ReceiveChannel *c) {
+ NetDev *netdev;
+ int r;
+
+ assert(c);
+ assert(c->macsec);
+
+ netdev = NETDEV(c->macsec);
+
+ assert(netdev->state != _NETDEV_STATE_INVALID);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EEXIST)
+ log_netdev_debug(netdev,
+ "MACsec receive channel exists, using it without changing parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r,
+ "Failed to add receive secure channel: %m");
+ netdev_enter_failed(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "Receive channel is configured");
+
+ for (unsigned i = 0; i < c->n_rxsa; i++) {
+ r = netdev_macsec_configure_receive_association(netdev, c->rxsa[i]);
+ if (r < 0) {
+ log_netdev_warning_errno(netdev, r,
+ "Failed to configure receive security association: %m");
+ netdev_enter_failed(netdev);
+ return 1;
+ }
+ }
+
+ return 1;
+}
+
+static void receive_channel_destroy_callback(ReceiveChannel *c) {
+ assert(c);
+ assert(c->macsec);
+
+ netdev_unref(NETDEV(c->macsec));
+}
+
+static int netdev_macsec_configure_receive_channel(NetDev *netdev, ReceiveChannel *c) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+ assert(c);
+
+ r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_RXSC, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m");
+
+ r = netdev_macsec_fill_message_sci(netdev, &c->sci, m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m");
+
+ r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_receive_channel_handler,
+ receive_channel_destroy_callback, c);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to configure receive channel: %m");
+
+ netdev_ref(netdev);
+
+ return 0;
+}
+
+static int macsec_transmit_association_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
+ int r;
+
+ assert(netdev);
+ assert(netdev->state != _NETDEV_STATE_INVALID);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EEXIST)
+ log_netdev_info(netdev,
+ "MACsec transmit secure association exists, using it without changing parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r,
+ "Failed to add transmit secure association: %m");
+ netdev_enter_failed(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "Transmit secure association is configured");
+
+ return 1;
+}
+
+static int netdev_macsec_configure_transmit_association(NetDev *netdev, TransmitAssociation *a) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+ assert(a);
+
+ r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_TXSA, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m");
+
+ r = netdev_macsec_fill_message_sa(netdev, &a->sa, m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m");
+
+ r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_transmit_association_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to configure transmit secure association: %m");
+
+ netdev_ref(netdev);
+
+ return 0;
+}
+
+static int netdev_macsec_configure(NetDev *netdev, Link *link) {
+ TransmitAssociation *a;
+ ReceiveChannel *c;
+ MACsec *s;
+ int r;
+
+ assert(netdev);
+ s = MACSEC(netdev);
+ assert(s);
+
+ ORDERED_HASHMAP_FOREACH(a, s->transmit_associations_by_section) {
+ r = netdev_macsec_configure_transmit_association(netdev, a);
+ if (r < 0)
+ return r;
+ }
+
+ ORDERED_HASHMAP_FOREACH(c, s->receive_channels) {
+ r = netdev_macsec_configure_receive_channel(netdev, c);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int netdev_macsec_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ MACsec *v;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ v = MACSEC(netdev);
+
+ assert(v);
+
+ if (v->port > 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_MACSEC_PORT, v->port);
+ if (r < 0)
+ return r;
+ }
+
+ if (v->encrypt >= 0) {
+ r = sd_netlink_message_append_u8(m, IFLA_MACSEC_ENCRYPT, v->encrypt);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_MACSEC_ENCODING_SA, v->encoding_an);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_macsec_port(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
+ _cleanup_(macsec_receive_channel_free_or_set_invalidp) ReceiveChannel *c = NULL;
+ MACsec *s = userdata;
+ uint16_t port;
+ void *dest;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ /* This parses port used to make Secure Channel Identifier (SCI) */
+
+ if (streq(section, "MACsec"))
+ dest = &s->port;
+ else if (streq(section, "MACsecReceiveChannel")) {
+ r = macsec_receive_channel_new_static(s, filename, section_line, &c);
+ if (r < 0)
+ return log_oom();
+
+ dest = &c->sci.port;
+ } else {
+ assert(streq(section, "MACsecReceiveAssociation"));
+
+ r = macsec_receive_association_new_static(s, filename, section_line, &b);
+ if (r < 0)
+ return log_oom();
+
+ dest = &b->sci.port;
+ }
+
+ r = parse_ip_port(rvalue, &port);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse port '%s' for secure channel identifier. Ignoring assignment: %m",
+ rvalue);
+ return 0;
+ }
+
+ unaligned_write_be16(dest, port);
+
+ TAKE_PTR(b);
+ TAKE_PTR(c);
+
+ return 0;
+}
+
+int config_parse_macsec_hw_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
+ _cleanup_(macsec_receive_channel_free_or_set_invalidp) ReceiveChannel *c = NULL;
+ MACsec *s = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(section, "MACsecReceiveChannel"))
+ r = macsec_receive_channel_new_static(s, filename, section_line, &c);
+ else
+ r = macsec_receive_association_new_static(s, filename, section_line, &b);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_ether_addr(rvalue, b ? &b->sci.mac : &c->sci.mac);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse MAC address for secure channel identifier. "
+ "Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(b);
+ TAKE_PTR(c);
+
+ return 0;
+}
+
+int config_parse_macsec_packet_number(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
+ _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
+ MACsec *s = userdata;
+ uint32_t val, *dest;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(section, "MACsecTransmitAssociation"))
+ r = macsec_transmit_association_new_static(s, filename, section_line, &a);
+ else
+ r = macsec_receive_association_new_static(s, filename, section_line, &b);
+ if (r < 0)
+ return log_oom();
+
+ dest = a ? &a->sa.packet_number : &b->sa.packet_number;
+
+ r = safe_atou32(rvalue, &val);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse packet number. Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (streq(section, "MACsecTransmitAssociation") && val == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid packet number. Ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ *dest = val;
+ TAKE_PTR(a);
+ TAKE_PTR(b);
+
+ return 0;
+}
+
+int config_parse_macsec_key(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
+ _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
+ _cleanup_(erase_and_freep) void *p = NULL;
+ MACsec *s = userdata;
+ SecurityAssociation *dest;
+ size_t l;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ (void) warn_file_is_world_accessible(filename, NULL, unit, line);
+
+ if (streq(section, "MACsecTransmitAssociation"))
+ r = macsec_transmit_association_new_static(s, filename, section_line, &a);
+ else
+ r = macsec_receive_association_new_static(s, filename, section_line, &b);
+ if (r < 0)
+ return log_oom();
+
+ dest = a ? &a->sa : &b->sa;
+
+ r = unhexmem_full(rvalue, strlen(rvalue), true, &p, &l);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse key. Ignoring assignment: %m");
+ return 0;
+ }
+
+ if (l != 16) {
+ /* See DEFAULT_SAK_LEN in drivers/net/macsec.c */
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid key length (%zu). Ignoring assignment", l);
+ return 0;
+ }
+
+ explicit_bzero_safe(dest->key, dest->key_len);
+ free_and_replace(dest->key, p);
+ dest->key_len = l;
+
+ TAKE_PTR(a);
+ TAKE_PTR(b);
+
+ return 0;
+}
+
+int config_parse_macsec_key_file(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
+ _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
+ _cleanup_free_ char *path = NULL;
+ MACsec *s = userdata;
+ char **dest;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(section, "MACsecTransmitAssociation"))
+ r = macsec_transmit_association_new_static(s, filename, section_line, &a);
+ else
+ r = macsec_receive_association_new_static(s, filename, section_line, &b);
+ if (r < 0)
+ return log_oom();
+
+ dest = a ? &a->sa.key_file : &b->sa.key_file;
+
+ if (isempty(rvalue)) {
+ *dest = mfree(*dest);
+ return 0;
+ }
+
+ path = strdup(rvalue);
+ if (!path)
+ return log_oom();
+
+ if (path_simplify_and_warn(path, PATH_CHECK_ABSOLUTE, unit, filename, line, lvalue) < 0)
+ return 0;
+
+ free_and_replace(*dest, path);
+ TAKE_PTR(a);
+ TAKE_PTR(b);
+
+ return 0;
+}
+
+int config_parse_macsec_key_id(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
+ _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
+ _cleanup_free_ void *p = NULL;
+ MACsec *s = userdata;
+ uint8_t *dest;
+ size_t l;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(section, "MACsecTransmitAssociation"))
+ r = macsec_transmit_association_new_static(s, filename, section_line, &a);
+ else
+ r = macsec_receive_association_new_static(s, filename, section_line, &b);
+ if (r < 0)
+ return log_oom();
+
+ r = unhexmem(rvalue, strlen(rvalue), &p, &l);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse KeyId=%s, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+ if (l > MACSEC_KEYID_LEN) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified KeyId= is larger then the allowed maximum (%zu > %i), ignoring: %s",
+ l, MACSEC_KEYID_LEN, rvalue);
+ return 0;
+ }
+
+ dest = a ? a->sa.key_id : b->sa.key_id;
+ memcpy_safe(dest, p, l);
+ memzero(dest + l, MACSEC_KEYID_LEN - l);
+
+ TAKE_PTR(a);
+ TAKE_PTR(b);
+
+ return 0;
+}
+
+int config_parse_macsec_sa_activate(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
+ _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
+ MACsec *s = userdata;
+ int *dest;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(section, "MACsecTransmitAssociation"))
+ r = macsec_transmit_association_new_static(s, filename, section_line, &a);
+ else
+ r = macsec_receive_association_new_static(s, filename, section_line, &b);
+ if (r < 0)
+ return log_oom();
+
+ dest = a ? &a->sa.activate : &b->sa.activate;
+
+ if (isempty(rvalue))
+ r = -1;
+ else {
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse activation mode of %s security association. "
+ "Ignoring assignment: %s",
+ streq(section, "MACsecTransmitAssociation") ? "transmit" : "receive",
+ rvalue);
+ return 0;
+ }
+ }
+
+ *dest = r;
+ TAKE_PTR(a);
+ TAKE_PTR(b);
+
+ return 0;
+}
+
+int config_parse_macsec_use_for_encoding(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
+ MACsec *s = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = macsec_transmit_association_new_static(s, filename, section_line, &a);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ a->sa.use_for_encoding = -1;
+ TAKE_PTR(a);
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s= setting. Ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ a->sa.use_for_encoding = r;
+ if (a->sa.use_for_encoding > 0)
+ a->sa.activate = true;
+
+ TAKE_PTR(a);
+
+ return 0;
+}
+
+static int macsec_read_key_file(NetDev *netdev, SecurityAssociation *sa) {
+ _cleanup_(erase_and_freep) uint8_t *key = NULL;
+ size_t key_len;
+ int r;
+
+ assert(netdev);
+ assert(sa);
+
+ if (!sa->key_file)
+ return 0;
+
+ r = read_full_file_full(
+ AT_FDCWD, sa->key_file, UINT64_MAX, SIZE_MAX,
+ READ_FULL_FILE_SECURE | READ_FULL_FILE_UNHEX | READ_FULL_FILE_WARN_WORLD_READABLE | READ_FULL_FILE_CONNECT_SOCKET,
+ NULL, (char **) &key, &key_len);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r,
+ "Failed to read key from '%s', ignoring: %m",
+ sa->key_file);
+
+ if (key_len != 16)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "Invalid key length (%zu bytes), ignoring: %m", key_len);
+
+ explicit_bzero_safe(sa->key, sa->key_len);
+ free_and_replace(sa->key, key);
+ sa->key_len = key_len;
+
+ return 0;
+}
+
+static int macsec_receive_channel_verify(ReceiveChannel *c) {
+ NetDev *netdev;
+ int r;
+
+ assert(c);
+ assert(c->macsec);
+
+ netdev = NETDEV(c->macsec);
+
+ if (section_is_invalid(c->section))
+ return -EINVAL;
+
+ if (ether_addr_is_null(&c->sci.mac))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: MACsec receive channel without MAC address configured. "
+ "Ignoring [MACsecReceiveChannel] section from line %u",
+ c->section->filename, c->section->line);
+
+ if (c->sci.port == 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: MACsec receive channel without port configured. "
+ "Ignoring [MACsecReceiveChannel] section from line %u",
+ c->section->filename, c->section->line);
+
+ r = ordered_hashmap_ensure_put(&c->macsec->receive_channels, &uint64_hash_ops, &c->sci.as_uint64, c);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r == -EEXIST)
+ return log_netdev_error_errno(netdev, r,
+ "%s: Multiple [MACsecReceiveChannel] sections have same SCI, "
+ "Ignoring [MACsecReceiveChannel] section from line %u",
+ c->section->filename, c->section->line);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r,
+ "%s: Failed to store [MACsecReceiveChannel] section at hashmap, "
+ "Ignoring [MACsecReceiveChannel] section from line %u",
+ c->section->filename, c->section->line);
+ return 0;
+}
+
+static int macsec_transmit_association_verify(TransmitAssociation *t) {
+ NetDev *netdev;
+ int r;
+
+ assert(t);
+ assert(t->macsec);
+
+ netdev = NETDEV(t->macsec);
+
+ if (section_is_invalid(t->section))
+ return -EINVAL;
+
+ if (t->sa.packet_number == 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: MACsec transmit secure association without PacketNumber= configured. "
+ "Ignoring [MACsecTransmitAssociation] section from line %u",
+ t->section->filename, t->section->line);
+
+ r = macsec_read_key_file(netdev, &t->sa);
+ if (r < 0)
+ return r;
+
+ if (t->sa.key_len <= 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: MACsec transmit secure association without key configured. "
+ "Ignoring [MACsecTransmitAssociation] section from line %u",
+ t->section->filename, t->section->line);
+
+ return 0;
+}
+
+static int macsec_receive_association_verify(ReceiveAssociation *a) {
+ ReceiveChannel *c;
+ NetDev *netdev;
+ int r;
+
+ assert(a);
+ assert(a->macsec);
+
+ netdev = NETDEV(a->macsec);
+
+ if (section_is_invalid(a->section))
+ return -EINVAL;
+
+ r = macsec_read_key_file(netdev, &a->sa);
+ if (r < 0)
+ return r;
+
+ if (a->sa.key_len <= 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: MACsec receive secure association without key configured. "
+ "Ignoring [MACsecReceiveAssociation] section from line %u",
+ a->section->filename, a->section->line);
+
+ if (ether_addr_is_null(&a->sci.mac))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: MACsec receive secure association without MAC address configured. "
+ "Ignoring [MACsecReceiveAssociation] section from line %u",
+ a->section->filename, a->section->line);
+
+ if (a->sci.port == 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: MACsec receive secure association without port configured. "
+ "Ignoring [MACsecReceiveAssociation] section from line %u",
+ a->section->filename, a->section->line);
+
+ c = ordered_hashmap_get(a->macsec->receive_channels, &a->sci.as_uint64);
+ if (!c) {
+ _cleanup_(macsec_receive_channel_freep) ReceiveChannel *new_channel = NULL;
+
+ r = macsec_receive_channel_new(a->macsec, a->sci.as_uint64, &new_channel);
+ if (r < 0)
+ return log_oom();
+
+ r = ordered_hashmap_ensure_put(&a->macsec->receive_channels, &uint64_hash_ops, &new_channel->sci.as_uint64, new_channel);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r,
+ "%s: Failed to store receive channel at hashmap, "
+ "Ignoring [MACsecReceiveAssociation] section from line %u",
+ a->section->filename, a->section->line);
+ c = TAKE_PTR(new_channel);
+ }
+ if (c->n_rxsa >= MACSEC_MAX_ASSOCIATION_NUMBER)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(ERANGE),
+ "%s: Too many [MACsecReceiveAssociation] sections for the same receive channel, "
+ "Ignoring [MACsecReceiveAssociation] section from line %u",
+ a->section->filename, a->section->line);
+
+ a->sa.association_number = c->n_rxsa;
+ c->rxsa[c->n_rxsa++] = a;
+
+ return 0;
+}
+
+static int netdev_macsec_verify(NetDev *netdev, const char *filename) {
+ MACsec *v = MACSEC(netdev);
+ TransmitAssociation *a;
+ ReceiveAssociation *n;
+ ReceiveChannel *c;
+ uint8_t an, encoding_an;
+ bool use_for_encoding;
+ int r;
+
+ assert(netdev);
+ assert(v);
+ assert(filename);
+
+ ORDERED_HASHMAP_FOREACH(c, v->receive_channels_by_section) {
+ r = macsec_receive_channel_verify(c);
+ if (r < 0)
+ macsec_receive_channel_free(c);
+ }
+
+ an = 0;
+ use_for_encoding = false;
+ encoding_an = 0;
+ ORDERED_HASHMAP_FOREACH(a, v->transmit_associations_by_section) {
+ r = macsec_transmit_association_verify(a);
+ if (r < 0) {
+ macsec_transmit_association_free(a);
+ continue;
+ }
+
+ if (an >= MACSEC_MAX_ASSOCIATION_NUMBER) {
+ log_netdev_error(netdev,
+ "%s: Too many [MACsecTransmitAssociation] sections configured. "
+ "Ignoring [MACsecTransmitAssociation] section from line %u",
+ a->section->filename, a->section->line);
+ macsec_transmit_association_free(a);
+ continue;
+ }
+
+ a->sa.association_number = an++;
+
+ if (a->sa.use_for_encoding > 0) {
+ if (use_for_encoding) {
+ log_netdev_warning(netdev,
+ "%s: Multiple security associations are set to be used for transmit channel."
+ "Disabling UseForEncoding= in [MACsecTransmitAssociation] section from line %u",
+ a->section->filename, a->section->line);
+ a->sa.use_for_encoding = false;
+ } else {
+ encoding_an = a->sa.association_number;
+ use_for_encoding = true;
+ }
+ }
+ }
+
+ assert(encoding_an < MACSEC_MAX_ASSOCIATION_NUMBER);
+ v->encoding_an = encoding_an;
+
+ ORDERED_HASHMAP_FOREACH(n, v->receive_associations_by_section) {
+ r = macsec_receive_association_verify(n);
+ if (r < 0)
+ macsec_receive_association_free(n);
+ }
+
+ return 0;
+}
+
+static void macsec_init(NetDev *netdev) {
+ MACsec *v;
+
+ assert(netdev);
+
+ v = MACSEC(netdev);
+
+ assert(v);
+
+ v->encrypt = -1;
+}
+
+static void macsec_done(NetDev *netdev) {
+ MACsec *t;
+
+ assert(netdev);
+
+ t = MACSEC(netdev);
+
+ assert(t);
+
+ ordered_hashmap_free_with_destructor(t->receive_channels, macsec_receive_channel_free);
+ ordered_hashmap_free_with_destructor(t->receive_channels_by_section, macsec_receive_channel_free);
+ ordered_hashmap_free_with_destructor(t->transmit_associations_by_section, macsec_transmit_association_free);
+ ordered_hashmap_free_with_destructor(t->receive_associations_by_section, macsec_receive_association_free);
+}
+
+const NetDevVTable macsec_vtable = {
+ .object_size = sizeof(MACsec),
+ .init = macsec_init,
+ .sections = NETDEV_COMMON_SECTIONS "MACsec\0MACsecReceiveChannel\0MACsecTransmitAssociation\0MACsecReceiveAssociation\0",
+ .fill_message_create = netdev_macsec_fill_message_create,
+ .post_create = netdev_macsec_configure,
+ .done = macsec_done,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_macsec_verify,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/macsec.h b/src/network/netdev/macsec.h
new file mode 100644
index 0000000..17bb1ca
--- /dev/null
+++ b/src/network/netdev/macsec.h
@@ -0,0 +1,87 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+#include <linux/if_macsec.h>
+
+#include "ether-addr-util.h"
+#include "in-addr-util.h"
+#include "netdev.h"
+#include "networkd-util.h"
+#include "sparse-endian.h"
+
+/* See the definition of MACSEC_NUM_AN in kernel's drivers/net/macsec.c */
+#define MACSEC_MAX_ASSOCIATION_NUMBER 4
+
+typedef struct MACsec MACsec;
+
+typedef union MACsecSCI {
+ uint64_t as_uint64;
+
+ struct {
+ struct ether_addr mac;
+ be16_t port;
+ } _packed_;
+} MACsecSCI;
+
+assert_cc(sizeof(MACsecSCI) == sizeof(uint64_t));
+
+typedef struct SecurityAssociation {
+ uint8_t association_number;
+ uint32_t packet_number;
+ uint8_t key_id[MACSEC_KEYID_LEN];
+ uint8_t *key;
+ uint32_t key_len;
+ char *key_file;
+ int activate;
+ int use_for_encoding;
+} SecurityAssociation;
+
+typedef struct TransmitAssociation {
+ MACsec *macsec;
+ ConfigSection *section;
+
+ SecurityAssociation sa;
+} TransmitAssociation;
+
+typedef struct ReceiveAssociation {
+ MACsec *macsec;
+ ConfigSection *section;
+
+ MACsecSCI sci;
+ SecurityAssociation sa;
+} ReceiveAssociation;
+
+typedef struct ReceiveChannel {
+ MACsec *macsec;
+ ConfigSection *section;
+
+ MACsecSCI sci;
+ ReceiveAssociation *rxsa[MACSEC_MAX_ASSOCIATION_NUMBER];
+ unsigned n_rxsa;
+} ReceiveChannel;
+
+struct MACsec {
+ NetDev meta;
+
+ uint16_t port;
+ int encrypt;
+ uint8_t encoding_an;
+
+ OrderedHashmap *receive_channels;
+ OrderedHashmap *receive_channels_by_section;
+ OrderedHashmap *transmit_associations_by_section;
+ OrderedHashmap *receive_associations_by_section;
+};
+
+DEFINE_NETDEV_CAST(MACSEC, MACsec);
+extern const NetDevVTable macsec_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_macsec_port);
+CONFIG_PARSER_PROTOTYPE(config_parse_macsec_hw_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_macsec_packet_number);
+CONFIG_PARSER_PROTOTYPE(config_parse_macsec_key_id);
+CONFIG_PARSER_PROTOTYPE(config_parse_macsec_key);
+CONFIG_PARSER_PROTOTYPE(config_parse_macsec_key_file);
+CONFIG_PARSER_PROTOTYPE(config_parse_macsec_sa_activate);
+CONFIG_PARSER_PROTOTYPE(config_parse_macsec_use_for_encoding);
diff --git a/src/network/netdev/macvlan.c b/src/network/netdev/macvlan.c
new file mode 100644
index 0000000..1114bb0
--- /dev/null
+++ b/src/network/netdev/macvlan.c
@@ -0,0 +1,170 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "conf-parser.h"
+#include "macvlan.h"
+#include "macvlan-util.h"
+#include "networkd-network.h"
+#include "parse-util.h"
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_macvlan_mode, macvlan_mode, MacVlanMode, "Failed to parse macvlan mode");
+
+static int netdev_macvlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *req) {
+ MacVlan *m;
+ int r;
+
+ assert(netdev);
+ assert(link);
+ assert(netdev->ifname);
+ assert(link->network);
+
+ if (netdev->kind == NETDEV_KIND_MACVLAN)
+ m = MACVLAN(netdev);
+ else
+ m = MACVTAP(netdev);
+
+ assert(m);
+
+ if (m->mode == NETDEV_MACVLAN_MODE_SOURCE && !set_isempty(m->match_source_mac)) {
+ const struct ether_addr *mac_addr;
+
+ r = sd_netlink_message_append_u32(req, IFLA_MACVLAN_MACADDR_MODE, MACVLAN_MACADDR_SET);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container(req, IFLA_MACVLAN_MACADDR_DATA);
+ if (r < 0)
+ return r;
+
+ SET_FOREACH(mac_addr, m->match_source_mac) {
+ r = sd_netlink_message_append_ether_addr(req, IFLA_MACVLAN_MACADDR, mac_addr);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+ }
+
+ if (m->mode != _NETDEV_MACVLAN_MODE_INVALID) {
+ r = sd_netlink_message_append_u32(req, IFLA_MACVLAN_MODE, m->mode);
+ if (r < 0)
+ return r;
+ }
+
+ /* set the nopromisc flag if Promiscuous= of the link is explicitly set to false */
+ if (m->mode == NETDEV_MACVLAN_MODE_PASSTHRU && link->network->promiscuous == 0) {
+ r = sd_netlink_message_append_u16(req, IFLA_MACVLAN_FLAGS, MACVLAN_FLAG_NOPROMISC);
+ if (r < 0)
+ return r;
+ }
+
+ if (m->bc_queue_length != UINT32_MAX) {
+ r = sd_netlink_message_append_u32(req, IFLA_MACVLAN_BC_QUEUE_LEN, m->bc_queue_length);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int config_parse_macvlan_broadcast_queue_size(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ MacVlan *m = ASSERT_PTR(userdata);
+ uint32_t v;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ m->bc_queue_length = UINT32_MAX;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse BroadcastMulticastQueueLength=%s, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (v == UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid BroadcastMulticastQueueLength=%s, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ m->bc_queue_length = v;
+ return 0;
+}
+
+static void macvlan_done(NetDev *n) {
+ MacVlan *m;
+
+ assert(n);
+
+ if (n->kind == NETDEV_KIND_MACVLAN)
+ m = MACVLAN(n);
+ else
+ m = MACVTAP(n);
+
+ assert(m);
+
+ set_free(m->match_source_mac);
+}
+
+static void macvlan_init(NetDev *n) {
+ MacVlan *m;
+
+ assert(n);
+
+ if (n->kind == NETDEV_KIND_MACVLAN)
+ m = MACVLAN(n);
+ else
+ m = MACVTAP(n);
+
+ assert(m);
+
+ m->mode = _NETDEV_MACVLAN_MODE_INVALID;
+ m->bc_queue_length = UINT32_MAX;
+}
+
+const NetDevVTable macvtap_vtable = {
+ .object_size = sizeof(MacVlan),
+ .init = macvlan_init,
+ .done = macvlan_done,
+ .sections = NETDEV_COMMON_SECTIONS "MACVTAP\0",
+ .fill_message_create = netdev_macvlan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
+
+const NetDevVTable macvlan_vtable = {
+ .object_size = sizeof(MacVlan),
+ .init = macvlan_init,
+ .done = macvlan_done,
+ .sections = NETDEV_COMMON_SECTIONS "MACVLAN\0",
+ .fill_message_create = netdev_macvlan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/macvlan.h b/src/network/netdev/macvlan.h
new file mode 100644
index 0000000..c45fc4f
--- /dev/null
+++ b/src/network/netdev/macvlan.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct MacVlan MacVlan;
+
+#include "macvlan-util.h"
+#include "netdev.h"
+#include "set.h"
+
+struct MacVlan {
+ NetDev meta;
+
+ MacVlanMode mode;
+ Set *match_source_mac;
+
+ uint32_t bc_queue_length;
+};
+
+DEFINE_NETDEV_CAST(MACVLAN, MacVlan);
+DEFINE_NETDEV_CAST(MACVTAP, MacVlan);
+extern const NetDevVTable macvlan_vtable;
+extern const NetDevVTable macvtap_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_macvlan_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_macvlan_broadcast_queue_size);
diff --git a/src/network/netdev/netdev-gperf.gperf b/src/network/netdev/netdev-gperf.gperf
new file mode 100644
index 0000000..3cfcd51
--- /dev/null
+++ b/src/network/netdev/netdev-gperf.gperf
@@ -0,0 +1,270 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "bareudp.h"
+#include "batadv.h"
+#include "bond.h"
+#include "bridge.h"
+#include "conf-parser.h"
+#include "fou-tunnel.h"
+#include "geneve.h"
+#include "ipoib.h"
+#include "ipvlan.h"
+#include "l2tp-tunnel.h"
+#include "macsec.h"
+#include "macvlan.h"
+#include "net-condition.h"
+#include "netdev.h"
+#include "tunnel.h"
+#include "tuntap.h"
+#include "veth.h"
+#include "vlan-util.h"
+#include "vlan.h"
+#include "vrf.h"
+#include "vxcan.h"
+#include "vxlan.h"
+#include "wireguard.h"
+#include "wlan.h"
+#include "xfrm.h"
+%}
+struct ConfigPerfItem;
+%null_strings
+%language=ANSI-C
+%define slot-name section_and_lvalue
+%define hash-function-name network_netdev_gperf_hash
+%define lookup-function-name network_netdev_gperf_lookup
+%readonly-tables
+%omit-struct-type
+%struct-type
+%includes
+%%
+Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(NetDev, conditions)
+Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(NetDev, conditions)
+Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(NetDev, conditions)
+Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(NetDev, conditions)
+Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(NetDev, conditions)
+Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(NetDev, conditions)
+Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(NetDev, conditions)
+NetDev.Description, config_parse_string, 0, offsetof(NetDev, description)
+NetDev.Name, config_parse_ifname, 0, offsetof(NetDev, ifname)
+NetDev.Kind, config_parse_netdev_kind, 0, offsetof(NetDev, kind)
+NetDev.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(NetDev, mtu)
+NetDev.MACAddress, config_parse_netdev_hw_addr, ETH_ALEN, offsetof(NetDev, hw_addr)
+VLAN.Id, config_parse_vlanid, 0, offsetof(VLan, id)
+VLAN.Protocol, config_parse_vlanprotocol, 0, offsetof(VLan, protocol)
+VLAN.GVRP, config_parse_tristate, 0, offsetof(VLan, gvrp)
+VLAN.MVRP, config_parse_tristate, 0, offsetof(VLan, mvrp)
+VLAN.LooseBinding, config_parse_tristate, 0, offsetof(VLan, loose_binding)
+VLAN.ReorderHeader, config_parse_tristate, 0, offsetof(VLan, reorder_hdr)
+VLAN.EgressQOSMaps, config_parse_vlan_qos_maps, 0, offsetof(VLan, egress_qos_maps)
+VLAN.IngressQOSMaps, config_parse_vlan_qos_maps, 0, offsetof(VLan, ingress_qos_maps)
+MACVLAN.Mode, config_parse_macvlan_mode, 0, offsetof(MacVlan, mode)
+MACVLAN.SourceMACAddress, config_parse_ether_addrs, 0, offsetof(MacVlan, match_source_mac)
+MACVLAN.BroadcastMulticastQueueLength, config_parse_macvlan_broadcast_queue_size, 0, offsetof(MacVlan, bc_queue_length)
+MACVTAP.Mode, config_parse_macvlan_mode, 0, offsetof(MacVlan, mode)
+MACVTAP.SourceMACAddress, config_parse_ether_addrs, 0, offsetof(MacVlan, match_source_mac)
+IPVLAN.Mode, config_parse_ipvlan_mode, 0, offsetof(IPVlan, mode)
+IPVLAN.Flags, config_parse_ipvlan_flags, 0, offsetof(IPVlan, flags)
+IPVTAP.Mode, config_parse_ipvlan_mode, 0, offsetof(IPVlan, mode)
+IPVTAP.Flags, config_parse_ipvlan_flags, 0, offsetof(IPVlan, flags)
+Tunnel.Local, config_parse_tunnel_local_address, 0, 0
+Tunnel.Remote, config_parse_tunnel_remote_address, 0, 0
+Tunnel.TOS, config_parse_unsigned, 0, offsetof(Tunnel, tos)
+Tunnel.TTL, config_parse_unsigned, 0, offsetof(Tunnel, ttl)
+Tunnel.Key, config_parse_tunnel_key, 0, offsetof(Tunnel, key)
+Tunnel.InputKey, config_parse_tunnel_key, 0, offsetof(Tunnel, ikey)
+Tunnel.OutputKey, config_parse_tunnel_key, 0, offsetof(Tunnel, okey)
+Tunnel.DiscoverPathMTU, config_parse_bool, 0, offsetof(Tunnel, pmtudisc)
+Tunnel.Mode, config_parse_ip6tnl_mode, 0, offsetof(Tunnel, ip6tnl_mode)
+Tunnel.IPv6FlowLabel, config_parse_ipv6_flowlabel, 0, 0
+Tunnel.CopyDSCP, config_parse_bool, 0, offsetof(Tunnel, copy_dscp)
+Tunnel.EncapsulationLimit, config_parse_encap_limit, 0, 0
+Tunnel.Independent, config_parse_bool, 0, offsetof(Tunnel, independent)
+Tunnel.AssignToLoopback, config_parse_bool, 0, offsetof(Tunnel, assign_to_loopback)
+Tunnel.AllowLocalRemote, config_parse_tristate, 0, offsetof(Tunnel, allow_localremote)
+Tunnel.FooOverUDP, config_parse_bool, 0, offsetof(Tunnel, fou_tunnel)
+Tunnel.FOUDestinationPort, config_parse_ip_port, 0, offsetof(Tunnel, fou_destination_port)
+Tunnel.FOUSourcePort, config_parse_ip_port, 0, offsetof(Tunnel, encap_src_port)
+Tunnel.Encapsulation, config_parse_fou_encap_type, 0, offsetof(Tunnel, fou_encap_type)
+Tunnel.IPv6RapidDeploymentPrefix, config_parse_6rd_prefix, 0, 0
+Tunnel.ERSPANVersion, config_parse_erspan_version, 0, offsetof(Tunnel, erspan_version)
+Tunnel.ERSPANIndex, config_parse_erspan_index, 0, offsetof(Tunnel, erspan_index)
+Tunnel.ERSPANDirection, config_parse_erspan_direction, 0, offsetof(Tunnel, erspan_direction)
+Tunnel.ERSPANHardwareId, config_parse_erspan_hwid, 0, offsetof(Tunnel, erspan_hwid)
+Tunnel.SerializeTunneledPackets, config_parse_tristate, 0, offsetof(Tunnel, gre_erspan_sequence)
+Tunnel.ISATAP, config_parse_tristate, 0, offsetof(Tunnel, isatap)
+Tunnel.External, config_parse_bool, 0, offsetof(Tunnel, external)
+FooOverUDP.Protocol, config_parse_ip_protocol, 0, offsetof(FouTunnel, fou_protocol)
+FooOverUDP.Encapsulation, config_parse_fou_encap_type, 0, offsetof(FouTunnel, fou_encap_type)
+FooOverUDP.Port, config_parse_ip_port, 0, offsetof(FouTunnel, port)
+FooOverUDP.PeerPort, config_parse_ip_port, 0, offsetof(FouTunnel, peer_port)
+FooOverUDP.Local, config_parse_fou_tunnel_address, 0, offsetof(FouTunnel, local)
+FooOverUDP.Peer, config_parse_fou_tunnel_address, 0, offsetof(FouTunnel, peer)
+L2TP.TunnelId, config_parse_l2tp_tunnel_id, 0, offsetof(L2tpTunnel, tunnel_id)
+L2TP.PeerTunnelId, config_parse_l2tp_tunnel_id, 0, offsetof(L2tpTunnel, peer_tunnel_id)
+L2TP.UDPSourcePort, config_parse_ip_port, 0, offsetof(L2tpTunnel, l2tp_udp_sport)
+L2TP.UDPDestinationPort, config_parse_ip_port, 0, offsetof(L2tpTunnel, l2tp_udp_dport)
+L2TP.Local, config_parse_l2tp_tunnel_local_address, 0, 0
+L2TP.Remote, config_parse_l2tp_tunnel_remote_address, 0, 0
+L2TP.EncapsulationType, config_parse_l2tp_encap_type, 0, offsetof(L2tpTunnel, l2tp_encap_type)
+L2TP.UDPCheckSum, config_parse_bool, 0, offsetof(L2tpTunnel, udp_csum)
+L2TP.UDP6CheckSumRx, config_parse_bool, 0, offsetof(L2tpTunnel, udp6_csum_rx)
+L2TP.UDP6CheckSumTx, config_parse_bool, 0, offsetof(L2tpTunnel, udp6_csum_tx)
+L2TPSession.SessionId, config_parse_l2tp_session_id, 0, 0
+L2TPSession.PeerSessionId, config_parse_l2tp_session_id, 0, 0
+L2TPSession.Layer2SpecificHeader, config_parse_l2tp_session_l2spec, 0, 0
+L2TPSession.Name, config_parse_l2tp_session_name, 0, 0
+Peer.Name, config_parse_ifname, 0, offsetof(Veth, ifname_peer)
+Peer.MACAddress, config_parse_netdev_hw_addr, ETH_ALEN, offsetof(Veth, hw_addr_peer)
+VXCAN.Peer, config_parse_ifname, 0, offsetof(VxCan, ifname_peer)
+VXLAN.VNI, config_parse_uint32, 0, offsetof(VxLan, vni)
+VXLAN.Id, config_parse_uint32, 0, offsetof(VxLan, vni) /* deprecated */
+VXLAN.Group, config_parse_vxlan_address, 0, offsetof(VxLan, group)
+VXLAN.Local, config_parse_vxlan_address, 0, offsetof(VxLan, local)
+VXLAN.Remote, config_parse_vxlan_address, 0, offsetof(VxLan, remote)
+VXLAN.TOS, config_parse_unsigned, 0, offsetof(VxLan, tos)
+VXLAN.TTL, config_parse_vxlan_ttl, 0, offsetof(VxLan, ttl)
+VXLAN.MacLearning, config_parse_bool, 0, offsetof(VxLan, learning)
+VXLAN.ARPProxy, config_parse_bool, 0, offsetof(VxLan, arp_proxy)
+VXLAN.ReduceARPProxy, config_parse_bool, 0, offsetof(VxLan, arp_proxy)
+VXLAN.L2MissNotification, config_parse_bool, 0, offsetof(VxLan, l2miss)
+VXLAN.L3MissNotification, config_parse_bool, 0, offsetof(VxLan, l3miss)
+VXLAN.RouteShortCircuit, config_parse_bool, 0, offsetof(VxLan, route_short_circuit)
+VXLAN.UDPCheckSum, config_parse_bool, 0, offsetof(VxLan, udpcsum)
+VXLAN.UDPChecksum, config_parse_bool, 0, offsetof(VxLan, udpcsum)
+VXLAN.UDP6ZeroCheckSumRx, config_parse_bool, 0, offsetof(VxLan, udp6zerocsumrx)
+VXLAN.UDP6ZeroChecksumRx, config_parse_bool, 0, offsetof(VxLan, udp6zerocsumrx)
+VXLAN.UDP6ZeroCheckSumTx, config_parse_bool, 0, offsetof(VxLan, udp6zerocsumtx)
+VXLAN.UDP6ZeroChecksumTx, config_parse_bool, 0, offsetof(VxLan, udp6zerocsumtx)
+VXLAN.RemoteChecksumTx, config_parse_bool, 0, offsetof(VxLan, remote_csum_tx)
+VXLAN.RemoteChecksumRx, config_parse_bool, 0, offsetof(VxLan, remote_csum_rx)
+VXLAN.FDBAgeingSec, config_parse_sec, 0, offsetof(VxLan, fdb_ageing)
+VXLAN.GroupPolicyExtension, config_parse_bool, 0, offsetof(VxLan, group_policy)
+VXLAN.GenericProtocolExtension, config_parse_bool, 0, offsetof(VxLan, generic_protocol_extension)
+VXLAN.MaximumFDBEntries, config_parse_unsigned, 0, offsetof(VxLan, max_fdb)
+VXLAN.PortRange, config_parse_port_range, 0, 0
+VXLAN.DestinationPort, config_parse_ip_port, 0, offsetof(VxLan, dest_port)
+VXLAN.FlowLabel, config_parse_flow_label, 0, 0
+VXLAN.IPDoNotFragment, config_parse_df, 0, offsetof(VxLan, df)
+VXLAN.Independent, config_parse_bool, 0, offsetof(VxLan, independent)
+GENEVE.Id, config_parse_geneve_vni, 0, offsetof(Geneve, id)
+GENEVE.Remote, config_parse_geneve_address, 0, offsetof(Geneve, remote)
+GENEVE.TOS, config_parse_uint8, 0, offsetof(Geneve, tos)
+GENEVE.TTL, config_parse_geneve_ttl, 0, offsetof(Geneve, ttl)
+GENEVE.UDPChecksum, config_parse_bool, 0, offsetof(Geneve, udpcsum)
+GENEVE.UDP6ZeroCheckSumRx, config_parse_bool, 0, offsetof(Geneve, udp6zerocsumrx)
+GENEVE.UDP6ZeroChecksumRx, config_parse_bool, 0, offsetof(Geneve, udp6zerocsumrx)
+GENEVE.UDP6ZeroCheckSumTx, config_parse_bool, 0, offsetof(Geneve, udp6zerocsumtx)
+GENEVE.UDP6ZeroChecksumTx, config_parse_bool, 0, offsetof(Geneve, udp6zerocsumtx)
+GENEVE.DestinationPort, config_parse_ip_port, 0, offsetof(Geneve, dest_port)
+GENEVE.IPDoNotFragment, config_parse_geneve_df, 0, offsetof(Geneve, geneve_df)
+GENEVE.FlowLabel, config_parse_geneve_flow_label, 0, 0
+MACsec.Port, config_parse_macsec_port, 0, 0
+MACsec.Encrypt, config_parse_tristate, 0, offsetof(MACsec, encrypt)
+MACsecReceiveChannel.Port, config_parse_macsec_port, 0, 0
+MACsecReceiveChannel.MACAddress, config_parse_macsec_hw_address, 0, 0
+MACsecTransmitAssociation.PacketNumber, config_parse_macsec_packet_number, 0, 0
+MACsecTransmitAssociation.KeyId, config_parse_macsec_key_id, 0, 0
+MACsecTransmitAssociation.Key, config_parse_macsec_key, 0, 0
+MACsecTransmitAssociation.KeyFile, config_parse_macsec_key_file, 0, 0
+MACsecTransmitAssociation.Activate, config_parse_macsec_sa_activate, 0, 0
+MACsecTransmitAssociation.UseForEncoding, config_parse_macsec_use_for_encoding, 0, 0
+MACsecReceiveAssociation.Port, config_parse_macsec_port, 0, 0
+MACsecReceiveAssociation.MACAddress, config_parse_macsec_hw_address, 0, 0
+MACsecReceiveAssociation.PacketNumber, config_parse_macsec_packet_number, 0, 0
+MACsecReceiveAssociation.KeyId, config_parse_macsec_key_id, 0, 0
+MACsecReceiveAssociation.Key, config_parse_macsec_key, 0, 0
+MACsecReceiveAssociation.KeyFile, config_parse_macsec_key_file, 0, 0
+MACsecReceiveAssociation.Activate, config_parse_macsec_sa_activate, 0, 0
+Tun.OneQueue, config_parse_warn_compat, DISABLED_LEGACY, 0
+Tun.MultiQueue, config_parse_bool, 0, offsetof(TunTap, multi_queue)
+Tun.PacketInfo, config_parse_bool, 0, offsetof(TunTap, packet_info)
+Tun.VNetHeader, config_parse_bool, 0, offsetof(TunTap, vnet_hdr)
+Tun.User, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(TunTap, user_name)
+Tun.Group, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(TunTap, group_name)
+Tun.KeepCarrier, config_parse_bool, 0, offsetof(TunTap, keep_fd)
+Tap.OneQueue, config_parse_warn_compat, DISABLED_LEGACY, 0
+Tap.MultiQueue, config_parse_bool, 0, offsetof(TunTap, multi_queue)
+Tap.PacketInfo, config_parse_bool, 0, offsetof(TunTap, packet_info)
+Tap.VNetHeader, config_parse_bool, 0, offsetof(TunTap, vnet_hdr)
+Tap.User, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(TunTap, user_name)
+Tap.Group, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(TunTap, group_name)
+Tap.KeepCarrier, config_parse_bool, 0, offsetof(TunTap, keep_fd)
+Bond.Mode, config_parse_bond_mode, 0, offsetof(Bond, mode)
+Bond.TransmitHashPolicy, config_parse_bond_xmit_hash_policy, 0, offsetof(Bond, xmit_hash_policy)
+Bond.LACPTransmitRate, config_parse_bond_lacp_rate, 0, offsetof(Bond, lacp_rate)
+Bond.AdSelect, config_parse_bond_ad_select, 0, offsetof(Bond, ad_select)
+Bond.FailOverMACPolicy, config_parse_bond_fail_over_mac, 0, offsetof(Bond, fail_over_mac)
+Bond.ARPIPTargets, config_parse_arp_ip_target_address, 0, 0
+Bond.ARPValidate, config_parse_bond_arp_validate, 0, offsetof(Bond, arp_validate)
+Bond.ARPAllTargets, config_parse_bond_arp_all_targets, 0, offsetof(Bond, arp_all_targets)
+Bond.PrimaryReselectPolicy, config_parse_bond_primary_reselect, 0, offsetof(Bond, primary_reselect)
+Bond.ResendIGMP, config_parse_unsigned, 0, offsetof(Bond, resend_igmp)
+Bond.PacketsPerSlave, config_parse_unsigned, 0, offsetof(Bond, packets_per_slave)
+Bond.GratuitousARP, config_parse_unsigned, 0, offsetof(Bond, num_grat_arp)
+Bond.AllSlavesActive, config_parse_bool, 0, offsetof(Bond, all_slaves_active)
+Bond.DynamicTransmitLoadBalancing, config_parse_tristate, 0, offsetof(Bond, tlb_dynamic_lb)
+Bond.MinLinks, config_parse_unsigned, 0, offsetof(Bond, min_links)
+Bond.MIIMonitorSec, config_parse_sec, 0, offsetof(Bond, miimon)
+Bond.UpDelaySec, config_parse_sec, 0, offsetof(Bond, updelay)
+Bond.DownDelaySec, config_parse_sec, 0, offsetof(Bond, downdelay)
+Bond.ARPIntervalSec, config_parse_sec, 0, offsetof(Bond, arp_interval)
+Bond.LearnPacketIntervalSec, config_parse_sec, 0, offsetof(Bond, lp_interval)
+Bond.AdActorSystemPriority, config_parse_ad_actor_sys_prio, 0, offsetof(Bond, ad_actor_sys_prio)
+Bond.AdUserPortKey, config_parse_ad_user_port_key, 0, offsetof(Bond, ad_user_port_key)
+Bond.AdActorSystem, config_parse_ad_actor_system, 0, offsetof(Bond, ad_actor_system)
+Bridge.HelloTimeSec, config_parse_sec, 0, offsetof(Bridge, hello_time)
+Bridge.MaxAgeSec, config_parse_sec, 0, offsetof(Bridge, max_age)
+Bridge.AgeingTimeSec, config_parse_sec, 0, offsetof(Bridge, ageing_time)
+Bridge.ForwardDelaySec, config_parse_sec, 0, offsetof(Bridge, forward_delay)
+Bridge.Priority, config_parse_uint16, 0, offsetof(Bridge, priority)
+Bridge.GroupForwardMask, config_parse_uint16, 0, offsetof(Bridge, group_fwd_mask)
+Bridge.DefaultPVID, config_parse_default_port_vlanid, 0, offsetof(Bridge, default_pvid)
+Bridge.MulticastQuerier, config_parse_tristate, 0, offsetof(Bridge, mcast_querier)
+Bridge.MulticastSnooping, config_parse_tristate, 0, offsetof(Bridge, mcast_snooping)
+Bridge.VLANFiltering, config_parse_tristate, 0, offsetof(Bridge, vlan_filtering)
+Bridge.VLANProtocol, config_parse_vlanprotocol, 0, offsetof(Bridge, vlan_protocol)
+Bridge.STP, config_parse_tristate, 0, offsetof(Bridge, stp)
+Bridge.MulticastIGMPVersion, config_parse_uint8, 0, offsetof(Bridge, igmp_version)
+VRF.TableId, config_parse_uint32, 0, offsetof(Vrf, table) /* deprecated */
+VRF.Table, config_parse_uint32, 0, offsetof(Vrf, table)
+BareUDP.DestinationPort, config_parse_ip_port, 0, offsetof(BareUDP, dest_port)
+BareUDP.EtherType, config_parse_bare_udp_iftype, 0, offsetof(BareUDP, iftype)
+WireGuard.FirewallMark, config_parse_unsigned, 0, offsetof(Wireguard, fwmark)
+WireGuard.FwMark, config_parse_unsigned, 0, offsetof(Wireguard, fwmark) /* deprecated */
+WireGuard.ListenPort, config_parse_wireguard_listen_port, 0, offsetof(Wireguard, port)
+WireGuard.PrivateKey, config_parse_wireguard_private_key, 0, 0
+WireGuard.PrivateKeyFile, config_parse_wireguard_private_key_file, 0, 0
+WireGuard.RouteTable, config_parse_wireguard_route_table, 0, offsetof(Wireguard, route_table)
+WireGuard.RouteMetric, config_parse_wireguard_route_priority, 0, offsetof(Wireguard, route_priority)
+WireGuardPeer.AllowedIPs, config_parse_wireguard_allowed_ips, 0, 0
+WireGuardPeer.Endpoint, config_parse_wireguard_endpoint, 0, 0
+WireGuardPeer.PublicKey, config_parse_wireguard_peer_key, 0, 0
+WireGuardPeer.PresharedKey, config_parse_wireguard_peer_key, 0, 0
+WireGuardPeer.PresharedKeyFile, config_parse_wireguard_preshared_key_file, 0, 0
+WireGuardPeer.PersistentKeepalive, config_parse_wireguard_keepalive, 0, 0
+WireGuardPeer.RouteTable, config_parse_wireguard_peer_route_table, 0, 0
+WireGuardPeer.RouteMetric, config_parse_wireguard_peer_route_priority,0, 0
+Xfrm.InterfaceId, config_parse_uint32, 0, offsetof(Xfrm, if_id)
+Xfrm.Independent, config_parse_bool, 0, offsetof(Xfrm, independent)
+BatmanAdvanced.Aggregation, config_parse_bool, 0, offsetof(BatmanAdvanced, aggregation)
+BatmanAdvanced.BridgeLoopAvoidance, config_parse_bool, 0, offsetof(BatmanAdvanced, bridge_loop_avoidance)
+BatmanAdvanced.DistributedArpTable, config_parse_bool, 0, offsetof(BatmanAdvanced, distributed_arp_table)
+BatmanAdvanced.Fragmentation, config_parse_bool, 0, offsetof(BatmanAdvanced, fragmentation)
+BatmanAdvanced.GatewayMode, config_parse_batadv_gateway_mode, 0, offsetof(BatmanAdvanced, gateway_mode)
+BatmanAdvanced.GatewayBandwithDown, config_parse_badadv_bandwidth, 0, offsetof(BatmanAdvanced, gateway_bandwidth_down)
+BatmanAdvanced.GatewayBandwithUp, config_parse_badadv_bandwidth, 0, offsetof(BatmanAdvanced, gateway_bandwidth_up)
+BatmanAdvanced.GatewayBandwidthDown, config_parse_badadv_bandwidth, 0, offsetof(BatmanAdvanced, gateway_bandwidth_down)
+BatmanAdvanced.GatewayBandwidthUp, config_parse_badadv_bandwidth, 0, offsetof(BatmanAdvanced, gateway_bandwidth_up)
+BatmanAdvanced.HopPenalty, config_parse_uint8, 0, offsetof(BatmanAdvanced, hop_penalty)
+BatmanAdvanced.OriginatorIntervalSec, config_parse_sec, 0, offsetof(BatmanAdvanced, originator_interval)
+BatmanAdvanced.RoutingAlgorithm, config_parse_batadv_routing_algorithm, 0, offsetof(BatmanAdvanced, routing_algorithm)
+IPoIB.PartitionKey, config_parse_ipoib_pkey, 0, offsetof(IPoIB, pkey)
+IPoIB.Mode, config_parse_ipoib_mode, 0, offsetof(IPoIB, mode)
+IPoIB.IgnoreUserspaceMulticastGroups, config_parse_tristate, 0, offsetof(IPoIB, umcast)
+WLAN.PhysicalDevice, config_parse_wiphy, 0, 0
+WLAN.Type, config_parse_wlan_iftype, 0, offsetof(WLan, iftype)
+WLAN.WDS, config_parse_tristate, 0, offsetof(WLan, wds)
diff --git a/src/network/netdev/netdev-util.c b/src/network/netdev/netdev-util.c
new file mode 100644
index 0000000..2350624
--- /dev/null
+++ b/src/network/netdev/netdev-util.c
@@ -0,0 +1,100 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "netdev-util.h"
+#include "networkd-address.h"
+#include "networkd-link.h"
+#include "string-table.h"
+
+static const char * const netdev_local_address_type_table[_NETDEV_LOCAL_ADDRESS_TYPE_MAX] = {
+ [NETDEV_LOCAL_ADDRESS_IPV4LL] = "ipv4_link_local",
+ [NETDEV_LOCAL_ADDRESS_IPV6LL] = "ipv6_link_local",
+ [NETDEV_LOCAL_ADDRESS_DHCP4] = "dhcp4",
+ [NETDEV_LOCAL_ADDRESS_DHCP6] = "dhcp6",
+ [NETDEV_LOCAL_ADDRESS_SLAAC] = "slaac",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(netdev_local_address_type, NetDevLocalAddressType);
+
+int link_get_local_address(
+ Link *link,
+ NetDevLocalAddressType type,
+ int family,
+ int *ret_family,
+ union in_addr_union *ret_address) {
+
+ Address *a;
+
+ assert(link);
+
+ switch (type) {
+ case NETDEV_LOCAL_ADDRESS_IPV4LL:
+ assert(IN_SET(family, AF_UNSPEC, AF_INET));
+ family = AF_INET;
+ break;
+ case NETDEV_LOCAL_ADDRESS_IPV6LL:
+ assert(IN_SET(family, AF_UNSPEC, AF_INET6));
+ family = AF_INET6;
+ break;
+ case NETDEV_LOCAL_ADDRESS_DHCP4:
+ assert(IN_SET(family, AF_UNSPEC, AF_INET));
+ family = AF_INET;
+ break;
+ case NETDEV_LOCAL_ADDRESS_DHCP6:
+ assert(IN_SET(family, AF_UNSPEC, AF_INET6));
+ family = AF_INET6;
+ break;
+ case NETDEV_LOCAL_ADDRESS_SLAAC:
+ assert(IN_SET(family, AF_UNSPEC, AF_INET6));
+ family = AF_INET6;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false))
+ return -EBUSY;
+
+ SET_FOREACH(a, link->addresses) {
+ if (!address_is_ready(a))
+ continue;
+
+ if (a->family != family)
+ continue;
+
+ if (in_addr_is_set(a->family, &a->in_addr_peer))
+ continue;
+
+ switch (type) {
+ case NETDEV_LOCAL_ADDRESS_IPV4LL:
+ if (a->source != NETWORK_CONFIG_SOURCE_IPV4LL)
+ continue;
+ break;
+ case NETDEV_LOCAL_ADDRESS_IPV6LL:
+ if (!in6_addr_is_link_local(&a->in_addr.in6))
+ continue;
+ break;
+ case NETDEV_LOCAL_ADDRESS_DHCP4:
+ if (a->source != NETWORK_CONFIG_SOURCE_DHCP4)
+ continue;
+ break;
+ case NETDEV_LOCAL_ADDRESS_DHCP6:
+ if (a->source != NETWORK_CONFIG_SOURCE_DHCP6)
+ continue;
+ break;
+ case NETDEV_LOCAL_ADDRESS_SLAAC:
+ if (a->source != NETWORK_CONFIG_SOURCE_NDISC)
+ continue;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ if (ret_family)
+ *ret_family = a->family;
+ if (ret_address)
+ *ret_address = a->in_addr;
+ return 1;
+ }
+
+ return -ENXIO;
+}
diff --git a/src/network/netdev/netdev-util.h b/src/network/netdev/netdev-util.h
new file mode 100644
index 0000000..02b07e3
--- /dev/null
+++ b/src/network/netdev/netdev-util.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "in-addr-util.h"
+#include "macro.h"
+
+typedef struct Link Link;
+
+typedef enum NetDevLocalAddressType {
+ NETDEV_LOCAL_ADDRESS_IPV4LL,
+ NETDEV_LOCAL_ADDRESS_IPV6LL,
+ NETDEV_LOCAL_ADDRESS_DHCP4,
+ NETDEV_LOCAL_ADDRESS_DHCP6,
+ NETDEV_LOCAL_ADDRESS_SLAAC,
+ _NETDEV_LOCAL_ADDRESS_TYPE_MAX,
+ _NETDEV_LOCAL_ADDRESS_TYPE_INVALID = -EINVAL,
+} NetDevLocalAddressType;
+
+const char *netdev_local_address_type_to_string(NetDevLocalAddressType t) _const_;
+NetDevLocalAddressType netdev_local_address_type_from_string(const char *s) _pure_;
+
+int link_get_local_address(
+ Link *link,
+ NetDevLocalAddressType type,
+ int family,
+ int *ret_family,
+ union in_addr_union *ret_address);
diff --git a/src/network/netdev/netdev.c b/src/network/netdev/netdev.c
new file mode 100644
index 0000000..038a27c
--- /dev/null
+++ b/src/network/netdev/netdev.c
@@ -0,0 +1,950 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "arphrd-util.h"
+#include "bareudp.h"
+#include "batadv.h"
+#include "bond.h"
+#include "bridge.h"
+#include "conf-files.h"
+#include "conf-parser.h"
+#include "dummy.h"
+#include "fd-util.h"
+#include "fou-tunnel.h"
+#include "geneve.h"
+#include "ifb.h"
+#include "ipoib.h"
+#include "ipvlan.h"
+#include "l2tp-tunnel.h"
+#include "list.h"
+#include "macsec.h"
+#include "macvlan.h"
+#include "netdev.h"
+#include "netdevsim.h"
+#include "netif-util.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "networkd-queue.h"
+#include "networkd-setlink.h"
+#include "networkd-sriov.h"
+#include "nlmon.h"
+#include "path-lookup.h"
+#include "siphash24.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tunnel.h"
+#include "tuntap.h"
+#include "vcan.h"
+#include "veth.h"
+#include "vlan.h"
+#include "vrf.h"
+#include "vxcan.h"
+#include "vxlan.h"
+#include "wireguard.h"
+#include "wlan.h"
+#include "xfrm.h"
+
+const NetDevVTable * const netdev_vtable[_NETDEV_KIND_MAX] = {
+ [NETDEV_KIND_BAREUDP] = &bare_udp_vtable,
+ [NETDEV_KIND_BATADV] = &batadv_vtable,
+ [NETDEV_KIND_BOND] = &bond_vtable,
+ [NETDEV_KIND_BRIDGE] = &bridge_vtable,
+ [NETDEV_KIND_DUMMY] = &dummy_vtable,
+ [NETDEV_KIND_ERSPAN] = &erspan_vtable,
+ [NETDEV_KIND_FOU] = &foutnl_vtable,
+ [NETDEV_KIND_GENEVE] = &geneve_vtable,
+ [NETDEV_KIND_GRE] = &gre_vtable,
+ [NETDEV_KIND_GRETAP] = &gretap_vtable,
+ [NETDEV_KIND_IFB] = &ifb_vtable,
+ [NETDEV_KIND_IP6GRE] = &ip6gre_vtable,
+ [NETDEV_KIND_IP6GRETAP] = &ip6gretap_vtable,
+ [NETDEV_KIND_IP6TNL] = &ip6tnl_vtable,
+ [NETDEV_KIND_IPIP] = &ipip_vtable,
+ [NETDEV_KIND_IPOIB] = &ipoib_vtable,
+ [NETDEV_KIND_IPVLAN] = &ipvlan_vtable,
+ [NETDEV_KIND_IPVTAP] = &ipvtap_vtable,
+ [NETDEV_KIND_L2TP] = &l2tptnl_vtable,
+ [NETDEV_KIND_MACSEC] = &macsec_vtable,
+ [NETDEV_KIND_MACVLAN] = &macvlan_vtable,
+ [NETDEV_KIND_MACVTAP] = &macvtap_vtable,
+ [NETDEV_KIND_NETDEVSIM] = &netdevsim_vtable,
+ [NETDEV_KIND_NLMON] = &nlmon_vtable,
+ [NETDEV_KIND_SIT] = &sit_vtable,
+ [NETDEV_KIND_TAP] = &tap_vtable,
+ [NETDEV_KIND_TUN] = &tun_vtable,
+ [NETDEV_KIND_VCAN] = &vcan_vtable,
+ [NETDEV_KIND_VETH] = &veth_vtable,
+ [NETDEV_KIND_VLAN] = &vlan_vtable,
+ [NETDEV_KIND_VRF] = &vrf_vtable,
+ [NETDEV_KIND_VTI6] = &vti6_vtable,
+ [NETDEV_KIND_VTI] = &vti_vtable,
+ [NETDEV_KIND_VXCAN] = &vxcan_vtable,
+ [NETDEV_KIND_VXLAN] = &vxlan_vtable,
+ [NETDEV_KIND_WIREGUARD] = &wireguard_vtable,
+ [NETDEV_KIND_WLAN] = &wlan_vtable,
+ [NETDEV_KIND_XFRM] = &xfrm_vtable,
+};
+
+static const char* const netdev_kind_table[_NETDEV_KIND_MAX] = {
+ [NETDEV_KIND_BAREUDP] = "bareudp",
+ [NETDEV_KIND_BATADV] = "batadv",
+ [NETDEV_KIND_BOND] = "bond",
+ [NETDEV_KIND_BRIDGE] = "bridge",
+ [NETDEV_KIND_DUMMY] = "dummy",
+ [NETDEV_KIND_ERSPAN] = "erspan",
+ [NETDEV_KIND_FOU] = "fou",
+ [NETDEV_KIND_GENEVE] = "geneve",
+ [NETDEV_KIND_GRE] = "gre",
+ [NETDEV_KIND_GRETAP] = "gretap",
+ [NETDEV_KIND_IFB] = "ifb",
+ [NETDEV_KIND_IP6GRE] = "ip6gre",
+ [NETDEV_KIND_IP6GRETAP] = "ip6gretap",
+ [NETDEV_KIND_IP6TNL] = "ip6tnl",
+ [NETDEV_KIND_IPIP] = "ipip",
+ [NETDEV_KIND_IPOIB] = "ipoib",
+ [NETDEV_KIND_IPVLAN] = "ipvlan",
+ [NETDEV_KIND_IPVTAP] = "ipvtap",
+ [NETDEV_KIND_L2TP] = "l2tp",
+ [NETDEV_KIND_MACSEC] = "macsec",
+ [NETDEV_KIND_MACVLAN] = "macvlan",
+ [NETDEV_KIND_MACVTAP] = "macvtap",
+ [NETDEV_KIND_NETDEVSIM] = "netdevsim",
+ [NETDEV_KIND_NLMON] = "nlmon",
+ [NETDEV_KIND_SIT] = "sit",
+ [NETDEV_KIND_TAP] = "tap",
+ [NETDEV_KIND_TUN] = "tun",
+ [NETDEV_KIND_VCAN] = "vcan",
+ [NETDEV_KIND_VETH] = "veth",
+ [NETDEV_KIND_VLAN] = "vlan",
+ [NETDEV_KIND_VRF] = "vrf",
+ [NETDEV_KIND_VTI6] = "vti6",
+ [NETDEV_KIND_VTI] = "vti",
+ [NETDEV_KIND_VXCAN] = "vxcan",
+ [NETDEV_KIND_VXLAN] = "vxlan",
+ [NETDEV_KIND_WIREGUARD] = "wireguard",
+ [NETDEV_KIND_WLAN] = "wlan",
+ [NETDEV_KIND_XFRM] = "xfrm",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(netdev_kind, NetDevKind);
+
+bool netdev_is_managed(NetDev *netdev) {
+ if (!netdev || !netdev->manager || !netdev->ifname)
+ return false;
+
+ return hashmap_get(netdev->manager->netdevs, netdev->ifname) == netdev;
+}
+
+static bool netdev_is_stacked_and_independent(NetDev *netdev) {
+ assert(netdev);
+
+ if (netdev_get_create_type(netdev) != NETDEV_CREATE_STACKED)
+ return false;
+
+ switch (netdev->kind) {
+ case NETDEV_KIND_ERSPAN:
+ return ERSPAN(netdev)->independent;
+ case NETDEV_KIND_GRE:
+ return GRE(netdev)->independent;
+ case NETDEV_KIND_GRETAP:
+ return GRETAP(netdev)->independent;
+ case NETDEV_KIND_IP6GRE:
+ return IP6GRE(netdev)->independent;
+ case NETDEV_KIND_IP6GRETAP:
+ return IP6GRETAP(netdev)->independent;
+ case NETDEV_KIND_IP6TNL:
+ return IP6TNL(netdev)->independent;
+ case NETDEV_KIND_IPIP:
+ return IPIP(netdev)->independent;
+ case NETDEV_KIND_SIT:
+ return SIT(netdev)->independent;
+ case NETDEV_KIND_VTI:
+ return VTI(netdev)->independent;
+ case NETDEV_KIND_VTI6:
+ return VTI6(netdev)->independent;
+ case NETDEV_KIND_VXLAN:
+ return VXLAN(netdev)->independent;
+ case NETDEV_KIND_XFRM:
+ return XFRM(netdev)->independent;
+ default:
+ return false;
+ }
+}
+
+static bool netdev_is_stacked(NetDev *netdev) {
+ assert(netdev);
+
+ if (netdev_get_create_type(netdev) != NETDEV_CREATE_STACKED)
+ return false;
+
+ if (netdev_is_stacked_and_independent(netdev))
+ return false;
+
+ return true;
+}
+
+static void netdev_detach_from_manager(NetDev *netdev) {
+ if (netdev->ifname && netdev->manager)
+ hashmap_remove(netdev->manager->netdevs, netdev->ifname);
+}
+
+static NetDev *netdev_free(NetDev *netdev) {
+ assert(netdev);
+
+ netdev_detach_from_manager(netdev);
+
+ free(netdev->filename);
+
+ free(netdev->description);
+ free(netdev->ifname);
+ condition_free_list(netdev->conditions);
+
+ /* Invoke the per-kind done() destructor, but only if the state field is initialized. We conditionalize that
+ * because we parse .netdev files twice: once to determine the kind (with a short, minimal NetDev structure
+ * allocation, with no room for per-kind fields), and once to read the kind's properties (with a full,
+ * comprehensive NetDev structure allocation with enough space for whatever the specific kind needs). Now, in
+ * the first case we shouldn't try to destruct the per-kind NetDev fields on destruction, in the second case we
+ * should. We use the state field to discern the two cases: it's _NETDEV_STATE_INVALID on the first "raw"
+ * call. */
+ if (netdev->state != _NETDEV_STATE_INVALID &&
+ NETDEV_VTABLE(netdev) &&
+ NETDEV_VTABLE(netdev)->done)
+ NETDEV_VTABLE(netdev)->done(netdev);
+
+ return mfree(netdev);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(NetDev, netdev, netdev_free);
+
+void netdev_drop(NetDev *netdev) {
+ if (!netdev)
+ return;
+
+ if (netdev_is_stacked(netdev)) {
+ /* The netdev may be removed due to the underlying device removal, and the device may
+ * be re-added later. */
+ netdev->state = NETDEV_STATE_LOADING;
+ netdev->ifindex = 0;
+
+ log_netdev_debug(netdev, "netdev removed");
+ return;
+ }
+
+ if (NETDEV_VTABLE(netdev) && NETDEV_VTABLE(netdev)->drop)
+ NETDEV_VTABLE(netdev)->drop(netdev);
+
+ netdev->state = NETDEV_STATE_LINGER;
+
+ log_netdev_debug(netdev, "netdev removed");
+
+ netdev_detach_from_manager(netdev);
+ netdev_unref(netdev);
+ return;
+}
+
+int netdev_get(Manager *manager, const char *name, NetDev **ret) {
+ NetDev *netdev;
+
+ assert(manager);
+ assert(name);
+ assert(ret);
+
+ netdev = hashmap_get(manager->netdevs, name);
+ if (!netdev)
+ return -ENOENT;
+
+ *ret = netdev;
+
+ return 0;
+}
+
+void netdev_enter_failed(NetDev *netdev) {
+ netdev->state = NETDEV_STATE_FAILED;
+}
+
+static int netdev_enter_ready(NetDev *netdev) {
+ assert(netdev);
+ assert(netdev->ifname);
+
+ if (netdev->state != NETDEV_STATE_CREATING)
+ return 0;
+
+ netdev->state = NETDEV_STATE_READY;
+
+ log_netdev_info(netdev, "netdev ready");
+
+ if (NETDEV_VTABLE(netdev)->post_create)
+ NETDEV_VTABLE(netdev)->post_create(netdev, NULL);
+
+ return 0;
+}
+
+/* callback for netdev's created without a backing Link */
+static int netdev_create_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
+ int r;
+
+ assert(netdev);
+ assert(netdev->state != _NETDEV_STATE_INVALID);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EEXIST)
+ log_netdev_info(netdev, "netdev exists, using existing without changing its parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "netdev could not be created: %m");
+ netdev_enter_failed(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "Created");
+
+ return 1;
+}
+
+int netdev_set_ifindex(NetDev *netdev, sd_netlink_message *message) {
+ uint16_t type;
+ const char *kind;
+ const char *received_kind;
+ const char *received_name;
+ int r, ifindex;
+
+ assert(netdev);
+ assert(message);
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not get rtnl message type: %m");
+
+ if (type != RTM_NEWLINK)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), "Cannot set ifindex from unexpected rtnl message type.");
+
+ r = sd_rtnl_message_link_get_ifindex(message, &ifindex);
+ if (r < 0) {
+ log_netdev_error_errno(netdev, r, "Could not get ifindex: %m");
+ netdev_enter_failed(netdev);
+ return r;
+ } else if (ifindex <= 0) {
+ log_netdev_error(netdev, "Got invalid ifindex: %d", ifindex);
+ netdev_enter_failed(netdev);
+ return -EINVAL;
+ }
+
+ if (netdev->ifindex > 0) {
+ if (netdev->ifindex != ifindex) {
+ log_netdev_error(netdev, "Could not set ifindex to %d, already set to %d",
+ ifindex, netdev->ifindex);
+ netdev_enter_failed(netdev);
+ return -EEXIST;
+ } else
+ /* ifindex already set to the same for this netdev */
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string(message, IFLA_IFNAME, &received_name);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not get IFNAME: %m");
+
+ if (!streq(netdev->ifname, received_name)) {
+ log_netdev_error(netdev, "Received newlink with wrong IFNAME %s", received_name);
+ netdev_enter_failed(netdev);
+ return -EINVAL;
+ }
+
+ if (!NETDEV_VTABLE(netdev)->skip_netdev_kind_check) {
+
+ r = sd_netlink_message_enter_container(message, IFLA_LINKINFO);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not get LINKINFO: %m");
+
+ r = sd_netlink_message_read_string(message, IFLA_INFO_KIND, &received_kind);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not get KIND: %m");
+
+ r = sd_netlink_message_exit_container(message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not exit container: %m");
+
+ if (netdev->kind == NETDEV_KIND_TAP)
+ /* the kernel does not distinguish between tun and tap */
+ kind = "tun";
+ else {
+ kind = netdev_kind_to_string(netdev->kind);
+ if (!kind) {
+ log_netdev_error(netdev, "Could not get kind");
+ netdev_enter_failed(netdev);
+ return -EINVAL;
+ }
+ }
+
+ if (!streq(kind, received_kind)) {
+ log_netdev_error(netdev, "Received newlink with wrong KIND %s, expected %s",
+ received_kind, kind);
+ netdev_enter_failed(netdev);
+ return -EINVAL;
+ }
+ }
+
+ netdev->ifindex = ifindex;
+
+ log_netdev_debug(netdev, "netdev has index %d", netdev->ifindex);
+
+ netdev_enter_ready(netdev);
+
+ return 0;
+}
+
+#define HASH_KEY SD_ID128_MAKE(52,e1,45,bd,00,6f,29,96,21,c6,30,6d,83,71,04,48)
+
+int netdev_generate_hw_addr(
+ NetDev *netdev,
+ Link *parent,
+ const char *name,
+ const struct hw_addr_data *hw_addr,
+ struct hw_addr_data *ret) {
+
+ struct hw_addr_data a = HW_ADDR_NULL;
+ bool is_static = false;
+ int r;
+
+ assert(netdev);
+ assert(name);
+ assert(hw_addr);
+ assert(ret);
+
+ if (hw_addr_equal(hw_addr, &HW_ADDR_NONE)) {
+ *ret = HW_ADDR_NULL;
+ return 0;
+ }
+
+ if (hw_addr->length == 0) {
+ uint64_t result;
+
+ /* HardwareAddress= is not specified. */
+
+ if (!NETDEV_VTABLE(netdev)->generate_mac)
+ goto finalize;
+
+ if (!IN_SET(NETDEV_VTABLE(netdev)->iftype, ARPHRD_ETHER, ARPHRD_INFINIBAND))
+ goto finalize;
+
+ r = net_get_unique_predictable_data_from_name(name, &HASH_KEY, &result);
+ if (r < 0) {
+ log_netdev_warning_errno(netdev, r,
+ "Failed to generate persistent MAC address, ignoring: %m");
+ goto finalize;
+ }
+
+ a.length = arphrd_to_hw_addr_len(NETDEV_VTABLE(netdev)->iftype);
+
+ switch (NETDEV_VTABLE(netdev)->iftype) {
+ case ARPHRD_ETHER:
+ assert(a.length <= sizeof(result));
+ memcpy(a.bytes, &result, a.length);
+
+ if (ether_addr_is_null(&a.ether) || ether_addr_is_broadcast(&a.ether)) {
+ log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "Failed to generate persistent MAC address, ignoring: %m");
+ a = HW_ADDR_NULL;
+ goto finalize;
+ }
+
+ break;
+ case ARPHRD_INFINIBAND:
+ if (result == 0) {
+ log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "Failed to generate persistent MAC address: %m");
+ goto finalize;
+ }
+
+ assert(a.length >= sizeof(result));
+ memzero(a.bytes, a.length - sizeof(result));
+ memcpy(a.bytes + a.length - sizeof(result), &result, sizeof(result));
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ } else {
+ a = *hw_addr;
+ is_static = true;
+ }
+
+ r = net_verify_hardware_address(name, is_static, NETDEV_VTABLE(netdev)->iftype,
+ parent ? &parent->hw_addr : NULL, &a);
+ if (r < 0)
+ return r;
+
+finalize:
+ *ret = a;
+ return 0;
+}
+
+static int netdev_create_message(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ int r;
+
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, netdev->ifname);
+ if (r < 0)
+ return r;
+
+ struct hw_addr_data hw_addr;
+ r = netdev_generate_hw_addr(netdev, link, netdev->ifname, &netdev->hw_addr, &hw_addr);
+ if (r < 0)
+ return r;
+
+ if (hw_addr.length > 0) {
+ log_netdev_debug(netdev, "Using MAC address: %s", HW_ADDR_TO_STR(&hw_addr));
+ r = netlink_message_append_hw_addr(m, IFLA_ADDRESS, &hw_addr);
+ if (r < 0)
+ return r;
+ }
+
+ if (netdev->mtu != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_MTU, netdev->mtu);
+ if (r < 0)
+ return r;
+ }
+
+ if (link) {
+ r = sd_netlink_message_append_u32(m, IFLA_LINK, link->ifindex);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_open_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return r;
+
+ if (NETDEV_VTABLE(netdev)->fill_message_create) {
+ r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, netdev_kind_to_string(netdev->kind));
+ if (r < 0)
+ return r;
+
+ r = NETDEV_VTABLE(netdev)->fill_message_create(netdev, link, m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_netlink_message_append_string(m, IFLA_INFO_KIND, netdev_kind_to_string(netdev->kind));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int independent_netdev_create(NetDev *netdev) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+
+ /* create netdev */
+ if (NETDEV_VTABLE(netdev)->create) {
+ r = NETDEV_VTABLE(netdev)->create(netdev);
+ if (r < 0)
+ return r;
+
+ log_netdev_debug(netdev, "Created");
+ return 0;
+ }
+
+ r = sd_rtnl_message_new_link(netdev->manager->rtnl, &m, RTM_NEWLINK, 0);
+ if (r < 0)
+ return r;
+
+ r = netdev_create_message(netdev, NULL, m);
+ if (r < 0)
+ return r;
+
+ r = netlink_call_async(netdev->manager->rtnl, NULL, m, netdev_create_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return r;
+
+ netdev_ref(netdev);
+
+ netdev->state = NETDEV_STATE_CREATING;
+ log_netdev_debug(netdev, "Creating");
+ return 0;
+}
+
+static int stacked_netdev_create(NetDev *netdev, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+ assert(netdev->manager);
+ assert(link);
+ assert(req);
+
+ r = sd_rtnl_message_new_link(netdev->manager->rtnl, &m, RTM_NEWLINK, 0);
+ if (r < 0)
+ return r;
+
+ r = netdev_create_message(netdev, link, m);
+ if (r < 0)
+ return r;
+
+ r = request_call_netlink_async(netdev->manager->rtnl, m, req);
+ if (r < 0)
+ return r;
+
+ netdev->state = NETDEV_STATE_CREATING;
+ log_netdev_debug(netdev, "Creating");
+ return 0;
+}
+
+static bool link_is_ready_to_create_stacked_netdev_one(Link *link, bool allow_unmanaged) {
+ assert(link);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED, LINK_STATE_UNMANAGED))
+ return false;
+
+ if (!link->network)
+ return allow_unmanaged;
+
+ if (link->set_link_messages > 0)
+ return false;
+
+ /* If stacked netdevs are created before the underlying interface being activated, then
+ * the activation policy for the netdevs are ignored. See issue #22593. */
+ if (!link->activated)
+ return false;
+
+ return true;
+}
+
+static bool link_is_ready_to_create_stacked_netdev(Link *link) {
+ return check_ready_for_all_sr_iov_ports(link, /* allow_unmanaged = */ false,
+ link_is_ready_to_create_stacked_netdev_one);
+}
+
+static int netdev_is_ready_to_create(NetDev *netdev, Link *link) {
+ assert(netdev);
+
+ if (netdev->state != NETDEV_STATE_LOADING)
+ return false;
+
+ if (link && !link_is_ready_to_create_stacked_netdev(link))
+ return false;
+
+ if (NETDEV_VTABLE(netdev)->is_ready_to_create)
+ return NETDEV_VTABLE(netdev)->is_ready_to_create(netdev, link);
+
+ return true;
+}
+
+static int stacked_netdev_process_request(Request *req, Link *link, void *userdata) {
+ NetDev *netdev = ASSERT_PTR(userdata);
+ int r;
+
+ assert(req);
+ assert(link);
+
+ r = netdev_is_ready_to_create(netdev, link);
+ if (r <= 0)
+ return r;
+
+ r = stacked_netdev_create(netdev, link, req);
+ if (r < 0)
+ return log_netdev_warning_errno(netdev, r, "Failed to create netdev: %m");
+
+ return 1;
+}
+
+static int create_stacked_netdev_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not create stacked netdev");
+ link_enter_failed(link);
+ return 0;
+ }
+
+ if (link->create_stacked_netdev_messages == 0) {
+ link->stacked_netdevs_created = true;
+ log_link_debug(link, "Stacked netdevs created.");
+ link_check_ready(link);
+ }
+
+ return 0;
+}
+
+int link_request_stacked_netdev(Link *link, NetDev *netdev) {
+ int r;
+
+ assert(link);
+ assert(netdev);
+
+ if (!netdev_is_stacked(netdev))
+ return -EINVAL;
+
+ if (!IN_SET(netdev->state, NETDEV_STATE_LOADING, NETDEV_STATE_FAILED) || netdev->ifindex > 0)
+ return 0; /* Already created. */
+
+ link->stacked_netdevs_created = false;
+ r = link_queue_request_full(link, REQUEST_TYPE_NETDEV_STACKED,
+ netdev_ref(netdev), (mfree_func_t) netdev_unref,
+ trivial_hash_func, trivial_compare_func,
+ stacked_netdev_process_request,
+ &link->create_stacked_netdev_messages,
+ create_stacked_netdev_handler, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request stacked netdev '%s': %m",
+ netdev->ifname);
+
+ log_link_debug(link, "Requested stacked netdev '%s'", netdev->ifname);
+ return 0;
+}
+
+static int independent_netdev_process_request(Request *req, Link *link, void *userdata) {
+ NetDev *netdev = ASSERT_PTR(userdata);
+ int r;
+
+ assert(!link);
+
+ r = netdev_is_ready_to_create(netdev, NULL);
+ if (r <= 0)
+ return r;
+
+ r = independent_netdev_create(netdev);
+ if (r < 0)
+ return log_netdev_warning_errno(netdev, r, "Failed to create netdev: %m");
+
+ return 1;
+}
+
+static int netdev_request_to_create(NetDev *netdev) {
+ int r;
+
+ assert(netdev);
+
+ if (netdev_is_stacked(netdev))
+ return 0;
+
+ r = netdev_is_ready_to_create(netdev, NULL);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ /* If the netdev has no dependency, then create it now. */
+ r = independent_netdev_create(netdev);
+ if (r < 0)
+ return log_netdev_warning_errno(netdev, r, "Failed to create netdev: %m");
+
+ } else {
+ /* Otherwise, wait for the dependencies being resolved. */
+ r = netdev_queue_request(netdev, independent_netdev_process_request, NULL);
+ if (r < 0)
+ return log_netdev_warning_errno(netdev, r, "Failed to request to create netdev: %m");
+ }
+
+ return 0;
+}
+
+int netdev_load_one(Manager *manager, const char *filename) {
+ _cleanup_(netdev_unrefp) NetDev *netdev_raw = NULL, *netdev = NULL;
+ const char *dropin_dirname;
+ int r;
+
+ assert(manager);
+ assert(filename);
+
+ r = null_or_empty_path(filename);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to check if \"%s\" is empty: %m", filename);
+ if (r > 0) {
+ log_debug("Skipping empty file: %s", filename);
+ return 0;
+ }
+
+ netdev_raw = new(NetDev, 1);
+ if (!netdev_raw)
+ return log_oom();
+
+ *netdev_raw = (NetDev) {
+ .n_ref = 1,
+ .kind = _NETDEV_KIND_INVALID,
+ .state = _NETDEV_STATE_INVALID, /* an invalid state means done() of the implementation won't be called on destruction */
+ };
+
+ dropin_dirname = strjoina(basename(filename), ".d");
+ r = config_parse_many(
+ STRV_MAKE_CONST(filename), NETWORK_DIRS, dropin_dirname,
+ NETDEV_COMMON_SECTIONS NETDEV_OTHER_SECTIONS,
+ config_item_perf_lookup, network_netdev_gperf_lookup,
+ CONFIG_PARSE_WARN,
+ netdev_raw,
+ NULL,
+ NULL);
+ if (r < 0)
+ return r; /* config_parse_many() logs internally. */
+
+ /* skip out early if configuration does not match the environment */
+ if (!condition_test_list(netdev_raw->conditions, environ, NULL, NULL, NULL)) {
+ log_debug("%s: Conditions in the file do not match the system environment, skipping.", filename);
+ return 0;
+ }
+
+ if (netdev_raw->kind == _NETDEV_KIND_INVALID)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "NetDev has no Kind= configured in \"%s\", ignoring.", filename);
+
+ if (!netdev_raw->ifname)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "NetDev without Name= configured in \"%s\", ignoring.", filename);
+
+ netdev = malloc0(NETDEV_VTABLE(netdev_raw)->object_size);
+ if (!netdev)
+ return log_oom();
+
+ netdev->n_ref = 1;
+ netdev->manager = manager;
+ netdev->kind = netdev_raw->kind;
+ netdev->state = NETDEV_STATE_LOADING; /* we initialize the state here for the first time,
+ so that done() will be called on destruction */
+
+ if (NETDEV_VTABLE(netdev)->init)
+ NETDEV_VTABLE(netdev)->init(netdev);
+
+ r = config_parse_many(
+ STRV_MAKE_CONST(filename), NETWORK_DIRS, dropin_dirname,
+ NETDEV_VTABLE(netdev)->sections,
+ config_item_perf_lookup, network_netdev_gperf_lookup,
+ CONFIG_PARSE_WARN,
+ netdev, NULL, NULL);
+ if (r < 0)
+ return r; /* config_parse_many() logs internally. */
+
+ /* verify configuration */
+ if (NETDEV_VTABLE(netdev)->config_verify) {
+ r = NETDEV_VTABLE(netdev)->config_verify(netdev, filename);
+ if (r < 0)
+ return r; /* config_verify() logs internally. */
+ }
+
+ netdev->filename = strdup(filename);
+ if (!netdev->filename)
+ return log_oom();
+
+ r = hashmap_ensure_put(&netdev->manager->netdevs, &string_hash_ops, netdev->ifname, netdev);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r == -EEXIST) {
+ NetDev *n = hashmap_get(netdev->manager->netdevs, netdev->ifname);
+
+ assert(n);
+ if (!streq(netdev->filename, n->filename))
+ log_netdev_warning_errno(netdev, r,
+ "Device was already configured by \"%s\", ignoring %s.",
+ n->filename, netdev->filename);
+
+ /* Clear ifname before netdev_free() is called. Otherwise, the NetDev object 'n' is
+ * removed from the hashmap 'manager->netdevs'. */
+ netdev->ifname = mfree(netdev->ifname);
+ return -EEXIST;
+ }
+ assert(r > 0);
+
+ log_netdev_debug(netdev, "loaded \"%s\"", netdev_kind_to_string(netdev->kind));
+
+ r = netdev_request_to_create(netdev);
+ if (r < 0)
+ return r; /* netdev_request_to_create() logs internally. */
+
+ TAKE_PTR(netdev);
+ return 0;
+}
+
+int netdev_load(Manager *manager, bool reload) {
+ _cleanup_strv_free_ char **files = NULL;
+ int r;
+
+ assert(manager);
+
+ if (!reload)
+ hashmap_clear_with_destructor(manager->netdevs, netdev_unref);
+
+ r = conf_files_list_strv(&files, ".netdev", NULL, 0, NETWORK_DIRS);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate netdev files: %m");
+
+ STRV_FOREACH(f, files)
+ (void) netdev_load_one(manager, *f);
+
+ return 0;
+}
+
+int config_parse_netdev_kind(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ NetDevKind k, *kind = ASSERT_PTR(data);
+
+ assert(filename);
+ assert(rvalue);
+
+ k = netdev_kind_from_string(rvalue);
+ if (k < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, k, "Failed to parse netdev kind, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (*kind != _NETDEV_KIND_INVALID && *kind != k) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified netdev kind is different from the previous value '%s', ignoring assignment: %s",
+ netdev_kind_to_string(*kind), rvalue);
+ return 0;
+ }
+
+ *kind = k;
+
+ return 0;
+}
+
+int config_parse_netdev_hw_addr(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ struct hw_addr_data *hw_addr = ASSERT_PTR(data);
+
+ assert(rvalue);
+
+ if (streq(rvalue, "none")) {
+ *hw_addr = HW_ADDR_NONE;
+ return 0;
+ }
+
+ return config_parse_hw_addr(unit, filename, line, section, section_line, lvalue, ltype, rvalue, data, userdata);
+}
diff --git a/src/network/netdev/netdev.h b/src/network/netdev/netdev.h
new file mode 100644
index 0000000..49eadbb
--- /dev/null
+++ b/src/network/netdev/netdev.h
@@ -0,0 +1,262 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+#include "ether-addr-util.h"
+#include "list.h"
+#include "log-link.h"
+#include "networkd-link.h"
+#include "time-util.h"
+
+/* Special hardware address value to suppress generating persistent hardware address for the netdev. */
+#define HW_ADDR_NONE ((struct hw_addr_data) { .length = 1, })
+
+#define NETDEV_COMMON_SECTIONS "Match\0NetDev\0"
+/* This is the list of known sections. We need to ignore them in the initial parsing phase. */
+#define NETDEV_OTHER_SECTIONS \
+ "-BareUDP\0" \
+ "-BatmanAdvanced\0" \
+ "-Bond\0" \
+ "-Bridge\0" \
+ "-FooOverUDP\0" \
+ "-GENEVE\0" \
+ "-IPoIB\0" \
+ "-IPVLAN\0" \
+ "-IPVTAP\0" \
+ "-L2TP\0" \
+ "-L2TPSession\0" \
+ "-MACsec\0" \
+ "-MACsecReceiveAssociation\0" \
+ "-MACsecReceiveChannel\0" \
+ "-MACsecTransmitAssociation\0" \
+ "-MACVLAN\0" \
+ "-MACVTAP\0" \
+ "-Peer\0" \
+ "-Tap\0" \
+ "-Tun\0" \
+ "-Tunnel\0" \
+ "-VLAN\0" \
+ "-VRF\0" \
+ "-VXCAN\0" \
+ "-VXLAN\0" \
+ "-WLAN\0" \
+ "-WireGuard\0" \
+ "-WireGuardPeer\0" \
+ "-Xfrm\0"
+
+typedef enum NetDevKind {
+ NETDEV_KIND_BAREUDP,
+ NETDEV_KIND_BATADV,
+ NETDEV_KIND_BOND,
+ NETDEV_KIND_BRIDGE,
+ NETDEV_KIND_DUMMY,
+ NETDEV_KIND_ERSPAN,
+ NETDEV_KIND_FOU,
+ NETDEV_KIND_GENEVE,
+ NETDEV_KIND_GRE,
+ NETDEV_KIND_GRETAP,
+ NETDEV_KIND_IFB,
+ NETDEV_KIND_IP6GRE,
+ NETDEV_KIND_IP6GRETAP,
+ NETDEV_KIND_IP6TNL,
+ NETDEV_KIND_IPIP,
+ NETDEV_KIND_IPOIB,
+ NETDEV_KIND_IPVLAN,
+ NETDEV_KIND_IPVTAP,
+ NETDEV_KIND_L2TP,
+ NETDEV_KIND_MACSEC,
+ NETDEV_KIND_MACVLAN,
+ NETDEV_KIND_MACVTAP,
+ NETDEV_KIND_NETDEVSIM,
+ NETDEV_KIND_NLMON,
+ NETDEV_KIND_SIT,
+ NETDEV_KIND_TAP,
+ NETDEV_KIND_TUN,
+ NETDEV_KIND_VCAN,
+ NETDEV_KIND_VETH,
+ NETDEV_KIND_VLAN,
+ NETDEV_KIND_VRF,
+ NETDEV_KIND_VTI,
+ NETDEV_KIND_VTI6,
+ NETDEV_KIND_VXCAN,
+ NETDEV_KIND_VXLAN,
+ NETDEV_KIND_WIREGUARD,
+ NETDEV_KIND_WLAN,
+ NETDEV_KIND_XFRM,
+ _NETDEV_KIND_MAX,
+ _NETDEV_KIND_TUNNEL, /* Used by config_parse_stacked_netdev() */
+ _NETDEV_KIND_INVALID = -EINVAL,
+} NetDevKind;
+
+typedef enum NetDevState {
+ NETDEV_STATE_LOADING,
+ NETDEV_STATE_FAILED,
+ NETDEV_STATE_CREATING,
+ NETDEV_STATE_READY,
+ NETDEV_STATE_LINGER,
+ _NETDEV_STATE_MAX,
+ _NETDEV_STATE_INVALID = -EINVAL,
+} NetDevState;
+
+typedef enum NetDevCreateType {
+ NETDEV_CREATE_INDEPENDENT,
+ NETDEV_CREATE_STACKED,
+ _NETDEV_CREATE_MAX,
+ _NETDEV_CREATE_INVALID = -EINVAL,
+} NetDevCreateType;
+
+typedef struct Manager Manager;
+typedef struct Condition Condition;
+
+typedef struct NetDev {
+ Manager *manager;
+
+ unsigned n_ref;
+
+ char *filename;
+
+ LIST_HEAD(Condition, conditions);
+
+ NetDevState state;
+ NetDevKind kind;
+ char *description;
+ char *ifname;
+ struct hw_addr_data hw_addr;
+ uint32_t mtu;
+ int ifindex;
+} NetDev;
+
+typedef struct NetDevVTable {
+ /* How much memory does an object of this unit type need */
+ size_t object_size;
+
+ /* Config file sections this netdev kind understands, separated
+ * by NUL chars */
+ const char *sections;
+
+ /* This should reset all type-specific variables. This should
+ * not allocate memory, and is called with zero-initialized
+ * data. It should hence only initialize variables that need
+ * to be set != 0. */
+ void (*init)(NetDev *n);
+
+ /* This is called when the interface is removed. */
+ void (*drop)(NetDev *n);
+
+ /* This should free all kind-specific variables. It should be
+ * idempotent. */
+ void (*done)(NetDev *n);
+
+ /* fill in message to create netdev */
+ int (*fill_message_create)(NetDev *netdev, Link *link, sd_netlink_message *message);
+
+ /* specifies if netdev is independent, or a master device or a stacked device */
+ NetDevCreateType create_type;
+
+ /* This is used for stacked netdev. Return true when the underlying link is ready. */
+ int (*is_ready_to_create)(NetDev *netdev, Link *link);
+
+ /* create netdev, if not done via rtnl */
+ int (*create)(NetDev *netdev);
+
+ /* perform additional configuration after netdev has been createad */
+ int (*post_create)(NetDev *netdev, Link *link);
+
+ /* verify that compulsory configuration options were specified */
+ int (*config_verify)(NetDev *netdev, const char *filename);
+
+ /* expected iftype, e.g. ARPHRD_ETHER. */
+ uint16_t iftype;
+
+ /* Generate MAC address when MACAddress= is not specified. */
+ bool generate_mac;
+
+ /* When assigning ifindex to the netdev, skip to check if the netdev kind matches. */
+ bool skip_netdev_kind_check;
+} NetDevVTable;
+
+extern const NetDevVTable * const netdev_vtable[_NETDEV_KIND_MAX];
+
+#define NETDEV_VTABLE(n) ((n)->kind != _NETDEV_KIND_INVALID ? netdev_vtable[(n)->kind] : NULL)
+
+/* For casting a netdev into the various netdev kinds */
+#define DEFINE_NETDEV_CAST(UPPERCASE, MixedCase) \
+ static inline MixedCase* UPPERCASE(NetDev *n) { \
+ if (_unlikely_(!n || \
+ n->kind != NETDEV_KIND_##UPPERCASE) || \
+ n->state == _NETDEV_STATE_INVALID) \
+ return NULL; \
+ \
+ return (MixedCase*) n; \
+ }
+
+/* For casting the various netdev kinds into a netdev */
+#define NETDEV(n) (&(n)->meta)
+
+int netdev_load(Manager *manager, bool reload);
+int netdev_load_one(Manager *manager, const char *filename);
+void netdev_drop(NetDev *netdev);
+void netdev_enter_failed(NetDev *netdev);
+
+NetDev *netdev_unref(NetDev *netdev);
+NetDev *netdev_ref(NetDev *netdev);
+DEFINE_TRIVIAL_DESTRUCTOR(netdev_destroy_callback, NetDev, netdev_unref);
+DEFINE_TRIVIAL_CLEANUP_FUNC(NetDev*, netdev_unref);
+
+bool netdev_is_managed(NetDev *netdev);
+int netdev_get(Manager *manager, const char *name, NetDev **ret);
+int netdev_set_ifindex(NetDev *netdev, sd_netlink_message *newlink);
+int netdev_generate_hw_addr(NetDev *netdev, Link *link, const char *name,
+ const struct hw_addr_data *hw_addr, struct hw_addr_data *ret);
+
+int link_request_stacked_netdev(Link *link, NetDev *netdev);
+
+const char *netdev_kind_to_string(NetDevKind d) _const_;
+NetDevKind netdev_kind_from_string(const char *d) _pure_;
+
+static inline NetDevCreateType netdev_get_create_type(NetDev *netdev) {
+ assert(netdev);
+ assert(NETDEV_VTABLE(netdev));
+
+ return NETDEV_VTABLE(netdev)->create_type;
+}
+
+CONFIG_PARSER_PROTOTYPE(config_parse_netdev_kind);
+CONFIG_PARSER_PROTOTYPE(config_parse_netdev_hw_addr);
+
+/* gperf */
+const struct ConfigPerfItem* network_netdev_gperf_lookup(const char *key, GPERF_LEN_TYPE length);
+
+/* Macros which append INTERFACE= to the message */
+
+#define log_netdev_full_errno_zerook(netdev, level, error, ...) \
+ ({ \
+ const NetDev *_n = (netdev); \
+ log_interface_full_errno_zerook(_n ? _n->ifname : NULL, level, error, __VA_ARGS__); \
+ })
+
+#define log_netdev_full_errno(netdev, level, error, ...) \
+ ({ \
+ int _error = (error); \
+ ASSERT_NON_ZERO(_error); \
+ log_netdev_full_errno_zerook(netdev, level, _error, __VA_ARGS__); \
+ })
+
+#define log_netdev_full(netdev, level, ...) (void) log_netdev_full_errno_zerook(netdev, level, 0, __VA_ARGS__)
+
+#define log_netdev_debug(netdev, ...) log_netdev_full(netdev, LOG_DEBUG, __VA_ARGS__)
+#define log_netdev_info(netdev, ...) log_netdev_full(netdev, LOG_INFO, __VA_ARGS__)
+#define log_netdev_notice(netdev, ...) log_netdev_full(netdev, LOG_NOTICE, __VA_ARGS__)
+#define log_netdev_warning(netdev, ...) log_netdev_full(netdev, LOG_WARNING, __VA_ARGS__)
+#define log_netdev_error(netdev, ...) log_netdev_full(netdev, LOG_ERR, __VA_ARGS__)
+
+#define log_netdev_debug_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_DEBUG, error, __VA_ARGS__)
+#define log_netdev_info_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_INFO, error, __VA_ARGS__)
+#define log_netdev_notice_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_NOTICE, error, __VA_ARGS__)
+#define log_netdev_warning_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_WARNING, error, __VA_ARGS__)
+#define log_netdev_error_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_ERR, error, __VA_ARGS__)
+
+#define LOG_NETDEV_MESSAGE(netdev, fmt, ...) "MESSAGE=%s: " fmt, (netdev)->ifname, ##__VA_ARGS__
+#define LOG_NETDEV_INTERFACE(netdev) "INTERFACE=%s", (netdev)->ifname
diff --git a/src/network/netdev/netdevsim.c b/src/network/netdev/netdevsim.c
new file mode 100644
index 0000000..15d5c13
--- /dev/null
+++ b/src/network/netdev/netdevsim.c
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/if_arp.h>
+
+#include "netdevsim.h"
+
+const NetDevVTable netdevsim_vtable = {
+ .object_size = sizeof(NetDevSim),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/netdevsim.h b/src/network/netdev/netdevsim.h
new file mode 100644
index 0000000..27adc59
--- /dev/null
+++ b/src/network/netdev/netdevsim.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct NetDevSim NetDevSim;
+
+#include "netdev.h"
+
+struct NetDevSim {
+ NetDev meta;
+};
+
+DEFINE_NETDEV_CAST(NETDEVSIM, NetDevSim);
+extern const NetDevVTable netdevsim_vtable;
diff --git a/src/network/netdev/nlmon.c b/src/network/netdev/nlmon.c
new file mode 100644
index 0000000..ff37209
--- /dev/null
+++ b/src/network/netdev/nlmon.c
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/if_arp.h>
+
+#include "nlmon.h"
+
+static int netdev_nlmon_verify(NetDev *netdev, const char *filename) {
+ assert(netdev);
+ assert(filename);
+
+ if (netdev->hw_addr.length > 0) {
+ log_netdev_warning(netdev, "%s: MACAddress= is not supported. Ignoring", filename);
+ netdev->hw_addr = HW_ADDR_NULL;
+ }
+
+ return 0;
+}
+
+const NetDevVTable nlmon_vtable = {
+ .object_size = sizeof(NLMon),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = netdev_nlmon_verify,
+ .iftype = ARPHRD_NETLINK,
+};
diff --git a/src/network/netdev/nlmon.h b/src/network/netdev/nlmon.h
new file mode 100644
index 0000000..edfc504
--- /dev/null
+++ b/src/network/netdev/nlmon.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct NLMon NLMon;
+
+#include "netdev.h"
+
+struct NLMon {
+ NetDev meta;
+};
+
+DEFINE_NETDEV_CAST(NLMON, NLMon);
+
+extern const NetDevVTable nlmon_vtable;
diff --git a/src/network/netdev/tunnel.c b/src/network/netdev/tunnel.c
new file mode 100644
index 0000000..db414a8
--- /dev/null
+++ b/src/network/netdev/tunnel.c
@@ -0,0 +1,1318 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/fou.h>
+#include <linux/if_arp.h>
+#include <linux/if_tunnel.h>
+#include <linux/ip.h>
+#include <linux/ip6_tunnel.h>
+
+#include "af-list.h"
+#include "conf-parser.h"
+#include "hexdecoct.h"
+#include "missing_network.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "siphash24.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "tunnel.h"
+#include "util.h"
+
+#define DEFAULT_IPV6_TTL 64
+#define IP6_FLOWINFO_FLOWLABEL htobe32(0x000FFFFF)
+#define IP6_TNL_F_ALLOW_LOCAL_REMOTE 0x40
+
+static const char* const ip6tnl_mode_table[_NETDEV_IP6_TNL_MODE_MAX] = {
+ [NETDEV_IP6_TNL_MODE_IP6IP6] = "ip6ip6",
+ [NETDEV_IP6_TNL_MODE_IPIP6] = "ipip6",
+ [NETDEV_IP6_TNL_MODE_ANYIP6] = "any",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(ip6tnl_mode, Ip6TnlMode);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_ip6tnl_mode, ip6tnl_mode, Ip6TnlMode, "Failed to parse ip6 tunnel Mode");
+
+#define HASH_KEY SD_ID128_MAKE(74,c4,de,12,f3,d9,41,34,bb,3d,c1,a4,42,93,50,87)
+
+int dhcp4_pd_create_6rd_tunnel_name(Link *link, char **ret) {
+ _cleanup_free_ char *ifname_alloc = NULL;
+ uint8_t ipv4masklen, sixrd_prefixlen, *buf, *p;
+ struct in_addr ipv4address;
+ struct in6_addr sixrd_prefix;
+ char ifname[IFNAMSIZ];
+ uint64_t result;
+ size_t sz;
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &ipv4address);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to get DHCPv4 address: %m");
+
+ r = sd_dhcp_lease_get_6rd(link->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, NULL, NULL);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to get 6rd option: %m");
+
+ sz = sizeof(uint8_t) * 2 + sizeof(struct in6_addr) + sizeof(struct in_addr);
+ buf = newa(uint8_t, sz);
+ p = buf;
+ p = mempcpy(p, &ipv4masklen, sizeof(uint8_t));
+ p = mempcpy(p, &ipv4address, sizeof(struct in_addr));
+ p = mempcpy(p, &sixrd_prefixlen, sizeof(uint8_t));
+ p = mempcpy(p, &sixrd_prefix, sizeof(struct in6_addr));
+
+ result = siphash24(buf, sz, HASH_KEY.bytes);
+ memcpy(ifname, "6rd-", STRLEN("6rd-"));
+ ifname[STRLEN("6rd-") ] = urlsafe_base64char(result >> 54);
+ ifname[STRLEN("6rd-") + 1] = urlsafe_base64char(result >> 48);
+ ifname[STRLEN("6rd-") + 2] = urlsafe_base64char(result >> 42);
+ ifname[STRLEN("6rd-") + 3] = urlsafe_base64char(result >> 36);
+ ifname[STRLEN("6rd-") + 4] = urlsafe_base64char(result >> 30);
+ ifname[STRLEN("6rd-") + 5] = urlsafe_base64char(result >> 24);
+ ifname[STRLEN("6rd-") + 6] = urlsafe_base64char(result >> 18);
+ ifname[STRLEN("6rd-") + 7] = urlsafe_base64char(result >> 12);
+ ifname[STRLEN("6rd-") + 8] = urlsafe_base64char(result >> 6);
+ ifname[STRLEN("6rd-") + 9] = urlsafe_base64char(result);
+ ifname[STRLEN("6rd-") + 10] = '\0';
+ assert_cc(STRLEN("6rd-") + 10 <= IFNAMSIZ);
+
+ ifname_alloc = strdup(ifname);
+ if (!ifname_alloc)
+ return log_oom_debug();
+
+ *ret = TAKE_PTR(ifname_alloc);
+ return 0;
+}
+
+static int dhcp4_pd_create_6rd_tunnel_message(
+ Link *link,
+ sd_netlink_message *m,
+ const struct in_addr *ipv4address,
+ uint8_t ipv4masklen,
+ const struct in6_addr *sixrd_prefix,
+ uint8_t sixrd_prefixlen) {
+ int r;
+
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, link->dhcp4_6rd_tunnel_name);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, "sit");
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_IPTUN_LOCAL, ipv4address);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_TTL, 64);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_6RD_PREFIX, sixrd_prefix);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_6RD_PREFIXLEN, sixrd_prefixlen);
+ if (r < 0)
+ return r;
+
+ struct in_addr relay_prefix = *ipv4address;
+ (void) in4_addr_mask(&relay_prefix, ipv4masklen);
+ r = sd_netlink_message_append_u32(m, IFLA_IPTUN_6RD_RELAY_PREFIX, relay_prefix.s_addr);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_6RD_RELAY_PREFIXLEN, ipv4masklen);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int dhcp4_pd_create_6rd_tunnel(Link *link, link_netlink_message_handler_t callback) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ uint8_t ipv4masklen, sixrd_prefixlen;
+ struct in_addr ipv4address;
+ struct in6_addr sixrd_prefix;
+ int r;
+
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->dhcp_lease);
+ assert(link->dhcp4_6rd_tunnel_name);
+ assert(callback);
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &ipv4address);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to get DHCPv4 address: %m");
+
+ r = sd_dhcp_lease_get_6rd(link->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, NULL, NULL);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to get 6rd option: %m");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_NEWLINK, 0);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to create netlink message: %m");
+
+ r = dhcp4_pd_create_6rd_tunnel_message(link, m,
+ &ipv4address, ipv4masklen,
+ &sixrd_prefix, sixrd_prefixlen);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to fill netlink message: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, m, callback,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not send netlink message: %m");
+
+ link_ref(link);
+
+ return 0;
+}
+
+static int tunnel_get_local_address(Tunnel *t, Link *link, union in_addr_union *ret) {
+ assert(t);
+
+ if (t->local_type < 0) {
+ if (ret)
+ *ret = t->local;
+ return 0;
+ }
+
+ return link_get_local_address(link, t->local_type, t->family, NULL, ret);
+}
+
+static int netdev_ipip_sit_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ union in_addr_union local;
+ Tunnel *t;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ if (netdev->kind == NETDEV_KIND_IPIP)
+ t = IPIP(netdev);
+ else
+ t = SIT(netdev);
+
+ assert(t);
+
+ if (t->external) {
+ r = sd_netlink_message_append_flag(m, IFLA_IPTUN_COLLECT_METADATA);
+ if (r < 0)
+ return r;
+
+ /* If external mode is enabled, then the following settings should not be appended. */
+ return 0;
+ }
+
+ if (link || t->assign_to_loopback) {
+ r = sd_netlink_message_append_u32(m, IFLA_IPTUN_LINK, link ? link->ifindex : LOOPBACK_IFINDEX);
+ if (r < 0)
+ return r;
+ }
+
+ r = tunnel_get_local_address(t, link, &local);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not find local address: %m");
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_IPTUN_LOCAL, &local.in);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_IPTUN_REMOTE, &t->remote.in);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_TTL, t->ttl);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_PMTUDISC, t->pmtudisc);
+ if (r < 0)
+ return r;
+
+ if (t->fou_tunnel) {
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_TYPE, t->fou_encap_type);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_SPORT, htobe16(t->encap_src_port));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_DPORT, htobe16(t->fou_destination_port));
+ if (r < 0)
+ return r;
+ }
+
+ if (netdev->kind == NETDEV_KIND_SIT) {
+ if (t->sixrd_prefixlen > 0) {
+ r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_6RD_PREFIX, &t->sixrd_prefix);
+ if (r < 0)
+ return r;
+
+ /* u16 is deliberate here, even though we're passing a netmask that can never be >128. The kernel is
+ * expecting to receive the prefixlen as a u16.
+ */
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_6RD_PREFIXLEN, t->sixrd_prefixlen);
+ if (r < 0)
+ return r;
+ }
+
+ if (t->isatap >= 0) {
+ uint16_t flags = 0;
+
+ SET_FLAG(flags, SIT_ISATAP, t->isatap);
+
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_FLAGS, flags);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+static int netdev_gre_erspan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ union in_addr_union local;
+ uint32_t ikey = 0;
+ uint32_t okey = 0;
+ uint16_t iflags = 0;
+ uint16_t oflags = 0;
+ Tunnel *t;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ switch (netdev->kind) {
+ case NETDEV_KIND_GRE:
+ t = GRE(netdev);
+ break;
+ case NETDEV_KIND_ERSPAN:
+ t = ERSPAN(netdev);
+ break;
+ case NETDEV_KIND_GRETAP:
+ t = GRETAP(netdev);
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ assert(t);
+
+ if (t->external) {
+ r = sd_netlink_message_append_flag(m, IFLA_GRE_COLLECT_METADATA);
+ if (r < 0)
+ return r;
+
+ /* If external mode is enabled, then the following settings should not be appended. */
+ return 0;
+ }
+
+ if (link || t->assign_to_loopback) {
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_LINK, link ? link->ifindex : LOOPBACK_IFINDEX);
+ if (r < 0)
+ return r;
+ }
+
+ if (netdev->kind == NETDEV_KIND_ERSPAN) {
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_ERSPAN_VER, t->erspan_version);
+ if (r < 0)
+ return r;
+
+ if (t->erspan_version == 1) {
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_ERSPAN_INDEX, t->erspan_index);
+ if (r < 0)
+ return r;
+
+ } else if (t->erspan_version == 2) {
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_ERSPAN_DIR, t->erspan_direction);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_ERSPAN_HWID, t->erspan_hwid);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ r = tunnel_get_local_address(t, link, &local);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not find local address: %m");
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_GRE_LOCAL, &local.in);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_GRE_REMOTE, &t->remote.in);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_TTL, t->ttl);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_TOS, t->tos);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_PMTUDISC, t->pmtudisc);
+ if (r < 0)
+ return r;
+
+ if (t->key != 0) {
+ ikey = okey = htobe32(t->key);
+ iflags |= GRE_KEY;
+ oflags |= GRE_KEY;
+ }
+
+ if (t->ikey != 0) {
+ ikey = htobe32(t->ikey);
+ iflags |= GRE_KEY;
+ }
+
+ if (t->okey != 0) {
+ okey = htobe32(t->okey);
+ oflags |= GRE_KEY;
+ }
+
+ if (t->gre_erspan_sequence > 0) {
+ iflags |= GRE_SEQ;
+ oflags |= GRE_SEQ;
+ } else if (t->gre_erspan_sequence == 0) {
+ iflags &= ~GRE_SEQ;
+ oflags &= ~GRE_SEQ;
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_IKEY, ikey);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_OKEY, okey);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_IFLAGS, iflags);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_OFLAGS, oflags);
+ if (r < 0)
+ return r;
+
+ if (t->fou_tunnel) {
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_TYPE, t->fou_encap_type);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_SPORT, htobe16(t->encap_src_port));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_DPORT, htobe16(t->fou_destination_port));
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int netdev_ip6gre_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ union in_addr_union local;
+ uint32_t ikey = 0;
+ uint32_t okey = 0;
+ uint16_t iflags = 0;
+ uint16_t oflags = 0;
+ Tunnel *t;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ if (netdev->kind == NETDEV_KIND_IP6GRE)
+ t = IP6GRE(netdev);
+ else
+ t = IP6GRETAP(netdev);
+
+ assert(t);
+
+ if (t->external) {
+ r = sd_netlink_message_append_flag(m, IFLA_GRE_COLLECT_METADATA);
+ if (r < 0)
+ return r;
+
+ /* If external mode is enabled, then the following settings should not be appended. */
+ return 0;
+ }
+
+ if (link || t->assign_to_loopback) {
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_LINK, link ? link->ifindex : LOOPBACK_IFINDEX);
+ if (r < 0)
+ return r;
+ }
+
+ r = tunnel_get_local_address(t, link, &local);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not find local address: %m");
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_GRE_LOCAL, &local.in6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_GRE_REMOTE, &t->remote.in6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_TTL, t->ttl);
+ if (r < 0)
+ return r;
+
+ if (t->ipv6_flowlabel != _NETDEV_IPV6_FLOWLABEL_INVALID) {
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_FLOWINFO, t->ipv6_flowlabel);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_FLAGS, t->flags);
+ if (r < 0)
+ return r;
+
+ if (t->key != 0) {
+ ikey = okey = htobe32(t->key);
+ iflags |= GRE_KEY;
+ oflags |= GRE_KEY;
+ }
+
+ if (t->ikey != 0) {
+ ikey = htobe32(t->ikey);
+ iflags |= GRE_KEY;
+ }
+
+ if (t->okey != 0) {
+ okey = htobe32(t->okey);
+ oflags |= GRE_KEY;
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_IKEY, ikey);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_OKEY, okey);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_IFLAGS, iflags);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_OFLAGS, oflags);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int netdev_vti_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ union in_addr_union local;
+ uint32_t ikey, okey;
+ Tunnel *t;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ if (netdev->kind == NETDEV_KIND_VTI)
+ t = VTI(netdev);
+ else
+ t = VTI6(netdev);
+
+ assert(t);
+
+ if (link || t->assign_to_loopback) {
+ r = sd_netlink_message_append_u32(m, IFLA_VTI_LINK, link ? link->ifindex : LOOPBACK_IFINDEX);
+ if (r < 0)
+ return r;
+ }
+
+ if (t->key != 0)
+ ikey = okey = htobe32(t->key);
+ else {
+ ikey = htobe32(t->ikey);
+ okey = htobe32(t->okey);
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_VTI_IKEY, ikey);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, IFLA_VTI_OKEY, okey);
+ if (r < 0)
+ return r;
+
+ r = tunnel_get_local_address(t, link, &local);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not find local address: %m");
+
+ r = netlink_message_append_in_addr_union(m, IFLA_VTI_LOCAL, t->family, &local);
+ if (r < 0)
+ return r;
+
+ r = netlink_message_append_in_addr_union(m, IFLA_VTI_REMOTE, t->family, &t->remote);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int netdev_ip6tnl_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ union in_addr_union local;
+ uint8_t proto;
+ Tunnel *t;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ t = IP6TNL(netdev);
+
+ assert(t);
+
+ switch (t->ip6tnl_mode) {
+ case NETDEV_IP6_TNL_MODE_IP6IP6:
+ proto = IPPROTO_IPV6;
+ break;
+ case NETDEV_IP6_TNL_MODE_IPIP6:
+ proto = IPPROTO_IPIP;
+ break;
+ case NETDEV_IP6_TNL_MODE_ANYIP6:
+ default:
+ proto = 0;
+ break;
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_PROTO, proto);
+ if (r < 0)
+ return r;
+
+ if (t->external) {
+ r = sd_netlink_message_append_flag(m, IFLA_IPTUN_COLLECT_METADATA);
+ if (r < 0)
+ return r;
+
+ /* If external mode is enabled, then the following settings should not be appended. */
+ return 0;
+ }
+
+ if (link || t->assign_to_loopback) {
+ r = sd_netlink_message_append_u32(m, IFLA_IPTUN_LINK, link ? link->ifindex : LOOPBACK_IFINDEX);
+ if (r < 0)
+ return r;
+ }
+
+ r = tunnel_get_local_address(t, link, &local);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not find local address: %m");
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_LOCAL, &local.in6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_REMOTE, &t->remote.in6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_TTL, t->ttl);
+ if (r < 0)
+ return r;
+
+ if (t->ipv6_flowlabel != _NETDEV_IPV6_FLOWLABEL_INVALID) {
+ r = sd_netlink_message_append_u32(m, IFLA_IPTUN_FLOWINFO, t->ipv6_flowlabel);
+ if (r < 0)
+ return r;
+ }
+
+ if (t->copy_dscp)
+ t->flags |= IP6_TNL_F_RCV_DSCP_COPY;
+
+ if (t->allow_localremote >= 0)
+ SET_FLAG(t->flags, IP6_TNL_F_ALLOW_LOCAL_REMOTE, t->allow_localremote);
+
+ r = sd_netlink_message_append_u32(m, IFLA_IPTUN_FLAGS, t->flags);
+ if (r < 0)
+ return r;
+
+ if (t->encap_limit != 0) {
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_ENCAP_LIMIT, t->encap_limit);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int netdev_tunnel_is_ready_to_create(NetDev *netdev, Link *link) {
+ Tunnel *t;
+
+ assert(netdev);
+
+ t = TUNNEL(netdev);
+
+ assert(t);
+
+ if (t->independent)
+ return true;
+
+ return tunnel_get_local_address(t, link, NULL) >= 0;
+}
+
+static int netdev_tunnel_verify(NetDev *netdev, const char *filename) {
+ Tunnel *t;
+
+ assert(netdev);
+ assert(filename);
+
+ t = TUNNEL(netdev);
+
+ assert(t);
+
+ if (netdev->kind == NETDEV_KIND_IP6TNL &&
+ t->ip6tnl_mode == _NETDEV_IP6_TNL_MODE_INVALID)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "ip6tnl without mode configured in %s. Ignoring", filename);
+
+ if (t->external) {
+ if (IN_SET(netdev->kind, NETDEV_KIND_VTI, NETDEV_KIND_VTI6))
+ log_netdev_debug(netdev, "vti/vti6 tunnel do not support external mode, ignoring.");
+ else {
+ /* tunnel with external mode does not require underlying interface. */
+ t->independent = true;
+
+ /* tunnel with external mode does not require any settings checked below. */
+ return 0;
+ }
+ }
+
+ if (IN_SET(netdev->kind, NETDEV_KIND_VTI, NETDEV_KIND_IPIP, NETDEV_KIND_SIT, NETDEV_KIND_GRE) &&
+ !IN_SET(t->family, AF_UNSPEC, AF_INET))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "vti/ipip/sit/gre tunnel without a local/remote IPv4 address configured in %s. Ignoring", filename);
+
+ if (IN_SET(netdev->kind, NETDEV_KIND_GRETAP, NETDEV_KIND_ERSPAN) &&
+ (t->family != AF_INET || !in_addr_is_set(t->family, &t->remote)))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "gretap/erspan tunnel without a remote IPv4 address configured in %s. Ignoring", filename);
+
+ if ((IN_SET(netdev->kind, NETDEV_KIND_VTI6, NETDEV_KIND_IP6TNL) && t->family != AF_INET6) ||
+ (netdev->kind == NETDEV_KIND_IP6GRE && !IN_SET(t->family, AF_UNSPEC, AF_INET6)))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "vti6/ip6tnl/ip6gre tunnel without a local/remote IPv6 address configured in %s. Ignoring", filename);
+
+ if (netdev->kind == NETDEV_KIND_IP6GRETAP &&
+ (t->family != AF_INET6 || !in_addr_is_set(t->family, &t->remote)))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "ip6gretap tunnel without a remote IPv6 address configured in %s. Ignoring", filename);
+
+ if (t->fou_tunnel && t->fou_destination_port <= 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "FooOverUDP missing port configured in %s. Ignoring", filename);
+
+ /* netlink_message_append_in_addr_union() is used for vti/vti6. So, t->family cannot be AF_UNSPEC. */
+ if (netdev->kind == NETDEV_KIND_VTI)
+ t->family = AF_INET;
+
+ if (t->assign_to_loopback)
+ t->independent = true;
+
+ if (t->independent && t->local_type >= 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "The local address cannot be '%s' when Independent= or AssignToLoopback= is enabled, ignoring.",
+ strna(netdev_local_address_type_to_string(t->local_type)));
+
+ return 0;
+}
+
+static int unset_local(Tunnel *t) {
+ assert(t);
+
+ /* Unset the previous assignment. */
+ t->local = IN_ADDR_NULL;
+ t->local_type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID;
+
+ /* If the remote address is not specified, also clear the address family. */
+ if (!in_addr_is_set(t->family, &t->remote))
+ t->family = AF_UNSPEC;
+
+ return 0;
+}
+
+int config_parse_tunnel_local_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ union in_addr_union buffer = IN_ADDR_NULL;
+ NetDevLocalAddressType type;
+ Tunnel *t = ASSERT_PTR(userdata);
+ int r, f;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue) || streq(rvalue, "any"))
+ return unset_local(t);
+
+ type = netdev_local_address_type_from_string(rvalue);
+ if (IN_SET(type, NETDEV_LOCAL_ADDRESS_IPV4LL, NETDEV_LOCAL_ADDRESS_DHCP4))
+ f = AF_INET;
+ else if (IN_SET(type, NETDEV_LOCAL_ADDRESS_IPV6LL, NETDEV_LOCAL_ADDRESS_DHCP6, NETDEV_LOCAL_ADDRESS_SLAAC))
+ f = AF_INET6;
+ else {
+ type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID;
+ r = in_addr_from_string_auto(rvalue, &f, &buffer);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Tunnel address \"%s\" invalid, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (in_addr_is_null(f, &buffer))
+ return unset_local(t);
+ }
+
+ if (t->family != AF_UNSPEC && t->family != f) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Address family does not match the previous assignment, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ t->family = f;
+ t->local = buffer;
+ t->local_type = type;
+ return 0;
+}
+
+static int unset_remote(Tunnel *t) {
+ assert(t);
+
+ /* Unset the previous assignment. */
+ t->remote = IN_ADDR_NULL;
+
+ /* If the local address is not specified, also clear the address family. */
+ if (t->local_type == _NETDEV_LOCAL_ADDRESS_TYPE_INVALID &&
+ !in_addr_is_set(t->family, &t->local))
+ t->family = AF_UNSPEC;
+
+ return 0;
+}
+
+int config_parse_tunnel_remote_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ union in_addr_union buffer;
+ Tunnel *t = ASSERT_PTR(userdata);
+ int r, f;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue) || streq(rvalue, "any"))
+ return unset_remote(t);
+
+ r = in_addr_from_string_auto(rvalue, &f, &buffer);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Tunnel address \"%s\" invalid, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (in_addr_is_null(f, &buffer))
+ return unset_remote(t);
+
+ if (t->family != AF_UNSPEC && t->family != f) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Address family does not match the previous assignment, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ t->family = f;
+ t->remote = buffer;
+ return 0;
+}
+
+int config_parse_tunnel_key(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint32_t *dest = ASSERT_PTR(data), k;
+ union in_addr_union buffer;
+ int r;
+
+ assert(filename);
+ assert(rvalue);
+
+ r = in_addr_from_string(AF_INET, rvalue, &buffer);
+ if (r < 0) {
+ r = safe_atou32(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse tunnel key ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ } else
+ k = be32toh(buffer.in.s_addr);
+
+ *dest = k;
+ return 0;
+}
+
+int config_parse_ipv6_flowlabel(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Tunnel *t = ASSERT_PTR(userdata);
+ int k, r;
+
+ assert(filename);
+ assert(rvalue);
+
+ if (streq(rvalue, "inherit")) {
+ t->ipv6_flowlabel = IP6_FLOWINFO_FLOWLABEL;
+ t->flags |= IP6_TNL_F_USE_ORIG_FLOWLABEL;
+ return 0;
+ }
+
+ r = safe_atoi(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse tunnel IPv6 flowlabel, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (k > 0xFFFFF) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid tunnel IPv6 flowlabel, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ t->ipv6_flowlabel = htobe32(k) & IP6_FLOWINFO_FLOWLABEL;
+ t->flags &= ~IP6_TNL_F_USE_ORIG_FLOWLABEL;
+ return 0;
+}
+
+int config_parse_encap_limit(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Tunnel *t = ASSERT_PTR(userdata);
+ int k, r;
+
+ assert(filename);
+ assert(rvalue);
+
+ if (streq(rvalue, "none")) {
+ t->flags |= IP6_TNL_F_IGN_ENCAP_LIMIT;
+ t->encap_limit = 0;
+ return 0;
+ }
+
+ r = safe_atoi(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse Tunnel Encapsulation Limit option, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (k > 255 || k < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid Tunnel Encapsulation value, ignoring assignment: %d", k);
+ return 0;
+ }
+
+ t->encap_limit = k;
+ t->flags &= ~IP6_TNL_F_IGN_ENCAP_LIMIT;
+ return 0;
+}
+
+int config_parse_6rd_prefix(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Tunnel *t = userdata;
+ union in_addr_union p;
+ uint8_t l;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = in_addr_prefix_from_string(rvalue, AF_INET6, &p, &l);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse 6rd prefix \"%s\", ignoring: %m", rvalue);
+ return 0;
+ }
+ if (l == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "6rd prefix length of \"%s\" must be greater than zero, ignoring", rvalue);
+ return 0;
+ }
+
+ t->sixrd_prefix = p.in6;
+ t->sixrd_prefixlen = l;
+
+ return 0;
+}
+
+int config_parse_erspan_version(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint8_t n, *v = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *v = 1; /* defaults to 1 */
+ return 0;
+ }
+
+ r = safe_atou8(rvalue, &n);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse erspan version \"%s\", ignoring: %m", rvalue);
+ return 0;
+ }
+ if (!IN_SET(n, 0, 1, 2)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid erspan version \"%s\", which must be 0, 1 or 2, ignoring.", rvalue);
+ return 0;
+ }
+
+ *v = n;
+ return 0;
+}
+
+int config_parse_erspan_index(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint32_t n, *v = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *v = 0; /* defaults to 0 */
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &n);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse erspan index \"%s\", ignoring: %m", rvalue);
+ return 0;
+ }
+ if (n >= 0x100000) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid erspan index \"%s\", which must be less than 0x100000, ignoring.", rvalue);
+ return 0;
+ }
+
+ *v = n;
+ return 0;
+}
+
+int config_parse_erspan_direction(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint8_t *v = ASSERT_PTR(data);
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue) || streq(rvalue, "ingress"))
+ *v = 0; /* defaults to ingress */
+ else if (streq(rvalue, "egress"))
+ *v = 1;
+ else
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid erspan direction \"%s\", which must be \"ingress\" or \"egress\", ignoring.", rvalue);
+
+ return 0;
+}
+
+int config_parse_erspan_hwid(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint16_t n, *v = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *v = 0; /* defaults to 0 */
+ return 0;
+ }
+
+ r = safe_atou16(rvalue, &n);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse erspan hwid \"%s\", ignoring: %m", rvalue);
+ return 0;
+ }
+ if (n >= 64) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid erspan index \"%s\", which must be less than 64, ignoring.", rvalue);
+ return 0;
+ }
+
+ *v = n;
+ return 0;
+}
+
+static void netdev_tunnel_init(NetDev *netdev) {
+ Tunnel *t;
+
+ assert(netdev);
+
+ t = TUNNEL(netdev);
+
+ assert(t);
+
+ t->local_type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID;
+ t->pmtudisc = true;
+ t->fou_encap_type = NETDEV_FOO_OVER_UDP_ENCAP_DIRECT;
+ t->isatap = -1;
+ t->gre_erspan_sequence = -1;
+ t->encap_limit = IPV6_DEFAULT_TNL_ENCAP_LIMIT;
+ t->ip6tnl_mode = _NETDEV_IP6_TNL_MODE_INVALID;
+ t->ipv6_flowlabel = _NETDEV_IPV6_FLOWLABEL_INVALID;
+ t->allow_localremote = -1;
+ t->erspan_version = 1;
+
+ if (IN_SET(netdev->kind, NETDEV_KIND_IP6GRE, NETDEV_KIND_IP6GRETAP, NETDEV_KIND_IP6TNL))
+ t->ttl = DEFAULT_IPV6_TTL;
+}
+
+const NetDevVTable ipip_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ipip_sit_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_TUNNEL,
+};
+
+const NetDevVTable sit_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ipip_sit_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_SIT,
+};
+
+const NetDevVTable vti_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_vti_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_TUNNEL,
+};
+
+const NetDevVTable vti6_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_vti_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_TUNNEL6,
+};
+
+const NetDevVTable gre_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_gre_erspan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_IPGRE,
+};
+
+const NetDevVTable gretap_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_gre_erspan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
+
+const NetDevVTable ip6gre_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ip6gre_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_IP6GRE,
+};
+
+const NetDevVTable ip6gretap_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ip6gre_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
+
+const NetDevVTable ip6tnl_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ip6tnl_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_TUNNEL6,
+};
+
+const NetDevVTable erspan_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = netdev_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_gre_erspan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_tunnel_is_ready_to_create,
+ .config_verify = netdev_tunnel_verify,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/tunnel.h b/src/network/netdev/tunnel.h
new file mode 100644
index 0000000..7c81f22
--- /dev/null
+++ b/src/network/netdev/tunnel.h
@@ -0,0 +1,138 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "in-addr-util.h"
+
+#include "conf-parser.h"
+#include "fou-tunnel.h"
+#include "netdev-util.h"
+#include "netdev.h"
+#include "networkd-link.h"
+
+typedef enum Ip6TnlMode {
+ NETDEV_IP6_TNL_MODE_IP6IP6,
+ NETDEV_IP6_TNL_MODE_IPIP6,
+ NETDEV_IP6_TNL_MODE_ANYIP6,
+ _NETDEV_IP6_TNL_MODE_MAX,
+ _NETDEV_IP6_TNL_MODE_INVALID = -EINVAL,
+} Ip6TnlMode;
+
+typedef enum IPv6FlowLabel {
+ NETDEV_IPV6_FLOWLABEL_INHERIT = 0xFFFFF + 1,
+ _NETDEV_IPV6_FLOWLABEL_MAX,
+ _NETDEV_IPV6_FLOWLABEL_INVALID = -EINVAL,
+} IPv6FlowLabel;
+
+typedef struct Tunnel {
+ NetDev meta;
+
+ uint8_t encap_limit;
+
+ int family;
+ int ipv6_flowlabel;
+ int allow_localremote;
+ int gre_erspan_sequence;
+ int isatap;
+
+ unsigned ttl;
+ unsigned tos;
+ unsigned flags;
+
+ uint32_t key;
+ uint32_t ikey;
+ uint32_t okey;
+
+ uint8_t erspan_version;
+ uint32_t erspan_index; /* version 1 */
+ uint8_t erspan_direction; /* version 2 */
+ uint16_t erspan_hwid; /* version 2 */
+
+ NetDevLocalAddressType local_type;
+ union in_addr_union local;
+ union in_addr_union remote;
+
+ Ip6TnlMode ip6tnl_mode;
+ FooOverUDPEncapType fou_encap_type;
+
+ bool pmtudisc;
+ bool copy_dscp;
+ bool independent;
+ bool fou_tunnel;
+ bool assign_to_loopback;
+ bool external; /* a.k.a collect metadata mode */
+
+ uint16_t encap_src_port;
+ uint16_t fou_destination_port;
+
+ struct in6_addr sixrd_prefix;
+ uint8_t sixrd_prefixlen;
+} Tunnel;
+
+int dhcp4_pd_create_6rd_tunnel_name(Link *link, char **ret);
+int dhcp4_pd_create_6rd_tunnel(Link *link, link_netlink_message_handler_t callback);
+
+DEFINE_NETDEV_CAST(IPIP, Tunnel);
+DEFINE_NETDEV_CAST(GRE, Tunnel);
+DEFINE_NETDEV_CAST(GRETAP, Tunnel);
+DEFINE_NETDEV_CAST(IP6GRE, Tunnel);
+DEFINE_NETDEV_CAST(IP6GRETAP, Tunnel);
+DEFINE_NETDEV_CAST(SIT, Tunnel);
+DEFINE_NETDEV_CAST(VTI, Tunnel);
+DEFINE_NETDEV_CAST(VTI6, Tunnel);
+DEFINE_NETDEV_CAST(IP6TNL, Tunnel);
+DEFINE_NETDEV_CAST(ERSPAN, Tunnel);
+
+static inline Tunnel* TUNNEL(NetDev *netdev) {
+ assert(netdev);
+
+ switch (netdev->kind) {
+ case NETDEV_KIND_IPIP:
+ return IPIP(netdev);
+ case NETDEV_KIND_SIT:
+ return SIT(netdev);
+ case NETDEV_KIND_GRE:
+ return GRE(netdev);
+ case NETDEV_KIND_GRETAP:
+ return GRETAP(netdev);
+ case NETDEV_KIND_IP6GRE:
+ return IP6GRE(netdev);
+ case NETDEV_KIND_IP6GRETAP:
+ return IP6GRETAP(netdev);
+ case NETDEV_KIND_VTI:
+ return VTI(netdev);
+ case NETDEV_KIND_VTI6:
+ return VTI6(netdev);
+ case NETDEV_KIND_IP6TNL:
+ return IP6TNL(netdev);
+ case NETDEV_KIND_ERSPAN:
+ return ERSPAN(netdev);
+ default:
+ return NULL;
+ }
+}
+
+extern const NetDevVTable ipip_vtable;
+extern const NetDevVTable sit_vtable;
+extern const NetDevVTable vti_vtable;
+extern const NetDevVTable vti6_vtable;
+extern const NetDevVTable gre_vtable;
+extern const NetDevVTable gretap_vtable;
+extern const NetDevVTable ip6gre_vtable;
+extern const NetDevVTable ip6gretap_vtable;
+extern const NetDevVTable ip6tnl_vtable;
+extern const NetDevVTable erspan_vtable;
+
+const char *ip6tnl_mode_to_string(Ip6TnlMode d) _const_;
+Ip6TnlMode ip6tnl_mode_from_string(const char *d) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ip6tnl_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_tunnel_local_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_tunnel_remote_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_flowlabel);
+CONFIG_PARSER_PROTOTYPE(config_parse_encap_limit);
+CONFIG_PARSER_PROTOTYPE(config_parse_tunnel_key);
+CONFIG_PARSER_PROTOTYPE(config_parse_6rd_prefix);
+CONFIG_PARSER_PROTOTYPE(config_parse_erspan_version);
+CONFIG_PARSER_PROTOTYPE(config_parse_erspan_index);
+CONFIG_PARSER_PROTOTYPE(config_parse_erspan_direction);
+CONFIG_PARSER_PROTOTYPE(config_parse_erspan_hwid);
diff --git a/src/network/netdev/tuntap.c b/src/network/netdev/tuntap.c
new file mode 100644
index 0000000..10c93be
--- /dev/null
+++ b/src/network/netdev/tuntap.c
@@ -0,0 +1,261 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <net/if.h>
+#include <netinet/if_ether.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <linux/if_tun.h>
+
+#include "alloc-util.h"
+#include "daemon-util.h"
+#include "fd-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "socket-util.h"
+#include "tuntap.h"
+#include "user-util.h"
+
+#define TUN_DEV "/dev/net/tun"
+
+static TunTap* TUNTAP(NetDev *netdev) {
+ assert(netdev);
+
+ switch (netdev->kind) {
+ case NETDEV_KIND_TAP:
+ return TAP(netdev);
+ case NETDEV_KIND_TUN:
+ return TUN(netdev);
+ default:
+ return NULL;
+ }
+}
+
+static void *close_fd_ptr(void *p) {
+ safe_close(PTR_TO_FD(p));
+ return NULL;
+}
+
+DEFINE_PRIVATE_HASH_OPS_FULL(named_fd_hash_ops, char, string_hash_func, string_compare_func, free, void, close_fd_ptr);
+
+int manager_add_tuntap_fd(Manager *m, int fd, const char *name) {
+ _cleanup_free_ char *tuntap_name = NULL;
+ const char *p;
+ int r;
+
+ assert(m);
+ assert(fd >= 0);
+ assert(name);
+
+ p = startswith(name, "tuntap-");
+ if (!p)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Received unknown fd (%s).", name);
+
+ if (!ifname_valid(p))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Received tuntap fd with invalid name (%s).", p);
+
+ tuntap_name = strdup(p);
+ if (!tuntap_name)
+ return log_oom_debug();
+
+ r = hashmap_ensure_put(&m->tuntap_fds_by_name, &named_fd_hash_ops, tuntap_name, FD_TO_PTR(fd));
+ if (r < 0)
+ return log_debug_errno(r, "Failed to store tuntap fd: %m");
+
+ TAKE_PTR(tuntap_name);
+ return 0;
+}
+
+void manager_clear_unmanaged_tuntap_fds(Manager *m) {
+ char *name;
+ void *p;
+
+ assert(m);
+
+ while ((p = hashmap_steal_first_key_and_value(m->tuntap_fds_by_name, (void**) &name))) {
+ close_and_notify_warn(PTR_TO_FD(p), name);
+ name = mfree(name);
+ }
+}
+
+static int tuntap_take_fd(NetDev *netdev) {
+ _cleanup_free_ char *name = NULL;
+ void *p;
+ int r;
+
+ assert(netdev);
+ assert(netdev->manager);
+
+ r = link_get_by_name(netdev->manager, netdev->ifname, NULL);
+ if (r < 0)
+ return r;
+
+ p = hashmap_remove2(netdev->manager->tuntap_fds_by_name, netdev->ifname, (void**) &name);
+ if (!p)
+ return -ENOENT;
+
+ log_netdev_debug(netdev, "Found file descriptor in fd store.");
+ return PTR_TO_FD(p);
+}
+
+static int netdev_create_tuntap(NetDev *netdev) {
+ _cleanup_close_ int fd = -1;
+ struct ifreq ifr = {};
+ TunTap *t;
+ int r;
+
+ assert(netdev);
+ t = TUNTAP(netdev);
+ assert(t);
+
+ fd = TAKE_FD(t->fd);
+ if (fd < 0)
+ fd = tuntap_take_fd(netdev);
+ if (fd < 0)
+ fd = open(TUN_DEV, O_RDWR|O_CLOEXEC);
+ if (fd < 0)
+ return log_netdev_error_errno(netdev, errno, "Failed to open " TUN_DEV ": %m");
+
+ if (netdev->kind == NETDEV_KIND_TAP)
+ ifr.ifr_flags |= IFF_TAP;
+ else
+ ifr.ifr_flags |= IFF_TUN;
+
+ if (!t->packet_info)
+ ifr.ifr_flags |= IFF_NO_PI;
+
+ if (t->multi_queue)
+ ifr.ifr_flags |= IFF_MULTI_QUEUE;
+
+ if (t->vnet_hdr)
+ ifr.ifr_flags |= IFF_VNET_HDR;
+
+ strncpy(ifr.ifr_name, netdev->ifname, IFNAMSIZ-1);
+
+ if (ioctl(fd, TUNSETIFF, &ifr) < 0)
+ return log_netdev_error_errno(netdev, errno, "TUNSETIFF failed: %m");
+
+ if (t->multi_queue) {
+ /* If we don't detach the queue, the kernel will send packets to our queue and they
+ * will be dropped because we never read them, which is especially important in case
+ * of KeepCarrier option which persists open FD. So detach our queue right after
+ * device create/attach to make kernel not send the packets to it. The option is
+ * available for multi-queue devices only.
+ *
+ * See https://github.com/systemd/systemd/pull/30504 for details. */
+ struct ifreq detach_request = { .ifr_flags = IFF_DETACH_QUEUE };
+ if (ioctl(fd, TUNSETQUEUE, &detach_request) < 0)
+ return log_netdev_error_errno(netdev, errno, "TUNSETQUEUE failed: %m");
+ }
+
+ if (t->user_name) {
+ const char *user = t->user_name;
+ uid_t uid;
+
+ r = get_user_creds(&user, &uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Cannot resolve user name %s: %m", t->user_name);
+
+ if (ioctl(fd, TUNSETOWNER, uid) < 0)
+ return log_netdev_error_errno(netdev, errno, "TUNSETOWNER failed: %m");
+ }
+
+ if (t->group_name) {
+ const char *group = t->group_name;
+ gid_t gid;
+
+ r = get_group_creds(&group, &gid, USER_CREDS_ALLOW_MISSING);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Cannot resolve group name %s: %m", t->group_name);
+
+ if (ioctl(fd, TUNSETGROUP, gid) < 0)
+ return log_netdev_error_errno(netdev, errno, "TUNSETGROUP failed: %m");
+
+ }
+
+ if (ioctl(fd, TUNSETPERSIST, 1) < 0)
+ return log_netdev_error_errno(netdev, errno, "TUNSETPERSIST failed: %m");
+
+ if (t->keep_fd) {
+ t->fd = TAKE_FD(fd);
+ (void) notify_push_fdf(t->fd, "tuntap-%s", netdev->ifname);
+ }
+
+ return 0;
+}
+
+static void tuntap_init(NetDev *netdev) {
+ TunTap *t;
+
+ assert(netdev);
+ t = TUNTAP(netdev);
+ assert(t);
+
+ t->fd = -1;
+}
+
+static void tuntap_drop(NetDev *netdev) {
+ TunTap *t;
+
+ assert(netdev);
+ t = TUNTAP(netdev);
+ assert(t);
+
+ t->fd = close_and_notify_warn(t->fd, netdev->ifname);
+}
+
+static void tuntap_done(NetDev *netdev) {
+ TunTap *t;
+
+ assert(netdev);
+ t = TUNTAP(netdev);
+ assert(t);
+
+ t->fd = safe_close(t->fd);
+ t->user_name = mfree(t->user_name);
+ t->group_name = mfree(t->group_name);
+}
+
+static int tuntap_verify(NetDev *netdev, const char *filename) {
+ assert(netdev);
+
+ if (netdev->mtu != 0)
+ log_netdev_warning(netdev,
+ "MTUBytes= configured for %s device in %s will be ignored.\n"
+ "Please set it in the corresponding .network file.",
+ netdev_kind_to_string(netdev->kind), filename);
+
+ if (netdev->hw_addr.length > 0)
+ log_netdev_warning(netdev,
+ "MACAddress= configured for %s device in %s will be ignored.\n"
+ "Please set it in the corresponding .network file.",
+ netdev_kind_to_string(netdev->kind), filename);
+
+ return 0;
+}
+
+const NetDevVTable tun_vtable = {
+ .object_size = sizeof(TunTap),
+ .sections = NETDEV_COMMON_SECTIONS "Tun\0",
+ .config_verify = tuntap_verify,
+ .init = tuntap_init,
+ .drop = tuntap_drop,
+ .done = tuntap_done,
+ .create = netdev_create_tuntap,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_NONE,
+};
+
+const NetDevVTable tap_vtable = {
+ .object_size = sizeof(TunTap),
+ .sections = NETDEV_COMMON_SECTIONS "Tap\0",
+ .config_verify = tuntap_verify,
+ .init = tuntap_init,
+ .drop = tuntap_drop,
+ .done = tuntap_done,
+ .create = netdev_create_tuntap,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_ETHER,
+};
diff --git a/src/network/netdev/tuntap.h b/src/network/netdev/tuntap.h
new file mode 100644
index 0000000..88e0ce5
--- /dev/null
+++ b/src/network/netdev/tuntap.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct TunTap TunTap;
+
+#include "netdev.h"
+
+struct TunTap {
+ NetDev meta;
+
+ int fd;
+ char *user_name;
+ char *group_name;
+ bool multi_queue;
+ bool packet_info;
+ bool vnet_hdr;
+ bool keep_fd;
+};
+
+DEFINE_NETDEV_CAST(TUN, TunTap);
+DEFINE_NETDEV_CAST(TAP, TunTap);
+extern const NetDevVTable tun_vtable;
+extern const NetDevVTable tap_vtable;
+
+int manager_add_tuntap_fd(Manager *m, int fd, const char *name);
+void manager_clear_unmanaged_tuntap_fds(Manager *m);
diff --git a/src/network/netdev/vcan.c b/src/network/netdev/vcan.c
new file mode 100644
index 0000000..380547e
--- /dev/null
+++ b/src/network/netdev/vcan.c
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/if_arp.h>
+
+#include "vcan.h"
+
+const NetDevVTable vcan_vtable = {
+ .object_size = sizeof(VCan),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_CAN,
+};
diff --git a/src/network/netdev/vcan.h b/src/network/netdev/vcan.h
new file mode 100644
index 0000000..843984f
--- /dev/null
+++ b/src/network/netdev/vcan.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct VCan VCan;
+
+#include <netinet/in.h>
+#include <linux/can/netlink.h>
+
+#include "netdev.h"
+
+struct VCan {
+ NetDev meta;
+};
+
+DEFINE_NETDEV_CAST(VCAN, VCan);
+
+extern const NetDevVTable vcan_vtable;
diff --git a/src/network/netdev/veth.c b/src/network/netdev/veth.c
new file mode 100644
index 0000000..fb00e66
--- /dev/null
+++ b/src/network/netdev/veth.c
@@ -0,0 +1,98 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+#include <linux/veth.h>
+
+#include "netlink-util.h"
+#include "veth.h"
+
+static int netdev_veth_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ struct hw_addr_data hw_addr;
+ Veth *v;
+ int r;
+
+ assert(netdev);
+ assert(!link);
+ assert(m);
+
+ v = VETH(netdev);
+
+ assert(v);
+
+ r = sd_netlink_message_open_container(m, VETH_INFO_PEER);
+ if (r < 0)
+ return r;
+
+ if (v->ifname_peer) {
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, v->ifname_peer);
+ if (r < 0)
+ return r;
+ }
+
+ r = netdev_generate_hw_addr(netdev, NULL, v->ifname_peer, &v->hw_addr_peer, &hw_addr);
+ if (r < 0)
+ return r;
+
+ if (hw_addr.length > 0) {
+ log_netdev_debug(netdev, "Using MAC address for peer: %s", HW_ADDR_TO_STR(&hw_addr));
+ r = netlink_message_append_hw_addr(m, IFLA_ADDRESS, &hw_addr);
+ if (r < 0)
+ return r;
+ }
+
+ if (netdev->mtu != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_MTU, netdev->mtu);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int netdev_veth_verify(NetDev *netdev, const char *filename) {
+ Veth *v;
+
+ assert(netdev);
+ assert(filename);
+
+ v = VETH(netdev);
+
+ assert(v);
+
+ if (!v->ifname_peer)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "Veth NetDev without peer name configured in %s. Ignoring",
+ filename);
+
+ return 0;
+}
+
+static void veth_done(NetDev *n) {
+ Veth *v;
+
+ assert(n);
+
+ v = VETH(n);
+
+ assert(v);
+
+ free(v->ifname_peer);
+}
+
+const NetDevVTable veth_vtable = {
+ .object_size = sizeof(Veth),
+ .sections = NETDEV_COMMON_SECTIONS "Peer\0",
+ .done = veth_done,
+ .fill_message_create = netdev_veth_fill_message_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = netdev_veth_verify,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/veth.h b/src/network/netdev/veth.h
new file mode 100644
index 0000000..e0d6fd4
--- /dev/null
+++ b/src/network/netdev/veth.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct Veth Veth;
+
+#include "netdev.h"
+
+struct Veth {
+ NetDev meta;
+
+ char *ifname_peer;
+ struct hw_addr_data hw_addr_peer;
+};
+
+DEFINE_NETDEV_CAST(VETH, Veth);
+extern const NetDevVTable veth_vtable;
diff --git a/src/network/netdev/vlan.c b/src/network/netdev/vlan.c
new file mode 100644
index 0000000..5eb36ef
--- /dev/null
+++ b/src/network/netdev/vlan.c
@@ -0,0 +1,234 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <net/if.h>
+#include <linux/if_arp.h>
+#include <linux/if_vlan.h>
+
+#include "parse-util.h"
+#include "vlan-util.h"
+#include "vlan.h"
+
+static int netdev_vlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *req) {
+ struct ifla_vlan_flags flags = {};
+ VLan *v;
+ int r;
+
+ assert(netdev);
+ assert(link);
+ assert(req);
+
+ v = VLAN(netdev);
+
+ assert(v);
+
+ r = sd_netlink_message_append_u16(req, IFLA_VLAN_ID, v->id);
+ if (r < 0)
+ return r;
+
+ if (v->protocol >= 0) {
+ r = sd_netlink_message_append_u16(req, IFLA_VLAN_PROTOCOL, htobe16(v->protocol));
+ if (r < 0)
+ return r;
+ }
+
+ if (v->gvrp != -1) {
+ flags.mask |= VLAN_FLAG_GVRP;
+ SET_FLAG(flags.flags, VLAN_FLAG_GVRP, v->gvrp);
+ }
+
+ if (v->mvrp != -1) {
+ flags.mask |= VLAN_FLAG_MVRP;
+ SET_FLAG(flags.flags, VLAN_FLAG_MVRP, v->mvrp);
+ }
+
+ if (v->reorder_hdr != -1) {
+ flags.mask |= VLAN_FLAG_REORDER_HDR;
+ SET_FLAG(flags.flags, VLAN_FLAG_REORDER_HDR, v->reorder_hdr);
+ }
+
+ if (v->loose_binding != -1) {
+ flags.mask |= VLAN_FLAG_LOOSE_BINDING;
+ SET_FLAG(flags.flags, VLAN_FLAG_LOOSE_BINDING, v->loose_binding);
+ }
+
+ r = sd_netlink_message_append_data(req, IFLA_VLAN_FLAGS, &flags, sizeof(struct ifla_vlan_flags));
+ if (r < 0)
+ return r;
+
+ if (!set_isempty(v->egress_qos_maps)) {
+ struct ifla_vlan_qos_mapping *m;
+
+ r = sd_netlink_message_open_container(req, IFLA_VLAN_EGRESS_QOS);
+ if (r < 0)
+ return r;
+
+ SET_FOREACH(m, v->egress_qos_maps) {
+ r = sd_netlink_message_append_data(req, IFLA_VLAN_QOS_MAPPING, m, sizeof(struct ifla_vlan_qos_mapping));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+ }
+
+ if (!set_isempty(v->ingress_qos_maps)) {
+ struct ifla_vlan_qos_mapping *m;
+
+ r = sd_netlink_message_open_container(req, IFLA_VLAN_INGRESS_QOS);
+ if (r < 0)
+ return r;
+
+ SET_FOREACH(m, v->ingress_qos_maps) {
+ r = sd_netlink_message_append_data(req, IFLA_VLAN_QOS_MAPPING, m, sizeof(struct ifla_vlan_qos_mapping));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static void vlan_qos_maps_hash_func(const struct ifla_vlan_qos_mapping *x, struct siphash *state) {
+ siphash24_compress(&x->from, sizeof(x->from), state);
+ siphash24_compress(&x->to, sizeof(x->to), state);
+}
+
+static int vlan_qos_maps_compare_func(const struct ifla_vlan_qos_mapping *a, const struct ifla_vlan_qos_mapping *b) {
+ int r;
+
+ r = CMP(a->from, b->from);
+ if (r != 0)
+ return r;
+
+ return CMP(a->to, b->to);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ vlan_qos_maps_hash_ops,
+ struct ifla_vlan_qos_mapping,
+ vlan_qos_maps_hash_func,
+ vlan_qos_maps_compare_func,
+ free);
+
+int config_parse_vlan_qos_maps(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Set **s = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *s = set_free(*s);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ struct ifla_vlan_qos_mapping *m = NULL;
+ _cleanup_free_ char *w = NULL;
+ unsigned from, to;
+
+ r = extract_first_word(&p, &w, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse %s, ignoring: %s", lvalue, rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ r = parse_range(w, &from, &to);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse %s, ignoring: %s", lvalue, w);
+ continue;
+ }
+
+ m = new(struct ifla_vlan_qos_mapping, 1);
+ if (!m)
+ return log_oom();
+
+ *m = (struct ifla_vlan_qos_mapping) {
+ .from = from,
+ .to = to,
+ };
+
+ r = set_ensure_consume(s, &vlan_qos_maps_hash_ops, TAKE_PTR(m));
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to store %s, ignoring: %s", lvalue, w);
+ continue;
+ }
+ }
+}
+
+static int netdev_vlan_verify(NetDev *netdev, const char *filename) {
+ VLan *v;
+
+ assert(netdev);
+ assert(filename);
+
+ v = VLAN(netdev);
+
+ assert(v);
+
+ if (v->id == VLANID_INVALID) {
+ log_netdev_warning(netdev, "VLAN without valid Id (%"PRIu16") configured in %s.", v->id, filename);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static void vlan_done(NetDev *n) {
+ VLan *v;
+
+ v = VLAN(n);
+
+ assert(v);
+
+ set_free(v->egress_qos_maps);
+ set_free(v->ingress_qos_maps);
+}
+
+static void vlan_init(NetDev *netdev) {
+ VLan *v = VLAN(netdev);
+
+ assert(netdev);
+ assert(v);
+
+ v->id = VLANID_INVALID;
+ v->protocol = -1;
+ v->gvrp = -1;
+ v->mvrp = -1;
+ v->loose_binding = -1;
+ v->reorder_hdr = -1;
+}
+
+const NetDevVTable vlan_vtable = {
+ .object_size = sizeof(VLan),
+ .init = vlan_init,
+ .sections = NETDEV_COMMON_SECTIONS "VLAN\0",
+ .fill_message_create = netdev_vlan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_vlan_verify,
+ .done = vlan_done,
+ .iftype = ARPHRD_ETHER,
+};
diff --git a/src/network/netdev/vlan.h b/src/network/netdev/vlan.h
new file mode 100644
index 0000000..1e5e590
--- /dev/null
+++ b/src/network/netdev/vlan.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct VLan VLan;
+
+#include "netdev.h"
+#include "set.h"
+
+struct VLan {
+ NetDev meta;
+
+ uint16_t id;
+ int protocol;
+
+ int gvrp;
+ int mvrp;
+ int loose_binding;
+ int reorder_hdr;
+
+ Set *egress_qos_maps;
+ Set *ingress_qos_maps;
+};
+
+DEFINE_NETDEV_CAST(VLAN, VLan);
+extern const NetDevVTable vlan_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_vlan_qos_maps);
diff --git a/src/network/netdev/vrf.c b/src/network/netdev/vrf.c
new file mode 100644
index 0000000..05ef3ff
--- /dev/null
+++ b/src/network/netdev/vrf.c
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "vrf.h"
+
+static int netdev_vrf_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ Vrf *v;
+ int r;
+
+ assert(netdev);
+ assert(!link);
+ assert(m);
+
+ v = VRF(netdev);
+
+ assert(v);
+
+ r = sd_netlink_message_append_u32(m, IFLA_VRF_TABLE, v->table);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+const NetDevVTable vrf_vtable = {
+ .object_size = sizeof(Vrf),
+ .sections = NETDEV_COMMON_SECTIONS "VRF\0",
+ .fill_message_create = netdev_vrf_fill_message_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/vrf.h b/src/network/netdev/vrf.h
new file mode 100644
index 0000000..87977e2
--- /dev/null
+++ b/src/network/netdev/vrf.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct Vrf Vrf;
+
+#include "netdev.h"
+
+struct Vrf {
+ NetDev meta;
+
+ uint32_t table;
+};
+
+DEFINE_NETDEV_CAST(VRF, Vrf);
+extern const NetDevVTable vrf_vtable;
diff --git a/src/network/netdev/vxcan.c b/src/network/netdev/vxcan.c
new file mode 100644
index 0000000..83269b0
--- /dev/null
+++ b/src/network/netdev/vxcan.c
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/can/vxcan.h>
+#include <linux/if_arp.h>
+
+#include "vxcan.h"
+
+static int netdev_vxcan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ VxCan *v;
+ int r;
+
+ assert(netdev);
+ assert(!link);
+ assert(m);
+
+ v = VXCAN(netdev);
+
+ assert(v);
+
+ r = sd_netlink_message_open_container(m, VXCAN_INFO_PEER);
+ if (r < 0)
+ return r;
+
+ if (v->ifname_peer) {
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, v->ifname_peer);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int netdev_vxcan_verify(NetDev *netdev, const char *filename) {
+ VxCan *v;
+
+ assert(netdev);
+ assert(filename);
+
+ v = VXCAN(netdev);
+
+ assert(v);
+
+ if (!v->ifname_peer)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "VxCan NetDev without peer name configured in %s. Ignoring", filename);
+
+ return 0;
+}
+
+static void vxcan_done(NetDev *n) {
+ VxCan *v;
+
+ assert(n);
+
+ v = VXCAN(n);
+
+ assert(v);
+
+ free(v->ifname_peer);
+}
+
+const NetDevVTable vxcan_vtable = {
+ .object_size = sizeof(VxCan),
+ .sections = NETDEV_COMMON_SECTIONS "VXCAN\0",
+ .done = vxcan_done,
+ .fill_message_create = netdev_vxcan_fill_message_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = netdev_vxcan_verify,
+ .iftype = ARPHRD_CAN,
+};
diff --git a/src/network/netdev/vxcan.h b/src/network/netdev/vxcan.h
new file mode 100644
index 0000000..47be3f0
--- /dev/null
+++ b/src/network/netdev/vxcan.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct VxCan VxCan;
+
+#include "netdev.h"
+
+struct VxCan {
+ NetDev meta;
+
+ char *ifname_peer;
+};
+
+DEFINE_NETDEV_CAST(VXCAN, VxCan);
+
+extern const NetDevVTable vxcan_vtable;
diff --git a/src/network/netdev/vxlan.c b/src/network/netdev/vxlan.c
new file mode 100644
index 0000000..5891619
--- /dev/null
+++ b/src/network/netdev/vxlan.c
@@ -0,0 +1,468 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "conf-parser.h"
+#include "alloc-util.h"
+#include "extract-word.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "parse-util.h"
+#include "vxlan.h"
+
+static const char* const df_table[_NETDEV_VXLAN_DF_MAX] = {
+ [NETDEV_VXLAN_DF_NO] = "no",
+ [NETDEV_VXLAN_DF_YES] = "yes",
+ [NETDEV_VXLAN_DF_INHERIT] = "inherit",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(df, VxLanDF, NETDEV_VXLAN_DF_YES);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_df, df, VxLanDF, "Failed to parse VXLAN IPDoNotFragment= setting");
+
+static int vxlan_get_local_address(VxLan *v, Link *link, int *ret_family, union in_addr_union *ret_address) {
+ assert(v);
+
+ if (v->local_type < 0) {
+ if (ret_family)
+ *ret_family = v->local_family;
+ if (ret_address)
+ *ret_address = v->local;
+ return 0;
+ }
+
+ return link_get_local_address(link, v->local_type, v->local_family, ret_family, ret_address);
+}
+
+static int netdev_vxlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ union in_addr_union local;
+ int local_family, r;
+ VxLan *v;
+
+ assert(netdev);
+ assert(m);
+
+ v = VXLAN(netdev);
+
+ assert(v);
+
+ if (v->vni <= VXLAN_VID_MAX) {
+ r = sd_netlink_message_append_u32(m, IFLA_VXLAN_ID, v->vni);
+ if (r < 0)
+ return r;
+ }
+
+ if (in_addr_is_set(v->group_family, &v->group)) {
+ if (v->group_family == AF_INET)
+ r = sd_netlink_message_append_in_addr(m, IFLA_VXLAN_GROUP, &v->group.in);
+ else
+ r = sd_netlink_message_append_in6_addr(m, IFLA_VXLAN_GROUP6, &v->group.in6);
+ if (r < 0)
+ return r;
+ } else if (in_addr_is_set(v->remote_family, &v->remote)) {
+ if (v->remote_family == AF_INET)
+ r = sd_netlink_message_append_in_addr(m, IFLA_VXLAN_GROUP, &v->remote.in);
+ else
+ r = sd_netlink_message_append_in6_addr(m, IFLA_VXLAN_GROUP6, &v->remote.in6);
+ if (r < 0)
+ return r;
+ }
+
+ r = vxlan_get_local_address(v, link, &local_family, &local);
+ if (r < 0)
+ return r;
+
+ if (in_addr_is_set(local_family, &local)) {
+ if (local_family == AF_INET)
+ r = sd_netlink_message_append_in_addr(m, IFLA_VXLAN_LOCAL, &local.in);
+ else
+ r = sd_netlink_message_append_in6_addr(m, IFLA_VXLAN_LOCAL6, &local.in6);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LINK, link ? link->ifindex : 0);
+ if (r < 0)
+ return r;
+
+ if (v->inherit) {
+ r = sd_netlink_message_append_flag(m, IFLA_VXLAN_TTL_INHERIT);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_TTL, v->ttl);
+ if (r < 0)
+ return r;
+ }
+
+ if (v->tos != 0) {
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_TOS, v->tos);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_LEARNING, v->learning);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_RSC, v->route_short_circuit);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_PROXY, v->arp_proxy);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_L2MISS, v->l2miss);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_L3MISS, v->l3miss);
+ if (r < 0)
+ return r;
+
+ if (v->fdb_ageing != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_VXLAN_AGEING, v->fdb_ageing / USEC_PER_SEC);
+ if (r < 0)
+ return r;
+ }
+
+ if (v->max_fdb != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LIMIT, v->max_fdb);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_CSUM, v->udpcsum);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_ZERO_CSUM6_TX, v->udp6zerocsumtx);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_ZERO_CSUM6_RX, v->udp6zerocsumrx);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_REMCSUM_TX, v->remote_csum_tx);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_REMCSUM_RX, v->remote_csum_rx);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u16(m, IFLA_VXLAN_PORT, htobe16(v->dest_port));
+ if (r < 0)
+ return r;
+
+ if (v->port_range.low != 0 || v->port_range.high != 0) {
+ struct ifla_vxlan_port_range port_range;
+
+ port_range.low = htobe16(v->port_range.low);
+ port_range.high = htobe16(v->port_range.high);
+
+ r = sd_netlink_message_append_data(m, IFLA_VXLAN_PORT_RANGE, &port_range, sizeof(port_range));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LABEL, htobe32(v->flow_label));
+ if (r < 0)
+ return r;
+
+ if (v->group_policy) {
+ r = sd_netlink_message_append_flag(m, IFLA_VXLAN_GBP);
+ if (r < 0)
+ return r;
+ }
+
+ if (v->generic_protocol_extension) {
+ r = sd_netlink_message_append_flag(m, IFLA_VXLAN_GPE);
+ if (r < 0)
+ return r;
+ }
+
+ if (v->df != _NETDEV_VXLAN_DF_INVALID) {
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_DF, v->df);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int config_parse_vxlan_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ VxLan *v = ASSERT_PTR(userdata);
+ union in_addr_union *addr = data, buffer;
+ int *family, f, r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(lvalue, "Local"))
+ family = &v->local_family;
+ else if (streq(lvalue, "Remote"))
+ family = &v->remote_family;
+ else if (streq(lvalue, "Group"))
+ family = &v->group_family;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *addr = IN_ADDR_NULL;
+ *family = AF_UNSPEC;
+ return 0;
+ }
+
+ if (streq(lvalue, "Local")) {
+ NetDevLocalAddressType t;
+
+ t = netdev_local_address_type_from_string(rvalue);
+ if (t >= 0) {
+ v->local = IN_ADDR_NULL;
+ v->local_family = AF_UNSPEC;
+ v->local_type = t;
+ return 0;
+ }
+ }
+
+ r = in_addr_from_string_auto(rvalue, &f, &buffer);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ r = in_addr_is_multicast(f, &buffer);
+
+ if (streq(lvalue, "Group")) {
+ if (r <= 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "%s= must be a multicast address, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+ } else {
+ if (r > 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "%s= cannot be a multicast address, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+ }
+
+ if (streq(lvalue, "Local"))
+ v->local_type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID;
+ *addr = buffer;
+ *family = f;
+
+ return 0;
+}
+
+int config_parse_port_range(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ VxLan *v = userdata;
+ uint16_t low, high;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = parse_ip_port_range(rvalue, &low, &high);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse VXLAN port range '%s'. Port should be greater than 0 and less than 65535.", rvalue);
+ return 0;
+ }
+
+ v->port_range.low = low;
+ v->port_range.high = high;
+
+ return 0;
+}
+
+int config_parse_flow_label(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ VxLan *v = userdata;
+ unsigned f;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou(rvalue, &f);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse VXLAN flow label '%s'.", rvalue);
+ return 0;
+ }
+
+ if (f & ~VXLAN_FLOW_LABEL_MAX_MASK) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "VXLAN flow label '%s' not valid. Flow label range should be [0-1048575].", rvalue);
+ return 0;
+ }
+
+ v->flow_label = f;
+
+ return 0;
+}
+
+int config_parse_vxlan_ttl(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ VxLan *v = userdata;
+ unsigned f;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(rvalue, "inherit"))
+ v->inherit = true;
+ else {
+ r = safe_atou(rvalue, &f);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse VXLAN TTL '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (f > 255) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid VXLAN TTL '%s'. TTL must be <= 255. Ignoring assignment.", rvalue);
+ return 0;
+ }
+
+ v->ttl = f;
+ }
+
+ return 0;
+}
+
+static int netdev_vxlan_verify(NetDev *netdev, const char *filename) {
+ VxLan *v = VXLAN(netdev);
+
+ assert(netdev);
+ assert(v);
+ assert(filename);
+
+ if (v->vni > VXLAN_VID_MAX)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: VXLAN without valid VNI (or VXLAN Segment ID) configured. Ignoring.",
+ filename);
+
+ if (v->ttl > 255)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: VXLAN TTL must be <= 255. Ignoring.",
+ filename);
+
+ if (!v->dest_port && v->generic_protocol_extension)
+ v->dest_port = 4790;
+
+ if (in_addr_is_set(v->group_family, &v->group) && in_addr_is_set(v->remote_family, &v->remote))
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: VXLAN both 'Group=' and 'Remote=' cannot be specified. Ignoring.",
+ filename);
+
+ if (v->independent && v->local_type >= 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "The local address cannot be '%s' when Independent= is enabled, ignoring.",
+ strna(netdev_local_address_type_to_string(v->local_type)));
+
+ return 0;
+}
+
+static int netdev_vxlan_is_ready_to_create(NetDev *netdev, Link *link) {
+ VxLan *v;
+
+ assert(netdev);
+
+ v = VXLAN(netdev);
+
+ assert(v);
+
+ if (v->independent)
+ return true;
+
+ return vxlan_get_local_address(v, link, NULL, NULL) >= 0;
+}
+
+static void vxlan_init(NetDev *netdev) {
+ VxLan *v;
+
+ assert(netdev);
+
+ v = VXLAN(netdev);
+
+ assert(v);
+
+ v->local_type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID;
+ v->vni = VXLAN_VID_MAX + 1;
+ v->df = _NETDEV_VXLAN_DF_INVALID;
+ v->learning = true;
+ v->udpcsum = false;
+ v->udp6zerocsumtx = false;
+ v->udp6zerocsumrx = false;
+}
+
+const NetDevVTable vxlan_vtable = {
+ .object_size = sizeof(VxLan),
+ .init = vxlan_init,
+ .sections = NETDEV_COMMON_SECTIONS "VXLAN\0",
+ .fill_message_create = netdev_vxlan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .is_ready_to_create = netdev_vxlan_is_ready_to_create,
+ .config_verify = netdev_vxlan_verify,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/vxlan.h b/src/network/netdev/vxlan.h
new file mode 100644
index 0000000..141ac4d
--- /dev/null
+++ b/src/network/netdev/vxlan.h
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct VxLan VxLan;
+
+#include <linux/if_link.h>
+
+#include "in-addr-util.h"
+#include "netdev-util.h"
+#include "netdev.h"
+
+#define VXLAN_VID_MAX (1u << 24) - 1
+#define VXLAN_FLOW_LABEL_MAX_MASK 0xFFFFFU
+
+typedef enum VxLanDF {
+ NETDEV_VXLAN_DF_NO = VXLAN_DF_UNSET,
+ NETDEV_VXLAN_DF_YES = VXLAN_DF_SET,
+ NETDEV_VXLAN_DF_INHERIT = VXLAN_DF_INHERIT,
+ _NETDEV_VXLAN_DF_MAX,
+ _NETDEV_VXLAN_DF_INVALID = -EINVAL,
+} VxLanDF;
+
+struct VxLan {
+ NetDev meta;
+
+ uint32_t vni;
+
+ int remote_family;
+ int local_family;
+ int group_family;
+
+ VxLanDF df;
+
+ NetDevLocalAddressType local_type;
+ union in_addr_union local;
+ union in_addr_union remote;
+ union in_addr_union group;
+
+ unsigned tos;
+ unsigned ttl;
+ unsigned max_fdb;
+ unsigned flow_label;
+
+ uint16_t dest_port;
+
+ usec_t fdb_ageing;
+
+ bool learning;
+ bool arp_proxy;
+ bool route_short_circuit;
+ bool l2miss;
+ bool l3miss;
+ bool udpcsum;
+ bool udp6zerocsumtx;
+ bool udp6zerocsumrx;
+ bool remote_csum_tx;
+ bool remote_csum_rx;
+ bool group_policy;
+ bool generic_protocol_extension;
+ bool inherit;
+ bool independent;
+
+ struct ifla_vxlan_port_range port_range;
+};
+
+DEFINE_NETDEV_CAST(VXLAN, VxLan);
+extern const NetDevVTable vxlan_vtable;
+
+const char *df_to_string(VxLanDF d) _const_;
+VxLanDF df_from_string(const char *d) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_vxlan_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_port_range);
+CONFIG_PARSER_PROTOTYPE(config_parse_flow_label);
+CONFIG_PARSER_PROTOTYPE(config_parse_df);
+CONFIG_PARSER_PROTOTYPE(config_parse_vxlan_ttl);
diff --git a/src/network/netdev/wireguard.c b/src/network/netdev/wireguard.c
new file mode 100644
index 0000000..51e7e02
--- /dev/null
+++ b/src/network/netdev/wireguard.c
@@ -0,0 +1,1183 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2015-2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+***/
+
+#include <sys/ioctl.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+#include <linux/ipv6_route.h>
+
+#include "sd-resolve.h"
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "event-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "hexdecoct.h"
+#include "memory-util.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "networkd-route-util.h"
+#include "networkd-route.h"
+#include "networkd-util.h"
+#include "parse-helpers.h"
+#include "parse-util.h"
+#include "random-util.h"
+#include "resolve-private.h"
+#include "string-util.h"
+#include "strv.h"
+#include "wireguard.h"
+
+static void wireguard_resolve_endpoints(NetDev *netdev);
+static int peer_resolve_endpoint(WireguardPeer *peer);
+
+static WireguardPeer* wireguard_peer_free(WireguardPeer *peer) {
+ WireguardIPmask *mask;
+
+ if (!peer)
+ return NULL;
+
+ if (peer->wireguard) {
+ LIST_REMOVE(peers, peer->wireguard->peers, peer);
+
+ if (peer->section)
+ hashmap_remove(peer->wireguard->peers_by_section, peer->section);
+ }
+
+ config_section_free(peer->section);
+
+ while ((mask = peer->ipmasks)) {
+ LIST_REMOVE(ipmasks, peer->ipmasks, mask);
+ free(mask);
+ }
+
+ free(peer->endpoint_host);
+ free(peer->endpoint_port);
+ free(peer->preshared_key_file);
+ explicit_bzero_safe(peer->preshared_key, WG_KEY_LEN);
+
+ sd_event_source_disable_unref(peer->resolve_retry_event_source);
+ sd_resolve_query_unref(peer->resolve_query);
+
+ return mfree(peer);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(WireguardPeer, wireguard_peer_free);
+
+static int wireguard_peer_new_static(Wireguard *w, const char *filename, unsigned section_line, WireguardPeer **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(wireguard_peer_freep) WireguardPeer *peer = NULL;
+ int r;
+
+ assert(w);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ peer = hashmap_get(w->peers_by_section, n);
+ if (peer) {
+ *ret = TAKE_PTR(peer);
+ return 0;
+ }
+
+ peer = new(WireguardPeer, 1);
+ if (!peer)
+ return -ENOMEM;
+
+ *peer = (WireguardPeer) {
+ .flags = WGPEER_F_REPLACE_ALLOWEDIPS,
+ .wireguard = w,
+ .section = TAKE_PTR(n),
+ };
+
+ LIST_PREPEND(peers, w->peers, peer);
+
+ r = hashmap_ensure_put(&w->peers_by_section, &config_section_hash_ops, peer->section, peer);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(peer);
+ return 0;
+}
+
+static int wireguard_set_ipmask_one(NetDev *netdev, sd_netlink_message *message, const WireguardIPmask *mask, uint16_t index) {
+ int r;
+
+ assert(message);
+ assert(mask);
+ assert(index > 0);
+
+ /* This returns 1 on success, 0 on recoverable error, and negative errno on failure. */
+
+ r = sd_netlink_message_open_array(message, index);
+ if (r < 0)
+ return 0;
+
+ r = sd_netlink_message_append_u16(message, WGALLOWEDIP_A_FAMILY, mask->family);
+ if (r < 0)
+ goto cancel;
+
+ r = netlink_message_append_in_addr_union(message, WGALLOWEDIP_A_IPADDR, mask->family, &mask->ip);
+ if (r < 0)
+ goto cancel;
+
+ r = sd_netlink_message_append_u8(message, WGALLOWEDIP_A_CIDR_MASK, mask->cidr);
+ if (r < 0)
+ goto cancel;
+
+ r = sd_netlink_message_close_container(message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not add wireguard allowed ip: %m");
+
+ return 1;
+
+cancel:
+ r = sd_netlink_message_cancel_array(message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not cancel wireguard allowed ip message attribute: %m");
+
+ return 0;
+}
+
+static int wireguard_set_peer_one(NetDev *netdev, sd_netlink_message *message, const WireguardPeer *peer, uint16_t index, WireguardIPmask **mask_start) {
+ WireguardIPmask *start, *last = NULL;
+ uint16_t j = 0;
+ int r;
+
+ assert(message);
+ assert(peer);
+ assert(index > 0);
+ assert(mask_start);
+
+ /* This returns 1 on success, 0 on recoverable error, and negative errno on failure. */
+
+ start = *mask_start ?: peer->ipmasks;
+
+ r = sd_netlink_message_open_array(message, index);
+ if (r < 0)
+ return 0;
+
+ r = sd_netlink_message_append_data(message, WGPEER_A_PUBLIC_KEY, &peer->public_key, sizeof(peer->public_key));
+ if (r < 0)
+ goto cancel;
+
+ if (!*mask_start) {
+ r = sd_netlink_message_append_data(message, WGPEER_A_PRESHARED_KEY, &peer->preshared_key, WG_KEY_LEN);
+ if (r < 0)
+ goto cancel;
+
+ r = sd_netlink_message_append_u32(message, WGPEER_A_FLAGS, peer->flags);
+ if (r < 0)
+ goto cancel;
+
+ r = sd_netlink_message_append_u16(message, WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, peer->persistent_keepalive_interval);
+ if (r < 0)
+ goto cancel;
+
+ if (IN_SET(peer->endpoint.sa.sa_family, AF_INET, AF_INET6)) {
+ r = netlink_message_append_sockaddr_union(message, WGPEER_A_ENDPOINT, &peer->endpoint);
+ if (r < 0)
+ goto cancel;
+ }
+ }
+
+ r = sd_netlink_message_open_container(message, WGPEER_A_ALLOWEDIPS);
+ if (r < 0)
+ goto cancel;
+
+ LIST_FOREACH(ipmasks, mask, start) {
+ r = wireguard_set_ipmask_one(netdev, message, mask, ++j);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ last = mask;
+ break;
+ }
+ }
+
+ r = sd_netlink_message_close_container(message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not add wireguard allowed ip: %m");
+
+ r = sd_netlink_message_close_container(message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not add wireguard peer: %m");
+
+ *mask_start = last; /* Start next cycle from this mask. */
+ return !last;
+
+cancel:
+ r = sd_netlink_message_cancel_array(message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not cancel wireguard peers: %m");
+
+ return 0;
+}
+
+static int wireguard_set_interface(NetDev *netdev) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL;
+ WireguardIPmask *mask_start = NULL;
+ WireguardPeer *peer_start;
+ bool sent_once = false;
+ uint32_t serial;
+ Wireguard *w;
+ int r;
+
+ assert(netdev);
+ w = WIREGUARD(netdev);
+ assert(w);
+
+ for (peer_start = w->peers; peer_start || !sent_once; ) {
+ uint16_t i = 0;
+
+ message = sd_netlink_message_unref(message);
+
+ r = sd_genl_message_new(netdev->manager->genl, WG_GENL_NAME, WG_CMD_SET_DEVICE, &message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to allocate generic netlink message: %m");
+
+ r = sd_netlink_message_append_string(message, WGDEVICE_A_IFNAME, netdev->ifname);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append wireguard interface name: %m");
+
+ if (peer_start == w->peers) {
+ r = sd_netlink_message_append_data(message, WGDEVICE_A_PRIVATE_KEY, &w->private_key, WG_KEY_LEN);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append wireguard private key: %m");
+
+ r = sd_netlink_message_append_u16(message, WGDEVICE_A_LISTEN_PORT, w->port);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append wireguard port: %m");
+
+ r = sd_netlink_message_append_u32(message, WGDEVICE_A_FWMARK, w->fwmark);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append wireguard fwmark: %m");
+
+ r = sd_netlink_message_append_u32(message, WGDEVICE_A_FLAGS, w->flags);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append wireguard flags: %m");
+ }
+
+ r = sd_netlink_message_open_container(message, WGDEVICE_A_PEERS);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append wireguard peer attributes: %m");
+
+ WireguardPeer *peer_last = NULL;
+ LIST_FOREACH(peers, peer, peer_start) {
+ r = wireguard_set_peer_one(netdev, message, peer, ++i, &mask_start);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ peer_last = peer;
+ break;
+ }
+ }
+ peer_start = peer_last; /* Start next cycle from this peer. */
+
+ r = sd_netlink_message_close_container(message);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not close wireguard container: %m");
+
+ r = sd_netlink_send(netdev->manager->genl, message, &serial);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not set wireguard device: %m");
+
+ sent_once = true;
+ }
+
+ return 0;
+}
+
+static int on_resolve_retry(sd_event_source *s, usec_t usec, void *userdata) {
+ WireguardPeer *peer = ASSERT_PTR(userdata);
+ NetDev *netdev;
+
+ assert(peer->wireguard);
+
+ netdev = NETDEV(peer->wireguard);
+
+ if (!netdev_is_managed(netdev))
+ return 0;
+
+ peer->resolve_query = sd_resolve_query_unref(peer->resolve_query);
+
+ (void) peer_resolve_endpoint(peer);
+ return 0;
+}
+
+static usec_t peer_next_resolve_usec(WireguardPeer *peer) {
+ usec_t usec;
+
+ /* Given the number of retries this function will return an exponential increasing amount of
+ * milliseconds to wait starting at 200ms and capped at 25 seconds. */
+
+ assert(peer);
+
+ usec = (2 << MIN(peer->n_retries, 7U)) * 100 * USEC_PER_MSEC;
+
+ return random_u64_range(usec / 10) + usec * 9 / 10;
+}
+
+static int wireguard_peer_resolve_handler(
+ sd_resolve_query *q,
+ int ret,
+ const struct addrinfo *ai,
+ void *userdata) {
+
+ WireguardPeer *peer = ASSERT_PTR(userdata);
+ NetDev *netdev;
+ int r;
+
+ assert(peer->wireguard);
+
+ netdev = NETDEV(peer->wireguard);
+
+ if (!netdev_is_managed(netdev))
+ return 0;
+
+ if (ret != 0) {
+ log_netdev_warning(netdev, "Failed to resolve host '%s:%s', ignoring: %s",
+ peer->endpoint_host, peer->endpoint_port, gai_strerror(ret));
+ peer->n_retries++;
+
+ } else {
+ bool found = false;
+ for (; ai; ai = ai->ai_next) {
+ if (!IN_SET(ai->ai_family, AF_INET, AF_INET6))
+ continue;
+
+ if (ai->ai_addrlen != (ai->ai_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6)))
+ continue;
+
+ memcpy(&peer->endpoint, ai->ai_addr, ai->ai_addrlen);
+ (void) wireguard_set_interface(netdev);
+ peer->n_retries = 0;
+ found = true;
+ break;
+ }
+
+ if (!found) {
+ log_netdev_warning(netdev, "Neither IPv4 nor IPv6 address found for peer endpoint %s:%s, ignoring the endpoint.",
+ peer->endpoint_host, peer->endpoint_port);
+ peer->n_retries++;
+ }
+ }
+
+ if (peer->n_retries > 0) {
+ r = event_reset_time_relative(netdev->manager->event,
+ &peer->resolve_retry_event_source,
+ CLOCK_BOOTTIME,
+ peer_next_resolve_usec(peer), 0,
+ on_resolve_retry, peer, 0, "wireguard-resolve-retry", true);
+ if (r < 0)
+ log_netdev_warning_errno(netdev, r, "Could not arm resolve retry handler for endpoint %s:%s, ignoring: %m",
+ peer->endpoint_host, peer->endpoint_port);
+ }
+
+ wireguard_resolve_endpoints(netdev);
+ return 0;
+}
+
+static int peer_resolve_endpoint(WireguardPeer *peer) {
+ static const struct addrinfo hints = {
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_DGRAM,
+ .ai_protocol = IPPROTO_UDP
+ };
+ NetDev *netdev;
+ int r;
+
+ assert(peer);
+ assert(peer->wireguard);
+
+ netdev = NETDEV(peer->wireguard);
+
+ if (!peer->endpoint_host || !peer->endpoint_port)
+ /* Not necessary to resolve the endpoint. */
+ return 0;
+
+ if (sd_event_source_get_enabled(peer->resolve_retry_event_source, NULL) > 0)
+ /* Timer event source is enabled. The endpoint will be resolved later. */
+ return 0;
+
+ if (peer->resolve_query)
+ /* Being resolved, or already resolved. */
+ return 0;
+
+ r = sd_resolve_getaddrinfo(netdev->manager->resolve,
+ &peer->resolve_query,
+ peer->endpoint_host,
+ peer->endpoint_port,
+ &hints,
+ wireguard_peer_resolve_handler,
+ peer);
+ if (r < 0)
+ return log_netdev_full_errno(netdev, r == -ENOBUFS ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to create endpoint resolver for %s:%s, ignoring: %m",
+ peer->endpoint_host, peer->endpoint_port);
+
+ return 0;
+}
+
+static void wireguard_resolve_endpoints(NetDev *netdev) {
+ Wireguard *w;
+
+ assert(netdev);
+ w = WIREGUARD(netdev);
+ assert(w);
+
+ LIST_FOREACH(peers, peer, w->peers)
+ if (peer_resolve_endpoint(peer) == -ENOBUFS)
+ /* Too many requests. Let's resolve remaining endpoints later. */
+ break;
+}
+
+static int netdev_wireguard_post_create(NetDev *netdev, Link *link) {
+ assert(netdev);
+ assert(WIREGUARD(netdev));
+
+ (void) wireguard_set_interface(netdev);
+ wireguard_resolve_endpoints(netdev);
+ return 0;
+}
+
+int config_parse_wireguard_listen_port(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint16_t *s = ASSERT_PTR(data);
+ int r;
+
+ assert(rvalue);
+
+ if (isempty(rvalue) || streq(rvalue, "auto")) {
+ *s = 0;
+ return 0;
+ }
+
+ r = parse_ip_port(rvalue, s);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid port specification, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ return 0;
+}
+
+static int wireguard_decode_key_and_warn(
+ const char *rvalue,
+ uint8_t ret[static WG_KEY_LEN],
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *lvalue) {
+
+ _cleanup_(erase_and_freep) void *key = NULL;
+ size_t len;
+ int r;
+
+ assert(rvalue);
+ assert(ret);
+ assert(filename);
+ assert(lvalue);
+
+ if (isempty(rvalue)) {
+ memzero(ret, WG_KEY_LEN);
+ return 0;
+ }
+
+ if (!streq(lvalue, "PublicKey"))
+ (void) warn_file_is_world_accessible(filename, NULL, unit, line);
+
+ r = unbase64mem_full(rvalue, strlen(rvalue), true, &key, &len);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to decode wireguard key provided by %s=, ignoring assignment: %m", lvalue);
+ return 0;
+ }
+ if (len != WG_KEY_LEN) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Wireguard key provided by %s= has invalid length (%zu bytes), ignoring assignment.",
+ lvalue, len);
+ return 0;
+ }
+
+ memcpy(ret, key, WG_KEY_LEN);
+ return 0;
+}
+
+int config_parse_wireguard_private_key(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Wireguard *w;
+
+ assert(data);
+ w = WIREGUARD(data);
+ assert(w);
+
+ return wireguard_decode_key_and_warn(rvalue, w->private_key, unit, filename, line, lvalue);
+}
+
+int config_parse_wireguard_private_key_file(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *path = NULL;
+ Wireguard *w;
+
+ assert(data);
+ w = WIREGUARD(data);
+ assert(w);
+
+ if (isempty(rvalue)) {
+ w->private_key_file = mfree(w->private_key_file);
+ return 0;
+ }
+
+ path = strdup(rvalue);
+ if (!path)
+ return log_oom();
+
+ if (path_simplify_and_warn(path, PATH_CHECK_ABSOLUTE, unit, filename, line, lvalue) < 0)
+ return 0;
+
+ return free_and_replace(w->private_key_file, path);
+}
+
+int config_parse_wireguard_peer_key(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
+ Wireguard *w;
+ int r;
+
+ assert(data);
+ w = WIREGUARD(data);
+ assert(w);
+
+ r = wireguard_peer_new_static(w, filename, section_line, &peer);
+ if (r < 0)
+ return log_oom();
+
+ r = wireguard_decode_key_and_warn(rvalue,
+ streq(lvalue, "PublicKey") ? peer->public_key : peer->preshared_key,
+ unit, filename, line, lvalue);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(peer);
+ return 0;
+}
+
+int config_parse_wireguard_preshared_key_file(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
+ _cleanup_free_ char *path = NULL;
+ Wireguard *w;
+ int r;
+
+ assert(data);
+ w = WIREGUARD(data);
+ assert(w);
+
+ r = wireguard_peer_new_static(w, filename, section_line, &peer);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ peer->preshared_key_file = mfree(peer->preshared_key_file);
+ TAKE_PTR(peer);
+ return 0;
+ }
+
+ path = strdup(rvalue);
+ if (!path)
+ return log_oom();
+
+ if (path_simplify_and_warn(path, PATH_CHECK_ABSOLUTE, unit, filename, line, lvalue) < 0)
+ return 0;
+
+ free_and_replace(peer->preshared_key_file, path);
+ TAKE_PTR(peer);
+ return 0;
+}
+
+int config_parse_wireguard_allowed_ips(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
+ union in_addr_union addr;
+ unsigned char prefixlen;
+ int r, family;
+ Wireguard *w;
+ WireguardIPmask *ipmask;
+
+ assert(rvalue);
+ assert(data);
+
+ w = WIREGUARD(data);
+ assert(w);
+
+ r = wireguard_peer_new_static(w, filename, section_line, &peer);
+ if (r < 0)
+ return log_oom();
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *word = NULL;
+ union in_addr_union masked;
+
+ r = extract_first_word(&p, &word, "," WHITESPACE, 0);
+ if (r == 0)
+ break;
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to split allowed ips \"%s\" option: %m", rvalue);
+ break;
+ }
+
+ r = in_addr_prefix_from_string_auto(word, &family, &addr, &prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Network address is invalid, ignoring assignment: %s", word);
+ continue;
+ }
+
+ masked = addr;
+ assert_se(in_addr_mask(family, &masked, prefixlen) >= 0);
+ if (!in_addr_equal(family, &masked, &addr))
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified address '%s' is not properly masked, assuming '%s'.",
+ word,
+ IN_ADDR_PREFIX_TO_STRING(family, &masked, prefixlen));
+
+ ipmask = new(WireguardIPmask, 1);
+ if (!ipmask)
+ return log_oom();
+
+ *ipmask = (WireguardIPmask) {
+ .family = family,
+ .ip = masked,
+ .cidr = prefixlen,
+ };
+
+ LIST_PREPEND(ipmasks, peer->ipmasks, ipmask);
+ }
+
+ TAKE_PTR(peer);
+ return 0;
+}
+
+int config_parse_wireguard_endpoint(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
+ _cleanup_free_ char *host = NULL;
+ union in_addr_union addr;
+ const char *p;
+ uint16_t port;
+ Wireguard *w;
+ int family, r;
+
+ assert(filename);
+ assert(rvalue);
+ assert(userdata);
+
+ w = WIREGUARD(userdata);
+ assert(w);
+
+ r = wireguard_peer_new_static(w, filename, section_line, &peer);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_port_ifindex_name_from_string_auto(rvalue, &family, &addr, &port, NULL, NULL);
+ if (r >= 0) {
+ if (family == AF_INET)
+ peer->endpoint.in = (struct sockaddr_in) {
+ .sin_family = AF_INET,
+ .sin_addr = addr.in,
+ .sin_port = htobe16(port),
+ };
+ else if (family == AF_INET6)
+ peer->endpoint.in6 = (struct sockaddr_in6) {
+ .sin6_family = AF_INET6,
+ .sin6_addr = addr.in6,
+ .sin6_port = htobe16(port),
+ };
+ else
+ assert_not_reached();
+
+ peer->endpoint_host = mfree(peer->endpoint_host);
+ peer->endpoint_port = mfree(peer->endpoint_port);
+
+ TAKE_PTR(peer);
+ return 0;
+ }
+
+ p = strrchr(rvalue, ':');
+ if (!p) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Unable to find port of endpoint, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ host = strndup(rvalue, p - rvalue);
+ if (!host)
+ return log_oom();
+
+ if (!dns_name_is_valid(host)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid domain name of endpoint, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ p++;
+ r = parse_ip_port(p, &port);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid port of endpoint, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ peer->endpoint = (union sockaddr_union) {};
+
+ free_and_replace(peer->endpoint_host, host);
+
+ r = free_and_strdup(&peer->endpoint_port, p);
+ if (r < 0)
+ return log_oom();
+
+ TAKE_PTR(peer); /* The peer may already have been in the hash map, that is fine too. */
+ return 0;
+}
+
+int config_parse_wireguard_keepalive(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
+ uint16_t keepalive = 0;
+ Wireguard *w;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ w = WIREGUARD(data);
+ assert(w);
+
+ r = wireguard_peer_new_static(w, filename, section_line, &peer);
+ if (r < 0)
+ return log_oom();
+
+ if (streq(rvalue, "off"))
+ keepalive = 0;
+ else {
+ r = safe_atou16(rvalue, &keepalive);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse \"%s\" as keepalive interval (range 0–65535), ignoring assignment: %m",
+ rvalue);
+ return 0;
+ }
+ }
+
+ peer->persistent_keepalive_interval = keepalive;
+
+ TAKE_PTR(peer);
+ return 0;
+}
+
+int config_parse_wireguard_route_table(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ NetDev *netdev = ASSERT_PTR(userdata);
+ uint32_t *table = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue) || parse_boolean(rvalue) == 0) {
+ *table = 0; /* Disabled. */
+ return 0;
+ }
+
+ r = manager_get_route_table_from_string(netdev->manager, rvalue, table);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ return 0;
+}
+
+int config_parse_wireguard_peer_route_table(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
+ NetDev *netdev = ASSERT_PTR(userdata);
+ Wireguard *w;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(netdev->manager);
+
+ w = WIREGUARD(netdev);
+ assert(w);
+
+ r = wireguard_peer_new_static(w, filename, section_line, &peer);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ peer->route_table_set = false; /* Use the table specified in [WireGuard] section. */
+ TAKE_PTR(peer);
+ return 0;
+ }
+
+ if (parse_boolean(rvalue) == 0) {
+ peer->route_table = 0; /* Disabled. */
+ peer->route_table_set = true;
+ TAKE_PTR(peer);
+ return 0;
+ }
+
+ r = manager_get_route_table_from_string(netdev->manager, rvalue, &peer->route_table);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ peer->route_table_set = true;
+ TAKE_PTR(peer);
+ return 0;
+}
+
+int config_parse_wireguard_route_priority(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint32_t *priority = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *priority = 0;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, priority);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ return 0;
+}
+
+int config_parse_wireguard_peer_route_priority(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
+ Wireguard *w;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(userdata);
+
+ w = WIREGUARD(userdata);
+ assert(w);
+
+ r = wireguard_peer_new_static(w, filename, section_line, &peer);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ peer->route_priority_set = false; /* Use the priority specified in [WireGuard] section. */
+ TAKE_PTR(peer);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &peer->route_priority);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ peer->route_priority_set = true;
+ TAKE_PTR(peer);
+ return 0;
+}
+
+static void wireguard_init(NetDev *netdev) {
+ Wireguard *w;
+
+ assert(netdev);
+ w = WIREGUARD(netdev);
+ assert(w);
+
+ w->flags = WGDEVICE_F_REPLACE_PEERS;
+}
+
+static void wireguard_done(NetDev *netdev) {
+ Wireguard *w;
+
+ assert(netdev);
+ w = WIREGUARD(netdev);
+ assert(w);
+
+ explicit_bzero_safe(w->private_key, WG_KEY_LEN);
+ free(w->private_key_file);
+
+ hashmap_free_with_destructor(w->peers_by_section, wireguard_peer_free);
+
+ set_free(w->routes);
+}
+
+static int wireguard_read_key_file(const char *filename, uint8_t dest[static WG_KEY_LEN]) {
+ _cleanup_(erase_and_freep) char *key = NULL;
+ size_t key_len;
+ int r;
+
+ if (!filename)
+ return 0;
+
+ assert(dest);
+
+ r = read_full_file_full(
+ AT_FDCWD, filename, UINT64_MAX, SIZE_MAX,
+ READ_FULL_FILE_SECURE | READ_FULL_FILE_UNBASE64 | READ_FULL_FILE_WARN_WORLD_READABLE | READ_FULL_FILE_CONNECT_SOCKET,
+ NULL, &key, &key_len);
+ if (r < 0)
+ return r;
+
+ if (key_len != WG_KEY_LEN)
+ return -EINVAL;
+
+ memcpy(dest, key, WG_KEY_LEN);
+ return 0;
+}
+
+static int wireguard_peer_verify(WireguardPeer *peer) {
+ NetDev *netdev = NETDEV(peer->wireguard);
+ int r;
+
+ if (section_is_invalid(peer->section))
+ return -EINVAL;
+
+ if (eqzero(peer->public_key))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: WireGuardPeer section without PublicKey= configured. "
+ "Ignoring [WireGuardPeer] section from line %u.",
+ peer->section->filename, peer->section->line);
+
+ r = wireguard_read_key_file(peer->preshared_key_file, peer->preshared_key);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r,
+ "%s: Failed to read preshared key from '%s'. "
+ "Ignoring [WireGuardPeer] section from line %u.",
+ peer->section->filename, peer->preshared_key_file,
+ peer->section->line);
+
+ return 0;
+}
+
+static int wireguard_verify(NetDev *netdev, const char *filename) {
+ Wireguard *w;
+ int r;
+
+ assert(netdev);
+ w = WIREGUARD(netdev);
+ assert(w);
+
+ r = wireguard_read_key_file(w->private_key_file, w->private_key);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r,
+ "Failed to read private key from %s. Ignoring network device.",
+ w->private_key_file);
+
+ if (eqzero(w->private_key))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: Missing PrivateKey= or PrivateKeyFile=, "
+ "Ignoring network device.", filename);
+
+ LIST_FOREACH(peers, peer, w->peers) {
+ if (wireguard_peer_verify(peer) < 0) {
+ wireguard_peer_free(peer);
+ continue;
+ }
+
+ if ((peer->route_table_set ? peer->route_table : w->route_table) == 0)
+ continue;
+
+ LIST_FOREACH(ipmasks, ipmask, peer->ipmasks) {
+ _cleanup_(route_freep) Route *route = NULL;
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->family = ipmask->family;
+ route->dst = ipmask->ip;
+ route->dst_prefixlen = ipmask->cidr;
+ route->scope = RT_SCOPE_UNIVERSE;
+ route->protocol = RTPROT_STATIC;
+ route->table = peer->route_table_set ? peer->route_table : w->route_table;
+ route->priority = peer->route_priority_set ? peer->route_priority : w->route_priority;
+ if (route->priority == 0 && route->family == AF_INET6)
+ route->priority = IP6_RT_PRIO_USER;
+ route->source = NETWORK_CONFIG_SOURCE_STATIC;
+
+ r = set_ensure_consume(&w->routes, &route_hash_ops, TAKE_PTR(route));
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ return 0;
+}
+
+const NetDevVTable wireguard_vtable = {
+ .object_size = sizeof(Wireguard),
+ .sections = NETDEV_COMMON_SECTIONS "WireGuard\0WireGuardPeer\0",
+ .post_create = netdev_wireguard_post_create,
+ .init = wireguard_init,
+ .done = wireguard_done,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = wireguard_verify,
+ .iftype = ARPHRD_NONE,
+};
diff --git a/src/network/netdev/wireguard.h b/src/network/netdev/wireguard.h
new file mode 100644
index 0000000..09dca88
--- /dev/null
+++ b/src/network/netdev/wireguard.h
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#pragma once
+
+typedef struct Wireguard Wireguard;
+
+#include <netinet/in.h>
+#include <linux/wireguard.h>
+
+#include "sd-event.h"
+#include "sd-resolve.h"
+
+#include "in-addr-util.h"
+#include "netdev.h"
+#include "socket-util.h"
+
+typedef struct WireguardIPmask {
+ uint16_t family;
+ union in_addr_union ip;
+ uint8_t cidr;
+
+ LIST_FIELDS(struct WireguardIPmask, ipmasks);
+} WireguardIPmask;
+
+typedef struct WireguardPeer {
+ Wireguard *wireguard;
+ ConfigSection *section;
+
+ uint8_t public_key[WG_KEY_LEN];
+ uint8_t preshared_key[WG_KEY_LEN];
+ char *preshared_key_file;
+ uint32_t flags;
+ uint16_t persistent_keepalive_interval;
+
+ union sockaddr_union endpoint;
+ char *endpoint_host;
+ char *endpoint_port;
+
+ unsigned n_retries;
+ sd_event_source *resolve_retry_event_source;
+ sd_resolve_query *resolve_query;
+
+ uint32_t route_table;
+ uint32_t route_priority;
+ bool route_table_set;
+ bool route_priority_set;
+
+ LIST_HEAD(WireguardIPmask, ipmasks);
+ LIST_FIELDS(struct WireguardPeer, peers);
+} WireguardPeer;
+
+struct Wireguard {
+ NetDev meta;
+ unsigned last_peer_section;
+
+ uint32_t flags;
+ uint8_t private_key[WG_KEY_LEN];
+ char *private_key_file;
+ uint16_t port;
+ uint32_t fwmark;
+
+ Hashmap *peers_by_section;
+ LIST_HEAD(WireguardPeer, peers);
+
+ Set *routes;
+ uint32_t route_table;
+ uint32_t route_priority;
+};
+
+DEFINE_NETDEV_CAST(WIREGUARD, Wireguard);
+extern const NetDevVTable wireguard_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_allowed_ips);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_endpoint);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_listen_port);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_peer_key);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_private_key);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_private_key_file);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_preshared_key_file);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_keepalive);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_route_table);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_peer_route_table);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_route_priority);
+CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_peer_route_priority);
diff --git a/src/network/netdev/wlan.c b/src/network/netdev/wlan.c
new file mode 100644
index 0000000..816e106
--- /dev/null
+++ b/src/network/netdev/wlan.c
@@ -0,0 +1,258 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if_arp.h>
+
+#include "sd-netlink.h"
+
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "networkd-wiphy.h"
+#include "parse-util.h"
+#include "wifi-util.h"
+#include "wlan.h"
+
+static void wlan_done(NetDev *netdev) {
+ WLan *w;
+
+ assert(netdev);
+
+ w = WLAN(netdev);
+
+ assert(w);
+
+ w->wiphy_name = mfree(w->wiphy_name);
+}
+
+static void wlan_init(NetDev *netdev) {
+ WLan *w;
+
+ assert(netdev);
+
+ w = WLAN(netdev);
+
+ assert(w);
+
+ w->wiphy_index = UINT32_MAX;
+ w->wds = -1;
+}
+
+static int wlan_get_wiphy(NetDev *netdev, Wiphy **ret) {
+ WLan *w;
+
+ assert(netdev);
+
+ w = WLAN(netdev);
+
+ assert(w);
+
+ if (w->wiphy_name)
+ return wiphy_get_by_name(netdev->manager, w->wiphy_name, ret);
+
+ return wiphy_get_by_index(netdev->manager, w->wiphy_index, ret);
+}
+
+static int wlan_is_ready_to_create(NetDev *netdev, Link *link) {
+ return wlan_get_wiphy(netdev, NULL) >= 0;
+}
+
+static int wlan_fill_message(NetDev *netdev, sd_netlink_message *m) {
+ Wiphy *wiphy;
+ WLan *w;
+ int r;
+
+ assert(netdev);
+ assert(m);
+
+ w = WLAN(netdev);
+
+ assert(w);
+
+ r = wlan_get_wiphy(netdev, &wiphy);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, NL80211_ATTR_WIPHY, wiphy->index);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_string(m, NL80211_ATTR_IFNAME, netdev->ifname);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, NL80211_ATTR_IFTYPE, w->iftype);
+ if (r < 0)
+ return r;
+
+ if (!hw_addr_is_null(&netdev->hw_addr) && netdev->hw_addr.length == ETH_ALEN) {
+ r = sd_netlink_message_append_ether_addr(m, NL80211_ATTR_MAC, &netdev->hw_addr.ether);
+ if (r < 0)
+ return r;
+ }
+
+ if (w->wds >= 0) {
+ r = sd_netlink_message_append_u8(m, NL80211_ATTR_4ADDR, w->wds);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int wlan_create_handler(sd_netlink *genl, sd_netlink_message *m, NetDev *netdev) {
+ int r;
+
+ assert(netdev);
+ assert(netdev->state != _NETDEV_STATE_INVALID);
+
+ r = sd_netlink_message_get_errno(m);
+ if (IN_SET(r, -EEXIST, -ENFILE))
+ /* Unlike the other netdevs, the kernel may return -ENFILE. See dev_alloc_name(). */
+ log_netdev_info(netdev, "WLAN interface exists, using existing without changing its parameters.");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "WLAN interface could not be created: %m");
+ netdev_enter_failed(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "WLAN interface is created.");
+ return 1;
+}
+
+static int wlan_create(NetDev *netdev) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(netdev);
+ assert(netdev->manager);
+ assert(netdev->manager->genl);
+
+ r = sd_genl_message_new(netdev->manager->genl, NL80211_GENL_NAME, NL80211_CMD_NEW_INTERFACE, &m);
+ if (r < 0)
+ return log_netdev_warning_errno(netdev, r, "Failed to allocate netlink message: %m");
+
+ r = wlan_fill_message(netdev, m);
+ if (r < 0)
+ return log_netdev_warning_errno(netdev, r, "Failed to fill netlink message: %m");
+
+ r = netlink_call_async(netdev->manager->genl, NULL, m, wlan_create_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_warning_errno(netdev, r, "Failed to send netlink message: %m");
+
+ netdev_ref(netdev);
+ return 0;
+}
+
+static int wlan_verify(NetDev *netdev, const char *filename) {
+ WLan *w;
+
+ assert(netdev);
+ assert(filename);
+
+ w = WLAN(netdev);
+
+ assert(w);
+
+ if (w->iftype == NL80211_IFTYPE_UNSPECIFIED)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: WLAN interface type is not specified, ignoring.",
+ filename);
+
+ if (w->wiphy_index == UINT32_MAX && isempty(w->wiphy_name))
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: physical WLAN device is not specified, ignoring.",
+ filename);
+
+ return 0;
+}
+
+int config_parse_wiphy(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ WLan *w = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ w->wiphy_name = mfree(w->wiphy_name);
+ w->wiphy_index = UINT32_MAX;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &w->wiphy_index);
+ if (r >= 0) {
+ w->wiphy_name = mfree(w->wiphy_name);
+ return 0;
+ }
+
+ r = free_and_strdup_warn(&w->wiphy_name, rvalue);
+ if (r < 0)
+ return r;
+
+ w->wiphy_index = UINT32_MAX;
+ return 0;
+}
+
+int config_parse_wlan_iftype(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ enum nl80211_iftype t, *iftype = ASSERT_PTR(data);
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *iftype = NL80211_IFTYPE_UNSPECIFIED;
+ return 0;
+ }
+
+ t = nl80211_iftype_from_string(rvalue);
+ /* We reuse the kernel provided enum which does not contain negative value. So, the cast
+ * below is mandatory. Otherwise, the check below always passes. */
+ if ((int) t < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, t,
+ "Failed to parse wlan interface type, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ *iftype = t;
+ return 0;
+}
+
+const NetDevVTable wlan_vtable = {
+ .object_size = sizeof(WLan),
+ .init = wlan_init,
+ .done = wlan_done,
+ .sections = NETDEV_COMMON_SECTIONS "WLAN\0",
+ .is_ready_to_create = wlan_is_ready_to_create,
+ .create = wlan_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = wlan_verify,
+ .iftype = ARPHRD_ETHER,
+ .generate_mac = true,
+ .skip_netdev_kind_check = true,
+};
diff --git a/src/network/netdev/wlan.h b/src/network/netdev/wlan.h
new file mode 100644
index 0000000..bcc2dbc
--- /dev/null
+++ b/src/network/netdev/wlan.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <linux/nl80211.h>
+
+#include "conf-parser.h"
+#include "netdev.h"
+
+typedef struct WLan {
+ NetDev meta;
+
+ char *wiphy_name;
+ uint32_t wiphy_index;
+ enum nl80211_iftype iftype;
+ int wds; /* tristate */
+} WLan;
+
+DEFINE_NETDEV_CAST(WLAN, WLan);
+extern const NetDevVTable wlan_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_wiphy);
+CONFIG_PARSER_PROTOTYPE(config_parse_wlan_iftype);
diff --git a/src/network/netdev/xfrm.c b/src/network/netdev/xfrm.c
new file mode 100644
index 0000000..a961d8f
--- /dev/null
+++ b/src/network/netdev/xfrm.c
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/if_arp.h>
+
+#include "missing_network.h"
+#include "xfrm.h"
+
+static int xfrm_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *message) {
+ Xfrm *x;
+ int r;
+
+ assert(netdev);
+ assert(message);
+
+ x = XFRM(netdev);
+
+ assert(x);
+ assert(link || x->independent);
+
+ r = sd_netlink_message_append_u32(message, IFLA_XFRM_LINK, link ? link->ifindex : LOOPBACK_IFINDEX);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(message, IFLA_XFRM_IF_ID, x->if_id);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int xfrm_verify(NetDev *netdev, const char *filename) {
+ Xfrm *x;
+
+ assert(netdev);
+ assert(filename);
+
+ x = XFRM(netdev);
+
+ assert(x);
+
+ if (x->if_id == 0)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: Xfrm interface ID cannot be zero.", filename);
+
+ return 0;
+}
+
+const NetDevVTable xfrm_vtable = {
+ .object_size = sizeof(Xfrm),
+ .sections = NETDEV_COMMON_SECTIONS "Xfrm\0",
+ .fill_message_create = xfrm_fill_message_create,
+ .config_verify = xfrm_verify,
+ .create_type = NETDEV_CREATE_STACKED,
+ .iftype = ARPHRD_NONE,
+};
diff --git a/src/network/netdev/xfrm.h b/src/network/netdev/xfrm.h
new file mode 100644
index 0000000..f56c4f2
--- /dev/null
+++ b/src/network/netdev/xfrm.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "netdev.h"
+
+typedef struct Xfrm {
+ NetDev meta;
+
+ uint32_t if_id;
+ bool independent;
+} Xfrm;
+
+DEFINE_NETDEV_CAST(XFRM, Xfrm);
+extern const NetDevVTable xfrm_vtable;
diff --git a/src/network/networkctl.c b/src/network/networkctl.c
new file mode 100644
index 0000000..f593aa1
--- /dev/null
+++ b/src/network/networkctl.c
@@ -0,0 +1,2965 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <arpa/inet.h>
+#include <getopt.h>
+#include <linux/if_addrlabel.h>
+#include <net/if.h>
+#include <stdbool.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <linux/if_bridge.h>
+#include <linux/if_tunnel.h>
+
+#include "sd-bus.h"
+#include "sd-device.h"
+#include "sd-dhcp-client.h"
+#include "sd-hwdb.h"
+#include "sd-lldp-rx.h"
+#include "sd-netlink.h"
+#include "sd-network.h"
+
+#include "alloc-util.h"
+#include "bond-util.h"
+#include "bridge-util.h"
+#include "bus-common-errors.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "device-util.h"
+#include "escape.h"
+#include "ether-addr-util.h"
+#include "ethtool-util.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "geneve-util.h"
+#include "glob-util.h"
+#include "hwdb-util.h"
+#include "ipvlan-util.h"
+#include "local-addresses.h"
+#include "locale-util.h"
+#include "logs-show.h"
+#include "macro.h"
+#include "macvlan-util.h"
+#include "main-func.h"
+#include "netif-util.h"
+#include "netlink-util.h"
+#include "network-internal.h"
+#include "network-util.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "set.h"
+#include "sigbus.h"
+#include "socket-netlink.h"
+#include "socket-util.h"
+#include "sort-util.h"
+#include "sparse-endian.h"
+#include "stdio-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "terminal-util.h"
+#include "unit-def.h"
+#include "verbs.h"
+#include "wifi-util.h"
+
+/* Kernel defines MODULE_NAME_LEN as 64 - sizeof(unsigned long). So, 64 is enough. */
+#define NETDEV_KIND_MAX 64
+
+/* use 128 kB for receive socket kernel queue, we shouldn't need more here */
+#define RCVBUF_SIZE (128*1024)
+
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+static bool arg_all = false;
+static bool arg_stats = false;
+static bool arg_full = false;
+static unsigned arg_lines = 10;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+
+static int get_description(sd_bus *bus, JsonVariant **ret) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ const char *text = NULL;
+ int r;
+
+ r = bus_call_method(bus, bus_network_mgr, "Describe", &error, &reply, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get description: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "s", &text);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = json_parse(text, 0, ret, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON: %m");
+
+ return 0;
+}
+
+static int dump_manager_description(sd_bus *bus) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ int r;
+
+ r = get_description(bus, &v);
+ if (r < 0)
+ return r;
+
+ json_variant_dump(v, arg_json_format_flags, NULL, NULL);
+ return 0;
+}
+
+static int dump_link_description(sd_bus *bus, char **patterns) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ bool *matched_patterns = NULL;
+ JsonVariant *i;
+ size_t c = 0;
+ int r;
+
+ r = get_description(bus, &v);
+ if (r < 0)
+ return r;
+
+ matched_patterns = new0(bool, strv_length(patterns));
+ if (!matched_patterns)
+ return log_oom();
+
+ JSON_VARIANT_ARRAY_FOREACH(i, json_variant_by_key(v, "Interfaces")) {
+ char ifindex_str[DECIMAL_STR_MAX(int64_t)];
+ const char *name;
+ int64_t index;
+ size_t pos;
+
+ name = json_variant_string(json_variant_by_key(i, "Name"));
+ index = json_variant_integer(json_variant_by_key(i, "Index"));
+ xsprintf(ifindex_str, "%" PRIi64, index);
+
+ if (!strv_fnmatch_full(patterns, ifindex_str, 0, &pos) &&
+ !strv_fnmatch_full(patterns, name, 0, &pos)) {
+ bool match = false;
+ JsonVariant *a;
+
+ JSON_VARIANT_ARRAY_FOREACH(a, json_variant_by_key(i, "AlternativeNames"))
+ if (strv_fnmatch_full(patterns, json_variant_string(a), 0, &pos)) {
+ match = true;
+ break;
+ }
+
+ if (!match)
+ continue;
+ }
+
+ matched_patterns[pos] = true;
+ json_variant_dump(i, arg_json_format_flags, NULL, NULL);
+ c++;
+ }
+
+ /* Look if we matched all our arguments that are not globs. It is OK for a glob to match
+ * nothing, but not for an exact argument. */
+ for (size_t pos = 0; pos < strv_length(patterns); pos++) {
+ if (matched_patterns[pos])
+ continue;
+
+ if (string_is_glob(patterns[pos]))
+ log_debug("Pattern \"%s\" doesn't match any interface, ignoring.",
+ patterns[pos]);
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(ENODEV),
+ "Interface \"%s\" not found.", patterns[pos]);
+ }
+
+ if (c == 0)
+ log_warning("No interfaces matched.");
+
+ return 0;
+}
+
+static void operational_state_to_color(const char *name, const char *state, const char **on, const char **off) {
+ if (STRPTR_IN_SET(state, "routable", "enslaved") ||
+ (streq_ptr(name, "lo") && streq_ptr(state, "carrier"))) {
+ if (on)
+ *on = ansi_highlight_green();
+ if (off)
+ *off = ansi_normal();
+ } else if (streq_ptr(state, "degraded")) {
+ if (on)
+ *on = ansi_highlight_yellow();
+ if (off)
+ *off = ansi_normal();
+ } else {
+ if (on)
+ *on = "";
+ if (off)
+ *off = "";
+ }
+}
+
+static void setup_state_to_color(const char *state, const char **on, const char **off) {
+ if (streq_ptr(state, "configured")) {
+ if (on)
+ *on = ansi_highlight_green();
+ if (off)
+ *off = ansi_normal();
+ } else if (streq_ptr(state, "configuring")) {
+ if (on)
+ *on = ansi_highlight_yellow();
+ if (off)
+ *off = ansi_normal();
+ } else if (STRPTR_IN_SET(state, "failed", "linger")) {
+ if (on)
+ *on = ansi_highlight_red();
+ if (off)
+ *off = ansi_normal();
+ } else {
+ if (on)
+ *on = "";
+ if (off)
+ *off = "";
+ }
+}
+
+static void online_state_to_color(const char *state, const char **on, const char **off) {
+ if (streq_ptr(state, "online")) {
+ if (on)
+ *on = ansi_highlight_green();
+ if (off)
+ *off = ansi_normal();
+ } else if (streq_ptr(state, "partial")) {
+ if (on)
+ *on = ansi_highlight_yellow();
+ if (off)
+ *off = ansi_normal();
+ } else {
+ if (on)
+ *on = "";
+ if (off)
+ *off = "";
+ }
+}
+
+typedef struct VxLanInfo {
+ uint32_t vni;
+ uint32_t link;
+
+ int local_family;
+ int group_family;
+
+ union in_addr_union local;
+ union in_addr_union group;
+
+ uint16_t dest_port;
+
+ uint8_t proxy;
+ uint8_t learning;
+ uint8_t rsc;
+ uint8_t l2miss;
+ uint8_t l3miss;
+ uint8_t tos;
+ uint8_t ttl;
+} VxLanInfo;
+
+typedef struct LinkInfo {
+ char name[IFNAMSIZ+1];
+ char *netdev_kind;
+ sd_device *sd_device;
+ int ifindex;
+ unsigned short iftype;
+ struct hw_addr_data hw_address;
+ struct hw_addr_data permanent_hw_address;
+ uint32_t master;
+ uint32_t mtu;
+ uint32_t min_mtu;
+ uint32_t max_mtu;
+ uint32_t tx_queues;
+ uint32_t rx_queues;
+ uint8_t addr_gen_mode;
+ char *qdisc;
+ char **alternative_names;
+
+ union {
+ struct rtnl_link_stats64 stats64;
+ struct rtnl_link_stats stats;
+ };
+
+ uint64_t tx_bitrate;
+ uint64_t rx_bitrate;
+
+ /* bridge info */
+ uint32_t forward_delay;
+ uint32_t hello_time;
+ uint32_t max_age;
+ uint32_t ageing_time;
+ uint32_t stp_state;
+ uint32_t cost;
+ uint16_t priority;
+ uint8_t mcast_igmp_version;
+ uint8_t port_state;
+
+ /* vxlan info */
+ VxLanInfo vxlan_info;
+
+ /* vlan info */
+ uint16_t vlan_id;
+
+ /* tunnel info */
+ uint8_t ttl;
+ uint8_t tos;
+ uint8_t inherit;
+ uint8_t df;
+ uint8_t csum;
+ uint8_t csum6_tx;
+ uint8_t csum6_rx;
+ uint16_t tunnel_port;
+ uint32_t vni;
+ uint32_t label;
+ union in_addr_union local;
+ union in_addr_union remote;
+
+ /* bonding info */
+ uint8_t mode;
+ uint32_t miimon;
+ uint32_t updelay;
+ uint32_t downdelay;
+
+ /* macvlan and macvtap info */
+ uint32_t macvlan_mode;
+
+ /* ipvlan info */
+ uint16_t ipvlan_mode;
+ uint16_t ipvlan_flags;
+
+ /* ethtool info */
+ int autonegotiation;
+ uint64_t speed;
+ Duplex duplex;
+ NetDevPort port;
+
+ /* wlan info */
+ enum nl80211_iftype wlan_iftype;
+ char *ssid;
+ struct ether_addr bssid;
+
+ bool has_hw_address:1;
+ bool has_permanent_hw_address:1;
+ bool has_tx_queues:1;
+ bool has_rx_queues:1;
+ bool has_stats64:1;
+ bool has_stats:1;
+ bool has_bitrates:1;
+ bool has_ethtool_link_info:1;
+ bool has_wlan_link_info:1;
+ bool has_tunnel_ipv4:1;
+ bool has_ipv6_address_generation_mode:1;
+
+ bool needs_freeing:1;
+} LinkInfo;
+
+static int link_info_compare(const LinkInfo *a, const LinkInfo *b) {
+ return CMP(a->ifindex, b->ifindex);
+}
+
+static LinkInfo* link_info_array_free(LinkInfo *array) {
+ for (unsigned i = 0; array && array[i].needs_freeing; i++) {
+ sd_device_unref(array[i].sd_device);
+ free(array[i].netdev_kind);
+ free(array[i].ssid);
+ free(array[i].qdisc);
+ strv_free(array[i].alternative_names);
+ }
+
+ return mfree(array);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_array_free);
+
+static int decode_netdev(sd_netlink_message *m, LinkInfo *info) {
+ int r;
+
+ assert(m);
+ assert(info);
+
+ r = sd_netlink_message_enter_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_read_string_strdup(m, IFLA_INFO_KIND, &info->netdev_kind);
+ if (r < 0) {
+ (void) sd_netlink_message_exit_container(m);
+ return r;
+ }
+
+ r = sd_netlink_message_enter_container(m, IFLA_INFO_DATA);
+ if (r < 0)
+ return r;
+
+ if (streq(info->netdev_kind, "bridge")) {
+ (void) sd_netlink_message_read_u32(m, IFLA_BR_FORWARD_DELAY, &info->forward_delay);
+ (void) sd_netlink_message_read_u32(m, IFLA_BR_HELLO_TIME, &info->hello_time);
+ (void) sd_netlink_message_read_u32(m, IFLA_BR_MAX_AGE, &info->max_age);
+ (void) sd_netlink_message_read_u32(m, IFLA_BR_AGEING_TIME, &info->ageing_time);
+ (void) sd_netlink_message_read_u32(m, IFLA_BR_STP_STATE, &info->stp_state);
+ (void) sd_netlink_message_read_u32(m, IFLA_BRPORT_COST, &info->cost);
+ (void) sd_netlink_message_read_u16(m, IFLA_BR_PRIORITY, &info->priority);
+ (void) sd_netlink_message_read_u8(m, IFLA_BR_MCAST_IGMP_VERSION, &info->mcast_igmp_version);
+ (void) sd_netlink_message_read_u8(m, IFLA_BRPORT_STATE, &info->port_state);
+ } if (streq(info->netdev_kind, "bond")) {
+ (void) sd_netlink_message_read_u8(m, IFLA_BOND_MODE, &info->mode);
+ (void) sd_netlink_message_read_u32(m, IFLA_BOND_MIIMON, &info->miimon);
+ (void) sd_netlink_message_read_u32(m, IFLA_BOND_DOWNDELAY, &info->downdelay);
+ (void) sd_netlink_message_read_u32(m, IFLA_BOND_UPDELAY, &info->updelay);
+ } else if (streq(info->netdev_kind, "vxlan")) {
+ (void) sd_netlink_message_read_u32(m, IFLA_VXLAN_ID, &info->vxlan_info.vni);
+
+ r = sd_netlink_message_read_in_addr(m, IFLA_VXLAN_GROUP, &info->vxlan_info.group.in);
+ if (r >= 0)
+ info->vxlan_info.group_family = AF_INET;
+ else {
+ r = sd_netlink_message_read_in6_addr(m, IFLA_VXLAN_GROUP6, &info->vxlan_info.group.in6);
+ if (r >= 0)
+ info->vxlan_info.group_family = AF_INET6;
+ }
+
+ r = sd_netlink_message_read_in_addr(m, IFLA_VXLAN_LOCAL, &info->vxlan_info.local.in);
+ if (r >= 0)
+ info->vxlan_info.local_family = AF_INET;
+ else {
+ r = sd_netlink_message_read_in6_addr(m, IFLA_VXLAN_LOCAL6, &info->vxlan_info.local.in6);
+ if (r >= 0)
+ info->vxlan_info.local_family = AF_INET6;
+ }
+
+ (void) sd_netlink_message_read_u32(m, IFLA_VXLAN_LINK, &info->vxlan_info.link);
+ (void) sd_netlink_message_read_u16(m, IFLA_VXLAN_PORT, &info->vxlan_info.dest_port);
+ (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_PROXY, &info->vxlan_info.proxy);
+ (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_LEARNING, &info->vxlan_info.learning);
+ (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_RSC, &info->vxlan_info.rsc);
+ (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_L3MISS, &info->vxlan_info.l3miss);
+ (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_L2MISS, &info->vxlan_info.l2miss);
+ (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_TOS, &info->vxlan_info.tos);
+ (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_TTL, &info->vxlan_info.ttl);
+ } else if (streq(info->netdev_kind, "vlan"))
+ (void) sd_netlink_message_read_u16(m, IFLA_VLAN_ID, &info->vlan_id);
+ else if (STR_IN_SET(info->netdev_kind, "ipip", "sit")) {
+ (void) sd_netlink_message_read_in_addr(m, IFLA_IPTUN_LOCAL, &info->local.in);
+ (void) sd_netlink_message_read_in_addr(m, IFLA_IPTUN_REMOTE, &info->remote.in);
+ } else if (streq(info->netdev_kind, "geneve")) {
+ (void) sd_netlink_message_read_u32(m, IFLA_GENEVE_ID, &info->vni);
+
+ r = sd_netlink_message_read_in_addr(m, IFLA_GENEVE_REMOTE, &info->remote.in);
+ if (r >= 0)
+ info->has_tunnel_ipv4 = true;
+ else
+ (void) sd_netlink_message_read_in6_addr(m, IFLA_GENEVE_REMOTE6, &info->remote.in6);
+
+ (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_TTL, &info->ttl);
+ (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_TTL_INHERIT, &info->inherit);
+ (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_TOS, &info->tos);
+ (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_DF, &info->df);
+ (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_UDP_CSUM, &info->csum);
+ (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_TX, &info->csum6_tx);
+ (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_RX, &info->csum6_rx);
+ (void) sd_netlink_message_read_u16(m, IFLA_GENEVE_PORT, &info->tunnel_port);
+ (void) sd_netlink_message_read_u32(m, IFLA_GENEVE_LABEL, &info->label);
+ } else if (STR_IN_SET(info->netdev_kind, "gre", "gretap", "erspan")) {
+ (void) sd_netlink_message_read_in_addr(m, IFLA_GRE_LOCAL, &info->local.in);
+ (void) sd_netlink_message_read_in_addr(m, IFLA_GRE_REMOTE, &info->remote.in);
+ } else if (STR_IN_SET(info->netdev_kind, "ip6gre", "ip6gretap", "ip6erspan")) {
+ (void) sd_netlink_message_read_in6_addr(m, IFLA_GRE_LOCAL, &info->local.in6);
+ (void) sd_netlink_message_read_in6_addr(m, IFLA_GRE_REMOTE, &info->remote.in6);
+ } else if (streq(info->netdev_kind, "vti")) {
+ (void) sd_netlink_message_read_in_addr(m, IFLA_VTI_LOCAL, &info->local.in);
+ (void) sd_netlink_message_read_in_addr(m, IFLA_VTI_REMOTE, &info->remote.in);
+ } else if (streq(info->netdev_kind, "vti6")) {
+ (void) sd_netlink_message_read_in6_addr(m, IFLA_VTI_LOCAL, &info->local.in6);
+ (void) sd_netlink_message_read_in6_addr(m, IFLA_VTI_REMOTE, &info->remote.in6);
+ } else if (STR_IN_SET(info->netdev_kind, "macvlan", "macvtap"))
+ (void) sd_netlink_message_read_u32(m, IFLA_MACVLAN_MODE, &info->macvlan_mode);
+ else if (streq(info->netdev_kind, "ipvlan")) {
+ (void) sd_netlink_message_read_u16(m, IFLA_IPVLAN_MODE, &info->ipvlan_mode);
+ (void) sd_netlink_message_read_u16(m, IFLA_IPVLAN_FLAGS, &info->ipvlan_flags);
+ }
+
+ (void) sd_netlink_message_exit_container(m);
+ (void) sd_netlink_message_exit_container(m);
+
+ return 0;
+}
+
+static int decode_link(sd_netlink_message *m, LinkInfo *info, char **patterns, bool matched_patterns[]) {
+ _cleanup_strv_free_ char **altnames = NULL;
+ const char *name, *qdisc;
+ int ifindex, r;
+ uint16_t type;
+
+ assert(m);
+ assert(info);
+
+ r = sd_netlink_message_get_type(m, &type);
+ if (r < 0)
+ return r;
+
+ if (type != RTM_NEWLINK)
+ return 0;
+
+ r = sd_rtnl_message_link_get_ifindex(m, &ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_read_string(m, IFLA_IFNAME, &name);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_read_strv(m, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &altnames);
+ if (r < 0 && r != -ENODATA)
+ return r;
+
+ if (patterns) {
+ char str[DECIMAL_STR_MAX(int)];
+ size_t pos;
+
+ assert(matched_patterns);
+
+ xsprintf(str, "%i", ifindex);
+ if (!strv_fnmatch_full(patterns, str, 0, &pos) &&
+ !strv_fnmatch_full(patterns, name, 0, &pos)) {
+ bool match = false;
+
+ STRV_FOREACH(p, altnames)
+ if (strv_fnmatch_full(patterns, *p, 0, &pos)) {
+ match = true;
+ break;
+ }
+ if (!match)
+ return 0;
+ }
+
+ matched_patterns[pos] = true;
+ }
+
+ r = sd_rtnl_message_link_get_type(m, &info->iftype);
+ if (r < 0)
+ return r;
+
+ strscpy(info->name, sizeof info->name, name);
+ info->ifindex = ifindex;
+ info->alternative_names = TAKE_PTR(altnames);
+
+ info->has_hw_address =
+ netlink_message_read_hw_addr(m, IFLA_ADDRESS, &info->hw_address) >= 0 &&
+ info->hw_address.length > 0;
+
+ info->has_permanent_hw_address =
+ (netlink_message_read_hw_addr(m, IFLA_PERM_ADDRESS, &info->permanent_hw_address) >= 0 ||
+ ethtool_get_permanent_hw_addr(NULL, info->name, &info->permanent_hw_address) >= 0) &&
+ !hw_addr_is_null(&info->permanent_hw_address) &&
+ !hw_addr_equal(&info->permanent_hw_address, &info->hw_address);
+
+ (void) sd_netlink_message_read_u32(m, IFLA_MTU, &info->mtu);
+ (void) sd_netlink_message_read_u32(m, IFLA_MIN_MTU, &info->min_mtu);
+ (void) sd_netlink_message_read_u32(m, IFLA_MAX_MTU, &info->max_mtu);
+
+ info->has_rx_queues =
+ sd_netlink_message_read_u32(m, IFLA_NUM_RX_QUEUES, &info->rx_queues) >= 0 &&
+ info->rx_queues > 0;
+
+ info->has_tx_queues =
+ sd_netlink_message_read_u32(m, IFLA_NUM_TX_QUEUES, &info->tx_queues) >= 0 &&
+ info->tx_queues > 0;
+
+ if (sd_netlink_message_read(m, IFLA_STATS64, sizeof info->stats64, &info->stats64) >= 0)
+ info->has_stats64 = true;
+ else if (sd_netlink_message_read(m, IFLA_STATS, sizeof info->stats, &info->stats) >= 0)
+ info->has_stats = true;
+
+ r = sd_netlink_message_read_string(m, IFLA_QDISC, &qdisc);
+ if (r >= 0) {
+ info->qdisc = strdup(qdisc);
+ if (!info->qdisc)
+ return log_oom();
+ }
+
+ (void) sd_netlink_message_read_u32(m, IFLA_MASTER, &info->master);
+
+ r = sd_netlink_message_enter_container(m, IFLA_AF_SPEC);
+ if (r >= 0) {
+ r = sd_netlink_message_enter_container(m, AF_INET6);
+ if (r >= 0) {
+ r = sd_netlink_message_read_u8(m, IFLA_INET6_ADDR_GEN_MODE, &info->addr_gen_mode);
+ if (r >= 0 && IN_SET(info->addr_gen_mode,
+ IN6_ADDR_GEN_MODE_EUI64,
+ IN6_ADDR_GEN_MODE_NONE,
+ IN6_ADDR_GEN_MODE_STABLE_PRIVACY,
+ IN6_ADDR_GEN_MODE_RANDOM))
+ info->has_ipv6_address_generation_mode = true;
+
+ (void) sd_netlink_message_exit_container(m);
+ }
+ (void) sd_netlink_message_exit_container(m);
+ }
+
+ /* fill kind info */
+ (void) decode_netdev(m, info);
+
+ return 1;
+}
+
+static int link_get_property(
+ sd_bus *bus,
+ const LinkInfo *link,
+ sd_bus_error *error,
+ sd_bus_message **reply,
+ const char *iface,
+ const char *propname) {
+
+ char ifindex_str[DECIMAL_STR_MAX(int)];
+ _cleanup_free_ char *path = NULL;
+ int r;
+
+ xsprintf(ifindex_str, "%i", link->ifindex);
+
+ r = sd_bus_path_encode("/org/freedesktop/network1/link", ifindex_str, &path);
+ if (r < 0)
+ return r;
+
+ return sd_bus_call_method(
+ bus,
+ "org.freedesktop.network1",
+ path,
+ "org.freedesktop.DBus.Properties",
+ "Get",
+ error,
+ reply,
+ "ss",
+ iface,
+ propname);
+}
+
+static int acquire_link_bitrates(sd_bus *bus, LinkInfo *link) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ r = link_get_property(bus, link, &error, &reply, "org.freedesktop.network1.Link", "BitRates");
+ if (r < 0) {
+ bool quiet = sd_bus_error_has_names(&error, SD_BUS_ERROR_UNKNOWN_PROPERTY,
+ BUS_ERROR_SPEED_METER_INACTIVE);
+
+ return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING,
+ r, "Failed to query link bit rates: %s", bus_error_message(&error, r));
+ }
+
+ r = sd_bus_message_enter_container(reply, 'v', "(tt)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read(reply, "(tt)", &link->tx_bitrate, &link->rx_bitrate);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ link->has_bitrates = link->tx_bitrate != UINT64_MAX && link->rx_bitrate != UINT64_MAX;
+
+ return 0;
+}
+
+static void acquire_ether_link_info(int *fd, LinkInfo *link) {
+ if (ethtool_get_link_info(fd, link->name,
+ &link->autonegotiation,
+ &link->speed,
+ &link->duplex,
+ &link->port) >= 0)
+ link->has_ethtool_link_info = true;
+}
+
+static void acquire_wlan_link_info(LinkInfo *link) {
+ _cleanup_(sd_netlink_unrefp) sd_netlink *genl = NULL;
+ const char *type = NULL;
+ int r, k = 0;
+
+ if (link->sd_device)
+ (void) sd_device_get_devtype(link->sd_device, &type);
+ if (!streq_ptr(type, "wlan"))
+ return;
+
+ r = sd_genl_socket_open(&genl);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to open generic netlink socket: %m");
+ return;
+ }
+
+ (void) sd_netlink_increase_rxbuf(genl, RCVBUF_SIZE);
+
+ r = wifi_get_interface(genl, link->ifindex, &link->wlan_iftype, &link->ssid);
+ if (r < 0)
+ log_debug_errno(r, "%s: failed to query ssid: %m", link->name);
+
+ if (link->wlan_iftype == NL80211_IFTYPE_STATION) {
+ k = wifi_get_station(genl, link->ifindex, &link->bssid);
+ if (k < 0)
+ log_debug_errno(k, "%s: failed to query bssid: %m", link->name);
+ }
+
+ link->has_wlan_link_info = r > 0 || k > 0;
+}
+
+static int acquire_link_info(sd_bus *bus, sd_netlink *rtnl, char **patterns, LinkInfo **ret) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL;
+ _cleanup_(link_info_array_freep) LinkInfo *links = NULL;
+ _cleanup_close_ int fd = -1;
+ size_t c = 0;
+ int r;
+
+ assert(rtnl);
+ assert(ret);
+
+ r = sd_rtnl_message_new_link(rtnl, &req, RTM_GETLINK, 0);
+ if (r < 0)
+ return rtnl_log_create_error(r);
+
+ r = sd_netlink_message_set_request_dump(req, true);
+ if (r < 0)
+ return rtnl_log_create_error(r);
+
+ r = sd_netlink_call(rtnl, req, 0, &reply);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate links: %m");
+
+ _cleanup_free_ bool *matched_patterns = NULL;
+ if (patterns) {
+ matched_patterns = new0(bool, strv_length(patterns));
+ if (!matched_patterns)
+ return log_oom();
+ }
+
+ for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) {
+ if (!GREEDY_REALLOC0(links, c + 2)) /* We keep one trailing one as marker */
+ return -ENOMEM;
+
+ r = decode_link(i, links + c, patterns, matched_patterns);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ links[c].needs_freeing = true;
+
+ (void) sd_device_new_from_ifindex(&links[c].sd_device, links[c].ifindex);
+
+ acquire_ether_link_info(&fd, &links[c]);
+ acquire_wlan_link_info(&links[c]);
+
+ c++;
+ }
+
+ /* Look if we matched all our arguments that are not globs. It
+ * is OK for a glob to match nothing, but not for an exact argument. */
+ for (size_t pos = 0; pos < strv_length(patterns); pos++) {
+ if (matched_patterns[pos])
+ continue;
+
+ if (string_is_glob(patterns[pos]))
+ log_debug("Pattern \"%s\" doesn't match any interface, ignoring.",
+ patterns[pos]);
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(ENODEV),
+ "Interface \"%s\" not found.", patterns[pos]);
+ }
+
+ typesafe_qsort(links, c, link_info_compare);
+
+ if (bus)
+ for (size_t j = 0; j < c; j++)
+ (void) acquire_link_bitrates(bus, links + j);
+
+ *ret = TAKE_PTR(links);
+
+ if (patterns && c == 0)
+ log_warning("No interfaces matched.");
+
+ return (int) c;
+}
+
+static int list_links(int argc, char *argv[], void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ _cleanup_(link_info_array_freep) LinkInfo *links = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ TableCell *cell;
+ int c, r;
+
+ if (arg_json_format_flags != JSON_FORMAT_OFF) {
+ if (arg_all || argc <= 1)
+ return dump_manager_description(bus);
+ else
+ return dump_link_description(bus, strv_skip(argv, 1));
+ }
+
+ r = sd_netlink_open(&rtnl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to netlink: %m");
+
+ c = acquire_link_info(NULL, rtnl, argc > 1 ? argv + 1 : NULL, &links);
+ if (c < 0)
+ return c;
+
+ pager_open(arg_pager_flags);
+
+ table = table_new("idx", "link", "type", "operational", "setup");
+ if (!table)
+ return log_oom();
+
+ if (arg_full)
+ table_set_width(table, 0);
+
+ table_set_header(table, arg_legend);
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ (void) table_set_minimum_width(table, cell, 3);
+ (void) table_set_weight(table, cell, 0);
+ (void) table_set_ellipsize_percent(table, cell, 100);
+ (void) table_set_align_percent(table, cell, 100);
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ for (int i = 0; i < c; i++) {
+ _cleanup_free_ char *setup_state = NULL, *operational_state = NULL;
+ const char *on_color_operational, *on_color_setup;
+ _cleanup_free_ char *t = NULL;
+
+ (void) sd_network_link_get_operational_state(links[i].ifindex, &operational_state);
+ operational_state_to_color(links[i].name, operational_state, &on_color_operational, NULL);
+
+ (void) sd_network_link_get_setup_state(links[i].ifindex, &setup_state);
+ setup_state_to_color(setup_state, &on_color_setup, NULL);
+
+ r = net_get_type_string(links[i].sd_device, links[i].iftype, &t);
+ if (r == -ENOMEM)
+ return log_oom();
+
+ r = table_add_many(table,
+ TABLE_INT, links[i].ifindex,
+ TABLE_STRING, links[i].name,
+ TABLE_STRING, t,
+ TABLE_STRING, operational_state,
+ TABLE_SET_COLOR, on_color_operational,
+ TABLE_STRING, setup_state ?: "unmanaged",
+ TABLE_SET_COLOR, on_color_setup);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return table_log_print_error(r);
+
+ if (arg_legend)
+ printf("\n%i links listed.\n", c);
+
+ return 0;
+}
+
+/* IEEE Organizationally Unique Identifier vendor string */
+static int ieee_oui(sd_hwdb *hwdb, const struct ether_addr *mac, char **ret) {
+ const char *description;
+ char modalias[STRLEN("OUI:XXYYXXYYXXYY") + 1], *desc;
+ int r;
+
+ assert(ret);
+
+ if (!hwdb)
+ return -EINVAL;
+
+ if (!mac)
+ return -EINVAL;
+
+ /* skip commonly misused 00:00:00 (Xerox) prefix */
+ if (memcmp(mac, "\0\0\0", 3) == 0)
+ return -EINVAL;
+
+ xsprintf(modalias, "OUI:" ETHER_ADDR_FORMAT_STR,
+ ETHER_ADDR_FORMAT_VAL(*mac));
+
+ r = sd_hwdb_get(hwdb, modalias, "ID_OUI_FROM_DATABASE", &description);
+ if (r < 0)
+ return r;
+
+ desc = strdup(description);
+ if (!desc)
+ return -ENOMEM;
+
+ *ret = desc;
+
+ return 0;
+}
+
+static int get_gateway_description(
+ sd_netlink *rtnl,
+ sd_hwdb *hwdb,
+ int ifindex,
+ int family,
+ union in_addr_union *gateway,
+ char **gateway_description) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL;
+ int r;
+
+ assert(rtnl);
+ assert(ifindex >= 0);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(gateway);
+ assert(gateway_description);
+
+ r = sd_rtnl_message_new_neigh(rtnl, &req, RTM_GETNEIGH, ifindex, family);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_set_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ for (sd_netlink_message *m = reply; m; m = sd_netlink_message_next(m)) {
+ union in_addr_union gw = IN_ADDR_NULL;
+ struct ether_addr mac = ETHER_ADDR_NULL;
+ uint16_t type;
+ int ifi, fam;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_error_errno(r, "got error: %m");
+ continue;
+ }
+
+ r = sd_netlink_message_get_type(m, &type);
+ if (r < 0) {
+ log_error_errno(r, "could not get type: %m");
+ continue;
+ }
+
+ if (type != RTM_NEWNEIGH) {
+ log_error("type is not RTM_NEWNEIGH");
+ continue;
+ }
+
+ r = sd_rtnl_message_neigh_get_family(m, &fam);
+ if (r < 0) {
+ log_error_errno(r, "could not get family: %m");
+ continue;
+ }
+
+ if (fam != family) {
+ log_error("family is not correct");
+ continue;
+ }
+
+ r = sd_rtnl_message_neigh_get_ifindex(m, &ifi);
+ if (r < 0) {
+ log_error_errno(r, "could not get ifindex: %m");
+ continue;
+ }
+
+ if (ifindex > 0 && ifi != ifindex)
+ continue;
+
+ switch (fam) {
+ case AF_INET:
+ r = sd_netlink_message_read_in_addr(m, NDA_DST, &gw.in);
+ if (r < 0)
+ continue;
+
+ break;
+ case AF_INET6:
+ r = sd_netlink_message_read_in6_addr(m, NDA_DST, &gw.in6);
+ if (r < 0)
+ continue;
+
+ break;
+ default:
+ continue;
+ }
+
+ if (!in_addr_equal(fam, &gw, gateway))
+ continue;
+
+ r = sd_netlink_message_read(m, NDA_LLADDR, sizeof(mac), &mac);
+ if (r < 0)
+ continue;
+
+ r = ieee_oui(hwdb, &mac, gateway_description);
+ if (r < 0)
+ continue;
+
+ return 0;
+ }
+
+ return -ENODATA;
+}
+
+static int dump_list(Table *table, const char *prefix, char * const *l) {
+ int r;
+
+ if (strv_isempty(l))
+ return 0;
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, prefix,
+ TABLE_STRV, l);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_gateways(
+ sd_netlink *rtnl,
+ sd_hwdb *hwdb,
+ Table *table,
+ int ifindex) {
+ _cleanup_free_ struct local_address *local = NULL;
+ _cleanup_strv_free_ char **buf = NULL;
+ int r, n;
+
+ assert(rtnl);
+ assert(table);
+
+ n = local_gateways(rtnl, ifindex, AF_UNSPEC, &local);
+ if (n <= 0)
+ return n;
+
+ for (int i = 0; i < n; i++) {
+ _cleanup_free_ char *description = NULL;
+
+ r = get_gateway_description(rtnl, hwdb, local[i].ifindex, local[i].family, &local[i].address, &description);
+ if (r < 0)
+ log_debug_errno(r, "Could not get description of gateway, ignoring: %m");
+
+ /* Show interface name for the entry if we show entries for all interfaces */
+ r = strv_extendf(&buf, "%s%s%s%s%s%s",
+ IN_ADDR_TO_STRING(local[i].family, &local[i].address),
+ description ? " (" : "",
+ strempty(description),
+ description ? ")" : "",
+ ifindex <= 0 ? " on " : "",
+ ifindex <= 0 ? FORMAT_IFNAME_FULL(local[i].ifindex, FORMAT_IFNAME_IFINDEX_WITH_PERCENT) : "");
+ if (r < 0)
+ return log_oom();
+ }
+
+ return dump_list(table, "Gateway:", buf);
+}
+
+static int dump_addresses(
+ sd_netlink *rtnl,
+ sd_dhcp_lease *lease,
+ Table *table,
+ int ifindex) {
+
+ _cleanup_free_ struct local_address *local = NULL;
+ _cleanup_strv_free_ char **buf = NULL;
+ struct in_addr dhcp4_address = {};
+ int r, n;
+
+ assert(rtnl);
+ assert(table);
+
+ n = local_addresses(rtnl, ifindex, AF_UNSPEC, &local);
+ if (n <= 0)
+ return n;
+
+ if (lease)
+ (void) sd_dhcp_lease_get_address(lease, &dhcp4_address);
+
+ for (int i = 0; i < n; i++) {
+ struct in_addr server_address;
+ bool dhcp4 = false;
+
+ if (local[i].family == AF_INET && in4_addr_equal(&local[i].address.in, &dhcp4_address))
+ dhcp4 = sd_dhcp_lease_get_server_identifier(lease, &server_address) >= 0;
+
+ r = strv_extendf(&buf, "%s%s%s%s%s%s",
+ IN_ADDR_TO_STRING(local[i].family, &local[i].address),
+ dhcp4 ? " (DHCP4 via " : "",
+ dhcp4 ? IN4_ADDR_TO_STRING(&server_address) : "",
+ dhcp4 ? ")" : "",
+ ifindex <= 0 ? " on " : "",
+ ifindex <= 0 ? FORMAT_IFNAME_FULL(local[i].ifindex, FORMAT_IFNAME_IFINDEX_WITH_PERCENT) : "");
+ if (r < 0)
+ return log_oom();
+ }
+
+ return dump_list(table, "Address:", buf);
+}
+
+static int dump_address_labels(sd_netlink *rtnl) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ TableCell *cell;
+ int r;
+
+ assert(rtnl);
+
+ r = sd_rtnl_message_new_addrlabel(rtnl, &req, RTM_GETADDRLABEL, 0, AF_INET6);
+ if (r < 0)
+ return log_error_errno(r, "Could not allocate RTM_GETADDRLABEL message: %m");
+
+ r = sd_netlink_message_set_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ table = table_new("label", "prefix/prefixlen");
+ if (!table)
+ return log_oom();
+
+ if (arg_full)
+ table_set_width(table, 0);
+
+ r = table_set_sort(table, (size_t) 0);
+ if (r < 0)
+ return r;
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ (void) table_set_align_percent(table, cell, 100);
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ (void) table_set_align_percent(table, cell, 100);
+
+ for (sd_netlink_message *m = reply; m; m = sd_netlink_message_next(m)) {
+ struct in6_addr prefix;
+ uint8_t prefixlen;
+ uint32_t label;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_error_errno(r, "got error: %m");
+ continue;
+ }
+
+ r = sd_netlink_message_read_u32(m, IFAL_LABEL, &label);
+ if (r < 0 && r != -ENODATA) {
+ log_error_errno(r, "Could not read IFAL_LABEL, ignoring: %m");
+ continue;
+ }
+
+ r = sd_netlink_message_read_in6_addr(m, IFAL_ADDRESS, &prefix);
+ if (r < 0)
+ continue;
+
+ r = sd_rtnl_message_addrlabel_get_prefixlen(m, &prefixlen);
+ if (r < 0)
+ continue;
+
+ r = table_add_cell(table, NULL, TABLE_UINT32, &label);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_cell_stringf(table, NULL, "%s/%u", IN6_ADDR_TO_STRING(&prefix), prefixlen);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return table_log_print_error(r);
+
+ return 0;
+}
+
+static int list_address_labels(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ int r;
+
+ r = sd_netlink_open(&rtnl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to netlink: %m");
+
+ dump_address_labels(rtnl);
+
+ return 0;
+}
+
+static int open_lldp_neighbors(int ifindex, FILE **ret) {
+ char p[STRLEN("/run/systemd/netif/lldp/") + DECIMAL_STR_MAX(int)];
+ FILE *f;
+
+ xsprintf(p, "/run/systemd/netif/lldp/%i", ifindex);
+ f = fopen(p, "re");
+ if (!f)
+ return -errno;
+
+ *ret = f;
+ return 0;
+}
+
+static int next_lldp_neighbor(FILE *f, sd_lldp_neighbor **ret) {
+ _cleanup_free_ void *raw = NULL;
+ size_t l;
+ le64_t u;
+ int r;
+
+ assert(f);
+ assert(ret);
+
+ l = fread(&u, 1, sizeof(u), f);
+ if (l == 0 && feof(f))
+ return 0;
+ if (l != sizeof(u))
+ return -EBADMSG;
+
+ /* each LLDP packet is at most MTU size, but let's allow up to 4KiB just in case */
+ if (le64toh(u) >= 4096)
+ return -EBADMSG;
+
+ raw = new(uint8_t, le64toh(u));
+ if (!raw)
+ return -ENOMEM;
+
+ if (fread(raw, 1, le64toh(u), f) != le64toh(u))
+ return -EBADMSG;
+
+ r = sd_lldp_neighbor_from_raw(ret, raw, le64toh(u));
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int dump_lldp_neighbors(Table *table, const char *prefix, int ifindex) {
+ _cleanup_strv_free_ char **buf = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(table);
+ assert(prefix);
+ assert(ifindex > 0);
+
+ r = open_lldp_neighbors(ifindex, &f);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ const char *system_name = NULL, *port_id = NULL, *port_description = NULL;
+ _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = NULL;
+
+ r = next_lldp_neighbor(f, &n);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ (void) sd_lldp_neighbor_get_system_name(n, &system_name);
+ (void) sd_lldp_neighbor_get_port_id_as_string(n, &port_id);
+ (void) sd_lldp_neighbor_get_port_description(n, &port_description);
+
+ r = strv_extendf(&buf, "%s on port %s%s%s%s",
+ strna(system_name),
+ strna(port_id),
+ isempty(port_description) ? "" : " (",
+ strempty(port_description),
+ isempty(port_description) ? "" : ")");
+ if (r < 0)
+ return log_oom();
+ }
+
+ return dump_list(table, prefix, buf);
+}
+
+static int dump_dhcp_leases(Table *table, const char *prefix, sd_bus *bus, const LinkInfo *link) {
+ _cleanup_strv_free_ char **buf = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ r = link_get_property(bus, link, &error, &reply, "org.freedesktop.network1.DHCPServer", "Leases");
+ if (r < 0) {
+ bool quiet = sd_bus_error_has_name(&error, SD_BUS_ERROR_UNKNOWN_PROPERTY);
+
+ log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING,
+ r, "Failed to query link DHCP leases: %s", bus_error_message(&error, r));
+ return 0;
+ }
+
+ r = sd_bus_message_enter_container(reply, 'v', "a(uayayayayt)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_enter_container(reply, 'a', "(uayayayayt)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ while ((r = sd_bus_message_enter_container(reply, 'r', "uayayayayt")) > 0) {
+ _cleanup_free_ char *id = NULL, *ip = NULL;
+ const void *client_id, *addr, *gtw, *hwaddr;
+ size_t client_id_sz, sz;
+ uint64_t expiration;
+ uint32_t family;
+
+ r = sd_bus_message_read(reply, "u", &family);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read_array(reply, 'y', &client_id, &client_id_sz);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read_array(reply, 'y', &addr, &sz);
+ if (r < 0 || sz != 4)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read_array(reply, 'y', &gtw, &sz);
+ if (r < 0 || sz != 4)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read_array(reply, 'y', &hwaddr, &sz);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read_basic(reply, 't', &expiration);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_dhcp_client_id_to_string(client_id, client_id_sz, &id);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = in_addr_to_string(family, addr, &ip);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = strv_extendf(&buf, "%s (to %s)", ip, id);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ }
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (strv_isempty(buf)) {
+ r = strv_extendf(&buf, "none");
+ if (r < 0)
+ return log_oom();
+ }
+
+ return dump_list(table, prefix, buf);
+}
+
+static int dump_ifindexes(Table *table, const char *prefix, const int *ifindexes) {
+ int r;
+
+ assert(prefix);
+
+ if (!ifindexes || ifindexes[0] <= 0)
+ return 0;
+
+ for (unsigned c = 0; ifindexes[c] > 0; c++) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, c == 0 ? prefix : "",
+ TABLE_IFINDEX, ifindexes[c]);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ return 0;
+}
+
+#define DUMP_STATS_ONE(name, val_name) \
+ r = table_add_many(table, \
+ TABLE_EMPTY, \
+ TABLE_STRING, name ":"); \
+ if (r < 0) \
+ return table_log_add_error(r); \
+ r = table_add_cell(table, NULL, \
+ info->has_stats64 ? TABLE_UINT64 : TABLE_UINT32, \
+ info->has_stats64 ? (void*) &info->stats64.val_name : (void*) &info->stats.val_name); \
+ if (r < 0) \
+ return table_log_add_error(r);
+
+static int dump_statistics(Table *table, const LinkInfo *info) {
+ int r;
+
+ if (!arg_stats)
+ return 0;
+
+ if (!info->has_stats64 && !info->has_stats)
+ return 0;
+
+ DUMP_STATS_ONE("Rx Packets", rx_packets);
+ DUMP_STATS_ONE("Tx Packets", tx_packets);
+ DUMP_STATS_ONE("Rx Bytes", rx_bytes);
+ DUMP_STATS_ONE("Tx Bytes", tx_bytes);
+ DUMP_STATS_ONE("Rx Errors", rx_errors);
+ DUMP_STATS_ONE("Tx Errors", tx_errors);
+ DUMP_STATS_ONE("Rx Dropped", rx_dropped);
+ DUMP_STATS_ONE("Tx Dropped", tx_dropped);
+ DUMP_STATS_ONE("Multicast Packets", multicast);
+ DUMP_STATS_ONE("Collisions", collisions);
+
+ return 0;
+}
+
+static int dump_hw_address(Table *table, sd_hwdb *hwdb, const char *field, const struct hw_addr_data *addr) {
+ _cleanup_free_ char *description = NULL;
+ int r;
+
+ assert(table);
+ assert(field);
+ assert(addr);
+
+ if (addr->length == ETH_ALEN)
+ (void) ieee_oui(hwdb, &addr->ether, &description);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, field);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_cell_stringf(table, NULL, "%s%s%s%s",
+ HW_ADDR_TO_STR(addr),
+ description ? " (" : "",
+ strempty(description),
+ description ? ")" : "");
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static OutputFlags get_output_flags(void) {
+ return
+ arg_all * OUTPUT_SHOW_ALL |
+ (arg_full || !on_tty() || pager_have()) * OUTPUT_FULL_WIDTH |
+ colors_enabled() * OUTPUT_COLOR;
+}
+
+static int show_logs(const LinkInfo *info) {
+ _cleanup_(sd_journal_closep) sd_journal *j = NULL;
+ int r;
+
+ if (arg_lines == 0)
+ return 0;
+
+ r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open journal: %m");
+
+ r = add_match_this_boot(j, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add boot matches: %m");
+
+ if (info) {
+ char m1[STRLEN("_KERNEL_DEVICE=n") + DECIMAL_STR_MAX(int)];
+ const char *m2, *m3;
+
+ /* kernel */
+ xsprintf(m1, "_KERNEL_DEVICE=n%i", info->ifindex);
+ /* networkd */
+ m2 = strjoina("INTERFACE=", info->name);
+ /* udevd */
+ m3 = strjoina("DEVICE=", info->name);
+
+ (void)(
+ (r = sd_journal_add_match(j, m1, 0)) ||
+ (r = sd_journal_add_disjunction(j)) ||
+ (r = sd_journal_add_match(j, m2, 0)) ||
+ (r = sd_journal_add_disjunction(j)) ||
+ (r = sd_journal_add_match(j, m3, 0))
+ );
+ if (r < 0)
+ return log_error_errno(r, "Failed to add link matches: %m");
+ } else {
+ r = add_matches_for_unit(j, "systemd-networkd.service");
+ if (r < 0)
+ return log_error_errno(r, "Failed to add unit matches: %m");
+
+ r = add_matches_for_unit(j, "systemd-networkd-wait-online.service");
+ if (r < 0)
+ return log_error_errno(r, "Failed to add unit matches: %m");
+ }
+
+ return show_journal(
+ stdout,
+ j,
+ OUTPUT_SHORT,
+ 0,
+ 0,
+ arg_lines,
+ get_output_flags() | OUTPUT_BEGIN_NEWLINE,
+ NULL);
+}
+
+static int table_add_string_line(Table *table, const char *key, const char *value) {
+ int r;
+
+ assert(table);
+ assert(key);
+
+ if (isempty(value))
+ return 0;
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, key,
+ TABLE_STRING, value);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int link_status_one(
+ sd_bus *bus,
+ sd_netlink *rtnl,
+ sd_hwdb *hwdb,
+ const LinkInfo *info) {
+
+ _cleanup_strv_free_ char **dns = NULL, **ntp = NULL, **sip = NULL, **search_domains = NULL, **route_domains = NULL;
+ _cleanup_free_ char *t = NULL, *network = NULL, *iaid = NULL, *duid = NULL,
+ *setup_state = NULL, *operational_state = NULL, *online_state = NULL, *activation_policy = NULL;
+ const char *driver = NULL, *path = NULL, *vendor = NULL, *model = NULL, *link = NULL,
+ *on_color_operational, *off_color_operational, *on_color_setup, *off_color_setup, *on_color_online;
+ _cleanup_free_ int *carrier_bound_to = NULL, *carrier_bound_by = NULL;
+ _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ TableCell *cell;
+ int r;
+
+ assert(rtnl);
+ assert(info);
+
+ (void) sd_network_link_get_operational_state(info->ifindex, &operational_state);
+ operational_state_to_color(info->name, operational_state, &on_color_operational, &off_color_operational);
+
+ (void) sd_network_link_get_online_state(info->ifindex, &online_state);
+ online_state_to_color(online_state, &on_color_online, NULL);
+
+ (void) sd_network_link_get_setup_state(info->ifindex, &setup_state);
+ setup_state_to_color(setup_state, &on_color_setup, &off_color_setup);
+
+ (void) sd_network_link_get_dns(info->ifindex, &dns);
+ (void) sd_network_link_get_search_domains(info->ifindex, &search_domains);
+ (void) sd_network_link_get_route_domains(info->ifindex, &route_domains);
+ (void) sd_network_link_get_ntp(info->ifindex, &ntp);
+ (void) sd_network_link_get_sip(info->ifindex, &sip);
+ (void) sd_network_link_get_network_file(info->ifindex, &network);
+ (void) sd_network_link_get_carrier_bound_to(info->ifindex, &carrier_bound_to);
+ (void) sd_network_link_get_carrier_bound_by(info->ifindex, &carrier_bound_by);
+ (void) sd_network_link_get_activation_policy(info->ifindex, &activation_policy);
+
+ if (info->sd_device) {
+ (void) sd_device_get_property_value(info->sd_device, "ID_NET_LINK_FILE", &link);
+ (void) sd_device_get_property_value(info->sd_device, "ID_NET_DRIVER", &driver);
+ (void) sd_device_get_property_value(info->sd_device, "ID_PATH", &path);
+
+ if (sd_device_get_property_value(info->sd_device, "ID_VENDOR_FROM_DATABASE", &vendor) < 0)
+ (void) sd_device_get_property_value(info->sd_device, "ID_VENDOR", &vendor);
+
+ if (sd_device_get_property_value(info->sd_device, "ID_MODEL_FROM_DATABASE", &model) < 0)
+ (void) sd_device_get_property_value(info->sd_device, "ID_MODEL", &model);
+ }
+
+ r = net_get_type_string(info->sd_device, info->iftype, &t);
+ if (r == -ENOMEM)
+ return log_oom();
+
+ char lease_file[STRLEN("/run/systemd/netif/leases/") + DECIMAL_STR_MAX(int)];
+ xsprintf(lease_file, "/run/systemd/netif/leases/%i", info->ifindex);
+
+ (void) dhcp_lease_load(&lease, lease_file);
+
+ table = table_new("dot", "key", "value");
+ if (!table)
+ return log_oom();
+
+ if (arg_full)
+ table_set_width(table, 0);
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ table_set_header(table, false);
+
+ /* First line: circle, ifindex, ifname. */
+ r = table_add_many(table,
+ TABLE_STRING, special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE),
+ TABLE_SET_COLOR, on_color_operational);
+ if (r < 0)
+ return table_log_add_error(r);
+ r = table_add_cell_stringf(table, &cell, "%i: %s", info->ifindex, info->name);
+ if (r < 0)
+ return table_log_add_error(r);
+ r = table_add_many(table, TABLE_EMPTY);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ (void) table_set_align_percent(table, cell, 0);
+
+ /* unit files and basic states. */
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Link File:",
+ TABLE_SET_ALIGN_PERCENT, 100,
+ TABLE_STRING, strna(link),
+ TABLE_EMPTY,
+ TABLE_STRING, "Network File:",
+ TABLE_STRING, strna(network),
+ TABLE_EMPTY,
+ TABLE_STRING, "State:");
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_cell_stringf(table, NULL, "%s%s%s (%s%s%s)",
+ on_color_operational, strna(operational_state), off_color_operational,
+ on_color_setup, setup_state ?: "unmanaged", off_color_setup);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Online state:",
+ TABLE_STRING, online_state ?: "unknown",
+ TABLE_SET_COLOR, on_color_online);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_string_line(table, "Type:", t);
+ if (r < 0)
+ return r;
+
+ r = table_add_string_line(table, "Kind:", info->netdev_kind);
+ if (r < 0)
+ return r;
+
+ r = table_add_string_line(table, "Path:", path);
+ if (r < 0)
+ return r;
+
+ r = table_add_string_line(table, "Driver:", driver);
+ if (r < 0)
+ return r;
+
+ r = table_add_string_line(table, "Vendor:", vendor);
+ if (r < 0)
+ return r;
+
+ r = table_add_string_line(table, "Model:", model);
+ if (r < 0)
+ return r;
+
+ strv_sort(info->alternative_names);
+ r = dump_list(table, "Alternative Names:", info->alternative_names);
+ if (r < 0)
+ return r;
+
+ if (info->has_hw_address) {
+ r = dump_hw_address(table, hwdb, "Hardware Address:", &info->hw_address);
+ if (r < 0)
+ return r;
+ }
+
+ if (info->has_permanent_hw_address) {
+ r = dump_hw_address(table, hwdb, "Permanent Hardware Address:", &info->permanent_hw_address);
+ if (r < 0)
+ return r;
+ }
+
+ if (info->mtu > 0) {
+ char min_str[DECIMAL_STR_MAX(uint32_t)], max_str[DECIMAL_STR_MAX(uint32_t)];
+
+ xsprintf(min_str, "%" PRIu32, info->min_mtu);
+ xsprintf(max_str, "%" PRIu32, info->max_mtu);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "MTU:");
+ if (r < 0)
+ return table_log_add_error(r);
+ r = table_add_cell_stringf(table, NULL, "%" PRIu32 "%s%s%s%s%s%s%s",
+ info->mtu,
+ info->min_mtu > 0 || info->max_mtu > 0 ? " (" : "",
+ info->min_mtu > 0 ? "min: " : "",
+ info->min_mtu > 0 ? min_str : "",
+ info->min_mtu > 0 && info->max_mtu > 0 ? ", " : "",
+ info->max_mtu > 0 ? "max: " : "",
+ info->max_mtu > 0 ? max_str : "",
+ info->min_mtu > 0 || info->max_mtu > 0 ? ")" : "");
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_string_line(table, "QDisc:", info->qdisc);
+ if (r < 0)
+ return r;
+
+ if (info->master > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Master:",
+ TABLE_IFINDEX, info->master);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->has_ipv6_address_generation_mode) {
+ static const struct {
+ const char *mode;
+ } mode_table[] = {
+ { "eui64" },
+ { "none" },
+ { "stable-privacy" },
+ { "random" },
+ };
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "IPv6 Address Generation Mode:",
+ TABLE_STRING, mode_table[info->addr_gen_mode]);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (streq_ptr(info->netdev_kind, "bridge")) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Forward Delay:",
+ TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->forward_delay),
+ TABLE_EMPTY,
+ TABLE_STRING, "Hello Time:",
+ TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->hello_time),
+ TABLE_EMPTY,
+ TABLE_STRING, "Max Age:",
+ TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->max_age),
+ TABLE_EMPTY,
+ TABLE_STRING, "Ageing Time:",
+ TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->ageing_time),
+ TABLE_EMPTY,
+ TABLE_STRING, "Priority:",
+ TABLE_UINT16, info->priority,
+ TABLE_EMPTY,
+ TABLE_STRING, "STP:",
+ TABLE_BOOLEAN, info->stp_state > 0,
+ TABLE_EMPTY,
+ TABLE_STRING, "Multicast IGMP Version:",
+ TABLE_UINT8, info->mcast_igmp_version,
+ TABLE_EMPTY,
+ TABLE_STRING, "Cost:",
+ TABLE_UINT32, info->cost);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (info->port_state <= BR_STATE_BLOCKING) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Port State:",
+ TABLE_STRING, bridge_state_to_string(info->port_state));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ } else if (streq_ptr(info->netdev_kind, "bond")) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Mode:",
+ TABLE_STRING, bond_mode_to_string(info->mode),
+ TABLE_EMPTY,
+ TABLE_STRING, "Miimon:",
+ TABLE_TIMESPAN_MSEC, info->miimon * USEC_PER_MSEC,
+ TABLE_EMPTY,
+ TABLE_STRING, "Updelay:",
+ TABLE_TIMESPAN_MSEC, info->updelay * USEC_PER_MSEC,
+ TABLE_EMPTY,
+ TABLE_STRING, "Downdelay:",
+ TABLE_TIMESPAN_MSEC, info->downdelay * USEC_PER_MSEC);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ } else if (streq_ptr(info->netdev_kind, "vxlan")) {
+ char ttl[CONST_MAX(STRLEN("auto") + 1, DECIMAL_STR_MAX(uint8_t))];
+
+ if (info->vxlan_info.vni > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "VNI:",
+ TABLE_UINT32, info->vxlan_info.vni);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (IN_SET(info->vxlan_info.group_family, AF_INET, AF_INET6)) {
+ const char *p;
+
+ r = in_addr_is_multicast(info->vxlan_info.group_family, &info->vxlan_info.group);
+ if (r <= 0)
+ p = "Remote:";
+ else
+ p = "Group:";
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, p,
+ info->vxlan_info.group_family == AF_INET ? TABLE_IN_ADDR : TABLE_IN6_ADDR,
+ &info->vxlan_info.group);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (IN_SET(info->vxlan_info.local_family, AF_INET, AF_INET6)) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Local:",
+ info->vxlan_info.local_family == AF_INET ? TABLE_IN_ADDR : TABLE_IN6_ADDR,
+ &info->vxlan_info.local);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->vxlan_info.dest_port > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Destination Port:",
+ TABLE_UINT16, be16toh(info->vxlan_info.dest_port));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->vxlan_info.link > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Underlying Device:",
+ TABLE_IFINDEX, info->vxlan_info.link);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Learning:",
+ TABLE_BOOLEAN, info->vxlan_info.learning);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "RSC:",
+ TABLE_BOOLEAN, info->vxlan_info.rsc);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "L3MISS:",
+ TABLE_BOOLEAN, info->vxlan_info.l3miss);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "L2MISS:",
+ TABLE_BOOLEAN, info->vxlan_info.l2miss);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (info->vxlan_info.tos > 1) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "TOS:",
+ TABLE_UINT8, info->vxlan_info.tos);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->vxlan_info.ttl > 0)
+ xsprintf(ttl, "%" PRIu8, info->vxlan_info.ttl);
+ else
+ strcpy(ttl, "auto");
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "TTL:",
+ TABLE_STRING, ttl);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ } else if (streq_ptr(info->netdev_kind, "vlan") && info->vlan_id > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "VLan Id:",
+ TABLE_UINT16, info->vlan_id);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ } else if (STRPTR_IN_SET(info->netdev_kind, "ipip", "sit", "gre", "gretap", "erspan", "vti")) {
+ if (in_addr_is_set(AF_INET, &info->local)) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Local:",
+ TABLE_IN_ADDR, &info->local);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (in_addr_is_set(AF_INET, &info->remote)) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Remote:",
+ TABLE_IN_ADDR, &info->remote);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ } else if (STRPTR_IN_SET(info->netdev_kind, "ip6gre", "ip6gretap", "ip6erspan", "vti6")) {
+ if (in_addr_is_set(AF_INET6, &info->local)) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Local:",
+ TABLE_IN6_ADDR, &info->local);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (in_addr_is_set(AF_INET6, &info->remote)) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Remote:",
+ TABLE_IN6_ADDR, &info->remote);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ } else if (streq_ptr(info->netdev_kind, "geneve")) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "VNI:",
+ TABLE_UINT32, info->vni);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (info->has_tunnel_ipv4 && in_addr_is_set(AF_INET, &info->remote)) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Remote:",
+ TABLE_IN_ADDR, &info->remote);
+ if (r < 0)
+ return table_log_add_error(r);
+ } else if (in_addr_is_set(AF_INET6, &info->remote)) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Remote:",
+ TABLE_IN6_ADDR, &info->remote);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->ttl > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "TTL:",
+ TABLE_UINT8, info->ttl);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->tos > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "TOS:",
+ TABLE_UINT8, info->tos);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Port:",
+ TABLE_UINT16, info->tunnel_port);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Inherit:",
+ TABLE_STRING, geneve_df_to_string(info->inherit));
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (info->df > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "IPDoNotFragment:",
+ TABLE_UINT8, info->df);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "UDPChecksum:",
+ TABLE_BOOLEAN, info->csum);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "UDP6ZeroChecksumTx:",
+ TABLE_BOOLEAN, info->csum6_tx);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "UDP6ZeroChecksumRx:",
+ TABLE_BOOLEAN, info->csum6_rx);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (info->label > 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "FlowLabel:",
+ TABLE_UINT32, info->label);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ } else if (STRPTR_IN_SET(info->netdev_kind, "macvlan", "macvtap")) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Mode:",
+ TABLE_STRING, macvlan_mode_to_string(info->macvlan_mode));
+ if (r < 0)
+ return table_log_add_error(r);
+
+ } else if (streq_ptr(info->netdev_kind, "ipvlan")) {
+ _cleanup_free_ char *p = NULL, *s = NULL;
+
+ if (info->ipvlan_flags & IPVLAN_F_PRIVATE)
+ p = strdup("private");
+ else if (info->ipvlan_flags & IPVLAN_F_VEPA)
+ p = strdup("vepa");
+ else
+ p = strdup("bridge");
+ if (!p)
+ log_oom();
+
+ s = strjoin(ipvlan_mode_to_string(info->ipvlan_mode), " (", p, ")");
+ if (!s)
+ return log_oom();
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Mode:",
+ TABLE_STRING, s);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->has_wlan_link_info) {
+ _cleanup_free_ char *esc = NULL;
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "WiFi access point:");
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (info->ssid)
+ esc = cescape(info->ssid);
+
+ r = table_add_cell_stringf(table, NULL, "%s (%s)",
+ strnull(esc),
+ ETHER_ADDR_TO_STR(&info->bssid));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->has_bitrates) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Bit Rate (Tx/Rx):");
+ if (r < 0)
+ return table_log_add_error(r);
+ r = table_add_cell_stringf(table, NULL, "%sbps/%sbps",
+ FORMAT_BYTES_FULL(info->tx_bitrate, 0),
+ FORMAT_BYTES_FULL(info->rx_bitrate, 0));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->has_tx_queues || info->has_rx_queues) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Number of Queues (Tx/Rx):");
+ if (r < 0)
+ return table_log_add_error(r);
+ r = table_add_cell_stringf(table, NULL, "%" PRIu32 "/%" PRIu32, info->tx_queues, info->rx_queues);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->has_ethtool_link_info) {
+ if (IN_SET(info->autonegotiation, AUTONEG_DISABLE, AUTONEG_ENABLE)) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Auto negotiation:",
+ TABLE_BOOLEAN, info->autonegotiation == AUTONEG_ENABLE);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->speed > 0 && info->speed != UINT64_MAX) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Speed:",
+ TABLE_BPS, info->speed);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_string_line(table, "Duplex:", duplex_to_string(info->duplex));
+ if (r < 0)
+ return r;
+
+ r = table_add_string_line(table, "Port:", port_to_string(info->port));
+ if (r < 0)
+ return r;
+ }
+
+ r = dump_addresses(rtnl, lease, table, info->ifindex);
+ if (r < 0)
+ return r;
+ r = dump_gateways(rtnl, hwdb, table, info->ifindex);
+ if (r < 0)
+ return r;
+ r = dump_list(table, "DNS:", dns);
+ if (r < 0)
+ return r;
+ r = dump_list(table, "Search Domains:", search_domains);
+ if (r < 0)
+ return r;
+ r = dump_list(table, "Route Domains:", route_domains);
+ if (r < 0)
+ return r;
+ r = dump_list(table, "NTP:", ntp);
+ if (r < 0)
+ return r;
+ r = dump_list(table, "SIP:", sip);
+ if (r < 0)
+ return r;
+ r = dump_ifindexes(table, "Carrier Bound To:", carrier_bound_to);
+ if (r < 0)
+ return r;
+ r = dump_ifindexes(table, "Carrier Bound By:", carrier_bound_by);
+ if (r < 0)
+ return r;
+
+ r = table_add_string_line(table, "Activation Policy:", activation_policy);
+ if (r < 0)
+ return r;
+
+ r = sd_network_link_get_required_for_online(info->ifindex);
+ if (r >= 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Required For Online:",
+ TABLE_BOOLEAN, r);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (lease) {
+ const void *client_id;
+ size_t client_id_len;
+ const char *tz;
+
+ r = sd_dhcp_lease_get_timezone(lease, &tz);
+ if (r >= 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Time Zone:",
+ TABLE_STRING, tz);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = sd_dhcp_lease_get_client_id(lease, &client_id, &client_id_len);
+ if (r >= 0) {
+ _cleanup_free_ char *id = NULL;
+
+ r = sd_dhcp_client_id_to_string(client_id, client_id_len, &id);
+ if (r >= 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "DHCP4 Client ID:",
+ TABLE_STRING, id);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ }
+ }
+
+ r = sd_network_link_get_dhcp6_client_iaid_string(info->ifindex, &iaid);
+ if (r >= 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "DHCP6 Client IAID:",
+ TABLE_STRING, iaid);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = sd_network_link_get_dhcp6_client_duid_string(info->ifindex, &duid);
+ if (r >= 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "DHCP6 Client DUID:",
+ TABLE_STRING, duid);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = dump_lldp_neighbors(table, "Connected To:", info->ifindex);
+ if (r < 0)
+ return r;
+
+ r = dump_dhcp_leases(table, "Offered DHCP leases:", bus, info);
+ if (r < 0)
+ return r;
+
+ r = dump_statistics(table, info);
+ if (r < 0)
+ return r;
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return table_log_print_error(r);
+
+ return show_logs(info);
+}
+
+static int system_status(sd_netlink *rtnl, sd_hwdb *hwdb) {
+ _cleanup_free_ char *operational_state = NULL, *online_state = NULL;
+ _cleanup_strv_free_ char **dns = NULL, **ntp = NULL, **search_domains = NULL, **route_domains = NULL;
+ const char *on_color_operational, *on_color_online;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ TableCell *cell;
+ int r;
+
+ assert(rtnl);
+
+ (void) sd_network_get_operational_state(&operational_state);
+ operational_state_to_color(NULL, operational_state, &on_color_operational, NULL);
+
+ (void) sd_network_get_online_state(&online_state);
+ online_state_to_color(online_state, &on_color_online, NULL);
+
+ table = table_new("dot", "key", "value");
+ if (!table)
+ return log_oom();
+
+ if (arg_full)
+ table_set_width(table, 0);
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ (void) table_set_align_percent(table, cell, 100);
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ table_set_header(table, false);
+
+ r = table_add_many(table,
+ TABLE_STRING, special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE),
+ TABLE_SET_COLOR, on_color_operational,
+ TABLE_STRING, "State:",
+ TABLE_STRING, strna(operational_state),
+ TABLE_SET_COLOR, on_color_operational,
+ TABLE_EMPTY,
+ TABLE_STRING, "Online state:",
+ TABLE_STRING, online_state ?: "unknown",
+ TABLE_SET_COLOR, on_color_online);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = dump_addresses(rtnl, NULL, table, 0);
+ if (r < 0)
+ return r;
+ r = dump_gateways(rtnl, hwdb, table, 0);
+ if (r < 0)
+ return r;
+
+ (void) sd_network_get_dns(&dns);
+ r = dump_list(table, "DNS:", dns);
+ if (r < 0)
+ return r;
+
+ (void) sd_network_get_search_domains(&search_domains);
+ r = dump_list(table, "Search Domains:", search_domains);
+ if (r < 0)
+ return r;
+
+ (void) sd_network_get_route_domains(&route_domains);
+ r = dump_list(table, "Route Domains:", route_domains);
+ if (r < 0)
+ return r;
+
+ (void) sd_network_get_ntp(&ntp);
+ r = dump_list(table, "NTP:", ntp);
+ if (r < 0)
+ return r;
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return table_log_print_error(r);
+
+ return show_logs(NULL);
+}
+
+static int link_status(int argc, char *argv[], void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL;
+ _cleanup_(link_info_array_freep) LinkInfo *links = NULL;
+ int r, c;
+
+ if (arg_json_format_flags != JSON_FORMAT_OFF) {
+ if (arg_all || argc <= 1)
+ return dump_manager_description(bus);
+ else
+ return dump_link_description(bus, strv_skip(argv, 1));
+ }
+
+ pager_open(arg_pager_flags);
+
+ r = sd_netlink_open(&rtnl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to netlink: %m");
+
+ r = sd_hwdb_new(&hwdb);
+ if (r < 0)
+ log_debug_errno(r, "Failed to open hardware database: %m");
+
+ if (arg_all)
+ c = acquire_link_info(bus, rtnl, NULL, &links);
+ else if (argc <= 1)
+ return system_status(rtnl, hwdb);
+ else
+ c = acquire_link_info(bus, rtnl, argv + 1, &links);
+ if (c < 0)
+ return c;
+
+ for (int i = 0; i < c; i++) {
+ if (i > 0)
+ fputc('\n', stdout);
+
+ link_status_one(bus, rtnl, hwdb, links + i);
+ }
+
+ return 0;
+}
+
+static char *lldp_capabilities_to_string(uint16_t x) {
+ static const char characters[] = {
+ 'o', 'p', 'b', 'w', 'r', 't', 'd', 'a', 'c', 's', 'm',
+ };
+ char *ret;
+ unsigned i;
+
+ ret = new(char, ELEMENTSOF(characters) + 1);
+ if (!ret)
+ return NULL;
+
+ for (i = 0; i < ELEMENTSOF(characters); i++)
+ ret[i] = (x & (1U << i)) ? characters[i] : '.';
+
+ ret[i] = 0;
+ return ret;
+}
+
+static void lldp_capabilities_legend(uint16_t x) {
+ unsigned cols = columns();
+ static const char* const table[] = {
+ "o - Other",
+ "p - Repeater",
+ "b - Bridge",
+ "w - WLAN Access Point",
+ "r - Router",
+ "t - Telephone",
+ "d - DOCSIS cable device",
+ "a - Station",
+ "c - Customer VLAN",
+ "s - Service VLAN",
+ "m - Two-port MAC Relay (TPMR)",
+ };
+
+ if (x == 0)
+ return;
+
+ printf("\nCapability Flags:\n");
+ for (unsigned w = 0, i = 0; i < ELEMENTSOF(table); i++)
+ if (x & (1U << i) || arg_all) {
+ bool newline;
+
+ newline = w + strlen(table[i]) + (w == 0 ? 0 : 2) > cols;
+ if (newline)
+ w = 0;
+ w += printf("%s%s%s", newline ? "\n" : "", w == 0 ? "" : "; ", table[i]);
+ }
+ puts("");
+}
+
+static int link_lldp_status(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ _cleanup_(link_info_array_freep) LinkInfo *links = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int r, c, m = 0;
+ uint16_t all = 0;
+ TableCell *cell;
+
+ r = sd_netlink_open(&rtnl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to netlink: %m");
+
+ c = acquire_link_info(NULL, rtnl, argc > 1 ? argv + 1 : NULL, &links);
+ if (c < 0)
+ return c;
+
+ pager_open(arg_pager_flags);
+
+ table = table_new("link",
+ "chassis-id",
+ "system-name",
+ "caps",
+ "port-id",
+ "port-description");
+ if (!table)
+ return log_oom();
+
+ if (arg_full)
+ table_set_width(table, 0);
+
+ table_set_header(table, arg_legend);
+
+ assert_se(cell = table_get_cell(table, 0, 3));
+ table_set_minimum_width(table, cell, 11);
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+
+ for (int i = 0; i < c; i++) {
+ _cleanup_fclose_ FILE *f = NULL;
+
+ r = open_lldp_neighbors(links[i].ifindex, &f);
+ if (r == -ENOENT)
+ continue;
+ if (r < 0) {
+ log_warning_errno(r, "Failed to open LLDP data for %i, ignoring: %m", links[i].ifindex);
+ continue;
+ }
+
+ for (;;) {
+ const char *chassis_id = NULL, *port_id = NULL, *system_name = NULL, *port_description = NULL;
+ _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = NULL;
+ _cleanup_free_ char *capabilities = NULL;
+ uint16_t cc;
+
+ r = next_lldp_neighbor(f, &n);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to read neighbor data: %m");
+ break;
+ }
+ if (r == 0)
+ break;
+
+ (void) sd_lldp_neighbor_get_chassis_id_as_string(n, &chassis_id);
+ (void) sd_lldp_neighbor_get_port_id_as_string(n, &port_id);
+ (void) sd_lldp_neighbor_get_system_name(n, &system_name);
+ (void) sd_lldp_neighbor_get_port_description(n, &port_description);
+
+ if (sd_lldp_neighbor_get_enabled_capabilities(n, &cc) >= 0) {
+ capabilities = lldp_capabilities_to_string(cc);
+ all |= cc;
+ }
+
+ r = table_add_many(table,
+ TABLE_STRING, links[i].name,
+ TABLE_STRING, chassis_id,
+ TABLE_STRING, system_name,
+ TABLE_STRING, capabilities,
+ TABLE_STRING, port_id,
+ TABLE_STRING, port_description);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ m++;
+ }
+ }
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return table_log_print_error(r);
+
+ if (arg_legend) {
+ lldp_capabilities_legend(all);
+ printf("\n%i neighbors listed.\n", m);
+ }
+
+ return 0;
+}
+
+static int link_delete_send_message(sd_netlink *rtnl, int index) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(rtnl);
+
+ r = sd_rtnl_message_new_link(rtnl, &req, RTM_DELLINK, index);
+ if (r < 0)
+ return rtnl_log_create_error(r);
+
+ r = sd_netlink_call(rtnl, req, 0, NULL);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int link_up_down_send_message(sd_netlink *rtnl, char *command, int index) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(rtnl);
+
+ r = sd_rtnl_message_new_link(rtnl, &req, RTM_SETLINK, index);
+ if (r < 0)
+ return rtnl_log_create_error(r);
+
+ if (streq(command, "up"))
+ r = sd_rtnl_message_link_set_flags(req, IFF_UP, IFF_UP);
+ else
+ r = sd_rtnl_message_link_set_flags(req, 0, IFF_UP);
+ if (r < 0)
+ return log_error_errno(r, "Could not set link flags: %m");
+
+ r = sd_netlink_call(rtnl, req, 0, NULL);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int link_up_down(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ _cleanup_set_free_ Set *indexes = NULL;
+ int index, r;
+ void *p;
+
+ r = sd_netlink_open(&rtnl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to netlink: %m");
+
+ indexes = set_new(NULL);
+ if (!indexes)
+ return log_oom();
+
+ for (int i = 1; i < argc; i++) {
+ index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]);
+ if (index < 0)
+ return index;
+
+ r = set_put(indexes, INT_TO_PTR(index));
+ if (r < 0)
+ return log_oom();
+ }
+
+ SET_FOREACH(p, indexes) {
+ index = PTR_TO_INT(p);
+ r = link_up_down_send_message(rtnl, argv[0], index);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bring %s interface %s: %m",
+ argv[0], FORMAT_IFNAME_FULL(index, FORMAT_IFNAME_IFINDEX));
+ }
+
+ return r;
+}
+
+static int link_delete(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ _cleanup_set_free_ Set *indexes = NULL;
+ int index, r;
+ void *p;
+
+ r = sd_netlink_open(&rtnl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to netlink: %m");
+
+ indexes = set_new(NULL);
+ if (!indexes)
+ return log_oom();
+
+ for (int i = 1; i < argc; i++) {
+ index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]);
+ if (index < 0)
+ return index;
+
+ r = set_put(indexes, INT_TO_PTR(index));
+ if (r < 0)
+ return log_oom();
+ }
+
+ SET_FOREACH(p, indexes) {
+ index = PTR_TO_INT(p);
+ r = link_delete_send_message(rtnl, index);
+ if (r < 0)
+ return log_error_errno(r, "Failed to delete interface %s: %m",
+ FORMAT_IFNAME_FULL(index, FORMAT_IFNAME_IFINDEX));
+ }
+
+ return r;
+}
+
+static int link_renew_one(sd_bus *bus, int index, const char *name) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ r = bus_call_method(bus, bus_network_mgr, "RenewLink", &error, NULL, "i", index);
+ if (r < 0)
+ return log_error_errno(r, "Failed to renew dynamic configuration of interface %s: %s",
+ name, bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int link_renew(int argc, char *argv[], void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ int index, k = 0, r;
+
+ for (int i = 1; i < argc; i++) {
+ index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]);
+ if (index < 0)
+ return index;
+
+ r = link_renew_one(bus, index, argv[i]);
+ if (r < 0 && k >= 0)
+ k = r;
+ }
+
+ return k;
+}
+
+static int link_force_renew_one(sd_bus *bus, int index, const char *name) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ r = bus_call_method(bus, bus_network_mgr, "ForceRenewLink", &error, NULL, "i", index);
+ if (r < 0)
+ return log_error_errno(r, "Failed to force renew dynamic configuration of interface %s: %s",
+ name, bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int link_force_renew(int argc, char *argv[], void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ int k = 0, r;
+
+ for (int i = 1; i < argc; i++) {
+ int index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]);
+ if (index < 0)
+ return index;
+
+ r = link_force_renew_one(bus, index, argv[i]);
+ if (r < 0 && k >= 0)
+ k = r;
+ }
+
+ return k;
+}
+
+static int verb_reload(int argc, char *argv[], void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ r = bus_call_method(bus, bus_network_mgr, "Reload", &error, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to reload network settings: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int verb_reconfigure(int argc, char *argv[], void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ _cleanup_set_free_ Set *indexes = NULL;
+ int index, r;
+ void *p;
+
+ indexes = set_new(NULL);
+ if (!indexes)
+ return log_oom();
+
+ for (int i = 1; i < argc; i++) {
+ index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]);
+ if (index < 0)
+ return index;
+
+ r = set_put(indexes, INT_TO_PTR(index));
+ if (r < 0)
+ return log_oom();
+ }
+
+ SET_FOREACH(p, indexes) {
+ index = PTR_TO_INT(p);
+ r = bus_call_method(bus, bus_network_mgr, "ReconfigureLink", &error, NULL, "i", index);
+ if (r < 0)
+ return log_error_errno(r, "Failed to reconfigure network interface %s: %s",
+ FORMAT_IFNAME_FULL(index, FORMAT_IFNAME_IFINDEX),
+ bus_error_message(&error, r));
+ }
+
+ return 0;
+}
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("networkctl", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...] COMMAND\n\n"
+ "%sQuery and control the networking subsystem.%s\n"
+ "\nCommands:\n"
+ " list [PATTERN...] List links\n"
+ " status [PATTERN...] Show link status\n"
+ " lldp [PATTERN...] Show LLDP neighbors\n"
+ " label Show current address label entries in the kernel\n"
+ " delete DEVICES... Delete virtual netdevs\n"
+ " up DEVICES... Bring devices up\n"
+ " down DEVICES... Bring devices down\n"
+ " renew DEVICES... Renew dynamic configurations\n"
+ " forcerenew DEVICES... Trigger DHCP reconfiguration of all connected clients\n"
+ " reconfigure DEVICES... Reconfigure interfaces\n"
+ " reload Reload .network and .netdev files\n"
+ "\nOptions:\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
+ " -a --all Show status for all links\n"
+ " -s --stats Show detailed link statistics\n"
+ " -l --full Do not ellipsize output\n"
+ " -n --lines=INTEGER Number of journal entries to show\n"
+ " --json=pretty|short|off\n"
+ " Generate JSON output\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ link);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_JSON,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "all", no_argument, NULL, 'a' },
+ { "stats", no_argument, NULL, 's' },
+ { "full", no_argument, NULL, 'l' },
+ { "lines", required_argument, NULL, 'n' },
+ { "json", required_argument, NULL, ARG_JSON },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hasln:", options, NULL)) >= 0) {
+
+ switch (c) {
+
+ case 'h':
+ return help();
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case 'a':
+ arg_all = true;
+ break;
+
+ case 's':
+ arg_stats = true;
+ break;
+
+ case 'l':
+ arg_full = true;
+ break;
+
+ case 'n':
+ if (safe_atou(optarg, &arg_lines) < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse lines '%s'", optarg);
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ return 1;
+}
+
+static int networkctl_main(sd_bus *bus, int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "list", VERB_ANY, VERB_ANY, VERB_DEFAULT, list_links },
+ { "status", VERB_ANY, VERB_ANY, 0, link_status },
+ { "lldp", VERB_ANY, VERB_ANY, 0, link_lldp_status },
+ { "label", 1, 1, 0, list_address_labels },
+ { "delete", 2, VERB_ANY, 0, link_delete },
+ { "up", 2, VERB_ANY, 0, link_up_down },
+ { "down", 2, VERB_ANY, 0, link_up_down },
+ { "renew", 2, VERB_ANY, 0, link_renew },
+ { "forcerenew", 2, VERB_ANY, 0, link_force_renew },
+ { "reconfigure", 2, VERB_ANY, 0, verb_reconfigure },
+ { "reload", 1, 1, 0, verb_reload },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, bus);
+}
+
+static int check_netns_match(sd_bus *bus) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ struct stat st;
+ uint64_t id;
+ int r;
+
+ r = sd_bus_get_property_trivial(
+ bus,
+ "org.freedesktop.network1",
+ "/org/freedesktop/network1",
+ "org.freedesktop.network1.Manager",
+ "NamespaceId",
+ &error,
+ 't',
+ &id);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to query network namespace of networkd, ignoring: %s", bus_error_message(&error, r));
+ return 0;
+ }
+ if (id == 0) {
+ log_debug("systemd-networkd.service not running in a network namespace (?), skipping netns check.");
+ return 0;
+ }
+
+ if (stat("/proc/self/ns/net", &st) < 0)
+ return log_error_errno(r, "Failed to determine our own network namespace ID: %m");
+
+ if (id != st.st_ino)
+ return log_error_errno(SYNTHETIC_ERRNO(EREMOTE),
+ "networkctl must be invoked in same network namespace as systemd-networkd.service.");
+
+ return 0;
+}
+
+static void warn_networkd_missing(void) {
+
+ if (access("/run/systemd/netif/state", F_OK) >= 0)
+ return;
+
+ fprintf(stderr, "WARNING: systemd-networkd is not running, output will be incomplete.\n\n");
+}
+
+static int run(int argc, char* argv[]) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ log_setup();
+
+ sigbus_install();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect system bus: %m");
+
+ r = check_netns_match(bus);
+ if (r < 0)
+ return r;
+
+ warn_networkd_missing();
+
+ return networkctl_main(bus, argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/network/networkd-address-generation.c b/src/network/networkd-address-generation.c
new file mode 100644
index 0000000..769cccf
--- /dev/null
+++ b/src/network/networkd-address-generation.c
@@ -0,0 +1,440 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if_arp.h>
+
+#include "sd-id128.h"
+
+#include "arphrd-util.h"
+#include "id128-util.h"
+#include "memory-util.h"
+#include "networkd-address-generation.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "string-util.h"
+
+#define DAD_CONFLICTS_IDGEN_RETRIES_RFC7217 3
+
+/* https://www.iana.org/assignments/ipv6-interface-ids/ipv6-interface-ids.xml */
+#define SUBNET_ROUTER_ANYCAST_ADDRESS ((const struct in6_addr) { .s6_addr = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } })
+#define SUBNET_ROUTER_ANYCAST_PREFIXLEN 64
+#define RESERVED_INTERFACE_IDENTIFIERS_ADDRESS ((const struct in6_addr) { .s6_addr = { 0x02, 0x00, 0x5E, 0xFF, 0xFE } })
+#define RESERVED_INTERFACE_IDENTIFIERS_PREFIXLEN 40
+#define RESERVED_SUBNET_ANYCAST_ADDRESSES ((const struct in6_addr) { .s6_addr = { 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80 } })
+#define RESERVED_SUBNET_ANYCAST_PREFIXLEN 57
+
+#define DHCP_PD_APP_ID SD_ID128_MAKE(fb,b9,37,ca,4a,ed,4a,4d,b0,70,7f,aa,71,c0,c9,85)
+#define NDISC_APP_ID SD_ID128_MAKE(13,ac,81,a7,d5,3f,49,78,92,79,5d,0c,29,3a,bc,7e)
+#define RADV_APP_ID SD_ID128_MAKE(1f,1e,90,c8,5c,78,4f,dc,8e,61,2d,59,0d,53,c1,25)
+
+typedef enum AddressGenerationType {
+ ADDRESS_GENERATION_EUI64,
+ ADDRESS_GENERATION_STATIC,
+ ADDRESS_GENERATION_PREFIXSTABLE,
+ _ADDRESS_GENERATION_TYPE_MAX,
+ _ADDRESS_GENERATION_TYPE_INVALID = -EINVAL,
+} AddressGenerationType;
+
+typedef struct IPv6Token {
+ AddressGenerationType type;
+ struct in6_addr address;
+ sd_id128_t secret_key;
+} IPv6Token;
+
+static int generate_eui64_address(const Link *link, const struct in6_addr *prefix, struct in6_addr *ret) {
+ assert(link);
+ assert(prefix);
+ assert(ret);
+
+ memcpy(ret->s6_addr, prefix, 8);
+
+ switch (link->iftype) {
+ case ARPHRD_INFINIBAND:
+ /* Use last 8 byte. See RFC4391 section 8 */
+ memcpy(&ret->s6_addr[8], &link->hw_addr.infiniband[INFINIBAND_ALEN - 8], 8);
+ break;
+ case ARPHRD_ETHER:
+ /* see RFC4291 section 2.5.1 */
+ ret->s6_addr[8] = link->hw_addr.ether.ether_addr_octet[0];
+ ret->s6_addr[9] = link->hw_addr.ether.ether_addr_octet[1];
+ ret->s6_addr[10] = link->hw_addr.ether.ether_addr_octet[2];
+ ret->s6_addr[11] = 0xff;
+ ret->s6_addr[12] = 0xfe;
+ ret->s6_addr[13] = link->hw_addr.ether.ether_addr_octet[3];
+ ret->s6_addr[14] = link->hw_addr.ether.ether_addr_octet[4];
+ ret->s6_addr[15] = link->hw_addr.ether.ether_addr_octet[5];
+ break;
+ default:
+ return log_link_debug_errno(link, SYNTHETIC_ERRNO(EINVAL),
+ "Token=eui64 is not supported for interface type %s, ignoring.",
+ strna(arphrd_to_name(link->iftype)));
+ }
+
+ ret->s6_addr[8] ^= 1 << 1;
+ return 0;
+}
+
+static bool stable_private_address_is_valid(const struct in6_addr *addr) {
+ assert(addr);
+
+ /* According to rfc4291, generated address should not be in the following ranges. */
+
+ if (in6_addr_prefix_covers(&SUBNET_ROUTER_ANYCAST_ADDRESS, SUBNET_ROUTER_ANYCAST_PREFIXLEN, addr))
+ return false;
+
+ if (in6_addr_prefix_covers(&RESERVED_INTERFACE_IDENTIFIERS_ADDRESS, RESERVED_INTERFACE_IDENTIFIERS_PREFIXLEN, addr))
+ return false;
+
+ if (in6_addr_prefix_covers(&RESERVED_SUBNET_ANYCAST_ADDRESSES, RESERVED_SUBNET_ANYCAST_PREFIXLEN, addr))
+ return false;
+
+ return true;
+}
+
+static void generate_stable_private_address_one(
+ Link *link,
+ const sd_id128_t *secret_key,
+ const struct in6_addr *prefix,
+ uint8_t dad_counter,
+ struct in6_addr *ret) {
+
+ struct siphash state;
+ uint64_t rid;
+
+ assert(link);
+ assert(secret_key);
+ assert(prefix);
+ assert(ret);
+
+ /* According to RFC7217 section 5.1
+ * RID = F(Prefix, Net_Iface, Network_ID, DAD_Counter, secret_key) */
+
+ siphash24_init(&state, secret_key->bytes);
+
+ siphash24_compress(prefix, 8, &state);
+ siphash24_compress_string(link->ifname, &state);
+ if (link->iftype == ARPHRD_INFINIBAND)
+ /* Only last 8 bytes of IB MAC are stable */
+ siphash24_compress(&link->hw_addr.infiniband[INFINIBAND_ALEN - 8], 8, &state);
+ else
+ siphash24_compress(link->hw_addr.bytes, link->hw_addr.length, &state);
+ siphash24_compress(&dad_counter, sizeof(uint8_t), &state);
+
+ rid = htole64(siphash24_finalize(&state));
+
+ memcpy(ret->s6_addr, prefix->s6_addr, 8);
+ memcpy(ret->s6_addr + 8, &rid, 8);
+}
+
+static int generate_stable_private_address(
+ Link *link,
+ const sd_id128_t *app_id,
+ const sd_id128_t *secret_key,
+ const struct in6_addr *prefix,
+ struct in6_addr *ret) {
+
+ sd_id128_t secret_machine_key;
+ struct in6_addr addr;
+ uint8_t i;
+ int r;
+
+ assert(link);
+ assert(app_id);
+ assert(secret_key);
+ assert(prefix);
+ assert(ret);
+
+ if (sd_id128_is_null(*secret_key)) {
+ r = sd_id128_get_machine_app_specific(*app_id, &secret_machine_key);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to generate secret key for IPv6 stable private address: %m");
+
+ secret_key = &secret_machine_key;
+ }
+
+ /* While this loop uses dad_counter and a retry limit as specified in RFC 7217, the loop does
+ * not actually attempt Duplicate Address Detection; the counter will be incremented only when
+ * the address generation algorithm produces an invalid address, and the loop may exit with an
+ * address which ends up being unusable due to duplication on the link. */
+ for (i = 0; i < DAD_CONFLICTS_IDGEN_RETRIES_RFC7217; i++) {
+ generate_stable_private_address_one(link, secret_key, prefix, i, &addr);
+
+ if (stable_private_address_is_valid(&addr))
+ break;
+ }
+ if (i >= DAD_CONFLICTS_IDGEN_RETRIES_RFC7217)
+ /* propagate recognizable errors. */
+ return log_link_debug_errno(link, SYNTHETIC_ERRNO(ENOANO),
+ "Failed to generate stable private address.");
+
+ *ret = addr;
+ return 0;
+}
+
+static int generate_addresses(
+ Link *link,
+ Set *tokens,
+ const sd_id128_t *app_id,
+ const struct in6_addr *prefix,
+ uint8_t prefixlen,
+ Set **ret) {
+
+ _cleanup_set_free_ Set *addresses = NULL;
+ struct in6_addr masked;
+ IPv6Token *j;
+ int r;
+
+ assert(link);
+ assert(app_id);
+ assert(prefix);
+ assert(prefixlen > 0 && prefixlen <= 64);
+ assert(ret);
+
+ masked = *prefix;
+ in6_addr_mask(&masked, prefixlen);
+
+ SET_FOREACH(j, tokens) {
+ struct in6_addr addr, *copy;
+
+ switch (j->type) {
+ case ADDRESS_GENERATION_EUI64:
+ if (generate_eui64_address(link, &masked, &addr) < 0)
+ continue;
+ break;
+
+ case ADDRESS_GENERATION_STATIC:
+ memcpy(addr.s6_addr, masked.s6_addr, 8);
+ memcpy(addr.s6_addr + 8, j->address.s6_addr + 8, 8);
+ break;
+
+ case ADDRESS_GENERATION_PREFIXSTABLE:
+ if (in6_addr_is_set(&j->address) && !in6_addr_equal(&j->address, &masked))
+ continue;
+
+ if (generate_stable_private_address(link, app_id, &j->secret_key, &masked, &addr) < 0)
+ continue;
+
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ copy = newdup(struct in6_addr, &addr, 1);
+ if (!copy)
+ return -ENOMEM;
+
+ r = set_ensure_consume(&addresses, &in6_addr_hash_ops_free, copy);
+ if (r < 0)
+ return r;
+ }
+
+ /* fall back to EUI-64 if no token is provided */
+ if (set_isempty(addresses)) {
+ _cleanup_free_ struct in6_addr *addr = NULL;
+
+ addr = new(struct in6_addr, 1);
+ if (!addr)
+ return -ENOMEM;
+
+ if (IN_SET(link->iftype, ARPHRD_ETHER, ARPHRD_INFINIBAND))
+ r = generate_eui64_address(link, &masked, addr);
+ else
+ r = generate_stable_private_address(link, app_id, &SD_ID128_NULL, &masked, addr);
+ if (r < 0)
+ return r;
+
+ r = set_ensure_consume(&addresses, &in6_addr_hash_ops_free, TAKE_PTR(addr));
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(addresses);
+ return 0;
+}
+
+int dhcp_pd_generate_addresses(Link *link, const struct in6_addr *prefix, Set **ret) {
+ return generate_addresses(link, link->network->dhcp_pd_tokens, &DHCP_PD_APP_ID, prefix, 64, ret);
+}
+
+int ndisc_generate_addresses(Link *link, const struct in6_addr *prefix, uint8_t prefixlen, Set **ret) {
+ return generate_addresses(link, link->network->ndisc_tokens, &NDISC_APP_ID, prefix, prefixlen, ret);
+}
+
+int radv_generate_addresses(Link *link, Set *tokens, const struct in6_addr *prefix, uint8_t prefixlen, Set **ret) {
+ return generate_addresses(link, tokens, &RADV_APP_ID, prefix, prefixlen, ret);
+}
+
+static void ipv6_token_hash_func(const IPv6Token *p, struct siphash *state) {
+ siphash24_compress(&p->type, sizeof(p->type), state);
+ siphash24_compress(&p->address, sizeof(p->address), state);
+ id128_hash_func(&p->secret_key, state);
+}
+
+static int ipv6_token_compare_func(const IPv6Token *a, const IPv6Token *b) {
+ int r;
+
+ r = CMP(a->type, b->type);
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a->address, &b->address, sizeof(struct in6_addr));
+ if (r != 0)
+ return r;
+
+ return id128_compare_func(&a->secret_key, &b->secret_key);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ ipv6_token_hash_ops,
+ IPv6Token,
+ ipv6_token_hash_func,
+ ipv6_token_compare_func,
+ free);
+
+static int ipv6_token_add(Set **tokens, AddressGenerationType type, const struct in6_addr *addr, const sd_id128_t *secret_key) {
+ IPv6Token *p;
+
+ assert(tokens);
+ assert(type >= 0 && type < _ADDRESS_GENERATION_TYPE_MAX);
+ assert(addr);
+ assert(secret_key);
+
+ p = new(IPv6Token, 1);
+ if (!p)
+ return -ENOMEM;
+
+ *p = (IPv6Token) {
+ .type = type,
+ .address = *addr,
+ .secret_key = *secret_key,
+ };
+
+ return set_ensure_consume(tokens, &ipv6_token_hash_ops, p);
+}
+
+int config_parse_address_generation_type(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *addr_alloc = NULL;
+ sd_id128_t secret_key = SD_ID128_NULL;
+ union in_addr_union buffer = {};
+ AddressGenerationType type;
+ Set **tokens = ASSERT_PTR(data);
+ const char *addr;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *tokens = set_free(*tokens);
+ return 0;
+ }
+
+ if ((addr = startswith(rvalue, "prefixstable"))) {
+ const char *comma;
+
+ type = ADDRESS_GENERATION_PREFIXSTABLE;
+
+ if (*addr == ':') {
+ addr++;
+
+ comma = strchr(addr, ',');
+ if (comma) {
+ addr_alloc = strndup(addr, comma - addr);
+ if (!addr_alloc)
+ return log_oom();
+
+ addr = addr_alloc;
+ }
+ } else if (*addr == ',')
+ comma = TAKE_PTR(addr);
+ else if (*addr == '\0') {
+ comma = NULL;
+ addr = NULL;
+ } else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid IPv6 token mode in %s=, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (comma) {
+ r = sd_id128_from_string(comma + 1, &secret_key);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse secret key in %s=, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (sd_id128_is_null(secret_key)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Secret key in %s= cannot be null, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ }
+
+ } else if (streq(rvalue, "eui64")) {
+ type = ADDRESS_GENERATION_EUI64;
+ addr = NULL;
+ } else {
+ type = ADDRESS_GENERATION_STATIC;
+
+ addr = startswith(rvalue, "static:");
+ if (!addr)
+ addr = rvalue;
+ }
+
+ if (addr) {
+ r = in_addr_from_string(AF_INET6, addr, &buffer);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse IP address in %s=, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ }
+
+ switch (type) {
+ case ADDRESS_GENERATION_EUI64:
+ assert(in6_addr_is_null(&buffer.in6));
+ break;
+
+ case ADDRESS_GENERATION_STATIC:
+ /* Only last 64 bits are used. */
+ memzero(buffer.in6.s6_addr, 8);
+
+ if (in6_addr_is_null(&buffer.in6)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "IPv6 address in %s= cannot be the ANY address, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ break;
+
+ case ADDRESS_GENERATION_PREFIXSTABLE:
+ /* At most, the initial 64 bits are used. */
+ (void) in6_addr_mask(&buffer.in6, 64);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ r = ipv6_token_add(tokens, type, &buffer.in6, &secret_key);
+ if (r < 0)
+ return log_oom();
+
+ return 0;
+}
diff --git a/src/network/networkd-address-generation.h b/src/network/networkd-address-generation.h
new file mode 100644
index 0000000..901b2ec
--- /dev/null
+++ b/src/network/networkd-address-generation.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "set.h"
+
+typedef struct Link Link;
+
+int dhcp_pd_generate_addresses(Link *link, const struct in6_addr *prefix, Set **ret);
+int ndisc_generate_addresses(Link *link, const struct in6_addr *prefix, uint8_t prefixlen, Set **ret);
+int radv_generate_addresses(Link *link, Set *tokens, const struct in6_addr *prefix, uint8_t prefixlen, Set **ret);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_address_generation_type);
diff --git a/src/network/networkd-address-label.c b/src/network/networkd-address-label.c
new file mode 100644
index 0000000..745b959
--- /dev/null
+++ b/src/network/networkd-address-label.c
@@ -0,0 +1,298 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <linux/if_addrlabel.h>
+
+#include "alloc-util.h"
+#include "netlink-util.h"
+#include "networkd-address-label.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "parse-util.h"
+
+AddressLabel *address_label_free(AddressLabel *label) {
+ if (!label)
+ return NULL;
+
+ if (label->network) {
+ assert(label->section);
+ hashmap_remove(label->network->address_labels_by_section, label->section);
+ }
+
+ config_section_free(label->section);
+ return mfree(label);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(AddressLabel, address_label_free);
+
+static int address_label_new_static(Network *network, const char *filename, unsigned section_line, AddressLabel **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(address_label_freep) AddressLabel *label = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ label = hashmap_get(network->address_labels_by_section, n);
+ if (label) {
+ *ret = TAKE_PTR(label);
+ return 0;
+ }
+
+ label = new(AddressLabel, 1);
+ if (!label)
+ return -ENOMEM;
+
+ *label = (AddressLabel) {
+ .network = network,
+ .section = TAKE_PTR(n),
+ .label = UINT32_MAX,
+ };
+
+ r = hashmap_ensure_put(&network->address_labels_by_section, &config_section_hash_ops, label->section, label);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(label);
+ return 0;
+}
+
+static int address_label_configure_handler(
+ sd_netlink *rtnl,
+ sd_netlink_message *m,
+ Request *req,
+ Link *link,
+ void *userdata) {
+
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set address label");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->static_address_label_messages == 0) {
+ log_link_debug(link, "Addresses label set");
+ link->static_address_labels_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int address_label_configure(AddressLabel *label, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(label);
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(req);
+
+ r = sd_rtnl_message_new_addrlabel(link->manager->rtnl, &m, RTM_NEWADDRLABEL,
+ link->ifindex, AF_INET6);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_addrlabel_set_prefixlen(m, label->prefixlen);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, IFAL_LABEL, label->label);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in6_addr(m, IFA_ADDRESS, &label->prefix);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static int address_label_process_request(Request *req, Link *link, void *userdata) {
+ AddressLabel *label = ASSERT_PTR(userdata);
+ int r;
+
+ assert(req);
+ assert(link);
+
+ if (!link_is_ready_to_configure(link, false))
+ return 0;
+
+ r = address_label_configure(label, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure address label: %m");
+
+ return 1;
+}
+
+int link_request_static_address_labels(Link *link) {
+ AddressLabel *label;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->static_address_labels_configured = false;
+
+ HASHMAP_FOREACH(label, link->network->address_labels_by_section) {
+ r = link_queue_request_full(link, REQUEST_TYPE_ADDRESS_LABEL,
+ label, NULL, trivial_hash_func, trivial_compare_func,
+ address_label_process_request,
+ &link->static_address_label_messages,
+ address_label_configure_handler, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request address label: %m");
+ }
+
+ if (link->static_address_label_messages == 0) {
+ link->static_address_labels_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Setting address labels.");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+static int address_label_section_verify(AddressLabel *label) {
+ assert(label);
+ assert(label->section);
+
+ if (section_is_invalid(label->section))
+ return -EINVAL;
+
+ if (!label->prefix_set)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: [IPv6AddressLabel] section without Prefix= setting specified. "
+ "Ignoring [IPv6AddressLabel] section from line %u.",
+ label->section->filename, label->section->line);
+
+ if (label->label == UINT32_MAX)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: [IPv6AddressLabel] section without Label= setting specified. "
+ "Ignoring [IPv6AddressLabel] section from line %u.",
+ label->section->filename, label->section->line);
+
+ return 0;
+}
+
+void network_drop_invalid_address_labels(Network *network) {
+ AddressLabel *label;
+
+ assert(network);
+
+ HASHMAP_FOREACH(label, network->address_labels_by_section)
+ if (address_label_section_verify(label) < 0)
+ address_label_free(label);
+}
+
+int config_parse_address_label_prefix(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(address_label_free_or_set_invalidp) AddressLabel *n = NULL;
+ Network *network = userdata;
+ unsigned char prefixlen;
+ union in_addr_union a;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_label_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_prefix_from_string(rvalue, AF_INET6, &a, &prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid prefix for address label, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (in6_addr_is_ipv4_mapped_address(&a.in6) && prefixlen > 96) {
+ /* See ip6addrlbl_alloc() in net/ipv6/addrlabel.c of kernel. */
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "The prefix length of IPv4 mapped address for address label must be equal to or smaller than 96, "
+ "ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ n->prefix = a.in6;
+ n->prefixlen = prefixlen;
+ n->prefix_set = true;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_address_label(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(address_label_free_or_set_invalidp) AddressLabel *n = NULL;
+ Network *network = userdata;
+ uint32_t k;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_label_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = safe_atou32(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse address label, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (k == UINT_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Address label is invalid, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ n->label = k;
+ TAKE_PTR(n);
+
+ return 0;
+}
diff --git a/src/network/networkd-address-label.h b/src/network/networkd-address-label.h
new file mode 100644
index 0000000..1e2ee70
--- /dev/null
+++ b/src/network/networkd-address-label.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+typedef struct AddressLabel {
+ Network *network;
+ ConfigSection *section;
+
+ uint32_t label;
+ struct in6_addr prefix;
+ unsigned char prefixlen;
+ bool prefix_set;
+} AddressLabel;
+
+AddressLabel *address_label_free(AddressLabel *label);
+
+void network_drop_invalid_address_labels(Network *network);
+
+int link_request_static_address_labels(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_address_label);
+CONFIG_PARSER_PROTOTYPE(config_parse_address_label_prefix);
diff --git a/src/network/networkd-address-pool.c b/src/network/networkd-address-pool.c
new file mode 100644
index 0000000..51f092a
--- /dev/null
+++ b/src/network/networkd-address-pool.c
@@ -0,0 +1,175 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "networkd-address-pool.h"
+#include "networkd-address.h"
+#include "networkd-manager.h"
+#include "set.h"
+#include "string-util.h"
+
+#define RANDOM_PREFIX_TRIAL_MAX 1024
+
+static int address_pool_new(
+ Manager *m,
+ int family,
+ const union in_addr_union *u,
+ unsigned prefixlen) {
+
+ _cleanup_free_ AddressPool *p = NULL;
+ int r;
+
+ assert(m);
+ assert(u);
+
+ p = new(AddressPool, 1);
+ if (!p)
+ return -ENOMEM;
+
+ *p = (AddressPool) {
+ .manager = m,
+ .family = family,
+ .prefixlen = prefixlen,
+ .in_addr = *u,
+ };
+
+ r = ordered_set_ensure_put(&m->address_pools, NULL, p);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(p);
+ return 0;
+}
+
+static int address_pool_new_from_string(
+ Manager *m,
+ int family,
+ const char *p,
+ unsigned prefixlen) {
+
+ union in_addr_union u;
+ int r;
+
+ assert(m);
+ assert(p);
+
+ r = in_addr_from_string(family, p, &u);
+ if (r < 0)
+ return r;
+
+ return address_pool_new(m, family, &u, prefixlen);
+}
+
+int address_pool_setup_default(Manager *m) {
+ int r;
+
+ assert(m);
+
+ /* Add in the well-known private address ranges. */
+ r = address_pool_new_from_string(m, AF_INET6, "fd00::", 8);
+ if (r < 0)
+ return r;
+
+ r = address_pool_new_from_string(m, AF_INET, "192.168.0.0", 16);
+ if (r < 0)
+ return r;
+
+ r = address_pool_new_from_string(m, AF_INET, "172.16.0.0", 12);
+ if (r < 0)
+ return r;
+
+ r = address_pool_new_from_string(m, AF_INET, "10.0.0.0", 8);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static bool address_pool_prefix_is_taken(
+ AddressPool *p,
+ const union in_addr_union *u,
+ unsigned prefixlen) {
+
+ Link *l;
+ Network *n;
+
+ assert(p);
+ assert(u);
+
+ HASHMAP_FOREACH(l, p->manager->links_by_index) {
+ Address *a;
+
+ /* Don't clash with assigned addresses */
+ SET_FOREACH(a, l->addresses) {
+ if (a->family != p->family)
+ continue;
+
+ if (in_addr_prefix_intersect(p->family, u, prefixlen, &a->in_addr, a->prefixlen))
+ return true;
+ }
+ }
+
+ /* And don't clash with configured but un-assigned addresses either */
+ ORDERED_HASHMAP_FOREACH(n, p->manager->networks) {
+ Address *a;
+
+ ORDERED_HASHMAP_FOREACH(a, n->addresses_by_section) {
+ if (a->family != p->family)
+ continue;
+
+ if (in_addr_prefix_intersect(p->family, u, prefixlen, &a->in_addr, a->prefixlen))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static int address_pool_acquire_one(AddressPool *p, int family, unsigned prefixlen, union in_addr_union *found) {
+ union in_addr_union u;
+ int r;
+
+ assert(p);
+ assert(prefixlen > 0);
+ assert(found);
+
+ if (p->family != family)
+ return 0;
+
+ if (p->prefixlen >= prefixlen)
+ return 0;
+
+ u = p->in_addr;
+
+ for (unsigned i = 0; i < RANDOM_PREFIX_TRIAL_MAX; i++) {
+ r = in_addr_random_prefix(p->family, &u, p->prefixlen, prefixlen);
+ if (r <= 0)
+ return r;
+
+ if (!address_pool_prefix_is_taken(p, &u, prefixlen)) {
+ log_debug("Found range %s", IN_ADDR_PREFIX_TO_STRING(p->family, &u, prefixlen));
+
+ *found = u;
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int address_pool_acquire(Manager *m, int family, unsigned prefixlen, union in_addr_union *found) {
+ AddressPool *p;
+ int r;
+
+ assert(m);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(prefixlen > 0);
+ assert(found);
+
+ ORDERED_SET_FOREACH(p, m->address_pools) {
+ r = address_pool_acquire_one(p, family, prefixlen, found);
+ if (r != 0)
+ return r;
+ }
+
+ return 0;
+}
diff --git a/src/network/networkd-address-pool.h b/src/network/networkd-address-pool.h
new file mode 100644
index 0000000..93bdec8
--- /dev/null
+++ b/src/network/networkd-address-pool.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "in-addr-util.h"
+
+typedef struct Manager Manager;
+
+typedef struct AddressPool {
+ Manager *manager;
+
+ int family;
+ unsigned prefixlen;
+ union in_addr_union in_addr;
+} AddressPool;
+
+int address_pool_setup_default(Manager *m);
+int address_pool_acquire(Manager *m, int family, unsigned prefixlen, union in_addr_union *found);
diff --git a/src/network/networkd-address.c b/src/network/networkd-address.c
new file mode 100644
index 0000000..3521cd8
--- /dev/null
+++ b/src/network/networkd-address.c
@@ -0,0 +1,2182 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <net/if_arp.h>
+
+#include "alloc-util.h"
+#include "firewall-util.h"
+#include "memory-util.h"
+#include "netlink-util.h"
+#include "networkd-address-pool.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-server.h"
+#include "networkd-ipv4acd.h"
+#include "networkd-manager.h"
+#include "networkd-netlabel.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "networkd-route-util.h"
+#include "networkd-route.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+
+#define ADDRESSES_PER_LINK_MAX 2048U
+#define STATIC_ADDRESSES_PER_NETWORK_MAX 1024U
+
+int address_flags_to_string_alloc(uint32_t flags, int family, char **ret) {
+ _cleanup_free_ char *str = NULL;
+ static const char* map[] = {
+ [LOG2U(IFA_F_SECONDARY)] = "secondary", /* This is also called "temporary" for ipv6. */
+ [LOG2U(IFA_F_NODAD)] = "nodad",
+ [LOG2U(IFA_F_OPTIMISTIC)] = "optimistic",
+ [LOG2U(IFA_F_DADFAILED)] = "dadfailed",
+ [LOG2U(IFA_F_HOMEADDRESS)] = "home-address",
+ [LOG2U(IFA_F_DEPRECATED)] = "deprecated",
+ [LOG2U(IFA_F_TENTATIVE)] = "tentative",
+ [LOG2U(IFA_F_PERMANENT)] = "permanent",
+ [LOG2U(IFA_F_MANAGETEMPADDR)] = "manage-temporary-address",
+ [LOG2U(IFA_F_NOPREFIXROUTE)] = "no-prefixroute",
+ [LOG2U(IFA_F_MCAUTOJOIN)] = "auto-join",
+ [LOG2U(IFA_F_STABLE_PRIVACY)] = "stable-privacy",
+ };
+
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(ret);
+
+ for (size_t i = 0; i < ELEMENTSOF(map); i++)
+ if (FLAGS_SET(flags, 1 << i) && map[i])
+ if (!strextend_with_separator(
+ &str, ",",
+ family == AF_INET6 && (1 << i) == IFA_F_SECONDARY ? "temporary" : map[i]))
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(str);
+ return 0;
+}
+
+int address_new(Address **ret) {
+ _cleanup_(address_freep) Address *address = NULL;
+
+ address = new(Address, 1);
+ if (!address)
+ return -ENOMEM;
+
+ *address = (Address) {
+ .family = AF_UNSPEC,
+ .scope = RT_SCOPE_UNIVERSE,
+ .lifetime_valid_usec = USEC_INFINITY,
+ .lifetime_preferred_usec = USEC_INFINITY,
+ .set_broadcast = -1,
+ };
+
+ *ret = TAKE_PTR(address);
+
+ return 0;
+}
+
+static int address_new_static(Network *network, const char *filename, unsigned section_line, Address **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(address_freep) Address *address = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ address = ordered_hashmap_get(network->addresses_by_section, n);
+ if (address) {
+ *ret = TAKE_PTR(address);
+ return 0;
+ }
+
+ if (ordered_hashmap_size(network->addresses_by_section) >= STATIC_ADDRESSES_PER_NETWORK_MAX)
+ return -E2BIG;
+
+ r = address_new(&address);
+ if (r < 0)
+ return r;
+
+ address->network = network;
+ address->section = TAKE_PTR(n);
+ address->source = NETWORK_CONFIG_SOURCE_STATIC;
+ /* This will be adjusted in address_section_verify(). */
+ address->duplicate_address_detection = _ADDRESS_FAMILY_INVALID;
+
+ r = ordered_hashmap_ensure_put(&network->addresses_by_section, &config_section_hash_ops, address->section, address);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(address);
+ return 0;
+}
+
+Address *address_free(Address *address) {
+ if (!address)
+ return NULL;
+
+ if (address->network) {
+ assert(address->section);
+ ordered_hashmap_remove(address->network->addresses_by_section, address->section);
+ }
+
+ if (address->link) {
+ set_remove(address->link->addresses, address);
+
+ if (address->family == AF_INET6 &&
+ in6_addr_equal(&address->in_addr.in6, &address->link->ipv6ll_address))
+ memzero(&address->link->ipv6ll_address, sizeof(struct in6_addr));
+ }
+
+ sd_ipv4acd_unref(address->acd);
+
+ config_section_free(address->section);
+ free(address->label);
+ free(address->netlabel);
+ return mfree(address);
+}
+
+static bool address_lifetime_is_valid(const Address *a) {
+ assert(a);
+
+ return
+ a->lifetime_valid_usec == USEC_INFINITY ||
+ a->lifetime_valid_usec > now(CLOCK_BOOTTIME);
+}
+
+bool address_is_ready(const Address *a) {
+ assert(a);
+
+ if (!ipv4acd_bound(a))
+ return false;
+
+ if (FLAGS_SET(a->flags, IFA_F_TENTATIVE))
+ return false;
+
+ if (FLAGS_SET(a->state, NETWORK_CONFIG_STATE_REMOVING))
+ return false;
+
+ if (!FLAGS_SET(a->state, NETWORK_CONFIG_STATE_CONFIGURED))
+ return false;
+
+ return address_lifetime_is_valid(a);
+}
+
+void link_mark_addresses(Link *link, NetworkConfigSource source) {
+ Address *a;
+
+ assert(link);
+
+ SET_FOREACH(a, link->addresses) {
+ if (a->source != source)
+ continue;
+
+ address_mark(a);
+ }
+}
+
+static bool address_needs_to_set_broadcast(const Address *a, Link *link) {
+ assert(a);
+ assert(link);
+
+ if (a->family != AF_INET)
+ return false;
+
+ if (in4_addr_is_set(&a->in_addr_peer.in))
+ return false;
+
+ /* A /31 or /32 IPv4 address does not have a broadcast address.
+ * See https://tools.ietf.org/html/rfc3021 */
+ if (a->prefixlen > 30)
+ return false;
+
+ /* If explicitly configured, do not update the address. */
+ if (in4_addr_is_set(&a->broadcast))
+ return false;
+
+ if (a->set_broadcast >= 0)
+ return a->set_broadcast;
+
+ /* Defaults to true, except for wireguard, as typical configuration for wireguard does not set
+ * broadcast. */
+ return !streq_ptr(link->kind, "wireguard");
+}
+
+void address_set_broadcast(Address *a, Link *link) {
+ assert(a);
+ assert(link);
+
+ if (!address_needs_to_set_broadcast(a, link))
+ return;
+
+ a->broadcast.s_addr = a->in_addr.in.s_addr | htobe32(UINT32_C(0xffffffff) >> a->prefixlen);
+}
+
+static void address_set_cinfo(Manager *m, const Address *a, struct ifa_cacheinfo *cinfo) {
+ usec_t now_usec;
+
+ assert(m);
+ assert(a);
+ assert(cinfo);
+
+ assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &now_usec) >= 0);
+
+ *cinfo = (struct ifa_cacheinfo) {
+ .ifa_valid = usec_to_sec(a->lifetime_valid_usec, now_usec),
+ .ifa_prefered = usec_to_sec(a->lifetime_preferred_usec, now_usec),
+ };
+}
+
+static void address_set_lifetime(Manager *m, Address *a, const struct ifa_cacheinfo *cinfo) {
+ usec_t now_usec;
+
+ assert(m);
+ assert(a);
+ assert(cinfo);
+
+ assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &now_usec) >= 0);
+
+ a->lifetime_valid_usec = sec_to_usec(cinfo->ifa_valid, now_usec);
+ a->lifetime_preferred_usec = sec_to_usec(cinfo->ifa_prefered, now_usec);
+}
+
+static uint32_t address_prefix(const Address *a) {
+ assert(a);
+
+ /* make sure we don't try to shift by 32.
+ * See ISO/IEC 9899:TC3 § 6.5.7.3. */
+ if (a->prefixlen == 0)
+ return 0;
+
+ if (a->in_addr_peer.in.s_addr != 0)
+ return be32toh(a->in_addr_peer.in.s_addr) >> (32 - a->prefixlen);
+ else
+ return be32toh(a->in_addr.in.s_addr) >> (32 - a->prefixlen);
+}
+
+static void address_kernel_hash_func(const Address *a, struct siphash *state) {
+ assert(a);
+
+ siphash24_compress(&a->family, sizeof(a->family), state);
+
+ switch (a->family) {
+ case AF_INET:
+ siphash24_compress(&a->prefixlen, sizeof(a->prefixlen), state);
+
+ uint32_t prefix = address_prefix(a);
+ siphash24_compress(&prefix, sizeof(prefix), state);
+
+ _fallthrough_;
+ case AF_INET6:
+ siphash24_compress(&a->in_addr, FAMILY_ADDRESS_SIZE(a->family), state);
+ break;
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ break;
+ }
+}
+
+static int address_kernel_compare_func(const Address *a1, const Address *a2) {
+ int r;
+
+ r = CMP(a1->family, a2->family);
+ if (r != 0)
+ return r;
+
+ switch (a1->family) {
+ case AF_INET:
+ /* See kernel's find_matching_ifa() in net/ipv4/devinet.c */
+ r = CMP(a1->prefixlen, a2->prefixlen);
+ if (r != 0)
+ return r;
+
+ r = CMP(address_prefix(a1), address_prefix(a2));
+ if (r != 0)
+ return r;
+
+ _fallthrough_;
+ case AF_INET6:
+ /* See kernel's ipv6_get_ifaddr() in net/ipv6/addrconf.c */
+ return memcmp(&a1->in_addr, &a2->in_addr, FAMILY_ADDRESS_SIZE(a1->family));
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ return 0;
+ }
+}
+
+DEFINE_PRIVATE_HASH_OPS(
+ address_kernel_hash_ops,
+ Address,
+ address_kernel_hash_func,
+ address_kernel_compare_func);
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ address_kernel_hash_ops_free,
+ Address,
+ address_kernel_hash_func,
+ address_kernel_compare_func,
+ address_free);
+
+/* The functions below are mainly used by managing Request. */
+static void address_hash_func(const Address *a, struct siphash *state) {
+ assert(a);
+
+ siphash24_compress(&a->family, sizeof(a->family), state);
+
+ /* treat any other address family as AF_UNSPEC */
+ if (!IN_SET(a->family, AF_INET, AF_INET6))
+ return;
+
+ siphash24_compress(&a->prefixlen, sizeof(a->prefixlen), state);
+ siphash24_compress(&a->in_addr, FAMILY_ADDRESS_SIZE(a->family), state);
+ siphash24_compress(&a->in_addr_peer, FAMILY_ADDRESS_SIZE(a->family), state);
+
+ if (a->family == AF_INET) {
+ /* On update, the kernel ignores the address label and broadcast address, hence we need
+ * to distinguish addresses with different labels or broadcast addresses. Otherwise,
+ * the label or broadcast address change will not be applied when we reconfigure the
+ * interface. */
+ siphash24_compress_string(a->label, state);
+ siphash24_compress(&a->broadcast, sizeof(a->broadcast), state);
+ }
+}
+
+int address_compare_func(const Address *a1, const Address *a2) {
+ int r;
+
+ r = CMP(a1->family, a2->family);
+ if (r != 0)
+ return r;
+
+ if (!IN_SET(a1->family, AF_INET, AF_INET6))
+ return 0;
+
+ r = CMP(a1->prefixlen, a2->prefixlen);
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a1->in_addr, &a2->in_addr, FAMILY_ADDRESS_SIZE(a1->family));
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a1->in_addr_peer, &a2->in_addr_peer, FAMILY_ADDRESS_SIZE(a1->family));
+ if (r != 0)
+ return r;
+
+ if (a1->family == AF_INET) {
+ r = strcmp_ptr(a1->label, a2->label);
+ if (r != 0)
+ return r;
+
+ r = CMP(a1->broadcast.s_addr, a2->broadcast.s_addr);
+ if (r != 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int address_equal(const Address *a1, const Address *a2) {
+ if (a1 == a2)
+ return true;
+
+ if (!a1 || !a2)
+ return false;
+
+ return address_compare_func(a1, a2) == 0;
+}
+
+static int address_equalify(Address *address, const Address *src) {
+ int r;
+
+ assert(address);
+ assert(src);
+
+ if (address_kernel_compare_func(address, src) != 0)
+ return -EINVAL;
+
+ if (address->family == AF_INET) {
+ address->broadcast = src->broadcast;
+ r = free_and_strdup(&address->label, src->label);
+ if (r < 0)
+ return r;
+ } else {
+ address->prefixlen = src->prefixlen;
+ address->in_addr_peer = src->in_addr_peer;
+ }
+
+ return 0;
+}
+
+int address_dup(const Address *src, Address **ret) {
+ _cleanup_(address_freep) Address *dest = NULL;
+ int r;
+
+ assert(src);
+ assert(ret);
+
+ dest = newdup(Address, src, 1);
+ if (!dest)
+ return -ENOMEM;
+
+ /* clear all pointers */
+ dest->network = NULL;
+ dest->section = NULL;
+ dest->link = NULL;
+ dest->label = NULL;
+ dest->acd = NULL;
+ dest->netlabel = NULL;
+
+ if (src->family == AF_INET) {
+ r = free_and_strdup(&dest->label, src->label);
+ if (r < 0)
+ return r;
+ }
+
+ r = free_and_strdup(&dest->netlabel, src->netlabel);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(dest);
+ return 0;
+}
+
+static int address_set_masquerade(Address *address, bool add) {
+ union in_addr_union masked;
+ int r;
+
+ assert(address);
+ assert(address->link);
+
+ if (!address->link->network)
+ return 0;
+
+ if (address->family == AF_INET &&
+ !FLAGS_SET(address->link->network->ip_masquerade, ADDRESS_FAMILY_IPV4))
+ return 0;
+
+ if (address->family == AF_INET6 &&
+ !FLAGS_SET(address->link->network->ip_masquerade, ADDRESS_FAMILY_IPV6))
+ return 0;
+
+ if (address->scope >= RT_SCOPE_LINK)
+ return 0;
+
+ if (address->ip_masquerade_done == add)
+ return 0;
+
+ masked = address->in_addr;
+ r = in_addr_mask(address->family, &masked, address->prefixlen);
+ if (r < 0)
+ return r;
+
+ r = fw_add_masquerade(&address->link->manager->fw_ctx, add, address->family, &masked, address->prefixlen);
+ if (r < 0)
+ return r;
+
+ address->ip_masquerade_done = add;
+
+ return 0;
+}
+
+static int address_add(Link *link, Address *address) {
+ int r;
+
+ assert(link);
+ assert(address);
+
+ r = set_ensure_put(&link->addresses, &address_kernel_hash_ops_free, address);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ address->link = link;
+ return 0;
+}
+
+static int address_update(Address *address) {
+ Link *link;
+ int r;
+
+ assert(address);
+ assert(address->link);
+
+ link = address->link;
+
+ if (address_is_ready(address) &&
+ address->family == AF_INET6 &&
+ in6_addr_is_link_local(&address->in_addr.in6) &&
+ in6_addr_is_null(&link->ipv6ll_address)) {
+
+ link->ipv6ll_address = address->in_addr.in6;
+
+ r = link_ipv6ll_gained(link);
+ if (r < 0)
+ return r;
+ }
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ r = address_set_masquerade(address, true);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not enable IP masquerading: %m");
+
+ address_add_netlabel(address);
+
+ if (address_is_ready(address) && address->callback) {
+ r = address->callback(address);
+ if (r < 0)
+ return r;
+ }
+
+ link_update_operstate(link, true);
+ link_check_ready(link);
+ return 0;
+}
+
+static int address_drop(Address *address) {
+ Link *link;
+ bool ready;
+ int r;
+
+ assert(address);
+ assert(address->link);
+
+ ready = address_is_ready(address);
+ link = address->link;
+
+ r = address_set_masquerade(address, false);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to disable IP masquerading, ignoring: %m");
+
+ address_del_netlabel(address);
+
+ if (address->state == 0)
+ address_free(address);
+
+ link_update_operstate(link, true);
+
+ if (link && !ready)
+ link_check_ready(link);
+
+ return 0;
+}
+
+int address_get(Link *link, const Address *in, Address **ret) {
+ Address *existing;
+
+ assert(link);
+ assert(in);
+
+ existing = set_get(link->addresses, in);
+ if (!existing)
+ return -ENOENT;
+
+ if (ret)
+ *ret = existing;
+ return 0;
+}
+
+int link_get_address(Link *link, int family, const union in_addr_union *address, unsigned char prefixlen, Address **ret) {
+ Address *a;
+ int r;
+
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(address);
+
+ /* This find an Address object on the link which matches the given address and prefix length
+ * and does not have peer address. When the prefixlen is zero, then an Address object with an
+ * arbitrary prefixlen will be returned. */
+
+ if (family == AF_INET6 || prefixlen != 0) {
+ _cleanup_(address_freep) Address *tmp = NULL;
+
+ /* In this case, we can use address_get(). */
+
+ r = address_new(&tmp);
+ if (r < 0)
+ return r;
+
+ tmp->family = family;
+ tmp->in_addr = *address;
+ tmp->prefixlen = prefixlen;
+
+ r = address_get(link, tmp, &a);
+ if (r < 0)
+ return r;
+
+ if (family == AF_INET6) {
+ /* IPv6 addresses are managed without peer address and prefix length. Hence, we need
+ * to check them explicitly. */
+ if (in_addr_is_set(family, &a->in_addr_peer))
+ return -ENOENT;
+ if (prefixlen != 0 && a->prefixlen != prefixlen)
+ return -ENOENT;
+ }
+
+ if (ret)
+ *ret = a;
+
+ return 0;
+ }
+
+ SET_FOREACH(a, link->addresses) {
+ if (a->family != family)
+ continue;
+
+ if (!in_addr_equal(family, &a->in_addr, address))
+ continue;
+
+ if (in_addr_is_set(family, &a->in_addr_peer))
+ continue;
+
+ if (ret)
+ *ret = a;
+
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+int manager_get_address(Manager *manager, int family, const union in_addr_union *address, unsigned char prefixlen, Address **ret) {
+ Link *link;
+
+ assert(manager);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(address);
+
+ HASHMAP_FOREACH(link, manager->links_by_index) {
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ continue;
+
+ if (link_get_address(link, family, address, prefixlen, ret) >= 0)
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+bool manager_has_address(Manager *manager, int family, const union in_addr_union *address, bool check_ready) {
+ Address *a;
+
+ assert(manager);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(address);
+
+ if (manager_get_address(manager, family, address, 0, &a) < 0)
+ return false;
+
+ return check_ready ? address_is_ready(a) : (address_exists(a) && address_lifetime_is_valid(a));
+}
+
+const char* format_lifetime(char *buf, size_t l, usec_t lifetime_usec) {
+ assert(buf);
+ assert(l > 4);
+
+ if (lifetime_usec == USEC_INFINITY)
+ return "forever";
+
+ sprintf(buf, "for ");
+ /* format_timespan() never fails */
+ assert_se(format_timespan(buf + 4, l - 4, usec_sub_unsigned(lifetime_usec, now(CLOCK_BOOTTIME)), USEC_PER_SEC));
+ return buf;
+}
+
+static void log_address_debug(const Address *address, const char *str, const Link *link) {
+ _cleanup_free_ char *state = NULL, *flags_str = NULL, *scope_str = NULL;
+
+ assert(address);
+ assert(str);
+ assert(link);
+
+ if (!DEBUG_LOGGING)
+ return;
+
+ (void) network_config_state_to_string_alloc(address->state, &state);
+
+ const char *peer = in_addr_is_set(address->family, &address->in_addr_peer) ?
+ IN_ADDR_TO_STRING(address->family, &address->in_addr_peer) : NULL;
+
+ (void) address_flags_to_string_alloc(address->flags, address->family, &flags_str);
+ (void) route_scope_to_string_alloc(address->scope, &scope_str);
+
+ log_link_debug(link, "%s %s address (%s): %s%s%s/%u (valid %s, preferred %s), flags: %s, scope: %s",
+ str, strna(network_config_source_to_string(address->source)), strna(state),
+ IN_ADDR_TO_STRING(address->family, &address->in_addr),
+ peer ? " peer " : "", strempty(peer), address->prefixlen,
+ FORMAT_LIFETIME(address->lifetime_valid_usec),
+ FORMAT_LIFETIME(address->lifetime_preferred_usec),
+ strna(flags_str), strna(scope_str));
+}
+
+static int address_set_netlink_message(const Address *address, sd_netlink_message *m, Link *link) {
+ uint32_t flags;
+ int r;
+
+ assert(address);
+ assert(m);
+ assert(link);
+
+ r = sd_rtnl_message_addr_set_prefixlen(m, address->prefixlen);
+ if (r < 0)
+ return r;
+
+ /* On remove, only IFA_F_MANAGETEMPADDR flag for IPv6 addresses are used. But anyway, set all
+ * flags except tentative flag here unconditionally. Without setting the flag, the template
+ * addresses generated by kernel will not be removed automatically when the main address is
+ * removed. */
+ flags = address->flags & ~IFA_F_TENTATIVE;
+ r = sd_rtnl_message_addr_set_flags(m, flags & 0xff);
+ if (r < 0)
+ return r;
+
+ if ((flags & ~0xff) != 0) {
+ r = sd_netlink_message_append_u32(m, IFA_FLAGS, flags);
+ if (r < 0)
+ return r;
+ }
+
+ r = netlink_message_append_in_addr_union(m, IFA_LOCAL, address->family, &address->in_addr);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int address_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EADDRNOTAVAIL)
+ log_link_message_warning_errno(link, m, r, "Could not drop address");
+
+ return 1;
+}
+
+int address_remove(Address *address) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ Link *link;
+ int r;
+
+ assert(address);
+ assert(IN_SET(address->family, AF_INET, AF_INET6));
+ assert(address->link);
+ assert(address->link->ifindex > 0);
+ assert(address->link->manager);
+ assert(address->link->manager->rtnl);
+
+ link = address->link;
+
+ log_address_debug(address, "Removing", link);
+
+ r = sd_rtnl_message_new_addr(link->manager->rtnl, &m, RTM_DELADDR,
+ link->ifindex, address->family);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not allocate RTM_DELADDR message: %m");
+
+ r = address_set_netlink_message(address, m, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set netlink attributes: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, m,
+ address_remove_handler,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not send rtnetlink message: %m");
+
+ link_ref(link);
+
+ address_enter_removing(address);
+
+ /* The operational state is determined by address state and carrier state. Hence, if we remove
+ * an address, the operational state may be changed. */
+ link_update_operstate(link, true);
+ return 0;
+}
+
+int address_remove_and_drop(Address *address) {
+ if (!address)
+ return 0;
+
+ address_cancel_request(address);
+
+ if (address_exists(address))
+ return address_remove(address);
+
+ return address_drop(address);
+}
+
+bool link_address_is_dynamic(const Link *link, const Address *address) {
+ Route *route;
+
+ assert(link);
+ assert(address);
+
+ if (address->lifetime_preferred_usec != USEC_INFINITY)
+ return true;
+
+ /* Even when the address is leased from a DHCP server, networkd assign the address
+ * without lifetime when KeepConfiguration=dhcp. So, let's check that we have
+ * corresponding routes with RTPROT_DHCP. */
+ SET_FOREACH(route, link->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ /* The route is not assigned yet, or already removed. Ignoring. */
+ if (!route_exists(route))
+ continue;
+
+ if (route->protocol != RTPROT_DHCP)
+ continue;
+
+ if (address->family != route->family)
+ continue;
+
+ if (in_addr_equal(address->family, &address->in_addr, &route->prefsrc))
+ return true;
+ }
+
+ return false;
+}
+
+int link_drop_ipv6ll_addresses(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ /* IPv6LL address may be in the tentative state, and in that case networkd has not received it.
+ * So, we need to dump all IPv6 addresses. */
+
+ if (link_may_have_ipv6ll(link, /* check_multicast = */ false))
+ return 0;
+
+ r = sd_rtnl_message_new_addr(link->manager->rtnl, &req, RTM_GETADDR, link->ifindex, AF_INET6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_set_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(link->manager->rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ for (sd_netlink_message *addr = reply; addr; addr = sd_netlink_message_next(addr)) {
+ _cleanup_(address_freep) Address *a = NULL;
+ unsigned char flags, prefixlen;
+ struct in6_addr address;
+ Address *existing;
+ int ifindex;
+
+ /* NETLINK_GET_STRICT_CHK socket option is supported since kernel 4.20. To support
+ * older kernels, we need to check ifindex here. */
+ r = sd_rtnl_message_addr_get_ifindex(addr, &ifindex);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "rtnl: received address message without valid ifindex, ignoring: %m");
+ continue;
+ } else if (link->ifindex != ifindex)
+ continue;
+
+ r = sd_rtnl_message_addr_get_flags(addr, &flags);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "rtnl: received address message without valid flags, ignoring: %m");
+ continue;
+ }
+
+ r = sd_rtnl_message_addr_get_prefixlen(addr, &prefixlen);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "rtnl: received address message without prefixlen, ignoring: %m");
+ continue;
+ }
+
+ if (sd_netlink_message_read_in6_addr(addr, IFA_LOCAL, NULL) >= 0)
+ /* address with peer, ignoring. */
+ continue;
+
+ r = sd_netlink_message_read_in6_addr(addr, IFA_ADDRESS, &address);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "rtnl: received address message without valid address, ignoring: %m");
+ continue;
+ }
+
+ if (!in6_addr_is_link_local(&address))
+ continue;
+
+ r = address_new(&a);
+ if (r < 0)
+ return -ENOMEM;
+
+ a->family = AF_INET6;
+ a->in_addr.in6 = address;
+ a->prefixlen = prefixlen;
+ a->flags = flags;
+
+ if (address_get(link, a, &existing) < 0) {
+ r = address_add(link, a);
+ if (r < 0)
+ return r;
+
+ existing = TAKE_PTR(a);
+ }
+
+ r = address_remove(existing);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int link_drop_foreign_addresses(Link *link) {
+ Address *address;
+ int k, r = 0;
+
+ assert(link);
+ assert(link->network);
+
+ /* First, mark all addresses. */
+ SET_FOREACH(address, link->addresses) {
+ /* We consider IPv6LL addresses to be managed by the kernel, or dropped in link_drop_ipv6ll_addresses() */
+ if (address->family == AF_INET6 && in6_addr_is_link_local(&address->in_addr.in6))
+ continue;
+
+ /* Do not remove localhost address (127.0.0.1 and ::1) */
+ if (link->flags & IFF_LOOPBACK && in_addr_is_localhost_one(address->family, &address->in_addr) > 0)
+ continue;
+
+ /* Ignore addresses we configured. */
+ if (address->source != NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ /* Ignore addresses not assigned yet or already removing. */
+ if (!address_exists(address))
+ continue;
+
+ /* link_address_is_dynamic() is slightly heavy. Let's call the function only when KeepConfiguration= is set. */
+ if (IN_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP, KEEP_CONFIGURATION_STATIC) &&
+ link_address_is_dynamic(link, address) == (link->network->keep_configuration == KEEP_CONFIGURATION_DHCP))
+ continue;
+
+ address_mark(address);
+ }
+
+ /* Then, unmark requested addresses. */
+ ORDERED_HASHMAP_FOREACH(address, link->network->addresses_by_section) {
+ Address *existing;
+
+ /* On update, the kernel ignores the address label and broadcast address. Hence we need to
+ * distinguish addresses with different labels or broadcast addresses. Thus, we need to check
+ * the existing address with address_equal(). Otherwise, the label or broadcast address
+ * change will not be applied when we reconfigure the interface. */
+ if (address_get(link, address, &existing) >= 0 && address_equal(address, existing))
+ address_unmark(existing);
+ }
+
+ /* Finally, remove all marked addresses. */
+ SET_FOREACH(address, link->addresses) {
+ if (!address_is_marked(address))
+ continue;
+
+ k = address_remove(address);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+int link_drop_managed_addresses(Link *link) {
+ Address *address;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses) {
+ /* Do not touch addresses managed by kernel or other tools. */
+ if (address->source == NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ /* Ignore addresses not assigned yet or already removing. */
+ if (!address_exists(address))
+ continue;
+
+ k = address_remove(address);
+ if (k < 0 && r >= 0) {
+ r = k;
+ continue;
+ }
+ }
+
+ return r;
+}
+
+void link_foreignize_addresses(Link *link) {
+ Address *address;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses)
+ address->source = NETWORK_CONFIG_SOURCE_FOREIGN;
+}
+
+static int address_acquire(Link *link, const Address *original, Address **ret) {
+ _cleanup_(address_freep) Address *na = NULL;
+ union in_addr_union in_addr;
+ int r;
+
+ assert(link);
+ assert(original);
+ assert(ret);
+
+ /* Something useful was configured? just use it */
+ if (in_addr_is_set(original->family, &original->in_addr)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ /* The address is configured to be 0.0.0.0 or [::] by the user?
+ * Then let's acquire something more useful from the pool. */
+ r = address_pool_acquire(link->manager, original->family, original->prefixlen, &in_addr);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EBUSY;
+
+ /* Pick first address in range for ourselves. */
+ if (original->family == AF_INET)
+ in_addr.in.s_addr = in_addr.in.s_addr | htobe32(1);
+ else if (original->family == AF_INET6)
+ in_addr.in6.s6_addr[15] |= 1;
+
+ r = address_dup(original, &na);
+ if (r < 0)
+ return r;
+
+ na->in_addr = in_addr;
+
+ *ret = TAKE_PTR(na);
+ return 1;
+}
+
+int address_configure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg) {
+ int r;
+
+ assert(rtnl);
+ assert(m);
+ assert(link);
+ assert(error_msg);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, error_msg);
+ link_enter_failed(link);
+ return 0;
+ }
+
+ return 1;
+}
+
+static int address_configure(const Address *address, const struct ifa_cacheinfo *c, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(address);
+ assert(IN_SET(address->family, AF_INET, AF_INET6));
+ assert(c);
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(req);
+
+ log_address_debug(address, "Configuring", link);
+
+ r = sd_rtnl_message_new_addr_update(link->manager->rtnl, &m, link->ifindex, address->family);
+ if (r < 0)
+ return r;
+
+ r = address_set_netlink_message(address, m, link);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_addr_set_scope(m, address->scope);
+ if (r < 0)
+ return r;
+
+ if (in_addr_is_set(address->family, &address->in_addr_peer)) {
+ r = netlink_message_append_in_addr_union(m, IFA_ADDRESS, address->family, &address->in_addr_peer);
+ if (r < 0)
+ return r;
+ } else if (in4_addr_is_set(&address->broadcast)) {
+ r = sd_netlink_message_append_in_addr(m, IFA_BROADCAST, &address->broadcast);
+ if (r < 0)
+ return r;
+ }
+
+ if (address->family == AF_INET && address->label) {
+ r = sd_netlink_message_append_string(m, IFA_LABEL, address->label);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_cache_info(m, IFA_CACHEINFO, c);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, IFA_RT_PRIORITY, address->route_metric);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static bool address_is_ready_to_configure(Link *link, const Address *address) {
+ assert(link);
+ assert(address);
+
+ if (!link_is_ready_to_configure(link, false))
+ return false;
+
+ if (address_is_removing(address))
+ return false;
+
+ if (!ipv4acd_bound(address))
+ return false;
+
+ /* Refuse adding more than the limit */
+ if (set_size(link->addresses) >= ADDRESSES_PER_LINK_MAX)
+ return false;
+
+ return true;
+}
+
+static int address_process_request(Request *req, Link *link, Address *address) {
+ struct ifa_cacheinfo c;
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(address);
+
+ if (!address_is_ready_to_configure(link, address))
+ return 0;
+
+ address_set_cinfo(link->manager, address, &c);
+ if (c.ifa_valid == 0) {
+ log_link_debug(link, "Refuse to configure %s address %s, as its valid lifetime is zero.",
+ network_config_source_to_string(address->source),
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen));
+ address_cancel_requesting(address);
+ return 1;
+ }
+
+ r = address_configure(address, &c, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure address: %m");
+
+ address_enter_configuring(address);
+ return 1;
+}
+
+int link_request_address(
+ Link *link,
+ Address *address,
+ bool consume_object,
+ unsigned *message_counter,
+ address_netlink_handler_t netlink_handler,
+ Request **ret) {
+
+ Address *acquired, *existing = NULL;
+ int r;
+
+ assert(link);
+ assert(address);
+ assert(address->source != NETWORK_CONFIG_SOURCE_FOREIGN);
+
+ r = address_acquire(link, address, &acquired);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to acquire an address from pool: %m");
+ if (r > 0) {
+ if (consume_object)
+ address_free(address);
+
+ address = acquired;
+ consume_object = true;
+ }
+
+ if (address_needs_to_set_broadcast(address, link)) {
+ if (!consume_object) {
+ Address *a;
+
+ r = address_dup(address, &a);
+ if (r < 0)
+ return r;
+
+ address = a;
+ consume_object = true;
+ }
+
+ address_set_broadcast(address, link);
+ }
+
+ (void) address_get(link, address, &existing);
+
+ if (address->lifetime_valid_usec == 0) {
+ if (consume_object)
+ address_free(address);
+
+ /* The requested address is outdated. Let's remove it. */
+ return address_remove_and_drop(existing);
+ }
+
+ if (!existing) {
+ _cleanup_(address_freep) Address *tmp = NULL;
+
+ if (consume_object)
+ tmp = address;
+ else {
+ r = address_dup(address, &tmp);
+ if (r < 0)
+ return r;
+ }
+
+ /* Consider address tentative until we get the real flags from the kernel */
+ tmp->flags |= IFA_F_TENTATIVE;
+
+ r = address_add(link, tmp);
+ if (r < 0)
+ return r;
+
+ existing = TAKE_PTR(tmp);
+ } else {
+ r = address_equalify(existing, address);
+ if (r < 0)
+ return r;
+ existing->source = address->source;
+ existing->provider = address->provider;
+ existing->duplicate_address_detection = address->duplicate_address_detection;
+ existing->lifetime_valid_usec = address->lifetime_valid_usec;
+ existing->lifetime_preferred_usec = address->lifetime_preferred_usec;
+ if (consume_object)
+ address_free(address);
+ }
+
+ r = ipv4acd_configure(existing);
+ if (r < 0)
+ return r;
+
+ log_address_debug(existing, "Requesting", link);
+ r = link_queue_request_safe(link, REQUEST_TYPE_ADDRESS,
+ existing, NULL,
+ address_hash_func,
+ address_compare_func,
+ address_process_request,
+ message_counter, netlink_handler, ret);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request address: %m");
+ if (r == 0)
+ return 0;
+
+ address_enter_requesting(existing);
+
+ return 1;
+}
+
+static int static_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) {
+ int r;
+
+ assert(link);
+
+ r = address_configure_handler_internal(rtnl, m, link, "Failed to set static address");
+ if (r <= 0)
+ return r;
+
+ if (link->static_address_messages == 0) {
+ log_link_debug(link, "Addresses set");
+ link->static_addresses_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+int link_request_static_address(Link *link, Address *address, bool consume) {
+ assert(link);
+ assert(address);
+ assert(address->source == NETWORK_CONFIG_SOURCE_STATIC);
+
+ return link_request_address(link, address, consume, &link->static_address_messages,
+ static_address_handler, NULL);
+}
+
+int link_request_static_addresses(Link *link) {
+ Address *a;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->static_addresses_configured = false;
+
+ ORDERED_HASHMAP_FOREACH(a, link->network->addresses_by_section) {
+ r = link_request_static_address(link, a, false);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_request_radv_addresses(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_dhcp_server_address(link);
+ if (r < 0)
+ return r;
+
+ if (link->static_address_messages == 0) {
+ link->static_addresses_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Setting addresses");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+void address_cancel_request(Address *address) {
+ Request req;
+
+ assert(address);
+ assert(address->link);
+
+ if (!address_is_requesting(address))
+ return;
+
+ req = (Request) {
+ .link = address->link,
+ .type = REQUEST_TYPE_ADDRESS,
+ .userdata = address,
+ .hash_func = (hash_func_t) address_hash_func,
+ .compare_func = (compare_func_t) address_compare_func,
+ };
+
+ request_detach(address->link->manager, &req);
+ address_cancel_requesting(address);
+}
+
+int manager_rtnl_process_address(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(address_freep) Address *tmp = NULL;
+ struct ifa_cacheinfo cinfo;
+ Link *link = NULL;
+ uint16_t type;
+ Address *address = NULL;
+ int ifindex, r;
+
+ assert(rtnl);
+ assert(message);
+ assert(m);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "rtnl: failed to receive address message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get message type, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(type, RTM_NEWADDR, RTM_DELADDR)) {
+ log_warning("rtnl: received unexpected message type %u when processing address, ignoring.", type);
+ return 0;
+ }
+
+ r = sd_rtnl_message_addr_get_ifindex(message, &ifindex);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m");
+ return 0;
+ } else if (ifindex <= 0) {
+ log_warning("rtnl: received address message with invalid ifindex %d, ignoring.", ifindex);
+ return 0;
+ }
+
+ r = link_get_by_index(m, ifindex, &link);
+ if (r < 0 || !link) {
+ /* when enumerating we might be out of sync, but we will get the address again, so just
+ * ignore it */
+ if (!m->enumerating)
+ log_warning("rtnl: received address for link '%d' we don't know about, ignoring.", ifindex);
+ return 0;
+ }
+
+ r = address_new(&tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_rtnl_message_addr_get_family(message, &tmp->family);
+ if (r < 0) {
+ log_link_warning(link, "rtnl: received address message without family, ignoring.");
+ return 0;
+ } else if (!IN_SET(tmp->family, AF_INET, AF_INET6)) {
+ log_link_debug(link, "rtnl: received address message with invalid family '%i', ignoring.", tmp->family);
+ return 0;
+ }
+
+ r = sd_rtnl_message_addr_get_prefixlen(message, &tmp->prefixlen);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received address message without prefixlen, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_addr_get_scope(message, &tmp->scope);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received address message without scope, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, IFA_FLAGS, &tmp->flags);
+ if (r == -ENODATA) {
+ unsigned char flags;
+
+ /* For old kernels. */
+ r = sd_rtnl_message_addr_get_flags(message, &flags);
+ if (r >= 0)
+ tmp->flags = flags;
+ }
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received address message without flags, ignoring: %m");
+ return 0;
+ }
+
+ switch (tmp->family) {
+ case AF_INET:
+ r = sd_netlink_message_read_in_addr(message, IFA_LOCAL, &tmp->in_addr.in);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received address message without valid address, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_in_addr(message, IFA_ADDRESS, &tmp->in_addr_peer.in);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: could not get peer address from address message, ignoring: %m");
+ return 0;
+ } else if (r >= 0) {
+ if (in4_addr_equal(&tmp->in_addr.in, &tmp->in_addr_peer.in))
+ tmp->in_addr_peer = IN_ADDR_NULL;
+ }
+
+ r = sd_netlink_message_read_in_addr(message, IFA_BROADCAST, &tmp->broadcast);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: could not get broadcast from address message, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string_strdup(message, IFA_LABEL, &tmp->label);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: could not get label from address message, ignoring: %m");
+ return 0;
+ } else if (r >= 0 && streq_ptr(tmp->label, link->ifname))
+ tmp->label = mfree(tmp->label);
+
+ break;
+
+ case AF_INET6:
+ r = sd_netlink_message_read_in6_addr(message, IFA_LOCAL, &tmp->in_addr.in6);
+ if (r >= 0) {
+ /* Have peer address. */
+ r = sd_netlink_message_read_in6_addr(message, IFA_ADDRESS, &tmp->in_addr_peer.in6);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: could not get peer address from address message, ignoring: %m");
+ return 0;
+ }
+ } else if (r == -ENODATA) {
+ /* Does not have peer address. */
+ r = sd_netlink_message_read_in6_addr(message, IFA_ADDRESS, &tmp->in_addr.in6);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received address message without valid address, ignoring: %m");
+ return 0;
+ }
+ } else {
+ log_link_warning_errno(link, r, "rtnl: could not get local address from address message, ignoring: %m");
+ return 0;
+ }
+
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ r = sd_netlink_message_read_cache_info(message, IFA_CACHEINFO, &cinfo);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: cannot get IFA_CACHEINFO attribute, ignoring: %m");
+ return 0;
+ }
+
+ (void) address_get(link, tmp, &address);
+
+ switch (type) {
+ case RTM_NEWADDR:
+ if (address) {
+ /* update flags and etc. */
+ r = address_equalify(address, tmp);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to update properties of address %s, ignoring: %m",
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen));
+ return 0;
+ }
+ address->flags = tmp->flags;
+ address->scope = tmp->scope;
+ address_set_lifetime(m, address, &cinfo);
+ address_enter_configured(address);
+ log_address_debug(address, "Received updated", link);
+ } else {
+ address_set_lifetime(m, tmp, &cinfo);
+ address_enter_configured(tmp);
+ log_address_debug(tmp, "Received new", link);
+
+ r = address_add(link, tmp);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to remember foreign address %s, ignoring: %m",
+ IN_ADDR_PREFIX_TO_STRING(tmp->family, &tmp->in_addr, tmp->prefixlen));
+ return 0;
+ }
+
+ address = TAKE_PTR(tmp);
+ }
+
+ /* address_update() logs internally, so we don't need to here. */
+ r = address_update(address);
+ if (r < 0)
+ link_enter_failed(link);
+
+ break;
+
+ case RTM_DELADDR:
+ if (address) {
+ address_enter_removed(address);
+ log_address_debug(address, address->state == 0 ? "Forgetting" : "Removed", link);
+ (void) address_drop(address);
+ } else
+ log_address_debug(tmp, "Kernel removed unknown", link);
+
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+int config_parse_broadcast(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ union in_addr_union u;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (isempty(rvalue)) {
+ /* The broadcast address will be calculated based on Address=, and set if the link is
+ * not a wireguard interface. Here, we do not check or set n->family. */
+ n->broadcast = (struct in_addr) {};
+ n->set_broadcast = -1;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r >= 0) {
+ /* The broadcast address will be calculated based on Address=. Here, we do not check or
+ * set n->family. */
+ n->broadcast = (struct in_addr) {};
+ n->set_broadcast = r;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ if (n->family == AF_INET6) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Broadcast is not valid for IPv6 addresses, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ r = in_addr_from_string(AF_INET, rvalue, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Broadcast is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (in4_addr_is_null(&u.in)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Broadcast cannot be ANY address, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ n->broadcast = u.in;
+ n->set_broadcast = true;
+ n->family = AF_INET;
+ TAKE_PTR(n);
+
+ return 0;
+}
+
+int config_parse_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ union in_addr_union buffer;
+ unsigned char prefixlen;
+ int r, f;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(section, "Network"))
+ /* we are not in an Address section, so use line number instead. */
+ r = address_new_static(network, filename, line, &n);
+ else
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ /* Address=address/prefixlen */
+ r = in_addr_prefix_from_string_auto_internal(rvalue, PREFIXLEN_REFUSE, &f, &buffer, &prefixlen);
+ if (r == -ENOANO) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "An address '%s' is specified without prefix length. "
+ "The behavior of parsing addresses without prefix length will be changed in the future release. "
+ "Please specify prefix length explicitly.", rvalue);
+
+ r = in_addr_prefix_from_string_auto_internal(rvalue, PREFIXLEN_LEGACY, &f, &buffer, &prefixlen);
+ }
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid address '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (n->family != AF_UNSPEC && f != n->family) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Address is incompatible, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (in_addr_is_null(f, &buffer)) {
+ /* Will use address from address pool. Note that for ipv6 case, prefix of the address
+ * pool is 8, but 40 bit is used by the global ID and 16 bit by the subnet ID. So,
+ * let's limit the prefix length to 64 or larger. See RFC4193. */
+ if ((f == AF_INET && prefixlen < 8) ||
+ (f == AF_INET6 && prefixlen < 64)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Null address with invalid prefixlen='%u', ignoring assignment: %s",
+ prefixlen, rvalue);
+ return 0;
+ }
+ }
+
+ n->family = f;
+ n->prefixlen = prefixlen;
+
+ if (streq(lvalue, "Address"))
+ n->in_addr = buffer;
+ else
+ n->in_addr_peer = buffer;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_label(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (isempty(rvalue)) {
+ n->label = mfree(n->label);
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ if (!address_label_valid(rvalue)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Interface label is too long or invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ r = free_and_strdup(&n->label, rvalue);
+ if (r < 0)
+ return log_oom();
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_lifetime(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ usec_t k;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ /* We accept only "forever", "infinity", empty, or "0". */
+ if (STR_IN_SET(rvalue, "forever", "infinity", ""))
+ k = USEC_INFINITY;
+ else if (streq(rvalue, "0"))
+ k = 0;
+ else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid PreferredLifetime= value, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ n->lifetime_preferred_usec = k;
+ TAKE_PTR(n);
+
+ return 0;
+}
+
+int config_parse_address_flags(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "AddPrefixRoute"))
+ r = !r;
+
+ SET_FLAG(n->flags, ltype, r);
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_address_scope(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = route_scope_from_string(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse address scope \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ n->scope = r;
+ n->scope_set = true;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_address_route_metric(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &n->route_metric);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_duplicate_address_detection(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r >= 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "For historical reasons, %s=%s means %s=%s. "
+ "Please use 'both', 'ipv4', 'ipv6' or 'none' instead.",
+ lvalue, rvalue, lvalue, r ? "none" : "both");
+ n->duplicate_address_detection = r ? ADDRESS_FAMILY_NO : ADDRESS_FAMILY_YES;
+ n = NULL;
+ return 0;
+ }
+
+ AddressFamily a = duplicate_address_detection_address_family_from_string(rvalue);
+ if (a < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, a,
+ "Failed to parse %s=, ignoring: %s", lvalue, rvalue);
+ return 0;
+ }
+ n->duplicate_address_detection = a;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_address_netlabel(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(address_free_or_set_invalidp) Address *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+ assert(network);
+
+ r = address_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate new address, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = config_parse_string(unit, filename, line, section, section_line,
+ lvalue, CONFIG_PARSE_STRING_SAFE, rvalue, &n->netlabel, network);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+static int address_section_verify(Address *address) {
+ if (section_is_invalid(address->section))
+ return -EINVAL;
+
+ if (address->family == AF_UNSPEC) {
+ assert(address->section);
+
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Address section without Address= field configured. "
+ "Ignoring [Address] section from line %u.",
+ address->section->filename, address->section->line);
+ }
+
+ assert(IN_SET(address->family, AF_INET, AF_INET6));
+
+ if (in4_addr_is_set(&address->broadcast) &&
+ (address->family == AF_INET6 || address->prefixlen > 30 ||
+ in_addr_is_set(address->family, &address->in_addr_peer))) {
+ log_warning("%s: broadcast address is set for an IPv6 address, "
+ "an IPv4 address with peer address, or with prefix length larger than 30. "
+ "Ignoring Broadcast= setting in the [Address] section from line %u.",
+ address->section->filename, address->section->line);
+
+ address->broadcast.s_addr = 0;
+ }
+
+ if (address->family == AF_INET6 && address->label) {
+ log_warning("%s: address label is set for IPv6 address in the [Address] section from line %u. "
+ "Ignoring Label= setting.",
+ address->section->filename, address->section->line);
+
+ address->label = mfree(address->label);
+ }
+
+ if (!address->scope_set) {
+ if (in_addr_is_localhost(address->family, &address->in_addr) > 0)
+ address->scope = RT_SCOPE_HOST;
+ else if (in_addr_is_link_local(address->family, &address->in_addr) > 0)
+ address->scope = RT_SCOPE_LINK;
+ }
+
+ if (address->duplicate_address_detection < 0) {
+ if (address->family == AF_INET6)
+ address->duplicate_address_detection = ADDRESS_FAMILY_IPV6;
+ else if (in4_addr_is_link_local(&address->in_addr.in))
+ address->duplicate_address_detection = ADDRESS_FAMILY_IPV4;
+ else
+ address->duplicate_address_detection = ADDRESS_FAMILY_NO;
+ } else if (address->duplicate_address_detection == ADDRESS_FAMILY_IPV6 && address->family == AF_INET)
+ log_warning("%s: DuplicateAddressDetection=ipv6 is specified for IPv4 address, ignoring.",
+ address->section->filename);
+ else if (address->duplicate_address_detection == ADDRESS_FAMILY_IPV4 && address->family == AF_INET6)
+ log_warning("%s: DuplicateAddressDetection=ipv4 is specified for IPv6 address, ignoring.",
+ address->section->filename);
+
+ if (address->family == AF_INET6 &&
+ !FLAGS_SET(address->duplicate_address_detection, ADDRESS_FAMILY_IPV6))
+ address->flags |= IFA_F_NODAD;
+
+ return 0;
+}
+
+int network_drop_invalid_addresses(Network *network) {
+ _cleanup_set_free_ Set *addresses = NULL;
+ Address *address;
+ int r;
+
+ assert(network);
+
+ ORDERED_HASHMAP_FOREACH(address, network->addresses_by_section) {
+ Address *dup;
+
+ if (address_section_verify(address) < 0) {
+ /* Drop invalid [Address] sections or Address= settings in [Network].
+ * Note that address_free() will drop the address from addresses_by_section. */
+ address_free(address);
+ continue;
+ }
+
+ /* Always use the setting specified later. So, remove the previously assigned setting. */
+ dup = set_remove(addresses, address);
+ if (dup) {
+ log_warning("%s: Duplicated address %s is specified at line %u and %u, "
+ "dropping the address setting specified at line %u.",
+ dup->section->filename,
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen),
+ address->section->line,
+ dup->section->line, dup->section->line);
+ /* address_free() will drop the address from addresses_by_section. */
+ address_free(dup);
+ }
+
+ /* Use address_kernel_hash_ops, instead of address_kernel_hash_ops_free. Otherwise, the
+ * Address objects will be freed. */
+ r = set_ensure_put(&addresses, &address_kernel_hash_ops, address);
+ if (r < 0)
+ return log_oom();
+ assert(r > 0);
+ }
+
+ return 0;
+}
diff --git a/src/network/networkd-address.h b/src/network/networkd-address.h
new file mode 100644
index 0000000..89b9621
--- /dev/null
+++ b/src/network/networkd-address.h
@@ -0,0 +1,135 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "sd-ipv4acd.h"
+
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "networkd-link.h"
+#include "networkd-util.h"
+#include "time-util.h"
+
+typedef struct Address Address;
+typedef struct Manager Manager;
+typedef struct Network Network;
+typedef struct Request Request;
+typedef int (*address_ready_callback_t)(Address *address);
+typedef int (*address_netlink_handler_t)(
+ sd_netlink *rtnl,
+ sd_netlink_message *m,
+ Request *req,
+ Link *link,
+ Address *address);
+
+struct Address {
+ Link *link;
+ Network *network;
+ ConfigSection *section;
+ NetworkConfigSource source;
+ NetworkConfigState state;
+ union in_addr_union provider; /* DHCP server or router address */
+
+ int family;
+ unsigned char prefixlen;
+ unsigned char scope;
+ uint32_t flags;
+ uint32_t route_metric; /* route metric for prefix route */
+ char *label, *netlabel;
+
+ int set_broadcast;
+ struct in_addr broadcast;
+
+ union in_addr_union in_addr;
+ union in_addr_union in_addr_peer;
+
+ /* These are absolute points in time, and NOT timespans/durations.
+ * Must be specified with clock_boottime_or_monotonic(). */
+ usec_t lifetime_valid_usec;
+ usec_t lifetime_preferred_usec;
+
+ bool scope_set:1;
+ bool ip_masquerade_done:1;
+
+ /* duplicate_address_detection is only used by static or IPv4 dynamic addresses.
+ * To control DAD for IPv6 dynamic addresses, set IFA_F_NODAD to flags. */
+ AddressFamily duplicate_address_detection;
+ sd_ipv4acd *acd;
+ bool acd_bound;
+
+ /* Called when address become ready */
+ address_ready_callback_t callback;
+};
+
+const char* format_lifetime(char *buf, size_t l, usec_t lifetime_usec) _warn_unused_result_;
+/* Note: the lifetime of the compound literal is the immediately surrounding block,
+ * see C11 §6.5.2.5, and
+ * https://stackoverflow.com/questions/34880638/compound-literal-lifetime-and-if-blocks */
+#define FORMAT_LIFETIME(lifetime) \
+ format_lifetime((char[FORMAT_TIMESPAN_MAX+STRLEN("for ")]){}, FORMAT_TIMESPAN_MAX+STRLEN("for "), lifetime)
+
+int address_flags_to_string_alloc(uint32_t flags, int family, char **ret);
+
+int address_new(Address **ret);
+Address* address_free(Address *address);
+int address_get(Link *link, const Address *in, Address **ret);
+int address_configure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg);
+int address_remove(Address *address);
+int address_remove_and_drop(Address *address);
+int address_dup(const Address *src, Address **ret);
+bool address_is_ready(const Address *a);
+void address_set_broadcast(Address *a, Link *link);
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(Address, address_free);
+
+int link_drop_managed_addresses(Link *link);
+int link_drop_foreign_addresses(Link *link);
+int link_drop_ipv6ll_addresses(Link *link);
+void link_foreignize_addresses(Link *link);
+bool link_address_is_dynamic(const Link *link, const Address *address);
+int link_get_address(Link *link, int family, const union in_addr_union *address, unsigned char prefixlen, Address **ret);
+static inline int link_get_ipv6_address(Link *link, const struct in6_addr *address, unsigned char prefixlen, Address **ret) {
+ assert(address);
+ return link_get_address(link, AF_INET6, &(union in_addr_union) { .in6 = *address }, prefixlen, ret);
+}
+static inline int link_get_ipv4_address(Link *link, const struct in_addr *address, unsigned char prefixlen, Address **ret) {
+ assert(address);
+ return link_get_address(link, AF_INET, &(union in_addr_union) { .in = *address }, prefixlen, ret);
+}
+int manager_get_address(Manager *manager, int family, const union in_addr_union *address, unsigned char prefixlen, Address **ret);
+bool manager_has_address(Manager *manager, int family, const union in_addr_union *address, bool check_ready);
+
+void address_cancel_request(Address *address);
+int link_request_address(
+ Link *link,
+ Address *address,
+ bool consume_object,
+ unsigned *message_counter,
+ address_netlink_handler_t netlink_handler,
+ Request **ret);
+int link_request_static_address(Link *link, Address *address, bool consume);
+int link_request_static_addresses(Link *link);
+
+int manager_rtnl_process_address(sd_netlink *nl, sd_netlink_message *message, Manager *m);
+
+int network_drop_invalid_addresses(Network *network);
+
+int address_compare_func(const Address *a1, const Address *a2);
+int address_equal(const Address *a1, const Address *a2);
+
+DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(Address, address);
+
+void link_mark_addresses(Link *link, NetworkConfigSource source);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_broadcast);
+CONFIG_PARSER_PROTOTYPE(config_parse_label);
+CONFIG_PARSER_PROTOTYPE(config_parse_lifetime);
+CONFIG_PARSER_PROTOTYPE(config_parse_address_flags);
+CONFIG_PARSER_PROTOTYPE(config_parse_address_scope);
+CONFIG_PARSER_PROTOTYPE(config_parse_address_route_metric);
+CONFIG_PARSER_PROTOTYPE(config_parse_duplicate_address_detection);
+CONFIG_PARSER_PROTOTYPE(config_parse_address_netlabel);
diff --git a/src/network/networkd-bridge-fdb.c b/src/network/networkd-bridge-fdb.c
new file mode 100644
index 0000000..803e27c
--- /dev/null
+++ b/src/network/networkd-bridge-fdb.c
@@ -0,0 +1,535 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2014 Intel Corporation. All rights reserved.
+***/
+
+#include <net/ethernet.h>
+#include <net/if.h>
+
+#include "alloc-util.h"
+#include "bridge.h"
+#include "netlink-util.h"
+#include "networkd-bridge-fdb.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "networkd-util.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "vlan-util.h"
+#include "vxlan.h"
+
+#define STATIC_BRIDGE_FDB_ENTRIES_PER_NETWORK_MAX 1024U
+
+/* remove and FDB entry. */
+BridgeFDB *bridge_fdb_free(BridgeFDB *fdb) {
+ if (!fdb)
+ return NULL;
+
+ if (fdb->network) {
+ assert(fdb->section);
+ hashmap_remove(fdb->network->bridge_fdb_entries_by_section, fdb->section);
+ }
+
+ config_section_free(fdb->section);
+
+ free(fdb->outgoing_ifname);
+ return mfree(fdb);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(BridgeFDB, bridge_fdb_free);
+
+/* create a new FDB entry or get an existing one. */
+static int bridge_fdb_new_static(
+ Network *network,
+ const char *filename,
+ unsigned section_line,
+ BridgeFDB **ret) {
+
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(bridge_fdb_freep) BridgeFDB *fdb = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ /* search entry in hashmap first. */
+ fdb = hashmap_get(network->bridge_fdb_entries_by_section, n);
+ if (fdb) {
+ *ret = TAKE_PTR(fdb);
+ return 0;
+ }
+
+ if (hashmap_size(network->bridge_fdb_entries_by_section) >= STATIC_BRIDGE_FDB_ENTRIES_PER_NETWORK_MAX)
+ return -E2BIG;
+
+ /* allocate space for and FDB entry. */
+ fdb = new(BridgeFDB, 1);
+ if (!fdb)
+ return -ENOMEM;
+
+ /* init FDB structure. */
+ *fdb = (BridgeFDB) {
+ .network = network,
+ .section = TAKE_PTR(n),
+ .vni = VXLAN_VID_MAX + 1,
+ .ntf_flags = NEIGHBOR_CACHE_ENTRY_FLAGS_SELF,
+ };
+
+ r = hashmap_ensure_put(&network->bridge_fdb_entries_by_section, &config_section_hash_ops, fdb->section, fdb);
+ if (r < 0)
+ return r;
+
+ /* return allocated FDB structure. */
+ *ret = TAKE_PTR(fdb);
+
+ return 0;
+}
+
+static int bridge_fdb_configure_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not add bridge FDB entry");
+ link_enter_failed(link);
+ return 0;
+ }
+
+ if (link->static_bridge_fdb_messages == 0) {
+ log_link_debug(link, "Bridge FDB entries set");
+ link->static_bridge_fdb_configured = true;
+ link_check_ready(link);
+ }
+
+ return 0;
+}
+
+/* send a request to the kernel to add a FDB entry in its static MAC table. */
+static int bridge_fdb_configure_message(const BridgeFDB *fdb, Link *link, sd_netlink_message *req) {
+ int r;
+
+ assert(fdb);
+ assert(link);
+
+ r = sd_rtnl_message_neigh_set_flags(req, fdb->ntf_flags);
+ if (r < 0)
+ return r;
+
+ /* only NUD_PERMANENT state supported. */
+ r = sd_rtnl_message_neigh_set_state(req, NUD_NOARP | NUD_PERMANENT);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, NDA_LLADDR, &fdb->mac_addr, sizeof(fdb->mac_addr));
+ if (r < 0)
+ return r;
+
+ /* VLAN Id is optional. We'll add VLAN Id only if it's specified. */
+ if (fdb->vlan_id > 0) {
+ r = sd_netlink_message_append_u16(req, NDA_VLAN, fdb->vlan_id);
+ if (r < 0)
+ return r;
+ }
+
+ if (fdb->outgoing_ifindex > 0) {
+ r = sd_netlink_message_append_u32(req, NDA_IFINDEX, fdb->outgoing_ifindex);
+ if (r < 0)
+ return r;
+ }
+
+ if (in_addr_is_set(fdb->family, &fdb->destination_addr)) {
+ r = netlink_message_append_in_addr_union(req, NDA_DST, fdb->family, &fdb->destination_addr);
+ if (r < 0)
+ return r;
+ }
+
+ if (fdb->vni <= VXLAN_VID_MAX) {
+ r = sd_netlink_message_append_u32(req, NDA_VNI, fdb->vni);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int bridge_fdb_configure(BridgeFDB *fdb, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(fdb);
+ assert(link);
+ assert(link->manager);
+ assert(req);
+
+ r = sd_rtnl_message_new_neigh(link->manager->rtnl, &m, RTM_NEWNEIGH, link->ifindex, AF_BRIDGE);
+ if (r < 0)
+ return r;
+
+ r = bridge_fdb_configure_message(fdb, link, m);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static bool bridge_fdb_is_ready_to_configure(BridgeFDB *fdb, Link *link) {
+ Link *out = NULL;
+
+ assert(fdb);
+ assert(link);
+ assert(link->manager);
+
+ if (!link_is_ready_to_configure(link, false))
+ return false;
+
+ if (fdb->outgoing_ifname) {
+ if (link_get_by_name(link->manager, fdb->outgoing_ifname, &out) < 0)
+ return false;
+
+ fdb->outgoing_ifindex = out->ifindex;
+ } else if (fdb->outgoing_ifindex > 0) {
+ if (link_get_by_index(link->manager, fdb->outgoing_ifindex, &out) < 0)
+ return false;
+ }
+ if (out && !link_is_ready_to_configure(out, false))
+ return false;
+
+ return true;
+}
+
+static int bridge_fdb_process_request(Request *req, Link *link, void *userdata) {
+ BridgeFDB *fdb = ASSERT_PTR(userdata);
+ int r;
+
+ assert(req);
+ assert(link);
+
+ if (!bridge_fdb_is_ready_to_configure(fdb, link))
+ return 0;
+
+ r = bridge_fdb_configure(fdb, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure bridge FDB: %m");
+
+ return 1;
+}
+
+int link_request_static_bridge_fdb(Link *link) {
+ BridgeFDB *fdb;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->static_bridge_fdb_configured = false;
+
+ HASHMAP_FOREACH(fdb, link->network->bridge_fdb_entries_by_section) {
+ r = link_queue_request_full(link, REQUEST_TYPE_BRIDGE_FDB,
+ fdb, NULL,
+ trivial_hash_func,
+ trivial_compare_func,
+ bridge_fdb_process_request,
+ &link->static_bridge_fdb_messages,
+ bridge_fdb_configure_handler,
+ NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request static bridge FDB entry: %m");
+ }
+
+ if (link->static_bridge_fdb_messages == 0) {
+ link->static_bridge_fdb_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Setting bridge FDB entries");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+void network_drop_invalid_bridge_fdb_entries(Network *network) {
+ BridgeFDB *fdb;
+
+ assert(network);
+
+ HASHMAP_FOREACH(fdb, network->bridge_fdb_entries_by_section)
+ if (section_is_invalid(fdb->section))
+ bridge_fdb_free(fdb);
+}
+
+/* parse the HW address from config files. */
+int config_parse_fdb_hwaddr(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = bridge_fdb_new_static(network, filename, section_line, &fdb);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_ether_addr(rvalue, &fdb->mac_addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Not a valid MAC address, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(fdb);
+ return 0;
+}
+
+/* parse the VLAN Id from config files. */
+int config_parse_fdb_vlan_id(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = bridge_fdb_new_static(network, filename, section_line, &fdb);
+ if (r < 0)
+ return log_oom();
+
+ r = config_parse_vlanid(unit, filename, line, section,
+ section_line, lvalue, ltype,
+ rvalue, &fdb->vlan_id, userdata);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(fdb);
+ return 0;
+}
+
+int config_parse_fdb_destination(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = bridge_fdb_new_static(network, filename, section_line, &fdb);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_from_string_auto(rvalue, &fdb->family, &fdb->destination_addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "FDB destination IP address is invalid, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(fdb);
+ return 0;
+}
+
+int config_parse_fdb_vxlan_vni(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL;
+ Network *network = userdata;
+ uint32_t vni;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = bridge_fdb_new_static(network, filename, section_line, &fdb);
+ if (r < 0)
+ return log_oom();
+
+ r = safe_atou32(rvalue, &vni);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse VXLAN Network Identifier (VNI), ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ if (vni > VXLAN_VID_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "FDB invalid VXLAN Network Identifier (VNI), ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ fdb->vni = vni;
+
+ TAKE_PTR(fdb);
+ return 0;
+}
+
+static const char* const ntf_flags_table[_NEIGHBOR_CACHE_ENTRY_FLAGS_MAX] = {
+ [NEIGHBOR_CACHE_ENTRY_FLAGS_USE] = "use",
+ [NEIGHBOR_CACHE_ENTRY_FLAGS_SELF] = "self",
+ [NEIGHBOR_CACHE_ENTRY_FLAGS_MASTER] = "master",
+ [NEIGHBOR_CACHE_ENTRY_FLAGS_ROUTER] = "router",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(ntf_flags, NeighborCacheEntryFlags);
+
+int config_parse_fdb_ntf_flags(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL;
+ Network *network = userdata;
+ NeighborCacheEntryFlags f;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = bridge_fdb_new_static(network, filename, section_line, &fdb);
+ if (r < 0)
+ return log_oom();
+
+ f = ntf_flags_from_string(rvalue);
+ if (f < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, f,
+ "FDB failed to parse AssociatedWith=, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ fdb->ntf_flags = f;
+
+ TAKE_PTR(fdb);
+ return 0;
+}
+
+int config_parse_fdb_interface(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = bridge_fdb_new_static(network, filename, section_line, &fdb);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ fdb->outgoing_ifname = mfree(fdb->outgoing_ifname);
+ fdb->outgoing_ifindex = 0;
+ TAKE_PTR(fdb);
+ return 0;
+ }
+
+ r = parse_ifindex(rvalue);
+ if (r > 0) {
+ fdb->outgoing_ifname = mfree(fdb->outgoing_ifname);
+ fdb->outgoing_ifindex = r;
+ TAKE_PTR(fdb);
+ return 0;
+ }
+
+ if (!ifname_valid_full(rvalue, IFNAME_VALID_ALTERNATIVE)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid interface name in %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ r = free_and_strdup(&fdb->outgoing_ifname, rvalue);
+ if (r < 0)
+ return log_oom();
+ fdb->outgoing_ifindex = 0;
+
+ TAKE_PTR(fdb);
+ return 0;
+}
diff --git a/src/network/networkd-bridge-fdb.h b/src/network/networkd-bridge-fdb.h
new file mode 100644
index 0000000..b59d673
--- /dev/null
+++ b/src/network/networkd-bridge-fdb.h
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+/***
+ Copyright © 2014 Intel Corporation. All rights reserved.
+***/
+
+#include <inttypes.h>
+#include <linux/neighbour.h>
+
+#include "conf-parser.h"
+#include "ether-addr-util.h"
+#include "in-addr-util.h"
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+typedef enum NeighborCacheEntryFlags {
+ NEIGHBOR_CACHE_ENTRY_FLAGS_USE = NTF_USE,
+ NEIGHBOR_CACHE_ENTRY_FLAGS_SELF = NTF_SELF,
+ NEIGHBOR_CACHE_ENTRY_FLAGS_MASTER = NTF_MASTER,
+ NEIGHBOR_CACHE_ENTRY_FLAGS_ROUTER = NTF_ROUTER,
+ _NEIGHBOR_CACHE_ENTRY_FLAGS_MAX,
+ _NEIGHBOR_CACHE_ENTRY_FLAGS_INVALID = -EINVAL,
+} NeighborCacheEntryFlags;
+
+typedef struct BridgeFDB {
+ Network *network;
+ ConfigSection *section;
+
+ uint32_t vni;
+
+ int family;
+ uint16_t vlan_id;
+
+ struct ether_addr mac_addr;
+ union in_addr_union destination_addr;
+ NeighborCacheEntryFlags ntf_flags;
+ char *outgoing_ifname;
+ int outgoing_ifindex;
+} BridgeFDB;
+
+BridgeFDB *bridge_fdb_free(BridgeFDB *fdb);
+
+void network_drop_invalid_bridge_fdb_entries(Network *network);
+
+int link_request_static_bridge_fdb(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_fdb_hwaddr);
+CONFIG_PARSER_PROTOTYPE(config_parse_fdb_vlan_id);
+CONFIG_PARSER_PROTOTYPE(config_parse_fdb_destination);
+CONFIG_PARSER_PROTOTYPE(config_parse_fdb_vxlan_vni);
+CONFIG_PARSER_PROTOTYPE(config_parse_fdb_ntf_flags);
+CONFIG_PARSER_PROTOTYPE(config_parse_fdb_interface);
diff --git a/src/network/networkd-bridge-mdb.c b/src/network/networkd-bridge-mdb.c
new file mode 100644
index 0000000..bd1a974
--- /dev/null
+++ b/src/network/networkd-bridge-mdb.c
@@ -0,0 +1,365 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <linux/if_bridge.h>
+
+#include "netlink-util.h"
+#include "networkd-bridge-mdb.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "string-util.h"
+#include "vlan-util.h"
+
+#define STATIC_BRIDGE_MDB_ENTRIES_PER_NETWORK_MAX 1024U
+
+/* remove MDB entry. */
+BridgeMDB *bridge_mdb_free(BridgeMDB *mdb) {
+ if (!mdb)
+ return NULL;
+
+ if (mdb->network) {
+ assert(mdb->section);
+ hashmap_remove(mdb->network->bridge_mdb_entries_by_section, mdb->section);
+ }
+
+ config_section_free(mdb->section);
+
+ return mfree(mdb);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(BridgeMDB, bridge_mdb_free);
+
+/* create a new MDB entry or get an existing one. */
+static int bridge_mdb_new_static(
+ Network *network,
+ const char *filename,
+ unsigned section_line,
+ BridgeMDB **ret) {
+
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(bridge_mdb_freep) BridgeMDB *mdb = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ /* search entry in hashmap first. */
+ mdb = hashmap_get(network->bridge_mdb_entries_by_section, n);
+ if (mdb) {
+ *ret = TAKE_PTR(mdb);
+ return 0;
+ }
+
+ if (hashmap_size(network->bridge_mdb_entries_by_section) >= STATIC_BRIDGE_MDB_ENTRIES_PER_NETWORK_MAX)
+ return -E2BIG;
+
+ /* allocate space for an MDB entry. */
+ mdb = new(BridgeMDB, 1);
+ if (!mdb)
+ return -ENOMEM;
+
+ /* init MDB structure. */
+ *mdb = (BridgeMDB) {
+ .network = network,
+ .section = TAKE_PTR(n),
+ };
+
+ r = hashmap_ensure_put(&network->bridge_mdb_entries_by_section, &config_section_hash_ops, mdb->section, mdb);
+ if (r < 0)
+ return r;
+
+ /* return allocated MDB structure. */
+ *ret = TAKE_PTR(mdb);
+ return 0;
+}
+
+static int bridge_mdb_configure_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EINVAL && streq_ptr(link->kind, "bridge") && link->master_ifindex <= 0) {
+ /* To configure bridge MDB entries on bridge master, 1bc844ee0faa1b92e3ede00bdd948021c78d7088 (v5.4) is required. */
+ if (!link->manager->bridge_mdb_on_master_not_supported) {
+ log_link_warning_errno(link, r, "Kernel seems not to support bridge MDB entries on bridge master, ignoring: %m");
+ link->manager->bridge_mdb_on_master_not_supported = true;
+ }
+ } else if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not add MDB entry");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->static_bridge_mdb_messages == 0) {
+ link->static_bridge_mdb_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+/* send a request to the kernel to add an MDB entry */
+static int bridge_mdb_configure(BridgeMDB *mdb, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ struct br_mdb_entry entry;
+ int r;
+
+ assert(mdb);
+ assert(link);
+ assert(link->manager);
+ assert(req);
+
+ if (DEBUG_LOGGING)
+ log_link_debug(link, "Configuring bridge MDB entry: MulticastGroupAddress=%s, VLANId=%u",
+ IN_ADDR_TO_STRING(mdb->family, &mdb->group_addr), mdb->vlan_id);
+
+ entry = (struct br_mdb_entry) {
+ /* If MDB entry is added on bridge master, then the state must be MDB_TEMPORARY.
+ * See br_mdb_add_group() in net/bridge/br_mdb.c of kernel. */
+ .state = link->master_ifindex <= 0 ? MDB_TEMPORARY : MDB_PERMANENT,
+ .ifindex = link->ifindex,
+ .vid = mdb->vlan_id,
+ };
+
+ switch (mdb->family) {
+ case AF_INET:
+ entry.addr.u.ip4 = mdb->group_addr.in.s_addr;
+ entry.addr.proto = htobe16(ETH_P_IP);
+ break;
+
+ case AF_INET6:
+ entry.addr.u.ip6 = mdb->group_addr.in6;
+ entry.addr.proto = htobe16(ETH_P_IPV6);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ r = sd_rtnl_message_new_mdb(link->manager->rtnl, &m, RTM_NEWMDB,
+ link->master_ifindex > 0 ? link->master_ifindex : link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(m, MDBA_SET_ENTRY, &entry, sizeof(entry));
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static bool bridge_mdb_is_ready_to_configure(Link *link) {
+ Link *master;
+
+ assert(link);
+
+ if (!link_is_ready_to_configure(link, false))
+ return false;
+
+ if (!link->master_set)
+ return false;
+
+ if (link->master_ifindex <= 0 && streq_ptr(link->kind, "bridge"))
+ return true; /* The interface is bridge master. */
+
+ if (link_get_master(link, &master) < 0)
+ return false;
+
+ if (!streq_ptr(master->kind, "bridge"))
+ return false;
+
+ if (!IN_SET(master->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return false;
+
+ if (master->set_flags_messages > 0)
+ return false;
+
+ if (!link_has_carrier(master))
+ return false;
+
+ return true;
+}
+
+static int bridge_mdb_process_request(Request *req, Link *link, void *userdata) {
+ BridgeMDB *mdb = ASSERT_PTR(userdata);
+ int r;
+
+ assert(req);
+ assert(link);
+
+ if (!bridge_mdb_is_ready_to_configure(link))
+ return 0;
+
+ r = bridge_mdb_configure(mdb, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure bridge MDB: %m");
+
+ return 1;
+}
+
+int link_request_static_bridge_mdb(Link *link) {
+ BridgeMDB *mdb;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ link->static_bridge_mdb_configured = false;
+
+ if (!link->network)
+ return 0;
+
+ if (hashmap_isempty(link->network->bridge_mdb_entries_by_section))
+ goto finish;
+
+ HASHMAP_FOREACH(mdb, link->network->bridge_mdb_entries_by_section) {
+ r = link_queue_request_full(link, REQUEST_TYPE_BRIDGE_MDB,
+ mdb, NULL,
+ trivial_hash_func,
+ trivial_compare_func,
+ bridge_mdb_process_request,
+ &link->static_bridge_mdb_messages,
+ bridge_mdb_configure_handler,
+ NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request MDB entry to multicast group database: %m");
+ }
+
+finish:
+ if (link->static_bridge_mdb_messages == 0) {
+ link->static_bridge_mdb_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Setting bridge MDB entries.");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+static int bridge_mdb_verify(BridgeMDB *mdb) {
+ if (section_is_invalid(mdb->section))
+ return -EINVAL;
+
+ if (mdb->family == AF_UNSPEC)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: [BridgeMDB] section without MulticastGroupAddress= field configured. "
+ "Ignoring [BridgeMDB] section from line %u.",
+ mdb->section->filename, mdb->section->line);
+
+ if (!in_addr_is_multicast(mdb->family, &mdb->group_addr))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: MulticastGroupAddress= is not a multicast address. "
+ "Ignoring [BridgeMDB] section from line %u.",
+ mdb->section->filename, mdb->section->line);
+
+ if (mdb->family == AF_INET) {
+ if (in4_addr_is_local_multicast(&mdb->group_addr.in))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: MulticastGroupAddress= is a local multicast address. "
+ "Ignoring [BridgeMDB] section from line %u.",
+ mdb->section->filename, mdb->section->line);
+ } else {
+ if (in6_addr_is_link_local_all_nodes(&mdb->group_addr.in6))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: MulticastGroupAddress= is the multicast all nodes address. "
+ "Ignoring [BridgeMDB] section from line %u.",
+ mdb->section->filename, mdb->section->line);
+ }
+
+ return 0;
+}
+
+void network_drop_invalid_bridge_mdb_entries(Network *network) {
+ BridgeMDB *mdb;
+
+ assert(network);
+
+ HASHMAP_FOREACH(mdb, network->bridge_mdb_entries_by_section)
+ if (bridge_mdb_verify(mdb) < 0)
+ bridge_mdb_free(mdb);
+}
+
+/* parse the VLAN Id from config files. */
+int config_parse_mdb_vlan_id(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(bridge_mdb_free_or_set_invalidp) BridgeMDB *mdb = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = bridge_mdb_new_static(network, filename, section_line, &mdb);
+ if (r < 0)
+ return log_oom();
+
+ r = config_parse_vlanid(unit, filename, line, section,
+ section_line, lvalue, ltype,
+ rvalue, &mdb->vlan_id, userdata);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(mdb);
+ return 0;
+}
+
+/* parse the multicast group from config files. */
+int config_parse_mdb_group_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(bridge_mdb_free_or_set_invalidp) BridgeMDB *mdb = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = bridge_mdb_new_static(network, filename, section_line, &mdb);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_from_string_auto(rvalue, &mdb->family, &mdb->group_addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Cannot parse multicast group address: %m");
+ return 0;
+ }
+
+ TAKE_PTR(mdb);
+ return 0;
+}
diff --git a/src/network/networkd-bridge-mdb.h b/src/network/networkd-bridge-mdb.h
new file mode 100644
index 0000000..edea255
--- /dev/null
+++ b/src/network/networkd-bridge-mdb.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+typedef struct BridgeMDB {
+ Network *network;
+ ConfigSection *section;
+
+ int family;
+ union in_addr_union group_addr;
+ uint16_t vlan_id;
+} BridgeMDB;
+
+BridgeMDB *bridge_mdb_free(BridgeMDB *mdb);
+
+void network_drop_invalid_bridge_mdb_entries(Network *network);
+
+int link_request_static_bridge_mdb(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_mdb_group_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_mdb_vlan_id);
diff --git a/src/network/networkd-bridge-vlan.c b/src/network/networkd-bridge-vlan.c
new file mode 100644
index 0000000..36e3610
--- /dev/null
+++ b/src/network/networkd-bridge-vlan.c
@@ -0,0 +1,249 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2016 BISDN GmbH. All rights reserved.
+***/
+
+#include <netinet/in.h>
+#include <linux/if_bridge.h>
+#include <stdbool.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netlink-util.h"
+#include "networkd-bridge-vlan.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "parse-util.h"
+#include "vlan-util.h"
+
+static bool is_bit_set(unsigned bit, uint32_t scope) {
+ assert(bit < sizeof(scope)*8);
+ return scope & (UINT32_C(1) << bit);
+}
+
+static void set_bit(unsigned nr, uint32_t *addr) {
+ if (nr < BRIDGE_VLAN_BITMAP_MAX)
+ addr[nr / 32] |= (UINT32_C(1) << (nr % 32));
+}
+
+static int find_next_bit(int i, uint32_t x) {
+ int j;
+
+ if (i >= 32)
+ return -1;
+
+ /* find first bit */
+ if (i < 0)
+ return BUILTIN_FFS_U32(x);
+
+ /* mask off prior finds to get next */
+ j = __builtin_ffs(x >> i);
+ return j ? j + i : 0;
+}
+
+int bridge_vlan_append_info(
+ const Link *link,
+ sd_netlink_message *req,
+ uint16_t pvid,
+ const uint32_t *br_vid_bitmap,
+ const uint32_t *br_untagged_bitmap) {
+
+ struct bridge_vlan_info br_vlan;
+ bool done, untagged = false;
+ uint16_t begin, end;
+ int r, cnt;
+
+ assert(link);
+ assert(req);
+ assert(br_vid_bitmap);
+ assert(br_untagged_bitmap);
+
+ cnt = 0;
+
+ begin = end = UINT16_MAX;
+ for (int k = 0; k < BRIDGE_VLAN_BITMAP_LEN; k++) {
+ uint32_t untagged_map = br_untagged_bitmap[k];
+ uint32_t vid_map = br_vid_bitmap[k];
+ unsigned base_bit = k * 32;
+ int i = -1;
+
+ done = false;
+ do {
+ int j = find_next_bit(i, vid_map);
+ if (j > 0) {
+ /* first hit of any bit */
+ if (begin == UINT16_MAX && end == UINT16_MAX) {
+ begin = end = j - 1 + base_bit;
+ untagged = is_bit_set(j - 1, untagged_map);
+ goto next;
+ }
+
+ /* this bit is a continuation of prior bits */
+ if (j - 2 + base_bit == end && untagged == is_bit_set(j - 1, untagged_map) && (uint16_t)j - 1 + base_bit != pvid && (uint16_t)begin != pvid) {
+ end++;
+ goto next;
+ }
+ } else
+ done = true;
+
+ if (begin != UINT16_MAX) {
+ cnt++;
+ if (done && k < BRIDGE_VLAN_BITMAP_LEN - 1)
+ break;
+
+ br_vlan.flags = 0;
+ if (untagged)
+ br_vlan.flags |= BRIDGE_VLAN_INFO_UNTAGGED;
+
+ if (begin == end) {
+ br_vlan.vid = begin;
+
+ if (begin == pvid)
+ br_vlan.flags |= BRIDGE_VLAN_INFO_PVID;
+
+ r = sd_netlink_message_append_data(req, IFLA_BRIDGE_VLAN_INFO, &br_vlan, sizeof(br_vlan));
+ if (r < 0)
+ return r;
+ } else {
+ br_vlan.vid = begin;
+ br_vlan.flags |= BRIDGE_VLAN_INFO_RANGE_BEGIN;
+
+ r = sd_netlink_message_append_data(req, IFLA_BRIDGE_VLAN_INFO, &br_vlan, sizeof(br_vlan));
+ if (r < 0)
+ return r;
+
+ br_vlan.vid = end;
+ br_vlan.flags &= ~BRIDGE_VLAN_INFO_RANGE_BEGIN;
+ br_vlan.flags |= BRIDGE_VLAN_INFO_RANGE_END;
+
+ r = sd_netlink_message_append_data(req, IFLA_BRIDGE_VLAN_INFO, &br_vlan, sizeof(br_vlan));
+ if (r < 0)
+ return r;
+ }
+
+ if (done)
+ break;
+ }
+ if (j > 0) {
+ begin = end = j - 1 + base_bit;
+ untagged = is_bit_set(j - 1, untagged_map);
+ }
+
+ next:
+ i = j;
+ } while (!done);
+ }
+
+ assert(cnt > 0);
+ return cnt;
+}
+
+void network_adjust_bridge_vlan(Network *network) {
+ assert(network);
+
+ if (!network->use_br_vlan)
+ return;
+
+ /* pvid might not be in br_vid_bitmap yet */
+ if (network->pvid)
+ set_bit(network->pvid, network->br_vid_bitmap);
+}
+
+int config_parse_brvlan_pvid(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ uint16_t pvid;
+ int r;
+
+ r = parse_vlanid(rvalue, &pvid);
+ if (r < 0)
+ return r;
+
+ network->pvid = pvid;
+ network->use_br_vlan = true;
+
+ return 0;
+}
+
+int config_parse_brvlan_vlan(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ uint16_t vid, vid_end;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = parse_vid_range(rvalue, &vid, &vid_end);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse VLAN, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ for (; vid <= vid_end; vid++)
+ set_bit(vid, network->br_vid_bitmap);
+
+ network->use_br_vlan = true;
+ return 0;
+}
+
+int config_parse_brvlan_untagged(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ uint16_t vid, vid_end;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = parse_vid_range(rvalue, &vid, &vid_end);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Could not parse VLAN: %s", rvalue);
+ return 0;
+ }
+
+ for (; vid <= vid_end; vid++) {
+ set_bit(vid, network->br_vid_bitmap);
+ set_bit(vid, network->br_untagged_bitmap);
+ }
+
+ network->use_br_vlan = true;
+ return 0;
+}
diff --git a/src/network/networkd-bridge-vlan.h b/src/network/networkd-bridge-vlan.h
new file mode 100644
index 0000000..f44b810
--- /dev/null
+++ b/src/network/networkd-bridge-vlan.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+/***
+ Copyright © 2016 BISDN GmbH. All rights reserved.
+***/
+
+#include <inttypes.h>
+
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+
+#define BRIDGE_VLAN_BITMAP_MAX 4096
+#define BRIDGE_VLAN_BITMAP_LEN (BRIDGE_VLAN_BITMAP_MAX / 32)
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+void network_adjust_bridge_vlan(Network *network);
+
+int bridge_vlan_append_info(
+ const Link * link,
+ sd_netlink_message *req,
+ uint16_t pvid,
+ const uint32_t *br_vid_bitmap,
+ const uint32_t *br_untagged_bitmap);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_brvlan_pvid);
+CONFIG_PARSER_PROTOTYPE(config_parse_brvlan_vlan);
+CONFIG_PARSER_PROTOTYPE(config_parse_brvlan_untagged);
diff --git a/src/network/networkd-can.c b/src/network/networkd-can.c
new file mode 100644
index 0000000..b8a1871
--- /dev/null
+++ b/src/network/networkd-can.c
@@ -0,0 +1,336 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <linux/can/netlink.h>
+
+#include "networkd-can.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "networkd-setlink.h"
+#include "parse-util.h"
+#include "string-util.h"
+
+#define CAN_TERMINATION_DEFAULT_OHM_VALUE 120
+
+int can_set_netlink_message(Link *link, sd_netlink_message *m) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(m);
+
+ r = sd_netlink_message_set_flags(m, NLM_F_REQUEST | NLM_F_ACK);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, link->kind);
+ if (r < 0)
+ return r;
+
+ if (link->network->can_bitrate > 0) {
+ struct can_bittiming bt = {
+ .bitrate = link->network->can_bitrate,
+ .sample_point = link->network->can_sample_point,
+ .sjw = link->network->can_sync_jump_width,
+ };
+
+ log_link_debug(link, "Setting bitrate = %u bit/s", bt.bitrate);
+ if (link->network->can_sample_point > 0)
+ log_link_debug(link, "Setting sample point = %u.%u%%", bt.sample_point / 10, bt.sample_point % 10);
+ else
+ log_link_debug(link, "Using default sample point");
+
+ r = sd_netlink_message_append_data(m, IFLA_CAN_BITTIMING, &bt, sizeof(bt));
+ if (r < 0)
+ return r;
+ } else if (link->network->can_time_quanta_ns > 0) {
+ struct can_bittiming bt = {
+ .tq = link->network->can_time_quanta_ns,
+ .prop_seg = link->network->can_propagation_segment,
+ .phase_seg1 = link->network->can_phase_buffer_segment_1,
+ .phase_seg2 = link->network->can_phase_buffer_segment_2,
+ .sjw = link->network->can_sync_jump_width,
+ };
+
+ log_link_debug(link, "Setting time quanta = %"PRIu32" nsec", bt.tq);
+ r = sd_netlink_message_append_data(m, IFLA_CAN_BITTIMING, &bt, sizeof(bt));
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->can_data_bitrate > 0) {
+ struct can_bittiming bt = {
+ .bitrate = link->network->can_data_bitrate,
+ .sample_point = link->network->can_data_sample_point,
+ .sjw = link->network->can_data_sync_jump_width,
+ };
+
+ log_link_debug(link, "Setting data bitrate = %u bit/s", bt.bitrate);
+ if (link->network->can_data_sample_point > 0)
+ log_link_debug(link, "Setting data sample point = %u.%u%%", bt.sample_point / 10, bt.sample_point % 10);
+ else
+ log_link_debug(link, "Using default data sample point");
+
+ r = sd_netlink_message_append_data(m, IFLA_CAN_DATA_BITTIMING, &bt, sizeof(bt));
+ if (r < 0)
+ return r;
+ } else if (link->network->can_data_time_quanta_ns > 0) {
+ struct can_bittiming bt = {
+ .tq = link->network->can_data_time_quanta_ns,
+ .prop_seg = link->network->can_data_propagation_segment,
+ .phase_seg1 = link->network->can_data_phase_buffer_segment_1,
+ .phase_seg2 = link->network->can_data_phase_buffer_segment_2,
+ .sjw = link->network->can_data_sync_jump_width,
+ };
+
+ log_link_debug(link, "Setting data time quanta = %"PRIu32" nsec", bt.tq);
+ r = sd_netlink_message_append_data(m, IFLA_CAN_DATA_BITTIMING, &bt, sizeof(bt));
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->can_restart_us > 0) {
+ uint64_t restart_ms;
+
+ if (link->network->can_restart_us == USEC_INFINITY)
+ restart_ms = 0;
+ else
+ restart_ms = DIV_ROUND_UP(link->network->can_restart_us, USEC_PER_MSEC);
+
+ log_link_debug(link, "Setting restart = %s", FORMAT_TIMESPAN(restart_ms * 1000, MSEC_PER_SEC));
+ r = sd_netlink_message_append_u32(m, IFLA_CAN_RESTART_MS, restart_ms);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->can_control_mode_mask != 0) {
+ struct can_ctrlmode cm = {
+ .mask = link->network->can_control_mode_mask,
+ .flags = link->network->can_control_mode_flags,
+ };
+
+ r = sd_netlink_message_append_data(m, IFLA_CAN_CTRLMODE, &cm, sizeof(cm));
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->can_termination_set) {
+ log_link_debug(link, "Setting can-termination to '%u'.", link->network->can_termination);
+
+ r = sd_netlink_message_append_u16(m, IFLA_CAN_TERMINATION, link->network->can_termination);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_can_bitrate(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint32_t *br = ASSERT_PTR(data);
+ uint64_t sz;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = parse_size(rvalue, 1000, &sz);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse can bitrate '%s', ignoring: %m", rvalue);
+ return 0;
+ }
+
+ /* Linux uses __u32 for bitrates, so the value should not exceed that. */
+ if (sz <= 0 || sz > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Bit rate out of permitted range 1...4294967295");
+ return 0;
+ }
+
+ *br = (uint32_t) sz;
+
+ return 0;
+}
+
+int config_parse_can_time_quanta(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ nsec_t val, *tq = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = parse_nsec(rvalue, &val);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse can time quanta '%s', ignoring: %m", rvalue);
+ return 0;
+ }
+
+ /* Linux uses __u32 for bitrates, so the value should not exceed that. */
+ if (val <= 0 || val > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Time quanta out of permitted range 1...4294967295");
+ return 0;
+ }
+
+ *tq = val;
+ return 0;
+}
+
+int config_parse_can_restart_usec(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ usec_t usec, *restart_usec = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = parse_sec(rvalue, &usec);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse CAN restart sec '%s', ignoring: %m", rvalue);
+ return 0;
+ }
+
+ if (usec != USEC_INFINITY &&
+ DIV_ROUND_UP(usec, USEC_PER_MSEC) > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "CAN RestartSec= must be in the range 0...%"PRIu32"ms, ignoring: %s", UINT32_MAX, rvalue);
+ return 0;
+ }
+
+ *restart_usec = usec;
+ return 0;
+}
+
+int config_parse_can_control_mode(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ uint32_t mask = ltype;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(mask != 0);
+
+ if (isempty(rvalue)) {
+ network->can_control_mode_mask &= ~mask;
+ network->can_control_mode_flags &= ~mask;
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse CAN control mode '%s', ignoring: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ network->can_control_mode_mask |= mask;
+ SET_FLAG(network->can_control_mode_flags, mask, r);
+ return 0;
+}
+
+int config_parse_can_termination(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ network->can_termination_set = false;
+ return 0;
+ }
+
+ /* Note that 0 termination ohm value means no termination resistor, and there is no conflict
+ * between parse_boolean() and safe_atou16() when Termination=0. However, Termination=1 must be
+ * treated as 1 ohm, instead of true (and then the default ohm value). So, we need to parse the
+ * string with safe_atou16() at first. */
+
+ r = safe_atou16(rvalue, &network->can_termination);
+ if (r < 0) {
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse CAN termination value, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ network->can_termination = r ? CAN_TERMINATION_DEFAULT_OHM_VALUE : 0;
+ }
+
+ network->can_termination_set = true;
+ return 0;
+}
diff --git a/src/network/networkd-can.h b/src/network/networkd-can.h
new file mode 100644
index 0000000..3945082
--- /dev/null
+++ b/src/network/networkd-can.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <linux/can/netlink.h>
+
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+
+int can_set_netlink_message(Link *link, sd_netlink_message *m);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_can_bitrate);
+CONFIG_PARSER_PROTOTYPE(config_parse_can_time_quanta);
+CONFIG_PARSER_PROTOTYPE(config_parse_can_restart_usec);
+CONFIG_PARSER_PROTOTYPE(config_parse_can_control_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_can_termination);
diff --git a/src/network/networkd-conf.c b/src/network/networkd-conf.c
new file mode 100644
index 0000000..9190dbf
--- /dev/null
+++ b/src/network/networkd-conf.c
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2014 Vinay Kulkarni <kulkarniv@vmware.com>
+ ***/
+
+#include "conf-parser.h"
+#include "def.h"
+#include "networkd-conf.h"
+#include "networkd-manager.h"
+#include "networkd-speed-meter.h"
+
+int manager_parse_config_file(Manager *m) {
+ int r;
+
+ assert(m);
+
+ r = config_parse_many_nulstr(
+ PKGSYSCONFDIR "/networkd.conf",
+ CONF_PATHS_NULSTR("systemd/networkd.conf.d"),
+ "Network\0"
+ "DHCPv4\0"
+ "DHCPv6\0"
+ "DHCP\0",
+ config_item_perf_lookup, networkd_gperf_lookup,
+ CONFIG_PARSE_WARN,
+ m,
+ NULL);
+ if (r < 0)
+ return r;
+
+ if (m->use_speed_meter && m->speed_meter_interval_usec < SPEED_METER_MINIMUM_TIME_INTERVAL) {
+ log_warning("SpeedMeterIntervalSec= is too small, using %s.",
+ FORMAT_TIMESPAN(SPEED_METER_MINIMUM_TIME_INTERVAL, USEC_PER_SEC));
+ m->speed_meter_interval_usec = SPEED_METER_MINIMUM_TIME_INTERVAL;
+ }
+
+ return 0;
+}
diff --git a/src/network/networkd-conf.h b/src/network/networkd-conf.h
new file mode 100644
index 0000000..6f8612a
--- /dev/null
+++ b/src/network/networkd-conf.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+/***
+ Copyright © 2014 Vinay Kulkarni <kulkarniv@vmware.com>
+***/
+
+#include "conf-parser.h"
+
+typedef struct Manager Manager;
+
+int manager_parse_config_file(Manager *m);
+
+const struct ConfigPerfItem* networkd_gperf_lookup(const char *key, GPERF_LEN_TYPE length);
diff --git a/src/network/networkd-dhcp-common.c b/src/network/networkd-dhcp-common.c
new file mode 100644
index 0000000..77d5e3a
--- /dev/null
+++ b/src/network/networkd-dhcp-common.c
@@ -0,0 +1,1306 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "dhcp-identifier.h"
+#include "dhcp-internal.h"
+#include "dhcp6-internal.h"
+#include "escape.h"
+#include "hexdecoct.h"
+#include "in-addr-prefix-util.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-route-util.h"
+#include "parse-util.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "vrf.h"
+
+static uint32_t link_get_vrf_table(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ return link->network->vrf ? VRF(link->network->vrf)->table : RT_TABLE_MAIN;
+}
+
+uint32_t link_get_dhcp4_route_table(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ /* When the interface is part of an VRF use the VRFs routing table, unless
+ * another table is explicitly specified. */
+
+ if (link->network->dhcp_route_table_set)
+ return link->network->dhcp_route_table;
+ return link_get_vrf_table(link);
+}
+
+uint32_t link_get_ipv6_accept_ra_route_table(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (link->network->ipv6_accept_ra_route_table_set)
+ return link->network->ipv6_accept_ra_route_table;
+ return link_get_vrf_table(link);
+}
+
+bool link_dhcp_enabled(Link *link, int family) {
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+
+ /* Currently, sd-dhcp-client supports only ethernet and infiniband. */
+ if (family == AF_INET && !IN_SET(link->iftype, ARPHRD_ETHER, ARPHRD_INFINIBAND))
+ return false;
+
+ if (family == AF_INET6 && !socket_ipv6_is_supported())
+ return false;
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ if (link->iftype == ARPHRD_CAN)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ return link->network->dhcp & (family == AF_INET ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_IPV6);
+}
+
+void network_adjust_dhcp(Network *network) {
+ assert(network);
+ assert(network->dhcp >= 0);
+
+ if (network->dhcp == ADDRESS_FAMILY_NO)
+ return;
+
+ /* Bonding slave does not support addressing. */
+ if (network->bond) {
+ log_warning("%s: Cannot enable DHCP= when Bond= is specified, disabling DHCP=.",
+ network->filename);
+ network->dhcp = ADDRESS_FAMILY_NO;
+ return;
+ }
+
+ if (!FLAGS_SET(network->link_local, ADDRESS_FAMILY_IPV6) &&
+ FLAGS_SET(network->dhcp, ADDRESS_FAMILY_IPV6)) {
+ log_warning("%s: DHCPv6 client is enabled but IPv6 link-local addressing is disabled. "
+ "Disabling DHCPv6 client.", network->filename);
+ SET_FLAG(network->dhcp, ADDRESS_FAMILY_IPV6, false);
+ }
+
+ network_adjust_dhcp4(network);
+}
+
+static bool duid_needs_product_uuid(const DUID *duid) {
+ assert(duid);
+
+ return duid->type == DUID_TYPE_UUID && duid->raw_data_len == 0;
+}
+
+static const struct DUID fallback_duid = { .type = DUID_TYPE_EN };
+
+const DUID *link_get_duid(Link *link, int family) {
+ const DUID *duid;
+
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+
+ if (link->network) {
+ duid = family == AF_INET ? &link->network->dhcp_duid : &link->network->dhcp6_duid;
+ if (duid->type != _DUID_TYPE_INVALID) {
+ if (duid_needs_product_uuid(duid))
+ return &link->manager->duid_product_uuid;
+ else
+ return duid;
+ }
+ }
+
+ duid = family == AF_INET ? &link->manager->dhcp_duid : &link->manager->dhcp6_duid;
+ if (link->hw_addr.length == 0 && IN_SET(duid->type, DUID_TYPE_LLT, DUID_TYPE_LL))
+ /* Fallback to DUID that works without MAC address.
+ * This is useful for tunnel devices without MAC address. */
+ return &fallback_duid;
+
+ return duid;
+}
+
+static int get_product_uuid_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
+ Manager *manager = ASSERT_PTR(userdata);
+ const sd_bus_error *e;
+ const void *a;
+ size_t sz;
+ int r;
+
+ assert(m);
+
+ /* To avoid calling GetProductUUID() bus method so frequently, set the flag below
+ * even if the method fails. */
+ manager->has_product_uuid = true;
+
+ e = sd_bus_message_get_error(m);
+ if (e) {
+ r = sd_bus_error_get_errno(e);
+ log_warning_errno(r, "Could not get product UUID. Falling back to use machine-app-specific ID as DUID-UUID: %s",
+ bus_error_message(e, r));
+ return 0;
+ }
+
+ r = sd_bus_message_read_array(m, 'y', &a, &sz);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to get product UUID. Falling back to use machine-app-specific ID as DUID-UUID: %m");
+ return 0;
+ }
+
+ if (sz != sizeof(sd_id128_t)) {
+ log_warning("Invalid product UUID. Falling back to use machine-app-specific ID as DUID-UUID.");
+ return 0;
+ }
+
+ log_debug("Successfully obtained product UUID");
+
+ memcpy(&manager->duid_product_uuid.raw_data, a, sz);
+ manager->duid_product_uuid.raw_data_len = sz;
+
+ return 0;
+}
+
+int manager_request_product_uuid(Manager *m) {
+ static bool bus_method_is_called = false;
+ int r;
+
+ assert(m);
+
+ if (bus_method_is_called)
+ return 0;
+
+ if (sd_bus_is_ready(m->bus) <= 0 && !m->product_uuid_requested) {
+ log_debug("Not connected to system bus, requesting product UUID later.");
+ m->product_uuid_requested = true;
+ return 0;
+ }
+
+ m->product_uuid_requested = false;
+
+ r = bus_call_method_async(
+ m->bus,
+ NULL,
+ bus_hostname,
+ "GetProductUUID",
+ get_product_uuid_handler,
+ m,
+ "b",
+ false);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to get product UUID: %m");
+
+ log_debug("Requesting product UUID.");
+
+ bus_method_is_called = true;
+
+ return 0;
+}
+
+int dhcp_configure_duid(Link *link, const DUID *duid) {
+ Manager *m;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(duid);
+
+ m = link->manager;
+
+ if (!duid_needs_product_uuid(duid))
+ return 1;
+
+ if (m->has_product_uuid)
+ return 1;
+
+ r = manager_request_product_uuid(m);
+ if (r < 0) {
+ log_link_warning_errno(link, r,
+ "Failed to get product UUID. Falling back to use machine-app-specific ID as DUID-UUID: %m");
+
+ m->has_product_uuid = true; /* Do not request UUID again on failure. */
+ return 1;
+ }
+
+ return 0;
+}
+
+bool address_is_filtered(int family, const union in_addr_union *address, uint8_t prefixlen, Set *allow_list, Set *deny_list) {
+ struct in_addr_prefix *p;
+
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(address);
+
+ if (allow_list) {
+ SET_FOREACH(p, allow_list)
+ if (p->family == family &&
+ p->prefixlen <= prefixlen &&
+ in_addr_prefix_covers(family, &p->address, p->prefixlen, address) > 0)
+ return false;
+
+ return true;
+ }
+
+ SET_FOREACH(p, deny_list)
+ if (p->family == family &&
+ in_addr_prefix_intersect(family, &p->address, p->prefixlen, address, prefixlen) > 0)
+ return true;
+
+ return false;
+}
+
+int config_parse_dhcp(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ AddressFamily *dhcp = data, s;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ /* Note that this is mostly like
+ * config_parse_address_family(), except that it
+ * understands some old names for the enum values */
+
+ s = address_family_from_string(rvalue);
+ if (s < 0) {
+
+ /* Previously, we had a slightly different enum here,
+ * support its values for compatibility. */
+
+ s = dhcp_deprecated_address_family_from_string(rvalue);
+ if (s < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, s,
+ "Failed to parse DHCP option, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "DHCP=%s is deprecated, please use DHCP=%s instead.",
+ rvalue, address_family_to_string(s));
+ }
+
+ *dhcp = s;
+ return 0;
+}
+
+int config_parse_dhcp_or_ra_route_metric(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ uint32_t metric;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou32(rvalue, &metric);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse RouteMetric=%s, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ switch (ltype) {
+ case AF_INET:
+ network->dhcp_route_metric = metric;
+ network->dhcp_route_metric_set = true;
+ break;
+ case AF_INET6:
+ network->ipv6_accept_ra_route_metric = metric;
+ network->ipv6_accept_ra_route_metric_set = true;
+ break;
+ case AF_UNSPEC:
+ /* For backward compatibility. */
+ if (!network->dhcp_route_metric_set)
+ network->dhcp_route_metric = metric;
+ if (!network->ipv6_accept_ra_route_metric_set)
+ network->ipv6_accept_ra_route_metric = metric;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+int config_parse_dhcp_use_dns(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(rvalue);
+ assert(data);
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse UseDNS=%s, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ switch (ltype) {
+ case AF_INET:
+ network->dhcp_use_dns = r;
+ network->dhcp_use_dns_set = true;
+ break;
+ case AF_INET6:
+ network->dhcp6_use_dns = r;
+ network->dhcp6_use_dns_set = true;
+ break;
+ case AF_UNSPEC:
+ /* For backward compatibility. */
+ if (!network->dhcp_use_dns_set)
+ network->dhcp_use_dns = r;
+ if (!network->dhcp6_use_dns_set)
+ network->dhcp6_use_dns = r;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+int config_parse_dhcp_use_domains(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ DHCPUseDomains d;
+
+ assert(filename);
+ assert(lvalue);
+ assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(rvalue);
+ assert(data);
+
+ d = dhcp_use_domains_from_string(rvalue);
+ if (d < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, d,
+ "Failed to parse %s=%s, ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ switch (ltype) {
+ case AF_INET:
+ network->dhcp_use_domains = d;
+ network->dhcp_use_domains_set = true;
+ break;
+ case AF_INET6:
+ network->dhcp6_use_domains = d;
+ network->dhcp6_use_domains_set = true;
+ break;
+ case AF_UNSPEC:
+ /* For backward compatibility. */
+ if (!network->dhcp_use_domains_set)
+ network->dhcp_use_domains = d;
+ if (!network->dhcp6_use_domains_set)
+ network->dhcp6_use_domains = d;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+int config_parse_dhcp_use_ntp(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(rvalue);
+ assert(data);
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse UseNTP=%s, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ switch (ltype) {
+ case AF_INET:
+ network->dhcp_use_ntp = r;
+ network->dhcp_use_ntp_set = true;
+ break;
+ case AF_INET6:
+ network->dhcp6_use_ntp = r;
+ network->dhcp6_use_ntp_set = true;
+ break;
+ case AF_UNSPEC:
+ /* For backward compatibility. */
+ if (!network->dhcp_use_ntp_set)
+ network->dhcp_use_ntp = r;
+ if (!network->dhcp6_use_ntp_set)
+ network->dhcp6_use_ntp = r;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+int config_parse_dhcp_or_ra_route_table(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ uint32_t rt;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(IN_SET(ltype, AF_INET, AF_INET6));
+ assert(rvalue);
+
+ r = manager_get_route_table_from_string(network->manager, rvalue, &rt);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse RouteTable=%s, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ switch (ltype) {
+ case AF_INET:
+ network->dhcp_route_table = rt;
+ network->dhcp_route_table_set = true;
+ break;
+ case AF_INET6:
+ network->ipv6_accept_ra_route_table = rt;
+ network->ipv6_accept_ra_route_table_set = true;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+int config_parse_iaid(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ uint32_t iaid;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(IN_SET(ltype, AF_INET, AF_INET6));
+
+ r = safe_atou32(rvalue, &iaid);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Unable to read IAID, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (ltype == AF_INET) {
+ network->dhcp_iaid = iaid;
+ network->dhcp_iaid_set = true;
+ if (!network->dhcp6_iaid_set_explicitly) {
+ /* Backward compatibility. Previously, IAID is shared by DHCPv4 and DHCPv6.
+ * If DHCPv6 IAID is not specified explicitly, then use DHCPv4 IAID for DHCPv6. */
+ network->dhcp6_iaid = iaid;
+ network->dhcp6_iaid_set = true;
+ }
+ } else {
+ assert(ltype == AF_INET6);
+ network->dhcp6_iaid = iaid;
+ network->dhcp6_iaid_set = true;
+ network->dhcp6_iaid_set_explicitly = true;
+ }
+
+ return 0;
+}
+
+int config_parse_dhcp_user_or_vendor_class(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ char ***l = ASSERT_PTR(data);
+ int r;
+
+ assert(lvalue);
+ assert(rvalue);
+ assert(IN_SET(ltype, AF_INET, AF_INET6));
+
+ if (isempty(rvalue)) {
+ *l = strv_free(*l);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *w = NULL;
+ size_t len;
+
+ r = extract_first_word(&p, &w, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to split user classes option, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ len = strlen(w);
+ if (ltype == AF_INET) {
+ if (len > UINT8_MAX || len == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "%s length is not in the range 1…255, ignoring.", w);
+ continue;
+ }
+ } else {
+ if (len > UINT16_MAX || len == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "%s length is not in the range 1…65535, ignoring.", w);
+ continue;
+ }
+ }
+
+ r = strv_consume(l, TAKE_PTR(w));
+ if (r < 0)
+ return log_oom();
+ }
+}
+
+int config_parse_dhcp_send_option(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(sd_dhcp_option_unrefp) sd_dhcp_option *opt4 = NULL;
+ _cleanup_(sd_dhcp6_option_unrefp) sd_dhcp6_option *opt6 = NULL;
+ _unused_ _cleanup_(sd_dhcp_option_unrefp) sd_dhcp_option *old4 = NULL;
+ _unused_ _cleanup_(sd_dhcp6_option_unrefp) sd_dhcp6_option *old6 = NULL;
+ uint32_t uint32_data, enterprise_identifier = 0;
+ _cleanup_free_ char *word = NULL, *q = NULL;
+ OrderedHashmap **options = ASSERT_PTR(data);
+ uint16_t u16, uint16_data;
+ union in_addr_union addr;
+ DHCPOptionDataType type;
+ uint8_t u8, uint8_data;
+ const void *udata;
+ const char *p;
+ ssize_t sz;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *options = ordered_hashmap_free(*options);
+ return 0;
+ }
+
+ p = rvalue;
+ if (ltype == AF_INET6 && streq(lvalue, "SendVendorOption")) {
+ r = extract_first_word(&p, &word, ":", 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r <= 0 || isempty(p)) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid DHCP option, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ r = safe_atou32(word, &enterprise_identifier);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCPv6 enterprise identifier data, ignoring assignment: %s", p);
+ return 0;
+ }
+ word = mfree(word);
+ }
+
+ r = extract_first_word(&p, &word, ":", 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r <= 0 || isempty(p)) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid DHCP option, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (ltype == AF_INET6) {
+ r = safe_atou16(word, &u16);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid DHCP option, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (u16 < 1 || u16 >= UINT16_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid DHCP option, valid range is 1-65535, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ } else {
+ r = safe_atou8(word, &u8);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid DHCP option, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (u8 < 1 || u8 >= UINT8_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid DHCP option, valid range is 1-254, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ }
+
+ word = mfree(word);
+ r = extract_first_word(&p, &word, ":", 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r <= 0 || isempty(p)) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid DHCP option, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ type = dhcp_option_data_type_from_string(word);
+ if (type < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, type,
+ "Invalid DHCP option data type, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ switch (type) {
+ case DHCP_OPTION_DATA_UINT8:{
+ r = safe_atou8(p, &uint8_data);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP uint8 data, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ udata = &uint8_data;
+ sz = sizeof(uint8_t);
+ break;
+ }
+ case DHCP_OPTION_DATA_UINT16:{
+ uint16_t k;
+
+ r = safe_atou16(p, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP uint16 data, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ uint16_data = htobe16(k);
+ udata = &uint16_data;
+ sz = sizeof(uint16_t);
+ break;
+ }
+ case DHCP_OPTION_DATA_UINT32: {
+ uint32_t k;
+
+ r = safe_atou32(p, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP uint32 data, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ uint32_data = htobe32(k);
+ udata = &uint32_data;
+ sz = sizeof(uint32_t);
+
+ break;
+ }
+ case DHCP_OPTION_DATA_IPV4ADDRESS: {
+ r = in_addr_from_string(AF_INET, p, &addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP ipv4address data, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ udata = &addr.in;
+ sz = sizeof(addr.in.s_addr);
+ break;
+ }
+ case DHCP_OPTION_DATA_IPV6ADDRESS: {
+ r = in_addr_from_string(AF_INET6, p, &addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP ipv6address data, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ udata = &addr.in6;
+ sz = sizeof(addr.in6.s6_addr);
+ break;
+ }
+ case DHCP_OPTION_DATA_STRING:
+ sz = cunescape(p, UNESCAPE_ACCEPT_NUL, &q);
+ if (sz < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, sz,
+ "Failed to decode DHCP option data, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ udata = q;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (ltype == AF_INET6) {
+ r = sd_dhcp6_option_new(u16, udata, sz, enterprise_identifier, &opt6);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store DHCP option '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ r = ordered_hashmap_ensure_allocated(options, &dhcp6_option_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ /* Overwrite existing option */
+ old6 = ordered_hashmap_get(*options, UINT_TO_PTR(u16));
+ r = ordered_hashmap_replace(*options, UINT_TO_PTR(u16), opt6);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store DHCP option '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+ TAKE_PTR(opt6);
+ } else {
+ r = sd_dhcp_option_new(u8, udata, sz, &opt4);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store DHCP option '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ r = ordered_hashmap_ensure_allocated(options, &dhcp_option_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ /* Overwrite existing option */
+ old4 = ordered_hashmap_get(*options, UINT_TO_PTR(u8));
+ r = ordered_hashmap_replace(*options, UINT_TO_PTR(u8), opt4);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store DHCP option '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+ TAKE_PTR(opt4);
+ }
+ return 0;
+}
+
+int config_parse_dhcp_request_options(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ if (ltype == AF_INET)
+ network->dhcp_request_options = set_free(network->dhcp_request_options);
+ else
+ network->dhcp6_request_options = set_free(network->dhcp6_request_options);
+
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *n = NULL;
+ uint32_t i;
+
+ r = extract_first_word(&p, &n, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP request option, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ r = safe_atou32(n, &i);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "DHCP request option is invalid, ignoring assignment: %s", n);
+ continue;
+ }
+
+ if (i < 1 || i >= UINT8_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "DHCP request option is invalid, valid range is 1-254, ignoring assignment: %s", n);
+ continue;
+ }
+
+ r = set_ensure_put(ltype == AF_INET ? &network->dhcp_request_options : &network->dhcp6_request_options,
+ NULL, UINT32_TO_PTR(i));
+ if (r < 0)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store DHCP request option '%s', ignoring assignment: %m", n);
+ }
+}
+
+static const char* const dhcp_use_domains_table[_DHCP_USE_DOMAINS_MAX] = {
+ [DHCP_USE_DOMAINS_NO] = "no",
+ [DHCP_USE_DOMAINS_ROUTE] = "route",
+ [DHCP_USE_DOMAINS_YES] = "yes",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(dhcp_use_domains, DHCPUseDomains, DHCP_USE_DOMAINS_YES);
+
+static const char * const dhcp_option_data_type_table[_DHCP_OPTION_DATA_MAX] = {
+ [DHCP_OPTION_DATA_UINT8] = "uint8",
+ [DHCP_OPTION_DATA_UINT16] = "uint16",
+ [DHCP_OPTION_DATA_UINT32] = "uint32",
+ [DHCP_OPTION_DATA_STRING] = "string",
+ [DHCP_OPTION_DATA_IPV4ADDRESS] = "ipv4address",
+ [DHCP_OPTION_DATA_IPV6ADDRESS] = "ipv6address",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(dhcp_option_data_type, DHCPOptionDataType);
+
+static const char* const duid_type_table[_DUID_TYPE_MAX] = {
+ [DUID_TYPE_LLT] = "link-layer-time",
+ [DUID_TYPE_EN] = "vendor",
+ [DUID_TYPE_LL] = "link-layer",
+ [DUID_TYPE_UUID] = "uuid",
+};
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(duid_type, DUIDType);
+
+int config_parse_duid_type(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *type_string = NULL;
+ const char *p = ASSERT_PTR(rvalue);
+ bool force = ltype;
+ DUID *duid = ASSERT_PTR(data);
+ DUIDType type;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+
+ if (!force && duid->set)
+ return 0;
+
+ r = extract_first_word(&p, &type_string, ":", 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid syntax, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to extract DUID type from '%s', ignoring.", rvalue);
+ return 0;
+ }
+
+ type = duid_type_from_string(type_string);
+ if (type < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, type,
+ "Failed to parse DUID type '%s', ignoring.", type_string);
+ return 0;
+ }
+
+ if (!isempty(p)) {
+ usec_t u;
+
+ if (type != DUID_TYPE_LLT) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid syntax, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ r = parse_timestamp(p, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse timestamp, ignoring: %s", p);
+ return 0;
+ }
+
+ duid->llt_time = u;
+ }
+
+ duid->type = type;
+ duid->set = force;
+
+ return 0;
+}
+
+int config_parse_manager_duid_type(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Manager *manager = ASSERT_PTR(userdata);
+ int r;
+
+ /* For backward compatibility. Setting both DHCPv4 and DHCPv6 DUID if they are not specified explicitly. */
+
+ r = config_parse_duid_type(unit, filename, line, section, section_line, lvalue, false, rvalue, &manager->dhcp_duid, manager);
+ if (r < 0)
+ return r;
+
+ return config_parse_duid_type(unit, filename, line, section, section_line, lvalue, false, rvalue, &manager->dhcp6_duid, manager);
+}
+
+int config_parse_network_duid_type(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ int r;
+
+ r = config_parse_duid_type(unit, filename, line, section, section_line, lvalue, true, rvalue, &network->dhcp_duid, network);
+ if (r < 0)
+ return r;
+
+ /* For backward compatibility, also set DHCPv6 DUID if not specified explicitly. */
+ return config_parse_duid_type(unit, filename, line, section, section_line, lvalue, false, rvalue, &network->dhcp6_duid, network);
+}
+
+int config_parse_duid_rawdata(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint8_t raw_data[MAX_DUID_LEN];
+ unsigned count = 0;
+ bool force = ltype;
+ DUID *duid = ASSERT_PTR(data);
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (!force && duid->set)
+ return 0;
+
+ /* RawData contains DUID in format "NN:NN:NN..." */
+ for (const char *p = rvalue;;) {
+ int n1, n2, len, r;
+ uint32_t byte;
+ _cleanup_free_ char *cbyte = NULL;
+
+ r = extract_first_word(&p, &cbyte, ":", 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to read DUID, ignoring assignment: %s.", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ break;
+
+ if (count >= MAX_DUID_LEN) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Max DUID length exceeded, ignoring assignment: %s.", rvalue);
+ return 0;
+ }
+
+ len = strlen(cbyte);
+ if (!IN_SET(len, 1, 2)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid length - DUID byte: %s, ignoring assignment: %s.", cbyte, rvalue);
+ return 0;
+ }
+ n1 = unhexchar(cbyte[0]);
+ if (len == 2)
+ n2 = unhexchar(cbyte[1]);
+ else
+ n2 = 0;
+
+ if (n1 < 0 || n2 < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid DUID byte: %s. Ignoring assignment: %s.", cbyte, rvalue);
+ return 0;
+ }
+
+ byte = ((uint8_t) n1 << (4 * (len-1))) | (uint8_t) n2;
+ raw_data[count++] = byte;
+ }
+
+ assert_cc(sizeof(raw_data) == sizeof(duid->raw_data));
+ memcpy(duid->raw_data, raw_data, count);
+ duid->raw_data_len = count;
+ duid->set = force;
+
+ return 0;
+}
+
+int config_parse_manager_duid_rawdata(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Manager *manager = ASSERT_PTR(userdata);
+ int r;
+
+ /* For backward compatibility. Setting both DHCPv4 and DHCPv6 DUID if they are not specified explicitly. */
+
+ r = config_parse_duid_rawdata(unit, filename, line, section, section_line, lvalue, false, rvalue, &manager->dhcp_duid, manager);
+ if (r < 0)
+ return r;
+
+ return config_parse_duid_rawdata(unit, filename, line, section, section_line, lvalue, false, rvalue, &manager->dhcp6_duid, manager);
+}
+
+int config_parse_network_duid_rawdata(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ int r;
+
+ r = config_parse_duid_rawdata(unit, filename, line, section, section_line, lvalue, true, rvalue, &network->dhcp_duid, network);
+ if (r < 0)
+ return r;
+
+ /* For backward compatibility, also set DHCPv6 DUID if not specified explicitly. */
+ return config_parse_duid_rawdata(unit, filename, line, section, section_line, lvalue, false, rvalue, &network->dhcp6_duid, network);
+}
+
+int config_parse_uplink(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ bool accept_none = true;
+ int *index, r;
+ char **name;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (streq(section, "DHCPServer")) {
+ index = &network->dhcp_server_uplink_index;
+ name = &network->dhcp_server_uplink_name;
+ } else if (streq(section, "IPv6SendRA")) {
+ index = &network->router_uplink_index;
+ name = &network->router_uplink_name;
+ } else if (STR_IN_SET(section, "DHCPv6PrefixDelegation", "DHCPPrefixDelegation")) {
+ index = &network->dhcp_pd_uplink_index;
+ name = &network->dhcp_pd_uplink_name;
+ accept_none = false;
+ } else
+ assert_not_reached();
+
+ if (isempty(rvalue) || streq(rvalue, ":auto")) {
+ *index = UPLINK_INDEX_AUTO;
+ *name = mfree(*name);
+ return 0;
+ }
+
+ if (accept_none && streq(rvalue, ":none")) {
+ *index = UPLINK_INDEX_NONE;
+ *name = mfree(*name);
+ return 0;
+ }
+
+ if (!accept_none && streq(rvalue, ":self")) {
+ *index = UPLINK_INDEX_SELF;
+ *name = mfree(*name);
+ return 0;
+ }
+
+ r = parse_ifindex(rvalue);
+ if (r > 0) {
+ *index = r;
+ *name = mfree(*name);
+ return 0;
+ }
+
+ if (!ifname_valid_full(rvalue, IFNAME_VALID_ALTERNATIVE)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid interface name in %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ /* The interface name will be resolved later. */
+ r = free_and_strdup_warn(name, rvalue);
+ if (r < 0)
+ return r;
+
+ /* Note, if uplink_name is set, then uplink_index will be ignored. So, the below does not mean
+ * an uplink interface will be selected automatically. */
+ *index = UPLINK_INDEX_AUTO;
+ return 0;
+}
diff --git a/src/network/networkd-dhcp-common.h b/src/network/networkd-dhcp-common.h
new file mode 100644
index 0000000..c19bc10
--- /dev/null
+++ b/src/network/networkd-dhcp-common.h
@@ -0,0 +1,107 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+
+#include "conf-parser.h"
+#include "dhcp-identifier.h"
+#include "in-addr-util.h"
+#include "set.h"
+#include "time-util.h"
+
+/* Special values for *_uplink_index. */
+#define UPLINK_INDEX_AUTO 0 /* uplink will be selected automatically */
+#define UPLINK_INDEX_NONE -1 /* uplink will not be selected automatically */
+#define UPLINK_INDEX_SELF -2 /* the interface itself is uplink */
+
+#define DHCP_ROUTE_METRIC 1024
+#define DHCP6PD_ROUTE_METRIC 256
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+typedef struct Network Network;
+
+typedef enum DHCPUseDomains {
+ DHCP_USE_DOMAINS_NO,
+ DHCP_USE_DOMAINS_YES,
+ DHCP_USE_DOMAINS_ROUTE,
+ _DHCP_USE_DOMAINS_MAX,
+ _DHCP_USE_DOMAINS_INVALID = -EINVAL,
+} DHCPUseDomains;
+
+typedef enum DHCPOptionDataType {
+ DHCP_OPTION_DATA_UINT8,
+ DHCP_OPTION_DATA_UINT16,
+ DHCP_OPTION_DATA_UINT32,
+ DHCP_OPTION_DATA_STRING,
+ DHCP_OPTION_DATA_IPV4ADDRESS,
+ DHCP_OPTION_DATA_IPV6ADDRESS,
+ _DHCP_OPTION_DATA_MAX,
+ _DHCP_OPTION_DATA_INVALID,
+} DHCPOptionDataType;
+
+typedef struct DUID {
+ /* Value of Type in [DHCP] section */
+ DUIDType type;
+
+ uint8_t raw_data_len;
+ uint8_t raw_data[MAX_DUID_LEN];
+ usec_t llt_time;
+ bool set;
+} DUID;
+
+uint32_t link_get_dhcp4_route_table(Link *link);
+uint32_t link_get_ipv6_accept_ra_route_table(Link *link);
+
+bool link_dhcp_enabled(Link *link, int family);
+static inline bool link_dhcp4_enabled(Link *link) {
+ return link_dhcp_enabled(link, AF_INET);
+}
+static inline bool link_dhcp6_enabled(Link *link) {
+ return link_dhcp_enabled(link, AF_INET6);
+}
+
+void network_adjust_dhcp(Network *network);
+
+const DUID *link_get_duid(Link *link, int family);
+static inline const DUID *link_get_dhcp4_duid(Link *link) {
+ return link_get_duid(link, AF_INET);
+}
+static inline const DUID *link_get_dhcp6_duid(Link *link) {
+ return link_get_duid(link, AF_INET6);
+}
+
+int dhcp_configure_duid(Link *link, const DUID *duid);
+int manager_request_product_uuid(Manager *m);
+
+bool address_is_filtered(int family, const union in_addr_union *address, uint8_t prefixlen, Set *allow_list, Set *deny_list);
+static inline bool in4_address_is_filtered(const struct in_addr *address, Set *allow_list, Set *deny_list) {
+ return address_is_filtered(AF_INET, &(union in_addr_union) { .in = *address }, 32, allow_list, deny_list);
+}
+static inline bool in6_prefix_is_filtered(const struct in6_addr *prefix, uint8_t prefixlen, Set *allow_list, Set *deny_list) {
+ return address_is_filtered(AF_INET6, &(union in_addr_union) { .in6 = *prefix }, prefixlen, allow_list, deny_list);
+}
+
+const char* dhcp_use_domains_to_string(DHCPUseDomains p) _const_;
+DHCPUseDomains dhcp_use_domains_from_string(const char *s) _pure_;
+
+const char *dhcp_option_data_type_to_string(DHCPOptionDataType d) _const_;
+DHCPOptionDataType dhcp_option_data_type_from_string(const char *d) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_or_ra_route_metric);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_dns);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_domains);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_ntp);
+CONFIG_PARSER_PROTOTYPE(config_parse_iaid);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_or_ra_route_table);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_user_or_vendor_class);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_send_option);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_request_options);
+CONFIG_PARSER_PROTOTYPE(config_parse_duid_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_manager_duid_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_network_duid_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_duid_rawdata);
+CONFIG_PARSER_PROTOTYPE(config_parse_manager_duid_rawdata);
+CONFIG_PARSER_PROTOTYPE(config_parse_network_duid_rawdata);
+CONFIG_PARSER_PROTOTYPE(config_parse_uplink);
diff --git a/src/network/networkd-dhcp-prefix-delegation.c b/src/network/networkd-dhcp-prefix-delegation.c
new file mode 100644
index 0000000..084f1c3
--- /dev/null
+++ b/src/network/networkd-dhcp-prefix-delegation.c
@@ -0,0 +1,1298 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/ipv6_route.h>
+
+#include "sd-dhcp6-client.h"
+
+#include "hashmap.h"
+#include "in-addr-prefix-util.h"
+#include "networkd-address-generation.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-prefix-delegation.h"
+#include "networkd-dhcp6.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-queue.h"
+#include "networkd-radv.h"
+#include "networkd-route.h"
+#include "networkd-setlink.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tunnel.h"
+
+bool link_dhcp_pd_is_enabled(Link *link) {
+ assert(link);
+
+ if (!link->network)
+ return false;
+
+ return link->network->dhcp_pd;
+}
+
+bool dhcp_pd_is_uplink(Link *link, Link *target, bool accept_auto) {
+ assert(link);
+ assert(target);
+
+ if (!link_dhcp_pd_is_enabled(link))
+ return false;
+
+ if (link->network->dhcp_pd_uplink_name)
+ return streq_ptr(target->ifname, link->network->dhcp_pd_uplink_name) ||
+ strv_contains(target->alternative_names, link->network->dhcp_pd_uplink_name);
+
+ if (link->network->dhcp_pd_uplink_index > 0)
+ return target->ifindex == link->network->dhcp_pd_uplink_index;
+
+ if (link->network->dhcp_pd_uplink_index == UPLINK_INDEX_SELF)
+ return link == target;
+
+ assert(link->network->dhcp_pd_uplink_index == UPLINK_INDEX_AUTO);
+ return accept_auto;
+}
+
+bool dhcp4_lease_has_pd_prefix(sd_dhcp_lease *lease) {
+ if (!lease)
+ return false;
+
+ return sd_dhcp_lease_get_6rd(lease, NULL, NULL, NULL, NULL, NULL) >= 0;
+}
+
+bool dhcp6_lease_has_pd_prefix(sd_dhcp6_lease *lease) {
+ uint32_t lifetime_preferred_sec, lifetime_valid_sec;
+ struct in6_addr pd_prefix;
+ uint8_t pd_prefix_len;
+
+ if (!lease)
+ return false;
+
+ sd_dhcp6_lease_reset_pd_prefix_iter(lease);
+
+ return sd_dhcp6_lease_get_pd(lease, &pd_prefix, &pd_prefix_len, &lifetime_preferred_sec, &lifetime_valid_sec) >= 0;
+}
+
+static void link_remove_dhcp_pd_subnet_prefix(Link *link, const struct in6_addr *prefix) {
+ void *key;
+
+ assert(link);
+ assert(link->manager);
+ assert(prefix);
+
+ if (hashmap_get(link->manager->links_by_dhcp_pd_subnet_prefix, prefix) != link)
+ return;
+
+ hashmap_remove2(link->manager->links_by_dhcp_pd_subnet_prefix, prefix, &key);
+ free(key);
+}
+
+static int link_add_dhcp_pd_subnet_prefix(Link *link, const struct in6_addr *prefix) {
+ _cleanup_free_ struct in6_addr *copy = NULL;
+ int r;
+
+ assert(link);
+ assert(prefix);
+
+ copy = newdup(struct in6_addr, prefix, 1);
+ if (!copy)
+ return -ENOMEM;
+
+ r = hashmap_ensure_put(&link->manager->links_by_dhcp_pd_subnet_prefix, &in6_addr_hash_ops_free, copy, link);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ TAKE_PTR(copy);
+
+ return 0;
+}
+
+static int link_get_by_dhcp_pd_subnet_prefix(Manager *manager, const struct in6_addr *prefix, Link **ret) {
+ Link *link;
+
+ assert(manager);
+ assert(prefix);
+
+ link = hashmap_get(manager->links_by_dhcp_pd_subnet_prefix, prefix);
+ if (!link)
+ return -ENODEV;
+
+ if (ret)
+ *ret = link;
+ return 0;
+}
+
+static int dhcp_pd_get_assigned_subnet_prefix(Link *link, const struct in6_addr *pd_prefix, uint8_t pd_prefix_len, struct in6_addr *ret) {
+ assert(link);
+ assert(pd_prefix);
+
+ if (!link_dhcp_pd_is_enabled(link))
+ return -ENOENT;
+
+ if (link->network->dhcp_pd_assign) {
+ Address *address;
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_DHCP_PD)
+ continue;
+ assert(address->family == AF_INET6);
+
+ if (in6_addr_prefix_covers(pd_prefix, pd_prefix_len, &address->in_addr.in6) <= 0)
+ continue;
+
+ if (ret) {
+ struct in6_addr prefix = address->in_addr.in6;
+
+ in6_addr_mask(&prefix, 64);
+ *ret = prefix;
+ }
+ return 0;
+ }
+ } else {
+ Route *route;
+
+ SET_FOREACH(route, link->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_DHCP_PD)
+ continue;
+ assert(route->family == AF_INET6);
+
+ if (in6_addr_prefix_covers(pd_prefix, pd_prefix_len, &route->dst.in6) > 0) {
+ if (ret)
+ *ret = route->dst.in6;
+ return 0;
+ }
+ }
+ }
+
+ return -ENOENT;
+}
+
+int dhcp_pd_remove(Link *link, bool only_marked) {
+ int k, r = 0;
+
+ assert(link);
+ assert(link->manager);
+
+ if (!link_dhcp_pd_is_enabled(link))
+ return 0;
+
+ if (!only_marked)
+ link->dhcp_pd_configured = false;
+
+ if (!link->network->dhcp_pd_assign) {
+ Route *route;
+
+ SET_FOREACH(route, link->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_DHCP_PD)
+ continue;
+ if (only_marked && !route_is_marked(route))
+ continue;
+
+ if (link->radv)
+ sd_radv_remove_prefix(link->radv, &route->dst.in6, 64);
+
+ link_remove_dhcp_pd_subnet_prefix(link, &route->dst.in6);
+
+ k = route_remove(route);
+ if (k < 0)
+ r = k;
+
+ route_cancel_request(route, link);
+ }
+ } else {
+ Address *address;
+
+ SET_FOREACH(address, link->addresses) {
+ struct in6_addr prefix;
+
+ if (address->source != NETWORK_CONFIG_SOURCE_DHCP_PD)
+ continue;
+ if (only_marked && !address_is_marked(address))
+ continue;
+
+ prefix = address->in_addr.in6;
+ in6_addr_mask(&prefix, 64);
+
+ if (link->radv)
+ sd_radv_remove_prefix(link->radv, &prefix, 64);
+
+ link_remove_dhcp_pd_subnet_prefix(link, &prefix);
+
+ k = address_remove(address);
+ if (k < 0)
+ r = k;
+
+ address_cancel_request(address);
+ }
+ }
+
+ return r;
+}
+
+static int dhcp_pd_check_ready(Link *link);
+
+static int dhcp_pd_address_ready_callback(Address *address) {
+ Address *a;
+
+ assert(address);
+ assert(address->link);
+
+ SET_FOREACH(a, address->link->addresses)
+ if (a->source == NETWORK_CONFIG_SOURCE_DHCP_PD)
+ a->callback = NULL;
+
+ return dhcp_pd_check_ready(address->link);
+}
+
+static int dhcp_pd_check_ready(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->dhcp_pd_messages > 0) {
+ log_link_debug(link, "%s(): DHCP-PD addresses and routes are not set.", __func__);
+ return 0;
+ }
+
+ if (link->network->dhcp_pd_assign) {
+ bool has_ready = false;
+ Address *address;
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_DHCP_PD)
+ continue;
+ if (address_is_ready(address)) {
+ has_ready = true;
+ break;
+ }
+ }
+
+ if (!has_ready) {
+ SET_FOREACH(address, link->addresses)
+ if (address->source == NETWORK_CONFIG_SOURCE_DHCP_PD)
+ address->callback = dhcp_pd_address_ready_callback;
+
+ log_link_debug(link, "%s(): no DHCP-PD address is ready.", __func__);
+ return 0;
+ }
+ }
+
+ link->dhcp_pd_configured = true;
+
+ log_link_debug(link, "DHCP-PD addresses and routes set.");
+
+ r = dhcp_pd_remove(link, /* only_marked = */ true);
+ if (r < 0)
+ return r;
+
+ link_check_ready(link);
+ return 1;
+}
+
+static int dhcp_pd_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) {
+ int r;
+
+ assert(link);
+
+ r = route_configure_handler_internal(rtnl, m, link, "Failed to add prefix route for DHCP delegated subnet prefix");
+ if (r <= 0)
+ return r;
+
+ r = dhcp_pd_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int dhcp_pd_request_route(Link *link, const struct in6_addr *prefix, usec_t lifetime_usec) {
+ _cleanup_(route_freep) Route *route = NULL;
+ Route *existing;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(prefix);
+
+ if (link->network->dhcp_pd_assign)
+ return 0;
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route->source = NETWORK_CONFIG_SOURCE_DHCP_PD;
+ route->family = AF_INET6;
+ route->dst.in6 = *prefix;
+ route->dst_prefixlen = 64;
+ route->protocol = RTPROT_DHCP;
+ route->priority = link->network->dhcp_pd_route_metric;
+ route->lifetime_usec = lifetime_usec;
+
+ if (route_get(NULL, link, route, &existing) < 0)
+ link->dhcp_pd_configured = false;
+ else
+ route_unmark(existing);
+
+ r = link_request_route(link, TAKE_PTR(route), true, &link->dhcp_pd_messages,
+ dhcp_pd_route_handler, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request DHCP-PD prefix route: %m");
+
+ return 0;
+}
+
+static int dhcp_pd_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) {
+ int r;
+
+ assert(link);
+
+ r = address_configure_handler_internal(rtnl, m, link, "Could not set DHCP-PD address");
+ if (r <= 0)
+ return r;
+
+ r = dhcp_pd_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static void log_dhcp_pd_address(Link *link, const Address *address) {
+ assert(address);
+ assert(address->family == AF_INET6);
+
+ int log_level = address_get(link, address, NULL) >= 0 ? LOG_DEBUG : LOG_INFO;
+
+ if (log_level < log_get_max_level())
+ return;
+
+ log_link_full(link, log_level, "DHCP-PD address %s (valid %s, preferred %s)",
+ IN6_ADDR_PREFIX_TO_STRING(&address->in_addr.in6, address->prefixlen),
+ FORMAT_LIFETIME(address->lifetime_valid_usec),
+ FORMAT_LIFETIME(address->lifetime_preferred_usec));
+}
+
+static int dhcp_pd_request_address(
+ Link *link,
+ const struct in6_addr *prefix,
+ usec_t lifetime_preferred_usec,
+ usec_t lifetime_valid_usec) {
+
+ _cleanup_set_free_ Set *addresses = NULL;
+ struct in6_addr *a;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(prefix);
+
+ if (!link->network->dhcp_pd_assign)
+ return 0;
+
+ r = dhcp_pd_generate_addresses(link, prefix, &addresses);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to generate addresses for acquired DHCP delegated prefix: %m");
+
+ SET_FOREACH(a, addresses) {
+ _cleanup_(address_freep) Address *address = NULL;
+ Address *existing;
+
+ r = address_new(&address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to allocate address for DHCP delegated prefix: %m");
+
+ address->source = NETWORK_CONFIG_SOURCE_DHCP_PD;
+ address->family = AF_INET6;
+ address->in_addr.in6 = *a;
+ address->prefixlen = 64;
+ address->lifetime_preferred_usec = lifetime_preferred_usec;
+ address->lifetime_valid_usec = lifetime_valid_usec;
+ SET_FLAG(address->flags, IFA_F_MANAGETEMPADDR, link->network->dhcp_pd_manage_temporary_address);
+ address->route_metric = link->network->dhcp_pd_route_metric;
+
+ log_dhcp_pd_address(link, address);
+
+ r = free_and_strdup_warn(&address->netlabel, link->network->dhcp_pd_netlabel);
+ if (r < 0)
+ return r;
+
+ if (address_get(link, address, &existing) < 0)
+ link->dhcp_pd_configured = false;
+ else
+ address_unmark(existing);
+
+ r = link_request_address(link, TAKE_PTR(address), true, &link->dhcp_pd_messages,
+ dhcp_pd_address_handler, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request DHCP delegated prefix address: %m");
+ }
+
+ return 0;
+}
+
+static int dhcp_pd_calculate_subnet_prefix(
+ const struct in6_addr *pd_prefix,
+ uint8_t pd_prefix_len,
+ uint64_t subnet_id,
+ struct in6_addr *ret) {
+
+ struct in6_addr prefix;
+
+ assert(pd_prefix);
+ assert(pd_prefix_len <= 64);
+ assert(ret);
+
+ if (subnet_id >= UINT64_C(1) << (64 - pd_prefix_len))
+ return -ERANGE;
+
+ prefix = *pd_prefix;
+
+ if (pd_prefix_len < 32)
+ prefix.s6_addr32[0] |= htobe32(subnet_id >> 32);
+
+ prefix.s6_addr32[1] |= htobe32(subnet_id & 0xffffffff);
+
+ *ret = prefix;
+ return 0;
+}
+
+static int dhcp_pd_get_preferred_subnet_prefix(
+ Link *link,
+ const struct in6_addr *pd_prefix,
+ uint8_t pd_prefix_len,
+ struct in6_addr *ret) {
+
+ struct in6_addr prefix;
+ Link *assigned_link;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+ assert(pd_prefix);
+
+ if (link->network->dhcp_pd_subnet_id >= 0) {
+ /* If the link has a preference for a particular subnet id try to allocate that */
+
+ r = dhcp_pd_calculate_subnet_prefix(pd_prefix, pd_prefix_len, link->network->dhcp_pd_subnet_id, &prefix);
+ if (r < 0)
+ return log_link_warning_errno(link, r,
+ "subnet id %" PRIi64 " is out of range. Only have %" PRIu64 " subnets.",
+ link->network->dhcp_pd_subnet_id, UINT64_C(1) << (64 - pd_prefix_len));
+
+ *ret = prefix;
+ return 0;
+ }
+
+ if (dhcp_pd_get_assigned_subnet_prefix(link, pd_prefix, pd_prefix_len, ret) >= 0)
+ return 0;
+
+ for (uint64_t n = 0; ; n++) {
+ /* If we do not have an allocation preference just iterate
+ * through the address space and return the first free prefix. */
+
+ r = dhcp_pd_calculate_subnet_prefix(pd_prefix, pd_prefix_len, n, &prefix);
+ if (r < 0)
+ return log_link_warning_errno(link, r,
+ "Couldn't find a suitable prefix. Ran out of address space.");
+
+ /* Do not use explicitly requested subnet IDs. Note that the corresponding link may not
+ * appear yet. So, we need to check the ID is not used in any .network files. */
+ if (set_contains(link->manager->dhcp_pd_subnet_ids, &n))
+ continue;
+
+ /* Check that the prefix is not assigned to another link. */
+ if (link_get_by_dhcp_pd_subnet_prefix(link->manager, &prefix, &assigned_link) < 0 ||
+ assigned_link == link)
+ break;
+ }
+
+ r = link_add_dhcp_pd_subnet_prefix(link, &prefix);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to save acquired free subnet prefix: %m");
+
+ *ret = prefix;
+ return 0;
+}
+
+static int dhcp_pd_assign_subnet_prefix(
+ Link *link,
+ const struct in6_addr *pd_prefix,
+ uint8_t pd_prefix_len,
+ usec_t lifetime_preferred_usec,
+ usec_t lifetime_valid_usec,
+ bool is_uplink) {
+
+ struct in6_addr prefix;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(pd_prefix);
+
+ r = dhcp_pd_get_preferred_subnet_prefix(link, pd_prefix, pd_prefix_len, &prefix);
+ if (r < 0)
+ return r == -ERANGE ? 0 : r;
+
+ const char *pretty = IN6_ADDR_PREFIX_TO_STRING(&prefix, 64);
+
+ if (link_radv_enabled(link) && link->network->dhcp_pd_announce) {
+ if (is_uplink)
+ log_link_debug(link, "Ignoring Announce= setting on upstream interface.");
+ else {
+ r = radv_add_prefix(link, &prefix, 64, lifetime_preferred_usec, lifetime_valid_usec);
+ if (r < 0)
+ return log_link_warning_errno(link, r,
+ "Failed to assign/update prefix %s to IPv6 Router Advertisement: %m",
+ pretty);
+ }
+ }
+
+ r = dhcp_pd_request_route(link, &prefix, lifetime_valid_usec);
+ if (r < 0)
+ return log_link_warning_errno(link, r,
+ "Failed to assign/update route for prefix %s: %m", pretty);
+
+ r = dhcp_pd_request_address(link, &prefix, lifetime_preferred_usec, lifetime_valid_usec);
+ if (r < 0)
+ return log_link_warning_errno(link, r,
+ "Failed to assign/update address for prefix %s: %m", pretty);
+
+ log_link_debug(link, "Assigned prefix %s", pretty);
+ return 1;
+}
+
+static int dhcp_pd_prepare(Link *link) {
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return 0;
+
+ if (!link_dhcp_pd_is_enabled(link))
+ return 0;
+
+ if (link_radv_enabled(link) && link->network->dhcp_pd_announce && !link->radv)
+ return 0;
+
+ link_mark_addresses(link, NETWORK_CONFIG_SOURCE_DHCP_PD);
+ link_mark_routes(link, NETWORK_CONFIG_SOURCE_DHCP_PD);
+
+ return 1;
+}
+
+static int dhcp_pd_finalize(Link *link) {
+ int r;
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return 0;
+
+ if (link->dhcp_pd_messages == 0) {
+ link->dhcp_pd_configured = false;
+
+ r = dhcp_pd_remove(link, /* only_marked = */ true);
+ if (r < 0)
+ return r;
+ }
+
+ if (!link->dhcp_pd_configured)
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ link_check_ready(link);
+ return 0;
+}
+
+void dhcp_pd_prefix_lost(Link *uplink) {
+ Route *route;
+ Link *link;
+ int r;
+
+ assert(uplink);
+ assert(uplink->manager);
+
+ HASHMAP_FOREACH(link, uplink->manager->links_by_index) {
+ if (!dhcp_pd_is_uplink(link, uplink, /* accept_auto = */ true))
+ continue;
+
+ r = dhcp_pd_remove(link, /* only_marked = */ false);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ SET_FOREACH(route, uplink->manager->routes) {
+ if (!IN_SET(route->source, NETWORK_CONFIG_SOURCE_DHCP4, NETWORK_CONFIG_SOURCE_DHCP6))
+ continue;
+ if (route->family != AF_INET6)
+ continue;
+ if (route->type != RTN_UNREACHABLE)
+ continue;
+ if (!set_contains(uplink->dhcp_pd_prefixes,
+ &(struct in_addr_prefix) {
+ .family = AF_INET6,
+ .prefixlen = route->dst_prefixlen,
+ .address = route->dst }))
+ continue;
+
+ (void) route_remove(route);
+
+ route_cancel_request(route, uplink);
+ }
+
+ set_clear(uplink->dhcp_pd_prefixes);
+}
+
+void dhcp4_pd_prefix_lost(Link *uplink) {
+ Link *tunnel;
+
+ dhcp_pd_prefix_lost(uplink);
+
+ if (uplink->dhcp4_6rd_tunnel_name &&
+ link_get_by_name(uplink->manager, uplink->dhcp4_6rd_tunnel_name, &tunnel) >= 0)
+ (void) link_remove(tunnel);
+}
+
+static int dhcp4_unreachable_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) {
+ int r;
+
+ assert(link);
+
+ r = route_configure_handler_internal(rtnl, m, link, "Failed to set unreachable route for DHCPv4 delegated prefix");
+ if (r <= 0)
+ return r;
+
+ r = dhcp4_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int dhcp6_unreachable_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) {
+ int r;
+
+ assert(link);
+
+ r = route_configure_handler_internal(rtnl, m, link, "Failed to set unreachable route for DHCPv6 delegated prefix");
+ if (r <= 0)
+ return r;
+
+ r = dhcp6_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int dhcp_request_unreachable_route(
+ Link *link,
+ const struct in6_addr *addr,
+ uint8_t prefixlen,
+ usec_t lifetime_usec,
+ NetworkConfigSource source,
+ const union in_addr_union *server_address,
+ unsigned *counter,
+ route_netlink_handler_t callback,
+ bool *configured) {
+
+ _cleanup_(route_freep) Route *route = NULL;
+ Route *existing;
+ int r;
+
+ assert(link);
+ assert(addr);
+ assert(IN_SET(source, NETWORK_CONFIG_SOURCE_DHCP4, NETWORK_CONFIG_SOURCE_DHCP6));
+ assert(server_address);
+ assert(counter);
+ assert(callback);
+ assert(configured);
+
+ if (prefixlen >= 64) {
+ log_link_debug(link, "Not adding a blocking route for DHCP delegated prefix %s since the prefix has length >= 64.",
+ IN6_ADDR_PREFIX_TO_STRING(addr, prefixlen));
+ return 0;
+ }
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->source = source;
+ route->provider = *server_address;
+ route->family = AF_INET6;
+ route->dst.in6 = *addr;
+ route->dst_prefixlen = prefixlen;
+ route->type = RTN_UNREACHABLE;
+ route->protocol = RTPROT_DHCP;
+ route->priority = IP6_RT_PRIO_USER;
+ route->lifetime_usec = lifetime_usec;
+
+ if (route_get(link->manager, NULL, route, &existing) < 0)
+ *configured = false;
+ else
+ route_unmark(existing);
+
+ r = link_request_route(link, TAKE_PTR(route), true, counter, callback, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request unreachable route for DHCP delegated prefix %s: %m",
+ IN6_ADDR_PREFIX_TO_STRING(addr, prefixlen));
+
+ return 0;
+}
+
+static int dhcp4_request_unreachable_route(
+ Link *link,
+ const struct in6_addr *addr,
+ uint8_t prefixlen,
+ usec_t lifetime_usec,
+ const union in_addr_union *server_address) {
+
+ return dhcp_request_unreachable_route(link, addr, prefixlen, lifetime_usec,
+ NETWORK_CONFIG_SOURCE_DHCP4, server_address,
+ &link->dhcp4_messages, dhcp4_unreachable_route_handler,
+ &link->dhcp4_configured);
+}
+
+static int dhcp6_request_unreachable_route(
+ Link *link,
+ const struct in6_addr *addr,
+ uint8_t prefixlen,
+ usec_t lifetime_usec,
+ const union in_addr_union *server_address) {
+
+ return dhcp_request_unreachable_route(link, addr, prefixlen, lifetime_usec,
+ NETWORK_CONFIG_SOURCE_DHCP6, server_address,
+ &link->dhcp6_messages, dhcp6_unreachable_route_handler,
+ &link->dhcp6_configured);
+}
+
+static int dhcp_pd_prefix_add(Link *link, const struct in6_addr *prefix, uint8_t prefixlen) {
+ struct in_addr_prefix *p;
+ int r;
+
+ assert(link);
+ assert(prefix);
+
+ p = new(struct in_addr_prefix, 1);
+ if (!p)
+ return log_oom();
+
+ *p = (struct in_addr_prefix) {
+ .family = AF_INET6,
+ .prefixlen = prefixlen,
+ .address.in6 = *prefix,
+ };
+
+ int log_level = set_contains(link->dhcp_pd_prefixes, p) ? LOG_DEBUG :
+ prefixlen > 64 || prefixlen < 48 ? LOG_WARNING : LOG_INFO;
+ log_link_full(link,
+ log_level,
+ "DHCP: received delegated prefix %s%s",
+ IN6_ADDR_PREFIX_TO_STRING(prefix, prefixlen),
+ prefixlen > 64 ? " with prefix length > 64, ignoring." :
+ prefixlen < 48 ? " with prefix length < 48, looks unusual.": "");
+
+ /* Store PD prefix even if prefixlen > 64, not to make logged at warning level so frequently. */
+ r = set_ensure_consume(&link->dhcp_pd_prefixes, &in_addr_prefix_hash_ops_free, p);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store DHCP delegated prefix %s: %m",
+ IN6_ADDR_PREFIX_TO_STRING(prefix, prefixlen));
+ return 0;
+}
+
+static int dhcp4_pd_request_default_gateway_on_6rd_tunnel(Link *link, const struct in_addr *br_address, usec_t lifetime_usec) {
+ _cleanup_(route_freep) Route *route = NULL;
+ Route *existing;
+ int r;
+
+ assert(link);
+ assert(br_address);
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to allocate default gateway for DHCP delegated prefix: %m");
+
+ route->source = NETWORK_CONFIG_SOURCE_DHCP_PD;
+ route->family = AF_INET6;
+ route->gw_family = AF_INET6;
+ route->gw.in6.s6_addr32[3] = br_address->s_addr;
+ route->scope = RT_SCOPE_UNIVERSE;
+ route->protocol = RTPROT_DHCP;
+ route->priority = IP6_RT_PRIO_USER;
+ route->lifetime_usec = lifetime_usec;
+
+ if (route_get(NULL, link, route, &existing) < 0) /* This is a new route. */
+ link->dhcp_pd_configured = false;
+ else
+ route_unmark(existing);
+
+ r = link_request_route(link, TAKE_PTR(route), true, &link->dhcp_pd_messages,
+ dhcp_pd_route_handler, NULL);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to request default gateway for DHCP delegated prefix: %m");
+
+ return 0;
+}
+
+static void dhcp4_calculate_pd_prefix(
+ const struct in_addr *ipv4address,
+ uint8_t ipv4masklen,
+ const struct in6_addr *sixrd_prefix,
+ uint8_t sixrd_prefixlen,
+ struct in6_addr *ret_pd_prefix,
+ uint8_t *ret_pd_prefixlen) {
+
+ struct in6_addr pd_prefix;
+
+ assert(ipv4address);
+ assert(ipv4masklen <= 32);
+ assert(sixrd_prefix);
+ assert(32 - ipv4masklen + sixrd_prefixlen <= 128);
+ assert(ret_pd_prefix);
+
+ pd_prefix = *sixrd_prefix;
+ for (unsigned i = 0; i < (unsigned) (32 - ipv4masklen); i++)
+ if (ipv4address->s_addr & htobe32(UINT32_C(1) << (32 - ipv4masklen - i - 1)))
+ pd_prefix.s6_addr[(i + sixrd_prefixlen) / 8] |= 1 << (7 - (i + sixrd_prefixlen) % 8);
+
+ *ret_pd_prefix = pd_prefix;
+ if (ret_pd_prefixlen)
+ *ret_pd_prefixlen = 32 - ipv4masklen + sixrd_prefixlen;
+}
+
+static int dhcp4_pd_assign_subnet_prefix(Link *link, Link *uplink) {
+ uint8_t ipv4masklen, sixrd_prefixlen, pd_prefixlen;
+ struct in6_addr sixrd_prefix, pd_prefix;
+ const struct in_addr *br_addresses;
+ struct in_addr ipv4address;
+ uint32_t lifetime_sec;
+ usec_t lifetime_usec, now_usec;
+ int r;
+
+ assert(link);
+ assert(uplink);
+ assert(uplink->manager);
+ assert(uplink->dhcp_lease);
+
+ r = sd_dhcp_lease_get_address(uplink->dhcp_lease, &ipv4address);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get DHCPv4 address: %m");
+
+ r = sd_dhcp_lease_get_lifetime(uplink->dhcp_lease, &lifetime_sec);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get lifetime of DHCPv4 lease: %m");
+
+ assert_se(sd_event_now(uplink->manager->event, CLOCK_BOOTTIME, &now_usec) >= 0);
+ lifetime_usec = sec_to_usec(lifetime_sec, now_usec);
+
+ r = sd_dhcp_lease_get_6rd(uplink->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, &br_addresses, NULL);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get DHCPv4 6rd option: %m");
+
+ dhcp4_calculate_pd_prefix(&ipv4address, ipv4masklen, &sixrd_prefix, sixrd_prefixlen, &pd_prefix, &pd_prefixlen);
+
+ if (pd_prefixlen > 64)
+ return 0;
+
+ r = dhcp_pd_prepare(link);
+ if (r <= 0)
+ return r;
+
+ if (streq_ptr(uplink->dhcp4_6rd_tunnel_name, link->ifname)) {
+ r = dhcp4_pd_request_default_gateway_on_6rd_tunnel(link, &br_addresses[0], lifetime_usec);
+ if (r < 0)
+ return r;
+ }
+
+ r = dhcp_pd_assign_subnet_prefix(link, &pd_prefix, pd_prefixlen, lifetime_usec, lifetime_usec, /* is_uplink = */ false);
+ if (r < 0)
+ return r;
+
+ return dhcp_pd_finalize(link);
+}
+
+static int dhcp4_pd_6rd_tunnel_create_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->manager);
+ assert(link->dhcp4_6rd_tunnel_name);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_link_message_warning_errno(link, m, r, "Failed to create tunnel device for DHCPv4 6rd");
+ link_enter_failed(link);
+ return 0;
+ }
+
+ return 0;
+}
+
+int dhcp4_pd_prefix_acquired(Link *uplink) {
+ _cleanup_free_ char *tunnel_name = NULL;
+ uint8_t ipv4masklen, sixrd_prefixlen, pd_prefixlen;
+ struct in6_addr sixrd_prefix, pd_prefix;
+ struct in_addr ipv4address;
+ union in_addr_union server_address;
+ const struct in_addr *br_addresses;
+ uint32_t lifetime_sec;
+ usec_t lifetime_usec, now_usec;
+ Link *link;
+ int r;
+
+ assert(uplink);
+ assert(uplink->manager);
+ assert(uplink->dhcp_lease);
+
+ r = sd_dhcp_lease_get_address(uplink->dhcp_lease, &ipv4address);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get DHCPv4 address: %m");
+
+ r = sd_dhcp_lease_get_lifetime(uplink->dhcp_lease, &lifetime_sec);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get lifetime of DHCPv4 lease: %m");
+
+ assert_se(sd_event_now(uplink->manager->event, CLOCK_BOOTTIME, &now_usec) >= 0);
+ lifetime_usec = sec_to_usec(lifetime_sec, now_usec);
+
+ r = sd_dhcp_lease_get_server_identifier(uplink->dhcp_lease, &server_address.in);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get server address of DHCPv4 lease: %m");
+
+ r = sd_dhcp_lease_get_6rd(uplink->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, &br_addresses, NULL);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get DHCPv4 6rd option: %m");
+
+ if (DEBUG_LOGGING)
+ log_link_debug(uplink, "DHCPv4: 6rd option is acquired: IPv4_masklen=%u, 6rd_prefix=%s, br_address="IPV4_ADDRESS_FMT_STR,
+ ipv4masklen,
+ IN6_ADDR_PREFIX_TO_STRING(&sixrd_prefix, sixrd_prefixlen),
+ IPV4_ADDRESS_FMT_VAL(*br_addresses));
+
+ /* Calculate PD prefix */
+ dhcp4_calculate_pd_prefix(&ipv4address, ipv4masklen, &sixrd_prefix, sixrd_prefixlen, &pd_prefix, &pd_prefixlen);
+
+ /* Register and log PD prefix */
+ r = dhcp_pd_prefix_add(uplink, &pd_prefix, pd_prefixlen);
+ if (r < 0)
+ return r;
+
+ /* Request unreachable route */
+ r = dhcp4_request_unreachable_route(uplink, &pd_prefix, pd_prefixlen, lifetime_usec, &server_address);
+ if (r < 0)
+ return r;
+
+ /* Generate 6rd SIT tunnel device name. */
+ r = dhcp4_pd_create_6rd_tunnel_name(uplink, &tunnel_name);
+ if (r < 0)
+ return r;
+
+ /* Remove old tunnel device if exists. */
+ if (!streq_ptr(uplink->dhcp4_6rd_tunnel_name, tunnel_name)) {
+ Link *old_tunnel;
+
+ if (uplink->dhcp4_6rd_tunnel_name &&
+ link_get_by_name(uplink->manager, uplink->dhcp4_6rd_tunnel_name, &old_tunnel) >= 0)
+ (void) link_remove(old_tunnel);
+
+ free_and_replace(uplink->dhcp4_6rd_tunnel_name, tunnel_name);
+ }
+
+ /* Create 6rd SIT tunnel device if it does not exist yet. */
+ if (link_get_by_name(uplink->manager, uplink->dhcp4_6rd_tunnel_name, NULL) < 0) {
+ r = dhcp4_pd_create_6rd_tunnel(uplink, dhcp4_pd_6rd_tunnel_create_handler);
+ if (r < 0)
+ return r;
+ }
+
+ /* Then, assign subnet prefixes to downstream interfaces. */
+ HASHMAP_FOREACH(link, uplink->manager->links_by_index) {
+ if (!dhcp_pd_is_uplink(link, uplink, /* accept_auto = */ true))
+ continue;
+
+ r = dhcp4_pd_assign_subnet_prefix(link, uplink);
+ if (r < 0) {
+ /* When failed on the upstream interface (i.e., the case link == uplink),
+ * immediately abort the assignment of the prefixes. As, the all assigned
+ * prefixes will be dropped soon in link_enter_failed(), and it is meaningless
+ * to continue the assignment. */
+ if (link == uplink)
+ return r;
+
+ link_enter_failed(link);
+ }
+ }
+
+ return 0;
+}
+
+static int dhcp6_pd_assign_subnet_prefixes(Link *link, Link *uplink) {
+ usec_t timestamp_usec;
+ int r;
+
+ assert(link);
+ assert(uplink);
+ assert(uplink->dhcp6_lease);
+
+ r = dhcp_pd_prepare(link);
+ if (r <= 0)
+ return r;
+
+ r = sd_dhcp6_lease_get_timestamp(uplink->dhcp6_lease, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return r;
+
+ for (sd_dhcp6_lease_reset_pd_prefix_iter(uplink->dhcp6_lease);;) {
+ uint32_t lifetime_preferred_sec, lifetime_valid_sec;
+ struct in6_addr pd_prefix;
+ uint8_t pd_prefix_len;
+
+ r = sd_dhcp6_lease_get_pd(uplink->dhcp6_lease, &pd_prefix, &pd_prefix_len,
+ &lifetime_preferred_sec, &lifetime_valid_sec);
+ if (r < 0)
+ break;
+
+ if (pd_prefix_len > 64)
+ continue;
+
+ /* Mask prefix for safety. */
+ r = in6_addr_mask(&pd_prefix, pd_prefix_len);
+ if (r < 0)
+ return r;
+
+ r = dhcp_pd_assign_subnet_prefix(link, &pd_prefix, pd_prefix_len,
+ sec_to_usec(lifetime_preferred_sec, timestamp_usec),
+ sec_to_usec(lifetime_valid_sec, timestamp_usec),
+ /* is_uplink = */ link == uplink);
+ if (r < 0)
+ return r;
+ }
+
+ return dhcp_pd_finalize(link);
+}
+
+int dhcp6_pd_prefix_acquired(Link *uplink) {
+ union in_addr_union server_address;
+ usec_t timestamp_usec;
+ Link *link;
+ int r;
+
+ assert(uplink);
+ assert(uplink->dhcp6_lease);
+
+ r = sd_dhcp6_lease_get_server_address(uplink->dhcp6_lease, &server_address.in6);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get server address of DHCPv6 lease: %m");
+
+ r = sd_dhcp6_lease_get_timestamp(uplink->dhcp6_lease, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return log_link_warning_errno(uplink, r, "Failed to get timestamp of DHCPv6 lease: %m");
+
+ /* First, logs acquired prefixes and request unreachable routes. */
+ for (sd_dhcp6_lease_reset_pd_prefix_iter(uplink->dhcp6_lease);;) {
+ uint32_t lifetime_preferred_sec, lifetime_valid_sec;
+ struct in6_addr pd_prefix;
+ uint8_t pd_prefix_len;
+
+ r = sd_dhcp6_lease_get_pd(uplink->dhcp6_lease, &pd_prefix, &pd_prefix_len,
+ &lifetime_preferred_sec, &lifetime_valid_sec);
+ if (r < 0)
+ break;
+
+ /* Mask prefix for safety. */
+ r = in6_addr_mask(&pd_prefix, pd_prefix_len);
+ if (r < 0)
+ return log_link_error_errno(uplink, r, "Failed to mask DHCPv6 delegated prefix: %m");
+
+ r = dhcp_pd_prefix_add(uplink, &pd_prefix, pd_prefix_len);
+ if (r < 0)
+ return r;
+
+ r = dhcp6_request_unreachable_route(uplink, &pd_prefix, pd_prefix_len,
+ sec_to_usec(lifetime_valid_sec, timestamp_usec),
+ &server_address);
+ if (r < 0)
+ return r;
+ }
+
+ /* Then, assign subnet prefixes. */
+ HASHMAP_FOREACH(link, uplink->manager->links_by_index) {
+ if (!dhcp_pd_is_uplink(link, uplink, /* accept_auto = */ true))
+ continue;
+
+ r = dhcp6_pd_assign_subnet_prefixes(link, uplink);
+ if (r < 0) {
+ /* When failed on the upstream interface (i.e., the case link == uplink),
+ * immediately abort the assignment of the prefixes. As, the all assigned
+ * prefixes will be dropped soon in link_enter_failed(), and it is meaningless
+ * to continue the assignment. */
+ if (link == uplink)
+ return r;
+
+ link_enter_failed(link);
+ }
+ }
+
+ return 0;
+}
+
+static bool dhcp4_pd_uplink_is_ready(Link *link) {
+ assert(link);
+
+ if (!link->network)
+ return false;
+
+ if (!link->network->dhcp_use_6rd)
+ return false;
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return false;
+
+ if (!link->dhcp_client)
+ return false;
+
+ if (sd_dhcp_client_is_running(link->dhcp_client) <= 0)
+ return false;
+
+ if (!link->dhcp_lease)
+ return false;
+
+ return dhcp4_lease_has_pd_prefix(link->dhcp_lease);
+}
+
+static bool dhcp6_pd_uplink_is_ready(Link *link) {
+ assert(link);
+
+ if (!link->network)
+ return false;
+
+ if (!link->network->dhcp6_use_pd_prefix)
+ return false;
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return false;
+
+ if (!link->dhcp6_client)
+ return false;
+
+ if (sd_dhcp6_client_is_running(link->dhcp6_client) <= 0)
+ return false;
+
+ if (!link->dhcp6_lease)
+ return false;
+
+ return dhcp6_lease_has_pd_prefix(link->dhcp6_lease);
+}
+
+int dhcp_pd_find_uplink(Link *link, Link **ret) {
+ Link *uplink = NULL;
+ int r = 0;
+
+ assert(link);
+ assert(link->manager);
+ assert(link_dhcp_pd_is_enabled(link));
+ assert(ret);
+
+ if (link->network->dhcp_pd_uplink_name)
+ r = link_get_by_name(link->manager, link->network->dhcp_pd_uplink_name, &uplink);
+ else if (link->network->dhcp_pd_uplink_index > 0)
+ r = link_get_by_index(link->manager, link->network->dhcp_pd_uplink_index, &uplink);
+ else if (link->network->dhcp_pd_uplink_index == UPLINK_INDEX_SELF)
+ uplink = link;
+ if (r < 0)
+ return r;
+
+ if (uplink) {
+ if (dhcp4_pd_uplink_is_ready(uplink)) {
+ *ret = uplink;
+ return AF_INET;
+ }
+
+ if (dhcp6_pd_uplink_is_ready(uplink)) {
+ *ret = uplink;
+ return AF_INET6;
+ }
+
+ return -EBUSY;
+ }
+
+ HASHMAP_FOREACH(uplink, link->manager->links_by_index) {
+ /* Assume that there exists at most one link which acquired delegated prefixes. */
+ if (dhcp4_pd_uplink_is_ready(uplink)) {
+ *ret = uplink;
+ return AF_INET;
+ }
+
+ if (dhcp6_pd_uplink_is_ready(uplink)) {
+ *ret = uplink;
+ return AF_INET6;
+ }
+ }
+
+ return -ENODEV;
+}
+
+int dhcp_request_prefix_delegation(Link *link) {
+ Link *uplink;
+ int r;
+
+ assert(link);
+
+ if (!link_dhcp_pd_is_enabled(link))
+ return 0;
+
+ r = dhcp_pd_find_uplink(link, &uplink);
+ if (r < 0)
+ return 0;
+
+ log_link_debug(link, "Requesting subnets of delegated prefixes acquired by DHCPv%c client on %s",
+ r == AF_INET ? '4' : '6', uplink->ifname);
+
+ return r == AF_INET ?
+ dhcp4_pd_assign_subnet_prefix(link, uplink) :
+ dhcp6_pd_assign_subnet_prefixes(link, uplink);
+}
+
+int config_parse_dhcp_pd_subnet_id(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ int64_t *p = ASSERT_PTR(data);
+ uint64_t t;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue) || streq(rvalue, "auto")) {
+ *p = -1;
+ return 0;
+ }
+
+ r = safe_atoux64(rvalue, &t);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (t > INT64_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid subnet id '%s', ignoring assignment.",
+ rvalue);
+ return 0;
+ }
+
+ *p = (int64_t) t;
+
+ return 0;
+}
diff --git a/src/network/networkd-dhcp-prefix-delegation.h b/src/network/networkd-dhcp-prefix-delegation.h
new file mode 100644
index 0000000..d67f919
--- /dev/null
+++ b/src/network/networkd-dhcp-prefix-delegation.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sd-dhcp-lease.h"
+#include "sd-dhcp6-lease.h"
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+
+bool link_dhcp_pd_is_enabled(Link *link);
+bool dhcp_pd_is_uplink(Link *link, Link *target, bool accept_auto);
+int dhcp_pd_find_uplink(Link *link, Link **ret);
+bool dhcp4_lease_has_pd_prefix(sd_dhcp_lease *lease);
+bool dhcp6_lease_has_pd_prefix(sd_dhcp6_lease *lease);
+int dhcp_pd_remove(Link *link, bool only_marked);
+int dhcp_request_prefix_delegation(Link *link);
+int dhcp4_pd_prefix_acquired(Link *uplink);
+int dhcp6_pd_prefix_acquired(Link *uplink);
+void dhcp_pd_prefix_lost(Link *uplink);
+void dhcp4_pd_prefix_lost(Link *uplink);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_pd_subnet_id);
diff --git a/src/network/networkd-dhcp-server-bus.c b/src/network/networkd-dhcp-server-bus.c
new file mode 100644
index 0000000..e3397c3
--- /dev/null
+++ b/src/network/networkd-dhcp-server-bus.c
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-util.h"
+#include "dhcp-server-internal.h"
+#include "networkd-dhcp-server-bus.h"
+#include "networkd-link-bus.h"
+#include "networkd-manager.h"
+#include "strv.h"
+
+static int property_get_leases(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ sd_dhcp_server *s;
+ DHCPLease *lease;
+ int r;
+
+ assert(reply);
+
+ s = l->dhcp_server;
+ if (!s)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Link %s has no DHCP server.", l->ifname);
+
+ if (sd_dhcp_server_is_in_relay_mode(s))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Link %s has DHCP relay agent active.", l->ifname);
+
+ r = sd_bus_message_open_container(reply, 'a', "(uayayayayt)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(lease, s->bound_leases_by_client_id) {
+ r = sd_bus_message_open_container(reply, 'r', "uayayayayt");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(reply, "u", (uint32_t)AF_INET);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_array(reply, 'y', lease->client_id.data, lease->client_id.length);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_array(reply, 'y', &lease->address, sizeof(lease->address));
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_array(reply, 'y', &lease->gateway, sizeof(lease->gateway));
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_array(reply, 'y', &lease->chaddr, sizeof(lease->chaddr));
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_basic(reply, 't', &lease->expiration);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_message_close_container(reply);
+}
+
+static int dhcp_server_emit_changed(Link *link, const char *property, ...) {
+ _cleanup_free_ char *path = NULL;
+ char **l;
+
+ assert(link);
+
+ if (sd_bus_is_ready(link->manager->bus) <= 0)
+ return 0;
+
+ path = link_bus_path(link);
+ if (!path)
+ return log_oom();
+
+ l = strv_from_stdarg_alloca(property);
+
+ return sd_bus_emit_properties_changed_strv(
+ link->manager->bus,
+ path,
+ "org.freedesktop.network1.DHCPServer",
+ l);
+}
+
+void dhcp_server_callback(sd_dhcp_server *s, uint64_t event, void *data) {
+ Link *l = ASSERT_PTR(data);
+
+ if (event & SD_DHCP_SERVER_EVENT_LEASE_CHANGED)
+ (void) dhcp_server_emit_changed(l, "Leases", NULL);
+}
+
+static const sd_bus_vtable dhcp_server_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+
+ SD_BUS_PROPERTY("Leases", "a(uayayayayt)", property_get_leases, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+
+ SD_BUS_VTABLE_END
+};
+
+const BusObjectImplementation dhcp_server_object = {
+ "/org/freedesktop/network1/link",
+ "org.freedesktop.network1.DHCPServer",
+ .fallback_vtables = BUS_FALLBACK_VTABLES({dhcp_server_vtable, link_object_find}),
+ .node_enumerator = link_node_enumerator,
+};
diff --git a/src/network/networkd-dhcp-server-bus.h b/src/network/networkd-dhcp-server-bus.h
new file mode 100644
index 0000000..f52be82
--- /dev/null
+++ b/src/network/networkd-dhcp-server-bus.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+#include "sd-dhcp-server.h"
+
+#include "bus-object.h"
+
+extern const BusObjectImplementation dhcp_server_object;
+
+void dhcp_server_callback(sd_dhcp_server *server, uint64_t event, void *data);
diff --git a/src/network/networkd-dhcp-server-static-lease.c b/src/network/networkd-dhcp-server-static-lease.c
new file mode 100644
index 0000000..8e7eec6
--- /dev/null
+++ b/src/network/networkd-dhcp-server-static-lease.c
@@ -0,0 +1,210 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "ether-addr-util.h"
+#include "hashmap.h"
+#include "networkd-dhcp-server-static-lease.h"
+#include "networkd-network.h"
+#include "networkd-util.h"
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(DHCPStaticLease, dhcp_static_lease_free);
+
+DHCPStaticLease *dhcp_static_lease_free(DHCPStaticLease *static_lease) {
+ if (!static_lease)
+ return NULL;
+
+ if (static_lease->network && static_lease->section)
+ hashmap_remove(static_lease->network->dhcp_static_leases_by_section, static_lease->section);
+
+ config_section_free(static_lease->section);
+ free(static_lease->client_id);
+ return mfree(static_lease);
+}
+
+static int dhcp_static_lease_new(DHCPStaticLease **ret) {
+ DHCPStaticLease *p;
+
+ assert(ret);
+
+ p = new0(DHCPStaticLease, 1);
+ if (!p)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(p);
+ return 0;
+}
+
+static int lease_new_static(Network *network, const char *filename, unsigned section_line, DHCPStaticLease **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(dhcp_static_lease_freep) DHCPStaticLease *static_lease = NULL;
+ int r;
+
+ assert(network);
+ assert(filename);
+ assert(section_line > 0);
+ assert(ret);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ static_lease = hashmap_get(network->dhcp_static_leases_by_section, n);
+ if (static_lease) {
+ *ret = TAKE_PTR(static_lease);
+ return 0;
+ }
+
+ r = dhcp_static_lease_new(&static_lease);
+ if (r < 0)
+ return r;
+
+ static_lease->network = network;
+ static_lease->section = TAKE_PTR(n);
+ r = hashmap_ensure_put(&network->dhcp_static_leases_by_section, &config_section_hash_ops, static_lease->section, static_lease);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(static_lease);
+ return 0;
+}
+
+static int static_lease_verify(DHCPStaticLease *static_lease) {
+ if (section_is_invalid(static_lease->section))
+ return -EINVAL;
+
+ if (in4_addr_is_null(&static_lease->address))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: DHCP static lease without Address= field configured. "
+ "Ignoring [DHCPServerStaticLease] section from line %u.",
+ static_lease->section->filename, static_lease->section->line);
+
+ /* TODO: check that the address is in the pool. */
+
+ if (static_lease->client_id_size == 0 || !static_lease->client_id)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: DHCP static lease without MACAddress= field configured. "
+ "Ignoring [DHCPServerStaticLease] section from line %u.",
+ static_lease->section->filename, static_lease->section->line);
+
+ assert(static_lease->client_id_size == ETH_ALEN + 1);
+
+ return 0;
+}
+
+void network_drop_invalid_static_leases(Network *network) {
+ DHCPStaticLease *static_lease;
+
+ assert(network);
+
+ HASHMAP_FOREACH(static_lease, network->dhcp_static_leases_by_section)
+ if (static_lease_verify(static_lease) < 0)
+ dhcp_static_lease_free(static_lease);
+}
+
+int config_parse_dhcp_static_lease_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(dhcp_static_lease_free_or_set_invalidp) DHCPStaticLease *lease = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ union in_addr_union addr;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = lease_new_static(network, filename, section_line, &lease);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ lease->address.s_addr = 0;
+ TAKE_PTR(lease);
+ return 0;
+ }
+
+ r = in_addr_from_string(AF_INET, rvalue, &addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse IPv4 address for DHCPv4 static lease, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (in4_addr_is_null(&addr.in)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "IPv4 address for DHCPv4 static lease cannot be the ANY address, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ lease->address = addr.in;
+
+ TAKE_PTR(lease);
+ return 0;
+}
+
+int config_parse_dhcp_static_lease_hwaddr(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(dhcp_static_lease_free_or_set_invalidp) DHCPStaticLease *lease = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ struct ether_addr hwaddr;
+ uint8_t *c;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = lease_new_static(network, filename, section_line, &lease);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ lease->client_id = mfree(lease->client_id);
+ lease->client_id_size = 0;
+ return 0;
+ }
+
+ r = parse_ether_addr(rvalue, &hwaddr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse MAC address for DHCPv4 static lease, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (ether_addr_is_null(&hwaddr) || (hwaddr.ether_addr_octet[0] & 0x01)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "MAC address for DHCPv4 static lease cannot be null or multicast, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ c = new(uint8_t, ETH_ALEN + 1);
+ if (!c)
+ return log_oom();
+
+ /* set client id type to 1: Ethernet Link-Layer (RFC 2132) */
+ c[0] = 0x01;
+ memcpy(c + 1, &hwaddr, ETH_ALEN);
+
+ free_and_replace(lease->client_id, c);
+ lease->client_id_size = ETH_ALEN + 1;
+
+ TAKE_PTR(lease);
+ return 0;
+}
diff --git a/src/network/networkd-dhcp-server-static-lease.h b/src/network/networkd-dhcp-server-static-lease.h
new file mode 100644
index 0000000..9b8e78b
--- /dev/null
+++ b/src/network/networkd-dhcp-server-static-lease.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#pragma once
+
+#include <inttypes.h>
+
+#include "conf-parser.h"
+#include "in-addr-util.h"
+
+typedef struct Network Network;
+typedef struct ConfigSection ConfigSection;
+
+typedef struct DHCPStaticLease {
+ Network *network;
+ ConfigSection *section;
+
+ struct in_addr address;
+ uint8_t *client_id;
+ size_t client_id_size;
+} DHCPStaticLease;
+
+DHCPStaticLease *dhcp_static_lease_free(DHCPStaticLease *lease);
+void network_drop_invalid_static_leases(Network *network);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_hwaddr);
diff --git a/src/network/networkd-dhcp-server.c b/src/network/networkd-dhcp-server.c
new file mode 100644
index 0000000..620fbbd
--- /dev/null
+++ b/src/network/networkd-dhcp-server.c
@@ -0,0 +1,736 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+#include <linux/if.h>
+
+#include "sd-dhcp-server.h"
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-server-bus.h"
+#include "networkd-dhcp-server-static-lease.h"
+#include "networkd-dhcp-server.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "networkd-route-util.h"
+#include "parse-util.h"
+#include "socket-netlink.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+static bool link_dhcp4_server_enabled(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ if (link->iftype == ARPHRD_CAN)
+ return false;
+
+ return link->network->dhcp_server;
+}
+
+void network_adjust_dhcp_server(Network *network) {
+ assert(network);
+
+ if (!network->dhcp_server)
+ return;
+
+ if (network->bond) {
+ log_warning("%s: DHCPServer= is enabled for bond slave. Disabling DHCP server.",
+ network->filename);
+ network->dhcp_server = false;
+ return;
+ }
+
+ if (!in4_addr_is_set(&network->dhcp_server_address)) {
+ Address *address;
+ bool have = false;
+
+ ORDERED_HASHMAP_FOREACH(address, network->addresses_by_section) {
+ if (section_is_invalid(address->section))
+ continue;
+
+ if (address->family != AF_INET)
+ continue;
+
+ if (in4_addr_is_localhost(&address->in_addr.in))
+ continue;
+
+ if (in4_addr_is_link_local(&address->in_addr.in))
+ continue;
+
+ if (in4_addr_is_set(&address->in_addr_peer.in))
+ continue;
+
+ have = true;
+ break;
+ }
+ if (!have) {
+ log_warning("%s: DHCPServer= is enabled, but no static address configured. "
+ "Disabling DHCP server.",
+ network->filename);
+ network->dhcp_server = false;
+ return;
+ }
+ }
+}
+
+int link_request_dhcp_server_address(Link *link) {
+ _cleanup_(address_freep) Address *address = NULL;
+ Address *existing;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!link_dhcp4_server_enabled(link))
+ return 0;
+
+ if (!in4_addr_is_set(&link->network->dhcp_server_address))
+ return 0;
+
+ r = address_new(&address);
+ if (r < 0)
+ return r;
+
+ address->source = NETWORK_CONFIG_SOURCE_STATIC;
+ address->family = AF_INET;
+ address->in_addr.in = link->network->dhcp_server_address;
+ address->prefixlen = link->network->dhcp_server_address_prefixlen;
+ address_set_broadcast(address, link);
+
+ if (address_get(link, address, &existing) >= 0 &&
+ address_exists(existing) &&
+ existing->source == NETWORK_CONFIG_SOURCE_STATIC)
+ /* The same address seems explicitly configured in [Address] or [Network] section.
+ * Configure the DHCP server address only when it is not. */
+ return 0;
+
+ return link_request_static_address(link, TAKE_PTR(address), true);
+}
+
+static int link_find_dhcp_server_address(Link *link, Address **ret) {
+ Address *address;
+
+ assert(link);
+ assert(link->network);
+
+ /* If ServerAddress= is specified, then use the address. */
+ if (in4_addr_is_set(&link->network->dhcp_server_address))
+ return link_get_ipv4_address(link, &link->network->dhcp_server_address,
+ link->network->dhcp_server_address_prefixlen, ret);
+
+ /* If not, then select one from static addresses. */
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_STATIC)
+ continue;
+ if (!address_exists(address))
+ continue;
+ if (address->family != AF_INET)
+ continue;
+ if (in4_addr_is_localhost(&address->in_addr.in))
+ continue;
+ if (in4_addr_is_link_local(&address->in_addr.in))
+ continue;
+ if (in4_addr_is_set(&address->in_addr_peer.in))
+ continue;
+
+ *ret = address;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int dhcp_server_find_uplink(Link *link, Link **ret) {
+ assert(link);
+
+ if (link->network->dhcp_server_uplink_name)
+ return link_get_by_name(link->manager, link->network->dhcp_server_uplink_name, ret);
+
+ if (link->network->dhcp_server_uplink_index > 0)
+ return link_get_by_index(link->manager, link->network->dhcp_server_uplink_index, ret);
+
+ if (link->network->dhcp_server_uplink_index == UPLINK_INDEX_AUTO) {
+ /* It is not necessary to propagate error in automatic selection. */
+ if (manager_find_uplink(link->manager, AF_INET, link, ret) < 0)
+ *ret = NULL;
+ return 0;
+ }
+
+ *ret = NULL;
+ return 0;
+}
+
+static int link_push_uplink_to_dhcp_server(
+ Link *link,
+ sd_dhcp_lease_server_type_t what,
+ sd_dhcp_server *s) {
+
+ _cleanup_free_ struct in_addr *addresses = NULL;
+ bool use_dhcp_lease_data = true;
+ size_t n_addresses = 0;
+
+ assert(link);
+
+ if (!link->network)
+ return 0;
+ assert(link->network);
+
+ log_link_debug(link, "Copying %s from link", dhcp_lease_server_type_to_string(what));
+
+ switch (what) {
+
+ case SD_DHCP_LEASE_DNS:
+ /* For DNS we have a special case. We the data configured explicitly locally along with the
+ * data from the DHCP lease. */
+
+ for (unsigned i = 0; i < link->network->n_dns; i++) {
+ struct in_addr ia;
+
+ /* Only look for IPv4 addresses */
+ if (link->network->dns[i]->family != AF_INET)
+ continue;
+
+ ia = link->network->dns[i]->address.in;
+
+ /* Never propagate obviously borked data */
+ if (in4_addr_is_null(&ia) || in4_addr_is_localhost(&ia))
+ continue;
+
+ if (!GREEDY_REALLOC(addresses, n_addresses + 1))
+ return log_oom();
+
+ addresses[n_addresses++] = ia;
+ }
+
+ use_dhcp_lease_data = link->network->dhcp_use_dns;
+ break;
+
+ case SD_DHCP_LEASE_NTP: {
+ /* For NTP things are similar, but for NTP hostnames can be configured too, which we cannot
+ * propagate via DHCP. Hence let's only propagate those which are IP addresses. */
+
+ STRV_FOREACH(i, link->network->ntp) {
+ union in_addr_union ia;
+
+ if (in_addr_from_string(AF_INET, *i, &ia) < 0)
+ continue;
+
+ /* Never propagate obviously borked data */
+ if (in4_addr_is_null(&ia.in) || in4_addr_is_localhost(&ia.in))
+ continue;
+
+ if (!GREEDY_REALLOC(addresses, n_addresses + 1))
+ return log_oom();
+
+ addresses[n_addresses++] = ia.in;
+ }
+
+ use_dhcp_lease_data = link->network->dhcp_use_ntp;
+ break;
+ }
+
+ case SD_DHCP_LEASE_SIP:
+
+ /* For SIP we don't allow explicit, local configuration, but there's control whether to use the data */
+ use_dhcp_lease_data = link->network->dhcp_use_sip;
+ break;
+
+ case SD_DHCP_LEASE_POP3:
+ case SD_DHCP_LEASE_SMTP:
+ case SD_DHCP_LEASE_LPR:
+ /* For the other server types we currently do not allow local configuration of server data,
+ * since there are typically no local consumers of the data. */
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (use_dhcp_lease_data && link->dhcp_lease) {
+ const struct in_addr *da;
+
+ int n = sd_dhcp_lease_get_servers(link->dhcp_lease, what, &da);
+ if (n > 0) {
+ if (!GREEDY_REALLOC(addresses, n_addresses + n))
+ return log_oom();
+
+ for (int j = 0; j < n; j++)
+ if (in4_addr_is_non_local(&da[j]))
+ addresses[n_addresses++] = da[j];
+ }
+ }
+
+ if (n_addresses <= 0)
+ return 0;
+
+ return sd_dhcp_server_set_servers(s, what, addresses, n_addresses);
+}
+
+static int dhcp4_server_parse_dns_server_string_and_warn(
+ const char *string,
+ struct in_addr **addresses,
+ size_t *n_addresses) {
+
+ for (;;) {
+ _cleanup_free_ char *word = NULL, *server_name = NULL;
+ union in_addr_union address;
+ int family, r, ifindex = 0;
+
+ r = extract_first_word(&string, &word, NULL, 0);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ r = in_addr_ifindex_name_from_string_auto(word, &family, &address, &ifindex, &server_name);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse DNS server address '%s', ignoring: %m", word);
+ continue;
+ }
+
+ /* Only look for IPv4 addresses */
+ if (family != AF_INET)
+ continue;
+
+ /* Never propagate obviously borked data */
+ if (in4_addr_is_null(&address.in) || in4_addr_is_localhost(&address.in))
+ continue;
+
+ if (!GREEDY_REALLOC(*addresses, *n_addresses + 1))
+ return log_oom();
+
+ (*addresses)[(*n_addresses)++] = address.in;
+ }
+
+ return 0;
+}
+
+static int dhcp4_server_set_dns_from_resolve_conf(Link *link) {
+ _cleanup_free_ struct in_addr *addresses = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ size_t n_addresses = 0;
+ int r;
+
+ f = fopen(PRIVATE_UPLINK_RESOLV_CONF, "re");
+ if (!f) {
+ if (errno == ENOENT)
+ return 0;
+
+ return log_warning_errno(errno, "Failed to open " PRIVATE_UPLINK_RESOLV_CONF ": %m");
+ }
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ const char *a;
+ char *l;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read " PRIVATE_UPLINK_RESOLV_CONF ": %m");
+ if (r == 0)
+ break;
+
+ l = strstrip(line);
+ if (IN_SET(*l, '#', ';', 0))
+ continue;
+
+ a = first_word(l, "nameserver");
+ if (!a)
+ continue;
+
+ r = dhcp4_server_parse_dns_server_string_and_warn(a, &addresses, &n_addresses);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse DNS server address '%s', ignoring.", a);
+ }
+
+ if (n_addresses <= 0)
+ return 0;
+
+ return sd_dhcp_server_set_dns(link->dhcp_server, addresses, n_addresses);
+}
+
+static int dhcp4_server_configure(Link *link) {
+ bool acquired_uplink = false;
+ sd_dhcp_option *p;
+ DHCPStaticLease *static_lease;
+ Link *uplink = NULL;
+ Address *address;
+ bool bind_to_interface;
+ int r;
+
+ assert(link);
+
+ log_link_debug(link, "Configuring DHCP Server.");
+
+ if (link->dhcp_server)
+ return -EBUSY;
+
+ r = sd_dhcp_server_new(&link->dhcp_server, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_server_attach_event(link->dhcp_server, link->manager->event, 0);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_server_set_callback(link->dhcp_server, dhcp_server_callback, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to set callback for DHCPv4 server instance: %m");
+
+ r = link_find_dhcp_server_address(link, &address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to find suitable address for DHCPv4 server instance: %m");
+
+ /* use the server address' subnet as the pool */
+ r = sd_dhcp_server_configure_pool(link->dhcp_server, &address->in_addr.in, address->prefixlen,
+ link->network->dhcp_server_pool_offset, link->network->dhcp_server_pool_size);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to configure address pool for DHCPv4 server instance: %m");
+
+ if (link->network->dhcp_server_max_lease_time_usec > 0) {
+ r = sd_dhcp_server_set_max_lease_time(link->dhcp_server,
+ DIV_ROUND_UP(link->network->dhcp_server_max_lease_time_usec, USEC_PER_SEC));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set maximum lease time for DHCPv4 server instance: %m");
+ }
+
+ if (link->network->dhcp_server_default_lease_time_usec > 0) {
+ r = sd_dhcp_server_set_default_lease_time(link->dhcp_server,
+ DIV_ROUND_UP(link->network->dhcp_server_default_lease_time_usec, USEC_PER_SEC));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set default lease time for DHCPv4 server instance: %m");
+ }
+
+ r = sd_dhcp_server_set_boot_server_address(link->dhcp_server, &link->network->dhcp_server_boot_server_address);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to set boot server address for DHCPv4 server instance: %m");
+
+ r = sd_dhcp_server_set_boot_server_name(link->dhcp_server, link->network->dhcp_server_boot_server_name);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to set boot server name for DHCPv4 server instance: %m");
+
+ r = sd_dhcp_server_set_boot_filename(link->dhcp_server, link->network->dhcp_server_boot_filename);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to set boot filename for DHCPv4 server instance: %m");
+
+ for (sd_dhcp_lease_server_type_t type = 0; type < _SD_DHCP_LEASE_SERVER_TYPE_MAX; type ++) {
+
+ if (!link->network->dhcp_server_emit[type].emit)
+ continue;
+
+ if (link->network->dhcp_server_emit[type].n_addresses > 0)
+ /* Explicitly specified servers to emit */
+ r = sd_dhcp_server_set_servers(
+ link->dhcp_server,
+ type,
+ link->network->dhcp_server_emit[type].addresses,
+ link->network->dhcp_server_emit[type].n_addresses);
+ else {
+ /* Emission is requested, but nothing explicitly configured. Let's find a suitable upling */
+ if (!acquired_uplink) {
+ (void) dhcp_server_find_uplink(link, &uplink);
+ acquired_uplink = true;
+ }
+
+ if (uplink && uplink->network)
+ r = link_push_uplink_to_dhcp_server(uplink, type, link->dhcp_server);
+ else if (type == SD_DHCP_LEASE_DNS)
+ r = dhcp4_server_set_dns_from_resolve_conf(link);
+ else {
+ log_link_debug(link,
+ "Not emitting %s on link, couldn't find suitable uplink.",
+ dhcp_lease_server_type_to_string(type));
+ continue;
+ }
+ }
+
+ if (r < 0)
+ log_link_warning_errno(link, r,
+ "Failed to set %s for DHCP server, ignoring: %m",
+ dhcp_lease_server_type_to_string(type));
+ }
+
+ if (link->network->dhcp_server_emit_router) {
+ r = sd_dhcp_server_set_router(link->dhcp_server, &link->network->dhcp_server_router);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set router address for DHCP server: %m");
+ }
+
+ r = sd_dhcp_server_set_relay_target(link->dhcp_server, &link->network->dhcp_server_relay_target);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set relay target for DHCP server: %m");
+
+ bind_to_interface = sd_dhcp_server_is_in_relay_mode(link->dhcp_server) ? false : link->network->dhcp_server_bind_to_interface;
+ r = sd_dhcp_server_set_bind_to_interface(link->dhcp_server, bind_to_interface);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set interface binding for DHCP server: %m");
+
+ r = sd_dhcp_server_set_relay_agent_information(link->dhcp_server, link->network->dhcp_server_relay_agent_circuit_id, link->network->dhcp_server_relay_agent_remote_id);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set agent circuit/remote id for DHCP server: %m");
+
+ if (link->network->dhcp_server_emit_timezone) {
+ _cleanup_free_ char *buffer = NULL;
+ const char *tz = NULL;
+
+ if (link->network->dhcp_server_timezone)
+ tz = link->network->dhcp_server_timezone;
+ else {
+ r = get_timezone(&buffer);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to determine timezone, not sending timezone: %m");
+ else
+ tz = buffer;
+ }
+
+ if (tz) {
+ r = sd_dhcp_server_set_timezone(link->dhcp_server, tz);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set timezone for DHCP server: %m");
+ }
+ }
+
+ ORDERED_HASHMAP_FOREACH(p, link->network->dhcp_server_send_options) {
+ r = sd_dhcp_server_add_option(link->dhcp_server, p);
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set DHCPv4 option: %m");
+ }
+
+ ORDERED_HASHMAP_FOREACH(p, link->network->dhcp_server_send_vendor_options) {
+ r = sd_dhcp_server_add_vendor_option(link->dhcp_server, p);
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set DHCPv4 option: %m");
+ }
+
+ HASHMAP_FOREACH(static_lease, link->network->dhcp_static_leases_by_section) {
+ r = sd_dhcp_server_set_static_lease(link->dhcp_server, &static_lease->address, static_lease->client_id, static_lease->client_id_size);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set DHCPv4 static lease for DHCP server: %m");
+ }
+
+ r = sd_dhcp_server_start(link->dhcp_server);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not start DHCPv4 server instance: %m");
+
+ log_link_debug(link, "Offering DHCPv4 leases");
+ return 0;
+}
+
+static bool dhcp_server_is_ready_to_configure(Link *link) {
+ Link *uplink = NULL;
+ Address *a;
+
+ assert(link);
+
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false))
+ return false;
+
+ if (!link_has_carrier(link))
+ return false;
+
+ if (!link->static_addresses_configured)
+ return false;
+
+ if (link_find_dhcp_server_address(link, &a) < 0)
+ return false;
+
+ if (!address_is_ready(a))
+ return false;
+
+ if (dhcp_server_find_uplink(link, &uplink) < 0)
+ return false;
+
+ if (uplink && !uplink->network)
+ return false;
+
+ return true;
+}
+
+static int dhcp_server_process_request(Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(link);
+
+ if (!dhcp_server_is_ready_to_configure(link))
+ return 0;
+
+ r = dhcp4_server_configure(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure DHCP server: %m");
+
+ return 1;
+}
+
+int link_request_dhcp_server(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link_dhcp4_server_enabled(link))
+ return 0;
+
+ if (link->dhcp_server)
+ return 0;
+
+ log_link_debug(link, "Requesting DHCP server.");
+ r = link_queue_request(link, REQUEST_TYPE_DHCP_SERVER, dhcp_server_process_request, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request configuration of DHCP server: %m");
+
+ return 0;
+}
+
+int config_parse_dhcp_server_relay_agent_suboption(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ char **suboption_value = data;
+ char* p;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *suboption_value = mfree(*suboption_value);
+ return 0;
+ }
+
+ p = startswith(rvalue, "string:");
+ if (!p) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse %s=%s'. Invalid format, ignoring.", lvalue, rvalue);
+ return 0;
+ }
+ return free_and_strdup(suboption_value, empty_to_null(p));
+}
+
+int config_parse_dhcp_server_emit(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ NetworkDHCPServerEmitAddress *emit = ASSERT_PTR(data);
+
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ emit->addresses = mfree(emit->addresses);
+ emit->n_addresses = 0;
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *w = NULL;
+ union in_addr_union a;
+ int r;
+
+ r = extract_first_word(&p, &w, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract word, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ if (streq(w, "_server_address"))
+ a = IN_ADDR_NULL; /* null address will be converted to the server address. */
+ else {
+ r = in_addr_from_string(AF_INET, w, &a);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s= address '%s', ignoring: %m", lvalue, w);
+ continue;
+ }
+
+ if (in4_addr_is_null(&a.in)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Found a null address in %s=, ignoring.", lvalue);
+ continue;
+ }
+ }
+
+ if (!GREEDY_REALLOC(emit->addresses, emit->n_addresses + 1))
+ return log_oom();
+
+ emit->addresses[emit->n_addresses++] = a.in;
+ }
+}
+
+int config_parse_dhcp_server_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ union in_addr_union a;
+ unsigned char prefixlen;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->dhcp_server_address = (struct in_addr) {};
+ network->dhcp_server_address_prefixlen = 0;
+ return 0;
+ }
+
+ r = in_addr_prefix_from_string(rvalue, AF_INET, &a, &prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+ if (in4_addr_is_null(&a.in) || in4_addr_is_localhost(&a.in)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "DHCP server address cannot be the ANY address or a localhost address, "
+ "ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ network->dhcp_server_address = a.in;
+ network->dhcp_server_address_prefixlen = prefixlen;
+ return 0;
+}
diff --git a/src/network/networkd-dhcp-server.h b/src/network/networkd-dhcp-server.h
new file mode 100644
index 0000000..cb2a8b6
--- /dev/null
+++ b/src/network/networkd-dhcp-server.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+void network_adjust_dhcp_server(Network *network);
+
+int link_request_dhcp_server_address(Link *link);
+int link_request_dhcp_server(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_relay_agent_suboption);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_emit);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_address);
diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c
new file mode 100644
index 0000000..eef7633
--- /dev/null
+++ b/src/network/networkd-dhcp4.c
@@ -0,0 +1,1784 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+
+#include "alloc-util.h"
+#include "dhcp-client-internal.h"
+#include "hostname-setup.h"
+#include "hostname-util.h"
+#include "parse-util.h"
+#include "network-internal.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-prefix-delegation.h"
+#include "networkd-dhcp4.h"
+#include "networkd-ipv4acd.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-nexthop.h"
+#include "networkd-queue.h"
+#include "networkd-route.h"
+#include "networkd-setlink.h"
+#include "networkd-state-file.h"
+#include "string-table.h"
+#include "strv.h"
+#include "sysctl-util.h"
+
+void network_adjust_dhcp4(Network *network) {
+ assert(network);
+
+ if (!FLAGS_SET(network->dhcp, ADDRESS_FAMILY_IPV4))
+ return;
+
+ if (network->dhcp_use_gateway < 0)
+ network->dhcp_use_gateway = network->dhcp_use_routes;
+
+ /* RFC7844 section 3.: MAY contain the Client Identifier option
+ * Section 3.5: clients MUST use client identifiers based solely on the link-layer address
+ * NOTE: Using MAC, as it does not reveal extra information, and some servers might not answer
+ * if this option is not sent */
+ if (network->dhcp_anonymize &&
+ network->dhcp_client_identifier >= 0 &&
+ network->dhcp_client_identifier != DHCP_CLIENT_ID_MAC) {
+ log_warning("%s: ClientIdentifier= is set, although Anonymize=yes. Using ClientIdentifier=mac.",
+ network->filename);
+ network->dhcp_client_identifier = DHCP_CLIENT_ID_MAC;
+ }
+
+ if (network->dhcp_client_identifier < 0)
+ network->dhcp_client_identifier = network->dhcp_anonymize ? DHCP_CLIENT_ID_MAC : DHCP_CLIENT_ID_DUID;
+}
+
+static int dhcp4_remove_address_and_routes(Link *link, bool only_marked) {
+ Address *address;
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(route, link->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_DHCP4)
+ continue;
+ if (only_marked && !route_is_marked(route))
+ continue;
+
+ k = route_remove(route);
+ if (k < 0)
+ r = k;
+
+ route_cancel_request(route, link);
+ }
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_DHCP4)
+ continue;
+ if (only_marked && !address_is_marked(address))
+ continue;
+
+ k = address_remove(address);
+ if (k < 0)
+ r = k;
+
+ address_cancel_request(address);
+ }
+
+ return r;
+}
+
+static int dhcp4_address_get(Link *link, Address **ret) {
+ Address *address;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_DHCP4)
+ continue;
+ if (address_is_marked(address))
+ continue;
+
+ if (ret)
+ *ret = address;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int dhcp4_address_ready_callback(Address *address) {
+ assert(address);
+ assert(address->link);
+
+ /* Do not call this again. */
+ address->callback = NULL;
+
+ return dhcp4_check_ready(address->link);
+}
+
+int dhcp4_check_ready(Link *link) {
+ Address *address;
+ int r;
+
+ assert(link);
+
+ if (link->dhcp4_messages > 0) {
+ log_link_debug(link, "%s(): DHCPv4 address and routes are not set.", __func__);
+ return 0;
+ }
+
+ if (dhcp4_address_get(link, &address) < 0) {
+ log_link_debug(link, "%s(): DHCPv4 address is not set.", __func__);
+ return 0;
+ }
+
+ if (!address_is_ready(address)) {
+ log_link_debug(link, "%s(): DHCPv4 address is not ready.", __func__);
+ address->callback = dhcp4_address_ready_callback;
+ return 0;
+ }
+
+ link->dhcp4_configured = true;
+ log_link_debug(link, "DHCPv4 address and routes set.");
+
+ /* New address and routes are configured now. Let's release old lease. */
+ r = dhcp4_remove_address_and_routes(link, /* only_marked = */ true);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4ll_stop(link->ipv4ll);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to drop IPv4 link-local address: %m");
+
+ link_check_ready(link);
+ return 0;
+}
+
+static int dhcp4_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set DHCPv4 route");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ r = dhcp4_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int dhcp4_request_route(Route *in, Link *link) {
+ _cleanup_(route_freep) Route *route = in;
+ struct in_addr server;
+ Route *existing;
+ int r;
+
+ assert(route);
+ assert(link);
+ assert(link->dhcp_lease);
+
+ r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &server);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to get DHCP server IP address: %m");
+
+ route->source = NETWORK_CONFIG_SOURCE_DHCP4;
+ route->provider.in = server;
+ route->family = AF_INET;
+ if (!route->protocol_set)
+ route->protocol = RTPROT_DHCP;
+ if (!route->priority_set)
+ route->priority = link->network->dhcp_route_metric;
+ if (!route->table_set)
+ route->table = link_get_dhcp4_route_table(link);
+ if (route->mtu == 0)
+ route->mtu = link->network->dhcp_route_mtu;
+
+ if (route_get(NULL, link, route, &existing) < 0) /* This is a new route. */
+ link->dhcp4_configured = false;
+ else
+ route_unmark(existing);
+
+ return link_request_route(link, TAKE_PTR(route), true, &link->dhcp4_messages,
+ dhcp4_route_handler, NULL);
+}
+
+static bool link_prefixroute(Link *link) {
+ return !link->network->dhcp_route_table_set ||
+ link->network->dhcp_route_table == RT_TABLE_MAIN;
+}
+
+static int dhcp4_request_prefix_route(Link *link) {
+ _cleanup_(route_freep) Route *route = NULL;
+ struct in_addr address, netmask;
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+
+ if (link_prefixroute(link))
+ /* When true, the route will be created by kernel. See dhcp4_update_address(). */
+ return 0;
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &address);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_lease_get_netmask(link->dhcp_lease, &netmask);
+ if (r < 0)
+ return r;
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route->dst.in.s_addr = address.s_addr & netmask.s_addr;
+ route->dst_prefixlen = in4_addr_netmask_to_prefixlen(&netmask);
+ route->prefsrc.in = address;
+ route->scope = RT_SCOPE_LINK;
+
+ return dhcp4_request_route(TAKE_PTR(route), link);
+}
+
+static int dhcp4_request_route_to_gateway(Link *link, const struct in_addr *gw) {
+ _cleanup_(route_freep) Route *route = NULL;
+ struct in_addr address;
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(gw);
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &address);
+ if (r < 0)
+ return r;
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route->dst.in = *gw;
+ route->dst_prefixlen = 32;
+ route->prefsrc.in = address;
+ route->scope = RT_SCOPE_LINK;
+
+ return dhcp4_request_route(TAKE_PTR(route), link);
+}
+
+static int dhcp4_request_route_auto(
+ Route *in,
+ Link *link,
+ const struct in_addr *gw,
+ bool force_use_gw) {
+
+ _cleanup_(route_freep) Route *route = in;
+ struct in_addr address, netmask, prefix;
+ uint8_t prefixlen;
+ int r;
+
+ assert(route);
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(gw);
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &address);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_lease_get_netmask(link->dhcp_lease, &netmask);
+ if (r < 0)
+ return r;
+
+ prefix.s_addr = address.s_addr & netmask.s_addr;
+ prefixlen = in4_addr_netmask_to_prefixlen(&netmask);
+
+ if (in4_addr_is_localhost(&route->dst.in)) {
+ if (in4_addr_is_set(gw))
+ log_link_debug(link, "DHCP: requested route destination "IPV4_ADDRESS_FMT_STR"/%u is localhost, "
+ "ignoring gateway address "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(route->dst.in), route->dst_prefixlen, IPV4_ADDRESS_FMT_VAL(*gw));
+
+ route->scope = RT_SCOPE_HOST;
+ route->gw_family = AF_UNSPEC;
+ route->gw = IN_ADDR_NULL;
+ route->prefsrc = IN_ADDR_NULL;
+
+ } else if (in4_addr_equal(&route->dst.in, &address)) {
+ if (in4_addr_is_set(gw))
+ log_link_debug(link, "DHCP: requested route destination "IPV4_ADDRESS_FMT_STR"/%u is equivalent to the acquired address, "
+ "ignoring gateway address "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(route->dst.in), route->dst_prefixlen, IPV4_ADDRESS_FMT_VAL(*gw));
+
+ route->scope = RT_SCOPE_HOST;
+ route->gw_family = AF_UNSPEC;
+ route->gw = IN_ADDR_NULL;
+ route->prefsrc.in = address;
+
+ } else if (!force_use_gw &&
+ route->dst_prefixlen >= prefixlen &&
+ (route->dst.in.s_addr & netmask.s_addr) == prefix.s_addr) {
+ if (in4_addr_is_set(gw))
+ log_link_debug(link, "DHCP: requested route destination "IPV4_ADDRESS_FMT_STR"/%u is in the assigned network "
+ IPV4_ADDRESS_FMT_STR"/%u, ignoring gateway address "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(route->dst.in), route->dst_prefixlen,
+ IPV4_ADDRESS_FMT_VAL(prefix), prefixlen,
+ IPV4_ADDRESS_FMT_VAL(*gw));
+
+ route->scope = RT_SCOPE_LINK;
+ route->gw_family = AF_UNSPEC;
+ route->gw = IN_ADDR_NULL;
+ route->prefsrc.in = address;
+
+ } else if (in4_addr_is_null(gw)) {
+ log_link_debug(link, "DHCP: requested route destination "IPV4_ADDRESS_FMT_STR"/%u is not in the assigned network "
+ IPV4_ADDRESS_FMT_STR"/%u, but no gateway is specified, using 'link' scope.",
+ IPV4_ADDRESS_FMT_VAL(route->dst.in), route->dst_prefixlen,
+ IPV4_ADDRESS_FMT_VAL(prefix), prefixlen);
+
+ route->scope = RT_SCOPE_LINK;
+ route->gw_family = AF_UNSPEC;
+ route->gw = IN_ADDR_NULL;
+ route->prefsrc.in = address;
+
+ } else {
+ r = dhcp4_request_route_to_gateway(link, gw);
+ if (r < 0)
+ return r;
+
+ route->scope = RT_SCOPE_UNIVERSE;
+ route->gw_family = AF_INET;
+ route->gw.in = *gw;
+ route->prefsrc.in = address;
+ }
+
+ return dhcp4_request_route(TAKE_PTR(route), link);
+}
+
+static int dhcp4_request_static_routes(Link *link, struct in_addr *ret_default_gw) {
+ _cleanup_free_ sd_dhcp_route **static_routes = NULL, **classless_routes = NULL;
+ size_t n_static_routes, n_classless_routes, n;
+ struct in_addr default_gw = {};
+ sd_dhcp_route **routes;
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(ret_default_gw);
+
+ r = sd_dhcp_lease_get_static_routes(link->dhcp_lease, &static_routes);
+ if (r == -ENODATA)
+ n_static_routes = 0;
+ else if (r < 0)
+ return r;
+ else
+ n_static_routes = r;
+
+ r = sd_dhcp_lease_get_classless_routes(link->dhcp_lease, &classless_routes);
+ if (r == -ENODATA)
+ n_classless_routes = 0;
+ else if (r < 0)
+ return r;
+ else
+ n_classless_routes = r;
+
+ if (n_classless_routes == 0 && n_static_routes == 0) {
+ log_link_debug(link, "DHCP: No static routes received from DHCP server.");
+ return 0;
+ }
+
+ /* if the DHCP server returns both a Classless Static Routes option and a Static Routes option,
+ * the DHCP client MUST ignore the Static Routes option. */
+ if (n_classless_routes > 0 && n_static_routes > 0)
+ log_link_debug(link, "Classless static routes received from DHCP server: ignoring static-route option");
+
+ if (!link->network->dhcp_use_routes) {
+
+ /* Even if UseRoutes=no, try to find default gateway to make semi-static routes and
+ * routes to DNS or NTP servers can be configured in later steps. */
+
+ for (size_t i = 0; i < n_classless_routes; i++) {
+ struct in_addr dst;
+ uint8_t prefixlen;
+
+ r = sd_dhcp_route_get_destination(classless_routes[i], &dst);
+ if (r < 0)
+ return r;
+
+ if (in4_addr_is_set(&dst))
+ continue;
+
+ r = sd_dhcp_route_get_destination_prefix_length(classless_routes[i], &prefixlen);
+ if (r < 0)
+ return r;
+
+ if (prefixlen != 0)
+ continue;
+
+ r = sd_dhcp_route_get_gateway(classless_routes[i], ret_default_gw);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ /* Do not return 1 here, to ensure the router option can override the default gateway
+ * that was found. */
+ return 0;
+ }
+
+ if (n_classless_routes > 0) {
+ n = n_classless_routes;
+ routes = classless_routes;
+ } else if (n_static_routes > 0){
+ n = n_static_routes;
+ routes = static_routes;
+ } else
+ assert_not_reached();
+
+ for (size_t i = 0; i < n; i++) {
+ _cleanup_(route_freep) Route *route = NULL;
+ struct in_addr gw;
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route->gw_family = AF_INET;
+
+ r = sd_dhcp_route_get_gateway(routes[i], &gw);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_route_get_destination(routes[i], &route->dst.in);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_route_get_destination_prefix_length(routes[i], &route->dst_prefixlen);
+ if (r < 0)
+ return r;
+
+ /* When classless static routes are provided, then router option will be ignored. To
+ * use the default gateway later in other routes, e.g., routes to dns servers, here we
+ * need to find the default gateway in the classless static routes. */
+ if (n_classless_routes > 0 &&
+ in4_addr_is_null(&route->dst.in) && route->dst_prefixlen == 0 &&
+ in4_addr_is_null(&default_gw))
+ default_gw = gw;
+
+ /* Do not ignore the gateway given by the classless route option even if the destination is
+ * in the same network. See issue #28280. */
+ r = dhcp4_request_route_auto(TAKE_PTR(route), link, &gw, /* force_use_gw = */ n_classless_routes > 0);
+ if (r < 0)
+ return r;
+ }
+
+ *ret_default_gw = default_gw;
+ return n_classless_routes > 0;
+}
+
+static int dhcp4_request_gateway(Link *link, struct in_addr *gw) {
+ _cleanup_(route_freep) Route *route = NULL;
+ const struct in_addr *router;
+ struct in_addr address;
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(gw);
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &address);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_lease_get_router(link->dhcp_lease, &router);
+ if (IN_SET(r, 0, -ENODATA)) {
+ log_link_debug(link, "DHCP: No gateway received from DHCP server.");
+ return 0;
+ }
+ if (r < 0)
+ return r;
+ if (in4_addr_is_null(&router[0])) {
+ log_link_debug(link, "DHCP: Received gateway address is null.");
+ return 0;
+ }
+
+ if (!link->network->dhcp_use_gateway) {
+ /* When no classless static route is provided, even if UseGateway=no, use the gateway
+ * address to configure semi-static routes or routes to DNS or NTP servers. Note, if
+ * neither UseRoutes= nor UseGateway= is disabled, use the default gateway in classless
+ * static routes if provided (in that case, in4_addr_is_null(gw) below is true). */
+ if (in4_addr_is_null(gw))
+ *gw = router[0];
+ return 0;
+ }
+
+ /* The dhcp netmask may mask out the gateway. First, add an explicit route for the gateway host
+ * so that we can route no matter the netmask or existing kernel route tables. */
+ r = dhcp4_request_route_to_gateway(link, &router[0]);
+ if (r < 0)
+ return r;
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ /* Next, add a default gateway. */
+ route->gw_family = AF_INET;
+ route->gw.in = router[0];
+ route->prefsrc.in = address;
+
+ r = dhcp4_request_route(TAKE_PTR(route), link);
+ if (r < 0)
+ return r;
+
+ /* When no classless static route is provided, or UseRoutes=no, then use the router address to
+ * configure semi-static routes and routes to DNS or NTP servers in later steps. */
+ *gw = router[0];
+ return 0;
+}
+
+static int dhcp4_request_semi_static_routes(Link *link, const struct in_addr *gw) {
+ Route *rt;
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(link->network);
+ assert(gw);
+
+ if (in4_addr_is_null(gw))
+ return 0;
+
+ HASHMAP_FOREACH(rt, link->network->routes_by_section) {
+ _cleanup_(route_freep) Route *route = NULL;
+
+ if (!rt->gateway_from_dhcp_or_ra)
+ continue;
+
+ if (rt->gw_family != AF_INET)
+ continue;
+
+ r = dhcp4_request_route_to_gateway(link, gw);
+ if (r < 0)
+ return r;
+
+ r = route_dup(rt, &route);
+ if (r < 0)
+ return r;
+
+ route->gw.in = *gw;
+
+ r = dhcp4_request_route(TAKE_PTR(route), link);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int dhcp4_request_routes_to_servers(
+ Link *link,
+ const struct in_addr *servers,
+ size_t n_servers,
+ const struct in_addr *gw) {
+
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(link->network);
+ assert(servers || n_servers == 0);
+ assert(gw);
+
+ for (size_t i = 0; i < n_servers; i++) {
+ _cleanup_(route_freep) Route *route = NULL;
+
+ if (in4_addr_is_null(&servers[i]))
+ continue;
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route->dst.in = servers[i];
+ route->dst_prefixlen = 32;
+
+ r = dhcp4_request_route_auto(TAKE_PTR(route), link, gw, /* force_use_gw = */ false);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int dhcp4_request_routes_to_dns(Link *link, const struct in_addr *gw) {
+ const struct in_addr *dns;
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(link->network);
+ assert(gw);
+
+ if (!link->network->dhcp_use_dns ||
+ !link->network->dhcp_routes_to_dns)
+ return 0;
+
+ r = sd_dhcp_lease_get_dns(link->dhcp_lease, &dns);
+ if (IN_SET(r, 0, -ENODATA))
+ return 0;
+ if (r < 0)
+ return r;
+
+ return dhcp4_request_routes_to_servers(link, dns, r, gw);
+}
+
+static int dhcp4_request_routes_to_ntp(Link *link, const struct in_addr *gw) {
+ const struct in_addr *ntp;
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(link->network);
+ assert(gw);
+
+ if (!link->network->dhcp_use_ntp ||
+ !link->network->dhcp_routes_to_ntp)
+ return 0;
+
+ r = sd_dhcp_lease_get_ntp(link->dhcp_lease, &ntp);
+ if (IN_SET(r, 0, -ENODATA))
+ return 0;
+ if (r < 0)
+ return r;
+
+ return dhcp4_request_routes_to_servers(link, ntp, r, gw);
+}
+
+static int dhcp4_request_routes(Link *link) {
+ struct in_addr gw = {};
+ int r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+
+ r = dhcp4_request_prefix_route(link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: Could not request prefix route: %m");
+
+ r = dhcp4_request_static_routes(link, &gw);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: Could not request static routes: %m");
+ if (r == 0) {
+ /* According to RFC 3442: If the DHCP server returns both a Classless Static Routes option and
+ * a Router option, the DHCP client MUST ignore the Router option. */
+ r = dhcp4_request_gateway(link, &gw);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: Could not request gateway: %m");
+ }
+
+ r = dhcp4_request_semi_static_routes(link, &gw);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: Could not request routes with Gateway=_dhcp4 setting: %m");
+
+ r = dhcp4_request_routes_to_dns(link, &gw);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: Could not request routes to DNS servers: %m");
+
+ r = dhcp4_request_routes_to_ntp(link, &gw);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: Could not request routes to NTP servers: %m");
+
+ return 0;
+}
+
+static int dhcp_reset_mtu(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link->network->dhcp_use_mtu)
+ return 0;
+
+ r = link_request_to_set_mtu(link, link->original_mtu);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: Could not queue request to reset MTU: %m");
+
+ return 0;
+}
+
+static int dhcp_reset_hostname(Link *link) {
+ const char *hostname;
+ int r;
+
+ assert(link);
+
+ if (!link->network->dhcp_use_hostname)
+ return 0;
+
+ hostname = link->network->dhcp_hostname;
+ if (!hostname)
+ (void) sd_dhcp_lease_get_hostname(link->dhcp_lease, &hostname);
+
+ if (!hostname)
+ return 0;
+
+ /* If a hostname was set due to the lease, then unset it now. */
+ r = manager_set_hostname(link->manager, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: Failed to reset transient hostname: %m");
+
+ return 0;
+}
+
+int dhcp4_lease_lost(Link *link) {
+ int k, r = 0;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(link->network);
+
+ log_link_info(link, "DHCP lease lost");
+
+ link->dhcp4_configured = false;
+
+ if (link->network->dhcp_use_6rd &&
+ dhcp4_lease_has_pd_prefix(link->dhcp_lease))
+ dhcp4_pd_prefix_lost(link);
+
+ k = dhcp4_remove_address_and_routes(link, /* only_marked = */ false);
+ if (k < 0)
+ r = k;
+
+ k = dhcp_reset_mtu(link);
+ if (k < 0)
+ r = k;
+
+ k = dhcp_reset_hostname(link);
+ if (k < 0)
+ r = k;
+
+ link->dhcp_lease = sd_dhcp_lease_unref(link->dhcp_lease);
+ link_dirty(link);
+
+ /* If one of the above failed. Do not request nexthops and routes. */
+ if (r < 0)
+ return r;
+
+ r = link_request_static_nexthops(link, true);
+ if (r < 0)
+ return r;
+
+ return link_request_static_routes(link, true);
+}
+
+static int dhcp4_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) {
+ int r;
+
+ assert(link);
+
+ r = address_configure_handler_internal(rtnl, m, link, "Could not set DHCPv4 address");
+ if (r <= 0)
+ return r;
+
+ r = dhcp4_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int dhcp4_request_address(Link *link, bool announce) {
+ _cleanup_(address_freep) Address *addr = NULL;
+ struct in_addr address, netmask, server;
+ unsigned prefixlen;
+ Address *existing;
+ usec_t lifetime_usec;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+ assert(link->dhcp_lease);
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &address);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "DHCP error: no address: %m");
+
+ r = sd_dhcp_lease_get_netmask(link->dhcp_lease, &netmask);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "DHCP error: no netmask: %m");
+
+ r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &server);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCP error: failed to get DHCP server IP address: %m");
+
+ if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) {
+ uint32_t lifetime_sec;
+ usec_t now_usec;
+
+ r = sd_dhcp_lease_get_lifetime(link->dhcp_lease, &lifetime_sec);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "DHCP error: no lifetime: %m");
+
+ assert_se(sd_event_now(link->manager->event, CLOCK_BOOTTIME, &now_usec) >= 0);
+ lifetime_usec = sec_to_usec(lifetime_sec, now_usec);
+ } else
+ lifetime_usec = USEC_INFINITY;
+
+ prefixlen = in4_addr_netmask_to_prefixlen(&netmask);
+
+ if (announce) {
+ const struct in_addr *router;
+
+ r = sd_dhcp_lease_get_router(link->dhcp_lease, &router);
+ if (r < 0 && r != -ENODATA)
+ return log_link_error_errno(link, r, "DHCP error: Could not get gateway: %m");
+
+ if (r > 0 && in4_addr_is_set(&router[0]))
+ log_struct(LOG_INFO,
+ LOG_LINK_INTERFACE(link),
+ LOG_LINK_MESSAGE(link, "DHCPv4 address "IPV4_ADDRESS_FMT_STR"/%u, gateway "IPV4_ADDRESS_FMT_STR" acquired from "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(address),
+ prefixlen,
+ IPV4_ADDRESS_FMT_VAL(router[0]),
+ IPV4_ADDRESS_FMT_VAL(server)),
+ "ADDRESS="IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(address),
+ "PREFIXLEN=%u", prefixlen,
+ "GATEWAY="IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(router[0]));
+ else
+ log_struct(LOG_INFO,
+ LOG_LINK_INTERFACE(link),
+ LOG_LINK_MESSAGE(link, "DHCPv4 address "IPV4_ADDRESS_FMT_STR"/%u acquired from "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(address),
+ prefixlen,
+ IPV4_ADDRESS_FMT_VAL(server)),
+ "ADDRESS="IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(address),
+ "PREFIXLEN=%u", prefixlen);
+ }
+
+ r = address_new(&addr);
+ if (r < 0)
+ return log_oom();
+
+ addr->source = NETWORK_CONFIG_SOURCE_DHCP4;
+ addr->provider.in = server;
+ addr->family = AF_INET;
+ addr->in_addr.in.s_addr = address.s_addr;
+ addr->lifetime_preferred_usec = lifetime_usec;
+ addr->lifetime_valid_usec = lifetime_usec;
+ addr->prefixlen = prefixlen;
+ address_set_broadcast(addr, link);
+ SET_FLAG(addr->flags, IFA_F_NOPREFIXROUTE, !link_prefixroute(link));
+ addr->route_metric = link->network->dhcp_route_metric;
+ addr->duplicate_address_detection = link->network->dhcp_send_decline ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_NO;
+
+ r = free_and_strdup_warn(&addr->label, link->network->dhcp_label);
+ if (r < 0)
+ return r;
+
+ r = free_and_strdup_warn(&addr->netlabel, link->network->dhcp_netlabel);
+ if (r < 0)
+ return r;
+
+ if (address_get(link, addr, &existing) < 0) /* The address is new. */
+ link->dhcp4_configured = false;
+ else
+ address_unmark(existing);
+
+ r = link_request_address(link, TAKE_PTR(addr), true, &link->dhcp4_messages,
+ dhcp4_address_handler, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request DHCPv4 address: %m");
+
+ return 0;
+}
+
+static int dhcp4_request_address_and_routes(Link *link, bool announce) {
+ int r;
+
+ assert(link);
+
+ link_mark_addresses(link, NETWORK_CONFIG_SOURCE_DHCP4);
+ link_mark_routes(link, NETWORK_CONFIG_SOURCE_DHCP4);
+
+ r = dhcp4_request_address(link, announce);
+ if (r < 0)
+ return r;
+
+ r = dhcp4_request_routes(link);
+ if (r < 0)
+ return r;
+
+ if (!link->dhcp4_configured) {
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ link_check_ready(link);
+ }
+
+ return 0;
+}
+
+static int dhcp_lease_renew(sd_dhcp_client *client, Link *link) {
+ _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *old_lease = NULL;
+ sd_dhcp_lease *lease;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(client);
+
+ r = sd_dhcp_client_get_lease(client, &lease);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "DHCP error: no lease: %m");
+
+ old_lease = TAKE_PTR(link->dhcp_lease);
+ link->dhcp_lease = sd_dhcp_lease_ref(lease);
+ link_dirty(link);
+
+ if (link->network->dhcp_use_6rd) {
+ if (dhcp4_lease_has_pd_prefix(link->dhcp_lease)) {
+ r = dhcp4_pd_prefix_acquired(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to process 6rd option: %m");
+ } else if (dhcp4_lease_has_pd_prefix(old_lease))
+ dhcp4_pd_prefix_lost(link);
+ }
+
+ return dhcp4_request_address_and_routes(link, false);
+}
+
+static int dhcp_lease_acquired(sd_dhcp_client *client, Link *link) {
+ sd_dhcp_lease *lease;
+ int r;
+
+ assert(client);
+ assert(link);
+
+ r = sd_dhcp_client_get_lease(client, &lease);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: No lease: %m");
+
+ sd_dhcp_lease_unref(link->dhcp_lease);
+ link->dhcp_lease = sd_dhcp_lease_ref(lease);
+ link_dirty(link);
+
+ if (link->network->dhcp_use_mtu) {
+ uint16_t mtu;
+
+ r = sd_dhcp_lease_get_mtu(lease, &mtu);
+ if (r >= 0) {
+ r = link_request_to_set_mtu(link, mtu);
+ if (r < 0)
+ log_link_error_errno(link, r, "Failed to set MTU to %" PRIu16 ": %m", mtu);
+ }
+ }
+
+ if (link->network->dhcp_use_hostname) {
+ const char *dhcpname = NULL;
+ _cleanup_free_ char *hostname = NULL;
+
+ if (link->network->dhcp_hostname)
+ dhcpname = link->network->dhcp_hostname;
+ else
+ (void) sd_dhcp_lease_get_hostname(lease, &dhcpname);
+
+ if (dhcpname) {
+ r = shorten_overlong(dhcpname, &hostname);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Unable to shorten overlong DHCP hostname '%s', ignoring: %m", dhcpname);
+ if (r == 1)
+ log_link_notice(link, "Overlong DHCP hostname received, shortened from '%s' to '%s'", dhcpname, hostname);
+ }
+
+ if (hostname) {
+ r = manager_set_hostname(link->manager, hostname);
+ if (r < 0)
+ log_link_error_errno(link, r, "Failed to set transient hostname to '%s': %m", hostname);
+ }
+ }
+
+ if (link->network->dhcp_use_timezone) {
+ const char *tz = NULL;
+
+ (void) sd_dhcp_lease_get_timezone(link->dhcp_lease, &tz);
+
+ if (tz) {
+ r = manager_set_timezone(link->manager, tz);
+ if (r < 0)
+ log_link_error_errno(link, r, "Failed to set timezone to '%s': %m", tz);
+ }
+ }
+
+ if (link->network->dhcp_use_6rd &&
+ dhcp4_lease_has_pd_prefix(link->dhcp_lease)) {
+ r = dhcp4_pd_prefix_acquired(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to process 6rd option: %m");
+ }
+
+ return dhcp4_request_address_and_routes(link, true);
+}
+
+static int dhcp_lease_ip_change(sd_dhcp_client *client, Link *link) {
+ int r;
+
+ r = dhcp_lease_acquired(client, link);
+ if (r < 0)
+ (void) dhcp4_lease_lost(link);
+
+ return r;
+}
+
+static int dhcp_server_is_filtered(Link *link, sd_dhcp_client *client) {
+ sd_dhcp_lease *lease;
+ struct in_addr addr;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(client);
+
+ r = sd_dhcp_client_get_lease(client, &lease);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get DHCP lease: %m");
+
+ r = sd_dhcp_lease_get_server_identifier(lease, &addr);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to get DHCP server IP address: %m");
+
+ if (in4_address_is_filtered(&addr, link->network->dhcp_allow_listed_ip, link->network->dhcp_deny_listed_ip)) {
+ if (DEBUG_LOGGING) {
+ if (link->network->dhcp_allow_listed_ip)
+ log_link_debug(link, "DHCPv4 server IP address "IPV4_ADDRESS_FMT_STR" not found in allow-list, ignoring offer.",
+ IPV4_ADDRESS_FMT_VAL(addr));
+ else
+ log_link_debug(link, "DHCPv4 server IP address "IPV4_ADDRESS_FMT_STR" found in deny-list, ignoring offer.",
+ IPV4_ADDRESS_FMT_VAL(addr));
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+static int dhcp4_handler(sd_dhcp_client *client, int event, void *userdata) {
+ Link *link = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link->network);
+ assert(link->manager);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ switch (event) {
+ case SD_DHCP_CLIENT_EVENT_STOP:
+ if (link->ipv4ll) {
+ log_link_debug(link, "DHCP client is stopped. Acquiring IPv4 link-local address");
+
+ if (in4_addr_is_set(&link->network->ipv4ll_start_address)) {
+ r = sd_ipv4ll_set_address(link->ipv4ll, &link->network->ipv4ll_start_address);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set IPv4 link-local start address: %m");
+ }
+
+ r = sd_ipv4ll_start(link->ipv4ll);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m");
+ }
+
+ if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) {
+ log_link_notice(link, "DHCPv4 connection considered critical, ignoring request to reconfigure it.");
+ return 0;
+ }
+
+ if (link->dhcp_lease) {
+ if (link->network->dhcp_send_release) {
+ r = sd_dhcp_client_send_release(client);
+ if (r < 0)
+ log_link_full_errno(link,
+ ERRNO_IS_DISCONNECT(r) ? LOG_DEBUG : LOG_WARNING,
+ r, "Failed to send DHCP RELEASE, ignoring: %m");
+ }
+
+ r = dhcp4_lease_lost(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ break;
+ case SD_DHCP_CLIENT_EVENT_EXPIRED:
+ if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) {
+ log_link_notice(link, "DHCPv4 connection considered critical, ignoring request to reconfigure it.");
+ return 0;
+ }
+
+ if (link->dhcp_lease) {
+ r = dhcp4_lease_lost(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ break;
+ case SD_DHCP_CLIENT_EVENT_IP_CHANGE:
+ if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) {
+ log_link_notice(link, "DHCPv4 connection considered critical, ignoring request to reconfigure it.");
+ return 0;
+ }
+
+ r = dhcp_lease_ip_change(client, link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+
+ break;
+ case SD_DHCP_CLIENT_EVENT_RENEW:
+ r = dhcp_lease_renew(client, link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ break;
+ case SD_DHCP_CLIENT_EVENT_IP_ACQUIRE:
+ r = dhcp_lease_acquired(client, link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ break;
+ case SD_DHCP_CLIENT_EVENT_SELECTING:
+ r = dhcp_server_is_filtered(link, client);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ if (r > 0)
+ return -ENOMSG;
+ break;
+
+ case SD_DHCP_CLIENT_EVENT_TRANSIENT_FAILURE:
+ if (link->ipv4ll && !sd_ipv4ll_is_running(link->ipv4ll)) {
+ log_link_debug(link, "Problems acquiring DHCP lease, acquiring IPv4 link-local address");
+
+ if (in4_addr_is_set(&link->network->ipv4ll_start_address)) {
+ r = sd_ipv4ll_set_address(link->ipv4ll, &link->network->ipv4ll_start_address);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set IPv4 link-local start address: %m");
+ }
+
+ r = sd_ipv4ll_start(link->ipv4ll);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m");
+ }
+ break;
+
+ default:
+ if (event < 0)
+ log_link_warning_errno(link, event, "DHCP error: Client failed: %m");
+ else
+ log_link_warning(link, "DHCP unknown event: %i", event);
+ break;
+ }
+
+ return 0;
+}
+
+static int dhcp4_set_hostname(Link *link) {
+ _cleanup_free_ char *hostname = NULL;
+ const char *hn;
+ int r;
+
+ assert(link);
+
+ if (!link->network->dhcp_send_hostname)
+ hn = NULL;
+ else if (link->network->dhcp_hostname)
+ hn = link->network->dhcp_hostname;
+ else {
+ r = gethostname_strict(&hostname);
+ if (r < 0 && r != -ENXIO) /* ENXIO: no hostname set or hostname is "localhost" */
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to get hostname: %m");
+
+ hn = hostname;
+ }
+
+ r = sd_dhcp_client_set_hostname(link->dhcp_client, hn);
+ if (r == -EINVAL && hostname)
+ /* Ignore error when the machine's hostname is not suitable to send in DHCP packet. */
+ log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set hostname from kernel hostname, ignoring: %m");
+ else if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set hostname: %m");
+
+ return 0;
+}
+
+static int dhcp4_set_client_identifier(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->dhcp_client);
+
+ switch (link->network->dhcp_client_identifier) {
+ case DHCP_CLIENT_ID_DUID: {
+ /* If configured, apply user specified DUID and IAID */
+ const DUID *duid = link_get_dhcp4_duid(link);
+
+ if (duid->type == DUID_TYPE_LLT && duid->raw_data_len == 0)
+ r = sd_dhcp_client_set_iaid_duid_llt(link->dhcp_client,
+ link->network->dhcp_iaid_set,
+ link->network->dhcp_iaid,
+ duid->llt_time);
+ else
+ r = sd_dhcp_client_set_iaid_duid(link->dhcp_client,
+ link->network->dhcp_iaid_set,
+ link->network->dhcp_iaid,
+ duid->type,
+ duid->raw_data_len > 0 ? duid->raw_data : NULL,
+ duid->raw_data_len);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set IAID+DUID: %m");
+ break;
+ }
+ case DHCP_CLIENT_ID_DUID_ONLY: {
+ /* If configured, apply user specified DUID */
+ const DUID *duid = link_get_dhcp4_duid(link);
+
+ if (duid->type == DUID_TYPE_LLT && duid->raw_data_len == 0)
+ r = sd_dhcp_client_set_duid_llt(link->dhcp_client,
+ duid->llt_time);
+ else
+ r = sd_dhcp_client_set_duid(link->dhcp_client,
+ duid->type,
+ duid->raw_data_len > 0 ? duid->raw_data : NULL,
+ duid->raw_data_len);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set DUID: %m");
+ break;
+ }
+ case DHCP_CLIENT_ID_MAC: {
+ const uint8_t *hw_addr = link->hw_addr.bytes;
+ size_t hw_addr_len = link->hw_addr.length;
+
+ if (link->iftype == ARPHRD_INFINIBAND && hw_addr_len == INFINIBAND_ALEN) {
+ /* set_client_id expects only last 8 bytes of an IB address */
+ hw_addr += INFINIBAND_ALEN - 8;
+ hw_addr_len -= INFINIBAND_ALEN - 8;
+ }
+
+ r = sd_dhcp_client_set_client_id(link->dhcp_client,
+ link->iftype,
+ hw_addr,
+ hw_addr_len);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set client ID: %m");
+ break;
+ }
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+static int dhcp4_set_request_address(Link *link) {
+ Address *a;
+
+ assert(link);
+ assert(link->network);
+ assert(link->dhcp_client);
+
+ if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP))
+ return 0;
+
+ SET_FOREACH(a, link->addresses) {
+ if (a->source != NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+ if (a->family != AF_INET)
+ continue;
+ if (link_address_is_dynamic(link, a))
+ break;
+ }
+
+ if (!a)
+ return 0;
+
+ log_link_debug(link, "DHCPv4 CLIENT: requesting " IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(a->in_addr.in));
+
+ return sd_dhcp_client_set_request_address(link->dhcp_client, &a->in_addr.in);
+}
+
+static bool link_needs_dhcp_broadcast(Link *link) {
+ const char *val;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ /* Return the setting in DHCP[4].RequestBroadcast if specified. Otherwise return the device property
+ * ID_NET_DHCP_BROADCAST setting, which may be set for interfaces requiring that the DHCPOFFER message
+ * is being broadcast because they can't handle unicast messages while not fully configured.
+ * If neither is set or a failure occurs, return false, which is the default for this flag.
+ */
+ r = link->network->dhcp_broadcast;
+ if (r < 0 && link->dev && sd_device_get_property_value(link->dev, "ID_NET_DHCP_BROADCAST", &val) >= 0) {
+ r = parse_boolean(val);
+ if (r < 0)
+ log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to parse ID_NET_DHCP_BROADCAST, ignoring: %m");
+ else
+ log_link_debug(link, "DHCPv4 CLIENT: Detected ID_NET_DHCP_BROADCAST='%d'.", r);
+
+ }
+ return r == true;
+}
+
+static int dhcp4_configure(Link *link) {
+ sd_dhcp_option *send_option;
+ void *request_options;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->dhcp_client)
+ return log_link_debug_errno(link, SYNTHETIC_ERRNO(EBUSY), "DHCPv4 client is already configured.");
+
+ r = sd_dhcp_client_new(&link->dhcp_client, link->network->dhcp_anonymize);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to allocate DHCPv4 client: %m");
+
+ r = sd_dhcp_client_attach_event(link->dhcp_client, link->manager->event, 0);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to attach event to DHCPv4 client: %m");
+
+ r = sd_dhcp_client_set_mac(link->dhcp_client,
+ link->hw_addr.bytes,
+ link->bcast_addr.length > 0 ? link->bcast_addr.bytes : NULL,
+ link->hw_addr.length, link->iftype);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set MAC address: %m");
+
+ r = sd_dhcp_client_set_ifindex(link->dhcp_client, link->ifindex);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set ifindex: %m");
+
+ r = sd_dhcp_client_set_callback(link->dhcp_client, dhcp4_handler, link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set callback: %m");
+
+ r = sd_dhcp_client_set_request_broadcast(link->dhcp_client, link_needs_dhcp_broadcast(link));
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for broadcast: %m");
+
+ if (link->mtu > 0) {
+ r = sd_dhcp_client_set_mtu(link->dhcp_client, link->mtu);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set MTU: %m");
+ }
+
+ if (!link->network->dhcp_anonymize) {
+ if (link->network->dhcp_use_mtu) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_MTU_INTERFACE);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for MTU: %m");
+ }
+
+ if (link->network->dhcp_use_routes) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_STATIC_ROUTE);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for static route: %m");
+
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for classless static route: %m");
+ }
+
+ if (link->network->dhcp_use_domains != DHCP_USE_DOMAINS_NO) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_DOMAIN_SEARCH);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for domain search list: %m");
+ }
+
+ if (link->network->dhcp_use_ntp) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_NTP_SERVER);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for NTP server: %m");
+ }
+
+ if (link->network->dhcp_use_sip) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_SIP_SERVER);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for SIP server: %m");
+ }
+
+ if (link->network->dhcp_use_timezone) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_TZDB_TIMEZONE);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for timezone: %m");
+ }
+
+ if (link->network->dhcp_use_6rd) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_6RD);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for 6rd: %m");
+ }
+
+ SET_FOREACH(request_options, link->network->dhcp_request_options) {
+ uint32_t option = PTR_TO_UINT32(request_options);
+
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, option);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for '%u': %m", option);
+ }
+
+ ORDERED_HASHMAP_FOREACH(send_option, link->network->dhcp_client_send_options) {
+ r = sd_dhcp_client_add_option(link->dhcp_client, send_option);
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set send option: %m");
+ }
+
+ ORDERED_HASHMAP_FOREACH(send_option, link->network->dhcp_client_send_vendor_options) {
+ r = sd_dhcp_client_add_vendor_option(link->dhcp_client, send_option);
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set send option: %m");
+ }
+
+ r = dhcp4_set_hostname(link);
+ if (r < 0)
+ return r;
+
+ if (link->network->dhcp_vendor_class_identifier) {
+ r = sd_dhcp_client_set_vendor_class_identifier(link->dhcp_client,
+ link->network->dhcp_vendor_class_identifier);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set vendor class identifier: %m");
+ }
+
+ if (link->network->dhcp_mudurl) {
+ r = sd_dhcp_client_set_mud_url(link->dhcp_client, link->network->dhcp_mudurl);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set MUD URL: %m");
+ }
+
+ if (link->network->dhcp_user_class) {
+ r = sd_dhcp_client_set_user_class(link->dhcp_client, link->network->dhcp_user_class);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set user class: %m");
+ }
+ }
+
+ if (link->network->dhcp_client_port > 0) {
+ r = sd_dhcp_client_set_client_port(link->dhcp_client, link->network->dhcp_client_port);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set listen port: %m");
+ }
+
+ if (link->network->dhcp_max_attempts > 0) {
+ r = sd_dhcp_client_set_max_attempts(link->dhcp_client, link->network->dhcp_max_attempts);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set max attempts: %m");
+ }
+
+ if (link->network->dhcp_ip_service_type >= 0) {
+ r = sd_dhcp_client_set_service_type(link->dhcp_client, link->network->dhcp_ip_service_type);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set IP service type: %m");
+ }
+
+ if (link->network->dhcp_fallback_lease_lifetime > 0) {
+ r = sd_dhcp_client_set_fallback_lease_lifetime(link->dhcp_client, link->network->dhcp_fallback_lease_lifetime);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed set to lease lifetime: %m");
+ }
+
+ r = dhcp4_set_request_address(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set initial DHCPv4 address: %m");
+
+ return dhcp4_set_client_identifier(link);
+}
+
+int dhcp4_update_mac(Link *link) {
+ bool restart;
+ int r;
+
+ assert(link);
+
+ if (!link->dhcp_client)
+ return 0;
+
+ restart = sd_dhcp_client_is_running(link->dhcp_client);
+
+ r = sd_dhcp_client_stop(link->dhcp_client);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_client_set_mac(link->dhcp_client,
+ link->hw_addr.bytes,
+ link->bcast_addr.length > 0 ? link->bcast_addr.bytes : NULL,
+ link->hw_addr.length, link->iftype);
+ if (r < 0)
+ return r;
+
+ r = dhcp4_set_client_identifier(link);
+ if (r < 0)
+ return r;
+
+ if (restart) {
+ r = sd_dhcp_client_start(link->dhcp_client);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int dhcp4_start(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link->dhcp_client)
+ return 0;
+
+ if (!link_has_carrier(link))
+ return 0;
+
+ if (sd_dhcp_client_is_running(link->dhcp_client) > 0)
+ return 0;
+
+ r = sd_dhcp_client_start(link->dhcp_client);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int dhcp4_configure_duid(Link *link) {
+ assert(link);
+
+ if (!IN_SET(link->network->dhcp_client_identifier, DHCP_CLIENT_ID_DUID, DHCP_CLIENT_ID_DUID_ONLY))
+ return 1;
+
+ return dhcp_configure_duid(link, link_get_dhcp4_duid(link));
+}
+
+static int dhcp4_process_request(Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(link);
+
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false))
+ return 0;
+
+ r = dhcp4_configure_duid(link);
+ if (r <= 0)
+ return r;
+
+ r = dhcp4_configure(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure DHCPv4 client: %m");
+
+ r = dhcp4_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start DHCPv4 client: %m");
+
+ log_link_debug(link, "DHCPv4 client is configured%s.",
+ r > 0 ? ", acquiring DHCPv4 lease" : "");
+ return 1;
+}
+
+int link_request_dhcp4_client(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link_dhcp4_enabled(link))
+ return 0;
+
+ if (link->dhcp_client)
+ return 0;
+
+ r = link_queue_request(link, REQUEST_TYPE_DHCP4_CLIENT, dhcp4_process_request, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request configuring of the DHCPv4 client: %m");
+
+ log_link_debug(link, "Requested configuring of the DHCPv4 client.");
+ return 0;
+}
+
+int config_parse_dhcp_max_attempts(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(data);
+ uint64_t a;
+ int r;
+
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->dhcp_max_attempts = 0;
+ return 0;
+ }
+
+ if (streq(rvalue, "infinity")) {
+ network->dhcp_max_attempts = UINT64_MAX;
+ return 0;
+ }
+
+ r = safe_atou64(rvalue, &a);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP maximum attempts, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (a == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "%s= must be positive integer or 'infinity', ignoring: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ network->dhcp_max_attempts = a;
+
+ return 0;
+}
+
+int config_parse_dhcp_ip_service_type(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ int *tos = ASSERT_PTR(data);
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue))
+ *tos = -1; /* use sd_dhcp_client's default (currently, CS6). */
+ else if (streq(rvalue, "none"))
+ *tos = 0;
+ else if (streq(rvalue, "CS4"))
+ *tos = IPTOS_CLASS_CS4;
+ else if (streq(rvalue, "CS6"))
+ *tos = IPTOS_CLASS_CS6;
+ else
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+
+ return 0;
+}
+
+int config_parse_dhcp_fallback_lease_lifetime(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ network->dhcp_fallback_lease_lifetime = 0;
+ return 0;
+ }
+
+ /* We accept only "forever" or "infinity". */
+ if (!STR_IN_SET(rvalue, "forever", "infinity")) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid LeaseLifetime= value, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ network->dhcp_fallback_lease_lifetime = UINT32_MAX;
+
+ return 0;
+}
+
+int config_parse_dhcp_label(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ char **label = ASSERT_PTR(data);
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *label = mfree(*label);
+ return 0;
+ }
+
+ if (!address_label_valid(rvalue)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Address label is too long or invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ return free_and_strdup_warn(label, rvalue);
+}
+
+static const char* const dhcp_client_identifier_table[_DHCP_CLIENT_ID_MAX] = {
+ [DHCP_CLIENT_ID_MAC] = "mac",
+ [DHCP_CLIENT_ID_DUID] = "duid",
+ [DHCP_CLIENT_ID_DUID_ONLY] = "duid-only",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(dhcp_client_identifier, DHCPClientIdentifier);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_dhcp_client_identifier, dhcp_client_identifier, DHCPClientIdentifier,
+ "Failed to parse client identifier type");
diff --git a/src/network/networkd-dhcp4.h b/src/network/networkd-dhcp4.h
new file mode 100644
index 0000000..1d30cd1
--- /dev/null
+++ b/src/network/networkd-dhcp4.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+typedef enum DHCPClientIdentifier {
+ DHCP_CLIENT_ID_MAC,
+ DHCP_CLIENT_ID_DUID,
+ /* The following option may not be good for RFC regarding DHCP (3315 and 4361).
+ * But some setups require this. E.g., Sky Broadband, the second largest provider in the UK
+ * requires the client id to be set to a custom string, reported at
+ * https://github.com/systemd/systemd/issues/7828 */
+ DHCP_CLIENT_ID_DUID_ONLY,
+ _DHCP_CLIENT_ID_MAX,
+ _DHCP_CLIENT_ID_INVALID = -EINVAL,
+} DHCPClientIdentifier;
+
+void network_adjust_dhcp4(Network *network);
+int dhcp4_update_mac(Link *link);
+int dhcp4_start(Link *link);
+int dhcp4_lease_lost(Link *link);
+int dhcp4_check_ready(Link *link);
+
+int link_request_dhcp4_client(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_client_identifier);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_max_attempts);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_ip_service_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_mud_url);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_fallback_lease_lifetime);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_label);
diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c
new file mode 100644
index 0000000..259bb12
--- /dev/null
+++ b/src/network/networkd-dhcp6.c
@@ -0,0 +1,859 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2014 Intel Corporation. All rights reserved.
+***/
+
+#include "sd-dhcp6-client.h"
+
+#include "hashmap.h"
+#include "hostname-setup.h"
+#include "hostname-util.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-prefix-delegation.h"
+#include "networkd-dhcp6.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-queue.h"
+#include "networkd-route.h"
+#include "string-table.h"
+#include "string-util.h"
+
+bool link_dhcp6_with_address_enabled(Link *link) {
+ if (!link_dhcp6_enabled(link))
+ return false;
+
+ return link->network->dhcp6_use_address;
+}
+
+static DHCP6ClientStartMode link_get_dhcp6_client_start_mode(Link *link) {
+ assert(link);
+
+ if (!link->network)
+ return DHCP6_CLIENT_START_MODE_NO;
+
+ /* When WithoutRA= is explicitly specified, then honor it. */
+ if (link->network->dhcp6_client_start_mode >= 0)
+ return link->network->dhcp6_client_start_mode;
+
+ /* When this interface itself is an uplink interface, then start dhcp6 client in solicit mode. */
+ if (dhcp_pd_is_uplink(link, link, /* accept_auto = */ false))
+ return DHCP6_CLIENT_START_MODE_SOLICIT;
+
+ /* Otherwise, start dhcp6 client when RA is received. */
+ return DHCP6_CLIENT_START_MODE_NO;
+}
+
+static int dhcp6_remove(Link *link, bool only_marked) {
+ Address *address;
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+
+ if (!only_marked)
+ link->dhcp6_configured = false;
+
+ SET_FOREACH(route, link->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_DHCP6)
+ continue;
+ if (only_marked && !route_is_marked(route))
+ continue;
+
+ k = route_remove(route);
+ if (k < 0)
+ r = k;
+
+ route_cancel_request(route, link);
+ }
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_DHCP6)
+ continue;
+ if (only_marked && !address_is_marked(address))
+ continue;
+
+ k = address_remove(address);
+ if (k < 0)
+ r = k;
+
+ address_cancel_request(address);
+ }
+
+ return r;
+}
+
+static int dhcp6_address_ready_callback(Address *address) {
+ Address *a;
+
+ assert(address);
+ assert(address->link);
+
+ SET_FOREACH(a, address->link->addresses)
+ if (a->source == NETWORK_CONFIG_SOURCE_DHCP6)
+ a->callback = NULL;
+
+ return dhcp6_check_ready(address->link);
+}
+
+int dhcp6_check_ready(Link *link) {
+ bool has_ready = false;
+ Address *address;
+ int r;
+
+ assert(link);
+
+ if (link->dhcp6_messages > 0) {
+ log_link_debug(link, "%s(): DHCPv6 addresses and routes are not set.", __func__);
+ return 0;
+ }
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_DHCP6)
+ continue;
+ if (address_is_ready(address)) {
+ has_ready = true;
+ break;
+ }
+ }
+
+ if (!has_ready) {
+ SET_FOREACH(address, link->addresses)
+ if (address->source == NETWORK_CONFIG_SOURCE_DHCP6)
+ address->callback = dhcp6_address_ready_callback;
+
+ log_link_debug(link, "%s(): no DHCPv6 address is ready.", __func__);
+ return 0;
+ }
+
+ link->dhcp6_configured = true;
+ log_link_debug(link, "DHCPv6 addresses and routes set.");
+
+ r = dhcp6_remove(link, /* only_marked = */ true);
+ if (r < 0)
+ return r;
+
+ link_check_ready(link);
+ return 0;
+}
+
+static int dhcp6_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) {
+ int r;
+
+ assert(link);
+
+ r = address_configure_handler_internal(rtnl, m, link, "Could not set DHCPv6 address");
+ if (r <= 0)
+ return r;
+
+ r = dhcp6_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int verify_dhcp6_address(Link *link, const Address *address) {
+ bool by_ndisc = false;
+ Address *existing;
+ int log_level;
+
+ assert(link);
+ assert(address);
+ assert(address->family == AF_INET6);
+
+ const char *pretty = IN6_ADDR_TO_STRING(&address->in_addr.in6);
+
+ if (address_get(link, address, &existing) < 0) {
+ /* New address. */
+ log_level = LOG_INFO;
+ goto simple_log;
+ } else
+ log_level = LOG_DEBUG;
+
+ if (address->prefixlen == existing->prefixlen)
+ /* Currently, only conflict in prefix length is reported. */
+ goto simple_log;
+
+ if (existing->source == NETWORK_CONFIG_SOURCE_NDISC)
+ by_ndisc = true;
+
+ log_link_warning(link, "Ignoring DHCPv6 address %s/%u (valid %s, preferred %s) which conflicts with %s/%u%s.",
+ pretty, address->prefixlen,
+ FORMAT_LIFETIME(address->lifetime_valid_usec),
+ FORMAT_LIFETIME(address->lifetime_preferred_usec),
+ pretty, existing->prefixlen,
+ by_ndisc ? " assigned by NDisc" : "");
+ if (by_ndisc)
+ log_link_warning(link, "Hint: use IPv6Token= setting to change the address generated by NDisc or set UseAutonomousPrefix=no.");
+
+ return -EEXIST;
+
+simple_log:
+ log_link_full(link, log_level, "DHCPv6 address %s/%u (valid %s, preferred %s)",
+ pretty, address->prefixlen,
+ FORMAT_LIFETIME(address->lifetime_valid_usec),
+ FORMAT_LIFETIME(address->lifetime_preferred_usec));
+ return 0;
+}
+
+static int dhcp6_request_address(
+ Link *link,
+ const struct in6_addr *server_address,
+ const struct in6_addr *ip6_addr,
+ usec_t lifetime_preferred_usec,
+ usec_t lifetime_valid_usec) {
+
+ _cleanup_(address_freep) Address *addr = NULL;
+ Address *existing;
+ int r;
+
+ r = address_new(&addr);
+ if (r < 0)
+ return log_oom();
+
+ addr->source = NETWORK_CONFIG_SOURCE_DHCP6;
+ addr->provider.in6 = *server_address;
+ addr->family = AF_INET6;
+ addr->in_addr.in6 = *ip6_addr;
+ addr->flags = IFA_F_NOPREFIXROUTE;
+ addr->prefixlen = 128;
+ addr->lifetime_preferred_usec = lifetime_preferred_usec;
+ addr->lifetime_valid_usec = lifetime_valid_usec;
+
+ if (verify_dhcp6_address(link, addr) < 0)
+ return 0;
+
+ r = free_and_strdup_warn(&addr->netlabel, link->network->dhcp6_netlabel);
+ if (r < 0)
+ return r;
+
+ if (address_get(link, addr, &existing) < 0)
+ link->dhcp6_configured = false;
+ else
+ address_unmark(existing);
+
+ r = link_request_address(link, TAKE_PTR(addr), true, &link->dhcp6_messages,
+ dhcp6_address_handler, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request DHCPv6 address %s/128: %m",
+ IN6_ADDR_TO_STRING(ip6_addr));
+ return 0;
+}
+
+static int dhcp6_address_acquired(Link *link) {
+ struct in6_addr server_address;
+ usec_t timestamp_usec;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->dhcp6_lease);
+
+ if (!link->network->dhcp6_use_address)
+ return 0;
+
+ r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &server_address);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to get server address of DHCPv6 lease: %m");
+
+ r = sd_dhcp6_lease_get_timestamp(link->dhcp6_lease, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to get timestamp of DHCPv6 lease: %m");
+
+ for (sd_dhcp6_lease_reset_address_iter(link->dhcp6_lease);;) {
+ uint32_t lifetime_preferred_sec, lifetime_valid_sec;
+ struct in6_addr ip6_addr;
+
+ r = sd_dhcp6_lease_get_address(link->dhcp6_lease, &ip6_addr, &lifetime_preferred_sec, &lifetime_valid_sec);
+ if (r < 0)
+ break;
+
+ r = dhcp6_request_address(link, &server_address, &ip6_addr,
+ sec_to_usec(lifetime_preferred_sec, timestamp_usec),
+ sec_to_usec(lifetime_valid_sec, timestamp_usec));
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->dhcp6_use_hostname) {
+ const char *dhcpname = NULL;
+ _cleanup_free_ char *hostname = NULL;
+
+ (void) sd_dhcp6_lease_get_fqdn(link->dhcp6_lease, &dhcpname);
+
+ if (dhcpname) {
+ r = shorten_overlong(dhcpname, &hostname);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Unable to shorten overlong DHCP hostname '%s', ignoring: %m", dhcpname);
+ if (r == 1)
+ log_link_notice(link, "Overlong DHCP hostname received, shortened from '%s' to '%s'", dhcpname, hostname);
+ }
+ if (hostname) {
+ r = manager_set_hostname(link->manager, hostname);
+ if (r < 0)
+ log_link_error_errno(link, r, "Failed to set transient hostname to '%s': %m", hostname);
+ }
+ }
+
+ return 0;
+}
+
+static int dhcp6_lease_ip_acquired(sd_dhcp6_client *client, Link *link) {
+ _cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease_old = NULL;
+ sd_dhcp6_lease *lease;
+ int r;
+
+ link_mark_addresses(link, NETWORK_CONFIG_SOURCE_DHCP6);
+ link_mark_routes(link, NETWORK_CONFIG_SOURCE_DHCP6);
+
+ r = sd_dhcp6_client_get_lease(client, &lease);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get DHCPv6 lease: %m");
+
+ lease_old = TAKE_PTR(link->dhcp6_lease);
+ link->dhcp6_lease = sd_dhcp6_lease_ref(lease);
+
+ r = dhcp6_address_acquired(link);
+ if (r < 0)
+ return r;
+
+ if (dhcp6_lease_has_pd_prefix(lease)) {
+ r = dhcp6_pd_prefix_acquired(link);
+ if (r < 0)
+ return r;
+ } else if (dhcp6_lease_has_pd_prefix(lease_old))
+ /* When we had PD prefixes but not now, we need to remove them. */
+ dhcp_pd_prefix_lost(link);
+
+ if (link->dhcp6_messages == 0) {
+ link->dhcp6_configured = true;
+
+ r = dhcp6_remove(link, /* only_marked = */ true);
+ if (r < 0)
+ return r;
+ } else
+ log_link_debug(link, "Setting DHCPv6 addresses and routes");
+
+ if (!link->dhcp6_configured)
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ link_check_ready(link);
+ return 0;
+}
+
+static int dhcp6_lease_information_acquired(sd_dhcp6_client *client, Link *link) {
+ return 0;
+}
+
+static int dhcp6_lease_lost(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ log_link_info(link, "DHCPv6 lease lost");
+
+ if (dhcp6_lease_has_pd_prefix(link->dhcp6_lease))
+ dhcp_pd_prefix_lost(link);
+
+ link->dhcp6_lease = sd_dhcp6_lease_unref(link->dhcp6_lease);
+
+ r = dhcp6_remove(link, /* only_marked = */ false);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static void dhcp6_handler(sd_dhcp6_client *client, int event, void *userdata) {
+ Link *link = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link->network);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return;
+
+ switch (event) {
+ case SD_DHCP6_CLIENT_EVENT_STOP:
+ case SD_DHCP6_CLIENT_EVENT_RESEND_EXPIRE:
+ case SD_DHCP6_CLIENT_EVENT_RETRANS_MAX:
+ r = dhcp6_lease_lost(link);
+ if (r < 0)
+ link_enter_failed(link);
+ break;
+
+ case SD_DHCP6_CLIENT_EVENT_IP_ACQUIRE:
+ r = dhcp6_lease_ip_acquired(client, link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return;
+ }
+
+ _fallthrough_;
+ case SD_DHCP6_CLIENT_EVENT_INFORMATION_REQUEST:
+ r = dhcp6_lease_information_acquired(client, link);
+ if (r < 0)
+ link_enter_failed(link);
+ break;
+
+ default:
+ if (event < 0)
+ log_link_warning_errno(link, event, "DHCPv6 error: %m");
+ else
+ log_link_warning(link, "DHCPv6 unknown event: %d", event);
+ return;
+ }
+}
+
+int dhcp6_start_on_ra(Link *link, bool information_request) {
+ int r;
+
+ assert(link);
+ assert(link->dhcp6_client);
+ assert(link->network);
+ assert(in6_addr_is_link_local(&link->ipv6ll_address));
+
+ if (link_get_dhcp6_client_start_mode(link) != DHCP6_CLIENT_START_MODE_NO)
+ /* When WithoutRA= is specified, then the DHCPv6 client should be already running in
+ * the requested mode. Hence, ignore the requests by RA. */
+ return 0;
+
+ r = sd_dhcp6_client_is_running(link->dhcp6_client);
+ if (r < 0)
+ return r;
+
+ if (r > 0) {
+ int inf_req;
+
+ r = sd_dhcp6_client_get_information_request(link->dhcp6_client, &inf_req);
+ if (r < 0)
+ return r;
+
+ if (inf_req == information_request)
+ /* The client is already running in the requested mode. */
+ return 0;
+
+ if (!inf_req) {
+ log_link_debug(link,
+ "The DHCPv6 client is already running in the managed mode, "
+ "refusing to start the client in the information requesting mode.");
+ return 0;
+ }
+
+ log_link_debug(link,
+ "The DHCPv6 client is running in the information requesting mode. "
+ "Restarting the client in the managed mode.");
+
+ r = sd_dhcp6_client_stop(link->dhcp6_client);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_dhcp6_client_set_local_address(link->dhcp6_client, &link->ipv6ll_address);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_dhcp6_client_set_information_request(link->dhcp6_client, information_request);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp6_client_start(link->dhcp6_client);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int dhcp6_start(Link *link) {
+ DHCP6ClientStartMode start_mode;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!link->dhcp6_client)
+ return 0;
+
+ if (!link_dhcp6_enabled(link))
+ return 0;
+
+ if (!link_has_carrier(link))
+ return 0;
+
+ if (sd_dhcp6_client_is_running(link->dhcp6_client) > 0)
+ return 0;
+
+ if (!in6_addr_is_link_local(&link->ipv6ll_address)) {
+ log_link_debug(link, "IPv6 link-local address is not set, delaying to start DHCPv6 client.");
+ return 0;
+ }
+
+ r = sd_dhcp6_client_set_local_address(link->dhcp6_client, &link->ipv6ll_address);
+ if (r < 0)
+ return r;
+
+ start_mode = link_get_dhcp6_client_start_mode(link);
+ if (start_mode == DHCP6_CLIENT_START_MODE_NO)
+ return 0;
+
+ r = sd_dhcp6_client_set_information_request(link->dhcp6_client,
+ start_mode == DHCP6_CLIENT_START_MODE_INFORMATION_REQUEST);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp6_client_start(link->dhcp6_client);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int dhcp6_set_hostname(sd_dhcp6_client *client, Link *link) {
+ _cleanup_free_ char *hostname = NULL;
+ const char *hn;
+ int r;
+
+ assert(link);
+
+ if (!link->network->dhcp_send_hostname)
+ hn = NULL;
+ else if (link->network->dhcp_hostname)
+ hn = link->network->dhcp_hostname;
+ else {
+ r = gethostname_strict(&hostname);
+ if (r < 0 && r != -ENXIO) /* ENXIO: no hostname set or hostname is "localhost" */
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to get hostname: %m");
+
+ hn = hostname;
+ }
+
+ r = sd_dhcp6_client_set_fqdn(client, hn);
+ if (r == -EINVAL && hostname)
+ /* Ignore error when the machine's hostname is not suitable to send in DHCP packet. */
+ log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set hostname from kernel hostname, ignoring: %m");
+ else if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set hostname: %m");
+
+ return 0;
+}
+
+static int dhcp6_set_identifier(Link *link, sd_dhcp6_client *client) {
+ const DUID *duid;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(client);
+
+ r = sd_dhcp6_client_set_mac(client, link->hw_addr.bytes, link->hw_addr.length, link->iftype);
+ if (r < 0)
+ return r;
+
+ if (link->network->dhcp6_iaid_set) {
+ r = sd_dhcp6_client_set_iaid(client, link->network->dhcp6_iaid);
+ if (r < 0)
+ return r;
+ }
+
+ duid = link_get_dhcp6_duid(link);
+ if (duid->type == DUID_TYPE_LLT && duid->raw_data_len == 0)
+ r = sd_dhcp6_client_set_duid_llt(client, duid->llt_time);
+ else
+ r = sd_dhcp6_client_set_duid(client,
+ duid->type,
+ duid->raw_data_len > 0 ? duid->raw_data : NULL,
+ duid->raw_data_len);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int dhcp6_configure(Link *link) {
+ _cleanup_(sd_dhcp6_client_unrefp) sd_dhcp6_client *client = NULL;
+ sd_dhcp6_option *vendor_option;
+ sd_dhcp6_option *send_option;
+ void *request_options;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->dhcp6_client)
+ return log_link_debug_errno(link, SYNTHETIC_ERRNO(EBUSY), "DHCPv6 client is already configured.");
+
+ r = sd_dhcp6_client_new(&client);
+ if (r == -ENOMEM)
+ return log_oom_debug();
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to create DHCPv6 client: %m");
+
+ r = sd_dhcp6_client_attach_event(client, link->manager->event, 0);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to attach event: %m");
+
+ r = dhcp6_set_identifier(link, client);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set identifier: %m");
+
+ ORDERED_HASHMAP_FOREACH(send_option, link->network->dhcp6_client_send_options) {
+ r = sd_dhcp6_client_add_option(client, send_option);
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set option: %m");
+ }
+
+ r = dhcp6_set_hostname(client, link);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp6_client_set_ifindex(client, link->ifindex);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set ifindex: %m");
+
+ if (link->network->dhcp6_mudurl) {
+ r = sd_dhcp6_client_set_request_mud_url(client, link->network->dhcp6_mudurl);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set MUD URL: %m");
+ }
+
+ if (link->network->dhcp6_use_dns) {
+ r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_DNS_SERVER);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request DNS servers: %m");
+ }
+
+ if (link->network->dhcp6_use_domains > 0) {
+ r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_DOMAIN);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request domains: %m");
+ }
+
+ if (link->network->dhcp6_use_ntp) {
+ r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NTP_SERVER);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request NTP servers: %m");
+
+ /* If the server does not provide NTP servers, then we fallback to use SNTP servers. */
+ r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_SNTP_SERVER);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request SNTP servers: %m");
+ }
+
+ SET_FOREACH(request_options, link->network->dhcp6_request_options) {
+ uint32_t option = PTR_TO_UINT32(request_options);
+
+ r = sd_dhcp6_client_set_request_option(client, option);
+ if (r == -EEXIST) {
+ log_link_debug(link, "DHCPv6 CLIENT: Failed to set request flag for '%u' already exists, ignoring.", option);
+ continue;
+ }
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set request flag for '%u': %m", option);
+ }
+
+ if (link->network->dhcp6_user_class) {
+ r = sd_dhcp6_client_set_request_user_class(client, link->network->dhcp6_user_class);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set user class: %m");
+ }
+
+ if (link->network->dhcp6_vendor_class) {
+ r = sd_dhcp6_client_set_request_vendor_class(client, link->network->dhcp6_vendor_class);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set vendor class: %m");
+ }
+
+ ORDERED_HASHMAP_FOREACH(vendor_option, link->network->dhcp6_client_send_vendor_options) {
+ r = sd_dhcp6_client_add_vendor_option(client, vendor_option);
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set vendor option: %m");
+ }
+
+ r = sd_dhcp6_client_set_callback(client, dhcp6_handler, link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set callback: %m");
+
+ r = sd_dhcp6_client_set_prefix_delegation(client, link->network->dhcp6_use_pd_prefix);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to %s requesting prefixes to be delegated: %m",
+ enable_disable(link->network->dhcp6_use_pd_prefix));
+
+ /* Even if UseAddress=no, we need to request IA_NA, as the dhcp6 client may be started in solicit mode. */
+ r = sd_dhcp6_client_set_address_request(client, link->network->dhcp6_use_pd_prefix ? link->network->dhcp6_use_address : true);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to %s requesting address: %m",
+ enable_disable(link->network->dhcp6_use_address));
+
+ if (link->network->dhcp6_pd_prefix_length > 0) {
+ r = sd_dhcp6_client_set_prefix_delegation_hint(client,
+ link->network->dhcp6_pd_prefix_length,
+ &link->network->dhcp6_pd_prefix_hint);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set prefix delegation hint: %m");
+ }
+
+ r = sd_dhcp6_client_set_rapid_commit(client, link->network->dhcp6_use_rapid_commit);
+ if (r < 0)
+ return log_link_debug_errno(link, r,
+ "DHCPv6 CLIENT: Failed to %s rapid commit: %m",
+ enable_disable(link->network->dhcp6_use_rapid_commit));
+
+ link->dhcp6_client = TAKE_PTR(client);
+
+ return 0;
+}
+
+int dhcp6_update_mac(Link *link) {
+ bool restart;
+ int r;
+
+ assert(link);
+
+ if (!link->dhcp6_client)
+ return 0;
+
+ restart = sd_dhcp6_client_is_running(link->dhcp6_client) > 0;
+
+ if (restart) {
+ r = sd_dhcp6_client_stop(link->dhcp6_client);
+ if (r < 0)
+ return r;
+ }
+
+ r = dhcp6_set_identifier(link, link->dhcp6_client);
+ if (r < 0)
+ return r;
+
+ if (restart) {
+ r = sd_dhcp6_client_start(link->dhcp6_client);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not restart DHCPv6 client: %m");
+ }
+
+ return 0;
+}
+
+static int dhcp6_process_request(Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(link);
+
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false))
+ return 0;
+
+ r = dhcp_configure_duid(link, link_get_dhcp6_duid(link));
+ if (r <= 0)
+ return r;
+
+ r = dhcp6_configure(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure DHCPv6 client: %m");
+
+ r = ndisc_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m");
+
+ r = dhcp6_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start DHCPv6 client: %m");
+
+ log_link_debug(link, "DHCPv6 client is configured%s.",
+ r > 0 ? ", acquiring DHCPv6 lease" : "");
+ return 1;
+}
+
+int link_request_dhcp6_client(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link_dhcp6_enabled(link) && !link_ipv6_accept_ra_enabled(link))
+ return 0;
+
+ if (link->dhcp6_client)
+ return 0;
+
+ r = link_queue_request(link, REQUEST_TYPE_DHCP6_CLIENT, dhcp6_process_request, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request configuring of the DHCPv6 client: %m");
+
+ log_link_debug(link, "Requested configuring of the DHCPv6 client.");
+ return 0;
+}
+
+int link_serialize_dhcp6_client(Link *link, FILE *f) {
+ _cleanup_free_ char *duid = NULL;
+ uint32_t iaid;
+ int r;
+
+ assert(link);
+
+ if (!link->dhcp6_client)
+ return 0;
+
+ r = sd_dhcp6_client_get_iaid(link->dhcp6_client, &iaid);
+ if (r >= 0)
+ fprintf(f, "DHCP6_CLIENT_IAID=0x%x\n", iaid);
+
+ r = sd_dhcp6_client_duid_as_string(link->dhcp6_client, &duid);
+ if (r >= 0)
+ fprintf(f, "DHCP6_CLIENT_DUID=%s\n", duid);
+
+ return 0;
+}
+
+int config_parse_dhcp6_pd_prefix_hint(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ union in_addr_union u;
+ unsigned char prefixlen;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = in_addr_prefix_from_string(rvalue, AF_INET6, &u, &prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=%s, ignoring assignment.", lvalue, rvalue);
+ return 0;
+ }
+
+ if (prefixlen < 1 || prefixlen > 128) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid prefix length in %s=%s, ignoring assignment.", lvalue, rvalue);
+ return 0;
+ }
+
+ network->dhcp6_pd_prefix_hint = u.in6;
+ network->dhcp6_pd_prefix_length = prefixlen;
+
+ return 0;
+}
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_dhcp6_client_start_mode, dhcp6_client_start_mode, DHCP6ClientStartMode,
+ "Failed to parse WithoutRA= setting");
+
+static const char* const dhcp6_client_start_mode_table[_DHCP6_CLIENT_START_MODE_MAX] = {
+ [DHCP6_CLIENT_START_MODE_NO] = "no",
+ [DHCP6_CLIENT_START_MODE_INFORMATION_REQUEST] = "information-request",
+ [DHCP6_CLIENT_START_MODE_SOLICIT] = "solicit",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(dhcp6_client_start_mode, DHCP6ClientStartMode);
diff --git a/src/network/networkd-dhcp6.h b/src/network/networkd-dhcp6.h
new file mode 100644
index 0000000..81267c2
--- /dev/null
+++ b/src/network/networkd-dhcp6.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "macro.h"
+
+typedef enum DHCP6ClientStartMode {
+ DHCP6_CLIENT_START_MODE_NO,
+ DHCP6_CLIENT_START_MODE_INFORMATION_REQUEST,
+ DHCP6_CLIENT_START_MODE_SOLICIT,
+ _DHCP6_CLIENT_START_MODE_MAX,
+ _DHCP6_CLIENT_START_MODE_INVALID = -EINVAL,
+} DHCP6ClientStartMode;
+
+typedef struct Link Link;
+
+bool link_dhcp6_with_address_enabled(Link *link);
+int dhcp6_check_ready(Link *link);
+int dhcp6_update_mac(Link *link);
+int dhcp6_start(Link *link);
+int dhcp6_start_on_ra(Link *link, bool information_request);
+
+int link_request_dhcp6_client(Link *link);
+
+int link_serialize_dhcp6_client(Link *link, FILE *f);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_pd_prefix_hint);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_mud_url);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_client_start_mode);
+
+const char* dhcp6_client_start_mode_to_string(DHCP6ClientStartMode i) _const_;
+DHCP6ClientStartMode dhcp6_client_start_mode_from_string(const char *s) _pure_;
diff --git a/src/network/networkd-gperf.gperf b/src/network/networkd-gperf.gperf
new file mode 100644
index 0000000..8ed90f0
--- /dev/null
+++ b/src/network/networkd-gperf.gperf
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "conf-parser.h"
+#include "networkd-conf.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-manager.h"
+#include "networkd-route-util.h"
+%}
+struct ConfigPerfItem;
+%null_strings
+%language=ANSI-C
+%define slot-name section_and_lvalue
+%define hash-function-name networkd_gperf_hash
+%define lookup-function-name networkd_gperf_lookup
+%readonly-tables
+%omit-struct-type
+%struct-type
+%includes
+%%
+Network.SpeedMeter, config_parse_bool, 0, offsetof(Manager, use_speed_meter)
+Network.SpeedMeterIntervalSec, config_parse_sec, 0, offsetof(Manager, speed_meter_interval_usec)
+Network.ManageForeignRoutingPolicyRules, config_parse_bool, 0, offsetof(Manager, manage_foreign_rules)
+Network.ManageForeignRoutes, config_parse_bool, 0, offsetof(Manager, manage_foreign_routes)
+Network.RouteTable, config_parse_route_table_names, 0, 0
+DHCPv4.DUIDType, config_parse_duid_type, 0, offsetof(Manager, dhcp_duid)
+DHCPv4.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Manager, dhcp_duid)
+DHCPv6.DUIDType, config_parse_duid_type, 0, offsetof(Manager, dhcp6_duid)
+DHCPv6.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Manager, dhcp6_duid)
+/* Deprecated */
+DHCP.DUIDType, config_parse_manager_duid_type, 0, 0
+DHCP.DUIDRawData, config_parse_manager_duid_rawdata, 0, 0
diff --git a/src/network/networkd-ipv4acd.c b/src/network/networkd-ipv4acd.c
new file mode 100644
index 0000000..877dee0
--- /dev/null
+++ b/src/network/networkd-ipv4acd.c
@@ -0,0 +1,327 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h> /* IFF_LOOPBACK */
+#include <net/if_arp.h> /* ARPHRD_ETHER */
+
+#include "sd-dhcp-client.h"
+#include "sd-ipv4acd.h"
+
+#include "ipvlan.h"
+#include "networkd-address.h"
+#include "networkd-dhcp4.h"
+#include "networkd-ipv4acd.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+
+bool link_ipv4acd_supported(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ /* ARPHRD_INFINIBAND seems to potentially support IPv4ACD.
+ * But currently sd-ipv4acd only supports ARPHRD_ETHER. */
+ if (link->iftype != ARPHRD_ETHER)
+ return false;
+
+ if (link->hw_addr.length != ETH_ALEN)
+ return false;
+
+ if (ether_addr_is_null(&link->hw_addr.ether))
+ return false;
+
+ if (streq_ptr(link->kind, "vrf"))
+ return false;
+
+ /* L3 or L3S mode do not support ARP. */
+ if (IN_SET(link_get_ipvlan_mode(link), NETDEV_IPVLAN_MODE_L3, NETDEV_IPVLAN_MODE_L3S))
+ return false;
+
+ return true;
+}
+
+static bool address_ipv4acd_enabled(Address *address) {
+ assert(address);
+ assert(address->link);
+
+ if (address->family != AF_INET)
+ return false;
+
+ if (!FLAGS_SET(address->duplicate_address_detection, ADDRESS_FAMILY_IPV4))
+ return false;
+
+ /* Currently, only static and DHCP4 addresses are supported. */
+ if (!IN_SET(address->source, NETWORK_CONFIG_SOURCE_STATIC, NETWORK_CONFIG_SOURCE_DHCP4))
+ return false;
+
+ if (!link_ipv4acd_supported(address->link))
+ return false;
+
+ return true;
+}
+
+bool ipv4acd_bound(const Address *address) {
+ assert(address);
+
+ if (!address->acd)
+ return true;
+
+ return address->acd_bound;
+}
+
+static int static_ipv4acd_address_remove(Link *link, Address *address, bool on_conflict) {
+ int r;
+
+ assert(link);
+ assert(address);
+
+ if (!address_exists(address))
+ return 0; /* Not assigned. */
+
+ if (on_conflict)
+ log_link_warning(link, "Dropping address "IPV4_ADDRESS_FMT_STR", as an address conflict was detected.",
+ IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
+ else
+ log_link_debug(link, "Removing address "IPV4_ADDRESS_FMT_STR", as the ACD client is stopped.",
+ IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
+
+ r = address_remove(address);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to remove address "IPV4_ADDRESS_FMT_STR": %m",
+ IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
+
+ return 0;
+}
+
+static int dhcp4_address_on_conflict(Link *link, Address *address) {
+ int r;
+
+ assert(link);
+ assert(link->dhcp_client);
+
+ r = sd_dhcp_client_send_decline(link->dhcp_client);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to send DHCP DECLINE, ignoring: %m");
+
+ if (!link->dhcp_lease)
+ /* Unlikely, but during probing the address, the lease may be lost. */
+ return 0;
+
+ log_link_warning(link, "Dropping DHCPv4 lease, as an address conflict was detected.");
+ r = dhcp4_lease_lost(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to drop DHCPv4 lease: %m");
+
+ /* It is not necessary to call address_remove() here, as dhcp4_lease_lost() removes it. */
+ return 0;
+}
+
+static void on_acd(sd_ipv4acd *acd, int event, void *userdata) {
+ Address *address = ASSERT_PTR(userdata);
+ Link *link;
+ int r;
+
+ assert(acd);
+ assert(address->acd == acd);
+ assert(address->link);
+ assert(address->family == AF_INET);
+ assert(IN_SET(address->source, NETWORK_CONFIG_SOURCE_STATIC, NETWORK_CONFIG_SOURCE_DHCP4));
+
+ link = address->link;
+
+ switch (event) {
+ case SD_IPV4ACD_EVENT_STOP:
+ address->acd_bound = false;
+
+ if (address->source == NETWORK_CONFIG_SOURCE_STATIC) {
+ r = static_ipv4acd_address_remove(link, address, /* on_conflict = */ false);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ /* We have nothing to do for DHCPv4 lease here, as the dhcp client is already stopped
+ * when stopping the ipv4acd client. See link_stop_engines(). */
+ break;
+
+ case SD_IPV4ACD_EVENT_BIND:
+ address->acd_bound = true;
+
+ log_link_debug(link, "Successfully claimed address "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
+ break;
+
+ case SD_IPV4ACD_EVENT_CONFLICT:
+ address->acd_bound = false;
+
+ log_link_warning(link, "Dropping address "IPV4_ADDRESS_FMT_STR", as an address conflict was detected.",
+ IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
+
+ if (address->source == NETWORK_CONFIG_SOURCE_STATIC)
+ r = static_ipv4acd_address_remove(link, address, /* on_conflict = */ true);
+ else
+ r = dhcp4_address_on_conflict(link, address);
+ if (r < 0)
+ link_enter_failed(link);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+}
+
+static int ipv4acd_check_mac(sd_ipv4acd *acd, const struct ether_addr *mac, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+ struct hw_addr_data hw_addr;
+
+ assert(mac);
+
+ hw_addr = (struct hw_addr_data) {
+ .length = ETH_ALEN,
+ .ether = *mac,
+ };
+
+ return link_get_by_hw_addr(m, &hw_addr, NULL) >= 0;
+}
+
+static int address_ipv4acd_start(Address *address) {
+ assert(address);
+ assert(address->link);
+
+ if (!address->acd)
+ return 0;
+
+ if (sd_ipv4acd_is_running(address->acd))
+ return 0;
+
+ if (!link_has_carrier(address->link))
+ return 0;
+
+ return sd_ipv4acd_start(address->acd, true);
+}
+
+int ipv4acd_configure(Address *address) {
+ Link *link;
+ int r;
+
+ assert(address);
+
+ link = ASSERT_PTR(address->link);
+
+ if (!address_ipv4acd_enabled(address)) {
+ address->acd = sd_ipv4acd_unref(address->acd);
+ address->acd_bound = false;
+ return 0;
+ }
+
+ if (address->acd)
+ return address_ipv4acd_start(address);
+
+ log_link_debug(link, "Configuring IPv4ACD for address "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
+
+ r = sd_ipv4acd_new(&address->acd);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_attach_event(address->acd, link->manager->event, 0);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_ifindex(address->acd, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_mac(address->acd, &link->hw_addr.ether);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_address(address->acd, &address->in_addr.in);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_callback(address->acd, on_acd, address);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_check_mac_callback(address->acd, ipv4acd_check_mac, link->manager);
+ if (r < 0)
+ return r;
+
+ return address_ipv4acd_start(address);
+}
+
+int ipv4acd_update_mac(Link *link) {
+ Address *address;
+ int k, r = 0;
+
+ assert(link);
+
+ if (link->hw_addr.length != ETH_ALEN)
+ return 0;
+ if (ether_addr_is_null(&link->hw_addr.ether))
+ return 0;
+
+ SET_FOREACH(address, link->addresses) {
+ if (!address->acd)
+ continue;
+
+ k = sd_ipv4acd_set_mac(address->acd, &link->hw_addr.ether);
+ if (k < 0)
+ r = k;
+ }
+ if (r < 0)
+ link_enter_failed(link);
+
+ return r;
+}
+
+int ipv4acd_start(Link *link) {
+ Address *address;
+ int r;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses) {
+ r = address_ipv4acd_start(address);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int ipv4acd_stop(Link *link) {
+ Address *address;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses) {
+ if (!address->acd)
+ continue;
+
+ k = sd_ipv4acd_stop(address->acd);
+ if (k < 0)
+ r = k;
+ }
+
+ return r;
+}
+
+int ipv4acd_set_ifname(Link *link) {
+ Address *address;
+ int r;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses) {
+ if (!address->acd)
+ continue;
+
+ r = sd_ipv4acd_set_ifname(address->acd, link->ifname);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
diff --git a/src/network/networkd-ipv4acd.h b/src/network/networkd-ipv4acd.h
new file mode 100644
index 0000000..1ec9481
--- /dev/null
+++ b/src/network/networkd-ipv4acd.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct Address Address;
+typedef struct Link Link;
+
+bool link_ipv4acd_supported(Link *link);
+bool ipv4acd_bound(const Address *address);
+int ipv4acd_configure(Address *address);
+int ipv4acd_update_mac(Link *link);
+int ipv4acd_start(Link *link);
+int ipv4acd_stop(Link *link);
+int ipv4acd_set_ifname(Link *link);
diff --git a/src/network/networkd-ipv4ll.c b/src/network/networkd-ipv4ll.c
new file mode 100644
index 0000000..8450b34
--- /dev/null
+++ b/src/network/networkd-ipv4ll.c
@@ -0,0 +1,320 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if.h>
+
+#include "netif-util.h"
+#include "networkd-address.h"
+#include "networkd-ipv4acd.h"
+#include "networkd-ipv4ll.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-queue.h"
+#include "parse-util.h"
+
+bool link_ipv4ll_enabled(Link *link) {
+ assert(link);
+
+ if (!link_ipv4acd_supported(link))
+ return false;
+
+ if (!link->network)
+ return false;
+
+ if (link->network->bond)
+ return false;
+
+ return link->network->link_local & ADDRESS_FAMILY_IPV4;
+}
+
+static int address_new_from_ipv4ll(Link *link, Address **ret) {
+ _cleanup_(address_freep) Address *address = NULL;
+ struct in_addr addr;
+ int r;
+
+ assert(link);
+ assert(link->ipv4ll);
+ assert(ret);
+
+ r = sd_ipv4ll_get_address(link->ipv4ll, &addr);
+ if (r < 0)
+ return r;
+
+ r = address_new(&address);
+ if (r < 0)
+ return -ENOMEM;
+
+ address->source = NETWORK_CONFIG_SOURCE_IPV4LL;
+ address->family = AF_INET;
+ address->in_addr.in = addr;
+ address->prefixlen = 16;
+ address->scope = RT_SCOPE_LINK;
+ address->route_metric = IPV4LL_ROUTE_METRIC;
+ address_set_broadcast(address, link);
+
+ *ret = TAKE_PTR(address);
+ return 0;
+}
+
+static int ipv4ll_address_lost(Link *link) {
+ _cleanup_(address_freep) Address *address = NULL;
+ Address *existing;
+ int r;
+
+ assert(link);
+
+ link->ipv4ll_address_configured = false;
+
+ r = address_new_from_ipv4ll(link, &address);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ if (address_get(link, address, &existing) < 0)
+ return 0;
+
+ if (existing->source != NETWORK_CONFIG_SOURCE_IPV4LL)
+ return 0;
+
+ if (!address_exists(existing))
+ return 0;
+
+ log_link_debug(link, "IPv4 link-local release "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
+
+ return address_remove(existing);
+}
+
+static int ipv4ll_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) {
+ int r;
+
+ assert(link);
+ assert(!link->ipv4ll_address_configured);
+
+ r = address_configure_handler_internal(rtnl, m, link, "Could not set ipv4ll address");
+ if (r <= 0)
+ return r;
+
+ link->ipv4ll_address_configured = true;
+ link_check_ready(link);
+
+ return 1;
+}
+
+static int ipv4ll_address_claimed(sd_ipv4ll *ll, Link *link) {
+ _cleanup_(address_freep) Address *address = NULL;
+ int r;
+
+ assert(ll);
+ assert(link);
+
+ link->ipv4ll_address_configured = false;
+
+ r = address_new_from_ipv4ll(link, &address);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ log_link_debug(link, "IPv4 link-local claim "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
+
+ return link_request_address(link, TAKE_PTR(address), true, NULL, ipv4ll_address_handler, NULL);
+}
+
+static void ipv4ll_handler(sd_ipv4ll *ll, int event, void *userdata) {
+ Link *link = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link->network);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return;
+
+ switch (event) {
+ case SD_IPV4LL_EVENT_STOP:
+ r = ipv4ll_address_lost(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return;
+ }
+ break;
+ case SD_IPV4LL_EVENT_CONFLICT:
+ r = ipv4ll_address_lost(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return;
+ }
+
+ r = sd_ipv4ll_restart(ll);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m");
+ link_enter_failed(link);
+ }
+ break;
+ case SD_IPV4LL_EVENT_BIND:
+ r = ipv4ll_address_claimed(ll, link);
+ if (r < 0) {
+ log_link_error(link, "Failed to configure ipv4ll address: %m");
+ link_enter_failed(link);
+ return;
+ }
+ break;
+ default:
+ log_link_warning(link, "IPv4 link-local unknown event: %d", event);
+ break;
+ }
+}
+
+static int ipv4ll_check_mac(sd_ipv4ll *ll, const struct ether_addr *mac, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+ struct hw_addr_data hw_addr;
+
+ assert(mac);
+
+ hw_addr = (struct hw_addr_data) {
+ .length = ETH_ALEN,
+ .ether = *mac,
+ };
+
+ return link_get_by_hw_addr(m, &hw_addr, NULL) >= 0;
+}
+
+int ipv4ll_configure(Link *link) {
+ uint64_t seed;
+ int r;
+
+ assert(link);
+
+ if (!link_ipv4ll_enabled(link))
+ return 0;
+
+ if (link->ipv4ll)
+ return -EBUSY;
+
+ r = sd_ipv4ll_new(&link->ipv4ll);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4ll_attach_event(link->ipv4ll, link->manager->event, 0);
+ if (r < 0)
+ return r;
+
+ if (link->dev &&
+ net_get_unique_predictable_data(link->dev, true, &seed) >= 0) {
+ r = sd_ipv4ll_set_address_seed(link->ipv4ll, seed);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_ipv4ll_set_mac(link->ipv4ll, &link->hw_addr.ether);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4ll_set_ifindex(link->ipv4ll, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4ll_set_callback(link->ipv4ll, ipv4ll_handler, link);
+ if (r < 0)
+ return r;
+
+ return sd_ipv4ll_set_check_mac_callback(link->ipv4ll, ipv4ll_check_mac, link->manager);
+}
+
+int ipv4ll_update_mac(Link *link) {
+ assert(link);
+
+ if (link->hw_addr.length != ETH_ALEN)
+ return 0;
+ if (ether_addr_is_null(&link->hw_addr.ether))
+ return 0;
+ if (!link->ipv4ll)
+ return 0;
+
+ return sd_ipv4ll_set_mac(link->ipv4ll, &link->hw_addr.ether);
+}
+
+int config_parse_ipv4ll(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ AddressFamily *link_local = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ /* Note that this is mostly like
+ * config_parse_address_family(), except that it
+ * applies only to IPv4 */
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=%s, ignoring assignment. "
+ "Note that the setting %s= is deprecated, please use LinkLocalAddressing= instead.",
+ lvalue, rvalue, lvalue);
+ return 0;
+ }
+
+ SET_FLAG(*link_local, ADDRESS_FAMILY_IPV4, r);
+
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "%s=%s is deprecated, please use LinkLocalAddressing=%s instead.",
+ lvalue, rvalue, address_family_to_string(*link_local));
+
+ return 0;
+}
+
+int config_parse_ipv4ll_address(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ union in_addr_union a;
+ struct in_addr *ipv4ll_address = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *ipv4ll_address = (struct in_addr) {};
+ return 0;
+ }
+
+ r = in_addr_from_string(AF_INET, rvalue, &a);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+ if (!in4_addr_is_link_local_dynamic(&a.in)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified address cannot be used as an IPv4 link local address, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ *ipv4ll_address = a.in;
+ return 0;
+}
diff --git a/src/network/networkd-ipv4ll.h b/src/network/networkd-ipv4ll.h
new file mode 100644
index 0000000..fa53bd2
--- /dev/null
+++ b/src/network/networkd-ipv4ll.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+
+#define IPV4LL_ROUTE_METRIC 2048
+
+typedef struct Link Link;
+
+bool link_ipv4ll_enabled(Link *link);
+
+int ipv4ll_configure(Link *link);
+int ipv4ll_update_mac(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv4ll);
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv4ll_address);
diff --git a/src/network/networkd-ipv6-proxy-ndp.c b/src/network/networkd-ipv6-proxy-ndp.c
new file mode 100644
index 0000000..edd369a
--- /dev/null
+++ b/src/network/networkd-ipv6-proxy-ndp.c
@@ -0,0 +1,180 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if.h>
+
+#include "netlink-util.h"
+#include "networkd-ipv6-proxy-ndp.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "socket-util.h"
+#include "string-util.h"
+
+void network_adjust_ipv6_proxy_ndp(Network *network) {
+ assert(network);
+
+ if (set_isempty(network->ipv6_proxy_ndp_addresses))
+ return;
+
+ if (!socket_ipv6_is_supported()) {
+ log_once(LOG_WARNING,
+ "%s: IPv6 proxy NDP addresses are set, but IPv6 is not supported by kernel, "
+ "Ignoring IPv6 proxy NDP addresses.", network->filename);
+ network->ipv6_proxy_ndp_addresses = set_free_free(network->ipv6_proxy_ndp_addresses);
+ }
+}
+
+static int ipv6_proxy_ndp_address_configure_handler(
+ sd_netlink *rtnl,
+ sd_netlink_message *m,
+ Request *req,
+ Link *link,
+ struct in6_addr *address) {
+
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ log_link_message_warning_errno(link, m, r, "Could not add IPv6 proxy ndp address entry, ignoring");
+
+ if (link->static_ipv6_proxy_ndp_messages == 0) {
+ log_link_debug(link, "IPv6 proxy NDP addresses set.");
+ link->static_ipv6_proxy_ndp_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+/* send a request to the kernel to add an IPv6 Proxy entry to the neighbour table */
+static int ipv6_proxy_ndp_address_configure(const struct in6_addr *address, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(address);
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(req);
+
+ /* create new netlink message */
+ r = sd_rtnl_message_new_neigh(link->manager->rtnl, &m, RTM_NEWNEIGH, link->ifindex, AF_INET6);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_neigh_set_flags(m, NTF_PROXY);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in6_addr(m, NDA_DST, address);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static int ipv6_proxy_ndp_address_process_request(Request *req, Link *link, struct in6_addr *address) {
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(address);
+
+ if (!link_is_ready_to_configure(link, false))
+ return 0;
+
+ r = ipv6_proxy_ndp_address_configure(address, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure IPv6 proxy NDP address: %m");
+
+ return 1;
+}
+
+int link_request_static_ipv6_proxy_ndp_addresses(Link *link) {
+ struct in6_addr *address;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->static_ipv6_proxy_ndp_configured = false;
+
+ SET_FOREACH(address, link->network->ipv6_proxy_ndp_addresses) {
+ r = link_queue_request_safe(link, REQUEST_TYPE_IPV6_PROXY_NDP,
+ address, NULL,
+ in6_addr_hash_func,
+ in6_addr_compare_func,
+ ipv6_proxy_ndp_address_process_request,
+ &link->static_ipv6_proxy_ndp_messages,
+ ipv6_proxy_ndp_address_configure_handler,
+ NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request IPv6 proxy NDP address: %m");
+ }
+
+ if (link->static_ipv6_proxy_ndp_messages == 0) {
+ link->static_ipv6_proxy_ndp_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Setting IPv6 proxy NDP addresses.");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+int config_parse_ipv6_proxy_ndp_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ struct in6_addr *address = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ union in_addr_union buffer;
+ int r;
+
+ assert(filename);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->ipv6_proxy_ndp_addresses = set_free_free(network->ipv6_proxy_ndp_addresses);
+ return 0;
+ }
+
+ r = in_addr_from_string(AF_INET6, rvalue, &buffer);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse IPv6 proxy NDP address, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (in_addr_is_null(AF_INET6, &buffer)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "IPv6 proxy NDP address cannot be the ANY address, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ address = newdup(struct in6_addr, &buffer.in6, 1);
+ if (!address)
+ return log_oom();
+
+ r = set_ensure_put(&network->ipv6_proxy_ndp_addresses, &in6_addr_hash_ops, address);
+ if (r < 0)
+ return log_oom();
+ if (r > 0)
+ TAKE_PTR(address);
+
+ return 0;
+}
diff --git a/src/network/networkd-ipv6-proxy-ndp.h b/src/network/networkd-ipv6-proxy-ndp.h
new file mode 100644
index 0000000..e57d28f
--- /dev/null
+++ b/src/network/networkd-ipv6-proxy-ndp.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+void network_adjust_ipv6_proxy_ndp(Network *network);
+
+int link_request_static_ipv6_proxy_ndp_addresses(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_proxy_ndp_address);
diff --git a/src/network/networkd-ipv6ll.c b/src/network/networkd-ipv6ll.c
new file mode 100644
index 0000000..32229a3
--- /dev/null
+++ b/src/network/networkd-ipv6ll.c
@@ -0,0 +1,247 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/if.h>
+#include <linux/if_arp.h>
+
+#include "in-addr-util.h"
+#include "networkd-address.h"
+#include "networkd-ipv6ll.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "networkd-util.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "sysctl-util.h"
+
+bool link_ipv6ll_enabled(Link *link) {
+ assert(link);
+
+ if (!socket_ipv6_is_supported())
+ return false;
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ if (link->iftype == ARPHRD_CAN)
+ return false;
+
+ if (STRPTR_IN_SET(link->kind, "vrf", "wireguard", "ipip", "gre", "sit", "vti", "nlmon"))
+ return false;
+
+ if (link->network->bond)
+ return false;
+
+ return link->network->link_local & ADDRESS_FAMILY_IPV6;
+}
+
+bool link_may_have_ipv6ll(Link *link, bool check_multicast) {
+ assert(link);
+
+ /*
+ * This is equivalent to link_ipv6ll_enabled() for non-WireGuard interfaces.
+ *
+ * For WireGuard interface, the kernel does not assign any IPv6LL addresses, but we can assign
+ * it manually. It is necessary to set an IPv6LL address manually to run NDisc or RADV on
+ * WireGuard interface. Note, also Multicast=yes must be set. See #17380.
+ *
+ * TODO: May be better to introduce GenerateIPv6LinkLocalAddress= setting, and use algorithms
+ * used in networkd-address-generation.c
+ */
+
+ if (link_ipv6ll_enabled(link))
+ return true;
+
+ /* IPv6LL address can be manually assigned on WireGuard interface. */
+ if (streq_ptr(link->kind, "wireguard")) {
+ Address *a;
+
+ if (!link->network)
+ return false;
+
+ if (check_multicast && !FLAGS_SET(link->flags, IFF_MULTICAST) && link->network->multicast <= 0)
+ return false;
+
+ ORDERED_HASHMAP_FOREACH(a, link->network->addresses_by_section) {
+ if (a->family != AF_INET6)
+ continue;
+ if (in6_addr_is_set(&a->in_addr_peer.in6))
+ continue;
+ if (in6_addr_is_link_local(&a->in_addr.in6))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+IPv6LinkLocalAddressGenMode link_get_ipv6ll_addrgen_mode(Link *link) {
+ assert(link);
+
+ if (!link_ipv6ll_enabled(link))
+ return IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE;
+
+ if (link->network->ipv6ll_address_gen_mode >= 0)
+ return link->network->ipv6ll_address_gen_mode;
+
+ if (in6_addr_is_set(&link->network->ipv6ll_stable_secret))
+ return IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY;
+
+ return IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_EUI64;
+}
+
+int ipv6ll_addrgen_mode_fill_message(sd_netlink_message *message, IPv6LinkLocalAddressGenMode mode) {
+ int r;
+
+ assert(message);
+ assert(mode >= 0 && mode < _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX);
+
+ r = sd_netlink_message_open_container(message, IFLA_AF_SPEC);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container(message, AF_INET6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(message, IFLA_INET6_ADDR_GEN_MODE, mode);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(message);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(message);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int link_update_ipv6ll_addrgen_mode(Link *link, sd_netlink_message *message) {
+ uint8_t mode;
+ int family, r;
+
+ assert(link);
+ assert(message);
+
+ r = sd_rtnl_message_get_family(message, &family);
+ if (r < 0)
+ return r;
+
+ if (family != AF_UNSPEC)
+ return 0;
+
+ r = sd_netlink_message_enter_container(message, IFLA_AF_SPEC);
+ if (r == -ENODATA)
+ return 0;
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_enter_container(message, AF_INET6);
+ if (r == -ENODATA)
+ return sd_netlink_message_exit_container(message);
+ if (r < 0)
+ return r;
+
+ mode = (uint8_t) link->ipv6ll_address_gen_mode;
+ r = sd_netlink_message_read_u8(message, IFLA_INET6_ADDR_GEN_MODE, &mode);
+ if (r < 0 && r != -ENODATA)
+ return r;
+
+ r = sd_netlink_message_exit_container(message);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_exit_container(message);
+ if (r < 0)
+ return r;
+
+ if (mode == (uint8_t) link->ipv6ll_address_gen_mode)
+ return 0;
+
+ if (mode >= _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX) {
+ log_link_debug(link, "Received invalid IPv6 link-local address generation mode (%u), ignoring.", mode);
+ return 0;
+ }
+
+ if (link->ipv6ll_address_gen_mode < 0)
+ log_link_debug(link, "Saved IPv6 link-local address generation mode: %s",
+ ipv6_link_local_address_gen_mode_to_string(mode));
+ else
+ log_link_debug(link, "IPv6 link-local address generation mode is changed: %s -> %s",
+ ipv6_link_local_address_gen_mode_to_string(link->ipv6ll_address_gen_mode),
+ ipv6_link_local_address_gen_mode_to_string(mode));
+
+ link->ipv6ll_address_gen_mode = mode;
+ return 0;
+}
+
+#define STABLE_SECRET_APP_ID_1 SD_ID128_MAKE(aa,05,1d,94,43,68,45,07,b9,73,f1,e8,e4,b7,34,52)
+#define STABLE_SECRET_APP_ID_2 SD_ID128_MAKE(52,c4,40,a0,9f,2f,48,58,a9,3a,f6,29,25,ba,7a,7d)
+
+int link_set_ipv6ll_stable_secret(Link *link) {
+ struct in6_addr a;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->network->ipv6ll_address_gen_mode != IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY)
+ return 0;
+
+ if (in6_addr_is_set(&link->network->ipv6ll_stable_secret))
+ a = link->network->ipv6ll_stable_secret;
+ else {
+ sd_id128_t key;
+ le64_t v;
+
+ /* Generate a stable secret address from machine-ID and the interface name. */
+
+ r = sd_id128_get_machine_app_specific(STABLE_SECRET_APP_ID_1, &key);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to generate key: %m");
+
+ v = htole64(siphash24_string(link->ifname, key.bytes));
+ memcpy(a.s6_addr, &v, sizeof(v));
+
+ r = sd_id128_get_machine_app_specific(STABLE_SECRET_APP_ID_2, &key);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to generate key: %m");
+
+ v = htole64(siphash24_string(link->ifname, key.bytes));
+ assert_cc(sizeof(v) * 2 == sizeof(a.s6_addr));
+ memcpy(a.s6_addr + sizeof(v), &v, sizeof(v));
+ }
+
+ return sysctl_write_ip_property(AF_INET6, link->ifname, "stable_secret",
+ IN6_ADDR_TO_STRING(&a));
+}
+
+int link_set_ipv6ll_addrgen_mode(Link *link, IPv6LinkLocalAddressGenMode mode) {
+ assert(link);
+ assert(mode >= 0 && mode < _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX);
+
+ if (mode == link->ipv6ll_address_gen_mode)
+ return 0;
+
+ return sysctl_write_ip_property_uint32(AF_INET6, link->ifname, "addr_gen_mode", mode);
+}
+
+static const char* const ipv6_link_local_address_gen_mode_table[_IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX] = {
+ [IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_EUI64] = "eui64",
+ [IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE] = "none",
+ [IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY] = "stable-privacy",
+ [IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_RANDOM] = "random",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(ipv6_link_local_address_gen_mode, IPv6LinkLocalAddressGenMode);
+DEFINE_CONFIG_PARSE_ENUM(
+ config_parse_ipv6_link_local_address_gen_mode,
+ ipv6_link_local_address_gen_mode,
+ IPv6LinkLocalAddressGenMode,
+ "Failed to parse IPv6 link-local address generation mode");
diff --git a/src/network/networkd-ipv6ll.h b/src/network/networkd-ipv6ll.h
new file mode 100644
index 0000000..2759eed
--- /dev/null
+++ b/src/network/networkd-ipv6ll.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <errno.h>
+#include <linux/if_link.h>
+#include <stdbool.h>
+
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+#include "macro.h"
+
+typedef struct Link Link;
+
+typedef enum IPv6LinkLocalAddressGenMode {
+ IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_EUI64 = IN6_ADDR_GEN_MODE_EUI64,
+ IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE = IN6_ADDR_GEN_MODE_NONE,
+ IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY = IN6_ADDR_GEN_MODE_STABLE_PRIVACY,
+ IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_RANDOM = IN6_ADDR_GEN_MODE_RANDOM,
+ _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX,
+ _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_INVALID = -EINVAL,
+} IPv6LinkLocalAddressGenMode;
+
+bool link_ipv6ll_enabled(Link *link);
+bool link_may_have_ipv6ll(Link *link, bool check_multicast);
+
+IPv6LinkLocalAddressGenMode link_get_ipv6ll_addrgen_mode(Link *link);
+int ipv6ll_addrgen_mode_fill_message(sd_netlink_message *message, IPv6LinkLocalAddressGenMode mode);
+int link_update_ipv6ll_addrgen_mode(Link *link, sd_netlink_message *message);
+
+int link_set_ipv6ll_stable_secret(Link *link);
+int link_set_ipv6ll_addrgen_mode(Link *link, IPv6LinkLocalAddressGenMode mode);
+
+const char* ipv6_link_local_address_gen_mode_to_string(IPv6LinkLocalAddressGenMode s) _const_;
+IPv6LinkLocalAddressGenMode ipv6_link_local_address_gen_mode_from_string(const char *s) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_link_local_address_gen_mode);
diff --git a/src/network/networkd-json.c b/src/network/networkd-json.c
new file mode 100644
index 0000000..cbbb1e6
--- /dev/null
+++ b/src/network/networkd-json.c
@@ -0,0 +1,1422 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/nexthop.h>
+
+#include "dns-domain.h"
+#include "ip-protocol-list.h"
+#include "netif-util.h"
+#include "networkd-address.h"
+#include "networkd-json.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-neighbor.h"
+#include "networkd-nexthop.h"
+#include "networkd-network.h"
+#include "networkd-route-util.h"
+#include "networkd-route.h"
+#include "networkd-routing-policy-rule.h"
+#include "sort-util.h"
+#include "user-util.h"
+#include "wifi-util.h"
+
+static int address_build_json(Address *address, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *scope = NULL, *flags = NULL, *state = NULL;
+ int r;
+
+ assert(address);
+ assert(ret);
+
+ r = route_scope_to_string_alloc(address->scope, &scope);
+ if (r < 0)
+ return r;
+
+ r = address_flags_to_string_alloc(address->flags, address->family, &flags);
+ if (r < 0)
+ return r;
+
+ r = network_config_state_to_string_alloc(address->state, &state);
+ if (r < 0)
+ return r;
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_INTEGER("Family", address->family),
+ JSON_BUILD_PAIR_IN_ADDR("Address", &address->in_addr, address->family),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("Peer", &address->in_addr_peer, address->family),
+ JSON_BUILD_PAIR_IN4_ADDR_NON_NULL("Broadcast", &address->broadcast),
+ JSON_BUILD_PAIR_UNSIGNED("PrefixLength", address->prefixlen),
+ JSON_BUILD_PAIR_UNSIGNED("Scope", address->scope),
+ JSON_BUILD_PAIR_STRING("ScopeString", scope),
+ JSON_BUILD_PAIR_UNSIGNED("Flags", address->flags),
+ JSON_BUILD_PAIR_STRING("FlagsString", flags),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("Label", address->label),
+ JSON_BUILD_PAIR_FINITE_USEC("PreferredLifetimeUsec", address->lifetime_preferred_usec),
+ JSON_BUILD_PAIR_FINITE_USEC("ValidLifetimeUsec", address->lifetime_valid_usec),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(address->source)),
+ JSON_BUILD_PAIR_STRING("ConfigState", state),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", &address->provider, address->family)));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static int addresses_build_json(Set *addresses, JsonVariant **ret) {
+ JsonVariant **elements;
+ Address *address;
+ size_t n = 0;
+ int r;
+
+ assert(ret);
+
+ if (set_isempty(addresses)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ elements = new(JsonVariant*, set_size(addresses));
+ if (!elements)
+ return -ENOMEM;
+
+ SET_FOREACH(address, addresses) {
+ r = address_build_json(address, elements + n);
+ if (r < 0)
+ goto finalize;
+ n++;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("Addresses", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int neighbor_build_json(Neighbor *n, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *state = NULL;
+ int r;
+
+ assert(n);
+ assert(ret);
+
+ r = network_config_state_to_string_alloc(n->state, &state);
+ if (r < 0)
+ return r;
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_INTEGER("Family", n->family),
+ JSON_BUILD_PAIR_IN_ADDR("Destination", &n->in_addr, n->family),
+ JSON_BUILD_PAIR_HW_ADDR("LinkLayerAddress", &n->ll_addr),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(n->source)),
+ JSON_BUILD_PAIR_STRING("ConfigState", state)));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static int neighbors_build_json(Set *neighbors, JsonVariant **ret) {
+ JsonVariant **elements;
+ Neighbor *neighbor;
+ size_t n = 0;
+ int r;
+
+ assert(ret);
+
+ if (set_isempty(neighbors)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ elements = new(JsonVariant*, set_size(neighbors));
+ if (!elements)
+ return -ENOMEM;
+
+ SET_FOREACH(neighbor, neighbors) {
+ r = neighbor_build_json(neighbor, elements + n);
+ if (r < 0)
+ goto finalize;
+ n++;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("Neighbors", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int nexthop_group_build_json(NextHop *nexthop, JsonVariant **ret) {
+ JsonVariant **elements;
+ struct nexthop_grp *g;
+ size_t n = 0;
+ int r;
+
+ assert(nexthop);
+ assert(ret);
+
+ if (hashmap_isempty(nexthop->group)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ elements = new(JsonVariant*, hashmap_size(nexthop->group));
+ if (!elements)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(g, nexthop->group) {
+ r = json_build(elements + n, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_UNSIGNED("ID", g->id),
+ JSON_BUILD_PAIR_UNSIGNED("Weight", g->weight+1)));
+ if (r < 0)
+ goto failure;
+
+ n++;
+ }
+
+ r = json_variant_new_array(ret, elements, n);
+
+failure:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int nexthop_build_json(NextHop *n, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *group = NULL;
+ _cleanup_free_ char *flags = NULL, *protocol = NULL, *state = NULL;
+ int r;
+
+ assert(n);
+ assert(ret);
+
+ r = route_flags_to_string_alloc(n->flags, &flags);
+ if (r < 0)
+ return r;
+
+ r = route_protocol_to_string_alloc(n->protocol, &protocol);
+ if (r < 0)
+ return r;
+
+ r = network_config_state_to_string_alloc(n->state, &state);
+ if (r < 0)
+ return r;
+
+ r = nexthop_group_build_json(n, &group);
+ if (r < 0)
+ return r;
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_UNSIGNED("ID", n->id),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("Gateway", &n->gw, n->family),
+ JSON_BUILD_PAIR_UNSIGNED("Flags", n->flags),
+ JSON_BUILD_PAIR_STRING("FlagsString", strempty(flags)),
+ JSON_BUILD_PAIR_UNSIGNED("Protocol", n->protocol),
+ JSON_BUILD_PAIR_STRING("ProtocolString", protocol),
+ JSON_BUILD_PAIR_BOOLEAN("Blackhole", n->blackhole),
+ JSON_BUILD_PAIR_VARIANT_NON_NULL("Group", group),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(n->source)),
+ JSON_BUILD_PAIR_STRING("ConfigState", state)));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static int nexthops_build_json(Set *nexthops, JsonVariant **ret) {
+ JsonVariant **elements;
+ NextHop *nexthop;
+ size_t n = 0;
+ int r;
+
+ assert(ret);
+
+ if (set_isempty(nexthops)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ elements = new(JsonVariant*, set_size(nexthops));
+ if (!elements)
+ return -ENOMEM;
+
+ SET_FOREACH(nexthop, nexthops) {
+ r = nexthop_build_json(nexthop, elements + n);
+ if (r < 0)
+ goto finalize;
+ n++;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("NextHops", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int route_build_json(Route *route, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *scope = NULL, *protocol = NULL, *table = NULL, *flags = NULL, *state = NULL;
+ Manager *manager;
+ int r;
+
+ assert(route);
+ assert(ret);
+
+ manager = route->link ? route->link->manager : route->manager;
+
+ assert(manager);
+
+ r = route_scope_to_string_alloc(route->scope, &scope);
+ if (r < 0)
+ return r;
+
+ r = route_protocol_to_string_alloc(route->protocol, &protocol);
+ if (r < 0)
+ return r;
+
+ r = manager_get_route_table_to_string(manager, route->table, &table);
+ if (r < 0)
+ return r;
+
+ r = route_flags_to_string_alloc(route->flags, &flags);
+ if (r < 0)
+ return r;
+
+ r = network_config_state_to_string_alloc(route->state, &state);
+ if (r < 0)
+ return r;
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_INTEGER("Family", route->family),
+ JSON_BUILD_PAIR_IN_ADDR("Destination", &route->dst, route->family),
+ JSON_BUILD_PAIR_UNSIGNED("DestinationPrefixLength", route->dst_prefixlen),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("Gateway", &route->gw, route->gw_family),
+ JSON_BUILD_PAIR_CONDITION(route->src_prefixlen > 0,
+ "Source", JSON_BUILD_IN_ADDR(&route->src, route->family)),
+ JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("SourcePrefixLength", route->src_prefixlen),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("PreferredSource", &route->prefsrc, route->family),
+ JSON_BUILD_PAIR_UNSIGNED("Scope", route->scope),
+ JSON_BUILD_PAIR_STRING("ScopeString", scope),
+ JSON_BUILD_PAIR_UNSIGNED("Protocol", route->protocol),
+ JSON_BUILD_PAIR_STRING("ProtocolString", protocol),
+ JSON_BUILD_PAIR_UNSIGNED("Type", route->type),
+ JSON_BUILD_PAIR_STRING("TypeString", route_type_to_string(route->type)),
+ JSON_BUILD_PAIR_UNSIGNED("Priority", route->priority),
+ JSON_BUILD_PAIR_UNSIGNED("Table", route->table),
+ JSON_BUILD_PAIR_STRING("TableString", table),
+ JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("MTU", route->mtu),
+ JSON_BUILD_PAIR_UNSIGNED("Preference", route->pref),
+ JSON_BUILD_PAIR_UNSIGNED("Flags", route->flags),
+ JSON_BUILD_PAIR_STRING("FlagsString", strempty(flags)),
+ JSON_BUILD_PAIR_FINITE_USEC("LifetimeUSec", route->lifetime_usec),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(route->source)),
+ JSON_BUILD_PAIR_STRING("ConfigState", state),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", &route->provider, route->family)));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static int routes_build_json(Set *routes, JsonVariant **ret) {
+ JsonVariant **elements;
+ Route *route;
+ size_t n = 0;
+ int r;
+
+ assert(ret);
+
+ if (set_isempty(routes)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ elements = new(JsonVariant*, set_size(routes));
+ if (!elements)
+ return -ENOMEM;
+
+ SET_FOREACH(route, routes) {
+ r = route_build_json(route, elements + n);
+ if (r < 0)
+ goto finalize;
+ n++;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("Routes", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int routing_policy_rule_build_json(RoutingPolicyRule *rule, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *table = NULL, *protocol = NULL, *state = NULL;
+ int r;
+
+ assert(rule);
+ assert(rule->manager);
+ assert(ret);
+
+ r = manager_get_route_table_to_string(rule->manager, rule->table, &table);
+ if (r < 0 && r != -EINVAL)
+ return r;
+
+ r = route_protocol_to_string_alloc(rule->protocol, &protocol);
+ if (r < 0)
+ return r;
+
+ r = network_config_state_to_string_alloc(rule->state, &state);
+ if (r < 0)
+ return r;
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_INTEGER("Family", rule->family),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("FromPrefix", &rule->from, rule->family),
+ JSON_BUILD_PAIR_CONDITION(in_addr_is_set(rule->family, &rule->from),
+ "FromPrefixLength", JSON_BUILD_UNSIGNED(rule->from_prefixlen)),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ToPrefix", &rule->to, rule->family),
+ JSON_BUILD_PAIR_CONDITION(in_addr_is_set(rule->family, &rule->to),
+ "ToPrefixLength", JSON_BUILD_UNSIGNED(rule->to_prefixlen)),
+ JSON_BUILD_PAIR_UNSIGNED("Protocol", rule->protocol),
+ JSON_BUILD_PAIR_STRING("ProtocolString", protocol),
+ JSON_BUILD_PAIR_UNSIGNED("TOS", rule->tos),
+ JSON_BUILD_PAIR_UNSIGNED("Type", rule->type),
+ JSON_BUILD_PAIR_STRING("TypeString", fr_act_type_full_to_string(rule->type)),
+ JSON_BUILD_PAIR_UNSIGNED("IPProtocol", rule->ipproto),
+ JSON_BUILD_PAIR_STRING("IPProtocolString", ip_protocol_to_name(rule->ipproto)),
+ JSON_BUILD_PAIR_UNSIGNED("Priority", rule->priority),
+ JSON_BUILD_PAIR_UNSIGNED("FirewallMark", rule->fwmark),
+ JSON_BUILD_PAIR_UNSIGNED("FirewallMask", rule->fwmask),
+ JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("Table", rule->table),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("TableString", table),
+ JSON_BUILD_PAIR_BOOLEAN("Invert", rule->invert_rule),
+ JSON_BUILD_PAIR_CONDITION(rule->suppress_prefixlen >= 0,
+ "SuppressPrefixLength", JSON_BUILD_UNSIGNED(rule->suppress_prefixlen)),
+ JSON_BUILD_PAIR_CONDITION(rule->suppress_ifgroup >= 0,
+ "SuppressInterfaceGroup", JSON_BUILD_UNSIGNED(rule->suppress_ifgroup)),
+ JSON_BUILD_PAIR_CONDITION(rule->sport.start != 0 || rule->sport.end != 0, "SourcePort",
+ JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(rule->sport.start), JSON_BUILD_UNSIGNED(rule->sport.end))),
+ JSON_BUILD_PAIR_CONDITION(rule->dport.start != 0 || rule->dport.end != 0, "DestinationPort",
+ JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(rule->dport.start), JSON_BUILD_UNSIGNED(rule->dport.end))),
+ JSON_BUILD_PAIR_CONDITION(rule->uid_range.start != UID_INVALID && rule->uid_range.end != UID_INVALID, "User",
+ JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(rule->uid_range.start), JSON_BUILD_UNSIGNED(rule->uid_range.end))),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("IncomingInterface", rule->iif),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("OutgoingInterface", rule->oif),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(rule->source)),
+ JSON_BUILD_PAIR_STRING("ConfigState", state)));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static int routing_policy_rules_build_json(Set *rules, JsonVariant **ret) {
+ JsonVariant **elements;
+ RoutingPolicyRule *rule;
+ size_t n = 0;
+ int r;
+
+ assert(ret);
+
+ if (set_isempty(rules)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ elements = new(JsonVariant*, set_size(rules));
+ if (!elements)
+ return -ENOMEM;
+
+ SET_FOREACH(rule, rules) {
+ r = routing_policy_rule_build_json(rule, elements + n);
+ if (r < 0)
+ goto finalize;
+ n++;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("RoutingPolicyRules", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int network_build_json(Network *network, JsonVariant **ret) {
+ assert(ret);
+
+ if (!network) {
+ *ret = NULL;
+ return 0;
+ }
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("NetworkFile", network->filename),
+ JSON_BUILD_PAIR_BOOLEAN("RequiredForOnline", network->required_for_online),
+ JSON_BUILD_PAIR("RequiredOperationalStateForOnline",
+ JSON_BUILD_ARRAY(JSON_BUILD_STRING(link_operstate_to_string(network->required_operstate_for_online.min)),
+ JSON_BUILD_STRING(link_operstate_to_string(network->required_operstate_for_online.max)))),
+ JSON_BUILD_PAIR_STRING("RequiredFamilyForOnline",
+ link_required_address_family_to_string(network->required_family_for_online)),
+ JSON_BUILD_PAIR_STRING("ActivationPolicy",
+ activation_policy_to_string(network->activation_policy))));
+}
+
+static int device_build_json(sd_device *device, JsonVariant **ret) {
+ const char *link = NULL, *path = NULL, *vendor = NULL, *model = NULL;
+
+ assert(ret);
+
+ if (!device) {
+ *ret = NULL;
+ return 0;
+ }
+
+ (void) sd_device_get_property_value(device, "ID_NET_LINK_FILE", &link);
+ (void) sd_device_get_property_value(device, "ID_PATH", &path);
+
+ if (sd_device_get_property_value(device, "ID_VENDOR_FROM_DATABASE", &vendor) < 0)
+ (void) sd_device_get_property_value(device, "ID_VENDOR", &vendor);
+
+ if (sd_device_get_property_value(device, "ID_MODEL_FROM_DATABASE", &model) < 0)
+ (void) sd_device_get_property_value(device, "ID_MODEL", &model);
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("LinkFile", link),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("Path", path),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("Vendor", vendor),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("Model", model)));
+}
+
+static int dns_build_json_one(Link *link, const struct in_addr_full *a, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ int r;
+
+ assert(link);
+ assert(a);
+ assert(ret);
+
+ if (a->ifindex != 0 && a->ifindex != link->ifindex) {
+ *ret = NULL;
+ return 0;
+ }
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_INTEGER("Family", a->family),
+ JSON_BUILD_PAIR_IN_ADDR("Address", &a->address, a->family),
+ JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("Port", a->port),
+ JSON_BUILD_PAIR_CONDITION(a->ifindex != 0, "InterfaceIndex", JSON_BUILD_INTEGER(a->ifindex)),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("ServerName", a->server_name),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, a->family)));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 1;
+}
+
+static int dns_build_json(Link *link, JsonVariant **ret) {
+ JsonVariant **elements = NULL;
+ size_t n = 0;
+ int r;
+
+ assert(link);
+ assert(ret);
+
+ if (!link->network) {
+ *ret = NULL;
+ return 0;
+ }
+
+ if (link->n_dns != UINT_MAX) {
+ for (unsigned i = 0; i < link->n_dns; i++) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = dns_build_json_one(link, link->dns[i], NETWORK_CONFIG_SOURCE_RUNTIME, NULL, elements + n);
+ if (r < 0)
+ goto finalize;
+ if (r > 0)
+ n++;
+ }
+ } else {
+ for (unsigned i = 0; i < link->network->n_dns; i++) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = dns_build_json_one(link, link->network->dns[i], NETWORK_CONFIG_SOURCE_STATIC, NULL, elements + n);
+ if (r < 0)
+ goto finalize;
+ if (r > 0)
+ n++;
+ }
+
+ if (link->dhcp_lease && link->network->dhcp_use_dns) {
+ const struct in_addr *dns;
+ union in_addr_union s;
+ int n_dns;
+
+ r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in);
+ if (r < 0)
+ goto finalize;
+
+ n_dns = sd_dhcp_lease_get_dns(link->dhcp_lease, &dns);
+ for (int i = 0; i < n_dns; i++) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = dns_build_json_one(link,
+ &(struct in_addr_full) { .family = AF_INET, .address.in = dns[i], },
+ NETWORK_CONFIG_SOURCE_DHCP4,
+ &s,
+ elements + n);
+ if (r < 0)
+ goto finalize;
+ if (r > 0)
+ n++;
+ }
+ }
+
+ if (link->dhcp6_lease && link->network->dhcp6_use_dns) {
+ const struct in6_addr *dns;
+ union in_addr_union s;
+ int n_dns;
+
+ r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &s.in6);
+ if (r < 0)
+ goto finalize;
+
+ n_dns = sd_dhcp6_lease_get_dns(link->dhcp6_lease, &dns);
+ for (int i = 0; i < n_dns; i++) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = dns_build_json_one(link,
+ &(struct in_addr_full) { .family = AF_INET6, .address.in6 = dns[i], },
+ NETWORK_CONFIG_SOURCE_DHCP6,
+ &s,
+ elements + n);
+ if (r < 0)
+ goto finalize;
+ if (r > 0)
+ n++;
+ }
+ }
+
+ if (link->network->ipv6_accept_ra_use_dns) {
+ NDiscRDNSS *a;
+
+ SET_FOREACH(a, link->ndisc_rdnss) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = dns_build_json_one(link,
+ &(struct in_addr_full) { .family = AF_INET6, .address.in6 = a->address, },
+ NETWORK_CONFIG_SOURCE_NDISC,
+ &(union in_addr_union) { .in6 = a->router },
+ elements + n);
+ if (r < 0)
+ goto finalize;
+ if (r > 0)
+ n++;
+ }
+ }
+ }
+
+ if (n == 0) {
+ *ret = NULL;
+ r = 0;
+ goto finalize;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("DNS", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int server_build_json_one_addr(int family, const union in_addr_union *a, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **ret) {
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(a);
+ assert(ret);
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_INTEGER("Family", family),
+ JSON_BUILD_PAIR_IN_ADDR("Address", a, family),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, family)));
+}
+
+static int server_build_json_one_fqdn(int family, const char *fqdn, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **ret) {
+ assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(fqdn);
+ assert(ret);
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("Server", fqdn),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, family)));
+}
+
+static int server_build_json_one_string(const char *str, NetworkConfigSource s, JsonVariant **ret) {
+ union in_addr_union a;
+ int family;
+
+ assert(str);
+ assert(ret);
+
+ if (in_addr_from_string_auto(str, &family, &a) >= 0)
+ return server_build_json_one_addr(family, &a, s, NULL, ret);
+
+ return server_build_json_one_fqdn(AF_UNSPEC, str, s, NULL, ret);
+}
+
+static int ntp_build_json(Link *link, JsonVariant **ret) {
+ JsonVariant **elements = NULL;
+ size_t n = 0;
+ int r;
+
+ assert(link);
+ assert(ret);
+
+ if (!link->network) {
+ *ret = NULL;
+ return 0;
+ }
+
+ STRV_FOREACH(p, link->ntp ?: link->network->ntp) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = server_build_json_one_string(*p, NETWORK_CONFIG_SOURCE_RUNTIME, elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ if (!link->ntp) {
+ if (link->dhcp_lease && link->network->dhcp_use_ntp) {
+ const struct in_addr *ntp;
+ union in_addr_union s;
+ int n_ntp;
+
+ r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in);
+ if (r < 0)
+ goto finalize;
+
+ n_ntp = sd_dhcp_lease_get_ntp(link->dhcp_lease, &ntp);
+ for (int i = 0; i < n_ntp; i++) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = server_build_json_one_addr(AF_INET,
+ &(union in_addr_union) { .in = ntp[i], },
+ NETWORK_CONFIG_SOURCE_DHCP4,
+ &s,
+ elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+ }
+
+ if (link->dhcp6_lease && link->network->dhcp6_use_ntp) {
+ const struct in6_addr *ntp_addr;
+ union in_addr_union s;
+ char **ntp_fqdn;
+ int n_ntp;
+
+ r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &s.in6);
+ if (r < 0)
+ goto finalize;
+
+ n_ntp = sd_dhcp6_lease_get_ntp_addrs(link->dhcp6_lease, &ntp_addr);
+ for (int i = 0; i < n_ntp; i++) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = server_build_json_one_addr(AF_INET6,
+ &(union in_addr_union) { .in6 = ntp_addr[i], },
+ NETWORK_CONFIG_SOURCE_DHCP6,
+ &s,
+ elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ n_ntp = sd_dhcp6_lease_get_ntp_fqdn(link->dhcp6_lease, &ntp_fqdn);
+ for (int i = 0; i < n_ntp; i++) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = server_build_json_one_fqdn(AF_INET6,
+ ntp_fqdn[i],
+ NETWORK_CONFIG_SOURCE_DHCP6,
+ &s,
+ elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+ }
+ }
+
+ if (n == 0) {
+ *ret = NULL;
+ return 0;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("NTP", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int sip_build_json(Link *link, JsonVariant **ret) {
+ const struct in_addr *sip;
+ JsonVariant **elements;
+ union in_addr_union s;
+ size_t n = 0;
+ int n_sip, r;
+
+ assert(link);
+ assert(ret);
+
+ if (!link->network || !link->network->dhcp_use_sip || !link->dhcp_lease) {
+ *ret = NULL;
+ return 0;
+ }
+
+ n_sip = sd_dhcp_lease_get_sip(link->dhcp_lease, &sip);
+ if (n_sip <= 0) {
+ *ret = NULL;
+ return 0;
+ }
+
+ r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in);
+ if (r < 0)
+ return r;
+
+ elements = new(JsonVariant*, n_sip);
+ if (!elements)
+ return -ENOMEM;
+
+ for (int i = 0; i < n_sip; i++) {
+ r = server_build_json_one_addr(AF_INET,
+ &(union in_addr_union) { .in = sip[i], },
+ NETWORK_CONFIG_SOURCE_DHCP4,
+ &s,
+ elements + n);
+ if (r < 0)
+ goto finalize;
+ n++;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("SIP", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int domain_build_json(int family, const char *domain, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **ret) {
+ assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(domain);
+ assert(ret);
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("Domain", domain),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)),
+ JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, family)));
+}
+
+static int domains_build_json(Link *link, bool is_route, JsonVariant **ret) {
+ OrderedSet *link_domains, *network_domains;
+ JsonVariant **elements = NULL;
+ DHCPUseDomains use_domains;
+ union in_addr_union s;
+ char **domains;
+ const char *domain;
+ size_t n = 0;
+ int r;
+
+ assert(link);
+ assert(ret);
+
+ if (!link->network) {
+ *ret = NULL;
+ return 0;
+ }
+
+ link_domains = is_route ? link->route_domains : link->search_domains;
+ network_domains = is_route ? link->network->route_domains : link->network->search_domains;
+ use_domains = is_route ? DHCP_USE_DOMAINS_ROUTE : DHCP_USE_DOMAINS_YES;
+
+ ORDERED_SET_FOREACH(domain, link_domains ?: network_domains) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = domain_build_json(AF_UNSPEC, domain,
+ link_domains ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC,
+ NULL, elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ if (!link_domains) {
+ if (link->dhcp_lease &&
+ link->network->dhcp_use_domains == use_domains) {
+ r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in);
+ if (r < 0)
+ goto finalize;
+
+ if (sd_dhcp_lease_get_domainname(link->dhcp_lease, &domain) >= 0) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = domain_build_json(AF_INET, domain, NETWORK_CONFIG_SOURCE_DHCP4, &s, elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ if (sd_dhcp_lease_get_search_domains(link->dhcp_lease, &domains) >= 0) {
+ STRV_FOREACH(p, domains) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = domain_build_json(AF_INET, *p, NETWORK_CONFIG_SOURCE_DHCP4, &s, elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+ }
+ }
+
+ if (link->dhcp6_lease &&
+ link->network->dhcp6_use_domains == use_domains) {
+ r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &s.in6);
+ if (r < 0)
+ goto finalize;
+
+ if (sd_dhcp6_lease_get_domains(link->dhcp6_lease, &domains) >= 0) {
+ STRV_FOREACH(p, domains) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = domain_build_json(AF_INET6, *p, NETWORK_CONFIG_SOURCE_DHCP6, &s, elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+ }
+ }
+
+ if (link->network->ipv6_accept_ra_use_domains == use_domains) {
+ NDiscDNSSL *a;
+
+ SET_FOREACH(a, link->ndisc_dnssl) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = domain_build_json(AF_INET6, NDISC_DNSSL_DOMAIN(a), NETWORK_CONFIG_SOURCE_NDISC,
+ &(union in_addr_union) { .in6 = a->router },
+ elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+ }
+ }
+
+ if (n == 0) {
+ *ret = NULL;
+ return 0;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR(is_route ? "RouteDomains" : "SearchDomains",
+ JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int nta_build_json(const char *nta, NetworkConfigSource s, JsonVariant **ret) {
+ assert(nta);
+ assert(ret);
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("DNSSECNegativeTrustAnchor", nta),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s))));
+}
+
+static int ntas_build_json(Link *link, JsonVariant **ret) {
+ JsonVariant **elements = NULL;
+ const char *nta;
+ size_t n = 0;
+ int r;
+
+ assert(link);
+ assert(ret);
+
+ if (!link->network) {
+ *ret = NULL;
+ return 0;
+ }
+
+ SET_FOREACH(nta, link->dnssec_negative_trust_anchors ?: link->network->dnssec_negative_trust_anchors) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = nta_build_json(nta,
+ link->dnssec_negative_trust_anchors ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC,
+ elements + n);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ if (n == 0) {
+ *ret = NULL;
+ return 0;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("DNSSECNegativeTrustAnchors",
+ JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+static int dns_misc_build_json(Link *link, JsonVariant **ret) {
+ JsonVariant **elements = NULL;
+ ResolveSupport resolve_support;
+ NetworkConfigSource source;
+ DnsOverTlsMode mode;
+ size_t n = 0;
+ int t, r;
+
+ assert(link);
+ assert(ret);
+
+ if (!link->network) {
+ *ret = NULL;
+ return 0;
+ }
+
+ resolve_support = link->llmnr >= 0 ? link->llmnr : link->network->llmnr;
+ if (resolve_support >= 0) {
+ source = link->llmnr >= 0 ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC;
+
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = json_build(elements + n, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("LLMNR", resolve_support_to_string(resolve_support)),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(source))));
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ resolve_support = link->mdns >= 0 ? link->mdns : link->network->mdns;
+ if (resolve_support >= 0) {
+ source = link->mdns >= 0 ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC;
+
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = json_build(elements + n, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("MDNS", resolve_support_to_string(resolve_support)),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(source))));
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ t = link->dns_default_route >= 0 ? link->dns_default_route : link->network->dns_default_route;
+ if (t >= 0) {
+ source = link->dns_default_route >= 0 ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC;
+
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = json_build(elements + n, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_BOOLEAN("DNSDefaultRoute", t),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(source))));
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ mode = link->dns_over_tls_mode >= 0 ? link->dns_over_tls_mode : link->network->dns_over_tls_mode;
+ if (mode >= 0) {
+ source = link->dns_over_tls_mode >= 0 ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC;
+
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = json_build(elements + n, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("DNSOverTLS", dns_over_tls_mode_to_string(mode)),
+ JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(source))));
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("DNSSettings",
+ JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+int link_build_json(Link *link, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL;
+ _cleanup_free_ char *type = NULL, *flags = NULL;
+ int r;
+
+ assert(link);
+ assert(ret);
+
+ r = net_get_type_string(link->dev, link->iftype, &type);
+ if (r == -ENOMEM)
+ return r;
+
+ r = link_flags_to_string_alloc(link->flags, &flags);
+ if (r < 0)
+ return r;
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ /* basic information */
+ JSON_BUILD_PAIR_INTEGER("Index", link->ifindex),
+ JSON_BUILD_PAIR_STRING("Name", link->ifname),
+ JSON_BUILD_PAIR_STRV_NON_EMPTY("AlternativeNames", link->alternative_names),
+ JSON_BUILD_PAIR_CONDITION(link->master_ifindex > 0,
+ "MasterInterfaceIndex", JSON_BUILD_INTEGER(link->master_ifindex)),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("Kind", link->kind),
+ JSON_BUILD_PAIR_STRING("Type", type),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("Driver", link->driver),
+ JSON_BUILD_PAIR_UNSIGNED("Flags", link->flags),
+ JSON_BUILD_PAIR_STRING("FlagsString", flags),
+ JSON_BUILD_PAIR_UNSIGNED("KernelOperationalState", link->kernel_operstate),
+ JSON_BUILD_PAIR_STRING("KernelOperationalStateString", kernel_operstate_to_string(link->kernel_operstate)),
+ JSON_BUILD_PAIR_UNSIGNED("MTU", link->mtu),
+ JSON_BUILD_PAIR_UNSIGNED("MinimumMTU", link->min_mtu),
+ JSON_BUILD_PAIR_UNSIGNED("MaximumMTU", link->max_mtu),
+ JSON_BUILD_PAIR_HW_ADDR_NON_NULL("HardwareAddress", &link->hw_addr),
+ JSON_BUILD_PAIR_HW_ADDR_NON_NULL("PermanentHardwareAddress", &link->permanent_hw_addr),
+ JSON_BUILD_PAIR_HW_ADDR_NON_NULL("BroadcastAddress", &link->bcast_addr),
+ JSON_BUILD_PAIR_IN6_ADDR_NON_NULL("IPv6LinkLocalAddress", &link->ipv6ll_address),
+ /* wlan information */
+ JSON_BUILD_PAIR_CONDITION(link->wlan_iftype > 0, "WirelessLanInterfaceType",
+ JSON_BUILD_UNSIGNED(link->wlan_iftype)),
+ JSON_BUILD_PAIR_CONDITION(link->wlan_iftype > 0, "WirelessLanInterfaceTypeString",
+ JSON_BUILD_STRING(nl80211_iftype_to_string(link->wlan_iftype))),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("SSID", link->ssid),
+ JSON_BUILD_PAIR_ETHER_ADDR_NON_NULL("BSSID", &link->bssid),
+ /* link state */
+ JSON_BUILD_PAIR_STRING("AdministrativeState", link_state_to_string(link->state)),
+ JSON_BUILD_PAIR_STRING("OperationalState", link_operstate_to_string(link->operstate)),
+ JSON_BUILD_PAIR_STRING("CarrierState", link_carrier_state_to_string(link->carrier_state)),
+ JSON_BUILD_PAIR_STRING("AddressState", link_address_state_to_string(link->address_state)),
+ JSON_BUILD_PAIR_STRING("IPv4AddressState", link_address_state_to_string(link->ipv4_address_state)),
+ JSON_BUILD_PAIR_STRING("IPv6AddressState", link_address_state_to_string(link->ipv6_address_state)),
+ JSON_BUILD_PAIR_STRING("OnlineState", link_online_state_to_string(link->online_state))));
+ if (r < 0)
+ return r;
+
+ r = network_build_json(link->network, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = device_build_json(link->dev, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = dns_build_json(link, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = ntp_build_json(link, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = sip_build_json(link, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = domains_build_json(link, /* is_route = */ false, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = domains_build_json(link, /* is_route = */ true, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = ntas_build_json(link, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = dns_misc_build_json(link, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = addresses_build_json(link->addresses, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = neighbors_build_json(link->neighbors, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = nexthops_build_json(link->nexthops, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = routes_build_json(link->routes, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static int link_json_compare(JsonVariant * const *a, JsonVariant * const *b) {
+ int64_t index_a, index_b;
+
+ assert(a && *a);
+ assert(b && *b);
+
+ index_a = json_variant_integer(json_variant_by_key(*a, "Index"));
+ index_b = json_variant_integer(json_variant_by_key(*b, "Index"));
+
+ return CMP(index_a, index_b);
+}
+
+static int links_build_json(Manager *manager, JsonVariant **ret) {
+ JsonVariant **elements;
+ Link *link;
+ size_t n = 0;
+ int r;
+
+ assert(manager);
+ assert(ret);
+
+ elements = new(JsonVariant*, hashmap_size(manager->links_by_index));
+ if (!elements)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(link, manager->links_by_index) {
+ r = link_build_json(link, elements + n);
+ if (r < 0)
+ goto finalize;
+ n++;
+ }
+
+ typesafe_qsort(elements, n, link_json_compare);
+
+ r = json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("Interfaces", JSON_BUILD_VARIANT_ARRAY(elements, n))));
+
+finalize:
+ json_variant_unref_many(elements, n);
+ free(elements);
+ return r;
+}
+
+int manager_build_json(Manager *manager, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL;
+ int r;
+
+ assert(manager);
+ assert(ret);
+
+ r = links_build_json(manager, &v);
+ if (r < 0)
+ return r;
+
+ r = nexthops_build_json(manager->nexthops, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = routes_build_json(manager->routes, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ w = json_variant_unref(w);
+
+ r = routing_policy_rules_build_json(manager->rules, &w);
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&v, w);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
diff --git a/src/network/networkd-json.h b/src/network/networkd-json.h
new file mode 100644
index 0000000..25018fa
--- /dev/null
+++ b/src/network/networkd-json.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "json.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+
+int link_build_json(Link *link, JsonVariant **ret);
+int manager_build_json(Manager *manager, JsonVariant **ret);
diff --git a/src/network/networkd-link-bus.c b/src/network/networkd-link-bus.c
new file mode 100644
index 0000000..a6b2eef
--- /dev/null
+++ b/src/network/networkd-link-bus.c
@@ -0,0 +1,908 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <sys/capability.h>
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-get-properties.h"
+#include "bus-message-util.h"
+#include "bus-polkit.h"
+#include "dns-domain.h"
+#include "networkd-json.h"
+#include "networkd-link-bus.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-state-file.h"
+#include "parse-util.h"
+#include "resolve-util.h"
+#include "socket-netlink.h"
+#include "strv.h"
+#include "user-util.h"
+
+BUS_DEFINE_PROPERTY_GET_ENUM(property_get_operational_state, link_operstate, LinkOperationalState);
+BUS_DEFINE_PROPERTY_GET_ENUM(property_get_carrier_state, link_carrier_state, LinkCarrierState);
+BUS_DEFINE_PROPERTY_GET_ENUM(property_get_address_state, link_address_state, LinkAddressState);
+BUS_DEFINE_PROPERTY_GET_ENUM(property_get_online_state, link_online_state, LinkOnlineState);
+static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_administrative_state, link_state, LinkState);
+
+static int property_get_bit_rates(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Link *link = ASSERT_PTR(userdata);
+ Manager *manager;
+ double interval_sec;
+ uint64_t tx, rx;
+
+ assert(bus);
+ assert(reply);
+
+ manager = link->manager;
+
+ if (!manager->use_speed_meter ||
+ manager->speed_meter_usec_old == 0 ||
+ !link->stats_updated)
+ return sd_bus_message_append(reply, "(tt)", UINT64_MAX, UINT64_MAX);
+
+ assert(manager->speed_meter_usec_new > manager->speed_meter_usec_old);
+ interval_sec = (manager->speed_meter_usec_new - manager->speed_meter_usec_old) / USEC_PER_SEC;
+
+ if (link->stats_new.tx_bytes > link->stats_old.tx_bytes)
+ tx = (uint64_t) ((link->stats_new.tx_bytes - link->stats_old.tx_bytes) / interval_sec);
+ else
+ tx = (uint64_t) ((UINT64_MAX - (link->stats_old.tx_bytes - link->stats_new.tx_bytes)) / interval_sec);
+
+ if (link->stats_new.rx_bytes > link->stats_old.rx_bytes)
+ rx = (uint64_t) ((link->stats_new.rx_bytes - link->stats_old.rx_bytes) / interval_sec);
+ else
+ rx = (uint64_t) ((UINT64_MAX - (link->stats_old.rx_bytes - link->stats_new.rx_bytes)) / interval_sec);
+
+ return sd_bus_message_append(reply, "(tt)", tx, rx);
+}
+
+static int verify_managed_link(Link *l, sd_bus_error *error) {
+ assert(l);
+
+ if (l->flags & IFF_LOOPBACK)
+ return sd_bus_error_setf(error, BUS_ERROR_LINK_BUSY, "Link %s is loopback device.", l->ifname);
+
+ return 0;
+}
+
+int bus_link_method_set_ntp_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_strv_free_ char **ntp = NULL;
+ Link *l = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read_strv(message, &ntp);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, ntp) {
+ r = dns_name_is_valid_or_address(*i);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid NTP server: %s", *i);
+ }
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-ntp-servers",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ strv_free_and_replace(l->ntp, ntp);
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+static int bus_link_method_set_dns_servers_internal(sd_bus_message *message, void *userdata, sd_bus_error *error, bool extended) {
+ struct in_addr_full **dns;
+ Link *l = ASSERT_PTR(userdata);
+ size_t n;
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = bus_message_read_dns_servers(message, error, extended, &dns, &n);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-dns-servers",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ goto finalize;
+ if (r == 0) {
+ r = 1; /* Polkit will call us back */
+ goto finalize;
+ }
+
+ if (l->n_dns != UINT_MAX)
+ for (unsigned i = 0; i < l->n_dns; i++)
+ in_addr_full_free(l->dns[i]);
+
+ free_and_replace(l->dns, dns);
+ l->n_dns = n;
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+
+finalize:
+ for (size_t i = 0; i < n; i++)
+ in_addr_full_free(dns[i]);
+ free(dns);
+
+ return r;
+}
+
+int bus_link_method_set_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return bus_link_method_set_dns_servers_internal(message, userdata, error, false);
+}
+
+int bus_link_method_set_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return bus_link_method_set_dns_servers_internal(message, userdata, error, true);
+}
+
+int bus_link_method_set_domains(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_ordered_set_free_ OrderedSet *search_domains = NULL, *route_domains = NULL;
+ Link *l = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_enter_container(message, 'a', "(sb)");
+ if (r < 0)
+ return r;
+
+ search_domains = ordered_set_new(&string_hash_ops_free);
+ if (!search_domains)
+ return -ENOMEM;
+
+ route_domains = ordered_set_new(&string_hash_ops_free);
+ if (!route_domains)
+ return -ENOMEM;
+
+ for (;;) {
+ _cleanup_free_ char *str = NULL;
+ const char *name;
+ int route_only;
+
+ r = sd_bus_message_read(message, "(sb)", &name, &route_only);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ r = dns_name_is_valid(name);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid search domain %s", name);
+ if (!route_only && dns_name_is_root(name))
+ return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Root domain is not suitable as search domain");
+
+ r = dns_name_normalize(name, 0, &str);
+ if (r < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid search domain %s", name);
+
+ r = ordered_set_consume(route_only ? route_domains : search_domains, TAKE_PTR(str));
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-domains",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ ordered_set_free(l->search_domains);
+ ordered_set_free(l->route_domains);
+ l->search_domains = TAKE_PTR(search_domains);
+ l->route_domains = TAKE_PTR(route_domains);
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ int r, b;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "b", &b);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-default-route",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ if (l->dns_default_route != b) {
+ l->dns_default_route = b;
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ ResolveSupport mode;
+ const char *llmnr;
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "s", &llmnr);
+ if (r < 0)
+ return r;
+
+ if (isempty(llmnr))
+ mode = RESOLVE_SUPPORT_YES;
+ else {
+ mode = resolve_support_from_string(llmnr);
+ if (mode < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid LLMNR setting: %s", llmnr);
+ }
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-llmnr",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ if (l->llmnr != mode) {
+ l->llmnr = mode;
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ ResolveSupport mode;
+ const char *mdns;
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "s", &mdns);
+ if (r < 0)
+ return r;
+
+ if (isempty(mdns))
+ mode = RESOLVE_SUPPORT_NO;
+ else {
+ mode = resolve_support_from_string(mdns);
+ if (mode < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid MulticastDNS setting: %s", mdns);
+ }
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-mdns",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ if (l->mdns != mode) {
+ l->mdns = mode;
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ const char *dns_over_tls;
+ DnsOverTlsMode mode;
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "s", &dns_over_tls);
+ if (r < 0)
+ return r;
+
+ if (isempty(dns_over_tls))
+ mode = _DNS_OVER_TLS_MODE_INVALID;
+ else {
+ mode = dns_over_tls_mode_from_string(dns_over_tls);
+ if (mode < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid DNSOverTLS setting: %s", dns_over_tls);
+ }
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-dns-over-tls",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ if (l->dns_over_tls_mode != mode) {
+ l->dns_over_tls_mode = mode;
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ const char *dnssec;
+ DnssecMode mode;
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "s", &dnssec);
+ if (r < 0)
+ return r;
+
+ if (isempty(dnssec))
+ mode = _DNSSEC_MODE_INVALID;
+ else {
+ mode = dnssec_mode_from_string(dnssec);
+ if (mode < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid DNSSEC setting: %s", dnssec);
+ }
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-dnssec",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ if (l->dnssec_mode != mode) {
+ l->dnssec_mode = mode;
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_set_free_free_ Set *ns = NULL;
+ _cleanup_strv_free_ char **ntas = NULL;
+ Link *l = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read_strv(message, &ntas);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, ntas) {
+ r = dns_name_is_valid(*i);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid negative trust anchor domain: %s", *i);
+ }
+
+ ns = set_new(&dns_name_hash_ops);
+ if (!ns)
+ return -ENOMEM;
+
+ STRV_FOREACH(i, ntas) {
+ r = set_put_strdup(&ns, *i);
+ if (r < 0)
+ return r;
+ }
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.set-dnssec-negative-trust-anchors",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ set_free_free(l->dnssec_negative_trust_anchors);
+ l->dnssec_negative_trust_anchors = TAKE_PTR(ns);
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_revert_ntp(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.revert-ntp",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ link_ntp_settings_clear(l);
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_revert_dns(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ r = verify_managed_link(l, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.revert-dns",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ link_dns_settings_clear(l);
+
+ link_dirty(l);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_force_renew(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ int r;
+
+ if (!l->network)
+ return sd_bus_error_setf(error, BUS_ERROR_UNMANAGED_INTERFACE,
+ "Interface %s is not managed by systemd-networkd",
+ l->ifname);
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.forcerenew",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ if (sd_dhcp_server_is_running(l->dhcp_server)) {
+ r = sd_dhcp_server_forcerenew(l->dhcp_server);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_renew(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ int r;
+
+ if (!l->network)
+ return sd_bus_error_setf(error, BUS_ERROR_UNMANAGED_INTERFACE,
+ "Interface %s is not managed by systemd-networkd",
+ l->ifname);
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.renew",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ if (sd_dhcp_client_is_running(l->dhcp_client))
+ r = sd_dhcp_client_send_renew(l->dhcp_client);
+ else
+ /* The DHCPv4 client may have been stopped by the IPv6 only mode. Let's unconditionally
+ * restart the client here. Note, if the DHCPv4 client is disabled, then dhcp4_start() does
+ * nothing and returns 0. */
+ r = dhcp4_start(l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_reconfigure(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Link *l = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.reconfigure",
+ NULL, true, UID_INVALID,
+ &l->manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ r = link_reconfigure(l, /* force = */ true);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ link_set_state(l, LINK_STATE_INITIALIZED);
+ r = link_save_and_clean(l);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_describe(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *text = NULL;
+ Link *link = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ r = link_build_json(link, &v);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to build JSON data: %m");
+
+ r = json_variant_format(v, 0, &text);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to format JSON data: %m");
+
+ r = sd_bus_message_new_method_return(message, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(reply, "s", text);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static const sd_bus_vtable link_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+
+ SD_BUS_PROPERTY("OperationalState", "s", property_get_operational_state, offsetof(Link, operstate), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("CarrierState", "s", property_get_carrier_state, offsetof(Link, carrier_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("AddressState", "s", property_get_address_state, offsetof(Link, address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("IPv4AddressState", "s", property_get_address_state, offsetof(Link, ipv4_address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("IPv6AddressState", "s", property_get_address_state, offsetof(Link, ipv6_address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("OnlineState", "s", property_get_online_state, offsetof(Link, online_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("AdministrativeState", "s", property_get_administrative_state, offsetof(Link, state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("BitRates", "(tt)", property_get_bit_rates, 0, 0),
+
+ SD_BUS_METHOD_WITH_ARGS("SetNTP",
+ SD_BUS_ARGS("as", servers),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_ntp_servers,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetDNS",
+ SD_BUS_ARGS("a(iay)", addresses),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_dns_servers,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetDNSEx",
+ SD_BUS_ARGS("a(iayqs)", addresses),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_dns_servers_ex,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetDomains",
+ SD_BUS_ARGS("a(sb)", domains),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_domains,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetDefaultRoute",
+ SD_BUS_ARGS("b", enable),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_default_route,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLLMNR",
+ SD_BUS_ARGS("s", mode),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_llmnr,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetMulticastDNS",
+ SD_BUS_ARGS("s", mode),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_mdns,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetDNSOverTLS",
+ SD_BUS_ARGS("s", mode),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_dns_over_tls,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetDNSSEC",
+ SD_BUS_ARGS("s", mode),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_dnssec,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetDNSSECNegativeTrustAnchors",
+ SD_BUS_ARGS("as", names),
+ SD_BUS_NO_RESULT,
+ bus_link_method_set_dnssec_negative_trust_anchors,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("RevertNTP",
+ SD_BUS_NO_ARGS,
+ SD_BUS_NO_RESULT,
+ bus_link_method_revert_ntp,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("RevertDNS",
+ SD_BUS_NO_ARGS,
+ SD_BUS_NO_RESULT,
+ bus_link_method_revert_dns,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("Renew",
+ SD_BUS_NO_ARGS,
+ SD_BUS_NO_RESULT,
+ bus_link_method_renew,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("ForceRenew",
+ SD_BUS_NO_ARGS,
+ SD_BUS_NO_RESULT,
+ bus_link_method_force_renew,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("Reconfigure",
+ SD_BUS_NO_ARGS,
+ SD_BUS_NO_RESULT,
+ bus_link_method_reconfigure,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("Describe",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("s", json),
+ bus_link_method_describe,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_VTABLE_END
+};
+
+char *link_bus_path(Link *link) {
+ _cleanup_free_ char *ifindex = NULL;
+ char *p;
+ int r;
+
+ assert(link);
+ assert(link->ifindex > 0);
+
+ if (asprintf(&ifindex, "%d", link->ifindex) < 0)
+ return NULL;
+
+ r = sd_bus_path_encode("/org/freedesktop/network1/link", ifindex, &p);
+ if (r < 0)
+ return NULL;
+
+ return p;
+}
+
+int link_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) {
+ _cleanup_strv_free_ char **l = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ unsigned c = 0;
+ Link *link;
+
+ assert(bus);
+ assert(path);
+ assert(nodes);
+
+ l = new0(char*, hashmap_size(m->links_by_index) + 1);
+ if (!l)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(link, m->links_by_index) {
+ char *p;
+
+ p = link_bus_path(link);
+ if (!p)
+ return -ENOMEM;
+
+ l[c++] = p;
+ }
+
+ l[c] = NULL;
+ *nodes = TAKE_PTR(l);
+
+ return 1;
+}
+
+int link_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) {
+ _cleanup_free_ char *identifier = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ Link *link;
+ int ifindex, r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(found);
+
+ r = sd_bus_path_decode(path, "/org/freedesktop/network1/link", &identifier);
+ if (r <= 0)
+ return 0;
+
+ ifindex = parse_ifindex(identifier);
+ if (ifindex < 0)
+ return 0;
+
+ r = link_get_by_index(m, ifindex, &link);
+ if (r < 0)
+ return 0;
+
+ if (streq(interface, "org.freedesktop.network1.DHCPServer") &&
+ (!link->dhcp_server || sd_dhcp_server_is_in_relay_mode(link->dhcp_server)))
+ return 0;
+
+ *found = link;
+
+ return 1;
+}
+
+int link_send_changed_strv(Link *link, char **properties) {
+ _cleanup_free_ char *p = NULL;
+
+ assert(link);
+ assert(link->manager);
+ assert(properties);
+
+ if (sd_bus_is_ready(link->manager->bus) <= 0)
+ return 0;
+
+ p = link_bus_path(link);
+ if (!p)
+ return -ENOMEM;
+
+ return sd_bus_emit_properties_changed_strv(
+ link->manager->bus,
+ p,
+ "org.freedesktop.network1.Link",
+ properties);
+}
+
+int link_send_changed(Link *link, const char *property, ...) {
+ char **properties;
+
+ properties = strv_from_stdarg_alloca(property);
+
+ return link_send_changed_strv(link, properties);
+}
+
+const BusObjectImplementation link_object = {
+ "/org/freedesktop/network1/link",
+ "org.freedesktop.network1.Link",
+ .fallback_vtables = BUS_FALLBACK_VTABLES({link_vtable, link_object_find}),
+ .node_enumerator = link_node_enumerator,
+};
diff --git a/src/network/networkd-link-bus.h b/src/network/networkd-link-bus.h
new file mode 100644
index 0000000..924d997
--- /dev/null
+++ b/src/network/networkd-link-bus.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "bus-object.h"
+#include "macro.h"
+
+typedef struct Link Link;
+
+extern const BusObjectImplementation link_object;
+
+char *link_bus_path(Link *link);
+int link_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error);
+int link_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error);
+int link_send_changed_strv(Link *link, char **properties);
+int link_send_changed(Link *link, const char *property, ...) _sentinel_;
+
+int property_get_operational_state(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error);
+int property_get_carrier_state(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error);
+int property_get_address_state(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error);
+int property_get_online_state(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error);
+
+int bus_link_method_set_ntp_servers(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_domains(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_set_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_revert_ntp(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_revert_dns(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_renew(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_force_renew(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_reconfigure(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_link_method_describe(sd_bus_message *message, void *userdata, sd_bus_error *error);
diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c
new file mode 100644
index 0000000..cb21929
--- /dev/null
+++ b/src/network/networkd-link.c
@@ -0,0 +1,2736 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+#include <linux/if_link.h>
+#include <linux/netdevice.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "arphrd-util.h"
+#include "batadv.h"
+#include "bond.h"
+#include "bridge.h"
+#include "bus-util.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "dhcp-identifier.h"
+#include "dhcp-lease-internal.h"
+#include "env-file.h"
+#include "ethtool-util.h"
+#include "event-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "glyph-util.h"
+#include "missing_network.h"
+#include "netlink-util.h"
+#include "network-internal.h"
+#include "networkd-address-label.h"
+#include "networkd-address.h"
+#include "networkd-bridge-fdb.h"
+#include "networkd-bridge-mdb.h"
+#include "networkd-can.h"
+#include "networkd-dhcp-prefix-delegation.h"
+#include "networkd-dhcp-server.h"
+#include "networkd-dhcp4.h"
+#include "networkd-dhcp6.h"
+#include "networkd-ipv4acd.h"
+#include "networkd-ipv4ll.h"
+#include "networkd-ipv6-proxy-ndp.h"
+#include "networkd-link-bus.h"
+#include "networkd-link.h"
+#include "networkd-lldp-tx.h"
+#include "networkd-manager.h"
+#include "networkd-ndisc.h"
+#include "networkd-neighbor.h"
+#include "networkd-nexthop.h"
+#include "networkd-queue.h"
+#include "networkd-radv.h"
+#include "networkd-route.h"
+#include "networkd-routing-policy-rule.h"
+#include "networkd-setlink.h"
+#include "networkd-sriov.h"
+#include "networkd-state-file.h"
+#include "networkd-sysctl.h"
+#include "set.h"
+#include "socket-util.h"
+#include "stdio-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "tc.h"
+#include "tmpfile-util.h"
+#include "tuntap.h"
+#include "udev-util.h"
+#include "util.h"
+#include "vrf.h"
+
+bool link_ipv6_enabled(Link *link) {
+ assert(link);
+
+ if (!socket_ipv6_is_supported())
+ return false;
+
+ if (link->iftype == ARPHRD_CAN)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ if (link->network->bond)
+ return false;
+
+ if (link_may_have_ipv6ll(link, /* check_multicast = */ false))
+ return true;
+
+ if (network_has_static_ipv6_configurations(link->network))
+ return true;
+
+ return false;
+}
+
+static bool link_is_ready_to_configure_one(Link *link, bool allow_unmanaged) {
+ assert(link);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED, LINK_STATE_UNMANAGED))
+ return false;
+
+ if (!link->network)
+ return allow_unmanaged;
+
+ if (!link->network->configure_without_carrier) {
+ if (link->set_flags_messages > 0)
+ return false;
+
+ if (!link_has_carrier(link))
+ return false;
+ }
+
+ if (link->set_link_messages > 0)
+ return false;
+
+ if (!link->activated)
+ return false;
+
+ return true;
+}
+
+bool link_is_ready_to_configure(Link *link, bool allow_unmanaged) {
+ return check_ready_for_all_sr_iov_ports(link, allow_unmanaged, link_is_ready_to_configure_one);
+}
+
+void link_ntp_settings_clear(Link *link) {
+ link->ntp = strv_free(link->ntp);
+}
+
+void link_dns_settings_clear(Link *link) {
+ if (link->n_dns != UINT_MAX)
+ for (unsigned i = 0; i < link->n_dns; i++)
+ in_addr_full_free(link->dns[i]);
+ link->dns = mfree(link->dns);
+ link->n_dns = UINT_MAX;
+
+ link->search_domains = ordered_set_free(link->search_domains);
+ link->route_domains = ordered_set_free(link->route_domains);
+
+ link->dns_default_route = -1;
+ link->llmnr = _RESOLVE_SUPPORT_INVALID;
+ link->mdns = _RESOLVE_SUPPORT_INVALID;
+ link->dnssec_mode = _DNSSEC_MODE_INVALID;
+ link->dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID;
+
+ link->dnssec_negative_trust_anchors = set_free_free(link->dnssec_negative_trust_anchors);
+}
+
+static void link_free_engines(Link *link) {
+ if (!link)
+ return;
+
+ link->dhcp_server = sd_dhcp_server_unref(link->dhcp_server);
+
+ link->dhcp_client = sd_dhcp_client_unref(link->dhcp_client);
+ link->dhcp_lease = sd_dhcp_lease_unref(link->dhcp_lease);
+ link->dhcp4_6rd_tunnel_name = mfree(link->dhcp4_6rd_tunnel_name);
+
+ link->lldp_rx = sd_lldp_rx_unref(link->lldp_rx);
+ link->lldp_tx = sd_lldp_tx_unref(link->lldp_tx);
+
+ link->ipv4ll = sd_ipv4ll_unref(link->ipv4ll);
+
+ link->dhcp6_client = sd_dhcp6_client_unref(link->dhcp6_client);
+ link->dhcp6_lease = sd_dhcp6_lease_unref(link->dhcp6_lease);
+
+ link->ndisc = sd_ndisc_unref(link->ndisc);
+ link->ndisc_expire = sd_event_source_disable_unref(link->ndisc_expire);
+ ndisc_flush(link);
+
+ link->radv = sd_radv_unref(link->radv);
+}
+
+static Link *link_free(Link *link) {
+ assert(link);
+
+ link_ntp_settings_clear(link);
+ link_dns_settings_clear(link);
+
+ link->routes = set_free(link->routes);
+ link->nexthops = set_free(link->nexthops);
+ link->neighbors = set_free(link->neighbors);
+ link->addresses = set_free(link->addresses);
+ link->qdiscs = set_free(link->qdiscs);
+ link->tclasses = set_free(link->tclasses);
+
+ link->dhcp_pd_prefixes = set_free(link->dhcp_pd_prefixes);
+
+ link_free_engines(link);
+
+ set_free(link->sr_iov_virt_port_ifindices);
+ free(link->ifname);
+ strv_free(link->alternative_names);
+ free(link->kind);
+ free(link->ssid);
+ free(link->previous_ssid);
+ free(link->driver);
+
+ unlink_and_free(link->lease_file);
+ unlink_and_free(link->lldp_file);
+ unlink_and_free(link->state_file);
+
+ sd_device_unref(link->dev);
+ netdev_unref(link->netdev);
+
+ hashmap_free(link->bound_to_links);
+ hashmap_free(link->bound_by_links);
+
+ set_free_with_destructor(link->slaves, link_unref);
+
+ network_unref(link->network);
+
+ sd_event_source_disable_unref(link->carrier_lost_timer);
+
+ return mfree(link);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(Link, link, link_free);
+
+int link_get_by_index(Manager *m, int ifindex, Link **ret) {
+ Link *link;
+
+ assert(m);
+ assert(ifindex > 0);
+
+ link = hashmap_get(m->links_by_index, INT_TO_PTR(ifindex));
+ if (!link)
+ return -ENODEV;
+
+ if (ret)
+ *ret = link;
+ return 0;
+}
+
+int link_get_by_name(Manager *m, const char *ifname, Link **ret) {
+ Link *link;
+
+ assert(m);
+ assert(ifname);
+
+ link = hashmap_get(m->links_by_name, ifname);
+ if (!link)
+ return -ENODEV;
+
+ if (ret)
+ *ret = link;
+ return 0;
+}
+
+int link_get_by_hw_addr(Manager *m, const struct hw_addr_data *hw_addr, Link **ret) {
+ Link *link;
+
+ assert(m);
+ assert(hw_addr);
+
+ link = hashmap_get(m->links_by_hw_addr, hw_addr);
+ if (!link)
+ return -ENODEV;
+
+ if (ret)
+ *ret = link;
+ return 0;
+}
+
+int link_get_master(Link *link, Link **ret) {
+ assert(link);
+ assert(link->manager);
+ assert(ret);
+
+ if (link->master_ifindex <= 0 || link->master_ifindex == link->ifindex)
+ return -ENODEV;
+
+ return link_get_by_index(link->manager, link->master_ifindex, ret);
+}
+
+void link_set_state(Link *link, LinkState state) {
+ assert(link);
+
+ if (link->state == state)
+ return;
+
+ log_link_debug(link, "State changed: %s -> %s",
+ link_state_to_string(link->state),
+ link_state_to_string(state));
+
+ link->state = state;
+
+ link_send_changed(link, "AdministrativeState", NULL);
+ link_dirty(link);
+}
+
+int link_stop_engines(Link *link, bool may_keep_dhcp) {
+ int r = 0, k;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->event);
+
+ bool keep_dhcp = may_keep_dhcp &&
+ link->network &&
+ !link->network->dhcp_send_decline && /* IPv4 ACD for the DHCPv4 address is running. */
+ (link->manager->restarting ||
+ FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP_ON_STOP));
+
+ if (!keep_dhcp) {
+ k = sd_dhcp_client_stop(link->dhcp_client);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop DHCPv4 client: %m");
+ }
+
+ k = sd_dhcp_server_stop(link->dhcp_server);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop DHCPv4 server: %m");
+
+ k = sd_lldp_rx_stop(link->lldp_rx);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop LLDP Rx: %m");
+
+ k = sd_lldp_tx_stop(link->lldp_tx);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop LLDP Tx: %m");
+
+ k = sd_ipv4ll_stop(link->ipv4ll);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop IPv4 link-local: %m");
+
+ k = ipv4acd_stop(link);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop IPv4 ACD client: %m");
+
+ k = sd_dhcp6_client_stop(link->dhcp6_client);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop DHCPv6 client: %m");
+
+ k = dhcp_pd_remove(link, /* only_marked = */ false);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not remove DHCPv6 PD addresses and routes: %m");
+
+ k = ndisc_stop(link);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop IPv6 Router Discovery: %m");
+
+ ndisc_flush(link);
+
+ k = sd_radv_stop(link->radv);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop IPv6 Router Advertisement: %m");
+
+ return r;
+}
+
+void link_enter_failed(Link *link) {
+ assert(link);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return;
+
+ log_link_warning(link, "Failed");
+
+ link_set_state(link, LINK_STATE_FAILED);
+
+ (void) link_stop_engines(link, false);
+}
+
+void link_check_ready(Link *link) {
+ Address *a;
+
+ assert(link);
+
+ if (link->state == LINK_STATE_CONFIGURED)
+ return;
+
+ if (link->state != LINK_STATE_CONFIGURING)
+ return (void) log_link_debug(link, "%s(): link is in %s state.", __func__, link_state_to_string(link->state));
+
+ if (!link->network)
+ return (void) log_link_debug(link, "%s(): link is unmanaged.", __func__);
+
+ if (!link->tc_configured)
+ return (void) log_link_debug(link, "%s(): traffic controls are not configured.", __func__);
+
+ if (link->set_link_messages > 0)
+ return (void) log_link_debug(link, "%s(): link layer is configuring.", __func__);
+
+ if (!link->activated)
+ return (void) log_link_debug(link, "%s(): link is not activated.", __func__);
+
+ if (link->iftype == ARPHRD_CAN) {
+ /* let's shortcut things for CAN which doesn't need most of checks below. */
+ link_set_state(link, LINK_STATE_CONFIGURED);
+ return;
+ }
+
+ if (!link->stacked_netdevs_created)
+ return (void) log_link_debug(link, "%s(): stacked netdevs are not created.", __func__);
+
+ if (!link->static_addresses_configured)
+ return (void) log_link_debug(link, "%s(): static addresses are not configured.", __func__);
+
+ SET_FOREACH(a, link->addresses)
+ if (!address_is_ready(a))
+ return (void) log_link_debug(link, "%s(): address %s is not ready.", __func__,
+ IN_ADDR_PREFIX_TO_STRING(a->family, &a->in_addr, a->prefixlen));
+
+ if (!link->static_address_labels_configured)
+ return (void) log_link_debug(link, "%s(): static address labels are not configured.", __func__);
+
+ if (!link->static_bridge_fdb_configured)
+ return (void) log_link_debug(link, "%s(): static bridge MDB entries are not configured.", __func__);
+
+ if (!link->static_bridge_mdb_configured)
+ return (void) log_link_debug(link, "%s(): static bridge MDB entries are not configured.", __func__);
+
+ if (!link->static_ipv6_proxy_ndp_configured)
+ return (void) log_link_debug(link, "%s(): static IPv6 proxy NDP addresses are not configured.", __func__);
+
+ if (!link->static_neighbors_configured)
+ return (void) log_link_debug(link, "%s(): static neighbors are not configured.", __func__);
+
+ if (!link->static_nexthops_configured)
+ return (void) log_link_debug(link, "%s(): static nexthops are not configured.", __func__);
+
+ if (!link->static_routes_configured)
+ return (void) log_link_debug(link, "%s(): static routes are not configured.", __func__);
+
+ if (!link->static_routing_policy_rules_configured)
+ return (void) log_link_debug(link, "%s(): static routing policy rules are not configured.", __func__);
+
+ if (!link->sr_iov_configured)
+ return (void) log_link_debug(link, "%s(): SR-IOV is not configured.", __func__);
+
+ /* IPv6LL is assigned after the link gains its carrier. */
+ if (!link->network->configure_without_carrier &&
+ link_ipv6ll_enabled(link) &&
+ !in6_addr_is_set(&link->ipv6ll_address))
+ return (void) log_link_debug(link, "%s(): IPv6LL is not configured yet.", __func__);
+
+ bool has_dynamic_address = false;
+ SET_FOREACH(a, link->addresses) {
+ if (address_is_marked(a))
+ continue;
+ if (!address_exists(a))
+ continue;
+ if (IN_SET(a->source,
+ NETWORK_CONFIG_SOURCE_IPV4LL,
+ NETWORK_CONFIG_SOURCE_DHCP4,
+ NETWORK_CONFIG_SOURCE_DHCP6,
+ NETWORK_CONFIG_SOURCE_DHCP_PD,
+ NETWORK_CONFIG_SOURCE_NDISC)) {
+ has_dynamic_address = true;
+ break;
+ }
+ }
+
+ if ((link_ipv4ll_enabled(link) || link_dhcp4_enabled(link) || link_dhcp6_with_address_enabled(link) ||
+ (link_dhcp_pd_is_enabled(link) && link->network->dhcp_pd_assign)) && !has_dynamic_address)
+ /* When DHCP[46] or IPv4LL is enabled, at least one address is acquired by them. */
+ return (void) log_link_debug(link, "%s(): DHCPv4, DHCPv6, DHCP-PD or IPv4LL is enabled but no dynamic address is assigned yet.", __func__);
+
+ /* Ignore NDisc when ConfigureWithoutCarrier= is enabled, as IPv6AcceptRA= is enabled by default. */
+ if (link_ipv4ll_enabled(link) || link_dhcp4_enabled(link) ||
+ link_dhcp6_enabled(link) || link_dhcp_pd_is_enabled(link) ||
+ (!link->network->configure_without_carrier && link_ipv6_accept_ra_enabled(link))) {
+
+ if (!link->ipv4ll_address_configured && !link->dhcp4_configured &&
+ !link->dhcp6_configured && !link->dhcp_pd_configured && !link->ndisc_configured)
+ /* When DHCP[46], NDisc, or IPv4LL is enabled, at least one protocol must be finished. */
+ return (void) log_link_debug(link, "%s(): dynamic addresses or routes are not configured.", __func__);
+
+ log_link_debug(link, "%s(): IPv4LL:%s DHCPv4:%s DHCPv6:%s DHCP-PD:%s NDisc:%s",
+ __func__,
+ yes_no(link->ipv4ll_address_configured),
+ yes_no(link->dhcp4_configured),
+ yes_no(link->dhcp6_configured),
+ yes_no(link->dhcp_pd_configured),
+ yes_no(link->ndisc_configured));
+ }
+
+ link_set_state(link, LINK_STATE_CONFIGURED);
+}
+
+static int link_request_static_configs(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state != _LINK_STATE_INVALID);
+
+ r = link_request_static_addresses(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_address_labels(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_bridge_fdb(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_bridge_mdb(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_ipv6_proxy_ndp_addresses(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_neighbors(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_nexthops(link, false);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_routes(link, false);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_routing_policy_rules(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int link_request_stacked_netdevs(Link *link) {
+ NetDev *netdev;
+ int r;
+
+ assert(link);
+
+ link->stacked_netdevs_created = false;
+
+ HASHMAP_FOREACH(netdev, link->network->stacked_netdevs) {
+ r = link_request_stacked_netdev(link, netdev);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->create_stacked_netdev_messages == 0) {
+ link->stacked_netdevs_created = true;
+ link_check_ready(link);
+ }
+
+ return 0;
+}
+
+static int link_acquire_dynamic_ipv6_conf(Link *link) {
+ int r;
+
+ assert(link);
+
+ r = radv_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start IPv6 Router Advertisement engine: %m");
+
+ r = ndisc_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m");
+
+ r = dhcp6_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start DHCPv6 client: %m");
+
+ return 0;
+}
+
+static int link_acquire_dynamic_ipv4_conf(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->event);
+
+ if (link->dhcp_client) {
+ r = dhcp4_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start DHCPv4 client: %m");
+
+ log_link_debug(link, "Acquiring DHCPv4 lease.");
+
+ } else if (link->ipv4ll) {
+ if (in4_addr_is_set(&link->network->ipv4ll_start_address)) {
+ r = sd_ipv4ll_set_address(link->ipv4ll, &link->network->ipv4ll_start_address);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set IPv4 link-local start address: %m");
+ }
+
+ r = sd_ipv4ll_start(link->ipv4ll);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m");
+
+ log_link_debug(link, "Acquiring IPv4 link-local address.");
+ }
+
+ if (link->dhcp_server) {
+ r = sd_dhcp_server_start(link->dhcp_server);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not start DHCP server: %m");
+ }
+
+ r = ipv4acd_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not start IPv4 ACD client: %m");
+
+ return 0;
+}
+
+static int link_acquire_dynamic_conf(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ r = link_acquire_dynamic_ipv4_conf(link);
+ if (r < 0)
+ return r;
+
+ if (in6_addr_is_set(&link->ipv6ll_address)) {
+ r = link_acquire_dynamic_ipv6_conf(link);
+ if (r < 0)
+ return r;
+ }
+
+ if (!link_radv_enabled(link) || !link->network->dhcp_pd_announce) {
+ /* DHCPv6PD downstream does not require IPv6LL address. But may require RADV to be
+ * configured, and RADV may not be configured yet here. Only acquire subnet prefix when
+ * RADV is disabled, or the announcement of the prefix is disabled. Otherwise, the
+ * below will be called in radv_start(). */
+ r = dhcp_request_prefix_delegation(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request DHCP delegated subnet prefix: %m");
+ }
+
+ if (link->lldp_tx) {
+ r = sd_lldp_tx_start(link->lldp_tx);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start LLDP transmission: %m");
+ }
+
+ if (link->lldp_rx) {
+ r = sd_lldp_rx_start(link->lldp_rx);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start LLDP client: %m");
+ }
+
+ return 0;
+}
+
+int link_ipv6ll_gained(Link *link) {
+ int r;
+
+ assert(link);
+
+ log_link_info(link, "Gained IPv6LL");
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return 0;
+
+ r = link_acquire_dynamic_ipv6_conf(link);
+ if (r < 0)
+ return r;
+
+ link_check_ready(link);
+ return 0;
+}
+
+int link_handle_bound_to_list(Link *link) {
+ bool required_up = false;
+ bool link_is_up = false;
+ Link *l;
+
+ assert(link);
+
+ /* If at least one interface in bound_to_links has carrier, then make this interface up.
+ * If all interfaces in bound_to_links do not, then make this interface down. */
+
+ if (hashmap_isempty(link->bound_to_links))
+ return 0;
+
+ if (link->flags & IFF_UP)
+ link_is_up = true;
+
+ HASHMAP_FOREACH(l, link->bound_to_links)
+ if (link_has_carrier(l)) {
+ required_up = true;
+ break;
+ }
+
+ if (!required_up && link_is_up)
+ return link_request_to_bring_up_or_down(link, /* up = */ false);
+ if (required_up && !link_is_up)
+ return link_request_to_bring_up_or_down(link, /* up = */ true);
+
+ return 0;
+}
+
+static int link_handle_bound_by_list(Link *link) {
+ Link *l;
+ int r;
+
+ assert(link);
+
+ /* Update up or down state of interfaces which depend on this interface's carrier state. */
+
+ if (hashmap_isempty(link->bound_by_links))
+ return 0;
+
+ HASHMAP_FOREACH(l, link->bound_by_links) {
+ r = link_handle_bound_to_list(l);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int link_put_carrier(Link *link, Link *carrier, Hashmap **h) {
+ int r;
+
+ assert(link);
+ assert(carrier);
+
+ if (link == carrier)
+ return 0;
+
+ if (hashmap_get(*h, INT_TO_PTR(carrier->ifindex)))
+ return 0;
+
+ r = hashmap_ensure_put(h, NULL, INT_TO_PTR(carrier->ifindex), carrier);
+ if (r < 0)
+ return r;
+
+ link_dirty(link);
+
+ return 0;
+}
+
+static int link_new_bound_by_list(Link *link) {
+ Manager *m;
+ Link *carrier;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ m = link->manager;
+
+ HASHMAP_FOREACH(carrier, m->links_by_index) {
+ if (!carrier->network)
+ continue;
+
+ if (strv_isempty(carrier->network->bind_carrier))
+ continue;
+
+ if (strv_fnmatch(carrier->network->bind_carrier, link->ifname)) {
+ r = link_put_carrier(link, carrier, &link->bound_by_links);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ HASHMAP_FOREACH(carrier, link->bound_by_links) {
+ r = link_put_carrier(carrier, link, &carrier->bound_to_links);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int link_new_bound_to_list(Link *link) {
+ Manager *m;
+ Link *carrier;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ if (!link->network)
+ return 0;
+
+ if (strv_isempty(link->network->bind_carrier))
+ return 0;
+
+ m = link->manager;
+
+ HASHMAP_FOREACH(carrier, m->links_by_index) {
+ if (strv_fnmatch(link->network->bind_carrier, carrier->ifname)) {
+ r = link_put_carrier(link, carrier, &link->bound_to_links);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ HASHMAP_FOREACH(carrier, link->bound_to_links) {
+ r = link_put_carrier(carrier, link, &carrier->bound_by_links);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static void link_free_bound_to_list(Link *link) {
+ bool updated = false;
+ Link *bound_to;
+
+ assert(link);
+
+ while ((bound_to = hashmap_steal_first(link->bound_to_links))) {
+ updated = true;
+
+ if (hashmap_remove(bound_to->bound_by_links, INT_TO_PTR(link->ifindex)))
+ link_dirty(bound_to);
+ }
+
+ if (updated)
+ link_dirty(link);
+}
+
+static void link_free_bound_by_list(Link *link) {
+ bool updated = false;
+ Link *bound_by;
+
+ assert(link);
+
+ while ((bound_by = hashmap_steal_first(link->bound_by_links))) {
+ updated = true;
+
+ if (hashmap_remove(bound_by->bound_to_links, INT_TO_PTR(link->ifindex))) {
+ link_dirty(bound_by);
+ link_handle_bound_to_list(bound_by);
+ }
+ }
+
+ if (updated)
+ link_dirty(link);
+}
+
+static int link_append_to_master(Link *link) {
+ Link *master;
+ int r;
+
+ assert(link);
+
+ /* - The link may have no master.
+ * - RTM_NEWLINK message about master interface may not be received yet. */
+ if (link_get_master(link, &master) < 0)
+ return 0;
+
+ r = set_ensure_put(&master->slaves, NULL, link);
+ if (r <= 0)
+ return r;
+
+ link_ref(link);
+ return 0;
+}
+
+static void link_drop_from_master(Link *link) {
+ Link *master;
+
+ assert(link);
+
+ if (!link->manager)
+ return;
+
+ if (link_get_master(link, &master) < 0)
+ return;
+
+ link_unref(set_remove(master->slaves, link));
+}
+
+static void link_drop_requests(Link *link) {
+ Request *req;
+
+ assert(link);
+ assert(link->manager);
+
+ ORDERED_SET_FOREACH(req, link->manager->request_queue)
+ if (req->link == link)
+ request_detach(link->manager, req);
+}
+
+static Link *link_drop(Link *link) {
+ if (!link)
+ return NULL;
+
+ assert(link->manager);
+
+ link_set_state(link, LINK_STATE_LINGER);
+
+ /* Drop all references from other links and manager. Note that async netlink calls may have
+ * references to the link, and they will be dropped when we receive replies. */
+
+ link_drop_requests(link);
+
+ link_free_bound_to_list(link);
+ link_free_bound_by_list(link);
+
+ link_clear_sr_iov_ifindices(link);
+
+ link_drop_from_master(link);
+
+ if (link->state_file)
+ (void) unlink(link->state_file);
+
+ link_clean(link);
+
+ STRV_FOREACH(n, link->alternative_names)
+ hashmap_remove(link->manager->links_by_name, *n);
+ hashmap_remove(link->manager->links_by_name, link->ifname);
+
+ /* bonding master and its slaves have the same hardware address. */
+ hashmap_remove_value(link->manager->links_by_hw_addr, &link->hw_addr, link);
+
+ /* The following must be called at last. */
+ assert_se(hashmap_remove(link->manager->links_by_index, INT_TO_PTR(link->ifindex)) == link);
+ return link_unref(link);
+}
+
+static int link_drop_foreign_config(Link *link) {
+ int k, r;
+
+ assert(link);
+ assert(link->manager);
+
+ /* Drop foreign config, but ignore unmanaged, loopback, or critical interfaces. We do not want
+ * to remove loopback address or addresses used for root NFS. */
+
+ if (IN_SET(link->state, LINK_STATE_UNMANAGED, LINK_STATE_PENDING, LINK_STATE_INITIALIZED))
+ return 0;
+ if (FLAGS_SET(link->flags, IFF_LOOPBACK))
+ return 0;
+ if (link->network->keep_configuration == KEEP_CONFIGURATION_YES)
+ return 0;
+
+ r = link_drop_foreign_routes(link);
+
+ k = link_drop_foreign_nexthops(link);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ k = link_drop_foreign_addresses(link);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ k = link_drop_foreign_neighbors(link);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ k = manager_drop_foreign_routing_policy_rules(link->manager);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ return r;
+}
+
+static int link_drop_managed_config(Link *link) {
+ int k, r;
+
+ assert(link);
+ assert(link->manager);
+
+ r = link_drop_managed_routes(link);
+
+ k = link_drop_managed_nexthops(link);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ k = link_drop_managed_addresses(link);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ k = link_drop_managed_neighbors(link);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ k = link_drop_managed_routing_policy_rules(link);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ return r;
+}
+
+static void link_foreignize_config(Link *link) {
+ assert(link);
+ assert(link->manager);
+
+ link_foreignize_routes(link);
+ link_foreignize_nexthops(link);
+ link_foreignize_addresses(link);
+ link_foreignize_neighbors(link);
+ link_foreignize_routing_policy_rules(link);
+}
+
+static int link_configure(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state == LINK_STATE_INITIALIZED);
+
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ r = link_new_bound_to_list(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_traffic_control(link);
+ if (r < 0)
+ return r;
+
+ r = link_configure_mtu(link);
+ if (r < 0)
+ return r;
+
+ if (link->iftype == ARPHRD_CAN) {
+ /* let's shortcut things for CAN which doesn't need most of what's done below. */
+ r = link_request_to_set_can(link);
+ if (r < 0)
+ return r;
+
+ return link_request_to_activate(link);
+ }
+
+ r = link_request_sr_iov_vfs(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_sysctl(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_mac(link, /* allow_retry = */ true);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_ipoib(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_flags(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_group(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_addrgen_mode(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_master(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_stacked_netdevs(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_bond(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_bridge(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_set_bridge_vlan(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_to_activate(link);
+ if (r < 0)
+ return r;
+
+ r = ipv4ll_configure(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_dhcp4_client(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_dhcp6_client(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_ndisc(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_dhcp_server(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_radv(link);
+ if (r < 0)
+ return r;
+
+ r = link_lldp_rx_configure(link);
+ if (r < 0)
+ return r;
+
+ r = link_lldp_tx_configure(link);
+ if (r < 0)
+ return r;
+
+ r = link_drop_foreign_config(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_configs(link);
+ if (r < 0)
+ return r;
+
+ if (!link_has_carrier(link))
+ return 0;
+
+ return link_acquire_dynamic_conf(link);
+}
+
+static int link_get_network(Link *link, Network **ret) {
+ Network *network;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(ret);
+
+ ORDERED_HASHMAP_FOREACH(network, link->manager->networks) {
+ bool warn = false;
+
+ r = net_match_config(
+ &network->match,
+ link->dev,
+ &link->hw_addr,
+ &link->permanent_hw_addr,
+ link->driver,
+ link->iftype,
+ link->kind,
+ link->ifname,
+ link->alternative_names,
+ link->wlan_iftype,
+ link->ssid,
+ &link->bssid);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ if (network->match.ifname && link->dev) {
+ uint8_t name_assign_type = NET_NAME_UNKNOWN;
+ const char *attr;
+
+ if (sd_device_get_sysattr_value(link->dev, "name_assign_type", &attr) >= 0)
+ (void) safe_atou8(attr, &name_assign_type);
+
+ warn = name_assign_type == NET_NAME_ENUM;
+ }
+
+ log_link_full(link, warn ? LOG_WARNING : LOG_DEBUG,
+ "found matching network '%s'%s.",
+ network->filename,
+ warn ? ", based on potentially unpredictable interface name" : "");
+
+ if (network->unmanaged)
+ return -ENOENT;
+
+ *ret = network;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+int link_reconfigure_impl(Link *link, bool force) {
+ Network *network = NULL;
+ NetDev *netdev = NULL;
+ int r;
+
+ assert(link);
+
+ if (!IN_SET(link->state, LINK_STATE_INITIALIZED, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED, LINK_STATE_UNMANAGED))
+ return 0;
+
+ r = netdev_get(link->manager, link->ifname, &netdev);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ r = link_get_network(link, &network);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ if (link->state != LINK_STATE_UNMANAGED && !network)
+ /* If link is in initialized state, then link->network is also NULL. */
+ force = true;
+
+ if (link->network == network && !force)
+ return 0;
+
+ if (network) {
+ if (link->state == LINK_STATE_INITIALIZED)
+ log_link_info(link, "Configuring with %s.", network->filename);
+ else
+ log_link_info(link, "Reconfiguring with %s.", network->filename);
+ } else
+ log_link_full(link, link->state == LINK_STATE_INITIALIZED ? LOG_DEBUG : LOG_INFO,
+ "Unmanaging interface.");
+
+ /* Dropping old .network file */
+ r = link_stop_engines(link, false);
+ if (r < 0)
+ return r;
+
+ link_drop_requests(link);
+
+ if (network && !force && network->keep_configuration != KEEP_CONFIGURATION_YES)
+ /* When a new/updated .network file is assigned, first make all configs (addresses,
+ * routes, and so on) foreign, and then drop unnecessary configs later by
+ * link_drop_foreign_config() in link_configure().
+ * Note, when KeepConfiguration=yes, link_drop_foreign_config() does nothing. Hence,
+ * here we need to drop the configs such as addresses, routes, and so on configured by
+ * the previously assigned .network file. */
+ link_foreignize_config(link);
+ else {
+ /* Remove all managed configs. Note, foreign configs are removed in later by
+ * link_configure() -> link_drop_foreign_config() if the link is managed by us. */
+ r = link_drop_managed_config(link);
+ if (r < 0)
+ return r;
+ }
+
+ /* The bound_to map depends on .network file, hence it needs to be freed. But, do not free the
+ * bound_by map. Otherwise, if a link enters unmanaged state below, then its carrier state will
+ * not propagated to other interfaces anymore. Moreover, it is not necessary to recreate the
+ * map here, as it depends on .network files assigned to other links. */
+ link_free_bound_to_list(link);
+
+ link_free_engines(link);
+ link->network = network_unref(link->network);
+
+ netdev_unref(link->netdev);
+ link->netdev = netdev_ref(netdev);
+
+ if (!network) {
+ link_set_state(link, LINK_STATE_UNMANAGED);
+ return 0;
+ }
+
+ /* Then, apply new .network file */
+ link->network = network_ref(network);
+ link_update_operstate(link, true);
+ link_dirty(link);
+
+ link_set_state(link, LINK_STATE_INITIALIZED);
+ link->activated = false;
+
+ r = link_configure(link);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int link_reconfigure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, bool force) {
+ int r;
+
+ assert(link);
+
+ r = link_getlink_handler_internal(rtnl, m, link, "Failed to update link state");
+ if (r <= 0)
+ return r;
+
+ r = link_reconfigure_impl(link, force);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 0;
+ }
+
+ return r;
+}
+
+static int link_reconfigure_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ return link_reconfigure_handler_internal(rtnl, m, link, /* force = */ false);
+}
+
+static int link_force_reconfigure_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ return link_reconfigure_handler_internal(rtnl, m, link, /* force = */ true);
+}
+
+static int link_reconfigure_after_sleep_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+
+ r = link_reconfigure_handler_internal(rtnl, m, link, /* force = */ false);
+ if (r != 0)
+ return r;
+
+ /* r == 0 means an error occurs, the link is unmanaged, or the matching network file is unchanged. */
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return 0;
+
+ /* re-request static configs, and restart engines. */
+ r = link_stop_engines(link, false);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 0;
+ }
+
+ r = link_acquire_dynamic_conf(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 0;
+ }
+
+ r = link_request_static_configs(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 0;
+ }
+
+ return 0;
+}
+
+static int link_reconfigure_internal(Link *link, link_netlink_message_handler_t callback) {
+ int r;
+
+ assert(link);
+ assert(callback);
+
+ /* When link in pending or initialized state, then link_configure() will be called. To prevent
+ * the function from being called multiple times simultaneously, refuse to reconfigure the
+ * interface in these cases. */
+ if (IN_SET(link->state, LINK_STATE_PENDING, LINK_STATE_INITIALIZED, LINK_STATE_LINGER))
+ return 0; /* 0 means no-op. */
+
+ r = link_call_getlink(link, callback);
+ if (r < 0)
+ return r;
+
+ return 1; /* 1 means the interface will be reconfigured. */
+}
+
+int link_reconfigure(Link *link, bool force) {
+ return link_reconfigure_internal(link, force ? link_force_reconfigure_handler : link_reconfigure_handler);
+}
+
+int link_reconfigure_after_sleep(Link *link) {
+ return link_reconfigure_internal(link, link_reconfigure_after_sleep_handler);
+}
+
+static int link_initialized_and_synced(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ if (link->manager->test_mode) {
+ log_link_debug(link, "Running in test mode, refusing to enter initialized state.");
+ link_set_state(link, LINK_STATE_UNMANAGED);
+ return 0;
+ }
+
+ /* This may get called either from the asynchronous netlink callback,
+ * or directly from link_check_initialized() if running in a container. */
+ if (!IN_SET(link->state, LINK_STATE_PENDING, LINK_STATE_INITIALIZED))
+ return 0;
+
+ log_link_debug(link, "Link state is up-to-date");
+ link_set_state(link, LINK_STATE_INITIALIZED);
+
+ r = link_new_bound_by_list(link);
+ if (r < 0)
+ return r;
+
+ r = link_handle_bound_by_list(link);
+ if (r < 0)
+ return r;
+
+ return link_reconfigure_impl(link, /* force = */ false);
+}
+
+static int link_initialized_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ r = link_getlink_handler_internal(rtnl, m, link, "Failed to wait for the interface to be initialized");
+ if (r <= 0)
+ return r;
+
+ r = link_initialized_and_synced(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 0;
+}
+
+static int link_initialized(Link *link, sd_device *device) {
+ int r;
+
+ assert(link);
+ assert(device);
+
+ /* Always replace with the new sd_device object. As the sysname (and possibly other properties
+ * or sysattrs) may be outdated. */
+ device_unref_and_replace(link->dev, device);
+
+ r = link_set_sr_iov_ifindices(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to manage SR-IOV PF and VF ports, ignoring: %m");
+
+ /* Do not ignore unamanaged state case here. If an interface is renamed after being once
+ * configured, and the corresponding .network file has Name= in [Match] section, then the
+ * interface may be already in unmanaged state. See #20657. */
+ if (!IN_SET(link->state, LINK_STATE_PENDING, LINK_STATE_UNMANAGED))
+ return 0;
+
+ log_link_debug(link, "udev initialized link");
+ link_set_state(link, LINK_STATE_INITIALIZED);
+
+ /* udev has initialized the link, but we don't know if we have yet
+ * processed the NEWLINK messages with the latest state. Do a GETLINK,
+ * when it returns we know that the pending NEWLINKs have already been
+ * processed and that we are up-to-date */
+
+ return link_call_getlink(link, link_initialized_handler);
+}
+
+static int link_check_initialized(Link *link) {
+ _cleanup_(sd_device_unrefp) sd_device *device = NULL;
+ int r;
+
+ assert(link);
+
+ if (!udev_available())
+ return link_initialized_and_synced(link);
+
+ /* udev should be around */
+ r = sd_device_new_from_ifindex(&device, link->ifindex);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "Could not find device, waiting for device initialization: %m");
+ return 0;
+ }
+
+ r = sd_device_get_is_initialized(device);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not determine whether the device is initialized: %m");
+ if (r == 0) {
+ /* not yet ready */
+ log_link_debug(link, "link pending udev initialization...");
+ return 0;
+ }
+
+ r = device_is_renaming(device);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to determine the device is being renamed: %m");
+ if (r > 0) {
+ log_link_debug(link, "Interface is being renamed, pending initialization.");
+ return 0;
+ }
+
+ return link_initialized(link, device);
+}
+
+int manager_udev_process_link(Manager *m, sd_device *device, sd_device_action_t action) {
+ int r, ifindex;
+ Link *link;
+
+ assert(m);
+ assert(device);
+
+ r = sd_device_get_ifindex(device, &ifindex);
+ if (r < 0)
+ return log_device_debug_errno(device, r, "Failed to get ifindex: %m");
+
+ r = link_get_by_index(m, ifindex, &link);
+ if (r < 0) {
+ /* This error is not critical, as the corresponding rtnl message may be received later. */
+ log_device_debug_errno(device, r, "Failed to get link from ifindex %i, ignoring: %m", ifindex);
+ return 0;
+ }
+
+ /* Let's unref the sd-device object assigned to the corresponding Link object, but keep the Link
+ * object here. It will be removed only when rtnetlink says so. */
+ if (action == SD_DEVICE_REMOVE) {
+ link->dev = sd_device_unref(link->dev);
+ return 0;
+ }
+
+ r = device_is_renaming(device);
+ if (r < 0)
+ return log_device_debug_errno(device, r, "Failed to determine if the device is renaming or not: %m");
+ if (r > 0) {
+ log_device_debug(device, "Device is renaming, waiting for the interface to be renamed.");
+ /* TODO:
+ * What happens when a device is initialized, then soon renamed after that? When we detect
+ * such, maybe we should cancel or postpone all queued requests for the interface. */
+ return 0;
+ }
+
+ r = link_initialized(link, device);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 0;
+}
+
+static int link_carrier_gained(Link *link) {
+ bool force_reconfigure;
+ int r;
+
+ assert(link);
+
+ r = event_source_disable(link->carrier_lost_timer);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to disable carrier lost timer, ignoring: %m");
+
+ /* If a wireless interface was connected to an access point, and the SSID is changed (that is,
+ * both previous_ssid and ssid are non-NULL), then the connected wireless network could be
+ * changed. So, always reconfigure the link. Which means e.g. the DHCP client will be
+ * restarted, and the correct network information will be gained.
+ *
+ * However, do not reconfigure the wireless interface forcibly if it was not connected to any
+ * access points previously (previous_ssid is NULL in this case). As, a .network file may be
+ * already assigned to the interface (in that case, the .network file does not have the SSID=
+ * setting in the [Match] section), and the interface is already being configured. Of course,
+ * there may exist another .network file with higher priority and a matching SSID= setting. But
+ * in that case, link_reconfigure_impl() can handle that without the force_reconfigure flag.
+ *
+ * For non-wireless interfaces, we have no way to detect the connected network change. So,
+ * setting force_reconfigure = false. Note, both ssid and previous_ssid are NULL in that case. */
+ force_reconfigure = link->previous_ssid && !streq_ptr(link->previous_ssid, link->ssid);
+ link->previous_ssid = mfree(link->previous_ssid);
+
+ if (!IN_SET(link->state, LINK_STATE_PENDING, LINK_STATE_FAILED, LINK_STATE_LINGER)) {
+ /* At this stage, both wlan and link information should be up-to-date. Hence,
+ * it is not necessary to call RTM_GETLINK, NL80211_CMD_GET_INTERFACE, or
+ * NL80211_CMD_GET_STATION commands, and simply call link_reconfigure_impl().
+ * Note, link_reconfigure_impl() returns 1 when the link is reconfigured. */
+ r = link_reconfigure_impl(link, force_reconfigure);
+ if (r != 0)
+ return r;
+ }
+
+ r = link_handle_bound_by_list(link);
+ if (r < 0)
+ return r;
+
+ if (link->iftype == ARPHRD_CAN)
+ /* let's shortcut things for CAN which doesn't need most of what's done below. */
+ return 0;
+
+ if (IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) {
+ r = link_acquire_dynamic_conf(link);
+ if (r < 0)
+ return r;
+
+ r = link_request_static_configs(link);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int link_carrier_lost_impl(Link *link) {
+ int r, ret = 0;
+
+ assert(link);
+
+ link->previous_ssid = mfree(link->previous_ssid);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ r = link_stop_engines(link, false);
+ if (r < 0)
+ ret = r;
+
+ r = link_drop_managed_config(link);
+ if (r < 0 && ret >= 0)
+ ret = r;
+
+ return ret;
+}
+
+static int link_carrier_lost_handler(sd_event_source *s, uint64_t usec, void *userdata) {
+ Link *link = ASSERT_PTR(userdata);
+ int r;
+
+ r = link_carrier_lost_impl(link);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to process carrier lost event: %m");
+ link_enter_failed(link);
+ }
+
+ return 0;
+}
+
+static int link_carrier_lost(Link *link) {
+ uint16_t dhcp_mtu;
+ usec_t usec;
+ int r;
+
+ assert(link);
+
+ r = link_handle_bound_by_list(link);
+ if (r < 0)
+ return r;
+
+ if (link->iftype == ARPHRD_CAN)
+ /* let's shortcut things for CAN which doesn't need most of what's done below. */
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->ignore_carrier_loss_set)
+ /* If IgnoreCarrierLoss= is explicitly specified, then use the specified value. */
+ usec = link->network->ignore_carrier_loss_usec;
+
+ else if (link->network->bond && link->wlan_iftype > 0)
+ /* Enslaving wlan interface to a bond disconnects from the connected AP, and causes its
+ * carrier to be lost. See #19832. */
+ usec = 3 * USEC_PER_SEC;
+
+ else if (link->network->dhcp_use_mtu &&
+ link->dhcp_lease &&
+ sd_dhcp_lease_get_mtu(link->dhcp_lease, &dhcp_mtu) >= 0 &&
+ dhcp_mtu != link->original_mtu)
+ /* Some drivers reset interfaces when changing MTU. Resetting interfaces by the static
+ * MTU should not cause any issues, as MTU is changed only once. However, setting MTU
+ * through DHCP lease causes an infinite loop of resetting the interface. See #18738. */
+ usec = 5 * USEC_PER_SEC;
+
+ else
+ /* Otherwise, use the implied default value. */
+ usec = link->network->ignore_carrier_loss_usec;
+
+ if (usec == USEC_INFINITY)
+ return 0;
+
+ if (usec == 0)
+ return link_carrier_lost_impl(link);
+
+ return event_reset_time_relative(link->manager->event,
+ &link->carrier_lost_timer,
+ CLOCK_BOOTTIME,
+ usec,
+ 0,
+ link_carrier_lost_handler,
+ link,
+ 0,
+ "link-carrier-loss",
+ true);
+}
+
+static int link_admin_state_up(Link *link) {
+ int r;
+
+ assert(link);
+
+ /* This is called every time an interface admin state changes to up;
+ * specifically, when IFF_UP flag changes from unset to set. */
+
+ if (!link->network)
+ return 0;
+
+ if (link->activated && link->network->activation_policy == ACTIVATION_POLICY_ALWAYS_DOWN) {
+ log_link_info(link, "Activation policy is \"always-down\", forcing link down.");
+ return link_request_to_bring_up_or_down(link, /* up = */ false);
+ }
+
+ /* We set the ipv6 mtu after the device mtu, but the kernel resets
+ * ipv6 mtu on NETDEV_UP, so we need to reset it. */
+ r = link_set_ipv6_mtu(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv6 MTU, ignoring: %m");
+
+ return 0;
+}
+
+static int link_admin_state_down(Link *link) {
+ assert(link);
+
+ if (!link->network)
+ return 0;
+
+ if (link->activated && link->network->activation_policy == ACTIVATION_POLICY_ALWAYS_UP) {
+ log_link_info(link, "Activation policy is \"always-up\", forcing link up.");
+ return link_request_to_bring_up_or_down(link, /* up = */ true);
+ }
+
+ return 0;
+}
+
+static bool link_is_enslaved(Link *link) {
+ if (link->flags & IFF_SLAVE)
+ return true;
+
+ if (link->master_ifindex > 0)
+ return true;
+
+ return false;
+}
+
+static LinkAddressState address_state_from_scope(uint8_t scope) {
+ if (scope < RT_SCOPE_SITE)
+ /* universally accessible addresses found */
+ return LINK_ADDRESS_STATE_ROUTABLE;
+
+ if (scope < RT_SCOPE_HOST)
+ /* only link or site local addresses found */
+ return LINK_ADDRESS_STATE_DEGRADED;
+
+ /* no useful addresses found */
+ return LINK_ADDRESS_STATE_OFF;
+}
+
+void link_update_operstate(Link *link, bool also_update_master) {
+ LinkOperationalState operstate;
+ LinkCarrierState carrier_state;
+ LinkAddressState ipv4_address_state, ipv6_address_state, address_state;
+ LinkOnlineState online_state;
+ _cleanup_strv_free_ char **p = NULL;
+ uint8_t ipv4_scope = RT_SCOPE_NOWHERE, ipv6_scope = RT_SCOPE_NOWHERE;
+ bool changed = false;
+ Address *address;
+
+ assert(link);
+
+ if (link->kernel_operstate == IF_OPER_DORMANT)
+ carrier_state = LINK_CARRIER_STATE_DORMANT;
+ else if (link_has_carrier(link)) {
+ if (link_is_enslaved(link))
+ carrier_state = LINK_CARRIER_STATE_ENSLAVED;
+ else
+ carrier_state = LINK_CARRIER_STATE_CARRIER;
+ } else if (link->flags & IFF_UP)
+ carrier_state = LINK_CARRIER_STATE_NO_CARRIER;
+ else
+ carrier_state = LINK_CARRIER_STATE_OFF;
+
+ if (carrier_state >= LINK_CARRIER_STATE_CARRIER) {
+ Link *slave;
+
+ SET_FOREACH(slave, link->slaves) {
+ link_update_operstate(slave, false);
+
+ if (slave->carrier_state < LINK_CARRIER_STATE_CARRIER)
+ carrier_state = LINK_CARRIER_STATE_DEGRADED_CARRIER;
+ }
+ }
+
+ SET_FOREACH(address, link->addresses) {
+ if (!address_is_ready(address))
+ continue;
+
+ if (address->family == AF_INET)
+ ipv4_scope = MIN(ipv4_scope, address->scope);
+
+ if (address->family == AF_INET6)
+ ipv6_scope = MIN(ipv6_scope, address->scope);
+ }
+
+ ipv4_address_state = address_state_from_scope(ipv4_scope);
+ ipv6_address_state = address_state_from_scope(ipv6_scope);
+ address_state = address_state_from_scope(MIN(ipv4_scope, ipv6_scope));
+
+ /* Mapping of address and carrier state vs operational state
+ * carrier state
+ * | off | no-carrier | dormant | degraded-carrier | carrier | enslaved
+ * ------------------------------------------------------------------------------
+ * off | off | no-carrier | dormant | degraded-carrier | carrier | enslaved
+ * address_state degraded | off | no-carrier | dormant | degraded | degraded | enslaved
+ * routable | off | no-carrier | dormant | routable | routable | routable
+ */
+
+ if (carrier_state == LINK_CARRIER_STATE_DEGRADED_CARRIER && address_state == LINK_ADDRESS_STATE_ROUTABLE)
+ operstate = LINK_OPERSTATE_ROUTABLE;
+ else if (carrier_state == LINK_CARRIER_STATE_DEGRADED_CARRIER && address_state == LINK_ADDRESS_STATE_DEGRADED)
+ operstate = LINK_OPERSTATE_DEGRADED;
+ else if (carrier_state < LINK_CARRIER_STATE_CARRIER || address_state == LINK_ADDRESS_STATE_OFF)
+ operstate = (LinkOperationalState) carrier_state;
+ else if (address_state == LINK_ADDRESS_STATE_ROUTABLE)
+ operstate = LINK_OPERSTATE_ROUTABLE;
+ else if (carrier_state == LINK_CARRIER_STATE_CARRIER)
+ operstate = LINK_OPERSTATE_DEGRADED;
+ else
+ operstate = LINK_OPERSTATE_ENSLAVED;
+
+ /* Only determine online state for managed links with RequiredForOnline=yes */
+ if (!link->network || !link->network->required_for_online)
+ online_state = _LINK_ONLINE_STATE_INVALID;
+ else if (operstate < link->network->required_operstate_for_online.min ||
+ operstate > link->network->required_operstate_for_online.max)
+ online_state = LINK_ONLINE_STATE_OFFLINE;
+ else {
+ AddressFamily required_family = link->network->required_family_for_online;
+ bool needs_ipv4 = required_family & ADDRESS_FAMILY_IPV4;
+ bool needs_ipv6 = required_family & ADDRESS_FAMILY_IPV6;
+
+ /* The operational state is within the range required for online.
+ * If a particular address family is also required, we might revert
+ * to offline in the blocks below. */
+ online_state = LINK_ONLINE_STATE_ONLINE;
+
+ if (link->network->required_operstate_for_online.min >= LINK_OPERSTATE_DEGRADED) {
+ if (needs_ipv4 && ipv4_address_state < LINK_ADDRESS_STATE_DEGRADED)
+ online_state = LINK_ONLINE_STATE_OFFLINE;
+ if (needs_ipv6 && ipv6_address_state < LINK_ADDRESS_STATE_DEGRADED)
+ online_state = LINK_ONLINE_STATE_OFFLINE;
+ }
+
+ if (link->network->required_operstate_for_online.min >= LINK_OPERSTATE_ROUTABLE) {
+ if (needs_ipv4 && ipv4_address_state < LINK_ADDRESS_STATE_ROUTABLE)
+ online_state = LINK_ONLINE_STATE_OFFLINE;
+ if (needs_ipv6 && ipv6_address_state < LINK_ADDRESS_STATE_ROUTABLE)
+ online_state = LINK_ONLINE_STATE_OFFLINE;
+ }
+ }
+
+ if (link->carrier_state != carrier_state) {
+ link->carrier_state = carrier_state;
+ changed = true;
+ if (strv_extend(&p, "CarrierState") < 0)
+ log_oom();
+ }
+
+ if (link->address_state != address_state) {
+ link->address_state = address_state;
+ changed = true;
+ if (strv_extend(&p, "AddressState") < 0)
+ log_oom();
+ }
+
+ if (link->ipv4_address_state != ipv4_address_state) {
+ link->ipv4_address_state = ipv4_address_state;
+ changed = true;
+ if (strv_extend(&p, "IPv4AddressState") < 0)
+ log_oom();
+ }
+
+ if (link->ipv6_address_state != ipv6_address_state) {
+ link->ipv6_address_state = ipv6_address_state;
+ changed = true;
+ if (strv_extend(&p, "IPv6AddressState") < 0)
+ log_oom();
+ }
+
+ if (link->operstate != operstate) {
+ link->operstate = operstate;
+ changed = true;
+ if (strv_extend(&p, "OperationalState") < 0)
+ log_oom();
+ }
+
+ if (link->online_state != online_state) {
+ link->online_state = online_state;
+ changed = true;
+ if (strv_extend(&p, "OnlineState") < 0)
+ log_oom();
+ }
+
+ if (p)
+ link_send_changed_strv(link, p);
+ if (changed)
+ link_dirty(link);
+
+ if (also_update_master) {
+ Link *master;
+
+ if (link_get_master(link, &master) >= 0)
+ link_update_operstate(master, true);
+ }
+}
+
+#define FLAG_STRING(string, flag, old, new) \
+ (((old ^ new) & flag) \
+ ? ((old & flag) ? (" -" string) : (" +" string)) \
+ : "")
+
+static int link_update_flags(Link *link, sd_netlink_message *message) {
+ bool link_was_admin_up, had_carrier;
+ uint8_t operstate;
+ unsigned flags;
+ int r;
+
+ assert(link);
+ assert(message);
+
+ r = sd_rtnl_message_link_get_flags(message, &flags);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "rtnl: failed to read link flags: %m");
+
+ r = sd_netlink_message_read_u8(message, IFLA_OPERSTATE, &operstate);
+ if (r == -ENODATA)
+ /* If we got a message without operstate, assume the state was unchanged. */
+ operstate = link->kernel_operstate;
+ else if (r < 0)
+ return log_link_debug_errno(link, r, "rtnl: failed to read operational state: %m");
+
+ if (link->flags == flags && link->kernel_operstate == operstate)
+ return 0;
+
+ if (link->flags != flags) {
+ unsigned unknown_flags, unknown_flags_added, unknown_flags_removed;
+
+ log_link_debug(link, "Flags change:%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
+ FLAG_STRING("LOOPBACK", IFF_LOOPBACK, link->flags, flags),
+ FLAG_STRING("MASTER", IFF_MASTER, link->flags, flags),
+ FLAG_STRING("SLAVE", IFF_SLAVE, link->flags, flags),
+ FLAG_STRING("UP", IFF_UP, link->flags, flags),
+ FLAG_STRING("DORMANT", IFF_DORMANT, link->flags, flags),
+ FLAG_STRING("LOWER_UP", IFF_LOWER_UP, link->flags, flags),
+ FLAG_STRING("RUNNING", IFF_RUNNING, link->flags, flags),
+ FLAG_STRING("MULTICAST", IFF_MULTICAST, link->flags, flags),
+ FLAG_STRING("BROADCAST", IFF_BROADCAST, link->flags, flags),
+ FLAG_STRING("POINTOPOINT", IFF_POINTOPOINT, link->flags, flags),
+ FLAG_STRING("PROMISC", IFF_PROMISC, link->flags, flags),
+ FLAG_STRING("ALLMULTI", IFF_ALLMULTI, link->flags, flags),
+ FLAG_STRING("PORTSEL", IFF_PORTSEL, link->flags, flags),
+ FLAG_STRING("AUTOMEDIA", IFF_AUTOMEDIA, link->flags, flags),
+ FLAG_STRING("DYNAMIC", IFF_DYNAMIC, link->flags, flags),
+ FLAG_STRING("NOARP", IFF_NOARP, link->flags, flags),
+ FLAG_STRING("NOTRAILERS", IFF_NOTRAILERS, link->flags, flags),
+ FLAG_STRING("DEBUG", IFF_DEBUG, link->flags, flags),
+ FLAG_STRING("ECHO", IFF_ECHO, link->flags, flags));
+
+ unknown_flags = ~(IFF_LOOPBACK | IFF_MASTER | IFF_SLAVE | IFF_UP |
+ IFF_DORMANT | IFF_LOWER_UP | IFF_RUNNING |
+ IFF_MULTICAST | IFF_BROADCAST | IFF_POINTOPOINT |
+ IFF_PROMISC | IFF_ALLMULTI | IFF_PORTSEL |
+ IFF_AUTOMEDIA | IFF_DYNAMIC | IFF_NOARP |
+ IFF_NOTRAILERS | IFF_DEBUG | IFF_ECHO);
+ unknown_flags_added = ((link->flags ^ flags) & flags & unknown_flags);
+ unknown_flags_removed = ((link->flags ^ flags) & link->flags & unknown_flags);
+
+ if (unknown_flags_added)
+ log_link_debug(link, "Unknown link flags gained, ignoring: %#.5x", unknown_flags_added);
+
+ if (unknown_flags_removed)
+ log_link_debug(link, "Unknown link flags lost, ignoring: %#.5x", unknown_flags_removed);
+ }
+
+ link_was_admin_up = link->flags & IFF_UP;
+ had_carrier = link_has_carrier(link);
+
+ link->flags = flags;
+ link->kernel_operstate = operstate;
+
+ link_update_operstate(link, true);
+
+ if (!link_was_admin_up && (link->flags & IFF_UP)) {
+ log_link_info(link, "Link UP");
+
+ r = link_admin_state_up(link);
+ if (r < 0)
+ return r;
+ } else if (link_was_admin_up && !(link->flags & IFF_UP)) {
+ log_link_info(link, "Link DOWN");
+
+ r = link_admin_state_down(link);
+ if (r < 0)
+ return r;
+ }
+
+ if (!had_carrier && link_has_carrier(link)) {
+ log_link_info(link, "Gained carrier");
+
+ r = link_carrier_gained(link);
+ if (r < 0)
+ return r;
+ } else if (had_carrier && !link_has_carrier(link)) {
+ log_link_info(link, "Lost carrier");
+
+ r = link_carrier_lost(link);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int link_update_master(Link *link, sd_netlink_message *message) {
+ int master_ifindex, r;
+
+ assert(link);
+ assert(message);
+
+ r = sd_netlink_message_read_u32(message, IFLA_MASTER, (uint32_t*) &master_ifindex);
+ if (r == -ENODATA)
+ return 0;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "rtnl: failed to read master ifindex: %m");
+
+ if (master_ifindex == link->ifindex)
+ master_ifindex = 0;
+
+ if (master_ifindex != link->master_ifindex) {
+ if (link->master_ifindex == 0)
+ log_link_debug(link, "Attached to master interface: %i", master_ifindex);
+ else if (master_ifindex == 0)
+ log_link_debug(link, "Detached from master interface: %i", link->master_ifindex);
+ else
+ log_link_debug(link, "Master interface changed: %i %s %i", link->master_ifindex,
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), master_ifindex);
+
+ link_drop_from_master(link);
+ link->master_ifindex = master_ifindex;
+ }
+
+ r = link_append_to_master(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to append link to master: %m");
+
+ return 0;
+}
+
+static int link_update_driver(Link *link, sd_netlink_message *message) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(message);
+
+ /* Driver is already read. Assuming the driver is never changed. */
+ if (link->driver)
+ return 0;
+
+ /* When udevd is running, read the driver after the interface is initialized by udevd.
+ * Otherwise, ethtool may not work correctly. See issue #22538.
+ * When udevd is not running, read the value when the interface is detected. */
+ if (link->state != (udev_available() ? LINK_STATE_INITIALIZED : LINK_STATE_PENDING))
+ return 0;
+
+ r = ethtool_get_driver(&link->manager->ethtool_fd, link->ifname, &link->driver);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "Failed to get driver, continuing without: %m");
+ return 0;
+ }
+
+ log_link_debug(link, "Found driver: %s", strna(link->driver));
+
+ if (streq_ptr(link->driver, "dsa")) {
+ uint32_t dsa_master_ifindex = 0;
+
+ r = sd_netlink_message_read_u32(message, IFLA_LINK, &dsa_master_ifindex);
+ if (r < 0 && r != -ENODATA)
+ return log_link_debug_errno(link, r, "rtnl: failed to read ifindex of the DSA master interface: %m");
+
+ if (dsa_master_ifindex > INT_MAX) {
+ log_link_debug(link, "rtnl: received too large DSA master ifindex (%"PRIu32" > INT_MAX), ignoring.",
+ dsa_master_ifindex);
+ dsa_master_ifindex = 0;
+ }
+
+ link->dsa_master_ifindex = (int) dsa_master_ifindex;
+ }
+
+ return 0;
+}
+
+static int link_update_permanent_hardware_address(Link *link, sd_netlink_message *message) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(message);
+
+ if (link->permanent_hw_addr.length > 0)
+ return 0;
+
+ /* When udevd is running, read the permanent hardware address after the interface is
+ * initialized by udevd. Otherwise, ethtool may not work correctly. See issue #22538.
+ * When udevd is not running, read the value when the interface is detected. */
+ if (link->state != (udev_available() ? LINK_STATE_INITIALIZED : LINK_STATE_PENDING))
+ return 0;
+
+ r = netlink_message_read_hw_addr(message, IFLA_PERM_ADDRESS, &link->permanent_hw_addr);
+ if (r < 0) {
+ if (r != -ENODATA)
+ return log_link_debug_errno(link, r, "Failed to read IFLA_PERM_ADDRESS attribute: %m");
+
+ if (netlink_message_read_hw_addr(message, IFLA_ADDRESS, NULL) >= 0) {
+ /* Fallback to ethtool, if the link has a hardware address. */
+ r = ethtool_get_permanent_hw_addr(&link->manager->ethtool_fd, link->ifname, &link->permanent_hw_addr);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Permanent hardware address not found, continuing without: %m");
+ }
+ }
+
+ if (link->permanent_hw_addr.length > 0)
+ log_link_debug(link, "Saved permanent hardware address: %s", HW_ADDR_TO_STR(&link->permanent_hw_addr));
+
+ return 0;
+}
+
+static int link_update_hardware_address(Link *link, sd_netlink_message *message) {
+ struct hw_addr_data addr;
+ int r;
+
+ assert(link);
+ assert(message);
+
+ r = netlink_message_read_hw_addr(message, IFLA_BROADCAST, &link->bcast_addr);
+ if (r < 0 && r != -ENODATA)
+ return log_link_debug_errno(link, r, "rtnl: failed to read broadcast address: %m");
+
+ r = netlink_message_read_hw_addr(message, IFLA_ADDRESS, &addr);
+ if (r == -ENODATA)
+ return 0;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "rtnl: failed to read hardware address: %m");
+
+ if (hw_addr_equal(&link->hw_addr, &addr))
+ return 0;
+
+ if (link->hw_addr.length == 0)
+ log_link_debug(link, "Saved hardware address: %s", HW_ADDR_TO_STR(&addr));
+ else {
+ log_link_debug(link, "Hardware address is changed: %s %s %s",
+ HW_ADDR_TO_STR(&link->hw_addr),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
+ HW_ADDR_TO_STR(&addr));
+
+ hashmap_remove_value(link->manager->links_by_hw_addr, &link->hw_addr, link);
+ }
+
+ link->hw_addr = addr;
+
+ if (!hw_addr_is_null(&link->hw_addr)) {
+ r = hashmap_ensure_put(&link->manager->links_by_hw_addr, &hw_addr_hash_ops, &link->hw_addr, link);
+ if (r == -EEXIST && streq_ptr(link->kind, "bond"))
+ /* bonding master and its slaves have the same hardware address. */
+ r = hashmap_replace(link->manager->links_by_hw_addr, &link->hw_addr, link);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Failed to manage link by its new hardware address, ignoring: %m");
+ }
+
+ r = ipv4acd_update_mac(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MAC address in IPv4 ACD client: %m");
+
+ r = ipv4ll_update_mac(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MAC address in IPv4LL client: %m");
+
+ r = dhcp4_update_mac(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MAC address in DHCP client: %m");
+
+ r = dhcp6_update_mac(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MAC address in DHCPv6 client: %m");
+
+ r = radv_update_mac(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MAC address for Router Advertisement: %m");
+
+ if (link->ndisc && link->hw_addr.length == ETH_ALEN) {
+ r = sd_ndisc_set_mac(link->ndisc, &link->hw_addr.ether);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MAC for NDisc: %m");
+ }
+
+ if (link->lldp_rx) {
+ r = sd_lldp_rx_set_filter_address(link->lldp_rx, &link->hw_addr.ether);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MAC address for LLDP Rx: %m");
+ }
+
+ if (link->lldp_tx) {
+ r = sd_lldp_tx_set_hwaddr(link->lldp_tx, &link->hw_addr.ether);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MAC address for LLDP Tx: %m");
+ }
+
+ return 0;
+}
+
+static int link_update_mtu(Link *link, sd_netlink_message *message) {
+ uint32_t mtu, min_mtu = 0, max_mtu = UINT32_MAX;
+ int r;
+
+ assert(link);
+ assert(message);
+
+ r = sd_netlink_message_read_u32(message, IFLA_MTU, &mtu);
+ if (r == -ENODATA)
+ return 0;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "rtnl: failed to read MTU in RTM_NEWLINK message: %m");
+ if (mtu == 0)
+ return 0;
+
+ r = sd_netlink_message_read_u32(message, IFLA_MIN_MTU, &min_mtu);
+ if (r < 0 && r != -ENODATA)
+ return log_link_debug_errno(link, r, "rtnl: failed to read minimum MTU in RTM_NEWLINK message: %m");
+
+ r = sd_netlink_message_read_u32(message, IFLA_MAX_MTU, &max_mtu);
+ if (r < 0 && r != -ENODATA)
+ return log_link_debug_errno(link, r, "rtnl: failed to read maximum MTU in RTM_NEWLINK message: %m");
+
+ if (max_mtu == 0)
+ max_mtu = UINT32_MAX;
+
+ link->min_mtu = min_mtu;
+ link->max_mtu = max_mtu;
+
+ if (link->original_mtu == 0) {
+ link->original_mtu = mtu;
+ log_link_debug(link, "Saved original MTU %" PRIu32" (min: %"PRIu32", max: %"PRIu32")",
+ link->original_mtu, link->min_mtu, link->max_mtu);
+ }
+
+ if (link->mtu == mtu)
+ return 0;
+
+ if (link->mtu != 0)
+ log_link_debug(link, "MTU is changed: %"PRIu32" %s %"PRIu32" (min: %"PRIu32", max: %"PRIu32")",
+ link->mtu, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), mtu,
+ link->min_mtu, link->max_mtu);
+
+ link->mtu = mtu;
+
+ if (link->dhcp_client) {
+ r = sd_dhcp_client_set_mtu(link->dhcp_client, link->mtu);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not update MTU in DHCP client: %m");
+ }
+
+ if (link->radv) {
+ r = sd_radv_set_mtu(link->radv, link->mtu);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not set MTU for Router Advertisement: %m");
+ }
+
+ return 0;
+}
+
+static int link_update_alternative_names(Link *link, sd_netlink_message *message) {
+ _cleanup_strv_free_ char **altnames = NULL;
+ int r;
+
+ assert(link);
+ assert(message);
+
+ r = sd_netlink_message_read_strv(message, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &altnames);
+ if (r == -ENODATA)
+ /* The message does not have IFLA_PROP_LIST container attribute. It does not means the
+ * interface has no alternative name. */
+ return 0;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "rtnl: failed to read alternative names: %m");
+
+ if (strv_equal(altnames, link->alternative_names))
+ return 0;
+
+ STRV_FOREACH(n, link->alternative_names)
+ hashmap_remove(link->manager->links_by_name, *n);
+
+ strv_free_and_replace(link->alternative_names, altnames);
+
+ STRV_FOREACH(n, link->alternative_names) {
+ r = hashmap_ensure_put(&link->manager->links_by_name, &string_hash_ops, *n, link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to manage link by its new alternative names: %m");
+ }
+
+ return 0;
+}
+
+static int link_update_name(Link *link, sd_netlink_message *message) {
+ char ifname_from_index[IF_NAMESIZE];
+ const char *ifname;
+ int r;
+
+ assert(link);
+ assert(message);
+
+ r = sd_netlink_message_read_string(message, IFLA_IFNAME, &ifname);
+ if (r == -ENODATA)
+ /* Hmm?? But ok. */
+ return 0;
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to read interface name in RTM_NEWLINK message: %m");
+
+ if (streq(ifname, link->ifname))
+ return 0;
+
+ r = format_ifname(link->ifindex, ifname_from_index);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not get interface name for index %i.", link->ifindex);
+
+ if (!streq(ifname, ifname_from_index)) {
+ log_link_debug(link, "New interface name '%s' received from the kernel does not correspond "
+ "with the name currently configured on the actual interface '%s'. Ignoring.",
+ ifname, ifname_from_index);
+ return 0;
+ }
+
+ log_link_info(link, "Interface name change detected, renamed to %s.", ifname);
+
+ hashmap_remove(link->manager->links_by_name, link->ifname);
+
+ r = free_and_strdup(&link->ifname, ifname);
+ if (r < 0)
+ return log_oom_debug();
+
+ r = hashmap_ensure_put(&link->manager->links_by_name, &string_hash_ops, link->ifname, link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to manage link by its new name: %m");
+
+ if (link->dhcp_client) {
+ r = sd_dhcp_client_set_ifname(link->dhcp_client, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in DHCP client: %m");
+ }
+
+ if (link->dhcp6_client) {
+ r = sd_dhcp6_client_set_ifname(link->dhcp6_client, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in DHCP6 client: %m");
+ }
+
+ if (link->ndisc) {
+ r = sd_ndisc_set_ifname(link->ndisc, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in NDisc: %m");
+ }
+
+ if (link->dhcp_server) {
+ r = sd_dhcp_server_set_ifname(link->dhcp_server, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in DHCP server: %m");
+ }
+
+ if (link->radv) {
+ r = sd_radv_set_ifname(link->radv, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in Router Advertisement: %m");
+ }
+
+ if (link->lldp_rx) {
+ r = sd_lldp_rx_set_ifname(link->lldp_rx, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in LLDP Rx: %m");
+ }
+
+ if (link->lldp_tx) {
+ r = sd_lldp_tx_set_ifname(link->lldp_tx, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in LLDP Tx: %m");
+ }
+
+ if (link->ipv4ll) {
+ r = sd_ipv4ll_set_ifname(link->ipv4ll, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in IPv4LL client: %m");
+ }
+
+ r = ipv4acd_set_ifname(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in IPv4ACD client: %m");
+
+ return 0;
+}
+
+static int link_update(Link *link, sd_netlink_message *message) {
+ int r;
+
+ assert(link);
+ assert(message);
+
+ r = link_update_name(link, message);
+ if (r < 0)
+ return r;
+
+ r = link_update_alternative_names(link, message);
+ if (r < 0)
+ return r;
+
+ r = link_update_mtu(link, message);
+ if (r < 0)
+ return r;
+
+ r = link_update_driver(link, message);
+ if (r < 0)
+ return r;
+
+ r = link_update_permanent_hardware_address(link, message);
+ if (r < 0)
+ return r;
+
+ r = link_update_hardware_address(link, message);
+ if (r < 0)
+ return r;
+
+ r = link_update_master(link, message);
+ if (r < 0)
+ return r;
+
+ r = link_update_ipv6ll_addrgen_mode(link, message);
+ if (r < 0)
+ return r;
+
+ return link_update_flags(link, message);
+}
+
+static Link *link_drop_or_unref(Link *link) {
+ if (!link)
+ return NULL;
+ if (!link->manager)
+ return link_unref(link);
+ return link_drop(link);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_drop_or_unref);
+
+static int link_new(Manager *manager, sd_netlink_message *message, Link **ret) {
+ _cleanup_free_ char *ifname = NULL, *kind = NULL, *state_file = NULL, *lease_file = NULL, *lldp_file = NULL;
+ _cleanup_(link_drop_or_unrefp) Link *link = NULL;
+ unsigned short iftype;
+ int r, ifindex;
+
+ assert(manager);
+ assert(message);
+ assert(ret);
+
+ r = sd_rtnl_message_link_get_ifindex(message, &ifindex);
+ if (r < 0)
+ return log_debug_errno(r, "rtnl: failed to read ifindex from link message: %m");
+ else if (ifindex <= 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "rtnl: received link message without valid ifindex.");
+
+ r = sd_rtnl_message_link_get_type(message, &iftype);
+ if (r < 0)
+ return log_debug_errno(r, "rtnl: failed to read interface type from link message: %m");
+
+ r = sd_netlink_message_read_string_strdup(message, IFLA_IFNAME, &ifname);
+ if (r < 0)
+ return log_debug_errno(r, "rtnl: failed to read interface name from link message: %m");
+
+ /* check for link kind */
+ r = sd_netlink_message_enter_container(message, IFLA_LINKINFO);
+ if (r >= 0) {
+ r = sd_netlink_message_read_string_strdup(message, IFLA_INFO_KIND, &kind);
+ if (r < 0 && r != -ENODATA)
+ return log_debug_errno(r, "rtnl: failed to read interface kind from link message: %m");
+ r = sd_netlink_message_exit_container(message);
+ if (r < 0)
+ return log_debug_errno(r, "rtnl: failed to exit IFLA_LINKINFO container: %m");
+ }
+
+ if (!manager->test_mode) {
+ /* Do not update state files when running in test mode. */
+ if (asprintf(&state_file, "/run/systemd/netif/links/%d", ifindex) < 0)
+ return log_oom_debug();
+
+ if (asprintf(&lease_file, "/run/systemd/netif/leases/%d", ifindex) < 0)
+ return log_oom_debug();
+
+ if (asprintf(&lldp_file, "/run/systemd/netif/lldp/%d", ifindex) < 0)
+ return log_oom_debug();
+ }
+
+ link = new(Link, 1);
+ if (!link)
+ return -ENOMEM;
+
+ *link = (Link) {
+ .n_ref = 1,
+ .state = LINK_STATE_PENDING,
+ .online_state = _LINK_ONLINE_STATE_INVALID,
+ .ifindex = ifindex,
+ .iftype = iftype,
+ .ifname = TAKE_PTR(ifname),
+ .kind = TAKE_PTR(kind),
+
+ .ipv6ll_address_gen_mode = _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_INVALID,
+
+ .state_file = TAKE_PTR(state_file),
+ .lease_file = TAKE_PTR(lease_file),
+ .lldp_file = TAKE_PTR(lldp_file),
+
+ .n_dns = UINT_MAX,
+ .dns_default_route = -1,
+ .llmnr = _RESOLVE_SUPPORT_INVALID,
+ .mdns = _RESOLVE_SUPPORT_INVALID,
+ .dnssec_mode = _DNSSEC_MODE_INVALID,
+ .dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID,
+ };
+
+ r = hashmap_ensure_put(&manager->links_by_index, NULL, INT_TO_PTR(link->ifindex), link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to store link into manager: %m");
+
+ link->manager = manager;
+
+ r = hashmap_ensure_put(&manager->links_by_name, &string_hash_ops, link->ifname, link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to manage link by its interface name: %m");
+
+ log_link_debug(link, "Saved new link: ifindex=%i, iftype=%s(%u), kind=%s",
+ link->ifindex, strna(arphrd_to_name(link->iftype)), link->iftype, strna(link->kind));
+
+ *ret = TAKE_PTR(link);
+ return 0;
+}
+
+int manager_rtnl_process_link(sd_netlink *rtnl, sd_netlink_message *message, Manager *manager) {
+ Link *link = NULL;
+ NetDev *netdev = NULL;
+ uint16_t type;
+ const char *name;
+ int r, ifindex;
+
+ assert(rtnl);
+ assert(message);
+ assert(manager);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "rtnl: Could not receive link message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: Could not get message type, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(type, RTM_NEWLINK, RTM_DELLINK)) {
+ log_warning("rtnl: Received unexpected message type %u when processing link, ignoring.", type);
+ return 0;
+ }
+
+ r = sd_rtnl_message_link_get_ifindex(message, &ifindex);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: Could not get ifindex from link message, ignoring: %m");
+ return 0;
+ } else if (ifindex <= 0) {
+ log_warning("rtnl: received link message with invalid ifindex %d, ignoring.", ifindex);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string(message, IFLA_IFNAME, &name);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: Received link message without ifname, ignoring: %m");
+ return 0;
+ }
+
+ (void) link_get_by_index(manager, ifindex, &link);
+ (void) netdev_get(manager, name, &netdev);
+
+ switch (type) {
+ case RTM_NEWLINK:
+ if (netdev) {
+ /* netdev exists, so make sure the ifindex matches */
+ r = netdev_set_ifindex(netdev, message);
+ if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "Could not process new link message for netdev, ignoring: %m");
+ return 0;
+ }
+ }
+
+ if (!link) {
+ /* link is new, so add it */
+ r = link_new(manager, message, &link);
+ if (r < 0) {
+ log_warning_errno(r, "Could not process new link message: %m");
+ return 0;
+ }
+
+ r = link_update(link, message);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not process link message: %m");
+ link_enter_failed(link);
+ return 0;
+ }
+
+ r = link_check_initialized(link);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to check link is initialized: %m");
+ link_enter_failed(link);
+ return 0;
+ }
+ } else {
+ r = link_update(link, message);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not process link message: %m");
+ link_enter_failed(link);
+ return 0;
+ }
+ }
+ break;
+
+ case RTM_DELLINK:
+ link_drop(link);
+ netdev_drop(netdev);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+int link_getlink_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg) {
+ uint16_t message_type;
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(error_msg);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_link_message_warning_errno(link, m, r, error_msg);
+ link_enter_failed(link);
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(m, &message_type);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "rtnl: failed to read link message type, ignoring: %m");
+ return 0;
+ }
+ if (message_type != RTM_NEWLINK) {
+ log_link_debug(link, "rtnl: received invalid link message type, ignoring.");
+ return 0;
+ }
+
+ r = link_update(link, m);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 0;
+ }
+
+ return 1;
+}
+
+int link_call_getlink(Link *link, link_netlink_message_handler_t callback) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(callback);
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_GETLINK, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, callback,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return r;
+
+ link_ref(link);
+ return 0;
+}
+
+static const char* const link_state_table[_LINK_STATE_MAX] = {
+ [LINK_STATE_PENDING] = "pending",
+ [LINK_STATE_INITIALIZED] = "initialized",
+ [LINK_STATE_CONFIGURING] = "configuring",
+ [LINK_STATE_CONFIGURED] = "configured",
+ [LINK_STATE_UNMANAGED] = "unmanaged",
+ [LINK_STATE_FAILED] = "failed",
+ [LINK_STATE_LINGER] = "linger",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(link_state, LinkState);
+
+int link_flags_to_string_alloc(uint32_t flags, char **ret) {
+ _cleanup_free_ char *str = NULL;
+ static const char* map[] = {
+ [LOG2U(IFF_UP)] = "up", /* interface is up. */
+ [LOG2U(IFF_BROADCAST)] = "broadcast", /* broadcast address valid.*/
+ [LOG2U(IFF_DEBUG)] = "debug", /* turn on debugging. */
+ [LOG2U(IFF_LOOPBACK)] = "loopback", /* interface is a loopback net. */
+ [LOG2U(IFF_POINTOPOINT)] = "point-to-point", /* interface has p-p link. */
+ [LOG2U(IFF_NOTRAILERS)] = "no-trailers", /* avoid use of trailers. */
+ [LOG2U(IFF_RUNNING)] = "running", /* interface RFC2863 OPER_UP. */
+ [LOG2U(IFF_NOARP)] = "no-arp", /* no ARP protocol. */
+ [LOG2U(IFF_PROMISC)] = "promiscuous", /* receive all packets. */
+ [LOG2U(IFF_ALLMULTI)] = "all-multicast", /* receive all multicast packets. */
+ [LOG2U(IFF_MASTER)] = "master", /* master of a load balancer. */
+ [LOG2U(IFF_SLAVE)] = "slave", /* slave of a load balancer. */
+ [LOG2U(IFF_MULTICAST)] = "multicast", /* supports multicast.*/
+ [LOG2U(IFF_PORTSEL)] = "portsel", /* can set media type. */
+ [LOG2U(IFF_AUTOMEDIA)] = "auto-media", /* auto media select active. */
+ [LOG2U(IFF_DYNAMIC)] = "dynamic", /* dialup device with changing addresses. */
+ [LOG2U(IFF_LOWER_UP)] = "lower-up", /* driver signals L1 up. */
+ [LOG2U(IFF_DORMANT)] = "dormant", /* driver signals dormant. */
+ [LOG2U(IFF_ECHO)] = "echo", /* echo sent packets. */
+ };
+
+ assert(ret);
+
+ for (size_t i = 0; i < ELEMENTSOF(map); i++)
+ if (FLAGS_SET(flags, 1 << i) && map[i])
+ if (!strextend_with_separator(&str, ",", map[i]))
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(str);
+ return 0;
+}
+
+static const char * const kernel_operstate_table[] = {
+ [IF_OPER_UNKNOWN] = "unknown",
+ [IF_OPER_NOTPRESENT] = "not-present",
+ [IF_OPER_DOWN] = "down",
+ [IF_OPER_LOWERLAYERDOWN] = "lower-layer-down",
+ [IF_OPER_TESTING] = "testing",
+ [IF_OPER_DORMANT] = "dormant",
+ [IF_OPER_UP] = "up",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_TO_STRING(kernel_operstate, int);
diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h
new file mode 100644
index 0000000..f0d5d46
--- /dev/null
+++ b/src/network/networkd-link.h
@@ -0,0 +1,245 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <endian.h>
+#include <linux/nl80211.h>
+
+#include "sd-bus.h"
+#include "sd-device.h"
+#include "sd-dhcp-client.h"
+#include "sd-dhcp-server.h"
+#include "sd-dhcp6-client.h"
+#include "sd-ipv4acd.h"
+#include "sd-ipv4ll.h"
+#include "sd-lldp-rx.h"
+#include "sd-lldp-tx.h"
+#include "sd-ndisc.h"
+#include "sd-radv.h"
+#include "sd-netlink.h"
+
+#include "ether-addr-util.h"
+#include "log-link.h"
+#include "netif-util.h"
+#include "network-util.h"
+#include "networkd-ipv6ll.h"
+#include "networkd-util.h"
+#include "ordered-set.h"
+#include "resolve-util.h"
+#include "set.h"
+
+typedef enum LinkState {
+ LINK_STATE_PENDING, /* udev has not initialized the link */
+ LINK_STATE_INITIALIZED, /* udev has initialized the link */
+ LINK_STATE_CONFIGURING, /* configuring addresses, routes, etc. */
+ LINK_STATE_CONFIGURED, /* everything is configured */
+ LINK_STATE_UNMANAGED, /* Unmanaged=yes is set */
+ LINK_STATE_FAILED, /* at least one configuration process failed */
+ LINK_STATE_LINGER, /* RTM_DELLINK for the link has been received */
+ _LINK_STATE_MAX,
+ _LINK_STATE_INVALID = -EINVAL,
+} LinkState;
+
+typedef struct Manager Manager;
+typedef struct Network Network;
+typedef struct NetDev NetDev;
+typedef struct DUID DUID;
+
+typedef struct Link {
+ Manager *manager;
+
+ unsigned n_ref;
+
+ int ifindex;
+ int master_ifindex;
+ int dsa_master_ifindex;
+ int sr_iov_phys_port_ifindex;
+ Set *sr_iov_virt_port_ifindices;
+
+ char *ifname;
+ char **alternative_names;
+ char *kind;
+ unsigned short iftype;
+ char *state_file;
+ struct hw_addr_data hw_addr;
+ struct hw_addr_data bcast_addr;
+ struct hw_addr_data permanent_hw_addr;
+ struct hw_addr_data requested_hw_addr;
+ struct in6_addr ipv6ll_address;
+ uint32_t mtu;
+ uint32_t min_mtu;
+ uint32_t max_mtu;
+ uint32_t original_mtu;
+ sd_device *dev;
+ char *driver;
+
+ /* link-local addressing */
+ IPv6LinkLocalAddressGenMode ipv6ll_address_gen_mode;
+
+ /* wlan */
+ enum nl80211_iftype wlan_iftype;
+ char *ssid;
+ char *previous_ssid;
+ struct ether_addr bssid;
+
+ unsigned flags;
+ uint8_t kernel_operstate;
+
+ sd_event_source *carrier_lost_timer;
+
+ Network *network;
+ NetDev *netdev;
+
+ LinkState state;
+ LinkOperationalState operstate;
+ LinkCarrierState carrier_state;
+ LinkAddressState address_state;
+ LinkAddressState ipv4_address_state;
+ LinkAddressState ipv6_address_state;
+ LinkOnlineState online_state;
+
+ unsigned static_address_messages;
+ unsigned static_address_label_messages;
+ unsigned static_bridge_fdb_messages;
+ unsigned static_bridge_mdb_messages;
+ unsigned static_ipv6_proxy_ndp_messages;
+ unsigned static_neighbor_messages;
+ unsigned static_nexthop_messages;
+ unsigned static_route_messages;
+ unsigned static_routing_policy_rule_messages;
+ unsigned tc_messages;
+ unsigned sr_iov_messages;
+ unsigned set_link_messages;
+ unsigned set_flags_messages;
+ unsigned create_stacked_netdev_messages;
+
+ Set *addresses;
+ Set *neighbors;
+ Set *routes;
+ Set *nexthops;
+ Set *qdiscs;
+ Set *tclasses;
+
+ sd_dhcp_client *dhcp_client;
+ sd_dhcp_lease *dhcp_lease;
+ char *lease_file;
+ unsigned dhcp4_messages;
+ bool dhcp4_configured;
+ char *dhcp4_6rd_tunnel_name;
+
+ sd_ipv4ll *ipv4ll;
+ bool ipv4ll_address_configured:1;
+
+ bool static_addresses_configured:1;
+ bool static_address_labels_configured:1;
+ bool static_bridge_fdb_configured:1;
+ bool static_bridge_mdb_configured:1;
+ bool static_ipv6_proxy_ndp_configured:1;
+ bool static_neighbors_configured:1;
+ bool static_nexthops_configured:1;
+ bool static_routes_configured:1;
+ bool static_routing_policy_rules_configured:1;
+ bool tc_configured:1;
+ bool sr_iov_configured:1;
+ bool activated:1;
+ bool master_set:1;
+ bool stacked_netdevs_created:1;
+
+ sd_dhcp_server *dhcp_server;
+
+ sd_ndisc *ndisc;
+ sd_event_source *ndisc_expire;
+ Set *ndisc_rdnss;
+ Set *ndisc_dnssl;
+ unsigned ndisc_messages;
+ bool ndisc_configured:1;
+
+ sd_radv *radv;
+
+ sd_dhcp6_client *dhcp6_client;
+ sd_dhcp6_lease *dhcp6_lease;
+ unsigned dhcp6_messages;
+ bool dhcp6_configured;
+
+ Set *dhcp_pd_prefixes;
+ unsigned dhcp_pd_messages;
+ bool dhcp_pd_configured;
+
+ /* This is about LLDP reception */
+ sd_lldp_rx *lldp_rx;
+ char *lldp_file;
+
+ /* This is about LLDP transmission */
+ sd_lldp_tx *lldp_tx;
+
+ Hashmap *bound_by_links;
+ Hashmap *bound_to_links;
+ Set *slaves;
+
+ /* For speed meter */
+ struct rtnl_link_stats64 stats_old, stats_new;
+ bool stats_updated;
+
+ /* All kinds of DNS configuration the user configured via D-Bus */
+ struct in_addr_full **dns;
+ unsigned n_dns;
+ OrderedSet *search_domains, *route_domains;
+
+ int dns_default_route;
+ ResolveSupport llmnr;
+ ResolveSupport mdns;
+ DnssecMode dnssec_mode;
+ DnsOverTlsMode dns_over_tls_mode;
+ Set *dnssec_negative_trust_anchors;
+
+ /* Similar, but NTP server configuration */
+ char **ntp;
+} Link;
+
+typedef int (*link_netlink_message_handler_t)(sd_netlink*, sd_netlink_message*, Link*);
+
+bool link_is_ready_to_configure(Link *link, bool allow_unmanaged);
+
+void link_ntp_settings_clear(Link *link);
+void link_dns_settings_clear(Link *link);
+Link *link_unref(Link *link);
+Link *link_ref(Link *link);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_unref);
+DEFINE_TRIVIAL_DESTRUCTOR(link_netlink_destroy_callback, Link, link_unref);
+
+int link_get_by_index(Manager *m, int ifindex, Link **ret);
+int link_get_by_name(Manager *m, const char *ifname, Link **ret);
+int link_get_by_hw_addr(Manager *m, const struct hw_addr_data *hw_addr, Link **ret);
+int link_get_master(Link *link, Link **ret);
+
+int link_getlink_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg);
+int link_call_getlink(Link *link, link_netlink_message_handler_t callback);
+int link_handle_bound_to_list(Link *link);
+
+void link_enter_failed(Link *link);
+void link_set_state(Link *link, LinkState state);
+void link_check_ready(Link *link);
+
+void link_update_operstate(Link *link, bool also_update_bond_master);
+
+static inline bool link_has_carrier(Link *link) {
+ assert(link);
+ return netif_has_carrier(link->kernel_operstate, link->flags);
+}
+
+bool link_ipv6_enabled(Link *link);
+int link_ipv6ll_gained(Link *link);
+
+int link_stop_engines(Link *link, bool may_keep_dhcp);
+
+const char* link_state_to_string(LinkState s) _const_;
+LinkState link_state_from_string(const char *s) _pure_;
+
+int link_reconfigure_impl(Link *link, bool force);
+int link_reconfigure(Link *link, bool force);
+int link_reconfigure_after_sleep(Link *link);
+
+int manager_udev_process_link(Manager *m, sd_device *device, sd_device_action_t action);
+int manager_rtnl_process_link(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+int link_flags_to_string_alloc(uint32_t flags, char **ret);
+const char *kernel_operstate_to_string(int t) _const_;
diff --git a/src/network/networkd-lldp-rx.c b/src/network/networkd-lldp-rx.c
new file mode 100644
index 0000000..3a59884
--- /dev/null
+++ b/src/network/networkd-lldp-rx.c
@@ -0,0 +1,173 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <net/if_arp.h>
+#include <unistd.h>
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "networkd-link.h"
+#include "networkd-lldp-rx.h"
+#include "networkd-lldp-tx.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_lldp_mode, lldp_mode, LLDPMode, "Failed to parse LLDP= setting.");
+
+static const char* const lldp_mode_table[_LLDP_MODE_MAX] = {
+ [LLDP_MODE_NO] = "no",
+ [LLDP_MODE_YES] = "yes",
+ [LLDP_MODE_ROUTERS_ONLY] = "routers-only",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(lldp_mode, LLDPMode, LLDP_MODE_YES);
+
+static bool link_lldp_rx_enabled(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ if (link->iftype != ARPHRD_ETHER)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ /* LLDP should be handled on bridge and bond slaves as those have a direct connection to their peers,
+ * not on the bridge/bond master. Linux doesn't even (by default) forward lldp packets to the bridge
+ * master. */
+ if (link->kind && STR_IN_SET(link->kind, "bridge", "bond"))
+ return false;
+
+ return link->network->lldp_mode != LLDP_MODE_NO;
+}
+
+static void lldp_rx_handler(sd_lldp_rx *lldp_rx, sd_lldp_rx_event_t event, sd_lldp_neighbor *n, void *userdata) {
+ Link *link = ASSERT_PTR(userdata);
+ int r;
+
+ (void) link_lldp_save(link);
+
+ if (link->lldp_tx && event == SD_LLDP_RX_EVENT_ADDED) {
+ /* If we received information about a new neighbor, restart the LLDP "fast" logic */
+
+ log_link_debug(link, "Received LLDP datagram from previously unknown neighbor, restarting 'fast' LLDP transmission.");
+
+ (void) sd_lldp_tx_stop(link->lldp_tx);
+ r = sd_lldp_tx_start(link->lldp_tx);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to restart LLDP transmission: %m");
+ }
+}
+
+int link_lldp_rx_configure(Link *link) {
+ int r;
+
+ if (!link_lldp_rx_enabled(link))
+ return 0;
+
+ if (link->lldp_rx)
+ return -EBUSY;
+
+ r = sd_lldp_rx_new(&link->lldp_rx);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_rx_attach_event(link->lldp_rx, link->manager->event, 0);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_rx_set_ifindex(link->lldp_rx, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_rx_match_capabilities(link->lldp_rx,
+ link->network->lldp_mode == LLDP_MODE_ROUTERS_ONLY ?
+ SD_LLDP_SYSTEM_CAPABILITIES_ALL_ROUTERS :
+ SD_LLDP_SYSTEM_CAPABILITIES_ALL);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_rx_set_filter_address(link->lldp_rx, &link->hw_addr.ether);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_rx_set_callback(link->lldp_rx, lldp_rx_handler, link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int link_lldp_save(Link *link) {
+ _cleanup_(unlink_and_freep) char *temp_path = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ sd_lldp_neighbor **l = NULL;
+ int n = 0, r, i;
+
+ assert(link);
+
+ if (isempty(link->lldp_file))
+ return 0; /* Do not update state file when running in test mode. */
+
+ if (!link->lldp_rx) {
+ (void) unlink(link->lldp_file);
+ return 0;
+ }
+
+ r = sd_lldp_rx_get_neighbors(link->lldp_rx, &l);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ (void) unlink(link->lldp_file);
+ return 0;
+ }
+
+ n = r;
+
+ r = fopen_temporary(link->lldp_file, &f, &temp_path);
+ if (r < 0)
+ goto finish;
+
+ (void) fchmod(fileno(f), 0644);
+
+ for (i = 0; i < n; i++) {
+ const void *p;
+ le64_t u;
+ size_t sz;
+
+ r = sd_lldp_neighbor_get_raw(l[i], &p, &sz);
+ if (r < 0)
+ goto finish;
+
+ u = htole64(sz);
+ (void) fwrite(&u, 1, sizeof(u), f);
+ (void) fwrite(p, 1, sz, f);
+ }
+
+ r = fflush_and_check(f);
+ if (r < 0)
+ goto finish;
+
+ r = conservative_rename(temp_path, link->lldp_file);
+ if (r < 0)
+ goto finish;
+
+finish:
+ if (r < 0)
+ log_link_error_errno(link, r, "Failed to save LLDP data to %s: %m", link->lldp_file);
+
+ if (l) {
+ for (i = 0; i < n; i++)
+ sd_lldp_neighbor_unref(l[i]);
+ free(l);
+ }
+
+ return r;
+}
diff --git a/src/network/networkd-lldp-rx.h b/src/network/networkd-lldp-rx.h
new file mode 100644
index 0000000..22f6602
--- /dev/null
+++ b/src/network/networkd-lldp-rx.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+
+typedef enum LLDPMode {
+ LLDP_MODE_NO = 0,
+ LLDP_MODE_YES = 1,
+ LLDP_MODE_ROUTERS_ONLY = 2,
+ _LLDP_MODE_MAX,
+ _LLDP_MODE_INVALID = -EINVAL,
+} LLDPMode;
+
+int link_lldp_rx_configure(Link *link);
+int link_lldp_save(Link *link);
+
+const char* lldp_mode_to_string(LLDPMode m) _const_;
+LLDPMode lldp_mode_from_string(const char *s) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_lldp_mode);
diff --git a/src/network/networkd-lldp-tx.c b/src/network/networkd-lldp-tx.c
new file mode 100644
index 0000000..fc9196f
--- /dev/null
+++ b/src/network/networkd-lldp-tx.c
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <net/if_arp.h>
+
+#include "sd-lldp-tx.h"
+
+#include "networkd-link.h"
+#include "networkd-lldp-tx.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+static bool link_lldp_tx_enabled(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ if (link->iftype != ARPHRD_ETHER)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ if (link->kind && STR_IN_SET(link->kind, "bridge", "bond"))
+ return false;
+
+ return link->network->lldp_multicast_mode >= 0 &&
+ link->network->lldp_multicast_mode < _SD_LLDP_MULTICAST_MODE_MAX;
+}
+
+int link_lldp_tx_configure(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link_lldp_tx_enabled(link))
+ return 0;
+
+ if (link->lldp_tx)
+ return -EBUSY;
+
+ r = sd_lldp_tx_new(&link->lldp_tx);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_tx_attach_event(link->lldp_tx, link->manager->event, 0);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_tx_set_ifindex(link->lldp_tx, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_tx_set_hwaddr(link->lldp_tx, &link->hw_addr.ether);
+ if (r < 0)
+ return r;
+
+ assert(link->network);
+
+ r = sd_lldp_tx_set_multicast_mode(link->lldp_tx, link->network->lldp_multicast_mode);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_tx_set_capabilities(link->lldp_tx,
+ SD_LLDP_SYSTEM_CAPABILITIES_STATION |
+ SD_LLDP_SYSTEM_CAPABILITIES_BRIDGE |
+ SD_LLDP_SYSTEM_CAPABILITIES_ROUTER,
+ (link->network->ip_forward != ADDRESS_FAMILY_NO) ?
+ SD_LLDP_SYSTEM_CAPABILITIES_ROUTER :
+ SD_LLDP_SYSTEM_CAPABILITIES_STATION);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_tx_set_port_description(link->lldp_tx, link->network->description);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_tx_set_mud_url(link->lldp_tx, link->network->lldp_mudurl);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static const char * const lldp_multicast_mode_table[_SD_LLDP_MULTICAST_MODE_MAX] = {
+ [SD_LLDP_MULTICAST_MODE_NEAREST_BRIDGE] = "nearest-bridge",
+ [SD_LLDP_MULTICAST_MODE_NON_TPMR_BRIDGE] = "non-tpmr-bridge",
+ [SD_LLDP_MULTICAST_MODE_CUSTOMER_BRIDGE] = "customer-bridge",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(lldp_multicast_mode, sd_lldp_multicast_mode_t);
+
+int config_parse_lldp_multicast_mode(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ sd_lldp_multicast_mode_t m, *mode = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *mode = _SD_LLDP_MULTICAST_MODE_INVALID;
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r >= 0) {
+ *mode = r == 0 ? _SD_LLDP_MULTICAST_MODE_INVALID : SD_LLDP_MULTICAST_MODE_NEAREST_BRIDGE;
+ return 0;
+ }
+
+ m = lldp_multicast_mode_from_string(rvalue);
+ if (m < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, m,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ *mode = m;
+ return 0;
+}
diff --git a/src/network/networkd-lldp-tx.h b/src/network/networkd-lldp-tx.h
new file mode 100644
index 0000000..73757f1
--- /dev/null
+++ b/src/network/networkd-lldp-tx.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+
+int link_lldp_tx_configure(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_lldp_multicast_mode);
diff --git a/src/network/networkd-manager-bus.c b/src/network/networkd-manager-bus.c
new file mode 100644
index 0000000..2ab3aaa
--- /dev/null
+++ b/src/network/networkd-manager-bus.c
@@ -0,0 +1,428 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <sys/capability.h>
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-message-util.h"
+#include "bus-polkit.h"
+#include "networkd-dhcp-server-bus.h"
+#include "networkd-json.h"
+#include "networkd-link-bus.h"
+#include "networkd-link.h"
+#include "networkd-manager-bus.h"
+#include "networkd-manager.h"
+#include "networkd-network-bus.h"
+#include "path-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+static int method_list_links(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ Manager *manager = userdata;
+ Link *link;
+ int r;
+
+ r = sd_bus_message_new_method_return(message, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "(iso)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(link, manager->links_by_index) {
+ _cleanup_free_ char *path = NULL;
+
+ path = link_bus_path(link);
+ if (!path)
+ return -ENOMEM;
+
+ r = sd_bus_message_append(
+ reply, "(iso)",
+ link->ifindex,
+ link->ifname,
+ empty_to_root(path));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int method_get_link_by_name(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_free_ char *path = NULL;
+ Manager *manager = userdata;
+ const char *name;
+ Link *link;
+ int r;
+
+ r = sd_bus_message_read(message, "s", &name);
+ if (r < 0)
+ return r;
+
+ if (link_get_by_name(manager, name, &link) < 0)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %s not known", name);
+
+ r = sd_bus_message_new_method_return(message, &reply);
+ if (r < 0)
+ return r;
+
+ path = link_bus_path(link);
+ if (!path)
+ return -ENOMEM;
+
+ r = sd_bus_message_append(reply, "io", link->ifindex, empty_to_root(path));
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int method_get_link_by_index(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_free_ char *path = NULL;
+ Manager *manager = userdata;
+ int ifindex, r;
+ Link *link;
+
+ r = bus_message_read_ifindex(message, error, &ifindex);
+ if (r < 0)
+ return r;
+
+ r = link_get_by_index(manager, ifindex, &link);
+ if (r < 0)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %i not known", ifindex);
+
+ r = sd_bus_message_new_method_return(message, &reply);
+ if (r < 0)
+ return r;
+
+ path = link_bus_path(link);
+ if (!path)
+ return -ENOMEM;
+
+ r = sd_bus_message_append(reply, "so", link->ifname, empty_to_root(path));
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int call_link_method(Manager *m, sd_bus_message *message, sd_bus_message_handler_t handler, sd_bus_error *error) {
+ int ifindex, r;
+ Link *l;
+
+ assert(m);
+ assert(message);
+ assert(handler);
+
+ r = bus_message_read_ifindex(message, error, &ifindex);
+ if (r < 0)
+ return r;
+
+ r = link_get_by_index(m, ifindex, &l);
+ if (r < 0)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %i not known", ifindex);
+
+ return handler(message, l, error);
+}
+
+static int bus_method_set_link_ntp_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_ntp_servers, error);
+}
+
+static int bus_method_set_link_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_dns_servers, error);
+}
+
+static int bus_method_set_link_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_dns_servers_ex, error);
+}
+
+static int bus_method_set_link_domains(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_domains, error);
+}
+
+static int bus_method_set_link_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_default_route, error);
+}
+
+static int bus_method_set_link_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_llmnr, error);
+}
+
+static int bus_method_set_link_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_mdns, error);
+}
+
+static int bus_method_set_link_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_dns_over_tls, error);
+}
+
+static int bus_method_set_link_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_dnssec, error);
+}
+
+static int bus_method_set_link_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_set_dnssec_negative_trust_anchors, error);
+}
+
+static int bus_method_revert_link_ntp(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_revert_ntp, error);
+}
+
+static int bus_method_revert_link_dns(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_revert_dns, error);
+}
+
+static int bus_method_renew_link(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_renew, error);
+}
+
+static int bus_method_force_renew_link(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_force_renew, error);
+}
+
+static int bus_method_reconfigure_link(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_reconfigure, error);
+}
+
+static int bus_method_reload(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Manager *manager = userdata;
+ Link *link;
+ int r;
+
+ r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+ "org.freedesktop.network1.reload",
+ NULL, true, UID_INVALID,
+ &manager->polkit_registry, error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Polkit will call us back */
+
+ r = netdev_load(manager, true);
+ if (r < 0)
+ return r;
+
+ r = network_reload(manager);
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(link, manager->links_by_index) {
+ r = link_reconfigure(link, /* force = */ false);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+static int bus_method_describe_link(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return call_link_method(userdata, message, bus_link_method_describe, error);
+}
+
+static int bus_method_describe(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *text = NULL;
+ Manager *manager = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ r = manager_build_json(manager, &v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build JSON data: %m");
+
+ r = json_variant_format(v, 0, &text);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format JSON data: %m");
+
+ r = sd_bus_message_new_method_return(message, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(reply, "s", text);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int property_get_namespace_id(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ uint64_t id = 0;
+ struct stat st;
+
+ assert(bus);
+ assert(reply);
+
+ /* Returns our own network namespace ID, i.e. the inode number of /proc/self/ns/net. This allows
+ * unprivileged clients to determine whether they are in the same network namespace as us (note that
+ * access to that path is restricted, thus they can't check directly unless privileged). */
+
+ if (stat("/proc/self/ns/net", &st) < 0) {
+ log_warning_errno(errno, "Failed to stat network namespace, ignoring: %m");
+ id = 0;
+ } else
+ id = st.st_ino;
+
+ return sd_bus_message_append(reply, "t", id);
+}
+
+static const sd_bus_vtable manager_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+
+ SD_BUS_PROPERTY("OperationalState", "s", property_get_operational_state, offsetof(Manager, operational_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("CarrierState", "s", property_get_carrier_state, offsetof(Manager, carrier_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("AddressState", "s", property_get_address_state, offsetof(Manager, address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("IPv4AddressState", "s", property_get_address_state, offsetof(Manager, ipv4_address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("IPv6AddressState", "s", property_get_address_state, offsetof(Manager, ipv6_address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("OnlineState", "s", property_get_online_state, offsetof(Manager, online_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("NamespaceId", "t", property_get_namespace_id, 0, SD_BUS_VTABLE_PROPERTY_CONST),
+
+ SD_BUS_METHOD_WITH_ARGS("ListLinks",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("a(iso)", links),
+ method_list_links,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("GetLinkByName",
+ SD_BUS_ARGS("s", name),
+ SD_BUS_RESULT("i", ifindex, "o", path),
+ method_get_link_by_name,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("GetLinkByIndex",
+ SD_BUS_ARGS("i", ifindex),
+ SD_BUS_RESULT("s", name, "o", path),
+ method_get_link_by_index,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkNTP",
+ SD_BUS_ARGS("i", ifindex, "as", servers),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_ntp_servers,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkDNS",
+ SD_BUS_ARGS("i", ifindex, "a(iay)", addresses),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_dns_servers,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkDNSEx",
+ SD_BUS_ARGS("i", ifindex, "a(iayqs)", addresses),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_dns_servers_ex,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkDomains",
+ SD_BUS_ARGS("i", ifindex, "a(sb)", domains),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_domains,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkDefaultRoute",
+ SD_BUS_ARGS("i", ifindex, "b", enable),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_default_route,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkLLMNR",
+ SD_BUS_ARGS("i", ifindex, "s", mode),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_llmnr,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkMulticastDNS",
+ SD_BUS_ARGS("i", ifindex, "s", mode),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_mdns,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkDNSOverTLS",
+ SD_BUS_ARGS("i", ifindex, "s", mode),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_dns_over_tls,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkDNSSEC",
+ SD_BUS_ARGS("i", ifindex, "s", mode),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_dnssec,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetLinkDNSSECNegativeTrustAnchors",
+ SD_BUS_ARGS("i", ifindex, "as", names),
+ SD_BUS_NO_RESULT,
+ bus_method_set_link_dnssec_negative_trust_anchors,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("RevertLinkNTP",
+ SD_BUS_ARGS("i", ifindex),
+ SD_BUS_NO_RESULT,
+ bus_method_revert_link_ntp,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("RevertLinkDNS",
+ SD_BUS_ARGS("i", ifindex),
+ SD_BUS_NO_RESULT,
+ bus_method_revert_link_dns,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("RenewLink",
+ SD_BUS_ARGS("i", ifindex),
+ SD_BUS_NO_RESULT,
+ bus_method_renew_link,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("ForceRenewLink",
+ SD_BUS_ARGS("i", ifindex),
+ SD_BUS_NO_RESULT,
+ bus_method_force_renew_link,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("ReconfigureLink",
+ SD_BUS_ARGS("i", ifindex),
+ SD_BUS_NO_RESULT,
+ bus_method_reconfigure_link,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("Reload",
+ SD_BUS_NO_ARGS,
+ SD_BUS_NO_RESULT,
+ bus_method_reload,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("DescribeLink",
+ SD_BUS_ARGS("i", ifindex),
+ SD_BUS_RESULT("s", json),
+ bus_method_describe_link,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("Describe",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("s", json),
+ bus_method_describe,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_VTABLE_END
+};
+
+int manager_send_changed_strv(Manager *manager, char **properties) {
+ assert(manager);
+ assert(properties);
+
+ if (sd_bus_is_ready(manager->bus) <= 0)
+ return 0;
+
+ return sd_bus_emit_properties_changed_strv(
+ manager->bus,
+ "/org/freedesktop/network1",
+ "org.freedesktop.network1.Manager",
+ properties);
+}
+
+const BusObjectImplementation manager_object = {
+ "/org/freedesktop/network1",
+ "org.freedesktop.network1.Manager",
+ .vtables = BUS_VTABLES(manager_vtable),
+ .children = BUS_IMPLEMENTATIONS(&dhcp_server_object, &link_object, &network_object),
+};
diff --git a/src/network/networkd-manager-bus.h b/src/network/networkd-manager-bus.h
new file mode 100644
index 0000000..5cd7f16
--- /dev/null
+++ b/src/network/networkd-manager-bus.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "bus-object.h"
+
+typedef struct Manager Manager;
+
+extern const BusObjectImplementation manager_object;
+
+int manager_send_changed_strv(Manager *m, char **properties);
diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c
new file mode 100644
index 0000000..79ef6d4
--- /dev/null
+++ b/src/network/networkd-manager.c
@@ -0,0 +1,1075 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include <linux/if.h>
+#include <linux/fib_rules.h>
+#include <linux/nexthop.h>
+#include <linux/nl80211.h>
+
+#include "sd-netlink.h"
+
+#include "alloc-util.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "bus-log-control-api.h"
+#include "bus-polkit.h"
+#include "bus-util.h"
+#include "conf-parser.h"
+#include "daemon-util.h"
+#include "def.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "dns-domain.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "firewall-util.h"
+#include "fs-util.h"
+#include "local-addresses.h"
+#include "netlink-util.h"
+#include "network-internal.h"
+#include "networkd-address-pool.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-server-bus.h"
+#include "networkd-dhcp6.h"
+#include "networkd-link-bus.h"
+#include "networkd-manager-bus.h"
+#include "networkd-manager.h"
+#include "networkd-neighbor.h"
+#include "networkd-network-bus.h"
+#include "networkd-nexthop.h"
+#include "networkd-queue.h"
+#include "networkd-route.h"
+#include "networkd-routing-policy-rule.h"
+#include "networkd-speed-meter.h"
+#include "networkd-state-file.h"
+#include "networkd-wifi.h"
+#include "networkd-wiphy.h"
+#include "ordered-set.h"
+#include "path-lookup.h"
+#include "path-util.h"
+#include "qdisc.h"
+#include "selinux-util.h"
+#include "set.h"
+#include "signal-util.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "sysctl-util.h"
+#include "tclass.h"
+#include "tmpfile-util.h"
+#include "tuntap.h"
+#include "udev-util.h"
+
+/* use 128 MB for receive socket kernel queue. */
+#define RCVBUF_SIZE (128*1024*1024)
+
+static int manager_reset_all(Manager *m) {
+ Link *link;
+ int r;
+
+ assert(m);
+
+ HASHMAP_FOREACH(link, m->links_by_index) {
+ r = link_reconfigure_after_sleep(link);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to reconfigure interface: %m");
+ link_enter_failed(link);
+ }
+ }
+
+ return 0;
+}
+
+static int match_prepare_for_sleep(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) {
+ Manager *m = ASSERT_PTR(userdata);
+ int b, r;
+
+ assert(message);
+
+ r = sd_bus_message_read(message, "b", &b);
+ if (r < 0) {
+ bus_log_parse_error(r);
+ return 0;
+ }
+
+ if (b)
+ return 0;
+
+ log_debug("Coming back from suspend, resetting all connections...");
+
+ (void) manager_reset_all(m);
+
+ return 0;
+}
+
+static int on_connected(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) {
+ Manager *m = ASSERT_PTR(userdata);
+
+ assert(message);
+
+ /* Did we get a timezone or transient hostname from DHCP while D-Bus wasn't up yet? */
+ if (m->dynamic_hostname)
+ (void) manager_set_hostname(m, m->dynamic_hostname);
+ if (m->dynamic_timezone)
+ (void) manager_set_timezone(m, m->dynamic_timezone);
+ if (m->product_uuid_requested)
+ (void) manager_request_product_uuid(m);
+
+ return 0;
+}
+
+static int manager_connect_bus(Manager *m) {
+ int r;
+
+ assert(m);
+ assert(!m->bus);
+
+ r = bus_open_system_watch_bind_with_description(&m->bus, "bus-api-network");
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to bus: %m");
+
+ r = bus_add_implementation(m->bus, &manager_object, m);
+ if (r < 0)
+ return r;
+
+ r = bus_log_control_api_register(m->bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.network1", 0, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request name: %m");
+
+ r = sd_bus_attach_event(m->bus, m->event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ r = sd_bus_match_signal_async(
+ m->bus,
+ NULL,
+ "org.freedesktop.DBus.Local",
+ NULL,
+ "org.freedesktop.DBus.Local",
+ "Connected",
+ on_connected, NULL, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request match on Connected signal: %m");
+
+ r = sd_bus_match_signal_async(
+ m->bus,
+ NULL,
+ "org.freedesktop.login1",
+ "/org/freedesktop/login1",
+ "org.freedesktop.login1.Manager",
+ "PrepareForSleep",
+ match_prepare_for_sleep, NULL, m);
+ if (r < 0)
+ log_warning_errno(r, "Failed to request match for PrepareForSleep, ignoring: %m");
+
+ return 0;
+}
+
+static int manager_process_uevent(sd_device_monitor *monitor, sd_device *device, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+ sd_device_action_t action;
+ const char *s;
+ int r;
+
+ assert(device);
+
+ r = sd_device_get_action(device, &action);
+ if (r < 0)
+ return log_device_warning_errno(device, r, "Failed to get udev action, ignoring: %m");
+
+ r = sd_device_get_subsystem(device, &s);
+ if (r < 0)
+ return log_device_warning_errno(device, r, "Failed to get subsystem, ignoring: %m");
+
+ if (streq(s, "net"))
+ r = manager_udev_process_link(m, device, action);
+ else if (streq(s, "ieee80211"))
+ r = manager_udev_process_wiphy(m, device, action);
+ else if (streq(s, "rfkill"))
+ r = manager_udev_process_rfkill(m, device, action);
+ else {
+ log_device_debug(device, "Received device with unexpected subsystem \"%s\", ignoring.", s);
+ return 0;
+ }
+ if (r < 0)
+ log_device_warning_errno(device, r, "Failed to process \"%s\" uevent, ignoring: %m",
+ device_action_to_string(action));
+
+ return 0;
+}
+
+static int manager_connect_udev(Manager *m) {
+ int r;
+
+ /* udev does not initialize devices inside containers, so we rely on them being already
+ * initialized before entering the container. */
+ if (!udev_available())
+ return 0;
+
+ r = sd_device_monitor_new(&m->device_monitor);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize device monitor: %m");
+
+ r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "net", NULL);
+ if (r < 0)
+ return log_error_errno(r, "Could not add device monitor filter for net subsystem: %m");
+
+ r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "ieee80211", NULL);
+ if (r < 0)
+ return log_error_errno(r, "Could not add device monitor filter for ieee80211 subsystem: %m");
+
+ r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "rfkill", NULL);
+ if (r < 0)
+ return log_error_errno(r, "Could not add device monitor filter for rfkill subsystem: %m");
+
+ r = sd_device_monitor_attach_event(m->device_monitor, m->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach event to device monitor: %m");
+
+ r = sd_device_monitor_start(m->device_monitor, manager_process_uevent, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+
+ return 0;
+}
+
+static int manager_listen_fds(Manager *m, int *ret_rtnl_fd) {
+ _cleanup_strv_free_ char **names = NULL;
+ int n, rtnl_fd = -1;
+
+ assert(m);
+ assert(ret_rtnl_fd);
+
+ n = sd_listen_fds_with_names(/* unset_environment = */ true, &names);
+ if (n < 0)
+ return n;
+
+ if (strv_length(names) != (size_t) n)
+ return -EINVAL;
+
+ for (int i = 0; i < n; i++) {
+ int fd = i + SD_LISTEN_FDS_START;
+
+ if (sd_is_socket(fd, AF_NETLINK, SOCK_RAW, -1) > 0) {
+ if (rtnl_fd >= 0) {
+ log_debug("Received multiple netlink socket, ignoring.");
+ safe_close(fd);
+ continue;
+ }
+
+ rtnl_fd = fd;
+ continue;
+ }
+
+ if (manager_add_tuntap_fd(m, fd, names[i]) >= 0)
+ continue;
+
+ if (m->test_mode)
+ safe_close(fd);
+ else
+ close_and_notify_warn(fd, names[i]);
+ }
+
+ *ret_rtnl_fd = rtnl_fd;
+ return 0;
+}
+
+static int manager_connect_genl(Manager *m) {
+ int r;
+
+ assert(m);
+
+ r = sd_genl_socket_open(&m->genl);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_increase_rxbuf(m->genl, RCVBUF_SIZE);
+ if (r < 0)
+ log_warning_errno(r, "Failed to increase receive buffer size for general netlink socket, ignoring: %m");
+
+ r = sd_netlink_attach_event(m->genl, m->event, 0);
+ if (r < 0)
+ return r;
+
+ r = genl_add_match(m->genl, NULL, NL80211_GENL_NAME, NL80211_MULTICAST_GROUP_CONFIG, 0,
+ &manager_genl_process_nl80211_config, NULL, m, "network-genl_process_nl80211_config");
+ if (r < 0 && r != -EOPNOTSUPP)
+ return r;
+
+ r = genl_add_match(m->genl, NULL, NL80211_GENL_NAME, NL80211_MULTICAST_GROUP_MLME, 0,
+ &manager_genl_process_nl80211_mlme, NULL, m, "network-genl_process_nl80211_mlme");
+ if (r < 0 && r != -EOPNOTSUPP)
+ return r;
+
+ return 0;
+}
+
+static int manager_setup_rtnl_filter(Manager *manager) {
+ struct sock_filter filter[] = {
+ /* Check the packet length. */
+ BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0), /* A <- packet length */
+ BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct nlmsghdr), 1, 0), /* A (packet length) >= sizeof(struct nlmsghdr) ? */
+ BPF_STMT(BPF_RET + BPF_K, 0), /* reject */
+ /* Always accept multipart message. */
+ BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct nlmsghdr, nlmsg_flags)), /* A <- message flags */
+ BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, htobe16(NLM_F_MULTI), 0, 1), /* message flags has NLM_F_MULTI ? */
+ BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */
+ /* Accept all message types except for RTM_NEWNEIGH or RTM_DELNEIGH. */
+ BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct nlmsghdr, nlmsg_type)), /* A <- message type */
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, htobe16(RTM_NEWNEIGH), 2, 0), /* message type == RTM_NEWNEIGH ? */
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, htobe16(RTM_DELNEIGH), 1, 0), /* message type == RTM_DELNEIGH ? */
+ BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */
+ /* Check the packet length. */
+ BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0), /* A <- packet length */
+ BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct nlmsghdr) + sizeof(struct ndmsg), 1, 0),
+ /* packet length >= sizeof(struct nlmsghdr) + sizeof(struct ndmsg) ? */
+ BPF_STMT(BPF_RET + BPF_K, 0), /* reject */
+ /* Reject the message when the neighbor state does not have NUD_PERMANENT flag. */
+ BPF_STMT(BPF_LD + BPF_H + BPF_ABS, sizeof(struct nlmsghdr) + offsetof(struct ndmsg, ndm_state)),
+ /* A <- neighbor state */
+ BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, htobe16(NUD_PERMANENT), 1, 0), /* neighbor state has NUD_PERMANENT ? */
+ BPF_STMT(BPF_RET + BPF_K, 0), /* reject */
+ BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */
+ };
+
+ assert(manager);
+ assert(manager->rtnl);
+
+ return sd_netlink_attach_filter(manager->rtnl, ELEMENTSOF(filter), filter);
+}
+
+static int manager_connect_rtnl(Manager *m, int fd) {
+ _unused_ _cleanup_close_ int fd_close = fd;
+ int r;
+
+ assert(m);
+
+ /* This takes input fd. */
+
+ if (fd < 0)
+ r = sd_netlink_open(&m->rtnl);
+ else
+ r = sd_netlink_open_fd(&m->rtnl, fd);
+ if (r < 0)
+ return r;
+ TAKE_FD(fd_close);
+
+ /* Bump receiver buffer, but only if we are not called via socket activation, as in that
+ * case systemd sets the receive buffer size for us, and the value in the .socket unit
+ * should take full effect. */
+ if (fd < 0) {
+ r = sd_netlink_increase_rxbuf(m->rtnl, RCVBUF_SIZE);
+ if (r < 0)
+ log_warning_errno(r, "Failed to increase receive buffer size for rtnl socket, ignoring: %m");
+ }
+
+ r = sd_netlink_attach_event(m->rtnl, m->event, 0);
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_NEWLINK, &manager_rtnl_process_link, NULL, m, "network-rtnl_process_link");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_DELLINK, &manager_rtnl_process_link, NULL, m, "network-rtnl_process_link");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_NEWQDISC, &manager_rtnl_process_qdisc, NULL, m, "network-rtnl_process_qdisc");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_DELQDISC, &manager_rtnl_process_qdisc, NULL, m, "network-rtnl_process_qdisc");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_NEWTCLASS, &manager_rtnl_process_tclass, NULL, m, "network-rtnl_process_tclass");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_DELTCLASS, &manager_rtnl_process_tclass, NULL, m, "network-rtnl_process_tclass");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_NEWADDR, &manager_rtnl_process_address, NULL, m, "network-rtnl_process_address");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_DELADDR, &manager_rtnl_process_address, NULL, m, "network-rtnl_process_address");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_NEWNEIGH, &manager_rtnl_process_neighbor, NULL, m, "network-rtnl_process_neighbor");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_DELNEIGH, &manager_rtnl_process_neighbor, NULL, m, "network-rtnl_process_neighbor");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_NEWROUTE, &manager_rtnl_process_route, NULL, m, "network-rtnl_process_route");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_DELROUTE, &manager_rtnl_process_route, NULL, m, "network-rtnl_process_route");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_NEWRULE, &manager_rtnl_process_rule, NULL, m, "network-rtnl_process_rule");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_DELRULE, &manager_rtnl_process_rule, NULL, m, "network-rtnl_process_rule");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_NEWNEXTHOP, &manager_rtnl_process_nexthop, NULL, m, "network-rtnl_process_nexthop");
+ if (r < 0)
+ return r;
+
+ r = netlink_add_match(m->rtnl, NULL, RTM_DELNEXTHOP, &manager_rtnl_process_nexthop, NULL, m, "network-rtnl_process_nexthop");
+ if (r < 0)
+ return r;
+
+ return manager_setup_rtnl_filter(m);
+}
+
+static int manager_dirty_handler(sd_event_source *s, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+ Link *link;
+ int r;
+
+ if (m->dirty) {
+ r = manager_save(m);
+ if (r < 0)
+ log_warning_errno(r, "Failed to update state file %s, ignoring: %m", m->state_file);
+ }
+
+ SET_FOREACH(link, m->dirty_links) {
+ r = link_save_and_clean(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to update link state file %s, ignoring: %m", link->state_file);
+ }
+
+ return 1;
+}
+
+static int signal_terminate_callback(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+
+ m->restarting = false;
+
+ log_debug("Terminate operation initiated.");
+
+ return sd_event_exit(sd_event_source_get_event(s), 0);
+}
+
+static int signal_restart_callback(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+
+ m->restarting = true;
+
+ log_debug("Restart operation initiated.");
+
+ return sd_event_exit(sd_event_source_get_event(s), 0);
+}
+
+static int manager_set_keep_configuration(Manager *m) {
+ int r;
+
+ assert(m);
+
+ if (in_initrd()) {
+ log_debug("Running in initrd, keep DHCPv4 addresses on stopping networkd by default.");
+ m->keep_configuration = KEEP_CONFIGURATION_DHCP_ON_STOP;
+ return 0;
+ }
+
+ r = path_is_network_fs("/");
+ if (r < 0)
+ return log_error_errno(r, "Failed to detect if root is network filesystem: %m");
+ if (r == 0) {
+ m->keep_configuration = _KEEP_CONFIGURATION_INVALID;
+ return 0;
+ }
+
+ log_debug("Running on network filesystem, enabling KeepConfiguration= by default.");
+ m->keep_configuration = KEEP_CONFIGURATION_YES;
+ return 0;
+}
+
+int manager_setup(Manager *m) {
+ _cleanup_close_ int rtnl_fd = -1;
+ int r;
+
+ assert(m);
+
+ r = sd_event_default(&m->event);
+ if (r < 0)
+ return r;
+
+ assert_se(sigprocmask_many(SIG_SETMASK, NULL, SIGINT, SIGTERM, SIGUSR2, -1) >= 0);
+
+ (void) sd_event_set_watchdog(m->event, true);
+ (void) sd_event_add_signal(m->event, NULL, SIGTERM, signal_terminate_callback, m);
+ (void) sd_event_add_signal(m->event, NULL, SIGINT, signal_terminate_callback, m);
+ (void) sd_event_add_signal(m->event, NULL, SIGUSR2, signal_restart_callback, m);
+
+ r = sd_event_add_post(m->event, NULL, manager_dirty_handler, m);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_post(m->event, NULL, manager_process_requests, m);
+ if (r < 0)
+ return r;
+
+ r = manager_listen_fds(m, &rtnl_fd);
+ if (r < 0)
+ return r;
+
+ r = manager_connect_rtnl(m, TAKE_FD(rtnl_fd));
+ if (r < 0)
+ return r;
+
+ r = manager_connect_genl(m);
+ if (r < 0)
+ return r;
+
+ if (m->test_mode)
+ return 0;
+
+ r = manager_connect_bus(m);
+ if (r < 0)
+ return r;
+
+ r = manager_connect_udev(m);
+ if (r < 0)
+ return r;
+
+ r = sd_resolve_default(&m->resolve);
+ if (r < 0)
+ return r;
+
+ r = sd_resolve_attach_event(m->resolve, m->event, 0);
+ if (r < 0)
+ return r;
+
+ r = address_pool_setup_default(m);
+ if (r < 0)
+ return r;
+
+ r = manager_set_keep_configuration(m);
+ if (r < 0)
+ return r;
+
+ m->state_file = strdup("/run/systemd/netif/state");
+ if (!m->state_file)
+ return -ENOMEM;
+
+ return 0;
+}
+
+int manager_new(Manager **ret, bool test_mode) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+
+ m = new(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ *m = (Manager) {
+ .keep_configuration = _KEEP_CONFIGURATION_INVALID,
+ .test_mode = test_mode,
+ .speed_meter_interval_usec = SPEED_METER_DEFAULT_TIME_INTERVAL,
+ .online_state = _LINK_ONLINE_STATE_INVALID,
+ .manage_foreign_routes = true,
+ .manage_foreign_rules = true,
+ .ethtool_fd = -1,
+ .dhcp_duid.type = DUID_TYPE_EN,
+ .dhcp6_duid.type = DUID_TYPE_EN,
+ .duid_product_uuid.type = DUID_TYPE_UUID,
+ };
+
+ *ret = TAKE_PTR(m);
+ return 0;
+}
+
+Manager* manager_free(Manager *m) {
+ Link *link;
+
+ if (!m)
+ return NULL;
+
+ free(m->state_file);
+
+ HASHMAP_FOREACH(link, m->links_by_index)
+ (void) link_stop_engines(link, true);
+
+ m->request_queue = ordered_set_free(m->request_queue);
+
+ m->dirty_links = set_free_with_destructor(m->dirty_links, link_unref);
+ m->links_by_name = hashmap_free(m->links_by_name);
+ m->links_by_hw_addr = hashmap_free(m->links_by_hw_addr);
+ m->links_by_dhcp_pd_subnet_prefix = hashmap_free(m->links_by_dhcp_pd_subnet_prefix);
+ m->links_by_index = hashmap_free_with_destructor(m->links_by_index, link_unref);
+
+ m->dhcp_pd_subnet_ids = set_free(m->dhcp_pd_subnet_ids);
+ m->networks = ordered_hashmap_free_with_destructor(m->networks, network_unref);
+
+ m->netdevs = hashmap_free_with_destructor(m->netdevs, netdev_unref);
+
+ m->tuntap_fds_by_name = hashmap_free(m->tuntap_fds_by_name);
+
+ m->wiphy_by_name = hashmap_free(m->wiphy_by_name);
+ m->wiphy_by_index = hashmap_free_with_destructor(m->wiphy_by_index, wiphy_free);
+
+ ordered_set_free_free(m->address_pools);
+
+ hashmap_free(m->route_table_names_by_number);
+ hashmap_free(m->route_table_numbers_by_name);
+
+ set_free(m->rules);
+
+ sd_netlink_unref(m->rtnl);
+ sd_netlink_unref(m->genl);
+ sd_resolve_unref(m->resolve);
+
+ /* reject (e.g. unreachable) type routes are managed by Manager, but may be referenced by a
+ * link. E.g., DHCP6 with prefix delegation creates unreachable routes, and they are referenced
+ * by the upstream link. And the links may be referenced by netlink slots. Hence, two
+ * set_free() must be called after the above sd_netlink_unref(). */
+ m->routes = set_free(m->routes);
+
+ m->nexthops = set_free(m->nexthops);
+ m->nexthops_by_id = hashmap_free(m->nexthops_by_id);
+
+ sd_event_source_unref(m->speed_meter_event_source);
+ sd_event_unref(m->event);
+
+ sd_device_monitor_unref(m->device_monitor);
+
+ bus_verify_polkit_async_registry_free(m->polkit_registry);
+ sd_bus_flush_close_unref(m->bus);
+
+ free(m->dynamic_timezone);
+ free(m->dynamic_hostname);
+
+ safe_close(m->ethtool_fd);
+
+ m->fw_ctx = fw_ctx_free(m->fw_ctx);
+
+ return mfree(m);
+}
+
+int manager_start(Manager *m) {
+ Link *link;
+ int r;
+
+ assert(m);
+
+ r = manager_start_speed_meter(m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize speed meter: %m");
+
+ /* The dirty handler will deal with future serialization, but the first one
+ must be done explicitly. */
+
+ r = manager_save(m);
+ if (r < 0)
+ log_warning_errno(r, "Failed to update state file %s, ignoring: %m", m->state_file);
+
+ HASHMAP_FOREACH(link, m->links_by_index) {
+ r = link_save(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to update link state file %s, ignoring: %m", link->state_file);
+ }
+
+ return 0;
+}
+
+int manager_load_config(Manager *m) {
+ int r;
+
+ r = netdev_load(m, false);
+ if (r < 0)
+ return r;
+
+ manager_clear_unmanaged_tuntap_fds(m);
+
+ r = network_load(m, &m->networks);
+ if (r < 0)
+ return r;
+
+ return manager_build_dhcp_pd_subnet_ids(m);
+}
+
+static int manager_enumerate_internal(
+ Manager *m,
+ sd_netlink *nl,
+ sd_netlink_message *req,
+ int (*process)(sd_netlink *, sd_netlink_message *, Manager *)) {
+
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *reply = NULL;
+ int k, r;
+
+ assert(m);
+ assert(nl);
+ assert(req);
+ assert(process);
+
+ r = sd_netlink_message_set_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(nl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ m->enumerating = true;
+ for (sd_netlink_message *reply_one = reply; reply_one; reply_one = sd_netlink_message_next(reply_one)) {
+ k = process(nl, reply_one, m);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+ m->enumerating = false;
+
+ return r;
+}
+
+static int manager_enumerate_links(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ r = sd_rtnl_message_new_link(m->rtnl, &req, RTM_GETLINK, 0);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_link);
+}
+
+static int manager_enumerate_qdisc(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ r = sd_rtnl_message_new_traffic_control(m->rtnl, &req, RTM_GETQDISC, 0, 0, 0);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_qdisc);
+}
+
+static int manager_enumerate_tclass(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ r = sd_rtnl_message_new_traffic_control(m->rtnl, &req, RTM_GETTCLASS, 0, 0, 0);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_tclass);
+}
+
+static int manager_enumerate_addresses(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ r = sd_rtnl_message_new_addr(m->rtnl, &req, RTM_GETADDR, 0, 0);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_address);
+}
+
+static int manager_enumerate_neighbors(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ r = sd_rtnl_message_new_neigh(m->rtnl, &req, RTM_GETNEIGH, 0, AF_UNSPEC);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_neighbor);
+}
+
+static int manager_enumerate_routes(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ if (!m->manage_foreign_routes)
+ return 0;
+
+ r = sd_rtnl_message_new_route(m->rtnl, &req, RTM_GETROUTE, 0, 0);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_route);
+}
+
+static int manager_enumerate_rules(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ if (!m->manage_foreign_rules)
+ return 0;
+
+ r = sd_rtnl_message_new_routing_policy_rule(m->rtnl, &req, RTM_GETRULE, 0);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_rule);
+}
+
+static int manager_enumerate_nexthop(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ r = sd_rtnl_message_new_nexthop(m->rtnl, &req, RTM_GETNEXTHOP, 0, 0);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_nexthop);
+}
+
+static int manager_enumerate_nl80211_wiphy(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->genl);
+
+ r = sd_genl_message_new(m->genl, NL80211_GENL_NAME, NL80211_CMD_GET_WIPHY, &req);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->genl, req, manager_genl_process_nl80211_wiphy);
+}
+
+static int manager_enumerate_nl80211_config(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->genl);
+
+ r = sd_genl_message_new(m->genl, NL80211_GENL_NAME, NL80211_CMD_GET_INTERFACE, &req);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, m->genl, req, manager_genl_process_nl80211_config);
+}
+
+static int manager_enumerate_nl80211_mlme(Manager *m) {
+ Link *link;
+ int r;
+
+ assert(m);
+ assert(m->genl);
+
+ HASHMAP_FOREACH(link, m->links_by_index) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+
+ if (link->wlan_iftype != NL80211_IFTYPE_STATION)
+ continue;
+
+ r = sd_genl_message_new(m->genl, NL80211_GENL_NAME, NL80211_CMD_GET_STATION, &req);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(req, NL80211_ATTR_IFINDEX, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = manager_enumerate_internal(m, m->genl, req, manager_genl_process_nl80211_mlme);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int manager_enumerate(Manager *m) {
+ int r;
+
+ r = manager_enumerate_links(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not enumerate links: %m");
+
+ r = manager_enumerate_qdisc(m);
+ if (r == -EOPNOTSUPP)
+ log_debug_errno(r, "Could not enumerate QDiscs, ignoring: %m");
+ else if (r < 0)
+ return log_error_errno(r, "Could not enumerate QDisc: %m");
+
+ r = manager_enumerate_tclass(m);
+ if (r == -EOPNOTSUPP)
+ log_debug_errno(r, "Could not enumerate TClasses, ignoring: %m");
+ else if (r < 0)
+ return log_error_errno(r, "Could not enumerate TClass: %m");
+
+ r = manager_enumerate_addresses(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not enumerate addresses: %m");
+
+ r = manager_enumerate_neighbors(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not enumerate neighbors: %m");
+
+ /* NextHop support is added in kernel v5.3 (65ee00a9409f751188a8cdc0988167858eb4a536),
+ * and older kernels return -EOPNOTSUPP, or -EINVAL if SELinux is enabled. */
+ r = manager_enumerate_nexthop(m);
+ if (r == -EOPNOTSUPP || (r == -EINVAL && mac_selinux_enforcing()))
+ log_debug_errno(r, "Could not enumerate nexthops, ignoring: %m");
+ else if (r < 0)
+ return log_error_errno(r, "Could not enumerate nexthops: %m");
+
+ r = manager_enumerate_routes(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not enumerate routes: %m");
+
+ /* If kernel is built with CONFIG_FIB_RULES=n, it returns -EOPNOTSUPP. */
+ r = manager_enumerate_rules(m);
+ if (r == -EOPNOTSUPP)
+ log_debug_errno(r, "Could not enumerate routing policy rules, ignoring: %m");
+ else if (r < 0)
+ return log_error_errno(r, "Could not enumerate routing policy rules: %m");
+
+ r = manager_enumerate_nl80211_wiphy(m);
+ if (r == -EOPNOTSUPP)
+ log_debug_errno(r, "Could not enumerate wireless LAN phy, ignoring: %m");
+ else if (r < 0)
+ return log_error_errno(r, "Could not enumerate wireless LAN phy: %m");
+
+ r = manager_enumerate_nl80211_config(m);
+ if (r == -EOPNOTSUPP)
+ log_debug_errno(r, "Could not enumerate wireless LAN interfaces, ignoring: %m");
+ else if (r < 0)
+ return log_error_errno(r, "Could not enumerate wireless LAN interfaces: %m");
+
+ r = manager_enumerate_nl80211_mlme(m);
+ if (r == -EOPNOTSUPP)
+ log_debug_errno(r, "Could not enumerate wireless LAN stations, ignoring: %m");
+ else if (r < 0)
+ return log_error_errno(r, "Could not enumerate wireless LAN stations: %m");
+
+ return 0;
+}
+
+static int set_hostname_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
+ const sd_bus_error *e;
+ int r;
+
+ assert(m);
+
+ e = sd_bus_message_get_error(m);
+ if (e) {
+ r = sd_bus_error_get_errno(e);
+ log_warning_errno(r, "Could not set hostname: %s", bus_error_message(e, r));
+ }
+
+ return 1;
+}
+
+int manager_set_hostname(Manager *m, const char *hostname) {
+ int r;
+
+ log_debug("Setting transient hostname: '%s'", strna(hostname));
+
+ r = free_and_strdup_warn(&m->dynamic_hostname, hostname);
+ if (r < 0)
+ return r;
+
+ if (sd_bus_is_ready(m->bus) <= 0) {
+ log_debug("Not connected to system bus, setting system hostname later.");
+ return 0;
+ }
+
+ r = bus_call_method_async(
+ m->bus,
+ NULL,
+ bus_hostname,
+ "SetHostname",
+ set_hostname_handler,
+ m,
+ "sb",
+ hostname,
+ false);
+ if (r < 0)
+ return log_error_errno(r, "Could not set transient hostname: %m");
+
+ return 0;
+}
+
+static int set_timezone_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
+ const sd_bus_error *e;
+ int r;
+
+ assert(m);
+
+ e = sd_bus_message_get_error(m);
+ if (e) {
+ r = sd_bus_error_get_errno(e);
+ log_warning_errno(r, "Could not set timezone: %s", bus_error_message(e, r));
+ }
+
+ return 1;
+}
+
+int manager_set_timezone(Manager *m, const char *tz) {
+ int r;
+
+ assert(m);
+ assert(tz);
+
+ log_debug("Setting system timezone: '%s'", tz);
+ r = free_and_strdup_warn(&m->dynamic_timezone, tz);
+ if (r < 0)
+ return r;
+
+ if (sd_bus_is_ready(m->bus) <= 0) {
+ log_debug("Not connected to system bus, setting system timezone later.");
+ return 0;
+ }
+
+ r = bus_call_method_async(
+ m->bus,
+ NULL,
+ bus_timedate,
+ "SetTimezone",
+ set_timezone_handler,
+ m,
+ "sb",
+ tz,
+ false);
+ if (r < 0)
+ return log_error_errno(r, "Could not set timezone: %m");
+
+ return 0;
+}
diff --git a/src/network/networkd-manager.h b/src/network/networkd-manager.h
new file mode 100644
index 0000000..2db9278
--- /dev/null
+++ b/src/network/networkd-manager.h
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+#include "sd-device.h"
+#include "sd-event.h"
+#include "sd-id128.h"
+#include "sd-netlink.h"
+#include "sd-resolve.h"
+
+#include "dhcp-identifier.h"
+#include "firewall-util.h"
+#include "hashmap.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "ordered-set.h"
+#include "set.h"
+#include "time-util.h"
+
+struct Manager {
+ sd_netlink *rtnl;
+ /* lazy initialized */
+ sd_netlink *genl;
+ sd_event *event;
+ sd_resolve *resolve;
+ sd_bus *bus;
+ sd_device_monitor *device_monitor;
+ Hashmap *polkit_registry;
+ int ethtool_fd;
+
+ KeepConfiguration keep_configuration;
+
+ bool test_mode;
+ bool enumerating;
+ bool dirty;
+ bool restarting;
+ bool manage_foreign_routes;
+ bool manage_foreign_rules;
+
+ Set *dirty_links;
+
+ char *state_file;
+ LinkOperationalState operational_state;
+ LinkCarrierState carrier_state;
+ LinkAddressState address_state;
+ LinkAddressState ipv4_address_state;
+ LinkAddressState ipv6_address_state;
+ LinkOnlineState online_state;
+
+ Hashmap *links_by_index;
+ Hashmap *links_by_name;
+ Hashmap *links_by_hw_addr;
+ Hashmap *links_by_dhcp_pd_subnet_prefix;
+ Hashmap *netdevs;
+ OrderedHashmap *networks;
+ OrderedSet *address_pools;
+ Set *dhcp_pd_subnet_ids;
+
+ DUID dhcp_duid;
+ DUID dhcp6_duid;
+ DUID duid_product_uuid;
+ bool has_product_uuid;
+ bool product_uuid_requested;
+
+ char* dynamic_hostname;
+ char* dynamic_timezone;
+
+ Set *rules;
+
+ /* Manage nexthops by id. */
+ Hashmap *nexthops_by_id;
+
+ /* Manager stores nexthops without RTA_OIF attribute. */
+ Set *nexthops;
+
+ /* Manager stores routes without RTA_OIF attribute. */
+ unsigned route_remove_messages;
+ Set *routes;
+
+ /* Route table name */
+ Hashmap *route_table_numbers_by_name;
+ Hashmap *route_table_names_by_number;
+
+ /* Wiphy */
+ Hashmap *wiphy_by_index;
+ Hashmap *wiphy_by_name;
+
+ /* For link speed meter */
+ bool use_speed_meter;
+ sd_event_source *speed_meter_event_source;
+ usec_t speed_meter_interval_usec;
+ usec_t speed_meter_usec_new;
+ usec_t speed_meter_usec_old;
+
+ bool bridge_mdb_on_master_not_supported;
+
+ FirewallContext *fw_ctx;
+
+ OrderedSet *request_queue;
+
+ Hashmap *tuntap_fds_by_name;
+};
+
+int manager_new(Manager **ret, bool test_mode);
+Manager* manager_free(Manager *m);
+
+int manager_setup(Manager *m);
+int manager_start(Manager *m);
+
+int manager_load_config(Manager *m);
+
+int manager_enumerate(Manager *m);
+
+int manager_set_hostname(Manager *m, const char *hostname);
+int manager_set_timezone(Manager *m, const char *timezone);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
diff --git a/src/network/networkd-ndisc.c b/src/network/networkd-ndisc.c
new file mode 100644
index 0000000..5fb5d96
--- /dev/null
+++ b/src/network/networkd-ndisc.c
@@ -0,0 +1,1211 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2014 Intel Corporation. All rights reserved.
+***/
+
+#include <arpa/inet.h>
+#include <netinet/icmp6.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+
+#include "sd-ndisc.h"
+
+#include "event-util.h"
+#include "missing_network.h"
+#include "networkd-address-generation.h"
+#include "networkd-address.h"
+#include "networkd-dhcp6.h"
+#include "networkd-manager.h"
+#include "networkd-ndisc.h"
+#include "networkd-queue.h"
+#include "networkd-route.h"
+#include "networkd-state-file.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+#define NDISC_DNSSL_MAX 64U
+#define NDISC_RDNSS_MAX 64U
+
+bool link_ipv6_accept_ra_enabled(Link *link) {
+ assert(link);
+
+ if (!socket_ipv6_is_supported())
+ return false;
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ if (link->iftype == ARPHRD_CAN)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ if (!link_may_have_ipv6ll(link, /* check_multicast = */ true))
+ return false;
+
+ assert(link->network->ipv6_accept_ra >= 0);
+ return link->network->ipv6_accept_ra;
+}
+
+void network_adjust_ipv6_accept_ra(Network *network) {
+ assert(network);
+
+ if (!FLAGS_SET(network->link_local, ADDRESS_FAMILY_IPV6)) {
+ if (network->ipv6_accept_ra > 0)
+ log_warning("%s: IPv6AcceptRA= is enabled but IPv6 link-local addressing is disabled or not supported. "
+ "Disabling IPv6AcceptRA=.", network->filename);
+ network->ipv6_accept_ra = false;
+ }
+
+ if (network->ipv6_accept_ra < 0)
+ /* default to accept RA if ip_forward is disabled and ignore RA if ip_forward is enabled */
+ network->ipv6_accept_ra = !FLAGS_SET(network->ip_forward, ADDRESS_FAMILY_IPV6);
+
+ /* When RouterAllowList=, PrefixAllowList= or RouteAllowList= are specified, then
+ * RouterDenyList=, PrefixDenyList= or RouteDenyList= are ignored, respectively. */
+ if (!set_isempty(network->ndisc_allow_listed_router))
+ network->ndisc_deny_listed_router = set_free_free(network->ndisc_deny_listed_router);
+ if (!set_isempty(network->ndisc_allow_listed_prefix))
+ network->ndisc_deny_listed_prefix = set_free_free(network->ndisc_deny_listed_prefix);
+ if (!set_isempty(network->ndisc_allow_listed_route_prefix))
+ network->ndisc_deny_listed_route_prefix = set_free_free(network->ndisc_deny_listed_route_prefix);
+}
+
+static int ndisc_check_ready(Link *link);
+
+static int ndisc_address_ready_callback(Address *address) {
+ Address *a;
+
+ assert(address);
+ assert(address->link);
+
+ SET_FOREACH(a, address->link->addresses)
+ if (a->source == NETWORK_CONFIG_SOURCE_NDISC)
+ a->callback = NULL;
+
+ return ndisc_check_ready(address->link);
+}
+
+static int ndisc_check_ready(Link *link) {
+ bool found = false, ready = false;
+ Address *address;
+
+ assert(link);
+
+ if (link->ndisc_messages > 0) {
+ log_link_debug(link, "%s(): SLAAC addresses and routes are not set.", __func__);
+ return 0;
+ }
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_NDISC)
+ continue;
+
+ found = true;
+
+ if (address_is_ready(address)) {
+ ready = true;
+ break;
+ }
+ }
+
+ if (found && !ready) {
+ SET_FOREACH(address, link->addresses)
+ if (address->source == NETWORK_CONFIG_SOURCE_NDISC)
+ address->callback = ndisc_address_ready_callback;
+
+ log_link_debug(link, "%s(): no SLAAC address is ready.", __func__);
+ return 0;
+ }
+
+ link->ndisc_configured = true;
+ log_link_debug(link, "SLAAC addresses and routes set.");
+
+ link_check_ready(link);
+ return 0;
+}
+
+static int ndisc_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) {
+ int r;
+
+ assert(link);
+
+ r = route_configure_handler_internal(rtnl, m, link, "Could not set NDisc route");
+ if (r <= 0)
+ return r;
+
+ r = ndisc_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int ndisc_request_route(Route *in, Link *link, sd_ndisc_router *rt) {
+ _cleanup_(route_freep) Route *route = in;
+ struct in6_addr router;
+ bool is_new;
+ int r;
+
+ assert(route);
+ assert(link);
+ assert(rt);
+
+ r = sd_ndisc_router_get_address(rt, &router);
+ if (r < 0)
+ return r;
+
+ route->source = NETWORK_CONFIG_SOURCE_NDISC;
+ route->provider.in6 = router;
+ if (!route->table_set)
+ route->table = link_get_ipv6_accept_ra_route_table(link);
+ if (!route->priority_set)
+ route->priority = link->network->ipv6_accept_ra_route_metric;
+ if (!route->protocol_set)
+ route->protocol = RTPROT_RA;
+
+ is_new = route_get(NULL, link, route, NULL) < 0;
+
+ r = link_request_route(link, TAKE_PTR(route), true, &link->ndisc_messages,
+ ndisc_route_handler, NULL);
+ if (r < 0)
+ return r;
+ if (r > 0 && is_new)
+ link->ndisc_configured = false;
+
+ return 0;
+}
+
+static int ndisc_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) {
+ int r;
+
+ assert(link);
+
+ r = address_configure_handler_internal(rtnl, m, link, "Could not set NDisc address");
+ if (r <= 0)
+ return r;
+
+ r = ndisc_check_ready(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int ndisc_request_address(Address *in, Link *link, sd_ndisc_router *rt) {
+ _cleanup_(address_freep) Address *address = in;
+ struct in6_addr router;
+ bool is_new;
+ int r;
+
+ assert(address);
+ assert(link);
+ assert(rt);
+
+ r = sd_ndisc_router_get_address(rt, &router);
+ if (r < 0)
+ return r;
+
+ address->source = NETWORK_CONFIG_SOURCE_NDISC;
+ address->provider.in6 = router;
+
+ r = free_and_strdup_warn(&address->netlabel, link->network->ndisc_netlabel);
+ if (r < 0)
+ return r;
+
+ is_new = address_get(link, address, NULL) < 0;
+
+ r = link_request_address(link, TAKE_PTR(address), true, &link->ndisc_messages,
+ ndisc_address_handler, NULL);
+ if (r < 0)
+ return r;
+ if (r > 0 && is_new)
+ link->ndisc_configured = false;
+
+ return 0;
+}
+
+static int ndisc_router_process_default(Link *link, sd_ndisc_router *rt) {
+ usec_t lifetime_usec, timestamp_usec;
+ struct in6_addr gateway;
+ uint16_t lifetime_sec;
+ unsigned preference;
+ uint32_t mtu = 0;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(rt);
+
+ if (!link->network->ipv6_accept_ra_use_gateway &&
+ hashmap_isempty(link->network->routes_by_section))
+ return 0;
+
+ r = sd_ndisc_router_get_lifetime(rt, &lifetime_sec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get gateway lifetime from RA: %m");
+
+ r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %m");
+
+ lifetime_usec = sec16_to_usec(lifetime_sec, timestamp_usec);
+
+ r = sd_ndisc_router_get_address(rt, &gateway);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get gateway address from RA: %m");
+
+ if (link_get_ipv6_address(link, &gateway, 0, NULL) >= 0) {
+ if (DEBUG_LOGGING)
+ log_link_debug(link, "No NDisc route added, gateway %s matches local address",
+ IN6_ADDR_TO_STRING(&gateway));
+ return 0;
+ }
+
+ r = sd_ndisc_router_get_preference(rt, &preference);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get default router preference from RA: %m");
+
+ if (link->network->ipv6_accept_ra_use_mtu) {
+ r = sd_ndisc_router_get_mtu(rt, &mtu);
+ if (r < 0 && r != -ENODATA)
+ return log_link_error_errno(link, r, "Failed to get default router MTU from RA: %m");
+ }
+
+ if (link->network->ipv6_accept_ra_use_gateway) {
+ _cleanup_(route_freep) Route *route = NULL;
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->family = AF_INET6;
+ route->pref = preference;
+ route->gw_family = AF_INET6;
+ route->gw.in6 = gateway;
+ route->lifetime_usec = lifetime_usec;
+ route->mtu = mtu;
+
+ r = ndisc_request_route(TAKE_PTR(route), link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not request default route: %m");
+ }
+
+ Route *route_gw;
+ HASHMAP_FOREACH(route_gw, link->network->routes_by_section) {
+ _cleanup_(route_freep) Route *route = NULL;
+
+ if (!route_gw->gateway_from_dhcp_or_ra)
+ continue;
+
+ if (route_gw->gw_family != AF_INET6)
+ continue;
+
+ r = route_dup(route_gw, &route);
+ if (r < 0)
+ return r;
+
+ route->gw.in6 = gateway;
+ if (!route->pref_set)
+ route->pref = preference;
+ route->lifetime_usec = lifetime_usec;
+ if (route->mtu == 0)
+ route->mtu = mtu;
+
+ r = ndisc_request_route(TAKE_PTR(route), link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not request gateway: %m");
+ }
+
+ return 0;
+}
+
+static int ndisc_router_process_autonomous_prefix(Link *link, sd_ndisc_router *rt) {
+ uint32_t lifetime_valid_sec, lifetime_preferred_sec;
+ usec_t lifetime_valid_usec, lifetime_preferred_usec, timestamp_usec;
+ _cleanup_set_free_ Set *addresses = NULL;
+ struct in6_addr prefix, *a;
+ unsigned prefixlen;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(rt);
+
+ if (!link->network->ipv6_accept_ra_use_autonomous_prefix)
+ return 0;
+
+ r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %m");
+
+ r = sd_ndisc_router_prefix_get_address(rt, &prefix);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix address: %m");
+
+ r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix length: %m");
+
+ /* ndisc_generate_addresses() below requires the prefix length <= 64. */
+ if (prefixlen > 64) {
+ log_link_debug(link, "Prefix is longer than 64, ignoring autonomous prefix %s.",
+ IN6_ADDR_PREFIX_TO_STRING(&prefix, prefixlen));
+ return 0;
+ }
+
+ r = sd_ndisc_router_prefix_get_valid_lifetime(rt, &lifetime_valid_sec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix valid lifetime: %m");
+
+ r = sd_ndisc_router_prefix_get_preferred_lifetime(rt, &lifetime_preferred_sec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix preferred lifetime: %m");
+
+ /* The preferred lifetime is never greater than the valid lifetime */
+ if (lifetime_preferred_sec > lifetime_valid_sec)
+ return 0;
+
+ lifetime_valid_usec = sec_to_usec(lifetime_valid_sec, timestamp_usec);
+ lifetime_preferred_usec = sec_to_usec(lifetime_preferred_sec, timestamp_usec);
+
+ r = ndisc_generate_addresses(link, &prefix, prefixlen, &addresses);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to generate SLAAC addresses: %m");
+
+ SET_FOREACH(a, addresses) {
+ _cleanup_(address_freep) Address *address = NULL;
+
+ r = address_new(&address);
+ if (r < 0)
+ return log_oom();
+
+ address->family = AF_INET6;
+ address->in_addr.in6 = *a;
+ address->prefixlen = prefixlen;
+ address->flags = IFA_F_NOPREFIXROUTE|IFA_F_MANAGETEMPADDR;
+ address->lifetime_valid_usec = lifetime_valid_usec;
+ address->lifetime_preferred_usec = lifetime_preferred_usec;
+
+ r = ndisc_request_address(TAKE_PTR(address), link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not request SLAAC address: %m");
+ }
+
+ return 0;
+}
+
+static int ndisc_router_process_onlink_prefix(Link *link, sd_ndisc_router *rt) {
+ _cleanup_(route_freep) Route *route = NULL;
+ usec_t timestamp_usec;
+ uint32_t lifetime_sec;
+ struct in6_addr prefix;
+ unsigned prefixlen;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(rt);
+
+ if (!link->network->ipv6_accept_ra_use_onlink_prefix)
+ return 0;
+
+ r = sd_ndisc_router_prefix_get_valid_lifetime(rt, &lifetime_sec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix lifetime: %m");
+
+ r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %m");
+
+ r = sd_ndisc_router_prefix_get_address(rt, &prefix);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix address: %m");
+
+ r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix length: %m");
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->family = AF_INET6;
+ route->flags = RTM_F_PREFIX;
+ route->dst.in6 = prefix;
+ route->dst_prefixlen = prefixlen;
+ route->lifetime_usec = sec_to_usec(lifetime_sec, timestamp_usec);
+
+ r = ndisc_request_route(TAKE_PTR(route), link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not request prefix route: %m");
+
+ return 0;
+}
+
+static int ndisc_router_process_prefix(Link *link, sd_ndisc_router *rt) {
+ unsigned prefixlen;
+ struct in6_addr a;
+ uint8_t flags;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(rt);
+
+ r = sd_ndisc_router_prefix_get_address(rt, &a);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix address: %m");
+
+ /* RFC 4861 Section 4.6.2:
+ * A router SHOULD NOT send a prefix option for the link-local prefix and a host SHOULD ignore such
+ * a prefix option. */
+ if (in6_addr_is_link_local(&a)) {
+ log_link_debug(link, "Received link-local prefix, ignoring autonomous prefix.");
+ return 0;
+ }
+
+ r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix length: %m");
+
+ if (in6_prefix_is_filtered(&a, prefixlen, link->network->ndisc_allow_listed_prefix, link->network->ndisc_deny_listed_prefix)) {
+ if (DEBUG_LOGGING)
+ log_link_debug(link, "Prefix '%s' is %s, ignoring",
+ !set_isempty(link->network->ndisc_allow_listed_prefix) ? "not in allow list"
+ : "in deny list",
+ IN6_ADDR_PREFIX_TO_STRING(&a, prefixlen));
+ return 0;
+ }
+
+ r = sd_ndisc_router_prefix_get_flags(rt, &flags);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA prefix flags: %m");
+
+ if (FLAGS_SET(flags, ND_OPT_PI_FLAG_ONLINK)) {
+ r = ndisc_router_process_onlink_prefix(link, rt);
+ if (r < 0)
+ return r;
+ }
+
+ if (FLAGS_SET(flags, ND_OPT_PI_FLAG_AUTO)) {
+ r = ndisc_router_process_autonomous_prefix(link, rt);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int ndisc_router_process_route(Link *link, sd_ndisc_router *rt) {
+ _cleanup_(route_freep) Route *route = NULL;
+ unsigned preference, prefixlen;
+ struct in6_addr gateway, dst;
+ uint32_t lifetime_sec;
+ usec_t timestamp_usec;
+ int r;
+
+ assert(link);
+
+ if (!link->network->ipv6_accept_ra_use_route_prefix)
+ return 0;
+
+ r = sd_ndisc_router_route_get_lifetime(rt, &lifetime_sec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get route lifetime from RA: %m");
+
+ r = sd_ndisc_router_route_get_address(rt, &dst);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get route destination address: %m");
+
+ r = sd_ndisc_router_route_get_prefixlen(rt, &prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get route prefix length: %m");
+
+ if (in6_addr_is_null(&dst) && prefixlen == 0) {
+ log_link_debug(link, "Route prefix is ::/0, ignoring");
+ return 0;
+ }
+
+ if (in6_prefix_is_filtered(&dst, prefixlen,
+ link->network->ndisc_allow_listed_route_prefix,
+ link->network->ndisc_deny_listed_route_prefix)) {
+
+ if (DEBUG_LOGGING)
+ log_link_debug(link, "Route prefix %s is %s, ignoring",
+ !set_isempty(link->network->ndisc_allow_listed_route_prefix) ? "not in allow list"
+ : "in deny list",
+ IN6_ADDR_PREFIX_TO_STRING(&dst, prefixlen));
+ return 0;
+ }
+
+ r = sd_ndisc_router_get_address(rt, &gateway);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get gateway address from RA: %m");
+
+ if (link_get_ipv6_address(link, &gateway, 0, NULL) >= 0) {
+ if (DEBUG_LOGGING)
+ log_link_debug(link, "Advertised route gateway %s is local to the link, ignoring route",
+ IN6_ADDR_TO_STRING(&gateway));
+ return 0;
+ }
+
+ r = sd_ndisc_router_route_get_preference(rt, &preference);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get default router preference from RA: %m");
+
+ r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %m");
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->family = AF_INET6;
+ route->pref = preference;
+ route->gw.in6 = gateway;
+ route->gw_family = AF_INET6;
+ route->dst.in6 = dst;
+ route->dst_prefixlen = prefixlen;
+ route->lifetime_usec = sec_to_usec(lifetime_sec, timestamp_usec);
+
+ r = ndisc_request_route(TAKE_PTR(route), link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not request additional route: %m");
+
+ return 0;
+}
+
+static void ndisc_rdnss_hash_func(const NDiscRDNSS *x, struct siphash *state) {
+ siphash24_compress(&x->address, sizeof(x->address), state);
+}
+
+static int ndisc_rdnss_compare_func(const NDiscRDNSS *a, const NDiscRDNSS *b) {
+ return memcmp(&a->address, &b->address, sizeof(a->address));
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ ndisc_rdnss_hash_ops,
+ NDiscRDNSS,
+ ndisc_rdnss_hash_func,
+ ndisc_rdnss_compare_func,
+ free);
+
+static int ndisc_router_process_rdnss(Link *link, sd_ndisc_router *rt) {
+ usec_t lifetime_usec, timestamp_usec;
+ uint32_t lifetime_sec;
+ const struct in6_addr *a;
+ struct in6_addr router;
+ bool updated = false, logged_about_too_many = false;
+ int n, r;
+
+ assert(link);
+ assert(link->network);
+ assert(rt);
+
+ if (!link->network->ipv6_accept_ra_use_dns)
+ return 0;
+
+ r = sd_ndisc_router_get_address(rt, &router);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get router address from RA: %m");
+
+ r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %m");
+
+ r = sd_ndisc_router_rdnss_get_lifetime(rt, &lifetime_sec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RDNSS lifetime: %m");
+
+ lifetime_usec = sec_to_usec(lifetime_sec, timestamp_usec);
+
+ n = sd_ndisc_router_rdnss_get_addresses(rt, &a);
+ if (n < 0)
+ return log_link_error_errno(link, n, "Failed to get RDNSS addresses: %m");
+
+ for (int j = 0; j < n; j++) {
+ _cleanup_free_ NDiscRDNSS *x = NULL;
+ NDiscRDNSS *rdnss, d = {
+ .address = a[j],
+ };
+
+ if (lifetime_usec == 0) {
+ /* The entry is outdated. */
+ free(set_remove(link->ndisc_rdnss, &d));
+ updated = true;
+ continue;
+ }
+
+ rdnss = set_get(link->ndisc_rdnss, &d);
+ if (rdnss) {
+ rdnss->router = router;
+ rdnss->lifetime_usec = lifetime_usec;
+ continue;
+ }
+
+ if (set_size(link->ndisc_rdnss) >= NDISC_RDNSS_MAX) {
+ if (!logged_about_too_many)
+ log_link_warning(link, "Too many RDNSS records per link. Only first %u records will be used.", NDISC_RDNSS_MAX);
+ logged_about_too_many = true;
+ continue;
+ }
+
+ x = new(NDiscRDNSS, 1);
+ if (!x)
+ return log_oom();
+
+ *x = (NDiscRDNSS) {
+ .address = a[j],
+ .router = router,
+ .lifetime_usec = lifetime_usec,
+ };
+
+ r = set_ensure_consume(&link->ndisc_rdnss, &ndisc_rdnss_hash_ops, TAKE_PTR(x));
+ if (r < 0)
+ return log_oom();
+ assert(r > 0);
+
+ updated = true;
+ }
+
+ if (updated)
+ link_dirty(link);
+
+ return 0;
+}
+
+static void ndisc_dnssl_hash_func(const NDiscDNSSL *x, struct siphash *state) {
+ siphash24_compress_string(NDISC_DNSSL_DOMAIN(x), state);
+}
+
+static int ndisc_dnssl_compare_func(const NDiscDNSSL *a, const NDiscDNSSL *b) {
+ return strcmp(NDISC_DNSSL_DOMAIN(a), NDISC_DNSSL_DOMAIN(b));
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ ndisc_dnssl_hash_ops,
+ NDiscDNSSL,
+ ndisc_dnssl_hash_func,
+ ndisc_dnssl_compare_func,
+ free);
+
+static int ndisc_router_process_dnssl(Link *link, sd_ndisc_router *rt) {
+ _cleanup_strv_free_ char **l = NULL;
+ usec_t lifetime_usec, timestamp_usec;
+ struct in6_addr router;
+ uint32_t lifetime_sec;
+ bool updated = false, logged_about_too_many = false;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(rt);
+
+ if (link->network->ipv6_accept_ra_use_domains == DHCP_USE_DOMAINS_NO)
+ return 0;
+
+ r = sd_ndisc_router_get_address(rt, &router);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get router address from RA: %m");
+
+ r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %m");
+
+ r = sd_ndisc_router_dnssl_get_lifetime(rt, &lifetime_sec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get DNSSL lifetime: %m");
+
+ lifetime_usec = sec_to_usec(lifetime_sec, timestamp_usec);
+
+ r = sd_ndisc_router_dnssl_get_domains(rt, &l);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get DNSSL addresses: %m");
+
+ STRV_FOREACH(j, l) {
+ _cleanup_free_ NDiscDNSSL *s = NULL;
+ NDiscDNSSL *dnssl;
+
+ s = malloc0(ALIGN(sizeof(NDiscDNSSL)) + strlen(*j) + 1);
+ if (!s)
+ return log_oom();
+
+ strcpy(NDISC_DNSSL_DOMAIN(s), *j);
+
+ if (lifetime_usec == 0) {
+ /* The entry is outdated. */
+ free(set_remove(link->ndisc_dnssl, s));
+ updated = true;
+ continue;
+ }
+
+ dnssl = set_get(link->ndisc_dnssl, s);
+ if (dnssl) {
+ dnssl->router = router;
+ dnssl->lifetime_usec = lifetime_usec;
+ continue;
+ }
+
+ if (set_size(link->ndisc_dnssl) >= NDISC_DNSSL_MAX) {
+ if (!logged_about_too_many)
+ log_link_warning(link, "Too many DNSSL records per link. Only first %u records will be used.", NDISC_DNSSL_MAX);
+ logged_about_too_many = true;
+ continue;
+ }
+
+ s->router = router;
+ s->lifetime_usec = lifetime_usec;
+
+ r = set_ensure_consume(&link->ndisc_dnssl, &ndisc_dnssl_hash_ops, TAKE_PTR(s));
+ if (r < 0)
+ return log_oom();
+ assert(r > 0);
+
+ updated = true;
+ }
+
+ if (updated)
+ link_dirty(link);
+
+ return 0;
+}
+
+static int ndisc_router_process_options(Link *link, sd_ndisc_router *rt) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(rt);
+
+ for (r = sd_ndisc_router_option_rewind(rt); ; r = sd_ndisc_router_option_next(rt)) {
+ uint8_t type;
+
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to iterate through options: %m");
+ if (r == 0) /* EOF */
+ return 0;
+
+ r = sd_ndisc_router_option_get_type(rt, &type);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA option type: %m");
+
+ switch (type) {
+ case SD_NDISC_OPTION_PREFIX_INFORMATION:
+ r = ndisc_router_process_prefix(link, rt);
+ break;
+
+ case SD_NDISC_OPTION_ROUTE_INFORMATION:
+ r = ndisc_router_process_route(link, rt);
+ break;
+
+ case SD_NDISC_OPTION_RDNSS:
+ r = ndisc_router_process_rdnss(link, rt);
+ break;
+
+ case SD_NDISC_OPTION_DNSSL:
+ r = ndisc_router_process_dnssl(link, rt);
+ break;
+ }
+ if (r < 0 && r != -EBADMSG)
+ return r;
+ }
+}
+
+static int ndisc_drop_outdated(Link *link, usec_t timestamp_usec) {
+ bool updated = false;
+ NDiscDNSSL *dnssl;
+ NDiscRDNSS *rdnss;
+ Address *address;
+ Route *route;
+ int r = 0, k;
+
+ assert(link);
+
+ /* If an address or friends is already assigned, but not valid anymore, then refuse to update it,
+ * and let's immediately remove it.
+ * See RFC4862, section 5.5.3.e. But the following logic is deviated from RFC4862 by honoring all
+ * valid lifetimes to improve the reaction of SLAAC to renumbering events.
+ * See draft-ietf-6man-slaac-renum-02, section 4.2. */
+
+ SET_FOREACH(route, link->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_NDISC)
+ continue;
+
+ if (route->lifetime_usec >= timestamp_usec)
+ continue; /* the route is still valid */
+
+ k = route_remove_and_drop(route);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Failed to remove outdated SLAAC route, ignoring: %m");
+ }
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_NDISC)
+ continue;
+
+ if (address->lifetime_valid_usec >= timestamp_usec)
+ continue; /* the address is still valid */
+
+ k = address_remove_and_drop(address);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Failed to remove outdated SLAAC address, ignoring: %m");
+ }
+
+ SET_FOREACH(rdnss, link->ndisc_rdnss) {
+ if (rdnss->lifetime_usec >= timestamp_usec)
+ continue; /* the DNS server is still valid */
+
+ free(set_remove(link->ndisc_rdnss, rdnss));
+ updated = true;
+ }
+
+ SET_FOREACH(dnssl, link->ndisc_dnssl) {
+ if (dnssl->lifetime_usec >= timestamp_usec)
+ continue; /* the DNS domain is still valid */
+
+ free(set_remove(link->ndisc_dnssl, dnssl));
+ updated = true;
+ }
+
+ if (updated)
+ link_dirty(link);
+
+ return r;
+}
+
+static int ndisc_setup_expire(Link *link);
+
+static int ndisc_expire_handler(sd_event_source *s, uint64_t usec, void *userdata) {
+ Link *link = ASSERT_PTR(userdata);
+ usec_t now_usec;
+
+ assert(link->manager);
+
+ assert_se(sd_event_now(link->manager->event, CLOCK_BOOTTIME, &now_usec) >= 0);
+
+ (void) ndisc_drop_outdated(link, now_usec);
+ (void) ndisc_setup_expire(link);
+ return 0;
+}
+
+static int ndisc_setup_expire(Link *link) {
+ usec_t lifetime_usec = USEC_INFINITY;
+ NDiscDNSSL *dnssl;
+ NDiscRDNSS *rdnss;
+ Address *address;
+ Route *route;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ SET_FOREACH(route, link->routes) {
+ if (route->source != NETWORK_CONFIG_SOURCE_NDISC)
+ continue;
+
+ if (!route_exists(route))
+ continue;
+
+ lifetime_usec = MIN(lifetime_usec, route->lifetime_usec);
+ }
+
+ SET_FOREACH(address, link->addresses) {
+ if (address->source != NETWORK_CONFIG_SOURCE_NDISC)
+ continue;
+
+ if (!address_exists(address))
+ continue;
+
+ lifetime_usec = MIN(lifetime_usec, address->lifetime_valid_usec);
+ }
+
+ SET_FOREACH(rdnss, link->ndisc_rdnss)
+ lifetime_usec = MIN(lifetime_usec, rdnss->lifetime_usec);
+
+ SET_FOREACH(dnssl, link->ndisc_dnssl)
+ lifetime_usec = MIN(lifetime_usec, dnssl->lifetime_usec);
+
+ if (lifetime_usec == USEC_INFINITY)
+ return 0;
+
+ r = event_reset_time(link->manager->event, &link->ndisc_expire, CLOCK_BOOTTIME,
+ lifetime_usec, 0, ndisc_expire_handler, link, 0, "ndisc-expiration", true);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to update expiration timer for ndisc: %m");
+
+ return 0;
+}
+
+static int ndisc_start_dhcp6_client(Link *link, sd_ndisc_router *rt) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ switch (link->network->ipv6_accept_ra_start_dhcp6_client) {
+ case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO:
+ return 0;
+
+ case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES: {
+ uint64_t flags;
+
+ r = sd_ndisc_router_get_flags(rt, &flags);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to get RA flags: %m");
+
+ if ((flags & (ND_RA_FLAG_MANAGED | ND_RA_FLAG_OTHER)) == 0)
+ return 0;
+
+ /* (re)start DHCPv6 client in stateful or stateless mode according to RA flags.
+ * Note, if both "managed" and "other configuration" bits are set, then ignore
+ * "other configuration" bit. See RFC 4861. */
+ r = dhcp6_start_on_ra(link, !(flags & ND_RA_FLAG_MANAGED));
+ break;
+ }
+ case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS:
+ /* When IPv6AcceptRA.DHCPv6Client=always, start dhcp6 client in solicit mode
+ * even if the router flags have neither M nor O flags. */
+ r = dhcp6_start_on_ra(link, /* information_request = */ false);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not acquire DHCPv6 lease on NDisc request: %m");
+
+ log_link_debug(link, "Acquiring DHCPv6 lease on NDisc request");
+ return 0;
+}
+
+static int ndisc_router_handler(Link *link, sd_ndisc_router *rt) {
+ struct in6_addr router;
+ usec_t timestamp_usec;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(rt);
+
+ r = sd_ndisc_router_get_address(rt, &router);
+ if (r == -ENODATA) {
+ log_link_debug(link, "Received RA without router address, ignoring.");
+ return 0;
+ }
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get router address from RA: %m");
+
+ if (in6_prefix_is_filtered(&router, 128, link->network->ndisc_allow_listed_router, link->network->ndisc_deny_listed_router)) {
+ if (DEBUG_LOGGING) {
+ if (!set_isempty(link->network->ndisc_allow_listed_router))
+ log_link_debug(link, "Router %s is not in allow list, ignoring.", IN6_ADDR_TO_STRING(&router));
+ else
+ log_link_debug(link, "Router %s is in deny list, ignoring.", IN6_ADDR_TO_STRING(&router));
+ }
+ return 0;
+ }
+
+ r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, &timestamp_usec);
+ if (r == -ENODATA) {
+ log_link_debug(link, "Received RA without timestamp, ignoring.");
+ return 0;
+ }
+ if (r < 0)
+ return r;
+
+ r = ndisc_drop_outdated(link, timestamp_usec);
+ if (r < 0)
+ return r;
+
+ r = ndisc_start_dhcp6_client(link, rt);
+ if (r < 0)
+ return r;
+
+ r = ndisc_router_process_default(link, rt);
+ if (r < 0)
+ return r;
+
+ r = ndisc_router_process_options(link, rt);
+ if (r < 0)
+ return r;
+
+ r = ndisc_setup_expire(link);
+ if (r < 0)
+ return r;
+
+ if (link->ndisc_messages == 0)
+ link->ndisc_configured = true;
+ else
+ log_link_debug(link, "Setting SLAAC addresses and router.");
+
+ if (!link->ndisc_configured)
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ link_check_ready(link);
+ return 0;
+}
+
+static void ndisc_handler(sd_ndisc *nd, sd_ndisc_event_t event, sd_ndisc_router *rt, void *userdata) {
+ Link *link = ASSERT_PTR(userdata);
+ int r;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return;
+
+ switch (event) {
+
+ case SD_NDISC_EVENT_ROUTER:
+ r = ndisc_router_handler(link, rt);
+ if (r < 0 && r != -EBADMSG) {
+ link_enter_failed(link);
+ return;
+ }
+ break;
+
+ case SD_NDISC_EVENT_TIMEOUT:
+ log_link_debug(link, "NDisc handler get timeout event");
+ if (link->ndisc_messages == 0) {
+ link->ndisc_configured = true;
+ link_check_ready(link);
+ }
+ break;
+ default:
+ assert_not_reached();
+ }
+}
+
+static int ndisc_configure(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link_ipv6_accept_ra_enabled(link))
+ return 0;
+
+ if (link->ndisc)
+ return -EBUSY; /* Already configured. */
+
+ r = sd_ndisc_new(&link->ndisc);
+ if (r < 0)
+ return r;
+
+ r = sd_ndisc_attach_event(link->ndisc, link->manager->event, 0);
+ if (r < 0)
+ return r;
+
+ if (link->hw_addr.length == ETH_ALEN) {
+ r = sd_ndisc_set_mac(link->ndisc, &link->hw_addr.ether);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_ndisc_set_ifindex(link->ndisc, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_ndisc_set_callback(link->ndisc, ndisc_handler, link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int ndisc_start(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link->ndisc || !link->dhcp6_client)
+ return 0;
+
+ if (!link_has_carrier(link))
+ return 0;
+
+ if (in6_addr_is_null(&link->ipv6ll_address))
+ return 0;
+
+ log_link_debug(link, "Discovering IPv6 routers");
+
+ r = sd_ndisc_start(link->ndisc);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int ndisc_process_request(Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(link);
+
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false))
+ return 0;
+
+ r = ndisc_configure(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure IPv6 Router Discovery: %m");
+
+ r = ndisc_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m");
+
+ log_link_debug(link, "IPv6 Router Discovery is configured%s.",
+ r > 0 ? " and started" : "");
+ return 1;
+}
+
+int link_request_ndisc(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link_ipv6_accept_ra_enabled(link))
+ return 0;
+
+ if (link->ndisc)
+ return 0;
+
+ r = link_queue_request(link, REQUEST_TYPE_NDISC, ndisc_process_request, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request configuring of the IPv6 Router Discovery: %m");
+
+ log_link_debug(link, "Requested configuring of the IPv6 Router Discovery.");
+ return 0;
+}
+
+int ndisc_stop(Link *link) {
+ assert(link);
+
+ link->ndisc_expire = sd_event_source_disable_unref(link->ndisc_expire);
+
+ return sd_ndisc_stop(link->ndisc);
+}
+
+
+void ndisc_flush(Link *link) {
+ assert(link);
+
+ /* Removes all RDNSS and DNSSL entries, without exception */
+
+ link->ndisc_rdnss = set_free(link->ndisc_rdnss);
+ link->ndisc_dnssl = set_free(link->ndisc_dnssl);
+}
+
+static const char* const ipv6_accept_ra_start_dhcp6_client_table[_IPV6_ACCEPT_RA_START_DHCP6_CLIENT_MAX] = {
+ [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO] = "no",
+ [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS] = "always",
+ [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES] = "yes",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(ipv6_accept_ra_start_dhcp6_client, IPv6AcceptRAStartDHCP6Client, IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES);
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_ipv6_accept_ra_use_domains, dhcp_use_domains, DHCPUseDomains,
+ "Failed to parse UseDomains= setting");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_ipv6_accept_ra_start_dhcp6_client, ipv6_accept_ra_start_dhcp6_client, IPv6AcceptRAStartDHCP6Client,
+ "Failed to parse DHCPv6Client= setting");
diff --git a/src/network/networkd-ndisc.h b/src/network/networkd-ndisc.h
new file mode 100644
index 0000000..6ccc129
--- /dev/null
+++ b/src/network/networkd-ndisc.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "time-util.h"
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+typedef enum IPv6AcceptRAStartDHCP6Client {
+ IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO,
+ IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS,
+ IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES,
+ _IPV6_ACCEPT_RA_START_DHCP6_CLIENT_MAX,
+ _IPV6_ACCEPT_RA_START_DHCP6_CLIENT_INVALID = -EINVAL,
+} IPv6AcceptRAStartDHCP6Client;
+
+typedef struct NDiscRDNSS {
+ struct in6_addr router;
+ /* This is an absolute point in time, and NOT a timespan/duration.
+ * Must be specified with clock_boottime_or_monotonic(). */
+ usec_t lifetime_usec;
+ struct in6_addr address;
+} NDiscRDNSS;
+
+typedef struct NDiscDNSSL {
+ struct in6_addr router;
+ /* This is an absolute point in time, and NOT a timespan/duration.
+ * Must be specified with clock_boottime_or_monotonic(). */
+ usec_t lifetime_usec;
+ /* The domain name follows immediately. */
+} NDiscDNSSL;
+
+static inline char* NDISC_DNSSL_DOMAIN(const NDiscDNSSL *n) {
+ return ((char*) n) + ALIGN(sizeof(NDiscDNSSL));
+}
+
+bool link_ipv6_accept_ra_enabled(Link *link);
+
+void network_adjust_ipv6_accept_ra(Network *network);
+
+int ndisc_start(Link *link);
+int ndisc_stop(Link *link);
+void ndisc_flush(Link *link);
+
+int link_request_ndisc(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_accept_ra_start_dhcp6_client);
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_accept_ra_use_domains);
diff --git a/src/network/networkd-neighbor.c b/src/network/networkd-neighbor.c
new file mode 100644
index 0000000..ee40a3c
--- /dev/null
+++ b/src/network/networkd-neighbor.c
@@ -0,0 +1,683 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "hashmap.h"
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-neighbor.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "set.h"
+
+Neighbor *neighbor_free(Neighbor *neighbor) {
+ if (!neighbor)
+ return NULL;
+
+ if (neighbor->network) {
+ assert(neighbor->section);
+ hashmap_remove(neighbor->network->neighbors_by_section, neighbor->section);
+ }
+
+ config_section_free(neighbor->section);
+
+ if (neighbor->link)
+ set_remove(neighbor->link->neighbors, neighbor);
+
+ return mfree(neighbor);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(Neighbor, neighbor_free);
+
+static int neighbor_new_static(Network *network, const char *filename, unsigned section_line, Neighbor **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(neighbor_freep) Neighbor *neighbor = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ neighbor = hashmap_get(network->neighbors_by_section, n);
+ if (neighbor) {
+ *ret = TAKE_PTR(neighbor);
+ return 0;
+ }
+
+ neighbor = new(Neighbor, 1);
+ if (!neighbor)
+ return -ENOMEM;
+
+ *neighbor = (Neighbor) {
+ .network = network,
+ .family = AF_UNSPEC,
+ .section = TAKE_PTR(n),
+ .source = NETWORK_CONFIG_SOURCE_STATIC,
+ };
+
+ r = hashmap_ensure_put(&network->neighbors_by_section, &config_section_hash_ops, neighbor->section, neighbor);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(neighbor);
+ return 0;
+}
+
+static int neighbor_dup(const Neighbor *neighbor, Neighbor **ret) {
+ _cleanup_(neighbor_freep) Neighbor *dest = NULL;
+
+ assert(neighbor);
+ assert(ret);
+
+ dest = newdup(Neighbor, neighbor, 1);
+ if (!dest)
+ return -ENOMEM;
+
+ /* Unset all pointers */
+ dest->link = NULL;
+ dest->network = NULL;
+ dest->section = NULL;
+
+ *ret = TAKE_PTR(dest);
+ return 0;
+}
+
+static void neighbor_hash_func(const Neighbor *neighbor, struct siphash *state) {
+ assert(neighbor);
+
+ siphash24_compress(&neighbor->family, sizeof(neighbor->family), state);
+
+ switch (neighbor->family) {
+ case AF_INET:
+ case AF_INET6:
+ /* Equality of neighbors are given by the pair (addr,lladdr) */
+ siphash24_compress(&neighbor->in_addr, FAMILY_ADDRESS_SIZE(neighbor->family), state);
+ break;
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ break;
+ }
+
+ hw_addr_hash_func(&neighbor->ll_addr, state);
+}
+
+static int neighbor_compare_func(const Neighbor *a, const Neighbor *b) {
+ int r;
+
+ r = CMP(a->family, b->family);
+ if (r != 0)
+ return r;
+
+ switch (a->family) {
+ case AF_INET:
+ case AF_INET6:
+ r = memcmp(&a->in_addr, &b->in_addr, FAMILY_ADDRESS_SIZE(a->family));
+ if (r != 0)
+ return r;
+ }
+
+ return hw_addr_compare(&a->ll_addr, &b->ll_addr);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(neighbor_hash_ops, Neighbor, neighbor_hash_func, neighbor_compare_func, neighbor_free);
+
+static int neighbor_get(Link *link, const Neighbor *in, Neighbor **ret) {
+ Neighbor *existing;
+
+ assert(link);
+ assert(in);
+
+ existing = set_get(link->neighbors, in);
+ if (!existing)
+ return -ENOENT;
+
+ if (ret)
+ *ret = existing;
+ return 0;
+}
+
+static int neighbor_add(Link *link, Neighbor *neighbor) {
+ int r;
+
+ assert(link);
+ assert(neighbor);
+
+ r = set_ensure_put(&link->neighbors, &neighbor_hash_ops, neighbor);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ neighbor->link = link;
+ return 0;
+}
+
+static void log_neighbor_debug(const Neighbor *neighbor, const char *str, const Link *link) {
+ _cleanup_free_ char *state = NULL;
+
+ assert(neighbor);
+ assert(str);
+
+ if (!DEBUG_LOGGING)
+ return;
+
+ (void) network_config_state_to_string_alloc(neighbor->state, &state);
+
+ log_link_debug(link,
+ "%s %s neighbor (%s): lladdr: %s, dst: %s",
+ str, strna(network_config_source_to_string(neighbor->source)), strna(state),
+ HW_ADDR_TO_STR(&neighbor->ll_addr),
+ IN_ADDR_TO_STRING(neighbor->family, &neighbor->in_addr));
+}
+
+static int neighbor_configure_message(Neighbor *neighbor, Link *link, sd_netlink_message *req) {
+ int r;
+
+ r = sd_rtnl_message_neigh_set_state(req, NUD_PERMANENT);
+ if (r < 0)
+ return r;
+
+ r = netlink_message_append_hw_addr(req, NDA_LLADDR, &neighbor->ll_addr);
+ if (r < 0)
+ return r;
+
+ r = netlink_message_append_in_addr_union(req, NDA_DST, neighbor->family, &neighbor->in_addr);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int neighbor_configure(Neighbor *neighbor, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(neighbor);
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(req);
+
+ log_neighbor_debug(neighbor, "Configuring", link);
+
+ r = sd_rtnl_message_new_neigh(link->manager->rtnl, &m, RTM_NEWNEIGH,
+ link->ifindex, neighbor->family);
+ if (r < 0)
+ return r;
+
+ r = neighbor_configure_message(neighbor, link, m);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static int neighbor_process_request(Request *req, Link *link, Neighbor *neighbor) {
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(neighbor);
+
+ if (!link_is_ready_to_configure(link, false))
+ return 0;
+
+ r = neighbor_configure(neighbor, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure neighbor: %m");
+
+ neighbor_enter_configuring(neighbor);
+ return 1;
+}
+
+static int static_neighbor_configure_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Neighbor *neighbor) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set neighbor");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->static_neighbor_messages == 0) {
+ log_link_debug(link, "Neighbors set");
+ link->static_neighbors_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int link_request_neighbor(Link *link, const Neighbor *neighbor) {
+ Neighbor *existing;
+ int r;
+
+ assert(link);
+ assert(neighbor);
+ assert(neighbor->source != NETWORK_CONFIG_SOURCE_FOREIGN);
+
+ if (neighbor_get(link, neighbor, &existing) < 0) {
+ _cleanup_(neighbor_freep) Neighbor *tmp = NULL;
+
+ r = neighbor_dup(neighbor, &tmp);
+ if (r < 0)
+ return r;
+
+ r = neighbor_add(link, tmp);
+ if (r < 0)
+ return r;
+
+ existing = TAKE_PTR(tmp);
+ } else
+ existing->source = neighbor->source;
+
+ log_neighbor_debug(existing, "Requesting", link);
+ r = link_queue_request_safe(link, REQUEST_TYPE_NEIGHBOR,
+ existing, NULL,
+ neighbor_hash_func,
+ neighbor_compare_func,
+ neighbor_process_request,
+ &link->static_neighbor_messages,
+ static_neighbor_configure_handler,
+ NULL);
+ if (r <= 0)
+ return r;
+
+ neighbor_enter_requesting(existing);
+ return 1;
+}
+
+int link_request_static_neighbors(Link *link) {
+ Neighbor *neighbor;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state != _LINK_STATE_INVALID);
+
+ link->static_neighbors_configured = false;
+
+ HASHMAP_FOREACH(neighbor, link->network->neighbors_by_section) {
+ r = link_request_neighbor(link, neighbor);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not request neighbor: %m");
+ }
+
+ if (link->static_neighbor_messages == 0) {
+ link->static_neighbors_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Requesting neighbors");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+static int neighbor_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -ESRCH)
+ /* Neighbor may not exist because it already got deleted, ignore that. */
+ log_link_message_warning_errno(link, m, r, "Could not remove neighbor");
+
+ return 1;
+}
+
+static int neighbor_remove(Neighbor *neighbor) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ Link *link;
+ int r;
+
+ assert(neighbor);
+ assert(neighbor->link);
+ assert(neighbor->link->manager);
+ assert(neighbor->link->manager->rtnl);
+
+ link = neighbor->link;
+
+ log_neighbor_debug(neighbor, "Removing", link);
+
+ r = sd_rtnl_message_new_neigh(link->manager->rtnl, &req, RTM_DELNEIGH,
+ link->ifindex, neighbor->family);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_DELNEIGH message: %m");
+
+ r = netlink_message_append_in_addr_union(req, NDA_DST, neighbor->family, &neighbor->in_addr);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NDA_DST attribute: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, neighbor_remove_handler,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not send rtnetlink message: %m");
+
+ link_ref(link);
+
+ neighbor_enter_removing(neighbor);
+ return 0;
+}
+
+int link_drop_foreign_neighbors(Link *link) {
+ Neighbor *neighbor;
+ int k, r = 0;
+
+ assert(link);
+ assert(link->network);
+
+ /* First, mark all neighbors. */
+ SET_FOREACH(neighbor, link->neighbors) {
+ /* Do not remove neighbors we configured. */
+ if (neighbor->source != NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ /* Ignore neighbors not assigned yet or already removing. */
+ if (!neighbor_exists(neighbor))
+ continue;
+
+ neighbor_mark(neighbor);
+ }
+
+ /* Next, unmark requested neighbors. They will be configured later. */
+ HASHMAP_FOREACH(neighbor, link->network->neighbors_by_section) {
+ Neighbor *existing;
+
+ if (neighbor_get(link, neighbor, &existing) >= 0)
+ neighbor_unmark(existing);
+ }
+
+ SET_FOREACH(neighbor, link->neighbors) {
+ if (!neighbor_is_marked(neighbor))
+ continue;
+
+ k = neighbor_remove(neighbor);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+int link_drop_managed_neighbors(Link *link) {
+ Neighbor *neighbor;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(neighbor, link->neighbors) {
+ /* Do not touch nexthops managed by kernel or other tools. */
+ if (neighbor->source == NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ /* Ignore neighbors not assigned yet or already removing. */
+ if (!neighbor_exists(neighbor))
+ continue;
+
+ k = neighbor_remove(neighbor);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+void link_foreignize_neighbors(Link *link) {
+ Neighbor *neighbor;
+
+ assert(link);
+
+ SET_FOREACH(neighbor, link->neighbors)
+ neighbor->source = NETWORK_CONFIG_SOURCE_FOREIGN;
+}
+
+int manager_rtnl_process_neighbor(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(neighbor_freep) Neighbor *tmp = NULL;
+ Neighbor *neighbor = NULL;
+ uint16_t type, state;
+ int ifindex, r;
+ Link *link;
+
+ assert(rtnl);
+ assert(message);
+ assert(m);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "rtnl: failed to receive neighbor message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get message type, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(type, RTM_NEWNEIGH, RTM_DELNEIGH)) {
+ log_warning("rtnl: received unexpected message type %u when processing neighbor, ignoring.", type);
+ return 0;
+ }
+
+ r = sd_rtnl_message_neigh_get_state(message, &state);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: received neighbor message with invalid state, ignoring: %m");
+ return 0;
+ } else if (!FLAGS_SET(state, NUD_PERMANENT)) {
+ log_debug("rtnl: received non-static neighbor, ignoring.");
+ return 0;
+ }
+
+ r = sd_rtnl_message_neigh_get_ifindex(message, &ifindex);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m");
+ return 0;
+ } else if (ifindex <= 0) {
+ log_warning("rtnl: received neighbor message with invalid ifindex %d, ignoring.", ifindex);
+ return 0;
+ }
+
+ r = link_get_by_index(m, ifindex, &link);
+ if (r < 0 || !link) {
+ /* when enumerating we might be out of sync, but we will get the neighbor again. Also,
+ * kernel sends messages about neighbors after a link is removed. So, just ignore it. */
+ log_debug("rtnl: received neighbor for link '%d' we don't know about, ignoring.", ifindex);
+ return 0;
+ }
+
+ tmp = new0(Neighbor, 1);
+ if (!tmp)
+ return log_oom();
+
+ r = sd_rtnl_message_neigh_get_family(message, &tmp->family);
+ if (r < 0) {
+ log_link_warning(link, "rtnl: received neighbor message without family, ignoring.");
+ return 0;
+ } else if (!IN_SET(tmp->family, AF_INET, AF_INET6)) {
+ log_link_debug(link, "rtnl: received neighbor message with invalid family '%i', ignoring.", tmp->family);
+ return 0;
+ }
+
+ r = netlink_message_read_in_addr_union(message, NDA_DST, tmp->family, &tmp->in_addr);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received neighbor message without valid address, ignoring: %m");
+ return 0;
+ }
+
+ r = netlink_message_read_hw_addr(message, NDA_LLADDR, &tmp->ll_addr);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received neighbor message without valid link layer address, ignoring: %m");
+ return 0;
+ }
+
+ (void) neighbor_get(link, tmp, &neighbor);
+
+ switch (type) {
+ case RTM_NEWNEIGH:
+ if (neighbor) {
+ neighbor_enter_configured(neighbor);
+ log_neighbor_debug(neighbor, "Received remembered", link);
+ } else {
+ neighbor_enter_configured(tmp);
+ log_neighbor_debug(tmp, "Remembering", link);
+ r = neighbor_add(link, tmp);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to remember foreign neighbor, ignoring: %m");
+ return 0;
+ }
+ TAKE_PTR(tmp);
+ }
+
+ break;
+
+ case RTM_DELNEIGH:
+ if (neighbor) {
+ neighbor_enter_removed(neighbor);
+ if (neighbor->state == 0) {
+ log_neighbor_debug(neighbor, "Forgetting", link);
+ neighbor_free(neighbor);
+ } else
+ log_neighbor_debug(neighbor, "Removed", link);
+ } else
+ log_neighbor_debug(tmp, "Kernel removed unknown", link);
+
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int neighbor_section_verify(Neighbor *neighbor) {
+ if (section_is_invalid(neighbor->section))
+ return -EINVAL;
+
+ if (neighbor->family == AF_UNSPEC)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Neighbor section without Address= configured. "
+ "Ignoring [Neighbor] section from line %u.",
+ neighbor->section->filename, neighbor->section->line);
+
+ if (neighbor->ll_addr.length == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Neighbor section without LinkLayerAddress= configured. "
+ "Ignoring [Neighbor] section from line %u.",
+ neighbor->section->filename, neighbor->section->line);
+
+ return 0;
+}
+
+void network_drop_invalid_neighbors(Network *network) {
+ Neighbor *neighbor;
+
+ assert(network);
+
+ HASHMAP_FOREACH(neighbor, network->neighbors_by_section)
+ if (neighbor_section_verify(neighbor) < 0)
+ neighbor_free(neighbor);
+}
+
+
+int config_parse_neighbor_address(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(neighbor_free_or_set_invalidp) Neighbor *n = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = neighbor_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ n->family = AF_UNSPEC;
+ n->in_addr = IN_ADDR_NULL;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = in_addr_from_string_auto(rvalue, &n->family, &n->in_addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Neighbor Address is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_neighbor_lladdr(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(neighbor_free_or_set_invalidp) Neighbor *n = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = neighbor_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ n->ll_addr = HW_ADDR_NULL;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = parse_hw_addr(rvalue, &n->ll_addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Neighbor %s= is invalid, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
diff --git a/src/network/networkd-neighbor.h b/src/network/networkd-neighbor.h
new file mode 100644
index 0000000..758475a
--- /dev/null
+++ b/src/network/networkd-neighbor.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+#include "ether-addr-util.h"
+#include "in-addr-util.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+typedef struct Network Network;
+
+typedef struct Neighbor {
+ Network *network;
+ Link *link;
+ ConfigSection *section;
+ NetworkConfigSource source;
+ NetworkConfigState state;
+
+ int family;
+ union in_addr_union in_addr;
+ struct hw_addr_data ll_addr;
+} Neighbor;
+
+Neighbor *neighbor_free(Neighbor *neighbor);
+
+void network_drop_invalid_neighbors(Network *network);
+
+int link_drop_managed_neighbors(Link *link);
+int link_drop_foreign_neighbors(Link *link);
+void link_foreignize_neighbors(Link *link);
+
+int link_request_static_neighbors(Link *link);
+
+int manager_rtnl_process_neighbor(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(Neighbor, neighbor);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_neighbor_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_neighbor_lladdr);
diff --git a/src/network/networkd-netlabel.c b/src/network/networkd-netlabel.c
new file mode 100644
index 0000000..94bf8f5
--- /dev/null
+++ b/src/network/networkd-netlabel.c
@@ -0,0 +1,128 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "escape.h"
+#include "netlink-util.h"
+#include "networkd-address.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-netlabel.h"
+#include "networkd-network.h"
+
+static int netlabel_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert_se(rtnl);
+ assert_se(m);
+ assert_se(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_link_message_warning_errno(link, m, r, "NetLabel operation failed, ignoring");
+ return 1;
+ }
+
+ log_link_debug(link, "NetLabel operation successful");
+
+ return 1;
+}
+
+static int netlabel_command(uint16_t command, const char *label, const Address *address) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(command != NLBL_UNLABEL_C_UNSPEC && command < __NLBL_UNLABEL_C_MAX);
+ assert(address);
+ assert(address->link);
+ assert(address->link->ifname);
+ assert(address->link->manager);
+ assert(address->link->manager->genl);
+ assert(IN_SET(address->family, AF_INET, AF_INET6));
+
+ r = sd_genl_message_new(address->link->manager->genl, NETLBL_NLTYPE_UNLABELED_NAME, command, &m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_string(m, NLBL_UNLABEL_A_IFACE, address->link->ifname);
+ if (r < 0)
+ return r;
+
+ if (command == NLBL_UNLABEL_C_STATICADD) {
+ assert(label);
+ r = sd_netlink_message_append_string(m, NLBL_UNLABEL_A_SECCTX, label);
+ if (r < 0)
+ return r;
+ }
+
+ union in_addr_union netmask, masked_addr;
+ r = in_addr_prefixlen_to_netmask(address->family, &netmask, address->prefixlen);
+ if (r < 0)
+ return r;
+
+ /*
+ * When adding rules, kernel adds the address to its hash table _applying also the netmask_, but on
+ * removal, an exact match is required _without netmask applied_, so apply the mask on both
+ * operations.
+ */
+ masked_addr = address->in_addr;
+ r = in_addr_mask(address->family, &masked_addr, address->prefixlen);
+ if (r < 0)
+ return r;
+
+ if (address->family == AF_INET) {
+ r = sd_netlink_message_append_in_addr(m, NLBL_UNLABEL_A_IPV4ADDR, &masked_addr.in);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in_addr(m, NLBL_UNLABEL_A_IPV4MASK, &netmask.in);
+ } else if (address->family == AF_INET6) {
+ r = sd_netlink_message_append_in6_addr(m, NLBL_UNLABEL_A_IPV6ADDR, &masked_addr.in6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_in6_addr(m, NLBL_UNLABEL_A_IPV6MASK, &netmask.in6);
+ }
+ if (r < 0)
+ return r;
+
+ r = netlink_call_async(address->link->manager->genl, NULL, m, netlabel_handler, link_netlink_destroy_callback,
+ address->link);
+ if (r < 0)
+ return r;
+
+ link_ref(address->link);
+ return 0;
+}
+
+void address_add_netlabel(const Address *address) {
+ int r;
+
+ assert(address);
+
+ if (!address->netlabel)
+ return;
+
+ r = netlabel_command(NLBL_UNLABEL_C_STATICADD, address->netlabel, address);
+ if (r < 0)
+ log_link_warning_errno(address->link, r, "Adding NetLabel %s for IP address %s failed, ignoring", address->netlabel,
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen));
+ else
+ log_link_debug(address->link, "Adding NetLabel %s for IP address %s", address->netlabel,
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen));
+}
+
+void address_del_netlabel(const Address *address) {
+ int r;
+
+ assert(address);
+
+ if (!address->netlabel)
+ return;
+
+ r = netlabel_command(NLBL_UNLABEL_C_STATICREMOVE, address->netlabel, address);
+ if (r < 0)
+ log_link_warning_errno(address->link, r, "Deleting NetLabel %s for IP address %s failed, ignoring", address->netlabel,
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen));
+ else
+ log_link_debug(address->link, "Deleting NetLabel %s for IP address %s", address->netlabel,
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen));
+}
diff --git a/src/network/networkd-netlabel.h b/src/network/networkd-netlabel.h
new file mode 100644
index 0000000..2f30b8f
--- /dev/null
+++ b/src/network/networkd-netlabel.h
@@ -0,0 +1,5 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+void address_add_netlabel(const Address *address);
+void address_del_netlabel(const Address *address);
diff --git a/src/network/networkd-network-bus.c b/src/network/networkd-network-bus.c
new file mode 100644
index 0000000..e1e89e8
--- /dev/null
+++ b/src/network/networkd-network-bus.c
@@ -0,0 +1,141 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "ether-addr-util.h"
+#include "networkd-manager.h"
+#include "networkd-network-bus.h"
+#include "string-util.h"
+#include "strv.h"
+
+static int property_get_hw_addrs(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ const struct hw_addr_data *p;
+ Set *s;
+ int r;
+
+ assert(bus);
+ assert(reply);
+ assert(userdata);
+
+ s = *(Set **) userdata;
+
+ r = sd_bus_message_open_container(reply, 'a', "s");
+ if (r < 0)
+ return r;
+
+ SET_FOREACH(p, s) {
+ r = sd_bus_message_append(reply, "s", HW_ADDR_TO_STR(p));
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_message_close_container(reply);
+}
+
+static const sd_bus_vtable network_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+
+ SD_BUS_PROPERTY("Description", "s", NULL, offsetof(Network, description), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("SourcePath", "s", NULL, offsetof(Network, filename), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("MatchMAC", "as", property_get_hw_addrs, offsetof(Network, match.hw_addr), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("MatchPath", "as", NULL, offsetof(Network, match.path), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("MatchDriver", "as", NULL, offsetof(Network, match.driver), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("MatchType", "as", NULL, offsetof(Network, match.iftype), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("MatchName", "as", NULL, offsetof(Network, match.ifname), SD_BUS_VTABLE_PROPERTY_CONST),
+
+ SD_BUS_VTABLE_END
+};
+
+static char *network_bus_path(Network *network) {
+ _cleanup_free_ char *name = NULL;
+ char *networkname, *d, *path;
+ int r;
+
+ assert(network);
+ assert(network->filename);
+
+ name = strdup(network->filename);
+ if (!name)
+ return NULL;
+
+ networkname = basename(name);
+
+ d = strrchr(networkname, '.');
+ if (!d)
+ return NULL;
+
+ assert(streq(d, ".network"));
+
+ *d = '\0';
+
+ r = sd_bus_path_encode("/org/freedesktop/network1/network", networkname, &path);
+ if (r < 0)
+ return NULL;
+
+ return path;
+}
+
+int network_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) {
+ _cleanup_strv_free_ char **l = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ Network *network;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(nodes);
+
+ ORDERED_HASHMAP_FOREACH(network, m->networks) {
+ char *p;
+
+ p = network_bus_path(network);
+ if (!p)
+ return -ENOMEM;
+
+ r = strv_consume(&l, p);
+ if (r < 0)
+ return r;
+ }
+
+ *nodes = TAKE_PTR(l);
+
+ return 1;
+}
+
+int network_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) {
+ Manager *m = ASSERT_PTR(userdata);
+ Network *network;
+ _cleanup_free_ char *name = NULL;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(found);
+
+ r = sd_bus_path_decode(path, "/org/freedesktop/network1/network", &name);
+ if (r < 0)
+ return 0;
+
+ r = network_get_by_name(m, name, &network);
+ if (r < 0)
+ return 0;
+
+ *found = network;
+
+ return 1;
+}
+
+const BusObjectImplementation network_object = {
+ "/org/freedesktop/network1/network",
+ "org.freedesktop.network1.Network",
+ .fallback_vtables = BUS_FALLBACK_VTABLES({network_vtable, network_object_find}),
+ .node_enumerator = network_node_enumerator,
+};
diff --git a/src/network/networkd-network-bus.h b/src/network/networkd-network-bus.h
new file mode 100644
index 0000000..68ed951
--- /dev/null
+++ b/src/network/networkd-network-bus.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "bus-object.h"
+
+typedef struct Link Link;
+
+extern const BusObjectImplementation network_object;
+
+int network_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error);
+int network_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error);
diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf
new file mode 100644
index 0000000..aec5452
--- /dev/null
+++ b/src/network/networkd-network-gperf.gperf
@@ -0,0 +1,591 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "conf-parser.h"
+#include "in-addr-prefix-util.h"
+#include "netem.h"
+#include "net-condition.h"
+#include "networkd-address-generation.h"
+#include "networkd-address-label.h"
+#include "networkd-address.h"
+#include "networkd-bridge-fdb.h"
+#include "networkd-bridge-mdb.h"
+#include "networkd-can.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-dhcp-prefix-delegation.h"
+#include "networkd-dhcp-server-static-lease.h"
+#include "networkd-dhcp-server.h"
+#include "networkd-dhcp4.h"
+#include "networkd-dhcp6.h"
+#include "networkd-ipv4ll.h"
+#include "networkd-ipv6-proxy-ndp.h"
+#include "networkd-ipv6ll.h"
+#include "networkd-lldp-tx.h"
+#include "networkd-ndisc.h"
+#include "networkd-netlabel.h"
+#include "networkd-network.h"
+#include "networkd-neighbor.h"
+#include "networkd-nexthop.h"
+#include "networkd-radv.h"
+#include "networkd-route.h"
+#include "networkd-routing-policy-rule.h"
+#include "networkd-sriov.h"
+#include "qdisc.h"
+#include "tclass.h"
+#include "vlan-util.h"
+%}
+struct ConfigPerfItem;
+%null_strings
+%language=ANSI-C
+%define slot-name section_and_lvalue
+%define hash-function-name network_network_gperf_hash
+%define lookup-function-name network_network_gperf_lookup
+%readonly-tables
+%omit-struct-type
+%struct-type
+%includes
+%%
+Match.MACAddress, config_parse_hw_addrs, 0, offsetof(Network, match.hw_addr)
+Match.PermanentMACAddress, config_parse_hw_addrs, 0, offsetof(Network, match.permanent_hw_addr)
+Match.Path, config_parse_match_strv, 0, offsetof(Network, match.path)
+Match.Driver, config_parse_match_strv, 0, offsetof(Network, match.driver)
+Match.Type, config_parse_match_strv, 0, offsetof(Network, match.iftype)
+Match.Kind, config_parse_match_strv, 0, offsetof(Network, match.kind)
+Match.WLANInterfaceType, config_parse_match_strv, 0, offsetof(Network, match.wlan_iftype)
+Match.SSID, config_parse_match_strv, 0, offsetof(Network, match.ssid)
+Match.BSSID, config_parse_ether_addrs, 0, offsetof(Network, match.bssid)
+Match.Name, config_parse_match_ifnames, IFNAME_VALID_ALTERNATIVE, offsetof(Network, match.ifname)
+Match.Property, config_parse_match_property, 0, offsetof(Network, match.property)
+Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(Network, conditions)
+Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(Network, conditions)
+Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(Network, conditions)
+Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(Network, conditions)
+Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(Network, conditions)
+Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(Network, conditions)
+Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(Network, conditions)
+Link.MACAddress, config_parse_hw_addr, 0, offsetof(Network, hw_addr)
+Link.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(Network, mtu)
+Link.Group, config_parse_link_group, 0, 0
+Link.ARP, config_parse_tristate, 0, offsetof(Network, arp)
+Link.Multicast, config_parse_tristate, 0, offsetof(Network, multicast)
+Link.AllMulticast, config_parse_tristate, 0, offsetof(Network, allmulticast)
+Link.Promiscuous, config_parse_tristate, 0, offsetof(Network, promiscuous)
+Link.Unmanaged, config_parse_bool, 0, offsetof(Network, unmanaged)
+Link.ActivationPolicy, config_parse_activation_policy, 0, offsetof(Network, activation_policy)
+Link.RequiredForOnline, config_parse_required_for_online, 0, 0
+Link.RequiredFamilyForOnline, config_parse_required_family_for_online, 0, offsetof(Network, required_family_for_online)
+SR-IOV.VirtualFunction, config_parse_sr_iov_uint32, 0, offsetof(Network, sr_iov_by_section)
+SR-IOV.VLANId, config_parse_sr_iov_uint32, 0, offsetof(Network, sr_iov_by_section)
+SR-IOV.QualityOfService, config_parse_sr_iov_uint32, 0, offsetof(Network, sr_iov_by_section)
+SR-IOV.VLANProtocol, config_parse_sr_iov_vlan_proto, 0, offsetof(Network, sr_iov_by_section)
+SR-IOV.MACSpoofCheck, config_parse_sr_iov_boolean, 0, offsetof(Network, sr_iov_by_section)
+SR-IOV.QueryReceiveSideScaling, config_parse_sr_iov_boolean, 0, offsetof(Network, sr_iov_by_section)
+SR-IOV.Trust, config_parse_sr_iov_boolean, 0, offsetof(Network, sr_iov_by_section)
+SR-IOV.LinkState, config_parse_sr_iov_link_state, 0, offsetof(Network, sr_iov_by_section)
+SR-IOV.MACAddress, config_parse_sr_iov_mac, 0, offsetof(Network, sr_iov_by_section)
+Network.Description, config_parse_string, 0, offsetof(Network, description)
+Network.KeepMaster, config_parse_bool, 0, offsetof(Network, keep_master)
+Network.BatmanAdvanced, config_parse_ifname, 0, offsetof(Network, batadv_name)
+Network.Bond, config_parse_ifname, 0, offsetof(Network, bond_name)
+Network.Bridge, config_parse_ifname, 0, offsetof(Network, bridge_name)
+Network.VRF, config_parse_ifname, 0, offsetof(Network, vrf_name)
+Network.IPoIB, config_parse_stacked_netdev, NETDEV_KIND_IPOIB, offsetof(Network, stacked_netdev_names)
+Network.IPVLAN, config_parse_stacked_netdev, NETDEV_KIND_IPVLAN, offsetof(Network, stacked_netdev_names)
+Network.IPVTAP, config_parse_stacked_netdev, NETDEV_KIND_IPVTAP, offsetof(Network, stacked_netdev_names)
+Network.L2TP, config_parse_warn_compat, DISABLED_LEGACY, 0
+Network.MACsec, config_parse_stacked_netdev, NETDEV_KIND_MACSEC, offsetof(Network, stacked_netdev_names)
+Network.MACVLAN, config_parse_stacked_netdev, NETDEV_KIND_MACVLAN, offsetof(Network, stacked_netdev_names)
+Network.MACVTAP, config_parse_stacked_netdev, NETDEV_KIND_MACVTAP, offsetof(Network, stacked_netdev_names)
+Network.Tunnel, config_parse_stacked_netdev, _NETDEV_KIND_TUNNEL, offsetof(Network, stacked_netdev_names)
+Network.VLAN, config_parse_stacked_netdev, NETDEV_KIND_VLAN, offsetof(Network, stacked_netdev_names)
+Network.VXLAN, config_parse_stacked_netdev, NETDEV_KIND_VXLAN, offsetof(Network, stacked_netdev_names)
+Network.Xfrm, config_parse_stacked_netdev, NETDEV_KIND_XFRM, offsetof(Network, stacked_netdev_names)
+Network.DHCP, config_parse_dhcp, 0, offsetof(Network, dhcp)
+Network.DHCPServer, config_parse_bool, 0, offsetof(Network, dhcp_server)
+Network.LinkLocalAddressing, config_parse_link_local_address_family, 0, offsetof(Network, link_local)
+Network.IPv6LinkLocalAddressGenerationMode, config_parse_ipv6_link_local_address_gen_mode, 0, offsetof(Network, ipv6ll_address_gen_mode)
+Network.IPv6StableSecretAddress, config_parse_in_addr_non_null, AF_INET6, offsetof(Network, ipv6ll_stable_secret)
+Network.IPv4LLStartAddress, config_parse_ipv4ll_address, 0, offsetof(Network, ipv4ll_start_address)
+Network.IPv4LLRoute, config_parse_bool, 0, offsetof(Network, ipv4ll_route)
+Network.DefaultRouteOnDevice, config_parse_bool, 0, offsetof(Network, default_route_on_device)
+Network.LLDP, config_parse_lldp_mode, 0, offsetof(Network, lldp_mode)
+Network.EmitLLDP, config_parse_lldp_multicast_mode, 0, offsetof(Network, lldp_multicast_mode)
+Network.Address, config_parse_address, 0, 0
+Network.Gateway, config_parse_gateway, 0, 0
+Network.Domains, config_parse_domains, 0, 0
+Network.DNS, config_parse_dns, 0, 0
+Network.DNSDefaultRoute, config_parse_tristate, 0, offsetof(Network, dns_default_route)
+Network.LLMNR, config_parse_resolve_support, 0, offsetof(Network, llmnr)
+Network.MulticastDNS, config_parse_resolve_support, 0, offsetof(Network, mdns)
+Network.DNSOverTLS, config_parse_dns_over_tls_mode, 0, offsetof(Network, dns_over_tls_mode)
+Network.DNSSEC, config_parse_dnssec_mode, 0, offsetof(Network, dnssec_mode)
+Network.DNSSECNegativeTrustAnchors, config_parse_dnssec_negative_trust_anchors, 0, offsetof(Network, dnssec_negative_trust_anchors)
+Network.NTP, config_parse_ntp, 0, offsetof(Network, ntp)
+Network.IPForward, config_parse_address_family_with_kernel, 0, offsetof(Network, ip_forward)
+Network.IPMasquerade, config_parse_ip_masquerade, 0, offsetof(Network, ip_masquerade)
+Network.IPv6PrivacyExtensions, config_parse_ipv6_privacy_extensions, 0, offsetof(Network, ipv6_privacy_extensions)
+Network.IPv6AcceptRA, config_parse_tristate, 0, offsetof(Network, ipv6_accept_ra)
+Network.IPv6AcceptRouterAdvertisements, config_parse_tristate, 0, offsetof(Network, ipv6_accept_ra)
+Network.IPv6DuplicateAddressDetection, config_parse_int, 0, offsetof(Network, ipv6_dad_transmits)
+Network.IPv6HopLimit, config_parse_int, 0, offsetof(Network, ipv6_hop_limit)
+Network.IPv6ProxyNDP, config_parse_tristate, 0, offsetof(Network, ipv6_proxy_ndp)
+Network.IPv6MTUBytes, config_parse_mtu, AF_INET6, offsetof(Network, ipv6_mtu)
+Network.IPv4AcceptLocal, config_parse_tristate, 0, offsetof(Network, ipv4_accept_local)
+Network.IPv4RouteLocalnet, config_parse_tristate, 0, offsetof(Network, ipv4_route_localnet)
+Network.ActiveSlave, config_parse_bool, 0, offsetof(Network, active_slave)
+Network.PrimarySlave, config_parse_bool, 0, offsetof(Network, primary_slave)
+Network.IPv4ProxyARP, config_parse_tristate, 0, offsetof(Network, proxy_arp)
+Network.ProxyARP, config_parse_tristate, 0, offsetof(Network, proxy_arp)
+Network.IPv6ProxyNDPAddress, config_parse_ipv6_proxy_ndp_address, 0, 0
+Network.BindCarrier, config_parse_strv, 0, offsetof(Network, bind_carrier)
+Network.ConfigureWithoutCarrier, config_parse_bool, 0, offsetof(Network, configure_without_carrier)
+Network.IgnoreCarrierLoss, config_parse_ignore_carrier_loss, 0, 0
+Network.KeepConfiguration, config_parse_keep_configuration, 0, offsetof(Network, keep_configuration)
+Network.IPv6SendRA, config_parse_router_prefix_delegation, 0, offsetof(Network, router_prefix_delegation)
+Network.DHCPPrefixDelegation, config_parse_tristate, 0, offsetof(Network, dhcp_pd)
+Address.Address, config_parse_address, 0, 0
+Address.Peer, config_parse_address, 0, 0
+Address.Broadcast, config_parse_broadcast, 0, 0
+Address.Label, config_parse_label, 0, 0
+Address.PreferredLifetime, config_parse_lifetime, 0, 0
+Address.HomeAddress, config_parse_address_flags, IFA_F_HOMEADDRESS, 0
+Address.ManageTemporaryAddress, config_parse_address_flags, IFA_F_MANAGETEMPADDR, 0
+Address.PrefixRoute, config_parse_address_flags, IFA_F_NOPREFIXROUTE, 0 /* deprecated */
+Address.AddPrefixRoute, config_parse_address_flags, IFA_F_NOPREFIXROUTE, 0
+Address.AutoJoin, config_parse_address_flags, IFA_F_MCAUTOJOIN, 0
+Address.DuplicateAddressDetection, config_parse_duplicate_address_detection, 0, 0
+Address.Scope, config_parse_address_scope, 0, 0
+Address.RouteMetric, config_parse_address_route_metric, 0, 0
+Address.NetLabel, config_parse_address_netlabel, 0, 0
+IPv6AddressLabel.Prefix, config_parse_address_label_prefix, 0, 0
+IPv6AddressLabel.Label, config_parse_address_label, 0, 0
+Neighbor.Address, config_parse_neighbor_address, 0, 0
+Neighbor.LinkLayerAddress, config_parse_neighbor_lladdr, 0, 0
+Neighbor.MACAddress, config_parse_neighbor_lladdr, 0, 0 /* deprecated */
+RoutingPolicyRule.TypeOfService, config_parse_routing_policy_rule_tos, 0, 0
+RoutingPolicyRule.Priority, config_parse_routing_policy_rule_priority, 0, 0
+RoutingPolicyRule.Table, config_parse_routing_policy_rule_table, 0, 0
+RoutingPolicyRule.FirewallMark, config_parse_routing_policy_rule_fwmark_mask, 0, 0
+RoutingPolicyRule.From, config_parse_routing_policy_rule_prefix, 0, 0
+RoutingPolicyRule.To, config_parse_routing_policy_rule_prefix, 0, 0
+RoutingPolicyRule.IncomingInterface, config_parse_routing_policy_rule_device, 0, 0
+RoutingPolicyRule.OutgoingInterface, config_parse_routing_policy_rule_device, 0, 0
+RoutingPolicyRule.IPProtocol, config_parse_routing_policy_rule_ip_protocol, 0, 0
+RoutingPolicyRule.SourcePort, config_parse_routing_policy_rule_port_range, 0, 0
+RoutingPolicyRule.DestinationPort, config_parse_routing_policy_rule_port_range, 0, 0
+RoutingPolicyRule.InvertRule, config_parse_routing_policy_rule_invert, 0, 0
+RoutingPolicyRule.Family, config_parse_routing_policy_rule_family, 0, 0
+RoutingPolicyRule.User, config_parse_routing_policy_rule_uid_range, 0, 0
+RoutingPolicyRule.SuppressInterfaceGroup, config_parse_routing_policy_rule_suppress_ifgroup, 0, 0
+RoutingPolicyRule.SuppressPrefixLength, config_parse_routing_policy_rule_suppress_prefixlen, 0, 0
+RoutingPolicyRule.Type, config_parse_routing_policy_rule_type, 0, 0
+Route.Gateway, config_parse_gateway, 0, 0
+Route.Destination, config_parse_destination, 0, 0
+Route.Source, config_parse_destination, 0, 0
+Route.Metric, config_parse_route_priority, 0, 0
+Route.Scope, config_parse_route_scope, 0, 0
+Route.PreferredSource, config_parse_preferred_src, 0, 0
+Route.Table, config_parse_route_table, 0, 0
+Route.MTUBytes, config_parse_route_mtu, AF_UNSPEC, 0
+Route.GatewayOnLink, config_parse_route_boolean, 0, 0
+Route.GatewayOnlink, config_parse_route_boolean, 0, 0
+Route.IPv6Preference, config_parse_ipv6_route_preference, 0, 0
+Route.Protocol, config_parse_route_protocol, 0, 0
+Route.Type, config_parse_route_type, 0, 0
+Route.InitialCongestionWindow, config_parse_tcp_window, 0, 0
+Route.InitialAdvertisedReceiveWindow, config_parse_tcp_window, 0, 0
+Route.TCPAdvertisedMaximumSegmentSize, config_parse_tcp_advmss, 0, 0
+Route.TCPCongestionControlAlgorithm, config_parse_tcp_congestion, 0, 0
+Route.QuickAck, config_parse_route_boolean, 0, 0
+Route.FastOpenNoCookie, config_parse_route_boolean, 0, 0
+Route.TTLPropagate, config_parse_route_boolean, 0, 0
+Route.MultiPathRoute, config_parse_multipath_route, 0, 0
+Route.NextHop, config_parse_route_nexthop, 0, 0
+NextHop.Id, config_parse_nexthop_id, 0, 0
+NextHop.Gateway, config_parse_nexthop_gateway, 0, 0
+NextHop.Family, config_parse_nexthop_family, 0, 0
+NextHop.OnLink, config_parse_nexthop_onlink, 0, 0
+NextHop.Blackhole, config_parse_nexthop_blackhole, 0, 0
+NextHop.Group, config_parse_nexthop_group, 0, 0
+DHCPv4.ClientIdentifier, config_parse_dhcp_client_identifier, 0, offsetof(Network, dhcp_client_identifier)
+DHCPv4.UseDNS, config_parse_dhcp_use_dns, AF_INET, 0
+DHCPv4.RoutesToDNS, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_dns)
+DHCPv4.UseNTP, config_parse_dhcp_use_ntp, AF_INET, 0
+DHCPv4.RoutesToNTP, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_ntp)
+DHCPv4.UseSIP, config_parse_bool, 0, offsetof(Network, dhcp_use_sip)
+DHCPv4.UseMTU, config_parse_bool, 0, offsetof(Network, dhcp_use_mtu)
+DHCPv4.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp_use_hostname)
+DHCPv4.UseDomains, config_parse_dhcp_use_domains, AF_INET, 0
+DHCPv4.UseRoutes, config_parse_bool, 0, offsetof(Network, dhcp_use_routes)
+DHCPv4.UseGateway, config_parse_tristate, 0, offsetof(Network, dhcp_use_gateway)
+DHCPv4.RequestOptions, config_parse_dhcp_request_options, AF_INET, 0
+DHCPv4.Anonymize, config_parse_bool, 0, offsetof(Network, dhcp_anonymize)
+DHCPv4.SendHostname, config_parse_bool, 0, offsetof(Network, dhcp_send_hostname)
+DHCPv4.Hostname, config_parse_hostname, 0, offsetof(Network, dhcp_hostname)
+DHCPv4.Label, config_parse_dhcp_label, 0, offsetof(Network, dhcp_label)
+DHCPv4.RequestBroadcast, config_parse_tristate, 0, offsetof(Network, dhcp_broadcast)
+DHCPv4.VendorClassIdentifier, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_vendor_class_identifier)
+DHCPv4.MUDURL, config_parse_mud_url, 0, offsetof(Network, dhcp_mudurl)
+DHCPv4.MaxAttempts, config_parse_dhcp_max_attempts, 0, 0
+DHCPv4.UserClass, config_parse_dhcp_user_or_vendor_class, AF_INET, offsetof(Network, dhcp_user_class)
+DHCPv4.IAID, config_parse_iaid, AF_INET, 0
+DHCPv4.DUIDType, config_parse_network_duid_type, 0, 0
+DHCPv4.DUIDRawData, config_parse_network_duid_rawdata, 0, 0
+DHCPv4.RouteMetric, config_parse_dhcp_or_ra_route_metric, AF_INET, 0
+DHCPv4.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET, 0
+DHCPv4.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone)
+DHCPv4.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port)
+DHCPv4.SendRelease, config_parse_bool, 0, offsetof(Network, dhcp_send_release)
+DHCPv4.SendDecline, config_parse_bool, 0, offsetof(Network, dhcp_send_decline)
+DHCPv4.DenyList, config_parse_in_addr_prefixes, AF_INET, offsetof(Network, dhcp_deny_listed_ip)
+DHCPv4.AllowList, config_parse_in_addr_prefixes, AF_INET, offsetof(Network, dhcp_allow_listed_ip)
+DHCPv4.IPServiceType, config_parse_dhcp_ip_service_type, 0, offsetof(Network, dhcp_ip_service_type)
+DHCPv4.SendOption, config_parse_dhcp_send_option, AF_INET, offsetof(Network, dhcp_client_send_options)
+DHCPv4.SendVendorOption, config_parse_dhcp_send_option, 0, offsetof(Network, dhcp_client_send_vendor_options)
+DHCPv4.RouteMTUBytes, config_parse_mtu, AF_INET, offsetof(Network, dhcp_route_mtu)
+DHCPv4.FallbackLeaseLifetimeSec, config_parse_dhcp_fallback_lease_lifetime, 0, 0
+DHCPv4.Use6RD, config_parse_bool, 0, offsetof(Network, dhcp_use_6rd)
+DHCPv4.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_netlabel)
+DHCPv6.UseAddress, config_parse_bool, 0, offsetof(Network, dhcp6_use_address)
+DHCPv6.UseDelegatedPrefix, config_parse_bool, 0, offsetof(Network, dhcp6_use_pd_prefix)
+DHCPv6.UseDNS, config_parse_dhcp_use_dns, AF_INET6, 0
+DHCPv6.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp6_use_hostname)
+DHCPv6.UseDomains, config_parse_dhcp_use_domains, AF_INET6, 0
+DHCPv6.UseNTP, config_parse_dhcp_use_ntp, AF_INET6, 0
+DHCPv6.MUDURL, config_parse_mud_url, 0, offsetof(Network, dhcp6_mudurl)
+DHCPv6.RequestOptions, config_parse_dhcp_request_options, AF_INET6, 0
+DHCPv6.UserClass, config_parse_dhcp_user_or_vendor_class, AF_INET6, offsetof(Network, dhcp6_user_class)
+DHCPv6.VendorClass, config_parse_dhcp_user_or_vendor_class, AF_INET6, offsetof(Network, dhcp6_vendor_class)
+DHCPv6.SendVendorOption, config_parse_dhcp_send_option, AF_INET6, offsetof(Network, dhcp6_client_send_vendor_options)
+DHCPv6.PrefixDelegationHint, config_parse_dhcp6_pd_prefix_hint, 0, 0
+DHCPv6.WithoutRA, config_parse_dhcp6_client_start_mode, 0, offsetof(Network, dhcp6_client_start_mode)
+DHCPv6.SendOption, config_parse_dhcp_send_option, AF_INET6, offsetof(Network, dhcp6_client_send_options)
+DHCPv6.IAID, config_parse_iaid, AF_INET6, 0
+DHCPv6.DUIDType, config_parse_duid_type, 0, offsetof(Network, dhcp6_duid)
+DHCPv6.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Network, dhcp6_duid)
+DHCPv6.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp6_use_rapid_commit)
+DHCPv6.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp6_netlabel)
+IPv6AcceptRA.UseGateway, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_gateway)
+IPv6AcceptRA.UseRoutePrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_route_prefix)
+IPv6AcceptRA.UseAutonomousPrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_autonomous_prefix)
+IPv6AcceptRA.UseOnLinkPrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_onlink_prefix)
+IPv6AcceptRA.UseDNS, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_dns)
+IPv6AcceptRA.UseDomains, config_parse_ipv6_accept_ra_use_domains, 0, offsetof(Network, ipv6_accept_ra_use_domains)
+IPv6AcceptRA.UseMTU, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_mtu)
+IPv6AcceptRA.DHCPv6Client, config_parse_ipv6_accept_ra_start_dhcp6_client, 0, offsetof(Network, ipv6_accept_ra_start_dhcp6_client)
+IPv6AcceptRA.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET6, 0
+IPv6AcceptRA.RouteMetric, config_parse_dhcp_or_ra_route_metric, AF_INET6, 0
+IPv6AcceptRA.RouterAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_router)
+IPv6AcceptRA.RouterDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_router)
+IPv6AcceptRA.PrefixAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_prefix)
+IPv6AcceptRA.PrefixDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_prefix)
+IPv6AcceptRA.RouteAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_route_prefix)
+IPv6AcceptRA.RouteDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_route_prefix)
+IPv6AcceptRA.Token, config_parse_address_generation_type, 0, offsetof(Network, ndisc_tokens)
+IPv6AcceptRA.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, ndisc_netlabel)
+DHCPServer.ServerAddress, config_parse_dhcp_server_address, 0, 0
+DHCPServer.UplinkInterface, config_parse_uplink, 0, 0
+DHCPServer.RelayTarget, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_relay_target)
+DHCPServer.RelayAgentCircuitId, config_parse_dhcp_server_relay_agent_suboption, 0, offsetof(Network, dhcp_server_relay_agent_circuit_id)
+DHCPServer.RelayAgentRemoteId, config_parse_dhcp_server_relay_agent_suboption, 0, offsetof(Network, dhcp_server_relay_agent_remote_id)
+DHCPServer.MaxLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_max_lease_time_usec)
+DHCPServer.DefaultLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_default_lease_time_usec)
+DHCPServer.EmitDNS, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_DNS].emit)
+DHCPServer.DNS, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_DNS])
+DHCPServer.EmitNTP, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_NTP].emit)
+DHCPServer.NTP, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_NTP])
+DHCPServer.EmitSIP, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_SIP].emit)
+DHCPServer.SIP, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_SIP])
+DHCPServer.EmitPOP3, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_POP3].emit)
+DHCPServer.POP3, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_POP3])
+DHCPServer.EmitSMTP, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_SMTP].emit)
+DHCPServer.SMTP, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_SMTP])
+DHCPServer.EmitLPR, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_LPR].emit)
+DHCPServer.LPR, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_LPR])
+DHCPServer.EmitRouter, config_parse_bool, 0, offsetof(Network, dhcp_server_emit_router)
+DHCPServer.Router, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_router)
+DHCPServer.EmitTimezone, config_parse_bool, 0, offsetof(Network, dhcp_server_emit_timezone)
+DHCPServer.Timezone, config_parse_timezone, 0, offsetof(Network, dhcp_server_timezone)
+DHCPServer.PoolOffset, config_parse_uint32, 0, offsetof(Network, dhcp_server_pool_offset)
+DHCPServer.PoolSize, config_parse_uint32, 0, offsetof(Network, dhcp_server_pool_size)
+DHCPServer.SendVendorOption, config_parse_dhcp_send_option, 0, offsetof(Network, dhcp_server_send_vendor_options)
+DHCPServer.SendOption, config_parse_dhcp_send_option, 0, offsetof(Network, dhcp_server_send_options)
+DHCPServer.BindToInterface, config_parse_bool, 0, offsetof(Network, dhcp_server_bind_to_interface)
+DHCPServer.BootServerAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_boot_server_address)
+DHCPServer.BootServerName, config_parse_dns_name, 0, offsetof(Network, dhcp_server_boot_server_name)
+DHCPServer.BootFilename, config_parse_string, CONFIG_PARSE_STRING_SAFE_AND_ASCII, offsetof(Network, dhcp_server_boot_filename)
+DHCPServerStaticLease.Address, config_parse_dhcp_static_lease_address, 0, 0
+DHCPServerStaticLease.MACAddress, config_parse_dhcp_static_lease_hwaddr, 0, 0
+Bridge.Cost, config_parse_uint32, 0, offsetof(Network, cost)
+Bridge.UseBPDU, config_parse_tristate, 0, offsetof(Network, use_bpdu)
+Bridge.HairPin, config_parse_tristate, 0, offsetof(Network, hairpin)
+Bridge.Isolated, config_parse_tristate, 0, offsetof(Network, isolated)
+Bridge.FastLeave, config_parse_tristate, 0, offsetof(Network, fast_leave)
+Bridge.AllowPortToBeRoot, config_parse_tristate, 0, offsetof(Network, allow_port_to_be_root)
+Bridge.UnicastFlood, config_parse_tristate, 0, offsetof(Network, unicast_flood)
+Bridge.MulticastFlood, config_parse_tristate, 0, offsetof(Network, multicast_flood)
+Bridge.MulticastToUnicast, config_parse_tristate, 0, offsetof(Network, multicast_to_unicast)
+Bridge.NeighborSuppression, config_parse_tristate, 0, offsetof(Network, neighbor_suppression)
+Bridge.Learning, config_parse_tristate, 0, offsetof(Network, learning)
+Bridge.ProxyARP, config_parse_tristate, 0, offsetof(Network, bridge_proxy_arp)
+Bridge.ProxyARPWiFi, config_parse_tristate, 0, offsetof(Network, bridge_proxy_arp_wifi)
+Bridge.Priority, config_parse_bridge_port_priority, 0, offsetof(Network, priority)
+Bridge.MulticastRouter, config_parse_multicast_router, 0, offsetof(Network, multicast_router)
+BridgeFDB.MACAddress, config_parse_fdb_hwaddr, 0, 0
+BridgeFDB.VLANId, config_parse_fdb_vlan_id, 0, 0
+BridgeFDB.Destination, config_parse_fdb_destination, 0, 0
+BridgeFDB.VNI, config_parse_fdb_vxlan_vni, 0, 0
+BridgeFDB.AssociatedWith, config_parse_fdb_ntf_flags, 0, 0
+BridgeFDB.OutgoingInterface, config_parse_fdb_interface, 0, 0
+BridgeMDB.MulticastGroupAddress, config_parse_mdb_group_address, 0, 0
+BridgeMDB.VLANId, config_parse_mdb_vlan_id, 0, 0
+BridgeVLAN.PVID, config_parse_brvlan_pvid, 0, 0
+BridgeVLAN.VLAN, config_parse_brvlan_vlan, 0, 0
+BridgeVLAN.EgressUntagged, config_parse_brvlan_untagged, 0, 0
+DHCPPrefixDelegation.UplinkInterface, config_parse_uplink, 0, 0
+DHCPPrefixDelegation.SubnetId, config_parse_dhcp_pd_subnet_id, 0, offsetof(Network, dhcp_pd_subnet_id)
+DHCPPrefixDelegation.Announce, config_parse_bool, 0, offsetof(Network, dhcp_pd_announce)
+DHCPPrefixDelegation.Assign, config_parse_bool, 0, offsetof(Network, dhcp_pd_assign)
+DHCPPrefixDelegation.ManageTemporaryAddress, config_parse_bool, 0, offsetof(Network, dhcp_pd_manage_temporary_address)
+DHCPPrefixDelegation.Token, config_parse_address_generation_type, 0, offsetof(Network, dhcp_pd_tokens)
+DHCPPrefixDelegation.RouteMetric, config_parse_uint32, 0, offsetof(Network, dhcp_pd_route_metric)
+DHCPPrefixDelegation.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_pd_netlabel)
+IPv6SendRA.RouterLifetimeSec, config_parse_router_lifetime, 0, offsetof(Network, router_lifetime_usec)
+IPv6SendRA.Managed, config_parse_bool, 0, offsetof(Network, router_managed)
+IPv6SendRA.OtherInformation, config_parse_bool, 0, offsetof(Network, router_other_information)
+IPv6SendRA.RouterPreference, config_parse_router_preference, 0, 0
+IPv6SendRA.EmitDNS, config_parse_bool, 0, offsetof(Network, router_emit_dns)
+IPv6SendRA.DNS, config_parse_radv_dns, 0, 0
+IPv6SendRA.EmitDomains, config_parse_bool, 0, offsetof(Network, router_emit_domains)
+IPv6SendRA.Domains, config_parse_radv_search_domains, 0, 0
+IPv6SendRA.DNSLifetimeSec, config_parse_sec, 0, offsetof(Network, router_dns_lifetime_usec)
+IPv6SendRA.UplinkInterface, config_parse_uplink, 0, 0
+IPv6Prefix.Prefix, config_parse_prefix, 0, 0
+IPv6Prefix.OnLink, config_parse_prefix_boolean, 0, 0
+IPv6Prefix.AddressAutoconfiguration, config_parse_prefix_boolean, 0, 0
+IPv6Prefix.ValidLifetimeSec, config_parse_prefix_lifetime, 0, 0
+IPv6Prefix.PreferredLifetimeSec, config_parse_prefix_lifetime, 0, 0
+IPv6Prefix.Assign, config_parse_prefix_boolean, 0, 0
+IPv6Prefix.RouteMetric, config_parse_prefix_metric, 0, 0
+IPv6Prefix.Token, config_parse_prefix_token, 0, 0
+IPv6RoutePrefix.Route, config_parse_route_prefix, 0, 0
+IPv6RoutePrefix.LifetimeSec, config_parse_route_prefix_lifetime, 0, 0
+LLDP.MUDURL, config_parse_mud_url, 0, offsetof(Network, lldp_mudurl)
+CAN.BitRate, config_parse_can_bitrate, 0, offsetof(Network, can_bitrate)
+CAN.SamplePoint, config_parse_permille, 0, offsetof(Network, can_sample_point)
+CAN.TimeQuantaNSec, config_parse_can_time_quanta, 0, offsetof(Network, can_time_quanta_ns)
+CAN.PropagationSegment, config_parse_uint32, 0, offsetof(Network, can_propagation_segment)
+CAN.PhaseBufferSegment1, config_parse_uint32, 0, offsetof(Network, can_phase_buffer_segment_1)
+CAN.PhaseBufferSegment2, config_parse_uint32, 0, offsetof(Network, can_phase_buffer_segment_2)
+CAN.SyncJumpWidth, config_parse_uint32, 0, offsetof(Network, can_sync_jump_width)
+CAN.DataBitRate, config_parse_can_bitrate, 0, offsetof(Network, can_data_bitrate)
+CAN.DataSamplePoint, config_parse_permille, 0, offsetof(Network, can_data_sample_point)
+CAN.DataTimeQuantaNSec, config_parse_can_time_quanta, 0, offsetof(Network, can_data_time_quanta_ns)
+CAN.DataPropagationSegment, config_parse_uint32, 0, offsetof(Network, can_data_propagation_segment)
+CAN.DataPhaseBufferSegment1, config_parse_uint32, 0, offsetof(Network, can_data_phase_buffer_segment_1)
+CAN.DataPhaseBufferSegment2, config_parse_uint32, 0, offsetof(Network, can_data_phase_buffer_segment_2)
+CAN.DataSyncJumpWidth, config_parse_uint32, 0, offsetof(Network, can_data_sync_jump_width)
+CAN.RestartSec, config_parse_can_restart_usec, 0, offsetof(Network, can_restart_us)
+CAN.Loopback, config_parse_can_control_mode, CAN_CTRLMODE_LOOPBACK, 0
+CAN.ListenOnly, config_parse_can_control_mode, CAN_CTRLMODE_LISTENONLY, 0
+CAN.TripleSampling, config_parse_can_control_mode, CAN_CTRLMODE_3_SAMPLES, 0
+CAN.OneShot, config_parse_can_control_mode, CAN_CTRLMODE_ONE_SHOT, 0
+CAN.BusErrorReporting, config_parse_can_control_mode, CAN_CTRLMODE_BERR_REPORTING, 0
+CAN.FDMode, config_parse_can_control_mode, CAN_CTRLMODE_FD, 0
+CAN.PresumeACK, config_parse_can_control_mode, CAN_CTRLMODE_PRESUME_ACK, 0
+CAN.FDNonISO, config_parse_can_control_mode, CAN_CTRLMODE_FD_NON_ISO, 0
+CAN.ClassicDataLengthCode, config_parse_can_control_mode, CAN_CTRLMODE_CC_LEN8_DLC, 0
+CAN.Termination, config_parse_can_termination, 0, 0
+IPoIB.Mode, config_parse_ipoib_mode, 0, offsetof(Network, ipoib_mode)
+IPoIB.IgnoreUserspaceMulticastGroups, config_parse_tristate, 0, offsetof(Network, ipoib_umcast)
+QDisc.Parent, config_parse_qdisc_parent, _QDISC_KIND_INVALID, 0
+QDisc.Handle, config_parse_qdisc_handle, _QDISC_KIND_INVALID, 0
+BFIFO.Parent, config_parse_qdisc_parent, QDISC_KIND_BFIFO, 0
+BFIFO.Handle, config_parse_qdisc_handle, QDISC_KIND_BFIFO, 0
+BFIFO.LimitBytes, config_parse_bfifo_size, QDISC_KIND_BFIFO, 0
+CAKE.Parent, config_parse_qdisc_parent, QDISC_KIND_CAKE, 0
+CAKE.Handle, config_parse_qdisc_handle, QDISC_KIND_CAKE, 0
+CAKE.Bandwidth, config_parse_cake_bandwidth, QDISC_KIND_CAKE, 0
+CAKE.AutoRateIngress, config_parse_cake_tristate, QDISC_KIND_CAKE, 0
+CAKE.OverheadBytes, config_parse_cake_overhead, QDISC_KIND_CAKE, 0
+CAKE.MPUBytes, config_parse_cake_mpu, QDISC_KIND_CAKE, 0
+CAKE.CompensationMode, config_parse_cake_compensation_mode, QDISC_KIND_CAKE, 0
+CAKE.UseRawPacketSize, config_parse_cake_tristate, QDISC_KIND_CAKE, 0
+CAKE.FlowIsolationMode, config_parse_cake_flow_isolation_mode, QDISC_KIND_CAKE, 0
+CAKE.NAT, config_parse_cake_tristate, QDISC_KIND_CAKE, 0
+CAKE.PriorityQueueingPreset, config_parse_cake_priority_queueing_preset, QDISC_KIND_CAKE, 0
+CAKE.FirewallMark, config_parse_cake_fwmark, QDISC_KIND_CAKE, 0
+CAKE.Wash, config_parse_cake_tristate, QDISC_KIND_CAKE, 0
+CAKE.SplitGSO, config_parse_cake_tristate, QDISC_KIND_CAKE, 0
+ControlledDelay.Parent, config_parse_qdisc_parent, QDISC_KIND_CODEL, 0
+ControlledDelay.Handle, config_parse_qdisc_handle, QDISC_KIND_CODEL, 0
+ControlledDelay.PacketLimit, config_parse_controlled_delay_u32, QDISC_KIND_CODEL, 0
+ControlledDelay.TargetSec, config_parse_controlled_delay_usec, QDISC_KIND_CODEL, 0
+ControlledDelay.IntervalSec, config_parse_controlled_delay_usec, QDISC_KIND_CODEL, 0
+ControlledDelay.CEThresholdSec, config_parse_controlled_delay_usec, QDISC_KIND_CODEL, 0
+ControlledDelay.ECN, config_parse_controlled_delay_bool, QDISC_KIND_CODEL, 0
+DeficitRoundRobinScheduler.Parent, config_parse_qdisc_parent, QDISC_KIND_DRR, 0
+DeficitRoundRobinScheduler.Handle, config_parse_qdisc_handle, QDISC_KIND_DRR, 0
+DeficitRoundRobinSchedulerClass.Parent, config_parse_tclass_parent, TCLASS_KIND_DRR, 0
+DeficitRoundRobinSchedulerClass.ClassId, config_parse_tclass_classid, TCLASS_KIND_DRR, 0
+DeficitRoundRobinSchedulerClass.QuantumBytes, config_parse_drr_size, TCLASS_KIND_DRR, 0
+EnhancedTransmissionSelection.Parent, config_parse_qdisc_parent, QDISC_KIND_ETS, 0
+EnhancedTransmissionSelection.Handle, config_parse_qdisc_handle, QDISC_KIND_ETS, 0
+EnhancedTransmissionSelection.Bands, config_parse_ets_u8, QDISC_KIND_ETS, 0
+EnhancedTransmissionSelection.StrictBands, config_parse_ets_u8, QDISC_KIND_ETS, 0
+EnhancedTransmissionSelection.QuantumBytes, config_parse_ets_quanta, QDISC_KIND_ETS, 0
+EnhancedTransmissionSelection.PriorityMap, config_parse_ets_prio, QDISC_KIND_ETS, 0
+PFIFO.Parent, config_parse_qdisc_parent, QDISC_KIND_PFIFO, 0
+PFIFO.Handle, config_parse_qdisc_handle, QDISC_KIND_PFIFO, 0
+PFIFO.PacketLimit, config_parse_pfifo_size, QDISC_KIND_PFIFO, 0
+PFIFOFast.Parent, config_parse_qdisc_parent, QDISC_KIND_PFIFO_FAST, 0
+PFIFOFast.Handle, config_parse_qdisc_handle, QDISC_KIND_PFIFO_FAST, 0
+PFIFOHeadDrop.Parent, config_parse_qdisc_parent, QDISC_KIND_PFIFO_HEAD_DROP, 0
+PFIFOHeadDrop.Handle, config_parse_qdisc_handle, QDISC_KIND_PFIFO_HEAD_DROP, 0
+PFIFOHeadDrop.PacketLimit, config_parse_pfifo_size, QDISC_KIND_PFIFO_HEAD_DROP, 0
+QuickFairQueueing.Parent, config_parse_qdisc_parent, QDISC_KIND_QFQ, 0
+QuickFairQueueing.Handle, config_parse_qdisc_handle, QDISC_KIND_QFQ, 0
+QuickFairQueueingClass.Parent, config_parse_tclass_parent, TCLASS_KIND_QFQ, 0
+QuickFairQueueingClass.ClassId, config_parse_tclass_classid, TCLASS_KIND_QFQ, 0
+QuickFairQueueingClass.Weight, config_parse_quick_fair_queueing_weight, TCLASS_KIND_QFQ, 0
+QuickFairQueueingClass.MaxPacketBytes, config_parse_quick_fair_queueing_max_packet, TCLASS_KIND_QFQ, 0
+FairQueueing.Parent, config_parse_qdisc_parent, QDISC_KIND_FQ, 0
+FairQueueing.Handle, config_parse_qdisc_handle, QDISC_KIND_FQ, 0
+FairQueueing.PacketLimit, config_parse_fair_queueing_u32, QDISC_KIND_FQ, 0
+FairQueueing.FlowLimit, config_parse_fair_queueing_u32, QDISC_KIND_FQ, 0
+FairQueueing.QuantumBytes, config_parse_fair_queueing_size, QDISC_KIND_FQ, 0
+FairQueueing.InitialQuantumBytes, config_parse_fair_queueing_size, QDISC_KIND_FQ, 0
+FairQueueing.MaximumRate, config_parse_fair_queueing_max_rate, QDISC_KIND_FQ, 0
+FairQueueing.Buckets, config_parse_fair_queueing_u32, QDISC_KIND_FQ, 0
+FairQueueing.OrphanMask, config_parse_fair_queueing_u32, QDISC_KIND_FQ, 0
+FairQueueing.Pacing, config_parse_fair_queueing_bool, QDISC_KIND_FQ, 0
+FairQueueing.CEThresholdSec, config_parse_fair_queueing_usec, QDISC_KIND_FQ, 0
+FairQueueingControlledDelay.Parent, config_parse_qdisc_parent, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.Handle, config_parse_qdisc_handle, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.PacketLimit, config_parse_fair_queueing_controlled_delay_u32, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.MemoryLimitBytes, config_parse_fair_queueing_controlled_delay_size, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.Flows, config_parse_fair_queueing_controlled_delay_u32, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.QuantumBytes, config_parse_fair_queueing_controlled_delay_size, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.TargetSec, config_parse_fair_queueing_controlled_delay_usec, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.IntervalSec, config_parse_fair_queueing_controlled_delay_usec, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.CEThresholdSec, config_parse_fair_queueing_controlled_delay_usec, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.ECN, config_parse_fair_queueing_controlled_delay_bool, QDISC_KIND_FQ_CODEL, 0
+FlowQueuePIE.Parent, config_parse_qdisc_parent, QDISC_KIND_FQ_PIE, 0
+FlowQueuePIE.Handle, config_parse_qdisc_handle, QDISC_KIND_FQ_PIE, 0
+FlowQueuePIE.PacketLimit, config_parse_fq_pie_packet_limit, QDISC_KIND_FQ_PIE, 0
+GenericRandomEarlyDetection.Parent, config_parse_qdisc_parent, QDISC_KIND_GRED, 0
+GenericRandomEarlyDetection.Handle, config_parse_qdisc_handle, QDISC_KIND_GRED, 0
+GenericRandomEarlyDetection.VirtualQueues, config_parse_generic_random_early_detection_u32, QDISC_KIND_GRED, 0
+GenericRandomEarlyDetection.DefaultVirtualQueue, config_parse_generic_random_early_detection_u32, QDISC_KIND_GRED, 0
+GenericRandomEarlyDetection.GenericRIO, config_parse_generic_random_early_detection_bool, QDISC_KIND_GRED, 0
+HeavyHitterFilter.Parent, config_parse_qdisc_parent, QDISC_KIND_HHF, 0
+HeavyHitterFilter.Handle, config_parse_qdisc_handle, QDISC_KIND_HHF, 0
+HeavyHitterFilter.PacketLimit, config_parse_heavy_hitter_filter_packet_limit, QDISC_KIND_HHF, 0
+HierarchyTokenBucket.Parent, config_parse_qdisc_parent, QDISC_KIND_HTB, 0
+HierarchyTokenBucket.Handle, config_parse_qdisc_handle, QDISC_KIND_HTB, 0
+HierarchyTokenBucket.DefaultClass, config_parse_hierarchy_token_bucket_default_class, QDISC_KIND_HTB, 0
+HierarchyTokenBucket.RateToQuantum, config_parse_hierarchy_token_bucket_u32, QDISC_KIND_HTB, 0
+HierarchyTokenBucketClass.Parent, config_parse_tclass_parent, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.ClassId, config_parse_tclass_classid, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.Priority, config_parse_hierarchy_token_bucket_class_u32, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.QuantumBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.MTUBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.OverheadBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.Rate, config_parse_hierarchy_token_bucket_class_rate, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.CeilRate, config_parse_hierarchy_token_bucket_class_rate, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.BufferBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0
+HierarchyTokenBucketClass.CeilBufferBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0
+NetworkEmulator.Parent, config_parse_qdisc_parent, QDISC_KIND_NETEM, 0
+NetworkEmulator.Handle, config_parse_qdisc_handle, QDISC_KIND_NETEM, 0
+NetworkEmulator.DelaySec, config_parse_network_emulator_delay, QDISC_KIND_NETEM, 0
+NetworkEmulator.DelayJitterSec, config_parse_network_emulator_delay, QDISC_KIND_NETEM, 0
+NetworkEmulator.LossRate, config_parse_network_emulator_rate, QDISC_KIND_NETEM, 0
+NetworkEmulator.DuplicateRate, config_parse_network_emulator_rate, QDISC_KIND_NETEM, 0
+NetworkEmulator.PacketLimit, config_parse_network_emulator_packet_limit, QDISC_KIND_NETEM, 0
+PIE.Parent, config_parse_qdisc_parent, QDISC_KIND_PIE, 0
+PIE.Handle, config_parse_qdisc_handle, QDISC_KIND_PIE, 0
+PIE.PacketLimit, config_parse_pie_packet_limit, QDISC_KIND_PIE, 0
+StochasticFairBlue.Parent, config_parse_qdisc_parent, QDISC_KIND_SFB, 0
+StochasticFairBlue.Handle, config_parse_qdisc_handle, QDISC_KIND_SFB, 0
+StochasticFairBlue.PacketLimit, config_parse_stochastic_fair_blue_u32, QDISC_KIND_SFB, 0
+StochasticFairnessQueueing.Parent, config_parse_qdisc_parent, QDISC_KIND_SFQ, 0
+StochasticFairnessQueueing.Handle, config_parse_qdisc_handle, QDISC_KIND_SFQ, 0
+StochasticFairnessQueueing.PerturbPeriodSec, config_parse_stochastic_fairness_queueing_perturb_period, QDISC_KIND_SFQ, 0
+TokenBucketFilter.Parent, config_parse_qdisc_parent, QDISC_KIND_TBF, 0
+TokenBucketFilter.Handle, config_parse_qdisc_handle, QDISC_KIND_TBF, 0
+TokenBucketFilter.Rate, config_parse_token_bucket_filter_rate, QDISC_KIND_TBF, 0
+TokenBucketFilter.BurstBytes, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0
+TokenBucketFilter.LimitBytes, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0
+TokenBucketFilter.MTUBytes, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0
+TokenBucketFilter.MPUBytes, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0
+TokenBucketFilter.PeakRate, config_parse_token_bucket_filter_rate, QDISC_KIND_TBF, 0
+TokenBucketFilter.LatencySec, config_parse_token_bucket_filter_latency, QDISC_KIND_TBF, 0
+TrivialLinkEqualizer.Parent, config_parse_qdisc_parent, QDISC_KIND_TEQL, 0
+TrivialLinkEqualizer.Handle, config_parse_qdisc_handle, QDISC_KIND_TEQL, 0
+TrivialLinkEqualizer.Id, config_parse_trivial_link_equalizer_id, QDISC_KIND_TEQL, 0
+/* backwards compatibility: do not add new entries to this section */
+Network.IPv4LL, config_parse_ipv4ll, 0, offsetof(Network, link_local)
+Network.IPv6Token, config_parse_address_generation_type, 0, offsetof(Network, ndisc_tokens)
+Network.IPv6PrefixDelegation, config_parse_router_prefix_delegation, 0, offsetof(Network, router_prefix_delegation)
+Network.DHCPv6PrefixDelegation, config_parse_tristate, 0, offsetof(Network, dhcp_pd)
+IPv6PrefixDelegation.RouterLifetimeSec, config_parse_sec, 0, offsetof(Network, router_lifetime_usec)
+IPv6PrefixDelegation.Managed, config_parse_bool, 0, offsetof(Network, router_managed)
+IPv6PrefixDelegation.OtherInformation, config_parse_bool, 0, offsetof(Network, router_other_information)
+IPv6PrefixDelegation.RouterPreference, config_parse_router_preference, 0, 0
+IPv6PrefixDelegation.EmitDNS, config_parse_bool, 0, offsetof(Network, router_emit_dns)
+IPv6PrefixDelegation.DNS, config_parse_radv_dns, 0, 0
+IPv6PrefixDelegation.EmitDomains, config_parse_bool, 0, offsetof(Network, router_emit_domains)
+IPv6PrefixDelegation.Domains, config_parse_radv_search_domains, 0, 0
+IPv6PrefixDelegation.DNSLifetimeSec, config_parse_sec, 0, offsetof(Network, router_dns_lifetime_usec)
+DHCPv4.BlackList, config_parse_in_addr_prefixes, AF_INET, offsetof(Network, dhcp_deny_listed_ip)
+DHCP.ClientIdentifier, config_parse_dhcp_client_identifier, 0, offsetof(Network, dhcp_client_identifier)
+DHCP.UseDNS, config_parse_dhcp_use_dns, AF_UNSPEC, 0
+DHCP.UseNTP, config_parse_dhcp_use_ntp, AF_UNSPEC, 0
+DHCP.UseMTU, config_parse_bool, 0, offsetof(Network, dhcp_use_mtu)
+DHCP.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp_use_hostname)
+DHCP.UseDomains, config_parse_dhcp_use_domains, AF_UNSPEC, 0
+DHCP.UseDomainName, config_parse_dhcp_use_domains, AF_UNSPEC, 0
+DHCP.UseRoutes, config_parse_bool, 0, offsetof(Network, dhcp_use_routes)
+DHCP.Anonymize, config_parse_bool, 0, offsetof(Network, dhcp_anonymize)
+DHCP.SendHostname, config_parse_bool, 0, offsetof(Network, dhcp_send_hostname)
+DHCP.Hostname, config_parse_hostname, 0, offsetof(Network, dhcp_hostname)
+DHCP.RequestBroadcast, config_parse_tristate, 0, offsetof(Network, dhcp_broadcast)
+DHCP.CriticalConnection, config_parse_tristate, 0, offsetof(Network, dhcp_critical)
+DHCP.VendorClassIdentifier, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_vendor_class_identifier)
+DHCP.UserClass, config_parse_dhcp_user_or_vendor_class, AF_INET, offsetof(Network, dhcp_user_class)
+DHCP.IAID, config_parse_iaid, AF_INET, 0
+DHCP.DUIDType, config_parse_network_duid_type, 0, 0
+DHCP.DUIDRawData, config_parse_network_duid_rawdata, 0, 0
+DHCP.RouteMetric, config_parse_dhcp_or_ra_route_metric, AF_UNSPEC, 0
+DHCP.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET, 0
+DHCP.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone)
+DHCP.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port)
+DHCP.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp6_use_rapid_commit)
+DHCP.ForceDHCPv6PDOtherInformation, config_parse_warn_compat, DISABLED_LEGACY, 0
+DHCPv4.UseDomainName, config_parse_dhcp_use_domains, AF_INET, 0
+DHCPv4.CriticalConnection, config_parse_tristate, 0, offsetof(Network, dhcp_critical)
+DHCPv6.RouteMetric, config_parse_dhcp_or_ra_route_metric, AF_INET6, 0
+DHCPv6.ForceDHCPv6PDOtherInformation, config_parse_warn_compat, DISABLED_LEGACY, 0
+DHCPv6PrefixDelegation.SubnetId, config_parse_dhcp_pd_subnet_id, 0, offsetof(Network, dhcp_pd_subnet_id)
+DHCPv6PrefixDelegation.Announce, config_parse_bool, 0, offsetof(Network, dhcp_pd_announce)
+DHCPv6PrefixDelegation.Assign, config_parse_bool, 0, offsetof(Network, dhcp_pd_assign)
+DHCPv6PrefixDelegation.ManageTemporaryAddress, config_parse_bool, 0, offsetof(Network, dhcp_pd_manage_temporary_address)
+DHCPv6PrefixDelegation.Token, config_parse_address_generation_type, 0, offsetof(Network, dhcp_pd_tokens)
+DHCPv6PrefixDelegation.RouteMetric, config_parse_uint32, 0, offsetof(Network, dhcp_pd_route_metric)
+IPv6AcceptRA.DenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_prefix)
+IPv6AcceptRA.BlackList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_prefix)
+TrafficControlQueueingDiscipline.Parent, config_parse_qdisc_parent, _QDISC_KIND_INVALID, 0
+TrafficControlQueueingDiscipline.NetworkEmulatorDelaySec, config_parse_network_emulator_delay, 0, 0
+TrafficControlQueueingDiscipline.NetworkEmulatorDelayJitterSec, config_parse_network_emulator_delay, 0, 0
+TrafficControlQueueingDiscipline.NetworkEmulatorLossRate, config_parse_network_emulator_rate, 0, 0
+TrafficControlQueueingDiscipline.NetworkEmulatorDuplicateRate, config_parse_network_emulator_rate, 0, 0
+TrafficControlQueueingDiscipline.NetworkEmulatorPacketLimit, config_parse_network_emulator_packet_limit, 0, 0
+FairQueueing.Quantum, config_parse_fair_queueing_size, QDISC_KIND_FQ, 0
+FairQueueing.InitialQuantum, config_parse_fair_queueing_size, QDISC_KIND_FQ, 0
+FairQueueingControlledDelay.MemoryLimit, config_parse_fair_queueing_controlled_delay_size, QDISC_KIND_FQ_CODEL, 0
+FairQueueingControlledDelay.Quantum, config_parse_fair_queueing_controlled_delay_size, QDISC_KIND_FQ_CODEL, 0
+TokenBucketFilter.Burst, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0
+TokenBucketFilter.LimitSize, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0
diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c
new file mode 100644
index 0000000..799da2d
--- /dev/null
+++ b/src/network/networkd-network.c
@@ -0,0 +1,1335 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/netdevice.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "conf-files.h"
+#include "conf-parser.h"
+#include "dns-domain.h"
+#include "fd-util.h"
+#include "hostname-util.h"
+#include "in-addr-util.h"
+#include "net-condition.h"
+#include "netdev/macvlan.h"
+#include "networkd-address-label.h"
+#include "networkd-address.h"
+#include "networkd-bridge-fdb.h"
+#include "networkd-bridge-mdb.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-dhcp-server-static-lease.h"
+#include "networkd-dhcp-server.h"
+#include "networkd-ipv6-proxy-ndp.h"
+#include "networkd-manager.h"
+#include "networkd-ndisc.h"
+#include "networkd-neighbor.h"
+#include "networkd-network.h"
+#include "networkd-nexthop.h"
+#include "networkd-radv.h"
+#include "networkd-route.h"
+#include "networkd-routing-policy-rule.h"
+#include "networkd-sriov.h"
+#include "parse-util.h"
+#include "path-lookup.h"
+#include "qdisc.h"
+#include "radv-internal.h"
+#include "set.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tclass.h"
+#include "util.h"
+
+/* Let's assume that anything above this number is a user misconfiguration. */
+#define MAX_NTP_SERVERS 128U
+
+static int network_resolve_netdev_one(Network *network, const char *name, NetDevKind kind, NetDev **ret) {
+ const char *kind_string;
+ NetDev *netdev;
+ int r;
+
+ /* For test-networkd-conf, the check must be earlier than the assertions. */
+ if (!name)
+ return 0;
+
+ assert(network);
+ assert(network->manager);
+ assert(network->filename);
+ assert(ret);
+
+ if (kind == _NETDEV_KIND_TUNNEL)
+ kind_string = "tunnel";
+ else {
+ kind_string = netdev_kind_to_string(kind);
+ if (!kind_string)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Invalid NetDev kind of %s, ignoring assignment.",
+ network->filename, name);
+ }
+
+ r = netdev_get(network->manager, name, &netdev);
+ if (r < 0)
+ return log_warning_errno(r, "%s: %s NetDev could not be found, ignoring assignment.",
+ network->filename, name);
+
+ if (netdev->kind != kind && !(kind == _NETDEV_KIND_TUNNEL &&
+ IN_SET(netdev->kind,
+ NETDEV_KIND_ERSPAN,
+ NETDEV_KIND_GRE,
+ NETDEV_KIND_GRETAP,
+ NETDEV_KIND_IP6GRE,
+ NETDEV_KIND_IP6GRETAP,
+ NETDEV_KIND_IP6TNL,
+ NETDEV_KIND_IPIP,
+ NETDEV_KIND_SIT,
+ NETDEV_KIND_VTI,
+ NETDEV_KIND_VTI6)))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: NetDev %s is not a %s, ignoring assignment",
+ network->filename, name, kind_string);
+
+ *ret = netdev_ref(netdev);
+ return 1;
+}
+
+static int network_resolve_stacked_netdevs(Network *network) {
+ void *name, *kind;
+ int r;
+
+ assert(network);
+
+ HASHMAP_FOREACH_KEY(kind, name, network->stacked_netdev_names) {
+ _cleanup_(netdev_unrefp) NetDev *netdev = NULL;
+
+ if (network_resolve_netdev_one(network, name, PTR_TO_INT(kind), &netdev) <= 0)
+ continue;
+
+ r = hashmap_ensure_put(&network->stacked_netdevs, &string_hash_ops, netdev->ifname, netdev);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ log_warning_errno(r, "%s: Failed to add NetDev '%s' to network, ignoring: %m",
+ network->filename, (const char *) name);
+
+ netdev = NULL;
+ }
+
+ return 0;
+}
+
+int network_verify(Network *network) {
+ int r;
+
+ assert(network);
+ assert(network->manager);
+ assert(network->filename);
+
+ if (net_match_is_empty(&network->match) && !network->conditions)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: No valid settings found in the [Match] section, ignoring file. "
+ "To match all interfaces, add Name=* in the [Match] section.",
+ network->filename);
+
+ /* skip out early if configuration does not match the environment */
+ if (!condition_test_list(network->conditions, environ, NULL, NULL, NULL))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Conditions in the file do not match the system environment, skipping.",
+ network->filename);
+
+ if (network->keep_master) {
+ if (network->batadv_name)
+ log_warning("%s: BatmanAdvanced= set with KeepMaster= enabled, ignoring BatmanAdvanced=.",
+ network->filename);
+ if (network->bond_name)
+ log_warning("%s: Bond= set with KeepMaster= enabled, ignoring Bond=.",
+ network->filename);
+ if (network->bridge_name)
+ log_warning("%s: Bridge= set with KeepMaster= enabled, ignoring Bridge=.",
+ network->filename);
+ if (network->vrf_name)
+ log_warning("%s: VRF= set with KeepMaster= enabled, ignoring VRF=.",
+ network->filename);
+
+ network->batadv_name = mfree(network->batadv_name);
+ network->bond_name = mfree(network->bond_name);
+ network->bridge_name = mfree(network->bridge_name);
+ network->vrf_name = mfree(network->vrf_name);
+ }
+
+ (void) network_resolve_netdev_one(network, network->batadv_name, NETDEV_KIND_BATADV, &network->batadv);
+ (void) network_resolve_netdev_one(network, network->bond_name, NETDEV_KIND_BOND, &network->bond);
+ (void) network_resolve_netdev_one(network, network->bridge_name, NETDEV_KIND_BRIDGE, &network->bridge);
+ (void) network_resolve_netdev_one(network, network->vrf_name, NETDEV_KIND_VRF, &network->vrf);
+ r = network_resolve_stacked_netdevs(network);
+ if (r < 0)
+ return r;
+
+ /* Free unnecessary entries. */
+ network->batadv_name = mfree(network->batadv_name);
+ network->bond_name = mfree(network->bond_name);
+ network->bridge_name = mfree(network->bridge_name);
+ network->vrf_name = mfree(network->vrf_name);
+ network->stacked_netdev_names = hashmap_free_free_key(network->stacked_netdev_names);
+
+ if (network->bond) {
+ /* Bonding slave does not support addressing. */
+ if (network->link_local >= 0 && network->link_local != ADDRESS_FAMILY_NO) {
+ log_warning("%s: Cannot enable LinkLocalAddressing= when Bond= is specified, disabling LinkLocalAddressing=.",
+ network->filename);
+ network->link_local = ADDRESS_FAMILY_NO;
+ }
+ if (network->dhcp_server) {
+ log_warning("%s: Cannot enable DHCPServer= when Bond= is specified, disabling DHCPServer=.",
+ network->filename);
+ network->dhcp_server = false;
+ }
+ if (!ordered_hashmap_isempty(network->addresses_by_section))
+ log_warning("%s: Cannot set addresses when Bond= is specified, ignoring addresses.",
+ network->filename);
+ if (!hashmap_isempty(network->routes_by_section))
+ log_warning("%s: Cannot set routes when Bond= is specified, ignoring routes.",
+ network->filename);
+
+ network->addresses_by_section = ordered_hashmap_free_with_destructor(network->addresses_by_section, address_free);
+ network->routes_by_section = hashmap_free_with_destructor(network->routes_by_section, route_free);
+ }
+
+ if (network->link_local < 0) {
+ network->link_local = ADDRESS_FAMILY_IPV6;
+
+ if (network->keep_master || network->bridge)
+ network->link_local = ADDRESS_FAMILY_NO;
+ else {
+ NetDev *netdev;
+
+ HASHMAP_FOREACH(netdev, network->stacked_netdevs) {
+ MacVlan *m;
+
+ if (netdev->kind == NETDEV_KIND_MACVLAN)
+ m = MACVLAN(netdev);
+ else if (netdev->kind == NETDEV_KIND_MACVTAP)
+ m = MACVTAP(netdev);
+ else
+ continue;
+
+ assert(m);
+
+ if (m->mode == NETDEV_MACVLAN_MODE_PASSTHRU)
+ network->link_local = ADDRESS_FAMILY_NO;
+
+ /* There won't be a passthru MACVLAN/MACVTAP if there's already one in another mode */
+ break;
+ }
+ }
+ }
+
+ if (network->ipv6ll_address_gen_mode == IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE)
+ SET_FLAG(network->link_local, ADDRESS_FAMILY_IPV6, false);
+
+ if (in6_addr_is_set(&network->ipv6ll_stable_secret) &&
+ network->ipv6ll_address_gen_mode < 0)
+ network->ipv6ll_address_gen_mode = IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY;
+
+ /* IPMasquerade implies IPForward */
+ network->ip_forward |= network->ip_masquerade;
+
+ network_adjust_ipv6_proxy_ndp(network);
+ network_adjust_ipv6_accept_ra(network);
+ network_adjust_dhcp(network);
+ network_adjust_radv(network);
+ network_adjust_bridge_vlan(network);
+
+ if (network->mtu > 0 && network->dhcp_use_mtu) {
+ log_warning("%s: MTUBytes= in [Link] section and UseMTU= in [DHCP] section are set. "
+ "Disabling UseMTU=.", network->filename);
+ network->dhcp_use_mtu = false;
+ }
+
+ if (network->dhcp_critical >= 0) {
+ if (network->keep_configuration >= 0) {
+ if (network->manager->keep_configuration < 0)
+ log_warning("%s: Both KeepConfiguration= and deprecated CriticalConnection= are set. "
+ "Ignoring CriticalConnection=.", network->filename);
+ } else if (network->dhcp_critical)
+ /* CriticalConnection=yes also preserve foreign static configurations. */
+ network->keep_configuration = KEEP_CONFIGURATION_YES;
+ else
+ network->keep_configuration = KEEP_CONFIGURATION_NO;
+ }
+
+ if (!strv_isempty(network->bind_carrier)) {
+ if (!IN_SET(network->activation_policy, _ACTIVATION_POLICY_INVALID, ACTIVATION_POLICY_BOUND))
+ log_warning("%s: ActivationPolicy=bound is required with BindCarrier=. "
+ "Setting ActivationPolicy=bound.", network->filename);
+ network->activation_policy = ACTIVATION_POLICY_BOUND;
+ } else if (network->activation_policy == ACTIVATION_POLICY_BOUND) {
+ log_warning("%s: ActivationPolicy=bound requires BindCarrier=. "
+ "Ignoring ActivationPolicy=bound.", network->filename);
+ network->activation_policy = ACTIVATION_POLICY_UP;
+ }
+
+ if (network->activation_policy == _ACTIVATION_POLICY_INVALID)
+ network->activation_policy = ACTIVATION_POLICY_UP;
+
+ if (network->activation_policy == ACTIVATION_POLICY_ALWAYS_UP) {
+ if (network->ignore_carrier_loss_set && network->ignore_carrier_loss_usec < USEC_INFINITY)
+ log_warning("%s: IgnoreCarrierLoss=no or finite timespan conflicts with ActivationPolicy=always-up. "
+ "Setting IgnoreCarrierLoss=yes.", network->filename);
+ network->ignore_carrier_loss_set = true;
+ network->ignore_carrier_loss_usec = USEC_INFINITY;
+ }
+
+ if (!network->ignore_carrier_loss_set) /* Set implied default. */
+ network->ignore_carrier_loss_usec = network->configure_without_carrier ? USEC_INFINITY : 0;
+
+ if (IN_SET(network->activation_policy, ACTIVATION_POLICY_DOWN, ACTIVATION_POLICY_ALWAYS_DOWN, ACTIVATION_POLICY_MANUAL)) {
+ if (network->required_for_online < 0 ||
+ (network->required_for_online == true && network->activation_policy == ACTIVATION_POLICY_ALWAYS_DOWN)) {
+ log_debug("%s: Setting RequiredForOnline=no because ActivationPolicy=%s.", network->filename,
+ activation_policy_to_string(network->activation_policy));
+ network->required_for_online = false;
+ } else if (network->required_for_online == true)
+ log_warning("%s: RequiredForOnline=yes and ActivationPolicy=%s, "
+ "this may cause a delay at boot.", network->filename,
+ activation_policy_to_string(network->activation_policy));
+ }
+
+ if (network->required_for_online < 0)
+ network->required_for_online = true;
+
+ if (network->keep_configuration < 0)
+ network->keep_configuration = KEEP_CONFIGURATION_NO;
+
+ if (network->ipv6_proxy_ndp == 0 && !set_isempty(network->ipv6_proxy_ndp_addresses)) {
+ log_warning("%s: IPv6ProxyNDP= is disabled. Ignoring IPv6ProxyNDPAddress=.", network->filename);
+ network->ipv6_proxy_ndp_addresses = set_free_free(network->ipv6_proxy_ndp_addresses);
+ }
+
+ r = network_drop_invalid_addresses(network);
+ if (r < 0)
+ return r; /* network_drop_invalid_addresses() logs internally. */
+ network_drop_invalid_routes(network);
+ network_drop_invalid_nexthops(network);
+ network_drop_invalid_bridge_fdb_entries(network);
+ network_drop_invalid_bridge_mdb_entries(network);
+ network_drop_invalid_neighbors(network);
+ network_drop_invalid_address_labels(network);
+ network_drop_invalid_prefixes(network);
+ network_drop_invalid_route_prefixes(network);
+ network_drop_invalid_routing_policy_rules(network);
+ network_drop_invalid_qdisc(network);
+ network_drop_invalid_tclass(network);
+ r = sr_iov_drop_invalid_sections(UINT32_MAX, network->sr_iov_by_section);
+ if (r < 0)
+ return r; /* sr_iov_drop_invalid_sections() logs internally. */
+ network_drop_invalid_static_leases(network);
+
+ network_adjust_dhcp_server(network);
+
+ return 0;
+}
+
+int network_load_one(Manager *manager, OrderedHashmap **networks, const char *filename) {
+ _cleanup_free_ char *fname = NULL, *name = NULL;
+ _cleanup_(network_unrefp) Network *network = NULL;
+ const char *dropin_dirname;
+ char *d;
+ int r;
+
+ assert(manager);
+ assert(filename);
+
+ r = null_or_empty_path(filename);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to check if \"%s\" is empty: %m", filename);
+ if (r > 0) {
+ log_debug("Skipping empty file: %s", filename);
+ return 0;
+ }
+
+ fname = strdup(filename);
+ if (!fname)
+ return log_oom();
+
+ name = strdup(basename(filename));
+ if (!name)
+ return log_oom();
+
+ d = strrchr(name, '.');
+ if (!d)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid file name: %s", filename);
+
+ *d = '\0';
+
+ dropin_dirname = strjoina(name, ".network.d");
+
+ network = new(Network, 1);
+ if (!network)
+ return log_oom();
+
+ *network = (Network) {
+ .filename = TAKE_PTR(fname),
+ .name = TAKE_PTR(name),
+
+ .manager = manager,
+ .n_ref = 1,
+
+ .required_for_online = -1,
+ .required_operstate_for_online = LINK_OPERSTATE_RANGE_DEFAULT,
+ .activation_policy = _ACTIVATION_POLICY_INVALID,
+ .group = -1,
+ .arp = -1,
+ .multicast = -1,
+ .allmulticast = -1,
+ .promiscuous = -1,
+
+ .keep_configuration = manager->keep_configuration,
+
+ .dhcp_duid.type = _DUID_TYPE_INVALID,
+ .dhcp_critical = -1,
+ .dhcp_use_ntp = true,
+ .dhcp_routes_to_ntp = true,
+ .dhcp_use_sip = true,
+ .dhcp_use_dns = true,
+ .dhcp_routes_to_dns = true,
+ .dhcp_use_hostname = true,
+ .dhcp_use_routes = true,
+ .dhcp_use_gateway = -1,
+ .dhcp_send_hostname = true,
+ .dhcp_send_release = true,
+ .dhcp_route_metric = DHCP_ROUTE_METRIC,
+ .dhcp_client_identifier = _DHCP_CLIENT_ID_INVALID,
+ .dhcp_route_table = RT_TABLE_MAIN,
+ .dhcp_ip_service_type = -1,
+ .dhcp_broadcast = -1,
+
+ .dhcp6_use_address = true,
+ .dhcp6_use_pd_prefix = true,
+ .dhcp6_use_dns = true,
+ .dhcp6_use_hostname = true,
+ .dhcp6_use_ntp = true,
+ .dhcp6_use_rapid_commit = true,
+ .dhcp6_duid.type = _DUID_TYPE_INVALID,
+ .dhcp6_client_start_mode = _DHCP6_CLIENT_START_MODE_INVALID,
+
+ .dhcp_pd = -1,
+ .dhcp_pd_announce = true,
+ .dhcp_pd_assign = true,
+ .dhcp_pd_manage_temporary_address = true,
+ .dhcp_pd_subnet_id = -1,
+ .dhcp_pd_route_metric = DHCP6PD_ROUTE_METRIC,
+
+ .dhcp_server_bind_to_interface = true,
+ .dhcp_server_emit[SD_DHCP_LEASE_DNS].emit = true,
+ .dhcp_server_emit[SD_DHCP_LEASE_NTP].emit = true,
+ .dhcp_server_emit[SD_DHCP_LEASE_SIP].emit = true,
+ .dhcp_server_emit_router = true,
+ .dhcp_server_emit_timezone = true,
+
+ .router_lifetime_usec = RADV_DEFAULT_ROUTER_LIFETIME_USEC,
+ .router_dns_lifetime_usec = RADV_DEFAULT_VALID_LIFETIME_USEC,
+ .router_emit_dns = true,
+ .router_emit_domains = true,
+
+ .use_bpdu = -1,
+ .hairpin = -1,
+ .isolated = -1,
+ .fast_leave = -1,
+ .allow_port_to_be_root = -1,
+ .unicast_flood = -1,
+ .multicast_flood = -1,
+ .multicast_to_unicast = -1,
+ .neighbor_suppression = -1,
+ .learning = -1,
+ .bridge_proxy_arp = -1,
+ .bridge_proxy_arp_wifi = -1,
+ .priority = LINK_BRIDGE_PORT_PRIORITY_INVALID,
+ .multicast_router = _MULTICAST_ROUTER_INVALID,
+
+ .lldp_mode = LLDP_MODE_ROUTERS_ONLY,
+ .lldp_multicast_mode = _SD_LLDP_MULTICAST_MODE_INVALID,
+
+ .dns_default_route = -1,
+ .llmnr = RESOLVE_SUPPORT_YES,
+ .mdns = RESOLVE_SUPPORT_NO,
+ .dnssec_mode = _DNSSEC_MODE_INVALID,
+ .dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID,
+
+ /* If LinkLocalAddressing= is not set, then set to ADDRESS_FAMILY_IPV6 later. */
+ .link_local = _ADDRESS_FAMILY_INVALID,
+ .ipv6ll_address_gen_mode = _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_INVALID,
+
+ .ipv4_accept_local = -1,
+ .ipv4_route_localnet = -1,
+ .ipv6_privacy_extensions = IPV6_PRIVACY_EXTENSIONS_NO,
+ .ipv6_dad_transmits = -1,
+ .ipv6_hop_limit = -1,
+ .ipv6_proxy_ndp = -1,
+ .proxy_arp = -1,
+
+ .ipv6_accept_ra = -1,
+ .ipv6_accept_ra_use_dns = true,
+ .ipv6_accept_ra_use_gateway = true,
+ .ipv6_accept_ra_use_route_prefix = true,
+ .ipv6_accept_ra_use_autonomous_prefix = true,
+ .ipv6_accept_ra_use_onlink_prefix = true,
+ .ipv6_accept_ra_use_mtu = true,
+ .ipv6_accept_ra_route_table = RT_TABLE_MAIN,
+ .ipv6_accept_ra_route_metric = DHCP_ROUTE_METRIC,
+ .ipv6_accept_ra_start_dhcp6_client = IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES,
+
+ .can_termination = -1,
+
+ .ipoib_mode = _IP_OVER_INFINIBAND_MODE_INVALID,
+ .ipoib_umcast = -1,
+ };
+
+ r = config_parse_many(
+ STRV_MAKE_CONST(filename), NETWORK_DIRS, dropin_dirname,
+ "Match\0"
+ "Link\0"
+ "SR-IOV\0"
+ "Network\0"
+ "Address\0"
+ "Neighbor\0"
+ "IPv6AddressLabel\0"
+ "RoutingPolicyRule\0"
+ "Route\0"
+ "NextHop\0"
+ "DHCP\0" /* compat */
+ "DHCPv4\0"
+ "DHCPv6\0"
+ "DHCPv6PrefixDelegation\0" /* compat */
+ "DHCPPrefixDelegation\0"
+ "DHCPServer\0"
+ "DHCPServerStaticLease\0"
+ "IPv6AcceptRA\0"
+ "IPv6NDPProxyAddress\0"
+ "Bridge\0"
+ "BridgeFDB\0"
+ "BridgeMDB\0"
+ "BridgeVLAN\0"
+ "IPv6SendRA\0"
+ "IPv6PrefixDelegation\0"
+ "IPv6Prefix\0"
+ "IPv6RoutePrefix\0"
+ "LLDP\0"
+ "TrafficControlQueueingDiscipline\0"
+ "CAN\0"
+ "QDisc\0"
+ "BFIFO\0"
+ "CAKE\0"
+ "ControlledDelay\0"
+ "DeficitRoundRobinScheduler\0"
+ "DeficitRoundRobinSchedulerClass\0"
+ "EnhancedTransmissionSelection\0"
+ "FairQueueing\0"
+ "FairQueueingControlledDelay\0"
+ "FlowQueuePIE\0"
+ "GenericRandomEarlyDetection\0"
+ "HeavyHitterFilter\0"
+ "HierarchyTokenBucket\0"
+ "HierarchyTokenBucketClass\0"
+ "NetworkEmulator\0"
+ "PFIFO\0"
+ "PFIFOFast\0"
+ "PFIFOHeadDrop\0"
+ "PIE\0"
+ "QuickFairQueueing\0"
+ "QuickFairQueueingClass\0"
+ "StochasticFairBlue\0"
+ "StochasticFairnessQueueing\0"
+ "TokenBucketFilter\0"
+ "TrivialLinkEqualizer\0",
+ config_item_perf_lookup, network_network_gperf_lookup,
+ CONFIG_PARSE_WARN,
+ network,
+ &network->stats_by_path,
+ NULL);
+ if (r < 0)
+ return r; /* config_parse_many() logs internally. */
+
+ r = network_add_ipv4ll_route(network);
+ if (r < 0)
+ return log_warning_errno(r, "%s: Failed to add IPv4LL route: %m", network->filename);
+
+ r = network_add_default_route_on_device(network);
+ if (r < 0)
+ return log_warning_errno(r, "%s: Failed to add default route on device: %m",
+ network->filename);
+
+ r = network_verify(network);
+ if (r < 0)
+ return r; /* network_verify() logs internally. */
+
+ r = ordered_hashmap_ensure_put(networks, &string_hash_ops, network->name, network);
+ if (r < 0)
+ return log_warning_errno(r, "%s: Failed to store configuration into hashmap: %m", filename);
+
+ TAKE_PTR(network);
+ return 0;
+}
+
+int network_load(Manager *manager, OrderedHashmap **networks) {
+ _cleanup_strv_free_ char **files = NULL;
+ int r;
+
+ assert(manager);
+
+ ordered_hashmap_clear_with_destructor(*networks, network_unref);
+
+ r = conf_files_list_strv(&files, ".network", NULL, 0, NETWORK_DIRS);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate network files: %m");
+
+ STRV_FOREACH(f, files)
+ (void) network_load_one(manager, networks, *f);
+
+ return 0;
+}
+
+int network_reload(Manager *manager) {
+ OrderedHashmap *new_networks = NULL;
+ Network *n, *old;
+ int r;
+
+ assert(manager);
+
+ r = network_load(manager, &new_networks);
+ if (r < 0)
+ goto failure;
+
+ ORDERED_HASHMAP_FOREACH(n, new_networks) {
+ r = network_get_by_name(manager, n->name, &old);
+ if (r < 0) {
+ log_debug("Found new .network file: %s", n->filename);
+ continue;
+ }
+
+ if (!stats_by_path_equal(n->stats_by_path, old->stats_by_path)) {
+ log_debug("Found updated .network file: %s", n->filename);
+ continue;
+ }
+
+ r = ordered_hashmap_replace(new_networks, old->name, old);
+ if (r < 0)
+ goto failure;
+
+ network_ref(old);
+ network_unref(n);
+ }
+
+ ordered_hashmap_free_with_destructor(manager->networks, network_unref);
+ manager->networks = new_networks;
+
+ return manager_build_dhcp_pd_subnet_ids(manager);
+
+failure:
+ ordered_hashmap_free_with_destructor(new_networks, network_unref);
+
+ return r;
+}
+
+int manager_build_dhcp_pd_subnet_ids(Manager *manager) {
+ Network *n;
+ int r;
+
+ assert(manager);
+
+ set_clear(manager->dhcp_pd_subnet_ids);
+
+ ORDERED_HASHMAP_FOREACH(n, manager->networks) {
+ if (n->unmanaged)
+ continue;
+
+ if (!n->dhcp_pd)
+ continue;
+
+ if (n->dhcp_pd_subnet_id < 0)
+ continue;
+
+ r = set_ensure_put(&manager->dhcp_pd_subnet_ids, &uint64_hash_ops, &n->dhcp_pd_subnet_id);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static Network *network_free(Network *network) {
+ if (!network)
+ return NULL;
+
+ free(network->name);
+ free(network->filename);
+ free(network->description);
+ hashmap_free(network->stats_by_path);
+
+ /* conditions */
+ net_match_clear(&network->match);
+ condition_free_list(network->conditions);
+
+ /* link settings */
+ strv_free(network->bind_carrier);
+
+ /* NTP */
+ strv_free(network->ntp);
+
+ /* DNS */
+ for (unsigned i = 0; i < network->n_dns; i++)
+ in_addr_full_free(network->dns[i]);
+ free(network->dns);
+ ordered_set_free(network->search_domains);
+ ordered_set_free(network->route_domains);
+ set_free_free(network->dnssec_negative_trust_anchors);
+
+ /* DHCP server */
+ free(network->dhcp_server_relay_agent_circuit_id);
+ free(network->dhcp_server_relay_agent_remote_id);
+ free(network->dhcp_server_boot_server_name);
+ free(network->dhcp_server_boot_filename);
+ free(network->dhcp_server_timezone);
+ free(network->dhcp_server_uplink_name);
+ for (sd_dhcp_lease_server_type_t t = 0; t < _SD_DHCP_LEASE_SERVER_TYPE_MAX; t++)
+ free(network->dhcp_server_emit[t].addresses);
+ ordered_hashmap_free(network->dhcp_server_send_options);
+ ordered_hashmap_free(network->dhcp_server_send_vendor_options);
+
+ /* DHCP client */
+ free(network->dhcp_vendor_class_identifier);
+ free(network->dhcp_mudurl);
+ free(network->dhcp_hostname);
+ free(network->dhcp_label);
+ set_free(network->dhcp_deny_listed_ip);
+ set_free(network->dhcp_allow_listed_ip);
+ strv_free(network->dhcp_user_class);
+ set_free(network->dhcp_request_options);
+ ordered_hashmap_free(network->dhcp_client_send_options);
+ ordered_hashmap_free(network->dhcp_client_send_vendor_options);
+ free(network->dhcp_netlabel);
+
+ /* DHCPv6 client */
+ free(network->dhcp6_mudurl);
+ strv_free(network->dhcp6_user_class);
+ strv_free(network->dhcp6_vendor_class);
+ set_free(network->dhcp6_request_options);
+ ordered_hashmap_free(network->dhcp6_client_send_options);
+ ordered_hashmap_free(network->dhcp6_client_send_vendor_options);
+ free(network->dhcp6_netlabel);
+
+ /* DHCP PD */
+ free(network->dhcp_pd_uplink_name);
+ set_free(network->dhcp_pd_tokens);
+ free(network->dhcp_pd_netlabel);
+
+ /* Router advertisement */
+ ordered_set_free(network->router_search_domains);
+ free(network->router_dns);
+ free(network->router_uplink_name);
+
+ /* NDisc */
+ set_free(network->ndisc_deny_listed_router);
+ set_free(network->ndisc_allow_listed_router);
+ set_free(network->ndisc_deny_listed_prefix);
+ set_free(network->ndisc_allow_listed_prefix);
+ set_free(network->ndisc_deny_listed_route_prefix);
+ set_free(network->ndisc_allow_listed_route_prefix);
+ set_free(network->ndisc_tokens);
+ free(network->ndisc_netlabel);
+
+ /* LLDP */
+ free(network->lldp_mudurl);
+
+ /* netdev */
+ free(network->batadv_name);
+ free(network->bridge_name);
+ free(network->bond_name);
+ free(network->vrf_name);
+ hashmap_free_free_key(network->stacked_netdev_names);
+ netdev_unref(network->bridge);
+ netdev_unref(network->bond);
+ netdev_unref(network->vrf);
+ hashmap_free_with_destructor(network->stacked_netdevs, netdev_unref);
+
+ /* static configs */
+ set_free_free(network->ipv6_proxy_ndp_addresses);
+ ordered_hashmap_free_with_destructor(network->addresses_by_section, address_free);
+ hashmap_free_with_destructor(network->routes_by_section, route_free);
+ hashmap_free_with_destructor(network->nexthops_by_section, nexthop_free);
+ hashmap_free_with_destructor(network->bridge_fdb_entries_by_section, bridge_fdb_free);
+ hashmap_free_with_destructor(network->bridge_mdb_entries_by_section, bridge_mdb_free);
+ hashmap_free_with_destructor(network->neighbors_by_section, neighbor_free);
+ hashmap_free_with_destructor(network->address_labels_by_section, address_label_free);
+ hashmap_free_with_destructor(network->prefixes_by_section, prefix_free);
+ hashmap_free_with_destructor(network->route_prefixes_by_section, route_prefix_free);
+ hashmap_free_with_destructor(network->rules_by_section, routing_policy_rule_free);
+ hashmap_free_with_destructor(network->dhcp_static_leases_by_section, dhcp_static_lease_free);
+ ordered_hashmap_free_with_destructor(network->sr_iov_by_section, sr_iov_free);
+ hashmap_free_with_destructor(network->qdiscs_by_section, qdisc_free);
+ hashmap_free_with_destructor(network->tclasses_by_section, tclass_free);
+
+ return mfree(network);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(Network, network, network_free);
+
+int network_get_by_name(Manager *manager, const char *name, Network **ret) {
+ Network *network;
+
+ assert(manager);
+ assert(name);
+ assert(ret);
+
+ network = ordered_hashmap_get(manager->networks, name);
+ if (!network)
+ return -ENOENT;
+
+ *ret = network;
+
+ return 0;
+}
+
+bool network_has_static_ipv6_configurations(Network *network) {
+ Address *address;
+ Route *route;
+ BridgeFDB *fdb;
+ BridgeMDB *mdb;
+ Neighbor *neighbor;
+
+ assert(network);
+
+ ORDERED_HASHMAP_FOREACH(address, network->addresses_by_section)
+ if (address->family == AF_INET6)
+ return true;
+
+ HASHMAP_FOREACH(route, network->routes_by_section)
+ if (route->family == AF_INET6)
+ return true;
+
+ HASHMAP_FOREACH(fdb, network->bridge_fdb_entries_by_section)
+ if (fdb->family == AF_INET6)
+ return true;
+
+ HASHMAP_FOREACH(mdb, network->bridge_mdb_entries_by_section)
+ if (mdb->family == AF_INET6)
+ return true;
+
+ HASHMAP_FOREACH(neighbor, network->neighbors_by_section)
+ if (neighbor->family == AF_INET6)
+ return true;
+
+ if (!hashmap_isempty(network->address_labels_by_section))
+ return true;
+
+ if (!hashmap_isempty(network->prefixes_by_section))
+ return true;
+
+ if (!hashmap_isempty(network->route_prefixes_by_section))
+ return true;
+
+ return false;
+}
+
+int config_parse_stacked_netdev(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *name = NULL;
+ NetDevKind kind = ltype;
+ Hashmap **h = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(IN_SET(kind,
+ NETDEV_KIND_IPOIB,
+ NETDEV_KIND_IPVLAN,
+ NETDEV_KIND_IPVTAP,
+ NETDEV_KIND_MACSEC,
+ NETDEV_KIND_MACVLAN,
+ NETDEV_KIND_MACVTAP,
+ NETDEV_KIND_VLAN,
+ NETDEV_KIND_VXLAN,
+ NETDEV_KIND_XFRM,
+ _NETDEV_KIND_TUNNEL));
+
+ if (!ifname_valid(rvalue)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid netdev name in %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ name = strdup(rvalue);
+ if (!name)
+ return log_oom();
+
+ r = hashmap_ensure_put(h, &string_hash_ops, name, INT_TO_PTR(kind));
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Cannot add NetDev '%s' to network, ignoring assignment: %m", name);
+ else if (r == 0)
+ log_syntax(unit, LOG_DEBUG, filename, line, r,
+ "NetDev '%s' specified twice, ignoring.", name);
+ else
+ TAKE_PTR(name);
+
+ return 0;
+}
+
+int config_parse_domains(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *n = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ n->search_domains = ordered_set_free(n->search_domains);
+ n->route_domains = ordered_set_free(n->route_domains);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *w = NULL, *normalized = NULL;
+ const char *domain;
+ bool is_route;
+
+ r = extract_first_word(&p, &w, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract search or route domain, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ is_route = w[0] == '~';
+ domain = is_route ? w + 1 : w;
+
+ if (dns_name_is_root(domain) || streq(domain, "*")) {
+ /* If the root domain appears as is, or the special token "*" is found, we'll
+ * consider this as routing domain, unconditionally. */
+ is_route = true;
+ domain = "."; /* make sure we don't allow empty strings, thus write the root
+ * domain as "." */
+ } else {
+ r = dns_name_normalize(domain, 0, &normalized);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "'%s' is not a valid domain name, ignoring.", domain);
+ continue;
+ }
+
+ domain = normalized;
+
+ if (is_localhost(domain)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "'localhost' domain may not be configured as search or route domain, ignoring assignment: %s",
+ domain);
+ continue;
+ }
+ }
+
+ OrderedSet **set = is_route ? &n->route_domains : &n->search_domains;
+ r = ordered_set_put_strdup(set, domain);
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return log_oom();
+ }
+}
+
+int config_parse_timezone(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ char **tz = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *tz = mfree(*tz);
+ return 0;
+ }
+
+ r = verify_timezone(rvalue, LOG_WARNING);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Timezone is not valid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ return free_and_strdup_warn(tz, rvalue);
+}
+
+int config_parse_dns(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *n = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ for (unsigned i = 0; i < n->n_dns; i++)
+ in_addr_full_free(n->dns[i]);
+ n->dns = mfree(n->dns);
+ n->n_dns = 0;
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_(in_addr_full_freep) struct in_addr_full *dns = NULL;
+ _cleanup_free_ char *w = NULL;
+ struct in_addr_full **m;
+
+ r = extract_first_word(&p, &w, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid syntax, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ r = in_addr_full_new_from_string(w, &dns);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse dns server address, ignoring: %s", w);
+ continue;
+ }
+
+ if (IN_SET(dns->port, 53, 853))
+ dns->port = 0;
+
+ m = reallocarray(n->dns, n->n_dns + 1, sizeof(struct in_addr_full*));
+ if (!m)
+ return log_oom();
+
+ m[n->n_dns++] = TAKE_PTR(dns);
+ n->dns = m;
+ }
+}
+
+int config_parse_dnssec_negative_trust_anchors(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Set **nta = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *nta = set_free_free(*nta);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *w = NULL;
+
+ r = extract_first_word(&p, &w, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract negative trust anchor domain, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ r = dns_name_is_valid(w);
+ if (r <= 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "%s is not a valid domain name, ignoring.", w);
+ continue;
+ }
+
+ r = set_ensure_consume(nta, &dns_name_hash_ops, TAKE_PTR(w));
+ if (r < 0)
+ return log_oom();
+ }
+}
+
+int config_parse_ntp(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ char ***l = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *l = strv_free(*l);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *w = NULL;
+
+ r = extract_first_word(&p, &w, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract NTP server name, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ r = dns_name_is_valid_or_address(w);
+ if (r <= 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "%s is not a valid domain name or IP address, ignoring.", w);
+ continue;
+ }
+
+ if (strv_length(*l) > MAX_NTP_SERVERS) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "More than %u NTP servers specified, ignoring \"%s\" and any subsequent entries.",
+ MAX_NTP_SERVERS, w);
+ return 0;
+ }
+
+ r = strv_consume(l, TAKE_PTR(w));
+ if (r < 0)
+ return log_oom();
+ }
+}
+
+int config_parse_required_for_online(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ LinkOperationalStateRange range;
+ bool required = true;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->required_for_online = -1;
+ network->required_operstate_for_online = LINK_OPERSTATE_RANGE_DEFAULT;
+ return 0;
+ }
+
+ r = parse_operational_state_range(rvalue, &range);
+ if (r < 0) {
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s= setting, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ required = r;
+ range = LINK_OPERSTATE_RANGE_DEFAULT;
+ }
+
+ network->required_for_online = required;
+ network->required_operstate_for_online = range;
+
+ return 0;
+}
+
+int config_parse_link_group(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ int r;
+ int32_t group;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->group = -1;
+ return 0;
+ }
+
+ r = safe_atoi32(rvalue, &group);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse Group=, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (group < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Value of Group= must be in the range 0…2147483647, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ network->group = group;
+ return 0;
+}
+
+int config_parse_ignore_carrier_loss(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = ASSERT_PTR(userdata);
+ usec_t usec;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->ignore_carrier_loss_set = false;
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r >= 0) {
+ network->ignore_carrier_loss_set = true;
+ network->ignore_carrier_loss_usec = r > 0 ? USEC_INFINITY : 0;
+ return 0;
+ }
+
+ r = parse_sec(rvalue, &usec);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ network->ignore_carrier_loss_set = true;
+ network->ignore_carrier_loss_usec = usec;
+ return 0;
+}
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_required_family_for_online, link_required_address_family, AddressFamily,
+ "Failed to parse RequiredFamilyForOnline= setting");
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_keep_configuration, keep_configuration, KeepConfiguration,
+ "Failed to parse KeepConfiguration= setting");
+
+static const char* const keep_configuration_table[_KEEP_CONFIGURATION_MAX] = {
+ [KEEP_CONFIGURATION_NO] = "no",
+ [KEEP_CONFIGURATION_DHCP_ON_STOP] = "dhcp-on-stop",
+ [KEEP_CONFIGURATION_DHCP] = "dhcp",
+ [KEEP_CONFIGURATION_STATIC] = "static",
+ [KEEP_CONFIGURATION_YES] = "yes",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(keep_configuration, KeepConfiguration, KEEP_CONFIGURATION_YES);
+
+static const char* const activation_policy_table[_ACTIVATION_POLICY_MAX] = {
+ [ACTIVATION_POLICY_UP] = "up",
+ [ACTIVATION_POLICY_ALWAYS_UP] = "always-up",
+ [ACTIVATION_POLICY_MANUAL] = "manual",
+ [ACTIVATION_POLICY_ALWAYS_DOWN] = "always-down",
+ [ACTIVATION_POLICY_DOWN] = "down",
+ [ACTIVATION_POLICY_BOUND] = "bound",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(activation_policy, ActivationPolicy);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_activation_policy, activation_policy, ActivationPolicy, "Failed to parse activation policy");
diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h
new file mode 100644
index 0000000..a2743f9
--- /dev/null
+++ b/src/network/networkd-network.h
@@ -0,0 +1,400 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <linux/nl80211.h>
+
+#include "sd-bus.h"
+#include "sd-device.h"
+#include "sd-lldp-tx.h"
+
+#include "bridge.h"
+#include "condition.h"
+#include "conf-parser.h"
+#include "hashmap.h"
+#include "ipoib.h"
+#include "net-condition.h"
+#include "netdev.h"
+#include "networkd-bridge-vlan.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-dhcp4.h"
+#include "networkd-dhcp6.h"
+#include "networkd-ipv6ll.h"
+#include "networkd-lldp-rx.h"
+#include "networkd-ndisc.h"
+#include "networkd-radv.h"
+#include "networkd-sysctl.h"
+#include "networkd-util.h"
+#include "ordered-set.h"
+#include "resolve-util.h"
+#include "socket-netlink.h"
+
+typedef enum KeepConfiguration {
+ KEEP_CONFIGURATION_NO = 0,
+ KEEP_CONFIGURATION_DHCP_ON_START = 1 << 0,
+ KEEP_CONFIGURATION_DHCP_ON_STOP = 1 << 1,
+ KEEP_CONFIGURATION_DHCP = KEEP_CONFIGURATION_DHCP_ON_START | KEEP_CONFIGURATION_DHCP_ON_STOP,
+ KEEP_CONFIGURATION_STATIC = 1 << 2,
+ KEEP_CONFIGURATION_YES = KEEP_CONFIGURATION_DHCP | KEEP_CONFIGURATION_STATIC,
+ _KEEP_CONFIGURATION_MAX,
+ _KEEP_CONFIGURATION_INVALID = -EINVAL,
+} KeepConfiguration;
+
+typedef enum ActivationPolicy {
+ ACTIVATION_POLICY_UP,
+ ACTIVATION_POLICY_ALWAYS_UP,
+ ACTIVATION_POLICY_MANUAL,
+ ACTIVATION_POLICY_ALWAYS_DOWN,
+ ACTIVATION_POLICY_DOWN,
+ ACTIVATION_POLICY_BOUND,
+ _ACTIVATION_POLICY_MAX,
+ _ACTIVATION_POLICY_INVALID = -EINVAL,
+} ActivationPolicy;
+
+typedef struct Manager Manager;
+
+typedef struct NetworkDHCPServerEmitAddress {
+ bool emit;
+ struct in_addr *addresses;
+ size_t n_addresses;
+} NetworkDHCPServerEmitAddress;
+
+struct Network {
+ Manager *manager;
+
+ unsigned n_ref;
+
+ char *name;
+ char *filename;
+ Hashmap *stats_by_path;
+ char *description;
+
+ /* [Match] section */
+ NetMatch match;
+ LIST_HEAD(Condition, conditions);
+
+ /* Master or stacked netdevs */
+ bool keep_master;
+ NetDev *batadv;
+ NetDev *bridge;
+ NetDev *bond;
+ NetDev *vrf;
+ NetDev *xfrm;
+ Hashmap *stacked_netdevs;
+ char *batadv_name;
+ char *bridge_name;
+ char *bond_name;
+ char *vrf_name;
+ Hashmap *stacked_netdev_names;
+
+ /* [Link] section */
+ struct hw_addr_data hw_addr;
+ uint32_t mtu;
+ int32_t group;
+ int arp;
+ int multicast;
+ int allmulticast;
+ int promiscuous;
+ bool unmanaged;
+ int required_for_online; /* Is this network required to be considered online? */
+ LinkOperationalStateRange required_operstate_for_online;
+ AddressFamily required_family_for_online;
+ ActivationPolicy activation_policy;
+
+ /* misc settings */
+ bool configure_without_carrier;
+ bool ignore_carrier_loss_set;
+ usec_t ignore_carrier_loss_usec; /* timespan */
+ KeepConfiguration keep_configuration;
+ char **bind_carrier;
+ bool default_route_on_device;
+ AddressFamily ip_masquerade;
+
+ /* DHCP Client Support */
+ AddressFamily dhcp;
+ DHCPClientIdentifier dhcp_client_identifier;
+ DUID dhcp_duid;
+ uint32_t dhcp_iaid;
+ bool dhcp_iaid_set;
+ char *dhcp_vendor_class_identifier;
+ char *dhcp_mudurl;
+ char **dhcp_user_class;
+ char *dhcp_hostname;
+ char *dhcp_label;
+ uint64_t dhcp_max_attempts;
+ uint32_t dhcp_route_metric;
+ bool dhcp_route_metric_set;
+ uint32_t dhcp_route_table;
+ bool dhcp_route_table_set;
+ uint32_t dhcp_fallback_lease_lifetime;
+ uint32_t dhcp_route_mtu;
+ uint16_t dhcp_client_port;
+ int dhcp_critical;
+ int dhcp_ip_service_type;
+ bool dhcp_anonymize;
+ bool dhcp_send_hostname;
+ int dhcp_broadcast;
+ bool dhcp_use_dns;
+ bool dhcp_use_dns_set;
+ bool dhcp_routes_to_dns;
+ bool dhcp_use_ntp;
+ bool dhcp_use_ntp_set;
+ bool dhcp_routes_to_ntp;
+ bool dhcp_use_sip;
+ bool dhcp_use_mtu;
+ bool dhcp_use_routes;
+ int dhcp_use_gateway;
+ bool dhcp_use_timezone;
+ bool dhcp_use_hostname;
+ bool dhcp_use_6rd;
+ bool dhcp_send_release;
+ bool dhcp_send_decline;
+ DHCPUseDomains dhcp_use_domains;
+ bool dhcp_use_domains_set;
+ Set *dhcp_deny_listed_ip;
+ Set *dhcp_allow_listed_ip;
+ Set *dhcp_request_options;
+ OrderedHashmap *dhcp_client_send_options;
+ OrderedHashmap *dhcp_client_send_vendor_options;
+ char *dhcp_netlabel;
+
+ /* DHCPv6 Client support */
+ bool dhcp6_use_address;
+ bool dhcp6_use_pd_prefix;
+ bool dhcp6_use_dns;
+ bool dhcp6_use_dns_set;
+ bool dhcp6_use_hostname;
+ bool dhcp6_use_ntp;
+ bool dhcp6_use_ntp_set;
+ bool dhcp6_use_rapid_commit;
+ DHCPUseDomains dhcp6_use_domains;
+ bool dhcp6_use_domains_set;
+ uint32_t dhcp6_iaid;
+ bool dhcp6_iaid_set;
+ bool dhcp6_iaid_set_explicitly;
+ DUID dhcp6_duid;
+ uint8_t dhcp6_pd_prefix_length;
+ struct in6_addr dhcp6_pd_prefix_hint;
+ char *dhcp6_mudurl;
+ char **dhcp6_user_class;
+ char **dhcp6_vendor_class;
+ DHCP6ClientStartMode dhcp6_client_start_mode;
+ OrderedHashmap *dhcp6_client_send_options;
+ OrderedHashmap *dhcp6_client_send_vendor_options;
+ Set *dhcp6_request_options;
+ char *dhcp6_netlabel;
+
+ /* DHCP Server Support */
+ bool dhcp_server;
+ bool dhcp_server_bind_to_interface;
+ unsigned char dhcp_server_address_prefixlen;
+ struct in_addr dhcp_server_address;
+ int dhcp_server_uplink_index;
+ char *dhcp_server_uplink_name;
+ struct in_addr dhcp_server_relay_target;
+ char *dhcp_server_relay_agent_circuit_id;
+ char *dhcp_server_relay_agent_remote_id;
+ NetworkDHCPServerEmitAddress dhcp_server_emit[_SD_DHCP_LEASE_SERVER_TYPE_MAX];
+ bool dhcp_server_emit_router;
+ struct in_addr dhcp_server_router;
+ bool dhcp_server_emit_timezone;
+ char *dhcp_server_timezone;
+ usec_t dhcp_server_default_lease_time_usec, dhcp_server_max_lease_time_usec;
+ uint32_t dhcp_server_pool_offset;
+ uint32_t dhcp_server_pool_size;
+ OrderedHashmap *dhcp_server_send_options;
+ OrderedHashmap *dhcp_server_send_vendor_options;
+ struct in_addr dhcp_server_boot_server_address;
+ char *dhcp_server_boot_server_name;
+ char *dhcp_server_boot_filename;
+
+ /* link-local addressing support */
+ AddressFamily link_local;
+ IPv6LinkLocalAddressGenMode ipv6ll_address_gen_mode;
+ struct in6_addr ipv6ll_stable_secret;
+ struct in_addr ipv4ll_start_address;
+ bool ipv4ll_route;
+
+ /* IPv6 RA support */
+ RADVPrefixDelegation router_prefix_delegation;
+ usec_t router_lifetime_usec;
+ uint8_t router_preference;
+ bool router_managed;
+ bool router_other_information;
+ bool router_emit_dns;
+ bool router_emit_domains;
+ usec_t router_dns_lifetime_usec;
+ struct in6_addr *router_dns;
+ unsigned n_router_dns;
+ OrderedSet *router_search_domains;
+ int router_uplink_index;
+ char *router_uplink_name;
+
+ /* DHCP Prefix Delegation support */
+ int dhcp_pd;
+ bool dhcp_pd_announce;
+ bool dhcp_pd_assign;
+ bool dhcp_pd_manage_temporary_address;
+ int64_t dhcp_pd_subnet_id;
+ uint32_t dhcp_pd_route_metric;
+ Set *dhcp_pd_tokens;
+ int dhcp_pd_uplink_index;
+ char *dhcp_pd_uplink_name;
+ char *dhcp_pd_netlabel;
+
+ /* Bridge Support */
+ int use_bpdu;
+ int hairpin;
+ int isolated;
+ int fast_leave;
+ int allow_port_to_be_root;
+ int unicast_flood;
+ int multicast_flood;
+ int multicast_to_unicast;
+ int neighbor_suppression;
+ int learning;
+ int bridge_proxy_arp;
+ int bridge_proxy_arp_wifi;
+ uint32_t cost;
+ uint16_t priority;
+ MulticastRouter multicast_router;
+
+ /* Bridge VLAN */
+ bool use_br_vlan;
+ uint16_t pvid;
+ uint32_t br_vid_bitmap[BRIDGE_VLAN_BITMAP_LEN];
+ uint32_t br_untagged_bitmap[BRIDGE_VLAN_BITMAP_LEN];
+
+ /* CAN support */
+ uint32_t can_bitrate;
+ unsigned can_sample_point;
+ nsec_t can_time_quanta_ns;
+ uint32_t can_propagation_segment;
+ uint32_t can_phase_buffer_segment_1;
+ uint32_t can_phase_buffer_segment_2;
+ uint32_t can_sync_jump_width;
+ uint32_t can_data_bitrate;
+ unsigned can_data_sample_point;
+ nsec_t can_data_time_quanta_ns;
+ uint32_t can_data_propagation_segment;
+ uint32_t can_data_phase_buffer_segment_1;
+ uint32_t can_data_phase_buffer_segment_2;
+ uint32_t can_data_sync_jump_width;
+ usec_t can_restart_us;
+ uint32_t can_control_mode_mask;
+ uint32_t can_control_mode_flags;
+ uint16_t can_termination;
+ bool can_termination_set;
+
+ /* IPoIB support */
+ IPoIBMode ipoib_mode;
+ int ipoib_umcast;
+
+ /* sysctl settings */
+ AddressFamily ip_forward;
+ int ipv4_accept_local;
+ int ipv4_route_localnet;
+ int ipv6_dad_transmits;
+ int ipv6_hop_limit;
+ int proxy_arp;
+ uint32_t ipv6_mtu;
+ IPv6PrivacyExtensions ipv6_privacy_extensions;
+ int ipv6_proxy_ndp;
+ Set *ipv6_proxy_ndp_addresses;
+
+ /* IPv6 accept RA */
+ int ipv6_accept_ra;
+ bool ipv6_accept_ra_use_dns;
+ bool ipv6_accept_ra_use_gateway;
+ bool ipv6_accept_ra_use_route_prefix;
+ bool ipv6_accept_ra_use_autonomous_prefix;
+ bool ipv6_accept_ra_use_onlink_prefix;
+ bool ipv6_accept_ra_use_mtu;
+ bool active_slave;
+ bool primary_slave;
+ DHCPUseDomains ipv6_accept_ra_use_domains;
+ IPv6AcceptRAStartDHCP6Client ipv6_accept_ra_start_dhcp6_client;
+ uint32_t ipv6_accept_ra_route_table;
+ bool ipv6_accept_ra_route_table_set;
+ uint32_t ipv6_accept_ra_route_metric;
+ bool ipv6_accept_ra_route_metric_set;
+ Set *ndisc_deny_listed_router;
+ Set *ndisc_allow_listed_router;
+ Set *ndisc_deny_listed_prefix;
+ Set *ndisc_allow_listed_prefix;
+ Set *ndisc_deny_listed_route_prefix;
+ Set *ndisc_allow_listed_route_prefix;
+ Set *ndisc_tokens;
+ char *ndisc_netlabel;
+
+ /* LLDP support */
+ LLDPMode lldp_mode; /* LLDP reception */
+ sd_lldp_multicast_mode_t lldp_multicast_mode; /* LLDP transmission */
+ char *lldp_mudurl; /* LLDP MUD URL */
+
+ OrderedHashmap *addresses_by_section;
+ Hashmap *routes_by_section;
+ Hashmap *nexthops_by_section;
+ Hashmap *bridge_fdb_entries_by_section;
+ Hashmap *bridge_mdb_entries_by_section;
+ Hashmap *neighbors_by_section;
+ Hashmap *address_labels_by_section;
+ Hashmap *prefixes_by_section;
+ Hashmap *route_prefixes_by_section;
+ Hashmap *rules_by_section;
+ Hashmap *dhcp_static_leases_by_section;
+ Hashmap *qdiscs_by_section;
+ Hashmap *tclasses_by_section;
+ OrderedHashmap *sr_iov_by_section;
+
+ /* All kinds of DNS configuration */
+ struct in_addr_full **dns;
+ unsigned n_dns;
+ OrderedSet *search_domains, *route_domains;
+ int dns_default_route;
+ ResolveSupport llmnr;
+ ResolveSupport mdns;
+ DnssecMode dnssec_mode;
+ DnsOverTlsMode dns_over_tls_mode;
+ Set *dnssec_negative_trust_anchors;
+
+ /* NTP */
+ char **ntp;
+};
+
+Network *network_ref(Network *network);
+Network *network_unref(Network *network);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Network*, network_unref);
+
+int network_load(Manager *manager, OrderedHashmap **networks);
+int network_reload(Manager *manager);
+int network_load_one(Manager *manager, OrderedHashmap **networks, const char *filename);
+int network_verify(Network *network);
+
+int manager_build_dhcp_pd_subnet_ids(Manager *manager);
+
+int network_get_by_name(Manager *manager, const char *name, Network **ret);
+void network_apply_anonymize_if_set(Network *network);
+
+bool network_has_static_ipv6_configurations(Network *network);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_stacked_netdev);
+CONFIG_PARSER_PROTOTYPE(config_parse_tunnel);
+CONFIG_PARSER_PROTOTYPE(config_parse_domains);
+CONFIG_PARSER_PROTOTYPE(config_parse_dns);
+CONFIG_PARSER_PROTOTYPE(config_parse_timezone);
+CONFIG_PARSER_PROTOTYPE(config_parse_dnssec_negative_trust_anchors);
+CONFIG_PARSER_PROTOTYPE(config_parse_ntp);
+CONFIG_PARSER_PROTOTYPE(config_parse_required_for_online);
+CONFIG_PARSER_PROTOTYPE(config_parse_required_family_for_online);
+CONFIG_PARSER_PROTOTYPE(config_parse_keep_configuration);
+CONFIG_PARSER_PROTOTYPE(config_parse_activation_policy);
+CONFIG_PARSER_PROTOTYPE(config_parse_link_group);
+CONFIG_PARSER_PROTOTYPE(config_parse_ignore_carrier_loss);
+
+const struct ConfigPerfItem* network_network_gperf_lookup(const char *key, GPERF_LEN_TYPE length);
+
+const char* keep_configuration_to_string(KeepConfiguration i) _const_;
+KeepConfiguration keep_configuration_from_string(const char *s) _pure_;
+
+const char* activation_policy_to_string(ActivationPolicy i) _const_;
+ActivationPolicy activation_policy_from_string(const char *s) _pure_;
diff --git a/src/network/networkd-nexthop.c b/src/network/networkd-nexthop.c
new file mode 100644
index 0000000..d827667
--- /dev/null
+++ b/src/network/networkd-nexthop.c
@@ -0,0 +1,1406 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc.
+ */
+
+#include <net/if.h>
+#include <linux/nexthop.h>
+
+#include "alloc-util.h"
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-nexthop.h"
+#include "networkd-queue.h"
+#include "networkd-route-util.h"
+#include "parse-util.h"
+#include "set.h"
+#include "stdio-util.h"
+#include "string-util.h"
+
+NextHop *nexthop_free(NextHop *nexthop) {
+ if (!nexthop)
+ return NULL;
+
+ if (nexthop->network) {
+ assert(nexthop->section);
+ hashmap_remove(nexthop->network->nexthops_by_section, nexthop->section);
+ }
+
+ config_section_free(nexthop->section);
+
+ if (nexthop->link) {
+ set_remove(nexthop->link->nexthops, nexthop);
+
+ if (nexthop->link->manager && nexthop->id > 0)
+ hashmap_remove(nexthop->link->manager->nexthops_by_id, UINT32_TO_PTR(nexthop->id));
+ }
+
+ if (nexthop->manager) {
+ set_remove(nexthop->manager->nexthops, nexthop);
+
+ if (nexthop->id > 0)
+ hashmap_remove(nexthop->manager->nexthops_by_id, UINT32_TO_PTR(nexthop->id));
+ }
+
+ hashmap_free_free(nexthop->group);
+
+ return mfree(nexthop);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(NextHop, nexthop_free);
+
+static int nexthop_new(NextHop **ret) {
+ _cleanup_(nexthop_freep) NextHop *nexthop = NULL;
+
+ nexthop = new(NextHop, 1);
+ if (!nexthop)
+ return -ENOMEM;
+
+ *nexthop = (NextHop) {
+ .family = AF_UNSPEC,
+ .onlink = -1,
+ };
+
+ *ret = TAKE_PTR(nexthop);
+
+ return 0;
+}
+
+static int nexthop_new_static(Network *network, const char *filename, unsigned section_line, NextHop **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(nexthop_freep) NextHop *nexthop = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ nexthop = hashmap_get(network->nexthops_by_section, n);
+ if (nexthop) {
+ *ret = TAKE_PTR(nexthop);
+ return 0;
+ }
+
+ r = nexthop_new(&nexthop);
+ if (r < 0)
+ return r;
+
+ nexthop->protocol = RTPROT_STATIC;
+ nexthop->network = network;
+ nexthop->section = TAKE_PTR(n);
+ nexthop->source = NETWORK_CONFIG_SOURCE_STATIC;
+
+ r = hashmap_ensure_put(&network->nexthops_by_section, &config_section_hash_ops, nexthop->section, nexthop);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(nexthop);
+ return 0;
+}
+
+static void nexthop_hash_func(const NextHop *nexthop, struct siphash *state) {
+ assert(nexthop);
+
+ siphash24_compress(&nexthop->protocol, sizeof(nexthop->protocol), state);
+ siphash24_compress(&nexthop->id, sizeof(nexthop->id), state);
+ siphash24_compress(&nexthop->blackhole, sizeof(nexthop->blackhole), state);
+ siphash24_compress(&nexthop->family, sizeof(nexthop->family), state);
+
+ switch (nexthop->family) {
+ case AF_INET:
+ case AF_INET6:
+ siphash24_compress(&nexthop->gw, FAMILY_ADDRESS_SIZE(nexthop->family), state);
+
+ break;
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ break;
+ }
+}
+
+static int nexthop_compare_func(const NextHop *a, const NextHop *b) {
+ int r;
+
+ r = CMP(a->protocol, b->protocol);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->id, b->id);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->blackhole, b->blackhole);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->family, b->family);
+ if (r != 0)
+ return r;
+
+ if (IN_SET(a->family, AF_INET, AF_INET6))
+ return memcmp(&a->gw, &b->gw, FAMILY_ADDRESS_SIZE(a->family));
+
+ return 0;
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ nexthop_hash_ops,
+ NextHop,
+ nexthop_hash_func,
+ nexthop_compare_func,
+ nexthop_free);
+
+static bool nexthop_equal(const NextHop *a, const NextHop *b) {
+ if (a == b)
+ return true;
+
+ if (!a || !b)
+ return false;
+
+ return nexthop_compare_func(a, b) == 0;
+}
+
+static int nexthop_dup(const NextHop *src, NextHop **ret) {
+ _cleanup_(nexthop_freep) NextHop *dest = NULL;
+ struct nexthop_grp *nhg;
+ int r;
+
+ assert(src);
+ assert(ret);
+
+ dest = newdup(NextHop, src, 1);
+ if (!dest)
+ return -ENOMEM;
+
+ /* unset all pointers */
+ dest->manager = NULL;
+ dest->link = NULL;
+ dest->network = NULL;
+ dest->section = NULL;
+ dest->group = NULL;
+
+ HASHMAP_FOREACH(nhg, src->group) {
+ _cleanup_free_ struct nexthop_grp *g = NULL;
+
+ g = newdup(struct nexthop_grp, nhg, 1);
+ if (!g)
+ return -ENOMEM;
+
+ r = hashmap_ensure_put(&dest->group, NULL, UINT32_TO_PTR(g->id), g);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ TAKE_PTR(g);
+ }
+
+ *ret = TAKE_PTR(dest);
+ return 0;
+}
+
+int manager_get_nexthop_by_id(Manager *manager, uint32_t id, NextHop **ret) {
+ NextHop *nh;
+
+ assert(manager);
+
+ if (id == 0)
+ return -EINVAL;
+
+ nh = hashmap_get(manager->nexthops_by_id, UINT32_TO_PTR(id));
+ if (!nh)
+ return -ENOENT;
+
+ if (ret)
+ *ret = nh;
+ return 0;
+}
+
+static bool nexthop_owned_by_link(const NextHop *nexthop) {
+ return !nexthop->blackhole && hashmap_isempty(nexthop->group);
+}
+
+static int nexthop_get(Manager *manager, Link *link, NextHop *in, NextHop **ret) {
+ NextHop *nexthop;
+ Set *nexthops;
+
+ assert(in);
+
+ if (nexthop_owned_by_link(in)) {
+ if (!link)
+ return -ENOENT;
+
+ nexthops = link->nexthops;
+ } else {
+ if (!manager)
+ return -ENOENT;
+
+ nexthops = manager->nexthops;
+ }
+
+ nexthop = set_get(nexthops, in);
+ if (nexthop) {
+ if (ret)
+ *ret = nexthop;
+ return 0;
+ }
+
+ if (in->id > 0)
+ return -ENOENT;
+
+ /* Also find nexthop configured without ID. */
+ SET_FOREACH(nexthop, nexthops) {
+ uint32_t id;
+ bool found;
+
+ id = nexthop->id;
+ nexthop->id = 0;
+ found = nexthop_equal(nexthop, in);
+ nexthop->id = id;
+
+ if (!found)
+ continue;
+
+ if (ret)
+ *ret = nexthop;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int nexthop_add(Manager *manager, Link *link, NextHop *nexthop) {
+ int r;
+
+ assert(nexthop);
+ assert(nexthop->id > 0);
+
+ if (nexthop_owned_by_link(nexthop)) {
+ assert(link);
+
+ r = set_ensure_put(&link->nexthops, &nexthop_hash_ops, nexthop);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ nexthop->link = link;
+
+ manager = link->manager;
+ } else {
+ assert(manager);
+
+ r = set_ensure_put(&manager->nexthops, &nexthop_hash_ops, nexthop);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ nexthop->manager = manager;
+ }
+
+ return hashmap_ensure_put(&manager->nexthops_by_id, NULL, UINT32_TO_PTR(nexthop->id), nexthop);
+}
+
+static int nexthop_acquire_id(Manager *manager, NextHop *nexthop) {
+ _cleanup_set_free_ Set *ids = NULL;
+ Network *network;
+ uint32_t id;
+ int r;
+
+ assert(manager);
+ assert(nexthop);
+
+ if (nexthop->id > 0)
+ return 0;
+
+ /* Find the lowest unused ID. */
+
+ ORDERED_HASHMAP_FOREACH(network, manager->networks) {
+ NextHop *tmp;
+
+ HASHMAP_FOREACH(tmp, network->nexthops_by_section) {
+ if (tmp->id == 0)
+ continue;
+
+ r = set_ensure_put(&ids, NULL, UINT32_TO_PTR(tmp->id));
+ if (r < 0)
+ return r;
+ }
+ }
+
+ for (id = 1; id < UINT32_MAX; id++) {
+ if (manager_get_nexthop_by_id(manager, id, NULL) >= 0)
+ continue;
+ if (set_contains(ids, UINT32_TO_PTR(id)))
+ continue;
+ break;
+ }
+
+ nexthop->id = id;
+ return 0;
+}
+
+static void log_nexthop_debug(const NextHop *nexthop, const char *str, const Link *link) {
+ _cleanup_free_ char *state = NULL, *group = NULL, *flags = NULL;
+ struct nexthop_grp *nhg;
+
+ assert(nexthop);
+ assert(str);
+
+ /* link may be NULL. */
+
+ if (!DEBUG_LOGGING)
+ return;
+
+ (void) network_config_state_to_string_alloc(nexthop->state, &state);
+ (void) route_flags_to_string_alloc(nexthop->flags, &flags);
+
+ HASHMAP_FOREACH(nhg, nexthop->group)
+ (void) strextendf_with_separator(&group, ",", "%"PRIu32":%"PRIu32, nhg->id, nhg->weight+1u);
+
+ log_link_debug(link, "%s %s nexthop (%s): id: %"PRIu32", gw: %s, blackhole: %s, group: %s, flags: %s",
+ str, strna(network_config_source_to_string(nexthop->source)), strna(state),
+ nexthop->id,
+ IN_ADDR_TO_STRING(nexthop->family, &nexthop->gw),
+ yes_no(nexthop->blackhole), strna(group), strna(flags));
+}
+
+static int nexthop_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+
+ /* link may be NULL. */
+
+ if (link && IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -ENOENT)
+ log_link_message_warning_errno(link, m, r, "Could not drop nexthop, ignoring");
+
+ return 1;
+}
+
+static int nexthop_remove(NextHop *nexthop) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ Manager *manager;
+ Link *link;
+ int r;
+
+ assert(nexthop);
+ assert(nexthop->manager || (nexthop->link && nexthop->link->manager));
+
+ /* link may be NULL. */
+ link = nexthop->link;
+ manager = nexthop->manager ?: nexthop->link->manager;
+
+ if (nexthop->id == 0) {
+ log_link_debug(link, "Cannot remove nexthop without valid ID, ignoring.");
+ return 0;
+ }
+
+ log_nexthop_debug(nexthop, "Removing", link);
+
+ r = sd_rtnl_message_new_nexthop(manager->rtnl, &req, RTM_DELNEXTHOP, AF_UNSPEC, RTPROT_UNSPEC);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create RTM_DELNEXTHOP message: %m");
+
+ r = sd_netlink_message_append_u32(req, NHA_ID, nexthop->id);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NHA_ID attribute: %m");
+
+ r = netlink_call_async(manager->rtnl, NULL, req, nexthop_remove_handler,
+ link ? link_netlink_destroy_callback : NULL, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not send rtnetlink message: %m");
+
+ link_ref(link); /* link may be NULL, link_ref() is OK with that */
+
+ nexthop_enter_removing(nexthop);
+ return 0;
+}
+
+static int nexthop_configure(NextHop *nexthop, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(nexthop);
+ assert(IN_SET(nexthop->family, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+ assert(req);
+
+ log_nexthop_debug(nexthop, "Configuring", link);
+
+ r = sd_rtnl_message_new_nexthop(link->manager->rtnl, &m, RTM_NEWNEXTHOP, nexthop->family, nexthop->protocol);
+ if (r < 0)
+ return r;
+
+ if (nexthop->id > 0) {
+ r = sd_netlink_message_append_u32(m, NHA_ID, nexthop->id);
+ if (r < 0)
+ return r;
+ }
+
+ if (!hashmap_isempty(nexthop->group)) {
+ _cleanup_free_ struct nexthop_grp *group = NULL;
+ struct nexthop_grp *p, *nhg;
+
+ group = new(struct nexthop_grp, hashmap_size(nexthop->group));
+ if (!group)
+ return log_oom();
+
+ p = group;
+ HASHMAP_FOREACH(nhg, nexthop->group)
+ *p++ = *nhg;
+
+ r = sd_netlink_message_append_data(m, NHA_GROUP, group, sizeof(struct nexthop_grp) * hashmap_size(nexthop->group));
+ if (r < 0)
+ return r;
+
+ } else if (nexthop->blackhole) {
+ r = sd_netlink_message_append_flag(m, NHA_BLACKHOLE);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_netlink_message_append_u32(m, NHA_OIF, link->ifindex);
+ if (r < 0)
+ return r;
+
+ if (in_addr_is_set(nexthop->family, &nexthop->gw)) {
+ r = netlink_message_append_in_addr_union(m, NHA_GATEWAY, nexthop->family, &nexthop->gw);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_nexthop_set_flags(m, nexthop->flags & RTNH_F_ONLINK);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static int static_nexthop_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, NextHop *nexthop) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set nexthop");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->static_nexthop_messages == 0) {
+ log_link_debug(link, "Nexthops set");
+ link->static_nexthops_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static bool nexthop_is_ready_to_configure(Link *link, const NextHop *nexthop) {
+ struct nexthop_grp *nhg;
+
+ assert(link);
+ assert(nexthop);
+
+ if (!link_is_ready_to_configure(link, false))
+ return false;
+
+ if (nexthop_owned_by_link(nexthop)) {
+ /* TODO: fdb nexthop does not require IFF_UP. The conditions below needs to be updated
+ * when fdb nexthop support is added. See rtm_to_nh_config() in net/ipv4/nexthop.c of
+ * kernel. */
+ if (link->set_flags_messages > 0)
+ return false;
+ if (!FLAGS_SET(link->flags, IFF_UP))
+ return false;
+ }
+
+ /* All group members must be configured first. */
+ HASHMAP_FOREACH(nhg, nexthop->group) {
+ NextHop *g;
+
+ if (manager_get_nexthop_by_id(link->manager, nhg->id, &g) < 0)
+ return false;
+
+ if (!nexthop_exists(g))
+ return false;
+ }
+
+ if (nexthop->id == 0) {
+ Request *req;
+
+ ORDERED_SET_FOREACH(req, link->manager->request_queue) {
+ if (req->type != REQUEST_TYPE_NEXTHOP)
+ continue;
+ if (((NextHop*) req->userdata)->id != 0)
+ return false; /* first configure nexthop with id. */
+ }
+ }
+
+ return gateway_is_ready(link, FLAGS_SET(nexthop->flags, RTNH_F_ONLINK), nexthop->family, &nexthop->gw);
+}
+
+static int nexthop_process_request(Request *req, Link *link, NextHop *nexthop) {
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(nexthop);
+
+ if (!nexthop_is_ready_to_configure(link, nexthop))
+ return 0;
+
+ r = nexthop_configure(nexthop, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure nexthop");
+
+ nexthop_enter_configuring(nexthop);
+ return 1;
+}
+
+static int link_request_nexthop(Link *link, NextHop *nexthop) {
+ NextHop *existing;
+ int r;
+
+ assert(link);
+ assert(nexthop);
+ assert(nexthop->source != NETWORK_CONFIG_SOURCE_FOREIGN);
+
+ if (nexthop_get(link->manager, link, nexthop, &existing) < 0) {
+ _cleanup_(nexthop_freep) NextHop *tmp = NULL;
+
+ r = nexthop_dup(nexthop, &tmp);
+ if (r < 0)
+ return r;
+
+ r = nexthop_acquire_id(link->manager, tmp);
+ if (r < 0)
+ return r;
+
+ r = nexthop_add(link->manager, link, tmp);
+ if (r < 0)
+ return r;
+
+ existing = TAKE_PTR(tmp);
+ } else
+ existing->source = nexthop->source;
+
+ log_nexthop_debug(existing, "Requesting", link);
+ r = link_queue_request_safe(link, REQUEST_TYPE_NEXTHOP,
+ existing, NULL,
+ nexthop_hash_func,
+ nexthop_compare_func,
+ nexthop_process_request,
+ &link->static_nexthop_messages,
+ static_nexthop_handler,
+ NULL);
+ if (r <= 0)
+ return r;
+
+ nexthop_enter_requesting(existing);
+ return 1;
+}
+
+int link_request_static_nexthops(Link *link, bool only_ipv4) {
+ NextHop *nh;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->static_nexthops_configured = false;
+
+ HASHMAP_FOREACH(nh, link->network->nexthops_by_section) {
+ if (only_ipv4 && nh->family != AF_INET)
+ continue;
+
+ r = link_request_nexthop(link, nh);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not request nexthop: %m");
+ }
+
+ if (link->static_nexthop_messages == 0) {
+ link->static_nexthops_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Requesting nexthops");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+static void manager_mark_nexthops(Manager *manager, bool foreign, const Link *except) {
+ NextHop *nexthop;
+ Link *link;
+
+ assert(manager);
+
+ /* First, mark all nexthops. */
+ SET_FOREACH(nexthop, manager->nexthops) {
+ /* do not touch nexthop created by the kernel */
+ if (nexthop->protocol == RTPROT_KERNEL)
+ continue;
+
+ /* When 'foreign' is true, mark only foreign nexthops, and vice versa. */
+ if (foreign != (nexthop->source == NETWORK_CONFIG_SOURCE_FOREIGN))
+ continue;
+
+ /* Ignore nexthops not assigned yet or already removed. */
+ if (!nexthop_exists(nexthop))
+ continue;
+
+ nexthop_mark(nexthop);
+ }
+
+ /* Then, unmark all nexthops requested by active links. */
+ HASHMAP_FOREACH(link, manager->links_by_index) {
+ if (link == except)
+ continue;
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ continue;
+
+ HASHMAP_FOREACH(nexthop, link->network->nexthops_by_section) {
+ NextHop *existing;
+
+ if (nexthop_get(manager, NULL, nexthop, &existing) >= 0)
+ nexthop_unmark(existing);
+ }
+ }
+}
+
+static int manager_drop_marked_nexthops(Manager *manager) {
+ NextHop *nexthop;
+ int k, r = 0;
+
+ assert(manager);
+
+ SET_FOREACH(nexthop, manager->nexthops) {
+ if (!nexthop_is_marked(nexthop))
+ continue;
+
+ k = nexthop_remove(nexthop);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+int link_drop_foreign_nexthops(Link *link) {
+ NextHop *nexthop;
+ int k, r = 0;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+
+ /* First, mark all nexthops. */
+ SET_FOREACH(nexthop, link->nexthops) {
+ /* do not touch nexthop created by the kernel */
+ if (nexthop->protocol == RTPROT_KERNEL)
+ continue;
+
+ /* Do not remove nexthops we configured. */
+ if (nexthop->source != NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ /* Ignore nexthops not assigned yet or already removed. */
+ if (!nexthop_exists(nexthop))
+ continue;
+
+ nexthop_mark(nexthop);
+ }
+
+ /* Then, unmark all nexthops requested by active links. */
+ HASHMAP_FOREACH(nexthop, link->network->nexthops_by_section) {
+ NextHop *existing;
+
+ if (nexthop_get(NULL, link, nexthop, &existing) >= 0)
+ nexthop_unmark(existing);
+ }
+
+ /* Finally, remove all marked rules. */
+ SET_FOREACH(nexthop, link->nexthops) {
+ if (!nexthop_is_marked(nexthop))
+ continue;
+
+ k = nexthop_remove(nexthop);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ manager_mark_nexthops(link->manager, /* foreign = */ true, NULL);
+
+ k = manager_drop_marked_nexthops(link->manager);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ return r;
+}
+
+int link_drop_managed_nexthops(Link *link) {
+ NextHop *nexthop;
+ int k, r = 0;
+
+ assert(link);
+ assert(link->manager);
+
+ SET_FOREACH(nexthop, link->nexthops) {
+ /* do not touch nexthop created by the kernel */
+ if (nexthop->protocol == RTPROT_KERNEL)
+ continue;
+
+ /* Do not touch addresses managed by kernel or other tools. */
+ if (nexthop->source == NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ /* Ignore nexthops not assigned yet or already removing. */
+ if (!nexthop_exists(nexthop))
+ continue;
+
+ k = nexthop_remove(nexthop);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ manager_mark_nexthops(link->manager, /* foreign = */ false, link);
+
+ k = manager_drop_marked_nexthops(link->manager);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ return r;
+}
+
+void link_foreignize_nexthops(Link *link) {
+ NextHop *nexthop;
+
+ assert(link);
+
+ SET_FOREACH(nexthop, link->nexthops)
+ nexthop->source = NETWORK_CONFIG_SOURCE_FOREIGN;
+
+ manager_mark_nexthops(link->manager, /* foreign = */ false, link);
+
+ SET_FOREACH(nexthop, link->manager->nexthops) {
+ if (!nexthop_is_marked(nexthop))
+ continue;
+
+ nexthop->source = NETWORK_CONFIG_SOURCE_FOREIGN;
+ }
+}
+
+int manager_rtnl_process_nexthop(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(nexthop_freep) NextHop *tmp = NULL;
+ _cleanup_free_ void *raw_group = NULL;
+ NextHop *nexthop = NULL;
+ size_t raw_group_size;
+ uint32_t ifindex;
+ uint16_t type;
+ Link *link = NULL;
+ int r;
+
+ assert(rtnl);
+ assert(message);
+ assert(m);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "rtnl: failed to receive rule message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get message type, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(type, RTM_NEWNEXTHOP, RTM_DELNEXTHOP)) {
+ log_warning("rtnl: received unexpected message type %u when processing nexthop, ignoring.", type);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, NHA_OIF, &ifindex);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get NHA_OIF attribute, ignoring: %m");
+ return 0;
+ } else if (r >= 0) {
+ if (ifindex <= 0) {
+ log_warning("rtnl: received nexthop message with invalid ifindex %"PRIu32", ignoring.", ifindex);
+ return 0;
+ }
+
+ r = link_get_by_index(m, ifindex, &link);
+ if (r < 0 || !link) {
+ if (!m->enumerating)
+ log_warning("rtnl: received nexthop message for link (%"PRIu32") we do not know about, ignoring", ifindex);
+ return 0;
+ }
+ }
+
+ r = nexthop_new(&tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_rtnl_message_get_family(message, &tmp->family);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: could not get nexthop family, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(tmp->family, AF_UNSPEC, AF_INET, AF_INET6)) {
+ log_link_debug(link, "rtnl: received nexthop message with invalid family %d, ignoring.", tmp->family);
+ return 0;
+ }
+
+ r = sd_rtnl_message_nexthop_get_protocol(message, &tmp->protocol);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: could not get nexthop protocol, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_nexthop_get_flags(message, &tmp->flags);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: could not get nexthop flags, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_data(message, NHA_GROUP, &raw_group_size, &raw_group);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: could not get NHA_GROUP attribute, ignoring: %m");
+ return 0;
+ } else if (r >= 0) {
+ struct nexthop_grp *group = raw_group;
+ size_t n_group;
+
+ if (raw_group_size == 0 || raw_group_size % sizeof(struct nexthop_grp) != 0) {
+ log_link_warning(link, "rtnl: received nexthop message with invalid nexthop group size, ignoring.");
+ return 0;
+ }
+
+ assert((uintptr_t) group % __alignof__(struct nexthop_grp) == 0);
+
+ n_group = raw_group_size / sizeof(struct nexthop_grp);
+ for (size_t i = 0; i < n_group; i++) {
+ _cleanup_free_ struct nexthop_grp *nhg = NULL;
+
+ if (group[i].id == 0) {
+ log_link_warning(link, "rtnl: received nexthop message with invalid ID in group, ignoring.");
+ return 0;
+ }
+ if (group[i].weight > 254) {
+ log_link_warning(link, "rtnl: received nexthop message with invalid weight in group, ignoring.");
+ return 0;
+ }
+
+ nhg = newdup(struct nexthop_grp, group + i, 1);
+ if (!nhg)
+ return log_oom();
+
+ r = hashmap_ensure_put(&tmp->group, NULL, UINT32_TO_PTR(nhg->id), nhg);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to store nexthop group, ignoring: %m");
+ return 0;
+ }
+ if (r > 0)
+ TAKE_PTR(nhg);
+ }
+ }
+
+ if (tmp->family != AF_UNSPEC) {
+ r = netlink_message_read_in_addr_union(message, NHA_GATEWAY, tmp->family, &tmp->gw);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: could not get NHA_GATEWAY attribute, ignoring: %m");
+ return 0;
+ }
+ }
+
+ r = sd_netlink_message_has_flag(message, NHA_BLACKHOLE);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: could not get NHA_BLACKHOLE attribute, ignoring: %m");
+ return 0;
+ }
+ tmp->blackhole = r;
+
+ r = sd_netlink_message_read_u32(message, NHA_ID, &tmp->id);
+ if (r == -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received nexthop message without NHA_ID attribute, ignoring: %m");
+ return 0;
+ } else if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: could not get NHA_ID attribute, ignoring: %m");
+ return 0;
+ } else if (tmp->id == 0) {
+ log_link_warning(link, "rtnl: received nexthop message with invalid nexthop ID, ignoring: %m");
+ return 0;
+ }
+
+ /* All blackhole or group nexthops are managed by Manager. Note that the linux kernel does not
+ * set NHA_OID attribute when NHA_BLACKHOLE or NHA_GROUP is set. Just for safety. */
+ if (!nexthop_owned_by_link(tmp))
+ link = NULL;
+
+ (void) nexthop_get(m, link, tmp, &nexthop);
+
+ switch (type) {
+ case RTM_NEWNEXTHOP:
+ if (nexthop) {
+ nexthop->flags = tmp->flags;
+ nexthop_enter_configured(nexthop);
+ log_nexthop_debug(tmp, "Received remembered", link);
+ } else {
+ nexthop_enter_configured(tmp);
+ log_nexthop_debug(tmp, "Remembering", link);
+
+ r = nexthop_add(m, link, tmp);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not remember foreign nexthop, ignoring: %m");
+ return 0;
+ }
+
+ TAKE_PTR(tmp);
+ }
+
+ break;
+ case RTM_DELNEXTHOP:
+ if (nexthop) {
+ nexthop_enter_removed(nexthop);
+ if (nexthop->state == 0) {
+ log_nexthop_debug(nexthop, "Forgetting", link);
+ nexthop_free(nexthop);
+ } else
+ log_nexthop_debug(nexthop, "Removed", link);
+ } else
+ log_nexthop_debug(tmp, "Kernel removed unknown", link);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int nexthop_section_verify(NextHop *nh) {
+ if (section_is_invalid(nh->section))
+ return -EINVAL;
+
+ if (!hashmap_isempty(nh->group)) {
+ if (in_addr_is_set(nh->family, &nh->gw))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: nexthop group cannot have gateway address. "
+ "Ignoring [NextHop] section from line %u.",
+ nh->section->filename, nh->section->line);
+
+ if (nh->family != AF_UNSPEC)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: nexthop group cannot have Family= setting. "
+ "Ignoring [NextHop] section from line %u.",
+ nh->section->filename, nh->section->line);
+
+ if (nh->blackhole && in_addr_is_set(nh->family, &nh->gw))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: nexthop group cannot be a blackhole. "
+ "Ignoring [NextHop] section from line %u.",
+ nh->section->filename, nh->section->line);
+ } else if (nh->family == AF_UNSPEC)
+ /* When neither Family=, Gateway=, nor Group= is specified, assume IPv4. */
+ nh->family = AF_INET;
+
+ if (nh->blackhole && in_addr_is_set(nh->family, &nh->gw))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: blackhole nexthop cannot have gateway address. "
+ "Ignoring [NextHop] section from line %u.",
+ nh->section->filename, nh->section->line);
+
+ if (nh->onlink < 0 && in_addr_is_set(nh->family, &nh->gw) &&
+ ordered_hashmap_isempty(nh->network->addresses_by_section)) {
+ /* If no address is configured, in most cases the gateway cannot be reachable.
+ * TODO: we may need to improve the condition above. */
+ log_warning("%s: Gateway= without static address configured. "
+ "Enabling OnLink= option.",
+ nh->section->filename);
+ nh->onlink = true;
+ }
+
+ if (nh->onlink >= 0)
+ SET_FLAG(nh->flags, RTNH_F_ONLINK, nh->onlink);
+
+ return 0;
+}
+
+void network_drop_invalid_nexthops(Network *network) {
+ NextHop *nh;
+
+ assert(network);
+
+ HASHMAP_FOREACH(nh, network->nexthops_by_section)
+ if (nexthop_section_verify(nh) < 0)
+ nexthop_free(nh);
+}
+
+int config_parse_nexthop_id(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL;
+ Network *network = userdata;
+ uint32_t id;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = nexthop_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ n->id = 0;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &id);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse nexthop id \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+ if (id == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid nexthop id \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ n->id = id;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_nexthop_gateway(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = nexthop_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ n->family = AF_UNSPEC;
+ n->gw = IN_ADDR_NULL;
+
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = in_addr_from_string_auto(rvalue, &n->family, &n->gw);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_nexthop_family(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL;
+ Network *network = userdata;
+ AddressFamily a;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = nexthop_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue) &&
+ !in_addr_is_set(n->family, &n->gw)) {
+ /* Accept an empty string only when Gateway= is null or not specified. */
+ n->family = AF_UNSPEC;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ a = nexthop_address_family_from_string(rvalue);
+ if (a < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ if (in_addr_is_set(n->family, &n->gw) &&
+ ((a == ADDRESS_FAMILY_IPV4 && n->family == AF_INET6) ||
+ (a == ADDRESS_FAMILY_IPV6 && n->family == AF_INET))) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified family '%s' conflicts with the family of the previously specified Gateway=, "
+ "ignoring assignment.", rvalue);
+ return 0;
+ }
+
+ switch (a) {
+ case ADDRESS_FAMILY_IPV4:
+ n->family = AF_INET;
+ break;
+ case ADDRESS_FAMILY_IPV6:
+ n->family = AF_INET6;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_nexthop_onlink(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = nexthop_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ n->onlink = -1;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ n->onlink = r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_nexthop_blackhole(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = nexthop_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ n->blackhole = r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_nexthop_group(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = nexthop_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ n->group = hashmap_free_free(n->group);
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ struct nexthop_grp *nhg = NULL;
+ _cleanup_free_ char *word = NULL;
+ uint32_t w;
+ char *sep;
+
+ r = extract_first_word(&p, &word, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+ if (r == 0)
+ break;
+
+ nhg = new0(struct nexthop_grp, 1);
+ if (!nhg)
+ return log_oom();
+
+ sep = strchr(word, ':');
+ if (sep) {
+ *sep++ = '\0';
+ r = safe_atou32(sep, &w);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse weight for nexthop group, ignoring assignment: %s:%s",
+ word, sep);
+ continue;
+ }
+ if (w == 0 || w > 256) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid weight for nexthop group, ignoring assignment: %s:%s",
+ word, sep);
+ continue;
+ }
+ /* See comments in config_parse_multipath_route(). */
+ nhg->weight = w - 1;
+ }
+
+ r = safe_atou32(word, &nhg->id);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse nexthop ID in %s=, ignoring assignment: %s%s%s",
+ lvalue, word, sep ? ":" : "", strempty(sep));
+ continue;
+ }
+ if (nhg->id == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Nexthop ID in %s= must be positive, ignoring assignment: %s%s%s",
+ lvalue, word, sep ? ":" : "", strempty(sep));
+ continue;
+ }
+
+ r = hashmap_ensure_put(&n->group, NULL, UINT32_TO_PTR(nhg->id), nhg);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r == -EEXIST) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Nexthop ID %"PRIu32" is specified multiple times in %s=, ignoring assignment: %s%s%s",
+ nhg->id, lvalue, word, sep ? ":" : "", strempty(sep));
+ continue;
+ }
+ assert(r > 0);
+ TAKE_PTR(nhg);
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
diff --git a/src/network/networkd-nexthop.h b/src/network/networkd-nexthop.h
new file mode 100644
index 0000000..6f2aa6f
--- /dev/null
+++ b/src/network/networkd-nexthop.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc.
+ */
+
+#pragma once
+
+#include <inttypes.h>
+
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+typedef struct Network Network;
+
+typedef struct NextHop {
+ Network *network;
+ Manager *manager;
+ Link *link;
+ ConfigSection *section;
+ NetworkConfigSource source;
+ NetworkConfigState state;
+
+ uint8_t protocol;
+
+ uint32_t id;
+ bool blackhole;
+ int family;
+ union in_addr_union gw;
+ uint8_t flags;
+ int onlink; /* Only used in conf parser and nexthop_section_verify(). */
+ Hashmap *group;
+} NextHop;
+
+NextHop *nexthop_free(NextHop *nexthop);
+
+void network_drop_invalid_nexthops(Network *network);
+
+int link_drop_managed_nexthops(Link *link);
+int link_drop_foreign_nexthops(Link *link);
+void link_foreignize_nexthops(Link *link);
+
+int link_request_static_nexthops(Link *link, bool only_ipv4);
+
+int manager_get_nexthop_by_id(Manager *manager, uint32_t id, NextHop **ret);
+int manager_rtnl_process_nexthop(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(NextHop, nexthop);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_id);
+CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_gateway);
+CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_family);
+CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_onlink);
+CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_blackhole);
+CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_group);
diff --git a/src/network/networkd-queue.c b/src/network/networkd-queue.c
new file mode 100644
index 0000000..c7a26c7
--- /dev/null
+++ b/src/network/networkd-queue.c
@@ -0,0 +1,308 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "netdev.h"
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-queue.h"
+#include "string-table.h"
+
+static Request *request_free(Request *req) {
+ if (!req)
+ return NULL;
+
+ /* To prevent from triggering assertions in the hash and compare functions, remove this request
+ * before freeing userdata below. */
+ if (req->manager)
+ ordered_set_remove(req->manager->request_queue, req);
+
+ if (req->free_func)
+ req->free_func(req->userdata);
+
+ if (req->counter)
+ (*req->counter)--;
+
+ link_unref(req->link); /* link may be NULL, but link_unref() can handle it gracefully. */
+
+ return mfree(req);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(Request, request, request_free);
+DEFINE_TRIVIAL_DESTRUCTOR(request_destroy_callback, Request, request_unref);
+
+void request_detach(Manager *manager, Request *req) {
+ assert(manager);
+
+ if (!req)
+ return;
+
+ req = ordered_set_remove(manager->request_queue, req);
+ if (!req)
+ return;
+
+ req->manager = NULL;
+ request_unref(req);
+}
+
+static void request_hash_func(const Request *req, struct siphash *state) {
+ assert(req);
+ assert(state);
+
+ siphash24_compress_boolean(req->link, state);
+ if (req->link)
+ siphash24_compress(&req->link->ifindex, sizeof(req->link->ifindex), state);
+
+ siphash24_compress(&req->type, sizeof(req->type), state);
+
+ siphash24_compress(&req->hash_func, sizeof(req->hash_func), state);
+ siphash24_compress(&req->compare_func, sizeof(req->compare_func), state);
+
+ if (req->hash_func)
+ req->hash_func(req->userdata, state);
+}
+
+static int request_compare_func(const struct Request *a, const struct Request *b) {
+ int r;
+
+ assert(a);
+ assert(b);
+
+ r = CMP(!!a->link, !!b->link);
+ if (r != 0)
+ return r;
+
+ if (a->link) {
+ r = CMP(a->link->ifindex, b->link->ifindex);
+ if (r != 0)
+ return r;
+ }
+
+ r = CMP(a->type, b->type);
+ if (r != 0)
+ return r;
+
+ r = CMP(PTR_TO_UINT64(a->hash_func), PTR_TO_UINT64(b->hash_func));
+ if (r != 0)
+ return r;
+
+ r = CMP(PTR_TO_UINT64(a->compare_func), PTR_TO_UINT64(b->compare_func));
+ if (r != 0)
+ return r;
+
+ if (a->compare_func)
+ return a->compare_func(a->userdata, b->userdata);
+
+ return 0;
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ request_hash_ops,
+ Request,
+ request_hash_func,
+ request_compare_func,
+ request_unref);
+
+static int request_new(
+ Manager *manager,
+ Link *link,
+ RequestType type,
+ void *userdata,
+ mfree_func_t free_func,
+ hash_func_t hash_func,
+ compare_func_t compare_func,
+ request_process_func_t process,
+ unsigned *counter,
+ request_netlink_handler_t netlink_handler,
+ Request **ret) {
+
+ _cleanup_(request_unrefp) Request *req = NULL;
+ Request *existing;
+ int r;
+
+ assert(manager);
+ assert(process);
+
+ req = new(Request, 1);
+ if (!req)
+ return -ENOMEM;
+
+ *req = (Request) {
+ .n_ref = 1,
+ .link = link_ref(link), /* link may be NULL, but link_ref() handles it gracefully. */
+ .type = type,
+ .userdata = userdata,
+ .free_func = free_func,
+ .hash_func = hash_func,
+ .compare_func = compare_func,
+ .process = process,
+ .netlink_handler = netlink_handler,
+ };
+
+ existing = ordered_set_get(manager->request_queue, req);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 0;
+ }
+
+ r = ordered_set_ensure_put(&manager->request_queue, &request_hash_ops, req);
+ if (r < 0)
+ return r;
+
+ req->manager = manager;
+ req->counter = counter;
+ if (req->counter)
+ (*req->counter)++;
+
+ if (ret)
+ *ret = req;
+
+ TAKE_PTR(req);
+ return 1;
+}
+
+int netdev_queue_request(
+ NetDev *netdev,
+ request_process_func_t process,
+ Request **ret) {
+
+ int r;
+
+ assert(netdev);
+
+ r = request_new(netdev->manager, NULL, REQUEST_TYPE_NETDEV_INDEPENDENT,
+ netdev, (mfree_func_t) netdev_unref,
+ trivial_hash_func, trivial_compare_func,
+ process, NULL, NULL, ret);
+ if (r <= 0)
+ return r;
+
+ netdev_ref(netdev);
+ return 1;
+}
+
+int link_queue_request_full(
+ Link *link,
+ RequestType type,
+ void *userdata,
+ mfree_func_t free_func,
+ hash_func_t hash_func,
+ compare_func_t compare_func,
+ request_process_func_t process,
+ unsigned *counter,
+ request_netlink_handler_t netlink_handler,
+ Request **ret) {
+
+ assert(link);
+
+ return request_new(link->manager, link, type,
+ userdata, free_func, hash_func, compare_func,
+ process, counter, netlink_handler, ret);
+}
+
+int manager_process_requests(sd_event_source *s, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+ int r;
+
+ for (;;) {
+ bool processed = false;
+ Request *req;
+
+ ORDERED_SET_FOREACH(req, manager->request_queue) {
+ _unused_ _cleanup_(request_unrefp) Request *ref = request_ref(req);
+ _cleanup_(link_unrefp) Link *link = link_ref(req->link);
+
+ assert(req->process);
+
+ r = req->process(req, link, req->userdata);
+ if (r == 0)
+ continue;
+
+ processed = true;
+ request_detach(manager, req);
+
+ if (r < 0 && link) {
+ link_enter_failed(link);
+ /* link_enter_failed() may remove multiple requests,
+ * hence we need to exit from the loop. */
+ break;
+ }
+ }
+
+ /* When at least one request is processed, then another request may be ready now. */
+ if (!processed)
+ break;
+ }
+
+ return 0;
+}
+
+static int request_netlink_handler(sd_netlink *nl, sd_netlink_message *m, Request *req) {
+ assert(req);
+
+ if (req->counter) {
+ assert(*req->counter > 0);
+ (*req->counter)--;
+ req->counter = NULL; /* To prevent double decrement on free. */
+ }
+
+ if (req->link && IN_SET(req->link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ if (req->netlink_handler)
+ return req->netlink_handler(nl, m, req, req->link, req->userdata);
+
+ return 0;
+}
+
+int request_call_netlink_async(sd_netlink *nl, sd_netlink_message *m, Request *req) {
+ int r;
+
+ assert(nl);
+ assert(m);
+ assert(req);
+
+ r = netlink_call_async(nl, NULL, m, request_netlink_handler, request_destroy_callback, req);
+ if (r < 0)
+ return r;
+
+ request_ref(req);
+ return 0;
+}
+
+static const char *const request_type_table[_REQUEST_TYPE_MAX] = {
+ [REQUEST_TYPE_ACTIVATE_LINK] = "activate link",
+ [REQUEST_TYPE_ADDRESS] = "address",
+ [REQUEST_TYPE_ADDRESS_LABEL] = "address label",
+ [REQUEST_TYPE_BRIDGE_FDB] = "bridge FDB",
+ [REQUEST_TYPE_BRIDGE_MDB] = "bridge MDB",
+ [REQUEST_TYPE_DHCP_SERVER] = "DHCP server",
+ [REQUEST_TYPE_DHCP4_CLIENT] = "DHCPv4 client",
+ [REQUEST_TYPE_DHCP6_CLIENT] = "DHCPv6 client",
+ [REQUEST_TYPE_IPV6_PROXY_NDP] = "IPv6 proxy NDP",
+ [REQUEST_TYPE_NDISC] = "NDisc",
+ [REQUEST_TYPE_NEIGHBOR] = "neighbor",
+ [REQUEST_TYPE_NETDEV_INDEPENDENT] = "independent netdev",
+ [REQUEST_TYPE_NETDEV_STACKED] = "stacked netdev",
+ [REQUEST_TYPE_NEXTHOP] = "nexthop",
+ [REQUEST_TYPE_RADV] = "RADV",
+ [REQUEST_TYPE_ROUTE] = "route",
+ [REQUEST_TYPE_ROUTING_POLICY_RULE] = "routing policy rule",
+ [REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE] = "IPv6LL address generation mode",
+ [REQUEST_TYPE_SET_LINK_BOND] = "bond configurations",
+ [REQUEST_TYPE_SET_LINK_BRIDGE] = "bridge configurations",
+ [REQUEST_TYPE_SET_LINK_BRIDGE_VLAN] = "bridge VLAN configurations",
+ [REQUEST_TYPE_SET_LINK_CAN] = "CAN interface configurations",
+ [REQUEST_TYPE_SET_LINK_FLAGS] = "link flags",
+ [REQUEST_TYPE_SET_LINK_GROUP] = "interface group",
+ [REQUEST_TYPE_SET_LINK_IPOIB] = "IPoIB configurations",
+ [REQUEST_TYPE_SET_LINK_MAC] = "MAC address",
+ [REQUEST_TYPE_SET_LINK_MASTER] = "master interface",
+ [REQUEST_TYPE_SET_LINK_MTU] = "MTU",
+ [REQUEST_TYPE_SRIOV] = "SR-IOV",
+ [REQUEST_TYPE_TC_QDISC] = "QDisc",
+ [REQUEST_TYPE_TC_CLASS] = "TClass",
+ [REQUEST_TYPE_UP_DOWN] = "bring link up or down",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_TO_STRING(request_type, RequestType);
diff --git a/src/network/networkd-queue.h b/src/network/networkd-queue.h
new file mode 100644
index 0000000..6db0005
--- /dev/null
+++ b/src/network/networkd-queue.h
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-event.h"
+#include "sd-netlink.h"
+
+#include "alloc-util.h"
+#include "hash-funcs.h"
+
+typedef struct Link Link;
+typedef struct NetDev NetDev;
+typedef struct Manager Manager;
+typedef struct Request Request;
+
+typedef int (*request_process_func_t)(Request *req, Link *link, void *userdata);
+typedef int (*request_netlink_handler_t)(sd_netlink *nl, sd_netlink_message *m, Request *req, Link *link, void *userdata);
+
+typedef enum RequestType {
+ REQUEST_TYPE_ACTIVATE_LINK,
+ REQUEST_TYPE_ADDRESS,
+ REQUEST_TYPE_ADDRESS_LABEL,
+ REQUEST_TYPE_BRIDGE_FDB,
+ REQUEST_TYPE_BRIDGE_MDB,
+ REQUEST_TYPE_DHCP_SERVER,
+ REQUEST_TYPE_DHCP4_CLIENT,
+ REQUEST_TYPE_DHCP6_CLIENT,
+ REQUEST_TYPE_IPV6_PROXY_NDP,
+ REQUEST_TYPE_NDISC,
+ REQUEST_TYPE_NEIGHBOR,
+ REQUEST_TYPE_NETDEV_INDEPENDENT,
+ REQUEST_TYPE_NETDEV_STACKED,
+ REQUEST_TYPE_NEXTHOP,
+ REQUEST_TYPE_RADV,
+ REQUEST_TYPE_ROUTE,
+ REQUEST_TYPE_ROUTING_POLICY_RULE,
+ REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE, /* Setting IPv6LL address generation mode. */
+ REQUEST_TYPE_SET_LINK_BOND, /* Setting bond configs. */
+ REQUEST_TYPE_SET_LINK_BRIDGE, /* Setting bridge configs. */
+ REQUEST_TYPE_SET_LINK_BRIDGE_VLAN, /* Setting bridge VLAN configs. */
+ REQUEST_TYPE_SET_LINK_CAN, /* Setting CAN interface configs. */
+ REQUEST_TYPE_SET_LINK_FLAGS, /* Setting IFF_NOARP or friends. */
+ REQUEST_TYPE_SET_LINK_GROUP, /* Setting interface group. */
+ REQUEST_TYPE_SET_LINK_IPOIB, /* Setting IPoIB configs. */
+ REQUEST_TYPE_SET_LINK_MAC, /* Setting MAC address. */
+ REQUEST_TYPE_SET_LINK_MASTER, /* Setting IFLA_MASTER. */
+ REQUEST_TYPE_SET_LINK_MTU, /* Setting MTU. */
+ REQUEST_TYPE_SRIOV,
+ REQUEST_TYPE_TC_CLASS,
+ REQUEST_TYPE_TC_QDISC,
+ REQUEST_TYPE_UP_DOWN,
+ _REQUEST_TYPE_MAX,
+ _REQUEST_TYPE_INVALID = -EINVAL,
+} RequestType;
+
+struct Request {
+ unsigned n_ref;
+
+ Manager *manager; /* must be non-NULL */
+ Link *link; /* can be NULL */
+
+ RequestType type;
+
+ /* Target object, e.g. Address, Route, NetDev, and so on. */
+ void *userdata;
+ /* freeing userdata when the request is completed or failed. */
+ mfree_func_t free_func;
+
+ /* hash and compare functions for userdata, used for dedup requests. */
+ hash_func_t hash_func;
+ compare_func_t compare_func;
+
+ /* Checks the request dependencies, and then processes this request, e.g. call address_configure().
+ * Return 1 when processed, 0 when its dependencies not resolved, and negative errno on failure. */
+ request_process_func_t process;
+
+ /* incremented when requested, decremented when request is completed or failed. */
+ unsigned *counter;
+ /* called in netlink handler, the 'counter' is decremented before this is called.
+ * If this is specified, then the 'process' function must increment the reference of this
+ * request, and pass this request to the netlink_call_async(), and set the destroy function
+ * to the slot. */
+ request_netlink_handler_t netlink_handler;
+};
+
+Request *request_ref(Request *req);
+Request *request_unref(Request *req);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Request*, request_unref);
+
+void request_detach(Manager *manager, Request *req);
+
+int netdev_queue_request(
+ NetDev *netdev,
+ request_process_func_t process,
+ Request **ret);
+
+int link_queue_request_full(
+ Link *link,
+ RequestType type,
+ void *userdata,
+ mfree_func_t free_func,
+ hash_func_t hash_func,
+ compare_func_t compare_func,
+ request_process_func_t process,
+ unsigned *counter,
+ request_netlink_handler_t netlink_handler,
+ Request **ret);
+
+static inline int link_queue_request(
+ Link *link,
+ RequestType type,
+ request_process_func_t process,
+ Request **ret) {
+
+ return link_queue_request_full(link, type, NULL, NULL, NULL, NULL,
+ process, NULL, NULL, ret);
+}
+
+#define link_queue_request_safe(link, type, userdata, free_func, hash_func, compare_func, process, counter, netlink_handler, ret) \
+ ({ \
+ typeof(userdata) (*_f)(typeof(userdata)) = (free_func); \
+ void (*_h)(const typeof(*userdata)*, struct siphash*) = (hash_func); \
+ int (*_c)(const typeof(*userdata)*, const typeof(*userdata)*) = (compare_func); \
+ int (*_p)(Request*, Link*, typeof(userdata)) = (process); \
+ int (*_n)(sd_netlink*, sd_netlink_message*, Request*, Link*, typeof(userdata)) = (netlink_handler); \
+ \
+ link_queue_request_full(link, type, userdata, \
+ (mfree_func_t) _f, \
+ (hash_func_t) _h, \
+ (compare_func_t) _c, \
+ (request_process_func_t) _p, \
+ counter, \
+ (request_netlink_handler_t) _n, \
+ ret); \
+ })
+
+int manager_process_requests(sd_event_source *s, void *userdata);
+int request_call_netlink_async(sd_netlink *nl, sd_netlink_message *m, Request *req);
+
+const char* request_type_to_string(RequestType t) _const_;
diff --git a/src/network/networkd-radv.c b/src/network/networkd-radv.c
new file mode 100644
index 0000000..db1ecb4
--- /dev/null
+++ b/src/network/networkd-radv.c
@@ -0,0 +1,1339 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2017 Intel Corporation. All rights reserved.
+***/
+
+#include <netinet/icmp6.h>
+#include <arpa/inet.h>
+
+#include "dns-domain.h"
+#include "networkd-address-generation.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-prefix-delegation.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "networkd-radv.h"
+#include "networkd-route-util.h"
+#include "parse-util.h"
+#include "radv-internal.h"
+#include "string-util.h"
+#include "string-table.h"
+#include "strv.h"
+
+void network_adjust_radv(Network *network) {
+ assert(network);
+
+ /* After this function is called, network->router_prefix_delegation can be treated as a boolean. */
+
+ if (network->dhcp_pd < 0)
+ /* For backward compatibility. */
+ network->dhcp_pd = FLAGS_SET(network->router_prefix_delegation, RADV_PREFIX_DELEGATION_DHCP6);
+
+ if (!FLAGS_SET(network->link_local, ADDRESS_FAMILY_IPV6)) {
+ if (network->router_prefix_delegation != RADV_PREFIX_DELEGATION_NONE)
+ log_warning("%s: IPv6PrefixDelegation= is enabled but IPv6 link-local addressing is disabled. "
+ "Disabling IPv6PrefixDelegation=.", network->filename);
+
+ network->router_prefix_delegation = RADV_PREFIX_DELEGATION_NONE;
+ }
+
+ if (network->router_prefix_delegation == RADV_PREFIX_DELEGATION_NONE) {
+ network->n_router_dns = 0;
+ network->router_dns = mfree(network->router_dns);
+ network->router_search_domains = ordered_set_free(network->router_search_domains);
+ }
+
+ if (!FLAGS_SET(network->router_prefix_delegation, RADV_PREFIX_DELEGATION_STATIC)) {
+ network->prefixes_by_section = hashmap_free_with_destructor(network->prefixes_by_section, prefix_free);
+ network->route_prefixes_by_section = hashmap_free_with_destructor(network->route_prefixes_by_section, route_prefix_free);
+ }
+}
+
+bool link_radv_enabled(Link *link) {
+ assert(link);
+
+ if (!link_may_have_ipv6ll(link, /* check_multicast = */ true))
+ return false;
+
+ if (link->hw_addr.length != ETH_ALEN)
+ return false;
+
+ return link->network->router_prefix_delegation;
+}
+
+Prefix *prefix_free(Prefix *prefix) {
+ if (!prefix)
+ return NULL;
+
+ if (prefix->network) {
+ assert(prefix->section);
+ hashmap_remove(prefix->network->prefixes_by_section, prefix->section);
+ }
+
+ config_section_free(prefix->section);
+ set_free(prefix->tokens);
+
+ return mfree(prefix);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(Prefix, prefix_free);
+
+static int prefix_new_static(Network *network, const char *filename, unsigned section_line, Prefix **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(prefix_freep) Prefix *prefix = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ prefix = hashmap_get(network->prefixes_by_section, n);
+ if (prefix) {
+ *ret = TAKE_PTR(prefix);
+ return 0;
+ }
+
+ prefix = new(Prefix, 1);
+ if (!prefix)
+ return -ENOMEM;
+
+ *prefix = (Prefix) {
+ .network = network,
+ .section = TAKE_PTR(n),
+
+ .preferred_lifetime = RADV_DEFAULT_PREFERRED_LIFETIME_USEC,
+ .valid_lifetime = RADV_DEFAULT_VALID_LIFETIME_USEC,
+ .onlink = true,
+ .address_auto_configuration = true,
+ };
+
+ r = hashmap_ensure_put(&network->prefixes_by_section, &config_section_hash_ops, prefix->section, prefix);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(prefix);
+ return 0;
+}
+
+RoutePrefix *route_prefix_free(RoutePrefix *prefix) {
+ if (!prefix)
+ return NULL;
+
+ if (prefix->network) {
+ assert(prefix->section);
+ hashmap_remove(prefix->network->route_prefixes_by_section, prefix->section);
+ }
+
+ config_section_free(prefix->section);
+
+ return mfree(prefix);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(RoutePrefix, route_prefix_free);
+
+static int route_prefix_new_static(Network *network, const char *filename, unsigned section_line, RoutePrefix **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(route_prefix_freep) RoutePrefix *prefix = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ prefix = hashmap_get(network->route_prefixes_by_section, n);
+ if (prefix) {
+ *ret = TAKE_PTR(prefix);
+ return 0;
+ }
+
+ prefix = new(RoutePrefix, 1);
+ if (!prefix)
+ return -ENOMEM;
+
+ *prefix = (RoutePrefix) {
+ .network = network,
+ .section = TAKE_PTR(n),
+
+ .lifetime = RADV_DEFAULT_VALID_LIFETIME_USEC,
+ };
+
+ r = hashmap_ensure_put(&network->route_prefixes_by_section, &config_section_hash_ops, prefix->section, prefix);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(prefix);
+ return 0;
+}
+
+int link_request_radv_addresses(Link *link) {
+ Prefix *p;
+ int r;
+
+ assert(link);
+
+ if (!link_radv_enabled(link))
+ return 0;
+
+ HASHMAP_FOREACH(p, link->network->prefixes_by_section) {
+ _cleanup_set_free_ Set *addresses = NULL;
+ struct in6_addr *a;
+
+ if (!p->assign)
+ continue;
+
+ /* radv_generate_addresses() below requires the prefix length <= 64. */
+ if (p->prefixlen > 64)
+ continue;
+
+ r = radv_generate_addresses(link, p->tokens, &p->prefix, p->prefixlen, &addresses);
+ if (r < 0)
+ return r;
+
+ SET_FOREACH(a, addresses) {
+ _cleanup_(address_freep) Address *address = NULL;
+
+ r = address_new(&address);
+ if (r < 0)
+ return -ENOMEM;
+
+ address->source = NETWORK_CONFIG_SOURCE_STATIC;
+ address->family = AF_INET6;
+ address->in_addr.in6 = *a;
+ address->prefixlen = p->prefixlen;
+ address->route_metric = p->route_metric;
+
+ r = link_request_static_address(link, TAKE_PTR(address), true);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+static uint32_t usec_to_lifetime(usec_t usec) {
+ uint64_t t;
+
+ if (usec == USEC_INFINITY)
+ return UINT32_MAX;
+
+ t = DIV_ROUND_UP(usec, USEC_PER_SEC);
+ if (t >= UINT32_MAX)
+ return UINT32_MAX;
+
+ return (uint32_t) t;
+}
+
+static int radv_set_prefix(Link *link, Prefix *prefix) {
+ _cleanup_(sd_radv_prefix_unrefp) sd_radv_prefix *p = NULL;
+ int r;
+
+ assert(link);
+ assert(link->radv);
+ assert(prefix);
+
+ r = sd_radv_prefix_new(&p);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_prefix(p, &prefix->prefix, prefix->prefixlen);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_preferred_lifetime(p, prefix->preferred_lifetime, USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_valid_lifetime(p, prefix->valid_lifetime, USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_onlink(p, prefix->onlink);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_address_autoconfiguration(p, prefix->address_auto_configuration);
+ if (r < 0)
+ return r;
+
+ return sd_radv_add_prefix(link->radv, p);
+}
+
+static int radv_set_route_prefix(Link *link, RoutePrefix *prefix) {
+ _cleanup_(sd_radv_route_prefix_unrefp) sd_radv_route_prefix *p = NULL;
+ int r;
+
+ assert(link);
+ assert(link->radv);
+ assert(prefix);
+
+ r = sd_radv_route_prefix_new(&p);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_route_prefix_set_prefix(p, &prefix->prefix, prefix->prefixlen);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_route_prefix_set_lifetime(p, prefix->lifetime, USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ return sd_radv_add_route_prefix(link->radv, p);
+}
+
+static int network_get_ipv6_dns(Network *network, struct in6_addr **ret_addresses, size_t *ret_size) {
+ _cleanup_free_ struct in6_addr *addresses = NULL;
+ size_t n_addresses = 0;
+
+ assert(network);
+ assert(ret_addresses);
+ assert(ret_size);
+
+ for (size_t i = 0; i < network->n_dns; i++) {
+ union in_addr_union *addr;
+
+ if (network->dns[i]->family != AF_INET6)
+ continue;
+
+ addr = &network->dns[i]->address;
+
+ if (in_addr_is_null(AF_INET6, addr) ||
+ in_addr_is_link_local(AF_INET6, addr) ||
+ in_addr_is_localhost(AF_INET6, addr))
+ continue;
+
+ if (!GREEDY_REALLOC(addresses, n_addresses + 1))
+ return -ENOMEM;
+
+ addresses[n_addresses++] = addr->in6;
+ }
+
+ *ret_addresses = TAKE_PTR(addresses);
+ *ret_size = n_addresses;
+
+ return n_addresses;
+}
+
+static int radv_set_dns(Link *link, Link *uplink) {
+ _cleanup_free_ struct in6_addr *dns = NULL;
+ size_t n_dns;
+ int r;
+
+ if (!link->network->router_emit_dns)
+ return 0;
+
+ if (link->network->router_dns) {
+ struct in6_addr *p;
+
+ dns = new(struct in6_addr, link->network->n_router_dns);
+ if (!dns)
+ return -ENOMEM;
+
+ p = dns;
+ for (size_t i = 0; i < link->network->n_router_dns; i++)
+ if (in6_addr_is_null(&link->network->router_dns[i])) {
+ if (in6_addr_is_set(&link->ipv6ll_address))
+ *(p++) = link->ipv6ll_address;
+ } else
+ *(p++) = link->network->router_dns[i];
+
+ n_dns = p - dns;
+
+ goto set_dns;
+ }
+
+ r = network_get_ipv6_dns(link->network, &dns, &n_dns);
+ if (r > 0)
+ goto set_dns;
+
+ if (uplink) {
+ assert(uplink->network);
+
+ r = network_get_ipv6_dns(uplink->network, &dns, &n_dns);
+ if (r > 0)
+ goto set_dns;
+ }
+
+ return 0;
+
+set_dns:
+ return sd_radv_set_rdnss(link->radv,
+ usec_to_lifetime(link->network->router_dns_lifetime_usec),
+ dns, n_dns);
+}
+
+static int radv_set_domains(Link *link, Link *uplink) {
+ _cleanup_free_ char **s = NULL; /* just free() because the strings are owned by the set */
+ OrderedSet *search_domains;
+
+ if (!link->network->router_emit_domains)
+ return 0;
+
+ search_domains = link->network->router_search_domains;
+
+ if (search_domains)
+ goto set_domains;
+
+ search_domains = link->network->search_domains;
+ if (search_domains)
+ goto set_domains;
+
+ if (uplink) {
+ assert(uplink->network);
+
+ search_domains = uplink->network->search_domains;
+ if (search_domains)
+ goto set_domains;
+ }
+
+ return 0;
+
+set_domains:
+ s = ordered_set_get_strv(search_domains);
+ if (!s)
+ return log_oom();
+
+ return sd_radv_set_dnssl(link->radv,
+ usec_to_lifetime(link->network->router_dns_lifetime_usec),
+ s);
+
+}
+
+static int radv_find_uplink(Link *link, Link **ret) {
+ int r;
+
+ assert(link);
+
+ if (link->network->router_uplink_name)
+ return link_get_by_name(link->manager, link->network->router_uplink_name, ret);
+
+ if (link->network->router_uplink_index > 0)
+ return link_get_by_index(link->manager, link->network->router_uplink_index, ret);
+
+ if (link->network->router_uplink_index == UPLINK_INDEX_AUTO) {
+ if (link_dhcp_pd_is_enabled(link))
+ r = dhcp_pd_find_uplink(link, ret); /* When DHCP-PD is enabled, use its uplink. */
+ else
+ r = manager_find_uplink(link->manager, AF_INET6, link, ret);
+ if (r < 0)
+ /* It is not necessary to propagate error in automatic selection. */
+ *ret = NULL;
+ return 0;
+ }
+
+ *ret = NULL;
+ return 0;
+}
+
+static int radv_configure(Link *link) {
+ Link *uplink = NULL;
+ RoutePrefix *q;
+ Prefix *p;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->radv)
+ return -EBUSY;
+
+ r = sd_radv_new(&link->radv);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_attach_event(link->radv, link->manager->event, 0);
+ if (r < 0)
+ return r;
+
+ if (link->hw_addr.length == ETH_ALEN) {
+ r = sd_radv_set_mac(link->radv, &link->hw_addr.ether);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_radv_set_ifindex(link->radv, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_set_managed_information(link->radv, link->network->router_managed);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_set_other_information(link->radv, link->network->router_other_information);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_set_router_lifetime(link->radv, link->network->router_lifetime_usec);
+ if (r < 0)
+ return r;
+
+ if (link->network->router_lifetime_usec > 0) {
+ r = sd_radv_set_preference(link->radv, link->network->router_preference);
+ if (r < 0)
+ return r;
+ }
+
+ HASHMAP_FOREACH(p, link->network->prefixes_by_section) {
+ r = radv_set_prefix(link, p);
+ if (r < 0 && r != -EEXIST)
+ return r;
+ }
+
+ HASHMAP_FOREACH(q, link->network->route_prefixes_by_section) {
+ r = radv_set_route_prefix(link, q);
+ if (r < 0 && r != -EEXIST)
+ return r;
+ }
+
+ (void) radv_find_uplink(link, &uplink);
+
+ r = radv_set_dns(link, uplink);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not set RA DNS: %m");
+
+ r = radv_set_domains(link, uplink);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not set RA Domains: %m");
+
+ return 0;
+}
+
+int radv_update_mac(Link *link) {
+ bool restart;
+ int r;
+
+ assert(link);
+
+ if (!link->radv)
+ return 0;
+
+ if (link->hw_addr.length != ETH_ALEN)
+ return 0;
+
+ restart = sd_radv_is_running(link->radv);
+
+ r = sd_radv_stop(link->radv);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_set_mac(link->radv, &link->hw_addr.ether);
+ if (r < 0)
+ return r;
+
+ if (restart) {
+ r = sd_radv_start(link->radv);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int radv_is_ready_to_configure(Link *link) {
+ bool needs_uplink = false;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false))
+ return false;
+
+ if (in6_addr_is_null(&link->ipv6ll_address))
+ return false;
+
+ if (link->hw_addr.length != ETH_ALEN || hw_addr_is_null(&link->hw_addr))
+ return false;
+
+ if (link->network->router_emit_dns && !link->network->router_dns) {
+ _cleanup_free_ struct in6_addr *dns = NULL;
+ size_t n_dns;
+
+ r = network_get_ipv6_dns(link->network, &dns, &n_dns);
+ if (r < 0)
+ return r;
+
+ needs_uplink = r == 0;
+ }
+
+ if (link->network->router_emit_domains &&
+ !link->network->router_search_domains &&
+ !link->network->search_domains)
+ needs_uplink = true;
+
+ if (needs_uplink) {
+ Link *uplink = NULL;
+
+ if (radv_find_uplink(link, &uplink) < 0)
+ return false;
+
+ if (uplink && !uplink->network)
+ return false;
+ }
+
+ return true;
+}
+
+static int radv_process_request(Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(link);
+
+ r = radv_is_ready_to_configure(link);
+ if (r <= 0)
+ return r;
+
+ r = radv_configure(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure IPv6 Router Advertisement engine: %m");
+
+ if (link_has_carrier(link)) {
+ r = radv_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start IPv6 Router Advertisement engine: %m");
+ }
+
+ log_link_debug(link, "IPv6 Router Advertisement engine is configured%s.",
+ link_has_carrier(link) ? " and started" : "");
+ return 1;
+}
+
+int link_request_radv(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link_radv_enabled(link))
+ return 0;
+
+ if (link->radv)
+ return 0;
+
+ r = link_queue_request(link, REQUEST_TYPE_RADV, radv_process_request, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request configuring of the IPv6 Router Advertisement engine: %m");
+
+ log_link_debug(link, "Requested configuring of the IPv6 Router Advertisement engine.");
+ return 0;
+}
+
+int radv_start(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!link->radv)
+ return 0;
+
+ if (!link_has_carrier(link))
+ return 0;
+
+ if (in6_addr_is_null(&link->ipv6ll_address))
+ return 0;
+
+ if (sd_radv_is_running(link->radv))
+ return 0;
+
+ if (link->network->dhcp_pd_announce) {
+ r = dhcp_request_prefix_delegation(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to request DHCP delegated subnet prefix: %m");
+ }
+
+ log_link_debug(link, "Starting IPv6 Router Advertisements");
+ return sd_radv_start(link->radv);
+}
+
+int radv_add_prefix(
+ Link *link,
+ const struct in6_addr *prefix,
+ uint8_t prefix_len,
+ usec_t lifetime_preferred_usec,
+ usec_t lifetime_valid_usec) {
+
+ _cleanup_(sd_radv_prefix_unrefp) sd_radv_prefix *p = NULL;
+ int r;
+
+ assert(link);
+
+ if (!link->radv)
+ return 0;
+
+ r = sd_radv_prefix_new(&p);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_prefix(p, prefix, prefix_len);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_preferred_lifetime(p, RADV_DEFAULT_PREFERRED_LIFETIME_USEC, lifetime_preferred_usec);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_valid_lifetime(p, RADV_DEFAULT_VALID_LIFETIME_USEC, lifetime_valid_usec);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_add_prefix(link->radv, p);
+ if (r < 0 && r != -EEXIST)
+ return r;
+
+ return 0;
+}
+
+static int prefix_section_verify(Prefix *p) {
+ assert(p);
+
+ if (section_is_invalid(p->section))
+ return -EINVAL;
+
+ if (in6_addr_is_null(&p->prefix))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: [IPv6Prefix] section without Prefix= field configured, "
+ "or specified prefix is the null address. "
+ "Ignoring [IPv6Prefix] section from line %u.",
+ p->section->filename, p->section->line);
+
+ if (p->prefixlen < 3 || p->prefixlen > 128)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Invalid prefix length %u is specified in [IPv6Prefix] section. "
+ "Valid range is 3…128. Ignoring [IPv6Prefix] section from line %u.",
+ p->section->filename, p->prefixlen, p->section->line);
+
+ if (p->prefixlen > 64) {
+ log_info("%s:%u: Unusual prefix length %u (> 64) is specified in [IPv6Prefix] section from line %s%s.",
+ p->section->filename, p->section->line,
+ p->prefixlen,
+ p->assign ? ", refusing to assign an address in " : "",
+ p->assign ? IN6_ADDR_PREFIX_TO_STRING(&p->prefix, p->prefixlen) : "");
+
+ p->assign = false;
+ }
+
+ if (p->valid_lifetime == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: The valid lifetime of prefix cannot be zero. "
+ "Ignoring [IPv6Prefix] section from line %u.",
+ p->section->filename, p->section->line);
+
+ if (p->preferred_lifetime > p->valid_lifetime)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: The preferred lifetime %s is longer than the valid lifetime %s. "
+ "Ignoring [IPv6Prefix] section from line %u.",
+ p->section->filename,
+ FORMAT_TIMESPAN(p->preferred_lifetime, USEC_PER_SEC),
+ FORMAT_TIMESPAN(p->valid_lifetime, USEC_PER_SEC),
+ p->section->line);
+
+ return 0;
+}
+
+void network_drop_invalid_prefixes(Network *network) {
+ Prefix *p;
+
+ assert(network);
+
+ HASHMAP_FOREACH(p, network->prefixes_by_section)
+ if (prefix_section_verify(p) < 0)
+ prefix_free(p);
+}
+
+static int route_prefix_section_verify(RoutePrefix *p) {
+ if (section_is_invalid(p->section))
+ return -EINVAL;
+
+ if (p->prefixlen > 128)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Invalid prefix length %u is specified in [IPv6RoutePrefix] section. "
+ "Valid range is 0…128. Ignoring [IPv6RoutePrefix] section from line %u.",
+ p->section->filename, p->prefixlen, p->section->line);
+
+ if (p->lifetime == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: The lifetime of route cannot be zero. "
+ "Ignoring [IPv6RoutePrefix] section from line %u.",
+ p->section->filename, p->section->line);
+
+ return 0;
+}
+
+void network_drop_invalid_route_prefixes(Network *network) {
+ RoutePrefix *p;
+
+ assert(network);
+
+ HASHMAP_FOREACH(p, network->route_prefixes_by_section)
+ if (route_prefix_section_verify(p) < 0)
+ route_prefix_free(p);
+}
+
+int config_parse_prefix(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ union in_addr_union a;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = prefix_new_static(network, filename, section_line, &p);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_prefix_from_string(rvalue, AF_INET6, &a, &p->prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Prefix is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ (void) in6_addr_mask(&a.in6, p->prefixlen);
+ p->prefix = a.in6;
+
+ TAKE_PTR(p);
+ return 0;
+}
+
+int config_parse_prefix_boolean(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = prefix_new_static(network, filename, section_line, &p);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "OnLink"))
+ p->onlink = r;
+ else if (streq(lvalue, "AddressAutoconfiguration"))
+ p->address_auto_configuration = r;
+ else if (streq(lvalue, "Assign"))
+ p->assign = r;
+ else
+ assert_not_reached();
+
+ TAKE_PTR(p);
+ return 0;
+}
+
+int config_parse_prefix_lifetime(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ usec_t usec;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = prefix_new_static(network, filename, section_line, &p);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_sec(rvalue, &usec);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Lifetime is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (usec != USEC_INFINITY && DIV_ROUND_UP(usec, USEC_PER_SEC) >= UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Lifetime is too long, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "PreferredLifetimeSec"))
+ p->preferred_lifetime = usec;
+ else if (streq(lvalue, "ValidLifetimeSec"))
+ p->valid_lifetime = usec;
+ else
+ assert_not_reached();
+
+ TAKE_PTR(p);
+ return 0;
+}
+
+int config_parse_prefix_metric(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = prefix_new_static(network, filename, section_line, &p);
+ if (r < 0)
+ return log_oom();
+
+ r = safe_atou32(rvalue, &p->route_metric);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(p);
+ return 0;
+}
+
+int config_parse_prefix_token(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = prefix_new_static(network, filename, section_line, &p);
+ if (r < 0)
+ return log_oom();
+
+ r = config_parse_address_generation_type(unit, filename, line, section, section_line,
+ lvalue, ltype, rvalue, &p->tokens, userdata);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(p);
+ return 0;
+}
+
+int config_parse_route_prefix(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(route_prefix_free_or_set_invalidp) RoutePrefix *p = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ union in_addr_union a;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = route_prefix_new_static(network, filename, section_line, &p);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_prefix_from_string(rvalue, AF_INET6, &a, &p->prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Route prefix is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ (void) in6_addr_mask(&a.in6, p->prefixlen);
+ p->prefix = a.in6;
+
+ TAKE_PTR(p);
+ return 0;
+}
+
+int config_parse_route_prefix_lifetime(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(route_prefix_free_or_set_invalidp) RoutePrefix *p = NULL;
+ Network *network = ASSERT_PTR(userdata);
+ usec_t usec;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = route_prefix_new_static(network, filename, section_line, &p);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_sec(rvalue, &usec);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Route lifetime is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (usec != USEC_INFINITY && DIV_ROUND_UP(usec, USEC_PER_SEC) >= UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Lifetime is too long, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ p->lifetime = usec;
+
+ TAKE_PTR(p);
+ return 0;
+}
+
+int config_parse_radv_dns(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *n = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ n->n_router_dns = 0;
+ n->router_dns = mfree(n->router_dns);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *w = NULL;
+ union in_addr_union a;
+
+ r = extract_first_word(&p, &w, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract word, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ if (streq(w, "_link_local"))
+ a = IN_ADDR_NULL;
+ else {
+ r = in_addr_from_string(AF_INET6, w, &a);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DNS server address, ignoring: %s", w);
+ continue;
+ }
+
+ if (in_addr_is_null(AF_INET6, &a)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "DNS server address is null, ignoring: %s", w);
+ continue;
+ }
+ }
+
+ struct in6_addr *m;
+ m = reallocarray(n->router_dns, n->n_router_dns + 1, sizeof(struct in6_addr));
+ if (!m)
+ return log_oom();
+
+ m[n->n_router_dns++] = a.in6;
+ n->router_dns = m;
+ }
+}
+
+int config_parse_radv_search_domains(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *n = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ n->router_search_domains = ordered_set_free(n->router_search_domains);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *w = NULL, *idna = NULL;
+
+ r = extract_first_word(&p, &w, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract word, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ r = dns_name_apply_idna(w, &idna);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to apply IDNA to domain name '%s', ignoring: %m", w);
+ continue;
+ } else if (r == 0)
+ /* transfer ownership to simplify subsequent operations */
+ idna = TAKE_PTR(w);
+
+ r = ordered_set_ensure_allocated(&n->router_search_domains, &string_hash_ops_free);
+ if (r < 0)
+ return log_oom();
+
+ r = ordered_set_consume(n->router_search_domains, TAKE_PTR(idna));
+ if (r < 0)
+ return log_oom();
+ }
+}
+
+static const char * const radv_prefix_delegation_table[_RADV_PREFIX_DELEGATION_MAX] = {
+ [RADV_PREFIX_DELEGATION_NONE] = "no",
+ [RADV_PREFIX_DELEGATION_STATIC] = "static",
+ [RADV_PREFIX_DELEGATION_DHCP6] = "dhcpv6",
+ [RADV_PREFIX_DELEGATION_BOTH] = "yes",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(
+ radv_prefix_delegation,
+ RADVPrefixDelegation,
+ RADV_PREFIX_DELEGATION_BOTH);
+
+int config_parse_router_prefix_delegation(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ RADVPrefixDelegation val, *ra = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (streq(lvalue, "IPv6SendRA")) {
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid %s= setting, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ /* When IPv6SendRA= is enabled, only static prefixes are sent by default, and users
+ * need to explicitly enable DHCPv6PrefixDelegation=. */
+ *ra = r ? RADV_PREFIX_DELEGATION_STATIC : RADV_PREFIX_DELEGATION_NONE;
+ return 0;
+ }
+
+ /* For backward compatibility */
+ val = radv_prefix_delegation_from_string(rvalue);
+ if (val < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, val,
+ "Invalid %s= setting, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ *ra = val;
+ return 0;
+}
+
+int config_parse_router_lifetime(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ usec_t usec, *lifetime = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *lifetime = RADV_DEFAULT_ROUTER_LIFETIME_USEC;
+ return 0;
+ }
+
+ r = parse_sec(rvalue, &usec);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse router lifetime, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (usec > 0) {
+ if (usec < RADV_MIN_ROUTER_LIFETIME_USEC) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Router lifetime %s is too short, using %s.",
+ FORMAT_TIMESPAN(usec, USEC_PER_SEC),
+ FORMAT_TIMESPAN(RADV_MIN_ROUTER_LIFETIME_USEC, USEC_PER_SEC));
+ usec = RADV_MIN_ROUTER_LIFETIME_USEC;
+ } else if (usec > RADV_MAX_ROUTER_LIFETIME_USEC) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Router lifetime %s is too large, using %s.",
+ FORMAT_TIMESPAN(usec, USEC_PER_SEC),
+ FORMAT_TIMESPAN(RADV_MAX_ROUTER_LIFETIME_USEC, USEC_PER_SEC));
+ usec = RADV_MAX_ROUTER_LIFETIME_USEC;
+ }
+ }
+
+ *lifetime = usec;
+ return 0;
+}
+
+int config_parse_router_preference(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(rvalue, "high"))
+ network->router_preference = SD_NDISC_PREFERENCE_HIGH;
+ else if (STR_IN_SET(rvalue, "medium", "normal", "default"))
+ network->router_preference = SD_NDISC_PREFERENCE_MEDIUM;
+ else if (streq(rvalue, "low"))
+ network->router_preference = SD_NDISC_PREFERENCE_LOW;
+ else
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid router preference, ignoring assignment: %s", rvalue);
+
+ return 0;
+}
diff --git a/src/network/networkd-radv.h b/src/network/networkd-radv.h
new file mode 100644
index 0000000..ebebb39
--- /dev/null
+++ b/src/network/networkd-radv.h
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+/***
+ Copyright © 2017 Intel Corporation. All rights reserved.
+***/
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+#include "sd-radv.h"
+
+#include "in-addr-util.h"
+#include "conf-parser.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+typedef struct Network Network;
+
+typedef enum RADVPrefixDelegation {
+ RADV_PREFIX_DELEGATION_NONE = 0,
+ RADV_PREFIX_DELEGATION_STATIC = 1 << 0,
+ RADV_PREFIX_DELEGATION_DHCP6 = 1 << 1,
+ RADV_PREFIX_DELEGATION_BOTH = RADV_PREFIX_DELEGATION_STATIC | RADV_PREFIX_DELEGATION_DHCP6,
+ _RADV_PREFIX_DELEGATION_MAX,
+ _RADV_PREFIX_DELEGATION_INVALID = -EINVAL,
+} RADVPrefixDelegation;
+
+typedef struct Prefix {
+ Network *network;
+ ConfigSection *section;
+
+ struct in6_addr prefix;
+ uint8_t prefixlen;
+ usec_t preferred_lifetime;
+ usec_t valid_lifetime;
+
+ bool onlink;
+ bool address_auto_configuration;
+
+ bool assign;
+ uint32_t route_metric;
+ Set *tokens;
+} Prefix;
+
+typedef struct RoutePrefix {
+ Network *network;
+ ConfigSection *section;
+
+ struct in6_addr prefix;
+ uint8_t prefixlen;
+ usec_t lifetime;
+} RoutePrefix;
+
+Prefix *prefix_free(Prefix *prefix);
+RoutePrefix *route_prefix_free(RoutePrefix *prefix);
+
+void network_drop_invalid_prefixes(Network *network);
+void network_drop_invalid_route_prefixes(Network *network);
+void network_adjust_radv(Network *network);
+
+int link_request_radv_addresses(Link *link);
+
+bool link_radv_enabled(Link *link);
+int radv_start(Link *link);
+int radv_update_mac(Link *link);
+int radv_add_prefix(Link *link, const struct in6_addr *prefix, uint8_t prefix_len,
+ usec_t lifetime_preferred_usec, usec_t lifetime_valid_usec);
+
+int link_request_radv(Link *link);
+
+const char* radv_prefix_delegation_to_string(RADVPrefixDelegation i) _const_;
+RADVPrefixDelegation radv_prefix_delegation_from_string(const char *s) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_router_prefix_delegation);
+CONFIG_PARSER_PROTOTYPE(config_parse_router_lifetime);
+CONFIG_PARSER_PROTOTYPE(config_parse_router_preference);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix_boolean);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix_lifetime);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix_metric);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix_token);
+CONFIG_PARSER_PROTOTYPE(config_parse_radv_dns);
+CONFIG_PARSER_PROTOTYPE(config_parse_radv_search_domains);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_prefix);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_prefix_lifetime);
diff --git a/src/network/networkd-route-util.c b/src/network/networkd-route-util.c
new file mode 100644
index 0000000..2dd646f
--- /dev/null
+++ b/src/network/networkd-route-util.c
@@ -0,0 +1,567 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/rtnetlink.h>
+
+#include "alloc-util.h"
+#include "missing_threads.h"
+#include "networkd-address.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-route-util.h"
+#include "networkd-route.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "sysctl-util.h"
+
+#define ROUTES_DEFAULT_MAX_PER_FAMILY 4096U
+
+unsigned routes_max(void) {
+ static thread_local unsigned cached = 0;
+ _cleanup_free_ char *s4 = NULL, *s6 = NULL;
+ unsigned val4 = ROUTES_DEFAULT_MAX_PER_FAMILY, val6 = ROUTES_DEFAULT_MAX_PER_FAMILY;
+
+ if (cached > 0)
+ return cached;
+
+ if (sysctl_read_ip_property(AF_INET, NULL, "route/max_size", &s4) >= 0)
+ if (safe_atou(s4, &val4) >= 0 && val4 == 2147483647U)
+ /* This is the default "no limit" value in the kernel */
+ val4 = ROUTES_DEFAULT_MAX_PER_FAMILY;
+
+ if (sysctl_read_ip_property(AF_INET6, NULL, "route/max_size", &s6) >= 0)
+ (void) safe_atou(s6, &val6);
+
+ cached = MAX(ROUTES_DEFAULT_MAX_PER_FAMILY, val4) +
+ MAX(ROUTES_DEFAULT_MAX_PER_FAMILY, val6);
+ return cached;
+}
+
+static bool route_lifetime_is_valid(const Route *route) {
+ assert(route);
+
+ return
+ route->lifetime_usec == USEC_INFINITY ||
+ route->lifetime_usec > now(CLOCK_BOOTTIME);
+}
+
+static Route *link_find_default_gateway(Link *link, int family, Route *gw) {
+ Route *route;
+
+ assert(link);
+
+ SET_FOREACH(route, link->routes) {
+ if (!route_exists(route))
+ continue;
+ if (family != AF_UNSPEC && route->family != family)
+ continue;
+ if (route->dst_prefixlen != 0)
+ continue;
+ if (route->src_prefixlen != 0)
+ continue;
+ if (route->table != RT_TABLE_MAIN)
+ continue;
+ if (route->type != RTN_UNICAST)
+ continue;
+ if (route->scope != RT_SCOPE_UNIVERSE)
+ continue;
+ if (!in_addr_is_set(route->gw_family, &route->gw))
+ continue;
+ if (gw) {
+ if (route->gw_weight > gw->gw_weight)
+ continue;
+ if (route->priority >= gw->priority)
+ continue;
+ }
+ gw = route;
+ }
+
+ return gw;
+}
+
+int manager_find_uplink(Manager *m, int family, Link *exclude, Link **ret) {
+ Route *gw = NULL;
+ Link *link;
+
+ assert(m);
+ assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6));
+
+ /* Looks for a suitable "uplink", via black magic: an interface that is up and where the
+ * default route with the highest priority points to. */
+
+ HASHMAP_FOREACH(link, m->links_by_index) {
+ if (link == exclude)
+ continue;
+
+ if (link->state != LINK_STATE_CONFIGURED)
+ continue;
+
+ gw = link_find_default_gateway(link, family, gw);
+ }
+
+ if (!gw)
+ return -ENOENT;
+
+ if (ret) {
+ assert(gw->link);
+ *ret = gw->link;
+ }
+
+ return 0;
+}
+
+bool gateway_is_ready(Link *link, bool onlink, int family, const union in_addr_union *gw) {
+ Route *route;
+ Address *a;
+
+ assert(link);
+ assert(link->manager);
+
+ if (onlink)
+ return true;
+
+ if (!gw || !in_addr_is_set(family, gw))
+ return true;
+
+ if (family == AF_INET6 && in6_addr_is_link_local(&gw->in6))
+ return true;
+
+ SET_FOREACH(route, link->routes) {
+ if (!route_exists(route))
+ continue;
+ if (!route_lifetime_is_valid(route))
+ continue;
+ if (route->family != family)
+ continue;
+ if (!in_addr_is_set(route->family, &route->dst) && route->dst_prefixlen == 0)
+ continue;
+ if (in_addr_prefix_covers(family, &route->dst, route->dst_prefixlen, gw) > 0)
+ return true;
+ }
+
+ if (link->manager->manage_foreign_routes)
+ return false;
+
+ /* If we do not manage foreign routes, then there may exist a prefix route we do not know,
+ * which was created on configuring an address. Hence, also check the addresses. */
+ SET_FOREACH(a, link->addresses) {
+ if (!address_is_ready(a))
+ continue;
+ if (a->family != family)
+ continue;
+ if (FLAGS_SET(a->flags, IFA_F_NOPREFIXROUTE))
+ continue;
+ if (in_addr_prefix_covers(a->family,
+ in_addr_is_set(a->family, &a->in_addr_peer) ? &a->in_addr_peer : &a->in_addr,
+ a->prefixlen, gw) > 0)
+ return true;
+ }
+
+ return false;
+}
+
+static int link_address_is_reachable_internal(
+ Link *link,
+ int family,
+ const union in_addr_union *address,
+ const union in_addr_union *prefsrc, /* optional */
+ Route **ret) {
+
+ Route *route, *found = NULL;
+
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(address);
+
+ SET_FOREACH(route, link->routes) {
+ if (!route_exists(route))
+ continue;
+
+ if (!route_lifetime_is_valid(route))
+ continue;
+
+ if (route->type != RTN_UNICAST)
+ continue;
+
+ if (route->family != family)
+ continue;
+
+ if (in_addr_prefix_covers(family, &route->dst, route->dst_prefixlen, address) <= 0)
+ continue;
+
+ if (prefsrc &&
+ in_addr_is_set(family, prefsrc) &&
+ in_addr_is_set(family, &route->prefsrc) &&
+ !in_addr_equal(family, prefsrc, &route->prefsrc))
+ continue;
+
+ if (found && found->priority <= route->priority)
+ continue;
+
+ found = route;
+ }
+
+ if (!found)
+ return -ENOENT;
+
+ if (ret)
+ *ret = found;
+
+ return 0;
+}
+
+int link_address_is_reachable(
+ Link *link,
+ int family,
+ const union in_addr_union *address,
+ const union in_addr_union *prefsrc, /* optional */
+ Address **ret) {
+
+ Route *route;
+ Address *a;
+ int r;
+
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(address);
+
+ /* This checks if the address is reachable, and optionally return the Address object of the
+ * preferred source to access the address. */
+
+ r = link_address_is_reachable_internal(link, family, address, prefsrc, &route);
+ if (r < 0)
+ return r;
+
+ if (!in_addr_is_set(route->family, &route->prefsrc)) {
+ if (ret)
+ *ret = NULL;
+ return 0;
+ }
+
+ r = link_get_address(link, route->family, &route->prefsrc, 0, &a);
+ if (r < 0)
+ return r;
+
+ if (!address_is_ready(a))
+ return -EBUSY;
+
+ if (ret)
+ *ret = a;
+
+ return 0;
+}
+
+int manager_address_is_reachable(
+ Manager *manager,
+ int family,
+ const union in_addr_union *address,
+ const union in_addr_union *prefsrc, /* optional */
+ Address **ret) {
+
+ Route *route, *found = NULL;
+ Address *a;
+ Link *link;
+ int r;
+
+ assert(manager);
+
+ HASHMAP_FOREACH(link, manager->links_by_index) {
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ continue;
+
+ if (link_address_is_reachable_internal(link, family, address, prefsrc, &route) < 0)
+ continue;
+
+ if (found && found->priority <= route->priority)
+ continue;
+
+ found = route;
+ }
+
+ if (!found)
+ return -ENOENT;
+
+ if (!in_addr_is_set(found->family, &found->prefsrc)) {
+ if (ret)
+ *ret = NULL;
+ return 0;
+ }
+
+ r = link_get_address(found->link, found->family, &found->prefsrc, 0, &a);
+ if (r < 0)
+ return r;
+
+ if (!address_is_ready(a))
+ return -EBUSY;
+
+ if (ret)
+ *ret = a;
+
+ return 0;
+}
+
+static const char * const route_type_table[__RTN_MAX] = {
+ [RTN_UNICAST] = "unicast",
+ [RTN_LOCAL] = "local",
+ [RTN_BROADCAST] = "broadcast",
+ [RTN_ANYCAST] = "anycast",
+ [RTN_MULTICAST] = "multicast",
+ [RTN_BLACKHOLE] = "blackhole",
+ [RTN_UNREACHABLE] = "unreachable",
+ [RTN_PROHIBIT] = "prohibit",
+ [RTN_THROW] = "throw",
+ [RTN_NAT] = "nat",
+ [RTN_XRESOLVE] = "xresolve",
+};
+
+assert_cc(__RTN_MAX <= UCHAR_MAX);
+DEFINE_STRING_TABLE_LOOKUP(route_type, int);
+
+static const char * const route_scope_table[] = {
+ [RT_SCOPE_UNIVERSE] = "global",
+ [RT_SCOPE_SITE] = "site",
+ [RT_SCOPE_LINK] = "link",
+ [RT_SCOPE_HOST] = "host",
+ [RT_SCOPE_NOWHERE] = "nowhere",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(route_scope, int, UINT8_MAX);
+
+static const char * const route_protocol_table[] = {
+ [RTPROT_KERNEL] = "kernel",
+ [RTPROT_BOOT] = "boot",
+ [RTPROT_STATIC] = "static",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(route_protocol, int, UINT8_MAX);
+
+static const char * const route_protocol_full_table[] = {
+ [RTPROT_REDIRECT] = "redirect",
+ [RTPROT_KERNEL] = "kernel",
+ [RTPROT_BOOT] = "boot",
+ [RTPROT_STATIC] = "static",
+ [RTPROT_GATED] = "gated",
+ [RTPROT_RA] = "ra",
+ [RTPROT_MRT] = "mrt",
+ [RTPROT_ZEBRA] = "zebra",
+ [RTPROT_BIRD] = "bird",
+ [RTPROT_DNROUTED] = "dnrouted",
+ [RTPROT_XORP] = "xorp",
+ [RTPROT_NTK] = "ntk",
+ [RTPROT_DHCP] = "dhcp",
+ [RTPROT_MROUTED] = "mrouted",
+ [RTPROT_BABEL] = "babel",
+ [RTPROT_BGP] = "bgp",
+ [RTPROT_ISIS] = "isis",
+ [RTPROT_OSPF] = "ospf",
+ [RTPROT_RIP] = "rip",
+ [RTPROT_EIGRP] = "eigrp",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(route_protocol_full, int, UINT8_MAX);
+
+int route_flags_to_string_alloc(uint32_t flags, char **ret) {
+ _cleanup_free_ char *str = NULL;
+ static const char* map[] = {
+ [LOG2U(RTNH_F_DEAD)] = "dead", /* Nexthop is dead (used by multipath) */
+ [LOG2U(RTNH_F_PERVASIVE)] = "pervasive", /* Do recursive gateway lookup */
+ [LOG2U(RTNH_F_ONLINK)] = "onlink" , /* Gateway is forced on link */
+ [LOG2U(RTNH_F_OFFLOAD)] = "offload", /* Nexthop is offloaded */
+ [LOG2U(RTNH_F_LINKDOWN)] = "linkdown", /* carrier-down on nexthop */
+ [LOG2U(RTNH_F_UNRESOLVED)] = "unresolved", /* The entry is unresolved (ipmr) */
+ [LOG2U(RTNH_F_TRAP)] = "trap", /* Nexthop is trapping packets */
+ };
+
+ assert(ret);
+
+ for (size_t i = 0; i < ELEMENTSOF(map); i++)
+ if (FLAGS_SET(flags, 1 << i) && map[i])
+ if (!strextend_with_separator(&str, ",", map[i]))
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(str);
+ return 0;
+}
+
+static const char * const route_table_table[] = {
+ [RT_TABLE_DEFAULT] = "default",
+ [RT_TABLE_MAIN] = "main",
+ [RT_TABLE_LOCAL] = "local",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP(route_table, int);
+
+int manager_get_route_table_from_string(const Manager *m, const char *s, uint32_t *ret) {
+ uint32_t t;
+ int r;
+
+ assert(m);
+ assert(s);
+ assert(ret);
+
+ r = route_table_from_string(s);
+ if (r >= 0) {
+ *ret = (uint32_t) r;
+ return 0;
+ }
+
+ t = PTR_TO_UINT32(hashmap_get(m->route_table_numbers_by_name, s));
+ if (t != 0) {
+ *ret = t;
+ return 0;
+ }
+
+ r = safe_atou32(s, &t);
+ if (r < 0)
+ return r;
+
+ if (t == 0)
+ return -ERANGE;
+
+ *ret = t;
+ return 0;
+}
+
+int manager_get_route_table_to_string(const Manager *m, uint32_t table, char **ret) {
+ _cleanup_free_ char *str = NULL;
+ const char *s;
+ int r;
+
+ assert(m);
+ assert(ret);
+
+ /* Unlike manager_get_route_table_from_string(), this accepts 0, as the kernel may create routes with
+ * table 0. See issue #25089. */
+
+ s = route_table_to_string(table);
+ if (!s)
+ s = hashmap_get(m->route_table_names_by_number, UINT32_TO_PTR(table));
+
+ if (s)
+ /* Currently, this is only used in debugging logs. To not confuse any bug
+ * reports, let's include the table number. */
+ r = asprintf(&str, "%s(%" PRIu32 ")", s, table);
+ else
+ r = asprintf(&str, "%" PRIu32, table);
+ if (r < 0)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(str);
+ return 0;
+}
+
+int config_parse_route_table_names(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Manager *m = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ m->route_table_names_by_number = hashmap_free(m->route_table_names_by_number);
+ m->route_table_numbers_by_name = hashmap_free(m->route_table_numbers_by_name);
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *name = NULL;
+ uint32_t table;
+ char *num;
+
+ r = extract_first_word(&p, &name, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid RouteTable=, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ num = strchr(name, ':');
+ if (!num) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid route table name and number pair, ignoring assignment: %s", name);
+ continue;
+ }
+
+ *num++ = '\0';
+
+ if (isempty(name)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Route table name cannot be empty. Ignoring assignment: %s:%s", name, num);
+ continue;
+ }
+ if (in_charset(name, DIGITS)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Route table name cannot be numeric. Ignoring assignment: %s:%s", name, num);
+ continue;
+ }
+ if (STR_IN_SET(name, "default", "main", "local")) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Route table name %s is already predefined. Ignoring assignment: %s:%s", name, name, num);
+ continue;
+ }
+
+ r = safe_atou32(num, &table);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse route table number '%s', ignoring assignment: %s:%s", num, name, num);
+ continue;
+ }
+ if (table == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid route table number, ignoring assignment: %s:%s", name, num);
+ continue;
+ }
+
+ r = hashmap_ensure_put(&m->route_table_numbers_by_name, &string_hash_ops_free, name, UINT32_TO_PTR(table));
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r == -EEXIST) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Specified route table name and number pair conflicts with others, ignoring assignment: %s:%s", name, num);
+ continue;
+ }
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store route table name and number pair, ignoring assignment: %s:%s", name, num);
+ continue;
+ }
+ if (r == 0)
+ /* The entry is duplicated. It should not be added to route_table_names_by_number hashmap. */
+ continue;
+
+ r = hashmap_ensure_put(&m->route_table_names_by_number, NULL, UINT32_TO_PTR(table), name);
+ if (r < 0) {
+ hashmap_remove(m->route_table_numbers_by_name, name);
+
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r == -EEXIST)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Specified route table name and number pair conflicts with others, ignoring assignment: %s:%s", name, num);
+ else
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store route table name and number pair, ignoring assignment: %s:%s", name, num);
+ continue;
+ }
+ assert(r > 0);
+
+ TAKE_PTR(name);
+ }
+}
diff --git a/src/network/networkd-route-util.h b/src/network/networkd-route-util.h
new file mode 100644
index 0000000..b862cd6
--- /dev/null
+++ b/src/network/networkd-route-util.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+typedef struct Address Address;
+
+unsigned routes_max(void);
+
+int manager_find_uplink(Manager *m, int family, Link *exclude, Link **ret);
+
+bool gateway_is_ready(Link *link, bool onlink, int family, const union in_addr_union *gw);
+
+int link_address_is_reachable(
+ Link *link,
+ int family,
+ const union in_addr_union *address,
+ const union in_addr_union *prefsrc, /* optional */
+ Address **ret);
+
+int manager_address_is_reachable(
+ Manager *manager,
+ int family,
+ const union in_addr_union *address,
+ const union in_addr_union *prefsrc, /* optional */
+ Address **ret);
+
+int route_type_from_string(const char *s) _pure_;
+const char *route_type_to_string(int t) _const_;
+
+int route_scope_from_string(const char *s);
+int route_scope_to_string_alloc(int t, char **ret);
+
+int route_protocol_from_string(const char *s);
+int route_protocol_to_string_alloc(int t, char **ret);
+int route_protocol_full_from_string(const char *s);
+int route_protocol_full_to_string_alloc(int t, char **ret);
+
+int route_flags_to_string_alloc(uint32_t flags, char **ret);
+
+int manager_get_route_table_from_string(const Manager *m, const char *table, uint32_t *ret);
+int manager_get_route_table_to_string(const Manager *m, uint32_t table, char **ret);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_route_table_names);
diff --git a/src/network/networkd-route.c b/src/network/networkd-route.c
new file mode 100644
index 0000000..88e9ad5
--- /dev/null
+++ b/src/network/networkd-route.c
@@ -0,0 +1,2996 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/icmpv6.h>
+#include <linux/ipv6_route.h>
+#include <linux/nexthop.h>
+
+#include "alloc-util.h"
+#include "event-util.h"
+#include "netlink-util.h"
+#include "networkd-address.h"
+#include "networkd-ipv4ll.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-nexthop.h"
+#include "networkd-queue.h"
+#include "networkd-route-util.h"
+#include "networkd-route.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "vrf.h"
+#include "wireguard.h"
+
+int route_new(Route **ret) {
+ _cleanup_(route_freep) Route *route = NULL;
+
+ route = new(Route, 1);
+ if (!route)
+ return -ENOMEM;
+
+ *route = (Route) {
+ .family = AF_UNSPEC,
+ .scope = RT_SCOPE_UNIVERSE,
+ .protocol = RTPROT_UNSPEC,
+ .type = RTN_UNICAST,
+ .table = RT_TABLE_MAIN,
+ .lifetime_usec = USEC_INFINITY,
+ .quickack = -1,
+ .fast_open_no_cookie = -1,
+ .gateway_onlink = -1,
+ .ttl_propagate = -1,
+ };
+
+ *ret = TAKE_PTR(route);
+
+ return 0;
+}
+
+static int route_new_static(Network *network, const char *filename, unsigned section_line, Route **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(route_freep) Route *route = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ route = hashmap_get(network->routes_by_section, n);
+ if (route) {
+ *ret = TAKE_PTR(route);
+ return 0;
+ }
+
+ if (hashmap_size(network->routes_by_section) >= routes_max())
+ return -E2BIG;
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route->protocol = RTPROT_STATIC;
+ route->network = network;
+ route->section = TAKE_PTR(n);
+ route->source = NETWORK_CONFIG_SOURCE_STATIC;
+
+ r = hashmap_ensure_put(&network->routes_by_section, &config_section_hash_ops, route->section, route);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(route);
+ return 0;
+}
+
+Route *route_free(Route *route) {
+ if (!route)
+ return NULL;
+
+ if (route->network) {
+ assert(route->section);
+ hashmap_remove(route->network->routes_by_section, route->section);
+ }
+
+ config_section_free(route->section);
+
+ if (route->link)
+ set_remove(route->link->routes, route);
+
+ if (route->manager)
+ set_remove(route->manager->routes, route);
+
+ ordered_set_free_with_destructor(route->multipath_routes, multipath_route_free);
+
+ sd_event_source_disable_unref(route->expire);
+
+ free(route->tcp_congestion_control_algo);
+
+ return mfree(route);
+}
+
+static void route_hash_func(const Route *route, struct siphash *state) {
+ assert(route);
+
+ siphash24_compress(&route->family, sizeof(route->family), state);
+
+ switch (route->family) {
+ case AF_INET:
+ case AF_INET6:
+ siphash24_compress(&route->dst_prefixlen, sizeof(route->dst_prefixlen), state);
+ siphash24_compress(&route->dst, FAMILY_ADDRESS_SIZE(route->family), state);
+
+ siphash24_compress(&route->src_prefixlen, sizeof(route->src_prefixlen), state);
+ siphash24_compress(&route->src, FAMILY_ADDRESS_SIZE(route->family), state);
+
+ siphash24_compress(&route->gw_family, sizeof(route->gw_family), state);
+ if (IN_SET(route->gw_family, AF_INET, AF_INET6)) {
+ siphash24_compress(&route->gw, FAMILY_ADDRESS_SIZE(route->gw_family), state);
+ siphash24_compress(&route->gw_weight, sizeof(route->gw_weight), state);
+ }
+
+ siphash24_compress(&route->prefsrc, FAMILY_ADDRESS_SIZE(route->family), state);
+
+ siphash24_compress(&route->tos, sizeof(route->tos), state);
+ siphash24_compress(&route->priority, sizeof(route->priority), state);
+ siphash24_compress(&route->table, sizeof(route->table), state);
+ siphash24_compress(&route->protocol, sizeof(route->protocol), state);
+ siphash24_compress(&route->scope, sizeof(route->scope), state);
+ siphash24_compress(&route->type, sizeof(route->type), state);
+
+ siphash24_compress(&route->initcwnd, sizeof(route->initcwnd), state);
+ siphash24_compress(&route->initrwnd, sizeof(route->initrwnd), state);
+
+ siphash24_compress(&route->advmss, sizeof(route->advmss), state);
+ siphash24_compress(&route->nexthop_id, sizeof(route->nexthop_id), state);
+
+ break;
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ break;
+ }
+}
+
+static int route_compare_func(const Route *a, const Route *b) {
+ int r;
+
+ r = CMP(a->family, b->family);
+ if (r != 0)
+ return r;
+
+ switch (a->family) {
+ case AF_INET:
+ case AF_INET6:
+ r = CMP(a->dst_prefixlen, b->dst_prefixlen);
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a->dst, &b->dst, FAMILY_ADDRESS_SIZE(a->family));
+ if (r != 0)
+ return r;
+
+ r = CMP(a->src_prefixlen, b->src_prefixlen);
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a->src, &b->src, FAMILY_ADDRESS_SIZE(a->family));
+ if (r != 0)
+ return r;
+
+ r = CMP(a->gw_family, b->gw_family);
+ if (r != 0)
+ return r;
+
+ if (IN_SET(a->gw_family, AF_INET, AF_INET6)) {
+ r = memcmp(&a->gw, &b->gw, FAMILY_ADDRESS_SIZE(a->family));
+ if (r != 0)
+ return r;
+
+ r = CMP(a->gw_weight, b->gw_weight);
+ if (r != 0)
+ return r;
+ }
+
+ r = memcmp(&a->prefsrc, &b->prefsrc, FAMILY_ADDRESS_SIZE(a->family));
+ if (r != 0)
+ return r;
+
+ r = CMP(a->tos, b->tos);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->priority, b->priority);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->table, b->table);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->protocol, b->protocol);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->scope, b->scope);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->type, b->type);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->initcwnd, b->initcwnd);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->initrwnd, b->initrwnd);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->advmss, b->advmss);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->nexthop_id, b->nexthop_id);
+ if (r != 0)
+ return r;
+
+ return 0;
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ return 0;
+ }
+}
+
+DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ route_hash_ops,
+ Route,
+ route_hash_func,
+ route_compare_func,
+ route_free);
+
+static bool route_type_is_reject(const Route *route) {
+ assert(route);
+
+ return IN_SET(route->type, RTN_UNREACHABLE, RTN_PROHIBIT, RTN_BLACKHOLE, RTN_THROW);
+}
+
+static bool route_needs_convert(const Route *route) {
+ assert(route);
+
+ return route->nexthop_id > 0 || !ordered_set_isempty(route->multipath_routes);
+}
+
+static int route_add(Manager *manager, Link *link, Route *route) {
+ int r;
+
+ assert(route);
+
+ if (route_type_is_reject(route)) {
+ assert(manager);
+
+ r = set_ensure_put(&manager->routes, &route_hash_ops, route);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ route->manager = manager;
+ } else {
+ assert(link);
+
+ r = set_ensure_put(&link->routes, &route_hash_ops, route);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ route->link = link;
+ }
+
+ return 0;
+}
+
+int route_get(Manager *manager, Link *link, const Route *in, Route **ret) {
+ Route *route;
+
+ assert(in);
+
+ if (route_type_is_reject(in)) {
+ if (!manager)
+ return -ENOENT;
+
+ route = set_get(manager->routes, in);
+ } else {
+ if (!link)
+ return -ENOENT;
+
+ route = set_get(link->routes, in);
+ }
+ if (!route)
+ return -ENOENT;
+
+ if (ret)
+ *ret = route;
+
+ return 0;
+}
+
+int route_dup(const Route *src, Route **ret) {
+ _cleanup_(route_freep) Route *dest = NULL;
+ int r;
+
+ /* This does not copy mulipath routes. */
+
+ assert(src);
+ assert(ret);
+
+ dest = newdup(Route, src, 1);
+ if (!dest)
+ return -ENOMEM;
+
+ /* Unset all pointers */
+ dest->network = NULL;
+ dest->section = NULL;
+ dest->link = NULL;
+ dest->manager = NULL;
+ dest->multipath_routes = NULL;
+ dest->expire = NULL;
+ dest->tcp_congestion_control_algo = NULL;
+
+ r = free_and_strdup(&dest->tcp_congestion_control_algo, src->tcp_congestion_control_algo);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(dest);
+ return 0;
+}
+
+static void route_apply_nexthop(Route *route, const NextHop *nh, uint8_t nh_weight) {
+ assert(route);
+ assert(nh);
+ assert(hashmap_isempty(nh->group));
+
+ route->gw_family = nh->family;
+ route->gw = nh->gw;
+
+ if (nh_weight != UINT8_MAX)
+ route->gw_weight = nh_weight;
+
+ if (nh->blackhole)
+ route->type = RTN_BLACKHOLE;
+}
+
+static void route_apply_multipath_route(Route *route, const MultipathRoute *m) {
+ assert(route);
+ assert(m);
+
+ route->gw_family = m->gateway.family;
+ route->gw = m->gateway.address;
+ route->gw_weight = m->weight;
+}
+
+static int multipath_route_get_link(Manager *manager, const MultipathRoute *m, Link **ret) {
+ int r;
+
+ assert(manager);
+ assert(m);
+
+ if (m->ifname) {
+ r = link_get_by_name(manager, m->ifname, ret);
+ return r < 0 ? r : 1;
+
+ } else if (m->ifindex > 0) { /* Always ignore ifindex if ifname is set. */
+ r = link_get_by_index(manager, m->ifindex, ret);
+ return r < 0 ? r : 1;
+ }
+
+ if (ret)
+ *ret = NULL;
+ return 0;
+}
+
+typedef struct ConvertedRoutes {
+ size_t n;
+ Route **routes;
+ Link **links;
+} ConvertedRoutes;
+
+static ConvertedRoutes *converted_routes_free(ConvertedRoutes *c) {
+ if (!c)
+ return NULL;
+
+ for (size_t i = 0; i < c->n; i++)
+ route_free(c->routes[i]);
+
+ free(c->routes);
+ free(c->links);
+
+ return mfree(c);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(ConvertedRoutes*, converted_routes_free);
+
+static int converted_routes_new(size_t n, ConvertedRoutes **ret) {
+ _cleanup_(converted_routes_freep) ConvertedRoutes *c = NULL;
+ _cleanup_free_ Route **routes = NULL;
+ _cleanup_free_ Link **links = NULL;
+
+ assert(n > 0);
+ assert(ret);
+
+ routes = new0(Route*, n);
+ if (!routes)
+ return -ENOMEM;
+
+ links = new0(Link*, n);
+ if (!links)
+ return -ENOMEM;
+
+ c = new(ConvertedRoutes, 1);
+ if (!c)
+ return -ENOMEM;
+
+ *c = (ConvertedRoutes) {
+ .n = n,
+ .routes = TAKE_PTR(routes),
+ .links = TAKE_PTR(links),
+ };
+
+ *ret = TAKE_PTR(c);
+ return 0;
+}
+
+static int route_convert(Manager *manager, const Route *route, ConvertedRoutes **ret) {
+ _cleanup_(converted_routes_freep) ConvertedRoutes *c = NULL;
+ int r;
+
+ assert(manager);
+ assert(route);
+ assert(ret);
+
+ if (!route_needs_convert(route)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ if (route->nexthop_id > 0) {
+ struct nexthop_grp *nhg;
+ NextHop *nh;
+
+ r = manager_get_nexthop_by_id(manager, route->nexthop_id, &nh);
+ if (r < 0)
+ return r;
+
+ if (hashmap_isempty(nh->group)) {
+ r = converted_routes_new(1, &c);
+ if (r < 0)
+ return r;
+
+ r = route_dup(route, &c->routes[0]);
+ if (r < 0)
+ return r;
+
+ route_apply_nexthop(c->routes[0], nh, UINT8_MAX);
+ c->links[0] = nh->link;
+
+ *ret = TAKE_PTR(c);
+ return 1;
+ }
+
+ r = converted_routes_new(hashmap_size(nh->group), &c);
+ if (r < 0)
+ return r;
+
+ size_t i = 0;
+ HASHMAP_FOREACH(nhg, nh->group) {
+ NextHop *h;
+
+ r = manager_get_nexthop_by_id(manager, nhg->id, &h);
+ if (r < 0)
+ return r;
+
+ r = route_dup(route, &c->routes[i]);
+ if (r < 0)
+ return r;
+
+ route_apply_nexthop(c->routes[i], h, nhg->weight);
+ c->links[i] = h->link;
+
+ i++;
+ }
+
+ *ret = TAKE_PTR(c);
+ return 1;
+
+ }
+
+ assert(!ordered_set_isempty(route->multipath_routes));
+
+ r = converted_routes_new(ordered_set_size(route->multipath_routes), &c);
+ if (r < 0)
+ return r;
+
+ size_t i = 0;
+ MultipathRoute *m;
+ ORDERED_SET_FOREACH(m, route->multipath_routes) {
+ r = route_dup(route, &c->routes[i]);
+ if (r < 0)
+ return r;
+
+ route_apply_multipath_route(c->routes[i], m);
+
+ r = multipath_route_get_link(manager, m, &c->links[i]);
+ if (r < 0)
+ return r;
+
+ i++;
+ }
+
+ *ret = TAKE_PTR(c);
+ return 1;
+}
+
+void link_mark_routes(Link *link, NetworkConfigSource source) {
+ Route *route;
+
+ assert(link);
+
+ SET_FOREACH(route, link->routes) {
+ if (route->source != source)
+ continue;
+
+ route_mark(route);
+ }
+}
+
+static void log_route_debug(const Route *route, const char *str, const Link *link, const Manager *manager) {
+ _cleanup_free_ char *state = NULL, *gw_alloc = NULL, *prefsrc = NULL,
+ *table = NULL, *scope = NULL, *proto = NULL, *flags = NULL;
+ const char *gw = NULL, *dst, *src;
+
+ assert(route);
+ assert(str);
+ assert(manager);
+
+ /* link may be NULL. */
+
+ if (!DEBUG_LOGGING)
+ return;
+
+ (void) network_config_state_to_string_alloc(route->state, &state);
+
+ dst = in_addr_is_set(route->family, &route->dst) || route->dst_prefixlen > 0 ?
+ IN_ADDR_PREFIX_TO_STRING(route->family, &route->dst, route->dst_prefixlen) : NULL;
+ src = in_addr_is_set(route->family, &route->src) || route->src_prefixlen > 0 ?
+ IN_ADDR_PREFIX_TO_STRING(route->family, &route->src, route->src_prefixlen) : NULL;
+
+ if (in_addr_is_set(route->gw_family, &route->gw)) {
+ (void) in_addr_to_string(route->gw_family, &route->gw, &gw_alloc);
+ gw = gw_alloc;
+ } else if (route->gateway_from_dhcp_or_ra) {
+ if (route->gw_family == AF_INET)
+ gw = "_dhcp4";
+ else if (route->gw_family == AF_INET6)
+ gw = "_ipv6ra";
+ } else {
+ MultipathRoute *m;
+
+ ORDERED_SET_FOREACH(m, route->multipath_routes) {
+ _cleanup_free_ char *buf = NULL;
+ union in_addr_union a = m->gateway.address;
+
+ (void) in_addr_to_string(m->gateway.family, &a, &buf);
+ (void) strextend_with_separator(&gw_alloc, ",", strna(buf));
+ if (m->ifname)
+ (void) strextend(&gw_alloc, "@", m->ifname);
+ else if (m->ifindex > 0)
+ (void) strextendf(&gw_alloc, "@%i", m->ifindex);
+ /* See comments in config_parse_multipath_route(). */
+ (void) strextendf(&gw_alloc, ":%"PRIu32, m->weight + 1);
+ }
+ gw = gw_alloc;
+ }
+ if (in_addr_is_set(route->family, &route->prefsrc))
+ (void) in_addr_to_string(route->family, &route->prefsrc, &prefsrc);
+ (void) route_scope_to_string_alloc(route->scope, &scope);
+ (void) manager_get_route_table_to_string(manager, route->table, &table);
+ (void) route_protocol_full_to_string_alloc(route->protocol, &proto);
+ (void) route_flags_to_string_alloc(route->flags, &flags);
+
+ log_link_debug(link,
+ "%s %s route (%s): dst: %s, src: %s, gw: %s, prefsrc: %s, scope: %s, table: %s, "
+ "proto: %s, type: %s, nexthop: %"PRIu32", priority: %"PRIu32", flags: %s",
+ str, strna(network_config_source_to_string(route->source)), strna(state),
+ strna(dst), strna(src), strna(gw), strna(prefsrc),
+ strna(scope), strna(table), strna(proto),
+ strna(route_type_to_string(route->type)),
+ route->nexthop_id, route->priority, strna(flags));
+}
+
+static int route_set_netlink_message(const Route *route, sd_netlink_message *req, Link *link) {
+ int r;
+
+ assert(route);
+ assert(req);
+
+ /* link may be NULL */
+
+ if (in_addr_is_set(route->gw_family, &route->gw) && route->nexthop_id == 0) {
+ if (route->gw_family == route->family) {
+ r = netlink_message_append_in_addr_union(req, RTA_GATEWAY, route->gw_family, &route->gw);
+ if (r < 0)
+ return r;
+ } else {
+ RouteVia rtvia = {
+ .family = route->gw_family,
+ .address = route->gw,
+ };
+
+ r = sd_netlink_message_append_data(req, RTA_VIA, &rtvia, sizeof(rtvia));
+ if (r < 0)
+ return r;
+ }
+ }
+
+ if (route->dst_prefixlen > 0) {
+ r = netlink_message_append_in_addr_union(req, RTA_DST, route->family, &route->dst);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_route_set_dst_prefixlen(req, route->dst_prefixlen);
+ if (r < 0)
+ return r;
+ }
+
+ if (route->src_prefixlen > 0) {
+ r = netlink_message_append_in_addr_union(req, RTA_SRC, route->family, &route->src);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_route_set_src_prefixlen(req, route->src_prefixlen);
+ if (r < 0)
+ return r;
+ }
+
+ if (in_addr_is_set(route->family, &route->prefsrc)) {
+ r = netlink_message_append_in_addr_union(req, RTA_PREFSRC, route->family, &route->prefsrc);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_rtnl_message_route_set_scope(req, route->scope);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_route_set_flags(req, route->flags & RTNH_F_ONLINK);
+ if (r < 0)
+ return r;
+
+ if (route->table < 256) {
+ r = sd_rtnl_message_route_set_table(req, route->table);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_rtnl_message_route_set_table(req, RT_TABLE_UNSPEC);
+ if (r < 0)
+ return r;
+
+ /* Table attribute to allow more than 256. */
+ r = sd_netlink_message_append_u32(req, RTA_TABLE, route->table);
+ if (r < 0)
+ return r;
+ }
+
+ if (!route_type_is_reject(route) &&
+ route->nexthop_id == 0 &&
+ ordered_set_isempty(route->multipath_routes)) {
+ assert(link); /* Those routes must be attached to a specific link */
+
+ r = sd_netlink_message_append_u32(req, RTA_OIF, link->ifindex);
+ if (r < 0)
+ return r;
+ }
+
+ if (route->nexthop_id > 0) {
+ r = sd_netlink_message_append_u32(req, RTA_NH_ID, route->nexthop_id);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u8(req, RTA_PREF, route->pref);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(req, RTA_PRIORITY, route->priority);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int route_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+
+ /* link may be NULL. */
+
+ if (link && IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -ESRCH)
+ log_link_message_warning_errno(link, m, r, "Could not drop route, ignoring");
+
+ return 1;
+}
+
+int route_remove(Route *route) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ unsigned char type;
+ Manager *manager;
+ Link *link;
+ int r;
+
+ assert(route);
+ assert(route->manager || (route->link && route->link->manager));
+ assert(IN_SET(route->family, AF_INET, AF_INET6));
+
+ link = route->link;
+ manager = route->manager ?: link->manager;
+
+ log_route_debug(route, "Removing", link, manager);
+
+ r = sd_rtnl_message_new_route(manager->rtnl, &req,
+ RTM_DELROUTE, route->family,
+ route->protocol);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create netlink message: %m");
+
+ if (route->family == AF_INET && route->nexthop_id > 0 && route->type == RTN_BLACKHOLE)
+ /* When IPv4 route has nexthop id and the nexthop type is blackhole, even though kernel
+ * sends RTM_NEWROUTE netlink message with blackhole type, kernel's internal route type
+ * fib_rt_info::type may not be blackhole. Thus, we cannot know the internal value.
+ * Moreover, on route removal, the matching is done with the hidden value if we set
+ * non-zero type in RTM_DELROUTE message. Note, sd_rtnl_message_new_route() sets
+ * RTN_UNICAST by default. So, we need to clear the type here. */
+ type = RTN_UNSPEC;
+ else
+ type = route->type;
+
+ r = sd_rtnl_message_route_set_type(req, type);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set route type: %m");
+
+ r = route_set_netlink_message(route, req, link);
+ if (r < 0)
+ return log_error_errno(r, "Could not fill netlink message: %m");
+
+ r = netlink_call_async(manager->rtnl, NULL, req, route_remove_handler,
+ link ? link_netlink_destroy_callback : NULL, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not send netlink message: %m");
+
+ link_ref(link);
+
+ route_enter_removing(route);
+ return 0;
+}
+
+int route_remove_and_drop(Route *route) {
+ if (!route)
+ return 0;
+
+ route_cancel_request(route, NULL);
+
+ if (route_exists(route))
+ return route_remove(route);
+
+ if (route->state == 0)
+ route_free(route);
+
+ return 0;
+}
+
+static void manager_mark_routes(Manager *manager, bool foreign, const Link *except) {
+ Route *route;
+ Link *link;
+ int r;
+
+ assert(manager);
+
+ /* First, mark all routes. */
+ SET_FOREACH(route, manager->routes) {
+ /* Do not touch routes managed by the kernel. */
+ if (route->protocol == RTPROT_KERNEL)
+ continue;
+
+ /* When 'foreign' is true, mark only foreign routes, and vice versa. */
+ if (foreign != (route->source == NETWORK_CONFIG_SOURCE_FOREIGN))
+ continue;
+
+ /* Do not touch dynamic routes. They will removed by dhcp_pd_prefix_lost() */
+ if (IN_SET(route->source, NETWORK_CONFIG_SOURCE_DHCP4, NETWORK_CONFIG_SOURCE_DHCP6))
+ continue;
+
+ /* Ignore routes not assigned yet or already removed. */
+ if (!route_exists(route))
+ continue;
+
+ route_mark(route);
+ }
+
+ /* Then, unmark all routes requested by active links. */
+ HASHMAP_FOREACH(link, manager->links_by_index) {
+ if (link == except)
+ continue;
+
+ if (!link->network)
+ continue;
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ continue;
+
+ HASHMAP_FOREACH(route, link->network->routes_by_section) {
+ _cleanup_(converted_routes_freep) ConvertedRoutes *converted = NULL;
+ Route *existing;
+
+ r = route_convert(manager, route, &converted);
+ if (r < 0)
+ continue;
+ if (r == 0) {
+ if (route_get(manager, NULL, route, &existing) >= 0)
+ route_unmark(existing);
+ continue;
+ }
+
+ for (size_t i = 0; i < converted->n; i++)
+ if (route_get(manager, NULL, converted->routes[i], &existing) >= 0)
+ route_unmark(existing);
+ }
+ }
+}
+
+static int manager_drop_marked_routes(Manager *manager) {
+ Route *route;
+ int k, r = 0;
+
+ assert(manager);
+
+ SET_FOREACH(route, manager->routes) {
+ if (!route_is_marked(route))
+ continue;
+
+ k = route_remove(route);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static bool route_by_kernel(const Route *route) {
+ assert(route);
+
+ if (route->protocol == RTPROT_KERNEL)
+ return true;
+
+ /* The kernels older than a826b04303a40d52439aa141035fca5654ccaccd (v5.11) create the IPv6
+ * multicast with RTPROT_BOOT. Do not touch it. */
+ if (route->protocol == RTPROT_BOOT &&
+ route->family == AF_INET6 &&
+ route->dst_prefixlen == 8 &&
+ in6_addr_equal(&route->dst.in6, & (struct in6_addr) {{{ 0xff,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 }}}))
+ return true;
+
+ return false;
+}
+
+static void link_unmark_wireguard_routes(Link *link) {
+ Route *route, *existing;
+ Wireguard *w;
+
+ assert(link);
+
+ w = WIREGUARD(link->netdev);
+ if (!w)
+ return;
+
+ SET_FOREACH(route, w->routes)
+ if (route_get(NULL, link, route, &existing) >= 0)
+ route_unmark(existing);
+}
+
+int link_drop_foreign_routes(Link *link) {
+ Route *route;
+ int k, r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+
+ SET_FOREACH(route, link->routes) {
+ /* do not touch routes managed by the kernel */
+ if (route_by_kernel(route))
+ continue;
+
+ /* Do not remove routes we configured. */
+ if (route->source != NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ /* Ignore routes not assigned yet or already removed. */
+ if (!route_exists(route))
+ continue;
+
+ if (route->protocol == RTPROT_STATIC &&
+ FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_STATIC))
+ continue;
+
+ if (route->protocol == RTPROT_DHCP &&
+ FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP))
+ continue;
+
+ route_mark(route);
+ }
+
+ HASHMAP_FOREACH(route, link->network->routes_by_section) {
+ _cleanup_(converted_routes_freep) ConvertedRoutes *converted = NULL;
+ Route *existing;
+
+ r = route_convert(link->manager, route, &converted);
+ if (r < 0)
+ continue;
+ if (r == 0) {
+ if (route_get(NULL, link, route, &existing) >= 0)
+ route_unmark(existing);
+ continue;
+ }
+
+ for (size_t i = 0; i < converted->n; i++)
+ if (route_get(NULL, link, converted->routes[i], &existing) >= 0)
+ route_unmark(existing);
+ }
+
+ link_unmark_wireguard_routes(link);
+
+ r = 0;
+ SET_FOREACH(route, link->routes) {
+ if (!route_is_marked(route))
+ continue;
+
+ k = route_remove(route);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ manager_mark_routes(link->manager, /* foreign = */ true, NULL);
+
+ k = manager_drop_marked_routes(link->manager);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ return r;
+}
+
+int link_drop_managed_routes(Link *link) {
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(route, link->routes) {
+ /* do not touch routes managed by the kernel */
+ if (route_by_kernel(route))
+ continue;
+
+ /* Do not touch routes managed by kernel or other tools. */
+ if (route->source == NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ if (!route_exists(route))
+ continue;
+
+ k = route_remove(route);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ manager_mark_routes(link->manager, /* foreign = */ false, link);
+
+ k = manager_drop_marked_routes(link->manager);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ return r;
+}
+
+void link_foreignize_routes(Link *link) {
+ Route *route;
+
+ assert(link);
+
+ SET_FOREACH(route, link->routes)
+ route->source = NETWORK_CONFIG_SOURCE_FOREIGN;
+
+ manager_mark_routes(link->manager, /* foreign = */ false, link);
+
+ SET_FOREACH(route, link->manager->routes) {
+ if (!route_is_marked(route))
+ continue;
+
+ route->source = NETWORK_CONFIG_SOURCE_FOREIGN;
+ }
+}
+
+static int route_expire_handler(sd_event_source *s, uint64_t usec, void *userdata) {
+ Route *route = ASSERT_PTR(userdata);
+ Link *link;
+ int r;
+
+ assert(route->manager || (route->link && route->link->manager));
+
+ link = route->link; /* This may be NULL. */
+
+ r = route_remove(route);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not remove route: %m");
+ if (link)
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+static int route_setup_timer(Route *route, const struct rta_cacheinfo *cacheinfo) {
+ Manager *manager;
+ int r;
+
+ assert(route);
+ assert(route->manager || (route->link && route->link->manager));
+
+ manager = route->manager ?: route->link->manager;
+
+ if (route->lifetime_usec == USEC_INFINITY)
+ return 0;
+
+ if (cacheinfo && cacheinfo->rta_expires != 0)
+ /* Assume that non-zero rta_expires means kernel will handle the route expiration. */
+ return 0;
+
+ r = event_reset_time(manager->event, &route->expire, CLOCK_BOOTTIME,
+ route->lifetime_usec, 0, route_expire_handler, route, 0, "route-expiration", true);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int append_nexthop_one(const Link *link, const Route *route, const MultipathRoute *m, struct rtattr **rta, size_t offset) {
+ struct rtnexthop *rtnh;
+ struct rtattr *new_rta;
+ int r;
+
+ assert(route);
+ assert(m);
+ assert(rta);
+ assert(*rta);
+
+ new_rta = realloc(*rta, RTA_ALIGN((*rta)->rta_len) + RTA_SPACE(sizeof(struct rtnexthop)));
+ if (!new_rta)
+ return -ENOMEM;
+ *rta = new_rta;
+
+ rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset);
+ *rtnh = (struct rtnexthop) {
+ .rtnh_len = sizeof(*rtnh),
+ .rtnh_ifindex = m->ifindex > 0 ? m->ifindex : link->ifindex,
+ .rtnh_hops = m->weight,
+ };
+
+ (*rta)->rta_len += sizeof(struct rtnexthop);
+
+ if (route->family == m->gateway.family) {
+ r = rtattr_append_attribute(rta, RTA_GATEWAY, &m->gateway.address, FAMILY_ADDRESS_SIZE(m->gateway.family));
+ if (r < 0)
+ goto clear;
+ rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset);
+ rtnh->rtnh_len += RTA_SPACE(FAMILY_ADDRESS_SIZE(m->gateway.family));
+ } else {
+ r = rtattr_append_attribute(rta, RTA_VIA, &m->gateway, FAMILY_ADDRESS_SIZE(m->gateway.family) + sizeof(m->gateway.family));
+ if (r < 0)
+ goto clear;
+ rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset);
+ rtnh->rtnh_len += RTA_SPACE(FAMILY_ADDRESS_SIZE(m->gateway.family) + sizeof(m->gateway.family));
+ }
+
+ return 0;
+
+clear:
+ (*rta)->rta_len -= sizeof(struct rtnexthop);
+ return r;
+}
+
+static int append_nexthops(const Link *link, const Route *route, sd_netlink_message *req) {
+ _cleanup_free_ struct rtattr *rta = NULL;
+ struct rtnexthop *rtnh;
+ MultipathRoute *m;
+ size_t offset;
+ int r;
+
+ assert(link);
+ assert(route);
+ assert(req);
+
+ if (ordered_set_isempty(route->multipath_routes))
+ return 0;
+
+ rta = new(struct rtattr, 1);
+ if (!rta)
+ return -ENOMEM;
+
+ *rta = (struct rtattr) {
+ .rta_type = RTA_MULTIPATH,
+ .rta_len = RTA_LENGTH(0),
+ };
+ offset = (uint8_t *) RTA_DATA(rta) - (uint8_t *) rta;
+
+ ORDERED_SET_FOREACH(m, route->multipath_routes) {
+ r = append_nexthop_one(link, route, m, &rta, offset);
+ if (r < 0)
+ return r;
+
+ rtnh = (struct rtnexthop *)((uint8_t *) rta + offset);
+ offset = (uint8_t *) RTNH_NEXT(rtnh) - (uint8_t *) rta;
+ }
+
+ r = sd_netlink_message_append_data(req, RTA_MULTIPATH, RTA_DATA(rta), RTA_PAYLOAD(rta));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int route_configure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(error_msg);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set route");
+ link_enter_failed(link);
+ return 0;
+ }
+
+ return 1;
+}
+
+static int route_configure(const Route *route, uint32_t lifetime_sec, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(route);
+ assert(IN_SET(route->family, AF_INET, AF_INET6));
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+ assert(req);
+
+ log_route_debug(route, "Configuring", link, link->manager);
+
+ r = sd_rtnl_message_new_route(link->manager->rtnl, &m, RTM_NEWROUTE, route->family, route->protocol);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_route_set_type(m, route->type);
+ if (r < 0)
+ return r;
+
+ r = route_set_netlink_message(route, m, link);
+ if (r < 0)
+ return r;
+
+ if (lifetime_sec != UINT32_MAX) {
+ r = sd_netlink_message_append_u32(m, RTA_EXPIRES, lifetime_sec);
+ if (r < 0)
+ return r;
+ }
+
+ if (route->ttl_propagate >= 0) {
+ r = sd_netlink_message_append_u8(m, RTA_TTL_PROPAGATE, route->ttl_propagate);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_open_container(m, RTA_METRICS);
+ if (r < 0)
+ return r;
+
+ if (route->mtu > 0) {
+ r = sd_netlink_message_append_u32(m, RTAX_MTU, route->mtu);
+ if (r < 0)
+ return r;
+ }
+
+ if (route->initcwnd > 0) {
+ r = sd_netlink_message_append_u32(m, RTAX_INITCWND, route->initcwnd);
+ if (r < 0)
+ return r;
+ }
+
+ if (route->initrwnd > 0) {
+ r = sd_netlink_message_append_u32(m, RTAX_INITRWND, route->initrwnd);
+ if (r < 0)
+ return r;
+ }
+
+ if (route->quickack >= 0) {
+ r = sd_netlink_message_append_u32(m, RTAX_QUICKACK, route->quickack);
+ if (r < 0)
+ return r;
+ }
+
+ if (route->fast_open_no_cookie >= 0) {
+ r = sd_netlink_message_append_u32(m, RTAX_FASTOPEN_NO_COOKIE, route->fast_open_no_cookie);
+ if (r < 0)
+ return r;
+ }
+
+ if (route->advmss > 0) {
+ r = sd_netlink_message_append_u32(m, RTAX_ADVMSS, route->advmss);
+ if (r < 0)
+ return r;
+ }
+
+ if (!isempty(route->tcp_congestion_control_algo)) {
+ r = sd_netlink_message_append_string(m, RTAX_CC_ALGO, route->tcp_congestion_control_algo);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return r;
+
+ if (!ordered_set_isempty(route->multipath_routes)) {
+ assert(route->nexthop_id == 0);
+ assert(!in_addr_is_set(route->gw_family, &route->gw));
+
+ r = append_nexthops(link, route, m);
+ if (r < 0)
+ return r;
+ }
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static int route_is_ready_to_configure(const Route *route, Link *link) {
+ int r;
+
+ assert(route);
+ assert(link);
+
+ if (!link_is_ready_to_configure(link, false))
+ return false;
+
+ if (set_size(link->routes) >= routes_max())
+ return false;
+
+ if (route->nexthop_id > 0) {
+ struct nexthop_grp *nhg;
+ NextHop *nh;
+
+ if (manager_get_nexthop_by_id(link->manager, route->nexthop_id, &nh) < 0)
+ return false;
+
+ if (!nexthop_exists(nh))
+ return false;
+
+ HASHMAP_FOREACH(nhg, nh->group) {
+ NextHop *g;
+
+ if (manager_get_nexthop_by_id(link->manager, nhg->id, &g) < 0)
+ return false;
+
+ if (!nexthop_exists(g))
+ return false;
+ }
+ }
+
+ if (in_addr_is_set(route->family, &route->prefsrc) > 0) {
+ r = manager_has_address(link->manager, route->family, &route->prefsrc, route->family == AF_INET6);
+ if (r <= 0)
+ return r;
+ }
+
+ if (!gateway_is_ready(link, FLAGS_SET(route->flags, RTNH_F_ONLINK), route->gw_family, &route->gw))
+ return false;
+
+ MultipathRoute *m;
+ ORDERED_SET_FOREACH(m, route->multipath_routes) {
+ union in_addr_union a = m->gateway.address;
+ Link *l = NULL;
+
+ r = multipath_route_get_link(link->manager, m, &l);
+ if (r < 0)
+ return false;
+ if (r > 0) {
+ if (!link_is_ready_to_configure(l, /* allow_unmanaged = */ true) ||
+ !link_has_carrier(l))
+ return false;
+
+ m->ifindex = l->ifindex;
+ }
+
+ if (!gateway_is_ready(l ?: link, FLAGS_SET(route->flags, RTNH_F_ONLINK), m->gateway.family, &a))
+ return false;
+ }
+
+ return true;
+}
+
+static int route_process_request(Request *req, Link *link, Route *route) {
+ _cleanup_(converted_routes_freep) ConvertedRoutes *converted = NULL;
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(link->manager);
+ assert(route);
+
+ r = route_is_ready_to_configure(route, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to check if route is ready to configure: %m");
+ if (r == 0)
+ return 0;
+
+ if (route_needs_convert(route)) {
+ r = route_convert(link->manager, route, &converted);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to convert route: %m");
+
+ assert(r > 0);
+ assert(converted);
+
+ for (size_t i = 0; i < converted->n; i++) {
+ Route *existing;
+
+ if (route_get(link->manager, converted->links[i] ?: link, converted->routes[i], &existing) < 0) {
+ _cleanup_(route_freep) Route *tmp = NULL;
+
+ r = route_dup(converted->routes[i], &tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = route_add(link->manager, converted->links[i] ?: link, tmp);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to add route: %m");
+
+ TAKE_PTR(tmp);
+ } else {
+ existing->source = converted->routes[i]->source;
+ existing->provider = converted->routes[i]->provider;
+ }
+ }
+ }
+
+ usec_t now_usec;
+ assert_se(sd_event_now(link->manager->event, CLOCK_BOOTTIME, &now_usec) >= 0);
+ uint32_t sec = usec_to_sec(route->lifetime_usec, now_usec);
+ if (sec == 0) {
+ log_link_debug(link, "Refuse to configure %s route with zero lifetime.",
+ network_config_source_to_string(route->source));
+
+ if (converted)
+ for (size_t i = 0; i < converted->n; i++) {
+ Route *existing;
+
+ assert_se(route_get(link->manager, converted->links[i] ?: link, converted->routes[i], &existing) >= 0);
+ route_cancel_requesting(existing);
+ }
+ else
+ route_cancel_requesting(route);
+
+ return 1;
+ }
+
+ r = route_configure(route, sec, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure route: %m");
+
+ if (converted)
+ for (size_t i = 0; i < converted->n; i++) {
+ Route *existing;
+
+ assert_se(route_get(link->manager, converted->links[i] ?: link, converted->routes[i], &existing) >= 0);
+ route_enter_configuring(existing);
+ }
+ else
+ route_enter_configuring(route);
+
+ return 1;
+}
+
+int link_request_route(
+ Link *link,
+ Route *route,
+ bool consume_object,
+ unsigned *message_counter,
+ route_netlink_handler_t netlink_handler,
+ Request **ret) {
+
+ Route *existing = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(route);
+ assert(route->source != NETWORK_CONFIG_SOURCE_FOREIGN);
+ assert(!route_needs_convert(route));
+
+ (void) route_get(link->manager, link, route, &existing);
+
+ if (route->lifetime_usec == 0) {
+ if (consume_object)
+ route_free(route);
+
+ /* The requested route is outdated. Let's remove it. */
+ return route_remove_and_drop(existing);
+ }
+
+ if (!existing) {
+ _cleanup_(route_freep) Route *tmp = NULL;
+
+ if (consume_object)
+ tmp = route;
+ else {
+ r = route_dup(route, &tmp);
+ if (r < 0)
+ return r;
+ }
+
+ r = route_add(link->manager, link, tmp);
+ if (r < 0)
+ return r;
+
+ existing = TAKE_PTR(tmp);
+ } else {
+ existing->source = route->source;
+ existing->provider = route->provider;
+ existing->lifetime_usec = route->lifetime_usec;
+ if (consume_object)
+ route_free(route);
+
+ if (existing->expire) {
+ /* When re-configuring an existing route, kernel does not send RTM_NEWROUTE
+ * message, so we need to update the timer here. */
+ r = route_setup_timer(existing, NULL);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to update expiration timer for route, ignoring: %m");
+ if (r > 0)
+ log_route_debug(existing, "Updated expiration timer for", link, link->manager);
+ }
+ }
+
+ log_route_debug(existing, "Requesting", link, link->manager);
+ r = link_queue_request_safe(link, REQUEST_TYPE_ROUTE,
+ existing, NULL,
+ route_hash_func,
+ route_compare_func,
+ route_process_request,
+ message_counter, netlink_handler, ret);
+ if (r <= 0)
+ return r;
+
+ route_enter_requesting(existing);
+ return 1;
+}
+
+static int static_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) {
+ int r;
+
+ assert(link);
+
+ r = route_configure_handler_internal(rtnl, m, link, "Could not set route");
+ if (r <= 0)
+ return r;
+
+ if (link->static_route_messages == 0) {
+ log_link_debug(link, "Routes set");
+ link->static_routes_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int link_request_static_route(Link *link, Route *route) {
+ assert(link);
+ assert(link->manager);
+ assert(route);
+
+ if (!route_needs_convert(route))
+ return link_request_route(link, route, false, &link->static_route_messages,
+ static_route_handler, NULL);
+
+ log_route_debug(route, "Requesting", link, link->manager);
+ return link_queue_request_safe(link, REQUEST_TYPE_ROUTE,
+ route, NULL, route_hash_func, route_compare_func,
+ route_process_request,
+ &link->static_route_messages, static_route_handler, NULL);
+}
+
+static int link_request_wireguard_routes(Link *link, bool only_ipv4) {
+ NetDev *netdev;
+ Wireguard *w;
+ Route *route;
+ int r;
+
+ assert(link);
+
+ if (!streq_ptr(link->kind, "wireguard"))
+ return 0;
+
+ if (netdev_get(link->manager, link->ifname, &netdev) < 0)
+ return 0;
+
+ w = WIREGUARD(netdev);
+ if (!w)
+ return 0;
+
+ SET_FOREACH(route, w->routes) {
+ if (only_ipv4 && route->family != AF_INET)
+ continue;
+
+ r = link_request_static_route(link, route);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int link_request_static_routes(Link *link, bool only_ipv4) {
+ Route *route;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->static_routes_configured = false;
+
+ HASHMAP_FOREACH(route, link->network->routes_by_section) {
+ if (route->gateway_from_dhcp_or_ra)
+ continue;
+
+ if (only_ipv4 && route->family != AF_INET)
+ continue;
+
+ r = link_request_static_route(link, route);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_request_wireguard_routes(link, only_ipv4);
+ if (r < 0)
+ return r;
+
+ if (link->static_route_messages == 0) {
+ link->static_routes_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Requesting routes");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+void route_cancel_request(Route *route, Link *link) {
+ Request req;
+
+ assert(route);
+
+ link = route->link ?: link;
+
+ assert(link);
+
+ if (!route_is_requesting(route))
+ return;
+
+ req = (Request) {
+ .link = link,
+ .type = REQUEST_TYPE_ROUTE,
+ .userdata = route,
+ .hash_func = (hash_func_t) route_hash_func,
+ .compare_func = (compare_func_t) route_compare_func,
+ };
+
+ request_detach(link->manager, &req);
+ route_cancel_requesting(route);
+}
+
+static int process_route_one(
+ Manager *manager,
+ Link *link,
+ uint16_t type,
+ Route *in,
+ const struct rta_cacheinfo *cacheinfo) {
+
+ _cleanup_(route_freep) Route *tmp = in;
+ Route *route = NULL;
+ int r;
+
+ assert(manager);
+ assert(tmp);
+ assert(IN_SET(type, RTM_NEWROUTE, RTM_DELROUTE));
+
+ /* link may be NULL. This consumes 'in'. */
+
+ (void) route_get(manager, link, tmp, &route);
+
+ switch (type) {
+ case RTM_NEWROUTE:
+ if (route) {
+ route->flags = tmp->flags;
+ route_enter_configured(route);
+ log_route_debug(route, "Received remembered", link, manager);
+
+ r = route_setup_timer(route, cacheinfo);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to configure expiration timer for route, ignoring: %m");
+ if (r > 0)
+ log_route_debug(route, "Configured expiration timer for", link, manager);
+
+ } else if (!manager->manage_foreign_routes) {
+ route_enter_configured(tmp);
+ log_route_debug(tmp, "Ignoring received", link, manager);
+
+ } else {
+ /* A route appeared that we did not request */
+ route_enter_configured(tmp);
+ log_route_debug(tmp, "Received new", link, manager);
+ r = route_add(manager, link, tmp);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to remember foreign route, ignoring: %m");
+ return 0;
+ }
+ TAKE_PTR(tmp);
+ }
+
+ break;
+
+ case RTM_DELROUTE:
+ if (route) {
+ route_enter_removed(route);
+ if (route->state == 0) {
+ log_route_debug(route, "Forgetting", link, manager);
+ route_free(route);
+ } else
+ log_route_debug(route, "Removed", link, manager);
+ } else
+ log_route_debug(tmp,
+ manager->manage_foreign_routes ? "Kernel removed unknown" : "Ignoring received",
+ link, manager);
+
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+int manager_rtnl_process_route(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(converted_routes_freep) ConvertedRoutes *converted = NULL;
+ _cleanup_(route_freep) Route *tmp = NULL;
+ _cleanup_free_ void *rta_multipath = NULL;
+ struct rta_cacheinfo cacheinfo;
+ bool has_cacheinfo;
+ Link *link = NULL;
+ uint32_t ifindex;
+ uint16_t type;
+ size_t rta_len;
+ int r;
+
+ assert(rtnl);
+ assert(message);
+ assert(m);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "rtnl: failed to receive route message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get message type, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(type, RTM_NEWROUTE, RTM_DELROUTE)) {
+ log_warning("rtnl: received unexpected message type %u when processing route, ignoring.", type);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, RTA_OIF, &ifindex);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get ifindex from route message, ignoring: %m");
+ return 0;
+ } else if (r >= 0) {
+ if (ifindex <= 0) {
+ log_warning("rtnl: received route message with invalid ifindex %u, ignoring.", ifindex);
+ return 0;
+ }
+
+ r = link_get_by_index(m, ifindex, &link);
+ if (r < 0 || !link) {
+ /* when enumerating we might be out of sync, but we will
+ * get the route again, so just ignore it */
+ if (!m->enumerating)
+ log_warning("rtnl: received route message for link (%u) we do not know about, ignoring", ifindex);
+ return 0;
+ }
+ }
+
+ r = route_new(&tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_rtnl_message_route_get_family(message, &tmp->family);
+ if (r < 0) {
+ log_link_warning(link, "rtnl: received route message without family, ignoring");
+ return 0;
+ } else if (!IN_SET(tmp->family, AF_INET, AF_INET6)) {
+ log_link_debug(link, "rtnl: received route message with invalid family '%i', ignoring", tmp->family);
+ return 0;
+ }
+
+ r = sd_rtnl_message_route_get_protocol(message, &tmp->protocol);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: received route message without route protocol, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_route_get_flags(message, &tmp->flags);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: received route message without route flags, ignoring: %m");
+ return 0;
+ }
+
+ r = netlink_message_read_in_addr_union(message, RTA_DST, tmp->family, &tmp->dst);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid destination, ignoring: %m");
+ return 0;
+ }
+
+ r = netlink_message_read_in_addr_union(message, RTA_GATEWAY, tmp->family, &tmp->gw);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid gateway, ignoring: %m");
+ return 0;
+ } else if (r >= 0)
+ tmp->gw_family = tmp->family;
+ else if (tmp->family == AF_INET) {
+ RouteVia via;
+
+ r = sd_netlink_message_read(message, RTA_VIA, sizeof(via), &via);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid gateway, ignoring: %m");
+ return 0;
+ } else if (r >= 0) {
+ tmp->gw_family = via.family;
+ tmp->gw = via.address;
+ }
+ }
+
+ r = netlink_message_read_in_addr_union(message, RTA_SRC, tmp->family, &tmp->src);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid source, ignoring: %m");
+ return 0;
+ }
+
+ r = netlink_message_read_in_addr_union(message, RTA_PREFSRC, tmp->family, &tmp->prefsrc);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid preferred source, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_route_get_dst_prefixlen(message, &tmp->dst_prefixlen);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid destination prefixlen, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_route_get_src_prefixlen(message, &tmp->src_prefixlen);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid source prefixlen, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_route_get_scope(message, &tmp->scope);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid scope, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_route_get_tos(message, &tmp->tos);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid tos, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_route_get_type(message, &tmp->type);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid type, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, RTA_TABLE, &tmp->table);
+ if (r == -ENODATA) {
+ unsigned char table;
+
+ r = sd_rtnl_message_route_get_table(message, &table);
+ if (r >= 0)
+ tmp->table = table;
+ }
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid table, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, RTA_PRIORITY, &tmp->priority);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid priority, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, RTA_NH_ID, &tmp->nexthop_id);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid nexthop id, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_enter_container(message, RTA_METRICS);
+ if (r < 0 && r != -ENODATA) {
+ log_link_error_errno(link, r, "rtnl: Could not enter RTA_METRICS container, ignoring: %m");
+ return 0;
+ }
+ if (r >= 0) {
+ r = sd_netlink_message_read_u32(message, RTAX_INITCWND, &tmp->initcwnd);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid initcwnd, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, RTAX_INITRWND, &tmp->initrwnd);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid initrwnd, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, RTAX_ADVMSS, &tmp->advmss);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid advmss, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_exit_container(message);
+ if (r < 0) {
+ log_link_error_errno(link, r, "rtnl: Could not exit from RTA_METRICS container, ignoring: %m");
+ return 0;
+ }
+ }
+
+ r = sd_netlink_message_read_data(message, RTA_MULTIPATH, &rta_len, &rta_multipath);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: failed to read RTA_MULTIPATH attribute, ignoring: %m");
+ return 0;
+ } else if (r >= 0) {
+ r = rtattr_read_nexthop(rta_multipath, rta_len, tmp->family, &tmp->multipath_routes);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: failed to parse RTA_MULTIPATH attribute, ignoring: %m");
+ return 0;
+ }
+ }
+
+ r = sd_netlink_message_read(message, RTA_CACHEINFO, sizeof(cacheinfo), &cacheinfo);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: failed to read RTA_CACHEINFO attribute, ignoring: %m");
+ return 0;
+ }
+ has_cacheinfo = r >= 0;
+
+ /* IPv6 routes with reject type are always assigned to the loopback interface. See kernel's
+ * fib6_nh_init() in net/ipv6/route.c. However, we'd like to manage them by Manager. Hence, set
+ * link to NULL here. */
+ if (route_type_is_reject(tmp))
+ link = NULL;
+
+ if (!route_needs_convert(tmp))
+ return process_route_one(m, link, type, TAKE_PTR(tmp), has_cacheinfo ? &cacheinfo : NULL);
+
+ r = route_convert(m, tmp, &converted);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: failed to convert received route, ignoring: %m");
+ return 0;
+ }
+
+ assert(r > 0);
+ assert(converted);
+
+ for (size_t i = 0; i < converted->n; i++)
+ (void) process_route_one(m,
+ converted->links[i] ?: link,
+ type,
+ TAKE_PTR(converted->routes[i]),
+ has_cacheinfo ? &cacheinfo : NULL);
+
+ return 1;
+}
+
+int network_add_ipv4ll_route(Network *network) {
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ unsigned section_line;
+ int r;
+
+ assert(network);
+
+ if (!network->ipv4ll_route)
+ return 0;
+
+ section_line = hashmap_find_free_section_line(network->routes_by_section);
+
+ /* IPv4LLRoute= is in [Network] section. */
+ r = route_new_static(network, network->filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ r = in_addr_from_string(AF_INET, "169.254.0.0", &n->dst);
+ if (r < 0)
+ return r;
+
+ n->family = AF_INET;
+ n->dst_prefixlen = 16;
+ n->scope = RT_SCOPE_LINK;
+ n->scope_set = true;
+ n->table_set = true;
+ n->priority = IPV4LL_ROUTE_METRIC;
+ n->protocol = RTPROT_STATIC;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int network_add_default_route_on_device(Network *network) {
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ unsigned section_line;
+ int r;
+
+ assert(network);
+
+ if (!network->default_route_on_device)
+ return 0;
+
+ section_line = hashmap_find_free_section_line(network->routes_by_section);
+
+ /* DefaultRouteOnDevice= is in [Network] section. */
+ r = route_new_static(network, network->filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ n->family = AF_INET;
+ n->scope = RT_SCOPE_LINK;
+ n->scope_set = true;
+ n->protocol = RTPROT_STATIC;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_gateway(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(section, "Network")) {
+ /* we are not in an Route section, so use line number instead */
+ r = route_new_static(network, filename, line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+ } else {
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (isempty(rvalue)) {
+ n->gateway_from_dhcp_or_ra = false;
+ n->gw_family = AF_UNSPEC;
+ n->gw = IN_ADDR_NULL;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ if (streq(rvalue, "_dhcp")) {
+ n->gateway_from_dhcp_or_ra = true;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ if (streq(rvalue, "_dhcp4")) {
+ n->gw_family = AF_INET;
+ n->gateway_from_dhcp_or_ra = true;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ if (streq(rvalue, "_ipv6ra")) {
+ n->gw_family = AF_INET6;
+ n->gateway_from_dhcp_or_ra = true;
+ TAKE_PTR(n);
+ return 0;
+ }
+ }
+
+ r = in_addr_from_string_auto(rvalue, &n->gw_family, &n->gw);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ n->gateway_from_dhcp_or_ra = false;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_preferred_src(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (n->family == AF_UNSPEC)
+ r = in_addr_from_string_auto(rvalue, &n->family, &n->prefsrc);
+ else
+ r = in_addr_from_string(n->family, rvalue, &n->prefsrc);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, EINVAL,
+ "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_destination(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ union in_addr_union *buffer;
+ unsigned char *prefixlen;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (streq(lvalue, "Destination")) {
+ buffer = &n->dst;
+ prefixlen = &n->dst_prefixlen;
+ } else if (streq(lvalue, "Source")) {
+ buffer = &n->src;
+ prefixlen = &n->src_prefixlen;
+ } else
+ assert_not_reached();
+
+ if (n->family == AF_UNSPEC)
+ r = in_addr_prefix_from_string_auto(rvalue, &n->family, buffer, prefixlen);
+ else
+ r = in_addr_prefix_from_string(rvalue, n->family, buffer, prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, EINVAL,
+ "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ (void) in_addr_mask(n->family, buffer, *prefixlen);
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_route_priority(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &n->priority);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ n->priority_set = true;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_route_scope(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = route_scope_from_string(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Unknown route scope: %s", rvalue);
+ return 0;
+ }
+
+ n->scope = r;
+ n->scope_set = true;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_route_nexthop(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ uint32_t id;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (isempty(rvalue)) {
+ n->nexthop_id = 0;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &id);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse nexthop ID, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (id == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid nexthop ID, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ n->nexthop_id = id;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_route_table(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = manager_get_route_table_from_string(network->manager, rvalue, &n->table);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse route table \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ n->table_set = true;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_route_boolean(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse %s=\"%s\", ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ if (STR_IN_SET(lvalue, "GatewayOnLink", "GatewayOnlink"))
+ n->gateway_onlink = r;
+ else if (streq(lvalue, "QuickAck"))
+ n->quickack = r;
+ else if (streq(lvalue, "FastOpenNoCookie"))
+ n->fast_open_no_cookie = r;
+ else if (streq(lvalue, "TTLPropagate"))
+ n->ttl_propagate = r;
+ else
+ assert_not_reached();
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_ipv6_route_preference(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (streq(rvalue, "low"))
+ n->pref = ICMPV6_ROUTER_PREF_LOW;
+ else if (streq(rvalue, "medium"))
+ n->pref = ICMPV6_ROUTER_PREF_MEDIUM;
+ else if (streq(rvalue, "high"))
+ n->pref = ICMPV6_ROUTER_PREF_HIGH;
+ else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Unknown route preference: %s", rvalue);
+ return 0;
+ }
+
+ n->pref_set = true;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_route_protocol(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = route_protocol_from_string(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse route protocol \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ n->protocol = r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_route_type(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int t, r;
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ t = route_type_from_string(rvalue);
+ if (t < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse route type \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ n->type = (unsigned char) t;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_tcp_congestion(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = config_parse_string(unit, filename, line, section, section_line, lvalue, ltype,
+ rvalue, &n->tcp_congestion_control_algo, userdata);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_tcp_advmss(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ Network *network = userdata;
+ uint64_t u;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (isempty(rvalue)) {
+ n->advmss = 0;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1024, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse TCPAdvertisedMaximumSegmentSize= \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (u == 0 || u > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid TCPAdvertisedMaximumSegmentSize= \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ n->advmss = u;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_tcp_window(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ Network *network = userdata;
+ uint32_t k;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse TCP %s \"%s\", ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+ if (k >= 1024) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified TCP %s \"%s\" is too large, ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+ if (k == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid TCP %s \"%s\", ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "InitialCongestionWindow"))
+ n->initcwnd = k;
+ else if (streq(lvalue, "InitialAdvertisedReceiveWindow"))
+ n->initrwnd = k;
+ else
+ assert_not_reached();
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_route_mtu(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = config_parse_mtu(unit, filename, line, section, section_line, lvalue, ltype, rvalue, &n->mtu, userdata);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_multipath_route(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(multipath_route_freep) MultipathRoute *m = NULL;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ _cleanup_free_ char *word = NULL;
+ Network *network = userdata;
+ union in_addr_union a;
+ int family, r;
+ const char *p;
+ char *dev;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (isempty(rvalue)) {
+ n->multipath_routes = ordered_set_free_with_destructor(n->multipath_routes, multipath_route_free);
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ m = new0(MultipathRoute, 1);
+ if (!m)
+ return log_oom();
+
+ p = rvalue;
+ r = extract_first_word(&p, &word, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r <= 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid multipath route option, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ dev = strchr(word, '@');
+ if (dev) {
+ *dev++ = '\0';
+
+ r = parse_ifindex(dev);
+ if (r > 0)
+ m->ifindex = r;
+ else {
+ if (!ifname_valid_full(dev, IFNAME_VALID_ALTERNATIVE)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid interface name '%s' in %s=, ignoring: %s", dev, lvalue, rvalue);
+ return 0;
+ }
+
+ m->ifname = strdup(dev);
+ if (!m->ifname)
+ return log_oom();
+ }
+ }
+
+ r = in_addr_from_string_auto(word, &family, &a);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid multipath route gateway '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+ m->gateway.address = a;
+ m->gateway.family = family;
+
+ if (!isempty(p)) {
+ r = safe_atou32(p, &m->weight);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid multipath route weight, ignoring assignment: %s", p);
+ return 0;
+ }
+ /* ip command takes weight in the range 1…255, while kernel takes the value in the
+ * range 0…254. MultiPathRoute= setting also takes weight in the same range which ip
+ * command uses, then networkd decreases by one and stores it to match the range which
+ * kernel uses. */
+ if (m->weight == 0 || m->weight > 256) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid multipath route weight, ignoring assignment: %s", p);
+ return 0;
+ }
+ m->weight--;
+ }
+
+ r = ordered_set_ensure_put(&n->multipath_routes, NULL, m);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store multipath route, ignoring assignment: %m");
+ return 0;
+ }
+
+ TAKE_PTR(m);
+ TAKE_PTR(n);
+ return 0;
+}
+
+static int route_section_verify(Route *route, Network *network) {
+ if (section_is_invalid(route->section))
+ return -EINVAL;
+
+ /* Currently, we do not support static route with finite lifetime. */
+ assert(route->lifetime_usec == USEC_INFINITY);
+
+ if (route->gateway_from_dhcp_or_ra) {
+ if (route->gw_family == AF_UNSPEC) {
+ /* When deprecated Gateway=_dhcp is set, then assume gateway family based on other settings. */
+ switch (route->family) {
+ case AF_UNSPEC:
+ log_warning("%s: Deprecated value \"_dhcp\" is specified for Gateway= in [Route] section from line %u. "
+ "Please use \"_dhcp4\" or \"_ipv6ra\" instead. Assuming \"_dhcp4\".",
+ route->section->filename, route->section->line);
+ route->family = AF_INET;
+ break;
+ case AF_INET:
+ case AF_INET6:
+ log_warning("%s: Deprecated value \"_dhcp\" is specified for Gateway= in [Route] section from line %u. "
+ "Assuming \"%s\" based on Destination=, Source=, or PreferredSource= setting.",
+ route->section->filename, route->section->line, route->family == AF_INET ? "_dhcp4" : "_ipv6ra");
+ break;
+ default:
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Invalid route family. Ignoring [Route] section from line %u.",
+ route->section->filename, route->section->line);
+ }
+ route->gw_family = route->family;
+ }
+
+ if (route->gw_family == AF_INET && !FLAGS_SET(network->dhcp, ADDRESS_FAMILY_IPV4))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Gateway=\"_dhcp4\" is specified but DHCPv4 client is disabled. "
+ "Ignoring [Route] section from line %u.",
+ route->section->filename, route->section->line);
+
+ if (route->gw_family == AF_INET6 && !network->ipv6_accept_ra)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Gateway=\"_ipv6ra\" is specified but IPv6AcceptRA= is disabled. "
+ "Ignoring [Route] section from line %u.",
+ route->section->filename, route->section->line);
+ }
+
+ /* When only Gateway= is specified, assume the route family based on the Gateway address. */
+ if (route->family == AF_UNSPEC)
+ route->family = route->gw_family;
+
+ if (route->family == AF_UNSPEC) {
+ assert(route->section);
+
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Route section without Gateway=, Destination=, Source=, "
+ "or PreferredSource= field configured. "
+ "Ignoring [Route] section from line %u.",
+ route->section->filename, route->section->line);
+ }
+
+ if (route->family == AF_INET6 && route->gw_family == AF_INET)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: IPv4 gateway is configured for IPv6 route. "
+ "Ignoring [Route] section from line %u.",
+ route->section->filename, route->section->line);
+
+ if (!route->table_set && network->vrf) {
+ route->table = VRF(network->vrf)->table;
+ route->table_set = true;
+ }
+
+ if (!route->table_set && IN_SET(route->type, RTN_LOCAL, RTN_BROADCAST, RTN_ANYCAST, RTN_NAT))
+ route->table = RT_TABLE_LOCAL;
+
+ if (!route->scope_set && route->family != AF_INET6) {
+ if (IN_SET(route->type, RTN_LOCAL, RTN_NAT))
+ route->scope = RT_SCOPE_HOST;
+ else if (IN_SET(route->type, RTN_BROADCAST, RTN_ANYCAST, RTN_MULTICAST))
+ route->scope = RT_SCOPE_LINK;
+ else if (IN_SET(route->type, RTN_UNICAST, RTN_UNSPEC) &&
+ !route->gateway_from_dhcp_or_ra &&
+ !in_addr_is_set(route->gw_family, &route->gw) &&
+ ordered_set_isempty(route->multipath_routes) &&
+ route->nexthop_id == 0)
+ route->scope = RT_SCOPE_LINK;
+ }
+
+ if (route->scope != RT_SCOPE_UNIVERSE && route->family == AF_INET6) {
+ log_warning("%s: Scope= is specified for IPv6 route. It will be ignored.", route->section->filename);
+ route->scope = RT_SCOPE_UNIVERSE;
+ }
+
+ if (route->family == AF_INET6 && route->priority == 0)
+ route->priority = IP6_RT_PRIO_USER;
+
+ if (route->gateway_onlink < 0 && in_addr_is_set(route->gw_family, &route->gw) &&
+ ordered_hashmap_isempty(network->addresses_by_section)) {
+ /* If no address is configured, in most cases the gateway cannot be reachable.
+ * TODO: we may need to improve the condition above. */
+ log_warning("%s: Gateway= without static address configured. "
+ "Enabling GatewayOnLink= option.",
+ network->filename);
+ route->gateway_onlink = true;
+ }
+
+ if (route->gateway_onlink >= 0)
+ SET_FLAG(route->flags, RTNH_F_ONLINK, route->gateway_onlink);
+
+ if (route->family == AF_INET6) {
+ MultipathRoute *m;
+
+ ORDERED_SET_FOREACH(m, route->multipath_routes)
+ if (m->gateway.family == AF_INET)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: IPv4 multipath route is specified for IPv6 route. "
+ "Ignoring [Route] section from line %u.",
+ route->section->filename, route->section->line);
+ }
+
+ if ((route->gateway_from_dhcp_or_ra ||
+ in_addr_is_set(route->gw_family, &route->gw)) &&
+ !ordered_set_isempty(route->multipath_routes))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Gateway= cannot be specified with MultiPathRoute=. "
+ "Ignoring [Route] section from line %u.",
+ route->section->filename, route->section->line);
+
+ if (route->nexthop_id > 0 &&
+ (route->gateway_from_dhcp_or_ra ||
+ in_addr_is_set(route->gw_family, &route->gw) ||
+ !ordered_set_isempty(route->multipath_routes)))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: NextHopId= cannot be specified with Gateway= or MultiPathRoute=. "
+ "Ignoring [Route] section from line %u.",
+ route->section->filename, route->section->line);
+
+ return 0;
+}
+
+void network_drop_invalid_routes(Network *network) {
+ Route *route;
+
+ assert(network);
+
+ HASHMAP_FOREACH(route, network->routes_by_section)
+ if (route_section_verify(route, network) < 0)
+ route_free(route);
+}
diff --git a/src/network/networkd-route.h b/src/network/networkd-route.h
new file mode 100644
index 0000000..5fc76b1
--- /dev/null
+++ b/src/network/networkd-route.h
@@ -0,0 +1,130 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "networkd-link.h"
+#include "networkd-util.h"
+
+typedef struct Manager Manager;
+typedef struct Network Network;
+typedef struct Request Request;
+typedef struct Route Route;
+typedef int (*route_netlink_handler_t)(
+ sd_netlink *rtnl,
+ sd_netlink_message *m,
+ Request *req,
+ Link *link,
+ Route *route);
+
+struct Route {
+ Link *link;
+ Manager *manager;
+ Network *network;
+ ConfigSection *section;
+ NetworkConfigSource source;
+ NetworkConfigState state;
+ union in_addr_union provider; /* DHCP server or router address */
+
+ int family;
+ int gw_family;
+ uint32_t gw_weight;
+ int quickack;
+ int fast_open_no_cookie;
+ int ttl_propagate;
+
+ unsigned char dst_prefixlen;
+ unsigned char src_prefixlen;
+ unsigned char scope;
+ unsigned char protocol; /* RTPROT_* */
+ unsigned char type; /* RTN_* */
+ unsigned char tos;
+ uint32_t priority; /* note that ip(8) calls this 'metric' */
+ uint32_t table;
+ uint32_t mtu;
+ uint32_t initcwnd;
+ uint32_t initrwnd;
+ uint32_t advmss;
+ char *tcp_congestion_control_algo;
+ unsigned char pref;
+ unsigned flags;
+ int gateway_onlink; /* Only used in conf parser and route_section_verify(). */
+ uint32_t nexthop_id;
+
+ bool scope_set:1;
+ bool table_set:1;
+ bool priority_set:1;
+ bool protocol_set:1;
+ bool pref_set:1;
+ bool gateway_from_dhcp_or_ra:1;
+
+ union in_addr_union gw;
+ union in_addr_union dst;
+ union in_addr_union src;
+ union in_addr_union prefsrc;
+ OrderedSet *multipath_routes;
+
+ /* This is an absolute point in time, and NOT a timespan/duration.
+ * Must be specified with clock_boottime_or_monotonic(). */
+ usec_t lifetime_usec;
+ /* Used when kernel does not support RTA_EXPIRES attribute. */
+ sd_event_source *expire;
+};
+
+extern const struct hash_ops route_hash_ops;
+
+int route_new(Route **ret);
+Route *route_free(Route *route);
+DEFINE_SECTION_CLEANUP_FUNCTIONS(Route, route_free);
+int route_dup(const Route *src, Route **ret);
+
+int route_configure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg);
+int route_remove(Route *route);
+int route_remove_and_drop(Route *route);
+
+int route_get(Manager *manager, Link *link, const Route *in, Route **ret);
+
+int link_drop_managed_routes(Link *link);
+int link_drop_foreign_routes(Link *link);
+void link_foreignize_routes(Link *link);
+
+void route_cancel_request(Route *route, Link *link);
+int link_request_route(
+ Link *link,
+ Route *route,
+ bool consume_object,
+ unsigned *message_counter,
+ route_netlink_handler_t netlink_handler,
+ Request **ret);
+int link_request_static_routes(Link *link, bool only_ipv4);
+
+int manager_rtnl_process_route(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+int network_add_ipv4ll_route(Network *network);
+int network_add_default_route_on_device(Network *network);
+void network_drop_invalid_routes(Network *network);
+
+DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(Route, route);
+void link_mark_routes(Link *link, NetworkConfigSource source);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_gateway);
+CONFIG_PARSER_PROTOTYPE(config_parse_preferred_src);
+CONFIG_PARSER_PROTOTYPE(config_parse_destination);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_priority);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_scope);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_table);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_boolean);
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_route_preference);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_protocol);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_tcp_window);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_mtu);
+CONFIG_PARSER_PROTOTYPE(config_parse_multipath_route);
+CONFIG_PARSER_PROTOTYPE(config_parse_tcp_congestion);
+CONFIG_PARSER_PROTOTYPE(config_parse_tcp_advmss);
+CONFIG_PARSER_PROTOTYPE(config_parse_route_nexthop);
diff --git a/src/network/networkd-routing-policy-rule.c b/src/network/networkd-routing-policy-rule.c
new file mode 100644
index 0000000..130ed5e
--- /dev/null
+++ b/src/network/networkd-routing-policy-rule.c
@@ -0,0 +1,1756 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <linux/fib_rules.h>
+
+#include "af-list.h"
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "hashmap.h"
+#include "ip-protocol-list.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "networkd-queue.h"
+#include "networkd-route-util.h"
+#include "networkd-routing-policy-rule.h"
+#include "networkd-util.h"
+#include "parse-util.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+static const char *const fr_act_type_table[__FR_ACT_MAX] = {
+ [FR_ACT_BLACKHOLE] = "blackhole",
+ [FR_ACT_UNREACHABLE] = "unreachable",
+ [FR_ACT_PROHIBIT] = "prohibit",
+};
+
+static const char *const fr_act_type_full_table[__FR_ACT_MAX] = {
+ [FR_ACT_TO_TBL] = "table",
+ [FR_ACT_GOTO] = "goto",
+ [FR_ACT_NOP] = "nop",
+ [FR_ACT_BLACKHOLE] = "blackhole",
+ [FR_ACT_UNREACHABLE] = "unreachable",
+ [FR_ACT_PROHIBIT] = "prohibit",
+};
+
+assert_cc(__FR_ACT_MAX <= UINT8_MAX);
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(fr_act_type, int);
+DEFINE_STRING_TABLE_LOOKUP_TO_STRING(fr_act_type_full, int);
+
+RoutingPolicyRule *routing_policy_rule_free(RoutingPolicyRule *rule) {
+ if (!rule)
+ return NULL;
+
+ if (rule->network) {
+ assert(rule->section);
+ hashmap_remove(rule->network->rules_by_section, rule->section);
+ }
+
+ if (rule->manager)
+ set_remove(rule->manager->rules, rule);
+
+ config_section_free(rule->section);
+ free(rule->iif);
+ free(rule->oif);
+
+ return mfree(rule);
+}
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(RoutingPolicyRule, routing_policy_rule_free);
+
+static int routing_policy_rule_new(RoutingPolicyRule **ret) {
+ RoutingPolicyRule *rule;
+
+ rule = new(RoutingPolicyRule, 1);
+ if (!rule)
+ return -ENOMEM;
+
+ *rule = (RoutingPolicyRule) {
+ .table = RT_TABLE_MAIN,
+ .uid_range.start = UID_INVALID,
+ .uid_range.end = UID_INVALID,
+ .suppress_prefixlen = -1,
+ .suppress_ifgroup = -1,
+ .protocol = RTPROT_UNSPEC,
+ .type = FR_ACT_TO_TBL,
+ };
+
+ *ret = rule;
+ return 0;
+}
+
+static int routing_policy_rule_new_static(Network *network, const char *filename, unsigned section_line, RoutingPolicyRule **ret) {
+ _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *rule = NULL;
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ rule = hashmap_get(network->rules_by_section, n);
+ if (rule) {
+ *ret = TAKE_PTR(rule);
+ return 0;
+ }
+
+ r = routing_policy_rule_new(&rule);
+ if (r < 0)
+ return r;
+
+ rule->network = network;
+ rule->section = TAKE_PTR(n);
+ rule->source = NETWORK_CONFIG_SOURCE_STATIC;
+ rule->protocol = RTPROT_STATIC;
+
+ r = hashmap_ensure_put(&network->rules_by_section, &config_section_hash_ops, rule->section, rule);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(rule);
+ return 0;
+}
+
+static int routing_policy_rule_dup(const RoutingPolicyRule *src, RoutingPolicyRule **ret) {
+ _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *dest = NULL;
+
+ assert(src);
+ assert(ret);
+
+ dest = newdup(RoutingPolicyRule, src, 1);
+ if (!dest)
+ return -ENOMEM;
+
+ /* Unset all pointers */
+ dest->manager = NULL;
+ dest->network = NULL;
+ dest->section = NULL;
+ dest->iif = dest->oif = NULL;
+
+ if (src->iif) {
+ dest->iif = strdup(src->iif);
+ if (!dest->iif)
+ return -ENOMEM;
+ }
+
+ if (src->oif) {
+ dest->oif = strdup(src->oif);
+ if (!dest->oif)
+ return -ENOMEM;
+ }
+
+ *ret = TAKE_PTR(dest);
+ return 0;
+}
+
+static void routing_policy_rule_hash_func(const RoutingPolicyRule *rule, struct siphash *state) {
+ assert(rule);
+
+ siphash24_compress(&rule->family, sizeof(rule->family), state);
+
+ switch (rule->family) {
+ case AF_INET:
+ case AF_INET6:
+ siphash24_compress(&rule->from, FAMILY_ADDRESS_SIZE(rule->family), state);
+ siphash24_compress(&rule->from_prefixlen, sizeof(rule->from_prefixlen), state);
+
+ siphash24_compress(&rule->to, FAMILY_ADDRESS_SIZE(rule->family), state);
+ siphash24_compress(&rule->to_prefixlen, sizeof(rule->to_prefixlen), state);
+
+ siphash24_compress_boolean(rule->invert_rule, state);
+
+ siphash24_compress(&rule->tos, sizeof(rule->tos), state);
+ siphash24_compress(&rule->type, sizeof(rule->type), state);
+ siphash24_compress(&rule->fwmark, sizeof(rule->fwmark), state);
+ siphash24_compress(&rule->fwmask, sizeof(rule->fwmask), state);
+ siphash24_compress(&rule->priority, sizeof(rule->priority), state);
+ siphash24_compress(&rule->table, sizeof(rule->table), state);
+ siphash24_compress(&rule->suppress_prefixlen, sizeof(rule->suppress_prefixlen), state);
+ siphash24_compress(&rule->suppress_ifgroup, sizeof(rule->suppress_ifgroup), state);
+
+ siphash24_compress(&rule->ipproto, sizeof(rule->ipproto), state);
+ siphash24_compress(&rule->protocol, sizeof(rule->protocol), state);
+ siphash24_compress(&rule->sport, sizeof(rule->sport), state);
+ siphash24_compress(&rule->dport, sizeof(rule->dport), state);
+ siphash24_compress(&rule->uid_range, sizeof(rule->uid_range), state);
+
+ siphash24_compress_string(rule->iif, state);
+ siphash24_compress_string(rule->oif, state);
+
+ break;
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ break;
+ }
+}
+
+static int routing_policy_rule_compare_func(const RoutingPolicyRule *a, const RoutingPolicyRule *b) {
+ int r;
+
+ r = CMP(a->family, b->family);
+ if (r != 0)
+ return r;
+
+ switch (a->family) {
+ case AF_INET:
+ case AF_INET6:
+ r = CMP(a->from_prefixlen, b->from_prefixlen);
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a->from, &b->from, FAMILY_ADDRESS_SIZE(a->family));
+ if (r != 0)
+ return r;
+
+ r = CMP(a->to_prefixlen, b->to_prefixlen);
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a->to, &b->to, FAMILY_ADDRESS_SIZE(a->family));
+ if (r != 0)
+ return r;
+
+ r = CMP(a->invert_rule, b->invert_rule);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->tos, b->tos);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->type, b->type);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->fwmark, b->fwmark);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->fwmask, b->fwmask);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->priority, b->priority);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->table, b->table);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->suppress_prefixlen, b->suppress_prefixlen);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->suppress_ifgroup, b->suppress_ifgroup);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->ipproto, b->ipproto);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->protocol, b->protocol);
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a->sport, &b->sport, sizeof(a->sport));
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a->dport, &b->dport, sizeof(a->dport));
+ if (r != 0)
+ return r;
+
+ r = memcmp(&a->uid_range, &b->uid_range, sizeof(a->uid_range));
+ if (r != 0)
+ return r;
+
+ r = strcmp_ptr(a->iif, b->iif);
+ if (r != 0)
+ return r;
+
+ r = strcmp_ptr(a->oif, b->oif);
+ if (r != 0)
+ return r;
+
+ return 0;
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ return 0;
+ }
+}
+
+static bool routing_policy_rule_equal(const RoutingPolicyRule *rule1, const RoutingPolicyRule *rule2) {
+ if (rule1 == rule2)
+ return true;
+
+ if (!rule1 || !rule2)
+ return false;
+
+ return routing_policy_rule_compare_func(rule1, rule2) == 0;
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ routing_policy_rule_hash_ops,
+ RoutingPolicyRule,
+ routing_policy_rule_hash_func,
+ routing_policy_rule_compare_func,
+ routing_policy_rule_free);
+
+static int routing_policy_rule_get(Manager *m, const RoutingPolicyRule *in, RoutingPolicyRule **ret) {
+ RoutingPolicyRule *rule;
+
+ assert(m);
+ assert(in);
+
+ rule = set_get(m->rules, in);
+ if (rule) {
+ if (ret)
+ *ret = rule;
+ return 0;
+ }
+
+ if (in->priority_set)
+ return -ENOENT;
+
+ /* Also find rules configured without priority. */
+ SET_FOREACH(rule, m->rules) {
+ uint32_t priority;
+ bool found;
+
+ if (rule->priority_set)
+ /* The rule is configured with priority. */
+ continue;
+
+ priority = rule->priority;
+ rule->priority = 0;
+ found = routing_policy_rule_equal(rule, in);
+ rule->priority = priority;
+
+ if (found) {
+ if (ret)
+ *ret = rule;
+ return 0;
+ }
+ }
+
+ return -ENOENT;
+}
+
+static int routing_policy_rule_add(Manager *m, RoutingPolicyRule *rule) {
+ int r;
+
+ assert(m);
+ assert(rule);
+ assert(IN_SET(rule->family, AF_INET, AF_INET6));
+
+ r = set_ensure_put(&m->rules, &routing_policy_rule_hash_ops, rule);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ rule->manager = m;
+ return 0;
+}
+
+static int routing_policy_rule_acquire_priority(Manager *manager, RoutingPolicyRule *rule) {
+ _cleanup_set_free_ Set *priorities = NULL;
+ RoutingPolicyRule *tmp;
+ uint32_t priority;
+ Network *network;
+ int r;
+
+ assert(manager);
+ assert(rule);
+ assert(IN_SET(rule->family, AF_INET, AF_INET6));
+
+ if (rule->priority_set)
+ return 0;
+
+ /* Find the highest unused priority. Note that 32766 is already used by kernel.
+ * See kernel_rules[] below. */
+
+ SET_FOREACH(tmp, manager->rules) {
+ if (tmp->family != rule->family)
+ continue;
+ if (tmp->priority == 0 || tmp->priority > 32765)
+ continue;
+ r = set_ensure_put(&priorities, NULL, UINT32_TO_PTR(tmp->priority));
+ if (r < 0)
+ return r;
+ }
+
+ ORDERED_HASHMAP_FOREACH(network, manager->networks)
+ HASHMAP_FOREACH(tmp, network->rules_by_section) {
+ if (tmp->family != AF_UNSPEC && tmp->family != rule->family)
+ continue;
+ if (!tmp->priority_set)
+ continue;
+ if (tmp->priority == 0 || tmp->priority > 32765)
+ continue;
+ r = set_ensure_put(&priorities, NULL, UINT32_TO_PTR(tmp->priority));
+ if (r < 0)
+ return r;
+ }
+
+ for (priority = 32765; priority > 0; priority--)
+ if (!set_contains(priorities, UINT32_TO_PTR(priority)))
+ break;
+
+ rule->priority = priority;
+ return 0;
+}
+
+static void log_routing_policy_rule_debug(const RoutingPolicyRule *rule, const char *str, const Link *link, const Manager *m) {
+ _cleanup_free_ char *state = NULL, *table = NULL;
+
+ assert(rule);
+ assert(IN_SET(rule->family, AF_INET, AF_INET6));
+ assert(str);
+ assert(m);
+
+ /* link may be NULL. */
+
+ if (!DEBUG_LOGGING)
+ return;
+
+ (void) network_config_state_to_string_alloc(rule->state, &state);
+ (void) manager_get_route_table_to_string(m, rule->table, &table);
+
+ log_link_debug(link,
+ "%s %s routing policy rule (%s): priority: %"PRIu32", %s -> %s, iif: %s, oif: %s, table: %s",
+ str, strna(network_config_source_to_string(rule->source)), strna(state),
+ rule->priority,
+ IN_ADDR_PREFIX_TO_STRING(rule->family, &rule->from, rule->from_prefixlen),
+ IN_ADDR_PREFIX_TO_STRING(rule->family, &rule->to, rule->to_prefixlen),
+ strna(rule->iif), strna(rule->oif), strna(table));
+}
+
+static int routing_policy_rule_set_netlink_message(const RoutingPolicyRule *rule, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(rule);
+ assert(m);
+
+ /* link may be NULL. */
+
+ if (rule->from_prefixlen > 0) {
+ r = netlink_message_append_in_addr_union(m, FRA_SRC, rule->family, &rule->from);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_routing_policy_rule_set_fib_src_prefixlen(m, rule->from_prefixlen);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->to_prefixlen > 0) {
+ r = netlink_message_append_in_addr_union(m, FRA_DST, rule->family, &rule->to);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_routing_policy_rule_set_fib_dst_prefixlen(m, rule->to_prefixlen);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u32(m, FRA_PRIORITY, rule->priority);
+ if (r < 0)
+ return r;
+
+ if (rule->tos > 0) {
+ r = sd_rtnl_message_routing_policy_rule_set_tos(m, rule->tos);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->table < 256) {
+ r = sd_rtnl_message_routing_policy_rule_set_table(m, rule->table);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_rtnl_message_routing_policy_rule_set_table(m, RT_TABLE_UNSPEC);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, FRA_TABLE, rule->table);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->fwmark > 0) {
+ r = sd_netlink_message_append_u32(m, FRA_FWMARK, rule->fwmark);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u32(m, FRA_FWMASK, rule->fwmask);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->iif) {
+ r = sd_netlink_message_append_string(m, FRA_IIFNAME, rule->iif);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->oif) {
+ r = sd_netlink_message_append_string(m, FRA_OIFNAME, rule->oif);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u8(m, FRA_IP_PROTO, rule->ipproto);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(m, FRA_PROTOCOL, rule->protocol);
+ if (r < 0)
+ return r;
+
+ if (rule->sport.start != 0 || rule->sport.end != 0) {
+ r = sd_netlink_message_append_data(m, FRA_SPORT_RANGE, &rule->sport, sizeof(rule->sport));
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->dport.start != 0 || rule->dport.end != 0) {
+ r = sd_netlink_message_append_data(m, FRA_DPORT_RANGE, &rule->dport, sizeof(rule->dport));
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->uid_range.start != UID_INVALID && rule->uid_range.end != UID_INVALID) {
+ r = sd_netlink_message_append_data(m, FRA_UID_RANGE, &rule->uid_range, sizeof(rule->uid_range));
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->invert_rule) {
+ r = sd_rtnl_message_routing_policy_rule_set_flags(m, FIB_RULE_INVERT);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->suppress_prefixlen >= 0) {
+ r = sd_netlink_message_append_u32(m, FRA_SUPPRESS_PREFIXLEN, (uint32_t) rule->suppress_prefixlen);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule->suppress_ifgroup >= 0) {
+ r = sd_netlink_message_append_u32(m, FRA_SUPPRESS_IFGROUP, (uint32_t) rule->suppress_ifgroup);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_rtnl_message_routing_policy_rule_set_fib_type(m, rule->type);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int routing_policy_rule_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, void *userdata) {
+ int r;
+
+ assert(m);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ log_message_warning_errno(m, r, "Could not drop routing policy rule");
+
+ return 1;
+}
+
+static int routing_policy_rule_remove(RoutingPolicyRule *rule) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(rule);
+ assert(rule->manager);
+ assert(rule->manager->rtnl);
+ assert(IN_SET(rule->family, AF_INET, AF_INET6));
+
+ log_routing_policy_rule_debug(rule, "Removing", NULL, rule->manager);
+
+ r = sd_rtnl_message_new_routing_policy_rule(rule->manager->rtnl, &m, RTM_DELRULE, rule->family);
+ if (r < 0)
+ return log_warning_errno(r, "Could not allocate netlink message: %m");
+
+ r = routing_policy_rule_set_netlink_message(rule, m, NULL);
+ if (r < 0)
+ return log_warning_errno(r, "Could not create netlink message: %m");
+
+ r = netlink_call_async(rule->manager->rtnl, NULL, m,
+ routing_policy_rule_remove_handler,
+ NULL, NULL);
+ if (r < 0)
+ return log_warning_errno(r, "Could not send netlink message: %m");
+
+ routing_policy_rule_enter_removing(rule);
+ return 0;
+}
+
+static int routing_policy_rule_configure(RoutingPolicyRule *rule, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(rule);
+ assert(IN_SET(rule->family, AF_INET, AF_INET6));
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(req);
+
+ log_routing_policy_rule_debug(rule, "Configuring", link, link->manager);
+
+ r = sd_rtnl_message_new_routing_policy_rule(link->manager->rtnl, &m, RTM_NEWRULE, rule->family);
+ if (r < 0)
+ return r;
+
+ r = routing_policy_rule_set_netlink_message(rule, m, link);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static void manager_mark_routing_policy_rules(Manager *m, bool foreign, const Link *except) {
+ RoutingPolicyRule *rule;
+ Link *link;
+
+ assert(m);
+
+ /* First, mark all existing rules. */
+ SET_FOREACH(rule, m->rules) {
+ /* Do not touch rules managed by kernel. */
+ if (rule->protocol == RTPROT_KERNEL)
+ continue;
+
+ /* When 'foreign' is true, mark only foreign rules, and vice versa. */
+ if (foreign != (rule->source == NETWORK_CONFIG_SOURCE_FOREIGN))
+ continue;
+
+ /* Ignore rules not assigned yet or already removing. */
+ if (!routing_policy_rule_exists(rule))
+ continue;
+
+ routing_policy_rule_mark(rule);
+ }
+
+ /* Then, unmark all rules requested by active links. */
+ HASHMAP_FOREACH(link, m->links_by_index) {
+ if (link == except)
+ continue;
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ continue;
+
+ HASHMAP_FOREACH(rule, link->network->rules_by_section) {
+ RoutingPolicyRule *existing;
+
+ if (IN_SET(rule->family, AF_INET, AF_INET6)) {
+ if (routing_policy_rule_get(m, rule, &existing) >= 0)
+ routing_policy_rule_unmark(existing);
+ } else {
+ /* The case Family=both. */
+ rule->family = AF_INET;
+ if (routing_policy_rule_get(m, rule, &existing) >= 0)
+ routing_policy_rule_unmark(existing);
+
+ rule->family = AF_INET6;
+ if (routing_policy_rule_get(m, rule, &existing) >= 0)
+ routing_policy_rule_unmark(existing);
+
+ rule->family = AF_UNSPEC;
+ }
+ }
+ }
+}
+
+int manager_drop_routing_policy_rules_internal(Manager *m, bool foreign, const Link *except) {
+ RoutingPolicyRule *rule;
+ int k, r = 0;
+
+ assert(m);
+
+ manager_mark_routing_policy_rules(m, foreign, except);
+
+ SET_FOREACH(rule, m->rules) {
+ if (!routing_policy_rule_is_marked(rule))
+ continue;
+
+ k = routing_policy_rule_remove(rule);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+void link_foreignize_routing_policy_rules(Link *link) {
+ RoutingPolicyRule *rule;
+
+ assert(link);
+ assert(link->manager);
+
+ manager_mark_routing_policy_rules(link->manager, /* foreign = */ false, link);
+
+ SET_FOREACH(rule, link->manager->rules) {
+ if (!routing_policy_rule_is_marked(rule))
+ continue;
+
+ rule->source = NETWORK_CONFIG_SOURCE_FOREIGN;
+ }
+}
+
+static int routing_policy_rule_process_request(Request *req, Link *link, RoutingPolicyRule *rule) {
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(rule);
+
+ if (!link_is_ready_to_configure(link, false))
+ return 0;
+
+ r = routing_policy_rule_configure(rule, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure routing policy rule: %m");
+
+ routing_policy_rule_enter_configuring(rule);
+ return 1;
+}
+
+static int static_routing_policy_rule_configure_handler(
+ sd_netlink *rtnl,
+ sd_netlink_message *m,
+ Request *req,
+ Link *link,
+ RoutingPolicyRule *rule) {
+
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not add routing policy rule");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->static_routing_policy_rule_messages == 0) {
+ log_link_debug(link, "Routing policy rule configured");
+ link->static_routing_policy_rules_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int link_request_routing_policy_rule(Link *link, RoutingPolicyRule *rule) {
+ RoutingPolicyRule *existing;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(rule);
+ assert(rule->source != NETWORK_CONFIG_SOURCE_FOREIGN);
+
+ if (routing_policy_rule_get(link->manager, rule, &existing) < 0) {
+ _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *tmp = NULL;
+
+ r = routing_policy_rule_dup(rule, &tmp);
+ if (r < 0)
+ return r;
+
+ r = routing_policy_rule_acquire_priority(link->manager, tmp);
+ if (r < 0)
+ return r;
+
+ r = routing_policy_rule_add(link->manager, tmp);
+ if (r < 0)
+ return r;
+
+ existing = TAKE_PTR(tmp);
+ } else
+ existing->source = rule->source;
+
+ log_routing_policy_rule_debug(existing, "Requesting", link, link->manager);
+ r = link_queue_request_safe(link, REQUEST_TYPE_ROUTING_POLICY_RULE,
+ existing, NULL,
+ routing_policy_rule_hash_func,
+ routing_policy_rule_compare_func,
+ routing_policy_rule_process_request,
+ &link->static_routing_policy_rule_messages,
+ static_routing_policy_rule_configure_handler,
+ NULL);
+ if (r <= 0)
+ return r;
+
+ routing_policy_rule_enter_requesting(existing);
+ return 1;
+}
+
+static int link_request_static_routing_policy_rule(Link *link, RoutingPolicyRule *rule) {
+ int r;
+
+ if (IN_SET(rule->family, AF_INET, AF_INET6))
+ return link_request_routing_policy_rule(link, rule);
+
+ rule->family = AF_INET;
+ r = link_request_routing_policy_rule(link, rule);
+ if (r < 0) {
+ rule->family = AF_UNSPEC;
+ return r;
+ }
+
+ rule->family = AF_INET6;
+ r = link_request_routing_policy_rule(link, rule);
+ rule->family = AF_UNSPEC;
+ return r;
+}
+
+int link_request_static_routing_policy_rules(Link *link) {
+ RoutingPolicyRule *rule;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->static_routing_policy_rules_configured = false;
+
+ HASHMAP_FOREACH(rule, link->network->rules_by_section) {
+ r = link_request_static_routing_policy_rule(link, rule);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not request routing policy rule: %m");
+ }
+
+ if (link->static_routing_policy_rule_messages == 0) {
+ link->static_routing_policy_rules_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Requesting routing policy rules");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+static const RoutingPolicyRule kernel_rules[] = {
+ { .family = AF_INET, .priority_set = true, .priority = 0, .table = RT_TABLE_LOCAL, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, },
+ { .family = AF_INET, .priority_set = true, .priority = 32766, .table = RT_TABLE_MAIN, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, },
+ { .family = AF_INET, .priority_set = true, .priority = 32767, .table = RT_TABLE_DEFAULT, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, },
+ { .family = AF_INET6, .priority_set = true, .priority = 0, .table = RT_TABLE_LOCAL, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, },
+ { .family = AF_INET6, .priority_set = true, .priority = 32766, .table = RT_TABLE_MAIN, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, },
+};
+
+static bool routing_policy_rule_is_created_by_kernel(const RoutingPolicyRule *rule) {
+ assert(rule);
+
+ if (rule->l3mdev > 0)
+ /* Currently, [RoutingPolicyRule] does not explicitly set FRA_L3MDEV. So, if the flag
+ * is set, it is safe to treat the rule as created by kernel. */
+ return true;
+
+ for (size_t i = 0; i < ELEMENTSOF(kernel_rules); i++)
+ if (routing_policy_rule_equal(rule, &kernel_rules[i]))
+ return true;
+
+ return false;
+}
+
+int manager_rtnl_process_rule(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *tmp = NULL;
+ RoutingPolicyRule *rule = NULL;
+ bool adjust_protocol = false;
+ uint16_t type;
+ int r;
+
+ assert(rtnl);
+ assert(message);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "rtnl: failed to receive rule message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get message type, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(type, RTM_NEWRULE, RTM_DELRULE)) {
+ log_warning("rtnl: received unexpected message type %u when processing rule, ignoring.", type);
+ return 0;
+ }
+
+ r = routing_policy_rule_new(&tmp);
+ if (r < 0) {
+ log_oom();
+ return 0;
+ }
+
+ r = sd_rtnl_message_get_family(message, &tmp->family);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get rule family, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(tmp->family, AF_INET, AF_INET6)) {
+ log_debug("rtnl: received rule message with invalid family %d, ignoring.", tmp->family);
+ return 0;
+ }
+
+ r = netlink_message_read_in_addr_union(message, FRA_SRC, tmp->family, &tmp->from);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_SRC attribute, ignoring: %m");
+ return 0;
+ } else if (r >= 0) {
+ r = sd_rtnl_message_routing_policy_rule_get_fib_src_prefixlen(message, &tmp->from_prefixlen);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: received rule message without valid source prefix length, ignoring: %m");
+ return 0;
+ }
+ }
+
+ r = netlink_message_read_in_addr_union(message, FRA_DST, tmp->family, &tmp->to);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_DST attribute, ignoring: %m");
+ return 0;
+ } else if (r >= 0) {
+ r = sd_rtnl_message_routing_policy_rule_get_fib_dst_prefixlen(message, &tmp->to_prefixlen);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: received rule message without valid destination prefix length, ignoring: %m");
+ return 0;
+ }
+ }
+
+ unsigned flags;
+ r = sd_rtnl_message_routing_policy_rule_get_flags(message, &flags);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: received rule message without valid flag, ignoring: %m");
+ return 0;
+ }
+ tmp->invert_rule = flags & FIB_RULE_INVERT;
+
+ r = sd_netlink_message_read_u32(message, FRA_FWMARK, &tmp->fwmark);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_FWMARK attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, FRA_FWMASK, &tmp->fwmask);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_FWMASK attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, FRA_PRIORITY, &tmp->priority);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_PRIORITY attribute, ignoring: %m");
+ return 0;
+ }
+ /* The kernel does not send priority if priority is zero. So, the flag below must be always set
+ * even if the message does not contain FRA_PRIORITY. */
+ tmp->priority_set = true;
+
+ r = sd_netlink_message_read_u32(message, FRA_TABLE, &tmp->table);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_TABLE attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_routing_policy_rule_get_tos(message, &tmp->tos);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FIB rule TOS, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_routing_policy_rule_get_fib_type(message, &tmp->type);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FIB rule type, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string_strdup(message, FRA_IIFNAME, &tmp->iif);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_IIFNAME attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string_strdup(message, FRA_OIFNAME, &tmp->oif);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_OIFNAME attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u8(message, FRA_IP_PROTO, &tmp->ipproto);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_IP_PROTO attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u8(message, FRA_PROTOCOL, &tmp->protocol);
+ if (r == -ENODATA)
+ /* If FRA_PROTOCOL is supported by kernel, then the attribute is always appended.
+ * When the received message does not have FRA_PROTOCOL, then we need to adjust the
+ * protocol of the rule later. */
+ adjust_protocol = true;
+ else if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get FRA_PROTOCOL attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u8(message, FRA_L3MDEV, &tmp->l3mdev);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_L3MDEV attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read(message, FRA_SPORT_RANGE, sizeof(tmp->sport), &tmp->sport);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_SPORT_RANGE attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read(message, FRA_DPORT_RANGE, sizeof(tmp->dport), &tmp->dport);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_DPORT_RANGE attribute, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read(message, FRA_UID_RANGE, sizeof(tmp->uid_range), &tmp->uid_range);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_UID_RANGE attribute, ignoring: %m");
+ return 0;
+ }
+
+ uint32_t suppress_prefixlen;
+ r = sd_netlink_message_read_u32(message, FRA_SUPPRESS_PREFIXLEN, &suppress_prefixlen);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_SUPPRESS_PREFIXLEN attribute, ignoring: %m");
+ return 0;
+ }
+ if (r >= 0)
+ tmp->suppress_prefixlen = (int32_t) suppress_prefixlen;
+
+ uint32_t suppress_ifgroup;
+ r = sd_netlink_message_read_u32(message, FRA_SUPPRESS_IFGROUP, &suppress_ifgroup);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_SUPPRESS_IFGROUP attribute, ignoring: %m");
+ return 0;
+ }
+ if (r >= 0)
+ tmp->suppress_ifgroup = (int32_t) suppress_ifgroup;
+
+ if (adjust_protocol)
+ /* As .network files does not have setting to specify protocol, we can assume the
+ * protocol of the received rule is RTPROT_KERNEL or RTPROT_STATIC. */
+ tmp->protocol = routing_policy_rule_is_created_by_kernel(tmp) ? RTPROT_KERNEL : RTPROT_STATIC;
+
+ (void) routing_policy_rule_get(m, tmp, &rule);
+
+ switch (type) {
+ case RTM_NEWRULE:
+ if (rule) {
+ routing_policy_rule_enter_configured(rule);
+ log_routing_policy_rule_debug(rule, "Received remembered", NULL, m);
+ } else if (!m->manage_foreign_rules) {
+ routing_policy_rule_enter_configured(tmp);
+ log_routing_policy_rule_debug(tmp, "Ignoring received", NULL, m);
+ } else {
+ routing_policy_rule_enter_configured(tmp);
+ log_routing_policy_rule_debug(tmp, "Remembering", NULL, m);
+ r = routing_policy_rule_add(m, tmp);
+ if (r < 0) {
+ log_warning_errno(r, "Could not remember foreign rule, ignoring: %m");
+ return 0;
+ }
+ TAKE_PTR(tmp);
+ }
+ break;
+ case RTM_DELRULE:
+ if (rule) {
+ routing_policy_rule_enter_removed(rule);
+ if (rule->state == 0) {
+ log_routing_policy_rule_debug(rule, "Forgetting", NULL, m);
+ routing_policy_rule_free(rule);
+ } else
+ log_routing_policy_rule_debug(rule, "Removed", NULL, m);
+ } else
+ log_routing_policy_rule_debug(tmp, "Kernel removed unknown", NULL, m);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int parse_fwmark_fwmask(const char *s, uint32_t *ret_fwmark, uint32_t *ret_fwmask) {
+ _cleanup_free_ char *fwmark_str = NULL;
+ uint32_t fwmark, fwmask = 0;
+ const char *slash;
+ int r;
+
+ assert(s);
+ assert(ret_fwmark);
+ assert(ret_fwmask);
+
+ slash = strchr(s, '/');
+ if (slash) {
+ fwmark_str = strndup(s, slash - s);
+ if (!fwmark_str)
+ return -ENOMEM;
+ }
+
+ r = safe_atou32(fwmark_str ?: s, &fwmark);
+ if (r < 0)
+ return r;
+
+ if (fwmark > 0) {
+ if (slash) {
+ r = safe_atou32(slash + 1, &fwmask);
+ if (r < 0)
+ return r;
+ } else
+ fwmask = UINT32_MAX;
+ }
+
+ *ret_fwmark = fwmark;
+ *ret_fwmask = fwmask;
+
+ return 0;
+}
+
+int config_parse_routing_policy_rule_tos(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = safe_atou8(rvalue, &n->tos);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule TOS, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_priority(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ n->priority = 0;
+ n->priority_set = false;
+ TAKE_PTR(n);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &n->priority);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule priority, ignoring: %s", rvalue);
+ return 0;
+ }
+ n->priority_set = true;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_table(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = manager_get_route_table_from_string(network->manager, rvalue, &n->table);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse RPDB rule route table \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_fwmark_mask(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_fwmark_fwmask(rvalue, &n->fwmark, &n->fwmask);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule firewall mark or mask, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_prefix(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ union in_addr_union *buffer;
+ uint8_t *prefixlen;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (streq(lvalue, "To")) {
+ buffer = &n->to;
+ prefixlen = &n->to_prefixlen;
+ } else {
+ buffer = &n->from;
+ prefixlen = &n->from_prefixlen;
+ }
+
+ if (n->family == AF_UNSPEC)
+ r = in_addr_prefix_from_string_auto(rvalue, &n->family, buffer, prefixlen);
+ else
+ r = in_addr_prefix_from_string(rvalue, n->family, buffer, prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "RPDB rule prefix is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_device(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (!ifname_valid(rvalue)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid interface name '%s' in %s=, ignoring assignment.", rvalue, lvalue);
+ return 0;
+ }
+
+ r = free_and_strdup(streq(lvalue, "IncomingInterface") ? &n->iif : &n->oif, rvalue);
+ if (r < 0)
+ return log_oom();
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_port_range(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ uint16_t low, high;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_ip_port_range(rvalue, &low, &high);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse routing policy rule port range '%s'", rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "SourcePort")) {
+ n->sport.start = low;
+ n->sport.end = high;
+ } else {
+ n->dport.start = low;
+ n->dport.end = high;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_ip_protocol(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_ip_protocol(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse IP protocol '%s' for routing policy rule, ignoring: %m", rvalue);
+ return 0;
+ }
+
+ n->ipproto = r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_invert(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule invert, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ n->invert_rule = r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_family(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ AddressFamily a;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ a = routing_policy_rule_address_family_from_string(rvalue);
+ if (a < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, a,
+ "Invalid address family '%s', ignoring.", rvalue);
+ return 0;
+ }
+
+ n->address_family = a;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_uid_range(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ uid_t start, end;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = get_user_creds(&rvalue, &start, NULL, NULL, NULL, 0);
+ if (r >= 0)
+ end = start;
+ else {
+ r = parse_uid_range(rvalue, &start, &end);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid uid or uid range '%s', ignoring: %m", rvalue);
+ return 0;
+ }
+ }
+
+ n->uid_range.start = start;
+ n->uid_range.end = end;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_suppress_prefixlen(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = parse_ip_prefix_length(rvalue, &n->suppress_prefixlen);
+ if (r == -ERANGE) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Prefix length outside of valid range 0-128, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule suppress_prefixlen, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_suppress_ifgroup(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int32_t suppress_ifgroup;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ if (isempty(rvalue)) {
+ n->suppress_ifgroup = -1;
+ return 0;
+ }
+
+ r = safe_atoi32(rvalue, &suppress_ifgroup);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse SuppressInterfaceGroup=, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ if (suppress_ifgroup < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Value of SuppressInterfaceGroup= must be in the range 0…2147483647, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ n->suppress_ifgroup = suppress_ifgroup;
+ TAKE_PTR(n);
+ return 0;
+}
+
+int config_parse_routing_policy_rule_type(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL;
+ Network *network = userdata;
+ int r, t;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = routing_policy_rule_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ t = fr_act_type_from_string(rvalue);
+ if (t < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, t,
+ "Could not parse FIB rule type \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ n->type = (uint8_t) t;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
+static int routing_policy_rule_section_verify(RoutingPolicyRule *rule) {
+ if (section_is_invalid(rule->section))
+ return -EINVAL;
+
+ if ((rule->family == AF_INET && FLAGS_SET(rule->address_family, ADDRESS_FAMILY_IPV6)) ||
+ (rule->family == AF_INET6 && FLAGS_SET(rule->address_family, ADDRESS_FAMILY_IPV4)))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: address family specified by Family= conflicts with the address "
+ "specified by To= or From=. Ignoring [RoutingPolicyRule] section from line %u.",
+ rule->section->filename, rule->section->line);
+
+ if (rule->family == AF_UNSPEC) {
+ if (IN_SET(rule->address_family, ADDRESS_FAMILY_IPV4, ADDRESS_FAMILY_NO))
+ rule->family = AF_INET;
+ else if (rule->address_family == ADDRESS_FAMILY_IPV6)
+ rule->family = AF_INET6;
+ /* rule->family can be AF_UNSPEC only when Family=both. */
+ }
+
+ /* Currently, [RoutingPolicyRule] does not have a setting to set FRA_L3MDEV flag. Please also
+ * update routing_policy_rule_is_created_by_kernel() when a new setting which sets the flag is
+ * added in the future. */
+ if (rule->l3mdev > 0)
+ assert_not_reached();
+
+ return 0;
+}
+
+void network_drop_invalid_routing_policy_rules(Network *network) {
+ RoutingPolicyRule *rule;
+
+ assert(network);
+
+ HASHMAP_FOREACH(rule, network->rules_by_section)
+ if (routing_policy_rule_section_verify(rule) < 0)
+ routing_policy_rule_free(rule);
+}
diff --git a/src/network/networkd-routing-policy-rule.h b/src/network/networkd-routing-policy-rule.h
new file mode 100644
index 0000000..b6ce2fa
--- /dev/null
+++ b/src/network/networkd-routing-policy-rule.h
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <linux/fib_rules.h>
+#include <stdbool.h>
+
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+typedef struct Network Network;
+
+typedef struct RoutingPolicyRule {
+ Manager *manager;
+ Network *network;
+ ConfigSection *section;
+ NetworkConfigSource source;
+ NetworkConfigState state;
+
+ bool invert_rule;
+ bool priority_set;
+
+ uint8_t tos;
+ uint8_t type;
+ uint8_t ipproto; /* FRA_IP_PROTO */
+ uint8_t protocol; /* FRA_PROTOCOL */
+ uint8_t to_prefixlen;
+ uint8_t from_prefixlen;
+ uint8_t l3mdev; /* FRA_L3MDEV */
+
+ uint32_t table;
+ uint32_t fwmark;
+ uint32_t fwmask;
+ uint32_t priority;
+
+ AddressFamily address_family; /* Specified by Family= */
+ int family; /* Automatically determined by From= or To= */
+
+ char *iif;
+ char *oif;
+
+ union in_addr_union to;
+ union in_addr_union from;
+
+ struct fib_rule_port_range sport;
+ struct fib_rule_port_range dport;
+ struct fib_rule_uid_range uid_range;
+
+ int suppress_prefixlen;
+ int32_t suppress_ifgroup;
+} RoutingPolicyRule;
+
+const char *fr_act_type_full_to_string(int t) _const_;
+
+RoutingPolicyRule *routing_policy_rule_free(RoutingPolicyRule *rule);
+
+void network_drop_invalid_routing_policy_rules(Network *network);
+
+int link_request_static_routing_policy_rules(Link *link);
+
+int manager_rtnl_process_rule(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+int manager_drop_routing_policy_rules_internal(Manager *m, bool foreign, const Link *except);
+static inline int manager_drop_foreign_routing_policy_rules(Manager *m) {
+ return manager_drop_routing_policy_rules_internal(m, true, NULL);
+}
+static inline int link_drop_managed_routing_policy_rules(Link *link) {
+ assert(link);
+ return manager_drop_routing_policy_rules_internal(link->manager, false, link);
+}
+void link_foreignize_routing_policy_rules(Link *link);
+
+DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(RoutingPolicyRule, routing_policy_rule);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_tos);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_table);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_fwmark_mask);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_prefix);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_priority);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_device);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_port_range);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_ip_protocol);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_invert);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_family);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_uid_range);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_suppress_prefixlen);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_suppress_ifgroup);
+CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_type);
diff --git a/src/network/networkd-setlink.c b/src/network/networkd-setlink.c
new file mode 100644
index 0000000..0886ee9
--- /dev/null
+++ b/src/network/networkd-setlink.c
@@ -0,0 +1,1299 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+#include <linux/if_bridge.h>
+
+#include "missing_network.h"
+#include "netif-util.h"
+#include "netlink-util.h"
+#include "networkd-address.h"
+#include "networkd-can.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-queue.h"
+#include "networkd-setlink.h"
+#include "networkd-sriov.h"
+#include "networkd-wiphy.h"
+
+static int get_link_default_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ return link_getlink_handler_internal(rtnl, m, link, "Failed to sync link information");
+}
+
+static int get_link_master_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ if (get_link_default_handler(rtnl, m, link) > 0)
+ link->master_set = true;
+ return 0;
+}
+
+static int get_link_update_flag_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ assert(link);
+ assert(link->set_flags_messages > 0);
+
+ link->set_flags_messages--;
+
+ return get_link_default_handler(rtnl, m, link);
+}
+
+static int set_link_handler_internal(
+ sd_netlink *rtnl,
+ sd_netlink_message *m,
+ Request *req,
+ Link *link,
+ bool ignore,
+ link_netlink_message_handler_t get_link_handler) {
+
+ int r;
+
+ assert(m);
+ assert(req);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ const char *error_msg;
+
+ error_msg = strjoina("Failed to set ", request_type_to_string(req->type), ignore ? ", ignoring" : "");
+ log_link_message_warning_errno(link, m, r, error_msg);
+
+ if (!ignore)
+ link_enter_failed(link);
+ return 0;
+ }
+
+ log_link_debug(link, "%s set.", request_type_to_string(req->type));
+
+ if (get_link_handler) {
+ r = link_call_getlink(link, get_link_handler);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 0;
+ }
+ }
+
+ if (link->set_link_messages == 0)
+ link_check_ready(link);
+
+ return 1;
+}
+
+static int link_set_addrgen_mode_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ int r;
+
+ r = set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, NULL);
+ if (r <= 0)
+ return r;
+
+ r = link_drop_ipv6ll_addresses(link);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to drop IPv6LL addresses: %m");
+ link_enter_failed(link);
+ }
+
+ return 0;
+}
+
+static int link_set_bond_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, NULL);
+}
+
+static int link_set_bridge_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, NULL);
+}
+
+static int link_set_bridge_vlan_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, NULL);
+}
+
+static int link_set_can_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, NULL);
+}
+
+static int link_set_flags_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, get_link_default_handler);
+}
+
+static int link_set_group_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, NULL);
+}
+
+static int link_set_ipoib_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, NULL);
+}
+
+static int link_set_mac_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, get_link_default_handler);
+}
+
+static int link_set_mac_allow_retry_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EBUSY) {
+ /* Most real network devices refuse to set its hardware address with -EBUSY when its
+ * operstate is not down. See, eth_prepare_mac_addr_change() in net/ethernet/eth.c
+ * of kernel. */
+
+ log_link_message_debug_errno(link, m, r, "Failed to set MAC address, retrying again: %m");
+
+ r = link_request_to_set_mac(link, /* allow_retry = */ false);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 0;
+ }
+
+ return link_set_mac_handler(rtnl, m, req, link, userdata);
+}
+
+static int link_set_master_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, get_link_master_handler);
+}
+
+static int link_unset_master_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ /* Some devices do not support setting master ifindex. Let's ignore error on unsetting master ifindex. */
+ return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, get_link_master_handler);
+}
+
+static int link_set_mtu_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ int r;
+
+ r = set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, get_link_default_handler);
+ if (r <= 0)
+ return r;
+
+ /* The kernel resets ipv6 mtu after changing device mtu;
+ * we must set this here, after we've set device mtu */
+ r = link_set_ipv6_mtu(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to set IPv6 MTU, ignoring: %m");
+
+ return 0;
+}
+
+static int link_configure_fill_message(
+ Link *link,
+ sd_netlink_message *req,
+ RequestType type,
+ void *userdata) {
+ int r;
+
+ switch (type) {
+ case REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE:
+ r = ipv6ll_addrgen_mode_fill_message(req, PTR_TO_UINT8(userdata));
+ if (r < 0)
+ return r;
+ break;
+ case REQUEST_TYPE_SET_LINK_BOND:
+ r = sd_netlink_message_set_flags(req, NLM_F_REQUEST | NLM_F_ACK);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container(req, IFLA_LINKINFO);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container_union(req, IFLA_INFO_DATA, "bond");
+ if (r < 0)
+ return r;
+
+ if (link->network->active_slave) {
+ r = sd_netlink_message_append_u32(req, IFLA_BOND_ACTIVE_SLAVE, link->ifindex);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->primary_slave) {
+ r = sd_netlink_message_append_u32(req, IFLA_BOND_PRIMARY, link->ifindex);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ break;
+ case REQUEST_TYPE_SET_LINK_BRIDGE:
+ r = sd_rtnl_message_link_set_family(req, AF_BRIDGE);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container(req, IFLA_PROTINFO);
+ if (r < 0)
+ return r;
+
+ if (link->network->use_bpdu >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_GUARD, !link->network->use_bpdu);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->hairpin >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MODE, link->network->hairpin);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->isolated >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_ISOLATED, link->network->isolated);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->fast_leave >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_FAST_LEAVE, link->network->fast_leave);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->allow_port_to_be_root >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_PROTECT, !link->network->allow_port_to_be_root);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->unicast_flood >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_UNICAST_FLOOD, link->network->unicast_flood);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->multicast_flood >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MCAST_FLOOD, link->network->multicast_flood);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->multicast_to_unicast >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MCAST_TO_UCAST, link->network->multicast_to_unicast);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->neighbor_suppression >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_NEIGH_SUPPRESS, link->network->neighbor_suppression);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->learning >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_LEARNING, link->network->learning);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->bridge_proxy_arp >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_PROXYARP, link->network->bridge_proxy_arp);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->bridge_proxy_arp_wifi >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_PROXYARP_WIFI, link->network->bridge_proxy_arp_wifi);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->cost != 0) {
+ r = sd_netlink_message_append_u32(req, IFLA_BRPORT_COST, link->network->cost);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->priority != LINK_BRIDGE_PORT_PRIORITY_INVALID) {
+ r = sd_netlink_message_append_u16(req, IFLA_BRPORT_PRIORITY, link->network->priority);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->multicast_router != _MULTICAST_ROUTER_INVALID) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MULTICAST_ROUTER, link->network->multicast_router);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+ break;
+ case REQUEST_TYPE_SET_LINK_BRIDGE_VLAN:
+ r = sd_rtnl_message_link_set_family(req, AF_BRIDGE);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_open_container(req, IFLA_AF_SPEC);
+ if (r < 0)
+ return r;
+
+ if (link->master_ifindex <= 0) {
+ /* master needs BRIDGE_FLAGS_SELF flag */
+ r = sd_netlink_message_append_u16(req, IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF);
+ if (r < 0)
+ return r;
+ }
+
+ r = bridge_vlan_append_info(link, req, link->network->pvid, link->network->br_vid_bitmap, link->network->br_untagged_bitmap);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ break;
+ case REQUEST_TYPE_SET_LINK_CAN:
+ r = can_set_netlink_message(link, req);
+ if (r < 0)
+ return r;
+ break;
+ case REQUEST_TYPE_SET_LINK_FLAGS: {
+ unsigned ifi_change = 0, ifi_flags = 0;
+
+ if (link->network->arp >= 0) {
+ ifi_change |= IFF_NOARP;
+ SET_FLAG(ifi_flags, IFF_NOARP, link->network->arp == 0);
+ }
+
+ if (link->network->multicast >= 0) {
+ ifi_change |= IFF_MULTICAST;
+ SET_FLAG(ifi_flags, IFF_MULTICAST, link->network->multicast);
+ }
+
+ if (link->network->allmulticast >= 0) {
+ ifi_change |= IFF_ALLMULTI;
+ SET_FLAG(ifi_flags, IFF_ALLMULTI, link->network->allmulticast);
+ }
+
+ if (link->network->promiscuous >= 0) {
+ ifi_change |= IFF_PROMISC;
+ SET_FLAG(ifi_flags, IFF_PROMISC, link->network->promiscuous);
+ }
+
+ r = sd_rtnl_message_link_set_flags(req, ifi_flags, ifi_change);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+ case REQUEST_TYPE_SET_LINK_GROUP:
+ r = sd_netlink_message_append_u32(req, IFLA_GROUP, (uint32_t) link->network->group);
+ if (r < 0)
+ return r;
+ break;
+ case REQUEST_TYPE_SET_LINK_MAC:
+ r = netlink_message_append_hw_addr(req, IFLA_ADDRESS, &link->requested_hw_addr);
+ if (r < 0)
+ return r;
+ break;
+ case REQUEST_TYPE_SET_LINK_IPOIB:
+ r = ipoib_set_netlink_message(link, req);
+ if (r < 0)
+ return r;
+ break;
+ case REQUEST_TYPE_SET_LINK_MASTER:
+ r = sd_netlink_message_append_u32(req, IFLA_MASTER, PTR_TO_UINT32(userdata));
+ if (r < 0)
+ return r;
+ break;
+ case REQUEST_TYPE_SET_LINK_MTU:
+ r = sd_netlink_message_append_u32(req, IFLA_MTU, PTR_TO_UINT32(userdata));
+ if (r < 0)
+ return r;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+static int link_configure(Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(req);
+
+ log_link_debug(link, "Setting %s", request_type_to_string(req->type));
+
+ if (req->type == REQUEST_TYPE_SET_LINK_BOND)
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_NEWLINK, link->master_ifindex);
+ else if (IN_SET(req->type, REQUEST_TYPE_SET_LINK_CAN, REQUEST_TYPE_SET_LINK_IPOIB))
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_NEWLINK, link->ifindex);
+ else
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = link_configure_fill_message(link, m, req->type, req->userdata);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static bool netdev_is_ready(NetDev *netdev) {
+ assert(netdev);
+
+ if (netdev->state != NETDEV_STATE_READY)
+ return false;
+ if (netdev->ifindex == 0)
+ return false;
+
+ return true;
+}
+
+static int link_is_ready_to_set_link(Link *link, Request *req) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+ assert(req);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return false;
+
+ switch (req->type) {
+ case REQUEST_TYPE_SET_LINK_BOND:
+ case REQUEST_TYPE_SET_LINK_BRIDGE:
+ if (!link->master_set)
+ return false;
+
+ if (link->network->keep_master && link->master_ifindex <= 0)
+ return false;
+ break;
+
+ case REQUEST_TYPE_SET_LINK_BRIDGE_VLAN:
+ if (!link->master_set)
+ return false;
+
+ if (link->network->keep_master && link->master_ifindex <= 0 && !streq_ptr(link->kind, "bridge"))
+ return false;
+
+ break;
+
+ case REQUEST_TYPE_SET_LINK_CAN:
+ /* Do not check link->set_flgas_messages here, as it is ok even if link->flags
+ * is outdated, and checking the counter causes a deadlock. */
+ if (FLAGS_SET(link->flags, IFF_UP)) {
+ /* The CAN interface must be down to configure bitrate, etc... */
+ r = link_down_now(link);
+ if (r < 0)
+ return r;
+ }
+ break;
+
+ case REQUEST_TYPE_SET_LINK_MAC:
+ if (req->netlink_handler == link_set_mac_handler) {
+ /* This is the second attempt to set hardware address. On the first attempt
+ * req->netlink_handler points to link_set_mac_allow_retry_handler().
+ * The first attempt failed as the interface was up. */
+ r = link_down_now(link);
+ if (r < 0)
+ return r;
+
+ /* If the kind of the link is "bond", we need
+ * set the slave link down as well. */
+ if (streq_ptr(link->kind, "bond")) {
+ r = link_down_slave_links(link);
+ if (r < 0)
+ return r;
+ }
+ }
+ break;
+
+ case REQUEST_TYPE_SET_LINK_MASTER: {
+ uint32_t m = 0;
+ Request req_mac = {
+ .link = link,
+ .type = REQUEST_TYPE_SET_LINK_MAC,
+ };
+
+ if (link->network->batadv) {
+ if (!netdev_is_ready(link->network->batadv))
+ return false;
+ m = link->network->batadv->ifindex;
+ } else if (link->network->bond) {
+ if (ordered_set_contains(link->manager->request_queue, &req_mac))
+ return false;
+ if (!netdev_is_ready(link->network->bond))
+ return false;
+ m = link->network->bond->ifindex;
+
+ /* Do not check link->set_flgas_messages here, as it is ok even if link->flags
+ * is outdated, and checking the counter causes a deadlock. */
+ if (FLAGS_SET(link->flags, IFF_UP)) {
+ /* link must be down when joining to bond master. */
+ r = link_down_now(link);
+ if (r < 0)
+ return r;
+ }
+ } else if (link->network->bridge) {
+ if (ordered_set_contains(link->manager->request_queue, &req_mac))
+ return false;
+ if (!netdev_is_ready(link->network->bridge))
+ return false;
+ m = link->network->bridge->ifindex;
+ } else if (link->network->vrf) {
+ if (!netdev_is_ready(link->network->vrf))
+ return false;
+ m = link->network->vrf->ifindex;
+ }
+
+ req->userdata = UINT32_TO_PTR(m);
+ break;
+ }
+ case REQUEST_TYPE_SET_LINK_MTU: {
+ if (ordered_set_contains(link->manager->request_queue,
+ &(const Request) {
+ .link = link,
+ .type = REQUEST_TYPE_SET_LINK_IPOIB,
+ }))
+ return false;
+
+ /* Changing FD mode may affect MTU. */
+ if (ordered_set_contains(link->manager->request_queue,
+ &(const Request) {
+ .link = link,
+ .type = REQUEST_TYPE_SET_LINK_CAN,
+ }))
+ return false;
+ }
+ default:
+ break;
+ }
+
+ return true;
+}
+
+static int link_process_set_link(Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(req);
+ assert(link);
+
+ r = link_is_ready_to_set_link(link, req);
+ if (r <= 0)
+ return r;
+
+ r = link_configure(link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to set %s", request_type_to_string(req->type));
+
+ return 1;
+}
+
+static int link_request_set_link(
+ Link *link,
+ RequestType type,
+ request_netlink_handler_t netlink_handler,
+ Request **ret) {
+
+ Request *req;
+ int r;
+
+ assert(link);
+
+ r = link_queue_request_full(link, type, NULL, NULL, NULL, NULL,
+ link_process_set_link,
+ &link->set_link_messages,
+ netlink_handler,
+ &req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request to set %s: %m",
+ request_type_to_string(type));
+
+ log_link_debug(link, "Requested to set %s", request_type_to_string(type));
+
+ if (ret)
+ *ret = req;
+ return 0;
+}
+
+int link_request_to_set_addrgen_mode(Link *link) {
+ IPv6LinkLocalAddressGenMode mode;
+ Request *req;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!socket_ipv6_is_supported())
+ return 0;
+
+ mode = link_get_ipv6ll_addrgen_mode(link);
+
+ if (mode == link->ipv6ll_address_gen_mode)
+ return 0;
+
+ /* If the link is already up, then changing the mode by netlink does not take effect until the
+ * link goes down. Hence, we need to reset the interface. However, setting the mode by sysctl
+ * does not need that. Let's use the sysctl interface when the link is already up.
+ * See also issue #22424. */
+ if (mode != IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE &&
+ FLAGS_SET(link->flags, IFF_UP)) {
+ r = link_set_ipv6ll_addrgen_mode(link, mode);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv6 address generation mode, ignoring: %m");
+
+ return 0;
+ }
+
+ r = link_request_set_link(link, REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE,
+ link_set_addrgen_mode_handler,
+ &req);
+ if (r < 0)
+ return r;
+
+ req->userdata = UINT8_TO_PTR(mode);
+ return 0;
+}
+
+int link_request_to_set_bond(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (!link->network->bond) {
+ Link *master;
+
+ if (!link->network->keep_master)
+ return 0;
+
+ if (link_get_master(link, &master) < 0)
+ return 0;
+
+ if (!streq_ptr(master->kind, "bond"))
+ return 0;
+ }
+
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_BOND,
+ link_set_bond_handler, NULL);
+}
+
+int link_request_to_set_bridge(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (!link->network->bridge) {
+ Link *master;
+
+ if (!link->network->keep_master)
+ return 0;
+
+ if (link_get_master(link, &master) < 0)
+ return 0;
+
+ if (!streq_ptr(master->kind, "bridge"))
+ return 0;
+ }
+
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_BRIDGE,
+ link_set_bridge_handler,
+ NULL);
+}
+
+int link_request_to_set_bridge_vlan(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (!link->network->use_br_vlan)
+ return 0;
+
+ if (!link->network->bridge && !streq_ptr(link->kind, "bridge")) {
+ Link *master;
+
+ if (!link->network->keep_master)
+ return 0;
+
+ if (link_get_master(link, &master) < 0)
+ return 0;
+
+ if (!streq_ptr(master->kind, "bridge"))
+ return 0;
+ }
+
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_BRIDGE_VLAN,
+ link_set_bridge_vlan_handler,
+ NULL);
+}
+
+int link_request_to_set_can(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (link->iftype != ARPHRD_CAN)
+ return 0;
+
+ if (!streq_ptr(link->kind, "can"))
+ return 0;
+
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_CAN,
+ link_set_can_handler,
+ NULL);
+}
+
+int link_request_to_set_flags(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (link->network->arp < 0 &&
+ link->network->multicast < 0 &&
+ link->network->allmulticast < 0 &&
+ link->network->promiscuous < 0)
+ return 0;
+
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_FLAGS,
+ link_set_flags_handler,
+ NULL);
+}
+
+int link_request_to_set_group(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (link->network->group < 0)
+ return 0;
+
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_GROUP,
+ link_set_group_handler,
+ NULL);
+}
+
+int link_request_to_set_mac(Link *link, bool allow_retry) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->network->hw_addr.length == 0)
+ return 0;
+
+ link->requested_hw_addr = link->network->hw_addr;
+ r = net_verify_hardware_address(link->ifname, /* is_static = */ true,
+ link->iftype, &link->hw_addr, &link->requested_hw_addr);
+ if (r < 0)
+ return r;
+
+ if (hw_addr_equal(&link->hw_addr, &link->requested_hw_addr))
+ return 0;
+
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_MAC,
+ allow_retry ? link_set_mac_allow_retry_handler : link_set_mac_handler,
+ NULL);
+}
+
+int link_request_to_set_ipoib(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (link->iftype != ARPHRD_INFINIBAND)
+ return 0;
+
+ if (link->network->ipoib_mode < 0 &&
+ link->network->ipoib_umcast < 0)
+ return 0;
+
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_IPOIB,
+ link_set_ipoib_handler,
+ NULL);
+}
+
+int link_request_to_set_master(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (link->network->keep_master) {
+ /* When KeepMaster=yes, BatmanAdvanced=, Bond=, Bridge=, and VRF= are ignored. */
+ link->master_set = true;
+ return 0;
+
+ } else if (link->network->batadv || link->network->bond || link->network->bridge || link->network->vrf) {
+ link->master_set = false;
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_MASTER,
+ link_set_master_handler,
+ NULL);
+
+ } else if (link->master_ifindex != 0) {
+ /* Unset master only when it is set. */
+ link->master_set = false;
+ return link_request_set_link(link, REQUEST_TYPE_SET_LINK_MASTER,
+ link_unset_master_handler,
+ NULL);
+
+ } else {
+ /* Nothing we need to do. */
+ link->master_set = true;
+ return 0;
+ }
+}
+
+int link_request_to_set_mtu(Link *link, uint32_t mtu) {
+ const char *origin;
+ uint32_t min_mtu, max_mtu;
+ Request *req;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ min_mtu = link->min_mtu;
+ origin = "the minimum MTU of the interface";
+ if (link_ipv6_enabled(link)) {
+ /* IPv6 protocol requires a minimum MTU of IPV6_MTU_MIN(1280) bytes on the interface. Bump up
+ * MTU bytes to IPV6_MTU_MIN. */
+ if (min_mtu < IPV6_MIN_MTU) {
+ min_mtu = IPV6_MIN_MTU;
+ origin = "the minimum IPv6 MTU";
+ }
+ if (min_mtu < link->network->ipv6_mtu) {
+ min_mtu = link->network->ipv6_mtu;
+ origin = "the requested IPv6 MTU in IPv6MTUBytes=";
+ }
+ }
+
+ if (mtu < min_mtu) {
+ log_link_warning(link, "Bumping the requested MTU %"PRIu32" to %s (%"PRIu32")",
+ mtu, origin, min_mtu);
+ mtu = min_mtu;
+ }
+
+ max_mtu = link->max_mtu;
+ if (link->iftype == ARPHRD_CAN)
+ /* The maximum MTU may be changed when FD mode is changed.
+ * See https://docs.kernel.org/networking/can.html#can-fd-flexible-data-rate-driver-support
+ * MTU = 16 (CAN_MTU) => Classical CAN device
+ * MTU = 72 (CANFD_MTU) => CAN FD capable device
+ * So, even if the current maximum is 16, we should not reduce the requested value now. */
+ max_mtu = MAX(max_mtu, 72u);
+
+ if (mtu > max_mtu) {
+ log_link_warning(link, "Reducing the requested MTU %"PRIu32" to the interface's maximum MTU %"PRIu32".",
+ mtu, max_mtu);
+ mtu = max_mtu;
+ }
+
+ if (link->mtu == mtu)
+ return 0;
+
+ r = link_request_set_link(link, REQUEST_TYPE_SET_LINK_MTU,
+ link_set_mtu_handler,
+ &req);
+ if (r < 0)
+ return r;
+
+ req->userdata = UINT32_TO_PTR(mtu);
+ return 0;
+}
+
+static bool link_reduces_vlan_mtu(Link *link) {
+ /* See netif_reduces_vlan_mtu() in kernel. */
+ return streq_ptr(link->kind, "macsec");
+}
+
+static uint32_t link_get_requested_mtu_by_stacked_netdevs(Link *link) {
+ uint32_t mtu = 0;
+ NetDev *dev;
+
+ HASHMAP_FOREACH(dev, link->network->stacked_netdevs)
+ if (dev->kind == NETDEV_KIND_VLAN && dev->mtu > 0)
+ /* See vlan_dev_change_mtu() in kernel. */
+ mtu = MAX(mtu, link_reduces_vlan_mtu(link) ? dev->mtu + 4 : dev->mtu);
+
+ else if (dev->kind == NETDEV_KIND_MACVLAN && dev->mtu > mtu)
+ /* See macvlan_change_mtu() in kernel. */
+ mtu = dev->mtu;
+
+ return mtu;
+}
+
+int link_configure_mtu(Link *link) {
+ uint32_t mtu;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->network->mtu > 0)
+ return link_request_to_set_mtu(link, link->network->mtu);
+
+ mtu = link_get_requested_mtu_by_stacked_netdevs(link);
+ if (link->mtu >= mtu)
+ return 0;
+
+ log_link_notice(link, "Bumping MTU bytes from %"PRIu32" to %"PRIu32" because of stacked device. "
+ "If it is not desired, then please explicitly specify MTUBytes= setting.",
+ link->mtu, mtu);
+
+ return link_request_to_set_mtu(link, mtu);
+}
+
+static int link_up_dsa_slave(Link *link) {
+ Link *master;
+ int r;
+
+ assert(link);
+
+ /* For older kernels (specifically, older than 9d5ef190e5615a7b63af89f88c4106a5bc127974, kernel-5.12),
+ * it is necessary to bring up a DSA slave that its master interface is already up. And bringing up
+ * the slave fails with -ENETDOWN. So, let's bring up the master even if it is not managed by us,
+ * and try to bring up the slave after the master becomes up. */
+
+ if (link->dsa_master_ifindex <= 0)
+ return 0;
+
+ if (!streq_ptr(link->driver, "dsa"))
+ return 0;
+
+ if (link_get_by_index(link->manager, link->dsa_master_ifindex, &master) < 0)
+ return 0;
+
+ if (master->state == LINK_STATE_UNMANAGED) {
+ /* If the DSA master interface is unmanaged, then it will never become up.
+ * Let's request to bring up the master. */
+ r = link_request_to_bring_up_or_down(master, /* up = */ true);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_request_to_bring_up_or_down(link, /* up = */ true);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int link_up_or_down_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) {
+ bool on_activate, up;
+ int r;
+
+ assert(m);
+ assert(req);
+ assert(link);
+
+ on_activate = req->type == REQUEST_TYPE_ACTIVATE_LINK;
+ up = PTR_TO_INT(req->userdata);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -ENETDOWN && up && link_up_dsa_slave(link) > 0)
+ log_link_message_debug_errno(link, m, r, "Could not bring up dsa slave, retrying again after dsa master becomes up");
+ else if (r < 0)
+ log_link_message_warning_errno(link, m, r, up ?
+ "Could not bring up interface, ignoring" :
+ "Could not bring down interface, ignoring");
+
+ r = link_call_getlink(link, get_link_update_flag_handler);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 0;
+ }
+
+ link->set_flags_messages++;
+
+ if (on_activate) {
+ link->activated = true;
+ link_check_ready(link);
+ }
+
+ return 0;
+}
+
+static const char *up_or_down(bool up) {
+ return up ? "up" : "down";
+}
+
+static int link_up_or_down(Link *link, bool up, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(req);
+
+ log_link_debug(link, "Bringing link %s", up_or_down(up));
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_rtnl_message_link_set_flags(m, up ? IFF_UP : 0, IFF_UP);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static bool link_is_ready_to_activate_one(Link *link, bool allow_unmanaged) {
+ assert(link);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED, LINK_STATE_UNMANAGED))
+ return false;
+
+ if (!link->network)
+ return allow_unmanaged;
+
+ if (link->set_link_messages > 0)
+ return false;
+
+ return true;
+}
+
+ static bool link_is_ready_to_activate(Link *link, bool up) {
+ assert(link);
+
+ if (!check_ready_for_all_sr_iov_ports(link, /* allow_unmanaged = */ false,
+ link_is_ready_to_activate_one))
+ return false;
+
+ if (up && link_rfkilled(link) > 0)
+ return false;
+
+ return true;
+}
+
+static int link_process_activation(Request *req, Link *link, void *userdata) {
+ bool up = PTR_TO_INT(userdata);
+ int r;
+
+ assert(req);
+ assert(link);
+
+ if (!link_is_ready_to_activate(link, up))
+ return 0;
+
+ r = link_up_or_down(link, up, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to activate link: %m");
+
+ return 1;
+}
+
+int link_request_to_activate(Link *link) {
+ bool up;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ switch (link->network->activation_policy) {
+ case ACTIVATION_POLICY_BOUND:
+ r = link_handle_bound_to_list(link);
+ if (r < 0)
+ return r;
+ _fallthrough_;
+ case ACTIVATION_POLICY_MANUAL:
+ link->activated = true;
+ link_check_ready(link);
+ return 0;
+ case ACTIVATION_POLICY_UP:
+ case ACTIVATION_POLICY_ALWAYS_UP:
+ up = true;
+ break;
+ case ACTIVATION_POLICY_DOWN:
+ case ACTIVATION_POLICY_ALWAYS_DOWN:
+ up = false;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ link->activated = false;
+
+ r = link_queue_request_full(link, REQUEST_TYPE_ACTIVATE_LINK,
+ INT_TO_PTR(up), NULL, NULL, NULL,
+ link_process_activation,
+ &link->set_flags_messages,
+ link_up_or_down_handler, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to request to activate link: %m");
+
+ log_link_debug(link, "Requested to activate link");
+ return 0;
+}
+
+static bool link_is_ready_to_bring_up_or_down(Link *link, bool up) {
+ assert(link);
+
+ if (up && link->dsa_master_ifindex > 0) {
+ Link *master;
+
+ /* The master interface must be up. See comments in link_up_dsa_slave(). */
+
+ if (link_get_by_index(link->manager, link->dsa_master_ifindex, &master) < 0)
+ return false;
+
+ if (!FLAGS_SET(master->flags, IFF_UP))
+ return false;
+ }
+
+ if (link->state == LINK_STATE_UNMANAGED)
+ return true;
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return false;
+
+ if (link->set_link_messages > 0)
+ return false;
+
+ if (!link->activated)
+ return false;
+
+ if (up && link_rfkilled(link) > 0)
+ return false;
+
+ return true;
+}
+
+static int link_process_up_or_down(Request *req, Link *link, void *userdata) {
+ bool up = PTR_TO_INT(userdata);
+ int r;
+
+ assert(req);
+ assert(link);
+
+ if (!link_is_ready_to_bring_up_or_down(link, up))
+ return 0;
+
+ r = link_up_or_down(link, up, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to bring link %s: %m", up_or_down(up));
+
+ return 1;
+}
+
+int link_request_to_bring_up_or_down(Link *link, bool up) {
+ int r;
+
+ assert(link);
+
+ r = link_queue_request_full(link, REQUEST_TYPE_UP_DOWN,
+ INT_TO_PTR(up), NULL, NULL, NULL,
+ link_process_up_or_down,
+ &link->set_flags_messages,
+ link_up_or_down_handler, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request to bring link %s: %m",
+ up_or_down(up));
+
+ log_link_debug(link, "Requested to bring link %s", up_or_down(up));
+ return 0;
+}
+
+static int link_down_now_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->set_flags_messages > 0);
+
+ link->set_flags_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ log_link_message_warning_errno(link, m, r, "Could not bring down interface, ignoring");
+
+ r = link_call_getlink(link, get_link_update_flag_handler);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 0;
+ }
+
+ link->set_flags_messages++;
+ return 0;
+}
+
+int link_down_now(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ log_link_debug(link, "Bringing link down");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_rtnl_message_link_set_flags(req, 0, IFF_UP);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set link flags: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_down_now_handler,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not send rtnetlink message: %m");
+
+ link->set_flags_messages++;
+ link_ref(link);
+ return 0;
+}
+
+int link_down_slave_links(Link *link) {
+ Link *slave;
+ int r;
+
+ assert(link);
+
+ SET_FOREACH(slave, link->slaves) {
+ r = link_down_now(slave);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int link_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ log_link_message_warning_errno(link, m, r, "Could not remove interface, ignoring");
+
+ return 0;
+}
+
+int link_remove(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ log_link_debug(link, "Removing link.");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_DELLINK, link->ifindex);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not allocate RTM_DELLINK message: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_remove_handler,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not send rtnetlink message: %m");
+
+ link_ref(link);
+
+ return 0;
+}
diff --git a/src/network/networkd-setlink.h b/src/network/networkd-setlink.h
new file mode 100644
index 0000000..841e5ee
--- /dev/null
+++ b/src/network/networkd-setlink.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+typedef struct Link Link;
+
+int link_request_to_set_addrgen_mode(Link *link);
+int link_request_to_set_bond(Link *link);
+int link_request_to_set_bridge(Link *link);
+int link_request_to_set_bridge_vlan(Link *link);
+int link_request_to_set_can(Link *link);
+int link_request_to_set_flags(Link *link);
+int link_request_to_set_group(Link *link);
+int link_request_to_set_mac(Link *link, bool allow_retry);
+int link_request_to_set_ipoib(Link *link);
+int link_request_to_set_master(Link *link);
+int link_request_to_set_mtu(Link *link, uint32_t mtu);
+
+int link_configure_mtu(Link *link);
+
+int link_request_to_activate(Link *link);
+
+int link_request_to_bring_up_or_down(Link *link, bool up);
+
+int link_down_now(Link *link);
+int link_down_slave_links(Link *link);
+int link_remove(Link *link);
diff --git a/src/network/networkd-speed-meter.c b/src/network/networkd-speed-meter.c
new file mode 100644
index 0000000..cf8294e
--- /dev/null
+++ b/src/network/networkd-speed-meter.c
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+
+#include "sd-event.h"
+#include "sd-netlink.h"
+
+#include "networkd-link-bus.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-speed-meter.h"
+
+static int process_message(Manager *manager, sd_netlink_message *message) {
+ uint16_t type;
+ int ifindex, r;
+ Link *link;
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0)
+ return r;
+
+ if (type != RTM_NEWLINK)
+ return 0;
+
+ r = sd_rtnl_message_link_get_ifindex(message, &ifindex);
+ if (r < 0)
+ return r;
+
+ r = link_get_by_index(manager, ifindex, &link);
+ if (r < 0)
+ return r;
+
+ link->stats_old = link->stats_new;
+
+ r = sd_netlink_message_read(message, IFLA_STATS64, sizeof link->stats_new, &link->stats_new);
+ if (r < 0)
+ return r;
+
+ link->stats_updated = true;
+
+ return 0;
+}
+
+static int speed_meter_handler(sd_event_source *s, uint64_t usec, void *userdata) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL;
+ Manager *manager = ASSERT_PTR(userdata);
+ usec_t usec_now;
+ Link *link;
+ int r;
+
+ assert(s);
+
+ r = sd_event_now(sd_event_source_get_event(s), CLOCK_MONOTONIC, &usec_now);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_time(s, usec_now + manager->speed_meter_interval_usec);
+ if (r < 0)
+ return r;
+
+ manager->speed_meter_usec_old = manager->speed_meter_usec_new;
+ manager->speed_meter_usec_new = usec_now;
+
+ HASHMAP_FOREACH(link, manager->links_by_index)
+ link->stats_updated = false;
+
+ r = sd_rtnl_message_new_link(manager->rtnl, &req, RTM_GETLINK, 0);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to allocate RTM_GETLINK netlink message, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_set_request_dump(req, true);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to set dump flag, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_call(manager->rtnl, req, 0, &reply);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to call RTM_GETLINK, ignoring: %m");
+ return 0;
+ }
+
+ for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i))
+ (void) process_message(manager, i);
+
+ return 0;
+}
+
+int manager_start_speed_meter(Manager *manager) {
+ _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL;
+ int r;
+
+ assert(manager);
+ assert(manager->event);
+
+ if (!manager->use_speed_meter)
+ return 0;
+
+ r = sd_event_add_time(manager->event, &s, CLOCK_MONOTONIC, 0, 0, speed_meter_handler, manager);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_enabled(s, SD_EVENT_ON);
+ if (r < 0)
+ return r;
+
+ manager->speed_meter_event_source = TAKE_PTR(s);
+ return 0;
+}
diff --git a/src/network/networkd-speed-meter.h b/src/network/networkd-speed-meter.h
new file mode 100644
index 0000000..4dd024b
--- /dev/null
+++ b/src/network/networkd-speed-meter.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+/* Default interval is 10sec. The speed meter periodically make networkd
+ * to be woke up. So, too small interval value is not desired.
+ * We set the minimum value 100msec = 0.1sec. */
+#define SPEED_METER_DEFAULT_TIME_INTERVAL (10 * USEC_PER_SEC)
+#define SPEED_METER_MINIMUM_TIME_INTERVAL (100 * USEC_PER_MSEC)
+
+typedef struct Manager Manager;
+
+int manager_start_speed_meter(Manager *m);
diff --git a/src/network/networkd-sriov.c b/src/network/networkd-sriov.c
new file mode 100644
index 0000000..3e228a7
--- /dev/null
+++ b/src/network/networkd-sriov.c
@@ -0,0 +1,360 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include "device-enumerator-private.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-queue.h"
+#include "networkd-sriov.h"
+
+static int sr_iov_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, SRIOV *sr_iov) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_error_errno(link, m, r, "Could not set up SR-IOV");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->sr_iov_messages == 0) {
+ log_link_debug(link, "SR-IOV configured");
+ link->sr_iov_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int sr_iov_configure(SRIOV *sr_iov, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(sr_iov);
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+ assert(req);
+
+ log_link_debug(link, "Setting SR-IOV virtual function %"PRIu32".", sr_iov->vf);
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sr_iov_set_netlink_message(sr_iov, m);
+ if (r < 0)
+ return r;
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static int sr_iov_process_request(Request *req, Link *link, SRIOV *sr_iov) {
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(sr_iov);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return 0;
+
+ r = sr_iov_configure(sr_iov, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r,
+ "Failed to configure SR-IOV virtual function %"PRIu32": %m",
+ sr_iov->vf);
+
+ return 1;
+}
+
+int link_request_sr_iov_vfs(Link *link) {
+ SRIOV *sr_iov;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->sr_iov_configured = false;
+
+ ORDERED_HASHMAP_FOREACH(sr_iov, link->network->sr_iov_by_section) {
+ r = link_queue_request_safe(link, REQUEST_TYPE_SRIOV,
+ sr_iov, NULL,
+ sr_iov_hash_func,
+ sr_iov_compare_func,
+ sr_iov_process_request,
+ &link->sr_iov_messages,
+ sr_iov_handler,
+ NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r,
+ "Failed to request SR-IOV virtual function %"PRIu32": %m",
+ sr_iov->vf);
+ }
+
+ if (link->sr_iov_messages == 0) {
+ link->sr_iov_configured = true;
+ link_check_ready(link);
+ } else
+ log_link_debug(link, "Configuring SR-IOV");
+
+ return 0;
+}
+
+static int find_ifindex_from_pci_dev_port(sd_device *pci_dev, const char *dev_port) {
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ sd_device *dev;
+ int ifindex, r;
+
+ assert(pci_dev);
+ assert(dev_port);
+
+ r = sd_device_enumerator_new(&e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_allow_uninitialized(e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_parent(e, pci_dev);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_subsystem(e, "net", true);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_sysattr(e, "dev_port", dev_port, true);
+ if (r < 0)
+ return r;
+
+ dev = sd_device_enumerator_get_device_first(e);
+ if (!dev)
+ return -ENODEV; /* no device found */
+
+ if (sd_device_enumerator_get_device_next(e))
+ return -ENXIO; /* multiple devices found */
+
+ r = sd_device_get_ifindex(dev, &ifindex);
+ if (r < 0)
+ return r;
+
+ assert(ifindex > 0);
+ return ifindex;
+}
+
+static int manager_update_sr_iov_ifindices(Manager *manager, int phys_port_ifindex, int virt_port_ifindex) {
+ Link *phys_link = NULL, *virt_link = NULL;
+ int r;
+
+ assert(manager);
+ assert(phys_port_ifindex > 0);
+ assert(virt_port_ifindex > 0);
+
+ /* This sets ifindices only when both interfaces are already managed by us. */
+
+ r = link_get_by_index(manager, phys_port_ifindex, &phys_link);
+ if (r < 0)
+ return r;
+
+ r = link_get_by_index(manager, virt_port_ifindex, &virt_link);
+ if (r < 0)
+ return r;
+
+ /* update VF ifindex in PF */
+ r = set_ensure_put(&phys_link->sr_iov_virt_port_ifindices, NULL, INT_TO_PTR(virt_port_ifindex));
+ if (r < 0)
+ return r;
+
+ log_link_debug(phys_link,
+ "Found SR-IOV VF port %s(%i).",
+ virt_link ? virt_link->ifname : "n/a", virt_port_ifindex);
+
+ /* update PF ifindex in VF */
+ if (virt_link->sr_iov_phys_port_ifindex > 0 && virt_link->sr_iov_phys_port_ifindex != phys_port_ifindex) {
+ Link *old_phys_link;
+
+ if (link_get_by_index(manager, virt_link->sr_iov_phys_port_ifindex, &old_phys_link) >= 0)
+ set_remove(old_phys_link->sr_iov_virt_port_ifindices, INT_TO_PTR(virt_port_ifindex));
+ }
+
+ virt_link->sr_iov_phys_port_ifindex = phys_port_ifindex;
+
+ log_link_debug(virt_link,
+ "Found SR-IOV PF port %s(%i).",
+ phys_link ? phys_link->ifname : "n/a", phys_port_ifindex);
+
+ return 0;
+}
+
+static int link_set_sr_iov_phys_port(Link *link) {
+ _cleanup_(sd_device_unrefp) sd_device *pci_physfn_dev = NULL;
+ const char *dev_port;
+ sd_device *pci_dev;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ if (link->sr_iov_phys_port_ifindex > 0)
+ return 0;
+
+ if (!link->dev)
+ return -ENODEV;
+
+ /* This may return -EINVAL or -ENODEV, instead of -ENOENT, if the device has been removed or is being
+ * removed. Let's map -EINVAL to -ENODEV, as the caller will ignore -ENODEV. */
+ r = sd_device_get_sysattr_value(link->dev, "dev_port", &dev_port);
+ if (r == -EINVAL)
+ return -ENODEV;
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_parent_with_subsystem_devtype(link->dev, "pci", NULL, &pci_dev);
+ if (r < 0)
+ return r;
+
+ r = sd_device_new_child(&pci_physfn_dev, pci_dev, "physfn");
+ if (r < 0)
+ return r;
+
+ r = find_ifindex_from_pci_dev_port(pci_physfn_dev, dev_port);
+ if (r < 0)
+ return r;
+
+ return manager_update_sr_iov_ifindices(link->manager, r, link->ifindex);
+}
+
+static int link_set_sr_iov_virt_ports(Link *link) {
+ const char *dev_port, *name;
+ sd_device *pci_dev, *child;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ set_clear(link->sr_iov_virt_port_ifindices);
+
+ if (!link->dev)
+ return -ENODEV;
+
+ r = sd_device_get_sysattr_value(link->dev, "dev_port", &dev_port);
+ if (r == -EINVAL)
+ return -ENODEV;
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_parent_with_subsystem_devtype(link->dev, "pci", NULL, &pci_dev);
+ if (r < 0)
+ return r;
+
+ FOREACH_DEVICE_CHILD_WITH_SUFFIX(pci_dev, child, name) {
+ const char *n;
+
+ /* Accept name prefixed with "virtfn", but refuse "virtfn" itself. */
+ n = startswith(name, "virtfn");
+ if (isempty(n) || !in_charset(n, DIGITS))
+ continue;
+
+ r = find_ifindex_from_pci_dev_port(child, dev_port);
+ if (r < 0)
+ continue;
+
+ if (manager_update_sr_iov_ifindices(link->manager, link->ifindex, r) < 0)
+ continue;
+ }
+
+ return 0;
+}
+
+int link_set_sr_iov_ifindices(Link *link) {
+ int r;
+
+ assert(link);
+
+ r = link_set_sr_iov_phys_port(link);
+ if (r < 0 && !ERRNO_IS_DEVICE_ABSENT(r))
+ return r;
+
+ r = link_set_sr_iov_virt_ports(link);
+ if (r < 0 && !ERRNO_IS_DEVICE_ABSENT(r))
+ return r;
+
+ return 0;
+}
+
+void link_clear_sr_iov_ifindices(Link *link) {
+ void *v;
+
+ assert(link);
+ assert(link->manager);
+
+ if (link->sr_iov_phys_port_ifindex > 0) {
+ Link *phys_link;
+
+ if (link_get_by_index(link->manager, link->sr_iov_phys_port_ifindex, &phys_link) >= 0)
+ set_remove(phys_link->sr_iov_virt_port_ifindices, INT_TO_PTR(link->ifindex));
+
+ link->sr_iov_phys_port_ifindex = 0;
+ }
+
+ while ((v = set_steal_first(link->sr_iov_virt_port_ifindices))) {
+ Link *virt_link;
+
+ if (link_get_by_index(link->manager, PTR_TO_INT(v), &virt_link) >= 0)
+ virt_link->sr_iov_phys_port_ifindex = 0;
+ }
+}
+
+bool check_ready_for_all_sr_iov_ports(
+ Link *link,
+ bool allow_unmanaged, /* for the main target */
+ bool (check_one)(Link *link, bool allow_unmanaged)) {
+
+ Link *phys_link;
+ void *v;
+
+ assert(link);
+ assert(link->manager);
+ assert(check_one);
+
+ /* Some drivers make VF ports become down when their PF port becomes down, and may fail to configure
+ * VF ports. Also, when a VF port becomes up/down, its PF port and other VF ports may become down.
+ * See issue #23315. */
+
+ /* First, check the main target. */
+ if (!check_one(link, allow_unmanaged))
+ return false;
+
+ /* If this is a VF port, then also check the PF port. */
+ if (link->sr_iov_phys_port_ifindex > 0) {
+ if (link_get_by_index(link->manager, link->sr_iov_phys_port_ifindex, &phys_link) < 0 ||
+ !check_one(phys_link, /* allow_unmanaged = */ true))
+ return false;
+ } else
+ phys_link = link;
+
+ /* Also check all VF ports. */
+ SET_FOREACH(v, phys_link->sr_iov_virt_port_ifindices) {
+ int ifindex = PTR_TO_INT(v);
+ Link *virt_link;
+
+ if (ifindex == link->ifindex)
+ continue; /* The main target link is a VF port, and its state is already checked. */
+
+ if (link_get_by_index(link->manager, ifindex, &virt_link) < 0)
+ return false;
+
+ if (!check_one(virt_link, /* allow_unmanaged = */ true))
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/network/networkd-sriov.h b/src/network/networkd-sriov.h
new file mode 100644
index 0000000..0d4276e
--- /dev/null
+++ b/src/network/networkd-sriov.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "netif-sriov.h"
+
+typedef struct Link Link;
+
+int link_request_sr_iov_vfs(Link *link);
+
+int link_set_sr_iov_ifindices(Link *link);
+void link_clear_sr_iov_ifindices(Link *link);
+
+bool check_ready_for_all_sr_iov_ports(
+ Link *link,
+ bool allow_unmanaged, /* for the main target */
+ bool (check_one)(Link *link, bool allow_unmanaged));
diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c
new file mode 100644
index 0000000..0800e91
--- /dev/null
+++ b/src/network/networkd-state-file.c
@@ -0,0 +1,730 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if.h>
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "network-internal.h"
+#include "networkd-link.h"
+#include "networkd-manager-bus.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-state-file.h"
+#include "ordered-set.h"
+#include "set.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+
+static int ordered_set_put_dns_server(OrderedSet **s, int ifindex, struct in_addr_full *dns) {
+ const char *p;
+ int r;
+
+ assert(s);
+ assert(dns);
+
+ if (dns->ifindex != 0 && dns->ifindex != ifindex)
+ return 0;
+
+ p = in_addr_full_to_string(dns);
+ if (!p)
+ return 0;
+
+ r = ordered_set_put_strdup(s, p);
+ if (r == -EEXIST)
+ return 0;
+
+ return r;
+}
+
+static int ordered_set_put_dns_servers(OrderedSet **s, int ifindex, struct in_addr_full **dns, unsigned n) {
+ int r, c = 0;
+
+ assert(s);
+ assert(dns || n == 0);
+
+ for (unsigned i = 0; i < n; i++) {
+ r = ordered_set_put_dns_server(s, ifindex, dns[i]);
+ if (r < 0)
+ return r;
+
+ c += r;
+ }
+
+ return c;
+}
+
+static int ordered_set_put_in4_addr(OrderedSet **s, const struct in_addr *address) {
+ _cleanup_free_ char *p = NULL;
+ int r;
+
+ assert(s);
+ assert(address);
+
+ r = in_addr_to_string(AF_INET, (const union in_addr_union*) address, &p);
+ if (r < 0)
+ return r;
+
+ r = ordered_set_ensure_allocated(s, &string_hash_ops_free);
+ if (r < 0)
+ return r;
+
+ r = ordered_set_consume(*s, TAKE_PTR(p));
+ if (r == -EEXIST)
+ return 0;
+
+ return r;
+}
+
+static int ordered_set_put_in4_addrv(
+ OrderedSet **s,
+ const struct in_addr *addresses,
+ size_t n,
+ bool (*predicate)(const struct in_addr *addr)) {
+
+ int r, c = 0;
+
+ assert(s);
+ assert(n == 0 || addresses);
+
+ for (size_t i = 0; i < n; i++) {
+ if (predicate && !predicate(&addresses[i]))
+ continue;
+ r = ordered_set_put_in4_addr(s, addresses+i);
+ if (r < 0)
+ return r;
+
+ c += r;
+ }
+
+ return c;
+}
+
+int manager_save(Manager *m) {
+ _cleanup_ordered_set_free_ OrderedSet *dns = NULL, *ntp = NULL, *sip = NULL, *search_domains = NULL, *route_domains = NULL;
+ const char *operstate_str, *carrier_state_str, *address_state_str, *ipv4_address_state_str, *ipv6_address_state_str, *online_state_str;
+ LinkOperationalState operstate = LINK_OPERSTATE_OFF;
+ LinkCarrierState carrier_state = LINK_CARRIER_STATE_OFF;
+ LinkAddressState ipv4_address_state = LINK_ADDRESS_STATE_OFF, ipv6_address_state = LINK_ADDRESS_STATE_OFF,
+ address_state = LINK_ADDRESS_STATE_OFF;
+ LinkOnlineState online_state;
+ size_t links_offline = 0, links_online = 0;
+ _cleanup_(unlink_and_freep) char *temp_path = NULL;
+ _cleanup_strv_free_ char **p = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ Link *link;
+ int r;
+
+ assert(m);
+
+ if (isempty(m->state_file))
+ return 0; /* Do not update state file when running in test mode. */
+
+ HASHMAP_FOREACH(link, m->links_by_index) {
+ const struct in_addr *addresses;
+
+ if (link->flags & IFF_LOOPBACK)
+ continue;
+
+ operstate = MAX(operstate, link->operstate);
+ carrier_state = MAX(carrier_state, link->carrier_state);
+ address_state = MAX(address_state, link->address_state);
+ ipv4_address_state = MAX(ipv4_address_state, link->ipv4_address_state);
+ ipv6_address_state = MAX(ipv6_address_state, link->ipv6_address_state);
+
+ if (!link->network)
+ continue;
+
+ if (link->network->required_for_online) {
+ if (link->online_state == LINK_ONLINE_STATE_OFFLINE)
+ links_offline++;
+ else if (link->online_state == LINK_ONLINE_STATE_ONLINE)
+ links_online++;
+ }
+
+ /* First add the static configured entries */
+ if (link->n_dns != UINT_MAX)
+ r = ordered_set_put_dns_servers(&dns, link->ifindex, link->dns, link->n_dns);
+ else
+ r = ordered_set_put_dns_servers(&dns, link->ifindex, link->network->dns, link->network->n_dns);
+ if (r < 0)
+ return r;
+
+ r = ordered_set_put_strdupv(&ntp, link->ntp ?: link->network->ntp);
+ if (r < 0)
+ return r;
+
+ r = ordered_set_put_string_set(&search_domains, link->search_domains ?: link->network->search_domains);
+ if (r < 0)
+ return r;
+
+ r = ordered_set_put_string_set(&route_domains, link->route_domains ?: link->network->route_domains);
+ if (r < 0)
+ return r;
+
+ if (!link->dhcp_lease)
+ continue;
+
+ /* Secondly, add the entries acquired via DHCP */
+ if (link->network->dhcp_use_dns) {
+ r = sd_dhcp_lease_get_dns(link->dhcp_lease, &addresses);
+ if (r > 0) {
+ r = ordered_set_put_in4_addrv(&dns, addresses, r, in4_addr_is_non_local);
+ if (r < 0)
+ return r;
+ } else if (r < 0 && r != -ENODATA)
+ return r;
+ }
+
+ if (link->network->dhcp_use_ntp) {
+ r = sd_dhcp_lease_get_ntp(link->dhcp_lease, &addresses);
+ if (r > 0) {
+ r = ordered_set_put_in4_addrv(&ntp, addresses, r, in4_addr_is_non_local);
+ if (r < 0)
+ return r;
+ } else if (r < 0 && r != -ENODATA)
+ return r;
+ }
+
+ if (link->network->dhcp_use_sip) {
+ r = sd_dhcp_lease_get_sip(link->dhcp_lease, &addresses);
+ if (r > 0) {
+ r = ordered_set_put_in4_addrv(&sip, addresses, r, in4_addr_is_non_local);
+ if (r < 0)
+ return r;
+ } else if (r < 0 && r != -ENODATA)
+ return r;
+ }
+
+ if (link->network->dhcp_use_domains != DHCP_USE_DOMAINS_NO) {
+ OrderedSet **target_domains;
+ const char *domainname;
+ char **domains = NULL;
+
+ target_domains = link->network->dhcp_use_domains == DHCP_USE_DOMAINS_YES ? &search_domains : &route_domains;
+ r = sd_dhcp_lease_get_domainname(link->dhcp_lease, &domainname);
+ if (r >= 0) {
+ r = ordered_set_put_strdup(target_domains, domainname);
+ if (r < 0)
+ return r;
+ } else if (r != -ENODATA)
+ return r;
+
+ r = sd_dhcp_lease_get_search_domains(link->dhcp_lease, &domains);
+ if (r >= 0) {
+ r = ordered_set_put_strdupv(target_domains, domains);
+ if (r < 0)
+ return r;
+ } else if (r != -ENODATA)
+ return r;
+ }
+ }
+
+ if (carrier_state >= LINK_CARRIER_STATE_ENSLAVED)
+ carrier_state = LINK_CARRIER_STATE_CARRIER;
+
+ online_state = links_online > 0 ?
+ (links_offline > 0 ? LINK_ONLINE_STATE_PARTIAL : LINK_ONLINE_STATE_ONLINE) :
+ (links_offline > 0 ? LINK_ONLINE_STATE_OFFLINE : _LINK_ONLINE_STATE_INVALID);
+
+ operstate_str = link_operstate_to_string(operstate);
+ assert(operstate_str);
+
+ carrier_state_str = link_carrier_state_to_string(carrier_state);
+ assert(carrier_state_str);
+
+ address_state_str = link_address_state_to_string(address_state);
+ assert(address_state_str);
+
+ ipv4_address_state_str = link_address_state_to_string(ipv4_address_state);
+ assert(ipv4_address_state_str);
+
+ ipv6_address_state_str = link_address_state_to_string(ipv6_address_state);
+ assert(ipv6_address_state_str);
+
+ r = fopen_temporary(m->state_file, &f, &temp_path);
+ if (r < 0)
+ return r;
+
+ (void) fchmod(fileno(f), 0644);
+
+ fprintf(f,
+ "# This is private data. Do not parse.\n"
+ "OPER_STATE=%s\n"
+ "CARRIER_STATE=%s\n"
+ "ADDRESS_STATE=%s\n"
+ "IPV4_ADDRESS_STATE=%s\n"
+ "IPV6_ADDRESS_STATE=%s\n",
+ operstate_str, carrier_state_str, address_state_str, ipv4_address_state_str, ipv6_address_state_str);
+
+ online_state_str = link_online_state_to_string(online_state);
+ if (online_state_str)
+ fprintf(f, "ONLINE_STATE=%s\n", online_state_str);
+
+ ordered_set_print(f, "DNS=", dns);
+ ordered_set_print(f, "NTP=", ntp);
+ ordered_set_print(f, "SIP=", sip);
+ ordered_set_print(f, "DOMAINS=", search_domains);
+ ordered_set_print(f, "ROUTE_DOMAINS=", route_domains);
+
+ r = fflush_and_check(f);
+ if (r < 0)
+ return r;
+
+ r = conservative_rename(temp_path, m->state_file);
+ if (r < 0)
+ return r;
+
+ temp_path = mfree(temp_path);
+
+ if (m->operational_state != operstate) {
+ m->operational_state = operstate;
+ if (strv_extend(&p, "OperationalState") < 0)
+ log_oom();
+ }
+
+ if (m->carrier_state != carrier_state) {
+ m->carrier_state = carrier_state;
+ if (strv_extend(&p, "CarrierState") < 0)
+ log_oom();
+ }
+
+ if (m->address_state != address_state) {
+ m->address_state = address_state;
+ if (strv_extend(&p, "AddressState") < 0)
+ log_oom();
+ }
+
+ if (m->ipv4_address_state != ipv4_address_state) {
+ m->ipv4_address_state = ipv4_address_state;
+ if (strv_extend(&p, "IPv4AddressState") < 0)
+ log_oom();
+ }
+
+ if (m->ipv6_address_state != ipv6_address_state) {
+ m->ipv6_address_state = ipv6_address_state;
+ if (strv_extend(&p, "IPv6AddressState") < 0)
+ log_oom();
+ }
+
+ if (m->online_state != online_state) {
+ m->online_state = online_state;
+ if (strv_extend(&p, "OnlineState") < 0)
+ log_oom();
+ }
+
+ if (p) {
+ r = manager_send_changed_strv(m, p);
+ if (r < 0)
+ log_warning_errno(r, "Could not emit changed properties, ignoring: %m");
+ }
+
+ m->dirty = false;
+
+ return 0;
+}
+
+static void print_link_hashmap(FILE *f, const char *prefix, Hashmap* h) {
+ bool space = false;
+ Link *link;
+
+ assert(f);
+ assert(prefix);
+
+ if (hashmap_isempty(h))
+ return;
+
+ fputs(prefix, f);
+ HASHMAP_FOREACH(link, h) {
+ if (space)
+ fputc(' ', f);
+
+ fprintf(f, "%i", link->ifindex);
+ space = true;
+ }
+
+ fputc('\n', f);
+}
+
+static void link_save_dns(Link *link, FILE *f, struct in_addr_full **dns, unsigned n_dns, bool *space) {
+ bool _space = false;
+
+ if (!space)
+ space = &_space;
+
+ for (unsigned j = 0; j < n_dns; j++) {
+ const char *str;
+
+ if (dns[j]->ifindex != 0 && dns[j]->ifindex != link->ifindex)
+ continue;
+
+ str = in_addr_full_to_string(dns[j]);
+ if (!str)
+ continue;
+
+ if (*space)
+ fputc(' ', f);
+ fputs(str, f);
+ *space = true;
+ }
+}
+
+static void serialize_addresses(
+ FILE *f,
+ const char *lvalue,
+ bool *space,
+ char **addresses,
+ sd_dhcp_lease *lease,
+ bool conditional,
+ sd_dhcp_lease_server_type_t what,
+ sd_dhcp6_lease *lease6,
+ bool conditional6,
+ int (*lease6_get_addr)(sd_dhcp6_lease*, const struct in6_addr**),
+ int (*lease6_get_fqdn)(sd_dhcp6_lease*, char ***)) {
+
+ bool _space = false;
+ int r;
+
+ if (!space)
+ space = &_space;
+
+ if (lvalue)
+ fprintf(f, "%s=", lvalue);
+ fputstrv(f, addresses, NULL, space);
+
+ if (lease && conditional) {
+ const struct in_addr *lease_addresses;
+
+ r = sd_dhcp_lease_get_servers(lease, what, &lease_addresses);
+ if (r > 0)
+ serialize_in_addrs(f, lease_addresses, r, space, in4_addr_is_non_local);
+ }
+
+ if (lease6 && conditional6 && lease6_get_addr) {
+ const struct in6_addr *in6_addrs;
+
+ r = lease6_get_addr(lease6, &in6_addrs);
+ if (r > 0)
+ serialize_in6_addrs(f, in6_addrs, r, space);
+ }
+
+ if (lease6 && conditional6 && lease6_get_fqdn) {
+ char **in6_hosts;
+
+ r = lease6_get_fqdn(lease6, &in6_hosts);
+ if (r > 0)
+ fputstrv(f, in6_hosts, NULL, space);
+ }
+
+ if (lvalue)
+ fputc('\n', f);
+}
+
+static void link_save_domains(Link *link, FILE *f, OrderedSet *static_domains, DHCPUseDomains use_domains) {
+ bool space = false;
+ const char *p;
+
+ assert(link);
+ assert(link->network);
+ assert(f);
+
+ ORDERED_SET_FOREACH(p, static_domains)
+ fputs_with_space(f, p, NULL, &space);
+
+ if (use_domains == DHCP_USE_DOMAINS_NO)
+ return;
+
+ if (link->dhcp_lease && link->network->dhcp_use_domains == use_domains) {
+ const char *domainname;
+ char **domains;
+
+ if (sd_dhcp_lease_get_domainname(link->dhcp_lease, &domainname) >= 0)
+ fputs_with_space(f, domainname, NULL, &space);
+ if (sd_dhcp_lease_get_search_domains(link->dhcp_lease, &domains) >= 0)
+ fputstrv(f, domains, NULL, &space);
+ }
+
+ if (link->dhcp6_lease && link->network->dhcp6_use_domains == use_domains) {
+ char **domains;
+
+ if (sd_dhcp6_lease_get_domains(link->dhcp6_lease, &domains) >= 0)
+ fputstrv(f, domains, NULL, &space);
+ }
+
+ if (link->network->ipv6_accept_ra_use_domains == use_domains) {
+ NDiscDNSSL *dd;
+
+ SET_FOREACH(dd, link->ndisc_dnssl)
+ fputs_with_space(f, NDISC_DNSSL_DOMAIN(dd), NULL, &space);
+ }
+}
+
+int link_save(Link *link) {
+ const char *admin_state, *oper_state, *carrier_state, *address_state, *ipv4_address_state, *ipv6_address_state;
+ _cleanup_(unlink_and_freep) char *temp_path = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ if (isempty(link->state_file))
+ return 0; /* Do not update state files when running in test mode. */
+
+ if (link->state == LINK_STATE_LINGER)
+ return 0;
+
+ link_lldp_save(link);
+
+ admin_state = link_state_to_string(link->state);
+ assert(admin_state);
+
+ oper_state = link_operstate_to_string(link->operstate);
+ assert(oper_state);
+
+ carrier_state = link_carrier_state_to_string(link->carrier_state);
+ assert(carrier_state);
+
+ address_state = link_address_state_to_string(link->address_state);
+ assert(address_state);
+
+ ipv4_address_state = link_address_state_to_string(link->ipv4_address_state);
+ assert(ipv4_address_state);
+
+ ipv6_address_state = link_address_state_to_string(link->ipv6_address_state);
+ assert(ipv6_address_state);
+
+ r = fopen_temporary(link->state_file, &f, &temp_path);
+ if (r < 0)
+ return r;
+
+ (void) fchmod(fileno(f), 0644);
+
+ fprintf(f,
+ "# This is private data. Do not parse.\n"
+ "ADMIN_STATE=%s\n"
+ "OPER_STATE=%s\n"
+ "CARRIER_STATE=%s\n"
+ "ADDRESS_STATE=%s\n"
+ "IPV4_ADDRESS_STATE=%s\n"
+ "IPV6_ADDRESS_STATE=%s\n",
+ admin_state, oper_state, carrier_state, address_state, ipv4_address_state, ipv6_address_state);
+
+ if (link->network) {
+ const char *online_state;
+ bool space;
+
+ online_state = link_online_state_to_string(link->online_state);
+ if (online_state)
+ fprintf(f, "ONLINE_STATE=%s\n", online_state);
+
+ fprintf(f, "REQUIRED_FOR_ONLINE=%s\n",
+ yes_no(link->network->required_for_online));
+
+ LinkOperationalStateRange st = link->network->required_operstate_for_online;
+ fprintf(f, "REQUIRED_OPER_STATE_FOR_ONLINE=%s%s%s\n",
+ strempty(link_operstate_to_string(st.min)),
+ st.max != LINK_OPERSTATE_RANGE_DEFAULT.max ? ":" : "",
+ st.max != LINK_OPERSTATE_RANGE_DEFAULT.max ? strempty(link_operstate_to_string(st.max)) : "");
+
+ fprintf(f, "REQUIRED_FAMILY_FOR_ONLINE=%s\n",
+ link_required_address_family_to_string(link->network->required_family_for_online));
+
+ fprintf(f, "ACTIVATION_POLICY=%s\n",
+ activation_policy_to_string(link->network->activation_policy));
+
+ fprintf(f, "NETWORK_FILE=%s\n", link->network->filename);
+
+ /************************************************************/
+
+ fputs("DNS=", f);
+ if (link->n_dns != UINT_MAX)
+ link_save_dns(link, f, link->dns, link->n_dns, NULL);
+ else {
+ space = false;
+ link_save_dns(link, f, link->network->dns, link->network->n_dns, &space);
+
+ serialize_addresses(f, NULL, &space,
+ NULL,
+ link->dhcp_lease,
+ link->network->dhcp_use_dns,
+ SD_DHCP_LEASE_DNS,
+ link->dhcp6_lease,
+ link->network->dhcp6_use_dns,
+ sd_dhcp6_lease_get_dns,
+ NULL);
+
+ if (link->network->ipv6_accept_ra_use_dns) {
+ NDiscRDNSS *dd;
+
+ SET_FOREACH(dd, link->ndisc_rdnss)
+ serialize_in6_addrs(f, &dd->address, 1, &space);
+ }
+ }
+
+ fputc('\n', f);
+
+ /************************************************************/
+
+ if (link->ntp) {
+ fputs("NTP=", f);
+ fputstrv(f, link->ntp, NULL, NULL);
+ fputc('\n', f);
+ } else
+ serialize_addresses(f, "NTP", NULL,
+ link->network->ntp,
+ link->dhcp_lease,
+ link->network->dhcp_use_ntp,
+ SD_DHCP_LEASE_NTP,
+ link->dhcp6_lease,
+ link->network->dhcp6_use_ntp,
+ sd_dhcp6_lease_get_ntp_addrs,
+ sd_dhcp6_lease_get_ntp_fqdn);
+
+ serialize_addresses(f, "SIP", NULL,
+ NULL,
+ link->dhcp_lease,
+ link->network->dhcp_use_sip,
+ SD_DHCP_LEASE_SIP,
+ NULL, false, NULL, NULL);
+
+ /************************************************************/
+
+ fputs("DOMAINS=", f);
+ if (link->search_domains)
+ link_save_domains(link, f, link->search_domains, DHCP_USE_DOMAINS_NO);
+ else
+ link_save_domains(link, f, link->network->search_domains, DHCP_USE_DOMAINS_YES);
+ fputc('\n', f);
+
+ /************************************************************/
+
+ fputs("ROUTE_DOMAINS=", f);
+ if (link->route_domains)
+ link_save_domains(link, f, link->route_domains, DHCP_USE_DOMAINS_NO);
+ else
+ link_save_domains(link, f, link->network->route_domains, DHCP_USE_DOMAINS_ROUTE);
+ fputc('\n', f);
+
+ /************************************************************/
+
+ fprintf(f, "LLMNR=%s\n",
+ resolve_support_to_string(link->llmnr >= 0 ? link->llmnr : link->network->llmnr));
+
+ /************************************************************/
+
+ fprintf(f, "MDNS=%s\n",
+ resolve_support_to_string(link->mdns >= 0 ? link->mdns : link->network->mdns));
+
+ /************************************************************/
+
+ int dns_default_route =
+ link->dns_default_route >= 0 ? link->dns_default_route :
+ link->network->dns_default_route;
+ if (dns_default_route >= 0)
+ fprintf(f, "DNS_DEFAULT_ROUTE=%s\n", yes_no(dns_default_route));
+
+ /************************************************************/
+
+ DnsOverTlsMode dns_over_tls_mode =
+ link->dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID ? link->dns_over_tls_mode :
+ link->network->dns_over_tls_mode;
+ if (dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID)
+ fprintf(f, "DNS_OVER_TLS=%s\n", dns_over_tls_mode_to_string(dns_over_tls_mode));
+
+ /************************************************************/
+
+ DnssecMode dnssec_mode =
+ link->dnssec_mode != _DNSSEC_MODE_INVALID ? link->dnssec_mode :
+ link->network->dnssec_mode;
+ if (dnssec_mode != _DNSSEC_MODE_INVALID)
+ fprintf(f, "DNSSEC=%s\n", dnssec_mode_to_string(dnssec_mode));
+
+ /************************************************************/
+
+ Set *nta_anchors = link->dnssec_negative_trust_anchors;
+ if (set_isempty(nta_anchors))
+ nta_anchors = link->network->dnssec_negative_trust_anchors;
+
+ if (!set_isempty(nta_anchors)) {
+ const char *n;
+
+ fputs("DNSSEC_NTA=", f);
+ space = false;
+ SET_FOREACH(n, nta_anchors)
+ fputs_with_space(f, n, NULL, &space);
+ fputc('\n', f);
+ }
+ }
+
+ print_link_hashmap(f, "CARRIER_BOUND_TO=", link->bound_to_links);
+ print_link_hashmap(f, "CARRIER_BOUND_BY=", link->bound_by_links);
+
+ if (link->dhcp_lease) {
+ r = dhcp_lease_save(link->dhcp_lease, link->lease_file);
+ if (r < 0)
+ return r;
+
+ fprintf(f,
+ "DHCP_LEASE=%s\n",
+ link->lease_file);
+ } else
+ (void) unlink(link->lease_file);
+
+ r = link_serialize_dhcp6_client(link, f);
+ if (r < 0)
+ return r;
+
+ r = fflush_and_check(f);
+ if (r < 0)
+ return r;
+
+ r = conservative_rename(temp_path, link->state_file);
+ if (r < 0)
+ return r;
+
+ temp_path = mfree(temp_path);
+
+ return 0;
+}
+
+void link_dirty(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ /* The serialized state in /run is no longer up-to-date. */
+
+ /* Also mark manager dirty as link is dirty */
+ link->manager->dirty = true;
+
+ r = set_ensure_put(&link->manager->dirty_links, NULL, link);
+ if (r <= 0)
+ /* Ignore allocation errors and don't take another ref if the link was already dirty */
+ return;
+ link_ref(link);
+}
+
+void link_clean(Link *link) {
+ assert(link);
+ assert(link->manager);
+
+ /* The serialized state in /run is up-to-date */
+
+ link_unref(set_remove(link->manager->dirty_links, link));
+}
+
+int link_save_and_clean(Link *link) {
+ int r;
+
+ r = link_save(link);
+ if (r < 0)
+ return r;
+
+ link_clean(link);
+ return 0;
+}
diff --git a/src/network/networkd-state-file.h b/src/network/networkd-state-file.h
new file mode 100644
index 0000000..44a109f
--- /dev/null
+++ b/src/network/networkd-state-file.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+
+void link_dirty(Link *link);
+void link_clean(Link *link);
+int link_save(Link *link);
+int link_save_and_clean(Link *link);
+
+int manager_save(Manager *m);
diff --git a/src/network/networkd-sysctl.c b/src/network/networkd-sysctl.c
new file mode 100644
index 0000000..51a697a
--- /dev/null
+++ b/src/network/networkd-sysctl.c
@@ -0,0 +1,350 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if.h>
+
+#include "missing_network.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "networkd-sysctl.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "sysctl-util.h"
+
+static int link_update_ipv6_sysctl(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link_ipv6_enabled(link))
+ return 0;
+
+ return sysctl_write_ip_property_boolean(AF_INET6, link->ifname, "disable_ipv6", false);
+}
+
+static int link_set_proxy_arp(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->proxy_arp < 0)
+ return 0;
+
+ return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "proxy_arp", link->network->proxy_arp > 0);
+}
+
+static bool link_ip_forward_enabled(Link *link, int family) {
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+
+ if (family == AF_INET6 && !socket_ipv6_is_supported())
+ return false;
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ return link->network->ip_forward & (family == AF_INET ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_IPV6);
+}
+
+static int link_set_ipv4_forward(Link *link) {
+ assert(link);
+
+ if (!link_ip_forward_enabled(link, AF_INET))
+ return 0;
+
+ /* We propagate the forwarding flag from one interface to the
+ * global setting one way. This means: as long as at least one
+ * interface was configured at any time that had IP forwarding
+ * enabled the setting will stay on for good. We do this
+ * primarily to keep IPv4 and IPv6 packet forwarding behaviour
+ * somewhat in sync (see below). */
+
+ return sysctl_write_ip_property(AF_INET, NULL, "ip_forward", "1");
+}
+
+static int link_set_ipv6_forward(Link *link) {
+ assert(link);
+
+ if (!link_ip_forward_enabled(link, AF_INET6))
+ return 0;
+
+ /* On Linux, the IPv6 stack does not know a per-interface
+ * packet forwarding setting: either packet forwarding is on
+ * for all, or off for all. We hence don't bother with a
+ * per-interface setting, but simply propagate the interface
+ * flag, if it is set, to the global flag, one-way. Note that
+ * while IPv4 would allow a per-interface flag, we expose the
+ * same behaviour there and also propagate the setting from
+ * one to all, to keep things simple (see above). */
+
+ return sysctl_write_ip_property(AF_INET6, "all", "forwarding", "1");
+}
+
+static int link_set_ipv6_privacy_extensions(Link *link) {
+ assert(link);
+
+ if (!socket_ipv6_is_supported())
+ return 0;
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ // this is the special "kernel" value
+ if (link->network->ipv6_privacy_extensions == _IPV6_PRIVACY_EXTENSIONS_INVALID)
+ return 0;
+
+ return sysctl_write_ip_property_int(AF_INET6, link->ifname, "use_tempaddr", (int) link->network->ipv6_privacy_extensions);
+}
+
+static int link_set_ipv6_accept_ra(Link *link) {
+ assert(link);
+
+ /* Make this a NOP if IPv6 is not available */
+ if (!socket_ipv6_is_supported())
+ return 0;
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ return sysctl_write_ip_property(AF_INET6, link->ifname, "accept_ra", "0");
+}
+
+static int link_set_ipv6_dad_transmits(Link *link) {
+ assert(link);
+
+ /* Make this a NOP if IPv6 is not available */
+ if (!socket_ipv6_is_supported())
+ return 0;
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->ipv6_dad_transmits < 0)
+ return 0;
+
+ return sysctl_write_ip_property_int(AF_INET6, link->ifname, "dad_transmits", link->network->ipv6_dad_transmits);
+}
+
+static int link_set_ipv6_hop_limit(Link *link) {
+ assert(link);
+
+ /* Make this a NOP if IPv6 is not available */
+ if (!socket_ipv6_is_supported())
+ return 0;
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->ipv6_hop_limit < 0)
+ return 0;
+
+ return sysctl_write_ip_property_int(AF_INET6, link->ifname, "hop_limit", link->network->ipv6_hop_limit);
+}
+
+static int link_set_ipv6_proxy_ndp(Link *link) {
+ bool v;
+
+ assert(link);
+
+ if (!socket_ipv6_is_supported())
+ return 0;
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->ipv6_proxy_ndp >= 0)
+ v = link->network->ipv6_proxy_ndp;
+ else
+ v = !set_isempty(link->network->ipv6_proxy_ndp_addresses);
+
+ return sysctl_write_ip_property_boolean(AF_INET6, link->ifname, "proxy_ndp", v);
+}
+
+int link_set_ipv6_mtu(Link *link) {
+ uint32_t mtu;
+
+ assert(link);
+
+ /* Make this a NOP if IPv6 is not available */
+ if (!socket_ipv6_is_supported())
+ return 0;
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->ipv6_mtu == 0)
+ return 0;
+
+ mtu = link->network->ipv6_mtu;
+ if (mtu > link->max_mtu) {
+ log_link_warning(link, "Reducing requested IPv6 MTU %"PRIu32" to the interface's maximum MTU %"PRIu32".",
+ mtu, link->max_mtu);
+ mtu = link->max_mtu;
+ }
+
+ return sysctl_write_ip_property_uint32(AF_INET6, link->ifname, "mtu", mtu);
+}
+
+static int link_set_ipv4_accept_local(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (link->network->ipv4_accept_local < 0)
+ return 0;
+
+ return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "accept_local", link->network->ipv4_accept_local > 0);
+}
+
+static int link_set_ipv4_route_localnet(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (link->network->ipv4_route_localnet < 0)
+ return 0;
+
+ return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "route_localnet", link->network->ipv4_route_localnet > 0);
+}
+
+int link_set_sysctl(Link *link) {
+ int r;
+
+ assert(link);
+
+ /* If IPv6 configured that is static IPv6 address and IPv6LL autoconfiguration is enabled
+ * for this interface, then enable IPv6 */
+ r = link_update_ipv6_sysctl(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot enable IPv6, ignoring: %m");
+
+ r = link_set_proxy_arp(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot configure proxy ARP for interface, ignoring: %m");
+
+ r = link_set_ipv4_forward(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot turn on IPv4 packet forwarding, ignoring: %m");
+
+ r = link_set_ipv6_forward(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot configure IPv6 packet forwarding, ignoring: %m");
+
+ r = link_set_ipv6_privacy_extensions(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot configure IPv6 privacy extensions for interface, ignoring: %m");
+
+ r = link_set_ipv6_accept_ra(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot disable kernel IPv6 accept_ra for interface, ignoring: %m");
+
+ r = link_set_ipv6_dad_transmits(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv6 dad transmits for interface, ignoring: %m");
+
+ r = link_set_ipv6_hop_limit(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv6 hop limit for interface, ignoring: %m");
+
+ r = link_set_ipv6_proxy_ndp(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv6 proxy NDP, ignoring: %m");
+
+ r = link_set_ipv6_mtu(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv6 MTU, ignoring: %m");
+
+ r = link_set_ipv6ll_stable_secret(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set stable secret address for IPv6 link-local address: %m");
+
+ r = link_set_ipv4_accept_local(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv4 accept_local flag for interface, ignoring: %m");
+
+ r = link_set_ipv4_route_localnet(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv4 route_localnet flag for interface, ignoring: %m");
+
+ /* If promote_secondaries is not set, DHCP will work only as long as the IP address does not
+ * changes between leases. The kernel will remove all secondary IP addresses of an interface
+ * otherwise. The way systemd-networkd works is that the new IP of a lease is added as a
+ * secondary IP and when the primary one expires it relies on the kernel to promote the
+ * secondary IP. See also https://github.com/systemd/systemd/issues/7163 */
+ r = sysctl_write_ip_property_boolean(AF_INET, link->ifname, "promote_secondaries", true);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot enable promote_secondaries for interface, ignoring: %m");
+
+ return 0;
+}
+
+static const char* const ipv6_privacy_extensions_table[_IPV6_PRIVACY_EXTENSIONS_MAX] = {
+ [IPV6_PRIVACY_EXTENSIONS_NO] = "no",
+ [IPV6_PRIVACY_EXTENSIONS_PREFER_PUBLIC] = "prefer-public",
+ [IPV6_PRIVACY_EXTENSIONS_YES] = "yes",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(ipv6_privacy_extensions, IPv6PrivacyExtensions,
+ IPV6_PRIVACY_EXTENSIONS_YES);
+
+int config_parse_ipv6_privacy_extensions(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ IPv6PrivacyExtensions s, *ipv6_privacy_extensions = ASSERT_PTR(data);
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ s = ipv6_privacy_extensions_from_string(rvalue);
+ if (s < 0) {
+ if (streq(rvalue, "kernel"))
+ s = _IPV6_PRIVACY_EXTENSIONS_INVALID;
+ else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse IPv6 privacy extensions option, ignoring: %s", rvalue);
+ return 0;
+ }
+ }
+
+ *ipv6_privacy_extensions = s;
+
+ return 0;
+}
diff --git a/src/network/networkd-sysctl.h b/src/network/networkd-sysctl.h
new file mode 100644
index 0000000..cb1db10
--- /dev/null
+++ b/src/network/networkd-sysctl.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+
+typedef enum IPv6PrivacyExtensions {
+ /* The values map to the kernel's /proc/sys/net/ipv6/conf/xxx/use_tempaddr values */
+ IPV6_PRIVACY_EXTENSIONS_NO,
+ IPV6_PRIVACY_EXTENSIONS_PREFER_PUBLIC,
+ IPV6_PRIVACY_EXTENSIONS_YES, /* aka prefer-temporary */
+ _IPV6_PRIVACY_EXTENSIONS_MAX,
+ _IPV6_PRIVACY_EXTENSIONS_INVALID = -EINVAL,
+} IPv6PrivacyExtensions;
+
+int link_set_sysctl(Link *link);
+int link_set_ipv6_mtu(Link *link);
+
+const char* ipv6_privacy_extensions_to_string(IPv6PrivacyExtensions i) _const_;
+IPv6PrivacyExtensions ipv6_privacy_extensions_from_string(const char *s) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_privacy_extensions);
diff --git a/src/network/networkd-util.c b/src/network/networkd-util.c
new file mode 100644
index 0000000..69347d8
--- /dev/null
+++ b/src/network/networkd-util.c
@@ -0,0 +1,256 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "condition.h"
+#include "conf-parser.h"
+#include "escape.h"
+#include "networkd-link.h"
+#include "networkd-util.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "web-util.h"
+
+/* This is used in log messages, and never used in parsing settings. So, upper cases are OK. */
+static const char * const network_config_source_table[_NETWORK_CONFIG_SOURCE_MAX] = {
+ [NETWORK_CONFIG_SOURCE_FOREIGN] = "foreign",
+ [NETWORK_CONFIG_SOURCE_STATIC] = "static",
+ [NETWORK_CONFIG_SOURCE_IPV4LL] = "IPv4LL",
+ [NETWORK_CONFIG_SOURCE_DHCP4] = "DHCPv4",
+ [NETWORK_CONFIG_SOURCE_DHCP6] = "DHCPv6",
+ [NETWORK_CONFIG_SOURCE_DHCP_PD] = "DHCP-PD",
+ [NETWORK_CONFIG_SOURCE_NDISC] = "NDisc",
+ [NETWORK_CONFIG_SOURCE_RUNTIME] = "runtime",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_TO_STRING(network_config_source, NetworkConfigSource);
+
+int network_config_state_to_string_alloc(NetworkConfigState s, char **ret) {
+ static const char* states[] = {
+ [LOG2U(NETWORK_CONFIG_STATE_REQUESTING)] = "requesting",
+ [LOG2U(NETWORK_CONFIG_STATE_CONFIGURING)] = "configuring",
+ [LOG2U(NETWORK_CONFIG_STATE_CONFIGURED)] = "configured",
+ [LOG2U(NETWORK_CONFIG_STATE_MARKED)] = "marked",
+ [LOG2U(NETWORK_CONFIG_STATE_REMOVING)] = "removing",
+ };
+ _cleanup_free_ char *buf = NULL;
+
+ assert(ret);
+
+ for (size_t i = 0; i < ELEMENTSOF(states); i++)
+ if (FLAGS_SET(s, 1 << i)) {
+ assert(states[i]);
+
+ if (!strextend_with_separator(&buf, ",", states[i]))
+ return -ENOMEM;
+ }
+
+ *ret = TAKE_PTR(buf);
+ return 0;
+}
+
+static const char * const address_family_table[_ADDRESS_FAMILY_MAX] = {
+ [ADDRESS_FAMILY_NO] = "no",
+ [ADDRESS_FAMILY_YES] = "yes",
+ [ADDRESS_FAMILY_IPV4] = "ipv4",
+ [ADDRESS_FAMILY_IPV6] = "ipv6",
+};
+
+static const char * const routing_policy_rule_address_family_table[_ADDRESS_FAMILY_MAX] = {
+ [ADDRESS_FAMILY_YES] = "both",
+ [ADDRESS_FAMILY_IPV4] = "ipv4",
+ [ADDRESS_FAMILY_IPV6] = "ipv6",
+};
+
+static const char * const nexthop_address_family_table[_ADDRESS_FAMILY_MAX] = {
+ [ADDRESS_FAMILY_IPV4] = "ipv4",
+ [ADDRESS_FAMILY_IPV6] = "ipv6",
+};
+
+static const char * const duplicate_address_detection_address_family_table[_ADDRESS_FAMILY_MAX] = {
+ [ADDRESS_FAMILY_NO] = "none",
+ [ADDRESS_FAMILY_YES] = "both",
+ [ADDRESS_FAMILY_IPV4] = "ipv4",
+ [ADDRESS_FAMILY_IPV6] = "ipv6",
+};
+
+static const char * const dhcp_deprecated_address_family_table[_ADDRESS_FAMILY_MAX] = {
+ [ADDRESS_FAMILY_NO] = "none",
+ [ADDRESS_FAMILY_YES] = "both",
+ [ADDRESS_FAMILY_IPV4] = "v4",
+ [ADDRESS_FAMILY_IPV6] = "v6",
+};
+
+static const char * const ip_masquerade_address_family_table[_ADDRESS_FAMILY_MAX] = {
+ [ADDRESS_FAMILY_NO] = "no",
+ [ADDRESS_FAMILY_YES] = "both",
+ [ADDRESS_FAMILY_IPV4] = "ipv4",
+ [ADDRESS_FAMILY_IPV6] = "ipv6",
+};
+
+static const char * const dhcp_lease_server_type_table[_SD_DHCP_LEASE_SERVER_TYPE_MAX] = {
+ [SD_DHCP_LEASE_DNS] = "DNS servers",
+ [SD_DHCP_LEASE_NTP] = "NTP servers",
+ [SD_DHCP_LEASE_SIP] = "SIP servers",
+ [SD_DHCP_LEASE_POP3] = "POP3 servers",
+ [SD_DHCP_LEASE_SMTP] = "SMTP servers",
+ [SD_DHCP_LEASE_LPR] = "LPR servers",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(address_family, AddressFamily, ADDRESS_FAMILY_YES);
+
+AddressFamily link_local_address_family_from_string(const char *s) {
+ if (streq_ptr(s, "fallback")) /* compat name */
+ return ADDRESS_FAMILY_YES;
+ if (streq_ptr(s, "fallback-ipv4")) /* compat name */
+ return ADDRESS_FAMILY_IPV4;
+ return address_family_from_string(s);
+}
+
+DEFINE_STRING_TABLE_LOOKUP(routing_policy_rule_address_family, AddressFamily);
+DEFINE_STRING_TABLE_LOOKUP(nexthop_address_family, AddressFamily);
+DEFINE_STRING_TABLE_LOOKUP(duplicate_address_detection_address_family, AddressFamily);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_link_local_address_family, link_local_address_family,
+ AddressFamily, "Failed to parse option");
+DEFINE_STRING_TABLE_LOOKUP_FROM_STRING(dhcp_deprecated_address_family, AddressFamily);
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(ip_masquerade_address_family, AddressFamily);
+DEFINE_STRING_TABLE_LOOKUP(dhcp_lease_server_type, sd_dhcp_lease_server_type_t);
+
+int config_parse_address_family_with_kernel(
+ const char* unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ AddressFamily *fwd = data, s;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ /* This function is mostly obsolete now. It simply redirects
+ * "kernel" to "no". In older networkd versions we used to
+ * distinguish IPForward=off from IPForward=kernel, where the
+ * former would explicitly turn off forwarding while the
+ * latter would simply not touch the setting. But that logic
+ * is gone, hence silently accept the old setting, but turn it
+ * to "no". */
+
+ s = address_family_from_string(rvalue);
+ if (s < 0) {
+ if (streq(rvalue, "kernel"))
+ s = ADDRESS_FAMILY_NO;
+ else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Failed to parse IPForward= option, ignoring: %s", rvalue);
+ return 0;
+ }
+ }
+
+ *fwd = s;
+
+ return 0;
+}
+
+int config_parse_ip_masquerade(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ AddressFamily a, *ret = data;
+ int r;
+
+ if (isempty(rvalue)) {
+ *ret = ADDRESS_FAMILY_NO;
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r >= 0) {
+ if (r)
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "IPMasquerade=%s is deprecated, and it is handled as \"ipv4\" instead of \"both\". "
+ "Please use \"ipv4\" or \"both\".",
+ rvalue);
+
+ *ret = r ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_NO;
+ return 0;
+ }
+
+ a = ip_masquerade_address_family_from_string(rvalue);
+ if (a < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, a,
+ "Failed to parse IPMasquerade= setting, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ *ret = a;
+ return 0;
+}
+
+int config_parse_mud_url(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_free_ char *unescaped = NULL;
+ char **url = ASSERT_PTR(data);
+ ssize_t l;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *url = mfree(*url);
+ return 0;
+ }
+
+ l = cunescape(rvalue, 0, &unescaped);
+ if (l < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, l,
+ "Failed to unescape MUD URL, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (l > UINT8_MAX || !http_url_is_valid(unescaped)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid MUD URL, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ return free_and_replace(*url, unescaped);
+}
+
+int log_link_message_full_errno(Link *link, sd_netlink_message *m, int level, int err, const char *msg) {
+ const char *err_msg = NULL;
+
+ /* link may be NULL. */
+
+ (void) sd_netlink_message_read_string(m, NLMSGERR_ATTR_MSG, &err_msg);
+ return log_link_full_errno(link, level, err,
+ "%s: %s%s%s%m",
+ msg,
+ strempty(err_msg),
+ err_msg && !endswith(err_msg, ".") ? "." : "",
+ err_msg ? " " : "");
+}
diff --git a/src/network/networkd-util.h b/src/network/networkd-util.h
new file mode 100644
index 0000000..8bdf8e8
--- /dev/null
+++ b/src/network/networkd-util.h
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-dhcp-lease.h"
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+#include "hashmap.h"
+#include "log.h"
+#include "macro.h"
+#include "network-util.h"
+#include "string-util.h"
+
+typedef struct Link Link;
+
+typedef enum NetworkConfigSource {
+ NETWORK_CONFIG_SOURCE_FOREIGN, /* configured by kernel */
+ NETWORK_CONFIG_SOURCE_STATIC,
+ NETWORK_CONFIG_SOURCE_IPV4LL,
+ NETWORK_CONFIG_SOURCE_DHCP4,
+ NETWORK_CONFIG_SOURCE_DHCP6,
+ NETWORK_CONFIG_SOURCE_DHCP_PD,
+ NETWORK_CONFIG_SOURCE_NDISC,
+ NETWORK_CONFIG_SOURCE_RUNTIME, /* through D-Bus method */
+ _NETWORK_CONFIG_SOURCE_MAX,
+ _NETWORK_CONFIG_SOURCE_INVALID = -EINVAL,
+} NetworkConfigSource;
+
+typedef enum NetworkConfigState {
+ NETWORK_CONFIG_STATE_REQUESTING = 1 << 0, /* request is queued */
+ NETWORK_CONFIG_STATE_CONFIGURING = 1 << 1, /* e.g. address_configure() is called, but no response is received yet */
+ NETWORK_CONFIG_STATE_CONFIGURED = 1 << 2, /* e.g. address_configure() is called and received a response from kernel.
+ * Note that address may not be ready yet, so please use address_is_ready()
+ * to check whether the address can be usable or not. */
+ NETWORK_CONFIG_STATE_MARKED = 1 << 3, /* used GC'ing the old config */
+ NETWORK_CONFIG_STATE_REMOVING = 1 << 4, /* e.g. address_remove() is called, but no response is received yet */
+} NetworkConfigState;
+
+static inline usec_t sec_to_usec(uint32_t sec, usec_t timestamp_usec) {
+ return
+ sec == 0 ? 0 :
+ sec == UINT32_MAX ? USEC_INFINITY :
+ usec_add(timestamp_usec, sec * USEC_PER_SEC);
+}
+
+static inline usec_t sec16_to_usec(uint16_t sec, usec_t timestamp_usec) {
+ return sec_to_usec(sec == UINT16_MAX ? UINT32_MAX : (uint32_t) sec, timestamp_usec);
+}
+
+static inline uint32_t usec_to_sec(usec_t usec, usec_t now_usec) {
+ return MIN(DIV_ROUND_UP(usec_sub_unsigned(usec, now_usec), USEC_PER_SEC), UINT32_MAX);
+}
+
+CONFIG_PARSER_PROTOTYPE(config_parse_link_local_address_family);
+CONFIG_PARSER_PROTOTYPE(config_parse_address_family_with_kernel);
+CONFIG_PARSER_PROTOTYPE(config_parse_ip_masquerade);
+CONFIG_PARSER_PROTOTYPE(config_parse_mud_url);
+
+const char *network_config_source_to_string(NetworkConfigSource s) _const_;
+
+int network_config_state_to_string_alloc(NetworkConfigState s, char **ret);
+
+#define DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(type, name) \
+ static inline void name##_update_state( \
+ type *t, \
+ NetworkConfigState mask, \
+ NetworkConfigState value) { \
+ \
+ assert(t); \
+ \
+ t->state = (t->state & ~mask) | (value & mask); \
+ } \
+ static inline bool name##_exists(const type *t) { \
+ assert(t); \
+ \
+ if ((t->state & (NETWORK_CONFIG_STATE_CONFIGURING | \
+ NETWORK_CONFIG_STATE_CONFIGURED)) == 0) \
+ return false; /* Not assigned yet. */ \
+ if (FLAGS_SET(t->state, NETWORK_CONFIG_STATE_REMOVING)) \
+ return false; /* Already removing. */ \
+ return true; \
+ } \
+ static inline void name##_enter_requesting(type *t) { \
+ name##_update_state(t, \
+ NETWORK_CONFIG_STATE_REQUESTING, \
+ NETWORK_CONFIG_STATE_REQUESTING); \
+ } \
+ static inline void name##_cancel_requesting(type *t) { \
+ name##_update_state(t, \
+ NETWORK_CONFIG_STATE_REQUESTING, \
+ 0); \
+ } \
+ static inline bool name##_is_requesting(const type *t) { \
+ return FLAGS_SET(t->state, NETWORK_CONFIG_STATE_REQUESTING); \
+ } \
+ static inline void name##_enter_configuring(type *t) { \
+ name##_update_state(t, \
+ NETWORK_CONFIG_STATE_REQUESTING | \
+ NETWORK_CONFIG_STATE_CONFIGURING | \
+ NETWORK_CONFIG_STATE_REMOVING, \
+ NETWORK_CONFIG_STATE_CONFIGURING); \
+ } \
+ static inline void name##_enter_configured(type *t) { \
+ name##_update_state(t, \
+ NETWORK_CONFIG_STATE_CONFIGURING | \
+ NETWORK_CONFIG_STATE_CONFIGURED, \
+ NETWORK_CONFIG_STATE_CONFIGURED); \
+ } \
+ static inline void name##_mark(type *t) { \
+ name##_update_state(t, \
+ NETWORK_CONFIG_STATE_MARKED, \
+ NETWORK_CONFIG_STATE_MARKED); \
+ } \
+ static inline void name##_unmark(type *t) { \
+ name##_update_state(t, NETWORK_CONFIG_STATE_MARKED, 0); \
+ } \
+ static inline bool name##_is_marked(const type *t) { \
+ assert(t); \
+ return FLAGS_SET(t->state, NETWORK_CONFIG_STATE_MARKED); \
+ } \
+ static inline void name##_enter_removing(type *t) { \
+ name##_update_state(t, \
+ NETWORK_CONFIG_STATE_MARKED | \
+ NETWORK_CONFIG_STATE_REMOVING, \
+ NETWORK_CONFIG_STATE_REMOVING); \
+ } \
+ static inline bool name##_is_removing(const type *t) { \
+ assert(t); \
+ return FLAGS_SET(t->state, NETWORK_CONFIG_STATE_REMOVING); \
+ } \
+ static inline void name##_enter_removed(type *t) { \
+ name##_update_state(t, \
+ NETWORK_CONFIG_STATE_CONFIGURED | \
+ NETWORK_CONFIG_STATE_REMOVING, \
+ 0); \
+ }
+
+const char *address_family_to_string(AddressFamily b) _const_;
+AddressFamily address_family_from_string(const char *s) _pure_;
+
+AddressFamily link_local_address_family_from_string(const char *s) _pure_;
+
+const char *routing_policy_rule_address_family_to_string(AddressFamily b) _const_;
+AddressFamily routing_policy_rule_address_family_from_string(const char *s) _pure_;
+
+const char *nexthop_address_family_to_string(AddressFamily b) _const_;
+AddressFamily nexthop_address_family_from_string(const char *s) _pure_;
+
+const char *duplicate_address_detection_address_family_to_string(AddressFamily b) _const_;
+AddressFamily duplicate_address_detection_address_family_from_string(const char *s) _pure_;
+
+AddressFamily dhcp_deprecated_address_family_from_string(const char *s) _pure_;
+
+const char *dhcp_lease_server_type_to_string(sd_dhcp_lease_server_type_t t) _const_;
+sd_dhcp_lease_server_type_t dhcp_lease_server_type_from_string(const char *s) _pure_;
+
+int log_link_message_full_errno(Link *link, sd_netlink_message *m, int level, int err, const char *msg);
+#define log_link_message_error_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_ERR, err, msg)
+#define log_link_message_warning_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_WARNING, err, msg)
+#define log_link_message_notice_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_NOTICE, err, msg)
+#define log_link_message_info_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_INFO, err, msg)
+#define log_link_message_debug_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_DEBUG, err, msg)
+#define log_message_full_errno(m, level, err, msg) log_link_message_full_errno(NULL, m, level, err, msg)
+#define log_message_error_errno(m, err, msg) log_message_full_errno(m, LOG_ERR, err, msg)
+#define log_message_warning_errno(m, err, msg) log_message_full_errno(m, LOG_WARNING, err, msg)
+#define log_message_notice_errno(m, err, msg) log_message_full_errno(m, LOG_NOTICE, err, msg)
+#define log_message_info_errno(m, err, msg) log_message_full_errno(m, LOG_INFO, err, msg)
+#define log_message_debug_errno(m, err, msg) log_message_full_errno(m, LOG_DEBUG, err, msg)
diff --git a/src/network/networkd-wifi.c b/src/network/networkd-wifi.c
new file mode 100644
index 0000000..62cbca0
--- /dev/null
+++ b/src/network/networkd-wifi.c
@@ -0,0 +1,300 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/ethernet.h>
+#include <linux/nl80211.h>
+
+#include "ether-addr-util.h"
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-wifi.h"
+#include "networkd-wiphy.h"
+#include "string-util.h"
+#include "wifi-util.h"
+
+static int link_get_wlan_interface(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL;
+ int r;
+
+ assert(link);
+
+ r = sd_genl_message_new(link->manager->genl, NL80211_GENL_NAME, NL80211_CMD_GET_INTERFACE, &req);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to create generic netlink message: %m");
+
+ r = sd_netlink_message_append_u32(req, NL80211_ATTR_IFINDEX, link->ifindex);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Could not append NL80211_ATTR_IFINDEX attribute: %m");
+
+ r = sd_netlink_call(link->manager->genl, req, 0, &reply);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to request information about wlan interface: %m");
+ if (!reply) {
+ log_link_debug(link, "No reply received to request for information about wifi interface, ignoring.");
+ return 0;
+ }
+
+ return manager_genl_process_nl80211_config(link->manager->genl, reply, link->manager);
+}
+
+int manager_genl_process_nl80211_config(sd_netlink *genl, sd_netlink_message *message, Manager *manager) {
+ _cleanup_free_ char *ssid = NULL;
+ uint32_t ifindex, wlan_iftype;
+ const char *family, *ifname;
+ uint8_t cmd;
+ size_t len;
+ Link *link;
+ int r;
+
+ assert(genl);
+ assert(message);
+ assert(manager);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "nl80211: received error message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_genl_message_get_family_name(genl, message, &family);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: failed to determine genl family, ignoring: %m");
+ return 0;
+ }
+ if (!streq(family, NL80211_GENL_NAME)) {
+ log_debug("nl80211: received message of unexpected genl family '%s', ignoring.", family);
+ return 0;
+ }
+
+ r = sd_genl_message_get_command(genl, message, &cmd);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: failed to determine genl message command, ignoring: %m");
+ return 0;
+ }
+ if (IN_SET(cmd, NL80211_CMD_NEW_WIPHY, NL80211_CMD_DEL_WIPHY))
+ return manager_genl_process_nl80211_wiphy(genl, message, manager);
+ if (!IN_SET(cmd, NL80211_CMD_SET_INTERFACE, NL80211_CMD_NEW_INTERFACE, NL80211_CMD_DEL_INTERFACE)) {
+ log_debug("nl80211: ignoring nl80211 %s(%u) message.",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, NL80211_ATTR_IFINDEX, &ifindex);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: received %s(%u) message without valid ifindex, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ r = link_get_by_index(manager, ifindex, &link);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: received %s(%u) message for link '%"PRIu32"' we don't know about, ignoring.",
+ strna(nl80211_cmd_to_string(cmd)), cmd, ifindex);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string(message, NL80211_ATTR_IFNAME, &ifname);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid interface name, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ if (!streq(ifname, link->ifname)) {
+ log_link_debug_errno(link, r, "nl80211: received %s(%u) message with invalid interface name '%s', ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd, ifname);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, NL80211_ATTR_IFTYPE, &wlan_iftype);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid wlan interface type, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_data_suffix0(message, NL80211_ATTR_SSID, &len, (void**) &ssid);
+ if (r < 0 && r != -ENODATA) {
+ log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid SSID, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+ if (r >= 0) {
+ if (len == 0) {
+ log_link_debug(link, "nl80211: received SSID has zero length, ignoring it: %m");
+ ssid = mfree(ssid);
+ } else if (strlen_ptr(ssid) != len) {
+ log_link_debug(link, "nl80211: received SSID contains NUL characters, ignoring it.");
+ ssid = mfree(ssid);
+ }
+ }
+
+ log_link_debug(link, "nl80211: received %s(%u) message: iftype=%s, ssid=%s",
+ strna(nl80211_cmd_to_string(cmd)), cmd,
+ strna(nl80211_iftype_to_string(wlan_iftype)), strna(ssid));
+
+ switch (cmd) {
+ case NL80211_CMD_SET_INTERFACE:
+ case NL80211_CMD_NEW_INTERFACE:
+ link->wlan_iftype = wlan_iftype;
+ free_and_replace(link->ssid, ssid);
+ break;
+
+ case NL80211_CMD_DEL_INTERFACE:
+ link->wlan_iftype = NL80211_IFTYPE_UNSPECIFIED;
+ link->ssid = mfree(link->ssid);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+int manager_genl_process_nl80211_mlme(sd_netlink *genl, sd_netlink_message *message, Manager *manager) {
+ const char *family;
+ uint32_t ifindex;
+ uint8_t cmd;
+ Link *link;
+ int r;
+
+ assert(genl);
+ assert(message);
+ assert(manager);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "nl80211: received error message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_genl_message_get_family_name(genl, message, &family);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: failed to determine genl family, ignoring: %m");
+ return 0;
+ }
+ if (!streq(family, NL80211_GENL_NAME)) {
+ log_debug("nl80211: Received message of unexpected genl family '%s', ignoring.", family);
+ return 0;
+ }
+
+ r = sd_genl_message_get_command(genl, message, &cmd);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: failed to determine genl message command, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, NL80211_ATTR_IFINDEX, &ifindex);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: received %s(%u) message without valid ifindex, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ r = link_get_by_index(manager, ifindex, &link);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: received %s(%u) message for link '%"PRIu32"' we don't know about, ignoring.",
+ strna(nl80211_cmd_to_string(cmd)), cmd, ifindex);
+ return 0;
+ }
+
+ switch (cmd) {
+ case NL80211_CMD_NEW_STATION:
+ case NL80211_CMD_DEL_STATION: {
+ struct ether_addr bssid;
+
+ r = sd_netlink_message_read_ether_addr(message, NL80211_ATTR_MAC, &bssid);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid BSSID, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ log_link_debug(link, "nl80211: received %s(%u) message: bssid=%s",
+ strna(nl80211_cmd_to_string(cmd)), cmd, ETHER_ADDR_TO_STR(&bssid));
+
+ if (cmd == NL80211_CMD_DEL_STATION) {
+ link->bssid = ETHER_ADDR_NULL;
+ return 0;
+ }
+
+ link->bssid = bssid;
+
+ if (manager->enumerating &&
+ link->wlan_iftype == NL80211_IFTYPE_STATION && link->ssid)
+ log_link_info(link, "Connected WiFi access point: %s (%s)",
+ link->ssid, ETHER_ADDR_TO_STR(&link->bssid));
+ break;
+ }
+ case NL80211_CMD_CONNECT: {
+ struct ether_addr bssid;
+ uint16_t status_code;
+
+ r = sd_netlink_message_read_ether_addr(message, NL80211_ATTR_MAC, &bssid);
+ if (r < 0 && r != -ENODATA) {
+ log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid BSSID, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u16(message, NL80211_ATTR_STATUS_CODE, &status_code);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid status code, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ log_link_debug(link, "nl80211: received %s(%u) message: status=%u, bssid=%s",
+ strna(nl80211_cmd_to_string(cmd)), cmd, status_code, ETHER_ADDR_TO_STR(&bssid));
+
+ if (status_code != 0)
+ return 0;
+
+ link->bssid = bssid;
+
+ if (!manager->enumerating) {
+ r = link_get_wlan_interface(link);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to update wireless LAN interface: %m");
+ link_enter_failed(link);
+ return 0;
+ }
+ }
+
+ if (link->wlan_iftype == NL80211_IFTYPE_STATION && link->ssid)
+ log_link_info(link, "Connected WiFi access point: %s (%s)",
+ link->ssid, ETHER_ADDR_TO_STR(&link->bssid));
+
+ /* Sometimes, RTM_NEWLINK message with carrier is received earlier than NL80211_CMD_CONNECT.
+ * To make SSID= or other WiFi related settings in [Match] section work, let's try to
+ * reconfigure the interface. */
+ if (link->ssid && link_has_carrier(link)) {
+ r = link_reconfigure_impl(link, /* force = */ false);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to reconfigure interface: %m");
+ link_enter_failed(link);
+ return 0;
+ }
+ }
+ break;
+ }
+ case NL80211_CMD_DISCONNECT:
+ log_link_debug(link, "nl80211: received %s(%u) message.",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+
+ link->bssid = ETHER_ADDR_NULL;
+ free_and_replace(link->previous_ssid, link->ssid);
+ break;
+
+ default:
+ log_link_debug(link, "nl80211: received %s(%u) message.",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ }
+
+ return 0;
+}
diff --git a/src/network/networkd-wifi.h b/src/network/networkd-wifi.h
new file mode 100644
index 0000000..a4ca21e
--- /dev/null
+++ b/src/network/networkd-wifi.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-netlink.h"
+
+typedef struct Manager Manager;
+
+int manager_genl_process_nl80211_config(sd_netlink *genl, sd_netlink_message *message, Manager *manager);
+int manager_genl_process_nl80211_mlme(sd_netlink *genl, sd_netlink_message *message, Manager *manager);
diff --git a/src/network/networkd-wiphy.c b/src/network/networkd-wiphy.c
new file mode 100644
index 0000000..63874cd
--- /dev/null
+++ b/src/network/networkd-wiphy.c
@@ -0,0 +1,500 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if_arp.h>
+#include <linux/nl80211.h>
+
+#include "device-private.h"
+#include "device-util.h"
+#include "networkd-manager.h"
+#include "networkd-wiphy.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "udev-util.h"
+#include "wifi-util.h"
+
+Wiphy *wiphy_free(Wiphy *w) {
+ if (!w)
+ return NULL;
+
+ if (w->manager) {
+ hashmap_remove_value(w->manager->wiphy_by_index, UINT32_TO_PTR(w->index), w);
+ if (w->name)
+ hashmap_remove_value(w->manager->wiphy_by_name, w->name, w);
+ }
+
+ sd_device_unref(w->dev);
+ sd_device_unref(w->rfkill);
+
+ free(w->name);
+ return mfree(w);
+}
+
+static int wiphy_new(Manager *manager, sd_netlink_message *message, Wiphy **ret) {
+ _cleanup_(wiphy_freep) Wiphy *w = NULL;
+ _cleanup_free_ char *name = NULL;
+ uint32_t index;
+ int r;
+
+ assert(manager);
+ assert(message);
+
+ r = sd_netlink_message_read_u32(message, NL80211_ATTR_WIPHY, &index);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_read_string_strdup(message, NL80211_ATTR_WIPHY_NAME, &name);
+ if (r < 0)
+ return r;
+
+ w = new(Wiphy, 1);
+ if (!w)
+ return -ENOMEM;
+
+ *w = (Wiphy) {
+ .manager = manager,
+ .index = index,
+ .name = TAKE_PTR(name),
+ };
+
+ r = hashmap_ensure_put(&manager->wiphy_by_index, NULL, UINT32_TO_PTR(w->index), w);
+ if (r < 0)
+ return r;
+
+ r = hashmap_ensure_put(&w->manager->wiphy_by_name, &string_hash_ops, w->name, w);
+ if (r < 0)
+ return r;
+
+ log_wiphy_debug(w, "Saved new wiphy: index=%"PRIu32, w->index);
+
+ if (ret)
+ *ret = w;
+
+ TAKE_PTR(w);
+ return 0;
+}
+
+int wiphy_get_by_index(Manager *manager, uint32_t index, Wiphy **ret) {
+ Wiphy *w;
+
+ assert(manager);
+
+ w = hashmap_get(manager->wiphy_by_index, UINT32_TO_PTR(index));
+ if (!w)
+ return -ENODEV;
+
+ if (ret)
+ *ret = w;
+
+ return 0;
+}
+
+int wiphy_get_by_name(Manager *manager, const char *name, Wiphy **ret) {
+ Wiphy *w;
+
+ assert(manager);
+ assert(name);
+
+ w = hashmap_get(manager->wiphy_by_name, name);
+ if (!w)
+ return -ENODEV;
+
+ if (ret)
+ *ret = w;
+
+ return 0;
+}
+
+static int link_get_wiphy(Link *link, Wiphy **ret) {
+ _cleanup_(sd_device_unrefp) sd_device *phy = NULL;
+ const char *s;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ if (link->iftype != ARPHRD_ETHER)
+ return -EOPNOTSUPP;
+
+ if (!link->dev)
+ return -ENODEV;
+
+ r = sd_device_get_devtype(link->dev, &s);
+ if (r < 0)
+ return r;
+
+ if (!streq_ptr(s, "wlan"))
+ return -EOPNOTSUPP;
+
+ r = sd_device_new_child(&phy, link->dev, "phy80211");
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_sysname(phy, &s);
+ if (r < 0)
+ return r;
+
+ /* TODO:
+ * Maybe, it is better to cache the found Wiphy object in the Link object.
+ * To support that, we need to investigate what happens when the _phy_ is renamed. */
+
+ return wiphy_get_by_name(link->manager, s, ret);
+}
+
+static int rfkill_get_state(sd_device *dev) {
+ int r;
+
+ assert(dev);
+
+ /* The previous values may be outdated. Let's clear cache and re-read the values. */
+ device_clear_sysattr_cache(dev);
+
+ r = device_get_sysattr_bool(dev, "soft");
+ if (r < 0 && r != -ENOENT)
+ return r;
+ if (r > 0)
+ return RFKILL_SOFT;
+
+ r = device_get_sysattr_bool(dev, "hard");
+ if (r < 0 && r != -ENOENT)
+ return r;
+ if (r > 0)
+ return RFKILL_HARD;
+
+ return RFKILL_UNBLOCKED;
+}
+
+static int wiphy_rfkilled(Wiphy *w) {
+ int r;
+
+ assert(w);
+
+ if (!udev_available()) {
+ if (w->rfkill_state != RFKILL_UNBLOCKED) {
+ log_wiphy_debug(w, "Running in container, assuming the radio transmitter is unblocked.");
+ w->rfkill_state = RFKILL_UNBLOCKED; /* To suppress the above log message, cache the state. */
+ }
+ return false;
+ }
+
+ if (!w->rfkill) {
+ if (w->rfkill_state != RFKILL_UNBLOCKED) {
+ log_wiphy_debug(w, "No rfkill device found, assuming the radio transmitter is unblocked.");
+ w->rfkill_state = RFKILL_UNBLOCKED; /* To suppress the above log message, cache the state. */
+ }
+ return false;
+ }
+
+ r = rfkill_get_state(w->rfkill);
+ if (r < 0)
+ return log_wiphy_debug_errno(w, r, "Could not get rfkill state: %m");
+
+ if (w->rfkill_state != r)
+ switch (r) {
+ case RFKILL_UNBLOCKED:
+ log_wiphy_debug(w, "The radio transmitter is unblocked.");
+ break;
+ case RFKILL_SOFT:
+ log_wiphy_debug(w, "The radio transmitter is turned off by software.");
+ break;
+ case RFKILL_HARD:
+ log_wiphy_debug(w, "The radio transmitter is forced off by something outside of the driver's control.");
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ w->rfkill_state = r; /* Cache the state to suppress the above log messages. */
+ return r != RFKILL_UNBLOCKED;
+}
+
+int link_rfkilled(Link *link) {
+ Wiphy *w;
+ int r;
+
+ assert(link);
+
+ r = link_get_wiphy(link, &w);
+ if (r < 0) {
+ if (ERRNO_IS_NOT_SUPPORTED(r) || ERRNO_IS_DEVICE_ABSENT(r))
+ return false; /* Typically, non-wifi interface or running in container */
+ return log_link_debug_errno(link, r, "Could not get phy: %m");
+ }
+
+ return wiphy_rfkilled(w);
+}
+
+static int wiphy_update_name(Wiphy *w, sd_netlink_message *message) {
+ const char *name;
+ int r;
+
+ assert(w);
+ assert(w->manager);
+ assert(message);
+
+ r = sd_netlink_message_read_string(message, NL80211_ATTR_WIPHY_NAME, &name);
+ if (r == -ENODATA)
+ return 0;
+ if (r < 0)
+ return r;
+
+ if (streq(w->name, name))
+ return 0;
+
+ log_wiphy_debug(w, "Wiphy name change detected, renamed to %s.", name);
+
+ hashmap_remove_value(w->manager->wiphy_by_name, w->name, w);
+
+ r = free_and_strdup(&w->name, name);
+ if (r < 0)
+ return r;
+
+ r = hashmap_ensure_put(&w->manager->wiphy_by_name, &string_hash_ops, w->name, w);
+ if (r < 0)
+ return r;
+
+ return 1; /* updated */
+}
+
+static int wiphy_update_device(Wiphy *w) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ int r;
+
+ assert(w);
+ assert(w->name);
+
+ if (!udev_available())
+ return 0;
+
+ w->dev = sd_device_unref(w->dev);
+
+ r = sd_device_new_from_subsystem_sysname(&dev, "ieee80211", w->name);
+ if (r < 0)
+ return r;
+
+ if (DEBUG_LOGGING) {
+ const char *s = NULL;
+
+ (void) sd_device_get_syspath(dev, &s);
+ log_wiphy_debug(w, "Found device: %s", strna(s));
+ }
+
+ w->dev = TAKE_PTR(dev);
+ return 0;
+}
+
+static int wiphy_update_rfkill(Wiphy *w) {
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ sd_device *rfkill;
+ int r;
+
+ assert(w);
+
+ if (!udev_available())
+ return 0;
+
+ w->rfkill = sd_device_unref(w->rfkill);
+
+ if (!w->dev)
+ return 0;
+
+ r = sd_device_enumerator_new(&e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_allow_uninitialized(e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_subsystem(e, "rfkill", true);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_parent(e, w->dev);
+ if (r < 0)
+ return r;
+
+ rfkill = sd_device_enumerator_get_device_first(e);
+ if (!rfkill)
+ /* rfkill device may not detected by the kernel yet, and may appear later. */
+ return -ENODEV;
+
+ if (sd_device_enumerator_get_device_next(e))
+ return -ENXIO; /* multiple devices found */
+
+ w->rfkill = sd_device_ref(rfkill);
+
+ if (DEBUG_LOGGING) {
+ const char *s = NULL;
+
+ (void) sd_device_get_syspath(rfkill, &s);
+ log_wiphy_debug(w, "Found rfkill device: %s", strna(s));
+ }
+
+ return 0;
+}
+
+static int wiphy_update(Wiphy *w) {
+ int r;
+
+ assert(w);
+
+ r = wiphy_update_device(w);
+ if (r < 0) {
+ if (ERRNO_IS_DEVICE_ABSENT(r))
+ log_wiphy_debug_errno(w, r, "Failed to update wiphy device, ignoring: %m");
+ else
+ return log_wiphy_warning_errno(w, r, "Failed to update wiphy device: %m");
+ }
+
+ r = wiphy_update_rfkill(w);
+ if (r < 0) {
+ if (ERRNO_IS_DEVICE_ABSENT(r))
+ log_wiphy_debug_errno(w, r, "Failed to update rfkill device, ignoring: %m");
+ else
+ return log_wiphy_warning_errno(w, r, "Failed to update rfkill device: %m");
+ }
+
+ return 0;
+}
+
+int manager_genl_process_nl80211_wiphy(sd_netlink *genl, sd_netlink_message *message, Manager *manager) {
+ const char *family;
+ uint32_t index;
+ uint8_t cmd;
+ Wiphy *w = NULL;
+ int r;
+
+ assert(genl);
+ assert(message);
+ assert(manager);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "nl80211: received error message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_genl_message_get_family_name(genl, message, &family);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: failed to determine genl family, ignoring: %m");
+ return 0;
+ }
+ if (!streq(family, NL80211_GENL_NAME)) {
+ log_debug("nl80211: Received message of unexpected genl family '%s', ignoring.", family);
+ return 0;
+ }
+
+ r = sd_genl_message_get_command(genl, message, &cmd);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: failed to determine genl message command, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_u32(message, NL80211_ATTR_WIPHY, &index);
+ if (r < 0) {
+ log_debug_errno(r, "nl80211: received %s(%u) message without valid index, ignoring: %m",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ return 0;
+ }
+
+ (void) wiphy_get_by_index(manager, index, &w);
+
+ switch (cmd) {
+ case NL80211_CMD_NEW_WIPHY: {
+
+ if (!w) {
+ r = wiphy_new(manager, message, &w);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to save new wiphy, ignoring: %m");
+ return 0;
+ }
+ } else {
+ r = wiphy_update_name(w, message);
+ if (r < 0) {
+ log_wiphy_warning_errno(w, r, "Failed to update wiphy name, ignoring: %m");
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+ }
+
+ r = wiphy_update(w);
+ if (r < 0)
+ log_wiphy_warning_errno(w, r, "Failed to update wiphy, ignoring: %m");
+
+ break;
+ }
+ case NL80211_CMD_DEL_WIPHY:
+
+ if (!w) {
+ log_debug("The kernel removes wiphy we do not know, ignoring: %m");
+ return 0;
+ }
+
+ log_wiphy_debug(w, "Removed.");
+ wiphy_free(w);
+ break;
+
+ default:
+ log_wiphy_debug(w, "nl80211: received %s(%u) message.",
+ strna(nl80211_cmd_to_string(cmd)), cmd);
+ }
+
+ return 0;
+}
+
+int manager_udev_process_wiphy(Manager *m, sd_device *device, sd_device_action_t action) {
+ const char *name;
+ Wiphy *w;
+ int r;
+
+ assert(m);
+ assert(device);
+
+ r = sd_device_get_sysname(device, &name);
+ if (r < 0)
+ return log_device_debug_errno(device, r, "Failed to get sysname: %m");
+
+ r = wiphy_get_by_name(m, name, &w);
+ if (r < 0) {
+ /* This error is not critical, as the corresponding genl message may be received later. */
+ log_device_debug_errno(device, r, "Failed to get Wiphy object, ignoring: %m");
+ return 0;
+ }
+
+ return device_unref_and_replace(w->dev, action == SD_DEVICE_REMOVE ? NULL : device);
+}
+
+int manager_udev_process_rfkill(Manager *m, sd_device *device, sd_device_action_t action) {
+ _cleanup_free_ char *parent_path = NULL, *parent_name = NULL;
+ const char *s;
+ Wiphy *w;
+ int r;
+
+ assert(m);
+ assert(device);
+
+ r = sd_device_get_syspath(device, &s);
+ if (r < 0)
+ return log_device_debug_errno(device, r, "Failed to get syspath: %m");
+
+ /* Do not use sd_device_get_parent() here, as this might be a 'remove' uevent. */
+ r = path_extract_directory(s, &parent_path);
+ if (r < 0)
+ return log_device_debug_errno(device, r, "Failed to get parent syspath: %m");
+
+ r = path_extract_filename(parent_path, &parent_name);
+ if (r < 0)
+ return log_device_debug_errno(device, r, "Failed to get parent name: %m");
+
+ r = wiphy_get_by_name(m, parent_name, &w);
+ if (r < 0) {
+ /* This error is not critical, as the corresponding genl message may be received later. */
+ log_device_debug_errno(device, r, "Failed to get Wiphy object: %m");
+ return 0;
+ }
+
+ return device_unref_and_replace(w->rfkill, action == SD_DEVICE_REMOVE ? NULL : device);
+}
diff --git a/src/network/networkd-wiphy.h b/src/network/networkd-wiphy.h
new file mode 100644
index 0000000..b9056e8
--- /dev/null
+++ b/src/network/networkd-wiphy.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+
+#include "sd-device.h"
+
+#include "macro.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+
+/* The following values are different from the ones defined in linux/rfkill.h. */
+typedef enum RFKillState {
+ RFKILL_UNKNOWN,
+ RFKILL_UNBLOCKED,
+ RFKILL_SOFT,
+ RFKILL_HARD,
+ _RFKILL_STATE_MAX,
+ _RFKILL_STATE_INVALID = -EINVAL,
+} RFKillState;
+
+typedef struct Wiphy {
+ Manager *manager;
+
+ uint32_t index;
+ char *name;
+
+ sd_device *dev;
+ sd_device *rfkill;
+ RFKillState rfkill_state;
+} Wiphy;
+
+Wiphy *wiphy_free(Wiphy *w);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Wiphy*, wiphy_free);
+
+int wiphy_get_by_index(Manager *manager, uint32_t index, Wiphy **ret);
+int wiphy_get_by_name(Manager *manager, const char *name, Wiphy **ret);
+
+int link_rfkilled(Link *link);
+
+int manager_genl_process_nl80211_wiphy(sd_netlink *genl, sd_netlink_message *message, Manager *manager);
+int manager_udev_process_wiphy(Manager *m, sd_device *device, sd_device_action_t action);
+int manager_udev_process_rfkill(Manager *m, sd_device *device, sd_device_action_t action);
+
+#define log_wiphy_full_errno_zerook(w, level, error, ...) \
+ ({ \
+ const Wiphy *_w = (w); \
+ log_interface_full_errno_zerook(_w ? _w->name : NULL, level, error, __VA_ARGS__); \
+ })
+
+#define log_wiphy_full_errno(w, level, error, ...) \
+ ({ \
+ int _error = (error); \
+ ASSERT_NON_ZERO(_error); \
+ log_wiphy_full_errno_zerook(w, level, _error, __VA_ARGS__); \
+ })
+
+#define log_wiphy_full(w, level, ...) (void) log_wiphy_full_errno_zerook(w, level, 0, __VA_ARGS__)
+
+#define log_wiphy_debug(w, ...) log_wiphy_full(w, LOG_DEBUG, __VA_ARGS__)
+#define log_wiphy_info(w, ...) log_wiphy_full(w, LOG_INFO, __VA_ARGS__)
+#define log_wiphy_notice(w, ...) log_wiphy_full(w, LOG_NOTICE, __VA_ARGS__)
+#define log_wiphy_warning(w, ...) log_wiphy_full(w, LOG_WARNING, __VA_ARGS__)
+#define log_wiphy_error(w, ...) log_wiphy_full(w, LOG_ERR, __VA_ARGS__)
+
+#define log_wiphy_debug_errno(w, error, ...) log_wiphy_full_errno(w, LOG_DEBUG, error, __VA_ARGS__)
+#define log_wiphy_info_errno(w, error, ...) log_wiphy_full_errno(w, LOG_INFO, error, __VA_ARGS__)
+#define log_wiphy_notice_errno(w, error, ...) log_wiphy_full_errno(w, LOG_NOTICE, error, __VA_ARGS__)
+#define log_wiphy_warning_errno(w, error, ...) log_wiphy_full_errno(w, LOG_WARNING, error, __VA_ARGS__)
+#define log_wiphy_error_errno(w, error, ...) log_wiphy_full_errno(w, LOG_ERR, error, __VA_ARGS__)
diff --git a/src/network/networkd.c b/src/network/networkd.c
new file mode 100644
index 0000000..d61769d
--- /dev/null
+++ b/src/network/networkd.c
@@ -0,0 +1,121 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "sd-daemon.h"
+#include "sd-event.h"
+
+#include "bus-log-control-api.h"
+#include "capability-util.h"
+#include "daemon-util.h"
+#include "firewall-util.h"
+#include "main-func.h"
+#include "mkdir-label.h"
+#include "networkd-conf.h"
+#include "networkd-manager-bus.h"
+#include "networkd-manager.h"
+#include "service-util.h"
+#include "signal-util.h"
+#include "user-util.h"
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _unused_ _cleanup_(notify_on_cleanup) const char *notify_message = NULL;
+ int r;
+
+ log_setup();
+
+ r = service_parse_argv("systemd-networkd.service",
+ "Manage and configure network devices, create virtual network devices",
+ BUS_IMPLEMENTATIONS(&manager_object, &log_control_object),
+ argc, argv);
+ if (r <= 0)
+ return r;
+
+ umask(0022);
+
+ if (argc != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
+
+ /* Drop privileges, but only if we have been started as root. If we are not running as root we assume all
+ * privileges are already dropped and we can't create our runtime directory. */
+ if (geteuid() == 0) {
+ const char *user = "systemd-network";
+ uid_t uid;
+ gid_t gid;
+
+ r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0);
+ if (r < 0)
+ return log_error_errno(r, "Cannot resolve user name %s: %m", user);
+
+ /* Create runtime directory. This is not necessary when networkd is
+ * started with "RuntimeDirectory=systemd/netif", or after
+ * systemd-tmpfiles-setup.service. */
+ r = mkdir_safe_label("/run/systemd/netif", 0755, uid, gid, MKDIR_WARN_MODE);
+ if (r < 0)
+ log_warning_errno(r, "Could not create runtime directory: %m");
+
+ r = drop_privileges(uid, gid,
+ (1ULL << CAP_NET_ADMIN) |
+ (1ULL << CAP_NET_BIND_SERVICE) |
+ (1ULL << CAP_NET_BROADCAST) |
+ (1ULL << CAP_NET_RAW));
+ if (r < 0)
+ return log_error_errno(r, "Failed to drop privileges: %m");
+ }
+
+ /* Always create the directories people can create inotify watches in.
+ * It is necessary to create the following subdirectories after drop_privileges()
+ * to support old kernels not supporting AmbientCapabilities=. */
+ r = mkdir_safe_label("/run/systemd/netif/links", 0755, UID_INVALID, GID_INVALID, MKDIR_WARN_MODE);
+ if (r < 0)
+ log_warning_errno(r, "Could not create runtime directory 'links': %m");
+
+ r = mkdir_safe_label("/run/systemd/netif/leases", 0755, UID_INVALID, GID_INVALID, MKDIR_WARN_MODE);
+ if (r < 0)
+ log_warning_errno(r, "Could not create runtime directory 'leases': %m");
+
+ r = mkdir_safe_label("/run/systemd/netif/lldp", 0755, UID_INVALID, GID_INVALID, MKDIR_WARN_MODE);
+ if (r < 0)
+ log_warning_errno(r, "Could not create runtime directory 'lldp': %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+
+ r = manager_new(&m, /* test_mode = */ false);
+ if (r < 0)
+ return log_error_errno(r, "Could not create manager: %m");
+
+ r = manager_setup(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not setup manager: %m");
+
+ r = manager_parse_config_file(m);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse configuration file: %m");
+
+ r = manager_load_config(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not load configuration files: %m");
+
+ r = manager_enumerate(m);
+ if (r < 0)
+ return r;
+
+ r = manager_start(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not start manager: %m");
+
+ log_info("Enumeration completed");
+
+ notify_message = notify_start(NOTIFY_READY, NOTIFY_STOPPING);
+
+ r = sd_event_loop(m->event);
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/network/networkd.conf b/src/network/networkd.conf
new file mode 100644
index 0000000..38dc9f1
--- /dev/null
+++ b/src/network/networkd.conf
@@ -0,0 +1,28 @@
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Entries in this file show the compile time defaults. Local configuration
+# should be created by either modifying this file, or by creating "drop-ins" in
+# the networkd.conf.d/ subdirectory. The latter is generally recommended.
+# Defaults can be restored by simply deleting this file and all drop-ins.
+#
+# See networkd.conf(5) for details.
+
+[Network]
+#SpeedMeter=no
+#SpeedMeterIntervalSec=10sec
+#ManageForeignRoutingPolicyRules=yes
+#ManageForeignRoutes=yes
+#RouteTable=
+
+[DHCPv4]
+#DUIDType=vendor
+#DUIDRawData=
+
+[DHCPv6]
+#DUIDType=vendor
+#DUIDRawData=
diff --git a/src/network/org.freedesktop.network1.conf b/src/network/org.freedesktop.network1.conf
new file mode 100644
index 0000000..5bd796d
--- /dev/null
+++ b/src/network/org.freedesktop.network1.conf
@@ -0,0 +1,27 @@
+<?xml version="1.0"?> <!--*-nxml-*-->
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+
+<!--
+ This file is part of systemd.
+
+ systemd is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+-->
+
+<busconfig>
+
+ <policy user="systemd-network">
+ <allow own="org.freedesktop.network1"/>
+ <allow send_destination="org.freedesktop.network1"/>
+ <allow receive_sender="org.freedesktop.network1"/>
+ </policy>
+
+ <policy context="default">
+ <allow send_destination="org.freedesktop.network1"/>
+ <allow receive_sender="org.freedesktop.network1"/>
+ </policy>
+
+</busconfig>
diff --git a/src/network/org.freedesktop.network1.policy b/src/network/org.freedesktop.network1.policy
new file mode 100644
index 0000000..1e2d8d7
--- /dev/null
+++ b/src/network/org.freedesktop.network1.policy
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
+<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+
+<!--
+ SPDX-License-Identifier: LGPL-2.1-or-later
+
+ This file is part of systemd.
+
+ systemd is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+-->
+
+<policyconfig>
+
+ <vendor>The systemd Project</vendor>
+ <vendor_url>https://systemd.io</vendor_url>
+
+ <action id="org.freedesktop.network1.set-ntp-servers">
+ <description gettext-domain="systemd">Set NTP servers</description>
+ <message gettext-domain="systemd">Authentication is required to set NTP servers.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.set-dns-servers">
+ <description gettext-domain="systemd">Set DNS servers</description>
+ <message gettext-domain="systemd">Authentication is required to set DNS servers.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.set-domains">
+ <description gettext-domain="systemd">Set domains</description>
+ <message gettext-domain="systemd">Authentication is required to set domains.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.set-default-route">
+ <description gettext-domain="systemd">Set default route</description>
+ <message gettext-domain="systemd">Authentication is required to set default route.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.set-llmnr">
+ <description gettext-domain="systemd">Enable/disable LLMNR</description>
+ <message gettext-domain="systemd">Authentication is required to enable or disable LLMNR.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.set-mdns">
+ <description gettext-domain="systemd">Enable/disable multicast DNS</description>
+ <message gettext-domain="systemd">Authentication is required to enable or disable multicast DNS.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.set-dns-over-tls">
+ <description gettext-domain="systemd">Enable/disable DNS over TLS</description>
+ <message gettext-domain="systemd">Authentication is required to enable or disable DNS over TLS.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.set-dnssec">
+ <description gettext-domain="systemd">Enable/disable DNSSEC</description>
+ <message gettext-domain="systemd">Authentication is required to enable or disable DNSSEC.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.set-dnssec-negative-trust-anchors">
+ <description gettext-domain="systemd">Set DNSSEC Negative Trust Anchors</description>
+ <message gettext-domain="systemd">Authentication is required to set DNSSEC Negative Trust Anchors.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.revert-ntp">
+ <description gettext-domain="systemd">Revert NTP settings</description>
+ <message gettext-domain="systemd">Authentication is required to reset NTP settings.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.revert-dns">
+ <description gettext-domain="systemd">Revert DNS settings</description>
+ <message gettext-domain="systemd">Authentication is required to reset DNS settings.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.forcerenew">
+ <description gettext-domain="systemd">DHCP server sends force renew message</description>
+ <message gettext-domain="systemd">Authentication is required to send force renew message.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.renew">
+ <description gettext-domain="systemd">Renew dynamic addresses</description>
+ <message gettext-domain="systemd">Authentication is required to renew dynamic addresses.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.reload">
+ <description gettext-domain="systemd">Reload network settings</description>
+ <message gettext-domain="systemd">Authentication is required to reload network settings.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+ <action id="org.freedesktop.network1.reconfigure">
+ <description gettext-domain="systemd">Reconfigure network interface</description>
+ <message gettext-domain="systemd">Authentication is required to reconfigure network interface.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate>
+ </action>
+
+</policyconfig>
diff --git a/src/network/org.freedesktop.network1.service b/src/network/org.freedesktop.network1.service
new file mode 100644
index 0000000..ddbf3eb
--- /dev/null
+++ b/src/network/org.freedesktop.network1.service
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[D-BUS Service]
+Name=org.freedesktop.network1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.network1.service
diff --git a/src/network/systemd-networkd.pkla b/src/network/systemd-networkd.pkla
new file mode 100644
index 0000000..c56ea1b
--- /dev/null
+++ b/src/network/systemd-networkd.pkla
@@ -0,0 +1,7 @@
+# This file is part of systemd.
+# See systemd-networkd.service(8) and polkit(8) for more information.
+
+[Allow systemd-networkd to set timezone and transient hostname]
+Identity=unix-user:systemd-network
+Action=org.freedesktop.hostname1.set-hostname;org.freedesktop.hostname1.get-product-uuid;org.freedesktop.timedate1.set-timezone;
+ResultAny=yes
diff --git a/src/network/systemd-networkd.rules b/src/network/systemd-networkd.rules
new file mode 100644
index 0000000..86cc849
--- /dev/null
+++ b/src/network/systemd-networkd.rules
@@ -0,0 +1,13 @@
+// This file is part of systemd.
+// See systemd-networkd.service(8) and polkit(8) for more information.
+
+// Allow systemd-networkd to set timezone, get product UUID,
+// and transient hostname
+polkit.addRule(function(action, subject) {
+ if ((action.id == "org.freedesktop.hostname1.set-hostname" ||
+ action.id == "org.freedesktop.hostname1.get-product-uuid" ||
+ action.id == "org.freedesktop.timedate1.set-timezone") &&
+ subject.user == "systemd-network") {
+ return polkit.Result.YES;
+ }
+});
diff --git a/src/network/tc/cake.c b/src/network/tc/cake.c
new file mode 100644
index 0000000..8d770b0
--- /dev/null
+++ b/src/network/tc/cake.c
@@ -0,0 +1,613 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "cake.h"
+#include "conf-parser.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "string-table.h"
+#include "string-util.h"
+
+static int cake_init(QDisc *qdisc) {
+ CommonApplicationsKeptEnhanced *c;
+
+ assert(qdisc);
+
+ c = CAKE(qdisc);
+
+ c->autorate = -1;
+ c->compensation_mode = _CAKE_COMPENSATION_MODE_INVALID;
+ c->raw = -1;
+ c->flow_isolation_mode = _CAKE_FLOW_ISOLATION_MODE_INVALID;
+ c->nat = -1;
+ c->preset = _CAKE_PRESET_INVALID;
+ c->wash = -1;
+ c->split_gso = -1;
+
+ return 0;
+}
+
+static int cake_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ CommonApplicationsKeptEnhanced *c;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(c = CAKE(qdisc));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "cake");
+ if (r < 0)
+ return r;
+
+ if (c->bandwidth > 0) {
+ r = sd_netlink_message_append_u64(req, TCA_CAKE_BASE_RATE64, c->bandwidth);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->autorate >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_AUTORATE, c->autorate);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->overhead_set) {
+ r = sd_netlink_message_append_s32(req, TCA_CAKE_OVERHEAD, c->overhead);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->mpu > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_MPU, c->mpu);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->compensation_mode >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_ATM, c->compensation_mode);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->raw > 0) {
+ /* TCA_CAKE_RAW attribute is mostly a flag, not boolean. */
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_RAW, 0);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->flow_isolation_mode >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_FLOW_MODE, c->flow_isolation_mode);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->nat >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_NAT, c->nat);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->preset >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_DIFFSERV_MODE, c->preset);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->fwmark > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_FWMARK, c->fwmark);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->wash >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_WASH, c->wash);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->split_gso >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CAKE_SPLIT_GSO, c->split_gso);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_cake_bandwidth(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ CommonApplicationsKeptEnhanced *c;
+ Network *network = ASSERT_PTR(data);
+ uint64_t k;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ c = CAKE(qdisc);
+
+ if (isempty(rvalue)) {
+ c->bandwidth = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1000, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ c->bandwidth = k/8;
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int config_parse_cake_overhead(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ CommonApplicationsKeptEnhanced *c;
+ Network *network = ASSERT_PTR(data);
+ int32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ c = CAKE(qdisc);
+
+ if (isempty(rvalue)) {
+ c->overhead_set = false;
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atoi32(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (v < -64 || v > 256) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ c->overhead = v;
+ c->overhead_set = true;
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+int config_parse_cake_mpu(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ CommonApplicationsKeptEnhanced *c;
+ Network *network = ASSERT_PTR(data);
+ uint32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ c = CAKE(qdisc);
+
+ if (isempty(rvalue)) {
+ c->mpu = 0;
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (v <= 0 || v > 256) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ c->mpu = v;
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+int config_parse_cake_tristate(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ CommonApplicationsKeptEnhanced *c;
+ Network *network = ASSERT_PTR(data);
+ int *dest, r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ c = CAKE(qdisc);
+
+ if (streq(lvalue, "AutoRateIngress"))
+ dest = &c->autorate;
+ else if (streq(lvalue, "UseRawPacketSize"))
+ dest = &c->raw;
+ else if (streq(lvalue, "NAT"))
+ dest = &c->nat;
+ else if (streq(lvalue, "Wash"))
+ dest = &c->wash;
+ else if (streq(lvalue, "SplitGSO"))
+ dest = &c->split_gso;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *dest = -1;
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ *dest = r;
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+static const char * const cake_compensation_mode_table[_CAKE_COMPENSATION_MODE_MAX] = {
+ [CAKE_COMPENSATION_MODE_NONE] = "none",
+ [CAKE_COMPENSATION_MODE_ATM] = "atm",
+ [CAKE_COMPENSATION_MODE_PTM] = "ptm",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(cake_compensation_mode, CakeCompensationMode);
+
+int config_parse_cake_compensation_mode(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ CommonApplicationsKeptEnhanced *c;
+ Network *network = ASSERT_PTR(data);
+ CakeCompensationMode mode;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ c = CAKE(qdisc);
+
+ if (isempty(rvalue)) {
+ c->compensation_mode = _CAKE_COMPENSATION_MODE_INVALID;
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ mode = cake_compensation_mode_from_string(rvalue);
+ if (mode < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, mode,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ c->compensation_mode = mode;
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+static const char * const cake_flow_isolation_mode_table[_CAKE_FLOW_ISOLATION_MODE_MAX] = {
+ [CAKE_FLOW_ISOLATION_MODE_NONE] = "none",
+ [CAKE_FLOW_ISOLATION_MODE_SRC_IP] = "src-host",
+ [CAKE_FLOW_ISOLATION_MODE_DST_IP] = "dst-host",
+ [CAKE_FLOW_ISOLATION_MODE_HOSTS] = "hosts",
+ [CAKE_FLOW_ISOLATION_MODE_FLOWS] = "flows",
+ [CAKE_FLOW_ISOLATION_MODE_DUAL_SRC] = "dual-src-host",
+ [CAKE_FLOW_ISOLATION_MODE_DUAL_DST] = "dual-dst-host",
+ [CAKE_FLOW_ISOLATION_MODE_TRIPLE] = "triple",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(cake_flow_isolation_mode, CakeFlowIsolationMode);
+
+int config_parse_cake_flow_isolation_mode(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ CommonApplicationsKeptEnhanced *c;
+ Network *network = ASSERT_PTR(data);
+ CakeFlowIsolationMode mode;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ c = CAKE(qdisc);
+
+ if (isempty(rvalue)) {
+ c->flow_isolation_mode = _CAKE_FLOW_ISOLATION_MODE_INVALID;
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ mode = cake_flow_isolation_mode_from_string(rvalue);
+ if (mode < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, mode,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ c->flow_isolation_mode = mode;
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+static const char * const cake_priority_queueing_preset_table[_CAKE_PRESET_MAX] = {
+ [CAKE_PRESET_DIFFSERV3] = "diffserv3",
+ [CAKE_PRESET_DIFFSERV4] = "diffserv4",
+ [CAKE_PRESET_DIFFSERV8] = "diffserv8",
+ [CAKE_PRESET_BESTEFFORT] = "besteffort",
+ [CAKE_PRESET_PRECEDENCE] = "precedence",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(cake_priority_queueing_preset, CakePriorityQueueingPreset);
+
+int config_parse_cake_priority_queueing_preset(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ CommonApplicationsKeptEnhanced *c;
+ CakePriorityQueueingPreset preset;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ c = CAKE(qdisc);
+
+ if (isempty(rvalue)) {
+ c->preset = _CAKE_PRESET_INVALID;
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ preset = cake_priority_queueing_preset_from_string(rvalue);
+ if (preset < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, preset,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ c->preset = preset;
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+int config_parse_cake_fwmark(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ CommonApplicationsKeptEnhanced *c;
+ Network *network = ASSERT_PTR(data);
+ uint32_t fwmark;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ c = CAKE(qdisc);
+
+ if (isempty(rvalue)) {
+ c->fwmark = 0;
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &fwmark);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (fwmark <= 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ c->fwmark = fwmark;
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+const QDiscVTable cake_vtable = {
+ .object_size = sizeof(CommonApplicationsKeptEnhanced),
+ .tca_kind = "cake",
+ .init = cake_init,
+ .fill_message = cake_fill_message,
+};
diff --git a/src/network/tc/cake.h b/src/network/tc/cake.h
new file mode 100644
index 0000000..ff68ced
--- /dev/null
+++ b/src/network/tc/cake.h
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include <linux/pkt_sched.h>
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef enum CakeCompensationMode {
+ CAKE_COMPENSATION_MODE_NONE = CAKE_ATM_NONE,
+ CAKE_COMPENSATION_MODE_ATM = CAKE_ATM_ATM,
+ CAKE_COMPENSATION_MODE_PTM = CAKE_ATM_PTM,
+ _CAKE_COMPENSATION_MODE_MAX,
+ _CAKE_COMPENSATION_MODE_INVALID = -EINVAL,
+} CakeCompensationMode;
+
+typedef enum CakeFlowIsolationMode {
+ CAKE_FLOW_ISOLATION_MODE_NONE = CAKE_FLOW_NONE,
+ CAKE_FLOW_ISOLATION_MODE_SRC_IP = CAKE_FLOW_SRC_IP,
+ CAKE_FLOW_ISOLATION_MODE_DST_IP = CAKE_FLOW_DST_IP,
+ CAKE_FLOW_ISOLATION_MODE_HOSTS = CAKE_FLOW_HOSTS,
+ CAKE_FLOW_ISOLATION_MODE_FLOWS = CAKE_FLOW_FLOWS,
+ CAKE_FLOW_ISOLATION_MODE_DUAL_SRC = CAKE_FLOW_DUAL_SRC,
+ CAKE_FLOW_ISOLATION_MODE_DUAL_DST = CAKE_FLOW_DUAL_DST,
+ CAKE_FLOW_ISOLATION_MODE_TRIPLE = CAKE_FLOW_TRIPLE,
+ _CAKE_FLOW_ISOLATION_MODE_MAX,
+ _CAKE_FLOW_ISOLATION_MODE_INVALID = -EINVAL,
+} CakeFlowIsolationMode;
+
+typedef enum CakePriorityQueueingPreset {
+ CAKE_PRESET_DIFFSERV3 = CAKE_DIFFSERV_DIFFSERV3,
+ CAKE_PRESET_DIFFSERV4 = CAKE_DIFFSERV_DIFFSERV4,
+ CAKE_PRESET_DIFFSERV8 = CAKE_DIFFSERV_DIFFSERV8,
+ CAKE_PRESET_BESTEFFORT = CAKE_DIFFSERV_BESTEFFORT,
+ CAKE_PRESET_PRECEDENCE = CAKE_DIFFSERV_PRECEDENCE,
+ _CAKE_PRESET_MAX,
+ _CAKE_PRESET_INVALID = -EINVAL,
+} CakePriorityQueueingPreset;
+
+typedef struct CommonApplicationsKeptEnhanced {
+ QDisc meta;
+
+ /* Shaper parameters */
+ int autorate;
+ uint64_t bandwidth;
+
+ /* Overhead compensation parameters */
+ bool overhead_set;
+ int overhead;
+ uint32_t mpu;
+ CakeCompensationMode compensation_mode;
+ int raw;
+
+ /* Flow isolation parameters */
+ CakeFlowIsolationMode flow_isolation_mode;
+ int nat;
+
+ /* Priority queue parameters */
+ CakePriorityQueueingPreset preset;
+ uint32_t fwmark;
+
+ /* Other parameters */
+ int wash;
+ int split_gso;
+
+} CommonApplicationsKeptEnhanced;
+
+DEFINE_QDISC_CAST(CAKE, CommonApplicationsKeptEnhanced);
+extern const QDiscVTable cake_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_cake_bandwidth);
+CONFIG_PARSER_PROTOTYPE(config_parse_cake_overhead);
+CONFIG_PARSER_PROTOTYPE(config_parse_cake_mpu);
+CONFIG_PARSER_PROTOTYPE(config_parse_cake_tristate);
+CONFIG_PARSER_PROTOTYPE(config_parse_cake_compensation_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_cake_flow_isolation_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_cake_priority_queueing_preset);
+CONFIG_PARSER_PROTOTYPE(config_parse_cake_fwmark);
diff --git a/src/network/tc/codel.c b/src/network/tc/codel.c
new file mode 100644
index 0000000..b5f95f7
--- /dev/null
+++ b/src/network/tc/codel.c
@@ -0,0 +1,252 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "string-util.h"
+
+static int controlled_delay_init(QDisc *qdisc) {
+ ControlledDelay *cd;
+
+ assert(qdisc);
+
+ cd = CODEL(qdisc);
+
+ cd->ce_threshold_usec = USEC_INFINITY;
+ cd->ecn = -1;
+
+ return 0;
+}
+
+static int controlled_delay_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ ControlledDelay *cd;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(cd = CODEL(qdisc));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "codel");
+ if (r < 0)
+ return r;
+
+ if (cd->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_LIMIT, cd->packet_limit);
+ if (r < 0)
+ return r;
+ }
+
+ if (cd->interval_usec > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_INTERVAL, cd->interval_usec);
+ if (r < 0)
+ return r;
+ }
+
+ if (cd->target_usec > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_TARGET, cd->target_usec);
+ if (r < 0)
+ return r;
+ }
+
+ if (cd->ecn >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_ECN, cd->ecn);
+ if (r < 0)
+ return r;
+ }
+
+ if (cd->ce_threshold_usec != USEC_INFINITY) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_CE_THRESHOLD, cd->ce_threshold_usec);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_controlled_delay_u32(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ ControlledDelay *cd;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CODEL, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ cd = CODEL(qdisc);
+
+ if (isempty(rvalue)) {
+ cd->packet_limit = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &cd->packet_limit);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_controlled_delay_usec(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ ControlledDelay *cd;
+ Network *network = ASSERT_PTR(data);
+ usec_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CODEL, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ cd = CODEL(qdisc);
+
+ if (streq(lvalue, "TargetSec"))
+ p = &cd->target_usec;
+ else if (streq(lvalue, "IntervalSec"))
+ p = &cd->interval_usec;
+ else if (streq(lvalue, "CEThresholdSec"))
+ p = &cd->ce_threshold_usec;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ if (streq(lvalue, "CEThresholdSec"))
+ *p = USEC_INFINITY;
+ else
+ *p = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = parse_sec(rvalue, p);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_controlled_delay_bool(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ ControlledDelay *cd;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_CODEL, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ cd = CODEL(qdisc);
+
+ if (isempty(rvalue)) {
+ cd->ecn = -1;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ cd->ecn = r;
+ qdisc = NULL;
+
+ return 0;
+}
+
+const QDiscVTable codel_vtable = {
+ .object_size = sizeof(ControlledDelay),
+ .tca_kind = "codel",
+ .init = controlled_delay_init,
+ .fill_message = controlled_delay_fill_message,
+};
diff --git a/src/network/tc/codel.h b/src/network/tc/codel.h
new file mode 100644
index 0000000..4fe5283
--- /dev/null
+++ b/src/network/tc/codel.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+#include "time-util.h"
+
+typedef struct ControlledDelay {
+ QDisc meta;
+
+ uint32_t packet_limit;
+ usec_t interval_usec;
+ usec_t target_usec;
+ usec_t ce_threshold_usec;
+ int ecn;
+} ControlledDelay;
+
+DEFINE_QDISC_CAST(CODEL, ControlledDelay);
+extern const QDiscVTable codel_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_controlled_delay_u32);
+CONFIG_PARSER_PROTOTYPE(config_parse_controlled_delay_usec);
+CONFIG_PARSER_PROTOTYPE(config_parse_controlled_delay_bool);
diff --git a/src/network/tc/drr.c b/src/network/tc/drr.c
new file mode 100644
index 0000000..373911b
--- /dev/null
+++ b/src/network/tc/drr.c
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "drr.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+
+const QDiscVTable drr_vtable = {
+ .object_size = sizeof(DeficitRoundRobinScheduler),
+ .tca_kind = "drr",
+};
+
+static int drr_class_fill_message(Link *link, TClass *tclass, sd_netlink_message *req) {
+ DeficitRoundRobinSchedulerClass *drr;
+ int r;
+
+ assert(link);
+ assert(tclass);
+ assert(req);
+
+ assert_se(drr = TCLASS_TO_DRR(tclass));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "drr");
+ if (r < 0)
+ return r;
+
+ if (drr->quantum > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_DRR_QUANTUM, drr->quantum);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_drr_size(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL;
+ DeficitRoundRobinSchedulerClass *drr;
+ Network *network = ASSERT_PTR(data);
+ uint64_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = tclass_new_static(TCLASS_KIND_DRR, network, filename, section_line, &tclass);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to create traffic control class, ignoring assignment: %m");
+ return 0;
+ }
+
+ drr = TCLASS_TO_DRR(tclass);
+
+ if (isempty(rvalue)) {
+ drr->quantum = 0;
+
+ TAKE_PTR(tclass);
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1024, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (u > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ drr->quantum = (uint32_t) u;
+
+ TAKE_PTR(tclass);
+ return 0;
+}
+
+const TClassVTable drr_tclass_vtable = {
+ .object_size = sizeof(DeficitRoundRobinSchedulerClass),
+ .tca_kind = "drr",
+ .fill_message = drr_class_fill_message,
+};
diff --git a/src/network/tc/drr.h b/src/network/tc/drr.h
new file mode 100644
index 0000000..c96cc4d
--- /dev/null
+++ b/src/network/tc/drr.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "qdisc.h"
+
+typedef struct DeficitRoundRobinScheduler {
+ QDisc meta;
+} DeficitRoundRobinScheduler;
+
+DEFINE_QDISC_CAST(DRR, DeficitRoundRobinScheduler);
+extern const QDiscVTable drr_vtable;
+
+typedef struct DeficitRoundRobinSchedulerClass {
+ TClass meta;
+
+ uint32_t quantum;
+} DeficitRoundRobinSchedulerClass;
+
+DEFINE_TCLASS_CAST(DRR, DeficitRoundRobinSchedulerClass);
+extern const TClassVTable drr_tclass_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_drr_size);
diff --git a/src/network/tc/ets.c b/src/network/tc/ets.c
new file mode 100644
index 0000000..730b0a1
--- /dev/null
+++ b/src/network/tc/ets.c
@@ -0,0 +1,342 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "ets.h"
+#include "extract-word.h"
+#include "memory-util.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "string-util.h"
+#include "tc-util.h"
+
+static int enhanced_transmission_selection_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ EnhancedTransmissionSelection *ets;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(ets = ETS(qdisc));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "ets");
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_u8(req, TCA_ETS_NBANDS, ets->n_bands);
+ if (r < 0)
+ return r;
+
+ if (ets->n_strict > 0) {
+ r = sd_netlink_message_append_u8(req, TCA_ETS_NSTRICT, ets->n_strict);
+ if (r < 0)
+ return r;
+ }
+
+ if (ets->n_quanta > 0) {
+ r = sd_netlink_message_open_container(req, TCA_ETS_QUANTA);
+ if (r < 0)
+ return r;
+
+ for (unsigned i = 0; i < ets->n_quanta; i++) {
+ r = sd_netlink_message_append_u32(req, TCA_ETS_QUANTA_BAND, ets->quanta[i]);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+ }
+
+ if (ets->n_prio > 0) {
+ r = sd_netlink_message_open_container(req, TCA_ETS_PRIOMAP);
+ if (r < 0)
+ return r;
+
+ for (unsigned i = 0; i < ets->n_prio; i++) {
+ r = sd_netlink_message_append_u8(req, TCA_ETS_PRIOMAP_BAND, ets->prio[i]);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_ets_u8(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ EnhancedTransmissionSelection *ets;
+ Network *network = ASSERT_PTR(data);
+ uint8_t v, *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_ETS, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ ets = ETS(qdisc);
+ if (streq(lvalue, "Bands"))
+ p = &ets->n_bands;
+ else if (streq(lvalue, "StrictBands"))
+ p = &ets->n_strict;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *p = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = safe_atou8(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (v > TCQ_ETS_MAX_BANDS) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s='. The value must be <= %d, ignoring assignment: %s",
+ lvalue, TCQ_ETS_MAX_BANDS, rvalue);
+ return 0;
+ }
+
+ *p = v;
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_ets_quanta(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ EnhancedTransmissionSelection *ets;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_ETS, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ ets = ETS(qdisc);
+
+ if (isempty(rvalue)) {
+ memzero(ets->quanta, sizeof(uint32_t) * TCQ_ETS_MAX_BANDS);
+ ets->n_quanta = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *word = NULL;
+ uint64_t v;
+
+ r = extract_first_word(&p, &word, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract next value, ignoring: %m");
+ break;
+ }
+ if (r == 0)
+ break;
+
+ r = parse_size(word, 1024, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, word);
+ continue;
+ }
+ if (v == 0 || v > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, word);
+ continue;
+ }
+ if (ets->n_quanta >= TCQ_ETS_MAX_BANDS) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Too many quanta in '%s=', ignoring assignment: %s",
+ lvalue, word);
+ continue;
+ }
+
+ ets->quanta[ets->n_quanta++] = v;
+ }
+
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_ets_prio(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ EnhancedTransmissionSelection *ets;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_ETS, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ ets = ETS(qdisc);
+
+ if (isempty(rvalue)) {
+ memzero(ets->prio, sizeof(uint8_t) * (TC_PRIO_MAX + 1));
+ ets->n_prio = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *word = NULL;
+ uint8_t v;
+
+ r = extract_first_word(&p, &word, NULL, 0);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to extract next value, ignoring: %m");
+ break;
+ }
+ if (r == 0)
+ break;
+
+ r = safe_atou8(word, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, word);
+ continue;
+ }
+ if (ets->n_prio > TC_PRIO_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Too many priomap in '%s=', ignoring assignment: %s",
+ lvalue, word);
+ continue;
+ }
+
+ ets->prio[ets->n_prio++] = v;
+ }
+
+ qdisc = NULL;
+
+ return 0;
+}
+
+static int enhanced_transmission_selection_verify(QDisc *qdisc) {
+ EnhancedTransmissionSelection *ets;
+
+ assert(qdisc);
+
+ ets = ETS(qdisc);
+
+ if (ets->n_bands == 0)
+ ets->n_bands = ets->n_strict + ets->n_quanta;
+
+ if (ets->n_bands == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: At least one of Band=, Strict=, or Quanta= must be specified. "
+ "Ignoring [EnhancedTransmissionSelection] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ if (ets->n_bands < ets->n_strict + ets->n_quanta)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Not enough total bands to cover all the strict bands and quanta. "
+ "Ignoring [EnhancedTransmissionSelection] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ for (unsigned i = 0; i < ets->n_prio; i++)
+ if (ets->prio[i] >= ets->n_bands)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: PriorityMap= element is out of bands. "
+ "Ignoring [EnhancedTransmissionSelection] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ return 0;
+}
+
+const QDiscVTable ets_vtable = {
+ .object_size = sizeof(EnhancedTransmissionSelection),
+ .tca_kind = "ets",
+ .fill_message = enhanced_transmission_selection_fill_message,
+ .verify = enhanced_transmission_selection_verify,
+};
diff --git a/src/network/tc/ets.h b/src/network/tc/ets.h
new file mode 100644
index 0000000..b6dd428
--- /dev/null
+++ b/src/network/tc/ets.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <linux/pkt_sched.h>
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct EnhancedTransmissionSelection {
+ QDisc meta;
+
+ uint8_t n_bands;
+ uint8_t n_strict;
+ unsigned n_quanta;
+ uint32_t quanta[TCQ_ETS_MAX_BANDS];
+ unsigned n_prio;
+ uint8_t prio[TC_PRIO_MAX + 1];
+} EnhancedTransmissionSelection;
+
+DEFINE_QDISC_CAST(ETS, EnhancedTransmissionSelection);
+extern const QDiscVTable ets_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ets_u8);
+CONFIG_PARSER_PROTOTYPE(config_parse_ets_quanta);
+CONFIG_PARSER_PROTOTYPE(config_parse_ets_prio);
diff --git a/src/network/tc/fifo.c b/src/network/tc/fifo.c
new file mode 100644
index 0000000..940fa00
--- /dev/null
+++ b/src/network/tc/fifo.c
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "fifo.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+
+static int fifo_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ FirstInFirstOut *fifo;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ switch (qdisc->kind) {
+ case QDISC_KIND_PFIFO:
+ assert_se(fifo = PFIFO(qdisc));
+ break;
+ case QDISC_KIND_BFIFO:
+ assert_se(fifo = BFIFO(qdisc));
+ break;
+ case QDISC_KIND_PFIFO_HEAD_DROP:
+ assert_se(fifo = PFIFO_HEAD_DROP(qdisc));
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ const struct tc_fifo_qopt opt = { .limit = fifo->limit };
+ r = sd_netlink_message_append_data(req, TCA_OPTIONS, &opt, sizeof(opt));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_pfifo_size(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ FirstInFirstOut *fifo;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(ltype, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ switch (qdisc->kind) {
+ case QDISC_KIND_PFIFO:
+ fifo = PFIFO(qdisc);
+ break;
+ case QDISC_KIND_PFIFO_HEAD_DROP:
+ fifo = PFIFO_HEAD_DROP(qdisc);
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ if (isempty(rvalue)) {
+ fifo->limit = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &fifo->limit);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+int config_parse_bfifo_size(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ FirstInFirstOut *fifo;
+ uint64_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_BFIFO, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fifo = BFIFO(qdisc);
+
+ if (isempty(rvalue)) {
+ fifo->limit = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1024, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (u > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ fifo->limit = (uint32_t) u;
+
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+const QDiscVTable pfifo_vtable = {
+ .object_size = sizeof(FirstInFirstOut),
+ .tca_kind = "pfifo",
+ .fill_message = fifo_fill_message,
+};
+
+const QDiscVTable bfifo_vtable = {
+ .object_size = sizeof(FirstInFirstOut),
+ .tca_kind = "bfifo",
+ .fill_message = fifo_fill_message,
+};
+
+const QDiscVTable pfifo_head_drop_vtable = {
+ .object_size = sizeof(FirstInFirstOut),
+ .tca_kind = "pfifo_head_drop",
+ .fill_message = fifo_fill_message,
+};
+
+const QDiscVTable pfifo_fast_vtable = {
+ .object_size = sizeof(FirstInFirstOut),
+ .tca_kind = "pfifo_fast",
+};
diff --git a/src/network/tc/fifo.h b/src/network/tc/fifo.h
new file mode 100644
index 0000000..b9bbd09
--- /dev/null
+++ b/src/network/tc/fifo.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct FirstInFirstOut {
+ QDisc meta;
+
+ uint32_t limit;
+} FirstInFirstOut;
+
+DEFINE_QDISC_CAST(PFIFO, FirstInFirstOut);
+DEFINE_QDISC_CAST(BFIFO, FirstInFirstOut);
+DEFINE_QDISC_CAST(PFIFO_HEAD_DROP, FirstInFirstOut);
+DEFINE_QDISC_CAST(PFIFO_FAST, FirstInFirstOut);
+
+extern const QDiscVTable pfifo_vtable;
+extern const QDiscVTable bfifo_vtable;
+extern const QDiscVTable pfifo_head_drop_vtable;
+extern const QDiscVTable pfifo_fast_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_pfifo_size);
+CONFIG_PARSER_PROTOTYPE(config_parse_bfifo_size);
diff --git a/src/network/tc/fq-codel.c b/src/network/tc/fq-codel.c
new file mode 100644
index 0000000..50ec203
--- /dev/null
+++ b/src/network/tc/fq-codel.c
@@ -0,0 +1,351 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "string-util.h"
+#include "strv.h"
+
+static int fair_queueing_controlled_delay_init(QDisc *qdisc) {
+ FairQueueingControlledDelay *fqcd;
+
+ assert(qdisc);
+
+ fqcd = FQ_CODEL(qdisc);
+
+ fqcd->memory_limit = UINT32_MAX;
+ fqcd->ce_threshold_usec = USEC_INFINITY;
+ fqcd->ecn = -1;
+
+ return 0;
+}
+
+static int fair_queueing_controlled_delay_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ FairQueueingControlledDelay *fqcd;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(fqcd = FQ_CODEL(qdisc));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq_codel");
+ if (r < 0)
+ return r;
+
+ if (fqcd->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_LIMIT, fqcd->packet_limit);
+ if (r < 0)
+ return r;
+ }
+
+ if (fqcd->flows > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_FLOWS, fqcd->flows);
+ if (r < 0)
+ return r;
+ }
+
+ if (fqcd->quantum > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_QUANTUM, fqcd->quantum);
+ if (r < 0)
+ return r;
+ }
+
+ if (fqcd->interval_usec > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_INTERVAL, fqcd->interval_usec);
+ if (r < 0)
+ return r;
+ }
+
+ if (fqcd->target_usec > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_TARGET, fqcd->target_usec);
+ if (r < 0)
+ return r;
+ }
+
+ if (fqcd->ecn >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_ECN, fqcd->ecn);
+ if (r < 0)
+ return r;
+ }
+
+ if (fqcd->ce_threshold_usec != USEC_INFINITY) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_CE_THRESHOLD, fqcd->ce_threshold_usec);
+ if (r < 0)
+ return r;
+ }
+
+ if (fqcd->memory_limit != UINT32_MAX) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_MEMORY_LIMIT, fqcd->memory_limit);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_fair_queueing_controlled_delay_u32(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueingControlledDelay *fqcd;
+ Network *network = ASSERT_PTR(data);
+ uint32_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ_CODEL, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fqcd = FQ_CODEL(qdisc);
+
+ if (streq(lvalue, "PacketLimit"))
+ p = &fqcd->packet_limit;
+ else if (streq(lvalue, "Flows"))
+ p = &fqcd->flows;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *p = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, p);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int config_parse_fair_queueing_controlled_delay_usec(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueingControlledDelay *fqcd;
+ Network *network = ASSERT_PTR(data);
+ usec_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ_CODEL, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fqcd = FQ_CODEL(qdisc);
+
+ if (streq(lvalue, "TargetSec"))
+ p = &fqcd->target_usec;
+ else if (streq(lvalue, "IntervalSec"))
+ p = &fqcd->interval_usec;
+ else if (streq(lvalue, "CEThresholdSec"))
+ p = &fqcd->ce_threshold_usec;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ if (streq(lvalue, "CEThresholdSec"))
+ *p = USEC_INFINITY;
+ else
+ *p = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_sec(rvalue, p);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int config_parse_fair_queueing_controlled_delay_bool(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueingControlledDelay *fqcd;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ_CODEL, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fqcd = FQ_CODEL(qdisc);
+
+ if (isempty(rvalue)) {
+ fqcd->ecn = -1;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ fqcd->ecn = r;
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int config_parse_fair_queueing_controlled_delay_size(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueingControlledDelay *fqcd;
+ Network *network = ASSERT_PTR(data);
+ uint64_t sz;
+ uint32_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ_CODEL, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fqcd = FQ_CODEL(qdisc);
+
+ if (STR_IN_SET(lvalue, "MemoryLimitBytes", "MemoryLimit"))
+ p = &fqcd->memory_limit;
+ else if (STR_IN_SET(lvalue, "QuantumBytes", "Quantum"))
+ p = &fqcd->quantum;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ if (STR_IN_SET(lvalue, "MemoryLimitBytes", "MemoryLimit"))
+ *p = UINT32_MAX;
+ else
+ *p = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1024, &sz);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (sz >= UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified '%s=' is too large, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ *p = sz;
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+const QDiscVTable fq_codel_vtable = {
+ .object_size = sizeof(FairQueueingControlledDelay),
+ .tca_kind = "fq_codel",
+ .init = fair_queueing_controlled_delay_init,
+ .fill_message = fair_queueing_controlled_delay_fill_message,
+};
diff --git a/src/network/tc/fq-codel.h b/src/network/tc/fq-codel.h
new file mode 100644
index 0000000..2553c59
--- /dev/null
+++ b/src/network/tc/fq-codel.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+#include "time-util.h"
+
+typedef struct FairQueueingControlledDelay {
+ QDisc meta;
+
+ uint32_t packet_limit;
+ uint32_t flows;
+ uint32_t quantum;
+ uint32_t memory_limit;
+ usec_t target_usec;
+ usec_t interval_usec;
+ usec_t ce_threshold_usec;
+ int ecn;
+} FairQueueingControlledDelay;
+
+DEFINE_QDISC_CAST(FQ_CODEL, FairQueueingControlledDelay);
+extern const QDiscVTable fq_codel_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_controlled_delay_u32);
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_controlled_delay_usec);
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_controlled_delay_bool);
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_controlled_delay_size);
diff --git a/src/network/tc/fq-pie.c b/src/network/tc/fq-pie.c
new file mode 100644
index 0000000..c8b2e7b
--- /dev/null
+++ b/src/network/tc/fq-pie.c
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "fq-pie.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+
+static int fq_pie_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ FlowQueuePIE *fq_pie;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(fq_pie = FQ_PIE(qdisc));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq_pie");
+ if (r < 0)
+ return r;
+
+ if (fq_pie->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_PIE_LIMIT, fq_pie->packet_limit);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_fq_pie_packet_limit(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FlowQueuePIE *fq_pie;
+ Network *network = ASSERT_PTR(data);
+ uint32_t val;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ_PIE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+
+ fq_pie = FQ_PIE(qdisc);
+
+ if (isempty(rvalue)) {
+ fq_pie->packet_limit = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &val);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (val == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ fq_pie->packet_limit = val;
+ qdisc = NULL;
+
+ return 0;
+}
+
+const QDiscVTable fq_pie_vtable = {
+ .object_size = sizeof(FlowQueuePIE),
+ .tca_kind = "fq_pie",
+ .fill_message = fq_pie_fill_message,
+};
diff --git a/src/network/tc/fq-pie.h b/src/network/tc/fq-pie.h
new file mode 100644
index 0000000..51fb626
--- /dev/null
+++ b/src/network/tc/fq-pie.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct FlowQueuePIE {
+ QDisc meta;
+
+ uint32_t packet_limit;
+} FlowQueuePIE;
+
+DEFINE_QDISC_CAST(FQ_PIE, FlowQueuePIE);
+extern const QDiscVTable fq_pie_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_fq_pie_packet_limit);
diff --git a/src/network/tc/fq.c b/src/network/tc/fq.c
new file mode 100644
index 0000000..c588f38
--- /dev/null
+++ b/src/network/tc/fq.c
@@ -0,0 +1,415 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "fq.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+
+static int fair_queueing_init(QDisc *qdisc) {
+ FairQueueing *fq;
+
+ assert(qdisc);
+
+ fq = FQ(qdisc);
+
+ fq->pacing = -1;
+ fq->ce_threshold_usec = USEC_INFINITY;
+
+ return 0;
+}
+
+static int fair_queueing_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ FairQueueing *fq;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(fq = FQ(qdisc));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq");
+ if (r < 0)
+ return r;
+
+ if (fq->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_PLIMIT, fq->packet_limit);
+ if (r < 0)
+ return r;
+ }
+
+ if (fq->flow_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_FLOW_PLIMIT, fq->flow_limit);
+ if (r < 0)
+ return r;
+ }
+
+ if (fq->quantum > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_QUANTUM, fq->quantum);
+ if (r < 0)
+ return r;
+ }
+
+ if (fq->initial_quantum > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_INITIAL_QUANTUM, fq->initial_quantum);
+ if (r < 0)
+ return r;
+ }
+
+ if (fq->pacing >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_RATE_ENABLE, fq->pacing);
+ if (r < 0)
+ return r;
+ }
+
+ if (fq->max_rate > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_FLOW_MAX_RATE, fq->max_rate);
+ if (r < 0)
+ return r;
+ }
+
+ if (fq->buckets > 0) {
+ uint32_t l;
+
+ l = log2u(fq->buckets);
+ r = sd_netlink_message_append_u32(req, TCA_FQ_BUCKETS_LOG, l);
+ if (r < 0)
+ return r;
+ }
+
+ if (fq->orphan_mask > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_ORPHAN_MASK, fq->orphan_mask);
+ if (r < 0)
+ return r;
+ }
+
+ if (fq->ce_threshold_usec != USEC_INFINITY) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CE_THRESHOLD, fq->ce_threshold_usec);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_fair_queueing_u32(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueing *fq;
+ Network *network = ASSERT_PTR(data);
+ uint32_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fq = FQ(qdisc);
+
+ if (streq(lvalue, "PacketLimit"))
+ p = &fq->packet_limit;
+ else if (streq(lvalue, "FlowLimit"))
+ p = &fq->flow_limit;
+ else if (streq(lvalue, "Buckets"))
+ p = &fq->buckets;
+ else if (streq(lvalue, "OrphanMask"))
+ p = &fq->orphan_mask;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *p = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, p);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_fair_queueing_size(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueing *fq;
+ Network *network = ASSERT_PTR(data);
+ uint64_t sz;
+ uint32_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fq = FQ(qdisc);
+
+ if (STR_IN_SET(lvalue, "QuantumBytes", "Quantum"))
+ p = &fq->quantum;
+ else if (STR_IN_SET(lvalue, "InitialQuantumBytes", "InitialQuantum"))
+ p = &fq->initial_quantum;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *p = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1024, &sz);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (sz > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified '%s=' is too large, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ *p = sz;
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_fair_queueing_bool(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueing *fq;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fq = FQ(qdisc);
+
+ if (isempty(rvalue)) {
+ fq->pacing = -1;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ fq->pacing = r;
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_fair_queueing_usec(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueing *fq;
+ Network *network = ASSERT_PTR(data);
+ usec_t sec;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fq = FQ(qdisc);
+
+ if (isempty(rvalue)) {
+ fq->ce_threshold_usec = USEC_INFINITY;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = parse_sec(rvalue, &sec);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (sec > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified '%s=' is too large, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ fq->ce_threshold_usec = sec;
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_fair_queueing_max_rate(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ FairQueueing *fq;
+ Network *network = ASSERT_PTR(data);
+ uint64_t sz;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ fq = FQ(qdisc);
+
+ if (isempty(rvalue)) {
+ fq->max_rate = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1000, &sz);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (sz / 8 > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Specified '%s=' is too large, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ fq->max_rate = sz / 8;
+ qdisc = NULL;
+
+ return 0;
+}
+
+const QDiscVTable fq_vtable = {
+ .init = fair_queueing_init,
+ .object_size = sizeof(FairQueueing),
+ .tca_kind = "fq",
+ .fill_message = fair_queueing_fill_message,
+};
diff --git a/src/network/tc/fq.h b/src/network/tc/fq.h
new file mode 100644
index 0000000..77469c4
--- /dev/null
+++ b/src/network/tc/fq.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct FairQueueing {
+ QDisc meta;
+
+ uint32_t packet_limit;
+ uint32_t flow_limit;
+ uint32_t quantum;
+ uint32_t initial_quantum;
+ uint32_t max_rate;
+ uint32_t buckets;
+ uint32_t orphan_mask;
+ int pacing;
+ usec_t ce_threshold_usec;
+} FairQueueing;
+
+DEFINE_QDISC_CAST(FQ, FairQueueing);
+extern const QDiscVTable fq_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_u32);
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_size);
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_bool);
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_usec);
+CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_max_rate);
diff --git a/src/network/tc/gred.c b/src/network/tc/gred.c
new file mode 100644
index 0000000..476122c
--- /dev/null
+++ b/src/network/tc/gred.c
@@ -0,0 +1,193 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "string-util.h"
+
+static int generic_random_early_detection_init(QDisc *qdisc) {
+ GenericRandomEarlyDetection *gred;
+
+ assert(qdisc);
+
+ gred = GRED(qdisc);
+
+ gred->grio = -1;
+
+ return 0;
+}
+
+static int generic_random_early_detection_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ GenericRandomEarlyDetection *gred;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(gred = GRED(qdisc));
+
+ const struct tc_gred_sopt opt = {
+ .DPs = gred->virtual_queues,
+ .def_DP = gred->default_virtual_queue,
+ .grio = gred->grio,
+ };
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "gred");
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, TCA_GRED_DPS, &opt, sizeof(opt));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int generic_random_early_detection_verify(QDisc *qdisc) {
+ GenericRandomEarlyDetection *gred = GRED(qdisc);
+
+ if (gred->default_virtual_queue >= gred->virtual_queues)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: DefaultVirtualQueue= must be less than VirtualQueues=. "
+ "Ignoring [GenericRandomEarlyDetection] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ return 0;
+}
+
+int config_parse_generic_random_early_detection_u32(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ GenericRandomEarlyDetection *gred;
+ Network *network = ASSERT_PTR(data);
+ uint32_t *p;
+ uint32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_GRED, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ gred = GRED(qdisc);
+
+ if (streq(lvalue, "VirtualQueues"))
+ p = &gred->virtual_queues;
+ else if (streq(lvalue, "DefaultVirtualQueue"))
+ p = &gred->default_virtual_queue;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *p = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (v > MAX_DPs)
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+
+ *p = v;
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+int config_parse_generic_random_early_detection_bool(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ GenericRandomEarlyDetection *gred;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_GRED, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ gred = GRED(qdisc);
+
+ if (isempty(rvalue)) {
+ gred->grio = -1;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ gred->grio = r;
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+const QDiscVTable gred_vtable = {
+ .object_size = sizeof(GenericRandomEarlyDetection),
+ .tca_kind = "gred",
+ .init = generic_random_early_detection_init,
+ .fill_message = generic_random_early_detection_fill_message,
+ .verify = generic_random_early_detection_verify,
+};
diff --git a/src/network/tc/gred.h b/src/network/tc/gred.h
new file mode 100644
index 0000000..c084ff1
--- /dev/null
+++ b/src/network/tc/gred.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct GenericRandomEarlyDetection {
+ QDisc meta;
+
+ uint32_t virtual_queues;
+ uint32_t default_virtual_queue;
+ int grio;
+} GenericRandomEarlyDetection;
+
+DEFINE_QDISC_CAST(GRED, GenericRandomEarlyDetection);
+extern const QDiscVTable gred_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_generic_random_early_detection_u32);
+CONFIG_PARSER_PROTOTYPE(config_parse_generic_random_early_detection_bool);
diff --git a/src/network/tc/hhf.c b/src/network/tc/hhf.c
new file mode 100644
index 0000000..2d71a42
--- /dev/null
+++ b/src/network/tc/hhf.c
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "hhf.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "util.h"
+
+static int heavy_hitter_filter_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ HeavyHitterFilter *hhf;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(hhf = HHF(qdisc));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "hhf");
+ if (r < 0)
+ return r;
+
+ if (hhf->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_HHF_BACKLOG_LIMIT, hhf->packet_limit);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_heavy_hitter_filter_packet_limit(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ HeavyHitterFilter *hhf;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_HHF, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ hhf = HHF(qdisc);
+
+ if (isempty(rvalue)) {
+ hhf->packet_limit = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &hhf->packet_limit);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+const QDiscVTable hhf_vtable = {
+ .object_size = sizeof(HeavyHitterFilter),
+ .tca_kind = "hhf",
+ .fill_message = heavy_hitter_filter_fill_message,
+};
diff --git a/src/network/tc/hhf.h b/src/network/tc/hhf.h
new file mode 100644
index 0000000..04caaa8
--- /dev/null
+++ b/src/network/tc/hhf.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct HeavyHitterFilter {
+ QDisc meta;
+
+ uint32_t packet_limit;
+} HeavyHitterFilter;
+
+DEFINE_QDISC_CAST(HHF, HeavyHitterFilter);
+extern const QDiscVTable hhf_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_heavy_hitter_filter_packet_limit);
diff --git a/src/network/tc/htb.c b/src/network/tc/htb.c
new file mode 100644
index 0000000..eb2c8cf
--- /dev/null
+++ b/src/network/tc/htb.c
@@ -0,0 +1,487 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "htb.h"
+#include "string-util.h"
+#include "tc-util.h"
+
+#define HTB_DEFAULT_RATE_TO_QUANTUM 10
+#define HTB_DEFAULT_MTU 1600 /* Ethernet packet length */
+
+static int hierarchy_token_bucket_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ HierarchyTokenBucket *htb;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(htb = HTB(qdisc));
+
+ struct tc_htb_glob opt = {
+ .version = 3,
+ .rate2quantum = htb->rate_to_quantum,
+ .defcls = htb->default_class,
+ };
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "htb");
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, TCA_HTB_INIT, &opt, sizeof(opt));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+ return 0;
+}
+
+int config_parse_hierarchy_token_bucket_default_class(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ HierarchyTokenBucket *htb;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_HTB, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ htb = HTB(qdisc);
+
+ if (isempty(rvalue)) {
+ htb->default_class = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32_full(rvalue, 16, &htb->default_class);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int config_parse_hierarchy_token_bucket_u32(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ HierarchyTokenBucket *htb;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_HTB, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ htb = HTB(qdisc);
+
+ if (isempty(rvalue)) {
+ htb->rate_to_quantum = HTB_DEFAULT_RATE_TO_QUANTUM;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &htb->rate_to_quantum);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+static int hierarchy_token_bucket_init(QDisc *qdisc) {
+ HierarchyTokenBucket *htb;
+
+ assert(qdisc);
+
+ htb = HTB(qdisc);
+
+ htb->rate_to_quantum = HTB_DEFAULT_RATE_TO_QUANTUM;
+
+ return 0;
+}
+
+const QDiscVTable htb_vtable = {
+ .object_size = sizeof(HierarchyTokenBucket),
+ .tca_kind = "htb",
+ .fill_message = hierarchy_token_bucket_fill_message,
+ .init = hierarchy_token_bucket_init,
+};
+
+static int hierarchy_token_bucket_class_fill_message(Link *link, TClass *tclass, sd_netlink_message *req) {
+ HierarchyTokenBucketClass *htb;
+ uint32_t rtab[256], ctab[256];
+ int r;
+
+ assert(link);
+ assert(tclass);
+ assert(req);
+
+ assert_se(htb = TCLASS_TO_HTB(tclass));
+
+ struct tc_htb_opt opt = {
+ .prio = htb->priority,
+ .quantum = htb->quantum,
+ .rate.rate = (htb->rate >= (1ULL << 32)) ? ~0U : htb->rate,
+ .ceil.rate = (htb->ceil_rate >= (1ULL << 32)) ? ~0U : htb->ceil_rate,
+ .rate.overhead = htb->overhead,
+ .ceil.overhead = htb->overhead,
+ };
+
+ r = tc_transmit_time(htb->rate, htb->buffer, &opt.buffer);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to calculate buffer size: %m");
+
+ r = tc_transmit_time(htb->ceil_rate, htb->ceil_buffer, &opt.cbuffer);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to calculate ceil buffer size: %m");
+
+ r = tc_fill_ratespec_and_table(&opt.rate, rtab, htb->mtu);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to calculate rate table: %m");
+
+ r = tc_fill_ratespec_and_table(&opt.ceil, ctab, htb->mtu);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to calculate ceil rate table: %m");
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "htb");
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, TCA_HTB_PARMS, &opt, sizeof(opt));
+ if (r < 0)
+ return r;
+
+ if (htb->rate >= (1ULL << 32)) {
+ r = sd_netlink_message_append_u64(req, TCA_HTB_RATE64, htb->rate);
+ if (r < 0)
+ return r;
+ }
+
+ if (htb->ceil_rate >= (1ULL << 32)) {
+ r = sd_netlink_message_append_u64(req, TCA_HTB_CEIL64, htb->ceil_rate);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_data(req, TCA_HTB_RTAB, rtab, sizeof(rtab));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, TCA_HTB_CTAB, ctab, sizeof(ctab));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_hierarchy_token_bucket_class_u32(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL;
+ HierarchyTokenBucketClass *htb;
+ Network *network = ASSERT_PTR(data);
+ uint32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = tclass_new_static(TCLASS_KIND_HTB, network, filename, section_line, &tclass);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to create traffic control class, ignoring assignment: %m");
+ return 0;
+ }
+
+ htb = TCLASS_TO_HTB(tclass);
+
+ if (isempty(rvalue)) {
+ htb->priority = 0;
+ tclass = NULL;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ htb->priority = v;
+ tclass = NULL;
+
+ return 0;
+}
+
+int config_parse_hierarchy_token_bucket_class_size(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL;
+ HierarchyTokenBucketClass *htb;
+ Network *network = ASSERT_PTR(data);
+ uint64_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = tclass_new_static(TCLASS_KIND_HTB, network, filename, section_line, &tclass);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to create traffic control class, ignoring assignment: %m");
+ return 0;
+ }
+
+ htb = TCLASS_TO_HTB(tclass);
+
+ if (isempty(rvalue)) {
+ if (streq(lvalue, "QuantumBytes"))
+ htb->quantum = 0;
+ else if (streq(lvalue, "MTUBytes"))
+ htb->mtu = HTB_DEFAULT_MTU;
+ else if (streq(lvalue, "OverheadBytes"))
+ htb->overhead = 0;
+ else if (streq(lvalue, "BufferBytes"))
+ htb->buffer = 0;
+ else if (streq(lvalue, "CeilBufferBytes"))
+ htb->ceil_buffer = 0;
+ else
+ assert_not_reached();
+
+ tclass = NULL;
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1024, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if ((streq(lvalue, "OverheadBytes") && v > UINT16_MAX) || v > UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "QuantumBytes"))
+ htb->quantum = v;
+ else if (streq(lvalue, "OverheadBytes"))
+ htb->overhead = v;
+ else if (streq(lvalue, "MTUBytes"))
+ htb->mtu = v;
+ else if (streq(lvalue, "BufferBytes"))
+ htb->buffer = v;
+ else if (streq(lvalue, "CeilBufferBytes"))
+ htb->ceil_buffer = v;
+ else
+ assert_not_reached();
+
+ tclass = NULL;
+
+ return 0;
+}
+
+int config_parse_hierarchy_token_bucket_class_rate(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL;
+ HierarchyTokenBucketClass *htb;
+ Network *network = ASSERT_PTR(data);
+ uint64_t *v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = tclass_new_static(TCLASS_KIND_HTB, network, filename, section_line, &tclass);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to create traffic control class, ignoring assignment: %m");
+ return 0;
+ }
+
+ htb = TCLASS_TO_HTB(tclass);
+ if (streq(lvalue, "Rate"))
+ v = &htb->rate;
+ else if (streq(lvalue, "CeilRate"))
+ v = &htb->ceil_rate;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *v = 0;
+
+ tclass = NULL;
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1000, v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ *v /= 8;
+ tclass = NULL;
+
+ return 0;
+}
+
+static int hierarchy_token_bucket_class_init(TClass *tclass) {
+ HierarchyTokenBucketClass *htb;
+
+ assert(tclass);
+
+ htb = TCLASS_TO_HTB(tclass);
+
+ htb->mtu = HTB_DEFAULT_MTU;
+
+ return 0;
+}
+
+static int hierarchy_token_bucket_class_verify(TClass *tclass) {
+ HierarchyTokenBucketClass *htb;
+ uint32_t hz;
+ int r;
+
+ assert(tclass);
+
+ htb = TCLASS_TO_HTB(tclass);
+
+ if (htb->rate == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Rate= is mandatory. "
+ "Ignoring [HierarchyTokenBucketClass] section from line %u.",
+ tclass->section->filename, tclass->section->line);
+
+ /* if CeilRate= setting is missing, use the same as Rate= */
+ if (htb->ceil_rate == 0)
+ htb->ceil_rate = htb->rate;
+
+ r = tc_init(NULL, &hz);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read /proc/net/psched: %m");
+
+ if (htb->buffer == 0)
+ htb->buffer = htb->rate / hz + htb->mtu;
+ if (htb->ceil_buffer == 0)
+ htb->ceil_buffer = htb->ceil_rate / hz + htb->mtu;
+
+ return 0;
+}
+
+const TClassVTable htb_tclass_vtable = {
+ .object_size = sizeof(HierarchyTokenBucketClass),
+ .tca_kind = "htb",
+ .fill_message = hierarchy_token_bucket_class_fill_message,
+ .init = hierarchy_token_bucket_class_init,
+ .verify = hierarchy_token_bucket_class_verify,
+};
diff --git a/src/network/tc/htb.h b/src/network/tc/htb.h
new file mode 100644
index 0000000..55644db
--- /dev/null
+++ b/src/network/tc/htb.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+#include "tclass.h"
+
+typedef struct HierarchyTokenBucket {
+ QDisc meta;
+
+ uint32_t default_class;
+ uint32_t rate_to_quantum;
+} HierarchyTokenBucket;
+
+DEFINE_QDISC_CAST(HTB, HierarchyTokenBucket);
+extern const QDiscVTable htb_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_default_class);
+CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_u32);
+
+typedef struct HierarchyTokenBucketClass {
+ TClass meta;
+
+ uint32_t priority;
+ uint32_t quantum;
+ uint32_t mtu;
+ uint16_t overhead;
+ uint64_t rate;
+ uint32_t buffer;
+ uint64_t ceil_rate;
+ uint32_t ceil_buffer;
+} HierarchyTokenBucketClass;
+
+DEFINE_TCLASS_CAST(HTB, HierarchyTokenBucketClass);
+extern const TClassVTable htb_tclass_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_class_u32);
+CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_class_size);
+CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_class_rate);
diff --git a/src/network/tc/netem.c b/src/network/tc/netem.c
new file mode 100644
index 0000000..6a63221
--- /dev/null
+++ b/src/network/tc/netem.c
@@ -0,0 +1,227 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netem.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "strv.h"
+#include "tc-util.h"
+
+static int network_emulator_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ NetworkEmulator *ne;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(ne = NETEM(qdisc));
+
+ struct tc_netem_qopt opt = {
+ .limit = ne->limit > 0 ? ne->limit : 1000,
+ .loss = ne->loss,
+ .duplicate = ne->duplicate,
+ };
+
+ if (ne->delay != USEC_INFINITY) {
+ r = tc_time_to_tick(ne->delay, &opt.latency);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to calculate latency in TCA_OPTION: %m");
+ }
+
+ if (ne->jitter != USEC_INFINITY) {
+ r = tc_time_to_tick(ne->jitter, &opt.jitter);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to calculate jitter in TCA_OPTION: %m");
+ }
+
+ r = sd_netlink_message_append_data(req, TCA_OPTIONS, &opt, sizeof(opt));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_network_emulator_delay(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ NetworkEmulator *ne;
+ usec_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_NETEM, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ ne = NETEM(qdisc);
+
+ if (isempty(rvalue)) {
+ if (STR_IN_SET(lvalue, "DelaySec", "NetworkEmulatorDelaySec"))
+ ne->delay = USEC_INFINITY;
+ else if (STR_IN_SET(lvalue, "DelayJitterSec", "NetworkEmulatorDelayJitterSec"))
+ ne->jitter = USEC_INFINITY;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_sec(rvalue, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (STR_IN_SET(lvalue, "DelaySec", "NetworkEmulatorDelaySec"))
+ ne->delay = u;
+ else if (STR_IN_SET(lvalue, "DelayJitterSec", "NetworkEmulatorDelayJitterSec"))
+ ne->jitter = u;
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int config_parse_network_emulator_rate(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ NetworkEmulator *ne;
+ uint32_t rate;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_NETEM, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ ne = NETEM(qdisc);
+
+ if (isempty(rvalue)) {
+ if (STR_IN_SET(lvalue, "LossRate", "NetworkEmulatorLossRate"))
+ ne->loss = 0;
+ else if (STR_IN_SET(lvalue, "DuplicateRate", "NetworkEmulatorDuplicateRate"))
+ ne->duplicate = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_tc_percent(rvalue, &rate);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (STR_IN_SET(lvalue, "LossRate", "NetworkEmulatorLossRate"))
+ ne->loss = rate;
+ else if (STR_IN_SET(lvalue, "DuplicateRate", "NetworkEmulatorDuplicateRate"))
+ ne->duplicate = rate;
+
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+int config_parse_network_emulator_packet_limit(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ NetworkEmulator *ne;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_NETEM, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ ne = NETEM(qdisc);
+
+ if (isempty(rvalue)) {
+ ne->limit = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou(rvalue, &ne->limit);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+ return 0;
+}
+
+const QDiscVTable netem_vtable = {
+ .object_size = sizeof(NetworkEmulator),
+ .tca_kind = "netem",
+ .fill_message = network_emulator_fill_message,
+};
diff --git a/src/network/tc/netem.h b/src/network/tc/netem.h
new file mode 100644
index 0000000..d58d5ac
--- /dev/null
+++ b/src/network/tc/netem.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+#include "time-util.h"
+
+typedef struct NetworkEmulator {
+ QDisc meta;
+
+ usec_t delay;
+ usec_t jitter;
+
+ uint32_t limit;
+ uint32_t loss;
+ uint32_t duplicate;
+} NetworkEmulator;
+
+DEFINE_QDISC_CAST(NETEM, NetworkEmulator);
+extern const QDiscVTable netem_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_network_emulator_delay);
+CONFIG_PARSER_PROTOTYPE(config_parse_network_emulator_rate);
+CONFIG_PARSER_PROTOTYPE(config_parse_network_emulator_packet_limit);
diff --git a/src/network/tc/pie.c b/src/network/tc/pie.c
new file mode 100644
index 0000000..c9b171b
--- /dev/null
+++ b/src/network/tc/pie.c
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "pie.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+
+static int pie_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ ProportionalIntegralControllerEnhanced *pie;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(pie = PIE(qdisc));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "pie");
+ if (r < 0)
+ return r;
+
+ if (pie->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_PIE_LIMIT, pie->packet_limit);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_pie_packet_limit(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ ProportionalIntegralControllerEnhanced *pie;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_PIE, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ pie = PIE(qdisc);
+
+ if (isempty(rvalue)) {
+ pie->packet_limit = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &pie->packet_limit);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+const QDiscVTable pie_vtable = {
+ .object_size = sizeof(ProportionalIntegralControllerEnhanced),
+ .tca_kind = "pie",
+ .fill_message = pie_fill_message,
+};
diff --git a/src/network/tc/pie.h b/src/network/tc/pie.h
new file mode 100644
index 0000000..40a114e
--- /dev/null
+++ b/src/network/tc/pie.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct ProportionalIntegralControllerEnhanced {
+ QDisc meta;
+
+ uint32_t packet_limit;
+} ProportionalIntegralControllerEnhanced;
+
+DEFINE_QDISC_CAST(PIE, ProportionalIntegralControllerEnhanced);
+extern const QDiscVTable pie_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_pie_packet_limit);
diff --git a/src/network/tc/qdisc.c b/src/network/tc/qdisc.c
new file mode 100644
index 0000000..ca89169
--- /dev/null
+++ b/src/network/tc/qdisc.c
@@ -0,0 +1,687 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "set.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tc-util.h"
+
+const QDiscVTable * const qdisc_vtable[_QDISC_KIND_MAX] = {
+ [QDISC_KIND_BFIFO] = &bfifo_vtable,
+ [QDISC_KIND_CAKE] = &cake_vtable,
+ [QDISC_KIND_CODEL] = &codel_vtable,
+ [QDISC_KIND_DRR] = &drr_vtable,
+ [QDISC_KIND_ETS] = &ets_vtable,
+ [QDISC_KIND_FQ] = &fq_vtable,
+ [QDISC_KIND_FQ_CODEL] = &fq_codel_vtable,
+ [QDISC_KIND_FQ_PIE] = &fq_pie_vtable,
+ [QDISC_KIND_GRED] = &gred_vtable,
+ [QDISC_KIND_HHF] = &hhf_vtable,
+ [QDISC_KIND_HTB] = &htb_vtable,
+ [QDISC_KIND_NETEM] = &netem_vtable,
+ [QDISC_KIND_PIE] = &pie_vtable,
+ [QDISC_KIND_QFQ] = &qfq_vtable,
+ [QDISC_KIND_PFIFO] = &pfifo_vtable,
+ [QDISC_KIND_PFIFO_FAST] = &pfifo_fast_vtable,
+ [QDISC_KIND_PFIFO_HEAD_DROP] = &pfifo_head_drop_vtable,
+ [QDISC_KIND_SFB] = &sfb_vtable,
+ [QDISC_KIND_SFQ] = &sfq_vtable,
+ [QDISC_KIND_TBF] = &tbf_vtable,
+ [QDISC_KIND_TEQL] = &teql_vtable,
+};
+
+static int qdisc_new(QDiscKind kind, QDisc **ret) {
+ _cleanup_(qdisc_freep) QDisc *qdisc = NULL;
+ int r;
+
+ if (kind == _QDISC_KIND_INVALID) {
+ qdisc = new(QDisc, 1);
+ if (!qdisc)
+ return -ENOMEM;
+
+ *qdisc = (QDisc) {
+ .parent = TC_H_ROOT,
+ .kind = kind,
+ };
+ } else {
+ assert(kind >= 0 && kind < _QDISC_KIND_MAX);
+ qdisc = malloc0(qdisc_vtable[kind]->object_size);
+ if (!qdisc)
+ return -ENOMEM;
+
+ qdisc->parent = TC_H_ROOT;
+ qdisc->kind = kind;
+
+ if (QDISC_VTABLE(qdisc)->init) {
+ r = QDISC_VTABLE(qdisc)->init(qdisc);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ *ret = TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int qdisc_new_static(QDiscKind kind, Network *network, const char *filename, unsigned section_line, QDisc **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(qdisc_freep) QDisc *qdisc = NULL;
+ QDisc *existing;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ existing = hashmap_get(network->qdiscs_by_section, n);
+ if (existing) {
+ if (existing->kind != _QDISC_KIND_INVALID &&
+ kind != _QDISC_KIND_INVALID &&
+ existing->kind != kind)
+ return -EINVAL;
+
+ if (existing->kind == kind || kind == _QDISC_KIND_INVALID) {
+ *ret = existing;
+ return 0;
+ }
+ }
+
+ r = qdisc_new(kind, &qdisc);
+ if (r < 0)
+ return r;
+
+ if (existing) {
+ qdisc->handle = existing->handle;
+ qdisc->parent = existing->parent;
+ qdisc->tca_kind = TAKE_PTR(existing->tca_kind);
+
+ qdisc_free(existing);
+ }
+
+ qdisc->network = network;
+ qdisc->section = TAKE_PTR(n);
+ qdisc->source = NETWORK_CONFIG_SOURCE_STATIC;
+
+ r = hashmap_ensure_put(&network->qdiscs_by_section, &config_section_hash_ops, qdisc->section, qdisc);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(qdisc);
+ return 0;
+}
+
+QDisc* qdisc_free(QDisc *qdisc) {
+ if (!qdisc)
+ return NULL;
+
+ if (qdisc->network && qdisc->section)
+ hashmap_remove(qdisc->network->qdiscs_by_section, qdisc->section);
+
+ config_section_free(qdisc->section);
+
+ if (qdisc->link)
+ set_remove(qdisc->link->qdiscs, qdisc);
+
+ free(qdisc->tca_kind);
+ return mfree(qdisc);
+}
+
+static const char *qdisc_get_tca_kind(const QDisc *qdisc) {
+ assert(qdisc);
+
+ return (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->tca_kind) ?
+ QDISC_VTABLE(qdisc)->tca_kind : qdisc->tca_kind;
+}
+
+static void qdisc_hash_func(const QDisc *qdisc, struct siphash *state) {
+ assert(qdisc);
+ assert(state);
+
+ siphash24_compress(&qdisc->handle, sizeof(qdisc->handle), state);
+ siphash24_compress(&qdisc->parent, sizeof(qdisc->parent), state);
+ siphash24_compress_string(qdisc_get_tca_kind(qdisc), state);
+}
+
+static int qdisc_compare_func(const QDisc *a, const QDisc *b) {
+ int r;
+
+ assert(a);
+ assert(b);
+
+ r = CMP(a->handle, b->handle);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->parent, b->parent);
+ if (r != 0)
+ return r;
+
+ return strcmp_ptr(qdisc_get_tca_kind(a), qdisc_get_tca_kind(b));
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ qdisc_hash_ops,
+ QDisc,
+ qdisc_hash_func,
+ qdisc_compare_func,
+ qdisc_free);
+
+static int qdisc_get(Link *link, const QDisc *in, QDisc **ret) {
+ QDisc *existing;
+
+ assert(link);
+ assert(in);
+
+ existing = set_get(link->qdiscs, in);
+ if (!existing)
+ return -ENOENT;
+
+ if (ret)
+ *ret = existing;
+ return 0;
+}
+
+static int qdisc_add(Link *link, QDisc *qdisc) {
+ int r;
+
+ assert(link);
+ assert(qdisc);
+
+ r = set_ensure_put(&link->qdiscs, &qdisc_hash_ops, qdisc);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ qdisc->link = link;
+ return 0;
+}
+
+static int qdisc_dup(const QDisc *src, QDisc **ret) {
+ _cleanup_(qdisc_freep) QDisc *dst = NULL;
+
+ assert(src);
+ assert(ret);
+
+ if (QDISC_VTABLE(src))
+ dst = memdup(src, QDISC_VTABLE(src)->object_size);
+ else
+ dst = newdup(QDisc, src, 1);
+ if (!dst)
+ return -ENOMEM;
+
+ /* clear all pointers */
+ dst->network = NULL;
+ dst->section = NULL;
+ dst->link = NULL;
+ dst->tca_kind = NULL;
+
+ if (src->tca_kind) {
+ dst->tca_kind = strdup(src->tca_kind);
+ if (!dst->tca_kind)
+ return -ENOMEM;
+ }
+
+ *ret = TAKE_PTR(dst);
+ return 0;
+}
+
+static void log_qdisc_debug(QDisc *qdisc, Link *link, const char *str) {
+ _cleanup_free_ char *state = NULL;
+
+ assert(qdisc);
+ assert(str);
+
+ if (!DEBUG_LOGGING)
+ return;
+
+ (void) network_config_state_to_string_alloc(qdisc->state, &state);
+
+ log_link_debug(link, "%s %s QDisc (%s): handle=%"PRIx32":%"PRIx32", parent=%"PRIx32":%"PRIx32", kind=%s",
+ str, strna(network_config_source_to_string(qdisc->source)), strna(state),
+ TC_H_MAJ(qdisc->handle) >> 16, TC_H_MIN(qdisc->handle),
+ TC_H_MAJ(qdisc->parent) >> 16, TC_H_MIN(qdisc->parent),
+ strna(qdisc_get_tca_kind(qdisc)));
+}
+
+int link_find_qdisc(Link *link, uint32_t handle, uint32_t parent, const char *kind, QDisc **ret) {
+ QDisc *qdisc;
+
+ assert(link);
+
+ handle = TC_H_MAJ(handle);
+
+ SET_FOREACH(qdisc, link->qdiscs) {
+ if (qdisc->handle != handle)
+ continue;
+
+ if (qdisc->parent != parent)
+ continue;
+
+ if (qdisc->source == NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ if (!qdisc_exists(qdisc))
+ continue;
+
+ if (kind && !streq_ptr(kind, qdisc_get_tca_kind(qdisc)))
+ continue;
+
+ if (ret)
+ *ret = qdisc;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int qdisc_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, QDisc *qdisc) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_error_errno(link, m, r, "Could not set QDisc");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->tc_messages == 0) {
+ log_link_debug(link, "Traffic control configured");
+ link->tc_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int qdisc_configure(QDisc *qdisc, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(qdisc);
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+ assert(req);
+
+ log_qdisc_debug(qdisc, link, "Configuring");
+
+ r = sd_rtnl_message_new_traffic_control(link->manager->rtnl, &m, RTM_NEWQDISC,
+ link->ifindex, qdisc->handle, qdisc->parent);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_string(m, TCA_KIND, qdisc_get_tca_kind(qdisc));
+ if (r < 0)
+ return r;
+
+ if (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->fill_message) {
+ r = QDISC_VTABLE(qdisc)->fill_message(link, qdisc, m);
+ if (r < 0)
+ return r;
+ }
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static bool qdisc_is_ready_to_configure(QDisc *qdisc, Link *link) {
+ assert(qdisc);
+ assert(link);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return false;
+
+ /* TC_H_CLSACT == TC_H_INGRESS */
+ if (!IN_SET(qdisc->parent, TC_H_ROOT, TC_H_CLSACT) &&
+ link_find_tclass(link, qdisc->parent, NULL) < 0)
+ return false;
+
+ if (QDISC_VTABLE(qdisc) &&
+ QDISC_VTABLE(qdisc)->is_ready &&
+ QDISC_VTABLE(qdisc)->is_ready(qdisc, link) <= 0)
+ return false;
+
+ return true;
+}
+
+static int qdisc_process_request(Request *req, Link *link, QDisc *qdisc) {
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(qdisc);
+
+ if (!qdisc_is_ready_to_configure(qdisc, link))
+ return 0;
+
+ r = qdisc_configure(qdisc, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure QDisc: %m");
+
+ qdisc_enter_configuring(qdisc);
+ return 1;
+}
+
+int link_request_qdisc(Link *link, QDisc *qdisc) {
+ QDisc *existing;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+
+ if (qdisc_get(link, qdisc, &existing) < 0) {
+ _cleanup_(qdisc_freep) QDisc *tmp = NULL;
+
+ r = qdisc_dup(qdisc, &tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = qdisc_add(link, tmp);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to store QDisc: %m");
+
+ existing = TAKE_PTR(tmp);
+ } else
+ existing->source = qdisc->source;
+
+ log_qdisc_debug(existing, link, "Requesting");
+ r = link_queue_request_safe(link, REQUEST_TYPE_TC_QDISC,
+ existing, NULL,
+ qdisc_hash_func,
+ qdisc_compare_func,
+ qdisc_process_request,
+ &link->tc_messages,
+ qdisc_handler,
+ NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request QDisc: %m");
+ if (r == 0)
+ return 0;
+
+ qdisc_enter_requesting(existing);
+ return 1;
+}
+
+int manager_rtnl_process_qdisc(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(qdisc_freep) QDisc *tmp = NULL;
+ QDisc *qdisc = NULL;
+ Link *link;
+ uint16_t type;
+ int ifindex, r;
+
+ assert(rtnl);
+ assert(message);
+ assert(m);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "rtnl: failed to receive QDisc message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get message type, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(type, RTM_NEWQDISC, RTM_DELQDISC)) {
+ log_warning("rtnl: received unexpected message type %u when processing QDisc, ignoring.", type);
+ return 0;
+ }
+
+ r = sd_rtnl_message_traffic_control_get_ifindex(message, &ifindex);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m");
+ return 0;
+ } else if (ifindex <= 0) {
+ log_warning("rtnl: received QDisc message with invalid ifindex %d, ignoring.", ifindex);
+ return 0;
+ }
+
+ if (link_get_by_index(m, ifindex, &link) < 0) {
+ if (!m->enumerating)
+ log_warning("rtnl: received QDisc for link '%d' we don't know about, ignoring.", ifindex);
+ return 0;
+ }
+
+ r = qdisc_new(_QDISC_KIND_INVALID, &tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_rtnl_message_traffic_control_get_handle(message, &tmp->handle);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received QDisc message without handle, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_traffic_control_get_parent(message, &tmp->parent);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received QDisc message without parent, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string_strdup(message, TCA_KIND, &tmp->tca_kind);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received QDisc message without kind, ignoring: %m");
+ return 0;
+ }
+
+ (void) qdisc_get(link, tmp, &qdisc);
+
+ switch (type) {
+ case RTM_NEWQDISC:
+ if (qdisc) {
+ qdisc_enter_configured(qdisc);
+ log_qdisc_debug(qdisc, link, "Received remembered");
+ } else {
+ qdisc_enter_configured(tmp);
+ log_qdisc_debug(tmp, link, "Received new");
+
+ r = qdisc_add(link, tmp);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to remember QDisc, ignoring: %m");
+ return 0;
+ }
+
+ qdisc = TAKE_PTR(tmp);
+ }
+
+ break;
+
+ case RTM_DELQDISC:
+ if (qdisc) {
+ qdisc_enter_removed(qdisc);
+ if (qdisc->state == 0) {
+ log_qdisc_debug(qdisc, link, "Forgetting");
+ qdisc_free(qdisc);
+ } else
+ log_qdisc_debug(qdisc, link, "Removed");
+ } else
+ log_qdisc_debug(tmp, link, "Kernel removed unknown");
+
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int qdisc_section_verify(QDisc *qdisc, bool *has_root, bool *has_clsact) {
+ int r;
+
+ assert(qdisc);
+ assert(has_root);
+ assert(has_clsact);
+
+ if (section_is_invalid(qdisc->section))
+ return -EINVAL;
+
+ if (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->verify) {
+ r = QDISC_VTABLE(qdisc)->verify(qdisc);
+ if (r < 0)
+ return r;
+ }
+
+ if (qdisc->parent == TC_H_ROOT) {
+ if (*has_root)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: More than one root qdisc section is defined. "
+ "Ignoring the qdisc section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+ *has_root = true;
+ } else if (qdisc->parent == TC_H_CLSACT) { /* TC_H_CLSACT == TC_H_INGRESS */
+ if (*has_clsact)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: More than one clsact or ingress qdisc section is defined. "
+ "Ignoring the qdisc section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+ *has_clsact = true;
+ }
+
+ return 0;
+}
+
+void network_drop_invalid_qdisc(Network *network) {
+ bool has_root = false, has_clsact = false;
+ QDisc *qdisc;
+
+ assert(network);
+
+ HASHMAP_FOREACH(qdisc, network->qdiscs_by_section)
+ if (qdisc_section_verify(qdisc, &has_root, &has_clsact) < 0)
+ qdisc_free(qdisc);
+}
+
+int config_parse_qdisc_parent(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(ltype, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (streq(rvalue, "root"))
+ qdisc->parent = TC_H_ROOT;
+ else if (streq(rvalue, "clsact")) {
+ qdisc->parent = TC_H_CLSACT;
+ qdisc->handle = TC_H_MAKE(TC_H_CLSACT, 0);
+ } else if (streq(rvalue, "ingress")) {
+ qdisc->parent = TC_H_INGRESS;
+ qdisc->handle = TC_H_MAKE(TC_H_INGRESS, 0);
+ } else {
+ r = parse_handle(rvalue, &qdisc->parent);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse 'Parent=', ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ }
+
+ if (STR_IN_SET(rvalue, "clsact", "ingress")) {
+ r = free_and_strdup(&qdisc->tca_kind, rvalue);
+ if (r < 0)
+ return log_oom();
+ } else
+ qdisc->tca_kind = mfree(qdisc->tca_kind);
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int config_parse_qdisc_handle(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ uint16_t n;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(ltype, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (isempty(rvalue)) {
+ qdisc->handle = TC_H_UNSPEC;
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou16_full(rvalue, 16, &n);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse 'Handle=', ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ qdisc->handle = (uint32_t) n << 16;
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
diff --git a/src/network/tc/qdisc.h b/src/network/tc/qdisc.h
new file mode 100644
index 0000000..155e2ad
--- /dev/null
+++ b/src/network/tc/qdisc.h
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+typedef struct Network Network;
+
+typedef enum QDiscKind {
+ QDISC_KIND_BFIFO,
+ QDISC_KIND_CAKE,
+ QDISC_KIND_CODEL,
+ QDISC_KIND_DRR,
+ QDISC_KIND_ETS,
+ QDISC_KIND_FQ,
+ QDISC_KIND_FQ_CODEL,
+ QDISC_KIND_FQ_PIE,
+ QDISC_KIND_GRED,
+ QDISC_KIND_HHF,
+ QDISC_KIND_HTB,
+ QDISC_KIND_NETEM,
+ QDISC_KIND_PFIFO,
+ QDISC_KIND_PFIFO_FAST,
+ QDISC_KIND_PFIFO_HEAD_DROP,
+ QDISC_KIND_PIE,
+ QDISC_KIND_QFQ,
+ QDISC_KIND_SFB,
+ QDISC_KIND_SFQ,
+ QDISC_KIND_TBF,
+ QDISC_KIND_TEQL,
+ _QDISC_KIND_MAX,
+ _QDISC_KIND_INVALID = -EINVAL,
+} QDiscKind;
+
+typedef struct QDisc {
+ Link *link;
+ Network *network;
+ ConfigSection *section;
+ NetworkConfigSource source;
+ NetworkConfigState state;
+
+ uint32_t handle;
+ uint32_t parent;
+
+ char *tca_kind;
+ QDiscKind kind;
+} QDisc;
+
+typedef struct QDiscVTable {
+ size_t object_size;
+ const char *tca_kind;
+ /* called in qdisc_new() */
+ int (*init)(QDisc *qdisc);
+ int (*fill_message)(Link *link, QDisc *qdisc, sd_netlink_message *m);
+ int (*verify)(QDisc *qdisc);
+ int (*is_ready)(QDisc *qdisc, Link *link);
+} QDiscVTable;
+
+extern const QDiscVTable * const qdisc_vtable[_QDISC_KIND_MAX];
+
+#define QDISC_VTABLE(q) ((q)->kind != _QDISC_KIND_INVALID ? qdisc_vtable[(q)->kind] : NULL)
+
+/* For casting a qdisc into the various qdisc kinds */
+#define DEFINE_QDISC_CAST(UPPERCASE, MixedCase) \
+ static inline MixedCase* UPPERCASE(QDisc *q) { \
+ if (_unlikely_(!q || q->kind != QDISC_KIND_##UPPERCASE)) \
+ return NULL; \
+ \
+ return (MixedCase*) q; \
+ }
+
+DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(QDisc, qdisc);
+
+QDisc* qdisc_free(QDisc *qdisc);
+int qdisc_new_static(QDiscKind kind, Network *network, const char *filename, unsigned section_line, QDisc **ret);
+
+int link_find_qdisc(Link *link, uint32_t handle, uint32_t parent, const char *kind, QDisc **qdisc);
+
+int link_request_qdisc(Link *link, QDisc *qdisc);
+
+void network_drop_invalid_qdisc(Network *network);
+
+int manager_rtnl_process_qdisc(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(QDisc, qdisc_free);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_qdisc_parent);
+CONFIG_PARSER_PROTOTYPE(config_parse_qdisc_handle);
+
+#include "cake.h"
+#include "codel.h"
+#include "ets.h"
+#include "fifo.h"
+#include "fq-codel.h"
+#include "fq-pie.h"
+#include "fq.h"
+#include "gred.h"
+#include "hhf.h"
+#include "htb.h"
+#include "pie.h"
+#include "qfq.h"
+#include "netem.h"
+#include "drr.h"
+#include "sfb.h"
+#include "sfq.h"
+#include "tbf.h"
+#include "teql.h"
diff --git a/src/network/tc/qfq.c b/src/network/tc/qfq.c
new file mode 100644
index 0000000..7702e6f
--- /dev/null
+++ b/src/network/tc/qfq.c
@@ -0,0 +1,177 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "parse-util.h"
+#include "qdisc.h"
+#include "qfq.h"
+#include "string-util.h"
+
+#define QFQ_MAX_WEIGHT (1 << 10)
+#define QFQ_MIN_MAX_PACKET 512
+#define QFQ_MAX_MAX_PACKET (1 << 16)
+
+const QDiscVTable qfq_vtable = {
+ .object_size = sizeof(QuickFairQueueing),
+ .tca_kind = "qfq",
+};
+
+static int quick_fair_queueing_class_fill_message(Link *link, TClass *tclass, sd_netlink_message *req) {
+ QuickFairQueueingClass *qfq;
+ int r;
+
+ assert(link);
+ assert(tclass);
+ assert(req);
+
+ assert_se(qfq = TCLASS_TO_QFQ(tclass));
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "qfq");
+ if (r < 0)
+ return r;
+
+ if (qfq->weight > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_QFQ_WEIGHT, qfq->weight);
+ if (r < 0)
+ return r;
+ }
+
+ if (qfq->max_packet > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_QFQ_LMAX, qfq->max_packet);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_quick_fair_queueing_weight(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL;
+ QuickFairQueueingClass *qfq;
+ Network *network = ASSERT_PTR(data);
+ uint32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = tclass_new_static(TCLASS_KIND_QFQ, network, filename, section_line, &tclass);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to create traffic control class, ignoring assignment: %m");
+ return 0;
+ }
+
+ qfq = TCLASS_TO_QFQ(tclass);
+
+ if (isempty(rvalue)) {
+ qfq->weight = 0;
+ TAKE_PTR(tclass);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (v == 0 || v > QFQ_MAX_WEIGHT) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ qfq->weight = v;
+ TAKE_PTR(tclass);
+
+ return 0;
+}
+
+int config_parse_quick_fair_queueing_max_packet(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL;
+ QuickFairQueueingClass *qfq;
+ Network *network = ASSERT_PTR(data);
+ uint64_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = tclass_new_static(TCLASS_KIND_QFQ, network, filename, section_line, &tclass);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to create traffic control class, ignoring assignment: %m");
+ return 0;
+ }
+
+ qfq = TCLASS_TO_QFQ(tclass);
+
+ if (isempty(rvalue)) {
+ qfq->max_packet = 0;
+ TAKE_PTR(tclass);
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1024, &v);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (v < QFQ_MIN_MAX_PACKET || v > QFQ_MAX_MAX_PACKET) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ qfq->max_packet = (uint32_t) v;
+ TAKE_PTR(tclass);
+
+ return 0;
+}
+
+const TClassVTable qfq_tclass_vtable = {
+ .object_size = sizeof(QuickFairQueueingClass),
+ .tca_kind = "qfq",
+ .fill_message = quick_fair_queueing_class_fill_message,
+};
diff --git a/src/network/tc/qfq.h b/src/network/tc/qfq.h
new file mode 100644
index 0000000..0f013a9
--- /dev/null
+++ b/src/network/tc/qfq.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct QuickFairQueueing {
+ QDisc meta;
+} QuickFairQueueing;
+
+DEFINE_QDISC_CAST(QFQ, QuickFairQueueing);
+extern const QDiscVTable qfq_vtable;
+
+typedef struct QuickFairQueueingClass {
+ TClass meta;
+
+ uint32_t weight;
+ uint32_t max_packet;
+} QuickFairQueueingClass;
+
+DEFINE_TCLASS_CAST(QFQ, QuickFairQueueingClass);
+extern const TClassVTable qfq_tclass_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_quick_fair_queueing_weight);
+CONFIG_PARSER_PROTOTYPE(config_parse_quick_fair_queueing_max_packet);
diff --git a/src/network/tc/sfb.c b/src/network/tc/sfb.c
new file mode 100644
index 0000000..861c5fe
--- /dev/null
+++ b/src/network/tc/sfb.c
@@ -0,0 +1,107 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "sfb.h"
+#include "string-util.h"
+
+static int stochastic_fair_blue_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ StochasticFairBlue *sfb;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(sfb = SFB(qdisc));
+
+ const struct tc_sfb_qopt opt = {
+ .rehash_interval = 600*1000,
+ .warmup_time = 60*1000,
+ .penalty_rate = 10,
+ .penalty_burst = 20,
+ .increment = (SFB_MAX_PROB + 1000) / 2000,
+ .decrement = (SFB_MAX_PROB + 10000) / 20000,
+ .max = 25,
+ .bin_size = 20,
+ .limit = sfb->packet_limit,
+ };
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "sfb");
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, TCA_SFB_PARMS, &opt, sizeof(opt));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_stochastic_fair_blue_u32(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ StochasticFairBlue *sfb;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_SFB, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ sfb = SFB(qdisc);
+
+ if (isempty(rvalue)) {
+ sfb->packet_limit = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &sfb->packet_limit);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+const QDiscVTable sfb_vtable = {
+ .object_size = sizeof(StochasticFairBlue),
+ .tca_kind = "sfb",
+ .fill_message = stochastic_fair_blue_fill_message,
+};
diff --git a/src/network/tc/sfb.h b/src/network/tc/sfb.h
new file mode 100644
index 0000000..628df35
--- /dev/null
+++ b/src/network/tc/sfb.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct StochasticFairBlue {
+ QDisc meta;
+
+ uint32_t packet_limit;
+} StochasticFairBlue;
+
+DEFINE_QDISC_CAST(SFB, StochasticFairBlue);
+extern const QDiscVTable sfb_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_stochastic_fair_blue_u32);
diff --git a/src/network/tc/sfq.c b/src/network/tc/sfq.c
new file mode 100644
index 0000000..92dbae1
--- /dev/null
+++ b/src/network/tc/sfq.c
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "sfq.h"
+#include "string-util.h"
+
+static int stochastic_fairness_queueing_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ StochasticFairnessQueueing *sfq;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(sfq = SFQ(qdisc));
+
+ const struct tc_sfq_qopt_v1 opt = {
+ .v0.perturb_period = sfq->perturb_period / USEC_PER_SEC,
+ };
+
+ r = sd_netlink_message_append_data(req, TCA_OPTIONS, &opt, sizeof(opt));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_stochastic_fairness_queueing_perturb_period(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ StochasticFairnessQueueing *sfq;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_SFQ, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ sfq = SFQ(qdisc);
+
+ if (isempty(rvalue)) {
+ sfq->perturb_period = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_sec(rvalue, &sfq->perturb_period);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+const QDiscVTable sfq_vtable = {
+ .object_size = sizeof(StochasticFairnessQueueing),
+ .tca_kind = "sfq",
+ .fill_message = stochastic_fairness_queueing_fill_message,
+};
diff --git a/src/network/tc/sfq.h b/src/network/tc/sfq.h
new file mode 100644
index 0000000..1626775
--- /dev/null
+++ b/src/network/tc/sfq.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+#include "time-util.h"
+
+typedef struct StochasticFairnessQueueing {
+ QDisc meta;
+
+ usec_t perturb_period;
+} StochasticFairnessQueueing;
+
+DEFINE_QDISC_CAST(SFQ, StochasticFairnessQueueing);
+extern const QDiscVTable sfq_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_stochastic_fairness_queueing_perturb_period);
diff --git a/src/network/tc/tbf.c b/src/network/tc/tbf.c
new file mode 100644
index 0000000..647fc8c
--- /dev/null
+++ b/src/network/tc/tbf.c
@@ -0,0 +1,343 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+#include <math.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "netem.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "qdisc.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tc-util.h"
+
+static int token_bucket_filter_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ uint32_t rtab[256], ptab[256];
+ TokenBucketFilter *tbf;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ assert_se(tbf = TBF(qdisc));
+
+ struct tc_tbf_qopt opt = {
+ .rate.rate = tbf->rate >= (1ULL << 32) ? ~0U : tbf->rate,
+ .peakrate.rate = tbf->peak_rate >= (1ULL << 32) ? ~0U : tbf->peak_rate,
+ .rate.mpu = tbf->mpu,
+ };
+
+ if (tbf->limit > 0)
+ opt.limit = tbf->limit;
+ else {
+ double lim, lim2;
+
+ lim = tbf->rate * (double) tbf->latency / USEC_PER_SEC + tbf->burst;
+ if (tbf->peak_rate > 0) {
+ lim2 = tbf->peak_rate * (double) tbf->latency / USEC_PER_SEC + tbf->mtu;
+ lim = MIN(lim, lim2);
+ }
+ opt.limit = lim;
+ }
+
+ r = tc_fill_ratespec_and_table(&opt.rate, rtab, tbf->mtu);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to calculate ratespec: %m");
+
+ r = tc_transmit_time(opt.rate.rate, tbf->burst, &opt.buffer);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to calculate buffer size: %m");
+
+ if (opt.peakrate.rate > 0) {
+ opt.peakrate.mpu = tbf->mpu;
+
+ r = tc_fill_ratespec_and_table(&opt.peakrate, ptab, tbf->mtu);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to calculate ratespec: %m");
+
+ r = tc_transmit_time(opt.peakrate.rate, tbf->mtu, &opt.mtu);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to calculate mtu size: %m");
+ }
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "tbf");
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, TCA_TBF_PARMS, &opt, sizeof(opt));
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, TCA_TBF_BURST, &tbf->burst, sizeof(tbf->burst));
+ if (r < 0)
+ return r;
+
+ if (tbf->rate >= (1ULL << 32)) {
+ r = sd_netlink_message_append_u64(req, TCA_TBF_RATE64, tbf->rate);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_data(req, TCA_TBF_RTAB, rtab, sizeof(rtab));
+ if (r < 0)
+ return r;
+
+ if (opt.peakrate.rate > 0) {
+ if (tbf->peak_rate >= (1ULL << 32)) {
+ r = sd_netlink_message_append_u64(req, TCA_TBF_PRATE64, tbf->peak_rate);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_append_u32(req, TCA_TBF_PBURST, tbf->mtu);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_data(req, TCA_TBF_PTAB, ptab, sizeof(ptab));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_token_bucket_filter_size(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ TokenBucketFilter *tbf;
+ uint64_t k;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_TBF, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ tbf = TBF(qdisc);
+
+ if (isempty(rvalue)) {
+ if (STR_IN_SET(lvalue, "BurstBytes", "Burst"))
+ tbf->burst = 0;
+ else if (STR_IN_SET(lvalue, "LimitBytes", "LimitSize"))
+ tbf->limit = 0;
+ else if (streq(lvalue, "MTUBytes"))
+ tbf->mtu = 0;
+ else if (streq(lvalue, "MPUBytes"))
+ tbf->mpu = 0;
+ else
+ assert_not_reached();
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1024, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ if (STR_IN_SET(lvalue, "BurstBytes", "Burst"))
+ tbf->burst = k;
+ else if (STR_IN_SET(lvalue, "LimitBytes", "LimitSize"))
+ tbf->limit = k;
+ else if (streq(lvalue, "MPUBytes"))
+ tbf->mpu = k;
+ else if (streq(lvalue, "MTUBytes"))
+ tbf->mtu = k;
+ else
+ assert_not_reached();
+
+ TAKE_PTR(qdisc);
+
+ return 0;
+}
+
+int config_parse_token_bucket_filter_rate(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ TokenBucketFilter *tbf;
+ uint64_t k, *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_TBF, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ tbf = TBF(qdisc);
+ if (streq(lvalue, "Rate"))
+ p = &tbf->rate;
+ else if (streq(lvalue, "PeakRate"))
+ p = &tbf->peak_rate;
+ else
+ assert_not_reached();
+
+ if (isempty(rvalue)) {
+ *p = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = parse_size(rvalue, 1000, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ *p = k / 8;
+
+ qdisc = NULL;
+
+ return 0;
+}
+
+int config_parse_token_bucket_filter_latency(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ Network *network = ASSERT_PTR(data);
+ TokenBucketFilter *tbf;
+ usec_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_TBF, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ tbf = TBF(qdisc);
+
+ if (isempty(rvalue)) {
+ tbf->latency = 0;
+
+ qdisc = NULL;
+ return 0;
+ }
+
+ r = parse_sec(rvalue, &u);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+
+ tbf->latency = u;
+
+ qdisc = NULL;
+
+ return 0;
+}
+
+static int token_bucket_filter_verify(QDisc *qdisc) {
+ TokenBucketFilter *tbf = TBF(qdisc);
+
+ if (tbf->limit > 0 && tbf->latency > 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Specifying both LimitBytes= and LatencySec= is not allowed. "
+ "Ignoring [TokenBucketFilter] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ if (tbf->limit == 0 && tbf->latency == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Either LimitBytes= or LatencySec= is required. "
+ "Ignoring [TokenBucketFilter] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ if (tbf->rate == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: Rate= is mandatory. "
+ "Ignoring [TokenBucketFilter] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ if (tbf->burst == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: BurstBytes= is mandatory. "
+ "Ignoring [TokenBucketFilter] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ if (tbf->peak_rate > 0 && tbf->mtu == 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: MTUBytes= is mandatory when PeakRate= is specified. "
+ "Ignoring [TokenBucketFilter] section from line %u.",
+ qdisc->section->filename, qdisc->section->line);
+
+ return 0;
+}
+
+const QDiscVTable tbf_vtable = {
+ .object_size = sizeof(TokenBucketFilter),
+ .tca_kind = "tbf",
+ .fill_message = token_bucket_filter_fill_message,
+ .verify = token_bucket_filter_verify
+};
diff --git a/src/network/tc/tbf.h b/src/network/tc/tbf.h
new file mode 100644
index 0000000..6b4b017
--- /dev/null
+++ b/src/network/tc/tbf.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+#include "time-util.h"
+
+typedef struct TokenBucketFilter {
+ QDisc meta;
+
+ uint64_t rate;
+ uint64_t peak_rate;
+ uint32_t burst;
+ uint32_t mtu;
+ usec_t latency;
+ size_t limit;
+ size_t mpu;
+} TokenBucketFilter;
+
+DEFINE_QDISC_CAST(TBF, TokenBucketFilter);
+extern const QDiscVTable tbf_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_token_bucket_filter_latency);
+CONFIG_PARSER_PROTOTYPE(config_parse_token_bucket_filter_size);
+CONFIG_PARSER_PROTOTYPE(config_parse_token_bucket_filter_rate);
diff --git a/src/network/tc/tc-util.c b/src/network/tc/tc-util.c
new file mode 100644
index 0000000..3781182
--- /dev/null
+++ b/src/network/tc/tc-util.c
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include "alloc-util.h"
+#include "extract-word.h"
+#include "fileio.h"
+#include "parse-util.h"
+#include "percent-util.h"
+#include "tc-util.h"
+#include "time-util.h"
+
+int tc_init(double *ret_ticks_in_usec, uint32_t *ret_hz) {
+ static double ticks_in_usec = -1;
+ static uint32_t hz;
+
+ if (ticks_in_usec < 0) {
+ uint32_t clock_resolution, ticks_to_usec, usec_to_ticks;
+ _cleanup_free_ char *line = NULL;
+ double clock_factor;
+ int r;
+
+ r = read_one_line_file("/proc/net/psched", &line);
+ if (r < 0)
+ return r;
+
+ r = sscanf(line, "%08x%08x%08x%08x", &ticks_to_usec, &usec_to_ticks, &clock_resolution, &hz);
+ if (r < 4)
+ return -EIO;
+
+ clock_factor = (double) clock_resolution / USEC_PER_SEC;
+ ticks_in_usec = (double) ticks_to_usec / usec_to_ticks * clock_factor;
+ }
+
+ if (ret_ticks_in_usec)
+ *ret_ticks_in_usec = ticks_in_usec;
+ if (ret_hz)
+ *ret_hz = hz;
+
+ return 0;
+}
+
+int tc_time_to_tick(usec_t t, uint32_t *ret) {
+ double ticks_in_usec;
+ usec_t a;
+ int r;
+
+ assert(ret);
+
+ r = tc_init(&ticks_in_usec, NULL);
+ if (r < 0)
+ return r;
+
+ a = t * ticks_in_usec;
+ if (a > UINT32_MAX)
+ return -ERANGE;
+
+ *ret = a;
+ return 0;
+}
+
+int parse_tc_percent(const char *s, uint32_t *ret_fraction) {
+ int r;
+
+ assert(s);
+ assert(ret_fraction);
+
+ r = parse_permyriad(s);
+ if (r < 0)
+ return r;
+
+ *ret_fraction = (double) r / 10000 * UINT32_MAX;
+ return 0;
+}
+
+int tc_transmit_time(uint64_t rate, uint32_t size, uint32_t *ret) {
+ return tc_time_to_tick(USEC_PER_SEC * ((double)size / (double)rate), ret);
+}
+
+int tc_fill_ratespec_and_table(struct tc_ratespec *rate, uint32_t *rtab, uint32_t mtu) {
+ uint32_t cell_log = 0;
+ int r;
+
+ if (mtu == 0)
+ mtu = 2047;
+
+ while ((mtu >> cell_log) > 255)
+ cell_log++;
+
+ for (size_t i = 0; i < 256; i++) {
+ uint32_t sz;
+
+ sz = (i + 1) << cell_log;
+ if (sz < rate->mpu)
+ sz = rate->mpu;
+ r = tc_transmit_time(rate->rate, sz, &rtab[i]);
+ if (r < 0)
+ return r;
+ }
+
+ rate->cell_align = -1;
+ rate->cell_log = cell_log;
+ rate->linklayer = TC_LINKLAYER_ETHERNET;
+ return 0;
+}
+
+int parse_handle(const char *t, uint32_t *ret) {
+ _cleanup_free_ char *word = NULL;
+ uint16_t major, minor;
+ int r;
+
+ assert(t);
+ assert(ret);
+
+ /* Extract the major number. */
+ r = extract_first_word(&t, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EINVAL;
+ if (!t)
+ return -EINVAL;
+
+ r = safe_atou16_full(word, 16, &major);
+ if (r < 0)
+ return r;
+
+ r = safe_atou16_full(t, 16, &minor);
+ if (r < 0)
+ return r;
+
+ *ret = ((uint32_t) major << 16) | minor;
+ return 0;
+}
diff --git a/src/network/tc/tc-util.h b/src/network/tc/tc-util.h
new file mode 100644
index 0000000..83bad8e
--- /dev/null
+++ b/src/network/tc/tc-util.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include <linux/pkt_sched.h>
+
+#include "time-util.h"
+
+int tc_init(double *ret_ticks_in_usec, uint32_t *ret_hz);
+int tc_time_to_tick(usec_t t, uint32_t *ret);
+int parse_tc_percent(const char *s, uint32_t *percent);
+int tc_transmit_time(uint64_t rate, uint32_t size, uint32_t *ret);
+int tc_fill_ratespec_and_table(struct tc_ratespec *rate, uint32_t *rtab, uint32_t mtu);
+int parse_handle(const char *t, uint32_t *ret);
diff --git a/src/network/tc/tc.c b/src/network/tc/tc.c
new file mode 100644
index 0000000..8a1c5b3
--- /dev/null
+++ b/src/network/tc/tc.c
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "macro.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "qdisc.h"
+#include "tc.h"
+#include "tclass.h"
+
+int link_request_traffic_control(Link *link) {
+ TClass *tclass;
+ QDisc *qdisc;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->tc_configured = false;
+
+ HASHMAP_FOREACH(qdisc, link->network->qdiscs_by_section) {
+ r = link_request_qdisc(link, qdisc);
+ if (r < 0)
+ return r;
+ }
+
+ HASHMAP_FOREACH(tclass, link->network->tclasses_by_section) {
+ r = link_request_tclass(link, tclass);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->tc_messages == 0) {
+ link->tc_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Setting traffic control");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
diff --git a/src/network/tc/tc.h b/src/network/tc/tc.h
new file mode 100644
index 0000000..6226578
--- /dev/null
+++ b/src/network/tc/tc.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct Link Link;
+
+int link_request_traffic_control(Link *link);
diff --git a/src/network/tc/tclass.c b/src/network/tc/tclass.c
new file mode 100644
index 0000000..0452cc9
--- /dev/null
+++ b/src/network/tc/tclass.c
@@ -0,0 +1,605 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include <linux/pkt_sched.h>
+
+#include "alloc-util.h"
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "parse-util.h"
+#include "set.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tc-util.h"
+#include "tclass.h"
+
+const TClassVTable * const tclass_vtable[_TCLASS_KIND_MAX] = {
+ [TCLASS_KIND_DRR] = &drr_tclass_vtable,
+ [TCLASS_KIND_HTB] = &htb_tclass_vtable,
+ [TCLASS_KIND_QFQ] = &qfq_tclass_vtable,
+};
+
+static int tclass_new(TClassKind kind, TClass **ret) {
+ _cleanup_(tclass_freep) TClass *tclass = NULL;
+ int r;
+
+ if (kind == _TCLASS_KIND_INVALID) {
+ tclass = new(TClass, 1);
+ if (!tclass)
+ return -ENOMEM;
+
+ *tclass = (TClass) {
+ .parent = TC_H_ROOT,
+ .kind = kind,
+ };
+ } else {
+ assert(kind >= 0 && kind < _TCLASS_KIND_MAX);
+ tclass = malloc0(tclass_vtable[kind]->object_size);
+ if (!tclass)
+ return -ENOMEM;
+
+ tclass->parent = TC_H_ROOT;
+ tclass->kind = kind;
+
+ if (TCLASS_VTABLE(tclass)->init) {
+ r = TCLASS_VTABLE(tclass)->init(tclass);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ *ret = TAKE_PTR(tclass);
+
+ return 0;
+}
+
+int tclass_new_static(TClassKind kind, Network *network, const char *filename, unsigned section_line, TClass **ret) {
+ _cleanup_(config_section_freep) ConfigSection *n = NULL;
+ _cleanup_(tclass_freep) TClass *tclass = NULL;
+ TClass *existing;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ existing = hashmap_get(network->tclasses_by_section, n);
+ if (existing) {
+ if (existing->kind != kind)
+ return -EINVAL;
+
+ *ret = existing;
+ return 0;
+ }
+
+ r = tclass_new(kind, &tclass);
+ if (r < 0)
+ return r;
+
+ tclass->network = network;
+ tclass->section = TAKE_PTR(n);
+ tclass->source = NETWORK_CONFIG_SOURCE_STATIC;
+
+ r = hashmap_ensure_put(&network->tclasses_by_section, &config_section_hash_ops, tclass->section, tclass);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(tclass);
+ return 0;
+}
+
+TClass* tclass_free(TClass *tclass) {
+ if (!tclass)
+ return NULL;
+
+ if (tclass->network && tclass->section)
+ hashmap_remove(tclass->network->tclasses_by_section, tclass->section);
+
+ config_section_free(tclass->section);
+
+ if (tclass->link)
+ set_remove(tclass->link->tclasses, tclass);
+
+ free(tclass->tca_kind);
+ return mfree(tclass);
+}
+
+static const char *tclass_get_tca_kind(const TClass *tclass) {
+ assert(tclass);
+
+ return (TCLASS_VTABLE(tclass) && TCLASS_VTABLE(tclass)->tca_kind) ?
+ TCLASS_VTABLE(tclass)->tca_kind : tclass->tca_kind;
+}
+
+static void tclass_hash_func(const TClass *tclass, struct siphash *state) {
+ assert(tclass);
+ assert(state);
+
+ siphash24_compress(&tclass->classid, sizeof(tclass->classid), state);
+ siphash24_compress(&tclass->parent, sizeof(tclass->parent), state);
+ siphash24_compress_string(tclass_get_tca_kind(tclass), state);
+}
+
+static int tclass_compare_func(const TClass *a, const TClass *b) {
+ int r;
+
+ assert(a);
+ assert(b);
+
+ r = CMP(a->classid, b->classid);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->parent, b->parent);
+ if (r != 0)
+ return r;
+
+ return strcmp_ptr(tclass_get_tca_kind(a), tclass_get_tca_kind(b));
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ tclass_hash_ops,
+ TClass,
+ tclass_hash_func,
+ tclass_compare_func,
+ tclass_free);
+
+static int tclass_get(Link *link, const TClass *in, TClass **ret) {
+ TClass *existing;
+
+ assert(link);
+ assert(in);
+
+ existing = set_get(link->tclasses, in);
+ if (!existing)
+ return -ENOENT;
+
+ if (ret)
+ *ret = existing;
+ return 0;
+}
+
+static int tclass_add(Link *link, TClass *tclass) {
+ int r;
+
+ assert(link);
+ assert(tclass);
+
+ r = set_ensure_put(&link->tclasses, &tclass_hash_ops, tclass);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ tclass->link = link;
+ return 0;
+}
+
+static int tclass_dup(const TClass *src, TClass **ret) {
+ _cleanup_(tclass_freep) TClass *dst = NULL;
+
+ assert(src);
+ assert(ret);
+
+ if (TCLASS_VTABLE(src))
+ dst = memdup(src, TCLASS_VTABLE(src)->object_size);
+ else
+ dst = newdup(TClass, src, 1);
+ if (!dst)
+ return -ENOMEM;
+
+ /* clear all pointers */
+ dst->network = NULL;
+ dst->section = NULL;
+ dst->link = NULL;
+ dst->tca_kind = NULL;
+
+ if (src->tca_kind) {
+ dst->tca_kind = strdup(src->tca_kind);
+ if (!dst->tca_kind)
+ return -ENOMEM;
+ }
+
+ *ret = TAKE_PTR(dst);
+ return 0;
+}
+
+int link_find_tclass(Link *link, uint32_t classid, TClass **ret) {
+ TClass *tclass;
+
+ assert(link);
+
+ SET_FOREACH(tclass, link->tclasses) {
+ if (tclass->classid != classid)
+ continue;
+
+ if (tclass->source == NETWORK_CONFIG_SOURCE_FOREIGN)
+ continue;
+
+ if (!tclass_exists(tclass))
+ continue;
+
+ if (ret)
+ *ret = tclass;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static void log_tclass_debug(TClass *tclass, Link *link, const char *str) {
+ _cleanup_free_ char *state = NULL;
+
+ assert(tclass);
+ assert(str);
+
+ if (!DEBUG_LOGGING)
+ return;
+
+ (void) network_config_state_to_string_alloc(tclass->state, &state);
+
+ log_link_debug(link, "%s %s TClass (%s): classid=%"PRIx32":%"PRIx32", parent=%"PRIx32":%"PRIx32", kind=%s",
+ str, strna(network_config_source_to_string(tclass->source)), strna(state),
+ TC_H_MAJ(tclass->classid) >> 16, TC_H_MIN(tclass->classid),
+ TC_H_MAJ(tclass->parent) >> 16, TC_H_MIN(tclass->parent),
+ strna(tclass_get_tca_kind(tclass)));
+}
+
+static int tclass_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, TClass *tclass) {
+ int r;
+
+ assert(m);
+ assert(link);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_error_errno(link, m, r, "Could not set TClass");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->tc_messages == 0) {
+ log_link_debug(link, "Traffic control configured");
+ link->tc_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int tclass_configure(TClass *tclass, Link *link, Request *req) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(tclass);
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+ assert(req);
+
+ log_tclass_debug(tclass, link, "Configuring");
+
+ r = sd_rtnl_message_new_traffic_control(link->manager->rtnl, &m, RTM_NEWTCLASS,
+ link->ifindex, tclass->classid, tclass->parent);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_string(m, TCA_KIND, TCLASS_VTABLE(tclass)->tca_kind);
+ if (r < 0)
+ return r;
+
+ if (TCLASS_VTABLE(tclass)->fill_message) {
+ r = TCLASS_VTABLE(tclass)->fill_message(link, tclass, m);
+ if (r < 0)
+ return r;
+ }
+
+ return request_call_netlink_async(link->manager->rtnl, m, req);
+}
+
+static bool tclass_is_ready_to_configure(TClass *tclass, Link *link) {
+ assert(tclass);
+ assert(link);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return false;
+
+ return link_find_qdisc(link, tclass->classid, tclass->parent, tclass_get_tca_kind(tclass), NULL) >= 0;
+}
+
+static int tclass_process_request(Request *req, Link *link, TClass *tclass) {
+ int r;
+
+ assert(req);
+ assert(link);
+ assert(tclass);
+
+ if (!tclass_is_ready_to_configure(tclass, link))
+ return 0;
+
+ r = tclass_configure(tclass, link, req);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure TClass: %m");
+
+ tclass_enter_configuring(tclass);
+ return 1;
+}
+
+int link_request_tclass(Link *link, TClass *tclass) {
+ TClass *existing;
+ int r;
+
+ assert(link);
+ assert(tclass);
+
+ if (tclass_get(link, tclass, &existing) < 0) {
+ _cleanup_(tclass_freep) TClass *tmp = NULL;
+
+ r = tclass_dup(tclass, &tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = tclass_add(link, tmp);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to store TClass: %m");
+
+ existing = TAKE_PTR(tmp);
+ } else
+ existing->source = tclass->source;
+
+ log_tclass_debug(existing, link, "Requesting");
+ r = link_queue_request_safe(link, REQUEST_TYPE_TC_CLASS,
+ existing, NULL,
+ tclass_hash_func,
+ tclass_compare_func,
+ tclass_process_request,
+ &link->tc_messages,
+ tclass_handler,
+ NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request TClass: %m");
+ if (r == 0)
+ return 0;
+
+ tclass_enter_requesting(existing);
+ return 1;
+}
+
+int manager_rtnl_process_tclass(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(tclass_freep) TClass *tmp = NULL;
+ TClass *tclass = NULL;
+ Link *link;
+ uint16_t type;
+ int ifindex, r;
+
+ assert(rtnl);
+ assert(message);
+ assert(m);
+
+ if (sd_netlink_message_is_error(message)) {
+ r = sd_netlink_message_get_errno(message);
+ if (r < 0)
+ log_message_warning_errno(message, r, "rtnl: failed to receive TClass message, ignoring");
+
+ return 0;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get message type, ignoring: %m");
+ return 0;
+ } else if (!IN_SET(type, RTM_NEWTCLASS, RTM_DELTCLASS)) {
+ log_warning("rtnl: received unexpected message type %u when processing TClass, ignoring.", type);
+ return 0;
+ }
+
+ r = sd_rtnl_message_traffic_control_get_ifindex(message, &ifindex);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m");
+ return 0;
+ } else if (ifindex <= 0) {
+ log_warning("rtnl: received TClass message with invalid ifindex %d, ignoring.", ifindex);
+ return 0;
+ }
+
+ if (link_get_by_index(m, ifindex, &link) < 0) {
+ if (!m->enumerating)
+ log_warning("rtnl: received TClass for link '%d' we don't know about, ignoring.", ifindex);
+ return 0;
+ }
+
+ r = tclass_new(_TCLASS_KIND_INVALID, &tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_rtnl_message_traffic_control_get_handle(message, &tmp->classid);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received TClass message without handle, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_traffic_control_get_parent(message, &tmp->parent);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received TClass message without parent, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string_strdup(message, TCA_KIND, &tmp->tca_kind);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received TClass message without kind, ignoring: %m");
+ return 0;
+ }
+
+ (void) tclass_get(link, tmp, &tclass);
+
+ switch (type) {
+ case RTM_NEWTCLASS:
+ if (tclass) {
+ tclass_enter_configured(tclass);
+ log_tclass_debug(tclass, link, "Received remembered");
+ } else {
+ tclass_enter_configured(tmp);
+ log_tclass_debug(tmp, link, "Received new");
+
+ r = tclass_add(link, tmp);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to remember TClass, ignoring: %m");
+ return 0;
+ }
+
+ tclass = TAKE_PTR(tmp);
+ }
+
+ break;
+
+ case RTM_DELTCLASS:
+ if (tclass) {
+ tclass_enter_removed(tclass);
+ if (tclass->state == 0) {
+ log_tclass_debug(tclass, link, "Forgetting");
+ tclass_free(tclass);
+ } else
+ log_tclass_debug(tclass, link, "Removed");
+ } else
+ log_tclass_debug(tmp, link, "Kernel removed unknown");
+
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int tclass_section_verify(TClass *tclass) {
+ int r;
+
+ assert(tclass);
+
+ if (section_is_invalid(tclass->section))
+ return -EINVAL;
+
+ if (TCLASS_VTABLE(tclass)->verify) {
+ r = TCLASS_VTABLE(tclass)->verify(tclass);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+void network_drop_invalid_tclass(Network *network) {
+ TClass *tclass;
+
+ assert(network);
+
+ HASHMAP_FOREACH(tclass, network->tclasses_by_section)
+ if (tclass_section_verify(tclass) < 0)
+ tclass_free(tclass);
+}
+
+int config_parse_tclass_parent(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = tclass_new_static(ltype, network, filename, section_line, &tclass);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to create traffic control class, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (streq(rvalue, "root"))
+ tclass->parent = TC_H_ROOT;
+ else {
+ r = parse_handle(rvalue, &tclass->parent);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse 'Parent=', ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ }
+
+ TAKE_PTR(tclass);
+
+ return 0;
+}
+
+int config_parse_tclass_classid(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL;
+ Network *network = ASSERT_PTR(data);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = tclass_new_static(ltype, network, filename, section_line, &tclass);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to create traffic control class, ignoring assignment: %m");
+ return 0;
+ }
+
+ if (isempty(rvalue)) {
+ tclass->classid = TC_H_UNSPEC;
+ TAKE_PTR(tclass);
+ return 0;
+ }
+
+ r = parse_handle(rvalue, &tclass->classid);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse 'ClassId=', ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(tclass);
+
+ return 0;
+}
diff --git a/src/network/tc/tclass.h b/src/network/tc/tclass.h
new file mode 100644
index 0000000..606bb3f
--- /dev/null
+++ b/src/network/tc/tclass.h
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+typedef struct Network Network;
+
+typedef enum TClassKind {
+ TCLASS_KIND_DRR,
+ TCLASS_KIND_HTB,
+ TCLASS_KIND_QFQ,
+ _TCLASS_KIND_MAX,
+ _TCLASS_KIND_INVALID = -EINVAL,
+} TClassKind;
+
+typedef struct TClass {
+ Link *link;
+ Network *network;
+ ConfigSection *section;
+ NetworkConfigSource source;
+ NetworkConfigState state;
+
+ uint32_t classid;
+ uint32_t parent;
+
+ TClassKind kind;
+ char *tca_kind;
+} TClass;
+
+typedef struct TClassVTable {
+ size_t object_size;
+ const char *tca_kind;
+ /* called in tclass_new() */
+ int (*init)(TClass *tclass);
+ int (*fill_message)(Link *link, TClass *tclass, sd_netlink_message *m);
+ int (*verify)(TClass *tclass);
+} TClassVTable;
+
+extern const TClassVTable * const tclass_vtable[_TCLASS_KIND_MAX];
+
+#define TCLASS_VTABLE(t) ((t)->kind != _TCLASS_KIND_INVALID ? tclass_vtable[(t)->kind] : NULL)
+
+/* For casting a tclass into the various tclass kinds */
+#define DEFINE_TCLASS_CAST(UPPERCASE, MixedCase) \
+ static inline MixedCase* TCLASS_TO_##UPPERCASE(TClass *t) { \
+ if (_unlikely_(!t || t->kind != TCLASS_KIND_##UPPERCASE)) \
+ return NULL; \
+ \
+ return (MixedCase*) t; \
+ }
+
+DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(TClass, tclass);
+
+TClass* tclass_free(TClass *tclass);
+int tclass_new_static(TClassKind kind, Network *network, const char *filename, unsigned section_line, TClass **ret);
+
+int link_find_tclass(Link *link, uint32_t classid, TClass **ret);
+
+int link_request_tclass(Link *link, TClass *tclass);
+
+void network_drop_invalid_tclass(Network *network);
+
+int manager_rtnl_process_tclass(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+DEFINE_SECTION_CLEANUP_FUNCTIONS(TClass, tclass_free);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_tclass_parent);
+CONFIG_PARSER_PROTOTYPE(config_parse_tclass_classid);
+
+#include "drr.h"
+#include "htb.h"
+#include "qfq.h"
diff --git a/src/network/tc/teql.c b/src/network/tc/teql.c
new file mode 100644
index 0000000..dcb149d
--- /dev/null
+++ b/src/network/tc/teql.c
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "macro.h"
+#include "networkd-link.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "teql.h"
+
+static int trivial_link_equalizer_verify(QDisc *qdisc) {
+ _cleanup_free_ char *tca_kind = NULL;
+ TrivialLinkEqualizer *teql;
+
+ teql = TEQL(ASSERT_PTR(qdisc));
+
+ if (asprintf(&tca_kind, "teql%u", teql->id) < 0)
+ return log_oom();
+
+ return free_and_replace(qdisc->tca_kind, tca_kind);
+}
+
+static int trivial_link_equalizer_is_ready(QDisc *qdisc, Link *link) {
+ Link *teql;
+
+ assert(qdisc);
+ assert(qdisc->tca_kind);
+ assert(link);
+ assert(link->manager);
+
+ if (link_get_by_name(link->manager, qdisc->tca_kind, &teql) < 0)
+ return false;
+
+ return link_is_ready_to_configure(teql, /* allow_unmanaged = */ true);
+}
+
+const QDiscVTable teql_vtable = {
+ .object_size = sizeof(TrivialLinkEqualizer),
+ .verify = trivial_link_equalizer_verify,
+ .is_ready = trivial_link_equalizer_is_ready,
+};
+
+int config_parse_trivial_link_equalizer_id(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
+ TrivialLinkEqualizer *teql;
+ Network *network = ASSERT_PTR(data);
+ unsigned id;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = qdisc_new_static(QDISC_KIND_TEQL, network, filename, section_line, &qdisc);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "More than one kind of queueing discipline, ignoring assignment: %m");
+ return 0;
+ }
+
+ teql = TEQL(qdisc);
+
+ if (isempty(rvalue)) {
+ teql->id = 0;
+
+ TAKE_PTR(qdisc);
+ return 0;
+ }
+
+ r = safe_atou(rvalue, &id);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ if (id > INT_MAX)
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "'%s=' is too large, ignoring assignment: %s",
+ lvalue, rvalue);
+
+ teql->id = id;
+
+ TAKE_PTR(qdisc);
+ return 0;
+}
diff --git a/src/network/tc/teql.h b/src/network/tc/teql.h
new file mode 100644
index 0000000..8d0085e
--- /dev/null
+++ b/src/network/tc/teql.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "qdisc.h"
+
+typedef struct TrivialLinkEqualizer {
+ QDisc meta;
+
+ unsigned id;
+} TrivialLinkEqualizer;
+
+DEFINE_QDISC_CAST(TEQL, TrivialLinkEqualizer);
+extern const QDiscVTable teql_vtable;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_trivial_link_equalizer_id);
diff --git a/src/network/test-network-tables.c b/src/network/test-network-tables.c
new file mode 100644
index 0000000..e3e2607
--- /dev/null
+++ b/src/network/test-network-tables.c
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bond.h"
+#include "dhcp6-internal.h"
+#include "dhcp6-protocol.h"
+#include "ethtool-util.h"
+#include "ipvlan.h"
+#include "lldp-rx-internal.h"
+#include "macvlan.h"
+#include "ndisc-internal.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "networkd-util.h"
+#include "test-tables.h"
+#include "tunnel.h"
+
+int main(int argc, char **argv) {
+ test_table(bond_ad_select, NETDEV_BOND_AD_SELECT);
+ test_table(bond_arp_all_targets, NETDEV_BOND_ARP_ALL_TARGETS);
+ test_table(bond_arp_validate, NETDEV_BOND_ARP_VALIDATE);
+ test_table(bond_fail_over_mac, NETDEV_BOND_FAIL_OVER_MAC);
+ test_table(bond_lacp_rate, NETDEV_BOND_LACP_RATE);
+ test_table(bond_mode, NETDEV_BOND_MODE);
+ test_table(bond_primary_reselect, NETDEV_BOND_PRIMARY_RESELECT);
+ test_table(bond_xmit_hash_policy, NETDEV_BOND_XMIT_HASH_POLICY);
+ test_table(dhcp6_message_status, DHCP6_STATUS);
+ test_table_sparse(dhcp6_message_type, DHCP6_MESSAGE_TYPE); /* enum starts from 1 */
+ test_table(dhcp_use_domains, DHCP_USE_DOMAINS);
+ test_table(duplex, DUP);
+ test_table(ip6tnl_mode, NETDEV_IP6_TNL_MODE);
+ test_table(ipv6_privacy_extensions, IPV6_PRIVACY_EXTENSIONS);
+ test_table(ipvlan_flags, NETDEV_IPVLAN_FLAGS);
+ test_table(link_operstate, LINK_OPERSTATE);
+ /* test_table(link_state, LINK_STATE); — not a reversible mapping */
+ test_table(lldp_mode, LLDP_MODE);
+ test_table(netdev_kind, NETDEV_KIND);
+ test_table(radv_prefix_delegation, RADV_PREFIX_DELEGATION);
+ test_table(lldp_rx_event, SD_LLDP_RX_EVENT);
+ test_table(ndisc_event, SD_NDISC_EVENT);
+ test_table(dhcp_lease_server_type, SD_DHCP_LEASE_SERVER_TYPE);
+
+ test_table_sparse(ipvlan_mode, NETDEV_IPVLAN_MODE);
+ test_table_sparse(macvlan_mode, NETDEV_MACVLAN_MODE);
+ test_table_sparse(address_family, ADDRESS_FAMILY);
+
+ assert_cc(sizeof(sd_lldp_rx_event_t) == sizeof(int64_t));
+ assert_cc(sizeof(sd_ndisc_event_t) == sizeof(int64_t));
+ assert_cc(sizeof(sd_dhcp_lease_server_type_t) == sizeof(int64_t));
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/network/test-network.c b/src/network/test-network.c
new file mode 100644
index 0000000..250ab9e
--- /dev/null
+++ b/src/network/test-network.c
@@ -0,0 +1,294 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <arpa/inet.h>
+#include <sys/param.h>
+
+#include "sd-device.h"
+
+#include "alloc-util.h"
+#include "dhcp-lease-internal.h"
+#include "ether-addr-util.h"
+#include "hostname-setup.h"
+#include "network-internal.h"
+#include "networkd-address.h"
+#include "networkd-manager.h"
+#include "networkd-route-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+static void test_deserialize_in_addr(void) {
+ _cleanup_free_ struct in_addr *addresses = NULL;
+ _cleanup_free_ struct in6_addr *addresses6 = NULL;
+ union in_addr_union a, b, c, d, e, f;
+ int size;
+ const char *addresses_string = "192.168.0.1 0:0:0:0:0:FFFF:204.152.189.116 192.168.0.2 ::1 192.168.0.3 1:0:0:0:0:0:0:8";
+
+ assert_se(in_addr_from_string(AF_INET, "0:0:0:0:0:FFFF:204.152.189.116", &a) < 0);
+ assert_se(in_addr_from_string(AF_INET6, "192.168.0.1", &d) < 0);
+
+ assert_se(in_addr_from_string(AF_INET, "192.168.0.1", &a) >= 0);
+ assert_se(in_addr_from_string(AF_INET, "192.168.0.2", &b) >= 0);
+ assert_se(in_addr_from_string(AF_INET, "192.168.0.3", &c) >= 0);
+ assert_se(in_addr_from_string(AF_INET6, "0:0:0:0:0:FFFF:204.152.189.116", &d) >= 0);
+ assert_se(in_addr_from_string(AF_INET6, "::1", &e) >= 0);
+ assert_se(in_addr_from_string(AF_INET6, "1:0:0:0:0:0:0:8", &f) >= 0);
+
+ assert_se((size = deserialize_in_addrs(&addresses, addresses_string)) >= 0);
+ assert_se(size == 3);
+ assert_se(in4_addr_equal(&a.in, &addresses[0]));
+ assert_se(in4_addr_equal(&b.in, &addresses[1]));
+ assert_se(in4_addr_equal(&c.in, &addresses[2]));
+
+ assert_se((size = deserialize_in6_addrs(&addresses6, addresses_string)) >= 0);
+ assert_se(size == 3);
+ assert_se(in6_addr_equal(&d.in6, &addresses6[0]));
+ assert_se(in6_addr_equal(&e.in6, &addresses6[1]));
+ assert_se(in6_addr_equal(&f.in6, &addresses6[2]));
+}
+
+static void test_deserialize_dhcp_routes(void) {
+ size_t size;
+
+ {
+ _cleanup_free_ struct sd_dhcp_route *routes = NULL;
+ assert_se(deserialize_dhcp_routes(&routes, &size, "") >= 0);
+ assert_se(size == 0);
+ }
+
+ {
+ /* no errors */
+ _cleanup_free_ struct sd_dhcp_route *routes = NULL;
+ const char *routes_string = "192.168.0.0/16,192.168.0.1 10.1.2.0/24,10.1.2.1 0.0.0.0/0,10.0.1.1";
+
+ assert_se(deserialize_dhcp_routes(&routes, &size, routes_string) >= 0);
+
+ assert_se(size == 3);
+ assert_se(routes[0].dst_addr.s_addr == inet_addr("192.168.0.0"));
+ assert_se(routes[0].gw_addr.s_addr == inet_addr("192.168.0.1"));
+ assert_se(routes[0].dst_prefixlen == 16);
+
+ assert_se(routes[1].dst_addr.s_addr == inet_addr("10.1.2.0"));
+ assert_se(routes[1].gw_addr.s_addr == inet_addr("10.1.2.1"));
+ assert_se(routes[1].dst_prefixlen == 24);
+
+ assert_se(routes[2].dst_addr.s_addr == inet_addr("0.0.0.0"));
+ assert_se(routes[2].gw_addr.s_addr == inet_addr("10.0.1.1"));
+ assert_se(routes[2].dst_prefixlen == 0);
+ }
+
+ {
+ /* error in second word */
+ _cleanup_free_ struct sd_dhcp_route *routes = NULL;
+ const char *routes_string = "192.168.0.0/16,192.168.0.1 10.1.2.0#24,10.1.2.1 0.0.0.0/0,10.0.1.1";
+
+ assert_se(deserialize_dhcp_routes(&routes, &size, routes_string) >= 0);
+
+ assert_se(size == 2);
+ assert_se(routes[0].dst_addr.s_addr == inet_addr("192.168.0.0"));
+ assert_se(routes[0].gw_addr.s_addr == inet_addr("192.168.0.1"));
+ assert_se(routes[0].dst_prefixlen == 16);
+
+ assert_se(routes[1].dst_addr.s_addr == inet_addr("0.0.0.0"));
+ assert_se(routes[1].gw_addr.s_addr == inet_addr("10.0.1.1"));
+ assert_se(routes[1].dst_prefixlen == 0);
+ }
+
+ {
+ /* error in every word */
+ _cleanup_free_ struct sd_dhcp_route *routes = NULL;
+ const char *routes_string = "192.168.0.0/55,192.168.0.1 10.1.2.0#24,10.1.2.1 0.0.0.0/0,10.0.1.X";
+
+ assert_se(deserialize_dhcp_routes(&routes, &size, routes_string) >= 0);
+ assert_se(size == 0);
+ }
+}
+
+static void test_route_tables_one(Manager *manager, const char *name, uint32_t number) {
+ _cleanup_free_ char *str = NULL, *expected = NULL, *num_str = NULL;
+ uint32_t t;
+
+ if (!STR_IN_SET(name, "default", "main", "local")) {
+ assert_se(streq(hashmap_get(manager->route_table_names_by_number, UINT32_TO_PTR(number)), name));
+ assert_se(PTR_TO_UINT32(hashmap_get(manager->route_table_numbers_by_name, name)) == number);
+ }
+
+ assert_se(asprintf(&expected, "%s(%" PRIu32 ")", name, number) >= 0);
+ assert_se(manager_get_route_table_to_string(manager, number, &str) >= 0);
+ assert_se(streq(str, expected));
+
+ assert_se(manager_get_route_table_from_string(manager, name, &t) >= 0);
+ assert_se(t == number);
+
+ assert_se(asprintf(&num_str, "%" PRIu32, number) >= 0);
+ assert_se(manager_get_route_table_from_string(manager, num_str, &t) >= 0);
+ assert_se(t == number);
+}
+
+static void test_route_tables(Manager *manager) {
+ assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "hoge:123 foo:456 aaa:111", manager, manager) >= 0);
+ assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "bbb:11111 ccc:22222", manager, manager) >= 0);
+ assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "ddd:22222", manager, manager) >= 0);
+
+ test_route_tables_one(manager, "hoge", 123);
+ test_route_tables_one(manager, "foo", 456);
+ test_route_tables_one(manager, "aaa", 111);
+ test_route_tables_one(manager, "bbb", 11111);
+ test_route_tables_one(manager, "ccc", 22222);
+
+ assert_se(!hashmap_get(manager->route_table_numbers_by_name, "ddd"));
+
+ test_route_tables_one(manager, "default", 253);
+ test_route_tables_one(manager, "main", 254);
+ test_route_tables_one(manager, "local", 255);
+
+ assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "", manager, manager) >= 0);
+ assert_se(!manager->route_table_names_by_number);
+ assert_se(!manager->route_table_numbers_by_name);
+
+ /* Invalid pairs */
+ assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "main:123 default:333 local:999", manager, manager) >= 0);
+ assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "1234:321 :567 hoge:foo aaa:-888", manager, manager) >= 0);
+ assert_se(!manager->route_table_names_by_number);
+ assert_se(!manager->route_table_numbers_by_name);
+
+ test_route_tables_one(manager, "default", 253);
+ test_route_tables_one(manager, "main", 254);
+ test_route_tables_one(manager, "local", 255);
+}
+
+static int test_load_config(Manager *manager) {
+ int r;
+/* TODO: should_reload, is false if the config dirs do not exist, so
+ * so we can't do this test here, move it to a test for paths_check_timestamps
+ * directly
+ *
+ * assert_se(network_should_reload(manager) == true);
+*/
+
+ r = manager_load_config(manager);
+ if (r == -EPERM)
+ return r;
+ assert_se(r >= 0);
+
+ return 0;
+}
+
+static void test_address_equality(void) {
+ _cleanup_(address_freep) Address *a1 = NULL, *a2 = NULL;
+
+ assert_se(address_new(&a1) >= 0);
+ assert_se(address_new(&a2) >= 0);
+
+ assert_se(address_equal(NULL, NULL));
+ assert_se(!address_equal(a1, NULL));
+ assert_se(!address_equal(NULL, a2));
+ assert_se(address_equal(a1, a2));
+
+ a1->family = AF_INET;
+ assert_se(!address_equal(a1, a2));
+
+ a2->family = AF_INET;
+ assert_se(address_equal(a1, a2));
+
+ assert_se(in_addr_from_string(AF_INET, "192.168.3.9", &a1->in_addr) >= 0);
+ assert_se(!address_equal(a1, a2));
+ assert_se(in_addr_from_string(AF_INET, "192.168.3.9", &a2->in_addr) >= 0);
+ assert_se(address_equal(a1, a2));
+ assert_se(in_addr_from_string(AF_INET, "192.168.3.10", &a1->in_addr_peer) >= 0);
+ assert_se(!address_equal(a1, a2));
+ assert_se(in_addr_from_string(AF_INET, "192.168.3.11", &a2->in_addr_peer) >= 0);
+ assert_se(!address_equal(a1, a2));
+ assert_se(in_addr_from_string(AF_INET, "192.168.3.10", &a2->in_addr_peer) >= 0);
+ assert_se(address_equal(a1, a2));
+ a1->prefixlen = 10;
+ assert_se(!address_equal(a1, a2));
+ a2->prefixlen = 10;
+ assert_se(address_equal(a1, a2));
+
+ a1->family = AF_INET6;
+ assert_se(!address_equal(a1, a2));
+
+ a2->family = AF_INET6;
+ assert_se(in_addr_from_string(AF_INET6, "2001:4ca0:4f01::2", &a1->in_addr) >= 0);
+ assert_se(in_addr_from_string(AF_INET6, "2001:4ca0:4f01::2", &a2->in_addr) >= 0);
+ assert_se(address_equal(a1, a2));
+
+ a2->prefixlen = 8;
+ assert_se(!address_equal(a1, a2));
+
+ a2->prefixlen = 10;
+ assert_se(in_addr_from_string(AF_INET6, "2001:4ca0:4f01::1", &a2->in_addr) >= 0);
+ assert_se(!address_equal(a1, a2));
+}
+
+static void test_dhcp_hostname_shorten_overlong(void) {
+ int r;
+
+ {
+ /* simple hostname, no actions, no errors */
+ _cleanup_free_ char *shortened = NULL;
+ r = shorten_overlong("name1", &shortened);
+ assert_se(r == 0);
+ assert_se(streq("name1", shortened));
+ }
+
+ {
+ /* simple fqdn, no actions, no errors */
+ _cleanup_free_ char *shortened = NULL;
+ r = shorten_overlong("name1.example.com", &shortened);
+ assert_se(r == 0);
+ assert_se(streq("name1.example.com", shortened));
+ }
+
+ {
+ /* overlong fqdn, cut to first dot, no errors */
+ _cleanup_free_ char *shortened = NULL;
+ r = shorten_overlong("name1.test-dhcp-this-one-here-is-a-very-very-long-domain.example.com", &shortened);
+ assert_se(r == 1);
+ assert_se(streq("name1", shortened));
+ }
+
+ {
+ /* overlong hostname, cut to HOST_MAX_LEN, no errors */
+ _cleanup_free_ char *shortened = NULL;
+ r = shorten_overlong("test-dhcp-this-one-here-is-a-very-very-long-hostname-without-domainname", &shortened);
+ assert_se(r == 1);
+ assert_se(streq("test-dhcp-this-one-here-is-a-very-very-long-hostname-without-dom", shortened));
+ }
+
+ {
+ /* overlong fqdn, cut to first dot, empty result error */
+ _cleanup_free_ char *shortened = NULL;
+ r = shorten_overlong(".test-dhcp-this-one-here-is-a-very-very-long-hostname.example.com", &shortened);
+ assert_se(r == -EDOM);
+ assert_se(shortened == NULL);
+ }
+
+}
+
+int main(void) {
+ _cleanup_(manager_freep) Manager *manager = NULL;
+ int r;
+
+ test_setup_logging(LOG_INFO);
+
+ test_deserialize_in_addr();
+ test_deserialize_dhcp_routes();
+ test_address_equality();
+ test_dhcp_hostname_shorten_overlong();
+
+ assert_se(manager_new(&manager, /* test_mode = */ true) >= 0);
+ assert_se(manager_setup(manager) >= 0);
+
+ test_route_tables(manager);
+
+ r = test_load_config(manager);
+ if (r == -EPERM)
+ log_debug("Cannot load configuration, ignoring.");
+ else
+ assert_se(r == 0);
+
+ assert_se(manager_enumerate(manager) >= 0);
+ return 0;
+}
diff --git a/src/network/test-networkd-address.c b/src/network/test-networkd-address.c
new file mode 100644
index 0000000..a40c571
--- /dev/null
+++ b/src/network/test-networkd-address.c
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "networkd-address.h"
+#include "tests.h"
+#include "time-util.h"
+
+static void test_FORMAT_LIFETIME_one(usec_t lifetime, const char *expected) {
+ const char *t = FORMAT_LIFETIME(lifetime);
+
+ log_debug(USEC_FMT " → \"%s\" (expected \"%s\")", lifetime, t, expected);
+ assert_se(streq(t, expected));
+}
+
+TEST(FORMAT_LIFETIME) {
+ usec_t now_usec;
+
+ now_usec = now(CLOCK_BOOTTIME);
+
+ test_FORMAT_LIFETIME_one(now_usec, "for 0");
+ test_FORMAT_LIFETIME_one(usec_add(now_usec, 2 * USEC_PER_SEC - 1), "for 1s");
+ test_FORMAT_LIFETIME_one(usec_add(now_usec, 3 * USEC_PER_WEEK + USEC_PER_SEC - 1), "for 3w");
+ test_FORMAT_LIFETIME_one(USEC_INFINITY, "forever");
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/network/test-networkd-conf.c b/src/network/test-networkd-conf.c
new file mode 100644
index 0000000..2e4ca0c
--- /dev/null
+++ b/src/network/test-networkd-conf.c
@@ -0,0 +1,251 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "hexdecoct.h"
+#include "log.h"
+#include "macro.h"
+#include "net-condition.h"
+#include "networkd-address.h"
+#include "networkd-conf.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "strv.h"
+#include "tests.h"
+
+static void test_config_parse_duid_type_one(const char *rvalue, int ret, DUIDType expected, usec_t expected_time) {
+ DUID actual = {};
+ int r;
+
+ r = config_parse_duid_type("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &actual, NULL);
+ log_info_errno(r, "\"%s\" → %d (%m)", rvalue, actual.type);
+ assert_se(r == ret);
+ assert_se(expected == actual.type);
+ if (expected == DUID_TYPE_LLT)
+ assert_se(expected_time == actual.llt_time);
+}
+
+TEST(config_parse_duid_type) {
+ test_config_parse_duid_type_one("", 0, 0, 0);
+ test_config_parse_duid_type_one("link-layer-time", 0, DUID_TYPE_LLT, 0);
+ test_config_parse_duid_type_one("link-layer-time:2000-01-01 00:00:00 UTC", 0, DUID_TYPE_LLT, (usec_t) 946684800000000);
+ test_config_parse_duid_type_one("vendor", 0, DUID_TYPE_EN, 0);
+ test_config_parse_duid_type_one("vendor:2000-01-01 00:00:00 UTC", 0, 0, 0);
+ test_config_parse_duid_type_one("link-layer", 0, DUID_TYPE_LL, 0);
+ test_config_parse_duid_type_one("link-layer:2000-01-01 00:00:00 UTC", 0, 0, 0);
+ test_config_parse_duid_type_one("uuid", 0, DUID_TYPE_UUID, 0);
+ test_config_parse_duid_type_one("uuid:2000-01-01 00:00:00 UTC", 0, 0, 0);
+ test_config_parse_duid_type_one("foo", 0, 0, 0);
+ test_config_parse_duid_type_one("foo:2000-01-01 00:00:00 UTC", 0, 0, 0);
+}
+
+static void test_config_parse_duid_rawdata_one(const char *rvalue, int ret, const DUID* expected) {
+ DUID actual = {};
+ int r;
+ _cleanup_free_ char *d = NULL;
+
+ r = config_parse_duid_rawdata("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &actual, NULL);
+ d = hexmem(actual.raw_data, actual.raw_data_len);
+ log_info_errno(r, "\"%s\" → \"%s\" (%m)",
+ rvalue, strnull(d));
+ assert_se(r == ret);
+ if (expected) {
+ assert_se(actual.raw_data_len == expected->raw_data_len);
+ assert_se(memcmp(actual.raw_data, expected->raw_data, expected->raw_data_len) == 0);
+ }
+}
+
+static void test_config_parse_ether_addr_one(const char *rvalue, int ret, const struct ether_addr* expected) {
+ struct ether_addr *actual = NULL;
+ int r;
+
+ r = config_parse_ether_addr("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &actual, NULL);
+ assert_se(ret == r);
+ if (expected) {
+ assert_se(actual);
+ assert_se(ether_addr_equal(expected, actual));
+ } else
+ assert_se(actual == NULL);
+
+ free(actual);
+}
+
+static void test_config_parse_ether_addrs_one(const char *rvalue, const struct ether_addr* list, size_t n) {
+ _cleanup_set_free_free_ Set *s = NULL;
+
+ assert_se(config_parse_ether_addrs("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &s, NULL) == 0);
+ assert_se(set_size(s) == n);
+
+ for (size_t m = 0; m < n; m++) {
+ _cleanup_free_ struct ether_addr *q = NULL;
+
+ assert_se(q = set_remove(s, &list[m]));
+ }
+
+ assert_se(set_size(s) == 0);
+}
+
+#define BYTES_0_128 "0:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:10:11:12:13:14:15:16:17:18:19:1a:1b:1c:1d:1e:1f:20:21:22:23:24:25:26:27:28:29:2a:2b:2c:2d:2e:2f:30:31:32:33:34:35:36:37:38:39:3a:3b:3c:3d:3e:3f:40:41:42:43:44:45:46:47:48:49:4a:4b:4c:4d:4e:4f:50:51:52:53:54:55:56:57:58:59:5a:5b:5c:5d:5e:5f:60:61:62:63:64:65:66:67:68:69:6a:6b:6c:6d:6e:6f:70:71:72:73:74:75:76:77:78:79:7a:7b:7c:7d:7e:7f:80"
+
+#define BYTES_1_128 {0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f,0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x3b,0x3c,0x3d,0x3e,0x3f,0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x4b,0x4c,0x4d,0x4e,0x4f,0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0x5b,0x5c,0x5d,0x5e,0x5f,0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x7b,0x7c,0x7d,0x7e,0x7f,0x80}
+
+TEST(config_parse_duid_rawdata) {
+ test_config_parse_duid_rawdata_one("", 0, &(DUID){});
+ test_config_parse_duid_rawdata_one("00:11:22:33:44:55:66:77", 0,
+ &(DUID){0, 8, {0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77}});
+ test_config_parse_duid_rawdata_one("00:11:22:", 0,
+ &(DUID){0, 3, {0x00,0x11,0x22}});
+ test_config_parse_duid_rawdata_one("000:11:22", 0, &(DUID){}); /* error, output is all zeros */
+ test_config_parse_duid_rawdata_one("00:111:22", 0, &(DUID){});
+ test_config_parse_duid_rawdata_one("0:1:2:3:4:5:6:7", 0,
+ &(DUID){0, 8, {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7}});
+ test_config_parse_duid_rawdata_one("11::", 0, &(DUID){0, 1, {0x11}}); /* FIXME: should this be an error? */
+ test_config_parse_duid_rawdata_one("abcdef", 0, &(DUID){});
+ test_config_parse_duid_rawdata_one(BYTES_0_128, 0, &(DUID){});
+ test_config_parse_duid_rawdata_one(&BYTES_0_128[2], 0, &(DUID){0, 128, BYTES_1_128});
+}
+
+TEST(config_parse_ether_addr) {
+ const struct ether_addr t[] = {
+ { .ether_addr_octet = { 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff } },
+ { .ether_addr_octet = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab } },
+ };
+
+ test_config_parse_ether_addr_one("", 0, NULL);
+ test_config_parse_ether_addr_one("no:ta:ma:ca:dd:re", 0, NULL);
+ test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:fx", 0, NULL);
+ test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:ff", 0, &t[0]);
+ test_config_parse_ether_addr_one(" aa:bb:cc:dd:ee:ff", 0, NULL);
+ test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:ff \t\n", 0, NULL);
+ test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:ff \t\nxxx", 0, NULL);
+ test_config_parse_ether_addr_one("aa:bb:cc: dd:ee:ff", 0, NULL);
+ test_config_parse_ether_addr_one("aa:bb:cc:d d:ee:ff", 0, NULL);
+ test_config_parse_ether_addr_one("aa:bb:cc:dd:ee", 0, NULL);
+ test_config_parse_ether_addr_one("9:aa:bb:cc:dd:ee:ff", 0, NULL);
+ test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:ff:gg", 0, NULL);
+ test_config_parse_ether_addr_one("aa:Bb:CC:dd:ee:ff", 0, &t[0]);
+ test_config_parse_ether_addr_one("01:23:45:67:89:aB", 0, &t[1]);
+ test_config_parse_ether_addr_one("1:23:45:67:89:aB", 0, &t[1]);
+ test_config_parse_ether_addr_one("aa-bb-cc-dd-ee-ff", 0, &t[0]);
+ test_config_parse_ether_addr_one("AA-BB-CC-DD-EE-FF", 0, &t[0]);
+ test_config_parse_ether_addr_one("01-23-45-67-89-ab", 0, &t[1]);
+ test_config_parse_ether_addr_one("aabb.ccdd.eeff", 0, &t[0]);
+ test_config_parse_ether_addr_one("0123.4567.89ab", 0, &t[1]);
+ test_config_parse_ether_addr_one("123.4567.89ab.", 0, NULL);
+ test_config_parse_ether_addr_one("aabbcc.ddeeff", 0, NULL);
+ test_config_parse_ether_addr_one("aabbccddeeff", 0, NULL);
+ test_config_parse_ether_addr_one("aabbccddee:ff", 0, NULL);
+ test_config_parse_ether_addr_one("012345.6789ab", 0, NULL);
+ test_config_parse_ether_addr_one("123.4567.89ab", 0, &t[1]);
+
+ test_config_parse_ether_addrs_one("", t, 0);
+ test_config_parse_ether_addrs_one("no:ta:ma:ca:dd:re", t, 0);
+ test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:fx", t, 0);
+ test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:ff", t, 1);
+ test_config_parse_ether_addrs_one(" aa:bb:cc:dd:ee:ff", t, 1);
+ test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:ff \t\n", t, 1);
+ test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:ff \t\nxxx", t, 1);
+ test_config_parse_ether_addrs_one("aa:bb:cc: dd:ee:ff", t, 0);
+ test_config_parse_ether_addrs_one("aa:bb:cc:d d:ee:ff", t, 0);
+ test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee", t, 0);
+ test_config_parse_ether_addrs_one("9:aa:bb:cc:dd:ee:ff", t, 0);
+ test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:ff:gg", t, 0);
+ test_config_parse_ether_addrs_one("aa:Bb:CC:dd:ee:ff", t, 1);
+ test_config_parse_ether_addrs_one("01:23:45:67:89:aB", &t[1], 1);
+ test_config_parse_ether_addrs_one("1:23:45:67:89:aB", &t[1], 1);
+ test_config_parse_ether_addrs_one("aa-bb-cc-dd-ee-ff", t, 1);
+ test_config_parse_ether_addrs_one("AA-BB-CC-DD-EE-FF", t, 1);
+ test_config_parse_ether_addrs_one("01-23-45-67-89-ab", &t[1], 1);
+ test_config_parse_ether_addrs_one("aabb.ccdd.eeff", t, 1);
+ test_config_parse_ether_addrs_one("0123.4567.89ab", &t[1], 1);
+ test_config_parse_ether_addrs_one("123.4567.89ab.", t, 0);
+ test_config_parse_ether_addrs_one("aabbcc.ddeeff", t, 0);
+ test_config_parse_ether_addrs_one("aabbccddeeff", t, 0);
+ test_config_parse_ether_addrs_one("aabbccddee:ff", t, 0);
+ test_config_parse_ether_addrs_one("012345.6789ab", t, 0);
+ test_config_parse_ether_addrs_one("123.4567.89ab", &t[1], 1);
+
+ test_config_parse_ether_addrs_one("123.4567.89ab aa:bb:cc:dd:ee:ff 01-23-45-67-89-ab aa:Bb:CC:dd:ee:ff", t, 2);
+ test_config_parse_ether_addrs_one("123.4567.89ab aa:bb:cc:dd:ee:fx hogehoge 01-23-45-67-89-ab aaaa aa:Bb:CC:dd:ee:ff", t, 2);
+}
+
+static void test_config_parse_address_one(const char *rvalue, int family, unsigned n_addresses, const union in_addr_union *u, unsigned char prefixlen) {
+ _cleanup_(manager_freep) Manager *manager = NULL;
+ _cleanup_(network_unrefp) Network *network = NULL;
+
+ assert_se(manager_new(&manager, /* test_mode = */ true) >= 0);
+ assert_se(network = new0(Network, 1));
+ network->n_ref = 1;
+ network->manager = manager;
+ assert_se(network->filename = strdup("hogehoge.network"));
+
+ assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "*", &network->match.ifname, network) == 0);
+ assert_se(config_parse_address("network", "filename", 1, "section", 1, "Address", 0, rvalue, network, network) == 0);
+ assert_se(ordered_hashmap_size(network->addresses_by_section) == 1);
+ assert_se(network_verify(network) >= 0);
+ assert_se(ordered_hashmap_size(network->addresses_by_section) == n_addresses);
+ if (n_addresses > 0) {
+ Address *a;
+
+ assert_se(a = ordered_hashmap_first(network->addresses_by_section));
+ assert_se(a->prefixlen == prefixlen);
+ assert_se(a->family == family);
+ assert_se(in_addr_equal(family, &a->in_addr, u));
+ /* TODO: check Address.in_addr and Address.broadcast */
+ }
+}
+
+TEST(config_parse_address) {
+ test_config_parse_address_one("", AF_INET, 0, NULL, 0);
+ test_config_parse_address_one("/", AF_INET, 0, NULL, 0);
+ test_config_parse_address_one("/8", AF_INET, 0, NULL, 0);
+ test_config_parse_address_one("1.2.3.4", AF_INET, 1, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 8);
+ test_config_parse_address_one("1.2.3.4/0", AF_INET, 1, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 0);
+ test_config_parse_address_one("1.2.3.4/1", AF_INET, 1, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 1);
+ test_config_parse_address_one("1.2.3.4/2", AF_INET, 1, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 2);
+ test_config_parse_address_one("1.2.3.4/32", AF_INET, 1, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 32);
+ test_config_parse_address_one("1.2.3.4/33", AF_INET, 0, NULL, 0);
+ test_config_parse_address_one("1.2.3.4/-1", AF_INET, 0, NULL, 0);
+
+ test_config_parse_address_one("", AF_INET6, 0, NULL, 0);
+ test_config_parse_address_one("/", AF_INET6, 0, NULL, 0);
+ test_config_parse_address_one("/8", AF_INET6, 0, NULL, 0);
+ test_config_parse_address_one("::1", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 0);
+ test_config_parse_address_one("::1/0", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 0);
+ test_config_parse_address_one("::1/1", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 1);
+ test_config_parse_address_one("::1/2", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 2);
+ test_config_parse_address_one("::1/32", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 32);
+ test_config_parse_address_one("::1/33", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 33);
+ test_config_parse_address_one("::1/64", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 64);
+ test_config_parse_address_one("::1/128", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 128);
+ test_config_parse_address_one("::1/129", AF_INET6, 0, NULL, 0);
+ test_config_parse_address_one("::1/-1", AF_INET6, 0, NULL, 0);
+}
+
+TEST(config_parse_match_ifnames) {
+ _cleanup_strv_free_ char **names = NULL;
+
+ assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "!hoge hogehoge foo", &names, NULL) == 0);
+ assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "!baz", &names, NULL) == 0);
+ assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "aaa bbb ccc", &names, NULL) == 0);
+
+ assert_se(strv_equal(names, STRV_MAKE("!hoge", "!hogehoge", "!foo", "!baz", "aaa", "bbb", "ccc")));
+}
+
+TEST(config_parse_match_strv) {
+ _cleanup_strv_free_ char **names = NULL;
+
+ assert_se(config_parse_match_strv("network", "filename", 1, "section", 1, "Name", 0, "!hoge hogehoge foo", &names, NULL) == 0);
+ assert_se(config_parse_match_strv("network", "filename", 1, "section", 1, "Name", 0, "!baz", &names, NULL) == 0);
+ assert_se(config_parse_match_strv("network", "filename", 1, "section", 1, "Name", 0,
+ "KEY=val \"KEY2=val with space\" \"KEY3=val with \\\"quotation\\\"\"", &names, NULL) == 0);
+
+ assert_se(strv_equal(names,
+ STRV_MAKE("!hoge",
+ "!hogehoge",
+ "!foo",
+ "!baz",
+ "KEY=val",
+ "KEY2=val with space",
+ "KEY3=val with \\quotation\\")));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/network/test-networkd-util.c b/src/network/test-networkd-util.c
new file mode 100644
index 0000000..f29ca2c
--- /dev/null
+++ b/src/network/test-networkd-util.c
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "networkd-util.h"
+#include "tests.h"
+
+TEST(network_config_state_to_string_alloc) {
+ for (unsigned i = 1; i <= NETWORK_CONFIG_STATE_REMOVING; i <<= 1) {
+ _cleanup_free_ char *x;
+
+ assert_se(network_config_state_to_string_alloc(i, &x) == 0);
+ log_debug("%u → %s", i, x);
+ }
+
+ _cleanup_free_ char *x;
+ assert_se(network_config_state_to_string_alloc(~0u, &x) == 0);
+ log_debug("%u → %s", ~0u, x);
+};
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/network/wait-online/link.c b/src/network/wait-online/link.c
new file mode 100644
index 0000000..e197e62
--- /dev/null
+++ b/src/network/wait-online/link.c
@@ -0,0 +1,179 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-network.h"
+
+#include "alloc-util.h"
+#include "hashmap.h"
+#include "link.h"
+#include "manager.h"
+#include "string-util.h"
+
+int link_new(Manager *m, Link **ret, int ifindex, const char *ifname) {
+ _cleanup_(link_freep) Link *l = NULL;
+ _cleanup_free_ char *n = NULL;
+ int r;
+
+ assert(m);
+ assert(ifindex > 0);
+ assert(ifname);
+
+ n = strdup(ifname);
+ if (!n)
+ return -ENOMEM;
+
+ l = new(Link, 1);
+ if (!l)
+ return -ENOMEM;
+
+ *l = (Link) {
+ .manager = m,
+ .ifname = TAKE_PTR(n),
+ .ifindex = ifindex,
+ .required_operstate = LINK_OPERSTATE_RANGE_DEFAULT,
+ };
+
+ r = hashmap_ensure_put(&m->links_by_index, NULL, INT_TO_PTR(ifindex), l);
+ if (r < 0)
+ return r;
+
+ r = hashmap_ensure_put(&m->links_by_name, &string_hash_ops, l->ifname, l);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = l;
+
+ TAKE_PTR(l);
+ return 0;
+}
+
+Link *link_free(Link *l) {
+
+ if (!l)
+ return NULL;
+
+ if (l->manager) {
+ hashmap_remove(l->manager->links_by_index, INT_TO_PTR(l->ifindex));
+ hashmap_remove(l->manager->links_by_name, l->ifname);
+ }
+
+ free(l->state);
+ free(l->ifname);
+ return mfree(l);
+ }
+
+int link_update_rtnl(Link *l, sd_netlink_message *m) {
+ const char *ifname;
+ int r;
+
+ assert(l);
+ assert(l->manager);
+ assert(m);
+
+ r = sd_rtnl_message_link_get_flags(m, &l->flags);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_read_string(m, IFLA_IFNAME, &ifname);
+ if (r < 0)
+ return r;
+
+ if (!streq(l->ifname, ifname)) {
+ char *new_ifname;
+
+ new_ifname = strdup(ifname);
+ if (!new_ifname)
+ return -ENOMEM;
+
+ assert_se(hashmap_remove(l->manager->links_by_name, l->ifname) == l);
+ free_and_replace(l->ifname, new_ifname);
+
+ r = hashmap_put(l->manager->links_by_name, l->ifname, l);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int link_update_monitor(Link *l) {
+ _cleanup_free_ char *required_operstate = NULL, *required_family = NULL,
+ *ipv4_address_state = NULL, *ipv6_address_state = NULL, *state = NULL;
+ int r, ret = 0;
+
+ assert(l);
+ assert(l->ifname);
+
+ r = sd_network_link_get_required_for_online(l->ifindex);
+ if (r < 0 && r != -ENODATA)
+ ret = log_link_debug_errno(l, r, "Failed to determine whether the link is required for online or not, "
+ "assuming required: %m");
+ l->required_for_online = r != 0;
+
+ r = sd_network_link_get_required_operstate_for_online(l->ifindex, &required_operstate);
+ if (r < 0 && r != -ENODATA)
+ ret = log_link_debug_errno(l, r, "Failed to get required operational state, ignoring: %m");
+
+ if (isempty(required_operstate))
+ l->required_operstate = LINK_OPERSTATE_RANGE_DEFAULT;
+ else {
+ r = parse_operational_state_range(required_operstate, &l->required_operstate);
+ if (r < 0)
+ ret = log_link_debug_errno(l, SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse required operational state, ignoring: %m");
+ }
+
+ r = network_link_get_operational_state(l->ifindex, &l->operational_state);
+ if (r < 0)
+ ret = log_link_debug_errno(l, r, "Failed to get operational state, ignoring: %m");
+
+ r = sd_network_link_get_required_family_for_online(l->ifindex, &required_family);
+ if (r < 0 && r != -ENODATA)
+ ret = log_link_debug_errno(l, r, "Failed to get required address family, ignoring: %m");
+
+ if (isempty(required_family))
+ l->required_family = ADDRESS_FAMILY_NO;
+ else {
+ AddressFamily f;
+
+ f = link_required_address_family_from_string(required_family);
+ if (f < 0)
+ ret = log_link_debug_errno(l, f, "Failed to parse required address family, ignoring: %m");
+ else
+ l->required_family = f;
+ }
+
+ r = sd_network_link_get_ipv4_address_state(l->ifindex, &ipv4_address_state);
+ if (r < 0)
+ ret = log_link_debug_errno(l, r, "Failed to get IPv4 address state, ignoring: %m");
+ else {
+ LinkAddressState s;
+
+ s = link_address_state_from_string(ipv4_address_state);
+ if (s < 0)
+ ret = log_link_debug_errno(l, s, "Failed to parse IPv4 address state, ignoring: %m");
+ else
+ l->ipv4_address_state = s;
+ }
+
+ r = sd_network_link_get_ipv6_address_state(l->ifindex, &ipv6_address_state);
+ if (r < 0)
+ ret = log_link_debug_errno(l, r, "Failed to get IPv6 address state, ignoring: %m");
+ else {
+ LinkAddressState s;
+
+ s = link_address_state_from_string(ipv6_address_state);
+ if (s < 0)
+ ret = log_link_debug_errno(l, s, "Failed to parse IPv6 address state, ignoring: %m");
+ else
+ l->ipv6_address_state = s;
+ }
+
+ r = sd_network_link_get_setup_state(l->ifindex, &state);
+ if (r < 0)
+ ret = log_link_debug_errno(l, r, "Failed to get setup state, ignoring: %m");
+ else
+ free_and_replace(l->state, state);
+
+ return ret;
+}
diff --git a/src/network/wait-online/link.h b/src/network/wait-online/link.h
new file mode 100644
index 0000000..3072a91
--- /dev/null
+++ b/src/network/wait-online/link.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-netlink.h"
+
+#include "log-link.h"
+#include "network-util.h"
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+
+struct Link {
+ Manager *manager;
+
+ int ifindex;
+ char *ifname;
+ unsigned flags;
+
+ bool required_for_online;
+ LinkOperationalStateRange required_operstate;
+ LinkOperationalState operational_state;
+ AddressFamily required_family;
+ LinkAddressState ipv4_address_state;
+ LinkAddressState ipv6_address_state;
+ char *state;
+};
+
+int link_new(Manager *m, Link **ret, int ifindex, const char *ifname);
+Link *link_free(Link *l);
+int link_update_rtnl(Link *l, sd_netlink_message *m);
+int link_update_monitor(Link *l);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free);
diff --git a/src/network/wait-online/manager.c b/src/network/wait-online/manager.c
new file mode 100644
index 0000000..cd4009e
--- /dev/null
+++ b/src/network/wait-online/manager.c
@@ -0,0 +1,417 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/ether.h>
+#include <linux/if.h>
+#include <fnmatch.h>
+
+#include "alloc-util.h"
+#include "link.h"
+#include "manager.h"
+#include "netlink-util.h"
+#include "strv.h"
+#include "time-util.h"
+#include "util.h"
+
+static bool manager_ignore_link(Manager *m, Link *link) {
+ assert(m);
+ assert(link);
+
+ /* always ignore the loopback interface */
+ if (link->flags & IFF_LOOPBACK)
+ return true;
+
+ /* if interfaces are given on the command line, ignore all others */
+ if (m->command_line_interfaces_by_name &&
+ !hashmap_contains(m->command_line_interfaces_by_name, link->ifname))
+ return true;
+
+ if (!link->required_for_online)
+ return true;
+
+ /* ignore interfaces we explicitly are asked to ignore */
+ return strv_fnmatch(m->ignored_interfaces, link->ifname);
+}
+
+static int manager_link_is_online(Manager *m, Link *l, LinkOperationalStateRange s) {
+ AddressFamily required_family;
+ bool needs_ipv4;
+ bool needs_ipv6;
+
+ assert(m);
+ assert(l);
+
+ /* This returns the following:
+ * -EAGAIN : not processed by udev
+ * -EBUSY : being processed by networkd
+ * -EADDRNOTAVAIL: requested conditions (operstate and/or addresses) are not satisfied
+ * false : unmanaged
+ * true : online */
+
+ if (!l->state || streq(l->state, "pending"))
+ /* If no state string exists, networkd (and possibly also udevd) has not detected the
+ * interface yet, that mean we cannot determine whether the interface is managed or
+ * not. Hence, return negative value.
+ * If the link is in pending state, then udevd has not processed the link, and networkd
+ * has not tried to find .network file for the link. Hence, return negative value. */
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EAGAIN),
+ "link has not yet been processed by udev: setup state is %s.",
+ strna(l->state));
+
+ if (streq(l->state, "unmanaged")) {
+ /* If the link is in unmanaged state, then ignore the interface unless the interface is
+ * specified in '--interface/-i' option. */
+ if (!hashmap_contains(m->command_line_interfaces_by_name, l->ifname)) {
+ log_link_debug(l, "link is not managed by networkd.");
+ return false;
+ }
+
+ } else if (!streq(l->state, "configured"))
+ /* If the link is in non-configured state, return negative value here. */
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EBUSY),
+ "link is being processed by networkd: setup state is %s.",
+ l->state);
+
+ if (s.min < 0)
+ s.min = m->required_operstate.min >= 0 ? m->required_operstate.min
+ : l->required_operstate.min;
+
+ if (s.max < 0)
+ s.max = m->required_operstate.max >= 0 ? m->required_operstate.max
+ : l->required_operstate.max;
+
+ if (l->operational_state < s.min || l->operational_state > s.max)
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL),
+ "Operational state '%s' is not in range ['%s':'%s']",
+ link_operstate_to_string(l->operational_state),
+ link_operstate_to_string(s.min), link_operstate_to_string(s.max));
+
+ required_family = m->required_family > 0 ? m->required_family : l->required_family;
+ needs_ipv4 = required_family & ADDRESS_FAMILY_IPV4;
+ needs_ipv6 = required_family & ADDRESS_FAMILY_IPV6;
+
+ if (s.min < LINK_OPERSTATE_ROUTABLE) {
+ if (needs_ipv4 && l->ipv4_address_state < LINK_ADDRESS_STATE_DEGRADED)
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL),
+ "No routable or link-local IPv4 address is configured.");
+
+ if (needs_ipv6 && l->ipv6_address_state < LINK_ADDRESS_STATE_DEGRADED)
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL),
+ "No routable or link-local IPv6 address is configured.");
+ } else {
+ if (needs_ipv4 && l->ipv4_address_state < LINK_ADDRESS_STATE_ROUTABLE)
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL),
+ "No routable IPv4 address is configured.");
+
+ if (needs_ipv6 && l->ipv6_address_state < LINK_ADDRESS_STATE_ROUTABLE)
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL),
+ "No routable IPv6 address is configured.");
+ }
+
+ log_link_debug(l, "link is configured by networkd and online.");
+ return true;
+}
+
+bool manager_configured(Manager *m) {
+ Link *l;
+ int r;
+
+ if (!hashmap_isempty(m->command_line_interfaces_by_name)) {
+ LinkOperationalStateRange *range;
+ const char *ifname;
+
+ /* wait for all the links given on the command line to appear */
+ HASHMAP_FOREACH_KEY(range, ifname, m->command_line_interfaces_by_name) {
+
+ l = hashmap_get(m->links_by_name, ifname);
+ if (!l) {
+ if (range->min == LINK_OPERSTATE_MISSING) {
+ if (m->any)
+ return true;
+ } else {
+ log_debug("still waiting for %s", ifname);
+ if (!m->any)
+ return false;
+ }
+ continue;
+ }
+
+ r = manager_link_is_online(m, l, *range);
+ if (r <= 0 && !m->any)
+ return false;
+ if (r > 0 && m->any)
+ return true;
+ }
+
+ /* With '--any' : no interface is ready → return false
+ * Without '--any': all interfaces are ready → return true */
+ return !m->any;
+ }
+
+ /* wait for all links networkd manages */
+ bool has_online = false;
+ HASHMAP_FOREACH(l, m->links_by_index) {
+ if (manager_ignore_link(m, l)) {
+ log_link_debug(l, "link is ignored");
+ continue;
+ }
+
+ r = manager_link_is_online(m, l,
+ (LinkOperationalStateRange) { _LINK_OPERSTATE_INVALID,
+ _LINK_OPERSTATE_INVALID });
+ if (r < 0 && !m->any) /* Unlike the above loop, unmanaged interfaces are ignored here. */
+ return false;
+ if (r > 0) {
+ if (m->any)
+ return true;
+ has_online = true;
+ }
+ }
+
+ /* With '--any' : no interface is ready → return false
+ * Without '--any': all interfaces are ready or unmanaged
+ *
+ * In this stage, drivers for interfaces may not be loaded yet, and there may be only lo.
+ * To avoid that wait-online exits earlier than that drivers are loaded, let's request at least one
+ * managed online interface exists. See issue #27822. */
+ return !m->any && has_online;
+}
+
+static int manager_process_link(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+ uint16_t type;
+ Link *l;
+ const char *ifname;
+ int ifindex, r;
+
+ assert(rtnl);
+ assert(mm);
+
+ r = sd_netlink_message_get_type(mm, &type);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: Could not get message type, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_rtnl_message_link_get_ifindex(mm, &ifindex);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: Could not get ifindex from link, ignoring: %m");
+ return 0;
+ } else if (ifindex <= 0) {
+ log_warning("rtnl: received link message with invalid ifindex %d, ignoring", ifindex);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string(mm, IFLA_IFNAME, &ifname);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: Received link message without ifname, ignoring: %m");
+ return 0;
+ }
+
+ l = hashmap_get(m->links_by_index, INT_TO_PTR(ifindex));
+
+ switch (type) {
+
+ case RTM_NEWLINK:
+ if (!l) {
+ log_debug("Found link %i", ifindex);
+
+ r = link_new(m, &l, ifindex, ifname);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create link object: %m");
+ }
+
+ r = link_update_rtnl(l, mm);
+ if (r < 0)
+ log_link_warning_errno(l, r, "Failed to process RTNL link message, ignoring: %m");
+
+ r = link_update_monitor(l);
+ if (r < 0)
+ log_link_full_errno(l, IN_SET(r, -ENODATA, -ENOENT) ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to update link state, ignoring: %m");
+
+ break;
+
+ case RTM_DELLINK:
+ if (l) {
+ log_link_debug(l, "Removing link");
+ link_free(l);
+ }
+
+ break;
+ }
+
+ return 0;
+}
+
+static int on_rtnl_event(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) {
+ Manager *m = userdata;
+ int r;
+
+ r = manager_process_link(rtnl, mm, m);
+ if (r < 0)
+ return r;
+
+ if (manager_configured(m))
+ sd_event_exit(m->event, 0);
+
+ return 1;
+}
+
+static int manager_rtnl_listen(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL;
+ int r;
+
+ assert(m);
+
+ /* First, subscribe to interfaces coming and going */
+ r = sd_netlink_open(&m->rtnl);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_attach_event(m->rtnl, m->event, 0);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_add_match(m->rtnl, NULL, RTM_NEWLINK, on_rtnl_event, NULL, m, "wait-online-on-NEWLINK");
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_add_match(m->rtnl, NULL, RTM_DELLINK, on_rtnl_event, NULL, m, "wait-online-on-DELLINK");
+ if (r < 0)
+ return r;
+
+ /* Then, enumerate all links */
+ r = sd_rtnl_message_new_link(m->rtnl, &req, RTM_GETLINK, 0);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_set_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(m->rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) {
+ r = manager_process_link(m->rtnl, i, m);
+ if (r < 0)
+ return r;
+ }
+
+ return r;
+}
+
+static int on_network_event(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+ Link *l;
+ int r;
+
+ sd_network_monitor_flush(m->network_monitor);
+
+ HASHMAP_FOREACH(l, m->links_by_index) {
+ r = link_update_monitor(l);
+ if (r < 0)
+ log_link_full_errno(l, IN_SET(r, -ENODATA, -ENOENT) ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to update link state, ignoring: %m");
+ }
+
+ if (manager_configured(m))
+ sd_event_exit(m->event, 0);
+
+ return 0;
+}
+
+static int manager_network_monitor_listen(Manager *m) {
+ int r, fd, events;
+
+ assert(m);
+
+ r = sd_network_monitor_new(&m->network_monitor, NULL);
+ if (r < 0)
+ return r;
+
+ fd = sd_network_monitor_get_fd(m->network_monitor);
+ if (fd < 0)
+ return fd;
+
+ events = sd_network_monitor_get_events(m->network_monitor);
+ if (events < 0)
+ return events;
+
+ r = sd_event_add_io(m->event, &m->network_monitor_event_source,
+ fd, events, &on_network_event, m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int manager_new(Manager **ret,
+ Hashmap *command_line_interfaces_by_name,
+ char **ignored_interfaces,
+ LinkOperationalStateRange required_operstate,
+ AddressFamily required_family,
+ bool any,
+ usec_t timeout) {
+
+ _cleanup_(manager_freep) Manager *m = NULL;
+ int r;
+
+ assert(ret);
+
+ m = new(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ *m = (Manager) {
+ .command_line_interfaces_by_name = command_line_interfaces_by_name,
+ .ignored_interfaces = ignored_interfaces,
+ .required_operstate = required_operstate,
+ .required_family = required_family,
+ .any = any,
+ };
+
+ r = sd_event_default(&m->event);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL);
+ (void) sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL);
+
+ if (timeout > 0) {
+ r = sd_event_add_time_relative(m->event, NULL, CLOCK_BOOTTIME, timeout, 0, NULL, INT_TO_PTR(-ETIMEDOUT));
+ if (r < 0 && r != -EOVERFLOW)
+ return r;
+ }
+
+ sd_event_set_watchdog(m->event, true);
+
+ r = manager_network_monitor_listen(m);
+ if (r < 0)
+ return r;
+
+ r = manager_rtnl_listen(m);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(m);
+
+ return 0;
+}
+
+Manager* manager_free(Manager *m) {
+ if (!m)
+ return NULL;
+
+ hashmap_free_with_destructor(m->links_by_index, link_free);
+ hashmap_free(m->links_by_name);
+
+ sd_event_source_unref(m->network_monitor_event_source);
+ sd_network_monitor_unref(m->network_monitor);
+ sd_event_source_unref(m->rtnl_event_source);
+ sd_netlink_unref(m->rtnl);
+ sd_event_unref(m->event);
+
+ return mfree(m);
+}
diff --git a/src/network/wait-online/manager.h b/src/network/wait-online/manager.h
new file mode 100644
index 0000000..01ad18f
--- /dev/null
+++ b/src/network/wait-online/manager.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-event.h"
+#include "sd-netlink.h"
+#include "sd-network.h"
+
+#include "hashmap.h"
+#include "network-util.h"
+#include "time-util.h"
+
+typedef struct Manager Manager;
+typedef struct Link Link;
+
+struct Manager {
+ Hashmap *links_by_index;
+ Hashmap *links_by_name;
+
+ /* Do not free the two members below. */
+ Hashmap *command_line_interfaces_by_name;
+ char **ignored_interfaces;
+
+ LinkOperationalStateRange required_operstate;
+ AddressFamily required_family;
+ bool any;
+
+ sd_netlink *rtnl;
+ sd_event_source *rtnl_event_source;
+
+ sd_network_monitor *network_monitor;
+ sd_event_source *network_monitor_event_source;
+
+ sd_event *event;
+};
+
+Manager* manager_free(Manager *m);
+int manager_new(Manager **ret, Hashmap *command_line_interfaces_by_name, char **ignored_interfaces,
+ LinkOperationalStateRange required_operstate,
+ AddressFamily required_family,
+ bool any, usec_t timeout);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
+
+bool manager_configured(Manager *m);
diff --git a/src/network/wait-online/wait-online.c b/src/network/wait-online/wait-online.c
new file mode 100644
index 0000000..a679b85
--- /dev/null
+++ b/src/network/wait-online/wait-online.c
@@ -0,0 +1,237 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "sd-daemon.h"
+
+#include "daemon-util.h"
+#include "main-func.h"
+#include "manager.h"
+#include "pretty-print.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "strv.h"
+
+static bool arg_quiet = false;
+static usec_t arg_timeout = 120 * USEC_PER_SEC;
+static Hashmap *arg_interfaces = NULL;
+static char **arg_ignore = NULL;
+static LinkOperationalStateRange arg_required_operstate = { _LINK_OPERSTATE_INVALID, _LINK_OPERSTATE_INVALID };
+static AddressFamily arg_required_family = ADDRESS_FAMILY_NO;
+static bool arg_any = false;
+
+STATIC_DESTRUCTOR_REGISTER(arg_interfaces, hashmap_free_free_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_ignore, strv_freep);
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-networkd-wait-online.service", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...]\n\n"
+ "Block until network is configured.\n\n"
+ " -h --help Show this help\n"
+ " --version Print version string\n"
+ " -q --quiet Do not show status information\n"
+ " -i --interface=INTERFACE[:MIN_OPERSTATE[:MAX_OPERSTATE]]\n"
+ " Block until at least these interfaces have appeared\n"
+ " --ignore=INTERFACE Don't take these interfaces into account\n"
+ " -o --operational-state=MIN_OPERSTATE[:MAX_OPERSTATE]\n"
+ " Required operational state\n"
+ " -4 --ipv4 Requires at least one IPv4 address\n"
+ " -6 --ipv6 Requires at least one IPv6 address\n"
+ " --any Wait until at least one of the interfaces is online\n"
+ " --timeout=SECS Maximum time to wait for network connectivity\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ link);
+
+ return 0;
+}
+
+static int parse_interface_with_operstate_range(const char *str) {
+ _cleanup_free_ char *ifname = NULL;
+ _cleanup_free_ LinkOperationalStateRange *range = NULL;
+ const char *p;
+ int r;
+
+ assert(str);
+
+ range = new(LinkOperationalStateRange, 1);
+ if (!range)
+ return log_oom();
+
+ p = strchr(str, ':');
+ if (p) {
+ r = parse_operational_state_range(p + 1, range);
+ if (r < 0)
+ log_error_errno(r, "Invalid operational state range '%s'", p + 1);
+
+ ifname = strndup(optarg, p - optarg);
+ } else {
+ range->min = _LINK_OPERSTATE_INVALID;
+ range->max = _LINK_OPERSTATE_INVALID;
+ ifname = strdup(str);
+ }
+ if (!ifname)
+ return log_oom();
+
+ if (!ifname_valid(ifname))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid interface name '%s'", ifname);
+
+ r = hashmap_ensure_put(&arg_interfaces, &string_hash_ops, ifname, TAKE_PTR(range));
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_error_errno(r, "Failed to store interface name: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Interface name %s is already specified", ifname);
+
+ TAKE_PTR(ifname);
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_IGNORE,
+ ARG_ANY,
+ ARG_TIMEOUT,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "quiet", no_argument, NULL, 'q' },
+ { "interface", required_argument, NULL, 'i' },
+ { "ignore", required_argument, NULL, ARG_IGNORE },
+ { "operational-state", required_argument, NULL, 'o' },
+ { "ipv4", no_argument, NULL, '4' },
+ { "ipv6", no_argument, NULL, '6' },
+ { "any", no_argument, NULL, ARG_ANY },
+ { "timeout", required_argument, NULL, ARG_TIMEOUT },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hi:qo:46", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ help();
+ return 0;
+
+ case 'q':
+ arg_quiet = true;
+ break;
+
+ case ARG_VERSION:
+ return version();
+
+ case 'i':
+ r = parse_interface_with_operstate_range(optarg);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_IGNORE:
+ if (strv_extend(&arg_ignore, optarg) < 0)
+ return log_oom();
+
+ break;
+
+ case 'o': {
+ LinkOperationalStateRange range;
+
+ r = parse_operational_state_range(optarg, &range);
+ if (r < 0)
+ return log_error_errno(r, "Invalid operational state range '%s'", optarg);
+
+ arg_required_operstate = range;
+
+ break;
+ }
+
+ case '4':
+ arg_required_family |= ADDRESS_FAMILY_IPV4;
+ break;
+
+ case '6':
+ arg_required_family |= ADDRESS_FAMILY_IPV6;
+ break;
+
+ case ARG_ANY:
+ arg_any = true;
+ break;
+
+ case ARG_TIMEOUT:
+ r = parse_sec(optarg, &arg_timeout);
+ if (r < 0)
+ return r;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _unused_ _cleanup_(notify_on_cleanup) const char *notify_message = NULL;
+ int r;
+
+ log_setup();
+
+ umask(0022);
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (arg_quiet)
+ log_set_max_level(LOG_ERR);
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+
+ r = manager_new(&m, arg_interfaces, arg_ignore, arg_required_operstate, arg_required_family, arg_any, arg_timeout);
+ if (r < 0)
+ return log_error_errno(r, "Could not create manager: %m");
+
+ if (manager_configured(m))
+ goto success;
+
+ notify_message = notify_start("READY=1\n"
+ "STATUS=Waiting for network connections...",
+ "STATUS=Failed to wait for network connectivity...");
+
+ r = sd_event_loop(m->event);
+ if (r == -ETIMEDOUT)
+ return log_error_errno(r, "Timeout occurred while waiting for network connectivity.");
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+success:
+ notify_message = "STATUS=All interfaces configured...";
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);