summaryrefslogtreecommitdiffstats
path: root/src/network
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
commit2cb7e0aaedad73b076ea18c6900b0e86c5760d79 (patch)
treeda68ca54bb79f4080079bf0828acda937593a4e1 /src/network
parentInitial commit. (diff)
downloadsystemd-upstream.tar.xz
systemd-upstream.zip
Adding upstream version 247.3.upstream/247.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/network/fuzz-netdev-parser.c25
-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.c206
-rw-r--r--src/network/generator/network-generator.c1233
-rw-r--r--src/network/generator/network-generator.h108
-rw-r--r--src/network/generator/test-network-generator.c438
-rw-r--r--src/network/meson.build303
-rw-r--r--src/network/netdev/bareudp.c138
-rw-r--r--src/network/netdev/bareudp.h34
-rw-r--r--src/network/netdev/bond.c527
-rw-r--r--src/network/netdev/bond.h62
-rw-r--r--src/network/netdev/bridge.c368
-rw-r--r--src/network/netdev/bridge.h47
-rw-r--r--src/network/netdev/dummy.c10
-rw-r--r--src/network/netdev/dummy.h11
-rw-r--r--src/network/netdev/fou-tunnel.c279
-rw-r--r--src/network/netdev/fou-tunnel.h42
-rw-r--r--src/network/netdev/geneve.c356
-rw-r--r--src/network/netdev/geneve.h52
-rw-r--r--src/network/netdev/ifb.c11
-rw-r--r--src/network/netdev/ifb.h13
-rw-r--r--src/network/netdev/ipvlan.c91
-rw-r--r--src/network/netdev/ipvlan.h25
-rw-r--r--src/network/netdev/l2tp-tunnel.c728
-rw-r--r--src/network/netdev/l2tp-tunnel.h78
-rw-r--r--src/network/netdev/macsec.c1252
-rw-r--r--src/network/netdev/macsec.h87
-rw-r--r--src/network/netdev/macvlan.c105
-rw-r--r--src/network/netdev/macvlan.h22
-rw-r--r--src/network/netdev/netdev-gperf.gperf232
-rw-r--r--src/network/netdev/netdev.c868
-rw-r--r--src/network/netdev/netdev.h244
-rw-r--r--src/network/netdev/netdevsim.c10
-rw-r--r--src/network/netdev/netdevsim.h13
-rw-r--r--src/network/netdev/nlmon.c22
-rw-r--r--src/network/netdev/nlmon.h14
-rw-r--r--src/network/netdev/tunnel.c903
-rw-r--r--src/network/netdev/tunnel.h92
-rw-r--r--src/network/netdev/tuntap.c164
-rw-r--r--src/network/netdev/tuntap.h21
-rw-r--r--src/network/netdev/vcan.c10
-rw-r--r--src/network/netdev/vcan.h17
-rw-r--r--src/network/netdev/veth.c95
-rw-r--r--src/network/netdev/veth.h16
-rw-r--r--src/network/netdev/vlan.c92
-rw-r--r--src/network/netdev/vlan.h20
-rw-r--r--src/network/netdev/vrf.c32
-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.c390
-rw-r--r--src/network/netdev/vxlan.h74
-rw-r--r--src/network/netdev/wireguard.c946
-rw-r--r--src/network/netdev/wireguard.h70
-rw-r--r--src/network/netdev/xfrm.c33
-rw-r--r--src/network/netdev/xfrm.h14
-rw-r--r--src/network/networkctl.c2830
-rw-r--r--src/network/networkd-address-label.c242
-rw-r--r--src/network/networkd-address-label.h29
-rw-r--r--src/network/networkd-address-pool.c190
-rw-r--r--src/network/networkd-address-pool.h17
-rw-r--r--src/network/networkd-address.c1922
-rw-r--r--src/network/networkd-address.h93
-rw-r--r--src/network/networkd-brvlan.c283
-rw-r--r--src/network/networkd-brvlan.h19
-rw-r--r--src/network/networkd-can.c315
-rw-r--r--src/network/networkd-can.h10
-rw-r--r--src/network/networkd-conf.c191
-rw-r--r--src/network/networkd-conf.h17
-rw-r--r--src/network/networkd-dhcp-common.c935
-rw-r--r--src/network/networkd-dhcp-common.h72
-rw-r--r--src/network/networkd-dhcp-server-bus.c110
-rw-r--r--src/network/networkd-dhcp-server-bus.h9
-rw-r--r--src/network/networkd-dhcp-server.c439
-rw-r--r--src/network/networkd-dhcp-server.h12
-rw-r--r--src/network/networkd-dhcp4.c1761
-rw-r--r--src/network/networkd-dhcp4.h30
-rw-r--r--src/network/networkd-dhcp6.c1719
-rw-r--r--src/network/networkd-dhcp6.h45
-rw-r--r--src/network/networkd-fdb.c409
-rw-r--r--src/network/networkd-fdb.h52
-rw-r--r--src/network/networkd-gperf.gperf25
-rw-r--r--src/network/networkd-ipv4ll.c313
-rw-r--r--src/network/networkd-ipv4ll.h15
-rw-r--r--src/network/networkd-ipv6-proxy-ndp.c164
-rw-r--r--src/network/networkd-ipv6-proxy-ndp.h10
-rw-r--r--src/network/networkd-link-bus.c816
-rw-r--r--src/network/networkd-link-bus.h36
-rw-r--r--src/network/networkd-link.c3263
-rw-r--r--src/network/networkd-link.h249
-rw-r--r--src/network/networkd-lldp-rx.c205
-rw-r--r--src/network/networkd-lldp-rx.h23
-rw-r--r--src/network/networkd-lldp-tx.c493
-rw-r--r--src/network/networkd-lldp-tx.h23
-rw-r--r--src/network/networkd-manager-bus.c274
-rw-r--r--src/network/networkd-manager-bus.h10
-rw-r--r--src/network/networkd-manager.c1254
-rw-r--r--src/network/networkd-manager.h97
-rw-r--r--src/network/networkd-mdb.c365
-rw-r--r--src/network/networkd-mdb.h29
-rw-r--r--src/network/networkd-ndisc.c1516
-rw-r--r--src/network/networkd-ndisc.h85
-rw-r--r--src/network/networkd-neighbor.c725
-rw-r--r--src/network/networkd-neighbor.h45
-rw-r--r--src/network/networkd-network-bus.c137
-rw-r--r--src/network/networkd-network-bus.h11
-rw-r--r--src/network/networkd-network-gperf.gperf482
-rw-r--r--src/network/networkd-network.c1238
-rw-r--r--src/network/networkd-network.h340
-rw-r--r--src/network/networkd-nexthop.c534
-rw-r--r--src/network/networkd-nexthop.h41
-rw-r--r--src/network/networkd-radv.c999
-rw-r--r--src/network/networkd-radv.h70
-rw-r--r--src/network/networkd-route.c2537
-rw-r--r--src/network/networkd-route.h102
-rw-r--r--src/network/networkd-routing-policy-rule.c1810
-rw-r--r--src/network/networkd-routing-policy-rule.h73
-rw-r--r--src/network/networkd-speed-meter.c113
-rw-r--r--src/network/networkd-speed-meter.h12
-rw-r--r--src/network/networkd-sriov.c532
-rw-r--r--src/network/networkd-sriov.h46
-rw-r--r--src/network/networkd-sysctl.c288
-rw-r--r--src/network/networkd-sysctl.h25
-rw-r--r--src/network/networkd-util.c165
-rw-r--r--src/network/networkd-util.h83
-rw-r--r--src/network/networkd-wifi.c62
-rw-r--r--src/network/networkd-wifi.h8
-rw-r--r--src/network/networkd.c110
-rw-r--r--src/network/networkd.conf21
-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.pkla4
-rw-r--r--src/network/systemd-networkd.rules10
-rw-r--r--src/network/tc/cake.c163
-rw-r--r--src/network/tc/cake.h20
-rw-r--r--src/network/tc/codel.c255
-rw-r--r--src/network/tc/codel.h24
-rw-r--r--src/network/tc/drr.c109
-rw-r--r--src/network/tc/drr.h23
-rw-r--r--src/network/tc/ets.c344
-rw-r--r--src/network/tc/ets.h25
-rw-r--r--src/network/tc/fifo.c187
-rw-r--r--src/network/tc/fifo.h25
-rw-r--r--src/network/tc/fq-codel.c355
-rw-r--r--src/network/tc/fq-codel.h28
-rw-r--r--src/network/tc/fq-pie.c103
-rw-r--r--src/network/tc/fq-pie.h17
-rw-r--r--src/network/tc/fq.c420
-rw-r--r--src/network/tc/fq.h29
-rw-r--r--src/network/tc/gred.c196
-rw-r--r--src/network/tc/gred.h20
-rw-r--r--src/network/tc/hhf.c98
-rw-r--r--src/network/tc/hhf.h17
-rw-r--r--src/network/tc/htb.c489
-rw-r--r--src/network/tc/htb.h39
-rw-r--r--src/network/tc/netem.c236
-rw-r--r--src/network/tc/netem.h25
-rw-r--r--src/network/tc/pie.c97
-rw-r--r--src/network/tc/pie.h17
-rw-r--r--src/network/tc/qdisc.c381
-rw-r--r--src/network/tc/qdisc.h107
-rw-r--r--src/network/tc/qfq.c178
-rw-r--r--src/network/tc/qfq.h26
-rw-r--r--src/network/tc/sfb.c108
-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.c346
-rw-r--r--src/network/tc/tbf.h26
-rw-r--r--src/network/tc/tc-util.c132
-rw-r--r--src/network/tc/tc-util.h14
-rw-r--r--src/network/tc/tc.c81
-rw-r--r--src/network/tc/tc.h32
-rw-r--r--src/network/tc/tclass.c289
-rw-r--r--src/network/tc/tclass.h71
-rw-r--r--src/network/tc/teql.c91
-rw-r--r--src/network/tc/teql.h16
-rw-r--r--src/network/test-network-tables.c49
-rw-r--r--src/network/test-network.c258
-rw-r--r--src/network/test-networkd-conf.c260
-rw-r--r--src/network/test-routing-policy-rule.c90
-rw-r--r--src/network/wait-online/link.c153
-rw-r--r--src/network/wait-online/link.h30
-rw-r--r--src/network/wait-online/manager.c369
-rw-r--r--src/network/wait-online/manager.h42
-rw-r--r--src/network/wait-online/wait-online.c224
188 files changed, 49904 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..ddabe1c
--- /dev/null
+++ b/src/network/fuzz-netdev-parser.c
@@ -0,0 +1,25 @@
+/* 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 (!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) >= 0);
+ (void) netdev_load_one(manager, netdev_config);
+ return 0;
+}
diff --git a/src/network/fuzz-network-parser.c b/src/network/fuzz-network-parser.c
new file mode 100644
index 0000000..1292eba
--- /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 (size > 65535)
+ 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) >= 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..0824b19
--- /dev/null
+++ b/src/network/fuzz-network-parser.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65535
diff --git a/src/network/generator/main.c b/src/network/generator/main.c
new file mode 100644
index 0000000..f9cace7
--- /dev/null
+++ b/src/network/generator/main.c
@@ -0,0 +1,206 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.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);
+
+ r = asprintf(&filename, "90-%s.link",
+ link->ifname);
+ if (r < 0)
+ 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_name) {
+ 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("Unhandled option");
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(context_clear) Context context = {};
+ int i, r;
+
+ 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 (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..2fa21a0
--- /dev/null
+++ b/src/network/generator/network-generator.c
@@ -0,0 +1,1233 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "ether-addr-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "hostname-util.h"
+#include "log.h"
+#include "macro.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}
+ ip=<interface>:{dhcp|on|any|dhcp6|auto6}[:[<mtu>][:<macaddr>]]
+ ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[<mtu>][:<macaddr>]]
+ ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[<dns1>][:<dns2>]]
+ rd.route=<net>/<netmask>:<gateway>[:<interface>]
+ nameserver=<IP> [nameserver=<IP> ...]
+ rd.peerdns=0
+
+ # .link
+ ifname=<interface>:<MAC>
+
+ # .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_DHCP] = "dhcp",
+ [DHCP_TYPE_DHCP6] = "dhcp6",
+ [DHCP_TYPE_AUTO6] = "auto6",
+ [DHCP_TYPE_EITHER6] = "either6",
+ [DHCP_TYPE_IBFT] = "ibft",
+};
+
+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_DHCP] = "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",
+};
+
+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);
+
+ address = new(Address, 1);
+ if (!address)
+ return -ENOMEM;
+
+ *address = (Address) {
+ .family = family,
+ .prefixlen = prefixlen,
+ .address = *addr,
+ .peer = *peer,
+ };
+
+ 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);
+
+ route = new(Route, 1);
+ if (!route)
+ return -ENOMEM;
+
+ *route = (Route) {
+ .family = family,
+ .prefixlen = prefixlen,
+ .dest = dest ? *dest : IN_ADDR_NULL,
+ .gateway = *gateway,
+ };
+
+ 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_allocated(&context->networks_by_name, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(context->networks_by_name, 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);
+
+ 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_allocated(&context->netdevs_by_name, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(context->netdevs_by_name, 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->ifname);
+ return mfree(link);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free);
+
+static int link_new(Context *context, const char *name, struct ether_addr *mac, Link **ret) {
+ _cleanup_(link_freep) Link *link = NULL;
+ _cleanup_free_ char *ifname = NULL;
+ int r;
+
+ assert(context);
+
+ if (!ifname_valid(name))
+ return -EINVAL;
+
+ ifname = strdup(name);
+ if (!ifname)
+ return -ENOMEM;
+
+ link = new(Link, 1);
+ if (!link)
+ return -ENOMEM;
+
+ *link = (Link) {
+ .ifname = TAKE_PTR(ifname),
+ .mac = *mac,
+ };
+
+ r = hashmap_ensure_allocated(&context->links_by_name, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(context->links_by_name, link->ifname, link);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = link;
+
+ TAKE_PTR(link);
+ return 0;
+}
+
+Link *link_get(Context *context, const char *ifname) {
+ return hashmap_get(context->links_by_name, ifname);
+}
+
+static int network_set_dhcp_type(Context *context, const char *ifname, const char *dhcp_type) {
+ Network *network;
+ DHCPType t;
+ int r;
+
+ t = dracut_dhcp_type_from_string(dhcp_type);
+ if (t < 0)
+ return -EINVAL;
+
+ 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;
+
+ 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, int family, const char *mtu) {
+ Network *network;
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return parse_mtu(family, mtu, &network->mtu);
+}
+
+static int network_set_mac_address(Context *context, const char *ifname, const char *mac) {
+ Network *network;
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return ether_addr_from_string(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;
+
+ if (in_addr_is_null(family, addr) != 0)
+ 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;
+
+ if (in_addr_is_null(family, gateway) != 0)
+ 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, const char *dns) {
+ union in_addr_union a;
+ Network *network;
+ int family, r;
+
+ r = in_addr_from_string_auto(dns, &family, &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;
+
+ 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;
+
+ 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;
+
+ 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;
+
+ 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, int family, const char *value) {
+ const char *mtu, *p;
+ int r;
+
+ /* [<mtu>][:<macaddr>] */
+
+ p = strchr(value, ':');
+ if (!p)
+ mtu = value;
+ else
+ mtu = strndupa(value, p - value);
+
+ r = network_set_mtu(context, ifname, family, mtu);
+ if (r < 0)
+ return r;
+
+ if (!p)
+ 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 = *value, *q, *buf;
+ int r;
+
+ 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(p + 1, q - p - 1);
+ p = q + 2;
+ } else {
+ q = strchr(p, ':');
+ if (!q)
+ return -EINVAL;
+
+ buf = strndupa(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;
+
+ 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(*value, p - *value);
+ r = safe_atou8(q, ret);
+ if (r < 0)
+ return r;
+
+ *value = p + 1;
+ }
+
+ 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, *dns, *p;
+ unsigned char prefixlen;
+ int r;
+
+ /* ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[<mtu>][:<macaddr>]]
+ * ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[<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(value, p - value);
+ if (!hostname_is_valid(hostname, false))
+ return -EINVAL;
+ }
+
+ value = p + 1;
+
+ /* ifname */
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ ifname = strndupa(value, p - value);
+
+ value = p + 1;
+
+ /* dhcp_type */
+ p = strchr(value, ':');
+ if (!p)
+ dhcp_type = value;
+ else
+ dhcp_type = strndupa(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, AF_UNSPEC, p + 1);
+ if (r >= 0)
+ return 0;
+
+ /* Next, try [<dns1>][:<dns2>] */
+ value = p + 1;
+ p = strchr(value, ':');
+ if (!p) {
+ r = network_set_dns(context, ifname, value);
+ if (r < 0)
+ return r;
+ } else {
+ dns = strndupa(value, p - value);
+ r = network_set_dns(context, ifname, dns);
+ if (r < 0)
+ return r;
+ r = network_set_dns(context, ifname, p + 1);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int parse_cmdline_ip_interface(Context *context, const char *value) {
+ const char *ifname, *dhcp_type, *p;
+ int r;
+
+ /* ip=<interface>:{dhcp|on|any|dhcp6|auto6}[:[<mtu>][:<macaddr>]] */
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ ifname = strndupa(value, p - value);
+
+ value = p + 1;
+ p = strchr(value, ':');
+ if (!p)
+ dhcp_type = value;
+ else
+ dhcp_type = strndupa(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, AF_UNSPEC, p + 1);
+}
+
+static int parse_cmdline_ip(Context *context, const char *key, const char *value) {
+ const char *p;
+ int r;
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ /* ip={dhcp|on|any|dhcp6|auto6|either6} */
+ 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;
+
+ /* 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(value + 1, p - value - 1);
+ value = p + 2;
+ family = AF_INET6;
+ } else {
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ buf = strndupa(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) {
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ return network_set_dns(context, "", value);
+}
+
+static int parse_cmdline_rd_peerdns(Context *context, const char *key, const char *value) {
+ int r;
+
+ 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;
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa(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;
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa(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;
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa(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(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 ether_addr mac;
+ const char *name, *p;
+ int r;
+
+ /* ifname=<interface>:<MAC> */
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa(value, p - value);
+
+ r = ether_addr_from_string(p + 1, &mac);
+ if (r < 0)
+ return r;
+
+ return link_new(context, name, &mac, NULL);
+}
+
+int parse_cmdline_item(const char *key, const char *value, void *data) {
+ Context *context = data;
+
+ assert(key);
+ assert(data);
+
+ 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);
+
+ return 0;
+}
+
+int context_merge_networks(Context *context) {
+ Network *all, *network;
+ Route *route;
+ 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_name, link_free);
+}
+
+static int address_dump(Address *address, FILE *f) {
+ _cleanup_free_ char *addr = NULL, *peer = NULL;
+ int r;
+
+ r = in_addr_prefix_to_string(address->family, &address->address, address->prefixlen, &addr);
+ if (r < 0)
+ return r;
+
+ if (in_addr_is_null(address->family, &address->peer) == 0) {
+ r = in_addr_to_string(address->family, &address->peer, &peer);
+ if (r < 0)
+ return r;
+ }
+
+ fprintf(f,
+ "\n[Address]\n"
+ "Address=%s\n",
+ addr);
+
+ if (peer)
+ fprintf(f, "Peer=%s\n", peer);
+
+ return 0;
+}
+
+static int route_dump(Route *route, FILE *f) {
+ _cleanup_free_ char *dest = NULL, *gateway = NULL;
+ int r;
+
+ if (in_addr_is_null(route->family, &route->dest) == 0) {
+ r = in_addr_prefix_to_string(route->family, &route->dest, route->prefixlen, &dest);
+ if (r < 0)
+ return r;
+ }
+
+ r = in_addr_to_string(route->family, &route->gateway, &gateway);
+ if (r < 0)
+ return r;
+
+ fputs("\n[Route]\n", f);
+ if (dest)
+ fprintf(f, "Destination=%s\n", dest);
+ fprintf(f, "Gateway=%s\n", gateway);
+
+ return 0;
+}
+
+void network_dump(Network *network, FILE *f) {
+ char mac[ETHER_ADDR_TO_STRING_MAX];
+ Address *address;
+ Route *route;
+ const char *dhcp;
+ char **dns;
+
+ assert(network);
+ assert(f);
+
+ fprintf(f,
+ "[Match]\n"
+ "Name=%s\n",
+ isempty(network->ifname) ? "*" : network->ifname);
+
+ fputs("\n[Link]\n", f);
+
+ if (!ether_addr_is_null(&network->mac))
+ fprintf(f, "MACAddress=%s\n", ether_addr_to_string(&network->mac, 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) {
+ char mac[ETHER_ADDR_TO_STRING_MAX];
+
+ assert(link);
+ assert(f);
+
+ fputs("[Match]\n", f);
+
+ if (!ether_addr_is_null(&link->mac))
+ fprintf(f, "MACAddress=%s\n", ether_addr_to_string(&link->mac, mac));
+
+ fprintf(f,
+ "\n[Link]\n"
+ "Name=%s\n",
+ link->ifname);
+}
+
+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..86bcaec
--- /dev/null
+++ b/src/network/generator/network-generator.h
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <net/ethernet.h>
+#include <stdio.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_DHCP,
+ DHCP_TYPE_DHCP6,
+ DHCP_TYPE_AUTO6,
+ DHCP_TYPE_EITHER6,
+ DHCP_TYPE_IBFT,
+ _DHCP_TYPE_MAX,
+ _DHCP_TYPE_INVALID = -1,
+} 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 {
+ /* [Match] */
+ char *ifname;
+ struct ether_addr mac;
+};
+
+typedef struct Context {
+ Hashmap *networks_by_name;
+ Hashmap *netdevs_by_name;
+ Hashmap *links_by_name;
+} 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 *ifname);
+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..e658d89
--- /dev/null
+++ b/src/network/generator/test-network-generator.c
@@ -0,0 +1,438 @@
+/* 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 *ifname, 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, ifname));
+ 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"
+ "Name=*\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"
+ "Name=*\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"
+ "Name=*\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DNS=10.1.2.3\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("", "rd.peerdns", "0",
+ "[Match]\n"
+ "Name=*\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "UseDNS=no\n"
+ );
+
+ test_network_one("", "rd.peerdns", "1",
+ "[Match]\n"
+ "Name=*\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_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..f5ca183
--- /dev/null
+++ b/src/network/meson.build
@@ -0,0 +1,303 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+sources = files('''
+ netdev/bareudp.c
+ netdev/bareudp.h
+ netdev/bond.c
+ netdev/bond.h
+ netdev/bridge.c
+ netdev/bridge.h
+ netdev/dummy.c
+ netdev/dummy.h
+ netdev/ifb.c
+ netdev/ifb.h
+ netdev/ipvlan.c
+ netdev/ipvlan.h
+ netdev/macvlan.c
+ netdev/macvlan.h
+ netdev/netdev.c
+ netdev/netdev.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/vxlan.c
+ netdev/vxlan.h
+ netdev/geneve.c
+ netdev/geneve.h
+ netdev/vxcan.c
+ netdev/vxcan.h
+ netdev/wireguard.c
+ netdev/wireguard.h
+ netdev/netdevsim.c
+ netdev/netdevsim.h
+ netdev/fou-tunnel.c
+ netdev/fou-tunnel.h
+ netdev/l2tp-tunnel.c
+ netdev/l2tp-tunnel.h
+ netdev/macsec.c
+ netdev/macsec.h
+ netdev/xfrm.c
+ netdev/xfrm.h
+ networkd-address-label.c
+ networkd-address-label.h
+ networkd-address-pool.c
+ networkd-address-pool.h
+ networkd-address.c
+ networkd-address.h
+ networkd-brvlan.c
+ networkd-brvlan.h
+ networkd-can.c
+ networkd-can.h
+ networkd-conf.c
+ networkd-conf.h
+ networkd-dhcp-common.c
+ networkd-dhcp-common.h
+ networkd-dhcp-server-bus.c
+ networkd-dhcp-server-bus.h
+ networkd-dhcp-server.c
+ networkd-dhcp-server.h
+ networkd-dhcp4.c
+ networkd-dhcp4.h
+ networkd-dhcp6.c
+ networkd-dhcp6.h
+ networkd-fdb.c
+ networkd-fdb.h
+ networkd-ipv4ll.c
+ networkd-ipv4ll.h
+ networkd-ipv6-proxy-ndp.c
+ networkd-ipv6-proxy-ndp.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-mdb.c
+ networkd-mdb.h
+ networkd-ndisc.c
+ networkd-ndisc.h
+ networkd-neighbor.c
+ networkd-neighbor.h
+ networkd-radv.c
+ networkd-radv.h
+ networkd-network-bus.c
+ networkd-network-bus.h
+ networkd-network.c
+ networkd-network.h
+ networkd-nexthop.c
+ networkd-nexthop.h
+ networkd-route.c
+ networkd-route.h
+ networkd-routing-policy-rule.c
+ networkd-routing-policy-rule.h
+ networkd-speed-meter.c
+ networkd-speed-meter.h
+ networkd-sriov.c
+ networkd-sriov.h
+ networkd-sysctl.c
+ networkd-sysctl.h
+ networkd-util.c
+ networkd-util.h
+ networkd-wifi.c
+ networkd-wifi.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.c
+ tc/fq.h
+ tc/fq-codel.c
+ tc/fq-codel.h
+ tc/fq-pie.c
+ tc/fq-pie.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
+'''.split())
+
+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
+'''.split()) + network_internal_h
+
+networkctl_sources = files('networkctl.c')
+
+network_generator_sources = files('''
+ generator/main.c
+ generator/network-generator.c
+ generator/network-generator.h
+'''.split())
+
+network_include_dir = [includes, include_directories(['.', 'netdev', 'tc'])]
+
+if conf.get('ENABLE_NETWORKD') == 1
+ if get_option('link-networkd-shared')
+ networkd_link_with = [libshared]
+ else
+ networkd_link_with = [libsystemd_static,
+ libshared_static,
+ libjournal_client,
+ libbasic_gcrypt]
+ endif
+
+ networkd_gperf_c = custom_target(
+ 'networkd-gperf.c',
+ input : 'networkd-gperf.gperf',
+ output : 'networkd-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+ networkd_network_gperf_c = custom_target(
+ 'networkd-network-gperf.c',
+ input : 'networkd-network-gperf.gperf',
+ output : 'networkd-network-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+ netdev_gperf_c = custom_target(
+ 'netdev-gperf.c',
+ input : 'netdev/netdev-gperf.gperf',
+ output : 'netdev-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+ libnetworkd_core = static_library(
+ 'networkd-core',
+ sources,
+ network_internal_h,
+ networkd_gperf_c,
+ networkd_network_gperf_c,
+ netdev_gperf_c,
+ include_directories : network_include_dir,
+ link_with : [networkd_link_with])
+
+ 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
+ install_data('networkd.conf',
+ install_dir : pkgsysconfdir)
+ endif
+
+ fuzzers += [
+ [['src/network/fuzz-netdev-parser.c',
+ 'src/fuzz/fuzz.h'],
+ [libnetworkd_core,
+ libudev_static,
+ libsystemd_network,
+ networkd_link_with],
+ [threads],
+ [],
+ network_include_dir],
+
+ [['src/network/fuzz-network-parser.c',
+ 'src/fuzz/fuzz.h'],
+ [libnetworkd_core,
+ libudev_static,
+ libsystemd_network,
+ networkd_link_with],
+ [threads],
+ [],
+ network_include_dir],
+ ]
+
+ tests += [
+ [['src/network/test-networkd-conf.c'],
+ [libnetworkd_core,
+ libsystemd_network,
+ libudev],
+ [], '', '', [], network_include_dir],
+
+ [['src/network/test-network.c'],
+ [libnetworkd_core,
+ libudev_static,
+ libsystemd_network,
+ networkd_link_with],
+ [threads],
+ '', '', [], network_include_dir],
+
+ [['src/network/test-routing-policy-rule.c'],
+ [libnetworkd_core,
+ libsystemd_network,
+ libudev],
+ [], '', '', [], network_include_dir],
+
+ [['src/network/test-network-tables.c',
+ test_tables_h],
+ [libnetworkd_core,
+ libudev_static,
+ libsystemd_network,
+ networkd_link_with],
+ [threads],
+ '', '', [],
+ [network_include_dir]],
+
+ [['src/network/generator/test-network-generator.c',
+ 'src/network/generator/network-generator.c',
+ 'src/network/generator/network-generator.h'],
+ [networkd_link_with],
+ [], '', '', [], network_include_dir],
+ ]
+endif
diff --git a/src/network/netdev/bareudp.c b/src/network/netdev/bareudp.c
new file mode 100644
index 0000000..22c0e49
--- /dev/null
+++ b/src/network/netdev/bareudp.c
@@ -0,0 +1,138 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#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=");
+
+/* callback for bareudp netdev's created without a backing Link */
+static int bare_udp_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, "BareUDP netdev exists, using existing without changing its parameters.");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "BareUDP netdev could not be created: %m");
+ netdev_drop(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "BareUDP created.");
+
+ return 1;
+}
+
+static int netdev_bare_udp_create(NetDev *netdev) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ BareUDP *u;
+ int r;
+
+ assert(netdev);
+
+ u = BAREUDP(netdev);
+
+ assert(u);
+
+ r = sd_rtnl_message_new_link(netdev->manager->rtnl, &m, RTM_NEWLINK, 0);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not allocate RTM_NEWLINK message: %m");
+
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, netdev->ifname);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IFNAME, attribute: %m");
+
+ r = sd_netlink_message_open_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, netdev_kind_to_string(netdev->kind));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_BAREUDP_ETHERTYPE, htobe16(u->iftype));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BAREUDP_ETHERTYPE attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_BAREUDP_PORT, htobe16(u->dest_port));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BAREUDP_PORT attribute: %m");
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = netlink_call_async(netdev->manager->rtnl, NULL, m, bare_udp_netdev_create_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not send rtnetlink message: %m");
+
+ netdev_ref(netdev);
+ netdev->state = NETDEV_STATE_CREATING;
+
+ log_netdev_debug(netdev, "Creating");
+
+ return r;
+}
+
+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,
+ .create = netdev_bare_udp_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+};
diff --git a/src/network/netdev/bareudp.h b/src/network/netdev/bareudp.h
new file mode 100644
index 0000000..ea80bbf
--- /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 = -1
+} 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/bond.c b/src/network/netdev/bond.c
new file mode 100644
index 0000000..e27f360
--- /dev/null
+++ b/src/network/netdev/bond.c
@@ -0,0 +1,527 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_MODE attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_XMIT_HASH_POLICY attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_AD_LACP_RATE attribute: %m");
+ }
+
+ if (b->miimon != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_MIIMON, b->miimon / USEC_PER_MSEC);
+ if (r < 0)
+ log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_BOND_MIIMON attribute: %m");
+ }
+
+ if (b->downdelay != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_DOWNDELAY, b->downdelay / USEC_PER_MSEC);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_DOWNDELAY attribute: %m");
+ }
+
+ if (b->updelay != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_UPDELAY, b->updelay / USEC_PER_MSEC);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_UPDELAY attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_ARP_INTERVAL attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_LP_INTERVAL attribute: %m");
+ }
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_AD_SELECT attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_FAIL_OVER_MAC attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_ARP_VALIDATE attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_ARP_ALL_TARGETS attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_PRIMARY_RESELECT attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_RESEND_IGMP attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_PACKETS_PER_SLAVE attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_NUM_PEER_NOTIF attribute: %m");
+ }
+
+ if (b->min_links != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_BOND_MIN_LINKS, b->min_links);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_MIN_LINKS attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_AD_ACTOR_SYS_PRIO attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_AD_USER_PORT_KEY attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_AD_ACTOR_SYSTEM attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_BOND_ALL_SLAVES_ACTIVE, b->all_slaves_active);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_ALL_SLAVES_ACTIVE attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_TLB_DYNAMIC_LB attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not open contaniner IFLA_BOND_ARP_IP_TARGET : %m");
+
+ ORDERED_SET_FOREACH(val, b->arp_ip_targets) {
+ r = sd_netlink_message_append_u32(m, n++, PTR_TO_UINT32(val));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BOND_ARP_ALL_TARGETS attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not close contaniner IFLA_BOND_ARP_IP_TARGET : %m");
+ }
+
+ return 0;
+}
+
+static int link_set_bond_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not set bonding interface: %m");
+ return 1;
+ }
+
+ return 1;
+}
+
+int link_set_bond(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_NEWLINK, link->network->bond->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK 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 flags: %m");
+
+ r = sd_netlink_message_open_container(req, IFLA_LINKINFO);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_PROTINFO attribute: %m");
+
+ r = sd_netlink_message_open_container_union(req, IFLA_INFO_DATA, "bond");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ if (link->network->active_slave) {
+ r = sd_netlink_message_append_u32(req, IFLA_BOND_ACTIVE_SLAVE, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_BOND_ACTIVE_SLAVE attribute: %m");
+ }
+
+ if (link->network->primary_slave) {
+ r = sd_netlink_message_append_u32(req, IFLA_BOND_PRIMARY, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_BOND_PRIMARY attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_set_bond_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);
+
+ return r;
+}
+
+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;
+ }
+
+ r = ordered_set_ensure_allocated(&b->arp_ip_targets, NULL);
+ if (r < 0)
+ return log_oom();
+
+ 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_put(b->arp_ip_targets, UINT32_TO_PTR(ip.in.s_addr));
+ 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 = ether_addr_from_string(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_MASTER,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/bond.h b/src/network/netdev/bond.h
new file mode 100644
index 0000000..11d3e9b
--- /dev/null
+++ b/src/network/netdev/bond.h
@@ -0,0 +1,62 @@
+/* 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;
+
+int link_set_bond(Link *link);
+
+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..1f59cd8
--- /dev/null
+++ b/src/network/netdev/bridge.c
@@ -0,0 +1,368 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+
+#include "bridge.h"
+#include "netlink-util.h"
+#include "network-internal.h"
+#include "networkd-manager.h"
+#include "string-table.h"
+#include "vlan-util.h"
+
+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(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ Bridge *b;
+ int r;
+
+ assert(netdev);
+
+ b = BRIDGE(netdev);
+
+ assert(b);
+
+ 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 RTM_SETLINK 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 flags: %m");
+
+ r = sd_netlink_message_open_container(req, IFLA_LINKINFO);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = sd_netlink_message_open_container_union(req, IFLA_INFO_DATA, netdev_kind_to_string(netdev->kind));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ /* 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_FORWARD_DELAY attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_HELLO_TIME attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_MAX_AGE attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_AGEING_TIME attribute: %m");
+ }
+
+ if (b->priority > 0) {
+ r = sd_netlink_message_append_u16(req, IFLA_BR_PRIORITY, b->priority);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_PRIORITY attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_GROUP_FWD_MASK attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_VLAN_DEFAULT_PVID attribute: %m");
+ }
+
+ if (b->mcast_querier >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_QUERIER, b->mcast_querier);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_MCAST_QUERIER attribute: %m");
+ }
+
+ if (b->mcast_snooping >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_SNOOPING, b->mcast_snooping);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_MCAST_SNOOPING attribute: %m");
+ }
+
+ if (b->vlan_filtering >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BR_VLAN_FILTERING, b->vlan_filtering);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_VLAN_FILTERING attribute: %m");
+ }
+
+ if (b->vlan_protocol >= 0) {
+ r = sd_netlink_message_append_u16(req, IFLA_BR_VLAN_PROTOCOL, b->vlan_protocol);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_VLAN_PROTOCOL attribute: %m");
+ }
+
+ if (b->stp >= 0) {
+ r = sd_netlink_message_append_u32(req, IFLA_BR_STP_STATE, b->stp);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_STP_STATE attribute: %m");
+ }
+
+ if (b->igmp_version > 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_IGMP_VERSION, b->igmp_version);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_BR_MCAST_IGMP_VERSION attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %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 rtnetlink message: %m");
+
+ netdev_ref(netdev);
+
+ return r;
+}
+
+static int link_set_bridge_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not set bridge interface: %m");
+ return 1;
+ }
+
+ return 1;
+}
+
+int link_set_bridge(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_rtnl_message_link_set_family(req, AF_BRIDGE);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set message family: %m");
+
+ r = sd_netlink_message_open_container(req, IFLA_PROTINFO);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_PROTINFO attribute: %m");
+
+ if (link->network->use_bpdu >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_GUARD, link->network->use_bpdu);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_BRPORT_GUARD attribute: %m");
+ }
+
+ if (link->network->hairpin >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MODE, link->network->hairpin);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_BRPORT_MODE attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_FAST_LEAVE attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_PROTECT attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_UNICAST_FLOOD attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_MCAST_FLOOD attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_MCAST_TO_UCAST attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_NEIGH_SUPPRESS attribute: %m");
+ }
+
+ if (link->network->learning >= 0) {
+ r = sd_netlink_message_append_u8(req, IFLA_BRPORT_LEARNING, link->network->learning);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_BRPORT_LEARNING attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_PROXYARP attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_PROXYARP_WIFI attribute: %m");
+ }
+
+ if (link->network->cost != 0) {
+ r = sd_netlink_message_append_u32(req, IFLA_BRPORT_COST, link->network->cost);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_BRPORT_COST attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_PRIORITY attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRPORT_MULTICAST_ROUTER attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_set_bridge_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);
+
+ 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;
+}
+
+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_MASTER,
+};
diff --git a/src/network/netdev/bridge.h b/src/network/netdev/bridge.h
new file mode 100644
index 0000000..d6abda9
--- /dev/null
+++ b/src/network/netdev/bridge.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+#include <linux/if_bridge.h>
+
+#include "conf-parser.h"
+#include "netdev.h"
+
+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 = MDB_RTR_TYPE_DISABLED,
+ MULTICAST_ROUTER_TEMPORARY_QUERY = MDB_RTR_TYPE_TEMP_QUERY,
+ MULTICAST_ROUTER_PERMANENT = MDB_RTR_TYPE_PERM,
+ MULTICAST_ROUTER_TEMPORARY = MDB_RTR_TYPE_TEMP,
+ _MULTICAST_ROUTER_MAX,
+ _MULTICAST_ROUTER_INVALID = -1,
+} MulticastRouter;
+
+DEFINE_NETDEV_CAST(BRIDGE, Bridge);
+extern const NetDevVTable bridge_vtable;
+
+int link_set_bridge(Link *link);
+
+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);
diff --git a/src/network/netdev/dummy.c b/src/network/netdev/dummy.c
new file mode 100644
index 0000000..754ee98
--- /dev/null
+++ b/src/network/netdev/dummy.c
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "dummy.h"
+
+const NetDevVTable dummy_vtable = {
+ .object_size = sizeof(Dummy),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .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..6863257
--- /dev/null
+++ b/src/network/netdev/fou-tunnel.c
@@ -0,0 +1,279 @@
+/* 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 **ret) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ FouTunnel *t;
+ uint8_t encap_type;
+ int r;
+
+ assert(netdev);
+
+ t = FOU(netdev);
+
+ assert(t);
+
+ r = sd_genl_message_new(netdev->manager->genl, SD_GENL_FOU, FOU_CMD_ADD, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to allocate generic netlink message: %m");
+
+ r = sd_netlink_message_append_u16(m, FOU_ATTR_PORT, htobe16(t->port));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_PORT attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_PEER_PORT attribute: %m");
+ }
+
+ 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("invalid encap type");
+ }
+
+ r = sd_netlink_message_append_u8(m, FOU_ATTR_TYPE, encap_type);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_TYPE attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, FOU_ATTR_AF, AF_INET);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_AF attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, FOU_ATTR_IPPROTO, t->fou_protocol);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_IPPROTO attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_LOCAL_V4 attribute: %m");
+ } 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 log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_LOCAL_V6 attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_PEER_V4 attribute: %m");
+ } 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 log_netdev_error_errno(netdev, r, "Could not append FOU_ATTR_PEER_V6 attribute: %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_drop(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_fill_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 = 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);
+ assert(data);
+
+ 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 = data;
+ FouTunnel *t = userdata;
+ int r, *f;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid fou encap type");
+ }
+
+ 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..a6f10df
--- /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 = -1,
+} 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..edf92ec
--- /dev/null
+++ b/src/network/netdev/geneve.c
@@ -0,0 +1,356 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.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");
+
+/* callback for geneve netdev's created without a backing Link */
+static int geneve_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, "Geneve netdev exists, using existing without changing its parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "Geneve netdev could not be created: %m");
+ netdev_drop(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "Geneve created");
+
+ return 1;
+}
+
+static int netdev_geneve_create(NetDev *netdev) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ Geneve *v;
+ int r;
+
+ assert(netdev);
+
+ v = GENEVE(netdev);
+
+ r = sd_rtnl_message_new_link(netdev->manager->rtnl, &m, RTM_NEWLINK, 0);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not allocate RTM_NEWLINK message: %m");
+
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, netdev->ifname);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IFNAME, attribute: %m");
+
+ if (netdev->mac) {
+ r = sd_netlink_message_append_ether_addr(m, IFLA_ADDRESS, netdev->mac);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_ADDRESS attribute: %m");
+ }
+
+ if (netdev->mtu != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_MTU, netdev->mtu);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_MTU attribute: %m");
+ }
+
+ r = sd_netlink_message_open_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, netdev_kind_to_string(netdev->kind));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ if (v->id <= GENEVE_VID_MAX) {
+ r = sd_netlink_message_append_u32(m, IFLA_GENEVE_ID, v->id);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_ID attribute: %m");
+ }
+
+ if (in_addr_is_null(v->remote_family, &v->remote) == 0) {
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_REMOTE/IFLA_GENEVE_REMOTE6 attribute: %m");
+ }
+
+ if (v->inherit) {
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TTL_INHERIT, 1);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_TTL_INHERIT attribute: %m");
+ } else {
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TTL, v->ttl);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_TTL attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TOS, v->tos);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_TOS attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_CSUM, v->udpcsum);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_UDP_CSUM attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_TX, v->udp6zerocsumtx);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_UDP_ZERO_CSUM6_TX attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_RX, v->udp6zerocsumrx);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_UDP_ZERO_CSUM6_RX attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_PORT attribute: %m");
+ }
+
+ if (v->flow_label > 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_GENEVE_LABEL, htobe32(v->flow_label));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_LABEL attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_GENEVE_DF attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = netlink_call_async(netdev->manager->rtnl, NULL, m, geneve_netdev_create_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not send rtnetlink message: %m");
+
+ netdev_ref(netdev);
+ netdev->state = NETDEV_STATE_CREATING;
+
+ log_netdev_debug(netdev, "Creating");
+
+ return r;
+}
+
+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",
+ .create = netdev_geneve_create,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = netdev_geneve_verify,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/geneve.h b/src/network/netdev/geneve.h
new file mode 100644
index 0000000..b62eb7b
--- /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 = -1
+} 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..16ff49d
--- /dev/null
+++ b/src/network/netdev/ifb.c
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+
+#include "ifb.h"
+
+const NetDevVTable ifb_vtable = {
+ .object_size = sizeof(IntermediateFunctionalBlock),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .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/ipvlan.c b/src/network/netdev/ipvlan.c
new file mode 100644
index 0000000..92a8f58
--- /dev/null
+++ b/src/network/netdev/ipvlan.c
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.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 log_netdev_error_errno(netdev, r, "Could not append IFLA_IPVLAN_MODE attribute: %m");
+ }
+
+ if (m->flags != _NETDEV_IPVLAN_FLAGS_INVALID) {
+ r = sd_netlink_message_append_u16(req, IFLA_IPVLAN_FLAGS, m->flags);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPVLAN_FLAGS attribute: %m");
+ }
+
+ 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,
+ .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,
+ .generate_mac = true,
+};
+
+IPVlanMode link_get_ipvlan_mode(Link *link) {
+ NetDev *netdev;
+
+ if (!streq_ptr(link->kind, "ipvlan"))
+ return _NETDEV_IPVLAN_MODE_INVALID;
+
+ if (netdev_get(link->manager, link->ifname, &netdev) < 0)
+ return _NETDEV_IPVLAN_MODE_INVALID;
+
+ if (netdev->kind != NETDEV_KIND_IPVLAN)
+ return _NETDEV_IPVLAN_MODE_INVALID;
+
+ return IPVLAN(netdev)->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..eeea197
--- /dev/null
+++ b/src/network/netdev/l2tp-tunnel.c
@@ -0,0 +1,728 @@
+/* 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 "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 void l2tp_session_free(L2tpSession *s) {
+ if (!s)
+ return;
+
+ if (s->tunnel && s->section)
+ ordered_hashmap_remove(s->tunnel->sessions_by_section, s->section);
+
+ network_config_section_free(s->section);
+
+ free(s->name);
+
+ free(s);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(L2tpSession, l2tp_session_free);
+
+static int l2tp_session_new_static(L2tpTunnel *t, const char *filename, unsigned section_line, L2tpSession **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(l2tp_session_freep) L2tpSession *s = NULL;
+ int r;
+
+ assert(t);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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_allocated(&t->sessions_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(t->sessions_by_section, s->section, s);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(s);
+ return 0;
+}
+
+static int netdev_l2tp_fill_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);
+
+ t = L2TP(netdev);
+
+ assert(t);
+
+ r = sd_genl_message_new(netdev->manager->genl, SD_GENL_L2TP, L2TP_CMD_TUNNEL_CREATE, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create generic netlink message: %m");
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_CONN_ID, t->tunnel_id);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_CONN_ID attribute: %m");
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_CONN_ID, t->peer_tunnel_id);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_PEER_CONN_ID attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, L2TP_ATTR_PROTO_VERSION, 3);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_PROTO_VERSION attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_ENCAP_TYPE attribute: %m");
+
+ if (t->family == AF_INET) {
+ r = sd_netlink_message_append_in_addr(m, L2TP_ATTR_IP_SADDR, &local_address->in);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_IP_SADDR attribute: %m");
+
+ r = sd_netlink_message_append_in_addr(m, L2TP_ATTR_IP_DADDR, &t->remote.in);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_IP_DADDR attribute: %m");
+ } else {
+ r = sd_netlink_message_append_in6_addr(m, L2TP_ATTR_IP6_SADDR, &local_address->in6);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_IP6_SADDR attribute: %m");
+
+ r = sd_netlink_message_append_in6_addr(m, L2TP_ATTR_IP6_DADDR, &t->remote.in6);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_IP6_DADDR attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_UDP_SPORT, attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, L2TP_ATTR_UDP_DPORT, t->l2tp_udp_dport);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_UDP_DPORT attribute: %m");
+
+ if (t->udp_csum) {
+ r = sd_netlink_message_append_u8(m, L2TP_ATTR_UDP_CSUM, t->udp_csum);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_UDP_CSUM attribute: %m");
+ }
+
+ if (t->udp6_csum_tx) {
+ r = sd_netlink_message_append_flag(m, L2TP_ATTR_UDP_ZERO_CSUM6_TX);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_UDP_ZERO_CSUM6_TX attribute: %m");
+ }
+
+ if (t->udp6_csum_rx) {
+ r = sd_netlink_message_append_flag(m, L2TP_ATTR_UDP_ZERO_CSUM6_RX);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_UDP_ZERO_CSUM6_RX attribute: %m");
+ }
+ }
+
+ *ret = TAKE_PTR(m);
+
+ return 0;
+}
+
+static int netdev_l2tp_fill_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, SD_GENL_L2TP, L2TP_CMD_SESSION_CREATE, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create generic netlink message: %m");
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_CONN_ID, session->tunnel->tunnel_id);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_CONN_ID attribute: %m");
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_CONN_ID, session->tunnel->peer_tunnel_id);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_PEER_CONN_ID attribute: %m");
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_SESSION_ID, session->session_id);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_SESSION_ID attribute: %m");
+
+ r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_SESSION_ID, session->peer_session_id);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_PEER_SESSION_ID attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, L2TP_ATTR_PW_TYPE, L2TP_PWTYPE_ETH);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_PW_TYPE attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_L2SPEC_TYPE attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, L2TP_ATTR_L2SPEC_LEN, l2_spec_len);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_L2SPEC_LEN attribute: %m");
+
+ r = sd_netlink_message_append_string(m, L2TP_ATTR_IFNAME, session->name);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append L2TP_ATTR_IFNAME attribute: %m");
+
+ *ret = TAKE_PTR(m);
+
+ return 0;
+}
+
+static int l2tp_acquire_local_address_one(L2tpTunnel *t, Address *a, union in_addr_union *ret) {
+ if (a->family != t->family)
+ return -EINVAL;
+
+ if (in_addr_is_null(a->family, &a->in_addr_peer) <= 0)
+ return -EINVAL;
+
+ 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;
+
+ *ret = a->in_addr;
+ return 0;
+}
+
+static int l2tp_acquire_local_address(L2tpTunnel *t, Link *link, union in_addr_union *ret) {
+ Address *a;
+
+ assert(t);
+ assert(link);
+ assert(ret);
+ assert(IN_SET(t->family, AF_INET, AF_INET6));
+
+ if (!in_addr_is_null(t->family, &t->local)) {
+ /* local address is explicitly specified. */
+ *ret = t->local;
+ return 0;
+ }
+
+ SET_FOREACH(a, link->addresses)
+ if (l2tp_acquire_local_address_one(t, a, ret) >= 0)
+ return 1;
+
+ SET_FOREACH(a, link->addresses_foreign)
+ if (l2tp_acquire_local_address_one(t, a, ret) >= 0)
+ return 1;
+
+ return -ENODATA;
+}
+
+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_fill_message_session(netdev, session, &n);
+ if (r < 0)
+ return r;
+
+ 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_drop(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, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ union in_addr_union local_address;
+ L2tpTunnel *t;
+ int r;
+
+ assert(netdev);
+
+ t = L2TP(netdev);
+
+ assert(t);
+
+ r = l2tp_acquire_local_address(t, link, &local_address);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not find local address.");
+
+ if (r > 0 && DEBUG_LOGGING) {
+ _cleanup_free_ char *str = NULL;
+
+ (void) in_addr_to_string(t->family, &local_address, &str);
+ log_netdev_debug(netdev, "Local address %s acquired.", strna(str));
+ }
+
+ r = netdev_l2tp_fill_message_tunnel(netdev, &local_address, &m);
+ if (r < 0)
+ return r;
+
+ 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;
+}
+
+int config_parse_l2tp_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) {
+
+ L2tpTunnel *t = userdata;
+ union in_addr_union *addr = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (streq(lvalue, "Local")) {
+ L2tpLocalAddressType addr_type;
+
+ if (isempty(rvalue))
+ addr_type = NETDEV_L2TP_LOCAL_ADDRESS_AUTO;
+ else
+ addr_type = l2tp_local_address_type_from_string(rvalue);
+
+ if (addr_type >= 0) {
+ if (in_addr_is_null(t->family, &t->remote) != 0)
+ /* If Remote= is not specified yet, then also clear family. */
+ t->family = AF_UNSPEC;
+
+ t->local = IN_ADDR_NULL;
+ t->local_address_type = addr_type;
+
+ return 0;
+ }
+ }
+
+ if (t->family == AF_UNSPEC)
+ r = in_addr_from_string_auto(rvalue, &t->family, addr);
+ else
+ r = in_addr_from_string(t->family, rvalue, addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid L2TP Tunnel address specified in %s='%s', ignoring assignment: %m", lvalue, rvalue);
+ return 0;
+ }
+
+ 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, 0,
+ "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_null(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);
+}
+
+const NetDevVTable l2tptnl_vtable = {
+ .object_size = sizeof(L2tpTunnel),
+ .init = l2tp_tunnel_init,
+ .sections = NETDEV_COMMON_SECTIONS "L2TP\0L2TPSession\0",
+ .create_after_configured = l2tp_create_tunnel,
+ .done = l2tp_tunnel_done,
+ .create_type = NETDEV_CREATE_AFTER_CONFIGURED,
+ .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..048318d
--- /dev/null
+++ b/src/network/netdev/l2tp-tunnel.h
@@ -0,0 +1,78 @@
+/* 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 = -1,
+} 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 = -1,
+} 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 = -1,
+} L2tpLocalAddressType;
+
+typedef struct L2tpTunnel L2tpTunnel;
+
+typedef struct L2tpSession {
+ L2tpTunnel *tunnel;
+ NetworkConfigSection *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;
+
+ 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_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..82e71c3
--- /dev/null
+++ b/src/network/netdev/macsec.c
@@ -0,0 +1,1252 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.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 "network-internal.h"
+#include "networkd-manager.h"
+#include "path-util.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 void macsec_receive_association_free(ReceiveAssociation *c) {
+ if (!c)
+ return;
+
+ if (c->macsec && c->section)
+ ordered_hashmap_remove(c->macsec->receive_associations_by_section, c->section);
+
+ network_config_section_free(c->section);
+ security_association_clear(&c->sa);
+
+ free(c);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(ReceiveAssociation, macsec_receive_association_free);
+
+static int macsec_receive_association_new_static(MACsec *s, const char *filename, unsigned section_line, ReceiveAssociation **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(macsec_receive_association_freep) ReceiveAssociation *c = NULL;
+ int r;
+
+ assert(s);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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_allocated(&s->receive_associations_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(s->receive_associations_by_section, c->section, c);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(c);
+
+ return 0;
+}
+
+static void macsec_receive_channel_free(ReceiveChannel *c) {
+ if (!c)
+ return;
+
+ 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);
+ }
+
+ network_config_section_free(c->section);
+
+ free(c);
+}
+
+DEFINE_NETWORK_SECTION_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_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(macsec_receive_channel_freep) ReceiveChannel *c = NULL;
+ int r;
+
+ assert(s);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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_allocated(&s->receive_channels_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(s->receive_channels_by_section, c->section, c);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(c);
+
+ return 0;
+}
+
+static void macsec_transmit_association_free(TransmitAssociation *a) {
+ if (!a)
+ return;
+
+ if (a->macsec && a->section)
+ ordered_hashmap_remove(a->macsec->transmit_associations_by_section, a->section);
+
+ network_config_section_free(a->section);
+ security_association_clear(&a->sa);
+
+ free(a);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(TransmitAssociation, macsec_transmit_association_free);
+
+static int macsec_transmit_association_new_static(MACsec *s, const char *filename, unsigned section_line, TransmitAssociation **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(macsec_transmit_association_freep) TransmitAssociation *a = NULL;
+ int r;
+
+ assert(s);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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_allocated(&s->transmit_associations_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(s->transmit_associations_by_section, a->section, a);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(a);
+
+ return 0;
+}
+
+static int netdev_macsec_fill_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, SD_GENL_MACSEC, command, &m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to create generic netlink message: %m");
+
+ r = sd_netlink_message_append_u32(m, MACSEC_ATTR_IFINDEX, netdev->ifindex);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append MACSEC_ATTR_IFINDEX attribute: %m");
+
+ *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 log_netdev_error_errno(netdev, r, "Could not append MACSEC_ATTR_RXSC_CONFIG attribute: %m");
+
+ r = sd_netlink_message_append_u64(m, MACSEC_RXSC_ATTR_SCI, sci->as_uint64);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append MACSEC_RXSC_ATTR_SCI attribute: %m");
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append MACSEC_ATTR_RXSC_CONFIG attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append MACSEC_ATTR_SA_CONFIG attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, MACSEC_SA_ATTR_AN, a->association_number);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append MACSEC_SA_ATTR_AN attribute: %m");
+
+ if (a->packet_number > 0) {
+ r = sd_netlink_message_append_u32(m, MACSEC_SA_ATTR_PN, a->packet_number);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append MACSEC_SA_ATTR_PN attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append MACSEC_SA_ATTR_KEYID attribute: %m");
+
+ r = sd_netlink_message_append_data(m, MACSEC_SA_ATTR_KEY, a->key, a->key_len);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append MACSEC_SA_ATTR_KEY attribute: %m");
+ }
+
+ if (a->activate >= 0) {
+ r = sd_netlink_message_append_u8(m, MACSEC_SA_ATTR_ACTIVE, a->activate);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append MACSEC_SA_ATTR_ACTIVE attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append MACSEC_ATTR_SA_CONFIG attribute: %m");
+
+ 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 existing without changing its parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r,
+ "Failed to add receive secure association: %m");
+ netdev_drop(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_fill_message(netdev, MACSEC_CMD_ADD_RXSA, &m);
+ if (r < 0)
+ return r;
+
+ r = netdev_macsec_fill_message_sa(netdev, &a->sa, m);
+ if (r < 0)
+ return r;
+
+ r = netdev_macsec_fill_message_sci(netdev, &a->sci, m);
+ if (r < 0)
+ return r;
+
+ 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;
+ unsigned i;
+ 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 existing without changing its parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r,
+ "Failed to add receive secure channel: %m");
+ netdev_drop(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "Receive channel is configured");
+
+ for (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_drop(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_fill_message(netdev, MACSEC_CMD_ADD_RXSC, &m);
+ if (r < 0)
+ return r;
+
+ r = netdev_macsec_fill_message_sci(netdev, &c->sci, m);
+ if (r < 0)
+ return r;
+
+ 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 existing without changing its parameters");
+ else if (r < 0) {
+ log_netdev_warning_errno(netdev, r,
+ "Failed to add transmit secure association: %m");
+ netdev_drop(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_fill_message(netdev, MACSEC_CMD_ADD_TXSA, &m);
+ if (r < 0)
+ return r;
+
+ r = netdev_macsec_fill_message_sa(netdev, &a->sa, m);
+ if (r < 0)
+ return r;
+
+ 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, sd_netlink_message *m) {
+ 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);
+
+ if (v->port > 0) {
+ r = sd_netlink_message_append_u16(m, IFLA_MACSEC_PORT, v->port);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_MACSEC_PORT attribute: %m");
+ }
+
+ if (v->encrypt >= 0) {
+ r = sd_netlink_message_append_u8(m, IFLA_MACSEC_ENCRYPT, v->encrypt);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_MACSEC_ENCRYPT attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_MACSEC_ENCODING_SA, v->encoding_an);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_MACSEC_ENCODING_SA attribute: %m");
+
+ return r;
+}
+
+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 = ether_addr_from_string(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 > %u), 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;
+
+ (void) warn_file_is_world_accessible(sa->key_file, NULL, NULL, 0);
+
+ r = read_full_file_full(
+ AT_FDCWD, sa->key_file,
+ 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_allocated(&c->macsec->receive_channels, &uint64_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ r = ordered_hashmap_put(c->macsec->receive_channels, &c->sci.as_uint64, c);
+ 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_allocated(&a->macsec->receive_channels, &uint64_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ r = ordered_hashmap_put(a->macsec->receive_channels, &new_channel->sci.as_uint64, new_channel);
+ 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,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/macsec.h b/src/network/netdev/macsec.h
new file mode 100644
index 0000000..4d88e49
--- /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;
+ NetworkConfigSection *section;
+
+ SecurityAssociation sa;
+} TransmitAssociation;
+
+typedef struct ReceiveAssociation {
+ MACsec *macsec;
+ NetworkConfigSection *section;
+
+ MACsecSCI sci;
+ SecurityAssociation sa;
+} ReceiveAssociation;
+
+typedef struct ReceiveChannel {
+ MACsec *macsec;
+ NetworkConfigSection *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..9bdcf62
--- /dev/null
+++ b/src/network/netdev/macvlan.c
@@ -0,0 +1,105 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+
+#include "conf-parser.h"
+#include "macvlan.h"
+#include "macvlan-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);
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_MACVLAN_MACADDR_MODE attribute: %m");
+
+ r = sd_netlink_message_open_container(req, IFLA_MACVLAN_MACADDR_DATA);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not open IFLA_MACVLAN_MACADDR_DATA container: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_MACVLAN_MACADDR attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not close IFLA_MACVLAN_MACADDR_DATA container: %m");
+ }
+
+ if (m->mode != _NETDEV_MACVLAN_MODE_INVALID) {
+ r = sd_netlink_message_append_u32(req, IFLA_MACVLAN_MODE, m->mode);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_MACVLAN_MODE attribute: %m");
+ }
+
+ 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_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;
+}
+
+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,
+ .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,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/macvlan.h b/src/network/netdev/macvlan.h
new file mode 100644
index 0000000..cb7eece
--- /dev/null
+++ b/src/network/netdev/macvlan.h
@@ -0,0 +1,22 @@
+/* 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;
+};
+
+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);
diff --git a/src/network/netdev/netdev-gperf.gperf b/src/network/netdev/netdev-gperf.gperf
new file mode 100644
index 0000000..4e89761
--- /dev/null
+++ b/src/network/netdev/netdev-gperf.gperf
@@ -0,0 +1,232 @@
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "bareudp.h"
+#include "bond.h"
+#include "bridge.h"
+#include "conf-parser.h"
+#include "geneve.h"
+#include "ipvlan.h"
+#include "macsec.h"
+#include "macvlan.h"
+#include "tunnel.h"
+#include "tuntap.h"
+#include "veth.h"
+#include "vlan-util.h"
+#include "vlan.h"
+#include "vxlan.h"
+#include "vrf.h"
+#include "netdev.h"
+#include "network-internal.h"
+#include "vxcan.h"
+#include "wireguard.h"
+#include "fou-tunnel.h"
+#include "l2tp-tunnel.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.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, 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_hwaddr, 0, offsetof(NetDev, mac)
+VLAN.Id, config_parse_vlanid, 0, offsetof(VLan, id)
+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)
+MACVLAN.Mode, config_parse_macvlan_mode, 0, offsetof(MacVlan, mode)
+MACVLAN.SourceMACAddress, config_parse_hwaddrs, 0, offsetof(MacVlan, match_source_mac)
+MACVTAP.Mode, config_parse_macvlan_mode, 0, offsetof(MacVlan, mode)
+MACVTAP.SourceMACAddress, config_parse_hwaddrs, 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_address, 0, offsetof(Tunnel, local)
+Tunnel.Remote, config_parse_tunnel_address, 0, offsetof(Tunnel, remote)
+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, offsetof(Tunnel, ipv6_flowlabel)
+Tunnel.CopyDSCP, config_parse_bool, 0, offsetof(Tunnel, copy_dscp)
+Tunnel.EncapsulationLimit, config_parse_encap_limit, 0, offsetof(Tunnel, encap_limit)
+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.ERSPANIndex, config_parse_uint32, 0, offsetof(Tunnel, erspan_index)
+Tunnel.SerializeTunneledPackets, config_parse_tristate, 0, offsetof(Tunnel, gre_erspan_sequence)
+Tunnel.ISATAP, config_parse_tristate, 0, offsetof(Tunnel, isatap)
+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_address, 0, offsetof(L2tpTunnel, local)
+L2TP.Remote, config_parse_l2tp_tunnel_address, 0, offsetof(L2tpTunnel, remote)
+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_hwaddr, 0, offsetof(Veth, mac_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, 0, offsetof(TunTap, user_name)
+Tun.Group, config_parse_string, 0, offsetof(TunTap, group_name)
+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, 0, offsetof(TunTap, user_name)
+Tap.Group, config_parse_string, 0, offsetof(TunTap, group_name)
+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
+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
+Xfrm.InterfaceId, config_parse_uint32, 0, offsetof(Xfrm, if_id)
+Xfrm.Independent, config_parse_bool, 0, offsetof(Xfrm, independent)
diff --git a/src/network/netdev/netdev.c b/src/network/netdev/netdev.c
new file mode 100644
index 0000000..9f390b5
--- /dev/null
+++ b/src/network/netdev/netdev.c
@@ -0,0 +1,868 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "bareudp.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 "ipvlan.h"
+#include "l2tp-tunnel.h"
+#include "list.h"
+#include "macsec.h"
+#include "macvlan.h"
+#include "netdev.h"
+#include "netdevsim.h"
+#include "netlink-util.h"
+#include "network-internal.h"
+#include "networkd-manager.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 "xfrm.h"
+
+const NetDevVTable * const netdev_vtable[_NETDEV_KIND_MAX] = {
+ [NETDEV_KIND_BRIDGE] = &bridge_vtable,
+ [NETDEV_KIND_BOND] = &bond_vtable,
+ [NETDEV_KIND_VLAN] = &vlan_vtable,
+ [NETDEV_KIND_MACVLAN] = &macvlan_vtable,
+ [NETDEV_KIND_MACVTAP] = &macvtap_vtable,
+ [NETDEV_KIND_IPVLAN] = &ipvlan_vtable,
+ [NETDEV_KIND_IPVTAP] = &ipvtap_vtable,
+ [NETDEV_KIND_VXLAN] = &vxlan_vtable,
+ [NETDEV_KIND_IPIP] = &ipip_vtable,
+ [NETDEV_KIND_GRE] = &gre_vtable,
+ [NETDEV_KIND_GRETAP] = &gretap_vtable,
+ [NETDEV_KIND_IP6GRE] = &ip6gre_vtable,
+ [NETDEV_KIND_IP6GRETAP] = &ip6gretap_vtable,
+ [NETDEV_KIND_SIT] = &sit_vtable,
+ [NETDEV_KIND_VTI] = &vti_vtable,
+ [NETDEV_KIND_VTI6] = &vti6_vtable,
+ [NETDEV_KIND_VETH] = &veth_vtable,
+ [NETDEV_KIND_DUMMY] = &dummy_vtable,
+ [NETDEV_KIND_TUN] = &tun_vtable,
+ [NETDEV_KIND_TAP] = &tap_vtable,
+ [NETDEV_KIND_IP6TNL] = &ip6tnl_vtable,
+ [NETDEV_KIND_VRF] = &vrf_vtable,
+ [NETDEV_KIND_VCAN] = &vcan_vtable,
+ [NETDEV_KIND_GENEVE] = &geneve_vtable,
+ [NETDEV_KIND_VXCAN] = &vxcan_vtable,
+ [NETDEV_KIND_WIREGUARD] = &wireguard_vtable,
+ [NETDEV_KIND_NETDEVSIM] = &netdevsim_vtable,
+ [NETDEV_KIND_FOU] = &foutnl_vtable,
+ [NETDEV_KIND_ERSPAN] = &erspan_vtable,
+ [NETDEV_KIND_L2TP] = &l2tptnl_vtable,
+ [NETDEV_KIND_MACSEC] = &macsec_vtable,
+ [NETDEV_KIND_NLMON] = &nlmon_vtable,
+ [NETDEV_KIND_XFRM] = &xfrm_vtable,
+ [NETDEV_KIND_IFB] = &ifb_vtable,
+ [NETDEV_KIND_BAREUDP] = &bare_udp_vtable,
+};
+
+static const char* const netdev_kind_table[_NETDEV_KIND_MAX] = {
+ [NETDEV_KIND_BAREUDP] = "bareudp",
+ [NETDEV_KIND_BRIDGE] = "bridge",
+ [NETDEV_KIND_BOND] = "bond",
+ [NETDEV_KIND_VLAN] = "vlan",
+ [NETDEV_KIND_MACVLAN] = "macvlan",
+ [NETDEV_KIND_MACVTAP] = "macvtap",
+ [NETDEV_KIND_IPVLAN] = "ipvlan",
+ [NETDEV_KIND_IPVTAP] = "ipvtap",
+ [NETDEV_KIND_VXLAN] = "vxlan",
+ [NETDEV_KIND_IPIP] = "ipip",
+ [NETDEV_KIND_GRE] = "gre",
+ [NETDEV_KIND_GRETAP] = "gretap",
+ [NETDEV_KIND_IP6GRE] = "ip6gre",
+ [NETDEV_KIND_IP6GRETAP] = "ip6gretap",
+ [NETDEV_KIND_SIT] = "sit",
+ [NETDEV_KIND_VETH] = "veth",
+ [NETDEV_KIND_VTI] = "vti",
+ [NETDEV_KIND_VTI6] = "vti6",
+ [NETDEV_KIND_DUMMY] = "dummy",
+ [NETDEV_KIND_TUN] = "tun",
+ [NETDEV_KIND_TAP] = "tap",
+ [NETDEV_KIND_IP6TNL] = "ip6tnl",
+ [NETDEV_KIND_VRF] = "vrf",
+ [NETDEV_KIND_VCAN] = "vcan",
+ [NETDEV_KIND_GENEVE] = "geneve",
+ [NETDEV_KIND_VXCAN] = "vxcan",
+ [NETDEV_KIND_WIREGUARD] = "wireguard",
+ [NETDEV_KIND_NETDEVSIM] = "netdevsim",
+ [NETDEV_KIND_FOU] = "fou",
+ [NETDEV_KIND_ERSPAN] = "erspan",
+ [NETDEV_KIND_L2TP] = "l2tp",
+ [NETDEV_KIND_MACSEC] = "macsec",
+ [NETDEV_KIND_NLMON] = "nlmon",
+ [NETDEV_KIND_XFRM] = "xfrm",
+ [NETDEV_KIND_IFB] = "ifb",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(netdev_kind, NetDevKind);
+
+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 = data;
+
+ assert(rvalue);
+ assert(data);
+
+ k = netdev_kind_from_string(rvalue);
+ if (k < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "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;
+}
+
+static void netdev_callbacks_clear(NetDev *netdev) {
+ netdev_join_callback *callback;
+
+ if (!netdev)
+ return;
+
+ while ((callback = netdev->callbacks)) {
+ LIST_REMOVE(callbacks, netdev->callbacks, callback);
+ link_unref(callback->link);
+ free(callback);
+ }
+}
+
+bool netdev_is_managed(NetDev *netdev) {
+ if (!netdev || !netdev->manager || !netdev->ifname)
+ return false;
+
+ return hashmap_get(netdev->manager->netdevs, netdev->ifname) == netdev;
+}
+
+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_callbacks_clear(netdev);
+
+ netdev_detach_from_manager(netdev);
+
+ free(netdev->filename);
+
+ free(netdev->description);
+ free(netdev->ifname);
+ free(netdev->mac);
+ 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 || netdev->state == NETDEV_STATE_LINGER)
+ return;
+
+ netdev->state = NETDEV_STATE_LINGER;
+
+ log_netdev_debug(netdev, "netdev removed");
+
+ netdev_callbacks_clear(netdev);
+
+ 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) {
+ *ret = NULL;
+ return -ENOENT;
+ }
+
+ *ret = netdev;
+
+ return 0;
+}
+
+static int netdev_enter_failed(NetDev *netdev) {
+ netdev->state = NETDEV_STATE_FAILED;
+
+ netdev_callbacks_clear(netdev);
+
+ return 0;
+}
+
+static int netdev_enslave_ready(NetDev *netdev, Link* link, link_netlink_message_handler_t callback) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(netdev);
+ assert(netdev->state == NETDEV_STATE_READY);
+ assert(netdev->manager);
+ assert(netdev->manager->rtnl);
+ assert(IN_SET(netdev->kind, NETDEV_KIND_BRIDGE, NETDEV_KIND_BOND, NETDEV_KIND_VRF));
+ assert(link);
+ assert(callback);
+
+ if (link->flags & IFF_UP && netdev->kind == NETDEV_KIND_BOND) {
+ log_netdev_debug(netdev, "Link '%s' was up when attempting to enslave it. Bringing link down.", link->ifname);
+ r = link_down(link, NULL);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not bring link down: %m");
+ }
+
+ r = sd_rtnl_message_new_link(netdev->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_netlink_message_append_u32(req, IFLA_MASTER, netdev->ifindex);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_MASTER attribute: %m");
+
+ r = netlink_call_async(netdev->manager->rtnl, NULL, req, callback,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not send rtnetlink message: %m");
+
+ link_ref(link);
+
+ log_netdev_debug(netdev, "Enslaving link '%s'", link->ifname);
+
+ return 0;
+}
+
+static int netdev_enter_ready(NetDev *netdev) {
+ netdev_join_callback *callback, *callback_next;
+ int r;
+
+ assert(netdev);
+ assert(netdev->ifname);
+
+ if (netdev->state != NETDEV_STATE_CREATING)
+ return 0;
+
+ netdev->state = NETDEV_STATE_READY;
+
+ log_netdev_info(netdev, "netdev ready");
+
+ LIST_FOREACH_SAFE(callbacks, callback, callback_next, netdev->callbacks) {
+ /* enslave the links that were attempted to be enslaved before the
+ * link was ready */
+ r = netdev_enslave_ready(netdev, callback->link, callback->callback);
+ if (r < 0)
+ return r;
+
+ LIST_REMOVE(callbacks, netdev->callbacks, callback);
+ link_unref(callback->link);
+ free(callback);
+ }
+
+ if (NETDEV_VTABLE(netdev)->post_create)
+ NETDEV_VTABLE(netdev)->post_create(netdev, NULL, 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_drop(netdev);
+
+ return 1;
+ }
+
+ log_netdev_debug(netdev, "Created");
+
+ return 1;
+}
+
+static int netdev_enslave(NetDev *netdev, Link *link, link_netlink_message_handler_t callback) {
+ int r;
+
+ assert(netdev);
+ assert(netdev->manager);
+ assert(netdev->manager->rtnl);
+ assert(IN_SET(netdev->kind, NETDEV_KIND_BRIDGE, NETDEV_KIND_BOND, NETDEV_KIND_VRF));
+
+ if (netdev->state == NETDEV_STATE_READY) {
+ r = netdev_enslave_ready(netdev, link, callback);
+ if (r < 0)
+ return r;
+ } else if (IN_SET(netdev->state, NETDEV_STATE_LINGER, NETDEV_STATE_FAILED)) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+
+ r = rtnl_message_new_synthetic_error(netdev->manager->rtnl, -ENODEV, 0, &m);
+ if (r >= 0)
+ callback(netdev->manager->rtnl, m, link);
+ } else {
+ /* the netdev is not yet ready, save this request for when it is */
+ netdev_join_callback *cb;
+
+ cb = new(netdev_join_callback, 1);
+ if (!cb)
+ return log_oom();
+
+ *cb = (netdev_join_callback) {
+ .callback = callback,
+ .link = link_ref(link),
+ };
+
+ LIST_PREPEND(callbacks, netdev->callbacks, cb);
+
+ log_netdev_debug(netdev, "Will enslave '%s', when ready", link->ifname);
+ }
+
+ return 0;
+}
+
+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) {
+ log_netdev_error(netdev, "Cannot set ifindex from unexpected rtnl message type.");
+ return -EINVAL;
+ }
+
+ 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 r;
+ }
+
+ 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 r;
+ }
+
+ 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_get_mac(const char *ifname, struct ether_addr **ret) {
+ _cleanup_free_ struct ether_addr *mac = NULL;
+ uint64_t result;
+ size_t l, sz;
+ uint8_t *v;
+ int r;
+
+ assert(ifname);
+ assert(ret);
+
+ mac = new0(struct ether_addr, 1);
+ if (!mac)
+ return -ENOMEM;
+
+ l = strlen(ifname);
+ sz = sizeof(sd_id128_t) + l;
+ v = newa(uint8_t, sz);
+
+ /* fetch some persistent data unique to the machine */
+ r = sd_id128_get_machine((sd_id128_t*) v);
+ if (r < 0)
+ return r;
+
+ /* combine with some data unique (on this machine) to this
+ * netdev */
+ memcpy(v + sizeof(sd_id128_t), ifname, l);
+
+ /* Let's hash the host machine ID plus the container name. We
+ * use a fixed, but originally randomly created hash key here. */
+ result = siphash24(v, sz, HASH_KEY.bytes);
+
+ assert_cc(ETH_ALEN <= sizeof(result));
+ memcpy(mac->ether_addr_octet, &result, ETH_ALEN);
+
+ /* see eth_random_addr in the kernel */
+ mac->ether_addr_octet[0] &= 0xfe; /* clear multicast bit */
+ mac->ether_addr_octet[0] |= 0x02; /* set local assignment bit (IEEE802) */
+
+ *ret = TAKE_PTR(mac);
+
+ return 0;
+}
+
+static int netdev_create(NetDev *netdev, Link *link, link_netlink_message_handler_t callback) {
+ int r;
+
+ assert(netdev);
+ assert(!link || callback);
+
+ /* create netdev */
+ if (NETDEV_VTABLE(netdev)->create) {
+ assert(!link);
+
+ r = NETDEV_VTABLE(netdev)->create(netdev);
+ if (r < 0)
+ return r;
+
+ log_netdev_debug(netdev, "Created");
+ } else {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+
+ r = sd_rtnl_message_new_link(netdev->manager->rtnl, &m, RTM_NEWLINK, 0);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not allocate RTM_NEWLINK message: %m");
+
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, netdev->ifname);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IFNAME, attribute: %m");
+
+ if (netdev->mac) {
+ r = sd_netlink_message_append_ether_addr(m, IFLA_ADDRESS, netdev->mac);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_ADDRESS attribute: %m");
+ }
+
+ if (netdev->mtu) {
+ r = sd_netlink_message_append_u32(m, IFLA_MTU, netdev->mtu);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_MTU attribute: %m");
+ }
+
+ if (link) {
+ r = sd_netlink_message_append_u32(m, IFLA_LINK, link->ifindex);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINK attribute: %m");
+ }
+
+ r = sd_netlink_message_open_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, netdev_kind_to_string(netdev->kind));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ if (NETDEV_VTABLE(netdev)->fill_message_create) {
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_LINKINFO attribute: %m");
+
+ if (link) {
+ r = netlink_call_async(netdev->manager->rtnl, NULL, m, callback,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not send rtnetlink message: %m");
+
+ link_ref(link);
+ } else {
+ r = netlink_call_async(netdev->manager->rtnl, NULL, m, netdev_create_handler,
+ netdev_destroy_callback, netdev);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not send rtnetlink message: %m");
+
+ netdev_ref(netdev);
+ }
+
+ netdev->state = NETDEV_STATE_CREATING;
+
+ log_netdev_debug(netdev, "Creating");
+ }
+
+ return 0;
+}
+
+static int netdev_create_after_configured(NetDev *netdev, Link *link) {
+ assert(netdev);
+ assert(link);
+ assert(NETDEV_VTABLE(netdev)->create_after_configured);
+
+ return NETDEV_VTABLE(netdev)->create_after_configured(netdev, link);
+}
+
+/* the callback must be called, possibly after a timeout, as otherwise the Link will hang */
+int netdev_join(NetDev *netdev, Link *link, link_netlink_message_handler_t callback) {
+ int r;
+
+ assert(netdev);
+ assert(netdev->manager);
+ assert(netdev->manager->rtnl);
+
+ switch (netdev_get_create_type(netdev)) {
+ case NETDEV_CREATE_MASTER:
+ r = netdev_enslave(netdev, link, callback);
+ if (r < 0)
+ return r;
+
+ break;
+ case NETDEV_CREATE_STACKED:
+ r = netdev_create(netdev, link, callback);
+ if (r < 0)
+ return r;
+
+ break;
+ case NETDEV_CREATE_AFTER_CONFIGURED:
+ r = netdev_create_after_configured(netdev, link);
+ if (r < 0)
+ return r;
+ break;
+ default:
+ assert_not_reached("Cannot join independent netdev");
+ }
+
+ return 0;
+}
+
+int netdev_load_one(Manager *manager, const char *filename) {
+ _cleanup_(netdev_unrefp) NetDev *netdev_raw = NULL, *netdev = NULL;
+ _cleanup_fclose_ FILE *file = NULL;
+ const char *dropin_dirname;
+ bool independent = false;
+ int r;
+
+ assert(manager);
+ assert(filename);
+
+ file = fopen(filename, "re");
+ if (!file) {
+ if (errno == ENOENT)
+ return 0;
+
+ return -errno;
+ }
+
+ if (null_or_empty_fd(fileno(file))) {
+ 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(
+ 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);
+ if (r < 0)
+ return r;
+
+ /* 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) {
+ log_warning("NetDev has no Kind= configured in %s. Ignoring", filename);
+ return 0;
+ }
+
+ if (!netdev_raw->ifname) {
+ log_warning("NetDev without Name= configured in %s. Ignoring", filename);
+ return 0;
+ }
+
+ r = fseek(file, 0, SEEK_SET);
+ if (r < 0)
+ return -errno;
+
+ 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(
+ filename, NETWORK_DIRS, dropin_dirname,
+ NETDEV_VTABLE(netdev)->sections,
+ config_item_perf_lookup, network_netdev_gperf_lookup,
+ CONFIG_PARSE_WARN,
+ netdev, NULL);
+ if (r < 0)
+ return r;
+
+ /* verify configuration */
+ if (NETDEV_VTABLE(netdev)->config_verify) {
+ r = NETDEV_VTABLE(netdev)->config_verify(netdev, filename);
+ if (r < 0)
+ return 0;
+ }
+
+ netdev->filename = strdup(filename);
+ if (!netdev->filename)
+ return log_oom();
+
+ if (!netdev->mac && NETDEV_VTABLE(netdev)->generate_mac) {
+ r = netdev_get_mac(netdev->ifname, &netdev->mac);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r,
+ "Failed to generate predictable MAC address for %s: %m",
+ netdev->ifname);
+ }
+
+ r = hashmap_ensure_allocated(&netdev->manager->netdevs, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(netdev->manager->netdevs, netdev->ifname, netdev);
+ 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,
+ "The setting Name=%s in %s conflicts with the one in %s, ignoring",
+ netdev->ifname, netdev->filename, n->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 0;
+ }
+ if (r < 0)
+ return r;
+
+ LIST_HEAD_INIT(netdev->callbacks);
+
+ log_netdev_debug(netdev, "loaded %s", netdev_kind_to_string(netdev->kind));
+
+ if (IN_SET(netdev_get_create_type(netdev), NETDEV_CREATE_MASTER, NETDEV_CREATE_INDEPENDENT)) {
+ r = netdev_create(netdev, NULL, NULL);
+ if (r < 0)
+ return r;
+ }
+
+ switch (netdev->kind) {
+ case NETDEV_KIND_IPIP:
+ independent = IPIP(netdev)->independent;
+ break;
+ case NETDEV_KIND_GRE:
+ independent = GRE(netdev)->independent;
+ break;
+ case NETDEV_KIND_GRETAP:
+ independent = GRETAP(netdev)->independent;
+ break;
+ case NETDEV_KIND_IP6GRE:
+ independent = IP6GRE(netdev)->independent;
+ break;
+ case NETDEV_KIND_IP6GRETAP:
+ independent = IP6GRETAP(netdev)->independent;
+ break;
+ case NETDEV_KIND_SIT:
+ independent = SIT(netdev)->independent;
+ break;
+ case NETDEV_KIND_VTI:
+ independent = VTI(netdev)->independent;
+ break;
+ case NETDEV_KIND_VTI6:
+ independent = VTI6(netdev)->independent;
+ break;
+ case NETDEV_KIND_IP6TNL:
+ independent = IP6TNL(netdev)->independent;
+ break;
+ case NETDEV_KIND_ERSPAN:
+ independent = ERSPAN(netdev)->independent;
+ break;
+ case NETDEV_KIND_XFRM:
+ independent = XFRM(netdev)->independent;
+ break;
+ case NETDEV_KIND_VXLAN:
+ independent = VXLAN(netdev)->independent;
+ break;
+ default:
+ break;
+ }
+
+ if (independent) {
+ r = netdev_create(netdev, NULL, NULL);
+ if (r < 0)
+ return r;
+ }
+
+ netdev = NULL;
+
+ return 0;
+}
+
+int netdev_load(Manager *manager, bool reload) {
+ _cleanup_strv_free_ char **files = NULL;
+ char **f;
+ 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) {
+ r = netdev_load_one(manager, *f);
+ if (r < 0)
+ log_error_errno(r, "Failed to load %s, ignoring: %m", *f);
+ }
+
+ return 0;
+}
diff --git a/src/network/netdev/netdev.h b/src/network/netdev/netdev.h
new file mode 100644
index 0000000..468fae5
--- /dev/null
+++ b/src/network/netdev/netdev.h
@@ -0,0 +1,244 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-netlink.h"
+
+#include "conf-parser.h"
+#include "list.h"
+#include "networkd-link.h"
+#include "time-util.h"
+
+#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" \
+ "-Bond\0" \
+ "-Bridge\0" \
+ "-FooOverUDP\0" \
+ "-GENEVE\0" \
+ "-IPVLAN\0" \
+ "-IPVTAP\0" \
+ "-L2TP\0" \
+ "-L2TPSession\0" \
+ "-MACsec\0" \
+ "-MACsecReceiveChannel\0" \
+ "-MACsecTransmitAssociation\0" \
+ "-MACsecReceiveAssociation\0" \
+ "-MACVTAP\0" \
+ "-MACVLAN\0" \
+ "-Tunnel\0" \
+ "-Tun\0" \
+ "-Tap\0" \
+ "-Peer\0" \
+ "-VLAN\0" \
+ "-VRF\0" \
+ "-VXCAN\0" \
+ "-VXLAN\0" \
+ "-WireGuard\0" \
+ "-WireGuardPeer\0" \
+ "-Xfrm\0"
+
+typedef struct netdev_join_callback netdev_join_callback;
+
+struct netdev_join_callback {
+ link_netlink_message_handler_t callback;
+ Link *link;
+
+ LIST_FIELDS(netdev_join_callback, callbacks);
+};
+
+typedef enum NetDevKind {
+ NETDEV_KIND_BRIDGE,
+ NETDEV_KIND_BOND,
+ NETDEV_KIND_VLAN,
+ NETDEV_KIND_MACVLAN,
+ NETDEV_KIND_MACVTAP,
+ NETDEV_KIND_IPVLAN,
+ NETDEV_KIND_IPVTAP,
+ NETDEV_KIND_VXLAN,
+ NETDEV_KIND_IPIP,
+ NETDEV_KIND_GRE,
+ NETDEV_KIND_GRETAP,
+ NETDEV_KIND_IP6GRE,
+ NETDEV_KIND_IP6GRETAP,
+ NETDEV_KIND_SIT,
+ NETDEV_KIND_VETH,
+ NETDEV_KIND_VTI,
+ NETDEV_KIND_VTI6,
+ NETDEV_KIND_IP6TNL,
+ NETDEV_KIND_DUMMY,
+ NETDEV_KIND_TUN,
+ NETDEV_KIND_TAP,
+ NETDEV_KIND_VRF,
+ NETDEV_KIND_VCAN,
+ NETDEV_KIND_GENEVE,
+ NETDEV_KIND_VXCAN,
+ NETDEV_KIND_WIREGUARD,
+ NETDEV_KIND_NETDEVSIM,
+ NETDEV_KIND_FOU,
+ NETDEV_KIND_ERSPAN,
+ NETDEV_KIND_L2TP,
+ NETDEV_KIND_MACSEC,
+ NETDEV_KIND_NLMON,
+ NETDEV_KIND_XFRM,
+ NETDEV_KIND_IFB,
+ NETDEV_KIND_BAREUDP,
+ _NETDEV_KIND_MAX,
+ _NETDEV_KIND_TUNNEL, /* Used by config_parse_stacked_netdev() */
+ _NETDEV_KIND_INVALID = -1
+} 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 = -1,
+} NetDevState;
+
+typedef enum NetDevCreateType {
+ NETDEV_CREATE_INDEPENDENT,
+ NETDEV_CREATE_MASTER,
+ NETDEV_CREATE_STACKED,
+ NETDEV_CREATE_AFTER_CONFIGURED,
+ _NETDEV_CREATE_MAX,
+ _NETDEV_CREATE_INVALID = -1,
+} 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 ether_addr *mac;
+ uint32_t mtu;
+ int ifindex;
+
+ LIST_HEAD(netdev_join_callback, callbacks);
+} 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 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;
+
+ /* create netdev, if not done via rtnl */
+ int (*create)(NetDev *netdev);
+
+ /* create netdev after link is fully configured */
+ int (*create_after_configured)(NetDev *netdev, Link *link);
+
+ /* perform additional configuration after netdev has been createad */
+ int (*post_create)(NetDev *netdev, Link *link, sd_netlink_message *message);
+
+ /* verify that compulsory configuration options were specified */
+ int (*config_verify)(NetDev *netdev, const char *filename);
+
+ /* Generate MAC address or not When MACAddress= is not specified. */
+ bool generate_mac;
+} 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);
+
+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_get_mac(const char *ifname, struct ether_addr **ret);
+int netdev_join(NetDev *netdev, Link *link, link_netlink_message_handler_t cb);
+int netdev_join_after_configured(NetDev *netdev, Link *link, link_netlink_message_handler_t callback);
+
+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);
+
+/* 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(netdev, level, error, ...) \
+ ({ \
+ const NetDev *_n = (netdev); \
+ _n ? log_object_internal(level, error, PROJECT_FILE, __LINE__, __func__, "INTERFACE=", _n->ifname, NULL, NULL, ##__VA_ARGS__) : \
+ log_internal(level, error, PROJECT_FILE, __LINE__, __func__, ##__VA_ARGS__); \
+ })
+
+#define log_netdev_debug(netdev, ...) log_netdev_full(netdev, LOG_DEBUG, 0, ##__VA_ARGS__)
+#define log_netdev_info(netdev, ...) log_netdev_full(netdev, LOG_INFO, 0, ##__VA_ARGS__)
+#define log_netdev_notice(netdev, ...) log_netdev_full(netdev, LOG_NOTICE, 0, ##__VA_ARGS__)
+#define log_netdev_warning(netdev, ...) log_netdev_full(netdev, LOG_WARNING, 0, ## __VA_ARGS__)
+#define log_netdev_error(netdev, ...) log_netdev_full(netdev, LOG_ERR, 0, ##__VA_ARGS__)
+
+#define log_netdev_debug_errno(netdev, error, ...) log_netdev_full(netdev, LOG_DEBUG, error, ##__VA_ARGS__)
+#define log_netdev_info_errno(netdev, error, ...) log_netdev_full(netdev, LOG_INFO, error, ##__VA_ARGS__)
+#define log_netdev_notice_errno(netdev, error, ...) log_netdev_full(netdev, LOG_NOTICE, error, ##__VA_ARGS__)
+#define log_netdev_warning_errno(netdev, error, ...) log_netdev_full(netdev, LOG_WARNING, error, ##__VA_ARGS__)
+#define log_netdev_error_errno(netdev, error, ...) log_netdev_full(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..b281428
--- /dev/null
+++ b/src/network/netdev/netdevsim.c
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "netdevsim.h"
+
+const NetDevVTable netdevsim_vtable = {
+ .object_size = sizeof(NetDevSim),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .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..a8faed5
--- /dev/null
+++ b/src/network/netdev/nlmon.c
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "nlmon.h"
+
+static int netdev_nlmon_verify(NetDev *netdev, const char *filename) {
+ assert(netdev);
+ assert(filename);
+
+ if (netdev->mac) {
+ log_netdev_warning(netdev, "%s: MACAddress= is not supported. Ignoring", filename);
+ netdev->mac = mfree(netdev->mac);
+ }
+
+ return 0;
+}
+
+const NetDevVTable nlmon_vtable = {
+ .object_size = sizeof(NLMon),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .config_verify = netdev_nlmon_verify,
+};
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..66e8868
--- /dev/null
+++ b/src/network/netdev/tunnel.c
@@ -0,0 +1,903 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/fou.h>
+#include <linux/ip.h>
+#include <linux/if_tunnel.h>
+#include <linux/ip6_tunnel.h>
+
+#include "conf-parser.h"
+#include "missing_network.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "tunnel.h"
+#include "util.h"
+
+#define DEFAULT_TNL_HOP_LIMIT 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");
+
+static int netdev_ipip_sit_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ Tunnel *t;
+ int r;
+
+ assert(netdev);
+
+ if (netdev->kind == NETDEV_KIND_IPIP)
+ t = IPIP(netdev);
+ else
+ t = SIT(netdev);
+
+ assert(m);
+ assert(t);
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_LINK attribute: %m");
+ }
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_IPTUN_LOCAL, &t->local.in);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_LOCAL attribute: %m");
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_IPTUN_REMOTE, &t->remote.in);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_REMOTE attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_TTL, t->ttl);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_TTL attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_PMTUDISC, t->pmtudisc);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_PMTUDISC attribute: %m");
+
+ if (t->fou_tunnel) {
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_TYPE, t->fou_encap_type);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_ENCAP_TYPE attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_SPORT, htobe16(t->encap_src_port));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_ENCAP_SPORT attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_DPORT, htobe16(t->fou_destination_port));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_ENCAP_DPORT attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_6RD_PREFIX attribute: %m");
+
+ /* 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_6RD_PREFIXLEN attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_FLAGS attribute: %m");
+ }
+ }
+
+ return r;
+}
+
+static int netdev_gre_erspan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ 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("invalid netdev kind");
+ }
+
+ assert(t);
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_LINK attribute: %m");
+ }
+
+ if (netdev->kind == NETDEV_KIND_ERSPAN) {
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_ERSPAN_INDEX, t->erspan_index);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_ERSPAN_INDEX attribute: %m");
+ }
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_GRE_LOCAL, &t->local.in);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_LOCAL attribute: %m");
+
+ r = sd_netlink_message_append_in_addr(m, IFLA_GRE_REMOTE, &t->remote.in);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_REMOTE attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_TTL, t->ttl);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_TTL attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_TOS, t->tos);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_TOS attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_PMTUDISC, t->pmtudisc);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_PMTUDISC attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_IKEY attribute: %m");
+
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_OKEY, okey);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_OKEY attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_IFLAGS, iflags);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_IFLAGS attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_OFLAGS, oflags);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_OFLAGS, attribute: %m");
+
+ if (t->fou_tunnel) {
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_TYPE, t->fou_encap_type);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_ENCAP_TYPE attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_SPORT, htobe16(t->encap_src_port));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_ENCAP_SPORT attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_DPORT, htobe16(t->fou_destination_port));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_ENCAP_DPORT attribute: %m");
+ }
+
+ return r;
+}
+
+static int netdev_ip6gre_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ uint32_t ikey = 0;
+ uint32_t okey = 0;
+ uint16_t iflags = 0;
+ uint16_t oflags = 0;
+ Tunnel *t;
+ int r;
+
+ assert(netdev);
+
+ if (netdev->kind == NETDEV_KIND_IP6GRE)
+ t = IP6GRE(netdev);
+ else
+ t = IP6GRETAP(netdev);
+
+ assert(t);
+ assert(m);
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_LINK attribute: %m");
+ }
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_GRE_LOCAL, &t->local.in6);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_LOCAL attribute: %m");
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_GRE_REMOTE, &t->remote.in6);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_REMOTE attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_GRE_TTL, t->ttl);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_TTL attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_FLOWINFO attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_FLAGS, t->flags);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_FLAGS attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_IKEY attribute: %m");
+
+ r = sd_netlink_message_append_u32(m, IFLA_GRE_OKEY, okey);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_OKEY attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_IFLAGS, iflags);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_IFLAGS attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_GRE_OFLAGS, oflags);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_GRE_OFLAGS, attribute: %m");
+
+ return r;
+}
+
+static int netdev_vti_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VTI_LINK attribute: %m");
+ }
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VTI_IKEY attribute: %m");
+
+ r = sd_netlink_message_append_u32(m, IFLA_VTI_OKEY, okey);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VTI_OKEY attribute: %m");
+
+ r = netlink_message_append_in_addr_union(m, IFLA_VTI_LOCAL, t->family, &t->local);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VTI_LOCAL attribute: %m");
+
+ r = netlink_message_append_in_addr_union(m, IFLA_VTI_REMOTE, t->family, &t->remote);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VTI_REMOTE attribute: %m");
+
+ return r;
+}
+
+static int netdev_ip6tnl_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ Tunnel *t = IP6TNL(netdev);
+ uint8_t proto;
+ int r;
+
+ assert(netdev);
+ assert(m);
+ assert(t);
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_LINK attribute: %m");
+ }
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_LOCAL, &t->local.in6);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_LOCAL attribute: %m");
+
+ r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_REMOTE, &t->remote.in6);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_REMOTE attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_TTL, t->ttl);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_TTL attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_FLOWINFO attribute: %m");
+ }
+
+ 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);
+
+ if (t->encap_limit != IPV6_DEFAULT_TNL_ENCAP_LIMIT) {
+ r = sd_netlink_message_append_u8(m, IFLA_IPTUN_ENCAP_LIMIT, t->encap_limit);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_ENCAP_LIMIT attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_IPTUN_FLAGS, t->flags);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_FLAGS attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_IPTUN_PROTO attribute: %m");
+
+ return r;
+}
+
+static int netdev_tunnel_verify(NetDev *netdev, const char *filename) {
+ Tunnel *t = NULL;
+
+ assert(netdev);
+ assert(filename);
+
+ switch (netdev->kind) {
+ case NETDEV_KIND_IPIP:
+ t = IPIP(netdev);
+ break;
+ case NETDEV_KIND_SIT:
+ t = SIT(netdev);
+ break;
+ case NETDEV_KIND_GRE:
+ t = GRE(netdev);
+ break;
+ case NETDEV_KIND_GRETAP:
+ t = GRETAP(netdev);
+ break;
+ case NETDEV_KIND_IP6GRE:
+ t = IP6GRE(netdev);
+ break;
+ case NETDEV_KIND_IP6GRETAP:
+ t = IP6GRETAP(netdev);
+ break;
+ case NETDEV_KIND_VTI:
+ t = VTI(netdev);
+ break;
+ case NETDEV_KIND_VTI6:
+ t = VTI6(netdev);
+ break;
+ case NETDEV_KIND_IP6TNL:
+ t = IP6TNL(netdev);
+ break;
+ case NETDEV_KIND_ERSPAN:
+ t = ERSPAN(netdev);
+ break;
+ default:
+ assert_not_reached("Invalid tunnel kind");
+ }
+
+ assert(t);
+
+ 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_null(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_null(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 (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->fou_tunnel && t->fou_destination_port <= 0)
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "FooOverUDP missing port configured in %s. Ignoring", filename);
+
+ if (netdev->kind == NETDEV_KIND_ERSPAN && (t->erspan_index >= (1 << 20) || t->erspan_index == 0))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), "Invalid erspan index %d. Ignoring", t->erspan_index);
+
+ /* 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;
+
+ return 0;
+}
+
+int config_parse_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) {
+ Tunnel *t = userdata;
+ union in_addr_union *addr = data, buffer;
+ int r, f;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ /* This is used to parse addresses on both local and remote ends of the tunnel.
+ * Address families must match.
+ *
+ * "any" is a special value which means that the address is unspecified.
+ */
+
+ if (streq(rvalue, "any")) {
+ *addr = IN_ADDR_NULL;
+
+ /* As a special case, if both the local and remote addresses are
+ * unspecified, also clear the address family.
+ */
+ if (t->family != AF_UNSPEC &&
+ in_addr_is_null(t->family, &t->local) != 0 &&
+ in_addr_is_null(t->family, &t->remote) != 0)
+ t->family = AF_UNSPEC;
+ return 0;
+ }
+
+ 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 (t->family != AF_UNSPEC && t->family != f) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Tunnel addresses incompatible, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ t->family = f;
+ *addr = 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) {
+ union in_addr_union buffer;
+ Tunnel *t = userdata;
+ uint32_t k;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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, 0, "Failed to parse tunnel key ignoring assignment: %s", rvalue);
+ return 0;
+ }
+ } else
+ k = be32toh(buffer.in.s_addr);
+
+ if (streq(lvalue, "Key"))
+ t->key = k;
+ else if (streq(lvalue, "InputKey"))
+ t->ikey = k;
+ else
+ t->okey = 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) {
+ IPv6FlowLabel *ipv6_flowlabel = data;
+ Tunnel *t = userdata;
+ int k = 0;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(ipv6_flowlabel);
+
+ if (streq(rvalue, "inherit")) {
+ *ipv6_flowlabel = IP6_FLOWINFO_FLOWLABEL;
+ t->flags |= IP6_TNL_F_USE_ORIG_FLOWLABEL;
+ } else {
+ r = config_parse_int(unit, filename, line, section, section_line, lvalue, ltype, rvalue, &k, userdata);
+ if (r < 0)
+ return r;
+
+ if (k > 0xFFFFF)
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Failed to parse IPv6 flowlabel option, ignoring: %s", rvalue);
+ else {
+ *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 = userdata;
+ int k = 0;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (streq(rvalue, "none"))
+ t->flags |= IP6_TNL_F_IGN_ENCAP_LIMIT;
+ else {
+ r = safe_atoi(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse Tunnel Encapsulation Limit option, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (k > 255 || k < 0)
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid Tunnel Encapsulation value, ignoring: %d", k);
+ else {
+ 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;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ union in_addr_union p;
+ uint8_t l;
+ int r;
+
+ 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;
+}
+
+static void ipip_sit_init(NetDev *n) {
+ Tunnel *t;
+
+ assert(n);
+
+ switch (n->kind) {
+ case NETDEV_KIND_IPIP:
+ t = IPIP(n);
+ break;
+ case NETDEV_KIND_SIT:
+ t = SIT(n);
+ break;
+ default:
+ assert_not_reached("invalid netdev kind");
+ }
+
+ assert(t);
+
+ t->pmtudisc = true;
+ t->fou_encap_type = NETDEV_FOO_OVER_UDP_ENCAP_DIRECT;
+ t->isatap = -1;
+}
+
+static void vti_init(NetDev *n) {
+ Tunnel *t;
+
+ assert(n);
+
+ if (n->kind == NETDEV_KIND_VTI)
+ t = VTI(n);
+ else
+ t = VTI6(n);
+
+ assert(t);
+
+ t->pmtudisc = true;
+}
+
+static void gre_erspan_init(NetDev *n) {
+ Tunnel *t;
+
+ assert(n);
+
+ switch (n->kind) {
+ case NETDEV_KIND_GRE:
+ t = GRE(n);
+ break;
+ case NETDEV_KIND_ERSPAN:
+ t = ERSPAN(n);
+ break;
+ case NETDEV_KIND_GRETAP:
+ t = GRETAP(n);
+ break;
+ default:
+ assert_not_reached("invalid netdev kind");
+ }
+
+ assert(t);
+
+ t->pmtudisc = true;
+ t->gre_erspan_sequence = -1;
+ t->fou_encap_type = NETDEV_FOO_OVER_UDP_ENCAP_DIRECT;
+}
+
+static void ip6gre_init(NetDev *n) {
+ Tunnel *t;
+
+ assert(n);
+
+ if (n->kind == NETDEV_KIND_IP6GRE)
+ t = IP6GRE(n);
+ else
+ t = IP6GRETAP(n);
+
+ assert(t);
+
+ t->ttl = DEFAULT_TNL_HOP_LIMIT;
+}
+
+static void ip6tnl_init(NetDev *n) {
+ Tunnel *t = IP6TNL(n);
+
+ assert(n);
+ assert(t);
+
+ t->ttl = DEFAULT_TNL_HOP_LIMIT;
+ 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;
+}
+
+const NetDevVTable ipip_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = ipip_sit_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ipip_sit_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable sit_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = ipip_sit_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ipip_sit_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable vti_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = vti_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_vti_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable vti6_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = vti_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_vti_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable gre_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = gre_erspan_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_gre_erspan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable gretap_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = gre_erspan_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_gre_erspan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable ip6gre_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = ip6gre_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ip6gre_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable ip6gretap_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = ip6gre_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ip6gre_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable ip6tnl_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = ip6tnl_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_ip6tnl_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
+
+const NetDevVTable erspan_vtable = {
+ .object_size = sizeof(Tunnel),
+ .init = gre_erspan_init,
+ .sections = NETDEV_COMMON_SECTIONS "Tunnel\0",
+ .fill_message_create = netdev_gre_erspan_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED,
+ .config_verify = netdev_tunnel_verify,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/tunnel.h b/src/network/netdev/tunnel.h
new file mode 100644
index 0000000..d58ded7
--- /dev/null
+++ b/src/network/netdev/tunnel.h
@@ -0,0 +1,92 @@
+/* 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.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 = -1,
+} Ip6TnlMode;
+
+typedef enum IPv6FlowLabel {
+ NETDEV_IPV6_FLOWLABEL_INHERIT = 0xFFFFF + 1,
+ _NETDEV_IPV6_FLOWLABEL_MAX,
+ _NETDEV_IPV6_FLOWLABEL_INVALID = -1,
+} 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;
+ uint32_t erspan_index;
+
+ 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;
+
+ uint16_t encap_src_port;
+ uint16_t fou_destination_port;
+
+ struct in6_addr sixrd_prefix;
+ uint8_t sixrd_prefixlen;
+} Tunnel;
+
+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);
+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_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);
diff --git a/src/network/netdev/tuntap.c b/src/network/netdev/tuntap.c
new file mode 100644
index 0000000..d9d6544
--- /dev/null
+++ b/src/network/netdev/tuntap.c
@@ -0,0 +1,164 @@
+/* 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 "fd-util.h"
+#include "tuntap.h"
+#include "user-util.h"
+
+#define TUN_DEV "/dev/net/tun"
+
+static int netdev_fill_tuntap_message(NetDev *netdev, struct ifreq *ifr) {
+ TunTap *t;
+
+ assert(netdev);
+ assert(netdev->ifname);
+ assert(ifr);
+
+ if (netdev->kind == NETDEV_KIND_TAP) {
+ t = TAP(netdev);
+ ifr->ifr_flags |= IFF_TAP;
+ } else {
+ t = TUN(netdev);
+ 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);
+
+ return 0;
+}
+
+static int netdev_tuntap_add(NetDev *netdev, struct ifreq *ifr) {
+ _cleanup_close_ int fd;
+ TunTap *t = NULL;
+ const char *user;
+ const char *group;
+ uid_t uid;
+ gid_t gid;
+ int r;
+
+ assert(netdev);
+ assert(ifr);
+
+ 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 (ioctl(fd, TUNSETIFF, ifr) < 0)
+ return log_netdev_error_errno(netdev, -errno, "TUNSETIFF failed on tun dev: %m");
+
+ if (netdev->kind == NETDEV_KIND_TAP)
+ t = TAP(netdev);
+ else
+ t = TUN(netdev);
+
+ assert(t);
+
+ if (t->user_name) {
+ user = t->user_name;
+
+ 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 on tun dev: %m");
+ }
+
+ if (t->group_name) {
+ group = t->group_name;
+
+ 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 on tun dev: %m");
+
+ }
+
+ if (ioctl(fd, TUNSETPERSIST, 1) < 0)
+ return log_netdev_error_errno(netdev, -errno, "TUNSETPERSIST failed on tun dev: %m");
+
+ return 0;
+}
+
+static int netdev_create_tuntap(NetDev *netdev) {
+ struct ifreq ifr = {};
+ int r;
+
+ r = netdev_fill_tuntap_message(netdev, &ifr);
+ if (r < 0)
+ return r;
+
+ return netdev_tuntap_add(netdev, &ifr);
+}
+
+static void tuntap_done(NetDev *netdev) {
+ TunTap *t = NULL;
+
+ assert(netdev);
+
+ if (netdev->kind == NETDEV_KIND_TUN)
+ t = TUN(netdev);
+ else
+ t = TAP(netdev);
+
+ assert(t);
+
+ 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->mac)
+ 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,
+ .done = tuntap_done,
+ .create = netdev_create_tuntap,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+};
+
+const NetDevVTable tap_vtable = {
+ .object_size = sizeof(TunTap),
+ .sections = NETDEV_COMMON_SECTIONS "Tap\0",
+ .config_verify = tuntap_verify,
+ .done = tuntap_done,
+ .create = netdev_create_tuntap,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+};
diff --git a/src/network/netdev/tuntap.h b/src/network/netdev/tuntap.h
new file mode 100644
index 0000000..4d1e643
--- /dev/null
+++ b/src/network/netdev/tuntap.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct TunTap TunTap;
+
+#include "netdev.h"
+
+struct TunTap {
+ NetDev meta;
+
+ char *user_name;
+ char *group_name;
+ bool multi_queue;
+ bool packet_info;
+ bool vnet_hdr;
+};
+
+DEFINE_NETDEV_CAST(TUN, TunTap);
+DEFINE_NETDEV_CAST(TAP, TunTap);
+extern const NetDevVTable tun_vtable;
+extern const NetDevVTable tap_vtable;
diff --git a/src/network/netdev/vcan.c b/src/network/netdev/vcan.c
new file mode 100644
index 0000000..3621d4c
--- /dev/null
+++ b/src/network/netdev/vcan.c
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "vcan.h"
+
+const NetDevVTable vcan_vtable = {
+ .object_size = sizeof(VCan),
+ .sections = NETDEV_COMMON_SECTIONS,
+ .create_type = NETDEV_CREATE_INDEPENDENT,
+ .generate_mac = true,
+};
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..840a327
--- /dev/null
+++ b/src/network/netdev/veth.c
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <net/if.h>
+#include <linux/veth.h>
+
+#include "veth.h"
+
+static int netdev_veth_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ 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 log_netdev_error_errno(netdev, r, "Could not append VETH_INFO_PEER attribute: %m");
+
+ if (v->ifname_peer) {
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, v->ifname_peer);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to add netlink interface name: %m");
+ }
+
+ if (v->mac_peer) {
+ r = sd_netlink_message_append_ether_addr(m, IFLA_ADDRESS, v->mac_peer);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_ADDRESS attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ return r;
+}
+
+static int netdev_veth_verify(NetDev *netdev, const char *filename) {
+ Veth *v;
+ int r;
+
+ assert(netdev);
+ assert(filename);
+
+ v = VETH(netdev);
+
+ assert(v);
+
+ if (!v->ifname_peer) {
+ log_netdev_warning(netdev, "Veth NetDev without peer name configured in %s. Ignoring",
+ filename);
+ return -EINVAL;
+ }
+
+ if (!v->mac_peer) {
+ r = netdev_get_mac(v->ifname_peer, &v->mac_peer);
+ if (r < 0) {
+ log_netdev_warning(netdev,
+ "Failed to generate predictable MAC address for %s. Ignoring",
+ v->ifname_peer);
+ return -EINVAL;
+ }
+ }
+
+ return 0;
+}
+
+static void veth_done(NetDev *n) {
+ Veth *v;
+
+ assert(n);
+
+ v = VETH(n);
+
+ assert(v);
+
+ free(v->ifname_peer);
+ free(v->mac_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,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/veth.h b/src/network/netdev/veth.h
new file mode 100644
index 0000000..643f737
--- /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 ether_addr *mac_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..e7f03f0
--- /dev/null
+++ b/src/network/netdev/vlan.c
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <net/if.h>
+#include <linux/if_vlan.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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VLAN_ID attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VLAN_FLAGS attribute: %m");
+
+ return 0;
+}
+
+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_init(NetDev *netdev) {
+ VLan *v = VLAN(netdev);
+
+ assert(netdev);
+ assert(v);
+
+ v->id = VLANID_INVALID;
+ 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,
+};
diff --git a/src/network/netdev/vlan.h b/src/network/netdev/vlan.h
new file mode 100644
index 0000000..9dff924
--- /dev/null
+++ b/src/network/netdev/vlan.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct VLan VLan;
+
+#include "netdev.h"
+
+struct VLan {
+ NetDev meta;
+
+ uint16_t id;
+
+ int gvrp;
+ int mvrp;
+ int loose_binding;
+ int reorder_hdr;
+};
+
+DEFINE_NETDEV_CAST(VLAN, VLan);
+extern const NetDevVTable vlan_vtable;
diff --git a/src/network/netdev/vrf.c b/src/network/netdev/vrf.c
new file mode 100644
index 0000000..ae71ae9
--- /dev/null
+++ b/src/network/netdev/vrf.c
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.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 log_netdev_error_errno(netdev, r, "Could not append IPLA_VRF_TABLE attribute: %m");
+
+ return r;
+}
+
+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_MASTER,
+ .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..e4e32ff
--- /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 "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 log_netdev_error_errno(netdev, r, "Could not append VXCAN_INFO_PEER attribute: %m");
+
+ if (v->ifname_peer) {
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, v->ifname_peer);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Failed to add vxcan netlink interface peer name: %m");
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append VXCAN_INFO_PEER attribute: %m");
+
+ return r;
+}
+
+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) {
+ log_netdev_warning(netdev, "VxCan NetDev without peer name configured in %s. Ignoring", filename);
+ return -EINVAL;
+ }
+
+ 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,
+ .generate_mac = true,
+};
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..6748f67
--- /dev/null
+++ b/src/network/netdev/vxlan.c
@@ -0,0 +1,390 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.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 netdev_vxlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ VxLan *v;
+ int r;
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_ID attribute: %m");
+ }
+
+ if (in_addr_is_null(v->group_family, &v->group) == 0) {
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_GROUP attribute: %m");
+ } else if (in_addr_is_null(v->remote_family, &v->remote) == 0) {
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_GROUP attribute: %m");
+ }
+
+ if (in_addr_is_null(v->local_family, &v->local) == 0) {
+ if (v->local_family == AF_INET)
+ r = sd_netlink_message_append_in_addr(m, IFLA_VXLAN_LOCAL, &v->local.in);
+ else
+ r = sd_netlink_message_append_in6_addr(m, IFLA_VXLAN_LOCAL6, &v->local.in6);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_LOCAL attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LINK, link ? link->ifindex : 0);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_LINK attribute: %m");
+
+ if (v->inherit) {
+ r = sd_netlink_message_append_flag(m, IFLA_VXLAN_TTL_INHERIT);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_TTL_INHERIT attribute: %m");
+ } else {
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_TTL, v->ttl);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_TTL attribute: %m");
+ }
+
+ if (v->tos != 0) {
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_TOS, v->tos);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_TOS attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_LEARNING, v->learning);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_LEARNING attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_RSC, v->route_short_circuit);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_RSC attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_PROXY, v->arp_proxy);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_PROXY attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_L2MISS, v->l2miss);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_L2MISS attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_L3MISS, v->l3miss);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_L3MISS attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_AGEING attribute: %m");
+ }
+
+ if (v->max_fdb != 0) {
+ r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LIMIT, v->max_fdb);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_LIMIT attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_CSUM, v->udpcsum);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_UDP_CSUM attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_ZERO_CSUM6_TX, v->udp6zerocsumtx);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_UDP_ZERO_CSUM6_TX attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_ZERO_CSUM6_RX, v->udp6zerocsumrx);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_UDP_ZERO_CSUM6_RX attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_REMCSUM_TX, v->remote_csum_tx);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_REMCSUM_TX attribute: %m");
+
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_REMCSUM_RX, v->remote_csum_rx);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_REMCSUM_RX attribute: %m");
+
+ r = sd_netlink_message_append_u16(m, IFLA_VXLAN_PORT, htobe16(v->dest_port));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_PORT attribute: %m");
+
+ 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 log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_PORT_RANGE attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LABEL, htobe32(v->flow_label));
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_LABEL attribute: %m");
+
+ if (v->group_policy) {
+ r = sd_netlink_message_append_flag(m, IFLA_VXLAN_GBP);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_GBP attribute: %m");
+ }
+
+ if (v->generic_protocol_extension) {
+ r = sd_netlink_message_append_flag(m, IFLA_VXLAN_GPE);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_GPE attribute: %m");
+ }
+
+ if (v->df != _NETDEV_VXLAN_DF_INVALID) {
+ r = sd_netlink_message_append_u8(m, IFLA_VXLAN_DF, v->df);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_VXLAN_DF attribute: %m");
+ }
+
+ return r;
+}
+
+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 = 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, "vxlan '%s' address is invalid, 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, "vxlan %s invalid multicast address, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ v->group_family = f;
+ } else {
+ if (r > 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "vxlan %s cannot be a multicast address, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "Remote"))
+ v->remote_family = f;
+ else
+ v->local_family = f;
+ }
+
+ *addr = buffer;
+
+ 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_null(v->group_family, &v->group) == 0 && in_addr_is_null(v->remote_family, &v->remote) == 0)
+ return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: VXLAN both 'Group=' and 'Remote=' cannot be specified. Ignoring.",
+ filename);
+
+ return 0;
+}
+
+static void vxlan_init(NetDev *netdev) {
+ VxLan *v;
+
+ assert(netdev);
+
+ v = VXLAN(netdev);
+
+ assert(v);
+
+ 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,
+ .config_verify = netdev_vxlan_verify,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/vxlan.h b/src/network/netdev/vxlan.h
new file mode 100644
index 0000000..371653c
--- /dev/null
+++ b/src/network/netdev/vxlan.h
@@ -0,0 +1,74 @@
+/* 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.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 = -1
+} VxLanDF;
+
+struct VxLan {
+ NetDev meta;
+
+ uint32_t vni;
+
+ int remote_family;
+ int local_family;
+ int group_family;
+
+ VxLanDF df;
+
+ union in_addr_union remote;
+ union in_addr_union local;
+ 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..416e9b9
--- /dev/null
+++ b/src/network/netdev/wireguard.c
@@ -0,0 +1,946 @@
+/* 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 "sd-resolve.h"
+
+#include "alloc-util.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-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "resolve-private.h"
+#include "string-util.h"
+#include "strv.h"
+#include "wireguard.h"
+
+static void resolve_endpoints(NetDev *netdev);
+
+static void wireguard_peer_free(WireguardPeer *peer) {
+ WireguardIPmask *mask;
+
+ if (!peer)
+ return;
+
+ if (peer->wireguard) {
+ LIST_REMOVE(peers, peer->wireguard->peers, peer);
+
+ set_remove(peer->wireguard->peers_with_unresolved_endpoint, peer);
+ set_remove(peer->wireguard->peers_with_failed_endpoint, peer);
+
+ if (peer->section)
+ hashmap_remove(peer->wireguard->peers_by_section, peer->section);
+ }
+
+ network_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);
+
+ free(peer);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(WireguardPeer, wireguard_peer_free);
+
+static int wireguard_peer_new_static(Wireguard *w, const char *filename, unsigned section_line, WireguardPeer **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(wireguard_peer_freep) WireguardPeer *peer = NULL;
+ int r;
+
+ assert(w);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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_allocated(&w->peers_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(w->peers_by_section, 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 *mask, *start;
+ 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)
+ 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 = mask; /* Start next cycle from this mask. */
+ return !mask;
+
+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, *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, SD_GENL_WIREGUARD, 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");
+
+ 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)
+ break;
+ }
+ peer_start = peer; /* 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 void wireguard_peer_destroy_callback(WireguardPeer *peer) {
+ NetDev *netdev;
+
+ assert(peer);
+ assert(peer->wireguard);
+
+ netdev = NETDEV(peer->wireguard);
+
+ if (section_is_invalid(peer->section))
+ wireguard_peer_free(peer);
+
+ netdev_unref(netdev);
+}
+
+static int on_resolve_retry(sd_event_source *s, usec_t usec, void *userdata) {
+ NetDev *netdev = userdata;
+ Wireguard *w;
+
+ assert(netdev);
+ w = WIREGUARD(netdev);
+ assert(w);
+
+ if (!netdev_is_managed(netdev))
+ return 0;
+
+ assert(set_isempty(w->peers_with_unresolved_endpoint));
+
+ SWAP_TWO(w->peers_with_unresolved_endpoint, w->peers_with_failed_endpoint);
+
+ resolve_endpoints(netdev);
+
+ return 0;
+}
+
+/*
+ * Given the number of retries this function will return will an exponential
+ * increasing time in milliseconds to wait starting at 200ms and capped at 25 seconds.
+ */
+static int exponential_backoff_milliseconds(unsigned n_retries) {
+ return (2 << MIN(n_retries, 7U)) * 100 * USEC_PER_MSEC;
+}
+
+static int wireguard_resolve_handler(sd_resolve_query *q,
+ int ret,
+ const struct addrinfo *ai,
+ WireguardPeer *peer) {
+ NetDev *netdev;
+ Wireguard *w;
+ int r;
+
+ assert(peer);
+ assert(peer->wireguard);
+
+ w = peer->wireguard;
+ netdev = NETDEV(w);
+
+ if (!netdev_is_managed(netdev))
+ return 0;
+
+ if (ret != 0) {
+ log_netdev_error(netdev, "Failed to resolve host '%s:%s': %s", peer->endpoint_host, peer->endpoint_port, gai_strerror(ret));
+
+ r = set_ensure_put(&w->peers_with_failed_endpoint, NULL, peer);
+ if (r < 0) {
+ log_netdev_error(netdev, "Failed to save a peer, dropping the peer: %m");
+ peer->section->invalid = true;
+ goto resolve_next;
+ }
+
+ } else if ((ai->ai_family == AF_INET && ai->ai_addrlen == sizeof(struct sockaddr_in)) ||
+ (ai->ai_family == AF_INET6 && ai->ai_addrlen == sizeof(struct sockaddr_in6)))
+ memcpy(&peer->endpoint, ai->ai_addr, ai->ai_addrlen);
+ else
+ log_netdev_error(netdev, "Neither IPv4 nor IPv6 address found for peer endpoint %s:%s, ignoring the address.",
+ peer->endpoint_host, peer->endpoint_port);
+
+resolve_next:
+ if (!set_isempty(w->peers_with_unresolved_endpoint)) {
+ resolve_endpoints(netdev);
+ return 0;
+ }
+
+ (void) wireguard_set_interface(netdev);
+
+ if (!set_isempty(w->peers_with_failed_endpoint)) {
+ usec_t usec;
+
+ w->n_retries++;
+ usec = usec_add(now(CLOCK_MONOTONIC), exponential_backoff_milliseconds(w->n_retries));
+ r = event_reset_time(netdev->manager->event, &w->resolve_retry_event_source,
+ CLOCK_MONOTONIC, usec, 0, on_resolve_retry, netdev,
+ 0, "wireguard-resolve-retry", true);
+ if (r < 0) {
+ log_netdev_warning_errno(netdev, r, "Could not arm resolve retry handler: %m");
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static void resolve_endpoints(NetDev *netdev) {
+ static const struct addrinfo hints = {
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_DGRAM,
+ .ai_protocol = IPPROTO_UDP
+ };
+ WireguardPeer *peer;
+ Wireguard *w;
+ int r;
+
+ assert(netdev);
+ w = WIREGUARD(netdev);
+ assert(w);
+
+ SET_FOREACH(peer, w->peers_with_unresolved_endpoint) {
+ r = resolve_getaddrinfo(netdev->manager->resolve,
+ NULL,
+ peer->endpoint_host,
+ peer->endpoint_port,
+ &hints,
+ wireguard_resolve_handler,
+ wireguard_peer_destroy_callback,
+ peer);
+ if (r == -ENOBUFS)
+ break;
+ if (r < 0) {
+ log_netdev_error_errno(netdev, r, "Failed to create resolver: %m");
+ continue;
+ }
+
+ /* Avoid freeing netdev. It will be unrefed by the destroy callback. */
+ netdev_ref(netdev);
+
+ (void) set_remove(w->peers_with_unresolved_endpoint, peer);
+ }
+}
+
+static int netdev_wireguard_post_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+ assert(netdev);
+ assert(WIREGUARD(netdev));
+
+ (void) wireguard_set_interface(netdev);
+ 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 = data;
+ int r;
+
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ 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;
+ }
+
+ ipmask = new(WireguardIPmask, 1);
+ if (!ipmask)
+ return log_oom();
+
+ *ipmask = (WireguardIPmask) {
+ .family = family,
+ .ip.in6 = addr.in6,
+ .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;
+ const char *begin, *end;
+ Wireguard *w;
+ size_t len;
+ int r;
+
+ assert(data);
+ assert(rvalue);
+
+ w = WIREGUARD(data);
+ assert(w);
+
+ if (rvalue[0] == '[') {
+ begin = &rvalue[1];
+ end = strchr(rvalue, ']');
+ if (!end) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Unable to find matching brace of endpoint, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ len = end - begin;
+ ++end;
+ if (*end != ':' || !*(end + 1)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Unable to find port of endpoint, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ ++end;
+ } else {
+ begin = rvalue;
+ end = strrchr(rvalue, ':');
+ if (!end || !*(end + 1)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Unable to find port of endpoint, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ len = end - begin;
+ ++end;
+ }
+
+ r = wireguard_peer_new_static(w, filename, section_line, &peer);
+ if (r < 0)
+ return log_oom();
+
+ r = free_and_strndup(&peer->endpoint_host, begin, len);
+ if (r < 0)
+ return log_oom();
+
+ r = free_and_strdup(&peer->endpoint_port, end);
+ if (r < 0)
+ return log_oom();
+
+ r = set_ensure_put(&w->peers_with_unresolved_endpoint, NULL, peer);
+ 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;
+}
+
+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);
+
+ sd_event_source_unref(w->resolve_retry_event_source);
+
+ 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->peers_with_unresolved_endpoint);
+ set_free(w->peers_with_failed_endpoint);
+}
+
+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);
+
+ (void) warn_file_is_world_accessible(filename, NULL, NULL, 0);
+
+ r = read_full_file_full(
+ AT_FDCWD, filename,
+ 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) {
+ WireguardPeer *peer, *peer_next;
+ 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. Dropping network device %s.",
+ w->private_key_file, netdev->ifname);
+
+ if (eqzero(w->private_key))
+ return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: Missing PrivateKey= or PrivateKeyFile=, "
+ "Dropping network device %s.",
+ filename, netdev->ifname);
+
+ LIST_FOREACH_SAFE(peers, peer, peer_next, w->peers)
+ if (wireguard_peer_verify(peer) < 0)
+ wireguard_peer_free(peer);
+
+ 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,
+ .generate_mac = true,
+};
diff --git a/src/network/netdev/wireguard.h b/src/network/netdev/wireguard.h
new file mode 100644
index 0000000..b9b5ae9
--- /dev/null
+++ b/src/network/netdev/wireguard.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#pragma once
+
+typedef struct Wireguard Wireguard;
+
+#include <netinet/in.h>
+#include <linux/wireguard.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;
+ NetworkConfigSection *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;
+
+ 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;
+ Set *peers_with_unresolved_endpoint;
+ Set *peers_with_failed_endpoint;
+
+ LIST_HEAD(WireguardPeer, peers);
+
+ unsigned n_retries;
+ sd_event_source *resolve_retry_event_source;
+};
+
+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);
diff --git a/src/network/netdev/xfrm.c b/src/network/netdev/xfrm.c
new file mode 100644
index 0000000..a407c54
--- /dev/null
+++ b/src/network/netdev/xfrm.c
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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(link || x->independent);
+
+ r = sd_netlink_message_append_u32(message, IFLA_XFRM_LINK, link ? link->ifindex : LOOPBACK_IFINDEX);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_XFRM_LINK: %m");
+
+ r = sd_netlink_message_append_u32(message, IFLA_XFRM_IF_ID, x->if_id);
+ if (r < 0)
+ return log_netdev_error_errno(netdev, r, "Could not append IFLA_XFRM_IF_ID: %m");
+
+ return 0;
+}
+
+const NetDevVTable xfrm_vtable = {
+ .object_size = sizeof(Xfrm),
+ .sections = NETDEV_COMMON_SECTIONS "Xfrm\0",
+ .fill_message_create = xfrm_fill_message_create,
+ .create_type = NETDEV_CREATE_STACKED
+};
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..63a90bc
--- /dev/null
+++ b/src/network/networkctl.c
@@ -0,0 +1,2830 @@
+/* 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.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 "netlink-util.h"
+#include "network-internal.h"
+#include "pager.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "set.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 void operational_state_to_color(const char *name, const char *state, const char **on, const char **off) {
+ assert(on);
+ assert(off);
+
+ if (STRPTR_IN_SET(state, "routable", "enslaved") ||
+ (streq_ptr(name, "lo") && streq_ptr(state, "carrier"))) {
+ *on = ansi_highlight_green();
+ *off = ansi_normal();
+ } else if (streq_ptr(state, "degraded")) {
+ *on = ansi_highlight_yellow();
+ *off = ansi_normal();
+ } else
+ *on = *off = "";
+}
+
+static void setup_state_to_color(const char *state, const char **on, const char **off) {
+ assert(on);
+ assert(off);
+
+ if (streq_ptr(state, "configured")) {
+ *on = ansi_highlight_green();
+ *off = ansi_normal();
+ } else if (streq_ptr(state, "configuring")) {
+ *on = ansi_highlight_yellow();
+ *off = ansi_normal();
+ } else if (STRPTR_IN_SET(state, "failed", "linger")) {
+ *on = ansi_highlight_red();
+ *off = ansi_normal();
+ } else
+ *on = *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 inerit;
+ 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[NETDEV_KIND_MAX];
+ sd_device *sd_device;
+ int ifindex;
+ unsigned short iftype;
+ hw_addr_data hw_address;
+ struct ether_addr permanent_mac_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_mac_address:1;
+ bool has_permanent_mac_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 const 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].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) {
+ const char *received_kind;
+ 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(m, IFLA_INFO_KIND, &received_kind);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_enter_container(m, IFLA_INFO_DATA);
+ if (r < 0)
+ return r;
+
+ if (streq(received_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(received_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(received_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(received_kind, "vlan"))
+ (void) sd_netlink_message_read_u16(m, IFLA_VLAN_ID, &info->vlan_id);
+ else if (STR_IN_SET(received_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(received_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(received_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(received_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(received_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(received_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(received_kind, "macvlan", "macvtap"))
+ (void) sd_netlink_message_read_u32(m, IFLA_MACVLAN_MODE, &info->macvlan_mode);
+ else if (streq(received_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);
+ }
+
+ strncpy(info->netdev_kind, received_kind, IFNAMSIZ);
+
+ (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;
+ char **p;
+
+ 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_mac_address =
+ netlink_message_read_hw_addr(m, IFLA_ADDRESS, &info->hw_address) >= 0 &&
+ memcmp(&info->hw_address, &HW_ADDR_NULL, sizeof(hw_addr_data)) != 0;
+
+ info->has_permanent_mac_address =
+ ethtool_get_permanent_macaddr(NULL, info->name, &info->permanent_mac_address) >= 0 &&
+ memcmp(&info->permanent_mac_address, &ETHER_ADDR_NULL, sizeof(struct ether_addr)) != 0 &&
+ (info->hw_address.length != sizeof(struct ether_addr) ||
+ memcmp(&info->permanent_mac_address, info->hw_address.addr.bytes, sizeof(struct ether_addr)) != 0);
+
+ (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) {
+ _cleanup_free_ char *path = NULL, *ifindex_str = NULL;
+ int r;
+
+ if (asprintf(&ifindex_str, "%i", link->ifindex) < 0)
+ return -ENOMEM;
+
+ 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_inc_rcvbuf(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 allocated = 0, c = 0, j;
+ sd_netlink_message *i;
+ 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_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 (i = reply; i; i = sd_netlink_message_next(i)) {
+ if (!GREEDY_REALLOC0(links, allocated, 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;
+
+ char devid[2 + DECIMAL_STR_MAX(int)];
+ xsprintf(devid, "n%i", links[c].ifindex);
+ (void) sd_device_new_from_device_id(&links[c].sd_device, devid);
+
+ 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 (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) {
+ _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, i, r;
+
+ 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;
+
+ (void) 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);
+
+ 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 (i = 0; i < c; i++) {
+ _cleanup_free_ char *setup_state = NULL, *operational_state = NULL;
+ const char *on_color_operational, *off_color_operational,
+ *on_color_setup, *off_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, &off_color_operational);
+
+ r = sd_network_link_get_setup_state(links[i].ifindex, &setup_state);
+ if (r == -ENODATA) /* If there's no info available about this iface, it's unmanaged by networkd */
+ setup_state = strdup("unmanaged");
+ setup_state_to_color(setup_state, &on_color_setup, &off_color_setup);
+
+ t = link_get_type_string(links[i].iftype, links[i].sd_device);
+
+ r = table_add_many(table,
+ TABLE_INT, links[i].ifindex,
+ TABLE_STRING, links[i].name,
+ TABLE_STRING, strna(t),
+ TABLE_STRING, strna(operational_state),
+ TABLE_SET_COLOR, on_color_operational,
+ TABLE_STRING, strna(setup_state),
+ 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;
+ sd_netlink_message *m;
+ 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_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ for (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, i;
+
+ assert(rtnl);
+ assert(table);
+
+ n = local_gateways(rtnl, ifindex, AF_UNSPEC, &local);
+ if (n <= 0)
+ return n;
+
+ for (i = 0; i < n; i++) {
+ _cleanup_free_ char *gateway = NULL, *description = NULL, *with_description = NULL;
+ char name[IF_NAMESIZE+1];
+
+ r = in_addr_to_string(local[i].family, &local[i].address, &gateway);
+ if (r < 0)
+ return r;
+
+ 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");
+
+ if (description) {
+ with_description = strjoin(gateway, " (", description, ")");
+ if (!with_description)
+ return log_oom();
+ }
+
+ /* Show interface name for the entry if we show entries for all interfaces */
+ r = strv_extendf(&buf, "%s%s%s",
+ with_description ?: gateway,
+ ifindex <= 0 ? " on " : "",
+ ifindex <= 0 ? format_ifname_full(local[i].ifindex, name, 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, i;
+
+ 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 (i = 0; i < n; i++) {
+ _cleanup_free_ char *pretty = NULL;
+ char name[IF_NAMESIZE+1];
+
+ r = in_addr_to_string(local[i].family, &local[i].address, &pretty);
+ if (r < 0)
+ return r;
+
+ if (local[i].family == AF_INET && in4_addr_equal(&local[i].address.in, &dhcp4_address)) {
+ struct in_addr server_address;
+ char *p, s[INET_ADDRSTRLEN];
+
+ r = sd_dhcp_lease_get_server_identifier(lease, &server_address);
+ if (r >= 0 && inet_ntop(AF_INET, &server_address, s, sizeof(s)))
+ p = strjoin(pretty, " (DHCP4 via ", s, ")");
+ else
+ p = strjoin(pretty, " (DHCP4)");
+ if (!p)
+ return log_oom();
+
+ free_and_replace(pretty, p);
+ }
+
+ r = strv_extendf(&buf, "%s%s%s",
+ pretty,
+ ifindex <= 0 ? " on " : "",
+ ifindex <= 0 ? format_ifname_full(local[i].ifindex, name, 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;
+ sd_netlink_message *m;
+ 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_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, (size_t) SIZE_MAX);
+ 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 (m = reply; m; m = sd_netlink_message_next(m)) {
+ _cleanup_free_ char *pretty = NULL;
+ union in_addr_union prefix = IN_ADDR_NULL;
+ 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.in6);
+ if (r < 0)
+ continue;
+
+ r = in_addr_to_string(AF_INET6, &prefix, &pretty);
+ 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", pretty, 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) {
+ _cleanup_free_ char *p = NULL;
+ FILE *f;
+
+ if (asprintf(&p, "/run/systemd/netif/lldp/%i", ifindex) < 0)
+ return -ENOMEM;
+
+ 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) {
+ unsigned c;
+ int r;
+
+ assert(prefix);
+
+ if (!ifindexes || ifindexes[0] <= 0)
+ return 0;
+
+ for (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 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 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, *lease_file = 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;
+ _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);
+
+ r = sd_network_link_get_setup_state(info->ifindex, &setup_state);
+ if (r == -ENODATA) /* If there's no info available about this iface, it's unmanaged by networkd */
+ setup_state = strdup("unmanaged");
+ 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);
+
+ 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);
+ }
+
+ t = link_get_type_string(info->iftype, info->sd_device);
+
+ (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);
+
+ if (asprintf(&lease_file, "/run/systemd/netif/leases/%d", info->ifindex) < 0)
+ return log_oom();
+
+ (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);
+
+ 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);
+ (void) table_set_align_percent(table, cell, 0);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ 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, "Type:",
+ TABLE_STRING, strna(t),
+ 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, strna(setup_state), off_color_setup);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ strv_sort(info->alternative_names);
+ r = dump_list(table, "Alternative Names:", info->alternative_names);
+ if (r < 0)
+ return r;
+
+ if (path) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Path:",
+ TABLE_STRING, path);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ if (driver) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Driver:",
+ TABLE_STRING, driver);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ if (vendor) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Vendor:",
+ TABLE_STRING, vendor);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ if (model) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Model:",
+ TABLE_STRING, model);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->has_mac_address) {
+ _cleanup_free_ char *description = NULL;
+
+ if (info->hw_address.length == ETH_ALEN)
+ (void) ieee_oui(hwdb, &info->hw_address.addr.ether, &description);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "HW Address:");
+ if (r < 0)
+ return table_log_add_error(r);
+ r = table_add_cell_stringf(table, NULL, "%s%s%s%s",
+ HW_ADDR_TO_STR(&info->hw_address),
+ description ? " (" : "",
+ strempty(description),
+ description ? ")" : "");
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->has_permanent_mac_address) {
+ _cleanup_free_ char *description = NULL;
+ char ea[ETHER_ADDR_TO_STRING_MAX];
+
+ (void) ieee_oui(hwdb, &info->permanent_mac_address, &description);
+
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "HW Permanent Address:");
+ if (r < 0)
+ return table_log_add_error(r);
+ r = table_add_cell_stringf(table, NULL, "%s%s%s%s",
+ ether_addr_to_string(&info->permanent_mac_address, ea),
+ description ? " (" : "",
+ strempty(description),
+ description ? ")" : "");
+ if (r < 0)
+ return table_log_add_error(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);
+ }
+
+ if (info->qdisc) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "QDisc:",
+ TABLE_STRING, info->qdisc);
+ if (r < 0)
+ return table_log_add_error(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));
+ } 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, jiffies_to_usec(info->miimon),
+ TABLE_EMPTY,
+ TABLE_STRING, "Updelay:",
+ TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->updelay),
+ TABLE_EMPTY,
+ TABLE_STRING, "Downdelay:",
+ TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->downdelay));
+ 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_null(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_null(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_null(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_null(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_null(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_null(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;
+ char buf[ETHER_ADDR_TO_STRING_MAX];
+
+ 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_string(&info->bssid, buf));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (info->has_bitrates) {
+ char tx[FORMAT_BYTES_MAX], rx[FORMAT_BYTES_MAX];
+
+ 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(tx, sizeof tx, info->tx_bitrate, 0),
+ format_bytes_full(rx, sizeof rx, 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, "Queue Length (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) {
+ const char *duplex = duplex_to_string(info->duplex);
+ const char *port = port_to_string(info->port);
+
+ 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) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Speed:",
+ TABLE_BPS, info->speed);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (duplex) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Duplex:",
+ TABLE_STRING, duplex);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (port) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Port:",
+ TABLE_STRING, port);
+ if (r < 0)
+ return table_log_add_error(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;
+
+ 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;
+ _cleanup_strv_free_ char **dns = NULL, **ntp = NULL, **search_domains = NULL, **route_domains = NULL;
+ const char *on_color_operational, *off_color_operational;
+ _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, &off_color_operational);
+
+ 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);
+ 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) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _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, i;
+
+ (void) pager_open(arg_pager_flags);
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect system bus: %m");
+
+ 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 (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 w, i, 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 (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 i, 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;
+
+ (void) 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, 0));
+ table_set_minimum_width(table, cell, 16);
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ table_set_minimum_width(table, cell, 17);
+
+ assert_se(cell = table_get_cell(table, 0, 2));
+ table_set_minimum_width(table, cell, 16);
+
+ assert_se(cell = table_get_cell(table, 0, 3));
+ table_set_minimum_width(table, cell, 11);
+
+ assert_se(cell = table_get_cell(table, 0, 4));
+ table_set_minimum_width(table, cell, 17);
+
+ assert_se(cell = table_get_cell(table, 0, 5));
+ table_set_minimum_width(table, cell, 16);
+
+ for (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 (;;) {
+ _cleanup_free_ char *cid = NULL, *pid = NULL, *sname = NULL, *pdesc = NULL, *capabilities = NULL;
+ const char *chassis_id = NULL, *port_id = NULL, *system_name = NULL, *port_description = NULL;
+ _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = 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 (chassis_id) {
+ cid = ellipsize(chassis_id, 17, 100);
+ if (cid)
+ chassis_id = cid;
+ }
+
+ if (port_id) {
+ pid = ellipsize(port_id, 17, 100);
+ if (pid)
+ port_id = pid;
+ }
+
+ if (system_name) {
+ sname = ellipsize(system_name, 16, 100);
+ if (sname)
+ system_name = sname;
+ }
+
+ if (port_description) {
+ pdesc = ellipsize(port_description, 16, 100);
+ if (pdesc)
+ port_description = pdesc;
+ }
+
+ 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, strna(chassis_id),
+ TABLE_STRING, strna(system_name),
+ TABLE_STRING, strna(capabilities),
+ TABLE_STRING, strna(port_id),
+ TABLE_STRING, strna(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, i;
+ 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 (i = 1; i < argc; i++) {
+ index = 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) {
+ char ifname[IF_NAMESIZE + 1];
+
+ return log_error_errno(r, "Failed to %s interface %s: %m",
+ argv[1], format_ifname_full(index, ifname, 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, i;
+ 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 (i = 1; i < argc; i++) {
+ index = 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) {
+ char ifname[IF_NAMESIZE + 1];
+
+ return log_error_errno(r, "Failed to delete interface %s: %m",
+ format_ifname_full(index, ifname, 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) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ int index, i, k = 0, r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect system bus: %m");
+
+ for (i = 1; i < argc; i++) {
+ index = 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) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ int index, i, k = 0, r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect system bus: %m");
+
+ for (i = 1; i < argc; i++) {
+ index = 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) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect system bus: %m");
+
+ 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: %m");
+
+ return 0;
+}
+
+static int verb_reconfigure(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ _cleanup_set_free_ Set *indexes = NULL;
+ int index, i, r;
+ void *p;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect system bus: %m");
+
+ indexes = set_new(NULL);
+ if (!indexes)
+ return log_oom();
+
+ for (i = 1; i < argc; i++) {
+ index = 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) {
+ char ifname[IF_NAMESIZE + 1];
+
+ return log_error_errno(r, "Failed to reconfigure network interface %s: %m",
+ format_ifname_full(index, ifname, FORMAT_IFNAME_IFINDEX));
+ }
+ }
+
+ 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 statics\n"
+ " -l --full Do not ellipsize output\n"
+ " -n --lines=INTEGER Number of journal entries to show\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,
+ };
+
+ 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' },
+ {}
+ };
+
+ int c;
+
+ 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 '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+ }
+
+ return 1;
+}
+
+static int networkctl_main(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, NULL);
+}
+
+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[]) {
+ int r;
+
+ log_setup_cli();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ warn_networkd_missing();
+
+ return networkctl_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/network/networkd-address-label.c b/src/network/networkd-address-label.c
new file mode 100644
index 0000000..f933a1d
--- /dev/null
+++ b/src/network/networkd-address-label.c
@@ -0,0 +1,242 @@
+/* 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 "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);
+ }
+
+ network_config_section_free(label->section);
+ return mfree(label);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(AddressLabel, address_label_free);
+
+static int address_label_new_static(Network *network, const char *filename, unsigned section_line, AddressLabel **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(address_label_freep) AddressLabel *label = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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),
+ };
+
+ r = hashmap_ensure_allocated(&network->address_labels_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->address_labels_by_section, label->section, label);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(label);
+ return 0;
+}
+
+static int address_label_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(rtnl);
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+ assert(link->address_label_messages > 0);
+
+ link->address_label_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ 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;
+ } else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ if (link->address_label_messages == 0)
+ log_link_debug(link, "Addresses label set");
+
+ return 1;
+}
+
+static int address_label_configure(AddressLabel *label, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(label);
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ r = sd_rtnl_message_new_addrlabel(link->manager->rtnl, &req, RTM_NEWADDRLABEL,
+ link->ifindex, AF_INET6);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_NEWADDR message: %m");
+
+ r = sd_rtnl_message_addrlabel_set_prefixlen(req, label->prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set prefixlen: %m");
+
+ r = sd_netlink_message_append_u32(req, IFAL_LABEL, label->label);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFAL_LABEL attribute: %m");
+
+ r = sd_netlink_message_append_in6_addr(req, IFA_ADDRESS, &label->in_addr.in6);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFA_ADDRESS attribute: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req,
+ address_label_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);
+
+ return 0;
+}
+
+int link_set_address_labels(Link *link) {
+ AddressLabel *label;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ HASHMAP_FOREACH(label, link->network->address_labels_by_section) {
+ r = address_label_configure(label, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set address label: %m");
+
+ link->address_label_messages++;
+ }
+
+ return 0;
+}
+
+void network_drop_invalid_address_labels(Network *network) {
+ AddressLabel *label;
+
+ assert(network);
+
+ HASHMAP_FOREACH(label, network->address_labels_by_section)
+ if (section_is_invalid(label->section))
+ 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;
+ 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, &n->in_addr, &n->prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Address label is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ n = NULL;
+
+ 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 == 0xffffffffUL) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Address label is invalid, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ n->label = k;
+ n = NULL;
+
+ return 0;
+}
diff --git a/src/network/networkd-address-label.h b/src/network/networkd-address-label.h
new file mode 100644
index 0000000..11fdd9a
--- /dev/null
+++ b/src/network/networkd-address-label.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 Network Network;
+typedef struct Link Link;
+
+typedef struct AddressLabel {
+ Network *network;
+ NetworkConfigSection *section;
+
+ unsigned char prefixlen;
+ uint32_t label;
+ union in_addr_union in_addr;
+} AddressLabel;
+
+AddressLabel *address_label_free(AddressLabel *label);
+
+void network_drop_invalid_address_labels(Network *network);
+
+int link_set_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..7e27db6
--- /dev/null
+++ b/src/network/networkd-address-pool.c
@@ -0,0 +1,190 @@
+/* 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) {
+ 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;
+ }
+
+ /* Don't clash with addresses already pulled from the pool, but not assigned yet */
+ SET_FOREACH(a, l->pool_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;
+ unsigned i;
+ 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 (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)) {
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *s = NULL;
+
+ (void) in_addr_to_string(p->family, &u, &s);
+ log_debug("Found range %s/%u", strna(s), 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..961b248
--- /dev/null
+++ b/src/network/networkd-address.c
@@ -0,0 +1,1922 @@
+/* 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-manager.h"
+#include "networkd-network.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+
+#define ADDRESSES_PER_LINK_MAX 2048U
+#define STATIC_ADDRESSES_PER_NETWORK_MAX 1024U
+
+int generate_ipv6_eui_64_address(const Link *link, struct in6_addr *ret) {
+ assert(link);
+ assert(ret);
+
+ if (link->iftype == ARPHRD_INFINIBAND) {
+ /* see RFC4391 section 8 */
+ memcpy(&ret->s6_addr[8], &link->hw_addr.addr.infiniband[12], 8);
+ ret->s6_addr[8] ^= 1 << 1;
+
+ return 0;
+ }
+
+ /* see RFC4291 section 2.5.1 */
+ ret->s6_addr[8] = link->hw_addr.addr.ether.ether_addr_octet[0];
+ ret->s6_addr[8] ^= 1 << 1;
+ ret->s6_addr[9] = link->hw_addr.addr.ether.ether_addr_octet[1];
+ ret->s6_addr[10] = link->hw_addr.addr.ether.ether_addr_octet[2];
+ ret->s6_addr[11] = 0xff;
+ ret->s6_addr[12] = 0xfe;
+ ret->s6_addr[13] = link->hw_addr.addr.ether.ether_addr_octet[3];
+ ret->s6_addr[14] = link->hw_addr.addr.ether.ether_addr_octet[4];
+ ret->s6_addr[15] = link->hw_addr.addr.ether.ether_addr_octet[5];
+
+ 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,
+ .cinfo.ifa_prefered = CACHE_INFO_INFINITY_LIFE_TIME,
+ .cinfo.ifa_valid = CACHE_INFO_INFINITY_LIFE_TIME,
+ .duplicate_address_detection = ADDRESS_FAMILY_IPV6,
+ };
+
+ *ret = TAKE_PTR(address);
+
+ return 0;
+}
+
+static int address_new_static(Network *network, const char *filename, unsigned section_line, Address **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(address_freep) Address *address = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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);
+
+ r = ordered_hashmap_ensure_allocated(&network->addresses_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(network->addresses_by_section, 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) {
+ NDiscAddress *n;
+
+ set_remove(address->link->addresses, address);
+ set_remove(address->link->addresses_foreign, address);
+ set_remove(address->link->static_addresses, address);
+ if (address->link->dhcp_address == address)
+ address->link->dhcp_address = NULL;
+ if (address->link->dhcp_address_old == address)
+ address->link->dhcp_address_old = NULL;
+ set_remove(address->link->dhcp6_addresses, address);
+ set_remove(address->link->dhcp6_addresses_old, address);
+ set_remove(address->link->dhcp6_pd_addresses, address);
+ set_remove(address->link->dhcp6_pd_addresses_old, address);
+ SET_FOREACH(n, address->link->ndisc_addresses)
+ if (n->address == address)
+ free(set_remove(address->link->ndisc_addresses, n));
+
+ if (in_addr_equal(AF_INET6, &address->in_addr, (const union in_addr_union *) &address->link->ipv6ll_address))
+ memzero(&address->link->ipv6ll_address, sizeof(struct in6_addr));
+ }
+
+ sd_ipv4acd_unref(address->acd);
+
+ network_config_section_free(address->section);
+ free(address->label);
+ return mfree(address);
+}
+
+static bool address_may_have_broadcast(const Address *a) {
+ assert(a);
+
+ /* A /31 or /32 IPv4 address does not have a broadcast address.
+ * See https://tools.ietf.org/html/rfc3021 */
+
+ return a->family == AF_INET && in4_addr_is_null(&a->in_addr_peer.in) && a->prefixlen <= 30;
+}
+
+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);
+}
+
+void address_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;
+ }
+}
+
+int address_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_HASH_OPS_WITH_KEY_DESTRUCTOR(address_hash_ops, Address, address_hash_func, address_compare_func, address_free);
+
+bool 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_copy(Address *dest, const Address *src) {
+ int r;
+
+ assert(dest);
+ assert(src);
+
+ if (src->family == AF_INET) {
+ r = free_and_strdup(&dest->label, src->label);
+ if (r < 0)
+ return r;
+ }
+
+ dest->family = src->family;
+ dest->prefixlen = src->prefixlen;
+ dest->scope = src->scope;
+ dest->flags = src->flags;
+ dest->cinfo = src->cinfo;
+ dest->in_addr = src->in_addr;
+ dest->in_addr_peer = src->in_addr_peer;
+ if (address_may_have_broadcast(src))
+ dest->broadcast = src->broadcast;
+ dest->duplicate_address_detection = src->duplicate_address_detection;
+
+ 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->link->network->ip_masquerade)
+ return 0;
+
+ if (address->family != AF_INET)
+ 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(add, AF_INET, 0, &masked, address->prefixlen, NULL, NULL, 0);
+ if (r < 0)
+ return r;
+
+ address->ip_masquerade_done = add;
+
+ return 0;
+}
+
+static int address_add_internal(Link *link, Set **addresses, const Address *in, Address **ret) {
+ _cleanup_(address_freep) Address *address = NULL;
+ int r;
+
+ assert(link);
+ assert(addresses);
+ assert(in);
+
+ r = address_new(&address);
+ if (r < 0)
+ return r;
+
+ r = address_copy(address, in);
+ if (r < 0)
+ return r;
+
+ /* Consider address tentative until we get the real flags from the kernel */
+ address->flags = IFA_F_TENTATIVE;
+
+ r = set_ensure_put(addresses, &address_hash_ops, address);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ address->link = link;
+
+ if (ret)
+ *ret = address;
+ TAKE_PTR(address);
+ return 0;
+}
+
+static int address_add_foreign(Link *link, const Address *in, Address **ret) {
+ return address_add_internal(link, &link->addresses_foreign, in, ret);
+}
+
+static int address_add(Link *link, const Address *in, Address **ret) {
+ Address *address;
+ int r;
+
+ assert(link);
+ assert(in);
+
+ r = address_get(link, in, &address);
+ if (r == -ENOENT) {
+ /* Address does not exist, create a new one */
+ r = address_add_internal(link, &link->addresses, in, &address);
+ if (r < 0)
+ return r;
+ } else if (r == 0) {
+ /* Take over a foreign address */
+ r = set_ensure_put(&link->addresses, &address_hash_ops, address);
+ if (r < 0)
+ return r;
+
+ set_remove(link->addresses_foreign, address);
+ } else if (r == 1) {
+ /* Already exists, do nothing */
+ ;
+ } else
+ return r;
+
+ if (ret)
+ *ret = address;
+
+ return 0;
+}
+
+static int address_update(Address *address, const Address *src) {
+ bool ready;
+ int r;
+
+ assert(address);
+ assert(address->link);
+ assert(src);
+
+ ready = address_is_ready(address);
+
+ address->flags = src->flags;
+ address->scope = src->scope;
+ address->cinfo = src->cinfo;
+
+ if (IN_SET(address->link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ link_update_operstate(address->link, true);
+ link_check_ready(address->link);
+
+ if (!ready && address_is_ready(address)) {
+ if (address->callback) {
+ r = address->callback(address);
+ if (r < 0)
+ return r;
+ }
+
+ if (address->family == AF_INET6 &&
+ in_addr_is_link_local(AF_INET6, &address->in_addr) > 0 &&
+ IN6_IS_ADDR_UNSPECIFIED(&address->link->ipv6ll_address) > 0) {
+
+ r = link_ipv6ll_gained(address->link, &address->in_addr.in6);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+static int address_drop(Address *address) {
+ Link *link;
+ bool ready;
+ int r;
+
+ assert(address);
+
+ 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_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) {
+ if (ret)
+ *ret = existing;
+ return 1;
+ }
+
+ existing = set_get(link->addresses_foreign, in);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static bool address_exists_internal(Set *addresses, int family, const union in_addr_union *in_addr) {
+ Address *address;
+
+ SET_FOREACH(address, addresses) {
+ if (address->family != family)
+ continue;
+ if (in_addr_equal(address->family, &address->in_addr, in_addr))
+ return true;
+ }
+
+ return false;
+}
+
+bool address_exists(Link *link, int family, const union in_addr_union *in_addr) {
+ assert(link);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(in_addr);
+
+ if (address_exists_internal(link->addresses, family, in_addr))
+ return true;
+ if (address_exists_internal(link->addresses_foreign, family, in_addr))
+ return true;
+ return false;
+}
+
+static int address_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EADDRNOTAVAIL)
+ log_link_message_warning_errno(link, m, r, "Could not drop address");
+ else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ return 1;
+}
+
+int address_remove(
+ const Address *address,
+ Link *link,
+ link_netlink_message_handler_t callback) {
+
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(address);
+ assert(IN_SET(address->family, AF_INET, AF_INET6));
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *b = NULL;
+
+ (void) in_addr_to_string(address->family, &address->in_addr, &b);
+ log_link_debug(link, "Removing address %s", strna(b));
+ }
+
+ r = sd_rtnl_message_new_addr(link->manager->rtnl, &req, RTM_DELADDR,
+ link->ifindex, address->family);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_DELADDR message: %m");
+
+ r = sd_rtnl_message_addr_set_prefixlen(req, address->prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set prefixlen: %m");
+
+ r = netlink_message_append_in_addr_union(req, IFA_LOCAL, address->family, &address->in_addr);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFA_LOCAL attribute: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req,
+ callback ?: address_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);
+
+ return 0;
+}
+
+static bool link_is_static_address_configured(const Link *link, const Address *address) {
+ Address *net_address;
+
+ assert(link);
+ assert(address);
+
+ if (!link->network)
+ return false;
+
+ ORDERED_HASHMAP_FOREACH(net_address, link->network->addresses_by_section)
+ if (address_equal(net_address, address))
+ return true;
+
+ return false;
+}
+
+static bool link_address_is_dynamic(const Link *link, const Address *address) {
+ Route *route;
+
+ assert(link);
+ assert(address);
+
+ if (address->cinfo.ifa_prefered != CACHE_INFO_INFINITY_LIFE_TIME)
+ 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_foreign) {
+ 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;
+}
+
+static int link_enumerate_ipv6_tentative_addresses(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL;
+ sd_netlink_message *addr;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ r = sd_rtnl_message_new_addr(link->manager->rtnl, &req, RTM_GETADDR, 0, AF_INET6);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(link->manager->rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ for (addr = reply; addr; addr = sd_netlink_message_next(addr)) {
+ unsigned char flags;
+ int ifindex;
+
+ r = sd_rtnl_message_addr_get_ifindex(addr, &ifindex);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: invalid ifindex, ignoring: %m");
+ continue;
+ } else if (link->ifindex != ifindex)
+ continue;
+
+ r = sd_rtnl_message_addr_get_flags(addr, &flags);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received address message with invalid flags, ignoring: %m");
+ continue;
+ } else if (!(flags & IFA_F_TENTATIVE))
+ continue;
+
+ log_link_debug(link, "Found tentative ipv6 link-local address");
+ (void) manager_rtnl_process_address(link->manager->rtnl, addr, link->manager);
+ }
+
+ return 0;
+}
+
+int link_drop_foreign_addresses(Link *link) {
+ Address *address;
+ int k, r = 0;
+
+ assert(link);
+
+ /* The kernel doesn't notify us about tentative addresses;
+ * so if ipv6ll is disabled, we need to enumerate them now so we can drop them below */
+ if (!link_ipv6ll_enabled(link)) {
+ r = link_enumerate_ipv6_tentative_addresses(link);
+ if (r < 0)
+ return r;
+ }
+
+ SET_FOREACH(address, link->addresses_foreign) {
+ /* we consider IPv6LL addresses to be managed by the kernel */
+ if (address->family == AF_INET6 && in_addr_is_link_local(AF_INET6, &address->in_addr) == 1 && link_ipv6ll_enabled(link))
+ continue;
+
+ if (link_address_is_dynamic(link, address)) {
+ if (link->network && FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP))
+ continue;
+ } else if (link->network && FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_STATIC))
+ continue;
+
+ if (link_is_static_address_configured(link, address)) {
+ k = address_add(link, address, NULL);
+ if (k < 0) {
+ log_link_error_errno(link, k, "Failed to add address: %m");
+ if (r >= 0)
+ r = k;
+ }
+ } else {
+ k = address_remove(address, link, NULL);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+ }
+
+ return r;
+}
+
+static int remove_static_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+ assert(link->address_remove_messages > 0);
+
+ link->address_remove_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EADDRNOTAVAIL)
+ log_link_message_warning_errno(link, m, r, "Could not drop address");
+ else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ if (link->address_remove_messages == 0 && link->request_static_addresses) {
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ r = link_set_addresses(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+int link_drop_addresses(Link *link) {
+ Address *address, *pool_address;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses) {
+ /* we consider IPv6LL addresses to be managed by the kernel */
+ if (address->family == AF_INET6 && in_addr_is_link_local(AF_INET6, &address->in_addr) == 1 && link_ipv6ll_enabled(link))
+ continue;
+
+ k = address_remove(address, link, remove_static_address_handler);
+ if (k < 0 && r >= 0) {
+ r = k;
+ continue;
+ }
+
+ link->address_remove_messages++;
+
+ SET_FOREACH(pool_address, link->pool_addresses)
+ if (address_equal(address, pool_address))
+ address_free(set_remove(link->pool_addresses, pool_address));
+ }
+
+ return r;
+}
+
+static int address_acquire(Link *link, const Address *original, Address **ret) {
+ union in_addr_union in_addr = IN_ADDR_NULL;
+ struct in_addr broadcast = {};
+ _cleanup_(address_freep) Address *na = NULL;
+ int r;
+
+ assert(link);
+ assert(original);
+ assert(ret);
+
+ /* Something useful was configured? just use it */
+ r = in_addr_is_null(original->family, &original->in_addr);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ *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;
+
+ if (original->family == AF_INET) {
+ /* Pick first address in range for ourselves ... */
+ in_addr.in.s_addr = in_addr.in.s_addr | htobe32(1);
+
+ /* .. and use last as broadcast address */
+ if (original->prefixlen > 30)
+ broadcast.s_addr = 0;
+ else
+ broadcast.s_addr = in_addr.in.s_addr | htobe32(0xFFFFFFFFUL >> original->prefixlen);
+ } else if (original->family == AF_INET6)
+ in_addr.in6.s6_addr[15] |= 1;
+
+ r = address_new(&na);
+ if (r < 0)
+ return r;
+
+ r = address_copy(na, original);
+ if (r < 0)
+ return r;
+
+ na->broadcast = broadcast;
+ na->in_addr = in_addr;
+
+ r = set_ensure_put(&link->pool_addresses, &address_hash_ops, na);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ *ret = TAKE_PTR(na);
+ return 1;
+}
+
+static int ipv4_dad_configure(Address *address);
+
+int address_configure(
+ const Address *address,
+ Link *link,
+ link_netlink_message_handler_t callback,
+ bool update,
+ Address **ret) {
+
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ Address *acquired_address, *a;
+ uint32_t flags;
+ int r;
+
+ assert(address);
+ assert(IN_SET(address->family, AF_INET, AF_INET6));
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(callback);
+
+ /* If this is a new address, then refuse adding more than the limit */
+ if (address_get(link, address, NULL) <= 0 &&
+ set_size(link->addresses) >= ADDRESSES_PER_LINK_MAX)
+ return log_link_error_errno(link, SYNTHETIC_ERRNO(E2BIG),
+ "Too many addresses are configured, refusing: %m");
+
+ r = address_acquire(link, address, &acquired_address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to acquire an address from pool: %m");
+ if (acquired_address)
+ address = acquired_address;
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *str = NULL;
+
+ (void) in_addr_to_string(address->family, &address->in_addr, &str);
+ log_link_debug(link, "%s address: %s", update ? "Updating" : "Configuring", strna(str));
+ }
+
+ if (update)
+ r = sd_rtnl_message_new_addr_update(link->manager->rtnl, &req,
+ link->ifindex, address->family);
+ else
+ r = sd_rtnl_message_new_addr(link->manager->rtnl, &req, RTM_NEWADDR,
+ link->ifindex, address->family);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_NEWADDR message: %m");
+
+ r = sd_rtnl_message_addr_set_prefixlen(req, address->prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set prefixlen: %m");
+
+ flags = address->flags | IFA_F_PERMANENT;
+ r = sd_rtnl_message_addr_set_flags(req, flags & 0xff);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set flags: %m");
+
+ if (flags & ~0xff) {
+ r = sd_netlink_message_append_u32(req, IFA_FLAGS, flags);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set extended flags: %m");
+ }
+
+ r = sd_rtnl_message_addr_set_scope(req, address->scope);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set scope: %m");
+
+ r = netlink_message_append_in_addr_union(req, IFA_LOCAL, address->family, &address->in_addr);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFA_LOCAL attribute: %m");
+
+ if (in_addr_is_null(address->family, &address->in_addr_peer) == 0) {
+ r = netlink_message_append_in_addr_union(req, IFA_ADDRESS, address->family, &address->in_addr_peer);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFA_ADDRESS attribute: %m");
+ } else if (address_may_have_broadcast(address)) {
+ r = sd_netlink_message_append_in_addr(req, IFA_BROADCAST, &address->broadcast);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFA_BROADCAST attribute: %m");
+ }
+
+ if (address->family == AF_INET && address->label) {
+ r = sd_netlink_message_append_string(req, IFA_LABEL, address->label);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFA_LABEL attribute: %m");
+ }
+
+ r = sd_netlink_message_append_cache_info(req, IFA_CACHEINFO, &address->cinfo);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFA_CACHEINFO attribute: %m");
+
+ r = address_add(link, address, &a);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not add address: %m");
+
+ r = address_set_masquerade(a, true);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not enable IP masquerading, ignoring: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, callback, link_netlink_destroy_callback, link);
+ if (r < 0) {
+ (void) address_set_masquerade(a, false);
+ return log_link_error_errno(link, r, "Could not send rtnetlink message: %m");
+ }
+
+ link_ref(link);
+
+ if (FLAGS_SET(address->duplicate_address_detection, ADDRESS_FAMILY_IPV4)) {
+ r = ipv4_dad_configure(a);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to start IPv4ACD client, ignoring: %m");
+ }
+
+ if (ret)
+ *ret = a;
+
+ return 1;
+}
+
+static int static_address_ready_callback(Address *address) {
+ Address *a;
+ Link *link;
+
+ assert(address);
+ assert(address->link);
+
+ link = address->link;
+
+ if (!link->addresses_configured)
+ return 0;
+
+ SET_FOREACH(a, link->static_addresses)
+ if (!address_is_ready(a)) {
+ _cleanup_free_ char *str = NULL;
+
+ (void) in_addr_to_string(a->family, &a->in_addr, &str);
+ log_link_debug(link, "an address %s/%u is not ready", strnull(str), a->prefixlen);
+ return 0;
+ }
+
+ /* This should not be called again */
+ SET_FOREACH(a, link->static_addresses)
+ a->callback = NULL;
+
+ link->addresses_ready = true;
+
+ return link_set_routes(link);
+}
+
+static int address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(rtnl);
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+ assert(link->address_messages > 0);
+
+ link->address_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set address");
+ link_enter_failed(link);
+ return 1;
+ } else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ if (link->address_messages == 0) {
+ Address *a;
+
+ log_link_debug(link, "Addresses set");
+ link->addresses_configured = true;
+
+ /* When all static addresses are already ready, then static_address_ready_callback()
+ * will not be called automatically. So, call it here. */
+ a = set_first(link->static_addresses);
+ if (!a) {
+ log_link_debug(link, "No static address is stored. Already removed?");
+ return 1;
+ }
+
+ r = static_address_ready_callback(a);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+static int static_address_configure(const Address *address, Link *link, bool update) {
+ Address *ret;
+ int r;
+
+ assert(address);
+ assert(link);
+
+ r = address_configure(address, link, address_handler, update, &ret);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not configure static address: %m");
+
+ link->address_messages++;
+
+ r = set_ensure_put(&link->static_addresses, &address_hash_ops, ret);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to store static address: %m");
+
+ ret->callback = static_address_ready_callback;
+
+ return 0;
+}
+
+int link_set_addresses(Link *link) {
+ Address *ad;
+ Prefix *p;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->address_remove_messages != 0) {
+ log_link_debug(link, "Removing old addresses, new addresses will be configured later.");
+ link->request_static_addresses = true;
+ return 0;
+ }
+
+ ORDERED_HASHMAP_FOREACH(ad, link->network->addresses_by_section) {
+ bool update;
+
+ update = address_get(link, ad, NULL) > 0;
+ r = static_address_configure(ad, link, update);
+ if (r < 0)
+ return r;
+ }
+
+ HASHMAP_FOREACH(p, link->network->prefixes_by_section) {
+ _cleanup_(address_freep) Address *address = NULL;
+
+ if (!p->assign)
+ continue;
+
+ r = address_new(&address);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_radv_prefix_get_prefix(p->radv_prefix, &address->in_addr.in6, &address->prefixlen);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not get RA prefix: %m");
+
+ r = generate_ipv6_eui_64_address(link, &address->in_addr.in6);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not generate EUI64 address: %m");
+
+ address->family = AF_INET6;
+ r = static_address_configure(address, link, true);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->address_messages == 0) {
+ link->addresses_configured = true;
+ link->addresses_ready = true;
+ r = link_set_routes(link);
+ if (r < 0)
+ return r;
+ } else {
+ log_link_debug(link, "Setting addresses");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+int manager_rtnl_process_address(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(address_freep) Address *tmp = NULL;
+ _cleanup_free_ char *buf = NULL, *buf_peer = NULL;
+ Link *link = NULL;
+ uint16_t type;
+ unsigned char flags;
+ Address *address = NULL;
+ char valid_buf[FORMAT_TIMESPAN_MAX];
+ const char *valid_str = NULL;
+ int ifindex, r;
+ bool has_peer = false;
+
+ 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(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_rtnl_message_addr_get_flags(message, &flags);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received address message without flags, ignoring: %m");
+ return 0;
+ }
+ tmp->flags = flags;
+
+ 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;
+ else
+ has_peer = true;
+ }
+
+ 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;
+ }
+ has_peer = true;
+ } 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("Received unsupported address family");
+ }
+
+ (void) in_addr_to_string(tmp->family, &tmp->in_addr, &buf);
+ (void) in_addr_to_string(tmp->family, &tmp->in_addr_peer, &buf_peer);
+
+ r = sd_netlink_message_read_cache_info(message, IFA_CACHEINFO, &tmp->cinfo);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: cannot get IFA_CACHEINFO attribute, ignoring: %m");
+ return 0;
+ } else if (r >= 0 && tmp->cinfo.ifa_valid != CACHE_INFO_INFINITY_LIFE_TIME)
+ valid_str = format_timespan(valid_buf, FORMAT_TIMESPAN_MAX,
+ tmp->cinfo.ifa_valid * USEC_PER_SEC,
+ USEC_PER_SEC);
+
+ (void) address_get(link, tmp, &address);
+
+ switch (type) {
+ case RTM_NEWADDR:
+ if (address)
+ log_link_debug(link, "Remembering updated address: %s%s%s/%u (valid %s%s)",
+ strnull(buf), has_peer ? " peer " : "",
+ has_peer ? strnull(buf_peer) : "", tmp->prefixlen,
+ valid_str ? "for " : "forever", strempty(valid_str));
+ else {
+ /* An address appeared that we did not request */
+ r = address_add_foreign(link, tmp, &address);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to remember foreign address %s/%u, ignoring: %m",
+ strnull(buf), tmp->prefixlen);
+ return 0;
+ } else
+ log_link_debug(link, "Remembering foreign address: %s%s%s/%u (valid %s%s)",
+ strnull(buf), has_peer ? " peer " : "",
+ has_peer ? strnull(buf_peer) : "", tmp->prefixlen,
+ valid_str ? "for " : "forever", strempty(valid_str));
+ }
+
+ /* address_update() logs internally, so we don't need to here. */
+ r = address_update(address, tmp);
+ if (r < 0)
+ link_enter_failed(link);
+
+ break;
+
+ case RTM_DELADDR:
+ if (address) {
+ log_link_debug(link, "Forgetting address: %s%s%s/%u (valid %s%s)",
+ strnull(buf), has_peer ? " peer " : "",
+ has_peer ? strnull(buf_peer) : "", tmp->prefixlen,
+ valid_str ? "for " : "forever", strempty(valid_str));
+ (void) address_drop(address);
+ } else
+ log_link_debug(link, "Kernel removed an address we don't remember: %s%s%s/%u (valid %s%s), ignoring.",
+ strnull(buf), has_peer ? " peer " : "",
+ has_peer ? strnull(buf_peer) : "", tmp->prefixlen,
+ valid_str ? "for " : "forever", strempty(valid_str));
+
+ break;
+
+ default:
+ assert_not_reached("Received invalid RTNL message type");
+ }
+
+ return 1;
+}
+
+int link_serialize_addresses(Link *link, FILE *f) {
+ bool space = false;
+ Address *a;
+
+ assert(link);
+
+ fputs("ADDRESSES=", f);
+ SET_FOREACH(a, link->addresses) {
+ _cleanup_free_ char *address_str = NULL;
+
+ if (in_addr_to_string(a->family, &a->in_addr, &address_str) < 0)
+ continue;
+
+ fprintf(f, "%s%s/%u", space ? " " : "", address_str, a->prefixlen);
+ space = true;
+ }
+ fputc('\n', f);
+
+ return 0;
+}
+
+int link_deserialize_addresses(Link *link, const char *addresses) {
+ int r;
+
+ assert(link);
+
+ for (const char *p = addresses;; ) {
+ _cleanup_(address_freep) Address *tmp = NULL;
+ _cleanup_free_ char *address_str = NULL;
+
+ r = extract_first_word(&p, &address_str, NULL, 0);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to parse ADDRESSES=: %m");
+ if (r == 0)
+ return 0;
+
+ r = address_new(&tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_prefix_from_string_auto(address_str, &tmp->family, &tmp->in_addr, &tmp->prefixlen);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "Failed to parse address, ignoring: %s", address_str);
+ continue;
+ }
+
+ r = address_add(link, tmp, NULL);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Failed to add address %s, ignoring: %m", address_str);
+ }
+
+ return 0;
+}
+
+static void static_address_on_acd(sd_ipv4acd *acd, int event, void *userdata) {
+ _cleanup_free_ char *pretty = NULL;
+ Address *address;
+ Link *link;
+ int r;
+
+ assert(acd);
+ assert(userdata);
+
+ address = (Address *) userdata;
+ link = address->link;
+
+ (void) in_addr_to_string(address->family, &address->in_addr, &pretty);
+ switch (event) {
+ case SD_IPV4ACD_EVENT_STOP:
+ log_link_debug(link, "Stopping ACD client...");
+ return;
+
+ case SD_IPV4ACD_EVENT_BIND:
+ log_link_debug(link, "Successfully claimed address %s", strna(pretty));
+ link_check_ready(link);
+ break;
+
+ case SD_IPV4ACD_EVENT_CONFLICT:
+ log_link_warning(link, "DAD conflict. Dropping address %s", strna(pretty));
+ r = address_remove(address, link, NULL);
+ if (r < 0)
+ log_link_error_errno(link, r, "Failed to drop DAD conflicted address %s", strna(pretty));;
+
+ link_check_ready(link);
+ break;
+
+ default:
+ assert_not_reached("Invalid IPv4ACD event.");
+ }
+
+ (void) sd_ipv4acd_stop(acd);
+
+ return;
+}
+
+static int ipv4_dad_configure(Address *address) {
+ int r;
+
+ assert(address);
+ assert(address->link);
+
+ if (address->family != AF_INET)
+ return 0;
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *pretty = NULL;
+
+ (void) in_addr_to_string(address->family, &address->in_addr, &pretty);
+ log_link_debug(address->link, "Starting IPv4ACD client. Probing address %s", strna(pretty));
+ }
+
+ if (!address->acd) {
+ r = sd_ipv4acd_new(&address->acd);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_attach_event(address->acd, address->link->manager->event, 0);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_ipv4acd_set_ifindex(address->acd, address->link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_mac(address->acd, &address->link->hw_addr.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, static_address_on_acd, address);
+ if (r < 0)
+ return r;
+
+ return sd_ipv4acd_start(address->acd, true);
+}
+
+static int ipv4_dad_update_mac_one(Address *address) {
+ bool running;
+ int r;
+
+ assert(address);
+
+ if (!address->acd)
+ return 0;
+
+ running = sd_ipv4acd_is_running(address->acd);
+
+ r = sd_ipv4acd_stop(address->acd);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_mac(address->acd, &address->link->hw_addr.addr.ether);
+ if (r < 0)
+ return r;
+
+ if (running) {
+ r = sd_ipv4acd_start(address->acd, true);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int ipv4_dad_update_mac(Link *link) {
+ Address *address;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses) {
+ k = ipv4_dad_update_mac_one(address);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+int ipv4_dad_stop(Link *link) {
+ Address *address;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses) {
+ k = sd_ipv4acd_stop(address->acd);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+void ipv4_dad_unref(Link *link) {
+ Address *address;
+
+ assert(link);
+
+ SET_FOREACH(address, link->addresses)
+ address->acd = sd_ipv4acd_unref(address->acd);
+}
+
+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;
+ 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 (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, (union in_addr_union*) &n->broadcast);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Broadcast is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ n->family = AF_INET;
+ n = NULL;
+
+ 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;
+
+ n = NULL;
+
+ 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 (!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();
+
+ n = NULL;
+ 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;
+ uint32_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 = CACHE_INFO_INFINITY_LIFE_TIME;
+ 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->cinfo.ifa_prefered = 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);
+
+ n = NULL;
+ 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;
+ }
+
+ if (streq(rvalue, "host"))
+ n->scope = RT_SCOPE_HOST;
+ else if (streq(rvalue, "link"))
+ n->scope = RT_SCOPE_LINK;
+ else if (streq(rvalue, "global"))
+ n->scope = RT_SCOPE_UNIVERSE;
+ else {
+ r = safe_atou8(rvalue , &n->scope);
+ 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_set = true;
+ n = NULL;
+ 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;
+ AddressFamily a;
+ 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;
+ }
+
+ a = duplicate_address_detection_address_family_from_string(rvalue);
+ if (a < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse %s=, ignoring: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ n->duplicate_address_detection = a;
+ n = NULL;
+ return 0;
+}
+
+bool address_is_ready(const Address *a) {
+ assert(a);
+
+ return !(a->flags & IFA_F_TENTATIVE);
+}
+
+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);
+ }
+
+ if (address_may_have_broadcast(address)) {
+ if (address->broadcast.s_addr == 0)
+ address->broadcast.s_addr = address->in_addr.in.s_addr | htobe32(0xfffffffflu >> address->prefixlen);
+ } else if (address->broadcast.s_addr != 0) {
+ log_warning("%s: broadcast address is set for IPv6 address or IPv4 address with prefixlength 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 (in_addr_is_localhost(address->family, &address->in_addr) > 0 &&
+ (address->family == AF_INET || !address->scope_set)) {
+ /* For IPv4, scope must be always RT_SCOPE_HOST.
+ * For IPv6, use RT_SCOPE_HOST only when it is not explicitly specified. */
+
+ if (address->scope_set && address->scope != RT_SCOPE_HOST)
+ log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: non-host scope is set in the [Address] section from line %u. "
+ "Ignoring Scope= setting.",
+ address->section->filename, address->section->line);
+
+ address->scope = RT_SCOPE_HOST;
+ }
+
+ if (!FLAGS_SET(address->duplicate_address_detection, ADDRESS_FAMILY_IPV6))
+ address->flags |= IFA_F_NODAD;
+
+ return 0;
+}
+
+void network_drop_invalid_addresses(Network *network) {
+ Address *address;
+
+ assert(network);
+
+ ORDERED_HASHMAP_FOREACH(address, network->addresses_by_section)
+ if (address_section_verify(address) < 0)
+ address_free(address);
+}
diff --git a/src/network/networkd-address.h b/src/network/networkd-address.h
new file mode 100644
index 0000000..56e81da
--- /dev/null
+++ b/src/network/networkd-address.h
@@ -0,0 +1,93 @@
+/* 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"
+
+#define CACHE_INFO_INFINITY_LIFE_TIME 0xFFFFFFFFU
+
+typedef struct Manager Manager;
+typedef struct Network Network;
+typedef int (*address_ready_callback_t)(Address *address);
+
+typedef struct Address {
+ Network *network;
+ NetworkConfigSection *section;
+
+ Link *link;
+
+ int family;
+ unsigned char prefixlen;
+ unsigned char scope;
+ uint32_t flags;
+ char *label;
+
+ struct in_addr broadcast;
+ struct ifa_cacheinfo cinfo;
+
+ union in_addr_union in_addr;
+ union in_addr_union in_addr_peer;
+
+ bool scope_set:1;
+ bool ip_masquerade_done:1;
+ AddressFamily duplicate_address_detection;
+
+ /* Called when address become ready */
+ address_ready_callback_t callback;
+
+ sd_ipv4acd *acd;
+} Address;
+
+int address_new(Address **ret);
+Address *address_free(Address *address);
+int address_get(Link *link, const Address *in, Address **ret);
+bool address_exists(Link *link, int family, const union in_addr_union *in_addr);
+int address_configure(const Address *address, Link *link, link_netlink_message_handler_t callback, bool update, Address **ret);
+int address_remove(const Address *address, Link *link, link_netlink_message_handler_t callback);
+bool address_equal(const Address *a1, const Address *a2);
+bool address_is_ready(const Address *a);
+
+int generate_ipv6_eui_64_address(const Link *link, struct in6_addr *ret);
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(Address, address_free);
+
+int link_set_addresses(Link *link);
+int link_drop_addresses(Link *link);
+int link_drop_foreign_addresses(Link *link);
+int link_serialize_addresses(Link *link, FILE *f);
+int link_deserialize_addresses(Link *link, const char *addresses);
+
+void ipv4_dad_unref(Link *link);
+int ipv4_dad_stop(Link *link);
+int ipv4_dad_update_mac(Link *link);
+
+int manager_rtnl_process_address(sd_netlink *nl, sd_netlink_message *message, Manager *m);
+
+void network_drop_invalid_addresses(Network *network);
+
+void address_hash_func(const Address *a, struct siphash *state);
+int address_compare_func(const Address *a1, const Address *a2);
+extern const struct hash_ops address_hash_ops;
+
+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_duplicate_address_detection);
+
+#define IPV4_ADDRESS_FMT_STR "%u.%u.%u.%u"
+#define IPV4_ADDRESS_FMT_VAL(address) \
+ be32toh((address).s_addr) >> 24, \
+ (be32toh((address).s_addr) >> 16) & 0xFFu, \
+ (be32toh((address).s_addr) >> 8) & 0xFFu, \
+ be32toh((address).s_addr) & 0xFFu
diff --git a/src/network/networkd-brvlan.c b/src/network/networkd-brvlan.c
new file mode 100644
index 0000000..e53c73c
--- /dev/null
+++ b/src/network/networkd-brvlan.c
@@ -0,0 +1,283 @@
+/* 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-brvlan.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;
+}
+
+static int append_vlan_info_data(Link *const 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;
+ int i, j, k, r, cnt;
+ uint16_t begin, end;
+ bool done, untagged = false;
+
+ assert(link);
+ assert(req);
+ assert(br_vid_bitmap);
+ assert(br_untagged_bitmap);
+
+ cnt = 0;
+
+ begin = end = UINT16_MAX;
+ for (k = 0; k < BRIDGE_VLAN_BITMAP_LEN; k++) {
+ unsigned base_bit;
+ uint32_t vid_map = br_vid_bitmap[k];
+ uint32_t untagged_map = br_untagged_bitmap[k];
+
+ base_bit = k * 32;
+ i = -1;
+ done = false;
+ do {
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRIDGE_VLAN_INFO attribute: %m");
+ } 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 log_link_error_errno(link, r, "Could not append IFLA_BRIDGE_VLAN_INFO attribute: %m");
+
+ 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 log_link_error_errno(link, r, "Could not append IFLA_BRIDGE_VLAN_INFO attribute: %m");
+ }
+
+ 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;
+}
+
+static int set_brvlan_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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 VLAN to bridge port");
+
+ return 1;
+}
+
+int link_set_bridge_vlan(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+
+ if (!link->network->use_br_vlan)
+ return 0;
+
+ if (!link->network->bridge && !streq_ptr(link->kind, "bridge"))
+ return 0;
+
+ /* pvid might not be in br_vid_bitmap yet */
+ if (link->network->pvid)
+ set_bit(link->network->pvid, link->network->br_vid_bitmap);
+
+ /* create new RTM message */
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_rtnl_message_link_set_family(req, AF_BRIDGE);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set message family: %m");
+
+ r = sd_netlink_message_open_container(req, IFLA_AF_SPEC);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open IFLA_AF_SPEC container: %m");
+
+ /* master needs flag self */
+ if (!link->network->bridge) {
+ uint16_t flags = BRIDGE_FLAGS_SELF;
+ r = sd_netlink_message_append_data(req, IFLA_BRIDGE_FLAGS, &flags, sizeof(flags));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open IFLA_BRIDGE_FLAGS: %m");
+ }
+
+ /* add vlan info */
+ r = append_vlan_info_data(link, req, link->network->pvid, link->network->br_vid_bitmap, link->network->br_untagged_bitmap);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append VLANs: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close IFLA_AF_SPEC container: %m");
+
+ /* send message to the kernel */
+ r = netlink_call_async(link->manager->rtnl, NULL, req, set_brvlan_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);
+
+ return 0;
+}
+
+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;
+ int r;
+ uint16_t vid, vid_end;
+
+ 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-brvlan.h b/src/network/networkd-brvlan.h
new file mode 100644
index 0000000..938b790
--- /dev/null
+++ b/src/network/networkd-brvlan.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+/***
+ Copyright © 2016 BISDN GmbH. All rights reserved.
+***/
+
+#include "conf-parser.h"
+
+#define BRIDGE_VLAN_BITMAP_MAX 4096
+#define BRIDGE_VLAN_BITMAP_LEN (BRIDGE_VLAN_BITMAP_MAX / 32)
+
+typedef struct Link Link;
+
+int link_set_bridge_vlan(Link *link);
+
+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..7e31d2f
--- /dev/null
+++ b/src/network/networkd-can.c
@@ -0,0 +1,315 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <linux/can/netlink.h>
+
+#include "netlink-util.h"
+#include "networkd-can.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "string-util.h"
+
+#define CAN_TERMINATION_OHM_VALUE 120
+
+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 = data;
+ uint64_t sz;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+}
+
+static int link_up_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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)
+ /* we warn but don't fail the link, as it may be brought up later */
+ log_link_message_warning_errno(link, m, r, "Could not bring up interface");
+
+ return 1;
+}
+
+static int link_up_can(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+
+ log_link_debug(link, "Bringing CAN link up");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_rtnl_message_link_set_flags(req, IFF_UP, IFF_UP);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set link flags: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_up_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);
+
+ return 0;
+}
+
+static int link_set_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+
+ log_link_debug(link, "Set link");
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Failed to configure CAN link");
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+static int link_set_can(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ struct can_ctrlmode cm = {};
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ log_link_debug(link, "Configuring CAN link.");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_NEWLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to allocate netlink message: %m");
+
+ r = sd_netlink_message_set_flags(m, NLM_F_REQUEST | NLM_F_ACK);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set netlink flags: %m");
+
+ r = sd_netlink_message_open_container(m, IFLA_LINKINFO);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to open netlink container: %m");
+
+ r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, link->kind);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_INFO_DATA attribute: %m");
+
+ if (link->network->can_bitrate > 0 || link->network->can_sample_point > 0) {
+ struct can_bittiming bt = {
+ .bitrate = link->network->can_bitrate,
+ .sample_point = link->network->can_sample_point,
+ };
+
+ log_link_debug(link, "Setting bitrate = %d bit/s", bt.bitrate);
+ if (link->network->can_sample_point > 0)
+ log_link_debug(link, "Setting sample point = %d.%d%%", 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 log_link_error_errno(link, r, "Could not append IFLA_CAN_BITTIMING attribute: %m");
+ }
+
+ if (link->network->can_data_bitrate > 0 || link->network->can_data_sample_point > 0) {
+ struct can_bittiming bt = {
+ .bitrate = link->network->can_data_bitrate,
+ .sample_point = link->network->can_data_sample_point,
+ };
+
+ log_link_debug(link, "Setting data bitrate = %d bit/s", bt.bitrate);
+ if (link->network->can_data_sample_point > 0)
+ log_link_debug(link, "Setting data sample point = %d.%d%%", 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 log_link_error_errno(link, r, "Could not append IFLA_CAN_DATA_BITTIMING attribute: %m");
+ }
+
+ if (link->network->can_fd_mode >= 0) {
+ cm.mask |= CAN_CTRLMODE_FD;
+ SET_FLAG(cm.flags, CAN_CTRLMODE_FD, link->network->can_fd_mode > 0);
+ log_link_debug(link, "%sabling FD mode", link->network->can_fd_mode > 0 ? "En" : "Dis");
+ }
+
+ if (link->network->can_non_iso >= 0) {
+ cm.mask |= CAN_CTRLMODE_FD_NON_ISO;
+ SET_FLAG(cm.flags, CAN_CTRLMODE_FD_NON_ISO, link->network->can_non_iso > 0);
+ log_link_debug(link, "%sabling FD non-ISO mode", link->network->can_non_iso > 0 ? "En" : "Dis");
+ }
+
+ if (link->network->can_restart_us > 0) {
+ char time_string[FORMAT_TIMESPAN_MAX];
+ 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);
+
+ format_timespan(time_string, FORMAT_TIMESPAN_MAX, restart_ms * 1000, MSEC_PER_SEC);
+
+ if (restart_ms > UINT32_MAX) {
+ log_link_error(link, "restart timeout (%s) too big.", time_string);
+ return -ERANGE;
+ }
+
+ log_link_debug(link, "Setting restart = %s", time_string);
+
+ r = sd_netlink_message_append_u32(m, IFLA_CAN_RESTART_MS, restart_ms);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_CAN_RESTART_MS attribute: %m");
+ }
+
+ if (link->network->can_triple_sampling >= 0) {
+ cm.mask |= CAN_CTRLMODE_3_SAMPLES;
+ SET_FLAG(cm.flags, CAN_CTRLMODE_3_SAMPLES, link->network->can_triple_sampling);
+ log_link_debug(link, "%sabling triple-sampling", link->network->can_triple_sampling ? "En" : "Dis");
+ }
+
+ if (link->network->can_listen_only >= 0) {
+ cm.mask |= CAN_CTRLMODE_LISTENONLY;
+ SET_FLAG(cm.flags, CAN_CTRLMODE_LISTENONLY, link->network->can_listen_only);
+ log_link_debug(link, "%sabling listen-only mode", link->network->can_listen_only ? "En" : "Dis");
+ }
+
+ if (cm.mask != 0) {
+ r = sd_netlink_message_append_data(m, IFLA_CAN_CTRLMODE, &cm, sizeof(cm));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_CAN_CTRLMODE attribute: %m");
+ }
+
+ if (link->network->can_termination >= 0) {
+
+ log_link_debug(link, "%sabling can-termination", link->network->can_termination ? "En" : "Dis");
+
+ r = sd_netlink_message_append_u16(m, IFLA_CAN_TERMINATION,
+ link->network->can_termination ? CAN_TERMINATION_OHM_VALUE : 0);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_CAN_TERMINATION attribute: %m");
+
+ }
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to close netlink container: %m");
+
+ r = sd_netlink_message_close_container(m);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to close netlink container: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, m, link_set_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);
+
+ if (!(link->flags & IFF_UP))
+ return link_up_can(link);
+
+ return 0;
+}
+
+static int link_down_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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) {
+ log_link_message_warning_errno(link, m, r, "Could not bring down interface");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ r = link_set_can(link);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+int link_configure_can(Link *link) {
+ int r;
+
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ if (streq_ptr(link->kind, "can")) {
+ /* The CAN interface must be down to configure bitrate, etc... */
+ if ((link->flags & IFF_UP)) {
+ r = link_down(link, link_down_handler);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ } else {
+ r = link_set_can(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ return 0;
+ }
+
+ if (!(link->flags & IFF_UP)) {
+ r = link_up_can(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ return 0;
+}
diff --git a/src/network/networkd-can.h b/src/network/networkd-can.h
new file mode 100644
index 0000000..7a2705b
--- /dev/null
+++ b/src/network/networkd-can.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_configure_can(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_can_bitrate);
diff --git a/src/network/networkd-conf.c b/src/network/networkd-conf.c
new file mode 100644
index 0000000..bf51624
--- /dev/null
+++ b/src/network/networkd-conf.c
@@ -0,0 +1,191 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2014 Vinay Kulkarni <kulkarniv@vmware.com>
+ ***/
+
+#include <ctype.h>
+#include <netinet/ip.h>
+
+#include "conf-parser.h"
+#include "def.h"
+#include "dhcp-identifier.h"
+#include "extract-word.h"
+#include "hexdecoct.h"
+#include "networkd-conf.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-speed-meter.h"
+#include "networkd-dhcp4.h"
+#include "string-table.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"
+ "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) {
+ char buf[FORMAT_TIMESPAN_MAX];
+
+ log_warning("SpeedMeterIntervalSec= is too small, using %s.",
+ format_timespan(buf, sizeof buf, SPEED_METER_MINIMUM_TIME_INTERVAL, USEC_PER_SEC));
+ m->speed_meter_interval_usec = SPEED_METER_MINIMUM_TIME_INTERVAL;
+ }
+
+ return 0;
+}
+
+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 = rvalue;
+ DUID *duid = data;
+ DUIDType type;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(duid);
+
+ 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, 0,
+ "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;
+
+ return 0;
+}
+
+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) {
+
+ DUID *ret = data;
+ uint8_t raw_data[MAX_DUID_LEN];
+ unsigned count = 0;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(ret);
+
+ /* 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(ret->raw_data));
+ memcpy(ret->raw_data, raw_data, count);
+ ret->raw_data_len = count;
+ return 0;
+}
diff --git a/src/network/networkd-conf.h b/src/network/networkd-conf.h
new file mode 100644
index 0000000..b485e9e
--- /dev/null
+++ b/src/network/networkd-conf.h
@@ -0,0 +1,17 @@
+/* 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);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_duid_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_duid_rawdata);
diff --git a/src/network/networkd-dhcp-common.c b/src/network/networkd-dhcp-common.c
new file mode 100644
index 0000000..9f58121
--- /dev/null
+++ b/src/network/networkd-dhcp-common.c
@@ -0,0 +1,935 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if_arp.h>
+
+#include "dhcp-internal.h"
+#include "dhcp6-internal.h"
+#include "escape.h"
+#include "in-addr-util.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "parse-util.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "strv.h"
+
+bool link_dhcp_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->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);
+ }
+}
+
+static struct DUID fallback_duid = { .type = DUID_TYPE_EN };
+DUID* link_get_duid(Link *link) {
+ if (link->network->duid.type != _DUID_TYPE_INVALID)
+ return &link->network->duid;
+ else if (link->hw_addr.length == 0 &&
+ (link->manager->duid.type == DUID_TYPE_LLT ||
+ link->manager->duid.type == DUID_TYPE_LL))
+ /* Fallback to DUID that works without mac addresses.
+ * This is useful for tunnel devices without mac address. */
+ return &fallback_duid;
+ else
+ return &link->manager->duid;
+}
+
+static int duid_set_uuid(DUID *duid, sd_id128_t uuid) {
+ assert(duid);
+
+ if (duid->raw_data_len > 0)
+ return 0;
+
+ if (duid->type != DUID_TYPE_UUID)
+ return -EINVAL;
+
+ memcpy(&duid->raw_data, &uuid, sizeof(sd_id128_t));
+ duid->raw_data_len = sizeof(sd_id128_t);
+
+ return 1;
+}
+
+static int get_product_uuid_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
+ Manager *manager = userdata;
+ const sd_bus_error *e;
+ const void *a;
+ size_t sz;
+ DUID *duid;
+ Link *link;
+ int r;
+
+ assert(m);
+ assert(manager);
+
+ e = sd_bus_message_get_error(m);
+ if (e) {
+ log_error_errno(sd_bus_error_get_errno(e),
+ "Could not get product UUID. Falling back to use machine-app-specific ID as DUID-UUID: %s",
+ e->message);
+ goto configure;
+ }
+
+ r = sd_bus_message_read_array(m, 'y', &a, &sz);
+ if (r < 0)
+ goto configure;
+
+ if (sz != sizeof(sd_id128_t)) {
+ log_error("Invalid product UUID. Falling back to use machine-app-specific ID as DUID-UUID.");
+ goto configure;
+ }
+
+ memcpy(&manager->product_uuid, a, sz);
+ while ((duid = set_steal_first(manager->duids_requesting_uuid)))
+ (void) duid_set_uuid(duid, manager->product_uuid);
+
+ manager->duids_requesting_uuid = set_free(manager->duids_requesting_uuid);
+
+configure:
+ while ((link = set_steal_first(manager->links_requesting_uuid))) {
+ link_unref(link);
+
+ r = link_configure(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ manager->links_requesting_uuid = set_free(manager->links_requesting_uuid);
+
+ /* To avoid calling GetProductUUID() bus method so frequently, set the flag below
+ * even if the method fails. */
+ manager->has_product_uuid = true;
+
+ return 1;
+}
+
+int manager_request_product_uuid(Manager *m, Link *link) {
+ int r;
+
+ assert(m);
+
+ if (m->has_product_uuid)
+ return 0;
+
+ log_debug("Requesting product UUID");
+
+ if (link) {
+ DUID *duid;
+
+ assert_se(duid = link_get_duid(link));
+
+ r = set_ensure_put(&m->links_requesting_uuid, NULL, link);
+ if (r < 0)
+ return log_oom();
+ if (r > 0)
+ link_ref(link);
+
+ r = set_ensure_put(&m->duids_requesting_uuid, NULL, duid);
+ if (r < 0)
+ return log_oom();
+ }
+
+ if (!m->bus || sd_bus_is_ready(m->bus) <= 0) {
+ log_debug("Not connected to system bus, requesting product UUID later.");
+ return 0;
+ }
+
+ r = sd_bus_call_method_async(
+ m->bus,
+ NULL,
+ "org.freedesktop.hostname1",
+ "/org/freedesktop/hostname1",
+ "org.freedesktop.hostname1",
+ "GetProductUUID",
+ get_product_uuid_handler,
+ m,
+ "b",
+ false);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to get product UUID: %m");
+
+ return 0;
+}
+
+static bool link_requires_uuid(Link *link) {
+ const DUID *duid;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+
+ duid = link_get_duid(link);
+ if (duid->type != DUID_TYPE_UUID || duid->raw_data_len != 0)
+ return false;
+
+ if (link_dhcp4_enabled(link) && IN_SET(link->network->dhcp_client_identifier, DHCP_CLIENT_ID_DUID, DHCP_CLIENT_ID_DUID_ONLY))
+ return true;
+
+ if (link_dhcp6_enabled(link) || link_ipv6_accept_ra_enabled(link))
+ return true;
+
+ return false;
+}
+
+int link_configure_duid(Link *link) {
+ Manager *m;
+ DUID *duid;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+
+ m = link->manager;
+ duid = link_get_duid(link);
+
+ if (!link_requires_uuid(link))
+ return 1;
+
+ if (m->has_product_uuid) {
+ (void) duid_set_uuid(duid, m->product_uuid);
+ return 1;
+ }
+
+ if (!m->links_requesting_uuid) {
+ r = manager_request_product_uuid(m, link);
+ if (r < 0) {
+ if (r == -ENOMEM)
+ return r;
+
+ log_link_warning_errno(link, r,
+ "Failed to get product UUID. Falling back to use machine-app-specific ID as DUID-UUID: %m");
+ return 1;
+ }
+ } else {
+ r = set_put(m->links_requesting_uuid, link);
+ if (r < 0)
+ return log_oom();
+ if (r > 0)
+ link_ref(link);
+
+ r = set_put(m->duids_requesting_uuid, duid);
+ if (r < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+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. */
+
+ if (streq(rvalue, "none"))
+ s = ADDRESS_FAMILY_NO;
+ else if (streq(rvalue, "v4"))
+ s = ADDRESS_FAMILY_IPV4;
+ else if (streq(rvalue, "v6"))
+ s = ADDRESS_FAMILY_IPV6;
+ else if (streq(rvalue, "both"))
+ s = ADDRESS_FAMILY_YES;
+ else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "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_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 = data;
+ uint32_t metric;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ 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;
+ }
+
+ if (streq_ptr(section, "DHCPv4")) {
+ network->dhcp_route_metric = metric;
+ network->dhcp_route_metric_set = true;
+ } else if (streq_ptr(section, "DHCPv6")) {
+ network->dhcp6_route_metric = metric;
+ network->dhcp6_route_metric_set = true;
+ } else { /* [DHCP] section */
+ if (!network->dhcp_route_metric_set)
+ network->dhcp_route_metric = metric;
+ if (!network->dhcp6_route_metric_set)
+ network->dhcp6_route_metric = metric;
+ }
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ 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;
+ }
+
+ if (streq_ptr(section, "DHCPv4")) {
+ network->dhcp_use_dns = r;
+ network->dhcp_use_dns_set = true;
+ } else if (streq_ptr(section, "DHCPv6")) {
+ network->dhcp6_use_dns = r;
+ network->dhcp6_use_dns_set = true;
+ } else { /* [DHCP] section */
+ if (!network->dhcp_use_dns_set)
+ network->dhcp_use_dns = r;
+ if (!network->dhcp6_use_dns_set)
+ network->dhcp6_use_dns = r;
+ }
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ 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;
+ }
+
+ if (streq_ptr(section, "DHCPv4")) {
+ network->dhcp_use_ntp = r;
+ network->dhcp_use_ntp_set = true;
+ } else if (streq_ptr(section, "DHCPv6")) {
+ network->dhcp6_use_ntp = r;
+ network->dhcp6_use_ntp_set = true;
+ } else { /* [DHCP] section */
+ if (!network->dhcp_use_ntp_set)
+ network->dhcp_use_ntp = r;
+ if (!network->dhcp6_use_ntp_set)
+ network->dhcp6_use_ntp = r;
+ }
+
+ return 0;
+}
+
+int config_parse_section_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 = data;
+ uint32_t rt;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = safe_atou32(rvalue, &rt);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse RouteTable=%s, ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ if (STRPTR_IN_SET(section, "DHCP", "DHCPv4")) {
+ network->dhcp_route_table = rt;
+ network->dhcp_route_table_set = true;
+ } else { /* section is IPv6AcceptRA */
+ network->ipv6_accept_ra_route_table = rt;
+ network->ipv6_accept_ra_route_table_set = true;
+ }
+
+ 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 = data;
+ uint32_t iaid;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(network);
+
+ 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;
+ }
+
+ network->iaid = iaid;
+ network->iaid_set = true;
+
+ return 0;
+}
+
+int config_parse_dhcp_user_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 = data;
+ int r;
+
+ assert(l);
+ assert(lvalue);
+ assert(rvalue);
+
+ 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_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 = data;
+ int r;
+
+ assert(l);
+ 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, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to split vendor classes option, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ if (strlen(w) > UINT8_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "%s length is not in the range 1-255, ignoring.", w);
+ continue;
+ }
+
+ r = strv_push(l, w);
+ if (r < 0)
+ return log_oom();
+
+ w = NULL;
+ }
+}
+
+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, *old4 = NULL;
+ _cleanup_(sd_dhcp6_option_unrefp) sd_dhcp6_option *opt6 = NULL, *old6 = NULL;
+ uint32_t uint32_data, enterprise_identifier = 0;
+ _cleanup_free_ char *word = NULL, *q = NULL;
+ OrderedHashmap **options = 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);
+ assert(data);
+
+ 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, 0,
+ "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:{
+ r = safe_atou16(p, &uint16_data);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP uint16 data, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ udata = &uint16_data;
+ sz = sizeof(uint16_t);
+ break;
+ }
+ case DHCP_OPTION_DATA_UINT32: {
+ r = safe_atou32(p, &uint32_data);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCP uint32 data, ignoring assignment: %s", p);
+ return 0;
+ }
+
+ 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);
+
+ 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 = data;
+ const char *p;
+ 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 (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);
+ }
+}
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_dhcp_use_domains, dhcp_use_domains, DHCPUseDomains,
+ "Failed to parse DHCP use domains setting");
+
+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);
diff --git a/src/network/networkd-dhcp-common.h b/src/network/networkd-dhcp-common.h
new file mode 100644
index 0000000..78c149e
--- /dev/null
+++ b/src/network/networkd-dhcp-common.h
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "dhcp-identifier.h"
+#include "time-util.h"
+
+#define DHCP_ROUTE_METRIC 1024
+
+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 = -1,
+} 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;
+} DUID;
+
+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);
+
+DUID* link_get_duid(Link *link);
+int link_configure_duid(Link *link);
+int manager_request_product_uuid(Manager *m, Link *link);
+
+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_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_section_route_table);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_user_class);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_vendor_class);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_send_option);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_request_options);
diff --git a/src/network/networkd-dhcp-server-bus.c b/src/network/networkd-dhcp-server-bus.c
new file mode 100644
index 0000000..32f4bae
--- /dev/null
+++ b/src/network/networkd-dhcp-server-bus.c
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-dhcp-server.h"
+
+#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 = userdata;
+ sd_dhcp_server *s;
+ DHCPLease *lease;
+ int r;
+
+ assert(reply);
+ assert(l);
+
+ 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);
+
+ r = sd_bus_message_open_container(reply, 'a', "(uayayayayt)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(lease, s->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);
+
+ 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 = data;
+
+ assert(l);
+
+ if (event & SD_DHCP_SERVER_EVENT_LEASE_CHANGED)
+ (void) dhcp_server_emit_changed(l, "Leases", NULL);
+}
+
+
+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
+};
diff --git a/src/network/networkd-dhcp-server-bus.h b/src/network/networkd-dhcp-server-bus.h
new file mode 100644
index 0000000..7191478
--- /dev/null
+++ b/src/network/networkd-dhcp-server-bus.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+#include "networkd-link.h"
+
+extern const sd_bus_vtable dhcp_server_vtable[];
+
+void dhcp_server_callback(sd_dhcp_server *server, uint64_t event, void *data);
diff --git a/src/network/networkd-dhcp-server.c b/src/network/networkd-dhcp-server.c
new file mode 100644
index 0000000..cf279c6
--- /dev/null
+++ b/src/network/networkd-dhcp-server.c
@@ -0,0 +1,439 @@
+/* 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.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.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->network->bond)
+ return false;
+
+ if (link->iftype == ARPHRD_CAN)
+ return false;
+
+ return link->network->dhcp_server;
+}
+
+static Address* link_find_dhcp_server_address(Link *link) {
+ Address *address;
+
+ assert(link);
+ assert(link->network);
+
+ /* The first statically configured address if there is any */
+ ORDERED_HASHMAP_FOREACH(address, link->network->addresses_by_section)
+ if (address->family == AF_INET &&
+ !in_addr_is_null(address->family, &address->in_addr))
+ return address;
+
+ /* If that didn't work, find a suitable address we got from the pool */
+ SET_FOREACH(address, link->pool_addresses)
+ if (address->family == AF_INET)
+ return address;
+
+ return NULL;
+}
+
+static int link_push_uplink_to_dhcp_server(
+ Link *link,
+ sd_dhcp_lease_server_type what,
+ sd_dhcp_server *s) {
+
+ _cleanup_free_ struct in_addr *addresses = NULL;
+ size_t n_addresses = 0, n_allocated = 0;
+ bool use_dhcp_lease_data = true;
+
+ 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_allocated, 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: {
+ char **i;
+
+ /* 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_allocated, 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("Unexpected server type");
+ }
+
+ 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_allocated, 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(Link *l, const char *string, struct in_addr **addresses, size_t *n_allocated, 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_allocated, *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;
+ size_t n_addresses = 0, n_allocated = 0;
+ _cleanup_fclose_ FILE *f = NULL;
+ int n = 0, 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;
+
+ n++;
+
+ 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(link, a, &addresses, &n_allocated, &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);
+}
+
+int dhcp4_server_configure(Link *link) {
+ bool acquired_uplink = false;
+ sd_dhcp_option *p;
+ Link *uplink = NULL;
+ Address *address;
+ int r;
+
+ assert(link);
+
+ if (!link_dhcp4_server_enabled(link))
+ return 0;
+
+ if (!(link->flags & IFF_UP))
+ return 0;
+
+ if (!link->dhcp_server) {
+ 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;
+ }
+
+ address = link_find_dhcp_server_address(link);
+ if (!address)
+ return log_link_error_errno(link, SYNTHETIC_ERRNO(EBUSY),
+ "Failed to find suitable address for DHCPv4 server instance.");
+
+ /* 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");
+
+ /* TODO:
+ r = sd_dhcp_server_set_router(link->dhcp_server, &main_address->in_addr.in);
+ if (r < 0)
+ return r;
+ */
+
+ 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");
+ }
+
+ for (sd_dhcp_lease_server_type 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) {
+ uplink = manager_find_uplink(link->manager, link);
+ 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));
+ }
+
+ r = sd_dhcp_server_set_emit_router(link->dhcp_server, link->network->dhcp_server_emit_router);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set router emission for DHCP server: %m");
+
+ if (link->network->dhcp_server_emit_timezone) {
+ _cleanup_free_ char *buffer = NULL;
+ const char *tz;
+
+ if (link->network->dhcp_server_timezone)
+ tz = link->network->dhcp_server_timezone;
+ else {
+ r = get_timezone(&buffer);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to determine timezone: %m");
+
+ tz = buffer;
+ }
+
+ 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");
+ }
+
+ if (!sd_dhcp_server_is_running(link->dhcp_server)) {
+ 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;
+}
+
+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 = data;
+
+ assert(emit);
+ assert(rvalue);
+
+ 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;
+
+ 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;
+ }
+
+ struct in_addr *m = reallocarray(emit->addresses, emit->n_addresses + 1, sizeof(struct in_addr));
+ if (!m)
+ return log_oom();
+
+ emit->addresses = m;
+ emit->addresses[emit->n_addresses++] = a.in;
+ }
+}
diff --git a/src/network/networkd-dhcp-server.h b/src/network/networkd-dhcp-server.h
new file mode 100644
index 0000000..4bd5120
--- /dev/null
+++ b/src/network/networkd-dhcp-server.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "networkd-link.h"
+#include "networkd-util.h"
+
+typedef struct Link Link;
+
+int dhcp4_server_configure(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_emit);
diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c
new file mode 100644
index 0000000..f3c1e5f
--- /dev/null
+++ b/src/network/networkd-dhcp4.c
@@ -0,0 +1,1761 @@
+/* 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 "escape.h"
+#include "alloc-util.h"
+#include "dhcp-client-internal.h"
+#include "hostname-util.h"
+#include "parse-util.h"
+#include "network-internal.h"
+#include "networkd-address.h"
+#include "networkd-dhcp4.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "sysctl-util.h"
+#include "web-util.h"
+
+static int dhcp4_update_address(Link *link, bool announce);
+static int dhcp4_remove_all(Link *link);
+
+static int dhcp4_release_old_lease(Link *link) {
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+
+ if (!link->dhcp_address_old && set_isempty(link->dhcp_routes_old))
+ return 0;
+
+ log_link_debug(link, "Removing old DHCPv4 address and routes.");
+
+ link_dirty(link);
+
+ SET_FOREACH(route, link->dhcp_routes_old) {
+ k = route_remove(route, NULL, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ if (link->dhcp_address_old) {
+ k = address_remove(link->dhcp_address_old, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static void dhcp4_check_ready(Link *link) {
+ int r;
+
+ if (link->network->dhcp_send_decline && !link->dhcp4_address_bind)
+ return;
+
+ if (link->dhcp4_messages > 0)
+ return;
+
+ link->dhcp4_configured = true;
+
+ /* New address and routes are configured now. Let's release old lease. */
+ r = dhcp4_release_old_lease(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return;
+ }
+
+ link_check_ready(link);
+}
+
+static int dhcp4_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->dhcp4_messages > 0);
+
+ link->dhcp4_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -ENETUNREACH && !link->dhcp4_route_retrying) {
+
+ /* It seems kernel does not support that the prefix route cannot be configured with
+ * route table. Let's once drop the config and reconfigure them later. */
+
+ log_link_message_debug_errno(link, m, r, "Could not set DHCPv4 route, retrying later");
+ link->dhcp4_route_failed = true;
+ link->manager->dhcp4_prefix_root_cannot_set_table = true;
+ } else if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set DHCPv4 route");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->dhcp4_messages == 0 && link->dhcp4_route_failed) {
+ link->dhcp4_route_failed = false;
+ link->dhcp4_route_retrying = true;
+
+ r = dhcp4_remove_all(link);
+ if (r < 0)
+ link_enter_failed(link);
+ return 1;
+ }
+
+ dhcp4_check_ready(link);
+
+ return 1;
+}
+
+static int route_scope_from_address(const Route *route, const struct in_addr *self_addr) {
+ assert(route);
+ assert(self_addr);
+
+ if (in4_addr_is_localhost(&route->dst.in) ||
+ (!in4_addr_is_null(self_addr) && in4_addr_equal(&route->dst.in, self_addr)))
+ return RT_SCOPE_HOST;
+ else if (in4_addr_is_null(&route->gw.in))
+ return RT_SCOPE_LINK;
+ else
+ return RT_SCOPE_UNIVERSE;
+}
+
+static bool link_prefixroute(Link *link) {
+ return !link->network->dhcp_route_table_set ||
+ link->network->dhcp_route_table == RT_TABLE_MAIN ||
+ link->manager->dhcp4_prefix_root_cannot_set_table;
+}
+
+static int dhcp_route_configure(Route *route, Link *link) {
+ Route *ret;
+ int r;
+
+ assert(route);
+ assert(link);
+
+ r = route_configure(route, link, dhcp4_route_handler, &ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set DHCPv4 route: %m");
+
+ link->dhcp4_messages++;
+
+ r = set_ensure_put(&link->dhcp_routes, &route_hash_ops, ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store DHCPv4 route: %m");
+
+ (void) set_remove(link->dhcp_routes_old, ret);
+
+ return 0;
+}
+
+static int link_set_dns_routes(Link *link, const struct in_addr *address) {
+ const struct in_addr *dns;
+ uint32_t table;
+ int i, n, r;
+
+ assert(link);
+ assert(link->dhcp_lease);
+ assert(link->network);
+
+ if (!link->network->dhcp_use_dns ||
+ !link->network->dhcp_routes_to_dns)
+ return 0;
+
+ n = sd_dhcp_lease_get_dns(link->dhcp_lease, &dns);
+ if (IN_SET(n, 0, -ENODATA))
+ return 0;
+ if (n < 0)
+ return log_link_warning_errno(link, n, "DHCP error: could not get DNS servers: %m");
+
+ table = link_get_dhcp_route_table(link);
+
+ for (i = 0; i < n; i ++) {
+ _cleanup_(route_freep) Route *route = NULL;
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate route: %m");
+
+ /* Set routes to DNS servers. */
+
+ route->family = AF_INET;
+ route->dst.in = dns[i];
+ route->dst_prefixlen = 32;
+ route->prefsrc.in = *address;
+ route->scope = RT_SCOPE_LINK;
+ route->protocol = RTPROT_DHCP;
+ route->priority = link->network->dhcp_route_metric;
+ route->table = table;
+
+ r = dhcp_route_configure(route, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set route to DNS server: %m");
+ }
+
+ return 0;
+}
+
+static int dhcp_prefix_route_from_lease(
+ const sd_dhcp_lease *lease,
+ uint32_t table,
+ const struct in_addr *address,
+ Route **ret_route) {
+
+ Route *route;
+ struct in_addr netmask;
+ int r;
+
+ r = sd_dhcp_lease_get_netmask((sd_dhcp_lease*) lease, &netmask);
+ if (r < 0)
+ return r;
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route->family = AF_INET;
+ 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;
+ route->protocol = RTPROT_DHCP;
+ route->table = table;
+ *ret_route = route;
+ return 0;
+}
+
+static int link_set_dhcp_routes(Link *link) {
+ _cleanup_free_ sd_dhcp_route **static_routes = NULL;
+ bool classless_route = false, static_route = false;
+ struct in_addr address;
+ uint32_t table;
+ Route *rt;
+ int r, n;
+
+ assert(link);
+
+ if (!link->dhcp_lease) /* link went down while we configured the IP addresses? */
+ return 0;
+
+ if (!link->network) /* link went down while we configured the IP addresses? */
+ return 0;
+
+ if (!link_has_carrier(link) && !link->network->configure_without_carrier)
+ /* During configuring addresses, the link lost its carrier. As networkd is dropping
+ * the addresses now, let's not configure the routes either. */
+ return 0;
+
+ while ((rt = set_steal_first(link->dhcp_routes))) {
+ r = set_ensure_put(&link->dhcp_routes_old, &route_hash_ops, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store old DHCPv4 route: %m");
+ }
+
+ table = link_get_dhcp_route_table(link);
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &address);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "DHCP error: could not get address: %m");
+
+ if (!link_prefixroute(link)) {
+ _cleanup_(route_freep) Route *prefix_route = NULL;
+
+ r = dhcp_prefix_route_from_lease(link->dhcp_lease, table, &address, &prefix_route);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create prefix route: %m");
+
+ r = dhcp_route_configure(prefix_route, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set prefix route: %m");
+ }
+
+ n = sd_dhcp_lease_get_routes(link->dhcp_lease, &static_routes);
+ if (n == -ENODATA)
+ log_link_debug_errno(link, n, "DHCP: No routes received from DHCP server: %m");
+ else if (n < 0)
+ return log_link_error_errno(link, n, "DHCP: could not get routes: %m");
+
+ for (int i = 0; i < n; i++) {
+ switch (sd_dhcp_route_get_option(static_routes[i])) {
+ case SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE:
+ classless_route = true;
+ break;
+ case SD_DHCP_OPTION_STATIC_ROUTE:
+ static_route = true;
+ break;
+ }
+ }
+
+ if (link->network->dhcp_use_routes) {
+ /* 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 (classless_route && static_route)
+ log_link_warning(link, "Classless static routes received from DHCP server: ignoring static-route option");
+
+ for (int i = 0; i < n; i++) {
+ _cleanup_(route_freep) Route *route = NULL;
+
+ if (classless_route &&
+ sd_dhcp_route_get_option(static_routes[i]) != SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE)
+ continue;
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate route: %m");
+
+ route->family = AF_INET;
+ route->protocol = RTPROT_DHCP;
+ route->gw_family = AF_INET;
+ assert_se(sd_dhcp_route_get_gateway(static_routes[i], &route->gw.in) >= 0);
+ assert_se(sd_dhcp_route_get_destination(static_routes[i], &route->dst.in) >= 0);
+ assert_se(sd_dhcp_route_get_destination_prefix_length(static_routes[i], &route->dst_prefixlen) >= 0);
+ route->priority = link->network->dhcp_route_metric;
+ route->table = table;
+ route->mtu = link->network->dhcp_route_mtu;
+ route->scope = route_scope_from_address(route, &address);
+ if (IN_SET(route->scope, RT_SCOPE_LINK, RT_SCOPE_UNIVERSE))
+ route->prefsrc.in = address;
+
+ if (set_contains(link->dhcp_routes, route))
+ continue;
+
+ r = dhcp_route_configure(route, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set route: %m");
+ }
+ }
+
+ if (link->network->dhcp_use_gateway) {
+ const struct in_addr *router;
+
+ r = sd_dhcp_lease_get_router(link->dhcp_lease, &router);
+ if (IN_SET(r, 0, -ENODATA))
+ log_link_info(link, "DHCP: No gateway received from DHCP server.");
+ else if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: could not get gateway: %m");
+ else if (in4_addr_is_null(&router[0]))
+ log_link_info(link, "DHCP: Received gateway is null.");
+ else if (classless_route)
+ /* 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. */
+ log_link_warning(link, "Classless static routes received from DHCP server: ignoring router option");
+ else {
+ _cleanup_(route_freep) Route *route = NULL, *route_gw = NULL;
+
+ r = route_new(&route_gw);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate route: %m");
+
+ /* The dhcp netmask may mask out the gateway. Add an explicit
+ * route for the gw host so that we can route no matter the
+ * netmask or existing kernel route tables. */
+ route_gw->family = AF_INET;
+ route_gw->dst.in = router[0];
+ route_gw->dst_prefixlen = 32;
+ route_gw->prefsrc.in = address;
+ route_gw->scope = RT_SCOPE_LINK;
+ route_gw->protocol = RTPROT_DHCP;
+ route_gw->priority = link->network->dhcp_route_metric;
+ route_gw->table = table;
+ route_gw->mtu = link->network->dhcp_route_mtu;
+
+ r = dhcp_route_configure(route_gw, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set host route: %m");
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate route: %m");
+
+ route->family = AF_INET;
+ route->gw_family = AF_INET;
+ route->gw.in = router[0];
+ route->prefsrc.in = address;
+ route->protocol = RTPROT_DHCP;
+ route->priority = link->network->dhcp_route_metric;
+ route->table = table;
+ route->mtu = link->network->dhcp_route_mtu;
+
+ r = dhcp_route_configure(route, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set router: %m");
+
+ HASHMAP_FOREACH(rt, link->network->routes_by_section) {
+ if (!rt->gateway_from_dhcp_or_ra)
+ continue;
+
+ if (rt->gw_family != AF_INET)
+ continue;
+
+ rt->gw.in = router[0];
+ if (!rt->protocol_set)
+ rt->protocol = RTPROT_DHCP;
+ if (!rt->priority_set)
+ rt->priority = link->network->dhcp_route_metric;
+ if (!rt->table_set)
+ rt->table = table;
+ if (rt->mtu == 0)
+ rt->mtu = link->network->dhcp_route_mtu;
+
+ r = dhcp_route_configure(rt, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set gateway: %m");
+ }
+ }
+ }
+
+ return link_set_dns_routes(link, &address);
+}
+
+static int dhcp_reset_mtu(Link *link) {
+ uint16_t mtu;
+ int r;
+
+ assert(link);
+
+ if (!link->network->dhcp_use_mtu)
+ return 0;
+
+ r = sd_dhcp_lease_get_mtu(link->dhcp_lease, &mtu);
+ if (r == -ENODATA)
+ return 0;
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: failed to get MTU from lease: %m");
+
+ if (link->original_mtu == mtu)
+ return 0;
+
+ r = link_set_mtu(link, link->original_mtu);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP error: could not 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;
+}
+
+static int dhcp4_remove_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->dhcp4_remove_messages > 0);
+
+ link->dhcp4_remove_messages--;
+
+ 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)
+ log_link_message_warning_errno(link, m, r, "Failed to remove DHCPv4 route, ignoring");
+
+ if (link->dhcp4_remove_messages == 0) {
+ r = dhcp4_update_address(link, false);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+static int dhcp4_remove_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->dhcp4_remove_messages > 0);
+
+ link->dhcp4_remove_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EADDRNOTAVAIL)
+ log_link_message_warning_errno(link, m, r, "Failed to remove DHCPv4 address, ignoring");
+ else
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ if (link->dhcp4_remove_messages == 0) {
+ r = dhcp4_update_address(link, false);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+static int dhcp4_remove_all(Link *link) {
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(route, link->dhcp_routes) {
+ k = route_remove(route, NULL, link, dhcp4_remove_route_handler);
+ if (k < 0)
+ r = k;
+ else
+ link->dhcp4_remove_messages++;
+ }
+
+ if (link->dhcp_address) {
+ k = address_remove(link->dhcp_address, link, dhcp4_remove_address_handler);
+ if (k < 0)
+ r = k;
+ else
+ link->dhcp4_remove_messages++;
+ }
+
+ return r;
+}
+
+static int dhcp_lease_lost(Link *link) {
+ int k, r = 0;
+
+ assert(link);
+ assert(link->dhcp_lease);
+
+ log_link_info(link, "DHCP lease lost");
+
+ link->dhcp4_configured = false;
+
+ /* dhcp_lease_lost() may be called during renewing IP address. */
+ k = dhcp4_release_old_lease(link);
+ if (k < 0)
+ r = k;
+
+ k = dhcp4_remove_all(link);
+ 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);
+
+ (void) sd_ipv4acd_stop(link->dhcp_acd);
+
+ return r;
+}
+
+static void dhcp_address_on_acd(sd_ipv4acd *acd, int event, void *userdata) {
+ _cleanup_free_ char *pretty = NULL;
+ union in_addr_union address = {};
+ Link *link;
+ int r;
+
+ assert(acd);
+ assert(userdata);
+
+ link = userdata;
+
+ switch (event) {
+ case SD_IPV4ACD_EVENT_STOP:
+ log_link_debug(link, "Stopping ACD client for DHCP4...");
+ return;
+
+ case SD_IPV4ACD_EVENT_BIND:
+ if (DEBUG_LOGGING) {
+ (void) sd_dhcp_lease_get_address(link->dhcp_lease, &address.in);
+ (void) in_addr_to_string(AF_INET, &address, &pretty);
+ log_link_debug(link, "Successfully claimed DHCP4 address %s", strna(pretty));
+ }
+ link->dhcp4_address_bind = true;
+ dhcp4_check_ready(link);
+ break;
+
+ case SD_IPV4ACD_EVENT_CONFLICT:
+ (void) sd_dhcp_lease_get_address(link->dhcp_lease, &address.in);
+ (void) in_addr_to_string(AF_INET, &address, &pretty);
+ log_link_warning(link, "DAD conflict. Dropping DHCP4 address %s", strna(pretty));
+
+ 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) {
+ r = dhcp_lease_lost(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+ break;
+
+ default:
+ assert_not_reached("Invalid IPv4ACD event.");
+ }
+
+ (void) sd_ipv4acd_stop(acd);
+
+ return;
+}
+
+static int dhcp4_configure_dad(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+
+ if (!link->network->dhcp_send_decline)
+ return 0;
+
+ if (!link->dhcp_acd) {
+ r = sd_ipv4acd_new(&link->dhcp_acd);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_attach_event(link->dhcp_acd, link->manager->event, 0);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_ipv4acd_set_ifindex(link->dhcp_acd, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_mac(link->dhcp_acd, &link->hw_addr.addr.ether);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int dhcp4_dad_update_mac(Link *link) {
+ bool running;
+ int r;
+
+ assert(link);
+
+ if (!link->dhcp_acd)
+ return 0;
+
+ running = sd_ipv4acd_is_running(link->dhcp_acd);
+
+ r = sd_ipv4acd_stop(link->dhcp_acd);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_mac(link->dhcp_acd, &link->hw_addr.addr.ether);
+ if (r < 0)
+ return r;
+
+ if (running) {
+ r = sd_ipv4acd_start(link->dhcp_acd, true);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int dhcp4_start_acd(Link *link) {
+ union in_addr_union addr;
+ struct in_addr old;
+ int r;
+
+ if (!link->network->dhcp_send_decline)
+ return 0;
+
+ if (!link->dhcp_lease)
+ return 0;
+
+ (void) sd_ipv4acd_stop(link->dhcp_acd);
+
+ link->dhcp4_address_bind = false;
+
+ r = sd_dhcp_lease_get_address(link->dhcp_lease, &addr.in);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_get_address(link->dhcp_acd, &old);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_address(link->dhcp_acd, &addr.in);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_callback(link->dhcp_acd, dhcp_address_on_acd, link);
+ if (r < 0)
+ return r;
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *pretty = NULL;
+
+ (void) in_addr_to_string(AF_INET, &addr, &pretty);
+ log_link_debug(link, "Starting IPv4ACD client. Probing DHCPv4 address %s", strna(pretty));
+ }
+
+ r = sd_ipv4acd_start(link->dhcp_acd, !in4_addr_equal(&addr.in, &old));
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int dhcp4_address_ready_callback(Address *address) {
+ Link *link;
+ int r;
+
+ assert(address);
+
+ link = address->link;
+
+ /* Do not call this again. */
+ address->callback = NULL;
+
+ r = link_set_dhcp_routes(link);
+ if (r < 0)
+ return r;
+
+ /* Reconfigure static routes as kernel may remove some routes when lease expires. */
+ r = link_set_routes(link);
+ if (r < 0)
+ return r;
+
+ r = dhcp4_start_acd(link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to start IPv4ACD for DHCP4 address: %m");
+
+ dhcp4_check_ready(link);
+ return 0;
+}
+
+static int dhcp4_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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 != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set DHCPv4 address");
+ link_enter_failed(link);
+ return 1;
+ } else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ if (address_is_ready(link->dhcp_address)) {
+ r = dhcp4_address_ready_callback(link->dhcp_address);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+ } else
+ link->dhcp_address->callback = dhcp4_address_ready_callback;
+
+ return 1;
+}
+
+static int dhcp4_update_address(Link *link, bool announce) {
+ _cleanup_(address_freep) Address *addr = NULL;
+ uint32_t lifetime = CACHE_INFO_INFINITY_LIFE_TIME;
+ struct in_addr address, netmask;
+ unsigned prefixlen;
+ Address *ret;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!link->dhcp_lease)
+ return 0;
+
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ link->dhcp4_configured = false;
+
+ /* address_handler calls link_set_routes() and link_set_nexthop(). Before they are called, the
+ * related flags must be cleared. Otherwise, the link becomes configured state before routes
+ * are configured. */
+ link->static_routes_configured = false;
+ link->static_nexthops_configured = false;
+
+ 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");
+
+ if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) {
+ r = sd_dhcp_lease_get_lifetime(link->dhcp_lease, &lifetime);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "DHCP error: no lifetime: %m");
+ }
+
+ 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_null(&router[0]))
+ log_struct(LOG_INFO,
+ LOG_LINK_INTERFACE(link),
+ LOG_LINK_MESSAGE(link, "DHCPv4 address "IPV4_ADDRESS_FMT_STR"/%u via "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(address),
+ prefixlen,
+ IPV4_ADDRESS_FMT_VAL(router[0])),
+ "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",
+ IPV4_ADDRESS_FMT_VAL(address),
+ prefixlen),
+ "ADDRESS="IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(address),
+ "PREFIXLEN=%u", prefixlen);
+ }
+
+ r = address_new(&addr);
+ if (r < 0)
+ return log_oom();
+
+ addr->family = AF_INET;
+ addr->in_addr.in.s_addr = address.s_addr;
+ addr->cinfo.ifa_prefered = lifetime;
+ addr->cinfo.ifa_valid = lifetime;
+ addr->prefixlen = prefixlen;
+ if (prefixlen <= 30)
+ addr->broadcast.s_addr = address.s_addr | ~netmask.s_addr;
+ SET_FLAG(addr->flags, IFA_F_NOPREFIXROUTE, !link_prefixroute(link));
+
+ /* allow reusing an existing address and simply update its lifetime
+ * in case it already exists */
+ r = address_configure(addr, link, dhcp4_address_handler, true, &ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set DHCPv4 address: %m");
+
+ if (!address_equal(link->dhcp_address, ret))
+ link->dhcp_address_old = link->dhcp_address;
+ link->dhcp_address = ret;
+
+ return 0;
+}
+
+static int dhcp_lease_renew(sd_dhcp_client *client, Link *link) {
+ sd_dhcp_lease *lease;
+ int r;
+
+ assert(link);
+ 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");
+
+ sd_dhcp_lease_unref(link->dhcp_lease);
+ link->dhcp_lease = sd_dhcp_lease_ref(lease);
+ link_dirty(link);
+
+ return dhcp4_update_address(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_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->dhcp4_remove_messages == 0) {
+ r = dhcp4_update_address(link, true);
+ if (r < 0)
+ return r;
+ } else
+ log_link_debug(link,
+ "The link has previously assigned DHCPv4 address or routes. "
+ "The newly assigned address and routes will set up after old ones are removed.");
+
+ return 0;
+}
+
+static int dhcp_lease_ip_change(sd_dhcp_client *client, Link *link) {
+ int r;
+
+ r = dhcp_lease_acquired(client, link);
+ if (r < 0)
+ (void) dhcp_lease_lost(link);
+
+ return r;
+}
+
+static int dhcp_server_is_deny_listed(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 (set_contains(link->network->dhcp_deny_listed_ip, UINT32_TO_PTR(addr.s_addr))) {
+ log_struct(LOG_DEBUG,
+ LOG_LINK_INTERFACE(link),
+ LOG_LINK_MESSAGE(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 dhcp_server_is_allow_listed(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 (set_contains(link->network->dhcp_allow_listed_ip, UINT32_TO_PTR(addr.s_addr))) {
+ log_struct(LOG_DEBUG,
+ LOG_LINK_INTERFACE(link),
+ LOG_LINK_MESSAGE(link, "DHCPv4 server IP address "IPV4_ADDRESS_FMT_STR" found in allow-list, accepting offer",
+ IPV4_ADDRESS_FMT_VAL(addr)));
+ return true;
+ }
+
+ return false;
+}
+
+static int dhcp4_handler(sd_dhcp_client *client, int event, void *userdata) {
+ Link *link = userdata;
+ int r;
+
+ assert(link);
+ 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_enabled(link, ADDRESS_FAMILY_FALLBACK_IPV4)) {
+ assert(link->ipv4ll);
+
+ log_link_debug(link, "DHCP client is stopped. Acquiring IPv4 link-local address");
+
+ 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_warning_errno(link, r, "Failed to send DHCP RELEASE, ignoring: %m");
+ }
+
+ r = dhcp_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 = dhcp_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:
+ if (!set_isempty(link->network->dhcp_allow_listed_ip)) {
+ r = dhcp_server_is_allow_listed(link, client);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -ENOMSG;
+ } else {
+ r = dhcp_server_is_deny_listed(link, client);
+ if (r < 0)
+ return r;
+ if (r != 0)
+ return -ENOMSG;
+ }
+ 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 r;
+
+ 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_warning_errno(link, r, "DHCP4 CLIENT: Failed to set hostname from kernel hostname, ignoring: %m");
+ else if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set hostname: %m");
+
+ return 0;
+}
+
+static bool promote_secondaries_enabled(const char *ifname) {
+ _cleanup_free_ char *promote_secondaries_sysctl = NULL;
+ char *promote_secondaries_path;
+ int r;
+
+ promote_secondaries_path = strjoina("net/ipv4/conf/", ifname, "/promote_secondaries");
+ r = sysctl_read(promote_secondaries_path, &promote_secondaries_sysctl);
+ if (r < 0) {
+ log_debug_errno(r, "Cannot read sysctl %s", promote_secondaries_path);
+ return false;
+ }
+
+ truncate_nl(promote_secondaries_sysctl);
+ r = parse_boolean(promote_secondaries_sysctl);
+ if (r < 0)
+ log_warning_errno(r, "Cannot parse sysctl %s with content %s as boolean", promote_secondaries_path, promote_secondaries_sysctl);
+ return r > 0;
+}
+
+/* dhcp4_set_promote_secondaries will ensure this interface has
+ * the "promote_secondaries" option in the kernel set. If this sysctl
+ * 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-network 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
+ */
+static int dhcp4_set_promote_secondaries(Link *link) {
+ int r;
+
+ assert(link);
+
+ /* check if the kernel has promote_secondaries enabled for our
+ * interface. If it is not globally enabled or enabled for the
+ * specific interface we must either enable it.
+ */
+ if (!(promote_secondaries_enabled("all") || promote_secondaries_enabled(link->ifname))) {
+ char *promote_secondaries_path = NULL;
+
+ log_link_debug(link, "promote_secondaries is unset, setting it");
+ promote_secondaries_path = strjoina("net/ipv4/conf/", link->ifname, "/promote_secondaries");
+ r = sysctl_write(promote_secondaries_path, "1");
+ if (r < 0)
+ log_link_warning_errno(link, r, "cannot set sysctl %s to 1", promote_secondaries_path);
+ return r > 0;
+ }
+
+ 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_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->iaid_set,
+ link->network->iaid,
+ duid->llt_time);
+ else
+ r = sd_dhcp_client_set_iaid_duid(link->dhcp_client,
+ link->network->iaid_set,
+ link->network->iaid,
+ duid->type,
+ duid->raw_data_len > 0 ? duid->raw_data : NULL,
+ duid->raw_data_len);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 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_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_error_errno(link, r, "DHCP4 CLIENT: Failed to set DUID: %m");
+ break;
+ }
+ case DHCP_CLIENT_ID_MAC: {
+ const uint8_t *hw_addr = link->hw_addr.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_error_errno(link, r, "DHCP4 CLIENT: Failed to set client ID: %m");
+ break;
+ }
+ default:
+ assert_not_reached("Unknown client identifier type.");
+ }
+
+ return 0;
+}
+
+static int dhcp4_init(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (link->dhcp_client)
+ return 0;
+
+ r = sd_dhcp_client_new(&link->dhcp_client, link->network->dhcp_anonymize);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_client_attach_event(link->dhcp_client, link->manager->event, 0);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int dhcp4_configure(Link *link) {
+ sd_dhcp_option *send_option;
+ void *request_options;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!link_dhcp4_enabled(link))
+ return 0;
+
+ r = dhcp4_set_promote_secondaries(link);
+ if (r < 0)
+ return r;
+
+ r = dhcp4_init(link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to initialize DHCP4 client: %m");
+
+ r = sd_dhcp_client_set_mac(link->dhcp_client,
+ link->hw_addr.addr.bytes,
+ link->bcast_addr.length > 0 ? link->bcast_addr.addr.bytes : NULL,
+ link->hw_addr.length, link->iftype);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set MAC address: %m");
+
+ r = sd_dhcp_client_set_ifindex(link->dhcp_client, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set ifindex: %m");
+
+ r = sd_dhcp_client_set_callback(link->dhcp_client, dhcp4_handler, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set callback: %m");
+
+ r = sd_dhcp_client_set_request_broadcast(link->dhcp_client,
+ link->network->dhcp_broadcast);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set request flag for broadcast: %m");
+
+ if (link->mtu) {
+ r = sd_dhcp_client_set_mtu(link->dhcp_client, link->mtu);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set MTU: %m");
+ }
+
+ if (link->network->dhcp_use_mtu) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client,
+ SD_DHCP_OPTION_INTERFACE_MTU);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set request flag for MTU: %m");
+ }
+
+ /* NOTE: even if this variable is called "use", it also "sends" PRL
+ * options, maybe there should be a different configuration variable
+ * to send or not route options?. */
+ /* NOTE: when using Anonymize=yes, routes PRL options are sent
+ * by default, so they don't need to be added here. */
+ if (link->network->dhcp_use_routes && !link->network->dhcp_anonymize) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client,
+ SD_DHCP_OPTION_STATIC_ROUTE);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 CLIENT: Failed to set request flag for classless static route: %m");
+ }
+
+ if (link->network->dhcp_use_domains != DHCP_USE_DOMAINS_NO && !link->network->dhcp_anonymize) {
+ r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_DOMAIN_SEARCH_LIST);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 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_NEW_TZDB_TIMEZONE);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set request flag for timezone: %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_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 CLIENT: Failed to set user class: %m");
+ }
+
+ if (link->network->dhcp_client_port) {
+ r = sd_dhcp_client_set_client_port(link->dhcp_client, link->network->dhcp_client_port);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 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_error_errno(link, r, "DHCP4 CLIENT: Failed set to lease lifetime: %m");
+ }
+
+ r = dhcp4_configure_dad(link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to configure service type: %m");
+
+ return dhcp4_set_client_identifier(link);
+}
+
+int dhcp4_update_mac(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link->dhcp_client)
+ return 0;
+
+ r = sd_dhcp_client_set_mac(link->dhcp_client, link->hw_addr.addr.bytes,
+ link->bcast_addr.length > 0 ? link->bcast_addr.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;
+
+ r = dhcp4_dad_update_mac(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int link_deserialize_dhcp4(Link *link, const char *dhcp4_address) {
+ union in_addr_union address;
+ int r;
+
+ assert(link);
+
+ if (isempty(dhcp4_address))
+ return 0;
+
+ r = in_addr_from_string(AF_INET, dhcp4_address, &address);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to parse DHCPv4 address: %s", dhcp4_address);
+
+ r = dhcp4_init(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to initialize DHCPv4 client: %m");
+
+ r = sd_dhcp_client_set_request_address(link->dhcp_client, &address.in);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to set initial DHCPv4 address %s: %m", dhcp4_address);
+
+ 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 = data;
+ uint64_t a;
+ int r;
+
+ assert(network);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->dhcp_max_attempts = 0;
+ return 0;
+ }
+
+ if (streq(rvalue, "infinity")) {
+ network->dhcp_max_attempts = (uint64_t) -1;
+ 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_acl_ip_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 = data;
+ Set **acl;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ acl = STR_IN_SET(lvalue, "DenyList", "BlackList") ? &network->dhcp_deny_listed_ip : &network->dhcp_allow_listed_ip;
+
+ if (isempty(rvalue)) {
+ *acl = set_free(*acl);
+ 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 DHCP '%s=' IP address, ignoring assignment: %s",
+ lvalue, 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,
+ "DHCP '%s=' IP address is invalid, ignoring assignment: %s", lvalue, n);
+ continue;
+ }
+
+ r = set_ensure_put(acl, NULL, UINT32_TO_PTR(ip.in.s_addr));
+ if (r < 0)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store DHCP '%s=' IP address '%s', ignoring assignment: %m", lvalue, n);
+ }
+}
+
+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) {
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (streq(rvalue, "CS4"))
+ *((int *)data) = IPTOS_CLASS_CS4;
+ else if (streq(rvalue, "CS6"))
+ *((int *)data) = IPTOS_CLASS_CS6;
+ else
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse IPServiceType type '%s', ignoring.", rvalue);
+
+ return 0;
+}
+
+int config_parse_dhcp_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;
+ Network *network = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->dhcp_mudurl = mfree(network->dhcp_mudurl);
+ return 0;
+ }
+
+ r = cunescape(rvalue, 0, &unescaped);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to Failed to unescape MUD URL, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (!http_url_is_valid(unescaped) || strlen(unescaped) > 255) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse MUD URL '%s', ignoring: %m", rvalue);
+
+ return 0;
+ }
+
+ return free_and_strdup_warn(&network->dhcp_mudurl, unescaped);
+}
+
+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;
+ uint32_t k;
+
+ 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"))
+ k = CACHE_INFO_INFINITY_LIFE_TIME;
+ else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid LeaseLifetime= value, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ network->dhcp_fallback_lease_lifetime = k;
+
+ return 0;
+}
+
+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..daab5b1
--- /dev/null
+++ b/src/network/networkd-dhcp4.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+
+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 = -1,
+} DHCPClientIdentifier;
+
+int dhcp4_configure(Link *link);
+int dhcp4_update_mac(Link *link);
+
+int link_deserialize_dhcp4(Link *link, const char *dhcp4_address);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_client_identifier);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_acl_ip_address);
+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);
diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c
new file mode 100644
index 0000000..d4d4182
--- /dev/null
+++ b/src/network/networkd-dhcp6.c
@@ -0,0 +1,1719 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2014 Intel Corporation. All rights reserved.
+***/
+
+#include <netinet/in.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+
+#include "sd-dhcp6-client.h"
+
+#include "escape.h"
+#include "hashmap.h"
+#include "hostname-util.h"
+#include "missing_network.h"
+#include "network-internal.h"
+#include "networkd-address.h"
+#include "networkd-dhcp6.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-radv.h"
+#include "siphash24.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "radv-internal.h"
+#include "web-util.h"
+
+bool link_dhcp6_pd_is_enabled(Link *link) {
+ assert(link);
+
+ if (!link->network)
+ return false;
+
+ return link->network->dhcp6_pd;
+}
+
+static bool dhcp6_lease_has_pd_prefix(sd_dhcp6_lease *lease) {
+ uint32_t lifetime_preferred, lifetime_valid;
+ union in_addr_union 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.in6, &pd_prefix_len, &lifetime_preferred, &lifetime_valid) >= 0;
+}
+
+DHCP6DelegatedPrefix *dhcp6_pd_free(DHCP6DelegatedPrefix *p) {
+ if (!p)
+ return NULL;
+
+ if (p->link && p->link->manager) {
+ hashmap_remove(p->link->manager->dhcp6_prefixes, &p->prefix);
+ set_remove(p->link->manager->dhcp6_pd_prefixes, p);
+ }
+
+ link_unref(p->link);
+ return mfree(p);
+}
+
+static void dhcp6_pd_hash_func(const DHCP6DelegatedPrefix *p, struct siphash *state) {
+ assert(p);
+
+ siphash24_compress(&p->pd_prefix, sizeof(p->pd_prefix), state);
+ siphash24_compress(&p->link, sizeof(p->link), state);
+}
+
+static int dhcp6_pd_compare_func(const DHCP6DelegatedPrefix *a, const DHCP6DelegatedPrefix *b) {
+ int r;
+
+ r = memcmp(&a->pd_prefix, &b->pd_prefix, sizeof(a->pd_prefix));
+ if (r != 0)
+ return r;
+
+ return CMP(a->link, b->link);
+}
+
+DEFINE_HASH_OPS(dhcp6_pd_hash_ops, DHCP6DelegatedPrefix, dhcp6_pd_hash_func, dhcp6_pd_compare_func);
+
+static Link *dhcp6_pd_get_link_by_prefix(Link *link, const union in_addr_union *prefix) {
+ DHCP6DelegatedPrefix *pd;
+
+ assert(link);
+ assert(link->manager);
+ assert(prefix);
+
+ pd = hashmap_get(link->manager->dhcp6_prefixes, &prefix->in6);
+ if (!pd)
+ return NULL;
+
+ return pd->link;
+}
+
+static int dhcp6_pd_get_assigned_prefix(Link *link, const union in_addr_union *pd_prefix, union in_addr_union *ret_prefix) {
+ DHCP6DelegatedPrefix *pd, in;
+
+ assert(link);
+ assert(link->manager);
+ assert(pd_prefix);
+ assert(ret_prefix);
+
+ in = (DHCP6DelegatedPrefix) {
+ .pd_prefix = pd_prefix->in6,
+ .link = link,
+ };
+
+ pd = set_get(link->manager->dhcp6_pd_prefixes, &in);
+ if (!pd)
+ return -ENOENT;
+
+ ret_prefix->in6 = pd->prefix;
+ return 0;
+}
+
+static int dhcp6_pd_remove_old(Link *link, bool force);
+
+static int dhcp6_pd_address_callback(Address *address) {
+ Address *a;
+
+ assert(address);
+ assert(address->link);
+
+ /* Make this called only once */
+ SET_FOREACH(a, address->link->dhcp6_pd_addresses)
+ a->callback = NULL;
+
+ return dhcp6_pd_remove_old(address->link, true);
+}
+
+static int dhcp6_pd_remove_old(Link *link, bool force) {
+ Address *address;
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+ assert(link->manager);
+
+ if (!force && (link->dhcp6_pd_address_messages != 0 || link->dhcp6_pd_route_configured != 0))
+ return 0;
+
+ if (set_isempty(link->dhcp6_pd_addresses_old) && set_isempty(link->dhcp6_pd_routes_old))
+ return 0;
+
+ if (!force) {
+ bool set_callback = !set_isempty(link->dhcp6_pd_addresses);
+
+ SET_FOREACH(address, link->dhcp6_pd_addresses)
+ if (address_is_ready(address)) {
+ set_callback = false;
+ break;
+ }
+
+ if (set_callback) {
+ SET_FOREACH(address, link->dhcp6_pd_addresses)
+ address->callback = dhcp6_pd_address_callback;
+ return 0;
+ }
+ }
+
+ log_link_debug(link, "Removing old DHCPv6 Prefix Delegation addresses and routes.");
+
+ link_dirty(link);
+
+ SET_FOREACH(route, link->dhcp6_pd_routes_old) {
+ k = route_remove(route, NULL, link, NULL);
+ if (k < 0)
+ r = k;
+
+ if (link->radv)
+ (void) sd_radv_remove_prefix(link->radv, &route->dst.in6, 64);
+ dhcp6_pd_free(hashmap_get(link->manager->dhcp6_prefixes, &route->dst.in6));
+ }
+
+ SET_FOREACH(address, link->dhcp6_pd_addresses_old) {
+ k = address_remove(address, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ return r;
+}
+
+int dhcp6_pd_remove(Link *link) {
+ Address *address;
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+ assert(link->manager);
+
+ if (!link_dhcp6_pd_is_enabled(link))
+ return 0;
+
+ link->dhcp6_pd_address_configured = false;
+ link->dhcp6_pd_route_configured = false;
+
+ k = dhcp6_pd_remove_old(link, true);
+ if (k < 0)
+ r = k;
+
+ if (set_isempty(link->dhcp6_pd_addresses) && set_isempty(link->dhcp6_pd_routes))
+ return r;
+
+ log_link_debug(link, "Removing DHCPv6 Prefix Delegation addresses and routes.");
+
+ link_dirty(link);
+
+ SET_FOREACH(route, link->dhcp6_pd_routes) {
+ k = route_remove(route, NULL, link, NULL);
+ if (k < 0)
+ r = k;
+
+ if (link->radv)
+ (void) sd_radv_remove_prefix(link->radv, &route->dst.in6, 64);
+ dhcp6_pd_free(hashmap_get(link->manager->dhcp6_prefixes, &route->dst.in6));
+ }
+
+ SET_FOREACH(address, link->dhcp6_pd_addresses) {
+ k = address_remove(address, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int dhcp6_pd_route_handler(sd_netlink *nl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->dhcp6_pd_route_messages > 0);
+
+ link->dhcp6_pd_route_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Failed to add DHCPv6 Prefix Delegation route");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->dhcp6_pd_route_messages == 0) {
+ log_link_debug(link, "DHCPv6 prefix delegation routes set");
+ if (link->dhcp6_pd_prefixes_assigned)
+ link->dhcp6_pd_route_configured = true;
+
+ r = dhcp6_pd_remove_old(link, false);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int dhcp6_set_pd_route(Link *link, const union in_addr_union *prefix, const union in_addr_union *pd_prefix) {
+ _cleanup_(dhcp6_pd_freep) DHCP6DelegatedPrefix *pd = NULL;
+ _cleanup_(route_freep) Route *route = NULL;
+ Link *assigned_link;
+ Route *ret;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(prefix);
+ assert(pd_prefix);
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route->family = AF_INET6;
+ route->dst = *prefix;
+ route->dst_prefixlen = 64;
+
+ r = route_configure(route, link, dhcp6_pd_route_handler, &ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set DHCPv6 prefix route: %m");
+
+ link->dhcp6_pd_route_messages++;
+
+ r = set_ensure_put(&link->dhcp6_pd_routes, &route_hash_ops, ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store DHCPv6 prefix route: %m");
+
+ (void) set_remove(link->dhcp6_pd_routes_old, ret);
+
+ assigned_link = dhcp6_pd_get_link_by_prefix(link, prefix);
+ if (assigned_link) {
+ assert(assigned_link == link);
+ return 0;
+ }
+
+ pd = new(DHCP6DelegatedPrefix, 1);
+ if (!pd)
+ return log_oom();
+
+ *pd = (DHCP6DelegatedPrefix) {
+ .prefix = prefix->in6,
+ .pd_prefix = pd_prefix->in6,
+ .link = link_ref(link),
+ };
+
+ r = hashmap_ensure_allocated(&link->manager->dhcp6_prefixes, &in6_addr_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ r = hashmap_put(link->manager->dhcp6_prefixes, &pd->prefix, pd);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store DHCPv6 prefix route at manager: %m");
+
+ r = set_ensure_put(&link->manager->dhcp6_pd_prefixes, &dhcp6_pd_hash_ops, pd);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store DHCPv6 prefix route at manager: %m");
+
+ TAKE_PTR(pd);
+ return 0;
+}
+
+static int dhcp6_pd_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->dhcp6_pd_address_messages > 0);
+
+ link->dhcp6_pd_address_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set DHCPv6 delegated prefix address");
+ link_enter_failed(link);
+ return 1;
+ } else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ if (link->dhcp6_pd_address_messages == 0) {
+ log_link_debug(link, "DHCPv6 delegated prefix addresses set");
+ if (link->dhcp6_pd_prefixes_assigned)
+ link->dhcp6_pd_address_configured = true;
+
+ r = dhcp6_pd_remove_old(link, false);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+
+ r = link_set_routes(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+ }
+
+ return 1;
+}
+
+static int dhcp6_set_pd_address(Link *link,
+ const union in_addr_union *prefix,
+ uint8_t prefix_len,
+ uint32_t lifetime_preferred,
+ uint32_t lifetime_valid) {
+
+ _cleanup_(address_freep) Address *address = NULL;
+ Address *ret;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(prefix);
+
+ if (!link->network->dhcp6_pd_assign)
+ return 0;
+
+ r = address_new(&address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to allocate address for DHCPv6 delegated prefix: %m");
+
+ address->in_addr = *prefix;
+
+ if (!in_addr_is_null(AF_INET6, &link->network->dhcp6_pd_token))
+ memcpy(address->in_addr.in6.s6_addr + 8, link->network->dhcp6_pd_token.in6.s6_addr + 8, 8);
+ else {
+ r = generate_ipv6_eui_64_address(link, &address->in_addr.in6);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to generate EUI64 address for acquired DHCPv6 delegated prefix: %m");
+ }
+
+ address->prefixlen = prefix_len;
+ address->family = AF_INET6;
+ address->cinfo.ifa_prefered = lifetime_preferred;
+ address->cinfo.ifa_valid = lifetime_valid;
+
+ r = address_configure(address, link, dhcp6_pd_address_handler, true, &ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set DHCPv6 delegated prefix address: %m");
+
+ link->dhcp6_pd_address_messages++;
+
+ r = set_ensure_put(&link->dhcp6_pd_addresses, &address_hash_ops, ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store DHCPv6 delegated prefix address: %m");
+
+ (void) set_remove(link->dhcp6_pd_addresses_old, ret);
+
+ return 0;
+}
+
+static int dhcp6_pd_assign_prefix(Link *link, const union in_addr_union *prefix, const union in_addr_union *pd_prefix,
+ uint8_t prefix_len, uint32_t lifetime_preferred, uint32_t lifetime_valid) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(prefix);
+
+ if (link->network->dhcp6_pd_announce) {
+ r = radv_add_prefix(link, &prefix->in6, prefix_len, lifetime_preferred, lifetime_valid);
+ if (r < 0)
+ return r;
+ }
+
+ r = dhcp6_set_pd_route(link, prefix, pd_prefix);
+ if (r < 0)
+ return r;
+
+ r = dhcp6_set_pd_address(link, prefix, prefix_len, lifetime_preferred, lifetime_valid);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static bool link_has_preferred_subnet_id(Link *link) {
+ if (!link->network)
+ return false;
+
+ return link->network->dhcp6_pd_subnet_id >= 0;
+}
+
+static int dhcp6_get_preferred_delegated_prefix(
+ Link *link,
+ const union in_addr_union *masked_pd_prefix,
+ uint8_t pd_prefix_len,
+ union in_addr_union *ret) {
+
+ /* We start off with the original PD prefix we have been assigned and iterate from there */
+ union in_addr_union prefix;
+ uint64_t n_prefixes;
+ Link *assigned_link;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(masked_pd_prefix);
+ assert(pd_prefix_len <= 64);
+
+ n_prefixes = UINT64_C(1) << (64 - pd_prefix_len);
+ prefix = *masked_pd_prefix;
+
+ if (link_has_preferred_subnet_id(link)) {
+ uint64_t subnet_id = link->network->dhcp6_pd_subnet_id;
+
+ /* If the link has a preference for a particular subnet id try to allocate that */
+ if (subnet_id >= n_prefixes)
+ return log_link_warning_errno(link, SYNTHETIC_ERRNO(ERANGE),
+ "subnet id %" PRIu64 " is out of range. Only have %" PRIu64 " subnets.",
+ subnet_id, n_prefixes);
+
+ r = in_addr_prefix_nth(AF_INET6, &prefix, 64, subnet_id);
+ if (r < 0)
+ return log_link_warning_errno(link, r,
+ "subnet id %" PRIu64 " is out of range. Only have %" PRIu64 " subnets.",
+ subnet_id, n_prefixes);
+
+ /* Verify that the prefix we did calculate fits in the pd prefix.
+ * This should not fail as we checked the prefix size beforehand */
+ assert_se(in_addr_prefix_covers(AF_INET6, masked_pd_prefix, pd_prefix_len, &prefix) > 0);
+
+ assigned_link = dhcp6_pd_get_link_by_prefix(link, &prefix);
+ if (assigned_link && assigned_link != link) {
+ _cleanup_free_ char *assigned_buf = NULL;
+
+ (void) in_addr_to_string(AF_INET6, &prefix, &assigned_buf);
+ return log_link_warning_errno(link, SYNTHETIC_ERRNO(EAGAIN),
+ "The requested prefix %s is already assigned to another link.",
+ strna(assigned_buf));
+ }
+
+ *ret = prefix;
+ return 0;
+ }
+
+ for (uint64_t n = 0; n < n_prefixes; n++) {
+ /* If we do not have an allocation preference just iterate
+ * through the address space and return the first free prefix. */
+ assigned_link = dhcp6_pd_get_link_by_prefix(link, &prefix);
+ if (!assigned_link || assigned_link == link) {
+ *ret = prefix;
+ return 0;
+ }
+
+ r = in_addr_prefix_next(AF_INET6, &prefix, 64);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Can't allocate another prefix. Out of address space?: %m");
+ }
+
+ return log_link_warning_errno(link, SYNTHETIC_ERRNO(ERANGE), "Couldn't find a suitable prefix. Ran out of address space.");
+}
+
+static void dhcp6_pd_prefix_distribute(Link *dhcp6_link,
+ const union in_addr_union *masked_pd_prefix,
+ uint8_t pd_prefix_len,
+ uint32_t lifetime_preferred,
+ uint32_t lifetime_valid,
+ bool assign_preferred_subnet_id) {
+
+ Link *link;
+ int r;
+
+ assert(dhcp6_link);
+ assert(dhcp6_link->manager);
+ assert(masked_pd_prefix);
+ assert(pd_prefix_len <= 64);
+
+ HASHMAP_FOREACH(link, dhcp6_link->manager->links) {
+ _cleanup_free_ char *assigned_buf = NULL;
+ union in_addr_union assigned_prefix;
+
+ if (link == dhcp6_link)
+ continue;
+
+ if (!link_dhcp6_pd_is_enabled(link))
+ continue;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ continue;
+
+ if (assign_preferred_subnet_id != link_has_preferred_subnet_id(link))
+ continue;
+
+ r = dhcp6_pd_get_assigned_prefix(link, masked_pd_prefix, &assigned_prefix);
+ if (r < 0) {
+ r = dhcp6_get_preferred_delegated_prefix(link, masked_pd_prefix, pd_prefix_len, &assigned_prefix);
+ if (r < 0) {
+ link->dhcp6_pd_prefixes_assigned = false;
+ continue;
+ }
+ }
+
+ (void) in_addr_to_string(AF_INET6, &assigned_prefix, &assigned_buf);
+ r = dhcp6_pd_assign_prefix(link, &assigned_prefix, masked_pd_prefix, 64,
+ lifetime_preferred, lifetime_valid);
+ if (r < 0) {
+ log_link_error_errno(link, r, "Unable to assign/update prefix %s/64: %m",
+ strna(assigned_buf));
+ link_enter_failed(link);
+ } else
+ log_link_debug(link, "Assigned prefix %s/64", strna(assigned_buf));
+ }
+}
+
+static int dhcp6_pd_prepare(Link *link) {
+ Address *address;
+ Route *route;
+ int r;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ if (!link_dhcp6_pd_is_enabled(link))
+ return 0;
+
+ link_dirty(link);
+
+ link->dhcp6_pd_address_configured = false;
+ link->dhcp6_pd_route_configured = false;
+ link->dhcp6_pd_prefixes_assigned = true;
+
+ while ((address = set_steal_first(link->dhcp6_pd_addresses))) {
+ r = set_ensure_put(&link->dhcp6_pd_addresses_old, &address_hash_ops, address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store old DHCPv6 Prefix Delegation address: %m");
+ }
+
+ while ((route = set_steal_first(link->dhcp6_pd_routes))) {
+ r = set_ensure_put(&link->dhcp6_pd_routes_old, &route_hash_ops, route);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store old DHCPv6 Prefix Delegation route: %m");
+ }
+
+ return 0;
+}
+
+static int dhcp6_pd_finalize(Link *link) {
+ int r;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 0;
+
+ if (!link_dhcp6_pd_is_enabled(link))
+ return 0;
+
+ if (link->dhcp6_pd_address_messages == 0) {
+ if (link->dhcp6_pd_prefixes_assigned)
+ link->dhcp6_pd_address_configured = true;
+ } else {
+ log_link_debug(link, "Setting DHCPv6 PD addresses");
+ /* address_handler calls link_set_routes() and link_set_nexthop(). Before they are
+ * called, the related flags must be cleared. Otherwise, the link becomes configured
+ * state before routes are configured. */
+ link->static_routes_configured = false;
+ link->static_nexthops_configured = false;
+ }
+
+ if (link->dhcp6_pd_route_messages == 0) {
+ if (link->dhcp6_pd_prefixes_assigned)
+ link->dhcp6_pd_route_configured = true;
+ } else
+ log_link_debug(link, "Setting DHCPv6 PD routes");
+
+ r = dhcp6_pd_remove_old(link, false);
+ if (r < 0)
+ return r;
+
+ if (link->dhcp6_pd_address_configured && link->dhcp6_pd_route_configured)
+ link_check_ready(link);
+ else
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ return 0;
+}
+
+static void dhcp6_pd_prefix_lost(Link *dhcp6_link) {
+ Link *link;
+ int r;
+
+ assert(dhcp6_link);
+ assert(dhcp6_link->manager);
+
+ HASHMAP_FOREACH(link, dhcp6_link->manager->links) {
+ if (link == dhcp6_link)
+ continue;
+
+ r = dhcp6_pd_remove(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+}
+
+static int dhcp6_remove_old(Link *link, bool force);
+
+static int dhcp6_address_callback(Address *address) {
+ Address *a;
+
+ assert(address);
+ assert(address->link);
+
+ /* Make this called only once */
+ SET_FOREACH(a, address->link->dhcp6_addresses)
+ a->callback = NULL;
+
+ return dhcp6_remove_old(address->link, true);
+}
+
+static int dhcp6_remove_old(Link *link, bool force) {
+ Address *address;
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+
+ if (!force && (!link->dhcp6_address_configured || !link->dhcp6_route_configured))
+ return 0;
+
+ if (set_isempty(link->dhcp6_addresses_old) && set_isempty(link->dhcp6_routes_old))
+ return 0;
+
+ if (!force) {
+ bool set_callback = !set_isempty(link->dhcp6_addresses);
+
+ SET_FOREACH(address, link->dhcp6_addresses)
+ if (address_is_ready(address)) {
+ set_callback = false;
+ break;
+ }
+
+ if (set_callback) {
+ SET_FOREACH(address, link->dhcp6_addresses)
+ address->callback = dhcp6_address_callback;
+ return 0;
+ }
+ }
+
+ log_link_debug(link, "Removing old DHCPv6 addresses and routes.");
+
+ link_dirty(link);
+
+ SET_FOREACH(route, link->dhcp6_routes_old) {
+ k = route_remove(route, NULL, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ SET_FOREACH(address, link->dhcp6_addresses_old) {
+ k = address_remove(address, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int dhcp6_remove(Link *link) {
+ Address *address;
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+
+ link->dhcp6_address_configured = false;
+ link->dhcp6_route_configured = false;
+
+ k = dhcp6_remove_old(link, true);
+ if (k < 0)
+ r = k;
+
+ if (set_isempty(link->dhcp6_addresses) && set_isempty(link->dhcp6_routes))
+ return r;
+
+ log_link_debug(link, "Removing DHCPv6 addresses and routes.");
+
+ link_dirty(link);
+
+ SET_FOREACH(route, link->dhcp6_routes) {
+ k = route_remove(route, NULL, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ SET_FOREACH(address, link->dhcp6_addresses) {
+ k = address_remove(address, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int dhcp6_route_handler(sd_netlink *nl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->dhcp6_route_messages > 0);
+
+ link->dhcp6_route_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Failed to add unreachable route for DHCPv6 delegated subnet");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->dhcp6_route_messages == 0) {
+ log_link_debug(link, "Unreachable routes for DHCPv6 delegated subnets set");
+ link->dhcp6_route_configured = true;
+
+ r = dhcp6_remove_old(link, false);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int dhcp6_set_unreachable_route(Link *link, const union in_addr_union *addr, uint8_t prefixlen) {
+ _cleanup_(route_freep) Route *route = NULL;
+ _cleanup_free_ char *buf = NULL;
+ Route *ret;
+ int r;
+
+ assert(link);
+ assert(addr);
+
+ (void) in_addr_to_string(AF_INET6, addr, &buf);
+
+ if (prefixlen > 64) {
+ log_link_debug(link, "PD Prefix length > 64, ignoring prefix %s/%u",
+ strna(buf), prefixlen);
+ return 0;
+ }
+
+ if (prefixlen == 64) {
+ log_link_debug(link, "Not adding a blocking route for DHCPv6 delegated subnet %s/64 since distributed prefix is 64",
+ strna(buf));
+ return 1;
+ }
+
+ if (prefixlen < 48)
+ log_link_warning(link, "PD Prefix length < 48, looks unusual %s/%u",
+ strna(buf), prefixlen);
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->family = AF_INET6;
+ route->dst = *addr;
+ route->dst_prefixlen = prefixlen;
+ route->table = link_get_dhcp_route_table(link);
+ route->type = RTN_UNREACHABLE;
+
+ r = route_configure(route, link, dhcp6_route_handler, &ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set unreachable route for DHCPv6 delegated subnet %s/%u: %m",
+ strna(buf), prefixlen);
+
+ link->dhcp6_route_messages++;
+
+ r = set_ensure_put(&link->dhcp6_routes, &route_hash_ops, ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store unreachable route for DHCPv6 delegated subnet %s/%u: %m",
+ strna(buf), prefixlen);
+
+ (void) set_remove(link->dhcp6_routes_old, ret);
+
+ return 1;
+}
+
+static int dhcp6_pd_prefix_acquired(Link *dhcp6_link) {
+ Link *link;
+ int r;
+
+ assert(dhcp6_link);
+ assert(dhcp6_link->dhcp6_lease);
+
+ HASHMAP_FOREACH(link, dhcp6_link->manager->links) {
+ if (link == dhcp6_link)
+ continue;
+
+ r = dhcp6_pd_prepare(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ for (sd_dhcp6_lease_reset_pd_prefix_iter(dhcp6_link->dhcp6_lease);;) {
+ uint32_t lifetime_preferred, lifetime_valid;
+ union in_addr_union pd_prefix, prefix;
+ uint8_t pd_prefix_len;
+
+ r = sd_dhcp6_lease_get_pd(dhcp6_link->dhcp6_lease, &pd_prefix.in6, &pd_prefix_len,
+ &lifetime_preferred, &lifetime_valid);
+ if (r < 0)
+ break;
+
+ r = dhcp6_set_unreachable_route(dhcp6_link, &pd_prefix, pd_prefix_len);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ /* We are doing prefix allocation in two steps:
+ * 1. all those links that have a preferred subnet id will be assigned their subnet
+ * 2. all those links that remain will receive prefixes in sequential order. Prefixes
+ * that were previously already allocated to another link will be skipped.
+ * The assignment has to be split in two phases since subnet id
+ * preferences should be honored. Meaning that any subnet id should be
+ * handed out to the requesting link and not to some link that didn't
+ * specify any preference. */
+
+ assert(pd_prefix_len <= 64);
+
+ prefix = pd_prefix;
+ r = in_addr_mask(AF_INET6, &prefix, pd_prefix_len);
+ if (r < 0)
+ return log_link_error_errno(dhcp6_link, r, "Failed to mask DHCPv6 PD prefix: %m");
+
+ if (DEBUG_LOGGING) {
+ uint64_t n_prefixes = UINT64_C(1) << (64 - pd_prefix_len);
+ _cleanup_free_ char *buf = NULL;
+
+ (void) in_addr_to_string(AF_INET6, &prefix, &buf);
+ log_link_debug(dhcp6_link, "Assigning up to %" PRIu64 " prefixes from %s/%u",
+ n_prefixes, strna(buf), pd_prefix_len);
+ }
+
+ dhcp6_pd_prefix_distribute(dhcp6_link,
+ &prefix,
+ pd_prefix_len,
+ lifetime_preferred,
+ lifetime_valid,
+ true);
+
+ dhcp6_pd_prefix_distribute(dhcp6_link,
+ &prefix,
+ pd_prefix_len,
+ lifetime_preferred,
+ lifetime_valid,
+ false);
+ }
+
+ HASHMAP_FOREACH(link, dhcp6_link->manager->links) {
+ if (link == dhcp6_link)
+ continue;
+
+ r = dhcp6_pd_finalize(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ return 0;
+}
+
+static int dhcp6_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->dhcp6_address_messages > 0);
+
+ link->dhcp6_address_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not set DHCPv6 address");
+ link_enter_failed(link);
+ return 1;
+ } else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ if (link->dhcp6_address_messages == 0) {
+ log_link_debug(link, "DHCPv6 addresses set");
+ link->dhcp6_address_configured = true;
+
+ r = dhcp6_remove_old(link, false);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+
+ r = link_set_routes(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+ }
+
+ return 1;
+}
+
+static int dhcp6_update_address(
+ Link *link,
+ const struct in6_addr *ip6_addr,
+ uint32_t lifetime_preferred,
+ uint32_t lifetime_valid) {
+
+ _cleanup_(address_freep) Address *addr = NULL;
+ _cleanup_free_ char *buffer = NULL;
+ Address *ret;
+ int r;
+
+ r = address_new(&addr);
+ if (r < 0)
+ return log_oom();
+
+ addr->family = AF_INET6;
+ addr->in_addr.in6 = *ip6_addr;
+ addr->flags = IFA_F_NOPREFIXROUTE;
+ addr->prefixlen = 128;
+ addr->cinfo.ifa_prefered = lifetime_preferred;
+ addr->cinfo.ifa_valid = lifetime_valid;
+
+ (void) in_addr_to_string(addr->family, &addr->in_addr, &buffer);
+ log_link_full(link, set_contains(link->dhcp6_addresses, addr) ? LOG_DEBUG : LOG_INFO,
+ "DHCPv6 address %s/%u timeout preferred %d valid %d",
+ strna(buffer), addr->prefixlen, lifetime_preferred, lifetime_valid);
+
+ r = address_configure(addr, link, dhcp6_address_handler, true, &ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set DHCPv6 address %s/%u: %m",
+ strna(buffer), addr->prefixlen);
+
+ link->dhcp6_address_messages++;
+
+ r = set_ensure_put(&link->dhcp6_addresses, &address_hash_ops, ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store DHCPv6 address %s/%u: %m",
+ strna(buffer), addr->prefixlen);
+
+ (void) set_remove(link->dhcp6_addresses_old, ret);
+
+ return 0;
+}
+
+static int dhcp6_address_acquired(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->dhcp6_lease);
+
+ for (sd_dhcp6_lease_reset_address_iter(link->dhcp6_lease);;) {
+ uint32_t lifetime_preferred, lifetime_valid;
+ struct in6_addr ip6_addr;
+
+ r = sd_dhcp6_lease_get_address(link->dhcp6_lease, &ip6_addr, &lifetime_preferred, &lifetime_valid);
+ if (r < 0)
+ break;
+
+ r = dhcp6_update_address(link, &ip6_addr, lifetime_preferred, lifetime_valid);
+ if (r < 0)
+ return r;
+ }
+
+ 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;
+ Address *a;
+ Route *rt;
+ int r;
+
+ link->dhcp6_address_configured = false;
+ link->dhcp6_route_configured = false;
+
+ link_dirty(link);
+
+ while ((a = set_steal_first(link->dhcp6_addresses))) {
+ r = set_ensure_put(&link->dhcp6_addresses_old, &address_hash_ops, a);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store old DHCPv6 address: %m");
+ }
+
+ while ((rt = set_steal_first(link->dhcp6_routes))) {
+ r = set_ensure_put(&link->dhcp6_routes_old, &route_hash_ops, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store old DHCPv6 route: %m");
+ }
+
+ 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. */
+ dhcp6_pd_prefix_lost(link);
+
+ if (link->dhcp6_address_messages == 0)
+ link->dhcp6_address_configured = true;
+ else {
+ log_link_debug(link, "Setting DHCPv6 addresses");
+ /* address_handler calls link_set_routes() and link_set_nexthop(). Before they are
+ * called, the related flags must be cleared. Otherwise, the link becomes configured
+ * state before routes are configured. */
+ link->static_routes_configured = false;
+ link->static_nexthops_configured = false;
+ }
+
+ if (link->dhcp6_route_messages == 0)
+ link->dhcp6_route_configured = true;
+ else
+ log_link_debug(link, "Setting unreachable routes for DHCPv6 delegated subnets");
+
+ r = dhcp6_remove_old(link, false);
+ if (r < 0)
+ return r;
+
+ if (link->dhcp6_address_configured && link->dhcp6_route_configured)
+ link_check_ready(link);
+ else
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ 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))
+ dhcp6_pd_prefix_lost(link);
+
+ link->dhcp6_lease = sd_dhcp6_lease_unref(link->dhcp6_lease);
+
+ r = dhcp6_remove(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static void dhcp6_handler(sd_dhcp6_client *client, int event, void *userdata) {
+ Link *link = userdata;
+ int r;
+
+ assert(link);
+ 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_request_address(Link *link, int ir) {
+ int r, inf_req, pd;
+ bool running;
+
+ assert(link);
+ assert(link->dhcp6_client);
+ assert(link->network);
+ assert(in_addr_is_link_local(AF_INET6, (const union in_addr_union*) &link->ipv6ll_address) > 0);
+
+ r = sd_dhcp6_client_is_running(link->dhcp6_client);
+ if (r < 0)
+ return r;
+ running = r;
+
+ r = sd_dhcp6_client_get_prefix_delegation(link->dhcp6_client, &pd);
+ if (r < 0)
+ return r;
+
+ if (pd && ir && link->network->dhcp6_force_pd_other_information) {
+ log_link_debug(link, "Enabling managed mode to request DHCPv6 PD with 'Other Information' set");
+
+ r = sd_dhcp6_client_set_address_request(link->dhcp6_client, false);
+ if (r < 0)
+ return r;
+
+ ir = false;
+ }
+
+ if (running) {
+ r = sd_dhcp6_client_get_information_request(link->dhcp6_client, &inf_req);
+ if (r < 0)
+ return r;
+
+ if (inf_req == ir)
+ return 0;
+
+ 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, ir);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp6_client_start(link->dhcp6_client);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int dhcp6_request_prefix_delegation(Link *link) {
+ Link *l;
+
+ assert(link);
+ assert(link->manager);
+
+ if (!link_dhcp6_pd_is_enabled(link))
+ return 0;
+
+ log_link_debug(link, "Requesting DHCPv6 prefixes to be delegated for new link");
+
+ HASHMAP_FOREACH(l, link->manager->links) {
+ int r, enabled;
+
+ if (l == link)
+ continue;
+
+ if (!l->dhcp6_client)
+ continue;
+
+ r = sd_dhcp6_client_get_prefix_delegation(l->dhcp6_client, &enabled);
+ if (r < 0) {
+ log_link_warning_errno(l, r, "Cannot get prefix delegation when adding new link: %m");
+ link_enter_failed(l);
+ continue;
+ }
+
+ if (enabled == 0) {
+ r = sd_dhcp6_client_set_prefix_delegation(l->dhcp6_client, 1);
+ if (r < 0) {
+ log_link_warning_errno(l, r, "Cannot enable prefix delegation when adding new link: %m");
+ link_enter_failed(l);
+ continue;
+ }
+ }
+
+ r = sd_dhcp6_client_is_running(l->dhcp6_client);
+ if (r <= 0)
+ continue;
+
+ if (enabled != 0) {
+ if (dhcp6_lease_has_pd_prefix(l->dhcp6_lease)) {
+ log_link_debug(l, "Requesting re-assignment of delegated prefixes after adding new link");
+ r = dhcp6_pd_prefix_acquired(l);
+ if (r < 0)
+ link_enter_failed(l);
+ }
+ continue;
+ }
+
+ r = sd_dhcp6_client_stop(l->dhcp6_client);
+ if (r < 0) {
+ log_link_warning_errno(l, r, "Cannot stop DHCPv6 prefix delegation client after adding new link: %m");
+ link_enter_failed(l);
+ continue;
+ }
+
+ r = sd_dhcp6_client_start(l->dhcp6_client);
+ if (r < 0) {
+ log_link_warning_errno(l, r, "Cannot restart DHCPv6 prefix delegation client after adding new link: %m");
+ link_enter_failed(l);
+ continue;
+ }
+
+ log_link_debug(l, "Restarted DHCPv6 client to acquire prefix delegations after adding new link");
+ }
+
+ /* dhcp6_pd_prefix_acquired() may make the link in failed state. */
+ if (link->state == LINK_STATE_FAILED)
+ return -ENOANO;
+
+ return 0;
+}
+
+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 r;
+
+ 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_warning_errno(link, r, "DHCP6 CLIENT: Failed to set hostname from kernel hostname, ignoring: %m");
+ else if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 CLIENT: Failed to set hostname: %m");
+
+ return 0;
+}
+
+static bool dhcp6_enable_prefix_delegation(Link *dhcp6_link) {
+ Link *link;
+
+ assert(dhcp6_link);
+ assert(dhcp6_link->manager);
+
+ HASHMAP_FOREACH(link, dhcp6_link->manager->links) {
+ if (link == dhcp6_link)
+ continue;
+
+ if (!link_dhcp6_pd_is_enabled(link))
+ continue;
+
+ return true;
+ }
+
+ return false;
+}
+
+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.addr.bytes, link->hw_addr.length, link->iftype);
+ if (r < 0)
+ return r;
+
+ if (link->network->iaid_set) {
+ r = sd_dhcp6_client_set_iaid(client, link->network->iaid);
+ if (r < 0)
+ return r;
+ }
+
+ duid = link_get_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;
+}
+
+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_enabled(link) && !link_ipv6_accept_ra_enabled(link))
+ return 0;
+
+ if (link->dhcp6_client)
+ return 0;
+
+ r = sd_dhcp6_client_new(&client);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 CLIENT: Failed to create DHCP6 client: %m");
+
+ r = sd_dhcp6_client_attach_event(client, link->manager->event, 0);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 CLIENT: Failed to attach event: %m");
+
+ r = dhcp6_set_identifier(link, client);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 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_error_errno(link, r, "DHCP6 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_error_errno(link, r, "DHCP6 CLIENT: Failed to set ifindex: %m");
+
+ if (link->network->dhcp6_rapid_commit) {
+ r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_RAPID_COMMIT);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 CLIENT: Failed to set request flag for rapid commit: %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_error_errno(link, r, "DHCP6 CLIENT: Failed to set MUD URL: %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, "DHCP6 CLIENT: Failed to set request flag for '%u' already exists, ignoring.", option);
+ continue;
+ }
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 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_error_errno(link, r, "DHCP6 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_error_errno(link, r, "DHCP6 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_error_errno(link, r, "DHCP6 CLIENT: Failed to set vendor option: %m");
+ }
+
+ r = sd_dhcp6_client_set_callback(client, dhcp6_handler, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 CLIENT: Failed to set callback: %m");
+
+ if (dhcp6_enable_prefix_delegation(link)) {
+ r = sd_dhcp6_client_set_prefix_delegation(client, true);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 CLIENT: Failed to set prefix delegation: %m");
+ }
+
+ if (link->network->dhcp6_pd_length > 0) {
+ r = sd_dhcp6_client_set_prefix_delegation_hint(client, link->network->dhcp6_pd_length, &link->network->dhcp6_pd_address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP6 CLIENT: Failed to set prefix hint: %m");
+ }
+
+ 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;
+}
+
+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_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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = in_addr_prefix_from_string(rvalue, AF_INET6, (union in_addr_union *) &network->dhcp6_pd_address, &network->dhcp6_pd_length);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse PrefixDelegationHint=%s, ignoring assignment", rvalue);
+ return 0;
+ }
+
+ if (network->dhcp6_pd_length < 1 || network->dhcp6_pd_length > 128) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid prefix length='%d', ignoring assignment", network->dhcp6_pd_length);
+ network->dhcp6_pd_length = 0;
+ return 0;
+ }
+
+ return 0;
+}
+
+int config_parse_dhcp6_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;
+ Network *network = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ network->dhcp6_mudurl = mfree(network->dhcp6_mudurl);
+ return 0;
+ }
+
+ r = cunescape(rvalue, 0, &unescaped);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to Failed to unescape MUD URL, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (!http_url_is_valid(unescaped) || strlen(unescaped) > UINT8_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse MUD URL '%s', ignoring: %m", rvalue);
+ return 0;
+ }
+
+ return free_and_replace(network->dhcp6_mudurl, unescaped);
+}
+
+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);
+
+int config_parse_dhcp6_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 = data;
+ uint64_t t;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+}
+
+int config_parse_dhcp6_pd_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) {
+
+ union in_addr_union *addr = data, tmp;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ *addr = IN_ADDR_NULL;
+ return 0;
+ }
+
+ r = in_addr_from_string(AF_INET6, rvalue, &tmp);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse DHCPv6 Prefix Delegation token, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (in_addr_is_null(AF_INET6, &tmp)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "DHCPv6 Prefix Delegation token cannot be the ANY address, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ *addr = tmp;
+
+ return 0;
+}
diff --git a/src/network/networkd-dhcp6.h b/src/network/networkd-dhcp6.h
new file mode 100644
index 0000000..65b35fd
--- /dev/null
+++ b/src/network/networkd-dhcp6.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-dhcp6-client.h"
+
+#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 = -1,
+} DHCP6ClientStartMode;
+
+typedef struct Link Link;
+typedef struct Manager Manager;
+
+typedef struct DHCP6DelegatedPrefix {
+ struct in6_addr prefix; /* Prefix assigned to the link */
+ struct in6_addr pd_prefix; /* PD prefix provided by DHCP6 lease */
+ Link *link;
+} DHCP6DelegatedPrefix;
+
+DHCP6DelegatedPrefix *dhcp6_pd_free(DHCP6DelegatedPrefix *p);
+DEFINE_TRIVIAL_CLEANUP_FUNC(DHCP6DelegatedPrefix*, dhcp6_pd_free);
+
+bool link_dhcp6_pd_is_enabled(Link *link);
+int dhcp6_pd_remove(Link *link);
+int dhcp6_configure(Link *link);
+int dhcp6_update_mac(Link *link);
+int dhcp6_request_address(Link *link, int ir);
+int dhcp6_request_prefix_delegation(Link *link);
+
+int link_serialize_dhcp6_client(Link *link, FILE *f);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_pd_hint);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_mud_url);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_client_start_mode);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_pd_subnet_id);
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_pd_token);
+
+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-fdb.c b/src/network/networkd-fdb.c
new file mode 100644
index 0000000..283dece
--- /dev/null
+++ b/src/network/networkd-fdb.c
@@ -0,0 +1,409 @@
+/* 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-fdb.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "vlan-util.h"
+#include "vxlan.h"
+
+#define STATIC_FDB_ENTRIES_PER_NETWORK_MAX 1024U
+
+/* remove and FDB entry. */
+FdbEntry *fdb_entry_free(FdbEntry *fdb_entry) {
+ if (!fdb_entry)
+ return NULL;
+
+ if (fdb_entry->network) {
+ assert(fdb_entry->section);
+ hashmap_remove(fdb_entry->network->fdb_entries_by_section, fdb_entry->section);
+ }
+
+ network_config_section_free(fdb_entry->section);
+ return mfree(fdb_entry);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(FdbEntry, fdb_entry_free);
+
+/* create a new FDB entry or get an existing one. */
+static int fdb_entry_new_static(
+ Network *network,
+ const char *filename,
+ unsigned section_line,
+ FdbEntry **ret) {
+
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(fdb_entry_freep) FdbEntry *fdb_entry = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ /* search entry in hashmap first. */
+ fdb_entry = hashmap_get(network->fdb_entries_by_section, n);
+ if (fdb_entry) {
+ *ret = TAKE_PTR(fdb_entry);
+ return 0;
+ }
+
+ if (hashmap_size(network->fdb_entries_by_section) >= STATIC_FDB_ENTRIES_PER_NETWORK_MAX)
+ return -E2BIG;
+
+ /* allocate space for and FDB entry. */
+ fdb_entry = new(FdbEntry, 1);
+ if (!fdb_entry)
+ return -ENOMEM;
+
+ /* init FDB structure. */
+ *fdb_entry = (FdbEntry) {
+ .network = network,
+ .section = TAKE_PTR(n),
+ .vni = VXLAN_VID_MAX + 1,
+ .fdb_ntf_flags = NEIGHBOR_CACHE_ENTRY_FLAGS_SELF,
+ };
+
+ r = hashmap_ensure_allocated(&network->fdb_entries_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->fdb_entries_by_section, fdb_entry->section, fdb_entry);
+ if (r < 0)
+ return r;
+
+ /* return allocated FDB structure. */
+ *ret = TAKE_PTR(fdb_entry);
+
+ return 0;
+}
+
+static int set_fdb_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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 != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not add FDB entry");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ return 1;
+}
+
+/* send a request to the kernel to add a FDB entry in its static MAC table. */
+static int fdb_entry_configure(Link *link, FdbEntry *fdb_entry) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(fdb_entry);
+
+ /* create new RTM message */
+ r = sd_rtnl_message_new_neigh(link->manager->rtnl, &req, RTM_NEWNEIGH, link->ifindex, AF_BRIDGE);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create RTM_NEWNEIGH message: %m");
+
+ r = sd_rtnl_message_neigh_set_flags(req, fdb_entry->fdb_ntf_flags);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set neighbor flags: %m");
+
+ /* only NUD_PERMANENT state supported. */
+ r = sd_rtnl_message_neigh_set_state(req, NUD_NOARP | NUD_PERMANENT);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set neighbor state: %m");
+
+ r = sd_netlink_message_append_data(req, NDA_LLADDR, &fdb_entry->mac_addr, sizeof(fdb_entry->mac_addr));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NDA_LLADDR attribute: %m");
+
+ /* VLAN Id is optional. We'll add VLAN Id only if it's specified. */
+ if (fdb_entry->vlan_id > 0) {
+ r = sd_netlink_message_append_u16(req, NDA_VLAN, fdb_entry->vlan_id);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NDA_VLAN attribute: %m");
+ }
+
+ if (!in_addr_is_null(fdb_entry->family, &fdb_entry->destination_addr)) {
+ r = netlink_message_append_in_addr_union(req, NDA_DST, fdb_entry->family, &fdb_entry->destination_addr);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NDA_DST attribute: %m");
+ }
+
+ if (fdb_entry->vni <= VXLAN_VID_MAX) {
+ r = sd_netlink_message_append_u32(req, NDA_VNI, fdb_entry->vni);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NDA_VNI attribute: %m");
+ }
+
+ /* send message to the kernel to update its internal static MAC table. */
+ r = netlink_call_async(link->manager->rtnl, NULL, req, set_fdb_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);
+
+ return 1;
+}
+
+int link_set_bridge_fdb(Link *link) {
+ FdbEntry *fdb_entry;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ HASHMAP_FOREACH(fdb_entry, link->network->fdb_entries_by_section) {
+ r = fdb_entry_configure(link, fdb_entry);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to add MAC entry to static MAC table: %m");
+ }
+
+ return 0;
+}
+
+void network_drop_invalid_fdb_entries(Network *network) {
+ FdbEntry *fdb_entry;
+
+ assert(network);
+
+ HASHMAP_FOREACH(fdb_entry, network->fdb_entries_by_section)
+ if (section_is_invalid(fdb_entry->section))
+ fdb_entry_free(fdb_entry);
+}
+
+/* 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) {
+
+ Network *network = userdata;
+ _cleanup_(fdb_entry_free_or_set_invalidp) FdbEntry *fdb_entry = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = fdb_entry_new_static(network, filename, section_line, &fdb_entry);
+ if (r < 0)
+ return log_oom();
+
+ r = ether_addr_from_string(rvalue, &fdb_entry->mac_addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Not a valid MAC address, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ fdb_entry = NULL;
+
+ 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) {
+
+ Network *network = userdata;
+ _cleanup_(fdb_entry_free_or_set_invalidp) FdbEntry *fdb_entry = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = fdb_entry_new_static(network, filename, section_line, &fdb_entry);
+ if (r < 0)
+ return log_oom();
+
+ r = config_parse_vlanid(unit, filename, line, section,
+ section_line, lvalue, ltype,
+ rvalue, &fdb_entry->vlan_id, userdata);
+ if (r < 0)
+ return r;
+
+ fdb_entry = NULL;
+
+ 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_(fdb_entry_free_or_set_invalidp) FdbEntry *fdb_entry = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = fdb_entry_new_static(network, filename, section_line, &fdb_entry);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_from_string_auto(rvalue, &fdb_entry->family, &fdb_entry->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;
+ }
+
+ fdb_entry = NULL;
+
+ 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_(fdb_entry_free_or_set_invalidp) FdbEntry *fdb_entry = NULL;
+ Network *network = userdata;
+ uint32_t vni;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = fdb_entry_new_static(network, filename, section_line, &fdb_entry);
+ 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_entry->vni = vni;
+ fdb_entry = NULL;
+
+ return 0;
+}
+
+static const char* const fdb_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(fdb_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_(fdb_entry_free_or_set_invalidp) FdbEntry *fdb_entry = NULL;
+ Network *network = userdata;
+ NeighborCacheEntryFlags f;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = fdb_entry_new_static(network, filename, section_line, &fdb_entry);
+ if (r < 0)
+ return log_oom();
+
+ f = fdb_ntf_flags_from_string(rvalue);
+ if (f < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, SYNTHETIC_ERRNO(EINVAL),
+ "FDB failed to parse AssociatedWith=, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+
+ fdb_entry->fdb_ntf_flags = f;
+ fdb_entry = NULL;
+
+ return 0;
+}
diff --git a/src/network/networkd-fdb.h b/src/network/networkd-fdb.h
new file mode 100644
index 0000000..48f4e40
--- /dev/null
+++ b/src/network/networkd-fdb.h
@@ -0,0 +1,52 @@
+/* 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"
+#include "networkd-util.h"
+
+typedef struct Network Network;
+typedef struct Link Link;
+
+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 = -1,
+} NeighborCacheEntryFlags;
+
+typedef struct FdbEntry {
+ Network *network;
+ NetworkConfigSection *section;
+
+ uint32_t vni;
+
+ int family;
+ uint16_t vlan_id;
+
+ struct ether_addr mac_addr;
+ union in_addr_union destination_addr;
+ NeighborCacheEntryFlags fdb_ntf_flags;
+} FdbEntry;
+
+FdbEntry *fdb_entry_free(FdbEntry *fdb_entry);
+
+void network_drop_invalid_fdb_entries(Network *network);
+
+int link_set_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);
diff --git a/src/network/networkd-gperf.gperf b/src/network/networkd-gperf.gperf
new file mode 100644
index 0000000..aaabb3d
--- /dev/null
+++ b/src/network/networkd-gperf.gperf
@@ -0,0 +1,25 @@
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "conf-parser.h"
+#include "networkd-conf.h"
+#include "networkd-manager.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.ManageForeignRoutes, config_parse_bool, 0, offsetof(Manager, manage_foreign_routes)
+DHCP.DUIDType, config_parse_duid_type, 0, offsetof(Manager, duid)
+DHCP.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Manager, duid)
diff --git a/src/network/networkd-ipv4ll.c b/src/network/networkd-ipv4ll.c
new file mode 100644
index 0000000..295abe8
--- /dev/null
+++ b/src/network/networkd-ipv4ll.c
@@ -0,0 +1,313 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if.h>
+
+#include "network-internal.h"
+#include "networkd-address.h"
+#include "networkd-ipv4ll.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+
+static int ipv4ll_address_lost(Link *link) {
+ _cleanup_(address_freep) Address *address = NULL;
+ struct in_addr addr;
+ int r;
+
+ assert(link);
+
+ link->ipv4ll_address_configured = false;
+
+ r = sd_ipv4ll_get_address(link->ipv4ll, &addr);
+ if (r < 0)
+ return 0;
+
+ log_link_debug(link, "IPv4 link-local release "IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(addr));
+
+ r = address_new(&address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate address: %m");
+
+ address->family = AF_INET;
+ address->in_addr.in = addr;
+ address->prefixlen = 16;
+ address->scope = RT_SCOPE_LINK;
+
+ r = address_remove(address, link, NULL);
+ if (r < 0)
+ return r;
+
+ link_check_ready(link);
+
+ return 0;
+}
+
+static int ipv4ll_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(!link->ipv4ll_address_configured);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "could not set ipv4ll address");
+ link_enter_failed(link);
+ return 1;
+ } else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ 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 *ll_addr = NULL;
+ struct in_addr address;
+ int r;
+
+ assert(ll);
+ assert(link);
+
+ link->ipv4ll_address_configured = false;
+
+ r = sd_ipv4ll_get_address(ll, &address);
+ if (r == -ENOENT)
+ return 0;
+ else if (r < 0)
+ return r;
+
+ log_link_debug(link, "IPv4 link-local claim "IPV4_ADDRESS_FMT_STR,
+ IPV4_ADDRESS_FMT_VAL(address));
+
+ r = address_new(&ll_addr);
+ if (r < 0)
+ return r;
+
+ ll_addr->family = AF_INET;
+ ll_addr->in_addr.in = address;
+ ll_addr->prefixlen = 16;
+ ll_addr->broadcast.s_addr = ll_addr->in_addr.in.s_addr | htobe32(0xfffffffflu >> ll_addr->prefixlen);
+ ll_addr->scope = RT_SCOPE_LINK;
+
+ r = address_configure(ll_addr, link, ipv4ll_address_handler, false, NULL);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static void ipv4ll_handler(sd_ipv4ll *ll, int event, void *userdata) {
+ Link *link = userdata;
+ int r;
+
+ assert(link);
+ 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");
+ 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_init(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (link->ipv4ll)
+ return 0;
+
+ 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;
+
+ return 0;
+}
+
+int ipv4ll_configure(Link *link) {
+ uint64_t seed;
+ int r;
+
+ assert(link);
+
+ if (!link_ipv4ll_enabled(link, ADDRESS_FAMILY_IPV4 | ADDRESS_FAMILY_FALLBACK_IPV4))
+ return 0;
+
+ r = ipv4ll_init(link);
+ if (r < 0)
+ return r;
+
+ if (link->sd_device &&
+ net_get_unique_predictable_data(link->sd_device, 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.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 0;
+}
+
+int ipv4ll_update_mac(Link *link) {
+ bool restart;
+ int r;
+
+ assert(link);
+
+ if (!link->ipv4ll)
+ return 0;
+
+ restart = sd_ipv4ll_is_running(link->ipv4ll) > 0;
+
+ r = sd_ipv4ll_stop(link->ipv4ll);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4ll_set_mac(link->ipv4ll, &link->hw_addr.addr.ether);
+ if (r < 0)
+ return r;
+
+ if (restart) {
+ r = sd_ipv4ll_start(link->ipv4ll);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int link_serialize_ipv4ll(Link *link, FILE *f) {
+ struct in_addr address;
+ int r;
+
+ assert(link);
+
+ if (!link->ipv4ll)
+ return 0;
+
+ r = sd_ipv4ll_get_address(link->ipv4ll, &address);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ fputs("IPV4LL_ADDRESS=", f);
+ serialize_in_addrs(f, &address, 1, false, NULL);
+ fputc('\n', f);
+
+ return 0;
+}
+
+int link_deserialize_ipv4ll(Link *link, const char *ipv4ll_address) {
+ union in_addr_union address;
+ int r;
+
+ assert(link);
+
+ if (isempty(ipv4ll_address))
+ return 0;
+
+ r = in_addr_from_string(AF_INET, ipv4ll_address, &address);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to parse IPv4LL address: %s", ipv4ll_address);
+
+ r = ipv4ll_init(link);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to initialize IPv4LL client: %m");
+
+ r = sd_ipv4ll_set_address(link->ipv4ll, &address.in);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to set initial IPv4LL address %s: %m", ipv4ll_address);
+
+ return 0;
+}
+
+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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ /* 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;
+}
diff --git a/src/network/networkd-ipv4ll.h b/src/network/networkd-ipv4ll.h
new file mode 100644
index 0000000..fae48cd
--- /dev/null
+++ b/src/network/networkd-ipv4ll.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+
+#define IPV4LL_ROUTE_METRIC 2048
+
+typedef struct Link Link;
+
+int ipv4ll_configure(Link *link);
+int ipv4ll_update_mac(Link *link);
+int link_serialize_ipv4ll(Link *link, FILE *f);
+int link_deserialize_ipv4ll(Link *link, const char *ipv4ll_address);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv4ll);
diff --git a/src/network/networkd-ipv6-proxy-ndp.c b/src/network/networkd-ipv6-proxy-ndp.c
new file mode 100644
index 0000000..7a370e9
--- /dev/null
+++ b/src/network/networkd-ipv6-proxy-ndp.c
@@ -0,0 +1,164 @@
+/* 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 "socket-util.h"
+#include "string-util.h"
+#include "sysctl-util.h"
+
+static int set_ipv6_proxy_ndp_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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 IPv6 proxy ndp address entry, ignoring");
+
+ return 1;
+}
+
+/* send a request to the kernel to add a IPv6 Proxy entry to the neighbour table */
+static int ipv6_proxy_ndp_address_configure(Link *link, const struct in6_addr *address) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(address);
+
+ /* create new netlink message */
+ r = sd_rtnl_message_new_neigh(link->manager->rtnl, &req, RTM_NEWNEIGH, link->ifindex, AF_INET6);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create RTM_NEWNEIGH message: %m");
+
+ r = sd_rtnl_message_neigh_set_flags(req, NTF_PROXY);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set neighbor flags: %m");
+
+ r = sd_netlink_message_append_in6_addr(req, NDA_DST, address);
+ 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, set_ipv6_proxy_ndp_address_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);
+
+ return 0;
+}
+
+static bool ipv6_proxy_ndp_is_needed(Link *link) {
+ assert(link);
+
+ if (link->flags & IFF_LOOPBACK)
+ return false;
+
+ if (!link->network)
+ return false;
+
+ if (link->network->ipv6_proxy_ndp >= 0)
+ return link->network->ipv6_proxy_ndp;
+
+ return !set_isempty(link->network->ipv6_proxy_ndp_addresses);
+}
+
+static int ipv6_proxy_ndp_set(Link *link) {
+ bool v;
+ int r;
+
+ assert(link);
+
+ if (!socket_ipv6_is_supported())
+ return 0;
+
+ v = ipv6_proxy_ndp_is_needed(link);
+
+ r = sysctl_write_ip_property_boolean(AF_INET6, link->ifname, "proxy_ndp", v);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Cannot configure proxy NDP for the interface, ignoring: %m");
+
+ return v;
+}
+
+/* configure all ipv6 proxy ndp addresses */
+int link_set_ipv6_proxy_ndp_addresses(Link *link) {
+ struct in6_addr *address;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ /* enable or disable proxy_ndp itself depending on whether ipv6_proxy_ndp_addresses are set or not */
+ r = ipv6_proxy_ndp_set(link);
+ if (r <= 0)
+ return 0;
+
+ SET_FOREACH(address, link->network->ipv6_proxy_ndp_addresses) {
+ r = ipv6_proxy_ndp_address_configure(link, address);
+ if (r < 0)
+ return r;
+ }
+
+ 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 = userdata;
+ union in_addr_union buffer;
+ int r;
+
+ assert(filename);
+ assert(rvalue);
+ assert(network);
+
+ 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..27313ef
--- /dev/null
+++ b/src/network/networkd-ipv6-proxy-ndp.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_set_ipv6_proxy_ndp_addresses(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_proxy_ndp_address);
diff --git a/src/network/networkd-link-bus.c b/src/network/networkd-link-bus.c
new file mode 100644
index 0000000..4df31df
--- /dev/null
+++ b/src/network/networkd-link-bus.c
@@ -0,0 +1,816 @@
+/* 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-link-bus.h"
+#include "networkd-link.h"
+#include "networkd-manager.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);
+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 = userdata;
+ Manager *manager;
+ double interval_sec;
+ uint64_t tx, rx;
+
+ assert(bus);
+ assert(reply);
+ assert(userdata);
+
+ 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 = userdata;
+ int r;
+ char **i;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ size_t n;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 != (unsigned) -1)
+ 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_freep) OrderedSet *search_domains = NULL, *route_domains = NULL;
+ Link *l = userdata;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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;
+
+ for (;;) {
+ _cleanup_free_ char *str = NULL;
+ OrderedSet **domains;
+ 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_setf(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);
+
+ domains = route_only ? &route_domains : &search_domains;
+ r = ordered_set_ensure_allocated(domains, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_set_put(*domains, str);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(str);
+ }
+
+ 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_free(l->search_domains);
+ ordered_set_free_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 = userdata;
+ int r, b;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ ResolveSupport mode;
+ const char *llmnr;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ ResolveSupport mode;
+ const char *mdns;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ const char *dns_over_tls;
+ DnsOverTlsMode mode;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ const char *dnssec;
+ DnssecMode mode;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ int r;
+ char **i;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 = userdata;
+ int r;
+
+ assert(l);
+
+ 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 (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 = userdata;
+ int r;
+
+ assert(l);
+
+ 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 (l->dhcp_client) {
+ r = sd_dhcp_client_send_renew(l->dhcp_client);
+ 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 = userdata;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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, 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);
+}
+
+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("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("SetNTP", "as", NULL, bus_link_method_set_ntp_servers, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetDNS", "a(iay)", NULL, bus_link_method_set_dns_servers, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetDNSEx", "a(iayqs)", NULL, bus_link_method_set_dns_servers_ex, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetDomains", "a(sb)", NULL, bus_link_method_set_domains, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetDefaultRoute", "b", NULL, bus_link_method_set_default_route, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLLMNR", "s", NULL, bus_link_method_set_llmnr, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetMulticastDNS", "s", NULL, bus_link_method_set_mdns, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetDNSOverTLS", "s", NULL, bus_link_method_set_dns_over_tls, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetDNSSEC", "s", NULL, bus_link_method_set_dnssec, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetDNSSECNegativeTrustAnchors", "as", NULL, bus_link_method_set_dnssec_negative_trust_anchors, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("RevertNTP", NULL, NULL, bus_link_method_revert_ntp, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("RevertDNS", NULL, NULL, bus_link_method_revert_dns, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("Renew", NULL, NULL, bus_link_method_renew, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ForceRenew", NULL, NULL, bus_link_method_force_renew, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("Reconfigure", NULL, NULL, bus_link_method_reconfigure, 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 = userdata;
+ unsigned c = 0;
+ Link *link;
+
+ assert(bus);
+ assert(path);
+ assert(m);
+ assert(nodes);
+
+ l = new0(char*, hashmap_size(m->links) + 1);
+ if (!l)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(link, m->links) {
+ 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 = userdata;
+ Link *link;
+ int ifindex, r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(m);
+ 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(m, ifindex, &link);
+ if (r < 0)
+ return 0;
+
+ if (streq(interface, "org.freedesktop.network1.DHCPServer") && !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 (!link->manager->bus)
+ 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);
+}
diff --git a/src/network/networkd-link-bus.h b/src/network/networkd-link-bus.h
new file mode 100644
index 0000000..45594df
--- /dev/null
+++ b/src/network/networkd-link-bus.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "macro.h"
+
+typedef struct Link Link;
+
+extern const sd_bus_vtable link_vtable[];
+
+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 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);
diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c
new file mode 100644
index 0000000..8120343
--- /dev/null
+++ b/src/network/networkd-link.c
@@ -0,0 +1,3263 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+#include <linux/if_link.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "bond.h"
+#include "bridge.h"
+#include "bus-util.h"
+#include "dhcp-identifier.h"
+#include "dhcp-lease-internal.h"
+#include "env-file.h"
+#include "ethtool-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "ipvlan.h"
+#include "missing_network.h"
+#include "netlink-util.h"
+#include "network-internal.h"
+#include "networkd-address-label.h"
+#include "networkd-address.h"
+#include "networkd-can.h"
+#include "networkd-dhcp-server.h"
+#include "networkd-dhcp4.h"
+#include "networkd-dhcp6.h"
+#include "networkd-fdb.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-mdb.h"
+#include "networkd-ndisc.h"
+#include "networkd-neighbor.h"
+#include "networkd-nexthop.h"
+#include "networkd-sriov.h"
+#include "networkd-sysctl.h"
+#include "networkd-radv.h"
+#include "networkd-routing-policy-rule.h"
+#include "networkd-wifi.h"
+#include "set.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "sysctl-util.h"
+#include "tc.h"
+#include "tmpfile-util.h"
+#include "udev-util.h"
+#include "util.h"
+#include "vrf.h"
+
+bool link_ipv4ll_enabled(Link *link, AddressFamily mask) {
+ assert(link);
+ assert((mask & ~(ADDRESS_FAMILY_IPV4 | ADDRESS_FAMILY_FALLBACK_IPV4)) == 0);
+
+ 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", "ip6gre","ip6tnl", "sit", "vti",
+ "vti6", "nlmon", "xfrm", "bareudp"))
+ 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;
+
+ if (link->network->bond)
+ return false;
+
+ return link->network->link_local & mask;
+}
+
+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_ipv6_enabled(Link *link) {
+ assert(link);
+
+ if (!socket_ipv6_is_supported())
+ return false;
+
+ if (link->network->bond)
+ return false;
+
+ if (link->iftype == ARPHRD_CAN)
+ return false;
+
+ /* DHCPv6 client will not be started if no IPv6 link-local address is configured. */
+ if (link_ipv6ll_enabled(link))
+ return true;
+
+ if (network_has_static_ipv6_configurations(link->network))
+ return true;
+
+ return false;
+}
+
+static bool link_is_enslaved(Link *link) {
+ if (link->flags & IFF_SLAVE)
+ /* Even if the link is not managed by networkd, honor IFF_SLAVE flag. */
+ return true;
+
+ if (!link->network)
+ return false;
+
+ if (link->master_ifindex > 0 && link->network->bridge)
+ return true;
+
+ /* TODO: add conditions for other netdevs. */
+
+ return false;
+}
+
+static void link_update_master_operstate(Link *link, NetDev *netdev) {
+ Link *master;
+
+ if (!netdev)
+ return;
+
+ if (netdev->ifindex <= 0)
+ return;
+
+ if (link_get(link->manager, netdev->ifindex, &master) < 0)
+ return;
+
+ link_update_operstate(master, true);
+}
+
+void link_update_operstate(Link *link, bool also_update_master) {
+ LinkOperationalState operstate;
+ LinkCarrierState carrier_state;
+ LinkAddressState address_state;
+ _cleanup_strv_free_ char **p = NULL;
+ uint8_t 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->scope < scope)
+ scope = address->scope;
+ }
+
+ /* for operstate we also take foreign addresses into account */
+ SET_FOREACH(address, link->addresses_foreign) {
+ if (!address_is_ready(address))
+ continue;
+
+ if (address->scope < scope)
+ scope = address->scope;
+ }
+
+ if (scope < RT_SCOPE_SITE)
+ /* universally accessible addresses found */
+ address_state = LINK_ADDRESS_STATE_ROUTABLE;
+ else if (scope < RT_SCOPE_HOST)
+ /* only link or site local addresses found */
+ address_state = LINK_ADDRESS_STATE_DEGRADED;
+ else
+ /* no useful addresses found */
+ address_state = LINK_ADDRESS_STATE_OFF;
+
+ /* 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-carrier | degraded | enslaved
+ * routable | off | no-carrier | dormant | degraded-carrier | routable | routable
+ */
+
+ 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;
+
+ 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->operstate != operstate) {
+ link->operstate = operstate;
+ changed = true;
+ if (strv_extend(&p, "OperationalState") < 0)
+ log_oom();
+ }
+
+ if (p)
+ link_send_changed_strv(link, p);
+ if (changed)
+ link_dirty(link);
+
+ if (also_update_master && link->network) {
+ link_update_master_operstate(link, link->network->bond);
+ link_update_master_operstate(link, link->network->bridge);
+ }
+}
+
+#define FLAG_STRING(string, flag, old, new) \
+ (((old ^ new) & flag) \
+ ? ((old & flag) ? (" -" string) : (" +" string)) \
+ : "")
+
+static int link_update_flags(Link *link, sd_netlink_message *m, bool force_update_operstate) {
+ unsigned flags, unknown_flags_added, unknown_flags_removed, unknown_flags;
+ uint8_t operstate;
+ int r;
+
+ assert(link);
+
+ r = sd_rtnl_message_link_get_flags(m, &flags);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not get link flags: %m");
+
+ r = sd_netlink_message_read_u8(m, IFLA_OPERSTATE, &operstate);
+ if (r < 0)
+ /* if we got a message without operstate, take it to mean
+ the state was unchanged */
+ operstate = link->kernel_operstate;
+
+ if (!force_update_operstate && (link->flags == flags) && (link->kernel_operstate == operstate))
+ return 0;
+
+ if (link->flags != flags) {
+ 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);
+
+ /* link flags are currently at most 18 bits, let's align to
+ * printing 20 */
+ if (unknown_flags_added)
+ log_link_debug(link,
+ "Unknown link flags gained: %#.5x (ignoring)",
+ unknown_flags_added);
+
+ if (unknown_flags_removed)
+ log_link_debug(link,
+ "Unknown link flags lost: %#.5x (ignoring)",
+ unknown_flags_removed);
+ }
+
+ link->flags = flags;
+ link->kernel_operstate = operstate;
+
+ link_update_operstate(link, true);
+
+ return 0;
+}
+
+static int link_new(Manager *manager, sd_netlink_message *message, Link **ret) {
+ _cleanup_(link_unrefp) Link *link = NULL;
+ const char *ifname, *kind = NULL;
+ unsigned short iftype;
+ int r, ifindex;
+ uint16_t type;
+
+ assert(manager);
+ assert(message);
+ assert(ret);
+
+ /* check for link kind */
+ r = sd_netlink_message_enter_container(message, IFLA_LINKINFO);
+ if (r == 0) {
+ (void) sd_netlink_message_read_string(message, IFLA_INFO_KIND, &kind);
+ r = sd_netlink_message_exit_container(message);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_get_type(message, &type);
+ if (r < 0)
+ return r;
+ else if (type != RTM_NEWLINK)
+ return -EINVAL;
+
+ r = sd_rtnl_message_link_get_ifindex(message, &ifindex);
+ if (r < 0)
+ return r;
+ else if (ifindex <= 0)
+ return -EINVAL;
+
+ r = sd_rtnl_message_link_get_type(message, &iftype);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_read_string(message, IFLA_IFNAME, &ifname);
+ if (r < 0)
+ return r;
+
+ link = new(Link, 1);
+ if (!link)
+ return -ENOMEM;
+
+ *link = (Link) {
+ .n_ref = 1,
+ .manager = manager,
+ .state = LINK_STATE_PENDING,
+ .ifindex = ifindex,
+ .iftype = iftype,
+
+ .n_dns = (unsigned) -1,
+ .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,
+ };
+
+ link->ifname = strdup(ifname);
+ if (!link->ifname)
+ return -ENOMEM;
+
+ if (kind) {
+ link->kind = strdup(kind);
+ if (!link->kind)
+ return -ENOMEM;
+ }
+
+ r = sd_netlink_message_read_u32(message, IFLA_MASTER, (uint32_t *)&link->master_ifindex);
+ if (r < 0)
+ log_link_debug_errno(link, r, "New device has no master, continuing without");
+
+ r = netlink_message_read_hw_addr(message, IFLA_ADDRESS, &link->hw_addr);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Hardware address not found for new device, continuing without");
+
+ r = netlink_message_read_hw_addr(message, IFLA_BROADCAST, &link->bcast_addr);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Broadcast address not found for new device, continuing without");
+
+ r = ethtool_get_permanent_macaddr(&manager->ethtool_fd, link->ifname, &link->permanent_mac);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Permanent MAC address not found for new device, continuing without: %m");
+
+ r = ethtool_get_driver(&manager->ethtool_fd, link->ifname, &link->driver);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Failed to get driver, continuing without: %m");
+
+ r = sd_netlink_message_read_strv(message, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &link->alternative_names);
+ if (r < 0 && r != -ENODATA)
+ return r;
+
+ if (asprintf(&link->state_file, "/run/systemd/netif/links/%d", link->ifindex) < 0)
+ return -ENOMEM;
+
+ if (asprintf(&link->lease_file, "/run/systemd/netif/leases/%d", link->ifindex) < 0)
+ return -ENOMEM;
+
+ if (asprintf(&link->lldp_file, "/run/systemd/netif/lldp/%d", link->ifindex) < 0)
+ return -ENOMEM;
+
+ r = hashmap_ensure_allocated(&manager->links, NULL);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(manager->links, INT_TO_PTR(link->ifindex), link);
+ if (r < 0)
+ return r;
+
+ r = link_update_flags(link, message, false);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(link);
+
+ return 0;
+}
+
+void link_ntp_settings_clear(Link *link) {
+ link->ntp = strv_free(link->ntp);
+}
+
+void link_dns_settings_clear(Link *link) {
+ if (link->n_dns != (unsigned) -1)
+ for (unsigned i = 0; i < link->n_dns; i++)
+ in_addr_full_free(link->dns[i]);
+ link->dns = mfree(link->dns);
+ link->n_dns = (unsigned) -1;
+
+ link->search_domains = ordered_set_free_free(link->search_domains);
+ link->route_domains = ordered_set_free_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->dhcp_acd = sd_ipv4acd_unref(link->dhcp_acd);
+
+ link->lldp = sd_lldp_unref(link->lldp);
+ link_lldp_emit_stop(link);
+
+ ndisc_flush(link);
+
+ 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->radv = sd_radv_unref(link->radv);
+
+ ipv4_dad_unref(link);
+}
+
+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->routes_foreign = set_free(link->routes_foreign);
+ link->dhcp_routes = set_free(link->dhcp_routes);
+ link->dhcp_routes_old = set_free(link->dhcp_routes_old);
+ link->dhcp6_routes = set_free(link->dhcp6_routes);
+ link->dhcp6_routes_old = set_free(link->dhcp6_routes_old);
+ link->dhcp6_pd_routes = set_free(link->dhcp6_pd_routes);
+ link->dhcp6_pd_routes_old = set_free(link->dhcp6_pd_routes_old);
+ link->ndisc_routes = set_free(link->ndisc_routes);
+
+ link->nexthops = set_free(link->nexthops);
+ link->nexthops_foreign = set_free(link->nexthops_foreign);
+
+ link->neighbors = set_free(link->neighbors);
+ link->neighbors_foreign = set_free(link->neighbors_foreign);
+
+ link->addresses = set_free(link->addresses);
+ link->addresses_foreign = set_free(link->addresses_foreign);
+ link->pool_addresses = set_free(link->pool_addresses);
+ link->static_addresses = set_free(link->static_addresses);
+ link->dhcp6_addresses = set_free(link->dhcp6_addresses);
+ link->dhcp6_addresses_old = set_free(link->dhcp6_addresses_old);
+ link->dhcp6_pd_addresses = set_free(link->dhcp6_pd_addresses);
+ link->dhcp6_pd_addresses_old = set_free(link->dhcp6_pd_addresses_old);
+ link->ndisc_addresses = set_free(link->ndisc_addresses);
+
+ link_free_engines(link);
+ free(link->lease_file);
+ free(link->lldp_file);
+
+ free(link->ifname);
+ strv_free(link->alternative_names);
+ free(link->kind);
+ free(link->ssid);
+ free(link->driver);
+
+ (void) unlink(link->state_file);
+ free(link->state_file);
+
+ sd_device_unref(link->sd_device);
+
+ hashmap_free(link->bound_to_links);
+ hashmap_free(link->bound_by_links);
+
+ set_free_with_destructor(link->slaves, link_unref);
+
+ network_unref(link->network);
+
+ return mfree(link);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(Link, link, link_free);
+
+int link_get(Manager *m, int ifindex, Link **ret) {
+ Link *link;
+
+ assert(m);
+ assert(ifindex > 0);
+ assert(ret);
+
+ link = hashmap_get(m->links, INT_TO_PTR(ifindex));
+ if (!link)
+ return -ENODEV;
+
+ *ret = link;
+
+ return 0;
+}
+
+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);
+}
+
+static void link_enter_unmanaged(Link *link) {
+ assert(link);
+
+ link_set_state(link, LINK_STATE_UNMANAGED);
+
+ 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->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_ipv4acd_stop(link->dhcp_acd);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop IPv4 ACD client for DHCPv4: %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_stop(link->lldp);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop LLDP: %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 = ipv4_dad_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 = dhcp6_pd_remove(link);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not remove DHCPv6 PD addresses and routes: %m");
+
+ k = sd_ndisc_stop(link->ndisc);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop IPv6 Router Discovery: %m");
+
+ k = sd_radv_stop(link->radv);
+ if (k < 0)
+ r = log_link_warning_errno(link, k, "Could not stop IPv6 Router Advertisement: %m");
+
+ link_lldp_emit_stop(link);
+ 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);
+
+ link_dirty(link);
+}
+
+static int link_join_netdevs_after_configured(Link *link) {
+ NetDev *netdev;
+ int r;
+
+ HASHMAP_FOREACH(netdev, link->network->stacked_netdevs) {
+ if (netdev->ifindex > 0)
+ /* Assume already enslaved. */
+ continue;
+
+ if (netdev_get_create_type(netdev) != NETDEV_CREATE_AFTER_CONFIGURED)
+ continue;
+
+ log_struct(LOG_DEBUG,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(netdev),
+ LOG_LINK_MESSAGE(link, "Enslaving by '%s'", netdev->ifname));
+
+ r = netdev_join(netdev, link, NULL);
+ if (r < 0)
+ return log_struct_errno(LOG_WARNING, r,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(netdev),
+ LOG_LINK_MESSAGE(link, "Could not join netdev '%s': %m", netdev->ifname));
+ }
+
+ return 0;
+}
+
+static void link_enter_configured(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (link->state != LINK_STATE_CONFIGURING)
+ return;
+
+ link_set_state(link, LINK_STATE_CONFIGURED);
+
+ (void) link_join_netdevs_after_configured(link);
+
+ link_dirty(link);
+}
+
+void link_check_ready(Link *link) {
+ Address *a;
+
+ assert(link);
+
+ if (link->state == LINK_STATE_CONFIGURED)
+ return;
+
+ if (link->state != LINK_STATE_CONFIGURING) {
+ log_link_debug(link, "%s(): link is in %s state.", __func__, link_state_to_string(link->state));
+ return;
+ }
+
+ if (!link->network)
+ return;
+
+ if (!link->addresses_configured) {
+ log_link_debug(link, "%s(): static addresses are not configured.", __func__);
+ return;
+ }
+
+ if (!link->neighbors_configured) {
+ log_link_debug(link, "%s(): static neighbors are not configured.", __func__);
+ return;
+ }
+
+ SET_FOREACH(a, link->addresses)
+ if (!address_is_ready(a)) {
+ _cleanup_free_ char *str = NULL;
+
+ (void) in_addr_to_string(a->family, &a->in_addr, &str);
+ log_link_debug(link, "%s(): an address %s/%d is not ready.", __func__, strnull(str), a->prefixlen);
+ return;
+ }
+
+ if (!link->static_routes_configured) {
+ log_link_debug(link, "%s(): static routes are not configured.", __func__);
+ return;
+ }
+
+ if (!link->static_nexthops_configured) {
+ log_link_debug(link, "%s(): static nexthops are not configured.", __func__);
+ return;
+ }
+
+ if (!link->routing_policy_rules_configured) {
+ log_link_debug(link, "%s(): static routing policy rules are not configured.", __func__);
+ return;
+ }
+
+ if (!link->tc_configured) {
+ log_link_debug(link, "%s(): traffic controls are not configured.", __func__);
+ return;
+ }
+
+ if (!link->sr_iov_configured) {
+ log_link_debug(link, "%s(): SR-IOV is not configured.", __func__);
+ return;
+ }
+
+ if (!link->bridge_mdb_configured) {
+ log_link_debug(link, "%s(): Bridge MDB is not configured.", __func__);
+ return;
+ }
+
+ if (link_has_carrier(link) || !link->network->configure_without_carrier) {
+ bool has_ndisc_address = false;
+ NDiscAddress *n;
+
+ if (link_ipv4ll_enabled(link, ADDRESS_FAMILY_IPV4) && !link->ipv4ll_address_configured) {
+ log_link_debug(link, "%s(): IPv4LL is not configured.", __func__);
+ return;
+ }
+
+ if (link_ipv6ll_enabled(link) &&
+ in_addr_is_null(AF_INET6, (const union in_addr_union*) &link->ipv6ll_address)) {
+ log_link_debug(link, "%s(): IPv6LL is not configured.", __func__);
+ return;
+ }
+
+ SET_FOREACH(n, link->ndisc_addresses)
+ if (!n->marked) {
+ has_ndisc_address = true;
+ break;
+ }
+
+ if ((link_dhcp4_enabled(link) || link_dhcp6_enabled(link)) &&
+ !link->dhcp_address && set_isempty(link->dhcp6_addresses) && !has_ndisc_address &&
+ !(link_ipv4ll_enabled(link, ADDRESS_FAMILY_FALLBACK_IPV4) && link->ipv4ll_address_configured)) {
+ log_link_debug(link, "%s(): DHCP4 or DHCP6 is enabled but no dynamic address is assigned yet.", __func__);
+ return;
+ }
+
+ if (link_dhcp4_enabled(link) || link_dhcp6_enabled(link) || link_dhcp6_pd_is_enabled(link) || link_ipv6_accept_ra_enabled(link)) {
+ if (!link->dhcp4_configured &&
+ !(link->dhcp6_address_configured && link->dhcp6_route_configured) &&
+ !(link->dhcp6_pd_address_configured && link->dhcp6_pd_route_configured) &&
+ !(link->ndisc_addresses_configured && link->ndisc_routes_configured) &&
+ !(link_ipv4ll_enabled(link, ADDRESS_FAMILY_FALLBACK_IPV4) && link->ipv4ll_address_configured)) {
+ /* When DHCP or RA is enabled, at least one protocol must provide an address, or
+ * an IPv4ll fallback address must be configured. */
+ log_link_debug(link, "%s(): dynamic addresses or routes are not configured.", __func__);
+ return;
+ }
+
+ log_link_debug(link, "%s(): dhcp4:%s dhcp6_addresses:%s dhcp_routes:%s dhcp_pd_addresses:%s dhcp_pd_routes:%s ndisc_addresses:%s ndisc_routes:%s",
+ __func__,
+ yes_no(link->dhcp4_configured),
+ yes_no(link->dhcp6_address_configured),
+ yes_no(link->dhcp6_route_configured),
+ yes_no(link->dhcp6_pd_address_configured),
+ yes_no(link->dhcp6_pd_route_configured),
+ yes_no(link->ndisc_addresses_configured),
+ yes_no(link->ndisc_routes_configured));
+ }
+ }
+
+ link_enter_configured(link);
+
+ return;
+}
+
+static int link_set_static_configs(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state != _LINK_STATE_INVALID);
+
+ /* Reset all *_configured flags we are configuring. */
+ link->request_static_addresses = false;
+ link->addresses_configured = false;
+ link->addresses_ready = false;
+ link->neighbors_configured = false;
+ link->static_routes_configured = false;
+ link->static_nexthops_configured = false;
+ link->routing_policy_rules_configured = false;
+
+ r = link_set_bridge_fdb(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_bridge_mdb(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_neighbors(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_addresses(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_address_labels(link);
+ if (r < 0)
+ return r;
+
+ /* now that we can figure out a default address for the dhcp server, start it */
+ r = dhcp4_server_configure(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int link_configure_continue(Link *link);
+
+static int link_mac_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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)
+ log_link_message_warning_errno(link, m, r, "Could not set MAC address, ignoring");
+ else
+ log_link_debug(link, "Setting MAC address done.");
+
+ return 1;
+}
+
+static int link_set_mac(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ if (!link->network->mac)
+ return 0;
+
+ log_link_debug(link, "Setting MAC address");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_netlink_message_append_ether_addr(req, IFLA_ADDRESS, link->network->mac);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set MAC address: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_mac_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);
+
+ return 0;
+}
+
+static int link_nomaster_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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)
+ log_link_message_warning_errno(link, m, r, "Could not set nomaster, ignoring");
+ else
+ log_link_debug(link, "Setting nomaster done.");
+
+ return 1;
+}
+
+static int link_set_nomaster(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ /* set it free if not enslaved with networkd */
+ if (link->network->bridge || link->network->bond || link->network->vrf)
+ return 0;
+
+ log_link_debug(link, "Setting nomaster");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_netlink_message_append_u32(req, IFLA_MASTER, 0);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_MASTER attribute: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_nomaster_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);
+
+ return 0;
+}
+
+static int set_mtu_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+
+ link->setting_mtu = false;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ log_link_message_warning_errno(link, m, r, "Could not set MTU, ignoring");
+ else
+ log_link_debug(link, "Setting MTU done.");
+
+ if (link->state == LINK_STATE_INITIALIZED) {
+ r = link_configure_continue(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+int link_set_mtu(Link *link, uint32_t mtu) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ if (mtu == 0 || link->setting_mtu)
+ return 0;
+
+ if (link->mtu == mtu)
+ return 0;
+
+ log_link_debug(link, "Setting MTU: %" PRIu32, mtu);
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ /* IPv6 protocol requires a minimum MTU of IPV6_MTU_MIN(1280) bytes
+ * on the interface. Bump up MTU bytes to IPV6_MTU_MIN. */
+ if (link_ipv6_enabled(link) && mtu < IPV6_MIN_MTU) {
+
+ log_link_warning(link, "Bumping MTU to " STRINGIFY(IPV6_MIN_MTU) ", as "
+ "IPv6 is requested and requires a minimum MTU of " STRINGIFY(IPV6_MIN_MTU) " bytes");
+
+ mtu = IPV6_MIN_MTU;
+ }
+
+ r = sd_netlink_message_append_u32(req, IFLA_MTU, mtu);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append MTU: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, set_mtu_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);
+ link->setting_mtu = true;
+
+ 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;
+}
+
+static int link_configure_mtu(Link *link) {
+ uint32_t mtu;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->network->mtu > 0)
+ return link_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_set_mtu(link, mtu);
+}
+
+static int set_flags_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ log_link_message_warning_errno(link, m, r, "Could not set link flags, ignoring");
+
+ return 1;
+}
+
+static int link_set_flags(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ unsigned ifi_change = 0;
+ unsigned ifi_flags = 0;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ if (link->flags & IFF_LOOPBACK)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->arp < 0 && link->network->multicast < 0 && link->network->allmulticast < 0)
+ return 0;
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ 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);
+ }
+
+ r = sd_rtnl_message_link_set_flags(req, ifi_flags, ifi_change);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set link flags: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, set_flags_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);
+
+ return 0;
+}
+
+static int link_acquire_ipv6_conf(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (link->ndisc) {
+ log_link_debug(link, "Discovering IPv6 routers");
+
+ r = sd_ndisc_start(link->ndisc);
+ if (r < 0 && r != -EBUSY)
+ return log_link_warning_errno(link, r, "Could not start IPv6 Router Discovery: %m");
+ }
+
+ if (link->radv) {
+ assert(link->radv);
+ assert(in_addr_is_link_local(AF_INET6, (const union in_addr_union*)&link->ipv6ll_address) > 0);
+
+ log_link_debug(link, "Starting IPv6 Router Advertisements");
+
+ r = radv_emit_dns(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure DNS or Domains in IPv6 Router Advertisement: %m");
+
+ r = sd_radv_start(link->radv);
+ if (r < 0 && r != -EBUSY)
+ return log_link_warning_errno(link, r, "Could not start IPv6 Router Advertisement: %m");
+ }
+
+ if (link_dhcp6_enabled(link) && IN_SET(link->network->dhcp6_without_ra,
+ DHCP6_CLIENT_START_MODE_INFORMATION_REQUEST,
+ DHCP6_CLIENT_START_MODE_SOLICIT)) {
+ assert(link->dhcp6_client);
+ assert(in_addr_is_link_local(AF_INET6, (const union in_addr_union*)&link->ipv6ll_address) > 0);
+
+ r = dhcp6_request_address(link, link->network->dhcp6_without_ra == DHCP6_CLIENT_START_MODE_INFORMATION_REQUEST);
+ if (r < 0 && r != -EBUSY)
+ return log_link_warning_errno(link, r, "Could not acquire DHCPv6 lease: %m");
+ else
+ log_link_debug(link, "Acquiring DHCPv6 lease");
+ }
+
+ r = dhcp6_request_prefix_delegation(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request DHCPv6 prefix delegation: %m");
+
+ return 0;
+}
+
+static int link_acquire_ipv4_conf(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->event);
+
+ if (link_ipv4ll_enabled(link, ADDRESS_FAMILY_IPV4)) {
+ assert(link->ipv4ll);
+
+ log_link_debug(link, "Acquiring IPv4 link-local address");
+
+ 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 (link->dhcp_client) {
+ log_link_debug(link, "Acquiring DHCPv4 lease");
+
+ r = sd_dhcp_client_start(link->dhcp_client);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not acquire DHCPv4 lease: %m");
+ }
+
+ return 0;
+}
+
+static int link_acquire_conf(Link *link) {
+ int r;
+
+ assert(link);
+
+ r = link_acquire_ipv4_conf(link);
+ if (r < 0)
+ return r;
+
+ if (!in_addr_is_null(AF_INET6, (const union in_addr_union*) &link->ipv6ll_address)) {
+ r = link_acquire_ipv6_conf(link);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_lldp_emit_start(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start LLDP transmission: %m");
+
+ return 0;
+}
+
+bool link_has_carrier(Link *link) {
+ /* see Documentation/networking/operstates.txt in the kernel sources */
+
+ if (link->kernel_operstate == IF_OPER_UP)
+ return true;
+
+ if (link->kernel_operstate == IF_OPER_UNKNOWN)
+ /* operstate may not be implemented, so fall back to flags */
+ if (FLAGS_SET(link->flags, IFF_LOWER_UP | IFF_RUNNING) &&
+ !FLAGS_SET(link->flags, IFF_DORMANT))
+ return true;
+
+ return false;
+}
+
+static int link_address_genmode_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+
+ link->setting_genmode = false;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ log_link_message_warning_errno(link, m, r, "Could not set address genmode for interface, ignoring");
+ else
+ log_link_debug(link, "Setting address genmode done.");
+
+ if (link->state == LINK_STATE_INITIALIZED) {
+ r = link_configure_continue(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+static int link_configure_addrgen_mode(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ uint8_t ipv6ll_mode;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ if (!socket_ipv6_is_supported() || link->setting_genmode)
+ return 0;
+
+ log_link_debug(link, "Setting address genmode for link");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_netlink_message_open_container(req, IFLA_AF_SPEC);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open IFLA_AF_SPEC container: %m");
+
+ r = sd_netlink_message_open_container(req, AF_INET6);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open AF_INET6 container: %m");
+
+ if (!link_ipv6ll_enabled(link))
+ ipv6ll_mode = IN6_ADDR_GEN_MODE_NONE;
+ else if (link->network->ipv6ll_address_gen_mode < 0) {
+ r = sysctl_read_ip_property(AF_INET6, link->ifname, "stable_secret", NULL);
+ if (r < 0) {
+ /* The file may not exist. And even if it exists, when stable_secret is unset,
+ * reading the file fails with EIO. */
+ log_link_debug_errno(link, r, "Failed to read sysctl property stable_secret: %m");
+
+ ipv6ll_mode = IN6_ADDR_GEN_MODE_EUI64;
+ } else
+ ipv6ll_mode = IN6_ADDR_GEN_MODE_STABLE_PRIVACY;
+ } else
+ ipv6ll_mode = link->network->ipv6ll_address_gen_mode;
+
+ r = sd_netlink_message_append_u8(req, IFLA_INET6_ADDR_GEN_MODE, ipv6ll_mode);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_INET6_ADDR_GEN_MODE: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close AF_INET6 container: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close IFLA_AF_SPEC container: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_address_genmode_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);
+ link->setting_genmode = true;
+
+ return 0;
+}
+
+static int link_up_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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)
+ /* we warn but don't fail the link, as it may be brought up later */
+ log_link_message_warning_errno(link, m, r, "Could not bring up interface");
+
+ return 1;
+}
+
+static int link_up(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ log_link_debug(link, "Bringing link up");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_rtnl_message_link_set_flags(req, IFF_UP, IFF_UP);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set link flags: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_up_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);
+
+ return 0;
+}
+
+static int link_down_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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)
+ log_link_message_warning_errno(link, m, r, "Could not bring down interface");
+
+ return 1;
+}
+
+int link_down(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);
+
+ 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_error_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_error_errno(link, r, "Could not set link flags: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req,
+ callback ?: link_down_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);
+
+ return 0;
+}
+
+static int link_group_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ 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)
+ log_link_message_warning_errno(link, m, r, "Could not set group for the interface");
+
+ return 1;
+}
+
+static int link_set_group(Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ if (link->network->group <= 0)
+ return 0;
+
+ log_link_debug(link, "Setting group");
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_netlink_message_append_u32(req, IFLA_GROUP, link->network->group);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set link group: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, link_group_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);
+
+ return 0;
+}
+
+static int link_handle_bound_to_list(Link *link) {
+ Link *l;
+ int r;
+ bool required_up = false;
+ bool link_is_up = false;
+
+ assert(link);
+
+ 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) {
+ r = link_down(link, NULL);
+ if (r < 0)
+ return r;
+ } else if (required_up && !link_is_up) {
+ r = link_up(link);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int link_handle_bound_by_list(Link *link) {
+ Link *l;
+ int r;
+
+ assert(link);
+
+ 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_allocated(h, NULL);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(*h, INT_TO_PTR(carrier->ifindex), carrier);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int link_new_bound_by_list(Link *link) {
+ Manager *m;
+ Link *carrier;
+ int r;
+ bool list_updated = false;
+
+ assert(link);
+ assert(link->manager);
+
+ m = link->manager;
+
+ HASHMAP_FOREACH(carrier, m->links) {
+ 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;
+
+ list_updated = true;
+ }
+ }
+
+ if (list_updated)
+ link_dirty(link);
+
+ HASHMAP_FOREACH(carrier, link->bound_by_links) {
+ r = link_put_carrier(carrier, link, &carrier->bound_to_links);
+ if (r < 0)
+ return r;
+
+ link_dirty(carrier);
+ }
+
+ return 0;
+}
+
+static int link_new_bound_to_list(Link *link) {
+ Manager *m;
+ Link *carrier;
+ int r;
+ bool list_updated = false;
+
+ 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) {
+ if (strv_fnmatch(link->network->bind_carrier, carrier->ifname)) {
+ r = link_put_carrier(link, carrier, &link->bound_to_links);
+ if (r < 0)
+ return r;
+
+ list_updated = true;
+ }
+ }
+
+ if (list_updated)
+ link_dirty(link);
+
+ HASHMAP_FOREACH (carrier, link->bound_to_links) {
+ r = link_put_carrier(carrier, link, &carrier->bound_by_links);
+ if (r < 0)
+ return r;
+
+ link_dirty(carrier);
+ }
+
+ return 0;
+}
+
+static int link_new_carrier_maps(Link *link) {
+ int r;
+
+ r = link_new_bound_by_list(link);
+ if (r < 0)
+ return r;
+
+ r = link_handle_bound_by_list(link);
+ if (r < 0)
+ return r;
+
+ r = link_new_bound_to_list(link);
+ if (r < 0)
+ return r;
+
+ r = link_handle_bound_to_list(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static void link_free_bound_to_list(Link *link) {
+ Link *bound_to;
+
+ HASHMAP_FOREACH (bound_to, link->bound_to_links) {
+ hashmap_remove(link->bound_to_links, INT_TO_PTR(bound_to->ifindex));
+
+ if (hashmap_remove(bound_to->bound_by_links, INT_TO_PTR(link->ifindex)))
+ link_dirty(bound_to);
+ }
+
+ return;
+}
+
+static void link_free_bound_by_list(Link *link) {
+ Link *bound_by;
+
+ HASHMAP_FOREACH (bound_by, link->bound_by_links) {
+ hashmap_remove(link->bound_by_links, INT_TO_PTR(bound_by->ifindex));
+
+ if (hashmap_remove(bound_by->bound_to_links, INT_TO_PTR(link->ifindex))) {
+ link_dirty(bound_by);
+ link_handle_bound_to_list(bound_by);
+ }
+ }
+
+ return;
+}
+
+static void link_free_carrier_maps(Link *link) {
+ bool list_updated = false;
+
+ assert(link);
+
+ if (!hashmap_isempty(link->bound_to_links)) {
+ link_free_bound_to_list(link);
+ list_updated = true;
+ }
+
+ if (!hashmap_isempty(link->bound_by_links)) {
+ link_free_bound_by_list(link);
+ list_updated = true;
+ }
+
+ if (list_updated)
+ link_dirty(link);
+
+ return;
+}
+
+static int link_append_to_master(Link *link, NetDev *netdev) {
+ Link *master;
+ int r;
+
+ assert(link);
+ assert(netdev);
+
+ r = link_get(link->manager, netdev->ifindex, &master);
+ if (r < 0)
+ return r;
+
+ 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, NetDev *netdev) {
+ Link *master;
+
+ assert(link);
+
+ if (!link->manager || !netdev)
+ return;
+
+ if (link_get(link->manager, netdev->ifindex, &master) < 0)
+ return;
+
+ link_unref(set_remove(master->slaves, link));
+}
+
+static void link_detach_from_manager(Link *link) {
+ if (!link || !link->manager)
+ return;
+
+ link_unref(set_remove(link->manager->links_requesting_uuid, link));
+ link_clean(link);
+
+ /* The following must be called at last. */
+ assert_se(hashmap_remove(link->manager->links, INT_TO_PTR(link->ifindex)) == link);
+ link_unref(link);
+}
+
+void link_drop(Link *link) {
+ if (!link || link->state == LINK_STATE_LINGER)
+ return;
+
+ link_set_state(link, LINK_STATE_LINGER);
+
+ link_free_carrier_maps(link);
+
+ if (link->network) {
+ link_drop_from_master(link, link->network->bridge);
+ link_drop_from_master(link, link->network->bond);
+ }
+
+ log_link_debug(link, "Link removed");
+
+ (void) unlink(link->state_file);
+ link_detach_from_manager(link);
+}
+
+static int link_joined(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!hashmap_isempty(link->bound_to_links)) {
+ r = link_handle_bound_to_list(link);
+ if (r < 0)
+ return r;
+ } else if (!(link->flags & IFF_UP)) {
+ r = link_up(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ if (link->network->bridge) {
+ r = link_set_bridge(link);
+ if (r < 0)
+ log_link_error_errno(link, r, "Could not set bridge message: %m");
+
+ r = link_append_to_master(link, link->network->bridge);
+ if (r < 0)
+ log_link_error_errno(link, r, "Failed to add to bridge master's slave list: %m");
+ }
+
+ if (link->network->bond) {
+ r = link_set_bond(link);
+ if (r < 0)
+ log_link_error_errno(link, r, "Could not set bond message: %m");
+
+ r = link_append_to_master(link, link->network->bond);
+ if (r < 0)
+ log_link_error_errno(link, r, "Failed to add to bond master's slave list: %m");
+ }
+
+ r = link_set_bridge_vlan(link);
+ if (r < 0)
+ log_link_error_errno(link, r, "Could not set bridge vlan: %m");
+
+ /* Skip setting up addresses until it gets carrier,
+ or it would try to set addresses twice,
+ which is bad for non-idempotent steps. */
+ if (!link_has_carrier(link) && !link->network->configure_without_carrier)
+ return 0;
+
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ r = link_acquire_conf(link);
+ if (r < 0)
+ return r;
+
+ return link_set_static_configs(link);
+}
+
+static int netdev_join_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->enslaving > 0);
+
+ link->enslaving--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_warning_errno(link, m, r, "Could not join netdev");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ log_link_debug(link, "Joined netdev");
+
+ if (link->enslaving == 0) {
+ r = link_joined(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+
+ return 1;
+}
+
+static int link_enter_join_netdev(Link *link) {
+ NetDev *netdev;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state == LINK_STATE_INITIALIZED);
+
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ link_dirty(link);
+ link->enslaving = 0;
+
+ if (link->network->bond) {
+ if (link->network->bond->state == NETDEV_STATE_READY &&
+ link->network->bond->ifindex == link->master_ifindex)
+ return link_joined(link);
+
+ log_struct(LOG_DEBUG,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(link->network->bond),
+ LOG_LINK_MESSAGE(link, "Enslaving by '%s'", link->network->bond->ifname));
+
+ link->enslaving++;
+
+ r = netdev_join(link->network->bond, link, netdev_join_handler);
+ if (r < 0) {
+ log_struct_errno(LOG_WARNING, r,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(link->network->bond),
+ LOG_LINK_MESSAGE(link, "Could not join netdev '%s': %m", link->network->bond->ifname));
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ if (link->network->bridge) {
+ log_struct(LOG_DEBUG,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(link->network->bridge),
+ LOG_LINK_MESSAGE(link, "Enslaving by '%s'", link->network->bridge->ifname));
+
+ link->enslaving++;
+
+ r = netdev_join(link->network->bridge, link, netdev_join_handler);
+ if (r < 0) {
+ log_struct_errno(LOG_WARNING, r,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(link->network->bridge),
+ LOG_LINK_MESSAGE(link, "Could not join netdev '%s': %m", link->network->bridge->ifname));
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ if (link->network->vrf) {
+ log_struct(LOG_DEBUG,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(link->network->vrf),
+ LOG_LINK_MESSAGE(link, "Enslaving by '%s'", link->network->vrf->ifname));
+
+ link->enslaving++;
+
+ r = netdev_join(link->network->vrf, link, netdev_join_handler);
+ if (r < 0) {
+ log_struct_errno(LOG_WARNING, r,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(link->network->vrf),
+ LOG_LINK_MESSAGE(link, "Could not join netdev '%s': %m", link->network->vrf->ifname));
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ HASHMAP_FOREACH(netdev, link->network->stacked_netdevs) {
+
+ if (netdev->ifindex > 0)
+ /* Assume already enslaved. */
+ continue;
+
+ if (netdev_get_create_type(netdev) != NETDEV_CREATE_STACKED)
+ continue;
+
+ log_struct(LOG_DEBUG,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(netdev),
+ LOG_LINK_MESSAGE(link, "Enslaving by '%s'", netdev->ifname));
+
+ link->enslaving++;
+
+ r = netdev_join(netdev, link, netdev_join_handler);
+ if (r < 0) {
+ log_struct_errno(LOG_WARNING, r,
+ LOG_LINK_INTERFACE(link),
+ LOG_NETDEV_INTERFACE(netdev),
+ LOG_LINK_MESSAGE(link, "Could not join netdev '%s': %m", netdev->ifname));
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ if (link->enslaving == 0)
+ return link_joined(link);
+
+ return 0;
+}
+
+static int link_drop_foreign_config(Link *link) {
+ int r;
+
+ r = link_drop_foreign_addresses(link);
+ if (r < 0)
+ return r;
+
+ r = link_drop_foreign_neighbors(link);
+ if (r < 0)
+ return r;
+
+ return link_drop_foreign_routes(link);
+}
+
+static int link_drop_config(Link *link) {
+ int r;
+
+ r = link_drop_addresses(link);
+ if (r < 0)
+ return r;
+
+ r = link_drop_neighbors(link);
+ if (r < 0)
+ return r;
+
+ r = link_drop_routes(link);
+ if (r < 0)
+ return r;
+
+ ndisc_flush(link);
+
+ return 0;
+}
+
+int link_configure(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state == LINK_STATE_INITIALIZED);
+
+ r = link_configure_traffic_control(link);
+ if (r < 0)
+ return r;
+
+ r = link_configure_sr_iov(link);
+ if (r < 0)
+ return r;
+
+ if (link->iftype == ARPHRD_CAN)
+ return link_configure_can(link);
+
+ r = link_set_sysctl(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_ipv6_proxy_ndp_addresses(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_mac(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_nomaster(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_flags(link);
+ if (r < 0)
+ return r;
+
+ r = link_set_group(link);
+ if (r < 0)
+ return r;
+
+ r = ipv4ll_configure(link);
+ if (r < 0)
+ return r;
+
+ r = dhcp4_configure(link);
+ if (r < 0)
+ return r;
+
+ r = dhcp6_configure(link);
+ if (r < 0)
+ return r;
+
+ r = ndisc_configure(link);
+ if (r < 0)
+ return r;
+
+ r = radv_configure(link);
+ if (r < 0)
+ return r;
+
+ r = link_lldp_rx_configure(link);
+ if (r < 0)
+ return r;
+
+ r = link_configure_mtu(link);
+ if (r < 0)
+ return r;
+
+ r = link_configure_addrgen_mode(link);
+ if (r < 0)
+ return r;
+
+ return link_configure_continue(link);
+}
+
+/* The configuration continues in this separate function, instead of
+ * including this in the above link_configure() function, for two
+ * reasons:
+ * 1) some devices reset the link when the mtu is set, which caused
+ * an infinite loop here in networkd; see:
+ * https://github.com/systemd/systemd/issues/6593
+ * https://github.com/systemd/systemd/issues/9831
+ * 2) if ipv6ll is disabled, then bringing the interface up must be
+ * delayed until after we get confirmation from the kernel that
+ * the addr_gen_mode parameter has been set (via netlink), see:
+ * https://github.com/systemd/systemd/issues/13882
+ */
+static int link_configure_continue(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state == LINK_STATE_INITIALIZED);
+
+ if (link->setting_mtu || link->setting_genmode)
+ return 0;
+
+ /* Drop foreign config, but ignore loopback or critical devices.
+ * We do not want to remove loopback address or addresses used for root NFS. */
+ if (!(link->flags & IFF_LOOPBACK) &&
+ link->network->keep_configuration != KEEP_CONFIGURATION_YES) {
+ r = link_drop_foreign_config(link);
+ 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, "Cannot set IPv6 MTU for interface, ignoring: %m");
+
+ return link_enter_join_netdev(link);
+}
+
+static int link_reconfigure_internal(Link *link, sd_netlink_message *m, bool force) {
+ _cleanup_strv_free_ char **s = NULL;
+ Network *network;
+ int r;
+
+ assert(m);
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_read_strv(m, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &s);
+ if (r < 0 && r != -ENODATA)
+ return r;
+
+ strv_free_and_replace(link->alternative_names, s);
+
+ r = network_get(link->manager, link->iftype, link->sd_device,
+ link->ifname, link->alternative_names, link->driver,
+ &link->hw_addr.addr.ether, &link->permanent_mac,
+ link->wlan_iftype, link->ssid, &link->bssid, &network);
+ if (r == -ENOENT) {
+ link_enter_unmanaged(link);
+ return 0;
+ } else if (r == 0 && network->unmanaged) {
+ link_enter_unmanaged(link);
+ return 0;
+ } else if (r < 0)
+ return r;
+
+ if (link->network == network && !force)
+ return 0;
+
+ log_link_info(link, "Re-configuring with %s", network->filename);
+
+ /* Dropping old .network file */
+ r = link_stop_engines(link, false);
+ if (r < 0)
+ return r;
+
+ r = link_drop_config(link);
+ if (r < 0)
+ return r;
+
+ if (!IN_SET(link->state, LINK_STATE_UNMANAGED, LINK_STATE_PENDING, LINK_STATE_INITIALIZED)) {
+ log_link_debug(link, "State is %s, dropping config", link_state_to_string(link->state));
+ r = link_drop_foreign_config(link);
+ if (r < 0)
+ return r;
+ }
+
+ link_free_carrier_maps(link);
+ link_free_engines(link);
+ link->network = network_unref(link->network);
+
+ /* Then, apply new .network file */
+ r = network_apply(network, link);
+ if (r < 0)
+ return r;
+
+ r = link_new_carrier_maps(link);
+ if (r < 0)
+ return r;
+
+ link_set_state(link, LINK_STATE_INITIALIZED);
+ link_dirty(link);
+
+ /* link_configure_duid() returns 0 if it requests product UUID. In that case,
+ * link_configure() is called later asynchronously. */
+ r = link_configure_duid(link);
+ if (r <= 0)
+ return r;
+
+ r = link_configure(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int link_reconfigure_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ r = link_reconfigure_internal(link, m, false);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+static int link_force_reconfigure_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ r = link_reconfigure_internal(link, m, true);
+ if (r < 0)
+ link_enter_failed(link);
+
+ return 1;
+}
+
+int link_reconfigure(Link *link, bool force) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ /* When link in pending or initialized state, then link_configure() will be called. To prevent
+ * the function be called multiple times simultaneously, refuse to reconfigure the interface in
+ * these case. */
+ if (IN_SET(link->state, LINK_STATE_PENDING, LINK_STATE_INITIALIZED, LINK_STATE_LINGER))
+ return 0; /* o means no-op. */
+
+ 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,
+ force ? link_force_reconfigure_handler : link_reconfigure_handler,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return r;
+
+ link_ref(link);
+
+ return 1; /* 1 means the interface will be reconfigured. */
+}
+
+static int link_initialized_and_synced(Link *link) {
+ Network *network;
+ int r;
+
+ assert(link);
+ assert(link->ifname);
+ assert(link->manager);
+
+ /* We may get called either from the asynchronous netlink callback,
+ * or directly for link_add() if running in a container. See link_add(). */
+ 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;
+
+ if (!link->network) {
+ r = wifi_get_info(link);
+ if (r < 0)
+ return r;
+
+ r = network_get(link->manager, link->iftype, link->sd_device,
+ link->ifname, link->alternative_names, link->driver,
+ &link->hw_addr.addr.ether, &link->permanent_mac,
+ link->wlan_iftype, link->ssid, &link->bssid, &network);
+ if (r == -ENOENT) {
+ link_enter_unmanaged(link);
+ return 0;
+ } else if (r == 0 && network->unmanaged) {
+ link_enter_unmanaged(link);
+ return 0;
+ } else if (r < 0)
+ return r;
+
+ if (link->flags & IFF_LOOPBACK) {
+ if (network->link_local != ADDRESS_FAMILY_NO)
+ log_link_debug(link, "Ignoring link-local autoconfiguration for loopback link");
+
+ if (network->dhcp != ADDRESS_FAMILY_NO)
+ log_link_debug(link, "Ignoring DHCP clients for loopback link");
+
+ if (network->dhcp_server)
+ log_link_debug(link, "Ignoring DHCP server for loopback link");
+ }
+
+ r = network_apply(network, link);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_new_bound_to_list(link);
+ if (r < 0)
+ return r;
+
+ /* link_configure_duid() returns 0 if it requests product UUID. In that case,
+ * link_configure() is called later asynchronously. */
+ r = link_configure_duid(link);
+ if (r <= 0)
+ return r;
+
+ r = link_configure(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int link_initialized_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ _cleanup_strv_free_ char **s = NULL;
+ int r;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to wait for the interface to be initialized: %m");
+ link_enter_failed(link);
+ return 0;
+ }
+
+ r = sd_netlink_message_read_strv(m, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &s);
+ if (r < 0 && r != -ENODATA) {
+ link_enter_failed(link);
+ return 0;
+ }
+
+ strv_free_and_replace(link->alternative_names, s);
+
+ r = link_initialized_and_synced(link);
+ if (r < 0)
+ link_enter_failed(link);
+ return 1;
+}
+
+int link_initialized(Link *link, sd_device *device) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(device);
+
+ if (link->state != LINK_STATE_PENDING)
+ return 0;
+
+ if (link->sd_device)
+ return 0;
+
+ log_link_debug(link, "udev initialized link");
+ link_set_state(link, LINK_STATE_INITIALIZED);
+
+ link->sd_device = sd_device_ref(device);
+
+ /* 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 */
+
+ 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, link_initialized_handler,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return r;
+
+ link_ref(link);
+
+ return 0;
+}
+
+static int link_load(Link *link) {
+ _cleanup_free_ char *network_file = NULL,
+ *addresses = NULL,
+ *routes = NULL,
+ *dhcp4_address = NULL,
+ *ipv4ll_address = NULL;
+ int r;
+
+ assert(link);
+
+ r = parse_env_file(NULL, link->state_file,
+ "NETWORK_FILE", &network_file,
+ "ADDRESSES", &addresses,
+ "ROUTES", &routes,
+ "DHCP4_ADDRESS", &dhcp4_address,
+ "IPV4LL_ADDRESS", &ipv4ll_address);
+ if (r < 0 && r != -ENOENT)
+ return log_link_error_errno(link, r, "Failed to read %s: %m", link->state_file);
+
+ if (network_file) {
+ Network *network;
+ char *suffix;
+
+ /* drop suffix */
+ suffix = strrchr(network_file, '.');
+ if (!suffix) {
+ log_link_debug(link, "Failed to get network name from %s", network_file);
+ goto network_file_fail;
+ }
+ *suffix = '\0';
+
+ r = network_get_by_name(link->manager, basename(network_file), &network);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "Failed to get network %s: %m", basename(network_file));
+ goto network_file_fail;
+ }
+
+ r = network_apply(network, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to apply network %s: %m", basename(network_file));
+ }
+
+network_file_fail:
+
+ r = link_deserialize_addresses(link, addresses);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to load addresses from %s, ignoring: %m", link->state_file);
+
+ r = link_deserialize_routes(link, routes);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to load routes from %s, ignoring: %m", link->state_file);
+
+ r = link_deserialize_dhcp4(link, dhcp4_address);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to load DHCPv4 address from %s, ignoring: %m", link->state_file);
+
+ r = link_deserialize_ipv4ll(link, ipv4ll_address);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to load IPv4LL address from %s, ignoring: %m", link->state_file);
+
+ return 0;
+}
+
+int link_add(Manager *m, sd_netlink_message *message, Link **ret) {
+ _cleanup_(sd_device_unrefp) sd_device *device = NULL;
+ char ifindex_str[2 + DECIMAL_STR_MAX(int)];
+ Link *link;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+ assert(message);
+ assert(ret);
+
+ r = link_new(m, message, ret);
+ if (r < 0)
+ return r;
+
+ link = *ret;
+
+ log_link_debug(link, "Link %d added", link->ifindex);
+
+ r = link_load(link);
+ if (r < 0)
+ return r;
+
+ if (path_is_read_only_fs("/sys") <= 0) {
+ /* udev should be around */
+ sprintf(ifindex_str, "n%d", link->ifindex);
+ r = sd_device_new_from_device_id(&device, ifindex_str);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not find device, waiting for device initialization: %m");
+ return 0;
+ }
+
+ r = sd_device_get_is_initialized(device);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not determine whether the device is initialized: %m");
+ goto failed;
+ }
+ if (r == 0) {
+ /* not yet ready */
+ log_link_debug(link, "link pending udev initialization...");
+ return 0;
+ }
+
+ r = device_is_renaming(device);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to determine the device is being renamed: %m");
+ goto failed;
+ }
+ if (r > 0) {
+ log_link_debug(link, "Interface is being renamed, pending initialization.");
+ return 0;
+ }
+
+ r = link_initialized(link, device);
+ if (r < 0)
+ goto failed;
+ } else {
+ r = link_initialized_and_synced(link);
+ if (r < 0)
+ goto failed;
+ }
+
+ return 0;
+failed:
+ link_enter_failed(link);
+ return r;
+}
+
+int link_ipv6ll_gained(Link *link, const struct in6_addr *address) {
+ int r;
+
+ assert(link);
+
+ log_link_info(link, "Gained IPv6LL");
+
+ link->ipv6ll_address = *address;
+ link_check_ready(link);
+
+ if (IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) {
+ r = link_acquire_ipv6_conf(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+static int link_carrier_gained(Link *link) {
+ int r;
+
+ assert(link);
+
+ r = wifi_get_info(link);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ r = link_reconfigure(link, false);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ }
+
+ if (IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) {
+ r = link_acquire_conf(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ r = link_set_static_configs(link);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_handle_bound_by_list(link);
+ if (r < 0)
+ return r;
+
+ if (!link->bridge_mdb_configured) {
+ r = link_set_bridge_mdb(link);
+ if (r < 0)
+ return r;
+ }
+
+ if (streq_ptr(link->kind, "bridge")) {
+ Link *slave;
+
+ SET_FOREACH(slave, link->slaves) {
+ if (slave->bridge_mdb_configured)
+ continue;
+
+ r = link_set_bridge_mdb(slave);
+ if (r < 0)
+ link_enter_failed(slave);
+ }
+ }
+
+ return 0;
+}
+
+static int link_carrier_lost(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (link->network && link->network->ignore_carrier_loss)
+ return 0;
+
+ /* Some devices reset itself while setting the MTU. This causes the DHCP client fall into a loop.
+ * setting_mtu keep track whether the device got reset because of setting MTU and does not drop the
+ * configuration and stop the clients as well. */
+ if (link->setting_mtu)
+ return 0;
+
+ r = link_stop_engines(link, false);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+
+ r = link_drop_config(link);
+ if (r < 0)
+ return r;
+
+ if (!IN_SET(link->state, LINK_STATE_UNMANAGED, LINK_STATE_PENDING, LINK_STATE_INITIALIZED)) {
+ log_link_debug(link, "State is %s, dropping config", link_state_to_string(link->state));
+ r = link_drop_foreign_config(link);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_handle_bound_by_list(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int link_carrier_reset(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (link_has_carrier(link)) {
+ r = link_carrier_lost(link);
+ if (r < 0)
+ return r;
+
+ r = link_carrier_gained(link);
+ if (r < 0)
+ return r;
+
+ log_link_info(link, "Reset carrier");
+ }
+
+ return 0;
+}
+
+/* This is called every time an interface admin state changes to up;
+ * specifically, when IFF_UP flag changes from unset to set */
+static int link_admin_state_up(Link *link) {
+ int r;
+
+ /* We set the ipv6 mtu after the device mtu, but the kernel resets
+ * ipv6 mtu on NETDEV_UP, so we need to reset it. The check for
+ * ipv6_mtu_set prevents this from trying to set it too early before
+ * the link->network has been setup; we only need to reset it
+ * here if we've already set it during normal initialization. */
+ if (link->ipv6_mtu_set) {
+ r = link_set_ipv6_mtu(link);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int link_update(Link *link, sd_netlink_message *m) {
+ _cleanup_strv_free_ char **s = NULL;
+ hw_addr_data hw_addr;
+ const char *ifname;
+ uint32_t mtu;
+ bool had_carrier, carrier_gained, carrier_lost, link_was_admin_up;
+ int old_master, r;
+
+ assert(link);
+ assert(link->ifname);
+ assert(m);
+
+ if (link->state == LINK_STATE_LINGER) {
+ log_link_info(link, "Link re-added");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ r = link_new_carrier_maps(link);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_read_string(m, IFLA_IFNAME, &ifname);
+ if (r >= 0 && !streq(ifname, link->ifname)) {
+ Manager *manager = link->manager;
+
+ log_link_info(link, "Interface name change detected, %s has been renamed to %s.", link->ifname, ifname);
+
+ link_drop(link);
+ r = link_add(manager, m, &link);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_netlink_message_read_strv(m, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &s);
+ if (r >= 0)
+ strv_free_and_replace(link->alternative_names, s);
+
+ r = sd_netlink_message_read_u32(m, IFLA_MTU, &mtu);
+ if (r >= 0 && mtu > 0) {
+ link->mtu = mtu;
+ if (link->original_mtu == 0) {
+ link->original_mtu = mtu;
+ log_link_debug(link, "Saved original MTU: %" PRIu32, link->original_mtu);
+ }
+
+ if (link->dhcp_client) {
+ r = sd_dhcp_client_set_mtu(link->dhcp_client,
+ link->mtu);
+ if (r < 0)
+ return log_link_warning_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_warning_errno(link, r, "Could not set MTU for Router Advertisement: %m");
+ }
+ }
+
+ /* The kernel may broadcast NEWLINK messages without the MAC address
+ set, simply ignore them. */
+ r = netlink_message_read_hw_addr(m, IFLA_ADDRESS, &hw_addr);
+ if (r >= 0 && (link->hw_addr.length != hw_addr.length ||
+ memcmp(link->hw_addr.addr.bytes, hw_addr.addr.bytes, hw_addr.length) != 0)) {
+
+ memcpy(link->hw_addr.addr.bytes, hw_addr.addr.bytes, hw_addr.length);
+
+ log_link_debug(link, "Gained new hardware address: %s", HW_ADDR_TO_STR(&hw_addr));
+
+ r = ipv4ll_update_mac(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not update MAC address in IPv4LL client: %m");
+
+ r = dhcp4_update_mac(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not update MAC address in DHCP client: %m");
+
+ r = dhcp6_update_mac(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not update MAC address in DHCPv6 client: %m");
+
+ r = radv_update_mac(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not update MAC address for Router Advertisement: %m");
+
+ if (link->ndisc) {
+ r = sd_ndisc_set_mac(link->ndisc, &link->hw_addr.addr.ether);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not update MAC for NDisc: %m");
+ }
+
+ r = ipv4_dad_update_mac(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not update MAC address in IPv4 ACD client: %m");
+ }
+
+ old_master = link->master_ifindex;
+ (void) sd_netlink_message_read_u32(m, IFLA_MASTER, (uint32_t *) &link->master_ifindex);
+
+ link_was_admin_up = link->flags & IFF_UP;
+ had_carrier = link_has_carrier(link);
+
+ r = link_update_flags(link, m, old_master != link->master_ifindex);
+ if (r < 0)
+ return r;
+
+ 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_update_lldp(link);
+ if (r < 0)
+ return r;
+
+ carrier_gained = !had_carrier && link_has_carrier(link);
+ carrier_lost = had_carrier && !link_has_carrier(link);
+
+ if (carrier_gained) {
+ log_link_info(link, "Gained carrier");
+
+ r = link_carrier_gained(link);
+ if (r < 0)
+ return r;
+ } else if (carrier_lost) {
+ log_link_info(link, "Lost carrier");
+
+ r = link_carrier_lost(link);
+ if (r < 0)
+ return r;
+ }
+
+ 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) {
+ 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 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 ***)) {
+ int r;
+
+ bool _space = false;
+ 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);
+}
+
+int link_save(Link *link) {
+ const char *admin_state, *oper_state, *carrier_state, *address_state;
+ _cleanup_free_ char *temp_path = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(link);
+ assert(link->state_file);
+ assert(link->lease_file);
+ assert(link->manager);
+
+ if (link->state == LINK_STATE_LINGER) {
+ (void) unlink(link->state_file);
+ 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);
+
+ r = fopen_temporary(link->state_file, &f, &temp_path);
+ if (r < 0)
+ goto fail;
+
+ (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",
+ admin_state, oper_state, carrier_state, address_state);
+
+ if (link->network) {
+ char **dhcp6_domains = NULL, **dhcp_domains = NULL;
+ const char *dhcp_domainname = NULL, *p;
+ bool space;
+
+ 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, "NETWORK_FILE=%s\n", link->network->filename);
+
+ /************************************************************/
+
+ fputs("DNS=", f);
+ space = false;
+ if (link->n_dns != (unsigned) -1)
+ link_save_dns(link, f, link->dns, link->n_dns, &space);
+ else
+ 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);
+
+ /* Make sure to flush out old entries before we use the NDisc data */
+ ndisc_vacuum(link);
+
+ if (link->network->ipv6_accept_ra_use_dns && link->ndisc_rdnss) {
+ NDiscRDNSS *dd;
+
+ SET_FOREACH(dd, link->ndisc_rdnss)
+ serialize_in6_addrs(f, &dd->address, 1, &space);
+ }
+
+ fputc('\n', f);
+
+ /************************************************************/
+
+ serialize_addresses(f, "NTP", NULL,
+ link->ntp ?: 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);
+
+ /************************************************************/
+
+ if (link->network->dhcp_use_domains != DHCP_USE_DOMAINS_NO) {
+ if (link->dhcp_lease) {
+ (void) sd_dhcp_lease_get_domainname(link->dhcp_lease, &dhcp_domainname);
+ (void) sd_dhcp_lease_get_search_domains(link->dhcp_lease, &dhcp_domains);
+ }
+ if (link->dhcp6_lease)
+ (void) sd_dhcp6_lease_get_domains(link->dhcp6_lease, &dhcp6_domains);
+ }
+
+ fputs("DOMAINS=", f);
+ space = false;
+ ORDERED_SET_FOREACH(p, link->search_domains ?: link->network->search_domains)
+ fputs_with_space(f, p, NULL, &space);
+
+ if (link->network->dhcp_use_domains == DHCP_USE_DOMAINS_YES) {
+ if (dhcp_domainname)
+ fputs_with_space(f, dhcp_domainname, NULL, &space);
+ if (dhcp_domains)
+ fputstrv(f, dhcp_domains, NULL, &space);
+ if (dhcp6_domains)
+ fputstrv(f, dhcp6_domains, NULL, &space);
+ }
+
+ if (link->network->ipv6_accept_ra_use_domains == DHCP_USE_DOMAINS_YES) {
+ NDiscDNSSL *dd;
+
+ SET_FOREACH(dd, link->ndisc_dnssl)
+ fputs_with_space(f, NDISC_DNSSL_DOMAIN(dd), NULL, &space);
+ }
+
+ fputc('\n', f);
+
+ /************************************************************/
+
+ fputs("ROUTE_DOMAINS=", f);
+ space = false;
+ ORDERED_SET_FOREACH(p, link->route_domains ?: link->network->route_domains)
+ fputs_with_space(f, p, NULL, &space);
+
+ if (link->network->dhcp_use_domains == DHCP_USE_DOMAINS_ROUTE) {
+ if (dhcp_domainname)
+ fputs_with_space(f, dhcp_domainname, NULL, &space);
+ if (dhcp_domains)
+ fputstrv(f, dhcp_domains, NULL, &space);
+ if (dhcp6_domains)
+ fputstrv(f, dhcp6_domains, NULL, &space);
+ }
+
+ if (link->network->ipv6_accept_ra_use_domains == DHCP_USE_DOMAINS_ROUTE) {
+ NDiscDNSSL *dd;
+
+ SET_FOREACH(dd, link->ndisc_dnssl)
+ fputs_with_space(f, NDISC_DNSSL_DOMAIN(dd), NULL, &space);
+ }
+
+ 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);
+ }
+
+ /************************************************************/
+
+ r = link_serialize_addresses(link, f);
+ if (r < 0)
+ goto fail;
+
+ /************************************************************/
+
+ r = link_serialize_routes(link, f);
+ if (r < 0)
+ goto fail;
+ }
+
+ 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)
+ goto fail;
+
+ fprintf(f,
+ "DHCP_LEASE=%s\n",
+ link->lease_file);
+ } else
+ (void) unlink(link->lease_file);
+
+ r = link_serialize_ipv4ll(link, f);
+ if (r < 0)
+ goto fail;
+
+ r = link_serialize_dhcp6_client(link, f);
+ if (r < 0)
+ goto fail;
+
+ r = fflush_and_check(f);
+ if (r < 0)
+ goto fail;
+
+ if (rename(temp_path, link->state_file) < 0) {
+ r = -errno;
+ goto fail;
+ }
+
+ return 0;
+
+fail:
+ (void) unlink(link->state_file);
+ if (temp_path)
+ (void) unlink(temp_path);
+
+ return log_link_error_errno(link, r, "Failed to save link data to %s: %m", link->state_file);
+}
+
+/* The serialized state in /run is no longer up-to-date. */
+void link_dirty(Link *link) {
+ int r;
+
+ assert(link);
+
+ /* mark manager dirty as link is dirty */
+ manager_dirty(link->manager);
+
+ 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);
+}
+
+/* The serialized state in /run is up-to-date */
+void link_clean(Link *link) {
+ assert(link);
+ assert(link->manager);
+
+ 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;
+}
+
+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 log_link_message_full_errno(Link *link, sd_netlink_message *m, int level, int err, const char *msg) {
+ const char *err_msg = 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-link.h b/src/network/networkd-link.h
new file mode 100644
index 0000000..cd54192
--- /dev/null
+++ b/src/network/networkd-link.h
@@ -0,0 +1,249 @@
+/* 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.h"
+#include "sd-ndisc.h"
+#include "sd-radv.h"
+#include "sd-netlink.h"
+
+#include "ether-addr-util.h"
+#include "log-link.h"
+#include "network-util.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 = -1
+} LinkState;
+
+typedef struct Manager Manager;
+typedef struct Network Network;
+typedef struct Address Address;
+typedef struct DUID DUID;
+
+typedef struct Link {
+ Manager *manager;
+
+ unsigned n_ref;
+
+ int ifindex;
+ int master_ifindex;
+ char *ifname;
+ char **alternative_names;
+ char *kind;
+ unsigned short iftype;
+ char *state_file;
+ hw_addr_data hw_addr;
+ hw_addr_data bcast_addr;
+ struct ether_addr permanent_mac;
+ struct in6_addr ipv6ll_address;
+ uint32_t mtu;
+ sd_device *sd_device;
+ char *driver;
+
+ /* wlan */
+ enum nl80211_iftype wlan_iftype;
+ char *ssid;
+ struct ether_addr bssid;
+
+ unsigned flags;
+ uint8_t kernel_operstate;
+
+ Network *network;
+
+ LinkState state;
+ LinkOperationalState operstate;
+ LinkCarrierState carrier_state;
+ LinkAddressState address_state;
+
+ unsigned address_messages;
+ unsigned address_remove_messages;
+ unsigned address_label_messages;
+ unsigned neighbor_messages;
+ unsigned route_messages;
+ unsigned nexthop_messages;
+ unsigned routing_policy_rule_messages;
+ unsigned routing_policy_rule_remove_messages;
+ unsigned tc_messages;
+ unsigned sr_iov_messages;
+ unsigned enslaving;
+ unsigned bridge_mdb_messages;
+
+ Set *addresses;
+ Set *addresses_foreign;
+ Set *pool_addresses;
+ Set *static_addresses;
+ Set *neighbors;
+ Set *neighbors_foreign;
+ Set *routes;
+ Set *routes_foreign;
+ Set *nexthops;
+ Set *nexthops_foreign;
+
+ sd_dhcp_client *dhcp_client;
+ sd_dhcp_lease *dhcp_lease;
+ Address *dhcp_address, *dhcp_address_old;
+ Set *dhcp_routes, *dhcp_routes_old;
+ char *lease_file;
+ uint32_t original_mtu;
+ unsigned dhcp4_messages;
+ unsigned dhcp4_remove_messages;
+ sd_ipv4acd *dhcp_acd;
+ bool dhcp4_route_failed:1;
+ bool dhcp4_route_retrying:1;
+ bool dhcp4_configured:1;
+ bool dhcp4_address_bind:1;
+
+ sd_ipv4ll *ipv4ll;
+ bool ipv4ll_address_configured:1;
+
+ bool request_static_addresses:1;
+ bool addresses_configured:1;
+ bool addresses_ready:1;
+ bool neighbors_configured:1;
+ bool static_routes_configured:1;
+ bool static_nexthops_configured:1;
+ bool routing_policy_rules_configured:1;
+ bool tc_configured:1;
+ bool sr_iov_configured:1;
+ bool setting_mtu:1;
+ bool setting_genmode:1;
+ bool ipv6_mtu_set:1;
+ bool bridge_mdb_configured:1;
+
+ sd_dhcp_server *dhcp_server;
+
+ sd_ndisc *ndisc;
+ Set *ndisc_rdnss;
+ Set *ndisc_dnssl;
+ Set *ndisc_addresses;
+ Set *ndisc_routes;
+ unsigned ndisc_addresses_messages;
+ unsigned ndisc_routes_messages;
+ bool ndisc_addresses_configured:1;
+ bool ndisc_routes_configured:1;
+
+ sd_radv *radv;
+
+ sd_dhcp6_client *dhcp6_client;
+ sd_dhcp6_lease *dhcp6_lease;
+ Set *dhcp6_addresses, *dhcp6_addresses_old;
+ Set *dhcp6_routes, *dhcp6_routes_old;
+ Set *dhcp6_pd_addresses, *dhcp6_pd_addresses_old;
+ Set *dhcp6_pd_routes, *dhcp6_pd_routes_old;
+ unsigned dhcp6_address_messages;
+ unsigned dhcp6_route_messages;
+ unsigned dhcp6_pd_address_messages;
+ unsigned dhcp6_pd_route_messages;
+ bool dhcp6_address_configured:1;
+ bool dhcp6_route_configured:1;
+ bool dhcp6_pd_address_configured:1;
+ bool dhcp6_pd_route_configured:1;
+ bool dhcp6_pd_prefixes_assigned:1;
+
+ /* This is about LLDP reception */
+ sd_lldp *lldp;
+ char *lldp_file;
+
+ /* This is about LLDP transmission */
+ unsigned lldp_tx_fast; /* The LLDP txFast counter (See 802.1ab-2009, section 9.2.5.18) */
+ sd_event_source *lldp_emit_event_source;
+
+ 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*);
+
+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(Manager *m, int ifindex, Link **ret);
+int link_add(Manager *manager, sd_netlink_message *message, Link **ret);
+void link_drop(Link *link);
+
+int link_down(Link *link, link_netlink_message_handler_t callback);
+
+void link_enter_failed(Link *link);
+int link_initialized(Link *link, sd_device *device);
+
+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);
+int link_update(Link *link, sd_netlink_message *message);
+
+void link_dirty(Link *link);
+void link_clean(Link *link);
+int link_save(Link *link);
+int link_save_and_clean(Link *link);
+
+int link_carrier_reset(Link *link);
+bool link_has_carrier(Link *link);
+
+bool link_ipv6_enabled(Link *link);
+bool link_ipv6ll_enabled(Link *link);
+int link_ipv6ll_gained(Link *link, const struct in6_addr *address);
+
+int link_set_mtu(Link *link, uint32_t mtu);
+
+bool link_ipv4ll_enabled(Link *link, AddressFamily mask);
+
+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_configure(Link *link);
+int link_reconfigure(Link *link, bool force);
+
+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)
diff --git a/src/network/networkd-lldp-rx.c b/src/network/networkd-lldp-rx.c
new file mode 100644
index 0000000..c22852f
--- /dev/null
+++ b/src/network/networkd-lldp-rx.c
@@ -0,0 +1,205 @@
+/* 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 "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_handler(sd_lldp *lldp, sd_lldp_event event, sd_lldp_neighbor *n, void *userdata) {
+ Link *link = userdata;
+ int r;
+
+ assert(link);
+
+ (void) link_lldp_save(link);
+
+ if (link_lldp_emit_enabled(link) && event == SD_LLDP_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.");
+
+ r = link_lldp_emit_start(link);
+ 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) {
+ r = sd_lldp_new(&link->lldp);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_attach_event(link->lldp, link->manager->event, 0);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_lldp_set_ifindex(link->lldp, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_match_capabilities(link->lldp,
+ 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_set_filter_address(link->lldp, &link->hw_addr.addr.ether);
+ if (r < 0)
+ return r;
+
+ r = sd_lldp_set_callback(link->lldp, lldp_handler, link);
+ if (r < 0)
+ return r;
+
+ r = link_update_lldp(link);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int link_update_lldp(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link->lldp)
+ return 0;
+
+ if (link->flags & IFF_UP) {
+ r = sd_lldp_start(link->lldp);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start LLDP: %m");
+ if (r > 0)
+ log_link_debug(link, "Started LLDP.");
+ } else {
+ r = sd_lldp_stop(link->lldp);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to stop LLDP: %m");
+ if (r > 0)
+ log_link_debug(link, "Stopped LLDP.");
+ }
+
+ return r;
+}
+
+int link_lldp_save(Link *link) {
+ _cleanup_free_ char *temp_path = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ sd_lldp_neighbor **l = NULL;
+ int n = 0, r, i;
+
+ assert(link);
+ assert(link->lldp_file);
+
+ if (!link->lldp) {
+ (void) unlink(link->lldp_file);
+ return 0;
+ }
+
+ r = sd_lldp_get_neighbors(link->lldp, &l);
+ if (r < 0)
+ goto finish;
+ if (r == 0) {
+ (void) unlink(link->lldp_file);
+ goto finish;
+ }
+
+ n = r;
+
+ r = fopen_temporary(link->lldp_file, &f, &temp_path);
+ if (r < 0)
+ goto finish;
+
+ 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;
+
+ if (rename(temp_path, link->lldp_file) < 0) {
+ r = -errno;
+ goto finish;
+ }
+
+finish:
+ if (r < 0) {
+ (void) unlink(link->lldp_file);
+ if (temp_path)
+ (void) unlink(temp_path);
+
+ 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..78c5228
--- /dev/null
+++ b/src/network/networkd-lldp-rx.h
@@ -0,0 +1,23 @@
+/* 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 = -1,
+} LLDPMode;
+
+int link_lldp_rx_configure(Link *link);
+int link_update_lldp(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..b03d948
--- /dev/null
+++ b/src/network/networkd-lldp-tx.c
@@ -0,0 +1,493 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <endian.h>
+#include <inttypes.h>
+#include <net/if.h>
+#include <net/if_arp.h>
+
+#include "alloc-util.h"
+#include "env-file.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "hostname-util.h"
+#include "missing_network.h"
+#include "networkd-link.h"
+#include "networkd-lldp-tx.h"
+#include "networkd-manager.h"
+#include "parse-util.h"
+#include "random-util.h"
+#include "socket-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "unaligned.h"
+#include "web-util.h"
+
+/* The LLDP spec calls this "txFastInit", see 9.2.5.19 */
+#define LLDP_TX_FAST_INIT 4U
+
+/* The LLDP spec calls this "msgTxHold", see 9.2.5.6 */
+#define LLDP_TX_HOLD 4U
+
+/* The jitter range to add, see 9.2.2. */
+#define LLDP_JITTER_USEC (400U * USEC_PER_MSEC)
+
+/* The LLDP spec calls this msgTxInterval, but we subtract half the jitter off it. */
+#define LLDP_TX_INTERVAL_USEC (30U * USEC_PER_SEC - LLDP_JITTER_USEC / 2)
+
+/* The LLDP spec calls this msgFastTx, but we subtract half the jitter off it. */
+#define LLDP_FAST_TX_USEC (1U * USEC_PER_SEC - LLDP_JITTER_USEC / 2)
+
+static const struct ether_addr lldp_multicast_addr[_LLDP_EMIT_MAX] = {
+ [LLDP_EMIT_NEAREST_BRIDGE] = {{ 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e }},
+ [LLDP_EMIT_NON_TPMR_BRIDGE] = {{ 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03 }},
+ [LLDP_EMIT_CUSTOMER_BRIDGE] = {{ 0x01, 0x80, 0xc2, 0x00, 0x00, 0x00 }},
+};
+
+bool link_lldp_emit_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_emit != LLDP_EMIT_NO;
+}
+
+static int lldp_write_tlv_header(uint8_t **p, uint8_t id, size_t sz) {
+ assert(p);
+
+ if (id > 127)
+ return -EBADMSG;
+ if (sz > 511)
+ return -ENOBUFS;
+
+ (*p)[0] = (id << 1) | !!(sz & 256);
+ (*p)[1] = sz & 255;
+
+ *p = *p + 2;
+ return 0;
+}
+
+static int lldp_make_packet(
+ LLDPEmit mode,
+ const struct ether_addr *hwaddr,
+ const char *machine_id,
+ const char *ifname,
+ uint16_t ttl,
+ const char *port_description,
+ const char *hostname,
+ const char *pretty_hostname,
+ uint16_t system_capabilities,
+ uint16_t enabled_capabilities,
+ char *mud,
+ void **ret, size_t *sz) {
+
+ size_t machine_id_length, ifname_length, port_description_length = 0, hostname_length = 0,
+ pretty_hostname_length = 0, mud_length = 0;
+ _cleanup_free_ void *packet = NULL;
+ struct ether_header *h;
+ uint8_t *p;
+ size_t l;
+ int r;
+
+ assert(mode > LLDP_EMIT_NO);
+ assert(mode < _LLDP_EMIT_MAX);
+ assert(hwaddr);
+ assert(machine_id);
+ assert(ifname);
+ assert(ret);
+ assert(sz);
+
+ machine_id_length = strlen(machine_id);
+ ifname_length = strlen(ifname);
+
+ if (port_description)
+ port_description_length = strlen(port_description);
+
+ if (hostname)
+ hostname_length = strlen(hostname);
+
+ if (pretty_hostname)
+ pretty_hostname_length = strlen(pretty_hostname);
+
+ if (mud)
+ mud_length = strlen(mud);
+
+ l = sizeof(struct ether_header) +
+ /* Chassis ID */
+ 2 + 1 + machine_id_length +
+ /* Port ID */
+ 2 + 1 + ifname_length +
+ /* TTL */
+ 2 + 2 +
+ /* System Capabilities */
+ 2 + 4 +
+ /* End */
+ 2;
+
+ /* Port Description */
+ if (port_description)
+ l += 2 + port_description_length;
+
+ /* System Name */
+ if (hostname)
+ l += 2 + hostname_length;
+
+ /* System Description */
+ if (pretty_hostname)
+ l += 2 + pretty_hostname_length;
+
+ /* MUD URL */
+ if (mud)
+ l += 2 + sizeof(SD_LLDP_OUI_MUD) + 1 + mud_length;
+
+ packet = malloc(l);
+ if (!packet)
+ return -ENOMEM;
+
+ h = (struct ether_header*) packet;
+ h->ether_type = htobe16(ETHERTYPE_LLDP);
+ memcpy(h->ether_dhost, lldp_multicast_addr + mode, ETH_ALEN);
+ memcpy(h->ether_shost, hwaddr, ETH_ALEN);
+
+ p = (uint8_t*) packet + sizeof(struct ether_header);
+
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_CHASSIS_ID, 1 + machine_id_length);
+ if (r < 0)
+ return r;
+ *(p++) = SD_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED;
+ p = mempcpy(p, machine_id, machine_id_length);
+
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_PORT_ID, 1 + ifname_length);
+ if (r < 0)
+ return r;
+ *(p++) = SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME;
+ p = mempcpy(p, ifname, ifname_length);
+
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_TTL, 2);
+ if (r < 0)
+ return r;
+ unaligned_write_be16(p, ttl);
+ p += 2;
+
+ if (port_description) {
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_PORT_DESCRIPTION, port_description_length);
+ if (r < 0)
+ return r;
+ p = mempcpy(p, port_description, port_description_length);
+ }
+
+ if (hostname) {
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_SYSTEM_NAME, hostname_length);
+ if (r < 0)
+ return r;
+ p = mempcpy(p, hostname, hostname_length);
+ }
+
+ if (pretty_hostname) {
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_SYSTEM_DESCRIPTION, pretty_hostname_length);
+ if (r < 0)
+ return r;
+ p = mempcpy(p, pretty_hostname, pretty_hostname_length);
+ }
+
+ if (mud) {
+ uint8_t oui_mud[sizeof(SD_LLDP_OUI_MUD)] = {0x00, 0x00, 0x5E};
+ /*
+ * +--------+--------+----------+---------+--------------
+ * |TLV Type| len | OUI |subtype | MUDString
+ * | =127 | |= 00 00 5E| = 1 |
+ * |(7 bits)|(9 bits)|(3 octets)|(1 octet)|(1-255 octets)
+ * +--------+--------+----------+---------+--------------
+ * where:
+
+ * o TLV Type = 127 indicates a vendor-specific TLV
+ * o len = indicates the TLV string length
+ * o OUI = 00 00 5E is the organizationally unique identifier of IANA
+ * o subtype = 1 (as assigned by IANA for the MUDstring)
+ * o MUDstring = the length MUST NOT exceed 255 octets
+ */
+
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_PRIVATE, sizeof(SD_LLDP_OUI_MUD) + 1 + mud_length);
+ if (r < 0)
+ return r;
+
+ p = mempcpy(p, &oui_mud, sizeof(SD_LLDP_OUI_MUD));
+ *(p++) = SD_LLDP_OUI_SUBTYPE_MUD_USAGE_DESCRIPTION;
+ p = mempcpy(p, mud, mud_length);
+ }
+
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_SYSTEM_CAPABILITIES, 4);
+ if (r < 0)
+ return r;
+ unaligned_write_be16(p, system_capabilities);
+ p += 2;
+ unaligned_write_be16(p, enabled_capabilities);
+ p += 2;
+
+ r = lldp_write_tlv_header(&p, SD_LLDP_TYPE_END, 0);
+ if (r < 0)
+ return r;
+
+ assert(p == (uint8_t*) packet + l);
+
+ *ret = TAKE_PTR(packet);
+ *sz = l;
+
+ return 0;
+}
+
+static int lldp_send_packet(
+ int ifindex,
+ const struct ether_addr *address,
+ const void *packet,
+ size_t packet_size) {
+
+ union sockaddr_union sa = {
+ .ll.sll_family = AF_PACKET,
+ .ll.sll_protocol = htobe16(ETHERTYPE_LLDP),
+ .ll.sll_ifindex = ifindex,
+ .ll.sll_halen = ETH_ALEN,
+ };
+
+ _cleanup_close_ int fd = -1;
+ ssize_t l;
+
+ assert(ifindex > 0);
+ assert(address);
+ assert(packet || packet_size <= 0);
+
+ memcpy(sa.ll.sll_addr, address, ETH_ALEN);
+
+ fd = socket(AF_PACKET, SOCK_RAW|SOCK_CLOEXEC, IPPROTO_RAW);
+ if (fd < 0)
+ return -errno;
+
+ l = sendto(fd, packet, packet_size, MSG_NOSIGNAL, &sa.sa, sizeof(sa.ll));
+ if (l < 0)
+ return -errno;
+
+ if ((size_t) l != packet_size)
+ return -EIO;
+
+ return 0;
+}
+
+static int link_send_lldp(Link *link) {
+ char machine_id_string[SD_ID128_STRING_MAX];
+ _cleanup_free_ char *hostname = NULL, *pretty_hostname = NULL;
+ _cleanup_free_ void *packet = NULL;
+ size_t packet_size = 0;
+ sd_id128_t machine_id;
+ uint16_t caps;
+ usec_t ttl;
+ int r;
+
+ assert(link);
+
+ if (!link->network || link->network->lldp_emit == LLDP_EMIT_NO)
+ return 0;
+
+ assert(link->network->lldp_emit < _LLDP_EMIT_MAX);
+
+ r = sd_id128_get_machine(&machine_id);
+ if (r < 0)
+ return r;
+
+ (void) gethostname_strict(&hostname);
+ (void) parse_env_file(NULL, "/etc/machine-info", "PRETTY_HOSTNAME", &pretty_hostname);
+
+ assert_cc(LLDP_TX_INTERVAL_USEC * LLDP_TX_HOLD + 1 <= (UINT16_MAX - 1) * USEC_PER_SEC);
+ ttl = DIV_ROUND_UP(LLDP_TX_INTERVAL_USEC * LLDP_TX_HOLD + 1, USEC_PER_SEC);
+
+ caps = (link->network && link->network->ip_forward != ADDRESS_FAMILY_NO) ?
+ SD_LLDP_SYSTEM_CAPABILITIES_ROUTER :
+ SD_LLDP_SYSTEM_CAPABILITIES_STATION;
+
+ r = lldp_make_packet(link->network->lldp_emit,
+ &link->hw_addr.addr.ether,
+ sd_id128_to_string(machine_id, machine_id_string),
+ link->ifname,
+ (uint16_t) ttl,
+ link->network ? link->network->description : NULL,
+ hostname,
+ pretty_hostname,
+ SD_LLDP_SYSTEM_CAPABILITIES_STATION|SD_LLDP_SYSTEM_CAPABILITIES_BRIDGE|SD_LLDP_SYSTEM_CAPABILITIES_ROUTER,
+ caps,
+ link->network ? link->network->lldp_mud : NULL,
+ &packet, &packet_size);
+ if (r < 0)
+ return r;
+
+ return lldp_send_packet(link->ifindex, lldp_multicast_addr + link->network->lldp_emit, packet, packet_size);
+}
+
+static int on_lldp_timer(sd_event_source *s, usec_t t, void *userdata) {
+ Link *link = userdata;
+ usec_t delay;
+ int r;
+
+ assert(s);
+ assert(userdata);
+
+ log_link_debug(link, "Sending LLDP packet...");
+
+ r = link_send_lldp(link);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Failed to send LLDP packet, ignoring: %m");
+
+ if (link->lldp_tx_fast > 0)
+ link->lldp_tx_fast--;
+
+ delay = link->lldp_tx_fast > 0 ? LLDP_FAST_TX_USEC : LLDP_TX_INTERVAL_USEC;
+ delay = usec_add(delay, (usec_t) random_u64() % LLDP_JITTER_USEC);
+
+ r = sd_event_source_set_time_relative(s, delay);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to restart LLDP timer: %m");
+
+ r = sd_event_source_set_enabled(s, SD_EVENT_ONESHOT);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to enable LLDP timer: %m");
+
+ return 0;
+}
+
+int link_lldp_emit_start(Link *link) {
+ usec_t next;
+ int r;
+
+ assert(link);
+
+ if (!link_lldp_emit_enabled(link)) {
+ link_lldp_emit_stop(link);
+ return 0;
+ }
+
+ /* Starts the LLDP transmission in "fast" mode. If it is already started, turns "fast" mode back on again. */
+
+ link->lldp_tx_fast = LLDP_TX_FAST_INIT;
+
+ next = usec_add(usec_add(now(clock_boottime_or_monotonic()), LLDP_FAST_TX_USEC),
+ (usec_t) random_u64() % LLDP_JITTER_USEC);
+
+ if (link->lldp_emit_event_source) {
+ usec_t old;
+
+ /* Lower the timeout, maybe */
+ r = sd_event_source_get_time(link->lldp_emit_event_source, &old);
+ if (r < 0)
+ return r;
+
+ if (old <= next)
+ return 0;
+
+ return sd_event_source_set_time(link->lldp_emit_event_source, next);
+ } else {
+ r = sd_event_add_time(
+ link->manager->event,
+ &link->lldp_emit_event_source,
+ clock_boottime_or_monotonic(),
+ next,
+ 0,
+ on_lldp_timer,
+ link);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(link->lldp_emit_event_source, "lldp-tx");
+ }
+
+ return 0;
+}
+
+void link_lldp_emit_stop(Link *link) {
+ assert(link);
+
+ link->lldp_emit_event_source = sd_event_source_unref(link->lldp_emit_event_source);
+}
+
+int config_parse_lldp_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) {
+
+ LLDPEmit *emit = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue))
+ *emit = LLDP_EMIT_NO;
+ else if (streq(rvalue, "nearest-bridge"))
+ *emit = LLDP_EMIT_NEAREST_BRIDGE;
+ else if (streq(rvalue, "non-tpmr-bridge"))
+ *emit = LLDP_EMIT_NON_TPMR_BRIDGE;
+ else if (streq(rvalue, "customer-bridge"))
+ *emit = LLDP_EMIT_CUSTOMER_BRIDGE;
+ else {
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Failed to parse LLDP emission setting, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ *emit = r ? LLDP_EMIT_NEAREST_BRIDGE : LLDP_EMIT_NO;
+ }
+
+ return 0;
+}
+
+int config_parse_lldp_mud(
+ 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;
+ Network *n = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = cunescape(rvalue, 0, &unescaped);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to Failed to unescape LLDP MUD URL, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (!http_url_is_valid(unescaped) || strlen(unescaped) > 255) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Failed to parse LLDP MUD URL '%s', ignoring: %m", rvalue);
+
+ return 0;
+ }
+
+ return free_and_replace(n->lldp_mud, unescaped);
+}
diff --git a/src/network/networkd-lldp-tx.h b/src/network/networkd-lldp-tx.h
new file mode 100644
index 0000000..aae30cb
--- /dev/null
+++ b/src/network/networkd-lldp-tx.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "conf-parser.h"
+
+typedef struct Link Link;
+
+typedef enum LLDPEmit {
+ LLDP_EMIT_NO,
+ LLDP_EMIT_NEAREST_BRIDGE,
+ LLDP_EMIT_NON_TPMR_BRIDGE,
+ LLDP_EMIT_CUSTOMER_BRIDGE,
+ _LLDP_EMIT_MAX,
+} LLDPEmit;
+
+bool link_lldp_emit_enabled(Link *link);
+int link_lldp_emit_start(Link *link);
+void link_lldp_emit_stop(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_lldp_emit);
+CONFIG_PARSER_PROTOTYPE(config_parse_lldp_mud);
diff --git a/src/network/networkd-manager-bus.c b/src/network/networkd-manager-bus.c
new file mode 100644
index 0000000..a0ac8b5
--- /dev/null
+++ b/src/network/networkd-manager-bus.c
@@ -0,0 +1,274 @@
+/* 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-link-bus.h"
+#include "networkd-link.h"
+#include "networkd-manager-bus.h"
+#include "networkd-manager.h"
+#include "path-util.h"
+#include "socket-netlink.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) {
+ _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;
+ int index, r;
+ Link *link;
+
+ r = sd_bus_message_read(message, "s", &name);
+ if (r < 0)
+ return r;
+
+ index = resolve_ifname(&manager->rtnl, name);
+ if (index < 0)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %s cannot be resolved", name);
+
+ link = hashmap_get(manager->links, INT_TO_PTR(index));
+ if (!link)
+ 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;
+
+ link = hashmap_get(manager->links, INT_TO_PTR(ifindex));
+ if (!link)
+ 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;
+
+ l = hashmap_get(m->links, INT_TO_PTR(ifindex));
+ if (!l)
+ 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) {
+ r = link_reconfigure(link, false);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+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_METHOD("ListLinks", NULL, "a(iso)", method_list_links, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetLinkByName", "s", "io", method_get_link_by_name, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetLinkByIndex", "i", "so", method_get_link_by_index, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkNTP", "ias", NULL, bus_method_set_link_ntp_servers, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkDNS", "ia(iay)", NULL, bus_method_set_link_dns_servers, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkDNSEx", "ia(iayqs)", NULL, bus_method_set_link_dns_servers_ex, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkDomains", "ia(sb)", NULL, bus_method_set_link_domains, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkDefaultRoute", "ib", NULL, bus_method_set_link_default_route, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkLLMNR", "is", NULL, bus_method_set_link_llmnr, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkMulticastDNS", "is", NULL, bus_method_set_link_mdns, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkDNSOverTLS", "is", NULL, bus_method_set_link_dns_over_tls, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkDNSSEC", "is", NULL, bus_method_set_link_dnssec, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLinkDNSSECNegativeTrustAnchors", "ias", NULL, bus_method_set_link_dnssec_negative_trust_anchors, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("RevertLinkNTP", "i", NULL, bus_method_revert_link_ntp, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("RevertLinkDNS", "i", NULL, bus_method_revert_link_dns, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("RenewLink", "i", NULL, bus_method_renew_link, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ForceRenewLink", "i", NULL, bus_method_force_renew_link, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ReconfigureLink", "i", NULL, bus_method_reconfigure_link, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("Reload", NULL, NULL, bus_method_reload, SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_VTABLE_END
+};
+
+int manager_send_changed_strv(Manager *manager, char **properties) {
+ assert(manager);
+ assert(properties);
+
+ if (!manager->bus)
+ return 0;
+
+ return sd_bus_emit_properties_changed_strv(
+ manager->bus,
+ "/org/freedesktop/network1",
+ "org.freedesktop.network1.Manager",
+ properties);
+}
diff --git a/src/network/networkd-manager-bus.h b/src/network/networkd-manager-bus.h
new file mode 100644
index 0000000..08ddfbd
--- /dev/null
+++ b/src/network/networkd-manager-bus.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+typedef struct Manager Manager;
+
+extern const sd_bus_vtable manager_vtable[];
+
+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..19c3cc6
--- /dev/null
+++ b/src/network/networkd-manager.c
@@ -0,0 +1,1254 @@
+/* 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 "sd-daemon.h"
+#include "sd-netlink.h"
+
+#include "alloc-util.h"
+#include "bus-log-control-api.h"
+#include "bus-polkit.h"
+#include "bus-util.h"
+#include "conf-parser.h"
+#include "def.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "dns-domain.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "local-addresses.h"
+#include "netlink-util.h"
+#include "network-internal.h"
+#include "networkd-address-pool.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-routing-policy-rule.h"
+#include "networkd-speed-meter.h"
+#include "ordered-set.h"
+#include "path-lookup.h"
+#include "path-util.h"
+#include "selinux-util.h"
+#include "set.h"
+#include "signal-util.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "sysctl-util.h"
+#include "tmpfile-util.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) {
+ r = link_carrier_reset(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not reset carrier: %m");
+ }
+
+ return 0;
+}
+
+static int match_prepare_for_sleep(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) {
+ Manager *m = userdata;
+ int b, r;
+
+ assert(message);
+ assert(m);
+
+ 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 = userdata;
+
+ assert(message);
+ assert(m);
+
+ /* 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->links_requesting_uuid)
+ (void) manager_request_product_uuid(m, NULL);
+
+ return 0;
+}
+
+int manager_connect_bus(Manager *m) {
+ int r;
+
+ assert(m);
+
+ if (m->bus)
+ return 0;
+
+ 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 = sd_bus_add_object_vtable(m->bus, NULL, "/org/freedesktop/network1", "org.freedesktop.network1.Manager", manager_vtable, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add manager object vtable: %m");
+
+ r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/network1/link", "org.freedesktop.network1.Link", link_vtable, link_object_find, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add link object vtable: %m");
+
+ r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/network1/link", "org.freedesktop.network1.DHCPServer", dhcp_server_vtable, link_object_find, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add link object vtable: %m");
+
+ r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/network1/link", link_node_enumerator, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add link enumerator: %m");
+
+ r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/network1/network", "org.freedesktop.network1.Network", network_vtable, network_object_find, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add network object vtable: %m");
+
+ r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/network1/network", network_node_enumerator, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add network enumerator: %m");
+
+ 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_udev_process_link(sd_device_monitor *monitor, sd_device *device, void *userdata) {
+ Manager *m = userdata;
+ DeviceAction action;
+ Link *link = NULL;
+ int r, ifindex;
+
+ assert(m);
+ assert(device);
+
+ r = device_get_action(device, &action);
+ if (r < 0) {
+ log_device_debug_errno(device, r, "Failed to get udev action, ignoring device: %m");
+ return 0;
+ }
+
+ /* Ignore the "remove" uevent — let's remove a device only if rtnetlink says so. All other uevents
+ * are "positive" events in some form, i.e. inform us about a changed or new network interface, that
+ * still exists — and we are interested in that. */
+ if (action == DEVICE_ACTION_REMOVE)
+ return 0;
+
+ r = sd_device_get_ifindex(device, &ifindex);
+ if (r < 0) {
+ log_device_debug_errno(device, r, "Ignoring udev %s event for device without ifindex or with invalid ifindex: %m",
+ device_action_to_string(action));
+ return 0;
+ }
+
+ r = device_is_renaming(device);
+ if (r < 0) {
+ log_device_error_errno(device, r, "Failed to determine the device is renamed or not, ignoring '%s' uevent: %m",
+ device_action_to_string(action));
+ return 0;
+ }
+ if (r > 0) {
+ log_device_debug(device, "Interface is under renaming, wait for the interface to be renamed.");
+ return 0;
+ }
+
+ r = link_get(m, ifindex, &link);
+ if (r < 0) {
+ if (r != -ENODEV)
+ log_debug_errno(r, "Failed to get link from ifindex %i, ignoring: %m", ifindex);
+ return 0;
+ }
+
+ (void) link_initialized(link, device);
+
+ 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 (path_is_read_only_fs("/sys") > 0)
+ 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_set_receive_buffer_size(m->device_monitor, RCVBUF_SIZE);
+ if (r < 0)
+ log_warning_errno(r, "Failed to increase buffer size for device monitor, ignoring: %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: %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_udev_process_link, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+
+ return 0;
+}
+
+static int manager_rtnl_process_link(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ Link *link = NULL;
+ NetDev *netdev = NULL;
+ uint16_t type;
+ const char *name;
+ int r, ifindex;
+
+ 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: 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(m, ifindex, &link);
+ (void) netdev_get(m, name, &netdev);
+
+ switch (type) {
+ case RTM_NEWLINK:
+ if (!link) {
+ /* link is new, so add it */
+ r = link_add(m, message, &link);
+ if (r < 0) {
+ log_warning_errno(r, "Could not process new link message, ignoring: %m");
+ return 0;
+ }
+ }
+
+ if (netdev) {
+ /* netdev exists, so make sure the ifindex matches */
+ r = netdev_set_ifindex(netdev, message);
+ if (r < 0) {
+ log_warning_errno(r, "Could not process new link message for netdev, ignoring: %m");
+ return 0;
+ }
+ }
+
+ r = link_update(link, message);
+ if (r < 0) {
+ log_warning_errno(r, "Could not process link message, ignoring: %m");
+ return 0;
+ }
+
+ break;
+
+ case RTM_DELLINK:
+ link_drop(link);
+ netdev_drop(netdev);
+
+ break;
+
+ default:
+ assert_not_reached("Received link message with invalid RTNL message type.");
+ }
+
+ return 1;
+}
+
+static int systemd_netlink_fd(void) {
+ int n, fd, rtnl_fd = -EINVAL;
+
+ n = sd_listen_fds(true);
+ if (n <= 0)
+ return -EINVAL;
+
+ for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + n; fd ++) {
+ if (sd_is_socket(fd, AF_NETLINK, SOCK_RAW, -1) > 0) {
+ if (rtnl_fd >= 0)
+ return -EINVAL;
+
+ rtnl_fd = fd;
+ }
+ }
+
+ return rtnl_fd;
+}
+
+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_inc_rcvbuf(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;
+
+ return 0;
+}
+
+static int manager_connect_rtnl(Manager *m) {
+ int fd, r;
+
+ assert(m);
+
+ fd = systemd_netlink_fd();
+ if (fd < 0)
+ r = sd_netlink_open(&m->rtnl);
+ else
+ r = sd_netlink_open_fd(&m->rtnl, fd);
+ if (r < 0)
+ return r;
+
+ /* 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_inc_rcvbuf(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_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 0;
+}
+
+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;
+ unsigned i;
+
+ assert(s);
+ assert(dns || n == 0);
+
+ for (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) {
+ char *p;
+ 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_consume(s, 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;
+ size_t i;
+
+ assert(s);
+ assert(n == 0 || addresses);
+
+ for (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;
+}
+
+static int manager_save(Manager *m) {
+ _cleanup_ordered_set_free_free_ OrderedSet *dns = NULL, *ntp = NULL, *sip = NULL, *search_domains = NULL, *route_domains = NULL;
+ const char *operstate_str, *carrier_state_str, *address_state_str;
+ LinkOperationalState operstate = LINK_OPERSTATE_OFF;
+ LinkCarrierState carrier_state = LINK_CARRIER_STATE_OFF;
+ LinkAddressState address_state = LINK_ADDRESS_STATE_OFF;
+ _cleanup_free_ char *temp_path = NULL;
+ _cleanup_strv_free_ char **p = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ Link *link;
+ int r;
+
+ assert(m);
+ assert(m->state_file);
+
+ /* We add all NTP and DNS server to a set, to filter out duplicates */
+ dns = ordered_set_new(&string_hash_ops);
+ if (!dns)
+ return -ENOMEM;
+
+ ntp = ordered_set_new(&string_hash_ops);
+ if (!ntp)
+ return -ENOMEM;
+
+ sip = ordered_set_new(&string_hash_ops);
+ if (!sip)
+ return -ENOMEM;
+
+ search_domains = ordered_set_new(&dns_name_hash_ops);
+ if (!search_domains)
+ return -ENOMEM;
+
+ route_domains = ordered_set_new(&dns_name_hash_ops);
+ if (!route_domains)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(link, m->links) {
+ const struct in_addr *addresses;
+
+ if (link->flags & IFF_LOOPBACK)
+ continue;
+
+ if (link->operstate > operstate)
+ operstate = link->operstate;
+
+ if (link->carrier_state > carrier_state)
+ carrier_state = link->carrier_state;
+
+ if (link->address_state > address_state)
+ address_state = link->address_state;
+
+ if (!link->network)
+ continue;
+
+ /* First add the static configured entries */
+ if (link->n_dns != (unsigned) -1)
+ 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) {
+ const char *domainname;
+ char **domains = NULL;
+
+ OrderedSet *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;
+
+ 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);
+
+ 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",
+ operstate_str, carrier_state_str, address_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 = routing_policy_serialize_rules(m->rules, f);
+ if (r < 0)
+ goto fail;
+
+ r = fflush_and_check(f);
+ if (r < 0)
+ goto fail;
+
+ if (rename(temp_path, m->state_file) < 0) {
+ r = -errno;
+ goto fail;
+ }
+
+ 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 (p) {
+ r = manager_send_changed_strv(m, p);
+ if (r < 0)
+ log_error_errno(r, "Could not emit changed properties: %m");
+ }
+
+ m->dirty = false;
+
+ return 0;
+
+fail:
+ (void) unlink(m->state_file);
+ (void) unlink(temp_path);
+
+ return log_error_errno(r, "Failed to save network state to %s: %m", m->state_file);
+}
+
+static int manager_dirty_handler(sd_event_source *s, void *userdata) {
+ Manager *m = userdata;
+ Link *link;
+
+ assert(m);
+
+ if (m->dirty)
+ manager_save(m);
+
+ SET_FOREACH(link, m->dirty_links)
+ (void) link_save_and_clean(link);
+
+ return 1;
+}
+
+static int signal_terminate_callback(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *m = userdata;
+
+ assert(m);
+ 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 = userdata;
+
+ assert(m);
+ m->restarting = true;
+
+ log_debug("Restart operation initiated.");
+
+ return sd_event_exit(sd_event_source_get_event(s), 0);
+}
+
+int manager_new(Manager **ret) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ int r;
+
+ m = new(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ *m = (Manager) {
+ .speed_meter_interval_usec = SPEED_METER_DEFAULT_TIME_INTERVAL,
+ .manage_foreign_routes = true,
+ .ethtool_fd = -1,
+ };
+
+ m->state_file = strdup("/run/systemd/netif/state");
+ if (!m->state_file)
+ return -ENOMEM;
+
+ 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 = manager_connect_rtnl(m);
+ if (r < 0)
+ return r;
+
+ r = manager_connect_genl(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;
+
+ m->duid.type = DUID_TYPE_EN;
+
+ (void) routing_policy_load_rules(m->state_file, &m->rules_saved);
+
+ *ret = TAKE_PTR(m);
+
+ return 0;
+}
+
+void manager_free(Manager *m) {
+ Link *link;
+
+ if (!m)
+ return;
+
+ free(m->state_file);
+
+ HASHMAP_FOREACH(link, m->links)
+ (void) link_stop_engines(link, true);
+
+ m->dhcp6_prefixes = hashmap_free_with_destructor(m->dhcp6_prefixes, dhcp6_pd_free);
+ m->dhcp6_pd_prefixes = set_free_with_destructor(m->dhcp6_pd_prefixes, dhcp6_pd_free);
+
+ m->dirty_links = set_free_with_destructor(m->dirty_links, link_unref);
+ m->links_requesting_uuid = set_free_with_destructor(m->links_requesting_uuid, link_unref);
+ m->links = hashmap_free_with_destructor(m->links, link_unref);
+
+ m->duids_requesting_uuid = set_free(m->duids_requesting_uuid);
+ m->networks = ordered_hashmap_free_with_destructor(m->networks, network_unref);
+
+ m->netdevs = hashmap_free_with_destructor(m->netdevs, netdev_unref);
+
+ ordered_set_free_free(m->address_pools);
+
+ /* routing_policy_rule_free() access m->rules and m->rules_foreign.
+ * So, it is necessary to set NULL after the sets are freed. */
+ m->rules = set_free(m->rules);
+ m->rules_foreign = set_free(m->rules_foreign);
+ set_free(m->rules_saved);
+
+ 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->routes_foreign = set_free(m->routes_foreign);
+
+ 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);
+
+ free(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. */
+
+ manager_save(m);
+
+ HASHMAP_FOREACH(link, m->links)
+ (void) link_save(link);
+
+ return 0;
+}
+
+int manager_load_config(Manager *m) {
+ int r;
+
+ /* update timestamp */
+ paths_check_timestamp(NETWORK_DIRS, &m->network_dirs_ts_usec, true);
+
+ r = netdev_load(m, false);
+ if (r < 0)
+ return r;
+
+ r = network_load(m, &m->networks);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+bool manager_should_reload(Manager *m) {
+ return paths_check_timestamp(NETWORK_DIRS, &m->network_dirs_ts_usec, false);
+}
+
+static int manager_enumerate_internal(
+ Manager *m,
+ sd_netlink_message *req,
+ int (*process)(sd_netlink *, sd_netlink_message *, Manager *),
+ const char *name) {
+
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *reply = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+ assert(req);
+ assert(process);
+
+ r = sd_netlink_message_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(m->rtnl, req, 0, &reply);
+ if (r < 0) {
+ if (name && (r == -EOPNOTSUPP || (r == -EINVAL && mac_selinux_enforcing()))) {
+ log_debug_errno(r, "%s are not supported by the kernel. Ignoring.", name);
+ return 0;
+ }
+
+ return r;
+ }
+
+ for (sd_netlink_message *reply_one = reply; reply_one; reply_one = sd_netlink_message_next(reply_one)) {
+ int k;
+
+ m->enumerating = true;
+
+ k = process(m->rtnl, 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, req, manager_rtnl_process_link, NULL);
+}
+
+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, req, manager_rtnl_process_address, NULL);
+}
+
+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, req, manager_rtnl_process_neighbor, NULL);
+}
+
+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, req, manager_rtnl_process_route, NULL);
+}
+
+static int manager_enumerate_rules(Manager *m) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(m);
+ assert(m->rtnl);
+
+ r = sd_rtnl_message_new_routing_policy_rule(m->rtnl, &req, RTM_GETRULE, 0);
+ if (r < 0)
+ return r;
+
+ return manager_enumerate_internal(m, req, manager_rtnl_process_rule, "Routing policy rules");
+}
+
+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, req, manager_rtnl_process_nexthop, "Nexthop rules");
+}
+
+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_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");
+
+ r = manager_enumerate_routes(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not enumerate routes: %m");
+
+ r = manager_enumerate_rules(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not enumerate routing policy rules: %m");
+
+ r = manager_enumerate_nexthop(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not enumerate nexthop rules: %m");
+
+ return 0;
+}
+
+Link* manager_find_uplink(Manager *m, Link *exclude) {
+ _cleanup_free_ struct local_address *gateways = NULL;
+ int n, i;
+
+ assert(m);
+
+ /* Looks for a suitable "uplink", via black magic: an
+ * interface that is up and where the default route with the
+ * highest priority points to. */
+
+ n = local_gateways(m->rtnl, 0, AF_UNSPEC, &gateways);
+ if (n < 0) {
+ log_warning_errno(n, "Failed to determine list of default gateways: %m");
+ return NULL;
+ }
+
+ for (i = 0; i < n; i++) {
+ Link *link;
+
+ link = hashmap_get(m->links, INT_TO_PTR(gateways[i].ifindex));
+ if (!link) {
+ log_debug("Weird, found a gateway for a link we don't know. Ignoring.");
+ continue;
+ }
+
+ if (link == exclude)
+ continue;
+
+ if (link->operstate < LINK_OPERSTATE_ROUTABLE)
+ continue;
+
+ return link;
+ }
+
+ return NULL;
+}
+
+void manager_dirty(Manager *manager) {
+ assert(manager);
+
+ /* the serialized state in /run is no longer up-to-date */
+ manager->dirty = true;
+}
+
+static int set_hostname_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
+ _unused_ Manager *manager = userdata;
+ const sd_bus_error *e;
+
+ assert(m);
+ assert(manager);
+
+ e = sd_bus_message_get_error(m);
+ if (e)
+ log_warning_errno(sd_bus_error_get_errno(e), "Could not set hostname: %s", e->message);
+
+ return 1;
+}
+
+int manager_set_hostname(Manager *m, const char *hostname) {
+ int r;
+
+ log_debug("Setting transient hostname: '%s'", strna(hostname));
+
+ if (free_and_strdup(&m->dynamic_hostname, hostname) < 0)
+ return log_oom();
+
+ if (!m->bus || sd_bus_is_ready(m->bus) <= 0) {
+ log_debug("Not connected to system bus, setting hostname later.");
+ return 0;
+ }
+
+ r = sd_bus_call_method_async(
+ m->bus,
+ NULL,
+ "org.freedesktop.hostname1",
+ "/org/freedesktop/hostname1",
+ "org.freedesktop.hostname1",
+ "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) {
+ _unused_ Manager *manager = userdata;
+ const sd_bus_error *e;
+
+ assert(m);
+ assert(manager);
+
+ e = sd_bus_message_get_error(m);
+ if (e)
+ log_warning_errno(sd_bus_error_get_errno(e), "Could not set timezone: %s", e->message);
+
+ return 1;
+}
+
+int manager_set_timezone(Manager *m, const char *tz) {
+ int r;
+
+ assert(m);
+ assert(tz);
+
+ log_debug("Setting system timezone: '%s'", tz);
+ if (free_and_strdup(&m->dynamic_timezone, tz) < 0)
+ return log_oom();
+
+ if (!m->bus || sd_bus_is_ready(m->bus) <= 0) {
+ log_debug("Not connected to system bus, setting timezone later.");
+ return 0;
+ }
+
+ r = sd_bus_call_method_async(
+ m->bus,
+ NULL,
+ "org.freedesktop.timedate1",
+ "/org/freedesktop/timedate1",
+ "org.freedesktop.timedate1",
+ "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..b67116b
--- /dev/null
+++ b/src/network/networkd-manager.h
@@ -0,0 +1,97 @@
+/* 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 "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;
+
+ bool enumerating:1;
+ bool dirty:1;
+ bool restarting:1;
+ bool manage_foreign_routes;
+
+ Set *dirty_links;
+
+ char *state_file;
+ LinkOperationalState operational_state;
+ LinkCarrierState carrier_state;
+ LinkAddressState address_state;
+
+ Hashmap *links;
+ Hashmap *netdevs;
+ OrderedHashmap *networks;
+ Hashmap *dhcp6_prefixes;
+ Set *dhcp6_pd_prefixes;
+ OrderedSet *address_pools;
+
+ usec_t network_dirs_ts_usec;
+
+ DUID duid;
+ sd_id128_t product_uuid;
+ bool has_product_uuid;
+ Set *links_requesting_uuid;
+ Set *duids_requesting_uuid;
+
+ char* dynamic_hostname;
+ char* dynamic_timezone;
+
+ Set *rules;
+ Set *rules_foreign;
+ Set *rules_saved;
+
+ /* Manager stores routes without RTA_OIF attribute. */
+ Set *routes;
+ Set *routes_foreign;
+
+ /* 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 dhcp4_prefix_root_cannot_set_table:1;
+ bool bridge_mdb_on_master_not_supported:1;
+};
+
+int manager_new(Manager **ret);
+void manager_free(Manager *m);
+
+int manager_connect_bus(Manager *m);
+int manager_start(Manager *m);
+
+int manager_load_config(Manager *m);
+bool manager_should_reload(Manager *m);
+
+int manager_enumerate(Manager *m);
+
+void manager_dirty(Manager *m);
+
+Link* manager_find_uplink(Manager *m, Link *exclude);
+
+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-mdb.c b/src/network/networkd-mdb.c
new file mode 100644
index 0000000..0300dce
--- /dev/null
+++ b/src/network/networkd-mdb.c
@@ -0,0 +1,365 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-mdb.h"
+#include "networkd-network.h"
+#include "string-util.h"
+#include "vlan-util.h"
+
+#define STATIC_MDB_ENTRIES_PER_NETWORK_MAX 1024U
+
+/* remove MDB entry. */
+MdbEntry *mdb_entry_free(MdbEntry *mdb_entry) {
+ if (!mdb_entry)
+ return NULL;
+
+ if (mdb_entry->network) {
+ assert(mdb_entry->section);
+ hashmap_remove(mdb_entry->network->mdb_entries_by_section, mdb_entry->section);
+ }
+
+ network_config_section_free(mdb_entry->section);
+
+ return mfree(mdb_entry);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(MdbEntry, mdb_entry_free);
+
+/* create a new MDB entry or get an existing one. */
+static int mdb_entry_new_static(
+ Network *network,
+ const char *filename,
+ unsigned section_line,
+ MdbEntry **ret) {
+
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(mdb_entry_freep) MdbEntry *mdb_entry = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ /* search entry in hashmap first. */
+ mdb_entry = hashmap_get(network->mdb_entries_by_section, n);
+ if (mdb_entry) {
+ *ret = TAKE_PTR(mdb_entry);
+ return 0;
+ }
+
+ if (hashmap_size(network->mdb_entries_by_section) >= STATIC_MDB_ENTRIES_PER_NETWORK_MAX)
+ return -E2BIG;
+
+ /* allocate space for an MDB entry. */
+ mdb_entry = new(MdbEntry, 1);
+ if (!mdb_entry)
+ return -ENOMEM;
+
+ /* init MDB structure. */
+ *mdb_entry = (MdbEntry) {
+ .network = network,
+ .section = TAKE_PTR(n),
+ };
+
+ r = hashmap_ensure_allocated(&network->mdb_entries_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->mdb_entries_by_section, mdb_entry->section, mdb_entry);
+ if (r < 0)
+ return r;
+
+ /* return allocated MDB structure. */
+ *ret = TAKE_PTR(mdb_entry);
+ return 0;
+}
+
+static int set_mdb_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->bridge_mdb_messages > 0);
+
+ link->bridge_mdb_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r == -EINVAL && streq_ptr(link->kind, "bridge") && (!link->network || !link->network->bridge)) {
+ /* 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 configuring 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->bridge_mdb_messages == 0) {
+ link->bridge_mdb_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int link_get_bridge_master_ifindex(Link *link) {
+ assert(link);
+
+ if (link->network && link->network->bridge)
+ return link->network->bridge->ifindex;
+
+ if (streq_ptr(link->kind, "bridge"))
+ return link->ifindex;
+
+ return 0;
+}
+
+/* send a request to the kernel to add an MDB entry */
+static int mdb_entry_configure(Link *link, MdbEntry *mdb_entry) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ struct br_mdb_entry entry;
+ int master, r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(mdb_entry);
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *a = NULL;
+
+ (void) in_addr_to_string(mdb_entry->family, &mdb_entry->group_addr, &a);
+ log_link_debug(link, "Configuring bridge MDB entry: MulticastGroupAddress=%s, VLANId=%u",
+ strna(a), mdb_entry->vlan_id);
+ }
+
+ master = link_get_bridge_master_ifindex(link);
+ if (master <= 0)
+ return log_link_error_errno(link, SYNTHETIC_ERRNO(EINVAL), "Invalid bridge master ifindex %i", master);
+
+ 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 = master == link->ifindex ? MDB_TEMPORARY : MDB_PERMANENT,
+ .ifindex = link->ifindex,
+ .vid = mdb_entry->vlan_id,
+ };
+
+ /* create new RTM message */
+ r = sd_rtnl_message_new_mdb(link->manager->rtnl, &req, RTM_NEWMDB, master);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create RTM_NEWMDB message: %m");
+
+ switch (mdb_entry->family) {
+ case AF_INET:
+ entry.addr.u.ip4 = mdb_entry->group_addr.in.s_addr;
+ entry.addr.proto = htobe16(ETH_P_IP);
+ break;
+
+ case AF_INET6:
+ entry.addr.u.ip6 = mdb_entry->group_addr.in6;
+ entry.addr.proto = htobe16(ETH_P_IPV6);
+ break;
+
+ default:
+ assert_not_reached("Invalid address family");
+ }
+
+ r = sd_netlink_message_append_data(req, MDBA_SET_ENTRY, &entry, sizeof(entry));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append MDBA_SET_ENTRY attribute: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, set_mdb_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);
+
+ return 1;
+}
+
+int link_set_bridge_mdb(Link *link) {
+ MdbEntry *mdb_entry;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ link->bridge_mdb_configured = false;
+
+ if (!link->network)
+ return 0;
+
+ if (hashmap_isempty(link->network->mdb_entries_by_section))
+ goto finish;
+
+ if (!link_has_carrier(link))
+ return log_link_debug(link, "Link does not have carrier yet, setting MDB entries later.");
+
+ if (link->network->bridge) {
+ Link *master;
+
+ r = link_get(link->manager, link->network->bridge->ifindex, &master);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get Link object for Bridge=%s", link->network->bridge->ifname);
+
+ if (!link_has_carrier(master))
+ return log_link_debug(link, "Bridge interface %s does not have carrier yet, setting MDB entries later.", link->network->bridge->ifname);
+
+ } else if (!streq_ptr(link->kind, "bridge")) {
+ log_link_warning(link, "Link is neither a bridge master nor a bridge port, ignoring [BridgeMDB] sections.");
+ goto finish;
+ } else if (link->manager->bridge_mdb_on_master_not_supported) {
+ log_link_debug(link, "Kernel seems not to support configuring bridge MDB entries on bridge master, ignoring [BridgeMDB] sections.");
+ goto finish;
+ }
+
+ HASHMAP_FOREACH(mdb_entry, link->network->mdb_entries_by_section) {
+ r = mdb_entry_configure(link, mdb_entry);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to add MDB entry to multicast group database: %m");
+
+ link->bridge_mdb_messages++;
+ }
+
+finish:
+ if (link->bridge_mdb_messages == 0) {
+ link->bridge_mdb_configured = true;
+ link_check_ready(link);
+ }
+
+ return 0;
+}
+
+static int mdb_entry_verify(MdbEntry *mdb_entry) {
+ if (section_is_invalid(mdb_entry->section))
+ return -EINVAL;
+
+ if (mdb_entry->family == AF_UNSPEC)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: [BridgeMDB] section without MulticastGroupAddress= field configured. "
+ "Ignoring [BridgeMDB] section from line %u.",
+ mdb_entry->section->filename, mdb_entry->section->line);
+
+ if (!in_addr_is_multicast(mdb_entry->family, &mdb_entry->group_addr))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: MulticastGroupAddress= is not a multicast address. "
+ "Ignoring [BridgeMDB] section from line %u.",
+ mdb_entry->section->filename, mdb_entry->section->line);
+
+ if (mdb_entry->family == AF_INET) {
+ if (in4_addr_is_local_multicast(&mdb_entry->group_addr.in))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: MulticastGroupAddress= is a local multicast address. "
+ "Ignoring [BridgeMDB] section from line %u.",
+ mdb_entry->section->filename, mdb_entry->section->line);
+ } else {
+ if (in6_addr_is_link_local_all_nodes(&mdb_entry->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_entry->section->filename, mdb_entry->section->line);
+ }
+
+ return 0;
+}
+
+void network_drop_invalid_mdb_entries(Network *network) {
+ MdbEntry *mdb_entry;
+
+ assert(network);
+
+ HASHMAP_FOREACH(mdb_entry, network->mdb_entries_by_section)
+ if (mdb_entry_verify(mdb_entry) < 0)
+ mdb_entry_free(mdb_entry);
+}
+
+/* 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_(mdb_entry_free_or_set_invalidp) MdbEntry *mdb_entry = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = mdb_entry_new_static(network, filename, section_line, &mdb_entry);
+ if (r < 0)
+ return log_oom();
+
+ r = config_parse_vlanid(unit, filename, line, section,
+ section_line, lvalue, ltype,
+ rvalue, &mdb_entry->vlan_id, userdata);
+ if (r < 0)
+ return r;
+
+ mdb_entry = NULL;
+
+ 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_(mdb_entry_free_or_set_invalidp) MdbEntry *mdb_entry = NULL;
+ Network *network = userdata;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = mdb_entry_new_static(network, filename, section_line, &mdb_entry);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_from_string_auto(rvalue, &mdb_entry->family, &mdb_entry->group_addr);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Cannot parse multicast group address: %m");
+ return 0;
+ }
+
+ mdb_entry = NULL;
+
+ return 0;
+}
diff --git a/src/network/networkd-mdb.h b/src/network/networkd-mdb.h
new file mode 100644
index 0000000..ea88412
--- /dev/null
+++ b/src/network/networkd-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 Network Network;
+typedef struct Link Link;
+
+typedef struct MdbEntry {
+ Network *network;
+ NetworkConfigSection *section;
+
+ int family;
+ union in_addr_union group_addr;
+ uint16_t vlan_id;
+} MdbEntry;
+
+MdbEntry *mdb_entry_free(MdbEntry *mdb_entry);
+
+void network_drop_invalid_mdb_entries(Network *network);
+
+int link_set_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-ndisc.c b/src/network/networkd-ndisc.c
new file mode 100644
index 0000000..d2aa3db
--- /dev/null
+++ b/src/network/networkd-ndisc.c
@@ -0,0 +1,1516 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2014 Intel Corporation. All rights reserved.
+***/
+
+#include <arpa/inet.h>
+#include <netinet/icmp6.h>
+#include <net/if_arp.h>
+#include <linux/if.h>
+
+#include "sd-ndisc.h"
+
+#include "missing_network.h"
+#include "networkd-address.h"
+#include "networkd-dhcp6.h"
+#include "networkd-manager.h"
+#include "networkd-ndisc.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+#define NDISC_DNSSL_MAX 64U
+#define NDISC_RDNSS_MAX 64U
+#define NDISC_PREFIX_LFT_MIN 7200U
+
+#define DAD_CONFLICTS_IDGEN_RETRIES_RFC7217 3
+
+/* https://tools.ietf.org/html/rfc5453 */
+/* https://www.iana.org/assignments/ipv6-interface-ids/ipv6-interface-ids.xml */
+
+#define SUBNET_ROUTER_ANYCAST_ADDRESS_RFC4291 ((struct in6_addr) { .s6_addr = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } })
+#define SUBNET_ROUTER_ANYCAST_PREFIXLEN 8
+#define RESERVED_IPV6_INTERFACE_IDENTIFIERS_ADDRESS_RFC4291 ((struct in6_addr) { .s6_addr = { 0x02, 0x00, 0x5E, 0xFF, 0xFE } })
+#define RESERVED_IPV6_INTERFACE_IDENTIFIERS_PREFIXLEN 5
+#define RESERVED_SUBNET_ANYCAST_ADDRESSES_RFC4291 ((struct in6_addr) { .s6_addr = { 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } })
+#define RESERVED_SUBNET_ANYCAST_PREFIXLEN 7
+
+#define NDISC_APP_ID SD_ID128_MAKE(13,ac,81,a7,d5,3f,49,78,92,79,5d,0c,29,3a,bc,7e)
+
+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->network)
+ return false;
+
+ if (!link_ipv6ll_enabled(link))
+ 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);
+}
+
+static int ndisc_remove_old_one(Link *link, const struct in6_addr *router, bool force);
+
+static int ndisc_address_callback(Address *address) {
+ struct in6_addr router = {};
+ NDiscAddress *n;
+
+ assert(address);
+ assert(address->link);
+
+ SET_FOREACH(n, address->link->ndisc_addresses)
+ if (n->address == address) {
+ router = n->router;
+ break;
+ }
+
+ if (IN6_IS_ADDR_UNSPECIFIED(&router)) {
+ _cleanup_free_ char *buf = NULL;
+
+ (void) in_addr_to_string(address->family, &address->in_addr, &buf);
+ log_link_debug(address->link, "%s is called for %s/%u, but it is already removed, ignoring.",
+ __func__, strna(buf), address->prefixlen);
+ return 0;
+ }
+
+ /* Make this called only once */
+ SET_FOREACH(n, address->link->ndisc_addresses)
+ if (IN6_ARE_ADDR_EQUAL(&n->router, &router))
+ n->address->callback = NULL;
+
+ return ndisc_remove_old_one(address->link, &router, true);
+}
+
+static int ndisc_remove_old_one(Link *link, const struct in6_addr *router, bool force) {
+ NDiscAddress *na;
+ NDiscRoute *nr;
+ NDiscDNSSL *dnssl;
+ NDiscRDNSS *rdnss;
+ int k, r = 0;
+
+ assert(link);
+ assert(router);
+
+ if (!force) {
+ bool set_callback = false;
+
+ if (!link->ndisc_addresses_configured || !link->ndisc_routes_configured)
+ return 0;
+
+ SET_FOREACH(na, link->ndisc_addresses)
+ if (!na->marked && IN6_ARE_ADDR_EQUAL(&na->router, router)) {
+ set_callback = true;
+ break;
+ }
+
+ if (set_callback)
+ SET_FOREACH(na, link->ndisc_addresses)
+ if (!na->marked && address_is_ready(na->address)) {
+ set_callback = false;
+ break;
+ }
+
+ if (set_callback) {
+ SET_FOREACH(na, link->ndisc_addresses)
+ if (!na->marked && IN6_ARE_ADDR_EQUAL(&na->router, router))
+ na->address->callback = ndisc_address_callback;
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *buf = NULL;
+
+ (void) in_addr_to_string(AF_INET6, (union in_addr_union *) router, &buf);
+ log_link_debug(link, "No SLAAC address obtained from %s is ready. "
+ "The old NDisc information will be removed later.",
+ strna(buf));
+ }
+ return 0;
+ }
+ }
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *buf = NULL;
+
+ (void) in_addr_to_string(AF_INET6, (union in_addr_union *) router, &buf);
+ log_link_debug(link, "Removing old NDisc information obtained from %s.", strna(buf));
+ }
+
+ link_dirty(link);
+
+ SET_FOREACH(na, link->ndisc_addresses)
+ if (na->marked && IN6_ARE_ADDR_EQUAL(&na->router, router)) {
+ k = address_remove(na->address, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ SET_FOREACH(nr, link->ndisc_routes)
+ if (nr->marked && IN6_ARE_ADDR_EQUAL(&nr->router, router)) {
+ k = route_remove(nr->route, NULL, link, NULL);
+ if (k < 0)
+ r = k;
+ }
+
+ SET_FOREACH(rdnss, link->ndisc_rdnss)
+ if (rdnss->marked && IN6_ARE_ADDR_EQUAL(&rdnss->router, router))
+ free(set_remove(link->ndisc_rdnss, rdnss));
+
+ SET_FOREACH(dnssl, link->ndisc_dnssl)
+ if (dnssl->marked && IN6_ARE_ADDR_EQUAL(&dnssl->router, router))
+ free(set_remove(link->ndisc_dnssl, dnssl));
+
+ return r;
+}
+
+static int ndisc_remove_old(Link *link) {
+ _cleanup_set_free_free_ Set *routers = NULL;
+ _cleanup_free_ struct in6_addr *router = NULL;
+ struct in6_addr *a;
+ NDiscAddress *na;
+ NDiscRoute *nr;
+ NDiscDNSSL *dnssl;
+ NDiscRDNSS *rdnss;
+ int k, r;
+
+ assert(link);
+
+ routers = set_new(&in6_addr_hash_ops);
+ if (!routers)
+ return -ENOMEM;
+
+ SET_FOREACH(na, link->ndisc_addresses)
+ if (!set_contains(routers, &na->router)) {
+ router = newdup(struct in6_addr, &na->router, 1);
+ if (!router)
+ return -ENOMEM;
+
+ r = set_put(routers, router);
+ if (r < 0)
+ return r;
+
+ assert(r > 0);
+ TAKE_PTR(router);
+ }
+
+ SET_FOREACH(nr, link->ndisc_routes)
+ if (!set_contains(routers, &nr->router)) {
+ router = newdup(struct in6_addr, &nr->router, 1);
+ if (!router)
+ return -ENOMEM;
+
+ r = set_put(routers, router);
+ if (r < 0)
+ return r;
+
+ assert(r > 0);
+ TAKE_PTR(router);
+ }
+
+ SET_FOREACH(rdnss, link->ndisc_rdnss)
+ if (!set_contains(routers, &rdnss->router)) {
+ router = newdup(struct in6_addr, &rdnss->router, 1);
+ if (!router)
+ return -ENOMEM;
+
+ r = set_put(routers, router);
+ if (r < 0)
+ return r;
+
+ assert(r > 0);
+ TAKE_PTR(router);
+ }
+
+ SET_FOREACH(dnssl, link->ndisc_dnssl)
+ if (!set_contains(routers, &dnssl->router)) {
+ router = newdup(struct in6_addr, &dnssl->router, 1);
+ if (!router)
+ return -ENOMEM;
+
+ r = set_put(routers, router);
+ if (r < 0)
+ return r;
+
+ assert(r > 0);
+ TAKE_PTR(router);
+ }
+
+ r = 0;
+ SET_FOREACH(a, routers) {
+ k = ndisc_remove_old_one(link, a, false);
+ if (k < 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static void ndisc_route_hash_func(const NDiscRoute *x, struct siphash *state) {
+ route_hash_func(x->route, state);
+}
+
+static int ndisc_route_compare_func(const NDiscRoute *a, const NDiscRoute *b) {
+ return route_compare_func(a->route, b->route);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ ndisc_route_hash_ops,
+ NDiscRoute,
+ ndisc_route_hash_func,
+ ndisc_route_compare_func,
+ free);
+
+static int ndisc_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->ndisc_routes_messages > 0);
+
+ link->ndisc_routes_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_error_errno(link, m, r, "Could not set NDisc route");
+ link_enter_failed(link);
+ return 1;
+ }
+
+ if (link->ndisc_routes_messages == 0) {
+ log_link_debug(link, "NDisc routes set.");
+ link->ndisc_routes_configured = true;
+
+ r = ndisc_remove_old(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int ndisc_route_configure(Route *route, Link *link, sd_ndisc_router *rt) {
+ _cleanup_free_ NDiscRoute *nr = NULL;
+ NDiscRoute *nr_exist;
+ struct in6_addr router;
+ Route *ret;
+ int r;
+
+ assert(route);
+ assert(link);
+ assert(rt);
+
+ r = route_configure(route, link, ndisc_route_handler, &ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set NDisc route: %m");
+
+ link->ndisc_routes_messages++;
+
+ 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");
+
+ nr = new(NDiscRoute, 1);
+ if (!nr)
+ return log_oom();
+
+ *nr = (NDiscRoute) {
+ .router = router,
+ .route = ret,
+ };
+
+ nr_exist = set_get(link->ndisc_routes, nr);
+ if (nr_exist) {
+ nr_exist->marked = false;
+ nr_exist->router = router;
+ return 0;
+ }
+
+ r = set_ensure_put(&link->ndisc_routes, &ndisc_route_hash_ops, nr);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store NDisc SLAAC route: %m");
+ assert(r > 0);
+ TAKE_PTR(nr);
+
+ return 0;
+}
+
+static void ndisc_address_hash_func(const NDiscAddress *x, struct siphash *state) {
+ address_hash_func(x->address, state);
+}
+
+static int ndisc_address_compare_func(const NDiscAddress *a, const NDiscAddress *b) {
+ return address_compare_func(a->address, b->address);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ ndisc_address_hash_ops,
+ NDiscAddress,
+ ndisc_address_hash_func,
+ ndisc_address_compare_func,
+ free);
+
+static int ndisc_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->ndisc_addresses_messages > 0);
+
+ link->ndisc_addresses_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST) {
+ log_link_message_error_errno(link, m, r, "Could not set NDisc address");
+ link_enter_failed(link);
+ return 1;
+ } else if (r >= 0)
+ (void) manager_rtnl_process_address(rtnl, m, link->manager);
+
+ if (link->ndisc_addresses_messages == 0) {
+ log_link_debug(link, "NDisc SLAAC addresses set.");
+ link->ndisc_addresses_configured = true;
+
+ r = ndisc_remove_old(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+
+ r = link_set_routes(link);
+ if (r < 0) {
+ link_enter_failed(link);
+ return 1;
+ }
+ }
+
+ return 1;
+}
+
+static int ndisc_address_configure(Address *address, Link *link, sd_ndisc_router *rt) {
+ _cleanup_free_ NDiscAddress *na = NULL;
+ NDiscAddress *na_exist;
+ struct in6_addr router;
+ Address *ret;
+ int r;
+
+ assert(address);
+ assert(link);
+ assert(rt);
+
+ r = address_configure(address, link, ndisc_address_handler, true, &ret);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to set NDisc SLAAC address: %m");
+
+ link->ndisc_addresses_messages++;
+
+ 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");
+
+ na = new(NDiscAddress, 1);
+ if (!na)
+ return log_oom();
+
+ *na = (NDiscAddress) {
+ .router = router,
+ .address = ret,
+ };
+
+ na_exist = set_get(link->ndisc_addresses, na);
+ if (na_exist) {
+ na_exist->marked = false;
+ na_exist->router = router;
+ return 0;
+ }
+
+ r = set_ensure_put(&link->ndisc_addresses, &ndisc_address_hash_ops, na);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store NDisc SLAAC address: %m");
+ assert(r > 0);
+ TAKE_PTR(na);
+
+ return 0;
+}
+
+static int ndisc_router_process_default(Link *link, sd_ndisc_router *rt) {
+ _cleanup_(route_freep) Route *route = NULL;
+ union in_addr_union gateway;
+ uint16_t lifetime;
+ unsigned preference;
+ uint32_t table, mtu;
+ usec_t time_now;
+ int r;
+
+ assert(link);
+ assert(rt);
+
+ r = sd_ndisc_router_get_lifetime(rt, &lifetime);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get gateway lifetime from RA: %m");
+
+ if (lifetime == 0) /* not a default router */
+ return 0;
+
+ r = sd_ndisc_router_get_address(rt, &gateway.in6);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get gateway address from RA: %m");
+
+ if (address_exists(link, AF_INET6, &gateway)) {
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *buffer = NULL;
+
+ (void) in_addr_to_string(AF_INET6, &gateway, &buffer);
+ log_link_debug(link, "No NDisc route added, gateway %s matches local address",
+ strnull(buffer));
+ }
+ 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");
+
+ r = sd_ndisc_router_get_timestamp(rt, clock_boottime_or_monotonic(), &time_now);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %m");
+
+ r = sd_ndisc_router_get_mtu(rt, &mtu);
+ if (r == -ENODATA)
+ mtu = 0;
+ else if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get default router MTU from RA: %m");
+
+ table = link_get_ipv6_accept_ra_route_table(link);
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->family = AF_INET6;
+ route->table = table;
+ route->priority = link->network->dhcp6_route_metric;
+ route->protocol = RTPROT_RA;
+ route->pref = preference;
+ route->gw_family = AF_INET6;
+ route->gw = gateway;
+ route->lifetime = time_now + lifetime * USEC_PER_SEC;
+ route->mtu = mtu;
+
+ r = ndisc_route_configure(route, link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set default route: %m");
+
+ Route *route_gw;
+ HASHMAP_FOREACH(route_gw, link->network->routes_by_section) {
+ if (!route_gw->gateway_from_dhcp_or_ra)
+ continue;
+
+ if (route_gw->gw_family != AF_INET6)
+ continue;
+
+ route_gw->gw = gateway;
+ if (!route_gw->table_set)
+ route_gw->table = table;
+ if (!route_gw->priority_set)
+ route_gw->priority = link->network->dhcp6_route_metric;
+ if (!route_gw->protocol_set)
+ route_gw->protocol = RTPROT_RA;
+ if (!route_gw->pref_set)
+ route->pref = preference;
+ route_gw->lifetime = time_now + lifetime * USEC_PER_SEC;
+ if (route_gw->mtu == 0)
+ route_gw->mtu = mtu;
+
+ r = ndisc_route_configure(route_gw, link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set gateway: %m");
+ }
+
+ return 0;
+}
+
+static bool stableprivate_address_is_valid(const struct in6_addr *addr) {
+ assert(addr);
+
+ /* According to rfc4291, generated address should not be in the following ranges. */
+
+ if (memcmp(addr, &SUBNET_ROUTER_ANYCAST_ADDRESS_RFC4291, SUBNET_ROUTER_ANYCAST_PREFIXLEN) == 0)
+ return false;
+
+ if (memcmp(addr, &RESERVED_IPV6_INTERFACE_IDENTIFIERS_ADDRESS_RFC4291, RESERVED_IPV6_INTERFACE_IDENTIFIERS_PREFIXLEN) == 0)
+ return false;
+
+ if (memcmp(addr, &RESERVED_SUBNET_ANYCAST_ADDRESSES_RFC4291, RESERVED_SUBNET_ANYCAST_PREFIXLEN) == 0)
+ return false;
+
+ return true;
+}
+
+static int make_stableprivate_address(Link *link, const struct in6_addr *prefix, uint8_t prefix_len, uint8_t dad_counter, struct in6_addr **ret) {
+ _cleanup_free_ struct in6_addr *addr = NULL;
+ sd_id128_t secret_key;
+ struct siphash state;
+ uint64_t rid;
+ size_t l;
+ int r;
+
+ /* According to rfc7217 section 5.1
+ * RID = F(Prefix, Net_Iface, Network_ID, DAD_Counter, secret_key) */
+
+ r = sd_id128_get_machine_app_specific(NDISC_APP_ID, &secret_key);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate key: %m");
+
+ siphash24_init(&state, secret_key.bytes);
+
+ l = MAX(DIV_ROUND_UP(prefix_len, 8), 8);
+ siphash24_compress(prefix, l, &state);
+ siphash24_compress_string(link->ifname, &state);
+ /* Only last 8 bytes of IB MAC are stable */
+ if (link->iftype == ARPHRD_INFINIBAND)
+ siphash24_compress(&link->hw_addr.addr.infiniband[12], 8, &state);
+ else
+ siphash24_compress(link->hw_addr.addr.bytes, link->hw_addr.length, &state);
+ siphash24_compress(&dad_counter, sizeof(uint8_t), &state);
+
+ rid = htole64(siphash24_finalize(&state));
+
+ addr = new(struct in6_addr, 1);
+ if (!addr)
+ return log_oom();
+
+ memcpy(addr->s6_addr, prefix->s6_addr, l);
+ memcpy(addr->s6_addr + l, &rid, 16 - l);
+
+ if (!stableprivate_address_is_valid(addr)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ *ret = TAKE_PTR(addr);
+ return 1;
+}
+
+static int ndisc_router_generate_addresses(Link *link, struct in6_addr *address, uint8_t prefixlen, Set **ret) {
+ _cleanup_set_free_free_ Set *addresses = NULL;
+ IPv6Token *j;
+ int r;
+
+ assert(link);
+ assert(address);
+ assert(ret);
+
+ addresses = set_new(&in6_addr_hash_ops);
+ if (!addresses)
+ return log_oom();
+
+ ORDERED_SET_FOREACH(j, link->network->ipv6_tokens) {
+ _cleanup_free_ struct in6_addr *new_address = NULL;
+
+ if (j->address_generation_type == IPV6_TOKEN_ADDRESS_GENERATION_PREFIXSTABLE
+ && (IN6_IS_ADDR_UNSPECIFIED(&j->prefix) || IN6_ARE_ADDR_EQUAL(&j->prefix, address))) {
+ /* 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 (; j->dad_counter < DAD_CONFLICTS_IDGEN_RETRIES_RFC7217; j->dad_counter++) {
+ r = make_stableprivate_address(link, address, prefixlen, j->dad_counter, &new_address);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ break;
+ }
+ } else if (j->address_generation_type == IPV6_TOKEN_ADDRESS_GENERATION_STATIC) {
+ new_address = new(struct in6_addr, 1);
+ if (!new_address)
+ return log_oom();
+
+ memcpy(new_address->s6_addr, address->s6_addr, 8);
+ memcpy(new_address->s6_addr + 8, j->prefix.s6_addr + 8, 8);
+ }
+
+ if (new_address) {
+ r = set_put(addresses, new_address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store SLAAC address: %m");
+ else if (r == 0)
+ log_link_debug_errno(link, r, "Generated SLAAC address is duplicated, ignoring.");
+ else
+ TAKE_PTR(new_address);
+ }
+ }
+
+ /* fall back to EUI-64 if no tokens provided addresses */
+ if (set_isempty(addresses)) {
+ _cleanup_free_ struct in6_addr *new_address = NULL;
+
+ new_address = newdup(struct in6_addr, address, 1);
+ if (!new_address)
+ return log_oom();
+
+ r = generate_ipv6_eui_64_address(link, new_address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to generate EUI64 address: %m");
+
+ r = set_put(addresses, new_address);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to store SLAAC address: %m");
+
+ TAKE_PTR(new_address);
+ }
+
+ *ret = TAKE_PTR(addresses);
+
+ return 0;
+}
+
+static int ndisc_router_process_autonomous_prefix(Link *link, sd_ndisc_router *rt) {
+ uint32_t lifetime_valid, lifetime_preferred, lifetime_remaining;
+ _cleanup_set_free_free_ Set *addresses = NULL;
+ _cleanup_(address_freep) Address *address = NULL;
+ struct in6_addr addr, *a;
+ unsigned prefixlen;
+ usec_t time_now;
+ int r;
+
+ assert(link);
+ assert(rt);
+
+ r = sd_ndisc_router_get_timestamp(rt, clock_boottime_or_monotonic(), &time_now);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %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 = sd_ndisc_router_prefix_get_valid_lifetime(rt, &lifetime_valid);
+ 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);
+ 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 > lifetime_valid)
+ return 0;
+
+ r = sd_ndisc_router_prefix_get_address(rt, &addr);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix address: %m");
+
+ r = ndisc_router_generate_addresses(link, &addr, prefixlen, &addresses);
+ if (r < 0)
+ return r;
+
+ r = address_new(&address);
+ if (r < 0)
+ return log_oom();
+
+ address->family = AF_INET6;
+ address->prefixlen = prefixlen;
+ address->flags = IFA_F_NOPREFIXROUTE|IFA_F_MANAGETEMPADDR;
+ address->cinfo.ifa_prefered = lifetime_preferred;
+
+ SET_FOREACH(a, addresses) {
+ Address *existing_address;
+
+ address->in_addr.in6 = *a;
+
+ /* see RFC4862 section 5.5.3.e */
+ r = address_get(link, address, &existing_address);
+ if (r > 0) {
+ lifetime_remaining = existing_address->cinfo.tstamp / 100 + existing_address->cinfo.ifa_valid - time_now / USEC_PER_SEC;
+ if (lifetime_valid > NDISC_PREFIX_LFT_MIN || lifetime_valid > lifetime_remaining)
+ address->cinfo.ifa_valid = lifetime_valid;
+ else if (lifetime_remaining <= NDISC_PREFIX_LFT_MIN)
+ address->cinfo.ifa_valid = lifetime_remaining;
+ else
+ address->cinfo.ifa_valid = NDISC_PREFIX_LFT_MIN;
+ } else if (lifetime_valid > 0)
+ address->cinfo.ifa_valid = lifetime_valid;
+ else
+ continue; /* see RFC4862 section 5.5.3.d */
+
+ if (address->cinfo.ifa_valid == 0)
+ continue;
+
+ r = ndisc_address_configure(address, link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set 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 time_now;
+ uint32_t lifetime;
+ unsigned prefixlen;
+ int r;
+
+ assert(link);
+ assert(rt);
+
+ r = sd_ndisc_router_get_timestamp(rt, clock_boottime_or_monotonic(), &time_now);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA timestamp: %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 = sd_ndisc_router_prefix_get_valid_lifetime(rt, &lifetime);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix lifetime: %m");
+
+ r = route_new(&route);
+ if (r < 0)
+ return log_oom();
+
+ route->family = AF_INET6;
+ route->table = link_get_ipv6_accept_ra_route_table(link);
+ route->priority = link->network->dhcp6_route_metric;
+ route->protocol = RTPROT_RA;
+ route->flags = RTM_F_PREFIX;
+ route->dst_prefixlen = prefixlen;
+ route->lifetime = time_now + lifetime * USEC_PER_SEC;
+
+ r = sd_ndisc_router_prefix_get_address(rt, &route->dst.in6);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix address: %m");
+
+ r = ndisc_route_configure(route, link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set prefix route: %m");;
+
+ return 0;
+}
+
+static int ndisc_router_process_route(Link *link, sd_ndisc_router *rt) {
+ _cleanup_(route_freep) Route *route = NULL;
+ struct in6_addr gateway;
+ uint32_t lifetime;
+ unsigned preference, prefixlen;
+ usec_t time_now;
+ int r;
+
+ assert(link);
+
+ r = sd_ndisc_router_route_get_lifetime(rt, &lifetime);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get gateway lifetime from RA: %m");
+
+ if (lifetime == 0)
+ 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");
+
+ 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");
+
+ 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_or_monotonic(), &time_now);
+ 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->table = link_get_ipv6_accept_ra_route_table(link);
+ route->priority = link->network->dhcp6_route_metric;
+ route->protocol = RTPROT_RA;
+ route->pref = preference;
+ route->gw.in6 = gateway;
+ route->gw_family = AF_INET6;
+ route->dst_prefixlen = prefixlen;
+ route->lifetime = time_now + lifetime * USEC_PER_SEC;
+
+ r = sd_ndisc_router_route_get_address(rt, &route->dst.in6);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get route address: %m");
+
+ r = ndisc_route_configure(route, link, rt);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set 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) {
+ uint32_t lifetime;
+ const struct in6_addr *a;
+ struct in6_addr router;
+ NDiscRDNSS *rdnss;
+ usec_t time_now;
+ int n, r;
+
+ assert(link);
+ assert(rt);
+
+ 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_or_monotonic(), &time_now);
+ 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);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RDNSS lifetime: %m");
+
+ 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");
+
+ SET_FOREACH(rdnss, link->ndisc_rdnss)
+ if (IN6_ARE_ADDR_EQUAL(&rdnss->router, &router))
+ rdnss->marked = true;
+
+ if (lifetime == 0)
+ return 0;
+
+ if (n >= (int) NDISC_RDNSS_MAX) {
+ log_link_warning(link, "Too many RDNSS records per link. Only first %i records will be used.", NDISC_RDNSS_MAX);
+ n = NDISC_RDNSS_MAX;
+ }
+
+ for (int j = 0; j < n; j++) {
+ _cleanup_free_ NDiscRDNSS *x = NULL;
+ NDiscRDNSS d = {
+ .address = a[j],
+ };
+
+ rdnss = set_get(link->ndisc_rdnss, &d);
+ if (rdnss) {
+ rdnss->marked = false;
+ rdnss->router = router;
+ rdnss->valid_until = time_now + lifetime * USEC_PER_SEC;
+ continue;
+ }
+
+ x = new(NDiscRDNSS, 1);
+ if (!x)
+ return log_oom();
+
+ *x = (NDiscRDNSS) {
+ .address = a[j],
+ .router = router,
+ .valid_until = time_now + lifetime * USEC_PER_SEC,
+ };
+
+ r = set_ensure_consume(&link->ndisc_rdnss, &ndisc_rdnss_hash_ops, TAKE_PTR(x));
+ if (r < 0)
+ return log_oom();
+ assert(r > 0);
+ }
+
+ 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;
+ struct in6_addr router;
+ uint32_t lifetime;
+ usec_t time_now;
+ NDiscDNSSL *dnssl;
+ char **j;
+ int r;
+
+ assert(link);
+ assert(rt);
+
+ 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_or_monotonic(), &time_now);
+ 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);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get DNSSL lifetime: %m");
+
+ 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");
+
+ SET_FOREACH(dnssl, link->ndisc_dnssl)
+ if (IN6_ARE_ADDR_EQUAL(&dnssl->router, &router))
+ dnssl->marked = true;
+
+ if (lifetime == 0)
+ return 0;
+
+ if (strv_length(l) >= NDISC_DNSSL_MAX) {
+ log_link_warning(link, "Too many DNSSL records per link. Only first %i records will be used.", NDISC_DNSSL_MAX);
+ STRV_FOREACH(j, l + NDISC_DNSSL_MAX)
+ *j = mfree(*j);
+ }
+
+ STRV_FOREACH(j, l) {
+ _cleanup_free_ NDiscDNSSL *s = NULL;
+
+ s = malloc0(ALIGN(sizeof(NDiscDNSSL)) + strlen(*j) + 1);
+ if (!s)
+ return log_oom();
+
+ strcpy(NDISC_DNSSL_DOMAIN(s), *j);
+
+ dnssl = set_get(link->ndisc_dnssl, s);
+ if (dnssl) {
+ dnssl->marked = false;
+ dnssl->router = router;
+ dnssl->valid_until = time_now + lifetime * USEC_PER_SEC;
+ continue;
+ }
+
+ s->router = router;
+ s->valid_until = time_now + lifetime * USEC_PER_SEC;
+
+ r = set_ensure_consume(&link->ndisc_dnssl, &ndisc_dnssl_hash_ops, TAKE_PTR(s));
+ if (r < 0)
+ return log_oom();
+ assert(r > 0);
+ }
+
+ return 0;
+}
+
+static int ndisc_router_process_options(Link *link, sd_ndisc_router *rt) {
+ assert(link);
+ assert(link->network);
+ assert(rt);
+
+ for (int 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: {
+ union in_addr_union a;
+ uint8_t flags;
+
+ r = sd_ndisc_router_prefix_get_address(rt, &a.in6);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get prefix address: %m");
+
+ if (set_contains(link->network->ndisc_deny_listed_prefix, &a.in6)) {
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *b = NULL;
+
+ (void) in_addr_to_string(AF_INET6, &a, &b);
+ log_link_debug(link, "Prefix '%s' is deny-listed, ignoring", strna(b));
+ }
+ break;
+ }
+
+ 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 (link->network->ipv6_accept_ra_use_onlink_prefix &&
+ FLAGS_SET(flags, ND_OPT_PI_FLAG_ONLINK)) {
+ r = ndisc_router_process_onlink_prefix(link, rt);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->network->ipv6_accept_ra_use_autonomous_prefix &&
+ FLAGS_SET(flags, ND_OPT_PI_FLAG_AUTO)) {
+ r = ndisc_router_process_autonomous_prefix(link, rt);
+ if (r < 0)
+ return r;
+ }
+ break;
+ }
+
+ case SD_NDISC_OPTION_ROUTE_INFORMATION:
+ r = ndisc_router_process_route(link, rt);
+ if (r < 0)
+ return r;
+ break;
+
+ case SD_NDISC_OPTION_RDNSS:
+ if (link->network->ipv6_accept_ra_use_dns) {
+ r = ndisc_router_process_rdnss(link, rt);
+ if (r < 0)
+ return r;
+ }
+ break;
+
+ case SD_NDISC_OPTION_DNSSL:
+ if (link->network->ipv6_accept_ra_use_dns) {
+ r = ndisc_router_process_dnssl(link, rt);
+ if (r < 0)
+ return r;
+ }
+ break;
+ }
+ }
+}
+
+static int ndisc_router_handler(Link *link, sd_ndisc_router *rt) {
+ struct in6_addr router;
+ uint64_t flags;
+ NDiscAddress *na;
+ NDiscRoute *nr;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->manager);
+ assert(rt);
+
+ link->ndisc_addresses_configured = false;
+ link->ndisc_routes_configured = false;
+
+ link_dirty(link);
+
+ 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");
+
+ SET_FOREACH(na, link->ndisc_addresses)
+ if (IN6_ARE_ADDR_EQUAL(&na->router, &router))
+ na->marked = true;
+
+ SET_FOREACH(nr, link->ndisc_routes)
+ if (IN6_ARE_ADDR_EQUAL(&nr->router, &router))
+ nr->marked = true;
+
+ r = sd_ndisc_router_get_flags(rt, &flags);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get RA flags: %m");
+
+ if ((flags & (ND_RA_FLAG_MANAGED | ND_RA_FLAG_OTHER) &&
+ link->network->ipv6_accept_ra_start_dhcp6_client != IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO) ||
+ link->network->ipv6_accept_ra_start_dhcp6_client == IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS) {
+
+ if (flags & (ND_RA_FLAG_MANAGED | ND_RA_FLAG_OTHER))
+ /* (re)start DHCPv6 client in stateful or stateless mode according to RA flags */
+ r = dhcp6_request_address(link, !(flags & ND_RA_FLAG_MANAGED));
+ else
+ /* When IPv6AcceptRA.DHCPv6Client=always, start dhcp6 client in managed mode
+ * even if router does not have M or O flag. */
+ r = dhcp6_request_address(link, false);
+ if (r < 0 && r != -EBUSY)
+ return log_link_error_errno(link, r, "Could not acquire DHCPv6 lease on NDisc request: %m");
+ else
+ log_link_debug(link, "Acquiring DHCPv6 lease on NDisc request");
+ }
+
+ r = ndisc_router_process_default(link, rt);
+ if (r < 0)
+ return r;
+ r = ndisc_router_process_options(link, rt);
+ if (r < 0)
+ return r;
+
+ if (link->ndisc_addresses_messages == 0)
+ link->ndisc_addresses_configured = true;
+ else {
+ log_link_debug(link, "Setting SLAAC addresses.");
+
+ /* address_handler calls link_set_routes() and link_set_nexthop(). Before they are
+ * called, the related flags must be cleared. Otherwise, the link becomes configured
+ * state before routes are configured. */
+ link->static_routes_configured = false;
+ link->static_nexthops_configured = false;
+ }
+
+ if (link->ndisc_routes_messages == 0)
+ link->ndisc_routes_configured = true;
+ else
+ log_link_debug(link, "Setting NDisc routes.");
+
+ r = ndisc_remove_old(link);
+ if (r < 0)
+ return r;
+
+ if (link->ndisc_addresses_configured && link->ndisc_routes_configured)
+ link_check_ready(link);
+ else
+ link_set_state(link, LINK_STATE_CONFIGURING);
+
+ return 0;
+}
+
+static void ndisc_handler(sd_ndisc *nd, sd_ndisc_event event, sd_ndisc_router *rt, void *userdata) {
+ Link *link = userdata;
+ int r;
+
+ assert(link);
+
+ 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) {
+ link_enter_failed(link);
+ return;
+ }
+ break;
+
+ case SD_NDISC_EVENT_TIMEOUT:
+ log_link_debug(link, "NDisc handler get timeout event");
+ if (link->ndisc_addresses_messages == 0 && link->ndisc_routes_messages == 0) {
+ link->ndisc_addresses_configured = true;
+ link->ndisc_routes_configured = true;
+ link_check_ready(link);
+ }
+ break;
+ default:
+ assert_not_reached("Unknown NDisc event");
+ }
+}
+
+int ndisc_configure(Link *link) {
+ int r;
+
+ assert(link);
+
+ if (!link_ipv6_accept_ra_enabled(link))
+ return 0;
+
+ if (!link->ndisc) {
+ 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;
+ }
+
+ r = sd_ndisc_set_mac(link->ndisc, &link->hw_addr.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;
+}
+
+void ndisc_vacuum(Link *link) {
+ NDiscRDNSS *r;
+ NDiscDNSSL *d;
+ usec_t time_now;
+ bool updated = false;
+
+ assert(link);
+
+ /* Removes all RDNSS and DNSSL entries whose validity time has passed */
+
+ time_now = now(clock_boottime_or_monotonic());
+
+ SET_FOREACH(r, link->ndisc_rdnss)
+ if (r->valid_until < time_now) {
+ free(set_remove(link->ndisc_rdnss, r));
+ updated = true;
+ }
+
+ SET_FOREACH(d, link->ndisc_dnssl)
+ if (d->valid_until < time_now) {
+ free(set_remove(link->ndisc_dnssl, d));
+ updated = true;
+ }
+
+ if (updated)
+ link_dirty(link);
+}
+
+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);
+}
+
+int ipv6token_new(IPv6Token **ret) {
+ IPv6Token *p;
+
+ p = new(IPv6Token, 1);
+ if (!p)
+ return -ENOMEM;
+
+ *p = (IPv6Token) {
+ .address_generation_type = IPV6_TOKEN_ADDRESS_GENERATION_NONE,
+ };
+
+ *ret = TAKE_PTR(p);
+
+ return 0;
+}
+
+static void ipv6_token_hash_func(const IPv6Token *p, struct siphash *state) {
+ siphash24_compress(&p->address_generation_type, sizeof(p->address_generation_type), state);
+ siphash24_compress(&p->prefix, sizeof(p->prefix), state);
+}
+
+static int ipv6_token_compare_func(const IPv6Token *a, const IPv6Token *b) {
+ int r;
+
+ r = CMP(a->address_generation_type, b->address_generation_type);
+ if (r != 0)
+ return r;
+
+ return memcmp(&a->prefix, &b->prefix, sizeof(struct in6_addr));
+}
+
+DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ ipv6_token_hash_ops,
+ IPv6Token,
+ ipv6_token_hash_func,
+ ipv6_token_compare_func,
+ free);
+
+int config_parse_ndisc_deny_listed_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) {
+
+ Network *network = data;
+ const char *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ network->ndisc_deny_listed_prefix = set_free_free(network->ndisc_deny_listed_prefix);
+ return 0;
+ }
+
+ for (p = rvalue;;) {
+ _cleanup_free_ char *n = NULL;
+ _cleanup_free_ struct in6_addr *a = 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 NDisc deny-listed prefix, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ r = in_addr_from_string(AF_INET6, n, &ip);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "NDisc deny-listed prefix is invalid, ignoring assignment: %s", n);
+ continue;
+ }
+
+ if (set_contains(network->ndisc_deny_listed_prefix, &ip.in6))
+ continue;
+
+ a = newdup(struct in6_addr, &ip.in6, 1);
+ if (!a)
+ return log_oom();
+
+ r = set_ensure_consume(&network->ndisc_deny_listed_prefix, &in6_addr_hash_ops, TAKE_PTR(a));
+ if (r < 0)
+ return log_oom();
+ }
+}
+
+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_ IPv6Token *token = NULL;
+ union in_addr_union buffer;
+ Network *network = data;
+ const char *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ network->ipv6_tokens = ordered_set_free(network->ipv6_tokens);
+ return 0;
+ }
+
+ r = ipv6token_new(&token);
+ if (r < 0)
+ return log_oom();
+
+ if ((p = startswith(rvalue, "prefixstable"))) {
+ token->address_generation_type = IPV6_TOKEN_ADDRESS_GENERATION_PREFIXSTABLE;
+ if (*p == ':')
+ p++;
+ else if (*p == '\0')
+ p = NULL;
+ else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid IPv6 token mode in %s=, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ } else {
+ token->address_generation_type = IPV6_TOKEN_ADDRESS_GENERATION_STATIC;
+ p = startswith(rvalue, "static:");
+ if (!p)
+ p = rvalue;
+ }
+
+ if (p) {
+ r = in_addr_from_string(AF_INET6, p, &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;
+ }
+ if (token->address_generation_type == IPV6_TOKEN_ADDRESS_GENERATION_STATIC &&
+ in_addr_is_null(AF_INET6, &buffer)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "IPv6 address in %s= cannot be the ANY address, ignoring assignment: %s",
+ lvalue, rvalue);
+ return 0;
+ }
+ token->prefix = buffer.in6;
+ }
+
+ r = ordered_set_ensure_allocated(&network->ipv6_tokens, &ipv6_token_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ r = ordered_set_put(network->ipv6_tokens, token);
+ if (r == -EEXIST)
+ log_syntax(unit, LOG_DEBUG, filename, line, r,
+ "IPv6 token '%s' is duplicated, ignoring: %m", rvalue);
+ else if (r < 0)
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to store IPv6 token '%s', ignoring: %m", rvalue);
+ else
+ TAKE_PTR(token);
+
+ return 0;
+}
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_ipv6_accept_ra_start_dhcp6_client, ipv6_accept_ra_start_dhcp6_client, IPv6AcceptRAStartDHCP6Client,
+ "Failed to parse DHCPv6Client= setting")
+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_STRING_TABLE_LOOKUP_WITH_BOOLEAN(ipv6_accept_ra_start_dhcp6_client, IPv6AcceptRAStartDHCP6Client, IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES);
diff --git a/src/network/networkd-ndisc.h b/src/network/networkd-ndisc.h
new file mode 100644
index 0000000..1562411
--- /dev/null
+++ b/src/network/networkd-ndisc.h
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "networkd-address.h"
+#include "networkd-link.h"
+#include "networkd-route.h"
+#include "time-util.h"
+
+typedef struct IPv6Token IPv6Token;
+
+typedef enum IPv6TokenAddressGeneration {
+ IPV6_TOKEN_ADDRESS_GENERATION_NONE,
+ IPV6_TOKEN_ADDRESS_GENERATION_STATIC,
+ IPV6_TOKEN_ADDRESS_GENERATION_PREFIXSTABLE,
+ _IPV6_TOKEN_ADDRESS_GENERATION_MAX,
+ _IPV6_TOKEN_ADDRESS_GENERATION_INVALID = -1,
+} IPv6TokenAddressGeneration;
+
+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 = -1,
+} IPv6AcceptRAStartDHCP6Client;
+
+typedef struct NDiscAddress {
+ /* Used when GC'ing old DNS servers when configuration changes. */
+ bool marked;
+ struct in6_addr router;
+ Address *address;
+} NDiscAddress;
+
+typedef struct NDiscRoute {
+ /* Used when GC'ing old DNS servers when configuration changes. */
+ bool marked;
+ struct in6_addr router;
+ Route *route;
+} NDiscRoute;
+
+typedef struct NDiscRDNSS {
+ /* Used when GC'ing old DNS servers when configuration changes. */
+ bool marked;
+ struct in6_addr router;
+ usec_t valid_until;
+ struct in6_addr address;
+} NDiscRDNSS;
+
+typedef struct NDiscDNSSL {
+ /* Used when GC'ing old domains when configuration changes. */
+ bool marked;
+ struct in6_addr router;
+ usec_t valid_until;
+ /* The domain name follows immediately. */
+} NDiscDNSSL;
+
+struct IPv6Token {
+ IPv6TokenAddressGeneration address_generation_type;
+
+ uint8_t dad_counter;
+ struct in6_addr prefix;
+};
+
+int ipv6token_new(IPv6Token **ret);
+DEFINE_TRIVIAL_CLEANUP_FUNC(IPv6Token *, freep);
+
+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_configure(Link *link);
+void ndisc_vacuum(Link *link);
+void ndisc_flush(Link *link);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ndisc_deny_listed_prefix);
+CONFIG_PARSER_PROTOTYPE(config_parse_address_generation_type);
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_accept_ra_start_dhcp6_client);
+
+const char* ipv6_accept_ra_start_dhcp6_client_to_string(IPv6AcceptRAStartDHCP6Client i) _const_;
+IPv6AcceptRAStartDHCP6Client ipv6_accept_ra_start_dhcp6_client_from_string(const char *s) _pure_;
diff --git a/src/network/networkd-neighbor.c b/src/network/networkd-neighbor.c
new file mode 100644
index 0000000..c805d52
--- /dev/null
+++ b/src/network/networkd-neighbor.c
@@ -0,0 +1,725 @@
+/* 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 "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);
+ }
+
+ network_config_section_free(neighbor->section);
+
+ if (neighbor->link) {
+ set_remove(neighbor->link->neighbors, neighbor);
+ set_remove(neighbor->link->neighbors_foreign, neighbor);
+ }
+
+ return mfree(neighbor);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(Neighbor, neighbor_free);
+
+static int neighbor_new_static(Network *network, const char *filename, unsigned section_line, Neighbor **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(neighbor_freep) Neighbor *neighbor = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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),
+ };
+
+ r = hashmap_ensure_allocated(&network->neighbors_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->neighbors_by_section, neighbor->section, neighbor);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(neighbor);
+ return 0;
+}
+
+static void neighbor_hash_func(const Neighbor *neighbor, struct siphash *state) {
+ assert(neighbor);
+
+ siphash24_compress(&neighbor->family, sizeof(neighbor->family), state);
+ siphash24_compress(&neighbor->lladdr_size, sizeof(neighbor->lladdr_size), 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;
+ }
+
+ siphash24_compress(&neighbor->lladdr, neighbor->lladdr_size, 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;
+
+ r = CMP(a->lladdr_size, b->lladdr_size);
+ 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 memcmp(&a->lladdr, &b->lladdr, a->lladdr_size);
+}
+
+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) {
+ if (ret)
+ *ret = existing;
+ return 1;
+ }
+
+ existing = set_get(link->neighbors_foreign, in);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int neighbor_add_internal(Link *link, Set **neighbors, const Neighbor *in, Neighbor **ret) {
+ _cleanup_(neighbor_freep) Neighbor *neighbor = NULL;
+ int r;
+
+ assert(link);
+ assert(neighbors);
+ assert(in);
+
+ neighbor = new(Neighbor, 1);
+ if (!neighbor)
+ return -ENOMEM;
+
+ *neighbor = (Neighbor) {
+ .family = in->family,
+ .in_addr = in->in_addr,
+ .lladdr = in->lladdr,
+ .lladdr_size = in->lladdr_size,
+ };
+
+ r = set_ensure_put(neighbors, &neighbor_hash_ops, neighbor);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ neighbor->link = link;
+
+ if (ret)
+ *ret = neighbor;
+
+ TAKE_PTR(neighbor);
+ return 0;
+}
+
+static int neighbor_add(Link *link, const Neighbor *in, Neighbor **ret) {
+ Neighbor *neighbor;
+ int r;
+
+ r = neighbor_get(link, in, &neighbor);
+ if (r == -ENOENT) {
+ /* Neighbor doesn't exist, make a new one */
+ r = neighbor_add_internal(link, &link->neighbors, in, &neighbor);
+ if (r < 0)
+ return r;
+ } else if (r == 0) {
+ /* Neighbor is foreign, claim it as recognized */
+ r = set_ensure_put(&link->neighbors, &neighbor_hash_ops, neighbor);
+ if (r < 0)
+ return r;
+
+ set_remove(link->neighbors_foreign, neighbor);
+ } else if (r == 1) {
+ /* Neighbor already exists */
+ } else
+ return r;
+
+ if (ret)
+ *ret = neighbor;
+ return 0;
+}
+
+static int neighbor_add_foreign(Link *link, const Neighbor *in, Neighbor **ret) {
+ return neighbor_add_internal(link, &link->neighbors_foreign, in, ret);
+}
+
+static bool neighbor_equal(const Neighbor *n1, const Neighbor *n2) {
+ if (n1 == n2)
+ return true;
+
+ if (!n1 || !n2)
+ return false;
+
+ return neighbor_compare_func(n1, n2) == 0;
+}
+
+static int neighbor_configure_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->neighbor_messages > 0);
+
+ link->neighbor_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0 && r != -EEXIST)
+ /* Neighbor may not exist yet. So, do not enter failed state here. */
+ log_link_message_warning_errno(link, m, r, "Could not set neighbor, ignoring");
+
+ if (link->neighbor_messages == 0) {
+ log_link_debug(link, "Neighbors set");
+ link->neighbors_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int neighbor_configure(Neighbor *neighbor, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(neighbor);
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ r = sd_rtnl_message_new_neigh(link->manager->rtnl, &req, RTM_NEWNEIGH,
+ link->ifindex, neighbor->family);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_NEWNEIGH message: %m");
+
+ r = sd_rtnl_message_neigh_set_state(req, NUD_PERMANENT);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set state: %m");
+
+ r = sd_netlink_message_set_flags(req, NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set flags: %m");
+
+ r = sd_netlink_message_append_data(req, NDA_LLADDR, &neighbor->lladdr, neighbor->lladdr_size);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NDA_LLADDR attribute: %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_configure_handler,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not send rtnetlink message: %m");
+
+ link->neighbor_messages++;
+ link_ref(link);
+
+ r = neighbor_add(link, neighbor, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not add neighbor: %m");
+
+ return 0;
+}
+
+int link_set_neighbors(Link *link) {
+ Neighbor *neighbor;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state != _LINK_STATE_INVALID);
+
+ link->neighbors_configured = false;
+
+ HASHMAP_FOREACH(neighbor, link->network->neighbors_by_section) {
+ r = neighbor_configure(neighbor, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set neighbor: %m");
+ }
+
+ if (link->neighbor_messages == 0) {
+ link->neighbors_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Setting 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, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(neighbor);
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ 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);
+
+ return 0;
+}
+
+static bool link_is_neighbor_configured(Link *link, Neighbor *neighbor) {
+ Neighbor *net_neighbor;
+
+ assert(link);
+ assert(neighbor);
+
+ if (!link->network)
+ return false;
+
+ HASHMAP_FOREACH(net_neighbor, link->network->neighbors_by_section)
+ if (neighbor_equal(net_neighbor, neighbor))
+ return true;
+
+ return false;
+}
+
+int link_drop_foreign_neighbors(Link *link) {
+ Neighbor *neighbor;
+ int r;
+
+ assert(link);
+
+ SET_FOREACH(neighbor, link->neighbors_foreign)
+ if (link_is_neighbor_configured(link, neighbor)) {
+ r = neighbor_add(link, neighbor, NULL);
+ if (r < 0)
+ return r;
+ } else {
+ r = neighbor_remove(neighbor, link);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int link_drop_neighbors(Link *link) {
+ Neighbor *neighbor;
+ int k, r = 0;
+
+ assert(link);
+
+ SET_FOREACH(neighbor, link->neighbors) {
+ k = neighbor_remove(neighbor, link);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int manager_rtnl_process_neighbor_lladdr(sd_netlink_message *message, union lladdr_union *lladdr, size_t *size, char **str) {
+ int r;
+
+ assert(message);
+ assert(lladdr);
+ assert(size);
+ assert(str);
+
+ *str = NULL;
+
+ r = sd_netlink_message_read(message, NDA_LLADDR, sizeof(lladdr->ip.in6), &lladdr->ip.in6);
+ if (r >= 0) {
+ *size = sizeof(lladdr->ip.in6);
+ if (in_addr_to_string(AF_INET6, &lladdr->ip, str) < 0)
+ log_warning_errno(r, "Could not print lower address: %m");
+ return r;
+ }
+
+ r = sd_netlink_message_read(message, NDA_LLADDR, sizeof(lladdr->mac), &lladdr->mac);
+ if (r >= 0) {
+ *size = sizeof(lladdr->mac);
+ *str = new(char, ETHER_ADDR_TO_STRING_MAX);
+ if (!*str) {
+ log_oom();
+ return r;
+ }
+ ether_addr_to_string(&lladdr->mac, *str);
+ return r;
+ }
+
+ r = sd_netlink_message_read(message, NDA_LLADDR, sizeof(lladdr->ip.in), &lladdr->ip.in);
+ if (r >= 0) {
+ *size = sizeof(lladdr->ip.in);
+ if (in_addr_to_string(AF_INET, &lladdr->ip, str) < 0)
+ log_warning_errno(r, "Could not print lower address: %m");
+ return r;
+ }
+
+ return r;
+}
+
+int manager_rtnl_process_neighbor(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(neighbor_freep) Neighbor *tmp = NULL;
+ _cleanup_free_ char *addr_str = NULL, *lladdr_str = 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(m, ifindex, &link);
+ if (r < 0 || !link) {
+ /* when enumerating we might be out of sync, but we will get the neighbor again, so just
+ * ignore it */
+ if (!m->enumerating)
+ log_warning("rtnl: received neighbor for link '%d' we don't know about, ignoring.", ifindex);
+ return 0;
+ }
+
+ tmp = new0(Neighbor, 1);
+
+ 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;
+ }
+
+ if (in_addr_to_string(tmp->family, &tmp->in_addr, &addr_str) < 0)
+ log_link_warning_errno(link, r, "Could not print address: %m");
+
+ r = manager_rtnl_process_neighbor_lladdr(message, &tmp->lladdr, &tmp->lladdr_size, &lladdr_str);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received neighbor message with invalid lladdr, ignoring: %m");
+ return 0;
+ }
+
+ (void) neighbor_get(link, tmp, &neighbor);
+
+ switch (type) {
+ case RTM_NEWNEIGH:
+ if (neighbor)
+ log_link_debug(link, "Received remembered neighbor: %s->%s",
+ strnull(addr_str), strnull(lladdr_str));
+ else {
+ /* A neighbor appeared that we did not request */
+ r = neighbor_add_foreign(link, tmp, NULL);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to remember foreign neighbor %s->%s, ignoring: %m",
+ strnull(addr_str), strnull(lladdr_str));
+ return 0;
+ } else
+ log_link_debug(link, "Remembering foreign neighbor: %s->%s",
+ strnull(addr_str), strnull(lladdr_str));
+ }
+
+ break;
+
+ case RTM_DELNEIGH:
+ if (neighbor) {
+ log_link_debug(link, "Forgetting neighbor: %s->%s",
+ strnull(addr_str), strnull(lladdr_str));
+ (void) neighbor_free(neighbor);
+ } else
+ log_link_debug(link, "Kernel removed a neighbor we don't remember: %s->%s, ignoring.",
+ strnull(addr_str), strnull(lladdr_str));
+
+ break;
+
+ default:
+ assert_not_reached("Received invalid RTNL message type");
+ }
+
+ 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->lladdr_size == 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) {
+
+ Network *network = userdata;
+ _cleanup_(neighbor_free_or_set_invalidp) Neighbor *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = neighbor_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ 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) {
+
+ Network *network = userdata;
+ _cleanup_(neighbor_free_or_set_invalidp) Neighbor *n = NULL;
+ int family, r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = neighbor_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = ether_addr_from_string(rvalue, &n->lladdr.mac);
+ if (r >= 0)
+ n->lladdr_size = sizeof(n->lladdr.mac);
+ else {
+ r = in_addr_from_string_auto(rvalue, &family, &n->lladdr.ip);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Neighbor LinkLayerAddress= is invalid, ignoring assignment: %s",
+ rvalue);
+ return 0;
+ }
+ n->lladdr_size = family == AF_INET ? sizeof(n->lladdr.ip.in) : sizeof(n->lladdr.ip.in6);
+ }
+
+ TAKE_PTR(n);
+
+ return 0;
+}
+
+int config_parse_neighbor_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) {
+
+ Network *network = userdata;
+ _cleanup_(neighbor_free_or_set_invalidp) Neighbor *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = neighbor_new_static(network, filename, section_line, &n);
+ if (r < 0)
+ return log_oom();
+
+ r = ether_addr_from_string(rvalue, &n->lladdr.mac);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Neighbor MACAddress= is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ n->lladdr_size = sizeof(n->lladdr.mac);
+ 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..8ad790b
--- /dev/null
+++ b/src/network/networkd-neighbor.h
@@ -0,0 +1,45 @@
+/* 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 Manager Manager;
+typedef Network Network;
+typedef Link Link;
+
+union lladdr_union {
+ struct ether_addr mac;
+ union in_addr_union ip;
+};
+
+typedef struct Neighbor {
+ Network *network;
+ Link *link;
+ NetworkConfigSection *section;
+
+ int family;
+ union in_addr_union in_addr;
+ union lladdr_union lladdr;
+ size_t lladdr_size;
+} Neighbor;
+
+Neighbor *neighbor_free(Neighbor *neighbor);
+
+void network_drop_invalid_neighbors(Network *network);
+
+int link_set_neighbors(Link *link);
+int link_drop_neighbors(Link *link);
+int link_drop_foreign_neighbors(Link *link);
+
+int manager_rtnl_process_neighbor(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_neighbor_address);
+CONFIG_PARSER_PROTOTYPE(config_parse_neighbor_hwaddr);
+CONFIG_PARSER_PROTOTYPE(config_parse_neighbor_lladdr);
diff --git a/src/network/networkd-network-bus.c b/src/network/networkd-network-bus.c
new file mode 100644
index 0000000..0e5f148
--- /dev/null
+++ b/src/network/networkd-network-bus.c
@@ -0,0 +1,137 @@
+/* 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_ether_addrs(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ char buf[ETHER_ADDR_TO_STRING_MAX];
+ const struct ether_addr *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", ether_addr_to_string(p, buf));
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_message_close_container(reply);
+}
+
+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_ether_addrs, offsetof(Network, match_mac), 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_type), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("MatchName", "as", NULL, offsetof(Network, match_name), 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 = userdata;
+ Network *network;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(m);
+ 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 = userdata;
+ Network *network;
+ _cleanup_free_ char *name = NULL;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(m);
+ 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;
+}
diff --git a/src/network/networkd-network-bus.h b/src/network/networkd-network-bus.h
new file mode 100644
index 0000000..cca1e0a
--- /dev/null
+++ b/src/network/networkd-network-bus.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+typedef struct Link Link;
+
+extern const sd_bus_vtable network_vtable[];
+
+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..5cc9e3e
--- /dev/null
+++ b/src/network/networkd-network-gperf.gperf
@@ -0,0 +1,482 @@
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "conf-parser.h"
+#include "netem.h"
+#include "network-internal.h"
+#include "networkd-address-label.h"
+#include "networkd-address.h"
+#include "networkd-can.h"
+#include "networkd-conf.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-dhcp-server.h"
+#include "networkd-dhcp4.h"
+#include "networkd-dhcp6.h"
+#include "networkd-fdb.h"
+#include "networkd-ipv4ll.h"
+#include "networkd-ipv6-proxy-ndp.h"
+#include "networkd-mdb.h"
+#include "networkd-ndisc.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_hwaddrs, 0, offsetof(Network, match_mac)
+Match.PermanentMACAddress, config_parse_hwaddrs, 0, offsetof(Network, match_permanent_mac)
+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_type)
+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_hwaddrs, 0, offsetof(Network, match_bssid)
+Match.Name, config_parse_match_ifnames, IFNAME_VALID_ALTERNATIVE, offsetof(Network, match_name)
+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.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(Network, conditions)
+Link.MACAddress, config_parse_hwaddr, 0, offsetof(Network, mac)
+Link.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(Network, mtu)
+Link.Group, config_parse_uint32, 0, offsetof(Network, group)
+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.Unmanaged, config_parse_bool, 0, offsetof(Network, unmanaged)
+Link.RequiredForOnline, config_parse_required_for_online, 0, 0
+SR-IOV.VirtualFunction, config_parse_sr_iov_uint32, 0, 0
+SR-IOV.VLANId, config_parse_sr_iov_uint32, 0, 0
+SR-IOV.QualityOfService, config_parse_sr_iov_uint32, 0, 0
+SR-IOV.VLANProtocol, config_parse_sr_iov_vlan_proto, 0, 0
+SR-IOV.MACSpoofCheck, config_parse_sr_iov_boolean, 0, 0
+SR-IOV.QueryReceiveSideScaling, config_parse_sr_iov_boolean, 0, 0
+SR-IOV.Trust, config_parse_sr_iov_boolean, 0, 0
+SR-IOV.LinkState, config_parse_sr_iov_link_state, 0, 0
+SR-IOV.MACAddress, config_parse_sr_iov_mac, 0, 0
+Network.Description, config_parse_string, 0, offsetof(Network, description)
+Network.Bridge, config_parse_ifname, 0, offsetof(Network, bridge_name)
+Network.Bond, config_parse_ifname, 0, offsetof(Network, bond_name)
+Network.VLAN, config_parse_stacked_netdev, NETDEV_KIND_VLAN, 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.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.VXLAN, config_parse_stacked_netdev, NETDEV_KIND_VXLAN, offsetof(Network, stacked_netdev_names)
+Network.L2TP, config_parse_stacked_netdev, NETDEV_KIND_L2TP, offsetof(Network, stacked_netdev_names)
+Network.MACsec, config_parse_stacked_netdev, NETDEV_KIND_MACSEC, offsetof(Network, stacked_netdev_names)
+Network.Tunnel, config_parse_stacked_netdev, _NETDEV_KIND_TUNNEL, offsetof(Network, stacked_netdev_names)
+Network.Xfrm, config_parse_stacked_netdev, NETDEV_KIND_XFRM, offsetof(Network, stacked_netdev_names)
+Network.VRF, config_parse_ifname, 0, offsetof(Network, vrf_name)
+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.IPv4LLRoute, config_parse_bool, 0, offsetof(Network, ipv4ll_route)
+Network.DefaultRouteOnDevice, config_parse_bool, 0, offsetof(Network, default_route_on_device)
+Network.IPv6Token, config_parse_address_generation_type, 0, 0
+Network.LLDP, config_parse_lldp_mode, 0, offsetof(Network, lldp_mode)
+Network.EmitLLDP, config_parse_lldp_emit, 0, offsetof(Network, lldp_emit)
+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, 0
+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_bool, 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.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_tristate, 0, offsetof(Network, ignore_carrier_loss)
+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.DHCPv6PrefixDelegation, config_parse_tristate, 0, offsetof(Network, dhcp6_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
+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_hwaddr, 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.SuppressPrefixLength, config_parse_routing_policy_rule_suppress_prefixlen, 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.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
+NextHop.Id, config_parse_nexthop_id, 0, 0
+NextHop.Gateway, config_parse_nexthop_gateway, 0, 0
+DHCPv4.ClientIdentifier, config_parse_dhcp_client_identifier, 0, offsetof(Network, dhcp_client_identifier)
+DHCPv4.UseDNS, config_parse_dhcp_use_dns, 0, 0
+DHCPv4.RoutesToDNS, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_dns)
+DHCPv4.UseNTP, config_parse_dhcp_use_ntp, 0, 0
+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, 0, offsetof(Network, dhcp_use_domains)
+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.RequestBroadcast, config_parse_bool, 0, offsetof(Network, dhcp_broadcast)
+DHCPv4.VendorClassIdentifier, config_parse_string, 0, offsetof(Network, dhcp_vendor_class_identifier)
+DHCPv4.MUDURL, config_parse_dhcp_mud_url, 0, 0
+DHCPv4.MaxAttempts, config_parse_dhcp_max_attempts, 0, 0
+DHCPv4.UserClass, config_parse_dhcp_user_class, AF_INET, offsetof(Network, dhcp_user_class)
+DHCPv4.DUIDType, config_parse_duid_type, 0, offsetof(Network, duid)
+DHCPv4.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Network, duid)
+DHCPv4.RouteMetric, config_parse_dhcp_route_metric, 0, 0
+DHCPv4.RouteTable, config_parse_section_route_table, 0, 0
+DHCPv4.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone)
+DHCPv4.IAID, config_parse_iaid, 0, 0
+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_dhcp_acl_ip_address, 0, 0
+DHCPv4.AllowList, config_parse_dhcp_acl_ip_address, 0, 0
+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
+DHCPv6.UseDNS, config_parse_dhcp_use_dns, 0, 0
+DHCPv6.UseNTP, config_parse_dhcp_use_ntp, 0, 0
+DHCPv6.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp6_rapid_commit)
+DHCPv6.MUDURL, config_parse_dhcp6_mud_url, 0, 0
+DHCPv6.RequestOptions, config_parse_dhcp_request_options, AF_INET6, 0
+DHCPv6.UserClass, config_parse_dhcp_user_class, AF_INET6, offsetof(Network, dhcp6_user_class)
+DHCPv6.VendorClass, config_parse_dhcp_vendor_class, 0, offsetof(Network, dhcp6_vendor_class)
+DHCPv6.SendVendorOption, config_parse_dhcp_send_option, AF_INET6, offsetof(Network, dhcp6_client_send_vendor_options)
+DHCPv6.ForceDHCPv6PDOtherInformation, config_parse_bool, 0, offsetof(Network, dhcp6_force_pd_other_information)
+DHCPv6.PrefixDelegationHint, config_parse_dhcp6_pd_hint, 0, 0
+DHCPv6.WithoutRA, config_parse_dhcp6_client_start_mode, 0, offsetof(Network, dhcp6_without_ra)
+DHCPv6.SendOption, config_parse_dhcp_send_option, AF_INET6, offsetof(Network, dhcp6_client_send_options)
+DHCPv6.RouteMetric, config_parse_dhcp_route_metric, 0, 0
+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_dhcp_use_domains, 0, offsetof(Network, ipv6_accept_ra_use_domains)
+IPv6AcceptRA.DHCPv6Client, config_parse_ipv6_accept_ra_start_dhcp6_client, 0, offsetof(Network, ipv6_accept_ra_start_dhcp6_client)
+IPv6AcceptRA.RouteTable, config_parse_section_route_table, 0, 0
+IPv6AcceptRA.DenyList, config_parse_ndisc_deny_listed_prefix, 0, 0
+IPv6AcceptRA.BlackList, config_parse_ndisc_deny_listed_prefix, 0, 0
+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.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)
+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.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
+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
+DHCPv6PrefixDelegation.SubnetId, config_parse_dhcp6_pd_subnet_id, 0, offsetof(Network, dhcp6_pd_subnet_id)
+DHCPv6PrefixDelegation.Announce, config_parse_bool, 0, offsetof(Network, dhcp6_pd_announce)
+DHCPv6PrefixDelegation.Assign, config_parse_bool, 0, offsetof(Network, dhcp6_pd_assign)
+DHCPv6PrefixDelegation.Token, config_parse_dhcp6_pd_token, 0, offsetof(Network, dhcp6_pd_token)
+IPv6SendRA.RouterLifetimeSec, config_parse_sec, 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)
+IPv6Prefix.Prefix, config_parse_prefix, 0, 0
+IPv6Prefix.OnLink, config_parse_prefix_flags, 0, 0
+IPv6Prefix.AddressAutoconfiguration, config_parse_prefix_flags, 0, 0
+IPv6Prefix.ValidLifetimeSec, config_parse_prefix_lifetime, 0, 0
+IPv6Prefix.PreferredLifetimeSec, config_parse_prefix_lifetime, 0, 0
+IPv6Prefix.Assign, config_parse_prefix_assign, 0, 0
+IPv6RoutePrefix.Route, config_parse_route_prefix, 0, 0
+IPv6RoutePrefix.LifetimeSec, config_parse_route_prefix_lifetime, 0, 0
+LLDP.MUDURL, config_parse_lldp_mud, 0, 0
+CAN.BitRate, config_parse_can_bitrate, 0, offsetof(Network, can_bitrate)
+CAN.SamplePoint, config_parse_permille, 0, offsetof(Network, can_sample_point)
+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.FDMode, config_parse_tristate, 0, offsetof(Network, can_fd_mode)
+CAN.FDNonISO, config_parse_tristate, 0, offsetof(Network, can_non_iso)
+CAN.RestartSec, config_parse_sec, 0, offsetof(Network, can_restart_us)
+CAN.TripleSampling, config_parse_tristate, 0, offsetof(Network, can_triple_sampling)
+CAN.Termination, config_parse_tristate, 0, offsetof(Network, can_termination)
+CAN.ListenOnly, config_parse_tristate, 0, offsetof(Network, can_listen_only)
+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.OverheadBytes, config_parse_cake_overhead, 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.IPv6PrefixDelegation, config_parse_router_prefix_delegation, 0, offsetof(Network, router_prefix_delegation)
+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_dhcp_acl_ip_address, 0, 0
+DHCP.ClientIdentifier, config_parse_dhcp_client_identifier, 0, offsetof(Network, dhcp_client_identifier)
+DHCP.UseDNS, config_parse_dhcp_use_dns, 0, 0
+DHCP.UseNTP, config_parse_dhcp_use_ntp, 0, 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, 0, offsetof(Network, dhcp_use_domains)
+DHCP.UseDomainName, config_parse_dhcp_use_domains, 0, offsetof(Network, dhcp_use_domains)
+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_bool, 0, offsetof(Network, dhcp_broadcast)
+DHCP.CriticalConnection, config_parse_tristate, 0, offsetof(Network, dhcp_critical)
+DHCP.VendorClassIdentifier, config_parse_string, 0, offsetof(Network, dhcp_vendor_class_identifier)
+DHCP.UserClass, config_parse_dhcp_user_class, AF_INET, offsetof(Network, dhcp_user_class)
+DHCP.DUIDType, config_parse_duid_type, 0, offsetof(Network, duid)
+DHCP.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Network, duid)
+DHCP.RouteMetric, config_parse_dhcp_route_metric, 0, 0
+DHCP.RouteTable, config_parse_section_route_table, 0, 0
+DHCP.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone)
+DHCP.IAID, config_parse_iaid, 0, 0
+DHCP.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port)
+DHCP.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp6_rapid_commit)
+DHCP.ForceDHCPv6PDOtherInformation, config_parse_bool, 0, offsetof(Network, dhcp6_force_pd_other_information)
+DHCPv4.UseDomainName, config_parse_dhcp_use_domains, 0, offsetof(Network, dhcp_use_domains)
+DHCPv4.CriticalConnection, config_parse_tristate, 0, offsetof(Network, dhcp_critical)
+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..3254641
--- /dev/null
+++ b/src/network/networkd-network.c
@@ -0,0 +1,1238 @@
+/* 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 "networkd-dhcp-server.h"
+#include "network-internal.h"
+#include "networkd-address-label.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-fdb.h"
+#include "networkd-manager.h"
+#include "networkd-mdb.h"
+#include "networkd-ndisc.h"
+#include "networkd-neighbor.h"
+#include "networkd-network.h"
+#include "networkd-nexthop.h"
+#include "networkd-radv.h"
+#include "networkd-routing-policy-rule.h"
+#include "networkd-sriov.h"
+#include "parse-util.h"
+#include "path-lookup.h"
+#include "set.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tc.h"
+#include "util.h"
+
+/* Let's assume that anything above this number is a user misconfiguration. */
+#define MAX_NTP_SERVERS 128
+
+/* Set defaults following RFC7844 */
+void network_apply_anonymize_if_set(Network *network) {
+ if (!network->dhcp_anonymize)
+ return;
+ /* RFC7844 3.7
+ SHOULD NOT send the Host Name option */
+ network->dhcp_send_hostname = false;
+ /* 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 */
+ network->dhcp_client_identifier = DHCP_CLIENT_ID_MAC;
+ /* RFC 7844 3.10:
+ SHOULD NOT use the Vendor Class Identifier option */
+ network->dhcp_vendor_class_identifier = mfree(network->dhcp_vendor_class_identifier);
+ /* RFC7844 section 3.6.:
+ The client intending to protect its privacy SHOULD only request a
+ minimal number of options in the PRL and SHOULD also randomly shuffle
+ the ordering of option codes in the PRL. If this random ordering
+ cannot be implemented, the client MAY order the option codes in the
+ PRL by option code number (lowest to highest).
+ */
+ /* NOTE: dhcp_use_mtu is false by default,
+ * though it was not initiallized to any value in network_load_one.
+ * Maybe there should be another var called *send*?
+ * (to use the MTU sent by the server but to do not send
+ * the option in the PRL). */
+ network->dhcp_use_mtu = false;
+ /* NOTE: when Anonymize=yes, the PRL route options are sent by default,
+ * but this is needed to use them. */
+ network->dhcp_use_routes = true;
+ /* RFC7844 section 3.6.
+ * same comments as previous option */
+ network->dhcp_use_timezone = false;
+}
+
+static int network_resolve_netdev_one(Network *network, const char *name, NetDevKind kind, NetDev **ret_netdev) {
+ 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_netdev);
+
+ if (kind == _NETDEV_KIND_TUNNEL)
+ kind_string = "tunnel";
+ else {
+ kind_string = netdev_kind_to_string(kind);
+ if (!kind_string)
+ return log_error_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_error_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_IPIP,
+ NETDEV_KIND_SIT,
+ NETDEV_KIND_GRE,
+ NETDEV_KIND_GRETAP,
+ NETDEV_KIND_IP6GRE,
+ NETDEV_KIND_IP6GRETAP,
+ NETDEV_KIND_VTI,
+ NETDEV_KIND_VTI6,
+ NETDEV_KIND_IP6TNL,
+ NETDEV_KIND_ERSPAN)))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: NetDev %s is not a %s, ignoring assignment",
+ network->filename, name, kind_string);
+
+ *ret_netdev = 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;
+
+ r = network_resolve_netdev_one(network, name, PTR_TO_INT(kind), &netdev);
+ if (r <= 0)
+ continue;
+
+ r = hashmap_ensure_allocated(&network->stacked_netdevs, &string_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ r = hashmap_put(network->stacked_netdevs, netdev->ifname, netdev);
+ if (r < 0)
+ return log_error_errno(r, "%s: Failed to add NetDev '%s' to network: %m",
+ network->filename, (const char *) name);
+
+ netdev = NULL;
+ }
+
+ return 0;
+}
+
+int network_verify(Network *network) {
+ assert(network);
+ assert(network->filename);
+
+ if (set_isempty(network->match_mac) && set_isempty(network->match_permanent_mac) &&
+ strv_isempty(network->match_path) && strv_isempty(network->match_driver) &&
+ strv_isempty(network->match_type) && strv_isempty(network->match_name) &&
+ strv_isempty(network->match_property) && strv_isempty(network->match_wlan_iftype) &&
+ strv_isempty(network->match_ssid) && !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);
+
+ (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);
+ (void) network_resolve_stacked_netdevs(network);
+
+ /* Free unnecessary entries. */
+ 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 = network->bridge ? ADDRESS_FAMILY_NO : ADDRESS_FAMILY_IPV6;
+
+ if (FLAGS_SET(network->link_local, ADDRESS_FAMILY_FALLBACK_IPV4) &&
+ !FLAGS_SET(network->dhcp, ADDRESS_FAMILY_IPV4)) {
+ log_warning("%s: fallback assignment of IPv4 link local address is enabled but DHCPv4 is disabled. "
+ "Disabling the fallback assignment.", network->filename);
+ SET_FLAG(network->link_local, ADDRESS_FAMILY_FALLBACK_IPV4, false);
+ }
+
+ /* IPMasquerade=yes implies IPForward=yes */
+ if (network->ip_masquerade)
+ network->ip_forward |= ADDRESS_FAMILY_IPV4;
+
+ network_adjust_ipv6_accept_ra(network);
+ network_adjust_dhcp(network);
+ network_adjust_radv(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_use_gateway < 0)
+ network->dhcp_use_gateway = network->dhcp_use_routes;
+
+ if (network->ignore_carrier_loss < 0)
+ network->ignore_carrier_loss = network->configure_without_carrier;
+
+ if (network->dhcp_critical >= 0) {
+ if (network->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 (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);
+ }
+
+ network_drop_invalid_addresses(network);
+ network_drop_invalid_routes(network);
+ network_drop_invalid_nexthops(network);
+ network_drop_invalid_fdb_entries(network);
+ network_drop_invalid_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_traffic_control(network);
+ network_drop_invalid_sr_iov(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;
+ _cleanup_fclose_ FILE *file = NULL;
+ const char *dropin_dirname;
+ char *d;
+ int r;
+
+ assert(manager);
+ assert(filename);
+
+ file = fopen(filename, "re");
+ if (!file) {
+ if (errno == ENOENT)
+ return 0;
+
+ return -errno;
+ }
+
+ if (null_or_empty_fd(fileno(file))) {
+ 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 -EINVAL;
+
+ *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 = true,
+ .required_operstate_for_online = LINK_OPERSTATE_RANGE_DEFAULT,
+ .arp = -1,
+ .multicast = -1,
+ .allmulticast = -1,
+
+ .configure_without_carrier = false,
+ .ignore_carrier_loss = -1,
+ .keep_configuration = _KEEP_CONFIGURATION_INVALID,
+
+ .dhcp = ADDRESS_FAMILY_NO,
+ .duid.type = _DUID_TYPE_INVALID,
+ .dhcp_critical = -1,
+ .dhcp_use_ntp = true,
+ .dhcp_use_sip = true,
+ .dhcp_use_dns = true,
+ .dhcp_use_hostname = true,
+ .dhcp_use_routes = true,
+ .dhcp_use_gateway = -1,
+ /* NOTE: this var might be overwritten by network_apply_anonymize_if_set */
+ .dhcp_send_hostname = true,
+ .dhcp_send_release = true,
+ /* To enable/disable RFC7844 Anonymity Profiles */
+ .dhcp_anonymize = false,
+ .dhcp_route_metric = DHCP_ROUTE_METRIC,
+ /* NOTE: this var might be overwritten by network_apply_anonymize_if_set */
+ .dhcp_client_identifier = DHCP_CLIENT_ID_DUID,
+ .dhcp_route_table = RT_TABLE_MAIN,
+ .dhcp_route_table_set = false,
+ /* NOTE: from man: UseMTU=... Defaults to false*/
+ .dhcp_use_mtu = false,
+ /* NOTE: from man: UseTimezone=... Defaults to "no".*/
+ .dhcp_use_timezone = false,
+ .dhcp_ip_service_type = -1,
+
+ .dhcp6_rapid_commit = true,
+ .dhcp6_route_metric = DHCP_ROUTE_METRIC,
+ .dhcp6_use_ntp = true,
+ .dhcp6_use_dns = true,
+
+ .dhcp6_pd = -1,
+ .dhcp6_pd_announce = true,
+ .dhcp6_pd_assign = true,
+ .dhcp6_pd_subnet_id = -1,
+
+ .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 = 30 * USEC_PER_MINUTE,
+ .router_emit_dns = true,
+ .router_emit_domains = true,
+
+ .use_bpdu = -1,
+ .hairpin = -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,
+
+ .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,
+ .ipv6_privacy_extensions = IPV6_PRIVACY_EXTENSIONS_NO,
+ .ipv6_accept_ra = -1,
+ .ipv6_dad_transmits = -1,
+ .ipv6_hop_limit = -1,
+ .ipv6_proxy_ndp = -1,
+ .proxy_arp = -1,
+
+ .ipv6_accept_ra_use_dns = true,
+ .ipv6_accept_ra_use_autonomous_prefix = true,
+ .ipv6_accept_ra_use_onlink_prefix = true,
+ .ipv6_accept_ra_route_table = RT_TABLE_MAIN,
+ .ipv6_accept_ra_route_table_set = false,
+ .ipv6_accept_ra_start_dhcp6_client = IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES,
+
+ .can_triple_sampling = -1,
+ .can_termination = -1,
+ .can_listen_only = -1,
+ .can_fd_mode = -1,
+ .can_non_iso = -1,
+ };
+
+ r = config_parse_many(
+ 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"
+ "DHCPServer\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->timestamp);
+ if (r < 0)
+ return r;
+
+ network_apply_anonymize_if_set(network);
+
+ r = network_add_ipv4ll_route(network);
+ if (r < 0)
+ log_warning_errno(r, "%s: Failed to add IPv4LL route, ignoring: %m", network->filename);
+
+ r = network_add_default_route_on_device(network);
+ if (r < 0)
+ log_warning_errno(r, "%s: Failed to add default route on device, ignoring: %m",
+ network->filename);
+
+ if (network_verify(network) < 0)
+ /* Ignore .network files that do not match the conditions. */
+ return 0;
+
+ r = ordered_hashmap_ensure_allocated(networks, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(*networks, network->name, network);
+ if (r < 0)
+ return r;
+
+ network = NULL;
+ return 0;
+}
+
+int network_load(Manager *manager, OrderedHashmap **networks) {
+ _cleanup_strv_free_ char **files = NULL;
+ char **f;
+ 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) {
+ r = network_load_one(manager, networks, *f);
+ if (r < 0)
+ log_error_errno(r, "Failed to load %s, ignoring: %m", *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)
+ continue; /* The .network file is new. */
+
+ if (n->timestamp != old->timestamp)
+ continue; /* The .network file is modified. */
+
+ if (!streq(n->filename, old->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 0;
+
+failure:
+ ordered_hashmap_free_with_destructor(new_networks, network_unref);
+
+ return r;
+}
+
+static Network *network_free(Network *network) {
+ if (!network)
+ return NULL;
+
+ free(network->filename);
+
+ set_free_free(network->match_mac);
+ set_free_free(network->match_permanent_mac);
+ strv_free(network->match_path);
+ strv_free(network->match_driver);
+ strv_free(network->match_type);
+ strv_free(network->match_name);
+ strv_free(network->match_property);
+ strv_free(network->match_wlan_iftype);
+ strv_free(network->match_ssid);
+ set_free_free(network->match_bssid);
+ condition_free_list(network->conditions);
+
+ free(network->description);
+ free(network->dhcp_vendor_class_identifier);
+ free(network->dhcp_mudurl);
+ strv_free(network->dhcp_user_class);
+ free(network->dhcp_hostname);
+ set_free(network->dhcp_deny_listed_ip);
+ set_free(network->dhcp_allow_listed_ip);
+ set_free(network->dhcp_request_options);
+ set_free(network->dhcp6_request_options);
+ free(network->mac);
+ free(network->dhcp6_mudurl);
+ strv_free(network->dhcp6_user_class);
+ strv_free(network->dhcp6_vendor_class);
+
+ strv_free(network->ntp);
+ 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);
+ strv_free(network->bind_carrier);
+
+ ordered_set_free(network->router_search_domains);
+ free(network->router_dns);
+ set_free_free(network->ndisc_deny_listed_prefix);
+
+ 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);
+
+ 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->fdb_entries_by_section, fdb_entry_free);
+ hashmap_free_with_destructor(network->mdb_entries_by_section, mdb_entry_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);
+ ordered_hashmap_free_with_destructor(network->sr_iov_by_section, sr_iov_free);
+ ordered_hashmap_free_with_destructor(network->tc_by_section, traffic_control_free);
+
+ if (network->manager &&
+ network->manager->duids_requesting_uuid)
+ set_remove(network->manager->duids_requesting_uuid, &network->duid);
+
+ free(network->name);
+
+ free(network->dhcp_server_timezone);
+
+ for (sd_dhcp_lease_server_type t = 0; t < _SD_DHCP_LEASE_SERVER_TYPE_MAX; t++)
+ free(network->dhcp_server_emit[t].addresses);
+
+ set_free_free(network->dnssec_negative_trust_anchors);
+
+ free(network->lldp_mud);
+
+ ordered_hashmap_free(network->dhcp_client_send_options);
+ ordered_hashmap_free(network->dhcp_client_send_vendor_options);
+ ordered_hashmap_free(network->dhcp_server_send_options);
+ ordered_hashmap_free(network->dhcp_server_send_vendor_options);
+ ordered_set_free(network->ipv6_tokens);
+ ordered_hashmap_free(network->dhcp6_client_send_options);
+ ordered_hashmap_free(network->dhcp6_client_send_vendor_options);
+
+ 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;
+}
+
+int network_get(Manager *manager, unsigned short iftype, sd_device *device,
+ const char *ifname, char * const *alternative_names, const char *driver,
+ const struct ether_addr *mac, const struct ether_addr *permanent_mac,
+ enum nl80211_iftype wlan_iftype, const char *ssid, const struct ether_addr *bssid,
+ Network **ret) {
+ Network *network;
+
+ assert(manager);
+ assert(ret);
+
+ ORDERED_HASHMAP_FOREACH(network, manager->networks)
+ if (net_match_config(network->match_mac, network->match_permanent_mac,
+ network->match_path, network->match_driver,
+ network->match_type, network->match_name, network->match_property,
+ network->match_wlan_iftype, network->match_ssid, network->match_bssid,
+ device, mac, permanent_mac, driver, iftype,
+ ifname, alternative_names, wlan_iftype, ssid, bssid)) {
+ if (network->match_name && device) {
+ const char *attr;
+ uint8_t name_assign_type = NET_NAME_UNKNOWN;
+
+ if (sd_device_get_sysattr_value(device, "name_assign_type", &attr) >= 0)
+ (void) safe_atou8(attr, &name_assign_type);
+
+ if (name_assign_type == NET_NAME_ENUM)
+ log_warning("%s: found matching network '%s', based on potentially unpredictable ifname",
+ ifname, network->filename);
+ else
+ log_debug("%s: found matching network '%s'", ifname, network->filename);
+ } else
+ log_debug("%s: found matching network '%s'", ifname, network->filename);
+
+ *ret = network;
+ return 0;
+ }
+
+ *ret = NULL;
+
+ return -ENOENT;
+}
+
+int network_apply(Network *network, Link *link) {
+ assert(network);
+ assert(link);
+
+ link->network = network_ref(network);
+
+ if (network->n_dns > 0 ||
+ !strv_isempty(network->ntp) ||
+ !ordered_set_isempty(network->search_domains) ||
+ !ordered_set_isempty(network->route_domains))
+ link_dirty(link);
+
+ return 0;
+}
+
+bool network_has_static_ipv6_configurations(Network *network) {
+ Address *address;
+ Route *route;
+ FdbEntry *fdb;
+ MdbEntry *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->fdb_entries_by_section)
+ if (fdb->family == AF_INET6)
+ return true;
+
+ HASHMAP_FOREACH(mdb, network->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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+ assert(IN_SET(kind,
+ NETDEV_KIND_VLAN, NETDEV_KIND_MACVLAN, NETDEV_KIND_MACVTAP,
+ NETDEV_KIND_IPVLAN, NETDEV_KIND_IPVTAP, NETDEV_KIND_VXLAN,
+ NETDEV_KIND_L2TP, NETDEV_KIND_MACSEC, _NETDEV_KIND_TUNNEL,
+ NETDEV_KIND_XFRM));
+
+ 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_allocated(h, &string_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ r = hashmap_put(*h, name, INT_TO_PTR(kind));
+ 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
+ name = NULL;
+
+ 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 = data;
+ int r;
+
+ assert(n);
+ 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_ensure_allocated(set, &string_hash_ops_free);
+ if (r < 0)
+ return log_oom();
+
+ r = ordered_set_put_strdup(*set, domain);
+ if (r < 0)
+ return log_oom();
+ }
+}
+
+int config_parse_hostname(
+ 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 *hn = NULL;
+ char **hostname = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = config_parse_string(unit, filename, line, section, section_line, lvalue, ltype, rvalue, &hn, userdata);
+ if (r < 0)
+ return r;
+
+ if (!hostname_is_valid(hn, false)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Hostname is not valid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ r = dns_name_is_valid(hn);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to check validity of hostname '%s', ignoring assignment: %m", rvalue);
+ return 0;
+ }
+ if (r == 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Hostname is not a valid DNS domain name, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ return free_and_replace(*hostname, hn);
+}
+
+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) {
+
+ _cleanup_free_ char *tz = NULL;
+ char **datap = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ r = config_parse_string(unit, filename, line, section, section_line, lvalue, ltype, rvalue, &tz, userdata);
+ if (r < 0)
+ return r;
+
+ if (!timezone_is_valid(tz, LOG_WARNING)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Timezone is not valid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ return free_and_replace(*datap, tz);
+}
+
+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 = 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) {
+
+ Network *n = data;
+ int r;
+
+ assert(n);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ n->dnssec_negative_trust_anchors = set_free_free(n->dnssec_negative_trust_anchors);
+ 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(&n->dnssec_negative_trust_anchors, &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 = data;
+ int r;
+
+ assert(l);
+ 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 = data;
+ LinkOperationalStateRange range;
+ bool required = true;
+ int r;
+
+ if (isempty(rvalue)) {
+ network->required_for_online = true;
+ 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;
+}
+
+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 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-network.h b/src/network/networkd-network.h
new file mode 100644
index 0000000..fd0fe05
--- /dev/null
+++ b/src/network/networkd-network.h
@@ -0,0 +1,340 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <linux/nl80211.h>
+
+#include "sd-bus.h"
+#include "sd-device.h"
+
+#include "bridge.h"
+#include "condition.h"
+#include "conf-parser.h"
+#include "hashmap.h"
+#include "netdev.h"
+#include "networkd-brvlan.h"
+#include "networkd-dhcp-common.h"
+#include "networkd-dhcp4.h"
+#include "networkd-dhcp6.h"
+#include "networkd-dhcp-server.h"
+#include "networkd-lldp-rx.h"
+#include "networkd-lldp-tx.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 = -1,
+} KeepConfiguration;
+
+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 = -1
+} IPv6LinkLocalAddressGenMode;
+
+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;
+ usec_t timestamp;
+ char *description;
+
+ /* [Match] section */
+ Set *match_mac;
+ Set *match_permanent_mac;
+ char **match_path;
+ char **match_driver;
+ char **match_type;
+ char **match_name;
+ char **match_property;
+ char **match_wlan_iftype;
+ char **match_ssid;
+ Set *match_bssid;
+ LIST_HEAD(Condition, conditions);
+
+ /* Master or stacked netdevs */
+ NetDev *bridge;
+ NetDev *bond;
+ NetDev *vrf;
+ NetDev *xfrm;
+ Hashmap *stacked_netdevs;
+ char *bridge_name;
+ char *bond_name;
+ char *vrf_name;
+ Hashmap *stacked_netdev_names;
+
+ /* [Link] section */
+ struct ether_addr *mac;
+ uint32_t mtu;
+ uint32_t group;
+ int arp;
+ int multicast;
+ int allmulticast;
+ bool unmanaged;
+ bool required_for_online; /* Is this network required to be considered online? */
+ LinkOperationalStateRange required_operstate_for_online;
+
+ /* misc settings */
+ bool configure_without_carrier;
+ int ignore_carrier_loss;
+ KeepConfiguration keep_configuration;
+ char **bind_carrier;
+ bool default_route_on_device;
+ bool ip_masquerade;
+
+ /* DHCP Client Support */
+ AddressFamily dhcp;
+ DHCPClientIdentifier dhcp_client_identifier;
+ DUID duid;
+ uint32_t iaid;
+ bool iaid_set;
+ char *dhcp_vendor_class_identifier;
+ char *dhcp_mudurl;
+ char **dhcp_user_class;
+ char *dhcp_hostname;
+ uint64_t dhcp_max_attempts;
+ uint32_t dhcp_route_metric;
+ bool dhcp_route_metric_set;
+ uint32_t dhcp_route_table;
+ 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;
+ bool 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_use_sip;
+ bool dhcp_use_mtu;
+ bool dhcp_use_routes;
+ int dhcp_use_gateway;
+ bool dhcp_use_timezone;
+ bool dhcp_use_hostname;
+ bool dhcp_route_table_set;
+ bool dhcp_send_release;
+ bool dhcp_send_decline;
+ DHCPUseDomains dhcp_use_domains;
+ 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;
+
+ /* DHCPv6 Client support*/
+ bool dhcp6_use_dns;
+ bool dhcp6_use_dns_set;
+ bool dhcp6_use_ntp;
+ bool dhcp6_use_ntp_set;
+ bool dhcp6_rapid_commit;
+ uint8_t dhcp6_pd_length;
+ uint32_t dhcp6_route_metric;
+ bool dhcp6_route_metric_set;
+ char *dhcp6_mudurl;
+ char **dhcp6_user_class;
+ char **dhcp6_vendor_class;
+ struct in6_addr dhcp6_pd_address;
+ DHCP6ClientStartMode dhcp6_without_ra;
+ OrderedHashmap *dhcp6_client_send_options;
+ OrderedHashmap *dhcp6_client_send_vendor_options;
+ Set *dhcp6_request_options;
+ /* Start DHCPv6 PD also when 'O' RA flag is set, see RFC 7084, WPD-4 */
+ bool dhcp6_force_pd_other_information;
+
+ /* DHCP Server Support */
+ bool dhcp_server;
+ NetworkDHCPServerEmitAddress dhcp_server_emit[_SD_DHCP_LEASE_SERVER_TYPE_MAX];
+ bool dhcp_server_emit_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;
+
+ /* link local addressing support */
+ AddressFamily link_local;
+ IPv6LinkLocalAddressGenMode ipv6ll_address_gen_mode;
+ 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;
+
+ /* DHCPv6 Prefix Delegation support */
+ int dhcp6_pd;
+ bool dhcp6_pd_announce;
+ bool dhcp6_pd_assign;
+ int64_t dhcp6_pd_subnet_id;
+ union in_addr_union dhcp6_pd_token;
+
+ /* Bridge Support */
+ int use_bpdu;
+ int hairpin;
+ 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;
+ uint32_t can_data_bitrate;
+ unsigned can_data_sample_point;
+ usec_t can_restart_us;
+ int can_triple_sampling;
+ int can_termination;
+ int can_listen_only;
+ int can_fd_mode;
+ int can_non_iso;
+
+ /* sysctl settings */
+ AddressFamily ip_forward;
+ int ipv4_accept_local;
+ 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_autonomous_prefix;
+ bool ipv6_accept_ra_use_onlink_prefix;
+ bool active_slave;
+ bool primary_slave;
+ bool ipv6_accept_ra_route_table_set;
+ DHCPUseDomains ipv6_accept_ra_use_domains;
+ IPv6AcceptRAStartDHCP6Client ipv6_accept_ra_start_dhcp6_client;
+ uint32_t ipv6_accept_ra_route_table;
+ Set *ndisc_deny_listed_prefix;
+ OrderedSet *ipv6_tokens;
+
+ /* LLDP support */
+ LLDPMode lldp_mode; /* LLDP reception */
+ LLDPEmit lldp_emit; /* LLDP transmission */
+ char *lldp_mud; /* LLDP MUD URL */
+
+ OrderedHashmap *addresses_by_section;
+ Hashmap *routes_by_section;
+ Hashmap *nexthops_by_section;
+ Hashmap *fdb_entries_by_section;
+ Hashmap *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;
+ OrderedHashmap *tc_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 network_get_by_name(Manager *manager, const char *name, Network **ret);
+int network_get(Manager *manager, unsigned short iftype, sd_device *device,
+ const char *ifname, char * const *alternative_names, const char *driver,
+ const struct ether_addr *mac, const struct ether_addr *permanent_mac,
+ enum nl80211_iftype wlan_iftype, const char *ssid, const struct ether_addr *bssid,
+ Network **ret);
+int network_apply(Network *network, Link *link);
+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_hostname);
+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_keep_configuration);
+CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_link_local_address_gen_mode);
+
+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* ipv6_link_local_address_gen_mode_to_string(IPv6LinkLocalAddressGenMode s) _const_;
+IPv6LinkLocalAddressGenMode ipv6_link_local_address_gen_mode_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..4a09b4c
--- /dev/null
+++ b/src/network/networkd-nexthop.c
@@ -0,0 +1,534 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc.
+ */
+
+#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 "parse-util.h"
+#include "set.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);
+ }
+
+ network_config_section_free(nexthop->section);
+
+ if (nexthop->link) {
+ set_remove(nexthop->link->nexthops, nexthop);
+ set_remove(nexthop->link->nexthops_foreign, nexthop);
+ }
+
+ return mfree(nexthop);
+}
+
+DEFINE_NETWORK_SECTION_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,
+ };
+
+ *ret = TAKE_PTR(nexthop);
+
+ return 0;
+}
+
+static int nexthop_new_static(Network *network, const char *filename, unsigned section_line, NextHop **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(nexthop_freep) NextHop *nexthop = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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);
+
+ r = hashmap_ensure_allocated(&network->nexthops_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->nexthops_by_section, 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->id, sizeof(nexthop->id), 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->id, b->id);
+ 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_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ nexthop_hash_ops,
+ NextHop,
+ nexthop_hash_func,
+ nexthop_compare_func,
+ nexthop_free);
+
+static int nexthop_get(Link *link, NextHop *in, NextHop **ret) {
+ NextHop *existing;
+
+ assert(link);
+ assert(in);
+
+ existing = set_get(link->nexthops, in);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 1;
+ }
+
+ existing = set_get(link->nexthops_foreign, in);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int nexthop_add_internal(Link *link, Set **nexthops, NextHop *in, NextHop **ret) {
+ _cleanup_(nexthop_freep) NextHop *nexthop = NULL;
+ int r;
+
+ assert(link);
+ assert(nexthops);
+ assert(in);
+
+ r = nexthop_new(&nexthop);
+ if (r < 0)
+ return r;
+
+ nexthop->id = in->id;
+ nexthop->family = in->family;
+ nexthop->gw = in->gw;
+
+ r = set_ensure_put(nexthops, &nexthop_hash_ops, nexthop);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ nexthop->link = link;
+
+ if (ret)
+ *ret = nexthop;
+
+ nexthop = NULL;
+
+ return 0;
+}
+
+static int nexthop_add_foreign(Link *link, NextHop *in, NextHop **ret) {
+ return nexthop_add_internal(link, &link->nexthops_foreign, in, ret);
+}
+
+static int nexthop_add(Link *link, NextHop *in, NextHop **ret) {
+ NextHop *nexthop;
+ int r;
+
+ r = nexthop_get(link, in, &nexthop);
+ if (r == -ENOENT) {
+ /* NextHop does not exist, create a new one */
+ r = nexthop_add_internal(link, &link->nexthops, in, &nexthop);
+ if (r < 0)
+ return r;
+ } else if (r == 0) {
+ /* Take over a foreign nexthop */
+ r = set_ensure_put(&link->nexthops, &nexthop_hash_ops, nexthop);
+ if (r < 0)
+ return r;
+
+ set_remove(link->nexthops_foreign, nexthop);
+ } else if (r == 1) {
+ /* NextHop exists, do nothing */
+ ;
+ } else
+ return r;
+
+ if (ret)
+ *ret = nexthop;
+
+ return 0;
+}
+
+static int nexthop_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->nexthop_messages > 0);
+
+ link->nexthop_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ 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->nexthop_messages == 0) {
+ log_link_debug(link, "Nexthop set");
+ link->static_nexthops_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int nexthop_configure(NextHop *nexthop, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+ assert(IN_SET(nexthop->family, AF_INET, AF_INET6));
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *gw = NULL;
+
+ if (!in_addr_is_null(nexthop->family, &nexthop->gw))
+ (void) in_addr_to_string(nexthop->family, &nexthop->gw, &gw);
+
+ log_link_debug(link, "Configuring nexthop: gw: %s", strna(gw));
+ }
+
+ r = sd_rtnl_message_new_nexthop(link->manager->rtnl, &req,
+ RTM_NEWNEXTHOP, nexthop->family,
+ nexthop->protocol);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create RTM_NEWNEXTHOP 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 = sd_netlink_message_append_u32(req, NHA_OIF, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NHA_OIF attribute: %m");
+
+ if (in_addr_is_null(nexthop->family, &nexthop->gw) == 0) {
+ r = netlink_message_append_in_addr_union(req, NHA_GATEWAY, nexthop->family, &nexthop->gw);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append NHA_GATEWAY attribute: %m");
+
+ r = sd_rtnl_message_nexthop_set_family(req, nexthop->family);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set nexthop family: %m");
+ }
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, nexthop_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);
+
+ r = nexthop_add(link, nexthop, &nexthop);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not add nexthop: %m");
+
+ return 1;
+}
+
+int link_set_nexthop(Link *link) {
+ NextHop *nh;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->static_nexthops_configured = false;
+
+ HASHMAP_FOREACH(nh, link->network->nexthops_by_section) {
+ r = nexthop_configure(nh, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set nexthop: %m");
+
+ link->nexthop_messages++;
+ }
+
+ if (link->nexthop_messages == 0) {
+ link->static_nexthops_configured = true;
+ link_check_ready(link);
+ } else {
+ log_link_debug(link, "Setting nexthop");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 1;
+}
+
+int manager_rtnl_process_nexthop(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(nexthop_freep) NextHop *tmp = NULL;
+ _cleanup_free_ char *gateway = NULL;
+ NextHop *nexthop = NULL;
+ uint32_t ifindex;
+ uint16_t type;
+ Link *link;
+ 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 == -ENODATA) {
+ log_warning_errno(r, "rtnl: received nexthop message without NHA_OIF attribute, ignoring: %m");
+ return 0;
+ } else if (r < 0) {
+ log_warning_errno(r, "rtnl: could not get NHA_OIF attribute, ignoring: %m");
+ return 0;
+ } else if (ifindex <= 0) {
+ log_warning("rtnl: received nexthop message with invalid ifindex %"PRIu32", ignoring.", ifindex);
+ return 0;
+ }
+
+ r = link_get(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_INET, AF_INET6))
+ return log_link_debug(link, "rtnl: received nexthop message with invalid family %d, ignoring.", tmp->family);
+
+ 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_read_u32(message, NHA_ID, &tmp->id);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: could not get NHA_ID attribute, ignoring: %m");
+ return 0;
+ }
+
+ (void) nexthop_get(link, tmp, &nexthop);
+
+ if (DEBUG_LOGGING)
+ (void) in_addr_to_string(tmp->family, &tmp->gw, &gateway);
+
+ switch (type) {
+ case RTM_NEWNEXTHOP:
+ if (nexthop)
+ log_link_debug(link, "Received remembered nexthop: %s, id: %d", strna(gateway), tmp->id);
+ else {
+ log_link_debug(link, "Remembering foreign nexthop: %s, id: %d", strna(gateway), tmp->id);
+ r = nexthop_add_foreign(link, tmp, &nexthop);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Could not remember foreign nexthop, ignoring: %m");
+ return 0;
+ }
+ }
+ break;
+ case RTM_DELNEXTHOP:
+ if (nexthop) {
+ log_link_debug(link, "Forgetting nexthop: %s, id: %d", strna(gateway), tmp->id);
+ nexthop_free(nexthop);
+ } else
+ log_link_debug(link, "Kernel removed a nexthop we don't remember: %s, id: %d, ignoring.",
+ strna(gateway), tmp->id);
+ break;
+
+ default:
+ assert_not_reached("Received invalid RTNL message type");
+ }
+
+ return 1;
+}
+
+static int nexthop_section_verify(NextHop *nh) {
+ if (section_is_invalid(nh->section))
+ return -EINVAL;
+
+ if (in_addr_is_null(nh->family, &nh->gw) < 0)
+ return -EINVAL;
+
+ 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;
+ 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 = safe_atou32(rvalue, &n->id);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse nexthop id \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+
+ 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();
+
+ 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;
+}
diff --git a/src/network/networkd-nexthop.h b/src/network/networkd-nexthop.h
new file mode 100644
index 0000000..75714e7
--- /dev/null
+++ b/src/network/networkd-nexthop.h
@@ -0,0 +1,41 @@
+/* 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 "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;
+ NetworkConfigSection *section;
+
+ Link *link;
+
+ unsigned char protocol;
+
+ uint32_t id;
+ int family;
+ union in_addr_union gw;
+} NextHop;
+
+NextHop *nexthop_free(NextHop *nexthop);
+
+void network_drop_invalid_nexthops(Network *network);
+
+int link_set_nexthop(Link *link);
+
+int manager_rtnl_process_nexthop(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_id);
+CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_gateway);
diff --git a/src/network/networkd-radv.c b/src/network/networkd-radv.c
new file mode 100644
index 0000000..a8e1b2b
--- /dev/null
+++ b/src/network/networkd-radv.c
@@ -0,0 +1,999 @@
+/* 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-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-radv.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "string-table.h"
+#include "strv.h"
+
+Prefix *prefix_free(Prefix *prefix) {
+ if (!prefix)
+ return NULL;
+
+ if (prefix->network) {
+ assert(prefix->section);
+ hashmap_remove(prefix->network->prefixes_by_section, prefix->section);
+ }
+
+ network_config_section_free(prefix->section);
+ sd_radv_prefix_unref(prefix->radv_prefix);
+
+ return mfree(prefix);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(Prefix, prefix_free);
+
+static int prefix_new(Prefix **ret) {
+ _cleanup_(prefix_freep) Prefix *prefix = NULL;
+
+ prefix = new0(Prefix, 1);
+ if (!prefix)
+ return -ENOMEM;
+
+ if (sd_radv_prefix_new(&prefix->radv_prefix) < 0)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(prefix);
+
+ return 0;
+}
+
+static int prefix_new_static(Network *network, const char *filename, unsigned section_line, Prefix **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(prefix_freep) Prefix *prefix = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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;
+ }
+
+ r = prefix_new(&prefix);
+ if (r < 0)
+ return r;
+
+ prefix->network = network;
+ prefix->section = TAKE_PTR(n);
+
+ r = hashmap_ensure_allocated(&network->prefixes_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->prefixes_by_section, 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);
+ }
+
+ network_config_section_free(prefix->section);
+ sd_radv_route_prefix_unref(prefix->radv_route_prefix);
+
+ return mfree(prefix);
+}
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(RoutePrefix, route_prefix_free);
+
+static int route_prefix_new(RoutePrefix **ret) {
+ _cleanup_(route_prefix_freep) RoutePrefix *prefix = NULL;
+
+ prefix = new0(RoutePrefix, 1);
+ if (!prefix)
+ return -ENOMEM;
+
+ if (sd_radv_route_prefix_new(&prefix->radv_route_prefix) < 0)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(prefix);
+
+ return 0;
+}
+
+static int route_prefix_new_static(Network *network, const char *filename, unsigned section_line, RoutePrefix **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(route_prefix_freep) RoutePrefix *prefix = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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;
+ }
+
+ r = route_prefix_new(&prefix);
+ if (r < 0)
+ return r;
+
+ prefix->network = network;
+ prefix->section = TAKE_PTR(n);
+
+ r = hashmap_ensure_allocated(&network->route_prefixes_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->route_prefixes_by_section, prefix->section, prefix);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(prefix);
+
+ return 0;
+}
+
+void network_drop_invalid_prefixes(Network *network) {
+ Prefix *prefix;
+
+ assert(network);
+
+ HASHMAP_FOREACH(prefix, network->prefixes_by_section)
+ if (section_is_invalid(prefix->section))
+ prefix_free(prefix);
+}
+
+void network_drop_invalid_route_prefixes(Network *network) {
+ RoutePrefix *prefix;
+
+ assert(network);
+
+ HASHMAP_FOREACH(prefix, network->route_prefixes_by_section)
+ if (section_is_invalid(prefix->section))
+ route_prefix_free(prefix);
+}
+
+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->dhcp6_pd < 0)
+ /* For backward compatibility. */
+ network->dhcp6_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);
+ }
+}
+
+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) {
+
+ Network *network = userdata;
+ _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ uint8_t prefixlen = 64;
+ union in_addr_union in6addr;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = prefix_new_static(network, filename, section_line, &p);
+ if (r < 0)
+ return log_oom();
+
+ r = in_addr_prefix_from_string(rvalue, AF_INET6, &in6addr, &prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Prefix is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ r = sd_radv_prefix_set_prefix(p->radv_prefix, &in6addr.in6, prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to set radv prefix, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ p = NULL;
+
+ return 0;
+}
+
+int config_parse_prefix_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_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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"))
+ r = sd_radv_prefix_set_onlink(p->radv_prefix, r);
+ else if (streq(lvalue, "AddressAutoconfiguration"))
+ r = sd_radv_prefix_set_address_autoconfiguration(p->radv_prefix, r);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to set %s=, ignoring assignment: %m", lvalue);
+ return 0;
+ }
+
+ p = NULL;
+
+ 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) {
+
+ Network *network = userdata;
+ _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ usec_t usec;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ }
+
+ /* a value of 0xffffffff represents infinity */
+ if (streq(lvalue, "PreferredLifetimeSec"))
+ r = sd_radv_prefix_set_preferred_lifetime(p->radv_prefix,
+ DIV_ROUND_UP(usec, USEC_PER_SEC));
+ else if (streq(lvalue, "ValidLifetimeSec"))
+ r = sd_radv_prefix_set_valid_lifetime(p->radv_prefix,
+ DIV_ROUND_UP(usec, USEC_PER_SEC));
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to set %s=, ignoring assignment: %m", lvalue);
+ return 0;
+ }
+
+ p = NULL;
+
+ return 0;
+}
+
+int config_parse_prefix_assign(
+ 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_(prefix_free_or_set_invalidp) Prefix *p = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ }
+
+ p->assign = r;
+ p = NULL;
+
+ 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) {
+
+ Network *network = userdata;
+ _cleanup_(route_prefix_free_or_set_invalidp) RoutePrefix *p = NULL;
+ uint8_t prefixlen = 64;
+ union in_addr_union in6addr;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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, &in6addr, &prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Route prefix is invalid, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ r = sd_radv_prefix_set_route_prefix(p->radv_route_prefix, &in6addr.in6, prefixlen);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to set route prefix, ignoring assignment: %m");
+ return 0;
+ }
+
+ p = NULL;
+
+ 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) {
+
+ Network *network = userdata;
+ _cleanup_(route_prefix_free_or_set_invalidp) RoutePrefix *p = NULL;
+ usec_t usec;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ }
+
+ /* a value of 0xffffffff represents infinity */
+ r = sd_radv_route_prefix_set_lifetime(p->radv_route_prefix, DIV_ROUND_UP(usec, USEC_PER_SEC));
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to set route lifetime, ignoring assignment: %m");
+ return 0;
+ }
+
+ p = NULL;
+
+ return 0;
+}
+
+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, n_allocated = 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_allocated, 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;
+ usec_t lifetime_usec;
+ 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_IS_ADDR_UNSPECIFIED(&link->network->router_dns[i])) {
+ if (!IN6_IS_ADDR_UNSPECIFIED(&link->ipv6ll_address))
+ *(p++) = link->ipv6ll_address;
+ } else
+ *(p++) = link->network->router_dns[i];
+
+ n_dns = p - dns;
+ lifetime_usec = link->network->router_dns_lifetime_usec;
+
+ goto set_dns;
+ }
+
+ lifetime_usec = SD_RADV_DEFAULT_DNS_LIFETIME_USEC;
+
+ r = network_get_ipv6_dns(link->network, &dns, &n_dns);
+ if (r > 0)
+ goto set_dns;
+
+ if (uplink) {
+ if (!uplink->network) {
+ log_link_debug(uplink, "Cannot fetch DNS servers as uplink interface is not managed by us");
+ return 0;
+ }
+
+ 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,
+ DIV_ROUND_UP(lifetime_usec, USEC_PER_SEC),
+ dns, n_dns);
+}
+
+static int radv_set_domains(Link *link, Link *uplink) {
+ OrderedSet *search_domains;
+ usec_t lifetime_usec;
+ _cleanup_free_ char **s = NULL; /* just free() because the strings are owned by the set */
+
+ if (!link->network->router_emit_domains)
+ return 0;
+
+ search_domains = link->network->router_search_domains;
+ lifetime_usec = link->network->router_dns_lifetime_usec;
+
+ if (search_domains)
+ goto set_domains;
+
+ lifetime_usec = SD_RADV_DEFAULT_DNS_LIFETIME_USEC;
+
+ search_domains = link->network->search_domains;
+ if (search_domains)
+ goto set_domains;
+
+ if (uplink) {
+ if (!uplink->network) {
+ log_link_debug(uplink, "Cannot fetch DNS search domains as uplink interface is not managed by us");
+ return 0;
+ }
+
+ 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,
+ DIV_ROUND_UP(lifetime_usec, USEC_PER_SEC),
+ s);
+
+}
+
+int radv_emit_dns(Link *link) {
+ Link *uplink;
+ int r;
+
+ uplink = manager_find_uplink(link->manager, link);
+
+ r = radv_set_dns(link, uplink);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not set RA DNS: %m");
+
+ r = radv_set_domains(link, uplink);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not set RA Domains: %m");
+
+ return 0;
+}
+
+static bool link_radv_enabled(Link *link) {
+ assert(link);
+
+ if (!link_ipv6ll_enabled(link))
+ return false;
+
+ return link->network->router_prefix_delegation;
+}
+
+int radv_configure(Link *link) {
+ uint16_t router_lifetime;
+ RoutePrefix *q;
+ Prefix *p;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (!link_radv_enabled(link))
+ return 0;
+
+ 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;
+
+ r = sd_radv_set_mac(link->radv, &link->hw_addr.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;
+
+ /* a value of UINT16_MAX represents infinity, 0x0 means this host is not a router */
+ if (link->network->router_lifetime_usec == USEC_INFINITY)
+ router_lifetime = UINT16_MAX;
+ else if (link->network->router_lifetime_usec > (UINT16_MAX - 1) * USEC_PER_SEC)
+ router_lifetime = UINT16_MAX - 1;
+ else
+ router_lifetime = DIV_ROUND_UP(link->network->router_lifetime_usec, USEC_PER_SEC);
+
+ r = sd_radv_set_router_lifetime(link->radv, router_lifetime);
+ if (r < 0)
+ return r;
+
+ if (router_lifetime > 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 = sd_radv_add_prefix(link->radv, p->radv_prefix, false);
+ if (r == -EEXIST)
+ continue;
+ if (r == -ENOEXEC) {
+ log_link_warning_errno(link, r, "[IPv6Prefix] section configured without Prefix= setting, ignoring section.");
+ continue;
+ }
+ if (r < 0)
+ return r;
+ }
+
+ HASHMAP_FOREACH(q, link->network->route_prefixes_by_section) {
+ r = sd_radv_add_route_prefix(link->radv, q->radv_route_prefix, false);
+ if (r == -EEXIST)
+ continue;
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int radv_update_mac(Link *link) {
+ bool restart;
+ int r;
+
+ assert(link);
+
+ if (!link->radv)
+ 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.addr.ether);
+ if (r < 0)
+ return r;
+
+ if (restart) {
+ r = sd_radv_start(link->radv);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int radv_add_prefix(
+ Link *link,
+ const struct in6_addr *prefix,
+ uint8_t prefix_len,
+ uint32_t lifetime_preferred,
+ uint32_t lifetime_valid) {
+
+ _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, lifetime_preferred);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_prefix_set_valid_lifetime(p, lifetime_valid);
+ if (r < 0)
+ return r;
+
+ r = sd_radv_add_prefix(link->radv, p, true);
+ if (r < 0 && r != -EEXIST)
+ return r;
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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, 0,
+ "Invalid %s= setting, ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ *ra = val;
+ 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..4dfbefe
--- /dev/null
+++ b/src/network/networkd-radv.h
@@ -0,0 +1,70 @@
+/* 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 Network Network;
+typedef struct Link Link;
+
+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 = -1,
+} RADVPrefixDelegation;
+
+typedef struct Prefix {
+ Network *network;
+ NetworkConfigSection *section;
+
+ sd_radv_prefix *radv_prefix;
+
+ bool assign;
+} Prefix;
+
+typedef struct RoutePrefix {
+ Network *network;
+ NetworkConfigSection *section;
+
+ sd_radv_route_prefix *radv_route_prefix;
+} 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 radv_emit_dns(Link *link);
+int radv_configure(Link *link);
+int radv_update_mac(Link *link);
+int radv_add_prefix(Link *link, const struct in6_addr *prefix, uint8_t prefix_len,
+ uint32_t lifetime_preferred, uint32_t lifetime_valid);
+
+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_preference);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix_flags);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix_lifetime);
+CONFIG_PARSER_PROTOTYPE(config_parse_prefix_assign);
+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.c b/src/network/networkd-route.c
new file mode 100644
index 0000000..0ed8958
--- /dev/null
+++ b/src/network/networkd-route.c
@@ -0,0 +1,2537 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/icmpv6.h>
+#include <linux/ipv6_route.h>
+
+#include "alloc-util.h"
+#include "netlink-util.h"
+#include "networkd-ipv4ll.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-nexthop.h"
+#include "networkd-route.h"
+#include "networkd-routing-policy-rule.h"
+#include "parse-util.h"
+#include "socket-netlink.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "sysctl-util.h"
+#include "vrf.h"
+
+#define ROUTES_DEFAULT_MAX_PER_FAMILY 4096U
+
+static uint32_t link_get_vrf_table(const Link *link) {
+ return link->network->vrf ? VRF(link->network->vrf)->table : RT_TABLE_MAIN;
+}
+
+uint32_t link_get_dhcp_route_table(const Link *link) {
+ /* 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(const Link *link) {
+ if (link->network->ipv6_accept_ra_route_table_set)
+ return link->network->ipv6_accept_ra_route_table;
+ return link_get_vrf_table(link);
+}
+
+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_PRIVATE_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_PRIVATE_STRING_TABLE_LOOKUP(route_scope, int);
+
+#define ROUTE_SCOPE_STR_MAX CONST_MAX(DECIMAL_STR_MAX(int), STRLEN("nowhere") + 1)
+static const char *format_route_scope(int scope, char *buf, size_t size) {
+ const char *s;
+ char *p = buf;
+
+ s = route_scope_to_string(scope);
+ if (s)
+ strpcpy(&p, size, s);
+ else
+ strpcpyf(&p, size, "%d", scope);
+
+ return buf;
+}
+
+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);
+
+#define ROUTE_TABLE_STR_MAX CONST_MAX(DECIMAL_STR_MAX(int), STRLEN("default") + 1)
+static const char *format_route_table(int table, char *buf, size_t size) {
+ const char *s;
+ char *p = buf;
+
+ s = route_table_to_string(table);
+ if (s)
+ strpcpy(&p, size, s);
+ else
+ strpcpyf(&p, size, "%d", table);
+
+ return buf;
+}
+
+static const char * const route_protocol_table[] = {
+ [RTPROT_KERNEL] = "kernel",
+ [RTPROT_BOOT] = "boot",
+ [RTPROT_STATIC] = "static",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(route_protocol, int);
+
+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_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(route_protocol_full, int);
+
+#define ROUTE_PROTOCOL_STR_MAX CONST_MAX(DECIMAL_STR_MAX(int), STRLEN("redirect") + 1)
+static const char *format_route_protocol(int protocol, char *buf, size_t size) {
+ const char *s;
+ char *p = buf;
+
+ s = route_protocol_full_to_string(protocol);
+ if (s)
+ strpcpy(&p, size, s);
+ else
+ strpcpyf(&p, size, "%d", protocol);
+
+ return buf;
+}
+
+static 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("net/ipv4/route/max_size", &s4) >= 0) {
+ truncate_nl(s4);
+ 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("net/ipv6/route/max_size", &s6) >= 0) {
+ truncate_nl(s6);
+ (void) safe_atou(s6, &val6);
+ }
+
+ cached = MAX(ROUTES_DEFAULT_MAX_PER_FAMILY, val4) +
+ MAX(ROUTES_DEFAULT_MAX_PER_FAMILY, val6);
+ return cached;
+}
+
+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_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_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(route_freep) Route *route = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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);
+
+ r = hashmap_ensure_allocated(&network->routes_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->routes_by_section, 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);
+ }
+
+ network_config_section_free(route->section);
+
+ if (route->link) {
+ NDiscRoute *n;
+
+ set_remove(route->link->routes, route);
+ set_remove(route->link->routes_foreign, route);
+ set_remove(route->link->dhcp_routes, route);
+ set_remove(route->link->dhcp_routes_old, route);
+ set_remove(route->link->dhcp6_routes, route);
+ set_remove(route->link->dhcp6_routes_old, route);
+ set_remove(route->link->dhcp6_pd_routes, route);
+ set_remove(route->link->dhcp6_pd_routes_old, route);
+ SET_FOREACH(n, route->link->ndisc_routes)
+ if (n->route == route)
+ free(set_remove(route->link->ndisc_routes, n));
+ }
+
+ if (route->manager) {
+ set_remove(route->manager->routes, route);
+ set_remove(route->manager->routes_foreign, route);
+ }
+
+ ordered_set_free_free(route->multipath_routes);
+
+ sd_event_source_unref(route->expire);
+
+ return mfree(route);
+}
+
+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);
+
+ break;
+ default:
+ /* treat any other address family as AF_UNSPEC */
+ break;
+ }
+}
+
+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;
+
+ 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_equal(const Route *r1, const Route *r2) {
+ if (r1 == r2)
+ return true;
+
+ if (!r1 || !r2)
+ return false;
+
+ return route_compare_func(r1, r2) == 0;
+}
+
+static int route_get(const Manager *manager, const Link *link, const Route *in, Route **ret) {
+ Route *existing;
+
+ assert(manager || link);
+ assert(in);
+
+ if (link) {
+ existing = set_get(link->routes, in);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 1;
+ }
+
+ existing = set_get(link->routes_foreign, in);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 0;
+ }
+ } else {
+ existing = set_get(manager->routes, in);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 1;
+ }
+
+ existing = set_get(manager->routes_foreign, in);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 0;
+ }
+ }
+
+ return -ENOENT;
+}
+
+static void route_copy(Route *dest, const Route *src, const MultipathRoute *m) {
+ assert(dest);
+ assert(src);
+
+ dest->family = src->family;
+ dest->src = src->src;
+ dest->src_prefixlen = src->src_prefixlen;
+ dest->dst = src->dst;
+ dest->dst_prefixlen = src->dst_prefixlen;
+ dest->prefsrc = src->prefsrc;
+ dest->scope = src->scope;
+ dest->protocol = src->protocol;
+ dest->type = src->type;
+ dest->tos = src->tos;
+ dest->priority = src->priority;
+ dest->table = src->table;
+ dest->initcwnd = src->initcwnd;
+ dest->initrwnd = src->initrwnd;
+ dest->lifetime = src->lifetime;
+
+ if (m) {
+ dest->gw_family = m->gateway.family;
+ dest->gw = m->gateway.address;
+ dest->gw_weight = m->weight;
+ } else {
+ dest->gw_family = src->gw_family;
+ dest->gw = src->gw;
+ dest->gw_weight = src->gw_weight;
+ }
+}
+
+static int route_add_internal(Manager *manager, Link *link, Set **routes, const Route *in, Route **ret) {
+ _cleanup_(route_freep) Route *route = NULL;
+ int r;
+
+ assert(manager || link);
+ assert(routes);
+ assert(in);
+
+ r = route_new(&route);
+ if (r < 0)
+ return r;
+
+ route_copy(route, in, NULL);
+
+ r = set_ensure_put(routes, &route_hash_ops, route);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ route->link = link;
+ route->manager = manager;
+
+ if (ret)
+ *ret = route;
+
+ route = NULL;
+
+ return 0;
+}
+
+static int route_add_foreign(Manager *manager, Link *link, const Route *in, Route **ret) {
+ assert(manager || link);
+ return route_add_internal(manager, link, link ? &link->routes_foreign : &manager->routes_foreign, in, ret);
+}
+
+static int route_add(Manager *manager, Link *link, const Route *in, const MultipathRoute *m, Route **ret) {
+ _cleanup_(route_freep) Route *tmp = NULL;
+ Route *route;
+ int r;
+
+ assert(manager || link);
+ assert(in);
+
+ if (m) {
+ assert(link && (m->ifindex == 0 || m->ifindex == link->ifindex));
+
+ r = route_new(&tmp);
+ if (r < 0)
+ return r;
+
+ route_copy(tmp, in, m);
+ in = tmp;
+ }
+
+ r = route_get(manager, link, in, &route);
+ if (r == -ENOENT) {
+ /* Route does not exist, create a new one */
+ r = route_add_internal(manager, link, link ? &link->routes : &manager->routes, in, &route);
+ if (r < 0)
+ return r;
+ } else if (r == 0) {
+ /* Take over a foreign route */
+ if (link) {
+ r = set_ensure_put(&link->routes, &route_hash_ops, route);
+ if (r < 0)
+ return r;
+
+ set_remove(link->routes_foreign, route);
+ } else {
+ r = set_ensure_put(&manager->routes, &route_hash_ops, route);
+ if (r < 0)
+ return r;
+
+ set_remove(manager->routes_foreign, route);
+ }
+ } else if (r == 1) {
+ /* Route exists, do nothing */
+ ;
+ } else
+ return r;
+
+ if (ret)
+ *ret = route;
+
+ return 0;
+}
+
+static int route_set_netlink_message(const Route *route, sd_netlink_message *req, Link *link) {
+ unsigned flags;
+ int r;
+
+ assert(route);
+ assert(req);
+
+ /* link may be NULL */
+
+ if (in_addr_is_null(route->gw_family, &route->gw) == 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 log_link_error_errno(link, r, "Could not append RTA_GATEWAY attribute: %m");
+ } 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 log_link_error_errno(link, r, "Could not append RTA_VIA attribute: %m");
+ }
+ }
+
+ if (route->dst_prefixlen > 0) {
+ r = netlink_message_append_in_addr_union(req, RTA_DST, route->family, &route->dst);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_DST attribute: %m");
+
+ r = sd_rtnl_message_route_set_dst_prefixlen(req, route->dst_prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set destination prefix length: %m");
+ }
+
+ if (route->src_prefixlen > 0) {
+ r = netlink_message_append_in_addr_union(req, RTA_SRC, route->family, &route->src);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_SRC attribute: %m");
+
+ r = sd_rtnl_message_route_set_src_prefixlen(req, route->src_prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set source prefix length: %m");
+ }
+
+ if (in_addr_is_null(route->family, &route->prefsrc) == 0) {
+ r = netlink_message_append_in_addr_union(req, RTA_PREFSRC, route->family, &route->prefsrc);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_PREFSRC attribute: %m");
+ }
+
+ r = sd_rtnl_message_route_set_scope(req, route->scope);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set scope: %m");
+
+ flags = route->flags;
+ if (route->gateway_onlink >= 0)
+ SET_FLAG(flags, RTNH_F_ONLINK, route->gateway_onlink);
+
+ r = sd_rtnl_message_route_set_flags(req, flags);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set flags: %m");
+
+ if (route->table != RT_TABLE_MAIN) {
+ if (route->table < 256) {
+ r = sd_rtnl_message_route_set_table(req, route->table);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set route table: %m");
+ } else {
+ r = sd_rtnl_message_route_set_table(req, RT_TABLE_UNSPEC);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set route table: %m");
+
+ /* Table attribute to allow more than 256. */
+ r = sd_netlink_message_append_data(req, RTA_TABLE, &route->table, sizeof(route->table));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_TABLE attribute: %m");
+ }
+ }
+
+ r = sd_rtnl_message_route_set_type(req, route->type);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set route type: %m");
+
+ if (!IN_SET(route->type, RTN_UNREACHABLE, RTN_PROHIBIT, RTN_BLACKHOLE, RTN_THROW)) {
+ 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 log_link_error_errno(link, r, "Could not append RTA_OIF attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u8(req, RTA_PREF, route->pref);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_PREF attribute: %m");
+
+ r = sd_netlink_message_append_u32(req, RTA_PRIORITY, route->priority);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_PRIORITY attribute: %m");
+
+ return 0;
+}
+
+static int route_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+
+ /* Note that 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 != -ESRCH)
+ log_link_message_warning_errno(link, m, r, "Could not drop route, ignoring");
+
+ return 1;
+}
+
+int route_remove(
+ const Route *route,
+ Manager *manager,
+ Link *link,
+ link_netlink_message_handler_t callback) {
+
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link || manager);
+ assert(IN_SET(route->family, AF_INET, AF_INET6));
+
+ if (!manager)
+ manager = link->manager;
+ /* link may be NULL! */
+
+ 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 RTM_DELROUTE message: %m");
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *dst = NULL, *dst_prefixlen = NULL, *src = NULL, *gw = NULL, *prefsrc = NULL;
+ char scope[ROUTE_SCOPE_STR_MAX], table[ROUTE_TABLE_STR_MAX], protocol[ROUTE_PROTOCOL_STR_MAX];
+
+ if (!in_addr_is_null(route->family, &route->dst)) {
+ (void) in_addr_to_string(route->family, &route->dst, &dst);
+ (void) asprintf(&dst_prefixlen, "/%u", route->dst_prefixlen);
+ }
+ if (!in_addr_is_null(route->family, &route->src))
+ (void) in_addr_to_string(route->family, &route->src, &src);
+ if (!in_addr_is_null(route->gw_family, &route->gw))
+ (void) in_addr_to_string(route->gw_family, &route->gw, &gw);
+ if (!in_addr_is_null(route->family, &route->prefsrc))
+ (void) in_addr_to_string(route->family, &route->prefsrc, &prefsrc);
+
+ log_link_debug(link, "Removing route: dst: %s%s, src: %s, gw: %s, prefsrc: %s, scope: %s, table: %s, proto: %s, type: %s",
+ strna(dst), strempty(dst_prefixlen), strna(src), strna(gw), strna(prefsrc),
+ format_route_scope(route->scope, scope, sizeof(scope)),
+ format_route_table(route->table, table, sizeof(table)),
+ format_route_protocol(route->protocol, protocol, sizeof(protocol)),
+ strna(route_type_to_string(route->type)));
+ }
+
+ r = route_set_netlink_message(route, req, link);
+ if (r < 0)
+ return r;
+
+ r = netlink_call_async(manager->rtnl, NULL, req,
+ callback ?: route_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); /* link may be NULL, link_ref() is OK with that */
+
+ return 0;
+}
+
+static bool link_has_route(const Link *link, const Route *route) {
+ Route *net_route;
+
+ assert(link);
+ assert(route);
+
+ if (!link->network)
+ return false;
+
+ HASHMAP_FOREACH(net_route, link->network->routes_by_section)
+ if (route_equal(net_route, route))
+ return true;
+
+ return false;
+}
+
+static bool links_have_route(Manager *manager, const Route *route, const Link *except) {
+ Link *link;
+
+ assert(manager);
+
+ HASHMAP_FOREACH(link, manager->links) {
+ if (link == except)
+ continue;
+
+ if (link_has_route(link, route))
+ return true;
+ }
+
+ return false;
+}
+
+static int manager_drop_foreign_routes(Manager *manager) {
+ Route *route;
+ int k, r = 0;
+
+ assert(manager);
+
+ SET_FOREACH(route, manager->routes_foreign) {
+ /* do not touch routes managed by the kernel */
+ if (route->protocol == RTPROT_KERNEL)
+ continue;
+
+ if (links_have_route(manager, route, NULL))
+ /* The route will be configured later. */
+ continue;
+
+ /* The existing links do not have the route. Let's drop this now. It may by
+ * re-configured later. */
+ k = route_remove(route, manager, NULL, NULL);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int manager_drop_routes(Manager *manager, Link *except) {
+ Route *route;
+ int k, r = 0;
+
+ assert(manager);
+
+ SET_FOREACH(route, manager->routes) {
+ /* do not touch routes managed by the kernel */
+ if (route->protocol == RTPROT_KERNEL)
+ continue;
+
+ if (links_have_route(manager, route, except))
+ /* The route will be configured later. */
+ continue;
+
+ /* The existing links do not have the route. Let's drop this now. It may by
+ * re-configured later. */
+ k = route_remove(route, manager, NULL, NULL);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+int link_drop_foreign_routes(Link *link) {
+ Route *route;
+ int k, r = 0;
+
+ assert(link);
+ assert(link->manager);
+
+ SET_FOREACH(route, link->routes_foreign) {
+ /* do not touch routes managed by the kernel */
+ if (route->protocol == RTPROT_KERNEL)
+ continue;
+
+ /* do not touch multicast route added by kernel */
+ /* FIXME: Why the kernel adds this route with protocol RTPROT_BOOT??? We need to investigate that.
+ * https://tools.ietf.org/html/rfc4862#section-5.4 may explain why. */
+ if (route->protocol == RTPROT_BOOT &&
+ route->family == AF_INET6 &&
+ route->dst_prefixlen == 8 &&
+ in_addr_equal(AF_INET6, &route->dst, &(union in_addr_union) { .in6 = {{{ 0xff,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 }}} }))
+ continue;
+
+ if (route->protocol == RTPROT_STATIC && link->network &&
+ FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_STATIC))
+ continue;
+
+ if (route->protocol == RTPROT_DHCP && link->network &&
+ FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP))
+ continue;
+
+ if (link_has_route(link, route))
+ k = route_add(NULL, link, route, NULL, NULL);
+ else
+ k = route_remove(route, NULL, link, NULL);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ k = manager_drop_foreign_routes(link->manager);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ return r;
+}
+
+int link_drop_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->protocol == RTPROT_KERNEL)
+ continue;
+
+ k = route_remove(route, NULL, link, NULL);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ k = manager_drop_routes(link->manager, link);
+ if (k < 0 && r >= 0)
+ r = k;
+
+ return r;
+}
+
+static int route_expire_handler(sd_event_source *s, uint64_t usec, void *userdata) {
+ Route *route = userdata;
+ int r;
+
+ assert(route);
+
+ r = route_remove(route, route->manager, route->link, NULL);
+ if (r < 0) {
+ log_link_warning_errno(route->link, r, "Could not remove route: %m");
+ route_free(route);
+ }
+
+ return 1;
+}
+
+static int route_add_and_setup_timer(Link *link, const Route *route, const MultipathRoute *m, Route **ret) {
+ _cleanup_(sd_event_source_unrefp) sd_event_source *expire = NULL;
+ Route *nr;
+ int r;
+
+ assert(link);
+ assert(route);
+
+ if (IN_SET(route->type, RTN_UNREACHABLE, RTN_PROHIBIT, RTN_BLACKHOLE, RTN_THROW))
+ r = route_add(link->manager, NULL, route, NULL, &nr);
+ else if (!m || m->ifindex == 0 || m->ifindex == link->ifindex)
+ r = route_add(NULL, link, route, m, &nr);
+ else {
+ Link *link_gw;
+
+ r = link_get(link->manager, m->ifindex, &link_gw);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Failed to get link with ifindex %d: %m", m->ifindex);
+
+ r = route_add(NULL, link_gw, route, m, &nr);
+ }
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not add route: %m");
+
+ /* TODO: drop expiration handling once it can be pushed into the kernel */
+ if (nr->lifetime != USEC_INFINITY && !kernel_route_expiration_supported()) {
+ r = sd_event_add_time(link->manager->event, &expire, clock_boottime_or_monotonic(),
+ nr->lifetime, 0, route_expire_handler, nr);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not arm expiration timer: %m");
+ }
+
+ sd_event_source_unref(nr->expire);
+ nr->expire = TAKE_PTR(expire);
+
+ if (ret)
+ *ret = nr;
+
+ return 0;
+}
+
+static int append_nexthop_one(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,
+ .rtnh_hops = m->weight > 0 ? m->weight - 1 : 0,
+ };
+
+ (*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 Route *route, sd_netlink_message *req) {
+ _cleanup_free_ struct rtattr *rta = NULL;
+ struct rtnexthop *rtnh;
+ MultipathRoute *m;
+ size_t offset;
+ int r;
+
+ 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(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(
+ const Route *route,
+ Link *link,
+ link_netlink_message_handler_t callback,
+ Route **ret) {
+
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+ assert(IN_SET(route->family, AF_INET, AF_INET6));
+ assert(callback);
+
+ if (route_get(link->manager, link, route, NULL) <= 0 &&
+ set_size(link->routes) >= routes_max())
+ return log_link_error_errno(link, SYNTHETIC_ERRNO(E2BIG),
+ "Too many routes are configured, refusing: %m");
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *dst = NULL, *dst_prefixlen = NULL, *src = NULL, *gw = NULL, *prefsrc = NULL;
+ char scope[ROUTE_SCOPE_STR_MAX], table[ROUTE_TABLE_STR_MAX], protocol[ROUTE_PROTOCOL_STR_MAX];
+
+ if (!in_addr_is_null(route->family, &route->dst)) {
+ (void) in_addr_to_string(route->family, &route->dst, &dst);
+ (void) asprintf(&dst_prefixlen, "/%u", route->dst_prefixlen);
+ }
+ if (!in_addr_is_null(route->family, &route->src))
+ (void) in_addr_to_string(route->family, &route->src, &src);
+ if (!in_addr_is_null(route->gw_family, &route->gw))
+ (void) in_addr_to_string(route->gw_family, &route->gw, &gw);
+ if (!in_addr_is_null(route->family, &route->prefsrc))
+ (void) in_addr_to_string(route->family, &route->prefsrc, &prefsrc);
+
+ log_link_debug(link, "Configuring route: dst: %s%s, src: %s, gw: %s, prefsrc: %s, scope: %s, table: %s, proto: %s, type: %s",
+ strna(dst), strempty(dst_prefixlen), strna(src), strna(gw), strna(prefsrc),
+ format_route_scope(route->scope, scope, sizeof(scope)),
+ format_route_table(route->table, table, sizeof(table)),
+ format_route_protocol(route->protocol, protocol, sizeof(protocol)),
+ strna(route_type_to_string(route->type)));
+ }
+
+ r = sd_rtnl_message_new_route(link->manager->rtnl, &req,
+ RTM_NEWROUTE, route->family,
+ route->protocol);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create RTM_NEWROUTE message: %m");
+
+ r = route_set_netlink_message(route, req, link);
+ if (r < 0)
+ return r;
+
+ if (route->lifetime != USEC_INFINITY && kernel_route_expiration_supported()) {
+ r = sd_netlink_message_append_u32(req, RTA_EXPIRES,
+ DIV_ROUND_UP(usec_sub_unsigned(route->lifetime, now(clock_boottime_or_monotonic())), USEC_PER_SEC));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_EXPIRES attribute: %m");
+ }
+
+ if (route->ttl_propagate >= 0) {
+ r = sd_netlink_message_append_u8(req, RTA_TTL_PROPAGATE, route->ttl_propagate);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_TTL_PROPAGATE attribute: %m");
+ }
+
+ r = sd_netlink_message_open_container(req, RTA_METRICS);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_METRICS attribute: %m");
+
+ if (route->mtu > 0) {
+ r = sd_netlink_message_append_u32(req, RTAX_MTU, route->mtu);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTAX_MTU attribute: %m");
+ }
+
+ if (route->initcwnd > 0) {
+ r = sd_netlink_message_append_u32(req, RTAX_INITCWND, route->initcwnd);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTAX_INITCWND attribute: %m");
+ }
+
+ if (route->initrwnd > 0) {
+ r = sd_netlink_message_append_u32(req, RTAX_INITRWND, route->initrwnd);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTAX_INITRWND attribute: %m");
+ }
+
+ if (route->quickack >= 0) {
+ r = sd_netlink_message_append_u32(req, RTAX_QUICKACK, route->quickack);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTAX_QUICKACK attribute: %m");
+ }
+
+ if (route->fast_open_no_cookie >= 0) {
+ r = sd_netlink_message_append_u32(req, RTAX_FASTOPEN_NO_COOKIE, route->fast_open_no_cookie);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTAX_FASTOPEN_NO_COOKIE attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_METRICS attribute: %m");
+
+ r = append_nexthops(route, req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append RTA_MULTIPATH attribute: %m");
+
+ if (ordered_set_isempty(route->multipath_routes)) {
+ Route *nr;
+
+ r = route_add_and_setup_timer(link, route, NULL, &nr);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = nr;
+ } else {
+ MultipathRoute *m;
+
+ assert(!ret);
+
+ ORDERED_SET_FOREACH(m, route->multipath_routes) {
+ r = route_add_and_setup_timer(link, route, m, NULL);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, callback,
+ link_netlink_destroy_callback, link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not send rtnetlink message: %m");
+
+ link_ref(link);
+
+ return 0;
+}
+
+static int route_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->route_messages > 0);
+
+ link->route_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ 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 1;
+ }
+
+ if (link->route_messages == 0) {
+ log_link_debug(link, "Routes set");
+ link->static_routes_configured = true;
+ link_set_nexthop(link);
+ }
+
+ return 1;
+}
+
+int link_set_routes(Link *link) {
+ enum {
+ PHASE_NON_GATEWAY, /* First phase: Routes without a gateway */
+ PHASE_GATEWAY, /* Second phase: Routes with a gateway */
+ _PHASE_MAX
+ } phase;
+ Route *rt;
+ int r;
+
+ assert(link);
+ assert(link->network);
+ assert(link->state != _LINK_STATE_INVALID);
+
+ link->static_routes_configured = false;
+
+ if (!link->addresses_ready)
+ return 0;
+
+ if (!link_has_carrier(link) && !link->network->configure_without_carrier)
+ /* During configuring addresses, the link lost its carrier. As networkd is dropping
+ * the addresses now, let's not configure the routes either. */
+ return 0;
+
+ r = link_set_routing_policy_rules(link);
+ if (r < 0)
+ return r;
+
+ /* First add the routes that enable us to talk to gateways, then add in the others that need a gateway. */
+ for (phase = 0; phase < _PHASE_MAX; phase++)
+ HASHMAP_FOREACH(rt, link->network->routes_by_section) {
+ if (rt->gateway_from_dhcp_or_ra)
+ continue;
+
+ if ((in_addr_is_null(rt->gw_family, &rt->gw) && ordered_set_isempty(rt->multipath_routes)) != (phase == PHASE_NON_GATEWAY))
+ continue;
+
+ r = route_configure(rt, link, route_handler, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set routes: %m");
+
+ link->route_messages++;
+ }
+
+ if (link->route_messages == 0) {
+ link->static_routes_configured = true;
+ link_set_nexthop(link);
+ } else {
+ log_link_debug(link, "Setting routes");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+static int process_route_one(Manager *manager, Link *link, uint16_t type, const Route *tmp, const MultipathRoute *m) {
+ _cleanup_(route_freep) Route *nr = NULL;
+ Route *route = NULL;
+ int r;
+
+ assert(manager);
+ assert(tmp);
+ assert(IN_SET(type, RTM_NEWROUTE, RTM_DELROUTE));
+
+ if (m) {
+ if (link)
+ return log_link_warning_errno(link, SYNTHETIC_ERRNO(EINVAL),
+ "rtnl: received route contains both RTA_OIF and RTA_MULTIPATH, ignoring.");
+
+ if (m->ifindex <= 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "rtnl: received multipath route with invalid ifindex, ignoring.");
+
+ r = link_get(manager, m->ifindex, &link);
+ if (r < 0) {
+ log_warning_errno(r, "rtnl: received multipath route for link (%d) we do not know, ignoring: %m", m->ifindex);
+ return 0;
+ }
+
+ r = route_new(&nr);
+ if (r < 0)
+ return log_oom();
+
+ route_copy(nr, tmp, m);
+
+ tmp = nr;
+ }
+
+ (void) route_get(manager, link, tmp, &route);
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *buf_dst = NULL, *buf_dst_prefixlen = NULL,
+ *buf_src = NULL, *buf_gw = NULL, *buf_prefsrc = NULL;
+ char buf_scope[ROUTE_SCOPE_STR_MAX], buf_table[ROUTE_TABLE_STR_MAX],
+ buf_protocol[ROUTE_PROTOCOL_STR_MAX];
+
+ if (!in_addr_is_null(tmp->family, &tmp->dst)) {
+ (void) in_addr_to_string(tmp->family, &tmp->dst, &buf_dst);
+ (void) asprintf(&buf_dst_prefixlen, "/%u", tmp->dst_prefixlen);
+ }
+ if (!in_addr_is_null(tmp->family, &tmp->src))
+ (void) in_addr_to_string(tmp->family, &tmp->src, &buf_src);
+ if (!in_addr_is_null(tmp->gw_family, &tmp->gw))
+ (void) in_addr_to_string(tmp->gw_family, &tmp->gw, &buf_gw);
+ if (!in_addr_is_null(tmp->family, &tmp->prefsrc))
+ (void) in_addr_to_string(tmp->family, &tmp->prefsrc, &buf_prefsrc);
+
+ log_link_debug(link,
+ "%s route: dst: %s%s, src: %s, gw: %s, prefsrc: %s, scope: %s, table: %s, proto: %s, type: %s",
+ (!route && !manager->manage_foreign_routes) ? "Ignoring received foreign" :
+ type == RTM_DELROUTE ? "Forgetting" :
+ route ? "Received remembered" : "Remembering",
+ strna(buf_dst), strempty(buf_dst_prefixlen),
+ strna(buf_src), strna(buf_gw), strna(buf_prefsrc),
+ format_route_scope(tmp->scope, buf_scope, sizeof buf_scope),
+ format_route_table(tmp->table, buf_table, sizeof buf_table),
+ format_route_protocol(tmp->protocol, buf_protocol, sizeof buf_protocol),
+ strna(route_type_to_string(tmp->type)));
+ }
+
+ switch (type) {
+ case RTM_NEWROUTE:
+ if (!route && manager->manage_foreign_routes) {
+ /* A route appeared that we did not request */
+ r = route_add_foreign(manager, link, tmp, NULL);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to remember foreign route, ignoring: %m");
+ return 0;
+ }
+ }
+
+ break;
+
+ case RTM_DELROUTE:
+ route_free(route);
+ break;
+
+ default:
+ assert_not_reached("Received route message with invalid RTNL message type");
+ }
+
+ return 1;
+}
+
+int manager_rtnl_process_route(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_ordered_set_free_free_ OrderedSet *multipath_routes = NULL;
+ _cleanup_(route_freep) Route *tmp = NULL;
+ _cleanup_free_ void *rta_multipath = NULL;
+ Link *link = NULL;
+ uint32_t ifindex;
+ uint16_t type;
+ unsigned char table;
+ RouteVia via;
+ 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 %d, ignoring.", ifindex);
+ return 0;
+ }
+
+ r = link_get(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 (%d) 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: %m");
+ return 0;
+ }
+
+ switch (tmp->family) {
+ case AF_INET:
+ r = sd_netlink_message_read_in_addr(message, RTA_DST, &tmp->dst.in);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid destination, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_in_addr(message, RTA_GATEWAY, &tmp->gw.in);
+ 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 = AF_INET;
+
+ 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 = sd_netlink_message_read_in_addr(message, RTA_SRC, &tmp->src.in);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid source, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_in_addr(message, RTA_PREFSRC, &tmp->prefsrc.in);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid preferred source, ignoring: %m");
+ return 0;
+ }
+
+ break;
+
+ case AF_INET6:
+ r = sd_netlink_message_read_in6_addr(message, RTA_DST, &tmp->dst.in6);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid destination, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_in6_addr(message, RTA_GATEWAY, &tmp->gw.in6);
+ 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 = AF_INET6;
+
+ r = sd_netlink_message_read_in6_addr(message, RTA_SRC, &tmp->src.in6);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid source, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_in6_addr(message, RTA_PREFSRC, &tmp->prefsrc.in6);
+ if (r < 0 && r != -ENODATA) {
+ log_link_warning_errno(link, r, "rtnl: received route message without valid preferred source, ignoring: %m");
+ return 0;
+ }
+
+ break;
+
+ default:
+ assert_not_reached("Received route message with unsupported address family");
+ 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_rtnl_message_route_get_table(message, &table);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: received route message with invalid table, ignoring: %m");
+ return 0;
+ }
+ tmp->table = table;
+
+ 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_enter_container(message, RTA_METRICS);
+ if (r < 0 && r != -ENODATA) {
+ log_link_error_errno(link, r, "rtnl: Could not enter RTA_METRICS container: %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_exit_container(message);
+ if (r < 0) {
+ log_link_error_errno(link, r, "rtnl: Could not exit from RTA_METRICS container: %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, &multipath_routes);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "rtnl: failed to parse RTA_MULTIPATH attribute, ignoring: %m");
+ return 0;
+ }
+ }
+
+ if (ordered_set_isempty(multipath_routes))
+ (void) process_route_one(m, link, type, tmp, NULL);
+ else {
+ MultipathRoute *mr;
+
+ ORDERED_SET_FOREACH(mr, multipath_routes) {
+ r = process_route_one(m, link, type, tmp, mr);
+ if (r < 0)
+ break;
+ }
+ }
+
+ return 1;
+}
+
+int link_serialize_routes(const Link *link, FILE *f) {
+ bool space = false;
+ Route *route;
+
+ assert(link);
+ assert(link->network);
+ assert(f);
+
+ fputs("ROUTES=", f);
+ SET_FOREACH(route, link->routes) {
+ _cleanup_free_ char *route_str = NULL;
+
+ if (in_addr_to_string(route->family, &route->dst, &route_str) < 0)
+ continue;
+
+ fprintf(f, "%s%s/%hhu/%hhu/%"PRIu32"/%"PRIu32"/"USEC_FMT,
+ space ? " " : "", route_str,
+ route->dst_prefixlen, route->tos, route->priority, route->table, route->lifetime);
+ space = true;
+ }
+ fputc('\n', f);
+
+ return 0;
+}
+
+int link_deserialize_routes(Link *link, const char *routes) {
+ int r;
+
+ assert(link);
+
+ for (const char *p = routes;; ) {
+ _cleanup_(route_freep) Route *tmp = NULL;
+ _cleanup_free_ char *route_str = NULL;
+ char *prefixlen_str;
+
+ r = extract_first_word(&p, &route_str, NULL, 0);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to parse ROUTES=: %m");
+ if (r == 0)
+ return 0;
+
+ prefixlen_str = strchr(route_str, '/');
+ if (!prefixlen_str) {
+ log_link_debug(link, "Failed to parse route, ignoring: %s", route_str);
+ continue;
+ }
+ *prefixlen_str++ = '\0';
+
+ r = route_new(&tmp);
+ if (r < 0)
+ return log_oom();
+
+ r = sscanf(prefixlen_str,
+ "%hhu/%hhu/%"SCNu32"/%"PRIu32"/"USEC_FMT,
+ &tmp->dst_prefixlen,
+ &tmp->tos,
+ &tmp->priority,
+ &tmp->table,
+ &tmp->lifetime);
+ if (r != 5) {
+ log_link_debug(link,
+ "Failed to parse destination prefix length, tos, priority, table or expiration: %s",
+ prefixlen_str);
+ continue;
+ }
+
+ r = in_addr_from_string_auto(route_str, &tmp->family, &tmp->dst);
+ if (r < 0) {
+ log_link_debug_errno(link, r, "Failed to parse route destination %s: %m", route_str);
+ continue;
+ }
+
+ r = route_add_and_setup_timer(link, tmp, NULL, NULL);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to add route: %m");
+ }
+}
+
+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(lvalue);
+
+ 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;
+ }
+
+ 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, 0, "Unknown route scope: %s", rvalue);
+ return 0;
+ }
+
+ n->scope = r;
+ n->scope_set = true;
+ 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 = route_table_from_string(rvalue);
+ if (r >= 0)
+ n->table = r;
+ else {
+ r = safe_atou32(rvalue, &n->table);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse route table number \"%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("Invalid lvalue");
+
+ 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)
+ n->protocol = r;
+ else {
+ r = safe_atou8(rvalue , &n->protocol);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Could not parse route protocol \"%s\", ignoring assignment: %m", rvalue);
+ return 0;
+ }
+ }
+
+ 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, 0,
+ "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_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 (streq(lvalue, "InitialCongestionWindow"))
+ n->initcwnd = k;
+ else if (streq(lvalue, "InitialAdvertisedReceiveWindow"))
+ n->initrwnd = k;
+ else
+ assert_not_reached("Invalid TCP window type.");
+
+ 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_(route_free_or_set_invalidp) Route *n = NULL;
+ _cleanup_free_ char *word = NULL, *buf = NULL;
+ _cleanup_free_ MultipathRoute *m = NULL;
+ Network *network = userdata;
+ const char *p, *ip, *dev;
+ union in_addr_union a;
+ int family, 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->multipath_routes = ordered_set_free_free(n->multipath_routes);
+ 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) {
+ buf = strndup(word, dev - word);
+ if (!buf)
+ return log_oom();
+ ip = buf;
+ dev++;
+ } else
+ ip = word;
+
+ r = in_addr_from_string_auto(ip, &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 (dev) {
+ r = resolve_interface(NULL, dev);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Invalid interface name or index, ignoring assignment: %s", dev);
+ return 0;
+ }
+ m->ifindex = r;
+ }
+
+ 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;
+ }
+ 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;
+ }
+ }
+
+ r = ordered_set_ensure_allocated(&n->multipath_routes, NULL);
+ if (r < 0)
+ return log_oom();
+
+ r = ordered_set_put(n->multipath_routes, m);
+ 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;
+
+ 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;
+ }
+
+ 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 (ordered_hashmap_isempty(network->addresses_by_section) &&
+ in_addr_is_null(route->gw_family, &route->gw) == 0 &&
+ route->gateway_onlink < 0) {
+ log_warning("%s: Gateway= without static address configured. "
+ "Enabling GatewayOnLink= option.",
+ network->filename);
+ route->gateway_onlink = true;
+ }
+
+ 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..f593693
--- /dev/null
+++ b/src/network/networkd-route.h
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdio.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 Route {
+ Network *network;
+ NetworkConfigSection *section;
+
+ Link *link;
+ Manager *manager;
+
+ 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;
+ unsigned char pref;
+ unsigned flags;
+ int gateway_onlink;
+
+ 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;
+
+ usec_t lifetime;
+ sd_event_source *expire;
+} Route;
+
+void route_hash_func(const Route *route, struct siphash *state);
+int route_compare_func(const Route *a, const Route *b);
+extern const struct hash_ops route_hash_ops;
+
+int route_new(Route **ret);
+Route *route_free(Route *route);
+DEFINE_NETWORK_SECTION_FUNCTIONS(Route, route_free);
+
+int route_configure(const Route *route, Link *link, link_netlink_message_handler_t callback, Route **ret);
+int route_remove(const Route *route, Manager *manager, Link *link, link_netlink_message_handler_t callback);
+
+int link_set_routes(Link *link);
+int link_drop_routes(Link *link);
+int link_drop_foreign_routes(Link *link);
+int link_serialize_routes(const Link *link, FILE *f);
+int link_deserialize_routes(Link *link, const char *routes);
+
+uint32_t link_get_dhcp_route_table(const Link *link);
+uint32_t link_get_ipv6_accept_ra_route_table(const Link *link);
+
+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);
+
+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);
diff --git a/src/network/networkd-routing-policy-rule.c b/src/network/networkd-routing-policy-rule.c
new file mode 100644
index 0000000..e44ecb4
--- /dev/null
+++ b/src/network/networkd-routing-policy-rule.c
@@ -0,0 +1,1810 @@
+/* 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 "ip-protocol-list.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "networkd-routing-policy-rule.h"
+#include "networkd-util.h"
+#include "parse-util.h"
+#include "socket-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+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) {
+ if (set_get(rule->manager->rules, rule) == rule)
+ set_remove(rule->manager->rules, rule);
+ if (set_get(rule->manager->rules_foreign, rule) == rule)
+ set_remove(rule->manager->rules_foreign, rule);
+ }
+
+ network_config_section_free(rule->section);
+ free(rule->iif);
+ free(rule->oif);
+
+ return mfree(rule);
+}
+
+DEFINE_NETWORK_SECTION_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,
+ };
+
+ *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_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_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);
+
+ r = hashmap_ensure_allocated(&network->rules_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(network->rules_by_section, rule->section, rule);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(rule);
+ return 0;
+}
+
+static int routing_policy_rule_copy(RoutingPolicyRule *dest, RoutingPolicyRule *src) {
+ _cleanup_free_ char *iif = NULL, *oif = NULL;
+
+ assert(dest);
+ assert(src);
+
+ if (src->iif) {
+ iif = strdup(src->iif);
+ if (!iif)
+ return -ENOMEM;
+ }
+
+ if (src->oif) {
+ oif = strdup(src->oif);
+ if (!oif)
+ return -ENOMEM;
+ }
+
+ dest->family = src->family;
+ dest->from = src->from;
+ dest->from_prefixlen = src->from_prefixlen;
+ dest->to = src->to;
+ dest->to_prefixlen = src->to_prefixlen;
+ dest->invert_rule = src->invert_rule;
+ dest->tos = src->tos;
+ dest->fwmark = src->fwmark;
+ dest->fwmask = src->fwmask;
+ dest->priority = src->priority;
+ dest->table = src->table;
+ dest->iif = TAKE_PTR(iif);
+ dest->oif = TAKE_PTR(oif);
+ dest->protocol = src->protocol;
+ dest->sport = src->sport;
+ dest->dport = src->dport;
+ dest->uid_range = src->uid_range;
+ dest->suppress_prefixlen = src->suppress_prefixlen;
+
+ 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->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->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->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->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;
+ }
+}
+
+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, RoutingPolicyRule *rule, RoutingPolicyRule **ret) {
+
+ RoutingPolicyRule *existing;
+
+ assert(m);
+
+ existing = set_get(m->rules, rule);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 1;
+ }
+
+ existing = set_get(m->rules_foreign, rule);
+ if (existing) {
+ if (ret)
+ *ret = existing;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int routing_policy_rule_add_internal(Manager *m, Set **rules, RoutingPolicyRule *in, int family, RoutingPolicyRule **ret) {
+ _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *rule = NULL;
+ int r;
+
+ assert(m);
+ assert(rules);
+ assert(in);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(in->family == AF_UNSPEC || in->family == family);
+
+ r = routing_policy_rule_new(&rule);
+ if (r < 0)
+ return r;
+
+ r = routing_policy_rule_copy(rule, in);
+ if (r < 0)
+ return r;
+
+ rule->family = family;
+
+ r = set_ensure_put(rules, &routing_policy_rule_hash_ops, rule);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EEXIST;
+
+ rule->manager = m;
+
+ if (ret)
+ *ret = rule;
+
+ TAKE_PTR(rule);
+ return 0;
+}
+
+static int routing_policy_rule_add(Manager *m, RoutingPolicyRule *rule, int family, RoutingPolicyRule **ret) {
+ return routing_policy_rule_add_internal(m, &m->rules, rule, family, ret);
+}
+
+static int routing_policy_rule_add_foreign(Manager *m, RoutingPolicyRule *rule, RoutingPolicyRule **ret) {
+ return routing_policy_rule_add_internal(m, &m->rules_foreign, rule, rule->family, ret);
+}
+
+static int routing_policy_rule_set_netlink_message(RoutingPolicyRule *rule, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(rule);
+ assert(m);
+ assert(link);
+
+ if (in_addr_is_null(rule->family, &rule->from) == 0) {
+ r = netlink_message_append_in_addr_union(m, FRA_SRC, rule->family, &rule->from);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_SRC attribute: %m");
+
+ r = sd_rtnl_message_routing_policy_rule_set_rtm_src_prefixlen(m, rule->from_prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set source prefix length: %m");
+ }
+
+ if (in_addr_is_null(rule->family, &rule->to) == 0) {
+ r = netlink_message_append_in_addr_union(m, FRA_DST, rule->family, &rule->to);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_DST attribute: %m");
+
+ r = sd_rtnl_message_routing_policy_rule_set_rtm_dst_prefixlen(m, rule->to_prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set destination prefix length: %m");
+ }
+
+ r = sd_netlink_message_append_u32(m, FRA_PRIORITY, rule->priority);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_PRIORITY attribute: %m");
+
+ if (rule->tos > 0) {
+ r = sd_rtnl_message_routing_policy_rule_set_tos(m, rule->tos);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set IP rule TOS: %m");
+ }
+
+ if (rule->table < 256) {
+ r = sd_rtnl_message_routing_policy_rule_set_table(m, rule->table);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set IP rule table: %m");
+ } else {
+ r = sd_rtnl_message_routing_policy_rule_set_table(m, RT_TABLE_UNSPEC);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set IP rule table: %m");
+
+ r = sd_netlink_message_append_u32(m, FRA_TABLE, rule->table);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_TABLE attribute: %m");
+ }
+
+ if (rule->fwmark > 0) {
+ r = sd_netlink_message_append_u32(m, FRA_FWMARK, rule->fwmark);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_FWMARK attribute: %m");
+
+ r = sd_netlink_message_append_u32(m, FRA_FWMASK, rule->fwmask);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_FWMASK attribute: %m");
+ }
+
+ if (rule->iif) {
+ r = sd_netlink_message_append_string(m, FRA_IIFNAME, rule->iif);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_IIFNAME attribute: %m");
+ }
+
+ if (rule->oif) {
+ r = sd_netlink_message_append_string(m, FRA_OIFNAME, rule->oif);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_OIFNAME attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u8(m, FRA_IP_PROTO, rule->protocol);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_IP_PROTO attribute: %m");
+
+ 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 log_link_error_errno(link, r, "Could not append FRA_SPORT_RANGE attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append FRA_DPORT_RANGE attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append FRA_UID_RANGE attribute: %m");
+ }
+
+ if (rule->invert_rule) {
+ r = sd_rtnl_message_routing_policy_rule_set_flags(m, FIB_RULE_INVERT);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FIB_RULE_INVERT attribute: %m");
+ }
+
+ if (rule->suppress_prefixlen >= 0) {
+ r = sd_netlink_message_append_u32(m, FRA_SUPPRESS_PREFIXLEN, (uint32_t) rule->suppress_prefixlen);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append FRA_SUPPRESS_PREFIXLEN attribute: %m");
+ }
+
+ return 0;
+}
+
+static int routing_policy_rule_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+
+ link->routing_policy_rule_remove_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ r = sd_netlink_message_get_errno(m);
+ if (r < 0)
+ log_link_message_warning_errno(link, m, r, "Could not drop routing policy rule");
+
+ return 1;
+}
+
+static int routing_policy_rule_remove(RoutingPolicyRule *rule, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(rule);
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+ assert(IN_SET(rule->family, AF_INET, AF_INET6));
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *from = NULL, *to = NULL;
+
+ (void) in_addr_to_string(rule->family, &rule->from, &from);
+ (void) in_addr_to_string(rule->family, &rule->to, &to);
+
+ log_link_debug(link,
+ "Removing routing policy rule: priority: %"PRIu32", %s/%u -> %s/%u, iif: %s, oif: %s, table: %"PRIu32,
+ rule->priority, strna(from), rule->from_prefixlen, strna(to), rule->to_prefixlen, strna(rule->iif), strna(rule->oif), rule->table);
+ }
+
+ r = sd_rtnl_message_new_routing_policy_rule(link->manager->rtnl, &m, RTM_DELRULE, rule->family);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_DELRULE message: %m");
+
+ r = routing_policy_rule_set_netlink_message(rule, m, link);
+ if (r < 0)
+ return r;
+
+ r = netlink_call_async(link->manager->rtnl, NULL, m,
+ routing_policy_rule_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);
+
+ return 0;
+}
+
+static int routing_policy_rule_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(rtnl);
+ assert(m);
+ assert(link);
+ assert(link->ifname);
+ assert(link->routing_policy_rule_messages > 0);
+
+ link->routing_policy_rule_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ 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->routing_policy_rule_messages == 0) {
+ log_link_debug(link, "Routing policy rule configured");
+ link->routing_policy_rules_configured = true;
+ link_check_ready(link);
+ }
+
+ return 1;
+}
+
+static int routing_policy_rule_configure_internal(RoutingPolicyRule *rule, int family, Link *link) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ int r;
+
+ assert(rule);
+ assert(link);
+ assert(link->ifindex > 0);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *from = NULL, *to = NULL;
+
+ (void) in_addr_to_string(family, &rule->from, &from);
+ (void) in_addr_to_string(family, &rule->to, &to);
+
+ log_link_debug(link,
+ "Configuring routing policy rule: priority: %"PRIu32", %s/%u -> %s/%u, iif: %s, oif: %s, table: %"PRIu32,
+ rule->priority, strna(from), rule->from_prefixlen, strna(to), rule->to_prefixlen, strna(rule->iif), strna(rule->oif), rule->table);
+ }
+
+ r = sd_rtnl_message_new_routing_policy_rule(link->manager->rtnl, &m, RTM_NEWRULE, family);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_NEWRULE message: %m");
+
+ r = routing_policy_rule_set_netlink_message(rule, m, link);
+ if (r < 0)
+ return r;
+
+ r = netlink_call_async(link->manager->rtnl, NULL, m,
+ routing_policy_rule_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);
+ link->routing_policy_rule_messages++;
+
+ r = routing_policy_rule_add(link->manager, rule, family, NULL);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not add rule: %m");
+
+ return 1;
+}
+
+static int routing_policy_rule_configure(RoutingPolicyRule *rule, Link *link) {
+ int r;
+
+ if (IN_SET(rule->family, AF_INET, AF_INET6))
+ return routing_policy_rule_configure_internal(rule, rule->family, link);
+
+ if (FLAGS_SET(rule->address_family, ADDRESS_FAMILY_IPV4)) {
+ r = routing_policy_rule_configure_internal(rule, AF_INET, link);
+ if (r < 0)
+ return r;
+ }
+
+ if (FLAGS_SET(rule->address_family, ADDRESS_FAMILY_IPV6)) {
+ r = routing_policy_rule_configure_internal(rule, AF_INET6, link);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static bool manager_links_have_routing_policy_rule(Manager *m, RoutingPolicyRule *rule) {
+ Link *link;
+
+ assert(m);
+ assert(rule);
+
+ HASHMAP_FOREACH(link, m->links) {
+ RoutingPolicyRule *link_rule;
+
+ if (!link->network)
+ continue;
+
+ HASHMAP_FOREACH(link_rule, link->network->rules_by_section)
+ if (routing_policy_rule_compare_func(link_rule, rule) == 0)
+ return true;
+ }
+
+ return false;
+}
+
+static void routing_policy_rule_purge(Manager *m, Link *link) {
+ RoutingPolicyRule *rule;
+ int r;
+
+ assert(m);
+ assert(link);
+
+ SET_FOREACH(rule, m->rules_saved) {
+ RoutingPolicyRule *existing;
+
+ existing = set_get(m->rules_foreign, rule);
+ if (!existing)
+ continue; /* Saved rule does not exist anymore. */
+
+ if (manager_links_have_routing_policy_rule(m, existing))
+ continue; /* Existing links have the saved rule. */
+
+ /* Existing links do not have the saved rule. Let's drop the rule now, and re-configure it
+ * later when it is requested. */
+
+ r = routing_policy_rule_remove(existing, link);
+ if (r < 0) {
+ log_warning_errno(r, "Could not remove routing policy rules: %m");
+ continue;
+ }
+
+ link->routing_policy_rule_remove_messages++;
+
+ assert_se(set_remove(m->rules_foreign, existing) == existing);
+ routing_policy_rule_free(existing);
+ }
+}
+
+int link_set_routing_policy_rules(Link *link) {
+ RoutingPolicyRule *rule;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ link->routing_policy_rules_configured = false;
+
+ HASHMAP_FOREACH(rule, link->network->rules_by_section) {
+ RoutingPolicyRule *existing;
+
+ r = routing_policy_rule_get(link->manager, rule, &existing);
+ if (r > 0)
+ continue;
+ if (r == 0) {
+ r = set_ensure_put(&link->manager->rules, &routing_policy_rule_hash_ops, existing);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not store existing routing policy rule: %m");
+
+ set_remove(link->manager->rules_foreign, existing);
+ continue;
+ }
+
+ r = routing_policy_rule_configure(rule, link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not set routing policy rule: %m");
+ }
+
+ routing_policy_rule_purge(link->manager, link);
+ if (link->routing_policy_rule_messages == 0)
+ link->routing_policy_rules_configured = true;
+ else {
+ log_link_debug(link, "Setting routing policy rules");
+ link_set_state(link, LINK_STATE_CONFIGURING);
+ }
+
+ return 0;
+}
+
+int manager_rtnl_process_rule(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
+ _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *tmp = NULL;
+ _cleanup_free_ char *from = NULL, *to = NULL;
+ RoutingPolicyRule *rule = NULL;
+ const char *iif = NULL, *oif = NULL;
+ uint32_t suppress_prefixlen;
+ unsigned flags;
+ 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;
+ }
+
+ switch (tmp->family) {
+ case AF_INET:
+ r = sd_netlink_message_read_in_addr(message, FRA_SRC, &tmp->from.in);
+ 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_rtm_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 = sd_netlink_message_read_in_addr(message, FRA_DST, &tmp->to.in);
+ 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_rtm_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;
+ }
+ }
+
+ break;
+
+ case AF_INET6:
+ r = sd_netlink_message_read_in6_addr(message, FRA_SRC, &tmp->from.in6);
+ 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_rtm_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 = sd_netlink_message_read_in6_addr(message, FRA_DST, &tmp->to.in6);
+ 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_rtm_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;
+ }
+ }
+
+ break;
+
+ default:
+ assert_not_reached("Received rule message with unsupported address family");
+ }
+
+ 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;
+ }
+
+ 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 ip rule TOS, ignoring: %m");
+ return 0;
+ }
+
+ r = sd_netlink_message_read_string(message, FRA_IIFNAME, &iif);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_IIFNAME attribute, ignoring: %m");
+ return 0;
+ }
+ r = free_and_strdup(&tmp->iif, iif);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_netlink_message_read_string(message, FRA_OIFNAME, &oif);
+ if (r < 0 && r != -ENODATA) {
+ log_warning_errno(r, "rtnl: could not get FRA_OIFNAME attribute, ignoring: %m");
+ return 0;
+ }
+ r = free_and_strdup(&tmp->oif, oif);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_netlink_message_read_u8(message, FRA_IP_PROTO, &tmp->protocol);
+ 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(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;
+ }
+
+ 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 = (int) suppress_prefixlen;
+
+ (void) routing_policy_rule_get(m, tmp, &rule);
+
+ if (DEBUG_LOGGING) {
+ (void) in_addr_to_string(tmp->family, &tmp->from, &from);
+ (void) in_addr_to_string(tmp->family, &tmp->to, &to);
+ }
+
+ switch (type) {
+ case RTM_NEWRULE:
+ if (rule)
+ log_debug("Received remembered routing policy rule: priority: %"PRIu32", %s/%u -> %s/%u, iif: %s, oif: %s, table: %"PRIu32,
+ tmp->priority, strna(from), tmp->from_prefixlen, strna(to), tmp->to_prefixlen, strna(tmp->iif), strna(tmp->oif), tmp->table);
+ else {
+ log_debug("Remembering foreign routing policy rule: priority: %"PRIu32", %s/%u -> %s/%u, iif: %s, oif: %s, table: %"PRIu32,
+ tmp->priority, strna(from), tmp->from_prefixlen, strna(to), tmp->to_prefixlen, strna(tmp->iif), strna(tmp->oif), tmp->table);
+ r = routing_policy_rule_add_foreign(m, tmp, &rule);
+ if (r < 0) {
+ log_warning_errno(r, "Could not remember foreign rule, ignoring: %m");
+ return 0;
+ }
+ }
+ break;
+ case RTM_DELRULE:
+ if (rule) {
+ log_debug("Forgetting routing policy rule: priority: %"PRIu32", %s/%u -> %s/%u, iif: %s, oif: %s, table: %"PRIu32,
+ tmp->priority, strna(from), tmp->from_prefixlen, strna(to), tmp->to_prefixlen, strna(tmp->iif), strna(tmp->oif), tmp->table);
+ routing_policy_rule_free(rule);
+ } else
+ log_debug("Kernel removed a routing policy rule we don't remember: priority: %"PRIu32", %s/%u -> %s/%u, iif: %s, oif: %s, table: %"PRIu32", ignoring.",
+ tmp->priority, strna(from), tmp->from_prefixlen, strna(to), tmp->to_prefixlen, strna(tmp->iif), strna(tmp->oif), tmp->table);
+ break;
+
+ default:
+ assert_not_reached("Received invalid RTNL message type");
+ }
+
+ 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;
+ }
+
+ n = NULL;
+
+ 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();
+
+ 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 = NULL;
+
+ 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 = safe_atou32(rvalue, &n->table);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule table, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ n = NULL;
+
+ 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;
+ }
+
+ n = NULL;
+
+ 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;
+ }
+
+ n = NULL;
+
+ 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, "Failed to parse '%s' interface name, ignoring: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "IncomingInterface")) {
+ r = free_and_strdup(&n->iif, rvalue);
+ if (r < 0)
+ return log_oom();
+ } else {
+ r = free_and_strdup(&n->oif, rvalue);
+ if (r < 0)
+ return log_oom();
+ }
+
+ n = NULL;
+
+ 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;
+ }
+
+ n = NULL;
+
+ 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->protocol = r;
+
+ n = NULL;
+
+ 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;
+
+ n = NULL;
+
+ 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, 0,
+ "Invalid address family '%s', ignoring.", rvalue);
+ return 0;
+ }
+
+ n->address_family = a;
+ n = NULL;
+
+ 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;
+ n = NULL;
+
+ 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;
+ }
+
+ n = NULL;
+
+ 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 && rule->address_family == ADDRESS_FAMILY_NO)
+ rule->family = AF_INET;
+
+ 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);
+}
+
+int routing_policy_serialize_rules(Set *rules, FILE *f) {
+ RoutingPolicyRule *rule;
+ int r;
+
+ assert(f);
+
+ SET_FOREACH(rule, rules) {
+ const char *family_str;
+ bool space = false;
+
+ fputs("RULE=", f);
+
+ family_str = af_to_name(rule->family);
+ if (family_str) {
+ fprintf(f, "family=%s",
+ family_str);
+ space = true;
+ }
+
+ if (!in_addr_is_null(rule->family, &rule->from)) {
+ _cleanup_free_ char *str = NULL;
+
+ r = in_addr_to_string(rule->family, &rule->from, &str);
+ if (r < 0)
+ return r;
+
+ fprintf(f, "%sfrom=%s/%hhu",
+ space ? " " : "",
+ str, rule->from_prefixlen);
+ space = true;
+ }
+
+ if (!in_addr_is_null(rule->family, &rule->to)) {
+ _cleanup_free_ char *str = NULL;
+
+ r = in_addr_to_string(rule->family, &rule->to, &str);
+ if (r < 0)
+ return r;
+
+ fprintf(f, "%sto=%s/%hhu",
+ space ? " " : "",
+ str, rule->to_prefixlen);
+ space = true;
+ }
+
+ if (rule->tos != 0) {
+ fprintf(f, "%stos=%hhu",
+ space ? " " : "",
+ rule->tos);
+ space = true;
+ }
+
+ if (rule->priority != 0) {
+ fprintf(f, "%spriority=%"PRIu32,
+ space ? " " : "",
+ rule->priority);
+ space = true;
+ }
+
+ if (rule->fwmark != 0) {
+ fprintf(f, "%sfwmark=%"PRIu32,
+ space ? " " : "",
+ rule->fwmark);
+ if (rule->fwmask != UINT32_MAX)
+ fprintf(f, "/%"PRIu32, rule->fwmask);
+ space = true;
+ }
+
+ if (rule->iif) {
+ fprintf(f, "%siif=%s",
+ space ? " " : "",
+ rule->iif);
+ space = true;
+ }
+
+ if (rule->oif) {
+ fprintf(f, "%soif=%s",
+ space ? " " : "",
+ rule->oif);
+ space = true;
+ }
+
+ if (rule->protocol != 0) {
+ fprintf(f, "%sprotocol=%hhu",
+ space ? " " : "",
+ rule->protocol);
+ space = true;
+ }
+
+ if (rule->sport.start != 0 || rule->sport.end != 0) {
+ fprintf(f, "%ssourcesport=%"PRIu16"-%"PRIu16,
+ space ? " " : "",
+ rule->sport.start, rule->sport.end);
+ space = true;
+ }
+
+ if (rule->dport.start != 0 || rule->dport.end != 0) {
+ fprintf(f, "%sdestinationport=%"PRIu16"-%"PRIu16,
+ space ? " " : "",
+ rule->dport.start, rule->dport.end);
+ space = true;
+ }
+
+ if (rule->uid_range.start != UID_INVALID && rule->uid_range.end != UID_INVALID) {
+ assert_cc(sizeof(uid_t) == sizeof(uint32_t));
+ fprintf(f, "%suidrange="UID_FMT"-"UID_FMT,
+ space ? " " : "",
+ rule->uid_range.start, rule->uid_range.end);
+ space = true;
+ }
+
+ if (rule->suppress_prefixlen >= 0) {
+ fprintf(f, "%ssuppress_prefixlen=%d",
+ space ? " " : "",
+ rule->suppress_prefixlen);
+ space = true;
+ }
+
+ fprintf(f, "%sinvert_rule=%s table=%"PRIu32"\n",
+ space ? " " : "",
+ yes_no(rule->invert_rule),
+ rule->table);
+ }
+
+ return 0;
+}
+
+static int routing_policy_rule_read_full_file(const char *state_file, char ***ret) {
+ _cleanup_strv_free_ char **lines = NULL;
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ assert(state_file);
+
+ r = read_full_file(state_file, &s, NULL);
+ if (r == -ENOENT) {
+ *ret = NULL;
+ return 0;
+ }
+ if (r < 0)
+ return r;
+
+ lines = strv_split_newlines(s);
+ if (!lines)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(lines);
+ return 0;
+}
+
+int routing_policy_load_rules(const char *state_file, Set **rules) {
+ _cleanup_strv_free_ char **data = NULL;
+ char **i;
+ int r;
+
+ assert(state_file);
+ assert(rules);
+
+ r = routing_policy_rule_read_full_file(state_file, &data);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to read %s, ignoring: %m", state_file);
+
+ STRV_FOREACH(i, data) {
+ _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *rule = NULL;
+ const char *p;
+
+ p = startswith(*i, "RULE=");
+ if (!p)
+ continue;
+
+ r = routing_policy_rule_new(&rule);
+ if (r < 0)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_free_ char *a = NULL;
+ char *b;
+
+ r = extract_first_word(&p, &a, NULL, 0);
+ if (r < 0)
+ return log_oom();
+ if (r == 0)
+ break;
+
+ b = strchr(a, '=');
+ if (!b) {
+ log_warning_errno(r, "Failed to parse RPDB rule, ignoring: %s", a);
+ continue;
+ }
+ *b++ = '\0';
+
+ if (streq(a, "family")) {
+ r = af_from_name(b);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse RPDB rule family, ignoring: %s", b);
+ continue;
+ }
+ if (rule->family != AF_UNSPEC && rule->family != r) {
+ log_warning("RPDB rule family is already specified, ignoring assignment: %s", b);
+ continue;
+ }
+ rule->family = r;
+ } else if (STR_IN_SET(a, "from", "to")) {
+ union in_addr_union *buffer;
+ uint8_t *prefixlen;
+
+ if (streq(a, "to")) {
+ buffer = &rule->to;
+ prefixlen = &rule->to_prefixlen;
+ } else {
+ buffer = &rule->from;
+ prefixlen = &rule->from_prefixlen;
+ }
+
+ if (rule->family == AF_UNSPEC)
+ r = in_addr_prefix_from_string_auto(b, &rule->family, buffer, prefixlen);
+ else
+ r = in_addr_prefix_from_string(b, rule->family, buffer, prefixlen);
+ if (r < 0) {
+ log_warning_errno(r, "RPDB rule prefix is invalid, ignoring assignment: %s", b);
+ continue;
+ }
+ } else if (streq(a, "tos")) {
+ r = safe_atou8(b, &rule->tos);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse RPDB rule TOS, ignoring: %s", b);
+ continue;
+ }
+ } else if (streq(a, "table")) {
+ r = safe_atou32(b, &rule->table);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse RPDB rule table, ignoring: %s", b);
+ continue;
+ }
+ } else if (streq(a, "priority")) {
+ r = safe_atou32(b, &rule->priority);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse RPDB rule priority, ignoring: %s", b);
+ continue;
+ }
+ } else if (streq(a, "fwmark")) {
+ r = parse_fwmark_fwmask(b, &rule->fwmark, &rule->fwmask);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse RPDB rule firewall mark or mask, ignoring: %s", a);
+ continue;
+ }
+ } else if (streq(a, "iif")) {
+ if (free_and_strdup(&rule->iif, b) < 0)
+ return log_oom();
+
+ } else if (streq(a, "oif")) {
+
+ if (free_and_strdup(&rule->oif, b) < 0)
+ return log_oom();
+ } else if (streq(a, "protocol")) {
+ r = safe_atou8(b, &rule->protocol);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse RPDB rule protocol, ignoring: %s", b);
+ continue;
+ }
+ } else if (streq(a, "sourceport")) {
+ uint16_t low, high;
+
+ r = parse_ip_port_range(b, &low, &high);
+ if (r < 0) {
+ log_warning_errno(r, "Invalid routing policy rule source port range, ignoring assignment: '%s'", b);
+ continue;
+ }
+
+ rule->sport.start = low;
+ rule->sport.end = high;
+ } else if (streq(a, "destinationport")) {
+ uint16_t low, high;
+
+ r = parse_ip_port_range(b, &low, &high);
+ if (r < 0) {
+ log_warning_errno(r, "Invalid routing policy rule destination port range, ignoring assignment: '%s'", b);
+ continue;
+ }
+
+ rule->dport.start = low;
+ rule->dport.end = high;
+ } else if (streq(a, "uidrange")) {
+ uid_t lower, upper;
+
+ r = parse_uid_range(b, &lower, &upper);
+ if (r < 0) {
+ log_warning_errno(r, "Invalid routing policy rule uid range, ignoring assignment: '%s'", b);
+ continue;
+ }
+
+ rule->uid_range.start = lower;
+ rule->uid_range.end = upper;
+ } else if (streq(a, "suppress_prefixlen")) {
+ r = parse_ip_prefix_length(b, &rule->suppress_prefixlen);
+ if (r == -ERANGE) {
+ log_warning_errno(r, "Prefix length outside of valid range 0-128, ignoring: %s", b);
+ continue;
+ }
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse RPDB rule suppress_prefixlen, ignoring: %s", b);
+ continue;
+ }
+ } else if (streq(a, "invert_rule")) {
+ r = parse_boolean(b);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse RPDB rule invert_rule, ignoring: %s", b);
+ continue;
+ }
+ rule->invert_rule = r;
+ } else
+ log_warning("Unknown RPDB rule, ignoring: %s", a);
+ }
+
+ r = set_ensure_put(rules, &routing_policy_rule_hash_ops, rule);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to add RPDB rule to saved DB, ignoring: %s", *i);
+ continue;
+ }
+ if (r > 0)
+ rule = NULL;
+ }
+
+ return 0;
+}
diff --git a/src/network/networkd-routing-policy-rule.h b/src/network/networkd-routing-policy-rule.h
new file mode 100644
index 0000000..baf086f
--- /dev/null
+++ b/src/network/networkd-routing-policy-rule.h
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <linux/fib_rules.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "conf-parser.h"
+#include "in-addr-util.h"
+#include "networkd-util.h"
+#include "set.h"
+
+typedef struct Network Network;
+typedef struct Link Link;
+typedef struct Manager Manager;
+
+typedef struct RoutingPolicyRule {
+ Manager *manager;
+ Network *network;
+ NetworkConfigSection *section;
+
+ bool invert_rule;
+
+ uint8_t tos;
+ uint8_t protocol;
+
+ 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= */
+ unsigned char to_prefixlen;
+ unsigned char from_prefixlen;
+
+ 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;
+} RoutingPolicyRule;
+
+RoutingPolicyRule *routing_policy_rule_free(RoutingPolicyRule *rule);
+
+void network_drop_invalid_routing_policy_rules(Network *network);
+
+int link_set_routing_policy_rules(Link *link);
+
+int manager_rtnl_process_rule(sd_netlink *rtnl, sd_netlink_message *message, Manager *m);
+
+int routing_policy_serialize_rules(Set *rules, FILE *f);
+int routing_policy_load_rules(const char *state_file, Set **rules);
+
+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);
diff --git a/src/network/networkd-speed-meter.c b/src/network/networkd-speed-meter.c
new file mode 100644
index 0000000..e7f0682
--- /dev/null
+++ b/src/network/networkd-speed-meter.c
@@ -0,0 +1,113 @@
+/* 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;
+
+ link = hashmap_get(manager->links, INT_TO_PTR(ifindex));
+ if (!link)
+ return -ENODEV;
+
+ 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 = userdata;
+ sd_netlink_message *i;
+ usec_t usec_now;
+ Link *link;
+ int r;
+
+ assert(s);
+ assert(userdata);
+
+ 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)
+ 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_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 (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..68f43b5
--- /dev/null
+++ b/src/network/networkd-sriov.c
@@ -0,0 +1,532 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+
+#include "alloc-util.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "networkd-sriov.h"
+#include "parse-util.h"
+#include "set.h"
+#include "string-util.h"
+
+static int sr_iov_new(SRIOV **ret) {
+ SRIOV *sr_iov;
+
+ sr_iov = new(SRIOV, 1);
+ if (!sr_iov)
+ return -ENOMEM;
+
+ *sr_iov = (SRIOV) {
+ .vf = (uint32_t) -1,
+ .vlan_proto = ETH_P_8021Q,
+ .vf_spoof_check_setting = -1,
+ .trust = -1,
+ .query_rss = -1,
+ .link_state = _SR_IOV_LINK_STATE_INVALID,
+ };
+
+ *ret = TAKE_PTR(sr_iov);
+
+ return 0;
+}
+
+static int sr_iov_new_static(Network *network, const char *filename, unsigned section_line, SRIOV **ret) {
+ _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(sr_iov_freep) SRIOV *sr_iov = NULL;
+ SRIOV *existing = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ existing = ordered_hashmap_get(network->sr_iov_by_section, n);
+ if (existing) {
+ *ret = existing;
+ return 0;
+ }
+
+ r = sr_iov_new(&sr_iov);
+ if (r < 0)
+ return r;
+
+ sr_iov->network = network;
+ sr_iov->section = TAKE_PTR(n);
+
+ r = ordered_hashmap_ensure_allocated(&network->sr_iov_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(network->sr_iov_by_section, sr_iov->section, sr_iov);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(sr_iov);
+ return 0;
+}
+
+SRIOV *sr_iov_free(SRIOV *sr_iov) {
+ if (!sr_iov)
+ return NULL;
+
+ if (sr_iov->network && sr_iov->section)
+ ordered_hashmap_remove(sr_iov->network->sr_iov_by_section, sr_iov->section);
+
+ network_config_section_free(sr_iov->section);
+
+ return mfree(sr_iov);
+}
+
+static int sr_iov_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->sr_iov_messages > 0);
+ link->sr_iov_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ 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(Link *link, SRIOV *sr_iov) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+
+ log_link_debug(link, "Setting SR-IOV virtual function %"PRIu32, sr_iov->vf);
+
+ r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");
+
+ r = sd_netlink_message_open_container(req, IFLA_VFINFO_LIST);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open IFLA_VFINFO_LIST container: %m");
+
+ r = sd_netlink_message_open_container(req, IFLA_VF_INFO);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open IFLA_VF_INFO container: %m");
+
+ if (!ether_addr_is_null(&sr_iov->mac)) {
+ struct ifla_vf_mac ivm = {
+ .vf = sr_iov->vf,
+ };
+
+ memcpy(ivm.mac, &sr_iov->mac, ETH_ALEN);
+ r = sd_netlink_message_append_data(req, IFLA_VF_MAC, &ivm, sizeof(struct ifla_vf_mac));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_VF_MAC: %m");
+ }
+
+ if (sr_iov->vf_spoof_check_setting >= 0) {
+ struct ifla_vf_spoofchk ivs = {
+ .vf = sr_iov->vf,
+ .setting = sr_iov->vf_spoof_check_setting,
+ };
+
+ r = sd_netlink_message_append_data(req, IFLA_VF_SPOOFCHK, &ivs, sizeof(struct ifla_vf_spoofchk));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_VF_SPOOFCHK: %m");
+ }
+
+ if (sr_iov->query_rss >= 0) {
+ struct ifla_vf_rss_query_en ivs = {
+ .vf = sr_iov->vf,
+ .setting = sr_iov->query_rss,
+ };
+
+ r = sd_netlink_message_append_data(req, IFLA_VF_RSS_QUERY_EN, &ivs, sizeof(struct ifla_vf_rss_query_en));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_VF_RSS_QUERY_EN: %m");
+ }
+
+ if (sr_iov->trust >= 0) {
+ struct ifla_vf_trust ivt = {
+ .vf = sr_iov->vf,
+ .setting = sr_iov->trust,
+ };
+
+ r = sd_netlink_message_append_data(req, IFLA_VF_TRUST, &ivt, sizeof(struct ifla_vf_trust));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_VF_TRUST: %m");
+ }
+
+ if (sr_iov->link_state >= 0) {
+ struct ifla_vf_link_state ivl = {
+ .vf = sr_iov->vf,
+ .link_state = sr_iov->link_state,
+ };
+
+ r = sd_netlink_message_append_data(req, IFLA_VF_LINK_STATE, &ivl, sizeof(struct ifla_vf_link_state));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_VF_LINK_STATE: %m");
+ }
+
+ if (sr_iov->vlan > 0) {
+ /* Because of padding, first the buffer must be initialized with 0. */
+ struct ifla_vf_vlan_info ivvi = {};
+ ivvi.vf = sr_iov->vf;
+ ivvi.vlan = sr_iov->vlan;
+ ivvi.qos = sr_iov->qos;
+ ivvi.vlan_proto = htobe16(sr_iov->vlan_proto);
+
+ r = sd_netlink_message_open_container(req, IFLA_VF_VLAN_LIST);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open IFLA_VF_VLAN_LIST container: %m");
+
+ r = sd_netlink_message_append_data(req, IFLA_VF_VLAN_INFO, &ivvi, sizeof(struct ifla_vf_vlan_info));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append IFLA_VF_VLAN_INFO: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close IFLA_VF_VLAN_LIST container: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close IFLA_VF_INFO container: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close IFLA_VFINFO_LIST container: %m");
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, sr_iov_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);
+ link->sr_iov_messages++;
+
+ return 0;
+}
+
+int link_configure_sr_iov(Link *link) {
+ SRIOV *sr_iov;
+ int r;
+
+ link->sr_iov_configured = false;
+ link->sr_iov_messages = 0;
+
+ ORDERED_HASHMAP_FOREACH(sr_iov, link->network->sr_iov_by_section) {
+ r = sr_iov_configure(link, sr_iov);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->sr_iov_messages == 0)
+ link->sr_iov_configured = true;
+ else
+ log_link_debug(link, "Configuring SR-IOV");
+
+ return 0;
+}
+
+static int sr_iov_section_verify(SRIOV *sr_iov) {
+ assert(sr_iov);
+
+ if (section_is_invalid(sr_iov->section))
+ return -EINVAL;
+
+ if (sr_iov->vf == (uint32_t) -1)
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: [SRIOV] section without VirtualFunction= field configured. "
+ "Ignoring [SRIOV] section from line %u.",
+ sr_iov->section->filename, sr_iov->section->line);
+
+ return 0;
+}
+
+void network_drop_invalid_sr_iov(Network *network) {
+ SRIOV *sr_iov;
+
+ assert(network);
+
+ ORDERED_HASHMAP_FOREACH(sr_iov, network->sr_iov_by_section)
+ if (sr_iov_section_verify(sr_iov) < 0)
+ sr_iov_free(sr_iov);
+}
+
+int config_parse_sr_iov_uint32(
+ 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_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
+ Network *network = data;
+ uint32_t k;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = sr_iov_new_static(network, filename, section_line, &sr_iov);
+ if (r < 0)
+ return r;
+
+ if (isempty(rvalue)) {
+ if (streq(lvalue, "VirtualFunction"))
+ sr_iov->vf = (uint32_t) -1;
+ else if (streq(lvalue, "VLANId"))
+ sr_iov->vlan = 0;
+ else if (streq(lvalue, "QualityOfService"))
+ sr_iov->qos = 0;
+ else
+ assert_not_reached("Invalid lvalue");
+
+ TAKE_PTR(sr_iov);
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse SR-IOV '%s=', ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ if (streq(lvalue, "VLANId")) {
+ if (k == 0 || k > 4095) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid SR-IOV VLANId: %d", k);
+ return 0;
+ }
+ sr_iov->vlan = k;
+ } else if (streq(lvalue, "VirtualFunction")) {
+ if (k >= INT_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid SR-IOV virtual function: %d", k);
+ return 0;
+ }
+ sr_iov->vf = k;
+ } else if (streq(lvalue, "QualityOfService"))
+ sr_iov->qos = k;
+ else
+ assert_not_reached("Invalid lvalue");
+
+ TAKE_PTR(sr_iov);
+ return 0;
+}
+
+int config_parse_sr_iov_vlan_proto(
+ 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_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
+ Network *network = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = sr_iov_new_static(network, filename, section_line, &sr_iov);
+ if (r < 0)
+ return r;
+
+ if (isempty(rvalue) || streq(rvalue, "802.1Q"))
+ sr_iov->vlan_proto = ETH_P_8021Q;
+ else if (streq(rvalue, "802.1ad"))
+ sr_iov->vlan_proto = ETH_P_8021AD;
+ else {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid SR-IOV '%s=', ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(sr_iov);
+ return 0;
+}
+
+int config_parse_sr_iov_link_state(
+ 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_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
+ Network *network = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = sr_iov_new_static(network, filename, section_line, &sr_iov);
+ if (r < 0)
+ return r;
+
+ /* Unfortunately, SR_IOV_LINK_STATE_DISABLE is 2, not 0. So, we cannot use
+ * DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN() macro. */
+
+ if (isempty(rvalue)) {
+ sr_iov->link_state = _SR_IOV_LINK_STATE_INVALID;
+ TAKE_PTR(sr_iov);
+ return 0;
+ }
+
+ if (streq(rvalue, "auto")) {
+ sr_iov->link_state = SR_IOV_LINK_STATE_AUTO;
+ TAKE_PTR(sr_iov);
+ return 0;
+ }
+
+ r = parse_boolean(rvalue);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse SR-IOV '%s=', ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ sr_iov->link_state = r ? SR_IOV_LINK_STATE_ENABLE : SR_IOV_LINK_STATE_DISABLE;
+ TAKE_PTR(sr_iov);
+ return 0;
+}
+
+int config_parse_sr_iov_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_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
+ Network *network = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = sr_iov_new_static(network, filename, section_line, &sr_iov);
+ if (r < 0)
+ return r;
+
+ if (isempty(rvalue)) {
+ if (streq(lvalue, "MACSpoofCheck"))
+ sr_iov->vf_spoof_check_setting = -1;
+ else if (streq(lvalue, "QueryReceiveSideScaling"))
+ sr_iov->query_rss = -1;
+ else if (streq(lvalue, "Trust"))
+ sr_iov->trust = -1;
+ else
+ assert_not_reached("Invalid lvalue");
+
+ TAKE_PTR(sr_iov);
+ 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, "MACSpoofCheck"))
+ sr_iov->vf_spoof_check_setting = r;
+ else if (streq(lvalue, "QueryReceiveSideScaling"))
+ sr_iov->query_rss = r;
+ else if (streq(lvalue, "Trust"))
+ sr_iov->trust = r;
+ else
+ assert_not_reached("Invalid lvalue");
+
+ TAKE_PTR(sr_iov);
+ return 0;
+}
+
+int config_parse_sr_iov_mac(
+ 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_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
+ Network *network = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = sr_iov_new_static(network, filename, section_line, &sr_iov);
+ if (r < 0)
+ return r;
+
+ if (isempty(rvalue)) {
+ sr_iov->mac = ETHER_ADDR_NULL;
+ TAKE_PTR(sr_iov);
+ return 0;
+ }
+
+ r = ether_addr_from_string(rvalue, &sr_iov->mac);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse SR-IOV '%s=', ignoring assignment: %s", lvalue, rvalue);
+ return 0;
+ }
+
+ TAKE_PTR(sr_iov);
+ return 0;
+}
diff --git a/src/network/networkd-sriov.h b/src/network/networkd-sriov.h
new file mode 100644
index 0000000..dae5ff0
--- /dev/null
+++ b/src/network/networkd-sriov.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2020 VMware, Inc. */
+#pragma once
+
+#include <linux/if_link.h>
+
+#include "conf-parser.h"
+#include "ether-addr-util.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "networkd-util.h"
+
+typedef enum SRIOVLinkState {
+ SR_IOV_LINK_STATE_AUTO = IFLA_VF_LINK_STATE_AUTO,
+ SR_IOV_LINK_STATE_ENABLE = IFLA_VF_LINK_STATE_ENABLE,
+ SR_IOV_LINK_STATE_DISABLE = IFLA_VF_LINK_STATE_DISABLE,
+ _SR_IOV_LINK_STATE_MAX,
+ _SR_IOV_LINK_STATE_INVALID = -1,
+} SRIOVLinkState;
+
+typedef struct SRIOV {
+ NetworkConfigSection *section;
+ Network *network;
+
+ uint32_t vf; /* 0 - 2147483646 */
+ uint32_t vlan; /* 0 - 4095, 0 disables VLAN filter */
+ uint32_t qos;
+ uint16_t vlan_proto; /* ETH_P_8021Q or ETH_P_8021AD */
+ int vf_spoof_check_setting;
+ int query_rss;
+ int trust;
+ SRIOVLinkState link_state;
+ struct ether_addr mac;
+} SRIOV;
+
+SRIOV *sr_iov_free(SRIOV *sr_iov);
+int link_configure_sr_iov(Link *link);
+void network_drop_invalid_sr_iov(Network *network);
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(SRIOV, sr_iov_free);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_sr_iov_uint32);
+CONFIG_PARSER_PROTOTYPE(config_parse_sr_iov_boolean);
+CONFIG_PARSER_PROTOTYPE(config_parse_sr_iov_link_state);
+CONFIG_PARSER_PROTOTYPE(config_parse_sr_iov_vlan_proto);
+CONFIG_PARSER_PROTOTYPE(config_parse_sr_iov_mac);
diff --git a/src/network/networkd-sysctl.c b/src/network/networkd-sysctl.c
new file mode 100644
index 0000000..bde0cec
--- /dev/null
+++ b/src/network/networkd-sysctl.c
@@ -0,0 +1,288 @@
+/* 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_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);
+}
+
+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_ipv4_accept_local(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv4 accept_local flag for interface, ignoring: %m");
+
+ return 0;
+}
+
+int link_set_ipv6_mtu(Link *link) {
+ int r;
+
+ 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->ipv6_mtu == 0)
+ return 0;
+
+ r = sysctl_write_ip_property_uint32(AF_INET6, link->ifname, "mtu", link->network->ipv6_mtu);
+ if (r < 0)
+ return r;
+
+ link->ipv6_mtu_set = true;
+
+ 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 = data;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(ipv6_privacy_extensions);
+
+ 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..3568900
--- /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 = -1,
+} 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..8ddcbb2
--- /dev/null
+++ b/src/network/networkd-util.c
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "condition.h"
+#include "conf-parser.h"
+#include "networkd-util.h"
+#include "parse-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "util.h"
+
+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 link_local_address_family_table[_ADDRESS_FAMILY_MAX] = {
+ [ADDRESS_FAMILY_NO] = "no",
+ [ADDRESS_FAMILY_YES] = "yes",
+ [ADDRESS_FAMILY_IPV4] = "ipv4",
+ [ADDRESS_FAMILY_IPV6] = "ipv6",
+ [ADDRESS_FAMILY_FALLBACK] = "fallback",
+ [ADDRESS_FAMILY_FALLBACK_IPV4] = "ipv4-fallback",
+};
+
+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 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_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);
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(link_local_address_family, AddressFamily, ADDRESS_FAMILY_YES);
+DEFINE_STRING_TABLE_LOOKUP(routing_policy_rule_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(dhcp_lease_server_type, sd_dhcp_lease_server_type);
+
+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;
+}
+
+/* Router lifetime can be set with netlink interface since kernel >= 4.5
+ * so for the supported kernel we don't need to expire routes in userspace */
+int kernel_route_expiration_supported(void) {
+ static int cached = -1;
+ int r;
+
+ if (cached < 0) {
+ Condition c = {
+ .type = CONDITION_KERNEL_VERSION,
+ .parameter = (char *) ">= 4.5"
+ };
+ r = condition_test(&c, NULL);
+ if (r < 0)
+ return r;
+
+ cached = r;
+ }
+ return cached;
+}
+
+static void network_config_hash_func(const NetworkConfigSection *c, struct siphash *state) {
+ siphash24_compress_string(c->filename, state);
+ siphash24_compress(&c->line, sizeof(c->line), state);
+}
+
+static int network_config_compare_func(const NetworkConfigSection *x, const NetworkConfigSection *y) {
+ int r;
+
+ r = strcmp(x->filename, y->filename);
+ if (r != 0)
+ return r;
+
+ return CMP(x->line, y->line);
+}
+
+DEFINE_HASH_OPS(network_config_hash_ops, NetworkConfigSection, network_config_hash_func, network_config_compare_func);
+
+int network_config_section_new(const char *filename, unsigned line, NetworkConfigSection **s) {
+ NetworkConfigSection *cs;
+
+ cs = malloc0(offsetof(NetworkConfigSection, filename) + strlen(filename) + 1);
+ if (!cs)
+ return -ENOMEM;
+
+ strcpy(cs->filename, filename);
+ cs->line = line;
+
+ *s = TAKE_PTR(cs);
+
+ return 0;
+}
+
+void network_config_section_free(NetworkConfigSection *cs) {
+ free(cs);
+}
+
+unsigned hashmap_find_free_section_line(Hashmap *hashmap) {
+ NetworkConfigSection *cs;
+ unsigned n = 0;
+ void *entry;
+
+ HASHMAP_FOREACH_KEY(entry, cs, hashmap)
+ if (n < cs->line)
+ n = cs->line;
+
+ return n + 1;
+}
diff --git a/src/network/networkd-util.h b/src/network/networkd-util.h
new file mode 100644
index 0000000..6100a00
--- /dev/null
+++ b/src/network/networkd-util.h
@@ -0,0 +1,83 @@
+/* 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 "string-util.h"
+
+typedef enum AddressFamily {
+ /* This is a bitmask, though it usually doesn't feel that way! */
+ ADDRESS_FAMILY_NO = 0,
+ ADDRESS_FAMILY_IPV4 = 1 << 0,
+ ADDRESS_FAMILY_IPV6 = 1 << 1,
+ ADDRESS_FAMILY_YES = ADDRESS_FAMILY_IPV4 | ADDRESS_FAMILY_IPV6,
+ ADDRESS_FAMILY_FALLBACK_IPV4 = 1 << 2,
+ ADDRESS_FAMILY_FALLBACK = ADDRESS_FAMILY_FALLBACK_IPV4 | ADDRESS_FAMILY_IPV6,
+ _ADDRESS_FAMILY_MAX,
+ _ADDRESS_FAMILY_INVALID = -1,
+} AddressFamily;
+
+typedef struct NetworkConfigSection {
+ unsigned line;
+ bool invalid;
+ char filename[];
+} NetworkConfigSection;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_link_local_address_family);
+CONFIG_PARSER_PROTOTYPE(config_parse_address_family_with_kernel);
+
+const char *address_family_to_string(AddressFamily b) _const_;
+AddressFamily address_family_from_string(const char *s) _pure_;
+
+const char *link_local_address_family_to_string(AddressFamily b) _const_;
+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 *duplicate_address_detection_address_family_to_string(AddressFamily b) _const_;
+AddressFamily duplicate_address_detection_address_family_from_string(const char *s) _pure_;
+
+const char *dhcp_lease_server_type_to_string(sd_dhcp_lease_server_type t) _const_;
+sd_dhcp_lease_server_type dhcp_lease_server_type_from_string(const char *s) _pure_;
+
+int kernel_route_expiration_supported(void);
+
+int network_config_section_new(const char *filename, unsigned line, NetworkConfigSection **s);
+void network_config_section_free(NetworkConfigSection *network);
+DEFINE_TRIVIAL_CLEANUP_FUNC(NetworkConfigSection*, network_config_section_free);
+extern const struct hash_ops network_config_hash_ops;
+unsigned hashmap_find_free_section_line(Hashmap *hashmap);
+
+static inline bool section_is_invalid(NetworkConfigSection *section) {
+ /* If this returns false, then it does _not_ mean the section is valid. */
+
+ if (!section)
+ return false;
+
+ return section->invalid;
+}
+
+#define DEFINE_NETWORK_SECTION_FUNCTIONS(type, free_func) \
+ static inline void free_func##_or_set_invalid(type *p) { \
+ assert(p); \
+ \
+ if (p->section) \
+ p->section->invalid = true; \
+ else \
+ free_func(p); \
+ } \
+ DEFINE_TRIVIAL_CLEANUP_FUNC(type*, free_func); \
+ DEFINE_TRIVIAL_CLEANUP_FUNC(type*, free_func##_or_set_invalid);
+
+static inline int log_message_warning_errno(sd_netlink_message *m, int err, const char *msg) {
+ const char *err_msg = NULL;
+
+ (void) sd_netlink_message_read_string(m, NLMSGERR_ATTR_MSG, &err_msg);
+ return log_warning_errno(err, "%s: %s%s%m", msg, strempty(err_msg), err_msg ? " " : "");
+}
diff --git a/src/network/networkd-wifi.c b/src/network/networkd-wifi.c
new file mode 100644
index 0000000..0f2def7
--- /dev/null
+++ b/src/network/networkd-wifi.c
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/ethernet.h>
+#include <linux/nl80211.h>
+
+#include "sd-bus.h"
+
+#include "bus-util.h"
+#include "ether-addr-util.h"
+#include "netlink-internal.h"
+#include "netlink-util.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-wifi.h"
+#include "string-util.h"
+#include "wifi-util.h"
+
+int wifi_get_info(Link *link) {
+ const char *type;
+ int r, s = 0;
+
+ assert(link);
+
+ if (!link->sd_device)
+ return 0;
+
+ r = sd_device_get_devtype(link->sd_device, &type);
+ if (r == -ENOENT)
+ return 0;
+ else if (r < 0)
+ return r;
+
+ if (!streq(type, "wlan"))
+ return 0;
+
+ _cleanup_free_ char *ssid = NULL;
+ r = wifi_get_interface(link->manager->genl, link->ifindex, &link->wlan_iftype, &ssid);
+ if (r < 0)
+ return r;
+ if (r > 0 && streq_ptr(link->ssid, ssid))
+ r = 0;
+ free_and_replace(link->ssid, ssid);
+
+ if (link->wlan_iftype == NL80211_IFTYPE_STATION) {
+ struct ether_addr old_bssid = link->bssid;
+ s = wifi_get_station(link->manager->genl, link->ifindex, &link->bssid);
+ if (s < 0)
+ return s;
+ if (s > 0 && memcmp(&old_bssid, &link->bssid, sizeof old_bssid) == 0)
+ s = 0;
+ }
+
+ if (r > 0 || s > 0) {
+ char buf[ETHER_ADDR_TO_STRING_MAX];
+
+ if (link->wlan_iftype == NL80211_IFTYPE_STATION && link->ssid)
+ log_link_info(link, "Connected WiFi access point: %s (%s)",
+ link->ssid, ether_addr_to_string(&link->bssid, buf));
+ return 1;
+ }
+ return 0;
+}
diff --git a/src/network/networkd-wifi.h b/src/network/networkd-wifi.h
new file mode 100644
index 0000000..ab868eb
--- /dev/null
+++ b/src/network/networkd-wifi.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+typedef struct Link Link;
+
+int wifi_get_info(Link *link);
diff --git a/src/network/networkd.c b/src/network/networkd.c
new file mode 100644
index 0000000..b448d9b
--- /dev/null
+++ b/src/network/networkd.c
@@ -0,0 +1,110 @@
+/* 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 "capability-util.h"
+#include "daemon-util.h"
+#include "main-func.h"
+#include "mkdir.h"
+#include "networkd-conf.h"
+#include "networkd-manager.h"
+#include "signal-util.h"
+#include "user-util.h"
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_(notify_on_cleanup) const char *notify_message = NULL;
+ int r;
+
+ log_setup_service();
+
+ 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);
+ if (r < 0)
+ return log_error_errno(r, "Could not create manager: %m");
+
+ r = manager_connect_bus(m);
+ if (r < 0)
+ return log_error_errno(r, "Could not connect to bus: %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..5339e5e
--- /dev/null
+++ b/src/network/networkd.conf
@@ -0,0 +1,21 @@
+# 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.
+# You can change settings by editing this file.
+# Defaults can be restored by simply deleting this file.
+#
+# See networkd.conf(5) for details
+
+[Network]
+#SpeedMeter=no
+#SpeedMeterIntervalSec=10sec
+#ManageForeignRoutes=yes
+
+[DHCP]
+#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..366c630
--- /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"
+ "http://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..9e27f72
--- /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"
+ "http://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>http://www.freedesktop.org/wiki/Software/systemd</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..4d1bb45
--- /dev/null
+++ b/src/network/systemd-networkd.pkla
@@ -0,0 +1,4 @@
+[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..b9077c1
--- /dev/null
+++ b/src/network/systemd-networkd.rules
@@ -0,0 +1,10 @@
+// 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..76fb718
--- /dev/null
+++ b/src/network/tc/cake.c
@@ -0,0 +1,163 @@
+/* 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-util.h"
+
+static int cake_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ CommonApplicationsKeptEnhanced *c;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ c = CAKE(qdisc);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "cake");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ if (c->bandwidth > 0) {
+ r = sd_netlink_message_append_u64(req, TCA_CAKE_BASE_RATE64, c->bandwidth);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_CAKE_BASE_RATE64 attribute: %m");
+ }
+
+ r = sd_netlink_message_append_s32(req, TCA_CAKE_OVERHEAD, c->overhead);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_CAKE_OVERHEAD attribute: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ uint64_t k;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+ qdisc = NULL;
+
+ 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 = data;
+ int32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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 = 0;
+ qdisc = NULL;
+ 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;
+ qdisc = NULL;
+ return 0;
+}
+
+const QDiscVTable cake_vtable = {
+ .object_size = sizeof(CommonApplicationsKeptEnhanced),
+ .tca_kind = "cake",
+ .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..1da28b7
--- /dev/null
+++ b/src/network/tc/cake.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 CommonApplicationsKeptEnhanced {
+ QDisc meta;
+
+ int overhead;
+ uint64_t bandwidth;
+
+} 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);
diff --git a/src/network/tc/codel.c b/src/network/tc/codel.c
new file mode 100644
index 0000000..807c247
--- /dev/null
+++ b/src/network/tc/codel.c
@@ -0,0 +1,255 @@
+/* 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);
+
+ cd = CODEL(qdisc);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "codel");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ if (cd->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_LIMIT, cd->packet_limit);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_CODEL_LIMIT attribute: %m");
+ }
+
+ if (cd->interval_usec > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_INTERVAL, cd->interval_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_CODEL_INTERVAL attribute: %m");
+ }
+
+ if (cd->target_usec > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_TARGET, cd->target_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_CODEL_TARGET attribute: %m");
+ }
+
+ if (cd->ecn >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_CODEL_ECN, cd->ecn);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_CODEL_ECN attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_CODEL_CE_THRESHOLD attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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 = data;
+ usec_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue");
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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..86b7f43
--- /dev/null
+++ b/src/network/tc/drr.c
@@ -0,0 +1,109 @@
+/* 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);
+
+ drr = TCLASS_TO_DRR(tclass);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "drr");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ if (drr->quantum > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_DRR_QUANTUM, drr->quantum);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_DRR_QUANTUM, attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ uint64_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ tclass = NULL;
+ 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;
+
+ tclass = NULL;
+ 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..8214a57
--- /dev/null
+++ b/src/network/tc/ets.c
@@ -0,0 +1,344 @@
+/* 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 "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);
+
+ ets = ETS(qdisc);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "ets");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ r = sd_netlink_message_append_u8(req, TCA_ETS_NBANDS, ets->n_bands);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_ETS_NBANDS attribute: %m");
+
+ if (ets->n_strict > 0) {
+ r = sd_netlink_message_append_u8(req, TCA_ETS_NSTRICT, ets->n_strict);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_ETS_NSTRICT attribute: %m");
+ }
+
+ if (ets->n_quanta > 0) {
+ r = sd_netlink_message_open_container(req, TCA_ETS_QUANTA);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_ETS_QUANTA: %m");
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_ETS_QUANTA_BAND attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_ETS_QUANTA: %m");
+ }
+
+ if (ets->n_prio > 0) {
+ r = sd_netlink_message_open_container(req, TCA_ETS_PRIOMAP);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_ETS_PRIOMAP: %m");
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_ETS_PRIOMAP_BAND attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_ETS_PRIOMAP: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ uint8_t v, *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue.");
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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..8b1fa6e
--- /dev/null
+++ b/src/network/tc/fifo.c
@@ -0,0 +1,187 @@
+/* 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) {
+ struct tc_fifo_qopt opt = {};
+ FirstInFirstOut *fifo;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ switch(qdisc->kind) {
+ case QDISC_KIND_PFIFO:
+ fifo = PFIFO(qdisc);
+ break;
+ case QDISC_KIND_BFIFO:
+ fifo = BFIFO(qdisc);
+ break;
+ case QDISC_KIND_PFIFO_HEAD_DROP:
+ fifo = PFIFO_HEAD_DROP(qdisc);
+ break;
+ default:
+ assert_not_reached("Invalid QDisc kind.");
+ }
+
+ opt.limit = fifo->limit;
+
+ r = sd_netlink_message_append_data(req, TCA_OPTIONS, &opt, sizeof(struct tc_fifo_qopt));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_OPTIONS attribute: %m");
+
+ 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 = data;
+ FirstInFirstOut *fifo;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid QDisc kind.");
+ }
+
+ if (isempty(rvalue)) {
+ fifo->limit = 0;
+
+ qdisc = NULL;
+ 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;
+ }
+
+ qdisc = NULL;
+ 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 = data;
+ FirstInFirstOut *fifo;
+ uint64_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+
+ qdisc = NULL;
+ 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..958f65a
--- /dev/null
+++ b/src/network/tc/fq-codel.c
@@ -0,0 +1,355 @@
+/* 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);
+
+ fqcd = FQ_CODEL(qdisc);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq_codel");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ if (fqcd->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_LIMIT, fqcd->packet_limit);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_CODEL_LIMIT attribute: %m");
+ }
+
+ if (fqcd->flows > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_FLOWS, fqcd->flows);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_CODEL_FLOWS attribute: %m");
+ }
+
+ if (fqcd->quantum > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_QUANTUM, fqcd->quantum);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_CODEL_QUANTUM attribute: %m");
+ }
+
+ if (fqcd->interval_usec > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_INTERVAL, fqcd->interval_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_CODEL_INTERVAL attribute: %m");
+ }
+
+ if (fqcd->target_usec > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_TARGET, fqcd->target_usec);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_CODEL_TARGET attribute: %m");
+ }
+
+ if (fqcd->ecn >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_ECN, fqcd->ecn);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_CODEL_ECN attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_FQ_CODEL_CE_THRESHOLD attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_FQ_CODEL_MEMORY_LIMIT attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ uint32_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue.");
+
+ 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_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 = data;
+ usec_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue.");
+
+ 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_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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ 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;
+ }
+
+ fqcd->ecn = r;
+ qdisc = NULL;
+
+ 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 = data;
+ uint64_t sz;
+ uint32_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue.");
+
+ if (isempty(rvalue)) {
+ if (STR_IN_SET(lvalue, "MemoryLimitBytes", "MemoryLimit"))
+ *p = UINT32_MAX;
+ else
+ *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;
+}
+
+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..c7d7623
--- /dev/null
+++ b/src/network/tc/fq-pie.c
@@ -0,0 +1,103 @@
+/* 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);
+
+ fq_pie = FQ_PIE(qdisc);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq_pie");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_FQ_PIE_PLIMIT attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ uint32_t val;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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..d48aea8
--- /dev/null
+++ b/src/network/tc/fq.c
@@ -0,0 +1,420 @@
+/* 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);
+
+ fq = FQ(qdisc);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ if (fq->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_PLIMIT, fq->packet_limit);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_PLIMIT attribute: %m");
+ }
+
+ if (fq->flow_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_FLOW_PLIMIT, fq->flow_limit);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_FLOW_PLIMIT attribute: %m");
+ }
+
+ if (fq->quantum > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_QUANTUM, fq->quantum);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_QUANTUM attribute: %m");
+ }
+
+ if (fq->initial_quantum > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_INITIAL_QUANTUM, fq->initial_quantum);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_INITIAL_QUANTUM attribute: %m");
+ }
+
+ if (fq->pacing >= 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_RATE_ENABLE, fq->pacing);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_RATE_ENABLE attribute: %m");
+ }
+
+ if (fq->max_rate > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_FLOW_MAX_RATE, fq->max_rate);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_FLOW_MAX_RATE attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_FQ_BUCKETS_LOG attribute: %m");
+ }
+
+ if (fq->orphan_mask > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_FQ_ORPHAN_MASK, fq->orphan_mask);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_FQ_ORPHAN_MASK attribute: %m");
+ }
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_FQ_CE_THRESHOLD attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ uint32_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue");
+
+ 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 = data;
+ uint64_t sz;
+ uint32_t *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue");
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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 = data;
+ usec_t sec;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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 = data;
+ uint64_t sz;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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..46a9ead
--- /dev/null
+++ b/src/network/tc/gred.c
@@ -0,0 +1,196 @@
+/* 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;
+ struct tc_gred_sopt opt = {};
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ gred = GRED(qdisc);
+
+ opt.DPs = gred->virtual_queues;
+ opt.def_DP = gred->default_virtual_queue;
+
+ if (gred->grio >= 0)
+ opt.grio = gred->grio;
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "gred");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ r = sd_netlink_message_append_data(req, TCA_GRED_DPS, &opt, sizeof(struct tc_gred_sopt));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_GRED_DPS attribute: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ uint32_t *p;
+ uint32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue.");
+
+ if (isempty(rvalue)) {
+ *p = 0;
+
+ qdisc = 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;
+ }
+
+ if (v > MAX_DPs)
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid '%s=', ignoring assignment: %s",
+ lvalue, rvalue);
+
+ *p = v;
+ qdisc = NULL;
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ 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;
+ }
+
+ gred->grio = r;
+ qdisc = NULL;
+
+ 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..69c02f4
--- /dev/null
+++ b/src/network/tc/hhf.c
@@ -0,0 +1,98 @@
+/* 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);
+
+ hhf = HHF(qdisc);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "hhf");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ if (hhf->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_HHF_BACKLOG_LIMIT, hhf->packet_limit);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_HHF_BACKLOG_LIMIT attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+ }
+
+ qdisc = NULL;
+
+ 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..0969587
--- /dev/null
+++ b/src/network/tc/htb.c
@@ -0,0 +1,489 @@
+/* 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 "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;
+ struct tc_htb_glob opt = {
+ .version = 3,
+ };
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ htb = HTB(qdisc);
+
+ opt.rate2quantum = htb->rate_to_quantum;
+ opt.defcls = htb->default_class;
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "htb");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ r = sd_netlink_message_append_data(req, TCA_HTB_INIT, &opt, sizeof(opt));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_HTB_INIT attribute: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+ }
+
+ qdisc = NULL;
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+ }
+
+ qdisc = NULL;
+
+ 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;
+ struct tc_htb_opt opt = {};
+ uint32_t rtab[256], ctab[256];
+ int r;
+
+ assert(link);
+ assert(tclass);
+ assert(req);
+
+ htb = TCLASS_TO_HTB(tclass);
+
+ opt.prio = htb->priority;
+ opt.quantum = htb->quantum;
+ opt.rate.rate = (htb->rate >= (1ULL << 32)) ? ~0U : htb->rate;
+ opt.ceil.rate = (htb->ceil_rate >= (1ULL << 32)) ? ~0U : htb->ceil_rate;
+ opt.rate.overhead = htb->overhead;
+ opt.ceil.overhead = htb->overhead;
+
+ r = tc_transmit_time(htb->rate, htb->buffer, &opt.buffer);
+ if (r < 0)
+ return log_link_error_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_error_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_error_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_error_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 log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ r = sd_netlink_message_append_data(req, TCA_HTB_PARMS, &opt, sizeof(opt));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_HTB_PARMS attribute: %m");
+
+ if (htb->rate >= (1ULL << 32)) {
+ r = sd_netlink_message_append_u64(req, TCA_HTB_RATE64, htb->rate);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_HTB_RATE64 attribute: %m");
+ }
+
+ if (htb->ceil_rate >= (1ULL << 32)) {
+ r = sd_netlink_message_append_u64(req, TCA_HTB_CEIL64, htb->ceil_rate);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_HTB_CEIL64 attribute: %m");
+ }
+
+ r = sd_netlink_message_append_data(req, TCA_HTB_RTAB, rtab, sizeof(rtab));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_HTB_RTAB attribute: %m");
+
+ r = sd_netlink_message_append_data(req, TCA_HTB_CTAB, ctab, sizeof(ctab));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_HTB_CTAB attribute: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+ 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 = data;
+ uint32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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 = data;
+ uint64_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue");
+
+ 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("Invalid lvalue");
+
+ 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 = data;
+ uint64_t *v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("Invalid lvalue");
+
+ 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..454e556
--- /dev/null
+++ b/src/network/tc/netem.c
@@ -0,0 +1,236 @@
+/* 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) {
+ struct tc_netem_qopt opt = {
+ .limit = 1000,
+ };
+ NetworkEmulator *ne;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ ne = NETEM(qdisc);
+
+ if (ne->limit > 0)
+ opt.limit = ne->limit;
+
+ if (ne->loss > 0)
+ opt.loss = ne->loss;
+
+ if (ne->duplicate > 0)
+ opt.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(struct tc_netem_qopt));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_OPTION attribute: %m");
+
+ 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 = data;
+ NetworkEmulator *ne;
+ usec_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ 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;
+ }
+
+ if (STR_IN_SET(lvalue, "DelaySec", "NetworkEmulatorDelaySec"))
+ ne->delay = u;
+ else if (STR_IN_SET(lvalue, "DelayJitterSec", "NetworkEmulatorDelayJitterSec"))
+ ne->jitter = u;
+
+ qdisc = NULL;
+
+ 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 = data;
+ NetworkEmulator *ne;
+ uint32_t rate;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+
+ qdisc = NULL;
+ 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 = data;
+ NetworkEmulator *ne;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ qdisc = NULL;
+
+ 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;
+ }
+
+ qdisc = NULL;
+ 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..695a381
--- /dev/null
+++ b/src/network/tc/pie.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 "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);
+
+ pie = PIE(qdisc);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "pie");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ if (pie->packet_limit > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_PIE_LIMIT, pie->packet_limit);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_PIE_PLIMIT attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+ }
+
+ qdisc = NULL;
+
+ 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..2add128
--- /dev/null
+++ b/src/network/tc/qdisc.c
@@ -0,0 +1,381 @@
+/* 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-manager.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) {
+ .meta.kind = TC_KIND_QDISC,
+ .family = AF_UNSPEC,
+ .parent = TC_H_ROOT,
+ .kind = kind,
+ };
+ } else {
+ qdisc = malloc0(qdisc_vtable[kind]->object_size);
+ if (!qdisc)
+ return -ENOMEM;
+
+ qdisc->meta.kind = TC_KIND_QDISC,
+ qdisc->family = AF_UNSPEC;
+ 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_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(qdisc_freep) QDisc *qdisc = NULL;
+ TrafficControl *existing;
+ QDisc *q = NULL;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ existing = ordered_hashmap_get(network->tc_by_section, n);
+ if (existing) {
+ if (existing->kind != TC_KIND_QDISC)
+ return -EINVAL;
+
+ q = TC_TO_QDISC(existing);
+
+ if (q->kind != _QDISC_KIND_INVALID &&
+ kind != _QDISC_KIND_INVALID &&
+ q->kind != kind)
+ return -EINVAL;
+
+ if (q->kind == kind || kind == _QDISC_KIND_INVALID) {
+ *ret = q;
+ return 0;
+ }
+ }
+
+ r = qdisc_new(kind, &qdisc);
+ if (r < 0)
+ return r;
+
+ if (q) {
+ qdisc->family = q->family;
+ qdisc->handle = q->handle;
+ qdisc->parent = q->parent;
+ qdisc->tca_kind = TAKE_PTR(q->tca_kind);
+
+ qdisc_free(q);
+ }
+
+ qdisc->network = network;
+ qdisc->section = TAKE_PTR(n);
+
+ r = ordered_hashmap_ensure_allocated(&network->tc_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(network->tc_by_section, qdisc->section, TC(qdisc));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(qdisc);
+ return 0;
+}
+
+void qdisc_free(QDisc *qdisc) {
+ if (!qdisc)
+ return;
+
+ if (qdisc->network && qdisc->section)
+ ordered_hashmap_remove(qdisc->network->tc_by_section, qdisc->section);
+
+ network_config_section_free(qdisc->section);
+
+ free(qdisc->tca_kind);
+ free(qdisc);
+}
+
+static int qdisc_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->tc_messages > 0);
+ link->tc_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ 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;
+}
+
+int qdisc_configure(Link *link, QDisc *qdisc) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+
+ r = sd_rtnl_message_new_qdisc(link->manager->rtnl, &req, RTM_NEWQDISC, qdisc->family, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create RTM_NEWQDISC message: %m");
+
+ r = sd_rtnl_message_set_qdisc_parent(req, qdisc->parent);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create tcm_parent message: %m");
+
+ if (qdisc->handle != TC_H_UNSPEC) {
+ r = sd_rtnl_message_set_qdisc_handle(req, qdisc->handle);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set tcm_handle message: %m");
+ }
+
+ if (QDISC_VTABLE(qdisc)) {
+ if (QDISC_VTABLE(qdisc)->fill_tca_kind) {
+ r = QDISC_VTABLE(qdisc)->fill_tca_kind(link, qdisc, req);
+ if (r < 0)
+ return r;
+ } else {
+ r = sd_netlink_message_append_string(req, TCA_KIND, QDISC_VTABLE(qdisc)->tca_kind);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_KIND attribute: %m");
+ }
+
+ if (QDISC_VTABLE(qdisc)->fill_message) {
+ r = QDISC_VTABLE(qdisc)->fill_message(link, qdisc, req);
+ if (r < 0)
+ return r;
+ }
+ } else {
+ r = sd_netlink_message_append_string(req, TCA_KIND, qdisc->tca_kind);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_KIND attribute: %m");
+ }
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, qdisc_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);
+ link->tc_messages++;
+
+ return 0;
+}
+
+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;
+}
+
+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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ if (qdisc->handle == 0)
+ qdisc->handle = TC_H_UNSPEC;
+ } 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);
+
+ qdisc = NULL;
+
+ 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 = data;
+ uint16_t n;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ qdisc = NULL;
+ 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;
+ qdisc = NULL;
+
+ return 0;
+}
diff --git a/src/network/tc/qdisc.h b/src/network/tc/qdisc.h
new file mode 100644
index 0000000..f9a9954
--- /dev/null
+++ b/src/network/tc/qdisc.h
@@ -0,0 +1,107 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "networkd-util.h"
+#include "tc.h"
+
+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 = -1,
+} QDiscKind;
+
+typedef struct QDisc {
+ TrafficControl meta;
+
+ NetworkConfigSection *section;
+ Network *network;
+
+ int family;
+ 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_tca_kind)(Link *link, QDisc *qdisc, sd_netlink_message *m);
+ int (*fill_message)(Link *link, QDisc *qdisc, sd_netlink_message *m);
+ int (*verify)(QDisc *qdisc);
+} 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; \
+ }
+
+/* For casting the various qdisc kinds into a qdisc */
+#define QDISC(q) (&(q)->meta)
+
+void qdisc_free(QDisc *qdisc);
+int qdisc_new_static(QDiscKind kind, Network *network, const char *filename, unsigned section_line, QDisc **ret);
+
+int qdisc_configure(Link *link, QDisc *qdisc);
+int qdisc_section_verify(QDisc *qdisc, bool *has_root, bool *has_clsact);
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(QDisc, qdisc_free);
+
+DEFINE_TC_CAST(QDISC, QDisc);
+
+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..320f2c1
--- /dev/null
+++ b/src/network/tc/qfq.c
@@ -0,0 +1,178 @@
+/* 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);
+
+ qfq = TCLASS_TO_QFQ(tclass);
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "qfq");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ if (qfq->weight > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_QFQ_WEIGHT, qfq->weight);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_QFQ_WEIGHT attribute: %m");
+ }
+
+ if (qfq->max_packet > 0) {
+ r = sd_netlink_message_append_u32(req, TCA_QFQ_LMAX, qfq->max_packet);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_QFQ_LMAX attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+ 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 = data;
+ uint32_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ 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;
+ }
+
+ 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;
+ tclass = NULL;
+
+ 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 = data;
+ uint64_t v;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ 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 (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;
+ tclass = NULL;
+
+ 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..674fdf6
--- /dev/null
+++ b/src/network/tc/sfb.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 "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;
+ 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,
+ };
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ sfb = SFB(qdisc);
+
+ opt.limit = sfb->packet_limit;
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "sfb");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ r = sd_netlink_message_append_data(req, TCA_SFB_PARMS, &opt, sizeof(struct tc_sfb_qopt));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_SFB_PARMS attribute: %m");
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+ }
+
+ qdisc = NULL;
+
+ 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..387be83
--- /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;
+ struct tc_sfq_qopt_v1 opt = {};
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ sfq = SFQ(qdisc);
+
+ opt.v0.perturb_period = sfq->perturb_period / USEC_PER_SEC;
+
+ r = sd_netlink_message_append_data(req, TCA_OPTIONS, &opt, sizeof(struct tc_sfq_qopt_v1));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_OPTIONS attribute: %m");
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+ }
+
+ qdisc = NULL;
+
+ 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..2d84c5a
--- /dev/null
+++ b/src/network/tc/tbf.c
@@ -0,0 +1,346 @@
+/* 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];
+ struct tc_tbf_qopt opt = {};
+ TokenBucketFilter *tbf;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ tbf = TBF(qdisc);
+
+ opt.rate.rate = tbf->rate >= (1ULL << 32) ? ~0U : tbf->rate;
+ opt.peakrate.rate = tbf->peak_rate >= (1ULL << 32) ? ~0U : tbf->peak_rate;
+
+ 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;
+ }
+
+ opt.rate.mpu = tbf->mpu;
+
+ r = tc_fill_ratespec_and_table(&opt.rate, rtab, tbf->mtu);
+ if (r < 0)
+ return log_link_error_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_error_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_error_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_error_errno(link, r, "Failed to calculate mtu size: %m");
+ }
+
+ r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "tbf");
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not open container TCA_OPTIONS: %m");
+
+ r = sd_netlink_message_append_data(req, TCA_TBF_PARMS, &opt, sizeof(struct tc_tbf_qopt));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_TBF_PARMS attribute: %m");
+
+ r = sd_netlink_message_append_data(req, TCA_TBF_BURST, &tbf->burst, sizeof(tbf->burst));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_TBF_BURST attribute: %m");
+
+ if (tbf->rate >= (1ULL << 32)) {
+ r = sd_netlink_message_append_u64(req, TCA_TBF_RATE64, tbf->rate);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_TBF_RATE64 attribute: %m");
+ }
+
+ r = sd_netlink_message_append_data(req, TCA_TBF_RTAB, rtab, sizeof(rtab));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_TBF_RTAB attribute: %m");
+
+ 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 log_link_error_errno(link, r, "Could not append TCA_TBF_PRATE64 attribute: %m");
+ }
+
+ r = sd_netlink_message_append_u32(req, TCA_TBF_PBURST, tbf->mtu);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_TBF_PBURST attribute: %m");
+
+ r = sd_netlink_message_append_data(req, TCA_TBF_PTAB, ptab, sizeof(ptab));
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_TBF_PTAB attribute: %m");
+ }
+
+ r = sd_netlink_message_close_container(req);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not close container TCA_OPTIONS: %m");
+
+ 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 = data;
+ TokenBucketFilter *tbf;
+ uint64_t k;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("unknown lvalue");
+
+ qdisc = NULL;
+ 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("unknown lvalue");
+
+ qdisc = NULL;
+
+ 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 = data;
+ TokenBucketFilter *tbf;
+ uint64_t k, *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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("unknown lvalue");
+
+ if (isempty(rvalue)) {
+ *p = 0;
+
+ qdisc = NULL;
+ 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 = data;
+ TokenBucketFilter *tbf;
+ usec_t u;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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 LimitSize= 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 LimitSize= 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: Burst= 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..3e10b50
--- /dev/null
+++ b/src/network/tc/tc-util.c
@@ -0,0 +1,132 @@
+/* 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 "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 *percent) {
+ int r;
+
+ assert(s);
+ assert(percent);
+
+ r = parse_permille(s);
+ if (r < 0)
+ return r;
+
+ *percent = (double) r / 1000 * 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..c32b040
--- /dev/null
+++ b/src/network/tc/tc.c
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "macro.h"
+#include "qdisc.h"
+#include "tc.h"
+#include "tclass.h"
+
+void traffic_control_free(TrafficControl *tc) {
+ if (!tc)
+ return;
+
+ switch (tc->kind) {
+ case TC_KIND_QDISC:
+ qdisc_free(TC_TO_QDISC(tc));
+ break;
+ case TC_KIND_TCLASS:
+ tclass_free(TC_TO_TCLASS(tc));
+ break;
+ default:
+ assert_not_reached("Invalid traffic control type");
+ }
+}
+
+static int traffic_control_configure(Link *link, TrafficControl *tc) {
+ assert(link);
+ assert(tc);
+
+ switch(tc->kind) {
+ case TC_KIND_QDISC:
+ return qdisc_configure(link, TC_TO_QDISC(tc));
+ case TC_KIND_TCLASS:
+ return tclass_configure(link, TC_TO_TCLASS(tc));
+ default:
+ assert_not_reached("Invalid traffic control type");
+ }
+}
+
+int link_configure_traffic_control(Link *link) {
+ TrafficControl *tc;
+ int r;
+
+ link->tc_configured = false;
+ link->tc_messages = 0;
+
+ ORDERED_HASHMAP_FOREACH(tc, link->network->tc_by_section) {
+ r = traffic_control_configure(link, tc);
+ if (r < 0)
+ return r;
+ }
+
+ if (link->tc_messages == 0)
+ link->tc_configured = true;
+ else
+ log_link_debug(link, "Configuring traffic control");
+
+ return 0;
+}
+
+static int traffic_control_section_verify(TrafficControl *tc, bool *qdisc_has_root, bool *qdisc_has_clsact) {
+ assert(tc);
+
+ switch(tc->kind) {
+ case TC_KIND_QDISC:
+ return qdisc_section_verify(TC_TO_QDISC(tc), qdisc_has_root, qdisc_has_clsact);
+ case TC_KIND_TCLASS:
+ return tclass_section_verify(TC_TO_TCLASS(tc));
+ default:
+ assert_not_reached("Invalid traffic control type");
+ }
+}
+
+void network_drop_invalid_traffic_control(Network *network) {
+ bool has_root = false, has_clsact = false;
+ TrafficControl *tc;
+
+ assert(network);
+
+ ORDERED_HASHMAP_FOREACH(tc, network->tc_by_section)
+ if (traffic_control_section_verify(tc, &has_root, &has_clsact) < 0)
+ traffic_control_free(tc);
+}
diff --git a/src/network/tc/tc.h b/src/network/tc/tc.h
new file mode 100644
index 0000000..7fbd744
--- /dev/null
+++ b/src/network/tc/tc.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "networkd-link.h"
+
+typedef enum TrafficControlKind {
+ TC_KIND_QDISC,
+ TC_KIND_TCLASS,
+ TC_KIND_FILTER,
+ _TC_KIND_MAX,
+ _TC_KIND_INVALID = -1,
+} TrafficControlKind;
+
+typedef struct TrafficControl {
+ TrafficControlKind kind;
+} TrafficControl;
+
+/* For casting a tc into the various tc kinds */
+#define DEFINE_TC_CAST(UPPERCASE, MixedCase) \
+ static inline MixedCase* TC_TO_##UPPERCASE(TrafficControl *tc) { \
+ if (_unlikely_(!tc || tc->kind != TC_KIND_##UPPERCASE)) \
+ return NULL; \
+ \
+ return (MixedCase*) tc; \
+ }
+
+/* For casting the various tc kinds into a tc */
+#define TC(tc) (&(tc)->meta)
+
+void traffic_control_free(TrafficControl *tc);
+int link_configure_traffic_control(Link *link);
+void network_drop_invalid_traffic_control(Network *network);
diff --git a/src/network/tc/tclass.c b/src/network/tc/tclass.c
new file mode 100644
index 0000000..21b26b0
--- /dev/null
+++ b/src/network/tc/tclass.c
@@ -0,0 +1,289 @@
+/* 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-manager.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;
+
+ tclass = malloc0(tclass_vtable[kind]->object_size);
+ if (!tclass)
+ return -ENOMEM;
+
+ tclass->meta.kind = TC_KIND_TCLASS,
+ 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_(network_config_section_freep) NetworkConfigSection *n = NULL;
+ _cleanup_(tclass_freep) TClass *tclass = NULL;
+ TrafficControl *existing;
+ int r;
+
+ assert(network);
+ assert(ret);
+ assert(filename);
+ assert(section_line > 0);
+
+ r = network_config_section_new(filename, section_line, &n);
+ if (r < 0)
+ return r;
+
+ existing = ordered_hashmap_get(network->tc_by_section, n);
+ if (existing) {
+ TClass *t;
+
+ if (existing->kind != TC_KIND_TCLASS)
+ return -EINVAL;
+
+ t = TC_TO_TCLASS(existing);
+
+ if (t->kind != kind)
+ return -EINVAL;
+
+ *ret = t;
+ return 0;
+ }
+
+ r = tclass_new(kind, &tclass);
+ if (r < 0)
+ return r;
+
+ tclass->network = network;
+ tclass->section = TAKE_PTR(n);
+
+ r = ordered_hashmap_ensure_allocated(&network->tc_by_section, &network_config_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_hashmap_put(network->tc_by_section, tclass->section, tclass);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(tclass);
+ return 0;
+}
+
+void tclass_free(TClass *tclass) {
+ if (!tclass)
+ return;
+
+ if (tclass->network && tclass->section)
+ ordered_hashmap_remove(tclass->network->tc_by_section, tclass->section);
+
+ network_config_section_free(tclass->section);
+
+ free(tclass);
+}
+
+static int tclass_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->tc_messages > 0);
+ link->tc_messages--;
+
+ if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
+ return 1;
+
+ 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;
+}
+
+int tclass_configure(Link *link, TClass *tclass) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->manager->rtnl);
+ assert(link->ifindex > 0);
+
+ r = sd_rtnl_message_new_tclass(link->manager->rtnl, &req, RTM_NEWTCLASS, AF_UNSPEC, link->ifindex);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create RTM_NEWTCLASS message: %m");
+
+ r = sd_rtnl_message_set_tclass_parent(req, tclass->parent);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not create tcm_parent message: %m");
+
+ if (tclass->classid != TC_H_UNSPEC) {
+ r = sd_rtnl_message_set_tclass_handle(req, tclass->classid);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not set tcm_handle message: %m");
+ }
+
+ r = sd_netlink_message_append_string(req, TCA_KIND, TCLASS_VTABLE(tclass)->tca_kind);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_KIND attribute: %m");
+
+ if (TCLASS_VTABLE(tclass)->fill_message) {
+ r = TCLASS_VTABLE(tclass)->fill_message(link, tclass, req);
+ if (r < 0)
+ return r;
+ }
+
+ r = netlink_call_async(link->manager->rtnl, NULL, req, tclass_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);
+ link->tc_messages++;
+
+ return 0;
+}
+
+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;
+}
+
+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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ }
+ }
+
+ tclass = NULL;
+
+ 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 = data;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+ tclass = NULL;
+ 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;
+ }
+
+ tclass = NULL;
+
+ return 0;
+}
diff --git a/src/network/tc/tclass.h b/src/network/tc/tclass.h
new file mode 100644
index 0000000..f02a6a7
--- /dev/null
+++ b/src/network/tc/tclass.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright © 2019 VMware, Inc. */
+#pragma once
+
+#include "conf-parser.h"
+#include "networkd-link.h"
+#include "networkd-network.h"
+#include "networkd-util.h"
+#include "tc.h"
+
+typedef enum TClassKind {
+ TCLASS_KIND_DRR,
+ TCLASS_KIND_HTB,
+ TCLASS_KIND_QFQ,
+ _TCLASS_KIND_MAX,
+ _TCLASS_KIND_INVALID = -1,
+} TClassKind;
+
+typedef struct TClass {
+ TrafficControl meta;
+
+ NetworkConfigSection *section;
+ Network *network;
+
+ uint32_t classid;
+ uint32_t parent;
+
+ TClassKind 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; \
+ }
+
+/* For casting the various tclass kinds into a tclass */
+#define TCLASS(t) (&(t)->meta)
+
+void tclass_free(TClass *tclass);
+int tclass_new_static(TClassKind kind, Network *network, const char *filename, unsigned section_line, TClass **ret);
+
+int tclass_configure(Link *link, TClass *tclass);
+int tclass_section_verify(TClass *tclass);
+
+DEFINE_NETWORK_SECTION_FUNCTIONS(TClass, tclass_free);
+
+DEFINE_TC_CAST(TCLASS, TClass);
+
+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..0da2fc3
--- /dev/null
+++ b/src/network/tc/teql.c
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "macro.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "teql.h"
+
+static int trivial_link_equalizer_fill_tca_kind(Link *link, QDisc *qdisc, sd_netlink_message *req) {
+ char kind[STRLEN("teql") + DECIMAL_STR_MAX(unsigned)];
+ TrivialLinkEqualizer *teql;
+ int r;
+
+ assert(link);
+ assert(qdisc);
+ assert(req);
+
+ teql = TEQL(qdisc);
+
+ xsprintf(kind, "teql%u", teql->id);
+ r = sd_netlink_message_append_string(req, TCA_KIND, kind);
+ if (r < 0)
+ return log_link_error_errno(link, r, "Could not append TCA_KIND attribute: %m");
+
+ return 0;
+}
+
+const QDiscVTable teql_vtable = {
+ .object_size = sizeof(TrivialLinkEqualizer),
+ .fill_tca_kind = trivial_link_equalizer_fill_tca_kind,
+};
+
+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 = data;
+ unsigned id;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ 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;
+
+ qdisc = NULL;
+ 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;
+
+ qdisc = NULL;
+ 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..475cac7
--- /dev/null
+++ b/src/network/test-network-tables.c
@@ -0,0 +1,49 @@
+/* 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-internal.h"
+#include "macvlan.h"
+#include "ndisc-internal.h"
+#include "netlink-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); /* 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(nl_union_link_info_data, NL_UNION_LINK_INFO_DATA);
+ test_table(radv_prefix_delegation, RADV_PREFIX_DELEGATION);
+ test_table(wol, WOL);
+ test_table(lldp_event, SD_LLDP_EVENT);
+ test_table(ndisc_event, SD_NDISC_EVENT);
+
+ test_table_sparse(ipvlan_mode, NETDEV_IPVLAN_MODE);
+ test_table_sparse(macvlan_mode, NETDEV_MACVLAN_MODE);
+ test_table_sparse(address_family, ADDRESS_FAMILY);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/network/test-network.c b/src/network/test-network.c
new file mode 100644
index 0000000..03c9440
--- /dev/null
+++ b/src/network/test-network.c
@@ -0,0 +1,258 @@
+/* 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-util.h"
+#include "network-internal.h"
+#include "networkd-manager.h"
+#include "string-util.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(in_addr_equal(AF_INET, &a, (union in_addr_union *) &addresses[0]));
+ assert_se(in_addr_equal(AF_INET, &b, (union in_addr_union *) &addresses[1]));
+ assert_se(in_addr_equal(AF_INET, &c, (union in_addr_union *) &addresses[2]));
+
+ assert_se((size = deserialize_in6_addrs(&addresses6, addresses_string)) >= 0);
+ assert_se(size == 3);
+ assert_se(in_addr_equal(AF_INET6, &d, (union in_addr_union *) &addresses6[0]));
+ assert_se(in_addr_equal(AF_INET6, &e, (union in_addr_union *) &addresses6[1]));
+ assert_se(in_addr_equal(AF_INET6, &f, (union in_addr_union *) &addresses6[2]));
+}
+
+static void test_deserialize_dhcp_routes(void) {
+ size_t size, allocated;
+
+ {
+ _cleanup_free_ struct sd_dhcp_route *routes = NULL;
+ assert_se(deserialize_dhcp_routes(&routes, &size, &allocated, "") >= 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, &allocated, 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, &allocated, 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, &allocated, routes_string) >= 0);
+ assert_se(size == 0);
+ }
+}
+
+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);
+
+ assert_se(manager_should_reload(manager) == false);
+
+ return 0;
+}
+
+static void test_network_get(Manager *manager, sd_device *loopback) {
+ Network *network;
+ const struct ether_addr mac = ETHER_ADDR_NULL;
+ int r;
+
+ /* Let's hope that the test machine does not have a .network file that applies to loopback device…
+ * But it is still possible, so let's allow that case too. */
+ r = network_get(manager, 0, loopback, "lo", NULL, NULL, &mac, &mac, 0, NULL, NULL, &network);
+ if (r == -ENOENT)
+ /* The expected case */
+ assert_se(!network);
+ else if (r >= 0)
+ assert_se(network);
+ else
+ assert_not_reached("bad error!");
+}
+
+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));
+ 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));
+
+ 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;
+ _cleanup_(sd_device_unrefp) sd_device *loopback = NULL;
+ int ifindex, 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) >= 0);
+
+ r = test_load_config(manager);
+ if (r == -EPERM)
+ return log_tests_skipped("Cannot load configuration");
+ assert_se(r == 0);
+
+ assert_se(sd_device_new_from_syspath(&loopback, "/sys/class/net/lo") >= 0);
+ assert_se(loopback);
+ assert_se(sd_device_get_ifindex(loopback, &ifindex) >= 0);
+ assert_se(ifindex == 1);
+
+ test_network_get(manager, loopback);
+
+ assert_se(manager_enumerate(manager) >= 0);
+ return 0;
+}
diff --git a/src/network/test-networkd-conf.c b/src/network/test-networkd-conf.c
new file mode 100644
index 0000000..c771007
--- /dev/null
+++ b/src/network/test-networkd-conf.c
@@ -0,0 +1,260 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "ether-addr-util.h"
+#include "hexdecoct.h"
+#include "log.h"
+#include "macro.h"
+#include "set.h"
+#include "string-util.h"
+
+#include "network-internal.h"
+#include "networkd-conf.h"
+#include "networkd-network.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);
+}
+
+static void test_config_parse_duid_type(void) {
+ 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_hwaddr_one(const char *rvalue, int ret, const struct ether_addr* expected) {
+ struct ether_addr *actual = NULL;
+ int r;
+
+ r = config_parse_hwaddr("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_hwaddrs_one(const char *rvalue, const struct ether_addr* list, size_t n) {
+ _cleanup_set_free_free_ Set *s = NULL;
+ size_t m;
+
+ assert_se(config_parse_hwaddrs("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &s, NULL) == 0);
+ assert_se(set_size(s) == n);
+
+ for (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}
+
+static void test_config_parse_duid_rawdata(void) {
+ 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});
+}
+
+static void test_config_parse_hwaddr(void) {
+ 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_hwaddr_one("", 0, NULL);
+ test_config_parse_hwaddr_one("no:ta:ma:ca:dd:re", 0, NULL);
+ test_config_parse_hwaddr_one("aa:bb:cc:dd:ee:fx", 0, NULL);
+ test_config_parse_hwaddr_one("aa:bb:cc:dd:ee:ff", 0, &t[0]);
+ test_config_parse_hwaddr_one(" aa:bb:cc:dd:ee:ff", 0, &t[0]);
+ test_config_parse_hwaddr_one("aa:bb:cc:dd:ee:ff \t\n", 0, NULL);
+ test_config_parse_hwaddr_one("aa:bb:cc:dd:ee:ff \t\nxxx", 0, NULL);
+ test_config_parse_hwaddr_one("aa:bb:cc: dd:ee:ff", 0, NULL);
+ test_config_parse_hwaddr_one("aa:bb:cc:d d:ee:ff", 0, NULL);
+ test_config_parse_hwaddr_one("aa:bb:cc:dd:ee", 0, NULL);
+ test_config_parse_hwaddr_one("9:aa:bb:cc:dd:ee:ff", 0, NULL);
+ test_config_parse_hwaddr_one("aa:bb:cc:dd:ee:ff:gg", 0, NULL);
+ test_config_parse_hwaddr_one("aa:Bb:CC:dd:ee:ff", 0, &t[0]);
+ test_config_parse_hwaddr_one("01:23:45:67:89:aB", 0, &t[1]);
+ test_config_parse_hwaddr_one("1:23:45:67:89:aB", 0, &t[1]);
+ test_config_parse_hwaddr_one("aa-bb-cc-dd-ee-ff", 0, &t[0]);
+ test_config_parse_hwaddr_one("AA-BB-CC-DD-EE-FF", 0, &t[0]);
+ test_config_parse_hwaddr_one("01-23-45-67-89-ab", 0, &t[1]);
+ test_config_parse_hwaddr_one("aabb.ccdd.eeff", 0, &t[0]);
+ test_config_parse_hwaddr_one("0123.4567.89ab", 0, &t[1]);
+ test_config_parse_hwaddr_one("123.4567.89ab.", 0, NULL);
+ test_config_parse_hwaddr_one("aabbcc.ddeeff", 0, NULL);
+ test_config_parse_hwaddr_one("aabbccddeeff", 0, NULL);
+ test_config_parse_hwaddr_one("aabbccddee:ff", 0, NULL);
+ test_config_parse_hwaddr_one("012345.6789ab", 0, NULL);
+ test_config_parse_hwaddr_one("123.4567.89ab", 0, &t[1]);
+
+ test_config_parse_hwaddrs_one("", t, 0);
+ test_config_parse_hwaddrs_one("no:ta:ma:ca:dd:re", t, 0);
+ test_config_parse_hwaddrs_one("aa:bb:cc:dd:ee:fx", t, 0);
+ test_config_parse_hwaddrs_one("aa:bb:cc:dd:ee:ff", t, 1);
+ test_config_parse_hwaddrs_one(" aa:bb:cc:dd:ee:ff", t, 1);
+ test_config_parse_hwaddrs_one("aa:bb:cc:dd:ee:ff \t\n", t, 1);
+ test_config_parse_hwaddrs_one("aa:bb:cc:dd:ee:ff \t\nxxx", t, 1);
+ test_config_parse_hwaddrs_one("aa:bb:cc: dd:ee:ff", t, 0);
+ test_config_parse_hwaddrs_one("aa:bb:cc:d d:ee:ff", t, 0);
+ test_config_parse_hwaddrs_one("aa:bb:cc:dd:ee", t, 0);
+ test_config_parse_hwaddrs_one("9:aa:bb:cc:dd:ee:ff", t, 0);
+ test_config_parse_hwaddrs_one("aa:bb:cc:dd:ee:ff:gg", t, 0);
+ test_config_parse_hwaddrs_one("aa:Bb:CC:dd:ee:ff", t, 1);
+ test_config_parse_hwaddrs_one("01:23:45:67:89:aB", &t[1], 1);
+ test_config_parse_hwaddrs_one("1:23:45:67:89:aB", &t[1], 1);
+ test_config_parse_hwaddrs_one("aa-bb-cc-dd-ee-ff", t, 1);
+ test_config_parse_hwaddrs_one("AA-BB-CC-DD-EE-FF", t, 1);
+ test_config_parse_hwaddrs_one("01-23-45-67-89-ab", &t[1], 1);
+ test_config_parse_hwaddrs_one("aabb.ccdd.eeff", t, 1);
+ test_config_parse_hwaddrs_one("0123.4567.89ab", &t[1], 1);
+ test_config_parse_hwaddrs_one("123.4567.89ab.", t, 0);
+ test_config_parse_hwaddrs_one("aabbcc.ddeeff", t, 0);
+ test_config_parse_hwaddrs_one("aabbccddeeff", t, 0);
+ test_config_parse_hwaddrs_one("aabbccddee:ff", t, 0);
+ test_config_parse_hwaddrs_one("012345.6789ab", t, 0);
+ test_config_parse_hwaddrs_one("123.4567.89ab", &t[1], 1);
+
+ test_config_parse_hwaddrs_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_hwaddrs_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_(network_unrefp) Network *network = NULL;
+
+ assert_se(network = new0(Network, 1));
+ network->n_ref = 1;
+ assert_se(network->filename = strdup("hogehoge.network"));
+ assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "*", &network->match_name, 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 */
+ }
+}
+
+static void test_config_parse_address(void) {
+ 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);
+}
+
+static void test_config_parse_match_ifnames(void) {
+ _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")));
+}
+
+static void test_config_parse_match_strv(void) {
+ _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\\")));
+}
+
+int main(int argc, char **argv) {
+ log_parse_environment();
+ log_open();
+
+ test_config_parse_duid_type();
+ test_config_parse_duid_rawdata();
+ test_config_parse_hwaddr();
+ test_config_parse_address();
+ test_config_parse_match_ifnames();
+ test_config_parse_match_strv();
+
+ return 0;
+}
diff --git a/src/network/test-routing-policy-rule.c b/src/network/test-routing-policy-rule.c
new file mode 100644
index 0000000..8d87cdf
--- /dev/null
+++ b/src/network/test-routing-policy-rule.c
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "networkd-routing-policy-rule.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static void test_rule_serialization(const char *title, const char *ruleset, const char *expected) {
+ char pattern[] = "/tmp/systemd-test-routing-policy-rule.XXXXXX",
+ pattern2[] = "/tmp/systemd-test-routing-policy-rule.XXXXXX",
+ pattern3[] = "/tmp/systemd-test-routing-policy-rule.XXXXXX";
+ const char *cmd;
+ int fd, fd2, fd3;
+ _cleanup_fclose_ FILE *f = NULL, *f2 = NULL, *f3 = NULL;
+ Set *rules = NULL;
+ _cleanup_free_ char *buf = NULL;
+ size_t buf_size;
+
+ log_info("========== %s ==========", title);
+ log_info("put:\n%s\n", ruleset);
+
+ fd = mkostemp_safe(pattern);
+ assert_se(fd >= 0);
+ assert_se(f = fdopen(fd, "a+"));
+ assert_se(write_string_stream(f, ruleset, 0) == 0);
+
+ assert_se(routing_policy_load_rules(pattern, &rules) == 0);
+
+ fd2 = mkostemp_safe(pattern2);
+ assert_se(fd2 >= 0);
+ assert_se(f2 = fdopen(fd2, "a+"));
+
+ assert_se(routing_policy_serialize_rules(rules, f2) == 0);
+ assert_se(fflush_and_check(f2) == 0);
+
+ assert_se(read_full_file(pattern2, &buf, &buf_size) == 0);
+
+ log_info("got:\n%s", buf);
+
+ fd3 = mkostemp_safe(pattern3);
+ assert_se(fd3 >= 0);
+ assert_se(f3 = fdopen(fd3, "w"));
+ assert_se(write_string_stream(f3, expected ?: ruleset, 0) == 0);
+
+ cmd = strjoina("diff -u ", pattern3, " ", pattern2);
+ log_info("$ %s", cmd);
+ assert_se(system(cmd) == 0);
+
+ set_free(rules);
+}
+
+int main(int argc, char **argv) {
+ _cleanup_free_ char *p = NULL;
+
+ test_setup_logging(LOG_DEBUG);
+
+ test_rule_serialization("basic parsing",
+ "RULE=family=AF_INET from=1.2.3.4/32 to=2.3.4.5/32 tos=5 priority=10 fwmark=1/2 invert_rule=yes table=10", NULL);
+
+ test_rule_serialization("ignored values",
+ "RULE=something=to=ignore from=1.2.3.4/32 from=1.2.3.4/32"
+ " \t to=2.3.4.5/24 to=2.3.4.5/32 tos=5 fwmark=2 fwmark=1 table=10 table=20",
+ "RULE=family=AF_INET from=1.2.3.4/32 to=2.3.4.5/32 tos=5 fwmark=1 invert_rule=no table=20");
+
+ test_rule_serialization("ipv6",
+ "RULE=family=AF_INET6 from=1::2/64 to=2::3/64 invert_rule=yes table=6", NULL);
+
+ assert_se(asprintf(&p, "RULE=family=AF_INET6 from=1::2/64 to=2::3/64 invert_rule=no table=%d", RT_TABLE_MAIN) >= 0);
+ test_rule_serialization("default table",
+ "RULE=from=1::2/64 to=2::3/64", p);
+
+ test_rule_serialization("incoming interface",
+ "RULE=from=1::2/64 to=2::3/64 table=1 iif=lo",
+ "RULE=family=AF_INET6 from=1::2/64 to=2::3/64 iif=lo invert_rule=no table=1");
+
+ test_rule_serialization("outgoing interface",
+ "RULE=family=AF_INET6 from=1::2/64 to=2::3/64 oif=eth0 invert_rule=no table=1", NULL);
+
+ test_rule_serialization("freeing interface names",
+ "RULE=from=1::2/64 to=2::3/64 family=AF_INET6 iif=e0 iif=e1 oif=e0 oif=e1 table=1",
+ "RULE=family=AF_INET6 from=1::2/64 to=2::3/64 iif=e1 oif=e1 invert_rule=no table=1");
+
+ test_rule_serialization("ignoring invalid family",
+ "RULE=from=1::2/64 to=2::3/64 family=AF_UNSEPC family=AF_INET table=1",
+ "RULE=family=AF_INET6 from=1::2/64 to=2::3/64 invert_rule=no table=1");
+
+ return 0;
+}
diff --git a/src/network/wait-online/link.c b/src/network/wait-online/link.c
new file mode 100644
index 0000000..529fc9f
--- /dev/null
+++ b/src/network/wait-online/link.c
@@ -0,0 +1,153 @@
+/* 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);
+
+ r = hashmap_ensure_allocated(&m->links, NULL);
+ if (r < 0)
+ return r;
+
+ r = hashmap_ensure_allocated(&m->links_by_name, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ 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_put(m->links_by_name, l->ifname, l);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(m->links, INT_TO_PTR(ifindex), 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, 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 *operstate = NULL, *required_operstate = 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)
+ ret = log_link_debug_errno(l, r, "Failed to determine whether the link is required for online or not, "
+ "ignoring: %m");
+ else
+ l->required_for_online = r > 0;
+
+ r = sd_network_link_get_required_operstate_for_online(l->ifindex, &required_operstate);
+ if (r < 0)
+ ret = log_link_debug_errno(l, r, "Failed to get required operational state, ignoring: %m");
+ else 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 = sd_network_link_get_operational_state(l->ifindex, &operstate);
+ if (r < 0)
+ ret = log_link_debug_errno(l, r, "Failed to get operational state, ignoring: %m");
+ else {
+ LinkOperationalState s;
+
+ s = link_operstate_from_string(operstate);
+ if (s < 0)
+ ret = log_link_debug_errno(l, SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse operational state, ignoring: %m");
+ else
+ l->operational_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..3aa8357
--- /dev/null
+++ b/src/network/wait-online/link.h
@@ -0,0 +1,30 @@
+/* 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;
+ 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..79994bd
--- /dev/null
+++ b/src/network/wait-online/manager.c
@@ -0,0 +1,369 @@
+/* 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 "network-internal.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->interfaces && !hashmap_contains(m->interfaces, link->ifname))
+ return true;
+
+ if (!link->required_for_online)
+ return true;
+
+ /* ignore interfaces we explicitly are asked to ignore */
+ return strv_fnmatch(m->ignore, link->ifname);
+}
+
+static int manager_link_is_online(Manager *m, Link *l, LinkOperationalStateRange s) {
+ /* This returns the following:
+ * -EAGAIN: not processed by udev or networkd
+ * 0: operstate is not enough
+ * 1: online */
+
+ if (!l->state)
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EAGAIN),
+ "link has not yet been processed by udev");
+
+ if (STR_IN_SET(l->state, "configuring", "pending"))
+ return log_link_debug_errno(l, SYNTHETIC_ERRNO(EAGAIN),
+ "link is being processed by networkd");
+
+ 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) {
+ log_link_debug(l, "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));
+ return 0;
+ }
+
+ return 1;
+}
+
+bool manager_configured(Manager *m) {
+ bool one_ready = false;
+ const char *ifname;
+ void *p;
+ Link *l;
+ int r;
+
+ if (!hashmap_isempty(m->interfaces)) {
+ /* wait for all the links given on the command line to appear */
+ HASHMAP_FOREACH_KEY(p, ifname, m->interfaces) {
+ LinkOperationalStateRange *range = p;
+
+ l = hashmap_get(m->links_by_name, ifname);
+ if (!l && range->min == LINK_OPERSTATE_MISSING) {
+ one_ready = true;
+ continue;
+ }
+
+ if (!l) {
+ log_debug("still waiting for %s", ifname);
+ if (!m->any)
+ return false;
+ continue;
+ }
+
+ if (manager_link_is_online(m, l, *range) <= 0) {
+ if (!m->any)
+ return false;
+ continue;
+ }
+
+ one_ready = true;
+ }
+
+ /* all interfaces given by the command line are online, or
+ * one of the specified interfaces is online. */
+ return one_ready;
+ }
+
+ /* wait for all links networkd manages to be in admin state 'configured'
+ * and at least one link to gain a carrier */
+ HASHMAP_FOREACH(l, m->links) {
+ 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)
+ return false;
+ if (r > 0)
+ /* we wait for at least one link to be ready,
+ * regardless of who manages it */
+ one_ready = true;
+ }
+
+ return one_ready;
+}
+
+static int manager_process_link(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) {
+ Manager *m = userdata;
+ uint16_t type;
+ Link *l;
+ const char *ifname;
+ int ifindex, r;
+
+ assert(rtnl);
+ assert(m);
+ 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, 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 && r != -ENODATA)
+ log_link_warning_errno(l, 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;
+ sd_netlink_message *i;
+ 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_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(m->rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ for (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 = userdata;
+ Link *l;
+ int r;
+
+ assert(m);
+
+ sd_network_monitor_flush(m->network_monitor);
+
+ HASHMAP_FOREACH(l, m->links) {
+ r = link_update_monitor(l);
+ if (r < 0 && r != -ENODATA)
+ log_link_warning_errno(l, 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 *interfaces, char **ignore,
+ LinkOperationalStateRange required_operstate,
+ 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) {
+ .interfaces = interfaces,
+ .ignore = ignore,
+ .required_operstate = required_operstate,
+ .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) {
+ usec_t usec;
+
+ usec = now(clock_boottime_or_monotonic()) + timeout;
+
+ r = sd_event_add_time(m->event, NULL, clock_boottime_or_monotonic(), usec, 0, NULL, INT_TO_PTR(-ETIMEDOUT));
+ if (r < 0)
+ 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;
+}
+
+void manager_free(Manager *m) {
+ if (!m)
+ return;
+
+ hashmap_free_with_destructor(m->links, 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);
+ free(m);
+
+ return;
+}
diff --git a/src/network/wait-online/manager.h b/src/network/wait-online/manager.h
new file mode 100644
index 0000000..f5e8353
--- /dev/null
+++ b/src/network/wait-online/manager.h
@@ -0,0 +1,42 @@
+/* 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;
+ Hashmap *links_by_name;
+
+ /* Do not free the two members below. */
+ Hashmap *interfaces;
+ char **ignore;
+
+ LinkOperationalStateRange required_operstate;
+ 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;
+};
+
+void manager_free(Manager *m);
+int manager_new(Manager **ret, Hashmap *interfaces, char **ignore,
+ LinkOperationalStateRange required_operstate,
+ 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..c2bdcd4
--- /dev/null
+++ b/src/network/wait-online/wait-online.c
@@ -0,0 +1,224 @@
+/* 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 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"
+ " --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;
+ 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_allocated(&arg_interfaces, &string_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ r = hashmap_put(arg_interfaces, ifname, TAKE_PTR(range));
+ 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' },
+ { "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:", 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 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("Unhandled option");
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_(notify_on_cleanup) const char *notify_message = NULL;
+ int r;
+
+ log_setup_service();
+
+ 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_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 < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+success:
+ notify_message = "STATUS=All interfaces configured...";
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);