diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
commit | 55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch) | |
tree | 33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/network | |
parent | Initial commit. (diff) | |
download | systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip |
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/network')
226 files changed, 65130 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..f0988bd --- /dev/null +++ b/src/network/fuzz-netdev-parser.c @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "fd-util.h" +#include "fs-util.h" +#include "fuzz.h" +#include "networkd-manager.h" +#include "tmpfile-util.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(manager_freep) Manager *manager = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(unlink_tempfilep) char netdev_config[] = "/tmp/fuzz-networkd.XXXXXX"; + + if (outside_size_range(size, 0, 65536)) + return 0; + + fuzz_setup_logging(); + + assert_se(fmkostemp_safe(netdev_config, "r+", &f) == 0); + if (size != 0) + assert_se(fwrite(data, size, 1, f) == 1); + + fflush(f); + assert_se(manager_new(&manager, /* test_mode = */ true) >= 0); + (void) netdev_load_one(manager, netdev_config); + return 0; +} diff --git a/src/network/fuzz-netdev-parser.options b/src/network/fuzz-netdev-parser.options new file mode 100644 index 0000000..678d526 --- /dev/null +++ b/src/network/fuzz-netdev-parser.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 65536 diff --git a/src/network/fuzz-network-parser.c b/src/network/fuzz-network-parser.c new file mode 100644 index 0000000..eb17f09 --- /dev/null +++ b/src/network/fuzz-network-parser.c @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "fd-util.h" +#include "fs-util.h" +#include "fuzz.h" +#include "networkd-manager.h" +#include "tmpfile-util.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(manager_freep) Manager *manager = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(unlink_tempfilep) char network_config[] = "/tmp/fuzz-networkd.XXXXXX"; + + if (outside_size_range(size, 0, 65536)) + return 0; + + fuzz_setup_logging(); + + assert_se(fmkostemp_safe(network_config, "r+", &f) == 0); + if (size != 0) + assert_se(fwrite(data, size, 1, f) == 1); + + fflush(f); + assert_se(manager_new(&manager, /* test_mode = */ true) >= 0); + (void) network_load_one(manager, &manager->networks, network_config); + return 0; +} diff --git a/src/network/fuzz-network-parser.options b/src/network/fuzz-network-parser.options new file mode 100644 index 0000000..678d526 --- /dev/null +++ b/src/network/fuzz-network-parser.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 65536 diff --git a/src/network/generator/main.c b/src/network/generator/main.c new file mode 100644 index 0000000..0439a9d --- /dev/null +++ b/src/network/generator/main.c @@ -0,0 +1,218 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> + +#include "build.h" +#include "fd-util.h" +#include "fs-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_(unlink_and_freep) char *temp_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *p = NULL; + int r; + + assert(network); + + r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path); + if (r < 0) + return r; + + network_dump(network, f); + + if (asprintf(&p, "%s/%s-%s.network", + dest_dir, + isempty(network->ifname) ? "71" : "70", + isempty(network->ifname) ? "default" : network->ifname) < 0) + return log_oom(); + + r = conservative_rename(temp_path, p); + if (r < 0) + return r; + + temp_path = mfree(temp_path); + return 0; +} + +static int netdev_save(NetDev *netdev, const char *dest_dir) { + _cleanup_(unlink_and_freep) char *temp_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *p = NULL; + int r; + + assert(netdev); + + r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path); + if (r < 0) + return r; + + netdev_dump(netdev, f); + + if (asprintf(&p, "%s/70-%s.netdev", dest_dir, netdev->ifname) < 0) + return log_oom(); + + r = conservative_rename(temp_path, p); + if (r < 0) + return r; + + temp_path = mfree(temp_path); + return 0; +} + +static int link_save(Link *link, const char *dest_dir) { + _cleanup_(unlink_and_freep) char *temp_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *p = NULL; + int r; + + assert(link); + + r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path); + if (r < 0) + return r; + + link_dump(link, f); + + if (asprintf(&p, "%s/%s-%s.link", + dest_dir, + !isempty(link->ifname) ? "70" : !hw_addr_is_null(&link->mac) ? "71" : "72", + link->filename) < 0) + return log_oom(); + + r = conservative_rename(temp_path, p); + if (r < 0) + return r; + + temp_path = mfree(temp_path); + return 0; +} + +static int context_save(Context *context) { + Network *network; + NetDev *netdev; + Link *link; + int r; + + const char *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) + RET_GATHER(r, network_save(network, p)); + + HASHMAP_FOREACH(netdev, context->netdevs_by_name) + RET_GATHER(r, netdev_save(netdev, p)); + + HASHMAP_FOREACH(link, context->links_by_filename) + RET_GATHER(r, link_save(link, p)); + + return r; +} + +static int help(void) { + printf("%s [OPTIONS...] [-- KERNEL_CMDLINE]\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --root=PATH Operate on an alternate filesystem root\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_ROOT, + }; + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "root", required_argument, NULL, ARG_ROOT }, + {}, + }; + int c; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_ROOT: + arg_root = optarg; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + return 1; +} + +static int run(int argc, char *argv[]) { + _cleanup_(context_clear) Context context = {}; + int r; + + log_setup(); + + umask(0022); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (optind >= argc) { + r = proc_cmdline_parse(parse_cmdline_item, &context, 0); + if (r < 0) + return log_warning_errno(r, "Failed to parse kernel command line: %m"); + } else { + for (int i = optind; i < argc; i++) { + _cleanup_free_ char *word = NULL; + char *value; + + word = strdup(argv[i]); + if (!word) + return log_oom(); + + value = strchr(word, '='); + if (value) + *(value++) = 0; + + r = parse_cmdline_item(word, value, &context); + if (r < 0) + return log_warning_errno(r, "Failed to parse command line \"%s%s%s\": %m", + word, value ? "=" : "", strempty(value)); + } + } + + r = context_merge_networks(&context); + if (r < 0) + return log_warning_errno(r, "Failed to merge multiple command line options: %m"); + + return context_save(&context); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/network/generator/network-generator.c b/src/network/generator/network-generator.c new file mode 100644 index 0000000..48527a2 --- /dev/null +++ b/src/network/generator/network-generator.c @@ -0,0 +1,1432 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "fd-util.h" +#include "fileio.h" +#include "hostname-util.h" +#include "log.h" +#include "macro.h" +#include "memstream-util.h" +#include "netif-naming-scheme.h" +#include "network-generator.h" +#include "parse-util.h" +#include "proc-cmdline.h" +#include "socket-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" + +/* + # .network + ip={dhcp|on|any|dhcp6|auto6|either6|link6|link-local} + ip=<interface>:{dhcp|on|any|dhcp6|auto6|link6|link-local}[:[<mtu>][:<macaddr>]] + ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|link6|ibft|link-local}[:[<mtu>][:<macaddr>]] + ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|link6|ibft|link-local}[:[<dns1>][:<dns2>]] + rd.route=<net>/<netmask>:<gateway>[:<interface>] + nameserver=<IP> [nameserver=<IP> ...] + rd.peerdns=0 + + # .link + ifname=<interface>:<MAC> + net.ifname-policy=policy1[,policy2,...][,<MAC>] # This is an original rule, not supported by other tools. + + # .netdev + vlan=<vlanname>:<phydevice> + bond=<bondname>[:<bondslaves>:[:<options>[:<mtu>]]] + team=<teammaster>:<teamslaves> # not supported + bridge=<bridgename>:<ethnames> + + # ignored + bootdev=<interface> + BOOTIF=<MAC> + rd.bootif=0 + biosdevname=0 + rd.neednet=1 +*/ + +static const char * const dracut_dhcp_type_table[_DHCP_TYPE_MAX] = { + [DHCP_TYPE_NONE] = "none", + [DHCP_TYPE_OFF] = "off", + [DHCP_TYPE_ON] = "on", + [DHCP_TYPE_ANY] = "any", + [DHCP_TYPE_DHCP] = "dhcp", + [DHCP_TYPE_DHCP6] = "dhcp6", + [DHCP_TYPE_AUTO6] = "auto6", + [DHCP_TYPE_EITHER6] = "either6", + [DHCP_TYPE_IBFT] = "ibft", + [DHCP_TYPE_LINK6] = "link6", + [DHCP_TYPE_LINK_LOCAL] = "link-local", +}; + +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", + [DHCP_TYPE_LINK6] = "no", + [DHCP_TYPE_LINK_LOCAL] = "no", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(networkd_dhcp_type, DHCPType); + +static const char * const networkd_ipv6ra_type_table[_DHCP_TYPE_MAX] = { + [DHCP_TYPE_NONE] = "no", + [DHCP_TYPE_OFF] = "no", + [DHCP_TYPE_LINK6] = "no", + [DHCP_TYPE_LINK_LOCAL] = "no", + /* We omit the other entries, to leave the default in effect */ +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(networkd_ipv6ra_type, DHCPType); + +static const char * const networkd_link_local_type_table[_DHCP_TYPE_MAX] = { + [DHCP_TYPE_NONE] = "no", + [DHCP_TYPE_OFF] = "no", + [DHCP_TYPE_LINK6] = "ipv6", + [DHCP_TYPE_LINK_LOCAL] = "yes", + /* We omit the other entries, to leave the default in effect */ +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(networkd_link_local_type, DHCPType); + +static Address *address_free(Address *address) { + if (!address) + return NULL; + + if (address->network) + LIST_REMOVE(addresses, address->network->addresses, address); + + return mfree(address); +} + +static int address_new(Network *network, int family, unsigned char prefixlen, + union in_addr_union *addr, union in_addr_union *peer, Address **ret) { + Address *address; + + assert(network); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(addr); + + address = new(Address, 1); + if (!address) + return -ENOMEM; + + *address = (Address) { + .family = family, + .prefixlen = prefixlen, + .address = *addr, + .peer = peer ? *peer : IN_ADDR_NULL, + }; + + LIST_PREPEND(addresses, network->addresses, address); + + address->network = network; + + if (ret) + *ret = address; + return 0; +} + +static Route *route_free(Route *route) { + if (!route) + return NULL; + + if (route->network) + LIST_REMOVE(routes, route->network->routes, route); + + return mfree(route); +} + +static int route_new(Network *network, int family, unsigned char prefixlen, + union in_addr_union *dest, union in_addr_union *gateway, Route **ret) { + Route *route; + + assert(network); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(dest || gateway); + + route = new(Route, 1); + if (!route) + return -ENOMEM; + + *route = (Route) { + .family = family, + .prefixlen = prefixlen, + .dest = dest ? *dest : IN_ADDR_NULL, + .gateway = gateway ? *gateway : IN_ADDR_NULL, + }; + + LIST_PREPEND(routes, network->routes, route); + + route->network = network; + + if (ret) + *ret = route; + return 0; +} + +static Network *network_free(Network *network) { + Address *address; + Route *route; + + if (!network) + return NULL; + + free(network->ifname); + free(network->hostname); + strv_free(network->dns); + free(network->vlan); + free(network->bridge); + free(network->bond); + + while ((address = network->addresses)) + address_free(address); + + while ((route = network->routes)) + route_free(route); + + return mfree(network); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Network*, network_free); + +static int network_new(Context *context, const char *name, Network **ret) { + _cleanup_(network_freep) Network *network = NULL; + _cleanup_free_ char *ifname = NULL; + int r; + + assert(context); + + if (!isempty(name) && !ifname_valid(name)) + return -EINVAL; + + ifname = strdup(name); + if (!ifname) + return -ENOMEM; + + network = new(Network, 1); + if (!network) + return -ENOMEM; + + *network = (Network) { + .ifname = TAKE_PTR(ifname), + .dhcp_type = _DHCP_TYPE_INVALID, + .dhcp_use_dns = -1, + }; + + r = hashmap_ensure_put(&context->networks_by_name, &string_hash_ops, network->ifname, network); + if (r < 0) + return r; + + if (ret) + *ret = network; + + TAKE_PTR(network); + return 0; +} + +Network *network_get(Context *context, const char *ifname) { + return hashmap_get(context->networks_by_name, ifname); +} + +static NetDev *netdev_free(NetDev *netdev) { + if (!netdev) + return NULL; + + free(netdev->ifname); + free(netdev->kind); + return mfree(netdev); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(NetDev*, netdev_free); + +static int netdev_new(Context *context, const char *_kind, const char *_ifname, NetDev **ret) { + _cleanup_(netdev_freep) NetDev *netdev = NULL; + _cleanup_free_ char *kind = NULL, *ifname = NULL; + int r; + + assert(context); + assert(_kind); + + if (!ifname_valid(_ifname)) + return -EINVAL; + + kind = strdup(_kind); + if (!kind) + return -ENOMEM; + + ifname = strdup(_ifname); + if (!ifname) + return -ENOMEM; + + netdev = new(NetDev, 1); + if (!netdev) + return -ENOMEM; + + *netdev = (NetDev) { + .kind = TAKE_PTR(kind), + .ifname = TAKE_PTR(ifname), + }; + + r = hashmap_ensure_put(&context->netdevs_by_name, &string_hash_ops, netdev->ifname, netdev); + if (r < 0) + return r; + + if (ret) + *ret = netdev; + + TAKE_PTR(netdev); + return 0; +} + +NetDev *netdev_get(Context *context, const char *ifname) { + return hashmap_get(context->netdevs_by_name, ifname); +} + +static Link *link_free(Link *link) { + if (!link) + return NULL; + + free(link->filename); + free(link->ifname); + strv_free(link->policies); + strv_free(link->alt_policies); + return mfree(link); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free); + +static int link_new( + Context *context, + const char *name, + const struct hw_addr_data *mac, + Link **ret) { + + _cleanup_(link_freep) Link *link = NULL; + _cleanup_free_ char *ifname = NULL, *filename = NULL; + int r; + + assert(context); + assert(mac); + + if (name) { + if (!ifname_valid(name)) + return -EINVAL; + + ifname = strdup(name); + if (!ifname) + return -ENOMEM; + + filename = strdup(name); + if (!filename) + return -ENOMEM; + } + + if (!filename) { + filename = strdup(hw_addr_is_null(mac) ? "default" : + HW_ADDR_TO_STR_FULL(mac, HW_ADDR_TO_STRING_NO_COLON)); + if (!filename) + return -ENOMEM; + } + + link = new(Link, 1); + if (!link) + return -ENOMEM; + + *link = (Link) { + .filename = TAKE_PTR(filename), + .ifname = TAKE_PTR(ifname), + .mac = *mac, + }; + + r = hashmap_ensure_put(&context->links_by_filename, &string_hash_ops, link->filename, link); + if (r < 0) + return r; + + if (ret) + *ret = link; + + TAKE_PTR(link); + return 0; +} + +Link *link_get(Context *context, const char *filename) { + assert(context); + assert(filename); + return hashmap_get(context->links_by_filename, filename); +} + +static int network_set_dhcp_type(Context *context, const char *ifname, const char *dhcp_type) { + Network *network; + DHCPType t; + int r; + + assert(context); + assert(ifname); + assert(dhcp_type); + + t = dracut_dhcp_type_from_string(dhcp_type); + if (t < 0) + return t; + + network = network_get(context, ifname); + if (!network) { + r = network_new(context, ifname, &network); + if (r < 0) + return r; + } + + network->dhcp_type = t; + return 0; +} + +static int network_set_hostname(Context *context, const char *ifname, const char *hostname) { + Network *network; + + assert(context); + assert(ifname); + + network = network_get(context, ifname); + if (!network) + return -ENODEV; + + return free_and_strdup(&network->hostname, hostname); +} + +static int network_set_mtu(Context *context, const char *ifname, const char *mtu) { + Network *network; + + assert(context); + assert(ifname); + + if (isempty(mtu)) + return 0; + + network = network_get(context, ifname); + if (!network) + return -ENODEV; + + return parse_mtu(AF_UNSPEC, mtu, &network->mtu); +} + +static int network_set_mac_address(Context *context, const char *ifname, const char *mac) { + Network *network; + + assert(context); + assert(ifname); + assert(mac); + + network = network_get(context, ifname); + if (!network) + return -ENODEV; + + return parse_ether_addr(mac, &network->mac); +} + +static int network_set_address(Context *context, const char *ifname, int family, unsigned char prefixlen, + union in_addr_union *addr, union in_addr_union *peer) { + Network *network; + + assert(context); + assert(ifname); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(addr); + + if (!in_addr_is_set(family, addr)) + return 0; + + network = network_get(context, ifname); + if (!network) + return -ENODEV; + + return address_new(network, family, prefixlen, addr, peer, NULL); +} + +static int network_set_route(Context *context, const char *ifname, int family, unsigned char prefixlen, + union in_addr_union *dest, union in_addr_union *gateway) { + Network *network; + int r; + + assert(context); + assert(ifname); + assert(IN_SET(family, AF_INET, AF_INET6)); + + if (!(dest && in_addr_is_set(family, dest)) && + !(gateway && in_addr_is_set(family, gateway))) + return 0; + + network = network_get(context, ifname); + if (!network) { + r = network_new(context, ifname, &network); + if (r < 0) + return r; + } + + return route_new(network, family, prefixlen, dest, gateway, NULL); +} + +static int network_set_dns(Context *context, const char *ifname, int family, const char *dns) { + union in_addr_union a; + Network *network; + int r; + + assert(context); + assert(ifname); + assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6)); + assert(dns); + + if (family == AF_UNSPEC) + r = in_addr_from_string_auto(dns, &family, &a); + else + r = in_addr_from_string(family, dns, &a); + if (r < 0) + return r; + + network = network_get(context, ifname); + if (!network) { + r = network_new(context, ifname, &network); + if (r < 0) + return r; + } + + return strv_extend(&network->dns, dns); +} + +static int network_set_dhcp_use_dns(Context *context, const char *ifname, bool value) { + Network *network; + int r; + + assert(context); + assert(ifname); + + network = network_get(context, ifname); + if (!network) { + r = network_new(context, ifname, &network); + if (r < 0) + return r; + } + + network->dhcp_use_dns = value; + + return 0; +} + +static int network_set_vlan(Context *context, const char *ifname, const char *value) { + Network *network; + int r; + + assert(context); + assert(ifname); + + network = network_get(context, ifname); + if (!network) { + r = network_new(context, ifname, &network); + if (r < 0) + return r; + } + + return free_and_strdup(&network->vlan, value); +} + +static int network_set_bridge(Context *context, const char *ifname, const char *value) { + Network *network; + int r; + + assert(context); + assert(ifname); + + network = network_get(context, ifname); + if (!network) { + r = network_new(context, ifname, &network); + if (r < 0) + return r; + } + + return free_and_strdup(&network->bridge, value); +} + +static int network_set_bond(Context *context, const char *ifname, const char *value) { + Network *network; + int r; + + assert(context); + assert(ifname); + + network = network_get(context, ifname); + if (!network) { + r = network_new(context, ifname, &network); + if (r < 0) + return r; + } + + return free_and_strdup(&network->bond, value); +} + +static int parse_cmdline_ip_mtu_mac(Context *context, const char *ifname, const char *value) { + const char *mtu, *p; + int r; + + assert(context); + assert(ifname); + assert(value); + + /* [<mtu>][:<macaddr>] */ + + p = strchr(value, ':'); + if (!p) + mtu = value; + else + mtu = strndupa_safe(value, p - value); + + r = network_set_mtu(context, ifname, mtu); + if (r < 0) + return r; + + if (!p || isempty(p + 1)) + return 0; + + r = network_set_mac_address(context, ifname, p + 1); + if (r < 0) + return r; + + return 0; +} + +static int parse_ip_address_one(int family, const char **value, union in_addr_union *ret) { + const char *p, *q, *buf; + int r; + + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(value); + assert(ret); + + p = ASSERT_PTR(*value); + + if (p[0] == ':') { + *value = p + 1; + return 0; + } + + if (family == AF_INET6) { + if (p[0] != '[') + return -EINVAL; + + q = strchr(p + 1, ']'); + if (!q) + return -EINVAL; + + if (q[1] != ':') + return -EINVAL; + + buf = strndupa_safe(p + 1, q - p - 1); + p = q + 2; + } else { + q = strchr(p, ':'); + if (!q) + return -EINVAL; + + buf = strndupa_safe(p, q - p); + p = q + 1; + } + + r = in_addr_from_string(family, buf, ret); + if (r < 0) + return r; + + *value = p; + return 1; +} + +static int parse_netmask_or_prefixlen(int family, const char **value, unsigned char *ret) { + union in_addr_union netmask; + const char *p, *q; + int r; + + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(value); + assert(*value); + assert(ret); + + r = parse_ip_address_one(family, value, &netmask); + if (r > 0) { + if (family == AF_INET6) + /* TODO: Not supported yet. */ + return -EINVAL; + + *ret = in4_addr_netmask_to_prefixlen(&netmask.in); + } else if (r == 0) + *ret = family == AF_INET6 ? 128 : 32; + else { + p = strchr(*value, ':'); + if (!p) + return -EINVAL; + + q = strndupa_safe(*value, p - *value); + r = safe_atou8(q, ret); + if (r < 0) + return r; + + *value = p + 1; + } + + return 0; +} + +static int parse_ip_dns_address_one(Context *context, const char *ifname, const char **value) { + const char *p, *q, *buf; + int r, family; + + assert(context); + assert(ifname); + assert(value); + + p = ASSERT_PTR(*value); + + if (isempty(p)) + return 0; + + if (p[0] == '[') { + q = strchr(p + 1, ']'); + if (!q) + return -EINVAL; + if (!IN_SET(q[1], ':', '\0')) + return -EINVAL; + + buf = strndupa_safe(p + 1, q - p - 1); + p = q + 1; + family = AF_INET6; + } else { + q = strchr(p, ':'); + if (!q) + buf = *value; + else + buf = strndupa_safe(*value, q - *value); + + p += strlen(buf); + family = AF_INET; + } + + r = network_set_dns(context, ifname, family, buf); + if (r < 0) + return r; + + *value = p; + return 0; +} + +static int parse_cmdline_ip_address(Context *context, int family, const char *value) { + union in_addr_union addr = {}, peer = {}, gateway = {}; + const char *hostname = NULL, *ifname, *dhcp_type, *p; + unsigned char prefixlen; + int r; + + assert(context); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(value); + + /* ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft|link6}[:[<mtu>][:<macaddr>]] + * ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft|link6}[:[<dns1>][:<dns2>]] */ + + r = parse_ip_address_one(family, &value, &addr); + if (r < 0) + return r; + r = parse_ip_address_one(family, &value, &peer); + if (r < 0) + return r; + r = parse_ip_address_one(family, &value, &gateway); + if (r < 0) + return r; + r = parse_netmask_or_prefixlen(family, &value, &prefixlen); + if (r < 0) + return r; + + /* hostname */ + p = strchr(value, ':'); + if (!p) + return -EINVAL; + + if (p != value) { + hostname = strndupa_safe(value, p - value); + if (!hostname_is_valid(hostname, 0)) + return -EINVAL; + } + + value = p + 1; + + /* ifname */ + p = strchr(value, ':'); + if (!p) + return -EINVAL; + + ifname = strndupa_safe(value, p - value); + + value = p + 1; + + /* dhcp_type */ + p = strchr(value, ':'); + if (!p) + dhcp_type = value; + else + dhcp_type = strndupa_safe(value, p - value); + + r = network_set_dhcp_type(context, ifname, dhcp_type); + if (r < 0) + return r; + + /* set values */ + r = network_set_hostname(context, ifname, hostname); + if (r < 0) + return r; + + r = network_set_address(context, ifname, family, prefixlen, &addr, &peer); + if (r < 0) + return r; + + r = network_set_route(context, ifname, family, 0, NULL, &gateway); + if (r < 0) + return r; + + if (!p) + return 0; + + /* First, try [<mtu>][:<macaddr>] */ + r = parse_cmdline_ip_mtu_mac(context, ifname, p + 1); + if (r >= 0) + return 0; + + /* Next, try [<dns1>][:<dns2>] */ + value = p + 1; + r = parse_ip_dns_address_one(context, ifname, &value); + if (r < 0) + return r; + + value += *value == ':'; + r = parse_ip_dns_address_one(context, ifname, &value); + if (r < 0) + return r; + + /* refuse unexpected trailing strings */ + if (!isempty(value)) + return -EINVAL; + + return 0; +} + +static int parse_cmdline_ip_interface(Context *context, const char *value) { + const char *ifname, *dhcp_type, *p; + int r; + + assert(context); + assert(value); + + /* ip=<interface>:{dhcp|on|any|dhcp6|auto6|link6}[:[<mtu>][:<macaddr>]] */ + + p = strchr(value, ':'); + if (!p) + return -EINVAL; + + ifname = strndupa_safe(value, p - value); + + value = p + 1; + p = strchr(value, ':'); + if (!p) + dhcp_type = value; + else + dhcp_type = strndupa_safe(value, p - value); + + r = network_set_dhcp_type(context, ifname, dhcp_type); + if (r < 0) + return r; + + if (!p) + return 0; + + return parse_cmdline_ip_mtu_mac(context, ifname, p + 1); +} + +static int parse_cmdline_ip(Context *context, const char *key, const char *value) { + const char *p; + int r; + + assert(context); + assert(key); + + if (proc_cmdline_value_missing(key, value)) + return -EINVAL; + + p = strchr(value, ':'); + if (!p) + /* ip={dhcp|on|any|dhcp6|auto6|either6|link6} */ + return network_set_dhcp_type(context, "", value); + + if (value[0] == '[') + return parse_cmdline_ip_address(context, AF_INET6, value); + + r = parse_cmdline_ip_address(context, AF_INET, value); + if (r < 0) + return parse_cmdline_ip_interface(context, value); + + return 0; +} + +static int parse_cmdline_rd_route(Context *context, const char *key, const char *value) { + union in_addr_union addr = {}, gateway = {}; + unsigned char prefixlen; + const char *buf, *p; + int family, r; + + assert(context); + assert(key); + + /* rd.route=<net>/<netmask>:<gateway>[:<interface>] */ + + if (proc_cmdline_value_missing(key, value)) + return -EINVAL; + + if (value[0] == '[') { + p = strchr(value, ']'); + if (!p) + return -EINVAL; + + if (p[1] != ':') + return -EINVAL; + + buf = strndupa_safe(value + 1, p - value - 1); + value = p + 2; + family = AF_INET6; + } else { + p = strchr(value, ':'); + if (!p) + return -EINVAL; + + buf = strndupa_safe(value, p - value); + value = p + 1; + family = AF_INET; + } + + r = in_addr_prefix_from_string(buf, family, &addr, &prefixlen); + if (r < 0) + return r; + + p = strchr(value, ':'); + if (!p) + value = strjoina(value, ":"); + + r = parse_ip_address_one(family, &value, &gateway); + if (r < 0) + return r; + + return network_set_route(context, value, family, prefixlen, &addr, &gateway); +} + +static int parse_cmdline_nameserver(Context *context, const char *key, const char *value) { + assert(context); + assert(key); + + if (proc_cmdline_value_missing(key, value)) + return -EINVAL; + + return network_set_dns(context, "", AF_UNSPEC, value); +} + +static int parse_cmdline_rd_peerdns(Context *context, const char *key, const char *value) { + int r; + + assert(context); + assert(key); + + if (proc_cmdline_value_missing(key, value)) + return network_set_dhcp_use_dns(context, "", true); + + r = parse_boolean(value); + if (r < 0) + return r; + + return network_set_dhcp_use_dns(context, "", r); +} + +static int parse_cmdline_vlan(Context *context, const char *key, const char *value) { + const char *name, *p; + NetDev *netdev; + int r; + + assert(context); + assert(key); + + if (proc_cmdline_value_missing(key, value)) + return -EINVAL; + + p = strchr(value, ':'); + if (!p) + return -EINVAL; + + name = strndupa_safe(value, p - value); + + netdev = netdev_get(context, name); + if (!netdev) { + r = netdev_new(context, "vlan", name, &netdev); + if (r < 0) + return r; + } + + return network_set_vlan(context, p + 1, name); +} + +static int parse_cmdline_bridge(Context *context, const char *key, const char *value) { + const char *name, *p; + NetDev *netdev; + int r; + + assert(context); + assert(key); + + if (proc_cmdline_value_missing(key, value)) + return -EINVAL; + + p = strchr(value, ':'); + if (!p) + return -EINVAL; + + name = strndupa_safe(value, p - value); + + netdev = netdev_get(context, name); + if (!netdev) { + r = netdev_new(context, "bridge", name, &netdev); + if (r < 0) + return r; + } + + p++; + if (isempty(p)) + return -EINVAL; + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", 0); + if (r <= 0) + return r; + + r = network_set_bridge(context, word, name); + if (r < 0) + return r; + } +} + +static int parse_cmdline_bond(Context *context, const char *key, const char *value) { + const char *name, *slaves, *p; + NetDev *netdev; + int r; + + assert(context); + assert(key); + + if (proc_cmdline_value_missing(key, value)) + return -EINVAL; + + p = strchr(value, ':'); + if (!p) + return -EINVAL; + + name = strndupa_safe(value, p - value); + + netdev = netdev_get(context, name); + if (!netdev) { + r = netdev_new(context, "bond", name, &netdev); + if (r < 0) + return r; + } + + value = p + 1; + p = strchr(value, ':'); + if (!p) + slaves = value; + else + slaves = strndupa_safe(value, p - value); + + if (isempty(slaves)) + return -EINVAL; + + for (const char *q = slaves; ; ) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&q, &word, ",", 0); + if (r == 0) + break; + if (r < 0) + return r; + + r = network_set_bond(context, word, name); + if (r < 0) + return r; + } + + if (!p) + return 0; + + value = p + 1; + p = strchr(value, ':'); + if (!p) + /* TODO: set bonding options */ + return 0; + + return parse_mtu(AF_UNSPEC, p + 1, &netdev->mtu); +} + +static int parse_cmdline_ifname(Context *context, const char *key, const char *value) { + struct hw_addr_data mac; + const char *name, *p; + int r; + + assert(context); + assert(key); + + /* ifname=<interface>:<MAC> */ + + if (proc_cmdline_value_missing(key, value)) + return -EINVAL; + + p = strchr(value, ':'); + if (!p) + return -EINVAL; + + name = strndupa_safe(value, p - value); + + r = parse_hw_addr(p + 1, &mac); + if (r < 0) + return r; + + return link_new(context, name, &mac, NULL); +} + +static int parse_cmdline_ifname_policy(Context *context, const char *key, const char *value) { + _cleanup_strv_free_ char **policies = NULL, **alt_policies = NULL; + struct hw_addr_data mac = HW_ADDR_NULL; + Link *link; + int r; + + assert(context); + assert(key); + + /* net.ifname-policy=policy1[,policy2,...][,<MAC>] */ + + if (proc_cmdline_value_missing(key, value)) + return -EINVAL; + + for (const char *q = value; ; ) { + _cleanup_free_ char *word = NULL; + NamePolicy p; + + r = extract_first_word(&q, &word, ",", 0); + if (r == 0) + break; + if (r < 0) + return r; + + p = name_policy_from_string(word); + if (p < 0) { + r = parse_hw_addr(word, &mac); + if (r < 0) + return r; + + if (hw_addr_is_null(&mac)) + return -EINVAL; + + if (!isempty(q)) + return -EINVAL; + + break; + } + + if (alternative_names_policy_from_string(word) >= 0) { + r = strv_extend(&alt_policies, word); + if (r < 0) + return r; + } + + r = strv_consume(&policies, TAKE_PTR(word)); + if (r < 0) + return r; + } + + if (strv_isempty(policies)) + return -EINVAL; + + r = link_new(context, NULL, &mac, &link); + if (r < 0) + return r; + + link->policies = TAKE_PTR(policies); + link->alt_policies = TAKE_PTR(alt_policies); + return 0; +} + +int parse_cmdline_item(const char *key, const char *value, void *data) { + Context *context = ASSERT_PTR(data); + + assert(key); + + if (streq(key, "ip")) + return parse_cmdline_ip(context, key, value); + if (streq(key, "rd.route")) + return parse_cmdline_rd_route(context, key, value); + if (streq(key, "nameserver")) + return parse_cmdline_nameserver(context, key, value); + if (streq(key, "rd.peerdns")) + return parse_cmdline_rd_peerdns(context, key, value); + if (streq(key, "vlan")) + return parse_cmdline_vlan(context, key, value); + if (streq(key, "bridge")) + return parse_cmdline_bridge(context, key, value); + if (streq(key, "bond")) + return parse_cmdline_bond(context, key, value); + if (streq(key, "ifname")) + return parse_cmdline_ifname(context, key, value); + if (streq(key, "net.ifname-policy")) + return parse_cmdline_ifname_policy(context, key, value); + + return 0; +} + +int context_merge_networks(Context *context) { + Network *all, *network; + int r; + + assert(context); + + /* Copy settings about the following options + rd.route=<net>/<netmask>:<gateway>[:<interface>] + nameserver=<IP> [nameserver=<IP> ...] + rd.peerdns=0 */ + + all = network_get(context, ""); + if (!all) + return 0; + + if (hashmap_size(context->networks_by_name) <= 1) + return 0; + + HASHMAP_FOREACH(network, context->networks_by_name) { + if (network == all) + continue; + + network->dhcp_use_dns = all->dhcp_use_dns; + + r = strv_extend_strv(&network->dns, all->dns, false); + if (r < 0) + return r; + + LIST_FOREACH(routes, route, all->routes) { + r = route_new(network, route->family, route->prefixlen, &route->dest, &route->gateway, NULL); + if (r < 0) + return r; + } + } + + assert_se(hashmap_remove(context->networks_by_name, "") == all); + network_free(all); + return 0; +} + +void context_clear(Context *context) { + if (!context) + return; + + hashmap_free_with_destructor(context->networks_by_name, network_free); + hashmap_free_with_destructor(context->netdevs_by_name, netdev_free); + hashmap_free_with_destructor(context->links_by_filename, link_free); +} + +static int address_dump(Address *address, FILE *f) { + assert(address); + assert(f); + + fprintf(f, + "\n[Address]\n" + "Address=%s\n", + IN_ADDR_PREFIX_TO_STRING(address->family, &address->address, address->prefixlen)); + if (in_addr_is_set(address->family, &address->peer)) + fprintf(f, "Peer=%s\n", + IN_ADDR_TO_STRING(address->family, &address->peer)); + return 0; +} + +static int route_dump(Route *route, FILE *f) { + assert(route); + assert(f); + + fputs("\n[Route]\n", f); + if (in_addr_is_set(route->family, &route->dest)) + fprintf(f, "Destination=%s\n", + IN_ADDR_PREFIX_TO_STRING(route->family, &route->dest, route->prefixlen)); + if (in_addr_is_set(route->family, &route->gateway)) + fprintf(f, "Gateway=%s\n", + IN_ADDR_TO_STRING(route->family, &route->gateway)); + + return 0; +} + +void network_dump(Network *network, FILE *f) { + const char *dhcp; + + assert(network); + assert(f); + + fputs("[Match]\n", f); + + if (isempty(network->ifname)) + /* If the interface name is not specified, then let's make the .network file match the all + * physical interfaces. */ + fputs("Kind=!*\n" + "Type=!loopback\n", f); + else + fprintf(f, "Name=%s\n", network->ifname); + + fputs("\n[Link]\n", f); + + if (!ether_addr_is_null(&network->mac)) + fprintf(f, "MACAddress=%s\n", ETHER_ADDR_TO_STR(&network->mac)); + if (network->mtu > 0) + fprintf(f, "MTUBytes=%" PRIu32 "\n", network->mtu); + + fputs("\n[Network]\n", f); + + dhcp = networkd_dhcp_type_to_string(network->dhcp_type); + if (dhcp) + fprintf(f, "DHCP=%s\n", dhcp); + + const char *ll; + ll = networkd_link_local_type_to_string(network->dhcp_type); + if (ll) + fprintf(f, "LinkLocalAddressing=%s\n", ll); + + const char *ra; + ra = networkd_ipv6ra_type_to_string(network->dhcp_type); + if (ra) + fprintf(f, "IPv6AcceptRA=%s\n", ra); + + if (!strv_isempty(network->dns)) + STRV_FOREACH(dns, network->dns) + fprintf(f, "DNS=%s\n", *dns); + + if (network->vlan) + fprintf(f, "VLAN=%s\n", network->vlan); + + if (network->bridge) + fprintf(f, "Bridge=%s\n", network->bridge); + + if (network->bond) + fprintf(f, "Bond=%s\n", network->bond); + + fputs("\n[DHCP]\n", f); + + if (!isempty(network->hostname)) + fprintf(f, "Hostname=%s\n", network->hostname); + + if (network->dhcp_use_dns >= 0) + fprintf(f, "UseDNS=%s\n", yes_no(network->dhcp_use_dns)); + + LIST_FOREACH(addresses, address, network->addresses) + (void) address_dump(address, f); + + LIST_FOREACH(routes, route, network->routes) + (void) route_dump(route, f); +} + +void netdev_dump(NetDev *netdev, FILE *f) { + assert(netdev); + assert(f); + + fprintf(f, + "[NetDev]\n" + "Kind=%s\n" + "Name=%s\n", + netdev->kind, + netdev->ifname); + + if (netdev->mtu > 0) + fprintf(f, "MTUBytes=%" PRIu32 "\n", netdev->mtu); +} + +void link_dump(Link *link, FILE *f) { + assert(link); + assert(f); + + fputs("[Match]\n", f); + + if (!hw_addr_is_null(&link->mac)) + fprintf(f, "MACAddress=%s\n", HW_ADDR_TO_STR(&link->mac)); + else + fputs("OriginalName=*\n", f); + + fputs("\n[Link]\n", f); + + if (!isempty(link->ifname)) + fprintf(f, "Name=%s\n", link->ifname); + + if (!strv_isempty(link->policies)) { + fputs("NamePolicy=", f); + fputstrv(f, link->policies, " ", NULL); + fputc('\n', f); + } + + if (!strv_isempty(link->alt_policies)) { + fputs("AlternativeNamesPolicy=", f); + fputstrv(f, link->alt_policies, " ", NULL); + fputc('\n', f); + } +} + +int network_format(Network *network, char **ret) { + _cleanup_(memstream_done) MemStream m = {}; + FILE *f; + + assert(network); + assert(ret); + + f = memstream_init(&m); + if (!f) + return -ENOMEM; + + network_dump(network, f); + + return memstream_finalize(&m, ret, NULL); +} + +int netdev_format(NetDev *netdev, char **ret) { + _cleanup_(memstream_done) MemStream m = {}; + FILE *f; + + assert(netdev); + assert(ret); + + f = memstream_init(&m); + if (!f) + return -ENOMEM; + + netdev_dump(netdev, f); + + return memstream_finalize(&m, ret, NULL); +} + +int link_format(Link *link, char **ret) { + _cleanup_(memstream_done) MemStream m = {}; + FILE *f; + + assert(link); + assert(ret); + + f = memstream_init(&m); + if (!f) + return -ENOMEM; + + link_dump(link, f); + + return memstream_finalize(&m, ret, NULL); +} diff --git a/src/network/generator/network-generator.h b/src/network/generator/network-generator.h new file mode 100644 index 0000000..aa5ca9d --- /dev/null +++ b/src/network/generator/network-generator.h @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdio.h> + +#include "ether-addr-util.h" +#include "hashmap.h" +#include "in-addr-util.h" +#include "list.h" + +typedef enum DHCPType { + DHCP_TYPE_NONE, + DHCP_TYPE_OFF, /* Same as DHCP_TYPE_NONE */ + DHCP_TYPE_ON, + DHCP_TYPE_ANY, /* Same as DHCP_TYPE_ON */ + DHCP_TYPE_DHCP, /* Actually means: DHCPv4 */ + DHCP_TYPE_DHCP6, + DHCP_TYPE_AUTO6, + DHCP_TYPE_EITHER6, + DHCP_TYPE_IBFT, + DHCP_TYPE_LINK6, + DHCP_TYPE_LINK_LOCAL, + _DHCP_TYPE_MAX, + _DHCP_TYPE_INVALID = -EINVAL, +} DHCPType; + +typedef struct Address Address; +typedef struct Link Link; +typedef struct NetDev NetDev; +typedef struct Network Network; +typedef struct Route Route; +typedef struct Context Context; + +struct Address { + Network *network; + + union in_addr_union address, peer; + unsigned char prefixlen; + int family; + + LIST_FIELDS(Address, addresses); +}; + +struct Route { + Network *network; + + union in_addr_union dest, gateway; + unsigned char prefixlen; + int family; + + LIST_FIELDS(Route, routes); +}; + +struct Network { + /* [Match] */ + char *ifname; + + /* [Link] */ + struct ether_addr mac; + uint32_t mtu; + + /* [Network] */ + DHCPType dhcp_type; + char **dns; + char *vlan; + char *bridge; + char *bond; + + /* [DHCP] */ + char *hostname; + int dhcp_use_dns; + + LIST_HEAD(Address, addresses); + LIST_HEAD(Route, routes); +}; + +struct NetDev { + /* [NetDev] */ + char *ifname; + char *kind; + uint32_t mtu; +}; + +struct Link { + char *filename; + + /* [Match] */ + struct hw_addr_data mac; + + /* [Link] */ + char *ifname; + char **policies; + char **alt_policies; +}; + +typedef struct Context { + Hashmap *networks_by_name; + Hashmap *netdevs_by_name; + Hashmap *links_by_filename; +} Context; + +int parse_cmdline_item(const char *key, const char *value, void *data); +int context_merge_networks(Context *context); +void context_clear(Context *context); + +Network *network_get(Context *context, const char *ifname); +void network_dump(Network *network, FILE *f); +int network_format(Network *network, char **ret); + +NetDev *netdev_get(Context *context, const char *ifname); +void netdev_dump(NetDev *netdev, FILE *f); +int netdev_format(NetDev *netdev, char **ret); + +Link *link_get(Context *context, const char *filename); +void link_dump(Link *link, FILE *f); +int link_format(Link *link, char **ret); diff --git a/src/network/generator/test-network-generator.c b/src/network/generator/test-network-generator.c new file mode 100644 index 0000000..7850da9 --- /dev/null +++ b/src/network/generator/test-network-generator.c @@ -0,0 +1,462 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "macro.h" +#include "network-generator.h" +#include "string-util.h" +#include "tests.h" + +static void test_network_one(const char *ifname, const char *key, const char *value, const char *expected) { + _cleanup_(context_clear) Context context = {}; + _cleanup_free_ char *output = NULL; + Network *network; + + printf("# %s=%s\n", key, value); + assert_se(parse_cmdline_item(key, value, &context) >= 0); + assert_se(network = network_get(&context, ifname)); + assert_se(network_format(network, &output) >= 0); + puts(output); + assert_se(streq(output, expected)); +} + +static void test_network_two(const char *ifname, + const char *key1, const char *value1, + const char *key2, const char *value2, + const char *expected) { + _cleanup_(context_clear) Context context = {}; + _cleanup_free_ char *output = NULL; + Network *network; + + printf("# %s=%s\n", key1, value1); + printf("# %s=%s\n", key2, value2); + assert_se(parse_cmdline_item(key1, value1, &context) >= 0); + assert_se(parse_cmdline_item(key2, value2, &context) >= 0); + assert_se(context_merge_networks(&context) >= 0); + assert_se(network = network_get(&context, ifname)); + assert_se(network_format(network, &output) >= 0); + puts(output); + assert_se(streq(output, expected)); +} + +static void test_netdev_one(const char *ifname, const char *key, const char *value, const char *expected) { + _cleanup_(context_clear) Context context = {}; + _cleanup_free_ char *output = NULL; + NetDev *netdev; + + printf("# %s=%s\n", key, value); + assert_se(parse_cmdline_item(key, value, &context) >= 0); + assert_se(netdev = netdev_get(&context, ifname)); + assert_se(netdev_format(netdev, &output) >= 0); + puts(output); + assert_se(streq(output, expected)); +} + +static void test_link_one(const char *filename, const char *key, const char *value, const char *expected) { + _cleanup_(context_clear) Context context = {}; + _cleanup_free_ char *output = NULL; + Link *link; + + printf("# %s=%s\n", key, value); + assert_se(parse_cmdline_item(key, value, &context) >= 0); + assert_se(link = link_get(&context, filename)); + assert_se(link_format(link, &output) >= 0); + puts(output); + assert_se(streq(output, expected)); +} + +int main(int argc, char *argv[]) { + test_setup_logging(LOG_DEBUG); + + test_network_one("", "ip", "dhcp6", + "[Match]\n" + "Kind=!*\n" + "Type=!loopback\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=ipv6\n" + "\n[DHCP]\n" + ); + + test_network_one("eth0", "ip", "eth0:dhcp", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=ipv4\n" + "\n[DHCP]\n" + ); + + test_network_one("eth0", "ip", "eth0:dhcp:1530", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "MTUBytes=1530\n" + "\n[Network]\n" + "DHCP=ipv4\n" + "\n[DHCP]\n" + ); + + test_network_one("eth0", "ip", "eth0:dhcp:1530:00:11:22:33:44:55", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "MACAddress=00:11:22:33:44:55\n" + "MTUBytes=1530\n" + "\n[Network]\n" + "DHCP=ipv4\n" + "\n[DHCP]\n" + ); + + test_network_one("eth0", "ip", "192.168.0.10::192.168.0.1:255.255.255.0:hogehoge:eth0:on", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:1530", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "MTUBytes=1530\n" + "\n[Network]\n" + "DHCP=yes\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:1530:00:11:22:33:44:55", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "MACAddress=00:11:22:33:44:55\n" + "MTUBytes=1530\n" + "\n[Network]\n" + "DHCP=yes\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "DNS=10.10.10.10\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "DNS=10.10.10.10\n" + "DNS=10.10.10.11\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_one("eth0", "ip", "[2001:1234:56:8f63::10]::[2001:1234:56:8f63::1]:64:hogehoge:eth0:on", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=2001:1234:56:8f63::10/64\n" + "\n[Route]\n" + "Gateway=2001:1234:56:8f63::1\n" + ); + + test_network_one("eth0", "ip", "[2001:1234:56:8f63::10]:[2001:1234:56:8f63::2]:[2001:1234:56:8f63::1]:64:hogehoge:eth0:on", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=2001:1234:56:8f63::10/64\n" + "Peer=2001:1234:56:8f63::2\n" + "\n[Route]\n" + "Gateway=2001:1234:56:8f63::1\n" + ); + + test_network_one("", "rd.route", "10.1.2.3/16:10.0.2.3", + "[Match]\n" + "Kind=!*\n" + "Type=!loopback\n" + "\n[Link]\n" + "\n[Network]\n" + "\n[DHCP]\n" + "\n[Route]\n" + "Destination=10.1.2.3/16\n" + "Gateway=10.0.2.3\n" + ); + + test_network_one("eth0", "rd.route", "10.1.2.3/16:10.0.2.3:eth0", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "\n[DHCP]\n" + "\n[Route]\n" + "Destination=10.1.2.3/16\n" + "Gateway=10.0.2.3\n" + ); + + test_network_one("", "nameserver", "10.1.2.3", + "[Match]\n" + "Kind=!*\n" + "Type=!loopback\n" + "\n[Link]\n" + "\n[Network]\n" + "DNS=10.1.2.3\n" + "\n[DHCP]\n" + ); + + test_network_one("", "rd.peerdns", "0", + "[Match]\n" + "Kind=!*\n" + "Type=!loopback\n" + "\n[Link]\n" + "\n[Network]\n" + "\n[DHCP]\n" + "UseDNS=no\n" + ); + + test_network_one("", "rd.peerdns", "1", + "[Match]\n" + "Kind=!*\n" + "Type=!loopback\n" + "\n[Link]\n" + "\n[Network]\n" + "\n[DHCP]\n" + "UseDNS=yes\n" + ); + + test_network_one("eth0", "vlan", "vlan99:eth0", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "VLAN=vlan99\n" + "\n[DHCP]\n" + ); + + test_network_one("eth0", "bridge", "bridge99:eth0,eth1", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "Bridge=bridge99\n" + "\n[DHCP]\n" + ); + + test_network_one("eth1", "bridge", "bridge99:eth0,eth1", + "[Match]\n" + "Name=eth1\n" + "\n[Link]\n" + "\n[Network]\n" + "Bridge=bridge99\n" + "\n[DHCP]\n" + ); + + test_network_one("eth0", "bond", "bond99:eth0,eth1", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "Bond=bond99\n" + "\n[DHCP]\n" + ); + + test_network_one("eth1", "bond", "bond99:eth0,eth1::1530", + "[Match]\n" + "Name=eth1\n" + "\n[Link]\n" + "\n[Network]\n" + "Bond=bond99\n" + "\n[DHCP]\n" + ); + + test_netdev_one("bond99", "bond", "bond99:eth0,eth1::1530", + "[NetDev]\n" + "Kind=bond\n" + "Name=bond99\n" + "MTUBytes=1530\n" + ); + + test_link_one("hogehoge", "ifname", "hogehoge:00:11:22:33:44:55", + "[Match]\n" + "MACAddress=00:11:22:33:44:55\n" + "\n[Link]\n" + "Name=hogehoge\n" + ); + + test_link_one("001122334455", "net.ifname-policy", "keep,kernel,database,onboard,slot,path,mac,00:11:22:33:44:55", + "[Match]\n" + "MACAddress=00:11:22:33:44:55\n" + "\n[Link]\n" + "NamePolicy=keep kernel database onboard slot path mac\n" + "AlternativeNamesPolicy=database onboard slot path mac\n" + ); + + test_link_one("default", "net.ifname-policy", "keep,kernel,database,onboard,slot,path,mac", + "[Match]\n" + "OriginalName=*\n" + "\n[Link]\n" + "NamePolicy=keep kernel database onboard slot path mac\n" + "AlternativeNamesPolicy=database onboard slot path mac\n" + ); + + test_network_two("eth0", + "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11", + "rd.route", "10.1.2.3/16:10.0.2.3", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "DNS=10.10.10.10\n" + "DNS=10.10.10.11\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Destination=10.1.2.3/16\n" + "Gateway=10.0.2.3\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_two("eth0", + "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on", + "nameserver", "10.1.2.3", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "DNS=10.1.2.3\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_two("eth0", + "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11", + "nameserver", "10.1.2.3", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "DNS=10.10.10.10\n" + "DNS=10.10.10.11\n" + "DNS=10.1.2.3\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_two("eth0", + "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11", + "rd.peerdns", "1", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "DNS=10.10.10.10\n" + "DNS=10.10.10.11\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "UseDNS=yes\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + test_network_two("eth0", + "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11", + "bridge", "bridge99:eth0,eth1", + "[Match]\n" + "Name=eth0\n" + "\n[Link]\n" + "\n[Network]\n" + "DHCP=yes\n" + "DNS=10.10.10.10\n" + "DNS=10.10.10.11\n" + "Bridge=bridge99\n" + "\n[DHCP]\n" + "Hostname=hogehoge\n" + "\n[Address]\n" + "Address=192.168.0.10/24\n" + "Peer=192.168.0.2\n" + "\n[Route]\n" + "Gateway=192.168.0.1\n" + ); + + return 0; +} diff --git a/src/network/meson.build b/src/network/meson.build new file mode 100644 index 0000000..5c05eba --- /dev/null +++ b/src/network/meson.build @@ -0,0 +1,266 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +sources = files( + 'netdev/bareudp.c', + 'netdev/batadv.c', + 'netdev/bond.c', + 'netdev/bridge.c', + 'netdev/dummy.c', + 'netdev/fou-tunnel.c', + 'netdev/geneve.c', + 'netdev/ifb.c', + 'netdev/ipoib.c', + 'netdev/ipvlan.c', + 'netdev/l2tp-tunnel.c', + 'netdev/macsec.c', + 'netdev/macvlan.c', + 'netdev/netdev-util.c', + 'netdev/netdev.c', + 'netdev/netdevsim.c', + 'netdev/nlmon.c', + 'netdev/tunnel.c', + 'netdev/tuntap.c', + 'netdev/vcan.c', + 'netdev/veth.c', + 'netdev/vlan.c', + 'netdev/vrf.c', + 'netdev/vxcan.c', + 'netdev/vxlan.c', + 'netdev/wireguard.c', + 'netdev/wlan.c', + 'netdev/xfrm.c', + 'networkd-address-generation.c', + 'networkd-address-label.c', + 'networkd-address-pool.c', + 'networkd-address.c', + 'networkd-bridge-fdb.c', + 'networkd-bridge-mdb.c', + 'networkd-bridge-vlan.c', + 'networkd-can.c', + 'networkd-conf.c', + 'networkd-dhcp-common.c', + 'networkd-dhcp-prefix-delegation.c', + 'networkd-dhcp-server-bus.c', + 'networkd-dhcp-server-static-lease.c', + 'networkd-dhcp-server.c', + 'networkd-dhcp4-bus.c', + 'networkd-dhcp4.c', + 'networkd-dhcp6-bus.c', + 'networkd-dhcp6.c', + 'networkd-ipv4acd.c', + 'networkd-ipv4ll.c', + 'networkd-ipv6-proxy-ndp.c', + 'networkd-ipv6ll.c', + 'networkd-json.c', + 'networkd-link-bus.c', + 'networkd-link.c', + 'networkd-lldp-rx.c', + 'networkd-lldp-tx.c', + 'networkd-manager-bus.c', + 'networkd-manager.c', + 'networkd-ndisc.c', + 'networkd-neighbor.c', + 'networkd-netlabel.c', + 'networkd-network-bus.c', + 'networkd-network.c', + 'networkd-nexthop.c', + 'networkd-queue.c', + 'networkd-radv.c', + 'networkd-route-util.c', + 'networkd-route.c', + 'networkd-routing-policy-rule.c', + 'networkd-setlink.c', + 'networkd-speed-meter.c', + 'networkd-sriov.c', + 'networkd-state-file.c', + 'networkd-sysctl.c', + 'networkd-util.c', + 'networkd-wifi.c', + 'networkd-wiphy.c', + 'tc/cake.c', + 'tc/codel.c', + 'tc/drr.c', + 'tc/ets.c', + 'tc/fifo.c', + 'tc/fq-codel.c', + 'tc/fq-pie.c', + 'tc/fq.c', + 'tc/gred.c', + 'tc/hhf.c', + 'tc/htb.c', + 'tc/netem.c', + 'tc/pie.c', + 'tc/qdisc.c', + 'tc/qfq.c', + 'tc/sfb.c', + 'tc/sfq.c', + 'tc/tbf.c', + 'tc/tc-util.c', + 'tc/tc.c', + 'tc/tclass.c', + 'tc/teql.c', +) + +systemd_networkd_sources = files('networkd.c') + +systemd_networkd_wait_online_sources = files( + 'wait-online/link.c', + 'wait-online/manager.c', + 'wait-online/wait-online.c', +) + +networkctl_sources = files('networkctl.c') + +network_generator_sources = files( + 'generator/main.c', + 'generator/network-generator.c', +) + +networkd_network_gperf_gperf = files('networkd-network-gperf.gperf') +networkd_netdev_gperf_gperf = files('netdev/netdev-gperf.gperf') + +sources += custom_target( + 'networkd-gperf.c', + input : 'networkd-gperf.gperf', + output : 'networkd-gperf.c', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + +sources += custom_target( + 'networkd-network-gperf.c', + input : networkd_network_gperf_gperf, + output : 'networkd-network-gperf.c', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + +sources += custom_target( + 'netdev-gperf.c', + input : networkd_netdev_gperf_gperf, + output : 'netdev-gperf.c', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + +if get_option('link-networkd-shared') + networkd_link_with = [libshared] +else + networkd_link_with = [libsystemd_static, + libshared_static, + libbasic_gcrypt] +endif + +network_includes = [libsystemd_network_includes, include_directories(['.', 'netdev', 'tc'])] + +libnetworkd_core = static_library( + 'networkd-core', + sources, + include_directories : network_includes, + dependencies : userspace, + link_with : networkd_link_with, + build_by_default : false) + +network_test_template = test_template + { + 'link_with' : [ + libnetworkd_core, + libsystemd_network, + ], + 'include_directories' : network_includes, +} + +network_fuzz_template = fuzz_template + { + 'link_with' : [ + libnetworkd_core, + libsystemd_network, + ], + 'dependencies' : threads, + 'include_directories' : network_includes, +} + +executables += [ + libexec_template + { + 'name' : 'systemd-networkd', + 'dbus' : true, + 'conditions' : ['ENABLE_NETWORKD'], + 'sources' : systemd_networkd_sources, + 'include_directories' : network_includes, + 'link_with' : [ + libnetworkd_core, + libsystemd_network, + networkd_link_with, + ], + 'dependencies' : threads, + }, + libexec_template + { + 'name' : 'systemd-networkd-wait-online', + 'public' : true, + 'conditions' : ['ENABLE_NETWORKD'], + 'sources' : systemd_networkd_wait_online_sources, + 'link_with' : networkd_link_with, + }, + executable_template + { + 'name' : 'networkctl', + 'public' : true, + 'conditions' : ['ENABLE_NETWORKD'], + 'sources' : networkctl_sources, + 'include_directories' : libsystemd_network_includes, + 'link_with' : [ + libsystemd_network, + networkd_link_with, + ], + }, + libexec_template + { + 'name' : 'systemd-network-generator', + 'sources' : network_generator_sources, + 'link_with' : networkd_link_with, + }, + test_template + { + 'sources' : files( + 'generator/test-network-generator.c', + 'generator/network-generator.c', + ), + 'suite' : 'network', + }, + network_test_template + { + 'sources' : files('test-network-tables.c'), + 'dependencies' : threads, + }, + network_test_template + { + 'sources' : files('test-network.c'), + 'dependencies' : threads, + }, + network_test_template + { + 'sources' : files('test-networkd-address.c'), + 'dependencies' : libatomic, + }, + network_test_template + { + 'sources' : files('test-networkd-conf.c'), + 'dependencies' : libatomic, + }, + network_test_template + { + 'sources' : files('test-networkd-util.c'), + }, + network_fuzz_template + { + 'sources' : files('fuzz-netdev-parser.c'), + }, + network_fuzz_template + { + 'sources' : files('fuzz-network-parser.c'), + }, +] + +if conf.get('ENABLE_NETWORKD') == 1 + install_data('org.freedesktop.network1.conf', + install_dir : dbuspolicydir) + install_data('org.freedesktop.network1.service', + install_dir : dbussystemservicedir) + install_data('org.freedesktop.network1.policy', + install_dir : polkitpolicydir) + if install_polkit + install_data('systemd-networkd.rules', + install_dir : polkitrulesdir) + endif + if install_polkit_pkla + install_data('systemd-networkd.pkla', + install_dir : polkitpkladir) + endif + + if install_sysconfdir_samples + install_data('networkd.conf', + install_dir : pkgconfigfiledir) + endif +endif diff --git a/src/network/netdev/bareudp.c b/src/network/netdev/bareudp.c new file mode 100644 index 0000000..1df8865 --- /dev/null +++ b/src/network/netdev/bareudp.c @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <netinet/in.h> +#include <linux/if_arp.h> + +#include "bareudp.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "string-table.h" + +static const char* const bare_udp_protocol_table[_BARE_UDP_PROTOCOL_MAX] = { + [BARE_UDP_PROTOCOL_IPV4] = "ipv4", + [BARE_UDP_PROTOCOL_IPV6] = "ipv6", + [BARE_UDP_PROTOCOL_MPLS_UC] = "mpls-uc", + [BARE_UDP_PROTOCOL_MPLS_MC] = "mpls-mc", +}; + +DEFINE_STRING_TABLE_LOOKUP(bare_udp_protocol, BareUDPProtocol); +DEFINE_CONFIG_PARSE_ENUM(config_parse_bare_udp_iftype, bare_udp_protocol, BareUDPProtocol, + "Failed to parse EtherType="); + +static int netdev_bare_udp_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(m); + + BareUDP *u = BAREUDP(netdev); + int r; + + r = sd_netlink_message_append_u16(m, IFLA_BAREUDP_ETHERTYPE, htobe16(u->iftype)); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_BAREUDP_PORT, htobe16(u->dest_port)); + if (r < 0) + return r; + + return 0; +} + +static int netdev_bare_udp_verify(NetDev *netdev, const char *filename) { + assert(filename); + + BareUDP *u = BAREUDP(netdev); + + 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 = BAREUDP(netdev); + + u->iftype = _BARE_UDP_PROTOCOL_INVALID; +} + +const NetDevVTable bare_udp_vtable = { + .object_size = sizeof(BareUDP), + .sections = NETDEV_COMMON_SECTIONS "BareUDP\0", + .init = bare_udp_init, + .config_verify = netdev_bare_udp_verify, + .fill_message_create = netdev_bare_udp_fill_message_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_NONE, +}; diff --git a/src/network/netdev/bareudp.h b/src/network/netdev/bareudp.h new file mode 100644 index 0000000..8d8863c --- /dev/null +++ b/src/network/netdev/bareudp.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +typedef struct BareUDP BareUDP; + +#include <linux/if_ether.h> + +#include "conf-parser.h" +#include "netdev.h" + +typedef enum BareUDPProtocol { + BARE_UDP_PROTOCOL_IPV4 = ETH_P_IP, + BARE_UDP_PROTOCOL_IPV6 = ETH_P_IPV6, + BARE_UDP_PROTOCOL_MPLS_UC = ETH_P_MPLS_UC, + BARE_UDP_PROTOCOL_MPLS_MC = ETH_P_MPLS_MC, + _BARE_UDP_PROTOCOL_MAX, + _BARE_UDP_PROTOCOL_INVALID = -EINVAL, +} BareUDPProtocol; + +struct BareUDP { + NetDev meta; + + BareUDPProtocol iftype; + uint16_t dest_port; +}; + +DEFINE_NETDEV_CAST(BAREUDP, BareUDP); +extern const NetDevVTable bare_udp_vtable; + +const char *bare_udp_protocol_to_string(BareUDPProtocol d) _const_; +BareUDPProtocol bare_udp_protocol_from_string(const char *d) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_bare_udp_iftype); diff --git a/src/network/netdev/batadv.c b/src/network/netdev/batadv.c new file mode 100644 index 0000000..26da023 --- /dev/null +++ b/src/network/netdev/batadv.c @@ -0,0 +1,208 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <inttypes.h> +#include <netinet/in.h> +#include <linux/genetlink.h> +#include <linux/if_arp.h> + +#include "batadv.h" +#include "fileio.h" +#include "netlink-util.h" +#include "network-internal.h" +#include "networkd-manager.h" +#include "parse-util.h" +#include "stdio-util.h" +#include "string-table.h" +#include "string-util.h" + +static void batadv_init(NetDev *n) { + BatmanAdvanced *b = BATADV(n); + + /* Set defaults */ + b->aggregation = true; + b->gateway_bandwidth_down = 10000; + b->gateway_bandwidth_up = 2000; + b->bridge_loop_avoidance = true; + b->distributed_arp_table = true; + b->fragmentation = true; + b->hop_penalty = 15; + b->originator_interval = 1000; + b->routing_algorithm = BATADV_ROUTING_ALGORITHM_BATMAN_V; +} + +static const char* const batadv_gateway_mode_table[_BATADV_GATEWAY_MODE_MAX] = { + [BATADV_GATEWAY_MODE_OFF] = "off", + [BATADV_GATEWAY_MODE_CLIENT] = "client", + [BATADV_GATEWAY_MODE_SERVER] = "server", +}; + +static const char* const batadv_routing_algorithm_table[_BATADV_ROUTING_ALGORITHM_MAX] = { + [BATADV_ROUTING_ALGORITHM_BATMAN_V] = "batman-v", + [BATADV_ROUTING_ALGORITHM_BATMAN_IV] = "batman-iv", +}; + +static const char* const batadv_routing_algorithm_kernel_table[_BATADV_ROUTING_ALGORITHM_MAX] = { + [BATADV_ROUTING_ALGORITHM_BATMAN_V] = "BATMAN_V", + [BATADV_ROUTING_ALGORITHM_BATMAN_IV] = "BATMAN_IV", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(batadv_gateway_mode, BatadvGatewayModes); +DEFINE_CONFIG_PARSE_ENUM(config_parse_batadv_gateway_mode, batadv_gateway_mode, BatadvGatewayModes, + "Failed to parse GatewayMode="); + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(batadv_routing_algorithm, BatadvRoutingAlgorithm); +DEFINE_CONFIG_PARSE_ENUM(config_parse_batadv_routing_algorithm, batadv_routing_algorithm, BatadvRoutingAlgorithm, + "Failed to parse RoutingAlgorithm="); + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(batadv_routing_algorithm_kernel, BatadvRoutingAlgorithm); + +int config_parse_badadv_bandwidth ( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint64_t k; + uint32_t *bandwidth = data; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = parse_size(rvalue, 1000, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (k/1000/100 > UINT32_MAX) + log_syntax(unit, LOG_WARNING, filename, line, 0, + "The value of '%s=', is outside of 0...429496729500000 range: %s", + lvalue, rvalue); + + *bandwidth = k/1000/100; + + return 0; +} + +/* callback for batman netdev's parameter set */ +static int netdev_batman_set_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) { + int r; + + assert(netdev); + assert(m); + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + log_netdev_warning_errno(netdev, r, "BATADV parameters could not be set: %m"); + return 1; + } + + log_netdev_debug(netdev, "BATADV parameters set success"); + + return 1; +} + +static int netdev_batadv_post_create_message(NetDev *netdev, sd_netlink_message *message) { + BatmanAdvanced *b = BATADV(netdev); + int r; + + r = sd_netlink_message_append_u32(message, BATADV_ATTR_MESH_IFINDEX, netdev->ifindex); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(message, BATADV_ATTR_GW_MODE, b->gateway_mode); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(message, BATADV_ATTR_AGGREGATED_OGMS_ENABLED, b->aggregation); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(message, BATADV_ATTR_BRIDGE_LOOP_AVOIDANCE_ENABLED, b->bridge_loop_avoidance); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(message, BATADV_ATTR_DISTRIBUTED_ARP_TABLE_ENABLED, b->distributed_arp_table); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(message, BATADV_ATTR_FRAGMENTATION_ENABLED, b->fragmentation); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(message, BATADV_ATTR_HOP_PENALTY, b->hop_penalty); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(message, BATADV_ATTR_ORIG_INTERVAL, DIV_ROUND_UP(b->originator_interval, USEC_PER_MSEC)); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(message, BATADV_ATTR_GW_BANDWIDTH_DOWN, b->gateway_bandwidth_down); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(message, BATADV_ATTR_GW_BANDWIDTH_UP, b->gateway_bandwidth_up); + if (r < 0) + return r; + + return 0; +} + +static int netdev_batadv_post_create(NetDev *netdev, Link *link) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL; + int r; + + assert(netdev); + + r = sd_genl_message_new(netdev->manager->genl, BATADV_NL_NAME, BATADV_CMD_SET_MESH, &message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not allocate netlink message: %m"); + + r = netdev_batadv_post_create_message(netdev, message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not create netlink message: %m"); + + r = netlink_call_async(netdev->manager->genl, NULL, message, netdev_batman_set_handler, + netdev_destroy_callback, netdev); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not send netlink message: %m"); + + netdev_ref(netdev); + + return r; +} + +static int netdev_batadv_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(m); + + BatmanAdvanced *b = BATADV(netdev); + int r; + + r = sd_netlink_message_append_string(m, IFLA_BATADV_ALGO_NAME, batadv_routing_algorithm_kernel_to_string(b->routing_algorithm)); + if (r < 0) + return r; + + return 0; +} + +const NetDevVTable batadv_vtable = { + .object_size = sizeof(BatmanAdvanced), + .init = batadv_init, + .sections = NETDEV_COMMON_SECTIONS "BatmanAdvanced\0", + .fill_message_create = netdev_batadv_fill_message_create, + .post_create = netdev_batadv_post_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/batadv.h b/src/network/netdev/batadv.h new file mode 100644 index 0000000..f1f9b46 --- /dev/null +++ b/src/network/netdev/batadv.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +#include <linux/batman_adv.h> + +#include "conf-parser.h" +#include "netdev.h" + +#define BATADV_GENL_NAME "batadv" + +typedef enum BatadvGatewayModes { + BATADV_GATEWAY_MODE_OFF = BATADV_GW_MODE_OFF, + BATADV_GATEWAY_MODE_CLIENT = BATADV_GW_MODE_CLIENT, + BATADV_GATEWAY_MODE_SERVER = BATADV_GW_MODE_SERVER, + _BATADV_GATEWAY_MODE_MAX, + _BATADV_GATEWAY_MODE_INVALID = -EINVAL, +} BatadvGatewayModes; + +typedef enum BatadvRoutingAlgorithm { + BATADV_ROUTING_ALGORITHM_BATMAN_V, + BATADV_ROUTING_ALGORITHM_BATMAN_IV, + _BATADV_ROUTING_ALGORITHM_MAX, + _BATADV_ROUTING_ALGORITHM_INVALID = -EINVAL, +} BatadvRoutingAlgorithm; + +typedef struct Batadv { + NetDev meta; + + BatadvGatewayModes gateway_mode; + uint32_t gateway_bandwidth_down; + uint32_t gateway_bandwidth_up; + uint8_t hop_penalty; + BatadvRoutingAlgorithm routing_algorithm; + usec_t originator_interval; + bool aggregation; + bool bridge_loop_avoidance; + bool distributed_arp_table; + bool fragmentation; +} BatmanAdvanced; + +DEFINE_NETDEV_CAST(BATADV, BatmanAdvanced); +extern const NetDevVTable batadv_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_batadv_gateway_mode); +CONFIG_PARSER_PROTOTYPE(config_parse_batadv_routing_algorithm); +CONFIG_PARSER_PROTOTYPE(config_parse_badadv_bandwidth); diff --git a/src/network/netdev/bond.c b/src/network/netdev/bond.c new file mode 100644 index 0000000..4d75a0d --- /dev/null +++ b/src/network/netdev/bond.c @@ -0,0 +1,415 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/if_arp.h> + +#include "alloc-util.h" +#include "bond.h" +#include "bond-util.h" +#include "conf-parser.h" +#include "ether-addr-util.h" +#include "extract-word.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "string-table.h" + +/* + * Number of seconds between instances where the bonding + * driver sends learning packets to each slaves peer switch + */ +#define LEARNING_PACKETS_INTERVAL_MIN_SEC (1 * USEC_PER_SEC) +#define LEARNING_PACKETS_INTERVAL_MAX_SEC (0x7fffffff * USEC_PER_SEC) + +/* Number of IGMP membership reports to be issued after + * a failover event. + */ +#define RESEND_IGMP_MIN 0 +#define RESEND_IGMP_MAX 255 +#define RESEND_IGMP_DEFAULT 1 + +/* + * Number of packets to transmit through a slave before + * moving to the next one. + */ +#define PACKETS_PER_SLAVE_MIN 0 +#define PACKETS_PER_SLAVE_MAX 65535 +#define PACKETS_PER_SLAVE_DEFAULT 1 + +/* + * Number of peer notifications (gratuitous ARPs and + * unsolicited IPv6 Neighbor Advertisements) to be issued after a + * failover event. + */ +#define GRATUITOUS_ARP_MIN 0 +#define GRATUITOUS_ARP_MAX 255 +#define GRATUITOUS_ARP_DEFAULT 1 + +DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_mode, bond_mode, BondMode, "Failed to parse bond mode"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_xmit_hash_policy, + bond_xmit_hash_policy, + BondXmitHashPolicy, + "Failed to parse bond transmit hash policy"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_lacp_rate, bond_lacp_rate, BondLacpRate, "Failed to parse bond lacp rate"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_ad_select, bond_ad_select, BondAdSelect, "Failed to parse bond AD select"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_fail_over_mac, bond_fail_over_mac, BondFailOverMac, "Failed to parse bond fail over MAC"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_arp_validate, bond_arp_validate, BondArpValidate, "Failed to parse bond arp validate"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_arp_all_targets, bond_arp_all_targets, BondArpAllTargets, "Failed to parse bond Arp all targets"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_bond_primary_reselect, bond_primary_reselect, BondPrimaryReselect, "Failed to parse bond primary reselect"); + +static int netdev_bond_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(!link); + assert(m); + + Bond *b = BOND(netdev); + int r; + + if (b->mode != _NETDEV_BOND_MODE_INVALID) { + r = sd_netlink_message_append_u8(m, IFLA_BOND_MODE, b->mode); + if (r < 0) + return r; + } + + if (b->xmit_hash_policy != _NETDEV_BOND_XMIT_HASH_POLICY_INVALID) { + r = sd_netlink_message_append_u8(m, IFLA_BOND_XMIT_HASH_POLICY, b->xmit_hash_policy); + if (r < 0) + return r; + } + + if (b->lacp_rate != _NETDEV_BOND_LACP_RATE_INVALID && + b->mode == NETDEV_BOND_MODE_802_3AD) { + r = sd_netlink_message_append_u8(m, IFLA_BOND_AD_LACP_RATE, b->lacp_rate); + if (r < 0) + return r; + } + + if (b->miimon != 0) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_MIIMON, b->miimon / USEC_PER_MSEC); + if (r < 0) + return r; + } + + if (b->downdelay != 0) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_DOWNDELAY, b->downdelay / USEC_PER_MSEC); + if (r < 0) + return r; + } + + if (b->updelay != 0) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_UPDELAY, b->updelay / USEC_PER_MSEC); + if (r < 0) + return r; + } + + if (b->arp_interval != 0) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_ARP_INTERVAL, b->arp_interval / USEC_PER_MSEC); + if (r < 0) + return r; + + if (b->lp_interval >= LEARNING_PACKETS_INTERVAL_MIN_SEC && + b->lp_interval <= LEARNING_PACKETS_INTERVAL_MAX_SEC) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_LP_INTERVAL, b->lp_interval / USEC_PER_SEC); + if (r < 0) + return r; + } + } + + if (b->ad_select != _NETDEV_BOND_AD_SELECT_INVALID && + b->mode == NETDEV_BOND_MODE_802_3AD) { + r = sd_netlink_message_append_u8(m, IFLA_BOND_AD_SELECT, b->ad_select); + if (r < 0) + return r; + } + + if (b->fail_over_mac != _NETDEV_BOND_FAIL_OVER_MAC_INVALID && + b->mode == NETDEV_BOND_MODE_ACTIVE_BACKUP) { + r = sd_netlink_message_append_u8(m, IFLA_BOND_FAIL_OVER_MAC, b->fail_over_mac); + if (r < 0) + return r; + } + + if (b->arp_validate != _NETDEV_BOND_ARP_VALIDATE_INVALID) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_ARP_VALIDATE, b->arp_validate); + if (r < 0) + return r; + } + + if (b->arp_all_targets != _NETDEV_BOND_ARP_ALL_TARGETS_INVALID) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_ARP_ALL_TARGETS, b->arp_all_targets); + if (r < 0) + return r; + } + + if (b->primary_reselect != _NETDEV_BOND_PRIMARY_RESELECT_INVALID) { + r = sd_netlink_message_append_u8(m, IFLA_BOND_PRIMARY_RESELECT, b->primary_reselect); + if (r < 0) + return r; + } + + if (b->resend_igmp <= RESEND_IGMP_MAX) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_RESEND_IGMP, b->resend_igmp); + if (r < 0) + return r; + } + + if (b->packets_per_slave <= PACKETS_PER_SLAVE_MAX && + b->mode == NETDEV_BOND_MODE_BALANCE_RR) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_PACKETS_PER_SLAVE, b->packets_per_slave); + if (r < 0) + return r; + } + + if (b->num_grat_arp <= GRATUITOUS_ARP_MAX) { + r = sd_netlink_message_append_u8(m, IFLA_BOND_NUM_PEER_NOTIF, b->num_grat_arp); + if (r < 0) + return r; + } + + if (b->min_links != 0) { + r = sd_netlink_message_append_u32(m, IFLA_BOND_MIN_LINKS, b->min_links); + if (r < 0) + return r; + } + + if (b->ad_actor_sys_prio != 0) { + r = sd_netlink_message_append_u16(m, IFLA_BOND_AD_ACTOR_SYS_PRIO, b->ad_actor_sys_prio); + if (r < 0) + return r; + } + + if (b->ad_user_port_key != 0) { + r = sd_netlink_message_append_u16(m, IFLA_BOND_AD_USER_PORT_KEY, b->ad_user_port_key); + if (r < 0) + return r; + } + + if (!ether_addr_is_null(&b->ad_actor_system)) { + r = sd_netlink_message_append_ether_addr(m, IFLA_BOND_AD_ACTOR_SYSTEM, &b->ad_actor_system); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u8(m, IFLA_BOND_ALL_SLAVES_ACTIVE, b->all_slaves_active); + if (r < 0) + return r; + + if (b->tlb_dynamic_lb >= 0) { + r = sd_netlink_message_append_u8(m, IFLA_BOND_TLB_DYNAMIC_LB, b->tlb_dynamic_lb); + if (r < 0) + return r; + } + + if (b->arp_interval > 0 && !ordered_set_isempty(b->arp_ip_targets)) { + void *val; + int n = 0; + + r = sd_netlink_message_open_container(m, IFLA_BOND_ARP_IP_TARGET); + if (r < 0) + return r; + + ORDERED_SET_FOREACH(val, b->arp_ip_targets) { + r = sd_netlink_message_append_u32(m, n++, PTR_TO_UINT32(val)); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + } + + return 0; +} + +int config_parse_arp_ip_target_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + Bond *b = BOND(userdata); + int r; + + if (isempty(rvalue)) { + b->arp_ip_targets = ordered_set_free(b->arp_ip_targets); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *n = NULL; + union in_addr_union ip; + + r = extract_first_word(&p, &n, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse Bond ARP IP target address, ignoring assignment: %s", + rvalue); + return 0; + } + if (r == 0) + return 0; + + r = in_addr_from_string(AF_INET, n, &ip); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Bond ARP IP target address is invalid, ignoring assignment: %s", n); + continue; + } + + if (ordered_set_size(b->arp_ip_targets) >= NETDEV_BOND_ARP_TARGETS_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Too many ARP IP targets are specified. The maximum number is %d. Ignoring assignment: %s", + NETDEV_BOND_ARP_TARGETS_MAX, n); + continue; + } + + r = ordered_set_ensure_put(&b->arp_ip_targets, NULL, UINT32_TO_PTR(ip.in.s_addr)); + if (r == -ENOMEM) + return log_oom(); + if (r == -EEXIST) + log_syntax(unit, LOG_WARNING, filename, line, r, + "Bond ARP IP target address is duplicated, ignoring assignment: %s", n); + if (r < 0) + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store bond ARP IP target address '%s', ignoring assignment: %m", n); + } +} + +int config_parse_ad_actor_sys_prio( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + Bond *b = ASSERT_PTR(userdata); + + return config_parse_uint16_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 1, UINT16_MAX, true, + &b->ad_actor_sys_prio); +} + +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) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + Bond *b = ASSERT_PTR(userdata); + + return config_parse_uint16_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, 1023, /* ignoring= */ true, + &b->ad_user_port_key); +} + +int config_parse_ad_actor_system( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + Bond *b = userdata; + struct ether_addr n; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + r = parse_ether_addr(rvalue, &n); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Not a valid MAC address %s. Ignoring assignment: %m", + rvalue); + return 0; + } + if (ether_addr_is_null(&n) || (n.ether_addr_octet[0] & 0x01)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Not an appropriate MAC address %s, cannot be null or multicast. Ignoring assignment.", + rvalue); + return 0; + } + + b->ad_actor_system = n; + + return 0; +} + +static void bond_done(NetDev *netdev) { + Bond *b = BOND(netdev); + + ordered_set_free(b->arp_ip_targets); +} + +static void bond_init(NetDev *netdev) { + Bond *b = BOND(netdev); + + b->mode = _NETDEV_BOND_MODE_INVALID; + b->xmit_hash_policy = _NETDEV_BOND_XMIT_HASH_POLICY_INVALID; + b->lacp_rate = _NETDEV_BOND_LACP_RATE_INVALID; + b->ad_select = _NETDEV_BOND_AD_SELECT_INVALID; + b->fail_over_mac = _NETDEV_BOND_FAIL_OVER_MAC_INVALID; + b->arp_validate = _NETDEV_BOND_ARP_VALIDATE_INVALID; + b->arp_all_targets = _NETDEV_BOND_ARP_ALL_TARGETS_INVALID; + b->primary_reselect = _NETDEV_BOND_PRIMARY_RESELECT_INVALID; + + b->all_slaves_active = false; + b->tlb_dynamic_lb = -1; + + b->resend_igmp = RESEND_IGMP_DEFAULT; + b->packets_per_slave = PACKETS_PER_SLAVE_DEFAULT; + b->num_grat_arp = GRATUITOUS_ARP_DEFAULT; + b->lp_interval = LEARNING_PACKETS_INTERVAL_MIN_SEC; +} + +const NetDevVTable bond_vtable = { + .object_size = sizeof(Bond), + .init = bond_init, + .done = bond_done, + .sections = NETDEV_COMMON_SECTIONS "Bond\0", + .fill_message_create = netdev_bond_fill_message_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/bond.h b/src/network/netdev/bond.h new file mode 100644 index 0000000..e4b0a0d --- /dev/null +++ b/src/network/netdev/bond.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <netinet/in.h> +#include <linux/if_bonding.h> + +#include "bond-util.h" +#include "macro.h" +#include "netdev.h" +#include "ordered-set.h" + +typedef struct Bond { + NetDev meta; + + BondMode mode; + BondXmitHashPolicy xmit_hash_policy; + BondLacpRate lacp_rate; + BondAdSelect ad_select; + BondFailOverMac fail_over_mac; + BondArpValidate arp_validate; + BondArpAllTargets arp_all_targets; + BondPrimaryReselect primary_reselect; + + int tlb_dynamic_lb; + + bool all_slaves_active; + + unsigned resend_igmp; + unsigned packets_per_slave; + unsigned num_grat_arp; + unsigned min_links; + + uint16_t ad_actor_sys_prio; + uint16_t ad_user_port_key; + struct ether_addr ad_actor_system; + + usec_t miimon; + usec_t updelay; + usec_t downdelay; + usec_t arp_interval; + usec_t lp_interval; + + OrderedSet *arp_ip_targets; +} Bond; + +DEFINE_NETDEV_CAST(BOND, Bond); +extern const NetDevVTable bond_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_bond_mode); +CONFIG_PARSER_PROTOTYPE(config_parse_bond_xmit_hash_policy); +CONFIG_PARSER_PROTOTYPE(config_parse_bond_lacp_rate); +CONFIG_PARSER_PROTOTYPE(config_parse_bond_ad_select); +CONFIG_PARSER_PROTOTYPE(config_parse_bond_fail_over_mac); +CONFIG_PARSER_PROTOTYPE(config_parse_bond_arp_validate); +CONFIG_PARSER_PROTOTYPE(config_parse_bond_arp_all_targets); +CONFIG_PARSER_PROTOTYPE(config_parse_bond_primary_reselect); +CONFIG_PARSER_PROTOTYPE(config_parse_arp_ip_target_address); +CONFIG_PARSER_PROTOTYPE(config_parse_ad_actor_sys_prio); +CONFIG_PARSER_PROTOTYPE(config_parse_ad_user_port_key); +CONFIG_PARSER_PROTOTYPE(config_parse_ad_actor_system); diff --git a/src/network/netdev/bridge.c b/src/network/netdev/bridge.c new file mode 100644 index 0000000..3e394ed --- /dev/null +++ b/src/network/netdev/bridge.c @@ -0,0 +1,253 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> +#include <linux/if_bridge.h> + +#include "bridge.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "string-table.h" +#include "vlan-util.h" + +assert_cc((int) MULTICAST_ROUTER_NONE == (int) MDB_RTR_TYPE_DISABLED); +assert_cc((int) MULTICAST_ROUTER_TEMPORARY_QUERY == (int) MDB_RTR_TYPE_TEMP_QUERY); +assert_cc((int) MULTICAST_ROUTER_PERMANENT == (int) MDB_RTR_TYPE_PERM); +assert_cc((int) MULTICAST_ROUTER_TEMPORARY == (int) MDB_RTR_TYPE_TEMP); + +static const char* const multicast_router_table[_MULTICAST_ROUTER_MAX] = { + [MULTICAST_ROUTER_NONE] = "no", + [MULTICAST_ROUTER_TEMPORARY_QUERY] = "query", + [MULTICAST_ROUTER_PERMANENT] = "permanent", + [MULTICAST_ROUTER_TEMPORARY] = "temporary", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(multicast_router, MulticastRouter, _MULTICAST_ROUTER_INVALID); +DEFINE_CONFIG_PARSE_ENUM(config_parse_multicast_router, multicast_router, MulticastRouter, + "Failed to parse bridge multicast router setting"); + +/* callback for bridge netdev's parameter set */ +static int netdev_bridge_set_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) { + int r; + + assert(netdev); + assert(m); + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + log_netdev_warning_errno(netdev, r, "Bridge parameters could not be set: %m"); + return 1; + } + + log_netdev_debug(netdev, "Bridge parameters set success"); + + return 1; +} + +static int netdev_bridge_post_create_message(NetDev *netdev, sd_netlink_message *req) { + Bridge *b = BRIDGE(netdev); + int r; + + r = sd_netlink_message_open_container(req, IFLA_LINKINFO); + if (r < 0) + return r; + + r = sd_netlink_message_open_container_union(req, IFLA_INFO_DATA, netdev_kind_to_string(netdev->kind)); + if (r < 0) + return r; + + /* convert to jiffes */ + if (b->forward_delay != USEC_INFINITY) { + r = sd_netlink_message_append_u32(req, IFLA_BR_FORWARD_DELAY, usec_to_jiffies(b->forward_delay)); + if (r < 0) + return r; + } + + if (b->hello_time > 0) { + r = sd_netlink_message_append_u32(req, IFLA_BR_HELLO_TIME, usec_to_jiffies(b->hello_time)); + if (r < 0) + return r; + } + + if (b->max_age > 0) { + r = sd_netlink_message_append_u32(req, IFLA_BR_MAX_AGE, usec_to_jiffies(b->max_age)); + if (r < 0) + return r; + } + + if (b->ageing_time != USEC_INFINITY) { + r = sd_netlink_message_append_u32(req, IFLA_BR_AGEING_TIME, usec_to_jiffies(b->ageing_time)); + if (r < 0) + return r; + } + + if (b->priority > 0) { + r = sd_netlink_message_append_u16(req, IFLA_BR_PRIORITY, b->priority); + if (r < 0) + return r; + } + + if (b->group_fwd_mask > 0) { + r = sd_netlink_message_append_u16(req, IFLA_BR_GROUP_FWD_MASK, b->group_fwd_mask); + if (r < 0) + return r; + } + + if (b->default_pvid != VLANID_INVALID) { + r = sd_netlink_message_append_u16(req, IFLA_BR_VLAN_DEFAULT_PVID, b->default_pvid); + if (r < 0) + return r; + } + + if (b->mcast_querier >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_QUERIER, b->mcast_querier); + if (r < 0) + return r; + } + + if (b->mcast_snooping >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_SNOOPING, b->mcast_snooping); + if (r < 0) + return r; + } + + if (b->vlan_filtering >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BR_VLAN_FILTERING, b->vlan_filtering); + if (r < 0) + return r; + } + + if (b->vlan_protocol >= 0) { + r = sd_netlink_message_append_u16(req, IFLA_BR_VLAN_PROTOCOL, htobe16(b->vlan_protocol)); + if (r < 0) + return r; + } + + if (b->stp >= 0) { + r = sd_netlink_message_append_u32(req, IFLA_BR_STP_STATE, b->stp); + if (r < 0) + return r; + } + + if (b->igmp_version > 0) { + r = sd_netlink_message_append_u8(req, IFLA_BR_MCAST_IGMP_VERSION, b->igmp_version); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +static int netdev_bridge_post_create(NetDev *netdev, Link *link) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(netdev); + + r = sd_rtnl_message_new_link(netdev->manager->rtnl, &req, RTM_NEWLINK, netdev->ifindex); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not allocate netlink message: %m"); + + r = sd_netlink_message_set_flags(req, NLM_F_REQUEST | NLM_F_ACK); + if (r < 0) + return log_link_error_errno(link, r, "Could not set netlink message flags: %m"); + + r = netdev_bridge_post_create_message(netdev, req); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not create netlink message: %m"); + + r = netlink_call_async(netdev->manager->rtnl, NULL, req, netdev_bridge_set_handler, + netdev_destroy_callback, netdev); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not send netlink message: %m"); + + netdev_ref(netdev); + + return r; +} + +int config_parse_bridge_igmp_version( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + Bridge *b = ASSERT_PTR(userdata); + + if (isempty(rvalue)) { + b->igmp_version = 0; /* 0 means unset. */ + return 0; + } + + return config_parse_uint8_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 2, 3, true, + &b->igmp_version); +} + +int config_parse_bridge_port_priority( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + + uint16_t *prio = ASSERT_PTR(data); + + return config_parse_uint16_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, LINK_BRIDGE_PORT_PRIORITY_MAX, true, + prio); +} + +static void bridge_init(NetDev *netdev) { + Bridge *b = BRIDGE(netdev); + + b->mcast_querier = -1; + b->mcast_snooping = -1; + b->vlan_filtering = -1; + b->vlan_protocol = -1; + b->stp = -1; + b->default_pvid = VLANID_INVALID; + b->forward_delay = USEC_INFINITY; + b->ageing_time = USEC_INFINITY; +} + +const NetDevVTable bridge_vtable = { + .object_size = sizeof(Bridge), + .init = bridge_init, + .sections = NETDEV_COMMON_SECTIONS "Bridge\0", + .post_create = netdev_bridge_post_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/bridge.h b/src/network/netdev/bridge.h new file mode 100644 index 0000000..72dd3e4 --- /dev/null +++ b/src/network/netdev/bridge.h @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" +#include "netdev.h" + +#define LINK_BRIDGE_PORT_PRIORITY_INVALID 128U +#define LINK_BRIDGE_PORT_PRIORITY_MAX 63U + +typedef struct Bridge { + NetDev meta; + + int mcast_querier; + int mcast_snooping; + int vlan_filtering; + int vlan_protocol; + int stp; + uint16_t priority; + uint16_t group_fwd_mask; + uint16_t default_pvid; + uint8_t igmp_version; + + usec_t forward_delay; + usec_t hello_time; + usec_t max_age; + usec_t ageing_time; +} Bridge; + +typedef enum MulticastRouter { + MULTICAST_ROUTER_NONE, + MULTICAST_ROUTER_TEMPORARY_QUERY, + MULTICAST_ROUTER_PERMANENT, + MULTICAST_ROUTER_TEMPORARY, + _MULTICAST_ROUTER_MAX, + _MULTICAST_ROUTER_INVALID = -EINVAL, +} MulticastRouter; + +DEFINE_NETDEV_CAST(BRIDGE, Bridge); +extern const NetDevVTable bridge_vtable; + +const char* multicast_router_to_string(MulticastRouter i) _const_; +MulticastRouter multicast_router_from_string(const char *s) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_multicast_router); +CONFIG_PARSER_PROTOTYPE(config_parse_bridge_igmp_version); +CONFIG_PARSER_PROTOTYPE(config_parse_bridge_port_priority); diff --git a/src/network/netdev/dummy.c b/src/network/netdev/dummy.c new file mode 100644 index 0000000..00df1d2 --- /dev/null +++ b/src/network/netdev/dummy.c @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if_arp.h> + +#include "dummy.h" + +const NetDevVTable dummy_vtable = { + .object_size = sizeof(Dummy), + .sections = NETDEV_COMMON_SECTIONS, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/dummy.h b/src/network/netdev/dummy.h new file mode 100644 index 0000000..eafdf4b --- /dev/null +++ b/src/network/netdev/dummy.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "netdev.h" + +typedef struct Dummy { + NetDev meta; +} Dummy; + +DEFINE_NETDEV_CAST(DUMMY, Dummy); +extern const NetDevVTable dummy_vtable; diff --git a/src/network/netdev/fou-tunnel.c b/src/network/netdev/fou-tunnel.c new file mode 100644 index 0000000..3bf41a8 --- /dev/null +++ b/src/network/netdev/fou-tunnel.c @@ -0,0 +1,265 @@ +/* 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" + +static const char* const fou_encap_type_table[_NETDEV_FOO_OVER_UDP_ENCAP_MAX] = { + [NETDEV_FOO_OVER_UDP_ENCAP_DIRECT] = "FooOverUDP", + [NETDEV_FOO_OVER_UDP_ENCAP_GUE] = "GenericUDPEncapsulation", +}; + +DEFINE_STRING_TABLE_LOOKUP(fou_encap_type, FooOverUDPEncapType); +DEFINE_CONFIG_PARSE_ENUM(config_parse_fou_encap_type, fou_encap_type, FooOverUDPEncapType, + "Failed to parse Encapsulation="); + +static int netdev_fill_fou_tunnel_message(NetDev *netdev, sd_netlink_message *m) { + FouTunnel *t = FOU(netdev); + uint8_t encap_type; + int r; + + r = sd_netlink_message_append_u16(m, FOU_ATTR_PORT, htobe16(t->port)); + if (r < 0) + return r; + + if (IN_SET(t->peer_family, AF_INET, AF_INET6)) { + r = sd_netlink_message_append_u16(m, FOU_ATTR_PEER_PORT, htobe16(t->peer_port)); + if (r < 0) + return r; + } + + switch (t->fou_encap_type) { + case NETDEV_FOO_OVER_UDP_ENCAP_DIRECT: + encap_type = FOU_ENCAP_DIRECT; + break; + case NETDEV_FOO_OVER_UDP_ENCAP_GUE: + encap_type = FOU_ENCAP_GUE; + break; + default: + assert_not_reached(); + } + + r = sd_netlink_message_append_u8(m, FOU_ATTR_TYPE, encap_type); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, FOU_ATTR_AF, AF_INET); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, FOU_ATTR_IPPROTO, t->fou_protocol); + if (r < 0) + return r; + + if (t->local_family == AF_INET) { + r = sd_netlink_message_append_in_addr(m, FOU_ATTR_LOCAL_V4, &t->local.in); + if (r < 0) + return r; + } else if (t->local_family == AF_INET6) { + r = sd_netlink_message_append_in6_addr(m, FOU_ATTR_LOCAL_V6, &t->local.in6); + if (r < 0) + return r; + } + + if (t->peer_family == AF_INET) { + r = sd_netlink_message_append_in_addr(m, FOU_ATTR_PEER_V4, &t->peer.in); + if (r < 0) + return r; + } else if (t->peer_family == AF_INET6){ + r = sd_netlink_message_append_in6_addr(m, FOU_ATTR_PEER_V6, &t->peer.in6); + if (r < 0) + return r; + } + + return 0; +} + +static int netdev_create_fou_tunnel_message(NetDev *netdev, sd_netlink_message **ret) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(netdev); + + r = sd_genl_message_new(netdev->manager->genl, FOU_GENL_NAME, FOU_CMD_ADD, &m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not allocate netlink message: %m"); + + r = netdev_fill_fou_tunnel_message(netdev, m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not create netlink message: %m"); + + *ret = TAKE_PTR(m); + return 0; +} + +static int fou_tunnel_create_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) { + int r; + + assert(netdev); + assert(netdev->state != _NETDEV_STATE_INVALID); + + r = sd_netlink_message_get_errno(m); + if (r == -EEXIST) + log_netdev_info(netdev, "netdev exists, using existing without changing its parameters"); + else if (r < 0) { + log_netdev_warning_errno(netdev, r, "netdev could not be created: %m"); + netdev_enter_failed(netdev); + + return 1; + } + + log_netdev_debug(netdev, "FooOverUDP tunnel is created"); + return 1; +} + +static int netdev_fou_tunnel_create(NetDev *netdev) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(FOU(netdev)); + + r = netdev_create_fou_tunnel_message(netdev, &m); + if (r < 0) + return r; + + r = netlink_call_async(netdev->manager->genl, NULL, m, fou_tunnel_create_handler, + netdev_destroy_callback, netdev); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to create FooOverUDP tunnel: %m"); + + netdev_ref(netdev); + return 0; +} + +int config_parse_ip_protocol( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + uint8_t *proto = ASSERT_PTR(data); + int r; + + r = parse_ip_protocol_full(rvalue, /* relaxed= */ true); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=%s', ignoring: %m", + lvalue, rvalue); + return 0; + } + + if (r > UINT8_MAX) { + /* linux/fou.h defines the netlink field as one byte, so we need to reject + * protocols numbers that don't fit in one byte. */ + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid '%s=%s', allowed range is 0..255, ignoring.", + lvalue, rvalue); + return 0; + } + + *proto = r; + return 0; +} + +int config_parse_fou_tunnel_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + union in_addr_union *addr = ASSERT_PTR(data); + FouTunnel *t = userdata; + int r, *f; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (streq(lvalue, "Local")) + f = &t->local_family; + else + f = &t->peer_family; + + r = in_addr_from_string_auto(rvalue, f, addr); + if (r < 0) + log_syntax(unit, LOG_WARNING, filename, line, r, + "FooOverUDP tunnel '%s' address is invalid, ignoring assignment: %s", + lvalue, rvalue); + + return 0; +} + +static int netdev_fou_tunnel_verify(NetDev *netdev, const char *filename) { + assert(filename); + + FouTunnel *t = FOU(netdev); + + switch (t->fou_encap_type) { + case NETDEV_FOO_OVER_UDP_ENCAP_DIRECT: + if (t->fou_protocol <= 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "FooOverUDP protocol not configured in %s. Rejecting configuration.", + filename); + break; + case NETDEV_FOO_OVER_UDP_ENCAP_GUE: + if (t->fou_protocol > 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "FooOverUDP GUE can't be set with protocol configured in %s. Rejecting configuration.", + filename); + break; + default: + assert_not_reached(); + } + + if (t->peer_family == AF_UNSPEC && t->peer_port > 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "FooOverUDP peer port is set but peer address not configured in %s. Rejecting configuration.", + filename); + else if (t->peer_family != AF_UNSPEC && t->peer_port == 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "FooOverUDP peer port not set but peer address is configured in %s. Rejecting configuration.", + filename); + return 0; +} + +static void fou_tunnel_init(NetDev *netdev) { + FouTunnel *t = FOU(netdev); + + t->fou_encap_type = NETDEV_FOO_OVER_UDP_ENCAP_DIRECT; +} + +const NetDevVTable foutnl_vtable = { + .object_size = sizeof(FouTunnel), + .init = fou_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "FooOverUDP\0", + .create = netdev_fou_tunnel_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .config_verify = netdev_fou_tunnel_verify, +}; diff --git a/src/network/netdev/fou-tunnel.h b/src/network/netdev/fou-tunnel.h new file mode 100644 index 0000000..576d82e --- /dev/null +++ b/src/network/netdev/fou-tunnel.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <netinet/in.h> +#include <linux/fou.h> + +#include "in-addr-util.h" +#include "netdev.h" + +typedef enum FooOverUDPEncapType { + NETDEV_FOO_OVER_UDP_ENCAP_UNSPEC = FOU_ENCAP_UNSPEC, + NETDEV_FOO_OVER_UDP_ENCAP_DIRECT = FOU_ENCAP_DIRECT, + NETDEV_FOO_OVER_UDP_ENCAP_GUE = FOU_ENCAP_GUE, + _NETDEV_FOO_OVER_UDP_ENCAP_MAX, + _NETDEV_FOO_OVER_UDP_ENCAP_INVALID = -EINVAL, +} FooOverUDPEncapType; + +typedef struct FouTunnel { + NetDev meta; + + uint8_t fou_protocol; + + uint16_t port; + uint16_t peer_port; + + int local_family; + int peer_family; + + FooOverUDPEncapType fou_encap_type; + union in_addr_union local; + union in_addr_union peer; +} FouTunnel; + +DEFINE_NETDEV_CAST(FOU, FouTunnel); +extern const NetDevVTable foutnl_vtable; + +const char *fou_encap_type_to_string(FooOverUDPEncapType d) _const_; +FooOverUDPEncapType fou_encap_type_from_string(const char *d) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_fou_encap_type); +CONFIG_PARSER_PROTOTYPE(config_parse_ip_protocol); +CONFIG_PARSER_PROTOTYPE(config_parse_fou_tunnel_address); diff --git a/src/network/netdev/geneve.c b/src/network/netdev/geneve.c new file mode 100644 index 0000000..bc655ec --- /dev/null +++ b/src/network/netdev/geneve.c @@ -0,0 +1,276 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "extract-word.h" +#include "geneve.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "parse-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" + +#define GENEVE_FLOW_LABEL_MAX_MASK 0xFFFFFU +#define DEFAULT_GENEVE_DESTINATION_PORT 6081 + +static const char* const geneve_df_table[_NETDEV_GENEVE_DF_MAX] = { + [NETDEV_GENEVE_DF_NO] = "no", + [NETDEV_GENEVE_DF_YES] = "yes", + [NETDEV_GENEVE_DF_INHERIT] = "inherit", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(geneve_df, GeneveDF, NETDEV_GENEVE_DF_YES); +DEFINE_CONFIG_PARSE_ENUM(config_parse_geneve_df, geneve_df, GeneveDF, "Failed to parse Geneve IPDoNotFragment= setting"); + +static int netdev_geneve_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(m); + + Geneve *v = GENEVE(netdev); + int r; + + if (v->id <= GENEVE_VID_MAX) { + r = sd_netlink_message_append_u32(m, IFLA_GENEVE_ID, v->id); + if (r < 0) + return r; + } + + if (in_addr_is_set(v->remote_family, &v->remote)) { + if (v->remote_family == AF_INET) + r = sd_netlink_message_append_in_addr(m, IFLA_GENEVE_REMOTE, &v->remote.in); + else + r = sd_netlink_message_append_in6_addr(m, IFLA_GENEVE_REMOTE6, &v->remote.in6); + if (r < 0) + return r; + } + + if (v->inherit) { + r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TTL_INHERIT, 1); + if (r < 0) + return r; + } else { + r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TTL, v->ttl); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u8(m, IFLA_GENEVE_TOS, v->tos); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_CSUM, v->udpcsum); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_TX, v->udp6zerocsumtx); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_RX, v->udp6zerocsumrx); + if (r < 0) + return r; + + if (v->dest_port != DEFAULT_GENEVE_DESTINATION_PORT) { + r = sd_netlink_message_append_u16(m, IFLA_GENEVE_PORT, htobe16(v->dest_port)); + if (r < 0) + return r; + } + + if (v->flow_label > 0) { + r = sd_netlink_message_append_u32(m, IFLA_GENEVE_LABEL, htobe32(v->flow_label)); + if (r < 0) + return r; + } + + if (v->inherit_inner_protocol) { + r = sd_netlink_message_append_flag(m, IFLA_GENEVE_INNER_PROTO_INHERIT); + if (r < 0) + return r; + } + + if (v->geneve_df != _NETDEV_GENEVE_DF_INVALID) { + r = sd_netlink_message_append_u8(m, IFLA_GENEVE_DF, v->geneve_df); + if (r < 0) + return r; + } + + return 0; +} + +int config_parse_geneve_vni( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + Geneve *v = ASSERT_PTR(userdata); + + return config_parse_uint32_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, GENEVE_VID_MAX, true, + &v->id); +} + +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) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + Geneve *v = ASSERT_PTR(userdata); + union in_addr_union *addr = data, buffer; + int r, f; + + 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) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + Geneve *v = ASSERT_PTR(userdata); + uint32_t f; + int r; + + 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) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + Geneve *v = ASSERT_PTR(userdata); + int r; + + if (streq(rvalue, "inherit")) { + v->inherit = true; + v->ttl = 0; /* unset the unused ttl field for clarity */ + return 0; + } + + r = config_parse_uint8_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, UINT8_MAX, true, + &v->ttl); + if (r <= 0) + return r; + v->inherit = false; + return 0; +} + +static int netdev_geneve_verify(NetDev *netdev, const char *filename) { + assert(filename); + + Geneve *v = GENEVE(netdev); + + 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 = GENEVE(netdev); + + v->id = GENEVE_VID_MAX + 1; + v->geneve_df = _NETDEV_GENEVE_DF_INVALID; + v->dest_port = DEFAULT_GENEVE_DESTINATION_PORT; + v->udpcsum = false; + v->udp6zerocsumtx = false; + v->udp6zerocsumrx = false; +} + +const NetDevVTable geneve_vtable = { + .object_size = sizeof(Geneve), + .init = geneve_init, + .sections = NETDEV_COMMON_SECTIONS "GENEVE\0", + .fill_message_create = netdev_geneve_fill_message_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .config_verify = netdev_geneve_verify, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/geneve.h b/src/network/netdev/geneve.h new file mode 100644 index 0000000..3cbf694 --- /dev/null +++ b/src/network/netdev/geneve.h @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct Geneve Geneve; + +#include "in-addr-util.h" +#include "netdev.h" +#include "networkd-network.h" + +#define GENEVE_VID_MAX (1u << 24) - 1 + +typedef enum GeneveDF { + NETDEV_GENEVE_DF_NO = GENEVE_DF_UNSET, + NETDEV_GENEVE_DF_YES = GENEVE_DF_SET, + NETDEV_GENEVE_DF_INHERIT = GENEVE_DF_INHERIT, + _NETDEV_GENEVE_DF_MAX, + _NETDEV_GENEVE_DF_INVALID = -EINVAL, +} GeneveDF; + +struct Geneve { + NetDev meta; + + uint32_t id; + uint32_t flow_label; + + int remote_family; + + uint8_t tos; + uint8_t ttl; + + uint16_t dest_port; + + bool udpcsum; + bool udp6zerocsumtx; + bool udp6zerocsumrx; + bool inherit; + + GeneveDF geneve_df; + union in_addr_union remote; + + bool inherit_inner_protocol; +}; + +DEFINE_NETDEV_CAST(GENEVE, Geneve); +extern const NetDevVTable geneve_vtable; + +const char *geneve_df_to_string(GeneveDF d) _const_; +GeneveDF geneve_df_from_string(const char *d) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_geneve_vni); +CONFIG_PARSER_PROTOTYPE(config_parse_geneve_address); +CONFIG_PARSER_PROTOTYPE(config_parse_geneve_flow_label); +CONFIG_PARSER_PROTOTYPE(config_parse_geneve_df); +CONFIG_PARSER_PROTOTYPE(config_parse_geneve_ttl); diff --git a/src/network/netdev/ifb.c b/src/network/netdev/ifb.c new file mode 100644 index 0000000..d7ff44c --- /dev/null +++ b/src/network/netdev/ifb.c @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include <linux/if_arp.h> + +#include "ifb.h" + +const NetDevVTable ifb_vtable = { + .object_size = sizeof(IntermediateFunctionalBlock), + .sections = NETDEV_COMMON_SECTIONS, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/ifb.h b/src/network/netdev/ifb.h new file mode 100644 index 0000000..badfb4a --- /dev/null +++ b/src/network/netdev/ifb.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#pragma once + +#include "netdev.h" + +typedef struct IntermediateFunctionalBlock { + NetDev meta; +} IntermediateFunctionalBlock; + +DEFINE_NETDEV_CAST(IFB, IntermediateFunctionalBlock); +extern const NetDevVTable ifb_vtable; diff --git a/src/network/netdev/ipoib.c b/src/network/netdev/ipoib.c new file mode 100644 index 0000000..d5fe299 --- /dev/null +++ b/src/network/netdev/ipoib.c @@ -0,0 +1,150 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if_arp.h> +#include <linux/if_link.h> + +#include "ipoib.h" +#include "networkd-network.h" +#include "parse-util.h" +#include "string-table.h" + +assert_cc((int) IP_OVER_INFINIBAND_MODE_DATAGRAM == (int) IPOIB_MODE_DATAGRAM); +assert_cc((int) IP_OVER_INFINIBAND_MODE_CONNECTED == (int) IPOIB_MODE_CONNECTED); + +static void netdev_ipoib_init(NetDev *netdev) { + IPoIB *ipoib = IPOIB(netdev); + + ipoib->mode = _IP_OVER_INFINIBAND_MODE_INVALID; + ipoib->umcast = -1; +} + +static int netdev_ipoib_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(link); + assert(m); + + IPoIB *ipoib = IPOIB(netdev); + int r; + + if (ipoib->pkey > 0) { + r = sd_netlink_message_append_u16(m, IFLA_IPOIB_PKEY, ipoib->pkey); + if (r < 0) + return r; + } + + if (ipoib->mode >= 0) { + r = sd_netlink_message_append_u16(m, IFLA_IPOIB_MODE, ipoib->mode); + if (r < 0) + return r; + } + + if (ipoib->umcast >= 0) { + r = sd_netlink_message_append_u16(m, IFLA_IPOIB_UMCAST, ipoib->umcast); + if (r < 0) + return r; + } + + return 0; +} + +int ipoib_set_netlink_message(Link *link, sd_netlink_message *m) { + int r; + + assert(link); + assert(link->network); + assert(m); + + r = sd_netlink_message_set_flags(m, NLM_F_REQUEST | NLM_F_ACK); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(m, IFLA_LINKINFO); + if (r < 0) + return r; + + r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, link->kind); + if (r < 0) + return r; + + if (link->network->ipoib_mode >= 0) { + r = sd_netlink_message_append_u16(m, IFLA_IPOIB_MODE, link->network->ipoib_mode); + if (r < 0) + return r; + } + + if (link->network->ipoib_umcast >= 0) { + r = sd_netlink_message_append_u16(m, IFLA_IPOIB_UMCAST, link->network->ipoib_umcast); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + return 0; +} + +static const char * const ipoib_mode_table[_IP_OVER_INFINIBAND_MODE_MAX] = { + [IP_OVER_INFINIBAND_MODE_DATAGRAM] = "datagram", + [IP_OVER_INFINIBAND_MODE_CONNECTED] = "connected", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(ipoib_mode, IPoIBMode); +DEFINE_CONFIG_PARSE_ENUM(config_parse_ipoib_mode, ipoib_mode, IPoIBMode, "Failed to parse IPoIB mode"); + +int config_parse_ipoib_pkey( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint16_t u, *pkey = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *pkey = 0; /* 0 means unset. */ + return 0; + } + + r = safe_atou16(rvalue, &u); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse IPoIB pkey '%s', ignoring assignment: %m", + rvalue); + return 0; + } + if (IN_SET(u, 0, 0x8000)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "IPoIB pkey cannot be 0 nor 0x8000, ignoring assignment: %s", + rvalue); + return 0; + } + + *pkey = u; + return 0; +} + + +const NetDevVTable ipoib_vtable = { + .object_size = sizeof(IPoIB), + .sections = NETDEV_COMMON_SECTIONS "IPoIB\0", + .init = netdev_ipoib_init, + .fill_message_create = netdev_ipoib_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .iftype = ARPHRD_INFINIBAND, + .generate_mac = true, +}; diff --git a/src/network/netdev/ipoib.h b/src/network/netdev/ipoib.h new file mode 100644 index 0000000..415d3b1 --- /dev/null +++ b/src/network/netdev/ipoib.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <errno.h> + +#include "conf-parser.h" +#include "netdev.h" + +typedef enum IPoIBMode { + IP_OVER_INFINIBAND_MODE_DATAGRAM, + IP_OVER_INFINIBAND_MODE_CONNECTED, + _IP_OVER_INFINIBAND_MODE_MAX, + _IP_OVER_INFINIBAND_MODE_INVALID = -EINVAL, +} IPoIBMode; + +typedef struct IPoIB { + NetDev meta; + + uint16_t pkey; + IPoIBMode mode; + int umcast; +} IPoIB; + +DEFINE_NETDEV_CAST(IPOIB, IPoIB); +extern const NetDevVTable ipoib_vtable; + +int ipoib_set_netlink_message(Link *link, sd_netlink_message *m); + +CONFIG_PARSER_PROTOTYPE(config_parse_ipoib_pkey); +CONFIG_PARSER_PROTOTYPE(config_parse_ipoib_mode); diff --git a/src/network/netdev/ipvlan.c b/src/network/netdev/ipvlan.c new file mode 100644 index 0000000..05d5d01 --- /dev/null +++ b/src/network/netdev/ipvlan.c @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> + +#include "conf-parser.h" +#include "ipvlan.h" +#include "ipvlan-util.h" +#include "networkd-link.h" +#include "string-util.h" + +DEFINE_CONFIG_PARSE_ENUM(config_parse_ipvlan_mode, ipvlan_mode, IPVlanMode, "Failed to parse ipvlan mode"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_ipvlan_flags, ipvlan_flags, IPVlanFlags, "Failed to parse ipvlan flags"); + +static int netdev_ipvlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *req) { + assert(netdev); + assert(link); + assert(netdev->ifname); + + IPVlan *m = netdev->kind == NETDEV_KIND_IPVLAN ? IPVLAN(netdev) : IPVTAP(netdev); + int r; + + if (m->mode != _NETDEV_IPVLAN_MODE_INVALID) { + r = sd_netlink_message_append_u16(req, IFLA_IPVLAN_MODE, m->mode); + if (r < 0) + return r; + } + + if (m->flags != _NETDEV_IPVLAN_FLAGS_INVALID) { + r = sd_netlink_message_append_u16(req, IFLA_IPVLAN_FLAGS, m->flags); + if (r < 0) + return r; + } + + return 0; +} + +static void ipvlan_init(NetDev *netdev) { + IPVlan *m = ASSERT_PTR(netdev)->kind == NETDEV_KIND_IPVLAN ? IPVLAN(netdev) : IPVTAP(netdev); + + m->mode = _NETDEV_IPVLAN_MODE_INVALID; + m->flags = _NETDEV_IPVLAN_FLAGS_INVALID; +} + +const NetDevVTable ipvlan_vtable = { + .object_size = sizeof(IPVlan), + .init = ipvlan_init, + .sections = NETDEV_COMMON_SECTIONS "IPVLAN\0", + .fill_message_create = netdev_ipvlan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; + +const NetDevVTable ipvtap_vtable = { + .object_size = sizeof(IPVlan), + .init = ipvlan_init, + .sections = NETDEV_COMMON_SECTIONS "IPVTAP\0", + .fill_message_create = netdev_ipvlan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; + +IPVlanMode link_get_ipvlan_mode(Link *link) { + assert(link); + + if (!link->netdev || link->netdev->kind != NETDEV_KIND_IPVLAN) + return _NETDEV_IPVLAN_MODE_INVALID; + + return IPVLAN(link->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..8b9406b --- /dev/null +++ b/src/network/netdev/l2tp-tunnel.c @@ -0,0 +1,825 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/l2tp.h> +#include <linux/genetlink.h> + +#include "conf-parser.h" +#include "hashmap.h" +#include "l2tp-tunnel.h" +#include "netlink-util.h" +#include "networkd-address.h" +#include "networkd-manager.h" +#include "networkd-route-util.h" +#include "parse-util.h" +#include "socket-util.h" +#include "string-table.h" +#include "string-util.h" + +static const char* const l2tp_l2spec_type_table[_NETDEV_L2TP_L2SPECTYPE_MAX] = { + [NETDEV_L2TP_L2SPECTYPE_NONE] = "none", + [NETDEV_L2TP_L2SPECTYPE_DEFAULT] = "default", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(l2tp_l2spec_type, L2tpL2specType); + +static const char* const l2tp_encap_type_table[_NETDEV_L2TP_ENCAPTYPE_MAX] = { + [NETDEV_L2TP_ENCAPTYPE_UDP] = "udp", + [NETDEV_L2TP_ENCAPTYPE_IP] = "ip", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(l2tp_encap_type, L2tpEncapType); +DEFINE_CONFIG_PARSE_ENUM(config_parse_l2tp_encap_type, l2tp_encap_type, L2tpEncapType, "Failed to parse L2TP Encapsulation Type"); + +static const char* const l2tp_local_address_type_table[_NETDEV_L2TP_LOCAL_ADDRESS_MAX] = { + [NETDEV_L2TP_LOCAL_ADDRESS_AUTO] = "auto", + [NETDEV_L2TP_LOCAL_ADDRESS_STATIC] = "static", + [NETDEV_L2TP_LOCAL_ADDRESS_DYNAMIC] = "dynamic", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(l2tp_local_address_type, L2tpLocalAddressType); + +static L2tpSession* l2tp_session_free(L2tpSession *s) { + if (!s) + return NULL; + + if (s->tunnel && s->section) + ordered_hashmap_remove(s->tunnel->sessions_by_section, s->section); + + config_section_free(s->section); + free(s->name); + return mfree(s); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(L2tpSession, l2tp_session_free); + +static int l2tp_session_new_static(L2tpTunnel *t, const char *filename, unsigned section_line, L2tpSession **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(l2tp_session_freep) L2tpSession *s = NULL; + int r; + + assert(t); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + s = ordered_hashmap_get(t->sessions_by_section, n); + if (s) { + *ret = TAKE_PTR(s); + return 0; + } + + s = new(L2tpSession, 1); + if (!s) + return -ENOMEM; + + *s = (L2tpSession) { + .l2tp_l2spec_type = NETDEV_L2TP_L2SPECTYPE_DEFAULT, + .tunnel = t, + .section = TAKE_PTR(n), + }; + + r = ordered_hashmap_ensure_put(&t->sessions_by_section, &config_section_hash_ops, s->section, s); + if (r < 0) + return r; + + *ret = TAKE_PTR(s); + return 0; +} + +static int netdev_l2tp_create_message_tunnel(NetDev *netdev, union in_addr_union *local_address, sd_netlink_message **ret) { + assert(local_address); + + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + uint16_t encap_type; + L2tpTunnel *t = L2TP(netdev); + int r; + + r = sd_genl_message_new(netdev->manager->genl, L2TP_GENL_NAME, L2TP_CMD_TUNNEL_CREATE, &m); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, L2TP_ATTR_CONN_ID, t->tunnel_id); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_CONN_ID, t->peer_tunnel_id); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, L2TP_ATTR_PROTO_VERSION, 3); + if (r < 0) + return r; + + switch (t->l2tp_encap_type) { + case NETDEV_L2TP_ENCAPTYPE_IP: + encap_type = L2TP_ENCAPTYPE_IP; + break; + case NETDEV_L2TP_ENCAPTYPE_UDP: + default: + encap_type = L2TP_ENCAPTYPE_UDP; + break; + } + + r = sd_netlink_message_append_u16(m, L2TP_ATTR_ENCAP_TYPE, encap_type); + if (r < 0) + return r; + + if (t->family == AF_INET) { + r = sd_netlink_message_append_in_addr(m, L2TP_ATTR_IP_SADDR, &local_address->in); + if (r < 0) + return r; + + r = sd_netlink_message_append_in_addr(m, L2TP_ATTR_IP_DADDR, &t->remote.in); + if (r < 0) + return r; + } else { + r = sd_netlink_message_append_in6_addr(m, L2TP_ATTR_IP6_SADDR, &local_address->in6); + if (r < 0) + return r; + + r = sd_netlink_message_append_in6_addr(m, L2TP_ATTR_IP6_DADDR, &t->remote.in6); + if (r < 0) + return r; + } + + if (encap_type == L2TP_ENCAPTYPE_UDP) { + r = sd_netlink_message_append_u16(m, L2TP_ATTR_UDP_SPORT, t->l2tp_udp_sport); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, L2TP_ATTR_UDP_DPORT, t->l2tp_udp_dport); + if (r < 0) + return r; + + if (t->udp_csum) { + r = sd_netlink_message_append_u8(m, L2TP_ATTR_UDP_CSUM, t->udp_csum); + if (r < 0) + return r; + } + + if (t->udp6_csum_tx) { + r = sd_netlink_message_append_flag(m, L2TP_ATTR_UDP_ZERO_CSUM6_TX); + if (r < 0) + return r; + } + + if (t->udp6_csum_rx) { + r = sd_netlink_message_append_flag(m, L2TP_ATTR_UDP_ZERO_CSUM6_RX); + if (r < 0) + return r; + } + } + + *ret = TAKE_PTR(m); + + return 0; +} + +static int netdev_l2tp_create_message_session(NetDev *netdev, L2tpSession *session, sd_netlink_message **ret) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + uint16_t l2_spec_len; + uint8_t l2_spec_type; + int r; + + assert(netdev); + assert(session); + assert(session->tunnel); + + r = sd_genl_message_new(netdev->manager->genl, L2TP_GENL_NAME, L2TP_CMD_SESSION_CREATE, &m); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, L2TP_ATTR_CONN_ID, session->tunnel->tunnel_id); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_CONN_ID, session->tunnel->peer_tunnel_id); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, L2TP_ATTR_SESSION_ID, session->session_id); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, L2TP_ATTR_PEER_SESSION_ID, session->peer_session_id); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, L2TP_ATTR_PW_TYPE, L2TP_PWTYPE_ETH); + if (r < 0) + return r; + + switch (session->l2tp_l2spec_type) { + case NETDEV_L2TP_L2SPECTYPE_NONE: + l2_spec_type = L2TP_L2SPECTYPE_NONE; + l2_spec_len = 0; + break; + case NETDEV_L2TP_L2SPECTYPE_DEFAULT: + default: + l2_spec_type = L2TP_L2SPECTYPE_DEFAULT; + l2_spec_len = 4; + break; + } + + r = sd_netlink_message_append_u8(m, L2TP_ATTR_L2SPEC_TYPE, l2_spec_type); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, L2TP_ATTR_L2SPEC_LEN, l2_spec_len); + if (r < 0) + return r; + + r = sd_netlink_message_append_string(m, L2TP_ATTR_IFNAME, session->name); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + + return 0; +} + +static int link_get_l2tp_local_address(Link *link, L2tpTunnel *t, union in_addr_union *ret) { + Address *a; + + assert(link); + assert(t); + + SET_FOREACH(a, link->addresses) { + if (!address_is_ready(a)) + continue; + + if (a->family != t->family) + continue; + + if (in_addr_is_set(a->family, &a->in_addr_peer)) + continue; + + if (t->local_address_type == NETDEV_L2TP_LOCAL_ADDRESS_STATIC && + !FLAGS_SET(a->flags, IFA_F_PERMANENT)) + continue; + + if (t->local_address_type == NETDEV_L2TP_LOCAL_ADDRESS_DYNAMIC && + FLAGS_SET(a->flags, IFA_F_PERMANENT)) + continue; + + if (ret) + *ret = a->in_addr; + } + + return -ENOENT; +} + +static int l2tp_get_local_address(NetDev *netdev, union in_addr_union *ret) { + Link *link = NULL; + L2tpTunnel *t = L2TP(netdev); + Address *a = NULL; + int r; + + assert(netdev->manager); + + if (t->local_ifname) { + r = link_get_by_name(netdev->manager, t->local_ifname, &link); + if (r < 0) + return r; + + if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) + return -EBUSY; + } + + if (netdev->manager->manage_foreign_routes) { + /* First, check if the remote address is accessible. */ + if (link) + r = link_address_is_reachable(link, t->family, &t->remote, &t->local, &a); + else + r = manager_address_is_reachable(netdev->manager, t->family, &t->remote, &t->local, &a); + if (r < 0) + return r; + } + + if (in_addr_is_set(t->family, &t->local)) { + /* local address is explicitly specified. */ + + if (!a) { + if (link) + r = link_get_address(link, t->family, &t->local, 0, &a); + else + r = manager_get_address(netdev->manager, t->family, &t->local, 0, &a); + if (r < 0) + return r; + + if (!address_is_ready(a)) + return -EBUSY; + } + + if (ret) + *ret = a->in_addr; + + return 0; + } + + if (a) { + if (t->local_address_type == NETDEV_L2TP_LOCAL_ADDRESS_STATIC && + !FLAGS_SET(a->flags, IFA_F_PERMANENT)) + return -EINVAL; + + if (t->local_address_type == NETDEV_L2TP_LOCAL_ADDRESS_DYNAMIC && + FLAGS_SET(a->flags, IFA_F_PERMANENT)) + return -EINVAL; + + if (ret) + *ret = a->in_addr; + + return 0; + } + + if (link) + return link_get_l2tp_local_address(link, t, ret); + + HASHMAP_FOREACH(link, netdev->manager->links_by_index) { + if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) + continue; + + if (link_get_l2tp_local_address(link, t, ret) >= 0) + return 0; + } + + return -ENOENT; +} + +static void l2tp_session_destroy_callback(L2tpSession *session) { + if (!session) + return; + + netdev_unref(NETDEV(session->tunnel)); +} + +static int l2tp_create_session_handler(sd_netlink *rtnl, sd_netlink_message *m, L2tpSession *session) { + NetDev *netdev; + int r; + + assert(session); + assert(session->tunnel); + + netdev = NETDEV(session->tunnel); + + r = sd_netlink_message_get_errno(m); + if (r == -EEXIST) + log_netdev_info(netdev, "L2TP session %s exists, using existing without changing its parameters", + session->name); + else if (r < 0) { + log_netdev_warning_errno(netdev, r, "L2TP session %s could not be created: %m", session->name); + return 1; + } + + log_netdev_debug(netdev, "L2TP session %s created", session->name); + return 1; +} + +static int l2tp_create_session(NetDev *netdev, L2tpSession *session) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *n = NULL; + int r; + + r = netdev_l2tp_create_message_session(netdev, session, &n); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m"); + + r = netlink_call_async(netdev->manager->genl, NULL, n, l2tp_create_session_handler, + l2tp_session_destroy_callback, session); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to create L2TP session %s: %m", session->name); + + netdev_ref(netdev); + return 0; +} + +static int l2tp_create_tunnel_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) { + L2tpSession *session; + L2tpTunnel *t = L2TP(netdev); + int r; + + assert(netdev->state != _NETDEV_STATE_INVALID); + + r = sd_netlink_message_get_errno(m); + if (r == -EEXIST) + log_netdev_info(netdev, "netdev exists, using existing without changing its parameters"); + else if (r < 0) { + log_netdev_warning_errno(netdev, r, "netdev could not be created: %m"); + netdev_enter_failed(netdev); + + return 1; + } + + log_netdev_debug(netdev, "L2TP tunnel is created"); + + ORDERED_HASHMAP_FOREACH(session, t->sessions_by_section) + (void) l2tp_create_session(netdev, session); + + return 1; +} + +static int l2tp_create_tunnel(NetDev *netdev) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + union in_addr_union local_address; + L2tpTunnel *t = L2TP(netdev); + int r; + + r = l2tp_get_local_address(netdev, &local_address); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not find local address."); + + if (t->local_address_type >= 0 && DEBUG_LOGGING) + log_netdev_debug(netdev, "Local address %s acquired.", + IN_ADDR_TO_STRING(t->family, &local_address)); + + r = netdev_l2tp_create_message_tunnel(netdev, &local_address, &m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m"); + + r = netlink_call_async(netdev->manager->genl, NULL, m, l2tp_create_tunnel_handler, + netdev_destroy_callback, netdev); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to create L2TP tunnel: %m"); + + netdev_ref(netdev); + + return 0; +} + +static int netdev_l2tp_is_ready_to_create(NetDev *netdev, Link *link) { + return l2tp_get_local_address(netdev, NULL) >= 0; +} + +int config_parse_l2tp_tunnel_local_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *addr_or_type = NULL, *ifname = NULL; + L2tpLocalAddressType type; + L2tpTunnel *t = ASSERT_PTR(userdata); + const char *p = ASSERT_PTR(rvalue); + union in_addr_union a; + int r, f; + + assert(filename); + assert(lvalue); + + if (isempty(rvalue)) { + t->local_ifname = mfree(t->local_ifname); + t->local_address_type = NETDEV_L2TP_LOCAL_ADDRESS_AUTO; + t->local = IN_ADDR_NULL; + + if (!in_addr_is_set(t->family, &t->remote)) + /* If Remote= is not specified yet, then also clear family. */ + t->family = AF_UNSPEC; + + return 0; + } + + r = extract_first_word(&p, &addr_or_type, "@", 0); + if (r < 0) + return log_oom(); + if (r == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid L2TP Tunnel address specified in %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + if (!isempty(p)) { + if (!ifname_valid_full(p, IFNAME_VALID_ALTERNATIVE)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid interface name specified in %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + ifname = strdup(p); + if (!ifname) + return log_oom(); + } + + type = l2tp_local_address_type_from_string(addr_or_type); + if (type >= 0) { + free_and_replace(t->local_ifname, ifname); + t->local_address_type = type; + t->local = IN_ADDR_NULL; + + if (!in_addr_is_set(t->family, &t->remote)) + /* If Remote= is not specified yet, then also clear family. */ + t->family = AF_UNSPEC; + + return 0; + } + + r = in_addr_from_string_auto(addr_or_type, &f, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid L2TP Tunnel local address \"%s\" specified, ignoring assignment: %s", addr_or_type, rvalue); + return 0; + } + + if (in_addr_is_null(f, &a)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "L2TP Tunnel local address cannot be null, ignoring assignment: %s", rvalue); + return 0; + } + + if (t->family != AF_UNSPEC && t->family != f) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Address family does not match the previous assignment, ignoring assignment: %s", rvalue); + return 0; + } + + t->family = f; + t->local = a; + free_and_replace(t->local_ifname, ifname); + t->local_address_type = _NETDEV_L2TP_LOCAL_ADDRESS_INVALID; + return 0; +} + +int config_parse_l2tp_tunnel_remote_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + L2tpTunnel *t = ASSERT_PTR(userdata); + union in_addr_union a; + int r, f; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + t->remote = IN_ADDR_NULL; + + if (!in_addr_is_set(t->family, &t->local)) + /* If Local= is not specified yet, then also clear family. */ + t->family = AF_UNSPEC; + + return 0; + } + + r = in_addr_from_string_auto(rvalue, &f, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid L2TP Tunnel remote address specified, ignoring assignment: %s", rvalue); + return 0; + } + + if (in_addr_is_null(f, &a)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "L2TP Tunnel remote address cannot be null, ignoring assignment: %s", rvalue); + return 0; + } + + if (t->family != AF_UNSPEC && t->family != f) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Address family does not match the previous assignment, ignoring assignment: %s", rvalue); + return 0; + } + + t->family = f; + t->remote = a; + return 0; +} + +int config_parse_l2tp_tunnel_id( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + + uint32_t *id = ASSERT_PTR(data); + + return config_parse_uint32_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 1, UINT32_MAX, true, + id); +} + +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) { + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + L2tpTunnel *t = ASSERT_PTR(userdata); + _cleanup_(l2tp_session_free_or_set_invalidp) L2tpSession *session = NULL; + int r; + + r = l2tp_session_new_static(t, filename, section_line, &session); + if (r < 0) + return log_oom(); + + uint32_t *id = streq(lvalue, "SessionId") ? &session->session_id : &session->peer_session_id; + + r = config_parse_uint32_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 1, UINT32_MAX, true, + id); + if (r <= 0) + return r; + TAKE_PTR(session); + return 0; +} + +int config_parse_l2tp_session_l2spec( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(l2tp_session_free_or_set_invalidp) L2tpSession *session = NULL; + L2tpTunnel *t = userdata; + L2tpL2specType spec; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = l2tp_session_new_static(t, filename, section_line, &session); + if (r < 0) + return log_oom(); + + spec = l2tp_l2spec_type_from_string(rvalue); + if (spec < 0) { + log_syntax(unit, LOG_WARNING, filename, line, spec, + "Failed to parse layer2 specific header type. Ignoring assignment: %s", rvalue); + return 0; + } + + session->l2tp_l2spec_type = spec; + + session = NULL; + return 0; +} + +int config_parse_l2tp_session_name( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(l2tp_session_free_or_set_invalidp) L2tpSession *session = NULL; + L2tpTunnel *t = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = l2tp_session_new_static(t, filename, section_line, &session); + if (r < 0) + return log_oom(); + + if (!ifname_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Failed to parse L2TP tunnel session name. Ignoring assignment: %s", rvalue); + return 0; + } + + r = free_and_strdup(&session->name, rvalue); + if (r < 0) + return log_oom(); + + session = NULL; + return 0; +} + +static void l2tp_tunnel_init(NetDev *netdev) { + L2tpTunnel *t = L2TP(netdev); + + 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) { + assert(filename); + + L2tpTunnel *t = L2TP(netdev); + L2tpSession *session; + + if (!IN_SET(t->family, AF_INET, AF_INET6)) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: L2TP tunnel with invalid address family configured. Ignoring", + filename); + + if (!in_addr_is_set(t->family, &t->remote)) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: L2TP tunnel without a remote address configured. Ignoring", + filename); + + if (t->tunnel_id == 0 || t->peer_tunnel_id == 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: L2TP tunnel without tunnel IDs configured. Ignoring", + filename); + + ORDERED_HASHMAP_FOREACH(session, t->sessions_by_section) + if (l2tp_session_verify(session) < 0) + l2tp_session_free(session); + + return 0; +} + +static void l2tp_tunnel_done(NetDev *netdev) { + L2tpTunnel *t = L2TP(netdev); + + ordered_hashmap_free_with_destructor(t->sessions_by_section, l2tp_session_free); + free(t->local_ifname); +} + +const NetDevVTable l2tptnl_vtable = { + .object_size = sizeof(L2tpTunnel), + .init = l2tp_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "L2TP\0L2TPSession\0", + .create = l2tp_create_tunnel, + .done = l2tp_tunnel_done, + .create_type = NETDEV_CREATE_INDEPENDENT, + .is_ready_to_create = netdev_l2tp_is_ready_to_create, + .config_verify = netdev_l2tp_tunnel_verify, +}; diff --git a/src/network/netdev/l2tp-tunnel.h b/src/network/netdev/l2tp-tunnel.h new file mode 100644 index 0000000..6028b35 --- /dev/null +++ b/src/network/netdev/l2tp-tunnel.h @@ -0,0 +1,80 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <netinet/in.h> +#include <linux/l2tp.h> + +#include "in-addr-util.h" +#include "netdev.h" +#include "networkd-util.h" + +typedef enum L2tpL2specType { + NETDEV_L2TP_L2SPECTYPE_NONE = L2TP_L2SPECTYPE_NONE, + NETDEV_L2TP_L2SPECTYPE_DEFAULT = L2TP_L2SPECTYPE_DEFAULT, + _NETDEV_L2TP_L2SPECTYPE_MAX, + _NETDEV_L2TP_L2SPECTYPE_INVALID = -EINVAL, +} L2tpL2specType; + +typedef enum L2tpEncapType { + NETDEV_L2TP_ENCAPTYPE_UDP = L2TP_ENCAPTYPE_UDP, + NETDEV_L2TP_ENCAPTYPE_IP = L2TP_ENCAPTYPE_IP, + _NETDEV_L2TP_ENCAPTYPE_MAX, + _NETDEV_L2TP_ENCAPTYPE_INVALID = -EINVAL, +} L2tpEncapType; + +typedef enum L2tpLocalAddressType { + NETDEV_L2TP_LOCAL_ADDRESS_AUTO, + NETDEV_L2TP_LOCAL_ADDRESS_STATIC, + NETDEV_L2TP_LOCAL_ADDRESS_DYNAMIC, + _NETDEV_L2TP_LOCAL_ADDRESS_MAX, + _NETDEV_L2TP_LOCAL_ADDRESS_INVALID = -EINVAL, +} L2tpLocalAddressType; + +typedef struct L2tpTunnel L2tpTunnel; + +typedef struct L2tpSession { + L2tpTunnel *tunnel; + ConfigSection *section; + + char *name; + + uint32_t session_id; + uint32_t peer_session_id; + L2tpL2specType l2tp_l2spec_type; +} L2tpSession; + +struct L2tpTunnel { + NetDev meta; + + uint16_t l2tp_udp_sport; + uint16_t l2tp_udp_dport; + + uint32_t tunnel_id; + uint32_t peer_tunnel_id; + + int family; + + bool udp_csum; + bool udp6_csum_rx; + bool udp6_csum_tx; + + char *local_ifname; + L2tpLocalAddressType local_address_type; + union in_addr_union local; + union in_addr_union remote; + + L2tpEncapType l2tp_encap_type; + + OrderedHashmap *sessions_by_section; +}; + +DEFINE_NETDEV_CAST(L2TP, L2tpTunnel); +extern const NetDevVTable l2tptnl_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_tunnel_local_address); +CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_tunnel_remote_address); +CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_tunnel_id); +CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_encap_type); +CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_session_l2spec); +CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_session_id); +CONFIG_PARSER_PROTOTYPE(config_parse_l2tp_session_name); diff --git a/src/network/netdev/macsec.c b/src/network/netdev/macsec.c new file mode 100644 index 0000000..17d6ace --- /dev/null +++ b/src/network/netdev/macsec.c @@ -0,0 +1,1204 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/if_arp.h> +#include <linux/if_ether.h> +#include <linux/if_macsec.h> +#include <linux/genetlink.h> + +#include "conf-parser.h" +#include "fileio.h" +#include "hashmap.h" +#include "hexdecoct.h" +#include "macsec.h" +#include "memory-util.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "parse-helpers.h" +#include "socket-util.h" +#include "string-table.h" +#include "string-util.h" + +static void security_association_clear(SecurityAssociation *sa) { + if (!sa) + return; + + explicit_bzero_safe(sa->key, sa->key_len); + free(sa->key); + free(sa->key_file); +} + +static void security_association_init(SecurityAssociation *sa) { + assert(sa); + + sa->activate = -1; + sa->use_for_encoding = -1; +} + +static ReceiveAssociation* macsec_receive_association_free(ReceiveAssociation *c) { + if (!c) + return NULL; + + if (c->macsec && c->section) + ordered_hashmap_remove(c->macsec->receive_associations_by_section, c->section); + + config_section_free(c->section); + security_association_clear(&c->sa); + + return mfree(c); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(ReceiveAssociation, macsec_receive_association_free); + +static int macsec_receive_association_new_static(MACsec *s, const char *filename, unsigned section_line, ReceiveAssociation **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(macsec_receive_association_freep) ReceiveAssociation *c = NULL; + int r; + + assert(s); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + c = ordered_hashmap_get(s->receive_associations_by_section, n); + if (c) { + *ret = TAKE_PTR(c); + return 0; + } + + c = new(ReceiveAssociation, 1); + if (!c) + return -ENOMEM; + + *c = (ReceiveAssociation) { + .macsec = s, + .section = TAKE_PTR(n), + }; + + security_association_init(&c->sa); + + r = ordered_hashmap_ensure_put(&s->receive_associations_by_section, &config_section_hash_ops, c->section, c); + if (r < 0) + return r; + + *ret = TAKE_PTR(c); + + return 0; +} + +static ReceiveChannel* macsec_receive_channel_free(ReceiveChannel *c) { + if (!c) + return NULL; + + if (c->macsec) { + if (c->sci.as_uint64 > 0) + ordered_hashmap_remove_value(c->macsec->receive_channels, &c->sci.as_uint64, c); + + if (c->section) + ordered_hashmap_remove(c->macsec->receive_channels_by_section, c->section); + } + + config_section_free(c->section); + + return mfree(c); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(ReceiveChannel, macsec_receive_channel_free); + +static int macsec_receive_channel_new(MACsec *s, uint64_t sci, ReceiveChannel **ret) { + ReceiveChannel *c; + + assert(s); + + c = new(ReceiveChannel, 1); + if (!c) + return -ENOMEM; + + *c = (ReceiveChannel) { + .macsec = s, + .sci.as_uint64 = sci, + }; + + *ret = c; + return 0; +} + +static int macsec_receive_channel_new_static(MACsec *s, const char *filename, unsigned section_line, ReceiveChannel **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(macsec_receive_channel_freep) ReceiveChannel *c = NULL; + int r; + + assert(s); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + c = ordered_hashmap_get(s->receive_channels_by_section, n); + if (c) { + *ret = TAKE_PTR(c); + return 0; + } + + r = macsec_receive_channel_new(s, 0, &c); + if (r < 0) + return r; + + c->section = TAKE_PTR(n); + + r = ordered_hashmap_ensure_put(&s->receive_channels_by_section, &config_section_hash_ops, c->section, c); + if (r < 0) + return r; + + *ret = TAKE_PTR(c); + + return 0; +} + +static TransmitAssociation* macsec_transmit_association_free(TransmitAssociation *a) { + if (!a) + return NULL; + + if (a->macsec && a->section) + ordered_hashmap_remove(a->macsec->transmit_associations_by_section, a->section); + + config_section_free(a->section); + security_association_clear(&a->sa); + + return mfree(a); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(TransmitAssociation, macsec_transmit_association_free); + +static int macsec_transmit_association_new_static(MACsec *s, const char *filename, unsigned section_line, TransmitAssociation **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(macsec_transmit_association_freep) TransmitAssociation *a = NULL; + int r; + + assert(s); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + a = ordered_hashmap_get(s->transmit_associations_by_section, n); + if (a) { + *ret = TAKE_PTR(a); + return 0; + } + + a = new(TransmitAssociation, 1); + if (!a) + return -ENOMEM; + + *a = (TransmitAssociation) { + .macsec = s, + .section = TAKE_PTR(n), + }; + + security_association_init(&a->sa); + + r = ordered_hashmap_ensure_put(&s->transmit_associations_by_section, &config_section_hash_ops, a->section, a); + if (r < 0) + return r; + + *ret = TAKE_PTR(a); + + return 0; +} + +static int netdev_macsec_create_message(NetDev *netdev, int command, sd_netlink_message **ret) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(netdev); + assert(netdev->ifindex > 0); + + r = sd_genl_message_new(netdev->manager->genl, MACSEC_GENL_NAME, command, &m); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, MACSEC_ATTR_IFINDEX, netdev->ifindex); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + + return 0; +} + +static int netdev_macsec_fill_message_sci(NetDev *netdev, MACsecSCI *sci, sd_netlink_message *m) { + int r; + + assert(netdev); + assert(m); + assert(sci); + + r = sd_netlink_message_open_container(m, MACSEC_ATTR_RXSC_CONFIG); + if (r < 0) + return r; + + r = sd_netlink_message_append_u64(m, MACSEC_RXSC_ATTR_SCI, sci->as_uint64); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + return 0; +} + +static int netdev_macsec_fill_message_sa(NetDev *netdev, SecurityAssociation *a, sd_netlink_message *m) { + int r; + + assert(netdev); + assert(a); + assert(m); + + r = sd_netlink_message_open_container(m, MACSEC_ATTR_SA_CONFIG); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, MACSEC_SA_ATTR_AN, a->association_number); + if (r < 0) + return r; + + if (a->packet_number > 0) { + r = sd_netlink_message_append_u32(m, MACSEC_SA_ATTR_PN, a->packet_number); + if (r < 0) + return r; + } + + if (a->key_len > 0) { + r = sd_netlink_message_append_data(m, MACSEC_SA_ATTR_KEYID, a->key_id, MACSEC_KEYID_LEN); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(m, MACSEC_SA_ATTR_KEY, a->key, a->key_len); + if (r < 0) + return r; + } + + if (a->activate >= 0) { + r = sd_netlink_message_append_u8(m, MACSEC_SA_ATTR_ACTIVE, a->activate); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + return 0; +} + +static int macsec_receive_association_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) { + int r; + + assert(netdev); + assert(netdev->state != _NETDEV_STATE_INVALID); + + r = sd_netlink_message_get_errno(m); + if (r == -EEXIST) + log_netdev_info(netdev, + "MACsec receive secure association exists, using it without changing parameters"); + else if (r < 0) { + log_netdev_warning_errno(netdev, r, + "Failed to add receive secure association: %m"); + netdev_enter_failed(netdev); + + return 1; + } + + log_netdev_debug(netdev, "Receive secure association is configured"); + + return 1; +} + +static int netdev_macsec_configure_receive_association(NetDev *netdev, ReceiveAssociation *a) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(netdev); + assert(a); + + r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_RXSA, &m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m"); + + r = netdev_macsec_fill_message_sa(netdev, &a->sa, m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m"); + + r = netdev_macsec_fill_message_sci(netdev, &a->sci, m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m"); + + r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_receive_association_handler, + netdev_destroy_callback, netdev); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to configure receive secure association: %m"); + + netdev_ref(netdev); + + return 0; +} + +static int macsec_receive_channel_handler(sd_netlink *rtnl, sd_netlink_message *m, ReceiveChannel *c) { + assert(c); + assert(c->macsec); + + NetDev *netdev = ASSERT_PTR(NETDEV(c->macsec)); + int r; + + assert(netdev->state != _NETDEV_STATE_INVALID); + + r = sd_netlink_message_get_errno(m); + if (r == -EEXIST) + log_netdev_debug(netdev, + "MACsec receive channel exists, using it without changing parameters"); + else if (r < 0) { + log_netdev_warning_errno(netdev, r, + "Failed to add receive secure channel: %m"); + netdev_enter_failed(netdev); + + return 1; + } + + log_netdev_debug(netdev, "Receive channel is configured"); + + for (unsigned i = 0; i < c->n_rxsa; i++) { + r = netdev_macsec_configure_receive_association(netdev, c->rxsa[i]); + if (r < 0) { + log_netdev_warning_errno(netdev, r, + "Failed to configure receive security association: %m"); + netdev_enter_failed(netdev); + return 1; + } + } + + return 1; +} + +static void receive_channel_destroy_callback(ReceiveChannel *c) { + assert(c); + assert(c->macsec); + + netdev_unref(NETDEV(c->macsec)); +} + +static int netdev_macsec_configure_receive_channel(NetDev *netdev, ReceiveChannel *c) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(netdev); + assert(c); + + r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_RXSC, &m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m"); + + r = netdev_macsec_fill_message_sci(netdev, &c->sci, m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m"); + + r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_receive_channel_handler, + receive_channel_destroy_callback, c); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to configure receive channel: %m"); + + netdev_ref(netdev); + + return 0; +} + +static int macsec_transmit_association_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) { + int r; + + assert(netdev); + assert(netdev->state != _NETDEV_STATE_INVALID); + + r = sd_netlink_message_get_errno(m); + if (r == -EEXIST) + log_netdev_info(netdev, + "MACsec transmit secure association exists, using it without changing parameters"); + else if (r < 0) { + log_netdev_warning_errno(netdev, r, + "Failed to add transmit secure association: %m"); + netdev_enter_failed(netdev); + + return 1; + } + + log_netdev_debug(netdev, "Transmit secure association is configured"); + + return 1; +} + +static int netdev_macsec_configure_transmit_association(NetDev *netdev, TransmitAssociation *a) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(netdev); + assert(a); + + r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_TXSA, &m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m"); + + r = netdev_macsec_fill_message_sa(netdev, &a->sa, m); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m"); + + r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_transmit_association_handler, + netdev_destroy_callback, netdev); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to configure transmit secure association: %m"); + + netdev_ref(netdev); + + return 0; +} + +static int netdev_macsec_configure(NetDev *netdev, Link *link) { + MACsec *s = MACSEC(netdev); + TransmitAssociation *a; + ReceiveChannel *c; + int r; + + 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) { + assert(m); + + MACsec *v = MACSEC(netdev); + int r; + + if (v->port > 0) { + r = sd_netlink_message_append_u16(m, IFLA_MACSEC_PORT, v->port); + if (r < 0) + return r; + } + + if (v->encrypt >= 0) { + r = sd_netlink_message_append_u8(m, IFLA_MACSEC_ENCRYPT, v->encrypt); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u8(m, IFLA_MACSEC_ENCODING_SA, v->encoding_an); + if (r < 0) + return r; + + return 0; +} + +int config_parse_macsec_port( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + MACsec *s = ASSERT_PTR(userdata); + _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL; + _cleanup_(macsec_receive_channel_free_or_set_invalidp) ReceiveChannel *c = NULL; + uint16_t port; + void *dest; + int r; + + /* 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) { + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + MACsec *s = ASSERT_PTR(userdata); + _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL; + _cleanup_(macsec_receive_channel_free_or_set_invalidp) ReceiveChannel *c = NULL; + int r; + + if (streq(section, "MACsecReceiveChannel")) + r = macsec_receive_channel_new_static(s, filename, section_line, &c); + else + r = macsec_receive_association_new_static(s, filename, section_line, &b); + if (r < 0) + return log_oom(); + + r = parse_ether_addr(rvalue, b ? &b->sci.mac : &c->sci.mac); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse MAC address for secure channel identifier. " + "Ignoring assignment: %s", rvalue); + return 0; + } + + TAKE_PTR(b); + TAKE_PTR(c); + + return 0; +} + +int config_parse_macsec_packet_number( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + MACsec *s = ASSERT_PTR(userdata); + _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL; + _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL; + uint32_t val, *dest; + int r; + + if (streq(section, "MACsecTransmitAssociation")) + r = macsec_transmit_association_new_static(s, filename, section_line, &a); + else + r = macsec_receive_association_new_static(s, filename, section_line, &b); + if (r < 0) + return log_oom(); + + dest = a ? &a->sa.packet_number : &b->sa.packet_number; + + r = safe_atou32(rvalue, &val); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse packet number. Ignoring assignment: %s", rvalue); + return 0; + } + if (streq(section, "MACsecTransmitAssociation") && val == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid packet number. Ignoring assignment: %s", rvalue); + return 0; + } + + *dest = val; + TAKE_PTR(a); + TAKE_PTR(b); + + return 0; +} + +int config_parse_macsec_key( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL; + _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL; + _cleanup_(erase_and_freep) void *p = NULL; + MACsec *s = userdata; + SecurityAssociation *dest; + size_t l; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + (void) warn_file_is_world_accessible(filename, NULL, unit, line); + + if (streq(section, "MACsecTransmitAssociation")) + r = macsec_transmit_association_new_static(s, filename, section_line, &a); + else + r = macsec_receive_association_new_static(s, filename, section_line, &b); + if (r < 0) + return log_oom(); + + dest = a ? &a->sa : &b->sa; + + r = unhexmem_full(rvalue, strlen(rvalue), true, &p, &l); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse key. Ignoring assignment: %m"); + return 0; + } + + if (l != 16) { + /* See DEFAULT_SAK_LEN in drivers/net/macsec.c */ + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid key length (%zu). Ignoring assignment", l); + return 0; + } + + explicit_bzero_safe(dest->key, dest->key_len); + free_and_replace(dest->key, p); + dest->key_len = l; + + TAKE_PTR(a); + TAKE_PTR(b); + + return 0; +} + +int config_parse_macsec_key_file( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL; + _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL; + _cleanup_free_ char *path = NULL; + MACsec *s = userdata; + char **dest; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + if (streq(section, "MACsecTransmitAssociation")) + r = macsec_transmit_association_new_static(s, filename, section_line, &a); + else + r = macsec_receive_association_new_static(s, filename, section_line, &b); + if (r < 0) + return log_oom(); + + dest = a ? &a->sa.key_file : &b->sa.key_file; + + if (isempty(rvalue)) { + *dest = mfree(*dest); + return 0; + } + + path = strdup(rvalue); + if (!path) + return log_oom(); + + if (path_simplify_and_warn(path, PATH_CHECK_ABSOLUTE, unit, filename, line, lvalue) < 0) + return 0; + + free_and_replace(*dest, path); + TAKE_PTR(a); + TAKE_PTR(b); + + return 0; +} + +int config_parse_macsec_key_id( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL; + _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL; + _cleanup_free_ void *p = NULL; + MACsec *s = userdata; + uint8_t *dest; + size_t l; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + if (streq(section, "MACsecTransmitAssociation")) + r = macsec_transmit_association_new_static(s, filename, section_line, &a); + else + r = macsec_receive_association_new_static(s, filename, section_line, &b); + if (r < 0) + return log_oom(); + + r = unhexmem(rvalue, strlen(rvalue), &p, &l); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse KeyId=%s, ignoring assignment: %m", rvalue); + return 0; + } + if (l > MACSEC_KEYID_LEN) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified KeyId= is larger then the allowed maximum (%zu > %i), ignoring: %s", + l, MACSEC_KEYID_LEN, rvalue); + return 0; + } + + dest = a ? a->sa.key_id : b->sa.key_id; + memcpy_safe(dest, p, l); + memzero(dest + l, MACSEC_KEYID_LEN - l); + + TAKE_PTR(a); + TAKE_PTR(b); + + return 0; +} + +int config_parse_macsec_sa_activate( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL; + _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL; + MACsec *s = userdata; + int *dest, 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; + + r = parse_tristate(rvalue, dest); + 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; + } + + 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_tristate(rvalue, &a->sa.use_for_encoding); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s= setting. Ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (a->sa.use_for_encoding > 0) + a->sa.activate = true; + + TAKE_PTR(a); + + return 0; +} + +static int macsec_read_key_file(NetDev *netdev, SecurityAssociation *sa) { + _cleanup_(erase_and_freep) uint8_t *key = NULL; + size_t key_len; + int r; + + assert(netdev); + assert(sa); + + if (!sa->key_file) + return 0; + + r = read_full_file_full( + AT_FDCWD, sa->key_file, UINT64_MAX, MACSEC_KEYID_LEN, + READ_FULL_FILE_SECURE | + READ_FULL_FILE_UNHEX | + READ_FULL_FILE_WARN_WORLD_READABLE | + READ_FULL_FILE_CONNECT_SOCKET | + READ_FULL_FILE_FAIL_WHEN_LARGER, + 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 != MACSEC_KEYID_LEN) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "Invalid key length (%zu bytes), ignoring: %m", key_len); + + explicit_bzero_safe(sa->key, sa->key_len); + free_and_replace(sa->key, key); + sa->key_len = key_len; + + return 0; +} + +static int macsec_receive_channel_verify(ReceiveChannel *c) { + NetDev *netdev; + int r; + + assert(c); + assert(c->macsec); + + netdev = NETDEV(c->macsec); + + if (section_is_invalid(c->section)) + return -EINVAL; + + if (ether_addr_is_null(&c->sci.mac)) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: MACsec receive channel without MAC address configured. " + "Ignoring [MACsecReceiveChannel] section from line %u", + c->section->filename, c->section->line); + + if (c->sci.port == 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: MACsec receive channel without port configured. " + "Ignoring [MACsecReceiveChannel] section from line %u", + c->section->filename, c->section->line); + + r = ordered_hashmap_ensure_put(&c->macsec->receive_channels, &uint64_hash_ops, &c->sci.as_uint64, c); + if (r == -ENOMEM) + return log_oom(); + if (r == -EEXIST) + return log_netdev_error_errno(netdev, r, + "%s: Multiple [MACsecReceiveChannel] sections have same SCI, " + "Ignoring [MACsecReceiveChannel] section from line %u", + c->section->filename, c->section->line); + if (r < 0) + return log_netdev_error_errno(netdev, r, + "%s: Failed to store [MACsecReceiveChannel] section at hashmap, " + "Ignoring [MACsecReceiveChannel] section from line %u", + c->section->filename, c->section->line); + return 0; +} + +static int macsec_transmit_association_verify(TransmitAssociation *t) { + NetDev *netdev; + int r; + + assert(t); + assert(t->macsec); + + netdev = NETDEV(t->macsec); + + if (section_is_invalid(t->section)) + return -EINVAL; + + if (t->sa.packet_number == 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: MACsec transmit secure association without PacketNumber= configured. " + "Ignoring [MACsecTransmitAssociation] section from line %u", + t->section->filename, t->section->line); + + r = macsec_read_key_file(netdev, &t->sa); + if (r < 0) + return r; + + if (t->sa.key_len <= 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: MACsec transmit secure association without key configured. " + "Ignoring [MACsecTransmitAssociation] section from line %u", + t->section->filename, t->section->line); + + return 0; +} + +static int macsec_receive_association_verify(ReceiveAssociation *a) { + ReceiveChannel *c; + NetDev *netdev; + int r; + + assert(a); + assert(a->macsec); + + netdev = NETDEV(a->macsec); + + if (section_is_invalid(a->section)) + return -EINVAL; + + r = macsec_read_key_file(netdev, &a->sa); + if (r < 0) + return r; + + if (a->sa.key_len <= 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: MACsec receive secure association without key configured. " + "Ignoring [MACsecReceiveAssociation] section from line %u", + a->section->filename, a->section->line); + + if (ether_addr_is_null(&a->sci.mac)) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: MACsec receive secure association without MAC address configured. " + "Ignoring [MACsecReceiveAssociation] section from line %u", + a->section->filename, a->section->line); + + if (a->sci.port == 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: MACsec receive secure association without port configured. " + "Ignoring [MACsecReceiveAssociation] section from line %u", + a->section->filename, a->section->line); + + c = ordered_hashmap_get(a->macsec->receive_channels, &a->sci.as_uint64); + if (!c) { + _cleanup_(macsec_receive_channel_freep) ReceiveChannel *new_channel = NULL; + + r = macsec_receive_channel_new(a->macsec, a->sci.as_uint64, &new_channel); + if (r < 0) + return log_oom(); + + r = ordered_hashmap_ensure_put(&a->macsec->receive_channels, &uint64_hash_ops, &new_channel->sci.as_uint64, new_channel); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_netdev_error_errno(netdev, r, + "%s: Failed to store receive channel at hashmap, " + "Ignoring [MACsecReceiveAssociation] section from line %u", + a->section->filename, a->section->line); + c = TAKE_PTR(new_channel); + } + if (c->n_rxsa >= MACSEC_MAX_ASSOCIATION_NUMBER) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(ERANGE), + "%s: Too many [MACsecReceiveAssociation] sections for the same receive channel, " + "Ignoring [MACsecReceiveAssociation] section from line %u", + a->section->filename, a->section->line); + + a->sa.association_number = c->n_rxsa; + c->rxsa[c->n_rxsa++] = a; + + return 0; +} + +static int netdev_macsec_verify(NetDev *netdev, const char *filename) { + assert(filename); + + MACsec *v = MACSEC(netdev); + TransmitAssociation *a; + ReceiveAssociation *n; + ReceiveChannel *c; + uint8_t an, encoding_an; + bool use_for_encoding; + int r; + + 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 = MACSEC(netdev); + + v->encrypt = -1; +} + +static void macsec_done(NetDev *netdev) { + MACsec *v = MACSEC(netdev); + + ordered_hashmap_free_with_destructor(v->receive_channels, macsec_receive_channel_free); + ordered_hashmap_free_with_destructor(v->receive_channels_by_section, macsec_receive_channel_free); + ordered_hashmap_free_with_destructor(v->transmit_associations_by_section, macsec_transmit_association_free); + ordered_hashmap_free_with_destructor(v->receive_associations_by_section, macsec_receive_association_free); +} + +const NetDevVTable macsec_vtable = { + .object_size = sizeof(MACsec), + .init = macsec_init, + .sections = NETDEV_COMMON_SECTIONS "MACsec\0MACsecReceiveChannel\0MACsecTransmitAssociation\0MACsecReceiveAssociation\0", + .fill_message_create = netdev_macsec_fill_message_create, + .post_create = netdev_macsec_configure, + .done = macsec_done, + .create_type = NETDEV_CREATE_STACKED, + .config_verify = netdev_macsec_verify, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/macsec.h b/src/network/netdev/macsec.h new file mode 100644 index 0000000..17bb1ca --- /dev/null +++ b/src/network/netdev/macsec.h @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <netinet/in.h> +#include <linux/if_macsec.h> + +#include "ether-addr-util.h" +#include "in-addr-util.h" +#include "netdev.h" +#include "networkd-util.h" +#include "sparse-endian.h" + +/* See the definition of MACSEC_NUM_AN in kernel's drivers/net/macsec.c */ +#define MACSEC_MAX_ASSOCIATION_NUMBER 4 + +typedef struct MACsec MACsec; + +typedef union MACsecSCI { + uint64_t as_uint64; + + struct { + struct ether_addr mac; + be16_t port; + } _packed_; +} MACsecSCI; + +assert_cc(sizeof(MACsecSCI) == sizeof(uint64_t)); + +typedef struct SecurityAssociation { + uint8_t association_number; + uint32_t packet_number; + uint8_t key_id[MACSEC_KEYID_LEN]; + uint8_t *key; + uint32_t key_len; + char *key_file; + int activate; + int use_for_encoding; +} SecurityAssociation; + +typedef struct TransmitAssociation { + MACsec *macsec; + ConfigSection *section; + + SecurityAssociation sa; +} TransmitAssociation; + +typedef struct ReceiveAssociation { + MACsec *macsec; + ConfigSection *section; + + MACsecSCI sci; + SecurityAssociation sa; +} ReceiveAssociation; + +typedef struct ReceiveChannel { + MACsec *macsec; + ConfigSection *section; + + MACsecSCI sci; + ReceiveAssociation *rxsa[MACSEC_MAX_ASSOCIATION_NUMBER]; + unsigned n_rxsa; +} ReceiveChannel; + +struct MACsec { + NetDev meta; + + uint16_t port; + int encrypt; + uint8_t encoding_an; + + OrderedHashmap *receive_channels; + OrderedHashmap *receive_channels_by_section; + OrderedHashmap *transmit_associations_by_section; + OrderedHashmap *receive_associations_by_section; +}; + +DEFINE_NETDEV_CAST(MACSEC, MACsec); +extern const NetDevVTable macsec_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_macsec_port); +CONFIG_PARSER_PROTOTYPE(config_parse_macsec_hw_address); +CONFIG_PARSER_PROTOTYPE(config_parse_macsec_packet_number); +CONFIG_PARSER_PROTOTYPE(config_parse_macsec_key_id); +CONFIG_PARSER_PROTOTYPE(config_parse_macsec_key); +CONFIG_PARSER_PROTOTYPE(config_parse_macsec_key_file); +CONFIG_PARSER_PROTOTYPE(config_parse_macsec_sa_activate); +CONFIG_PARSER_PROTOTYPE(config_parse_macsec_use_for_encoding); diff --git a/src/network/netdev/macvlan.c b/src/network/netdev/macvlan.c new file mode 100644 index 0000000..203807e --- /dev/null +++ b/src/network/netdev/macvlan.c @@ -0,0 +1,132 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> + +#include "conf-parser.h" +#include "macvlan.h" +#include "macvlan-util.h" +#include "networkd-network.h" +#include "parse-util.h" + +DEFINE_CONFIG_PARSE_ENUM(config_parse_macvlan_mode, macvlan_mode, MacVlanMode, "Failed to parse macvlan mode"); + +static int netdev_macvlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *req) { + assert(netdev); + assert(netdev->ifname); + assert(link); + assert(link->network); + + MacVlan *m = netdev->kind == NETDEV_KIND_MACVLAN ? MACVLAN(netdev) : MACVTAP(netdev); + int r; + + if (m->mode == NETDEV_MACVLAN_MODE_SOURCE && !set_isempty(m->match_source_mac)) { + const struct ether_addr *mac_addr; + + r = sd_netlink_message_append_u32(req, IFLA_MACVLAN_MACADDR_MODE, MACVLAN_MACADDR_SET); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(req, IFLA_MACVLAN_MACADDR_DATA); + if (r < 0) + return r; + + SET_FOREACH(mac_addr, m->match_source_mac) { + r = sd_netlink_message_append_ether_addr(req, IFLA_MACVLAN_MACADDR, mac_addr); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + } + + if (m->mode != _NETDEV_MACVLAN_MODE_INVALID) { + r = sd_netlink_message_append_u32(req, IFLA_MACVLAN_MODE, m->mode); + if (r < 0) + return r; + } + + /* set the nopromisc flag if Promiscuous= of the link is explicitly set to false */ + if (m->mode == NETDEV_MACVLAN_MODE_PASSTHRU && link->network->promiscuous == 0) { + r = sd_netlink_message_append_u16(req, IFLA_MACVLAN_FLAGS, MACVLAN_FLAG_NOPROMISC); + if (r < 0) + return r; + } + + if (m->bc_queue_length != UINT32_MAX) { + r = sd_netlink_message_append_u32(req, IFLA_MACVLAN_BC_QUEUE_LEN, m->bc_queue_length); + if (r < 0) + return r; + } + + return 0; +} + +int config_parse_macvlan_broadcast_queue_size( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + MacVlan *m = ASSERT_PTR(userdata); + + if (isempty(rvalue)) { + m->bc_queue_length = UINT32_MAX; + return 0; + } + + return config_parse_uint32_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, UINT32_MAX - 1, true, + &m->bc_queue_length); +} + +static void macvlan_done(NetDev *netdev) { + MacVlan *m = ASSERT_PTR(netdev)->kind == NETDEV_KIND_MACVLAN ? MACVLAN(netdev) : MACVTAP(netdev); + + set_free(m->match_source_mac); +} + +static void macvlan_init(NetDev *netdev) { + MacVlan *m = ASSERT_PTR(netdev)->kind == NETDEV_KIND_MACVLAN ? MACVLAN(netdev) : MACVTAP(netdev); + + m->mode = _NETDEV_MACVLAN_MODE_INVALID; + m->bc_queue_length = UINT32_MAX; +} + +const NetDevVTable macvtap_vtable = { + .object_size = sizeof(MacVlan), + .init = macvlan_init, + .done = macvlan_done, + .sections = NETDEV_COMMON_SECTIONS "MACVTAP\0", + .fill_message_create = netdev_macvlan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; + +const NetDevVTable macvlan_vtable = { + .object_size = sizeof(MacVlan), + .init = macvlan_init, + .done = macvlan_done, + .sections = NETDEV_COMMON_SECTIONS "MACVLAN\0", + .fill_message_create = netdev_macvlan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/macvlan.h b/src/network/netdev/macvlan.h new file mode 100644 index 0000000..c45fc4f --- /dev/null +++ b/src/network/netdev/macvlan.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct MacVlan MacVlan; + +#include "macvlan-util.h" +#include "netdev.h" +#include "set.h" + +struct MacVlan { + NetDev meta; + + MacVlanMode mode; + Set *match_source_mac; + + uint32_t bc_queue_length; +}; + +DEFINE_NETDEV_CAST(MACVLAN, MacVlan); +DEFINE_NETDEV_CAST(MACVTAP, MacVlan); +extern const NetDevVTable macvlan_vtable; +extern const NetDevVTable macvtap_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_macvlan_mode); +CONFIG_PARSER_PROTOTYPE(config_parse_macvlan_broadcast_queue_size); diff --git a/src/network/netdev/netdev-gperf.gperf b/src/network/netdev/netdev-gperf.gperf new file mode 100644 index 0000000..d5aa522 --- /dev/null +++ b/src/network/netdev/netdev-gperf.gperf @@ -0,0 +1,272 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +%{ +#if __GNUC__ >= 7 +_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"") +#endif +#include <stddef.h> +#include "bareudp.h" +#include "batadv.h" +#include "bond.h" +#include "bridge.h" +#include "conf-parser.h" +#include "fou-tunnel.h" +#include "geneve.h" +#include "ipoib.h" +#include "ipvlan.h" +#include "l2tp-tunnel.h" +#include "macsec.h" +#include "macvlan.h" +#include "net-condition.h" +#include "netdev.h" +#include "tunnel.h" +#include "tuntap.h" +#include "veth.h" +#include "vlan-util.h" +#include "vlan.h" +#include "vrf.h" +#include "vxcan.h" +#include "vxlan.h" +#include "wireguard.h" +#include "wlan.h" +#include "xfrm.h" +%} +struct ConfigPerfItem; +%null_strings +%language=ANSI-C +%define slot-name section_and_lvalue +%define hash-function-name network_netdev_gperf_hash +%define lookup-function-name network_netdev_gperf_lookup +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(NetDev, conditions) +Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(NetDev, conditions) +Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(NetDev, conditions) +Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(NetDev, conditions) +Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(NetDev, conditions) +Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(NetDev, conditions) +Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(NetDev, conditions) +NetDev.Description, config_parse_string, 0, offsetof(NetDev, description) +NetDev.Name, config_parse_ifname, 0, offsetof(NetDev, ifname) +NetDev.Kind, config_parse_netdev_kind, 0, offsetof(NetDev, kind) +NetDev.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(NetDev, mtu) +NetDev.MACAddress, config_parse_netdev_hw_addr, ETH_ALEN, offsetof(NetDev, hw_addr) +VLAN.Id, config_parse_vlanid, 0, offsetof(VLan, id) +VLAN.Protocol, config_parse_vlanprotocol, 0, offsetof(VLan, protocol) +VLAN.GVRP, config_parse_tristate, 0, offsetof(VLan, gvrp) +VLAN.MVRP, config_parse_tristate, 0, offsetof(VLan, mvrp) +VLAN.LooseBinding, config_parse_tristate, 0, offsetof(VLan, loose_binding) +VLAN.ReorderHeader, config_parse_tristate, 0, offsetof(VLan, reorder_hdr) +VLAN.EgressQOSMaps, config_parse_vlan_qos_maps, 0, offsetof(VLan, egress_qos_maps) +VLAN.IngressQOSMaps, config_parse_vlan_qos_maps, 0, offsetof(VLan, ingress_qos_maps) +MACVLAN.Mode, config_parse_macvlan_mode, 0, offsetof(MacVlan, mode) +MACVLAN.SourceMACAddress, config_parse_ether_addrs, 0, offsetof(MacVlan, match_source_mac) +MACVLAN.BroadcastMulticastQueueLength, config_parse_macvlan_broadcast_queue_size, 0, offsetof(MacVlan, bc_queue_length) +MACVTAP.Mode, config_parse_macvlan_mode, 0, offsetof(MacVlan, mode) +MACVTAP.SourceMACAddress, config_parse_ether_addrs, 0, offsetof(MacVlan, match_source_mac) +IPVLAN.Mode, config_parse_ipvlan_mode, 0, offsetof(IPVlan, mode) +IPVLAN.Flags, config_parse_ipvlan_flags, 0, offsetof(IPVlan, flags) +IPVTAP.Mode, config_parse_ipvlan_mode, 0, offsetof(IPVlan, mode) +IPVTAP.Flags, config_parse_ipvlan_flags, 0, offsetof(IPVlan, flags) +Tunnel.Local, config_parse_tunnel_local_address, 0, 0 +Tunnel.Remote, config_parse_tunnel_remote_address, 0, 0 +Tunnel.TOS, config_parse_unsigned, 0, offsetof(Tunnel, tos) +Tunnel.TTL, config_parse_unsigned, 0, offsetof(Tunnel, ttl) +Tunnel.Key, config_parse_tunnel_key, 0, offsetof(Tunnel, key) +Tunnel.InputKey, config_parse_tunnel_key, 0, offsetof(Tunnel, ikey) +Tunnel.OutputKey, config_parse_tunnel_key, 0, offsetof(Tunnel, okey) +Tunnel.DiscoverPathMTU, config_parse_tristate, 0, offsetof(Tunnel, pmtudisc) +Tunnel.IgnoreDontFragment, config_parse_bool, 0, offsetof(Tunnel, ignore_df) +Tunnel.Mode, config_parse_ip6tnl_mode, 0, offsetof(Tunnel, ip6tnl_mode) +Tunnel.IPv6FlowLabel, config_parse_ipv6_flowlabel, 0, 0 +Tunnel.CopyDSCP, config_parse_bool, 0, offsetof(Tunnel, copy_dscp) +Tunnel.EncapsulationLimit, config_parse_encap_limit, 0, 0 +Tunnel.Independent, config_parse_bool, 0, offsetof(Tunnel, independent) +Tunnel.AssignToLoopback, config_parse_bool, 0, offsetof(Tunnel, assign_to_loopback) +Tunnel.AllowLocalRemote, config_parse_tristate, 0, offsetof(Tunnel, allow_localremote) +Tunnel.FooOverUDP, config_parse_bool, 0, offsetof(Tunnel, fou_tunnel) +Tunnel.FOUDestinationPort, config_parse_ip_port, 0, offsetof(Tunnel, fou_destination_port) +Tunnel.FOUSourcePort, config_parse_ip_port, 0, offsetof(Tunnel, encap_src_port) +Tunnel.Encapsulation, config_parse_fou_encap_type, 0, offsetof(Tunnel, fou_encap_type) +Tunnel.IPv6RapidDeploymentPrefix, config_parse_6rd_prefix, 0, 0 +Tunnel.ERSPANVersion, config_parse_erspan_version, 0, offsetof(Tunnel, erspan_version) +Tunnel.ERSPANIndex, config_parse_erspan_index, 0, offsetof(Tunnel, erspan_index) +Tunnel.ERSPANDirection, config_parse_erspan_direction, 0, offsetof(Tunnel, erspan_direction) +Tunnel.ERSPANHardwareId, config_parse_erspan_hwid, 0, offsetof(Tunnel, erspan_hwid) +Tunnel.SerializeTunneledPackets, config_parse_tristate, 0, offsetof(Tunnel, gre_erspan_sequence) +Tunnel.ISATAP, config_parse_tristate, 0, offsetof(Tunnel, isatap) +Tunnel.External, config_parse_bool, 0, offsetof(Tunnel, external) +FooOverUDP.Protocol, config_parse_ip_protocol, 0, offsetof(FouTunnel, fou_protocol) +FooOverUDP.Encapsulation, config_parse_fou_encap_type, 0, offsetof(FouTunnel, fou_encap_type) +FooOverUDP.Port, config_parse_ip_port, 0, offsetof(FouTunnel, port) +FooOverUDP.PeerPort, config_parse_ip_port, 0, offsetof(FouTunnel, peer_port) +FooOverUDP.Local, config_parse_fou_tunnel_address, 0, offsetof(FouTunnel, local) +FooOverUDP.Peer, config_parse_fou_tunnel_address, 0, offsetof(FouTunnel, peer) +L2TP.TunnelId, config_parse_l2tp_tunnel_id, 0, offsetof(L2tpTunnel, tunnel_id) +L2TP.PeerTunnelId, config_parse_l2tp_tunnel_id, 0, offsetof(L2tpTunnel, peer_tunnel_id) +L2TP.UDPSourcePort, config_parse_ip_port, 0, offsetof(L2tpTunnel, l2tp_udp_sport) +L2TP.UDPDestinationPort, config_parse_ip_port, 0, offsetof(L2tpTunnel, l2tp_udp_dport) +L2TP.Local, config_parse_l2tp_tunnel_local_address, 0, 0 +L2TP.Remote, config_parse_l2tp_tunnel_remote_address, 0, 0 +L2TP.EncapsulationType, config_parse_l2tp_encap_type, 0, offsetof(L2tpTunnel, l2tp_encap_type) +L2TP.UDPCheckSum, config_parse_bool, 0, offsetof(L2tpTunnel, udp_csum) +L2TP.UDP6CheckSumRx, config_parse_bool, 0, offsetof(L2tpTunnel, udp6_csum_rx) +L2TP.UDP6CheckSumTx, config_parse_bool, 0, offsetof(L2tpTunnel, udp6_csum_tx) +L2TPSession.SessionId, config_parse_l2tp_session_id, 0, 0 +L2TPSession.PeerSessionId, config_parse_l2tp_session_id, 0, 0 +L2TPSession.Layer2SpecificHeader, config_parse_l2tp_session_l2spec, 0, 0 +L2TPSession.Name, config_parse_l2tp_session_name, 0, 0 +Peer.Name, config_parse_ifname, 0, offsetof(Veth, ifname_peer) +Peer.MACAddress, config_parse_netdev_hw_addr, ETH_ALEN, offsetof(Veth, hw_addr_peer) +VXCAN.Peer, config_parse_ifname, 0, offsetof(VxCan, ifname_peer) +VXLAN.VNI, config_parse_uint32, 0, offsetof(VxLan, vni) +VXLAN.Id, config_parse_uint32, 0, offsetof(VxLan, vni) /* deprecated */ +VXLAN.Group, config_parse_vxlan_address, 0, offsetof(VxLan, group) +VXLAN.Local, config_parse_vxlan_address, 0, offsetof(VxLan, local) +VXLAN.Remote, config_parse_vxlan_address, 0, offsetof(VxLan, remote) +VXLAN.TOS, config_parse_unsigned, 0, offsetof(VxLan, tos) +VXLAN.TTL, config_parse_vxlan_ttl, 0, offsetof(VxLan, ttl) +VXLAN.MacLearning, config_parse_bool, 0, offsetof(VxLan, learning) +VXLAN.ARPProxy, config_parse_bool, 0, offsetof(VxLan, arp_proxy) +VXLAN.ReduceARPProxy, config_parse_bool, 0, offsetof(VxLan, arp_proxy) +VXLAN.L2MissNotification, config_parse_bool, 0, offsetof(VxLan, l2miss) +VXLAN.L3MissNotification, config_parse_bool, 0, offsetof(VxLan, l3miss) +VXLAN.RouteShortCircuit, config_parse_bool, 0, offsetof(VxLan, route_short_circuit) +VXLAN.UDPCheckSum, config_parse_bool, 0, offsetof(VxLan, udpcsum) +VXLAN.UDPChecksum, config_parse_bool, 0, offsetof(VxLan, udpcsum) +VXLAN.UDP6ZeroCheckSumRx, config_parse_bool, 0, offsetof(VxLan, udp6zerocsumrx) +VXLAN.UDP6ZeroChecksumRx, config_parse_bool, 0, offsetof(VxLan, udp6zerocsumrx) +VXLAN.UDP6ZeroCheckSumTx, config_parse_bool, 0, offsetof(VxLan, udp6zerocsumtx) +VXLAN.UDP6ZeroChecksumTx, config_parse_bool, 0, offsetof(VxLan, udp6zerocsumtx) +VXLAN.RemoteChecksumTx, config_parse_bool, 0, offsetof(VxLan, remote_csum_tx) +VXLAN.RemoteChecksumRx, config_parse_bool, 0, offsetof(VxLan, remote_csum_rx) +VXLAN.FDBAgeingSec, config_parse_sec, 0, offsetof(VxLan, fdb_ageing) +VXLAN.GroupPolicyExtension, config_parse_bool, 0, offsetof(VxLan, group_policy) +VXLAN.GenericProtocolExtension, config_parse_bool, 0, offsetof(VxLan, generic_protocol_extension) +VXLAN.MaximumFDBEntries, config_parse_unsigned, 0, offsetof(VxLan, max_fdb) +VXLAN.PortRange, config_parse_port_range, 0, 0 +VXLAN.DestinationPort, config_parse_ip_port, 0, offsetof(VxLan, dest_port) +VXLAN.FlowLabel, config_parse_flow_label, 0, 0 +VXLAN.IPDoNotFragment, config_parse_df, 0, offsetof(VxLan, df) +VXLAN.Independent, config_parse_bool, 0, offsetof(VxLan, independent) +GENEVE.Id, config_parse_geneve_vni, 0, offsetof(Geneve, id) +GENEVE.Remote, config_parse_geneve_address, 0, offsetof(Geneve, remote) +GENEVE.TOS, config_parse_uint8, 0, offsetof(Geneve, tos) +GENEVE.TTL, config_parse_geneve_ttl, 0, offsetof(Geneve, ttl) +GENEVE.UDPChecksum, config_parse_bool, 0, offsetof(Geneve, udpcsum) +GENEVE.UDP6ZeroCheckSumRx, config_parse_bool, 0, offsetof(Geneve, udp6zerocsumrx) +GENEVE.UDP6ZeroChecksumRx, config_parse_bool, 0, offsetof(Geneve, udp6zerocsumrx) +GENEVE.UDP6ZeroCheckSumTx, config_parse_bool, 0, offsetof(Geneve, udp6zerocsumtx) +GENEVE.UDP6ZeroChecksumTx, config_parse_bool, 0, offsetof(Geneve, udp6zerocsumtx) +GENEVE.DestinationPort, config_parse_ip_port, 0, offsetof(Geneve, dest_port) +GENEVE.IPDoNotFragment, config_parse_geneve_df, 0, offsetof(Geneve, geneve_df) +GENEVE.FlowLabel, config_parse_geneve_flow_label, 0, 0 +GENEVE.InheritInnerProtocol, config_parse_bool, 0, offsetof(Geneve, inherit_inner_protocol) +MACsec.Port, config_parse_macsec_port, 0, 0 +MACsec.Encrypt, config_parse_tristate, 0, offsetof(MACsec, encrypt) +MACsecReceiveChannel.Port, config_parse_macsec_port, 0, 0 +MACsecReceiveChannel.MACAddress, config_parse_macsec_hw_address, 0, 0 +MACsecTransmitAssociation.PacketNumber, config_parse_macsec_packet_number, 0, 0 +MACsecTransmitAssociation.KeyId, config_parse_macsec_key_id, 0, 0 +MACsecTransmitAssociation.Key, config_parse_macsec_key, 0, 0 +MACsecTransmitAssociation.KeyFile, config_parse_macsec_key_file, 0, 0 +MACsecTransmitAssociation.Activate, config_parse_macsec_sa_activate, 0, 0 +MACsecTransmitAssociation.UseForEncoding, config_parse_macsec_use_for_encoding, 0, 0 +MACsecReceiveAssociation.Port, config_parse_macsec_port, 0, 0 +MACsecReceiveAssociation.MACAddress, config_parse_macsec_hw_address, 0, 0 +MACsecReceiveAssociation.PacketNumber, config_parse_macsec_packet_number, 0, 0 +MACsecReceiveAssociation.KeyId, config_parse_macsec_key_id, 0, 0 +MACsecReceiveAssociation.Key, config_parse_macsec_key, 0, 0 +MACsecReceiveAssociation.KeyFile, config_parse_macsec_key_file, 0, 0 +MACsecReceiveAssociation.Activate, config_parse_macsec_sa_activate, 0, 0 +Tun.OneQueue, config_parse_warn_compat, DISABLED_LEGACY, 0 +Tun.MultiQueue, config_parse_bool, 0, offsetof(TunTap, multi_queue) +Tun.PacketInfo, config_parse_bool, 0, offsetof(TunTap, packet_info) +Tun.VNetHeader, config_parse_bool, 0, offsetof(TunTap, vnet_hdr) +Tun.User, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(TunTap, user_name) +Tun.Group, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(TunTap, group_name) +Tun.KeepCarrier, config_parse_bool, 0, offsetof(TunTap, keep_fd) +Tap.OneQueue, config_parse_warn_compat, DISABLED_LEGACY, 0 +Tap.MultiQueue, config_parse_bool, 0, offsetof(TunTap, multi_queue) +Tap.PacketInfo, config_parse_bool, 0, offsetof(TunTap, packet_info) +Tap.VNetHeader, config_parse_bool, 0, offsetof(TunTap, vnet_hdr) +Tap.User, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(TunTap, user_name) +Tap.Group, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(TunTap, group_name) +Tap.KeepCarrier, config_parse_bool, 0, offsetof(TunTap, keep_fd) +Bond.Mode, config_parse_bond_mode, 0, offsetof(Bond, mode) +Bond.TransmitHashPolicy, config_parse_bond_xmit_hash_policy, 0, offsetof(Bond, xmit_hash_policy) +Bond.LACPTransmitRate, config_parse_bond_lacp_rate, 0, offsetof(Bond, lacp_rate) +Bond.AdSelect, config_parse_bond_ad_select, 0, offsetof(Bond, ad_select) +Bond.FailOverMACPolicy, config_parse_bond_fail_over_mac, 0, offsetof(Bond, fail_over_mac) +Bond.ARPIPTargets, config_parse_arp_ip_target_address, 0, 0 +Bond.ARPValidate, config_parse_bond_arp_validate, 0, offsetof(Bond, arp_validate) +Bond.ARPAllTargets, config_parse_bond_arp_all_targets, 0, offsetof(Bond, arp_all_targets) +Bond.PrimaryReselectPolicy, config_parse_bond_primary_reselect, 0, offsetof(Bond, primary_reselect) +Bond.ResendIGMP, config_parse_unsigned, 0, offsetof(Bond, resend_igmp) +Bond.PacketsPerSlave, config_parse_unsigned, 0, offsetof(Bond, packets_per_slave) +Bond.GratuitousARP, config_parse_unsigned, 0, offsetof(Bond, num_grat_arp) +Bond.AllSlavesActive, config_parse_bool, 0, offsetof(Bond, all_slaves_active) +Bond.DynamicTransmitLoadBalancing, config_parse_tristate, 0, offsetof(Bond, tlb_dynamic_lb) +Bond.MinLinks, config_parse_unsigned, 0, offsetof(Bond, min_links) +Bond.MIIMonitorSec, config_parse_sec, 0, offsetof(Bond, miimon) +Bond.UpDelaySec, config_parse_sec, 0, offsetof(Bond, updelay) +Bond.DownDelaySec, config_parse_sec, 0, offsetof(Bond, downdelay) +Bond.ARPIntervalSec, config_parse_sec, 0, offsetof(Bond, arp_interval) +Bond.LearnPacketIntervalSec, config_parse_sec, 0, offsetof(Bond, lp_interval) +Bond.AdActorSystemPriority, config_parse_ad_actor_sys_prio, 0, offsetof(Bond, ad_actor_sys_prio) +Bond.AdUserPortKey, config_parse_ad_user_port_key, 0, offsetof(Bond, ad_user_port_key) +Bond.AdActorSystem, config_parse_ad_actor_system, 0, offsetof(Bond, ad_actor_system) +Bridge.HelloTimeSec, config_parse_sec, 0, offsetof(Bridge, hello_time) +Bridge.MaxAgeSec, config_parse_sec, 0, offsetof(Bridge, max_age) +Bridge.AgeingTimeSec, config_parse_sec, 0, offsetof(Bridge, ageing_time) +Bridge.ForwardDelaySec, config_parse_sec, 0, offsetof(Bridge, forward_delay) +Bridge.Priority, config_parse_uint16, 0, offsetof(Bridge, priority) +Bridge.GroupForwardMask, config_parse_uint16, 0, offsetof(Bridge, group_fwd_mask) +Bridge.DefaultPVID, config_parse_default_port_vlanid, 0, offsetof(Bridge, default_pvid) +Bridge.MulticastQuerier, config_parse_tristate, 0, offsetof(Bridge, mcast_querier) +Bridge.MulticastSnooping, config_parse_tristate, 0, offsetof(Bridge, mcast_snooping) +Bridge.VLANFiltering, config_parse_tristate, 0, offsetof(Bridge, vlan_filtering) +Bridge.VLANProtocol, config_parse_vlanprotocol, 0, offsetof(Bridge, vlan_protocol) +Bridge.STP, config_parse_tristate, 0, offsetof(Bridge, stp) +Bridge.MulticastIGMPVersion, config_parse_uint8, 0, offsetof(Bridge, igmp_version) +VRF.TableId, config_parse_uint32, 0, offsetof(Vrf, table) /* deprecated */ +VRF.Table, config_parse_uint32, 0, offsetof(Vrf, table) +BareUDP.DestinationPort, config_parse_ip_port, 0, offsetof(BareUDP, dest_port) +BareUDP.EtherType, config_parse_bare_udp_iftype, 0, offsetof(BareUDP, iftype) +WireGuard.FirewallMark, config_parse_unsigned, 0, offsetof(Wireguard, fwmark) +WireGuard.FwMark, config_parse_unsigned, 0, offsetof(Wireguard, fwmark) /* deprecated */ +WireGuard.ListenPort, config_parse_wireguard_listen_port, 0, offsetof(Wireguard, port) +WireGuard.PrivateKey, config_parse_wireguard_private_key, 0, 0 +WireGuard.PrivateKeyFile, config_parse_wireguard_private_key_file, 0, 0 +WireGuard.RouteTable, config_parse_wireguard_route_table, 0, offsetof(Wireguard, route_table) +WireGuard.RouteMetric, config_parse_wireguard_route_priority, 0, offsetof(Wireguard, route_priority) +WireGuardPeer.AllowedIPs, config_parse_wireguard_allowed_ips, 0, 0 +WireGuardPeer.Endpoint, config_parse_wireguard_endpoint, 0, 0 +WireGuardPeer.PublicKey, config_parse_wireguard_peer_key, 0, 0 +WireGuardPeer.PresharedKey, config_parse_wireguard_peer_key, 0, 0 +WireGuardPeer.PresharedKeyFile, config_parse_wireguard_preshared_key_file, 0, 0 +WireGuardPeer.PersistentKeepalive, config_parse_wireguard_keepalive, 0, 0 +WireGuardPeer.RouteTable, config_parse_wireguard_peer_route_table, 0, 0 +WireGuardPeer.RouteMetric, config_parse_wireguard_peer_route_priority,0, 0 +Xfrm.InterfaceId, config_parse_uint32, 0, offsetof(Xfrm, if_id) +Xfrm.Independent, config_parse_bool, 0, offsetof(Xfrm, independent) +BatmanAdvanced.Aggregation, config_parse_bool, 0, offsetof(BatmanAdvanced, aggregation) +BatmanAdvanced.BridgeLoopAvoidance, config_parse_bool, 0, offsetof(BatmanAdvanced, bridge_loop_avoidance) +BatmanAdvanced.DistributedArpTable, config_parse_bool, 0, offsetof(BatmanAdvanced, distributed_arp_table) +BatmanAdvanced.Fragmentation, config_parse_bool, 0, offsetof(BatmanAdvanced, fragmentation) +BatmanAdvanced.GatewayMode, config_parse_batadv_gateway_mode, 0, offsetof(BatmanAdvanced, gateway_mode) +BatmanAdvanced.GatewayBandwithDown, config_parse_badadv_bandwidth, 0, offsetof(BatmanAdvanced, gateway_bandwidth_down) +BatmanAdvanced.GatewayBandwithUp, config_parse_badadv_bandwidth, 0, offsetof(BatmanAdvanced, gateway_bandwidth_up) +BatmanAdvanced.GatewayBandwidthDown, config_parse_badadv_bandwidth, 0, offsetof(BatmanAdvanced, gateway_bandwidth_down) +BatmanAdvanced.GatewayBandwidthUp, config_parse_badadv_bandwidth, 0, offsetof(BatmanAdvanced, gateway_bandwidth_up) +BatmanAdvanced.HopPenalty, config_parse_uint8, 0, offsetof(BatmanAdvanced, hop_penalty) +BatmanAdvanced.OriginatorIntervalSec, config_parse_sec, 0, offsetof(BatmanAdvanced, originator_interval) +BatmanAdvanced.RoutingAlgorithm, config_parse_batadv_routing_algorithm, 0, offsetof(BatmanAdvanced, routing_algorithm) +IPoIB.PartitionKey, config_parse_ipoib_pkey, 0, offsetof(IPoIB, pkey) +IPoIB.Mode, config_parse_ipoib_mode, 0, offsetof(IPoIB, mode) +IPoIB.IgnoreUserspaceMulticastGroups, config_parse_tristate, 0, offsetof(IPoIB, umcast) +WLAN.PhysicalDevice, config_parse_wiphy, 0, 0 +WLAN.Type, config_parse_wlan_iftype, 0, offsetof(WLan, iftype) +WLAN.WDS, config_parse_tristate, 0, offsetof(WLan, wds) diff --git a/src/network/netdev/netdev-util.c b/src/network/netdev/netdev-util.c new file mode 100644 index 0000000..6229992 --- /dev/null +++ b/src/network/netdev/netdev-util.c @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "netdev-util.h" +#include "networkd-address.h" +#include "networkd-link.h" +#include "string-table.h" + +static const char * const netdev_local_address_type_table[_NETDEV_LOCAL_ADDRESS_TYPE_MAX] = { + [NETDEV_LOCAL_ADDRESS_IPV4LL] = "ipv4_link_local", + [NETDEV_LOCAL_ADDRESS_IPV6LL] = "ipv6_link_local", + [NETDEV_LOCAL_ADDRESS_DHCP4] = "dhcp4", + [NETDEV_LOCAL_ADDRESS_DHCP6] = "dhcp6", + [NETDEV_LOCAL_ADDRESS_SLAAC] = "slaac", +}; + +DEFINE_STRING_TABLE_LOOKUP(netdev_local_address_type, NetDevLocalAddressType); + +int link_get_local_address( + Link *link, + NetDevLocalAddressType type, + int family, + int *ret_family, + union in_addr_union *ret_address) { + + Address *a; + + assert(link); + + switch (type) { + case NETDEV_LOCAL_ADDRESS_IPV4LL: + assert(IN_SET(family, AF_UNSPEC, AF_INET)); + family = AF_INET; + break; + case NETDEV_LOCAL_ADDRESS_IPV6LL: + assert(IN_SET(family, AF_UNSPEC, AF_INET6)); + family = AF_INET6; + break; + case NETDEV_LOCAL_ADDRESS_DHCP4: + assert(IN_SET(family, AF_UNSPEC, AF_INET)); + family = AF_INET; + break; + case NETDEV_LOCAL_ADDRESS_DHCP6: + assert(IN_SET(family, AF_UNSPEC, AF_INET6)); + family = AF_INET6; + break; + case NETDEV_LOCAL_ADDRESS_SLAAC: + assert(IN_SET(family, AF_UNSPEC, AF_INET6)); + family = AF_INET6; + break; + default: + assert_not_reached(); + } + + if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) + return -EBUSY; + + SET_FOREACH(a, link->addresses) { + if (!address_is_ready(a)) + continue; + + if (a->family != family) + continue; + + if (in_addr_is_set(a->family, &a->in_addr_peer)) + continue; + + switch (type) { + case NETDEV_LOCAL_ADDRESS_IPV4LL: + if (a->source != NETWORK_CONFIG_SOURCE_IPV4LL) + continue; + break; + case NETDEV_LOCAL_ADDRESS_IPV6LL: + if (!in6_addr_is_link_local(&a->in_addr.in6)) + continue; + break; + case NETDEV_LOCAL_ADDRESS_DHCP4: + if (a->source != NETWORK_CONFIG_SOURCE_DHCP4) + continue; + break; + case NETDEV_LOCAL_ADDRESS_DHCP6: + if (a->source != NETWORK_CONFIG_SOURCE_DHCP6) + continue; + break; + case NETDEV_LOCAL_ADDRESS_SLAAC: + if (a->source != NETWORK_CONFIG_SOURCE_NDISC) + continue; + break; + default: + assert_not_reached(); + } + + if (ret_family) + *ret_family = a->family; + if (ret_address) + *ret_address = a->in_addr; + return 1; + } + + return -ENXIO; +} diff --git a/src/network/netdev/netdev-util.h b/src/network/netdev/netdev-util.h new file mode 100644 index 0000000..02b07e3 --- /dev/null +++ b/src/network/netdev/netdev-util.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "in-addr-util.h" +#include "macro.h" + +typedef struct Link Link; + +typedef enum NetDevLocalAddressType { + NETDEV_LOCAL_ADDRESS_IPV4LL, + NETDEV_LOCAL_ADDRESS_IPV6LL, + NETDEV_LOCAL_ADDRESS_DHCP4, + NETDEV_LOCAL_ADDRESS_DHCP6, + NETDEV_LOCAL_ADDRESS_SLAAC, + _NETDEV_LOCAL_ADDRESS_TYPE_MAX, + _NETDEV_LOCAL_ADDRESS_TYPE_INVALID = -EINVAL, +} NetDevLocalAddressType; + +const char *netdev_local_address_type_to_string(NetDevLocalAddressType t) _const_; +NetDevLocalAddressType netdev_local_address_type_from_string(const char *s) _pure_; + +int link_get_local_address( + Link *link, + NetDevLocalAddressType type, + int family, + int *ret_family, + union in_addr_union *ret_address); diff --git a/src/network/netdev/netdev.c b/src/network/netdev/netdev.c new file mode 100644 index 0000000..57127a8 --- /dev/null +++ b/src/network/netdev/netdev.c @@ -0,0 +1,957 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "arphrd-util.h" +#include "bareudp.h" +#include "batadv.h" +#include "bond.h" +#include "bridge.h" +#include "conf-files.h" +#include "conf-parser.h" +#include "dummy.h" +#include "fd-util.h" +#include "fou-tunnel.h" +#include "geneve.h" +#include "ifb.h" +#include "ipoib.h" +#include "ipvlan.h" +#include "l2tp-tunnel.h" +#include "list.h" +#include "macsec.h" +#include "macvlan.h" +#include "netdev.h" +#include "netdevsim.h" +#include "netif-util.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "networkd-queue.h" +#include "networkd-setlink.h" +#include "networkd-sriov.h" +#include "nlmon.h" +#include "path-lookup.h" +#include "siphash24.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "tunnel.h" +#include "tuntap.h" +#include "vcan.h" +#include "veth.h" +#include "vlan.h" +#include "vrf.h" +#include "vxcan.h" +#include "vxlan.h" +#include "wireguard.h" +#include "wlan.h" +#include "xfrm.h" + +const NetDevVTable * const netdev_vtable[_NETDEV_KIND_MAX] = { + [NETDEV_KIND_BAREUDP] = &bare_udp_vtable, + [NETDEV_KIND_BATADV] = &batadv_vtable, + [NETDEV_KIND_BOND] = &bond_vtable, + [NETDEV_KIND_BRIDGE] = &bridge_vtable, + [NETDEV_KIND_DUMMY] = &dummy_vtable, + [NETDEV_KIND_ERSPAN] = &erspan_vtable, + [NETDEV_KIND_FOU] = &foutnl_vtable, + [NETDEV_KIND_GENEVE] = &geneve_vtable, + [NETDEV_KIND_GRE] = &gre_vtable, + [NETDEV_KIND_GRETAP] = &gretap_vtable, + [NETDEV_KIND_IFB] = &ifb_vtable, + [NETDEV_KIND_IP6GRE] = &ip6gre_vtable, + [NETDEV_KIND_IP6GRETAP] = &ip6gretap_vtable, + [NETDEV_KIND_IP6TNL] = &ip6tnl_vtable, + [NETDEV_KIND_IPIP] = &ipip_vtable, + [NETDEV_KIND_IPOIB] = &ipoib_vtable, + [NETDEV_KIND_IPVLAN] = &ipvlan_vtable, + [NETDEV_KIND_IPVTAP] = &ipvtap_vtable, + [NETDEV_KIND_L2TP] = &l2tptnl_vtable, + [NETDEV_KIND_MACSEC] = &macsec_vtable, + [NETDEV_KIND_MACVLAN] = &macvlan_vtable, + [NETDEV_KIND_MACVTAP] = &macvtap_vtable, + [NETDEV_KIND_NETDEVSIM] = &netdevsim_vtable, + [NETDEV_KIND_NLMON] = &nlmon_vtable, + [NETDEV_KIND_SIT] = &sit_vtable, + [NETDEV_KIND_TAP] = &tap_vtable, + [NETDEV_KIND_TUN] = &tun_vtable, + [NETDEV_KIND_VCAN] = &vcan_vtable, + [NETDEV_KIND_VETH] = &veth_vtable, + [NETDEV_KIND_VLAN] = &vlan_vtable, + [NETDEV_KIND_VRF] = &vrf_vtable, + [NETDEV_KIND_VTI6] = &vti6_vtable, + [NETDEV_KIND_VTI] = &vti_vtable, + [NETDEV_KIND_VXCAN] = &vxcan_vtable, + [NETDEV_KIND_VXLAN] = &vxlan_vtable, + [NETDEV_KIND_WIREGUARD] = &wireguard_vtable, + [NETDEV_KIND_WLAN] = &wlan_vtable, + [NETDEV_KIND_XFRM] = &xfrm_vtable, +}; + +static const char* const netdev_kind_table[_NETDEV_KIND_MAX] = { + [NETDEV_KIND_BAREUDP] = "bareudp", + [NETDEV_KIND_BATADV] = "batadv", + [NETDEV_KIND_BOND] = "bond", + [NETDEV_KIND_BRIDGE] = "bridge", + [NETDEV_KIND_DUMMY] = "dummy", + [NETDEV_KIND_ERSPAN] = "erspan", + [NETDEV_KIND_FOU] = "fou", + [NETDEV_KIND_GENEVE] = "geneve", + [NETDEV_KIND_GRE] = "gre", + [NETDEV_KIND_GRETAP] = "gretap", + [NETDEV_KIND_IFB] = "ifb", + [NETDEV_KIND_IP6GRE] = "ip6gre", + [NETDEV_KIND_IP6GRETAP] = "ip6gretap", + [NETDEV_KIND_IP6TNL] = "ip6tnl", + [NETDEV_KIND_IPIP] = "ipip", + [NETDEV_KIND_IPOIB] = "ipoib", + [NETDEV_KIND_IPVLAN] = "ipvlan", + [NETDEV_KIND_IPVTAP] = "ipvtap", + [NETDEV_KIND_L2TP] = "l2tp", + [NETDEV_KIND_MACSEC] = "macsec", + [NETDEV_KIND_MACVLAN] = "macvlan", + [NETDEV_KIND_MACVTAP] = "macvtap", + [NETDEV_KIND_NETDEVSIM] = "netdevsim", + [NETDEV_KIND_NLMON] = "nlmon", + [NETDEV_KIND_SIT] = "sit", + [NETDEV_KIND_TAP] = "tap", + [NETDEV_KIND_TUN] = "tun", + [NETDEV_KIND_VCAN] = "vcan", + [NETDEV_KIND_VETH] = "veth", + [NETDEV_KIND_VLAN] = "vlan", + [NETDEV_KIND_VRF] = "vrf", + [NETDEV_KIND_VTI6] = "vti6", + [NETDEV_KIND_VTI] = "vti", + [NETDEV_KIND_VXCAN] = "vxcan", + [NETDEV_KIND_VXLAN] = "vxlan", + [NETDEV_KIND_WIREGUARD] = "wireguard", + [NETDEV_KIND_WLAN] = "wlan", + [NETDEV_KIND_XFRM] = "xfrm", +}; + +DEFINE_STRING_TABLE_LOOKUP(netdev_kind, NetDevKind); + +bool netdev_is_managed(NetDev *netdev) { + if (!netdev || !netdev->manager || !netdev->ifname) + return false; + + return hashmap_get(netdev->manager->netdevs, netdev->ifname) == netdev; +} + +static bool netdev_is_stacked_and_independent(NetDev *netdev) { + assert(netdev); + + if (netdev_get_create_type(netdev) != NETDEV_CREATE_STACKED) + return false; + + switch (netdev->kind) { + case NETDEV_KIND_ERSPAN: + return ERSPAN(netdev)->independent; + case NETDEV_KIND_GRE: + return GRE(netdev)->independent; + case NETDEV_KIND_GRETAP: + return GRETAP(netdev)->independent; + case NETDEV_KIND_IP6GRE: + return IP6GRE(netdev)->independent; + case NETDEV_KIND_IP6GRETAP: + return IP6GRETAP(netdev)->independent; + case NETDEV_KIND_IP6TNL: + return IP6TNL(netdev)->independent; + case NETDEV_KIND_IPIP: + return IPIP(netdev)->independent; + case NETDEV_KIND_SIT: + return SIT(netdev)->independent; + case NETDEV_KIND_VTI: + return VTI(netdev)->independent; + case NETDEV_KIND_VTI6: + return VTI6(netdev)->independent; + case NETDEV_KIND_VXLAN: + return VXLAN(netdev)->independent; + case NETDEV_KIND_XFRM: + return XFRM(netdev)->independent; + default: + return false; + } +} + +static bool netdev_is_stacked(NetDev *netdev) { + assert(netdev); + + if (netdev_get_create_type(netdev) != NETDEV_CREATE_STACKED) + return false; + + if (netdev_is_stacked_and_independent(netdev)) + return false; + + return true; +} + +static void netdev_detach_from_manager(NetDev *netdev) { + if (netdev->ifname && netdev->manager) + hashmap_remove(netdev->manager->netdevs, netdev->ifname); +} + +static NetDev *netdev_free(NetDev *netdev) { + assert(netdev); + + netdev_detach_from_manager(netdev); + + free(netdev->filename); + + free(netdev->description); + free(netdev->ifname); + condition_free_list(netdev->conditions); + + /* Invoke the per-kind done() destructor, but only if the state field is initialized. We conditionalize that + * because we parse .netdev files twice: once to determine the kind (with a short, minimal NetDev structure + * allocation, with no room for per-kind fields), and once to read the kind's properties (with a full, + * comprehensive NetDev structure allocation with enough space for whatever the specific kind needs). Now, in + * the first case we shouldn't try to destruct the per-kind NetDev fields on destruction, in the second case we + * should. We use the state field to discern the two cases: it's _NETDEV_STATE_INVALID on the first "raw" + * call. */ + if (netdev->state != _NETDEV_STATE_INVALID && + NETDEV_VTABLE(netdev) && + NETDEV_VTABLE(netdev)->done) + NETDEV_VTABLE(netdev)->done(netdev); + + return mfree(netdev); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(NetDev, netdev, netdev_free); + +void netdev_drop(NetDev *netdev) { + if (!netdev) + return; + + if (netdev_is_stacked(netdev)) { + /* The netdev may be removed due to the underlying device removal, and the device may + * be re-added later. */ + netdev->state = NETDEV_STATE_LOADING; + netdev->ifindex = 0; + + log_netdev_debug(netdev, "netdev removed"); + return; + } + + if (NETDEV_VTABLE(netdev) && NETDEV_VTABLE(netdev)->drop) + NETDEV_VTABLE(netdev)->drop(netdev); + + netdev->state = NETDEV_STATE_LINGER; + + log_netdev_debug(netdev, "netdev removed"); + + netdev_detach_from_manager(netdev); + netdev_unref(netdev); + return; +} + +int netdev_get(Manager *manager, const char *name, NetDev **ret) { + NetDev *netdev; + + assert(manager); + assert(name); + assert(ret); + + netdev = hashmap_get(manager->netdevs, name); + if (!netdev) + return -ENOENT; + + *ret = netdev; + + return 0; +} + +void netdev_enter_failed(NetDev *netdev) { + netdev->state = NETDEV_STATE_FAILED; +} + +static int netdev_enter_ready(NetDev *netdev) { + assert(netdev); + assert(netdev->ifname); + + if (netdev->state != NETDEV_STATE_CREATING) + return 0; + + netdev->state = NETDEV_STATE_READY; + + log_netdev_info(netdev, "netdev ready"); + + if (NETDEV_VTABLE(netdev)->post_create) + NETDEV_VTABLE(netdev)->post_create(netdev, NULL); + + return 0; +} + +/* callback for netdev's created without a backing Link */ +static int netdev_create_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) { + int r; + + assert(netdev); + assert(netdev->state != _NETDEV_STATE_INVALID); + + r = sd_netlink_message_get_errno(m); + if (r == -EEXIST) + log_netdev_info(netdev, "netdev exists, using existing without changing its parameters"); + else if (r < 0) { + log_netdev_warning_errno(netdev, r, "netdev could not be created: %m"); + netdev_enter_failed(netdev); + + return 1; + } + + log_netdev_debug(netdev, "Created"); + + return 1; +} + +int netdev_set_ifindex(NetDev *netdev, sd_netlink_message *message) { + uint16_t type; + const char *kind; + const char *received_kind; + const char *received_name; + int r, ifindex; + + assert(netdev); + assert(message); + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not get rtnl message type: %m"); + + if (type != RTM_NEWLINK) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), "Cannot set ifindex from unexpected rtnl message type."); + + r = sd_rtnl_message_link_get_ifindex(message, &ifindex); + if (r < 0) { + log_netdev_error_errno(netdev, r, "Could not get ifindex: %m"); + netdev_enter_failed(netdev); + return r; + } else if (ifindex <= 0) { + log_netdev_error(netdev, "Got invalid ifindex: %d", ifindex); + netdev_enter_failed(netdev); + return -EINVAL; + } + + if (netdev->ifindex > 0) { + if (netdev->ifindex != ifindex) { + log_netdev_error(netdev, "Could not set ifindex to %d, already set to %d", + ifindex, netdev->ifindex); + netdev_enter_failed(netdev); + return -EEXIST; + } else + /* ifindex already set to the same for this netdev */ + return 0; + } + + r = sd_netlink_message_read_string(message, IFLA_IFNAME, &received_name); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not get IFNAME: %m"); + + if (!streq(netdev->ifname, received_name)) { + log_netdev_error(netdev, "Received newlink with wrong IFNAME %s", received_name); + netdev_enter_failed(netdev); + return -EINVAL; + } + + if (!NETDEV_VTABLE(netdev)->skip_netdev_kind_check) { + + r = sd_netlink_message_enter_container(message, IFLA_LINKINFO); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not get LINKINFO: %m"); + + r = sd_netlink_message_read_string(message, IFLA_INFO_KIND, &received_kind); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not get KIND: %m"); + + r = sd_netlink_message_exit_container(message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not exit container: %m"); + + if (netdev->kind == NETDEV_KIND_TAP) + /* the kernel does not distinguish between tun and tap */ + kind = "tun"; + else { + kind = netdev_kind_to_string(netdev->kind); + if (!kind) { + log_netdev_error(netdev, "Could not get kind"); + netdev_enter_failed(netdev); + return -EINVAL; + } + } + + if (!streq(kind, received_kind)) { + log_netdev_error(netdev, "Received newlink with wrong KIND %s, expected %s", + received_kind, kind); + netdev_enter_failed(netdev); + return -EINVAL; + } + } + + netdev->ifindex = ifindex; + + log_netdev_debug(netdev, "netdev has index %d", netdev->ifindex); + + netdev_enter_ready(netdev); + + return 0; +} + +#define HASH_KEY SD_ID128_MAKE(52,e1,45,bd,00,6f,29,96,21,c6,30,6d,83,71,04,48) + +int netdev_generate_hw_addr( + NetDev *netdev, + Link *parent, + const char *name, + const struct hw_addr_data *hw_addr, + struct hw_addr_data *ret) { + + struct hw_addr_data a = HW_ADDR_NULL; + bool is_static = false; + int r; + + assert(netdev); + assert(name); + assert(hw_addr); + assert(ret); + + if (hw_addr_equal(hw_addr, &HW_ADDR_NONE)) { + *ret = HW_ADDR_NULL; + return 0; + } + + if (hw_addr->length == 0) { + uint64_t result; + + /* HardwareAddress= is not specified. */ + + if (!NETDEV_VTABLE(netdev)->generate_mac) + goto finalize; + + if (!IN_SET(NETDEV_VTABLE(netdev)->iftype, ARPHRD_ETHER, ARPHRD_INFINIBAND)) + goto finalize; + + r = net_get_unique_predictable_data_from_name(name, &HASH_KEY, &result); + if (r < 0) { + log_netdev_warning_errno(netdev, r, + "Failed to generate persistent MAC address, ignoring: %m"); + goto finalize; + } + + a.length = arphrd_to_hw_addr_len(NETDEV_VTABLE(netdev)->iftype); + + switch (NETDEV_VTABLE(netdev)->iftype) { + case ARPHRD_ETHER: + assert(a.length <= sizeof(result)); + memcpy(a.bytes, &result, a.length); + + if (ether_addr_is_null(&a.ether) || ether_addr_is_broadcast(&a.ether)) { + log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "Failed to generate persistent MAC address, ignoring: %m"); + a = HW_ADDR_NULL; + goto finalize; + } + + break; + case ARPHRD_INFINIBAND: + if (result == 0) { + log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "Failed to generate persistent MAC address: %m"); + goto finalize; + } + + assert(a.length >= sizeof(result)); + memzero(a.bytes, a.length - sizeof(result)); + memcpy(a.bytes + a.length - sizeof(result), &result, sizeof(result)); + break; + default: + assert_not_reached(); + } + + } else { + a = *hw_addr; + is_static = true; + } + + r = net_verify_hardware_address(name, is_static, NETDEV_VTABLE(netdev)->iftype, + parent ? &parent->hw_addr : NULL, &a); + if (r < 0) + return r; + +finalize: + *ret = a; + return 0; +} + +static int netdev_create_message(NetDev *netdev, Link *link, sd_netlink_message *m) { + int r; + + r = sd_netlink_message_append_string(m, IFLA_IFNAME, netdev->ifname); + if (r < 0) + return r; + + struct hw_addr_data hw_addr; + r = netdev_generate_hw_addr(netdev, link, netdev->ifname, &netdev->hw_addr, &hw_addr); + if (r < 0) + return r; + + if (hw_addr.length > 0) { + log_netdev_debug(netdev, "Using MAC address: %s", HW_ADDR_TO_STR(&hw_addr)); + r = netlink_message_append_hw_addr(m, IFLA_ADDRESS, &hw_addr); + if (r < 0) + return r; + } + + if (netdev->mtu != 0) { + r = sd_netlink_message_append_u32(m, IFLA_MTU, netdev->mtu); + if (r < 0) + return r; + } + + if (link) { + r = sd_netlink_message_append_u32(m, IFLA_LINK, link->ifindex); + if (r < 0) + return r; + } + + r = sd_netlink_message_open_container(m, IFLA_LINKINFO); + if (r < 0) + return r; + + if (NETDEV_VTABLE(netdev)->fill_message_create) { + r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, netdev_kind_to_string(netdev->kind)); + if (r < 0) + return r; + + r = NETDEV_VTABLE(netdev)->fill_message_create(netdev, link, m); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + } else { + r = sd_netlink_message_append_string(m, IFLA_INFO_KIND, netdev_kind_to_string(netdev->kind)); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + return 0; +} + +static int independent_netdev_create(NetDev *netdev) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(netdev); + + /* create netdev */ + if (NETDEV_VTABLE(netdev)->create) { + r = NETDEV_VTABLE(netdev)->create(netdev); + if (r < 0) + return r; + + log_netdev_debug(netdev, "Created"); + return 0; + } + + r = sd_rtnl_message_new_link(netdev->manager->rtnl, &m, RTM_NEWLINK, 0); + if (r < 0) + return r; + + r = netdev_create_message(netdev, NULL, m); + if (r < 0) + return r; + + r = netlink_call_async(netdev->manager->rtnl, NULL, m, netdev_create_handler, + netdev_destroy_callback, netdev); + if (r < 0) + return r; + + netdev_ref(netdev); + + netdev->state = NETDEV_STATE_CREATING; + log_netdev_debug(netdev, "Creating"); + return 0; +} + +static int stacked_netdev_create(NetDev *netdev, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(netdev); + assert(netdev->manager); + assert(link); + assert(req); + + r = sd_rtnl_message_new_link(netdev->manager->rtnl, &m, RTM_NEWLINK, 0); + if (r < 0) + return r; + + r = netdev_create_message(netdev, link, m); + if (r < 0) + return r; + + r = request_call_netlink_async(netdev->manager->rtnl, m, req); + if (r < 0) + return r; + + netdev->state = NETDEV_STATE_CREATING; + log_netdev_debug(netdev, "Creating"); + return 0; +} + +static bool link_is_ready_to_create_stacked_netdev_one(Link *link, bool allow_unmanaged) { + assert(link); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED, LINK_STATE_UNMANAGED)) + return false; + + if (!link->network) + return allow_unmanaged; + + if (link->set_link_messages > 0) + return false; + + /* If stacked netdevs are created before the underlying interface being activated, then + * the activation policy for the netdevs are ignored. See issue #22593. */ + if (!link->activated) + return false; + + return true; +} + +static bool link_is_ready_to_create_stacked_netdev(Link *link) { + return check_ready_for_all_sr_iov_ports(link, /* allow_unmanaged = */ false, + link_is_ready_to_create_stacked_netdev_one); +} + +static int netdev_is_ready_to_create(NetDev *netdev, Link *link) { + assert(netdev); + + if (netdev->state != NETDEV_STATE_LOADING) + return false; + + if (link && !link_is_ready_to_create_stacked_netdev(link)) + return false; + + if (NETDEV_VTABLE(netdev)->is_ready_to_create) + return NETDEV_VTABLE(netdev)->is_ready_to_create(netdev, link); + + return true; +} + +static int stacked_netdev_process_request(Request *req, Link *link, void *userdata) { + NetDev *netdev = ASSERT_PTR(userdata); + int r; + + assert(req); + assert(link); + + r = netdev_is_ready_to_create(netdev, link); + if (r <= 0) + return r; + + r = stacked_netdev_create(netdev, link, req); + if (r < 0) + return log_netdev_warning_errno(netdev, r, "Failed to create netdev: %m"); + + return 1; +} + +static int create_stacked_netdev_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not create stacked netdev"); + link_enter_failed(link); + return 0; + } + + if (link->create_stacked_netdev_messages == 0) { + link->stacked_netdevs_created = true; + log_link_debug(link, "Stacked netdevs created."); + link_check_ready(link); + } + + return 0; +} + +int link_request_stacked_netdev(Link *link, NetDev *netdev) { + int r; + + assert(link); + assert(netdev); + + if (!netdev_is_stacked(netdev)) + return -EINVAL; + + if (!IN_SET(netdev->state, NETDEV_STATE_LOADING, NETDEV_STATE_FAILED) || netdev->ifindex > 0) + return 0; /* Already created. */ + + link->stacked_netdevs_created = false; + r = link_queue_request_full(link, REQUEST_TYPE_NETDEV_STACKED, + netdev, (mfree_func_t) netdev_unref, + trivial_hash_func, trivial_compare_func, + stacked_netdev_process_request, + &link->create_stacked_netdev_messages, + create_stacked_netdev_handler, NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request stacked netdev '%s': %m", + netdev->ifname); + if (r == 0) + return 0; + + netdev_ref(netdev); + log_link_debug(link, "Requested stacked netdev '%s'", netdev->ifname); + return 1; +} + +static int independent_netdev_process_request(Request *req, Link *link, void *userdata) { + NetDev *netdev = ASSERT_PTR(userdata); + int r; + + assert(!link); + + r = netdev_is_ready_to_create(netdev, NULL); + if (r <= 0) + return r; + + r = independent_netdev_create(netdev); + if (r < 0) + return log_netdev_warning_errno(netdev, r, "Failed to create netdev: %m"); + + return 1; +} + +static int netdev_request_to_create(NetDev *netdev) { + int r; + + assert(netdev); + assert(netdev->manager); + + if (netdev->manager->test_mode) + return 0; + + if (netdev_is_stacked(netdev)) + return 0; + + r = netdev_is_ready_to_create(netdev, NULL); + if (r < 0) + return r; + if (r > 0) { + /* If the netdev has no dependency, then create it now. */ + r = independent_netdev_create(netdev); + if (r < 0) + return log_netdev_warning_errno(netdev, r, "Failed to create netdev: %m"); + + } else { + /* Otherwise, wait for the dependencies being resolved. */ + r = netdev_queue_request(netdev, independent_netdev_process_request, NULL); + if (r < 0) + return log_netdev_warning_errno(netdev, r, "Failed to request to create netdev: %m"); + } + + return 0; +} + +int netdev_load_one(Manager *manager, const char *filename) { + _cleanup_(netdev_unrefp) NetDev *netdev_raw = NULL, *netdev = NULL; + const char *dropin_dirname; + int r; + + assert(manager); + assert(filename); + + r = null_or_empty_path(filename); + if (r < 0) + return log_warning_errno(r, "Failed to check if \"%s\" is empty: %m", filename); + if (r > 0) { + log_debug("Skipping empty file: %s", filename); + return 0; + } + + netdev_raw = new(NetDev, 1); + if (!netdev_raw) + return log_oom(); + + *netdev_raw = (NetDev) { + .n_ref = 1, + .kind = _NETDEV_KIND_INVALID, + .state = _NETDEV_STATE_INVALID, /* an invalid state means done() of the implementation won't be called on destruction */ + }; + + dropin_dirname = strjoina(basename(filename), ".d"); + r = config_parse_many( + STRV_MAKE_CONST(filename), NETWORK_DIRS, dropin_dirname, /* root = */ NULL, + NETDEV_COMMON_SECTIONS NETDEV_OTHER_SECTIONS, + config_item_perf_lookup, network_netdev_gperf_lookup, + CONFIG_PARSE_WARN, + netdev_raw, + NULL, + NULL); + if (r < 0) + return r; /* config_parse_many() logs internally. */ + + /* skip out early if configuration does not match the environment */ + if (!condition_test_list(netdev_raw->conditions, environ, NULL, NULL, NULL)) { + log_debug("%s: Conditions in the file do not match the system environment, skipping.", filename); + return 0; + } + + if (netdev_raw->kind == _NETDEV_KIND_INVALID) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "NetDev has no Kind= configured in \"%s\", ignoring.", filename); + + if (!netdev_raw->ifname) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "NetDev without Name= configured in \"%s\", ignoring.", filename); + + netdev = malloc0(NETDEV_VTABLE(netdev_raw)->object_size); + if (!netdev) + return log_oom(); + + netdev->n_ref = 1; + netdev->manager = manager; + netdev->kind = netdev_raw->kind; + netdev->state = NETDEV_STATE_LOADING; /* we initialize the state here for the first time, + so that done() will be called on destruction */ + + if (NETDEV_VTABLE(netdev)->init) + NETDEV_VTABLE(netdev)->init(netdev); + + r = config_parse_many( + STRV_MAKE_CONST(filename), NETWORK_DIRS, dropin_dirname, /* root = */ NULL, + NETDEV_VTABLE(netdev)->sections, + config_item_perf_lookup, network_netdev_gperf_lookup, + CONFIG_PARSE_WARN, + netdev, NULL, NULL); + if (r < 0) + return r; /* config_parse_many() logs internally. */ + + /* verify configuration */ + if (NETDEV_VTABLE(netdev)->config_verify) { + r = NETDEV_VTABLE(netdev)->config_verify(netdev, filename); + if (r < 0) + return r; /* config_verify() logs internally. */ + } + + netdev->filename = strdup(filename); + if (!netdev->filename) + return log_oom(); + + r = hashmap_ensure_put(&netdev->manager->netdevs, &string_hash_ops, netdev->ifname, netdev); + if (r == -ENOMEM) + return log_oom(); + if (r == -EEXIST) { + NetDev *n = hashmap_get(netdev->manager->netdevs, netdev->ifname); + + assert(n); + if (!streq(netdev->filename, n->filename)) + log_netdev_warning_errno(netdev, r, + "Device was already configured by \"%s\", ignoring %s.", + n->filename, netdev->filename); + + /* Clear ifname before netdev_free() is called. Otherwise, the NetDev object 'n' is + * removed from the hashmap 'manager->netdevs'. */ + netdev->ifname = mfree(netdev->ifname); + return -EEXIST; + } + assert(r > 0); + + log_netdev_debug(netdev, "loaded \"%s\"", netdev_kind_to_string(netdev->kind)); + + r = netdev_request_to_create(netdev); + if (r < 0) + return r; /* netdev_request_to_create() logs internally. */ + + TAKE_PTR(netdev); + return 0; +} + +int netdev_load(Manager *manager, bool reload) { + _cleanup_strv_free_ char **files = NULL; + int r; + + assert(manager); + + if (!reload) + hashmap_clear_with_destructor(manager->netdevs, netdev_unref); + + r = conf_files_list_strv(&files, ".netdev", NULL, 0, NETWORK_DIRS); + if (r < 0) + return log_error_errno(r, "Failed to enumerate netdev files: %m"); + + STRV_FOREACH(f, files) + (void) netdev_load_one(manager, *f); + + return 0; +} + +int config_parse_netdev_kind( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + NetDevKind k, *kind = ASSERT_PTR(data); + + assert(filename); + assert(rvalue); + + k = netdev_kind_from_string(rvalue); + if (k < 0) { + log_syntax(unit, LOG_WARNING, filename, line, k, "Failed to parse netdev kind, ignoring assignment: %s", rvalue); + return 0; + } + + if (*kind != _NETDEV_KIND_INVALID && *kind != k) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified netdev kind is different from the previous value '%s', ignoring assignment: %s", + netdev_kind_to_string(*kind), rvalue); + return 0; + } + + *kind = k; + + return 0; +} + +int config_parse_netdev_hw_addr( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + struct hw_addr_data *hw_addr = ASSERT_PTR(data); + + assert(rvalue); + + if (streq(rvalue, "none")) { + *hw_addr = HW_ADDR_NONE; + return 0; + } + + return config_parse_hw_addr(unit, filename, line, section, section_line, lvalue, ltype, rvalue, data, userdata); +} diff --git a/src/network/netdev/netdev.h b/src/network/netdev/netdev.h new file mode 100644 index 0000000..cb8cc8c --- /dev/null +++ b/src/network/netdev/netdev.h @@ -0,0 +1,261 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-netlink.h" + +#include "conf-parser.h" +#include "ether-addr-util.h" +#include "list.h" +#include "log-link.h" +#include "networkd-link.h" +#include "time-util.h" + +/* Special hardware address value to suppress generating persistent hardware address for the netdev. */ +#define HW_ADDR_NONE ((struct hw_addr_data) { .length = 1, }) + +#define NETDEV_COMMON_SECTIONS "Match\0NetDev\0" +/* This is the list of known sections. We need to ignore them in the initial parsing phase. */ +#define NETDEV_OTHER_SECTIONS \ + "-BareUDP\0" \ + "-BatmanAdvanced\0" \ + "-Bond\0" \ + "-Bridge\0" \ + "-FooOverUDP\0" \ + "-GENEVE\0" \ + "-IPoIB\0" \ + "-IPVLAN\0" \ + "-IPVTAP\0" \ + "-L2TP\0" \ + "-L2TPSession\0" \ + "-MACsec\0" \ + "-MACsecReceiveAssociation\0" \ + "-MACsecReceiveChannel\0" \ + "-MACsecTransmitAssociation\0" \ + "-MACVLAN\0" \ + "-MACVTAP\0" \ + "-Peer\0" \ + "-Tap\0" \ + "-Tun\0" \ + "-Tunnel\0" \ + "-VLAN\0" \ + "-VRF\0" \ + "-VXCAN\0" \ + "-VXLAN\0" \ + "-WLAN\0" \ + "-WireGuard\0" \ + "-WireGuardPeer\0" \ + "-Xfrm\0" + +typedef enum NetDevKind { + NETDEV_KIND_BAREUDP, + NETDEV_KIND_BATADV, + NETDEV_KIND_BOND, + NETDEV_KIND_BRIDGE, + NETDEV_KIND_DUMMY, + NETDEV_KIND_ERSPAN, + NETDEV_KIND_FOU, + NETDEV_KIND_GENEVE, + NETDEV_KIND_GRE, + NETDEV_KIND_GRETAP, + NETDEV_KIND_IFB, + NETDEV_KIND_IP6GRE, + NETDEV_KIND_IP6GRETAP, + NETDEV_KIND_IP6TNL, + NETDEV_KIND_IPIP, + NETDEV_KIND_IPOIB, + NETDEV_KIND_IPVLAN, + NETDEV_KIND_IPVTAP, + NETDEV_KIND_L2TP, + NETDEV_KIND_MACSEC, + NETDEV_KIND_MACVLAN, + NETDEV_KIND_MACVTAP, + NETDEV_KIND_NETDEVSIM, + NETDEV_KIND_NLMON, + NETDEV_KIND_SIT, + NETDEV_KIND_TAP, + NETDEV_KIND_TUN, + NETDEV_KIND_VCAN, + NETDEV_KIND_VETH, + NETDEV_KIND_VLAN, + NETDEV_KIND_VRF, + NETDEV_KIND_VTI, + NETDEV_KIND_VTI6, + NETDEV_KIND_VXCAN, + NETDEV_KIND_VXLAN, + NETDEV_KIND_WIREGUARD, + NETDEV_KIND_WLAN, + NETDEV_KIND_XFRM, + _NETDEV_KIND_MAX, + _NETDEV_KIND_TUNNEL, /* Used by config_parse_stacked_netdev() */ + _NETDEV_KIND_INVALID = -EINVAL, +} NetDevKind; + +typedef enum NetDevState { + NETDEV_STATE_LOADING, + NETDEV_STATE_FAILED, + NETDEV_STATE_CREATING, + NETDEV_STATE_READY, + NETDEV_STATE_LINGER, + _NETDEV_STATE_MAX, + _NETDEV_STATE_INVALID = -EINVAL, +} NetDevState; + +typedef enum NetDevCreateType { + NETDEV_CREATE_INDEPENDENT, + NETDEV_CREATE_STACKED, + _NETDEV_CREATE_MAX, + _NETDEV_CREATE_INVALID = -EINVAL, +} NetDevCreateType; + +typedef struct Manager Manager; +typedef struct Condition Condition; + +typedef struct NetDev { + Manager *manager; + + unsigned n_ref; + + char *filename; + + LIST_HEAD(Condition, conditions); + + NetDevState state; + NetDevKind kind; + char *description; + char *ifname; + struct hw_addr_data hw_addr; + uint32_t mtu; + int ifindex; +} NetDev; + +typedef struct NetDevVTable { + /* How much memory does an object of this unit type need */ + size_t object_size; + + /* Config file sections this netdev kind understands, separated + * by NUL chars */ + const char *sections; + + /* This should reset all type-specific variables. This should + * not allocate memory, and is called with zero-initialized + * data. It should hence only initialize variables that need + * to be set != 0. */ + void (*init)(NetDev *n); + + /* This is called when the interface is removed. */ + void (*drop)(NetDev *n); + + /* This should free all kind-specific variables. It should be + * idempotent. */ + void (*done)(NetDev *n); + + /* fill in message to create netdev */ + int (*fill_message_create)(NetDev *netdev, Link *link, sd_netlink_message *message); + + /* specifies if netdev is independent, or a master device or a stacked device */ + NetDevCreateType create_type; + + /* This is used for stacked netdev. Return true when the underlying link is ready. */ + int (*is_ready_to_create)(NetDev *netdev, Link *link); + + /* create netdev, if not done via rtnl */ + int (*create)(NetDev *netdev); + + /* perform additional configuration after netdev has been createad */ + int (*post_create)(NetDev *netdev, Link *link); + + /* verify that compulsory configuration options were specified */ + int (*config_verify)(NetDev *netdev, const char *filename); + + /* expected iftype, e.g. ARPHRD_ETHER. */ + uint16_t iftype; + + /* Generate MAC address when MACAddress= is not specified. */ + bool generate_mac; + + /* When assigning ifindex to the netdev, skip to check if the netdev kind matches. */ + bool skip_netdev_kind_check; +} NetDevVTable; + +extern const NetDevVTable * const netdev_vtable[_NETDEV_KIND_MAX]; + +#define NETDEV_VTABLE(n) ((n)->kind != _NETDEV_KIND_INVALID ? netdev_vtable[(n)->kind] : NULL) + +/* For casting a netdev into the various netdev kinds */ +#define DEFINE_NETDEV_CAST(UPPERCASE, MixedCase) \ + static inline MixedCase* UPPERCASE(NetDev *n) { \ + assert(n); \ + assert(n->kind == NETDEV_KIND_##UPPERCASE); \ + assert(n->state < _NETDEV_STATE_MAX); \ + \ + return (MixedCase*) n; \ + } + +/* For casting the various netdev kinds into a netdev */ +#define NETDEV(n) (&(n)->meta) + +int netdev_load(Manager *manager, bool reload); +int netdev_load_one(Manager *manager, const char *filename); +void netdev_drop(NetDev *netdev); +void netdev_enter_failed(NetDev *netdev); + +NetDev *netdev_unref(NetDev *netdev); +NetDev *netdev_ref(NetDev *netdev); +DEFINE_TRIVIAL_DESTRUCTOR(netdev_destroy_callback, NetDev, netdev_unref); +DEFINE_TRIVIAL_CLEANUP_FUNC(NetDev*, netdev_unref); + +bool netdev_is_managed(NetDev *netdev); +int netdev_get(Manager *manager, const char *name, NetDev **ret); +int netdev_set_ifindex(NetDev *netdev, sd_netlink_message *newlink); +int netdev_generate_hw_addr(NetDev *netdev, Link *link, const char *name, + const struct hw_addr_data *hw_addr, struct hw_addr_data *ret); + +int link_request_stacked_netdev(Link *link, NetDev *netdev); + +const char *netdev_kind_to_string(NetDevKind d) _const_; +NetDevKind netdev_kind_from_string(const char *d) _pure_; + +static inline NetDevCreateType netdev_get_create_type(NetDev *netdev) { + assert(netdev); + assert(NETDEV_VTABLE(netdev)); + + return NETDEV_VTABLE(netdev)->create_type; +} + +CONFIG_PARSER_PROTOTYPE(config_parse_netdev_kind); +CONFIG_PARSER_PROTOTYPE(config_parse_netdev_hw_addr); + +/* gperf */ +const struct ConfigPerfItem* network_netdev_gperf_lookup(const char *key, GPERF_LEN_TYPE length); + +/* Macros which append INTERFACE= to the message */ + +#define log_netdev_full_errno_zerook(netdev, level, error, ...) \ + ({ \ + const NetDev *_n = (netdev); \ + log_interface_full_errno_zerook(_n ? _n->ifname : NULL, level, error, __VA_ARGS__); \ + }) + +#define log_netdev_full_errno(netdev, level, error, ...) \ + ({ \ + int _error = (error); \ + ASSERT_NON_ZERO(_error); \ + log_netdev_full_errno_zerook(netdev, level, _error, __VA_ARGS__); \ + }) + +#define log_netdev_full(netdev, level, ...) (void) log_netdev_full_errno_zerook(netdev, level, 0, __VA_ARGS__) + +#define log_netdev_debug(netdev, ...) log_netdev_full(netdev, LOG_DEBUG, __VA_ARGS__) +#define log_netdev_info(netdev, ...) log_netdev_full(netdev, LOG_INFO, __VA_ARGS__) +#define log_netdev_notice(netdev, ...) log_netdev_full(netdev, LOG_NOTICE, __VA_ARGS__) +#define log_netdev_warning(netdev, ...) log_netdev_full(netdev, LOG_WARNING, __VA_ARGS__) +#define log_netdev_error(netdev, ...) log_netdev_full(netdev, LOG_ERR, __VA_ARGS__) + +#define log_netdev_debug_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_DEBUG, error, __VA_ARGS__) +#define log_netdev_info_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_INFO, error, __VA_ARGS__) +#define log_netdev_notice_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_NOTICE, error, __VA_ARGS__) +#define log_netdev_warning_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_WARNING, error, __VA_ARGS__) +#define log_netdev_error_errno(netdev, error, ...) log_netdev_full_errno(netdev, LOG_ERR, error, __VA_ARGS__) + +#define LOG_NETDEV_MESSAGE(netdev, fmt, ...) "MESSAGE=%s: " fmt, (netdev)->ifname, ##__VA_ARGS__ +#define LOG_NETDEV_INTERFACE(netdev) "INTERFACE=%s", (netdev)->ifname diff --git a/src/network/netdev/netdevsim.c b/src/network/netdev/netdevsim.c new file mode 100644 index 0000000..15d5c13 --- /dev/null +++ b/src/network/netdev/netdevsim.c @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if_arp.h> + +#include "netdevsim.h" + +const NetDevVTable netdevsim_vtable = { + .object_size = sizeof(NetDevSim), + .sections = NETDEV_COMMON_SECTIONS, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/netdevsim.h b/src/network/netdev/netdevsim.h new file mode 100644 index 0000000..27adc59 --- /dev/null +++ b/src/network/netdev/netdevsim.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct NetDevSim NetDevSim; + +#include "netdev.h" + +struct NetDevSim { + NetDev meta; +}; + +DEFINE_NETDEV_CAST(NETDEVSIM, NetDevSim); +extern const NetDevVTable netdevsim_vtable; diff --git a/src/network/netdev/nlmon.c b/src/network/netdev/nlmon.c new file mode 100644 index 0000000..ff37209 --- /dev/null +++ b/src/network/netdev/nlmon.c @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if_arp.h> + +#include "nlmon.h" + +static int netdev_nlmon_verify(NetDev *netdev, const char *filename) { + assert(netdev); + assert(filename); + + if (netdev->hw_addr.length > 0) { + log_netdev_warning(netdev, "%s: MACAddress= is not supported. Ignoring", filename); + netdev->hw_addr = HW_ADDR_NULL; + } + + return 0; +} + +const NetDevVTable nlmon_vtable = { + .object_size = sizeof(NLMon), + .sections = NETDEV_COMMON_SECTIONS, + .create_type = NETDEV_CREATE_INDEPENDENT, + .config_verify = netdev_nlmon_verify, + .iftype = ARPHRD_NETLINK, +}; diff --git a/src/network/netdev/nlmon.h b/src/network/netdev/nlmon.h new file mode 100644 index 0000000..edfc504 --- /dev/null +++ b/src/network/netdev/nlmon.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct NLMon NLMon; + +#include "netdev.h" + +struct NLMon { + NetDev meta; +}; + +DEFINE_NETDEV_CAST(NLMON, NLMon); + +extern const NetDevVTable nlmon_vtable; diff --git a/src/network/netdev/tunnel.c b/src/network/netdev/tunnel.c new file mode 100644 index 0000000..db84e7c --- /dev/null +++ b/src/network/netdev/tunnel.c @@ -0,0 +1,1242 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/fou.h> +#include <linux/if_arp.h> +#include <linux/if_tunnel.h> +#include <linux/ip.h> +#include <linux/ip6_tunnel.h> + +#include "af-list.h" +#include "conf-parser.h" +#include "hexdecoct.h" +#include "missing_network.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "parse-util.h" +#include "siphash24.h" +#include "string-table.h" +#include "string-util.h" +#include "tunnel.h" + +#define DEFAULT_IPV6_TTL 64 +#define IP6_FLOWINFO_FLOWLABEL htobe32(0x000FFFFF) +#define IP6_TNL_F_ALLOW_LOCAL_REMOTE 0x40 + +static const char* const ip6tnl_mode_table[_NETDEV_IP6_TNL_MODE_MAX] = { + [NETDEV_IP6_TNL_MODE_IP6IP6] = "ip6ip6", + [NETDEV_IP6_TNL_MODE_IPIP6] = "ipip6", + [NETDEV_IP6_TNL_MODE_ANYIP6] = "any", +}; + +DEFINE_STRING_TABLE_LOOKUP(ip6tnl_mode, Ip6TnlMode); +DEFINE_CONFIG_PARSE_ENUM(config_parse_ip6tnl_mode, ip6tnl_mode, Ip6TnlMode, "Failed to parse ip6 tunnel Mode"); + +#define HASH_KEY SD_ID128_MAKE(74,c4,de,12,f3,d9,41,34,bb,3d,c1,a4,42,93,50,87) + +int dhcp4_pd_create_6rd_tunnel_name(Link *link, char **ret) { + _cleanup_free_ char *ifname_alloc = NULL; + uint8_t ipv4masklen, sixrd_prefixlen, *buf, *p; + struct in_addr ipv4address; + struct in6_addr sixrd_prefix; + char ifname[IFNAMSIZ]; + uint64_t result; + size_t sz; + int r; + + assert(link); + assert(link->dhcp_lease); + + r = sd_dhcp_lease_get_address(link->dhcp_lease, &ipv4address); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to get DHCPv4 address: %m"); + + r = sd_dhcp_lease_get_6rd(link->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, NULL, NULL); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to get 6rd option: %m"); + + sz = sizeof(uint8_t) * 2 + sizeof(struct in6_addr) + sizeof(struct in_addr); + buf = newa(uint8_t, sz); + p = buf; + p = mempcpy(p, &ipv4masklen, sizeof(uint8_t)); + p = mempcpy(p, &ipv4address, sizeof(struct in_addr)); + p = mempcpy(p, &sixrd_prefixlen, sizeof(uint8_t)); + p = mempcpy(p, &sixrd_prefix, sizeof(struct in6_addr)); + + result = siphash24(buf, sz, HASH_KEY.bytes); + memcpy(ifname, "6rd-", STRLEN("6rd-")); + ifname[STRLEN("6rd-") ] = urlsafe_base64char(result >> 54); + ifname[STRLEN("6rd-") + 1] = urlsafe_base64char(result >> 48); + ifname[STRLEN("6rd-") + 2] = urlsafe_base64char(result >> 42); + ifname[STRLEN("6rd-") + 3] = urlsafe_base64char(result >> 36); + ifname[STRLEN("6rd-") + 4] = urlsafe_base64char(result >> 30); + ifname[STRLEN("6rd-") + 5] = urlsafe_base64char(result >> 24); + ifname[STRLEN("6rd-") + 6] = urlsafe_base64char(result >> 18); + ifname[STRLEN("6rd-") + 7] = urlsafe_base64char(result >> 12); + ifname[STRLEN("6rd-") + 8] = urlsafe_base64char(result >> 6); + ifname[STRLEN("6rd-") + 9] = urlsafe_base64char(result); + ifname[STRLEN("6rd-") + 10] = '\0'; + assert_cc(STRLEN("6rd-") + 10 <= IFNAMSIZ); + + ifname_alloc = strdup(ifname); + if (!ifname_alloc) + return log_oom_debug(); + + *ret = TAKE_PTR(ifname_alloc); + return 0; +} + +static int dhcp4_pd_create_6rd_tunnel_message( + Link *link, + sd_netlink_message *m, + const struct in_addr *ipv4address, + uint8_t ipv4masklen, + const struct in6_addr *sixrd_prefix, + uint8_t sixrd_prefixlen) { + int r; + + r = sd_netlink_message_append_string(m, IFLA_IFNAME, link->dhcp4_6rd_tunnel_name); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(m, IFLA_LINKINFO); + if (r < 0) + return r; + + r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, "sit"); + if (r < 0) + return r; + + r = sd_netlink_message_append_in_addr(m, IFLA_IPTUN_LOCAL, ipv4address); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_IPTUN_TTL, 64); + if (r < 0) + return r; + + r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_6RD_PREFIX, sixrd_prefix); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_IPTUN_6RD_PREFIXLEN, sixrd_prefixlen); + if (r < 0) + return r; + + struct in_addr relay_prefix = *ipv4address; + (void) in4_addr_mask(&relay_prefix, ipv4masklen); + r = sd_netlink_message_append_u32(m, IFLA_IPTUN_6RD_RELAY_PREFIX, relay_prefix.s_addr); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_IPTUN_6RD_RELAY_PREFIXLEN, ipv4masklen); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + return 0; +} + +int dhcp4_pd_create_6rd_tunnel(Link *link, link_netlink_message_handler_t callback) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + uint8_t ipv4masklen, sixrd_prefixlen; + struct in_addr ipv4address; + struct in6_addr sixrd_prefix; + int r; + + assert(link); + assert(link->ifindex > 0); + assert(link->manager); + assert(link->dhcp_lease); + assert(link->dhcp4_6rd_tunnel_name); + assert(callback); + + r = sd_dhcp_lease_get_address(link->dhcp_lease, &ipv4address); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to get DHCPv4 address: %m"); + + r = sd_dhcp_lease_get_6rd(link->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, NULL, NULL); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to get 6rd option: %m"); + + r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_NEWLINK, 0); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to create netlink message: %m"); + + r = dhcp4_pd_create_6rd_tunnel_message(link, m, + &ipv4address, ipv4masklen, + &sixrd_prefix, sixrd_prefixlen); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to fill netlink message: %m"); + + r = netlink_call_async(link->manager->rtnl, NULL, m, callback, + link_netlink_destroy_callback, link); + if (r < 0) + return log_link_debug_errno(link, r, "Could not send netlink message: %m"); + + link_ref(link); + + return 0; +} + +static int tunnel_get_local_address(Tunnel *t, Link *link, union in_addr_union *ret) { + assert(t); + + if (t->local_type < 0) { + if (ret) + *ret = t->local; + return 0; + } + + return link_get_local_address(link, t->local_type, t->family, NULL, ret); +} + +static int netdev_ipip_sit_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(m); + + union in_addr_union local; + Tunnel *t = ASSERT_PTR(netdev)->kind == NETDEV_KIND_IPIP ? IPIP(netdev) : SIT(netdev); + int r; + + if (t->external) { + r = sd_netlink_message_append_flag(m, IFLA_IPTUN_COLLECT_METADATA); + if (r < 0) + return r; + + /* If external mode is enabled, then the following settings should not be appended. */ + return 0; + } + + if (link || t->assign_to_loopback) { + r = sd_netlink_message_append_u32(m, IFLA_IPTUN_LINK, link ? link->ifindex : LOOPBACK_IFINDEX); + if (r < 0) + return r; + } + + r = tunnel_get_local_address(t, link, &local); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not find local address: %m"); + + r = sd_netlink_message_append_in_addr(m, IFLA_IPTUN_LOCAL, &local.in); + if (r < 0) + return r; + + r = sd_netlink_message_append_in_addr(m, IFLA_IPTUN_REMOTE, &t->remote.in); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_IPTUN_TTL, t->ttl); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_IPTUN_PMTUDISC, t->pmtudisc); + if (r < 0) + return r; + + if (t->fou_tunnel) { + r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_TYPE, t->fou_encap_type); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_SPORT, htobe16(t->encap_src_port)); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_IPTUN_ENCAP_DPORT, htobe16(t->fou_destination_port)); + if (r < 0) + return r; + } + + if (netdev->kind == NETDEV_KIND_SIT) { + if (t->sixrd_prefixlen > 0) { + r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_6RD_PREFIX, &t->sixrd_prefix); + if (r < 0) + return r; + + /* u16 is deliberate here, even though we're passing a netmask that can never be + * >128. The kernel is expecting to receive the prefixlen as a u16. + */ + r = sd_netlink_message_append_u16(m, IFLA_IPTUN_6RD_PREFIXLEN, t->sixrd_prefixlen); + if (r < 0) + return r; + } + + if (t->isatap >= 0) { + uint16_t flags = 0; + + SET_FLAG(flags, SIT_ISATAP, t->isatap); + + r = sd_netlink_message_append_u16(m, IFLA_IPTUN_FLAGS, flags); + if (r < 0) + return r; + } + } + + return 0; +} + +static int netdev_gre_erspan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + union in_addr_union local; + uint32_t ikey = 0; + uint32_t okey = 0; + uint16_t iflags = 0; + uint16_t oflags = 0; + Tunnel *t; + int r; + + assert(netdev); + assert(m); + + switch (netdev->kind) { + case NETDEV_KIND_GRE: + t = GRE(netdev); + break; + case NETDEV_KIND_ERSPAN: + t = ERSPAN(netdev); + break; + case NETDEV_KIND_GRETAP: + t = GRETAP(netdev); + break; + default: + assert_not_reached(); + } + + if (t->external) { + r = sd_netlink_message_append_flag(m, IFLA_GRE_COLLECT_METADATA); + if (r < 0) + return r; + + /* If external mode is enabled, then the following settings should not be appended. */ + return 0; + } + + if (link || t->assign_to_loopback) { + r = sd_netlink_message_append_u32(m, IFLA_GRE_LINK, link ? link->ifindex : LOOPBACK_IFINDEX); + if (r < 0) + return r; + } + + if (netdev->kind == NETDEV_KIND_ERSPAN) { + r = sd_netlink_message_append_u8(m, IFLA_GRE_ERSPAN_VER, t->erspan_version); + if (r < 0) + return r; + + if (t->erspan_version == 1) { + r = sd_netlink_message_append_u32(m, IFLA_GRE_ERSPAN_INDEX, t->erspan_index); + if (r < 0) + return r; + + } else if (t->erspan_version == 2) { + r = sd_netlink_message_append_u8(m, IFLA_GRE_ERSPAN_DIR, t->erspan_direction); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_GRE_ERSPAN_HWID, t->erspan_hwid); + if (r < 0) + return r; + } + } + + r = tunnel_get_local_address(t, link, &local); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not find local address: %m"); + + r = sd_netlink_message_append_in_addr(m, IFLA_GRE_LOCAL, &local.in); + if (r < 0) + return r; + + r = sd_netlink_message_append_in_addr(m, IFLA_GRE_REMOTE, &t->remote.in); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_GRE_TTL, t->ttl); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_GRE_TOS, t->tos); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_GRE_PMTUDISC, t->pmtudisc); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_GRE_IGNORE_DF, t->ignore_df); + if (r < 0) + return r; + + if (t->key != 0) { + ikey = okey = htobe32(t->key); + iflags |= GRE_KEY; + oflags |= GRE_KEY; + } + + if (t->ikey != 0) { + ikey = htobe32(t->ikey); + iflags |= GRE_KEY; + } + + if (t->okey != 0) { + okey = htobe32(t->okey); + oflags |= GRE_KEY; + } + + if (t->gre_erspan_sequence > 0) { + iflags |= GRE_SEQ; + oflags |= GRE_SEQ; + } else if (t->gre_erspan_sequence == 0) { + iflags &= ~GRE_SEQ; + oflags &= ~GRE_SEQ; + } + + r = sd_netlink_message_append_u32(m, IFLA_GRE_IKEY, ikey); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, IFLA_GRE_OKEY, okey); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_GRE_IFLAGS, iflags); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_GRE_OFLAGS, oflags); + if (r < 0) + return r; + + if (t->fou_tunnel) { + r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_TYPE, t->fou_encap_type); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_SPORT, htobe16(t->encap_src_port)); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_GRE_ENCAP_DPORT, htobe16(t->fou_destination_port)); + if (r < 0) + return r; + } + + return 0; +} + +static int netdev_ip6gre_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + union in_addr_union local; + uint32_t ikey = 0, okey = 0; + uint16_t iflags = 0, oflags = 0; + Tunnel *t; + int r; + + assert(netdev); + assert(m); + + if (netdev->kind == NETDEV_KIND_IP6GRE) + t = IP6GRE(netdev); + else + t = IP6GRETAP(netdev); + + if (t->external) { + r = sd_netlink_message_append_flag(m, IFLA_GRE_COLLECT_METADATA); + if (r < 0) + return r; + + /* If external mode is enabled, then the following settings should not be appended. */ + return 0; + } + + if (link || t->assign_to_loopback) { + r = sd_netlink_message_append_u32(m, IFLA_GRE_LINK, link ? link->ifindex : LOOPBACK_IFINDEX); + if (r < 0) + return r; + } + + r = tunnel_get_local_address(t, link, &local); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not find local address: %m"); + + r = sd_netlink_message_append_in6_addr(m, IFLA_GRE_LOCAL, &local.in6); + if (r < 0) + return r; + + r = sd_netlink_message_append_in6_addr(m, IFLA_GRE_REMOTE, &t->remote.in6); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_GRE_TTL, t->ttl); + if (r < 0) + return r; + + if (t->ipv6_flowlabel != _NETDEV_IPV6_FLOWLABEL_INVALID) { + r = sd_netlink_message_append_u32(m, IFLA_GRE_FLOWINFO, t->ipv6_flowlabel); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u32(m, IFLA_GRE_FLAGS, t->flags); + if (r < 0) + return r; + + if (t->key != 0) { + ikey = okey = htobe32(t->key); + iflags |= GRE_KEY; + oflags |= GRE_KEY; + } + + if (t->ikey != 0) { + ikey = htobe32(t->ikey); + iflags |= GRE_KEY; + } + + if (t->okey != 0) { + okey = htobe32(t->okey); + oflags |= GRE_KEY; + } + + r = sd_netlink_message_append_u32(m, IFLA_GRE_IKEY, ikey); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, IFLA_GRE_OKEY, okey); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_GRE_IFLAGS, iflags); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_GRE_OFLAGS, oflags); + if (r < 0) + return r; + + return 0; +} + +static int netdev_vti_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(netdev); + assert(m); + + union in_addr_union local; + uint32_t ikey, okey; + Tunnel *t = netdev->kind == NETDEV_KIND_VTI ? VTI(netdev) : VTI6(netdev); + int r; + + if (link || t->assign_to_loopback) { + r = sd_netlink_message_append_u32(m, IFLA_VTI_LINK, link ? link->ifindex : LOOPBACK_IFINDEX); + if (r < 0) + return r; + } + + if (t->key != 0) + ikey = okey = htobe32(t->key); + else { + ikey = htobe32(t->ikey); + okey = htobe32(t->okey); + } + + r = sd_netlink_message_append_u32(m, IFLA_VTI_IKEY, ikey); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, IFLA_VTI_OKEY, okey); + if (r < 0) + return r; + + r = tunnel_get_local_address(t, link, &local); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not find local address: %m"); + + r = netlink_message_append_in_addr_union(m, IFLA_VTI_LOCAL, t->family, &local); + if (r < 0) + return r; + + r = netlink_message_append_in_addr_union(m, IFLA_VTI_REMOTE, t->family, &t->remote); + if (r < 0) + return r; + + return 0; +} + +static int netdev_ip6tnl_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(netdev); + assert(m); + + union in_addr_union local; + uint8_t proto; + Tunnel *t = IP6TNL(netdev); + int r; + + switch (t->ip6tnl_mode) { + case NETDEV_IP6_TNL_MODE_IP6IP6: + proto = IPPROTO_IPV6; + break; + case NETDEV_IP6_TNL_MODE_IPIP6: + proto = IPPROTO_IPIP; + break; + case NETDEV_IP6_TNL_MODE_ANYIP6: + default: + proto = 0; + break; + } + + r = sd_netlink_message_append_u8(m, IFLA_IPTUN_PROTO, proto); + if (r < 0) + return r; + + if (t->external) { + r = sd_netlink_message_append_flag(m, IFLA_IPTUN_COLLECT_METADATA); + if (r < 0) + return r; + + /* If external mode is enabled, then the following settings should not be appended. */ + return 0; + } + + if (link || t->assign_to_loopback) { + r = sd_netlink_message_append_u32(m, IFLA_IPTUN_LINK, link ? link->ifindex : LOOPBACK_IFINDEX); + if (r < 0) + return r; + } + + r = tunnel_get_local_address(t, link, &local); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not find local address: %m"); + + r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_LOCAL, &local.in6); + if (r < 0) + return r; + + r = sd_netlink_message_append_in6_addr(m, IFLA_IPTUN_REMOTE, &t->remote.in6); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_IPTUN_TTL, t->ttl); + if (r < 0) + return r; + + if (t->ipv6_flowlabel != _NETDEV_IPV6_FLOWLABEL_INVALID) { + r = sd_netlink_message_append_u32(m, IFLA_IPTUN_FLOWINFO, t->ipv6_flowlabel); + if (r < 0) + return r; + } + + if (t->copy_dscp) + t->flags |= IP6_TNL_F_RCV_DSCP_COPY; + + if (t->allow_localremote >= 0) + SET_FLAG(t->flags, IP6_TNL_F_ALLOW_LOCAL_REMOTE, t->allow_localremote); + + r = sd_netlink_message_append_u32(m, IFLA_IPTUN_FLAGS, t->flags); + if (r < 0) + return r; + + if (t->encap_limit != 0) { + r = sd_netlink_message_append_u8(m, IFLA_IPTUN_ENCAP_LIMIT, t->encap_limit); + if (r < 0) + return r; + } + + return 0; +} + +static int netdev_tunnel_is_ready_to_create(NetDev *netdev, Link *link) { + assert(netdev); + + Tunnel *t = ASSERT_PTR(TUNNEL(netdev)); + + if (t->independent) + return true; + + return tunnel_get_local_address(t, link, NULL) >= 0; +} + +static int netdev_tunnel_verify(NetDev *netdev, const char *filename) { + assert(netdev); + assert(filename); + + Tunnel *t = ASSERT_PTR(TUNNEL(netdev)); + + if (netdev->kind == NETDEV_KIND_IP6TNL && + t->ip6tnl_mode == _NETDEV_IP6_TNL_MODE_INVALID) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "ip6tnl without mode configured in %s. Ignoring", filename); + + if (t->external) { + if (IN_SET(netdev->kind, NETDEV_KIND_VTI, NETDEV_KIND_VTI6)) + log_netdev_debug(netdev, "vti/vti6 tunnel do not support external mode, ignoring."); + else { + /* tunnel with external mode does not require underlying interface. */ + t->independent = true; + + /* tunnel with external mode does not require any settings checked below. */ + return 0; + } + } + + if (IN_SET(netdev->kind, NETDEV_KIND_VTI, NETDEV_KIND_IPIP, NETDEV_KIND_SIT, NETDEV_KIND_GRE) && + !IN_SET(t->family, AF_UNSPEC, AF_INET)) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "vti/ipip/sit/gre tunnel without a local/remote IPv4 address configured in %s. Ignoring", filename); + + if (IN_SET(netdev->kind, NETDEV_KIND_GRETAP, NETDEV_KIND_ERSPAN) && + (t->family != AF_INET || !in_addr_is_set(t->family, &t->remote))) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "gretap/erspan tunnel without a remote IPv4 address configured in %s. Ignoring", filename); + + if ((IN_SET(netdev->kind, NETDEV_KIND_VTI6, NETDEV_KIND_IP6TNL) && t->family != AF_INET6) || + (netdev->kind == NETDEV_KIND_IP6GRE && !IN_SET(t->family, AF_UNSPEC, AF_INET6))) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "vti6/ip6tnl/ip6gre tunnel without a local/remote IPv6 address configured in %s. Ignoring", filename); + + if (netdev->kind == NETDEV_KIND_IP6GRETAP && + (t->family != AF_INET6 || !in_addr_is_set(t->family, &t->remote))) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "ip6gretap tunnel without a remote IPv6 address configured in %s. Ignoring", filename); + + if (t->fou_tunnel && t->fou_destination_port <= 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "FooOverUDP missing port configured in %s. Ignoring", filename); + + /* netlink_message_append_in_addr_union() is used for vti/vti6. So, t->family cannot be AF_UNSPEC. */ + if (netdev->kind == NETDEV_KIND_VTI) + t->family = AF_INET; + + if (t->assign_to_loopback) + t->independent = true; + + if (t->independent && t->local_type >= 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "The local address cannot be '%s' when Independent= or AssignToLoopback= is enabled, ignoring.", + strna(netdev_local_address_type_to_string(t->local_type))); + + if (t->pmtudisc > 0 && t->ignore_df) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "IgnoreDontFragment= cannot be enabled when DiscoverPathMTU= is enabled"); + if (t->pmtudisc < 0) + t->pmtudisc = !t->ignore_df; + return 0; +} + +static int unset_local(Tunnel *t) { + assert(t); + + /* Unset the previous assignment. */ + t->local = IN_ADDR_NULL; + t->local_type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID; + + /* If the remote address is not specified, also clear the address family. */ + if (!in_addr_is_set(t->family, &t->remote)) + t->family = AF_UNSPEC; + + return 0; +} + +int config_parse_tunnel_local_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + union in_addr_union buffer = IN_ADDR_NULL; + NetDevLocalAddressType type; + Tunnel *t = ASSERT_PTR(userdata); + int r, f; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue) || streq(rvalue, "any")) + return unset_local(t); + + type = netdev_local_address_type_from_string(rvalue); + if (IN_SET(type, NETDEV_LOCAL_ADDRESS_IPV4LL, NETDEV_LOCAL_ADDRESS_DHCP4)) + f = AF_INET; + else if (IN_SET(type, NETDEV_LOCAL_ADDRESS_IPV6LL, NETDEV_LOCAL_ADDRESS_DHCP6, NETDEV_LOCAL_ADDRESS_SLAAC)) + f = AF_INET6; + else { + type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID; + r = in_addr_from_string_auto(rvalue, &f, &buffer); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Tunnel address \"%s\" invalid, ignoring assignment: %m", rvalue); + return 0; + } + + if (in_addr_is_null(f, &buffer)) + return unset_local(t); + } + + if (t->family != AF_UNSPEC && t->family != f) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Address family does not match the previous assignment, ignoring assignment: %s", rvalue); + return 0; + } + + t->family = f; + t->local = buffer; + t->local_type = type; + return 0; +} + +static int unset_remote(Tunnel *t) { + assert(t); + + /* Unset the previous assignment. */ + t->remote = IN_ADDR_NULL; + + /* If the local address is not specified, also clear the address family. */ + if (t->local_type == _NETDEV_LOCAL_ADDRESS_TYPE_INVALID && + !in_addr_is_set(t->family, &t->local)) + t->family = AF_UNSPEC; + + return 0; +} + +int config_parse_tunnel_remote_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + union in_addr_union buffer; + Tunnel *t = ASSERT_PTR(userdata); + int r, f; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue) || streq(rvalue, "any")) + return unset_remote(t); + + r = in_addr_from_string_auto(rvalue, &f, &buffer); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Tunnel address \"%s\" invalid, ignoring assignment: %m", rvalue); + return 0; + } + + if (in_addr_is_null(f, &buffer)) + return unset_remote(t); + + if (t->family != AF_UNSPEC && t->family != f) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Address family does not match the previous assignment, ignoring assignment: %s", rvalue); + return 0; + } + + t->family = f; + t->remote = buffer; + return 0; +} + +int config_parse_tunnel_key( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint32_t *dest = ASSERT_PTR(data), k; + union in_addr_union buffer; + int r; + + assert(filename); + assert(rvalue); + + r = in_addr_from_string(AF_INET, rvalue, &buffer); + if (r < 0) { + r = safe_atou32(rvalue, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse tunnel key ignoring assignment: %s", rvalue); + return 0; + } + } else + k = be32toh(buffer.in.s_addr); + + *dest = k; + return 0; +} + +int config_parse_ipv6_flowlabel( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Tunnel *t = ASSERT_PTR(userdata); + uint32_t k; + int r; + + assert(filename); + assert(rvalue); + + if (streq(rvalue, "inherit")) { + t->ipv6_flowlabel = IP6_FLOWINFO_FLOWLABEL; + t->flags |= IP6_TNL_F_USE_ORIG_FLOWLABEL; + return 0; + } + + r = config_parse_uint32_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, 0xFFFFF, true, + &k); + if (r <= 0) + return r; + t->ipv6_flowlabel = htobe32(k) & IP6_FLOWINFO_FLOWLABEL; + t->flags &= ~IP6_TNL_F_USE_ORIG_FLOWLABEL; + + return 0; +} + +int config_parse_encap_limit( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(rvalue); + + Tunnel *t = ASSERT_PTR(userdata); + int r; + + if (streq(rvalue, "none")) { + t->encap_limit = 0; + t->flags |= IP6_TNL_F_IGN_ENCAP_LIMIT; + return 0; + } + + r = config_parse_uint8_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, UINT8_MAX, true, + &t->encap_limit); + if (r <= 0) + return r; + t->flags &= ~IP6_TNL_F_IGN_ENCAP_LIMIT; + + return 0; +} + +int config_parse_6rd_prefix( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Tunnel *t = userdata; + union in_addr_union p; + uint8_t l; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = in_addr_prefix_from_string(rvalue, AF_INET6, &p, &l); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse 6rd prefix \"%s\", ignoring: %m", rvalue); + return 0; + } + if (l == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "6rd prefix length of \"%s\" must be greater than zero, ignoring", rvalue); + return 0; + } + + t->sixrd_prefix = p.in6; + t->sixrd_prefixlen = l; + + return 0; +} + +int config_parse_erspan_version( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + + uint8_t *v = ASSERT_PTR(data); + + if (isempty(rvalue)) { + *v = 1; /* defaults to 1 */ + return 0; + } + + return config_parse_uint8_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, 2, true, + v); +} + +int config_parse_erspan_index( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + + uint32_t *v = ASSERT_PTR(data); + + if (isempty(rvalue)) { + *v = 0; /* defaults to 0 */ + return 0; + } + + return config_parse_uint32_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, 0x100000 - 1, true, + v); +} + +int config_parse_erspan_direction( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + + uint8_t *v = ASSERT_PTR(data); + + if (isempty(rvalue) || streq(rvalue, "ingress")) + *v = 0; /* defaults to ingress */ + else if (streq(rvalue, "egress")) + *v = 1; + else + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid erspan direction \"%s\", which must be \"ingress\" or \"egress\", ignoring.", rvalue); + + return 0; +} + +int config_parse_erspan_hwid( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + + uint16_t *v = ASSERT_PTR(data); + + if (isempty(rvalue)) { + *v = 0; /* defaults to 0 */ + return 0; + } + + return config_parse_uint16_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, 63, true, + v); +} + +static void netdev_tunnel_init(NetDev *netdev) { + Tunnel *t = ASSERT_PTR(TUNNEL(netdev)); + + t->local_type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID; + t->pmtudisc = -1; + t->fou_encap_type = NETDEV_FOO_OVER_UDP_ENCAP_DIRECT; + t->isatap = -1; + t->gre_erspan_sequence = -1; + t->encap_limit = IPV6_DEFAULT_TNL_ENCAP_LIMIT; + t->ip6tnl_mode = _NETDEV_IP6_TNL_MODE_INVALID; + t->ipv6_flowlabel = _NETDEV_IPV6_FLOWLABEL_INVALID; + t->allow_localremote = -1; + t->erspan_version = 1; + + if (IN_SET(netdev->kind, NETDEV_KIND_IP6GRE, NETDEV_KIND_IP6GRETAP, NETDEV_KIND_IP6TNL)) + t->ttl = DEFAULT_IPV6_TTL; +} + +const NetDevVTable ipip_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_ipip_sit_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_TUNNEL, +}; + +const NetDevVTable sit_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_ipip_sit_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_SIT, +}; + +const NetDevVTable vti_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_vti_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_TUNNEL, +}; + +const NetDevVTable vti6_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_vti_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_TUNNEL6, +}; + +const NetDevVTable gre_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_gre_erspan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_IPGRE, +}; + +const NetDevVTable gretap_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_gre_erspan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; + +const NetDevVTable ip6gre_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_ip6gre_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_IP6GRE, +}; + +const NetDevVTable ip6gretap_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_ip6gre_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; + +const NetDevVTable ip6tnl_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_ip6tnl_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_TUNNEL6, +}; + +const NetDevVTable erspan_vtable = { + .object_size = sizeof(Tunnel), + .init = netdev_tunnel_init, + .sections = NETDEV_COMMON_SECTIONS "Tunnel\0", + .fill_message_create = netdev_gre_erspan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_tunnel_is_ready_to_create, + .config_verify = netdev_tunnel_verify, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/tunnel.h b/src/network/netdev/tunnel.h new file mode 100644 index 0000000..713f2fb --- /dev/null +++ b/src/network/netdev/tunnel.h @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "in-addr-util.h" + +#include "conf-parser.h" +#include "fou-tunnel.h" +#include "netdev-util.h" +#include "netdev.h" +#include "networkd-link.h" + +typedef enum Ip6TnlMode { + NETDEV_IP6_TNL_MODE_IP6IP6, + NETDEV_IP6_TNL_MODE_IPIP6, + NETDEV_IP6_TNL_MODE_ANYIP6, + _NETDEV_IP6_TNL_MODE_MAX, + _NETDEV_IP6_TNL_MODE_INVALID = -EINVAL, +} Ip6TnlMode; + +typedef enum IPv6FlowLabel { + NETDEV_IPV6_FLOWLABEL_INHERIT = 0xFFFFF + 1, + _NETDEV_IPV6_FLOWLABEL_MAX, + _NETDEV_IPV6_FLOWLABEL_INVALID = -EINVAL, +} IPv6FlowLabel; + +typedef struct Tunnel { + NetDev meta; + + uint8_t encap_limit; + + int family; + int ipv6_flowlabel; + int allow_localremote; + int gre_erspan_sequence; + int isatap; + + unsigned ttl; + unsigned tos; + unsigned flags; + + uint32_t key; + uint32_t ikey; + uint32_t okey; + + uint8_t erspan_version; + uint32_t erspan_index; /* version 1 */ + uint8_t erspan_direction; /* version 2 */ + uint16_t erspan_hwid; /* version 2 */ + + NetDevLocalAddressType local_type; + union in_addr_union local; + union in_addr_union remote; + + Ip6TnlMode ip6tnl_mode; + FooOverUDPEncapType fou_encap_type; + + int pmtudisc; + bool ignore_df; + bool copy_dscp; + bool independent; + bool fou_tunnel; + bool assign_to_loopback; + bool external; /* a.k.a collect metadata mode */ + + uint16_t encap_src_port; + uint16_t fou_destination_port; + + struct in6_addr sixrd_prefix; + uint8_t sixrd_prefixlen; +} Tunnel; + +int dhcp4_pd_create_6rd_tunnel_name(Link *link, char **ret); +int dhcp4_pd_create_6rd_tunnel(Link *link, link_netlink_message_handler_t callback); + +DEFINE_NETDEV_CAST(IPIP, Tunnel); +DEFINE_NETDEV_CAST(GRE, Tunnel); +DEFINE_NETDEV_CAST(GRETAP, Tunnel); +DEFINE_NETDEV_CAST(IP6GRE, Tunnel); +DEFINE_NETDEV_CAST(IP6GRETAP, Tunnel); +DEFINE_NETDEV_CAST(SIT, Tunnel); +DEFINE_NETDEV_CAST(VTI, Tunnel); +DEFINE_NETDEV_CAST(VTI6, Tunnel); +DEFINE_NETDEV_CAST(IP6TNL, Tunnel); +DEFINE_NETDEV_CAST(ERSPAN, Tunnel); + +static inline Tunnel* TUNNEL(NetDev *netdev) { + assert(netdev); + + switch (netdev->kind) { + case NETDEV_KIND_IPIP: + return IPIP(netdev); + case NETDEV_KIND_SIT: + return SIT(netdev); + case NETDEV_KIND_GRE: + return GRE(netdev); + case NETDEV_KIND_GRETAP: + return GRETAP(netdev); + case NETDEV_KIND_IP6GRE: + return IP6GRE(netdev); + case NETDEV_KIND_IP6GRETAP: + return IP6GRETAP(netdev); + case NETDEV_KIND_VTI: + return VTI(netdev); + case NETDEV_KIND_VTI6: + return VTI6(netdev); + case NETDEV_KIND_IP6TNL: + return IP6TNL(netdev); + case NETDEV_KIND_ERSPAN: + return ERSPAN(netdev); + default: + return NULL; + } +} + +extern const NetDevVTable ipip_vtable; +extern const NetDevVTable sit_vtable; +extern const NetDevVTable vti_vtable; +extern const NetDevVTable vti6_vtable; +extern const NetDevVTable gre_vtable; +extern const NetDevVTable gretap_vtable; +extern const NetDevVTable ip6gre_vtable; +extern const NetDevVTable ip6gretap_vtable; +extern const NetDevVTable ip6tnl_vtable; +extern const NetDevVTable erspan_vtable; + +const char *ip6tnl_mode_to_string(Ip6TnlMode d) _const_; +Ip6TnlMode ip6tnl_mode_from_string(const char *d) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_ip6tnl_mode); +CONFIG_PARSER_PROTOTYPE(config_parse_tunnel_local_address); +CONFIG_PARSER_PROTOTYPE(config_parse_tunnel_remote_address); +CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_flowlabel); +CONFIG_PARSER_PROTOTYPE(config_parse_encap_limit); +CONFIG_PARSER_PROTOTYPE(config_parse_tunnel_key); +CONFIG_PARSER_PROTOTYPE(config_parse_6rd_prefix); +CONFIG_PARSER_PROTOTYPE(config_parse_erspan_version); +CONFIG_PARSER_PROTOTYPE(config_parse_erspan_index); +CONFIG_PARSER_PROTOTYPE(config_parse_erspan_direction); +CONFIG_PARSER_PROTOTYPE(config_parse_erspan_hwid); diff --git a/src/network/netdev/tuntap.c b/src/network/netdev/tuntap.c new file mode 100644 index 0000000..9e909d1 --- /dev/null +++ b/src/network/netdev/tuntap.c @@ -0,0 +1,261 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <fcntl.h> +#include <net/if.h> +#include <netinet/if_ether.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <linux/if_tun.h> + +#include "alloc-util.h" +#include "daemon-util.h" +#include "fd-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "socket-util.h" +#include "tuntap.h" +#include "user-util.h" + +#define TUN_DEV "/dev/net/tun" + +static TunTap* TUNTAP(NetDev *netdev) { + assert(netdev); + + switch (netdev->kind) { + case NETDEV_KIND_TAP: + return TAP(netdev); + case NETDEV_KIND_TUN: + return TUN(netdev); + default: + return NULL; + } +} + +static void *close_fd_ptr(void *p) { + safe_close(PTR_TO_FD(p)); + return NULL; +} + +DEFINE_PRIVATE_HASH_OPS_FULL(named_fd_hash_ops, char, string_hash_func, string_compare_func, free, void, close_fd_ptr); + +int manager_add_tuntap_fd(Manager *m, int fd, const char *name) { + _cleanup_free_ char *tuntap_name = NULL; + const char *p; + int r; + + assert(m); + assert(fd >= 0); + assert(name); + + p = startswith(name, "tuntap-"); + if (!p) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Received unknown fd (%s).", name); + + if (!ifname_valid(p)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Received tuntap fd with invalid name (%s).", p); + + tuntap_name = strdup(p); + if (!tuntap_name) + return log_oom_debug(); + + r = hashmap_ensure_put(&m->tuntap_fds_by_name, &named_fd_hash_ops, tuntap_name, FD_TO_PTR(fd)); + if (r < 0) + return log_debug_errno(r, "Failed to store tuntap fd: %m"); + + TAKE_PTR(tuntap_name); + return 0; +} + +void manager_clear_unmanaged_tuntap_fds(Manager *m) { + char *name; + void *p; + + assert(m); + + while ((p = hashmap_steal_first_key_and_value(m->tuntap_fds_by_name, (void**) &name))) { + close_and_notify_warn(PTR_TO_FD(p), name); + name = mfree(name); + } +} + +static int tuntap_take_fd(NetDev *netdev) { + _cleanup_free_ char *name = NULL; + void *p; + int r; + + assert(netdev); + assert(netdev->manager); + + r = link_get_by_name(netdev->manager, netdev->ifname, NULL); + if (r < 0) + return r; + + p = hashmap_remove2(netdev->manager->tuntap_fds_by_name, netdev->ifname, (void**) &name); + if (!p) + return -ENOENT; + + log_netdev_debug(netdev, "Found file descriptor in fd store."); + return PTR_TO_FD(p); +} + +static int netdev_create_tuntap(NetDev *netdev) { + _cleanup_close_ int fd = -EBADF; + struct ifreq ifr = {}; + TunTap *t; + int r; + + assert(netdev); + t = TUNTAP(netdev); + assert(t); + + fd = TAKE_FD(t->fd); + if (fd < 0) + fd = tuntap_take_fd(netdev); + if (fd < 0) + fd = open(TUN_DEV, O_RDWR|O_CLOEXEC); + if (fd < 0) + return log_netdev_error_errno(netdev, errno, "Failed to open " TUN_DEV ": %m"); + + if (netdev->kind == NETDEV_KIND_TAP) + ifr.ifr_flags |= IFF_TAP; + else + ifr.ifr_flags |= IFF_TUN; + + if (!t->packet_info) + ifr.ifr_flags |= IFF_NO_PI; + + if (t->multi_queue) + ifr.ifr_flags |= IFF_MULTI_QUEUE; + + if (t->vnet_hdr) + ifr.ifr_flags |= IFF_VNET_HDR; + + strncpy(ifr.ifr_name, netdev->ifname, IFNAMSIZ-1); + + if (ioctl(fd, TUNSETIFF, &ifr) < 0) + return log_netdev_error_errno(netdev, errno, "TUNSETIFF failed: %m"); + + if (t->multi_queue) { + /* If we don't detach the queue, the kernel will send packets to our queue and they + * will be dropped because we never read them, which is especially important in case + * of KeepCarrier option which persists open FD. So detach our queue right after + * device create/attach to make kernel not send the packets to it. The option is + * available for multi-queue devices only. + * + * See https://github.com/systemd/systemd/pull/30504 for details. */ + struct ifreq detach_request = { .ifr_flags = IFF_DETACH_QUEUE }; + if (ioctl(fd, TUNSETQUEUE, &detach_request) < 0) + return log_netdev_error_errno(netdev, errno, "TUNSETQUEUE failed: %m"); + } + + if (t->user_name) { + const char *user = t->user_name; + uid_t uid; + + r = get_user_creds(&user, &uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Cannot resolve user name %s: %m", t->user_name); + + if (ioctl(fd, TUNSETOWNER, uid) < 0) + return log_netdev_error_errno(netdev, errno, "TUNSETOWNER failed: %m"); + } + + if (t->group_name) { + const char *group = t->group_name; + gid_t gid; + + r = get_group_creds(&group, &gid, USER_CREDS_ALLOW_MISSING); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Cannot resolve group name %s: %m", t->group_name); + + if (ioctl(fd, TUNSETGROUP, gid) < 0) + return log_netdev_error_errno(netdev, errno, "TUNSETGROUP failed: %m"); + + } + + if (ioctl(fd, TUNSETPERSIST, 1) < 0) + return log_netdev_error_errno(netdev, errno, "TUNSETPERSIST failed: %m"); + + if (t->keep_fd) { + t->fd = TAKE_FD(fd); + (void) notify_push_fdf(t->fd, "tuntap-%s", netdev->ifname); + } + + return 0; +} + +static void tuntap_init(NetDev *netdev) { + TunTap *t; + + assert(netdev); + t = TUNTAP(netdev); + assert(t); + + t->fd = -EBADF; +} + +static void tuntap_drop(NetDev *netdev) { + TunTap *t; + + assert(netdev); + t = TUNTAP(netdev); + assert(t); + + t->fd = close_and_notify_warn(t->fd, netdev->ifname); +} + +static void tuntap_done(NetDev *netdev) { + TunTap *t; + + assert(netdev); + t = TUNTAP(netdev); + assert(t); + + t->fd = safe_close(t->fd); + t->user_name = mfree(t->user_name); + t->group_name = mfree(t->group_name); +} + +static int tuntap_verify(NetDev *netdev, const char *filename) { + assert(netdev); + + if (netdev->mtu != 0) + log_netdev_warning(netdev, + "MTUBytes= configured for %s device in %s will be ignored.\n" + "Please set it in the corresponding .network file.", + netdev_kind_to_string(netdev->kind), filename); + + if (netdev->hw_addr.length > 0) + log_netdev_warning(netdev, + "MACAddress= configured for %s device in %s will be ignored.\n" + "Please set it in the corresponding .network file.", + netdev_kind_to_string(netdev->kind), filename); + + return 0; +} + +const NetDevVTable tun_vtable = { + .object_size = sizeof(TunTap), + .sections = NETDEV_COMMON_SECTIONS "Tun\0", + .config_verify = tuntap_verify, + .init = tuntap_init, + .drop = tuntap_drop, + .done = tuntap_done, + .create = netdev_create_tuntap, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_NONE, +}; + +const NetDevVTable tap_vtable = { + .object_size = sizeof(TunTap), + .sections = NETDEV_COMMON_SECTIONS "Tap\0", + .config_verify = tuntap_verify, + .init = tuntap_init, + .drop = tuntap_drop, + .done = tuntap_done, + .create = netdev_create_tuntap, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_ETHER, +}; diff --git a/src/network/netdev/tuntap.h b/src/network/netdev/tuntap.h new file mode 100644 index 0000000..88e0ce5 --- /dev/null +++ b/src/network/netdev/tuntap.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct TunTap TunTap; + +#include "netdev.h" + +struct TunTap { + NetDev meta; + + int fd; + char *user_name; + char *group_name; + bool multi_queue; + bool packet_info; + bool vnet_hdr; + bool keep_fd; +}; + +DEFINE_NETDEV_CAST(TUN, TunTap); +DEFINE_NETDEV_CAST(TAP, TunTap); +extern const NetDevVTable tun_vtable; +extern const NetDevVTable tap_vtable; + +int manager_add_tuntap_fd(Manager *m, int fd, const char *name); +void manager_clear_unmanaged_tuntap_fds(Manager *m); diff --git a/src/network/netdev/vcan.c b/src/network/netdev/vcan.c new file mode 100644 index 0000000..380547e --- /dev/null +++ b/src/network/netdev/vcan.c @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if_arp.h> + +#include "vcan.h" + +const NetDevVTable vcan_vtable = { + .object_size = sizeof(VCan), + .sections = NETDEV_COMMON_SECTIONS, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_CAN, +}; diff --git a/src/network/netdev/vcan.h b/src/network/netdev/vcan.h new file mode 100644 index 0000000..843984f --- /dev/null +++ b/src/network/netdev/vcan.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct VCan VCan; + +#include <netinet/in.h> +#include <linux/can/netlink.h> + +#include "netdev.h" + +struct VCan { + NetDev meta; +}; + +DEFINE_NETDEV_CAST(VCAN, VCan); + +extern const NetDevVTable vcan_vtable; diff --git a/src/network/netdev/veth.c b/src/network/netdev/veth.c new file mode 100644 index 0000000..e0f5b4e --- /dev/null +++ b/src/network/netdev/veth.c @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> +#include <linux/veth.h> + +#include "netlink-util.h" +#include "veth.h" + +static int netdev_veth_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(!link); + assert(m); + + struct hw_addr_data hw_addr; + Veth *v = VETH(netdev); + int r; + + r = sd_netlink_message_open_container(m, VETH_INFO_PEER); + if (r < 0) + return r; + + if (v->ifname_peer) { + r = sd_netlink_message_append_string(m, IFLA_IFNAME, v->ifname_peer); + if (r < 0) + return r; + } + + r = netdev_generate_hw_addr(netdev, NULL, v->ifname_peer, &v->hw_addr_peer, &hw_addr); + if (r < 0) + return r; + + if (hw_addr.length > 0) { + log_netdev_debug(netdev, "Using MAC address for peer: %s", HW_ADDR_TO_STR(&hw_addr)); + r = netlink_message_append_hw_addr(m, IFLA_ADDRESS, &hw_addr); + if (r < 0) + return r; + } + + if (netdev->mtu != 0) { + r = sd_netlink_message_append_u32(m, IFLA_MTU, netdev->mtu); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + return 0; +} + +static int netdev_veth_verify(NetDev *netdev, const char *filename) { + assert(filename); + + Veth *v = VETH(netdev); + + if (!v->ifname_peer) + return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "Veth NetDev without peer name configured in %s. Ignoring", + filename); + + return 0; +} + +static void veth_done(NetDev *netdev) { + Veth *v = VETH(netdev); + + free(v->ifname_peer); +} + +const NetDevVTable veth_vtable = { + .object_size = sizeof(Veth), + .sections = NETDEV_COMMON_SECTIONS "Peer\0", + .done = veth_done, + .fill_message_create = netdev_veth_fill_message_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .config_verify = netdev_veth_verify, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/veth.h b/src/network/netdev/veth.h new file mode 100644 index 0000000..e0d6fd4 --- /dev/null +++ b/src/network/netdev/veth.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct Veth Veth; + +#include "netdev.h" + +struct Veth { + NetDev meta; + + char *ifname_peer; + struct hw_addr_data hw_addr_peer; +}; + +DEFINE_NETDEV_CAST(VETH, Veth); +extern const NetDevVTable veth_vtable; diff --git a/src/network/netdev/vlan.c b/src/network/netdev/vlan.c new file mode 100644 index 0000000..2390206 --- /dev/null +++ b/src/network/netdev/vlan.c @@ -0,0 +1,217 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <net/if.h> +#include <linux/if_arp.h> +#include <linux/if_vlan.h> + +#include "parse-util.h" +#include "vlan-util.h" +#include "vlan.h" + +static int netdev_vlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *req) { + assert(link); + assert(req); + + struct ifla_vlan_flags flags = {}; + VLan *v = VLAN(netdev); + int r; + + r = sd_netlink_message_append_u16(req, IFLA_VLAN_ID, v->id); + if (r < 0) + return r; + + if (v->protocol >= 0) { + r = sd_netlink_message_append_u16(req, IFLA_VLAN_PROTOCOL, htobe16(v->protocol)); + if (r < 0) + return r; + } + + if (v->gvrp != -1) { + flags.mask |= VLAN_FLAG_GVRP; + SET_FLAG(flags.flags, VLAN_FLAG_GVRP, v->gvrp); + } + + if (v->mvrp != -1) { + flags.mask |= VLAN_FLAG_MVRP; + SET_FLAG(flags.flags, VLAN_FLAG_MVRP, v->mvrp); + } + + if (v->reorder_hdr != -1) { + flags.mask |= VLAN_FLAG_REORDER_HDR; + SET_FLAG(flags.flags, VLAN_FLAG_REORDER_HDR, v->reorder_hdr); + } + + if (v->loose_binding != -1) { + flags.mask |= VLAN_FLAG_LOOSE_BINDING; + SET_FLAG(flags.flags, VLAN_FLAG_LOOSE_BINDING, v->loose_binding); + } + + r = sd_netlink_message_append_data(req, IFLA_VLAN_FLAGS, &flags, sizeof(struct ifla_vlan_flags)); + if (r < 0) + return r; + + if (!set_isempty(v->egress_qos_maps)) { + struct ifla_vlan_qos_mapping *m; + + r = sd_netlink_message_open_container(req, IFLA_VLAN_EGRESS_QOS); + if (r < 0) + return r; + + SET_FOREACH(m, v->egress_qos_maps) { + r = sd_netlink_message_append_data(req, IFLA_VLAN_QOS_MAPPING, m, sizeof(struct ifla_vlan_qos_mapping)); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + } + + if (!set_isempty(v->ingress_qos_maps)) { + struct ifla_vlan_qos_mapping *m; + + r = sd_netlink_message_open_container(req, IFLA_VLAN_INGRESS_QOS); + if (r < 0) + return r; + + SET_FOREACH(m, v->ingress_qos_maps) { + r = sd_netlink_message_append_data(req, IFLA_VLAN_QOS_MAPPING, m, sizeof(struct ifla_vlan_qos_mapping)); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + } + + return 0; +} + +static void vlan_qos_maps_hash_func(const struct ifla_vlan_qos_mapping *x, struct siphash *state) { + siphash24_compress(&x->from, sizeof(x->from), state); + siphash24_compress(&x->to, sizeof(x->to), state); +} + +static int vlan_qos_maps_compare_func(const struct ifla_vlan_qos_mapping *a, const struct ifla_vlan_qos_mapping *b) { + int r; + + r = CMP(a->from, b->from); + if (r != 0) + return r; + + return CMP(a->to, b->to); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + vlan_qos_maps_hash_ops, + struct ifla_vlan_qos_mapping, + vlan_qos_maps_hash_func, + vlan_qos_maps_compare_func, + free); + +int config_parse_vlan_qos_maps( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Set **s = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *s = set_free(*s); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ struct ifla_vlan_qos_mapping *m = NULL; + _cleanup_free_ char *w = NULL; + unsigned from, to; + + r = extract_first_word(&p, &w, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse %s, ignoring: %s", lvalue, rvalue); + return 0; + } + if (r == 0) + return 0; + + r = parse_range(w, &from, &to); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse %s, ignoring: %s", lvalue, w); + continue; + } + + m = new(struct ifla_vlan_qos_mapping, 1); + if (!m) + return log_oom(); + + *m = (struct ifla_vlan_qos_mapping) { + .from = from, + .to = to, + }; + + r = set_ensure_consume(s, &vlan_qos_maps_hash_ops, TAKE_PTR(m)); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to store %s, ignoring: %s", lvalue, w); + continue; + } + } +} + +static int netdev_vlan_verify(NetDev *netdev, const char *filename) { + assert(filename); + + VLan *v = VLAN(netdev); + + if (v->id == VLANID_INVALID) { + log_netdev_warning(netdev, "VLAN without valid Id (%"PRIu16") configured in %s.", v->id, filename); + return -EINVAL; + } + + return 0; +} + +static void vlan_done(NetDev *netdev) { + VLan *v = VLAN(netdev); + + set_free(v->egress_qos_maps); + set_free(v->ingress_qos_maps); +} + +static void vlan_init(NetDev *netdev) { + VLan *v = VLAN(netdev); + + v->id = VLANID_INVALID; + v->protocol = -1; + v->gvrp = -1; + v->mvrp = -1; + v->loose_binding = -1; + v->reorder_hdr = -1; +} + +const NetDevVTable vlan_vtable = { + .object_size = sizeof(VLan), + .init = vlan_init, + .sections = NETDEV_COMMON_SECTIONS "VLAN\0", + .fill_message_create = netdev_vlan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .config_verify = netdev_vlan_verify, + .done = vlan_done, + .iftype = ARPHRD_ETHER, +}; diff --git a/src/network/netdev/vlan.h b/src/network/netdev/vlan.h new file mode 100644 index 0000000..1e5e590 --- /dev/null +++ b/src/network/netdev/vlan.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct VLan VLan; + +#include "netdev.h" +#include "set.h" + +struct VLan { + NetDev meta; + + uint16_t id; + int protocol; + + int gvrp; + int mvrp; + int loose_binding; + int reorder_hdr; + + Set *egress_qos_maps; + Set *ingress_qos_maps; +}; + +DEFINE_NETDEV_CAST(VLAN, VLan); +extern const NetDevVTable vlan_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_vlan_qos_maps); diff --git a/src/network/netdev/vrf.c b/src/network/netdev/vrf.c new file mode 100644 index 0000000..b75ec2b --- /dev/null +++ b/src/network/netdev/vrf.c @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> + +#include "vrf.h" + +static int netdev_vrf_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(!link); + assert(m); + + Vrf *v = VRF(netdev); + int r; + + r = sd_netlink_message_append_u32(m, IFLA_VRF_TABLE, v->table); + if (r < 0) + return r; + + return 0; +} + +const NetDevVTable vrf_vtable = { + .object_size = sizeof(Vrf), + .sections = NETDEV_COMMON_SECTIONS "VRF\0", + .fill_message_create = netdev_vrf_fill_message_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/vrf.h b/src/network/netdev/vrf.h new file mode 100644 index 0000000..87977e2 --- /dev/null +++ b/src/network/netdev/vrf.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct Vrf Vrf; + +#include "netdev.h" + +struct Vrf { + NetDev meta; + + uint32_t table; +}; + +DEFINE_NETDEV_CAST(VRF, Vrf); +extern const NetDevVTable vrf_vtable; diff --git a/src/network/netdev/vxcan.c b/src/network/netdev/vxcan.c new file mode 100644 index 0000000..c0343f4 --- /dev/null +++ b/src/network/netdev/vxcan.c @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/can/vxcan.h> +#include <linux/if_arp.h> + +#include "vxcan.h" + +static int netdev_vxcan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(!link); + assert(m); + + VxCan *v = VXCAN(netdev); + int r; + + r = sd_netlink_message_open_container(m, VXCAN_INFO_PEER); + if (r < 0) + return r; + + if (v->ifname_peer) { + r = sd_netlink_message_append_string(m, IFLA_IFNAME, v->ifname_peer); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + return 0; +} + +static int netdev_vxcan_verify(NetDev *netdev, const char *filename) { + assert(filename); + + VxCan *v = VXCAN(netdev); + + if (!v->ifname_peer) + return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "VxCan NetDev without peer name configured in %s. Ignoring", filename); + + return 0; +} + +static void vxcan_done(NetDev *netdev) { + VxCan *v = VXCAN(netdev); + + free(v->ifname_peer); +} + +const NetDevVTable vxcan_vtable = { + .object_size = sizeof(VxCan), + .sections = NETDEV_COMMON_SECTIONS "VXCAN\0", + .done = vxcan_done, + .fill_message_create = netdev_vxcan_fill_message_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .config_verify = netdev_vxcan_verify, + .iftype = ARPHRD_CAN, +}; diff --git a/src/network/netdev/vxcan.h b/src/network/netdev/vxcan.h new file mode 100644 index 0000000..47be3f0 --- /dev/null +++ b/src/network/netdev/vxcan.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct VxCan VxCan; + +#include "netdev.h" + +struct VxCan { + NetDev meta; + + char *ifname_peer; +}; + +DEFINE_NETDEV_CAST(VXCAN, VxCan); + +extern const NetDevVTable vxcan_vtable; diff --git a/src/network/netdev/vxlan.c b/src/network/netdev/vxlan.c new file mode 100644 index 0000000..b11fdbb --- /dev/null +++ b/src/network/netdev/vxlan.c @@ -0,0 +1,435 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> + +#include "conf-parser.h" +#include "alloc-util.h" +#include "extract-word.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "parse-util.h" +#include "vxlan.h" + +static const char* const df_table[_NETDEV_VXLAN_DF_MAX] = { + [NETDEV_VXLAN_DF_NO] = "no", + [NETDEV_VXLAN_DF_YES] = "yes", + [NETDEV_VXLAN_DF_INHERIT] = "inherit", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(df, VxLanDF, NETDEV_VXLAN_DF_YES); +DEFINE_CONFIG_PARSE_ENUM(config_parse_df, df, VxLanDF, "Failed to parse VXLAN IPDoNotFragment= setting"); + +static int vxlan_get_local_address(VxLan *v, Link *link, int *ret_family, union in_addr_union *ret_address) { + assert(v); + + if (v->local_type < 0) { + if (ret_family) + *ret_family = v->local_family; + if (ret_address) + *ret_address = v->local; + return 0; + } + + return link_get_local_address(link, v->local_type, v->local_family, ret_family, ret_address); +} + +static int netdev_vxlan_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) { + assert(m); + + union in_addr_union local; + int local_family, r; + VxLan *v = VXLAN(netdev); + + if (v->vni <= VXLAN_VID_MAX) { + r = sd_netlink_message_append_u32(m, IFLA_VXLAN_ID, v->vni); + if (r < 0) + return r; + } + + if (in_addr_is_set(v->group_family, &v->group)) { + if (v->group_family == AF_INET) + r = sd_netlink_message_append_in_addr(m, IFLA_VXLAN_GROUP, &v->group.in); + else + r = sd_netlink_message_append_in6_addr(m, IFLA_VXLAN_GROUP6, &v->group.in6); + if (r < 0) + return r; + } else if (in_addr_is_set(v->remote_family, &v->remote)) { + if (v->remote_family == AF_INET) + r = sd_netlink_message_append_in_addr(m, IFLA_VXLAN_GROUP, &v->remote.in); + else + r = sd_netlink_message_append_in6_addr(m, IFLA_VXLAN_GROUP6, &v->remote.in6); + if (r < 0) + return r; + } + + r = vxlan_get_local_address(v, link, &local_family, &local); + if (r < 0) + return r; + + if (in_addr_is_set(local_family, &local)) { + if (local_family == AF_INET) + r = sd_netlink_message_append_in_addr(m, IFLA_VXLAN_LOCAL, &local.in); + else + r = sd_netlink_message_append_in6_addr(m, IFLA_VXLAN_LOCAL6, &local.in6); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LINK, link ? link->ifindex : 0); + if (r < 0) + return r; + + if (v->inherit) { + r = sd_netlink_message_append_flag(m, IFLA_VXLAN_TTL_INHERIT); + if (r < 0) + return r; + } else { + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_TTL, v->ttl); + if (r < 0) + return r; + } + + if (v->tos != 0) { + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_TOS, v->tos); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_LEARNING, v->learning); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_RSC, v->route_short_circuit); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_PROXY, v->arp_proxy); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_L2MISS, v->l2miss); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_L3MISS, v->l3miss); + if (r < 0) + return r; + + if (v->fdb_ageing != 0) { + r = sd_netlink_message_append_u32(m, IFLA_VXLAN_AGEING, v->fdb_ageing / USEC_PER_SEC); + if (r < 0) + return r; + } + + if (v->max_fdb != 0) { + r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LIMIT, v->max_fdb); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_CSUM, v->udpcsum); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_ZERO_CSUM6_TX, v->udp6zerocsumtx); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_UDP_ZERO_CSUM6_RX, v->udp6zerocsumrx); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_REMCSUM_TX, v->remote_csum_tx); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_REMCSUM_RX, v->remote_csum_rx); + if (r < 0) + return r; + + r = sd_netlink_message_append_u16(m, IFLA_VXLAN_PORT, htobe16(v->dest_port)); + if (r < 0) + return r; + + if (v->port_range.low != 0 || v->port_range.high != 0) { + struct ifla_vxlan_port_range port_range; + + port_range.low = htobe16(v->port_range.low); + port_range.high = htobe16(v->port_range.high); + + r = sd_netlink_message_append_data(m, IFLA_VXLAN_PORT_RANGE, &port_range, sizeof(port_range)); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u32(m, IFLA_VXLAN_LABEL, htobe32(v->flow_label)); + if (r < 0) + return r; + + if (v->group_policy) { + r = sd_netlink_message_append_flag(m, IFLA_VXLAN_GBP); + if (r < 0) + return r; + } + + if (v->generic_protocol_extension) { + r = sd_netlink_message_append_flag(m, IFLA_VXLAN_GPE); + if (r < 0) + return r; + } + + if (v->df != _NETDEV_VXLAN_DF_INVALID) { + r = sd_netlink_message_append_u8(m, IFLA_VXLAN_DF, v->df); + if (r < 0) + return r; + } + + return 0; +} + +int config_parse_vxlan_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + VxLan *v = ASSERT_PTR(userdata); + union in_addr_union *addr = data, buffer; + int *family, f, r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + if (streq(lvalue, "Local")) + family = &v->local_family; + else if (streq(lvalue, "Remote")) + family = &v->remote_family; + else if (streq(lvalue, "Group")) + family = &v->group_family; + else + assert_not_reached(); + + if (isempty(rvalue)) { + *addr = IN_ADDR_NULL; + *family = AF_UNSPEC; + return 0; + } + + if (streq(lvalue, "Local")) { + NetDevLocalAddressType t; + + t = netdev_local_address_type_from_string(rvalue); + if (t >= 0) { + v->local = IN_ADDR_NULL; + v->local_family = AF_UNSPEC; + v->local_type = t; + return 0; + } + } + + r = in_addr_from_string_auto(rvalue, &f, &buffer); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + r = in_addr_is_multicast(f, &buffer); + + if (streq(lvalue, "Group")) { + if (r <= 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "%s= must be a multicast address, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + } else { + if (r > 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "%s= cannot be a multicast address, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + } + + if (streq(lvalue, "Local")) + v->local_type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID; + *addr = buffer; + *family = f; + + return 0; +} + +int config_parse_port_range( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + VxLan *v = ASSERT_PTR(userdata); + int r; + + r = parse_ip_port_range(rvalue, &v->port_range.low, &v->port_range.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; +} + +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) { + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + VxLan *v = ASSERT_PTR(userdata); + int r; + + if (streq(rvalue, "inherit")) { + v->inherit = true; + v->ttl = 0; /* unset the unused ttl field for clarity */ + return 0; + } + + r = config_parse_unsigned_bounded( + unit, filename, line, section, section_line, lvalue, rvalue, + 0, UINT8_MAX, true, + &v->ttl); + if (r <= 0) + return r; + v->inherit = false; + return 0; +} + +static int netdev_vxlan_verify(NetDev *netdev, const char *filename) { + assert(filename); + + VxLan *v = VXLAN(netdev); + + if (v->vni > VXLAN_VID_MAX) + return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: VXLAN without valid VNI (or VXLAN Segment ID) configured. Ignoring.", + filename); + + if (v->ttl > 255) + return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: VXLAN TTL must be <= 255. Ignoring.", + filename); + + if (!v->dest_port && v->generic_protocol_extension) + v->dest_port = 4790; + + if (in_addr_is_set(v->group_family, &v->group) && in_addr_is_set(v->remote_family, &v->remote)) + return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: VXLAN both 'Group=' and 'Remote=' cannot be specified. Ignoring.", + filename); + + if (v->independent && v->local_type >= 0) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "The local address cannot be '%s' when Independent= is enabled, ignoring.", + strna(netdev_local_address_type_to_string(v->local_type))); + + return 0; +} + +static int netdev_vxlan_is_ready_to_create(NetDev *netdev, Link *link) { + VxLan *v = VXLAN(netdev); + + if (v->independent) + return true; + + return vxlan_get_local_address(v, link, NULL, NULL) >= 0; +} + +static void vxlan_init(NetDev *netdev) { + VxLan *v = VXLAN(netdev); + + v->local_type = _NETDEV_LOCAL_ADDRESS_TYPE_INVALID; + v->vni = VXLAN_VID_MAX + 1; + v->df = _NETDEV_VXLAN_DF_INVALID; + v->learning = true; + v->udpcsum = false; + v->udp6zerocsumtx = false; + v->udp6zerocsumrx = false; +} + +const NetDevVTable vxlan_vtable = { + .object_size = sizeof(VxLan), + .init = vxlan_init, + .sections = NETDEV_COMMON_SECTIONS "VXLAN\0", + .fill_message_create = netdev_vxlan_fill_message_create, + .create_type = NETDEV_CREATE_STACKED, + .is_ready_to_create = netdev_vxlan_is_ready_to_create, + .config_verify = netdev_vxlan_verify, + .iftype = ARPHRD_ETHER, + .generate_mac = true, +}; diff --git a/src/network/netdev/vxlan.h b/src/network/netdev/vxlan.h new file mode 100644 index 0000000..141ac4d --- /dev/null +++ b/src/network/netdev/vxlan.h @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct VxLan VxLan; + +#include <linux/if_link.h> + +#include "in-addr-util.h" +#include "netdev-util.h" +#include "netdev.h" + +#define VXLAN_VID_MAX (1u << 24) - 1 +#define VXLAN_FLOW_LABEL_MAX_MASK 0xFFFFFU + +typedef enum VxLanDF { + NETDEV_VXLAN_DF_NO = VXLAN_DF_UNSET, + NETDEV_VXLAN_DF_YES = VXLAN_DF_SET, + NETDEV_VXLAN_DF_INHERIT = VXLAN_DF_INHERIT, + _NETDEV_VXLAN_DF_MAX, + _NETDEV_VXLAN_DF_INVALID = -EINVAL, +} VxLanDF; + +struct VxLan { + NetDev meta; + + uint32_t vni; + + int remote_family; + int local_family; + int group_family; + + VxLanDF df; + + NetDevLocalAddressType local_type; + union in_addr_union local; + union in_addr_union remote; + union in_addr_union group; + + unsigned tos; + unsigned ttl; + unsigned max_fdb; + unsigned flow_label; + + uint16_t dest_port; + + usec_t fdb_ageing; + + bool learning; + bool arp_proxy; + bool route_short_circuit; + bool l2miss; + bool l3miss; + bool udpcsum; + bool udp6zerocsumtx; + bool udp6zerocsumrx; + bool remote_csum_tx; + bool remote_csum_rx; + bool group_policy; + bool generic_protocol_extension; + bool inherit; + bool independent; + + struct ifla_vxlan_port_range port_range; +}; + +DEFINE_NETDEV_CAST(VXLAN, VxLan); +extern const NetDevVTable vxlan_vtable; + +const char *df_to_string(VxLanDF d) _const_; +VxLanDF df_from_string(const char *d) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_vxlan_address); +CONFIG_PARSER_PROTOTYPE(config_parse_port_range); +CONFIG_PARSER_PROTOTYPE(config_parse_flow_label); +CONFIG_PARSER_PROTOTYPE(config_parse_df); +CONFIG_PARSER_PROTOTYPE(config_parse_vxlan_ttl); diff --git a/src/network/netdev/wireguard.c b/src/network/netdev/wireguard.c new file mode 100644 index 0000000..4c7d837 --- /dev/null +++ b/src/network/netdev/wireguard.c @@ -0,0 +1,1141 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2015-2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. +***/ + +#include <sys/ioctl.h> +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_arp.h> +#include <linux/ipv6_route.h> + +#include "sd-resolve.h" + +#include "alloc-util.h" +#include "dns-domain.h" +#include "event-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "memory-util.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "networkd-route-util.h" +#include "networkd-route.h" +#include "networkd-util.h" +#include "parse-helpers.h" +#include "parse-util.h" +#include "random-util.h" +#include "resolve-private.h" +#include "string-util.h" +#include "strv.h" +#include "wireguard.h" + +static void wireguard_resolve_endpoints(NetDev *netdev); +static int peer_resolve_endpoint(WireguardPeer *peer); + +static void wireguard_peer_clear_ipmasks(WireguardPeer *peer) { + assert(peer); + + LIST_CLEAR(ipmasks, peer->ipmasks, free); +} + +static WireguardPeer* wireguard_peer_free(WireguardPeer *peer) { + if (!peer) + return NULL; + + if (peer->wireguard) { + LIST_REMOVE(peers, peer->wireguard->peers, peer); + + if (peer->section) + hashmap_remove(peer->wireguard->peers_by_section, peer->section); + } + + config_section_free(peer->section); + + wireguard_peer_clear_ipmasks(peer); + + free(peer->endpoint_host); + free(peer->endpoint_port); + free(peer->preshared_key_file); + explicit_bzero_safe(peer->preshared_key, WG_KEY_LEN); + + sd_event_source_disable_unref(peer->resolve_retry_event_source); + sd_resolve_query_unref(peer->resolve_query); + + return mfree(peer); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(WireguardPeer, wireguard_peer_free); + +static int wireguard_peer_new_static(Wireguard *w, const char *filename, unsigned section_line, WireguardPeer **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(wireguard_peer_freep) WireguardPeer *peer = NULL; + int r; + + assert(w); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + peer = hashmap_get(w->peers_by_section, n); + if (peer) { + *ret = TAKE_PTR(peer); + return 0; + } + + peer = new(WireguardPeer, 1); + if (!peer) + return -ENOMEM; + + *peer = (WireguardPeer) { + .flags = WGPEER_F_REPLACE_ALLOWEDIPS, + .wireguard = w, + .section = TAKE_PTR(n), + }; + + LIST_PREPEND(peers, w->peers, peer); + + r = hashmap_ensure_put(&w->peers_by_section, &config_section_hash_ops, peer->section, peer); + if (r < 0) + return r; + + *ret = TAKE_PTR(peer); + return 0; +} + +static int wireguard_set_ipmask_one(NetDev *netdev, sd_netlink_message *message, const WireguardIPmask *mask, uint16_t index) { + int r; + + assert(message); + assert(mask); + assert(index > 0); + + /* This returns 1 on success, 0 on recoverable error, and negative errno on failure. */ + + r = sd_netlink_message_open_array(message, index); + if (r < 0) + return 0; + + r = sd_netlink_message_append_u16(message, WGALLOWEDIP_A_FAMILY, mask->family); + if (r < 0) + goto cancel; + + r = netlink_message_append_in_addr_union(message, WGALLOWEDIP_A_IPADDR, mask->family, &mask->ip); + if (r < 0) + goto cancel; + + r = sd_netlink_message_append_u8(message, WGALLOWEDIP_A_CIDR_MASK, mask->cidr); + if (r < 0) + goto cancel; + + r = sd_netlink_message_close_container(message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not add wireguard allowed ip: %m"); + + return 1; + +cancel: + r = sd_netlink_message_cancel_array(message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not cancel wireguard allowed ip message attribute: %m"); + + return 0; +} + +static int wireguard_set_peer_one(NetDev *netdev, sd_netlink_message *message, const WireguardPeer *peer, uint16_t index, WireguardIPmask **mask_start) { + WireguardIPmask *start, *last = NULL; + uint16_t j = 0; + int r; + + assert(message); + assert(peer); + assert(index > 0); + assert(mask_start); + + /* This returns 1 on success, 0 on recoverable error, and negative errno on failure. */ + + start = *mask_start ?: peer->ipmasks; + + r = sd_netlink_message_open_array(message, index); + if (r < 0) + return 0; + + r = sd_netlink_message_append_data(message, WGPEER_A_PUBLIC_KEY, &peer->public_key, sizeof(peer->public_key)); + if (r < 0) + goto cancel; + + if (!*mask_start) { + r = sd_netlink_message_append_data(message, WGPEER_A_PRESHARED_KEY, &peer->preshared_key, WG_KEY_LEN); + if (r < 0) + goto cancel; + + r = sd_netlink_message_append_u32(message, WGPEER_A_FLAGS, peer->flags); + if (r < 0) + goto cancel; + + r = sd_netlink_message_append_u16(message, WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, peer->persistent_keepalive_interval); + if (r < 0) + goto cancel; + + if (IN_SET(peer->endpoint.sa.sa_family, AF_INET, AF_INET6)) { + r = netlink_message_append_sockaddr_union(message, WGPEER_A_ENDPOINT, &peer->endpoint); + if (r < 0) + goto cancel; + } + } + + r = sd_netlink_message_open_container(message, WGPEER_A_ALLOWEDIPS); + if (r < 0) + goto cancel; + + LIST_FOREACH(ipmasks, mask, start) { + r = wireguard_set_ipmask_one(netdev, message, mask, ++j); + if (r < 0) + return r; + if (r == 0) { + last = mask; + break; + } + } + + r = sd_netlink_message_close_container(message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not add wireguard allowed ip: %m"); + + r = sd_netlink_message_close_container(message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not add wireguard peer: %m"); + + *mask_start = last; /* Start next cycle from this mask. */ + return !last; + +cancel: + r = sd_netlink_message_cancel_array(message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not cancel wireguard peers: %m"); + + return 0; +} + +static int wireguard_set_interface(NetDev *netdev) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL; + WireguardIPmask *mask_start = NULL; + bool sent_once = false; + uint32_t serial; + Wireguard *w = WIREGUARD(netdev); + int r; + + for (WireguardPeer *peer_start = w->peers; peer_start || !sent_once; ) { + uint16_t i = 0; + + message = sd_netlink_message_unref(message); + + r = sd_genl_message_new(netdev->manager->genl, WG_GENL_NAME, WG_CMD_SET_DEVICE, &message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Failed to allocate generic netlink message: %m"); + + r = sd_netlink_message_append_string(message, WGDEVICE_A_IFNAME, netdev->ifname); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not append wireguard interface name: %m"); + + if (peer_start == w->peers) { + r = sd_netlink_message_append_data(message, WGDEVICE_A_PRIVATE_KEY, &w->private_key, WG_KEY_LEN); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not append wireguard private key: %m"); + + r = sd_netlink_message_append_u16(message, WGDEVICE_A_LISTEN_PORT, w->port); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not append wireguard port: %m"); + + r = sd_netlink_message_append_u32(message, WGDEVICE_A_FWMARK, w->fwmark); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not append wireguard fwmark: %m"); + + r = sd_netlink_message_append_u32(message, WGDEVICE_A_FLAGS, w->flags); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not append wireguard flags: %m"); + } + + r = sd_netlink_message_open_container(message, WGDEVICE_A_PEERS); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not append wireguard peer attributes: %m"); + + WireguardPeer *peer_last = NULL; + LIST_FOREACH(peers, peer, peer_start) { + r = wireguard_set_peer_one(netdev, message, peer, ++i, &mask_start); + if (r < 0) + return r; + if (r == 0) { + peer_last = peer; + break; + } + } + peer_start = peer_last; /* Start next cycle from this peer. */ + + r = sd_netlink_message_close_container(message); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not close wireguard container: %m"); + + r = sd_netlink_send(netdev->manager->genl, message, &serial); + if (r < 0) + return log_netdev_error_errno(netdev, r, "Could not set wireguard device: %m"); + + sent_once = true; + } + + return 0; +} + +static int on_resolve_retry(sd_event_source *s, usec_t usec, void *userdata) { + WireguardPeer *peer = ASSERT_PTR(userdata); + NetDev *netdev; + + assert(peer->wireguard); + + netdev = NETDEV(peer->wireguard); + + if (!netdev_is_managed(netdev)) + return 0; + + peer->resolve_query = sd_resolve_query_unref(peer->resolve_query); + + (void) peer_resolve_endpoint(peer); + return 0; +} + +static usec_t peer_next_resolve_usec(WireguardPeer *peer) { + usec_t usec; + + /* Given the number of retries this function will return an exponential increasing amount of + * milliseconds to wait starting at 200ms and capped at 25 seconds. */ + + assert(peer); + + usec = (2 << MIN(peer->n_retries, 7U)) * 100 * USEC_PER_MSEC; + + return random_u64_range(usec / 10) + usec * 9 / 10; +} + +static int wireguard_peer_resolve_handler( + sd_resolve_query *q, + int ret, + const struct addrinfo *ai, + void *userdata) { + + WireguardPeer *peer = ASSERT_PTR(userdata); + NetDev *netdev; + int r; + + assert(peer->wireguard); + + netdev = NETDEV(peer->wireguard); + + if (!netdev_is_managed(netdev)) + return 0; + + if (ret != 0) { + log_netdev_warning(netdev, "Failed to resolve host '%s:%s', ignoring: %s", + peer->endpoint_host, peer->endpoint_port, gai_strerror(ret)); + peer->n_retries++; + + } else { + bool found = false; + for (; ai; ai = ai->ai_next) { + if (!IN_SET(ai->ai_family, AF_INET, AF_INET6)) + continue; + + if (ai->ai_addrlen != (ai->ai_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6))) + continue; + + memcpy(&peer->endpoint, ai->ai_addr, ai->ai_addrlen); + (void) wireguard_set_interface(netdev); + peer->n_retries = 0; + found = true; + break; + } + + if (!found) { + log_netdev_warning(netdev, "Neither IPv4 nor IPv6 address found for peer endpoint %s:%s, ignoring the endpoint.", + peer->endpoint_host, peer->endpoint_port); + peer->n_retries++; + } + } + + if (peer->n_retries > 0) { + r = event_reset_time_relative(netdev->manager->event, + &peer->resolve_retry_event_source, + CLOCK_BOOTTIME, + peer_next_resolve_usec(peer), 0, + on_resolve_retry, peer, 0, "wireguard-resolve-retry", true); + if (r < 0) + log_netdev_warning_errno(netdev, r, "Could not arm resolve retry handler for endpoint %s:%s, ignoring: %m", + peer->endpoint_host, peer->endpoint_port); + } + + wireguard_resolve_endpoints(netdev); + return 0; +} + +static int peer_resolve_endpoint(WireguardPeer *peer) { + static const struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM, + .ai_protocol = IPPROTO_UDP + }; + NetDev *netdev; + int r; + + assert(peer); + assert(peer->wireguard); + + netdev = NETDEV(peer->wireguard); + + if (!peer->endpoint_host || !peer->endpoint_port) + /* Not necessary to resolve the endpoint. */ + return 0; + + if (sd_event_source_get_enabled(peer->resolve_retry_event_source, NULL) > 0) + /* Timer event source is enabled. The endpoint will be resolved later. */ + return 0; + + if (peer->resolve_query) + /* Being resolved, or already resolved. */ + return 0; + + r = sd_resolve_getaddrinfo(netdev->manager->resolve, + &peer->resolve_query, + peer->endpoint_host, + peer->endpoint_port, + &hints, + wireguard_peer_resolve_handler, + peer); + if (r < 0) + return log_netdev_full_errno(netdev, r == -ENOBUFS ? LOG_DEBUG : LOG_WARNING, r, + "Failed to create endpoint resolver for %s:%s, ignoring: %m", + peer->endpoint_host, peer->endpoint_port); + + return 0; +} + +static void wireguard_resolve_endpoints(NetDev *netdev) { + Wireguard *w = WIREGUARD(netdev); + + LIST_FOREACH(peers, peer, w->peers) + if (peer_resolve_endpoint(peer) == -ENOBUFS) + /* Too many requests. Let's resolve remaining endpoints later. */ + break; +} + +static int netdev_wireguard_post_create(NetDev *netdev, Link *link) { + assert(WIREGUARD(netdev)); + + (void) wireguard_set_interface(netdev); + wireguard_resolve_endpoints(netdev); + return 0; +} + +int config_parse_wireguard_listen_port( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint16_t *s = ASSERT_PTR(data); + int r; + + assert(rvalue); + + if (isempty(rvalue) || streq(rvalue, "auto")) { + *s = 0; + return 0; + } + + r = parse_ip_port(rvalue, s); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid port specification, ignoring assignment: %s", rvalue); + return 0; + } + + return 0; +} + +static int wireguard_decode_key_and_warn( + const char *rvalue, + uint8_t ret[static WG_KEY_LEN], + const char *unit, + const char *filename, + unsigned line, + const char *lvalue) { + + _cleanup_(erase_and_freep) void *key = NULL; + size_t len; + int r; + + assert(rvalue); + assert(ret); + assert(filename); + assert(lvalue); + + if (isempty(rvalue)) { + memzero(ret, WG_KEY_LEN); + return 0; + } + + if (!streq(lvalue, "PublicKey")) + (void) warn_file_is_world_accessible(filename, NULL, unit, line); + + r = unbase64mem_full(rvalue, strlen(rvalue), true, &key, &len); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to decode wireguard key provided by %s=, ignoring assignment: %m", lvalue); + return 0; + } + if (len != WG_KEY_LEN) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Wireguard key provided by %s= has invalid length (%zu bytes), ignoring assignment.", + lvalue, len); + return 0; + } + + memcpy(ret, key, WG_KEY_LEN); + return 0; +} + +int config_parse_wireguard_private_key( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Wireguard *w = WIREGUARD(data); + + 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) { + + Wireguard *w = WIREGUARD(data); + _cleanup_free_ char *path = NULL; + + 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) { + + Wireguard *w = WIREGUARD(data); + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + int r; + + 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) { + + Wireguard *w = WIREGUARD(data); + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + _cleanup_free_ char *path = NULL; + int r; + + 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) { + + assert(rvalue); + + Wireguard *w = WIREGUARD(data); + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + union in_addr_union addr; + unsigned char prefixlen; + int r, family; + WireguardIPmask *ipmask; + + r = wireguard_peer_new_static(w, filename, section_line, &peer); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + wireguard_peer_clear_ipmasks(peer); + TAKE_PTR(peer); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *word = NULL; + union in_addr_union masked; + + r = extract_first_word(&p, &word, "," WHITESPACE, 0); + if (r == 0) + break; + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to split allowed ips \"%s\" option: %m", rvalue); + break; + } + + r = in_addr_prefix_from_string_auto(word, &family, &addr, &prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Network address is invalid, ignoring assignment: %s", word); + continue; + } + + masked = addr; + assert_se(in_addr_mask(family, &masked, prefixlen) >= 0); + if (!in_addr_equal(family, &masked, &addr)) + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified address '%s' is not properly masked, assuming '%s'.", + word, + IN_ADDR_PREFIX_TO_STRING(family, &masked, prefixlen)); + + ipmask = new(WireguardIPmask, 1); + if (!ipmask) + return log_oom(); + + *ipmask = (WireguardIPmask) { + .family = family, + .ip = masked, + .cidr = prefixlen, + }; + + LIST_PREPEND(ipmasks, peer->ipmasks, ipmask); + } + + TAKE_PTR(peer); + return 0; +} + +int config_parse_wireguard_endpoint( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(filename); + assert(rvalue); + assert(userdata); + + Wireguard *w = WIREGUARD(userdata); + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + _cleanup_free_ char *host = NULL; + union in_addr_union addr; + const char *p; + uint16_t port; + int family, r; + + r = wireguard_peer_new_static(w, filename, section_line, &peer); + if (r < 0) + return log_oom(); + + r = in_addr_port_ifindex_name_from_string_auto(rvalue, &family, &addr, &port, NULL, NULL); + if (r >= 0) { + if (family == AF_INET) + peer->endpoint.in = (struct sockaddr_in) { + .sin_family = AF_INET, + .sin_addr = addr.in, + .sin_port = htobe16(port), + }; + else if (family == AF_INET6) + peer->endpoint.in6 = (struct sockaddr_in6) { + .sin6_family = AF_INET6, + .sin6_addr = addr.in6, + .sin6_port = htobe16(port), + }; + else + assert_not_reached(); + + peer->endpoint_host = mfree(peer->endpoint_host); + peer->endpoint_port = mfree(peer->endpoint_port); + + TAKE_PTR(peer); + return 0; + } + + p = strrchr(rvalue, ':'); + if (!p) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Unable to find port of endpoint, ignoring assignment: %s", + rvalue); + return 0; + } + + host = strndup(rvalue, p - rvalue); + if (!host) + return log_oom(); + + if (!dns_name_is_valid(host)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid domain name of endpoint, ignoring assignment: %s", + rvalue); + return 0; + } + + p++; + r = parse_ip_port(p, &port); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid port of endpoint, ignoring assignment: %s", + rvalue); + return 0; + } + + peer->endpoint = (union sockaddr_union) {}; + + free_and_replace(peer->endpoint_host, host); + + r = free_and_strdup(&peer->endpoint_port, p); + if (r < 0) + return log_oom(); + + TAKE_PTR(peer); /* The peer may already have been in the hash map, that is fine too. */ + return 0; +} + +int config_parse_wireguard_keepalive( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + assert(rvalue); + + Wireguard *w = WIREGUARD(data); + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + uint16_t keepalive = 0; + int r; + + r = wireguard_peer_new_static(w, filename, section_line, &peer); + if (r < 0) + return log_oom(); + + if (streq(rvalue, "off")) + keepalive = 0; + else { + r = safe_atou16(rvalue, &keepalive); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse \"%s\" as keepalive interval (range 0–65535), ignoring assignment: %m", + rvalue); + return 0; + } + } + + peer->persistent_keepalive_interval = keepalive; + + TAKE_PTR(peer); + return 0; +} + +int config_parse_wireguard_route_table( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + NetDev *netdev = ASSERT_PTR(userdata); + uint32_t *table = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue) || parse_boolean(rvalue) == 0) { + *table = 0; /* Disabled. */ + return 0; + } + + r = manager_get_route_table_from_string(netdev->manager, rvalue, table); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + return 0; +} + +int config_parse_wireguard_peer_route_table( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Wireguard *w = WIREGUARD(userdata); + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(NETDEV(w)->manager); + + r = wireguard_peer_new_static(w, filename, section_line, &peer); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + peer->route_table_set = false; /* Use the table specified in [WireGuard] section. */ + TAKE_PTR(peer); + return 0; + } + + if (parse_boolean(rvalue) == 0) { + peer->route_table = 0; /* Disabled. */ + peer->route_table_set = true; + TAKE_PTR(peer); + return 0; + } + + r = manager_get_route_table_from_string(NETDEV(w)->manager, rvalue, &peer->route_table); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + peer->route_table_set = true; + TAKE_PTR(peer); + return 0; +} + +int config_parse_wireguard_route_priority( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint32_t *priority = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *priority = 0; + return 0; + } + + r = safe_atou32(rvalue, priority); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + return 0; +} + +int config_parse_wireguard_peer_route_priority( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + Wireguard *w; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(userdata); + + w = WIREGUARD(userdata); + assert(w); + + r = wireguard_peer_new_static(w, filename, section_line, &peer); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + peer->route_priority_set = false; /* Use the priority specified in [WireGuard] section. */ + TAKE_PTR(peer); + return 0; + } + + r = safe_atou32(rvalue, &peer->route_priority); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + peer->route_priority_set = true; + TAKE_PTR(peer); + return 0; +} + +static void wireguard_init(NetDev *netdev) { + Wireguard *w = WIREGUARD(netdev); + + w->flags = WGDEVICE_F_REPLACE_PEERS; +} + +static void wireguard_done(NetDev *netdev) { + Wireguard *w = WIREGUARD(netdev); + + explicit_bzero_safe(w->private_key, WG_KEY_LEN); + free(w->private_key_file); + + hashmap_free_with_destructor(w->peers_by_section, wireguard_peer_free); + + set_free(w->routes); +} + +static int wireguard_read_key_file(const char *filename, uint8_t dest[static WG_KEY_LEN]) { + _cleanup_(erase_and_freep) char *key = NULL; + size_t key_len; + int r; + + if (!filename) + return 0; + + assert(dest); + + r = read_full_file_full( + AT_FDCWD, filename, UINT64_MAX, WG_KEY_LEN, + READ_FULL_FILE_SECURE | + READ_FULL_FILE_UNBASE64 | + READ_FULL_FILE_WARN_WORLD_READABLE | + READ_FULL_FILE_CONNECT_SOCKET | + READ_FULL_FILE_FAIL_WHEN_LARGER, + NULL, &key, &key_len); + if (r < 0) + return r; + + if (key_len != WG_KEY_LEN) + return -EINVAL; + + memcpy(dest, key, WG_KEY_LEN); + return 0; +} + +static int wireguard_peer_verify(WireguardPeer *peer) { + NetDev *netdev = NETDEV(peer->wireguard); + int r; + + if (section_is_invalid(peer->section)) + return -EINVAL; + + if (eqzero(peer->public_key)) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: WireGuardPeer section without PublicKey= configured. " + "Ignoring [WireGuardPeer] section from line %u.", + peer->section->filename, peer->section->line); + + r = wireguard_read_key_file(peer->preshared_key_file, peer->preshared_key); + if (r < 0) + return log_netdev_error_errno(netdev, r, + "%s: Failed to read preshared key from '%s'. " + "Ignoring [WireGuardPeer] section from line %u.", + peer->section->filename, peer->preshared_key_file, + peer->section->line); + + return 0; +} + +static int wireguard_verify(NetDev *netdev, const char *filename) { + Wireguard *w = WIREGUARD(netdev); + int r; + + r = wireguard_read_key_file(w->private_key_file, w->private_key); + if (r < 0) + return log_netdev_error_errno(netdev, r, + "Failed to read private key from %s. Ignoring network device.", + w->private_key_file); + + if (eqzero(w->private_key)) + return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: Missing PrivateKey= or PrivateKeyFile=, " + "Ignoring network device.", filename); + + LIST_FOREACH(peers, peer, w->peers) { + if (wireguard_peer_verify(peer) < 0) { + wireguard_peer_free(peer); + continue; + } + + if ((peer->route_table_set ? peer->route_table : w->route_table) == 0) + continue; + + LIST_FOREACH(ipmasks, ipmask, peer->ipmasks) { + _cleanup_(route_freep) Route *route = NULL; + + r = route_new(&route); + if (r < 0) + return log_oom(); + + route->family = ipmask->family; + route->dst = ipmask->ip; + route->dst_prefixlen = ipmask->cidr; + route->scope = RT_SCOPE_UNIVERSE; + route->protocol = RTPROT_STATIC; + route->table = peer->route_table_set ? peer->route_table : w->route_table; + route->priority = peer->route_priority_set ? peer->route_priority : w->route_priority; + if (route->priority == 0 && route->family == AF_INET6) + route->priority = IP6_RT_PRIO_USER; + route->source = NETWORK_CONFIG_SOURCE_STATIC; + + r = set_ensure_consume(&w->routes, &route_hash_ops, TAKE_PTR(route)); + if (r < 0) + return log_oom(); + } + } + + return 0; +} + +const NetDevVTable wireguard_vtable = { + .object_size = sizeof(Wireguard), + .sections = NETDEV_COMMON_SECTIONS "WireGuard\0WireGuardPeer\0", + .post_create = netdev_wireguard_post_create, + .init = wireguard_init, + .done = wireguard_done, + .create_type = NETDEV_CREATE_INDEPENDENT, + .config_verify = wireguard_verify, + .iftype = ARPHRD_NONE, +}; diff --git a/src/network/netdev/wireguard.h b/src/network/netdev/wireguard.h new file mode 100644 index 0000000..09dca88 --- /dev/null +++ b/src/network/netdev/wireguard.h @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +typedef struct Wireguard Wireguard; + +#include <netinet/in.h> +#include <linux/wireguard.h> + +#include "sd-event.h" +#include "sd-resolve.h" + +#include "in-addr-util.h" +#include "netdev.h" +#include "socket-util.h" + +typedef struct WireguardIPmask { + uint16_t family; + union in_addr_union ip; + uint8_t cidr; + + LIST_FIELDS(struct WireguardIPmask, ipmasks); +} WireguardIPmask; + +typedef struct WireguardPeer { + Wireguard *wireguard; + ConfigSection *section; + + uint8_t public_key[WG_KEY_LEN]; + uint8_t preshared_key[WG_KEY_LEN]; + char *preshared_key_file; + uint32_t flags; + uint16_t persistent_keepalive_interval; + + union sockaddr_union endpoint; + char *endpoint_host; + char *endpoint_port; + + unsigned n_retries; + sd_event_source *resolve_retry_event_source; + sd_resolve_query *resolve_query; + + uint32_t route_table; + uint32_t route_priority; + bool route_table_set; + bool route_priority_set; + + LIST_HEAD(WireguardIPmask, ipmasks); + LIST_FIELDS(struct WireguardPeer, peers); +} WireguardPeer; + +struct Wireguard { + NetDev meta; + unsigned last_peer_section; + + uint32_t flags; + uint8_t private_key[WG_KEY_LEN]; + char *private_key_file; + uint16_t port; + uint32_t fwmark; + + Hashmap *peers_by_section; + LIST_HEAD(WireguardPeer, peers); + + Set *routes; + uint32_t route_table; + uint32_t route_priority; +}; + +DEFINE_NETDEV_CAST(WIREGUARD, Wireguard); +extern const NetDevVTable wireguard_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_allowed_ips); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_endpoint); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_listen_port); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_peer_key); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_private_key); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_private_key_file); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_preshared_key_file); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_keepalive); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_route_table); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_peer_route_table); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_route_priority); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_peer_route_priority); diff --git a/src/network/netdev/wlan.c b/src/network/netdev/wlan.c new file mode 100644 index 0000000..904e40f --- /dev/null +++ b/src/network/netdev/wlan.c @@ -0,0 +1,228 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if_arp.h> + +#include "sd-netlink.h" + +#include "netlink-util.h" +#include "networkd-manager.h" +#include "networkd-wiphy.h" +#include "parse-util.h" +#include "wifi-util.h" +#include "wlan.h" + +static void wlan_done(NetDev *netdev) { + WLan *w = WLAN(netdev); + + w->wiphy_name = mfree(w->wiphy_name); +} + +static void wlan_init(NetDev *netdev) { + WLan *w = WLAN(netdev); + + w->wiphy_index = UINT32_MAX; + w->wds = -1; +} + +static int wlan_get_wiphy(NetDev *netdev, Wiphy **ret) { + WLan *w = WLAN(netdev); + + if (w->wiphy_name) + return wiphy_get_by_name(netdev->manager, w->wiphy_name, ret); + + return wiphy_get_by_index(netdev->manager, w->wiphy_index, ret); +} + +static int wlan_is_ready_to_create(NetDev *netdev, Link *link) { + return wlan_get_wiphy(netdev, NULL) >= 0; +} + +static int wlan_fill_message(NetDev *netdev, sd_netlink_message *m) { + WLan *w = WLAN(netdev); + Wiphy *wiphy; + int r; + + r = wlan_get_wiphy(netdev, &wiphy); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, NL80211_ATTR_WIPHY, wiphy->index); + if (r < 0) + return r; + + r = sd_netlink_message_append_string(m, NL80211_ATTR_IFNAME, netdev->ifname); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, NL80211_ATTR_IFTYPE, w->iftype); + if (r < 0) + return r; + + if (!hw_addr_is_null(&netdev->hw_addr) && netdev->hw_addr.length == ETH_ALEN) { + r = sd_netlink_message_append_ether_addr(m, NL80211_ATTR_MAC, &netdev->hw_addr.ether); + if (r < 0) + return r; + } + + if (w->wds >= 0) { + r = sd_netlink_message_append_u8(m, NL80211_ATTR_4ADDR, w->wds); + if (r < 0) + return r; + } + + return 0; +} + +static int wlan_create_handler(sd_netlink *genl, sd_netlink_message *m, NetDev *netdev) { + int r; + + assert(netdev); + assert(netdev->state != _NETDEV_STATE_INVALID); + + r = sd_netlink_message_get_errno(m); + if (IN_SET(r, -EEXIST, -ENFILE)) + /* Unlike the other netdevs, the kernel may return -ENFILE. See dev_alloc_name(). */ + log_netdev_info(netdev, "WLAN interface exists, using existing without changing its parameters."); + else if (r < 0) { + log_netdev_warning_errno(netdev, r, "WLAN interface could not be created: %m"); + netdev_enter_failed(netdev); + + return 1; + } + + log_netdev_debug(netdev, "WLAN interface is created."); + return 1; +} + +static int wlan_create(NetDev *netdev) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(netdev); + assert(netdev->manager); + assert(netdev->manager->genl); + + r = sd_genl_message_new(netdev->manager->genl, NL80211_GENL_NAME, NL80211_CMD_NEW_INTERFACE, &m); + if (r < 0) + return log_netdev_warning_errno(netdev, r, "Failed to allocate netlink message: %m"); + + r = wlan_fill_message(netdev, m); + if (r < 0) + return log_netdev_warning_errno(netdev, r, "Failed to fill netlink message: %m"); + + r = netlink_call_async(netdev->manager->genl, NULL, m, wlan_create_handler, + netdev_destroy_callback, netdev); + if (r < 0) + return log_netdev_warning_errno(netdev, r, "Failed to send netlink message: %m"); + + netdev_ref(netdev); + return 0; +} + +static int wlan_verify(NetDev *netdev, const char *filename) { + WLan *w = WLAN(netdev); + + assert(filename); + + if (w->iftype == NL80211_IFTYPE_UNSPECIFIED) + return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: WLAN interface type is not specified, ignoring.", + filename); + + if (w->wiphy_index == UINT32_MAX && isempty(w->wiphy_name)) + return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: physical WLAN device is not specified, ignoring.", + filename); + + return 0; +} + +int config_parse_wiphy( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + WLan *w = WLAN(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + w->wiphy_name = mfree(w->wiphy_name); + w->wiphy_index = UINT32_MAX; + return 0; + } + + r = safe_atou32(rvalue, &w->wiphy_index); + if (r >= 0) { + w->wiphy_name = mfree(w->wiphy_name); + return 0; + } + + r = free_and_strdup_warn(&w->wiphy_name, rvalue); + if (r < 0) + return r; + + w->wiphy_index = UINT32_MAX; + return 0; +} + +int config_parse_wlan_iftype( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + enum nl80211_iftype t, *iftype = ASSERT_PTR(data); + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *iftype = NL80211_IFTYPE_UNSPECIFIED; + return 0; + } + + t = nl80211_iftype_from_string(rvalue); + /* We reuse the kernel provided enum which does not contain negative value. So, the cast + * below is mandatory. Otherwise, the check below always passes. */ + if ((int) t < 0) { + log_syntax(unit, LOG_WARNING, filename, line, t, + "Failed to parse wlan interface type, ignoring assignment: %s", + rvalue); + return 0; + } + + *iftype = t; + return 0; +} + +const NetDevVTable wlan_vtable = { + .object_size = sizeof(WLan), + .init = wlan_init, + .done = wlan_done, + .sections = NETDEV_COMMON_SECTIONS "WLAN\0", + .is_ready_to_create = wlan_is_ready_to_create, + .create = wlan_create, + .create_type = NETDEV_CREATE_INDEPENDENT, + .config_verify = wlan_verify, + .iftype = ARPHRD_ETHER, + .generate_mac = true, + .skip_netdev_kind_check = true, +}; diff --git a/src/network/netdev/wlan.h b/src/network/netdev/wlan.h new file mode 100644 index 0000000..bcc2dbc --- /dev/null +++ b/src/network/netdev/wlan.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <linux/nl80211.h> + +#include "conf-parser.h" +#include "netdev.h" + +typedef struct WLan { + NetDev meta; + + char *wiphy_name; + uint32_t wiphy_index; + enum nl80211_iftype iftype; + int wds; /* tristate */ +} WLan; + +DEFINE_NETDEV_CAST(WLAN, WLan); +extern const NetDevVTable wlan_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_wiphy); +CONFIG_PARSER_PROTOTYPE(config_parse_wlan_iftype); diff --git a/src/network/netdev/xfrm.c b/src/network/netdev/xfrm.c new file mode 100644 index 0000000..905bfc0 --- /dev/null +++ b/src/network/netdev/xfrm.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if_arp.h> + +#include "missing_network.h" +#include "xfrm.h" + +static int xfrm_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *message) { + assert(message); + + Xfrm *x = XFRM(netdev); + int r; + + assert(link || x->independent); + + r = sd_netlink_message_append_u32(message, IFLA_XFRM_LINK, link ? link->ifindex : LOOPBACK_IFINDEX); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(message, IFLA_XFRM_IF_ID, x->if_id); + if (r < 0) + return r; + + return 0; +} + +static int xfrm_verify(NetDev *netdev, const char *filename) { + assert(filename); + + Xfrm *x = XFRM(netdev); + + if (x->if_id == 0) + return log_netdev_warning_errno(netdev, SYNTHETIC_ERRNO(EINVAL), + "%s: Xfrm interface ID cannot be zero.", filename); + return 0; +} + +const NetDevVTable xfrm_vtable = { + .object_size = sizeof(Xfrm), + .sections = NETDEV_COMMON_SECTIONS "Xfrm\0", + .fill_message_create = xfrm_fill_message_create, + .config_verify = xfrm_verify, + .create_type = NETDEV_CREATE_STACKED, + .iftype = ARPHRD_NONE, +}; diff --git a/src/network/netdev/xfrm.h b/src/network/netdev/xfrm.h new file mode 100644 index 0000000..f56c4f2 --- /dev/null +++ b/src/network/netdev/xfrm.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "netdev.h" + +typedef struct Xfrm { + NetDev meta; + + uint32_t if_id; + bool independent; +} Xfrm; + +DEFINE_NETDEV_CAST(XFRM, Xfrm); +extern const NetDevVTable xfrm_vtable; diff --git a/src/network/networkctl.c b/src/network/networkctl.c new file mode 100644 index 0000000..ec31e8e --- /dev/null +++ b/src/network/networkctl.c @@ -0,0 +1,3499 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <getopt.h> +#include <linux/if_addrlabel.h> +#include <net/if.h> +#include <stdbool.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#include <linux/if_bridge.h> +#include <linux/if_tunnel.h> + +#include "sd-bus.h" +#include "sd-device.h" +#include "sd-dhcp-client.h" +#include "sd-hwdb.h" +#include "sd-lldp-rx.h" +#include "sd-netlink.h" +#include "sd-network.h" + +#include "alloc-util.h" +#include "bond-util.h" +#include "bridge-util.h" +#include "build.h" +#include "bus-common-errors.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-wait-for-jobs.h" +#include "conf-files.h" +#include "device-util.h" +#include "edit-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 "fs-util.h" +#include "geneve-util.h" +#include "glob-util.h" +#include "hwdb-util.h" +#include "ipvlan-util.h" +#include "local-addresses.h" +#include "locale-util.h" +#include "logs-show.h" +#include "macro.h" +#include "macvlan-util.h" +#include "main-func.h" +#include "netif-util.h" +#include "netlink-util.h" +#include "network-internal.h" +#include "network-util.h" +#include "pager.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "path-lookup.h" +#include "path-util.h" +#include "pretty-print.h" +#include "set.h" +#include "sigbus.h" +#include "socket-netlink.h" +#include "socket-util.h" +#include "sort-util.h" +#include "sparse-endian.h" +#include "stdio-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "terminal-util.h" +#include "udev-util.h" +#include "unit-def.h" +#include "verbs.h" +#include "virt.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_no_reload = false; +static bool arg_all = false; +static bool arg_stats = false; +static bool arg_full = false; +static unsigned arg_lines = 10; +static char *arg_drop_in = NULL; +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; + +STATIC_DESTRUCTOR_REGISTER(arg_drop_in, freep); + +static int check_netns_match(sd_bus *bus) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + struct stat st; + uint64_t id; + int r; + + assert(bus); + + r = bus_get_property_trivial(bus, bus_network_mgr, "NamespaceId", &error, 't', &id); + if (r < 0) { + log_debug_errno(r, "Failed to query network namespace of networkd, ignoring: %s", bus_error_message(&error, r)); + return 0; + } + if (id == 0) { + log_debug("systemd-networkd.service not running in a network namespace (?), skipping netns check."); + return 0; + } + + if (stat("/proc/self/ns/net", &st) < 0) + return log_error_errno(errno, "Failed to determine our own network namespace ID: %m"); + + if (id != st.st_ino) + return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), + "networkctl must be invoked in same network namespace as systemd-networkd.service."); + + return 0; +} + +static bool networkd_is_running(void) { + static int cached = -1; + int r; + + if (cached < 0) { + r = access("/run/systemd/netif/state", F_OK); + if (r < 0) { + if (errno != ENOENT) + log_debug_errno(errno, + "Failed to determine whether networkd is running, assuming it's not: %m"); + + cached = false; + } else + cached = true; + } + + return cached; +} + +static int acquire_bus(sd_bus **ret) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + assert(ret); + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to system bus: %m"); + + if (networkd_is_running()) { + r = check_netns_match(bus); + if (r < 0) + return r; + } else + log_warning("systemd-networkd is not running, output might be incomplete."); + + *ret = TAKE_PTR(bus); + return 0; +} + +static int get_description(sd_bus *bus, JsonVariant **ret) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + const char *text; + int r; + + assert(bus); + assert(ret); + + r = bus_call_method(bus, bus_network_mgr, "Describe", &error, &reply, NULL); + if (r < 0) + return log_error_errno(r, "Failed to get description: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "s", &text); + if (r < 0) + return bus_log_parse_error(r); + + r = json_parse(text, 0, ret, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON: %m"); + + return 0; +} + +static int dump_manager_description(sd_bus *bus) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(bus); + + r = get_description(bus, &v); + if (r < 0) + return r; + + json_variant_dump(v, arg_json_format_flags, NULL, NULL); + return 0; +} + +static int dump_link_description(sd_bus *bus, char * const *patterns) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ bool *matched_patterns = NULL; + JsonVariant *i; + size_t c = 0; + int r; + + assert(bus); + assert(patterns); + + r = get_description(bus, &v); + if (r < 0) + return r; + + matched_patterns = new0(bool, strv_length(patterns)); + if (!matched_patterns) + return log_oom(); + + JSON_VARIANT_ARRAY_FOREACH(i, json_variant_by_key(v, "Interfaces")) { + char ifindex_str[DECIMAL_STR_MAX(int64_t)]; + const char *name; + int64_t index; + size_t pos; + + name = json_variant_string(json_variant_by_key(i, "Name")); + index = json_variant_integer(json_variant_by_key(i, "Index")); + xsprintf(ifindex_str, "%" PRIi64, index); + + if (!strv_fnmatch_full(patterns, ifindex_str, 0, &pos) && + !strv_fnmatch_full(patterns, name, 0, &pos)) { + bool match = false; + JsonVariant *a; + + JSON_VARIANT_ARRAY_FOREACH(a, json_variant_by_key(i, "AlternativeNames")) + if (strv_fnmatch_full(patterns, json_variant_string(a), 0, &pos)) { + match = true; + break; + } + + if (!match) + continue; + } + + matched_patterns[pos] = true; + json_variant_dump(i, arg_json_format_flags, NULL, NULL); + c++; + } + + /* Look if we matched all our arguments that are not globs. It is OK for a glob to match + * nothing, but not for an exact argument. */ + for (size_t pos = 0; pos < strv_length(patterns); pos++) { + if (matched_patterns[pos]) + continue; + + if (string_is_glob(patterns[pos])) + log_debug("Pattern \"%s\" doesn't match any interface, ignoring.", + patterns[pos]); + else + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Interface \"%s\" not found.", patterns[pos]); + } + + if (c == 0) + log_warning("No interfaces matched."); + + return 0; +} + +static void operational_state_to_color( + const char *name, + const char *state, + const char **on, + const char **off) { + + if (STRPTR_IN_SET(state, "routable", "enslaved") || + (streq_ptr(name, "lo") && streq_ptr(state, "carrier"))) { + if (on) + *on = ansi_highlight_green(); + if (off) + *off = ansi_normal(); + } else if (streq_ptr(state, "degraded")) { + if (on) + *on = ansi_highlight_yellow(); + if (off) + *off = ansi_normal(); + } else { + if (on) + *on = ""; + if (off) + *off = ""; + } +} + +static void setup_state_to_color(const char *state, const char **on, const char **off) { + if (streq_ptr(state, "configured")) { + if (on) + *on = ansi_highlight_green(); + if (off) + *off = ansi_normal(); + } else if (streq_ptr(state, "configuring")) { + if (on) + *on = ansi_highlight_yellow(); + if (off) + *off = ansi_normal(); + } else if (STRPTR_IN_SET(state, "failed", "linger")) { + if (on) + *on = ansi_highlight_red(); + if (off) + *off = ansi_normal(); + } else { + if (on) + *on = ""; + if (off) + *off = ""; + } +} + +static void online_state_to_color(const char *state, const char **on, const char **off) { + if (streq_ptr(state, "online")) { + if (on) + *on = ansi_highlight_green(); + if (off) + *off = ansi_normal(); + } else if (streq_ptr(state, "partial")) { + if (on) + *on = ansi_highlight_yellow(); + if (off) + *off = ansi_normal(); + } else { + if (on) + *on = ""; + if (off) + *off = ""; + } +} + +typedef struct VxLanInfo { + uint32_t vni; + uint32_t link; + + int local_family; + int group_family; + + union in_addr_union local; + union in_addr_union group; + + uint16_t dest_port; + + uint8_t proxy; + uint8_t learning; + uint8_t rsc; + uint8_t l2miss; + uint8_t l3miss; + uint8_t tos; + uint8_t ttl; +} VxLanInfo; + +typedef struct LinkInfo { + char name[IFNAMSIZ+1]; + char *netdev_kind; + sd_device *sd_device; + int ifindex; + unsigned short iftype; + struct hw_addr_data hw_address; + struct hw_addr_data permanent_hw_address; + uint32_t master; + uint32_t mtu; + uint32_t min_mtu; + uint32_t max_mtu; + uint32_t tx_queues; + uint32_t rx_queues; + uint8_t addr_gen_mode; + char *qdisc; + char **alternative_names; + + union { + struct rtnl_link_stats64 stats64; + struct rtnl_link_stats stats; + }; + + uint64_t tx_bitrate; + uint64_t rx_bitrate; + + /* bridge info */ + uint32_t forward_delay; + uint32_t hello_time; + uint32_t max_age; + uint32_t ageing_time; + uint32_t stp_state; + uint32_t cost; + uint16_t priority; + uint8_t mcast_igmp_version; + uint8_t port_state; + + /* vxlan info */ + VxLanInfo vxlan_info; + + /* vlan info */ + uint16_t vlan_id; + + /* tunnel info */ + uint8_t ttl; + uint8_t tos; + uint8_t inherit; + uint8_t df; + uint8_t csum; + uint8_t csum6_tx; + uint8_t csum6_rx; + uint16_t tunnel_port; + uint32_t vni; + uint32_t label; + union in_addr_union local; + union in_addr_union remote; + + /* bonding info */ + uint8_t mode; + uint32_t miimon; + uint32_t updelay; + uint32_t downdelay; + + /* macvlan and macvtap info */ + uint32_t macvlan_mode; + + /* ipvlan info */ + uint16_t ipvlan_mode; + uint16_t ipvlan_flags; + + /* ethtool info */ + int autonegotiation; + uint64_t speed; + Duplex duplex; + NetDevPort port; + + /* wlan info */ + enum nl80211_iftype wlan_iftype; + char *ssid; + struct ether_addr bssid; + + bool has_hw_address:1; + bool has_permanent_hw_address:1; + bool has_tx_queues:1; + bool has_rx_queues:1; + bool has_stats64:1; + bool has_stats:1; + bool has_bitrates:1; + bool has_ethtool_link_info:1; + bool has_wlan_link_info:1; + bool has_tunnel_ipv4:1; + bool has_ipv6_address_generation_mode:1; + + bool needs_freeing:1; +} LinkInfo; + +static int link_info_compare(const LinkInfo *a, const LinkInfo *b) { + return CMP(a->ifindex, b->ifindex); +} + +static LinkInfo* link_info_array_free(LinkInfo *array) { + for (unsigned i = 0; array && array[i].needs_freeing; i++) { + sd_device_unref(array[i].sd_device); + free(array[i].netdev_kind); + free(array[i].ssid); + free(array[i].qdisc); + strv_free(array[i].alternative_names); + } + + return mfree(array); +} +DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_array_free); + +static int decode_netdev(sd_netlink_message *m, LinkInfo *info) { + int r; + + assert(m); + assert(info); + + r = sd_netlink_message_enter_container(m, IFLA_LINKINFO); + if (r < 0) + return r; + + r = sd_netlink_message_read_string_strdup(m, IFLA_INFO_KIND, &info->netdev_kind); + if (r < 0) { + (void) sd_netlink_message_exit_container(m); + return r; + } + + r = sd_netlink_message_enter_container(m, IFLA_INFO_DATA); + if (r < 0) + return r; + + if (streq(info->netdev_kind, "bridge")) { + (void) sd_netlink_message_read_u32(m, IFLA_BR_FORWARD_DELAY, &info->forward_delay); + (void) sd_netlink_message_read_u32(m, IFLA_BR_HELLO_TIME, &info->hello_time); + (void) sd_netlink_message_read_u32(m, IFLA_BR_MAX_AGE, &info->max_age); + (void) sd_netlink_message_read_u32(m, IFLA_BR_AGEING_TIME, &info->ageing_time); + (void) sd_netlink_message_read_u32(m, IFLA_BR_STP_STATE, &info->stp_state); + (void) sd_netlink_message_read_u32(m, IFLA_BRPORT_COST, &info->cost); + (void) sd_netlink_message_read_u16(m, IFLA_BR_PRIORITY, &info->priority); + (void) sd_netlink_message_read_u8(m, IFLA_BR_MCAST_IGMP_VERSION, &info->mcast_igmp_version); + (void) sd_netlink_message_read_u8(m, IFLA_BRPORT_STATE, &info->port_state); + } if (streq(info->netdev_kind, "bond")) { + (void) sd_netlink_message_read_u8(m, IFLA_BOND_MODE, &info->mode); + (void) sd_netlink_message_read_u32(m, IFLA_BOND_MIIMON, &info->miimon); + (void) sd_netlink_message_read_u32(m, IFLA_BOND_DOWNDELAY, &info->downdelay); + (void) sd_netlink_message_read_u32(m, IFLA_BOND_UPDELAY, &info->updelay); + } else if (streq(info->netdev_kind, "vxlan")) { + (void) sd_netlink_message_read_u32(m, IFLA_VXLAN_ID, &info->vxlan_info.vni); + + r = sd_netlink_message_read_in_addr(m, IFLA_VXLAN_GROUP, &info->vxlan_info.group.in); + if (r >= 0) + info->vxlan_info.group_family = AF_INET; + else { + r = sd_netlink_message_read_in6_addr(m, IFLA_VXLAN_GROUP6, &info->vxlan_info.group.in6); + if (r >= 0) + info->vxlan_info.group_family = AF_INET6; + } + + r = sd_netlink_message_read_in_addr(m, IFLA_VXLAN_LOCAL, &info->vxlan_info.local.in); + if (r >= 0) + info->vxlan_info.local_family = AF_INET; + else { + r = sd_netlink_message_read_in6_addr(m, IFLA_VXLAN_LOCAL6, &info->vxlan_info.local.in6); + if (r >= 0) + info->vxlan_info.local_family = AF_INET6; + } + + (void) sd_netlink_message_read_u32(m, IFLA_VXLAN_LINK, &info->vxlan_info.link); + (void) sd_netlink_message_read_u16(m, IFLA_VXLAN_PORT, &info->vxlan_info.dest_port); + (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_PROXY, &info->vxlan_info.proxy); + (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_LEARNING, &info->vxlan_info.learning); + (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_RSC, &info->vxlan_info.rsc); + (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_L3MISS, &info->vxlan_info.l3miss); + (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_L2MISS, &info->vxlan_info.l2miss); + (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_TOS, &info->vxlan_info.tos); + (void) sd_netlink_message_read_u8(m, IFLA_VXLAN_TTL, &info->vxlan_info.ttl); + } else if (streq(info->netdev_kind, "vlan")) + (void) sd_netlink_message_read_u16(m, IFLA_VLAN_ID, &info->vlan_id); + else if (STR_IN_SET(info->netdev_kind, "ipip", "sit")) { + (void) sd_netlink_message_read_in_addr(m, IFLA_IPTUN_LOCAL, &info->local.in); + (void) sd_netlink_message_read_in_addr(m, IFLA_IPTUN_REMOTE, &info->remote.in); + } else if (streq(info->netdev_kind, "geneve")) { + (void) sd_netlink_message_read_u32(m, IFLA_GENEVE_ID, &info->vni); + + r = sd_netlink_message_read_in_addr(m, IFLA_GENEVE_REMOTE, &info->remote.in); + if (r >= 0) + info->has_tunnel_ipv4 = true; + else + (void) sd_netlink_message_read_in6_addr(m, IFLA_GENEVE_REMOTE6, &info->remote.in6); + + (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_TTL, &info->ttl); + (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_TTL_INHERIT, &info->inherit); + (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_TOS, &info->tos); + (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_DF, &info->df); + (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_UDP_CSUM, &info->csum); + (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_TX, &info->csum6_tx); + (void) sd_netlink_message_read_u8(m, IFLA_GENEVE_UDP_ZERO_CSUM6_RX, &info->csum6_rx); + (void) sd_netlink_message_read_u16(m, IFLA_GENEVE_PORT, &info->tunnel_port); + (void) sd_netlink_message_read_u32(m, IFLA_GENEVE_LABEL, &info->label); + } else if (STR_IN_SET(info->netdev_kind, "gre", "gretap", "erspan")) { + (void) sd_netlink_message_read_in_addr(m, IFLA_GRE_LOCAL, &info->local.in); + (void) sd_netlink_message_read_in_addr(m, IFLA_GRE_REMOTE, &info->remote.in); + } else if (STR_IN_SET(info->netdev_kind, "ip6gre", "ip6gretap", "ip6erspan")) { + (void) sd_netlink_message_read_in6_addr(m, IFLA_GRE_LOCAL, &info->local.in6); + (void) sd_netlink_message_read_in6_addr(m, IFLA_GRE_REMOTE, &info->remote.in6); + } else if (streq(info->netdev_kind, "vti")) { + (void) sd_netlink_message_read_in_addr(m, IFLA_VTI_LOCAL, &info->local.in); + (void) sd_netlink_message_read_in_addr(m, IFLA_VTI_REMOTE, &info->remote.in); + } else if (streq(info->netdev_kind, "vti6")) { + (void) sd_netlink_message_read_in6_addr(m, IFLA_VTI_LOCAL, &info->local.in6); + (void) sd_netlink_message_read_in6_addr(m, IFLA_VTI_REMOTE, &info->remote.in6); + } else if (STR_IN_SET(info->netdev_kind, "macvlan", "macvtap")) + (void) sd_netlink_message_read_u32(m, IFLA_MACVLAN_MODE, &info->macvlan_mode); + else if (streq(info->netdev_kind, "ipvlan")) { + (void) sd_netlink_message_read_u16(m, IFLA_IPVLAN_MODE, &info->ipvlan_mode); + (void) sd_netlink_message_read_u16(m, IFLA_IPVLAN_FLAGS, &info->ipvlan_flags); + } + + (void) sd_netlink_message_exit_container(m); + (void) sd_netlink_message_exit_container(m); + + return 0; +} + +static int decode_link( + sd_netlink_message *m, + LinkInfo *info, + char * const *patterns, + bool matched_patterns[]) { + + _cleanup_strv_free_ char **altnames = NULL; + const char *name, *qdisc; + int ifindex, r; + uint16_t type; + + assert(m); + assert(info); + + r = sd_netlink_message_get_type(m, &type); + if (r < 0) + return r; + + if (type != RTM_NEWLINK) + return 0; + + r = sd_rtnl_message_link_get_ifindex(m, &ifindex); + if (r < 0) + return r; + + r = sd_netlink_message_read_string(m, IFLA_IFNAME, &name); + if (r < 0) + return r; + + r = sd_netlink_message_read_strv(m, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &altnames); + if (r < 0 && r != -ENODATA) + return r; + + if (patterns) { + char str[DECIMAL_STR_MAX(int)]; + size_t pos; + + assert(matched_patterns); + + xsprintf(str, "%i", ifindex); + if (!strv_fnmatch_full(patterns, str, 0, &pos) && + !strv_fnmatch_full(patterns, name, 0, &pos)) { + bool match = false; + + STRV_FOREACH(p, altnames) + if (strv_fnmatch_full(patterns, *p, 0, &pos)) { + match = true; + break; + } + if (!match) + return 0; + } + + matched_patterns[pos] = true; + } + + r = sd_rtnl_message_link_get_type(m, &info->iftype); + if (r < 0) + return r; + + strscpy(info->name, sizeof info->name, name); + info->ifindex = ifindex; + info->alternative_names = TAKE_PTR(altnames); + + info->has_hw_address = + netlink_message_read_hw_addr(m, IFLA_ADDRESS, &info->hw_address) >= 0 && + info->hw_address.length > 0; + + info->has_permanent_hw_address = + (netlink_message_read_hw_addr(m, IFLA_PERM_ADDRESS, &info->permanent_hw_address) >= 0 || + ethtool_get_permanent_hw_addr(NULL, info->name, &info->permanent_hw_address) >= 0) && + !hw_addr_is_null(&info->permanent_hw_address) && + !hw_addr_equal(&info->permanent_hw_address, &info->hw_address); + + (void) sd_netlink_message_read_u32(m, IFLA_MTU, &info->mtu); + (void) sd_netlink_message_read_u32(m, IFLA_MIN_MTU, &info->min_mtu); + (void) sd_netlink_message_read_u32(m, IFLA_MAX_MTU, &info->max_mtu); + + info->has_rx_queues = + sd_netlink_message_read_u32(m, IFLA_NUM_RX_QUEUES, &info->rx_queues) >= 0 && + info->rx_queues > 0; + + info->has_tx_queues = + sd_netlink_message_read_u32(m, IFLA_NUM_TX_QUEUES, &info->tx_queues) >= 0 && + info->tx_queues > 0; + + if (sd_netlink_message_read(m, IFLA_STATS64, sizeof info->stats64, &info->stats64) >= 0) + info->has_stats64 = true; + else if (sd_netlink_message_read(m, IFLA_STATS, sizeof info->stats, &info->stats) >= 0) + info->has_stats = true; + + r = sd_netlink_message_read_string(m, IFLA_QDISC, &qdisc); + if (r >= 0) { + info->qdisc = strdup(qdisc); + if (!info->qdisc) + return log_oom(); + } + + (void) sd_netlink_message_read_u32(m, IFLA_MASTER, &info->master); + + r = sd_netlink_message_enter_container(m, IFLA_AF_SPEC); + if (r >= 0) { + r = sd_netlink_message_enter_container(m, AF_INET6); + if (r >= 0) { + r = sd_netlink_message_read_u8(m, IFLA_INET6_ADDR_GEN_MODE, &info->addr_gen_mode); + if (r >= 0 && IN_SET(info->addr_gen_mode, + IN6_ADDR_GEN_MODE_EUI64, + IN6_ADDR_GEN_MODE_NONE, + IN6_ADDR_GEN_MODE_STABLE_PRIVACY, + IN6_ADDR_GEN_MODE_RANDOM)) + info->has_ipv6_address_generation_mode = true; + + (void) sd_netlink_message_exit_container(m); + } + (void) sd_netlink_message_exit_container(m); + } + + /* fill kind info */ + (void) decode_netdev(m, info); + + return 1; +} + +static int link_get_property( + sd_bus *bus, + const LinkInfo *link, + sd_bus_error *error, + sd_bus_message **reply, + const char *iface, + const char *propname, + const char *type) { + + _cleanup_free_ char *path = NULL; + char ifindex_str[DECIMAL_STR_MAX(int)]; + int r; + + assert(bus); + assert(link); + assert(link->ifindex >= 0); + assert(error); + assert(reply); + assert(iface); + assert(propname); + assert(type); + + xsprintf(ifindex_str, "%i", link->ifindex); + + r = sd_bus_path_encode("/org/freedesktop/network1/link", ifindex_str, &path); + if (r < 0) + return r; + + return sd_bus_get_property(bus, "org.freedesktop.network1", path, iface, propname, error, reply, type); +} + +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; + + assert(bus); + assert(link); + + r = link_get_property(bus, link, &error, &reply, "org.freedesktop.network1.Link", "BitRates", "(tt)"); + 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_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) { + assert(fd); + assert(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; + + assert(link); + + if (link->sd_device) + (void) sd_device_get_devtype(link->sd_device, &type); + if (!streq_ptr(type, "wlan")) + return; + + r = sd_genl_socket_open(&genl); + if (r < 0) { + log_debug_errno(r, "Failed to open generic netlink socket: %m"); + return; + } + + (void) sd_netlink_increase_rxbuf(genl, RCVBUF_SIZE); + + r = wifi_get_interface(genl, link->ifindex, &link->wlan_iftype, &link->ssid); + if (r < 0) + log_debug_errno(r, "%s: failed to query ssid: %m", link->name); + + if (link->wlan_iftype == NL80211_IFTYPE_STATION) { + k = wifi_get_station(genl, link->ifindex, &link->bssid); + if (k < 0) + log_debug_errno(k, "%s: failed to query bssid: %m", link->name); + } + + link->has_wlan_link_info = r > 0 || k > 0; +} + +static int acquire_link_info(sd_bus *bus, sd_netlink *rtnl, char * const *patterns, LinkInfo **ret) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + _cleanup_(link_info_array_freep) LinkInfo *links = NULL; + _cleanup_free_ bool *matched_patterns = NULL; + _cleanup_close_ int fd = -EBADF; + size_t c = 0; + int r; + + assert(rtnl); + assert(ret); + + r = sd_rtnl_message_new_link(rtnl, &req, RTM_GETLINK, 0); + if (r < 0) + return rtnl_log_create_error(r); + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return rtnl_log_create_error(r); + + r = sd_netlink_call(rtnl, req, 0, &reply); + if (r < 0) + return log_error_errno(r, "Failed to enumerate links: %m"); + + if (patterns) { + matched_patterns = new0(bool, strv_length(patterns)); + if (!matched_patterns) + return log_oom(); + } + + for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) { + if (!GREEDY_REALLOC0(links, c + 2)) /* We keep one trailing one as marker */ + return -ENOMEM; + + r = decode_link(i, links + c, patterns, matched_patterns); + if (r < 0) + return r; + if (r == 0) + continue; + + links[c].needs_freeing = true; + + (void) sd_device_new_from_ifindex(&links[c].sd_device, links[c].ifindex); + + acquire_ether_link_info(&fd, &links[c]); + acquire_wlan_link_info(&links[c]); + + c++; + } + + /* Look if we matched all our arguments that are not globs. It + * is OK for a glob to match nothing, but not for an exact argument. */ + for (size_t pos = 0; pos < strv_length(patterns); pos++) { + if (matched_patterns[pos]) + continue; + + if (string_is_glob(patterns[pos])) + log_debug("Pattern \"%s\" doesn't match any interface, ignoring.", + patterns[pos]); + else + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Interface \"%s\" not found.", patterns[pos]); + } + + typesafe_qsort(links, c, link_info_compare); + + if (bus) + FOREACH_ARRAY(link, links, c) + (void) acquire_link_bitrates(bus, link); + + *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_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + _cleanup_(link_info_array_freep) LinkInfo *links = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + TableCell *cell; + int c, r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + if (arg_json_format_flags != JSON_FORMAT_OFF) { + if (arg_all || argc <= 1) + return dump_manager_description(bus); + else + return dump_link_description(bus, strv_skip(argv, 1)); + } + + r = sd_netlink_open(&rtnl); + if (r < 0) + return log_error_errno(r, "Failed to connect to netlink: %m"); + + c = acquire_link_info(NULL, rtnl, argc > 1 ? argv + 1 : NULL, &links); + if (c < 0) + return c; + + pager_open(arg_pager_flags); + + table = table_new("idx", "link", "type", "operational", "setup"); + if (!table) + return log_oom(); + + if (arg_full) + table_set_width(table, 0); + + table_set_header(table, arg_legend); + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + + assert_se(cell = table_get_cell(table, 0, 0)); + (void) table_set_minimum_width(table, cell, 3); + (void) table_set_weight(table, cell, 0); + (void) table_set_ellipsize_percent(table, cell, 100); + (void) table_set_align_percent(table, cell, 100); + + assert_se(cell = table_get_cell(table, 0, 1)); + (void) table_set_ellipsize_percent(table, cell, 100); + + FOREACH_ARRAY(link, links, c) { + _cleanup_free_ char *setup_state = NULL, *operational_state = NULL; + _cleanup_free_ char *t = NULL; + const char *on_color_operational, *on_color_setup; + + (void) sd_network_link_get_operational_state(link->ifindex, &operational_state); + operational_state_to_color(link->name, operational_state, &on_color_operational, NULL); + + (void) sd_network_link_get_setup_state(link->ifindex, &setup_state); + setup_state_to_color(setup_state, &on_color_setup, NULL); + + r = net_get_type_string(link->sd_device, link->iftype, &t); + if (r == -ENOMEM) + return log_oom(); + + r = table_add_many(table, + TABLE_INT, link->ifindex, + TABLE_STRING, link->name, + TABLE_STRING, t, + TABLE_STRING, operational_state, + TABLE_SET_COLOR, on_color_operational, + TABLE_STRING, setup_state ?: "unmanaged", + TABLE_SET_COLOR, on_color_setup); + if (r < 0) + return table_log_add_error(r); + } + + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + + if (arg_legend) + printf("\n%i links listed.\n", c); + + return 0; +} + +/* IEEE Organizationally Unique Identifier vendor string */ +static int ieee_oui(sd_hwdb *hwdb, const struct ether_addr *mac, char **ret) { + _cleanup_free_ char *desc = NULL; + const char *description; + char modalias[STRLEN("OUI:XXYYXXYYXXYY") + 1]; + int r; + + assert(ret); + + if (!hwdb || !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 = TAKE_PTR(desc); + + return 0; +} + +static int get_gateway_description( + sd_netlink *rtnl, + sd_hwdb *hwdb, + int ifindex, + int family, + union in_addr_union *gateway, + char **ret) { + + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + int r; + + assert(rtnl); + assert(ifindex >= 0); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(gateway); + assert(ret); + + r = sd_rtnl_message_new_neigh(rtnl, &req, RTM_GETNEIGH, ifindex, family); + if (r < 0) + return r; + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return r; + + r = sd_netlink_call(rtnl, req, 0, &reply); + if (r < 0) + return r; + + for (sd_netlink_message *m = reply; m; m = sd_netlink_message_next(m)) { + union in_addr_union gw = IN_ADDR_NULL; + struct ether_addr mac = ETHER_ADDR_NULL; + uint16_t type; + int ifi, fam; + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + log_error_errno(r, "Failed to get netlink message, ignoring: %m"); + continue; + } + + r = sd_netlink_message_get_type(m, &type); + if (r < 0) { + log_error_errno(r, "Failed to get netlink message type, ignoring: %m"); + continue; + } + + if (type != RTM_NEWNEIGH) { + log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Got unexpected netlink message type %u, ignoring", + type); + continue; + } + + r = sd_rtnl_message_neigh_get_family(m, &fam); + if (r < 0) { + log_error_errno(r, "Failed to get rtnl family, ignoring: %m"); + continue; + } + + if (fam != family) { + log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Got invalid rtnl family %d, ignoring", fam); + continue; + } + + r = sd_rtnl_message_neigh_get_ifindex(m, &ifi); + if (r < 0) { + log_error_errno(r, "Failed to get rtnl ifindex, ignoring: %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, ret); + if (r < 0) + continue; + + return 0; + } + + return -ENODATA; +} + +static int dump_list(Table *table, const char *key, char * const *l) { + int r; + + assert(table); + assert(key); + + if (strv_isempty(l)) + return 0; + + r = table_add_many(table, + TABLE_FIELD, key, + 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_addrs = NULL; + _cleanup_strv_free_ char **buf = NULL; + int r, n; + + assert(rtnl); + assert(table); + + n = local_gateways(rtnl, ifindex, AF_UNSPEC, &local_addrs); + if (n <= 0) + return n; + + FOREACH_ARRAY(local, local_addrs, n) { + _cleanup_free_ char *description = NULL; + + r = get_gateway_description(rtnl, hwdb, local->ifindex, local->family, &local->address, &description); + if (r < 0) + log_debug_errno(r, "Could not get description of gateway, ignoring: %m"); + + /* Show interface name for the entry if we show entries for all interfaces */ + r = strv_extendf(&buf, "%s%s%s%s%s%s", + IN_ADDR_TO_STRING(local->family, &local->address), + description ? " (" : "", + strempty(description), + description ? ")" : "", + ifindex <= 0 ? " on " : "", + ifindex <= 0 ? FORMAT_IFNAME_FULL(local->ifindex, FORMAT_IFNAME_IFINDEX_WITH_PERCENT) : ""); + if (r < 0) + return log_oom(); + } + + return dump_list(table, "Gateway", buf); +} + +static int dump_addresses( + sd_netlink *rtnl, + sd_dhcp_lease *lease, + Table *table, + int ifindex) { + + _cleanup_free_ struct local_address *local_addrs = NULL; + _cleanup_strv_free_ char **buf = NULL; + struct in_addr dhcp4_address = {}; + int r, n; + + assert(rtnl); + assert(table); + + n = local_addresses(rtnl, ifindex, AF_UNSPEC, &local_addrs); + if (n <= 0) + return n; + + if (lease) + (void) sd_dhcp_lease_get_address(lease, &dhcp4_address); + + FOREACH_ARRAY(local, local_addrs, n) { + struct in_addr server_address; + bool dhcp4 = false; + + if (local->family == AF_INET && in4_addr_equal(&local->address.in, &dhcp4_address)) + dhcp4 = sd_dhcp_lease_get_server_identifier(lease, &server_address) >= 0; + + r = strv_extendf(&buf, "%s%s%s%s%s%s", + IN_ADDR_TO_STRING(local->family, &local->address), + dhcp4 ? " (DHCP4 via " : "", + dhcp4 ? IN4_ADDR_TO_STRING(&server_address) : "", + dhcp4 ? ")" : "", + ifindex <= 0 ? " on " : "", + ifindex <= 0 ? FORMAT_IFNAME_FULL(local->ifindex, FORMAT_IFNAME_IFINDEX_WITH_PERCENT) : ""); + if (r < 0) + return log_oom(); + } + + return dump_list(table, "Address", buf); +} + +static int dump_address_labels(sd_netlink *rtnl) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + TableCell *cell; + int r; + + assert(rtnl); + + r = sd_rtnl_message_new_addrlabel(rtnl, &req, RTM_GETADDRLABEL, 0, AF_INET6); + if (r < 0) + return log_error_errno(r, "Could not allocate RTM_GETADDRLABEL message: %m"); + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return r; + + r = sd_netlink_call(rtnl, req, 0, &reply); + if (r < 0) + return r; + + table = table_new("label", "prefix/prefixlen"); + if (!table) + return log_oom(); + + if (arg_full) + table_set_width(table, 0); + + r = table_set_sort(table, (size_t) 0); + if (r < 0) + return r; + + assert_se(cell = table_get_cell(table, 0, 0)); + (void) table_set_align_percent(table, cell, 100); + (void) table_set_ellipsize_percent(table, cell, 100); + + assert_se(cell = table_get_cell(table, 0, 1)); + (void) table_set_align_percent(table, cell, 100); + + for (sd_netlink_message *m = reply; m; m = sd_netlink_message_next(m)) { + struct in6_addr prefix; + uint8_t prefixlen; + uint32_t label; + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + log_error_errno(r, "Failed to get netlink message, ignoring: %m"); + continue; + } + + r = sd_netlink_message_read_u32(m, IFAL_LABEL, &label); + if (r < 0 && r != -ENODATA) { + log_error_errno(r, "Could not read IFAL_LABEL, ignoring: %m"); + continue; + } + + r = sd_netlink_message_read_in6_addr(m, IFAL_ADDRESS, &prefix); + if (r < 0) + continue; + + r = sd_rtnl_message_addrlabel_get_prefixlen(m, &prefixlen); + if (r < 0) + continue; + + r = table_add_cell(table, NULL, TABLE_UINT32, &label); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell_stringf(table, NULL, "%s/%u", IN6_ADDR_TO_STRING(&prefix), prefixlen); + if (r < 0) + return table_log_add_error(r); + } + + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + + return 0; +} + +static int list_address_labels(int argc, char *argv[], void *userdata) { + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + int r; + + r = sd_netlink_open(&rtnl); + if (r < 0) + return log_error_errno(r, "Failed to connect to netlink: %m"); + + return dump_address_labels(rtnl); +} + +static int open_lldp_neighbors(int ifindex, FILE **ret) { + _cleanup_fclose_ FILE *f = NULL; + char p[STRLEN("/run/systemd/netif/lldp/") + DECIMAL_STR_MAX(int)]; + + assert(ifindex >= 0); + assert(ret); + + xsprintf(p, "/run/systemd/netif/lldp/%i", ifindex); + + f = fopen(p, "re"); + if (!f) + return -errno; + + *ret = TAKE_PTR(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; + + assert(table); + assert(prefix); + assert(bus); + assert(link); + + r = link_get_property(bus, link, &error, &reply, "org.freedesktop.network1.DHCPServer", "Leases", "a(uayayayayt)"); + 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, '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', >w, &sz); + if (r < 0 || sz != 4) + return bus_log_parse_error(r); + + r = sd_bus_message_read_array(reply, 'y', &hwaddr, &sz); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read_basic(reply, 't', &expiration); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_dhcp_client_id_to_string(client_id, client_id_sz, &id); + if (r < 0) + return bus_log_parse_error(r); + + r = in_addr_to_string(family, addr, &ip); + if (r < 0) + return bus_log_parse_error(r); + + r = strv_extendf(&buf, "%s (to %s)", ip, id); + if (r < 0) + return log_oom(); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (strv_isempty(buf)) { + r = strv_extendf(&buf, "none"); + if (r < 0) + return log_oom(); + } + + return dump_list(table, prefix, buf); +} + +static int dump_ifindexes(Table *table, const char *prefix, const int *ifindexes) { + int r; + + assert(table); + assert(prefix); + + if (!ifindexes) + return 0; + + for (unsigned c = 0; ifindexes[c] > 0; c++) { + if (c == 0) + r = table_add_cell(table, NULL, TABLE_FIELD, prefix); + else + r = table_add_cell(table, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell(table, NULL, 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_cell(table, NULL, TABLE_FIELD, 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; + + assert(table); + assert(info); + + if (!arg_stats) + return 0; + + if (!info->has_stats64 && !info->has_stats) + return 0; + + DUMP_STATS_ONE("Rx Packets", rx_packets); + DUMP_STATS_ONE("Tx Packets", tx_packets); + DUMP_STATS_ONE("Rx Bytes", rx_bytes); + DUMP_STATS_ONE("Tx Bytes", tx_bytes); + DUMP_STATS_ONE("Rx Errors", rx_errors); + DUMP_STATS_ONE("Tx Errors", tx_errors); + DUMP_STATS_ONE("Rx Dropped", rx_dropped); + DUMP_STATS_ONE("Tx Dropped", tx_dropped); + DUMP_STATS_ONE("Multicast Packets", multicast); + DUMP_STATS_ONE("Collisions", collisions); + + return 0; +} + +static int dump_hw_address(Table *table, sd_hwdb *hwdb, const char *field, const struct hw_addr_data *addr) { + _cleanup_free_ char *description = NULL; + int r; + + assert(table); + assert(field); + assert(addr); + + if (addr->length == ETH_ALEN) + (void) ieee_oui(hwdb, &addr->ether, &description); + + r = table_add_cell(table, NULL, TABLE_FIELD, field); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell_stringf(table, NULL, "%s%s%s%s", + HW_ADDR_TO_STR(addr), + description ? " (" : "", + strempty(description), + description ? ")" : ""); + if (r < 0) + return table_log_add_error(r); + + return 0; +} + +static OutputFlags get_output_flags(void) { + return + arg_all * OUTPUT_SHOW_ALL | + (arg_full || !on_tty() || pager_have()) * OUTPUT_FULL_WIDTH | + colors_enabled() * OUTPUT_COLOR; +} + +static int show_logs(const LinkInfo *info) { + _cleanup_(sd_journal_closep) sd_journal *j = NULL; + int r; + + if (arg_lines == 0) + return 0; + + r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY); + if (r < 0) + return log_error_errno(r, "Failed to open journal: %m"); + + r = add_match_this_boot(j, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add boot matches: %m"); + + if (info) { + char m1[STRLEN("_KERNEL_DEVICE=n") + DECIMAL_STR_MAX(int)]; + const char *m2, *m3; + + /* kernel */ + xsprintf(m1, "_KERNEL_DEVICE=n%i", info->ifindex); + /* networkd */ + m2 = strjoina("INTERFACE=", info->name); + /* udevd */ + m3 = strjoina("DEVICE=", info->name); + + (void)( + (r = sd_journal_add_match(j, m1, 0)) || + (r = sd_journal_add_disjunction(j)) || + (r = sd_journal_add_match(j, m2, 0)) || + (r = sd_journal_add_disjunction(j)) || + (r = sd_journal_add_match(j, m3, 0)) + ); + if (r < 0) + return log_error_errno(r, "Failed to add link matches: %m"); + } else { + r = add_matches_for_unit(j, "systemd-networkd.service"); + if (r < 0) + return log_error_errno(r, "Failed to add unit matches: %m"); + + r = add_matches_for_unit(j, "systemd-networkd-wait-online.service"); + if (r < 0) + return log_error_errno(r, "Failed to add unit matches: %m"); + } + + return show_journal( + stdout, + j, + OUTPUT_SHORT, + 0, + 0, + arg_lines, + get_output_flags() | OUTPUT_BEGIN_NEWLINE, + NULL); +} + +static int table_add_string_line(Table *table, const char *key, const char *value) { + int r; + + assert(table); + assert(key); + + if (isempty(value)) + return 0; + + r = table_add_many(table, + TABLE_FIELD, key, + TABLE_STRING, value); + if (r < 0) + return table_log_add_error(r); + + return 0; +} + +static int format_dropins(char **dropins) { + STRV_FOREACH(d, dropins) { + _cleanup_free_ char *s = NULL; + int glyph = *(d + 1) == NULL ? SPECIAL_GLYPH_TREE_RIGHT : SPECIAL_GLYPH_TREE_BRANCH; + + s = strjoin(special_glyph(glyph), *d); + if (!s) + return log_oom(); + + free_and_replace(*d, s); + } + + return 0; +} + +static int link_status_one( + sd_bus *bus, + sd_netlink *rtnl, + sd_hwdb *hwdb, + const LinkInfo *info) { + + _cleanup_strv_free_ char **dns = NULL, **ntp = NULL, **sip = NULL, **search_domains = NULL, + **route_domains = NULL, **link_dropins = NULL, **network_dropins = NULL; + _cleanup_free_ char *t = NULL, *network = NULL, *iaid = NULL, *duid = NULL, *captive_portal = NULL, + *setup_state = NULL, *operational_state = NULL, *online_state = NULL, *activation_policy = NULL; + const char *driver = NULL, *path = NULL, *vendor = NULL, *model = NULL, *link = NULL, + *on_color_operational, *off_color_operational, *on_color_setup, *off_color_setup, *on_color_online; + _cleanup_free_ int *carrier_bound_to = NULL, *carrier_bound_by = NULL; + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + assert(bus); + assert(rtnl); + assert(info); + + (void) sd_network_link_get_operational_state(info->ifindex, &operational_state); + operational_state_to_color(info->name, operational_state, &on_color_operational, &off_color_operational); + + (void) sd_network_link_get_online_state(info->ifindex, &online_state); + online_state_to_color(online_state, &on_color_online, NULL); + + (void) sd_network_link_get_setup_state(info->ifindex, &setup_state); + setup_state_to_color(setup_state, &on_color_setup, &off_color_setup); + + (void) sd_network_link_get_dns(info->ifindex, &dns); + (void) sd_network_link_get_search_domains(info->ifindex, &search_domains); + (void) sd_network_link_get_route_domains(info->ifindex, &route_domains); + (void) sd_network_link_get_ntp(info->ifindex, &ntp); + (void) sd_network_link_get_sip(info->ifindex, &sip); + (void) sd_network_link_get_captive_portal(info->ifindex, &captive_portal); + (void) sd_network_link_get_network_file(info->ifindex, &network); + (void) sd_network_link_get_network_file_dropins(info->ifindex, &network_dropins); + (void) sd_network_link_get_carrier_bound_to(info->ifindex, &carrier_bound_to); + (void) sd_network_link_get_carrier_bound_by(info->ifindex, &carrier_bound_by); + (void) sd_network_link_get_activation_policy(info->ifindex, &activation_policy); + + if (info->sd_device) { + const char *joined; + + (void) sd_device_get_property_value(info->sd_device, "ID_NET_LINK_FILE", &link); + + if (sd_device_get_property_value(info->sd_device, "ID_NET_LINK_FILE_DROPINS", &joined) >= 0) { + r = strv_split_full(&link_dropins, joined, ":", EXTRACT_CUNESCAPE); + if (r < 0) + return r; + } + + (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); + (void) device_get_vendor_string(info->sd_device, &vendor); + (void) device_get_model_string(info->sd_device, &model); + } + + r = net_get_type_string(info->sd_device, info->iftype, &t); + if (r == -ENOMEM) + return log_oom(); + + char lease_file[STRLEN("/run/systemd/netif/leases/") + DECIMAL_STR_MAX(int)]; + xsprintf(lease_file, "/run/systemd/netif/leases/%i", info->ifindex); + + (void) dhcp_lease_load(&lease, lease_file); + + r = format_dropins(network_dropins); + if (r < 0) + return r; + + if (strv_prepend(&network_dropins, network) < 0) + return log_oom(); + + r = format_dropins(link_dropins); + if (r < 0) + return r; + + if (strv_prepend(&link_dropins, link) < 0) + return log_oom(); + + table = table_new_vertical(); + if (!table) + return log_oom(); + + if (arg_full) + table_set_width(table, 0); + + /* unit files and basic states. */ + r = table_add_many(table, + TABLE_FIELD, "Link File", + TABLE_STRV, link_dropins ?: STRV_MAKE("n/a"), + TABLE_FIELD, "Network File", + TABLE_STRV, network_dropins ?: STRV_MAKE("n/a"), + TABLE_FIELD, "State"); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell_stringf(table, NULL, "%s%s%s (%s%s%s)", + on_color_operational, strna(operational_state), off_color_operational, + on_color_setup, setup_state ?: "unmanaged", off_color_setup); + if (r < 0) + return table_log_add_error(r); + + r = table_add_many(table, + TABLE_FIELD, "Online state", + TABLE_STRING, online_state ?: "unknown", + TABLE_SET_COLOR, on_color_online); + if (r < 0) + return table_log_add_error(r); + + r = table_add_string_line(table, "Type", t); + if (r < 0) + return r; + + r = table_add_string_line(table, "Kind", info->netdev_kind); + if (r < 0) + return r; + + r = table_add_string_line(table, "Path", path); + if (r < 0) + return r; + + r = table_add_string_line(table, "Driver", driver); + if (r < 0) + return r; + + r = table_add_string_line(table, "Vendor", vendor); + if (r < 0) + return r; + + r = table_add_string_line(table, "Model", model); + if (r < 0) + return r; + + strv_sort(info->alternative_names); + r = dump_list(table, "Alternative Names", info->alternative_names); + if (r < 0) + return r; + + if (info->has_hw_address) { + r = dump_hw_address(table, hwdb, "Hardware Address", &info->hw_address); + if (r < 0) + return r; + } + + if (info->has_permanent_hw_address) { + r = dump_hw_address(table, hwdb, "Permanent Hardware Address", &info->permanent_hw_address); + if (r < 0) + return r; + } + + if (info->mtu > 0) { + char min_str[DECIMAL_STR_MAX(uint32_t)], max_str[DECIMAL_STR_MAX(uint32_t)]; + + xsprintf(min_str, "%" PRIu32, info->min_mtu); + xsprintf(max_str, "%" PRIu32, info->max_mtu); + + r = table_add_cell(table, NULL, TABLE_FIELD, "MTU"); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell_stringf(table, NULL, "%" PRIu32 "%s%s%s%s%s%s%s", + info->mtu, + info->min_mtu > 0 || info->max_mtu > 0 ? " (" : "", + info->min_mtu > 0 ? "min: " : "", + info->min_mtu > 0 ? min_str : "", + info->min_mtu > 0 && info->max_mtu > 0 ? ", " : "", + info->max_mtu > 0 ? "max: " : "", + info->max_mtu > 0 ? max_str : "", + info->min_mtu > 0 || info->max_mtu > 0 ? ")" : ""); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_string_line(table, "QDisc", info->qdisc); + if (r < 0) + return r; + + if (info->master > 0) { + r = table_add_many(table, + TABLE_FIELD, "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_FIELD, "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_FIELD, "Forward Delay", + TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->forward_delay), + TABLE_FIELD, "Hello Time", + TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->hello_time), + TABLE_FIELD, "Max Age", + TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->max_age), + TABLE_FIELD, "Ageing Time", + TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->ageing_time), + TABLE_FIELD, "Priority", + TABLE_UINT16, info->priority, + TABLE_FIELD, "STP", + TABLE_BOOLEAN, info->stp_state > 0, + TABLE_FIELD, "Multicast IGMP Version", + TABLE_UINT8, info->mcast_igmp_version, + TABLE_FIELD, "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_FIELD, "Port State", + TABLE_STRING, bridge_state_to_string(info->port_state)); + if (r < 0) + return table_log_add_error(r); + } + + } else if (streq_ptr(info->netdev_kind, "bond")) { + r = table_add_many(table, + TABLE_FIELD, "Mode", + TABLE_STRING, bond_mode_to_string(info->mode), + TABLE_FIELD, "Miimon", + TABLE_TIMESPAN_MSEC, info->miimon * USEC_PER_MSEC, + TABLE_FIELD, "Updelay", + TABLE_TIMESPAN_MSEC, info->updelay * USEC_PER_MSEC, + TABLE_FIELD, "Downdelay", + TABLE_TIMESPAN_MSEC, info->downdelay * USEC_PER_MSEC); + if (r < 0) + return table_log_add_error(r); + + } else if (streq_ptr(info->netdev_kind, "vxlan")) { + char ttl[CONST_MAX(STRLEN("auto") + 1, DECIMAL_STR_MAX(uint8_t))]; + + if (info->vxlan_info.vni > 0) { + r = table_add_many(table, + TABLE_FIELD, "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_FIELD, 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_FIELD, "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_FIELD, "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_FIELD, "Underlying Device", + TABLE_IFINDEX, info->vxlan_info.link); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_many(table, + TABLE_FIELD, "Learning", + TABLE_BOOLEAN, info->vxlan_info.learning, + TABLE_FIELD, "RSC", + TABLE_BOOLEAN, info->vxlan_info.rsc, + TABLE_FIELD, "L3MISS", + TABLE_BOOLEAN, info->vxlan_info.l3miss, + TABLE_FIELD, "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_FIELD, "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_FIELD, "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_FIELD, "VLan Id", + TABLE_UINT16, info->vlan_id); + if (r < 0) + return table_log_add_error(r); + + } else if (STRPTR_IN_SET(info->netdev_kind, "ipip", "sit", "gre", "gretap", "erspan", "vti")) { + if (in_addr_is_set(AF_INET, &info->local)) { + r = table_add_many(table, + TABLE_FIELD, "Local", + TABLE_IN_ADDR, &info->local); + if (r < 0) + return table_log_add_error(r); + } + + if (in_addr_is_set(AF_INET, &info->remote)) { + r = table_add_many(table, + TABLE_FIELD, "Remote", + TABLE_IN_ADDR, &info->remote); + if (r < 0) + return table_log_add_error(r); + } + + } else if (STRPTR_IN_SET(info->netdev_kind, "ip6gre", "ip6gretap", "ip6erspan", "vti6")) { + if (in_addr_is_set(AF_INET6, &info->local)) { + r = table_add_many(table, + TABLE_FIELD, "Local", + TABLE_IN6_ADDR, &info->local); + if (r < 0) + return table_log_add_error(r); + } + + if (in_addr_is_set(AF_INET6, &info->remote)) { + r = table_add_many(table, + TABLE_FIELD, "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_FIELD, "VNI", + TABLE_UINT32, info->vni); + if (r < 0) + return table_log_add_error(r); + + if (info->has_tunnel_ipv4 && in_addr_is_set(AF_INET, &info->remote)) { + r = table_add_many(table, + TABLE_FIELD, "Remote", + TABLE_IN_ADDR, &info->remote); + if (r < 0) + return table_log_add_error(r); + } else if (in_addr_is_set(AF_INET6, &info->remote)) { + r = table_add_many(table, + TABLE_FIELD, "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_FIELD, "TTL", + TABLE_UINT8, info->ttl); + if (r < 0) + return table_log_add_error(r); + } + + if (info->tos > 0) { + r = table_add_many(table, + TABLE_FIELD, "TOS", + TABLE_UINT8, info->tos); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_many(table, + TABLE_FIELD, "Port", + TABLE_UINT16, info->tunnel_port, + TABLE_FIELD, "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_FIELD, "IPDoNotFragment", + TABLE_UINT8, info->df); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_many(table, + TABLE_FIELD, "UDPChecksum", + TABLE_BOOLEAN, info->csum, + TABLE_FIELD, "UDP6ZeroChecksumTx", + TABLE_BOOLEAN, info->csum6_tx, + TABLE_FIELD, "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_FIELD, "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_FIELD, "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")) { + const char *p; + + if (info->ipvlan_flags & IPVLAN_F_PRIVATE) + p = "private"; + else if (info->ipvlan_flags & IPVLAN_F_VEPA) + p = "vepa"; + else + p = "bridge"; + + r = table_add_cell(table, NULL, TABLE_FIELD, "Mode"); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell_stringf(table, NULL, "%s (%s)", + ipvlan_mode_to_string(info->ipvlan_mode), p); + if (r < 0) + return table_log_add_error(r); + } + + if (info->has_wlan_link_info) { + _cleanup_free_ char *esc = NULL; + + r = table_add_cell(table, NULL, TABLE_FIELD, "Wi-Fi access point"); + if (r < 0) + return table_log_add_error(r); + + if (info->ssid) + esc = cescape(info->ssid); + + r = table_add_cell_stringf(table, NULL, "%s (%s)", + strnull(esc), + ETHER_ADDR_TO_STR(&info->bssid)); + if (r < 0) + return table_log_add_error(r); + } + + if (info->has_bitrates) { + r = table_add_cell(table, NULL, TABLE_FIELD, "Bit Rate (Tx/Rx)"); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell_stringf(table, NULL, "%sbps/%sbps", + FORMAT_BYTES_FULL(info->tx_bitrate, 0), + FORMAT_BYTES_FULL(info->rx_bitrate, 0)); + if (r < 0) + return table_log_add_error(r); + } + + if (info->has_tx_queues || info->has_rx_queues) { + r = table_add_cell(table, NULL, TABLE_FIELD, "Number of Queues (Tx/Rx)"); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell_stringf(table, NULL, "%" PRIu32 "/%" PRIu32, info->tx_queues, info->rx_queues); + if (r < 0) + return table_log_add_error(r); + } + + if (info->has_ethtool_link_info) { + if (IN_SET(info->autonegotiation, AUTONEG_DISABLE, AUTONEG_ENABLE)) { + r = table_add_many(table, + TABLE_FIELD, "Auto negotiation", + TABLE_BOOLEAN, info->autonegotiation == AUTONEG_ENABLE); + if (r < 0) + return table_log_add_error(r); + } + + if (info->speed > 0 && info->speed != UINT64_MAX) { + r = table_add_many(table, + TABLE_FIELD, "Speed", + TABLE_BPS, info->speed); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_string_line(table, "Duplex", duplex_to_string(info->duplex)); + if (r < 0) + return r; + + r = table_add_string_line(table, "Port", port_to_string(info->port)); + if (r < 0) + return r; + } + + r = dump_addresses(rtnl, lease, table, info->ifindex); + if (r < 0) + return r; + + r = dump_gateways(rtnl, hwdb, table, info->ifindex); + if (r < 0) + return r; + + r = dump_list(table, "DNS", dns); + if (r < 0) + return r; + + r = dump_list(table, "Search Domains", search_domains); + if (r < 0) + return r; + + r = dump_list(table, "Route Domains", route_domains); + if (r < 0) + return r; + + r = dump_list(table, "NTP", ntp); + if (r < 0) + return r; + + r = dump_list(table, "SIP", sip); + if (r < 0) + return r; + + r = dump_ifindexes(table, "Carrier Bound To", carrier_bound_to); + if (r < 0) + return r; + + r = dump_ifindexes(table, "Carrier Bound By", carrier_bound_by); + if (r < 0) + return r; + + r = table_add_string_line(table, "Activation Policy", activation_policy); + if (r < 0) + return r; + + r = sd_network_link_get_required_for_online(info->ifindex); + if (r >= 0) { + r = table_add_many(table, + TABLE_FIELD, "Required For Online", + TABLE_BOOLEAN, r); + if (r < 0) + return table_log_add_error(r); + } + + if (captive_portal) { + r = table_add_many(table, + TABLE_FIELD, "Captive Portal", + TABLE_STRING, captive_portal, + TABLE_SET_URL, captive_portal); + if (r < 0) + return table_log_add_error(r); + } + + if (lease) { + const void *client_id; + size_t client_id_len; + const char *tz; + + r = sd_dhcp_lease_get_timezone(lease, &tz); + if (r >= 0) { + r = table_add_many(table, + TABLE_FIELD, "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_FIELD, "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_FIELD, "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_FIELD, "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; + + /* First line: circle, ifindex, ifname. */ + printf("%s%s%s %d: %s\n", + on_color_operational, special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE), off_color_operational, + info->ifindex, info->name); + + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + + return show_logs(info); +} + +static int system_status(sd_netlink *rtnl, sd_hwdb *hwdb) { + _cleanup_free_ char *operational_state = NULL, *online_state = NULL, *netifs_joined = NULL; + _cleanup_strv_free_ char **netifs = NULL, **dns = NULL, **ntp = NULL, **search_domains = NULL, **route_domains = NULL; + const char *on_color_operational, *off_color_operational, *on_color_online; + _cleanup_(table_unrefp) Table *table = NULL; + 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); + + (void) sd_network_get_online_state(&online_state); + online_state_to_color(online_state, &on_color_online, NULL); + + table = table_new_vertical(); + if (!table) + return log_oom(); + + if (arg_full) + table_set_width(table, 0); + + r = get_files_in_directory("/run/systemd/netif/links/", &netifs); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to list network interfaces: %m"); + else if (r > 0) { + netifs_joined = strv_join(netifs, ", "); + if (!netifs_joined) + return log_oom(); + } + + r = table_add_many(table, + TABLE_FIELD, "State", + TABLE_STRING, strna(operational_state), + TABLE_SET_COLOR, on_color_operational, + TABLE_FIELD, "Online state", + TABLE_STRING, online_state ?: "unknown", + TABLE_SET_COLOR, on_color_online); + if (r < 0) + return table_log_add_error(r); + + r = dump_addresses(rtnl, NULL, table, 0); + if (r < 0) + return r; + + r = dump_gateways(rtnl, hwdb, table, 0); + if (r < 0) + return r; + + (void) sd_network_get_dns(&dns); + r = dump_list(table, "DNS", dns); + if (r < 0) + return r; + + (void) sd_network_get_search_domains(&search_domains); + r = dump_list(table, "Search Domains", search_domains); + if (r < 0) + return r; + + (void) sd_network_get_route_domains(&route_domains); + r = dump_list(table, "Route Domains", route_domains); + if (r < 0) + return r; + + (void) sd_network_get_ntp(&ntp); + r = dump_list(table, "NTP", ntp); + if (r < 0) + return r; + + printf("%s%s%s Interfaces: %s\n", + on_color_operational, special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE), off_color_operational, + strna(netifs_joined)); + + 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; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + if (arg_json_format_flags != JSON_FORMAT_OFF) { + if (arg_all || argc <= 1) + return dump_manager_description(bus); + else + return dump_link_description(bus, strv_skip(argv, 1)); + } + + pager_open(arg_pager_flags); + + r = sd_netlink_open(&rtnl); + if (r < 0) + return log_error_errno(r, "Failed to connect to netlink: %m"); + + r = sd_hwdb_new(&hwdb); + if (r < 0) + log_debug_errno(r, "Failed to open hardware database: %m"); + + if (arg_all) + c = acquire_link_info(bus, rtnl, NULL, &links); + else if (argc <= 1) + return system_status(rtnl, hwdb); + else + c = acquire_link_info(bus, rtnl, argv + 1, &links); + if (c < 0) + return c; + + r = 0; + + bool first = true; + FOREACH_ARRAY(i, links, c) { + if (!first) + putchar('\n'); + + RET_GATHER(r, link_status_one(bus, rtnl, hwdb, i)); + + first = false; + } + + return r; +} + +static char *lldp_capabilities_to_string(uint16_t x) { + static const char characters[] = { + 'o', 'p', 'b', 'w', 'r', 't', 'd', 'a', 'c', 's', 'm', + }; + char *ret; + unsigned i; + + ret = new(char, ELEMENTSOF(characters) + 1); + if (!ret) + return NULL; + + for (i = 0; i < ELEMENTSOF(characters); i++) + ret[i] = (x & (1U << i)) ? characters[i] : '.'; + + ret[i] = 0; + return ret; +} + +static void lldp_capabilities_legend(uint16_t x) { + unsigned cols = columns(); + static const char* const table[] = { + "o - Other", + "p - Repeater", + "b - Bridge", + "w - WLAN Access Point", + "r - Router", + "t - Telephone", + "d - DOCSIS cable device", + "a - Station", + "c - Customer VLAN", + "s - Service VLAN", + "m - Two-port MAC Relay (TPMR)", + }; + + if (x == 0) + return; + + printf("\nCapability Flags:\n"); + for (unsigned w = 0, i = 0; i < ELEMENTSOF(table); i++) + if (x & (1U << i) || arg_all) { + bool newline; + + newline = w + strlen(table[i]) + (w == 0 ? 0 : 2) > cols; + if (newline) + w = 0; + w += printf("%s%s%s", newline ? "\n" : "", w == 0 ? "" : "; ", table[i]); + } + puts(""); +} + +static int link_lldp_status(int argc, char *argv[], void *userdata) { + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + _cleanup_(link_info_array_freep) LinkInfo *links = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + int r, c, m = 0; + uint16_t all = 0; + TableCell *cell; + + r = sd_netlink_open(&rtnl); + if (r < 0) + return log_error_errno(r, "Failed to connect to netlink: %m"); + + c = acquire_link_info(NULL, rtnl, argc > 1 ? argv + 1 : NULL, &links); + if (c < 0) + return c; + + pager_open(arg_pager_flags); + + table = table_new("link", + "chassis-id", + "system-name", + "caps", + "port-id", + "port-description"); + if (!table) + return log_oom(); + + if (arg_full) + table_set_width(table, 0); + + table_set_header(table, arg_legend); + + assert_se(cell = table_get_cell(table, 0, 3)); + table_set_minimum_width(table, cell, 11); + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + + FOREACH_ARRAY(link, links, c) { + _cleanup_fclose_ FILE *f = NULL; + + r = open_lldp_neighbors(link->ifindex, &f); + if (r == -ENOENT) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to open LLDP data for %i, ignoring: %m", link->ifindex); + continue; + } + + for (;;) { + const char *chassis_id = NULL, *port_id = NULL, *system_name = NULL, *port_description = NULL; + _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = NULL; + _cleanup_free_ char *capabilities = NULL; + uint16_t cc; + + r = next_lldp_neighbor(f, &n); + if (r < 0) { + log_warning_errno(r, "Failed to read neighbor data: %m"); + break; + } + if (r == 0) + break; + + (void) sd_lldp_neighbor_get_chassis_id_as_string(n, &chassis_id); + (void) sd_lldp_neighbor_get_port_id_as_string(n, &port_id); + (void) sd_lldp_neighbor_get_system_name(n, &system_name); + (void) sd_lldp_neighbor_get_port_description(n, &port_description); + + if (sd_lldp_neighbor_get_enabled_capabilities(n, &cc) >= 0) { + capabilities = lldp_capabilities_to_string(cc); + all |= cc; + } + + r = table_add_many(table, + TABLE_STRING, link->name, + TABLE_STRING, chassis_id, + TABLE_STRING, system_name, + TABLE_STRING, capabilities, + TABLE_STRING, port_id, + TABLE_STRING, port_description); + if (r < 0) + return table_log_add_error(r); + + m++; + } + } + + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + + if (arg_legend) { + lldp_capabilities_legend(all); + printf("\n%i neighbors listed.\n", m); + } + + return 0; +} + +static int link_delete_send_message(sd_netlink *rtnl, int index) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(rtnl); + assert(index >= 0); + + 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); + assert(index >= 0); + + r = sd_rtnl_message_new_link(rtnl, &req, RTM_SETLINK, index); + if (r < 0) + return rtnl_log_create_error(r); + + if (streq(command, "up")) + r = sd_rtnl_message_link_set_flags(req, IFF_UP, IFF_UP); + else + r = sd_rtnl_message_link_set_flags(req, 0, IFF_UP); + if (r < 0) + return log_error_errno(r, "Could not set link flags: %m"); + + r = sd_netlink_call(rtnl, req, 0, NULL); + if (r < 0) + return r; + + return 0; +} + +static int link_up_down(int argc, char *argv[], void *userdata) { + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + _cleanup_set_free_ Set *indexes = NULL; + int index, r; + void *p; + + r = sd_netlink_open(&rtnl); + if (r < 0) + return log_error_errno(r, "Failed to connect to netlink: %m"); + + indexes = set_new(NULL); + if (!indexes) + return log_oom(); + + for (int i = 1; i < argc; i++) { + index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]); + if (index < 0) + return index; + + r = set_put(indexes, INT_TO_PTR(index)); + if (r < 0) + return log_oom(); + } + + SET_FOREACH(p, indexes) { + index = PTR_TO_INT(p); + r = link_up_down_send_message(rtnl, argv[0], index); + if (r < 0) + return log_error_errno(r, "Failed to bring %s interface %s: %m", + argv[0], FORMAT_IFNAME_FULL(index, FORMAT_IFNAME_IFINDEX)); + } + + return r; +} + +static int link_delete(int argc, char *argv[], void *userdata) { + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + _cleanup_set_free_ Set *indexes = NULL; + int index, r; + void *p; + + r = sd_netlink_open(&rtnl); + if (r < 0) + return log_error_errno(r, "Failed to connect to netlink: %m"); + + indexes = set_new(NULL); + if (!indexes) + return log_oom(); + + for (int i = 1; i < argc; i++) { + index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]); + if (index < 0) + return index; + + r = set_put(indexes, INT_TO_PTR(index)); + if (r < 0) + return log_oom(); + } + + SET_FOREACH(p, indexes) { + index = PTR_TO_INT(p); + r = link_delete_send_message(rtnl, index); + if (r < 0) + return log_error_errno(r, "Failed to delete interface %s: %m", + FORMAT_IFNAME_FULL(index, FORMAT_IFNAME_IFINDEX)); + } + + return r; +} + +static int link_renew_one(sd_bus *bus, int index, const char *name) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(bus); + assert(index >= 0); + assert(name); + + 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, k = 0, r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + for (int i = 1; i < argc; i++) { + index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]); + if (index < 0) + return index; + + r = link_renew_one(bus, index, argv[i]); + if (r < 0 && k >= 0) + k = r; + } + + return k; +} + +static int link_force_renew_one(sd_bus *bus, int index, const char *name) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(bus); + assert(index >= 0); + assert(name); + + 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 k = 0, r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + for (int i = 1; i < argc; i++) { + int index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]); + if (index < 0) + return index; + + r = link_force_renew_one(bus, index, argv[i]); + if (r < 0 && k >= 0) + k = r; + } + + return k; +} + +static int verb_reload(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = bus_call_method(bus, bus_network_mgr, "Reload", &error, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to reload network settings: %s", bus_error_message(&error, r)); + + return 0; +} + +static int verb_reconfigure(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + _cleanup_set_free_ Set *indexes = NULL; + int index, r; + void *p; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + indexes = set_new(NULL); + if (!indexes) + return log_oom(); + + for (int i = 1; i < argc; i++) { + index = rtnl_resolve_interface_or_warn(&rtnl, argv[i]); + if (index < 0) + return index; + + r = set_put(indexes, INT_TO_PTR(index)); + if (r < 0) + return log_oom(); + } + + SET_FOREACH(p, indexes) { + index = PTR_TO_INT(p); + r = bus_call_method(bus, bus_network_mgr, "ReconfigureLink", &error, NULL, "i", index); + if (r < 0) + return log_error_errno(r, "Failed to reconfigure network interface %s: %s", + FORMAT_IFNAME_FULL(index, FORMAT_IFNAME_IFINDEX), + bus_error_message(&error, r)); + } + + return 0; +} + +typedef enum ReloadFlags { + RELOAD_NETWORKD = 1 << 0, + RELOAD_UDEVD = 1 << 1, +} ReloadFlags; + +static int get_config_files_by_name(const char *name, char **ret_path, char ***ret_dropins) { + _cleanup_free_ char *path = NULL; + int r; + + assert(name); + assert(ret_path); + + STRV_FOREACH(i, NETWORK_DIRS) { + _cleanup_free_ char *p = NULL; + + p = path_join(*i, name); + if (!p) + return -ENOMEM; + + r = RET_NERRNO(access(p, F_OK)); + if (r >= 0) { + path = TAKE_PTR(p); + break; + } + + if (r != -ENOENT) + log_debug_errno(r, "Failed to determine whether '%s' exists, ignoring: %m", p); + } + + if (!path) + return -ENOENT; + + if (ret_dropins) { + _cleanup_free_ char *dropin_dirname = NULL; + + dropin_dirname = strjoin(name, ".d"); + if (!dropin_dirname) + return -ENOMEM; + + r = conf_files_list_dropins(ret_dropins, dropin_dirname, /* root = */ NULL, NETWORK_DIRS); + if (r < 0) + return r; + } + + *ret_path = TAKE_PTR(path); + + return 0; +} + +static int get_dropin_by_name( + const char *name, + char * const *dropins, + char **ret) { + + assert(name); + assert(dropins); + assert(ret); + + STRV_FOREACH(i, dropins) + if (path_equal_filename(*i, name)) { + _cleanup_free_ char *d = NULL; + + d = strdup(*i); + if (!d) + return -ENOMEM; + + *ret = TAKE_PTR(d); + return 1; + } + + *ret = NULL; + return 0; +} + +static int get_network_files_by_link( + sd_netlink **rtnl, + const char *link, + char **ret_path, + char ***ret_dropins) { + + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + int r, ifindex; + + assert(rtnl); + assert(link); + assert(ret_path); + assert(ret_dropins); + + ifindex = rtnl_resolve_interface_or_warn(rtnl, link); + if (ifindex < 0) + return ifindex; + + r = sd_network_link_get_network_file(ifindex, &path); + if (r == -ENODATA) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "Link '%s' has no associated network file.", link); + if (r < 0) + return log_error_errno(r, "Failed to get network file for link '%s': %m", link); + + r = sd_network_link_get_network_file_dropins(ifindex, &dropins); + if (r < 0 && r != -ENODATA) + return log_error_errno(r, "Failed to get network drop-ins for link '%s': %m", link); + + *ret_path = TAKE_PTR(path); + *ret_dropins = TAKE_PTR(dropins); + + return 0; +} + +static int get_link_files_by_link(const char *link, char **ret_path, char ***ret_dropins) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + _cleanup_strv_free_ char **dropins_split = NULL; + _cleanup_free_ char *p = NULL; + const char *path, *dropins; + int r; + + assert(link); + assert(ret_path); + assert(ret_dropins); + + r = sd_device_new_from_ifname(&device, link); + if (r < 0) + return log_error_errno(r, "Failed to create sd-device object for link '%s': %m", link); + + r = sd_device_get_property_value(device, "ID_NET_LINK_FILE", &path); + if (r == -ENOENT) + return log_error_errno(r, "Link '%s' has no associated link file.", link); + if (r < 0) + return log_error_errno(r, "Failed to get link file for link '%s': %m", link); + + r = sd_device_get_property_value(device, "ID_NET_LINK_FILE_DROPINS", &dropins); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to get link drop-ins for link '%s': %m", link); + if (r >= 0) { + r = strv_split_full(&dropins_split, dropins, ":", EXTRACT_CUNESCAPE); + if (r < 0) + return log_error_errno(r, "Failed to parse link drop-ins for link '%s': %m", link); + } + + p = strdup(path); + if (!p) + return log_oom(); + + *ret_path = TAKE_PTR(p); + *ret_dropins = TAKE_PTR(dropins_split); + + return 0; +} + +static int get_config_files_by_link_config( + const char *link_config, + sd_netlink **rtnl, + char **ret_path, + char ***ret_dropins, + ReloadFlags *ret_reload) { + + _cleanup_strv_free_ char **dropins = NULL, **link_config_split = NULL; + _cleanup_free_ char *path = NULL; + const char *ifname, *type; + ReloadFlags reload; + size_t n; + int r; + + assert(link_config); + assert(rtnl); + assert(ret_path); + assert(ret_dropins); + + link_config_split = strv_split(link_config, ":"); + if (!link_config_split) + return log_oom(); + + n = strv_length(link_config_split); + if (n == 0 || isempty(link_config_split[0])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No link name is given."); + if (n > 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid link config '%s'.", link_config); + + ifname = link_config_split[0]; + type = n == 2 ? link_config_split[1] : "network"; + + if (streq(type, "network")) { + if (!networkd_is_running()) + return log_error_errno(SYNTHETIC_ERRNO(ESRCH), + "Cannot get network file for link if systemd-networkd is not running."); + + r = get_network_files_by_link(rtnl, ifname, &path, &dropins); + if (r < 0) + return r; + + reload = RELOAD_NETWORKD; + } else if (streq(type, "link")) { + r = get_link_files_by_link(ifname, &path, &dropins); + if (r < 0) + return r; + + reload = RELOAD_UDEVD; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid config type '%s' for link '%s'.", type, ifname); + + *ret_path = TAKE_PTR(path); + *ret_dropins = TAKE_PTR(dropins); + + if (ret_reload) + *ret_reload = reload; + + return 0; +} + +static int add_config_to_edit( + EditFileContext *context, + const char *path, + char * const *dropins) { + + _cleanup_free_ char *new_path = NULL, *dropin_path = NULL, *old_dropin = NULL; + _cleanup_strv_free_ char **comment_paths = NULL; + int r; + + assert(context); + assert(path); + assert(!arg_drop_in || dropins); + + if (path_startswith(path, "/usr")) { + _cleanup_free_ char *name = NULL; + + r = path_extract_filename(path, &name); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from '%s': %m", path); + + new_path = path_join(NETWORK_DIRS[0], name); + if (!new_path) + return log_oom(); + } + + if (!arg_drop_in) + return edit_files_add(context, new_path ?: path, path, NULL); + + r = get_dropin_by_name(arg_drop_in, dropins, &old_dropin); + if (r < 0) + return log_error_errno(r, "Failed to acquire drop-in '%s': %m", arg_drop_in); + + if (r > 0 && !path_startswith(old_dropin, "/usr")) + /* An existing drop-in is found and not in /usr/. Let's edit it directly. */ + dropin_path = TAKE_PTR(old_dropin); + else { + /* No drop-in was found or an existing drop-in resides in /usr/. Let's create + * a new drop-in file. */ + dropin_path = strjoin(new_path ?: path, ".d/", arg_drop_in); + if (!dropin_path) + return log_oom(); + } + + comment_paths = strv_new(path); + if (!comment_paths) + return log_oom(); + + r = strv_extend_strv(&comment_paths, dropins, /* filter_duplicates = */ false); + if (r < 0) + return log_oom(); + + return edit_files_add(context, dropin_path, old_dropin, comment_paths); +} + +static int udevd_reload(sd_bus *bus) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL; + const char *job_path; + int r; + + assert(bus); + + r = bus_wait_for_jobs_new(bus, &w); + if (r < 0) + return log_error_errno(r, "Could not watch jobs: %m"); + + r = bus_call_method(bus, + bus_systemd_mgr, + "ReloadUnit", + &error, + &reply, + "ss", + "systemd-udevd.service", + "replace"); + if (r < 0) + return log_error_errno(r, "Failed to reload systemd-udevd: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "o", &job_path); + if (r < 0) + return bus_log_parse_error(r); + + r = bus_wait_for_jobs_one(w, job_path, /* quiet = */ true, NULL); + if (r == -ENOEXEC) { + log_debug("systemd-udevd is not running, skipping reload."); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to reload systemd-udevd: %m"); + + return 1; +} + +static int verb_edit(int argc, char *argv[], void *userdata) { + _cleanup_(edit_file_context_done) EditFileContext context = { + .marker_start = DROPIN_MARKER_START, + .marker_end = DROPIN_MARKER_END, + .remove_parent = !!arg_drop_in, + }; + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + ReloadFlags reload = 0; + int r; + + if (!on_tty()) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit network config files if not on a tty."); + + r = mac_selinux_init(); + if (r < 0) + return r; + + STRV_FOREACH(name, strv_skip(argv, 1)) { + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + const char *link_config; + + link_config = startswith(*name, "@"); + if (link_config) { + ReloadFlags flags; + + r = get_config_files_by_link_config(link_config, &rtnl, &path, &dropins, &flags); + if (r < 0) + return r; + + reload |= flags; + + r = add_config_to_edit(&context, path, dropins); + if (r < 0) + return r; + + continue; + } + + if (ENDSWITH_SET(*name, ".network", ".netdev")) + reload |= RELOAD_NETWORKD; + else if (endswith(*name, ".link")) + reload |= RELOAD_UDEVD; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid network config name '%s'.", *name); + + r = get_config_files_by_name(*name, &path, &dropins); + if (r == -ENOENT) { + if (arg_drop_in) + return log_error_errno(r, "Cannot find network config '%s'.", *name); + + log_debug("No existing network config '%s' found, creating a new file.", *name); + + path = path_join(NETWORK_DIRS[0], *name); + if (!path) + return log_oom(); + + r = edit_files_add(&context, path, NULL, NULL); + if (r < 0) + return r; + continue; + } + if (r < 0) + return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name); + + r = add_config_to_edit(&context, path, dropins); + if (r < 0) + return r; + } + + r = do_edit_files_and_install(&context); + if (r < 0) + return r; + + if (arg_no_reload) + return 0; + + if (!sd_booted() || running_in_chroot() > 0) { + log_debug("System is not booted with systemd or is running in chroot, skipping reload."); + return 0; + } + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to system bus: %m"); + + if (FLAGS_SET(reload, RELOAD_UDEVD)) { + r = udevd_reload(bus); + if (r < 0) + return r; + } + + if (FLAGS_SET(reload, RELOAD_NETWORKD)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + if (!networkd_is_running()) { + log_debug("systemd-networkd is not running, skipping reload."); + return 0; + } + + r = bus_call_method(bus, bus_network_mgr, "Reload", &error, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to reload systemd-networkd: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_cat(int argc, char *argv[], void *userdata) { + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + int r, ret = 0; + + pager_open(arg_pager_flags); + + STRV_FOREACH(name, strv_skip(argv, 1)) { + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + const char *link_config; + + link_config = startswith(*name, "@"); + if (link_config) { + r = get_config_files_by_link_config(link_config, &rtnl, &path, &dropins, /* ret_reload = */ NULL); + if (r < 0) + return ret < 0 ? ret : r; + } else { + r = get_config_files_by_name(*name, &path, &dropins); + if (r == -ENOENT) { + log_error_errno(r, "Cannot find network config file '%s'.", *name); + ret = ret < 0 ? ret : r; + continue; + } + if (r < 0) { + log_error_errno(r, "Failed to get the path of network config '%s': %m", *name); + return ret < 0 ? ret : r; + } + } + + r = cat_files(path, dropins, /* flags = */ CAT_FORMAT_HAS_SECTIONS); + if (r < 0) + return ret < 0 ? ret : r; + } + + return ret; +} + +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" + " edit FILES|DEVICES... Edit network configuration files\n" + " cat FILES|DEVICES... Show network configuration files\n" + "\nOptions:\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not show the headers and footers\n" + " -a --all Show status for all links\n" + " -s --stats Show detailed link statistics\n" + " -l --full Do not ellipsize output\n" + " -n --lines=INTEGER Number of journal entries to show\n" + " --json=pretty|short|off\n" + " Generate JSON output\n" + " --no-reload Do not reload systemd-networkd or systemd-udevd\n" + " after editing network config\n" + " --drop-in=NAME Edit specified drop-in instead of main config file\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_JSON, + ARG_NO_RELOAD, + ARG_DROP_IN, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "all", no_argument, NULL, 'a' }, + { "stats", no_argument, NULL, 's' }, + { "full", no_argument, NULL, 'l' }, + { "lines", required_argument, NULL, 'n' }, + { "json", required_argument, NULL, ARG_JSON }, + { "no-reload", no_argument, NULL, ARG_NO_RELOAD }, + { "drop-in", required_argument, NULL, ARG_DROP_IN }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hasln:", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_NO_RELOAD: + arg_no_reload = true; + break; + + case ARG_DROP_IN: + if (isempty(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty drop-in file name."); + + if (!endswith(optarg, ".conf")) { + char *conf; + + conf = strjoin(optarg, ".conf"); + if (!conf) + return log_oom(); + + free_and_replace(arg_drop_in, conf); + } else { + r = free_and_strdup(&arg_drop_in, optarg); + if (r < 0) + return log_oom(); + } + + if (!filename_is_valid(arg_drop_in)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid drop-in file name '%s'.", arg_drop_in); + + break; + + case 'a': + arg_all = true; + break; + + case 's': + arg_stats = true; + break; + + case 'l': + arg_full = true; + break; + + case 'n': + if (safe_atou(optarg, &arg_lines) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Failed to parse lines '%s'", optarg); + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + return 1; +} + +static int networkctl_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "list", VERB_ANY, VERB_ANY, VERB_DEFAULT|VERB_ONLINE_ONLY, list_links }, + { "status", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, 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, VERB_ONLINE_ONLY, link_renew }, + { "forcerenew", 2, VERB_ANY, VERB_ONLINE_ONLY, link_force_renew }, + { "reconfigure", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_reconfigure }, + { "reload", 1, 1, VERB_ONLINE_ONLY, verb_reload }, + { "edit", 2, VERB_ANY, 0, verb_edit }, + { "cat", 2, VERB_ANY, 0, verb_cat }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char* argv[]) { + int r; + + log_setup(); + + sigbus_install(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return networkctl_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/network/networkd-address-generation.c b/src/network/networkd-address-generation.c new file mode 100644 index 0000000..65f0009 --- /dev/null +++ b/src/network/networkd-address-generation.c @@ -0,0 +1,439 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if_arp.h> + +#include "sd-id128.h" + +#include "arphrd-util.h" +#include "id128-util.h" +#include "memory-util.h" +#include "networkd-address-generation.h" +#include "networkd-link.h" +#include "networkd-network.h" +#include "string-util.h" + +#define DAD_CONFLICTS_IDGEN_RETRIES_RFC7217 3 + +/* https://www.iana.org/assignments/ipv6-interface-ids/ipv6-interface-ids.xml */ +#define SUBNET_ROUTER_ANYCAST_ADDRESS ((const struct in6_addr) { .s6_addr = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } }) +#define SUBNET_ROUTER_ANYCAST_PREFIXLEN 64 +#define RESERVED_INTERFACE_IDENTIFIERS_ADDRESS ((const struct in6_addr) { .s6_addr = { 0x02, 0x00, 0x5E, 0xFF, 0xFE } }) +#define RESERVED_INTERFACE_IDENTIFIERS_PREFIXLEN 40 +#define RESERVED_SUBNET_ANYCAST_ADDRESSES ((const struct in6_addr) { .s6_addr = { 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80 } }) +#define RESERVED_SUBNET_ANYCAST_PREFIXLEN 57 + +#define DHCP_PD_APP_ID SD_ID128_MAKE(fb,b9,37,ca,4a,ed,4a,4d,b0,70,7f,aa,71,c0,c9,85) +#define NDISC_APP_ID SD_ID128_MAKE(13,ac,81,a7,d5,3f,49,78,92,79,5d,0c,29,3a,bc,7e) +#define RADV_APP_ID SD_ID128_MAKE(1f,1e,90,c8,5c,78,4f,dc,8e,61,2d,59,0d,53,c1,25) + +typedef enum AddressGenerationType { + ADDRESS_GENERATION_EUI64, + ADDRESS_GENERATION_STATIC, + ADDRESS_GENERATION_PREFIXSTABLE, + _ADDRESS_GENERATION_TYPE_MAX, + _ADDRESS_GENERATION_TYPE_INVALID = -EINVAL, +} AddressGenerationType; + +typedef struct IPv6Token { + AddressGenerationType type; + struct in6_addr address; + sd_id128_t secret_key; +} IPv6Token; + +static int generate_eui64_address(const Link *link, const struct in6_addr *prefix, struct in6_addr *ret) { + assert(link); + assert(prefix); + assert(ret); + + memcpy(ret->s6_addr, prefix, 8); + + switch (link->iftype) { + case ARPHRD_INFINIBAND: + /* Use last 8 byte. See RFC4391 section 8 */ + memcpy(&ret->s6_addr[8], &link->hw_addr.infiniband[INFINIBAND_ALEN - 8], 8); + break; + case ARPHRD_ETHER: + /* see RFC4291 section 2.5.1 */ + ret->s6_addr[8] = link->hw_addr.ether.ether_addr_octet[0]; + ret->s6_addr[9] = link->hw_addr.ether.ether_addr_octet[1]; + ret->s6_addr[10] = link->hw_addr.ether.ether_addr_octet[2]; + ret->s6_addr[11] = 0xff; + ret->s6_addr[12] = 0xfe; + ret->s6_addr[13] = link->hw_addr.ether.ether_addr_octet[3]; + ret->s6_addr[14] = link->hw_addr.ether.ether_addr_octet[4]; + ret->s6_addr[15] = link->hw_addr.ether.ether_addr_octet[5]; + break; + default: + return log_link_debug_errno(link, SYNTHETIC_ERRNO(EINVAL), + "Token=eui64 is not supported for interface type %s, ignoring.", + strna(arphrd_to_name(link->iftype))); + } + + ret->s6_addr[8] ^= 1 << 1; + return 0; +} + +static bool stable_private_address_is_valid(const struct in6_addr *addr) { + assert(addr); + + /* According to rfc4291, generated address should not be in the following ranges. */ + + if (in6_addr_prefix_covers(&SUBNET_ROUTER_ANYCAST_ADDRESS, SUBNET_ROUTER_ANYCAST_PREFIXLEN, addr)) + return false; + + if (in6_addr_prefix_covers(&RESERVED_INTERFACE_IDENTIFIERS_ADDRESS, RESERVED_INTERFACE_IDENTIFIERS_PREFIXLEN, addr)) + return false; + + if (in6_addr_prefix_covers(&RESERVED_SUBNET_ANYCAST_ADDRESSES, RESERVED_SUBNET_ANYCAST_PREFIXLEN, addr)) + return false; + + return true; +} + +static void generate_stable_private_address_one( + Link *link, + const sd_id128_t *secret_key, + const struct in6_addr *prefix, + uint8_t dad_counter, + struct in6_addr *ret) { + + struct siphash state; + uint64_t rid; + + assert(link); + assert(secret_key); + assert(prefix); + assert(ret); + + /* According to RFC7217 section 5.1 + * RID = F(Prefix, Net_Iface, Network_ID, DAD_Counter, secret_key) */ + + siphash24_init(&state, secret_key->bytes); + + siphash24_compress(prefix, 8, &state); + siphash24_compress_string(link->ifname, &state); + if (link->iftype == ARPHRD_INFINIBAND) + /* Only last 8 bytes of IB MAC are stable */ + siphash24_compress(&link->hw_addr.infiniband[INFINIBAND_ALEN - 8], 8, &state); + else + siphash24_compress(link->hw_addr.bytes, link->hw_addr.length, &state); + + if (link->ssid) + siphash24_compress_string(link->ssid, &state); + + siphash24_compress(&dad_counter, sizeof(uint8_t), &state); + + rid = htole64(siphash24_finalize(&state)); + + memcpy(ret->s6_addr, prefix->s6_addr, 8); + memcpy(ret->s6_addr + 8, &rid, 8); +} + +static int generate_stable_private_address( + Link *link, + const sd_id128_t *app_id, + const sd_id128_t *secret_key, + const struct in6_addr *prefix, + struct in6_addr *ret) { + + sd_id128_t secret_machine_key; + struct in6_addr addr; + uint8_t i; + int r; + + assert(link); + assert(app_id); + assert(secret_key); + assert(prefix); + assert(ret); + + if (sd_id128_is_null(*secret_key)) { + r = sd_id128_get_machine_app_specific(*app_id, &secret_machine_key); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to generate secret key for IPv6 stable private address: %m"); + + secret_key = &secret_machine_key; + } + + /* While this loop uses dad_counter and a retry limit as specified in RFC 7217, the loop does + * not actually attempt Duplicate Address Detection; the counter will be incremented only when + * the address generation algorithm produces an invalid address, and the loop may exit with an + * address which ends up being unusable due to duplication on the link. */ + for (i = 0; i < DAD_CONFLICTS_IDGEN_RETRIES_RFC7217; i++) { + generate_stable_private_address_one(link, secret_key, prefix, i, &addr); + + if (stable_private_address_is_valid(&addr)) + break; + } + if (i >= DAD_CONFLICTS_IDGEN_RETRIES_RFC7217) + /* propagate recognizable errors. */ + return log_link_debug_errno(link, SYNTHETIC_ERRNO(ENOANO), + "Failed to generate stable private address."); + + *ret = addr; + return 0; +} + +static int generate_addresses( + Link *link, + Set *tokens, + const sd_id128_t *app_id, + const struct in6_addr *prefix, + uint8_t prefixlen, + Set **ret) { + + _cleanup_set_free_ Set *addresses = NULL; + struct in6_addr masked; + IPv6Token *j; + int r; + + assert(link); + assert(app_id); + assert(prefix); + assert(prefixlen > 0 && prefixlen <= 64); + assert(ret); + + masked = *prefix; + in6_addr_mask(&masked, prefixlen); + + SET_FOREACH(j, tokens) { + struct in6_addr addr, *copy; + + switch (j->type) { + case ADDRESS_GENERATION_EUI64: + if (generate_eui64_address(link, &masked, &addr) < 0) + continue; + break; + + case ADDRESS_GENERATION_STATIC: + memcpy(addr.s6_addr, masked.s6_addr, 8); + memcpy(addr.s6_addr + 8, j->address.s6_addr + 8, 8); + break; + + case ADDRESS_GENERATION_PREFIXSTABLE: + if (in6_addr_is_set(&j->address) && !in6_addr_equal(&j->address, &masked)) + continue; + + if (generate_stable_private_address(link, app_id, &j->secret_key, &masked, &addr) < 0) + continue; + + break; + + default: + assert_not_reached(); + } + + copy = newdup(struct in6_addr, &addr, 1); + if (!copy) + return -ENOMEM; + + r = set_ensure_consume(&addresses, &in6_addr_hash_ops_free, copy); + if (r < 0) + return r; + } + + /* fall back to EUI-64 if no token is provided */ + if (set_isempty(addresses)) { + _cleanup_free_ struct in6_addr *addr = NULL; + + addr = new(struct in6_addr, 1); + if (!addr) + return -ENOMEM; + + if (IN_SET(link->iftype, ARPHRD_ETHER, ARPHRD_INFINIBAND)) + r = generate_eui64_address(link, &masked, addr); + else + r = generate_stable_private_address(link, app_id, &SD_ID128_NULL, &masked, addr); + if (r < 0) + return r; + + r = set_ensure_consume(&addresses, &in6_addr_hash_ops_free, TAKE_PTR(addr)); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(addresses); + return 0; +} + +int dhcp_pd_generate_addresses(Link *link, const struct in6_addr *prefix, Set **ret) { + return generate_addresses(link, link->network->dhcp_pd_tokens, &DHCP_PD_APP_ID, prefix, 64, ret); +} + +int ndisc_generate_addresses(Link *link, const struct in6_addr *prefix, uint8_t prefixlen, Set **ret) { + return generate_addresses(link, link->network->ndisc_tokens, &NDISC_APP_ID, prefix, prefixlen, ret); +} + +int radv_generate_addresses(Link *link, Set *tokens, const struct in6_addr *prefix, uint8_t prefixlen, Set **ret) { + return generate_addresses(link, tokens, &RADV_APP_ID, prefix, prefixlen, ret); +} + +static void ipv6_token_hash_func(const IPv6Token *p, struct siphash *state) { + siphash24_compress(&p->type, sizeof(p->type), state); + siphash24_compress(&p->address, sizeof(p->address), state); + id128_hash_func(&p->secret_key, state); +} + +static int ipv6_token_compare_func(const IPv6Token *a, const IPv6Token *b) { + int r; + + r = CMP(a->type, b->type); + if (r != 0) + return r; + + r = memcmp(&a->address, &b->address, sizeof(struct in6_addr)); + if (r != 0) + return r; + + return id128_compare_func(&a->secret_key, &b->secret_key); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + ipv6_token_hash_ops, + IPv6Token, + ipv6_token_hash_func, + ipv6_token_compare_func, + free); + +static int ipv6_token_add(Set **tokens, AddressGenerationType type, const struct in6_addr *addr, const sd_id128_t *secret_key) { + IPv6Token *p; + + assert(tokens); + assert(type >= 0 && type < _ADDRESS_GENERATION_TYPE_MAX); + assert(addr); + assert(secret_key); + + p = new(IPv6Token, 1); + if (!p) + return -ENOMEM; + + *p = (IPv6Token) { + .type = type, + .address = *addr, + .secret_key = *secret_key, + }; + + return set_ensure_consume(tokens, &ipv6_token_hash_ops, p); +} + +int config_parse_address_generation_type( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *addr_alloc = NULL; + sd_id128_t secret_key = SD_ID128_NULL; + union in_addr_union buffer = {}; + AddressGenerationType type; + Set **tokens = ASSERT_PTR(data); + const char *addr; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *tokens = set_free(*tokens); + return 0; + } + + if ((addr = startswith(rvalue, "prefixstable"))) { + const char *comma; + + type = ADDRESS_GENERATION_PREFIXSTABLE; + + if (*addr == ':') { + addr++; + + comma = strchr(addr, ','); + if (comma) { + addr_alloc = strndup(addr, comma - addr); + if (!addr_alloc) + return log_oom(); + + addr = addr_alloc; + } + } else if (*addr == ',') + comma = TAKE_PTR(addr); + else if (*addr == '\0') { + comma = NULL; + addr = NULL; + } else { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid IPv6 token mode in %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (comma) { + r = id128_from_string_nonzero(comma + 1, &secret_key); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + r == -ENXIO ? "Secret key in %s= cannot be null, ignoring assignment: %s" + : "Failed to parse secret key in %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + } + + } else if (streq(rvalue, "eui64")) { + type = ADDRESS_GENERATION_EUI64; + addr = NULL; + } else { + type = ADDRESS_GENERATION_STATIC; + + addr = startswith(rvalue, "static:"); + if (!addr) + addr = rvalue; + } + + if (addr) { + r = in_addr_from_string(AF_INET6, addr, &buffer); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse IP address in %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + } + + switch (type) { + case ADDRESS_GENERATION_EUI64: + assert(in6_addr_is_null(&buffer.in6)); + break; + + case ADDRESS_GENERATION_STATIC: + /* Only last 64 bits are used. */ + memzero(buffer.in6.s6_addr, 8); + + if (in6_addr_is_null(&buffer.in6)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "IPv6 address in %s= cannot be the ANY address, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + break; + + case ADDRESS_GENERATION_PREFIXSTABLE: + /* At most, the initial 64 bits are used. */ + (void) in6_addr_mask(&buffer.in6, 64); + break; + + default: + assert_not_reached(); + } + + r = ipv6_token_add(tokens, type, &buffer.in6, &secret_key); + if (r < 0) + return log_oom(); + + return 0; +} diff --git a/src/network/networkd-address-generation.h b/src/network/networkd-address-generation.h new file mode 100644 index 0000000..901b2ec --- /dev/null +++ b/src/network/networkd-address-generation.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" +#include "in-addr-util.h" +#include "set.h" + +typedef struct Link Link; + +int dhcp_pd_generate_addresses(Link *link, const struct in6_addr *prefix, Set **ret); +int ndisc_generate_addresses(Link *link, const struct in6_addr *prefix, uint8_t prefixlen, Set **ret); +int radv_generate_addresses(Link *link, Set *tokens, const struct in6_addr *prefix, uint8_t prefixlen, Set **ret); + +CONFIG_PARSER_PROTOTYPE(config_parse_address_generation_type); diff --git a/src/network/networkd-address-label.c b/src/network/networkd-address-label.c new file mode 100644 index 0000000..745b959 --- /dev/null +++ b/src/network/networkd-address-label.c @@ -0,0 +1,298 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <linux/if_addrlabel.h> + +#include "alloc-util.h" +#include "netlink-util.h" +#include "networkd-address-label.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "parse-util.h" + +AddressLabel *address_label_free(AddressLabel *label) { + if (!label) + return NULL; + + if (label->network) { + assert(label->section); + hashmap_remove(label->network->address_labels_by_section, label->section); + } + + config_section_free(label->section); + return mfree(label); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(AddressLabel, address_label_free); + +static int address_label_new_static(Network *network, const char *filename, unsigned section_line, AddressLabel **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(address_label_freep) AddressLabel *label = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + label = hashmap_get(network->address_labels_by_section, n); + if (label) { + *ret = TAKE_PTR(label); + return 0; + } + + label = new(AddressLabel, 1); + if (!label) + return -ENOMEM; + + *label = (AddressLabel) { + .network = network, + .section = TAKE_PTR(n), + .label = UINT32_MAX, + }; + + r = hashmap_ensure_put(&network->address_labels_by_section, &config_section_hash_ops, label->section, label); + if (r < 0) + return r; + + *ret = TAKE_PTR(label); + return 0; +} + +static int address_label_configure_handler( + sd_netlink *rtnl, + sd_netlink_message *m, + Request *req, + Link *link, + void *userdata) { + + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not set address label"); + link_enter_failed(link); + return 1; + } + + if (link->static_address_label_messages == 0) { + log_link_debug(link, "Addresses label set"); + link->static_address_labels_configured = true; + link_check_ready(link); + } + + return 1; +} + +static int address_label_configure(AddressLabel *label, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(label); + assert(link); + assert(link->ifindex > 0); + assert(link->manager); + assert(link->manager->rtnl); + assert(req); + + r = sd_rtnl_message_new_addrlabel(link->manager->rtnl, &m, RTM_NEWADDRLABEL, + link->ifindex, AF_INET6); + if (r < 0) + return r; + + r = sd_rtnl_message_addrlabel_set_prefixlen(m, label->prefixlen); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, IFAL_LABEL, label->label); + if (r < 0) + return r; + + r = sd_netlink_message_append_in6_addr(m, IFA_ADDRESS, &label->prefix); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static int address_label_process_request(Request *req, Link *link, void *userdata) { + AddressLabel *label = ASSERT_PTR(userdata); + int r; + + assert(req); + assert(link); + + if (!link_is_ready_to_configure(link, false)) + return 0; + + r = address_label_configure(label, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure address label: %m"); + + return 1; +} + +int link_request_static_address_labels(Link *link) { + AddressLabel *label; + int r; + + assert(link); + assert(link->network); + + link->static_address_labels_configured = false; + + HASHMAP_FOREACH(label, link->network->address_labels_by_section) { + r = link_queue_request_full(link, REQUEST_TYPE_ADDRESS_LABEL, + label, NULL, trivial_hash_func, trivial_compare_func, + address_label_process_request, + &link->static_address_label_messages, + address_label_configure_handler, NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request address label: %m"); + } + + if (link->static_address_label_messages == 0) { + link->static_address_labels_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Setting address labels."); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +static int address_label_section_verify(AddressLabel *label) { + assert(label); + assert(label->section); + + if (section_is_invalid(label->section)) + return -EINVAL; + + if (!label->prefix_set) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: [IPv6AddressLabel] section without Prefix= setting specified. " + "Ignoring [IPv6AddressLabel] section from line %u.", + label->section->filename, label->section->line); + + if (label->label == UINT32_MAX) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: [IPv6AddressLabel] section without Label= setting specified. " + "Ignoring [IPv6AddressLabel] section from line %u.", + label->section->filename, label->section->line); + + return 0; +} + +void network_drop_invalid_address_labels(Network *network) { + AddressLabel *label; + + assert(network); + + HASHMAP_FOREACH(label, network->address_labels_by_section) + if (address_label_section_verify(label) < 0) + address_label_free(label); +} + +int config_parse_address_label_prefix( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(address_label_free_or_set_invalidp) AddressLabel *n = NULL; + Network *network = userdata; + unsigned char prefixlen; + union in_addr_union a; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_label_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = in_addr_prefix_from_string(rvalue, AF_INET6, &a, &prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid prefix for address label, ignoring assignment: %s", rvalue); + return 0; + } + if (in6_addr_is_ipv4_mapped_address(&a.in6) && prefixlen > 96) { + /* See ip6addrlbl_alloc() in net/ipv6/addrlabel.c of kernel. */ + log_syntax(unit, LOG_WARNING, filename, line, 0, + "The prefix length of IPv4 mapped address for address label must be equal to or smaller than 96, " + "ignoring assignment: %s", rvalue); + return 0; + } + + n->prefix = a.in6; + n->prefixlen = prefixlen; + n->prefix_set = true; + + TAKE_PTR(n); + return 0; +} + +int config_parse_address_label( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(address_label_free_or_set_invalidp) AddressLabel *n = NULL; + Network *network = userdata; + uint32_t k; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_label_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = safe_atou32(rvalue, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse address label, ignoring: %s", rvalue); + return 0; + } + + if (k == UINT_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Address label is invalid, ignoring: %s", rvalue); + return 0; + } + + n->label = k; + TAKE_PTR(n); + + return 0; +} diff --git a/src/network/networkd-address-label.h b/src/network/networkd-address-label.h new file mode 100644 index 0000000..1e2ee70 --- /dev/null +++ b/src/network/networkd-address-label.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> + +#include "conf-parser.h" +#include "in-addr-util.h" +#include "networkd-util.h" + +typedef struct Link Link; +typedef struct Network Network; + +typedef struct AddressLabel { + Network *network; + ConfigSection *section; + + uint32_t label; + struct in6_addr prefix; + unsigned char prefixlen; + bool prefix_set; +} AddressLabel; + +AddressLabel *address_label_free(AddressLabel *label); + +void network_drop_invalid_address_labels(Network *network); + +int link_request_static_address_labels(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_address_label); +CONFIG_PARSER_PROTOTYPE(config_parse_address_label_prefix); diff --git a/src/network/networkd-address-pool.c b/src/network/networkd-address-pool.c new file mode 100644 index 0000000..d9ac78a --- /dev/null +++ b/src/network/networkd-address-pool.c @@ -0,0 +1,187 @@ +/* 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 "networkd-queue.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_intersect( + const Address *a, + int family, + const union in_addr_union *u, + unsigned prefixlen) { + + assert(a); + assert(u); + + if (a->family != family) + return false; + + return in_addr_prefix_intersect(family, u, prefixlen, &a->in_addr, a->prefixlen); +} + +static bool address_pool_prefix_is_taken( + AddressPool *p, + const union in_addr_union *u, + unsigned prefixlen) { + + Address *a; + Link *l; + Network *n; + Request *req; + + assert(p); + assert(u); + + /* Don't clash with assigned addresses. */ + HASHMAP_FOREACH(l, p->manager->links_by_index) + SET_FOREACH(a, l->addresses) + if (address_intersect(a, p->family, u, prefixlen)) + return true; + + /* And don't clash with configured but un-assigned addresses either. */ + ORDERED_HASHMAP_FOREACH(n, p->manager->networks) + ORDERED_HASHMAP_FOREACH(a, n->addresses_by_section) + if (address_intersect(a, p->family, u, prefixlen)) + return true; + + /* Also check queued addresses. */ + ORDERED_SET_FOREACH(req, p->manager->request_queue) { + if (req->type != REQUEST_TYPE_ADDRESS) + continue; + + if (address_intersect(req->userdata, p->family, u, prefixlen)) + return true; + } + + return false; +} + +static int address_pool_acquire_one(AddressPool *p, int family, unsigned prefixlen, union in_addr_union *found) { + union in_addr_union u; + int r; + + assert(p); + assert(prefixlen > 0); + assert(found); + + if (p->family != family) + return 0; + + if (p->prefixlen >= prefixlen) + return 0; + + u = p->in_addr; + + for (unsigned i = 0; i < RANDOM_PREFIX_TRIAL_MAX; i++) { + r = in_addr_random_prefix(p->family, &u, p->prefixlen, prefixlen); + if (r <= 0) + return r; + + if (!address_pool_prefix_is_taken(p, &u, prefixlen)) { + log_debug("Found range %s", IN_ADDR_PREFIX_TO_STRING(p->family, &u, prefixlen)); + + *found = u; + return 1; + } + } + + return 0; +} + +int address_pool_acquire(Manager *m, int family, unsigned prefixlen, union in_addr_union *found) { + AddressPool *p; + int r; + + assert(m); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(prefixlen > 0); + assert(found); + + ORDERED_SET_FOREACH(p, m->address_pools) { + r = address_pool_acquire_one(p, family, prefixlen, found); + if (r != 0) + return r; + } + + return 0; +} diff --git a/src/network/networkd-address-pool.h b/src/network/networkd-address-pool.h new file mode 100644 index 0000000..93bdec8 --- /dev/null +++ b/src/network/networkd-address-pool.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "in-addr-util.h" + +typedef struct Manager Manager; + +typedef struct AddressPool { + Manager *manager; + + int family; + unsigned prefixlen; + union in_addr_union in_addr; +} AddressPool; + +int address_pool_setup_default(Manager *m); +int address_pool_acquire(Manager *m, int family, unsigned prefixlen, union in_addr_union *found); diff --git a/src/network/networkd-address.c b/src/network/networkd-address.c new file mode 100644 index 0000000..0e4d87b --- /dev/null +++ b/src/network/networkd-address.c @@ -0,0 +1,2566 @@ +/* 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 "logarithm.h" +#include "memory-util.h" +#include "netlink-util.h" +#include "networkd-address-pool.h" +#include "networkd-address.h" +#include "networkd-dhcp-server.h" +#include "networkd-ipv4acd.h" +#include "networkd-manager.h" +#include "networkd-netlabel.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "networkd-route-util.h" +#include "networkd-route.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" + +#define ADDRESSES_PER_LINK_MAX 2048U +#define STATIC_ADDRESSES_PER_NETWORK_MAX 1024U + +#define KNOWN_FLAGS \ + (IFA_F_SECONDARY | \ + IFA_F_NODAD | \ + IFA_F_OPTIMISTIC | \ + IFA_F_DADFAILED | \ + IFA_F_HOMEADDRESS | \ + IFA_F_DEPRECATED | \ + IFA_F_TENTATIVE | \ + IFA_F_PERMANENT | \ + IFA_F_MANAGETEMPADDR | \ + IFA_F_NOPREFIXROUTE | \ + IFA_F_MCAUTOJOIN | \ + IFA_F_STABLE_PRIVACY) + +/* From net/ipv4/devinet.c */ +#define IPV6ONLY_FLAGS \ + (IFA_F_NODAD | \ + IFA_F_OPTIMISTIC | \ + IFA_F_DADFAILED | \ + IFA_F_HOMEADDRESS | \ + IFA_F_TENTATIVE | \ + IFA_F_MANAGETEMPADDR | \ + IFA_F_STABLE_PRIVACY) + +/* We do not control the following flags. */ +#define UNMANAGED_FLAGS \ + (IFA_F_SECONDARY | \ + IFA_F_DADFAILED | \ + IFA_F_DEPRECATED | \ + IFA_F_TENTATIVE | \ + IFA_F_PERMANENT | \ + IFA_F_STABLE_PRIVACY) + +int address_flags_to_string_alloc(uint32_t flags, int family, char **ret) { + _cleanup_free_ char *str = NULL; + static const char* map[] = { + [LOG2U(IFA_F_SECONDARY)] = "secondary", /* This is also called "temporary" for ipv6. */ + [LOG2U(IFA_F_NODAD)] = "nodad", + [LOG2U(IFA_F_OPTIMISTIC)] = "optimistic", + [LOG2U(IFA_F_DADFAILED)] = "dadfailed", + [LOG2U(IFA_F_HOMEADDRESS)] = "home-address", + [LOG2U(IFA_F_DEPRECATED)] = "deprecated", + [LOG2U(IFA_F_TENTATIVE)] = "tentative", + [LOG2U(IFA_F_PERMANENT)] = "permanent", + [LOG2U(IFA_F_MANAGETEMPADDR)] = "manage-temporary-address", + [LOG2U(IFA_F_NOPREFIXROUTE)] = "no-prefixroute", + [LOG2U(IFA_F_MCAUTOJOIN)] = "auto-join", + [LOG2U(IFA_F_STABLE_PRIVACY)] = "stable-privacy", + }; + + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(ret); + + for (size_t i = 0; i < ELEMENTSOF(map); i++) + if (FLAGS_SET(flags, 1 << i) && map[i]) + if (!strextend_with_separator( + &str, ",", + family == AF_INET6 && (1 << i) == IFA_F_SECONDARY ? "temporary" : map[i])) + return -ENOMEM; + + *ret = TAKE_PTR(str); + return 0; +} + +static LinkAddressState address_state_from_scope(uint8_t scope) { + if (scope < RT_SCOPE_SITE) + /* universally accessible addresses found */ + return LINK_ADDRESS_STATE_ROUTABLE; + + if (scope < RT_SCOPE_HOST) + /* only link or site local addresses found */ + return LINK_ADDRESS_STATE_DEGRADED; + + /* no useful addresses found */ + return LINK_ADDRESS_STATE_OFF; +} + +void link_get_address_states( + Link *link, + LinkAddressState *ret_ipv4, + LinkAddressState *ret_ipv6, + LinkAddressState *ret_all) { + + uint8_t ipv4_scope = RT_SCOPE_NOWHERE, ipv6_scope = RT_SCOPE_NOWHERE; + Address *address; + + assert(link); + + SET_FOREACH(address, link->addresses) { + if (!address_is_ready(address)) + continue; + + if (address->family == AF_INET) + ipv4_scope = MIN(ipv4_scope, address->scope); + + if (address->family == AF_INET6) + ipv6_scope = MIN(ipv6_scope, address->scope); + } + + if (ret_ipv4) + *ret_ipv4 = address_state_from_scope(ipv4_scope); + if (ret_ipv6) + *ret_ipv6 = address_state_from_scope(ipv6_scope); + if (ret_all) + *ret_all = address_state_from_scope(MIN(ipv4_scope, ipv6_scope)); +} + +int address_new(Address **ret) { + _cleanup_(address_freep) Address *address = NULL; + + address = new(Address, 1); + if (!address) + return -ENOMEM; + + *address = (Address) { + .family = AF_UNSPEC, + .scope = RT_SCOPE_UNIVERSE, + .lifetime_valid_usec = USEC_INFINITY, + .lifetime_preferred_usec = USEC_INFINITY, + .set_broadcast = -1, + }; + + *ret = TAKE_PTR(address); + + return 0; +} + +int address_new_static(Network *network, const char *filename, unsigned section_line, Address **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(address_freep) Address *address = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + address = ordered_hashmap_get(network->addresses_by_section, n); + if (address) { + *ret = TAKE_PTR(address); + return 0; + } + + if (ordered_hashmap_size(network->addresses_by_section) >= STATIC_ADDRESSES_PER_NETWORK_MAX) + return -E2BIG; + + r = address_new(&address); + if (r < 0) + return r; + + address->network = network; + address->section = TAKE_PTR(n); + address->source = NETWORK_CONFIG_SOURCE_STATIC; + /* This will be adjusted in address_section_verify(). */ + address->duplicate_address_detection = _ADDRESS_FAMILY_INVALID; + + r = ordered_hashmap_ensure_put(&network->addresses_by_section, &config_section_hash_ops, address->section, address); + if (r < 0) + return r; + + *ret = TAKE_PTR(address); + return 0; +} + +Address *address_free(Address *address) { + if (!address) + return NULL; + + if (address->network) { + assert(address->section); + ordered_hashmap_remove(address->network->addresses_by_section, address->section); + } + + if (address->link) { + set_remove(address->link->addresses, address); + + if (address->family == AF_INET6 && + in6_addr_equal(&address->in_addr.in6, &address->link->ipv6ll_address)) + memzero(&address->link->ipv6ll_address, sizeof(struct in6_addr)); + + ipv4acd_detach(address->link, address); + } + + config_section_free(address->section); + free(address->label); + free(address->netlabel); + nft_set_context_clear(&address->nft_set_context); + return mfree(address); +} + +static bool address_lifetime_is_valid(const Address *a) { + assert(a); + + return + a->lifetime_valid_usec == USEC_INFINITY || + a->lifetime_valid_usec > now(CLOCK_BOOTTIME); +} + +bool address_is_ready(const Address *a) { + assert(a); + assert(a->link); + + if (!ipv4acd_bound(a->link, a)) + return false; + + if (FLAGS_SET(a->flags, IFA_F_TENTATIVE)) + return false; + + if (FLAGS_SET(a->state, NETWORK_CONFIG_STATE_REMOVING)) + return false; + + if (!FLAGS_SET(a->state, NETWORK_CONFIG_STATE_CONFIGURED)) + return false; + + return address_lifetime_is_valid(a); +} + +bool link_check_addresses_ready(Link *link, NetworkConfigSource source) { + Address *a; + bool has = false; + + assert(link); + + /* Check if all addresses on the interface are ready. If there is no address, this will return false. */ + + SET_FOREACH(a, link->addresses) { + if (source >= 0 && a->source != source) + continue; + if (address_is_marked(a)) + continue; + if (!address_exists(a)) + continue; + if (!address_is_ready(a)) + return false; + has = true; + } + + return has; +} + +void link_mark_addresses(Link *link, NetworkConfigSource source) { + Address *a; + + assert(link); + + SET_FOREACH(a, link->addresses) { + if (a->source != source) + continue; + + address_mark(a); + } +} + +static int address_get_broadcast(const Address *a, Link *link, struct in_addr *ret) { + struct in_addr b_addr = {}; + + assert(a); + assert(link); + + /* Returns 0 when broadcast address is null, 1 when non-null broadcast address, -EAGAIN when the main + * address is null. */ + + /* broadcast is only for IPv4. */ + if (a->family != AF_INET) + goto finalize; + + /* broadcast address cannot be used when peer address is specified. */ + if (in4_addr_is_set(&a->in_addr_peer.in)) + goto finalize; + + /* A /31 or /32 IPv4 address does not have a broadcast address. + * See https://tools.ietf.org/html/rfc3021 */ + if (a->prefixlen > 30) + goto finalize; + + /* If explicitly configured, use the address as is. */ + if (in4_addr_is_set(&a->broadcast)) { + b_addr = a->broadcast; + goto finalize; + } + + /* If explicitly disabled, then return null address. */ + if (a->set_broadcast == 0) + goto finalize; + + /* For wireguard interfaces, broadcast is disabled by default. */ + if (a->set_broadcast < 0 && streq_ptr(link->kind, "wireguard")) + goto finalize; + + /* If the main address is null, e.g. Address=0.0.0.0/24, the broadcast address will be automatically + * determined after an address is acquired. */ + if (!in4_addr_is_set(&a->in_addr.in)) + return -EAGAIN; + + /* Otherwise, generate a broadcast address from the main address and prefix length. */ + b_addr.s_addr = a->in_addr.in.s_addr | htobe32(UINT32_C(0xffffffff) >> a->prefixlen); + +finalize: + if (ret) + *ret = b_addr; + + return in4_addr_is_set(&b_addr); +} + +static void address_set_broadcast(Address *a, Link *link) { + assert(a); + assert_se(address_get_broadcast(a, link, &a->broadcast) >= 0); +} + +static void address_set_cinfo(Manager *m, const Address *a, struct ifa_cacheinfo *cinfo) { + usec_t now_usec; + + assert(m); + assert(a); + assert(cinfo); + + assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &now_usec) >= 0); + + *cinfo = (struct ifa_cacheinfo) { + .ifa_valid = usec_to_sec(a->lifetime_valid_usec, now_usec), + .ifa_prefered = usec_to_sec(a->lifetime_preferred_usec, now_usec), + }; +} + +static void address_set_lifetime(Manager *m, Address *a, const struct ifa_cacheinfo *cinfo) { + usec_t now_usec; + + assert(m); + assert(a); + assert(cinfo); + + assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &now_usec) >= 0); + + a->lifetime_valid_usec = sec_to_usec(cinfo->ifa_valid, now_usec); + a->lifetime_preferred_usec = sec_to_usec(cinfo->ifa_prefered, now_usec); +} + +static bool address_is_static_null(const Address *address) { + assert(address); + + if (!address->network) + return false; + + if (!address->requested_as_null) + return false; + + assert(!in_addr_is_set(address->family, &address->in_addr)); + return true; +} + +static int address_ipv4_prefix(const Address *a, struct in_addr *ret) { + struct in_addr p; + int r; + + assert(a); + assert(a->family == AF_INET); + assert(ret); + + p = in4_addr_is_set(&a->in_addr_peer.in) ? a->in_addr_peer.in : a->in_addr.in; + r = in4_addr_mask(&p, a->prefixlen); + if (r < 0) + return r; + + *ret = p; + return 0; +} + +static 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: { + struct in_addr prefix; + + siphash24_compress(&a->prefixlen, sizeof(a->prefixlen), state); + + assert_se(address_ipv4_prefix(a, &prefix) >= 0); + siphash24_compress(&prefix, sizeof(prefix), state); + + siphash24_compress(&a->in_addr.in, sizeof(a->in_addr.in), state); + break; + } + case AF_INET6: + siphash24_compress(&a->in_addr.in6, sizeof(a->in_addr.in6), state); + + if (in6_addr_is_null(&a->in_addr.in6)) + siphash24_compress(&a->prefixlen, sizeof(a->prefixlen), state); + break; + + default: + /* treat any other address family as AF_UNSPEC */ + break; + } +} + +static 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: { + struct in_addr p1, p2; + + /* See kernel's find_matching_ifa() in net/ipv4/devinet.c */ + r = CMP(a1->prefixlen, a2->prefixlen); + if (r != 0) + return r; + + assert_se(address_ipv4_prefix(a1, &p1) >= 0); + assert_se(address_ipv4_prefix(a2, &p2) >= 0); + r = memcmp(&p1, &p2, sizeof(p1)); + if (r != 0) + return r; + + return memcmp(&a1->in_addr.in, &a2->in_addr.in, sizeof(a1->in_addr.in)); + } + case AF_INET6: + /* See kernel's ipv6_get_ifaddr() in net/ipv6/addrconf.c */ + r = memcmp(&a1->in_addr.in6, &a2->in_addr.in6, sizeof(a1->in_addr.in6)); + if (r != 0) + return r; + + /* To distinguish IPv6 null addresses with different prefixlen, e.g. ::48 vs ::64, let's + * compare the prefix length. */ + if (in6_addr_is_null(&a1->in_addr.in6)) + r = CMP(a1->prefixlen, a2->prefixlen); + + return r; + + default: + /* treat any other address family as AF_UNSPEC */ + return 0; + } +} + +DEFINE_HASH_OPS( + address_hash_ops, + Address, + address_hash_func, + address_compare_func); + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + address_hash_ops_free, + Address, + address_hash_func, + address_compare_func, + address_free); + +static bool address_can_update(const Address *la, const Address *na) { + assert(la); + assert(la->link); + assert(na); + assert(na->network); + + /* + * property | IPv4 | IPv6 + * ----------------------------------------- + * family | ✗ | ✗ + * prefixlen | ✗ | ✗ + * address | ✗ | ✗ + * scope | ✗ | - + * label | ✗ | - + * broadcast | ✗ | - + * peer | ✗ | ✓ + * flags | ✗ | ✓ + * lifetime | ✓ | ✓ + * route metric | ✓ | ✓ + * protocol | ✓ | ✓ + * + * ✗ : cannot be changed + * ✓ : can be changed + * - : unused + * + * IPv4 : See inet_rtm_newaddr() in net/ipv4/devinet.c. + * IPv6 : See inet6_addr_modify() in net/ipv6/addrconf.c. + */ + + if (la->family != na->family) + return false; + + if (la->prefixlen != na->prefixlen) + return false; + + /* When a null address is requested, the address to be assigned/updated will be determined later. */ + if (!address_is_static_null(na) && + in_addr_equal(la->family, &la->in_addr, &na->in_addr) <= 0) + return false; + + switch (la->family) { + case AF_INET: { + struct in_addr bcast; + + if (la->scope != na->scope) + return false; + if (((la->flags ^ na->flags) & KNOWN_FLAGS & ~IPV6ONLY_FLAGS & ~UNMANAGED_FLAGS) != 0) + return false; + if (!streq_ptr(la->label, na->label)) + return false; + if (!in4_addr_equal(&la->in_addr_peer.in, &na->in_addr_peer.in)) + return false; + if (address_get_broadcast(na, la->link, &bcast) >= 0) { + /* If the broadcast address can be determined now, check if they match. */ + if (!in4_addr_equal(&la->broadcast, &bcast)) + return false; + } else { + /* When a null address is requested, then the broadcast address will be + * automatically calculated from the acquired address, e.g. + * 192.168.0.10/24 -> 192.168.0.255 + * So, here let's only check if the broadcast is the last address in the range, e.g. + * 0.0.0.0/24 -> 0.0.0.255 */ + if (!FLAGS_SET(la->broadcast.s_addr, htobe32(UINT32_C(0xffffffff) >> la->prefixlen))) + return false; + } + break; + } + case AF_INET6: + break; + + default: + assert_not_reached(); + } + + return true; +} + +int address_dup(const Address *src, Address **ret) { + _cleanup_(address_freep) Address *dest = NULL; + int r; + + assert(src); + assert(ret); + + dest = newdup(Address, src, 1); + if (!dest) + return -ENOMEM; + + /* clear all pointers */ + dest->network = NULL; + dest->section = NULL; + dest->link = NULL; + dest->label = NULL; + dest->netlabel = NULL; + dest->nft_set_context.sets = NULL; + dest->nft_set_context.n_sets = 0; + + if (src->family == AF_INET) { + r = free_and_strdup(&dest->label, src->label); + if (r < 0) + return r; + } + + r = free_and_strdup(&dest->netlabel, src->netlabel); + if (r < 0) + return r; + + r = nft_set_context_dup(&src->nft_set_context, &dest->nft_set_context); + if (r < 0) + return r; + + *ret = TAKE_PTR(dest); + return 0; +} + +static int address_set_masquerade(Address *address, bool add) { + union in_addr_union masked; + int r; + + assert(address); + assert(address->link); + + if (!address->link->network) + return 0; + + if (address->family == AF_INET && + !FLAGS_SET(address->link->network->ip_masquerade, ADDRESS_FAMILY_IPV4)) + return 0; + + if (address->family == AF_INET6 && + !FLAGS_SET(address->link->network->ip_masquerade, ADDRESS_FAMILY_IPV6)) + return 0; + + if (address->scope >= RT_SCOPE_LINK) + return 0; + + if (address->ip_masquerade_done == add) + return 0; + + masked = address->in_addr; + r = in_addr_mask(address->family, &masked, address->prefixlen); + if (r < 0) + return r; + + r = fw_add_masquerade(&address->link->manager->fw_ctx, add, address->family, &masked, address->prefixlen); + if (r < 0) + return r; + + address->ip_masquerade_done = add; + + return 0; +} + +static void address_modify_nft_set_context(Address *address, bool add, NFTSetContext *nft_set_context) { + int r; + + assert(address); + assert(address->link); + assert(address->link->manager); + assert(nft_set_context); + + if (!address->link->manager->fw_ctx) { + r = fw_ctx_new_full(&address->link->manager->fw_ctx, /* init_tables= */ false); + if (r < 0) + return; + } + + FOREACH_ARRAY(nft_set, nft_set_context->sets, nft_set_context->n_sets) { + uint32_t ifindex; + + assert(nft_set); + + switch (nft_set->source) { + case NFT_SET_SOURCE_ADDRESS: + r = nft_set_element_modify_ip(address->link->manager->fw_ctx, add, nft_set->nfproto, address->family, nft_set->table, nft_set->set, + &address->in_addr); + break; + case NFT_SET_SOURCE_PREFIX: + r = nft_set_element_modify_iprange(address->link->manager->fw_ctx, add, nft_set->nfproto, address->family, nft_set->table, nft_set->set, + &address->in_addr, address->prefixlen); + break; + case NFT_SET_SOURCE_IFINDEX: + ifindex = address->link->ifindex; + r = nft_set_element_modify_any(address->link->manager->fw_ctx, add, nft_set->nfproto, nft_set->table, nft_set->set, + &ifindex, sizeof(ifindex)); + break; + default: + assert_not_reached(); + } + + if (r < 0) + log_warning_errno(r, "Failed to %s NFT set: family %s, table %s, set %s, IP address %s, ignoring", + add? "add" : "delete", + nfproto_to_string(nft_set->nfproto), nft_set->table, nft_set->set, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); + else + log_debug("%s NFT set: family %s, table %s, set %s, IP address %s", + add ? "Added" : "Deleted", + nfproto_to_string(nft_set->nfproto), nft_set->table, nft_set->set, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); + } +} + +static void address_modify_nft_set(Address *address, bool add) { + assert(address); + assert(address->link); + + if (!IN_SET(address->family, AF_INET, AF_INET6)) + return; + + if (!address->link->network) + return; + + switch (address->source) { + case NETWORK_CONFIG_SOURCE_DHCP4: + return address_modify_nft_set_context(address, add, &address->link->network->dhcp_nft_set_context); + case NETWORK_CONFIG_SOURCE_DHCP6: + return address_modify_nft_set_context(address, add, &address->link->network->dhcp6_nft_set_context); + case NETWORK_CONFIG_SOURCE_DHCP_PD: + return address_modify_nft_set_context(address, add, &address->link->network->dhcp_pd_nft_set_context); + case NETWORK_CONFIG_SOURCE_NDISC: + return address_modify_nft_set_context(address, add, &address->link->network->ndisc_nft_set_context); + case NETWORK_CONFIG_SOURCE_STATIC: + return address_modify_nft_set_context(address, add, &address->nft_set_context); + default: + return; + } +} + +static int address_add(Link *link, Address *address) { + int r; + + assert(link); + assert(address); + + r = set_ensure_put(&link->addresses, &address_hash_ops_free, address); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + address->link = link; + return 0; +} + +static int address_update(Address *address) { + Link *link = ASSERT_PTR(ASSERT_PTR(address)->link); + int r; + + if (address_is_ready(address) && + address->family == AF_INET6 && + in6_addr_is_link_local(&address->in_addr.in6) && + in6_addr_is_null(&link->ipv6ll_address)) { + + link->ipv6ll_address = address->in_addr.in6; + + r = link_ipv6ll_gained(link); + if (r < 0) + return r; + } + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + r = address_set_masquerade(address, /* add = */ true); + if (r < 0) + return log_link_warning_errno(link, r, "Could not enable IP masquerading: %m"); + + address_add_netlabel(address); + + address_modify_nft_set(address, /* add = */ true); + + if (address_is_ready(address) && address->callback) { + r = address->callback(address); + if (r < 0) + return r; + } + + link_update_operstate(link, /* also_update_master = */ true); + link_check_ready(link); + return 0; +} + +static int address_drop(Address *address) { + Link *link = ASSERT_PTR(ASSERT_PTR(address)->link); + int r; + + r = address_set_masquerade(address, /* add = */ false); + if (r < 0) + log_link_warning_errno(link, r, "Failed to disable IP masquerading, ignoring: %m"); + + address_modify_nft_set(address, /* add = */ false); + + address_del_netlabel(address); + + address_free(address); + + link_update_operstate(link, /* also_update_master = */ true); + link_check_ready(link); + return 0; +} + +static bool address_match_null(const Address *a, const Address *null_address) { + assert(a); + assert(null_address); + + if (!a->requested_as_null) + return false; + + /* Currently, null address is supported only by static addresses. Note that static + * address may be set as foreign during reconfiguring the interface. */ + if (!IN_SET(a->source, NETWORK_CONFIG_SOURCE_FOREIGN, NETWORK_CONFIG_SOURCE_STATIC)) + return false; + + if (a->family != null_address->family) + return false; + + if (a->prefixlen != null_address->prefixlen) + return false; + + return true; +} + +static int address_get_request(Link *link, const Address *address, Request **ret) { + Request *req; + + assert(link); + assert(link->manager); + assert(address); + + req = ordered_set_get( + link->manager->request_queue, + &(Request) { + .link = link, + .type = REQUEST_TYPE_ADDRESS, + .userdata = (void*) address, + .hash_func = (hash_func_t) address_hash_func, + .compare_func = (compare_func_t) address_compare_func, + }); + if (req) { + if (ret) + *ret = req; + return 0; + } + + if (address_is_static_null(address)) + ORDERED_SET_FOREACH(req, link->manager->request_queue) { + if (req->link != link) + continue; + if (req->type != REQUEST_TYPE_ADDRESS) + continue; + + if (!address_match_null(req->userdata, address)) + continue; + + if (ret) + *ret = req; + + return 0; + } + + return -ENOENT; +} + +int address_get(Link *link, const Address *in, Address **ret) { + Address *a; + + assert(link); + assert(in); + + a = set_get(link->addresses, in); + if (a) { + if (ret) + *ret = a; + return 0; + } + + /* Find matching address that originally requested as null address. */ + if (address_is_static_null(in)) + SET_FOREACH(a, link->addresses) { + if (!address_match_null(a, in)) + continue; + + if (ret) + *ret = a; + return 0; + } + + return -ENOENT; +} + +int address_get_harder(Link *link, const Address *in, Address **ret) { + Request *req; + int r; + + assert(link); + assert(in); + + if (address_get(link, in, ret) >= 0) + return 0; + + r = address_get_request(link, in, &req); + if (r < 0) + return r; + + if (ret) + *ret = ASSERT_PTR(req->userdata); + + return 0; +} + +int link_get_address(Link *link, int family, const union in_addr_union *address, unsigned char prefixlen, Address **ret) { + Address *a; + int r; + + assert(link); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(address); + + /* This find an Address object on the link which matches the given address and prefix length + * and does not have peer address. When the prefixlen is zero, then an Address object with an + * arbitrary prefixlen will be returned. */ + + if (family == AF_INET6 || prefixlen != 0) { + _cleanup_(address_freep) Address *tmp = NULL; + + /* In this case, we can use address_get(). */ + + r = address_new(&tmp); + if (r < 0) + return r; + + tmp->family = family; + tmp->in_addr = *address; + tmp->prefixlen = prefixlen; + + r = address_get(link, tmp, &a); + if (r < 0) + return r; + + if (family == AF_INET6) { + /* IPv6 addresses are managed without peer address and prefix length. Hence, we need + * to check them explicitly. */ + if (in_addr_is_set(family, &a->in_addr_peer)) + return -ENOENT; + if (prefixlen != 0 && a->prefixlen != prefixlen) + return -ENOENT; + } + + if (ret) + *ret = a; + + return 0; + } + + SET_FOREACH(a, link->addresses) { + if (a->family != family) + continue; + + if (!in_addr_equal(family, &a->in_addr, address)) + continue; + + if (in_addr_is_set(family, &a->in_addr_peer)) + continue; + + if (ret) + *ret = a; + + return 0; + } + + return -ENOENT; +} + +int manager_get_address(Manager *manager, int family, const union in_addr_union *address, unsigned char prefixlen, Address **ret) { + Link *link; + + assert(manager); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(address); + + HASHMAP_FOREACH(link, manager->links_by_index) { + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + continue; + + if (link_get_address(link, family, address, prefixlen, ret) >= 0) + return 0; + } + + return -ENOENT; +} + +bool manager_has_address(Manager *manager, int family, const union in_addr_union *address, bool check_ready) { + Address *a; + + assert(manager); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(address); + + if (manager_get_address(manager, family, address, 0, &a) < 0) + return false; + + return check_ready ? address_is_ready(a) : (address_exists(a) && address_lifetime_is_valid(a)); +} + +const char* format_lifetime(char *buf, size_t l, usec_t lifetime_usec) { + assert(buf); + assert(l > 4); + + if (lifetime_usec == USEC_INFINITY) + return "forever"; + + sprintf(buf, "for "); + /* format_timespan() never fails */ + assert_se(format_timespan(buf + 4, l - 4, usec_sub_unsigned(lifetime_usec, now(CLOCK_BOOTTIME)), USEC_PER_SEC)); + return buf; +} + +static void log_address_debug(const Address *address, const char *str, const Link *link) { + _cleanup_free_ char *state = NULL, *flags_str = NULL, *scope_str = NULL; + + assert(address); + assert(str); + assert(link); + + if (!DEBUG_LOGGING) + return; + + (void) network_config_state_to_string_alloc(address->state, &state); + + const char *peer = in_addr_is_set(address->family, &address->in_addr_peer) ? + IN_ADDR_TO_STRING(address->family, &address->in_addr_peer) : NULL; + + const char *broadcast = (address->family == AF_INET && in4_addr_is_set(&address->broadcast)) ? + IN4_ADDR_TO_STRING(&address->broadcast) : NULL; + + (void) address_flags_to_string_alloc(address->flags, address->family, &flags_str); + (void) route_scope_to_string_alloc(address->scope, &scope_str); + + log_link_debug(link, "%s %s address (%s): %s%s%s/%u%s%s (valid %s, preferred %s), flags: %s, scope: %s%s%s", + str, strna(network_config_source_to_string(address->source)), strna(state), + IN_ADDR_TO_STRING(address->family, &address->in_addr), + peer ? " peer " : "", strempty(peer), address->prefixlen, + broadcast ? " broadcast " : "", strempty(broadcast), + FORMAT_LIFETIME(address->lifetime_valid_usec), + FORMAT_LIFETIME(address->lifetime_preferred_usec), + strna(flags_str), strna(scope_str), + address->family == AF_INET ? ", label: " : "", + address->family == AF_INET ? strna(address->label) : ""); +} + +static int address_set_netlink_message(const Address *address, sd_netlink_message *m, Link *link) { + uint32_t flags; + int r; + + assert(address); + assert(m); + assert(link); + + r = sd_rtnl_message_addr_set_prefixlen(m, address->prefixlen); + if (r < 0) + return r; + + /* On remove, only IFA_F_MANAGETEMPADDR flag for IPv6 addresses are used. But anyway, set all + * flags except tentative flag here unconditionally. Without setting the flag, the template + * addresses generated by kernel will not be removed automatically when the main address is + * removed. */ + flags = address->flags & ~IFA_F_TENTATIVE; + r = sd_rtnl_message_addr_set_flags(m, flags & 0xff); + if (r < 0) + return r; + + if ((flags & ~0xff) != 0) { + r = sd_netlink_message_append_u32(m, IFA_FLAGS, flags); + if (r < 0) + return r; + } + + r = netlink_message_append_in_addr_union(m, IFA_LOCAL, address->family, &address->in_addr); + if (r < 0) + return r; + + return 0; +} + +static int address_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert(m); + assert(link); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EADDRNOTAVAIL) + log_link_message_warning_errno(link, m, r, "Could not drop address"); + + return 1; +} + +int address_remove(Address *address) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + Request *req; + Link *link; + int r; + + assert(address); + assert(IN_SET(address->family, AF_INET, AF_INET6)); + assert(address->link); + assert(address->link->ifindex > 0); + assert(address->link->manager); + assert(address->link->manager->rtnl); + + link = address->link; + + log_address_debug(address, "Removing", link); + + r = sd_rtnl_message_new_addr(link->manager->rtnl, &m, RTM_DELADDR, + link->ifindex, address->family); + if (r < 0) + return log_link_warning_errno(link, r, "Could not allocate RTM_DELADDR message: %m"); + + r = address_set_netlink_message(address, m, link); + if (r < 0) + return log_link_warning_errno(link, r, "Could not set netlink attributes: %m"); + + r = netlink_call_async(link->manager->rtnl, NULL, m, + address_remove_handler, + link_netlink_destroy_callback, link); + if (r < 0) + return log_link_warning_errno(link, r, "Could not send rtnetlink message: %m"); + + link_ref(link); + + address_enter_removing(address); + if (address_get_request(link, address, &req) >= 0) + address_enter_removing(req->userdata); + + /* The operational state is determined by address state and carrier state. Hence, if we remove + * an address, the operational state may be changed. */ + link_update_operstate(link, true); + return 0; +} + +int address_remove_and_drop(Address *address) { + if (!address) + return 0; + + address_cancel_request(address); + + if (address_exists(address)) + return address_remove(address); + + return address_drop(address); +} + +bool link_address_is_dynamic(const Link *link, const Address *address) { + Route *route; + + assert(link); + assert(address); + + if (address->lifetime_preferred_usec != USEC_INFINITY) + return true; + + /* Even when the address is leased from a DHCP server, networkd assign the address + * without lifetime when KeepConfiguration=dhcp. So, let's check that we have + * corresponding routes with RTPROT_DHCP. */ + SET_FOREACH(route, link->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + /* The route is not assigned yet, or already removed. Ignoring. */ + if (!route_exists(route)) + continue; + + if (route->protocol != RTPROT_DHCP) + continue; + + if (address->family != route->family) + continue; + + if (in_addr_equal(address->family, &address->in_addr, &route->prefsrc)) + return true; + } + + return false; +} + +int link_drop_ipv6ll_addresses(Link *link) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + int r; + + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + + /* IPv6LL address may be in the tentative state, and in that case networkd has not received it. + * So, we need to dump all IPv6 addresses. */ + + if (link_may_have_ipv6ll(link, /* check_multicast = */ false)) + return 0; + + r = sd_rtnl_message_new_addr(link->manager->rtnl, &req, RTM_GETADDR, link->ifindex, AF_INET6); + if (r < 0) + return r; + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return r; + + r = sd_netlink_call(link->manager->rtnl, req, 0, &reply); + if (r < 0) + return r; + + for (sd_netlink_message *addr = reply; addr; addr = sd_netlink_message_next(addr)) { + _cleanup_(address_freep) Address *a = NULL; + unsigned char flags, prefixlen; + struct in6_addr address; + Address *existing; + int ifindex; + + /* NETLINK_GET_STRICT_CHK socket option is supported since kernel 4.20. To support + * older kernels, we need to check ifindex here. */ + r = sd_rtnl_message_addr_get_ifindex(addr, &ifindex); + if (r < 0) { + log_link_debug_errno(link, r, "rtnl: received address message without valid ifindex, ignoring: %m"); + continue; + } else if (link->ifindex != ifindex) + continue; + + r = sd_rtnl_message_addr_get_flags(addr, &flags); + if (r < 0) { + log_link_debug_errno(link, r, "rtnl: received address message without valid flags, ignoring: %m"); + continue; + } + + r = sd_rtnl_message_addr_get_prefixlen(addr, &prefixlen); + if (r < 0) { + log_link_debug_errno(link, r, "rtnl: received address message without prefixlen, ignoring: %m"); + continue; + } + + if (sd_netlink_message_read_in6_addr(addr, IFA_LOCAL, NULL) >= 0) + /* address with peer, ignoring. */ + continue; + + r = sd_netlink_message_read_in6_addr(addr, IFA_ADDRESS, &address); + if (r < 0) { + log_link_debug_errno(link, r, "rtnl: received address message without valid address, ignoring: %m"); + continue; + } + + if (!in6_addr_is_link_local(&address)) + continue; + + r = address_new(&a); + if (r < 0) + return -ENOMEM; + + a->family = AF_INET6; + a->in_addr.in6 = address; + a->prefixlen = prefixlen; + a->flags = flags; + + if (address_get(link, a, &existing) < 0) { + r = address_add(link, a); + if (r < 0) + return r; + + existing = TAKE_PTR(a); + } + + r = address_remove(existing); + if (r < 0) + return r; + } + + return 0; +} + +int link_drop_foreign_addresses(Link *link) { + Address *address; + int r = 0; + + assert(link); + assert(link->network); + + /* First, mark all addresses. */ + SET_FOREACH(address, link->addresses) { + /* We consider IPv6LL addresses to be managed by the kernel, or dropped in link_drop_ipv6ll_addresses() */ + if (address->family == AF_INET6 && in6_addr_is_link_local(&address->in_addr.in6)) + continue; + + /* Do not remove localhost address (127.0.0.1 and ::1) */ + if (link->flags & IFF_LOOPBACK && in_addr_is_localhost_one(address->family, &address->in_addr) > 0) + continue; + + /* Ignore addresses we configured. */ + if (address->source != NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + /* Ignore addresses not assigned yet or already removing. */ + if (!address_exists(address)) + continue; + + /* link_address_is_dynamic() is slightly heavy. Let's call the function only when KeepConfiguration= is set. */ + if (IN_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP, KEEP_CONFIGURATION_STATIC) && + link_address_is_dynamic(link, address) == (link->network->keep_configuration == KEEP_CONFIGURATION_DHCP)) + continue; + + address_mark(address); + } + + /* Then, unmark requested addresses. */ + ORDERED_HASHMAP_FOREACH(address, link->network->addresses_by_section) { + Address *existing; + + if (address_get(link, address, &existing) < 0) + continue; + + if (!address_can_update(existing, address)) + continue; + + /* Found matching static configuration. Keep the existing address. */ + address_unmark(existing); + } + + /* Finally, remove all marked addresses. */ + SET_FOREACH(address, link->addresses) { + if (!address_is_marked(address)) + continue; + + RET_GATHER(r, address_remove(address)); + } + + return r; +} + +int link_drop_managed_addresses(Link *link) { + Address *address; + int r = 0; + + assert(link); + + SET_FOREACH(address, link->addresses) { + /* Do not touch addresses managed by kernel or other tools. */ + if (address->source == NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + /* Ignore addresses not assigned yet or already removing. */ + if (!address_exists(address)) + continue; + + RET_GATHER(r, address_remove(address)); + } + + return r; +} + +void link_foreignize_addresses(Link *link) { + Address *address; + + assert(link); + + SET_FOREACH(address, link->addresses) + address->source = NETWORK_CONFIG_SOURCE_FOREIGN; +} + +static int address_acquire(Link *link, const Address *original, Address **ret) { + _cleanup_(address_freep) Address *na = NULL; + union in_addr_union in_addr; + int r; + + assert(link); + assert(original); + assert(ret); + + /* Something useful was configured? just use it */ + if (in_addr_is_set(original->family, &original->in_addr)) + return address_dup(original, ret); + + /* The address is configured to be 0.0.0.0 or [::] by the user? + * Then let's acquire something more useful from the pool. */ + r = address_pool_acquire(link->manager, original->family, original->prefixlen, &in_addr); + if (r < 0) + return r; + if (r == 0) + return -EBUSY; + + /* Pick first address in range for ourselves. */ + if (original->family == AF_INET) + in_addr.in.s_addr = in_addr.in.s_addr | htobe32(1); + else if (original->family == AF_INET6) + in_addr.in6.s6_addr[15] |= 1; + + r = address_dup(original, &na); + if (r < 0) + return r; + + na->in_addr = in_addr; + + *ret = TAKE_PTR(na); + return 0; +} + +int address_configure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg) { + int r; + + assert(rtnl); + assert(m); + assert(link); + assert(error_msg); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, error_msg); + link_enter_failed(link); + return 0; + } + + return 1; +} + +static int address_configure(const Address *address, const struct ifa_cacheinfo *c, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(address); + assert(IN_SET(address->family, AF_INET, AF_INET6)); + assert(c); + assert(link); + assert(link->ifindex > 0); + assert(link->manager); + assert(link->manager->rtnl); + assert(req); + + log_address_debug(address, "Configuring", link); + + r = sd_rtnl_message_new_addr_update(link->manager->rtnl, &m, link->ifindex, address->family); + if (r < 0) + return r; + + r = address_set_netlink_message(address, m, link); + if (r < 0) + return r; + + r = sd_rtnl_message_addr_set_scope(m, address->scope); + if (r < 0) + return r; + + if (address->family == AF_INET6 || in_addr_is_set(address->family, &address->in_addr_peer)) { + r = netlink_message_append_in_addr_union(m, IFA_ADDRESS, address->family, &address->in_addr_peer); + if (r < 0) + return r; + } else if (in4_addr_is_set(&address->broadcast)) { + r = sd_netlink_message_append_in_addr(m, IFA_BROADCAST, &address->broadcast); + if (r < 0) + return r; + } + + if (address->family == AF_INET && address->label) { + r = sd_netlink_message_append_string(m, IFA_LABEL, address->label); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_cache_info(m, IFA_CACHEINFO, c); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, IFA_RT_PRIORITY, address->route_metric); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static bool address_is_ready_to_configure(Link *link, const Address *address) { + assert(link); + assert(address); + + if (!link_is_ready_to_configure(link, false)) + return false; + + if (!ipv4acd_bound(link, address)) + return false; + + /* Refuse adding more than the limit */ + if (set_size(link->addresses) >= ADDRESSES_PER_LINK_MAX) + return false; + + return true; +} + +static int address_process_request(Request *req, Link *link, Address *address) { + struct Address *existing; + struct ifa_cacheinfo c; + int r; + + assert(req); + assert(link); + assert(address); + + if (!address_is_ready_to_configure(link, address)) + return 0; + + address_set_cinfo(link->manager, address, &c); + if (c.ifa_valid == 0) { + log_link_debug(link, "Refuse to configure %s address %s, as its valid lifetime is zero.", + network_config_source_to_string(address->source), + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); + + address_cancel_requesting(address); + if (address_get(link, address, &existing) >= 0) + address_cancel_requesting(existing); + return 1; + } + + r = address_configure(address, &c, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure address: %m"); + + address_enter_configuring(address); + if (address_get(link, address, &existing) >= 0) + address_enter_configuring(existing); + + return 1; +} + +int link_request_address( + Link *link, + const Address *address, + unsigned *message_counter, + address_netlink_handler_t netlink_handler, + Request **ret) { + + _cleanup_(address_freep) Address *tmp = NULL; + Address *existing = NULL; + int r; + + assert(link); + assert(address); + assert(address->source != NETWORK_CONFIG_SOURCE_FOREIGN); + + if (address->lifetime_valid_usec == 0) + /* The requested address is outdated. Let's ignore the request. */ + return 0; + + if (address_get(link, address, &existing) < 0) { + if (address_get_request(link, address, NULL) >= 0) + return 0; /* already requested, skipping. */ + + r = address_acquire(link, address, &tmp); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to acquire an address from pool: %m"); + + /* Consider address tentative until we get the real flags from the kernel */ + tmp->flags |= IFA_F_TENTATIVE; + + } else { + r = address_dup(address, &tmp); + if (r < 0) + return log_oom(); + + /* Copy already assigned address when it is requested as a null address. */ + if (address_is_static_null(address)) + tmp->in_addr = existing->in_addr; + + /* Copy state for logging below. */ + tmp->state = existing->state; + } + + address_set_broadcast(tmp, link); + + r = ipv4acd_configure(link, tmp); + if (r < 0) + return r; + + log_address_debug(tmp, "Requesting", link); + r = link_queue_request_safe(link, REQUEST_TYPE_ADDRESS, + tmp, + address_free, + address_hash_func, + address_compare_func, + address_process_request, + message_counter, netlink_handler, ret); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request address: %m"); + if (r == 0) + return 0; + + address_enter_requesting(tmp); + if (existing) + address_enter_requesting(existing); + + TAKE_PTR(tmp); + return 1; +} + +static int static_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) { + int r; + + assert(link); + + r = address_configure_handler_internal(rtnl, m, link, "Failed to set static address"); + if (r <= 0) + return r; + + if (link->static_address_messages == 0) { + log_link_debug(link, "Addresses set"); + link->static_addresses_configured = true; + link_check_ready(link); + } + + return 1; +} + +int link_request_static_address(Link *link, const Address *address) { + assert(link); + assert(address); + assert(address->source == NETWORK_CONFIG_SOURCE_STATIC); + + return link_request_address(link, address, &link->static_address_messages, + static_address_handler, NULL); +} + +int link_request_static_addresses(Link *link) { + Address *a; + int r; + + assert(link); + assert(link->network); + + link->static_addresses_configured = false; + + ORDERED_HASHMAP_FOREACH(a, link->network->addresses_by_section) { + r = link_request_static_address(link, a); + if (r < 0) + return r; + } + + r = link_request_radv_addresses(link); + if (r < 0) + return r; + + if (link->static_address_messages == 0) { + link->static_addresses_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Setting addresses"); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +void address_cancel_request(Address *address) { + Request req; + + assert(address); + assert(address->link); + + if (!address_is_requesting(address)) + return; + + req = (Request) { + .link = address->link, + .type = REQUEST_TYPE_ADDRESS, + .userdata = address, + .hash_func = (hash_func_t) address_hash_func, + .compare_func = (compare_func_t) address_compare_func, + }; + + request_detach(address->link->manager, &req); + address_cancel_requesting(address); +} + +int manager_rtnl_process_address(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) { + _cleanup_(address_freep) Address *tmp = NULL; + struct ifa_cacheinfo cinfo; + Link *link; + uint16_t type; + Address *address = NULL; + Request *req = NULL; + bool is_new = false, update_dhcp4; + int ifindex, r; + + assert(rtnl); + assert(message); + assert(m); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "rtnl: failed to receive address message, ignoring"); + + return 0; + } + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get message type, ignoring: %m"); + return 0; + } else if (!IN_SET(type, RTM_NEWADDR, RTM_DELADDR)) { + log_warning("rtnl: received unexpected message type %u when processing address, ignoring.", type); + return 0; + } + + r = sd_rtnl_message_addr_get_ifindex(message, &ifindex); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m"); + return 0; + } else if (ifindex <= 0) { + log_warning("rtnl: received address message with invalid ifindex %d, ignoring.", ifindex); + return 0; + } + + r = link_get_by_index(m, ifindex, &link); + if (r < 0) { + /* 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(); + + /* First, read minimal information to make address_get() work below. */ + + 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; + } + + 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; + } + + break; + + case AF_INET6: + r = sd_netlink_message_read_in6_addr(message, IFA_LOCAL, &tmp->in_addr.in6); + if (r >= 0) { + /* Have peer address. */ + r = sd_netlink_message_read_in6_addr(message, IFA_ADDRESS, &tmp->in_addr_peer.in6); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: could not get peer address from address message, ignoring: %m"); + return 0; + } + } else if (r == -ENODATA) { + /* Does not have peer address. */ + r = sd_netlink_message_read_in6_addr(message, IFA_ADDRESS, &tmp->in_addr.in6); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received address message without valid address, ignoring: %m"); + return 0; + } + } else { + log_link_warning_errno(link, r, "rtnl: could not get local address from address message, ignoring: %m"); + return 0; + } + + break; + + default: + assert_not_reached(); + } + + update_dhcp4 = tmp->family == AF_INET6; + + /* Then, find the managed Address and Request objects corresponding to the received address. */ + (void) address_get(link, tmp, &address); + (void) address_get_request(link, tmp, &req); + + if (type == RTM_DELADDR) { + if (address) { + address_enter_removed(address); + log_address_debug(address, "Forgetting removed", link); + (void) address_drop(address); + } else + log_address_debug(tmp, "Kernel removed unknown", link); + + if (req) + address_enter_removed(req->userdata); + + goto finalize; + } + + if (!address) { + /* If we did not know the address, then save it. */ + r = address_add(link, tmp); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to save received address %s, ignoring: %m", + IN_ADDR_PREFIX_TO_STRING(tmp->family, &tmp->in_addr, tmp->prefixlen)); + return 0; + } + address = TAKE_PTR(tmp); + + is_new = true; + + } else { + /* Otherwise, update the managed Address object with the netlink notification. */ + address->prefixlen = tmp->prefixlen; + address->in_addr_peer = tmp->in_addr_peer; + } + + /* Also update information that cannot be obtained through netlink notification. */ + if (req && req->waiting_reply) { + Address *a = ASSERT_PTR(req->userdata); + + address->source = a->source; + address->provider = a->provider; + (void) free_and_strdup_warn(&address->netlabel, a->netlabel); + nft_set_context_clear(&address->nft_set_context); + (void) nft_set_context_dup(&a->nft_set_context, &address->nft_set_context); + address->requested_as_null = a->requested_as_null; + address->callback = a->callback; + } + + /* Then, update miscellaneous info. */ + r = sd_rtnl_message_addr_get_scope(message, &address->scope); + if (r < 0) + log_link_debug_errno(link, r, "rtnl: received address message without scope, ignoring: %m"); + + if (address->family == AF_INET) { + _cleanup_free_ char *label = NULL; + + r = sd_netlink_message_read_string_strdup(message, IFA_LABEL, &label); + if (r >= 0) { + if (!streq_ptr(label, link->ifname)) + free_and_replace(address->label, label); + } else if (r != -ENODATA) + log_link_debug_errno(link, r, "rtnl: could not get label from address message, ignoring: %m"); + + r = sd_netlink_message_read_in_addr(message, IFA_BROADCAST, &address->broadcast); + if (r < 0 && r != -ENODATA) + log_link_debug_errno(link, r, "rtnl: could not get broadcast from address message, ignoring: %m"); + } + + r = sd_netlink_message_read_u32(message, IFA_FLAGS, &address->flags); + if (r == -ENODATA) { + unsigned char flags; + + /* For old kernels. */ + r = sd_rtnl_message_addr_get_flags(message, &flags); + if (r >= 0) + address->flags = flags; + } else if (r < 0) + log_link_debug_errno(link, r, "rtnl: failed to read IFA_FLAGS attribute, ignoring: %m"); + + r = sd_netlink_message_read_cache_info(message, IFA_CACHEINFO, &cinfo); + if (r >= 0) + address_set_lifetime(m, address, &cinfo); + else if (r != -ENODATA) + log_link_debug_errno(link, r, "rtnl: failed to read IFA_CACHEINFO attribute, ignoring: %m"); + + r = sd_netlink_message_read_u32(message, IFA_RT_PRIORITY, &address->route_metric); + if (r < 0 && r != -ENODATA) + log_link_debug_errno(link, r, "rtnl: failed to read IFA_RT_PRIORITY attribute, ignoring: %m"); + + address_enter_configured(address); + if (req) + address_enter_configured(req->userdata); + + log_address_debug(address, is_new ? "Received new": "Received updated", link); + + /* address_update() logs internally, so we don't need to here. */ + r = address_update(address); + if (r < 0) + link_enter_failed(link); + +finalize: + if (update_dhcp4) { + r = dhcp4_update_ipv6_connectivity(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to notify IPv6 connectivity to DHCPv4 client: %m"); + link_enter_failed(link); + } + } + + return 1; +} + +int config_parse_broadcast( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + union in_addr_union u; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + if (isempty(rvalue)) { + /* The broadcast address will be calculated based on Address=, and set if the link is + * not a wireguard interface. Here, we do not check or set n->family. */ + n->broadcast = (struct in_addr) {}; + n->set_broadcast = -1; + TAKE_PTR(n); + return 0; + } + + r = parse_boolean(rvalue); + if (r >= 0) { + /* The broadcast address will be calculated based on Address=. Here, we do not check or + * set n->family. */ + n->broadcast = (struct in_addr) {}; + n->set_broadcast = r; + TAKE_PTR(n); + return 0; + } + + if (n->family == AF_INET6) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Broadcast is not valid for IPv6 addresses, ignoring assignment: %s", rvalue); + return 0; + } + + r = in_addr_from_string(AF_INET, rvalue, &u); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Broadcast is invalid, ignoring assignment: %s", rvalue); + return 0; + } + if (in4_addr_is_null(&u.in)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Broadcast cannot be ANY address, ignoring assignment: %s", rvalue); + return 0; + } + + n->broadcast = u.in; + n->set_broadcast = true; + n->family = AF_INET; + TAKE_PTR(n); + + return 0; +} + +int config_parse_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + union in_addr_union buffer; + unsigned char prefixlen; + int r, f; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + if (streq(section, "Network")) + /* we are not in an Address section, so use line number instead. */ + r = address_new_static(network, filename, line, &n); + else + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + /* Address=address/prefixlen */ + r = in_addr_prefix_from_string_auto_internal(rvalue, PREFIXLEN_REFUSE, &f, &buffer, &prefixlen); + if (r == -ENOANO) { + r = in_addr_prefix_from_string_auto(rvalue, &f, &buffer, &prefixlen); + if (r >= 0) + log_syntax(unit, LOG_WARNING, filename, line, r, + "Address '%s' is specified without prefix length. Assuming the prefix length is %u. " + "Please specify the prefix length explicitly.", rvalue, 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; + n->requested_as_null = !in_addr_is_set(n->family, &n->in_addr); + } else + n->in_addr_peer = buffer; + + TAKE_PTR(n); + return 0; +} + +int config_parse_label( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + if (isempty(rvalue)) { + n->label = mfree(n->label); + TAKE_PTR(n); + return 0; + } + + if (!address_label_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Interface label is too long or invalid, ignoring assignment: %s", rvalue); + return 0; + } + + r = free_and_strdup(&n->label, rvalue); + if (r < 0) + return log_oom(); + + TAKE_PTR(n); + return 0; +} + +int config_parse_lifetime( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + usec_t k; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + /* We accept only "forever", "infinity", empty, or "0". */ + if (STR_IN_SET(rvalue, "forever", "infinity", "")) + k = USEC_INFINITY; + else if (streq(rvalue, "0")) + k = 0; + else { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid PreferredLifetime= value, ignoring: %s", rvalue); + return 0; + } + + n->lifetime_preferred_usec = k; + TAKE_PTR(n); + + return 0; +} + +int config_parse_address_flags( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring: %s", lvalue, rvalue); + return 0; + } + + if (streq(lvalue, "AddPrefixRoute")) + r = !r; + + SET_FLAG(n->flags, ltype, r); + + TAKE_PTR(n); + return 0; +} + +int config_parse_address_scope( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + r = route_scope_from_string(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse address scope \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->scope = r; + n->scope_set = true; + TAKE_PTR(n); + return 0; +} + +int config_parse_address_route_metric( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + r = safe_atou32(rvalue, &n->route_metric); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_duplicate_address_detection( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + r = parse_boolean(rvalue); + if (r >= 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "For historical reasons, %s=%s means %s=%s. " + "Please use 'both', 'ipv4', 'ipv6' or 'none' instead.", + lvalue, rvalue, lvalue, r ? "none" : "both"); + n->duplicate_address_detection = r ? ADDRESS_FAMILY_NO : ADDRESS_FAMILY_YES; + n = NULL; + return 0; + } + + AddressFamily a = duplicate_address_detection_address_family_from_string(rvalue); + if (a < 0) { + log_syntax(unit, LOG_WARNING, filename, line, a, + "Failed to parse %s=, ignoring: %s", lvalue, rvalue); + return 0; + } + n->duplicate_address_detection = a; + + TAKE_PTR(n); + return 0; +} + +int config_parse_address_netlabel( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + assert(network); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate new address, ignoring assignment: %m"); + return 0; + } + + r = config_parse_string(unit, filename, line, section, section_line, + lvalue, CONFIG_PARSE_STRING_SAFE, rvalue, &n->netlabel, network); + if (r < 0) + return r; + + TAKE_PTR(n); + return 0; +} + +static void address_section_adjust_broadcast(Address *address) { + assert(address); + assert(address->section); + + if (!in4_addr_is_set(&address->broadcast)) + return; + + if (address->family == AF_INET6) + log_warning("%s: broadcast address is set for an IPv6 address. " + "Ignoring Broadcast= setting in the [Address] section from line %u.", + address->section->filename, address->section->line); + else if (address->prefixlen > 30) + log_warning("%s: broadcast address is set for an IPv4 address with prefix length larger than 30. " + "Ignoring Broadcast= setting in the [Address] section from line %u.", + address->section->filename, address->section->line); + else if (in4_addr_is_set(&address->in_addr_peer.in)) + log_warning("%s: broadcast address is set for an IPv4 address with peer address. " + "Ignoring Broadcast= setting in the [Address] section from line %u.", + address->section->filename, address->section->line); + else if (!in4_addr_is_set(&address->in_addr.in)) + log_warning("%s: broadcast address is set for an IPv4 address with null address. " + "Ignoring Broadcast= setting in the [Address] section from line %u.", + address->section->filename, address->section->line); + else + /* Otherwise, keep the specified broadcast address. */ + return; + + address->broadcast.s_addr = 0; +} + +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 was configured. " + "Ignoring [Address] section from line %u.", + address->section->filename, address->section->line); + } + + if (address->family == AF_INET6 && !socket_ipv6_is_supported()) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: an IPv6 address was configured, but the kernel does not support IPv6. " + "Ignoring [Address] section from line %u.", + address->section->filename, address->section->line); + + assert(IN_SET(address->family, AF_INET, AF_INET6)); + + address_section_adjust_broadcast(address); + + if (address->family == AF_INET6 && address->label) { + log_warning("%s: address label is set for IPv6 address in the [Address] section from line %u. " + "Ignoring Label= setting.", + address->section->filename, address->section->line); + + address->label = mfree(address->label); + } + + if (!address->scope_set) { + if (in_addr_is_localhost(address->family, &address->in_addr) > 0) + address->scope = RT_SCOPE_HOST; + else if (in_addr_is_link_local(address->family, &address->in_addr) > 0) + address->scope = RT_SCOPE_LINK; + } + + if (address->duplicate_address_detection < 0) { + if (address->family == AF_INET6) + address->duplicate_address_detection = ADDRESS_FAMILY_IPV6; + else if (in4_addr_is_link_local(&address->in_addr.in)) + address->duplicate_address_detection = ADDRESS_FAMILY_IPV4; + else + address->duplicate_address_detection = ADDRESS_FAMILY_NO; + } else if (address->duplicate_address_detection == ADDRESS_FAMILY_IPV6 && address->family == AF_INET) + log_warning("%s: DuplicateAddressDetection=ipv6 is specified for IPv4 address, ignoring.", + address->section->filename); + else if (address->duplicate_address_detection == ADDRESS_FAMILY_IPV4 && address->family == AF_INET6) + log_warning("%s: DuplicateAddressDetection=ipv4 is specified for IPv6 address, ignoring.", + address->section->filename); + + if (address->family == AF_INET6 && + !FLAGS_SET(address->duplicate_address_detection, ADDRESS_FAMILY_IPV6)) + address->flags |= IFA_F_NODAD; + + uint32_t filtered_flags = address->family == AF_INET ? + address->flags & KNOWN_FLAGS & ~UNMANAGED_FLAGS & ~IPV6ONLY_FLAGS : + address->flags & KNOWN_FLAGS & ~UNMANAGED_FLAGS; + if (address->flags != filtered_flags) { + _cleanup_free_ char *str = NULL; + + (void) address_flags_to_string_alloc(address->flags ^ filtered_flags, address->family, &str); + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: unexpected address flags \"%s\" were configured. " + "Ignoring [Address] section from line %u.", + address->section->filename, strna(str), address->section->line); + } + + return 0; +} + +int network_drop_invalid_addresses(Network *network) { + _cleanup_set_free_ Set *addresses = NULL; + Address *address; + int r; + + assert(network); + + ORDERED_HASHMAP_FOREACH(address, network->addresses_by_section) { + Address *dup; + + if (address_section_verify(address) < 0) { + /* Drop invalid [Address] sections or Address= settings in [Network]. + * Note that address_free() will drop the address from addresses_by_section. */ + address_free(address); + continue; + } + + /* Always use the setting specified later. So, remove the previously assigned setting. */ + dup = set_remove(addresses, address); + if (dup) { + log_warning("%s: Duplicated address %s is specified at line %u and %u, " + "dropping the address setting specified at line %u.", + dup->section->filename, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen), + address->section->line, + dup->section->line, dup->section->line); + /* address_free() will drop the address from addresses_by_section. */ + address_free(dup); + } + + /* Use address_hash_ops, instead of address_hash_ops_free. Otherwise, the Address objects + * will be freed. */ + r = set_ensure_put(&addresses, &address_hash_ops, address); + if (r < 0) + return log_oom(); + assert(r > 0); + } + + r = network_adjust_dhcp_server(network, &addresses); + if (r < 0) + return r; + + return 0; +} + +int config_parse_address_ip_nft_set( + const char *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(lvalue); + assert(rvalue); + assert(network); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate a new address, ignoring assignment: %m"); + return 0; + } + + r = config_parse_nft_set(unit, filename, line, section, section_line, lvalue, ltype, rvalue, &n->nft_set_context, network); + if (r < 0) + return r; + + TAKE_PTR(n); + return 0; +} diff --git a/src/network/networkd-address.h b/src/network/networkd-address.h new file mode 100644 index 0000000..5be2f77 --- /dev/null +++ b/src/network/networkd-address.h @@ -0,0 +1,145 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <stdio.h> + +#include "conf-parser.h" +#include "firewall-util.h" +#include "hash-funcs.h" +#include "in-addr-util.h" +#include "network-util.h" +#include "networkd-link.h" +#include "networkd-util.h" +#include "time-util.h" + +typedef struct Address Address; +typedef struct Manager Manager; +typedef struct Network Network; +typedef struct Request Request; +typedef int (*address_ready_callback_t)(Address *address); +typedef int (*address_netlink_handler_t)( + sd_netlink *rtnl, + sd_netlink_message *m, + Request *req, + Link *link, + Address *address); + +struct Address { + Link *link; + Network *network; + ConfigSection *section; + NetworkConfigSource source; + NetworkConfigState state; + union in_addr_union provider; /* DHCP server or router address */ + + int family; + unsigned char prefixlen; + unsigned char scope; + uint32_t flags; + uint32_t route_metric; /* route metric for prefix route */ + char *label, *netlabel; + + int set_broadcast; + struct in_addr broadcast; + + union in_addr_union in_addr; + union in_addr_union in_addr_peer; + + /* These are absolute points in time, and NOT timespans/durations. + * Must be specified with clock_boottime_or_monotonic(). */ + usec_t lifetime_valid_usec; + usec_t lifetime_preferred_usec; + + bool scope_set:1; + bool ip_masquerade_done:1; + bool requested_as_null:1; + + /* duplicate_address_detection is only used by static or IPv4 dynamic addresses. + * To control DAD for IPv6 dynamic addresses, set IFA_F_NODAD to flags. */ + AddressFamily duplicate_address_detection; + + /* Called when address become ready */ + address_ready_callback_t callback; + + NFTSetContext nft_set_context; +}; + +const char* format_lifetime(char *buf, size_t l, usec_t lifetime_usec) _warn_unused_result_; +/* Note: the lifetime of the compound literal is the immediately surrounding block, + * see C11 §6.5.2.5, and + * https://stackoverflow.com/questions/34880638/compound-literal-lifetime-and-if-blocks */ +#define FORMAT_LIFETIME(lifetime) \ + format_lifetime((char[FORMAT_TIMESPAN_MAX+STRLEN("for ")]){}, FORMAT_TIMESPAN_MAX+STRLEN("for "), lifetime) + +int address_flags_to_string_alloc(uint32_t flags, int family, char **ret); + +void link_get_address_states( + Link *link, + LinkAddressState *ret_ipv4, + LinkAddressState *ret_ipv6, + LinkAddressState *ret_all); + +extern const struct hash_ops address_hash_ops; + +int address_new(Address **ret); +int address_new_static(Network *network, const char *filename, unsigned section_line, Address **ret); +Address* address_free(Address *address); +int address_get(Link *link, const Address *in, Address **ret); +int address_get_harder(Link *link, const Address *in, Address **ret); +int address_configure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg); +int address_remove(Address *address); +int address_remove_and_drop(Address *address); +int address_dup(const Address *src, Address **ret); +bool address_is_ready(const Address *a); +bool link_check_addresses_ready(Link *link, NetworkConfigSource source); + +DEFINE_SECTION_CLEANUP_FUNCTIONS(Address, address_free); + +int link_drop_managed_addresses(Link *link); +int link_drop_foreign_addresses(Link *link); +int link_drop_ipv6ll_addresses(Link *link); +void link_foreignize_addresses(Link *link); +bool link_address_is_dynamic(const Link *link, const Address *address); +int link_get_address(Link *link, int family, const union in_addr_union *address, unsigned char prefixlen, Address **ret); +static inline int link_get_ipv6_address(Link *link, const struct in6_addr *address, unsigned char prefixlen, Address **ret) { + assert(address); + return link_get_address(link, AF_INET6, &(union in_addr_union) { .in6 = *address }, prefixlen, ret); +} +static inline int link_get_ipv4_address(Link *link, const struct in_addr *address, unsigned char prefixlen, Address **ret) { + assert(address); + return link_get_address(link, AF_INET, &(union in_addr_union) { .in = *address }, prefixlen, ret); +} +int manager_get_address(Manager *manager, int family, const union in_addr_union *address, unsigned char prefixlen, Address **ret); +bool manager_has_address(Manager *manager, int family, const union in_addr_union *address, bool check_ready); + +void address_cancel_request(Address *address); +int link_request_address( + Link *link, + const Address *address, + unsigned *message_counter, + address_netlink_handler_t netlink_handler, + Request **ret); +int link_request_static_address(Link *link, const Address *address); +int link_request_static_addresses(Link *link); + +int manager_rtnl_process_address(sd_netlink *nl, sd_netlink_message *message, Manager *m); + +int address_section_verify(Address *address); +int network_drop_invalid_addresses(Network *network); + +DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(Address, address); + +void link_mark_addresses(Link *link, NetworkConfigSource source); + +CONFIG_PARSER_PROTOTYPE(config_parse_address); +CONFIG_PARSER_PROTOTYPE(config_parse_broadcast); +CONFIG_PARSER_PROTOTYPE(config_parse_label); +CONFIG_PARSER_PROTOTYPE(config_parse_lifetime); +CONFIG_PARSER_PROTOTYPE(config_parse_address_flags); +CONFIG_PARSER_PROTOTYPE(config_parse_address_scope); +CONFIG_PARSER_PROTOTYPE(config_parse_address_route_metric); +CONFIG_PARSER_PROTOTYPE(config_parse_duplicate_address_detection); +CONFIG_PARSER_PROTOTYPE(config_parse_address_netlabel); +CONFIG_PARSER_PROTOTYPE(config_parse_address_ip_nft_set); diff --git a/src/network/networkd-bridge-fdb.c b/src/network/networkd-bridge-fdb.c new file mode 100644 index 0000000..803e27c --- /dev/null +++ b/src/network/networkd-bridge-fdb.c @@ -0,0 +1,535 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <net/ethernet.h> +#include <net/if.h> + +#include "alloc-util.h" +#include "bridge.h" +#include "netlink-util.h" +#include "networkd-bridge-fdb.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "networkd-util.h" +#include "parse-util.h" +#include "string-table.h" +#include "vlan-util.h" +#include "vxlan.h" + +#define STATIC_BRIDGE_FDB_ENTRIES_PER_NETWORK_MAX 1024U + +/* remove and FDB entry. */ +BridgeFDB *bridge_fdb_free(BridgeFDB *fdb) { + if (!fdb) + return NULL; + + if (fdb->network) { + assert(fdb->section); + hashmap_remove(fdb->network->bridge_fdb_entries_by_section, fdb->section); + } + + config_section_free(fdb->section); + + free(fdb->outgoing_ifname); + return mfree(fdb); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(BridgeFDB, bridge_fdb_free); + +/* create a new FDB entry or get an existing one. */ +static int bridge_fdb_new_static( + Network *network, + const char *filename, + unsigned section_line, + BridgeFDB **ret) { + + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(bridge_fdb_freep) BridgeFDB *fdb = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + /* search entry in hashmap first. */ + fdb = hashmap_get(network->bridge_fdb_entries_by_section, n); + if (fdb) { + *ret = TAKE_PTR(fdb); + return 0; + } + + if (hashmap_size(network->bridge_fdb_entries_by_section) >= STATIC_BRIDGE_FDB_ENTRIES_PER_NETWORK_MAX) + return -E2BIG; + + /* allocate space for and FDB entry. */ + fdb = new(BridgeFDB, 1); + if (!fdb) + return -ENOMEM; + + /* init FDB structure. */ + *fdb = (BridgeFDB) { + .network = network, + .section = TAKE_PTR(n), + .vni = VXLAN_VID_MAX + 1, + .ntf_flags = NEIGHBOR_CACHE_ENTRY_FLAGS_SELF, + }; + + r = hashmap_ensure_put(&network->bridge_fdb_entries_by_section, &config_section_hash_ops, fdb->section, fdb); + if (r < 0) + return r; + + /* return allocated FDB structure. */ + *ret = TAKE_PTR(fdb); + + return 0; +} + +static int bridge_fdb_configure_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not add bridge FDB entry"); + link_enter_failed(link); + return 0; + } + + if (link->static_bridge_fdb_messages == 0) { + log_link_debug(link, "Bridge FDB entries set"); + link->static_bridge_fdb_configured = true; + link_check_ready(link); + } + + return 0; +} + +/* send a request to the kernel to add a FDB entry in its static MAC table. */ +static int bridge_fdb_configure_message(const BridgeFDB *fdb, Link *link, sd_netlink_message *req) { + int r; + + assert(fdb); + assert(link); + + r = sd_rtnl_message_neigh_set_flags(req, fdb->ntf_flags); + if (r < 0) + return r; + + /* only NUD_PERMANENT state supported. */ + r = sd_rtnl_message_neigh_set_state(req, NUD_NOARP | NUD_PERMANENT); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, NDA_LLADDR, &fdb->mac_addr, sizeof(fdb->mac_addr)); + if (r < 0) + return r; + + /* VLAN Id is optional. We'll add VLAN Id only if it's specified. */ + if (fdb->vlan_id > 0) { + r = sd_netlink_message_append_u16(req, NDA_VLAN, fdb->vlan_id); + if (r < 0) + return r; + } + + if (fdb->outgoing_ifindex > 0) { + r = sd_netlink_message_append_u32(req, NDA_IFINDEX, fdb->outgoing_ifindex); + if (r < 0) + return r; + } + + if (in_addr_is_set(fdb->family, &fdb->destination_addr)) { + r = netlink_message_append_in_addr_union(req, NDA_DST, fdb->family, &fdb->destination_addr); + if (r < 0) + return r; + } + + if (fdb->vni <= VXLAN_VID_MAX) { + r = sd_netlink_message_append_u32(req, NDA_VNI, fdb->vni); + if (r < 0) + return r; + } + + return 0; +} + +static int bridge_fdb_configure(BridgeFDB *fdb, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(fdb); + assert(link); + assert(link->manager); + assert(req); + + r = sd_rtnl_message_new_neigh(link->manager->rtnl, &m, RTM_NEWNEIGH, link->ifindex, AF_BRIDGE); + if (r < 0) + return r; + + r = bridge_fdb_configure_message(fdb, link, m); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static bool bridge_fdb_is_ready_to_configure(BridgeFDB *fdb, Link *link) { + Link *out = NULL; + + assert(fdb); + assert(link); + assert(link->manager); + + if (!link_is_ready_to_configure(link, false)) + return false; + + if (fdb->outgoing_ifname) { + if (link_get_by_name(link->manager, fdb->outgoing_ifname, &out) < 0) + return false; + + fdb->outgoing_ifindex = out->ifindex; + } else if (fdb->outgoing_ifindex > 0) { + if (link_get_by_index(link->manager, fdb->outgoing_ifindex, &out) < 0) + return false; + } + if (out && !link_is_ready_to_configure(out, false)) + return false; + + return true; +} + +static int bridge_fdb_process_request(Request *req, Link *link, void *userdata) { + BridgeFDB *fdb = ASSERT_PTR(userdata); + int r; + + assert(req); + assert(link); + + if (!bridge_fdb_is_ready_to_configure(fdb, link)) + return 0; + + r = bridge_fdb_configure(fdb, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure bridge FDB: %m"); + + return 1; +} + +int link_request_static_bridge_fdb(Link *link) { + BridgeFDB *fdb; + int r; + + assert(link); + assert(link->network); + + link->static_bridge_fdb_configured = false; + + HASHMAP_FOREACH(fdb, link->network->bridge_fdb_entries_by_section) { + r = link_queue_request_full(link, REQUEST_TYPE_BRIDGE_FDB, + fdb, NULL, + trivial_hash_func, + trivial_compare_func, + bridge_fdb_process_request, + &link->static_bridge_fdb_messages, + bridge_fdb_configure_handler, + NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request static bridge FDB entry: %m"); + } + + if (link->static_bridge_fdb_messages == 0) { + link->static_bridge_fdb_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Setting bridge FDB entries"); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +void network_drop_invalid_bridge_fdb_entries(Network *network) { + BridgeFDB *fdb; + + assert(network); + + HASHMAP_FOREACH(fdb, network->bridge_fdb_entries_by_section) + if (section_is_invalid(fdb->section)) + bridge_fdb_free(fdb); +} + +/* parse the HW address from config files. */ +int config_parse_fdb_hwaddr( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = bridge_fdb_new_static(network, filename, section_line, &fdb); + if (r < 0) + return log_oom(); + + r = parse_ether_addr(rvalue, &fdb->mac_addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Not a valid MAC address, ignoring assignment: %s", rvalue); + return 0; + } + + TAKE_PTR(fdb); + return 0; +} + +/* parse the VLAN Id from config files. */ +int config_parse_fdb_vlan_id( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = bridge_fdb_new_static(network, filename, section_line, &fdb); + if (r < 0) + return log_oom(); + + r = config_parse_vlanid(unit, filename, line, section, + section_line, lvalue, ltype, + rvalue, &fdb->vlan_id, userdata); + if (r < 0) + return r; + + TAKE_PTR(fdb); + return 0; +} + +int config_parse_fdb_destination( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = bridge_fdb_new_static(network, filename, section_line, &fdb); + if (r < 0) + return log_oom(); + + r = in_addr_from_string_auto(rvalue, &fdb->family, &fdb->destination_addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "FDB destination IP address is invalid, ignoring assignment: %s", + rvalue); + return 0; + } + + TAKE_PTR(fdb); + return 0; +} + +int config_parse_fdb_vxlan_vni( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL; + Network *network = userdata; + uint32_t vni; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = bridge_fdb_new_static(network, filename, section_line, &fdb); + if (r < 0) + return log_oom(); + + r = safe_atou32(rvalue, &vni); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse VXLAN Network Identifier (VNI), ignoring assignment: %s", + rvalue); + return 0; + } + + if (vni > VXLAN_VID_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "FDB invalid VXLAN Network Identifier (VNI), ignoring assignment: %s", + rvalue); + return 0; + } + + fdb->vni = vni; + + TAKE_PTR(fdb); + return 0; +} + +static const char* const ntf_flags_table[_NEIGHBOR_CACHE_ENTRY_FLAGS_MAX] = { + [NEIGHBOR_CACHE_ENTRY_FLAGS_USE] = "use", + [NEIGHBOR_CACHE_ENTRY_FLAGS_SELF] = "self", + [NEIGHBOR_CACHE_ENTRY_FLAGS_MASTER] = "master", + [NEIGHBOR_CACHE_ENTRY_FLAGS_ROUTER] = "router", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(ntf_flags, NeighborCacheEntryFlags); + +int config_parse_fdb_ntf_flags( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL; + Network *network = userdata; + NeighborCacheEntryFlags f; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = bridge_fdb_new_static(network, filename, section_line, &fdb); + if (r < 0) + return log_oom(); + + f = ntf_flags_from_string(rvalue); + if (f < 0) { + log_syntax(unit, LOG_WARNING, filename, line, f, + "FDB failed to parse AssociatedWith=, ignoring assignment: %s", + rvalue); + return 0; + } + + fdb->ntf_flags = f; + + TAKE_PTR(fdb); + return 0; +} + +int config_parse_fdb_interface( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(bridge_fdb_free_or_set_invalidp) BridgeFDB *fdb = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = bridge_fdb_new_static(network, filename, section_line, &fdb); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + fdb->outgoing_ifname = mfree(fdb->outgoing_ifname); + fdb->outgoing_ifindex = 0; + TAKE_PTR(fdb); + return 0; + } + + r = parse_ifindex(rvalue); + if (r > 0) { + fdb->outgoing_ifname = mfree(fdb->outgoing_ifname); + fdb->outgoing_ifindex = r; + TAKE_PTR(fdb); + return 0; + } + + if (!ifname_valid_full(rvalue, IFNAME_VALID_ALTERNATIVE)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid interface name in %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + r = free_and_strdup(&fdb->outgoing_ifname, rvalue); + if (r < 0) + return log_oom(); + fdb->outgoing_ifindex = 0; + + TAKE_PTR(fdb); + return 0; +} diff --git a/src/network/networkd-bridge-fdb.h b/src/network/networkd-bridge-fdb.h new file mode 100644 index 0000000..b59d673 --- /dev/null +++ b/src/network/networkd-bridge-fdb.h @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <inttypes.h> +#include <linux/neighbour.h> + +#include "conf-parser.h" +#include "ether-addr-util.h" +#include "in-addr-util.h" + +typedef struct Link Link; +typedef struct Network Network; + +typedef enum NeighborCacheEntryFlags { + NEIGHBOR_CACHE_ENTRY_FLAGS_USE = NTF_USE, + NEIGHBOR_CACHE_ENTRY_FLAGS_SELF = NTF_SELF, + NEIGHBOR_CACHE_ENTRY_FLAGS_MASTER = NTF_MASTER, + NEIGHBOR_CACHE_ENTRY_FLAGS_ROUTER = NTF_ROUTER, + _NEIGHBOR_CACHE_ENTRY_FLAGS_MAX, + _NEIGHBOR_CACHE_ENTRY_FLAGS_INVALID = -EINVAL, +} NeighborCacheEntryFlags; + +typedef struct BridgeFDB { + Network *network; + ConfigSection *section; + + uint32_t vni; + + int family; + uint16_t vlan_id; + + struct ether_addr mac_addr; + union in_addr_union destination_addr; + NeighborCacheEntryFlags ntf_flags; + char *outgoing_ifname; + int outgoing_ifindex; +} BridgeFDB; + +BridgeFDB *bridge_fdb_free(BridgeFDB *fdb); + +void network_drop_invalid_bridge_fdb_entries(Network *network); + +int link_request_static_bridge_fdb(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_fdb_hwaddr); +CONFIG_PARSER_PROTOTYPE(config_parse_fdb_vlan_id); +CONFIG_PARSER_PROTOTYPE(config_parse_fdb_destination); +CONFIG_PARSER_PROTOTYPE(config_parse_fdb_vxlan_vni); +CONFIG_PARSER_PROTOTYPE(config_parse_fdb_ntf_flags); +CONFIG_PARSER_PROTOTYPE(config_parse_fdb_interface); diff --git a/src/network/networkd-bridge-mdb.c b/src/network/networkd-bridge-mdb.c new file mode 100644 index 0000000..bd1a974 --- /dev/null +++ b/src/network/networkd-bridge-mdb.c @@ -0,0 +1,365 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <linux/if_bridge.h> + +#include "netlink-util.h" +#include "networkd-bridge-mdb.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "string-util.h" +#include "vlan-util.h" + +#define STATIC_BRIDGE_MDB_ENTRIES_PER_NETWORK_MAX 1024U + +/* remove MDB entry. */ +BridgeMDB *bridge_mdb_free(BridgeMDB *mdb) { + if (!mdb) + return NULL; + + if (mdb->network) { + assert(mdb->section); + hashmap_remove(mdb->network->bridge_mdb_entries_by_section, mdb->section); + } + + config_section_free(mdb->section); + + return mfree(mdb); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(BridgeMDB, bridge_mdb_free); + +/* create a new MDB entry or get an existing one. */ +static int bridge_mdb_new_static( + Network *network, + const char *filename, + unsigned section_line, + BridgeMDB **ret) { + + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(bridge_mdb_freep) BridgeMDB *mdb = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + /* search entry in hashmap first. */ + mdb = hashmap_get(network->bridge_mdb_entries_by_section, n); + if (mdb) { + *ret = TAKE_PTR(mdb); + return 0; + } + + if (hashmap_size(network->bridge_mdb_entries_by_section) >= STATIC_BRIDGE_MDB_ENTRIES_PER_NETWORK_MAX) + return -E2BIG; + + /* allocate space for an MDB entry. */ + mdb = new(BridgeMDB, 1); + if (!mdb) + return -ENOMEM; + + /* init MDB structure. */ + *mdb = (BridgeMDB) { + .network = network, + .section = TAKE_PTR(n), + }; + + r = hashmap_ensure_put(&network->bridge_mdb_entries_by_section, &config_section_hash_ops, mdb->section, mdb); + if (r < 0) + return r; + + /* return allocated MDB structure. */ + *ret = TAKE_PTR(mdb); + return 0; +} + +static int bridge_mdb_configure_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r == -EINVAL && streq_ptr(link->kind, "bridge") && link->master_ifindex <= 0) { + /* To configure bridge MDB entries on bridge master, 1bc844ee0faa1b92e3ede00bdd948021c78d7088 (v5.4) is required. */ + if (!link->manager->bridge_mdb_on_master_not_supported) { + log_link_warning_errno(link, r, "Kernel seems not to support bridge MDB entries on bridge master, ignoring: %m"); + link->manager->bridge_mdb_on_master_not_supported = true; + } + } else if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not add MDB entry"); + link_enter_failed(link); + return 1; + } + + if (link->static_bridge_mdb_messages == 0) { + link->static_bridge_mdb_configured = true; + link_check_ready(link); + } + + return 1; +} + +/* send a request to the kernel to add an MDB entry */ +static int bridge_mdb_configure(BridgeMDB *mdb, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + struct br_mdb_entry entry; + int r; + + assert(mdb); + assert(link); + assert(link->manager); + assert(req); + + if (DEBUG_LOGGING) + log_link_debug(link, "Configuring bridge MDB entry: MulticastGroupAddress=%s, VLANId=%u", + IN_ADDR_TO_STRING(mdb->family, &mdb->group_addr), mdb->vlan_id); + + entry = (struct br_mdb_entry) { + /* If MDB entry is added on bridge master, then the state must be MDB_TEMPORARY. + * See br_mdb_add_group() in net/bridge/br_mdb.c of kernel. */ + .state = link->master_ifindex <= 0 ? MDB_TEMPORARY : MDB_PERMANENT, + .ifindex = link->ifindex, + .vid = mdb->vlan_id, + }; + + switch (mdb->family) { + case AF_INET: + entry.addr.u.ip4 = mdb->group_addr.in.s_addr; + entry.addr.proto = htobe16(ETH_P_IP); + break; + + case AF_INET6: + entry.addr.u.ip6 = mdb->group_addr.in6; + entry.addr.proto = htobe16(ETH_P_IPV6); + break; + + default: + assert_not_reached(); + } + + r = sd_rtnl_message_new_mdb(link->manager->rtnl, &m, RTM_NEWMDB, + link->master_ifindex > 0 ? link->master_ifindex : link->ifindex); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(m, MDBA_SET_ENTRY, &entry, sizeof(entry)); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static bool bridge_mdb_is_ready_to_configure(Link *link) { + Link *master; + + assert(link); + + if (!link_is_ready_to_configure(link, false)) + return false; + + if (!link->master_set) + return false; + + if (link->master_ifindex <= 0 && streq_ptr(link->kind, "bridge")) + return true; /* The interface is bridge master. */ + + if (link_get_master(link, &master) < 0) + return false; + + if (!streq_ptr(master->kind, "bridge")) + return false; + + if (!IN_SET(master->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return false; + + if (master->set_flags_messages > 0) + return false; + + if (!link_has_carrier(master)) + return false; + + return true; +} + +static int bridge_mdb_process_request(Request *req, Link *link, void *userdata) { + BridgeMDB *mdb = ASSERT_PTR(userdata); + int r; + + assert(req); + assert(link); + + if (!bridge_mdb_is_ready_to_configure(link)) + return 0; + + r = bridge_mdb_configure(mdb, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure bridge MDB: %m"); + + return 1; +} + +int link_request_static_bridge_mdb(Link *link) { + BridgeMDB *mdb; + int r; + + assert(link); + assert(link->manager); + + link->static_bridge_mdb_configured = false; + + if (!link->network) + return 0; + + if (hashmap_isempty(link->network->bridge_mdb_entries_by_section)) + goto finish; + + HASHMAP_FOREACH(mdb, link->network->bridge_mdb_entries_by_section) { + r = link_queue_request_full(link, REQUEST_TYPE_BRIDGE_MDB, + mdb, NULL, + trivial_hash_func, + trivial_compare_func, + bridge_mdb_process_request, + &link->static_bridge_mdb_messages, + bridge_mdb_configure_handler, + NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request MDB entry to multicast group database: %m"); + } + +finish: + if (link->static_bridge_mdb_messages == 0) { + link->static_bridge_mdb_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Setting bridge MDB entries."); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +static int bridge_mdb_verify(BridgeMDB *mdb) { + if (section_is_invalid(mdb->section)) + return -EINVAL; + + if (mdb->family == AF_UNSPEC) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: [BridgeMDB] section without MulticastGroupAddress= field configured. " + "Ignoring [BridgeMDB] section from line %u.", + mdb->section->filename, mdb->section->line); + + if (!in_addr_is_multicast(mdb->family, &mdb->group_addr)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: MulticastGroupAddress= is not a multicast address. " + "Ignoring [BridgeMDB] section from line %u.", + mdb->section->filename, mdb->section->line); + + if (mdb->family == AF_INET) { + if (in4_addr_is_local_multicast(&mdb->group_addr.in)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: MulticastGroupAddress= is a local multicast address. " + "Ignoring [BridgeMDB] section from line %u.", + mdb->section->filename, mdb->section->line); + } else { + if (in6_addr_is_link_local_all_nodes(&mdb->group_addr.in6)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: MulticastGroupAddress= is the multicast all nodes address. " + "Ignoring [BridgeMDB] section from line %u.", + mdb->section->filename, mdb->section->line); + } + + return 0; +} + +void network_drop_invalid_bridge_mdb_entries(Network *network) { + BridgeMDB *mdb; + + assert(network); + + HASHMAP_FOREACH(mdb, network->bridge_mdb_entries_by_section) + if (bridge_mdb_verify(mdb) < 0) + bridge_mdb_free(mdb); +} + +/* parse the VLAN Id from config files. */ +int config_parse_mdb_vlan_id( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(bridge_mdb_free_or_set_invalidp) BridgeMDB *mdb = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = bridge_mdb_new_static(network, filename, section_line, &mdb); + if (r < 0) + return log_oom(); + + r = config_parse_vlanid(unit, filename, line, section, + section_line, lvalue, ltype, + rvalue, &mdb->vlan_id, userdata); + if (r < 0) + return r; + + TAKE_PTR(mdb); + return 0; +} + +/* parse the multicast group from config files. */ +int config_parse_mdb_group_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(bridge_mdb_free_or_set_invalidp) BridgeMDB *mdb = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = bridge_mdb_new_static(network, filename, section_line, &mdb); + if (r < 0) + return log_oom(); + + r = in_addr_from_string_auto(rvalue, &mdb->family, &mdb->group_addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Cannot parse multicast group address: %m"); + return 0; + } + + TAKE_PTR(mdb); + return 0; +} diff --git a/src/network/networkd-bridge-mdb.h b/src/network/networkd-bridge-mdb.h new file mode 100644 index 0000000..edea255 --- /dev/null +++ b/src/network/networkd-bridge-mdb.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> + +#include "conf-parser.h" +#include "in-addr-util.h" +#include "networkd-util.h" + +typedef struct Link Link; +typedef struct Network Network; + +typedef struct BridgeMDB { + Network *network; + ConfigSection *section; + + int family; + union in_addr_union group_addr; + uint16_t vlan_id; +} BridgeMDB; + +BridgeMDB *bridge_mdb_free(BridgeMDB *mdb); + +void network_drop_invalid_bridge_mdb_entries(Network *network); + +int link_request_static_bridge_mdb(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_mdb_group_address); +CONFIG_PARSER_PROTOTYPE(config_parse_mdb_vlan_id); diff --git a/src/network/networkd-bridge-vlan.c b/src/network/networkd-bridge-vlan.c new file mode 100644 index 0000000..36e3610 --- /dev/null +++ b/src/network/networkd-bridge-vlan.c @@ -0,0 +1,249 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2016 BISDN GmbH. All rights reserved. +***/ + +#include <netinet/in.h> +#include <linux/if_bridge.h> +#include <stdbool.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netlink-util.h" +#include "networkd-bridge-vlan.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "parse-util.h" +#include "vlan-util.h" + +static bool is_bit_set(unsigned bit, uint32_t scope) { + assert(bit < sizeof(scope)*8); + return scope & (UINT32_C(1) << bit); +} + +static void set_bit(unsigned nr, uint32_t *addr) { + if (nr < BRIDGE_VLAN_BITMAP_MAX) + addr[nr / 32] |= (UINT32_C(1) << (nr % 32)); +} + +static int find_next_bit(int i, uint32_t x) { + int j; + + if (i >= 32) + return -1; + + /* find first bit */ + if (i < 0) + return BUILTIN_FFS_U32(x); + + /* mask off prior finds to get next */ + j = __builtin_ffs(x >> i); + return j ? j + i : 0; +} + +int bridge_vlan_append_info( + const Link *link, + sd_netlink_message *req, + uint16_t pvid, + const uint32_t *br_vid_bitmap, + const uint32_t *br_untagged_bitmap) { + + struct bridge_vlan_info br_vlan; + bool done, untagged = false; + uint16_t begin, end; + int r, cnt; + + assert(link); + assert(req); + assert(br_vid_bitmap); + assert(br_untagged_bitmap); + + cnt = 0; + + begin = end = UINT16_MAX; + for (int k = 0; k < BRIDGE_VLAN_BITMAP_LEN; k++) { + uint32_t untagged_map = br_untagged_bitmap[k]; + uint32_t vid_map = br_vid_bitmap[k]; + unsigned base_bit = k * 32; + int i = -1; + + done = false; + do { + int j = find_next_bit(i, vid_map); + if (j > 0) { + /* first hit of any bit */ + if (begin == UINT16_MAX && end == UINT16_MAX) { + begin = end = j - 1 + base_bit; + untagged = is_bit_set(j - 1, untagged_map); + goto next; + } + + /* this bit is a continuation of prior bits */ + if (j - 2 + base_bit == end && untagged == is_bit_set(j - 1, untagged_map) && (uint16_t)j - 1 + base_bit != pvid && (uint16_t)begin != pvid) { + end++; + goto next; + } + } else + done = true; + + if (begin != UINT16_MAX) { + cnt++; + if (done && k < BRIDGE_VLAN_BITMAP_LEN - 1) + break; + + br_vlan.flags = 0; + if (untagged) + br_vlan.flags |= BRIDGE_VLAN_INFO_UNTAGGED; + + if (begin == end) { + br_vlan.vid = begin; + + if (begin == pvid) + br_vlan.flags |= BRIDGE_VLAN_INFO_PVID; + + r = sd_netlink_message_append_data(req, IFLA_BRIDGE_VLAN_INFO, &br_vlan, sizeof(br_vlan)); + if (r < 0) + return r; + } else { + br_vlan.vid = begin; + br_vlan.flags |= BRIDGE_VLAN_INFO_RANGE_BEGIN; + + r = sd_netlink_message_append_data(req, IFLA_BRIDGE_VLAN_INFO, &br_vlan, sizeof(br_vlan)); + if (r < 0) + return r; + + br_vlan.vid = end; + br_vlan.flags &= ~BRIDGE_VLAN_INFO_RANGE_BEGIN; + br_vlan.flags |= BRIDGE_VLAN_INFO_RANGE_END; + + r = sd_netlink_message_append_data(req, IFLA_BRIDGE_VLAN_INFO, &br_vlan, sizeof(br_vlan)); + if (r < 0) + return r; + } + + if (done) + break; + } + if (j > 0) { + begin = end = j - 1 + base_bit; + untagged = is_bit_set(j - 1, untagged_map); + } + + next: + i = j; + } while (!done); + } + + assert(cnt > 0); + return cnt; +} + +void network_adjust_bridge_vlan(Network *network) { + assert(network); + + if (!network->use_br_vlan) + return; + + /* pvid might not be in br_vid_bitmap yet */ + if (network->pvid) + set_bit(network->pvid, network->br_vid_bitmap); +} + +int config_parse_brvlan_pvid( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + uint16_t pvid; + int r; + + r = parse_vlanid(rvalue, &pvid); + if (r < 0) + return r; + + network->pvid = pvid; + network->use_br_vlan = true; + + return 0; +} + +int config_parse_brvlan_vlan( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + uint16_t vid, vid_end; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = parse_vid_range(rvalue, &vid, &vid_end); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse VLAN, ignoring: %s", rvalue); + return 0; + } + + for (; vid <= vid_end; vid++) + set_bit(vid, network->br_vid_bitmap); + + network->use_br_vlan = true; + return 0; +} + +int config_parse_brvlan_untagged( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + uint16_t vid, vid_end; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = parse_vid_range(rvalue, &vid, &vid_end); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Could not parse VLAN: %s", rvalue); + return 0; + } + + for (; vid <= vid_end; vid++) { + set_bit(vid, network->br_vid_bitmap); + set_bit(vid, network->br_untagged_bitmap); + } + + network->use_br_vlan = true; + return 0; +} diff --git a/src/network/networkd-bridge-vlan.h b/src/network/networkd-bridge-vlan.h new file mode 100644 index 0000000..f44b810 --- /dev/null +++ b/src/network/networkd-bridge-vlan.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2016 BISDN GmbH. All rights reserved. +***/ + +#include <inttypes.h> + +#include "sd-netlink.h" + +#include "conf-parser.h" + +#define BRIDGE_VLAN_BITMAP_MAX 4096 +#define BRIDGE_VLAN_BITMAP_LEN (BRIDGE_VLAN_BITMAP_MAX / 32) + +typedef struct Link Link; +typedef struct Network Network; + +void network_adjust_bridge_vlan(Network *network); + +int bridge_vlan_append_info( + const Link * link, + sd_netlink_message *req, + uint16_t pvid, + const uint32_t *br_vid_bitmap, + const uint32_t *br_untagged_bitmap); + +CONFIG_PARSER_PROTOTYPE(config_parse_brvlan_pvid); +CONFIG_PARSER_PROTOTYPE(config_parse_brvlan_vlan); +CONFIG_PARSER_PROTOTYPE(config_parse_brvlan_untagged); diff --git a/src/network/networkd-can.c b/src/network/networkd-can.c new file mode 100644 index 0000000..b8a1871 --- /dev/null +++ b/src/network/networkd-can.c @@ -0,0 +1,336 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <linux/can/netlink.h> + +#include "networkd-can.h" +#include "networkd-link.h" +#include "networkd-network.h" +#include "networkd-setlink.h" +#include "parse-util.h" +#include "string-util.h" + +#define CAN_TERMINATION_DEFAULT_OHM_VALUE 120 + +int can_set_netlink_message(Link *link, sd_netlink_message *m) { + int r; + + assert(link); + assert(link->network); + assert(m); + + r = sd_netlink_message_set_flags(m, NLM_F_REQUEST | NLM_F_ACK); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(m, IFLA_LINKINFO); + if (r < 0) + return r; + + r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, link->kind); + if (r < 0) + return r; + + if (link->network->can_bitrate > 0) { + struct can_bittiming bt = { + .bitrate = link->network->can_bitrate, + .sample_point = link->network->can_sample_point, + .sjw = link->network->can_sync_jump_width, + }; + + log_link_debug(link, "Setting bitrate = %u bit/s", bt.bitrate); + if (link->network->can_sample_point > 0) + log_link_debug(link, "Setting sample point = %u.%u%%", bt.sample_point / 10, bt.sample_point % 10); + else + log_link_debug(link, "Using default sample point"); + + r = sd_netlink_message_append_data(m, IFLA_CAN_BITTIMING, &bt, sizeof(bt)); + if (r < 0) + return r; + } else if (link->network->can_time_quanta_ns > 0) { + struct can_bittiming bt = { + .tq = link->network->can_time_quanta_ns, + .prop_seg = link->network->can_propagation_segment, + .phase_seg1 = link->network->can_phase_buffer_segment_1, + .phase_seg2 = link->network->can_phase_buffer_segment_2, + .sjw = link->network->can_sync_jump_width, + }; + + log_link_debug(link, "Setting time quanta = %"PRIu32" nsec", bt.tq); + r = sd_netlink_message_append_data(m, IFLA_CAN_BITTIMING, &bt, sizeof(bt)); + if (r < 0) + return r; + } + + if (link->network->can_data_bitrate > 0) { + struct can_bittiming bt = { + .bitrate = link->network->can_data_bitrate, + .sample_point = link->network->can_data_sample_point, + .sjw = link->network->can_data_sync_jump_width, + }; + + log_link_debug(link, "Setting data bitrate = %u bit/s", bt.bitrate); + if (link->network->can_data_sample_point > 0) + log_link_debug(link, "Setting data sample point = %u.%u%%", bt.sample_point / 10, bt.sample_point % 10); + else + log_link_debug(link, "Using default data sample point"); + + r = sd_netlink_message_append_data(m, IFLA_CAN_DATA_BITTIMING, &bt, sizeof(bt)); + if (r < 0) + return r; + } else if (link->network->can_data_time_quanta_ns > 0) { + struct can_bittiming bt = { + .tq = link->network->can_data_time_quanta_ns, + .prop_seg = link->network->can_data_propagation_segment, + .phase_seg1 = link->network->can_data_phase_buffer_segment_1, + .phase_seg2 = link->network->can_data_phase_buffer_segment_2, + .sjw = link->network->can_data_sync_jump_width, + }; + + log_link_debug(link, "Setting data time quanta = %"PRIu32" nsec", bt.tq); + r = sd_netlink_message_append_data(m, IFLA_CAN_DATA_BITTIMING, &bt, sizeof(bt)); + if (r < 0) + return r; + } + + if (link->network->can_restart_us > 0) { + uint64_t restart_ms; + + if (link->network->can_restart_us == USEC_INFINITY) + restart_ms = 0; + else + restart_ms = DIV_ROUND_UP(link->network->can_restart_us, USEC_PER_MSEC); + + log_link_debug(link, "Setting restart = %s", FORMAT_TIMESPAN(restart_ms * 1000, MSEC_PER_SEC)); + r = sd_netlink_message_append_u32(m, IFLA_CAN_RESTART_MS, restart_ms); + if (r < 0) + return r; + } + + if (link->network->can_control_mode_mask != 0) { + struct can_ctrlmode cm = { + .mask = link->network->can_control_mode_mask, + .flags = link->network->can_control_mode_flags, + }; + + r = sd_netlink_message_append_data(m, IFLA_CAN_CTRLMODE, &cm, sizeof(cm)); + if (r < 0) + return r; + } + + if (link->network->can_termination_set) { + log_link_debug(link, "Setting can-termination to '%u'.", link->network->can_termination); + + r = sd_netlink_message_append_u16(m, IFLA_CAN_TERMINATION, link->network->can_termination); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + return 0; +} + +int config_parse_can_bitrate( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint32_t *br = ASSERT_PTR(data); + uint64_t sz; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = parse_size(rvalue, 1000, &sz); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse can bitrate '%s', ignoring: %m", rvalue); + return 0; + } + + /* Linux uses __u32 for bitrates, so the value should not exceed that. */ + if (sz <= 0 || sz > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Bit rate out of permitted range 1...4294967295"); + return 0; + } + + *br = (uint32_t) sz; + + return 0; +} + +int config_parse_can_time_quanta( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + nsec_t val, *tq = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = parse_nsec(rvalue, &val); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse can time quanta '%s', ignoring: %m", rvalue); + return 0; + } + + /* Linux uses __u32 for bitrates, so the value should not exceed that. */ + if (val <= 0 || val > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Time quanta out of permitted range 1...4294967295"); + return 0; + } + + *tq = val; + return 0; +} + +int config_parse_can_restart_usec( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + usec_t usec, *restart_usec = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = parse_sec(rvalue, &usec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse CAN restart sec '%s', ignoring: %m", rvalue); + return 0; + } + + if (usec != USEC_INFINITY && + DIV_ROUND_UP(usec, USEC_PER_MSEC) > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "CAN RestartSec= must be in the range 0...%"PRIu32"ms, ignoring: %s", UINT32_MAX, rvalue); + return 0; + } + + *restart_usec = usec; + return 0; +} + +int config_parse_can_control_mode( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + uint32_t mask = ltype; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(mask != 0); + + if (isempty(rvalue)) { + network->can_control_mode_mask &= ~mask; + network->can_control_mode_flags &= ~mask; + return 0; + } + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse CAN control mode '%s', ignoring: %s", lvalue, rvalue); + return 0; + } + + network->can_control_mode_mask |= mask; + SET_FLAG(network->can_control_mode_flags, mask, r); + return 0; +} + +int config_parse_can_termination( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + network->can_termination_set = false; + return 0; + } + + /* Note that 0 termination ohm value means no termination resistor, and there is no conflict + * between parse_boolean() and safe_atou16() when Termination=0. However, Termination=1 must be + * treated as 1 ohm, instead of true (and then the default ohm value). So, we need to parse the + * string with safe_atou16() at first. */ + + r = safe_atou16(rvalue, &network->can_termination); + if (r < 0) { + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse CAN termination value, ignoring: %s", rvalue); + return 0; + } + + network->can_termination = r ? CAN_TERMINATION_DEFAULT_OHM_VALUE : 0; + } + + network->can_termination_set = true; + return 0; +} diff --git a/src/network/networkd-can.h b/src/network/networkd-can.h new file mode 100644 index 0000000..3945082 --- /dev/null +++ b/src/network/networkd-can.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <linux/can/netlink.h> + +#include "sd-netlink.h" + +#include "conf-parser.h" + +typedef struct Link Link; + +int can_set_netlink_message(Link *link, sd_netlink_message *m); + +CONFIG_PARSER_PROTOTYPE(config_parse_can_bitrate); +CONFIG_PARSER_PROTOTYPE(config_parse_can_time_quanta); +CONFIG_PARSER_PROTOTYPE(config_parse_can_restart_usec); +CONFIG_PARSER_PROTOTYPE(config_parse_can_control_mode); +CONFIG_PARSER_PROTOTYPE(config_parse_can_termination); diff --git a/src/network/networkd-conf.c b/src/network/networkd-conf.c new file mode 100644 index 0000000..063732a --- /dev/null +++ b/src/network/networkd-conf.c @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Vinay Kulkarni <kulkarniv@vmware.com> + ***/ + +#include "conf-parser.h" +#include "constants.h" +#include "networkd-conf.h" +#include "networkd-manager.h" +#include "networkd-speed-meter.h" + +int manager_parse_config_file(Manager *m) { + int r; + + assert(m); + + r = config_parse_config_file("networkd.conf", + "Network\0" + "DHCPv4\0" + "DHCPv6\0" + "DHCP\0", + config_item_perf_lookup, networkd_gperf_lookup, + CONFIG_PARSE_WARN, + m); + if (r < 0) + return r; + + if (m->use_speed_meter && m->speed_meter_interval_usec < SPEED_METER_MINIMUM_TIME_INTERVAL) { + log_warning("SpeedMeterIntervalSec= is too small, using %s.", + FORMAT_TIMESPAN(SPEED_METER_MINIMUM_TIME_INTERVAL, USEC_PER_SEC)); + m->speed_meter_interval_usec = SPEED_METER_MINIMUM_TIME_INTERVAL; + } + + return 0; +} diff --git a/src/network/networkd-conf.h b/src/network/networkd-conf.h new file mode 100644 index 0000000..6f8612a --- /dev/null +++ b/src/network/networkd-conf.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014 Vinay Kulkarni <kulkarniv@vmware.com> +***/ + +#include "conf-parser.h" + +typedef struct Manager Manager; + +int manager_parse_config_file(Manager *m); + +const struct ConfigPerfItem* networkd_gperf_lookup(const char *key, GPERF_LEN_TYPE length); diff --git a/src/network/networkd-dhcp-common.c b/src/network/networkd-dhcp-common.c new file mode 100644 index 0000000..080b153 --- /dev/null +++ b/src/network/networkd-dhcp-common.c @@ -0,0 +1,1489 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/if_arp.h> + +#include "bus-error.h" +#include "bus-locator.h" +#include "dhcp-identifier.h" +#include "dhcp-option.h" +#include "dhcp6-internal.h" +#include "escape.h" +#include "hexdecoct.h" +#include "in-addr-prefix-util.h" +#include "networkd-dhcp-common.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-route-util.h" +#include "parse-util.h" +#include "socket-util.h" +#include "string-table.h" +#include "strv.h" +#include "vrf.h" + +static uint32_t link_get_vrf_table(Link *link) { + assert(link); + assert(link->network); + + return link->network->vrf ? VRF(link->network->vrf)->table : RT_TABLE_MAIN; +} + +uint32_t link_get_dhcp4_route_table(Link *link) { + assert(link); + assert(link->network); + + /* When the interface is part of an VRF use the VRFs routing table, unless + * another table is explicitly specified. */ + + if (link->network->dhcp_route_table_set) + return link->network->dhcp_route_table; + return link_get_vrf_table(link); +} + +uint32_t link_get_ipv6_accept_ra_route_table(Link *link) { + assert(link); + assert(link->network); + + if (link->network->ipv6_accept_ra_route_table_set) + return link->network->ipv6_accept_ra_route_table; + return link_get_vrf_table(link); +} + +bool link_dhcp_enabled(Link *link, int family) { + assert(link); + assert(IN_SET(family, AF_INET, AF_INET6)); + + /* Currently, sd-dhcp-client supports only ethernet and infiniband. */ + if (family == AF_INET && !IN_SET(link->iftype, ARPHRD_ETHER, ARPHRD_INFINIBAND)) + return false; + + if (family == AF_INET6 && !socket_ipv6_is_supported()) + return false; + + if (link->flags & IFF_LOOPBACK) + return false; + + if (link->iftype == ARPHRD_CAN) + return false; + + if (!link->network) + return false; + + return link->network->dhcp & (family == AF_INET ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_IPV6); +} + +void network_adjust_dhcp(Network *network) { + assert(network); + assert(network->dhcp >= 0); + + if (network->dhcp == ADDRESS_FAMILY_NO) + return; + + /* Bonding slave does not support addressing. */ + if (network->bond) { + log_warning("%s: Cannot enable DHCP= when Bond= is specified, disabling DHCP=.", + network->filename); + network->dhcp = ADDRESS_FAMILY_NO; + return; + } + + if (!FLAGS_SET(network->link_local, ADDRESS_FAMILY_IPV6) && + FLAGS_SET(network->dhcp, ADDRESS_FAMILY_IPV6)) { + log_warning("%s: DHCPv6 client is enabled but IPv6 link-local addressing is disabled. " + "Disabling DHCPv6 client.", network->filename); + SET_FLAG(network->dhcp, ADDRESS_FAMILY_IPV6, false); + } + + network_adjust_dhcp4(network); +} + +static bool duid_needs_product_uuid(const DUID *duid) { + assert(duid); + + return duid->type == DUID_TYPE_UUID && duid->raw_data_len == 0; +} + +static const struct DUID fallback_duid = { .type = DUID_TYPE_EN }; + +const DUID *link_get_duid(Link *link, int family) { + const DUID *duid; + + assert(link); + assert(IN_SET(family, AF_INET, AF_INET6)); + + if (link->network) { + duid = family == AF_INET ? &link->network->dhcp_duid : &link->network->dhcp6_duid; + if (duid->type != _DUID_TYPE_INVALID) { + if (duid_needs_product_uuid(duid)) + return &link->manager->duid_product_uuid; + else + return duid; + } + } + + duid = family == AF_INET ? &link->manager->dhcp_duid : &link->manager->dhcp6_duid; + if (link->hw_addr.length == 0 && IN_SET(duid->type, DUID_TYPE_LLT, DUID_TYPE_LL)) + /* Fallback to DUID that works without MAC address. + * This is useful for tunnel devices without MAC address. */ + return &fallback_duid; + + return duid; +} + +static int get_product_uuid_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + Manager *manager = ASSERT_PTR(userdata); + const sd_bus_error *e; + const void *a; + size_t sz; + int r; + + assert(m); + + /* To avoid calling GetProductUUID() bus method so frequently, set the flag below + * even if the method fails. */ + manager->has_product_uuid = true; + + e = sd_bus_message_get_error(m); + if (e) { + r = sd_bus_error_get_errno(e); + log_warning_errno(r, "Could not get product UUID. Falling back to use machine-app-specific ID as DUID-UUID: %s", + bus_error_message(e, r)); + return 0; + } + + r = sd_bus_message_read_array(m, 'y', &a, &sz); + if (r < 0) { + log_warning_errno(r, "Failed to get product UUID. Falling back to use machine-app-specific ID as DUID-UUID: %m"); + return 0; + } + + if (sz != sizeof(sd_id128_t)) { + log_warning("Invalid product UUID. Falling back to use machine-app-specific ID as DUID-UUID."); + return 0; + } + + log_debug("Successfully obtained product UUID"); + + memcpy(&manager->duid_product_uuid.raw_data, a, sz); + manager->duid_product_uuid.raw_data_len = sz; + + return 0; +} + +int manager_request_product_uuid(Manager *m) { + static bool bus_method_is_called = false; + int r; + + assert(m); + + if (bus_method_is_called) + return 0; + + if (sd_bus_is_ready(m->bus) <= 0 && !m->product_uuid_requested) { + log_debug("Not connected to system bus, requesting product UUID later."); + m->product_uuid_requested = true; + return 0; + } + + m->product_uuid_requested = false; + + r = bus_call_method_async( + m->bus, + NULL, + bus_hostname, + "GetProductUUID", + get_product_uuid_handler, + m, + "b", + false); + if (r < 0) + return log_warning_errno(r, "Failed to get product UUID: %m"); + + log_debug("Requesting product UUID."); + + bus_method_is_called = true; + + return 0; +} + +int dhcp_configure_duid(Link *link, const DUID *duid) { + Manager *m; + int r; + + assert(link); + assert(link->manager); + assert(duid); + + m = link->manager; + + if (!duid_needs_product_uuid(duid)) + return 1; + + if (m->has_product_uuid) + return 1; + + r = manager_request_product_uuid(m); + if (r < 0) { + log_link_warning_errno(link, r, + "Failed to get product UUID. Falling back to use machine-app-specific ID as DUID-UUID: %m"); + + m->has_product_uuid = true; /* Do not request UUID again on failure. */ + return 1; + } + + return 0; +} + +bool address_is_filtered(int family, const union in_addr_union *address, uint8_t prefixlen, Set *allow_list, Set *deny_list) { + struct in_addr_prefix *p; + + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(address); + + if (allow_list) { + SET_FOREACH(p, allow_list) + if (p->family == family && + p->prefixlen <= prefixlen && + in_addr_prefix_covers(family, &p->address, p->prefixlen, address) > 0) + return false; + + return true; + } + + SET_FOREACH(p, deny_list) + if (p->family == family && + in_addr_prefix_intersect(family, &p->address, p->prefixlen, address, prefixlen) > 0) + return true; + + return false; +} + +int link_get_captive_portal(Link *link, const char **ret) { + const char *dhcp4_cp = NULL, *dhcp6_cp = NULL, *ndisc_cp = NULL; + int r; + + assert(link); + + if (!link->network) { + *ret = NULL; + return 0; + } + + if (link->network->dhcp_use_captive_portal && link->dhcp_lease) { + r = sd_dhcp_lease_get_captive_portal(link->dhcp_lease, &dhcp4_cp); + if (r < 0 && r != -ENODATA) + return r; + } + + if (link->network->dhcp6_use_captive_portal && link->dhcp6_lease) { + r = sd_dhcp6_lease_get_captive_portal(link->dhcp6_lease, &dhcp6_cp); + if (r < 0 && r != -ENODATA) + return r; + } + + if (link->network->ipv6_accept_ra_use_captive_portal) { + NDiscCaptivePortal *cp; + usec_t usec = 0; + + /* Use the captive portal with the longest lifetime. */ + + SET_FOREACH(cp, link->ndisc_captive_portals) { + if (cp->lifetime_usec < usec) + continue; + + ndisc_cp = cp->captive_portal; + usec = cp->lifetime_usec; + } + + if (set_size(link->ndisc_captive_portals) > 1) + log_link_debug(link, "Multiple captive portals obtained by IPv6RA, using \"%s\" and ignoring others.", + ndisc_cp); + } + + if (dhcp4_cp) { + if (dhcp6_cp && !streq(dhcp4_cp, dhcp6_cp)) + log_link_debug(link, "DHCPv6 captive portal (%s) does not match DHCPv4 (%s), ignoring DHCPv6 captive portal.", + dhcp6_cp, dhcp4_cp); + + if (ndisc_cp && !streq(dhcp4_cp, ndisc_cp)) + log_link_debug(link, "IPv6RA captive portal (%s) does not match DHCPv4 (%s), ignoring IPv6RA captive portal.", + ndisc_cp, dhcp4_cp); + + *ret = dhcp4_cp; + return 1; + } + + if (dhcp6_cp) { + if (ndisc_cp && !streq(dhcp6_cp, ndisc_cp)) + log_link_debug(link, "IPv6RA captive portal (%s) does not match DHCPv6 (%s), ignoring IPv6RA captive portal.", + ndisc_cp, dhcp6_cp); + + *ret = dhcp6_cp; + return 1; + } + + *ret = ndisc_cp; + return !!ndisc_cp; +} + +int config_parse_dhcp( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + AddressFamily *dhcp = data, s; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + /* Note that this is mostly like + * config_parse_address_family(), except that it + * understands some old names for the enum values */ + + s = address_family_from_string(rvalue); + if (s < 0) { + + /* Previously, we had a slightly different enum here, + * support its values for compatibility. */ + + s = dhcp_deprecated_address_family_from_string(rvalue); + if (s < 0) { + log_syntax(unit, LOG_WARNING, filename, line, s, + "Failed to parse DHCP option, ignoring: %s", rvalue); + return 0; + } + + log_syntax(unit, LOG_WARNING, filename, line, 0, + "DHCP=%s is deprecated, please use DHCP=%s instead.", + rvalue, address_family_to_string(s)); + } + + *dhcp = s; + return 0; +} + +int config_parse_dhcp_route_metric( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + uint32_t metric; + int r; + + assert(filename); + assert(lvalue); + assert(IN_SET(ltype, AF_UNSPEC, AF_INET)); + assert(rvalue); + assert(data); + + r = safe_atou32(rvalue, &metric); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse RouteMetric=%s, ignoring assignment: %m", rvalue); + return 0; + } + + switch (ltype) { + case AF_INET: + network->dhcp_route_metric = metric; + network->dhcp_route_metric_set = true; + break; + case AF_UNSPEC: + /* For backward compatibility. */ + if (!network->dhcp_route_metric_set) + network->dhcp_route_metric = metric; + if (!network->ipv6_accept_ra_route_metric_set) { + network->ipv6_accept_ra_route_metric_high = metric; + network->ipv6_accept_ra_route_metric_medium = metric; + network->ipv6_accept_ra_route_metric_low = metric; + } + break; + default: + assert_not_reached(); + } + + return 0; +} + +int config_parse_ipv6_accept_ra_route_metric( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + uint32_t metric_high, metric_medium, metric_low; + int r, s, t; + + assert(filename); + assert(rvalue); + + if (safe_atou32(rvalue, &metric_low) >= 0) + metric_high = metric_medium = metric_low; + else { + _cleanup_free_ char *high = NULL, *medium = NULL, *low = NULL; + const char *p = rvalue; + + r = extract_many_words(&p, ":", EXTRACT_DONT_COALESCE_SEPARATORS, &high, &medium, &low, NULL); + if (r == -ENOMEM) + return log_oom(); + if (r != 3 || !isempty(p)) { + log_syntax(unit, LOG_WARNING, filename, line, r < 0 ? r : 0, + "Failed to parse RouteTable=%s, ignoring assignment: %m", rvalue); + return 0; + } + + r = safe_atou32(high, &metric_high); + s = safe_atou32(medium, &metric_medium); + t = safe_atou32(low, &metric_low); + if (r < 0 || s < 0 || t < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r < 0 ? r : s < 0 ? s : t, + "Failed to parse RouteTable=%s, ignoring assignment: %m", rvalue); + return 0; + } + + if (metric_high >= metric_medium || metric_medium >= metric_low) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid RouteTable=%s, ignoring assignment: %m", rvalue); + return 0; + } + } + + network->ipv6_accept_ra_route_metric_high = metric_high; + network->ipv6_accept_ra_route_metric_medium = metric_medium; + network->ipv6_accept_ra_route_metric_low = metric_low; + network->ipv6_accept_ra_route_metric_set = true; + + return 0; +} + +int config_parse_dhcp_send_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) { + + Network *network = userdata; + int r; + + assert(filename); + assert(lvalue); + assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6)); + assert(rvalue); + assert(data); + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse SendHostname=%s, ignoring assignment: %m", rvalue); + return 0; + } + + switch (ltype) { + case AF_INET: + network->dhcp_send_hostname = r; + network->dhcp_send_hostname_set = true; + break; + case AF_INET6: + network->dhcp6_send_hostname = r; + network->dhcp6_send_hostname_set = true; + break; + case AF_UNSPEC: + /* For backward compatibility. */ + if (!network->dhcp_send_hostname_set) + network->dhcp_send_hostname = r; + if (!network->dhcp6_send_hostname_set) + network->dhcp6_send_hostname = r; + break; + default: + assert_not_reached(); + } + + return 0; +} +int config_parse_dhcp_use_dns( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + int r; + + assert(filename); + assert(lvalue); + assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6)); + assert(rvalue); + assert(data); + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse UseDNS=%s, ignoring assignment: %m", rvalue); + return 0; + } + + switch (ltype) { + case AF_INET: + network->dhcp_use_dns = r; + network->dhcp_use_dns_set = true; + break; + case AF_INET6: + network->dhcp6_use_dns = r; + network->dhcp6_use_dns_set = true; + break; + case AF_UNSPEC: + /* For backward compatibility. */ + if (!network->dhcp_use_dns_set) + network->dhcp_use_dns = r; + if (!network->dhcp6_use_dns_set) + network->dhcp6_use_dns = r; + break; + default: + assert_not_reached(); + } + + return 0; +} + +int config_parse_dhcp_use_domains( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + DHCPUseDomains d; + + assert(filename); + assert(lvalue); + assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6)); + assert(rvalue); + assert(data); + + d = dhcp_use_domains_from_string(rvalue); + if (d < 0) { + log_syntax(unit, LOG_WARNING, filename, line, d, + "Failed to parse %s=%s, ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + switch (ltype) { + case AF_INET: + network->dhcp_use_domains = d; + network->dhcp_use_domains_set = true; + break; + case AF_INET6: + network->dhcp6_use_domains = d; + network->dhcp6_use_domains_set = true; + break; + case AF_UNSPEC: + /* For backward compatibility. */ + if (!network->dhcp_use_domains_set) + network->dhcp_use_domains = d; + if (!network->dhcp6_use_domains_set) + network->dhcp6_use_domains = d; + break; + default: + assert_not_reached(); + } + + return 0; +} + +int config_parse_dhcp_use_ntp( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + int r; + + assert(filename); + assert(lvalue); + assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6)); + assert(rvalue); + assert(data); + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse UseNTP=%s, ignoring assignment: %m", rvalue); + return 0; + } + + switch (ltype) { + case AF_INET: + network->dhcp_use_ntp = r; + network->dhcp_use_ntp_set = true; + break; + case AF_INET6: + network->dhcp6_use_ntp = r; + network->dhcp6_use_ntp_set = true; + break; + case AF_UNSPEC: + /* For backward compatibility. */ + if (!network->dhcp_use_ntp_set) + network->dhcp_use_ntp = r; + if (!network->dhcp6_use_ntp_set) + network->dhcp6_use_ntp = r; + break; + default: + assert_not_reached(); + } + + return 0; +} + +int config_parse_dhcp_or_ra_route_table( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + uint32_t rt; + int r; + + assert(filename); + assert(lvalue); + assert(IN_SET(ltype, AF_INET, AF_INET6)); + assert(rvalue); + + r = manager_get_route_table_from_string(network->manager, rvalue, &rt); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse RouteTable=%s, ignoring assignment: %m", rvalue); + return 0; + } + + switch (ltype) { + case AF_INET: + network->dhcp_route_table = rt; + network->dhcp_route_table_set = true; + break; + case AF_INET6: + network->ipv6_accept_ra_route_table = rt; + network->ipv6_accept_ra_route_table_set = true; + break; + default: + assert_not_reached(); + } + + return 0; +} + +int config_parse_iaid( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + uint32_t iaid; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(IN_SET(ltype, AF_INET, AF_INET6)); + + r = safe_atou32(rvalue, &iaid); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Unable to read IAID, ignoring assignment: %s", rvalue); + return 0; + } + + if (ltype == AF_INET) { + network->dhcp_iaid = iaid; + network->dhcp_iaid_set = true; + if (!network->dhcp6_iaid_set_explicitly) { + /* Backward compatibility. Previously, IAID is shared by DHCPv4 and DHCPv6. + * If DHCPv6 IAID is not specified explicitly, then use DHCPv4 IAID for DHCPv6. */ + network->dhcp6_iaid = iaid; + network->dhcp6_iaid_set = true; + } + } else { + assert(ltype == AF_INET6); + network->dhcp6_iaid = iaid; + network->dhcp6_iaid_set = true; + network->dhcp6_iaid_set_explicitly = true; + } + + return 0; +} + +int config_parse_dhcp_user_or_vendor_class( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + char ***l = ASSERT_PTR(data); + int r; + + assert(lvalue); + assert(rvalue); + assert(IN_SET(ltype, AF_INET, AF_INET6)); + + if (isempty(rvalue)) { + *l = strv_free(*l); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *w = NULL; + size_t len; + + r = extract_first_word(&p, &w, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to split user classes option, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + len = strlen(w); + if (ltype == AF_INET) { + if (len > UINT8_MAX || len == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "%s length is not in the range 1…255, ignoring.", w); + continue; + } + } else { + if (len > UINT16_MAX || len == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "%s length is not in the range 1…65535, ignoring.", w); + continue; + } + } + + r = strv_consume(l, TAKE_PTR(w)); + if (r < 0) + return log_oom(); + } +} + +int config_parse_dhcp_send_option( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(sd_dhcp_option_unrefp) sd_dhcp_option *opt4 = NULL; + _cleanup_(sd_dhcp6_option_unrefp) sd_dhcp6_option *opt6 = NULL; + _unused_ _cleanup_(sd_dhcp_option_unrefp) sd_dhcp_option *old4 = NULL; + _unused_ _cleanup_(sd_dhcp6_option_unrefp) sd_dhcp6_option *old6 = NULL; + uint32_t uint32_data, enterprise_identifier = 0; + _cleanup_free_ char *word = NULL, *q = NULL; + OrderedHashmap **options = ASSERT_PTR(data); + uint16_t u16, uint16_data; + union in_addr_union addr; + DHCPOptionDataType type; + uint8_t u8, uint8_data; + const void *udata; + const char *p; + ssize_t sz; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *options = ordered_hashmap_free(*options); + return 0; + } + + p = rvalue; + if (ltype == AF_INET6 && streq(lvalue, "SendVendorOption")) { + r = extract_first_word(&p, &word, ":", 0); + if (r == -ENOMEM) + return log_oom(); + if (r <= 0 || isempty(p)) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid DHCP option, ignoring assignment: %s", rvalue); + return 0; + } + + r = safe_atou32(word, &enterprise_identifier); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DHCPv6 enterprise identifier data, ignoring assignment: %s", p); + return 0; + } + word = mfree(word); + } + + r = extract_first_word(&p, &word, ":", 0); + if (r == -ENOMEM) + return log_oom(); + if (r <= 0 || isempty(p)) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid DHCP option, ignoring assignment: %s", rvalue); + return 0; + } + + if (ltype == AF_INET6) { + r = safe_atou16(word, &u16); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid DHCP option, ignoring assignment: %s", rvalue); + return 0; + } + if (u16 < 1 || u16 >= UINT16_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid DHCP option, valid range is 1-65535, ignoring assignment: %s", rvalue); + return 0; + } + } else { + r = safe_atou8(word, &u8); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid DHCP option, ignoring assignment: %s", rvalue); + return 0; + } + if (u8 < 1 || u8 >= UINT8_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid DHCP option, valid range is 1-254, ignoring assignment: %s", rvalue); + return 0; + } + } + + word = mfree(word); + r = extract_first_word(&p, &word, ":", 0); + if (r == -ENOMEM) + return log_oom(); + if (r <= 0 || isempty(p)) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid DHCP option, ignoring assignment: %s", rvalue); + return 0; + } + + type = dhcp_option_data_type_from_string(word); + if (type < 0) { + log_syntax(unit, LOG_WARNING, filename, line, type, + "Invalid DHCP option data type, ignoring assignment: %s", p); + return 0; + } + + switch (type) { + case DHCP_OPTION_DATA_UINT8:{ + r = safe_atou8(p, &uint8_data); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DHCP uint8 data, ignoring assignment: %s", p); + return 0; + } + + udata = &uint8_data; + sz = sizeof(uint8_t); + break; + } + case DHCP_OPTION_DATA_UINT16:{ + uint16_t k; + + r = safe_atou16(p, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DHCP uint16 data, ignoring assignment: %s", p); + return 0; + } + + uint16_data = htobe16(k); + udata = &uint16_data; + sz = sizeof(uint16_t); + break; + } + case DHCP_OPTION_DATA_UINT32: { + uint32_t k; + + r = safe_atou32(p, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DHCP uint32 data, ignoring assignment: %s", p); + return 0; + } + + uint32_data = htobe32(k); + udata = &uint32_data; + sz = sizeof(uint32_t); + + break; + } + case DHCP_OPTION_DATA_IPV4ADDRESS: { + r = in_addr_from_string(AF_INET, p, &addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DHCP ipv4address data, ignoring assignment: %s", p); + return 0; + } + + udata = &addr.in; + sz = sizeof(addr.in.s_addr); + break; + } + case DHCP_OPTION_DATA_IPV6ADDRESS: { + r = in_addr_from_string(AF_INET6, p, &addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DHCP ipv6address data, ignoring assignment: %s", p); + return 0; + } + + udata = &addr.in6; + sz = sizeof(addr.in6.s6_addr); + break; + } + case DHCP_OPTION_DATA_STRING: + sz = cunescape(p, UNESCAPE_ACCEPT_NUL, &q); + if (sz < 0) { + log_syntax(unit, LOG_WARNING, filename, line, sz, + "Failed to decode DHCP option data, ignoring assignment: %s", p); + return 0; + } + + udata = q; + break; + default: + return -EINVAL; + } + + if (ltype == AF_INET6) { + r = sd_dhcp6_option_new(u16, udata, sz, enterprise_identifier, &opt6); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store DHCP option '%s', ignoring assignment: %m", rvalue); + return 0; + } + + r = ordered_hashmap_ensure_allocated(options, &dhcp6_option_hash_ops); + if (r < 0) + return log_oom(); + + /* Overwrite existing option */ + old6 = ordered_hashmap_get(*options, UINT_TO_PTR(u16)); + r = ordered_hashmap_replace(*options, UINT_TO_PTR(u16), opt6); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store DHCP option '%s', ignoring assignment: %m", rvalue); + return 0; + } + TAKE_PTR(opt6); + } else { + r = sd_dhcp_option_new(u8, udata, sz, &opt4); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store DHCP option '%s', ignoring assignment: %m", rvalue); + return 0; + } + + r = ordered_hashmap_ensure_allocated(options, &dhcp_option_hash_ops); + if (r < 0) + return log_oom(); + + /* Overwrite existing option */ + old4 = ordered_hashmap_get(*options, UINT_TO_PTR(u8)); + r = ordered_hashmap_replace(*options, UINT_TO_PTR(u8), opt4); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store DHCP option '%s', ignoring assignment: %m", rvalue); + return 0; + } + TAKE_PTR(opt4); + } + return 0; +} + +int config_parse_dhcp_request_options( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + if (ltype == AF_INET) + network->dhcp_request_options = set_free(network->dhcp_request_options); + else + network->dhcp6_request_options = set_free(network->dhcp6_request_options); + + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *n = NULL; + uint32_t i; + + r = extract_first_word(&p, &n, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DHCP request option, ignoring assignment: %s", + rvalue); + return 0; + } + if (r == 0) + return 0; + + r = safe_atou32(n, &i); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "DHCP request option is invalid, ignoring assignment: %s", n); + continue; + } + + if (i < 1 || i >= UINT8_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "DHCP request option is invalid, valid range is 1-254, ignoring assignment: %s", n); + continue; + } + + r = set_ensure_put(ltype == AF_INET ? &network->dhcp_request_options : &network->dhcp6_request_options, + NULL, UINT32_TO_PTR(i)); + if (r < 0) + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store DHCP request option '%s', ignoring assignment: %m", n); + } +} + +static const char* const dhcp_use_domains_table[_DHCP_USE_DOMAINS_MAX] = { + [DHCP_USE_DOMAINS_NO] = "no", + [DHCP_USE_DOMAINS_ROUTE] = "route", + [DHCP_USE_DOMAINS_YES] = "yes", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(dhcp_use_domains, DHCPUseDomains, DHCP_USE_DOMAINS_YES); + +static const char * const dhcp_option_data_type_table[_DHCP_OPTION_DATA_MAX] = { + [DHCP_OPTION_DATA_UINT8] = "uint8", + [DHCP_OPTION_DATA_UINT16] = "uint16", + [DHCP_OPTION_DATA_UINT32] = "uint32", + [DHCP_OPTION_DATA_STRING] = "string", + [DHCP_OPTION_DATA_IPV4ADDRESS] = "ipv4address", + [DHCP_OPTION_DATA_IPV6ADDRESS] = "ipv6address", +}; + +DEFINE_STRING_TABLE_LOOKUP(dhcp_option_data_type, DHCPOptionDataType); + +static const char* const duid_type_table[_DUID_TYPE_MAX] = { + [DUID_TYPE_LLT] = "link-layer-time", + [DUID_TYPE_EN] = "vendor", + [DUID_TYPE_LL] = "link-layer", + [DUID_TYPE_UUID] = "uuid", +}; +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(duid_type, DUIDType); + +int config_parse_duid_type( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *type_string = NULL; + const char *p = ASSERT_PTR(rvalue); + bool force = ltype; + DUID *duid = ASSERT_PTR(data); + DUIDType type; + int r; + + assert(filename); + assert(lvalue); + + if (!force && duid->set) + return 0; + + r = extract_first_word(&p, &type_string, ":", 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid syntax, ignoring: %s", rvalue); + return 0; + } + if (r == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Failed to extract DUID type from '%s', ignoring.", rvalue); + return 0; + } + + type = duid_type_from_string(type_string); + if (type < 0) { + uint16_t t; + + r = safe_atou16(type_string, &t); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DUID type '%s', ignoring.", type_string); + return 0; + } + + type = t; + assert(type == t); /* Check if type can store uint16_t. */ + } + + if (!isempty(p)) { + usec_t u; + + if (type != DUID_TYPE_LLT) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid syntax, ignoring: %s", rvalue); + return 0; + } + + r = parse_timestamp(p, &u); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse timestamp, ignoring: %s", p); + return 0; + } + + duid->llt_time = u; + } + + duid->type = type; + duid->set = force; + + return 0; +} + +int config_parse_manager_duid_type( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Manager *manager = ASSERT_PTR(userdata); + int r; + + /* For backward compatibility. Setting both DHCPv4 and DHCPv6 DUID if they are not specified explicitly. */ + + r = config_parse_duid_type(unit, filename, line, section, section_line, lvalue, false, rvalue, &manager->dhcp_duid, manager); + if (r < 0) + return r; + + return config_parse_duid_type(unit, filename, line, section, section_line, lvalue, false, rvalue, &manager->dhcp6_duid, manager); +} + +int config_parse_network_duid_type( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + int r; + + r = config_parse_duid_type(unit, filename, line, section, section_line, lvalue, true, rvalue, &network->dhcp_duid, network); + if (r < 0) + return r; + + /* For backward compatibility, also set DHCPv6 DUID if not specified explicitly. */ + return config_parse_duid_type(unit, filename, line, section, section_line, lvalue, false, rvalue, &network->dhcp6_duid, network); +} + +int config_parse_duid_rawdata( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint8_t raw_data[MAX_DUID_DATA_LEN]; + unsigned count = 0; + bool force = ltype; + DUID *duid = ASSERT_PTR(data); + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (!force && duid->set) + return 0; + + /* RawData contains DUID in format "NN:NN:NN..." */ + for (const char *p = rvalue;;) { + int n1, n2, len, r; + uint32_t byte; + _cleanup_free_ char *cbyte = NULL; + + r = extract_first_word(&p, &cbyte, ":", 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to read DUID, ignoring assignment: %s.", rvalue); + return 0; + } + if (r == 0) + break; + + if (count >= MAX_DUID_DATA_LEN) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Max DUID length exceeded, ignoring assignment: %s.", rvalue); + return 0; + } + + len = strlen(cbyte); + if (!IN_SET(len, 1, 2)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid length - DUID byte: %s, ignoring assignment: %s.", cbyte, rvalue); + return 0; + } + n1 = unhexchar(cbyte[0]); + if (len == 2) + n2 = unhexchar(cbyte[1]); + else + n2 = 0; + + if (n1 < 0 || n2 < 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid DUID byte: %s. Ignoring assignment: %s.", cbyte, rvalue); + return 0; + } + + byte = ((uint8_t) n1 << (4 * (len-1))) | (uint8_t) n2; + raw_data[count++] = byte; + } + + assert_cc(sizeof(raw_data) == sizeof(duid->raw_data)); + memcpy(duid->raw_data, raw_data, count); + duid->raw_data_len = count; + duid->set = force; + + return 0; +} + +int config_parse_manager_duid_rawdata( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Manager *manager = ASSERT_PTR(userdata); + int r; + + /* For backward compatibility. Setting both DHCPv4 and DHCPv6 DUID if they are not specified explicitly. */ + + r = config_parse_duid_rawdata(unit, filename, line, section, section_line, lvalue, false, rvalue, &manager->dhcp_duid, manager); + if (r < 0) + return r; + + return config_parse_duid_rawdata(unit, filename, line, section, section_line, lvalue, false, rvalue, &manager->dhcp6_duid, manager); +} + +int config_parse_network_duid_rawdata( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + int r; + + r = config_parse_duid_rawdata(unit, filename, line, section, section_line, lvalue, true, rvalue, &network->dhcp_duid, network); + if (r < 0) + return r; + + /* For backward compatibility, also set DHCPv6 DUID if not specified explicitly. */ + return config_parse_duid_rawdata(unit, filename, line, section, section_line, lvalue, false, rvalue, &network->dhcp6_duid, network); +} + +int config_parse_uplink( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + bool accept_none = true; + int *index, r; + char **name; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + if (streq(section, "DHCPServer")) { + index = &network->dhcp_server_uplink_index; + name = &network->dhcp_server_uplink_name; + } else if (streq(section, "IPv6SendRA")) { + index = &network->router_uplink_index; + name = &network->router_uplink_name; + } else if (STR_IN_SET(section, "DHCPv6PrefixDelegation", "DHCPPrefixDelegation")) { + index = &network->dhcp_pd_uplink_index; + name = &network->dhcp_pd_uplink_name; + accept_none = false; + } else + assert_not_reached(); + + if (isempty(rvalue) || streq(rvalue, ":auto")) { + *index = UPLINK_INDEX_AUTO; + *name = mfree(*name); + return 0; + } + + if (accept_none && streq(rvalue, ":none")) { + *index = UPLINK_INDEX_NONE; + *name = mfree(*name); + return 0; + } + + if (!accept_none && streq(rvalue, ":self")) { + *index = UPLINK_INDEX_SELF; + *name = mfree(*name); + return 0; + } + + r = parse_ifindex(rvalue); + if (r > 0) { + *index = r; + *name = mfree(*name); + return 0; + } + + if (!ifname_valid_full(rvalue, IFNAME_VALID_ALTERNATIVE)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid interface name in %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + /* The interface name will be resolved later. */ + r = free_and_strdup_warn(name, rvalue); + if (r < 0) + return r; + + /* Note, if uplink_name is set, then uplink_index will be ignored. So, the below does not mean + * an uplink interface will be selected automatically. */ + *index = UPLINK_INDEX_AUTO; + return 0; +} diff --git a/src/network/networkd-dhcp-common.h b/src/network/networkd-dhcp-common.h new file mode 100644 index 0000000..6e3f3b2 --- /dev/null +++ b/src/network/networkd-dhcp-common.h @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <netinet/in.h> + +#include "conf-parser.h" +#include "dhcp-identifier.h" +#include "in-addr-util.h" +#include "set.h" +#include "time-util.h" + +/* Special values for *_uplink_index. */ +#define UPLINK_INDEX_AUTO 0 /* uplink will be selected automatically */ +#define UPLINK_INDEX_NONE -1 /* uplink will not be selected automatically */ +#define UPLINK_INDEX_SELF -2 /* the interface itself is uplink */ + +#define DHCP_ROUTE_METRIC 1024 +#define IPV6RA_ROUTE_METRIC_HIGH 512 +#define IPV6RA_ROUTE_METRIC_MEDIUM 1024 +#define IPV6RA_ROUTE_METRIC_LOW 2048 +#define DHCP6PD_ROUTE_METRIC 256 + +typedef struct Link Link; +typedef struct Manager Manager; +typedef struct Network Network; + +typedef enum DHCPUseDomains { + DHCP_USE_DOMAINS_NO, + DHCP_USE_DOMAINS_YES, + DHCP_USE_DOMAINS_ROUTE, + _DHCP_USE_DOMAINS_MAX, + _DHCP_USE_DOMAINS_INVALID = -EINVAL, +} DHCPUseDomains; + +typedef enum DHCPOptionDataType { + DHCP_OPTION_DATA_UINT8, + DHCP_OPTION_DATA_UINT16, + DHCP_OPTION_DATA_UINT32, + DHCP_OPTION_DATA_STRING, + DHCP_OPTION_DATA_IPV4ADDRESS, + DHCP_OPTION_DATA_IPV6ADDRESS, + _DHCP_OPTION_DATA_MAX, + _DHCP_OPTION_DATA_INVALID, +} DHCPOptionDataType; + +typedef struct DUID { + /* Value of Type in [DHCP] section */ + DUIDType type; + + uint8_t raw_data_len; + uint8_t raw_data[MAX_DUID_DATA_LEN]; + usec_t llt_time; + bool set; +} DUID; + +uint32_t link_get_dhcp4_route_table(Link *link); +uint32_t link_get_ipv6_accept_ra_route_table(Link *link); + +bool link_dhcp_enabled(Link *link, int family); +static inline bool link_dhcp4_enabled(Link *link) { + return link_dhcp_enabled(link, AF_INET); +} +static inline bool link_dhcp6_enabled(Link *link) { + return link_dhcp_enabled(link, AF_INET6); +} + +void network_adjust_dhcp(Network *network); + +const DUID *link_get_duid(Link *link, int family); +static inline const DUID *link_get_dhcp4_duid(Link *link) { + return link_get_duid(link, AF_INET); +} +static inline const DUID *link_get_dhcp6_duid(Link *link) { + return link_get_duid(link, AF_INET6); +} + +int dhcp_configure_duid(Link *link, const DUID *duid); +int manager_request_product_uuid(Manager *m); + +bool address_is_filtered(int family, const union in_addr_union *address, uint8_t prefixlen, Set *allow_list, Set *deny_list); +static inline bool in4_address_is_filtered(const struct in_addr *address, Set *allow_list, Set *deny_list) { + return address_is_filtered(AF_INET, &(union in_addr_union) { .in = *address }, 32, allow_list, deny_list); +} +static inline bool in6_prefix_is_filtered(const struct in6_addr *prefix, uint8_t prefixlen, Set *allow_list, Set *deny_list) { + return address_is_filtered(AF_INET6, &(union in_addr_union) { .in6 = *prefix }, prefixlen, allow_list, deny_list); +} + +int link_get_captive_portal(Link *link, const char **ret); + +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_ipv6_accept_ra_route_metric); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_send_hostname); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_dns); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_domains); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_ntp); +CONFIG_PARSER_PROTOTYPE(config_parse_iaid); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_or_ra_route_table); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_user_or_vendor_class); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_send_option); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_request_options); +CONFIG_PARSER_PROTOTYPE(config_parse_duid_type); +CONFIG_PARSER_PROTOTYPE(config_parse_manager_duid_type); +CONFIG_PARSER_PROTOTYPE(config_parse_network_duid_type); +CONFIG_PARSER_PROTOTYPE(config_parse_duid_rawdata); +CONFIG_PARSER_PROTOTYPE(config_parse_manager_duid_rawdata); +CONFIG_PARSER_PROTOTYPE(config_parse_network_duid_rawdata); +CONFIG_PARSER_PROTOTYPE(config_parse_uplink); diff --git a/src/network/networkd-dhcp-prefix-delegation.c b/src/network/networkd-dhcp-prefix-delegation.c new file mode 100644 index 0000000..af2fe9e --- /dev/null +++ b/src/network/networkd-dhcp-prefix-delegation.c @@ -0,0 +1,1257 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/ipv6_route.h> + +#include "dhcp6-lease-internal.h" +#include "hashmap.h" +#include "in-addr-prefix-util.h" +#include "networkd-address-generation.h" +#include "networkd-address.h" +#include "networkd-dhcp-prefix-delegation.h" +#include "networkd-dhcp6.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-queue.h" +#include "networkd-radv.h" +#include "networkd-route.h" +#include "networkd-setlink.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" +#include "tunnel.h" + +bool link_dhcp_pd_is_enabled(Link *link) { + assert(link); + + if (!link->network) + return false; + + return link->network->dhcp_pd; +} + +bool dhcp_pd_is_uplink(Link *link, Link *target, bool accept_auto) { + assert(link); + assert(target); + + if (!link_dhcp_pd_is_enabled(link)) + return false; + + if (link->network->dhcp_pd_uplink_name) + return streq_ptr(target->ifname, link->network->dhcp_pd_uplink_name) || + strv_contains(target->alternative_names, link->network->dhcp_pd_uplink_name); + + if (link->network->dhcp_pd_uplink_index > 0) + return target->ifindex == link->network->dhcp_pd_uplink_index; + + if (link->network->dhcp_pd_uplink_index == UPLINK_INDEX_SELF) + return link == target; + + assert(link->network->dhcp_pd_uplink_index == UPLINK_INDEX_AUTO); + return accept_auto; +} + +static void link_remove_dhcp_pd_subnet_prefix(Link *link, const struct in6_addr *prefix) { + void *key; + + assert(link); + assert(link->manager); + assert(prefix); + + if (hashmap_get(link->manager->links_by_dhcp_pd_subnet_prefix, prefix) != link) + return; + + hashmap_remove2(link->manager->links_by_dhcp_pd_subnet_prefix, prefix, &key); + free(key); +} + +static int link_add_dhcp_pd_subnet_prefix(Link *link, const struct in6_addr *prefix) { + _cleanup_free_ struct in6_addr *copy = NULL; + int r; + + assert(link); + assert(prefix); + + copy = newdup(struct in6_addr, prefix, 1); + if (!copy) + return -ENOMEM; + + r = hashmap_ensure_put(&link->manager->links_by_dhcp_pd_subnet_prefix, &in6_addr_hash_ops_free, copy, link); + if (r < 0) + return r; + if (r > 0) + TAKE_PTR(copy); + + return 0; +} + +static int link_get_by_dhcp_pd_subnet_prefix(Manager *manager, const struct in6_addr *prefix, Link **ret) { + Link *link; + + assert(manager); + assert(prefix); + + link = hashmap_get(manager->links_by_dhcp_pd_subnet_prefix, prefix); + if (!link) + return -ENODEV; + + if (ret) + *ret = link; + return 0; +} + +static int dhcp_pd_get_assigned_subnet_prefix(Link *link, const struct in6_addr *pd_prefix, uint8_t pd_prefix_len, struct in6_addr *ret) { + assert(link); + assert(pd_prefix); + + if (!link_dhcp_pd_is_enabled(link)) + return -ENOENT; + + if (link->network->dhcp_pd_assign) { + Address *address; + + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_DHCP_PD) + continue; + assert(address->family == AF_INET6); + + if (in6_addr_prefix_covers(pd_prefix, pd_prefix_len, &address->in_addr.in6) <= 0) + continue; + + if (ret) { + struct in6_addr prefix = address->in_addr.in6; + + in6_addr_mask(&prefix, 64); + *ret = prefix; + } + return 0; + } + } else { + Route *route; + + SET_FOREACH(route, link->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_DHCP_PD) + continue; + assert(route->family == AF_INET6); + + if (in6_addr_prefix_covers(pd_prefix, pd_prefix_len, &route->dst.in6) > 0) { + if (ret) + *ret = route->dst.in6; + return 0; + } + } + } + + return -ENOENT; +} + +int dhcp_pd_remove(Link *link, bool only_marked) { + int k, r = 0; + + assert(link); + assert(link->manager); + + if (!link_dhcp_pd_is_enabled(link)) + return 0; + + if (!only_marked) + link->dhcp_pd_configured = false; + + if (!link->network->dhcp_pd_assign) { + Route *route; + + SET_FOREACH(route, link->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_DHCP_PD) + continue; + if (only_marked && !route_is_marked(route)) + continue; + + if (link->radv) + sd_radv_remove_prefix(link->radv, &route->dst.in6, 64); + + link_remove_dhcp_pd_subnet_prefix(link, &route->dst.in6); + + k = route_remove(route); + if (k < 0) + r = k; + + route_cancel_request(route, link); + } + } else { + Address *address; + + SET_FOREACH(address, link->addresses) { + struct in6_addr prefix; + + if (address->source != NETWORK_CONFIG_SOURCE_DHCP_PD) + continue; + if (only_marked && !address_is_marked(address)) + continue; + + prefix = address->in_addr.in6; + in6_addr_mask(&prefix, 64); + + if (link->radv) + sd_radv_remove_prefix(link->radv, &prefix, 64); + + link_remove_dhcp_pd_subnet_prefix(link, &prefix); + + k = address_remove_and_drop(address); + if (k < 0) + r = k; + } + } + + return r; +} + +static int dhcp_pd_check_ready(Link *link); + +static int dhcp_pd_address_ready_callback(Address *address) { + Address *a; + + assert(address); + assert(address->link); + + SET_FOREACH(a, address->link->addresses) + if (a->source == NETWORK_CONFIG_SOURCE_DHCP_PD) + a->callback = NULL; + + return dhcp_pd_check_ready(address->link); +} + +static int dhcp_pd_check_ready(Link *link) { + int r; + + assert(link); + assert(link->network); + + if (link->dhcp_pd_messages > 0) { + log_link_debug(link, "%s(): DHCP-PD addresses and routes are not set.", __func__); + return 0; + } + + if (link->network->dhcp_pd_assign) { + bool has_ready = false; + Address *address; + + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_DHCP_PD) + continue; + if (address_is_ready(address)) { + has_ready = true; + break; + } + } + + if (!has_ready) { + SET_FOREACH(address, link->addresses) + if (address->source == NETWORK_CONFIG_SOURCE_DHCP_PD) + address->callback = dhcp_pd_address_ready_callback; + + log_link_debug(link, "%s(): no DHCP-PD address is ready.", __func__); + return 0; + } + } + + link->dhcp_pd_configured = true; + + log_link_debug(link, "DHCP-PD addresses and routes set."); + + r = dhcp_pd_remove(link, /* only_marked = */ true); + if (r < 0) + return r; + + link_check_ready(link); + return 1; +} + +static int dhcp_pd_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) { + int r; + + assert(link); + + r = route_configure_handler_internal(rtnl, m, link, "Failed to add prefix route for DHCP delegated subnet prefix"); + if (r <= 0) + return r; + + r = dhcp_pd_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static int dhcp_pd_request_route(Link *link, const struct in6_addr *prefix, usec_t lifetime_usec) { + _cleanup_(route_freep) Route *route = NULL; + Route *existing; + int r; + + assert(link); + assert(link->network); + assert(prefix); + + if (link->network->dhcp_pd_assign) + return 0; + + r = route_new(&route); + if (r < 0) + return r; + + route->source = NETWORK_CONFIG_SOURCE_DHCP_PD; + route->family = AF_INET6; + route->dst.in6 = *prefix; + route->dst_prefixlen = 64; + route->protocol = RTPROT_DHCP; + route->priority = link->network->dhcp_pd_route_metric; + route->lifetime_usec = lifetime_usec; + + if (route_get(NULL, link, route, &existing) < 0) + link->dhcp_pd_configured = false; + else + route_unmark(existing); + + r = link_request_route(link, TAKE_PTR(route), true, &link->dhcp_pd_messages, + dhcp_pd_route_handler, NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request DHCP-PD prefix route: %m"); + + return 0; +} + +static int dhcp_pd_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) { + int r; + + assert(link); + + r = address_configure_handler_internal(rtnl, m, link, "Could not set DHCP-PD address"); + if (r <= 0) + return r; + + r = dhcp_pd_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static void log_dhcp_pd_address(Link *link, const Address *address) { + assert(address); + assert(address->family == AF_INET6); + + int log_level = address_get_harder(link, address, NULL) >= 0 ? LOG_DEBUG : LOG_INFO; + + if (log_level < log_get_max_level()) + return; + + log_link_full(link, log_level, "DHCP-PD address %s (valid %s, preferred %s)", + IN6_ADDR_PREFIX_TO_STRING(&address->in_addr.in6, address->prefixlen), + FORMAT_LIFETIME(address->lifetime_valid_usec), + FORMAT_LIFETIME(address->lifetime_preferred_usec)); +} + +static int dhcp_pd_request_address( + Link *link, + const struct in6_addr *prefix, + usec_t lifetime_preferred_usec, + usec_t lifetime_valid_usec) { + + _cleanup_set_free_ Set *addresses = NULL; + struct in6_addr *a; + int r; + + assert(link); + assert(link->network); + assert(prefix); + + if (!link->network->dhcp_pd_assign) + return 0; + + r = dhcp_pd_generate_addresses(link, prefix, &addresses); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to generate addresses for acquired DHCP delegated prefix: %m"); + + SET_FOREACH(a, addresses) { + _cleanup_(address_freep) Address *address = NULL; + Address *existing; + + r = address_new(&address); + if (r < 0) + return log_link_error_errno(link, r, "Failed to allocate address for DHCP delegated prefix: %m"); + + address->source = NETWORK_CONFIG_SOURCE_DHCP_PD; + address->family = AF_INET6; + address->in_addr.in6 = *a; + address->prefixlen = 64; + address->lifetime_preferred_usec = lifetime_preferred_usec; + address->lifetime_valid_usec = lifetime_valid_usec; + SET_FLAG(address->flags, IFA_F_MANAGETEMPADDR, link->network->dhcp_pd_manage_temporary_address); + address->route_metric = link->network->dhcp_pd_route_metric; + + log_dhcp_pd_address(link, address); + + r = free_and_strdup_warn(&address->netlabel, link->network->dhcp_pd_netlabel); + if (r < 0) + return r; + + if (address_get(link, address, &existing) < 0) + link->dhcp_pd_configured = false; + else + address_unmark(existing); + + r = link_request_address(link, address, &link->dhcp_pd_messages, + dhcp_pd_address_handler, NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request DHCP delegated prefix address: %m"); + } + + return 0; +} + +static int dhcp_pd_calculate_subnet_prefix( + const struct in6_addr *pd_prefix, + uint8_t pd_prefix_len, + uint64_t subnet_id, + struct in6_addr *ret) { + + struct in6_addr prefix; + + assert(pd_prefix); + assert(pd_prefix_len <= 64); + assert(ret); + + if (subnet_id >= UINT64_C(1) << (64 - pd_prefix_len)) + return -ERANGE; + + prefix = *pd_prefix; + + if (pd_prefix_len < 32) + prefix.s6_addr32[0] |= htobe32(subnet_id >> 32); + + prefix.s6_addr32[1] |= htobe32(subnet_id & 0xffffffff); + + *ret = prefix; + return 0; +} + +static int dhcp_pd_get_preferred_subnet_prefix( + Link *link, + const struct in6_addr *pd_prefix, + uint8_t pd_prefix_len, + struct in6_addr *ret) { + + struct in6_addr prefix; + Link *assigned_link; + int r; + + assert(link); + assert(link->manager); + assert(link->network); + assert(pd_prefix); + + if (link->network->dhcp_pd_subnet_id >= 0) { + /* If the link has a preference for a particular subnet id try to allocate that */ + + r = dhcp_pd_calculate_subnet_prefix(pd_prefix, pd_prefix_len, link->network->dhcp_pd_subnet_id, &prefix); + if (r < 0) + return log_link_warning_errno(link, r, + "subnet id %" PRIi64 " is out of range. Only have %" PRIu64 " subnets.", + link->network->dhcp_pd_subnet_id, UINT64_C(1) << (64 - pd_prefix_len)); + + *ret = prefix; + return 0; + } + + if (dhcp_pd_get_assigned_subnet_prefix(link, pd_prefix, pd_prefix_len, ret) >= 0) + return 0; + + for (uint64_t n = 0; ; n++) { + /* If we do not have an allocation preference just iterate + * through the address space and return the first free prefix. */ + + r = dhcp_pd_calculate_subnet_prefix(pd_prefix, pd_prefix_len, n, &prefix); + if (r < 0) + return log_link_warning_errno(link, r, + "Couldn't find a suitable prefix. Ran out of address space."); + + /* Do not use explicitly requested subnet IDs. Note that the corresponding link may not + * appear yet. So, we need to check the ID is not used in any .network files. */ + if (set_contains(link->manager->dhcp_pd_subnet_ids, &n)) + continue; + + /* Check that the prefix is not assigned to another link. */ + if (link_get_by_dhcp_pd_subnet_prefix(link->manager, &prefix, &assigned_link) < 0 || + assigned_link == link) + break; + } + + r = link_add_dhcp_pd_subnet_prefix(link, &prefix); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to save acquired free subnet prefix: %m"); + + *ret = prefix; + return 0; +} + +static int dhcp_pd_assign_subnet_prefix( + Link *link, + const struct in6_addr *pd_prefix, + uint8_t pd_prefix_len, + usec_t lifetime_preferred_usec, + usec_t lifetime_valid_usec, + bool is_uplink) { + + struct in6_addr prefix; + int r; + + assert(link); + assert(link->network); + assert(pd_prefix); + + r = dhcp_pd_get_preferred_subnet_prefix(link, pd_prefix, pd_prefix_len, &prefix); + if (r < 0) + return r == -ERANGE ? 0 : r; + + const char *pretty = IN6_ADDR_PREFIX_TO_STRING(&prefix, 64); + + if (link_radv_enabled(link) && link->network->dhcp_pd_announce) { + if (is_uplink) + log_link_debug(link, "Ignoring Announce= setting on upstream interface."); + else { + r = radv_add_prefix(link, &prefix, 64, lifetime_preferred_usec, lifetime_valid_usec); + if (r < 0) + return log_link_warning_errno(link, r, + "Failed to assign/update prefix %s to IPv6 Router Advertisement: %m", + pretty); + } + } + + r = dhcp_pd_request_route(link, &prefix, lifetime_valid_usec); + if (r < 0) + return log_link_warning_errno(link, r, + "Failed to assign/update route for prefix %s: %m", pretty); + + r = dhcp_pd_request_address(link, &prefix, lifetime_preferred_usec, lifetime_valid_usec); + if (r < 0) + return log_link_warning_errno(link, r, + "Failed to assign/update address for prefix %s: %m", pretty); + + log_link_debug(link, "Assigned prefix %s", pretty); + return 1; +} + +static int dhcp_pd_prepare(Link *link) { + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return 0; + + if (!link_dhcp_pd_is_enabled(link)) + return 0; + + if (link_radv_enabled(link) && link->network->dhcp_pd_announce && !link->radv) + return 0; + + link_mark_addresses(link, NETWORK_CONFIG_SOURCE_DHCP_PD); + link_mark_routes(link, NETWORK_CONFIG_SOURCE_DHCP_PD); + + return 1; +} + +static int dhcp_pd_finalize(Link *link) { + int r; + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return 0; + + if (link->dhcp_pd_messages == 0) { + link->dhcp_pd_configured = false; + + r = dhcp_pd_remove(link, /* only_marked = */ true); + if (r < 0) + return r; + } + + if (!link->dhcp_pd_configured) + link_set_state(link, LINK_STATE_CONFIGURING); + + link_check_ready(link); + return 0; +} + +void dhcp_pd_prefix_lost(Link *uplink) { + Route *route; + Link *link; + int r; + + assert(uplink); + assert(uplink->manager); + + HASHMAP_FOREACH(link, uplink->manager->links_by_index) { + if (!dhcp_pd_is_uplink(link, uplink, /* accept_auto = */ true)) + continue; + + r = dhcp_pd_remove(link, /* only_marked = */ false); + if (r < 0) + link_enter_failed(link); + } + + SET_FOREACH(route, uplink->manager->routes) { + if (!IN_SET(route->source, NETWORK_CONFIG_SOURCE_DHCP4, NETWORK_CONFIG_SOURCE_DHCP6)) + continue; + if (route->family != AF_INET6) + continue; + if (route->type != RTN_UNREACHABLE) + continue; + if (!set_contains(uplink->dhcp_pd_prefixes, + &(struct in_addr_prefix) { + .family = AF_INET6, + .prefixlen = route->dst_prefixlen, + .address = route->dst })) + continue; + + (void) route_remove(route); + + route_cancel_request(route, uplink); + } + + set_clear(uplink->dhcp_pd_prefixes); +} + +void dhcp4_pd_prefix_lost(Link *uplink) { + Link *tunnel; + + dhcp_pd_prefix_lost(uplink); + + if (uplink->dhcp4_6rd_tunnel_name && + link_get_by_name(uplink->manager, uplink->dhcp4_6rd_tunnel_name, &tunnel) >= 0) + (void) link_remove(tunnel); +} + +static int dhcp4_unreachable_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) { + int r; + + assert(link); + + r = route_configure_handler_internal(rtnl, m, link, "Failed to set unreachable route for DHCPv4 delegated prefix"); + if (r <= 0) + return r; + + r = dhcp4_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static int dhcp6_unreachable_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) { + int r; + + assert(link); + + r = route_configure_handler_internal(rtnl, m, link, "Failed to set unreachable route for DHCPv6 delegated prefix"); + if (r <= 0) + return r; + + r = dhcp6_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static int dhcp_request_unreachable_route( + Link *link, + const struct in6_addr *addr, + uint8_t prefixlen, + usec_t lifetime_usec, + NetworkConfigSource source, + const union in_addr_union *server_address, + unsigned *counter, + route_netlink_handler_t callback, + bool *configured) { + + _cleanup_(route_freep) Route *route = NULL; + Route *existing; + int r; + + assert(link); + assert(addr); + assert(IN_SET(source, NETWORK_CONFIG_SOURCE_DHCP4, NETWORK_CONFIG_SOURCE_DHCP6)); + assert(server_address); + assert(counter); + assert(callback); + assert(configured); + + if (prefixlen >= 64) { + log_link_debug(link, "Not adding a blocking route for DHCP delegated prefix %s since the prefix has length >= 64.", + IN6_ADDR_PREFIX_TO_STRING(addr, prefixlen)); + return 0; + } + + r = route_new(&route); + if (r < 0) + return log_oom(); + + route->source = source; + route->provider = *server_address; + route->family = AF_INET6; + route->dst.in6 = *addr; + route->dst_prefixlen = prefixlen; + route->type = RTN_UNREACHABLE; + route->protocol = RTPROT_DHCP; + route->priority = IP6_RT_PRIO_USER; + route->lifetime_usec = lifetime_usec; + + if (route_get(link->manager, NULL, route, &existing) < 0) + *configured = false; + else + route_unmark(existing); + + r = link_request_route(link, TAKE_PTR(route), true, counter, callback, NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request unreachable route for DHCP delegated prefix %s: %m", + IN6_ADDR_PREFIX_TO_STRING(addr, prefixlen)); + + return 0; +} + +static int dhcp4_request_unreachable_route( + Link *link, + const struct in6_addr *addr, + uint8_t prefixlen, + usec_t lifetime_usec, + const union in_addr_union *server_address) { + + return dhcp_request_unreachable_route(link, addr, prefixlen, lifetime_usec, + NETWORK_CONFIG_SOURCE_DHCP4, server_address, + &link->dhcp4_messages, dhcp4_unreachable_route_handler, + &link->dhcp4_configured); +} + +static int dhcp6_request_unreachable_route( + Link *link, + const struct in6_addr *addr, + uint8_t prefixlen, + usec_t lifetime_usec, + const union in_addr_union *server_address) { + + return dhcp_request_unreachable_route(link, addr, prefixlen, lifetime_usec, + NETWORK_CONFIG_SOURCE_DHCP6, server_address, + &link->dhcp6_messages, dhcp6_unreachable_route_handler, + &link->dhcp6_configured); +} + +static int dhcp_pd_prefix_add(Link *link, const struct in6_addr *prefix, uint8_t prefixlen) { + struct in_addr_prefix *p; + int r; + + assert(link); + assert(prefix); + + p = new(struct in_addr_prefix, 1); + if (!p) + return log_oom(); + + *p = (struct in_addr_prefix) { + .family = AF_INET6, + .prefixlen = prefixlen, + .address.in6 = *prefix, + }; + + int log_level = set_contains(link->dhcp_pd_prefixes, p) ? LOG_DEBUG : + prefixlen > 64 || prefixlen < 48 ? LOG_WARNING : LOG_INFO; + log_link_full(link, + log_level, + "DHCP: received delegated prefix %s%s", + IN6_ADDR_PREFIX_TO_STRING(prefix, prefixlen), + prefixlen > 64 ? " with prefix length > 64, ignoring." : + prefixlen < 48 ? " with prefix length < 48, looks unusual.": ""); + + /* Store PD prefix even if prefixlen > 64, not to make logged at warning level so frequently. */ + r = set_ensure_consume(&link->dhcp_pd_prefixes, &in_addr_prefix_hash_ops_free, p); + if (r < 0) + return log_link_error_errno(link, r, "Failed to store DHCP delegated prefix %s: %m", + IN6_ADDR_PREFIX_TO_STRING(prefix, prefixlen)); + return 0; +} + +static int dhcp4_pd_request_default_gateway_on_6rd_tunnel(Link *link, const struct in_addr *br_address, usec_t lifetime_usec) { + _cleanup_(route_freep) Route *route = NULL; + Route *existing; + int r; + + assert(link); + assert(br_address); + + r = route_new(&route); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to allocate default gateway for DHCP delegated prefix: %m"); + + route->source = NETWORK_CONFIG_SOURCE_DHCP_PD; + route->family = AF_INET6; + route->gw_family = AF_INET6; + route->gw.in6.s6_addr32[3] = br_address->s_addr; + route->scope = RT_SCOPE_UNIVERSE; + route->protocol = RTPROT_DHCP; + route->priority = IP6_RT_PRIO_USER; + route->lifetime_usec = lifetime_usec; + + if (route_get(NULL, link, route, &existing) < 0) /* This is a new route. */ + link->dhcp_pd_configured = false; + else + route_unmark(existing); + + r = link_request_route(link, TAKE_PTR(route), true, &link->dhcp_pd_messages, + dhcp_pd_route_handler, NULL); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to request default gateway for DHCP delegated prefix: %m"); + + return 0; +} + +static void dhcp4_calculate_pd_prefix( + const struct in_addr *ipv4address, + uint8_t ipv4masklen, + const struct in6_addr *sixrd_prefix, + uint8_t sixrd_prefixlen, + struct in6_addr *ret_pd_prefix, + uint8_t *ret_pd_prefixlen) { + + struct in6_addr pd_prefix; + + assert(ipv4address); + assert(ipv4masklen <= 32); + assert(sixrd_prefix); + assert(32 - ipv4masklen + sixrd_prefixlen <= 128); + assert(ret_pd_prefix); + + pd_prefix = *sixrd_prefix; + for (unsigned i = 0; i < (unsigned) (32 - ipv4masklen); i++) + if (ipv4address->s_addr & htobe32(UINT32_C(1) << (32 - ipv4masklen - i - 1))) + pd_prefix.s6_addr[(i + sixrd_prefixlen) / 8] |= 1 << (7 - (i + sixrd_prefixlen) % 8); + + *ret_pd_prefix = pd_prefix; + if (ret_pd_prefixlen) + *ret_pd_prefixlen = 32 - ipv4masklen + sixrd_prefixlen; +} + +static int dhcp4_pd_assign_subnet_prefix(Link *link, Link *uplink) { + uint8_t ipv4masklen, sixrd_prefixlen, pd_prefixlen; + struct in6_addr sixrd_prefix, pd_prefix; + const struct in_addr *br_addresses; + struct in_addr ipv4address; + usec_t lifetime_usec; + int r; + + assert(link); + assert(uplink); + assert(uplink->manager); + assert(uplink->dhcp_lease); + + r = sd_dhcp_lease_get_address(uplink->dhcp_lease, &ipv4address); + if (r < 0) + return log_link_warning_errno(uplink, r, "Failed to get DHCPv4 address: %m"); + + r = sd_dhcp_lease_get_lifetime_timestamp(uplink->dhcp_lease, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(uplink, r, "Failed to get lifetime of DHCPv4 lease: %m"); + + r = sd_dhcp_lease_get_6rd(uplink->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, &br_addresses, NULL); + if (r < 0) + return log_link_warning_errno(uplink, r, "Failed to get DHCPv4 6rd option: %m"); + + dhcp4_calculate_pd_prefix(&ipv4address, ipv4masklen, &sixrd_prefix, sixrd_prefixlen, &pd_prefix, &pd_prefixlen); + + if (pd_prefixlen > 64) + return 0; + + r = dhcp_pd_prepare(link); + if (r <= 0) + return r; + + if (streq_ptr(uplink->dhcp4_6rd_tunnel_name, link->ifname)) { + r = dhcp4_pd_request_default_gateway_on_6rd_tunnel(link, &br_addresses[0], lifetime_usec); + if (r < 0) + return r; + } + + r = dhcp_pd_assign_subnet_prefix(link, &pd_prefix, pd_prefixlen, lifetime_usec, lifetime_usec, /* is_uplink = */ false); + if (r < 0) + return r; + + return dhcp_pd_finalize(link); +} + +static int dhcp4_pd_6rd_tunnel_create_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert(m); + assert(link); + assert(link->manager); + assert(link->dhcp4_6rd_tunnel_name); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + log_link_message_warning_errno(link, m, r, "Failed to create tunnel device for DHCPv4 6rd"); + link_enter_failed(link); + return 0; + } + + return 0; +} + +int dhcp4_pd_prefix_acquired(Link *uplink) { + _cleanup_free_ char *tunnel_name = NULL; + uint8_t ipv4masklen, sixrd_prefixlen, pd_prefixlen; + struct in6_addr sixrd_prefix, pd_prefix; + struct in_addr ipv4address; + union in_addr_union server_address; + const struct in_addr *br_addresses; + usec_t lifetime_usec; + Link *link; + int r; + + assert(uplink); + assert(uplink->manager); + assert(uplink->dhcp_lease); + + r = sd_dhcp_lease_get_address(uplink->dhcp_lease, &ipv4address); + if (r < 0) + return log_link_warning_errno(uplink, r, "Failed to get DHCPv4 address: %m"); + + r = sd_dhcp_lease_get_lifetime_timestamp(uplink->dhcp_lease, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(uplink, r, "Failed to get lifetime of DHCPv4 lease: %m"); + + r = sd_dhcp_lease_get_server_identifier(uplink->dhcp_lease, &server_address.in); + if (r < 0) + return log_link_warning_errno(uplink, r, "Failed to get server address of DHCPv4 lease: %m"); + + r = sd_dhcp_lease_get_6rd(uplink->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, &br_addresses, NULL); + if (r < 0) + return log_link_warning_errno(uplink, r, "Failed to get DHCPv4 6rd option: %m"); + + if (DEBUG_LOGGING) + log_link_debug(uplink, "DHCPv4: 6rd option is acquired: IPv4_masklen=%u, 6rd_prefix=%s, br_address="IPV4_ADDRESS_FMT_STR, + ipv4masklen, + IN6_ADDR_PREFIX_TO_STRING(&sixrd_prefix, sixrd_prefixlen), + IPV4_ADDRESS_FMT_VAL(*br_addresses)); + + /* Calculate PD prefix */ + dhcp4_calculate_pd_prefix(&ipv4address, ipv4masklen, &sixrd_prefix, sixrd_prefixlen, &pd_prefix, &pd_prefixlen); + + /* Register and log PD prefix */ + r = dhcp_pd_prefix_add(uplink, &pd_prefix, pd_prefixlen); + if (r < 0) + return r; + + /* Request unreachable route */ + r = dhcp4_request_unreachable_route(uplink, &pd_prefix, pd_prefixlen, lifetime_usec, &server_address); + if (r < 0) + return r; + + /* Generate 6rd SIT tunnel device name. */ + r = dhcp4_pd_create_6rd_tunnel_name(uplink, &tunnel_name); + if (r < 0) + return r; + + /* Remove old tunnel device if exists. */ + if (!streq_ptr(uplink->dhcp4_6rd_tunnel_name, tunnel_name)) { + Link *old_tunnel; + + if (uplink->dhcp4_6rd_tunnel_name && + link_get_by_name(uplink->manager, uplink->dhcp4_6rd_tunnel_name, &old_tunnel) >= 0) + (void) link_remove(old_tunnel); + + free_and_replace(uplink->dhcp4_6rd_tunnel_name, tunnel_name); + } + + /* Create 6rd SIT tunnel device if it does not exist yet. */ + if (link_get_by_name(uplink->manager, uplink->dhcp4_6rd_tunnel_name, NULL) < 0) { + r = dhcp4_pd_create_6rd_tunnel(uplink, dhcp4_pd_6rd_tunnel_create_handler); + if (r < 0) + return r; + } + + /* Then, assign subnet prefixes to downstream interfaces. */ + HASHMAP_FOREACH(link, uplink->manager->links_by_index) { + if (!dhcp_pd_is_uplink(link, uplink, /* accept_auto = */ true)) + continue; + + r = dhcp4_pd_assign_subnet_prefix(link, uplink); + if (r < 0) { + /* When failed on the upstream interface (i.e., the case link == uplink), + * immediately abort the assignment of the prefixes. As, the all assigned + * prefixes will be dropped soon in link_enter_failed(), and it is meaningless + * to continue the assignment. */ + if (link == uplink) + return r; + + link_enter_failed(link); + } + } + + return 0; +} + +static int dhcp6_pd_assign_subnet_prefixes(Link *link, Link *uplink) { + int r; + + assert(link); + assert(uplink); + assert(uplink->dhcp6_lease); + + r = dhcp_pd_prepare(link); + if (r <= 0) + return r; + + FOREACH_DHCP6_PD_PREFIX(uplink->dhcp6_lease) { + usec_t lifetime_preferred_usec, lifetime_valid_usec; + struct in6_addr pd_prefix; + uint8_t pd_prefix_len; + + r = sd_dhcp6_lease_get_pd_prefix(uplink->dhcp6_lease, &pd_prefix, &pd_prefix_len); + if (r < 0) + return r; + + if (pd_prefix_len > 64) + continue; + + /* Mask prefix for safety. */ + r = in6_addr_mask(&pd_prefix, pd_prefix_len); + if (r < 0) + return r; + + r = sd_dhcp6_lease_get_pd_lifetime_timestamp(uplink->dhcp6_lease, CLOCK_BOOTTIME, + &lifetime_preferred_usec, &lifetime_valid_usec); + if (r < 0) + return r; + + r = dhcp_pd_assign_subnet_prefix(link, &pd_prefix, pd_prefix_len, + lifetime_preferred_usec, lifetime_valid_usec, + /* is_uplink = */ link == uplink); + if (r < 0) + return r; + } + + return dhcp_pd_finalize(link); +} + +int dhcp6_pd_prefix_acquired(Link *uplink) { + union in_addr_union server_address; + Link *link; + int r; + + assert(uplink); + assert(uplink->dhcp6_lease); + + r = sd_dhcp6_lease_get_server_address(uplink->dhcp6_lease, &server_address.in6); + if (r < 0) + return log_link_warning_errno(uplink, r, "Failed to get server address of DHCPv6 lease: %m"); + + /* First, logs acquired prefixes and request unreachable routes. */ + FOREACH_DHCP6_PD_PREFIX(uplink->dhcp6_lease) { + usec_t lifetime_valid_usec; + struct in6_addr pd_prefix; + uint8_t pd_prefix_len; + + r = sd_dhcp6_lease_get_pd_prefix(uplink->dhcp6_lease, &pd_prefix, &pd_prefix_len); + if (r < 0) + return r; + + /* Mask prefix for safety. */ + r = in6_addr_mask(&pd_prefix, pd_prefix_len); + if (r < 0) + return log_link_error_errno(uplink, r, "Failed to mask DHCPv6 delegated prefix: %m"); + + r = dhcp_pd_prefix_add(uplink, &pd_prefix, pd_prefix_len); + if (r < 0) + return r; + + r = sd_dhcp6_lease_get_pd_lifetime_timestamp(uplink->dhcp6_lease, CLOCK_BOOTTIME, + NULL, &lifetime_valid_usec); + if (r < 0) + return r; + + r = dhcp6_request_unreachable_route(uplink, &pd_prefix, pd_prefix_len, + lifetime_valid_usec, &server_address); + if (r < 0) + return r; + } + + /* Then, assign subnet prefixes. */ + HASHMAP_FOREACH(link, uplink->manager->links_by_index) { + if (!dhcp_pd_is_uplink(link, uplink, /* accept_auto = */ true)) + continue; + + r = dhcp6_pd_assign_subnet_prefixes(link, uplink); + if (r < 0) { + /* When failed on the upstream interface (i.e., the case link == uplink), + * immediately abort the assignment of the prefixes. As, the all assigned + * prefixes will be dropped soon in link_enter_failed(), and it is meaningless + * to continue the assignment. */ + if (link == uplink) + return r; + + link_enter_failed(link); + } + } + + return 0; +} + +static bool dhcp4_pd_uplink_is_ready(Link *link) { + assert(link); + + if (!link->network) + return false; + + if (!link->network->dhcp_use_6rd) + return false; + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return false; + + if (!link->dhcp_client) + return false; + + if (sd_dhcp_client_is_running(link->dhcp_client) <= 0) + return false; + + return sd_dhcp_lease_has_6rd(link->dhcp_lease); +} + +static bool dhcp6_pd_uplink_is_ready(Link *link) { + assert(link); + + if (!link->network) + return false; + + if (!link->network->dhcp6_use_pd_prefix) + return false; + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return false; + + if (!link->dhcp6_client) + return false; + + if (sd_dhcp6_client_is_running(link->dhcp6_client) <= 0) + return false; + + return sd_dhcp6_lease_has_pd_prefix(link->dhcp6_lease); +} + +int dhcp_pd_find_uplink(Link *link, Link **ret) { + Link *uplink = NULL; + int r = 0; + + assert(link); + assert(link->manager); + assert(link_dhcp_pd_is_enabled(link)); + assert(ret); + + if (link->network->dhcp_pd_uplink_name) + r = link_get_by_name(link->manager, link->network->dhcp_pd_uplink_name, &uplink); + else if (link->network->dhcp_pd_uplink_index > 0) + r = link_get_by_index(link->manager, link->network->dhcp_pd_uplink_index, &uplink); + else if (link->network->dhcp_pd_uplink_index == UPLINK_INDEX_SELF) + uplink = link; + if (r < 0) + return r; + + if (uplink) { + if (dhcp4_pd_uplink_is_ready(uplink)) { + *ret = uplink; + return AF_INET; + } + + if (dhcp6_pd_uplink_is_ready(uplink)) { + *ret = uplink; + return AF_INET6; + } + + return -EBUSY; + } + + HASHMAP_FOREACH(uplink, link->manager->links_by_index) { + /* Assume that there exists at most one link which acquired delegated prefixes. */ + if (dhcp4_pd_uplink_is_ready(uplink)) { + *ret = uplink; + return AF_INET; + } + + if (dhcp6_pd_uplink_is_ready(uplink)) { + *ret = uplink; + return AF_INET6; + } + } + + return -ENODEV; +} + +int dhcp_request_prefix_delegation(Link *link) { + Link *uplink; + int r; + + assert(link); + + if (!link_dhcp_pd_is_enabled(link)) + return 0; + + r = dhcp_pd_find_uplink(link, &uplink); + if (r < 0) + return 0; + + log_link_debug(link, "Requesting subnets of delegated prefixes acquired by DHCPv%c client on %s", + r == AF_INET ? '4' : '6', uplink->ifname); + + return r == AF_INET ? + dhcp4_pd_assign_subnet_prefix(link, uplink) : + dhcp6_pd_assign_subnet_prefixes(link, uplink); +} + +int config_parse_dhcp_pd_subnet_id( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + int64_t *p = ASSERT_PTR(data); + uint64_t t; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue) || streq(rvalue, "auto")) { + *p = -1; + return 0; + } + + r = safe_atoux64(rvalue, &t); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (t > INT64_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid subnet id '%s', ignoring assignment.", + rvalue); + return 0; + } + + *p = (int64_t) t; + + return 0; +} diff --git a/src/network/networkd-dhcp-prefix-delegation.h b/src/network/networkd-dhcp-prefix-delegation.h new file mode 100644 index 0000000..e591b8a --- /dev/null +++ b/src/network/networkd-dhcp-prefix-delegation.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> + +#include "sd-dhcp-lease.h" +#include "sd-dhcp6-lease.h" + +#include "conf-parser.h" + +typedef struct Link Link; + +bool link_dhcp_pd_is_enabled(Link *link); +bool dhcp_pd_is_uplink(Link *link, Link *target, bool accept_auto); +int dhcp_pd_find_uplink(Link *link, Link **ret); +int dhcp_pd_remove(Link *link, bool only_marked); +int dhcp_request_prefix_delegation(Link *link); +int dhcp4_pd_prefix_acquired(Link *uplink); +int dhcp6_pd_prefix_acquired(Link *uplink); +void dhcp_pd_prefix_lost(Link *uplink); +void dhcp4_pd_prefix_lost(Link *uplink); + +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_pd_subnet_id); diff --git a/src/network/networkd-dhcp-server-bus.c b/src/network/networkd-dhcp-server-bus.c new file mode 100644 index 0000000..e3397c3 --- /dev/null +++ b/src/network/networkd-dhcp-server-bus.c @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-util.h" +#include "dhcp-server-internal.h" +#include "networkd-dhcp-server-bus.h" +#include "networkd-link-bus.h" +#include "networkd-manager.h" +#include "strv.h" + +static int property_get_leases( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + sd_dhcp_server *s; + DHCPLease *lease; + int r; + + assert(reply); + + s = l->dhcp_server; + if (!s) + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Link %s has no DHCP server.", l->ifname); + + if (sd_dhcp_server_is_in_relay_mode(s)) + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Link %s has DHCP relay agent active.", l->ifname); + + r = sd_bus_message_open_container(reply, 'a', "(uayayayayt)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(lease, s->bound_leases_by_client_id) { + r = sd_bus_message_open_container(reply, 'r', "uayayayayt"); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "u", (uint32_t)AF_INET); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', lease->client_id.data, lease->client_id.length); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', &lease->address, sizeof(lease->address)); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', &lease->gateway, sizeof(lease->gateway)); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', &lease->chaddr, sizeof(lease->chaddr)); + if (r < 0) + return r; + + r = sd_bus_message_append_basic(reply, 't', &lease->expiration); + if (r < 0) + return r; + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static int dhcp_server_emit_changed(Link *link, const char *property, ...) { + _cleanup_free_ char *path = NULL; + char **l; + + assert(link); + + if (sd_bus_is_ready(link->manager->bus) <= 0) + return 0; + + path = link_bus_path(link); + if (!path) + return log_oom(); + + l = strv_from_stdarg_alloca(property); + + return sd_bus_emit_properties_changed_strv( + link->manager->bus, + path, + "org.freedesktop.network1.DHCPServer", + l); +} + +void dhcp_server_callback(sd_dhcp_server *s, uint64_t event, void *data) { + Link *l = ASSERT_PTR(data); + + if (event & SD_DHCP_SERVER_EVENT_LEASE_CHANGED) + (void) dhcp_server_emit_changed(l, "Leases", NULL); +} + +static const sd_bus_vtable dhcp_server_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("Leases", "a(uayayayayt)", property_get_leases, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + + SD_BUS_VTABLE_END +}; + +const BusObjectImplementation dhcp_server_object = { + "/org/freedesktop/network1/link", + "org.freedesktop.network1.DHCPServer", + .fallback_vtables = BUS_FALLBACK_VTABLES({dhcp_server_vtable, link_object_find}), + .node_enumerator = link_node_enumerator, +}; diff --git a/src/network/networkd-dhcp-server-bus.h b/src/network/networkd-dhcp-server-bus.h new file mode 100644 index 0000000..f52be82 --- /dev/null +++ b/src/network/networkd-dhcp-server-bus.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" +#include "sd-dhcp-server.h" + +#include "bus-object.h" + +extern const BusObjectImplementation dhcp_server_object; + +void dhcp_server_callback(sd_dhcp_server *server, uint64_t event, void *data); diff --git a/src/network/networkd-dhcp-server-static-lease.c b/src/network/networkd-dhcp-server-static-lease.c new file mode 100644 index 0000000..8e7eec6 --- /dev/null +++ b/src/network/networkd-dhcp-server-static-lease.c @@ -0,0 +1,210 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "ether-addr-util.h" +#include "hashmap.h" +#include "networkd-dhcp-server-static-lease.h" +#include "networkd-network.h" +#include "networkd-util.h" + +DEFINE_SECTION_CLEANUP_FUNCTIONS(DHCPStaticLease, dhcp_static_lease_free); + +DHCPStaticLease *dhcp_static_lease_free(DHCPStaticLease *static_lease) { + if (!static_lease) + return NULL; + + if (static_lease->network && static_lease->section) + hashmap_remove(static_lease->network->dhcp_static_leases_by_section, static_lease->section); + + config_section_free(static_lease->section); + free(static_lease->client_id); + return mfree(static_lease); +} + +static int dhcp_static_lease_new(DHCPStaticLease **ret) { + DHCPStaticLease *p; + + assert(ret); + + p = new0(DHCPStaticLease, 1); + if (!p) + return -ENOMEM; + + *ret = TAKE_PTR(p); + return 0; +} + +static int lease_new_static(Network *network, const char *filename, unsigned section_line, DHCPStaticLease **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(dhcp_static_lease_freep) DHCPStaticLease *static_lease = NULL; + int r; + + assert(network); + assert(filename); + assert(section_line > 0); + assert(ret); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + static_lease = hashmap_get(network->dhcp_static_leases_by_section, n); + if (static_lease) { + *ret = TAKE_PTR(static_lease); + return 0; + } + + r = dhcp_static_lease_new(&static_lease); + if (r < 0) + return r; + + static_lease->network = network; + static_lease->section = TAKE_PTR(n); + r = hashmap_ensure_put(&network->dhcp_static_leases_by_section, &config_section_hash_ops, static_lease->section, static_lease); + if (r < 0) + return r; + + *ret = TAKE_PTR(static_lease); + return 0; +} + +static int static_lease_verify(DHCPStaticLease *static_lease) { + if (section_is_invalid(static_lease->section)) + return -EINVAL; + + if (in4_addr_is_null(&static_lease->address)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: DHCP static lease without Address= field configured. " + "Ignoring [DHCPServerStaticLease] section from line %u.", + static_lease->section->filename, static_lease->section->line); + + /* TODO: check that the address is in the pool. */ + + if (static_lease->client_id_size == 0 || !static_lease->client_id) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: DHCP static lease without MACAddress= field configured. " + "Ignoring [DHCPServerStaticLease] section from line %u.", + static_lease->section->filename, static_lease->section->line); + + assert(static_lease->client_id_size == ETH_ALEN + 1); + + return 0; +} + +void network_drop_invalid_static_leases(Network *network) { + DHCPStaticLease *static_lease; + + assert(network); + + HASHMAP_FOREACH(static_lease, network->dhcp_static_leases_by_section) + if (static_lease_verify(static_lease) < 0) + dhcp_static_lease_free(static_lease); +} + +int config_parse_dhcp_static_lease_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(dhcp_static_lease_free_or_set_invalidp) DHCPStaticLease *lease = NULL; + Network *network = ASSERT_PTR(userdata); + union in_addr_union addr; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = lease_new_static(network, filename, section_line, &lease); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + lease->address.s_addr = 0; + TAKE_PTR(lease); + return 0; + } + + r = in_addr_from_string(AF_INET, rvalue, &addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse IPv4 address for DHCPv4 static lease, ignoring assignment: %s", rvalue); + return 0; + } + if (in4_addr_is_null(&addr.in)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "IPv4 address for DHCPv4 static lease cannot be the ANY address, ignoring assignment: %s", rvalue); + return 0; + } + + lease->address = addr.in; + + TAKE_PTR(lease); + return 0; +} + +int config_parse_dhcp_static_lease_hwaddr( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(dhcp_static_lease_free_or_set_invalidp) DHCPStaticLease *lease = NULL; + Network *network = ASSERT_PTR(userdata); + struct ether_addr hwaddr; + uint8_t *c; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = lease_new_static(network, filename, section_line, &lease); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + lease->client_id = mfree(lease->client_id); + lease->client_id_size = 0; + return 0; + } + + r = parse_ether_addr(rvalue, &hwaddr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse MAC address for DHCPv4 static lease, ignoring assignment: %s", rvalue); + return 0; + } + if (ether_addr_is_null(&hwaddr) || (hwaddr.ether_addr_octet[0] & 0x01)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "MAC address for DHCPv4 static lease cannot be null or multicast, ignoring assignment: %s", rvalue); + return 0; + } + + c = new(uint8_t, ETH_ALEN + 1); + if (!c) + return log_oom(); + + /* set client id type to 1: Ethernet Link-Layer (RFC 2132) */ + c[0] = 0x01; + memcpy(c + 1, &hwaddr, ETH_ALEN); + + free_and_replace(lease->client_id, c); + lease->client_id_size = ETH_ALEN + 1; + + TAKE_PTR(lease); + return 0; +} diff --git a/src/network/networkd-dhcp-server-static-lease.h b/src/network/networkd-dhcp-server-static-lease.h new file mode 100644 index 0000000..9b8e78b --- /dev/null +++ b/src/network/networkd-dhcp-server-static-lease.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +#include <inttypes.h> + +#include "conf-parser.h" +#include "in-addr-util.h" + +typedef struct Network Network; +typedef struct ConfigSection ConfigSection; + +typedef struct DHCPStaticLease { + Network *network; + ConfigSection *section; + + struct in_addr address; + uint8_t *client_id; + size_t client_id_size; +} DHCPStaticLease; + +DHCPStaticLease *dhcp_static_lease_free(DHCPStaticLease *lease); +void network_drop_invalid_static_leases(Network *network); + +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_address); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_hwaddr); diff --git a/src/network/networkd-dhcp-server.c b/src/network/networkd-dhcp-server.c new file mode 100644 index 0000000..607fe00 --- /dev/null +++ b/src/network/networkd-dhcp-server.c @@ -0,0 +1,779 @@ +/* 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 "dhcp-protocol.h" +#include "fd-util.h" +#include "fileio.h" +#include "network-common.h" +#include "networkd-address.h" +#include "networkd-dhcp-server-bus.h" +#include "networkd-dhcp-server-static-lease.h" +#include "networkd-dhcp-server.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "networkd-route-util.h" +#include "parse-util.h" +#include "socket-netlink.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" + +static bool link_dhcp4_server_enabled(Link *link) { + assert(link); + + if (link->flags & IFF_LOOPBACK) + return false; + + if (!link->network) + return false; + + if (link->iftype == ARPHRD_CAN) + return false; + + return link->network->dhcp_server; +} + +int network_adjust_dhcp_server(Network *network, Set **addresses) { + int r; + + assert(network); + assert(addresses); + + if (!network->dhcp_server) + return 0; + + if (network->bond) { + log_warning("%s: DHCPServer= is enabled for bond slave. Disabling DHCP server.", + network->filename); + network->dhcp_server = false; + return 0; + } + + assert(network->dhcp_server_address_prefixlen <= 32); + + if (network->dhcp_server_address_prefixlen == 0) { + Address *address; + + /* If the server address is not specified, then find suitable static address. */ + + ORDERED_HASHMAP_FOREACH(address, network->addresses_by_section) { + assert(!section_is_invalid(address->section)); + + if (address->family != AF_INET) + continue; + + if (in4_addr_is_localhost(&address->in_addr.in)) + continue; + + if (in4_addr_is_link_local(&address->in_addr.in)) + continue; + + if (in4_addr_is_set(&address->in_addr_peer.in)) + continue; + + /* TODO: check if the prefix length is small enough for the pool. */ + + network->dhcp_server_address = address; + break; + } + if (!network->dhcp_server_address) { + log_warning("%s: DHCPServer= is enabled, but no suitable static address configured. " + "Disabling DHCP server.", + network->filename); + network->dhcp_server = false; + return 0; + } + + } else { + _cleanup_(address_freep) Address *a = NULL; + Address *existing; + unsigned line; + + /* TODO: check if the prefix length is small enough for the pool. */ + + /* If an address is explicitly specified, then check if the corresponding [Address] section + * is configured, and add one if not. */ + + existing = set_get(*addresses, + &(Address) { + .family = AF_INET, + .in_addr.in = network->dhcp_server_address_in_addr, + .prefixlen = network->dhcp_server_address_prefixlen, + }); + if (existing) { + /* Corresponding [Address] section already exists. */ + network->dhcp_server_address = existing; + return 0; + } + + r = ordered_hashmap_by_section_find_unused_line(network->addresses_by_section, network->filename, &line); + if (r < 0) + return log_warning_errno(r, "%s: Failed to find unused line number for DHCP server address: %m", + network->filename); + + r = address_new_static(network, network->filename, line, &a); + if (r < 0) + return log_warning_errno(r, "%s: Failed to add new static address object for DHCP server: %m", + network->filename); + + a->family = AF_INET; + a->prefixlen = network->dhcp_server_address_prefixlen; + a->in_addr.in = network->dhcp_server_address_in_addr; + a->requested_as_null = !in4_addr_is_set(&network->dhcp_server_address_in_addr); + + r = address_section_verify(a); + if (r < 0) + return r; + + r = set_ensure_put(addresses, &address_hash_ops, a); + if (r < 0) + return log_oom(); + assert(r > 0); + + network->dhcp_server_address = TAKE_PTR(a); + } + + return 0; +} + +static int dhcp_server_find_uplink(Link *link, Link **ret) { + assert(link); + + if (link->network->dhcp_server_uplink_name) + return link_get_by_name(link->manager, link->network->dhcp_server_uplink_name, ret); + + if (link->network->dhcp_server_uplink_index > 0) + return link_get_by_index(link->manager, link->network->dhcp_server_uplink_index, ret); + + if (link->network->dhcp_server_uplink_index == UPLINK_INDEX_AUTO) { + /* It is not necessary to propagate error in automatic selection. */ + if (manager_find_uplink(link->manager, AF_INET, link, ret) < 0) + *ret = NULL; + return 0; + } + + *ret = NULL; + return 0; +} + +static int link_push_uplink_to_dhcp_server( + Link *link, + sd_dhcp_lease_server_type_t what, + sd_dhcp_server *s) { + + _cleanup_free_ struct in_addr *addresses = NULL; + bool use_dhcp_lease_data = true; + size_t n_addresses = 0; + + assert(link); + + if (!link->network) + return 0; + assert(link->network); + + log_link_debug(link, "Copying %s from link", dhcp_lease_server_type_to_string(what)); + + switch (what) { + + case SD_DHCP_LEASE_DNS: + /* For DNS we have a special case. We the data configured explicitly locally along with the + * data from the DHCP lease. */ + + for (unsigned i = 0; i < link->network->n_dns; i++) { + struct in_addr ia; + + /* Only look for IPv4 addresses */ + if (link->network->dns[i]->family != AF_INET) + continue; + + ia = link->network->dns[i]->address.in; + + /* Never propagate obviously borked data */ + if (in4_addr_is_null(&ia) || in4_addr_is_localhost(&ia)) + continue; + + if (!GREEDY_REALLOC(addresses, n_addresses + 1)) + return log_oom(); + + addresses[n_addresses++] = ia; + } + + use_dhcp_lease_data = link->network->dhcp_use_dns; + break; + + case SD_DHCP_LEASE_NTP: { + /* For NTP things are similar, but for NTP hostnames can be configured too, which we cannot + * propagate via DHCP. Hence let's only propagate those which are IP addresses. */ + + STRV_FOREACH(i, link->network->ntp) { + union in_addr_union ia; + + if (in_addr_from_string(AF_INET, *i, &ia) < 0) + continue; + + /* Never propagate obviously borked data */ + if (in4_addr_is_null(&ia.in) || in4_addr_is_localhost(&ia.in)) + continue; + + if (!GREEDY_REALLOC(addresses, n_addresses + 1)) + return log_oom(); + + addresses[n_addresses++] = ia.in; + } + + use_dhcp_lease_data = link->network->dhcp_use_ntp; + break; + } + + case SD_DHCP_LEASE_SIP: + + /* For SIP we don't allow explicit, local configuration, but there's control whether to use the data */ + use_dhcp_lease_data = link->network->dhcp_use_sip; + break; + + case SD_DHCP_LEASE_POP3: + case SD_DHCP_LEASE_SMTP: + case SD_DHCP_LEASE_LPR: + /* For the other server types we currently do not allow local configuration of server data, + * since there are typically no local consumers of the data. */ + break; + + default: + assert_not_reached(); + } + + if (use_dhcp_lease_data && link->dhcp_lease) { + const struct in_addr *da; + + int n = sd_dhcp_lease_get_servers(link->dhcp_lease, what, &da); + if (n > 0) { + if (!GREEDY_REALLOC(addresses, n_addresses + n)) + return log_oom(); + + for (int j = 0; j < n; j++) + if (in4_addr_is_non_local(&da[j])) + addresses[n_addresses++] = da[j]; + } + } + + if (n_addresses <= 0) + return 0; + + return sd_dhcp_server_set_servers(s, what, addresses, n_addresses); +} + +static int dhcp4_server_parse_dns_server_string_and_warn( + const char *string, + struct in_addr **addresses, + size_t *n_addresses) { + + for (;;) { + _cleanup_free_ char *word = NULL, *server_name = NULL; + union in_addr_union address; + int family, r, ifindex = 0; + + r = extract_first_word(&string, &word, NULL, 0); + if (r < 0) + return r; + if (r == 0) + break; + + r = in_addr_ifindex_name_from_string_auto(word, &family, &address, &ifindex, &server_name); + if (r < 0) { + log_warning_errno(r, "Failed to parse DNS server address '%s', ignoring: %m", word); + continue; + } + + /* Only look for IPv4 addresses */ + if (family != AF_INET) + continue; + + /* Never propagate obviously borked data */ + if (in4_addr_is_null(&address.in) || in4_addr_is_localhost(&address.in)) + continue; + + if (!GREEDY_REALLOC(*addresses, *n_addresses + 1)) + return log_oom(); + + (*addresses)[(*n_addresses)++] = address.in; + } + + return 0; +} + +static int dhcp4_server_set_dns_from_resolve_conf(Link *link) { + _cleanup_free_ struct in_addr *addresses = NULL; + _cleanup_fclose_ FILE *f = NULL; + size_t n_addresses = 0; + int r; + + f = fopen(PRIVATE_UPLINK_RESOLV_CONF, "re"); + if (!f) { + if (errno == ENOENT) + return 0; + + return log_warning_errno(errno, "Failed to open " PRIVATE_UPLINK_RESOLV_CONF ": %m"); + } + + for (;;) { + _cleanup_free_ char *line = NULL; + const char *a; + + r = read_stripped_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; + + if (IN_SET(*line, '#', ';', 0)) + continue; + + a = first_word(line, "nameserver"); + if (!a) + continue; + + r = dhcp4_server_parse_dns_server_string_and_warn(a, &addresses, &n_addresses); + if (r < 0) + log_warning_errno(r, "Failed to parse DNS server address '%s', ignoring.", a); + } + + if (n_addresses <= 0) + return 0; + + return sd_dhcp_server_set_dns(link->dhcp_server, addresses, n_addresses); +} + +static int dhcp4_server_configure(Link *link) { + bool acquired_uplink = false; + sd_dhcp_option *p; + DHCPStaticLease *static_lease; + Link *uplink = NULL; + Address *address; + bool bind_to_interface; + int r; + + assert(link); + assert(link->network); + assert(link->network->dhcp_server_address); + + log_link_debug(link, "Configuring DHCP Server."); + + if (link->dhcp_server) + return -EBUSY; + + r = sd_dhcp_server_new(&link->dhcp_server, link->ifindex); + if (r < 0) + return r; + + r = sd_dhcp_server_attach_event(link->dhcp_server, link->manager->event, 0); + if (r < 0) + return r; + + r = sd_dhcp_server_set_callback(link->dhcp_server, dhcp_server_callback, link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to set callback for DHCPv4 server instance: %m"); + + r = address_get(link, link->network->dhcp_server_address, &address); + if (r < 0) + return log_link_error_errno(link, r, "Failed to find suitable address for DHCPv4 server instance: %m"); + + /* use the server address' subnet as the pool */ + r = sd_dhcp_server_configure_pool(link->dhcp_server, &address->in_addr.in, address->prefixlen, + link->network->dhcp_server_pool_offset, link->network->dhcp_server_pool_size); + if (r < 0) + return log_link_error_errno(link, r, "Failed to configure address pool for DHCPv4 server instance: %m"); + + if (link->network->dhcp_server_max_lease_time_usec > 0) { + r = sd_dhcp_server_set_max_lease_time(link->dhcp_server, link->network->dhcp_server_max_lease_time_usec); + 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, link->network->dhcp_server_default_lease_time_usec); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set default lease time for DHCPv4 server instance: %m"); + } + + r = sd_dhcp_server_set_ipv6_only_preferred_usec(link->dhcp_server, link->network->dhcp_server_ipv6_only_preferred_usec); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set IPv6 only preferred time for DHCPv4 server instance: %m"); + + r = sd_dhcp_server_set_boot_server_address(link->dhcp_server, &link->network->dhcp_server_boot_server_address); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to set boot server address for DHCPv4 server instance: %m"); + + r = sd_dhcp_server_set_boot_server_name(link->dhcp_server, link->network->dhcp_server_boot_server_name); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to set boot server name for DHCPv4 server instance: %m"); + + r = sd_dhcp_server_set_boot_filename(link->dhcp_server, link->network->dhcp_server_boot_filename); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to set boot filename for DHCPv4 server instance: %m"); + + r = sd_dhcp_server_set_rapid_commit(link->dhcp_server, link->network->dhcp_server_rapid_commit); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to %s Rapid Commit support for DHCPv4 server instance: %m", + enable_disable(link->network->dhcp_server_rapid_commit)); + + for (sd_dhcp_lease_server_type_t type = 0; type < _SD_DHCP_LEASE_SERVER_TYPE_MAX; type ++) { + + if (!link->network->dhcp_server_emit[type].emit) + continue; + + if (link->network->dhcp_server_emit[type].n_addresses > 0) + /* Explicitly specified servers to emit */ + r = sd_dhcp_server_set_servers( + link->dhcp_server, + type, + link->network->dhcp_server_emit[type].addresses, + link->network->dhcp_server_emit[type].n_addresses); + else { + /* Emission is requested, but nothing explicitly configured. Let's find a suitable upling */ + if (!acquired_uplink) { + (void) dhcp_server_find_uplink(link, &uplink); + acquired_uplink = true; + } + + if (uplink && uplink->network) + r = link_push_uplink_to_dhcp_server(uplink, type, link->dhcp_server); + else if (type == SD_DHCP_LEASE_DNS) + r = dhcp4_server_set_dns_from_resolve_conf(link); + else { + log_link_debug(link, + "Not emitting %s on link, couldn't find suitable uplink.", + dhcp_lease_server_type_to_string(type)); + continue; + } + } + + if (r < 0) + log_link_warning_errno(link, r, + "Failed to set %s for DHCP server, ignoring: %m", + dhcp_lease_server_type_to_string(type)); + } + + if (link->network->dhcp_server_emit_router) { + r = sd_dhcp_server_set_router(link->dhcp_server, &link->network->dhcp_server_router); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set router address for DHCP server: %m"); + } + + r = sd_dhcp_server_set_relay_target(link->dhcp_server, &link->network->dhcp_server_relay_target); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set relay target for DHCP server: %m"); + + bind_to_interface = sd_dhcp_server_is_in_relay_mode(link->dhcp_server) ? false : link->network->dhcp_server_bind_to_interface; + r = sd_dhcp_server_set_bind_to_interface(link->dhcp_server, bind_to_interface); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set interface binding for DHCP server: %m"); + + r = sd_dhcp_server_set_relay_agent_information(link->dhcp_server, link->network->dhcp_server_relay_agent_circuit_id, link->network->dhcp_server_relay_agent_remote_id); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set agent circuit/remote id for DHCP server: %m"); + + if (link->network->dhcp_server_emit_timezone) { + _cleanup_free_ char *buffer = NULL; + const char *tz = NULL; + + if (link->network->dhcp_server_timezone) + tz = link->network->dhcp_server_timezone; + else { + r = get_timezone(&buffer); + if (r < 0) + log_link_warning_errno(link, r, "Failed to determine timezone, not sending timezone: %m"); + else + tz = buffer; + } + + if (tz) { + r = sd_dhcp_server_set_timezone(link->dhcp_server, tz); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set timezone for DHCP server: %m"); + } + } + + ORDERED_HASHMAP_FOREACH(p, link->network->dhcp_server_send_options) { + r = sd_dhcp_server_add_option(link->dhcp_server, p); + if (r == -EEXIST) + continue; + if (r < 0) + return log_link_error_errno(link, r, "Failed to set DHCPv4 option: %m"); + } + + ORDERED_HASHMAP_FOREACH(p, link->network->dhcp_server_send_vendor_options) { + r = sd_dhcp_server_add_vendor_option(link->dhcp_server, p); + if (r == -EEXIST) + continue; + if (r < 0) + return log_link_error_errno(link, r, "Failed to set DHCPv4 option: %m"); + } + + HASHMAP_FOREACH(static_lease, link->network->dhcp_static_leases_by_section) { + r = sd_dhcp_server_set_static_lease(link->dhcp_server, &static_lease->address, static_lease->client_id, static_lease->client_id_size); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set DHCPv4 static lease for DHCP server: %m"); + } + + r = sd_dhcp_server_start(link->dhcp_server); + if (r < 0) + return log_link_error_errno(link, r, "Could not start DHCPv4 server instance: %m"); + + log_link_debug(link, "Offering DHCPv4 leases"); + return 0; +} + +static bool dhcp_server_is_ready_to_configure(Link *link) { + Link *uplink = NULL; + Address *a; + + assert(link); + assert(link->network); + assert(link->network->dhcp_server_address); + + if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) + return false; + + if (!link_has_carrier(link)) + return false; + + if (!link->static_addresses_configured) + return false; + + if (address_get(link, link->network->dhcp_server_address, &a) < 0) + return false; + + if (!address_is_ready(a)) + return false; + + if (dhcp_server_find_uplink(link, &uplink) < 0) + return false; + + if (uplink && !uplink->network) + return false; + + return true; +} + +static int dhcp_server_process_request(Request *req, Link *link, void *userdata) { + int r; + + assert(link); + + if (!dhcp_server_is_ready_to_configure(link)) + return 0; + + r = dhcp4_server_configure(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure DHCP server: %m"); + + return 1; +} + +int link_request_dhcp_server(Link *link) { + int r; + + assert(link); + + if (!link_dhcp4_server_enabled(link)) + return 0; + + if (link->dhcp_server) + return 0; + + log_link_debug(link, "Requesting DHCP server."); + r = link_queue_request(link, REQUEST_TYPE_DHCP_SERVER, dhcp_server_process_request, NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request configuration of DHCP server: %m"); + + return 0; +} + +int config_parse_dhcp_server_relay_agent_suboption( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + char **suboption_value = data; + char* p; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *suboption_value = mfree(*suboption_value); + return 0; + } + + p = startswith(rvalue, "string:"); + if (!p) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Failed to parse %s=%s'. Invalid format, ignoring.", lvalue, rvalue); + return 0; + } + return free_and_strdup(suboption_value, empty_to_null(p)); +} + +int config_parse_dhcp_server_emit( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + NetworkDHCPServerEmitAddress *emit = ASSERT_PTR(data); + + assert(rvalue); + + if (isempty(rvalue)) { + emit->addresses = mfree(emit->addresses); + emit->n_addresses = 0; + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *w = NULL; + union in_addr_union a; + int r; + + r = extract_first_word(&p, &w, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract word, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + if (streq(w, "_server_address")) + a = IN_ADDR_NULL; /* null address will be converted to the server address. */ + else { + r = in_addr_from_string(AF_INET, w, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s= address '%s', ignoring: %m", lvalue, w); + continue; + } + + if (in4_addr_is_null(&a.in)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Found a null address in %s=, ignoring.", lvalue); + continue; + } + } + + if (!GREEDY_REALLOC(emit->addresses, emit->n_addresses + 1)) + return log_oom(); + + emit->addresses[emit->n_addresses++] = a.in; + } +} + +int config_parse_dhcp_server_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + union in_addr_union a; + unsigned char prefixlen; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + network->dhcp_server_address_in_addr = (struct in_addr) {}; + network->dhcp_server_address_prefixlen = 0; + return 0; + } + + r = in_addr_prefix_from_string(rvalue, AF_INET, &a, &prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + if (in4_addr_is_localhost(&a.in) || in4_addr_is_link_local(&a.in)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "DHCP server address cannot be a localhost or link-local address, " + "ignoring assignment: %s", rvalue); + return 0; + } + + network->dhcp_server_address_in_addr = a.in; + network->dhcp_server_address_prefixlen = prefixlen; + return 0; +} + +int config_parse_dhcp_server_ipv6_only_preferred( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + usec_t t, *usec = ASSERT_PTR(data); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *usec = 0; + return 0; + } + + r = parse_sec(rvalue, &t); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse [%s] %s=, ignoring assignment: %s", section, lvalue, rvalue); + return 0; + } + + if (t < MIN_V6ONLY_WAIT_USEC && !network_test_mode_enabled()) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid [%s] %s=, ignoring assignment: %s", section, lvalue, rvalue); + return 0; + } + + *usec = t; + return 0; +} diff --git a/src/network/networkd-dhcp-server.h b/src/network/networkd-dhcp-server.h new file mode 100644 index 0000000..960232a --- /dev/null +++ b/src/network/networkd-dhcp-server.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" +#include "set.h" + +typedef struct Link Link; +typedef struct Network Network; + +int network_adjust_dhcp_server(Network *network, Set **addresses); + +int link_request_dhcp_server(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_relay_agent_suboption); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_emit); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_address); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_ipv6_only_preferred); diff --git a/src/network/networkd-dhcp4-bus.c b/src/network/networkd-dhcp4-bus.c new file mode 100644 index 0000000..e00aa03 --- /dev/null +++ b/src/network/networkd-dhcp4-bus.c @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-dhcp-client.h" + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-util.h" +#include "dhcp-client-internal.h" +#include "networkd-dhcp4-bus.h" +#include "networkd-link-bus.h" +#include "networkd-manager.h" +#include "string-table.h" +#include "strv.h" + +static int property_get_dhcp_client_state( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Link *l = ASSERT_PTR(userdata); + sd_dhcp_client *c; + + assert(reply); + + c = l->dhcp_client; + if (!c) + return sd_bus_message_append(reply, "s", "disabled"); + + return sd_bus_message_append(reply, "s", dhcp_state_to_string(dhcp_client_get_state(c))); +} + +static int dhcp_client_emit_changed(Link *link, const char *property, ...) { + _cleanup_free_ char *path = NULL; + char **l; + + assert(link); + + if (sd_bus_is_ready(link->manager->bus) <= 0) + return 0; + + path = link_bus_path(link); + if (!path) + return log_oom(); + + l = strv_from_stdarg_alloca(property); + + return sd_bus_emit_properties_changed_strv( + link->manager->bus, + path, + "org.freedesktop.network1.DHCPv4Client", + l); +} + +int dhcp_client_callback_bus(sd_dhcp_client *c, int event, void *userdata) { + Link *l = ASSERT_PTR(userdata); + + return dhcp_client_emit_changed(l, "State", NULL); +} + +static const sd_bus_vtable dhcp_client_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("State", "s", property_get_dhcp_client_state, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + + SD_BUS_VTABLE_END +}; + +const BusObjectImplementation dhcp_client_object = { + "/org/freedesktop/network1/link", + "org.freedesktop.network1.DHCPv4Client", + .fallback_vtables = BUS_FALLBACK_VTABLES({dhcp_client_vtable, link_object_find}), + .node_enumerator = link_node_enumerator, +}; diff --git a/src/network/networkd-dhcp4-bus.h b/src/network/networkd-dhcp4-bus.h new file mode 100644 index 0000000..482e824 --- /dev/null +++ b/src/network/networkd-dhcp4-bus.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-dhcp-client.h" + +#include "networkd-link-bus.h" + +extern const BusObjectImplementation dhcp_client_object; + +int dhcp_client_callback_bus(sd_dhcp_client *client, int event, void *userdata); diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c new file mode 100644 index 0000000..49c452d --- /dev/null +++ b/src/network/networkd-dhcp4.c @@ -0,0 +1,2025 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <netinet/ip.h> +#include <linux/if.h> +#include <linux/if_arp.h> + +#include "alloc-util.h" +#include "dhcp-client-internal.h" +#include "hostname-setup.h" +#include "hostname-util.h" +#include "parse-util.h" +#include "network-internal.h" +#include "networkd-address.h" +#include "networkd-dhcp-prefix-delegation.h" +#include "networkd-dhcp4-bus.h" +#include "networkd-dhcp4.h" +#include "networkd-ipv4acd.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-nexthop.h" +#include "networkd-queue.h" +#include "networkd-route.h" +#include "networkd-setlink.h" +#include "networkd-state-file.h" +#include "string-table.h" +#include "strv.h" +#include "sysctl-util.h" + +void network_adjust_dhcp4(Network *network) { + assert(network); + + if (!FLAGS_SET(network->dhcp, ADDRESS_FAMILY_IPV4)) + return; + + if (network->dhcp_use_gateway < 0) + network->dhcp_use_gateway = network->dhcp_use_routes; + + /* RFC7844 section 3.: MAY contain the Client Identifier option + * Section 3.5: clients MUST use client identifiers based solely on the link-layer address + * NOTE: Using MAC, as it does not reveal extra information, and some servers might not answer + * if this option is not sent */ + if (network->dhcp_anonymize && + network->dhcp_client_identifier >= 0 && + network->dhcp_client_identifier != DHCP_CLIENT_ID_MAC) { + log_warning("%s: ClientIdentifier= is set, although Anonymize=yes. Using ClientIdentifier=mac.", + network->filename); + network->dhcp_client_identifier = DHCP_CLIENT_ID_MAC; + } + + if (network->dhcp_client_identifier < 0) + network->dhcp_client_identifier = network->dhcp_anonymize ? DHCP_CLIENT_ID_MAC : DHCP_CLIENT_ID_DUID; + + /* By default, RapidCommit= is enabled when Anonymize=no and neither AllowList= nor DenyList= is specified. */ + if (network->dhcp_use_rapid_commit < 0) + network->dhcp_use_rapid_commit = + !network->dhcp_anonymize && + set_isempty(network->dhcp_allow_listed_ip) && + set_isempty(network->dhcp_deny_listed_ip); +} + +static int dhcp4_prefix_covers( + Link *link, + const struct in_addr *in_prefix, + uint8_t in_prefixlen) { + + struct in_addr prefix; + uint8_t prefixlen; + int r; + + assert(link); + assert(link->dhcp_lease); + assert(in_prefix); + + /* Return true if the input address or address range is in the assigned network. + * E.g. if the DHCP server provides 192.168.0.100/24, then this returns true for the address or + * address range in 192.168.0.0/24, and returns false otherwise. */ + + r = sd_dhcp_lease_get_prefix(link->dhcp_lease, &prefix, &prefixlen); + if (r < 0) + return r; + + return in4_addr_prefix_covers_full(&prefix, prefixlen, in_prefix, in_prefixlen); +} + +static int dhcp4_get_router(Link *link, struct in_addr *ret) { + const struct in_addr *routers; + int r; + + assert(link); + assert(link->dhcp_lease); + assert(ret); + + r = sd_dhcp_lease_get_router(link->dhcp_lease, &routers); + if (r < 0) + return r; + + /* The router option may provide multiple routers, We only use the first non-null address. */ + + FOREACH_ARRAY(router, routers, r) { + if (in4_addr_is_null(router)) + continue; + + *ret = *router; + return 0; + } + + return -ENODATA; +} + +static int dhcp4_get_classless_static_or_static_routes(Link *link, sd_dhcp_route ***ret_routes, size_t *ret_num) { + _cleanup_free_ sd_dhcp_route **routes = NULL; + int r; + + assert(link); + assert(link->dhcp_lease); + + /* 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. */ + + r = sd_dhcp_lease_get_classless_routes(link->dhcp_lease, &routes); + if (r >= 0) { + assert(r > 0); + if (ret_routes) + *ret_routes = TAKE_PTR(routes); + if (ret_num) + *ret_num = r; + return 1; /* classless */ + } else if (r != -ENODATA) + return r; + + r = sd_dhcp_lease_get_static_routes(link->dhcp_lease, &routes); + if (r < 0) + return r; + + assert(r > 0); + if (ret_routes) + *ret_routes = TAKE_PTR(routes); + if (ret_num) + *ret_num = r; + return 0; /* static */ +} + +static int dhcp4_find_gateway_for_destination( + Link *link, + const struct in_addr *destination, + uint8_t prefixlength, + bool allow_null, + struct in_addr *ret) { + + _cleanup_free_ sd_dhcp_route **routes = NULL; + size_t n_routes = 0; + bool is_classless, reachable; + uint8_t max_prefixlen = UINT8_MAX; + struct in_addr gw; + int r; + + assert(link); + assert(link->dhcp_lease); + assert(destination); + assert(ret); + + /* This tries to find the most suitable gateway for an address or address range. + * E.g. if the server provides the default gateway 192.168.0.1 and a classless static route for + * 8.0.0.0/8 with gateway 192.168.0.2, then this returns 192.168.0.2 for 8.8.8.8/32, and 192.168.0.1 + * for 9.9.9.9/32. If 'allow_null' flag is set, and the input address or address range is in the + * assigned network, then the default gateway will be ignored and the null address will be returned + * unless a matching non-default gateway found. */ + + r = dhcp4_prefix_covers(link, destination, prefixlength); + if (r < 0) + return r; + reachable = r > 0; + + r = dhcp4_get_classless_static_or_static_routes(link, &routes, &n_routes); + if (r < 0 && r != -ENODATA) + return r; + is_classless = r > 0; + + /* First, find most suitable gateway. */ + FOREACH_ARRAY(e, routes, n_routes) { + struct in_addr dst; + uint8_t len; + + r = sd_dhcp_route_get_destination(*e, &dst); + if (r < 0) + return r; + + r = sd_dhcp_route_get_destination_prefix_length(*e, &len); + if (r < 0) + return r; + + r = in4_addr_prefix_covers_full(&dst, len, destination, prefixlength); + if (r < 0) + return r; + if (r == 0) + continue; + + if (max_prefixlen != UINT8_MAX && max_prefixlen > len) + continue; + + r = sd_dhcp_route_get_gateway(*e, &gw); + if (r < 0) + return r; + + max_prefixlen = len; + } + + /* Found a suitable gateway in classless static routes or static routes. */ + if (max_prefixlen != UINT8_MAX) { + if (max_prefixlen == 0 && reachable && allow_null) + /* Do not return the default gateway, if the destination is in the assigned network. */ + *ret = (struct in_addr) {}; + else + *ret = gw; + return 0; + } + + /* When the destination is in the assigned network, return the null address if allowed. */ + if (reachable && allow_null) { + *ret = (struct in_addr) {}; + return 0; + } + + /* According to RFC 3442: If the DHCP server returns both a Classless Static Routes option and + * a Router option, the DHCP client MUST ignore the Router option. */ + if (!is_classless) { + r = dhcp4_get_router(link, ret); + if (r >= 0) + return 0; + if (r != -ENODATA) + return r; + } + + if (!reachable) + return -EHOSTUNREACH; /* Not in the same network, cannot reach the destination. */ + + assert(!allow_null); + return -ENODATA; /* No matching gateway found. */ +} + +static int dhcp4_remove_address_and_routes(Link *link, bool only_marked) { + Address *address; + Route *route; + int k, r = 0; + + assert(link); + + SET_FOREACH(route, link->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_DHCP4) + continue; + if (only_marked && !route_is_marked(route)) + continue; + + k = route_remove(route); + if (k < 0) + r = k; + + route_cancel_request(route, link); + } + + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_DHCP4) + continue; + if (only_marked && !address_is_marked(address)) + continue; + + k = address_remove_and_drop(address); + if (k < 0) + r = k; + } + + return r; +} + +static int dhcp4_address_get(Link *link, Address **ret) { + Address *address; + + assert(link); + + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_DHCP4) + continue; + if (address_is_marked(address)) + continue; + + if (ret) + *ret = address; + return 0; + } + + return -ENOENT; +} + +static int dhcp4_address_ready_callback(Address *address) { + assert(address); + assert(address->link); + + /* Do not call this again. */ + address->callback = NULL; + + return dhcp4_check_ready(address->link); +} + +int dhcp4_check_ready(Link *link) { + Address *address; + int r; + + assert(link); + + if (link->dhcp4_messages > 0) { + log_link_debug(link, "%s(): DHCPv4 address and routes are not set.", __func__); + return 0; + } + + if (dhcp4_address_get(link, &address) < 0) { + log_link_debug(link, "%s(): DHCPv4 address is not set.", __func__); + return 0; + } + + if (!address_is_ready(address)) { + log_link_debug(link, "%s(): DHCPv4 address is not ready.", __func__); + address->callback = dhcp4_address_ready_callback; + return 0; + } + + link->dhcp4_configured = true; + log_link_debug(link, "DHCPv4 address and routes set."); + + /* New address and routes are configured now. Let's release old lease. */ + r = dhcp4_remove_address_and_routes(link, /* only_marked = */ true); + if (r < 0) + return r; + + r = sd_ipv4ll_stop(link->ipv4ll); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to drop IPv4 link-local address: %m"); + + link_check_ready(link); + return 0; +} + +static int dhcp4_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not set DHCPv4 route"); + link_enter_failed(link); + return 1; + } + + r = dhcp4_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static int dhcp4_request_route(Route *in, Link *link) { + _cleanup_(route_freep) Route *route = in; + struct in_addr server; + Route *existing; + int r; + + assert(route); + assert(link); + assert(link->network); + assert(link->dhcp_lease); + + r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &server); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to get DHCP server IP address: %m"); + + route->source = NETWORK_CONFIG_SOURCE_DHCP4; + route->provider.in = server; + route->family = AF_INET; + if (!route->protocol_set) + route->protocol = RTPROT_DHCP; + if (!route->priority_set) + route->priority = link->network->dhcp_route_metric; + if (!route->table_set) + route->table = link_get_dhcp4_route_table(link); + if (route->mtu == 0) + route->mtu = link->network->dhcp_route_mtu; + if (route->quickack < 0) + route->quickack = link->network->dhcp_quickack; + if (route->initcwnd == 0) + route->initcwnd = link->network->dhcp_initial_congestion_window; + if (route->initrwnd == 0) + route->initrwnd = link->network->dhcp_advertised_receive_window; + + if (route_get(NULL, link, route, &existing) < 0) /* This is a new route. */ + link->dhcp4_configured = false; + else + route_unmark(existing); + + return link_request_route(link, TAKE_PTR(route), true, &link->dhcp4_messages, + dhcp4_route_handler, NULL); +} + +static bool link_prefixroute(Link *link) { + return !link->network->dhcp_route_table_set || + link->network->dhcp_route_table == RT_TABLE_MAIN; +} + +static int dhcp4_request_prefix_route(Link *link) { + _cleanup_(route_freep) Route *route = NULL; + int r; + + assert(link); + assert(link->dhcp_lease); + + if (link_prefixroute(link)) + /* When true, the route will be created by kernel. See dhcp4_update_address(). */ + return 0; + + r = route_new(&route); + if (r < 0) + return r; + + route->scope = RT_SCOPE_LINK; + + r = sd_dhcp_lease_get_prefix(link->dhcp_lease, &route->dst.in, &route->dst_prefixlen); + if (r < 0) + return r; + + r = sd_dhcp_lease_get_address(link->dhcp_lease, &route->prefsrc.in); + if (r < 0) + return r; + + return dhcp4_request_route(TAKE_PTR(route), link); +} + +static int dhcp4_request_route_to_gateway(Link *link, const struct in_addr *gw) { + _cleanup_(route_freep) Route *route = NULL; + struct in_addr address; + int r; + + assert(link); + assert(link->dhcp_lease); + assert(gw); + + r = sd_dhcp_lease_get_address(link->dhcp_lease, &address); + if (r < 0) + return r; + + r = route_new(&route); + if (r < 0) + return r; + + route->dst.in = *gw; + route->dst_prefixlen = 32; + route->prefsrc.in = address; + route->scope = RT_SCOPE_LINK; + + return dhcp4_request_route(TAKE_PTR(route), link); +} + +static int dhcp4_request_route_auto( + Route *in, + Link *link, + const struct in_addr *gw) { + + _cleanup_(route_freep) Route *route = in; + struct in_addr address; + int r; + + assert(route); + assert(link); + assert(link->dhcp_lease); + assert(gw); + + r = sd_dhcp_lease_get_address(link->dhcp_lease, &address); + if (r < 0) + return r; + + if (in4_addr_is_localhost(&route->dst.in)) { + if (in4_addr_is_set(gw)) + log_link_debug(link, "DHCP: requested route destination "IPV4_ADDRESS_FMT_STR"/%u is localhost, " + "ignoring gateway address "IPV4_ADDRESS_FMT_STR, + IPV4_ADDRESS_FMT_VAL(route->dst.in), route->dst_prefixlen, IPV4_ADDRESS_FMT_VAL(*gw)); + + route->scope = RT_SCOPE_HOST; + route->gw_family = AF_UNSPEC; + route->gw = IN_ADDR_NULL; + route->prefsrc = IN_ADDR_NULL; + + } else if (in4_addr_equal(&route->dst.in, &address)) { + if (in4_addr_is_set(gw)) + log_link_debug(link, "DHCP: requested route destination "IPV4_ADDRESS_FMT_STR"/%u is equivalent to the acquired address, " + "ignoring gateway address "IPV4_ADDRESS_FMT_STR, + IPV4_ADDRESS_FMT_VAL(route->dst.in), route->dst_prefixlen, IPV4_ADDRESS_FMT_VAL(*gw)); + + route->scope = RT_SCOPE_HOST; + route->gw_family = AF_UNSPEC; + route->gw = IN_ADDR_NULL; + route->prefsrc.in = address; + + } else if (in4_addr_is_null(gw)) { + r = dhcp4_prefix_covers(link, &route->dst.in, route->dst_prefixlen); + if (r < 0) + return r; + if (r == 0 && DEBUG_LOGGING) { + struct in_addr prefix; + uint8_t prefixlen; + + r = sd_dhcp_lease_get_prefix(link->dhcp_lease, &prefix, &prefixlen); + if (r < 0) + return r; + + log_link_debug(link, "DHCP: requested route destination "IPV4_ADDRESS_FMT_STR"/%u is not in the assigned network " + IPV4_ADDRESS_FMT_STR"/%u, but no gateway is specified, using 'link' scope.", + IPV4_ADDRESS_FMT_VAL(route->dst.in), route->dst_prefixlen, + IPV4_ADDRESS_FMT_VAL(prefix), prefixlen); + } + + route->scope = RT_SCOPE_LINK; + route->gw_family = AF_UNSPEC; + route->gw = IN_ADDR_NULL; + route->prefsrc.in = address; + + } else { + r = dhcp4_request_route_to_gateway(link, gw); + if (r < 0) + return r; + + route->scope = RT_SCOPE_UNIVERSE; + route->gw_family = AF_INET; + route->gw.in = *gw; + route->prefsrc.in = address; + } + + return dhcp4_request_route(TAKE_PTR(route), link); +} + +static int dhcp4_request_classless_static_or_static_routes(Link *link) { + _cleanup_free_ sd_dhcp_route **routes = NULL; + size_t n_routes; + int r; + + assert(link); + assert(link->dhcp_lease); + + if (!link->network->dhcp_use_routes) + return 0; + + r = dhcp4_get_classless_static_or_static_routes(link, &routes, &n_routes); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + FOREACH_ARRAY(e, routes, n_routes) { + _cleanup_(route_freep) Route *route = NULL; + struct in_addr gw; + + r = route_new(&route); + if (r < 0) + return r; + + r = sd_dhcp_route_get_gateway(*e, &gw); + if (r < 0) + return r; + + r = sd_dhcp_route_get_destination(*e, &route->dst.in); + if (r < 0) + return r; + + r = sd_dhcp_route_get_destination_prefix_length(*e, &route->dst_prefixlen); + if (r < 0) + return r; + + r = dhcp4_request_route_auto(TAKE_PTR(route), link, &gw); + if (r < 0) + return r; + } + + return 0; +} + +static int dhcp4_request_default_gateway(Link *link) { + _cleanup_(route_freep) Route *route = NULL; + struct in_addr address, router; + int r; + + assert(link); + assert(link->dhcp_lease); + + if (!link->network->dhcp_use_gateway) + return 0; + + /* According to RFC 3442: If the DHCP server returns both a Classless Static Routes option and + * a Router option, the DHCP client MUST ignore the Router option. */ + if (link->network->dhcp_use_routes && + dhcp4_get_classless_static_or_static_routes(link, NULL, NULL) > 0) + return 0; + + r = sd_dhcp_lease_get_address(link->dhcp_lease, &address); + if (r < 0) + return r; + + r = dhcp4_get_router(link, &router); + if (r == -ENODATA) { + log_link_debug(link, "DHCP: No valid router address received from DHCP server."); + return 0; + } + if (r < 0) + return r; + + /* The dhcp netmask may mask out the gateway. First, add an explicit route for the gateway host + * so that we can route no matter the netmask or existing kernel route tables. */ + r = dhcp4_request_route_to_gateway(link, &router); + if (r < 0) + return r; + + r = route_new(&route); + if (r < 0) + return r; + + /* Next, add a default gateway. */ + route->gw_family = AF_INET; + route->gw.in = router; + route->prefsrc.in = address; + + return dhcp4_request_route(TAKE_PTR(route), link); +} + +static int dhcp4_request_semi_static_routes(Link *link) { + Route *rt; + int r; + + assert(link); + assert(link->dhcp_lease); + assert(link->network); + + HASHMAP_FOREACH(rt, link->network->routes_by_section) { + _cleanup_(route_freep) Route *route = NULL; + struct in_addr gw; + + if (!rt->gateway_from_dhcp_or_ra) + continue; + + if (rt->gw_family != AF_INET) + continue; + + assert(rt->family == AF_INET); + + r = dhcp4_find_gateway_for_destination(link, &rt->dst.in, rt->dst_prefixlen, /* allow_null = */ false, &gw); + if (IN_SET(r, -EHOSTUNREACH, -ENODATA)) { + log_link_debug_errno(link, r, "DHCP: Cannot find suitable gateway for destination %s of semi-static route, ignoring: %m", + IN4_ADDR_PREFIX_TO_STRING(&rt->dst.in, rt->dst_prefixlen)); + continue; + } + if (r < 0) + return r; + + r = dhcp4_request_route_to_gateway(link, &gw); + if (r < 0) + return r; + + r = route_dup(rt, &route); + if (r < 0) + return r; + + route->gw.in = gw; + + r = dhcp4_request_route(TAKE_PTR(route), link); + if (r < 0) + return r; + } + + return 0; +} + +static int dhcp4_request_routes_to_servers( + Link *link, + const struct in_addr *servers, + size_t n_servers) { + + int r; + + assert(link); + assert(link->dhcp_lease); + assert(link->network); + assert(servers || n_servers == 0); + + FOREACH_ARRAY(dst, servers, n_servers) { + _cleanup_(route_freep) Route *route = NULL; + struct in_addr gw; + + if (in4_addr_is_null(dst)) + continue; + + r = dhcp4_find_gateway_for_destination(link, dst, 32, /* allow_null = */ true, &gw); + if (r == -EHOSTUNREACH) { + log_link_debug_errno(link, r, "DHCP: Cannot find suitable gateway for destination %s, ignoring: %m", + IN4_ADDR_PREFIX_TO_STRING(dst, 32)); + continue; + } + if (r < 0) + return r; + + r = route_new(&route); + if (r < 0) + return r; + + route->dst.in = *dst; + route->dst_prefixlen = 32; + + r = dhcp4_request_route_auto(TAKE_PTR(route), link, &gw); + if (r < 0) + return r; + } + + return 0; +} + +static int dhcp4_request_routes_to_dns(Link *link) { + const struct in_addr *dns; + int r; + + assert(link); + assert(link->dhcp_lease); + assert(link->network); + + if (!link->network->dhcp_use_dns || + !link->network->dhcp_routes_to_dns) + return 0; + + r = sd_dhcp_lease_get_dns(link->dhcp_lease, &dns); + if (IN_SET(r, 0, -ENODATA)) + return 0; + if (r < 0) + return r; + + return dhcp4_request_routes_to_servers(link, dns, r); +} + +static int dhcp4_request_routes_to_ntp(Link *link) { + const struct in_addr *ntp; + int r; + + assert(link); + assert(link->dhcp_lease); + assert(link->network); + + if (!link->network->dhcp_use_ntp || + !link->network->dhcp_routes_to_ntp) + return 0; + + r = sd_dhcp_lease_get_ntp(link->dhcp_lease, &ntp); + if (IN_SET(r, 0, -ENODATA)) + return 0; + if (r < 0) + return r; + + return dhcp4_request_routes_to_servers(link, ntp, r); +} + +static int dhcp4_request_routes(Link *link) { + int r; + + assert(link); + assert(link->dhcp_lease); + + r = dhcp4_request_prefix_route(link); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: Could not request prefix route: %m"); + + r = dhcp4_request_default_gateway(link); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: Could not request default gateway: %m"); + + r = dhcp4_request_classless_static_or_static_routes(link); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: Could not request static routes: %m"); + + r = dhcp4_request_semi_static_routes(link); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: Could not request routes with Gateway=_dhcp4 setting: %m"); + + r = dhcp4_request_routes_to_dns(link); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: Could not request routes to DNS servers: %m"); + + r = dhcp4_request_routes_to_ntp(link); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: Could not request routes to NTP servers: %m"); + + return 0; +} + +static int dhcp_reset_mtu(Link *link) { + int r; + + assert(link); + + if (!link->network->dhcp_use_mtu) + return 0; + + r = link_request_to_set_mtu(link, link->original_mtu); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: Could not queue request to reset MTU: %m"); + + return 0; +} + +static int dhcp_reset_hostname(Link *link) { + const char *hostname; + int r; + + assert(link); + + if (!link->network->dhcp_use_hostname) + return 0; + + hostname = link->network->dhcp_hostname; + if (!hostname) + (void) sd_dhcp_lease_get_hostname(link->dhcp_lease, &hostname); + + if (!hostname) + return 0; + + /* If a hostname was set due to the lease, then unset it now. */ + r = manager_set_hostname(link->manager, NULL); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: Failed to reset transient hostname: %m"); + + return 0; +} + +int dhcp4_lease_lost(Link *link) { + int k, r = 0; + + assert(link); + assert(link->dhcp_lease); + assert(link->network); + + log_link_info(link, "DHCP lease lost"); + + link->dhcp4_configured = false; + + if (link->network->dhcp_use_6rd && + sd_dhcp_lease_has_6rd(link->dhcp_lease)) + dhcp4_pd_prefix_lost(link); + + k = dhcp4_remove_address_and_routes(link, /* only_marked = */ false); + if (k < 0) + r = k; + + k = dhcp_reset_mtu(link); + if (k < 0) + r = k; + + k = dhcp_reset_hostname(link); + if (k < 0) + r = k; + + link->dhcp_lease = sd_dhcp_lease_unref(link->dhcp_lease); + link_dirty(link); + + /* If one of the above failed. Do not request nexthops and routes. */ + if (r < 0) + return r; + + r = link_request_static_nexthops(link, true); + if (r < 0) + return r; + + return link_request_static_routes(link, true); +} + +static int dhcp4_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) { + int r; + + assert(link); + + r = address_configure_handler_internal(rtnl, m, link, "Could not set DHCPv4 address"); + if (r <= 0) + return r; + + r = dhcp4_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static int dhcp4_request_address(Link *link, bool announce) { + _cleanup_(address_freep) Address *addr = NULL; + struct in_addr address, server; + uint8_t prefixlen; + Address *existing; + usec_t lifetime_usec; + int r; + + assert(link); + assert(link->manager); + assert(link->network); + assert(link->dhcp_lease); + + r = sd_dhcp_lease_get_address(link->dhcp_lease, &address); + if (r < 0) + return log_link_warning_errno(link, r, "DHCP error: no address: %m"); + + r = sd_dhcp_lease_get_prefix(link->dhcp_lease, NULL, &prefixlen); + if (r < 0) + return log_link_warning_errno(link, r, "DHCP error: no netmask: %m"); + + r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &server); + if (r < 0) + return log_link_debug_errno(link, r, "DHCP error: failed to get DHCP server IP address: %m"); + + if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) { + r = sd_dhcp_lease_get_lifetime_timestamp(link->dhcp_lease, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "DHCP error: failed to get lifetime: %m"); + } else + lifetime_usec = USEC_INFINITY; + + if (announce) { + const struct in_addr *router; + + r = sd_dhcp_lease_get_router(link->dhcp_lease, &router); + if (r < 0 && r != -ENODATA) + return log_link_error_errno(link, r, "DHCP error: Could not get gateway: %m"); + + if (r > 0 && in4_addr_is_set(&router[0])) + log_struct(LOG_INFO, + LOG_LINK_INTERFACE(link), + LOG_LINK_MESSAGE(link, "DHCPv4 address "IPV4_ADDRESS_FMT_STR"/%u, gateway "IPV4_ADDRESS_FMT_STR" acquired from "IPV4_ADDRESS_FMT_STR, + IPV4_ADDRESS_FMT_VAL(address), + prefixlen, + IPV4_ADDRESS_FMT_VAL(router[0]), + IPV4_ADDRESS_FMT_VAL(server)), + "ADDRESS="IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(address), + "PREFIXLEN=%u", prefixlen, + "GATEWAY="IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(router[0])); + else + log_struct(LOG_INFO, + LOG_LINK_INTERFACE(link), + LOG_LINK_MESSAGE(link, "DHCPv4 address "IPV4_ADDRESS_FMT_STR"/%u acquired from "IPV4_ADDRESS_FMT_STR, + IPV4_ADDRESS_FMT_VAL(address), + prefixlen, + IPV4_ADDRESS_FMT_VAL(server)), + "ADDRESS="IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(address), + "PREFIXLEN=%u", prefixlen); + } + + r = address_new(&addr); + if (r < 0) + return log_oom(); + + addr->source = NETWORK_CONFIG_SOURCE_DHCP4; + addr->provider.in = server; + addr->family = AF_INET; + addr->in_addr.in.s_addr = address.s_addr; + addr->lifetime_preferred_usec = lifetime_usec; + addr->lifetime_valid_usec = lifetime_usec; + addr->prefixlen = prefixlen; + r = sd_dhcp_lease_get_broadcast(link->dhcp_lease, &addr->broadcast); + if (r < 0 && r != -ENODATA) + return log_link_warning_errno(link, r, "DHCP: failed to get broadcast address: %m"); + SET_FLAG(addr->flags, IFA_F_NOPREFIXROUTE, !link_prefixroute(link)); + addr->route_metric = link->network->dhcp_route_metric; + addr->duplicate_address_detection = link->network->dhcp_send_decline ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_NO; + + r = free_and_strdup_warn(&addr->label, link->network->dhcp_label); + if (r < 0) + return r; + + r = free_and_strdup_warn(&addr->netlabel, link->network->dhcp_netlabel); + if (r < 0) + return r; + + if (address_get(link, addr, &existing) < 0) /* The address is new. */ + link->dhcp4_configured = false; + else + address_unmark(existing); + + r = link_request_address(link, addr, &link->dhcp4_messages, + dhcp4_address_handler, NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request DHCPv4 address: %m"); + + return 0; +} + +static int dhcp4_request_address_and_routes(Link *link, bool announce) { + int r; + + assert(link); + + link_mark_addresses(link, NETWORK_CONFIG_SOURCE_DHCP4); + link_mark_routes(link, NETWORK_CONFIG_SOURCE_DHCP4); + + r = dhcp4_request_address(link, announce); + if (r < 0) + return r; + + r = dhcp4_request_routes(link); + if (r < 0) + return r; + + if (!link->dhcp4_configured) { + link_set_state(link, LINK_STATE_CONFIGURING); + link_check_ready(link); + } + + return 0; +} + +static int dhcp_lease_renew(sd_dhcp_client *client, Link *link) { + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *old_lease = NULL; + sd_dhcp_lease *lease; + int r; + + assert(link); + assert(link->network); + assert(client); + + r = sd_dhcp_client_get_lease(client, &lease); + if (r < 0) + return log_link_warning_errno(link, r, "DHCP error: no lease: %m"); + + old_lease = TAKE_PTR(link->dhcp_lease); + link->dhcp_lease = sd_dhcp_lease_ref(lease); + link_dirty(link); + + if (link->network->dhcp_use_6rd) { + if (sd_dhcp_lease_has_6rd(link->dhcp_lease)) { + r = dhcp4_pd_prefix_acquired(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to process 6rd option: %m"); + } else if (sd_dhcp_lease_has_6rd(old_lease)) + dhcp4_pd_prefix_lost(link); + } + + return dhcp4_request_address_and_routes(link, false); +} + +static int dhcp_lease_acquired(sd_dhcp_client *client, Link *link) { + sd_dhcp_lease *lease; + int r; + + assert(client); + assert(link); + + r = sd_dhcp_client_get_lease(client, &lease); + if (r < 0) + return log_link_error_errno(link, r, "DHCP error: No lease: %m"); + + sd_dhcp_lease_unref(link->dhcp_lease); + link->dhcp_lease = sd_dhcp_lease_ref(lease); + link_dirty(link); + + if (link->network->dhcp_use_mtu) { + uint16_t mtu; + + r = sd_dhcp_lease_get_mtu(lease, &mtu); + if (r >= 0) { + r = link_request_to_set_mtu(link, mtu); + if (r < 0) + log_link_error_errno(link, r, "Failed to set MTU to %" PRIu16 ": %m", mtu); + } + } + + if (link->network->dhcp_use_hostname) { + const char *dhcpname = NULL; + _cleanup_free_ char *hostname = NULL; + + if (link->network->dhcp_hostname) + dhcpname = link->network->dhcp_hostname; + else + (void) sd_dhcp_lease_get_hostname(lease, &dhcpname); + + if (dhcpname) { + r = shorten_overlong(dhcpname, &hostname); + if (r < 0) + log_link_warning_errno(link, r, "Unable to shorten overlong DHCP hostname '%s', ignoring: %m", dhcpname); + if (r == 1) + log_link_notice(link, "Overlong DHCP hostname received, shortened from '%s' to '%s'", dhcpname, hostname); + } + + if (hostname) { + r = manager_set_hostname(link->manager, hostname); + if (r < 0) + log_link_error_errno(link, r, "Failed to set transient hostname to '%s': %m", hostname); + } + } + + if (link->network->dhcp_use_timezone) { + const char *tz = NULL; + + (void) sd_dhcp_lease_get_timezone(link->dhcp_lease, &tz); + + if (tz) { + r = manager_set_timezone(link->manager, tz); + if (r < 0) + log_link_error_errno(link, r, "Failed to set timezone to '%s': %m", tz); + } + } + + if (link->network->dhcp_use_6rd && + sd_dhcp_lease_has_6rd(link->dhcp_lease)) { + r = dhcp4_pd_prefix_acquired(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to process 6rd option: %m"); + } + + return dhcp4_request_address_and_routes(link, true); +} + +static int dhcp_lease_ip_change(sd_dhcp_client *client, Link *link) { + int r; + + r = dhcp_lease_acquired(client, link); + if (r < 0) + (void) dhcp4_lease_lost(link); + + return r; +} + +static int dhcp_server_is_filtered(Link *link, sd_dhcp_client *client) { + sd_dhcp_lease *lease; + struct in_addr addr; + int r; + + assert(link); + assert(link->network); + assert(client); + + r = sd_dhcp_client_get_lease(client, &lease); + if (r < 0) + return log_link_error_errno(link, r, "Failed to get DHCP lease: %m"); + + r = sd_dhcp_lease_get_server_identifier(lease, &addr); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to get DHCP server IP address: %m"); + + if (in4_address_is_filtered(&addr, link->network->dhcp_allow_listed_ip, link->network->dhcp_deny_listed_ip)) { + if (DEBUG_LOGGING) { + if (link->network->dhcp_allow_listed_ip) + log_link_debug(link, "DHCPv4 server IP address "IPV4_ADDRESS_FMT_STR" not found in allow-list, ignoring offer.", + IPV4_ADDRESS_FMT_VAL(addr)); + else + log_link_debug(link, "DHCPv4 server IP address "IPV4_ADDRESS_FMT_STR" found in deny-list, ignoring offer.", + IPV4_ADDRESS_FMT_VAL(addr)); + } + + return true; + } + + return false; +} + +static int dhcp4_handler(sd_dhcp_client *client, int event, void *userdata) { + Link *link = ASSERT_PTR(userdata); + int r; + + assert(link->network); + assert(link->manager); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + switch (event) { + case SD_DHCP_CLIENT_EVENT_STOP: + if (link->ipv4ll) { + log_link_debug(link, "DHCP client is stopped. Acquiring IPv4 link-local address"); + + if (in4_addr_is_set(&link->network->ipv4ll_start_address)) { + r = sd_ipv4ll_set_address(link->ipv4ll, &link->network->ipv4ll_start_address); + if (r < 0) + return log_link_warning_errno(link, r, "Could not set IPv4 link-local start address: %m"); + } + + r = sd_ipv4ll_start(link->ipv4ll); + if (r < 0) + return log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m"); + } + + if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) { + log_link_notice(link, "DHCPv4 connection considered critical, ignoring request to reconfigure it."); + return 0; + } + + if (link->dhcp_lease) { + if (link->network->dhcp_send_release) { + r = sd_dhcp_client_send_release(client); + if (r < 0) + log_link_full_errno(link, + ERRNO_IS_DISCONNECT(r) ? LOG_DEBUG : LOG_WARNING, + r, "Failed to send DHCP RELEASE, ignoring: %m"); + } + + r = dhcp4_lease_lost(link); + if (r < 0) { + link_enter_failed(link); + return r; + } + } + + break; + case SD_DHCP_CLIENT_EVENT_EXPIRED: + if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) { + log_link_notice(link, "DHCPv4 connection considered critical, ignoring request to reconfigure it."); + return 0; + } + + if (link->dhcp_lease) { + r = dhcp4_lease_lost(link); + if (r < 0) { + link_enter_failed(link); + return r; + } + } + + break; + case SD_DHCP_CLIENT_EVENT_IP_CHANGE: + if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) { + log_link_notice(link, "DHCPv4 connection considered critical, ignoring request to reconfigure it."); + return 0; + } + + r = dhcp_lease_ip_change(client, link); + if (r < 0) { + link_enter_failed(link); + return r; + } + + break; + case SD_DHCP_CLIENT_EVENT_RENEW: + r = dhcp_lease_renew(client, link); + if (r < 0) { + link_enter_failed(link); + return r; + } + break; + case SD_DHCP_CLIENT_EVENT_IP_ACQUIRE: + r = dhcp_lease_acquired(client, link); + if (r < 0) { + link_enter_failed(link); + return r; + } + break; + case SD_DHCP_CLIENT_EVENT_SELECTING: + r = dhcp_server_is_filtered(link, client); + if (r < 0) { + link_enter_failed(link); + return r; + } + if (r > 0) + return -ENOMSG; + break; + + case SD_DHCP_CLIENT_EVENT_TRANSIENT_FAILURE: + if (link->ipv4ll && !sd_ipv4ll_is_running(link->ipv4ll)) { + log_link_debug(link, "Problems acquiring DHCP lease, acquiring IPv4 link-local address"); + + if (in4_addr_is_set(&link->network->ipv4ll_start_address)) { + r = sd_ipv4ll_set_address(link->ipv4ll, &link->network->ipv4ll_start_address); + if (r < 0) + return log_link_warning_errno(link, r, "Could not set IPv4 link-local start address: %m"); + } + + r = sd_ipv4ll_start(link->ipv4ll); + if (r < 0) + return log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m"); + } + break; + + default: + if (event < 0) + log_link_warning_errno(link, event, "DHCP error: Client failed: %m"); + else + log_link_warning(link, "DHCP unknown event: %i", event); + break; + } + + return 0; +} + +static int dhcp4_set_hostname(Link *link) { + _cleanup_free_ char *hostname = NULL; + const char *hn; + int r; + + assert(link); + + if (!link->network->dhcp_send_hostname) + hn = NULL; + else if (link->network->dhcp_hostname) + hn = link->network->dhcp_hostname; + else { + r = gethostname_strict(&hostname); + if (r < 0 && r != -ENXIO) /* ENXIO: no hostname set or hostname is "localhost" */ + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to get hostname: %m"); + + hn = hostname; + } + + r = sd_dhcp_client_set_hostname(link->dhcp_client, hn); + if (r == -EINVAL && hostname) + /* Ignore error when the machine's hostname is not suitable to send in DHCP packet. */ + log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set hostname from kernel hostname, ignoring: %m"); + else if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set hostname: %m"); + + return 0; +} + +static int dhcp4_set_client_identifier(Link *link) { + int r; + + assert(link); + assert(link->network); + assert(link->dhcp_client); + + switch (link->network->dhcp_client_identifier) { + case DHCP_CLIENT_ID_DUID: { + /* If configured, apply user specified DUID and IAID */ + const DUID *duid = link_get_dhcp4_duid(link); + + if (duid->raw_data_len == 0) + switch (duid->type) { + case DUID_TYPE_LLT: + r = sd_dhcp_client_set_iaid_duid_llt(link->dhcp_client, + link->network->dhcp_iaid_set, + link->network->dhcp_iaid, + duid->llt_time); + break; + case DUID_TYPE_LL: + r = sd_dhcp_client_set_iaid_duid_ll(link->dhcp_client, + link->network->dhcp_iaid_set, + link->network->dhcp_iaid); + break; + case DUID_TYPE_EN: + r = sd_dhcp_client_set_iaid_duid_en(link->dhcp_client, + link->network->dhcp_iaid_set, + link->network->dhcp_iaid); + break; + case DUID_TYPE_UUID: + r = sd_dhcp_client_set_iaid_duid_uuid(link->dhcp_client, + link->network->dhcp_iaid_set, + link->network->dhcp_iaid); + break; + default: + r = sd_dhcp_client_set_iaid_duid_raw(link->dhcp_client, + link->network->dhcp_iaid_set, + link->network->dhcp_iaid, + duid->type, NULL, 0); + } + else + r = sd_dhcp_client_set_iaid_duid_raw(link->dhcp_client, + link->network->dhcp_iaid_set, + link->network->dhcp_iaid, + duid->type, duid->raw_data, duid->raw_data_len); + if (r < 0) + return r; + break; + } + case DHCP_CLIENT_ID_MAC: { + const uint8_t *hw_addr = link->hw_addr.bytes; + size_t hw_addr_len = link->hw_addr.length; + + if (link->iftype == ARPHRD_INFINIBAND && hw_addr_len == INFINIBAND_ALEN) { + /* set_client_id expects only last 8 bytes of an IB address */ + hw_addr += INFINIBAND_ALEN - 8; + hw_addr_len -= INFINIBAND_ALEN - 8; + } + + r = sd_dhcp_client_set_client_id(link->dhcp_client, + link->iftype, + hw_addr, + hw_addr_len); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set client ID: %m"); + break; + } + default: + assert_not_reached(); + } + + return 0; +} + +static int dhcp4_find_dynamic_address(Link *link, struct in_addr *ret) { + Address *a; + + assert(link); + assert(link->network); + assert(ret); + + if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) + return false; + + SET_FOREACH(a, link->addresses) { + if (a->source != NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + if (a->family != AF_INET) + continue; + if (link_address_is_dynamic(link, a)) + break; + } + + if (!a) + return false; + + *ret = a->in_addr.in; + return true; +} + +static int dhcp4_set_request_address(Link *link) { + struct in_addr a; + + assert(link); + assert(link->network); + assert(link->dhcp_client); + + a = link->network->dhcp_request_address; + + if (in4_addr_is_null(&a)) + (void) dhcp4_find_dynamic_address(link, &a); + + if (in4_addr_is_null(&a)) + return 0; + + log_link_debug(link, "DHCPv4 CLIENT: requesting %s.", IN4_ADDR_TO_STRING(&a)); + return sd_dhcp_client_set_request_address(link->dhcp_client, &a); +} + +static bool link_needs_dhcp_broadcast(Link *link) { + const char *val; + int r; + + assert(link); + assert(link->network); + + /* Return the setting in DHCP[4].RequestBroadcast if specified. Otherwise return the device property + * ID_NET_DHCP_BROADCAST setting, which may be set for interfaces requiring that the DHCPOFFER message + * is being broadcast because they can't handle unicast messages while not fully configured. + * If neither is set or a failure occurs, return false, which is the default for this flag. + */ + r = link->network->dhcp_broadcast; + if (r < 0 && link->dev && sd_device_get_property_value(link->dev, "ID_NET_DHCP_BROADCAST", &val) >= 0) { + r = parse_boolean(val); + if (r < 0) + log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to parse ID_NET_DHCP_BROADCAST, ignoring: %m"); + else + log_link_debug(link, "DHCPv4 CLIENT: Detected ID_NET_DHCP_BROADCAST='%d'.", r); + + } + return r == true; +} + +static bool link_dhcp4_ipv6_only_mode(Link *link) { + assert(link); + assert(link->network); + + /* If it is explicitly specified, then honor the setting. */ + if (link->network->dhcp_ipv6_only_mode >= 0) + return link->network->dhcp_ipv6_only_mode; + + /* Defaults to false, until we support 464XLAT. See issue #30891. */ + return false; +} + +static int dhcp4_configure(Link *link) { + sd_dhcp_option *send_option; + void *request_options; + int r; + + assert(link); + assert(link->network); + + if (link->dhcp_client) + return log_link_debug_errno(link, SYNTHETIC_ERRNO(EBUSY), "DHCPv4 client is already configured."); + + r = sd_dhcp_client_new(&link->dhcp_client, link->network->dhcp_anonymize); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to allocate DHCPv4 client: %m"); + + r = sd_dhcp_client_attach_event(link->dhcp_client, link->manager->event, 0); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to attach event to DHCPv4 client: %m"); + + r = sd_dhcp_client_attach_device(link->dhcp_client, link->dev); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to attach device: %m"); + + r = sd_dhcp_client_set_rapid_commit(link->dhcp_client, link->network->dhcp_use_rapid_commit); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set rapid commit: %m"); + + r = sd_dhcp_client_set_mac(link->dhcp_client, + link->hw_addr.bytes, + link->bcast_addr.length > 0 ? link->bcast_addr.bytes : NULL, + link->hw_addr.length, link->iftype); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set MAC address: %m"); + + r = sd_dhcp_client_set_ifindex(link->dhcp_client, link->ifindex); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set ifindex: %m"); + + r = sd_dhcp_client_set_callback(link->dhcp_client, dhcp4_handler, link); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set callback: %m"); + + r = sd_dhcp_client_set_request_broadcast(link->dhcp_client, link_needs_dhcp_broadcast(link)); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for broadcast: %m"); + + r = dhcp_client_set_state_callback(link->dhcp_client, dhcp_client_callback_bus, link); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set state change callback: %m"); + + if (link->mtu > 0) { + r = sd_dhcp_client_set_mtu(link->dhcp_client, link->mtu); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set MTU: %m"); + } + + if (!link->network->dhcp_anonymize) { + r = dhcp4_set_request_address(link); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set initial DHCPv4 address: %m"); + + if (link->network->dhcp_use_mtu) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_MTU_INTERFACE); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for MTU: %m"); + } + + if (link->network->dhcp_use_routes) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_STATIC_ROUTE); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for static route: %m"); + + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for classless static route: %m"); + } + + if (link->network->dhcp_use_domains != DHCP_USE_DOMAINS_NO) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_DOMAIN_SEARCH); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for domain search list: %m"); + } + + if (link->network->dhcp_use_ntp) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_NTP_SERVER); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for NTP server: %m"); + } + + if (link->network->dhcp_use_sip) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_SIP_SERVER); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for SIP server: %m"); + } + if (link->network->dhcp_use_captive_portal) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for captive portal: %m"); + } + + if (link->network->dhcp_use_timezone) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_TZDB_TIMEZONE); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for timezone: %m"); + } + + if (link->network->dhcp_use_6rd) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_6RD); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for 6rd: %m"); + } + + if (link_dhcp4_ipv6_only_mode(link)) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_IPV6_ONLY_PREFERRED); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for IPv6-only preferred option: %m"); + } + + SET_FOREACH(request_options, link->network->dhcp_request_options) { + uint32_t option = PTR_TO_UINT32(request_options); + + r = sd_dhcp_client_set_request_option(link->dhcp_client, option); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for '%u': %m", option); + } + + ORDERED_HASHMAP_FOREACH(send_option, link->network->dhcp_client_send_options) { + r = sd_dhcp_client_add_option(link->dhcp_client, send_option); + if (r == -EEXIST) + continue; + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set send option: %m"); + } + + ORDERED_HASHMAP_FOREACH(send_option, link->network->dhcp_client_send_vendor_options) { + r = sd_dhcp_client_add_vendor_option(link->dhcp_client, send_option); + if (r == -EEXIST) + continue; + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set send option: %m"); + } + + r = dhcp4_set_hostname(link); + if (r < 0) + return r; + + if (link->network->dhcp_vendor_class_identifier) { + r = sd_dhcp_client_set_vendor_class_identifier(link->dhcp_client, + link->network->dhcp_vendor_class_identifier); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set vendor class identifier: %m"); + } + + if (link->network->dhcp_mudurl) { + r = sd_dhcp_client_set_mud_url(link->dhcp_client, link->network->dhcp_mudurl); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set MUD URL: %m"); + } + + if (link->network->dhcp_user_class) { + r = sd_dhcp_client_set_user_class(link->dhcp_client, link->network->dhcp_user_class); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set user class: %m"); + } + } + + if (link->network->dhcp_client_port > 0) { + r = sd_dhcp_client_set_client_port(link->dhcp_client, link->network->dhcp_client_port); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set listen port: %m"); + } + + if (link->network->dhcp_max_attempts > 0) { + r = sd_dhcp_client_set_max_attempts(link->dhcp_client, link->network->dhcp_max_attempts); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set max attempts: %m"); + } + + if (link->network->dhcp_ip_service_type >= 0) { + r = sd_dhcp_client_set_service_type(link->dhcp_client, link->network->dhcp_ip_service_type); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set IP service type: %m"); + } + + if (link->network->dhcp_socket_priority_set) { + r = sd_dhcp_client_set_socket_priority(link->dhcp_client, link->network->dhcp_socket_priority); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set socket priority: %m"); + } + + if (link->network->dhcp_fallback_lease_lifetime_usec > 0) { + r = sd_dhcp_client_set_fallback_lease_lifetime(link->dhcp_client, link->network->dhcp_fallback_lease_lifetime_usec); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed set to lease lifetime: %m"); + } + + return dhcp4_set_client_identifier(link); +} + +int dhcp4_update_mac(Link *link) { + bool restart; + int r; + + assert(link); + + if (!link->dhcp_client) + return 0; + + restart = sd_dhcp_client_is_running(link->dhcp_client); + + r = sd_dhcp_client_stop(link->dhcp_client); + if (r < 0) + return r; + + r = sd_dhcp_client_set_mac(link->dhcp_client, + link->hw_addr.bytes, + link->bcast_addr.length > 0 ? link->bcast_addr.bytes : NULL, + link->hw_addr.length, link->iftype); + if (r < 0) + return r; + + r = dhcp4_set_client_identifier(link); + if (r < 0) + return r; + + if (restart) { + r = dhcp4_start(link); + if (r < 0) + return r; + } + + return 0; +} + +int dhcp4_update_ipv6_connectivity(Link *link) { + assert(link); + + if (!link->network) + return 0; + + if (!link->network->dhcp_ipv6_only_mode) + return 0; + + if (!link->dhcp_client) + return 0; + + /* If the client is running, set the current connectivity. */ + if (sd_dhcp_client_is_running(link->dhcp_client)) + return sd_dhcp_client_set_ipv6_connectivity(link->dhcp_client, link_has_ipv6_connectivity(link)); + + /* If the client has been already stopped or not started yet, let's check the current connectivity + * and start the client if necessary. */ + if (link_has_ipv6_connectivity(link)) + return 0; + + return dhcp4_start_full(link, /* set_ipv6_connectivity = */ false); +} + +int dhcp4_start_full(Link *link, bool set_ipv6_connectivity) { + int r; + + assert(link); + assert(link->network); + + if (!link->dhcp_client) + return 0; + + if (!link_has_carrier(link)) + return 0; + + if (sd_dhcp_client_is_running(link->dhcp_client) > 0) + return 0; + + r = sd_dhcp_client_start(link->dhcp_client); + if (r < 0) + return r; + + if (set_ipv6_connectivity) { + r = dhcp4_update_ipv6_connectivity(link); + if (r < 0) + return r; + } + + return 1; +} + +int dhcp4_renew(Link *link) { + assert(link); + + if (!link->dhcp_client) + return 0; + + /* The DHCPv4 client may have been stopped by the IPv6 only mode. Let's unconditionally restart the + * client if it is not running. */ + if (!sd_dhcp_client_is_running(link->dhcp_client)) + return dhcp4_start(link); + + /* The client may be waiting for IPv6 connectivity. Let's restart the client in that case. */ + if (dhcp_client_get_state(link->dhcp_client) != DHCP_STATE_BOUND) + return sd_dhcp_client_interrupt_ipv6_only_mode(link->dhcp_client); + + /* Otherwise, send a RENEW command. */ + return sd_dhcp_client_send_renew(link->dhcp_client); +} + +static int dhcp4_configure_duid(Link *link) { + assert(link); + assert(link->network); + + if (link->network->dhcp_client_identifier != DHCP_CLIENT_ID_DUID) + return 1; + + return dhcp_configure_duid(link, link_get_dhcp4_duid(link)); +} + +static int dhcp4_process_request(Request *req, Link *link, void *userdata) { + int r; + + assert(link); + + if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) + return 0; + + r = dhcp4_configure_duid(link); + if (r <= 0) + return r; + + r = dhcp4_configure(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure DHCPv4 client: %m"); + + r = dhcp4_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start DHCPv4 client: %m"); + + log_link_debug(link, "DHCPv4 client is configured%s.", + r > 0 ? ", acquiring DHCPv4 lease" : ""); + return 1; +} + +int link_request_dhcp4_client(Link *link) { + int r; + + assert(link); + + if (!link_dhcp4_enabled(link)) + return 0; + + if (link->dhcp_client) + return 0; + + r = link_queue_request(link, REQUEST_TYPE_DHCP4_CLIENT, dhcp4_process_request, NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request configuring of the DHCPv4 client: %m"); + + log_link_debug(link, "Requested configuring of the DHCPv4 client."); + return 0; +} + +int config_parse_dhcp_max_attempts( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(data); + uint64_t a; + int r; + + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + network->dhcp_max_attempts = 0; + return 0; + } + + if (streq(rvalue, "infinity")) { + network->dhcp_max_attempts = UINT64_MAX; + return 0; + } + + r = safe_atou64(rvalue, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DHCP maximum attempts, ignoring: %s", rvalue); + return 0; + } + + if (a == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "%s= must be positive integer or 'infinity', ignoring: %s", lvalue, rvalue); + return 0; + } + + network->dhcp_max_attempts = a; + + return 0; +} + +int config_parse_dhcp_ip_service_type( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + int *tos = ASSERT_PTR(data); + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) + *tos = -1; /* use sd_dhcp_client's default (currently, CS6). */ + else if (streq(rvalue, "none")) + *tos = 0; + else if (streq(rvalue, "CS4")) + *tos = IPTOS_CLASS_CS4; + else if (streq(rvalue, "CS6")) + *tos = IPTOS_CLASS_CS6; + else + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + + return 0; +} + +int config_parse_dhcp_socket_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 = ASSERT_PTR(data); + int a, r; + + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + network->dhcp_socket_priority_set = false; + return 0; + } + + r = safe_atoi(rvalue, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse socket priority, ignoring: %s", rvalue); + return 0; + } + + network->dhcp_socket_priority_set = true; + network->dhcp_socket_priority = a; + + return 0; +} + +int config_parse_dhcp_fallback_lease_lifetime( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + network->dhcp_fallback_lease_lifetime_usec = 0; + return 0; + } + + /* We accept only "forever" or "infinity". */ + if (!STR_IN_SET(rvalue, "forever", "infinity")) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid LeaseLifetime= value, ignoring: %s", rvalue); + return 0; + } + + network->dhcp_fallback_lease_lifetime_usec = USEC_INFINITY; + + return 0; +} + +int config_parse_dhcp_label( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + char **label = ASSERT_PTR(data); + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *label = mfree(*label); + return 0; + } + + if (!address_label_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Address label is too long or invalid, ignoring assignment: %s", rvalue); + return 0; + } + + return free_and_strdup_warn(label, rvalue); +} + +static const char* const dhcp_client_identifier_table[_DHCP_CLIENT_ID_MAX] = { + [DHCP_CLIENT_ID_MAC] = "mac", + [DHCP_CLIENT_ID_DUID] = "duid", +}; + +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..b3fe027 --- /dev/null +++ b/src/network/networkd-dhcp4.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" + +typedef struct Link Link; +typedef struct Network Network; + +typedef enum DHCPClientIdentifier { + DHCP_CLIENT_ID_MAC, + DHCP_CLIENT_ID_DUID, + _DHCP_CLIENT_ID_MAX, + _DHCP_CLIENT_ID_INVALID = -EINVAL, +} DHCPClientIdentifier; + +void network_adjust_dhcp4(Network *network); +int dhcp4_update_mac(Link *link); +int dhcp4_update_ipv6_connectivity(Link *link); +int dhcp4_start_full(Link *link, bool set_ipv6_connectivity); +static inline int dhcp4_start(Link *link) { + return dhcp4_start_full(link, true); +} +int dhcp4_renew(Link *link); +int dhcp4_lease_lost(Link *link); +int dhcp4_check_ready(Link *link); + +int link_request_dhcp4_client(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_client_identifier); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_max_attempts); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_ip_service_type); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_socket_priority); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_mud_url); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_fallback_lease_lifetime); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_label); diff --git a/src/network/networkd-dhcp6-bus.c b/src/network/networkd-dhcp6-bus.c new file mode 100644 index 0000000..a225877 --- /dev/null +++ b/src/network/networkd-dhcp6-bus.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-util.h" +#include "dhcp6-client-internal.h" +#include "dhcp6-protocol.h" +#include "networkd-dhcp6-bus.h" +#include "networkd-link-bus.h" +#include "networkd-manager.h" +#include "string-table.h" +#include "strv.h" + +static int property_get_dhcp6_client_state( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Link *l = ASSERT_PTR(userdata); + sd_dhcp6_client *c; + + assert(reply); + + c = l->dhcp6_client; + if (!c) + return sd_bus_message_append(reply, "s", "disabled"); + + return sd_bus_message_append(reply, "s", dhcp6_state_to_string(dhcp6_client_get_state(c))); +} + +static int dhcp6_client_emit_changed(Link *link, const char *property, ...) { + _cleanup_free_ char *path = NULL; + char **l; + + assert(link); + + if (sd_bus_is_ready(link->manager->bus) <= 0) + return 0; + + path = link_bus_path(link); + if (!path) + return log_oom(); + + l = strv_from_stdarg_alloca(property); + + return sd_bus_emit_properties_changed_strv( + link->manager->bus, + path, + "org.freedesktop.network1.DHCPv6Client", + l); +} + +void dhcp6_client_callback_bus(sd_dhcp6_client *c, int event, void *userdata) { + Link *l = ASSERT_PTR(userdata); + + dhcp6_client_emit_changed(l, "State", NULL); +} + +static const sd_bus_vtable dhcp6_client_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("State", "s", property_get_dhcp6_client_state, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + + SD_BUS_VTABLE_END +}; + +const BusObjectImplementation dhcp6_client_object = { + "/org/freedesktop/network1/link", + "org.freedesktop.network1.DHCPv6Client", + .fallback_vtables = BUS_FALLBACK_VTABLES({dhcp6_client_vtable, link_object_find}), + .node_enumerator = link_node_enumerator, +}; diff --git a/src/network/networkd-dhcp6-bus.h b/src/network/networkd-dhcp6-bus.h new file mode 100644 index 0000000..76a6b72 --- /dev/null +++ b/src/network/networkd-dhcp6-bus.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-dhcp6-client.h" + +#include "networkd-link-bus.h" + +extern const BusObjectImplementation dhcp6_client_object; + +void dhcp6_client_callback_bus(sd_dhcp6_client *client, int event, void *userdata); diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c new file mode 100644 index 0000000..f499d03 --- /dev/null +++ b/src/network/networkd-dhcp6.c @@ -0,0 +1,892 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include "dhcp6-client-internal.h" +#include "dhcp6-lease-internal.h" +#include "hashmap.h" +#include "hostname-setup.h" +#include "hostname-util.h" +#include "networkd-address.h" +#include "networkd-dhcp-prefix-delegation.h" +#include "networkd-dhcp6-bus.h" +#include "networkd-dhcp6.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-queue.h" +#include "networkd-route.h" +#include "networkd-state-file.h" +#include "string-table.h" +#include "string-util.h" + +bool link_dhcp6_with_address_enabled(Link *link) { + if (!link_dhcp6_enabled(link)) + return false; + + return link->network->dhcp6_use_address; +} + +static DHCP6ClientStartMode link_get_dhcp6_client_start_mode(Link *link) { + assert(link); + + if (!link->network) + return DHCP6_CLIENT_START_MODE_NO; + + /* When WithoutRA= is explicitly specified, then honor it. */ + if (link->network->dhcp6_client_start_mode >= 0) + return link->network->dhcp6_client_start_mode; + + /* When this interface itself is an uplink interface, then start dhcp6 client in solicit mode. */ + if (dhcp_pd_is_uplink(link, link, /* accept_auto = */ false)) + return DHCP6_CLIENT_START_MODE_SOLICIT; + + /* Otherwise, start dhcp6 client when RA is received. */ + return DHCP6_CLIENT_START_MODE_NO; +} + +static int dhcp6_remove(Link *link, bool only_marked) { + Address *address; + Route *route; + int k, r = 0; + + assert(link); + + if (!only_marked) + link->dhcp6_configured = false; + + SET_FOREACH(route, link->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_DHCP6) + continue; + if (only_marked && !route_is_marked(route)) + continue; + + k = route_remove(route); + if (k < 0) + r = k; + + route_cancel_request(route, link); + } + + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_DHCP6) + continue; + if (only_marked && !address_is_marked(address)) + continue; + + k = address_remove_and_drop(address); + if (k < 0) + r = k; + } + + return r; +} + +static int dhcp6_address_ready_callback(Address *address) { + Address *a; + + assert(address); + assert(address->link); + + SET_FOREACH(a, address->link->addresses) + if (a->source == NETWORK_CONFIG_SOURCE_DHCP6) + a->callback = NULL; + + return dhcp6_check_ready(address->link); +} + +int dhcp6_check_ready(Link *link) { + int r; + + assert(link); + assert(link->network); + + if (link->dhcp6_messages > 0) { + log_link_debug(link, "%s(): DHCPv6 addresses and routes are not set.", __func__); + return 0; + } + + if (link->network->dhcp6_use_address && + sd_dhcp6_lease_has_address(link->dhcp6_lease) && + !link_check_addresses_ready(link, NETWORK_CONFIG_SOURCE_DHCP6)) { + Address *address; + + SET_FOREACH(address, link->addresses) + if (address->source == NETWORK_CONFIG_SOURCE_DHCP6) + address->callback = dhcp6_address_ready_callback; + + log_link_debug(link, "%s(): no DHCPv6 address is ready.", __func__); + return 0; + } + + link->dhcp6_configured = true; + log_link_debug(link, "DHCPv6 addresses and routes set."); + + r = dhcp6_remove(link, /* only_marked = */ true); + if (r < 0) + return r; + + link_check_ready(link); + return 0; +} + +static int dhcp6_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) { + int r; + + assert(link); + + r = address_configure_handler_internal(rtnl, m, link, "Could not set DHCPv6 address"); + if (r <= 0) + return r; + + r = dhcp6_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static int verify_dhcp6_address(Link *link, const Address *address) { + bool by_ndisc = false; + Address *existing; + int log_level; + + assert(link); + assert(address); + assert(address->family == AF_INET6); + + const char *pretty = IN6_ADDR_TO_STRING(&address->in_addr.in6); + + if (address_get_harder(link, address, &existing) < 0) { + /* New address. */ + log_level = LOG_INFO; + goto simple_log; + } else + log_level = LOG_DEBUG; + + if (address->prefixlen == existing->prefixlen) + /* Currently, only conflict in prefix length is reported. */ + goto simple_log; + + if (existing->source == NETWORK_CONFIG_SOURCE_NDISC) + by_ndisc = true; + + log_link_warning(link, "Ignoring DHCPv6 address %s/%u (valid %s, preferred %s) which conflicts with %s/%u%s.", + pretty, address->prefixlen, + FORMAT_LIFETIME(address->lifetime_valid_usec), + FORMAT_LIFETIME(address->lifetime_preferred_usec), + pretty, existing->prefixlen, + by_ndisc ? " assigned by NDisc" : ""); + if (by_ndisc) + log_link_warning(link, "Hint: use IPv6Token= setting to change the address generated by NDisc or set UseAutonomousPrefix=no."); + + return -EEXIST; + +simple_log: + log_link_full(link, log_level, "DHCPv6 address %s/%u (valid %s, preferred %s)", + pretty, address->prefixlen, + FORMAT_LIFETIME(address->lifetime_valid_usec), + FORMAT_LIFETIME(address->lifetime_preferred_usec)); + return 0; +} + +static int dhcp6_request_address( + Link *link, + const struct in6_addr *server_address, + const struct in6_addr *ip6_addr, + usec_t lifetime_preferred_usec, + usec_t lifetime_valid_usec) { + + _cleanup_(address_freep) Address *addr = NULL; + Address *existing; + int r; + + r = address_new(&addr); + if (r < 0) + return log_oom(); + + addr->source = NETWORK_CONFIG_SOURCE_DHCP6; + addr->provider.in6 = *server_address; + addr->family = AF_INET6; + addr->in_addr.in6 = *ip6_addr; + addr->flags = IFA_F_NOPREFIXROUTE; + addr->prefixlen = 128; + addr->lifetime_preferred_usec = lifetime_preferred_usec; + addr->lifetime_valid_usec = lifetime_valid_usec; + + if (verify_dhcp6_address(link, addr) < 0) + return 0; + + r = free_and_strdup_warn(&addr->netlabel, link->network->dhcp6_netlabel); + if (r < 0) + return r; + + if (address_get(link, addr, &existing) < 0) + link->dhcp6_configured = false; + else + address_unmark(existing); + + r = link_request_address(link, addr, &link->dhcp6_messages, + dhcp6_address_handler, NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request DHCPv6 address %s/128: %m", + IN6_ADDR_TO_STRING(ip6_addr)); + return 0; +} + +static int dhcp6_address_acquired(Link *link) { + struct in6_addr server_address; + int r; + + assert(link); + assert(link->network); + assert(link->dhcp6_lease); + + if (!link->network->dhcp6_use_address) + return 0; + + r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &server_address); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get server address of DHCPv6 lease: %m"); + + FOREACH_DHCP6_ADDRESS(link->dhcp6_lease) { + usec_t lifetime_preferred_usec, lifetime_valid_usec; + struct in6_addr ip6_addr; + + r = sd_dhcp6_lease_get_address(link->dhcp6_lease, &ip6_addr); + if (r < 0) + return r; + + r = sd_dhcp6_lease_get_address_lifetime_timestamp(link->dhcp6_lease, CLOCK_BOOTTIME, + &lifetime_preferred_usec, &lifetime_valid_usec); + if (r < 0) + return r; + + r = dhcp6_request_address(link, &server_address, &ip6_addr, + lifetime_preferred_usec, + lifetime_valid_usec); + if (r < 0) + return r; + } + + if (link->network->dhcp6_use_hostname) { + const char *dhcpname = NULL; + _cleanup_free_ char *hostname = NULL; + + (void) sd_dhcp6_lease_get_fqdn(link->dhcp6_lease, &dhcpname); + + if (dhcpname) { + r = shorten_overlong(dhcpname, &hostname); + if (r < 0) + log_link_warning_errno(link, r, "Unable to shorten overlong DHCP hostname '%s', ignoring: %m", dhcpname); + if (r == 1) + log_link_notice(link, "Overlong DHCP hostname received, shortened from '%s' to '%s'", dhcpname, hostname); + } + if (hostname) { + r = manager_set_hostname(link->manager, hostname); + if (r < 0) + log_link_error_errno(link, r, "Failed to set transient hostname to '%s': %m", hostname); + } + } + + return 0; +} + +static int dhcp6_lease_ip_acquired(sd_dhcp6_client *client, Link *link) { + _cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease_old = NULL; + sd_dhcp6_lease *lease; + int r; + + link_mark_addresses(link, NETWORK_CONFIG_SOURCE_DHCP6); + link_mark_routes(link, NETWORK_CONFIG_SOURCE_DHCP6); + + r = sd_dhcp6_client_get_lease(client, &lease); + if (r < 0) + return log_link_error_errno(link, r, "Failed to get DHCPv6 lease: %m"); + + lease_old = TAKE_PTR(link->dhcp6_lease); + link->dhcp6_lease = sd_dhcp6_lease_ref(lease); + + r = dhcp6_address_acquired(link); + if (r < 0) + return r; + + if (sd_dhcp6_lease_has_pd_prefix(lease)) { + r = dhcp6_pd_prefix_acquired(link); + if (r < 0) + return r; + } else if (sd_dhcp6_lease_has_pd_prefix(lease_old)) + /* When we had PD prefixes but not now, we need to remove them. */ + dhcp_pd_prefix_lost(link); + + if (link->dhcp6_messages == 0) { + link->dhcp6_configured = true; + + r = dhcp6_remove(link, /* only_marked = */ true); + if (r < 0) + return r; + } else + log_link_debug(link, "Setting DHCPv6 addresses and routes"); + + if (!link->dhcp6_configured) + link_set_state(link, LINK_STATE_CONFIGURING); + + link_check_ready(link); + return 0; +} + +static int dhcp6_lease_information_acquired(sd_dhcp6_client *client, Link *link) { + sd_dhcp6_lease *lease; + int r; + + assert(client); + assert(link); + + r = sd_dhcp6_client_get_lease(client, &lease); + if (r < 0) + return log_link_error_errno(link, r, "Failed to get DHCPv6 lease: %m"); + + unref_and_replace_full(link->dhcp6_lease, lease, sd_dhcp6_lease_ref, sd_dhcp6_lease_unref); + + link_dirty(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 (sd_dhcp6_lease_has_pd_prefix(link->dhcp6_lease)) + dhcp_pd_prefix_lost(link); + + link->dhcp6_lease = sd_dhcp6_lease_unref(link->dhcp6_lease); + + r = dhcp6_remove(link, /* only_marked = */ false); + if (r < 0) + return r; + + return 0; +} + +static void dhcp6_handler(sd_dhcp6_client *client, int event, void *userdata) { + Link *link = ASSERT_PTR(userdata); + int r = 0; + + 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); + break; + + case SD_DHCP6_CLIENT_EVENT_IP_ACQUIRE: + r = dhcp6_lease_ip_acquired(client, link); + break; + + case SD_DHCP6_CLIENT_EVENT_INFORMATION_REQUEST: + r = dhcp6_lease_information_acquired(client, link); + break; + + default: + if (event < 0) + log_link_warning_errno(link, event, "DHCPv6 error, ignoring: %m"); + else + log_link_warning(link, "DHCPv6 unknown event: %d", event); + } + if (r < 0) + link_enter_failed(link); +} + +int dhcp6_start_on_ra(Link *link, bool information_request) { + int r; + + assert(link); + assert(link->dhcp6_client); + assert(link->network); + assert(in6_addr_is_link_local(&link->ipv6ll_address)); + + if (link_get_dhcp6_client_start_mode(link) != DHCP6_CLIENT_START_MODE_NO) + /* When WithoutRA= is specified, then the DHCPv6 client should be already running in + * the requested mode. Hence, ignore the requests by RA. */ + return 0; + + r = sd_dhcp6_client_is_running(link->dhcp6_client); + if (r < 0) + return r; + + if (r > 0) { + int inf_req; + + r = sd_dhcp6_client_get_information_request(link->dhcp6_client, &inf_req); + if (r < 0) + return r; + + if (inf_req == information_request) + /* The client is already running in the requested mode. */ + return 0; + + if (!inf_req) { + log_link_debug(link, + "The DHCPv6 client is already running in the managed mode, " + "refusing to start the client in the information requesting mode."); + return 0; + } + + log_link_debug(link, + "The DHCPv6 client is running in the information requesting mode. " + "Restarting the client in the managed mode."); + + r = sd_dhcp6_client_stop(link->dhcp6_client); + if (r < 0) + return r; + } else { + r = sd_dhcp6_client_set_local_address(link->dhcp6_client, &link->ipv6ll_address); + if (r < 0) + return r; + } + + r = sd_dhcp6_client_set_information_request(link->dhcp6_client, information_request); + if (r < 0) + return r; + + r = sd_dhcp6_client_start(link->dhcp6_client); + if (r < 0) + return r; + + return 0; +} + +int dhcp6_start(Link *link) { + DHCP6ClientStartMode start_mode; + int r; + + assert(link); + assert(link->network); + + if (!link->dhcp6_client) + return 0; + + if (!link_dhcp6_enabled(link)) + return 0; + + if (!link_has_carrier(link)) + return 0; + + if (sd_dhcp6_client_is_running(link->dhcp6_client) > 0) + return 0; + + if (!in6_addr_is_link_local(&link->ipv6ll_address)) { + log_link_debug(link, "IPv6 link-local address is not set, delaying to start DHCPv6 client."); + return 0; + } + + r = sd_dhcp6_client_set_local_address(link->dhcp6_client, &link->ipv6ll_address); + if (r < 0) + return r; + + start_mode = link_get_dhcp6_client_start_mode(link); + if (start_mode == DHCP6_CLIENT_START_MODE_NO) + return 0; + + r = sd_dhcp6_client_set_information_request(link->dhcp6_client, + start_mode == DHCP6_CLIENT_START_MODE_INFORMATION_REQUEST); + if (r < 0) + return r; + + r = sd_dhcp6_client_start(link->dhcp6_client); + if (r < 0) + return r; + + return 1; +} + +static int dhcp6_set_hostname(sd_dhcp6_client *client, Link *link) { + _cleanup_free_ char *hostname = NULL; + const char *hn; + int r; + + assert(link); + + if (!link->network->dhcp6_send_hostname) + hn = NULL; + else if (link->network->dhcp6_hostname) + hn = link->network->dhcp6_hostname; + else { + r = gethostname_strict(&hostname); + if (r < 0 && r != -ENXIO) /* ENXIO: no hostname set or hostname is "localhost" */ + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to get hostname: %m"); + + hn = hostname; + } + + r = sd_dhcp6_client_set_fqdn(client, hn); + if (r == -EINVAL && hostname) + /* Ignore error when the machine's hostname is not suitable to send in DHCP packet. */ + log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set hostname from kernel hostname, ignoring: %m"); + else if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set hostname: %m"); + + return 0; +} + +static int dhcp6_set_identifier(Link *link, sd_dhcp6_client *client) { + const DUID *duid; + int r; + + assert(link); + assert(link->network); + assert(client); + + r = sd_dhcp6_client_set_mac(client, link->hw_addr.bytes, link->hw_addr.length, link->iftype); + if (r < 0) + return r; + + if (link->network->dhcp6_iaid_set) { + r = sd_dhcp6_client_set_iaid(client, link->network->dhcp6_iaid); + if (r < 0) + return r; + } + + duid = link_get_dhcp6_duid(link); + + if (duid->raw_data_len == 0) + switch (duid->type) { + case DUID_TYPE_LLT: + r = sd_dhcp6_client_set_duid_llt(client, duid->llt_time); + break; + case DUID_TYPE_LL: + r = sd_dhcp6_client_set_duid_ll(client); + break; + case DUID_TYPE_EN: + r = sd_dhcp6_client_set_duid_en(client); + break; + case DUID_TYPE_UUID: + r = sd_dhcp6_client_set_duid_uuid(client); + break; + default: + r = sd_dhcp6_client_set_duid_raw(client, duid->type, NULL, 0); + } + else + r = sd_dhcp6_client_set_duid_raw(client, duid->type, duid->raw_data, duid->raw_data_len); + if (r < 0) + return r; + + return 0; +} + +static int dhcp6_configure(Link *link) { + _cleanup_(sd_dhcp6_client_unrefp) sd_dhcp6_client *client = NULL; + sd_dhcp6_option *vendor_option; + sd_dhcp6_option *send_option; + void *request_options; + int r; + + assert(link); + assert(link->network); + + if (link->dhcp6_client) + return log_link_debug_errno(link, SYNTHETIC_ERRNO(EBUSY), "DHCPv6 client is already configured."); + + r = sd_dhcp6_client_new(&client); + if (r == -ENOMEM) + return log_oom_debug(); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to create DHCPv6 client: %m"); + + r = sd_dhcp6_client_attach_event(client, link->manager->event, 0); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to attach event: %m"); + + r = sd_dhcp6_client_attach_device(client, link->dev); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to attach device: %m"); + + r = dhcp6_set_identifier(link, client); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set identifier: %m"); + + ORDERED_HASHMAP_FOREACH(send_option, link->network->dhcp6_client_send_options) { + r = sd_dhcp6_client_add_option(client, send_option); + if (r == -EEXIST) + continue; + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set option: %m"); + } + + r = dhcp6_set_hostname(client, link); + if (r < 0) + return r; + + r = sd_dhcp6_client_set_ifindex(client, link->ifindex); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set ifindex: %m"); + + if (link->network->dhcp6_mudurl) { + r = sd_dhcp6_client_set_request_mud_url(client, link->network->dhcp6_mudurl); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set MUD URL: %m"); + } + + if (link->network->dhcp6_use_dns) { + r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_DNS_SERVER); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request DNS servers: %m"); + } + + if (link->network->dhcp6_use_domains > 0) { + r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_DOMAIN); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request domains: %m"); + } + + if (link->network->dhcp6_use_captive_portal > 0) { + r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_CAPTIVE_PORTAL); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request captive portal: %m"); + } + + if (link->network->dhcp6_use_ntp) { + r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NTP_SERVER); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request NTP servers: %m"); + + /* If the server does not provide NTP servers, then we fallback to use SNTP servers. */ + r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_SNTP_SERVER); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request SNTP servers: %m"); + } + + SET_FOREACH(request_options, link->network->dhcp6_request_options) { + uint32_t option = PTR_TO_UINT32(request_options); + + r = sd_dhcp6_client_set_request_option(client, option); + if (r == -EEXIST) { + log_link_debug(link, "DHCPv6 CLIENT: Failed to set request flag for '%u' already exists, ignoring.", option); + continue; + } + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set request flag for '%u': %m", option); + } + + if (link->network->dhcp6_user_class) { + r = sd_dhcp6_client_set_request_user_class(client, link->network->dhcp6_user_class); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set user class: %m"); + } + + if (link->network->dhcp6_vendor_class) { + r = sd_dhcp6_client_set_request_vendor_class(client, link->network->dhcp6_vendor_class); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set vendor class: %m"); + } + + ORDERED_HASHMAP_FOREACH(vendor_option, link->network->dhcp6_client_send_vendor_options) { + r = sd_dhcp6_client_add_vendor_option(client, vendor_option); + if (r == -EEXIST) + continue; + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set vendor option: %m"); + } + + r = sd_dhcp6_client_set_callback(client, dhcp6_handler, link); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set callback: %m"); + + r = dhcp6_client_set_state_callback(client, dhcp6_client_callback_bus, link); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set state change callback: %m"); + + r = sd_dhcp6_client_set_prefix_delegation(client, link->network->dhcp6_use_pd_prefix); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to %s requesting prefixes to be delegated: %m", + enable_disable(link->network->dhcp6_use_pd_prefix)); + + /* Even if UseAddress=no, we need to request IA_NA, as the dhcp6 client may be started in solicit mode. */ + r = sd_dhcp6_client_set_address_request(client, link->network->dhcp6_use_pd_prefix ? link->network->dhcp6_use_address : true); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to %s requesting address: %m", + enable_disable(link->network->dhcp6_use_address)); + + if (link->network->dhcp6_pd_prefix_length > 0) { + r = sd_dhcp6_client_set_prefix_delegation_hint(client, + link->network->dhcp6_pd_prefix_length, + &link->network->dhcp6_pd_prefix_hint); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set prefix delegation hint: %m"); + } + + r = sd_dhcp6_client_set_rapid_commit(client, link->network->dhcp6_use_rapid_commit); + if (r < 0) + return log_link_debug_errno(link, r, + "DHCPv6 CLIENT: Failed to %s rapid commit: %m", + enable_disable(link->network->dhcp6_use_rapid_commit)); + + r = sd_dhcp6_client_set_send_release(client, link->network->dhcp6_send_release); + if (r < 0) + return log_link_debug_errno(link, r, + "DHCPv6 CLIENT: Failed to %s sending release message on stop: %m", + enable_disable(link->network->dhcp6_send_release)); + + link->dhcp6_client = TAKE_PTR(client); + + return 0; +} + +int dhcp6_update_mac(Link *link) { + bool restart; + int r; + + assert(link); + + if (!link->dhcp6_client) + return 0; + + restart = sd_dhcp6_client_is_running(link->dhcp6_client) > 0; + + if (restart) { + r = sd_dhcp6_client_stop(link->dhcp6_client); + if (r < 0) + return r; + } + + r = dhcp6_set_identifier(link, link->dhcp6_client); + if (r < 0) + return r; + + if (restart) { + r = sd_dhcp6_client_start(link->dhcp6_client); + if (r < 0) + return log_link_warning_errno(link, r, "Could not restart DHCPv6 client: %m"); + } + + return 0; +} + +static int dhcp6_process_request(Request *req, Link *link, void *userdata) { + int r; + + assert(link); + + if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) + return 0; + + r = dhcp_configure_duid(link, link_get_dhcp6_duid(link)); + if (r <= 0) + return r; + + r = dhcp6_configure(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure DHCPv6 client: %m"); + + r = ndisc_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m"); + + r = dhcp6_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start DHCPv6 client: %m"); + + log_link_debug(link, "DHCPv6 client is configured%s.", + r > 0 ? ", acquiring DHCPv6 lease" : ""); + return 1; +} + +int link_request_dhcp6_client(Link *link) { + int r; + + assert(link); + + if (!link_dhcp6_enabled(link) && !link_ipv6_accept_ra_enabled(link)) + return 0; + + if (link->dhcp6_client) + return 0; + + r = link_queue_request(link, REQUEST_TYPE_DHCP6_CLIENT, dhcp6_process_request, NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request configuring of the DHCPv6 client: %m"); + + log_link_debug(link, "Requested configuring of the DHCPv6 client."); + return 0; +} + +int link_serialize_dhcp6_client(Link *link, FILE *f) { + _cleanup_free_ char *duid = NULL; + uint32_t iaid; + int r; + + assert(link); + + if (!link->dhcp6_client) + return 0; + + r = sd_dhcp6_client_get_iaid(link->dhcp6_client, &iaid); + if (r >= 0) + fprintf(f, "DHCP6_CLIENT_IAID=0x%x\n", iaid); + + r = sd_dhcp6_client_duid_as_string(link->dhcp6_client, &duid); + if (r >= 0) + fprintf(f, "DHCP6_CLIENT_DUID=%s\n", duid); + + return 0; +} + +int config_parse_dhcp6_pd_prefix_hint( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + union in_addr_union u; + unsigned char prefixlen; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = in_addr_prefix_from_string(rvalue, AF_INET6, &u, &prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=%s, ignoring assignment.", lvalue, rvalue); + return 0; + } + + if (prefixlen < 1 || prefixlen > 128) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid prefix length in %s=%s, ignoring assignment.", lvalue, rvalue); + return 0; + } + + network->dhcp6_pd_prefix_hint = u.in6; + network->dhcp6_pd_prefix_length = prefixlen; + + return 0; +} + +DEFINE_CONFIG_PARSE_ENUM(config_parse_dhcp6_client_start_mode, dhcp6_client_start_mode, DHCP6ClientStartMode, + "Failed to parse WithoutRA= setting"); + +static const char* const dhcp6_client_start_mode_table[_DHCP6_CLIENT_START_MODE_MAX] = { + [DHCP6_CLIENT_START_MODE_NO] = "no", + [DHCP6_CLIENT_START_MODE_INFORMATION_REQUEST] = "information-request", + [DHCP6_CLIENT_START_MODE_SOLICIT] = "solicit", +}; + +DEFINE_STRING_TABLE_LOOKUP(dhcp6_client_start_mode, DHCP6ClientStartMode); diff --git a/src/network/networkd-dhcp6.h b/src/network/networkd-dhcp6.h new file mode 100644 index 0000000..81267c2 --- /dev/null +++ b/src/network/networkd-dhcp6.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" +#include "macro.h" + +typedef enum DHCP6ClientStartMode { + DHCP6_CLIENT_START_MODE_NO, + DHCP6_CLIENT_START_MODE_INFORMATION_REQUEST, + DHCP6_CLIENT_START_MODE_SOLICIT, + _DHCP6_CLIENT_START_MODE_MAX, + _DHCP6_CLIENT_START_MODE_INVALID = -EINVAL, +} DHCP6ClientStartMode; + +typedef struct Link Link; + +bool link_dhcp6_with_address_enabled(Link *link); +int dhcp6_check_ready(Link *link); +int dhcp6_update_mac(Link *link); +int dhcp6_start(Link *link); +int dhcp6_start_on_ra(Link *link, bool information_request); + +int link_request_dhcp6_client(Link *link); + +int link_serialize_dhcp6_client(Link *link, FILE *f); + +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_pd_prefix_hint); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_mud_url); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp6_client_start_mode); + +const char* dhcp6_client_start_mode_to_string(DHCP6ClientStartMode i) _const_; +DHCP6ClientStartMode dhcp6_client_start_mode_from_string(const char *s) _pure_; diff --git a/src/network/networkd-gperf.gperf b/src/network/networkd-gperf.gperf new file mode 100644 index 0000000..8542ffa --- /dev/null +++ b/src/network/networkd-gperf.gperf @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +%{ +#if __GNUC__ >= 7 +_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"") +#endif +#include <stddef.h> +#include "conf-parser.h" +#include "networkd-conf.h" +#include "networkd-dhcp-common.h" +#include "networkd-manager.h" +#include "networkd-route-util.h" +%} +struct ConfigPerfItem; +%null_strings +%language=ANSI-C +%define slot-name section_and_lvalue +%define hash-function-name networkd_gperf_hash +%define lookup-function-name networkd_gperf_lookup +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +Network.SpeedMeter, config_parse_bool, 0, offsetof(Manager, use_speed_meter) +Network.SpeedMeterIntervalSec, config_parse_sec, 0, offsetof(Manager, speed_meter_interval_usec) +Network.ManageForeignRoutingPolicyRules, config_parse_bool, 0, offsetof(Manager, manage_foreign_rules) +Network.ManageForeignRoutes, config_parse_bool, 0, offsetof(Manager, manage_foreign_routes) +Network.RouteTable, config_parse_route_table_names, 0, 0 +Network.IPv6PrivacyExtensions, config_parse_ipv6_privacy_extensions, 0, offsetof(Manager, ipv6_privacy_extensions) +DHCPv4.DUIDType, config_parse_duid_type, 0, offsetof(Manager, dhcp_duid) +DHCPv4.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Manager, dhcp_duid) +DHCPv6.DUIDType, config_parse_duid_type, 0, offsetof(Manager, dhcp6_duid) +DHCPv6.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Manager, dhcp6_duid) +/* Deprecated */ +DHCP.DUIDType, config_parse_manager_duid_type, 0, 0 +DHCP.DUIDRawData, config_parse_manager_duid_rawdata, 0, 0 diff --git a/src/network/networkd-ipv4acd.c b/src/network/networkd-ipv4acd.c new file mode 100644 index 0000000..3d5e203 --- /dev/null +++ b/src/network/networkd-ipv4acd.c @@ -0,0 +1,336 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> /* IFF_LOOPBACK */ +#include <net/if_arp.h> /* ARPHRD_ETHER */ + +#include "sd-dhcp-client.h" +#include "sd-ipv4acd.h" + +#include "ipvlan.h" +#include "networkd-address.h" +#include "networkd-dhcp4.h" +#include "networkd-ipv4acd.h" +#include "networkd-link.h" +#include "networkd-manager.h" + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + ipv4acd_hash_ops, + void, trivial_hash_func, trivial_compare_func, + sd_ipv4acd, sd_ipv4acd_unref); + +bool link_ipv4acd_supported(Link *link) { + assert(link); + + if (link->flags & IFF_LOOPBACK) + return false; + + /* ARPHRD_INFINIBAND seems to potentially support IPv4ACD. + * But currently sd-ipv4acd only supports ARPHRD_ETHER. */ + if (link->iftype != ARPHRD_ETHER) + return false; + + if (link->hw_addr.length != ETH_ALEN) + return false; + + if (ether_addr_is_null(&link->hw_addr.ether)) + return false; + + if (streq_ptr(link->kind, "vrf")) + return false; + + /* L3 or L3S mode do not support ARP. */ + if (IN_SET(link_get_ipvlan_mode(link), NETDEV_IPVLAN_MODE_L3, NETDEV_IPVLAN_MODE_L3S)) + return false; + + return true; +} + +static bool address_ipv4acd_enabled(Link *link, const Address *address) { + assert(link); + assert(address); + + if (address->family != AF_INET) + return false; + + if (!FLAGS_SET(address->duplicate_address_detection, ADDRESS_FAMILY_IPV4)) + return false; + + /* Currently, only static and DHCP4 addresses are supported. */ + if (!IN_SET(address->source, NETWORK_CONFIG_SOURCE_STATIC, NETWORK_CONFIG_SOURCE_DHCP4)) + return false; + + return link_ipv4acd_supported(link); +} + +bool ipv4acd_bound(Link *link, const Address *address) { + sd_ipv4acd *acd; + + assert(link); + assert(address); + + if (address->family != AF_INET) + return true; + + acd = hashmap_get(link->ipv4acd_by_address, IN4_ADDR_TO_PTR(&address->in_addr.in)); + if (!acd) + return true; + + return sd_ipv4acd_is_bound(acd) > 0; +} + +static int static_ipv4acd_address_remove(Link *link, Address *address, bool on_conflict) { + int r; + + assert(link); + assert(address); + + if (!address_exists(address)) + return 0; /* Not assigned. */ + + if (on_conflict) + log_link_warning(link, "Dropping address %s, as an address conflict was detected.", IN4_ADDR_TO_STRING(&address->in_addr.in)); + else + log_link_debug(link, "Removing address %s, as the ACD client is stopped.", IN4_ADDR_TO_STRING(&address->in_addr.in)); + + r = address_remove(address); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to remove address %s: %m", IN4_ADDR_TO_STRING(&address->in_addr.in)); + + return 0; +} + +static int dhcp4_address_on_conflict(Link *link) { + int r; + + assert(link); + assert(link->dhcp_client); + + r = sd_dhcp_client_send_decline(link->dhcp_client); + if (r < 0) + log_link_warning_errno(link, r, "Failed to send DHCP DECLINE, ignoring: %m"); + + if (!link->dhcp_lease) + /* Unlikely, but during probing the address, the lease may be lost. */ + return 0; + + log_link_warning(link, "Dropping DHCPv4 lease, as an address conflict was detected."); + r = dhcp4_lease_lost(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to drop DHCPv4 lease: %m"); + + /* It is not necessary to call address_remove() here, as dhcp4_lease_lost() removes it. */ + return 0; +} + +static void on_acd(sd_ipv4acd *acd, int event, void *userdata) { + Link *link = ASSERT_PTR(userdata); + Address *address = NULL; + struct in_addr a; + int r; + + assert(acd); + + r = sd_ipv4acd_get_address(acd, &a); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to get address from IPv4ACD: %m"); + link_enter_failed(link); + } + + (void) link_get_ipv4_address(link, &a, 0, &address); + + switch (event) { + case SD_IPV4ACD_EVENT_STOP: + if (!address) + break; + + if (address->source == NETWORK_CONFIG_SOURCE_STATIC) { + r = static_ipv4acd_address_remove(link, address, /* on_conflict = */ false); + if (r < 0) + link_enter_failed(link); + } + + /* We have nothing to do for DHCPv4 lease here, as the dhcp client is already stopped + * when stopping the ipv4acd client. See link_stop_engines(). */ + break; + + case SD_IPV4ACD_EVENT_BIND: + log_link_debug(link, "Successfully claimed address %s", IN4_ADDR_TO_STRING(&a)); + break; + + case SD_IPV4ACD_EVENT_CONFLICT: + if (!address) + break; + + log_link_warning(link, "Dropping address %s, as an address conflict was detected.", IN4_ADDR_TO_STRING(&a)); + + if (address->source == NETWORK_CONFIG_SOURCE_STATIC) + r = static_ipv4acd_address_remove(link, address, /* on_conflict = */ true); + else + r = dhcp4_address_on_conflict(link); + if (r < 0) + link_enter_failed(link); + break; + + default: + assert_not_reached(); + } +} + +static int ipv4acd_check_mac(sd_ipv4acd *acd, const struct ether_addr *mac, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + struct hw_addr_data hw_addr; + + assert(mac); + + hw_addr = (struct hw_addr_data) { + .length = ETH_ALEN, + .ether = *mac, + }; + + return link_get_by_hw_addr(m, &hw_addr, NULL) >= 0; +} + +static int ipv4acd_start_one(Link *link, sd_ipv4acd *acd) { + assert(link); + assert(acd); + + if (sd_ipv4acd_is_running(acd)) + return 0; + + if (!link_has_carrier(link)) + return 0; + + return sd_ipv4acd_start(acd, /* reset_conflicts = */ true); +} + +int ipv4acd_configure(Link *link, const Address *address) { + _cleanup_(sd_ipv4acd_unrefp) sd_ipv4acd *acd = NULL; + sd_ipv4acd *existing; + int r; + + assert(link); + assert(link->manager); + assert(address); + + if (address->family != AF_INET) + return 0; + + existing = hashmap_get(link->ipv4acd_by_address, IN4_ADDR_TO_PTR(&address->in_addr.in)); + + if (!address_ipv4acd_enabled(link, address)) + return sd_ipv4acd_stop(existing); + + if (existing) + return ipv4acd_start_one(link, existing); + + log_link_debug(link, "Configuring IPv4ACD for address %s.", IN4_ADDR_TO_STRING(&address->in_addr.in)); + + r = sd_ipv4acd_new(&acd); + if (r < 0) + return r; + + r = sd_ipv4acd_attach_event(acd, link->manager->event, 0); + if (r < 0) + return r; + + r = sd_ipv4acd_set_ifindex(acd, link->ifindex); + if (r < 0) + return r; + + r = sd_ipv4acd_set_mac(acd, &link->hw_addr.ether); + if (r < 0) + return r; + + r = sd_ipv4acd_set_address(acd, &address->in_addr.in); + if (r < 0) + return r; + + r = sd_ipv4acd_set_callback(acd, on_acd, link); + if (r < 0) + return r; + + r = sd_ipv4acd_set_check_mac_callback(acd, ipv4acd_check_mac, link->manager); + if (r < 0) + return r; + + r = hashmap_ensure_put(&link->ipv4acd_by_address, &ipv4acd_hash_ops, IN4_ADDR_TO_PTR(&address->in_addr.in), acd); + if (r < 0) + return r; + + return ipv4acd_start_one(link, TAKE_PTR(acd)); +} + +void ipv4acd_detach(Link *link, const Address *address) { + assert(link); + assert(address); + + if (address->family != AF_INET) + return; + + sd_ipv4acd_unref(hashmap_remove(link->ipv4acd_by_address, IN4_ADDR_TO_PTR(&address->in_addr.in))); +} + +int ipv4acd_update_mac(Link *link) { + sd_ipv4acd *acd; + int r; + + assert(link); + + if (link->hw_addr.length != ETH_ALEN) + return 0; + if (ether_addr_is_null(&link->hw_addr.ether)) + return 0; + + HASHMAP_FOREACH(acd, link->ipv4acd_by_address) { + r = sd_ipv4acd_set_mac(acd, &link->hw_addr.ether); + if (r < 0) + return r; + } + + return 0; +} + +int ipv4acd_start(Link *link) { + sd_ipv4acd *acd; + int r; + + assert(link); + + HASHMAP_FOREACH(acd, link->ipv4acd_by_address) { + r = ipv4acd_start_one(link, acd); + if (r < 0) + return r; + } + + return 0; +} + +int ipv4acd_stop(Link *link) { + sd_ipv4acd *acd; + int k, r = 0; + + assert(link); + + HASHMAP_FOREACH(acd, link->ipv4acd_by_address) { + k = sd_ipv4acd_stop(acd); + if (k < 0) + r = k; + } + + return r; +} + +int ipv4acd_set_ifname(Link *link) { + sd_ipv4acd *acd; + int r; + + assert(link); + + HASHMAP_FOREACH(acd, link->ipv4acd_by_address) { + r = sd_ipv4acd_set_ifname(acd, link->ifname); + if (r < 0) + return r; + } + + return 0; +} diff --git a/src/network/networkd-ipv4acd.h b/src/network/networkd-ipv4acd.h new file mode 100644 index 0000000..54da435 --- /dev/null +++ b/src/network/networkd-ipv4acd.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct Address Address; +typedef struct Link Link; + +bool link_ipv4acd_supported(Link *link); +bool ipv4acd_bound(Link *link, const Address *address); +int ipv4acd_configure(Link *link, const Address *address); +void ipv4acd_detach(Link *link, const Address *address); +int ipv4acd_update_mac(Link *link); +int ipv4acd_start(Link *link); +int ipv4acd_stop(Link *link); +int ipv4acd_set_ifname(Link *link); diff --git a/src/network/networkd-ipv4ll.c b/src/network/networkd-ipv4ll.c new file mode 100644 index 0000000..c357382 --- /dev/null +++ b/src/network/networkd-ipv4ll.c @@ -0,0 +1,319 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/if.h> + +#include "netif-util.h" +#include "networkd-address.h" +#include "networkd-ipv4acd.h" +#include "networkd-ipv4ll.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-queue.h" +#include "parse-util.h" + +bool link_ipv4ll_enabled(Link *link) { + assert(link); + + if (!link_ipv4acd_supported(link)) + return false; + + if (!link->network) + return false; + + if (link->network->bond) + return false; + + return link->network->link_local & ADDRESS_FAMILY_IPV4; +} + +static int address_new_from_ipv4ll(Link *link, Address **ret) { + _cleanup_(address_freep) Address *address = NULL; + struct in_addr addr; + int r; + + assert(link); + assert(link->ipv4ll); + assert(ret); + + r = sd_ipv4ll_get_address(link->ipv4ll, &addr); + if (r < 0) + return r; + + r = address_new(&address); + if (r < 0) + return -ENOMEM; + + address->source = NETWORK_CONFIG_SOURCE_IPV4LL; + address->family = AF_INET; + address->in_addr.in = addr; + address->prefixlen = 16; + address->scope = RT_SCOPE_LINK; + address->route_metric = IPV4LL_ROUTE_METRIC; + + *ret = TAKE_PTR(address); + return 0; +} + +static int ipv4ll_address_lost(Link *link) { + _cleanup_(address_freep) Address *address = NULL; + Address *existing; + int r; + + assert(link); + + link->ipv4ll_address_configured = false; + + r = address_new_from_ipv4ll(link, &address); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + if (address_get(link, address, &existing) < 0) + return 0; + + if (existing->source != NETWORK_CONFIG_SOURCE_IPV4LL) + return 0; + + if (!address_exists(existing)) + return 0; + + log_link_debug(link, "IPv4 link-local release "IPV4_ADDRESS_FMT_STR, + IPV4_ADDRESS_FMT_VAL(address->in_addr.in)); + + return address_remove(existing); +} + +static int ipv4ll_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) { + int r; + + assert(link); + assert(!link->ipv4ll_address_configured); + + r = address_configure_handler_internal(rtnl, m, link, "Could not set ipv4ll address"); + if (r <= 0) + return r; + + link->ipv4ll_address_configured = true; + link_check_ready(link); + + return 1; +} + +static int ipv4ll_address_claimed(sd_ipv4ll *ll, Link *link) { + _cleanup_(address_freep) Address *address = NULL; + int r; + + assert(ll); + assert(link); + + link->ipv4ll_address_configured = false; + + r = address_new_from_ipv4ll(link, &address); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + log_link_debug(link, "IPv4 link-local claim "IPV4_ADDRESS_FMT_STR, + IPV4_ADDRESS_FMT_VAL(address->in_addr.in)); + + return link_request_address(link, address, NULL, ipv4ll_address_handler, NULL); +} + +static void ipv4ll_handler(sd_ipv4ll *ll, int event, void *userdata) { + Link *link = ASSERT_PTR(userdata); + int r; + + assert(link->network); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return; + + switch (event) { + case SD_IPV4LL_EVENT_STOP: + r = ipv4ll_address_lost(link); + if (r < 0) { + link_enter_failed(link); + return; + } + break; + case SD_IPV4LL_EVENT_CONFLICT: + r = ipv4ll_address_lost(link); + if (r < 0) { + link_enter_failed(link); + return; + } + + r = sd_ipv4ll_restart(ll); + if (r < 0) { + log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m"); + link_enter_failed(link); + } + break; + case SD_IPV4LL_EVENT_BIND: + r = ipv4ll_address_claimed(ll, link); + if (r < 0) { + log_link_error(link, "Failed to configure ipv4ll address: %m"); + link_enter_failed(link); + return; + } + break; + default: + log_link_warning(link, "IPv4 link-local unknown event: %d", event); + break; + } +} + +static int ipv4ll_check_mac(sd_ipv4ll *ll, const struct ether_addr *mac, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + struct hw_addr_data hw_addr; + + assert(mac); + + hw_addr = (struct hw_addr_data) { + .length = ETH_ALEN, + .ether = *mac, + }; + + return link_get_by_hw_addr(m, &hw_addr, NULL) >= 0; +} + +int ipv4ll_configure(Link *link) { + uint64_t seed; + int r; + + assert(link); + + if (!link_ipv4ll_enabled(link)) + return 0; + + if (link->ipv4ll) + return -EBUSY; + + r = sd_ipv4ll_new(&link->ipv4ll); + if (r < 0) + return r; + + r = sd_ipv4ll_attach_event(link->ipv4ll, link->manager->event, 0); + if (r < 0) + return r; + + if (link->dev && + net_get_unique_predictable_data(link->dev, true, &seed) >= 0) { + r = sd_ipv4ll_set_address_seed(link->ipv4ll, seed); + if (r < 0) + return r; + } + + r = sd_ipv4ll_set_mac(link->ipv4ll, &link->hw_addr.ether); + if (r < 0) + return r; + + r = sd_ipv4ll_set_ifindex(link->ipv4ll, link->ifindex); + if (r < 0) + return r; + + r = sd_ipv4ll_set_callback(link->ipv4ll, ipv4ll_handler, link); + if (r < 0) + return r; + + return sd_ipv4ll_set_check_mac_callback(link->ipv4ll, ipv4ll_check_mac, link->manager); +} + +int ipv4ll_update_mac(Link *link) { + assert(link); + + if (link->hw_addr.length != ETH_ALEN) + return 0; + if (ether_addr_is_null(&link->hw_addr.ether)) + return 0; + if (!link->ipv4ll) + return 0; + + return sd_ipv4ll_set_mac(link->ipv4ll, &link->hw_addr.ether); +} + +int config_parse_ipv4ll( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + AddressFamily *link_local = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + /* Note that this is mostly like + * config_parse_address_family(), except that it + * applies only to IPv4 */ + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=%s, ignoring assignment. " + "Note that the setting %s= is deprecated, please use LinkLocalAddressing= instead.", + lvalue, rvalue, lvalue); + return 0; + } + + SET_FLAG(*link_local, ADDRESS_FAMILY_IPV4, r); + + log_syntax(unit, LOG_WARNING, filename, line, 0, + "%s=%s is deprecated, please use LinkLocalAddressing=%s instead.", + lvalue, rvalue, address_family_to_string(*link_local)); + + return 0; +} + +int config_parse_ipv4ll_address( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + union in_addr_union a; + struct in_addr *ipv4ll_address = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *ipv4ll_address = (struct in_addr) {}; + return 0; + } + + r = in_addr_from_string(AF_INET, rvalue, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + if (!in4_addr_is_link_local_dynamic(&a.in)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified address cannot be used as an IPv4 link local address, ignoring assignment: %s", + rvalue); + return 0; + } + + *ipv4ll_address = a.in; + return 0; +} diff --git a/src/network/networkd-ipv4ll.h b/src/network/networkd-ipv4ll.h new file mode 100644 index 0000000..fa53bd2 --- /dev/null +++ b/src/network/networkd-ipv4ll.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" + +#define IPV4LL_ROUTE_METRIC 2048 + +typedef struct Link Link; + +bool link_ipv4ll_enabled(Link *link); + +int ipv4ll_configure(Link *link); +int ipv4ll_update_mac(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_ipv4ll); +CONFIG_PARSER_PROTOTYPE(config_parse_ipv4ll_address); diff --git a/src/network/networkd-ipv6-proxy-ndp.c b/src/network/networkd-ipv6-proxy-ndp.c new file mode 100644 index 0000000..edd369a --- /dev/null +++ b/src/network/networkd-ipv6-proxy-ndp.c @@ -0,0 +1,180 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/if.h> + +#include "netlink-util.h" +#include "networkd-ipv6-proxy-ndp.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "socket-util.h" +#include "string-util.h" + +void network_adjust_ipv6_proxy_ndp(Network *network) { + assert(network); + + if (set_isempty(network->ipv6_proxy_ndp_addresses)) + return; + + if (!socket_ipv6_is_supported()) { + log_once(LOG_WARNING, + "%s: IPv6 proxy NDP addresses are set, but IPv6 is not supported by kernel, " + "Ignoring IPv6 proxy NDP addresses.", network->filename); + network->ipv6_proxy_ndp_addresses = set_free_free(network->ipv6_proxy_ndp_addresses); + } +} + +static int ipv6_proxy_ndp_address_configure_handler( + sd_netlink *rtnl, + sd_netlink_message *m, + Request *req, + Link *link, + struct in6_addr *address) { + + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0) + log_link_message_warning_errno(link, m, r, "Could not add IPv6 proxy ndp address entry, ignoring"); + + if (link->static_ipv6_proxy_ndp_messages == 0) { + log_link_debug(link, "IPv6 proxy NDP addresses set."); + link->static_ipv6_proxy_ndp_configured = true; + link_check_ready(link); + } + + return 1; +} + +/* send a request to the kernel to add an IPv6 Proxy entry to the neighbour table */ +static int ipv6_proxy_ndp_address_configure(const struct in6_addr *address, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(address); + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + assert(req); + + /* create new netlink message */ + r = sd_rtnl_message_new_neigh(link->manager->rtnl, &m, RTM_NEWNEIGH, link->ifindex, AF_INET6); + if (r < 0) + return r; + + r = sd_rtnl_message_neigh_set_flags(m, NTF_PROXY); + if (r < 0) + return r; + + r = sd_netlink_message_append_in6_addr(m, NDA_DST, address); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static int ipv6_proxy_ndp_address_process_request(Request *req, Link *link, struct in6_addr *address) { + int r; + + assert(req); + assert(link); + assert(address); + + if (!link_is_ready_to_configure(link, false)) + return 0; + + r = ipv6_proxy_ndp_address_configure(address, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure IPv6 proxy NDP address: %m"); + + return 1; +} + +int link_request_static_ipv6_proxy_ndp_addresses(Link *link) { + struct in6_addr *address; + int r; + + assert(link); + assert(link->network); + + link->static_ipv6_proxy_ndp_configured = false; + + SET_FOREACH(address, link->network->ipv6_proxy_ndp_addresses) { + r = link_queue_request_safe(link, REQUEST_TYPE_IPV6_PROXY_NDP, + address, NULL, + in6_addr_hash_func, + in6_addr_compare_func, + ipv6_proxy_ndp_address_process_request, + &link->static_ipv6_proxy_ndp_messages, + ipv6_proxy_ndp_address_configure_handler, + NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request IPv6 proxy NDP address: %m"); + } + + if (link->static_ipv6_proxy_ndp_messages == 0) { + link->static_ipv6_proxy_ndp_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Setting IPv6 proxy NDP addresses."); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +int config_parse_ipv6_proxy_ndp_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ struct in6_addr *address = NULL; + Network *network = ASSERT_PTR(userdata); + union in_addr_union buffer; + int r; + + assert(filename); + assert(rvalue); + + if (isempty(rvalue)) { + network->ipv6_proxy_ndp_addresses = set_free_free(network->ipv6_proxy_ndp_addresses); + return 0; + } + + r = in_addr_from_string(AF_INET6, rvalue, &buffer); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse IPv6 proxy NDP address, ignoring: %s", rvalue); + return 0; + } + + if (in_addr_is_null(AF_INET6, &buffer)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "IPv6 proxy NDP address cannot be the ANY address, ignoring: %s", rvalue); + return 0; + } + + address = newdup(struct in6_addr, &buffer.in6, 1); + if (!address) + return log_oom(); + + r = set_ensure_put(&network->ipv6_proxy_ndp_addresses, &in6_addr_hash_ops, address); + if (r < 0) + return log_oom(); + if (r > 0) + TAKE_PTR(address); + + return 0; +} diff --git a/src/network/networkd-ipv6-proxy-ndp.h b/src/network/networkd-ipv6-proxy-ndp.h new file mode 100644 index 0000000..e57d28f --- /dev/null +++ b/src/network/networkd-ipv6-proxy-ndp.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" + +typedef struct Link Link; +typedef struct Network Network; + +void network_adjust_ipv6_proxy_ndp(Network *network); + +int link_request_static_ipv6_proxy_ndp_addresses(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_proxy_ndp_address); diff --git a/src/network/networkd-ipv6ll.c b/src/network/networkd-ipv6ll.c new file mode 100644 index 0000000..32229a3 --- /dev/null +++ b/src/network/networkd-ipv6ll.c @@ -0,0 +1,247 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if.h> +#include <linux/if_arp.h> + +#include "in-addr-util.h" +#include "networkd-address.h" +#include "networkd-ipv6ll.h" +#include "networkd-link.h" +#include "networkd-network.h" +#include "networkd-util.h" +#include "socket-util.h" +#include "string-table.h" +#include "strv.h" +#include "sysctl-util.h" + +bool link_ipv6ll_enabled(Link *link) { + assert(link); + + if (!socket_ipv6_is_supported()) + return false; + + if (link->flags & IFF_LOOPBACK) + return false; + + if (!link->network) + return false; + + if (link->iftype == ARPHRD_CAN) + return false; + + if (STRPTR_IN_SET(link->kind, "vrf", "wireguard", "ipip", "gre", "sit", "vti", "nlmon")) + return false; + + if (link->network->bond) + return false; + + return link->network->link_local & ADDRESS_FAMILY_IPV6; +} + +bool link_may_have_ipv6ll(Link *link, bool check_multicast) { + assert(link); + + /* + * This is equivalent to link_ipv6ll_enabled() for non-WireGuard interfaces. + * + * For WireGuard interface, the kernel does not assign any IPv6LL addresses, but we can assign + * it manually. It is necessary to set an IPv6LL address manually to run NDisc or RADV on + * WireGuard interface. Note, also Multicast=yes must be set. See #17380. + * + * TODO: May be better to introduce GenerateIPv6LinkLocalAddress= setting, and use algorithms + * used in networkd-address-generation.c + */ + + if (link_ipv6ll_enabled(link)) + return true; + + /* IPv6LL address can be manually assigned on WireGuard interface. */ + if (streq_ptr(link->kind, "wireguard")) { + Address *a; + + if (!link->network) + return false; + + if (check_multicast && !FLAGS_SET(link->flags, IFF_MULTICAST) && link->network->multicast <= 0) + return false; + + ORDERED_HASHMAP_FOREACH(a, link->network->addresses_by_section) { + if (a->family != AF_INET6) + continue; + if (in6_addr_is_set(&a->in_addr_peer.in6)) + continue; + if (in6_addr_is_link_local(&a->in_addr.in6)) + return true; + } + } + + return false; +} + +IPv6LinkLocalAddressGenMode link_get_ipv6ll_addrgen_mode(Link *link) { + assert(link); + + if (!link_ipv6ll_enabled(link)) + return IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE; + + if (link->network->ipv6ll_address_gen_mode >= 0) + return link->network->ipv6ll_address_gen_mode; + + if (in6_addr_is_set(&link->network->ipv6ll_stable_secret)) + return IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY; + + return IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_EUI64; +} + +int ipv6ll_addrgen_mode_fill_message(sd_netlink_message *message, IPv6LinkLocalAddressGenMode mode) { + int r; + + assert(message); + assert(mode >= 0 && mode < _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX); + + r = sd_netlink_message_open_container(message, IFLA_AF_SPEC); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(message, AF_INET6); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(message, IFLA_INET6_ADDR_GEN_MODE, mode); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(message); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(message); + if (r < 0) + return r; + + return 0; +} + +int link_update_ipv6ll_addrgen_mode(Link *link, sd_netlink_message *message) { + uint8_t mode; + int family, r; + + assert(link); + assert(message); + + r = sd_rtnl_message_get_family(message, &family); + if (r < 0) + return r; + + if (family != AF_UNSPEC) + return 0; + + r = sd_netlink_message_enter_container(message, IFLA_AF_SPEC); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + r = sd_netlink_message_enter_container(message, AF_INET6); + if (r == -ENODATA) + return sd_netlink_message_exit_container(message); + if (r < 0) + return r; + + mode = (uint8_t) link->ipv6ll_address_gen_mode; + r = sd_netlink_message_read_u8(message, IFLA_INET6_ADDR_GEN_MODE, &mode); + if (r < 0 && r != -ENODATA) + return r; + + r = sd_netlink_message_exit_container(message); + if (r < 0) + return r; + + r = sd_netlink_message_exit_container(message); + if (r < 0) + return r; + + if (mode == (uint8_t) link->ipv6ll_address_gen_mode) + return 0; + + if (mode >= _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX) { + log_link_debug(link, "Received invalid IPv6 link-local address generation mode (%u), ignoring.", mode); + return 0; + } + + if (link->ipv6ll_address_gen_mode < 0) + log_link_debug(link, "Saved IPv6 link-local address generation mode: %s", + ipv6_link_local_address_gen_mode_to_string(mode)); + else + log_link_debug(link, "IPv6 link-local address generation mode is changed: %s -> %s", + ipv6_link_local_address_gen_mode_to_string(link->ipv6ll_address_gen_mode), + ipv6_link_local_address_gen_mode_to_string(mode)); + + link->ipv6ll_address_gen_mode = mode; + return 0; +} + +#define STABLE_SECRET_APP_ID_1 SD_ID128_MAKE(aa,05,1d,94,43,68,45,07,b9,73,f1,e8,e4,b7,34,52) +#define STABLE_SECRET_APP_ID_2 SD_ID128_MAKE(52,c4,40,a0,9f,2f,48,58,a9,3a,f6,29,25,ba,7a,7d) + +int link_set_ipv6ll_stable_secret(Link *link) { + struct in6_addr a; + int r; + + assert(link); + assert(link->network); + + if (link->network->ipv6ll_address_gen_mode != IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY) + return 0; + + if (in6_addr_is_set(&link->network->ipv6ll_stable_secret)) + a = link->network->ipv6ll_stable_secret; + else { + sd_id128_t key; + le64_t v; + + /* Generate a stable secret address from machine-ID and the interface name. */ + + r = sd_id128_get_machine_app_specific(STABLE_SECRET_APP_ID_1, &key); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to generate key: %m"); + + v = htole64(siphash24_string(link->ifname, key.bytes)); + memcpy(a.s6_addr, &v, sizeof(v)); + + r = sd_id128_get_machine_app_specific(STABLE_SECRET_APP_ID_2, &key); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to generate key: %m"); + + v = htole64(siphash24_string(link->ifname, key.bytes)); + assert_cc(sizeof(v) * 2 == sizeof(a.s6_addr)); + memcpy(a.s6_addr + sizeof(v), &v, sizeof(v)); + } + + return sysctl_write_ip_property(AF_INET6, link->ifname, "stable_secret", + IN6_ADDR_TO_STRING(&a)); +} + +int link_set_ipv6ll_addrgen_mode(Link *link, IPv6LinkLocalAddressGenMode mode) { + assert(link); + assert(mode >= 0 && mode < _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX); + + if (mode == link->ipv6ll_address_gen_mode) + return 0; + + return sysctl_write_ip_property_uint32(AF_INET6, link->ifname, "addr_gen_mode", mode); +} + +static const char* const ipv6_link_local_address_gen_mode_table[_IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX] = { + [IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_EUI64] = "eui64", + [IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE] = "none", + [IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY] = "stable-privacy", + [IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_RANDOM] = "random", +}; + +DEFINE_STRING_TABLE_LOOKUP(ipv6_link_local_address_gen_mode, IPv6LinkLocalAddressGenMode); +DEFINE_CONFIG_PARSE_ENUM( + config_parse_ipv6_link_local_address_gen_mode, + ipv6_link_local_address_gen_mode, + IPv6LinkLocalAddressGenMode, + "Failed to parse IPv6 link-local address generation mode"); diff --git a/src/network/networkd-ipv6ll.h b/src/network/networkd-ipv6ll.h new file mode 100644 index 0000000..2759eed --- /dev/null +++ b/src/network/networkd-ipv6ll.h @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <errno.h> +#include <linux/if_link.h> +#include <stdbool.h> + +#include "sd-netlink.h" + +#include "conf-parser.h" +#include "macro.h" + +typedef struct Link Link; + +typedef enum IPv6LinkLocalAddressGenMode { + IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_EUI64 = IN6_ADDR_GEN_MODE_EUI64, + IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE = IN6_ADDR_GEN_MODE_NONE, + IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY = IN6_ADDR_GEN_MODE_STABLE_PRIVACY, + IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_RANDOM = IN6_ADDR_GEN_MODE_RANDOM, + _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_MAX, + _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_INVALID = -EINVAL, +} IPv6LinkLocalAddressGenMode; + +bool link_ipv6ll_enabled(Link *link); +bool link_may_have_ipv6ll(Link *link, bool check_multicast); + +IPv6LinkLocalAddressGenMode link_get_ipv6ll_addrgen_mode(Link *link); +int ipv6ll_addrgen_mode_fill_message(sd_netlink_message *message, IPv6LinkLocalAddressGenMode mode); +int link_update_ipv6ll_addrgen_mode(Link *link, sd_netlink_message *message); + +int link_set_ipv6ll_stable_secret(Link *link); +int link_set_ipv6ll_addrgen_mode(Link *link, IPv6LinkLocalAddressGenMode mode); + +const char* ipv6_link_local_address_gen_mode_to_string(IPv6LinkLocalAddressGenMode s) _const_; +IPv6LinkLocalAddressGenMode ipv6_link_local_address_gen_mode_from_string(const char *s) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_link_local_address_gen_mode); diff --git a/src/network/networkd-json.c b/src/network/networkd-json.c new file mode 100644 index 0000000..eed8d9f --- /dev/null +++ b/src/network/networkd-json.c @@ -0,0 +1,1434 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/nexthop.h> + +#include "dhcp-server-internal.h" +#include "dhcp6-internal.h" +#include "dhcp6-lease-internal.h" +#include "dns-domain.h" +#include "ip-protocol-list.h" +#include "netif-util.h" +#include "networkd-address.h" +#include "networkd-dhcp-common.h" +#include "networkd-json.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-neighbor.h" +#include "networkd-network.h" +#include "networkd-nexthop.h" +#include "networkd-route-util.h" +#include "networkd-route.h" +#include "networkd-routing-policy-rule.h" +#include "sort-util.h" +#include "udev-util.h" +#include "user-util.h" +#include "wifi-util.h" + +static int address_build_json(Address *address, JsonVariant **ret) { + _cleanup_free_ char *scope = NULL, *flags = NULL, *state = NULL; + int r; + + assert(address); + assert(ret); + + r = route_scope_to_string_alloc(address->scope, &scope); + if (r < 0) + return r; + + r = address_flags_to_string_alloc(address->flags, address->family, &flags); + if (r < 0) + return r; + + r = network_config_state_to_string_alloc(address->state, &state); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_INTEGER("Family", address->family), + JSON_BUILD_PAIR_IN_ADDR("Address", &address->in_addr, address->family), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("Peer", &address->in_addr_peer, address->family), + JSON_BUILD_PAIR_IN4_ADDR_NON_NULL("Broadcast", &address->broadcast), + JSON_BUILD_PAIR_UNSIGNED("PrefixLength", address->prefixlen), + JSON_BUILD_PAIR_UNSIGNED("Scope", address->scope), + JSON_BUILD_PAIR_STRING("ScopeString", scope), + JSON_BUILD_PAIR_UNSIGNED("Flags", address->flags), + JSON_BUILD_PAIR_STRING("FlagsString", flags), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Label", address->label), + JSON_BUILD_PAIR_FINITE_USEC("PreferredLifetimeUSec", address->lifetime_preferred_usec), + JSON_BUILD_PAIR_FINITE_USEC("PreferredLifetimeUsec", address->lifetime_preferred_usec), /* for backward compat */ + JSON_BUILD_PAIR_FINITE_USEC("ValidLifetimeUSec", address->lifetime_valid_usec), + JSON_BUILD_PAIR_FINITE_USEC("ValidLifetimeUsec", address->lifetime_valid_usec), /* for backward compat */ + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(address->source)), + JSON_BUILD_PAIR_STRING("ConfigState", state), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", &address->provider, address->family))); +} + +static int addresses_append_json(Set *addresses, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + Address *address; + int r; + + assert(v); + + SET_FOREACH(address, addresses) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + r = address_build_json(address, &e); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "Addresses", array); +} + +static int neighbor_build_json(Neighbor *n, JsonVariant **ret) { + _cleanup_free_ char *state = NULL; + int r; + + assert(n); + assert(ret); + + r = network_config_state_to_string_alloc(n->state, &state); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_INTEGER("Family", n->family), + JSON_BUILD_PAIR_IN_ADDR("Destination", &n->in_addr, n->family), + JSON_BUILD_PAIR_HW_ADDR("LinkLayerAddress", &n->ll_addr), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(n->source)), + JSON_BUILD_PAIR_STRING("ConfigState", state))); +} + +static int neighbors_append_json(Set *neighbors, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + Neighbor *neighbor; + int r; + + assert(v); + + SET_FOREACH(neighbor, neighbors) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + r = neighbor_build_json(neighbor, &e); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "Neighbors", array); +} + +static int nexthop_group_build_json(NextHop *nexthop, JsonVariant **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + struct nexthop_grp *g; + int r; + + assert(nexthop); + assert(ret); + + HASHMAP_FOREACH(g, nexthop->group) { + r = json_variant_append_arrayb( + &array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("ID", g->id), + JSON_BUILD_PAIR_UNSIGNED("Weight", g->weight+1))); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(array); + return 0; +} + +static int nexthop_build_json(NextHop *n, JsonVariant **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *group = NULL; + _cleanup_free_ char *flags = NULL, *protocol = NULL, *state = NULL; + int r; + + assert(n); + assert(ret); + + r = route_flags_to_string_alloc(n->flags, &flags); + if (r < 0) + return r; + + r = route_protocol_to_string_alloc(n->protocol, &protocol); + if (r < 0) + return r; + + r = network_config_state_to_string_alloc(n->state, &state); + if (r < 0) + return r; + + r = nexthop_group_build_json(n, &group); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("ID", n->id), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("Gateway", &n->gw, n->family), + JSON_BUILD_PAIR_UNSIGNED("Flags", n->flags), + JSON_BUILD_PAIR_STRING("FlagsString", strempty(flags)), + JSON_BUILD_PAIR_UNSIGNED("Protocol", n->protocol), + JSON_BUILD_PAIR_STRING("ProtocolString", protocol), + JSON_BUILD_PAIR_BOOLEAN("Blackhole", n->blackhole), + JSON_BUILD_PAIR_VARIANT_NON_NULL("Group", group), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(n->source)), + JSON_BUILD_PAIR_STRING("ConfigState", state))); +} + +static int nexthops_append_json(Set *nexthops, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + NextHop *nexthop; + int r; + + assert(v); + + SET_FOREACH(nexthop, nexthops) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + r = nexthop_build_json(nexthop, &e); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "NextHops", array); +} + +static int route_build_json(Route *route, JsonVariant **ret) { + _cleanup_free_ char *scope = NULL, *protocol = NULL, *table = NULL, *flags = NULL, *state = NULL; + Manager *manager; + int r; + + assert(route); + assert(ret); + + manager = route->link ? route->link->manager : route->manager; + + assert(manager); + + r = route_scope_to_string_alloc(route->scope, &scope); + if (r < 0) + return r; + + r = route_protocol_to_string_alloc(route->protocol, &protocol); + if (r < 0) + return r; + + r = manager_get_route_table_to_string(manager, route->table, /* append_num = */ false, &table); + if (r < 0) + return r; + + r = route_flags_to_string_alloc(route->flags, &flags); + if (r < 0) + return r; + + r = network_config_state_to_string_alloc(route->state, &state); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_INTEGER("Family", route->family), + JSON_BUILD_PAIR_IN_ADDR("Destination", &route->dst, route->family), + JSON_BUILD_PAIR_UNSIGNED("DestinationPrefixLength", route->dst_prefixlen), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("Gateway", &route->gw, route->gw_family), + JSON_BUILD_PAIR_CONDITION(route->src_prefixlen > 0, + "Source", JSON_BUILD_IN_ADDR(&route->src, route->family)), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("SourcePrefixLength", route->src_prefixlen), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("PreferredSource", &route->prefsrc, route->family), + JSON_BUILD_PAIR_UNSIGNED("Scope", route->scope), + JSON_BUILD_PAIR_STRING("ScopeString", scope), + JSON_BUILD_PAIR_UNSIGNED("Protocol", route->protocol), + JSON_BUILD_PAIR_STRING("ProtocolString", protocol), + JSON_BUILD_PAIR_UNSIGNED("Type", route->type), + JSON_BUILD_PAIR_STRING("TypeString", route_type_to_string(route->type)), + JSON_BUILD_PAIR_UNSIGNED("Priority", route->priority), + JSON_BUILD_PAIR_UNSIGNED("Table", route->table), + JSON_BUILD_PAIR_STRING("TableString", table), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("MTU", route->mtu), + JSON_BUILD_PAIR_UNSIGNED("Preference", route->pref), + JSON_BUILD_PAIR_UNSIGNED("Flags", route->flags), + JSON_BUILD_PAIR_STRING("FlagsString", strempty(flags)), + JSON_BUILD_PAIR_FINITE_USEC("LifetimeUSec", route->lifetime_usec), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(route->source)), + JSON_BUILD_PAIR_STRING("ConfigState", state), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", &route->provider, route->family))); +} + +static int routes_append_json(Set *routes, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + Route *route; + int r; + + assert(v); + + SET_FOREACH(route, routes) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + r = route_build_json(route, &e); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "Routes", array); +} + +static int routing_policy_rule_build_json(RoutingPolicyRule *rule, JsonVariant **ret) { + _cleanup_free_ char *table = NULL, *protocol = NULL, *state = NULL; + int r; + + assert(rule); + assert(rule->manager); + assert(ret); + + r = manager_get_route_table_to_string(rule->manager, rule->table, /* append_num = */ false, &table); + if (r < 0 && r != -EINVAL) + return r; + + r = route_protocol_to_string_alloc(rule->protocol, &protocol); + if (r < 0) + return r; + + r = network_config_state_to_string_alloc(rule->state, &state); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_INTEGER("Family", rule->family), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("FromPrefix", &rule->from, rule->family), + JSON_BUILD_PAIR_CONDITION(in_addr_is_set(rule->family, &rule->from), + "FromPrefixLength", JSON_BUILD_UNSIGNED(rule->from_prefixlen)), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ToPrefix", &rule->to, rule->family), + JSON_BUILD_PAIR_CONDITION(in_addr_is_set(rule->family, &rule->to), + "ToPrefixLength", JSON_BUILD_UNSIGNED(rule->to_prefixlen)), + JSON_BUILD_PAIR_UNSIGNED("Protocol", rule->protocol), + JSON_BUILD_PAIR_STRING("ProtocolString", protocol), + JSON_BUILD_PAIR_UNSIGNED("TOS", rule->tos), + JSON_BUILD_PAIR_UNSIGNED("Type", rule->type), + JSON_BUILD_PAIR_STRING("TypeString", fr_act_type_full_to_string(rule->type)), + JSON_BUILD_PAIR_UNSIGNED("IPProtocol", rule->ipproto), + JSON_BUILD_PAIR_STRING("IPProtocolString", ip_protocol_to_name(rule->ipproto)), + JSON_BUILD_PAIR_UNSIGNED("Priority", rule->priority), + JSON_BUILD_PAIR_UNSIGNED("FirewallMark", rule->fwmark), + JSON_BUILD_PAIR_UNSIGNED("FirewallMask", rule->fwmask), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("Table", rule->table), + JSON_BUILD_PAIR_STRING_NON_EMPTY("TableString", table), + JSON_BUILD_PAIR_BOOLEAN("Invert", rule->invert_rule), + JSON_BUILD_PAIR_CONDITION(rule->suppress_prefixlen >= 0, + "SuppressPrefixLength", JSON_BUILD_UNSIGNED(rule->suppress_prefixlen)), + JSON_BUILD_PAIR_CONDITION(rule->suppress_ifgroup >= 0, + "SuppressInterfaceGroup", JSON_BUILD_UNSIGNED(rule->suppress_ifgroup)), + JSON_BUILD_PAIR_CONDITION(rule->sport.start != 0 || rule->sport.end != 0, "SourcePort", + JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(rule->sport.start), JSON_BUILD_UNSIGNED(rule->sport.end))), + JSON_BUILD_PAIR_CONDITION(rule->dport.start != 0 || rule->dport.end != 0, "DestinationPort", + JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(rule->dport.start), JSON_BUILD_UNSIGNED(rule->dport.end))), + JSON_BUILD_PAIR_CONDITION(rule->uid_range.start != UID_INVALID && rule->uid_range.end != UID_INVALID, "User", + JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(rule->uid_range.start), JSON_BUILD_UNSIGNED(rule->uid_range.end))), + JSON_BUILD_PAIR_STRING_NON_EMPTY("IncomingInterface", rule->iif), + JSON_BUILD_PAIR_STRING_NON_EMPTY("OutgoingInterface", rule->oif), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(rule->source)), + JSON_BUILD_PAIR_STRING("ConfigState", state))); +} + +static int routing_policy_rules_append_json(Set *rules, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + RoutingPolicyRule *rule; + int r; + + assert(v); + + SET_FOREACH(rule, rules) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + r = routing_policy_rule_build_json(rule, &e); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "RoutingPolicyRules", array); +} + +static int network_append_json(Network *network, JsonVariant **v) { + assert(v); + + if (!network) + return 0; + + return json_variant_merge_objectb( + v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("NetworkFile", network->filename), + JSON_BUILD_PAIR_STRV("NetworkFileDropins", network->dropins), + JSON_BUILD_PAIR_BOOLEAN("RequiredForOnline", network->required_for_online), + JSON_BUILD_PAIR("RequiredOperationalStateForOnline", + JSON_BUILD_ARRAY(JSON_BUILD_STRING(link_operstate_to_string(network->required_operstate_for_online.min)), + JSON_BUILD_STRING(link_operstate_to_string(network->required_operstate_for_online.max)))), + JSON_BUILD_PAIR_STRING("RequiredFamilyForOnline", + link_required_address_family_to_string(network->required_family_for_online)), + JSON_BUILD_PAIR_STRING("ActivationPolicy", + activation_policy_to_string(network->activation_policy)))); +} + +static int device_append_json(sd_device *device, JsonVariant **v) { + _cleanup_strv_free_ char **link_dropins = NULL; + const char *link = NULL, *path = NULL, *vendor = NULL, *model = NULL, *joined; + int r; + + assert(v); + + if (!device) + return 0; + + (void) sd_device_get_property_value(device, "ID_NET_LINK_FILE", &link); + + if (sd_device_get_property_value(device, "ID_NET_LINK_FILE_DROPINS", &joined) >= 0) { + r = strv_split_full(&link_dropins, joined, ":", EXTRACT_CUNESCAPE); + if (r < 0) + return r; + } + + (void) sd_device_get_property_value(device, "ID_PATH", &path); + + (void) device_get_vendor_string(device, &vendor); + (void) device_get_model_string(device, &model); + + return json_variant_merge_objectb( + v, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING_NON_EMPTY("LinkFile", link), + JSON_BUILD_PAIR_STRV_NON_EMPTY("LinkFileDropins", link_dropins), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Path", path), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Vendor", vendor), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Model", model))); +} + +static int dns_append_json_one(Link *link, const struct in_addr_full *a, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **array) { + assert(link); + assert(a); + assert(array); + + if (a->ifindex != 0 && a->ifindex != link->ifindex) + return 0; + + return json_variant_append_arrayb( + array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_INTEGER("Family", a->family), + JSON_BUILD_PAIR_IN_ADDR("Address", &a->address, a->family), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("Port", a->port), + JSON_BUILD_PAIR_CONDITION(a->ifindex != 0, "InterfaceIndex", JSON_BUILD_INTEGER(a->ifindex)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("ServerName", a->server_name), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, a->family))); +} + +static int dns_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + int r; + + assert(link); + assert(v); + + if (!link->network) + return 0; + + if (link->n_dns != UINT_MAX) + for (unsigned i = 0; i < link->n_dns; i++) { + r = dns_append_json_one(link, link->dns[i], NETWORK_CONFIG_SOURCE_RUNTIME, NULL, &array); + if (r < 0) + return r; + } + else { + for (unsigned i = 0; i < link->network->n_dns; i++) { + r = dns_append_json_one(link, link->network->dns[i], NETWORK_CONFIG_SOURCE_STATIC, NULL, &array); + if (r < 0) + return r; + } + + if (link->dhcp_lease && link->network->dhcp_use_dns) { + const struct in_addr *dns; + union in_addr_union s; + int n_dns; + + r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in); + if (r < 0) + return r; + + n_dns = sd_dhcp_lease_get_dns(link->dhcp_lease, &dns); + for (int i = 0; i < n_dns; i++) { + r = dns_append_json_one(link, + &(struct in_addr_full) { .family = AF_INET, .address.in = dns[i], }, + NETWORK_CONFIG_SOURCE_DHCP4, + &s, + &array); + if (r < 0) + return r; + } + } + + if (link->dhcp6_lease && link->network->dhcp6_use_dns) { + const struct in6_addr *dns; + union in_addr_union s; + int n_dns; + + r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &s.in6); + if (r < 0) + return r; + + n_dns = sd_dhcp6_lease_get_dns(link->dhcp6_lease, &dns); + for (int i = 0; i < n_dns; i++) { + r = dns_append_json_one(link, + &(struct in_addr_full) { .family = AF_INET6, .address.in6 = dns[i], }, + NETWORK_CONFIG_SOURCE_DHCP6, + &s, + &array); + if (r < 0) + return r; + } + } + + if (link->network->ipv6_accept_ra_use_dns) { + NDiscRDNSS *a; + + SET_FOREACH(a, link->ndisc_rdnss) { + r = dns_append_json_one(link, + &(struct in_addr_full) { .family = AF_INET6, .address.in6 = a->address, }, + NETWORK_CONFIG_SOURCE_NDISC, + &(union in_addr_union) { .in6 = a->router }, + &array); + if (r < 0) + return r; + } + } + } + + return json_variant_set_field_non_null(v, "DNS", array); +} + +static int server_append_json_one_addr(int family, const union in_addr_union *a, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **array) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(a); + assert(array); + + r = json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_INTEGER("Family", family), + JSON_BUILD_PAIR_IN_ADDR("Address", a, family), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, family))); + if (r < 0) + return r; + + return json_variant_append_array(array, v); +} + +static int server_append_json_one_fqdn(int family, const char *fqdn, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **array) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6)); + assert(fqdn); + assert(array); + + r = json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("Server", fqdn), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, family))); + if (r < 0) + return r; + + return json_variant_append_array(array, v); +} + +static int server_append_json_one_string(const char *str, NetworkConfigSource s, JsonVariant **array) { + union in_addr_union a; + int family; + + assert(str); + + if (in_addr_from_string_auto(str, &family, &a) >= 0) + return server_append_json_one_addr(family, &a, s, NULL, array); + + return server_append_json_one_fqdn(AF_UNSPEC, str, s, NULL, array); +} + +static int ntp_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + int r; + + assert(link); + assert(v); + + if (!link->network) + return 0; + + STRV_FOREACH(p, link->ntp ?: link->network->ntp) { + r = server_append_json_one_string(*p, NETWORK_CONFIG_SOURCE_RUNTIME, &array); + if (r < 0) + return r; + } + + if (!link->ntp) { + if (link->dhcp_lease && link->network->dhcp_use_ntp) { + const struct in_addr *ntp; + union in_addr_union s; + int n_ntp; + + r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in); + if (r < 0) + return r; + + n_ntp = sd_dhcp_lease_get_ntp(link->dhcp_lease, &ntp); + for (int i = 0; i < n_ntp; i++) { + r = server_append_json_one_addr(AF_INET, + &(union in_addr_union) { .in = ntp[i], }, + NETWORK_CONFIG_SOURCE_DHCP4, + &s, + &array); + if (r < 0) + return r; + } + } + + if (link->dhcp6_lease && link->network->dhcp6_use_ntp) { + const struct in6_addr *ntp_addr; + union in_addr_union s; + char **ntp_fqdn; + int n_ntp; + + r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &s.in6); + if (r < 0) + return r; + + n_ntp = sd_dhcp6_lease_get_ntp_addrs(link->dhcp6_lease, &ntp_addr); + for (int i = 0; i < n_ntp; i++) { + r = server_append_json_one_addr(AF_INET6, + &(union in_addr_union) { .in6 = ntp_addr[i], }, + NETWORK_CONFIG_SOURCE_DHCP6, + &s, + &array); + if (r < 0) + return r; + } + + n_ntp = sd_dhcp6_lease_get_ntp_fqdn(link->dhcp6_lease, &ntp_fqdn); + for (int i = 0; i < n_ntp; i++) { + r = server_append_json_one_fqdn(AF_INET6, + ntp_fqdn[i], + NETWORK_CONFIG_SOURCE_DHCP6, + &s, + &array); + if (r < 0) + return r; + } + } + } + + return json_variant_set_field_non_null(v, "NTP", array); +} + +static int sip_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + const struct in_addr *sip; + union in_addr_union s; + int n_sip, r; + + assert(link); + assert(v); + + if (!link->network || !link->network->dhcp_use_sip || !link->dhcp_lease) + return 0; + + n_sip = sd_dhcp_lease_get_sip(link->dhcp_lease, &sip); + if (n_sip <= 0) + return 0; + + r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in); + if (r < 0) + return r; + + for (int i = 0; i < n_sip; i++) { + r = server_append_json_one_addr(AF_INET, + &(union in_addr_union) { .in = sip[i], }, + NETWORK_CONFIG_SOURCE_DHCP4, + &s, + &array); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "SIP", array); +} + +static int domain_append_json(int family, const char *domain, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **array) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6)); + assert(domain); + assert(array); + + r = json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("Domain", domain), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, family))); + if (r < 0) + return r; + + return json_variant_append_array(array, v); +} + +static int domains_append_json(Link *link, bool is_route, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + OrderedSet *link_domains, *network_domains; + DHCPUseDomains use_domains; + union in_addr_union s; + char **domains; + const char *domain; + int r; + + assert(link); + assert(v); + + if (!link->network) + return 0; + + link_domains = is_route ? link->route_domains : link->search_domains; + network_domains = is_route ? link->network->route_domains : link->network->search_domains; + use_domains = is_route ? DHCP_USE_DOMAINS_ROUTE : DHCP_USE_DOMAINS_YES; + + ORDERED_SET_FOREACH(domain, link_domains ?: network_domains) { + r = domain_append_json(AF_UNSPEC, domain, + link_domains ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC, + NULL, &array); + if (r < 0) + return r; + } + + if (!link_domains) { + if (link->dhcp_lease && + link->network->dhcp_use_domains == use_domains) { + r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in); + if (r < 0) + return r; + + if (sd_dhcp_lease_get_domainname(link->dhcp_lease, &domain) >= 0) { + r = domain_append_json(AF_INET, domain, NETWORK_CONFIG_SOURCE_DHCP4, &s, &array); + if (r < 0) + return r; + } + + if (sd_dhcp_lease_get_search_domains(link->dhcp_lease, &domains) >= 0) + STRV_FOREACH(p, domains) { + r = domain_append_json(AF_INET, *p, NETWORK_CONFIG_SOURCE_DHCP4, &s, &array); + if (r < 0) + return r; + } + } + + if (link->dhcp6_lease && + link->network->dhcp6_use_domains == use_domains) { + r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &s.in6); + if (r < 0) + return r; + + if (sd_dhcp6_lease_get_domains(link->dhcp6_lease, &domains) >= 0) + STRV_FOREACH(p, domains) { + r = domain_append_json(AF_INET6, *p, NETWORK_CONFIG_SOURCE_DHCP6, &s, &array); + if (r < 0) + return r; + } + } + + if (link->network->ipv6_accept_ra_use_domains == use_domains) { + NDiscDNSSL *a; + + SET_FOREACH(a, link->ndisc_dnssl) { + r = domain_append_json(AF_INET6, NDISC_DNSSL_DOMAIN(a), NETWORK_CONFIG_SOURCE_NDISC, + &(union in_addr_union) { .in6 = a->router }, + &array); + if (r < 0) + return r; + } + } + } + + return json_variant_set_field_non_null(v, is_route ? "RouteDomains" : "SearchDomains", array); +} + +static int nta_append_json(const char *nta, NetworkConfigSource s, JsonVariant **array) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(nta); + assert(array); + + r = json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("DNSSECNegativeTrustAnchor", nta), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)))); + if (r < 0) + return r; + + return json_variant_append_array(array, v); +} + +static int ntas_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + const char *nta; + int r; + + assert(link); + assert(v); + + if (!link->network) + return 0; + + SET_FOREACH(nta, link->dnssec_negative_trust_anchors ?: link->network->dnssec_negative_trust_anchors) { + r = nta_append_json(nta, + link->dnssec_negative_trust_anchors ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC, + &array); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "DNSSECNegativeTrustAnchors", array); +} + +static int dns_misc_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + ResolveSupport resolve_support; + NetworkConfigSource source; + DnsOverTlsMode mode; + int t, r; + + assert(link); + assert(v); + + if (!link->network) + return 0; + + resolve_support = link->llmnr >= 0 ? link->llmnr : link->network->llmnr; + if (resolve_support >= 0) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + source = link->llmnr >= 0 ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC; + + r = json_build(&e, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("LLMNR", resolve_support_to_string(resolve_support)), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(source)))); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + resolve_support = link->mdns >= 0 ? link->mdns : link->network->mdns; + if (resolve_support >= 0) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + source = link->mdns >= 0 ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC; + + r = json_build(&e, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("MDNS", resolve_support_to_string(resolve_support)), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(source)))); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + t = link->dns_default_route >= 0 ? link->dns_default_route : link->network->dns_default_route; + if (t >= 0) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + source = link->dns_default_route >= 0 ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC; + + r = json_build(&e, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_BOOLEAN("DNSDefaultRoute", t), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(source)))); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + mode = link->dns_over_tls_mode >= 0 ? link->dns_over_tls_mode : link->network->dns_over_tls_mode; + if (mode >= 0) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + source = link->dns_over_tls_mode >= 0 ? NETWORK_CONFIG_SOURCE_RUNTIME : NETWORK_CONFIG_SOURCE_STATIC; + + r = json_build(&e, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("DNSOverTLS", dns_over_tls_mode_to_string(mode)), + JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(source)))); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "DNSSettings", array); +} + +static int captive_portal_append_json(Link *link, JsonVariant **v) { + const char *captive_portal; + int r; + + assert(link); + assert(v); + + r = link_get_captive_portal(link, &captive_portal); + if (r <= 0) + return r; + + return json_variant_merge_objectb(v, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("CaptivePortal", captive_portal))); +} + +static int pref64_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL, *w = NULL; + NDiscPREF64 *i; + int r; + + assert(link); + assert(v); + + if (!link->network || !link->network->ipv6_accept_ra_use_pref64) + return 0; + + SET_FOREACH(i, link->ndisc_pref64) { + r = json_variant_append_arrayb(&array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_IN6_ADDR_NON_NULL("Prefix", &i->prefix), + JSON_BUILD_PAIR_UNSIGNED("PrefixLength", i->prefix_len), + JSON_BUILD_PAIR_FINITE_USEC("LifetimeUSec", i->lifetime_usec), + JSON_BUILD_PAIR_IN6_ADDR_NON_NULL("ConfigProvider", &i->router))); + if (r < 0) + return r; + } + + r = json_variant_set_field_non_null(&w, "PREF64", array); + if (r < 0) + return r; + + return json_variant_set_field_non_null(v, "NDisc", w); +} + +static int dhcp_server_offered_leases_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + DHCPLease *lease; + int r; + + assert(link); + assert(v); + + if (!link->dhcp_server) + return 0; + + HASHMAP_FOREACH(lease, link->dhcp_server->bound_leases_by_client_id) { + struct in_addr address = { .s_addr = lease->address }; + + r = json_variant_append_arrayb( + &array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_BYTE_ARRAY( + "ClientId", + lease->client_id.data, + lease->client_id.length), + JSON_BUILD_PAIR_IN4_ADDR_NON_NULL("Address", &address), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Hostname", lease->hostname), + JSON_BUILD_PAIR_FINITE_USEC( + "ExpirationUSec", lease->expiration))); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "Leases", array); +} + +static int dhcp_server_static_leases_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + DHCPLease *lease; + int r; + + assert(link); + assert(v); + + if (!link->dhcp_server) + return 0; + + HASHMAP_FOREACH(lease, link->dhcp_server->static_leases_by_client_id) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + struct in_addr address = { .s_addr = lease->address }; + + r = json_build(&e, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_BYTE_ARRAY( + "ClientId", + lease->client_id.data, + lease->client_id.length), + JSON_BUILD_PAIR_IN4_ADDR_NON_NULL("Address", &address))); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "StaticLeases", array); +} + +static int dhcp_server_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + int r; + + assert(link); + assert(v); + + if (!link->dhcp_server) + return 0; + + r = json_build(&w, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("PoolOffset", link->dhcp_server->pool_offset), + JSON_BUILD_PAIR_UNSIGNED("PoolSize", link->dhcp_server->pool_size))); + if (r < 0) + return r; + + r = dhcp_server_offered_leases_append_json(link, &w); + if (r < 0) + return r; + + r = dhcp_server_static_leases_append_json(link, &w); + if (r < 0) + return r; + + return json_variant_set_field_non_null(v, "DHCPServer", w); +} + +static int dhcp6_client_vendor_options_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + sd_dhcp6_option **options = NULL; + int r, n_vendor_options; + + assert(link); + assert(v); + + if (!link->dhcp6_lease) + return 0; + + n_vendor_options = sd_dhcp6_lease_get_vendor_options(link->dhcp6_lease, &options); + + FOREACH_ARRAY(option, options, n_vendor_options) { + r = json_variant_append_arrayb(&array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("EnterpriseId", (*option)->enterprise_identifier), + JSON_BUILD_PAIR_UNSIGNED("SubOptionCode", (*option)->option), + JSON_BUILD_PAIR_HEX("SubOptionData", (*option)->data, (*option)->length))); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "VendorSpecificOptions", array); +} + +static int dhcp6_client_lease_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + usec_t ts = USEC_INFINITY, t1 = USEC_INFINITY, t2 = USEC_INFINITY; + int r; + + assert(link); + assert(v); + + if (!link->dhcp6_lease) + return 0; + + r = sd_dhcp6_lease_get_timestamp(link->dhcp6_lease, CLOCK_BOOTTIME, &ts); + if (r < 0 && r != -ENODATA) + return r; + + r = sd_dhcp6_lease_get_t1_timestamp(link->dhcp6_lease, CLOCK_BOOTTIME, &t1); + if (r < 0 && r != -ENODATA) + return r; + + r = sd_dhcp6_lease_get_t2_timestamp(link->dhcp6_lease, CLOCK_BOOTTIME, &t2); + if (r < 0 && r != -ENODATA) + return r; + + r = json_build(&w, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_FINITE_USEC("Timeout1USec", t1), + JSON_BUILD_PAIR_FINITE_USEC("Timeout2USec", t2), + JSON_BUILD_PAIR_FINITE_USEC("LeaseTimestampUSec", ts))); + if (r < 0) + return r; + + return json_variant_set_field_non_null(v, "Lease", w); +} + +static int dhcp6_client_pd_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + int r; + + assert(link); + assert(link->network); + assert(v); + + if (!link->network->dhcp6_use_pd_prefix || + !sd_dhcp6_lease_has_pd_prefix(link->dhcp6_lease)) + return 0; + + FOREACH_DHCP6_PD_PREFIX(link->dhcp6_lease) { + usec_t lifetime_preferred_usec, lifetime_valid_usec; + struct in6_addr prefix; + uint8_t prefix_len; + + r = sd_dhcp6_lease_get_pd_prefix(link->dhcp6_lease, &prefix, &prefix_len); + if (r < 0) + return r; + + r = sd_dhcp6_lease_get_pd_lifetime_timestamp(link->dhcp6_lease, CLOCK_BOOTTIME, + &lifetime_preferred_usec, &lifetime_valid_usec); + if (r < 0) + return r; + + r = json_variant_append_arrayb(&array, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_IN6_ADDR("Prefix", &prefix), + JSON_BUILD_PAIR_UNSIGNED("PrefixLength", prefix_len), + JSON_BUILD_PAIR_FINITE_USEC("PreferredLifetimeUSec", lifetime_preferred_usec), + JSON_BUILD_PAIR_FINITE_USEC("ValidLifetimeUSec", lifetime_valid_usec))); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "Prefixes", array); +} + +static int dhcp6_client_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + int r; + + assert(link); + assert(v); + + if (!link->dhcp6_client) + return 0; + + r = dhcp6_client_lease_append_json(link, &w); + if (r < 0) + return r; + + r = dhcp6_client_pd_append_json(link, &w); + if (r < 0) + return r; + + r = dhcp6_client_vendor_options_append_json(link, &w); + if (r < 0) + return r; + + return json_variant_set_field_non_null(v, "DHCPv6Client", w); +} + +static int dhcp_client_lease_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + usec_t lease_timestamp_usec = USEC_INFINITY, t1 = USEC_INFINITY, t2 = USEC_INFINITY; + int r; + + assert(link); + assert(v); + + if (!link->dhcp_client || !link->dhcp_lease) + return 0; + + r = sd_dhcp_lease_get_timestamp(link->dhcp_lease, CLOCK_BOOTTIME, &lease_timestamp_usec); + if (r < 0 && r != -ENODATA) + return r; + + r = sd_dhcp_lease_get_t1_timestamp(link->dhcp_lease, CLOCK_BOOTTIME, &t1); + if (r < 0 && r != -ENODATA) + return r; + + r = sd_dhcp_lease_get_t2_timestamp(link->dhcp_lease, CLOCK_BOOTTIME, &t2); + if (r < 0 && r != -ENODATA) + return r; + + r = json_build(&w, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_FINITE_USEC("LeaseTimestampUSec", lease_timestamp_usec), + JSON_BUILD_PAIR_FINITE_USEC("Timeout1USec", t1), + JSON_BUILD_PAIR_FINITE_USEC("Timeout2USec", t2))); + if (r < 0) + return r; + + return json_variant_set_field_non_null(v, "Lease", w); +} + +static int dhcp_client_pd_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *addresses = NULL, *array = NULL; + uint8_t ipv4masklen, sixrd_prefixlen; + struct in6_addr sixrd_prefix; + const struct in_addr *br_addresses; + size_t n_br_addresses = 0; + int r; + + assert(link); + assert(link->network); + assert(v); + + if (!link->network->dhcp_use_6rd || !sd_dhcp_lease_has_6rd(link->dhcp_lease)) + return 0; + + r = sd_dhcp_lease_get_6rd(link->dhcp_lease, &ipv4masklen, &sixrd_prefixlen, &sixrd_prefix, &br_addresses, &n_br_addresses); + if (r < 0) + return r; + + FOREACH_ARRAY(br_address, br_addresses, n_br_addresses) { + r = json_variant_append_arrayb(&addresses, JSON_BUILD_IN4_ADDR(br_address)); + if (r < 0) + return r; + } + + r = json_build(&array, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_IN6_ADDR("Prefix", &sixrd_prefix), + JSON_BUILD_PAIR_UNSIGNED("PrefixLength", sixrd_prefixlen), + JSON_BUILD_PAIR_UNSIGNED("IPv4MaskLength", ipv4masklen), + JSON_BUILD_PAIR_VARIANT_NON_NULL("BorderRouters", addresses))); + if (r < 0) + return r; + + return json_variant_set_field_non_null(v, "6rdPrefix", array); +} + +static int dhcp_client_append_json(Link *link, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + int r; + + assert(link); + assert(v); + + if (!link->dhcp_client) + return 0; + + r = dhcp_client_lease_append_json(link, &w); + if (r < 0) + return r; + + r = dhcp_client_pd_append_json(link, &w); + if (r < 0) + return r; + + return json_variant_set_field_non_null(v, "DHCPv4Client", w); +} + +int link_build_json(Link *link, JsonVariant **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ char *type = NULL, *flags = NULL; + int r; + + assert(link); + assert(ret); + + r = net_get_type_string(link->dev, link->iftype, &type); + if (r == -ENOMEM) + return r; + + r = link_flags_to_string_alloc(link->flags, &flags); + if (r < 0) + return r; + + r = json_build(&v, JSON_BUILD_OBJECT( + /* basic information */ + JSON_BUILD_PAIR_INTEGER("Index", link->ifindex), + JSON_BUILD_PAIR_STRING("Name", link->ifname), + JSON_BUILD_PAIR_STRV_NON_EMPTY("AlternativeNames", link->alternative_names), + JSON_BUILD_PAIR_CONDITION(link->master_ifindex > 0, + "MasterInterfaceIndex", JSON_BUILD_INTEGER(link->master_ifindex)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Kind", link->kind), + JSON_BUILD_PAIR_STRING("Type", type), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Driver", link->driver), + JSON_BUILD_PAIR_UNSIGNED("Flags", link->flags), + JSON_BUILD_PAIR_STRING("FlagsString", flags), + JSON_BUILD_PAIR_UNSIGNED("KernelOperationalState", link->kernel_operstate), + JSON_BUILD_PAIR_STRING("KernelOperationalStateString", kernel_operstate_to_string(link->kernel_operstate)), + JSON_BUILD_PAIR_UNSIGNED("MTU", link->mtu), + JSON_BUILD_PAIR_UNSIGNED("MinimumMTU", link->min_mtu), + JSON_BUILD_PAIR_UNSIGNED("MaximumMTU", link->max_mtu), + JSON_BUILD_PAIR_HW_ADDR_NON_NULL("HardwareAddress", &link->hw_addr), + JSON_BUILD_PAIR_HW_ADDR_NON_NULL("PermanentHardwareAddress", &link->permanent_hw_addr), + JSON_BUILD_PAIR_HW_ADDR_NON_NULL("BroadcastAddress", &link->bcast_addr), + JSON_BUILD_PAIR_IN6_ADDR_NON_NULL("IPv6LinkLocalAddress", &link->ipv6ll_address), + /* wlan information */ + JSON_BUILD_PAIR_CONDITION(link->wlan_iftype > 0, "WirelessLanInterfaceType", + JSON_BUILD_UNSIGNED(link->wlan_iftype)), + JSON_BUILD_PAIR_CONDITION(link->wlan_iftype > 0, "WirelessLanInterfaceTypeString", + JSON_BUILD_STRING(nl80211_iftype_to_string(link->wlan_iftype))), + JSON_BUILD_PAIR_STRING_NON_EMPTY("SSID", link->ssid), + JSON_BUILD_PAIR_ETHER_ADDR_NON_NULL("BSSID", &link->bssid), + /* link state */ + JSON_BUILD_PAIR_STRING("AdministrativeState", link_state_to_string(link->state)), + JSON_BUILD_PAIR_STRING("OperationalState", link_operstate_to_string(link->operstate)), + JSON_BUILD_PAIR_STRING("CarrierState", link_carrier_state_to_string(link->carrier_state)), + JSON_BUILD_PAIR_STRING("AddressState", link_address_state_to_string(link->address_state)), + JSON_BUILD_PAIR_STRING("IPv4AddressState", link_address_state_to_string(link->ipv4_address_state)), + JSON_BUILD_PAIR_STRING("IPv6AddressState", link_address_state_to_string(link->ipv6_address_state)), + JSON_BUILD_PAIR_STRING("OnlineState", link_online_state_to_string(link->online_state)))); + if (r < 0) + return r; + + r = network_append_json(link->network, &v); + if (r < 0) + return r; + + r = device_append_json(link->dev, &v); + if (r < 0) + return r; + + r = dns_append_json(link, &v); + if (r < 0) + return r; + + r = ntp_append_json(link, &v); + if (r < 0) + return r; + + r = sip_append_json(link, &v); + if (r < 0) + return r; + + r = domains_append_json(link, /* is_route = */ false, &v); + if (r < 0) + return r; + + r = domains_append_json(link, /* is_route = */ true, &v); + if (r < 0) + return r; + + r = ntas_append_json(link, &v); + if (r < 0) + return r; + + r = dns_misc_append_json(link, &v); + if (r < 0) + return r; + + r = captive_portal_append_json(link, &v); + if (r < 0) + return r; + + r = pref64_append_json(link, &v); + if (r < 0) + return r; + + r = addresses_append_json(link->addresses, &v); + if (r < 0) + return r; + + r = neighbors_append_json(link->neighbors, &v); + if (r < 0) + return r; + + r = nexthops_append_json(link->nexthops, &v); + if (r < 0) + return r; + + r = routes_append_json(link->routes, &v); + if (r < 0) + return r; + + r = dhcp_server_append_json(link, &v); + if (r < 0) + return r; + + r = dhcp_client_append_json(link, &v); + if (r < 0) + return r; + + r = dhcp6_client_append_json(link, &v); + if (r < 0) + return r; + + *ret = TAKE_PTR(v); + return 0; +} + +static int links_append_json(Manager *manager, JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + _cleanup_free_ Link **links = NULL; + size_t n_links = 0; + int r; + + assert(manager); + assert(v); + + r = hashmap_dump_sorted(manager->links_by_index, (void***) &links, &n_links); + if (r < 0) + return r; + + FOREACH_ARRAY(link, links, n_links) { + _cleanup_(json_variant_unrefp) JsonVariant *e = NULL; + + r = link_build_json(*link, &e); + if (r < 0) + return r; + + r = json_variant_append_array(&array, e); + if (r < 0) + return r; + } + + return json_variant_set_field_non_null(v, "Interfaces", array); +} + +int manager_build_json(Manager *manager, JsonVariant **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(manager); + assert(ret); + + r = links_append_json(manager, &v); + if (r < 0) + return r; + + r = nexthops_append_json(manager->nexthops, &v); + if (r < 0) + return r; + + r = routes_append_json(manager->routes, &v); + if (r < 0) + return r; + + r = routing_policy_rules_append_json(manager->rules, &v); + if (r < 0) + return r; + + *ret = TAKE_PTR(v); + return 0; +} diff --git a/src/network/networkd-json.h b/src/network/networkd-json.h new file mode 100644 index 0000000..25018fa --- /dev/null +++ b/src/network/networkd-json.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "json.h" + +typedef struct Link Link; +typedef struct Manager Manager; + +int link_build_json(Link *link, JsonVariant **ret); +int manager_build_json(Manager *manager, JsonVariant **ret); diff --git a/src/network/networkd-link-bus.c b/src/network/networkd-link-bus.c new file mode 100644 index 0000000..58d4875 --- /dev/null +++ b/src/network/networkd-link-bus.c @@ -0,0 +1,898 @@ +/* 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-dhcp4.h" +#include "networkd-json.h" +#include "networkd-link-bus.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-state-file.h" +#include "parse-util.h" +#include "resolve-util.h" +#include "socket-netlink.h" +#include "strv.h" +#include "user-util.h" + +BUS_DEFINE_PROPERTY_GET_ENUM(property_get_operational_state, link_operstate, LinkOperationalState); +BUS_DEFINE_PROPERTY_GET_ENUM(property_get_carrier_state, link_carrier_state, LinkCarrierState); +BUS_DEFINE_PROPERTY_GET_ENUM(property_get_address_state, link_address_state, LinkAddressState); +BUS_DEFINE_PROPERTY_GET_ENUM(property_get_online_state, link_online_state, LinkOnlineState); +static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_administrative_state, link_state, LinkState); + +static int property_get_bit_rates( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Link *link = ASSERT_PTR(userdata); + Manager *manager; + double interval_sec; + uint64_t tx, rx; + + assert(bus); + assert(reply); + + manager = link->manager; + + if (!manager->use_speed_meter || + manager->speed_meter_usec_old == 0 || + !link->stats_updated) + return sd_bus_message_append(reply, "(tt)", UINT64_MAX, UINT64_MAX); + + assert(manager->speed_meter_usec_new > manager->speed_meter_usec_old); + interval_sec = (manager->speed_meter_usec_new - manager->speed_meter_usec_old) / USEC_PER_SEC; + + if (link->stats_new.tx_bytes > link->stats_old.tx_bytes) + tx = (uint64_t) ((link->stats_new.tx_bytes - link->stats_old.tx_bytes) / interval_sec); + else + tx = (uint64_t) ((UINT64_MAX - (link->stats_old.tx_bytes - link->stats_new.tx_bytes)) / interval_sec); + + if (link->stats_new.rx_bytes > link->stats_old.rx_bytes) + rx = (uint64_t) ((link->stats_new.rx_bytes - link->stats_old.rx_bytes) / interval_sec); + else + rx = (uint64_t) ((UINT64_MAX - (link->stats_old.rx_bytes - link->stats_new.rx_bytes)) / interval_sec); + + return sd_bus_message_append(reply, "(tt)", tx, rx); +} + +static int verify_managed_link(Link *l, sd_bus_error *error) { + assert(l); + + if (l->flags & IFF_LOOPBACK) + return sd_bus_error_setf(error, BUS_ERROR_LINK_BUSY, "Link %s is loopback device.", l->ifname); + + return 0; +} + +int bus_link_method_set_ntp_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_strv_free_ char **ntp = NULL; + Link *l = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read_strv(message, &ntp); + if (r < 0) + return r; + + STRV_FOREACH(i, ntp) { + r = dns_name_is_valid_or_address(*i); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid NTP server: %s", *i); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-ntp-servers", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + strv_free_and_replace(l->ntp, ntp); + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + +static int bus_link_method_set_dns_servers_internal(sd_bus_message *message, void *userdata, sd_bus_error *error, bool extended) { + struct in_addr_full **dns; + Link *l = ASSERT_PTR(userdata); + size_t n; + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = bus_message_read_dns_servers(message, error, extended, &dns, &n); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-dns-servers", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + goto finalize; + if (r == 0) { + r = 1; /* Polkit will call us back */ + goto finalize; + } + + if (l->n_dns != UINT_MAX) + for (unsigned i = 0; i < l->n_dns; i++) + in_addr_full_free(l->dns[i]); + + free_and_replace(l->dns, dns); + l->n_dns = n; + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); + +finalize: + for (size_t i = 0; i < n; i++) + in_addr_full_free(dns[i]); + free(dns); + + return r; +} + +int bus_link_method_set_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return bus_link_method_set_dns_servers_internal(message, userdata, error, false); +} + +int bus_link_method_set_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return bus_link_method_set_dns_servers_internal(message, userdata, error, true); +} + +int bus_link_method_set_domains(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_ordered_set_free_ OrderedSet *search_domains = NULL, *route_domains = NULL; + Link *l = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_enter_container(message, 'a', "(sb)"); + if (r < 0) + return r; + + search_domains = ordered_set_new(&string_hash_ops_free); + if (!search_domains) + return -ENOMEM; + + route_domains = ordered_set_new(&string_hash_ops_free); + if (!route_domains) + return -ENOMEM; + + for (;;) { + _cleanup_free_ char *str = NULL; + const char *name; + int route_only; + + r = sd_bus_message_read(message, "(sb)", &name, &route_only); + if (r < 0) + return r; + if (r == 0) + break; + + r = dns_name_is_valid(name); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid search domain %s", name); + if (!route_only && dns_name_is_root(name)) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Root domain is not suitable as search domain"); + + r = dns_name_normalize(name, 0, &str); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid search domain %s", name); + + r = ordered_set_consume(route_only ? route_domains : search_domains, TAKE_PTR(str)); + if (r == -EEXIST) + continue; + if (r < 0) + return r; + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-domains", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + ordered_set_free(l->search_domains); + ordered_set_free(l->route_domains); + l->search_domains = TAKE_PTR(search_domains); + l->route_domains = TAKE_PTR(route_domains); + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + int r, b; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "b", &b); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-default-route", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + if (l->dns_default_route != b) { + l->dns_default_route = b; + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + ResolveSupport mode; + const char *llmnr; + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "s", &llmnr); + if (r < 0) + return r; + + if (isempty(llmnr)) + mode = RESOLVE_SUPPORT_YES; + else { + mode = resolve_support_from_string(llmnr); + if (mode < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid LLMNR setting: %s", llmnr); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-llmnr", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + if (l->llmnr != mode) { + l->llmnr = mode; + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + ResolveSupport mode; + const char *mdns; + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "s", &mdns); + if (r < 0) + return r; + + if (isempty(mdns)) + mode = RESOLVE_SUPPORT_NO; + else { + mode = resolve_support_from_string(mdns); + if (mode < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid MulticastDNS setting: %s", mdns); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-mdns", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + if (l->mdns != mode) { + l->mdns = mode; + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + const char *dns_over_tls; + DnsOverTlsMode mode; + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "s", &dns_over_tls); + if (r < 0) + return r; + + if (isempty(dns_over_tls)) + mode = _DNS_OVER_TLS_MODE_INVALID; + else { + mode = dns_over_tls_mode_from_string(dns_over_tls); + if (mode < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid DNSOverTLS setting: %s", dns_over_tls); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-dns-over-tls", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + if (l->dns_over_tls_mode != mode) { + l->dns_over_tls_mode = mode; + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + const char *dnssec; + DnssecMode mode; + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "s", &dnssec); + if (r < 0) + return r; + + if (isempty(dnssec)) + mode = _DNSSEC_MODE_INVALID; + else { + mode = dnssec_mode_from_string(dnssec); + if (mode < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid DNSSEC setting: %s", dnssec); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-dnssec", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + if (l->dnssec_mode != mode) { + l->dnssec_mode = mode; + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_set_free_free_ Set *ns = NULL; + _cleanup_strv_free_ char **ntas = NULL; + Link *l = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read_strv(message, &ntas); + if (r < 0) + return r; + + STRV_FOREACH(i, ntas) { + r = dns_name_is_valid(*i); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid negative trust anchor domain: %s", *i); + } + + ns = set_new(&dns_name_hash_ops); + if (!ns) + return -ENOMEM; + + STRV_FOREACH(i, ntas) { + r = set_put_strdup(&ns, *i); + if (r < 0) + return r; + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.set-dnssec-negative-trust-anchors", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + set_free_free(l->dnssec_negative_trust_anchors); + l->dnssec_negative_trust_anchors = TAKE_PTR(ns); + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_revert_ntp(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.revert-ntp", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + link_ntp_settings_clear(l); + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_revert_dns(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = verify_managed_link(l, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.revert-dns", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + link_dns_settings_clear(l); + + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_force_renew(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + int r; + + if (!l->network) + return sd_bus_error_setf(error, BUS_ERROR_UNMANAGED_INTERFACE, + "Interface %s is not managed by systemd-networkd", + l->ifname); + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.forcerenew", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + if (sd_dhcp_server_is_running(l->dhcp_server)) { + r = sd_dhcp_server_forcerenew(l->dhcp_server); + if (r < 0) + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_renew(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + int r; + + if (!l->network) + return sd_bus_error_setf(error, BUS_ERROR_UNMANAGED_INTERFACE, + "Interface %s is not managed by systemd-networkd", + l->ifname); + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.renew", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + r = dhcp4_renew(l); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_reconfigure(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.network1.reconfigure", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + r = link_reconfigure(l, /* force = */ true); + if (r < 0) + return r; + if (r > 0) { + link_set_state(l, LINK_STATE_INITIALIZED); + r = link_save_and_clean_full(l, /* also_save_manager = */ true); + if (r < 0) + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_describe(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ char *text = NULL; + Link *link = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = link_build_json(link, &v); + if (r < 0) + return log_link_error_errno(link, r, "Failed to build JSON data: %m"); + + r = json_variant_format(v, 0, &text); + if (r < 0) + return log_link_error_errno(link, r, "Failed to format JSON data: %m"); + + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "s", text); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static const sd_bus_vtable link_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("OperationalState", "s", property_get_operational_state, offsetof(Link, operstate), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("CarrierState", "s", property_get_carrier_state, offsetof(Link, carrier_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("AddressState", "s", property_get_address_state, offsetof(Link, address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("IPv4AddressState", "s", property_get_address_state, offsetof(Link, ipv4_address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("IPv6AddressState", "s", property_get_address_state, offsetof(Link, ipv6_address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("OnlineState", "s", property_get_online_state, offsetof(Link, online_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("AdministrativeState", "s", property_get_administrative_state, offsetof(Link, state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("BitRates", "(tt)", property_get_bit_rates, 0, 0), + + SD_BUS_METHOD_WITH_ARGS("SetNTP", + SD_BUS_ARGS("as", servers), + SD_BUS_NO_RESULT, + bus_link_method_set_ntp_servers, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNS", + SD_BUS_ARGS("a(iay)", addresses), + SD_BUS_NO_RESULT, + bus_link_method_set_dns_servers, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNSEx", + SD_BUS_ARGS("a(iayqs)", addresses), + SD_BUS_NO_RESULT, + bus_link_method_set_dns_servers_ex, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDomains", + SD_BUS_ARGS("a(sb)", domains), + SD_BUS_NO_RESULT, + bus_link_method_set_domains, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDefaultRoute", + SD_BUS_ARGS("b", enable), + SD_BUS_NO_RESULT, + bus_link_method_set_default_route, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLLMNR", + SD_BUS_ARGS("s", mode), + SD_BUS_NO_RESULT, + bus_link_method_set_llmnr, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetMulticastDNS", + SD_BUS_ARGS("s", mode), + SD_BUS_NO_RESULT, + bus_link_method_set_mdns, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNSOverTLS", + SD_BUS_ARGS("s", mode), + SD_BUS_NO_RESULT, + bus_link_method_set_dns_over_tls, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNSSEC", + SD_BUS_ARGS("s", mode), + SD_BUS_NO_RESULT, + bus_link_method_set_dnssec, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNSSECNegativeTrustAnchors", + SD_BUS_ARGS("as", names), + SD_BUS_NO_RESULT, + bus_link_method_set_dnssec_negative_trust_anchors, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RevertNTP", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_link_method_revert_ntp, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RevertDNS", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_link_method_revert_dns, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("Renew", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_link_method_renew, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ForceRenew", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_link_method_force_renew, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("Reconfigure", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_link_method_reconfigure, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("Describe", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("s", json), + bus_link_method_describe, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_VTABLE_END +}; + +char *link_bus_path(Link *link) { + _cleanup_free_ char *ifindex = NULL; + char *p; + int r; + + assert(link); + assert(link->ifindex > 0); + + if (asprintf(&ifindex, "%d", link->ifindex) < 0) + return NULL; + + r = sd_bus_path_encode("/org/freedesktop/network1/link", ifindex, &p); + if (r < 0) + return NULL; + + return p; +} + +int link_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) { + _cleanup_strv_free_ char **l = NULL; + Manager *m = ASSERT_PTR(userdata); + unsigned c = 0; + Link *link; + + assert(bus); + assert(path); + assert(nodes); + + l = new0(char*, hashmap_size(m->links_by_index) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(link, m->links_by_index) { + char *p; + + p = link_bus_path(link); + if (!p) + return -ENOMEM; + + l[c++] = p; + } + + l[c] = NULL; + *nodes = TAKE_PTR(l); + + return 1; +} + +int link_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { + _cleanup_free_ char *identifier = NULL; + Manager *m = ASSERT_PTR(userdata); + Link *link; + int ifindex, r; + + assert(bus); + assert(path); + assert(interface); + assert(found); + + r = sd_bus_path_decode(path, "/org/freedesktop/network1/link", &identifier); + if (r <= 0) + return 0; + + ifindex = parse_ifindex(identifier); + if (ifindex < 0) + return 0; + + r = link_get_by_index(m, ifindex, &link); + if (r < 0) + return 0; + + if (streq(interface, "org.freedesktop.network1.DHCPServer") && + (!link->dhcp_server || sd_dhcp_server_is_in_relay_mode(link->dhcp_server))) + return 0; + + if (streq(interface, "org.freedesktop.network1.DHCPv4Client") && !link->dhcp_client) + return 0; + + if (streq(interface, "org.freedesktop.network1.DHCPv6Client") && !link->dhcp6_client) + return 0; + + *found = link; + + return 1; +} + +int link_send_changed_strv(Link *link, char **properties) { + _cleanup_free_ char *p = NULL; + + assert(link); + assert(link->manager); + assert(properties); + + if (sd_bus_is_ready(link->manager->bus) <= 0) + return 0; + + p = link_bus_path(link); + if (!p) + return -ENOMEM; + + return sd_bus_emit_properties_changed_strv( + link->manager->bus, + p, + "org.freedesktop.network1.Link", + properties); +} + +int link_send_changed(Link *link, const char *property, ...) { + char **properties; + + properties = strv_from_stdarg_alloca(property); + + return link_send_changed_strv(link, properties); +} + +const BusObjectImplementation link_object = { + "/org/freedesktop/network1/link", + "org.freedesktop.network1.Link", + .fallback_vtables = BUS_FALLBACK_VTABLES({link_vtable, link_object_find}), + .node_enumerator = link_node_enumerator, +}; diff --git a/src/network/networkd-link-bus.h b/src/network/networkd-link-bus.h new file mode 100644 index 0000000..924d997 --- /dev/null +++ b/src/network/networkd-link-bus.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" + +#include "bus-object.h" +#include "macro.h" + +typedef struct Link Link; + +extern const BusObjectImplementation link_object; + +char *link_bus_path(Link *link); +int link_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error); +int link_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error); +int link_send_changed_strv(Link *link, char **properties); +int link_send_changed(Link *link, const char *property, ...) _sentinel_; + +int property_get_operational_state(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error); +int property_get_carrier_state(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error); +int property_get_address_state(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error); +int property_get_online_state(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error); + +int bus_link_method_set_ntp_servers(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_domains(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_revert_ntp(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_revert_dns(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_renew(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_force_renew(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_reconfigure(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_describe(sd_bus_message *message, void *userdata, sd_bus_error *error); diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c new file mode 100644 index 0000000..4ef1be4 --- /dev/null +++ b/src/network/networkd-link.c @@ -0,0 +1,2773 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if.h> +#include <linux/if_arp.h> +#include <linux/if_link.h> +#include <linux/netdevice.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "arphrd-util.h" +#include "batadv.h" +#include "bond.h" +#include "bridge.h" +#include "bus-util.h" +#include "device-private.h" +#include "device-util.h" +#include "dhcp-identifier.h" +#include "dhcp-lease-internal.h" +#include "env-file.h" +#include "ethtool-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "glyph-util.h" +#include "logarithm.h" +#include "missing_network.h" +#include "netlink-util.h" +#include "network-internal.h" +#include "networkd-address-label.h" +#include "networkd-address.h" +#include "networkd-bridge-fdb.h" +#include "networkd-bridge-mdb.h" +#include "networkd-can.h" +#include "networkd-dhcp-prefix-delegation.h" +#include "networkd-dhcp-server.h" +#include "networkd-dhcp4.h" +#include "networkd-dhcp6.h" +#include "networkd-ipv4acd.h" +#include "networkd-ipv4ll.h" +#include "networkd-ipv6-proxy-ndp.h" +#include "networkd-link-bus.h" +#include "networkd-link.h" +#include "networkd-lldp-tx.h" +#include "networkd-manager.h" +#include "networkd-ndisc.h" +#include "networkd-neighbor.h" +#include "networkd-nexthop.h" +#include "networkd-queue.h" +#include "networkd-radv.h" +#include "networkd-route-util.h" +#include "networkd-route.h" +#include "networkd-routing-policy-rule.h" +#include "networkd-setlink.h" +#include "networkd-sriov.h" +#include "networkd-state-file.h" +#include "networkd-sysctl.h" +#include "networkd-wifi.h" +#include "set.h" +#include "socket-util.h" +#include "stdio-util.h" +#include "string-table.h" +#include "strv.h" +#include "tc.h" +#include "tmpfile-util.h" +#include "tuntap.h" +#include "udev-util.h" +#include "vrf.h" + +bool link_ipv6_enabled(Link *link) { + assert(link); + + if (!socket_ipv6_is_supported()) + return false; + + if (link->iftype == ARPHRD_CAN) + return false; + + if (!link->network) + return false; + + if (link->network->bond) + return false; + + if (link_may_have_ipv6ll(link, /* check_multicast = */ false)) + return true; + + if (network_has_static_ipv6_configurations(link->network)) + return true; + + return false; +} + +bool link_has_ipv6_connectivity(Link *link) { + LinkAddressState ipv6_address_state; + + assert(link); + + link_get_address_states(link, NULL, &ipv6_address_state, NULL); + + switch (ipv6_address_state) { + case LINK_ADDRESS_STATE_ROUTABLE: + /* If the interface has a routable IPv6 address, then we assume yes. */ + return true; + + case LINK_ADDRESS_STATE_DEGRADED: + /* If the interface has only degraded IPv6 address (mostly, link-local address), then let's check + * there is an IPv6 default gateway. */ + return link_has_default_gateway(link, AF_INET6); + + case LINK_ADDRESS_STATE_OFF: + /* No IPv6 address. */ + return false; + + default: + assert_not_reached(); + } +} + +static bool link_is_ready_to_configure_one(Link *link, bool allow_unmanaged) { + assert(link); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED, LINK_STATE_UNMANAGED)) + return false; + + if (!link->network) + return allow_unmanaged; + + if (!link->network->configure_without_carrier) { + if (link->set_flags_messages > 0) + return false; + + if (!link_has_carrier(link)) + return false; + } + + if (link->set_link_messages > 0) + return false; + + if (!link->activated) + return false; + + return true; +} + +bool link_is_ready_to_configure(Link *link, bool allow_unmanaged) { + return check_ready_for_all_sr_iov_ports(link, allow_unmanaged, link_is_ready_to_configure_one); +} + +void link_ntp_settings_clear(Link *link) { + link->ntp = strv_free(link->ntp); +} + +void link_dns_settings_clear(Link *link) { + if (link->n_dns != UINT_MAX) + for (unsigned i = 0; i < link->n_dns; i++) + in_addr_full_free(link->dns[i]); + link->dns = mfree(link->dns); + link->n_dns = UINT_MAX; + + link->search_domains = ordered_set_free(link->search_domains); + link->route_domains = ordered_set_free(link->route_domains); + + link->dns_default_route = -1; + link->llmnr = _RESOLVE_SUPPORT_INVALID; + link->mdns = _RESOLVE_SUPPORT_INVALID; + link->dnssec_mode = _DNSSEC_MODE_INVALID; + link->dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID; + + link->dnssec_negative_trust_anchors = set_free_free(link->dnssec_negative_trust_anchors); +} + +static void link_free_engines(Link *link) { + if (!link) + return; + + link->dhcp_server = sd_dhcp_server_unref(link->dhcp_server); + + link->dhcp_client = sd_dhcp_client_unref(link->dhcp_client); + link->dhcp_lease = sd_dhcp_lease_unref(link->dhcp_lease); + link->dhcp4_6rd_tunnel_name = mfree(link->dhcp4_6rd_tunnel_name); + + link->lldp_rx = sd_lldp_rx_unref(link->lldp_rx); + link->lldp_tx = sd_lldp_tx_unref(link->lldp_tx); + + link->ipv4acd_by_address = hashmap_free(link->ipv4acd_by_address); + + link->ipv4ll = sd_ipv4ll_unref(link->ipv4ll); + + link->dhcp6_client = sd_dhcp6_client_unref(link->dhcp6_client); + link->dhcp6_lease = sd_dhcp6_lease_unref(link->dhcp6_lease); + + link->ndisc = sd_ndisc_unref(link->ndisc); + link->ndisc_expire = sd_event_source_disable_unref(link->ndisc_expire); + ndisc_flush(link); + + link->radv = sd_radv_unref(link->radv); +} + +static Link *link_free(Link *link) { + assert(link); + + link_ntp_settings_clear(link); + link_dns_settings_clear(link); + + link->routes = set_free(link->routes); + link->nexthops = set_free(link->nexthops); + link->neighbors = set_free(link->neighbors); + link->addresses = set_free(link->addresses); + link->qdiscs = set_free(link->qdiscs); + link->tclasses = set_free(link->tclasses); + + link->dhcp_pd_prefixes = set_free(link->dhcp_pd_prefixes); + + link_free_engines(link); + + set_free(link->sr_iov_virt_port_ifindices); + free(link->ifname); + strv_free(link->alternative_names); + free(link->kind); + free(link->ssid); + free(link->previous_ssid); + free(link->driver); + + unlink_and_free(link->lease_file); + unlink_and_free(link->lldp_file); + unlink_and_free(link->state_file); + + sd_device_unref(link->dev); + netdev_unref(link->netdev); + + hashmap_free(link->bound_to_links); + hashmap_free(link->bound_by_links); + + set_free_with_destructor(link->slaves, link_unref); + + network_unref(link->network); + + sd_event_source_disable_unref(link->carrier_lost_timer); + + return mfree(link); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(Link, link, link_free); + +int link_get_by_index(Manager *m, int ifindex, Link **ret) { + Link *link; + + assert(m); + assert(ifindex > 0); + + link = hashmap_get(m->links_by_index, INT_TO_PTR(ifindex)); + if (!link) + return -ENODEV; + + if (ret) + *ret = link; + return 0; +} + +int link_get_by_name(Manager *m, const char *ifname, Link **ret) { + Link *link; + + assert(m); + assert(ifname); + + link = hashmap_get(m->links_by_name, ifname); + if (!link) + return -ENODEV; + + if (ret) + *ret = link; + return 0; +} + +int link_get_by_hw_addr(Manager *m, const struct hw_addr_data *hw_addr, Link **ret) { + Link *link; + + assert(m); + assert(hw_addr); + + link = hashmap_get(m->links_by_hw_addr, hw_addr); + if (!link) + return -ENODEV; + + if (ret) + *ret = link; + return 0; +} + +int link_get_master(Link *link, Link **ret) { + assert(link); + assert(link->manager); + assert(ret); + + if (link->master_ifindex <= 0 || link->master_ifindex == link->ifindex) + return -ENODEV; + + return link_get_by_index(link->manager, link->master_ifindex, ret); +} + +void link_set_state(Link *link, LinkState state) { + assert(link); + + if (link->state == state) + return; + + log_link_debug(link, "State changed: %s -> %s", + link_state_to_string(link->state), + link_state_to_string(state)); + + link->state = state; + + link_send_changed(link, "AdministrativeState", NULL); + link_dirty(link); +} + +int link_stop_engines(Link *link, bool may_keep_dhcp) { + int r = 0, k; + + assert(link); + assert(link->manager); + assert(link->manager->event); + + bool keep_dhcp = may_keep_dhcp && + link->network && + !link->network->dhcp_send_decline && /* IPv4 ACD for the DHCPv4 address is running. */ + (link->manager->restarting || + FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP_ON_STOP)); + + if (!keep_dhcp) { + k = sd_dhcp_client_stop(link->dhcp_client); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop DHCPv4 client: %m"); + } + + k = sd_dhcp_server_stop(link->dhcp_server); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop DHCPv4 server: %m"); + + k = sd_lldp_rx_stop(link->lldp_rx); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop LLDP Rx: %m"); + + k = sd_lldp_tx_stop(link->lldp_tx); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop LLDP Tx: %m"); + + k = sd_ipv4ll_stop(link->ipv4ll); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop IPv4 link-local: %m"); + + k = ipv4acd_stop(link); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop IPv4 ACD client: %m"); + + k = sd_dhcp6_client_stop(link->dhcp6_client); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop DHCPv6 client: %m"); + + k = dhcp_pd_remove(link, /* only_marked = */ false); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not remove DHCPv6 PD addresses and routes: %m"); + + k = ndisc_stop(link); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop IPv6 Router Discovery: %m"); + + ndisc_flush(link); + + k = sd_radv_stop(link->radv); + if (k < 0) + r = log_link_warning_errno(link, k, "Could not stop IPv6 Router Advertisement: %m"); + + return r; +} + +void link_enter_failed(Link *link) { + assert(link); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return; + + log_link_warning(link, "Failed"); + + link_set_state(link, LINK_STATE_FAILED); + + (void) link_stop_engines(link, false); +} + +void link_check_ready(Link *link) { + Address *a; + + assert(link); + + if (link->state == LINK_STATE_CONFIGURED) + return; + + if (link->state != LINK_STATE_CONFIGURING) + return (void) log_link_debug(link, "%s(): link is in %s state.", __func__, link_state_to_string(link->state)); + + if (!link->network) + return (void) log_link_debug(link, "%s(): link is unmanaged.", __func__); + + if (!link->tc_configured) + return (void) log_link_debug(link, "%s(): traffic controls are not configured.", __func__); + + if (link->set_link_messages > 0) + return (void) log_link_debug(link, "%s(): link layer is configuring.", __func__); + + if (!link->activated) + return (void) log_link_debug(link, "%s(): link is not activated.", __func__); + + if (link->iftype == ARPHRD_CAN) { + /* let's shortcut things for CAN which doesn't need most of checks below. */ + link_set_state(link, LINK_STATE_CONFIGURED); + return; + } + + if (!link->stacked_netdevs_created) + return (void) log_link_debug(link, "%s(): stacked netdevs are not created.", __func__); + + if (!link->static_addresses_configured) + return (void) log_link_debug(link, "%s(): static addresses are not configured.", __func__); + + if (!link->static_address_labels_configured) + return (void) log_link_debug(link, "%s(): static address labels are not configured.", __func__); + + if (!link->static_bridge_fdb_configured) + return (void) log_link_debug(link, "%s(): static bridge MDB entries are not configured.", __func__); + + if (!link->static_bridge_mdb_configured) + return (void) log_link_debug(link, "%s(): static bridge MDB entries are not configured.", __func__); + + if (!link->static_ipv6_proxy_ndp_configured) + return (void) log_link_debug(link, "%s(): static IPv6 proxy NDP addresses are not configured.", __func__); + + if (!link->static_neighbors_configured) + return (void) log_link_debug(link, "%s(): static neighbors are not configured.", __func__); + + if (!link->static_nexthops_configured) + return (void) log_link_debug(link, "%s(): static nexthops are not configured.", __func__); + + if (!link->static_routes_configured) + return (void) log_link_debug(link, "%s(): static routes are not configured.", __func__); + + if (!link->static_routing_policy_rules_configured) + return (void) log_link_debug(link, "%s(): static routing policy rules are not configured.", __func__); + + if (!link->sr_iov_configured) + return (void) log_link_debug(link, "%s(): SR-IOV is not configured.", __func__); + + /* IPv6LL is assigned after the link gains its carrier. */ + if (!link->network->configure_without_carrier && + link_ipv6ll_enabled(link) && + !in6_addr_is_set(&link->ipv6ll_address)) + return (void) log_link_debug(link, "%s(): IPv6LL is not configured yet.", __func__); + + /* All static addresses must be ready. */ + bool has_static_address = false; + SET_FOREACH(a, link->addresses) { + if (a->source != NETWORK_CONFIG_SOURCE_STATIC) + continue; + if (!address_is_ready(a)) + return (void) log_link_debug(link, "%s(): static address %s is not ready.", __func__, + IN_ADDR_PREFIX_TO_STRING(a->family, &a->in_addr, a->prefixlen)); + has_static_address = true; + } + + /* If at least one static address is requested, do not request that dynamic addressing protocols are finished. */ + if (has_static_address) + goto ready; + + /* If no dynamic addressing protocol enabled, assume the interface is ready. + * Note, ignore NDisc when ConfigureWithoutCarrier= is enabled, as IPv6AcceptRA= is enabled by default. */ + if (!link_ipv4ll_enabled(link) && !link_dhcp4_enabled(link) && + !link_dhcp6_enabled(link) && !link_dhcp_pd_is_enabled(link) && + (link->network->configure_without_carrier || !link_ipv6_accept_ra_enabled(link))) + goto ready; + + bool ipv4ll_ready = + link_ipv4ll_enabled(link) && link->ipv4ll_address_configured && + link_check_addresses_ready(link, NETWORK_CONFIG_SOURCE_IPV4LL); + bool dhcp4_ready = + link_dhcp4_enabled(link) && link->dhcp4_configured && + link_check_addresses_ready(link, NETWORK_CONFIG_SOURCE_DHCP4); + bool dhcp6_ready = + link_dhcp6_enabled(link) && link->dhcp6_configured && + (!link->network->dhcp6_use_address || + link_check_addresses_ready(link, NETWORK_CONFIG_SOURCE_DHCP6)); + bool dhcp_pd_ready = + link_dhcp_pd_is_enabled(link) && link->dhcp_pd_configured && + (!link->network->dhcp_pd_assign || + link_check_addresses_ready(link, NETWORK_CONFIG_SOURCE_DHCP_PD)); + bool ndisc_ready = + link_ipv6_accept_ra_enabled(link) && link->ndisc_configured && + (!link->network->ipv6_accept_ra_use_autonomous_prefix || + link_check_addresses_ready(link, NETWORK_CONFIG_SOURCE_NDISC)); + + /* If the uplink for PD is self, then request the corresponding DHCP protocol is also ready. */ + if (dhcp_pd_is_uplink(link, link, /* accept_auto = */ false)) { + if (link_dhcp4_enabled(link) && link->network->dhcp_use_6rd && + sd_dhcp_lease_has_6rd(link->dhcp_lease)) { + if (!link->dhcp4_configured) + return (void) log_link_debug(link, "%s(): DHCPv4 6rd prefix is assigned, but DHCPv4 protocol is not finished yet.", __func__); + if (!dhcp_pd_ready) + return (void) log_link_debug(link, "%s(): DHCPv4 is finished, but prefix acquired by DHCPv4-6rd is not assigned yet.", __func__); + } + + if (link_dhcp6_enabled(link) && link->network->dhcp6_use_pd_prefix && + sd_dhcp6_lease_has_pd_prefix(link->dhcp6_lease)) { + if (!link->dhcp6_configured) + return (void) log_link_debug(link, "%s(): DHCPv6 IA_PD prefix is assigned, but DHCPv6 protocol is not finished yet.", __func__); + if (!dhcp_pd_ready) + return (void) log_link_debug(link, "%s(): DHCPv6 is finished, but prefix acquired by DHCPv6 IA_PD is not assigned yet.", __func__); + } + } + + /* At least one dynamic addressing protocol is finished. */ + if (!ipv4ll_ready && !dhcp4_ready && !dhcp6_ready && !dhcp_pd_ready && !ndisc_ready) + return (void) log_link_debug(link, "%s(): dynamic addressing protocols are enabled but none of them finished yet.", __func__); + + log_link_debug(link, "%s(): IPv4LL:%s DHCPv4:%s DHCPv6:%s DHCP-PD:%s NDisc:%s", + __func__, + yes_no(ipv4ll_ready), + yes_no(dhcp4_ready), + yes_no(dhcp6_ready), + yes_no(dhcp_pd_ready), + yes_no(ndisc_ready)); + +ready: + link_set_state(link, LINK_STATE_CONFIGURED); +} + +static int link_request_static_configs(Link *link) { + int r; + + assert(link); + assert(link->network); + assert(link->state != _LINK_STATE_INVALID); + + r = link_request_static_addresses(link); + if (r < 0) + return r; + + r = link_request_static_address_labels(link); + if (r < 0) + return r; + + r = link_request_static_bridge_fdb(link); + if (r < 0) + return r; + + r = link_request_static_bridge_mdb(link); + if (r < 0) + return r; + + r = link_request_static_ipv6_proxy_ndp_addresses(link); + if (r < 0) + return r; + + r = link_request_static_neighbors(link); + if (r < 0) + return r; + + r = link_request_static_nexthops(link, false); + if (r < 0) + return r; + + r = link_request_static_routes(link, false); + if (r < 0) + return r; + + r = link_request_static_routing_policy_rules(link); + if (r < 0) + return r; + + return 0; +} + +static int link_request_stacked_netdevs(Link *link) { + NetDev *netdev; + int r; + + assert(link); + + link->stacked_netdevs_created = false; + + HASHMAP_FOREACH(netdev, link->network->stacked_netdevs) { + r = link_request_stacked_netdev(link, netdev); + if (r < 0) + return r; + } + + if (link->create_stacked_netdev_messages == 0) { + link->stacked_netdevs_created = true; + link_check_ready(link); + } + + return 0; +} + +static int link_acquire_dynamic_ipv6_conf(Link *link) { + int r; + + assert(link); + + r = radv_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start IPv6 Router Advertisement engine: %m"); + + r = ndisc_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m"); + + r = dhcp6_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start DHCPv6 client: %m"); + + return 0; +} + +static int link_acquire_dynamic_ipv4_conf(Link *link) { + int r; + + assert(link); + assert(link->manager); + assert(link->manager->event); + + if (link->dhcp_client) { + r = dhcp4_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start DHCPv4 client: %m"); + + log_link_debug(link, "Acquiring DHCPv4 lease."); + + } else if (link->ipv4ll) { + if (in4_addr_is_set(&link->network->ipv4ll_start_address)) { + r = sd_ipv4ll_set_address(link->ipv4ll, &link->network->ipv4ll_start_address); + if (r < 0) + return log_link_warning_errno(link, r, "Could not set IPv4 link-local start address: %m"); + } + + r = sd_ipv4ll_start(link->ipv4ll); + if (r < 0) + return log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m"); + + log_link_debug(link, "Acquiring IPv4 link-local address."); + } + + if (link->dhcp_server) { + r = sd_dhcp_server_start(link->dhcp_server); + if (r < 0) + return log_link_warning_errno(link, r, "Could not start DHCP server: %m"); + } + + r = ipv4acd_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Could not start IPv4 ACD client: %m"); + + return 0; +} + +static int link_acquire_dynamic_conf(Link *link) { + int r; + + assert(link); + assert(link->network); + + r = link_acquire_dynamic_ipv4_conf(link); + if (r < 0) + return r; + + if (in6_addr_is_set(&link->ipv6ll_address)) { + r = link_acquire_dynamic_ipv6_conf(link); + if (r < 0) + return r; + } + + if (!link_radv_enabled(link) || !link->network->dhcp_pd_announce) { + /* DHCPv6PD downstream does not require IPv6LL address. But may require RADV to be + * configured, and RADV may not be configured yet here. Only acquire subnet prefix when + * RADV is disabled, or the announcement of the prefix is disabled. Otherwise, the + * below will be called in radv_start(). */ + r = dhcp_request_prefix_delegation(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request DHCP delegated subnet prefix: %m"); + } + + if (link->lldp_tx) { + r = sd_lldp_tx_start(link->lldp_tx); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start LLDP transmission: %m"); + } + + if (link->lldp_rx) { + r = sd_lldp_rx_start(link->lldp_rx); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start LLDP client: %m"); + } + + return 0; +} + +int link_ipv6ll_gained(Link *link) { + int r; + + assert(link); + + log_link_info(link, "Gained IPv6LL"); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return 0; + + r = link_acquire_dynamic_ipv6_conf(link); + if (r < 0) + return r; + + link_check_ready(link); + return 0; +} + +int link_handle_bound_to_list(Link *link) { + bool required_up = false; + bool link_is_up = false; + Link *l; + + assert(link); + + /* If at least one interface in bound_to_links has carrier, then make this interface up. + * If all interfaces in bound_to_links do not, then make this interface down. */ + + if (hashmap_isempty(link->bound_to_links)) + return 0; + + if (link->flags & IFF_UP) + link_is_up = true; + + HASHMAP_FOREACH(l, link->bound_to_links) + if (link_has_carrier(l)) { + required_up = true; + break; + } + + if (!required_up && link_is_up) + return link_request_to_bring_up_or_down(link, /* up = */ false); + if (required_up && !link_is_up) + return link_request_to_bring_up_or_down(link, /* up = */ true); + + return 0; +} + +static int link_handle_bound_by_list(Link *link) { + Link *l; + int r; + + assert(link); + + /* Update up or down state of interfaces which depend on this interface's carrier state. */ + + if (hashmap_isempty(link->bound_by_links)) + return 0; + + HASHMAP_FOREACH(l, link->bound_by_links) { + r = link_handle_bound_to_list(l); + if (r < 0) + return r; + } + + return 0; +} + +static int link_put_carrier(Link *link, Link *carrier, Hashmap **h) { + int r; + + assert(link); + assert(carrier); + + if (link == carrier) + return 0; + + if (hashmap_get(*h, INT_TO_PTR(carrier->ifindex))) + return 0; + + r = hashmap_ensure_put(h, NULL, INT_TO_PTR(carrier->ifindex), carrier); + if (r < 0) + return r; + + link_dirty(link); + + return 0; +} + +static int link_new_bound_by_list(Link *link) { + Manager *m; + Link *carrier; + int r; + + assert(link); + assert(link->manager); + + m = link->manager; + + HASHMAP_FOREACH(carrier, m->links_by_index) { + if (!carrier->network) + continue; + + if (strv_isempty(carrier->network->bind_carrier)) + continue; + + if (strv_fnmatch(carrier->network->bind_carrier, link->ifname)) { + r = link_put_carrier(link, carrier, &link->bound_by_links); + if (r < 0) + return r; + } + } + + HASHMAP_FOREACH(carrier, link->bound_by_links) { + r = link_put_carrier(carrier, link, &carrier->bound_to_links); + if (r < 0) + return r; + } + + return 0; +} + +static int link_new_bound_to_list(Link *link) { + Manager *m; + Link *carrier; + int r; + + assert(link); + assert(link->manager); + + if (!link->network) + return 0; + + if (strv_isempty(link->network->bind_carrier)) + return 0; + + m = link->manager; + + HASHMAP_FOREACH(carrier, m->links_by_index) { + if (strv_fnmatch(link->network->bind_carrier, carrier->ifname)) { + r = link_put_carrier(link, carrier, &link->bound_to_links); + if (r < 0) + return r; + } + } + + HASHMAP_FOREACH(carrier, link->bound_to_links) { + r = link_put_carrier(carrier, link, &carrier->bound_by_links); + if (r < 0) + return r; + } + + return 0; +} + +static void link_free_bound_to_list(Link *link) { + bool updated = false; + Link *bound_to; + + assert(link); + + while ((bound_to = hashmap_steal_first(link->bound_to_links))) { + updated = true; + + if (hashmap_remove(bound_to->bound_by_links, INT_TO_PTR(link->ifindex))) + link_dirty(bound_to); + } + + if (updated) + link_dirty(link); +} + +static void link_free_bound_by_list(Link *link) { + bool updated = false; + Link *bound_by; + + assert(link); + + while ((bound_by = hashmap_steal_first(link->bound_by_links))) { + updated = true; + + if (hashmap_remove(bound_by->bound_to_links, INT_TO_PTR(link->ifindex))) { + link_dirty(bound_by); + link_handle_bound_to_list(bound_by); + } + } + + if (updated) + link_dirty(link); +} + +static int link_append_to_master(Link *link) { + Link *master; + int r; + + assert(link); + + /* - The link may have no master. + * - RTM_NEWLINK message about master interface may not be received yet. */ + if (link_get_master(link, &master) < 0) + return 0; + + r = set_ensure_put(&master->slaves, NULL, link); + if (r <= 0) + return r; + + link_ref(link); + return 0; +} + +static void link_drop_from_master(Link *link) { + Link *master; + + assert(link); + + if (!link->manager) + return; + + if (link_get_master(link, &master) < 0) + return; + + link_unref(set_remove(master->slaves, link)); +} + +static void link_drop_requests(Link *link) { + Request *req; + + assert(link); + assert(link->manager); + + ORDERED_SET_FOREACH(req, link->manager->request_queue) + if (req->link == link) + request_detach(link->manager, req); +} + +static Link *link_drop(Link *link) { + if (!link) + return NULL; + + assert(link->manager); + + link_set_state(link, LINK_STATE_LINGER); + + /* Drop all references from other links and manager. Note that async netlink calls may have + * references to the link, and they will be dropped when we receive replies. */ + + link_drop_requests(link); + + link_free_bound_to_list(link); + link_free_bound_by_list(link); + + link_clear_sr_iov_ifindices(link); + + link_drop_from_master(link); + + if (link->state_file) + (void) unlink(link->state_file); + + link_clean(link); + + STRV_FOREACH(n, link->alternative_names) + hashmap_remove(link->manager->links_by_name, *n); + hashmap_remove(link->manager->links_by_name, link->ifname); + + /* bonding master and its slaves have the same hardware address. */ + hashmap_remove_value(link->manager->links_by_hw_addr, &link->hw_addr, link); + + /* The following must be called at last. */ + assert_se(hashmap_remove(link->manager->links_by_index, INT_TO_PTR(link->ifindex)) == link); + return link_unref(link); +} + +static int link_drop_foreign_config(Link *link) { + int r; + + assert(link); + assert(link->manager); + + /* Drop foreign config, but ignore unmanaged, loopback, or critical interfaces. We do not want + * to remove loopback address or addresses used for root NFS. */ + + if (IN_SET(link->state, LINK_STATE_UNMANAGED, LINK_STATE_PENDING, LINK_STATE_INITIALIZED)) + return 0; + if (FLAGS_SET(link->flags, IFF_LOOPBACK)) + return 0; + if (link->network->keep_configuration == KEEP_CONFIGURATION_YES) + return 0; + + r = link_drop_foreign_routes(link); + + RET_GATHER(r, link_drop_foreign_nexthops(link)); + RET_GATHER(r, link_drop_foreign_addresses(link)); + RET_GATHER(r, link_drop_foreign_neighbors(link)); + RET_GATHER(r, manager_drop_foreign_routing_policy_rules(link->manager)); + + return r; +} + +static int link_drop_managed_config(Link *link) { + int r; + + assert(link); + assert(link->manager); + + r = link_drop_managed_routes(link); + + RET_GATHER(r, link_drop_managed_nexthops(link)); + RET_GATHER(r, link_drop_managed_addresses(link)); + RET_GATHER(r, link_drop_managed_neighbors(link)); + RET_GATHER(r, link_drop_managed_routing_policy_rules(link)); + + return r; +} + +static void link_foreignize_config(Link *link) { + assert(link); + assert(link->manager); + + link_foreignize_routes(link); + link_foreignize_nexthops(link); + link_foreignize_addresses(link); + link_foreignize_neighbors(link); + link_foreignize_routing_policy_rules(link); +} + +static int link_configure(Link *link) { + int r; + + assert(link); + assert(link->network); + assert(link->state == LINK_STATE_INITIALIZED); + + link_set_state(link, LINK_STATE_CONFIGURING); + + r = link_new_bound_to_list(link); + if (r < 0) + return r; + + r = link_request_traffic_control(link); + if (r < 0) + return r; + + r = link_configure_mtu(link); + if (r < 0) + return r; + + if (link->iftype == ARPHRD_CAN) { + /* let's shortcut things for CAN which doesn't need most of what's done below. */ + r = link_request_to_set_can(link); + if (r < 0) + return r; + + return link_request_to_activate(link); + } + + r = link_request_sr_iov_vfs(link); + if (r < 0) + return r; + + r = link_set_sysctl(link); + if (r < 0) + return r; + + r = link_request_to_set_mac(link, /* allow_retry = */ true); + if (r < 0) + return r; + + r = link_request_to_set_ipoib(link); + if (r < 0) + return r; + + r = link_request_to_set_flags(link); + if (r < 0) + return r; + + r = link_request_to_set_group(link); + if (r < 0) + return r; + + r = link_request_to_set_addrgen_mode(link); + if (r < 0) + return r; + + r = link_request_to_set_master(link); + if (r < 0) + return r; + + r = link_request_stacked_netdevs(link); + if (r < 0) + return r; + + r = link_request_to_set_bond(link); + if (r < 0) + return r; + + r = link_request_to_set_bridge(link); + if (r < 0) + return r; + + r = link_request_to_set_bridge_vlan(link); + if (r < 0) + return r; + + r = link_request_to_activate(link); + if (r < 0) + return r; + + r = ipv4ll_configure(link); + if (r < 0) + return r; + + r = link_request_dhcp4_client(link); + if (r < 0) + return r; + + r = link_request_dhcp6_client(link); + if (r < 0) + return r; + + r = link_request_ndisc(link); + if (r < 0) + return r; + + r = link_request_dhcp_server(link); + if (r < 0) + return r; + + r = link_request_radv(link); + if (r < 0) + return r; + + r = link_lldp_rx_configure(link); + if (r < 0) + return r; + + r = link_lldp_tx_configure(link); + if (r < 0) + return r; + + r = link_drop_foreign_config(link); + if (r < 0) + return r; + + r = link_request_static_configs(link); + if (r < 0) + return r; + + if (!link_has_carrier(link)) + return 0; + + return link_acquire_dynamic_conf(link); +} + +static int link_get_network(Link *link, Network **ret) { + Network *network; + int r; + + assert(link); + assert(link->manager); + assert(ret); + + ORDERED_HASHMAP_FOREACH(network, link->manager->networks) { + bool warn = false; + + r = net_match_config( + &network->match, + link->dev, + &link->hw_addr, + &link->permanent_hw_addr, + link->driver, + link->iftype, + link->kind, + link->ifname, + link->alternative_names, + link->wlan_iftype, + link->ssid, + &link->bssid); + if (r < 0) + return r; + if (r == 0) + continue; + + if (network->match.ifname && link->dev) { + uint8_t name_assign_type = NET_NAME_UNKNOWN; + const char *attr; + + if (sd_device_get_sysattr_value(link->dev, "name_assign_type", &attr) >= 0) + (void) safe_atou8(attr, &name_assign_type); + + warn = name_assign_type == NET_NAME_ENUM; + } + + log_link_full(link, warn ? LOG_WARNING : LOG_DEBUG, + "found matching network '%s'%s.", + network->filename, + warn ? ", based on potentially unpredictable interface name" : ""); + + if (network->unmanaged) + return -ENOENT; + + *ret = network; + return 0; + } + + return -ENOENT; +} + +int link_reconfigure_impl(Link *link, bool force) { + Network *network = NULL; + NetDev *netdev = NULL; + int r; + + assert(link); + + if (IN_SET(link->state, LINK_STATE_PENDING, LINK_STATE_LINGER)) + return 0; + + r = netdev_get(link->manager, link->ifname, &netdev); + if (r < 0 && r != -ENOENT) + return r; + + r = link_get_network(link, &network); + if (r < 0 && r != -ENOENT) + return r; + + if (link->state != LINK_STATE_UNMANAGED && !network) + /* If link is in initialized state, then link->network is also NULL. */ + force = true; + + if (link->network == network && !force) + return 0; + + if (network) { + if (link->state == LINK_STATE_INITIALIZED) + log_link_info(link, "Configuring with %s.", network->filename); + else + log_link_info(link, "Reconfiguring with %s.", network->filename); + } else + log_link_full(link, link->state == LINK_STATE_INITIALIZED ? LOG_DEBUG : LOG_INFO, + "Unmanaging interface."); + + /* Dropping old .network file */ + r = link_stop_engines(link, false); + if (r < 0) + return r; + + link_drop_requests(link); + + if (network && !force && network->keep_configuration != KEEP_CONFIGURATION_YES) + /* When a new/updated .network file is assigned, first make all configs (addresses, + * routes, and so on) foreign, and then drop unnecessary configs later by + * link_drop_foreign_config() in link_configure(). + * Note, when KeepConfiguration=yes, link_drop_foreign_config() does nothing. Hence, + * here we need to drop the configs such as addresses, routes, and so on configured by + * the previously assigned .network file. */ + link_foreignize_config(link); + else { + /* Remove all managed configs. Note, foreign configs are removed in later by + * link_configure() -> link_drop_foreign_config() if the link is managed by us. */ + r = link_drop_managed_config(link); + if (r < 0) + return r; + } + + /* The bound_to map depends on .network file, hence it needs to be freed. But, do not free the + * bound_by map. Otherwise, if a link enters unmanaged state below, then its carrier state will + * not propagated to other interfaces anymore. Moreover, it is not necessary to recreate the + * map here, as it depends on .network files assigned to other links. */ + link_free_bound_to_list(link); + + link_free_engines(link); + link->network = network_unref(link->network); + + netdev_unref(link->netdev); + link->netdev = netdev_ref(netdev); + + if (!network) { + link_set_state(link, LINK_STATE_UNMANAGED); + return 0; + } + + /* Then, apply new .network file */ + link->network = network_ref(network); + link_update_operstate(link, true); + link_dirty(link); + + link_set_state(link, LINK_STATE_INITIALIZED); + link->activated = false; + + r = link_configure(link); + if (r < 0) + return r; + + return 1; +} + +static int link_reconfigure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, bool force) { + int r; + + assert(link); + + r = link_getlink_handler_internal(rtnl, m, link, "Failed to update link state"); + if (r <= 0) + return r; + + r = link_reconfigure_impl(link, force); + if (r < 0) { + link_enter_failed(link); + return 0; + } + + return r; +} + +static int link_reconfigure_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + return link_reconfigure_handler_internal(rtnl, m, link, /* force = */ false); +} + +static int link_force_reconfigure_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + return link_reconfigure_handler_internal(rtnl, m, link, /* force = */ true); +} + +int link_reconfigure(Link *link, bool force) { + int r; + + assert(link); + + /* When link in pending or initialized state, then link_configure() will be called. To prevent + * the function from being called multiple times simultaneously, refuse to reconfigure the + * interface in these cases. */ + if (IN_SET(link->state, LINK_STATE_PENDING, LINK_STATE_INITIALIZED, LINK_STATE_LINGER)) + return 0; /* 0 means no-op. */ + + r = link_call_getlink(link, force ? link_force_reconfigure_handler : link_reconfigure_handler); + if (r < 0) + return r; + + return 1; /* 1 means the interface will be reconfigured. */ +} + +static int link_initialized_and_synced(Link *link) { + int r; + + assert(link); + assert(link->manager); + + if (link->manager->test_mode) { + log_link_debug(link, "Running in test mode, refusing to enter initialized state."); + link_set_state(link, LINK_STATE_UNMANAGED); + return 0; + } + + if (link->state == LINK_STATE_PENDING) { + log_link_debug(link, "Link state is up-to-date"); + link_set_state(link, LINK_STATE_INITIALIZED); + + r = link_new_bound_by_list(link); + if (r < 0) + return r; + + r = link_handle_bound_by_list(link); + if (r < 0) + return r; + } + + return link_reconfigure_impl(link, /* force = */ false); +} + +static int link_initialized_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + r = link_getlink_handler_internal(rtnl, m, link, "Failed to wait for the interface to be initialized"); + if (r <= 0) + return r; + + r = link_initialized_and_synced(link); + if (r < 0) + link_enter_failed(link); + + return 0; +} + +static int link_initialized(Link *link, sd_device *device) { + int r; + + assert(link); + assert(device); + + /* Always replace with the new sd_device object. As the sysname (and possibly other properties + * or sysattrs) may be outdated. */ + device_unref_and_replace(link->dev, device); + + if (link->dhcp_client) { + r = sd_dhcp_client_attach_device(link->dhcp_client, link->dev); + if (r < 0) + log_link_warning_errno(link, r, "Failed to attach device to DHCPv4 client, ignoring: %m"); + } + + if (link->dhcp6_client) { + r = sd_dhcp6_client_attach_device(link->dhcp6_client, link->dev); + if (r < 0) + log_link_warning_errno(link, r, "Failed to attach device to DHCPv6 client, ignoring: %m"); + } + + r = link_set_sr_iov_ifindices(link); + if (r < 0) + log_link_warning_errno(link, r, "Failed to manage SR-IOV PF and VF ports, ignoring: %m"); + + if (link->state != LINK_STATE_PENDING) + return link_reconfigure(link, /* force = */ false); + + log_link_debug(link, "udev initialized link"); + + /* udev has initialized the link, but we don't know if we have yet + * processed the NEWLINK messages with the latest state. Do a GETLINK, + * when it returns we know that the pending NEWLINKs have already been + * processed and that we are up-to-date */ + + return link_call_getlink(link, link_initialized_handler); +} + +static int link_check_initialized(Link *link) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + int r; + + assert(link); + + if (!udev_available()) + return link_initialized_and_synced(link); + + /* udev should be around */ + r = sd_device_new_from_ifindex(&device, link->ifindex); + if (r < 0) { + log_link_debug_errno(link, r, "Could not find device, waiting for device initialization: %m"); + return 0; + } + + r = sd_device_get_is_initialized(device); + if (r < 0) + return log_link_warning_errno(link, r, "Could not determine whether the device is initialized: %m"); + if (r == 0) { + /* not yet ready */ + log_link_debug(link, "link pending udev initialization..."); + return 0; + } + + r = device_is_renaming(device); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to determine the device is being renamed: %m"); + if (r > 0) { + log_link_debug(link, "Interface is being renamed, pending initialization."); + return 0; + } + + return link_initialized(link, device); +} + +int manager_udev_process_link(Manager *m, sd_device *device, sd_device_action_t action) { + int r, ifindex; + const char *s; + Link *link; + + assert(m); + assert(device); + + r = sd_device_get_ifindex(device, &ifindex); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to get ifindex: %m"); + + r = link_get_by_index(m, ifindex, &link); + if (r < 0) { + /* This error is not critical, as the corresponding rtnl message may be received later. */ + log_device_debug_errno(device, r, "Failed to get link from ifindex %i, ignoring: %m", ifindex); + return 0; + } + + /* Let's unref the sd-device object assigned to the corresponding Link object, but keep the Link + * object here. It will be removed only when rtnetlink says so. */ + if (action == SD_DEVICE_REMOVE) { + link->dev = sd_device_unref(link->dev); + return 0; + } + + r = device_is_renaming(device); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to determine if the device is renaming or not: %m"); + if (r > 0) { + log_device_debug(device, "Device is renaming, waiting for the interface to be renamed."); + /* TODO: + * What happens when a device is initialized, then soon renamed after that? When we detect + * such, maybe we should cancel or postpone all queued requests for the interface. */ + return 0; + } + + r = sd_device_get_property_value(device, "ID_NET_MANAGED_BY", &s); + if (r < 0 && r != -ENOENT) + log_device_debug_errno(device, r, "Failed to get ID_NET_MANAGED_BY udev property, ignoring: %m"); + if (r >= 0 && !streq(s, "io.systemd.Network")) { + log_device_debug(device, "Interface is requested to be managed by '%s', not managing the interface.", s); + link_set_state(link, LINK_STATE_UNMANAGED); + return 0; + } + + r = link_initialized(link, device); + if (r < 0) + link_enter_failed(link); + + return 0; +} + +static int link_carrier_gained(Link *link) { + bool force_reconfigure; + int r; + + assert(link); + + r = event_source_disable(link->carrier_lost_timer); + if (r < 0) + log_link_warning_errno(link, r, "Failed to disable carrier lost timer, ignoring: %m"); + + /* If a wireless interface was connected to an access point, and the SSID is changed (that is, + * both previous_ssid and ssid are non-NULL), then the connected wireless network could be + * changed. So, always reconfigure the link. Which means e.g. the DHCP client will be + * restarted, and the correct network information will be gained. + * + * However, do not reconfigure the wireless interface forcibly if it was not connected to any + * access points previously (previous_ssid is NULL in this case). As, a .network file may be + * already assigned to the interface (in that case, the .network file does not have the SSID= + * setting in the [Match] section), and the interface is already being configured. Of course, + * there may exist another .network file with higher priority and a matching SSID= setting. But + * in that case, link_reconfigure_impl() can handle that without the force_reconfigure flag. + * + * For non-wireless interfaces, we have no way to detect the connected network change. So, + * setting force_reconfigure = false. Note, both ssid and previous_ssid are NULL in that case. */ + force_reconfigure = link->previous_ssid && !streq_ptr(link->previous_ssid, link->ssid); + link->previous_ssid = mfree(link->previous_ssid); + + /* AP and P2P-GO interfaces may have a new SSID - update the link properties in case a new .network + * profile wants to match on it with SSID= in its [Match] section. + */ + if (IN_SET(link->wlan_iftype, NL80211_IFTYPE_AP, NL80211_IFTYPE_P2P_GO)) { + r = link_get_wlan_interface(link); + if (r < 0) + return r; + } + + /* At this stage, both wlan and link information should be up-to-date. Hence, it is not necessary to + * call RTM_GETLINK, NL80211_CMD_GET_INTERFACE, or NL80211_CMD_GET_STATION commands, and simply call + * link_reconfigure_impl(). Note, link_reconfigure_impl() returns 1 when the link is reconfigured. */ + r = link_reconfigure_impl(link, force_reconfigure); + if (r != 0) + return r; + + r = link_handle_bound_by_list(link); + if (r < 0) + return r; + + if (link->iftype == ARPHRD_CAN) + /* let's shortcut things for CAN which doesn't need most of what's done below. */ + return 0; + + if (IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) { + r = link_acquire_dynamic_conf(link); + if (r < 0) + return r; + + r = link_request_static_configs(link); + if (r < 0) + return r; + } + + return 0; +} + +static int link_carrier_lost_impl(Link *link) { + int r, ret = 0; + + assert(link); + + link->previous_ssid = mfree(link->previous_ssid); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + if (!link->network) + return 0; + + r = link_stop_engines(link, false); + if (r < 0) + ret = r; + + r = link_drop_managed_config(link); + if (r < 0 && ret >= 0) + ret = r; + + return ret; +} + +static int link_carrier_lost_handler(sd_event_source *s, uint64_t usec, void *userdata) { + Link *link = ASSERT_PTR(userdata); + int r; + + r = link_carrier_lost_impl(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to process carrier lost event: %m"); + link_enter_failed(link); + } + + return 0; +} + +static int link_carrier_lost(Link *link) { + uint16_t dhcp_mtu; + usec_t usec; + int r; + + assert(link); + + r = link_handle_bound_by_list(link); + if (r < 0) + return r; + + if (link->iftype == ARPHRD_CAN) + /* let's shortcut things for CAN which doesn't need most of what's done below. */ + return 0; + + if (!link->network) + return 0; + + if (link->network->ignore_carrier_loss_set) + /* If IgnoreCarrierLoss= is explicitly specified, then use the specified value. */ + usec = link->network->ignore_carrier_loss_usec; + + else if (link->network->bond && link->wlan_iftype > 0) + /* Enslaving wlan interface to a bond disconnects from the connected AP, and causes its + * carrier to be lost. See #19832. */ + usec = 3 * USEC_PER_SEC; + + else if (link->network->dhcp_use_mtu && + link->dhcp_lease && + sd_dhcp_lease_get_mtu(link->dhcp_lease, &dhcp_mtu) >= 0 && + dhcp_mtu != link->original_mtu) + /* Some drivers reset interfaces when changing MTU. Resetting interfaces by the static + * MTU should not cause any issues, as MTU is changed only once. However, setting MTU + * through DHCP lease causes an infinite loop of resetting the interface. See #18738. */ + usec = 5 * USEC_PER_SEC; + + else + /* Otherwise, use the implied default value. */ + usec = link->network->ignore_carrier_loss_usec; + + if (usec == USEC_INFINITY) + return 0; + + if (usec == 0) + return link_carrier_lost_impl(link); + + return event_reset_time_relative(link->manager->event, + &link->carrier_lost_timer, + CLOCK_BOOTTIME, + usec, + 0, + link_carrier_lost_handler, + link, + 0, + "link-carrier-loss", + true); +} + +static int link_admin_state_up(Link *link) { + int r; + + assert(link); + + /* This is called every time an interface admin state changes to up; + * specifically, when IFF_UP flag changes from unset to set. */ + + if (!link->network) + return 0; + + if (link->activated && link->network->activation_policy == ACTIVATION_POLICY_ALWAYS_DOWN) { + log_link_info(link, "Activation policy is \"always-down\", forcing link down."); + return link_request_to_bring_up_or_down(link, /* up = */ false); + } + + /* We set the ipv6 mtu after the device mtu, but the kernel resets + * ipv6 mtu on NETDEV_UP, so we need to reset it. */ + r = link_set_ipv6_mtu(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv6 MTU, ignoring: %m"); + + return 0; +} + +static int link_admin_state_down(Link *link) { + assert(link); + + if (!link->network) + return 0; + + if (link->activated && link->network->activation_policy == ACTIVATION_POLICY_ALWAYS_UP) { + log_link_info(link, "Activation policy is \"always-up\", forcing link up."); + return link_request_to_bring_up_or_down(link, /* up = */ true); + } + + return 0; +} + +static bool link_is_enslaved(Link *link) { + if (link->flags & IFF_SLAVE) + return true; + + if (link->master_ifindex > 0) + return true; + + return false; +} + +void link_update_operstate(Link *link, bool also_update_master) { + LinkOperationalState operstate; + LinkCarrierState carrier_state; + LinkAddressState ipv4_address_state, ipv6_address_state, address_state; + LinkOnlineState online_state; + _cleanup_strv_free_ char **p = NULL; + bool changed = false; + + 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; + } + } + + link_get_address_states(link, &ipv4_address_state, &ipv6_address_state, &address_state); + + /* Mapping of address and carrier state vs operational state + * carrier state + * | off | no-carrier | dormant | degraded-carrier | carrier | enslaved + * ------------------------------------------------------------------------------ + * off | off | no-carrier | dormant | degraded-carrier | carrier | enslaved + * address_state degraded | off | no-carrier | dormant | degraded | degraded | enslaved + * routable | off | no-carrier | dormant | routable | routable | routable + */ + + if (carrier_state == LINK_CARRIER_STATE_DEGRADED_CARRIER && address_state == LINK_ADDRESS_STATE_ROUTABLE) + operstate = LINK_OPERSTATE_ROUTABLE; + else if (carrier_state == LINK_CARRIER_STATE_DEGRADED_CARRIER && address_state == LINK_ADDRESS_STATE_DEGRADED) + operstate = LINK_OPERSTATE_DEGRADED; + else if (carrier_state < LINK_CARRIER_STATE_CARRIER || address_state == LINK_ADDRESS_STATE_OFF) + operstate = (LinkOperationalState) carrier_state; + else if (address_state == LINK_ADDRESS_STATE_ROUTABLE) + operstate = LINK_OPERSTATE_ROUTABLE; + else if (carrier_state == LINK_CARRIER_STATE_CARRIER) + operstate = LINK_OPERSTATE_DEGRADED; + else + operstate = LINK_OPERSTATE_ENSLAVED; + + /* Only determine online state for managed links with RequiredForOnline=yes */ + if (!link->network || !link->network->required_for_online) + online_state = _LINK_ONLINE_STATE_INVALID; + else if (operstate < link->network->required_operstate_for_online.min || + operstate > link->network->required_operstate_for_online.max) + online_state = LINK_ONLINE_STATE_OFFLINE; + else { + AddressFamily required_family = link->network->required_family_for_online; + bool needs_ipv4 = required_family & ADDRESS_FAMILY_IPV4; + bool needs_ipv6 = required_family & ADDRESS_FAMILY_IPV6; + + /* The operational state is within the range required for online. + * If a particular address family is also required, we might revert + * to offline in the blocks below. */ + online_state = LINK_ONLINE_STATE_ONLINE; + + if (link->network->required_operstate_for_online.min >= LINK_OPERSTATE_DEGRADED) { + if (needs_ipv4 && ipv4_address_state < LINK_ADDRESS_STATE_DEGRADED) + online_state = LINK_ONLINE_STATE_OFFLINE; + if (needs_ipv6 && ipv6_address_state < LINK_ADDRESS_STATE_DEGRADED) + online_state = LINK_ONLINE_STATE_OFFLINE; + } + + if (link->network->required_operstate_for_online.min >= LINK_OPERSTATE_ROUTABLE) { + if (needs_ipv4 && ipv4_address_state < LINK_ADDRESS_STATE_ROUTABLE) + online_state = LINK_ONLINE_STATE_OFFLINE; + if (needs_ipv6 && ipv6_address_state < LINK_ADDRESS_STATE_ROUTABLE) + online_state = LINK_ONLINE_STATE_OFFLINE; + } + } + + if (link->carrier_state != carrier_state) { + link->carrier_state = carrier_state; + changed = true; + if (strv_extend(&p, "CarrierState") < 0) + log_oom(); + } + + if (link->address_state != address_state) { + link->address_state = address_state; + changed = true; + if (strv_extend(&p, "AddressState") < 0) + log_oom(); + } + + if (link->ipv4_address_state != ipv4_address_state) { + link->ipv4_address_state = ipv4_address_state; + changed = true; + if (strv_extend(&p, "IPv4AddressState") < 0) + log_oom(); + } + + if (link->ipv6_address_state != ipv6_address_state) { + link->ipv6_address_state = ipv6_address_state; + changed = true; + if (strv_extend(&p, "IPv6AddressState") < 0) + log_oom(); + } + + if (link->operstate != operstate) { + link->operstate = operstate; + changed = true; + if (strv_extend(&p, "OperationalState") < 0) + log_oom(); + } + + if (link->online_state != online_state) { + link->online_state = online_state; + changed = true; + if (strv_extend(&p, "OnlineState") < 0) + log_oom(); + } + + if (p) + link_send_changed_strv(link, p); + if (changed) + link_dirty(link); + + if (also_update_master) { + Link *master; + + if (link_get_master(link, &master) >= 0) + link_update_operstate(master, true); + } +} + +#define FLAG_STRING(string, flag, old, new) \ + (((old ^ new) & flag) \ + ? ((old & flag) ? (" -" string) : (" +" string)) \ + : "") + +static int link_update_flags(Link *link, sd_netlink_message *message) { + bool link_was_admin_up, had_carrier; + uint8_t operstate; + unsigned flags; + int r; + + assert(link); + assert(message); + + r = sd_rtnl_message_link_get_flags(message, &flags); + if (r < 0) + return log_link_debug_errno(link, r, "rtnl: failed to read link flags: %m"); + + r = sd_netlink_message_read_u8(message, IFLA_OPERSTATE, &operstate); + if (r == -ENODATA) + /* If we got a message without operstate, assume the state was unchanged. */ + operstate = link->kernel_operstate; + else if (r < 0) + return log_link_debug_errno(link, r, "rtnl: failed to read operational state: %m"); + + if (link->flags == flags && link->kernel_operstate == operstate) + return 0; + + if (link->flags != flags) { + unsigned unknown_flags, unknown_flags_added, unknown_flags_removed; + + log_link_debug(link, "Flags change:%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", + FLAG_STRING("LOOPBACK", IFF_LOOPBACK, link->flags, flags), + FLAG_STRING("MASTER", IFF_MASTER, link->flags, flags), + FLAG_STRING("SLAVE", IFF_SLAVE, link->flags, flags), + FLAG_STRING("UP", IFF_UP, link->flags, flags), + FLAG_STRING("DORMANT", IFF_DORMANT, link->flags, flags), + FLAG_STRING("LOWER_UP", IFF_LOWER_UP, link->flags, flags), + FLAG_STRING("RUNNING", IFF_RUNNING, link->flags, flags), + FLAG_STRING("MULTICAST", IFF_MULTICAST, link->flags, flags), + FLAG_STRING("BROADCAST", IFF_BROADCAST, link->flags, flags), + FLAG_STRING("POINTOPOINT", IFF_POINTOPOINT, link->flags, flags), + FLAG_STRING("PROMISC", IFF_PROMISC, link->flags, flags), + FLAG_STRING("ALLMULTI", IFF_ALLMULTI, link->flags, flags), + FLAG_STRING("PORTSEL", IFF_PORTSEL, link->flags, flags), + FLAG_STRING("AUTOMEDIA", IFF_AUTOMEDIA, link->flags, flags), + FLAG_STRING("DYNAMIC", IFF_DYNAMIC, link->flags, flags), + FLAG_STRING("NOARP", IFF_NOARP, link->flags, flags), + FLAG_STRING("NOTRAILERS", IFF_NOTRAILERS, link->flags, flags), + FLAG_STRING("DEBUG", IFF_DEBUG, link->flags, flags), + FLAG_STRING("ECHO", IFF_ECHO, link->flags, flags)); + + unknown_flags = ~(IFF_LOOPBACK | IFF_MASTER | IFF_SLAVE | IFF_UP | + IFF_DORMANT | IFF_LOWER_UP | IFF_RUNNING | + IFF_MULTICAST | IFF_BROADCAST | IFF_POINTOPOINT | + IFF_PROMISC | IFF_ALLMULTI | IFF_PORTSEL | + IFF_AUTOMEDIA | IFF_DYNAMIC | IFF_NOARP | + IFF_NOTRAILERS | IFF_DEBUG | IFF_ECHO); + unknown_flags_added = ((link->flags ^ flags) & flags & unknown_flags); + unknown_flags_removed = ((link->flags ^ flags) & link->flags & unknown_flags); + + if (unknown_flags_added) + log_link_debug(link, "Unknown link flags gained, ignoring: %#.5x", unknown_flags_added); + + if (unknown_flags_removed) + log_link_debug(link, "Unknown link flags lost, ignoring: %#.5x", unknown_flags_removed); + } + + link_was_admin_up = link->flags & IFF_UP; + had_carrier = link_has_carrier(link); + + link->flags = flags; + link->kernel_operstate = operstate; + + link_update_operstate(link, true); + + if (!link_was_admin_up && (link->flags & IFF_UP)) { + log_link_info(link, "Link UP"); + + r = link_admin_state_up(link); + if (r < 0) + return r; + } else if (link_was_admin_up && !(link->flags & IFF_UP)) { + log_link_info(link, "Link DOWN"); + + r = link_admin_state_down(link); + if (r < 0) + return r; + } + + if (!had_carrier && link_has_carrier(link)) { + log_link_info(link, "Gained carrier"); + + r = link_carrier_gained(link); + if (r < 0) + return r; + } else if (had_carrier && !link_has_carrier(link)) { + log_link_info(link, "Lost carrier"); + + r = link_carrier_lost(link); + if (r < 0) + return r; + } + + return 0; +} + +static int link_update_master(Link *link, sd_netlink_message *message) { + int master_ifindex, r; + + assert(link); + assert(message); + + r = sd_netlink_message_read_u32(message, IFLA_MASTER, (uint32_t*) &master_ifindex); + if (r == -ENODATA) + return 0; + if (r < 0) + return log_link_debug_errno(link, r, "rtnl: failed to read master ifindex: %m"); + + if (master_ifindex == link->ifindex) + master_ifindex = 0; + + if (master_ifindex != link->master_ifindex) { + if (link->master_ifindex == 0) + log_link_debug(link, "Attached to master interface: %i", master_ifindex); + else if (master_ifindex == 0) + log_link_debug(link, "Detached from master interface: %i", link->master_ifindex); + else + log_link_debug(link, "Master interface changed: %i %s %i", link->master_ifindex, + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), master_ifindex); + + link_drop_from_master(link); + link->master_ifindex = master_ifindex; + } + + r = link_append_to_master(link); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to append link to master: %m"); + + return 0; +} + +static int link_update_driver(Link *link, sd_netlink_message *message) { + int r; + + assert(link); + assert(link->manager); + assert(message); + + /* Driver is already read. Assuming the driver is never changed. */ + if (link->ethtool_driver_read) + return 0; + + /* When udevd is running, read the driver after the interface is initialized by udevd. + * Otherwise, ethtool may not work correctly. See issue #22538. + * When udevd is not running, read the value when the interface is detected. */ + if (udev_available() && !link->dev) + return 0; + + link->ethtool_driver_read = true; + + r = ethtool_get_driver(&link->manager->ethtool_fd, link->ifname, &link->driver); + if (r < 0) { + log_link_debug_errno(link, r, "Failed to get driver, continuing without: %m"); + return 0; + } + + log_link_debug(link, "Found driver: %s", strna(link->driver)); + + if (streq_ptr(link->driver, "dsa")) { + uint32_t dsa_master_ifindex = 0; + + r = sd_netlink_message_read_u32(message, IFLA_LINK, &dsa_master_ifindex); + if (r < 0 && r != -ENODATA) + return log_link_debug_errno(link, r, "rtnl: failed to read ifindex of the DSA master interface: %m"); + + if (dsa_master_ifindex > INT_MAX) { + log_link_debug(link, "rtnl: received too large DSA master ifindex (%"PRIu32" > INT_MAX), ignoring.", + dsa_master_ifindex); + dsa_master_ifindex = 0; + } + + link->dsa_master_ifindex = (int) dsa_master_ifindex; + } + + return 1; /* needs reconfigure */ +} + +static int link_update_permanent_hardware_address_from_ethtool(Link *link, sd_netlink_message *message) { + int r; + + assert(link); + assert(link->manager); + assert(message); + + if (link->ethtool_permanent_hw_addr_read) + return 0; + + /* When udevd is running, read the permanent hardware address after the interface is + * initialized by udevd. Otherwise, ethtool may not work correctly. See issue #22538. + * When udevd is not running, read the value when the interface is detected. */ + if (udev_available() && !link->dev) + return 0; + + /* If the interface does not have a hardware address, then it will not have a permanent address either. */ + r = netlink_message_read_hw_addr(message, IFLA_ADDRESS, NULL); + if (r == -ENODATA) + return 0; + if (r < 0) + return log_link_debug_errno(link, r, "Failed to read IFLA_ADDRESS attribute: %m"); + + link->ethtool_permanent_hw_addr_read = true; + + r = ethtool_get_permanent_hw_addr(&link->manager->ethtool_fd, link->ifname, &link->permanent_hw_addr); + if (r < 0) + log_link_debug_errno(link, r, "Permanent hardware address not found, continuing without: %m"); + + return 0; +} + +static int link_update_permanent_hardware_address(Link *link, sd_netlink_message *message) { + int r; + + assert(link); + assert(link->manager); + assert(message); + + if (link->permanent_hw_addr.length > 0) + return 0; + + r = netlink_message_read_hw_addr(message, IFLA_PERM_ADDRESS, &link->permanent_hw_addr); + if (r < 0) { + if (r != -ENODATA) + return log_link_debug_errno(link, r, "Failed to read IFLA_PERM_ADDRESS attribute: %m"); + + /* Fallback to ethtool for older kernels. */ + r = link_update_permanent_hardware_address_from_ethtool(link, message); + if (r < 0) + return r; + } + + if (link->permanent_hw_addr.length > 0) + log_link_debug(link, "Saved permanent hardware address: %s", HW_ADDR_TO_STR(&link->permanent_hw_addr)); + + return 1; /* needs reconfigure */ +} + +static int link_update_hardware_address(Link *link, sd_netlink_message *message) { + struct hw_addr_data addr; + int r; + + assert(link); + assert(message); + + r = netlink_message_read_hw_addr(message, IFLA_BROADCAST, &link->bcast_addr); + if (r < 0 && r != -ENODATA) + return log_link_debug_errno(link, r, "rtnl: failed to read broadcast address: %m"); + + r = netlink_message_read_hw_addr(message, IFLA_ADDRESS, &addr); + if (r == -ENODATA) + return 0; + if (r < 0) + return log_link_debug_errno(link, r, "rtnl: failed to read hardware address: %m"); + + if (hw_addr_equal(&link->hw_addr, &addr)) + return 0; + + if (link->hw_addr.length == 0) + log_link_debug(link, "Saved hardware address: %s", HW_ADDR_TO_STR(&addr)); + else { + log_link_debug(link, "Hardware address is changed: %s %s %s", + HW_ADDR_TO_STR(&link->hw_addr), + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), + HW_ADDR_TO_STR(&addr)); + + hashmap_remove_value(link->manager->links_by_hw_addr, &link->hw_addr, link); + } + + link->hw_addr = addr; + + if (!hw_addr_is_null(&link->hw_addr)) { + r = hashmap_ensure_put(&link->manager->links_by_hw_addr, &hw_addr_hash_ops, &link->hw_addr, link); + if (r == -EEXIST && streq_ptr(link->kind, "bond")) + /* bonding master and its slaves have the same hardware address. */ + r = hashmap_replace(link->manager->links_by_hw_addr, &link->hw_addr, link); + if (r < 0) + log_link_debug_errno(link, r, "Failed to manage link by its new hardware address, ignoring: %m"); + } + + r = ipv4acd_update_mac(link); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MAC address in IPv4 ACD client: %m"); + + r = ipv4ll_update_mac(link); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MAC address in IPv4LL client: %m"); + + r = dhcp4_update_mac(link); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MAC address in DHCP client: %m"); + + r = dhcp6_update_mac(link); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MAC address in DHCPv6 client: %m"); + + r = radv_update_mac(link); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MAC address for Router Advertisement: %m"); + + if (link->ndisc && link->hw_addr.length == ETH_ALEN) { + r = sd_ndisc_set_mac(link->ndisc, &link->hw_addr.ether); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MAC for NDisc: %m"); + } + + if (link->lldp_rx) { + r = sd_lldp_rx_set_filter_address(link->lldp_rx, &link->hw_addr.ether); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MAC address for LLDP Rx: %m"); + } + + if (link->lldp_tx) { + r = sd_lldp_tx_set_hwaddr(link->lldp_tx, &link->hw_addr.ether); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MAC address for LLDP Tx: %m"); + } + + return 1; /* needs reconfigure */ +} + +static int link_update_mtu(Link *link, sd_netlink_message *message) { + uint32_t mtu, min_mtu = 0, max_mtu = UINT32_MAX; + int r; + + assert(link); + assert(message); + + r = sd_netlink_message_read_u32(message, IFLA_MTU, &mtu); + if (r == -ENODATA) + return 0; + if (r < 0) + return log_link_debug_errno(link, r, "rtnl: failed to read MTU in RTM_NEWLINK message: %m"); + if (mtu == 0) + return 0; + + r = sd_netlink_message_read_u32(message, IFLA_MIN_MTU, &min_mtu); + if (r < 0 && r != -ENODATA) + return log_link_debug_errno(link, r, "rtnl: failed to read minimum MTU in RTM_NEWLINK message: %m"); + + r = sd_netlink_message_read_u32(message, IFLA_MAX_MTU, &max_mtu); + if (r < 0 && r != -ENODATA) + return log_link_debug_errno(link, r, "rtnl: failed to read maximum MTU in RTM_NEWLINK message: %m"); + + if (max_mtu == 0) + max_mtu = UINT32_MAX; + + link->min_mtu = min_mtu; + link->max_mtu = max_mtu; + + if (link->original_mtu == 0) { + link->original_mtu = mtu; + log_link_debug(link, "Saved original MTU %" PRIu32" (min: %"PRIu32", max: %"PRIu32")", + link->original_mtu, link->min_mtu, link->max_mtu); + } + + if (link->mtu == mtu) + return 0; + + if (link->mtu != 0) + log_link_debug(link, "MTU is changed: %"PRIu32" %s %"PRIu32" (min: %"PRIu32", max: %"PRIu32")", + link->mtu, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), mtu, + link->min_mtu, link->max_mtu); + + link->mtu = mtu; + + if (link->dhcp_client) { + r = sd_dhcp_client_set_mtu(link->dhcp_client, link->mtu); + if (r < 0) + return log_link_debug_errno(link, r, "Could not update MTU in DHCP client: %m"); + } + + if (link->radv) { + r = sd_radv_set_mtu(link->radv, link->mtu); + if (r < 0) + return log_link_debug_errno(link, r, "Could not set MTU for Router Advertisement: %m"); + } + + return 0; +} + +static int link_update_alternative_names(Link *link, sd_netlink_message *message) { + _cleanup_strv_free_ char **altnames = NULL; + int r; + + assert(link); + assert(message); + + r = sd_netlink_message_read_strv(message, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &altnames); + if (r == -ENODATA) + /* The message does not have IFLA_PROP_LIST container attribute. It does not mean the + * interface has no alternative name. */ + return 0; + if (r < 0) + return log_link_debug_errno(link, r, "rtnl: failed to read alternative names: %m"); + + if (strv_equal(altnames, link->alternative_names)) + return 0; + + STRV_FOREACH(n, link->alternative_names) + hashmap_remove(link->manager->links_by_name, *n); + + strv_free_and_replace(link->alternative_names, altnames); + + STRV_FOREACH(n, link->alternative_names) { + r = hashmap_ensure_put(&link->manager->links_by_name, &string_hash_ops, *n, link); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to manage link by its new alternative names: %m"); + } + + return 1; /* needs reconfigure */ +} + +static int link_update_name(Link *link, sd_netlink_message *message) { + char ifname_from_index[IF_NAMESIZE]; + const char *ifname; + int r; + + assert(link); + assert(message); + + r = sd_netlink_message_read_string(message, IFLA_IFNAME, &ifname); + if (r == -ENODATA) + /* Hmm?? But ok. */ + return 0; + if (r < 0) + return log_link_debug_errno(link, r, "Failed to read interface name in RTM_NEWLINK message: %m"); + + if (streq(ifname, link->ifname)) + return 0; + + r = format_ifname(link->ifindex, ifname_from_index); + if (r < 0) + return log_link_debug_errno(link, r, "Could not get interface name for index %i.", link->ifindex); + + if (!streq(ifname, ifname_from_index)) { + log_link_debug(link, "New interface name '%s' received from the kernel does not correspond " + "with the name currently configured on the actual interface '%s'. Ignoring.", + ifname, ifname_from_index); + return 0; + } + + log_link_info(link, "Interface name change detected, renamed to %s.", ifname); + + hashmap_remove(link->manager->links_by_name, link->ifname); + + r = free_and_strdup(&link->ifname, ifname); + if (r < 0) + return log_oom_debug(); + + r = hashmap_ensure_put(&link->manager->links_by_name, &string_hash_ops, link->ifname, link); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to manage link by its new name: %m"); + + if (link->dhcp_client) { + r = sd_dhcp_client_set_ifname(link->dhcp_client, link->ifname); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in DHCP client: %m"); + } + + if (link->dhcp6_client) { + r = sd_dhcp6_client_set_ifname(link->dhcp6_client, link->ifname); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in DHCP6 client: %m"); + } + + if (link->ndisc) { + r = sd_ndisc_set_ifname(link->ndisc, link->ifname); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in NDisc: %m"); + } + + if (link->dhcp_server) { + r = sd_dhcp_server_set_ifname(link->dhcp_server, link->ifname); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in DHCP server: %m"); + } + + if (link->radv) { + r = sd_radv_set_ifname(link->radv, link->ifname); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in Router Advertisement: %m"); + } + + if (link->lldp_rx) { + r = sd_lldp_rx_set_ifname(link->lldp_rx, link->ifname); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in LLDP Rx: %m"); + } + + if (link->lldp_tx) { + r = sd_lldp_tx_set_ifname(link->lldp_tx, link->ifname); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in LLDP Tx: %m"); + } + + if (link->ipv4ll) { + r = sd_ipv4ll_set_ifname(link->ipv4ll, link->ifname); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in IPv4LL client: %m"); + } + + r = ipv4acd_set_ifname(link); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to update interface name in IPv4ACD client: %m"); + + return 1; /* needs reconfigure */ +} + +static int link_update(Link *link, sd_netlink_message *message) { + bool needs_reconfigure = false; + int r; + + assert(link); + assert(message); + + r = link_update_name(link, message); + if (r < 0) + return r; + needs_reconfigure = needs_reconfigure || r > 0; + + r = link_update_alternative_names(link, message); + if (r < 0) + return r; + needs_reconfigure = needs_reconfigure || r > 0; + + r = link_update_mtu(link, message); + if (r < 0) + return r; + + r = link_update_driver(link, message); + if (r < 0) + return r; + needs_reconfigure = needs_reconfigure || r > 0; + + r = link_update_permanent_hardware_address(link, message); + if (r < 0) + return r; + needs_reconfigure = needs_reconfigure || r > 0; + + r = link_update_hardware_address(link, message); + if (r < 0) + return r; + needs_reconfigure = needs_reconfigure || r > 0; + + r = link_update_master(link, message); + if (r < 0) + return r; + + r = link_update_ipv6ll_addrgen_mode(link, message); + if (r < 0) + return r; + + r = link_update_flags(link, message); + if (r < 0) + return r; + + return needs_reconfigure; +} + +static Link *link_drop_or_unref(Link *link) { + if (!link) + return NULL; + if (!link->manager) + return link_unref(link); + return link_drop(link); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_drop_or_unref); + +static int link_new(Manager *manager, sd_netlink_message *message, Link **ret) { + _cleanup_free_ char *ifname = NULL, *kind = NULL, *state_file = NULL, *lease_file = NULL, *lldp_file = NULL; + _cleanup_(link_drop_or_unrefp) Link *link = NULL; + unsigned short iftype; + int r, ifindex; + + assert(manager); + assert(message); + assert(ret); + + r = sd_rtnl_message_link_get_ifindex(message, &ifindex); + if (r < 0) + return log_debug_errno(r, "rtnl: failed to read ifindex from link message: %m"); + else if (ifindex <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "rtnl: received link message without valid ifindex."); + + r = sd_rtnl_message_link_get_type(message, &iftype); + if (r < 0) + return log_debug_errno(r, "rtnl: failed to read interface type from link message: %m"); + + r = sd_netlink_message_read_string_strdup(message, IFLA_IFNAME, &ifname); + if (r < 0) + return log_debug_errno(r, "rtnl: failed to read interface name from link message: %m"); + + /* check for link kind */ + r = sd_netlink_message_enter_container(message, IFLA_LINKINFO); + if (r >= 0) { + r = sd_netlink_message_read_string_strdup(message, IFLA_INFO_KIND, &kind); + if (r < 0 && r != -ENODATA) + return log_debug_errno(r, "rtnl: failed to read interface kind from link message: %m"); + r = sd_netlink_message_exit_container(message); + if (r < 0) + return log_debug_errno(r, "rtnl: failed to exit IFLA_LINKINFO container: %m"); + } + + if (!manager->test_mode) { + /* Do not update state files when running in test mode. */ + if (asprintf(&state_file, "/run/systemd/netif/links/%d", ifindex) < 0) + return log_oom_debug(); + + if (asprintf(&lease_file, "/run/systemd/netif/leases/%d", ifindex) < 0) + return log_oom_debug(); + + if (asprintf(&lldp_file, "/run/systemd/netif/lldp/%d", ifindex) < 0) + return log_oom_debug(); + } + + link = new(Link, 1); + if (!link) + return -ENOMEM; + + *link = (Link) { + .n_ref = 1, + .state = LINK_STATE_PENDING, + .online_state = _LINK_ONLINE_STATE_INVALID, + .ifindex = ifindex, + .iftype = iftype, + .ifname = TAKE_PTR(ifname), + .kind = TAKE_PTR(kind), + + .ipv6ll_address_gen_mode = _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_INVALID, + + .state_file = TAKE_PTR(state_file), + .lease_file = TAKE_PTR(lease_file), + .lldp_file = TAKE_PTR(lldp_file), + + .n_dns = UINT_MAX, + .dns_default_route = -1, + .llmnr = _RESOLVE_SUPPORT_INVALID, + .mdns = _RESOLVE_SUPPORT_INVALID, + .dnssec_mode = _DNSSEC_MODE_INVALID, + .dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID, + }; + + r = hashmap_ensure_put(&manager->links_by_index, NULL, INT_TO_PTR(link->ifindex), link); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to store link into manager: %m"); + + link->manager = manager; + + r = hashmap_ensure_put(&manager->links_by_name, &string_hash_ops, link->ifname, link); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to manage link by its interface name: %m"); + + log_link_debug(link, "Saved new link: ifindex=%i, iftype=%s(%u), kind=%s", + link->ifindex, strna(arphrd_to_name(link->iftype)), link->iftype, strna(link->kind)); + + /* If contained in this set, the link is wireless and the corresponding NL80211_CMD_NEW_INTERFACE + * message arrived too early. Request the wireless link information again. + */ + if (set_remove(manager->new_wlan_ifindices, INT_TO_PTR(link->ifindex))) { + r = link_get_wlan_interface(link); + if (r < 0) + log_link_warning_errno(link, r, "Failed to get wireless interface, ignoring: %m"); + } + + *ret = TAKE_PTR(link); + return 0; +} + +int manager_rtnl_process_link(sd_netlink *rtnl, sd_netlink_message *message, Manager *manager) { + Link *link = NULL; + NetDev *netdev = NULL; + uint16_t type; + const char *name; + int r, ifindex; + + assert(rtnl); + assert(message); + assert(manager); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "rtnl: Could not receive link message, ignoring"); + + return 0; + } + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: Could not get message type, ignoring: %m"); + return 0; + } else if (!IN_SET(type, RTM_NEWLINK, RTM_DELLINK)) { + log_warning("rtnl: Received unexpected message type %u when processing link, ignoring.", type); + return 0; + } + + r = sd_rtnl_message_link_get_ifindex(message, &ifindex); + if (r < 0) { + log_warning_errno(r, "rtnl: Could not get ifindex from link message, ignoring: %m"); + return 0; + } else if (ifindex <= 0) { + log_warning("rtnl: received link message with invalid ifindex %d, ignoring.", ifindex); + return 0; + } + + r = sd_netlink_message_read_string(message, IFLA_IFNAME, &name); + if (r < 0) { + log_warning_errno(r, "rtnl: Received link message without ifname, ignoring: %m"); + return 0; + } + + (void) link_get_by_index(manager, ifindex, &link); + (void) netdev_get(manager, name, &netdev); + + switch (type) { + case RTM_NEWLINK: + if (netdev) { + /* netdev exists, so make sure the ifindex matches */ + r = netdev_set_ifindex(netdev, message); + if (r < 0) { + log_netdev_warning_errno(netdev, r, "Could not process new link message for netdev, ignoring: %m"); + return 0; + } + } + + if (!link) { + /* link is new, so add it */ + r = link_new(manager, message, &link); + if (r < 0) { + log_warning_errno(r, "Could not process new link message: %m"); + return 0; + } + + r = link_update(link, message); + if (r < 0) { + log_link_warning_errno(link, r, "Could not process link message: %m"); + link_enter_failed(link); + return 0; + } + + r = link_check_initialized(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to check link is initialized: %m"); + link_enter_failed(link); + return 0; + } + } else { + r = link_update(link, message); + if (r < 0) { + log_link_warning_errno(link, r, "Could not process link message: %m"); + link_enter_failed(link); + return 0; + } + if (r > 0) { + r = link_reconfigure_impl(link, /* force = */ false); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to reconfigure interface: %m"); + link_enter_failed(link); + return 0; + } + } + } + break; + + case RTM_DELLINK: + link_drop(link); + netdev_drop(netdev); + break; + + default: + assert_not_reached(); + } + + return 1; +} + +int link_getlink_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg) { + uint16_t message_type; + int r; + + assert(m); + assert(link); + assert(error_msg); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + log_link_message_warning_errno(link, m, r, error_msg); + link_enter_failed(link); + return 0; + } + + r = sd_netlink_message_get_type(m, &message_type); + if (r < 0) { + log_link_debug_errno(link, r, "rtnl: failed to read link message type, ignoring: %m"); + return 0; + } + if (message_type != RTM_NEWLINK) { + log_link_debug(link, "rtnl: received invalid link message type, ignoring."); + return 0; + } + + r = link_update(link, m); + if (r < 0) { + link_enter_failed(link); + return 0; + } + + return 1; +} + +int link_call_getlink(Link *link, link_netlink_message_handler_t callback) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + assert(callback); + + r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_GETLINK, link->ifindex); + if (r < 0) + return r; + + r = netlink_call_async(link->manager->rtnl, NULL, req, callback, + link_netlink_destroy_callback, link); + if (r < 0) + return r; + + link_ref(link); + return 0; +} + +static const char* const link_state_table[_LINK_STATE_MAX] = { + [LINK_STATE_PENDING] = "pending", + [LINK_STATE_INITIALIZED] = "initialized", + [LINK_STATE_CONFIGURING] = "configuring", + [LINK_STATE_CONFIGURED] = "configured", + [LINK_STATE_UNMANAGED] = "unmanaged", + [LINK_STATE_FAILED] = "failed", + [LINK_STATE_LINGER] = "linger", +}; + +DEFINE_STRING_TABLE_LOOKUP(link_state, LinkState); + +int link_flags_to_string_alloc(uint32_t flags, char **ret) { + _cleanup_free_ char *str = NULL; + static const char* map[] = { + [LOG2U(IFF_UP)] = "up", /* interface is up. */ + [LOG2U(IFF_BROADCAST)] = "broadcast", /* broadcast address valid. */ + [LOG2U(IFF_DEBUG)] = "debug", /* turn on debugging. */ + [LOG2U(IFF_LOOPBACK)] = "loopback", /* interface is a loopback net. */ + [LOG2U(IFF_POINTOPOINT)] = "point-to-point", /* interface has p-p link. */ + [LOG2U(IFF_NOTRAILERS)] = "no-trailers", /* avoid use of trailers. */ + [LOG2U(IFF_RUNNING)] = "running", /* interface RFC2863 OPER_UP. */ + [LOG2U(IFF_NOARP)] = "no-arp", /* no ARP protocol. */ + [LOG2U(IFF_PROMISC)] = "promiscuous", /* receive all packets. */ + [LOG2U(IFF_ALLMULTI)] = "all-multicast", /* receive all multicast packets. */ + [LOG2U(IFF_MASTER)] = "master", /* master of a load balancer. */ + [LOG2U(IFF_SLAVE)] = "slave", /* slave of a load balancer. */ + [LOG2U(IFF_MULTICAST)] = "multicast", /* supports multicast. */ + [LOG2U(IFF_PORTSEL)] = "portsel", /* can set media type. */ + [LOG2U(IFF_AUTOMEDIA)] = "auto-media", /* auto media select active. */ + [LOG2U(IFF_DYNAMIC)] = "dynamic", /* dialup device with changing addresses. */ + [LOG2U(IFF_LOWER_UP)] = "lower-up", /* driver signals L1 up. */ + [LOG2U(IFF_DORMANT)] = "dormant", /* driver signals dormant. */ + [LOG2U(IFF_ECHO)] = "echo", /* echo sent packets. */ + }; + + assert(ret); + + for (size_t i = 0; i < ELEMENTSOF(map); i++) + if (FLAGS_SET(flags, 1 << i) && map[i]) + if (!strextend_with_separator(&str, ",", map[i])) + return -ENOMEM; + + *ret = TAKE_PTR(str); + return 0; +} + +static const char * const kernel_operstate_table[] = { + [IF_OPER_UNKNOWN] = "unknown", + [IF_OPER_NOTPRESENT] = "not-present", + [IF_OPER_DOWN] = "down", + [IF_OPER_LOWERLAYERDOWN] = "lower-layer-down", + [IF_OPER_TESTING] = "testing", + [IF_OPER_DORMANT] = "dormant", + [IF_OPER_UP] = "up", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(kernel_operstate, int); diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h new file mode 100644 index 0000000..938bbf4 --- /dev/null +++ b/src/network/networkd-link.h @@ -0,0 +1,253 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <endian.h> +#include <linux/nl80211.h> + +#include "sd-bus.h" +#include "sd-device.h" +#include "sd-dhcp-client.h" +#include "sd-dhcp-server.h" +#include "sd-dhcp6-client.h" +#include "sd-ipv4acd.h" +#include "sd-ipv4ll.h" +#include "sd-lldp-rx.h" +#include "sd-lldp-tx.h" +#include "sd-ndisc.h" +#include "sd-radv.h" +#include "sd-netlink.h" + +#include "ether-addr-util.h" +#include "log-link.h" +#include "netif-util.h" +#include "network-util.h" +#include "networkd-ipv6ll.h" +#include "networkd-util.h" +#include "ordered-set.h" +#include "resolve-util.h" +#include "set.h" + +typedef enum LinkState { + LINK_STATE_PENDING, /* udev has not initialized the link */ + LINK_STATE_INITIALIZED, /* udev has initialized the link */ + LINK_STATE_CONFIGURING, /* configuring addresses, routes, etc. */ + LINK_STATE_CONFIGURED, /* everything is configured */ + LINK_STATE_UNMANAGED, /* Unmanaged=yes is set */ + LINK_STATE_FAILED, /* at least one configuration process failed */ + LINK_STATE_LINGER, /* RTM_DELLINK for the link has been received */ + _LINK_STATE_MAX, + _LINK_STATE_INVALID = -EINVAL, +} LinkState; + +typedef struct Manager Manager; +typedef struct Network Network; +typedef struct NetDev NetDev; +typedef struct DUID DUID; + +typedef struct Link { + Manager *manager; + + unsigned n_ref; + + int ifindex; + int master_ifindex; + int dsa_master_ifindex; + int sr_iov_phys_port_ifindex; + Set *sr_iov_virt_port_ifindices; + + char *ifname; + char **alternative_names; + char *kind; + unsigned short iftype; + char *state_file; + struct hw_addr_data hw_addr; + struct hw_addr_data bcast_addr; + struct hw_addr_data permanent_hw_addr; + struct hw_addr_data requested_hw_addr; + struct in6_addr ipv6ll_address; + uint32_t mtu; + uint32_t min_mtu; + uint32_t max_mtu; + uint32_t original_mtu; + sd_device *dev; + char *driver; + + /* to prevent multiple ethtool calls */ + bool ethtool_driver_read; + bool ethtool_permanent_hw_addr_read; + + /* link-local addressing */ + IPv6LinkLocalAddressGenMode ipv6ll_address_gen_mode; + + /* wlan */ + enum nl80211_iftype wlan_iftype; + char *ssid; + char *previous_ssid; + struct ether_addr bssid; + + unsigned flags; + uint8_t kernel_operstate; + + sd_event_source *carrier_lost_timer; + + Network *network; + NetDev *netdev; + + LinkState state; + LinkOperationalState operstate; + LinkCarrierState carrier_state; + LinkAddressState address_state; + LinkAddressState ipv4_address_state; + LinkAddressState ipv6_address_state; + LinkOnlineState online_state; + + unsigned static_address_messages; + unsigned static_address_label_messages; + unsigned static_bridge_fdb_messages; + unsigned static_bridge_mdb_messages; + unsigned static_ipv6_proxy_ndp_messages; + unsigned static_neighbor_messages; + unsigned static_nexthop_messages; + unsigned static_route_messages; + unsigned static_routing_policy_rule_messages; + unsigned tc_messages; + unsigned sr_iov_messages; + unsigned set_link_messages; + unsigned set_flags_messages; + unsigned create_stacked_netdev_messages; + + Set *addresses; + Set *neighbors; + Set *routes; + Set *nexthops; + Set *qdiscs; + Set *tclasses; + + sd_dhcp_client *dhcp_client; + sd_dhcp_lease *dhcp_lease; + char *lease_file; + unsigned dhcp4_messages; + bool dhcp4_configured; + char *dhcp4_6rd_tunnel_name; + + Hashmap *ipv4acd_by_address; + + sd_ipv4ll *ipv4ll; + bool ipv4ll_address_configured:1; + + bool static_addresses_configured:1; + bool static_address_labels_configured:1; + bool static_bridge_fdb_configured:1; + bool static_bridge_mdb_configured:1; + bool static_ipv6_proxy_ndp_configured:1; + bool static_neighbors_configured:1; + bool static_nexthops_configured:1; + bool static_routes_configured:1; + bool static_routing_policy_rules_configured:1; + bool tc_configured:1; + bool sr_iov_configured:1; + bool activated:1; + bool master_set:1; + bool stacked_netdevs_created:1; + + sd_dhcp_server *dhcp_server; + + sd_ndisc *ndisc; + sd_event_source *ndisc_expire; + Set *ndisc_rdnss; + Set *ndisc_dnssl; + Set *ndisc_captive_portals; + Set *ndisc_pref64; + unsigned ndisc_messages; + bool ndisc_configured:1; + + sd_radv *radv; + + sd_dhcp6_client *dhcp6_client; + sd_dhcp6_lease *dhcp6_lease; + unsigned dhcp6_messages; + bool dhcp6_configured; + + Set *dhcp_pd_prefixes; + unsigned dhcp_pd_messages; + bool dhcp_pd_configured; + + /* This is about LLDP reception */ + sd_lldp_rx *lldp_rx; + char *lldp_file; + + /* This is about LLDP transmission */ + sd_lldp_tx *lldp_tx; + + Hashmap *bound_by_links; + Hashmap *bound_to_links; + Set *slaves; + + /* For speed meter */ + struct rtnl_link_stats64 stats_old, stats_new; + bool stats_updated; + + /* All kinds of DNS configuration the user configured via D-Bus */ + struct in_addr_full **dns; + unsigned n_dns; + OrderedSet *search_domains, *route_domains; + + int dns_default_route; + ResolveSupport llmnr; + ResolveSupport mdns; + DnssecMode dnssec_mode; + DnsOverTlsMode dns_over_tls_mode; + Set *dnssec_negative_trust_anchors; + + /* Similar, but NTP server configuration */ + char **ntp; +} Link; + +typedef int (*link_netlink_message_handler_t)(sd_netlink*, sd_netlink_message*, Link*); + +bool link_is_ready_to_configure(Link *link, bool allow_unmanaged); + +void link_ntp_settings_clear(Link *link); +void link_dns_settings_clear(Link *link); +Link *link_unref(Link *link); +Link *link_ref(Link *link); +DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_unref); +DEFINE_TRIVIAL_DESTRUCTOR(link_netlink_destroy_callback, Link, link_unref); + +int link_get_by_index(Manager *m, int ifindex, Link **ret); +int link_get_by_name(Manager *m, const char *ifname, Link **ret); +int link_get_by_hw_addr(Manager *m, const struct hw_addr_data *hw_addr, Link **ret); +int link_get_master(Link *link, Link **ret); + +int link_getlink_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg); +int link_call_getlink(Link *link, link_netlink_message_handler_t callback); +int link_handle_bound_to_list(Link *link); + +void link_enter_failed(Link *link); +void link_set_state(Link *link, LinkState state); +void link_check_ready(Link *link); + +void link_update_operstate(Link *link, bool also_update_bond_master); + +static inline bool link_has_carrier(Link *link) { + assert(link); + return netif_has_carrier(link->kernel_operstate, link->flags); +} + +bool link_ipv6_enabled(Link *link); +int link_ipv6ll_gained(Link *link); +bool link_has_ipv6_connectivity(Link *link); + +int link_stop_engines(Link *link, bool may_keep_dhcp); + +const char* link_state_to_string(LinkState s) _const_; +LinkState link_state_from_string(const char *s) _pure_; + +int link_reconfigure_impl(Link *link, bool force); +int link_reconfigure(Link *link, bool force); + +int manager_udev_process_link(Manager *m, sd_device *device, sd_device_action_t action); +int manager_rtnl_process_link(sd_netlink *rtnl, sd_netlink_message *message, Manager *m); + +int link_flags_to_string_alloc(uint32_t flags, char **ret); +const char *kernel_operstate_to_string(int t) _const_; diff --git a/src/network/networkd-lldp-rx.c b/src/network/networkd-lldp-rx.c new file mode 100644 index 0000000..3a59884 --- /dev/null +++ b/src/network/networkd-lldp-rx.c @@ -0,0 +1,173 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <net/if_arp.h> +#include <unistd.h> + +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "networkd-link.h" +#include "networkd-lldp-rx.h" +#include "networkd-lldp-tx.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util.h" + +DEFINE_CONFIG_PARSE_ENUM(config_parse_lldp_mode, lldp_mode, LLDPMode, "Failed to parse LLDP= setting."); + +static const char* const lldp_mode_table[_LLDP_MODE_MAX] = { + [LLDP_MODE_NO] = "no", + [LLDP_MODE_YES] = "yes", + [LLDP_MODE_ROUTERS_ONLY] = "routers-only", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(lldp_mode, LLDPMode, LLDP_MODE_YES); + +static bool link_lldp_rx_enabled(Link *link) { + assert(link); + + if (link->flags & IFF_LOOPBACK) + return false; + + if (link->iftype != ARPHRD_ETHER) + return false; + + if (!link->network) + return false; + + /* LLDP should be handled on bridge and bond slaves as those have a direct connection to their peers, + * not on the bridge/bond master. Linux doesn't even (by default) forward lldp packets to the bridge + * master. */ + if (link->kind && STR_IN_SET(link->kind, "bridge", "bond")) + return false; + + return link->network->lldp_mode != LLDP_MODE_NO; +} + +static void lldp_rx_handler(sd_lldp_rx *lldp_rx, sd_lldp_rx_event_t event, sd_lldp_neighbor *n, void *userdata) { + Link *link = ASSERT_PTR(userdata); + int r; + + (void) link_lldp_save(link); + + if (link->lldp_tx && event == SD_LLDP_RX_EVENT_ADDED) { + /* If we received information about a new neighbor, restart the LLDP "fast" logic */ + + log_link_debug(link, "Received LLDP datagram from previously unknown neighbor, restarting 'fast' LLDP transmission."); + + (void) sd_lldp_tx_stop(link->lldp_tx); + r = sd_lldp_tx_start(link->lldp_tx); + if (r < 0) + log_link_warning_errno(link, r, "Failed to restart LLDP transmission: %m"); + } +} + +int link_lldp_rx_configure(Link *link) { + int r; + + if (!link_lldp_rx_enabled(link)) + return 0; + + if (link->lldp_rx) + return -EBUSY; + + r = sd_lldp_rx_new(&link->lldp_rx); + if (r < 0) + return r; + + r = sd_lldp_rx_attach_event(link->lldp_rx, link->manager->event, 0); + if (r < 0) + return r; + + r = sd_lldp_rx_set_ifindex(link->lldp_rx, link->ifindex); + if (r < 0) + return r; + + r = sd_lldp_rx_match_capabilities(link->lldp_rx, + link->network->lldp_mode == LLDP_MODE_ROUTERS_ONLY ? + SD_LLDP_SYSTEM_CAPABILITIES_ALL_ROUTERS : + SD_LLDP_SYSTEM_CAPABILITIES_ALL); + if (r < 0) + return r; + + r = sd_lldp_rx_set_filter_address(link->lldp_rx, &link->hw_addr.ether); + if (r < 0) + return r; + + r = sd_lldp_rx_set_callback(link->lldp_rx, lldp_rx_handler, link); + if (r < 0) + return r; + + return 0; +} + +int link_lldp_save(Link *link) { + _cleanup_(unlink_and_freep) char *temp_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + sd_lldp_neighbor **l = NULL; + int n = 0, r, i; + + assert(link); + + if (isempty(link->lldp_file)) + return 0; /* Do not update state file when running in test mode. */ + + if (!link->lldp_rx) { + (void) unlink(link->lldp_file); + return 0; + } + + r = sd_lldp_rx_get_neighbors(link->lldp_rx, &l); + if (r < 0) + return r; + if (r == 0) { + (void) unlink(link->lldp_file); + return 0; + } + + n = r; + + r = fopen_temporary(link->lldp_file, &f, &temp_path); + if (r < 0) + goto finish; + + (void) fchmod(fileno(f), 0644); + + for (i = 0; i < n; i++) { + const void *p; + le64_t u; + size_t sz; + + r = sd_lldp_neighbor_get_raw(l[i], &p, &sz); + if (r < 0) + goto finish; + + u = htole64(sz); + (void) fwrite(&u, 1, sizeof(u), f); + (void) fwrite(p, 1, sz, f); + } + + r = fflush_and_check(f); + if (r < 0) + goto finish; + + r = conservative_rename(temp_path, link->lldp_file); + if (r < 0) + goto finish; + +finish: + if (r < 0) + log_link_error_errno(link, r, "Failed to save LLDP data to %s: %m", link->lldp_file); + + if (l) { + for (i = 0; i < n; i++) + sd_lldp_neighbor_unref(l[i]); + free(l); + } + + return r; +} diff --git a/src/network/networkd-lldp-rx.h b/src/network/networkd-lldp-rx.h new file mode 100644 index 0000000..22f6602 --- /dev/null +++ b/src/network/networkd-lldp-rx.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" + +typedef struct Link Link; + +typedef enum LLDPMode { + LLDP_MODE_NO = 0, + LLDP_MODE_YES = 1, + LLDP_MODE_ROUTERS_ONLY = 2, + _LLDP_MODE_MAX, + _LLDP_MODE_INVALID = -EINVAL, +} LLDPMode; + +int link_lldp_rx_configure(Link *link); +int link_lldp_save(Link *link); + +const char* lldp_mode_to_string(LLDPMode m) _const_; +LLDPMode lldp_mode_from_string(const char *s) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_lldp_mode); diff --git a/src/network/networkd-lldp-tx.c b/src/network/networkd-lldp-tx.c new file mode 100644 index 0000000..fc9196f --- /dev/null +++ b/src/network/networkd-lldp-tx.c @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <net/if_arp.h> + +#include "sd-lldp-tx.h" + +#include "networkd-link.h" +#include "networkd-lldp-tx.h" +#include "networkd-manager.h" +#include "parse-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" + +static bool link_lldp_tx_enabled(Link *link) { + assert(link); + + if (link->flags & IFF_LOOPBACK) + return false; + + if (link->iftype != ARPHRD_ETHER) + return false; + + if (!link->network) + return false; + + if (link->kind && STR_IN_SET(link->kind, "bridge", "bond")) + return false; + + return link->network->lldp_multicast_mode >= 0 && + link->network->lldp_multicast_mode < _SD_LLDP_MULTICAST_MODE_MAX; +} + +int link_lldp_tx_configure(Link *link) { + int r; + + assert(link); + + if (!link_lldp_tx_enabled(link)) + return 0; + + if (link->lldp_tx) + return -EBUSY; + + r = sd_lldp_tx_new(&link->lldp_tx); + if (r < 0) + return r; + + r = sd_lldp_tx_attach_event(link->lldp_tx, link->manager->event, 0); + if (r < 0) + return r; + + r = sd_lldp_tx_set_ifindex(link->lldp_tx, link->ifindex); + if (r < 0) + return r; + + r = sd_lldp_tx_set_hwaddr(link->lldp_tx, &link->hw_addr.ether); + if (r < 0) + return r; + + assert(link->network); + + r = sd_lldp_tx_set_multicast_mode(link->lldp_tx, link->network->lldp_multicast_mode); + if (r < 0) + return r; + + r = sd_lldp_tx_set_capabilities(link->lldp_tx, + SD_LLDP_SYSTEM_CAPABILITIES_STATION | + SD_LLDP_SYSTEM_CAPABILITIES_BRIDGE | + SD_LLDP_SYSTEM_CAPABILITIES_ROUTER, + (link->network->ip_forward != ADDRESS_FAMILY_NO) ? + SD_LLDP_SYSTEM_CAPABILITIES_ROUTER : + SD_LLDP_SYSTEM_CAPABILITIES_STATION); + if (r < 0) + return r; + + r = sd_lldp_tx_set_port_description(link->lldp_tx, link->network->description); + if (r < 0) + return r; + + r = sd_lldp_tx_set_mud_url(link->lldp_tx, link->network->lldp_mudurl); + if (r < 0) + return r; + + return 0; +} + +static const char * const lldp_multicast_mode_table[_SD_LLDP_MULTICAST_MODE_MAX] = { + [SD_LLDP_MULTICAST_MODE_NEAREST_BRIDGE] = "nearest-bridge", + [SD_LLDP_MULTICAST_MODE_NON_TPMR_BRIDGE] = "non-tpmr-bridge", + [SD_LLDP_MULTICAST_MODE_CUSTOMER_BRIDGE] = "customer-bridge", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(lldp_multicast_mode, sd_lldp_multicast_mode_t); + +int config_parse_lldp_multicast_mode( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + sd_lldp_multicast_mode_t m, *mode = ASSERT_PTR(data); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *mode = _SD_LLDP_MULTICAST_MODE_INVALID; + return 0; + } + + r = parse_boolean(rvalue); + if (r >= 0) { + *mode = r == 0 ? _SD_LLDP_MULTICAST_MODE_INVALID : SD_LLDP_MULTICAST_MODE_NEAREST_BRIDGE; + return 0; + } + + m = lldp_multicast_mode_from_string(rvalue); + if (m < 0) { + log_syntax(unit, LOG_WARNING, filename, line, m, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + *mode = m; + return 0; +} diff --git a/src/network/networkd-lldp-tx.h b/src/network/networkd-lldp-tx.h new file mode 100644 index 0000000..73757f1 --- /dev/null +++ b/src/network/networkd-lldp-tx.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" + +typedef struct Link Link; + +int link_lldp_tx_configure(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_lldp_multicast_mode); diff --git a/src/network/networkd-manager-bus.c b/src/network/networkd-manager-bus.c new file mode 100644 index 0000000..aecbc1d --- /dev/null +++ b/src/network/networkd-manager-bus.c @@ -0,0 +1,425 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <sys/capability.h> + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-message-util.h" +#include "bus-polkit.h" +#include "networkd-dhcp-server-bus.h" +#include "networkd-dhcp4-bus.h" +#include "networkd-dhcp6-bus.h" +#include "networkd-json.h" +#include "networkd-link-bus.h" +#include "networkd-link.h" +#include "networkd-manager-bus.h" +#include "networkd-manager.h" +#include "networkd-network-bus.h" +#include "path-util.h" +#include "strv.h" +#include "user-util.h" + +static int method_list_links(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *manager = userdata; + Link *link; + int r; + + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(iso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(link, manager->links_by_index) { + _cleanup_free_ char *path = NULL; + + path = link_bus_path(link); + if (!path) + return -ENOMEM; + + r = sd_bus_message_append( + reply, "(iso)", + link->ifindex, + link->ifname, + empty_to_root(path)); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_get_link_by_name(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_free_ char *path = NULL; + Manager *manager = userdata; + const char *name; + Link *link; + int r; + + r = sd_bus_message_read(message, "s", &name); + if (r < 0) + return r; + + if (link_get_by_name(manager, name, &link) < 0) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %s not known", name); + + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + path = link_bus_path(link); + if (!path) + return -ENOMEM; + + r = sd_bus_message_append(reply, "io", link->ifindex, empty_to_root(path)); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_get_link_by_index(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_free_ char *path = NULL; + Manager *manager = userdata; + int ifindex, r; + Link *link; + + r = bus_message_read_ifindex(message, error, &ifindex); + if (r < 0) + return r; + + r = link_get_by_index(manager, ifindex, &link); + if (r < 0) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %i not known", ifindex); + + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + path = link_bus_path(link); + if (!path) + return -ENOMEM; + + r = sd_bus_message_append(reply, "so", link->ifname, empty_to_root(path)); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int call_link_method(Manager *m, sd_bus_message *message, sd_bus_message_handler_t handler, sd_bus_error *error) { + int ifindex, r; + Link *l; + + assert(m); + assert(message); + assert(handler); + + r = bus_message_read_ifindex(message, error, &ifindex); + if (r < 0) + return r; + + r = link_get_by_index(m, ifindex, &l); + if (r < 0) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %i not known", ifindex); + + return handler(message, l, error); +} + +static int bus_method_set_link_ntp_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_ntp_servers, error); +} + +static int bus_method_set_link_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dns_servers, error); +} + +static int bus_method_set_link_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dns_servers_ex, error); +} + +static int bus_method_set_link_domains(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_domains, error); +} + +static int bus_method_set_link_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_default_route, error); +} + +static int bus_method_set_link_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_llmnr, error); +} + +static int bus_method_set_link_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_mdns, error); +} + +static int bus_method_set_link_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dns_over_tls, error); +} + +static int bus_method_set_link_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dnssec, error); +} + +static int bus_method_set_link_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dnssec_negative_trust_anchors, error); +} + +static int bus_method_revert_link_ntp(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_revert_ntp, error); +} + +static int bus_method_revert_link_dns(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_revert_dns, error); +} + +static int bus_method_renew_link(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_renew, error); +} + +static int bus_method_force_renew_link(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_force_renew, error); +} + +static int bus_method_reconfigure_link(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_reconfigure, error); +} + +static int bus_method_reload(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *manager = userdata; + 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 = manager_reload(manager); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + +static int bus_method_describe_link(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_describe, error); +} + +static int bus_method_describe(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ char *text = NULL; + Manager *manager = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = manager_build_json(manager, &v); + if (r < 0) + return log_error_errno(r, "Failed to build JSON data: %m"); + + r = json_variant_format(v, 0, &text); + if (r < 0) + return log_error_errno(r, "Failed to format JSON data: %m"); + + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "s", text); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int property_get_namespace_id( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + uint64_t id = 0; + struct stat st; + + assert(bus); + assert(reply); + + /* Returns our own network namespace ID, i.e. the inode number of /proc/self/ns/net. This allows + * unprivileged clients to determine whether they are in the same network namespace as us (note that + * access to that path is restricted, thus they can't check directly unless privileged). */ + + if (stat("/proc/self/ns/net", &st) < 0) { + log_warning_errno(errno, "Failed to stat network namespace, ignoring: %m"); + id = 0; + } else + id = st.st_ino; + + return sd_bus_message_append(reply, "t", id); +} + +static const sd_bus_vtable manager_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("OperationalState", "s", property_get_operational_state, offsetof(Manager, operational_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("CarrierState", "s", property_get_carrier_state, offsetof(Manager, carrier_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("AddressState", "s", property_get_address_state, offsetof(Manager, address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("IPv4AddressState", "s", property_get_address_state, offsetof(Manager, ipv4_address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("IPv6AddressState", "s", property_get_address_state, offsetof(Manager, ipv6_address_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("OnlineState", "s", property_get_online_state, offsetof(Manager, online_state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("NamespaceId", "t", property_get_namespace_id, 0, SD_BUS_VTABLE_PROPERTY_CONST), + + SD_BUS_METHOD_WITH_ARGS("ListLinks", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("a(iso)", links), + method_list_links, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("GetLinkByName", + SD_BUS_ARGS("s", name), + SD_BUS_RESULT("i", ifindex, "o", path), + method_get_link_by_name, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("GetLinkByIndex", + SD_BUS_ARGS("i", ifindex), + SD_BUS_RESULT("s", name, "o", path), + method_get_link_by_index, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkNTP", + SD_BUS_ARGS("i", ifindex, "as", servers), + SD_BUS_NO_RESULT, + bus_method_set_link_ntp_servers, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNS", + SD_BUS_ARGS("i", ifindex, "a(iay)", addresses), + SD_BUS_NO_RESULT, + bus_method_set_link_dns_servers, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNSEx", + SD_BUS_ARGS("i", ifindex, "a(iayqs)", addresses), + SD_BUS_NO_RESULT, + bus_method_set_link_dns_servers_ex, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDomains", + SD_BUS_ARGS("i", ifindex, "a(sb)", domains), + SD_BUS_NO_RESULT, + bus_method_set_link_domains, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDefaultRoute", + SD_BUS_ARGS("i", ifindex, "b", enable), + SD_BUS_NO_RESULT, + bus_method_set_link_default_route, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkLLMNR", + SD_BUS_ARGS("i", ifindex, "s", mode), + SD_BUS_NO_RESULT, + bus_method_set_link_llmnr, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkMulticastDNS", + SD_BUS_ARGS("i", ifindex, "s", mode), + SD_BUS_NO_RESULT, + bus_method_set_link_mdns, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNSOverTLS", + SD_BUS_ARGS("i", ifindex, "s", mode), + SD_BUS_NO_RESULT, + bus_method_set_link_dns_over_tls, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNSSEC", + SD_BUS_ARGS("i", ifindex, "s", mode), + SD_BUS_NO_RESULT, + bus_method_set_link_dnssec, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNSSECNegativeTrustAnchors", + SD_BUS_ARGS("i", ifindex, "as", names), + SD_BUS_NO_RESULT, + bus_method_set_link_dnssec_negative_trust_anchors, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RevertLinkNTP", + SD_BUS_ARGS("i", ifindex), + SD_BUS_NO_RESULT, + bus_method_revert_link_ntp, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RevertLinkDNS", + SD_BUS_ARGS("i", ifindex), + SD_BUS_NO_RESULT, + bus_method_revert_link_dns, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RenewLink", + SD_BUS_ARGS("i", ifindex), + SD_BUS_NO_RESULT, + bus_method_renew_link, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ForceRenewLink", + SD_BUS_ARGS("i", ifindex), + SD_BUS_NO_RESULT, + bus_method_force_renew_link, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ReconfigureLink", + SD_BUS_ARGS("i", ifindex), + SD_BUS_NO_RESULT, + bus_method_reconfigure_link, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("Reload", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_method_reload, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("DescribeLink", + SD_BUS_ARGS("i", ifindex), + SD_BUS_RESULT("s", json), + bus_method_describe_link, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("Describe", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("s", json), + bus_method_describe, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_VTABLE_END +}; + +int manager_send_changed_strv(Manager *manager, char **properties) { + assert(manager); + assert(properties); + + if (sd_bus_is_ready(manager->bus) <= 0) + return 0; + + return sd_bus_emit_properties_changed_strv( + manager->bus, + "/org/freedesktop/network1", + "org.freedesktop.network1.Manager", + properties); +} + +const BusObjectImplementation manager_object = { + "/org/freedesktop/network1", + "org.freedesktop.network1.Manager", + .vtables = BUS_VTABLES(manager_vtable), + .children = BUS_IMPLEMENTATIONS( + &link_object, /* This is the main implementation for /org/freedesktop/network1/link, + * and must be earlier than the dhcp objects below. */ + &dhcp_server_object, + &dhcp_client_object, + &dhcp6_client_object, + &network_object), +}; diff --git a/src/network/networkd-manager-bus.h b/src/network/networkd-manager-bus.h new file mode 100644 index 0000000..5cd7f16 --- /dev/null +++ b/src/network/networkd-manager-bus.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" + +#include "bus-object.h" + +typedef struct Manager Manager; + +extern const BusObjectImplementation manager_object; + +int manager_send_changed_strv(Manager *m, char **properties); diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c new file mode 100644 index 0000000..c09dcfb --- /dev/null +++ b/src/network/networkd-manager.c @@ -0,0 +1,1108 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <sys/socket.h> +#include <unistd.h> +#include <linux/if.h> +#include <linux/fib_rules.h> +#include <linux/nexthop.h> +#include <linux/nl80211.h> + +#include "sd-netlink.h" + +#include "alloc-util.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-log-control-api.h" +#include "bus-polkit.h" +#include "bus-util.h" +#include "common-signal.h" +#include "conf-parser.h" +#include "constants.h" +#include "daemon-util.h" +#include "device-private.h" +#include "device-util.h" +#include "dns-domain.h" +#include "fd-util.h" +#include "fileio.h" +#include "firewall-util.h" +#include "fs-util.h" +#include "initrd-util.h" +#include "local-addresses.h" +#include "netlink-util.h" +#include "network-internal.h" +#include "networkd-address-pool.h" +#include "networkd-address.h" +#include "networkd-dhcp-server-bus.h" +#include "networkd-dhcp6.h" +#include "networkd-link-bus.h" +#include "networkd-manager-bus.h" +#include "networkd-manager.h" +#include "networkd-neighbor.h" +#include "networkd-network-bus.h" +#include "networkd-nexthop.h" +#include "networkd-queue.h" +#include "networkd-route.h" +#include "networkd-routing-policy-rule.h" +#include "networkd-speed-meter.h" +#include "networkd-state-file.h" +#include "networkd-wifi.h" +#include "networkd-wiphy.h" +#include "ordered-set.h" +#include "path-lookup.h" +#include "path-util.h" +#include "qdisc.h" +#include "selinux-util.h" +#include "set.h" +#include "signal-util.h" +#include "stat-util.h" +#include "strv.h" +#include "sysctl-util.h" +#include "tclass.h" +#include "tmpfile-util.h" +#include "tuntap.h" +#include "udev-util.h" + +/* use 128 MB for receive socket kernel queue. */ +#define RCVBUF_SIZE (128*1024*1024) + +static int match_prepare_for_sleep(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) { + Manager *m = ASSERT_PTR(userdata); + Link *link; + int b, r; + + assert(message); + + r = sd_bus_message_read(message, "b", &b); + if (r < 0) { + bus_log_parse_error(r); + return 0; + } + + if (b) + return 0; + + log_debug("Coming back from suspend, reconfiguring all connections..."); + + HASHMAP_FOREACH(link, m->links_by_index) { + r = link_reconfigure(link, /* force = */ true); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to reconfigure interface: %m"); + link_enter_failed(link); + } + } + + return 0; +} + +static int on_connected(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) { + Manager *m = ASSERT_PTR(userdata); + + assert(message); + + /* Did we get a timezone or transient hostname from DHCP while D-Bus wasn't up yet? */ + if (m->dynamic_hostname) + (void) manager_set_hostname(m, m->dynamic_hostname); + if (m->dynamic_timezone) + (void) manager_set_timezone(m, m->dynamic_timezone); + if (m->product_uuid_requested) + (void) manager_request_product_uuid(m); + + return 0; +} + +static int manager_connect_bus(Manager *m) { + int r; + + assert(m); + assert(!m->bus); + + r = bus_open_system_watch_bind_with_description(&m->bus, "bus-api-network"); + if (r < 0) + return log_error_errno(r, "Failed to connect to bus: %m"); + + r = bus_add_implementation(m->bus, &manager_object, m); + if (r < 0) + return r; + + r = bus_log_control_api_register(m->bus); + if (r < 0) + return r; + + r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.network1", 0, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to request name: %m"); + + r = sd_bus_attach_event(m->bus, m->event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + r = sd_bus_match_signal_async( + m->bus, + NULL, + "org.freedesktop.DBus.Local", + NULL, + "org.freedesktop.DBus.Local", + "Connected", + on_connected, NULL, m); + if (r < 0) + return log_error_errno(r, "Failed to request match on Connected signal: %m"); + + r = bus_match_signal_async( + m->bus, + NULL, + bus_login_mgr, + "PrepareForSleep", + match_prepare_for_sleep, NULL, m); + if (r < 0) + log_warning_errno(r, "Failed to request match for PrepareForSleep, ignoring: %m"); + + return 0; +} + +static int manager_process_uevent(sd_device_monitor *monitor, sd_device *device, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + sd_device_action_t action; + const char *s; + int r; + + assert(device); + + r = sd_device_get_action(device, &action); + if (r < 0) + return log_device_warning_errno(device, r, "Failed to get udev action, ignoring: %m"); + + r = sd_device_get_subsystem(device, &s); + if (r < 0) + return log_device_warning_errno(device, r, "Failed to get subsystem, ignoring: %m"); + + if (streq(s, "net")) + r = manager_udev_process_link(m, device, action); + else if (streq(s, "ieee80211")) + r = manager_udev_process_wiphy(m, device, action); + else if (streq(s, "rfkill")) + r = manager_udev_process_rfkill(m, device, action); + else { + log_device_debug(device, "Received device with unexpected subsystem \"%s\", ignoring.", s); + return 0; + } + if (r < 0) + log_device_warning_errno(device, r, "Failed to process \"%s\" uevent, ignoring: %m", + device_action_to_string(action)); + + return 0; +} + +static int manager_connect_udev(Manager *m) { + int r; + + /* udev does not initialize devices inside containers, so we rely on them being already + * initialized before entering the container. */ + if (!udev_available()) + return 0; + + r = sd_device_monitor_new(&m->device_monitor); + if (r < 0) + return log_error_errno(r, "Failed to initialize device monitor: %m"); + + r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "net", NULL); + if (r < 0) + return log_error_errno(r, "Could not add device monitor filter for net subsystem: %m"); + + r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "ieee80211", NULL); + if (r < 0) + return log_error_errno(r, "Could not add device monitor filter for ieee80211 subsystem: %m"); + + r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "rfkill", NULL); + if (r < 0) + return log_error_errno(r, "Could not add device monitor filter for rfkill subsystem: %m"); + + r = sd_device_monitor_attach_event(m->device_monitor, m->event); + if (r < 0) + return log_error_errno(r, "Failed to attach event to device monitor: %m"); + + r = sd_device_monitor_start(m->device_monitor, manager_process_uevent, m); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + return 0; +} + +static int manager_listen_fds(Manager *m, int *ret_rtnl_fd) { + _cleanup_strv_free_ char **names = NULL; + int n, rtnl_fd = -EBADF; + + assert(m); + assert(ret_rtnl_fd); + + n = sd_listen_fds_with_names(/* unset_environment = */ true, &names); + if (n < 0) + return n; + + if (strv_length(names) != (size_t) n) + return -EINVAL; + + for (int i = 0; i < n; i++) { + int fd = i + SD_LISTEN_FDS_START; + + if (sd_is_socket(fd, AF_NETLINK, SOCK_RAW, -1) > 0) { + if (rtnl_fd >= 0) { + log_debug("Received multiple netlink socket, ignoring."); + safe_close(fd); + continue; + } + + rtnl_fd = fd; + continue; + } + + if (manager_add_tuntap_fd(m, fd, names[i]) >= 0) + continue; + + if (m->test_mode) + safe_close(fd); + else + close_and_notify_warn(fd, names[i]); + } + + *ret_rtnl_fd = rtnl_fd; + return 0; +} + +static int manager_connect_genl(Manager *m) { + int r; + + assert(m); + + r = sd_genl_socket_open(&m->genl); + if (r < 0) + return r; + + r = sd_netlink_increase_rxbuf(m->genl, RCVBUF_SIZE); + if (r < 0) + log_warning_errno(r, "Failed to increase receive buffer size for general netlink socket, ignoring: %m"); + + r = sd_netlink_attach_event(m->genl, m->event, 0); + if (r < 0) + return r; + + r = genl_add_match(m->genl, NULL, NL80211_GENL_NAME, NL80211_MULTICAST_GROUP_CONFIG, 0, + &manager_genl_process_nl80211_config, NULL, m, "network-genl_process_nl80211_config"); + if (r < 0 && r != -EOPNOTSUPP) + return r; + + r = genl_add_match(m->genl, NULL, NL80211_GENL_NAME, NL80211_MULTICAST_GROUP_MLME, 0, + &manager_genl_process_nl80211_mlme, NULL, m, "network-genl_process_nl80211_mlme"); + if (r < 0 && r != -EOPNOTSUPP) + return r; + + return 0; +} + +static int manager_setup_rtnl_filter(Manager *manager) { + struct sock_filter filter[] = { + /* Check the packet length. */ + BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0), /* A <- packet length */ + BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct nlmsghdr), 1, 0), /* A (packet length) >= sizeof(struct nlmsghdr) ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* reject */ + /* Always accept multipart message. */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct nlmsghdr, nlmsg_flags)), /* A <- message flags */ + BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, htobe16(NLM_F_MULTI), 0, 1), /* message flags has NLM_F_MULTI ? */ + BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */ + /* Accept all message types except for RTM_NEWNEIGH or RTM_DELNEIGH. */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct nlmsghdr, nlmsg_type)), /* A <- message type */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, htobe16(RTM_NEWNEIGH), 2, 0), /* message type == RTM_NEWNEIGH ? */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, htobe16(RTM_DELNEIGH), 1, 0), /* message type == RTM_DELNEIGH ? */ + BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */ + /* Check the packet length. */ + BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0), /* A <- packet length */ + BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct nlmsghdr) + sizeof(struct ndmsg), 1, 0), + /* packet length >= sizeof(struct nlmsghdr) + sizeof(struct ndmsg) ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* reject */ + /* Reject the message when the neighbor state does not have NUD_PERMANENT flag. */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, sizeof(struct nlmsghdr) + offsetof(struct ndmsg, ndm_state)), + /* A <- neighbor state */ + BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, htobe16(NUD_PERMANENT), 1, 0), /* neighbor state has NUD_PERMANENT ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* reject */ + BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */ + }; + + assert(manager); + assert(manager->rtnl); + + return sd_netlink_attach_filter(manager->rtnl, ELEMENTSOF(filter), filter); +} + +static int manager_connect_rtnl(Manager *m, int fd) { + _unused_ _cleanup_close_ int fd_close = fd; + int r; + + assert(m); + + /* This takes input fd. */ + + if (fd < 0) + r = sd_netlink_open(&m->rtnl); + else + r = sd_netlink_open_fd(&m->rtnl, fd); + if (r < 0) + return r; + TAKE_FD(fd_close); + + /* Bump receiver buffer, but only if we are not called via socket activation, as in that + * case systemd sets the receive buffer size for us, and the value in the .socket unit + * should take full effect. */ + if (fd < 0) { + r = sd_netlink_increase_rxbuf(m->rtnl, RCVBUF_SIZE); + if (r < 0) + log_warning_errno(r, "Failed to increase receive buffer size for rtnl socket, ignoring: %m"); + } + + r = sd_netlink_attach_event(m->rtnl, m->event, 0); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_NEWLINK, &manager_rtnl_process_link, NULL, m, "network-rtnl_process_link"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_DELLINK, &manager_rtnl_process_link, NULL, m, "network-rtnl_process_link"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_NEWQDISC, &manager_rtnl_process_qdisc, NULL, m, "network-rtnl_process_qdisc"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_DELQDISC, &manager_rtnl_process_qdisc, NULL, m, "network-rtnl_process_qdisc"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_NEWTCLASS, &manager_rtnl_process_tclass, NULL, m, "network-rtnl_process_tclass"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_DELTCLASS, &manager_rtnl_process_tclass, NULL, m, "network-rtnl_process_tclass"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_NEWADDR, &manager_rtnl_process_address, NULL, m, "network-rtnl_process_address"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_DELADDR, &manager_rtnl_process_address, NULL, m, "network-rtnl_process_address"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_NEWNEIGH, &manager_rtnl_process_neighbor, NULL, m, "network-rtnl_process_neighbor"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_DELNEIGH, &manager_rtnl_process_neighbor, NULL, m, "network-rtnl_process_neighbor"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_NEWROUTE, &manager_rtnl_process_route, NULL, m, "network-rtnl_process_route"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_DELROUTE, &manager_rtnl_process_route, NULL, m, "network-rtnl_process_route"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_NEWRULE, &manager_rtnl_process_rule, NULL, m, "network-rtnl_process_rule"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_DELRULE, &manager_rtnl_process_rule, NULL, m, "network-rtnl_process_rule"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_NEWNEXTHOP, &manager_rtnl_process_nexthop, NULL, m, "network-rtnl_process_nexthop"); + if (r < 0) + return r; + + r = netlink_add_match(m->rtnl, NULL, RTM_DELNEXTHOP, &manager_rtnl_process_nexthop, NULL, m, "network-rtnl_process_nexthop"); + if (r < 0) + return r; + + return manager_setup_rtnl_filter(m); +} + +static int manager_dirty_handler(sd_event_source *s, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + Link *link; + int r; + + if (m->dirty) { + r = manager_save(m); + if (r < 0) + log_warning_errno(r, "Failed to update state file %s, ignoring: %m", m->state_file); + } + + SET_FOREACH(link, m->dirty_links) { + r = link_save_and_clean(link); + if (r < 0) + log_link_warning_errno(link, r, "Failed to update link state file %s, ignoring: %m", link->state_file); + } + + return 1; +} + +static int signal_terminate_callback(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + m->restarting = false; + + log_debug("Terminate operation initiated."); + + return sd_event_exit(sd_event_source_get_event(s), 0); +} + +static int signal_restart_callback(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + m->restarting = true; + + log_debug("Restart operation initiated."); + + return sd_event_exit(sd_event_source_get_event(s), 0); +} + +static int signal_reload_callback(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + manager_reload(m); + + return 0; +} + +static int manager_set_keep_configuration(Manager *m) { + int r; + + assert(m); + + if (in_initrd()) { + log_debug("Running in initrd, keep DHCPv4 addresses on stopping networkd by default."); + m->keep_configuration = KEEP_CONFIGURATION_DHCP_ON_STOP; + return 0; + } + + r = path_is_network_fs("/"); + if (r < 0) + return log_error_errno(r, "Failed to detect if root is network filesystem: %m"); + if (r == 0) { + m->keep_configuration = _KEEP_CONFIGURATION_INVALID; + return 0; + } + + log_debug("Running on network filesystem, enabling KeepConfiguration= by default."); + m->keep_configuration = KEEP_CONFIGURATION_YES; + return 0; +} + +int manager_setup(Manager *m) { + _cleanup_close_ int rtnl_fd = -EBADF; + int r; + + assert(m); + + r = sd_event_default(&m->event); + if (r < 0) + return r; + + (void) sd_event_set_watchdog(m->event, true); + (void) sd_event_add_signal(m->event, NULL, SIGTERM | SD_EVENT_SIGNAL_PROCMASK, signal_terminate_callback, m); + (void) sd_event_add_signal(m->event, NULL, SIGINT | SD_EVENT_SIGNAL_PROCMASK, signal_terminate_callback, m); + (void) sd_event_add_signal(m->event, NULL, SIGUSR2 | SD_EVENT_SIGNAL_PROCMASK, signal_restart_callback, m); + (void) sd_event_add_signal(m->event, NULL, SIGHUP | SD_EVENT_SIGNAL_PROCMASK, signal_reload_callback, m); + (void) sd_event_add_signal(m->event, NULL, (SIGRTMIN+18) | SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, NULL); + + r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL); + if (r < 0) + log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m"); + + r = sd_event_add_post(m->event, NULL, manager_dirty_handler, m); + if (r < 0) + return r; + + r = sd_event_add_post(m->event, NULL, manager_process_requests, m); + if (r < 0) + return r; + + r = manager_listen_fds(m, &rtnl_fd); + if (r < 0) + return r; + + r = manager_connect_rtnl(m, TAKE_FD(rtnl_fd)); + if (r < 0) + return r; + + r = manager_connect_genl(m); + if (r < 0) + return r; + + if (m->test_mode) + return 0; + + r = manager_connect_bus(m); + if (r < 0) + return r; + + r = manager_connect_udev(m); + if (r < 0) + return r; + + r = sd_resolve_default(&m->resolve); + if (r < 0) + return r; + + r = sd_resolve_attach_event(m->resolve, m->event, 0); + if (r < 0) + return r; + + r = address_pool_setup_default(m); + if (r < 0) + return r; + + r = manager_set_keep_configuration(m); + if (r < 0) + return r; + + m->state_file = strdup("/run/systemd/netif/state"); + if (!m->state_file) + return -ENOMEM; + + return 0; +} + +int manager_new(Manager **ret, bool test_mode) { + _cleanup_(manager_freep) Manager *m = NULL; + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .keep_configuration = _KEEP_CONFIGURATION_INVALID, + .ipv6_privacy_extensions = IPV6_PRIVACY_EXTENSIONS_NO, + .test_mode = test_mode, + .speed_meter_interval_usec = SPEED_METER_DEFAULT_TIME_INTERVAL, + .online_state = _LINK_ONLINE_STATE_INVALID, + .manage_foreign_routes = true, + .manage_foreign_rules = true, + .ethtool_fd = -EBADF, + .dhcp_duid.type = DUID_TYPE_EN, + .dhcp6_duid.type = DUID_TYPE_EN, + .duid_product_uuid.type = DUID_TYPE_UUID, + }; + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + Link *link; + + if (!m) + return NULL; + + free(m->state_file); + + HASHMAP_FOREACH(link, m->links_by_index) + (void) link_stop_engines(link, true); + + m->request_queue = ordered_set_free(m->request_queue); + + m->dirty_links = set_free_with_destructor(m->dirty_links, link_unref); + m->new_wlan_ifindices = set_free(m->new_wlan_ifindices); + m->links_by_name = hashmap_free(m->links_by_name); + m->links_by_hw_addr = hashmap_free(m->links_by_hw_addr); + m->links_by_dhcp_pd_subnet_prefix = hashmap_free(m->links_by_dhcp_pd_subnet_prefix); + m->links_by_index = hashmap_free_with_destructor(m->links_by_index, link_unref); + + m->dhcp_pd_subnet_ids = set_free(m->dhcp_pd_subnet_ids); + m->networks = ordered_hashmap_free_with_destructor(m->networks, network_unref); + + m->netdevs = hashmap_free_with_destructor(m->netdevs, netdev_unref); + + m->tuntap_fds_by_name = hashmap_free(m->tuntap_fds_by_name); + + m->wiphy_by_name = hashmap_free(m->wiphy_by_name); + m->wiphy_by_index = hashmap_free_with_destructor(m->wiphy_by_index, wiphy_free); + + ordered_set_free_free(m->address_pools); + + hashmap_free(m->route_table_names_by_number); + hashmap_free(m->route_table_numbers_by_name); + + set_free(m->rules); + + sd_netlink_unref(m->rtnl); + sd_netlink_unref(m->genl); + sd_resolve_unref(m->resolve); + + /* reject (e.g. unreachable) type routes are managed by Manager, but may be referenced by a + * link. E.g., DHCP6 with prefix delegation creates unreachable routes, and they are referenced + * by the upstream link. And the links may be referenced by netlink slots. Hence, two + * set_free() must be called after the above sd_netlink_unref(). */ + m->routes = set_free(m->routes); + + m->nexthops = set_free(m->nexthops); + m->nexthops_by_id = hashmap_free(m->nexthops_by_id); + + sd_event_source_unref(m->speed_meter_event_source); + sd_event_unref(m->event); + + sd_device_monitor_unref(m->device_monitor); + + bus_verify_polkit_async_registry_free(m->polkit_registry); + sd_bus_flush_close_unref(m->bus); + + free(m->dynamic_timezone); + free(m->dynamic_hostname); + + safe_close(m->ethtool_fd); + + m->fw_ctx = fw_ctx_free(m->fw_ctx); + + return mfree(m); +} + +int manager_start(Manager *m) { + Link *link; + int r; + + assert(m); + + r = manager_start_speed_meter(m); + if (r < 0) + return log_error_errno(r, "Failed to initialize speed meter: %m"); + + /* The dirty handler will deal with future serialization, but the first one + must be done explicitly. */ + + r = manager_save(m); + if (r < 0) + log_warning_errno(r, "Failed to update state file %s, ignoring: %m", m->state_file); + + HASHMAP_FOREACH(link, m->links_by_index) { + r = link_save_and_clean(link); + if (r < 0) + log_link_warning_errno(link, r, "Failed to update link state file %s, ignoring: %m", link->state_file); + } + + return 0; +} + +int manager_load_config(Manager *m) { + int r; + + r = netdev_load(m, false); + if (r < 0) + return r; + + manager_clear_unmanaged_tuntap_fds(m); + + r = network_load(m, &m->networks); + if (r < 0) + return r; + + return manager_build_dhcp_pd_subnet_ids(m); +} + +int manager_enumerate_internal( + Manager *m, + sd_netlink *nl, + sd_netlink_message *req, + int (*process)(sd_netlink *, sd_netlink_message *, Manager *)) { + + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *reply = NULL; + int r; + + assert(m); + assert(nl); + assert(req); + assert(process); + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return r; + + r = sd_netlink_call(nl, req, 0, &reply); + if (r < 0) + return r; + + m->enumerating = true; + for (sd_netlink_message *reply_one = reply; reply_one; reply_one = sd_netlink_message_next(reply_one)) + RET_GATHER(r, process(nl, reply_one, m)); + m->enumerating = false; + + return r; +} + +static int manager_enumerate_links(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->rtnl); + + r = sd_rtnl_message_new_link(m->rtnl, &req, RTM_GETLINK, 0); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_link); +} + +static int manager_enumerate_qdisc(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->rtnl); + + r = sd_rtnl_message_new_traffic_control(m->rtnl, &req, RTM_GETQDISC, 0, 0, 0); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_qdisc); +} + +static int manager_enumerate_tclass(Manager *m) { + Link *link; + int r = 0; + + assert(m); + assert(m->rtnl); + + /* TC class can be enumerated only per link. See tc_dump_tclass() in net/sched/sched_api.c. */ + + HASHMAP_FOREACH(link, m->links_by_index) + RET_GATHER(r, link_enumerate_tclass(link, 0)); + + return r; +} + +static int manager_enumerate_addresses(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->rtnl); + + r = sd_rtnl_message_new_addr(m->rtnl, &req, RTM_GETADDR, 0, 0); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_address); +} + +static int manager_enumerate_neighbors(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->rtnl); + + r = sd_rtnl_message_new_neigh(m->rtnl, &req, RTM_GETNEIGH, 0, AF_UNSPEC); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_neighbor); +} + +static int manager_enumerate_routes(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->rtnl); + + if (!m->manage_foreign_routes) + return 0; + + r = sd_rtnl_message_new_route(m->rtnl, &req, RTM_GETROUTE, 0, 0); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_route); +} + +static int manager_enumerate_rules(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->rtnl); + + if (!m->manage_foreign_rules) + return 0; + + r = sd_rtnl_message_new_routing_policy_rule(m->rtnl, &req, RTM_GETRULE, 0); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_rule); +} + +static int manager_enumerate_nexthop(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->rtnl); + + r = sd_rtnl_message_new_nexthop(m->rtnl, &req, RTM_GETNEXTHOP, 0, 0); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->rtnl, req, manager_rtnl_process_nexthop); +} + +static int manager_enumerate_nl80211_wiphy(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->genl); + + r = sd_genl_message_new(m->genl, NL80211_GENL_NAME, NL80211_CMD_GET_WIPHY, &req); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->genl, req, manager_genl_process_nl80211_wiphy); +} + +static int manager_enumerate_nl80211_config(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(m); + assert(m->genl); + + r = sd_genl_message_new(m->genl, NL80211_GENL_NAME, NL80211_CMD_GET_INTERFACE, &req); + if (r < 0) + return r; + + return manager_enumerate_internal(m, m->genl, req, manager_genl_process_nl80211_config); +} + +static int manager_enumerate_nl80211_mlme(Manager *m) { + Link *link; + int r; + + assert(m); + assert(m->genl); + + HASHMAP_FOREACH(link, m->links_by_index) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + + if (link->wlan_iftype != NL80211_IFTYPE_STATION) + continue; + + r = sd_genl_message_new(m->genl, NL80211_GENL_NAME, NL80211_CMD_GET_STATION, &req); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(req, NL80211_ATTR_IFINDEX, link->ifindex); + if (r < 0) + return r; + + r = manager_enumerate_internal(m, m->genl, req, manager_genl_process_nl80211_mlme); + if (r < 0) + return r; + } + + return 0; +} + +int manager_enumerate(Manager *m) { + int r; + + r = manager_enumerate_links(m); + if (r < 0) + return log_error_errno(r, "Could not enumerate links: %m"); + + r = manager_enumerate_qdisc(m); + if (r == -EOPNOTSUPP) + log_debug_errno(r, "Could not enumerate QDiscs, ignoring: %m"); + else if (r < 0) + return log_error_errno(r, "Could not enumerate QDisc: %m"); + + r = manager_enumerate_tclass(m); + if (r == -EOPNOTSUPP) + log_debug_errno(r, "Could not enumerate TClasses, ignoring: %m"); + else if (r < 0) + return log_error_errno(r, "Could not enumerate TClass: %m"); + + r = manager_enumerate_addresses(m); + if (r < 0) + return log_error_errno(r, "Could not enumerate addresses: %m"); + + r = manager_enumerate_neighbors(m); + if (r < 0) + return log_error_errno(r, "Could not enumerate neighbors: %m"); + + /* NextHop support is added in kernel v5.3 (65ee00a9409f751188a8cdc0988167858eb4a536), + * and older kernels return -EOPNOTSUPP, or -EINVAL if SELinux is enabled. */ + r = manager_enumerate_nexthop(m); + if (r == -EOPNOTSUPP || (r == -EINVAL && mac_selinux_enforcing())) + log_debug_errno(r, "Could not enumerate nexthops, ignoring: %m"); + else if (r < 0) + return log_error_errno(r, "Could not enumerate nexthops: %m"); + + r = manager_enumerate_routes(m); + if (r < 0) + return log_error_errno(r, "Could not enumerate routes: %m"); + + /* If kernel is built with CONFIG_FIB_RULES=n, it returns -EOPNOTSUPP. */ + r = manager_enumerate_rules(m); + if (r == -EOPNOTSUPP) + log_debug_errno(r, "Could not enumerate routing policy rules, ignoring: %m"); + else if (r < 0) + return log_error_errno(r, "Could not enumerate routing policy rules: %m"); + + r = manager_enumerate_nl80211_wiphy(m); + if (r == -EOPNOTSUPP) + log_debug_errno(r, "Could not enumerate wireless LAN phy, ignoring: %m"); + else if (r < 0) + return log_error_errno(r, "Could not enumerate wireless LAN phy: %m"); + + r = manager_enumerate_nl80211_config(m); + if (r == -EOPNOTSUPP) + log_debug_errno(r, "Could not enumerate wireless LAN interfaces, ignoring: %m"); + else if (r < 0) + return log_error_errno(r, "Could not enumerate wireless LAN interfaces: %m"); + + r = manager_enumerate_nl80211_mlme(m); + if (r == -EOPNOTSUPP) + log_debug_errno(r, "Could not enumerate wireless LAN stations, ignoring: %m"); + else if (r < 0) + return log_error_errno(r, "Could not enumerate wireless LAN stations: %m"); + + return 0; +} + +static int set_hostname_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + const sd_bus_error *e; + int r; + + assert(m); + + e = sd_bus_message_get_error(m); + if (e) { + r = sd_bus_error_get_errno(e); + log_warning_errno(r, "Could not set hostname: %s", bus_error_message(e, r)); + } + + return 1; +} + +int manager_set_hostname(Manager *m, const char *hostname) { + int r; + + log_debug("Setting transient hostname: '%s'", strna(hostname)); + + r = free_and_strdup_warn(&m->dynamic_hostname, hostname); + if (r < 0) + return r; + + if (sd_bus_is_ready(m->bus) <= 0) { + log_debug("Not connected to system bus, setting system hostname later."); + return 0; + } + + r = bus_call_method_async( + m->bus, + NULL, + bus_hostname, + "SetHostname", + set_hostname_handler, + m, + "sb", + hostname, + false); + if (r < 0) + return log_error_errno(r, "Could not set transient hostname: %m"); + + return 0; +} + +static int set_timezone_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + const sd_bus_error *e; + int r; + + assert(m); + + e = sd_bus_message_get_error(m); + if (e) { + r = sd_bus_error_get_errno(e); + log_warning_errno(r, "Could not set timezone: %s", bus_error_message(e, r)); + } + + return 1; +} + +int manager_set_timezone(Manager *m, const char *tz) { + int r; + + assert(m); + assert(tz); + + log_debug("Setting system timezone: '%s'", tz); + r = free_and_strdup_warn(&m->dynamic_timezone, tz); + if (r < 0) + return r; + + if (sd_bus_is_ready(m->bus) <= 0) { + log_debug("Not connected to system bus, setting system timezone later."); + return 0; + } + + r = bus_call_method_async( + m->bus, + NULL, + bus_timedate, + "SetTimezone", + set_timezone_handler, + m, + "sb", + tz, + false); + if (r < 0) + return log_error_errno(r, "Could not set timezone: %m"); + + return 0; +} + +int manager_reload(Manager *m) { + Link *link; + int r; + + assert(m); + + (void) sd_notifyf(/* unset= */ false, + "RELOADING=1\n" + "STATUS=Reloading configuration...\n" + "MONOTONIC_USEC=" USEC_FMT, now(CLOCK_MONOTONIC)); + + r = netdev_load(m, /* reload= */ true); + if (r < 0) + goto finish; + + r = network_reload(m); + if (r < 0) + goto finish; + + HASHMAP_FOREACH(link, m->links_by_index) { + r = link_reconfigure(link, /* force = */ false); + if (r < 0) + goto finish; + } + + r = 0; +finish: + (void) sd_notify(/* unset= */ false, NOTIFY_READY); + return r; +} diff --git a/src/network/networkd-manager.h b/src/network/networkd-manager.h new file mode 100644 index 0000000..fbef528 --- /dev/null +++ b/src/network/networkd-manager.h @@ -0,0 +1,127 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" +#include "sd-device.h" +#include "sd-event.h" +#include "sd-id128.h" +#include "sd-netlink.h" +#include "sd-resolve.h" + +#include "dhcp-identifier.h" +#include "firewall-util.h" +#include "hashmap.h" +#include "networkd-link.h" +#include "networkd-network.h" +#include "networkd-sysctl.h" +#include "ordered-set.h" +#include "set.h" +#include "time-util.h" + +struct Manager { + sd_netlink *rtnl; + /* lazy initialized */ + sd_netlink *genl; + sd_event *event; + sd_resolve *resolve; + sd_bus *bus; + sd_device_monitor *device_monitor; + Hashmap *polkit_registry; + int ethtool_fd; + + KeepConfiguration keep_configuration; + IPv6PrivacyExtensions ipv6_privacy_extensions; + + bool test_mode; + bool enumerating; + bool dirty; + bool restarting; + bool manage_foreign_routes; + bool manage_foreign_rules; + + Set *dirty_links; + Set *new_wlan_ifindices; + + char *state_file; + LinkOperationalState operational_state; + LinkCarrierState carrier_state; + LinkAddressState address_state; + LinkAddressState ipv4_address_state; + LinkAddressState ipv6_address_state; + LinkOnlineState online_state; + + Hashmap *links_by_index; + Hashmap *links_by_name; + Hashmap *links_by_hw_addr; + Hashmap *links_by_dhcp_pd_subnet_prefix; + Hashmap *netdevs; + OrderedHashmap *networks; + OrderedSet *address_pools; + Set *dhcp_pd_subnet_ids; + + DUID dhcp_duid; + DUID dhcp6_duid; + DUID duid_product_uuid; + bool has_product_uuid; + bool product_uuid_requested; + + char* dynamic_hostname; + char* dynamic_timezone; + + Set *rules; + + /* Manage nexthops by id. */ + Hashmap *nexthops_by_id; + + /* Manager stores nexthops without RTA_OIF attribute. */ + Set *nexthops; + + /* Manager stores routes without RTA_OIF attribute. */ + unsigned route_remove_messages; + Set *routes; + + /* Route table name */ + Hashmap *route_table_numbers_by_name; + Hashmap *route_table_names_by_number; + + /* Wiphy */ + Hashmap *wiphy_by_index; + Hashmap *wiphy_by_name; + + /* For link speed meter */ + bool use_speed_meter; + sd_event_source *speed_meter_event_source; + usec_t speed_meter_interval_usec; + usec_t speed_meter_usec_new; + usec_t speed_meter_usec_old; + + bool bridge_mdb_on_master_not_supported; + + FirewallContext *fw_ctx; + + OrderedSet *request_queue; + + Hashmap *tuntap_fds_by_name; +}; + +int manager_new(Manager **ret, bool test_mode); +Manager* manager_free(Manager *m); + +int manager_setup(Manager *m); +int manager_start(Manager *m); + +int manager_load_config(Manager *m); + +int manager_enumerate_internal( + Manager *m, + sd_netlink *nl, + sd_netlink_message *req, + int (*process)(sd_netlink *, sd_netlink_message *, Manager *)); +int manager_enumerate(Manager *m); + +int manager_set_hostname(Manager *m, const char *hostname); +int manager_set_timezone(Manager *m, const char *timezone); + +int manager_reload(Manager *m); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); diff --git a/src/network/networkd-ndisc.c b/src/network/networkd-ndisc.c new file mode 100644 index 0000000..840ccb1 --- /dev/null +++ b/src/network/networkd-ndisc.c @@ -0,0 +1,1531 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <arpa/inet.h> +#include <netinet/icmp6.h> +#include <linux/if.h> +#include <linux/if_arp.h> + +#include "sd-ndisc.h" + +#include "event-util.h" +#include "missing_network.h" +#include "networkd-address-generation.h" +#include "networkd-address.h" +#include "networkd-dhcp6.h" +#include "networkd-manager.h" +#include "networkd-ndisc.h" +#include "networkd-queue.h" +#include "networkd-route.h" +#include "networkd-state-file.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "sysctl-util.h" + +#define NDISC_DNSSL_MAX 64U +#define NDISC_RDNSS_MAX 64U +/* Not defined in the RFC, but let's set an upper limit to make not consume much memory. + * This should be safe as typically there should be at most 1 portal per network. */ +#define NDISC_CAPTIVE_PORTAL_MAX 64U +/* Neither defined in the RFC. Just for safety. Otherwise, malformed messages can make clients trigger OOM. + * Not sure if the threshold is high enough. Let's adjust later if not. */ +#define NDISC_PREF64_MAX 64U + +bool link_ipv6_accept_ra_enabled(Link *link) { + assert(link); + + if (!socket_ipv6_is_supported()) + return false; + + if (link->flags & IFF_LOOPBACK) + return false; + + if (link->iftype == ARPHRD_CAN) + return false; + + if (!link->network) + return false; + + if (!link_may_have_ipv6ll(link, /* check_multicast = */ true)) + return false; + + assert(link->network->ipv6_accept_ra >= 0); + return link->network->ipv6_accept_ra; +} + +void network_adjust_ipv6_accept_ra(Network *network) { + assert(network); + + if (!FLAGS_SET(network->link_local, ADDRESS_FAMILY_IPV6)) { + if (network->ipv6_accept_ra > 0) + log_warning("%s: IPv6AcceptRA= is enabled but IPv6 link-local addressing is disabled or not supported. " + "Disabling IPv6AcceptRA=.", network->filename); + network->ipv6_accept_ra = false; + } + + if (network->ipv6_accept_ra < 0) + /* default to accept RA if ip_forward is disabled and ignore RA if ip_forward is enabled */ + network->ipv6_accept_ra = !FLAGS_SET(network->ip_forward, ADDRESS_FAMILY_IPV6); + + /* When RouterAllowList=, PrefixAllowList= or RouteAllowList= are specified, then + * RouterDenyList=, PrefixDenyList= or RouteDenyList= are ignored, respectively. */ + if (!set_isempty(network->ndisc_allow_listed_router)) + network->ndisc_deny_listed_router = set_free_free(network->ndisc_deny_listed_router); + if (!set_isempty(network->ndisc_allow_listed_prefix)) + network->ndisc_deny_listed_prefix = set_free_free(network->ndisc_deny_listed_prefix); + if (!set_isempty(network->ndisc_allow_listed_route_prefix)) + network->ndisc_deny_listed_route_prefix = set_free_free(network->ndisc_deny_listed_route_prefix); +} + +static int ndisc_check_ready(Link *link); + +static int ndisc_address_ready_callback(Address *address) { + Address *a; + + assert(address); + assert(address->link); + + SET_FOREACH(a, address->link->addresses) + if (a->source == NETWORK_CONFIG_SOURCE_NDISC) + a->callback = NULL; + + return ndisc_check_ready(address->link); +} + +static int ndisc_check_ready(Link *link) { + bool found = false, ready = false; + Address *address; + + assert(link); + + if (link->ndisc_messages > 0) { + log_link_debug(link, "%s(): SLAAC addresses and routes are not set.", __func__); + return 0; + } + + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_NDISC) + continue; + + found = true; + + if (address_is_ready(address)) { + ready = true; + break; + } + } + + if (found && !ready) { + SET_FOREACH(address, link->addresses) + if (address->source == NETWORK_CONFIG_SOURCE_NDISC) + address->callback = ndisc_address_ready_callback; + + log_link_debug(link, "%s(): no SLAAC address is ready.", __func__); + return 0; + } + + link->ndisc_configured = true; + log_link_debug(link, "SLAAC addresses and routes set."); + + link_check_ready(link); + return 0; +} + +static int ndisc_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) { + int r; + + assert(link); + + r = route_configure_handler_internal(rtnl, m, link, "Could not set NDisc route"); + if (r <= 0) + return r; + + r = ndisc_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static void ndisc_set_route_priority(Link *link, Route *route) { + assert(link); + assert(route); + + if (route->priority_set) + return; /* explicitly configured. */ + + switch (route->pref) { + case SD_NDISC_PREFERENCE_LOW: + route->priority = link->network->ipv6_accept_ra_route_metric_low; + break; + case SD_NDISC_PREFERENCE_MEDIUM: + route->priority = link->network->ipv6_accept_ra_route_metric_medium; + break; + case SD_NDISC_PREFERENCE_HIGH: + route->priority = link->network->ipv6_accept_ra_route_metric_high; + break; + default: + assert_not_reached(); + } +} + +static int ndisc_request_route(Route *in, Link *link, sd_ndisc_router *rt) { + _cleanup_(route_freep) Route *route = in; + struct in6_addr router; + uint8_t hop_limit = 0; + uint32_t mtu = 0; + bool is_new; + int r; + + assert(route); + assert(link); + assert(link->network); + assert(rt); + + r = sd_ndisc_router_get_address(rt, &router); + if (r < 0) + return r; + + if (link->network->ipv6_accept_ra_use_mtu) { + r = sd_ndisc_router_get_mtu(rt, &mtu); + if (r < 0 && r != -ENODATA) + return log_link_warning_errno(link, r, "Failed to get default router MTU from RA: %m"); + } + + if (link->network->ipv6_accept_ra_use_hop_limit) { + r = sd_ndisc_router_get_hop_limit(rt, &hop_limit); + if (r < 0 && r != -ENODATA) + return log_link_warning_errno(link, r, "Failed to get default router hop limit from RA: %m"); + } + + route->source = NETWORK_CONFIG_SOURCE_NDISC; + route->provider.in6 = router; + if (!route->table_set) + route->table = link_get_ipv6_accept_ra_route_table(link); + ndisc_set_route_priority(link, route); + if (!route->protocol_set) + route->protocol = RTPROT_RA; + if (route->quickack < 0) + route->quickack = link->network->ipv6_accept_ra_quickack; + if (route->mtu == 0) + route->mtu = mtu; + if (route->hop_limit == 0) + route->hop_limit = hop_limit; + + is_new = route_get(NULL, link, route, NULL) < 0; + + r = link_request_route(link, TAKE_PTR(route), true, &link->ndisc_messages, + ndisc_route_handler, NULL); + if (r < 0) + return r; + if (r > 0 && is_new) + link->ndisc_configured = false; + + return 0; +} + +static int ndisc_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) { + int r; + + assert(link); + + r = address_configure_handler_internal(rtnl, m, link, "Could not set NDisc address"); + if (r <= 0) + return r; + + r = ndisc_check_ready(link); + if (r < 0) + link_enter_failed(link); + + return 1; +} + +static int ndisc_request_address(Address *in, Link *link, sd_ndisc_router *rt) { + _cleanup_(address_freep) Address *address = in; + struct in6_addr router; + bool is_new; + int r; + + assert(address); + assert(link); + assert(rt); + + r = sd_ndisc_router_get_address(rt, &router); + if (r < 0) + return r; + + address->source = NETWORK_CONFIG_SOURCE_NDISC; + address->provider.in6 = router; + + r = free_and_strdup_warn(&address->netlabel, link->network->ndisc_netlabel); + if (r < 0) + return r; + + is_new = address_get(link, address, NULL) < 0; + + r = link_request_address(link, address, &link->ndisc_messages, + ndisc_address_handler, NULL); + if (r < 0) + return r; + if (r > 0 && is_new) + link->ndisc_configured = false; + + return 0; +} + +static int ndisc_router_process_default(Link *link, sd_ndisc_router *rt) { + usec_t lifetime_usec; + struct in6_addr gateway; + unsigned preference; + int r; + + assert(link); + assert(link->network); + assert(rt); + + if (!link->network->ipv6_accept_ra_use_gateway && + hashmap_isempty(link->network->routes_by_section)) + return 0; + + r = sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get gateway lifetime from RA: %m"); + + r = sd_ndisc_router_get_address(rt, &gateway); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get gateway address from RA: %m"); + + if (link_get_ipv6_address(link, &gateway, 0, NULL) >= 0) { + if (DEBUG_LOGGING) + log_link_debug(link, "No NDisc route added, gateway %s matches local address", + IN6_ADDR_TO_STRING(&gateway)); + return 0; + } + + r = sd_ndisc_router_get_preference(rt, &preference); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get default router preference from RA: %m"); + + if (link->network->ipv6_accept_ra_use_gateway) { + _cleanup_(route_freep) Route *route = NULL; + + r = route_new(&route); + if (r < 0) + return log_oom(); + + route->family = AF_INET6; + route->pref = preference; + route->gw_family = AF_INET6; + route->gw.in6 = gateway; + route->lifetime_usec = lifetime_usec; + + r = ndisc_request_route(TAKE_PTR(route), link, rt); + if (r < 0) + return log_link_warning_errno(link, r, "Could not request default route: %m"); + } + + Route *route_gw; + HASHMAP_FOREACH(route_gw, link->network->routes_by_section) { + _cleanup_(route_freep) Route *route = NULL; + + if (!route_gw->gateway_from_dhcp_or_ra) + continue; + + if (route_gw->gw_family != AF_INET6) + continue; + + r = route_dup(route_gw, &route); + if (r < 0) + return r; + + route->gw.in6 = gateway; + if (!route->pref_set) + route->pref = preference; + route->lifetime_usec = lifetime_usec; + + r = ndisc_request_route(TAKE_PTR(route), link, rt); + if (r < 0) + return log_link_warning_errno(link, r, "Could not request gateway: %m"); + } + + return 0; +} + +static int ndisc_router_process_icmp6_ratelimit(Link *link, sd_ndisc_router *rt) { + usec_t icmp6_ratelimit, msec; + int r; + + assert(link); + assert(link->network); + assert(rt); + + if (!link->network->ipv6_accept_ra_use_icmp6_ratelimit) + return 0; + + r = sd_ndisc_router_get_icmp6_ratelimit(rt, &icmp6_ratelimit); + if (r < 0) { + log_link_debug(link, "Failed to get ICMP6 ratelimit from RA, ignoring: %m"); + return 0; + } + + /* We do not allow 0 here. */ + if (!timestamp_is_set(icmp6_ratelimit)) + return 0; + + msec = DIV_ROUND_UP(icmp6_ratelimit, USEC_PER_MSEC); + if (msec <= 0 || msec > INT_MAX) + return 0; + + /* Limit the maximal rates for sending ICMPv6 packets. 0 to disable any limiting, otherwise the + * minimal space between responses in milliseconds. Default: 1000. */ + r = sysctl_write_ip_property_int(AF_INET6, NULL, "icmp/ratelimit", (int) msec); + if (r < 0) + log_link_warning_errno(link, r, "Failed to apply ICMP6 ratelimit, ignoring: %m"); + + return 0; +} + +static int ndisc_router_process_autonomous_prefix(Link *link, sd_ndisc_router *rt) { + usec_t lifetime_valid_usec, lifetime_preferred_usec; + _cleanup_set_free_ Set *addresses = NULL; + struct in6_addr prefix, *a; + unsigned prefixlen; + int r; + + assert(link); + assert(link->network); + assert(rt); + + if (!link->network->ipv6_accept_ra_use_autonomous_prefix) + return 0; + + r = sd_ndisc_router_prefix_get_address(rt, &prefix); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix address: %m"); + + r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix length: %m"); + + /* ndisc_generate_addresses() below requires the prefix length <= 64. */ + if (prefixlen > 64) { + log_link_debug(link, "Prefix is longer than 64, ignoring autonomous prefix %s.", + IN6_ADDR_PREFIX_TO_STRING(&prefix, prefixlen)); + return 0; + } + + r = sd_ndisc_router_prefix_get_valid_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_valid_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix valid lifetime: %m"); + + r = sd_ndisc_router_prefix_get_preferred_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_preferred_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix preferred lifetime: %m"); + + /* The preferred lifetime is never greater than the valid lifetime */ + if (lifetime_preferred_usec > lifetime_valid_usec) + return 0; + + r = ndisc_generate_addresses(link, &prefix, prefixlen, &addresses); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to generate SLAAC addresses: %m"); + + SET_FOREACH(a, addresses) { + _cleanup_(address_freep) Address *address = NULL; + + r = address_new(&address); + if (r < 0) + return log_oom(); + + address->family = AF_INET6; + address->in_addr.in6 = *a; + address->prefixlen = prefixlen; + address->flags = IFA_F_NOPREFIXROUTE|IFA_F_MANAGETEMPADDR; + address->lifetime_valid_usec = lifetime_valid_usec; + address->lifetime_preferred_usec = lifetime_preferred_usec; + + r = ndisc_request_address(TAKE_PTR(address), link, rt); + if (r < 0) + return log_link_warning_errno(link, r, "Could not request SLAAC address: %m"); + } + + return 0; +} + +static int ndisc_router_process_onlink_prefix(Link *link, sd_ndisc_router *rt) { + _cleanup_(route_freep) Route *route = NULL; + unsigned prefixlen, preference; + usec_t lifetime_usec; + struct in6_addr prefix; + int r; + + assert(link); + assert(link->network); + assert(rt); + + if (!link->network->ipv6_accept_ra_use_onlink_prefix) + return 0; + + r = sd_ndisc_router_prefix_get_valid_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix lifetime: %m"); + + r = sd_ndisc_router_prefix_get_address(rt, &prefix); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix address: %m"); + + r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix length: %m"); + + /* Prefix Information option does not have preference, hence we use the 'main' preference here */ + r = sd_ndisc_router_get_preference(rt, &preference); + if (r < 0) + log_link_warning_errno(link, r, "Failed to get default router preference from RA: %m"); + + r = route_new(&route); + if (r < 0) + return log_oom(); + + route->family = AF_INET6; + route->dst.in6 = prefix; + route->dst_prefixlen = prefixlen; + route->pref = preference; + route->lifetime_usec = lifetime_usec; + + r = ndisc_request_route(TAKE_PTR(route), link, rt); + if (r < 0) + return log_link_warning_errno(link, r, "Could not request prefix route: %m"); + + return 0; +} + +static int ndisc_router_process_prefix(Link *link, sd_ndisc_router *rt) { + unsigned prefixlen; + struct in6_addr a; + uint8_t flags; + int r; + + assert(link); + assert(link->network); + assert(rt); + + r = sd_ndisc_router_prefix_get_address(rt, &a); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix address: %m"); + + /* RFC 4861 Section 4.6.2: + * A router SHOULD NOT send a prefix option for the link-local prefix and a host SHOULD ignore such + * a prefix option. */ + if (in6_addr_is_link_local(&a)) { + log_link_debug(link, "Received link-local prefix, ignoring autonomous prefix."); + return 0; + } + + r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get prefix length: %m"); + + if (in6_prefix_is_filtered(&a, prefixlen, link->network->ndisc_allow_listed_prefix, link->network->ndisc_deny_listed_prefix)) { + if (DEBUG_LOGGING) + log_link_debug(link, "Prefix '%s' is %s, ignoring", + !set_isempty(link->network->ndisc_allow_listed_prefix) ? "not in allow list" + : "in deny list", + IN6_ADDR_PREFIX_TO_STRING(&a, prefixlen)); + return 0; + } + + r = sd_ndisc_router_prefix_get_flags(rt, &flags); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get RA prefix flags: %m"); + + if (FLAGS_SET(flags, ND_OPT_PI_FLAG_ONLINK)) { + r = ndisc_router_process_onlink_prefix(link, rt); + if (r < 0) + return r; + } + + if (FLAGS_SET(flags, ND_OPT_PI_FLAG_AUTO)) { + r = ndisc_router_process_autonomous_prefix(link, rt); + if (r < 0) + return r; + } + + return 0; +} + +static int ndisc_router_process_route(Link *link, sd_ndisc_router *rt) { + _cleanup_(route_freep) Route *route = NULL; + unsigned preference, prefixlen; + struct in6_addr gateway, dst; + usec_t lifetime_usec; + int r; + + assert(link); + + if (!link->network->ipv6_accept_ra_use_route_prefix) + return 0; + + r = sd_ndisc_router_route_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get route lifetime from RA: %m"); + + r = sd_ndisc_router_route_get_address(rt, &dst); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get route destination address: %m"); + + r = sd_ndisc_router_route_get_prefixlen(rt, &prefixlen); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get route prefix length: %m"); + + if (in6_addr_is_null(&dst) && prefixlen == 0) { + log_link_debug(link, "Route prefix is ::/0, ignoring"); + return 0; + } + + if (in6_prefix_is_filtered(&dst, prefixlen, + link->network->ndisc_allow_listed_route_prefix, + link->network->ndisc_deny_listed_route_prefix)) { + + if (DEBUG_LOGGING) + log_link_debug(link, "Route prefix %s is %s, ignoring", + !set_isempty(link->network->ndisc_allow_listed_route_prefix) ? "not in allow list" + : "in deny list", + IN6_ADDR_PREFIX_TO_STRING(&dst, prefixlen)); + return 0; + } + + r = sd_ndisc_router_get_address(rt, &gateway); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get gateway address from RA: %m"); + + if (link_get_ipv6_address(link, &gateway, 0, NULL) >= 0) { + if (DEBUG_LOGGING) + log_link_debug(link, "Advertised route gateway %s is local to the link, ignoring route", + IN6_ADDR_TO_STRING(&gateway)); + return 0; + } + + r = sd_ndisc_router_route_get_preference(rt, &preference); + if (r == -ENOTSUP) { + log_link_debug_errno(link, r, "Received route prefix with unsupported preference, ignoring: %m"); + return 0; + } + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get default router preference from RA: %m"); + + r = route_new(&route); + if (r < 0) + return log_oom(); + + route->family = AF_INET6; + route->pref = preference; + route->gw.in6 = gateway; + route->gw_family = AF_INET6; + route->dst.in6 = dst; + route->dst_prefixlen = prefixlen; + route->lifetime_usec = lifetime_usec; + + r = ndisc_request_route(TAKE_PTR(route), link, rt); + if (r < 0) + return log_link_warning_errno(link, r, "Could not request additional route: %m"); + + return 0; +} + +static void ndisc_rdnss_hash_func(const NDiscRDNSS *x, struct siphash *state) { + siphash24_compress(&x->address, sizeof(x->address), state); +} + +static int ndisc_rdnss_compare_func(const NDiscRDNSS *a, const NDiscRDNSS *b) { + return memcmp(&a->address, &b->address, sizeof(a->address)); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + ndisc_rdnss_hash_ops, + NDiscRDNSS, + ndisc_rdnss_hash_func, + ndisc_rdnss_compare_func, + free); + +static int ndisc_router_process_rdnss(Link *link, sd_ndisc_router *rt) { + usec_t lifetime_usec; + const struct in6_addr *a; + struct in6_addr router; + bool updated = false, logged_about_too_many = false; + int n, r; + + assert(link); + assert(link->network); + assert(rt); + + if (!link->network->ipv6_accept_ra_use_dns) + return 0; + + r = sd_ndisc_router_get_address(rt, &router); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); + + r = sd_ndisc_router_rdnss_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get RDNSS lifetime: %m"); + + n = sd_ndisc_router_rdnss_get_addresses(rt, &a); + if (n < 0) + return log_link_warning_errno(link, n, "Failed to get RDNSS addresses: %m"); + + for (int j = 0; j < n; j++) { + _cleanup_free_ NDiscRDNSS *x = NULL; + NDiscRDNSS *rdnss, d = { + .address = a[j], + }; + + if (lifetime_usec == 0) { + /* The entry is outdated. */ + free(set_remove(link->ndisc_rdnss, &d)); + updated = true; + continue; + } + + rdnss = set_get(link->ndisc_rdnss, &d); + if (rdnss) { + rdnss->router = router; + rdnss->lifetime_usec = lifetime_usec; + continue; + } + + if (set_size(link->ndisc_rdnss) >= NDISC_RDNSS_MAX) { + if (!logged_about_too_many) + log_link_warning(link, "Too many RDNSS records per link. Only first %u records will be used.", NDISC_RDNSS_MAX); + logged_about_too_many = true; + continue; + } + + x = new(NDiscRDNSS, 1); + if (!x) + return log_oom(); + + *x = (NDiscRDNSS) { + .address = a[j], + .router = router, + .lifetime_usec = lifetime_usec, + }; + + r = set_ensure_consume(&link->ndisc_rdnss, &ndisc_rdnss_hash_ops, TAKE_PTR(x)); + if (r < 0) + return log_oom(); + assert(r > 0); + + updated = true; + } + + if (updated) + link_dirty(link); + + return 0; +} + +static void ndisc_dnssl_hash_func(const NDiscDNSSL *x, struct siphash *state) { + siphash24_compress_string(NDISC_DNSSL_DOMAIN(x), state); +} + +static int ndisc_dnssl_compare_func(const NDiscDNSSL *a, const NDiscDNSSL *b) { + return strcmp(NDISC_DNSSL_DOMAIN(a), NDISC_DNSSL_DOMAIN(b)); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + ndisc_dnssl_hash_ops, + NDiscDNSSL, + ndisc_dnssl_hash_func, + ndisc_dnssl_compare_func, + free); + +static int ndisc_router_process_dnssl(Link *link, sd_ndisc_router *rt) { + _cleanup_strv_free_ char **l = NULL; + usec_t lifetime_usec; + struct in6_addr router; + bool updated = false, logged_about_too_many = false; + int r; + + assert(link); + assert(link->network); + assert(rt); + + if (link->network->ipv6_accept_ra_use_domains == DHCP_USE_DOMAINS_NO) + return 0; + + r = sd_ndisc_router_get_address(rt, &router); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); + + r = sd_ndisc_router_dnssl_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get DNSSL lifetime: %m"); + + r = sd_ndisc_router_dnssl_get_domains(rt, &l); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get DNSSL addresses: %m"); + + STRV_FOREACH(j, l) { + _cleanup_free_ NDiscDNSSL *s = NULL; + NDiscDNSSL *dnssl; + + s = malloc0(ALIGN(sizeof(NDiscDNSSL)) + strlen(*j) + 1); + if (!s) + return log_oom(); + + strcpy(NDISC_DNSSL_DOMAIN(s), *j); + + if (lifetime_usec == 0) { + /* The entry is outdated. */ + free(set_remove(link->ndisc_dnssl, s)); + updated = true; + continue; + } + + dnssl = set_get(link->ndisc_dnssl, s); + if (dnssl) { + dnssl->router = router; + dnssl->lifetime_usec = lifetime_usec; + continue; + } + + if (set_size(link->ndisc_dnssl) >= NDISC_DNSSL_MAX) { + if (!logged_about_too_many) + log_link_warning(link, "Too many DNSSL records per link. Only first %u records will be used.", NDISC_DNSSL_MAX); + logged_about_too_many = true; + continue; + } + + s->router = router; + s->lifetime_usec = lifetime_usec; + + r = set_ensure_consume(&link->ndisc_dnssl, &ndisc_dnssl_hash_ops, TAKE_PTR(s)); + if (r < 0) + return log_oom(); + assert(r > 0); + + updated = true; + } + + if (updated) + link_dirty(link); + + return 0; +} + +static NDiscCaptivePortal* ndisc_captive_portal_free(NDiscCaptivePortal *x) { + if (!x) + return NULL; + + free(x->captive_portal); + return mfree(x); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(NDiscCaptivePortal*, ndisc_captive_portal_free); + +static void ndisc_captive_portal_hash_func(const NDiscCaptivePortal *x, struct siphash *state) { + assert(x); + siphash24_compress_string(x->captive_portal, state); +} + +static int ndisc_captive_portal_compare_func(const NDiscCaptivePortal *a, const NDiscCaptivePortal *b) { + assert(a); + assert(b); + return strcmp_ptr(a->captive_portal, b->captive_portal); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + ndisc_captive_portal_hash_ops, + NDiscCaptivePortal, + ndisc_captive_portal_hash_func, + ndisc_captive_portal_compare_func, + ndisc_captive_portal_free); + +static int ndisc_router_process_captive_portal(Link *link, sd_ndisc_router *rt) { + _cleanup_(ndisc_captive_portal_freep) NDiscCaptivePortal *new_entry = NULL; + _cleanup_free_ char *captive_portal = NULL; + usec_t lifetime_usec; + NDiscCaptivePortal *exist; + struct in6_addr router; + const char *uri; + size_t len; + int r; + + assert(link); + assert(link->network); + assert(rt); + + if (!link->network->ipv6_accept_ra_use_captive_portal) + return 0; + + r = sd_ndisc_router_get_address(rt, &router); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); + + /* RFC 4861 section 4.2. states that the lifetime in the message header should be used only for the + * default gateway, but the captive portal option does not have a lifetime field, hence, we use the + * main lifetime for the portal. */ + r = sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get lifetime of RA message: %m"); + + r = sd_ndisc_router_captive_portal_get_uri(rt, &uri, &len); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get captive portal from RA: %m"); + + if (len == 0) + return log_link_warning_errno(link, SYNTHETIC_ERRNO(EBADMSG), "Received empty captive portal, ignoring."); + + r = make_cstring(uri, len, MAKE_CSTRING_REFUSE_TRAILING_NUL, &captive_portal); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to convert captive portal URI: %m"); + + if (!in_charset(captive_portal, URI_VALID)) + return log_link_warning_errno(link, SYNTHETIC_ERRNO(EBADMSG), "Received invalid captive portal, ignoring."); + + if (lifetime_usec == 0) { + /* Drop the portal with zero lifetime. */ + ndisc_captive_portal_free(set_remove(link->ndisc_captive_portals, + &(NDiscCaptivePortal) { + .captive_portal = captive_portal, + })); + return 0; + } + + exist = set_get(link->ndisc_captive_portals, + &(NDiscCaptivePortal) { + .captive_portal = captive_portal, + }); + if (exist) { + /* update existing entry */ + exist->router = router; + exist->lifetime_usec = lifetime_usec; + return 1; + } + + if (set_size(link->ndisc_captive_portals) >= NDISC_CAPTIVE_PORTAL_MAX) { + NDiscCaptivePortal *c, *target = NULL; + + /* Find the portal who has the minimal lifetime and drop it to store new one. */ + SET_FOREACH(c, link->ndisc_captive_portals) + if (!target || c->lifetime_usec < target->lifetime_usec) + target = c; + + assert(target); + assert(set_remove(link->ndisc_captive_portals, target) == target); + ndisc_captive_portal_free(target); + } + + new_entry = new(NDiscCaptivePortal, 1); + if (!new_entry) + return log_oom(); + + *new_entry = (NDiscCaptivePortal) { + .router = router, + .lifetime_usec = lifetime_usec, + .captive_portal = TAKE_PTR(captive_portal), + }; + + r = set_ensure_put(&link->ndisc_captive_portals, &ndisc_captive_portal_hash_ops, new_entry); + if (r < 0) + return log_oom(); + assert(r > 0); + TAKE_PTR(new_entry); + + link_dirty(link); + return 1; +} + +static void ndisc_pref64_hash_func(const NDiscPREF64 *x, struct siphash *state) { + assert(x); + + siphash24_compress(&x->prefix_len, sizeof(x->prefix_len), state); + siphash24_compress(&x->prefix, sizeof(x->prefix), state); +} + +static int ndisc_pref64_compare_func(const NDiscPREF64 *a, const NDiscPREF64 *b) { + int r; + + assert(a); + assert(b); + + r = CMP(a->prefix_len, b->prefix_len); + if (r != 0) + return r; + + return memcmp(&a->prefix, &b->prefix, sizeof(a->prefix)); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + ndisc_pref64_hash_ops, + NDiscPREF64, + ndisc_pref64_hash_func, + ndisc_pref64_compare_func, + mfree); + +static int ndisc_router_process_pref64(Link *link, sd_ndisc_router *rt) { + _cleanup_free_ NDiscPREF64 *new_entry = NULL; + usec_t lifetime_usec; + struct in6_addr a, router; + unsigned prefix_len; + NDiscPREF64 *exist; + int r; + + assert(link); + assert(link->network); + assert(rt); + + if (!link->network->ipv6_accept_ra_use_pref64) + return 0; + + r = sd_ndisc_router_get_address(rt, &router); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); + + r = sd_ndisc_router_prefix64_get_prefix(rt, &a); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get pref64 prefix: %m"); + + r = sd_ndisc_router_prefix64_get_prefixlen(rt, &prefix_len); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get pref64 prefix length: %m"); + + r = sd_ndisc_router_prefix64_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get pref64 prefix lifetime: %m"); + + if (lifetime_usec == 0) { + free(set_remove(link->ndisc_pref64, + &(NDiscPREF64) { + .prefix = a, + .prefix_len = prefix_len + })); + return 0; + } + + exist = set_get(link->ndisc_pref64, + &(NDiscPREF64) { + .prefix = a, + .prefix_len = prefix_len + }); + if (exist) { + /* update existing entry */ + exist->router = router; + exist->lifetime_usec = lifetime_usec; + return 0; + } + + if (set_size(link->ndisc_pref64) >= NDISC_PREF64_MAX) { + log_link_debug(link, "Too many PREF64 records received. Only first %u records will be used.", NDISC_PREF64_MAX); + return 0; + } + + new_entry = new(NDiscPREF64, 1); + if (!new_entry) + return log_oom(); + + *new_entry = (NDiscPREF64) { + .router = router, + .lifetime_usec = lifetime_usec, + .prefix = a, + .prefix_len = prefix_len, + }; + + r = set_ensure_put(&link->ndisc_pref64, &ndisc_pref64_hash_ops, new_entry); + if (r < 0) + return log_oom(); + + assert(r > 0); + TAKE_PTR(new_entry); + + return 0; +} + +static int ndisc_router_process_options(Link *link, sd_ndisc_router *rt) { + size_t n_captive_portal = 0; + int r; + + assert(link); + assert(link->network); + assert(rt); + + for (r = sd_ndisc_router_option_rewind(rt); ; r = sd_ndisc_router_option_next(rt)) { + uint8_t type; + + if (r < 0) + return log_link_warning_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_warning_errno(link, r, "Failed to get RA option type: %m"); + + switch (type) { + case SD_NDISC_OPTION_PREFIX_INFORMATION: + r = ndisc_router_process_prefix(link, rt); + break; + + case SD_NDISC_OPTION_ROUTE_INFORMATION: + r = ndisc_router_process_route(link, rt); + break; + + case SD_NDISC_OPTION_RDNSS: + r = ndisc_router_process_rdnss(link, rt); + break; + + case SD_NDISC_OPTION_DNSSL: + r = ndisc_router_process_dnssl(link, rt); + break; + case SD_NDISC_OPTION_CAPTIVE_PORTAL: + if (n_captive_portal > 0) { + if (n_captive_portal == 1) + log_link_notice(link, "Received RA with multiple captive portals, only using the first one."); + + n_captive_portal++; + continue; + } + r = ndisc_router_process_captive_portal(link, rt); + if (r > 0) + n_captive_portal++; + break; + case SD_NDISC_OPTION_PREF64: + r = ndisc_router_process_pref64(link, rt); + break; + } + if (r < 0 && r != -EBADMSG) + return r; + } +} + +static int ndisc_drop_outdated(Link *link, usec_t timestamp_usec) { + bool updated = false; + NDiscDNSSL *dnssl; + NDiscRDNSS *rdnss; + NDiscCaptivePortal *cp; + NDiscPREF64 *p64; + Address *address; + Route *route; + int r = 0, k; + + assert(link); + + /* If an address or friends is already assigned, but not valid anymore, then refuse to update it, + * and let's immediately remove it. + * See RFC4862, section 5.5.3.e. But the following logic is deviated from RFC4862 by honoring all + * valid lifetimes to improve the reaction of SLAAC to renumbering events. + * See draft-ietf-6man-slaac-renum-02, section 4.2. */ + + SET_FOREACH(route, link->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_NDISC) + continue; + + if (route->lifetime_usec >= timestamp_usec) + continue; /* the route is still valid */ + + k = route_remove_and_drop(route); + if (k < 0) + r = log_link_warning_errno(link, k, "Failed to remove outdated SLAAC route, ignoring: %m"); + } + + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_NDISC) + continue; + + if (address->lifetime_valid_usec >= timestamp_usec) + continue; /* the address is still valid */ + + k = address_remove_and_drop(address); + if (k < 0) + r = log_link_warning_errno(link, k, "Failed to remove outdated SLAAC address, ignoring: %m"); + } + + SET_FOREACH(rdnss, link->ndisc_rdnss) { + if (rdnss->lifetime_usec >= timestamp_usec) + continue; /* the DNS server is still valid */ + + free(set_remove(link->ndisc_rdnss, rdnss)); + updated = true; + } + + SET_FOREACH(dnssl, link->ndisc_dnssl) { + if (dnssl->lifetime_usec >= timestamp_usec) + continue; /* the DNS domain is still valid */ + + free(set_remove(link->ndisc_dnssl, dnssl)); + updated = true; + } + + SET_FOREACH(cp, link->ndisc_captive_portals) { + if (cp->lifetime_usec >= timestamp_usec) + continue; /* the captive portal is still valid */ + + ndisc_captive_portal_free(set_remove(link->ndisc_captive_portals, cp)); + updated = true; + } + + SET_FOREACH(p64, link->ndisc_pref64) { + if (p64->lifetime_usec >= timestamp_usec) + continue; /* the pref64 prefix is still valid */ + + free(set_remove(link->ndisc_pref64, p64)); + /* The pref64 prefix is not exported through the state file, hence it is not necessary to set + * the 'updated' flag. */ + } + + if (updated) + link_dirty(link); + + return r; +} + +static int ndisc_setup_expire(Link *link); + +static int ndisc_expire_handler(sd_event_source *s, uint64_t usec, void *userdata) { + Link *link = ASSERT_PTR(userdata); + usec_t now_usec; + + assert(link->manager); + + assert_se(sd_event_now(link->manager->event, CLOCK_BOOTTIME, &now_usec) >= 0); + + (void) ndisc_drop_outdated(link, now_usec); + (void) ndisc_setup_expire(link); + return 0; +} + +static int ndisc_setup_expire(Link *link) { + usec_t lifetime_usec = USEC_INFINITY; + NDiscCaptivePortal *cp; + NDiscDNSSL *dnssl; + NDiscRDNSS *rdnss; + NDiscPREF64 *p64; + Address *address; + Route *route; + int r; + + assert(link); + assert(link->manager); + + SET_FOREACH(route, link->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_NDISC) + continue; + + if (!route_exists(route)) + continue; + + lifetime_usec = MIN(lifetime_usec, route->lifetime_usec); + } + + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_NDISC) + continue; + + if (!address_exists(address)) + continue; + + lifetime_usec = MIN(lifetime_usec, address->lifetime_valid_usec); + } + + SET_FOREACH(rdnss, link->ndisc_rdnss) + lifetime_usec = MIN(lifetime_usec, rdnss->lifetime_usec); + + SET_FOREACH(dnssl, link->ndisc_dnssl) + lifetime_usec = MIN(lifetime_usec, dnssl->lifetime_usec); + + SET_FOREACH(cp, link->ndisc_captive_portals) + lifetime_usec = MIN(lifetime_usec, cp->lifetime_usec); + + SET_FOREACH(p64, link->ndisc_pref64) + lifetime_usec = MIN(lifetime_usec, p64->lifetime_usec); + + if (lifetime_usec == USEC_INFINITY) + return 0; + + r = event_reset_time(link->manager->event, &link->ndisc_expire, CLOCK_BOOTTIME, + lifetime_usec, 0, ndisc_expire_handler, link, 0, "ndisc-expiration", true); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to update expiration timer for ndisc: %m"); + + return 0; +} + +static int ndisc_start_dhcp6_client(Link *link, sd_ndisc_router *rt) { + int r; + + assert(link); + assert(link->network); + + switch (link->network->ipv6_accept_ra_start_dhcp6_client) { + case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO: + return 0; + + case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES: { + uint64_t flags; + + r = sd_ndisc_router_get_flags(rt, &flags); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get RA flags: %m"); + + if ((flags & (ND_RA_FLAG_MANAGED | ND_RA_FLAG_OTHER)) == 0) + return 0; + + /* (re)start DHCPv6 client in stateful or stateless mode according to RA flags. + * Note, if both "managed" and "other configuration" bits are set, then ignore + * "other configuration" bit. See RFC 4861. */ + r = dhcp6_start_on_ra(link, !(flags & ND_RA_FLAG_MANAGED)); + break; + } + case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS: + /* When IPv6AcceptRA.DHCPv6Client=always, start dhcp6 client in solicit mode + * even if the router flags have neither M nor O flags. */ + r = dhcp6_start_on_ra(link, /* information_request = */ false); + break; + + default: + assert_not_reached(); + } + + if (r < 0) + return log_link_warning_errno(link, r, "Could not acquire DHCPv6 lease on NDisc request: %m"); + + log_link_debug(link, "Acquiring DHCPv6 lease on NDisc request"); + return 0; +} + +static int ndisc_router_handler(Link *link, sd_ndisc_router *rt) { + struct in6_addr router; + usec_t timestamp_usec; + int r; + + assert(link); + assert(link->network); + assert(link->manager); + assert(rt); + + r = sd_ndisc_router_get_address(rt, &router); + if (r == -ENODATA) { + log_link_debug(link, "Received RA without router address, ignoring."); + return 0; + } + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); + + if (in6_prefix_is_filtered(&router, 128, link->network->ndisc_allow_listed_router, link->network->ndisc_deny_listed_router)) { + if (DEBUG_LOGGING) { + if (!set_isempty(link->network->ndisc_allow_listed_router)) + log_link_debug(link, "Router %s is not in allow list, ignoring.", IN6_ADDR_TO_STRING(&router)); + else + log_link_debug(link, "Router %s is in deny list, ignoring.", IN6_ADDR_TO_STRING(&router)); + } + return 0; + } + + r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, ×tamp_usec); + if (r == -ENODATA) { + log_link_debug(link, "Received RA without timestamp, ignoring."); + return 0; + } + if (r < 0) + return r; + + r = ndisc_drop_outdated(link, timestamp_usec); + if (r < 0) + return r; + + r = ndisc_start_dhcp6_client(link, rt); + if (r < 0) + return r; + + r = ndisc_router_process_default(link, rt); + if (r < 0) + return r; + + r = ndisc_router_process_icmp6_ratelimit(link, rt); + if (r < 0) + return r; + + r = ndisc_router_process_options(link, rt); + if (r < 0) + return r; + + r = ndisc_setup_expire(link); + if (r < 0) + return r; + + if (link->ndisc_messages == 0) + link->ndisc_configured = true; + else + log_link_debug(link, "Setting SLAAC addresses and router."); + + if (!link->ndisc_configured) + link_set_state(link, LINK_STATE_CONFIGURING); + + link_check_ready(link); + return 0; +} + +static void ndisc_handler(sd_ndisc *nd, sd_ndisc_event_t event, sd_ndisc_router *rt, void *userdata) { + Link *link = ASSERT_PTR(userdata); + int r; + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return; + + switch (event) { + + case SD_NDISC_EVENT_ROUTER: + r = ndisc_router_handler(link, rt); + if (r < 0 && r != -EBADMSG) { + link_enter_failed(link); + return; + } + break; + + case SD_NDISC_EVENT_TIMEOUT: + log_link_debug(link, "NDisc handler get timeout event"); + if (link->ndisc_messages == 0) { + link->ndisc_configured = true; + link_check_ready(link); + } + break; + default: + assert_not_reached(); + } +} + +static int ndisc_configure(Link *link) { + int r; + + assert(link); + + if (!link_ipv6_accept_ra_enabled(link)) + return 0; + + if (link->ndisc) + return -EBUSY; /* Already configured. */ + + r = sd_ndisc_new(&link->ndisc); + if (r < 0) + return r; + + r = sd_ndisc_attach_event(link->ndisc, link->manager->event, 0); + if (r < 0) + return r; + + if (link->hw_addr.length == ETH_ALEN) { + r = sd_ndisc_set_mac(link->ndisc, &link->hw_addr.ether); + if (r < 0) + return r; + } + + r = sd_ndisc_set_ifindex(link->ndisc, link->ifindex); + if (r < 0) + return r; + + r = sd_ndisc_set_callback(link->ndisc, ndisc_handler, link); + if (r < 0) + return r; + + return 0; +} + +int ndisc_start(Link *link) { + int r; + + assert(link); + + if (!link->ndisc || !link->dhcp6_client) + return 0; + + if (!link_has_carrier(link)) + return 0; + + if (in6_addr_is_null(&link->ipv6ll_address)) + return 0; + + log_link_debug(link, "Discovering IPv6 routers"); + + r = sd_ndisc_start(link->ndisc); + if (r < 0) + return r; + + return 1; +} + +static int ndisc_process_request(Request *req, Link *link, void *userdata) { + int r; + + assert(link); + + if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) + return 0; + + r = ndisc_configure(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure IPv6 Router Discovery: %m"); + + r = ndisc_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m"); + + log_link_debug(link, "IPv6 Router Discovery is configured%s.", + r > 0 ? " and started" : ""); + return 1; +} + +int link_request_ndisc(Link *link) { + int r; + + assert(link); + + if (!link_ipv6_accept_ra_enabled(link)) + return 0; + + if (link->ndisc) + return 0; + + r = link_queue_request(link, REQUEST_TYPE_NDISC, ndisc_process_request, NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request configuring of the IPv6 Router Discovery: %m"); + + log_link_debug(link, "Requested configuring of the IPv6 Router Discovery."); + return 0; +} + +int ndisc_stop(Link *link) { + assert(link); + + link->ndisc_expire = sd_event_source_disable_unref(link->ndisc_expire); + + return sd_ndisc_stop(link->ndisc); +} + + +void ndisc_flush(Link *link) { + assert(link); + + /* Remove all RDNSS, DNSSL, and Captive Portal entries, without exception. */ + + link->ndisc_rdnss = set_free(link->ndisc_rdnss); + link->ndisc_dnssl = set_free(link->ndisc_dnssl); + link->ndisc_captive_portals = set_free(link->ndisc_captive_portals); + link->ndisc_pref64 = set_free(link->ndisc_pref64); +} + +static const char* const ipv6_accept_ra_start_dhcp6_client_table[_IPV6_ACCEPT_RA_START_DHCP6_CLIENT_MAX] = { + [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO] = "no", + [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS] = "always", + [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES] = "yes", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(ipv6_accept_ra_start_dhcp6_client, IPv6AcceptRAStartDHCP6Client, IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES); + +DEFINE_CONFIG_PARSE_ENUM(config_parse_ipv6_accept_ra_use_domains, dhcp_use_domains, DHCPUseDomains, + "Failed to parse UseDomains= setting"); +DEFINE_CONFIG_PARSE_ENUM(config_parse_ipv6_accept_ra_start_dhcp6_client, ipv6_accept_ra_start_dhcp6_client, IPv6AcceptRAStartDHCP6Client, + "Failed to parse DHCPv6Client= setting"); diff --git a/src/network/networkd-ndisc.h b/src/network/networkd-ndisc.h new file mode 100644 index 0000000..a463f42 --- /dev/null +++ b/src/network/networkd-ndisc.h @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" +#include "time-util.h" + +typedef struct Link Link; +typedef struct Network Network; + +typedef enum IPv6AcceptRAStartDHCP6Client { + IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO, + IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS, + IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES, + _IPV6_ACCEPT_RA_START_DHCP6_CLIENT_MAX, + _IPV6_ACCEPT_RA_START_DHCP6_CLIENT_INVALID = -EINVAL, +} IPv6AcceptRAStartDHCP6Client; + +typedef struct NDiscRDNSS { + struct in6_addr router; + /* This is an absolute point in time, and NOT a timespan/duration. + * Must be specified with CLOCK_BOOTTIME. */ + usec_t lifetime_usec; + struct in6_addr address; +} NDiscRDNSS; + +typedef struct NDiscDNSSL { + struct in6_addr router; + /* This is an absolute point in time, and NOT a timespan/duration. + * Must be specified with CLOCK_BOOTTIME. */ + usec_t lifetime_usec; + /* The domain name follows immediately. */ +} NDiscDNSSL; + +typedef struct NDiscCaptivePortal { + struct in6_addr router; + /* This is an absolute point in time, and NOT a timespan/duration. + * Must be specified with CLOCK_BOOTTIME. */ + usec_t lifetime_usec; + char *captive_portal; +} NDiscCaptivePortal; + +typedef struct NDiscPREF64 { + struct in6_addr router; + /* This is an absolute point in time, and NOT a timespan/duration. + * Must be specified with CLOCK_BOOTTIME. */ + usec_t lifetime_usec; + uint8_t prefix_len; + struct in6_addr prefix; +} NDiscPREF64; + +static inline char* NDISC_DNSSL_DOMAIN(const NDiscDNSSL *n) { + return ((char*) n) + ALIGN(sizeof(NDiscDNSSL)); +} + +bool link_ipv6_accept_ra_enabled(Link *link); + +void network_adjust_ipv6_accept_ra(Network *network); + +int ndisc_start(Link *link); +int ndisc_stop(Link *link); +void ndisc_flush(Link *link); + +int link_request_ndisc(Link *link); + +CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_accept_ra_start_dhcp6_client); +CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_accept_ra_use_domains); diff --git a/src/network/networkd-neighbor.c b/src/network/networkd-neighbor.c new file mode 100644 index 0000000..8321831 --- /dev/null +++ b/src/network/networkd-neighbor.c @@ -0,0 +1,756 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "hashmap.h" +#include "netlink-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-neighbor.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "set.h" + +Neighbor *neighbor_free(Neighbor *neighbor) { + if (!neighbor) + return NULL; + + if (neighbor->network) { + assert(neighbor->section); + ordered_hashmap_remove(neighbor->network->neighbors_by_section, neighbor->section); + } + + config_section_free(neighbor->section); + + if (neighbor->link) + set_remove(neighbor->link->neighbors, neighbor); + + return mfree(neighbor); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(Neighbor, neighbor_free); + +static int neighbor_new_static(Network *network, const char *filename, unsigned section_line, Neighbor **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(neighbor_freep) Neighbor *neighbor = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + neighbor = ordered_hashmap_get(network->neighbors_by_section, n); + if (neighbor) { + *ret = TAKE_PTR(neighbor); + return 0; + } + + neighbor = new(Neighbor, 1); + if (!neighbor) + return -ENOMEM; + + *neighbor = (Neighbor) { + .network = network, + .family = AF_UNSPEC, + .section = TAKE_PTR(n), + .source = NETWORK_CONFIG_SOURCE_STATIC, + }; + + r = ordered_hashmap_ensure_put(&network->neighbors_by_section, &config_section_hash_ops, neighbor->section, neighbor); + if (r < 0) + return r; + + *ret = TAKE_PTR(neighbor); + return 0; +} + +static int neighbor_dup(const Neighbor *neighbor, Neighbor **ret) { + _cleanup_(neighbor_freep) Neighbor *dest = NULL; + + assert(neighbor); + assert(ret); + + dest = newdup(Neighbor, neighbor, 1); + if (!dest) + return -ENOMEM; + + /* Unset all pointers */ + dest->link = NULL; + dest->network = NULL; + dest->section = NULL; + + *ret = TAKE_PTR(dest); + return 0; +} + +static void neighbor_hash_func(const Neighbor *neighbor, struct siphash *state) { + assert(neighbor); + + siphash24_compress(&neighbor->family, sizeof(neighbor->family), state); + + if (!IN_SET(neighbor->family, AF_INET, AF_INET6)) + /* treat any other address family as AF_UNSPEC */ + return; + + /* Equality of neighbors are given by the destination address. + * See neigh_lookup() in the kernel. */ + siphash24_compress(&neighbor->in_addr, FAMILY_ADDRESS_SIZE(neighbor->family), 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; + + if (!IN_SET(a->family, AF_INET, AF_INET6)) + /* treat any other address family as AF_UNSPEC */ + return 0; + + return memcmp(&a->in_addr, &b->in_addr, FAMILY_ADDRESS_SIZE(a->family)); +} + +DEFINE_PRIVATE_HASH_OPS( + neighbor_hash_ops, + Neighbor, + neighbor_hash_func, + neighbor_compare_func); + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + neighbor_hash_ops_free, + Neighbor, + neighbor_hash_func, + neighbor_compare_func, + neighbor_free); + +static int neighbor_get_request(Link *link, const Neighbor *neighbor, Request **ret) { + Request *req; + + assert(link); + assert(link->manager); + assert(neighbor); + + req = ordered_set_get( + link->manager->request_queue, + &(Request) { + .link = link, + .type = REQUEST_TYPE_NEIGHBOR, + .userdata = (void*) neighbor, + .hash_func = (hash_func_t) neighbor_hash_func, + .compare_func = (compare_func_t) neighbor_compare_func, + }); + if (!req) + return -ENOENT; + + if (ret) + *ret = req; + return 0; +} + +static int neighbor_get(Link *link, const Neighbor *in, Neighbor **ret) { + Neighbor *existing; + + assert(link); + assert(in); + + existing = set_get(link->neighbors, in); + if (!existing) + return -ENOENT; + + if (ret) + *ret = existing; + return 0; +} + +static int neighbor_add(Link *link, Neighbor *neighbor) { + int r; + + assert(link); + assert(neighbor); + + r = set_ensure_put(&link->neighbors, &neighbor_hash_ops_free, neighbor); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + neighbor->link = link; + return 0; +} + +static void log_neighbor_debug(const Neighbor *neighbor, const char *str, const Link *link) { + _cleanup_free_ char *state = NULL; + + assert(neighbor); + assert(str); + + if (!DEBUG_LOGGING) + return; + + (void) network_config_state_to_string_alloc(neighbor->state, &state); + + log_link_debug(link, + "%s %s neighbor (%s): lladdr: %s, dst: %s", + str, strna(network_config_source_to_string(neighbor->source)), strna(state), + HW_ADDR_TO_STR(&neighbor->ll_addr), + IN_ADDR_TO_STRING(neighbor->family, &neighbor->in_addr)); +} + +static int neighbor_configure(Neighbor *neighbor, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(neighbor); + assert(link); + assert(link->ifindex > 0); + assert(link->manager); + assert(link->manager->rtnl); + assert(req); + + log_neighbor_debug(neighbor, "Configuring", link); + + r = sd_rtnl_message_new_neigh(link->manager->rtnl, &m, RTM_NEWNEIGH, + link->ifindex, neighbor->family); + if (r < 0) + return r; + + r = sd_rtnl_message_neigh_set_state(m, NUD_PERMANENT); + if (r < 0) + return r; + + r = netlink_message_append_hw_addr(m, NDA_LLADDR, &neighbor->ll_addr); + if (r < 0) + return r; + + r = netlink_message_append_in_addr_union(m, NDA_DST, neighbor->family, &neighbor->in_addr); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static int neighbor_process_request(Request *req, Link *link, Neighbor *neighbor) { + Neighbor *existing; + int r; + + assert(req); + assert(link); + assert(neighbor); + + if (!link_is_ready_to_configure(link, false)) + return 0; + + r = neighbor_configure(neighbor, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure neighbor: %m"); + + neighbor_enter_configuring(neighbor); + if (neighbor_get(link, neighbor, &existing) >= 0) + neighbor_enter_configuring(existing); + + return 1; +} + +static int static_neighbor_configure_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Neighbor *neighbor) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not set neighbor"); + link_enter_failed(link); + return 1; + } + + if (link->static_neighbor_messages == 0) { + log_link_debug(link, "Neighbors set"); + link->static_neighbors_configured = true; + link_check_ready(link); + } + + return 1; +} + +static int link_request_neighbor(Link *link, const Neighbor *neighbor) { + _cleanup_(neighbor_freep) Neighbor *tmp = NULL; + Neighbor *existing = NULL; + int r; + + assert(link); + assert(neighbor); + assert(neighbor->source != NETWORK_CONFIG_SOURCE_FOREIGN); + + if (neighbor->ll_addr.length != link->hw_addr.length) { + log_link_debug(link, + "The link layer address length (%zu) for neighbor %s does not match with " + "the hardware address length (%zu), ignoring the setting.", + neighbor->ll_addr.length, + IN_ADDR_TO_STRING(neighbor->family, &neighbor->in_addr), + link->hw_addr.length); + return 0; + } + + r = neighbor_dup(neighbor, &tmp); + if (r < 0) + return r; + + if (neighbor_get(link, neighbor, &existing) >= 0) + /* Copy state for logging below. */ + tmp->state = existing->state; + + log_neighbor_debug(tmp, "Requesting", link); + r = link_queue_request_safe(link, REQUEST_TYPE_NEIGHBOR, + tmp, + neighbor_free, + neighbor_hash_func, + neighbor_compare_func, + neighbor_process_request, + &link->static_neighbor_messages, + static_neighbor_configure_handler, + NULL); + if (r <= 0) + return r; + + neighbor_enter_requesting(tmp); + if (existing) + neighbor_enter_requesting(existing); + + TAKE_PTR(tmp); + return 1; +} + +int link_request_static_neighbors(Link *link) { + Neighbor *neighbor; + int r; + + assert(link); + assert(link->network); + assert(link->state != _LINK_STATE_INVALID); + + link->static_neighbors_configured = false; + + ORDERED_HASHMAP_FOREACH(neighbor, link->network->neighbors_by_section) { + r = link_request_neighbor(link, neighbor); + if (r < 0) + return log_link_warning_errno(link, r, "Could not request neighbor: %m"); + } + + if (link->static_neighbor_messages == 0) { + link->static_neighbors_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Requesting neighbors"); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +static int neighbor_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert(m); + assert(link); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 1; + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -ESRCH) + /* Neighbor may not exist because it already got deleted, ignore that. */ + log_link_message_warning_errno(link, m, r, "Could not remove neighbor"); + + return 1; +} + +static int neighbor_remove(Neighbor *neighbor) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + Request *req; + Link *link; + int r; + + assert(neighbor); + assert(neighbor->link); + assert(neighbor->link->manager); + assert(neighbor->link->manager->rtnl); + + link = neighbor->link; + + log_neighbor_debug(neighbor, "Removing", link); + + r = sd_rtnl_message_new_neigh(link->manager->rtnl, &m, 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(m, 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, m, neighbor_remove_handler, + link_netlink_destroy_callback, link); + if (r < 0) + return log_link_error_errno(link, r, "Could not send rtnetlink message: %m"); + + link_ref(link); + + neighbor_enter_removing(neighbor); + if (neighbor_get_request(neighbor->link, neighbor, &req) >= 0) + neighbor_enter_removing(req->userdata); + + return 0; +} + +int link_drop_foreign_neighbors(Link *link) { + Neighbor *neighbor; + int r = 0; + + assert(link); + assert(link->network); + + /* First, mark all neighbors. */ + SET_FOREACH(neighbor, link->neighbors) { + /* Do not remove neighbors we configured. */ + if (neighbor->source != NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + /* Ignore neighbors not assigned yet or already removing. */ + if (!neighbor_exists(neighbor)) + continue; + + neighbor_mark(neighbor); + } + + /* Next, unmark requested neighbors. They will be configured later. */ + ORDERED_HASHMAP_FOREACH(neighbor, link->network->neighbors_by_section) { + Neighbor *existing; + + if (neighbor_get(link, neighbor, &existing) >= 0) + neighbor_unmark(existing); + } + + SET_FOREACH(neighbor, link->neighbors) { + if (!neighbor_is_marked(neighbor)) + continue; + + RET_GATHER(r, neighbor_remove(neighbor)); + } + + return r; +} + +int link_drop_managed_neighbors(Link *link) { + Neighbor *neighbor; + int r = 0; + + assert(link); + + SET_FOREACH(neighbor, link->neighbors) { + /* Do not touch nexthops managed by kernel or other tools. */ + if (neighbor->source == NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + /* Ignore neighbors not assigned yet or already removing. */ + if (!neighbor_exists(neighbor)) + continue; + + RET_GATHER(r, neighbor_remove(neighbor)); + } + + return r; +} + +void link_foreignize_neighbors(Link *link) { + Neighbor *neighbor; + + assert(link); + + SET_FOREACH(neighbor, link->neighbors) + neighbor->source = NETWORK_CONFIG_SOURCE_FOREIGN; +} + +int manager_rtnl_process_neighbor(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) { + _cleanup_(neighbor_freep) Neighbor *tmp = NULL; + Neighbor *neighbor = NULL; + Request *req = NULL; + uint16_t type, state; + bool is_new = false; + int ifindex, r; + Link *link; + + assert(rtnl); + assert(message); + assert(m); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "rtnl: failed to receive neighbor message, ignoring"); + + return 0; + } + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get message type, ignoring: %m"); + return 0; + } else if (!IN_SET(type, RTM_NEWNEIGH, RTM_DELNEIGH)) { + log_warning("rtnl: received unexpected message type %u when processing neighbor, ignoring.", type); + return 0; + } + + r = sd_rtnl_message_neigh_get_state(message, &state); + if (r < 0) { + log_warning_errno(r, "rtnl: received neighbor message with invalid state, ignoring: %m"); + return 0; + } else if (!FLAGS_SET(state, NUD_PERMANENT)) { + log_debug("rtnl: received non-static neighbor, ignoring."); + return 0; + } + + r = sd_rtnl_message_neigh_get_ifindex(message, &ifindex); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m"); + return 0; + } else if (ifindex <= 0) { + log_warning("rtnl: received neighbor message with invalid ifindex %d, ignoring.", ifindex); + return 0; + } + + r = link_get_by_index(m, ifindex, &link); + if (r < 0) { + /* when enumerating we might be out of sync, but we will get the neighbor again. Also, + * kernel sends messages about neighbors after a link is removed. So, just ignore it. */ + log_debug("rtnl: received neighbor for link '%d' we don't know about, ignoring.", ifindex); + return 0; + } + + tmp = new0(Neighbor, 1); + if (!tmp) + return log_oom(); + + /* First, retrieve the fundamental information about the neighbor. */ + 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; + } + + /* Then, find the managed Neighbor and Request objects corresponding to the netlink notification. */ + (void) neighbor_get(link, tmp, &neighbor); + (void) neighbor_get_request(link, tmp, &req); + + if (type == RTM_DELNEIGH) { + if (neighbor) { + neighbor_enter_removed(neighbor); + log_neighbor_debug(neighbor, "Forgetting removed", link); + neighbor_free(neighbor); + } else + log_neighbor_debug(tmp, "Kernel removed unknown", link); + + if (req) + neighbor_enter_removed(req->userdata); + + return 0; + } + + /* If we did not know the neighbor, then save it. */ + if (!neighbor) { + r = neighbor_add(link, tmp); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to save received neighbor, ignoring: %m"); + return 0; + } + neighbor = TAKE_PTR(tmp); + is_new = true; + } + + /* Also update information that cannot be obtained through netlink notification. */ + if (req && req->waiting_reply) { + Neighbor *n = ASSERT_PTR(req->userdata); + + neighbor->source = n->source; + } + + /* Then, update miscellaneous info. */ + r = netlink_message_read_hw_addr(message, NDA_LLADDR, &neighbor->ll_addr); + if (r < 0 && r != -ENODATA) + log_link_debug_errno(link, r, "rtnl: received neighbor message without valid link layer address, ignoring: %m"); + + neighbor_enter_configured(neighbor); + if (req) + neighbor_enter_configured(req->userdata); + + log_neighbor_debug(neighbor, is_new ? "Remembering" : "Received remembered", link); + 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->family == AF_INET6 && !socket_ipv6_is_supported()) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Neighbor section with an IPv6 destination address configured, " + "but the kernel does not support IPv6. " + "Ignoring [Neighbor] section from line %u.", + neighbor->section->filename, neighbor->section->line); + + if (neighbor->ll_addr.length == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Neighbor section without LinkLayerAddress= configured. " + "Ignoring [Neighbor] section from line %u.", + neighbor->section->filename, neighbor->section->line); + + return 0; +} + +int network_drop_invalid_neighbors(Network *network) { + _cleanup_set_free_ Set *neighbors = NULL; + Neighbor *neighbor; + int r; + + assert(network); + + ORDERED_HASHMAP_FOREACH(neighbor, network->neighbors_by_section) { + Neighbor *dup; + + if (neighbor_section_verify(neighbor) < 0) { + /* Drop invalid [Neighbor] sections. Note that neighbor_free() will drop the + * neighbor from neighbors_by_section. */ + neighbor_free(neighbor); + continue; + } + + /* Always use the setting specified later. So, remove the previously assigned setting. */ + dup = set_remove(neighbors, neighbor); + if (dup) { + log_warning("%s: Duplicated neighbor settings for %s is specified at line %u and %u, " + "dropping the address setting specified at line %u.", + dup->section->filename, + IN_ADDR_TO_STRING(neighbor->family, &neighbor->in_addr), + neighbor->section->line, + dup->section->line, dup->section->line); + /* neighbor_free() will drop the address from neighbors_by_section. */ + neighbor_free(dup); + } + + /* Use neighbor_hash_ops, instead of neighbor_hash_ops_free. Otherwise, the Neighbor objects + * will be freed. */ + r = set_ensure_put(&neighbors, &neighbor_hash_ops, neighbor); + if (r < 0) + return log_oom(); + assert(r > 0); + } + + return 0; +} + + +int config_parse_neighbor_address( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(neighbor_free_or_set_invalidp) Neighbor *n = NULL; + Network *network = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = neighbor_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + n->family = AF_UNSPEC; + n->in_addr = IN_ADDR_NULL; + TAKE_PTR(n); + return 0; + } + + r = in_addr_from_string_auto(rvalue, &n->family, &n->in_addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Neighbor Address is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_neighbor_lladdr( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(neighbor_free_or_set_invalidp) Neighbor *n = NULL; + Network *network = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = neighbor_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + n->ll_addr = HW_ADDR_NULL; + TAKE_PTR(n); + return 0; + } + + r = parse_hw_addr(rvalue, &n->ll_addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Neighbor %s= is invalid, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} diff --git a/src/network/networkd-neighbor.h b/src/network/networkd-neighbor.h new file mode 100644 index 0000000..683a310 --- /dev/null +++ b/src/network/networkd-neighbor.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> + +#include "sd-netlink.h" + +#include "conf-parser.h" +#include "ether-addr-util.h" +#include "in-addr-util.h" +#include "networkd-util.h" + +typedef struct Link Link; +typedef struct Manager Manager; +typedef struct Network Network; + +typedef struct Neighbor { + Network *network; + Link *link; + ConfigSection *section; + NetworkConfigSource source; + NetworkConfigState state; + + int family; + union in_addr_union in_addr; + struct hw_addr_data ll_addr; +} Neighbor; + +Neighbor *neighbor_free(Neighbor *neighbor); + +int network_drop_invalid_neighbors(Network *network); + +int link_drop_managed_neighbors(Link *link); +int link_drop_foreign_neighbors(Link *link); +void link_foreignize_neighbors(Link *link); + +int link_request_static_neighbors(Link *link); + +int manager_rtnl_process_neighbor(sd_netlink *rtnl, sd_netlink_message *message, Manager *m); + +DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(Neighbor, neighbor); + +CONFIG_PARSER_PROTOTYPE(config_parse_neighbor_address); +CONFIG_PARSER_PROTOTYPE(config_parse_neighbor_lladdr); diff --git a/src/network/networkd-netlabel.c b/src/network/networkd-netlabel.c new file mode 100644 index 0000000..94bf8f5 --- /dev/null +++ b/src/network/networkd-netlabel.c @@ -0,0 +1,128 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "escape.h" +#include "netlink-util.h" +#include "networkd-address.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-netlabel.h" +#include "networkd-network.h" + +static int netlabel_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert_se(rtnl); + assert_se(m); + assert_se(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + log_link_message_warning_errno(link, m, r, "NetLabel operation failed, ignoring"); + return 1; + } + + log_link_debug(link, "NetLabel operation successful"); + + return 1; +} + +static int netlabel_command(uint16_t command, const char *label, const Address *address) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(command != NLBL_UNLABEL_C_UNSPEC && command < __NLBL_UNLABEL_C_MAX); + assert(address); + assert(address->link); + assert(address->link->ifname); + assert(address->link->manager); + assert(address->link->manager->genl); + assert(IN_SET(address->family, AF_INET, AF_INET6)); + + r = sd_genl_message_new(address->link->manager->genl, NETLBL_NLTYPE_UNLABELED_NAME, command, &m); + if (r < 0) + return r; + + r = sd_netlink_message_append_string(m, NLBL_UNLABEL_A_IFACE, address->link->ifname); + if (r < 0) + return r; + + if (command == NLBL_UNLABEL_C_STATICADD) { + assert(label); + r = sd_netlink_message_append_string(m, NLBL_UNLABEL_A_SECCTX, label); + if (r < 0) + return r; + } + + union in_addr_union netmask, masked_addr; + r = in_addr_prefixlen_to_netmask(address->family, &netmask, address->prefixlen); + if (r < 0) + return r; + + /* + * When adding rules, kernel adds the address to its hash table _applying also the netmask_, but on + * removal, an exact match is required _without netmask applied_, so apply the mask on both + * operations. + */ + masked_addr = address->in_addr; + r = in_addr_mask(address->family, &masked_addr, address->prefixlen); + if (r < 0) + return r; + + if (address->family == AF_INET) { + r = sd_netlink_message_append_in_addr(m, NLBL_UNLABEL_A_IPV4ADDR, &masked_addr.in); + if (r < 0) + return r; + + r = sd_netlink_message_append_in_addr(m, NLBL_UNLABEL_A_IPV4MASK, &netmask.in); + } else if (address->family == AF_INET6) { + r = sd_netlink_message_append_in6_addr(m, NLBL_UNLABEL_A_IPV6ADDR, &masked_addr.in6); + if (r < 0) + return r; + + r = sd_netlink_message_append_in6_addr(m, NLBL_UNLABEL_A_IPV6MASK, &netmask.in6); + } + if (r < 0) + return r; + + r = netlink_call_async(address->link->manager->genl, NULL, m, netlabel_handler, link_netlink_destroy_callback, + address->link); + if (r < 0) + return r; + + link_ref(address->link); + return 0; +} + +void address_add_netlabel(const Address *address) { + int r; + + assert(address); + + if (!address->netlabel) + return; + + r = netlabel_command(NLBL_UNLABEL_C_STATICADD, address->netlabel, address); + if (r < 0) + log_link_warning_errno(address->link, r, "Adding NetLabel %s for IP address %s failed, ignoring", address->netlabel, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); + else + log_link_debug(address->link, "Adding NetLabel %s for IP address %s", address->netlabel, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); +} + +void address_del_netlabel(const Address *address) { + int r; + + assert(address); + + if (!address->netlabel) + return; + + r = netlabel_command(NLBL_UNLABEL_C_STATICREMOVE, address->netlabel, address); + if (r < 0) + log_link_warning_errno(address->link, r, "Deleting NetLabel %s for IP address %s failed, ignoring", address->netlabel, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); + else + log_link_debug(address->link, "Deleting NetLabel %s for IP address %s", address->netlabel, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); +} diff --git a/src/network/networkd-netlabel.h b/src/network/networkd-netlabel.h new file mode 100644 index 0000000..2f30b8f --- /dev/null +++ b/src/network/networkd-netlabel.h @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +void address_add_netlabel(const Address *address); +void address_del_netlabel(const Address *address); diff --git a/src/network/networkd-network-bus.c b/src/network/networkd-network-bus.c new file mode 100644 index 0000000..0c40326 --- /dev/null +++ b/src/network/networkd-network-bus.c @@ -0,0 +1,144 @@ +/* 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 "path-util.h" +#include "string-util.h" +#include "strv.h" + +static int property_get_hw_addrs( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + const struct hw_addr_data *p; + Set *s; + int r; + + assert(bus); + assert(reply); + assert(userdata); + + s = *(Set **) userdata; + + r = sd_bus_message_open_container(reply, 'a', "s"); + if (r < 0) + return r; + + SET_FOREACH(p, s) { + r = sd_bus_message_append(reply, "s", HW_ADDR_TO_STR(p)); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static const sd_bus_vtable network_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("Description", "s", NULL, offsetof(Network, description), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("SourcePath", "s", NULL, offsetof(Network, filename), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("MatchMAC", "as", property_get_hw_addrs, offsetof(Network, match.hw_addr), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("MatchPath", "as", NULL, offsetof(Network, match.path), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("MatchDriver", "as", NULL, offsetof(Network, match.driver), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("MatchType", "as", NULL, offsetof(Network, match.iftype), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("MatchName", "as", NULL, offsetof(Network, match.ifname), SD_BUS_VTABLE_PROPERTY_CONST), + + SD_BUS_VTABLE_END +}; + +static char *network_bus_path(Network *network) { + _cleanup_free_ char *name = NULL, *networkname= NULL; + char *d, *path; + int r; + + assert(network); + assert(network->filename); + + name = strdup(network->filename); + if (!name) + return NULL; + + r = path_extract_filename(name, &networkname); + if (r < 0) + return NULL; + + d = strrchr(networkname, '.'); + if (!d) + return NULL; + + assert(streq(d, ".network")); + + *d = '\0'; + + r = sd_bus_path_encode("/org/freedesktop/network1/network", networkname, &path); + if (r < 0) + return NULL; + + return path; +} + +int network_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) { + _cleanup_strv_free_ char **l = NULL; + Manager *m = ASSERT_PTR(userdata); + Network *network; + int r; + + assert(bus); + assert(path); + assert(nodes); + + ORDERED_HASHMAP_FOREACH(network, m->networks) { + char *p; + + p = network_bus_path(network); + if (!p) + return -ENOMEM; + + r = strv_consume(&l, p); + if (r < 0) + return r; + } + + *nodes = TAKE_PTR(l); + + return 1; +} + +int network_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + Network *network; + _cleanup_free_ char *name = NULL; + int r; + + assert(bus); + assert(path); + assert(interface); + assert(found); + + r = sd_bus_path_decode(path, "/org/freedesktop/network1/network", &name); + if (r < 0) + return 0; + + r = network_get_by_name(m, name, &network); + if (r < 0) + return 0; + + *found = network; + + return 1; +} + +const BusObjectImplementation network_object = { + "/org/freedesktop/network1/network", + "org.freedesktop.network1.Network", + .fallback_vtables = BUS_FALLBACK_VTABLES({network_vtable, network_object_find}), + .node_enumerator = network_node_enumerator, +}; diff --git a/src/network/networkd-network-bus.h b/src/network/networkd-network-bus.h new file mode 100644 index 0000000..68ed951 --- /dev/null +++ b/src/network/networkd-network-bus.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" + +#include "bus-object.h" + +typedef struct Link Link; + +extern const BusObjectImplementation network_object; + +int network_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error); +int network_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error); diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf new file mode 100644 index 0000000..a6593a0 --- /dev/null +++ b/src/network/networkd-network-gperf.gperf @@ -0,0 +1,627 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +%{ +#if __GNUC__ >= 7 +_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"") +#endif +#include <stddef.h> +#include "conf-parser.h" +#include "in-addr-prefix-util.h" +#include "netem.h" +#include "net-condition.h" +#include "networkd-address-generation.h" +#include "networkd-address-label.h" +#include "networkd-address.h" +#include "networkd-bridge-fdb.h" +#include "networkd-bridge-mdb.h" +#include "networkd-can.h" +#include "networkd-dhcp-common.h" +#include "networkd-dhcp-prefix-delegation.h" +#include "networkd-dhcp-server-static-lease.h" +#include "networkd-dhcp-server.h" +#include "networkd-dhcp4.h" +#include "networkd-dhcp6.h" +#include "networkd-ipv4ll.h" +#include "networkd-ipv6-proxy-ndp.h" +#include "networkd-ipv6ll.h" +#include "networkd-lldp-tx.h" +#include "networkd-ndisc.h" +#include "networkd-netlabel.h" +#include "networkd-network.h" +#include "networkd-neighbor.h" +#include "networkd-nexthop.h" +#include "networkd-radv.h" +#include "networkd-route.h" +#include "networkd-routing-policy-rule.h" +#include "networkd-sriov.h" +#include "qdisc.h" +#include "tclass.h" +#include "vlan-util.h" +%} +struct ConfigPerfItem; +%null_strings +%language=ANSI-C +%define slot-name section_and_lvalue +%define hash-function-name network_network_gperf_hash +%define lookup-function-name network_network_gperf_lookup +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +Match.MACAddress, config_parse_hw_addrs, 0, offsetof(Network, match.hw_addr) +Match.PermanentMACAddress, config_parse_hw_addrs, 0, offsetof(Network, match.permanent_hw_addr) +Match.Path, config_parse_match_strv, 0, offsetof(Network, match.path) +Match.Driver, config_parse_match_strv, 0, offsetof(Network, match.driver) +Match.Type, config_parse_match_strv, 0, offsetof(Network, match.iftype) +Match.Kind, config_parse_match_strv, 0, offsetof(Network, match.kind) +Match.WLANInterfaceType, config_parse_match_strv, 0, offsetof(Network, match.wlan_iftype) +Match.SSID, config_parse_match_strv, 0, offsetof(Network, match.ssid) +Match.BSSID, config_parse_ether_addrs, 0, offsetof(Network, match.bssid) +Match.Name, config_parse_match_ifnames, IFNAME_VALID_ALTERNATIVE, offsetof(Network, match.ifname) +Match.Property, config_parse_match_property, 0, offsetof(Network, match.property) +Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(Network, conditions) +Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(Network, conditions) +Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(Network, conditions) +Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(Network, conditions) +Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(Network, conditions) +Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(Network, conditions) +Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(Network, conditions) +Link.MACAddress, config_parse_hw_addr, 0, offsetof(Network, hw_addr) +Link.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(Network, mtu) +Link.Group, config_parse_link_group, 0, 0 +Link.ARP, config_parse_tristate, 0, offsetof(Network, arp) +Link.Multicast, config_parse_tristate, 0, offsetof(Network, multicast) +Link.AllMulticast, config_parse_tristate, 0, offsetof(Network, allmulticast) +Link.Promiscuous, config_parse_tristate, 0, offsetof(Network, promiscuous) +Link.Unmanaged, config_parse_bool, 0, offsetof(Network, unmanaged) +Link.ActivationPolicy, config_parse_activation_policy, 0, offsetof(Network, activation_policy) +Link.RequiredForOnline, config_parse_required_for_online, 0, 0 +Link.RequiredFamilyForOnline, config_parse_required_family_for_online, 0, offsetof(Network, required_family_for_online) +SR-IOV.VirtualFunction, config_parse_sr_iov_uint32, 0, offsetof(Network, sr_iov_by_section) +SR-IOV.VLANId, config_parse_sr_iov_uint32, 0, offsetof(Network, sr_iov_by_section) +SR-IOV.QualityOfService, config_parse_sr_iov_uint32, 0, offsetof(Network, sr_iov_by_section) +SR-IOV.VLANProtocol, config_parse_sr_iov_vlan_proto, 0, offsetof(Network, sr_iov_by_section) +SR-IOV.MACSpoofCheck, config_parse_sr_iov_boolean, 0, offsetof(Network, sr_iov_by_section) +SR-IOV.QueryReceiveSideScaling, config_parse_sr_iov_boolean, 0, offsetof(Network, sr_iov_by_section) +SR-IOV.Trust, config_parse_sr_iov_boolean, 0, offsetof(Network, sr_iov_by_section) +SR-IOV.LinkState, config_parse_sr_iov_link_state, 0, offsetof(Network, sr_iov_by_section) +SR-IOV.MACAddress, config_parse_sr_iov_mac, 0, offsetof(Network, sr_iov_by_section) +Network.Description, config_parse_string, 0, offsetof(Network, description) +Network.KeepMaster, config_parse_bool, 0, offsetof(Network, keep_master) +Network.BatmanAdvanced, config_parse_ifname, 0, offsetof(Network, batadv_name) +Network.Bond, config_parse_ifname, 0, offsetof(Network, bond_name) +Network.Bridge, config_parse_ifname, 0, offsetof(Network, bridge_name) +Network.VRF, config_parse_ifname, 0, offsetof(Network, vrf_name) +Network.IPoIB, config_parse_stacked_netdev, NETDEV_KIND_IPOIB, offsetof(Network, stacked_netdev_names) +Network.IPVLAN, config_parse_stacked_netdev, NETDEV_KIND_IPVLAN, offsetof(Network, stacked_netdev_names) +Network.IPVTAP, config_parse_stacked_netdev, NETDEV_KIND_IPVTAP, offsetof(Network, stacked_netdev_names) +Network.L2TP, config_parse_warn_compat, DISABLED_LEGACY, 0 +Network.MACsec, config_parse_stacked_netdev, NETDEV_KIND_MACSEC, offsetof(Network, stacked_netdev_names) +Network.MACVLAN, config_parse_stacked_netdev, NETDEV_KIND_MACVLAN, offsetof(Network, stacked_netdev_names) +Network.MACVTAP, config_parse_stacked_netdev, NETDEV_KIND_MACVTAP, offsetof(Network, stacked_netdev_names) +Network.Tunnel, config_parse_stacked_netdev, _NETDEV_KIND_TUNNEL, offsetof(Network, stacked_netdev_names) +Network.VLAN, config_parse_stacked_netdev, NETDEV_KIND_VLAN, offsetof(Network, stacked_netdev_names) +Network.VXLAN, config_parse_stacked_netdev, NETDEV_KIND_VXLAN, offsetof(Network, stacked_netdev_names) +Network.Xfrm, config_parse_stacked_netdev, NETDEV_KIND_XFRM, offsetof(Network, stacked_netdev_names) +Network.DHCP, config_parse_dhcp, 0, offsetof(Network, dhcp) +Network.DHCPServer, config_parse_bool, 0, offsetof(Network, dhcp_server) +Network.LinkLocalAddressing, config_parse_link_local_address_family, 0, offsetof(Network, link_local) +Network.IPv6LinkLocalAddressGenerationMode, config_parse_ipv6_link_local_address_gen_mode, 0, offsetof(Network, ipv6ll_address_gen_mode) +Network.IPv6StableSecretAddress, config_parse_in_addr_non_null, AF_INET6, offsetof(Network, ipv6ll_stable_secret) +Network.IPv4LLStartAddress, config_parse_ipv4ll_address, 0, offsetof(Network, ipv4ll_start_address) +Network.IPv4LLRoute, config_parse_bool, 0, offsetof(Network, ipv4ll_route) +Network.DefaultRouteOnDevice, config_parse_bool, 0, offsetof(Network, default_route_on_device) +Network.LLDP, config_parse_lldp_mode, 0, offsetof(Network, lldp_mode) +Network.EmitLLDP, config_parse_lldp_multicast_mode, 0, offsetof(Network, lldp_multicast_mode) +Network.Address, config_parse_address, 0, 0 +Network.Gateway, config_parse_gateway, 0, 0 +Network.Domains, config_parse_domains, 0, 0 +Network.DNS, config_parse_dns, 0, 0 +Network.DNSDefaultRoute, config_parse_tristate, 0, offsetof(Network, dns_default_route) +Network.LLMNR, config_parse_resolve_support, 0, offsetof(Network, llmnr) +Network.MulticastDNS, config_parse_resolve_support, 0, offsetof(Network, mdns) +Network.DNSOverTLS, config_parse_dns_over_tls_mode, 0, offsetof(Network, dns_over_tls_mode) +Network.DNSSEC, config_parse_dnssec_mode, 0, offsetof(Network, dnssec_mode) +Network.DNSSECNegativeTrustAnchors, config_parse_dnssec_negative_trust_anchors, 0, offsetof(Network, dnssec_negative_trust_anchors) +Network.NTP, config_parse_ntp, 0, offsetof(Network, ntp) +Network.IPForward, config_parse_address_family_with_kernel, 0, offsetof(Network, ip_forward) +Network.IPMasquerade, config_parse_ip_masquerade, 0, offsetof(Network, ip_masquerade) +Network.IPv6PrivacyExtensions, config_parse_ipv6_privacy_extensions, 0, offsetof(Network, ipv6_privacy_extensions) +Network.IPv6AcceptRA, config_parse_tristate, 0, offsetof(Network, ipv6_accept_ra) +Network.IPv6AcceptRouterAdvertisements, config_parse_tristate, 0, offsetof(Network, ipv6_accept_ra) +Network.IPv6DuplicateAddressDetection, config_parse_int, 0, offsetof(Network, ipv6_dad_transmits) +Network.IPv6HopLimit, config_parse_uint8, 0, offsetof(Network, ipv6_hop_limit) +Network.IPv6ProxyNDP, config_parse_tristate, 0, offsetof(Network, ipv6_proxy_ndp) +Network.IPv6MTUBytes, config_parse_mtu, AF_INET6, offsetof(Network, ipv6_mtu) +Network.IPv4AcceptLocal, config_parse_tristate, 0, offsetof(Network, ipv4_accept_local) +Network.IPv4RouteLocalnet, config_parse_tristate, 0, offsetof(Network, ipv4_route_localnet) +Network.ActiveSlave, config_parse_bool, 0, offsetof(Network, active_slave) +Network.PrimarySlave, config_parse_bool, 0, offsetof(Network, primary_slave) +Network.IPv4ProxyARP, config_parse_tristate, 0, offsetof(Network, proxy_arp) +Network.ProxyARP, config_parse_tristate, 0, offsetof(Network, proxy_arp) +Network.IPv6ProxyNDPAddress, config_parse_ipv6_proxy_ndp_address, 0, 0 +Network.IPv4ReversePathFilter, config_parse_ip_reverse_path_filter, 0, offsetof(Network, ipv4_rp_filter) +Network.BindCarrier, config_parse_strv, 0, offsetof(Network, bind_carrier) +Network.ConfigureWithoutCarrier, config_parse_bool, 0, offsetof(Network, configure_without_carrier) +Network.IgnoreCarrierLoss, config_parse_ignore_carrier_loss, 0, 0 +Network.KeepConfiguration, config_parse_keep_configuration, 0, offsetof(Network, keep_configuration) +Network.IPv6SendRA, config_parse_router_prefix_delegation, 0, offsetof(Network, router_prefix_delegation) +Network.DHCPPrefixDelegation, config_parse_tristate, 0, offsetof(Network, dhcp_pd) +Address.Address, config_parse_address, 0, 0 +Address.Peer, config_parse_address, 0, 0 +Address.Broadcast, config_parse_broadcast, 0, 0 +Address.Label, config_parse_label, 0, 0 +Address.PreferredLifetime, config_parse_lifetime, 0, 0 +Address.HomeAddress, config_parse_address_flags, IFA_F_HOMEADDRESS, 0 +Address.ManageTemporaryAddress, config_parse_address_flags, IFA_F_MANAGETEMPADDR, 0 +Address.PrefixRoute, config_parse_address_flags, IFA_F_NOPREFIXROUTE, 0 /* deprecated */ +Address.AddPrefixRoute, config_parse_address_flags, IFA_F_NOPREFIXROUTE, 0 +Address.AutoJoin, config_parse_address_flags, IFA_F_MCAUTOJOIN, 0 +Address.DuplicateAddressDetection, config_parse_duplicate_address_detection, 0, 0 +Address.Scope, config_parse_address_scope, 0, 0 +Address.RouteMetric, config_parse_address_route_metric, 0, 0 +Address.NetLabel, config_parse_address_netlabel, 0, 0 +Address.NFTSet, config_parse_address_ip_nft_set, NFT_SET_PARSE_NETWORK, 0 +IPv6AddressLabel.Prefix, config_parse_address_label_prefix, 0, 0 +IPv6AddressLabel.Label, config_parse_address_label, 0, 0 +Neighbor.Address, config_parse_neighbor_address, 0, 0 +Neighbor.LinkLayerAddress, config_parse_neighbor_lladdr, 0, 0 +Neighbor.MACAddress, config_parse_neighbor_lladdr, 0, 0 /* deprecated */ +RoutingPolicyRule.TypeOfService, config_parse_routing_policy_rule_tos, 0, 0 +RoutingPolicyRule.Priority, config_parse_routing_policy_rule_priority, 0, 0 +RoutingPolicyRule.Table, config_parse_routing_policy_rule_table, 0, 0 +RoutingPolicyRule.FirewallMark, config_parse_routing_policy_rule_fwmark_mask, 0, 0 +RoutingPolicyRule.From, config_parse_routing_policy_rule_prefix, 0, 0 +RoutingPolicyRule.To, config_parse_routing_policy_rule_prefix, 0, 0 +RoutingPolicyRule.IncomingInterface, config_parse_routing_policy_rule_device, 0, 0 +RoutingPolicyRule.OutgoingInterface, config_parse_routing_policy_rule_device, 0, 0 +RoutingPolicyRule.IPProtocol, config_parse_routing_policy_rule_ip_protocol, 0, 0 +RoutingPolicyRule.SourcePort, config_parse_routing_policy_rule_port_range, 0, 0 +RoutingPolicyRule.DestinationPort, config_parse_routing_policy_rule_port_range, 0, 0 +RoutingPolicyRule.InvertRule, config_parse_routing_policy_rule_invert, 0, 0 +RoutingPolicyRule.Family, config_parse_routing_policy_rule_family, 0, 0 +RoutingPolicyRule.User, config_parse_routing_policy_rule_uid_range, 0, 0 +RoutingPolicyRule.SuppressInterfaceGroup, config_parse_routing_policy_rule_suppress_ifgroup, 0, 0 +RoutingPolicyRule.SuppressPrefixLength, config_parse_routing_policy_rule_suppress_prefixlen, 0, 0 +RoutingPolicyRule.Type, config_parse_routing_policy_rule_type, 0, 0 +Route.Gateway, config_parse_gateway, 0, 0 +Route.Destination, config_parse_destination, 0, 0 +Route.Source, config_parse_destination, 0, 0 +Route.Metric, config_parse_route_priority, 0, 0 +Route.Scope, config_parse_route_scope, 0, 0 +Route.PreferredSource, config_parse_preferred_src, 0, 0 +Route.Table, config_parse_route_table, 0, 0 +Route.MTUBytes, config_parse_route_mtu, AF_UNSPEC, 0 +Route.GatewayOnLink, config_parse_route_boolean, 0, 0 +Route.GatewayOnlink, config_parse_route_boolean, 0, 0 +Route.IPv6Preference, config_parse_ipv6_route_preference, 0, 0 +Route.Protocol, config_parse_route_protocol, 0, 0 +Route.Type, config_parse_route_type, 0, 0 +Route.TCPRetransmissionTimeoutSec, config_parse_route_tcp_rto, 0, 0 +Route.HopLimit, config_parse_route_hop_limit, 0, 0 +Route.InitialCongestionWindow, config_parse_route_tcp_window, 0, 0 +Route.InitialAdvertisedReceiveWindow, config_parse_route_tcp_window, 0, 0 +Route.TCPAdvertisedMaximumSegmentSize, config_parse_tcp_advmss, 0, 0 +Route.TCPCongestionControlAlgorithm, config_parse_tcp_congestion, 0, 0 +Route.QuickAck, config_parse_route_boolean, 0, 0 +Route.FastOpenNoCookie, config_parse_route_boolean, 0, 0 +Route.TTLPropagate, config_parse_route_boolean, 0, 0 +Route.MultiPathRoute, config_parse_multipath_route, 0, 0 +Route.NextHop, config_parse_route_nexthop, 0, 0 +NextHop.Id, config_parse_nexthop_id, 0, 0 +NextHop.Gateway, config_parse_nexthop_gateway, 0, 0 +NextHop.Family, config_parse_nexthop_family, 0, 0 +NextHop.OnLink, config_parse_nexthop_onlink, 0, 0 +NextHop.Blackhole, config_parse_nexthop_blackhole, 0, 0 +NextHop.Group, config_parse_nexthop_group, 0, 0 +DHCPv4.RequestAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_request_address) +DHCPv4.ClientIdentifier, config_parse_dhcp_client_identifier, 0, offsetof(Network, dhcp_client_identifier) +DHCPv4.UseDNS, config_parse_dhcp_use_dns, AF_INET, 0 +DHCPv4.RoutesToDNS, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_dns) +DHCPv4.UseNTP, config_parse_dhcp_use_ntp, AF_INET, 0 +DHCPv4.RoutesToNTP, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_ntp) +DHCPv4.UseSIP, config_parse_bool, 0, offsetof(Network, dhcp_use_sip) +DHCPv4.UseCaptivePortal, config_parse_bool, 0, offsetof(Network, dhcp_use_captive_portal) +DHCPv4.UseMTU, config_parse_bool, 0, offsetof(Network, dhcp_use_mtu) +DHCPv4.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp_use_hostname) +DHCPv4.UseDomains, config_parse_dhcp_use_domains, AF_INET, 0 +DHCPv4.UseRoutes, config_parse_bool, 0, offsetof(Network, dhcp_use_routes) +DHCPv4.UseGateway, config_parse_tristate, 0, offsetof(Network, dhcp_use_gateway) +DHCPv4.QuickAck, config_parse_bool, 0, offsetof(Network, dhcp_quickack) +DHCPv4.RequestOptions, config_parse_dhcp_request_options, AF_INET, 0 +DHCPv4.Anonymize, config_parse_bool, 0, offsetof(Network, dhcp_anonymize) +DHCPv4.SendHostname, config_parse_dhcp_send_hostname, AF_INET, 0 +DHCPv4.Hostname, config_parse_hostname, 0, offsetof(Network, dhcp_hostname) +DHCPv4.Label, config_parse_dhcp_label, 0, offsetof(Network, dhcp_label) +DHCPv4.RequestBroadcast, config_parse_tristate, 0, offsetof(Network, dhcp_broadcast) +DHCPv4.VendorClassIdentifier, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_vendor_class_identifier) +DHCPv4.MUDURL, config_parse_mud_url, 0, offsetof(Network, dhcp_mudurl) +DHCPv4.MaxAttempts, config_parse_dhcp_max_attempts, 0, 0 +DHCPv4.UserClass, config_parse_dhcp_user_or_vendor_class, AF_INET, offsetof(Network, dhcp_user_class) +DHCPv4.IAID, config_parse_iaid, AF_INET, 0 +DHCPv4.DUIDType, config_parse_network_duid_type, 0, 0 +DHCPv4.DUIDRawData, config_parse_network_duid_rawdata, 0, 0 +DHCPv4.RouteMetric, config_parse_dhcp_route_metric, AF_INET, 0 +DHCPv4.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET, 0 +DHCPv4.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone) +DHCPv4.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port) +DHCPv4.SendRelease, config_parse_bool, 0, offsetof(Network, dhcp_send_release) +DHCPv4.SendDecline, config_parse_bool, 0, offsetof(Network, dhcp_send_decline) +DHCPv4.DenyList, config_parse_in_addr_prefixes, AF_INET, offsetof(Network, dhcp_deny_listed_ip) +DHCPv4.AllowList, config_parse_in_addr_prefixes, AF_INET, offsetof(Network, dhcp_allow_listed_ip) +DHCPv4.IPServiceType, config_parse_dhcp_ip_service_type, 0, offsetof(Network, dhcp_ip_service_type) +DHCPv4.SocketPriority, config_parse_dhcp_socket_priority, 0, 0 +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.InitialCongestionWindow, config_parse_tcp_window, 0, offsetof(Network, dhcp_initial_congestion_window) +DHCPv4.InitialAdvertisedReceiveWindow, config_parse_tcp_window, 0, offsetof(Network, dhcp_advertised_receive_window) +DHCPv4.FallbackLeaseLifetimeSec, config_parse_dhcp_fallback_lease_lifetime, 0, 0 +DHCPv4.Use6RD, config_parse_bool, 0, offsetof(Network, dhcp_use_6rd) +DHCPv4.IPv6OnlyMode, config_parse_tristate, 0, offsetof(Network, dhcp_ipv6_only_mode) +DHCPv4.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_netlabel) +DHCPv4.NFTSet, config_parse_nft_set, NFT_SET_PARSE_NETWORK, offsetof(Network, dhcp_nft_set_context) +DHCPv4.RapidCommit, config_parse_tristate, 0, offsetof(Network, dhcp_use_rapid_commit) +DHCPv6.UseAddress, config_parse_bool, 0, offsetof(Network, dhcp6_use_address) +DHCPv6.UseDelegatedPrefix, config_parse_bool, 0, offsetof(Network, dhcp6_use_pd_prefix) +DHCPv6.UseDNS, config_parse_dhcp_use_dns, AF_INET6, 0 +DHCPv6.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp6_use_hostname) +DHCPv6.UseDomains, config_parse_dhcp_use_domains, AF_INET6, 0 +DHCPv6.UseNTP, config_parse_dhcp_use_ntp, AF_INET6, 0 +DHCPv6.UseCaptivePortal, config_parse_bool, 0, offsetof(Network, dhcp6_use_captive_portal) +DHCPv6.MUDURL, config_parse_mud_url, 0, offsetof(Network, dhcp6_mudurl) +DHCPv6.SendHostname, config_parse_dhcp_send_hostname, AF_INET6, 0 +DHCPv6.Hostname, config_parse_hostname, 0, offsetof(Network, dhcp6_hostname) +DHCPv6.RequestOptions, config_parse_dhcp_request_options, AF_INET6, 0 +DHCPv6.UserClass, config_parse_dhcp_user_or_vendor_class, AF_INET6, offsetof(Network, dhcp6_user_class) +DHCPv6.VendorClass, config_parse_dhcp_user_or_vendor_class, AF_INET6, offsetof(Network, dhcp6_vendor_class) +DHCPv6.SendVendorOption, config_parse_dhcp_send_option, AF_INET6, offsetof(Network, dhcp6_client_send_vendor_options) +DHCPv6.PrefixDelegationHint, config_parse_dhcp6_pd_prefix_hint, 0, 0 +DHCPv6.WithoutRA, config_parse_dhcp6_client_start_mode, 0, offsetof(Network, dhcp6_client_start_mode) +DHCPv6.SendOption, config_parse_dhcp_send_option, AF_INET6, offsetof(Network, dhcp6_client_send_options) +DHCPv6.IAID, config_parse_iaid, AF_INET6, 0 +DHCPv6.DUIDType, config_parse_duid_type, 0, offsetof(Network, dhcp6_duid) +DHCPv6.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Network, dhcp6_duid) +DHCPv6.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp6_use_rapid_commit) +DHCPv6.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp6_netlabel) +DHCPv6.SendRelease, config_parse_bool, 0, offsetof(Network, dhcp6_send_release) +DHCPv6.NFTSet, config_parse_nft_set, NFT_SET_PARSE_NETWORK, offsetof(Network, dhcp6_nft_set_context) +IPv6AcceptRA.UseGateway, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_gateway) +IPv6AcceptRA.UseRoutePrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_route_prefix) +IPv6AcceptRA.UseAutonomousPrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_autonomous_prefix) +IPv6AcceptRA.UseOnLinkPrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_onlink_prefix) +IPv6AcceptRA.UsePREF64, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_pref64) +IPv6AcceptRA.UseDNS, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_dns) +IPv6AcceptRA.UseDomains, config_parse_ipv6_accept_ra_use_domains, 0, offsetof(Network, ipv6_accept_ra_use_domains) +IPv6AcceptRA.UseMTU, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_mtu) +IPv6AcceptRA.UseHopLimit, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_hop_limit) +IPv6AcceptRA.UseICMP6RateLimit, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_icmp6_ratelimit) +IPv6AcceptRA.DHCPv6Client, config_parse_ipv6_accept_ra_start_dhcp6_client, 0, offsetof(Network, ipv6_accept_ra_start_dhcp6_client) +IPv6AcceptRA.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET6, 0 +IPv6AcceptRA.RouteMetric, config_parse_ipv6_accept_ra_route_metric, 0, 0 +IPv6AcceptRA.QuickAck, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_quickack) +IPv6AcceptRA.UseCaptivePortal, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_captive_portal) +IPv6AcceptRA.RouterAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_router) +IPv6AcceptRA.RouterDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_router) +IPv6AcceptRA.PrefixAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_prefix) +IPv6AcceptRA.PrefixDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_prefix) +IPv6AcceptRA.RouteAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_route_prefix) +IPv6AcceptRA.RouteDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_route_prefix) +IPv6AcceptRA.Token, config_parse_address_generation_type, 0, offsetof(Network, ndisc_tokens) +IPv6AcceptRA.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, ndisc_netlabel) +IPv6AcceptRA.NFTSet, config_parse_nft_set, NFT_SET_PARSE_NETWORK, offsetof(Network, ndisc_nft_set_context) +DHCPServer.ServerAddress, config_parse_dhcp_server_address, 0, 0 +DHCPServer.UplinkInterface, config_parse_uplink, 0, 0 +DHCPServer.RelayTarget, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_relay_target) +DHCPServer.RelayAgentCircuitId, config_parse_dhcp_server_relay_agent_suboption, 0, offsetof(Network, dhcp_server_relay_agent_circuit_id) +DHCPServer.RelayAgentRemoteId, config_parse_dhcp_server_relay_agent_suboption, 0, offsetof(Network, dhcp_server_relay_agent_remote_id) +DHCPServer.MaxLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_max_lease_time_usec) +DHCPServer.DefaultLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_default_lease_time_usec) +DHCPServer.IPv6OnlyPreferredSec, config_parse_dhcp_server_ipv6_only_preferred, 0, offsetof(Network, dhcp_server_ipv6_only_preferred_usec) +DHCPServer.EmitDNS, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_DNS].emit) +DHCPServer.DNS, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_DNS]) +DHCPServer.EmitNTP, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_NTP].emit) +DHCPServer.NTP, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_NTP]) +DHCPServer.EmitSIP, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_SIP].emit) +DHCPServer.SIP, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_SIP]) +DHCPServer.EmitPOP3, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_POP3].emit) +DHCPServer.POP3, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_POP3]) +DHCPServer.EmitSMTP, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_SMTP].emit) +DHCPServer.SMTP, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_SMTP]) +DHCPServer.EmitLPR, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_LPR].emit) +DHCPServer.LPR, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_LPR]) +DHCPServer.EmitRouter, config_parse_bool, 0, offsetof(Network, dhcp_server_emit_router) +DHCPServer.Router, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_router) +DHCPServer.EmitTimezone, config_parse_bool, 0, offsetof(Network, dhcp_server_emit_timezone) +DHCPServer.Timezone, config_parse_timezone, 0, offsetof(Network, dhcp_server_timezone) +DHCPServer.PoolOffset, config_parse_uint32, 0, offsetof(Network, dhcp_server_pool_offset) +DHCPServer.PoolSize, config_parse_uint32, 0, offsetof(Network, dhcp_server_pool_size) +DHCPServer.SendVendorOption, config_parse_dhcp_send_option, 0, offsetof(Network, dhcp_server_send_vendor_options) +DHCPServer.SendOption, config_parse_dhcp_send_option, 0, offsetof(Network, dhcp_server_send_options) +DHCPServer.BindToInterface, config_parse_bool, 0, offsetof(Network, dhcp_server_bind_to_interface) +DHCPServer.BootServerAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_boot_server_address) +DHCPServer.BootServerName, config_parse_dns_name, 0, offsetof(Network, dhcp_server_boot_server_name) +DHCPServer.BootFilename, config_parse_string, CONFIG_PARSE_STRING_SAFE_AND_ASCII, offsetof(Network, dhcp_server_boot_filename) +DHCPServer.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp_server_rapid_commit) +DHCPServerStaticLease.Address, config_parse_dhcp_static_lease_address, 0, 0 +DHCPServerStaticLease.MACAddress, config_parse_dhcp_static_lease_hwaddr, 0, 0 +Bridge.Cost, config_parse_uint32, 0, offsetof(Network, cost) +Bridge.UseBPDU, config_parse_tristate, 0, offsetof(Network, use_bpdu) +Bridge.HairPin, config_parse_tristate, 0, offsetof(Network, hairpin) +Bridge.Isolated, config_parse_tristate, 0, offsetof(Network, isolated) +Bridge.FastLeave, config_parse_tristate, 0, offsetof(Network, fast_leave) +Bridge.AllowPortToBeRoot, config_parse_tristate, 0, offsetof(Network, allow_port_to_be_root) +Bridge.UnicastFlood, config_parse_tristate, 0, offsetof(Network, unicast_flood) +Bridge.MulticastFlood, config_parse_tristate, 0, offsetof(Network, multicast_flood) +Bridge.MulticastToUnicast, config_parse_tristate, 0, offsetof(Network, multicast_to_unicast) +Bridge.NeighborSuppression, config_parse_tristate, 0, offsetof(Network, neighbor_suppression) +Bridge.Learning, config_parse_tristate, 0, offsetof(Network, learning) +Bridge.ProxyARP, config_parse_tristate, 0, offsetof(Network, bridge_proxy_arp) +Bridge.ProxyARPWiFi, config_parse_tristate, 0, offsetof(Network, bridge_proxy_arp_wifi) +Bridge.Priority, config_parse_bridge_port_priority, 0, offsetof(Network, priority) +Bridge.MulticastRouter, config_parse_multicast_router, 0, offsetof(Network, multicast_router) +BridgeFDB.MACAddress, config_parse_fdb_hwaddr, 0, 0 +BridgeFDB.VLANId, config_parse_fdb_vlan_id, 0, 0 +BridgeFDB.Destination, config_parse_fdb_destination, 0, 0 +BridgeFDB.VNI, config_parse_fdb_vxlan_vni, 0, 0 +BridgeFDB.AssociatedWith, config_parse_fdb_ntf_flags, 0, 0 +BridgeFDB.OutgoingInterface, config_parse_fdb_interface, 0, 0 +BridgeMDB.MulticastGroupAddress, config_parse_mdb_group_address, 0, 0 +BridgeMDB.VLANId, config_parse_mdb_vlan_id, 0, 0 +BridgeVLAN.PVID, config_parse_brvlan_pvid, 0, 0 +BridgeVLAN.VLAN, config_parse_brvlan_vlan, 0, 0 +BridgeVLAN.EgressUntagged, config_parse_brvlan_untagged, 0, 0 +DHCPPrefixDelegation.UplinkInterface, config_parse_uplink, 0, 0 +DHCPPrefixDelegation.SubnetId, config_parse_dhcp_pd_subnet_id, 0, offsetof(Network, dhcp_pd_subnet_id) +DHCPPrefixDelegation.Announce, config_parse_bool, 0, offsetof(Network, dhcp_pd_announce) +DHCPPrefixDelegation.Assign, config_parse_bool, 0, offsetof(Network, dhcp_pd_assign) +DHCPPrefixDelegation.ManageTemporaryAddress, config_parse_bool, 0, offsetof(Network, dhcp_pd_manage_temporary_address) +DHCPPrefixDelegation.Token, config_parse_address_generation_type, 0, offsetof(Network, dhcp_pd_tokens) +DHCPPrefixDelegation.RouteMetric, config_parse_uint32, 0, offsetof(Network, dhcp_pd_route_metric) +DHCPPrefixDelegation.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_pd_netlabel) +DHCPPrefixDelegation.NFTSet, config_parse_nft_set, NFT_SET_PARSE_NETWORK, offsetof(Network, dhcp_pd_nft_set_context) +IPv6SendRA.RouterLifetimeSec, config_parse_router_lifetime, 0, offsetof(Network, router_lifetime_usec) +IPv6SendRA.RetransmitSec, config_parse_router_retransmit, 0, offsetof(Network, router_retransmit_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.HopLimit, config_parse_uint8, 0, offsetof(Network, router_hop_limit) +IPv6SendRA.EmitDNS, config_parse_bool, 0, offsetof(Network, router_emit_dns) +IPv6SendRA.DNS, config_parse_radv_dns, 0, 0 +IPv6SendRA.EmitDomains, config_parse_bool, 0, offsetof(Network, router_emit_domains) +IPv6SendRA.Domains, config_parse_radv_search_domains, 0, 0 +IPv6SendRA.DNSLifetimeSec, config_parse_sec, 0, offsetof(Network, router_dns_lifetime_usec) +IPv6SendRA.UplinkInterface, config_parse_uplink, 0, 0 +IPv6SendRA.HomeAgent, config_parse_bool, 0, offsetof(Network, router_home_agent_information) +IPv6SendRA.HomeAgentLifetimeSec, config_parse_router_home_agent_lifetime, 0, offsetof(Network, home_agent_lifetime_usec) +IPv6SendRA.HomeAgentPreference, config_parse_uint16, 0, offsetof(Network, router_home_agent_preference) +IPv6Prefix.Prefix, config_parse_prefix, 0, 0 +IPv6Prefix.OnLink, config_parse_prefix_boolean, 0, 0 +IPv6Prefix.AddressAutoconfiguration, config_parse_prefix_boolean, 0, 0 +IPv6Prefix.ValidLifetimeSec, config_parse_prefix_lifetime, 0, 0 +IPv6Prefix.PreferredLifetimeSec, config_parse_prefix_lifetime, 0, 0 +IPv6Prefix.Assign, config_parse_prefix_boolean, 0, 0 +IPv6Prefix.RouteMetric, config_parse_prefix_metric, 0, 0 +IPv6Prefix.Token, config_parse_prefix_token, 0, 0 +IPv6RoutePrefix.Route, config_parse_route_prefix, 0, 0 +IPv6RoutePrefix.LifetimeSec, config_parse_route_prefix_lifetime, 0, 0 +IPv6PREF64Prefix.Prefix, config_parse_pref64_prefix, 0, 0 +IPv6PREF64Prefix.LifetimeSec, config_parse_pref64_prefix_lifetime, 0, 0 +LLDP.MUDURL, config_parse_mud_url, 0, offsetof(Network, lldp_mudurl) +CAN.BitRate, config_parse_can_bitrate, 0, offsetof(Network, can_bitrate) +CAN.SamplePoint, config_parse_permille, 0, offsetof(Network, can_sample_point) +CAN.TimeQuantaNSec, config_parse_can_time_quanta, 0, offsetof(Network, can_time_quanta_ns) +CAN.PropagationSegment, config_parse_uint32, 0, offsetof(Network, can_propagation_segment) +CAN.PhaseBufferSegment1, config_parse_uint32, 0, offsetof(Network, can_phase_buffer_segment_1) +CAN.PhaseBufferSegment2, config_parse_uint32, 0, offsetof(Network, can_phase_buffer_segment_2) +CAN.SyncJumpWidth, config_parse_uint32, 0, offsetof(Network, can_sync_jump_width) +CAN.DataBitRate, config_parse_can_bitrate, 0, offsetof(Network, can_data_bitrate) +CAN.DataSamplePoint, config_parse_permille, 0, offsetof(Network, can_data_sample_point) +CAN.DataTimeQuantaNSec, config_parse_can_time_quanta, 0, offsetof(Network, can_data_time_quanta_ns) +CAN.DataPropagationSegment, config_parse_uint32, 0, offsetof(Network, can_data_propagation_segment) +CAN.DataPhaseBufferSegment1, config_parse_uint32, 0, offsetof(Network, can_data_phase_buffer_segment_1) +CAN.DataPhaseBufferSegment2, config_parse_uint32, 0, offsetof(Network, can_data_phase_buffer_segment_2) +CAN.DataSyncJumpWidth, config_parse_uint32, 0, offsetof(Network, can_data_sync_jump_width) +CAN.RestartSec, config_parse_can_restart_usec, 0, offsetof(Network, can_restart_us) +CAN.Loopback, config_parse_can_control_mode, CAN_CTRLMODE_LOOPBACK, 0 +CAN.ListenOnly, config_parse_can_control_mode, CAN_CTRLMODE_LISTENONLY, 0 +CAN.TripleSampling, config_parse_can_control_mode, CAN_CTRLMODE_3_SAMPLES, 0 +CAN.OneShot, config_parse_can_control_mode, CAN_CTRLMODE_ONE_SHOT, 0 +CAN.BusErrorReporting, config_parse_can_control_mode, CAN_CTRLMODE_BERR_REPORTING, 0 +CAN.FDMode, config_parse_can_control_mode, CAN_CTRLMODE_FD, 0 +CAN.PresumeACK, config_parse_can_control_mode, CAN_CTRLMODE_PRESUME_ACK, 0 +CAN.FDNonISO, config_parse_can_control_mode, CAN_CTRLMODE_FD_NON_ISO, 0 +CAN.ClassicDataLengthCode, config_parse_can_control_mode, CAN_CTRLMODE_CC_LEN8_DLC, 0 +CAN.Termination, config_parse_can_termination, 0, 0 +IPoIB.Mode, config_parse_ipoib_mode, 0, offsetof(Network, ipoib_mode) +IPoIB.IgnoreUserspaceMulticastGroups, config_parse_tristate, 0, offsetof(Network, ipoib_umcast) +QDisc.Parent, config_parse_qdisc_parent, _QDISC_KIND_INVALID, 0 +QDisc.Handle, config_parse_qdisc_handle, _QDISC_KIND_INVALID, 0 +BFIFO.Parent, config_parse_qdisc_parent, QDISC_KIND_BFIFO, 0 +BFIFO.Handle, config_parse_qdisc_handle, QDISC_KIND_BFIFO, 0 +BFIFO.LimitBytes, config_parse_bfifo_size, QDISC_KIND_BFIFO, 0 +CAKE.Parent, config_parse_qdisc_parent, QDISC_KIND_CAKE, 0 +CAKE.Handle, config_parse_qdisc_handle, QDISC_KIND_CAKE, 0 +CAKE.Bandwidth, config_parse_cake_bandwidth, QDISC_KIND_CAKE, 0 +CAKE.AutoRateIngress, config_parse_cake_tristate, QDISC_KIND_CAKE, 0 +CAKE.OverheadBytes, config_parse_cake_overhead, QDISC_KIND_CAKE, 0 +CAKE.MPUBytes, config_parse_cake_mpu, QDISC_KIND_CAKE, 0 +CAKE.CompensationMode, config_parse_cake_compensation_mode, QDISC_KIND_CAKE, 0 +CAKE.UseRawPacketSize, config_parse_cake_tristate, QDISC_KIND_CAKE, 0 +CAKE.FlowIsolationMode, config_parse_cake_flow_isolation_mode, QDISC_KIND_CAKE, 0 +CAKE.NAT, config_parse_cake_tristate, QDISC_KIND_CAKE, 0 +CAKE.PriorityQueueingPreset, config_parse_cake_priority_queueing_preset, QDISC_KIND_CAKE, 0 +CAKE.FirewallMark, config_parse_cake_fwmark, QDISC_KIND_CAKE, 0 +CAKE.Wash, config_parse_cake_tristate, QDISC_KIND_CAKE, 0 +CAKE.SplitGSO, config_parse_cake_tristate, QDISC_KIND_CAKE, 0 +CAKE.RTTSec, config_parse_cake_rtt, QDISC_KIND_CAKE, 0 +CAKE.AckFilter, config_parse_cake_ack_filter, QDISC_KIND_CAKE, 0 +ControlledDelay.Parent, config_parse_qdisc_parent, QDISC_KIND_CODEL, 0 +ControlledDelay.Handle, config_parse_qdisc_handle, QDISC_KIND_CODEL, 0 +ControlledDelay.PacketLimit, config_parse_controlled_delay_u32, QDISC_KIND_CODEL, 0 +ControlledDelay.TargetSec, config_parse_controlled_delay_usec, QDISC_KIND_CODEL, 0 +ControlledDelay.IntervalSec, config_parse_controlled_delay_usec, QDISC_KIND_CODEL, 0 +ControlledDelay.CEThresholdSec, config_parse_controlled_delay_usec, QDISC_KIND_CODEL, 0 +ControlledDelay.ECN, config_parse_controlled_delay_bool, QDISC_KIND_CODEL, 0 +DeficitRoundRobinScheduler.Parent, config_parse_qdisc_parent, QDISC_KIND_DRR, 0 +DeficitRoundRobinScheduler.Handle, config_parse_qdisc_handle, QDISC_KIND_DRR, 0 +DeficitRoundRobinSchedulerClass.Parent, config_parse_tclass_parent, TCLASS_KIND_DRR, 0 +DeficitRoundRobinSchedulerClass.ClassId, config_parse_tclass_classid, TCLASS_KIND_DRR, 0 +DeficitRoundRobinSchedulerClass.QuantumBytes, config_parse_drr_size, TCLASS_KIND_DRR, 0 +EnhancedTransmissionSelection.Parent, config_parse_qdisc_parent, QDISC_KIND_ETS, 0 +EnhancedTransmissionSelection.Handle, config_parse_qdisc_handle, QDISC_KIND_ETS, 0 +EnhancedTransmissionSelection.Bands, config_parse_ets_u8, QDISC_KIND_ETS, 0 +EnhancedTransmissionSelection.StrictBands, config_parse_ets_u8, QDISC_KIND_ETS, 0 +EnhancedTransmissionSelection.QuantumBytes, config_parse_ets_quanta, QDISC_KIND_ETS, 0 +EnhancedTransmissionSelection.PriorityMap, config_parse_ets_prio, QDISC_KIND_ETS, 0 +PFIFO.Parent, config_parse_qdisc_parent, QDISC_KIND_PFIFO, 0 +PFIFO.Handle, config_parse_qdisc_handle, QDISC_KIND_PFIFO, 0 +PFIFO.PacketLimit, config_parse_pfifo_size, QDISC_KIND_PFIFO, 0 +PFIFOFast.Parent, config_parse_qdisc_parent, QDISC_KIND_PFIFO_FAST, 0 +PFIFOFast.Handle, config_parse_qdisc_handle, QDISC_KIND_PFIFO_FAST, 0 +PFIFOHeadDrop.Parent, config_parse_qdisc_parent, QDISC_KIND_PFIFO_HEAD_DROP, 0 +PFIFOHeadDrop.Handle, config_parse_qdisc_handle, QDISC_KIND_PFIFO_HEAD_DROP, 0 +PFIFOHeadDrop.PacketLimit, config_parse_pfifo_size, QDISC_KIND_PFIFO_HEAD_DROP, 0 +QuickFairQueueing.Parent, config_parse_qdisc_parent, QDISC_KIND_QFQ, 0 +QuickFairQueueing.Handle, config_parse_qdisc_handle, QDISC_KIND_QFQ, 0 +QuickFairQueueingClass.Parent, config_parse_tclass_parent, TCLASS_KIND_QFQ, 0 +QuickFairQueueingClass.ClassId, config_parse_tclass_classid, TCLASS_KIND_QFQ, 0 +QuickFairQueueingClass.Weight, config_parse_quick_fair_queueing_weight, TCLASS_KIND_QFQ, 0 +QuickFairQueueingClass.MaxPacketBytes, config_parse_quick_fair_queueing_max_packet, TCLASS_KIND_QFQ, 0 +FairQueueing.Parent, config_parse_qdisc_parent, QDISC_KIND_FQ, 0 +FairQueueing.Handle, config_parse_qdisc_handle, QDISC_KIND_FQ, 0 +FairQueueing.PacketLimit, config_parse_fair_queueing_u32, QDISC_KIND_FQ, 0 +FairQueueing.FlowLimit, config_parse_fair_queueing_u32, QDISC_KIND_FQ, 0 +FairQueueing.QuantumBytes, config_parse_fair_queueing_size, QDISC_KIND_FQ, 0 +FairQueueing.InitialQuantumBytes, config_parse_fair_queueing_size, QDISC_KIND_FQ, 0 +FairQueueing.MaximumRate, config_parse_fair_queueing_max_rate, QDISC_KIND_FQ, 0 +FairQueueing.Buckets, config_parse_fair_queueing_u32, QDISC_KIND_FQ, 0 +FairQueueing.OrphanMask, config_parse_fair_queueing_u32, QDISC_KIND_FQ, 0 +FairQueueing.Pacing, config_parse_fair_queueing_bool, QDISC_KIND_FQ, 0 +FairQueueing.CEThresholdSec, config_parse_fair_queueing_usec, QDISC_KIND_FQ, 0 +FairQueueingControlledDelay.Parent, config_parse_qdisc_parent, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.Handle, config_parse_qdisc_handle, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.PacketLimit, config_parse_fair_queueing_controlled_delay_u32, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.MemoryLimitBytes, config_parse_fair_queueing_controlled_delay_size, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.Flows, config_parse_fair_queueing_controlled_delay_u32, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.QuantumBytes, config_parse_fair_queueing_controlled_delay_size, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.TargetSec, config_parse_fair_queueing_controlled_delay_usec, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.IntervalSec, config_parse_fair_queueing_controlled_delay_usec, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.CEThresholdSec, config_parse_fair_queueing_controlled_delay_usec, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.ECN, config_parse_fair_queueing_controlled_delay_bool, QDISC_KIND_FQ_CODEL, 0 +FlowQueuePIE.Parent, config_parse_qdisc_parent, QDISC_KIND_FQ_PIE, 0 +FlowQueuePIE.Handle, config_parse_qdisc_handle, QDISC_KIND_FQ_PIE, 0 +FlowQueuePIE.PacketLimit, config_parse_fq_pie_packet_limit, QDISC_KIND_FQ_PIE, 0 +GenericRandomEarlyDetection.Parent, config_parse_qdisc_parent, QDISC_KIND_GRED, 0 +GenericRandomEarlyDetection.Handle, config_parse_qdisc_handle, QDISC_KIND_GRED, 0 +GenericRandomEarlyDetection.VirtualQueues, config_parse_generic_random_early_detection_u32, QDISC_KIND_GRED, 0 +GenericRandomEarlyDetection.DefaultVirtualQueue, config_parse_generic_random_early_detection_u32, QDISC_KIND_GRED, 0 +GenericRandomEarlyDetection.GenericRIO, config_parse_generic_random_early_detection_bool, QDISC_KIND_GRED, 0 +HeavyHitterFilter.Parent, config_parse_qdisc_parent, QDISC_KIND_HHF, 0 +HeavyHitterFilter.Handle, config_parse_qdisc_handle, QDISC_KIND_HHF, 0 +HeavyHitterFilter.PacketLimit, config_parse_heavy_hitter_filter_packet_limit, QDISC_KIND_HHF, 0 +HierarchyTokenBucket.Parent, config_parse_qdisc_parent, QDISC_KIND_HTB, 0 +HierarchyTokenBucket.Handle, config_parse_qdisc_handle, QDISC_KIND_HTB, 0 +HierarchyTokenBucket.DefaultClass, config_parse_hierarchy_token_bucket_default_class, QDISC_KIND_HTB, 0 +HierarchyTokenBucket.RateToQuantum, config_parse_hierarchy_token_bucket_u32, QDISC_KIND_HTB, 0 +HierarchyTokenBucketClass.Parent, config_parse_tclass_parent, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.ClassId, config_parse_tclass_classid, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.Priority, config_parse_hierarchy_token_bucket_class_u32, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.QuantumBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.MTUBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.OverheadBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.Rate, config_parse_hierarchy_token_bucket_class_rate, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.CeilRate, config_parse_hierarchy_token_bucket_class_rate, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.BufferBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0 +HierarchyTokenBucketClass.CeilBufferBytes, config_parse_hierarchy_token_bucket_class_size, TCLASS_KIND_HTB, 0 +NetworkEmulator.Parent, config_parse_qdisc_parent, QDISC_KIND_NETEM, 0 +NetworkEmulator.Handle, config_parse_qdisc_handle, QDISC_KIND_NETEM, 0 +NetworkEmulator.DelaySec, config_parse_network_emulator_delay, QDISC_KIND_NETEM, 0 +NetworkEmulator.DelayJitterSec, config_parse_network_emulator_delay, QDISC_KIND_NETEM, 0 +NetworkEmulator.LossRate, config_parse_network_emulator_rate, QDISC_KIND_NETEM, 0 +NetworkEmulator.DuplicateRate, config_parse_network_emulator_rate, QDISC_KIND_NETEM, 0 +NetworkEmulator.PacketLimit, config_parse_network_emulator_packet_limit, QDISC_KIND_NETEM, 0 +PIE.Parent, config_parse_qdisc_parent, QDISC_KIND_PIE, 0 +PIE.Handle, config_parse_qdisc_handle, QDISC_KIND_PIE, 0 +PIE.PacketLimit, config_parse_pie_packet_limit, QDISC_KIND_PIE, 0 +StochasticFairBlue.Parent, config_parse_qdisc_parent, QDISC_KIND_SFB, 0 +StochasticFairBlue.Handle, config_parse_qdisc_handle, QDISC_KIND_SFB, 0 +StochasticFairBlue.PacketLimit, config_parse_stochastic_fair_blue_u32, QDISC_KIND_SFB, 0 +StochasticFairnessQueueing.Parent, config_parse_qdisc_parent, QDISC_KIND_SFQ, 0 +StochasticFairnessQueueing.Handle, config_parse_qdisc_handle, QDISC_KIND_SFQ, 0 +StochasticFairnessQueueing.PerturbPeriodSec, config_parse_stochastic_fairness_queueing_perturb_period, QDISC_KIND_SFQ, 0 +TokenBucketFilter.Parent, config_parse_qdisc_parent, QDISC_KIND_TBF, 0 +TokenBucketFilter.Handle, config_parse_qdisc_handle, QDISC_KIND_TBF, 0 +TokenBucketFilter.Rate, config_parse_token_bucket_filter_rate, QDISC_KIND_TBF, 0 +TokenBucketFilter.BurstBytes, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0 +TokenBucketFilter.LimitBytes, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0 +TokenBucketFilter.MTUBytes, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0 +TokenBucketFilter.MPUBytes, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0 +TokenBucketFilter.PeakRate, config_parse_token_bucket_filter_rate, QDISC_KIND_TBF, 0 +TokenBucketFilter.LatencySec, config_parse_token_bucket_filter_latency, QDISC_KIND_TBF, 0 +TrivialLinkEqualizer.Parent, config_parse_qdisc_parent, QDISC_KIND_TEQL, 0 +TrivialLinkEqualizer.Handle, config_parse_qdisc_handle, QDISC_KIND_TEQL, 0 +TrivialLinkEqualizer.Id, config_parse_trivial_link_equalizer_id, QDISC_KIND_TEQL, 0 +/* backwards compatibility: do not add new entries to this section */ +Network.IPv4LL, config_parse_ipv4ll, 0, offsetof(Network, link_local) +Network.IPv6Token, config_parse_address_generation_type, 0, offsetof(Network, ndisc_tokens) +Network.IPv6PrefixDelegation, config_parse_router_prefix_delegation, 0, offsetof(Network, router_prefix_delegation) +Network.DHCPv6PrefixDelegation, config_parse_tristate, 0, offsetof(Network, dhcp_pd) +IPv6PrefixDelegation.RouterLifetimeSec, config_parse_sec, 0, offsetof(Network, router_lifetime_usec) +IPv6PrefixDelegation.Managed, config_parse_bool, 0, offsetof(Network, router_managed) +IPv6PrefixDelegation.OtherInformation, config_parse_bool, 0, offsetof(Network, router_other_information) +IPv6PrefixDelegation.RouterPreference, config_parse_router_preference, 0, 0 +IPv6PrefixDelegation.EmitDNS, config_parse_bool, 0, offsetof(Network, router_emit_dns) +IPv6PrefixDelegation.DNS, config_parse_radv_dns, 0, 0 +IPv6PrefixDelegation.EmitDomains, config_parse_bool, 0, offsetof(Network, router_emit_domains) +IPv6PrefixDelegation.Domains, config_parse_radv_search_domains, 0, 0 +IPv6PrefixDelegation.DNSLifetimeSec, config_parse_sec, 0, offsetof(Network, router_dns_lifetime_usec) +DHCPv4.BlackList, config_parse_in_addr_prefixes, AF_INET, offsetof(Network, dhcp_deny_listed_ip) +DHCP.ClientIdentifier, config_parse_dhcp_client_identifier, 0, offsetof(Network, dhcp_client_identifier) +DHCP.UseDNS, config_parse_dhcp_use_dns, AF_UNSPEC, 0 +DHCP.UseNTP, config_parse_dhcp_use_ntp, AF_UNSPEC, 0 +DHCP.UseMTU, config_parse_bool, 0, offsetof(Network, dhcp_use_mtu) +DHCP.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp_use_hostname) +DHCP.UseDomains, config_parse_dhcp_use_domains, AF_UNSPEC, 0 +DHCP.UseDomainName, config_parse_dhcp_use_domains, AF_UNSPEC, 0 +DHCP.UseRoutes, config_parse_bool, 0, offsetof(Network, dhcp_use_routes) +DHCP.Anonymize, config_parse_bool, 0, offsetof(Network, dhcp_anonymize) +DHCP.SendHostname, config_parse_dhcp_send_hostname, AF_UNSPEC, 0 +DHCP.Hostname, config_parse_hostname, 0, offsetof(Network, dhcp_hostname) +DHCP.RequestBroadcast, config_parse_tristate, 0, offsetof(Network, dhcp_broadcast) +DHCP.CriticalConnection, config_parse_tristate, 0, offsetof(Network, dhcp_critical) +DHCP.VendorClassIdentifier, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_vendor_class_identifier) +DHCP.UserClass, config_parse_dhcp_user_or_vendor_class, AF_INET, offsetof(Network, dhcp_user_class) +DHCP.IAID, config_parse_iaid, AF_INET, 0 +DHCP.DUIDType, config_parse_network_duid_type, 0, 0 +DHCP.DUIDRawData, config_parse_network_duid_rawdata, 0, 0 +DHCP.RouteMetric, config_parse_dhcp_route_metric, AF_UNSPEC, 0 +DHCP.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET, 0 +DHCP.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone) +DHCP.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port) +DHCP.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp6_use_rapid_commit) +DHCP.ForceDHCPv6PDOtherInformation, config_parse_warn_compat, DISABLED_LEGACY, 0 +DHCPv4.UseDomainName, config_parse_dhcp_use_domains, AF_INET, 0 +DHCPv4.CriticalConnection, config_parse_tristate, 0, offsetof(Network, dhcp_critical) +DHCPv6.RouteMetric, config_parse_ipv6_accept_ra_route_metric, AF_INET6, 0 +DHCPv6.ForceDHCPv6PDOtherInformation, config_parse_warn_compat, DISABLED_LEGACY, 0 +DHCPv6PrefixDelegation.SubnetId, config_parse_dhcp_pd_subnet_id, 0, offsetof(Network, dhcp_pd_subnet_id) +DHCPv6PrefixDelegation.Announce, config_parse_bool, 0, offsetof(Network, dhcp_pd_announce) +DHCPv6PrefixDelegation.Assign, config_parse_bool, 0, offsetof(Network, dhcp_pd_assign) +DHCPv6PrefixDelegation.ManageTemporaryAddress, config_parse_bool, 0, offsetof(Network, dhcp_pd_manage_temporary_address) +DHCPv6PrefixDelegation.Token, config_parse_address_generation_type, 0, offsetof(Network, dhcp_pd_tokens) +DHCPv6PrefixDelegation.RouteMetric, config_parse_uint32, 0, offsetof(Network, dhcp_pd_route_metric) +IPv6AcceptRA.DenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_prefix) +IPv6AcceptRA.BlackList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_prefix) +TrafficControlQueueingDiscipline.Parent, config_parse_qdisc_parent, _QDISC_KIND_INVALID, 0 +TrafficControlQueueingDiscipline.NetworkEmulatorDelaySec, config_parse_network_emulator_delay, 0, 0 +TrafficControlQueueingDiscipline.NetworkEmulatorDelayJitterSec, config_parse_network_emulator_delay, 0, 0 +TrafficControlQueueingDiscipline.NetworkEmulatorLossRate, config_parse_network_emulator_rate, 0, 0 +TrafficControlQueueingDiscipline.NetworkEmulatorDuplicateRate, config_parse_network_emulator_rate, 0, 0 +TrafficControlQueueingDiscipline.NetworkEmulatorPacketLimit, config_parse_network_emulator_packet_limit, 0, 0 +FairQueueing.Quantum, config_parse_fair_queueing_size, QDISC_KIND_FQ, 0 +FairQueueing.InitialQuantum, config_parse_fair_queueing_size, QDISC_KIND_FQ, 0 +FairQueueingControlledDelay.MemoryLimit, config_parse_fair_queueing_controlled_delay_size, QDISC_KIND_FQ_CODEL, 0 +FairQueueingControlledDelay.Quantum, config_parse_fair_queueing_controlled_delay_size, QDISC_KIND_FQ_CODEL, 0 +TokenBucketFilter.Burst, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0 +TokenBucketFilter.LimitSize, config_parse_token_bucket_filter_size, QDISC_KIND_TBF, 0 diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c new file mode 100644 index 0000000..dcd3e5a --- /dev/null +++ b/src/network/networkd-network.c @@ -0,0 +1,1349 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <linux/netdevice.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "conf-files.h" +#include "conf-parser.h" +#include "dns-domain.h" +#include "fd-util.h" +#include "hostname-util.h" +#include "in-addr-util.h" +#include "net-condition.h" +#include "netdev/macvlan.h" +#include "networkd-address-label.h" +#include "networkd-address.h" +#include "networkd-bridge-fdb.h" +#include "networkd-bridge-mdb.h" +#include "networkd-dhcp-common.h" +#include "networkd-dhcp-server-static-lease.h" +#include "networkd-ipv6-proxy-ndp.h" +#include "networkd-manager.h" +#include "networkd-ndisc.h" +#include "networkd-neighbor.h" +#include "networkd-network.h" +#include "networkd-nexthop.h" +#include "networkd-radv.h" +#include "networkd-route.h" +#include "networkd-routing-policy-rule.h" +#include "networkd-sriov.h" +#include "parse-util.h" +#include "path-lookup.h" +#include "qdisc.h" +#include "radv-internal.h" +#include "set.h" +#include "socket-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "tclass.h" + +/* Let's assume that anything above this number is a user misconfiguration. */ +#define MAX_NTP_SERVERS 128U + +static int network_resolve_netdev_one(Network *network, const char *name, NetDevKind kind, NetDev **ret) { + const char *kind_string; + NetDev *netdev; + int r; + + /* For test-networkd-conf, the check must be earlier than the assertions. */ + if (!name) + return 0; + + assert(network); + assert(network->manager); + assert(network->filename); + assert(ret); + + if (kind == _NETDEV_KIND_TUNNEL) + kind_string = "tunnel"; + else { + kind_string = netdev_kind_to_string(kind); + if (!kind_string) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Invalid NetDev kind of %s, ignoring assignment.", + network->filename, name); + } + + r = netdev_get(network->manager, name, &netdev); + if (r < 0) + return log_warning_errno(r, "%s: %s NetDev could not be found, ignoring assignment.", + network->filename, name); + + if (netdev->kind != kind && !(kind == _NETDEV_KIND_TUNNEL && + IN_SET(netdev->kind, + NETDEV_KIND_ERSPAN, + NETDEV_KIND_GRE, + NETDEV_KIND_GRETAP, + NETDEV_KIND_IP6GRE, + NETDEV_KIND_IP6GRETAP, + NETDEV_KIND_IP6TNL, + NETDEV_KIND_IPIP, + NETDEV_KIND_SIT, + NETDEV_KIND_VTI, + NETDEV_KIND_VTI6))) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: NetDev %s is not a %s, ignoring assignment", + network->filename, name, kind_string); + + *ret = netdev_ref(netdev); + return 1; +} + +static int network_resolve_stacked_netdevs(Network *network) { + void *name, *kind; + int r; + + assert(network); + + HASHMAP_FOREACH_KEY(kind, name, network->stacked_netdev_names) { + _cleanup_(netdev_unrefp) NetDev *netdev = NULL; + + if (network_resolve_netdev_one(network, name, PTR_TO_INT(kind), &netdev) <= 0) + continue; + + r = hashmap_ensure_put(&network->stacked_netdevs, &string_hash_ops, netdev->ifname, netdev); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + log_warning_errno(r, "%s: Failed to add NetDev '%s' to network, ignoring: %m", + network->filename, (const char *) name); + + netdev = NULL; + } + + return 0; +} + +int network_verify(Network *network) { + int r; + + assert(network); + assert(network->manager); + assert(network->filename); + + if (net_match_is_empty(&network->match) && !network->conditions) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: No valid settings found in the [Match] section, ignoring file. " + "To match all interfaces, add Name=* in the [Match] section.", + network->filename); + + /* skip out early if configuration does not match the environment */ + if (!condition_test_list(network->conditions, environ, NULL, NULL, NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Conditions in the file do not match the system environment, skipping.", + network->filename); + + if (network->keep_master) { + if (network->batadv_name) + log_warning("%s: BatmanAdvanced= set with KeepMaster= enabled, ignoring BatmanAdvanced=.", + network->filename); + if (network->bond_name) + log_warning("%s: Bond= set with KeepMaster= enabled, ignoring Bond=.", + network->filename); + if (network->bridge_name) + log_warning("%s: Bridge= set with KeepMaster= enabled, ignoring Bridge=.", + network->filename); + if (network->vrf_name) + log_warning("%s: VRF= set with KeepMaster= enabled, ignoring VRF=.", + network->filename); + + network->batadv_name = mfree(network->batadv_name); + network->bond_name = mfree(network->bond_name); + network->bridge_name = mfree(network->bridge_name); + network->vrf_name = mfree(network->vrf_name); + } + + (void) network_resolve_netdev_one(network, network->batadv_name, NETDEV_KIND_BATADV, &network->batadv); + (void) network_resolve_netdev_one(network, network->bond_name, NETDEV_KIND_BOND, &network->bond); + (void) network_resolve_netdev_one(network, network->bridge_name, NETDEV_KIND_BRIDGE, &network->bridge); + (void) network_resolve_netdev_one(network, network->vrf_name, NETDEV_KIND_VRF, &network->vrf); + r = network_resolve_stacked_netdevs(network); + if (r < 0) + return r; + + /* Free unnecessary entries. */ + network->batadv_name = mfree(network->batadv_name); + network->bond_name = mfree(network->bond_name); + network->bridge_name = mfree(network->bridge_name); + network->vrf_name = mfree(network->vrf_name); + network->stacked_netdev_names = hashmap_free_free_key(network->stacked_netdev_names); + + if (network->bond) { + /* Bonding slave does not support addressing. */ + if (network->link_local >= 0 && network->link_local != ADDRESS_FAMILY_NO) { + log_warning("%s: Cannot enable LinkLocalAddressing= when Bond= is specified, disabling LinkLocalAddressing=.", + network->filename); + network->link_local = ADDRESS_FAMILY_NO; + } + if (!ordered_hashmap_isempty(network->addresses_by_section)) + log_warning("%s: Cannot set addresses when Bond= is specified, ignoring addresses.", + network->filename); + if (!hashmap_isempty(network->routes_by_section)) + log_warning("%s: Cannot set routes when Bond= is specified, ignoring routes.", + network->filename); + + network->addresses_by_section = ordered_hashmap_free_with_destructor(network->addresses_by_section, address_free); + network->routes_by_section = hashmap_free_with_destructor(network->routes_by_section, route_free); + } + + if (network->link_local < 0) { + network->link_local = ADDRESS_FAMILY_IPV6; + + if (network->keep_master || network->bridge) + network->link_local = ADDRESS_FAMILY_NO; + else { + NetDev *netdev; + + HASHMAP_FOREACH(netdev, network->stacked_netdevs) { + MacVlan *m; + + if (netdev->kind == NETDEV_KIND_MACVLAN) + m = MACVLAN(netdev); + else if (netdev->kind == NETDEV_KIND_MACVTAP) + m = MACVTAP(netdev); + else + continue; + + if (m->mode == NETDEV_MACVLAN_MODE_PASSTHRU) + network->link_local = ADDRESS_FAMILY_NO; + + /* There won't be a passthru MACVLAN/MACVTAP if there's already one in another mode */ + break; + } + } + } + + if (network->ipv6ll_address_gen_mode == IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE) + SET_FLAG(network->link_local, ADDRESS_FAMILY_IPV6, false); + + if (in6_addr_is_set(&network->ipv6ll_stable_secret) && + network->ipv6ll_address_gen_mode < 0) + network->ipv6ll_address_gen_mode = IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY; + + /* IPMasquerade implies IPForward */ + network->ip_forward |= network->ip_masquerade; + + network_adjust_ipv6_proxy_ndp(network); + network_adjust_ipv6_accept_ra(network); + network_adjust_dhcp(network); + network_adjust_radv(network); + network_adjust_bridge_vlan(network); + + if (network->mtu > 0 && network->dhcp_use_mtu) { + log_warning("%s: MTUBytes= in [Link] section and UseMTU= in [DHCP] section are set. " + "Disabling UseMTU=.", network->filename); + network->dhcp_use_mtu = false; + } + + if (network->dhcp_critical >= 0) { + if (network->keep_configuration >= 0) { + if (network->manager->keep_configuration < 0) + log_warning("%s: Both KeepConfiguration= and deprecated CriticalConnection= are set. " + "Ignoring CriticalConnection=.", network->filename); + } else if (network->dhcp_critical) + /* CriticalConnection=yes also preserve foreign static configurations. */ + network->keep_configuration = KEEP_CONFIGURATION_YES; + else + network->keep_configuration = KEEP_CONFIGURATION_NO; + } + + if (!strv_isempty(network->bind_carrier)) { + if (!IN_SET(network->activation_policy, _ACTIVATION_POLICY_INVALID, ACTIVATION_POLICY_BOUND)) + log_warning("%s: ActivationPolicy=bound is required with BindCarrier=. " + "Setting ActivationPolicy=bound.", network->filename); + network->activation_policy = ACTIVATION_POLICY_BOUND; + } else if (network->activation_policy == ACTIVATION_POLICY_BOUND) { + log_warning("%s: ActivationPolicy=bound requires BindCarrier=. " + "Ignoring ActivationPolicy=bound.", network->filename); + network->activation_policy = ACTIVATION_POLICY_UP; + } + + if (network->activation_policy == _ACTIVATION_POLICY_INVALID) + network->activation_policy = ACTIVATION_POLICY_UP; + + if (network->activation_policy == ACTIVATION_POLICY_ALWAYS_UP) { + if (network->ignore_carrier_loss_set && network->ignore_carrier_loss_usec < USEC_INFINITY) + log_warning("%s: IgnoreCarrierLoss=no or finite timespan conflicts with ActivationPolicy=always-up. " + "Setting IgnoreCarrierLoss=yes.", network->filename); + network->ignore_carrier_loss_set = true; + network->ignore_carrier_loss_usec = USEC_INFINITY; + } + + if (!network->ignore_carrier_loss_set) /* Set implied default. */ + network->ignore_carrier_loss_usec = network->configure_without_carrier ? USEC_INFINITY : 0; + + if (IN_SET(network->activation_policy, ACTIVATION_POLICY_DOWN, ACTIVATION_POLICY_ALWAYS_DOWN, ACTIVATION_POLICY_MANUAL)) { + if (network->required_for_online < 0 || + (network->required_for_online == true && network->activation_policy == ACTIVATION_POLICY_ALWAYS_DOWN)) { + log_debug("%s: Setting RequiredForOnline=no because ActivationPolicy=%s.", network->filename, + activation_policy_to_string(network->activation_policy)); + network->required_for_online = false; + } else if (network->required_for_online == true) + log_warning("%s: RequiredForOnline=yes and ActivationPolicy=%s, " + "this may cause a delay at boot.", network->filename, + activation_policy_to_string(network->activation_policy)); + } + + if (network->required_for_online < 0) + network->required_for_online = true; + + if (network->keep_configuration < 0) + network->keep_configuration = KEEP_CONFIGURATION_NO; + + if (network->ipv6_proxy_ndp == 0 && !set_isempty(network->ipv6_proxy_ndp_addresses)) { + log_warning("%s: IPv6ProxyNDP= is disabled. Ignoring IPv6ProxyNDPAddress=.", network->filename); + network->ipv6_proxy_ndp_addresses = set_free_free(network->ipv6_proxy_ndp_addresses); + } + + r = network_drop_invalid_addresses(network); + if (r < 0) + return r; /* network_drop_invalid_addresses() logs internally. */ + network_drop_invalid_routes(network); + network_drop_invalid_nexthops(network); + network_drop_invalid_bridge_fdb_entries(network); + network_drop_invalid_bridge_mdb_entries(network); + r = network_drop_invalid_neighbors(network); + if (r < 0) + return r; + network_drop_invalid_address_labels(network); + network_drop_invalid_prefixes(network); + network_drop_invalid_route_prefixes(network); + network_drop_invalid_routing_policy_rules(network); + network_drop_invalid_qdisc(network); + network_drop_invalid_tclass(network); + r = sr_iov_drop_invalid_sections(UINT32_MAX, network->sr_iov_by_section); + if (r < 0) + return r; /* sr_iov_drop_invalid_sections() logs internally. */ + network_drop_invalid_static_leases(network); + + return 0; +} + +int network_load_one(Manager *manager, OrderedHashmap **networks, const char *filename) { + _cleanup_free_ char *fname = NULL, *name = NULL; + _cleanup_(network_unrefp) Network *network = NULL; + const char *dropin_dirname; + char *d; + int r; + + assert(manager); + assert(filename); + + r = null_or_empty_path(filename); + if (r < 0) + return log_warning_errno(r, "Failed to check if \"%s\" is empty: %m", filename); + if (r > 0) { + log_debug("Skipping empty file: %s", filename); + return 0; + } + + fname = strdup(filename); + if (!fname) + return log_oom(); + + name = strdup(basename(filename)); + if (!name) + return log_oom(); + + d = strrchr(name, '.'); + if (!d) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid file name: %s", filename); + + *d = '\0'; + + dropin_dirname = strjoina(name, ".network.d"); + + network = new(Network, 1); + if (!network) + return log_oom(); + + *network = (Network) { + .filename = TAKE_PTR(fname), + .name = TAKE_PTR(name), + + .manager = manager, + .n_ref = 1, + + .required_for_online = -1, + .required_operstate_for_online = LINK_OPERSTATE_RANGE_DEFAULT, + .activation_policy = _ACTIVATION_POLICY_INVALID, + .group = -1, + .arp = -1, + .multicast = -1, + .allmulticast = -1, + .promiscuous = -1, + + .keep_configuration = manager->keep_configuration, + + .dhcp_duid.type = _DUID_TYPE_INVALID, + .dhcp_critical = -1, + .dhcp_use_ntp = true, + .dhcp_routes_to_ntp = true, + .dhcp_use_sip = true, + .dhcp_use_captive_portal = true, + .dhcp_use_dns = true, + .dhcp_routes_to_dns = true, + .dhcp_use_hostname = true, + .dhcp_use_routes = true, + .dhcp_use_gateway = -1, + .dhcp_send_hostname = true, + .dhcp_send_release = true, + .dhcp_route_metric = DHCP_ROUTE_METRIC, + .dhcp_use_rapid_commit = -1, + .dhcp_client_identifier = _DHCP_CLIENT_ID_INVALID, + .dhcp_route_table = RT_TABLE_MAIN, + .dhcp_ip_service_type = -1, + .dhcp_broadcast = -1, + .dhcp_ipv6_only_mode = -1, + + .dhcp6_use_address = true, + .dhcp6_use_pd_prefix = true, + .dhcp6_use_dns = true, + .dhcp6_use_hostname = true, + .dhcp6_use_ntp = true, + .dhcp6_use_captive_portal = true, + .dhcp6_use_rapid_commit = true, + .dhcp6_send_hostname = true, + .dhcp6_duid.type = _DUID_TYPE_INVALID, + .dhcp6_client_start_mode = _DHCP6_CLIENT_START_MODE_INVALID, + .dhcp6_send_release = true, + + .dhcp_pd = -1, + .dhcp_pd_announce = true, + .dhcp_pd_assign = true, + .dhcp_pd_manage_temporary_address = true, + .dhcp_pd_subnet_id = -1, + .dhcp_pd_route_metric = DHCP6PD_ROUTE_METRIC, + + .dhcp_server_bind_to_interface = true, + .dhcp_server_emit[SD_DHCP_LEASE_DNS].emit = true, + .dhcp_server_emit[SD_DHCP_LEASE_NTP].emit = true, + .dhcp_server_emit[SD_DHCP_LEASE_SIP].emit = true, + .dhcp_server_emit_router = true, + .dhcp_server_emit_timezone = true, + .dhcp_server_rapid_commit = true, + + .router_lifetime_usec = RADV_DEFAULT_ROUTER_LIFETIME_USEC, + .router_dns_lifetime_usec = RADV_DEFAULT_VALID_LIFETIME_USEC, + .router_emit_dns = true, + .router_emit_domains = true, + + .use_bpdu = -1, + .hairpin = -1, + .isolated = -1, + .fast_leave = -1, + .allow_port_to_be_root = -1, + .unicast_flood = -1, + .multicast_flood = -1, + .multicast_to_unicast = -1, + .neighbor_suppression = -1, + .learning = -1, + .bridge_proxy_arp = -1, + .bridge_proxy_arp_wifi = -1, + .priority = LINK_BRIDGE_PORT_PRIORITY_INVALID, + .multicast_router = _MULTICAST_ROUTER_INVALID, + + .lldp_mode = LLDP_MODE_ROUTERS_ONLY, + .lldp_multicast_mode = _SD_LLDP_MULTICAST_MODE_INVALID, + + .dns_default_route = -1, + .llmnr = RESOLVE_SUPPORT_YES, + .mdns = RESOLVE_SUPPORT_NO, + .dnssec_mode = _DNSSEC_MODE_INVALID, + .dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID, + + /* If LinkLocalAddressing= is not set, then set to ADDRESS_FAMILY_IPV6 later. */ + .link_local = _ADDRESS_FAMILY_INVALID, + .ipv6ll_address_gen_mode = _IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_INVALID, + + .ipv4_accept_local = -1, + .ipv4_route_localnet = -1, + .ipv6_privacy_extensions = _IPV6_PRIVACY_EXTENSIONS_INVALID, + .ipv6_dad_transmits = -1, + .ipv6_proxy_ndp = -1, + .proxy_arp = -1, + .ipv4_rp_filter = _IP_REVERSE_PATH_FILTER_INVALID, + + .ipv6_accept_ra = -1, + .ipv6_accept_ra_use_dns = true, + .ipv6_accept_ra_use_gateway = true, + .ipv6_accept_ra_use_captive_portal = true, + .ipv6_accept_ra_use_route_prefix = true, + .ipv6_accept_ra_use_autonomous_prefix = true, + .ipv6_accept_ra_use_onlink_prefix = true, + .ipv6_accept_ra_use_mtu = true, + .ipv6_accept_ra_use_hop_limit = true, + .ipv6_accept_ra_use_icmp6_ratelimit = true, + .ipv6_accept_ra_route_table = RT_TABLE_MAIN, + .ipv6_accept_ra_route_metric_high = IPV6RA_ROUTE_METRIC_HIGH, + .ipv6_accept_ra_route_metric_medium = IPV6RA_ROUTE_METRIC_MEDIUM, + .ipv6_accept_ra_route_metric_low = IPV6RA_ROUTE_METRIC_LOW, + .ipv6_accept_ra_start_dhcp6_client = IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES, + + .can_termination = -1, + + .ipoib_mode = _IP_OVER_INFINIBAND_MODE_INVALID, + .ipoib_umcast = -1, + }; + + r = config_parse_many( + STRV_MAKE_CONST(filename), NETWORK_DIRS, dropin_dirname, /* root = */ NULL, + "Match\0" + "Link\0" + "SR-IOV\0" + "Network\0" + "Address\0" + "Neighbor\0" + "IPv6AddressLabel\0" + "RoutingPolicyRule\0" + "Route\0" + "NextHop\0" + "DHCP\0" /* compat */ + "DHCPv4\0" + "DHCPv6\0" + "DHCPv6PrefixDelegation\0" /* compat */ + "DHCPPrefixDelegation\0" + "DHCPServer\0" + "DHCPServerStaticLease\0" + "IPv6AcceptRA\0" + "IPv6NDPProxyAddress\0" + "Bridge\0" + "BridgeFDB\0" + "BridgeMDB\0" + "BridgeVLAN\0" + "IPv6SendRA\0" + "IPv6PrefixDelegation\0" + "IPv6Prefix\0" + "IPv6RoutePrefix\0" + "IPv6PREF64Prefix\0" + "LLDP\0" + "TrafficControlQueueingDiscipline\0" + "CAN\0" + "QDisc\0" + "BFIFO\0" + "CAKE\0" + "ControlledDelay\0" + "DeficitRoundRobinScheduler\0" + "DeficitRoundRobinSchedulerClass\0" + "EnhancedTransmissionSelection\0" + "FairQueueing\0" + "FairQueueingControlledDelay\0" + "FlowQueuePIE\0" + "GenericRandomEarlyDetection\0" + "HeavyHitterFilter\0" + "HierarchyTokenBucket\0" + "HierarchyTokenBucketClass\0" + "NetworkEmulator\0" + "PFIFO\0" + "PFIFOFast\0" + "PFIFOHeadDrop\0" + "PIE\0" + "QuickFairQueueing\0" + "QuickFairQueueingClass\0" + "StochasticFairBlue\0" + "StochasticFairnessQueueing\0" + "TokenBucketFilter\0" + "TrivialLinkEqualizer\0", + config_item_perf_lookup, network_network_gperf_lookup, + CONFIG_PARSE_WARN, + network, + &network->stats_by_path, + &network->dropins); + if (r < 0) + return r; /* config_parse_many() logs internally. */ + + r = network_add_ipv4ll_route(network); + if (r < 0) + return log_warning_errno(r, "%s: Failed to add IPv4LL route: %m", network->filename); + + r = network_add_default_route_on_device(network); + if (r < 0) + return log_warning_errno(r, "%s: Failed to add default route on device: %m", + network->filename); + + r = network_verify(network); + if (r < 0) + return r; /* network_verify() logs internally. */ + + r = ordered_hashmap_ensure_put(networks, &string_hash_ops, network->name, network); + if (r < 0) + return log_warning_errno(r, "%s: Failed to store configuration into hashmap: %m", filename); + + TAKE_PTR(network); + return 0; +} + +int network_load(Manager *manager, OrderedHashmap **networks) { + _cleanup_strv_free_ char **files = NULL; + int r; + + assert(manager); + + ordered_hashmap_clear_with_destructor(*networks, network_unref); + + r = conf_files_list_strv(&files, ".network", NULL, 0, NETWORK_DIRS); + if (r < 0) + return log_error_errno(r, "Failed to enumerate network files: %m"); + + STRV_FOREACH(f, files) + (void) network_load_one(manager, networks, *f); + + return 0; +} + +int network_reload(Manager *manager) { + OrderedHashmap *new_networks = NULL; + Network *n, *old; + int r; + + assert(manager); + + r = network_load(manager, &new_networks); + if (r < 0) + goto failure; + + ORDERED_HASHMAP_FOREACH(n, new_networks) { + r = network_get_by_name(manager, n->name, &old); + if (r < 0) { + log_debug("Found new .network file: %s", n->filename); + continue; + } + + if (!stats_by_path_equal(n->stats_by_path, old->stats_by_path)) { + log_debug("Found updated .network file: %s", n->filename); + continue; + } + + r = ordered_hashmap_replace(new_networks, old->name, old); + if (r < 0) + goto failure; + + network_ref(old); + network_unref(n); + } + + ordered_hashmap_free_with_destructor(manager->networks, network_unref); + manager->networks = new_networks; + + return manager_build_dhcp_pd_subnet_ids(manager); + +failure: + ordered_hashmap_free_with_destructor(new_networks, network_unref); + + return r; +} + +int manager_build_dhcp_pd_subnet_ids(Manager *manager) { + Network *n; + int r; + + assert(manager); + + set_clear(manager->dhcp_pd_subnet_ids); + + ORDERED_HASHMAP_FOREACH(n, manager->networks) { + if (n->unmanaged) + continue; + + if (!n->dhcp_pd) + continue; + + if (n->dhcp_pd_subnet_id < 0) + continue; + + r = set_ensure_put(&manager->dhcp_pd_subnet_ids, &uint64_hash_ops, &n->dhcp_pd_subnet_id); + if (r < 0) + return r; + } + + return 0; +} + +static Network *network_free(Network *network) { + if (!network) + return NULL; + + free(network->name); + free(network->filename); + free(network->description); + strv_free(network->dropins); + hashmap_free(network->stats_by_path); + + /* conditions */ + net_match_clear(&network->match); + condition_free_list(network->conditions); + + /* link settings */ + strv_free(network->bind_carrier); + + /* NTP */ + strv_free(network->ntp); + + /* DNS */ + for (unsigned i = 0; i < network->n_dns; i++) + in_addr_full_free(network->dns[i]); + free(network->dns); + ordered_set_free(network->search_domains); + ordered_set_free(network->route_domains); + set_free_free(network->dnssec_negative_trust_anchors); + + /* DHCP server */ + free(network->dhcp_server_relay_agent_circuit_id); + free(network->dhcp_server_relay_agent_remote_id); + free(network->dhcp_server_boot_server_name); + free(network->dhcp_server_boot_filename); + free(network->dhcp_server_timezone); + free(network->dhcp_server_uplink_name); + for (sd_dhcp_lease_server_type_t t = 0; t < _SD_DHCP_LEASE_SERVER_TYPE_MAX; t++) + free(network->dhcp_server_emit[t].addresses); + ordered_hashmap_free(network->dhcp_server_send_options); + ordered_hashmap_free(network->dhcp_server_send_vendor_options); + + /* DHCP client */ + free(network->dhcp_vendor_class_identifier); + free(network->dhcp_mudurl); + free(network->dhcp_hostname); + free(network->dhcp_label); + set_free(network->dhcp_deny_listed_ip); + set_free(network->dhcp_allow_listed_ip); + strv_free(network->dhcp_user_class); + set_free(network->dhcp_request_options); + ordered_hashmap_free(network->dhcp_client_send_options); + ordered_hashmap_free(network->dhcp_client_send_vendor_options); + free(network->dhcp_netlabel); + nft_set_context_clear(&network->dhcp_nft_set_context); + + /* DHCPv6 client */ + free(network->dhcp6_mudurl); + free(network->dhcp6_hostname); + strv_free(network->dhcp6_user_class); + strv_free(network->dhcp6_vendor_class); + set_free(network->dhcp6_request_options); + ordered_hashmap_free(network->dhcp6_client_send_options); + ordered_hashmap_free(network->dhcp6_client_send_vendor_options); + free(network->dhcp6_netlabel); + nft_set_context_clear(&network->dhcp6_nft_set_context); + + /* DHCP PD */ + free(network->dhcp_pd_uplink_name); + set_free(network->dhcp_pd_tokens); + free(network->dhcp_pd_netlabel); + nft_set_context_clear(&network->dhcp_pd_nft_set_context); + + /* Router advertisement */ + ordered_set_free(network->router_search_domains); + free(network->router_dns); + free(network->router_uplink_name); + + /* NDisc */ + set_free(network->ndisc_deny_listed_router); + set_free(network->ndisc_allow_listed_router); + set_free(network->ndisc_deny_listed_prefix); + set_free(network->ndisc_allow_listed_prefix); + set_free(network->ndisc_deny_listed_route_prefix); + set_free(network->ndisc_allow_listed_route_prefix); + set_free(network->ndisc_tokens); + free(network->ndisc_netlabel); + nft_set_context_clear(&network->ndisc_nft_set_context); + + /* LLDP */ + free(network->lldp_mudurl); + + /* netdev */ + free(network->batadv_name); + free(network->bridge_name); + free(network->bond_name); + free(network->vrf_name); + hashmap_free_free_key(network->stacked_netdev_names); + netdev_unref(network->bridge); + netdev_unref(network->bond); + netdev_unref(network->vrf); + hashmap_free_with_destructor(network->stacked_netdevs, netdev_unref); + + /* static configs */ + set_free_free(network->ipv6_proxy_ndp_addresses); + ordered_hashmap_free_with_destructor(network->addresses_by_section, address_free); + hashmap_free_with_destructor(network->routes_by_section, route_free); + hashmap_free_with_destructor(network->nexthops_by_section, nexthop_free); + hashmap_free_with_destructor(network->bridge_fdb_entries_by_section, bridge_fdb_free); + hashmap_free_with_destructor(network->bridge_mdb_entries_by_section, bridge_mdb_free); + ordered_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->pref64_prefixes_by_section, pref64_prefix_free); + hashmap_free_with_destructor(network->rules_by_section, routing_policy_rule_free); + hashmap_free_with_destructor(network->dhcp_static_leases_by_section, dhcp_static_lease_free); + ordered_hashmap_free_with_destructor(network->sr_iov_by_section, sr_iov_free); + hashmap_free_with_destructor(network->qdiscs_by_section, qdisc_free); + hashmap_free_with_destructor(network->tclasses_by_section, tclass_free); + + return mfree(network); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(Network, network, network_free); + +int network_get_by_name(Manager *manager, const char *name, Network **ret) { + Network *network; + + assert(manager); + assert(name); + assert(ret); + + network = ordered_hashmap_get(manager->networks, name); + if (!network) + return -ENOENT; + + *ret = network; + + return 0; +} + +bool network_has_static_ipv6_configurations(Network *network) { + Address *address; + Route *route; + BridgeFDB *fdb; + BridgeMDB *mdb; + Neighbor *neighbor; + + assert(network); + + ORDERED_HASHMAP_FOREACH(address, network->addresses_by_section) + if (address->family == AF_INET6) + return true; + + HASHMAP_FOREACH(route, network->routes_by_section) + if (route->family == AF_INET6) + return true; + + HASHMAP_FOREACH(fdb, network->bridge_fdb_entries_by_section) + if (fdb->family == AF_INET6) + return true; + + HASHMAP_FOREACH(mdb, network->bridge_mdb_entries_by_section) + if (mdb->family == AF_INET6) + return true; + + ORDERED_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; + + if (!hashmap_isempty(network->pref64_prefixes_by_section)) + return true; + + return false; +} + +int config_parse_stacked_netdev( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *name = NULL; + NetDevKind kind = ltype; + Hashmap **h = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(IN_SET(kind, + NETDEV_KIND_IPOIB, + NETDEV_KIND_IPVLAN, + NETDEV_KIND_IPVTAP, + NETDEV_KIND_MACSEC, + NETDEV_KIND_MACVLAN, + NETDEV_KIND_MACVTAP, + NETDEV_KIND_VLAN, + NETDEV_KIND_VXLAN, + NETDEV_KIND_XFRM, + _NETDEV_KIND_TUNNEL)); + + if (!ifname_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid netdev name in %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + name = strdup(rvalue); + if (!name) + return log_oom(); + + r = hashmap_ensure_put(h, &string_hash_ops, name, INT_TO_PTR(kind)); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + log_syntax(unit, LOG_WARNING, filename, line, r, + "Cannot add NetDev '%s' to network, ignoring assignment: %m", name); + else if (r == 0) + log_syntax(unit, LOG_DEBUG, filename, line, r, + "NetDev '%s' specified twice, ignoring.", name); + else + TAKE_PTR(name); + + return 0; +} + +int config_parse_domains( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *n = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + n->search_domains = ordered_set_free(n->search_domains); + n->route_domains = ordered_set_free(n->route_domains); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *w = NULL, *normalized = NULL; + const char *domain; + bool is_route; + + r = extract_first_word(&p, &w, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract search or route domain, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + is_route = w[0] == '~'; + domain = is_route ? w + 1 : w; + + if (dns_name_is_root(domain) || streq(domain, "*")) { + /* If the root domain appears as is, or the special token "*" is found, we'll + * consider this as routing domain, unconditionally. */ + is_route = true; + domain = "."; /* make sure we don't allow empty strings, thus write the root + * domain as "." */ + } else { + r = dns_name_normalize(domain, 0, &normalized); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "'%s' is not a valid domain name, ignoring.", domain); + continue; + } + + domain = normalized; + + if (is_localhost(domain)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "'localhost' domain may not be configured as search or route domain, ignoring assignment: %s", + domain); + continue; + } + } + + OrderedSet **set = is_route ? &n->route_domains : &n->search_domains; + r = ordered_set_put_strdup(set, domain); + if (r == -EEXIST) + continue; + if (r < 0) + return log_oom(); + } +} + +int config_parse_timezone( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + char **tz = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *tz = mfree(*tz); + return 0; + } + + r = verify_timezone(rvalue, LOG_WARNING); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Timezone is not valid, ignoring assignment: %s", rvalue); + return 0; + } + + return free_and_strdup_warn(tz, rvalue); +} + +int config_parse_dns( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *n = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + for (unsigned i = 0; i < n->n_dns; i++) + in_addr_full_free(n->dns[i]); + n->dns = mfree(n->dns); + n->n_dns = 0; + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_(in_addr_full_freep) struct in_addr_full *dns = NULL; + _cleanup_free_ char *w = NULL; + struct in_addr_full **m; + + r = extract_first_word(&p, &w, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid syntax, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + r = in_addr_full_new_from_string(w, &dns); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse dns server address, ignoring: %s", w); + continue; + } + + if (IN_SET(dns->port, 53, 853)) + dns->port = 0; + + m = reallocarray(n->dns, n->n_dns + 1, sizeof(struct in_addr_full*)); + if (!m) + return log_oom(); + + m[n->n_dns++] = TAKE_PTR(dns); + n->dns = m; + } +} + +int config_parse_dnssec_negative_trust_anchors( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Set **nta = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *nta = set_free_free(*nta); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *w = NULL; + + r = extract_first_word(&p, &w, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract negative trust anchor domain, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + r = dns_name_is_valid(w); + if (r <= 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "%s is not a valid domain name, ignoring.", w); + continue; + } + + r = set_ensure_consume(nta, &dns_name_hash_ops, TAKE_PTR(w)); + if (r < 0) + return log_oom(); + } +} + +int config_parse_ntp( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + char ***l = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *l = strv_free(*l); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *w = NULL; + + r = extract_first_word(&p, &w, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract NTP server name, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + r = dns_name_is_valid_or_address(w); + if (r <= 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "%s is not a valid domain name or IP address, ignoring.", w); + continue; + } + + if (strv_length(*l) > MAX_NTP_SERVERS) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "More than %u NTP servers specified, ignoring \"%s\" and any subsequent entries.", + MAX_NTP_SERVERS, w); + return 0; + } + + r = strv_consume(l, TAKE_PTR(w)); + if (r < 0) + return log_oom(); + } +} + +int config_parse_required_for_online( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + LinkOperationalStateRange range; + bool required = true; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + network->required_for_online = -1; + network->required_operstate_for_online = LINK_OPERSTATE_RANGE_DEFAULT; + return 0; + } + + r = parse_operational_state_range(rvalue, &range); + if (r < 0) { + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s= setting, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + required = r; + range = LINK_OPERSTATE_RANGE_DEFAULT; + } + + network->required_for_online = required; + network->required_operstate_for_online = range; + + return 0; +} + +int config_parse_link_group( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + int r; + int32_t group; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + network->group = -1; + return 0; + } + + r = safe_atoi32(rvalue, &group); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse Group=, ignoring assignment: %s", rvalue); + return 0; + } + + if (group < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Value of Group= must be in the range 0…2147483647, ignoring assignment: %s", rvalue); + return 0; + } + + network->group = group; + return 0; +} + +int config_parse_ignore_carrier_loss( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + usec_t usec; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + network->ignore_carrier_loss_set = false; + return 0; + } + + r = parse_boolean(rvalue); + if (r >= 0) { + network->ignore_carrier_loss_set = true; + network->ignore_carrier_loss_usec = r > 0 ? USEC_INFINITY : 0; + return 0; + } + + r = parse_sec(rvalue, &usec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + network->ignore_carrier_loss_set = true; + network->ignore_carrier_loss_usec = usec; + return 0; +} + +DEFINE_CONFIG_PARSE_ENUM(config_parse_required_family_for_online, link_required_address_family, AddressFamily, + "Failed to parse RequiredFamilyForOnline= setting"); + +DEFINE_CONFIG_PARSE_ENUM(config_parse_keep_configuration, keep_configuration, KeepConfiguration, + "Failed to parse KeepConfiguration= setting"); + +static const char* const keep_configuration_table[_KEEP_CONFIGURATION_MAX] = { + [KEEP_CONFIGURATION_NO] = "no", + [KEEP_CONFIGURATION_DHCP_ON_STOP] = "dhcp-on-stop", + [KEEP_CONFIGURATION_DHCP] = "dhcp", + [KEEP_CONFIGURATION_STATIC] = "static", + [KEEP_CONFIGURATION_YES] = "yes", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(keep_configuration, KeepConfiguration, KEEP_CONFIGURATION_YES); + +static const char* const activation_policy_table[_ACTIVATION_POLICY_MAX] = { + [ACTIVATION_POLICY_UP] = "up", + [ACTIVATION_POLICY_ALWAYS_UP] = "always-up", + [ACTIVATION_POLICY_MANUAL] = "manual", + [ACTIVATION_POLICY_ALWAYS_DOWN] = "always-down", + [ACTIVATION_POLICY_DOWN] = "down", + [ACTIVATION_POLICY_BOUND] = "bound", +}; + +DEFINE_STRING_TABLE_LOOKUP(activation_policy, ActivationPolicy); +DEFINE_CONFIG_PARSE_ENUM(config_parse_activation_policy, activation_policy, ActivationPolicy, "Failed to parse activation policy"); diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h new file mode 100644 index 0000000..03131b7 --- /dev/null +++ b/src/network/networkd-network.h @@ -0,0 +1,440 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <linux/nl80211.h> + +#include "sd-bus.h" +#include "sd-device.h" +#include "sd-lldp-tx.h" + +#include "bridge.h" +#include "condition.h" +#include "conf-parser.h" +#include "firewall-util.h" +#include "hashmap.h" +#include "ipoib.h" +#include "net-condition.h" +#include "netdev.h" +#include "networkd-address.h" +#include "networkd-bridge-vlan.h" +#include "networkd-dhcp-common.h" +#include "networkd-dhcp4.h" +#include "networkd-dhcp6.h" +#include "networkd-ipv6ll.h" +#include "networkd-lldp-rx.h" +#include "networkd-ndisc.h" +#include "networkd-radv.h" +#include "networkd-sysctl.h" +#include "networkd-util.h" +#include "ordered-set.h" +#include "resolve-util.h" +#include "socket-netlink.h" + +typedef enum KeepConfiguration { + KEEP_CONFIGURATION_NO = 0, + KEEP_CONFIGURATION_DHCP_ON_START = 1 << 0, + KEEP_CONFIGURATION_DHCP_ON_STOP = 1 << 1, + KEEP_CONFIGURATION_DHCP = KEEP_CONFIGURATION_DHCP_ON_START | KEEP_CONFIGURATION_DHCP_ON_STOP, + KEEP_CONFIGURATION_STATIC = 1 << 2, + KEEP_CONFIGURATION_YES = KEEP_CONFIGURATION_DHCP | KEEP_CONFIGURATION_STATIC, + _KEEP_CONFIGURATION_MAX, + _KEEP_CONFIGURATION_INVALID = -EINVAL, +} KeepConfiguration; + +typedef enum ActivationPolicy { + ACTIVATION_POLICY_UP, + ACTIVATION_POLICY_ALWAYS_UP, + ACTIVATION_POLICY_MANUAL, + ACTIVATION_POLICY_ALWAYS_DOWN, + ACTIVATION_POLICY_DOWN, + ACTIVATION_POLICY_BOUND, + _ACTIVATION_POLICY_MAX, + _ACTIVATION_POLICY_INVALID = -EINVAL, +} ActivationPolicy; + +typedef struct Manager Manager; + +typedef struct NetworkDHCPServerEmitAddress { + bool emit; + struct in_addr *addresses; + size_t n_addresses; +} NetworkDHCPServerEmitAddress; + +struct Network { + Manager *manager; + + unsigned n_ref; + + char *name; + char *filename; + char **dropins; + Hashmap *stats_by_path; + char *description; + + /* [Match] section */ + NetMatch match; + LIST_HEAD(Condition, conditions); + + /* Master or stacked netdevs */ + bool keep_master; + NetDev *batadv; + NetDev *bridge; + NetDev *bond; + NetDev *vrf; + NetDev *xfrm; + Hashmap *stacked_netdevs; + char *batadv_name; + char *bridge_name; + char *bond_name; + char *vrf_name; + Hashmap *stacked_netdev_names; + + /* [Link] section */ + struct hw_addr_data hw_addr; + uint32_t mtu; + int32_t group; + int arp; + int multicast; + int allmulticast; + int promiscuous; + bool unmanaged; + int required_for_online; /* Is this network required to be considered online? */ + LinkOperationalStateRange required_operstate_for_online; + AddressFamily required_family_for_online; + ActivationPolicy activation_policy; + + /* misc settings */ + bool configure_without_carrier; + bool ignore_carrier_loss_set; + usec_t ignore_carrier_loss_usec; /* timespan */ + KeepConfiguration keep_configuration; + char **bind_carrier; + bool default_route_on_device; + AddressFamily ip_masquerade; + + /* DHCP Client Support */ + AddressFamily dhcp; + struct in_addr dhcp_request_address; + DHCPClientIdentifier dhcp_client_identifier; + DUID dhcp_duid; + uint32_t dhcp_iaid; + bool dhcp_iaid_set; + char *dhcp_vendor_class_identifier; + char *dhcp_mudurl; + char **dhcp_user_class; + char *dhcp_hostname; + char *dhcp_label; + uint64_t dhcp_max_attempts; + uint32_t dhcp_route_metric; + bool dhcp_route_metric_set; + uint32_t dhcp_route_table; + bool dhcp_route_table_set; + usec_t dhcp_fallback_lease_lifetime_usec; + uint32_t dhcp_route_mtu; + uint16_t dhcp_client_port; + int dhcp_critical; + int dhcp_ip_service_type; + int dhcp_socket_priority; + bool dhcp_socket_priority_set; + bool dhcp_anonymize; + bool dhcp_send_hostname; + bool dhcp_send_hostname_set; + int dhcp_broadcast; + int dhcp_ipv6_only_mode; + int dhcp_use_rapid_commit; + bool dhcp_use_dns; + bool dhcp_use_dns_set; + bool dhcp_routes_to_dns; + bool dhcp_use_ntp; + bool dhcp_use_ntp_set; + bool dhcp_routes_to_ntp; + bool dhcp_use_sip; + bool dhcp_use_captive_portal; + bool dhcp_use_mtu; + bool dhcp_use_routes; + int dhcp_use_gateway; + bool dhcp_quickack; + uint32_t dhcp_initial_congestion_window; + uint32_t dhcp_advertised_receive_window; + bool dhcp_use_timezone; + bool dhcp_use_hostname; + bool dhcp_use_6rd; + bool dhcp_send_release; + bool dhcp_send_decline; + DHCPUseDomains dhcp_use_domains; + bool dhcp_use_domains_set; + Set *dhcp_deny_listed_ip; + Set *dhcp_allow_listed_ip; + Set *dhcp_request_options; + OrderedHashmap *dhcp_client_send_options; + OrderedHashmap *dhcp_client_send_vendor_options; + char *dhcp_netlabel; + NFTSetContext dhcp_nft_set_context; + + /* DHCPv6 Client support */ + bool dhcp6_use_address; + bool dhcp6_use_pd_prefix; + bool dhcp6_send_hostname; + bool dhcp6_send_hostname_set; + bool dhcp6_use_dns; + bool dhcp6_use_dns_set; + bool dhcp6_use_hostname; + bool dhcp6_use_ntp; + bool dhcp6_use_ntp_set; + bool dhcp6_use_captive_portal; + bool dhcp6_use_rapid_commit; + DHCPUseDomains dhcp6_use_domains; + bool dhcp6_use_domains_set; + uint32_t dhcp6_iaid; + bool dhcp6_iaid_set; + bool dhcp6_iaid_set_explicitly; + DUID dhcp6_duid; + uint8_t dhcp6_pd_prefix_length; + struct in6_addr dhcp6_pd_prefix_hint; + char *dhcp6_hostname; + char *dhcp6_mudurl; + char **dhcp6_user_class; + char **dhcp6_vendor_class; + DHCP6ClientStartMode dhcp6_client_start_mode; + OrderedHashmap *dhcp6_client_send_options; + OrderedHashmap *dhcp6_client_send_vendor_options; + Set *dhcp6_request_options; + char *dhcp6_netlabel; + bool dhcp6_send_release; + NFTSetContext dhcp6_nft_set_context; + + /* DHCP Server Support */ + bool dhcp_server; + bool dhcp_server_bind_to_interface; + unsigned char dhcp_server_address_prefixlen; + struct in_addr dhcp_server_address_in_addr; + const Address *dhcp_server_address; + int dhcp_server_uplink_index; + char *dhcp_server_uplink_name; + struct in_addr dhcp_server_relay_target; + char *dhcp_server_relay_agent_circuit_id; + char *dhcp_server_relay_agent_remote_id; + NetworkDHCPServerEmitAddress dhcp_server_emit[_SD_DHCP_LEASE_SERVER_TYPE_MAX]; + bool dhcp_server_emit_router; + struct in_addr dhcp_server_router; + bool dhcp_server_emit_timezone; + char *dhcp_server_timezone; + usec_t dhcp_server_default_lease_time_usec, dhcp_server_max_lease_time_usec; + uint32_t dhcp_server_pool_offset; + uint32_t dhcp_server_pool_size; + OrderedHashmap *dhcp_server_send_options; + OrderedHashmap *dhcp_server_send_vendor_options; + struct in_addr dhcp_server_boot_server_address; + char *dhcp_server_boot_server_name; + char *dhcp_server_boot_filename; + usec_t dhcp_server_ipv6_only_preferred_usec; + bool dhcp_server_rapid_commit; + + /* link-local addressing support */ + AddressFamily link_local; + IPv6LinkLocalAddressGenMode ipv6ll_address_gen_mode; + struct in6_addr ipv6ll_stable_secret; + struct in_addr ipv4ll_start_address; + bool ipv4ll_route; + + /* IPv6 RA support */ + RADVPrefixDelegation router_prefix_delegation; + usec_t router_lifetime_usec; + uint8_t router_preference; + usec_t router_retransmit_usec; + uint8_t router_hop_limit; + bool router_managed; + bool router_other_information; + bool router_emit_dns; + bool router_emit_domains; + usec_t router_dns_lifetime_usec; + struct in6_addr *router_dns; + unsigned n_router_dns; + OrderedSet *router_search_domains; + int router_uplink_index; + char *router_uplink_name; + /* Mobile IPv6 Home Agent */ + bool router_home_agent_information; + uint16_t router_home_agent_preference; + usec_t home_agent_lifetime_usec; + + /* DHCP Prefix Delegation support */ + int dhcp_pd; + bool dhcp_pd_announce; + bool dhcp_pd_assign; + bool dhcp_pd_manage_temporary_address; + int64_t dhcp_pd_subnet_id; + uint32_t dhcp_pd_route_metric; + Set *dhcp_pd_tokens; + int dhcp_pd_uplink_index; + char *dhcp_pd_uplink_name; + char *dhcp_pd_netlabel; + NFTSetContext dhcp_pd_nft_set_context; + + /* Bridge Support */ + int use_bpdu; + int hairpin; + int isolated; + int fast_leave; + int allow_port_to_be_root; + int unicast_flood; + int multicast_flood; + int multicast_to_unicast; + int neighbor_suppression; + int learning; + int bridge_proxy_arp; + int bridge_proxy_arp_wifi; + uint32_t cost; + uint16_t priority; + MulticastRouter multicast_router; + + /* Bridge VLAN */ + bool use_br_vlan; + uint16_t pvid; + uint32_t br_vid_bitmap[BRIDGE_VLAN_BITMAP_LEN]; + uint32_t br_untagged_bitmap[BRIDGE_VLAN_BITMAP_LEN]; + + /* CAN support */ + uint32_t can_bitrate; + unsigned can_sample_point; + nsec_t can_time_quanta_ns; + uint32_t can_propagation_segment; + uint32_t can_phase_buffer_segment_1; + uint32_t can_phase_buffer_segment_2; + uint32_t can_sync_jump_width; + uint32_t can_data_bitrate; + unsigned can_data_sample_point; + nsec_t can_data_time_quanta_ns; + uint32_t can_data_propagation_segment; + uint32_t can_data_phase_buffer_segment_1; + uint32_t can_data_phase_buffer_segment_2; + uint32_t can_data_sync_jump_width; + usec_t can_restart_us; + uint32_t can_control_mode_mask; + uint32_t can_control_mode_flags; + uint16_t can_termination; + bool can_termination_set; + + /* IPoIB support */ + IPoIBMode ipoib_mode; + int ipoib_umcast; + + /* sysctl settings */ + AddressFamily ip_forward; + int ipv4_accept_local; + int ipv4_route_localnet; + int ipv6_dad_transmits; + uint8_t ipv6_hop_limit; + int proxy_arp; + uint32_t ipv6_mtu; + IPv6PrivacyExtensions ipv6_privacy_extensions; + IPReversePathFilter ipv4_rp_filter; + int ipv6_proxy_ndp; + Set *ipv6_proxy_ndp_addresses; + + /* IPv6 accept RA */ + int ipv6_accept_ra; + bool ipv6_accept_ra_use_dns; + bool ipv6_accept_ra_use_gateway; + bool ipv6_accept_ra_use_route_prefix; + bool ipv6_accept_ra_use_autonomous_prefix; + bool ipv6_accept_ra_use_onlink_prefix; + bool ipv6_accept_ra_use_mtu; + bool ipv6_accept_ra_use_hop_limit; + bool ipv6_accept_ra_use_icmp6_ratelimit; + bool ipv6_accept_ra_quickack; + bool ipv6_accept_ra_use_captive_portal; + bool ipv6_accept_ra_use_pref64; + bool active_slave; + bool primary_slave; + DHCPUseDomains ipv6_accept_ra_use_domains; + IPv6AcceptRAStartDHCP6Client ipv6_accept_ra_start_dhcp6_client; + uint32_t ipv6_accept_ra_route_table; + bool ipv6_accept_ra_route_table_set; + uint32_t ipv6_accept_ra_route_metric_high; + uint32_t ipv6_accept_ra_route_metric_medium; + uint32_t ipv6_accept_ra_route_metric_low; + bool ipv6_accept_ra_route_metric_set; + Set *ndisc_deny_listed_router; + Set *ndisc_allow_listed_router; + Set *ndisc_deny_listed_prefix; + Set *ndisc_allow_listed_prefix; + Set *ndisc_deny_listed_route_prefix; + Set *ndisc_allow_listed_route_prefix; + Set *ndisc_tokens; + char *ndisc_netlabel; + NFTSetContext ndisc_nft_set_context; + + /* LLDP support */ + LLDPMode lldp_mode; /* LLDP reception */ + sd_lldp_multicast_mode_t lldp_multicast_mode; /* LLDP transmission */ + char *lldp_mudurl; /* LLDP MUD URL */ + + OrderedHashmap *addresses_by_section; + Hashmap *routes_by_section; + Hashmap *nexthops_by_section; + Hashmap *bridge_fdb_entries_by_section; + Hashmap *bridge_mdb_entries_by_section; + OrderedHashmap *neighbors_by_section; + Hashmap *address_labels_by_section; + Hashmap *prefixes_by_section; + Hashmap *route_prefixes_by_section; + Hashmap *pref64_prefixes_by_section; + Hashmap *rules_by_section; + Hashmap *dhcp_static_leases_by_section; + Hashmap *qdiscs_by_section; + Hashmap *tclasses_by_section; + OrderedHashmap *sr_iov_by_section; + + /* All kinds of DNS configuration */ + struct in_addr_full **dns; + unsigned n_dns; + OrderedSet *search_domains, *route_domains; + int dns_default_route; + ResolveSupport llmnr; + ResolveSupport mdns; + DnssecMode dnssec_mode; + DnsOverTlsMode dns_over_tls_mode; + Set *dnssec_negative_trust_anchors; + + /* NTP */ + char **ntp; +}; + +Network *network_ref(Network *network); +Network *network_unref(Network *network); +DEFINE_TRIVIAL_CLEANUP_FUNC(Network*, network_unref); + +int network_load(Manager *manager, OrderedHashmap **networks); +int network_reload(Manager *manager); +int network_load_one(Manager *manager, OrderedHashmap **networks, const char *filename); +int network_verify(Network *network); + +int manager_build_dhcp_pd_subnet_ids(Manager *manager); + +int network_get_by_name(Manager *manager, const char *name, Network **ret); +void network_apply_anonymize_if_set(Network *network); + +bool network_has_static_ipv6_configurations(Network *network); + +CONFIG_PARSER_PROTOTYPE(config_parse_stacked_netdev); +CONFIG_PARSER_PROTOTYPE(config_parse_tunnel); +CONFIG_PARSER_PROTOTYPE(config_parse_domains); +CONFIG_PARSER_PROTOTYPE(config_parse_dns); +CONFIG_PARSER_PROTOTYPE(config_parse_timezone); +CONFIG_PARSER_PROTOTYPE(config_parse_dnssec_negative_trust_anchors); +CONFIG_PARSER_PROTOTYPE(config_parse_ntp); +CONFIG_PARSER_PROTOTYPE(config_parse_required_for_online); +CONFIG_PARSER_PROTOTYPE(config_parse_required_family_for_online); +CONFIG_PARSER_PROTOTYPE(config_parse_keep_configuration); +CONFIG_PARSER_PROTOTYPE(config_parse_activation_policy); +CONFIG_PARSER_PROTOTYPE(config_parse_link_group); +CONFIG_PARSER_PROTOTYPE(config_parse_ignore_carrier_loss); + +const struct ConfigPerfItem* network_network_gperf_lookup(const char *key, GPERF_LEN_TYPE length); + +const char* keep_configuration_to_string(KeepConfiguration i) _const_; +KeepConfiguration keep_configuration_from_string(const char *s) _pure_; + +const char* activation_policy_to_string(ActivationPolicy i) _const_; +ActivationPolicy activation_policy_from_string(const char *s) _pure_; diff --git a/src/network/networkd-nexthop.c b/src/network/networkd-nexthop.c new file mode 100644 index 0000000..e2ded28 --- /dev/null +++ b/src/network/networkd-nexthop.c @@ -0,0 +1,1384 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. + */ + +#include <net/if.h> +#include <linux/nexthop.h> + +#include "alloc-util.h" +#include "netlink-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-nexthop.h" +#include "networkd-queue.h" +#include "networkd-route-util.h" +#include "parse-util.h" +#include "set.h" +#include "stdio-util.h" +#include "string-util.h" + +NextHop *nexthop_free(NextHop *nexthop) { + if (!nexthop) + return NULL; + + if (nexthop->network) { + assert(nexthop->section); + hashmap_remove(nexthop->network->nexthops_by_section, nexthop->section); + } + + config_section_free(nexthop->section); + + if (nexthop->link) { + set_remove(nexthop->link->nexthops, nexthop); + + if (nexthop->link->manager && nexthop->id > 0) + hashmap_remove(nexthop->link->manager->nexthops_by_id, UINT32_TO_PTR(nexthop->id)); + } + + if (nexthop->manager) { + set_remove(nexthop->manager->nexthops, nexthop); + + if (nexthop->id > 0) + hashmap_remove(nexthop->manager->nexthops_by_id, UINT32_TO_PTR(nexthop->id)); + } + + hashmap_free_free(nexthop->group); + + return mfree(nexthop); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(NextHop, nexthop_free); + +static int nexthop_new(NextHop **ret) { + _cleanup_(nexthop_freep) NextHop *nexthop = NULL; + + nexthop = new(NextHop, 1); + if (!nexthop) + return -ENOMEM; + + *nexthop = (NextHop) { + .family = AF_UNSPEC, + .onlink = -1, + }; + + *ret = TAKE_PTR(nexthop); + + return 0; +} + +static int nexthop_new_static(Network *network, const char *filename, unsigned section_line, NextHop **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(nexthop_freep) NextHop *nexthop = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + nexthop = hashmap_get(network->nexthops_by_section, n); + if (nexthop) { + *ret = TAKE_PTR(nexthop); + return 0; + } + + r = nexthop_new(&nexthop); + if (r < 0) + return r; + + nexthop->protocol = RTPROT_STATIC; + nexthop->network = network; + nexthop->section = TAKE_PTR(n); + nexthop->source = NETWORK_CONFIG_SOURCE_STATIC; + + r = hashmap_ensure_put(&network->nexthops_by_section, &config_section_hash_ops, nexthop->section, nexthop); + if (r < 0) + return r; + + *ret = TAKE_PTR(nexthop); + return 0; +} + +static void nexthop_hash_func(const NextHop *nexthop, struct siphash *state) { + assert(nexthop); + + siphash24_compress(&nexthop->protocol, sizeof(nexthop->protocol), state); + siphash24_compress(&nexthop->id, sizeof(nexthop->id), state); + siphash24_compress(&nexthop->blackhole, sizeof(nexthop->blackhole), state); + siphash24_compress(&nexthop->family, sizeof(nexthop->family), state); + + switch (nexthop->family) { + case AF_INET: + case AF_INET6: + siphash24_compress(&nexthop->gw, FAMILY_ADDRESS_SIZE(nexthop->family), state); + + break; + default: + /* treat any other address family as AF_UNSPEC */ + break; + } +} + +static int nexthop_compare_func(const NextHop *a, const NextHop *b) { + int r; + + r = CMP(a->protocol, b->protocol); + if (r != 0) + return r; + + r = CMP(a->id, b->id); + if (r != 0) + return r; + + r = CMP(a->blackhole, b->blackhole); + if (r != 0) + return r; + + r = CMP(a->family, b->family); + if (r != 0) + return r; + + if (IN_SET(a->family, AF_INET, AF_INET6)) + return memcmp(&a->gw, &b->gw, FAMILY_ADDRESS_SIZE(a->family)); + + return 0; +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + nexthop_hash_ops, + NextHop, + nexthop_hash_func, + nexthop_compare_func, + nexthop_free); + +static bool nexthop_equal(const NextHop *a, const NextHop *b) { + if (a == b) + return true; + + if (!a || !b) + return false; + + return nexthop_compare_func(a, b) == 0; +} + +static int nexthop_dup(const NextHop *src, NextHop **ret) { + _cleanup_(nexthop_freep) NextHop *dest = NULL; + struct nexthop_grp *nhg; + int r; + + assert(src); + assert(ret); + + dest = newdup(NextHop, src, 1); + if (!dest) + return -ENOMEM; + + /* unset all pointers */ + dest->manager = NULL; + dest->link = NULL; + dest->network = NULL; + dest->section = NULL; + dest->group = NULL; + + HASHMAP_FOREACH(nhg, src->group) { + _cleanup_free_ struct nexthop_grp *g = NULL; + + g = newdup(struct nexthop_grp, nhg, 1); + if (!g) + return -ENOMEM; + + r = hashmap_ensure_put(&dest->group, NULL, UINT32_TO_PTR(g->id), g); + if (r < 0) + return r; + if (r > 0) + TAKE_PTR(g); + } + + *ret = TAKE_PTR(dest); + return 0; +} + +int manager_get_nexthop_by_id(Manager *manager, uint32_t id, NextHop **ret) { + NextHop *nh; + + assert(manager); + + if (id == 0) + return -EINVAL; + + nh = hashmap_get(manager->nexthops_by_id, UINT32_TO_PTR(id)); + if (!nh) + return -ENOENT; + + if (ret) + *ret = nh; + return 0; +} + +static bool nexthop_owned_by_link(const NextHop *nexthop) { + return !nexthop->blackhole && hashmap_isempty(nexthop->group); +} + +static int nexthop_get(Manager *manager, Link *link, NextHop *in, NextHop **ret) { + NextHop *nexthop; + Set *nexthops; + + assert(in); + + if (nexthop_owned_by_link(in)) { + if (!link) + return -ENOENT; + + nexthops = link->nexthops; + } else { + if (!manager) + return -ENOENT; + + nexthops = manager->nexthops; + } + + nexthop = set_get(nexthops, in); + if (nexthop) { + if (ret) + *ret = nexthop; + return 0; + } + + if (in->id > 0) + return -ENOENT; + + /* Also find nexthop configured without ID. */ + SET_FOREACH(nexthop, nexthops) { + uint32_t id; + bool found; + + id = nexthop->id; + nexthop->id = 0; + found = nexthop_equal(nexthop, in); + nexthop->id = id; + + if (!found) + continue; + + if (ret) + *ret = nexthop; + return 0; + } + + return -ENOENT; +} + +static int nexthop_add(Manager *manager, Link *link, NextHop *nexthop) { + int r; + + assert(nexthop); + assert(nexthop->id > 0); + + if (nexthop_owned_by_link(nexthop)) { + assert(link); + + r = set_ensure_put(&link->nexthops, &nexthop_hash_ops, nexthop); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + nexthop->link = link; + + manager = link->manager; + } else { + assert(manager); + + r = set_ensure_put(&manager->nexthops, &nexthop_hash_ops, nexthop); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + nexthop->manager = manager; + } + + return hashmap_ensure_put(&manager->nexthops_by_id, NULL, UINT32_TO_PTR(nexthop->id), nexthop); +} + +static int nexthop_acquire_id(Manager *manager, NextHop *nexthop) { + _cleanup_set_free_ Set *ids = NULL; + Network *network; + uint32_t id; + int r; + + assert(manager); + assert(nexthop); + + if (nexthop->id > 0) + return 0; + + /* Find the lowest unused ID. */ + + ORDERED_HASHMAP_FOREACH(network, manager->networks) { + NextHop *tmp; + + HASHMAP_FOREACH(tmp, network->nexthops_by_section) { + if (tmp->id == 0) + continue; + + r = set_ensure_put(&ids, NULL, UINT32_TO_PTR(tmp->id)); + if (r < 0) + return r; + } + } + + for (id = 1; id < UINT32_MAX; id++) { + if (manager_get_nexthop_by_id(manager, id, NULL) >= 0) + continue; + if (set_contains(ids, UINT32_TO_PTR(id))) + continue; + break; + } + + nexthop->id = id; + return 0; +} + +static void log_nexthop_debug(const NextHop *nexthop, const char *str, const Link *link) { + _cleanup_free_ char *state = NULL, *group = NULL, *flags = NULL; + struct nexthop_grp *nhg; + + assert(nexthop); + assert(str); + + /* link may be NULL. */ + + if (!DEBUG_LOGGING) + return; + + (void) network_config_state_to_string_alloc(nexthop->state, &state); + (void) route_flags_to_string_alloc(nexthop->flags, &flags); + + HASHMAP_FOREACH(nhg, nexthop->group) + (void) strextendf_with_separator(&group, ",", "%"PRIu32":%"PRIu32, nhg->id, nhg->weight+1u); + + log_link_debug(link, "%s %s nexthop (%s): id: %"PRIu32", gw: %s, blackhole: %s, group: %s, flags: %s", + str, strna(network_config_source_to_string(nexthop->source)), strna(state), + nexthop->id, + IN_ADDR_TO_STRING(nexthop->family, &nexthop->gw), + yes_no(nexthop->blackhole), strna(group), strna(flags)); +} + +static int nexthop_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert(m); + + /* link may be NULL. */ + + if (link && IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 1; + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -ENOENT) + log_link_message_warning_errno(link, m, r, "Could not drop nexthop, ignoring"); + + return 1; +} + +static int nexthop_remove(NextHop *nexthop) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + Manager *manager; + Link *link; + int r; + + assert(nexthop); + assert(nexthop->manager || (nexthop->link && nexthop->link->manager)); + + /* link may be NULL. */ + link = nexthop->link; + manager = nexthop->manager ?: nexthop->link->manager; + + if (nexthop->id == 0) { + log_link_debug(link, "Cannot remove nexthop without valid ID, ignoring."); + return 0; + } + + log_nexthop_debug(nexthop, "Removing", link); + + r = sd_rtnl_message_new_nexthop(manager->rtnl, &m, RTM_DELNEXTHOP, AF_UNSPEC, RTPROT_UNSPEC); + if (r < 0) + return log_link_error_errno(link, r, "Could not create RTM_DELNEXTHOP message: %m"); + + r = sd_netlink_message_append_u32(m, NHA_ID, nexthop->id); + if (r < 0) + return log_link_error_errno(link, r, "Could not append NHA_ID attribute: %m"); + + r = netlink_call_async(manager->rtnl, NULL, m, nexthop_remove_handler, + link ? link_netlink_destroy_callback : NULL, link); + if (r < 0) + return log_link_error_errno(link, r, "Could not send rtnetlink message: %m"); + + link_ref(link); /* link may be NULL, link_ref() is OK with that */ + + nexthop_enter_removing(nexthop); + return 0; +} + +static int nexthop_configure(NextHop *nexthop, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(nexthop); + assert(IN_SET(nexthop->family, AF_UNSPEC, AF_INET, AF_INET6)); + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + assert(link->ifindex > 0); + assert(req); + + log_nexthop_debug(nexthop, "Configuring", link); + + r = sd_rtnl_message_new_nexthop(link->manager->rtnl, &m, RTM_NEWNEXTHOP, nexthop->family, nexthop->protocol); + if (r < 0) + return r; + + if (nexthop->id > 0) { + r = sd_netlink_message_append_u32(m, NHA_ID, nexthop->id); + if (r < 0) + return r; + } + + if (!hashmap_isempty(nexthop->group)) { + _cleanup_free_ struct nexthop_grp *group = NULL; + struct nexthop_grp *p, *nhg; + + group = new(struct nexthop_grp, hashmap_size(nexthop->group)); + if (!group) + return log_oom(); + + p = group; + HASHMAP_FOREACH(nhg, nexthop->group) + *p++ = *nhg; + + r = sd_netlink_message_append_data(m, NHA_GROUP, group, sizeof(struct nexthop_grp) * hashmap_size(nexthop->group)); + if (r < 0) + return r; + + } else if (nexthop->blackhole) { + r = sd_netlink_message_append_flag(m, NHA_BLACKHOLE); + if (r < 0) + return r; + } else { + r = sd_netlink_message_append_u32(m, NHA_OIF, link->ifindex); + if (r < 0) + return r; + + if (in_addr_is_set(nexthop->family, &nexthop->gw)) { + r = netlink_message_append_in_addr_union(m, NHA_GATEWAY, nexthop->family, &nexthop->gw); + if (r < 0) + return r; + + r = sd_rtnl_message_nexthop_set_flags(m, nexthop->flags & RTNH_F_ONLINK); + if (r < 0) + return r; + } + } + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static int static_nexthop_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, NextHop *nexthop) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not set nexthop"); + link_enter_failed(link); + return 1; + } + + if (link->static_nexthop_messages == 0) { + log_link_debug(link, "Nexthops set"); + link->static_nexthops_configured = true; + link_check_ready(link); + } + + return 1; +} + +static bool nexthop_is_ready_to_configure(Link *link, const NextHop *nexthop) { + struct nexthop_grp *nhg; + + assert(link); + assert(nexthop); + + if (!link_is_ready_to_configure(link, false)) + return false; + + if (nexthop_owned_by_link(nexthop)) { + /* TODO: fdb nexthop does not require IFF_UP. The conditions below needs to be updated + * when fdb nexthop support is added. See rtm_to_nh_config() in net/ipv4/nexthop.c of + * kernel. */ + if (link->set_flags_messages > 0) + return false; + if (!FLAGS_SET(link->flags, IFF_UP)) + return false; + } + + /* All group members must be configured first. */ + HASHMAP_FOREACH(nhg, nexthop->group) { + NextHop *g; + + if (manager_get_nexthop_by_id(link->manager, nhg->id, &g) < 0) + return false; + + if (!nexthop_exists(g)) + return false; + } + + if (nexthop->id == 0) { + Request *req; + + ORDERED_SET_FOREACH(req, link->manager->request_queue) { + if (req->type != REQUEST_TYPE_NEXTHOP) + continue; + if (((NextHop*) req->userdata)->id != 0) + return false; /* first configure nexthop with id. */ + } + } + + return gateway_is_ready(link, FLAGS_SET(nexthop->flags, RTNH_F_ONLINK), nexthop->family, &nexthop->gw); +} + +static int nexthop_process_request(Request *req, Link *link, NextHop *nexthop) { + int r; + + assert(req); + assert(link); + assert(nexthop); + + if (!nexthop_is_ready_to_configure(link, nexthop)) + return 0; + + r = nexthop_configure(nexthop, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure nexthop"); + + nexthop_enter_configuring(nexthop); + return 1; +} + +static int link_request_nexthop(Link *link, NextHop *nexthop) { + NextHop *existing; + int r; + + assert(link); + assert(nexthop); + assert(nexthop->source != NETWORK_CONFIG_SOURCE_FOREIGN); + + if (nexthop_get(link->manager, link, nexthop, &existing) < 0) { + _cleanup_(nexthop_freep) NextHop *tmp = NULL; + + r = nexthop_dup(nexthop, &tmp); + if (r < 0) + return r; + + r = nexthop_acquire_id(link->manager, tmp); + if (r < 0) + return r; + + r = nexthop_add(link->manager, link, tmp); + if (r < 0) + return r; + + existing = TAKE_PTR(tmp); + } else + existing->source = nexthop->source; + + log_nexthop_debug(existing, "Requesting", link); + r = link_queue_request_safe(link, REQUEST_TYPE_NEXTHOP, + existing, NULL, + nexthop_hash_func, + nexthop_compare_func, + nexthop_process_request, + &link->static_nexthop_messages, + static_nexthop_handler, + NULL); + if (r <= 0) + return r; + + nexthop_enter_requesting(existing); + return 1; +} + +int link_request_static_nexthops(Link *link, bool only_ipv4) { + NextHop *nh; + int r; + + assert(link); + assert(link->network); + + link->static_nexthops_configured = false; + + HASHMAP_FOREACH(nh, link->network->nexthops_by_section) { + if (only_ipv4 && nh->family != AF_INET) + continue; + + r = link_request_nexthop(link, nh); + if (r < 0) + return log_link_warning_errno(link, r, "Could not request nexthop: %m"); + } + + if (link->static_nexthop_messages == 0) { + link->static_nexthops_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Requesting nexthops"); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +static void manager_mark_nexthops(Manager *manager, bool foreign, const Link *except) { + NextHop *nexthop; + Link *link; + + assert(manager); + + /* First, mark all nexthops. */ + SET_FOREACH(nexthop, manager->nexthops) { + /* do not touch nexthop created by the kernel */ + if (nexthop->protocol == RTPROT_KERNEL) + continue; + + /* When 'foreign' is true, mark only foreign nexthops, and vice versa. */ + if (foreign != (nexthop->source == NETWORK_CONFIG_SOURCE_FOREIGN)) + continue; + + /* Ignore nexthops not assigned yet or already removed. */ + if (!nexthop_exists(nexthop)) + continue; + + nexthop_mark(nexthop); + } + + /* Then, unmark all nexthops requested by active links. */ + HASHMAP_FOREACH(link, manager->links_by_index) { + if (link == except) + continue; + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + continue; + + HASHMAP_FOREACH(nexthop, link->network->nexthops_by_section) { + NextHop *existing; + + if (nexthop_get(manager, NULL, nexthop, &existing) >= 0) + nexthop_unmark(existing); + } + } +} + +static int manager_drop_marked_nexthops(Manager *manager) { + NextHop *nexthop; + int r = 0; + + assert(manager); + + SET_FOREACH(nexthop, manager->nexthops) { + if (!nexthop_is_marked(nexthop)) + continue; + + RET_GATHER(r, nexthop_remove(nexthop)); + } + + return r; +} + +int link_drop_foreign_nexthops(Link *link) { + NextHop *nexthop; + int r = 0; + + assert(link); + assert(link->manager); + assert(link->network); + + /* First, mark all nexthops. */ + SET_FOREACH(nexthop, link->nexthops) { + /* do not touch nexthop created by the kernel */ + if (nexthop->protocol == RTPROT_KERNEL) + continue; + + /* Do not remove nexthops we configured. */ + if (nexthop->source != NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + /* Ignore nexthops not assigned yet or already removed. */ + if (!nexthop_exists(nexthop)) + continue; + + nexthop_mark(nexthop); + } + + /* Then, unmark all nexthops requested by active links. */ + HASHMAP_FOREACH(nexthop, link->network->nexthops_by_section) { + NextHop *existing; + + if (nexthop_get(NULL, link, nexthop, &existing) >= 0) + nexthop_unmark(existing); + } + + /* Finally, remove all marked rules. */ + SET_FOREACH(nexthop, link->nexthops) { + if (!nexthop_is_marked(nexthop)) + continue; + + RET_GATHER(r, nexthop_remove(nexthop)); + } + + manager_mark_nexthops(link->manager, /* foreign = */ true, NULL); + + return RET_GATHER(r, manager_drop_marked_nexthops(link->manager)); +} + +int link_drop_managed_nexthops(Link *link) { + NextHop *nexthop; + int r = 0; + + assert(link); + assert(link->manager); + + SET_FOREACH(nexthop, link->nexthops) { + /* do not touch nexthop created by the kernel */ + if (nexthop->protocol == RTPROT_KERNEL) + continue; + + /* Do not touch addresses managed by kernel or other tools. */ + if (nexthop->source == NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + /* Ignore nexthops not assigned yet or already removing. */ + if (!nexthop_exists(nexthop)) + continue; + + RET_GATHER(r, nexthop_remove(nexthop)); + } + + manager_mark_nexthops(link->manager, /* foreign = */ false, link); + + return RET_GATHER(r, manager_drop_marked_nexthops(link->manager)); +} + +void link_foreignize_nexthops(Link *link) { + NextHop *nexthop; + + assert(link); + + SET_FOREACH(nexthop, link->nexthops) + nexthop->source = NETWORK_CONFIG_SOURCE_FOREIGN; + + manager_mark_nexthops(link->manager, /* foreign = */ false, link); + + SET_FOREACH(nexthop, link->manager->nexthops) { + if (!nexthop_is_marked(nexthop)) + continue; + + nexthop->source = NETWORK_CONFIG_SOURCE_FOREIGN; + } +} + +int manager_rtnl_process_nexthop(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) { + _cleanup_(nexthop_freep) NextHop *tmp = NULL; + _cleanup_free_ void *raw_group = NULL; + NextHop *nexthop = NULL; + size_t raw_group_size; + uint32_t ifindex; + uint16_t type; + Link *link = NULL; + int r; + + assert(rtnl); + assert(message); + assert(m); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "rtnl: failed to receive rule message, ignoring"); + + return 0; + } + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get message type, ignoring: %m"); + return 0; + } else if (!IN_SET(type, RTM_NEWNEXTHOP, RTM_DELNEXTHOP)) { + log_warning("rtnl: received unexpected message type %u when processing nexthop, ignoring.", type); + return 0; + } + + r = sd_netlink_message_read_u32(message, NHA_OIF, &ifindex); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get NHA_OIF attribute, ignoring: %m"); + return 0; + } else if (r >= 0) { + if (ifindex <= 0) { + log_warning("rtnl: received nexthop message with invalid ifindex %"PRIu32", ignoring.", ifindex); + return 0; + } + + r = link_get_by_index(m, ifindex, &link); + if (r < 0) { + if (!m->enumerating) + log_warning("rtnl: received nexthop message for link (%"PRIu32") we do not know about, ignoring", ifindex); + return 0; + } + } + + r = nexthop_new(&tmp); + if (r < 0) + return log_oom(); + + r = sd_rtnl_message_get_family(message, &tmp->family); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: could not get nexthop family, ignoring: %m"); + return 0; + } else if (!IN_SET(tmp->family, AF_UNSPEC, AF_INET, AF_INET6)) { + log_link_debug(link, "rtnl: received nexthop message with invalid family %d, ignoring.", tmp->family); + return 0; + } + + r = sd_rtnl_message_nexthop_get_protocol(message, &tmp->protocol); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: could not get nexthop protocol, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_nexthop_get_flags(message, &tmp->flags); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: could not get nexthop flags, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_data(message, NHA_GROUP, &raw_group_size, &raw_group); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: could not get NHA_GROUP attribute, ignoring: %m"); + return 0; + } else if (r >= 0) { + struct nexthop_grp *group = raw_group; + size_t n_group; + + if (raw_group_size == 0 || raw_group_size % sizeof(struct nexthop_grp) != 0) { + log_link_warning(link, "rtnl: received nexthop message with invalid nexthop group size, ignoring."); + return 0; + } + + assert((uintptr_t) group % alignof(struct nexthop_grp) == 0); + + n_group = raw_group_size / sizeof(struct nexthop_grp); + for (size_t i = 0; i < n_group; i++) { + _cleanup_free_ struct nexthop_grp *nhg = NULL; + + if (group[i].id == 0) { + log_link_warning(link, "rtnl: received nexthop message with invalid ID in group, ignoring."); + return 0; + } + if (group[i].weight > 254) { + log_link_warning(link, "rtnl: received nexthop message with invalid weight in group, ignoring."); + return 0; + } + + nhg = newdup(struct nexthop_grp, group + i, 1); + if (!nhg) + return log_oom(); + + r = hashmap_ensure_put(&tmp->group, NULL, UINT32_TO_PTR(nhg->id), nhg); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to store nexthop group, ignoring: %m"); + return 0; + } + if (r > 0) + TAKE_PTR(nhg); + } + } + + if (tmp->family != AF_UNSPEC) { + r = netlink_message_read_in_addr_union(message, NHA_GATEWAY, tmp->family, &tmp->gw); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: could not get NHA_GATEWAY attribute, ignoring: %m"); + return 0; + } + } + + r = sd_netlink_message_has_flag(message, NHA_BLACKHOLE); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: could not get NHA_BLACKHOLE attribute, ignoring: %m"); + return 0; + } + tmp->blackhole = r; + + r = sd_netlink_message_read_u32(message, NHA_ID, &tmp->id); + if (r == -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received nexthop message without NHA_ID attribute, ignoring: %m"); + return 0; + } else if (r < 0) { + log_link_warning_errno(link, r, "rtnl: could not get NHA_ID attribute, ignoring: %m"); + return 0; + } else if (tmp->id == 0) { + log_link_warning(link, "rtnl: received nexthop message with invalid nexthop ID, ignoring: %m"); + return 0; + } + + /* All blackhole or group nexthops are managed by Manager. Note that the linux kernel does not + * set NHA_OID attribute when NHA_BLACKHOLE or NHA_GROUP is set. Just for safety. */ + if (!nexthop_owned_by_link(tmp)) + link = NULL; + + (void) nexthop_get(m, link, tmp, &nexthop); + + switch (type) { + case RTM_NEWNEXTHOP: + if (nexthop) { + nexthop->flags = tmp->flags; + nexthop_enter_configured(nexthop); + log_nexthop_debug(tmp, "Received remembered", link); + } else { + nexthop_enter_configured(tmp); + log_nexthop_debug(tmp, "Remembering", link); + + r = nexthop_add(m, link, tmp); + if (r < 0) { + log_link_warning_errno(link, r, "Could not remember foreign nexthop, ignoring: %m"); + return 0; + } + + TAKE_PTR(tmp); + } + + break; + case RTM_DELNEXTHOP: + if (nexthop) { + nexthop_enter_removed(nexthop); + if (nexthop->state == 0) { + log_nexthop_debug(nexthop, "Forgetting", link); + nexthop_free(nexthop); + } else + log_nexthop_debug(nexthop, "Removed", link); + } else + log_nexthop_debug(tmp, "Kernel removed unknown", link); + break; + + default: + assert_not_reached(); + } + + return 1; +} + +static int nexthop_section_verify(NextHop *nh) { + if (section_is_invalid(nh->section)) + return -EINVAL; + + if (!hashmap_isempty(nh->group)) { + if (in_addr_is_set(nh->family, &nh->gw)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: nexthop group cannot have gateway address. " + "Ignoring [NextHop] section from line %u.", + nh->section->filename, nh->section->line); + + if (nh->family != AF_UNSPEC) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: nexthop group cannot have Family= setting. " + "Ignoring [NextHop] section from line %u.", + nh->section->filename, nh->section->line); + + if (nh->blackhole && in_addr_is_set(nh->family, &nh->gw)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: nexthop group cannot be a blackhole. " + "Ignoring [NextHop] section from line %u.", + nh->section->filename, nh->section->line); + } else if (nh->family == AF_UNSPEC) + /* When neither Family=, Gateway=, nor Group= is specified, assume IPv4. */ + nh->family = AF_INET; + + if (nh->blackhole && in_addr_is_set(nh->family, &nh->gw)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: blackhole nexthop cannot have gateway address. " + "Ignoring [NextHop] section from line %u.", + nh->section->filename, nh->section->line); + + if (nh->onlink < 0 && in_addr_is_set(nh->family, &nh->gw) && + ordered_hashmap_isempty(nh->network->addresses_by_section)) { + /* If no address is configured, in most cases the gateway cannot be reachable. + * TODO: we may need to improve the condition above. */ + log_warning("%s: Gateway= without static address configured. " + "Enabling OnLink= option.", + nh->section->filename); + nh->onlink = true; + } + + if (nh->onlink >= 0) + SET_FLAG(nh->flags, RTNH_F_ONLINK, nh->onlink); + + return 0; +} + +void network_drop_invalid_nexthops(Network *network) { + NextHop *nh; + + assert(network); + + HASHMAP_FOREACH(nh, network->nexthops_by_section) + if (nexthop_section_verify(nh) < 0) + nexthop_free(nh); +} + +int config_parse_nexthop_id( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL; + Network *network = userdata; + uint32_t id; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = nexthop_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + n->id = 0; + TAKE_PTR(n); + return 0; + } + + r = safe_atou32(rvalue, &id); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse nexthop id \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + if (id == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid nexthop id \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->id = id; + TAKE_PTR(n); + return 0; +} + +int config_parse_nexthop_gateway( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = nexthop_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + n->family = AF_UNSPEC; + n->gw = IN_ADDR_NULL; + + TAKE_PTR(n); + return 0; + } + + r = in_addr_from_string_auto(rvalue, &n->family, &n->gw); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_nexthop_family( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL; + Network *network = userdata; + AddressFamily a; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = nexthop_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (isempty(rvalue) && + !in_addr_is_set(n->family, &n->gw)) { + /* Accept an empty string only when Gateway= is null or not specified. */ + n->family = AF_UNSPEC; + TAKE_PTR(n); + return 0; + } + + a = nexthop_address_family_from_string(rvalue); + if (a < 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + if (in_addr_is_set(n->family, &n->gw) && + ((a == ADDRESS_FAMILY_IPV4 && n->family == AF_INET6) || + (a == ADDRESS_FAMILY_IPV6 && n->family == AF_INET))) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified family '%s' conflicts with the family of the previously specified Gateway=, " + "ignoring assignment.", rvalue); + return 0; + } + + switch (a) { + case ADDRESS_FAMILY_IPV4: + n->family = AF_INET; + break; + case ADDRESS_FAMILY_IPV6: + n->family = AF_INET6; + break; + default: + assert_not_reached(); + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_nexthop_onlink( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = nexthop_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = parse_tristate(rvalue, &n->onlink); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_nexthop_blackhole( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = nexthop_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + n->blackhole = r; + + TAKE_PTR(n); + return 0; +} + +int config_parse_nexthop_group( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(nexthop_free_or_set_invalidp) NextHop *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = nexthop_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + n->group = hashmap_free_free(n->group); + TAKE_PTR(n); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ struct nexthop_grp *nhg = NULL; + _cleanup_free_ char *word = NULL; + uint32_t w; + char *sep; + + r = extract_first_word(&p, &word, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + if (r == 0) + break; + + nhg = new0(struct nexthop_grp, 1); + if (!nhg) + return log_oom(); + + sep = strchr(word, ':'); + if (sep) { + *sep++ = '\0'; + r = safe_atou32(sep, &w); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse weight for nexthop group, ignoring assignment: %s:%s", + word, sep); + continue; + } + if (w == 0 || w > 256) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid weight for nexthop group, ignoring assignment: %s:%s", + word, sep); + continue; + } + /* See comments in config_parse_multipath_route(). */ + nhg->weight = w - 1; + } + + r = safe_atou32(word, &nhg->id); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse nexthop ID in %s=, ignoring assignment: %s%s%s", + lvalue, word, sep ? ":" : "", strempty(sep)); + continue; + } + if (nhg->id == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Nexthop ID in %s= must be positive, ignoring assignment: %s%s%s", + lvalue, word, sep ? ":" : "", strempty(sep)); + continue; + } + + r = hashmap_ensure_put(&n->group, NULL, UINT32_TO_PTR(nhg->id), nhg); + if (r == -ENOMEM) + return log_oom(); + if (r == -EEXIST) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Nexthop ID %"PRIu32" is specified multiple times in %s=, ignoring assignment: %s%s%s", + nhg->id, lvalue, word, sep ? ":" : "", strempty(sep)); + continue; + } + assert(r > 0); + TAKE_PTR(nhg); + } + + TAKE_PTR(n); + return 0; +} diff --git a/src/network/networkd-nexthop.h b/src/network/networkd-nexthop.h new file mode 100644 index 0000000..6f2aa6f --- /dev/null +++ b/src/network/networkd-nexthop.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. + */ + +#pragma once + +#include <inttypes.h> + +#include "sd-netlink.h" + +#include "conf-parser.h" +#include "hashmap.h" +#include "in-addr-util.h" +#include "networkd-util.h" + +typedef struct Link Link; +typedef struct Manager Manager; +typedef struct Network Network; + +typedef struct NextHop { + Network *network; + Manager *manager; + Link *link; + ConfigSection *section; + NetworkConfigSource source; + NetworkConfigState state; + + uint8_t protocol; + + uint32_t id; + bool blackhole; + int family; + union in_addr_union gw; + uint8_t flags; + int onlink; /* Only used in conf parser and nexthop_section_verify(). */ + Hashmap *group; +} NextHop; + +NextHop *nexthop_free(NextHop *nexthop); + +void network_drop_invalid_nexthops(Network *network); + +int link_drop_managed_nexthops(Link *link); +int link_drop_foreign_nexthops(Link *link); +void link_foreignize_nexthops(Link *link); + +int link_request_static_nexthops(Link *link, bool only_ipv4); + +int manager_get_nexthop_by_id(Manager *manager, uint32_t id, NextHop **ret); +int manager_rtnl_process_nexthop(sd_netlink *rtnl, sd_netlink_message *message, Manager *m); + +DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(NextHop, nexthop); + +CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_id); +CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_gateway); +CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_family); +CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_onlink); +CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_blackhole); +CONFIG_PARSER_PROTOTYPE(config_parse_nexthop_group); diff --git a/src/network/networkd-queue.c b/src/network/networkd-queue.c new file mode 100644 index 0000000..1128987 --- /dev/null +++ b/src/network/networkd-queue.c @@ -0,0 +1,333 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "netdev.h" +#include "netlink-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-queue.h" +#include "string-table.h" + +#define REPLY_CALLBACK_COUNT_THRESHOLD 128 + +static Request *request_free(Request *req) { + if (!req) + return NULL; + + /* To prevent from triggering assertions in the hash and compare functions, remove this request + * from the set before freeing userdata below. */ + if (req->manager) + ordered_set_remove(req->manager->request_queue, req); + + if (req->free_func) + req->free_func(req->userdata); + + if (req->counter) + (*req->counter)--; + + link_unref(req->link); /* link may be NULL, but link_unref() can handle it gracefully. */ + + return mfree(req); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(Request, request, request_free); + +void request_detach(Manager *manager, Request *req) { + assert(manager); + + if (!req) + return; + + req = ordered_set_remove(manager->request_queue, req); + if (!req) + return; + + req->manager = NULL; + request_unref(req); +} + +static void request_destroy_callback(Request *req) { + assert(req); + + if (req->manager) + request_detach(req->manager, req); + + request_unref(req); +} + +static void request_hash_func(const Request *req, struct siphash *state) { + assert(req); + assert(state); + + siphash24_compress_boolean(req->link, state); + if (req->link) + siphash24_compress(&req->link->ifindex, sizeof(req->link->ifindex), state); + + siphash24_compress(&req->type, sizeof(req->type), state); + + siphash24_compress(&req->hash_func, sizeof(req->hash_func), state); + siphash24_compress(&req->compare_func, sizeof(req->compare_func), state); + + if (req->hash_func) + req->hash_func(req->userdata, state); +} + +static int request_compare_func(const struct Request *a, const struct Request *b) { + int r; + + assert(a); + assert(b); + + r = CMP(!!a->link, !!b->link); + if (r != 0) + return r; + + if (a->link) { + r = CMP(a->link->ifindex, b->link->ifindex); + if (r != 0) + return r; + } + + r = CMP(a->type, b->type); + if (r != 0) + return r; + + r = CMP(PTR_TO_UINT64(a->hash_func), PTR_TO_UINT64(b->hash_func)); + if (r != 0) + return r; + + r = CMP(PTR_TO_UINT64(a->compare_func), PTR_TO_UINT64(b->compare_func)); + if (r != 0) + return r; + + if (a->compare_func) + return a->compare_func(a->userdata, b->userdata); + + return 0; +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + request_hash_ops, + Request, + request_hash_func, + request_compare_func, + request_unref); + +static int request_new( + Manager *manager, + Link *link, + RequestType type, + void *userdata, + mfree_func_t free_func, + hash_func_t hash_func, + compare_func_t compare_func, + request_process_func_t process, + unsigned *counter, + request_netlink_handler_t netlink_handler, + Request **ret) { + + _cleanup_(request_unrefp) Request *req = NULL; + Request *existing; + int r; + + assert(manager); + assert(process); + + req = new(Request, 1); + if (!req) + return -ENOMEM; + + *req = (Request) { + .n_ref = 1, + .link = link_ref(link), /* link may be NULL, but link_ref() handles it gracefully. */ + .type = type, + .userdata = userdata, + .hash_func = hash_func, + .compare_func = compare_func, + .process = process, + .netlink_handler = netlink_handler, + }; + + existing = ordered_set_get(manager->request_queue, req); + if (existing) { + if (ret) + *ret = existing; + return 0; + } + + r = ordered_set_ensure_put(&manager->request_queue, &request_hash_ops, req); + if (r < 0) + return r; + + req->manager = manager; + req->free_func = free_func; + req->counter = counter; + if (req->counter) + (*req->counter)++; + + if (ret) + *ret = req; + + TAKE_PTR(req); + return 1; +} + +int netdev_queue_request( + NetDev *netdev, + request_process_func_t process, + Request **ret) { + + int r; + + assert(netdev); + + r = request_new(netdev->manager, NULL, REQUEST_TYPE_NETDEV_INDEPENDENT, + netdev, (mfree_func_t) netdev_unref, + trivial_hash_func, trivial_compare_func, + process, NULL, NULL, ret); + if (r <= 0) + return r; + + netdev_ref(netdev); + return 1; +} + +int link_queue_request_full( + Link *link, + RequestType type, + void *userdata, + mfree_func_t free_func, + hash_func_t hash_func, + compare_func_t compare_func, + request_process_func_t process, + unsigned *counter, + request_netlink_handler_t netlink_handler, + Request **ret) { + + assert(link); + + return request_new(link->manager, link, type, + userdata, free_func, hash_func, compare_func, + process, counter, netlink_handler, ret); +} + +int manager_process_requests(sd_event_source *s, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + int r; + + for (;;) { + bool processed = false; + Request *req; + + ORDERED_SET_FOREACH(req, manager->request_queue) { + _cleanup_(link_unrefp) Link *link = link_ref(req->link); + + assert(req->process); + + if (req->waiting_reply) + continue; /* Waiting for netlink reply. */ + + /* Typically, requests send netlink message asynchronously. If there are many requests + * queued, then this event may make reply callback queue in sd-netlink full. */ + if (netlink_get_reply_callback_count(manager->rtnl) >= REPLY_CALLBACK_COUNT_THRESHOLD || + netlink_get_reply_callback_count(manager->genl) >= REPLY_CALLBACK_COUNT_THRESHOLD || + fw_ctx_get_reply_callback_count(manager->fw_ctx) >= REPLY_CALLBACK_COUNT_THRESHOLD) + return 0; + + r = req->process(req, link, req->userdata); + if (r == 0) + continue; + + processed = true; + + /* If the request sends netlink message, e.g. for Address or so, the Request object + * is referenced by the netlink slot, and will be detached later by its destroy callback. + * Otherwise, e.g. for DHCP client or so, detach the request from queue now. */ + if (!req->waiting_reply) + request_detach(manager, req); + + if (r < 0 && link) { + link_enter_failed(link); + /* link_enter_failed() may remove multiple requests, + * hence we need to exit from the loop. */ + break; + } + } + + /* When at least one request is processed, then another request may be ready now. */ + if (!processed) + break; + } + + return 0; +} + +static int request_netlink_handler(sd_netlink *nl, sd_netlink_message *m, Request *req) { + assert(req); + + if (req->counter) { + assert(*req->counter > 0); + (*req->counter)--; + req->counter = NULL; /* To prevent double decrement on free. */ + } + + if (req->link && IN_SET(req->link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + if (req->netlink_handler) + return req->netlink_handler(nl, m, req, req->link, req->userdata); + + return 0; +} + +int request_call_netlink_async(sd_netlink *nl, sd_netlink_message *m, Request *req) { + int r; + + assert(nl); + assert(m); + assert(req); + + r = netlink_call_async(nl, NULL, m, request_netlink_handler, request_destroy_callback, req); + if (r < 0) + return r; + + request_ref(req); + req->waiting_reply = true; + return 0; +} + +static const char *const request_type_table[_REQUEST_TYPE_MAX] = { + [REQUEST_TYPE_ACTIVATE_LINK] = "activate link", + [REQUEST_TYPE_ADDRESS] = "address", + [REQUEST_TYPE_ADDRESS_LABEL] = "address label", + [REQUEST_TYPE_BRIDGE_FDB] = "bridge FDB", + [REQUEST_TYPE_BRIDGE_MDB] = "bridge MDB", + [REQUEST_TYPE_DHCP_SERVER] = "DHCP server", + [REQUEST_TYPE_DHCP4_CLIENT] = "DHCPv4 client", + [REQUEST_TYPE_DHCP6_CLIENT] = "DHCPv6 client", + [REQUEST_TYPE_IPV6_PROXY_NDP] = "IPv6 proxy NDP", + [REQUEST_TYPE_NDISC] = "NDisc", + [REQUEST_TYPE_NEIGHBOR] = "neighbor", + [REQUEST_TYPE_NETDEV_INDEPENDENT] = "independent netdev", + [REQUEST_TYPE_NETDEV_STACKED] = "stacked netdev", + [REQUEST_TYPE_NEXTHOP] = "nexthop", + [REQUEST_TYPE_RADV] = "RADV", + [REQUEST_TYPE_ROUTE] = "route", + [REQUEST_TYPE_ROUTING_POLICY_RULE] = "routing policy rule", + [REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE] = "IPv6LL address generation mode", + [REQUEST_TYPE_SET_LINK_BOND] = "bond configurations", + [REQUEST_TYPE_SET_LINK_BRIDGE] = "bridge configurations", + [REQUEST_TYPE_SET_LINK_BRIDGE_VLAN] = "bridge VLAN configurations", + [REQUEST_TYPE_SET_LINK_CAN] = "CAN interface configurations", + [REQUEST_TYPE_SET_LINK_FLAGS] = "link flags", + [REQUEST_TYPE_SET_LINK_GROUP] = "interface group", + [REQUEST_TYPE_SET_LINK_IPOIB] = "IPoIB configurations", + [REQUEST_TYPE_SET_LINK_MAC] = "MAC address", + [REQUEST_TYPE_SET_LINK_MASTER] = "master interface", + [REQUEST_TYPE_SET_LINK_MTU] = "MTU", + [REQUEST_TYPE_SRIOV] = "SR-IOV", + [REQUEST_TYPE_TC_QDISC] = "QDisc", + [REQUEST_TYPE_TC_CLASS] = "TClass", + [REQUEST_TYPE_UP_DOWN] = "bring link up or down", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(request_type, RequestType); diff --git a/src/network/networkd-queue.h b/src/network/networkd-queue.h new file mode 100644 index 0000000..e58d1be --- /dev/null +++ b/src/network/networkd-queue.h @@ -0,0 +1,141 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-event.h" +#include "sd-netlink.h" + +#include "alloc-util.h" +#include "hash-funcs.h" + +typedef struct Link Link; +typedef struct NetDev NetDev; +typedef struct Manager Manager; +typedef struct Request Request; + +typedef int (*request_process_func_t)(Request *req, Link *link, void *userdata); +typedef int (*request_netlink_handler_t)(sd_netlink *nl, sd_netlink_message *m, Request *req, Link *link, void *userdata); + +typedef enum RequestType { + REQUEST_TYPE_ACTIVATE_LINK, + REQUEST_TYPE_ADDRESS, + REQUEST_TYPE_ADDRESS_LABEL, + REQUEST_TYPE_BRIDGE_FDB, + REQUEST_TYPE_BRIDGE_MDB, + REQUEST_TYPE_DHCP_SERVER, + REQUEST_TYPE_DHCP4_CLIENT, + REQUEST_TYPE_DHCP6_CLIENT, + REQUEST_TYPE_IPV6_PROXY_NDP, + REQUEST_TYPE_NDISC, + REQUEST_TYPE_NEIGHBOR, + REQUEST_TYPE_NETDEV_INDEPENDENT, + REQUEST_TYPE_NETDEV_STACKED, + REQUEST_TYPE_NEXTHOP, + REQUEST_TYPE_RADV, + REQUEST_TYPE_ROUTE, + REQUEST_TYPE_ROUTING_POLICY_RULE, + REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE, /* Setting IPv6LL address generation mode. */ + REQUEST_TYPE_SET_LINK_BOND, /* Setting bond configs. */ + REQUEST_TYPE_SET_LINK_BRIDGE, /* Setting bridge configs. */ + REQUEST_TYPE_SET_LINK_BRIDGE_VLAN, /* Setting bridge VLAN configs. */ + REQUEST_TYPE_SET_LINK_CAN, /* Setting CAN interface configs. */ + REQUEST_TYPE_SET_LINK_FLAGS, /* Setting IFF_NOARP or friends. */ + REQUEST_TYPE_SET_LINK_GROUP, /* Setting interface group. */ + REQUEST_TYPE_SET_LINK_IPOIB, /* Setting IPoIB configs. */ + REQUEST_TYPE_SET_LINK_MAC, /* Setting MAC address. */ + REQUEST_TYPE_SET_LINK_MASTER, /* Setting IFLA_MASTER. */ + REQUEST_TYPE_SET_LINK_MTU, /* Setting MTU. */ + REQUEST_TYPE_SRIOV, + REQUEST_TYPE_TC_CLASS, + REQUEST_TYPE_TC_QDISC, + REQUEST_TYPE_UP_DOWN, + _REQUEST_TYPE_MAX, + _REQUEST_TYPE_INVALID = -EINVAL, +} RequestType; + +struct Request { + unsigned n_ref; + + Manager *manager; /* must be non-NULL */ + Link *link; /* can be NULL */ + + RequestType type; + + /* Target object, e.g. Address, Route, NetDev, and so on. */ + void *userdata; + /* freeing userdata when the request is completed or failed. */ + mfree_func_t free_func; + + /* hash and compare functions for userdata, used for dedup requests. */ + hash_func_t hash_func; + compare_func_t compare_func; + + /* Checks the request dependencies, and then processes this request, e.g. call address_configure(). + * Return 1 when processed, 0 when its dependencies not resolved, and negative errno on failure. */ + request_process_func_t process; + + /* incremented when requested, decremented when request is completed or failed. */ + unsigned *counter; + /* called in netlink handler, the 'counter' is decremented before this is called. + * If this is specified, then the 'process' function must increment the reference of this + * request, and pass this request to the netlink_call_async(), and set the destroy function + * to the slot. */ + request_netlink_handler_t netlink_handler; + + bool waiting_reply; +}; + +Request *request_ref(Request *req); +Request *request_unref(Request *req); +DEFINE_TRIVIAL_CLEANUP_FUNC(Request*, request_unref); + +void request_detach(Manager *manager, Request *req); + +int netdev_queue_request( + NetDev *netdev, + request_process_func_t process, + Request **ret); + +int link_queue_request_full( + Link *link, + RequestType type, + void *userdata, + mfree_func_t free_func, + hash_func_t hash_func, + compare_func_t compare_func, + request_process_func_t process, + unsigned *counter, + request_netlink_handler_t netlink_handler, + Request **ret); + +static inline int link_queue_request( + Link *link, + RequestType type, + request_process_func_t process, + Request **ret) { + + return link_queue_request_full(link, type, NULL, NULL, NULL, NULL, + process, NULL, NULL, ret); +} + +#define link_queue_request_safe(link, type, userdata, free_func, hash_func, compare_func, process, counter, netlink_handler, ret) \ + ({ \ + typeof(userdata) (*_f)(typeof(userdata)) = (free_func); \ + void (*_h)(const typeof(*userdata)*, struct siphash*) = (hash_func); \ + int (*_c)(const typeof(*userdata)*, const typeof(*userdata)*) = (compare_func); \ + int (*_p)(Request*, Link*, typeof(userdata)) = (process); \ + int (*_n)(sd_netlink*, sd_netlink_message*, Request*, Link*, typeof(userdata)) = (netlink_handler); \ + \ + link_queue_request_full(link, type, userdata, \ + (mfree_func_t) _f, \ + (hash_func_t) _h, \ + (compare_func_t) _c, \ + (request_process_func_t) _p, \ + counter, \ + (request_netlink_handler_t) _n, \ + ret); \ + }) + +int manager_process_requests(sd_event_source *s, void *userdata); +int request_call_netlink_async(sd_netlink *nl, sd_netlink_message *m, Request *req); + +const char* request_type_to_string(RequestType t) _const_; diff --git a/src/network/networkd-radv.c b/src/network/networkd-radv.c new file mode 100644 index 0000000..fc36a00 --- /dev/null +++ b/src/network/networkd-radv.c @@ -0,0 +1,1619 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2017 Intel Corporation. All rights reserved. +***/ + +#include <netinet/icmp6.h> +#include <arpa/inet.h> + +#include "dns-domain.h" +#include "networkd-address-generation.h" +#include "networkd-address.h" +#include "networkd-dhcp-prefix-delegation.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "networkd-radv.h" +#include "networkd-route-util.h" +#include "parse-util.h" +#include "radv-internal.h" +#include "string-util.h" +#include "string-table.h" +#include "strv.h" + +void network_adjust_radv(Network *network) { + assert(network); + + /* After this function is called, network->router_prefix_delegation can be treated as a boolean. */ + + if (network->dhcp_pd < 0) + /* For backward compatibility. */ + network->dhcp_pd = FLAGS_SET(network->router_prefix_delegation, RADV_PREFIX_DELEGATION_DHCP6); + + if (!FLAGS_SET(network->link_local, ADDRESS_FAMILY_IPV6)) { + if (network->router_prefix_delegation != RADV_PREFIX_DELEGATION_NONE) + log_warning("%s: IPv6PrefixDelegation= is enabled but IPv6 link-local addressing is disabled. " + "Disabling IPv6PrefixDelegation=.", network->filename); + + network->router_prefix_delegation = RADV_PREFIX_DELEGATION_NONE; + } + + if (network->router_prefix_delegation == RADV_PREFIX_DELEGATION_NONE) { + network->n_router_dns = 0; + network->router_dns = mfree(network->router_dns); + network->router_search_domains = ordered_set_free(network->router_search_domains); + } + + if (!FLAGS_SET(network->router_prefix_delegation, RADV_PREFIX_DELEGATION_STATIC)) { + network->prefixes_by_section = hashmap_free_with_destructor(network->prefixes_by_section, prefix_free); + network->route_prefixes_by_section = hashmap_free_with_destructor(network->route_prefixes_by_section, route_prefix_free); + network->pref64_prefixes_by_section = hashmap_free_with_destructor(network->pref64_prefixes_by_section, pref64_prefix_free); + } +} + +bool link_radv_enabled(Link *link) { + assert(link); + + if (!link_may_have_ipv6ll(link, /* check_multicast = */ true)) + return false; + + if (link->hw_addr.length != ETH_ALEN) + return false; + + return link->network->router_prefix_delegation; +} + +Prefix *prefix_free(Prefix *prefix) { + if (!prefix) + return NULL; + + if (prefix->network) { + assert(prefix->section); + hashmap_remove(prefix->network->prefixes_by_section, prefix->section); + } + + config_section_free(prefix->section); + set_free(prefix->tokens); + + return mfree(prefix); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(Prefix, prefix_free); + +static int prefix_new_static(Network *network, const char *filename, unsigned section_line, Prefix **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(prefix_freep) Prefix *prefix = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + prefix = hashmap_get(network->prefixes_by_section, n); + if (prefix) { + *ret = TAKE_PTR(prefix); + return 0; + } + + prefix = new(Prefix, 1); + if (!prefix) + return -ENOMEM; + + *prefix = (Prefix) { + .network = network, + .section = TAKE_PTR(n), + + .preferred_lifetime = RADV_DEFAULT_PREFERRED_LIFETIME_USEC, + .valid_lifetime = RADV_DEFAULT_VALID_LIFETIME_USEC, + .onlink = true, + .address_auto_configuration = true, + }; + + r = hashmap_ensure_put(&network->prefixes_by_section, &config_section_hash_ops, prefix->section, prefix); + if (r < 0) + return r; + + *ret = TAKE_PTR(prefix); + return 0; +} + +RoutePrefix *route_prefix_free(RoutePrefix *prefix) { + if (!prefix) + return NULL; + + if (prefix->network) { + assert(prefix->section); + hashmap_remove(prefix->network->route_prefixes_by_section, prefix->section); + } + + config_section_free(prefix->section); + + return mfree(prefix); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(RoutePrefix, route_prefix_free); + +static int route_prefix_new_static(Network *network, const char *filename, unsigned section_line, RoutePrefix **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(route_prefix_freep) RoutePrefix *prefix = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + prefix = hashmap_get(network->route_prefixes_by_section, n); + if (prefix) { + *ret = TAKE_PTR(prefix); + return 0; + } + + prefix = new(RoutePrefix, 1); + if (!prefix) + return -ENOMEM; + + *prefix = (RoutePrefix) { + .network = network, + .section = TAKE_PTR(n), + + .lifetime = RADV_DEFAULT_VALID_LIFETIME_USEC, + }; + + r = hashmap_ensure_put(&network->route_prefixes_by_section, &config_section_hash_ops, prefix->section, prefix); + if (r < 0) + return r; + + *ret = TAKE_PTR(prefix); + return 0; +} + +pref64Prefix *pref64_prefix_free(pref64Prefix *prefix) { + if (!prefix) + return NULL; + + if (prefix->network) { + assert(prefix->section); + hashmap_remove(prefix->network->pref64_prefixes_by_section, prefix->section); + } + + config_section_free(prefix->section); + + return mfree(prefix); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(pref64Prefix, pref64_prefix_free); + +static int pref64_prefix_new_static(Network *network, const char *filename, unsigned section_line, pref64Prefix **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(pref64_prefix_freep) pref64Prefix *prefix = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + prefix = hashmap_get(network->pref64_prefixes_by_section, n); + if (prefix) { + *ret = TAKE_PTR(prefix); + return 0; + } + + prefix = new(pref64Prefix, 1); + if (!prefix) + return -ENOMEM; + + *prefix = (pref64Prefix) { + .network = network, + .section = TAKE_PTR(n), + + .lifetime = RADV_PREF64_DEFAULT_LIFETIME_USEC, + }; + + r = hashmap_ensure_put(&network->pref64_prefixes_by_section, &config_section_hash_ops, prefix->section, prefix); + if (r < 0) + return r; + + *ret = TAKE_PTR(prefix); + return 0; +} + +int link_request_radv_addresses(Link *link) { + Prefix *p; + int r; + + assert(link); + + if (!link_radv_enabled(link)) + return 0; + + HASHMAP_FOREACH(p, link->network->prefixes_by_section) { + _cleanup_set_free_ Set *addresses = NULL; + struct in6_addr *a; + + if (!p->assign) + continue; + + /* radv_generate_addresses() below requires the prefix length <= 64. */ + if (p->prefixlen > 64) + continue; + + r = radv_generate_addresses(link, p->tokens, &p->prefix, p->prefixlen, &addresses); + if (r < 0) + return r; + + SET_FOREACH(a, addresses) { + _cleanup_(address_freep) Address *address = NULL; + + r = address_new(&address); + if (r < 0) + return -ENOMEM; + + address->source = NETWORK_CONFIG_SOURCE_STATIC; + address->family = AF_INET6; + address->in_addr.in6 = *a; + address->prefixlen = p->prefixlen; + address->route_metric = p->route_metric; + + r = link_request_static_address(link, address); + if (r < 0) + return r; + } + } + + return 0; +} + +static int radv_set_prefix(Link *link, Prefix *prefix) { + _cleanup_(sd_radv_prefix_unrefp) sd_radv_prefix *p = NULL; + int r; + + assert(link); + assert(link->radv); + assert(prefix); + + r = sd_radv_prefix_new(&p); + if (r < 0) + return r; + + r = sd_radv_prefix_set_prefix(p, &prefix->prefix, prefix->prefixlen); + if (r < 0) + return r; + + r = sd_radv_prefix_set_preferred_lifetime(p, prefix->preferred_lifetime, USEC_INFINITY); + if (r < 0) + return r; + + r = sd_radv_prefix_set_valid_lifetime(p, prefix->valid_lifetime, USEC_INFINITY); + if (r < 0) + return r; + + r = sd_radv_prefix_set_onlink(p, prefix->onlink); + if (r < 0) + return r; + + r = sd_radv_prefix_set_address_autoconfiguration(p, prefix->address_auto_configuration); + if (r < 0) + return r; + + return sd_radv_add_prefix(link->radv, p); +} + +static int radv_set_route_prefix(Link *link, RoutePrefix *prefix) { + _cleanup_(sd_radv_route_prefix_unrefp) sd_radv_route_prefix *p = NULL; + int r; + + assert(link); + assert(link->radv); + assert(prefix); + + r = sd_radv_route_prefix_new(&p); + if (r < 0) + return r; + + r = sd_radv_route_prefix_set_prefix(p, &prefix->prefix, prefix->prefixlen); + if (r < 0) + return r; + + r = sd_radv_route_prefix_set_lifetime(p, prefix->lifetime, USEC_INFINITY); + if (r < 0) + return r; + + return sd_radv_add_route_prefix(link->radv, p); +} + +static int radv_set_pref64_prefix(Link *link, pref64Prefix *prefix) { + _cleanup_(sd_radv_pref64_prefix_unrefp) sd_radv_pref64_prefix *p = NULL; + int r; + + assert(link); + assert(link->radv); + assert(prefix); + + r = sd_radv_pref64_prefix_new(&p); + if (r < 0) + return r; + + r = sd_radv_pref64_prefix_set_prefix(p, &prefix->prefix, prefix->prefixlen, prefix->lifetime); + if (r < 0) + return r; + + return sd_radv_add_pref64_prefix(link->radv, p); +} + +static int network_get_ipv6_dns(Network *network, struct in6_addr **ret_addresses, size_t *ret_size) { + _cleanup_free_ struct in6_addr *addresses = NULL; + size_t n_addresses = 0; + + assert(network); + assert(ret_addresses); + assert(ret_size); + + for (size_t i = 0; i < network->n_dns; i++) { + union in_addr_union *addr; + + if (network->dns[i]->family != AF_INET6) + continue; + + addr = &network->dns[i]->address; + + if (in_addr_is_null(AF_INET6, addr) || + in_addr_is_link_local(AF_INET6, addr) || + in_addr_is_localhost(AF_INET6, addr)) + continue; + + if (!GREEDY_REALLOC(addresses, n_addresses + 1)) + return -ENOMEM; + + addresses[n_addresses++] = addr->in6; + } + + *ret_addresses = TAKE_PTR(addresses); + *ret_size = n_addresses; + + return n_addresses; +} + +static int radv_set_dns(Link *link, Link *uplink) { + _cleanup_free_ struct in6_addr *dns = NULL; + size_t n_dns; + int r; + + if (!link->network->router_emit_dns) + return 0; + + if (link->network->router_dns) { + struct in6_addr *p; + + dns = new(struct in6_addr, link->network->n_router_dns); + if (!dns) + return -ENOMEM; + + p = dns; + for (size_t i = 0; i < link->network->n_router_dns; i++) + if (in6_addr_is_null(&link->network->router_dns[i])) { + if (in6_addr_is_set(&link->ipv6ll_address)) + *(p++) = link->ipv6ll_address; + } else + *(p++) = link->network->router_dns[i]; + + n_dns = p - dns; + + goto set_dns; + } + + r = network_get_ipv6_dns(link->network, &dns, &n_dns); + if (r > 0) + goto set_dns; + + if (uplink) { + assert(uplink->network); + + r = network_get_ipv6_dns(uplink->network, &dns, &n_dns); + if (r > 0) + goto set_dns; + } + + return 0; + +set_dns: + return sd_radv_set_rdnss(link->radv, + link->network->router_dns_lifetime_usec, + dns, n_dns); +} + +static int radv_set_domains(Link *link, Link *uplink) { + _cleanup_free_ char **s = NULL; /* just free() because the strings are owned by the set */ + OrderedSet *search_domains; + + if (!link->network->router_emit_domains) + return 0; + + search_domains = link->network->router_search_domains; + + if (search_domains) + goto set_domains; + + search_domains = link->network->search_domains; + if (search_domains) + goto set_domains; + + if (uplink) { + assert(uplink->network); + + search_domains = uplink->network->search_domains; + if (search_domains) + goto set_domains; + } + + return 0; + +set_domains: + s = ordered_set_get_strv(search_domains); + if (!s) + return log_oom(); + + return sd_radv_set_dnssl(link->radv, + link->network->router_dns_lifetime_usec, + s); + +} + +static int radv_find_uplink(Link *link, Link **ret) { + int r; + + assert(link); + + if (link->network->router_uplink_name) + return link_get_by_name(link->manager, link->network->router_uplink_name, ret); + + if (link->network->router_uplink_index > 0) + return link_get_by_index(link->manager, link->network->router_uplink_index, ret); + + if (link->network->router_uplink_index == UPLINK_INDEX_AUTO) { + if (link_dhcp_pd_is_enabled(link)) + r = dhcp_pd_find_uplink(link, ret); /* When DHCP-PD is enabled, use its uplink. */ + else + r = manager_find_uplink(link->manager, AF_INET6, link, ret); + if (r < 0) + /* It is not necessary to propagate error in automatic selection. */ + *ret = NULL; + return 0; + } + + *ret = NULL; + return 0; +} + +static int radv_configure(Link *link) { + Link *uplink = NULL; + RoutePrefix *q; + pref64Prefix *n; + Prefix *p; + int r; + + assert(link); + assert(link->network); + + if (link->radv) + return -EBUSY; + + r = sd_radv_new(&link->radv); + if (r < 0) + return r; + + r = sd_radv_attach_event(link->radv, link->manager->event, 0); + if (r < 0) + return r; + + if (link->hw_addr.length == ETH_ALEN) { + r = sd_radv_set_mac(link->radv, &link->hw_addr.ether); + if (r < 0) + return r; + } + + r = sd_radv_set_ifindex(link->radv, link->ifindex); + if (r < 0) + return r; + + r = sd_radv_set_managed_information(link->radv, link->network->router_managed); + if (r < 0) + return r; + + r = sd_radv_set_other_information(link->radv, link->network->router_other_information); + if (r < 0) + return r; + + r = sd_radv_set_router_lifetime(link->radv, link->network->router_lifetime_usec); + if (r < 0) + return r; + + r = sd_radv_set_hop_limit(link->radv, link->network->router_hop_limit); + if (r < 0) + return r; + + if (link->network->router_lifetime_usec > 0) { + r = sd_radv_set_preference(link->radv, link->network->router_preference); + if (r < 0) + return r; + } + + if (link->network->router_retransmit_usec > 0) { + r = sd_radv_set_retransmit(link->radv, link->network->router_retransmit_usec); + if (r < 0) + return r; + } + + HASHMAP_FOREACH(p, link->network->prefixes_by_section) { + r = radv_set_prefix(link, p); + if (r < 0 && r != -EEXIST) + return r; + } + + HASHMAP_FOREACH(q, link->network->route_prefixes_by_section) { + r = radv_set_route_prefix(link, q); + if (r < 0 && r != -EEXIST) + return r; + } + + HASHMAP_FOREACH(n, link->network->pref64_prefixes_by_section) { + r = radv_set_pref64_prefix(link, n); + if (r < 0 && r != -EEXIST) + return r; + } + + (void) radv_find_uplink(link, &uplink); + + r = radv_set_dns(link, uplink); + if (r < 0) + return log_link_debug_errno(link, r, "Could not set RA DNS: %m"); + + r = radv_set_domains(link, uplink); + if (r < 0) + return log_link_debug_errno(link, r, "Could not set RA Domains: %m"); + + r = sd_radv_set_home_agent_information(link->radv, link->network->router_home_agent_information); + if (r < 0) + return r; + + r = sd_radv_set_home_agent_preference(link->radv, link->network->router_home_agent_preference); + if (r < 0) + return r; + + r = sd_radv_set_home_agent_lifetime(link->radv, link->network->home_agent_lifetime_usec); + if (r < 0) + return r; + + return 0; +} + +int radv_update_mac(Link *link) { + bool restart; + int r; + + assert(link); + + if (!link->radv) + return 0; + + if (link->hw_addr.length != ETH_ALEN) + return 0; + + restart = sd_radv_is_running(link->radv); + + r = sd_radv_stop(link->radv); + if (r < 0) + return r; + + r = sd_radv_set_mac(link->radv, &link->hw_addr.ether); + if (r < 0) + return r; + + if (restart) { + r = sd_radv_start(link->radv); + if (r < 0) + return r; + } + + return 0; +} + +static int radv_is_ready_to_configure(Link *link) { + bool needs_uplink = false; + int r; + + assert(link); + assert(link->network); + + if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) + return false; + + if (in6_addr_is_null(&link->ipv6ll_address)) + return false; + + if (link->hw_addr.length != ETH_ALEN || hw_addr_is_null(&link->hw_addr)) + return false; + + if (link->network->router_emit_dns && !link->network->router_dns) { + _cleanup_free_ struct in6_addr *dns = NULL; + size_t n_dns; + + r = network_get_ipv6_dns(link->network, &dns, &n_dns); + if (r < 0) + return r; + + needs_uplink = r == 0; + } + + if (link->network->router_emit_domains && + !link->network->router_search_domains && + !link->network->search_domains) + needs_uplink = true; + + if (needs_uplink) { + Link *uplink = NULL; + + if (radv_find_uplink(link, &uplink) < 0) + return false; + + if (uplink && !uplink->network) + return false; + } + + return true; +} + +static int radv_process_request(Request *req, Link *link, void *userdata) { + int r; + + assert(link); + + r = radv_is_ready_to_configure(link); + if (r <= 0) + return r; + + r = radv_configure(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure IPv6 Router Advertisement engine: %m"); + + if (link_has_carrier(link)) { + r = radv_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start IPv6 Router Advertisement engine: %m"); + } + + log_link_debug(link, "IPv6 Router Advertisement engine is configured%s.", + link_has_carrier(link) ? " and started" : ""); + return 1; +} + +int link_request_radv(Link *link) { + int r; + + assert(link); + + if (!link_radv_enabled(link)) + return 0; + + if (link->radv) + return 0; + + r = link_queue_request(link, REQUEST_TYPE_RADV, radv_process_request, NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request configuring of the IPv6 Router Advertisement engine: %m"); + + log_link_debug(link, "Requested configuring of the IPv6 Router Advertisement engine."); + return 0; +} + +int radv_start(Link *link) { + int r; + + assert(link); + assert(link->network); + + if (!link->radv) + return 0; + + if (!link_has_carrier(link)) + return 0; + + if (in6_addr_is_null(&link->ipv6ll_address)) + return 0; + + if (sd_radv_is_running(link->radv)) + return 0; + + if (link->network->dhcp_pd_announce) { + r = dhcp_request_prefix_delegation(link); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to request DHCP delegated subnet prefix: %m"); + } + + log_link_debug(link, "Starting IPv6 Router Advertisements"); + return sd_radv_start(link->radv); +} + +int radv_add_prefix( + Link *link, + const struct in6_addr *prefix, + uint8_t prefix_len, + usec_t lifetime_preferred_usec, + usec_t lifetime_valid_usec) { + + _cleanup_(sd_radv_prefix_unrefp) sd_radv_prefix *p = NULL; + int r; + + assert(link); + + if (!link->radv) + return 0; + + r = sd_radv_prefix_new(&p); + if (r < 0) + return r; + + r = sd_radv_prefix_set_prefix(p, prefix, prefix_len); + if (r < 0) + return r; + + r = sd_radv_prefix_set_preferred_lifetime(p, RADV_DEFAULT_PREFERRED_LIFETIME_USEC, lifetime_preferred_usec); + if (r < 0) + return r; + + r = sd_radv_prefix_set_valid_lifetime(p, RADV_DEFAULT_VALID_LIFETIME_USEC, lifetime_valid_usec); + if (r < 0) + return r; + + r = sd_radv_add_prefix(link->radv, p); + if (r < 0 && r != -EEXIST) + return r; + + return 0; +} + +static int prefix_section_verify(Prefix *p) { + assert(p); + + if (section_is_invalid(p->section)) + return -EINVAL; + + if (in6_addr_is_null(&p->prefix)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: [IPv6Prefix] section without Prefix= field configured, " + "or specified prefix is the null address. " + "Ignoring [IPv6Prefix] section from line %u.", + p->section->filename, p->section->line); + + if (p->prefixlen < 3 || p->prefixlen > 128) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Invalid prefix length %u is specified in [IPv6Prefix] section. " + "Valid range is 3…128. Ignoring [IPv6Prefix] section from line %u.", + p->section->filename, p->prefixlen, p->section->line); + + if (p->prefixlen > 64) { + log_info("%s:%u: Unusual prefix length %u (> 64) is specified in [IPv6Prefix] section from line %s%s.", + p->section->filename, p->section->line, + p->prefixlen, + p->assign ? ", refusing to assign an address in " : "", + p->assign ? IN6_ADDR_PREFIX_TO_STRING(&p->prefix, p->prefixlen) : ""); + + p->assign = false; + } + + if (p->valid_lifetime == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: The valid lifetime of prefix cannot be zero. " + "Ignoring [IPv6Prefix] section from line %u.", + p->section->filename, p->section->line); + + if (p->preferred_lifetime > p->valid_lifetime) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: The preferred lifetime %s is longer than the valid lifetime %s. " + "Ignoring [IPv6Prefix] section from line %u.", + p->section->filename, + FORMAT_TIMESPAN(p->preferred_lifetime, USEC_PER_SEC), + FORMAT_TIMESPAN(p->valid_lifetime, USEC_PER_SEC), + p->section->line); + + return 0; +} + +void network_drop_invalid_prefixes(Network *network) { + Prefix *p; + + assert(network); + + HASHMAP_FOREACH(p, network->prefixes_by_section) + if (prefix_section_verify(p) < 0) + prefix_free(p); +} + +static int route_prefix_section_verify(RoutePrefix *p) { + if (section_is_invalid(p->section)) + return -EINVAL; + + if (p->prefixlen > 128) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Invalid prefix length %u is specified in [IPv6RoutePrefix] section. " + "Valid range is 0…128. Ignoring [IPv6RoutePrefix] section from line %u.", + p->section->filename, p->prefixlen, p->section->line); + + if (p->lifetime == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: The lifetime of route cannot be zero. " + "Ignoring [IPv6RoutePrefix] section from line %u.", + p->section->filename, p->section->line); + + return 0; +} + +void network_drop_invalid_route_prefixes(Network *network) { + RoutePrefix *p; + + assert(network); + + HASHMAP_FOREACH(p, network->route_prefixes_by_section) + if (route_prefix_section_verify(p) < 0) + route_prefix_free(p); +} + +void network_drop_invalid_pref64_prefixes(Network *network) { + pref64Prefix *p; + + assert(network); + + HASHMAP_FOREACH(p, network->pref64_prefixes_by_section) + if (section_is_invalid(p->section)) + pref64_prefix_free(p); +} + +int config_parse_prefix( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + union in_addr_union a; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = prefix_new_static(network, filename, section_line, &p); + if (r < 0) + return log_oom(); + + r = in_addr_prefix_from_string(rvalue, AF_INET6, &a, &p->prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Prefix is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + (void) in6_addr_mask(&a.in6, p->prefixlen); + p->prefix = a.in6; + + TAKE_PTR(p); + return 0; +} + +int config_parse_prefix_boolean( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = prefix_new_static(network, filename, section_line, &p); + if (r < 0) + return log_oom(); + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + if (streq(lvalue, "OnLink")) + p->onlink = r; + else if (streq(lvalue, "AddressAutoconfiguration")) + p->address_auto_configuration = r; + else if (streq(lvalue, "Assign")) + p->assign = r; + else + assert_not_reached(); + + TAKE_PTR(p); + return 0; +} + +int config_parse_prefix_lifetime( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + usec_t usec; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = prefix_new_static(network, filename, section_line, &p); + if (r < 0) + return log_oom(); + + r = parse_sec(rvalue, &usec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Lifetime is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + if (usec != USEC_INFINITY && DIV_ROUND_UP(usec, USEC_PER_SEC) >= UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Lifetime is too long, ignoring assignment: %s", rvalue); + return 0; + } + + if (streq(lvalue, "PreferredLifetimeSec")) + p->preferred_lifetime = usec; + else if (streq(lvalue, "ValidLifetimeSec")) + p->valid_lifetime = usec; + else + assert_not_reached(); + + TAKE_PTR(p); + return 0; +} + +int config_parse_prefix_metric( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = prefix_new_static(network, filename, section_line, &p); + if (r < 0) + return log_oom(); + + r = safe_atou32(rvalue, &p->route_metric); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(p); + return 0; +} + +int config_parse_prefix_token( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(prefix_free_or_set_invalidp) Prefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = prefix_new_static(network, filename, section_line, &p); + if (r < 0) + return log_oom(); + + r = config_parse_address_generation_type(unit, filename, line, section, section_line, + lvalue, ltype, rvalue, &p->tokens, userdata); + if (r < 0) + return r; + + TAKE_PTR(p); + return 0; +} + +int config_parse_route_prefix( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(route_prefix_free_or_set_invalidp) RoutePrefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + union in_addr_union a; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = route_prefix_new_static(network, filename, section_line, &p); + if (r < 0) + return log_oom(); + + r = in_addr_prefix_from_string(rvalue, AF_INET6, &a, &p->prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Route prefix is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + (void) in6_addr_mask(&a.in6, p->prefixlen); + p->prefix = a.in6; + + TAKE_PTR(p); + return 0; +} + +int config_parse_route_prefix_lifetime( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(route_prefix_free_or_set_invalidp) RoutePrefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + usec_t usec; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = route_prefix_new_static(network, filename, section_line, &p); + if (r < 0) + return log_oom(); + + r = parse_sec(rvalue, &usec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Route lifetime is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + if (usec != USEC_INFINITY && DIV_ROUND_UP(usec, USEC_PER_SEC) >= UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Lifetime is too long, ignoring assignment: %s", rvalue); + return 0; + } + + p->lifetime = usec; + + TAKE_PTR(p); + return 0; +} + +int config_parse_pref64_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_(pref64_prefix_free_or_set_invalidp) pref64Prefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + union in_addr_union a; + uint8_t prefixlen; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = pref64_prefix_new_static(network, filename, section_line, &p); + if (r < 0) + return log_oom(); + + r = in_addr_prefix_from_string(rvalue, AF_INET6, &a, &prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "PREF64 prefix is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + if (!IN_SET(prefixlen, 96, 64, 56, 48, 40, 32)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "PREF64 prefixlen is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + (void) in6_addr_mask(&a.in6,prefixlen); + p->prefix = a.in6; + p->prefixlen = prefixlen; + + TAKE_PTR(p); + return 0; +} + +int config_parse_pref64_prefix_lifetime( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(pref64_prefix_free_or_set_invalidp) pref64Prefix *p = NULL; + Network *network = ASSERT_PTR(userdata); + usec_t usec; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + r = pref64_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, + "PREF64 lifetime is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + if (usec == USEC_INFINITY || DIV_ROUND_UP(usec, 8 * USEC_PER_SEC) >= UINT64_C(1) << 13) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "PREF64 lifetime is too long, ignoring assignment: %s", rvalue); + return 0; + } + + p->lifetime = usec; + + TAKE_PTR(p); + return 0; +} + +int config_parse_radv_dns( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *n = data; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + n->n_router_dns = 0; + n->router_dns = mfree(n->router_dns); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *w = NULL; + union in_addr_union a; + + r = extract_first_word(&p, &w, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract word, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + if (streq(w, "_link_local")) + a = IN_ADDR_NULL; + else { + r = in_addr_from_string(AF_INET6, w, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DNS server address, ignoring: %s", w); + continue; + } + + if (in_addr_is_null(AF_INET6, &a)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "DNS server address is null, ignoring: %s", w); + continue; + } + } + + struct in6_addr *m; + m = reallocarray(n->router_dns, n->n_router_dns + 1, sizeof(struct in6_addr)); + if (!m) + return log_oom(); + + m[n->n_router_dns++] = a.in6; + n->router_dns = m; + } +} + +int config_parse_radv_search_domains( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *n = data; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + n->router_search_domains = ordered_set_free(n->router_search_domains); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *w = NULL, *idna = NULL; + + r = extract_first_word(&p, &w, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract word, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + r = dns_name_apply_idna(w, &idna); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to apply IDNA to domain name '%s', ignoring: %m", w); + continue; + } else if (r == 0) + /* transfer ownership to simplify subsequent operations */ + idna = TAKE_PTR(w); + + r = ordered_set_ensure_allocated(&n->router_search_domains, &string_hash_ops_free); + if (r < 0) + return log_oom(); + + r = ordered_set_consume(n->router_search_domains, TAKE_PTR(idna)); + if (r < 0) + return log_oom(); + } +} + +static const char * const radv_prefix_delegation_table[_RADV_PREFIX_DELEGATION_MAX] = { + [RADV_PREFIX_DELEGATION_NONE] = "no", + [RADV_PREFIX_DELEGATION_STATIC] = "static", + [RADV_PREFIX_DELEGATION_DHCP6] = "dhcpv6", + [RADV_PREFIX_DELEGATION_BOTH] = "yes", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN( + radv_prefix_delegation, + RADVPrefixDelegation, + RADV_PREFIX_DELEGATION_BOTH); + +int config_parse_router_prefix_delegation( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + RADVPrefixDelegation val, *ra = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (streq(lvalue, "IPv6SendRA")) { + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid %s= setting, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + /* When IPv6SendRA= is enabled, only static prefixes are sent by default, and users + * need to explicitly enable DHCPv6PrefixDelegation=. */ + *ra = r ? RADV_PREFIX_DELEGATION_STATIC : RADV_PREFIX_DELEGATION_NONE; + return 0; + } + + /* For backward compatibility */ + val = radv_prefix_delegation_from_string(rvalue); + if (val < 0) { + log_syntax(unit, LOG_WARNING, filename, line, val, + "Invalid %s= setting, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + *ra = val; + return 0; +} + +int config_parse_router_lifetime( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + usec_t usec, *lifetime = ASSERT_PTR(data); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *lifetime = RADV_DEFAULT_ROUTER_LIFETIME_USEC; + return 0; + } + + r = parse_sec(rvalue, &usec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse router lifetime, ignoring assignment: %s", rvalue); + return 0; + } + if (usec > 0) { + if (usec < RADV_MIN_ROUTER_LIFETIME_USEC) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Router lifetime %s is too short, using %s.", + FORMAT_TIMESPAN(usec, USEC_PER_SEC), + FORMAT_TIMESPAN(RADV_MIN_ROUTER_LIFETIME_USEC, USEC_PER_SEC)); + usec = RADV_MIN_ROUTER_LIFETIME_USEC; + } else if (usec > RADV_MAX_ROUTER_LIFETIME_USEC) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Router lifetime %s is too large, using %s.", + FORMAT_TIMESPAN(usec, USEC_PER_SEC), + FORMAT_TIMESPAN(RADV_MAX_ROUTER_LIFETIME_USEC, USEC_PER_SEC)); + usec = RADV_MAX_ROUTER_LIFETIME_USEC; + } + } + + *lifetime = usec; + return 0; +} + +int config_parse_router_retransmit( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + usec_t usec, *router_retransmit_usec = ASSERT_PTR(data); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *router_retransmit_usec = 0; + return 0; + } + + r = parse_sec(rvalue, &usec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + if (usec != USEC_INFINITY && + usec > RADV_MAX_RETRANSMIT_USEC) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid [%s] %s=, ignoring assignment: %s", section, lvalue, rvalue); + return 0; + } + + *router_retransmit_usec = usec; + return 0; +} + +int config_parse_router_preference( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + if (streq(rvalue, "high")) + network->router_preference = SD_NDISC_PREFERENCE_HIGH; + else if (STR_IN_SET(rvalue, "medium", "normal", "default")) + network->router_preference = SD_NDISC_PREFERENCE_MEDIUM; + else if (streq(rvalue, "low")) + network->router_preference = SD_NDISC_PREFERENCE_LOW; + else + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid router preference, ignoring assignment: %s", rvalue); + + return 0; +} + +int config_parse_router_home_agent_lifetime( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + usec_t usec, *home_agent_lifetime_usec = ASSERT_PTR(data); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *home_agent_lifetime_usec = 0; + return 0; + } + + r = parse_sec(rvalue, &usec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue); + return 0; + } + + if (!timestamp_is_set(usec) || + usec > RADV_HOME_AGENT_MAX_LIFETIME_USEC) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid [%s] %s=, ignoring assignment: %s", section, lvalue, rvalue); + return 0; + } + + *home_agent_lifetime_usec = usec; + return 0; +} diff --git a/src/network/networkd-radv.h b/src/network/networkd-radv.h new file mode 100644 index 0000000..48677b5 --- /dev/null +++ b/src/network/networkd-radv.h @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2017 Intel Corporation. All rights reserved. +***/ + +#include <inttypes.h> +#include <stdbool.h> + +#include "sd-radv.h" + +#include "in-addr-util.h" +#include "conf-parser.h" +#include "networkd-util.h" + +typedef struct Link Link; +typedef struct Network Network; + +typedef enum RADVPrefixDelegation { + RADV_PREFIX_DELEGATION_NONE = 0, + RADV_PREFIX_DELEGATION_STATIC = 1 << 0, + RADV_PREFIX_DELEGATION_DHCP6 = 1 << 1, + RADV_PREFIX_DELEGATION_BOTH = RADV_PREFIX_DELEGATION_STATIC | RADV_PREFIX_DELEGATION_DHCP6, + _RADV_PREFIX_DELEGATION_MAX, + _RADV_PREFIX_DELEGATION_INVALID = -EINVAL, +} RADVPrefixDelegation; + +typedef struct Prefix { + Network *network; + ConfigSection *section; + + struct in6_addr prefix; + uint8_t prefixlen; + usec_t preferred_lifetime; + usec_t valid_lifetime; + + bool onlink; + bool address_auto_configuration; + + bool assign; + uint32_t route_metric; + Set *tokens; +} Prefix; + +typedef struct RoutePrefix { + Network *network; + ConfigSection *section; + + struct in6_addr prefix; + uint8_t prefixlen; + usec_t lifetime; +} RoutePrefix; + +typedef struct pref64Prefix { + Network *network; + ConfigSection *section; + + struct in6_addr prefix; + uint8_t prefixlen; + usec_t lifetime; +} pref64Prefix; + +Prefix *prefix_free(Prefix *prefix); +RoutePrefix *route_prefix_free(RoutePrefix *prefix); +pref64Prefix *pref64_prefix_free(pref64Prefix *prefix); + +void network_drop_invalid_prefixes(Network *network); +void network_drop_invalid_route_prefixes(Network *network); +void network_drop_invalid_pref64_prefixes(Network *network); +void network_adjust_radv(Network *network); + +int link_request_radv_addresses(Link *link); + +bool link_radv_enabled(Link *link); +int radv_start(Link *link); +int radv_update_mac(Link *link); +int radv_add_prefix(Link *link, const struct in6_addr *prefix, uint8_t prefix_len, + usec_t lifetime_preferred_usec, usec_t lifetime_valid_usec); + +int link_request_radv(Link *link); + +const char* radv_prefix_delegation_to_string(RADVPrefixDelegation i) _const_; +RADVPrefixDelegation radv_prefix_delegation_from_string(const char *s) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_router_prefix_delegation); +CONFIG_PARSER_PROTOTYPE(config_parse_router_lifetime); +CONFIG_PARSER_PROTOTYPE(config_parse_router_retransmit); +CONFIG_PARSER_PROTOTYPE(config_parse_router_preference); +CONFIG_PARSER_PROTOTYPE(config_parse_prefix); +CONFIG_PARSER_PROTOTYPE(config_parse_prefix_boolean); +CONFIG_PARSER_PROTOTYPE(config_parse_prefix_lifetime); +CONFIG_PARSER_PROTOTYPE(config_parse_prefix_metric); +CONFIG_PARSER_PROTOTYPE(config_parse_prefix_token); +CONFIG_PARSER_PROTOTYPE(config_parse_radv_dns); +CONFIG_PARSER_PROTOTYPE(config_parse_radv_search_domains); +CONFIG_PARSER_PROTOTYPE(config_parse_route_prefix); +CONFIG_PARSER_PROTOTYPE(config_parse_route_prefix_lifetime); +CONFIG_PARSER_PROTOTYPE(config_parse_pref64_prefix); +CONFIG_PARSER_PROTOTYPE(config_parse_pref64_prefix_lifetime); +CONFIG_PARSER_PROTOTYPE(config_parse_router_home_agent_lifetime); diff --git a/src/network/networkd-route-util.c b/src/network/networkd-route-util.c new file mode 100644 index 0000000..d49a0b9 --- /dev/null +++ b/src/network/networkd-route-util.c @@ -0,0 +1,586 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/rtnetlink.h> + +#include "alloc-util.h" +#include "logarithm.h" +#include "missing_threads.h" +#include "networkd-address.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-route-util.h" +#include "networkd-route.h" +#include "parse-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "sysctl-util.h" + +#define ROUTES_DEFAULT_MAX_PER_FAMILY 4096U + +unsigned routes_max(void) { + static thread_local unsigned cached = 0; + _cleanup_free_ char *s4 = NULL, *s6 = NULL; + unsigned val4 = ROUTES_DEFAULT_MAX_PER_FAMILY, val6 = ROUTES_DEFAULT_MAX_PER_FAMILY; + + if (cached > 0) + return cached; + + if (sysctl_read_ip_property(AF_INET, NULL, "route/max_size", &s4) >= 0) + if (safe_atou(s4, &val4) >= 0 && val4 == 2147483647U) + /* This is the default "no limit" value in the kernel */ + val4 = ROUTES_DEFAULT_MAX_PER_FAMILY; + + if (sysctl_read_ip_property(AF_INET6, NULL, "route/max_size", &s6) >= 0) + (void) safe_atou(s6, &val6); + + cached = MAX(ROUTES_DEFAULT_MAX_PER_FAMILY, val4) + + MAX(ROUTES_DEFAULT_MAX_PER_FAMILY, val6); + return cached; +} + +static bool route_lifetime_is_valid(const Route *route) { + assert(route); + + return + route->lifetime_usec == USEC_INFINITY || + route->lifetime_usec > now(CLOCK_BOOTTIME); +} + +bool link_find_default_gateway(Link *link, int family, Route **gw) { + bool found = false; + Route *route; + + assert(link); + + SET_FOREACH(route, link->routes) { + if (!route_exists(route)) + continue; + if (family != AF_UNSPEC && route->family != family) + continue; + if (route->dst_prefixlen != 0) + continue; + if (route->src_prefixlen != 0) + continue; + if (route->table != RT_TABLE_MAIN) + continue; + if (route->type != RTN_UNICAST) + continue; + if (route->scope != RT_SCOPE_UNIVERSE) + continue; + if (!in_addr_is_set(route->gw_family, &route->gw)) + continue; + + /* Found a default gateway. */ + if (!gw) + return true; + + /* If we have already found another gw, then let's compare their weight and priority. */ + if (*gw) { + if (route->gw_weight > (*gw)->gw_weight) + continue; + if (route->priority >= (*gw)->priority) + continue; + } + + *gw = route; + found = true; + } + + return found; +} + +int manager_find_uplink(Manager *m, int family, Link *exclude, Link **ret) { + Route *gw = NULL; + Link *link; + + assert(m); + assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6)); + + /* Looks for a suitable "uplink", via black magic: an interface that is up and where the + * default route with the highest priority points to. */ + + HASHMAP_FOREACH(link, m->links_by_index) { + if (link == exclude) + continue; + + if (link->state != LINK_STATE_CONFIGURED) + continue; + + link_find_default_gateway(link, family, &gw); + } + + if (!gw) + return -ENOENT; + + if (ret) { + assert(gw->link); + *ret = gw->link; + } + + return 0; +} + +bool gateway_is_ready(Link *link, bool onlink, int family, const union in_addr_union *gw) { + Route *route; + Address *a; + + assert(link); + assert(link->manager); + + if (onlink) + return true; + + if (!gw || !in_addr_is_set(family, gw)) + return true; + + if (family == AF_INET6 && in6_addr_is_link_local(&gw->in6)) + return true; + + SET_FOREACH(route, link->routes) { + if (!route_exists(route)) + continue; + if (!route_lifetime_is_valid(route)) + continue; + if (route->family != family) + continue; + if (!in_addr_is_set(route->family, &route->dst) && route->dst_prefixlen == 0) + continue; + if (in_addr_prefix_covers(family, &route->dst, route->dst_prefixlen, gw) > 0) + return true; + } + + if (link->manager->manage_foreign_routes) + return false; + + /* If we do not manage foreign routes, then there may exist a prefix route we do not know, + * which was created on configuring an address. Hence, also check the addresses. */ + SET_FOREACH(a, link->addresses) { + if (!address_is_ready(a)) + continue; + if (a->family != family) + continue; + if (FLAGS_SET(a->flags, IFA_F_NOPREFIXROUTE)) + continue; + if (in_addr_prefix_covers(a->family, + in_addr_is_set(a->family, &a->in_addr_peer) ? &a->in_addr_peer : &a->in_addr, + a->prefixlen, gw) > 0) + return true; + } + + return false; +} + +static int link_address_is_reachable_internal( + Link *link, + int family, + const union in_addr_union *address, + const union in_addr_union *prefsrc, /* optional */ + Route **ret) { + + Route *route, *found = NULL; + + assert(link); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(address); + + SET_FOREACH(route, link->routes) { + if (!route_exists(route)) + continue; + + if (!route_lifetime_is_valid(route)) + continue; + + if (route->type != RTN_UNICAST) + continue; + + if (route->family != family) + continue; + + if (in_addr_prefix_covers(family, &route->dst, route->dst_prefixlen, address) <= 0) + continue; + + if (prefsrc && + in_addr_is_set(family, prefsrc) && + in_addr_is_set(family, &route->prefsrc) && + !in_addr_equal(family, prefsrc, &route->prefsrc)) + continue; + + if (found && found->priority <= route->priority) + continue; + + found = route; + } + + if (!found) + return -ENOENT; + + if (ret) + *ret = found; + + return 0; +} + +int link_address_is_reachable( + Link *link, + int family, + const union in_addr_union *address, + const union in_addr_union *prefsrc, /* optional */ + Address **ret) { + + Route *route; + Address *a; + int r; + + assert(link); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(address); + + /* This checks if the address is reachable, and optionally return the Address object of the + * preferred source to access the address. */ + + r = link_address_is_reachable_internal(link, family, address, prefsrc, &route); + if (r < 0) + return r; + + if (!in_addr_is_set(route->family, &route->prefsrc)) { + if (ret) + *ret = NULL; + return 0; + } + + r = link_get_address(link, route->family, &route->prefsrc, 0, &a); + if (r < 0) + return r; + + if (!address_is_ready(a)) + return -EBUSY; + + if (ret) + *ret = a; + + return 0; +} + +int manager_address_is_reachable( + Manager *manager, + int family, + const union in_addr_union *address, + const union in_addr_union *prefsrc, /* optional */ + Address **ret) { + + Route *route, *found = NULL; + Address *a; + Link *link; + int r; + + assert(manager); + + HASHMAP_FOREACH(link, manager->links_by_index) { + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + continue; + + if (link_address_is_reachable_internal(link, family, address, prefsrc, &route) < 0) + continue; + + if (found && found->priority <= route->priority) + continue; + + found = route; + } + + if (!found) + return -ENOENT; + + if (!in_addr_is_set(found->family, &found->prefsrc)) { + if (ret) + *ret = NULL; + return 0; + } + + r = link_get_address(found->link, found->family, &found->prefsrc, 0, &a); + if (r < 0) + return r; + + if (!address_is_ready(a)) + return -EBUSY; + + if (ret) + *ret = a; + + return 0; +} + +static const char * const route_type_table[__RTN_MAX] = { + [RTN_UNICAST] = "unicast", + [RTN_LOCAL] = "local", + [RTN_BROADCAST] = "broadcast", + [RTN_ANYCAST] = "anycast", + [RTN_MULTICAST] = "multicast", + [RTN_BLACKHOLE] = "blackhole", + [RTN_UNREACHABLE] = "unreachable", + [RTN_PROHIBIT] = "prohibit", + [RTN_THROW] = "throw", + [RTN_NAT] = "nat", + [RTN_XRESOLVE] = "xresolve", +}; + +assert_cc(__RTN_MAX <= UCHAR_MAX); +DEFINE_STRING_TABLE_LOOKUP(route_type, int); + +static const char * const route_scope_table[] = { + [RT_SCOPE_UNIVERSE] = "global", + [RT_SCOPE_SITE] = "site", + [RT_SCOPE_LINK] = "link", + [RT_SCOPE_HOST] = "host", + [RT_SCOPE_NOWHERE] = "nowhere", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(route_scope, int, UINT8_MAX); + +static const char * const route_protocol_table[] = { + [RTPROT_KERNEL] = "kernel", + [RTPROT_BOOT] = "boot", + [RTPROT_STATIC] = "static", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(route_protocol, int, UINT8_MAX); + +static const char * const route_protocol_full_table[] = { + [RTPROT_REDIRECT] = "redirect", + [RTPROT_KERNEL] = "kernel", + [RTPROT_BOOT] = "boot", + [RTPROT_STATIC] = "static", + [RTPROT_GATED] = "gated", + [RTPROT_RA] = "ra", + [RTPROT_MRT] = "mrt", + [RTPROT_ZEBRA] = "zebra", + [RTPROT_BIRD] = "bird", + [RTPROT_DNROUTED] = "dnrouted", + [RTPROT_XORP] = "xorp", + [RTPROT_NTK] = "ntk", + [RTPROT_DHCP] = "dhcp", + [RTPROT_MROUTED] = "mrouted", + [RTPROT_BABEL] = "babel", + [RTPROT_BGP] = "bgp", + [RTPROT_ISIS] = "isis", + [RTPROT_OSPF] = "ospf", + [RTPROT_RIP] = "rip", + [RTPROT_EIGRP] = "eigrp", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(route_protocol_full, int, UINT8_MAX); + +int route_flags_to_string_alloc(uint32_t flags, char **ret) { + _cleanup_free_ char *str = NULL; + static const char* map[] = { + [LOG2U(RTNH_F_DEAD)] = "dead", /* Nexthop is dead (used by multipath) */ + [LOG2U(RTNH_F_PERVASIVE)] = "pervasive", /* Do recursive gateway lookup */ + [LOG2U(RTNH_F_ONLINK)] = "onlink" , /* Gateway is forced on link */ + [LOG2U(RTNH_F_OFFLOAD)] = "offload", /* Nexthop is offloaded */ + [LOG2U(RTNH_F_LINKDOWN)] = "linkdown", /* carrier-down on nexthop */ + [LOG2U(RTNH_F_UNRESOLVED)] = "unresolved", /* The entry is unresolved (ipmr) */ + [LOG2U(RTNH_F_TRAP)] = "trap", /* Nexthop is trapping packets */ + }; + + assert(ret); + + for (size_t i = 0; i < ELEMENTSOF(map); i++) + if (FLAGS_SET(flags, 1 << i) && map[i]) + if (!strextend_with_separator(&str, ",", map[i])) + return -ENOMEM; + + *ret = TAKE_PTR(str); + return 0; +} + +static const char * const route_table_table[] = { + [RT_TABLE_DEFAULT] = "default", + [RT_TABLE_MAIN] = "main", + [RT_TABLE_LOCAL] = "local", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP(route_table, int); + +int manager_get_route_table_from_string(const Manager *m, const char *s, uint32_t *ret) { + uint32_t t; + int r; + + assert(m); + assert(s); + assert(ret); + + r = route_table_from_string(s); + if (r >= 0) { + *ret = (uint32_t) r; + return 0; + } + + t = PTR_TO_UINT32(hashmap_get(m->route_table_numbers_by_name, s)); + if (t != 0) { + *ret = t; + return 0; + } + + r = safe_atou32(s, &t); + if (r < 0) + return r; + + if (t == 0) + return -ERANGE; + + *ret = t; + return 0; +} + +int manager_get_route_table_to_string(const Manager *m, uint32_t table, bool append_num, char **ret) { + _cleanup_free_ char *str = NULL; + const char *s; + + assert(m); + assert(ret); + + /* Unlike manager_get_route_table_from_string(), this accepts 0, as the kernel may create routes with + * table 0. See issue #25089. */ + + s = route_table_to_string(table); + if (!s) + s = hashmap_get(m->route_table_names_by_number, UINT32_TO_PTR(table)); + + if (s && !append_num) { + str = strdup(s); + if (!str) + return -ENOMEM; + + } else if (asprintf(&str, "%s%s%" PRIu32 "%s", + strempty(s), + s ? "(" : "", + table, + s ? ")" : "") < 0) + return -ENOMEM; + + *ret = TAKE_PTR(str); + return 0; +} + +int config_parse_route_table_names( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + m->route_table_names_by_number = hashmap_free(m->route_table_names_by_number); + m->route_table_numbers_by_name = hashmap_free(m->route_table_numbers_by_name); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *name = NULL; + uint32_t table; + char *num; + + r = extract_first_word(&p, &name, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid RouteTable=, ignoring assignment: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + num = strchr(name, ':'); + if (!num) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid route table name and number pair, ignoring assignment: %s", name); + continue; + } + + *num++ = '\0'; + + if (isempty(name)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Route table name cannot be empty. Ignoring assignment: %s:%s", name, num); + continue; + } + if (in_charset(name, DIGITS)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Route table name cannot be numeric. Ignoring assignment: %s:%s", name, num); + continue; + } + if (route_table_from_string(name) >= 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Route table name %s is predefined for %i. Ignoring assignment: %s:%s", + name, route_table_from_string(name), name, num); + continue; + } + + r = safe_atou32(num, &table); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse route table number '%s', ignoring assignment: %s:%s", num, name, num); + continue; + } + if (table == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid route table number, ignoring assignment: %s:%s", name, num); + continue; + } + if (route_table_to_string(table)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Route table name for %s is predefined (%s). Ignoring assignment: %s:%s", + num, route_table_to_string(table), name, num); + continue; + } + + r = hashmap_ensure_put(&m->route_table_numbers_by_name, &string_hash_ops_free, name, UINT32_TO_PTR(table)); + if (r == -ENOMEM) + return log_oom(); + if (r == -EEXIST) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Specified route table name and number pair conflicts with others, ignoring assignment: %s:%s", name, num); + continue; + } + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store route table name and number pair, ignoring assignment: %s:%s", name, num); + continue; + } + if (r == 0) + /* The entry is duplicated. It should not be added to route_table_names_by_number hashmap. */ + continue; + + r = hashmap_ensure_put(&m->route_table_names_by_number, NULL, UINT32_TO_PTR(table), name); + if (r < 0) { + hashmap_remove(m->route_table_numbers_by_name, name); + + if (r == -ENOMEM) + return log_oom(); + if (r == -EEXIST) + log_syntax(unit, LOG_WARNING, filename, line, r, + "Specified route table name and number pair conflicts with others, ignoring assignment: %s:%s", name, num); + else + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store route table name and number pair, ignoring assignment: %s:%s", name, num); + continue; + } + assert(r > 0); + + TAKE_PTR(name); + } +} diff --git a/src/network/networkd-route-util.h b/src/network/networkd-route-util.h new file mode 100644 index 0000000..f326888 --- /dev/null +++ b/src/network/networkd-route-util.h @@ -0,0 +1,55 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> + +#include "conf-parser.h" + +typedef struct Link Link; +typedef struct Manager Manager; +typedef struct Address Address; +typedef struct Route Route; + +unsigned routes_max(void); + +bool link_find_default_gateway(Link *link, int family, Route **gw); +static inline bool link_has_default_gateway(Link *link, int family) { + return link_find_default_gateway(link, family, NULL); +} + +int manager_find_uplink(Manager *m, int family, Link *exclude, Link **ret); + +bool gateway_is_ready(Link *link, bool onlink, int family, const union in_addr_union *gw); + +int link_address_is_reachable( + Link *link, + int family, + const union in_addr_union *address, + const union in_addr_union *prefsrc, /* optional */ + Address **ret); + +int manager_address_is_reachable( + Manager *manager, + int family, + const union in_addr_union *address, + const union in_addr_union *prefsrc, /* optional */ + Address **ret); + +int route_type_from_string(const char *s) _pure_; +const char *route_type_to_string(int t) _const_; + +int route_scope_from_string(const char *s); +int route_scope_to_string_alloc(int t, char **ret); + +int route_protocol_from_string(const char *s); +int route_protocol_to_string_alloc(int t, char **ret); +int route_protocol_full_from_string(const char *s); +int route_protocol_full_to_string_alloc(int t, char **ret); + +int route_flags_to_string_alloc(uint32_t flags, char **ret); + +int manager_get_route_table_from_string(const Manager *m, const char *table, uint32_t *ret); +int manager_get_route_table_to_string(const Manager *m, uint32_t table, bool append_num, char **ret); + +CONFIG_PARSER_PROTOTYPE(config_parse_route_table_names); diff --git a/src/network/networkd-route.c b/src/network/networkd-route.c new file mode 100644 index 0000000..eb502ae --- /dev/null +++ b/src/network/networkd-route.c @@ -0,0 +1,3148 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/icmpv6.h> +#include <linux/ipv6_route.h> +#include <linux/nexthop.h> + +#include "alloc-util.h" +#include "event-util.h" +#include "netlink-util.h" +#include "networkd-address.h" +#include "networkd-ipv4ll.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-nexthop.h" +#include "networkd-queue.h" +#include "networkd-route-util.h" +#include "networkd-route.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" +#include "vrf.h" +#include "wireguard.h" + +int route_new(Route **ret) { + _cleanup_(route_freep) Route *route = NULL; + + route = new(Route, 1); + if (!route) + return -ENOMEM; + + *route = (Route) { + .family = AF_UNSPEC, + .scope = RT_SCOPE_UNIVERSE, + .protocol = RTPROT_UNSPEC, + .type = RTN_UNICAST, + .table = RT_TABLE_MAIN, + .lifetime_usec = USEC_INFINITY, + .quickack = -1, + .fast_open_no_cookie = -1, + .gateway_onlink = -1, + .ttl_propagate = -1, + }; + + *ret = TAKE_PTR(route); + + return 0; +} + +static int route_new_static(Network *network, const char *filename, unsigned section_line, Route **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(route_freep) Route *route = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + route = hashmap_get(network->routes_by_section, n); + if (route) { + *ret = TAKE_PTR(route); + return 0; + } + + if (hashmap_size(network->routes_by_section) >= routes_max()) + return -E2BIG; + + r = route_new(&route); + if (r < 0) + return r; + + route->protocol = RTPROT_STATIC; + route->network = network; + route->section = TAKE_PTR(n); + route->source = NETWORK_CONFIG_SOURCE_STATIC; + + r = hashmap_ensure_put(&network->routes_by_section, &config_section_hash_ops, route->section, route); + if (r < 0) + return r; + + *ret = TAKE_PTR(route); + return 0; +} + +Route *route_free(Route *route) { + if (!route) + return NULL; + + if (route->network) { + assert(route->section); + hashmap_remove(route->network->routes_by_section, route->section); + } + + config_section_free(route->section); + + if (route->link) + set_remove(route->link->routes, route); + + if (route->manager) + set_remove(route->manager->routes, route); + + ordered_set_free_with_destructor(route->multipath_routes, multipath_route_free); + + sd_event_source_disable_unref(route->expire); + + free(route->tcp_congestion_control_algo); + + return mfree(route); +} + +static void route_hash_func(const Route *route, struct siphash *state) { + assert(route); + + siphash24_compress(&route->family, sizeof(route->family), state); + + switch (route->family) { + case AF_INET: + case AF_INET6: + siphash24_compress(&route->dst_prefixlen, sizeof(route->dst_prefixlen), state); + siphash24_compress(&route->dst, FAMILY_ADDRESS_SIZE(route->family), state); + + siphash24_compress(&route->src_prefixlen, sizeof(route->src_prefixlen), state); + siphash24_compress(&route->src, FAMILY_ADDRESS_SIZE(route->family), state); + + siphash24_compress(&route->gw_family, sizeof(route->gw_family), state); + if (IN_SET(route->gw_family, AF_INET, AF_INET6)) { + siphash24_compress(&route->gw, FAMILY_ADDRESS_SIZE(route->gw_family), state); + siphash24_compress(&route->gw_weight, sizeof(route->gw_weight), state); + } + + siphash24_compress(&route->prefsrc, FAMILY_ADDRESS_SIZE(route->family), state); + + siphash24_compress(&route->tos, sizeof(route->tos), state); + siphash24_compress(&route->priority, sizeof(route->priority), state); + siphash24_compress(&route->table, sizeof(route->table), state); + siphash24_compress(&route->protocol, sizeof(route->protocol), state); + siphash24_compress(&route->scope, sizeof(route->scope), state); + siphash24_compress(&route->type, sizeof(route->type), state); + + siphash24_compress(&route->initcwnd, sizeof(route->initcwnd), state); + siphash24_compress(&route->initrwnd, sizeof(route->initrwnd), state); + + siphash24_compress(&route->advmss, sizeof(route->advmss), state); + siphash24_compress(&route->nexthop_id, sizeof(route->nexthop_id), state); + + break; + default: + /* treat any other address family as AF_UNSPEC */ + break; + } +} + +static int route_compare_func(const Route *a, const Route *b) { + int r; + + r = CMP(a->family, b->family); + if (r != 0) + return r; + + switch (a->family) { + case AF_INET: + case AF_INET6: + r = CMP(a->dst_prefixlen, b->dst_prefixlen); + if (r != 0) + return r; + + r = memcmp(&a->dst, &b->dst, FAMILY_ADDRESS_SIZE(a->family)); + if (r != 0) + return r; + + r = CMP(a->src_prefixlen, b->src_prefixlen); + if (r != 0) + return r; + + r = memcmp(&a->src, &b->src, FAMILY_ADDRESS_SIZE(a->family)); + if (r != 0) + return r; + + r = CMP(a->gw_family, b->gw_family); + if (r != 0) + return r; + + if (IN_SET(a->gw_family, AF_INET, AF_INET6)) { + r = memcmp(&a->gw, &b->gw, FAMILY_ADDRESS_SIZE(a->family)); + if (r != 0) + return r; + + r = CMP(a->gw_weight, b->gw_weight); + if (r != 0) + return r; + } + + r = memcmp(&a->prefsrc, &b->prefsrc, FAMILY_ADDRESS_SIZE(a->family)); + if (r != 0) + return r; + + r = CMP(a->tos, b->tos); + if (r != 0) + return r; + + r = CMP(a->priority, b->priority); + if (r != 0) + return r; + + r = CMP(a->table, b->table); + if (r != 0) + return r; + + r = CMP(a->protocol, b->protocol); + if (r != 0) + return r; + + r = CMP(a->scope, b->scope); + if (r != 0) + return r; + + r = CMP(a->type, b->type); + if (r != 0) + return r; + + r = CMP(a->initcwnd, b->initcwnd); + if (r != 0) + return r; + + r = CMP(a->initrwnd, b->initrwnd); + if (r != 0) + return r; + + r = CMP(a->advmss, b->advmss); + if (r != 0) + return r; + + r = CMP(a->nexthop_id, b->nexthop_id); + if (r != 0) + return r; + + return 0; + default: + /* treat any other address family as AF_UNSPEC */ + return 0; + } +} + +DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR( + route_hash_ops, + Route, + route_hash_func, + route_compare_func, + route_free); + +static bool route_type_is_reject(const Route *route) { + assert(route); + + return IN_SET(route->type, RTN_UNREACHABLE, RTN_PROHIBIT, RTN_BLACKHOLE, RTN_THROW); +} + +static bool route_needs_convert(const Route *route) { + assert(route); + + return route->nexthop_id > 0 || !ordered_set_isempty(route->multipath_routes); +} + +static int route_add(Manager *manager, Link *link, Route *route) { + int r; + + assert(route); + + if (route_type_is_reject(route)) { + assert(manager); + + r = set_ensure_put(&manager->routes, &route_hash_ops, route); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + route->manager = manager; + } else { + assert(link); + + r = set_ensure_put(&link->routes, &route_hash_ops, route); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + route->link = link; + } + + return 0; +} + +int route_get(Manager *manager, Link *link, const Route *in, Route **ret) { + Route *route; + + assert(in); + + if (route_type_is_reject(in)) { + if (!manager) + return -ENOENT; + + route = set_get(manager->routes, in); + } else { + if (!link) + return -ENOENT; + + route = set_get(link->routes, in); + } + if (!route) + return -ENOENT; + + if (ret) + *ret = route; + + return 0; +} + +int route_dup(const Route *src, Route **ret) { + _cleanup_(route_freep) Route *dest = NULL; + int r; + + /* This does not copy mulipath routes. */ + + assert(src); + assert(ret); + + dest = newdup(Route, src, 1); + if (!dest) + return -ENOMEM; + + /* Unset all pointers */ + dest->network = NULL; + dest->section = NULL; + dest->link = NULL; + dest->manager = NULL; + dest->multipath_routes = NULL; + dest->expire = NULL; + dest->tcp_congestion_control_algo = NULL; + + r = free_and_strdup(&dest->tcp_congestion_control_algo, src->tcp_congestion_control_algo); + if (r < 0) + return r; + + *ret = TAKE_PTR(dest); + return 0; +} + +static void route_apply_nexthop(Route *route, const NextHop *nh, uint8_t nh_weight) { + assert(route); + assert(nh); + assert(hashmap_isempty(nh->group)); + + route->gw_family = nh->family; + route->gw = nh->gw; + + if (nh_weight != UINT8_MAX) + route->gw_weight = nh_weight; + + if (nh->blackhole) + route->type = RTN_BLACKHOLE; +} + +static void route_apply_multipath_route(Route *route, const MultipathRoute *m) { + assert(route); + assert(m); + + route->gw_family = m->gateway.family; + route->gw = m->gateway.address; + route->gw_weight = m->weight; +} + +static int multipath_route_get_link(Manager *manager, const MultipathRoute *m, Link **ret) { + int r; + + assert(manager); + assert(m); + + if (m->ifname) { + r = link_get_by_name(manager, m->ifname, ret); + return r < 0 ? r : 1; + + } else if (m->ifindex > 0) { /* Always ignore ifindex if ifname is set. */ + r = link_get_by_index(manager, m->ifindex, ret); + return r < 0 ? r : 1; + } + + if (ret) + *ret = NULL; + return 0; +} + +typedef struct ConvertedRoutes { + size_t n; + Route **routes; + Link **links; +} ConvertedRoutes; + +static ConvertedRoutes *converted_routes_free(ConvertedRoutes *c) { + if (!c) + return NULL; + + for (size_t i = 0; i < c->n; i++) + route_free(c->routes[i]); + + free(c->routes); + free(c->links); + + return mfree(c); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(ConvertedRoutes*, converted_routes_free); + +static int converted_routes_new(size_t n, ConvertedRoutes **ret) { + _cleanup_(converted_routes_freep) ConvertedRoutes *c = NULL; + _cleanup_free_ Route **routes = NULL; + _cleanup_free_ Link **links = NULL; + + assert(n > 0); + assert(ret); + + routes = new0(Route*, n); + if (!routes) + return -ENOMEM; + + links = new0(Link*, n); + if (!links) + return -ENOMEM; + + c = new(ConvertedRoutes, 1); + if (!c) + return -ENOMEM; + + *c = (ConvertedRoutes) { + .n = n, + .routes = TAKE_PTR(routes), + .links = TAKE_PTR(links), + }; + + *ret = TAKE_PTR(c); + return 0; +} + +static int route_convert(Manager *manager, const Route *route, ConvertedRoutes **ret) { + _cleanup_(converted_routes_freep) ConvertedRoutes *c = NULL; + int r; + + assert(manager); + assert(route); + assert(ret); + + if (!route_needs_convert(route)) { + *ret = NULL; + return 0; + } + + if (route->nexthop_id > 0) { + struct nexthop_grp *nhg; + NextHop *nh; + + r = manager_get_nexthop_by_id(manager, route->nexthop_id, &nh); + if (r < 0) + return r; + + if (hashmap_isempty(nh->group)) { + r = converted_routes_new(1, &c); + if (r < 0) + return r; + + r = route_dup(route, &c->routes[0]); + if (r < 0) + return r; + + route_apply_nexthop(c->routes[0], nh, UINT8_MAX); + c->links[0] = nh->link; + + *ret = TAKE_PTR(c); + return 1; + } + + r = converted_routes_new(hashmap_size(nh->group), &c); + if (r < 0) + return r; + + size_t i = 0; + HASHMAP_FOREACH(nhg, nh->group) { + NextHop *h; + + r = manager_get_nexthop_by_id(manager, nhg->id, &h); + if (r < 0) + return r; + + r = route_dup(route, &c->routes[i]); + if (r < 0) + return r; + + route_apply_nexthop(c->routes[i], h, nhg->weight); + c->links[i] = h->link; + + i++; + } + + *ret = TAKE_PTR(c); + return 1; + + } + + assert(!ordered_set_isempty(route->multipath_routes)); + + r = converted_routes_new(ordered_set_size(route->multipath_routes), &c); + if (r < 0) + return r; + + size_t i = 0; + MultipathRoute *m; + ORDERED_SET_FOREACH(m, route->multipath_routes) { + r = route_dup(route, &c->routes[i]); + if (r < 0) + return r; + + route_apply_multipath_route(c->routes[i], m); + + r = multipath_route_get_link(manager, m, &c->links[i]); + if (r < 0) + return r; + + i++; + } + + *ret = TAKE_PTR(c); + return 1; +} + +void link_mark_routes(Link *link, NetworkConfigSource source) { + Route *route; + + assert(link); + + SET_FOREACH(route, link->routes) { + if (route->source != source) + continue; + + route_mark(route); + } +} + +static void log_route_debug(const Route *route, const char *str, const Link *link, const Manager *manager) { + _cleanup_free_ char *state = NULL, *gw_alloc = NULL, *prefsrc = NULL, + *table = NULL, *scope = NULL, *proto = NULL, *flags = NULL; + const char *gw = NULL, *dst, *src; + + assert(route); + assert(str); + assert(manager); + + /* link may be NULL. */ + + if (!DEBUG_LOGGING) + return; + + (void) network_config_state_to_string_alloc(route->state, &state); + + dst = in_addr_is_set(route->family, &route->dst) || route->dst_prefixlen > 0 ? + IN_ADDR_PREFIX_TO_STRING(route->family, &route->dst, route->dst_prefixlen) : NULL; + src = in_addr_is_set(route->family, &route->src) || route->src_prefixlen > 0 ? + IN_ADDR_PREFIX_TO_STRING(route->family, &route->src, route->src_prefixlen) : NULL; + + if (in_addr_is_set(route->gw_family, &route->gw)) { + (void) in_addr_to_string(route->gw_family, &route->gw, &gw_alloc); + gw = gw_alloc; + } else if (route->gateway_from_dhcp_or_ra) { + if (route->gw_family == AF_INET) + gw = "_dhcp4"; + else if (route->gw_family == AF_INET6) + gw = "_ipv6ra"; + } else { + MultipathRoute *m; + + ORDERED_SET_FOREACH(m, route->multipath_routes) { + _cleanup_free_ char *buf = NULL; + union in_addr_union a = m->gateway.address; + + (void) in_addr_to_string(m->gateway.family, &a, &buf); + (void) strextend_with_separator(&gw_alloc, ",", strna(buf)); + if (m->ifname) + (void) strextend(&gw_alloc, "@", m->ifname); + else if (m->ifindex > 0) + (void) strextendf(&gw_alloc, "@%i", m->ifindex); + /* See comments in config_parse_multipath_route(). */ + (void) strextendf(&gw_alloc, ":%"PRIu32, m->weight + 1); + } + gw = gw_alloc; + } + if (in_addr_is_set(route->family, &route->prefsrc)) + (void) in_addr_to_string(route->family, &route->prefsrc, &prefsrc); + (void) route_scope_to_string_alloc(route->scope, &scope); + (void) manager_get_route_table_to_string(manager, route->table, /* append_num = */ true, &table); + (void) route_protocol_full_to_string_alloc(route->protocol, &proto); + (void) route_flags_to_string_alloc(route->flags, &flags); + + log_link_debug(link, + "%s %s route (%s): dst: %s, src: %s, gw: %s, prefsrc: %s, scope: %s, table: %s, " + "proto: %s, type: %s, nexthop: %"PRIu32", priority: %"PRIu32", flags: %s", + str, strna(network_config_source_to_string(route->source)), strna(state), + strna(dst), strna(src), strna(gw), strna(prefsrc), + strna(scope), strna(table), strna(proto), + strna(route_type_to_string(route->type)), + route->nexthop_id, route->priority, strna(flags)); +} + +static int route_set_netlink_message(const Route *route, sd_netlink_message *req, Link *link) { + int r; + + assert(route); + assert(req); + + /* link may be NULL */ + + if (in_addr_is_set(route->gw_family, &route->gw) && route->nexthop_id == 0) { + if (route->gw_family == route->family) { + r = netlink_message_append_in_addr_union(req, RTA_GATEWAY, route->gw_family, &route->gw); + if (r < 0) + return r; + } else { + RouteVia rtvia = { + .family = route->gw_family, + .address = route->gw, + }; + + r = sd_netlink_message_append_data(req, RTA_VIA, &rtvia, sizeof(rtvia)); + if (r < 0) + return r; + } + } + + if (route->dst_prefixlen > 0) { + r = netlink_message_append_in_addr_union(req, RTA_DST, route->family, &route->dst); + if (r < 0) + return r; + + r = sd_rtnl_message_route_set_dst_prefixlen(req, route->dst_prefixlen); + if (r < 0) + return r; + } + + if (route->src_prefixlen > 0) { + r = netlink_message_append_in_addr_union(req, RTA_SRC, route->family, &route->src); + if (r < 0) + return r; + + r = sd_rtnl_message_route_set_src_prefixlen(req, route->src_prefixlen); + if (r < 0) + return r; + } + + if (in_addr_is_set(route->family, &route->prefsrc)) { + r = netlink_message_append_in_addr_union(req, RTA_PREFSRC, route->family, &route->prefsrc); + if (r < 0) + return r; + } + + r = sd_rtnl_message_route_set_scope(req, route->scope); + if (r < 0) + return r; + + r = sd_rtnl_message_route_set_flags(req, route->flags & RTNH_F_ONLINK); + if (r < 0) + return r; + + if (route->table < 256) { + r = sd_rtnl_message_route_set_table(req, route->table); + if (r < 0) + return r; + } else { + r = sd_rtnl_message_route_set_table(req, RT_TABLE_UNSPEC); + if (r < 0) + return r; + + /* Table attribute to allow more than 256. */ + r = sd_netlink_message_append_u32(req, RTA_TABLE, route->table); + if (r < 0) + return r; + } + + if (!route_type_is_reject(route) && + route->nexthop_id == 0 && + ordered_set_isempty(route->multipath_routes)) { + assert(link); /* Those routes must be attached to a specific link */ + + r = sd_netlink_message_append_u32(req, RTA_OIF, link->ifindex); + if (r < 0) + return r; + } + + if (route->nexthop_id > 0) { + r = sd_netlink_message_append_u32(req, RTA_NH_ID, route->nexthop_id); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u8(req, RTA_PREF, route->pref); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(req, RTA_PRIORITY, route->priority); + if (r < 0) + return r; + + return 0; +} + +static int route_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert(m); + + /* link may be NULL. */ + + if (link && IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -ESRCH) + log_link_message_warning_errno(link, m, r, "Could not drop route, ignoring"); + + return 1; +} + +int route_remove(Route *route) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + unsigned char type; + Manager *manager; + Link *link; + int r; + + assert(route); + assert(route->manager || (route->link && route->link->manager)); + assert(IN_SET(route->family, AF_INET, AF_INET6)); + + link = route->link; + manager = route->manager ?: link->manager; + + log_route_debug(route, "Removing", link, manager); + + r = sd_rtnl_message_new_route(manager->rtnl, &req, + RTM_DELROUTE, route->family, + route->protocol); + if (r < 0) + return log_link_error_errno(link, r, "Could not create netlink message: %m"); + + if (route->family == AF_INET && route->nexthop_id > 0 && route->type == RTN_BLACKHOLE) + /* When IPv4 route has nexthop id and the nexthop type is blackhole, even though kernel + * sends RTM_NEWROUTE netlink message with blackhole type, kernel's internal route type + * fib_rt_info::type may not be blackhole. Thus, we cannot know the internal value. + * Moreover, on route removal, the matching is done with the hidden value if we set + * non-zero type in RTM_DELROUTE message. Note, sd_rtnl_message_new_route() sets + * RTN_UNICAST by default. So, we need to clear the type here. */ + type = RTN_UNSPEC; + else + type = route->type; + + r = sd_rtnl_message_route_set_type(req, type); + if (r < 0) + return log_link_error_errno(link, r, "Could not set route type: %m"); + + r = route_set_netlink_message(route, req, link); + if (r < 0) + return log_error_errno(r, "Could not fill netlink message: %m"); + + r = netlink_call_async(manager->rtnl, NULL, req, route_remove_handler, + link ? link_netlink_destroy_callback : NULL, link); + if (r < 0) + return log_link_error_errno(link, r, "Could not send netlink message: %m"); + + link_ref(link); + + route_enter_removing(route); + return 0; +} + +int route_remove_and_drop(Route *route) { + if (!route) + return 0; + + route_cancel_request(route, NULL); + + if (route_exists(route)) + return route_remove(route); + + if (route->state == 0) + route_free(route); + + return 0; +} + +static void manager_mark_routes(Manager *manager, bool foreign, const Link *except) { + Route *route; + Link *link; + int r; + + assert(manager); + + /* First, mark all routes. */ + SET_FOREACH(route, manager->routes) { + /* Do not touch routes managed by the kernel. */ + if (route->protocol == RTPROT_KERNEL) + continue; + + /* When 'foreign' is true, mark only foreign routes, and vice versa. */ + if (foreign != (route->source == NETWORK_CONFIG_SOURCE_FOREIGN)) + continue; + + /* Do not touch dynamic routes. They will removed by dhcp_pd_prefix_lost() */ + if (IN_SET(route->source, NETWORK_CONFIG_SOURCE_DHCP4, NETWORK_CONFIG_SOURCE_DHCP6)) + continue; + + /* Ignore routes not assigned yet or already removed. */ + if (!route_exists(route)) + continue; + + route_mark(route); + } + + /* Then, unmark all routes requested by active links. */ + HASHMAP_FOREACH(link, manager->links_by_index) { + if (link == except) + continue; + + if (!link->network) + continue; + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + continue; + + HASHMAP_FOREACH(route, link->network->routes_by_section) { + _cleanup_(converted_routes_freep) ConvertedRoutes *converted = NULL; + Route *existing; + + r = route_convert(manager, route, &converted); + if (r < 0) + continue; + if (r == 0) { + if (route_get(manager, NULL, route, &existing) >= 0) + route_unmark(existing); + continue; + } + + for (size_t i = 0; i < converted->n; i++) + if (route_get(manager, NULL, converted->routes[i], &existing) >= 0) + route_unmark(existing); + } + } +} + +static int manager_drop_marked_routes(Manager *manager) { + Route *route; + int r = 0; + + assert(manager); + + SET_FOREACH(route, manager->routes) { + if (!route_is_marked(route)) + continue; + + RET_GATHER(r, route_remove(route)); + } + + return r; +} + +static bool route_by_kernel(const Route *route) { + assert(route); + + if (route->protocol == RTPROT_KERNEL) + return true; + + /* The kernels older than a826b04303a40d52439aa141035fca5654ccaccd (v5.11) create the IPv6 + * multicast with RTPROT_BOOT. Do not touch it. */ + if (route->protocol == RTPROT_BOOT && + route->family == AF_INET6 && + route->dst_prefixlen == 8 && + in6_addr_equal(&route->dst.in6, & (struct in6_addr) {{{ 0xff,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 }}})) + return true; + + return false; +} + +static void link_unmark_wireguard_routes(Link *link) { + assert(link); + + if (!link->netdev || link->netdev->kind != NETDEV_KIND_WIREGUARD) + return; + + Route *route, *existing; + Wireguard *w = WIREGUARD(link->netdev); + + SET_FOREACH(route, w->routes) + if (route_get(NULL, link, route, &existing) >= 0) + route_unmark(existing); +} + +int link_drop_foreign_routes(Link *link) { + Route *route; + int r; + + assert(link); + assert(link->manager); + assert(link->network); + + SET_FOREACH(route, link->routes) { + /* do not touch routes managed by the kernel */ + if (route_by_kernel(route)) + continue; + + /* Do not remove routes we configured. */ + if (route->source != NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + /* Ignore routes not assigned yet or already removed. */ + if (!route_exists(route)) + continue; + + if (route->protocol == RTPROT_STATIC && + FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_STATIC)) + continue; + + if (route->protocol == RTPROT_DHCP && + FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) + continue; + + route_mark(route); + } + + HASHMAP_FOREACH(route, link->network->routes_by_section) { + _cleanup_(converted_routes_freep) ConvertedRoutes *converted = NULL; + Route *existing; + + r = route_convert(link->manager, route, &converted); + if (r < 0) + continue; + if (r == 0) { + if (route_get(NULL, link, route, &existing) >= 0) + route_unmark(existing); + continue; + } + + for (size_t i = 0; i < converted->n; i++) + if (route_get(NULL, link, converted->routes[i], &existing) >= 0) + route_unmark(existing); + } + + link_unmark_wireguard_routes(link); + + r = 0; + SET_FOREACH(route, link->routes) { + if (!route_is_marked(route)) + continue; + + RET_GATHER(r, route_remove(route)); + } + + manager_mark_routes(link->manager, /* foreign = */ true, NULL); + + return RET_GATHER(r, manager_drop_marked_routes(link->manager)); +} + +int link_drop_managed_routes(Link *link) { + Route *route; + int r = 0; + + assert(link); + + SET_FOREACH(route, link->routes) { + /* do not touch routes managed by the kernel */ + if (route_by_kernel(route)) + continue; + + /* Do not touch routes managed by kernel or other tools. */ + if (route->source == NETWORK_CONFIG_SOURCE_FOREIGN) + continue; + + if (!route_exists(route)) + continue; + + RET_GATHER(r, route_remove(route)); + } + + manager_mark_routes(link->manager, /* foreign = */ false, link); + + return RET_GATHER(r, manager_drop_marked_routes(link->manager)); +} + +void link_foreignize_routes(Link *link) { + Route *route; + + assert(link); + + SET_FOREACH(route, link->routes) + route->source = NETWORK_CONFIG_SOURCE_FOREIGN; + + manager_mark_routes(link->manager, /* foreign = */ false, link); + + SET_FOREACH(route, link->manager->routes) { + if (!route_is_marked(route)) + continue; + + route->source = NETWORK_CONFIG_SOURCE_FOREIGN; + } +} + +static int route_expire_handler(sd_event_source *s, uint64_t usec, void *userdata) { + Route *route = ASSERT_PTR(userdata); + Link *link; + int r; + + assert(route->manager || (route->link && route->link->manager)); + + link = route->link; /* This may be NULL. */ + + r = route_remove(route); + if (r < 0) { + log_link_warning_errno(link, r, "Could not remove route: %m"); + if (link) + link_enter_failed(link); + } + + return 1; +} + +static int route_setup_timer(Route *route, const struct rta_cacheinfo *cacheinfo) { + Manager *manager; + int r; + + assert(route); + assert(route->manager || (route->link && route->link->manager)); + + manager = route->manager ?: route->link->manager; + + if (route->lifetime_usec == USEC_INFINITY) + return 0; + + if (cacheinfo && cacheinfo->rta_expires != 0) + /* Assume that non-zero rta_expires means kernel will handle the route expiration. */ + return 0; + + r = event_reset_time(manager->event, &route->expire, CLOCK_BOOTTIME, + route->lifetime_usec, 0, route_expire_handler, route, 0, "route-expiration", true); + if (r < 0) + return r; + + return 1; +} + +static int append_nexthop_one(const Link *link, const Route *route, const MultipathRoute *m, struct rtattr **rta, size_t offset) { + struct rtnexthop *rtnh; + struct rtattr *new_rta; + int r; + + assert(route); + assert(m); + assert(rta); + assert(*rta); + + new_rta = realloc(*rta, RTA_ALIGN((*rta)->rta_len) + RTA_SPACE(sizeof(struct rtnexthop))); + if (!new_rta) + return -ENOMEM; + *rta = new_rta; + + rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset); + *rtnh = (struct rtnexthop) { + .rtnh_len = sizeof(*rtnh), + .rtnh_ifindex = m->ifindex > 0 ? m->ifindex : link->ifindex, + .rtnh_hops = m->weight, + }; + + (*rta)->rta_len += sizeof(struct rtnexthop); + + if (route->family == m->gateway.family) { + r = rtattr_append_attribute(rta, RTA_GATEWAY, &m->gateway.address, FAMILY_ADDRESS_SIZE(m->gateway.family)); + if (r < 0) + goto clear; + rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset); + rtnh->rtnh_len += RTA_SPACE(FAMILY_ADDRESS_SIZE(m->gateway.family)); + } else { + r = rtattr_append_attribute(rta, RTA_VIA, &m->gateway, FAMILY_ADDRESS_SIZE(m->gateway.family) + sizeof(m->gateway.family)); + if (r < 0) + goto clear; + rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset); + rtnh->rtnh_len += RTA_SPACE(FAMILY_ADDRESS_SIZE(m->gateway.family) + sizeof(m->gateway.family)); + } + + return 0; + +clear: + (*rta)->rta_len -= sizeof(struct rtnexthop); + return r; +} + +static int append_nexthops(const Link *link, const Route *route, sd_netlink_message *req) { + _cleanup_free_ struct rtattr *rta = NULL; + struct rtnexthop *rtnh; + MultipathRoute *m; + size_t offset; + int r; + + assert(link); + assert(route); + assert(req); + + if (ordered_set_isempty(route->multipath_routes)) + return 0; + + rta = new(struct rtattr, 1); + if (!rta) + return -ENOMEM; + + *rta = (struct rtattr) { + .rta_type = RTA_MULTIPATH, + .rta_len = RTA_LENGTH(0), + }; + offset = (uint8_t *) RTA_DATA(rta) - (uint8_t *) rta; + + ORDERED_SET_FOREACH(m, route->multipath_routes) { + r = append_nexthop_one(link, route, m, &rta, offset); + if (r < 0) + return r; + + rtnh = (struct rtnexthop *)((uint8_t *) rta + offset); + offset = (uint8_t *) RTNH_NEXT(rtnh) - (uint8_t *) rta; + } + + r = sd_netlink_message_append_data(req, RTA_MULTIPATH, RTA_DATA(rta), RTA_PAYLOAD(rta)); + if (r < 0) + return r; + + return 0; +} + +int route_configure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg) { + int r; + + assert(m); + assert(link); + assert(error_msg); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not set route"); + link_enter_failed(link); + return 0; + } + + return 1; +} + +static int route_configure(const Route *route, uint32_t lifetime_sec, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(route); + assert(IN_SET(route->family, AF_INET, AF_INET6)); + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + assert(link->ifindex > 0); + assert(req); + + log_route_debug(route, "Configuring", link, link->manager); + + r = sd_rtnl_message_new_route(link->manager->rtnl, &m, RTM_NEWROUTE, route->family, route->protocol); + if (r < 0) + return r; + + r = sd_rtnl_message_route_set_type(m, route->type); + if (r < 0) + return r; + + r = route_set_netlink_message(route, m, link); + if (r < 0) + return r; + + if (lifetime_sec != UINT32_MAX) { + r = sd_netlink_message_append_u32(m, RTA_EXPIRES, lifetime_sec); + if (r < 0) + return r; + } + + if (route->ttl_propagate >= 0) { + r = sd_netlink_message_append_u8(m, RTA_TTL_PROPAGATE, route->ttl_propagate); + if (r < 0) + return r; + } + + r = sd_netlink_message_open_container(m, RTA_METRICS); + if (r < 0) + return r; + + if (route->mtu > 0) { + r = sd_netlink_message_append_u32(m, RTAX_MTU, route->mtu); + if (r < 0) + return r; + } + + if (route->initcwnd > 0) { + r = sd_netlink_message_append_u32(m, RTAX_INITCWND, route->initcwnd); + if (r < 0) + return r; + } + + if (route->initrwnd > 0) { + r = sd_netlink_message_append_u32(m, RTAX_INITRWND, route->initrwnd); + if (r < 0) + return r; + } + + if (route->quickack >= 0) { + r = sd_netlink_message_append_u32(m, RTAX_QUICKACK, route->quickack); + if (r < 0) + return r; + } + + if (route->fast_open_no_cookie >= 0) { + r = sd_netlink_message_append_u32(m, RTAX_FASTOPEN_NO_COOKIE, route->fast_open_no_cookie); + if (r < 0) + return r; + } + + if (route->advmss > 0) { + r = sd_netlink_message_append_u32(m, RTAX_ADVMSS, route->advmss); + if (r < 0) + return r; + } + + if (!isempty(route->tcp_congestion_control_algo)) { + r = sd_netlink_message_append_string(m, RTAX_CC_ALGO, route->tcp_congestion_control_algo); + if (r < 0) + return r; + } + + if (route->hop_limit > 0) { + r = sd_netlink_message_append_u32(m, RTAX_HOPLIMIT, route->hop_limit); + if (r < 0) + return r; + } + + if (route->tcp_rto_usec > 0) { + r = sd_netlink_message_append_u32(m, RTAX_RTO_MIN, DIV_ROUND_UP(route->tcp_rto_usec, USEC_PER_MSEC)); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(m); + if (r < 0) + return r; + + if (!ordered_set_isempty(route->multipath_routes)) { + assert(route->nexthop_id == 0); + assert(!in_addr_is_set(route->gw_family, &route->gw)); + + r = append_nexthops(link, route, m); + if (r < 0) + return r; + } + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static int route_is_ready_to_configure(const Route *route, Link *link) { + int r; + + assert(route); + assert(link); + + if (!link_is_ready_to_configure(link, false)) + return false; + + if (set_size(link->routes) >= routes_max()) + return false; + + if (route->nexthop_id > 0) { + struct nexthop_grp *nhg; + NextHop *nh; + + if (manager_get_nexthop_by_id(link->manager, route->nexthop_id, &nh) < 0) + return false; + + if (!nexthop_exists(nh)) + return false; + + HASHMAP_FOREACH(nhg, nh->group) { + NextHop *g; + + if (manager_get_nexthop_by_id(link->manager, nhg->id, &g) < 0) + return false; + + if (!nexthop_exists(g)) + return false; + } + } + + if (in_addr_is_set(route->family, &route->prefsrc) > 0) { + r = manager_has_address(link->manager, route->family, &route->prefsrc, route->family == AF_INET6); + if (r <= 0) + return r; + } + + if (!gateway_is_ready(link, FLAGS_SET(route->flags, RTNH_F_ONLINK), route->gw_family, &route->gw)) + return false; + + MultipathRoute *m; + ORDERED_SET_FOREACH(m, route->multipath_routes) { + union in_addr_union a = m->gateway.address; + Link *l = NULL; + + r = multipath_route_get_link(link->manager, m, &l); + if (r < 0) + return false; + if (r > 0) { + if (!link_is_ready_to_configure(l, /* allow_unmanaged = */ true) || + !link_has_carrier(l)) + return false; + + m->ifindex = l->ifindex; + } + + if (!gateway_is_ready(l ?: link, FLAGS_SET(route->flags, RTNH_F_ONLINK), m->gateway.family, &a)) + return false; + } + + return true; +} + +static int route_process_request(Request *req, Link *link, Route *route) { + _cleanup_(converted_routes_freep) ConvertedRoutes *converted = NULL; + int r; + + assert(req); + assert(link); + assert(link->manager); + assert(route); + + r = route_is_ready_to_configure(route, link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to check if route is ready to configure: %m"); + if (r == 0) + return 0; + + if (route_needs_convert(route)) { + r = route_convert(link->manager, route, &converted); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to convert route: %m"); + + assert(r > 0); + assert(converted); + + for (size_t i = 0; i < converted->n; i++) { + Route *existing; + + if (route_get(link->manager, converted->links[i] ?: link, converted->routes[i], &existing) < 0) { + _cleanup_(route_freep) Route *tmp = NULL; + + r = route_dup(converted->routes[i], &tmp); + if (r < 0) + return log_oom(); + + r = route_add(link->manager, converted->links[i] ?: link, tmp); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to add route: %m"); + + TAKE_PTR(tmp); + } else { + existing->source = converted->routes[i]->source; + existing->provider = converted->routes[i]->provider; + } + } + } + + usec_t now_usec; + assert_se(sd_event_now(link->manager->event, CLOCK_BOOTTIME, &now_usec) >= 0); + uint32_t sec = usec_to_sec(route->lifetime_usec, now_usec); + if (sec == 0) { + log_link_debug(link, "Refuse to configure %s route with zero lifetime.", + network_config_source_to_string(route->source)); + + if (converted) + for (size_t i = 0; i < converted->n; i++) { + Route *existing; + + assert_se(route_get(link->manager, converted->links[i] ?: link, converted->routes[i], &existing) >= 0); + route_cancel_requesting(existing); + } + else + route_cancel_requesting(route); + + return 1; + } + + r = route_configure(route, sec, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure route: %m"); + + if (converted) + for (size_t i = 0; i < converted->n; i++) { + Route *existing; + + assert_se(route_get(link->manager, converted->links[i] ?: link, converted->routes[i], &existing) >= 0); + route_enter_configuring(existing); + } + else + route_enter_configuring(route); + + return 1; +} + +int link_request_route( + Link *link, + Route *route, + bool consume_object, + unsigned *message_counter, + route_netlink_handler_t netlink_handler, + Request **ret) { + + Route *existing = NULL; + int r; + + assert(link); + assert(link->manager); + assert(route); + assert(route->source != NETWORK_CONFIG_SOURCE_FOREIGN); + assert(!route_needs_convert(route)); + + (void) route_get(link->manager, link, route, &existing); + + if (route->lifetime_usec == 0) { + if (consume_object) + route_free(route); + + /* The requested route is outdated. Let's remove it. */ + return route_remove_and_drop(existing); + } + + if (!existing) { + _cleanup_(route_freep) Route *tmp = NULL; + + if (consume_object) + tmp = route; + else { + r = route_dup(route, &tmp); + if (r < 0) + return r; + } + + r = route_add(link->manager, link, tmp); + if (r < 0) + return r; + + existing = TAKE_PTR(tmp); + } else { + existing->source = route->source; + existing->provider = route->provider; + existing->lifetime_usec = route->lifetime_usec; + if (consume_object) + route_free(route); + + if (existing->expire) { + /* When re-configuring an existing route, kernel does not send RTM_NEWROUTE + * message, so we need to update the timer here. */ + r = route_setup_timer(existing, NULL); + if (r < 0) + log_link_warning_errno(link, r, "Failed to update expiration timer for route, ignoring: %m"); + if (r > 0) + log_route_debug(existing, "Updated expiration timer for", link, link->manager); + } + } + + log_route_debug(existing, "Requesting", link, link->manager); + r = link_queue_request_safe(link, REQUEST_TYPE_ROUTE, + existing, NULL, + route_hash_func, + route_compare_func, + route_process_request, + message_counter, netlink_handler, ret); + if (r <= 0) + return r; + + route_enter_requesting(existing); + return 1; +} + +static int static_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) { + int r; + + assert(link); + + r = route_configure_handler_internal(rtnl, m, link, "Could not set route"); + if (r <= 0) + return r; + + if (link->static_route_messages == 0) { + log_link_debug(link, "Routes set"); + link->static_routes_configured = true; + link_check_ready(link); + } + + return 1; +} + +static int link_request_static_route(Link *link, Route *route) { + assert(link); + assert(link->manager); + assert(route); + + if (!route_needs_convert(route)) + return link_request_route(link, route, false, &link->static_route_messages, + static_route_handler, NULL); + + log_route_debug(route, "Requesting", link, link->manager); + return link_queue_request_safe(link, REQUEST_TYPE_ROUTE, + route, NULL, route_hash_func, route_compare_func, + route_process_request, + &link->static_route_messages, static_route_handler, NULL); +} + +static int link_request_wireguard_routes(Link *link, bool only_ipv4) { + NetDev *netdev; + Route *route; + int r; + + assert(link); + + if (!streq_ptr(link->kind, "wireguard")) + return 0; + + if (netdev_get(link->manager, link->ifname, &netdev) < 0) + return 0; + + Wireguard *w = WIREGUARD(netdev); + + SET_FOREACH(route, w->routes) { + if (only_ipv4 && route->family != AF_INET) + continue; + + r = link_request_static_route(link, route); + if (r < 0) + return r; + } + + return 0; +} + +int link_request_static_routes(Link *link, bool only_ipv4) { + Route *route; + int r; + + assert(link); + assert(link->network); + + link->static_routes_configured = false; + + HASHMAP_FOREACH(route, link->network->routes_by_section) { + if (route->gateway_from_dhcp_or_ra) + continue; + + if (only_ipv4 && route->family != AF_INET) + continue; + + r = link_request_static_route(link, route); + if (r < 0) + return r; + } + + r = link_request_wireguard_routes(link, only_ipv4); + if (r < 0) + return r; + + if (link->static_route_messages == 0) { + link->static_routes_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Requesting routes"); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +void route_cancel_request(Route *route, Link *link) { + Request req; + + assert(route); + + link = route->link ?: link; + + assert(link); + + if (!route_is_requesting(route)) + return; + + req = (Request) { + .link = link, + .type = REQUEST_TYPE_ROUTE, + .userdata = route, + .hash_func = (hash_func_t) route_hash_func, + .compare_func = (compare_func_t) route_compare_func, + }; + + request_detach(link->manager, &req); + route_cancel_requesting(route); +} + +static int process_route_one( + Manager *manager, + Link *link, + uint16_t type, + Route *in, + const struct rta_cacheinfo *cacheinfo) { + + _cleanup_(route_freep) Route *tmp = in; + Route *route = NULL; + bool update_dhcp4; + int r; + + assert(manager); + assert(tmp); + assert(IN_SET(type, RTM_NEWROUTE, RTM_DELROUTE)); + + /* link may be NULL. This consumes 'in'. */ + + update_dhcp4 = link && tmp->family == AF_INET6 && tmp->dst_prefixlen == 0; + + (void) route_get(manager, link, tmp, &route); + + switch (type) { + case RTM_NEWROUTE: + if (route) { + route->flags = tmp->flags; + route_enter_configured(route); + log_route_debug(route, "Received remembered", link, manager); + + r = route_setup_timer(route, cacheinfo); + if (r < 0) + log_link_warning_errno(link, r, "Failed to configure expiration timer for route, ignoring: %m"); + if (r > 0) + log_route_debug(route, "Configured expiration timer for", link, manager); + + } else if (!manager->manage_foreign_routes) { + route_enter_configured(tmp); + log_route_debug(tmp, "Ignoring received", link, manager); + + } else { + /* A route appeared that we did not request */ + route_enter_configured(tmp); + log_route_debug(tmp, "Received new", link, manager); + r = route_add(manager, link, tmp); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to remember foreign route, ignoring: %m"); + return 0; + } + TAKE_PTR(tmp); + } + + break; + + case RTM_DELROUTE: + if (route) { + route_enter_removed(route); + if (route->state == 0) { + log_route_debug(route, "Forgetting", link, manager); + route_free(route); + } else + log_route_debug(route, "Removed", link, manager); + } else + log_route_debug(tmp, + manager->manage_foreign_routes ? "Kernel removed unknown" : "Ignoring received", + link, manager); + + break; + + default: + assert_not_reached(); + } + + if (update_dhcp4) { + r = dhcp4_update_ipv6_connectivity(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to notify IPv6 connectivity to DHCPv4 client: %m"); + link_enter_failed(link); + } + } + + return 1; +} + +int manager_rtnl_process_route(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) { + _cleanup_(converted_routes_freep) ConvertedRoutes *converted = NULL; + _cleanup_(route_freep) Route *tmp = NULL; + _cleanup_free_ void *rta_multipath = NULL; + struct rta_cacheinfo cacheinfo; + bool has_cacheinfo; + Link *link = NULL; + uint32_t ifindex; + uint16_t type; + size_t rta_len; + int r; + + assert(rtnl); + assert(message); + assert(m); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "rtnl: failed to receive route message, ignoring"); + + return 0; + } + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get message type, ignoring: %m"); + return 0; + } else if (!IN_SET(type, RTM_NEWROUTE, RTM_DELROUTE)) { + log_warning("rtnl: received unexpected message type %u when processing route, ignoring.", type); + return 0; + } + + r = sd_netlink_message_read_u32(message, RTA_OIF, &ifindex); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get ifindex from route message, ignoring: %m"); + return 0; + } else if (r >= 0) { + if (ifindex <= 0) { + log_warning("rtnl: received route message with invalid ifindex %u, ignoring.", ifindex); + return 0; + } + + r = link_get_by_index(m, ifindex, &link); + if (r < 0) { + /* when enumerating we might be out of sync, but we will + * get the route again, so just ignore it */ + if (!m->enumerating) + log_warning("rtnl: received route message for link (%u) we do not know about, ignoring", ifindex); + return 0; + } + } + + r = route_new(&tmp); + if (r < 0) + return log_oom(); + + r = sd_rtnl_message_route_get_family(message, &tmp->family); + if (r < 0) { + log_link_warning(link, "rtnl: received route message without family, ignoring"); + return 0; + } else if (!IN_SET(tmp->family, AF_INET, AF_INET6)) { + log_link_debug(link, "rtnl: received route message with invalid family '%i', ignoring", tmp->family); + return 0; + } + + r = sd_rtnl_message_route_get_protocol(message, &tmp->protocol); + if (r < 0) { + log_warning_errno(r, "rtnl: received route message without route protocol, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_route_get_flags(message, &tmp->flags); + if (r < 0) { + log_warning_errno(r, "rtnl: received route message without route flags, ignoring: %m"); + return 0; + } + + r = netlink_message_read_in_addr_union(message, RTA_DST, tmp->family, &tmp->dst); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message without valid destination, ignoring: %m"); + return 0; + } + + r = netlink_message_read_in_addr_union(message, RTA_GATEWAY, tmp->family, &tmp->gw); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message without valid gateway, ignoring: %m"); + return 0; + } else if (r >= 0) + tmp->gw_family = tmp->family; + else if (tmp->family == AF_INET) { + RouteVia via; + + r = sd_netlink_message_read(message, RTA_VIA, sizeof(via), &via); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message without valid gateway, ignoring: %m"); + return 0; + } else if (r >= 0) { + tmp->gw_family = via.family; + tmp->gw = via.address; + } + } + + r = netlink_message_read_in_addr_union(message, RTA_SRC, tmp->family, &tmp->src); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message without valid source, ignoring: %m"); + return 0; + } + + r = netlink_message_read_in_addr_union(message, RTA_PREFSRC, tmp->family, &tmp->prefsrc); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message without valid preferred source, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_route_get_dst_prefixlen(message, &tmp->dst_prefixlen); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid destination prefixlen, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_route_get_src_prefixlen(message, &tmp->src_prefixlen); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid source prefixlen, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_route_get_scope(message, &tmp->scope); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid scope, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_route_get_tos(message, &tmp->tos); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid tos, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_route_get_type(message, &tmp->type); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid type, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, RTA_TABLE, &tmp->table); + if (r == -ENODATA) { + unsigned char table; + + r = sd_rtnl_message_route_get_table(message, &table); + if (r >= 0) + tmp->table = table; + } + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid table, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, RTA_PRIORITY, &tmp->priority); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid priority, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, RTA_NH_ID, &tmp->nexthop_id); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid nexthop id, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_enter_container(message, RTA_METRICS); + if (r < 0 && r != -ENODATA) { + log_link_error_errno(link, r, "rtnl: Could not enter RTA_METRICS container, ignoring: %m"); + return 0; + } + if (r >= 0) { + r = sd_netlink_message_read_u32(message, RTAX_INITCWND, &tmp->initcwnd); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid initcwnd, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, RTAX_INITRWND, &tmp->initrwnd); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid initrwnd, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, RTAX_ADVMSS, &tmp->advmss); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: received route message with invalid advmss, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_exit_container(message); + if (r < 0) { + log_link_error_errno(link, r, "rtnl: Could not exit from RTA_METRICS container, ignoring: %m"); + return 0; + } + } + + r = sd_netlink_message_read_data(message, RTA_MULTIPATH, &rta_len, &rta_multipath); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: failed to read RTA_MULTIPATH attribute, ignoring: %m"); + return 0; + } else if (r >= 0) { + r = rtattr_read_nexthop(rta_multipath, rta_len, tmp->family, &tmp->multipath_routes); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: failed to parse RTA_MULTIPATH attribute, ignoring: %m"); + return 0; + } + } + + r = sd_netlink_message_read(message, RTA_CACHEINFO, sizeof(cacheinfo), &cacheinfo); + if (r < 0 && r != -ENODATA) { + log_link_warning_errno(link, r, "rtnl: failed to read RTA_CACHEINFO attribute, ignoring: %m"); + return 0; + } + has_cacheinfo = r >= 0; + + /* IPv6 routes with reject type are always assigned to the loopback interface. See kernel's + * fib6_nh_init() in net/ipv6/route.c. However, we'd like to manage them by Manager. Hence, set + * link to NULL here. */ + if (route_type_is_reject(tmp)) + link = NULL; + + if (!route_needs_convert(tmp)) + return process_route_one(m, link, type, TAKE_PTR(tmp), has_cacheinfo ? &cacheinfo : NULL); + + r = route_convert(m, tmp, &converted); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: failed to convert received route, ignoring: %m"); + return 0; + } + + assert(r > 0); + assert(converted); + + for (size_t i = 0; i < converted->n; i++) + (void) process_route_one(m, + converted->links[i] ?: link, + type, + TAKE_PTR(converted->routes[i]), + has_cacheinfo ? &cacheinfo : NULL); + + return 1; +} + +int network_add_ipv4ll_route(Network *network) { + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + unsigned section_line; + int r; + + assert(network); + + if (!network->ipv4ll_route) + return 0; + + r = hashmap_by_section_find_unused_line(network->routes_by_section, network->filename, §ion_line); + if (r < 0) + return r; + + /* 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; + + r = hashmap_by_section_find_unused_line(network->routes_by_section, network->filename, §ion_line); + if (r < 0) + return r; + + /* DefaultRouteOnDevice= is in [Network] section. */ + r = route_new_static(network, network->filename, section_line, &n); + if (r < 0) + return r; + + n->family = AF_INET; + n->scope = RT_SCOPE_LINK; + n->scope_set = true; + n->protocol = RTPROT_STATIC; + + TAKE_PTR(n); + return 0; +} + +int config_parse_gateway( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + if (streq(section, "Network")) { + /* we are not in an Route section, so use line number instead */ + r = route_new_static(network, filename, line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + } else { + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + if (isempty(rvalue)) { + n->gateway_from_dhcp_or_ra = false; + n->gw_family = AF_UNSPEC; + n->gw = IN_ADDR_NULL; + TAKE_PTR(n); + return 0; + } + + if (streq(rvalue, "_dhcp")) { + n->gateway_from_dhcp_or_ra = true; + TAKE_PTR(n); + return 0; + } + + if (streq(rvalue, "_dhcp4")) { + n->gw_family = AF_INET; + n->gateway_from_dhcp_or_ra = true; + TAKE_PTR(n); + return 0; + } + + if (streq(rvalue, "_ipv6ra")) { + n->gw_family = AF_INET6; + n->gateway_from_dhcp_or_ra = true; + TAKE_PTR(n); + return 0; + } + } + + r = in_addr_from_string_auto(rvalue, &n->gw_family, &n->gw); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + n->gateway_from_dhcp_or_ra = false; + TAKE_PTR(n); + return 0; +} + +int config_parse_preferred_src( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + if (n->family == AF_UNSPEC) + r = in_addr_from_string_auto(rvalue, &n->family, &n->prefsrc); + else + r = in_addr_from_string(n->family, rvalue, &n->prefsrc); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, EINVAL, + "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_destination( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + union in_addr_union *buffer; + unsigned char *prefixlen; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + if (streq(lvalue, "Destination")) { + buffer = &n->dst; + prefixlen = &n->dst_prefixlen; + } else if (streq(lvalue, "Source")) { + buffer = &n->src; + prefixlen = &n->src_prefixlen; + } else + assert_not_reached(); + + if (n->family == AF_UNSPEC) + r = in_addr_prefix_from_string_auto(rvalue, &n->family, buffer, prefixlen); + else + r = in_addr_prefix_from_string(rvalue, n->family, buffer, prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, EINVAL, + "Invalid %s='%s', ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + (void) in_addr_mask(n->family, buffer, *prefixlen); + + TAKE_PTR(n); + return 0; +} + +int config_parse_route_priority( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + r = safe_atou32(rvalue, &n->priority); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->priority_set = true; + TAKE_PTR(n); + return 0; +} + +int config_parse_route_scope( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + r = route_scope_from_string(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Unknown route scope: %s", rvalue); + return 0; + } + + n->scope = r; + n->scope_set = true; + TAKE_PTR(n); + return 0; +} + +int config_parse_route_nexthop( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + uint32_t id; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + if (isempty(rvalue)) { + n->nexthop_id = 0; + TAKE_PTR(n); + return 0; + } + + r = safe_atou32(rvalue, &id); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse nexthop ID, ignoring assignment: %s", rvalue); + return 0; + } + if (id == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid nexthop ID, ignoring assignment: %s", rvalue); + return 0; + } + + n->nexthop_id = id; + TAKE_PTR(n); + return 0; +} + +int config_parse_route_table( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + r = manager_get_route_table_from_string(network->manager, rvalue, &n->table); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse route table \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->table_set = true; + TAKE_PTR(n); + return 0; +} + +int config_parse_route_boolean( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse %s=\"%s\", ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + if (STR_IN_SET(lvalue, "GatewayOnLink", "GatewayOnlink")) + n->gateway_onlink = r; + else if (streq(lvalue, "QuickAck")) + n->quickack = r; + else if (streq(lvalue, "FastOpenNoCookie")) + n->fast_open_no_cookie = r; + else if (streq(lvalue, "TTLPropagate")) + n->ttl_propagate = r; + else + assert_not_reached(); + + TAKE_PTR(n); + return 0; +} + +int config_parse_ipv6_route_preference( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int r; + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + if (streq(rvalue, "low")) + n->pref = ICMPV6_ROUTER_PREF_LOW; + else if (streq(rvalue, "medium")) + n->pref = ICMPV6_ROUTER_PREF_MEDIUM; + else if (streq(rvalue, "high")) + n->pref = ICMPV6_ROUTER_PREF_HIGH; + else { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Unknown route preference: %s", rvalue); + return 0; + } + + n->pref_set = true; + TAKE_PTR(n); + return 0; +} + +int config_parse_route_protocol( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int r; + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + r = route_protocol_from_string(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse route protocol \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->protocol = r; + + TAKE_PTR(n); + return 0; +} + +int config_parse_route_type( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int t, r; + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + t = route_type_from_string(rvalue); + if (t < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse route type \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->type = (unsigned char) t; + + TAKE_PTR(n); + return 0; +} + +int config_parse_route_hop_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_(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; + } + + if (isempty(rvalue)) { + n->hop_limit = 0; + TAKE_PTR(n); + return 0; + } + + r = safe_atou32(rvalue, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse per route hop limit, ignoring assignment: %s", rvalue); + return 0; + } + if (k > 255) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified per route hop limit \"%s\" is too large, ignoring assignment: %m", rvalue); + return 0; + } + if (k == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid per route hop limit \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->hop_limit = k; + + TAKE_PTR(n); + return 0; +} + +int config_parse_tcp_congestion( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + r = config_parse_string(unit, filename, line, section, section_line, lvalue, ltype, + rvalue, &n->tcp_congestion_control_algo, userdata); + if (r < 0) + return r; + + TAKE_PTR(n); + return 0; +} + +int config_parse_tcp_advmss( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + Network *network = userdata; + uint64_t u; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + if (isempty(rvalue)) { + n->advmss = 0; + TAKE_PTR(n); + return 0; + } + + r = parse_size(rvalue, 1024, &u); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse TCPAdvertisedMaximumSegmentSize= \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + if (u == 0 || u > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid TCPAdvertisedMaximumSegmentSize= \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->advmss = u; + + TAKE_PTR(n); + return 0; +} + +int config_parse_tcp_window( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint32_t *window = ASSERT_PTR(data); + uint32_t k; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = safe_atou32(rvalue, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse TCP %s \"%s\", ignoring assignment: %m", lvalue, rvalue); + return 0; + } + if (k >= 1024) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified TCP %s \"%s\" is too large, ignoring assignment: %m", lvalue, rvalue); + return 0; + } + if (k == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid TCP %s \"%s\", ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + *window = k; + return 0; +} + +int config_parse_route_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 *d; + 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, "InitialCongestionWindow")) + d = &n->initcwnd; + else if (streq(lvalue, "InitialAdvertisedReceiveWindow")) + d = &n->initrwnd; + else + assert_not_reached(); + + r = config_parse_tcp_window(unit, filename, line, section, section_line, lvalue, ltype, rvalue, d, userdata); + if (r < 0) + return r; + + 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_route_tcp_rto( + const char *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; + usec_t usec; + 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_sec(rvalue, &usec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse route TCP retransmission timeout (RTO), ignoring assignment: %s", rvalue); + return 0; + } + + if (IN_SET(usec, 0, USEC_INFINITY) || + DIV_ROUND_UP(usec, USEC_PER_MSEC) > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Route TCP retransmission timeout (RTO) must be in the range 0…%"PRIu32"ms, ignoring assignment: %s", UINT32_MAX, rvalue); + return 0; + } + + n->tcp_rto_usec = usec; + + TAKE_PTR(n); + return 0; +} + +int config_parse_multipath_route( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(multipath_route_freep) MultipathRoute *m = NULL; + _cleanup_(route_free_or_set_invalidp) Route *n = NULL; + _cleanup_free_ char *word = NULL; + Network *network = userdata; + union in_addr_union a; + int family, r; + const char *p; + char *dev; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = route_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate route, ignoring assignment: %m"); + return 0; + } + + if (isempty(rvalue)) { + n->multipath_routes = ordered_set_free_with_destructor(n->multipath_routes, multipath_route_free); + TAKE_PTR(n); + return 0; + } + + m = new0(MultipathRoute, 1); + if (!m) + return log_oom(); + + p = rvalue; + r = extract_first_word(&p, &word, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r <= 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid multipath route option, ignoring assignment: %s", rvalue); + return 0; + } + + dev = strchr(word, '@'); + if (dev) { + *dev++ = '\0'; + + r = parse_ifindex(dev); + if (r > 0) + m->ifindex = r; + else { + if (!ifname_valid_full(dev, IFNAME_VALID_ALTERNATIVE)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid interface name '%s' in %s=, ignoring: %s", dev, lvalue, rvalue); + return 0; + } + + m->ifname = strdup(dev); + if (!m->ifname) + return log_oom(); + } + } + + r = in_addr_from_string_auto(word, &family, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid multipath route gateway '%s', ignoring assignment: %m", rvalue); + return 0; + } + m->gateway.address = a; + m->gateway.family = family; + + if (!isempty(p)) { + r = safe_atou32(p, &m->weight); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid multipath route weight, ignoring assignment: %s", p); + return 0; + } + /* ip command takes weight in the range 1…255, while kernel takes the value in the + * range 0…254. MultiPathRoute= setting also takes weight in the same range which ip + * command uses, then networkd decreases by one and stores it to match the range which + * kernel uses. */ + if (m->weight == 0 || m->weight > 256) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid multipath route weight, ignoring assignment: %s", p); + return 0; + } + m->weight--; + } + + r = ordered_set_ensure_put(&n->multipath_routes, NULL, m); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store multipath route, ignoring assignment: %m"); + return 0; + } + + TAKE_PTR(m); + TAKE_PTR(n); + return 0; +} + +static int route_section_verify(Route *route, Network *network) { + if (section_is_invalid(route->section)) + return -EINVAL; + + /* Currently, we do not support static route with finite lifetime. */ + assert(route->lifetime_usec == USEC_INFINITY); + + if (route->gateway_from_dhcp_or_ra) { + if (route->gw_family == AF_UNSPEC) { + /* When deprecated Gateway=_dhcp is set, then assume gateway family based on other settings. */ + switch (route->family) { + case AF_UNSPEC: + log_warning("%s: Deprecated value \"_dhcp\" is specified for Gateway= in [Route] section from line %u. " + "Please use \"_dhcp4\" or \"_ipv6ra\" instead. Assuming \"_dhcp4\".", + route->section->filename, route->section->line); + route->family = AF_INET; + break; + case AF_INET: + case AF_INET6: + log_warning("%s: Deprecated value \"_dhcp\" is specified for Gateway= in [Route] section from line %u. " + "Assuming \"%s\" based on Destination=, Source=, or PreferredSource= setting.", + route->section->filename, route->section->line, route->family == AF_INET ? "_dhcp4" : "_ipv6ra"); + break; + default: + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Invalid route family. Ignoring [Route] section from line %u.", + route->section->filename, route->section->line); + } + route->gw_family = route->family; + } + + if (route->gw_family == AF_INET && !FLAGS_SET(network->dhcp, ADDRESS_FAMILY_IPV4)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Gateway=\"_dhcp4\" is specified but DHCPv4 client is disabled. " + "Ignoring [Route] section from line %u.", + route->section->filename, route->section->line); + + if (route->gw_family == AF_INET6 && !network->ipv6_accept_ra) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Gateway=\"_ipv6ra\" is specified but IPv6AcceptRA= is disabled. " + "Ignoring [Route] section from line %u.", + route->section->filename, route->section->line); + } + + /* When only Gateway= is specified, assume the route family based on the Gateway address. */ + if (route->family == AF_UNSPEC) + route->family = route->gw_family; + + if (route->family == AF_UNSPEC) { + assert(route->section); + + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Route section without Gateway=, Destination=, Source=, " + "or PreferredSource= field configured. " + "Ignoring [Route] section from line %u.", + route->section->filename, route->section->line); + } + + if (route->family == AF_INET6 && route->gw_family == AF_INET) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: IPv4 gateway is configured for IPv6 route. " + "Ignoring [Route] section from line %u.", + route->section->filename, route->section->line); + + if (!route->table_set && network->vrf) { + route->table = VRF(network->vrf)->table; + route->table_set = true; + } + + if (!route->table_set && IN_SET(route->type, RTN_LOCAL, RTN_BROADCAST, RTN_ANYCAST, RTN_NAT)) + route->table = RT_TABLE_LOCAL; + + if (!route->scope_set && route->family != AF_INET6) { + if (IN_SET(route->type, RTN_LOCAL, RTN_NAT)) + route->scope = RT_SCOPE_HOST; + else if (IN_SET(route->type, RTN_BROADCAST, RTN_ANYCAST, RTN_MULTICAST)) + route->scope = RT_SCOPE_LINK; + else if (IN_SET(route->type, RTN_UNICAST, RTN_UNSPEC) && + !route->gateway_from_dhcp_or_ra && + !in_addr_is_set(route->gw_family, &route->gw) && + ordered_set_isempty(route->multipath_routes) && + route->nexthop_id == 0) + route->scope = RT_SCOPE_LINK; + } + + if (route->scope != RT_SCOPE_UNIVERSE && route->family == AF_INET6) { + log_warning("%s: Scope= is specified for IPv6 route. It will be ignored.", route->section->filename); + route->scope = RT_SCOPE_UNIVERSE; + } + + if (route->family == AF_INET6 && route->priority == 0) + route->priority = IP6_RT_PRIO_USER; + + if (route->gateway_onlink < 0 && in_addr_is_set(route->gw_family, &route->gw) && + ordered_hashmap_isempty(network->addresses_by_section)) { + /* If no address is configured, in most cases the gateway cannot be reachable. + * TODO: we may need to improve the condition above. */ + log_warning("%s: Gateway= without static address configured. " + "Enabling GatewayOnLink= option.", + network->filename); + route->gateway_onlink = true; + } + + if (route->gateway_onlink >= 0) + SET_FLAG(route->flags, RTNH_F_ONLINK, route->gateway_onlink); + + if (route->family == AF_INET6) { + MultipathRoute *m; + + ORDERED_SET_FOREACH(m, route->multipath_routes) + if (m->gateway.family == AF_INET) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: IPv4 multipath route is specified for IPv6 route. " + "Ignoring [Route] section from line %u.", + route->section->filename, route->section->line); + } + + if ((route->gateway_from_dhcp_or_ra || + in_addr_is_set(route->gw_family, &route->gw)) && + !ordered_set_isempty(route->multipath_routes)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Gateway= cannot be specified with MultiPathRoute=. " + "Ignoring [Route] section from line %u.", + route->section->filename, route->section->line); + + if (route->nexthop_id > 0 && + (route->gateway_from_dhcp_or_ra || + in_addr_is_set(route->gw_family, &route->gw) || + !ordered_set_isempty(route->multipath_routes))) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: NextHopId= cannot be specified with Gateway= or MultiPathRoute=. " + "Ignoring [Route] section from line %u.", + route->section->filename, route->section->line); + + return 0; +} + +void network_drop_invalid_routes(Network *network) { + Route *route; + + assert(network); + + HASHMAP_FOREACH(route, network->routes_by_section) + if (route_section_verify(route, network) < 0) + route_free(route); +} diff --git a/src/network/networkd-route.h b/src/network/networkd-route.h new file mode 100644 index 0000000..3d85889 --- /dev/null +++ b/src/network/networkd-route.h @@ -0,0 +1,135 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> + +#include "sd-netlink.h" + +#include "conf-parser.h" +#include "in-addr-util.h" +#include "networkd-link.h" +#include "networkd-util.h" + +typedef struct Manager Manager; +typedef struct Network Network; +typedef struct Request Request; +typedef struct Route Route; +typedef int (*route_netlink_handler_t)( + sd_netlink *rtnl, + sd_netlink_message *m, + Request *req, + Link *link, + Route *route); + +struct Route { + Link *link; + Manager *manager; + Network *network; + ConfigSection *section; + NetworkConfigSource source; + NetworkConfigState state; + union in_addr_union provider; /* DHCP server or router address */ + + int family; + int gw_family; + uint32_t gw_weight; + int quickack; + int fast_open_no_cookie; + int ttl_propagate; + + unsigned char dst_prefixlen; + unsigned char src_prefixlen; + unsigned char scope; + unsigned char protocol; /* RTPROT_* */ + unsigned char type; /* RTN_* */ + unsigned char tos; + uint32_t priority; /* note that ip(8) calls this 'metric' */ + uint32_t table; + uint32_t mtu; + uint32_t initcwnd; + uint32_t initrwnd; + uint32_t advmss; + uint32_t hop_limit; + char *tcp_congestion_control_algo; + unsigned char pref; + unsigned flags; + int gateway_onlink; /* Only used in conf parser and route_section_verify(). */ + uint32_t nexthop_id; + usec_t tcp_rto_usec; + + bool scope_set:1; + bool table_set:1; + bool priority_set:1; + bool protocol_set:1; + bool pref_set:1; + bool gateway_from_dhcp_or_ra:1; + + union in_addr_union gw; + union in_addr_union dst; + union in_addr_union src; + union in_addr_union prefsrc; + OrderedSet *multipath_routes; + + /* This is an absolute point in time, and NOT a timespan/duration. + * Must be specified with clock_boottime_or_monotonic(). */ + usec_t lifetime_usec; + /* Used when kernel does not support RTA_EXPIRES attribute. */ + sd_event_source *expire; +}; + +extern const struct hash_ops route_hash_ops; + +int route_new(Route **ret); +Route *route_free(Route *route); +DEFINE_SECTION_CLEANUP_FUNCTIONS(Route, route_free); +int route_dup(const Route *src, Route **ret); + +int route_configure_handler_internal(sd_netlink *rtnl, sd_netlink_message *m, Link *link, const char *error_msg); +int route_remove(Route *route); +int route_remove_and_drop(Route *route); + +int route_get(Manager *manager, Link *link, const Route *in, Route **ret); + +int link_drop_managed_routes(Link *link); +int link_drop_foreign_routes(Link *link); +void link_foreignize_routes(Link *link); + +void route_cancel_request(Route *route, Link *link); +int link_request_route( + Link *link, + Route *route, + bool consume_object, + unsigned *message_counter, + route_netlink_handler_t netlink_handler, + Request **ret); +int link_request_static_routes(Link *link, bool only_ipv4); + +int manager_rtnl_process_route(sd_netlink *rtnl, sd_netlink_message *message, Manager *m); + +int network_add_ipv4ll_route(Network *network); +int network_add_default_route_on_device(Network *network); +void network_drop_invalid_routes(Network *network); + +DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(Route, route); +void link_mark_routes(Link *link, NetworkConfigSource source); + +CONFIG_PARSER_PROTOTYPE(config_parse_gateway); +CONFIG_PARSER_PROTOTYPE(config_parse_preferred_src); +CONFIG_PARSER_PROTOTYPE(config_parse_destination); +CONFIG_PARSER_PROTOTYPE(config_parse_route_priority); +CONFIG_PARSER_PROTOTYPE(config_parse_route_scope); +CONFIG_PARSER_PROTOTYPE(config_parse_route_table); +CONFIG_PARSER_PROTOTYPE(config_parse_route_boolean); +CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_route_preference); +CONFIG_PARSER_PROTOTYPE(config_parse_route_protocol); +CONFIG_PARSER_PROTOTYPE(config_parse_route_type); +CONFIG_PARSER_PROTOTYPE(config_parse_route_tcp_window); +CONFIG_PARSER_PROTOTYPE(config_parse_route_hop_limit); +CONFIG_PARSER_PROTOTYPE(config_parse_tcp_window); +CONFIG_PARSER_PROTOTYPE(config_parse_route_tcp_rto); +CONFIG_PARSER_PROTOTYPE(config_parse_route_mtu); +CONFIG_PARSER_PROTOTYPE(config_parse_multipath_route); +CONFIG_PARSER_PROTOTYPE(config_parse_tcp_congestion); +CONFIG_PARSER_PROTOTYPE(config_parse_tcp_advmss); +CONFIG_PARSER_PROTOTYPE(config_parse_route_nexthop); diff --git a/src/network/networkd-routing-policy-rule.c b/src/network/networkd-routing-policy-rule.c new file mode 100644 index 0000000..0cb5831 --- /dev/null +++ b/src/network/networkd-routing-policy-rule.c @@ -0,0 +1,1754 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <linux/fib_rules.h> + +#include "af-list.h" +#include "alloc-util.h" +#include "conf-parser.h" +#include "fileio.h" +#include "format-util.h" +#include "hashmap.h" +#include "ip-protocol-list.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "networkd-queue.h" +#include "networkd-route-util.h" +#include "networkd-routing-policy-rule.h" +#include "networkd-util.h" +#include "parse-util.h" +#include "socket-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "user-util.h" + +static const char *const fr_act_type_table[__FR_ACT_MAX] = { + [FR_ACT_BLACKHOLE] = "blackhole", + [FR_ACT_UNREACHABLE] = "unreachable", + [FR_ACT_PROHIBIT] = "prohibit", +}; + +static const char *const fr_act_type_full_table[__FR_ACT_MAX] = { + [FR_ACT_TO_TBL] = "table", + [FR_ACT_GOTO] = "goto", + [FR_ACT_NOP] = "nop", + [FR_ACT_BLACKHOLE] = "blackhole", + [FR_ACT_UNREACHABLE] = "unreachable", + [FR_ACT_PROHIBIT] = "prohibit", +}; + +assert_cc(__FR_ACT_MAX <= UINT8_MAX); +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(fr_act_type, int); +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(fr_act_type_full, int); + +RoutingPolicyRule *routing_policy_rule_free(RoutingPolicyRule *rule) { + if (!rule) + return NULL; + + if (rule->network) { + assert(rule->section); + hashmap_remove(rule->network->rules_by_section, rule->section); + } + + if (rule->manager) + set_remove(rule->manager->rules, rule); + + config_section_free(rule->section); + free(rule->iif); + free(rule->oif); + + return mfree(rule); +} + +DEFINE_SECTION_CLEANUP_FUNCTIONS(RoutingPolicyRule, routing_policy_rule_free); + +static int routing_policy_rule_new(RoutingPolicyRule **ret) { + RoutingPolicyRule *rule; + + rule = new(RoutingPolicyRule, 1); + if (!rule) + return -ENOMEM; + + *rule = (RoutingPolicyRule) { + .table = RT_TABLE_MAIN, + .uid_range.start = UID_INVALID, + .uid_range.end = UID_INVALID, + .suppress_prefixlen = -1, + .suppress_ifgroup = -1, + .protocol = RTPROT_UNSPEC, + .type = FR_ACT_TO_TBL, + }; + + *ret = rule; + return 0; +} + +static int routing_policy_rule_new_static(Network *network, const char *filename, unsigned section_line, RoutingPolicyRule **ret) { + _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *rule = NULL; + _cleanup_(config_section_freep) ConfigSection *n = NULL; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + rule = hashmap_get(network->rules_by_section, n); + if (rule) { + *ret = TAKE_PTR(rule); + return 0; + } + + r = routing_policy_rule_new(&rule); + if (r < 0) + return r; + + rule->network = network; + rule->section = TAKE_PTR(n); + rule->source = NETWORK_CONFIG_SOURCE_STATIC; + rule->protocol = RTPROT_STATIC; + + r = hashmap_ensure_put(&network->rules_by_section, &config_section_hash_ops, rule->section, rule); + if (r < 0) + return r; + + *ret = TAKE_PTR(rule); + return 0; +} + +static int routing_policy_rule_dup(const RoutingPolicyRule *src, RoutingPolicyRule **ret) { + _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *dest = NULL; + + assert(src); + assert(ret); + + dest = newdup(RoutingPolicyRule, src, 1); + if (!dest) + return -ENOMEM; + + /* Unset all pointers */ + dest->manager = NULL; + dest->network = NULL; + dest->section = NULL; + dest->iif = dest->oif = NULL; + + if (src->iif) { + dest->iif = strdup(src->iif); + if (!dest->iif) + return -ENOMEM; + } + + if (src->oif) { + dest->oif = strdup(src->oif); + if (!dest->oif) + return -ENOMEM; + } + + *ret = TAKE_PTR(dest); + return 0; +} + +static void routing_policy_rule_hash_func(const RoutingPolicyRule *rule, struct siphash *state) { + assert(rule); + + siphash24_compress(&rule->family, sizeof(rule->family), state); + + switch (rule->family) { + case AF_INET: + case AF_INET6: + siphash24_compress(&rule->from, FAMILY_ADDRESS_SIZE(rule->family), state); + siphash24_compress(&rule->from_prefixlen, sizeof(rule->from_prefixlen), state); + + siphash24_compress(&rule->to, FAMILY_ADDRESS_SIZE(rule->family), state); + siphash24_compress(&rule->to_prefixlen, sizeof(rule->to_prefixlen), state); + + siphash24_compress_boolean(rule->invert_rule, state); + + siphash24_compress(&rule->tos, sizeof(rule->tos), state); + siphash24_compress(&rule->type, sizeof(rule->type), state); + siphash24_compress(&rule->fwmark, sizeof(rule->fwmark), state); + siphash24_compress(&rule->fwmask, sizeof(rule->fwmask), state); + siphash24_compress(&rule->priority, sizeof(rule->priority), state); + siphash24_compress(&rule->table, sizeof(rule->table), state); + siphash24_compress(&rule->suppress_prefixlen, sizeof(rule->suppress_prefixlen), state); + siphash24_compress(&rule->suppress_ifgroup, sizeof(rule->suppress_ifgroup), state); + + siphash24_compress(&rule->ipproto, sizeof(rule->ipproto), state); + siphash24_compress(&rule->protocol, sizeof(rule->protocol), state); + siphash24_compress(&rule->sport, sizeof(rule->sport), state); + siphash24_compress(&rule->dport, sizeof(rule->dport), state); + siphash24_compress(&rule->uid_range, sizeof(rule->uid_range), state); + + siphash24_compress_string(rule->iif, state); + siphash24_compress_string(rule->oif, state); + + break; + default: + /* treat any other address family as AF_UNSPEC */ + break; + } +} + +static int routing_policy_rule_compare_func(const RoutingPolicyRule *a, const RoutingPolicyRule *b) { + int r; + + r = CMP(a->family, b->family); + if (r != 0) + return r; + + switch (a->family) { + case AF_INET: + case AF_INET6: + r = CMP(a->from_prefixlen, b->from_prefixlen); + if (r != 0) + return r; + + r = memcmp(&a->from, &b->from, FAMILY_ADDRESS_SIZE(a->family)); + if (r != 0) + return r; + + r = CMP(a->to_prefixlen, b->to_prefixlen); + if (r != 0) + return r; + + r = memcmp(&a->to, &b->to, FAMILY_ADDRESS_SIZE(a->family)); + if (r != 0) + return r; + + r = CMP(a->invert_rule, b->invert_rule); + if (r != 0) + return r; + + r = CMP(a->tos, b->tos); + if (r != 0) + return r; + + r = CMP(a->type, b->type); + if (r != 0) + return r; + + r = CMP(a->fwmark, b->fwmark); + if (r != 0) + return r; + + r = CMP(a->fwmask, b->fwmask); + if (r != 0) + return r; + + r = CMP(a->priority, b->priority); + if (r != 0) + return r; + + r = CMP(a->table, b->table); + if (r != 0) + return r; + + r = CMP(a->suppress_prefixlen, b->suppress_prefixlen); + if (r != 0) + return r; + + r = CMP(a->suppress_ifgroup, b->suppress_ifgroup); + if (r != 0) + return r; + + r = CMP(a->ipproto, b->ipproto); + if (r != 0) + return r; + + r = CMP(a->protocol, b->protocol); + if (r != 0) + return r; + + r = memcmp(&a->sport, &b->sport, sizeof(a->sport)); + if (r != 0) + return r; + + r = memcmp(&a->dport, &b->dport, sizeof(a->dport)); + if (r != 0) + return r; + + r = memcmp(&a->uid_range, &b->uid_range, sizeof(a->uid_range)); + if (r != 0) + return r; + + r = strcmp_ptr(a->iif, b->iif); + if (r != 0) + return r; + + r = strcmp_ptr(a->oif, b->oif); + if (r != 0) + return r; + + return 0; + default: + /* treat any other address family as AF_UNSPEC */ + return 0; + } +} + +static bool routing_policy_rule_equal(const RoutingPolicyRule *rule1, const RoutingPolicyRule *rule2) { + if (rule1 == rule2) + return true; + + if (!rule1 || !rule2) + return false; + + return routing_policy_rule_compare_func(rule1, rule2) == 0; +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + routing_policy_rule_hash_ops, + RoutingPolicyRule, + routing_policy_rule_hash_func, + routing_policy_rule_compare_func, + routing_policy_rule_free); + +static int routing_policy_rule_get(Manager *m, const RoutingPolicyRule *in, RoutingPolicyRule **ret) { + RoutingPolicyRule *rule; + + assert(m); + assert(in); + + rule = set_get(m->rules, in); + if (rule) { + if (ret) + *ret = rule; + return 0; + } + + if (in->priority_set) + return -ENOENT; + + /* Also find rules configured without priority. */ + SET_FOREACH(rule, m->rules) { + uint32_t priority; + bool found; + + if (rule->priority_set) + /* The rule is configured with priority. */ + continue; + + priority = rule->priority; + rule->priority = 0; + found = routing_policy_rule_equal(rule, in); + rule->priority = priority; + + if (found) { + if (ret) + *ret = rule; + return 0; + } + } + + return -ENOENT; +} + +static int routing_policy_rule_add(Manager *m, RoutingPolicyRule *rule) { + int r; + + assert(m); + assert(rule); + assert(IN_SET(rule->family, AF_INET, AF_INET6)); + + r = set_ensure_put(&m->rules, &routing_policy_rule_hash_ops, rule); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + rule->manager = m; + return 0; +} + +static int routing_policy_rule_acquire_priority(Manager *manager, RoutingPolicyRule *rule) { + _cleanup_set_free_ Set *priorities = NULL; + RoutingPolicyRule *tmp; + uint32_t priority; + Network *network; + int r; + + assert(manager); + assert(rule); + assert(IN_SET(rule->family, AF_INET, AF_INET6)); + + if (rule->priority_set) + return 0; + + /* Find the highest unused priority. Note that 32766 is already used by kernel. + * See kernel_rules[] below. */ + + SET_FOREACH(tmp, manager->rules) { + if (tmp->family != rule->family) + continue; + if (tmp->priority == 0 || tmp->priority > 32765) + continue; + r = set_ensure_put(&priorities, NULL, UINT32_TO_PTR(tmp->priority)); + if (r < 0) + return r; + } + + ORDERED_HASHMAP_FOREACH(network, manager->networks) + HASHMAP_FOREACH(tmp, network->rules_by_section) { + if (tmp->family != AF_UNSPEC && tmp->family != rule->family) + continue; + if (!tmp->priority_set) + continue; + if (tmp->priority == 0 || tmp->priority > 32765) + continue; + r = set_ensure_put(&priorities, NULL, UINT32_TO_PTR(tmp->priority)); + if (r < 0) + return r; + } + + for (priority = 32765; priority > 0; priority--) + if (!set_contains(priorities, UINT32_TO_PTR(priority))) + break; + + rule->priority = priority; + return 0; +} + +static void log_routing_policy_rule_debug(const RoutingPolicyRule *rule, const char *str, const Link *link, const Manager *m) { + _cleanup_free_ char *state = NULL, *table = NULL; + + assert(rule); + assert(IN_SET(rule->family, AF_INET, AF_INET6)); + assert(str); + assert(m); + + /* link may be NULL. */ + + if (!DEBUG_LOGGING) + return; + + (void) network_config_state_to_string_alloc(rule->state, &state); + (void) manager_get_route_table_to_string(m, rule->table, /* append_num = */ true, &table); + + log_link_debug(link, + "%s %s routing policy rule (%s): priority: %"PRIu32", %s -> %s, iif: %s, oif: %s, table: %s", + str, strna(network_config_source_to_string(rule->source)), strna(state), + rule->priority, + IN_ADDR_PREFIX_TO_STRING(rule->family, &rule->from, rule->from_prefixlen), + IN_ADDR_PREFIX_TO_STRING(rule->family, &rule->to, rule->to_prefixlen), + strna(rule->iif), strna(rule->oif), strna(table)); +} + +static int routing_policy_rule_set_netlink_message(const RoutingPolicyRule *rule, sd_netlink_message *m, Link *link) { + int r; + + assert(rule); + assert(m); + + /* link may be NULL. */ + + if (rule->from_prefixlen > 0) { + r = netlink_message_append_in_addr_union(m, FRA_SRC, rule->family, &rule->from); + if (r < 0) + return r; + + r = sd_rtnl_message_routing_policy_rule_set_fib_src_prefixlen(m, rule->from_prefixlen); + if (r < 0) + return r; + } + + if (rule->to_prefixlen > 0) { + r = netlink_message_append_in_addr_union(m, FRA_DST, rule->family, &rule->to); + if (r < 0) + return r; + + r = sd_rtnl_message_routing_policy_rule_set_fib_dst_prefixlen(m, rule->to_prefixlen); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u32(m, FRA_PRIORITY, rule->priority); + if (r < 0) + return r; + + if (rule->tos > 0) { + r = sd_rtnl_message_routing_policy_rule_set_tos(m, rule->tos); + if (r < 0) + return r; + } + + if (rule->table < 256) { + r = sd_rtnl_message_routing_policy_rule_set_table(m, rule->table); + if (r < 0) + return r; + } else { + r = sd_rtnl_message_routing_policy_rule_set_table(m, RT_TABLE_UNSPEC); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, FRA_TABLE, rule->table); + if (r < 0) + return r; + } + + if (rule->fwmark > 0) { + r = sd_netlink_message_append_u32(m, FRA_FWMARK, rule->fwmark); + if (r < 0) + return r; + + r = sd_netlink_message_append_u32(m, FRA_FWMASK, rule->fwmask); + if (r < 0) + return r; + } + + if (rule->iif) { + r = sd_netlink_message_append_string(m, FRA_IIFNAME, rule->iif); + if (r < 0) + return r; + } + + if (rule->oif) { + r = sd_netlink_message_append_string(m, FRA_OIFNAME, rule->oif); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u8(m, FRA_IP_PROTO, rule->ipproto); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(m, FRA_PROTOCOL, rule->protocol); + if (r < 0) + return r; + + if (rule->sport.start != 0 || rule->sport.end != 0) { + r = sd_netlink_message_append_data(m, FRA_SPORT_RANGE, &rule->sport, sizeof(rule->sport)); + if (r < 0) + return r; + } + + if (rule->dport.start != 0 || rule->dport.end != 0) { + r = sd_netlink_message_append_data(m, FRA_DPORT_RANGE, &rule->dport, sizeof(rule->dport)); + if (r < 0) + return r; + } + + if (rule->uid_range.start != UID_INVALID && rule->uid_range.end != UID_INVALID) { + r = sd_netlink_message_append_data(m, FRA_UID_RANGE, &rule->uid_range, sizeof(rule->uid_range)); + if (r < 0) + return r; + } + + if (rule->invert_rule) { + r = sd_rtnl_message_routing_policy_rule_set_flags(m, FIB_RULE_INVERT); + if (r < 0) + return r; + } + + if (rule->suppress_prefixlen >= 0) { + r = sd_netlink_message_append_u32(m, FRA_SUPPRESS_PREFIXLEN, (uint32_t) rule->suppress_prefixlen); + if (r < 0) + return r; + } + + if (rule->suppress_ifgroup >= 0) { + r = sd_netlink_message_append_u32(m, FRA_SUPPRESS_IFGROUP, (uint32_t) rule->suppress_ifgroup); + if (r < 0) + return r; + } + + r = sd_rtnl_message_routing_policy_rule_set_fib_type(m, rule->type); + if (r < 0) + return r; + + return 0; +} + +static int routing_policy_rule_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, void *userdata) { + int r; + + assert(m); + + r = sd_netlink_message_get_errno(m); + if (r < 0) + log_message_warning_errno(m, r, "Could not drop routing policy rule"); + + return 1; +} + +static int routing_policy_rule_remove(RoutingPolicyRule *rule) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(rule); + assert(rule->manager); + assert(rule->manager->rtnl); + assert(IN_SET(rule->family, AF_INET, AF_INET6)); + + log_routing_policy_rule_debug(rule, "Removing", NULL, rule->manager); + + r = sd_rtnl_message_new_routing_policy_rule(rule->manager->rtnl, &m, RTM_DELRULE, rule->family); + if (r < 0) + return log_warning_errno(r, "Could not allocate netlink message: %m"); + + r = routing_policy_rule_set_netlink_message(rule, m, NULL); + if (r < 0) + return log_warning_errno(r, "Could not create netlink message: %m"); + + r = netlink_call_async(rule->manager->rtnl, NULL, m, + routing_policy_rule_remove_handler, + NULL, NULL); + if (r < 0) + return log_warning_errno(r, "Could not send netlink message: %m"); + + routing_policy_rule_enter_removing(rule); + return 0; +} + +static int routing_policy_rule_configure(RoutingPolicyRule *rule, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(rule); + assert(IN_SET(rule->family, AF_INET, AF_INET6)); + assert(link); + assert(link->ifindex > 0); + assert(link->manager); + assert(link->manager->rtnl); + assert(req); + + log_routing_policy_rule_debug(rule, "Configuring", link, link->manager); + + r = sd_rtnl_message_new_routing_policy_rule(link->manager->rtnl, &m, RTM_NEWRULE, rule->family); + if (r < 0) + return r; + + r = routing_policy_rule_set_netlink_message(rule, m, link); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static void manager_mark_routing_policy_rules(Manager *m, bool foreign, const Link *except) { + RoutingPolicyRule *rule; + Link *link; + + assert(m); + + /* First, mark all existing rules. */ + SET_FOREACH(rule, m->rules) { + /* Do not touch rules managed by kernel. */ + if (rule->protocol == RTPROT_KERNEL) + continue; + + /* When 'foreign' is true, mark only foreign rules, and vice versa. */ + if (foreign != (rule->source == NETWORK_CONFIG_SOURCE_FOREIGN)) + continue; + + /* Ignore rules not assigned yet or already removing. */ + if (!routing_policy_rule_exists(rule)) + continue; + + routing_policy_rule_mark(rule); + } + + /* Then, unmark all rules requested by active links. */ + HASHMAP_FOREACH(link, m->links_by_index) { + if (link == except) + continue; + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + continue; + + HASHMAP_FOREACH(rule, link->network->rules_by_section) { + RoutingPolicyRule *existing; + + if (IN_SET(rule->family, AF_INET, AF_INET6)) { + if (routing_policy_rule_get(m, rule, &existing) >= 0) + routing_policy_rule_unmark(existing); + } else { + /* The case Family=both. */ + rule->family = AF_INET; + if (routing_policy_rule_get(m, rule, &existing) >= 0) + routing_policy_rule_unmark(existing); + + rule->family = AF_INET6; + if (routing_policy_rule_get(m, rule, &existing) >= 0) + routing_policy_rule_unmark(existing); + + rule->family = AF_UNSPEC; + } + } + } +} + +int manager_drop_routing_policy_rules_internal(Manager *m, bool foreign, const Link *except) { + RoutingPolicyRule *rule; + int r = 0; + + assert(m); + + manager_mark_routing_policy_rules(m, foreign, except); + + SET_FOREACH(rule, m->rules) { + if (!routing_policy_rule_is_marked(rule)) + continue; + + RET_GATHER(r, routing_policy_rule_remove(rule)); + } + + return r; +} + +void link_foreignize_routing_policy_rules(Link *link) { + RoutingPolicyRule *rule; + + assert(link); + assert(link->manager); + + manager_mark_routing_policy_rules(link->manager, /* foreign = */ false, link); + + SET_FOREACH(rule, link->manager->rules) { + if (!routing_policy_rule_is_marked(rule)) + continue; + + rule->source = NETWORK_CONFIG_SOURCE_FOREIGN; + } +} + +static int routing_policy_rule_process_request(Request *req, Link *link, RoutingPolicyRule *rule) { + int r; + + assert(req); + assert(link); + assert(rule); + + if (!link_is_ready_to_configure(link, false)) + return 0; + + r = routing_policy_rule_configure(rule, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure routing policy rule: %m"); + + routing_policy_rule_enter_configuring(rule); + return 1; +} + +static int static_routing_policy_rule_configure_handler( + sd_netlink *rtnl, + sd_netlink_message *m, + Request *req, + Link *link, + RoutingPolicyRule *rule) { + + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not add routing policy rule"); + link_enter_failed(link); + return 1; + } + + if (link->static_routing_policy_rule_messages == 0) { + log_link_debug(link, "Routing policy rule configured"); + link->static_routing_policy_rules_configured = true; + link_check_ready(link); + } + + return 1; +} + +static int link_request_routing_policy_rule(Link *link, RoutingPolicyRule *rule) { + RoutingPolicyRule *existing; + int r; + + assert(link); + assert(link->manager); + assert(rule); + assert(rule->source != NETWORK_CONFIG_SOURCE_FOREIGN); + + if (routing_policy_rule_get(link->manager, rule, &existing) < 0) { + _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *tmp = NULL; + + r = routing_policy_rule_dup(rule, &tmp); + if (r < 0) + return r; + + r = routing_policy_rule_acquire_priority(link->manager, tmp); + if (r < 0) + return r; + + r = routing_policy_rule_add(link->manager, tmp); + if (r < 0) + return r; + + existing = TAKE_PTR(tmp); + } else + existing->source = rule->source; + + log_routing_policy_rule_debug(existing, "Requesting", link, link->manager); + r = link_queue_request_safe(link, REQUEST_TYPE_ROUTING_POLICY_RULE, + existing, NULL, + routing_policy_rule_hash_func, + routing_policy_rule_compare_func, + routing_policy_rule_process_request, + &link->static_routing_policy_rule_messages, + static_routing_policy_rule_configure_handler, + NULL); + if (r <= 0) + return r; + + routing_policy_rule_enter_requesting(existing); + return 1; +} + +static int link_request_static_routing_policy_rule(Link *link, RoutingPolicyRule *rule) { + int r; + + if (IN_SET(rule->family, AF_INET, AF_INET6)) + return link_request_routing_policy_rule(link, rule); + + rule->family = AF_INET; + r = link_request_routing_policy_rule(link, rule); + if (r < 0) { + rule->family = AF_UNSPEC; + return r; + } + + rule->family = AF_INET6; + r = link_request_routing_policy_rule(link, rule); + rule->family = AF_UNSPEC; + return r; +} + +int link_request_static_routing_policy_rules(Link *link) { + RoutingPolicyRule *rule; + int r; + + assert(link); + assert(link->network); + + link->static_routing_policy_rules_configured = false; + + HASHMAP_FOREACH(rule, link->network->rules_by_section) { + r = link_request_static_routing_policy_rule(link, rule); + if (r < 0) + return log_link_warning_errno(link, r, "Could not request routing policy rule: %m"); + } + + if (link->static_routing_policy_rule_messages == 0) { + link->static_routing_policy_rules_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Requesting routing policy rules"); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} + +static const RoutingPolicyRule kernel_rules[] = { + { .family = AF_INET, .priority_set = true, .priority = 0, .table = RT_TABLE_LOCAL, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, }, + { .family = AF_INET, .priority_set = true, .priority = 32766, .table = RT_TABLE_MAIN, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, }, + { .family = AF_INET, .priority_set = true, .priority = 32767, .table = RT_TABLE_DEFAULT, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, }, + { .family = AF_INET6, .priority_set = true, .priority = 0, .table = RT_TABLE_LOCAL, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, }, + { .family = AF_INET6, .priority_set = true, .priority = 32766, .table = RT_TABLE_MAIN, .type = FR_ACT_TO_TBL, .uid_range.start = UID_INVALID, .uid_range.end = UID_INVALID, .suppress_prefixlen = -1, .suppress_ifgroup = -1, }, +}; + +static bool routing_policy_rule_is_created_by_kernel(const RoutingPolicyRule *rule) { + assert(rule); + + if (rule->l3mdev > 0) + /* Currently, [RoutingPolicyRule] does not explicitly set FRA_L3MDEV. So, if the flag + * is set, it is safe to treat the rule as created by kernel. */ + return true; + + for (size_t i = 0; i < ELEMENTSOF(kernel_rules); i++) + if (routing_policy_rule_equal(rule, &kernel_rules[i])) + return true; + + return false; +} + +int manager_rtnl_process_rule(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) { + _cleanup_(routing_policy_rule_freep) RoutingPolicyRule *tmp = NULL; + RoutingPolicyRule *rule = NULL; + bool adjust_protocol = false; + uint16_t type; + int r; + + assert(rtnl); + assert(message); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "rtnl: failed to receive rule message, ignoring"); + + return 0; + } + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get message type, ignoring: %m"); + return 0; + } else if (!IN_SET(type, RTM_NEWRULE, RTM_DELRULE)) { + log_warning("rtnl: received unexpected message type %u when processing rule, ignoring.", type); + return 0; + } + + r = routing_policy_rule_new(&tmp); + if (r < 0) { + log_oom(); + return 0; + } + + r = sd_rtnl_message_get_family(message, &tmp->family); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get rule family, ignoring: %m"); + return 0; + } else if (!IN_SET(tmp->family, AF_INET, AF_INET6)) { + log_debug("rtnl: received rule message with invalid family %d, ignoring.", tmp->family); + return 0; + } + + r = netlink_message_read_in_addr_union(message, FRA_SRC, tmp->family, &tmp->from); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_SRC attribute, ignoring: %m"); + return 0; + } else if (r >= 0) { + r = sd_rtnl_message_routing_policy_rule_get_fib_src_prefixlen(message, &tmp->from_prefixlen); + if (r < 0) { + log_warning_errno(r, "rtnl: received rule message without valid source prefix length, ignoring: %m"); + return 0; + } + } + + r = netlink_message_read_in_addr_union(message, FRA_DST, tmp->family, &tmp->to); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_DST attribute, ignoring: %m"); + return 0; + } else if (r >= 0) { + r = sd_rtnl_message_routing_policy_rule_get_fib_dst_prefixlen(message, &tmp->to_prefixlen); + if (r < 0) { + log_warning_errno(r, "rtnl: received rule message without valid destination prefix length, ignoring: %m"); + return 0; + } + } + + unsigned flags; + r = sd_rtnl_message_routing_policy_rule_get_flags(message, &flags); + if (r < 0) { + log_warning_errno(r, "rtnl: received rule message without valid flag, ignoring: %m"); + return 0; + } + tmp->invert_rule = flags & FIB_RULE_INVERT; + + r = sd_netlink_message_read_u32(message, FRA_FWMARK, &tmp->fwmark); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_FWMARK attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, FRA_FWMASK, &tmp->fwmask); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_FWMASK attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, FRA_PRIORITY, &tmp->priority); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_PRIORITY attribute, ignoring: %m"); + return 0; + } + /* The kernel does not send priority if priority is zero. So, the flag below must be always set + * even if the message does not contain FRA_PRIORITY. */ + tmp->priority_set = true; + + r = sd_netlink_message_read_u32(message, FRA_TABLE, &tmp->table); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_TABLE attribute, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_routing_policy_rule_get_tos(message, &tmp->tos); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FIB rule TOS, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_routing_policy_rule_get_fib_type(message, &tmp->type); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FIB rule type, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_string_strdup(message, FRA_IIFNAME, &tmp->iif); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_IIFNAME attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_string_strdup(message, FRA_OIFNAME, &tmp->oif); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_OIFNAME attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u8(message, FRA_IP_PROTO, &tmp->ipproto); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_IP_PROTO attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u8(message, FRA_PROTOCOL, &tmp->protocol); + if (r == -ENODATA) + /* If FRA_PROTOCOL is supported by kernel, then the attribute is always appended. + * When the received message does not have FRA_PROTOCOL, then we need to adjust the + * protocol of the rule later. */ + adjust_protocol = true; + else if (r < 0) { + log_warning_errno(r, "rtnl: could not get FRA_PROTOCOL attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u8(message, FRA_L3MDEV, &tmp->l3mdev); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_L3MDEV attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read(message, FRA_SPORT_RANGE, sizeof(tmp->sport), &tmp->sport); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_SPORT_RANGE attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read(message, FRA_DPORT_RANGE, sizeof(tmp->dport), &tmp->dport); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_DPORT_RANGE attribute, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read(message, FRA_UID_RANGE, sizeof(tmp->uid_range), &tmp->uid_range); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_UID_RANGE attribute, ignoring: %m"); + return 0; + } + + uint32_t suppress_prefixlen; + r = sd_netlink_message_read_u32(message, FRA_SUPPRESS_PREFIXLEN, &suppress_prefixlen); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_SUPPRESS_PREFIXLEN attribute, ignoring: %m"); + return 0; + } + if (r >= 0) + tmp->suppress_prefixlen = (int32_t) suppress_prefixlen; + + uint32_t suppress_ifgroup; + r = sd_netlink_message_read_u32(message, FRA_SUPPRESS_IFGROUP, &suppress_ifgroup); + if (r < 0 && r != -ENODATA) { + log_warning_errno(r, "rtnl: could not get FRA_SUPPRESS_IFGROUP attribute, ignoring: %m"); + return 0; + } + if (r >= 0) + tmp->suppress_ifgroup = (int32_t) suppress_ifgroup; + + if (adjust_protocol) + /* As .network files does not have setting to specify protocol, we can assume the + * protocol of the received rule is RTPROT_KERNEL or RTPROT_STATIC. */ + tmp->protocol = routing_policy_rule_is_created_by_kernel(tmp) ? RTPROT_KERNEL : RTPROT_STATIC; + + (void) routing_policy_rule_get(m, tmp, &rule); + + switch (type) { + case RTM_NEWRULE: + if (rule) { + routing_policy_rule_enter_configured(rule); + log_routing_policy_rule_debug(rule, "Received remembered", NULL, m); + } else if (!m->manage_foreign_rules) { + routing_policy_rule_enter_configured(tmp); + log_routing_policy_rule_debug(tmp, "Ignoring received", NULL, m); + } else { + routing_policy_rule_enter_configured(tmp); + log_routing_policy_rule_debug(tmp, "Remembering", NULL, m); + r = routing_policy_rule_add(m, tmp); + if (r < 0) { + log_warning_errno(r, "Could not remember foreign rule, ignoring: %m"); + return 0; + } + TAKE_PTR(tmp); + } + break; + case RTM_DELRULE: + if (rule) { + routing_policy_rule_enter_removed(rule); + if (rule->state == 0) { + log_routing_policy_rule_debug(rule, "Forgetting", NULL, m); + routing_policy_rule_free(rule); + } else + log_routing_policy_rule_debug(rule, "Removed", NULL, m); + } else + log_routing_policy_rule_debug(tmp, "Kernel removed unknown", NULL, m); + break; + + default: + assert_not_reached(); + } + + return 1; +} + +static int parse_fwmark_fwmask(const char *s, uint32_t *ret_fwmark, uint32_t *ret_fwmask) { + _cleanup_free_ char *fwmark_str = NULL; + uint32_t fwmark, fwmask = 0; + const char *slash; + int r; + + assert(s); + assert(ret_fwmark); + assert(ret_fwmask); + + slash = strchr(s, '/'); + if (slash) { + fwmark_str = strndup(s, slash - s); + if (!fwmark_str) + return -ENOMEM; + } + + r = safe_atou32(fwmark_str ?: s, &fwmark); + if (r < 0) + return r; + + if (fwmark > 0) { + if (slash) { + r = safe_atou32(slash + 1, &fwmask); + if (r < 0) + return r; + } else + fwmask = UINT32_MAX; + } + + *ret_fwmark = fwmark; + *ret_fwmask = fwmask; + + return 0; +} + +int config_parse_routing_policy_rule_tos( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = safe_atou8(rvalue, &n->tos); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule TOS, ignoring: %s", rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_priority( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + n->priority = 0; + n->priority_set = false; + TAKE_PTR(n); + return 0; + } + + r = safe_atou32(rvalue, &n->priority); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule priority, ignoring: %s", rvalue); + return 0; + } + n->priority_set = true; + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_table( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = manager_get_route_table_from_string(network->manager, rvalue, &n->table); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse RPDB rule route table \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_fwmark_mask( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = parse_fwmark_fwmask(rvalue, &n->fwmark, &n->fwmask); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule firewall mark or mask, ignoring: %s", rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_prefix( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + union in_addr_union *buffer; + uint8_t *prefixlen; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (streq(lvalue, "To")) { + buffer = &n->to; + prefixlen = &n->to_prefixlen; + } else { + buffer = &n->from; + prefixlen = &n->from_prefixlen; + } + + if (n->family == AF_UNSPEC) + r = in_addr_prefix_from_string_auto(rvalue, &n->family, buffer, prefixlen); + else + r = in_addr_prefix_from_string(rvalue, n->family, buffer, prefixlen); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "RPDB rule prefix is invalid, ignoring assignment: %s", rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_device( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (!ifname_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid interface name '%s' in %s=, ignoring assignment.", rvalue, lvalue); + return 0; + } + + r = free_and_strdup(streq(lvalue, "IncomingInterface") ? &n->iif : &n->oif, rvalue); + if (r < 0) + return log_oom(); + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_port_range( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + uint16_t low, high; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = parse_ip_port_range(rvalue, &low, &high); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse routing policy rule port range '%s'", rvalue); + return 0; + } + + if (streq(lvalue, "SourcePort")) { + n->sport.start = low; + n->sport.end = high; + } else { + n->dport.start = low; + n->dport.end = high; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_ip_protocol( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = parse_ip_protocol(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse IP protocol '%s' for routing policy rule, ignoring: %m", rvalue); + return 0; + } + + n->ipproto = r; + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_invert( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = parse_boolean(rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule invert, ignoring: %s", rvalue); + return 0; + } + + n->invert_rule = r; + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_family( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + AddressFamily a; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + a = routing_policy_rule_address_family_from_string(rvalue); + if (a < 0) { + log_syntax(unit, LOG_WARNING, filename, line, a, + "Invalid address family '%s', ignoring.", rvalue); + return 0; + } + + n->address_family = a; + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_uid_range( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + uid_t start, end; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = get_user_creds(&rvalue, &start, NULL, NULL, NULL, 0); + if (r >= 0) + end = start; + else { + r = parse_uid_range(rvalue, &start, &end); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid uid or uid range '%s', ignoring: %m", rvalue); + return 0; + } + } + + n->uid_range.start = start; + n->uid_range.end = end; + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_suppress_prefixlen( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + r = parse_ip_prefix_length(rvalue, &n->suppress_prefixlen); + if (r == -ERANGE) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Prefix length outside of valid range 0-128, ignoring: %s", rvalue); + return 0; + } + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse RPDB rule suppress_prefixlen, ignoring: %s", rvalue); + return 0; + } + + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_suppress_ifgroup( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int32_t suppress_ifgroup; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + n->suppress_ifgroup = -1; + return 0; + } + + r = safe_atoi32(rvalue, &suppress_ifgroup); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse SuppressInterfaceGroup=, ignoring assignment: %s", rvalue); + return 0; + } + if (suppress_ifgroup < 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Value of SuppressInterfaceGroup= must be in the range 0…2147483647, ignoring assignment: %s", rvalue); + return 0; + } + n->suppress_ifgroup = suppress_ifgroup; + TAKE_PTR(n); + return 0; +} + +int config_parse_routing_policy_rule_type( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(routing_policy_rule_free_or_set_invalidp) RoutingPolicyRule *n = NULL; + Network *network = userdata; + int r, t; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = routing_policy_rule_new_static(network, filename, section_line, &n); + if (r < 0) + return log_oom(); + + t = fr_act_type_from_string(rvalue); + if (t < 0) { + log_syntax(unit, LOG_WARNING, filename, line, t, + "Could not parse FIB rule type \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + n->type = (uint8_t) t; + + TAKE_PTR(n); + return 0; +} + +static int routing_policy_rule_section_verify(RoutingPolicyRule *rule) { + if (section_is_invalid(rule->section)) + return -EINVAL; + + if ((rule->family == AF_INET && FLAGS_SET(rule->address_family, ADDRESS_FAMILY_IPV6)) || + (rule->family == AF_INET6 && FLAGS_SET(rule->address_family, ADDRESS_FAMILY_IPV4))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: address family specified by Family= conflicts with the address " + "specified by To= or From=. Ignoring [RoutingPolicyRule] section from line %u.", + rule->section->filename, rule->section->line); + + if (rule->family == AF_UNSPEC) { + if (IN_SET(rule->address_family, ADDRESS_FAMILY_IPV4, ADDRESS_FAMILY_NO)) + rule->family = AF_INET; + else if (rule->address_family == ADDRESS_FAMILY_IPV6) + rule->family = AF_INET6; + /* rule->family can be AF_UNSPEC only when Family=both. */ + } + + /* Currently, [RoutingPolicyRule] does not have a setting to set FRA_L3MDEV flag. Please also + * update routing_policy_rule_is_created_by_kernel() when a new setting which sets the flag is + * added in the future. */ + if (rule->l3mdev > 0) + assert_not_reached(); + + return 0; +} + +void network_drop_invalid_routing_policy_rules(Network *network) { + RoutingPolicyRule *rule; + + assert(network); + + HASHMAP_FOREACH(rule, network->rules_by_section) + if (routing_policy_rule_section_verify(rule) < 0) + routing_policy_rule_free(rule); +} diff --git a/src/network/networkd-routing-policy-rule.h b/src/network/networkd-routing-policy-rule.h new file mode 100644 index 0000000..b6ce2fa --- /dev/null +++ b/src/network/networkd-routing-policy-rule.h @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <linux/fib_rules.h> +#include <stdbool.h> + +#include "conf-parser.h" +#include "in-addr-util.h" +#include "networkd-util.h" + +typedef struct Link Link; +typedef struct Manager Manager; +typedef struct Network Network; + +typedef struct RoutingPolicyRule { + Manager *manager; + Network *network; + ConfigSection *section; + NetworkConfigSource source; + NetworkConfigState state; + + bool invert_rule; + bool priority_set; + + uint8_t tos; + uint8_t type; + uint8_t ipproto; /* FRA_IP_PROTO */ + uint8_t protocol; /* FRA_PROTOCOL */ + uint8_t to_prefixlen; + uint8_t from_prefixlen; + uint8_t l3mdev; /* FRA_L3MDEV */ + + uint32_t table; + uint32_t fwmark; + uint32_t fwmask; + uint32_t priority; + + AddressFamily address_family; /* Specified by Family= */ + int family; /* Automatically determined by From= or To= */ + + char *iif; + char *oif; + + union in_addr_union to; + union in_addr_union from; + + struct fib_rule_port_range sport; + struct fib_rule_port_range dport; + struct fib_rule_uid_range uid_range; + + int suppress_prefixlen; + int32_t suppress_ifgroup; +} RoutingPolicyRule; + +const char *fr_act_type_full_to_string(int t) _const_; + +RoutingPolicyRule *routing_policy_rule_free(RoutingPolicyRule *rule); + +void network_drop_invalid_routing_policy_rules(Network *network); + +int link_request_static_routing_policy_rules(Link *link); + +int manager_rtnl_process_rule(sd_netlink *rtnl, sd_netlink_message *message, Manager *m); +int manager_drop_routing_policy_rules_internal(Manager *m, bool foreign, const Link *except); +static inline int manager_drop_foreign_routing_policy_rules(Manager *m) { + return manager_drop_routing_policy_rules_internal(m, true, NULL); +} +static inline int link_drop_managed_routing_policy_rules(Link *link) { + assert(link); + return manager_drop_routing_policy_rules_internal(link->manager, false, link); +} +void link_foreignize_routing_policy_rules(Link *link); + +DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(RoutingPolicyRule, routing_policy_rule); + +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_tos); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_table); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_fwmark_mask); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_prefix); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_priority); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_device); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_port_range); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_ip_protocol); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_invert); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_family); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_uid_range); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_suppress_prefixlen); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_suppress_ifgroup); +CONFIG_PARSER_PROTOTYPE(config_parse_routing_policy_rule_type); diff --git a/src/network/networkd-setlink.c b/src/network/networkd-setlink.c new file mode 100644 index 0000000..011ea1f --- /dev/null +++ b/src/network/networkd-setlink.c @@ -0,0 +1,1309 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/if.h> +#include <linux/if_arp.h> +#include <linux/if_bridge.h> + +#include "missing_network.h" +#include "netif-util.h" +#include "netlink-util.h" +#include "networkd-address.h" +#include "networkd-can.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-queue.h" +#include "networkd-setlink.h" +#include "networkd-sriov.h" +#include "networkd-wiphy.h" + +static int get_link_default_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + return link_getlink_handler_internal(rtnl, m, link, "Failed to sync link information"); +} + +static int get_link_master_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + if (get_link_default_handler(rtnl, m, link) > 0) + link->master_set = true; + return 0; +} + +static int get_link_update_flag_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + assert(link); + assert(link->set_flags_messages > 0); + + link->set_flags_messages--; + + return get_link_default_handler(rtnl, m, link); +} + +static int set_link_handler_internal( + sd_netlink *rtnl, + sd_netlink_message *m, + Request *req, + Link *link, + bool ignore, + link_netlink_message_handler_t get_link_handler) { + + int r; + + assert(m); + assert(req); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + const char *error_msg; + + error_msg = strjoina("Failed to set ", request_type_to_string(req->type), ignore ? ", ignoring" : ""); + log_link_message_warning_errno(link, m, r, error_msg); + + if (!ignore) + link_enter_failed(link); + return 0; + } + + log_link_debug(link, "%s set.", request_type_to_string(req->type)); + + if (get_link_handler) { + r = link_call_getlink(link, get_link_handler); + if (r < 0) { + link_enter_failed(link); + return 0; + } + } + + if (link->set_link_messages == 0) + link_check_ready(link); + + return 1; +} + +static int link_set_addrgen_mode_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + int r; + + r = set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, NULL); + if (r <= 0) + return r; + + r = link_drop_ipv6ll_addresses(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to drop IPv6LL addresses: %m"); + link_enter_failed(link); + } + + return 0; +} + +static int link_set_bond_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, NULL); +} + +static int link_set_bridge_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, NULL); +} + +static int link_set_bridge_vlan_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, NULL); +} + +static int link_set_can_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, NULL); +} + +static int link_set_flags_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, get_link_default_handler); +} + +static int link_set_group_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, NULL); +} + +static int link_set_ipoib_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, NULL); +} + +static int link_set_mac_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, get_link_default_handler); +} + +static int link_set_mac_allow_retry_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r == -EBUSY) { + /* Most real network devices refuse to set its hardware address with -EBUSY when its + * operstate is not down. See, eth_prepare_mac_addr_change() in net/ethernet/eth.c + * of kernel. */ + + log_link_message_debug_errno(link, m, r, "Failed to set MAC address, retrying again: %m"); + + r = link_request_to_set_mac(link, /* allow_retry = */ false); + if (r < 0) + link_enter_failed(link); + + return 0; + } + + return link_set_mac_handler(rtnl, m, req, link, userdata); +} + +static int link_set_master_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ false, get_link_master_handler); +} + +static int link_unset_master_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + /* Some devices do not support setting master ifindex. Let's ignore error on unsetting master ifindex. */ + return set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, get_link_master_handler); +} + +static int link_set_mtu_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + int r; + + r = set_link_handler_internal(rtnl, m, req, link, /* ignore = */ true, get_link_default_handler); + if (r <= 0) + return r; + + /* The kernel resets ipv6 mtu after changing device mtu; + * we must set this here, after we've set device mtu */ + r = link_set_ipv6_mtu(link); + if (r < 0) + log_link_warning_errno(link, r, "Failed to set IPv6 MTU, ignoring: %m"); + + return 0; +} + +static int link_configure_fill_message( + Link *link, + sd_netlink_message *req, + RequestType type, + void *userdata) { + int r; + + switch (type) { + case REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE: + r = ipv6ll_addrgen_mode_fill_message(req, PTR_TO_UINT8(userdata)); + if (r < 0) + return r; + break; + case REQUEST_TYPE_SET_LINK_BOND: + r = sd_netlink_message_set_flags(req, NLM_F_REQUEST | NLM_F_ACK); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(req, IFLA_LINKINFO); + if (r < 0) + return r; + + r = sd_netlink_message_open_container_union(req, IFLA_INFO_DATA, "bond"); + if (r < 0) + return r; + + if (link->network->active_slave) { + r = sd_netlink_message_append_u32(req, IFLA_BOND_ACTIVE_SLAVE, link->ifindex); + if (r < 0) + return r; + } + + if (link->network->primary_slave) { + r = sd_netlink_message_append_u32(req, IFLA_BOND_PRIMARY, link->ifindex); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + break; + case REQUEST_TYPE_SET_LINK_BRIDGE: + r = sd_rtnl_message_link_set_family(req, AF_BRIDGE); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(req, IFLA_PROTINFO); + if (r < 0) + return r; + + if (link->network->use_bpdu >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_GUARD, !link->network->use_bpdu); + if (r < 0) + return r; + } + + if (link->network->hairpin >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MODE, link->network->hairpin); + if (r < 0) + return r; + } + + if (link->network->isolated >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_ISOLATED, link->network->isolated); + if (r < 0) + return r; + } + + if (link->network->fast_leave >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_FAST_LEAVE, link->network->fast_leave); + if (r < 0) + return r; + } + + if (link->network->allow_port_to_be_root >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_PROTECT, !link->network->allow_port_to_be_root); + if (r < 0) + return r; + } + + if (link->network->unicast_flood >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_UNICAST_FLOOD, link->network->unicast_flood); + if (r < 0) + return r; + } + + if (link->network->multicast_flood >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MCAST_FLOOD, link->network->multicast_flood); + if (r < 0) + return r; + } + + if (link->network->multicast_to_unicast >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MCAST_TO_UCAST, link->network->multicast_to_unicast); + if (r < 0) + return r; + } + + if (link->network->neighbor_suppression >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_NEIGH_SUPPRESS, link->network->neighbor_suppression); + if (r < 0) + return r; + } + + if (link->network->learning >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_LEARNING, link->network->learning); + if (r < 0) + return r; + } + + if (link->network->bridge_proxy_arp >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_PROXYARP, link->network->bridge_proxy_arp); + if (r < 0) + return r; + } + + if (link->network->bridge_proxy_arp_wifi >= 0) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_PROXYARP_WIFI, link->network->bridge_proxy_arp_wifi); + if (r < 0) + return r; + } + + if (link->network->cost != 0) { + r = sd_netlink_message_append_u32(req, IFLA_BRPORT_COST, link->network->cost); + if (r < 0) + return r; + } + + if (link->network->priority != LINK_BRIDGE_PORT_PRIORITY_INVALID) { + r = sd_netlink_message_append_u16(req, IFLA_BRPORT_PRIORITY, link->network->priority); + if (r < 0) + return r; + } + + if (link->network->multicast_router != _MULTICAST_ROUTER_INVALID) { + r = sd_netlink_message_append_u8(req, IFLA_BRPORT_MULTICAST_ROUTER, link->network->multicast_router); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + break; + case REQUEST_TYPE_SET_LINK_BRIDGE_VLAN: + r = sd_rtnl_message_link_set_family(req, AF_BRIDGE); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(req, IFLA_AF_SPEC); + if (r < 0) + return r; + + if (link->master_ifindex <= 0) { + /* master needs BRIDGE_FLAGS_SELF flag */ + r = sd_netlink_message_append_u16(req, IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF); + if (r < 0) + return r; + } + + r = bridge_vlan_append_info(link, req, link->network->pvid, link->network->br_vid_bitmap, link->network->br_untagged_bitmap); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + break; + case REQUEST_TYPE_SET_LINK_CAN: + r = can_set_netlink_message(link, req); + if (r < 0) + return r; + break; + case REQUEST_TYPE_SET_LINK_FLAGS: { + unsigned ifi_change = 0, ifi_flags = 0; + + if (link->network->arp >= 0) { + ifi_change |= IFF_NOARP; + SET_FLAG(ifi_flags, IFF_NOARP, link->network->arp == 0); + } + + if (link->network->multicast >= 0) { + ifi_change |= IFF_MULTICAST; + SET_FLAG(ifi_flags, IFF_MULTICAST, link->network->multicast); + } + + if (link->network->allmulticast >= 0) { + ifi_change |= IFF_ALLMULTI; + SET_FLAG(ifi_flags, IFF_ALLMULTI, link->network->allmulticast); + } + + if (link->network->promiscuous >= 0) { + ifi_change |= IFF_PROMISC; + SET_FLAG(ifi_flags, IFF_PROMISC, link->network->promiscuous); + } + + r = sd_rtnl_message_link_set_flags(req, ifi_flags, ifi_change); + if (r < 0) + return r; + + break; + } + case REQUEST_TYPE_SET_LINK_GROUP: + r = sd_netlink_message_append_u32(req, IFLA_GROUP, (uint32_t) link->network->group); + if (r < 0) + return r; + break; + case REQUEST_TYPE_SET_LINK_MAC: + r = netlink_message_append_hw_addr(req, IFLA_ADDRESS, &link->requested_hw_addr); + if (r < 0) + return r; + break; + case REQUEST_TYPE_SET_LINK_IPOIB: + r = ipoib_set_netlink_message(link, req); + if (r < 0) + return r; + break; + case REQUEST_TYPE_SET_LINK_MASTER: + r = sd_netlink_message_append_u32(req, IFLA_MASTER, PTR_TO_UINT32(userdata)); + if (r < 0) + return r; + break; + case REQUEST_TYPE_SET_LINK_MTU: + r = sd_netlink_message_append_u32(req, IFLA_MTU, PTR_TO_UINT32(userdata)); + if (r < 0) + return r; + break; + default: + assert_not_reached(); + } + + return 0; +} + +static int link_configure(Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(link); + assert(link->manager); + assert(req); + + log_link_debug(link, "Setting %s", request_type_to_string(req->type)); + + if (req->type == REQUEST_TYPE_SET_LINK_BOND) + r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_NEWLINK, link->master_ifindex); + else if (IN_SET(req->type, REQUEST_TYPE_SET_LINK_CAN, REQUEST_TYPE_SET_LINK_IPOIB)) + r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_NEWLINK, link->ifindex); + else + r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_SETLINK, link->ifindex); + if (r < 0) + return r; + + r = link_configure_fill_message(link, m, req->type, req->userdata); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static bool netdev_is_ready(NetDev *netdev) { + assert(netdev); + + if (netdev->state != NETDEV_STATE_READY) + return false; + if (netdev->ifindex == 0) + return false; + + return true; +} + +static int link_is_ready_to_set_link(Link *link, Request *req) { + int r; + + assert(link); + assert(link->manager); + assert(link->network); + assert(req); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return false; + + switch (req->type) { + case REQUEST_TYPE_SET_LINK_BOND: + case REQUEST_TYPE_SET_LINK_BRIDGE: + if (!link->master_set) + return false; + + if (link->network->keep_master && link->master_ifindex <= 0) + return false; + break; + + case REQUEST_TYPE_SET_LINK_BRIDGE_VLAN: + if (!link->master_set) + return false; + + if (link->network->keep_master && link->master_ifindex <= 0 && !streq_ptr(link->kind, "bridge")) + return false; + + break; + + case REQUEST_TYPE_SET_LINK_CAN: + /* Do not check link->set_flags_messages here, as it is ok even if link->flags + * is outdated, and checking the counter causes a deadlock. */ + if (FLAGS_SET(link->flags, IFF_UP)) { + /* The CAN interface must be down to configure bitrate, etc... */ + r = link_down_now(link); + if (r < 0) + return r; + } + break; + + case REQUEST_TYPE_SET_LINK_MAC: + if (req->netlink_handler == link_set_mac_handler) { + /* This is the second attempt to set hardware address. On the first attempt + * req->netlink_handler points to link_set_mac_allow_retry_handler(). + * The first attempt failed as the interface was up. */ + r = link_down_now(link); + if (r < 0) + return r; + + /* If the kind of the link is "bond", we need + * set the slave link down as well. */ + if (streq_ptr(link->kind, "bond")) { + r = link_down_slave_links(link); + if (r < 0) + return r; + } + } + break; + + case REQUEST_TYPE_SET_LINK_MASTER: { + uint32_t m = 0; + Request req_mac = { + .link = link, + .type = REQUEST_TYPE_SET_LINK_MAC, + }; + + if (link->network->batadv) { + if (!netdev_is_ready(link->network->batadv)) + return false; + m = link->network->batadv->ifindex; + } else if (link->network->bond) { + if (ordered_set_contains(link->manager->request_queue, &req_mac)) + return false; + if (!netdev_is_ready(link->network->bond)) + return false; + m = link->network->bond->ifindex; + } else if (link->network->bridge) { + if (ordered_set_contains(link->manager->request_queue, &req_mac)) + return false; + if (!netdev_is_ready(link->network->bridge)) + return false; + m = link->network->bridge->ifindex; + } else if (link->network->vrf) { + if (!netdev_is_ready(link->network->vrf)) + return false; + m = link->network->vrf->ifindex; + } + + if (m == (uint32_t) link->master_ifindex) { + /* The requested master is already set. */ + link->master_set = true; + return -EALREADY; /* indicate to cancel the request. */ + } + + /* Do not check link->set_flags_messages here, as it is ok even if link->flags is outdated, + * and checking the counter causes a deadlock. */ + if (link->network->bond && FLAGS_SET(link->flags, IFF_UP)) { + /* link must be down when joining to bond master. */ + r = link_down_now(link); + if (r < 0) + return r; + } + + req->userdata = UINT32_TO_PTR(m); + break; + } + case REQUEST_TYPE_SET_LINK_MTU: { + if (ordered_set_contains(link->manager->request_queue, + &(const Request) { + .link = link, + .type = REQUEST_TYPE_SET_LINK_IPOIB, + })) + return false; + + /* Changing FD mode may affect MTU. */ + if (ordered_set_contains(link->manager->request_queue, + &(const Request) { + .link = link, + .type = REQUEST_TYPE_SET_LINK_CAN, + })) + return false; + } + default: + break; + } + + return true; +} + +static int link_process_set_link(Request *req, Link *link, void *userdata) { + int r; + + assert(req); + assert(link); + + r = link_is_ready_to_set_link(link, req); + if (r == -EALREADY) + return 1; /* Cancel the request. */ + if (r <= 0) + return r; + + r = link_configure(link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to set %s", request_type_to_string(req->type)); + + return 1; +} + +static int link_request_set_link( + Link *link, + RequestType type, + request_netlink_handler_t netlink_handler, + Request **ret) { + + Request *req; + int r; + + assert(link); + + r = link_queue_request_full(link, type, NULL, NULL, NULL, NULL, + link_process_set_link, + &link->set_link_messages, + netlink_handler, + &req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request to set %s: %m", + request_type_to_string(type)); + + log_link_debug(link, "Requested to set %s", request_type_to_string(type)); + + if (ret) + *ret = req; + return 0; +} + +int link_request_to_set_addrgen_mode(Link *link) { + IPv6LinkLocalAddressGenMode mode; + Request *req; + int r; + + assert(link); + assert(link->network); + + if (!socket_ipv6_is_supported()) + return 0; + + mode = link_get_ipv6ll_addrgen_mode(link); + + if (mode == link->ipv6ll_address_gen_mode) + return 0; + + /* If the link is already up, then changing the mode by netlink does not take effect until the + * link goes down. Hence, we need to reset the interface. However, setting the mode by sysctl + * does not need that. Let's use the sysctl interface when the link is already up. + * See also issue #22424. */ + if (mode != IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE && + FLAGS_SET(link->flags, IFF_UP)) { + r = link_set_ipv6ll_addrgen_mode(link, mode); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv6 address generation mode, ignoring: %m"); + + return 0; + } + + r = link_request_set_link(link, REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE, + link_set_addrgen_mode_handler, + &req); + if (r < 0) + return r; + + req->userdata = UINT8_TO_PTR(mode); + return 0; +} + +int link_request_to_set_bond(Link *link) { + assert(link); + assert(link->network); + + if (!link->network->bond) { + Link *master; + + if (!link->network->keep_master) + return 0; + + if (link_get_master(link, &master) < 0) + return 0; + + if (!streq_ptr(master->kind, "bond")) + return 0; + } + + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_BOND, + link_set_bond_handler, NULL); +} + +int link_request_to_set_bridge(Link *link) { + assert(link); + assert(link->network); + + if (!link->network->bridge) { + Link *master; + + if (!link->network->keep_master) + return 0; + + if (link_get_master(link, &master) < 0) + return 0; + + if (!streq_ptr(master->kind, "bridge")) + return 0; + } + + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_BRIDGE, + link_set_bridge_handler, + NULL); +} + +int link_request_to_set_bridge_vlan(Link *link) { + assert(link); + assert(link->network); + + if (!link->network->use_br_vlan) + return 0; + + if (!link->network->bridge && !streq_ptr(link->kind, "bridge")) { + Link *master; + + if (!link->network->keep_master) + return 0; + + if (link_get_master(link, &master) < 0) + return 0; + + if (!streq_ptr(master->kind, "bridge")) + return 0; + } + + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_BRIDGE_VLAN, + link_set_bridge_vlan_handler, + NULL); +} + +int link_request_to_set_can(Link *link) { + assert(link); + assert(link->network); + + if (link->iftype != ARPHRD_CAN) + return 0; + + if (!streq_ptr(link->kind, "can")) + return 0; + + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_CAN, + link_set_can_handler, + NULL); +} + +int link_request_to_set_flags(Link *link) { + assert(link); + assert(link->network); + + if (link->network->arp < 0 && + link->network->multicast < 0 && + link->network->allmulticast < 0 && + link->network->promiscuous < 0) + return 0; + + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_FLAGS, + link_set_flags_handler, + NULL); +} + +int link_request_to_set_group(Link *link) { + assert(link); + assert(link->network); + + if (link->network->group < 0) + return 0; + + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_GROUP, + link_set_group_handler, + NULL); +} + +int link_request_to_set_mac(Link *link, bool allow_retry) { + int r; + + assert(link); + assert(link->network); + + if (link->network->hw_addr.length == 0) + return 0; + + link->requested_hw_addr = link->network->hw_addr; + r = net_verify_hardware_address(link->ifname, /* is_static = */ true, + link->iftype, &link->hw_addr, &link->requested_hw_addr); + if (r < 0) + return r; + + if (hw_addr_equal(&link->hw_addr, &link->requested_hw_addr)) + return 0; + + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_MAC, + allow_retry ? link_set_mac_allow_retry_handler : link_set_mac_handler, + NULL); +} + +int link_request_to_set_ipoib(Link *link) { + assert(link); + assert(link->network); + + if (link->iftype != ARPHRD_INFINIBAND) + return 0; + + if (link->network->ipoib_mode < 0 && + link->network->ipoib_umcast < 0) + return 0; + + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_IPOIB, + link_set_ipoib_handler, + NULL); +} + +int link_request_to_set_master(Link *link) { + assert(link); + assert(link->network); + + if (link->network->keep_master) { + /* When KeepMaster=yes, BatmanAdvanced=, Bond=, Bridge=, and VRF= are ignored. */ + link->master_set = true; + return 0; + + } else if (link->network->batadv || link->network->bond || link->network->bridge || link->network->vrf) { + link->master_set = false; + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_MASTER, + link_set_master_handler, + NULL); + + } else if (link->master_ifindex != 0) { + /* Unset master only when it is set. */ + link->master_set = false; + return link_request_set_link(link, REQUEST_TYPE_SET_LINK_MASTER, + link_unset_master_handler, + NULL); + + } else { + /* Nothing we need to do. */ + link->master_set = true; + return 0; + } +} + +int link_request_to_set_mtu(Link *link, uint32_t mtu) { + const char *origin; + uint32_t min_mtu, max_mtu; + Request *req; + int r; + + assert(link); + assert(link->network); + + min_mtu = link->min_mtu; + origin = "the minimum MTU of the interface"; + if (link_ipv6_enabled(link)) { + /* IPv6 protocol requires a minimum MTU of IPV6_MTU_MIN(1280) bytes on the interface. Bump up + * MTU bytes to IPV6_MTU_MIN. */ + if (min_mtu < IPV6_MIN_MTU) { + min_mtu = IPV6_MIN_MTU; + origin = "the minimum IPv6 MTU"; + } + if (min_mtu < link->network->ipv6_mtu) { + min_mtu = link->network->ipv6_mtu; + origin = "the requested IPv6 MTU in IPv6MTUBytes="; + } + } + + if (mtu < min_mtu) { + log_link_warning(link, "Bumping the requested MTU %"PRIu32" to %s (%"PRIu32")", + mtu, origin, min_mtu); + mtu = min_mtu; + } + + max_mtu = link->max_mtu; + if (link->iftype == ARPHRD_CAN) + /* The maximum MTU may be changed when FD mode is changed. + * See https://docs.kernel.org/networking/can.html#can-fd-flexible-data-rate-driver-support + * MTU = 16 (CAN_MTU) => Classical CAN device + * MTU = 72 (CANFD_MTU) => CAN FD capable device + * So, even if the current maximum is 16, we should not reduce the requested value now. */ + max_mtu = MAX(max_mtu, 72u); + + if (mtu > max_mtu) { + log_link_warning(link, "Reducing the requested MTU %"PRIu32" to the interface's maximum MTU %"PRIu32".", + mtu, max_mtu); + mtu = max_mtu; + } + + if (link->mtu == mtu) + return 0; + + r = link_request_set_link(link, REQUEST_TYPE_SET_LINK_MTU, + link_set_mtu_handler, + &req); + if (r < 0) + return r; + + req->userdata = UINT32_TO_PTR(mtu); + return 0; +} + +static bool link_reduces_vlan_mtu(Link *link) { + /* See netif_reduces_vlan_mtu() in kernel. */ + return streq_ptr(link->kind, "macsec"); +} + +static uint32_t link_get_requested_mtu_by_stacked_netdevs(Link *link) { + uint32_t mtu = 0; + NetDev *dev; + + HASHMAP_FOREACH(dev, link->network->stacked_netdevs) + if (dev->kind == NETDEV_KIND_VLAN && dev->mtu > 0) + /* See vlan_dev_change_mtu() in kernel. */ + mtu = MAX(mtu, link_reduces_vlan_mtu(link) ? dev->mtu + 4 : dev->mtu); + + else if (dev->kind == NETDEV_KIND_MACVLAN && dev->mtu > mtu) + /* See macvlan_change_mtu() in kernel. */ + mtu = dev->mtu; + + return mtu; +} + +int link_configure_mtu(Link *link) { + uint32_t mtu; + + assert(link); + assert(link->network); + + if (link->network->mtu > 0) + return link_request_to_set_mtu(link, link->network->mtu); + + mtu = link_get_requested_mtu_by_stacked_netdevs(link); + if (link->mtu >= mtu) + return 0; + + log_link_notice(link, "Bumping MTU bytes from %"PRIu32" to %"PRIu32" because of stacked device. " + "If it is not desired, then please explicitly specify MTUBytes= setting.", + link->mtu, mtu); + + return link_request_to_set_mtu(link, mtu); +} + +static int link_up_dsa_slave(Link *link) { + Link *master; + int r; + + assert(link); + + /* For older kernels (specifically, older than 9d5ef190e5615a7b63af89f88c4106a5bc127974, kernel-5.12), + * it is necessary to bring up a DSA slave that its master interface is already up. And bringing up + * the slave fails with -ENETDOWN. So, let's bring up the master even if it is not managed by us, + * and try to bring up the slave after the master becomes up. */ + + if (link->dsa_master_ifindex <= 0) + return 0; + + if (!streq_ptr(link->driver, "dsa")) + return 0; + + if (link_get_by_index(link->manager, link->dsa_master_ifindex, &master) < 0) + return 0; + + if (master->state == LINK_STATE_UNMANAGED) { + /* If the DSA master interface is unmanaged, then it will never become up. + * Let's request to bring up the master. */ + r = link_request_to_bring_up_or_down(master, /* up = */ true); + if (r < 0) + return r; + } + + r = link_request_to_bring_up_or_down(link, /* up = */ true); + if (r < 0) + return r; + + return 1; +} + +static int link_up_or_down_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, void *userdata) { + bool on_activate, up; + int r; + + assert(m); + assert(req); + assert(link); + + on_activate = req->type == REQUEST_TYPE_ACTIVATE_LINK; + up = PTR_TO_INT(req->userdata); + + r = sd_netlink_message_get_errno(m); + if (r == -ENETDOWN && up && link_up_dsa_slave(link) > 0) + log_link_message_debug_errno(link, m, r, "Could not bring up dsa slave, retrying again after dsa master becomes up"); + else if (r < 0) + log_link_message_warning_errno(link, m, r, up ? + "Could not bring up interface, ignoring" : + "Could not bring down interface, ignoring"); + + r = link_call_getlink(link, get_link_update_flag_handler); + if (r < 0) { + link_enter_failed(link); + return 0; + } + + link->set_flags_messages++; + + if (on_activate) { + link->activated = true; + link_check_ready(link); + } + + return 0; +} + +static const char *up_or_down(bool up) { + return up ? "up" : "down"; +} + +static int link_up_or_down(Link *link, bool up, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + assert(req); + + /* The log message is checked in the test. Please also update test_bond_active_slave() in + * test/test-network/systemd-networkd-tests.py. when the log message below is modified. */ + log_link_debug(link, "Bringing link %s", up_or_down(up)); + + r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_SETLINK, link->ifindex); + if (r < 0) + return r; + + r = sd_rtnl_message_link_set_flags(m, up ? IFF_UP : 0, IFF_UP); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static bool link_is_ready_to_activate_one(Link *link, bool allow_unmanaged) { + assert(link); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED, LINK_STATE_UNMANAGED)) + return false; + + if (!link->network) + return allow_unmanaged; + + if (link->set_link_messages > 0) + return false; + + return true; +} + + static bool link_is_ready_to_activate(Link *link, bool up) { + assert(link); + + if (!check_ready_for_all_sr_iov_ports(link, /* allow_unmanaged = */ false, + link_is_ready_to_activate_one)) + return false; + + if (up && link_rfkilled(link) > 0) + return false; + + return true; +} + +static int link_process_activation(Request *req, Link *link, void *userdata) { + bool up = PTR_TO_INT(userdata); + int r; + + assert(req); + assert(link); + + if (!link_is_ready_to_activate(link, up)) + return 0; + + r = link_up_or_down(link, up, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to activate link: %m"); + + return 1; +} + +int link_request_to_activate(Link *link) { + bool up; + int r; + + assert(link); + assert(link->network); + + switch (link->network->activation_policy) { + case ACTIVATION_POLICY_BOUND: + r = link_handle_bound_to_list(link); + if (r < 0) + return r; + _fallthrough_; + case ACTIVATION_POLICY_MANUAL: + link->activated = true; + link_check_ready(link); + return 0; + case ACTIVATION_POLICY_UP: + case ACTIVATION_POLICY_ALWAYS_UP: + up = true; + break; + case ACTIVATION_POLICY_DOWN: + case ACTIVATION_POLICY_ALWAYS_DOWN: + up = false; + break; + default: + assert_not_reached(); + } + + link->activated = false; + + r = link_queue_request_full(link, REQUEST_TYPE_ACTIVATE_LINK, + INT_TO_PTR(up), NULL, NULL, NULL, + link_process_activation, + &link->set_flags_messages, + link_up_or_down_handler, NULL); + if (r < 0) + return log_link_error_errno(link, r, "Failed to request to activate link: %m"); + + log_link_debug(link, "Requested to activate link"); + return 0; +} + +static bool link_is_ready_to_bring_up_or_down(Link *link, bool up) { + assert(link); + + if (up && link->dsa_master_ifindex > 0) { + Link *master; + + /* The master interface must be up. See comments in link_up_dsa_slave(). */ + + if (link_get_by_index(link->manager, link->dsa_master_ifindex, &master) < 0) + return false; + + if (!FLAGS_SET(master->flags, IFF_UP)) + return false; + } + + if (link->state == LINK_STATE_UNMANAGED) + return true; + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return false; + + if (link->set_link_messages > 0) + return false; + + if (!link->activated) + return false; + + if (up && link_rfkilled(link) > 0) + return false; + + return true; +} + +static int link_process_up_or_down(Request *req, Link *link, void *userdata) { + bool up = PTR_TO_INT(userdata); + int r; + + assert(req); + assert(link); + + if (!link_is_ready_to_bring_up_or_down(link, up)) + return 0; + + r = link_up_or_down(link, up, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to bring link %s: %m", up_or_down(up)); + + return 1; +} + +int link_request_to_bring_up_or_down(Link *link, bool up) { + int r; + + assert(link); + + r = link_queue_request_full(link, REQUEST_TYPE_UP_DOWN, + INT_TO_PTR(up), NULL, NULL, NULL, + link_process_up_or_down, + &link->set_flags_messages, + link_up_or_down_handler, NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request to bring link %s: %m", + up_or_down(up)); + + log_link_debug(link, "Requested to bring link %s", up_or_down(up)); + return 0; +} + +static int link_down_now_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert(m); + assert(link); + assert(link->set_flags_messages > 0); + + link->set_flags_messages--; + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + r = sd_netlink_message_get_errno(m); + if (r < 0) + log_link_message_warning_errno(link, m, r, "Could not bring down interface, ignoring"); + + r = link_call_getlink(link, get_link_update_flag_handler); + if (r < 0) { + link_enter_failed(link); + return 0; + } + + link->set_flags_messages++; + return 0; +} + +int link_down_now(Link *link) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + + log_link_debug(link, "Bringing link down"); + + r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex); + if (r < 0) + return log_link_warning_errno(link, r, "Could not allocate RTM_SETLINK message: %m"); + + r = sd_rtnl_message_link_set_flags(req, 0, IFF_UP); + if (r < 0) + return log_link_warning_errno(link, r, "Could not set link flags: %m"); + + r = netlink_call_async(link->manager->rtnl, NULL, req, link_down_now_handler, + link_netlink_destroy_callback, link); + if (r < 0) + return log_link_warning_errno(link, r, "Could not send rtnetlink message: %m"); + + link->set_flags_messages++; + link_ref(link); + return 0; +} + +int link_down_slave_links(Link *link) { + Link *slave; + int r; + + assert(link); + + SET_FOREACH(slave, link->slaves) { + r = link_down_now(slave); + if (r < 0) + return r; + } + + return 0; +} + +static int link_remove_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert(m); + assert(link); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 0; + + r = sd_netlink_message_get_errno(m); + if (r < 0) + log_link_message_warning_errno(link, m, r, "Could not remove interface, ignoring"); + + return 0; +} + +int link_remove(Link *link) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + + log_link_debug(link, "Removing link."); + + r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_DELLINK, link->ifindex); + if (r < 0) + return log_link_debug_errno(link, r, "Could not allocate RTM_DELLINK message: %m"); + + r = netlink_call_async(link->manager->rtnl, NULL, req, link_remove_handler, + link_netlink_destroy_callback, link); + if (r < 0) + return log_link_debug_errno(link, r, "Could not send rtnetlink message: %m"); + + link_ref(link); + + return 0; +} diff --git a/src/network/networkd-setlink.h b/src/network/networkd-setlink.h new file mode 100644 index 0000000..841e5ee --- /dev/null +++ b/src/network/networkd-setlink.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> + +typedef struct Link Link; + +int link_request_to_set_addrgen_mode(Link *link); +int link_request_to_set_bond(Link *link); +int link_request_to_set_bridge(Link *link); +int link_request_to_set_bridge_vlan(Link *link); +int link_request_to_set_can(Link *link); +int link_request_to_set_flags(Link *link); +int link_request_to_set_group(Link *link); +int link_request_to_set_mac(Link *link, bool allow_retry); +int link_request_to_set_ipoib(Link *link); +int link_request_to_set_master(Link *link); +int link_request_to_set_mtu(Link *link, uint32_t mtu); + +int link_configure_mtu(Link *link); + +int link_request_to_activate(Link *link); + +int link_request_to_bring_up_or_down(Link *link, bool up); + +int link_down_now(Link *link); +int link_down_slave_links(Link *link); +int link_remove(Link *link); diff --git a/src/network/networkd-speed-meter.c b/src/network/networkd-speed-meter.c new file mode 100644 index 0000000..cf8294e --- /dev/null +++ b/src/network/networkd-speed-meter.c @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> + +#include "sd-event.h" +#include "sd-netlink.h" + +#include "networkd-link-bus.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-speed-meter.h" + +static int process_message(Manager *manager, sd_netlink_message *message) { + uint16_t type; + int ifindex, r; + Link *link; + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) + return r; + + if (type != RTM_NEWLINK) + return 0; + + r = sd_rtnl_message_link_get_ifindex(message, &ifindex); + if (r < 0) + return r; + + r = link_get_by_index(manager, ifindex, &link); + if (r < 0) + return r; + + link->stats_old = link->stats_new; + + r = sd_netlink_message_read(message, IFLA_STATS64, sizeof link->stats_new, &link->stats_new); + if (r < 0) + return r; + + link->stats_updated = true; + + return 0; +} + +static int speed_meter_handler(sd_event_source *s, uint64_t usec, void *userdata) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + Manager *manager = ASSERT_PTR(userdata); + usec_t usec_now; + Link *link; + int r; + + assert(s); + + r = sd_event_now(sd_event_source_get_event(s), CLOCK_MONOTONIC, &usec_now); + if (r < 0) + return r; + + r = sd_event_source_set_time(s, usec_now + manager->speed_meter_interval_usec); + if (r < 0) + return r; + + manager->speed_meter_usec_old = manager->speed_meter_usec_new; + manager->speed_meter_usec_new = usec_now; + + HASHMAP_FOREACH(link, manager->links_by_index) + link->stats_updated = false; + + r = sd_rtnl_message_new_link(manager->rtnl, &req, RTM_GETLINK, 0); + if (r < 0) { + log_warning_errno(r, "Failed to allocate RTM_GETLINK netlink message, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) { + log_warning_errno(r, "Failed to set dump flag, ignoring: %m"); + return 0; + } + + r = sd_netlink_call(manager->rtnl, req, 0, &reply); + if (r < 0) { + log_warning_errno(r, "Failed to call RTM_GETLINK, ignoring: %m"); + return 0; + } + + for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) + (void) process_message(manager, i); + + return 0; +} + +int manager_start_speed_meter(Manager *manager) { + _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; + int r; + + assert(manager); + assert(manager->event); + + if (!manager->use_speed_meter) + return 0; + + r = sd_event_add_time(manager->event, &s, CLOCK_MONOTONIC, 0, 0, speed_meter_handler, manager); + if (r < 0) + return r; + + r = sd_event_source_set_enabled(s, SD_EVENT_ON); + if (r < 0) + return r; + + manager->speed_meter_event_source = TAKE_PTR(s); + return 0; +} diff --git a/src/network/networkd-speed-meter.h b/src/network/networkd-speed-meter.h new file mode 100644 index 0000000..4dd024b --- /dev/null +++ b/src/network/networkd-speed-meter.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/* Default interval is 10sec. The speed meter periodically make networkd + * to be woke up. So, too small interval value is not desired. + * We set the minimum value 100msec = 0.1sec. */ +#define SPEED_METER_DEFAULT_TIME_INTERVAL (10 * USEC_PER_SEC) +#define SPEED_METER_MINIMUM_TIME_INTERVAL (100 * USEC_PER_MSEC) + +typedef struct Manager Manager; + +int manager_start_speed_meter(Manager *m); diff --git a/src/network/networkd-sriov.c b/src/network/networkd-sriov.c new file mode 100644 index 0000000..78d8cef --- /dev/null +++ b/src/network/networkd-sriov.c @@ -0,0 +1,352 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include "device-enumerator-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-queue.h" +#include "networkd-sriov.h" + +static int sr_iov_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, SRIOV *sr_iov) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_error_errno(link, m, r, "Could not set up SR-IOV"); + link_enter_failed(link); + return 1; + } + + if (link->sr_iov_messages == 0) { + log_link_debug(link, "SR-IOV configured"); + link->sr_iov_configured = true; + link_check_ready(link); + } + + return 1; +} + +static int sr_iov_configure(SRIOV *sr_iov, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(sr_iov); + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + assert(link->ifindex > 0); + assert(req); + + log_link_debug(link, "Setting SR-IOV virtual function %"PRIu32".", sr_iov->vf); + + r = sd_rtnl_message_new_link(link->manager->rtnl, &m, RTM_SETLINK, link->ifindex); + if (r < 0) + return r; + + r = sr_iov_set_netlink_message(sr_iov, m); + if (r < 0) + return r; + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static int sr_iov_process_request(Request *req, Link *link, SRIOV *sr_iov) { + int r; + + assert(req); + assert(link); + assert(sr_iov); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return 0; + + r = sr_iov_configure(sr_iov, link, req); + if (r < 0) + return log_link_warning_errno(link, r, + "Failed to configure SR-IOV virtual function %"PRIu32": %m", + sr_iov->vf); + + return 1; +} + +int link_request_sr_iov_vfs(Link *link) { + SRIOV *sr_iov; + int r; + + assert(link); + assert(link->network); + + link->sr_iov_configured = false; + + ORDERED_HASHMAP_FOREACH(sr_iov, link->network->sr_iov_by_section) { + r = link_queue_request_safe(link, REQUEST_TYPE_SRIOV, + sr_iov, NULL, + sr_iov_hash_func, + sr_iov_compare_func, + sr_iov_process_request, + &link->sr_iov_messages, + sr_iov_handler, + NULL); + if (r < 0) + return log_link_warning_errno(link, r, + "Failed to request SR-IOV virtual function %"PRIu32": %m", + sr_iov->vf); + } + + if (link->sr_iov_messages == 0) { + link->sr_iov_configured = true; + link_check_ready(link); + } else + log_link_debug(link, "Configuring SR-IOV"); + + return 0; +} + +static int find_ifindex_from_pci_dev_port(sd_device *pci_dev, const char *dev_port) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + sd_device *dev; + int ifindex, r; + + assert(pci_dev); + assert(dev_port); + + r = sd_device_enumerator_new(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_allow_uninitialized(e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_parent(e, pci_dev); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_subsystem(e, "net", true); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_sysattr(e, "dev_port", dev_port, true); + if (r < 0) + return r; + + dev = sd_device_enumerator_get_device_first(e); + if (!dev) + return -ENODEV; /* no device found */ + + if (sd_device_enumerator_get_device_next(e)) + return -ENXIO; /* multiple devices found */ + + r = sd_device_get_ifindex(dev, &ifindex); + if (r < 0) + return r; + + assert(ifindex > 0); + return ifindex; +} + +static int manager_update_sr_iov_ifindices(Manager *manager, int phys_port_ifindex, int virt_port_ifindex) { + Link *phys_link = NULL, *virt_link = NULL; + int r; + + assert(manager); + assert(phys_port_ifindex > 0); + assert(virt_port_ifindex > 0); + + /* This sets ifindices only when both interfaces are already managed by us. */ + + r = link_get_by_index(manager, phys_port_ifindex, &phys_link); + if (r < 0) + return r; + + r = link_get_by_index(manager, virt_port_ifindex, &virt_link); + if (r < 0) + return r; + + /* update VF ifindex in PF */ + r = set_ensure_put(&phys_link->sr_iov_virt_port_ifindices, NULL, INT_TO_PTR(virt_port_ifindex)); + if (r < 0) + return r; + + log_link_debug(phys_link, + "Found SR-IOV VF port %s(%i).", + virt_link ? virt_link->ifname : "n/a", virt_port_ifindex); + + /* update PF ifindex in VF */ + if (virt_link->sr_iov_phys_port_ifindex > 0 && virt_link->sr_iov_phys_port_ifindex != phys_port_ifindex) { + Link *old_phys_link; + + if (link_get_by_index(manager, virt_link->sr_iov_phys_port_ifindex, &old_phys_link) >= 0) + set_remove(old_phys_link->sr_iov_virt_port_ifindices, INT_TO_PTR(virt_port_ifindex)); + } + + virt_link->sr_iov_phys_port_ifindex = phys_port_ifindex; + + log_link_debug(virt_link, + "Found SR-IOV PF port %s(%i).", + phys_link ? phys_link->ifname : "n/a", phys_port_ifindex); + + return 0; +} + +static int link_set_sr_iov_phys_port(Link *link, sd_device *pci_dev, const char *dev_port) { + _cleanup_(sd_device_unrefp) sd_device *pci_physfn_dev = NULL; + int r; + + assert(link); + assert(link->manager); + assert(pci_dev); + assert(dev_port); + + if (link->sr_iov_phys_port_ifindex > 0) + return 0; + + r = sd_device_new_child(&pci_physfn_dev, pci_dev, "physfn"); + if (r < 0) + return r; + + r = find_ifindex_from_pci_dev_port(pci_physfn_dev, dev_port); + if (r < 0) + return r; + + return manager_update_sr_iov_ifindices(link->manager, r, link->ifindex); +} + +static int link_set_sr_iov_virt_ports(Link *link, sd_device *pci_dev, const char *dev_port) { + const char *name; + int r; + + assert(link); + assert(link->manager); + assert(pci_dev); + assert(dev_port); + + set_clear(link->sr_iov_virt_port_ifindices); + + FOREACH_DEVICE_CHILD_WITH_SUFFIX(pci_dev, child, name) { + const char *n; + + /* Accept name prefixed with "virtfn", but refuse "virtfn" itself. */ + n = startswith(name, "virtfn"); + if (isempty(n) || !in_charset(n, DIGITS)) + continue; + + r = find_ifindex_from_pci_dev_port(child, dev_port); + if (r < 0) + continue; + + if (manager_update_sr_iov_ifindices(link->manager, link->ifindex, r) < 0) + continue; + } + + return 0; +} + +int link_set_sr_iov_ifindices(Link *link) { + const char *dev_port; + sd_device *pci_dev; + int r; + + assert(link); + + if (!link->dev) + return -ENODEV; + + r = sd_device_get_parent_with_subsystem_devtype(link->dev, "pci", NULL, &pci_dev); + if (ERRNO_IS_NEG_DEVICE_ABSENT(r)) + return 0; + if (r < 0) + return log_link_debug_errno(link, r, "Failed to get parent PCI device: %m"); + + /* This may return -EINVAL or -ENODEV, instead of -ENOENT, if the device has been removed or is being + * removed. Let's ignore the error codes here. */ + r = sd_device_get_sysattr_value(link->dev, "dev_port", &dev_port); + if (ERRNO_IS_NEG_DEVICE_ABSENT(r) || r == -EINVAL) + return 0; + if (r < 0) + return log_link_debug_errno(link, r, "Failed to get 'dev_port' sysfs attribute: %m"); + + r = link_set_sr_iov_phys_port(link, pci_dev, dev_port); + if (r < 0 && !ERRNO_IS_DEVICE_ABSENT(r)) + return log_link_debug_errno(link, r, "Failed to set SR-IOV physical port: %m"); + + r = link_set_sr_iov_virt_ports(link, pci_dev, dev_port); + if (r < 0 && !ERRNO_IS_DEVICE_ABSENT(r)) + return log_link_debug_errno(link, r, "Failed to set SR-IOV virtual ports: %m"); + + return 0; +} + +void link_clear_sr_iov_ifindices(Link *link) { + void *v; + + assert(link); + assert(link->manager); + + if (link->sr_iov_phys_port_ifindex > 0) { + Link *phys_link; + + if (link_get_by_index(link->manager, link->sr_iov_phys_port_ifindex, &phys_link) >= 0) + set_remove(phys_link->sr_iov_virt_port_ifindices, INT_TO_PTR(link->ifindex)); + + link->sr_iov_phys_port_ifindex = 0; + } + + while ((v = set_steal_first(link->sr_iov_virt_port_ifindices))) { + Link *virt_link; + + if (link_get_by_index(link->manager, PTR_TO_INT(v), &virt_link) >= 0) + virt_link->sr_iov_phys_port_ifindex = 0; + } +} + +bool check_ready_for_all_sr_iov_ports( + Link *link, + bool allow_unmanaged, /* for the main target */ + bool (check_one)(Link *link, bool allow_unmanaged)) { + + Link *phys_link; + void *v; + + assert(link); + assert(link->manager); + assert(check_one); + + /* Some drivers make VF ports become down when their PF port becomes down, and may fail to configure + * VF ports. Also, when a VF port becomes up/down, its PF port and other VF ports may become down. + * See issue #23315. */ + + /* First, check the main target. */ + if (!check_one(link, allow_unmanaged)) + return false; + + /* If this is a VF port, then also check the PF port. */ + if (link->sr_iov_phys_port_ifindex > 0) { + if (link_get_by_index(link->manager, link->sr_iov_phys_port_ifindex, &phys_link) < 0 || + !check_one(phys_link, /* allow_unmanaged = */ true)) + return false; + } else + phys_link = link; + + /* Also check all VF ports. */ + SET_FOREACH(v, phys_link->sr_iov_virt_port_ifindices) { + int ifindex = PTR_TO_INT(v); + Link *virt_link; + + if (ifindex == link->ifindex) + continue; /* The main target link is a VF port, and its state is already checked. */ + + if (link_get_by_index(link->manager, ifindex, &virt_link) < 0) + return false; + + if (!check_one(virt_link, /* allow_unmanaged = */ true)) + return false; + } + + return true; +} diff --git a/src/network/networkd-sriov.h b/src/network/networkd-sriov.h new file mode 100644 index 0000000..0d4276e --- /dev/null +++ b/src/network/networkd-sriov.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include "netif-sriov.h" + +typedef struct Link Link; + +int link_request_sr_iov_vfs(Link *link); + +int link_set_sr_iov_ifindices(Link *link); +void link_clear_sr_iov_ifindices(Link *link); + +bool check_ready_for_all_sr_iov_ports( + Link *link, + bool allow_unmanaged, /* for the main target */ + bool (check_one)(Link *link, bool allow_unmanaged)); diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c new file mode 100644 index 0000000..3a95ba8 --- /dev/null +++ b/src/network/networkd-state-file.c @@ -0,0 +1,863 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/if.h> + +#include "alloc-util.h" +#include "dns-domain.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "network-internal.h" +#include "networkd-dhcp-common.h" +#include "networkd-link.h" +#include "networkd-manager-bus.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-state-file.h" +#include "ordered-set.h" +#include "set.h" +#include "strv.h" +#include "tmpfile-util.h" + +static int ordered_set_put_dns_servers(OrderedSet **s, int ifindex, struct in_addr_full **dns, unsigned n) { + int r; + + assert(s); + assert(dns || n == 0); + + FOREACH_ARRAY(a, dns, n) { + const char *p; + + if ((*a)->ifindex != 0 && (*a)->ifindex != ifindex) + return 0; + + p = in_addr_full_to_string(*a); + if (!p) + return 0; + + r = ordered_set_put_strdup(s, p); + if (r < 0) + return r; + } + + return 0; +} + +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; + + assert(s); + assert(n == 0 || addresses); + + FOREACH_ARRAY(a, addresses, n) { + if (predicate && !predicate(a)) + continue; + + r = ordered_set_put_strdup(s, IN4_ADDR_TO_STRING(a)); + if (r < 0) + return r; + } + + return 0; +} + +static int ordered_set_put_in6_addrv( + OrderedSet **s, + const struct in6_addr *addresses, + size_t n) { + + int r; + + assert(s); + assert(n == 0 || addresses); + + FOREACH_ARRAY(a, addresses, n) { + r = ordered_set_put_strdup(s, IN6_ADDR_TO_STRING(a)); + if (r < 0) + return r; + } + + return 0; +} + +static int link_put_dns(Link *link, OrderedSet **s) { + int r; + + assert(link); + assert(link->network); + assert(s); + + if (link->n_dns != UINT_MAX) + return ordered_set_put_dns_servers(s, link->ifindex, link->dns, link->n_dns); + + r = ordered_set_put_dns_servers(s, link->ifindex, link->network->dns, link->network->n_dns); + if (r < 0) + return r; + + if (link->dhcp_lease && link->network->dhcp_use_dns) { + const struct in_addr *addresses; + + r = sd_dhcp_lease_get_dns(link->dhcp_lease, &addresses); + if (r >= 0) { + r = ordered_set_put_in4_addrv(s, addresses, r, in4_addr_is_non_local); + if (r < 0) + return r; + } + } + + if (link->dhcp6_lease && link->network->dhcp6_use_dns) { + const struct in6_addr *addresses; + + r = sd_dhcp6_lease_get_dns(link->dhcp6_lease, &addresses); + if (r >= 0) { + r = ordered_set_put_in6_addrv(s, addresses, r); + if (r < 0) + return r; + } + } + + if (link->network->ipv6_accept_ra_use_dns) { + NDiscRDNSS *a; + + SET_FOREACH(a, link->ndisc_rdnss) { + r = ordered_set_put_in6_addrv(s, &a->router, 1); + if (r < 0) + return r; + } + } + + return 0; +} + +static int link_put_ntp(Link *link, OrderedSet **s) { + int r; + + assert(link); + assert(link->network); + assert(s); + + if (link->ntp) + return ordered_set_put_strdupv(s, link->ntp); + + r = ordered_set_put_strdupv(s, link->network->ntp); + if (r < 0) + return r; + + if (link->dhcp_lease && link->network->dhcp_use_ntp) { + const struct in_addr *addresses; + + r = sd_dhcp_lease_get_ntp(link->dhcp_lease, &addresses); + if (r >= 0) { + r = ordered_set_put_in4_addrv(s, addresses, r, in4_addr_is_non_local); + if (r < 0) + return r; + } + } + + if (link->dhcp6_lease && link->network->dhcp6_use_ntp) { + const struct in6_addr *addresses; + char **fqdn; + + r = sd_dhcp6_lease_get_ntp_addrs(link->dhcp6_lease, &addresses); + if (r >= 0) { + r = ordered_set_put_in6_addrv(s, addresses, r); + if (r < 0) + return r; + } + + r = sd_dhcp6_lease_get_ntp_fqdn(link->dhcp6_lease, &fqdn); + if (r >= 0) { + r = ordered_set_put_strdupv(s, fqdn); + if (r < 0) + return r; + } + } + + return 0; +} + +static int link_put_sip(Link *link, OrderedSet **s) { + int r; + + assert(link); + assert(link->network); + assert(s); + + if (link->dhcp_lease && link->network->dhcp_use_ntp) { + const struct in_addr *addresses; + + r = sd_dhcp_lease_get_sip(link->dhcp_lease, &addresses); + if (r >= 0) { + r = ordered_set_put_in4_addrv(s, addresses, r, in4_addr_is_non_local); + if (r < 0) + return r; + } + } + + return 0; +} + +static int link_put_domains(Link *link, bool is_route, OrderedSet **s) { + OrderedSet *link_domains, *network_domains; + DHCPUseDomains use_domains; + int r; + + assert(link); + assert(link->network); + assert(s); + + link_domains = is_route ? link->route_domains : link->search_domains; + network_domains = is_route ? link->network->route_domains : link->network->search_domains; + use_domains = is_route ? DHCP_USE_DOMAINS_ROUTE : DHCP_USE_DOMAINS_YES; + + if (link_domains) + return ordered_set_put_string_set(s, link_domains); + + r = ordered_set_put_string_set(s, network_domains); + if (r < 0) + return r; + + if (link->dhcp_lease && link->network->dhcp_use_domains == use_domains) { + const char *domainname; + char **domains; + + r = sd_dhcp_lease_get_domainname(link->dhcp_lease, &domainname); + if (r >= 0) { + r = ordered_set_put_strdup(s, domainname); + if (r < 0) + return r; + } + + r = sd_dhcp_lease_get_search_domains(link->dhcp_lease, &domains); + if (r >= 0) { + r = ordered_set_put_strdupv(s, domains); + if (r < 0) + return r; + } + } + + if (link->dhcp6_lease && link->network->dhcp6_use_domains == use_domains) { + char **domains; + + r = sd_dhcp6_lease_get_domains(link->dhcp6_lease, &domains); + if (r >= 0) { + r = ordered_set_put_strdupv(s, domains); + if (r < 0) + return r; + } + } + + if (link->network->ipv6_accept_ra_use_domains == use_domains) { + NDiscDNSSL *a; + + SET_FOREACH(a, link->ndisc_dnssl) { + r = ordered_set_put_strdup(s, NDISC_DNSSL_DOMAIN(a)); + if (r < 0) + return r; + } + } + + return 0; +} + +int manager_save(Manager *m) { + _cleanup_ordered_set_free_ OrderedSet *dns = NULL, *ntp = NULL, *sip = NULL, *search_domains = NULL, *route_domains = NULL; + const char *operstate_str, *carrier_state_str, *address_state_str, *ipv4_address_state_str, *ipv6_address_state_str, *online_state_str; + LinkOperationalState operstate = LINK_OPERSTATE_OFF; + LinkCarrierState carrier_state = LINK_CARRIER_STATE_OFF; + LinkAddressState ipv4_address_state = LINK_ADDRESS_STATE_OFF, ipv6_address_state = LINK_ADDRESS_STATE_OFF, + address_state = LINK_ADDRESS_STATE_OFF; + LinkOnlineState online_state; + size_t links_offline = 0, links_online = 0; + _cleanup_(unlink_and_freep) char *temp_path = NULL; + _cleanup_strv_free_ char **p = NULL; + _cleanup_fclose_ FILE *f = NULL; + Link *link; + int r; + + assert(m); + + if (isempty(m->state_file)) + return 0; /* Do not update state file when running in test mode. */ + + HASHMAP_FOREACH(link, m->links_by_index) { + if (link->flags & IFF_LOOPBACK) + continue; + + operstate = MAX(operstate, link->operstate); + carrier_state = MAX(carrier_state, link->carrier_state); + address_state = MAX(address_state, link->address_state); + ipv4_address_state = MAX(ipv4_address_state, link->ipv4_address_state); + ipv6_address_state = MAX(ipv6_address_state, link->ipv6_address_state); + + if (!link->network) + continue; + + if (link->network->required_for_online) { + if (link->online_state == LINK_ONLINE_STATE_OFFLINE) + links_offline++; + else if (link->online_state == LINK_ONLINE_STATE_ONLINE) + links_online++; + } + + r = link_put_dns(link, &dns); + if (r < 0) + return r; + + r = link_put_ntp(link, &ntp); + if (r < 0) + return r; + + r = link_put_sip(link, &sip); + if (r < 0) + return r; + + r = link_put_domains(link, /* is_route = */ false, &search_domains); + if (r < 0) + return r; + + r = link_put_domains(link, /* is_route = */ true, &route_domains); + if (r < 0) + return r; + } + + if (carrier_state >= LINK_CARRIER_STATE_ENSLAVED) + carrier_state = LINK_CARRIER_STATE_CARRIER; + + online_state = links_online > 0 ? + (links_offline > 0 ? LINK_ONLINE_STATE_PARTIAL : LINK_ONLINE_STATE_ONLINE) : + (links_offline > 0 ? LINK_ONLINE_STATE_OFFLINE : _LINK_ONLINE_STATE_INVALID); + + operstate_str = link_operstate_to_string(operstate); + assert(operstate_str); + + carrier_state_str = link_carrier_state_to_string(carrier_state); + assert(carrier_state_str); + + address_state_str = link_address_state_to_string(address_state); + assert(address_state_str); + + ipv4_address_state_str = link_address_state_to_string(ipv4_address_state); + assert(ipv4_address_state_str); + + ipv6_address_state_str = link_address_state_to_string(ipv6_address_state); + assert(ipv6_address_state_str); + + r = fopen_temporary(m->state_file, &f, &temp_path); + if (r < 0) + return r; + + (void) fchmod(fileno(f), 0644); + + fprintf(f, + "# This is private data. Do not parse.\n" + "OPER_STATE=%s\n" + "CARRIER_STATE=%s\n" + "ADDRESS_STATE=%s\n" + "IPV4_ADDRESS_STATE=%s\n" + "IPV6_ADDRESS_STATE=%s\n", + operstate_str, carrier_state_str, address_state_str, ipv4_address_state_str, ipv6_address_state_str); + + online_state_str = link_online_state_to_string(online_state); + if (online_state_str) + fprintf(f, "ONLINE_STATE=%s\n", online_state_str); + + ordered_set_print(f, "DNS=", dns); + ordered_set_print(f, "NTP=", ntp); + ordered_set_print(f, "SIP=", sip); + ordered_set_print(f, "DOMAINS=", search_domains); + ordered_set_print(f, "ROUTE_DOMAINS=", route_domains); + + r = fflush_and_check(f); + if (r < 0) + return r; + + r = conservative_rename(temp_path, m->state_file); + if (r < 0) + return r; + + temp_path = mfree(temp_path); + + if (m->operational_state != operstate) { + m->operational_state = operstate; + if (strv_extend(&p, "OperationalState") < 0) + log_oom(); + } + + if (m->carrier_state != carrier_state) { + m->carrier_state = carrier_state; + if (strv_extend(&p, "CarrierState") < 0) + log_oom(); + } + + if (m->address_state != address_state) { + m->address_state = address_state; + if (strv_extend(&p, "AddressState") < 0) + log_oom(); + } + + if (m->ipv4_address_state != ipv4_address_state) { + m->ipv4_address_state = ipv4_address_state; + if (strv_extend(&p, "IPv4AddressState") < 0) + log_oom(); + } + + if (m->ipv6_address_state != ipv6_address_state) { + m->ipv6_address_state = ipv6_address_state; + if (strv_extend(&p, "IPv6AddressState") < 0) + log_oom(); + } + + if (m->online_state != online_state) { + m->online_state = online_state; + if (strv_extend(&p, "OnlineState") < 0) + log_oom(); + } + + if (p) { + r = manager_send_changed_strv(m, p); + if (r < 0) + log_warning_errno(r, "Could not emit changed properties, ignoring: %m"); + } + + m->dirty = false; + + return 0; +} + +static void print_link_hashmap(FILE *f, const char *prefix, Hashmap* h) { + bool space = false; + Link *link; + + assert(f); + assert(prefix); + + if (hashmap_isempty(h)) + return; + + fputs(prefix, f); + HASHMAP_FOREACH(link, h) { + if (space) + fputc(' ', f); + + fprintf(f, "%i", link->ifindex); + space = true; + } + + fputc('\n', f); +} + +static void link_save_dns(Link *link, FILE *f, struct in_addr_full **dns, unsigned n_dns, bool *space) { + bool _space = false; + + if (!space) + space = &_space; + + for (unsigned j = 0; j < n_dns; j++) { + const char *str; + + if (dns[j]->ifindex != 0 && dns[j]->ifindex != link->ifindex) + continue; + + str = in_addr_full_to_string(dns[j]); + if (!str) + continue; + + if (*space) + fputc(' ', f); + fputs(str, f); + *space = true; + } +} + +static void serialize_addresses( + FILE *f, + const char *lvalue, + bool *space, + char **addresses, + sd_dhcp_lease *lease, + bool conditional, + sd_dhcp_lease_server_type_t what, + sd_dhcp6_lease *lease6, + bool conditional6, + int (*lease6_get_addr)(sd_dhcp6_lease*, const struct in6_addr**), + int (*lease6_get_fqdn)(sd_dhcp6_lease*, char ***)) { + + bool _space = false; + int r; + + if (!space) + space = &_space; + + if (lvalue) + fprintf(f, "%s=", lvalue); + fputstrv(f, addresses, NULL, space); + + if (lease && conditional) { + const struct in_addr *lease_addresses; + + r = sd_dhcp_lease_get_servers(lease, what, &lease_addresses); + if (r > 0) + serialize_in_addrs(f, lease_addresses, r, space, in4_addr_is_non_local); + } + + if (lease6 && conditional6 && lease6_get_addr) { + const struct in6_addr *in6_addrs; + + r = lease6_get_addr(lease6, &in6_addrs); + if (r > 0) + serialize_in6_addrs(f, in6_addrs, r, space); + } + + if (lease6 && conditional6 && lease6_get_fqdn) { + char **in6_hosts; + + r = lease6_get_fqdn(lease6, &in6_hosts); + if (r > 0) + fputstrv(f, in6_hosts, NULL, space); + } + + if (lvalue) + fputc('\n', f); +} + +static void link_save_domains(Link *link, FILE *f, OrderedSet *static_domains, DHCPUseDomains use_domains) { + bool space = false; + const char *p; + + assert(link); + assert(link->network); + assert(f); + + ORDERED_SET_FOREACH(p, static_domains) + fputs_with_space(f, p, NULL, &space); + + if (use_domains == DHCP_USE_DOMAINS_NO) + return; + + if (link->dhcp_lease && link->network->dhcp_use_domains == use_domains) { + const char *domainname; + char **domains; + + if (sd_dhcp_lease_get_domainname(link->dhcp_lease, &domainname) >= 0) + fputs_with_space(f, domainname, NULL, &space); + if (sd_dhcp_lease_get_search_domains(link->dhcp_lease, &domains) >= 0) + fputstrv(f, domains, NULL, &space); + } + + if (link->dhcp6_lease && link->network->dhcp6_use_domains == use_domains) { + char **domains; + + if (sd_dhcp6_lease_get_domains(link->dhcp6_lease, &domains) >= 0) + fputstrv(f, domains, NULL, &space); + } + + if (link->network->ipv6_accept_ra_use_domains == use_domains) { + NDiscDNSSL *dd; + + SET_FOREACH(dd, link->ndisc_dnssl) + fputs_with_space(f, NDISC_DNSSL_DOMAIN(dd), NULL, &space); + } +} + +static int link_save(Link *link) { + const char *admin_state, *oper_state, *carrier_state, *address_state, *ipv4_address_state, *ipv6_address_state, + *captive_portal; + _cleanup_(unlink_and_freep) char *temp_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + int r; + + assert(link); + assert(link->manager); + + if (isempty(link->state_file)) + return 0; /* Do not update state files when running in test mode. */ + + if (link->state == LINK_STATE_LINGER) + return 0; + + link_lldp_save(link); + + admin_state = link_state_to_string(link->state); + assert(admin_state); + + oper_state = link_operstate_to_string(link->operstate); + assert(oper_state); + + carrier_state = link_carrier_state_to_string(link->carrier_state); + assert(carrier_state); + + address_state = link_address_state_to_string(link->address_state); + assert(address_state); + + ipv4_address_state = link_address_state_to_string(link->ipv4_address_state); + assert(ipv4_address_state); + + ipv6_address_state = link_address_state_to_string(link->ipv6_address_state); + assert(ipv6_address_state); + + r = fopen_temporary(link->state_file, &f, &temp_path); + if (r < 0) + return r; + + (void) fchmod(fileno(f), 0644); + + fprintf(f, + "# This is private data. Do not parse.\n" + "ADMIN_STATE=%s\n" + "OPER_STATE=%s\n" + "CARRIER_STATE=%s\n" + "ADDRESS_STATE=%s\n" + "IPV4_ADDRESS_STATE=%s\n" + "IPV6_ADDRESS_STATE=%s\n", + admin_state, oper_state, carrier_state, address_state, ipv4_address_state, ipv6_address_state); + + if (link->network) { + const char *online_state; + bool space = false; + + online_state = link_online_state_to_string(link->online_state); + if (online_state) + fprintf(f, "ONLINE_STATE=%s\n", online_state); + + fprintf(f, "REQUIRED_FOR_ONLINE=%s\n", + yes_no(link->network->required_for_online)); + + LinkOperationalStateRange st = link->network->required_operstate_for_online; + fprintf(f, "REQUIRED_OPER_STATE_FOR_ONLINE=%s%s%s\n", + strempty(link_operstate_to_string(st.min)), + st.max != LINK_OPERSTATE_RANGE_DEFAULT.max ? ":" : "", + st.max != LINK_OPERSTATE_RANGE_DEFAULT.max ? strempty(link_operstate_to_string(st.max)) : ""); + + fprintf(f, "REQUIRED_FAMILY_FOR_ONLINE=%s\n", + link_required_address_family_to_string(link->network->required_family_for_online)); + + fprintf(f, "ACTIVATION_POLICY=%s\n", + activation_policy_to_string(link->network->activation_policy)); + + fprintf(f, "NETWORK_FILE=%s\n", link->network->filename); + + fputs("NETWORK_FILE_DROPINS=\"", f); + STRV_FOREACH(d, link->network->dropins) { + _cleanup_free_ char *escaped = NULL; + + escaped = xescape(*d, ":"); + if (!escaped) + return -ENOMEM; + + fputs_with_space(f, escaped, ":", &space); + } + fputs("\"\n", f); + + /************************************************************/ + + fputs("DNS=", f); + if (link->n_dns != UINT_MAX) + link_save_dns(link, f, link->dns, link->n_dns, NULL); + else { + space = false; + link_save_dns(link, f, link->network->dns, link->network->n_dns, &space); + + serialize_addresses(f, NULL, &space, + NULL, + link->dhcp_lease, + link->network->dhcp_use_dns, + SD_DHCP_LEASE_DNS, + link->dhcp6_lease, + link->network->dhcp6_use_dns, + sd_dhcp6_lease_get_dns, + NULL); + + if (link->network->ipv6_accept_ra_use_dns) { + NDiscRDNSS *dd; + + SET_FOREACH(dd, link->ndisc_rdnss) + serialize_in6_addrs(f, &dd->address, 1, &space); + } + } + + fputc('\n', f); + + /************************************************************/ + + if (link->ntp) { + fputs("NTP=", f); + fputstrv(f, link->ntp, NULL, NULL); + fputc('\n', f); + } else + serialize_addresses(f, "NTP", NULL, + link->network->ntp, + link->dhcp_lease, + link->network->dhcp_use_ntp, + SD_DHCP_LEASE_NTP, + link->dhcp6_lease, + link->network->dhcp6_use_ntp, + sd_dhcp6_lease_get_ntp_addrs, + sd_dhcp6_lease_get_ntp_fqdn); + + serialize_addresses(f, "SIP", NULL, + NULL, + link->dhcp_lease, + link->network->dhcp_use_sip, + SD_DHCP_LEASE_SIP, + NULL, false, NULL, NULL); + + /************************************************************/ + + r = link_get_captive_portal(link, &captive_portal); + if (r < 0) + return r; + + if (captive_portal) + fprintf(f, "CAPTIVE_PORTAL=%s\n", captive_portal); + + /************************************************************/ + + fputs("DOMAINS=", f); + if (link->search_domains) + link_save_domains(link, f, link->search_domains, DHCP_USE_DOMAINS_NO); + else + link_save_domains(link, f, link->network->search_domains, DHCP_USE_DOMAINS_YES); + fputc('\n', f); + + /************************************************************/ + + fputs("ROUTE_DOMAINS=", f); + if (link->route_domains) + link_save_domains(link, f, link->route_domains, DHCP_USE_DOMAINS_NO); + else + link_save_domains(link, f, link->network->route_domains, DHCP_USE_DOMAINS_ROUTE); + fputc('\n', f); + + /************************************************************/ + + fprintf(f, "LLMNR=%s\n", + resolve_support_to_string(link->llmnr >= 0 ? link->llmnr : link->network->llmnr)); + + /************************************************************/ + + fprintf(f, "MDNS=%s\n", + resolve_support_to_string(link->mdns >= 0 ? link->mdns : link->network->mdns)); + + /************************************************************/ + + int dns_default_route = + link->dns_default_route >= 0 ? link->dns_default_route : + link->network->dns_default_route; + if (dns_default_route >= 0) + fprintf(f, "DNS_DEFAULT_ROUTE=%s\n", yes_no(dns_default_route)); + + /************************************************************/ + + DnsOverTlsMode dns_over_tls_mode = + link->dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID ? link->dns_over_tls_mode : + link->network->dns_over_tls_mode; + if (dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID) + fprintf(f, "DNS_OVER_TLS=%s\n", dns_over_tls_mode_to_string(dns_over_tls_mode)); + + /************************************************************/ + + DnssecMode dnssec_mode = + link->dnssec_mode != _DNSSEC_MODE_INVALID ? link->dnssec_mode : + link->network->dnssec_mode; + if (dnssec_mode != _DNSSEC_MODE_INVALID) + fprintf(f, "DNSSEC=%s\n", dnssec_mode_to_string(dnssec_mode)); + + /************************************************************/ + + Set *nta_anchors = link->dnssec_negative_trust_anchors; + if (set_isempty(nta_anchors)) + nta_anchors = link->network->dnssec_negative_trust_anchors; + + if (!set_isempty(nta_anchors)) { + const char *n; + + fputs("DNSSEC_NTA=", f); + space = false; + SET_FOREACH(n, nta_anchors) + fputs_with_space(f, n, NULL, &space); + fputc('\n', f); + } + } + + print_link_hashmap(f, "CARRIER_BOUND_TO=", link->bound_to_links); + print_link_hashmap(f, "CARRIER_BOUND_BY=", link->bound_by_links); + + if (link->dhcp_lease) { + r = dhcp_lease_save(link->dhcp_lease, link->lease_file); + if (r < 0) + return r; + + fprintf(f, + "DHCP_LEASE=%s\n", + link->lease_file); + } else + (void) unlink(link->lease_file); + + r = link_serialize_dhcp6_client(link, f); + if (r < 0) + return r; + + r = fflush_and_check(f); + if (r < 0) + return r; + + r = conservative_rename(temp_path, link->state_file); + if (r < 0) + return r; + + temp_path = mfree(temp_path); + + return 0; +} + +void link_dirty(Link *link) { + int r; + + assert(link); + assert(link->manager); + + /* The serialized state in /run is no longer up-to-date. */ + + /* Also mark manager dirty as link is dirty */ + link->manager->dirty = true; + + r = set_ensure_put(&link->manager->dirty_links, NULL, link); + if (r <= 0) + /* Ignore allocation errors and don't take another ref if the link was already dirty */ + return; + link_ref(link); +} + +void link_clean(Link *link) { + assert(link); + assert(link->manager); + + /* The serialized state in /run is up-to-date */ + + link_unref(set_remove(link->manager->dirty_links, link)); +} + +int link_save_and_clean_full(Link *link, bool also_save_manager) { + int r, k = 0; + + assert(link); + assert(link->manager); + + if (also_save_manager) + k = manager_save(link->manager); + + r = link_save(link); + if (r < 0) + return r; + + link_clean(link); + return k; +} diff --git a/src/network/networkd-state-file.h b/src/network/networkd-state-file.h new file mode 100644 index 0000000..684f0d1 --- /dev/null +++ b/src/network/networkd-state-file.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct Link Link; +typedef struct Manager Manager; + +void link_dirty(Link *link); +void link_clean(Link *link); +int link_save_and_clean_full(Link *link, bool also_save_manager); +static inline int link_save_and_clean(Link *link) { + return link_save_and_clean_full(link, false); +} + +int manager_save(Manager *m); diff --git a/src/network/networkd-sysctl.c b/src/network/networkd-sysctl.c new file mode 100644 index 0000000..2b226b2 --- /dev/null +++ b/src/network/networkd-sysctl.c @@ -0,0 +1,335 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <linux/if.h> +#include <linux/if_arp.h> + +#include "missing_network.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-sysctl.h" +#include "socket-util.h" +#include "string-table.h" +#include "sysctl-util.h" + +static bool link_is_configured_for_family(Link *link, int family) { + assert(link); + + if (!link->network) + return false; + + if (link->flags & IFF_LOOPBACK) + return false; + + /* CAN devices do not support IP layer. Most of the functions below are never called for CAN devices, + * but link_set_ipv6_mtu() may be called after setting interface MTU, and warn about the failure. For + * safety, let's unconditionally check if the interface is not a CAN device. */ + if (IN_SET(family, AF_INET, AF_INET6) && link->iftype == ARPHRD_CAN) + return false; + + if (family == AF_INET6 && !socket_ipv6_is_supported()) + return false; + + return true; +} + +static int link_update_ipv6_sysctl(Link *link) { + assert(link); + + if (!link_is_configured_for_family(link, AF_INET6)) + 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_is_configured_for_family(link, AF_INET)) + 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 (!link_is_configured_for_family(link, family)) + 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_ipv4_rp_filter(Link *link) { + assert(link); + + if (!link_is_configured_for_family(link, AF_INET)) + return 0; + + if (link->network->ipv4_rp_filter < 0) + return 0; + + return sysctl_write_ip_property_int(AF_INET, link->ifname, "rp_filter", link->network->ipv4_rp_filter); +} + +static int link_set_ipv6_privacy_extensions(Link *link) { + IPv6PrivacyExtensions val; + + assert(link); + assert(link->manager); + + if (!link_is_configured_for_family(link, AF_INET6)) + return 0; + + val = link->network->ipv6_privacy_extensions; + if (val < 0) /* If not specified, then use the global setting. */ + val = link->manager->ipv6_privacy_extensions; + + /* When "kernel", do not update the setting. */ + if (val == IPV6_PRIVACY_EXTENSIONS_KERNEL) + return 0; + + return sysctl_write_ip_property_int(AF_INET6, link->ifname, "use_tempaddr", (int) val); +} + +static int link_set_ipv6_accept_ra(Link *link) { + assert(link); + + if (!link_is_configured_for_family(link, AF_INET6)) + 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); + + if (!link_is_configured_for_family(link, AF_INET6)) + 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); + + if (!link_is_configured_for_family(link, AF_INET6)) + return 0; + + if (link->network->ipv6_hop_limit <= 0) + return 0; + + return sysctl_write_ip_property_int(AF_INET6, link->ifname, "hop_limit", link->network->ipv6_hop_limit); +} + +static int link_set_ipv6_proxy_ndp(Link *link) { + bool v; + + assert(link); + + if (!link_is_configured_for_family(link, AF_INET6)) + return 0; + + if (link->network->ipv6_proxy_ndp >= 0) + v = link->network->ipv6_proxy_ndp; + else + v = !set_isempty(link->network->ipv6_proxy_ndp_addresses); + + return sysctl_write_ip_property_boolean(AF_INET6, link->ifname, "proxy_ndp", v); +} + +int link_set_ipv6_mtu(Link *link) { + uint32_t mtu; + + assert(link); + + if (!link_is_configured_for_family(link, AF_INET6)) + return 0; + + if (link->network->ipv6_mtu == 0) + return 0; + + mtu = link->network->ipv6_mtu; + if (mtu > link->max_mtu) { + log_link_warning(link, "Reducing requested IPv6 MTU %"PRIu32" to the interface's maximum MTU %"PRIu32".", + mtu, link->max_mtu); + mtu = link->max_mtu; + } + + return sysctl_write_ip_property_uint32(AF_INET6, link->ifname, "mtu", mtu); +} + +static int link_set_ipv4_accept_local(Link *link) { + assert(link); + + if (!link_is_configured_for_family(link, AF_INET)) + return 0; + + if (link->network->ipv4_accept_local < 0) + return 0; + + return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "accept_local", link->network->ipv4_accept_local > 0); +} + +static int link_set_ipv4_route_localnet(Link *link) { + assert(link); + + if (!link_is_configured_for_family(link, AF_INET)) + return 0; + + if (link->network->ipv4_route_localnet < 0) + return 0; + + return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "route_localnet", link->network->ipv4_route_localnet > 0); +} + +static int link_set_ipv4_promote_secondaries(Link *link) { + assert(link); + + if (!link_is_configured_for_family(link, AF_INET)) + return 0; + + /* If promote_secondaries is not set, DHCP will work only as long as the IP address does not + * changes between leases. The kernel will remove all secondary IP addresses of an interface + * otherwise. The way systemd-networkd works is that the new IP of a lease is added as a + * secondary IP and when the primary one expires it relies on the kernel to promote the + * secondary IP. See also https://github.com/systemd/systemd/issues/7163 */ + return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "promote_secondaries", true); +} + +int link_set_sysctl(Link *link) { + int r; + + assert(link); + + /* If IPv6 configured that is static IPv6 address and IPv6LL autoconfiguration is enabled + * for this interface, then enable IPv6 */ + r = link_update_ipv6_sysctl(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot enable IPv6, ignoring: %m"); + + r = link_set_proxy_arp(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot configure proxy ARP for interface, ignoring: %m"); + + r = link_set_ipv4_forward(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot turn on IPv4 packet forwarding, ignoring: %m"); + + r = link_set_ipv6_forward(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot configure IPv6 packet forwarding, ignoring: %m"); + + r = link_set_ipv6_privacy_extensions(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot configure IPv6 privacy extensions for interface, ignoring: %m"); + + r = link_set_ipv6_accept_ra(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot disable kernel IPv6 accept_ra for interface, ignoring: %m"); + + r = link_set_ipv6_dad_transmits(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv6 dad transmits for interface, ignoring: %m"); + + r = link_set_ipv6_hop_limit(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv6 hop limit for interface, ignoring: %m"); + + r = link_set_ipv6_proxy_ndp(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv6 proxy NDP, ignoring: %m"); + + r = link_set_ipv6_mtu(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv6 MTU, ignoring: %m"); + + r = link_set_ipv6ll_stable_secret(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set stable secret address for IPv6 link-local address: %m"); + + r = link_set_ipv4_accept_local(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv4 accept_local flag for interface, ignoring: %m"); + + r = link_set_ipv4_route_localnet(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv4 route_localnet flag for interface, ignoring: %m"); + + r = link_set_ipv4_rp_filter(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot set IPv4 reverse path filtering for interface, ignoring: %m"); + + r = link_set_ipv4_promote_secondaries(link); + if (r < 0) + log_link_warning_errno(link, r, "Cannot enable promote_secondaries for interface, ignoring: %m"); + + return 0; +} + +static const char* const ipv6_privacy_extensions_table[_IPV6_PRIVACY_EXTENSIONS_MAX] = { + [IPV6_PRIVACY_EXTENSIONS_NO] = "no", + [IPV6_PRIVACY_EXTENSIONS_PREFER_PUBLIC] = "prefer-public", + [IPV6_PRIVACY_EXTENSIONS_YES] = "yes", + [IPV6_PRIVACY_EXTENSIONS_KERNEL] = "kernel", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(ipv6_privacy_extensions, IPv6PrivacyExtensions, + IPV6_PRIVACY_EXTENSIONS_YES); +DEFINE_CONFIG_PARSE_ENUM(config_parse_ipv6_privacy_extensions, ipv6_privacy_extensions, IPv6PrivacyExtensions, + "Failed to parse IPv6 privacy extensions option"); + +static const char* const ip_reverse_path_filter_table[_IP_REVERSE_PATH_FILTER_MAX] = { + [IP_REVERSE_PATH_FILTER_NO] = "no", + [IP_REVERSE_PATH_FILTER_STRICT] = "strict", + [IP_REVERSE_PATH_FILTER_LOOSE] = "loose", +}; + +DEFINE_STRING_TABLE_LOOKUP(ip_reverse_path_filter, IPReversePathFilter); +DEFINE_CONFIG_PARSE_ENUM(config_parse_ip_reverse_path_filter, ip_reverse_path_filter, IPReversePathFilter, + "Failed to parse IP reverse path filter option"); diff --git a/src/network/networkd-sysctl.h b/src/network/networkd-sysctl.h new file mode 100644 index 0000000..0644384 --- /dev/null +++ b/src/network/networkd-sysctl.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> + +#include "conf-parser.h" + +typedef struct Link Link; + +typedef enum IPv6PrivacyExtensions { + /* These values map to the kernel's /proc/sys/net/ipv6/conf/xxx/use_tempaddr values. Do not reorder! */ + IPV6_PRIVACY_EXTENSIONS_NO, + IPV6_PRIVACY_EXTENSIONS_PREFER_PUBLIC, + IPV6_PRIVACY_EXTENSIONS_YES, /* aka prefer-temporary */ + IPV6_PRIVACY_EXTENSIONS_KERNEL, /* keep the kernel's default value */ + _IPV6_PRIVACY_EXTENSIONS_MAX, + _IPV6_PRIVACY_EXTENSIONS_INVALID = -EINVAL, +} IPv6PrivacyExtensions; + +typedef enum IPReversePathFilter { + /* These values map to the kernel's /proc/sys/net/ipv6/conf/xxx/rp_filter values. Do not reorder! */ + IP_REVERSE_PATH_FILTER_NO, + IP_REVERSE_PATH_FILTER_STRICT, + IP_REVERSE_PATH_FILTER_LOOSE, + _IP_REVERSE_PATH_FILTER_MAX, + _IP_REVERSE_PATH_FILTER_INVALID = -EINVAL, +} IPReversePathFilter; + +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_; + +const char* ip_reverse_path_filter_to_string(IPReversePathFilter i) _const_; +IPReversePathFilter ip_reverse_path_filter_from_string(const char *s) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_privacy_extensions); +CONFIG_PARSER_PROTOTYPE(config_parse_ip_reverse_path_filter); diff --git a/src/network/networkd-util.c b/src/network/networkd-util.c new file mode 100644 index 0000000..33352ba --- /dev/null +++ b/src/network/networkd-util.c @@ -0,0 +1,257 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "condition.h" +#include "conf-parser.h" +#include "escape.h" +#include "logarithm.h" +#include "networkd-link.h" +#include "networkd-util.h" +#include "parse-util.h" +#include "string-table.h" +#include "string-util.h" +#include "web-util.h" + +/* This is used in log messages, and never used in parsing settings. So, upper cases are OK. */ +static const char * const network_config_source_table[_NETWORK_CONFIG_SOURCE_MAX] = { + [NETWORK_CONFIG_SOURCE_FOREIGN] = "foreign", + [NETWORK_CONFIG_SOURCE_STATIC] = "static", + [NETWORK_CONFIG_SOURCE_IPV4LL] = "IPv4LL", + [NETWORK_CONFIG_SOURCE_DHCP4] = "DHCPv4", + [NETWORK_CONFIG_SOURCE_DHCP6] = "DHCPv6", + [NETWORK_CONFIG_SOURCE_DHCP_PD] = "DHCP-PD", + [NETWORK_CONFIG_SOURCE_NDISC] = "NDisc", + [NETWORK_CONFIG_SOURCE_RUNTIME] = "runtime", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(network_config_source, NetworkConfigSource); + +int network_config_state_to_string_alloc(NetworkConfigState s, char **ret) { + static const char* states[] = { + [LOG2U(NETWORK_CONFIG_STATE_REQUESTING)] = "requesting", + [LOG2U(NETWORK_CONFIG_STATE_CONFIGURING)] = "configuring", + [LOG2U(NETWORK_CONFIG_STATE_CONFIGURED)] = "configured", + [LOG2U(NETWORK_CONFIG_STATE_MARKED)] = "marked", + [LOG2U(NETWORK_CONFIG_STATE_REMOVING)] = "removing", + }; + _cleanup_free_ char *buf = NULL; + + assert(ret); + + for (size_t i = 0; i < ELEMENTSOF(states); i++) + if (FLAGS_SET(s, 1 << i)) { + assert(states[i]); + + if (!strextend_with_separator(&buf, ",", states[i])) + return -ENOMEM; + } + + *ret = TAKE_PTR(buf); + return 0; +} + +static const char * const address_family_table[_ADDRESS_FAMILY_MAX] = { + [ADDRESS_FAMILY_NO] = "no", + [ADDRESS_FAMILY_YES] = "yes", + [ADDRESS_FAMILY_IPV4] = "ipv4", + [ADDRESS_FAMILY_IPV6] = "ipv6", +}; + +static const char * const routing_policy_rule_address_family_table[_ADDRESS_FAMILY_MAX] = { + [ADDRESS_FAMILY_YES] = "both", + [ADDRESS_FAMILY_IPV4] = "ipv4", + [ADDRESS_FAMILY_IPV6] = "ipv6", +}; + +static const char * const nexthop_address_family_table[_ADDRESS_FAMILY_MAX] = { + [ADDRESS_FAMILY_IPV4] = "ipv4", + [ADDRESS_FAMILY_IPV6] = "ipv6", +}; + +static const char * const duplicate_address_detection_address_family_table[_ADDRESS_FAMILY_MAX] = { + [ADDRESS_FAMILY_NO] = "none", + [ADDRESS_FAMILY_YES] = "both", + [ADDRESS_FAMILY_IPV4] = "ipv4", + [ADDRESS_FAMILY_IPV6] = "ipv6", +}; + +static const char * const dhcp_deprecated_address_family_table[_ADDRESS_FAMILY_MAX] = { + [ADDRESS_FAMILY_NO] = "none", + [ADDRESS_FAMILY_YES] = "both", + [ADDRESS_FAMILY_IPV4] = "v4", + [ADDRESS_FAMILY_IPV6] = "v6", +}; + +static const char * const ip_masquerade_address_family_table[_ADDRESS_FAMILY_MAX] = { + [ADDRESS_FAMILY_NO] = "no", + [ADDRESS_FAMILY_YES] = "both", + [ADDRESS_FAMILY_IPV4] = "ipv4", + [ADDRESS_FAMILY_IPV6] = "ipv6", +}; + +static const char * const dhcp_lease_server_type_table[_SD_DHCP_LEASE_SERVER_TYPE_MAX] = { + [SD_DHCP_LEASE_DNS] = "DNS servers", + [SD_DHCP_LEASE_NTP] = "NTP servers", + [SD_DHCP_LEASE_SIP] = "SIP servers", + [SD_DHCP_LEASE_POP3] = "POP3 servers", + [SD_DHCP_LEASE_SMTP] = "SMTP servers", + [SD_DHCP_LEASE_LPR] = "LPR servers", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(address_family, AddressFamily, ADDRESS_FAMILY_YES); + +AddressFamily link_local_address_family_from_string(const char *s) { + if (streq_ptr(s, "fallback")) /* compat name */ + return ADDRESS_FAMILY_YES; + if (streq_ptr(s, "fallback-ipv4")) /* compat name */ + return ADDRESS_FAMILY_IPV4; + return address_family_from_string(s); +} + +DEFINE_STRING_TABLE_LOOKUP(routing_policy_rule_address_family, AddressFamily); +DEFINE_STRING_TABLE_LOOKUP(nexthop_address_family, AddressFamily); +DEFINE_STRING_TABLE_LOOKUP(duplicate_address_detection_address_family, AddressFamily); +DEFINE_CONFIG_PARSE_ENUM(config_parse_link_local_address_family, link_local_address_family, + AddressFamily, "Failed to parse option"); +DEFINE_STRING_TABLE_LOOKUP_FROM_STRING(dhcp_deprecated_address_family, AddressFamily); +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(ip_masquerade_address_family, AddressFamily); +DEFINE_STRING_TABLE_LOOKUP(dhcp_lease_server_type, sd_dhcp_lease_server_type_t); + +int config_parse_address_family_with_kernel( + const char* unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + AddressFamily *fwd = data, s; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + /* This function is mostly obsolete now. It simply redirects + * "kernel" to "no". In older networkd versions we used to + * distinguish IPForward=off from IPForward=kernel, where the + * former would explicitly turn off forwarding while the + * latter would simply not touch the setting. But that logic + * is gone, hence silently accept the old setting, but turn it + * to "no". */ + + s = address_family_from_string(rvalue); + if (s < 0) { + if (streq(rvalue, "kernel")) + s = ADDRESS_FAMILY_NO; + else { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Failed to parse IPForward= option, ignoring: %s", rvalue); + return 0; + } + } + + *fwd = s; + + return 0; +} + +int config_parse_ip_masquerade( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + AddressFamily a, *ret = data; + int r; + + if (isempty(rvalue)) { + *ret = ADDRESS_FAMILY_NO; + return 0; + } + + r = parse_boolean(rvalue); + if (r >= 0) { + if (r) + log_syntax(unit, LOG_WARNING, filename, line, 0, + "IPMasquerade=%s is deprecated, and it is handled as \"ipv4\" instead of \"both\". " + "Please use \"ipv4\" or \"both\".", + rvalue); + + *ret = r ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_NO; + return 0; + } + + a = ip_masquerade_address_family_from_string(rvalue); + if (a < 0) { + log_syntax(unit, LOG_WARNING, filename, line, a, + "Failed to parse IPMasquerade= setting, ignoring assignment: %s", rvalue); + return 0; + } + + *ret = a; + return 0; +} + +int config_parse_mud_url( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *unescaped = NULL; + char **url = ASSERT_PTR(data); + ssize_t l; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *url = mfree(*url); + return 0; + } + + l = cunescape(rvalue, 0, &unescaped); + if (l < 0) { + log_syntax(unit, LOG_WARNING, filename, line, l, + "Failed to unescape MUD URL, ignoring: %s", rvalue); + return 0; + } + + if (l > UINT8_MAX || !http_url_is_valid(unescaped)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid MUD URL, ignoring: %s", rvalue); + return 0; + } + + return free_and_replace(*url, unescaped); +} + +int log_link_message_full_errno(Link *link, sd_netlink_message *m, int level, int err, const char *msg) { + const char *err_msg = NULL; + + /* link may be NULL. */ + + (void) sd_netlink_message_read_string(m, NLMSGERR_ATTR_MSG, &err_msg); + return log_link_full_errno(link, level, err, + "%s: %s%s%s%m", + msg, + strempty(err_msg), + err_msg && !endswith(err_msg, ".") ? "." : "", + err_msg ? " " : ""); +} diff --git a/src/network/networkd-util.h b/src/network/networkd-util.h new file mode 100644 index 0000000..9c360f5 --- /dev/null +++ b/src/network/networkd-util.h @@ -0,0 +1,165 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-dhcp-lease.h" +#include "sd-netlink.h" + +#include "conf-parser.h" +#include "hashmap.h" +#include "log.h" +#include "macro.h" +#include "network-util.h" +#include "string-util.h" + +typedef struct Link Link; + +typedef enum NetworkConfigSource { + NETWORK_CONFIG_SOURCE_FOREIGN, /* configured by kernel */ + NETWORK_CONFIG_SOURCE_STATIC, + NETWORK_CONFIG_SOURCE_IPV4LL, + NETWORK_CONFIG_SOURCE_DHCP4, + NETWORK_CONFIG_SOURCE_DHCP6, + NETWORK_CONFIG_SOURCE_DHCP_PD, + NETWORK_CONFIG_SOURCE_NDISC, + NETWORK_CONFIG_SOURCE_RUNTIME, /* through D-Bus method */ + _NETWORK_CONFIG_SOURCE_MAX, + _NETWORK_CONFIG_SOURCE_INVALID = -EINVAL, +} NetworkConfigSource; + +typedef enum NetworkConfigState { + NETWORK_CONFIG_STATE_REQUESTING = 1 << 0, /* request is queued */ + NETWORK_CONFIG_STATE_CONFIGURING = 1 << 1, /* e.g. address_configure() is called, but no response is received yet */ + NETWORK_CONFIG_STATE_CONFIGURED = 1 << 2, /* e.g. address_configure() is called and received a response from kernel. + * Note that address may not be ready yet, so please use address_is_ready() + * to check whether the address can be usable or not. */ + NETWORK_CONFIG_STATE_MARKED = 1 << 3, /* used GC'ing the old config */ + NETWORK_CONFIG_STATE_REMOVING = 1 << 4, /* e.g. address_remove() is called, but no response is received yet */ +} NetworkConfigState; + +static inline usec_t sec_to_usec(uint32_t sec, usec_t timestamp_usec) { + return + sec == 0 ? 0 : + sec == UINT32_MAX ? USEC_INFINITY : + usec_add(timestamp_usec, sec * USEC_PER_SEC); +} + +static inline usec_t sec16_to_usec(uint16_t sec, usec_t timestamp_usec) { + return sec_to_usec(sec == UINT16_MAX ? UINT32_MAX : (uint32_t) sec, timestamp_usec); +} + +static inline uint32_t usec_to_sec(usec_t usec, usec_t now_usec) { + return MIN(DIV_ROUND_UP(usec_sub_unsigned(usec, now_usec), USEC_PER_SEC), UINT32_MAX); +} + +CONFIG_PARSER_PROTOTYPE(config_parse_link_local_address_family); +CONFIG_PARSER_PROTOTYPE(config_parse_address_family_with_kernel); +CONFIG_PARSER_PROTOTYPE(config_parse_ip_masquerade); +CONFIG_PARSER_PROTOTYPE(config_parse_mud_url); + +const char *network_config_source_to_string(NetworkConfigSource s) _const_; + +int network_config_state_to_string_alloc(NetworkConfigState s, char **ret); + +#define DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(type, name) \ + static inline void name##_update_state( \ + type *t, \ + NetworkConfigState mask, \ + NetworkConfigState value) { \ + \ + assert(t); \ + \ + t->state = (t->state & ~mask) | (value & mask); \ + } \ + static inline bool name##_exists(const type *t) { \ + assert(t); \ + \ + if ((t->state & (NETWORK_CONFIG_STATE_CONFIGURING | \ + NETWORK_CONFIG_STATE_CONFIGURED)) == 0) \ + return false; /* Not assigned yet. */ \ + if (FLAGS_SET(t->state, NETWORK_CONFIG_STATE_REMOVING)) \ + return false; /* Already removing. */ \ + return true; \ + } \ + static inline void name##_enter_requesting(type *t) { \ + name##_update_state(t, \ + NETWORK_CONFIG_STATE_REQUESTING, \ + NETWORK_CONFIG_STATE_REQUESTING); \ + } \ + static inline void name##_cancel_requesting(type *t) { \ + name##_update_state(t, \ + NETWORK_CONFIG_STATE_REQUESTING, \ + 0); \ + } \ + static inline bool name##_is_requesting(const type *t) { \ + assert(t); \ + return FLAGS_SET(t->state, NETWORK_CONFIG_STATE_REQUESTING); \ + } \ + static inline void name##_enter_configuring(type *t) { \ + name##_update_state(t, \ + NETWORK_CONFIG_STATE_REQUESTING | \ + NETWORK_CONFIG_STATE_CONFIGURING | \ + NETWORK_CONFIG_STATE_REMOVING, \ + NETWORK_CONFIG_STATE_CONFIGURING); \ + } \ + static inline void name##_enter_configured(type *t) { \ + name##_update_state(t, \ + NETWORK_CONFIG_STATE_CONFIGURING | \ + NETWORK_CONFIG_STATE_CONFIGURED, \ + NETWORK_CONFIG_STATE_CONFIGURED); \ + } \ + static inline void name##_mark(type *t) { \ + name##_update_state(t, \ + NETWORK_CONFIG_STATE_MARKED, \ + NETWORK_CONFIG_STATE_MARKED); \ + } \ + static inline void name##_unmark(type *t) { \ + name##_update_state(t, NETWORK_CONFIG_STATE_MARKED, 0); \ + } \ + static inline bool name##_is_marked(const type *t) { \ + assert(t); \ + return FLAGS_SET(t->state, NETWORK_CONFIG_STATE_MARKED); \ + } \ + static inline void name##_enter_removing(type *t) { \ + name##_update_state(t, \ + NETWORK_CONFIG_STATE_MARKED | \ + NETWORK_CONFIG_STATE_REMOVING, \ + NETWORK_CONFIG_STATE_REMOVING); \ + } \ + static inline void name##_enter_removed(type *t) { \ + name##_update_state(t, \ + NETWORK_CONFIG_STATE_CONFIGURED | \ + NETWORK_CONFIG_STATE_REMOVING, \ + 0); \ + } + +const char *address_family_to_string(AddressFamily b) _const_; +AddressFamily address_family_from_string(const char *s) _pure_; + +AddressFamily link_local_address_family_from_string(const char *s) _pure_; + +const char *routing_policy_rule_address_family_to_string(AddressFamily b) _const_; +AddressFamily routing_policy_rule_address_family_from_string(const char *s) _pure_; + +const char *nexthop_address_family_to_string(AddressFamily b) _const_; +AddressFamily nexthop_address_family_from_string(const char *s) _pure_; + +const char *duplicate_address_detection_address_family_to_string(AddressFamily b) _const_; +AddressFamily duplicate_address_detection_address_family_from_string(const char *s) _pure_; + +AddressFamily dhcp_deprecated_address_family_from_string(const char *s) _pure_; + +const char *dhcp_lease_server_type_to_string(sd_dhcp_lease_server_type_t t) _const_; +sd_dhcp_lease_server_type_t dhcp_lease_server_type_from_string(const char *s) _pure_; + +int log_link_message_full_errno(Link *link, sd_netlink_message *m, int level, int err, const char *msg); +#define log_link_message_error_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_ERR, err, msg) +#define log_link_message_warning_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_WARNING, err, msg) +#define log_link_message_notice_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_NOTICE, err, msg) +#define log_link_message_info_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_INFO, err, msg) +#define log_link_message_debug_errno(link, m, err, msg) log_link_message_full_errno(link, m, LOG_DEBUG, err, msg) +#define log_message_full_errno(m, level, err, msg) log_link_message_full_errno(NULL, m, level, err, msg) +#define log_message_error_errno(m, err, msg) log_message_full_errno(m, LOG_ERR, err, msg) +#define log_message_warning_errno(m, err, msg) log_message_full_errno(m, LOG_WARNING, err, msg) +#define log_message_notice_errno(m, err, msg) log_message_full_errno(m, LOG_NOTICE, err, msg) +#define log_message_info_errno(m, err, msg) log_message_full_errno(m, LOG_INFO, err, msg) +#define log_message_debug_errno(m, err, msg) log_message_full_errno(m, LOG_DEBUG, err, msg) diff --git a/src/network/networkd-wifi.c b/src/network/networkd-wifi.c new file mode 100644 index 0000000..98e7a72 --- /dev/null +++ b/src/network/networkd-wifi.c @@ -0,0 +1,345 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/ethernet.h> +#include <linux/nl80211.h> + +#include "ether-addr-util.h" +#include "netlink-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-wifi.h" +#include "networkd-wiphy.h" +#include "string-util.h" +#include "wifi-util.h" + +int link_get_wlan_interface(Link *link) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + int r; + + assert(link); + + r = sd_genl_message_new(link->manager->genl, NL80211_GENL_NAME, NL80211_CMD_GET_INTERFACE, &req); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to create generic netlink message: %m"); + + r = sd_netlink_message_append_u32(req, NL80211_ATTR_IFINDEX, link->ifindex); + if (r < 0) + return log_link_debug_errno(link, r, "Could not append NL80211_ATTR_IFINDEX attribute: %m"); + + r = sd_netlink_call(link->manager->genl, req, 0, &reply); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to request information about wlan interface: %m"); + if (!reply) { + log_link_debug(link, "No reply received to request for information about wifi interface, ignoring."); + return 0; + } + + return manager_genl_process_nl80211_config(link->manager->genl, reply, link->manager); +} + +int manager_genl_process_nl80211_config(sd_netlink *genl, sd_netlink_message *message, Manager *manager) { + _cleanup_free_ char *ssid = NULL; + uint32_t ifindex, wlan_iftype; + const char *family, *ifname; + uint8_t cmd; + size_t len; + Link *link; + int r; + + assert(genl); + assert(message); + assert(manager); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "nl80211: received error message, ignoring"); + + return 0; + } + + r = sd_genl_message_get_family_name(genl, message, &family); + if (r < 0) { + log_debug_errno(r, "nl80211: failed to determine genl family, ignoring: %m"); + return 0; + } + if (!streq(family, NL80211_GENL_NAME)) { + log_debug("nl80211: received message of unexpected genl family '%s', ignoring.", family); + return 0; + } + + r = sd_genl_message_get_command(genl, message, &cmd); + if (r < 0) { + log_debug_errno(r, "nl80211: failed to determine genl message command, ignoring: %m"); + return 0; + } + if (IN_SET(cmd, NL80211_CMD_NEW_WIPHY, NL80211_CMD_DEL_WIPHY)) + return manager_genl_process_nl80211_wiphy(genl, message, manager); + if (!IN_SET(cmd, NL80211_CMD_SET_INTERFACE, NL80211_CMD_NEW_INTERFACE, NL80211_CMD_DEL_INTERFACE)) { + log_debug("nl80211: ignoring nl80211 %s(%u) message.", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + r = sd_netlink_message_read_u32(message, NL80211_ATTR_IFINDEX, &ifindex); + if (r < 0) { + log_debug_errno(r, "nl80211: received %s(%u) message without valid ifindex, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + r = link_get_by_index(manager, ifindex, &link); + if (r < 0) { + log_debug_errno(r, "nl80211: received %s(%u) message for link '%"PRIu32"' we don't know about, ignoring.", + strna(nl80211_cmd_to_string(cmd)), cmd, ifindex); + + /* The NL80211_CMD_NEW_INTERFACE message might arrive before RTM_NEWLINK, in which case a + * link will not have been created yet. Store the interface index such that the wireless + * properties of the link (such as wireless interface type) are queried again after the link + * is created. + */ + if (cmd == NL80211_CMD_NEW_INTERFACE) { + r = set_ensure_put(&manager->new_wlan_ifindices, NULL, INT_TO_PTR(ifindex)); + if (r < 0) + log_warning_errno(r, "Failed to add new wireless interface index to set, ignoring: %m"); + } else if (cmd == NL80211_CMD_DEL_INTERFACE) + set_remove(manager->new_wlan_ifindices, INT_TO_PTR(ifindex)); + + return 0; + } + + r = sd_netlink_message_read_string(message, NL80211_ATTR_IFNAME, &ifname); + if (r < 0) { + log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid interface name, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + if (!streq(ifname, link->ifname)) { + log_link_debug(link, "nl80211: received %s(%u) message with invalid interface name '%s', ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd, ifname); + return 0; + } + + r = sd_netlink_message_read_u32(message, NL80211_ATTR_IFTYPE, &wlan_iftype); + if (r < 0) { + log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid wlan interface type, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + r = sd_netlink_message_read_data_suffix0(message, NL80211_ATTR_SSID, &len, (void**) &ssid); + if (r < 0 && r != -ENODATA) { + log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid SSID, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + if (r >= 0) { + if (len == 0) { + log_link_debug(link, "nl80211: received SSID has zero length, ignoring it: %m"); + ssid = mfree(ssid); + } else if (strlen_ptr(ssid) != len) { + log_link_debug(link, "nl80211: received SSID contains NUL characters, ignoring it."); + ssid = mfree(ssid); + } + } + + log_link_debug(link, "nl80211: received %s(%u) message: iftype=%s, ssid=%s", + strna(nl80211_cmd_to_string(cmd)), cmd, + strna(nl80211_iftype_to_string(wlan_iftype)), strna(ssid)); + + switch (cmd) { + case NL80211_CMD_SET_INTERFACE: + case NL80211_CMD_NEW_INTERFACE: + link->wlan_iftype = wlan_iftype; + free_and_replace(link->ssid, ssid); + break; + + case NL80211_CMD_DEL_INTERFACE: + link->wlan_iftype = NL80211_IFTYPE_UNSPECIFIED; + link->ssid = mfree(link->ssid); + break; + + default: + assert_not_reached(); + } + + return 0; +} + +int manager_genl_process_nl80211_mlme(sd_netlink *genl, sd_netlink_message *message, Manager *manager) { + const char *family; + uint32_t ifindex; + uint8_t cmd; + Link *link; + int r; + + assert(genl); + assert(message); + assert(manager); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "nl80211: received error message, ignoring"); + + return 0; + } + + r = sd_genl_message_get_family_name(genl, message, &family); + if (r < 0) { + log_debug_errno(r, "nl80211: failed to determine genl family, ignoring: %m"); + return 0; + } + if (!streq(family, NL80211_GENL_NAME)) { + log_debug("nl80211: Received message of unexpected genl family '%s', ignoring.", family); + return 0; + } + + r = sd_genl_message_get_command(genl, message, &cmd); + if (r < 0) { + log_debug_errno(r, "nl80211: failed to determine genl message command, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, NL80211_ATTR_IFINDEX, &ifindex); + if (r < 0) { + log_debug_errno(r, "nl80211: received %s(%u) message without valid ifindex, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + r = link_get_by_index(manager, ifindex, &link); + if (r < 0) { + log_debug_errno(r, "nl80211: received %s(%u) message for link '%"PRIu32"' we don't know about, ignoring.", + strna(nl80211_cmd_to_string(cmd)), cmd, ifindex); + return 0; + } + + switch (cmd) { + case NL80211_CMD_NEW_STATION: + case NL80211_CMD_DEL_STATION: { + struct ether_addr bssid; + + r = sd_netlink_message_read_ether_addr(message, NL80211_ATTR_MAC, &bssid); + if (r < 0) { + log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid BSSID, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + log_link_debug(link, "nl80211: received %s(%u) message: bssid=%s", + strna(nl80211_cmd_to_string(cmd)), cmd, ETHER_ADDR_TO_STR(&bssid)); + + if (cmd == NL80211_CMD_DEL_STATION) { + link->bssid = ETHER_ADDR_NULL; + return 0; + } + + link->bssid = bssid; + + if (manager->enumerating && + link->wlan_iftype == NL80211_IFTYPE_STATION && link->ssid) + log_link_info(link, "Connected WiFi access point: %s (%s)", + link->ssid, ETHER_ADDR_TO_STR(&link->bssid)); + break; + } + case NL80211_CMD_CONNECT: { + struct ether_addr bssid; + uint16_t status_code; + + r = sd_netlink_message_read_ether_addr(message, NL80211_ATTR_MAC, &bssid); + if (r < 0 && r != -ENODATA) { + log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid BSSID, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + r = sd_netlink_message_read_u16(message, NL80211_ATTR_STATUS_CODE, &status_code); + if (r < 0) { + log_link_debug_errno(link, r, "nl80211: received %s(%u) message without valid status code, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + log_link_debug(link, "nl80211: received %s(%u) message: status=%u, bssid=%s", + strna(nl80211_cmd_to_string(cmd)), cmd, status_code, ETHER_ADDR_TO_STR(&bssid)); + + if (status_code != 0) + return 0; + + link->bssid = bssid; + + if (!manager->enumerating) { + r = link_get_wlan_interface(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to update wireless LAN interface: %m"); + link_enter_failed(link); + return 0; + } + } + + if (link->wlan_iftype == NL80211_IFTYPE_STATION && link->ssid) + log_link_info(link, "Connected WiFi access point: %s (%s)", + link->ssid, ETHER_ADDR_TO_STR(&link->bssid)); + + /* Sometimes, RTM_NEWLINK message with carrier is received earlier than NL80211_CMD_CONNECT. + * To make SSID= or other WiFi related settings in [Match] section work, let's try to + * reconfigure the interface. */ + if (link->ssid && link_has_carrier(link)) { + r = link_reconfigure_impl(link, /* force = */ false); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to reconfigure interface: %m"); + link_enter_failed(link); + return 0; + } + } + break; + } + case NL80211_CMD_DISCONNECT: + log_link_debug(link, "nl80211: received %s(%u) message.", + strna(nl80211_cmd_to_string(cmd)), cmd); + + link->bssid = ETHER_ADDR_NULL; + free_and_replace(link->previous_ssid, link->ssid); + break; + + case NL80211_CMD_START_AP: { + log_link_debug(link, "nl80211: received %s(%u) message.", + strna(nl80211_cmd_to_string(cmd)), cmd); + + /* No need to reconfigure during enumeration */ + if (manager->enumerating) + break; + + /* If there is no carrier, let the link get configured on + * carrier gain instead */ + if (!link_has_carrier(link)) + break; + + /* AP start event may indicate different properties (e.g. SSID) */ + r = link_get_wlan_interface(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to update wireless LAN interface: %m"); + link_enter_failed(link); + return 0; + } + + /* If necessary, reconfigure based on those new properties */ + r = link_reconfigure_impl(link, /* force = */ false); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to reconfigure interface: %m"); + link_enter_failed(link); + return 0; + } + + break; + } + + default: + log_link_debug(link, "nl80211: received %s(%u) message.", + strna(nl80211_cmd_to_string(cmd)), cmd); + } + + return 0; +} diff --git a/src/network/networkd-wifi.h b/src/network/networkd-wifi.h new file mode 100644 index 0000000..2ef0d30 --- /dev/null +++ b/src/network/networkd-wifi.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-netlink.h" + +typedef struct Link Link; +typedef struct Manager Manager; + +int manager_genl_process_nl80211_config(sd_netlink *genl, sd_netlink_message *message, Manager *manager); +int manager_genl_process_nl80211_mlme(sd_netlink *genl, sd_netlink_message *message, Manager *manager); +int link_get_wlan_interface(Link *link); diff --git a/src/network/networkd-wiphy.c b/src/network/networkd-wiphy.c new file mode 100644 index 0000000..13f2d72 --- /dev/null +++ b/src/network/networkd-wiphy.c @@ -0,0 +1,495 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if_arp.h> +#include <linux/nl80211.h> + +#include "device-private.h" +#include "device-util.h" +#include "networkd-manager.h" +#include "networkd-wiphy.h" +#include "parse-util.h" +#include "path-util.h" +#include "udev-util.h" +#include "wifi-util.h" + +Wiphy *wiphy_free(Wiphy *w) { + if (!w) + return NULL; + + if (w->manager) { + hashmap_remove_value(w->manager->wiphy_by_index, UINT32_TO_PTR(w->index), w); + if (w->name) + hashmap_remove_value(w->manager->wiphy_by_name, w->name, w); + } + + sd_device_unref(w->dev); + sd_device_unref(w->rfkill); + + free(w->name); + return mfree(w); +} + +static int wiphy_new(Manager *manager, sd_netlink_message *message, Wiphy **ret) { + _cleanup_(wiphy_freep) Wiphy *w = NULL; + _cleanup_free_ char *name = NULL; + uint32_t index; + int r; + + assert(manager); + assert(message); + + r = sd_netlink_message_read_u32(message, NL80211_ATTR_WIPHY, &index); + if (r < 0) + return r; + + r = sd_netlink_message_read_string_strdup(message, NL80211_ATTR_WIPHY_NAME, &name); + if (r < 0) + return r; + + w = new(Wiphy, 1); + if (!w) + return -ENOMEM; + + *w = (Wiphy) { + .manager = manager, + .index = index, + .name = TAKE_PTR(name), + }; + + r = hashmap_ensure_put(&manager->wiphy_by_index, NULL, UINT32_TO_PTR(w->index), w); + if (r < 0) + return r; + + r = hashmap_ensure_put(&w->manager->wiphy_by_name, &string_hash_ops, w->name, w); + if (r < 0) + return r; + + log_wiphy_debug(w, "Saved new wiphy: index=%"PRIu32, w->index); + + if (ret) + *ret = w; + + TAKE_PTR(w); + return 0; +} + +int wiphy_get_by_index(Manager *manager, uint32_t index, Wiphy **ret) { + Wiphy *w; + + assert(manager); + + w = hashmap_get(manager->wiphy_by_index, UINT32_TO_PTR(index)); + if (!w) + return -ENODEV; + + if (ret) + *ret = w; + + return 0; +} + +int wiphy_get_by_name(Manager *manager, const char *name, Wiphy **ret) { + Wiphy *w; + + assert(manager); + assert(name); + + w = hashmap_get(manager->wiphy_by_name, name); + if (!w) + return -ENODEV; + + if (ret) + *ret = w; + + return 0; +} + +static int link_get_wiphy(Link *link, Wiphy **ret) { + _cleanup_(sd_device_unrefp) sd_device *phy = NULL; + const char *s; + int r; + + assert(link); + assert(link->manager); + + if (link->iftype != ARPHRD_ETHER) + return -EOPNOTSUPP; + + if (!link->dev) + return -ENODEV; + + r = sd_device_get_devtype(link->dev, &s); + if (r < 0) + return r; + + if (!streq_ptr(s, "wlan")) + return -EOPNOTSUPP; + + r = sd_device_new_child(&phy, link->dev, "phy80211"); + if (r < 0) + return r; + + r = sd_device_get_sysname(phy, &s); + if (r < 0) + return r; + + /* TODO: + * Maybe, it is better to cache the found Wiphy object in the Link object. + * To support that, we need to investigate what happens when the _phy_ is renamed. */ + + return wiphy_get_by_name(link->manager, s, ret); +} + +static int rfkill_get_state(sd_device *dev) { + int r; + + assert(dev); + + /* The previous values may be outdated. Let's clear cache and re-read the values. */ + device_clear_sysattr_cache(dev); + + r = device_get_sysattr_bool(dev, "soft"); + if (r < 0 && r != -ENOENT) + return r; + if (r > 0) + return RFKILL_SOFT; + + r = device_get_sysattr_bool(dev, "hard"); + if (r < 0 && r != -ENOENT) + return r; + if (r > 0) + return RFKILL_HARD; + + return RFKILL_UNBLOCKED; +} + +static int wiphy_rfkilled(Wiphy *w) { + int r; + + assert(w); + + if (!udev_available()) { + if (w->rfkill_state != RFKILL_UNBLOCKED) { + log_wiphy_debug(w, "Running in container, assuming the radio transmitter is unblocked."); + w->rfkill_state = RFKILL_UNBLOCKED; /* To suppress the above log message, cache the state. */ + } + return false; + } + + if (!w->rfkill) { + if (w->rfkill_state != RFKILL_UNBLOCKED) { + log_wiphy_debug(w, "No rfkill device found, assuming the radio transmitter is unblocked."); + w->rfkill_state = RFKILL_UNBLOCKED; /* To suppress the above log message, cache the state. */ + } + return false; + } + + r = rfkill_get_state(w->rfkill); + if (r < 0) + return log_wiphy_debug_errno(w, r, "Could not get rfkill state: %m"); + + if (w->rfkill_state != r) + switch (r) { + case RFKILL_UNBLOCKED: + log_wiphy_debug(w, "The radio transmitter is unblocked."); + break; + case RFKILL_SOFT: + log_wiphy_debug(w, "The radio transmitter is turned off by software."); + break; + case RFKILL_HARD: + log_wiphy_debug(w, "The radio transmitter is forced off by something outside of the driver's control."); + break; + default: + assert_not_reached(); + } + + w->rfkill_state = r; /* Cache the state to suppress the above log messages. */ + return r != RFKILL_UNBLOCKED; +} + +int link_rfkilled(Link *link) { + Wiphy *w; + int r; + + assert(link); + + r = link_get_wiphy(link, &w); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r) || ERRNO_IS_NEG_DEVICE_ABSENT(r)) + return false; /* Typically, non-wifi interface or running in container */ + if (r < 0) + return log_link_debug_errno(link, r, "Could not get phy: %m"); + + return wiphy_rfkilled(w); +} + +static int wiphy_update_name(Wiphy *w, sd_netlink_message *message) { + const char *name; + int r; + + assert(w); + assert(w->manager); + assert(message); + + r = sd_netlink_message_read_string(message, NL80211_ATTR_WIPHY_NAME, &name); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + if (streq(w->name, name)) + return 0; + + log_wiphy_debug(w, "Wiphy name change detected, renamed to %s.", name); + + hashmap_remove_value(w->manager->wiphy_by_name, w->name, w); + + r = free_and_strdup(&w->name, name); + if (r < 0) + return r; + + r = hashmap_ensure_put(&w->manager->wiphy_by_name, &string_hash_ops, w->name, w); + if (r < 0) + return r; + + return 1; /* updated */ +} + +static int wiphy_update_device(Wiphy *w) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + int r; + + assert(w); + assert(w->name); + + if (!udev_available()) + return 0; + + w->dev = sd_device_unref(w->dev); + + r = sd_device_new_from_subsystem_sysname(&dev, "ieee80211", w->name); + if (r < 0) + return r; + + if (DEBUG_LOGGING) { + const char *s = NULL; + + (void) sd_device_get_syspath(dev, &s); + log_wiphy_debug(w, "Found device: %s", strna(s)); + } + + w->dev = TAKE_PTR(dev); + return 0; +} + +static int wiphy_update_rfkill(Wiphy *w) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + sd_device *rfkill; + int r; + + assert(w); + + if (!udev_available()) + return 0; + + w->rfkill = sd_device_unref(w->rfkill); + + if (!w->dev) + return 0; + + r = sd_device_enumerator_new(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_allow_uninitialized(e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_subsystem(e, "rfkill", true); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_parent(e, w->dev); + if (r < 0) + return r; + + rfkill = sd_device_enumerator_get_device_first(e); + if (!rfkill) + /* rfkill device may not detected by the kernel yet, and may appear later. */ + return -ENODEV; + + if (sd_device_enumerator_get_device_next(e)) + return -ENXIO; /* multiple devices found */ + + w->rfkill = sd_device_ref(rfkill); + + if (DEBUG_LOGGING) { + const char *s = NULL; + + (void) sd_device_get_syspath(rfkill, &s); + log_wiphy_debug(w, "Found rfkill device: %s", strna(s)); + } + + return 0; +} + +static int wiphy_update(Wiphy *w) { + int r; + + assert(w); + + r = wiphy_update_device(w); + if (ERRNO_IS_NEG_DEVICE_ABSENT(r)) + log_wiphy_debug_errno(w, r, "Failed to update wiphy device, ignoring: %m"); + else if (r < 0) + return log_wiphy_warning_errno(w, r, "Failed to update wiphy device: %m"); + + r = wiphy_update_rfkill(w); + if (ERRNO_IS_NEG_DEVICE_ABSENT(r)) + log_wiphy_debug_errno(w, r, "Failed to update rfkill device, ignoring: %m"); + else if (r < 0) + return log_wiphy_warning_errno(w, r, "Failed to update rfkill device: %m"); + + return 0; +} + +int manager_genl_process_nl80211_wiphy(sd_netlink *genl, sd_netlink_message *message, Manager *manager) { + const char *family; + uint32_t index; + uint8_t cmd; + Wiphy *w = NULL; + int r; + + assert(genl); + assert(message); + assert(manager); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "nl80211: received error message, ignoring"); + + return 0; + } + + r = sd_genl_message_get_family_name(genl, message, &family); + if (r < 0) { + log_debug_errno(r, "nl80211: failed to determine genl family, ignoring: %m"); + return 0; + } + if (!streq(family, NL80211_GENL_NAME)) { + log_debug("nl80211: Received message of unexpected genl family '%s', ignoring.", family); + return 0; + } + + r = sd_genl_message_get_command(genl, message, &cmd); + if (r < 0) { + log_debug_errno(r, "nl80211: failed to determine genl message command, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_u32(message, NL80211_ATTR_WIPHY, &index); + if (r < 0) { + log_debug_errno(r, "nl80211: received %s(%u) message without valid index, ignoring: %m", + strna(nl80211_cmd_to_string(cmd)), cmd); + return 0; + } + + (void) wiphy_get_by_index(manager, index, &w); + + switch (cmd) { + case NL80211_CMD_NEW_WIPHY: { + + if (!w) { + r = wiphy_new(manager, message, &w); + if (r < 0) { + log_warning_errno(r, "Failed to save new wiphy, ignoring: %m"); + return 0; + } + } else { + r = wiphy_update_name(w, message); + if (r < 0) { + log_wiphy_warning_errno(w, r, "Failed to update wiphy name, ignoring: %m"); + return 0; + } + if (r == 0) + return 0; + } + + r = wiphy_update(w); + if (r < 0) + log_wiphy_warning_errno(w, r, "Failed to update wiphy, ignoring: %m"); + + break; + } + case NL80211_CMD_DEL_WIPHY: + + if (!w) { + log_debug("The kernel removes wiphy we do not know, ignoring: %m"); + return 0; + } + + log_wiphy_debug(w, "Removed."); + wiphy_free(w); + break; + + default: + log_wiphy_debug(w, "nl80211: received %s(%u) message.", + strna(nl80211_cmd_to_string(cmd)), cmd); + } + + return 0; +} + +int manager_udev_process_wiphy(Manager *m, sd_device *device, sd_device_action_t action) { + const char *name; + Wiphy *w; + int r; + + assert(m); + assert(device); + + r = sd_device_get_sysname(device, &name); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to get sysname: %m"); + + r = wiphy_get_by_name(m, name, &w); + if (r < 0) { + /* This error is not critical, as the corresponding genl message may be received later. */ + log_device_debug_errno(device, r, "Failed to get Wiphy object, ignoring: %m"); + return 0; + } + + return device_unref_and_replace(w->dev, action == SD_DEVICE_REMOVE ? NULL : device); +} + +int manager_udev_process_rfkill(Manager *m, sd_device *device, sd_device_action_t action) { + _cleanup_free_ char *parent_path = NULL, *parent_name = NULL; + const char *s; + Wiphy *w; + int r; + + assert(m); + assert(device); + + r = sd_device_get_syspath(device, &s); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to get syspath: %m"); + + /* Do not use sd_device_get_parent() here, as this might be a 'remove' uevent. */ + r = path_extract_directory(s, &parent_path); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to get parent syspath: %m"); + + r = path_extract_filename(parent_path, &parent_name); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to get parent name: %m"); + + r = wiphy_get_by_name(m, parent_name, &w); + if (r < 0) { + /* This error is not critical, as the corresponding genl message may be received later. */ + log_device_debug_errno(device, r, "Failed to get Wiphy object: %m"); + return 0; + } + + return device_unref_and_replace(w->rfkill, action == SD_DEVICE_REMOVE ? NULL : device); +} diff --git a/src/network/networkd-wiphy.h b/src/network/networkd-wiphy.h new file mode 100644 index 0000000..b9056e8 --- /dev/null +++ b/src/network/networkd-wiphy.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> + +#include "sd-device.h" + +#include "macro.h" + +typedef struct Link Link; +typedef struct Manager Manager; + +/* The following values are different from the ones defined in linux/rfkill.h. */ +typedef enum RFKillState { + RFKILL_UNKNOWN, + RFKILL_UNBLOCKED, + RFKILL_SOFT, + RFKILL_HARD, + _RFKILL_STATE_MAX, + _RFKILL_STATE_INVALID = -EINVAL, +} RFKillState; + +typedef struct Wiphy { + Manager *manager; + + uint32_t index; + char *name; + + sd_device *dev; + sd_device *rfkill; + RFKillState rfkill_state; +} Wiphy; + +Wiphy *wiphy_free(Wiphy *w); +DEFINE_TRIVIAL_CLEANUP_FUNC(Wiphy*, wiphy_free); + +int wiphy_get_by_index(Manager *manager, uint32_t index, Wiphy **ret); +int wiphy_get_by_name(Manager *manager, const char *name, Wiphy **ret); + +int link_rfkilled(Link *link); + +int manager_genl_process_nl80211_wiphy(sd_netlink *genl, sd_netlink_message *message, Manager *manager); +int manager_udev_process_wiphy(Manager *m, sd_device *device, sd_device_action_t action); +int manager_udev_process_rfkill(Manager *m, sd_device *device, sd_device_action_t action); + +#define log_wiphy_full_errno_zerook(w, level, error, ...) \ + ({ \ + const Wiphy *_w = (w); \ + log_interface_full_errno_zerook(_w ? _w->name : NULL, level, error, __VA_ARGS__); \ + }) + +#define log_wiphy_full_errno(w, level, error, ...) \ + ({ \ + int _error = (error); \ + ASSERT_NON_ZERO(_error); \ + log_wiphy_full_errno_zerook(w, level, _error, __VA_ARGS__); \ + }) + +#define log_wiphy_full(w, level, ...) (void) log_wiphy_full_errno_zerook(w, level, 0, __VA_ARGS__) + +#define log_wiphy_debug(w, ...) log_wiphy_full(w, LOG_DEBUG, __VA_ARGS__) +#define log_wiphy_info(w, ...) log_wiphy_full(w, LOG_INFO, __VA_ARGS__) +#define log_wiphy_notice(w, ...) log_wiphy_full(w, LOG_NOTICE, __VA_ARGS__) +#define log_wiphy_warning(w, ...) log_wiphy_full(w, LOG_WARNING, __VA_ARGS__) +#define log_wiphy_error(w, ...) log_wiphy_full(w, LOG_ERR, __VA_ARGS__) + +#define log_wiphy_debug_errno(w, error, ...) log_wiphy_full_errno(w, LOG_DEBUG, error, __VA_ARGS__) +#define log_wiphy_info_errno(w, error, ...) log_wiphy_full_errno(w, LOG_INFO, error, __VA_ARGS__) +#define log_wiphy_notice_errno(w, error, ...) log_wiphy_full_errno(w, LOG_NOTICE, error, __VA_ARGS__) +#define log_wiphy_warning_errno(w, error, ...) log_wiphy_full_errno(w, LOG_WARNING, error, __VA_ARGS__) +#define log_wiphy_error_errno(w, error, ...) log_wiphy_full_errno(w, LOG_ERR, error, __VA_ARGS__) diff --git a/src/network/networkd.c b/src/network/networkd.c new file mode 100644 index 0000000..46c2c74 --- /dev/null +++ b/src/network/networkd.c @@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include "sd-daemon.h" +#include "sd-event.h" + +#include "bus-log-control-api.h" +#include "capability-util.h" +#include "daemon-util.h" +#include "firewall-util.h" +#include "main-func.h" +#include "mkdir-label.h" +#include "networkd-conf.h" +#include "networkd-manager-bus.h" +#include "networkd-manager.h" +#include "service-util.h" +#include "signal-util.h" +#include "user-util.h" + +static int run(int argc, char *argv[]) { + _cleanup_(manager_freep) Manager *m = NULL; + _unused_ _cleanup_(notify_on_cleanup) const char *notify_message = NULL; + int r; + + log_setup(); + + r = service_parse_argv("systemd-networkd.service", + "Manage and configure network devices, create virtual network devices", + BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), + argc, argv); + if (r <= 0) + return r; + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + /* Drop privileges, but only if we have been started as root. If we are not running as root we assume all + * privileges are already dropped and we can't create our runtime directory. */ + if (geteuid() == 0) { + const char *user = "systemd-network"; + uid_t uid; + gid_t gid; + + r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0); + if (r < 0) + return log_error_errno(r, "Cannot resolve user name %s: %m", user); + + /* Create runtime directory. This is not necessary when networkd is + * started with "RuntimeDirectory=systemd/netif", or after + * systemd-tmpfiles-setup.service. */ + r = mkdir_safe_label("/run/systemd/netif", 0755, uid, gid, MKDIR_WARN_MODE); + if (r < 0) + log_warning_errno(r, "Could not create runtime directory: %m"); + + r = drop_privileges(uid, gid, + (1ULL << CAP_NET_ADMIN) | + (1ULL << CAP_NET_BIND_SERVICE) | + (1ULL << CAP_NET_BROADCAST) | + (1ULL << CAP_NET_RAW)); + if (r < 0) + return log_error_errno(r, "Failed to drop privileges: %m"); + } + + /* Always create the directories people can create inotify watches in. + * It is necessary to create the following subdirectories after drop_privileges() + * to support old kernels not supporting AmbientCapabilities=. */ + r = mkdir_safe_label("/run/systemd/netif/links", 0755, UID_INVALID, GID_INVALID, MKDIR_WARN_MODE); + if (r < 0) + log_warning_errno(r, "Could not create runtime directory 'links': %m"); + + r = mkdir_safe_label("/run/systemd/netif/leases", 0755, UID_INVALID, GID_INVALID, MKDIR_WARN_MODE); + if (r < 0) + log_warning_errno(r, "Could not create runtime directory 'leases': %m"); + + r = mkdir_safe_label("/run/systemd/netif/lldp", 0755, UID_INVALID, GID_INVALID, MKDIR_WARN_MODE); + if (r < 0) + log_warning_errno(r, "Could not create runtime directory 'lldp': %m"); + + r = manager_new(&m, /* test_mode = */ false); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_setup(m); + if (r < 0) + return log_error_errno(r, "Could not set up manager: %m"); + + r = manager_parse_config_file(m); + if (r < 0) + log_warning_errno(r, "Failed to parse configuration file: %m"); + + r = manager_load_config(m); + if (r < 0) + return log_error_errno(r, "Could not load configuration files: %m"); + + r = manager_enumerate(m); + if (r < 0) + return r; + + r = manager_start(m); + if (r < 0) + return log_error_errno(r, "Could not start manager: %m"); + + log_info("Enumeration completed"); + + notify_message = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/network/networkd.conf b/src/network/networkd.conf new file mode 100644 index 0000000..e5a5e88 --- /dev/null +++ b/src/network/networkd.conf @@ -0,0 +1,33 @@ +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Entries in this file show the compile time defaults. Local configuration +# should be created by either modifying this file (or a copy of it placed in +# /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in +# the /etc/systemd/networkd.conf.d/ directory. The latter is generally +# recommended. Defaults can be restored by simply deleting the main +# configuration file and all drop-ins located in /etc/. +# +# Use 'systemd-analyze cat-config systemd/networkd.conf' to display the full config. +# +# See networkd.conf(5) for details. + +[Network] +#SpeedMeter=no +#SpeedMeterIntervalSec=10sec +#ManageForeignRoutingPolicyRules=yes +#ManageForeignRoutes=yes +#RouteTable= +#IPv6PrivacyExtensions=no + +[DHCPv4] +#DUIDType=vendor +#DUIDRawData= + +[DHCPv6] +#DUIDType=vendor +#DUIDRawData= diff --git a/src/network/org.freedesktop.network1.conf b/src/network/org.freedesktop.network1.conf new file mode 100644 index 0000000..5bd796d --- /dev/null +++ b/src/network/org.freedesktop.network1.conf @@ -0,0 +1,27 @@ +<?xml version="1.0"?> <!--*-nxml-*--> +<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> + +<!-- + This file is part of systemd. + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. +--> + +<busconfig> + + <policy user="systemd-network"> + <allow own="org.freedesktop.network1"/> + <allow send_destination="org.freedesktop.network1"/> + <allow receive_sender="org.freedesktop.network1"/> + </policy> + + <policy context="default"> + <allow send_destination="org.freedesktop.network1"/> + <allow receive_sender="org.freedesktop.network1"/> + </policy> + +</busconfig> diff --git a/src/network/org.freedesktop.network1.policy b/src/network/org.freedesktop.network1.policy new file mode 100644 index 0000000..1e2d8d7 --- /dev/null +++ b/src/network/org.freedesktop.network1.policy @@ -0,0 +1,186 @@ +<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*--> +<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" + "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> + +<!-- + SPDX-License-Identifier: LGPL-2.1-or-later + + This file is part of systemd. + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. +--> + +<policyconfig> + + <vendor>The systemd Project</vendor> + <vendor_url>https://systemd.io</vendor_url> + + <action id="org.freedesktop.network1.set-ntp-servers"> + <description gettext-domain="systemd">Set NTP servers</description> + <message gettext-domain="systemd">Authentication is required to set NTP servers.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.set-dns-servers"> + <description gettext-domain="systemd">Set DNS servers</description> + <message gettext-domain="systemd">Authentication is required to set DNS servers.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.set-domains"> + <description gettext-domain="systemd">Set domains</description> + <message gettext-domain="systemd">Authentication is required to set domains.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.set-default-route"> + <description gettext-domain="systemd">Set default route</description> + <message gettext-domain="systemd">Authentication is required to set default route.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.set-llmnr"> + <description gettext-domain="systemd">Enable/disable LLMNR</description> + <message gettext-domain="systemd">Authentication is required to enable or disable LLMNR.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.set-mdns"> + <description gettext-domain="systemd">Enable/disable multicast DNS</description> + <message gettext-domain="systemd">Authentication is required to enable or disable multicast DNS.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.set-dns-over-tls"> + <description gettext-domain="systemd">Enable/disable DNS over TLS</description> + <message gettext-domain="systemd">Authentication is required to enable or disable DNS over TLS.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.set-dnssec"> + <description gettext-domain="systemd">Enable/disable DNSSEC</description> + <message gettext-domain="systemd">Authentication is required to enable or disable DNSSEC.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.set-dnssec-negative-trust-anchors"> + <description gettext-domain="systemd">Set DNSSEC Negative Trust Anchors</description> + <message gettext-domain="systemd">Authentication is required to set DNSSEC Negative Trust Anchors.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.revert-ntp"> + <description gettext-domain="systemd">Revert NTP settings</description> + <message gettext-domain="systemd">Authentication is required to reset NTP settings.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.revert-dns"> + <description gettext-domain="systemd">Revert DNS settings</description> + <message gettext-domain="systemd">Authentication is required to reset DNS settings.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.forcerenew"> + <description gettext-domain="systemd">DHCP server sends force renew message</description> + <message gettext-domain="systemd">Authentication is required to send force renew message.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.renew"> + <description gettext-domain="systemd">Renew dynamic addresses</description> + <message gettext-domain="systemd">Authentication is required to renew dynamic addresses.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.reload"> + <description gettext-domain="systemd">Reload network settings</description> + <message gettext-domain="systemd">Authentication is required to reload network settings.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + + <action id="org.freedesktop.network1.reconfigure"> + <description gettext-domain="systemd">Reconfigure network interface</description> + <message gettext-domain="systemd">Authentication is required to reconfigure network interface.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-network</annotate> + </action> + +</policyconfig> diff --git a/src/network/org.freedesktop.network1.service b/src/network/org.freedesktop.network1.service new file mode 100644 index 0000000..ddbf3eb --- /dev/null +++ b/src/network/org.freedesktop.network1.service @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[D-BUS Service] +Name=org.freedesktop.network1 +Exec=/bin/false +User=root +SystemdService=dbus-org.freedesktop.network1.service diff --git a/src/network/systemd-networkd.pkla b/src/network/systemd-networkd.pkla new file mode 100644 index 0000000..c56ea1b --- /dev/null +++ b/src/network/systemd-networkd.pkla @@ -0,0 +1,7 @@ +# This file is part of systemd. +# See systemd-networkd.service(8) and polkit(8) for more information. + +[Allow systemd-networkd to set timezone and transient hostname] +Identity=unix-user:systemd-network +Action=org.freedesktop.hostname1.set-hostname;org.freedesktop.hostname1.get-product-uuid;org.freedesktop.timedate1.set-timezone; +ResultAny=yes diff --git a/src/network/systemd-networkd.rules b/src/network/systemd-networkd.rules new file mode 100644 index 0000000..86cc849 --- /dev/null +++ b/src/network/systemd-networkd.rules @@ -0,0 +1,13 @@ +// This file is part of systemd. +// See systemd-networkd.service(8) and polkit(8) for more information. + +// Allow systemd-networkd to set timezone, get product UUID, +// and transient hostname +polkit.addRule(function(action, subject) { + if ((action.id == "org.freedesktop.hostname1.set-hostname" || + action.id == "org.freedesktop.hostname1.get-product-uuid" || + action.id == "org.freedesktop.timedate1.set-timezone") && + subject.user == "systemd-network") { + return polkit.Result.YES; + } +}); diff --git a/src/network/tc/cake.c b/src/network/tc/cake.c new file mode 100644 index 0000000..c495faf --- /dev/null +++ b/src/network/tc/cake.c @@ -0,0 +1,737 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "cake.h" +#include "conf-parser.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "qdisc.h" +#include "string-table.h" +#include "string-util.h" + +static int cake_init(QDisc *qdisc) { + CommonApplicationsKeptEnhanced *c; + + assert(qdisc); + + c = CAKE(qdisc); + + c->autorate = -1; + c->compensation_mode = _CAKE_COMPENSATION_MODE_INVALID; + c->raw = -1; + c->flow_isolation_mode = _CAKE_FLOW_ISOLATION_MODE_INVALID; + c->nat = -1; + c->preset = _CAKE_PRESET_INVALID; + c->wash = -1; + c->split_gso = -1; + c->ack_filter = _CAKE_ACK_FILTER_INVALID; + + return 0; +} + +static int cake_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + CommonApplicationsKeptEnhanced *c; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(c = CAKE(qdisc)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "cake"); + if (r < 0) + return r; + + if (c->bandwidth > 0) { + r = sd_netlink_message_append_u64(req, TCA_CAKE_BASE_RATE64, c->bandwidth); + if (r < 0) + return r; + } + + if (c->autorate >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_AUTORATE, c->autorate); + if (r < 0) + return r; + } + + if (c->overhead_set) { + r = sd_netlink_message_append_s32(req, TCA_CAKE_OVERHEAD, c->overhead); + if (r < 0) + return r; + } + + if (c->mpu > 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_MPU, c->mpu); + if (r < 0) + return r; + } + + if (c->compensation_mode >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_ATM, c->compensation_mode); + if (r < 0) + return r; + } + + if (c->raw > 0) { + /* TCA_CAKE_RAW attribute is mostly a flag, not boolean. */ + r = sd_netlink_message_append_u32(req, TCA_CAKE_RAW, 0); + if (r < 0) + return r; + } + + if (c->flow_isolation_mode >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_FLOW_MODE, c->flow_isolation_mode); + if (r < 0) + return r; + } + + if (c->nat >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_NAT, c->nat); + if (r < 0) + return r; + } + + if (c->preset >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_DIFFSERV_MODE, c->preset); + if (r < 0) + return r; + } + + if (c->fwmark > 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_FWMARK, c->fwmark); + if (r < 0) + return r; + } + + if (c->wash >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_WASH, c->wash); + if (r < 0) + return r; + } + + if (c->split_gso >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_SPLIT_GSO, c->split_gso); + if (r < 0) + return r; + } + + if (c->rtt > 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_RTT, c->rtt); + if (r < 0) + return r; + } + + if (c->ack_filter >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CAKE_ACK_FILTER, c->ack_filter); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_cake_bandwidth( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + Network *network = ASSERT_PTR(data); + uint64_t k; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->bandwidth = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_size(rvalue, 1000, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->bandwidth = k/8; + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_cake_overhead( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + Network *network = ASSERT_PTR(data); + int32_t v; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->overhead_set = false; + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atoi32(rvalue, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (v < -64 || v > 256) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->overhead = v; + c->overhead_set = true; + TAKE_PTR(qdisc); + return 0; +} + +int config_parse_cake_mpu( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + Network *network = ASSERT_PTR(data); + uint32_t v; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->mpu = 0; + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (v <= 0 || v > 256) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->mpu = v; + TAKE_PTR(qdisc); + return 0; +} + +int config_parse_cake_tristate( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + Network *network = ASSERT_PTR(data); + int *dest, r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (streq(lvalue, "AutoRateIngress")) + dest = &c->autorate; + else if (streq(lvalue, "UseRawPacketSize")) + dest = &c->raw; + else if (streq(lvalue, "NAT")) + dest = &c->nat; + else if (streq(lvalue, "Wash")) + dest = &c->wash; + else if (streq(lvalue, "SplitGSO")) + dest = &c->split_gso; + else + assert_not_reached(); + + r = parse_tristate(rvalue, dest); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + return 0; +} + +static const char * const cake_compensation_mode_table[_CAKE_COMPENSATION_MODE_MAX] = { + [CAKE_COMPENSATION_MODE_NONE] = "none", + [CAKE_COMPENSATION_MODE_ATM] = "atm", + [CAKE_COMPENSATION_MODE_PTM] = "ptm", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(cake_compensation_mode, CakeCompensationMode); + +int config_parse_cake_compensation_mode( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + Network *network = ASSERT_PTR(data); + CakeCompensationMode mode; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->compensation_mode = _CAKE_COMPENSATION_MODE_INVALID; + TAKE_PTR(qdisc); + return 0; + } + + mode = cake_compensation_mode_from_string(rvalue); + if (mode < 0) { + log_syntax(unit, LOG_WARNING, filename, line, mode, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->compensation_mode = mode; + TAKE_PTR(qdisc); + return 0; +} + +static const char * const cake_flow_isolation_mode_table[_CAKE_FLOW_ISOLATION_MODE_MAX] = { + [CAKE_FLOW_ISOLATION_MODE_NONE] = "none", + [CAKE_FLOW_ISOLATION_MODE_SRC_IP] = "src-host", + [CAKE_FLOW_ISOLATION_MODE_DST_IP] = "dst-host", + [CAKE_FLOW_ISOLATION_MODE_HOSTS] = "hosts", + [CAKE_FLOW_ISOLATION_MODE_FLOWS] = "flows", + [CAKE_FLOW_ISOLATION_MODE_DUAL_SRC] = "dual-src-host", + [CAKE_FLOW_ISOLATION_MODE_DUAL_DST] = "dual-dst-host", + [CAKE_FLOW_ISOLATION_MODE_TRIPLE] = "triple", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(cake_flow_isolation_mode, CakeFlowIsolationMode); + +int config_parse_cake_flow_isolation_mode( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + Network *network = ASSERT_PTR(data); + CakeFlowIsolationMode mode; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->flow_isolation_mode = _CAKE_FLOW_ISOLATION_MODE_INVALID; + TAKE_PTR(qdisc); + return 0; + } + + mode = cake_flow_isolation_mode_from_string(rvalue); + if (mode < 0) { + log_syntax(unit, LOG_WARNING, filename, line, mode, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->flow_isolation_mode = mode; + TAKE_PTR(qdisc); + return 0; +} + +static const char * const cake_priority_queueing_preset_table[_CAKE_PRESET_MAX] = { + [CAKE_PRESET_DIFFSERV3] = "diffserv3", + [CAKE_PRESET_DIFFSERV4] = "diffserv4", + [CAKE_PRESET_DIFFSERV8] = "diffserv8", + [CAKE_PRESET_BESTEFFORT] = "besteffort", + [CAKE_PRESET_PRECEDENCE] = "precedence", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(cake_priority_queueing_preset, CakePriorityQueueingPreset); + +int config_parse_cake_priority_queueing_preset( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + CakePriorityQueueingPreset preset; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->preset = _CAKE_PRESET_INVALID; + TAKE_PTR(qdisc); + return 0; + } + + preset = cake_priority_queueing_preset_from_string(rvalue); + if (preset < 0) { + log_syntax(unit, LOG_WARNING, filename, line, preset, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->preset = preset; + TAKE_PTR(qdisc); + return 0; +} + +int config_parse_cake_fwmark( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + Network *network = ASSERT_PTR(data); + uint32_t fwmark; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->fwmark = 0; + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, &fwmark); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (fwmark <= 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->fwmark = fwmark; + TAKE_PTR(qdisc); + return 0; +} + +int config_parse_cake_rtt( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + CommonApplicationsKeptEnhanced *c; + Network *network = ASSERT_PTR(data); + usec_t t; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->rtt = 0; + TAKE_PTR(qdisc); + return 0; + } + + r = parse_sec(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 <= 0 || t > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->rtt = t; + TAKE_PTR(qdisc); + return 0; +} + +static const char * const cake_ack_filter_table[_CAKE_ACK_FILTER_MAX] = { + [CAKE_ACK_FILTER_NO] = "no", + [CAKE_ACK_FILTER_YES] = "yes", + [CAKE_ACK_FILTER_AGGRESSIVE] = "aggressive", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(cake_ack_filter, CakeAckFilter, CAKE_ACK_FILTER_YES); + +int config_parse_cake_ack_filter( + const char *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; + CakeAckFilter ack_filter; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CAKE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + c = CAKE(qdisc); + + if (isempty(rvalue)) { + c->ack_filter = _CAKE_ACK_FILTER_INVALID; + TAKE_PTR(qdisc); + return 0; + } + + ack_filter = cake_ack_filter_from_string(rvalue); + if (ack_filter < 0) { + log_syntax(unit, LOG_WARNING, filename, line, ack_filter, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + c->ack_filter = ack_filter; + TAKE_PTR(qdisc); + return 0; +} + +const QDiscVTable cake_vtable = { + .object_size = sizeof(CommonApplicationsKeptEnhanced), + .tca_kind = "cake", + .init = cake_init, + .fill_message = cake_fill_message, +}; diff --git a/src/network/tc/cake.h b/src/network/tc/cake.h new file mode 100644 index 0000000..5ca6dc6 --- /dev/null +++ b/src/network/tc/cake.h @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include <linux/pkt_sched.h> + +#include "conf-parser.h" +#include "qdisc.h" + +typedef enum CakeCompensationMode { + CAKE_COMPENSATION_MODE_NONE = CAKE_ATM_NONE, + CAKE_COMPENSATION_MODE_ATM = CAKE_ATM_ATM, + CAKE_COMPENSATION_MODE_PTM = CAKE_ATM_PTM, + _CAKE_COMPENSATION_MODE_MAX, + _CAKE_COMPENSATION_MODE_INVALID = -EINVAL, +} CakeCompensationMode; + +typedef enum CakeFlowIsolationMode { + CAKE_FLOW_ISOLATION_MODE_NONE = CAKE_FLOW_NONE, + CAKE_FLOW_ISOLATION_MODE_SRC_IP = CAKE_FLOW_SRC_IP, + CAKE_FLOW_ISOLATION_MODE_DST_IP = CAKE_FLOW_DST_IP, + CAKE_FLOW_ISOLATION_MODE_HOSTS = CAKE_FLOW_HOSTS, + CAKE_FLOW_ISOLATION_MODE_FLOWS = CAKE_FLOW_FLOWS, + CAKE_FLOW_ISOLATION_MODE_DUAL_SRC = CAKE_FLOW_DUAL_SRC, + CAKE_FLOW_ISOLATION_MODE_DUAL_DST = CAKE_FLOW_DUAL_DST, + CAKE_FLOW_ISOLATION_MODE_TRIPLE = CAKE_FLOW_TRIPLE, + _CAKE_FLOW_ISOLATION_MODE_MAX, + _CAKE_FLOW_ISOLATION_MODE_INVALID = -EINVAL, +} CakeFlowIsolationMode; + +typedef enum CakePriorityQueueingPreset { + CAKE_PRESET_DIFFSERV3 = CAKE_DIFFSERV_DIFFSERV3, + CAKE_PRESET_DIFFSERV4 = CAKE_DIFFSERV_DIFFSERV4, + CAKE_PRESET_DIFFSERV8 = CAKE_DIFFSERV_DIFFSERV8, + CAKE_PRESET_BESTEFFORT = CAKE_DIFFSERV_BESTEFFORT, + CAKE_PRESET_PRECEDENCE = CAKE_DIFFSERV_PRECEDENCE, + _CAKE_PRESET_MAX, + _CAKE_PRESET_INVALID = -EINVAL, +} CakePriorityQueueingPreset; + +typedef enum CakeAckFilter { + CAKE_ACK_FILTER_NO = CAKE_ACK_NONE, + CAKE_ACK_FILTER_YES = CAKE_ACK_FILTER, + CAKE_ACK_FILTER_AGGRESSIVE = CAKE_ACK_AGGRESSIVE, + _CAKE_ACK_FILTER_MAX, + _CAKE_ACK_FILTER_INVALID = -EINVAL, +} CakeAckFilter; + +typedef struct CommonApplicationsKeptEnhanced { + QDisc meta; + + /* Shaper parameters */ + int autorate; + uint64_t bandwidth; + + /* Overhead compensation parameters */ + bool overhead_set; + int overhead; + uint32_t mpu; + CakeCompensationMode compensation_mode; + int raw; + + /* Flow isolation parameters */ + CakeFlowIsolationMode flow_isolation_mode; + int nat; + + /* Priority queue parameters */ + CakePriorityQueueingPreset preset; + uint32_t fwmark; + + /* Other parameters */ + int wash; + int split_gso; + usec_t rtt; + CakeAckFilter ack_filter; +} CommonApplicationsKeptEnhanced; + +DEFINE_QDISC_CAST(CAKE, CommonApplicationsKeptEnhanced); +extern const QDiscVTable cake_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_cake_bandwidth); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_overhead); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_mpu); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_tristate); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_compensation_mode); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_flow_isolation_mode); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_priority_queueing_preset); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_fwmark); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_rtt); +CONFIG_PARSER_PROTOTYPE(config_parse_cake_ack_filter); diff --git a/src/network/tc/codel.c b/src/network/tc/codel.c new file mode 100644 index 0000000..e212523 --- /dev/null +++ b/src/network/tc/codel.c @@ -0,0 +1,244 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "qdisc.h" +#include "string-util.h" + +static int controlled_delay_init(QDisc *qdisc) { + ControlledDelay *cd; + + assert(qdisc); + + cd = CODEL(qdisc); + + cd->ce_threshold_usec = USEC_INFINITY; + cd->ecn = -1; + + return 0; +} + +static int controlled_delay_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + ControlledDelay *cd; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(cd = CODEL(qdisc)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "codel"); + if (r < 0) + return r; + + if (cd->packet_limit > 0) { + r = sd_netlink_message_append_u32(req, TCA_CODEL_LIMIT, cd->packet_limit); + if (r < 0) + return r; + } + + if (cd->interval_usec > 0) { + r = sd_netlink_message_append_u32(req, TCA_CODEL_INTERVAL, cd->interval_usec); + if (r < 0) + return r; + } + + if (cd->target_usec > 0) { + r = sd_netlink_message_append_u32(req, TCA_CODEL_TARGET, cd->target_usec); + if (r < 0) + return r; + } + + if (cd->ecn >= 0) { + r = sd_netlink_message_append_u32(req, TCA_CODEL_ECN, cd->ecn); + if (r < 0) + return r; + } + + if (cd->ce_threshold_usec != USEC_INFINITY) { + r = sd_netlink_message_append_u32(req, TCA_CODEL_CE_THRESHOLD, cd->ce_threshold_usec); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_controlled_delay_u32( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + ControlledDelay *cd; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CODEL, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + cd = CODEL(qdisc); + + if (isempty(rvalue)) { + cd->packet_limit = 0; + + qdisc = NULL; + return 0; + } + + r = safe_atou32(rvalue, &cd->packet_limit); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + qdisc = NULL; + + return 0; +} + +int config_parse_controlled_delay_usec( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + ControlledDelay *cd; + Network *network = ASSERT_PTR(data); + usec_t *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CODEL, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + cd = CODEL(qdisc); + + if (streq(lvalue, "TargetSec")) + p = &cd->target_usec; + else if (streq(lvalue, "IntervalSec")) + p = &cd->interval_usec; + else if (streq(lvalue, "CEThresholdSec")) + p = &cd->ce_threshold_usec; + else + assert_not_reached(); + + if (isempty(rvalue)) { + if (streq(lvalue, "CEThresholdSec")) + *p = USEC_INFINITY; + else + *p = 0; + + qdisc = NULL; + return 0; + } + + r = parse_sec(rvalue, p); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + qdisc = NULL; + + return 0; +} + +int config_parse_controlled_delay_bool( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + ControlledDelay *cd; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_CODEL, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + cd = CODEL(qdisc); + + r = parse_tristate(rvalue, &cd->ecn); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +const QDiscVTable codel_vtable = { + .object_size = sizeof(ControlledDelay), + .tca_kind = "codel", + .init = controlled_delay_init, + .fill_message = controlled_delay_fill_message, +}; diff --git a/src/network/tc/codel.h b/src/network/tc/codel.h new file mode 100644 index 0000000..4fe5283 --- /dev/null +++ b/src/network/tc/codel.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" +#include "time-util.h" + +typedef struct ControlledDelay { + QDisc meta; + + uint32_t packet_limit; + usec_t interval_usec; + usec_t target_usec; + usec_t ce_threshold_usec; + int ecn; +} ControlledDelay; + +DEFINE_QDISC_CAST(CODEL, ControlledDelay); +extern const QDiscVTable codel_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_controlled_delay_u32); +CONFIG_PARSER_PROTOTYPE(config_parse_controlled_delay_usec); +CONFIG_PARSER_PROTOTYPE(config_parse_controlled_delay_bool); diff --git a/src/network/tc/drr.c b/src/network/tc/drr.c new file mode 100644 index 0000000..373911b --- /dev/null +++ b/src/network/tc/drr.c @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "drr.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "string-util.h" + +const QDiscVTable drr_vtable = { + .object_size = sizeof(DeficitRoundRobinScheduler), + .tca_kind = "drr", +}; + +static int drr_class_fill_message(Link *link, TClass *tclass, sd_netlink_message *req) { + DeficitRoundRobinSchedulerClass *drr; + int r; + + assert(link); + assert(tclass); + assert(req); + + assert_se(drr = TCLASS_TO_DRR(tclass)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "drr"); + if (r < 0) + return r; + + if (drr->quantum > 0) { + r = sd_netlink_message_append_u32(req, TCA_DRR_QUANTUM, drr->quantum); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_drr_size( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL; + DeficitRoundRobinSchedulerClass *drr; + Network *network = ASSERT_PTR(data); + uint64_t u; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = tclass_new_static(TCLASS_KIND_DRR, network, filename, section_line, &tclass); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to create traffic control class, ignoring assignment: %m"); + return 0; + } + + drr = TCLASS_TO_DRR(tclass); + + if (isempty(rvalue)) { + drr->quantum = 0; + + TAKE_PTR(tclass); + return 0; + } + + r = parse_size(rvalue, 1024, &u); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (u > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + drr->quantum = (uint32_t) u; + + TAKE_PTR(tclass); + return 0; +} + +const TClassVTable drr_tclass_vtable = { + .object_size = sizeof(DeficitRoundRobinSchedulerClass), + .tca_kind = "drr", + .fill_message = drr_class_fill_message, +}; diff --git a/src/network/tc/drr.h b/src/network/tc/drr.h new file mode 100644 index 0000000..c96cc4d --- /dev/null +++ b/src/network/tc/drr.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include "qdisc.h" + +typedef struct DeficitRoundRobinScheduler { + QDisc meta; +} DeficitRoundRobinScheduler; + +DEFINE_QDISC_CAST(DRR, DeficitRoundRobinScheduler); +extern const QDiscVTable drr_vtable; + +typedef struct DeficitRoundRobinSchedulerClass { + TClass meta; + + uint32_t quantum; +} DeficitRoundRobinSchedulerClass; + +DEFINE_TCLASS_CAST(DRR, DeficitRoundRobinSchedulerClass); +extern const TClassVTable drr_tclass_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_drr_size); diff --git a/src/network/tc/ets.c b/src/network/tc/ets.c new file mode 100644 index 0000000..730b0a1 --- /dev/null +++ b/src/network/tc/ets.c @@ -0,0 +1,342 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "ets.h" +#include "extract-word.h" +#include "memory-util.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "qdisc.h" +#include "string-util.h" +#include "tc-util.h" + +static int enhanced_transmission_selection_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + EnhancedTransmissionSelection *ets; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(ets = ETS(qdisc)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "ets"); + if (r < 0) + return r; + + r = sd_netlink_message_append_u8(req, TCA_ETS_NBANDS, ets->n_bands); + if (r < 0) + return r; + + if (ets->n_strict > 0) { + r = sd_netlink_message_append_u8(req, TCA_ETS_NSTRICT, ets->n_strict); + if (r < 0) + return r; + } + + if (ets->n_quanta > 0) { + r = sd_netlink_message_open_container(req, TCA_ETS_QUANTA); + if (r < 0) + return r; + + for (unsigned i = 0; i < ets->n_quanta; i++) { + r = sd_netlink_message_append_u32(req, TCA_ETS_QUANTA_BAND, ets->quanta[i]); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + } + + if (ets->n_prio > 0) { + r = sd_netlink_message_open_container(req, TCA_ETS_PRIOMAP); + if (r < 0) + return r; + + for (unsigned i = 0; i < ets->n_prio; i++) { + r = sd_netlink_message_append_u8(req, TCA_ETS_PRIOMAP_BAND, ets->prio[i]); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_ets_u8( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + EnhancedTransmissionSelection *ets; + Network *network = ASSERT_PTR(data); + uint8_t v, *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_ETS, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + ets = ETS(qdisc); + if (streq(lvalue, "Bands")) + p = &ets->n_bands; + else if (streq(lvalue, "StrictBands")) + p = &ets->n_strict; + else + assert_not_reached(); + + if (isempty(rvalue)) { + *p = 0; + + qdisc = NULL; + return 0; + } + + r = safe_atou8(rvalue, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (v > TCQ_ETS_MAX_BANDS) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s='. The value must be <= %d, ignoring assignment: %s", + lvalue, TCQ_ETS_MAX_BANDS, rvalue); + return 0; + } + + *p = v; + qdisc = NULL; + + return 0; +} + +int config_parse_ets_quanta( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + EnhancedTransmissionSelection *ets; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_ETS, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + ets = ETS(qdisc); + + if (isempty(rvalue)) { + memzero(ets->quanta, sizeof(uint32_t) * TCQ_ETS_MAX_BANDS); + ets->n_quanta = 0; + + qdisc = NULL; + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *word = NULL; + uint64_t v; + + r = extract_first_word(&p, &word, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract next value, ignoring: %m"); + break; + } + if (r == 0) + break; + + r = parse_size(word, 1024, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, word); + continue; + } + if (v == 0 || v > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, word); + continue; + } + if (ets->n_quanta >= TCQ_ETS_MAX_BANDS) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Too many quanta in '%s=', ignoring assignment: %s", + lvalue, word); + continue; + } + + ets->quanta[ets->n_quanta++] = v; + } + + qdisc = NULL; + + return 0; +} + +int config_parse_ets_prio( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + EnhancedTransmissionSelection *ets; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_ETS, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + ets = ETS(qdisc); + + if (isempty(rvalue)) { + memzero(ets->prio, sizeof(uint8_t) * (TC_PRIO_MAX + 1)); + ets->n_prio = 0; + + qdisc = NULL; + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *word = NULL; + uint8_t v; + + r = extract_first_word(&p, &word, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract next value, ignoring: %m"); + break; + } + if (r == 0) + break; + + r = safe_atou8(word, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, word); + continue; + } + if (ets->n_prio > TC_PRIO_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Too many priomap in '%s=', ignoring assignment: %s", + lvalue, word); + continue; + } + + ets->prio[ets->n_prio++] = v; + } + + qdisc = NULL; + + return 0; +} + +static int enhanced_transmission_selection_verify(QDisc *qdisc) { + EnhancedTransmissionSelection *ets; + + assert(qdisc); + + ets = ETS(qdisc); + + if (ets->n_bands == 0) + ets->n_bands = ets->n_strict + ets->n_quanta; + + if (ets->n_bands == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: At least one of Band=, Strict=, or Quanta= must be specified. " + "Ignoring [EnhancedTransmissionSelection] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + if (ets->n_bands < ets->n_strict + ets->n_quanta) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Not enough total bands to cover all the strict bands and quanta. " + "Ignoring [EnhancedTransmissionSelection] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + for (unsigned i = 0; i < ets->n_prio; i++) + if (ets->prio[i] >= ets->n_bands) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: PriorityMap= element is out of bands. " + "Ignoring [EnhancedTransmissionSelection] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + return 0; +} + +const QDiscVTable ets_vtable = { + .object_size = sizeof(EnhancedTransmissionSelection), + .tca_kind = "ets", + .fill_message = enhanced_transmission_selection_fill_message, + .verify = enhanced_transmission_selection_verify, +}; diff --git a/src/network/tc/ets.h b/src/network/tc/ets.h new file mode 100644 index 0000000..b6dd428 --- /dev/null +++ b/src/network/tc/ets.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <linux/pkt_sched.h> + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct EnhancedTransmissionSelection { + QDisc meta; + + uint8_t n_bands; + uint8_t n_strict; + unsigned n_quanta; + uint32_t quanta[TCQ_ETS_MAX_BANDS]; + unsigned n_prio; + uint8_t prio[TC_PRIO_MAX + 1]; +} EnhancedTransmissionSelection; + +DEFINE_QDISC_CAST(ETS, EnhancedTransmissionSelection); +extern const QDiscVTable ets_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_ets_u8); +CONFIG_PARSER_PROTOTYPE(config_parse_ets_quanta); +CONFIG_PARSER_PROTOTYPE(config_parse_ets_prio); diff --git a/src/network/tc/fifo.c b/src/network/tc/fifo.c new file mode 100644 index 0000000..940fa00 --- /dev/null +++ b/src/network/tc/fifo.c @@ -0,0 +1,183 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "fifo.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "string-util.h" + +static int fifo_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + FirstInFirstOut *fifo; + int r; + + assert(link); + assert(qdisc); + assert(req); + + switch (qdisc->kind) { + case QDISC_KIND_PFIFO: + assert_se(fifo = PFIFO(qdisc)); + break; + case QDISC_KIND_BFIFO: + assert_se(fifo = BFIFO(qdisc)); + break; + case QDISC_KIND_PFIFO_HEAD_DROP: + assert_se(fifo = PFIFO_HEAD_DROP(qdisc)); + break; + default: + assert_not_reached(); + } + + const struct tc_fifo_qopt opt = { .limit = fifo->limit }; + r = sd_netlink_message_append_data(req, TCA_OPTIONS, &opt, sizeof(opt)); + if (r < 0) + return r; + + return 0; +} + +int config_parse_pfifo_size( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + FirstInFirstOut *fifo; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(ltype, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + switch (qdisc->kind) { + case QDISC_KIND_PFIFO: + fifo = PFIFO(qdisc); + break; + case QDISC_KIND_PFIFO_HEAD_DROP: + fifo = PFIFO_HEAD_DROP(qdisc); + break; + default: + assert_not_reached(); + } + + if (isempty(rvalue)) { + fifo->limit = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, &fifo->limit); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + return 0; +} + +int config_parse_bfifo_size( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + FirstInFirstOut *fifo; + uint64_t u; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_BFIFO, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fifo = BFIFO(qdisc); + + if (isempty(rvalue)) { + fifo->limit = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_size(rvalue, 1024, &u); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (u > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + fifo->limit = (uint32_t) u; + + TAKE_PTR(qdisc); + return 0; +} + +const QDiscVTable pfifo_vtable = { + .object_size = sizeof(FirstInFirstOut), + .tca_kind = "pfifo", + .fill_message = fifo_fill_message, +}; + +const QDiscVTable bfifo_vtable = { + .object_size = sizeof(FirstInFirstOut), + .tca_kind = "bfifo", + .fill_message = fifo_fill_message, +}; + +const QDiscVTable pfifo_head_drop_vtable = { + .object_size = sizeof(FirstInFirstOut), + .tca_kind = "pfifo_head_drop", + .fill_message = fifo_fill_message, +}; + +const QDiscVTable pfifo_fast_vtable = { + .object_size = sizeof(FirstInFirstOut), + .tca_kind = "pfifo_fast", +}; diff --git a/src/network/tc/fifo.h b/src/network/tc/fifo.h new file mode 100644 index 0000000..b9bbd09 --- /dev/null +++ b/src/network/tc/fifo.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct FirstInFirstOut { + QDisc meta; + + uint32_t limit; +} FirstInFirstOut; + +DEFINE_QDISC_CAST(PFIFO, FirstInFirstOut); +DEFINE_QDISC_CAST(BFIFO, FirstInFirstOut); +DEFINE_QDISC_CAST(PFIFO_HEAD_DROP, FirstInFirstOut); +DEFINE_QDISC_CAST(PFIFO_FAST, FirstInFirstOut); + +extern const QDiscVTable pfifo_vtable; +extern const QDiscVTable bfifo_vtable; +extern const QDiscVTable pfifo_head_drop_vtable; +extern const QDiscVTable pfifo_fast_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_pfifo_size); +CONFIG_PARSER_PROTOTYPE(config_parse_bfifo_size); diff --git a/src/network/tc/fq-codel.c b/src/network/tc/fq-codel.c new file mode 100644 index 0000000..124faf7 --- /dev/null +++ b/src/network/tc/fq-codel.c @@ -0,0 +1,343 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "qdisc.h" +#include "string-util.h" +#include "strv.h" + +static int fair_queueing_controlled_delay_init(QDisc *qdisc) { + FairQueueingControlledDelay *fqcd; + + assert(qdisc); + + fqcd = FQ_CODEL(qdisc); + + fqcd->memory_limit = UINT32_MAX; + fqcd->ce_threshold_usec = USEC_INFINITY; + fqcd->ecn = -1; + + return 0; +} + +static int fair_queueing_controlled_delay_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + FairQueueingControlledDelay *fqcd; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(fqcd = FQ_CODEL(qdisc)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq_codel"); + if (r < 0) + return r; + + if (fqcd->packet_limit > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_LIMIT, fqcd->packet_limit); + if (r < 0) + return r; + } + + if (fqcd->flows > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_FLOWS, fqcd->flows); + if (r < 0) + return r; + } + + if (fqcd->quantum > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_QUANTUM, fqcd->quantum); + if (r < 0) + return r; + } + + if (fqcd->interval_usec > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_INTERVAL, fqcd->interval_usec); + if (r < 0) + return r; + } + + if (fqcd->target_usec > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_TARGET, fqcd->target_usec); + if (r < 0) + return r; + } + + if (fqcd->ecn >= 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_ECN, fqcd->ecn); + if (r < 0) + return r; + } + + if (fqcd->ce_threshold_usec != USEC_INFINITY) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_CE_THRESHOLD, fqcd->ce_threshold_usec); + if (r < 0) + return r; + } + + if (fqcd->memory_limit != UINT32_MAX) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CODEL_MEMORY_LIMIT, fqcd->memory_limit); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_fair_queueing_controlled_delay_u32( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueingControlledDelay *fqcd; + Network *network = ASSERT_PTR(data); + uint32_t *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ_CODEL, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fqcd = FQ_CODEL(qdisc); + + if (streq(lvalue, "PacketLimit")) + p = &fqcd->packet_limit; + else if (streq(lvalue, "Flows")) + p = &fqcd->flows; + else + assert_not_reached(); + + if (isempty(rvalue)) { + *p = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, p); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_fair_queueing_controlled_delay_usec( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueingControlledDelay *fqcd; + Network *network = ASSERT_PTR(data); + usec_t *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ_CODEL, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fqcd = FQ_CODEL(qdisc); + + if (streq(lvalue, "TargetSec")) + p = &fqcd->target_usec; + else if (streq(lvalue, "IntervalSec")) + p = &fqcd->interval_usec; + else if (streq(lvalue, "CEThresholdSec")) + p = &fqcd->ce_threshold_usec; + else + assert_not_reached(); + + if (isempty(rvalue)) { + if (streq(lvalue, "CEThresholdSec")) + *p = USEC_INFINITY; + else + *p = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_sec(rvalue, p); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_fair_queueing_controlled_delay_bool( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueingControlledDelay *fqcd; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ_CODEL, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fqcd = FQ_CODEL(qdisc); + + r = parse_tristate(rvalue, &fqcd->ecn); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_fair_queueing_controlled_delay_size( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueingControlledDelay *fqcd; + Network *network = ASSERT_PTR(data); + uint64_t sz; + uint32_t *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ_CODEL, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fqcd = FQ_CODEL(qdisc); + + if (STR_IN_SET(lvalue, "MemoryLimitBytes", "MemoryLimit")) + p = &fqcd->memory_limit; + else if (STR_IN_SET(lvalue, "QuantumBytes", "Quantum")) + p = &fqcd->quantum; + else + assert_not_reached(); + + if (isempty(rvalue)) { + if (STR_IN_SET(lvalue, "MemoryLimitBytes", "MemoryLimit")) + *p = UINT32_MAX; + else + *p = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_size(rvalue, 1024, &sz); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (sz >= UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified '%s=' is too large, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + *p = sz; + TAKE_PTR(qdisc); + + return 0; +} + +const QDiscVTable fq_codel_vtable = { + .object_size = sizeof(FairQueueingControlledDelay), + .tca_kind = "fq_codel", + .init = fair_queueing_controlled_delay_init, + .fill_message = fair_queueing_controlled_delay_fill_message, +}; diff --git a/src/network/tc/fq-codel.h b/src/network/tc/fq-codel.h new file mode 100644 index 0000000..2553c59 --- /dev/null +++ b/src/network/tc/fq-codel.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" +#include "time-util.h" + +typedef struct FairQueueingControlledDelay { + QDisc meta; + + uint32_t packet_limit; + uint32_t flows; + uint32_t quantum; + uint32_t memory_limit; + usec_t target_usec; + usec_t interval_usec; + usec_t ce_threshold_usec; + int ecn; +} FairQueueingControlledDelay; + +DEFINE_QDISC_CAST(FQ_CODEL, FairQueueingControlledDelay); +extern const QDiscVTable fq_codel_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_controlled_delay_u32); +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_controlled_delay_usec); +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_controlled_delay_bool); +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_controlled_delay_size); diff --git a/src/network/tc/fq-pie.c b/src/network/tc/fq-pie.c new file mode 100644 index 0000000..c8b2e7b --- /dev/null +++ b/src/network/tc/fq-pie.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "fq-pie.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "string-util.h" + +static int fq_pie_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + FlowQueuePIE *fq_pie; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(fq_pie = FQ_PIE(qdisc)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq_pie"); + if (r < 0) + return r; + + if (fq_pie->packet_limit > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_PIE_LIMIT, fq_pie->packet_limit); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_fq_pie_packet_limit( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FlowQueuePIE *fq_pie; + Network *network = ASSERT_PTR(data); + uint32_t val; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ_PIE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + + fq_pie = FQ_PIE(qdisc); + + if (isempty(rvalue)) { + fq_pie->packet_limit = 0; + + qdisc = NULL; + return 0; + } + + r = safe_atou32(rvalue, &val); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (val == 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + fq_pie->packet_limit = val; + qdisc = NULL; + + return 0; +} + +const QDiscVTable fq_pie_vtable = { + .object_size = sizeof(FlowQueuePIE), + .tca_kind = "fq_pie", + .fill_message = fq_pie_fill_message, +}; diff --git a/src/network/tc/fq-pie.h b/src/network/tc/fq-pie.h new file mode 100644 index 0000000..51fb626 --- /dev/null +++ b/src/network/tc/fq-pie.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct FlowQueuePIE { + QDisc meta; + + uint32_t packet_limit; +} FlowQueuePIE; + +DEFINE_QDISC_CAST(FQ_PIE, FlowQueuePIE); +extern const QDiscVTable fq_pie_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_fq_pie_packet_limit); diff --git a/src/network/tc/fq.c b/src/network/tc/fq.c new file mode 100644 index 0000000..74785c9 --- /dev/null +++ b/src/network/tc/fq.c @@ -0,0 +1,409 @@ +/* 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 "logarithm.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" + +static int fair_queueing_init(QDisc *qdisc) { + FairQueueing *fq; + + assert(qdisc); + + fq = FQ(qdisc); + + fq->pacing = -1; + fq->ce_threshold_usec = USEC_INFINITY; + + return 0; +} + +static int fair_queueing_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + FairQueueing *fq; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(fq = FQ(qdisc)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "fq"); + if (r < 0) + return r; + + if (fq->packet_limit > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_PLIMIT, fq->packet_limit); + if (r < 0) + return r; + } + + if (fq->flow_limit > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_FLOW_PLIMIT, fq->flow_limit); + if (r < 0) + return r; + } + + if (fq->quantum > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_QUANTUM, fq->quantum); + if (r < 0) + return r; + } + + if (fq->initial_quantum > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_INITIAL_QUANTUM, fq->initial_quantum); + if (r < 0) + return r; + } + + if (fq->pacing >= 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_RATE_ENABLE, fq->pacing); + if (r < 0) + return r; + } + + if (fq->max_rate > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_FLOW_MAX_RATE, fq->max_rate); + if (r < 0) + return r; + } + + if (fq->buckets > 0) { + uint32_t l; + + l = log2u(fq->buckets); + r = sd_netlink_message_append_u32(req, TCA_FQ_BUCKETS_LOG, l); + if (r < 0) + return r; + } + + if (fq->orphan_mask > 0) { + r = sd_netlink_message_append_u32(req, TCA_FQ_ORPHAN_MASK, fq->orphan_mask); + if (r < 0) + return r; + } + + if (fq->ce_threshold_usec != USEC_INFINITY) { + r = sd_netlink_message_append_u32(req, TCA_FQ_CE_THRESHOLD, fq->ce_threshold_usec); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_fair_queueing_u32( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueing *fq; + Network *network = ASSERT_PTR(data); + uint32_t *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fq = FQ(qdisc); + + if (streq(lvalue, "PacketLimit")) + p = &fq->packet_limit; + else if (streq(lvalue, "FlowLimit")) + p = &fq->flow_limit; + else if (streq(lvalue, "Buckets")) + p = &fq->buckets; + else if (streq(lvalue, "OrphanMask")) + p = &fq->orphan_mask; + else + assert_not_reached(); + + if (isempty(rvalue)) { + *p = 0; + + qdisc = NULL; + return 0; + } + + r = safe_atou32(rvalue, p); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + qdisc = NULL; + + return 0; +} + +int config_parse_fair_queueing_size( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueing *fq; + Network *network = ASSERT_PTR(data); + uint64_t sz; + uint32_t *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fq = FQ(qdisc); + + if (STR_IN_SET(lvalue, "QuantumBytes", "Quantum")) + p = &fq->quantum; + else if (STR_IN_SET(lvalue, "InitialQuantumBytes", "InitialQuantum")) + p = &fq->initial_quantum; + else + assert_not_reached(); + + if (isempty(rvalue)) { + *p = 0; + + qdisc = NULL; + return 0; + } + + r = parse_size(rvalue, 1024, &sz); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (sz > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified '%s=' is too large, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + *p = sz; + qdisc = NULL; + + return 0; +} + +int config_parse_fair_queueing_bool( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueing *fq; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fq = FQ(qdisc); + + r = parse_tristate(rvalue, &fq->pacing); + 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; + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_fair_queueing_usec( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueing *fq; + Network *network = ASSERT_PTR(data); + usec_t sec; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fq = FQ(qdisc); + + if (isempty(rvalue)) { + fq->ce_threshold_usec = USEC_INFINITY; + + qdisc = NULL; + return 0; + } + + r = parse_sec(rvalue, &sec); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (sec > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified '%s=' is too large, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + fq->ce_threshold_usec = sec; + qdisc = NULL; + + return 0; +} + +int config_parse_fair_queueing_max_rate( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + FairQueueing *fq; + Network *network = ASSERT_PTR(data); + uint64_t sz; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_FQ, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + fq = FQ(qdisc); + + if (isempty(rvalue)) { + fq->max_rate = 0; + + qdisc = NULL; + return 0; + } + + r = parse_size(rvalue, 1000, &sz); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (sz / 8 > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Specified '%s=' is too large, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + fq->max_rate = sz / 8; + qdisc = NULL; + + return 0; +} + +const QDiscVTable fq_vtable = { + .init = fair_queueing_init, + .object_size = sizeof(FairQueueing), + .tca_kind = "fq", + .fill_message = fair_queueing_fill_message, +}; diff --git a/src/network/tc/fq.h b/src/network/tc/fq.h new file mode 100644 index 0000000..77469c4 --- /dev/null +++ b/src/network/tc/fq.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct FairQueueing { + QDisc meta; + + uint32_t packet_limit; + uint32_t flow_limit; + uint32_t quantum; + uint32_t initial_quantum; + uint32_t max_rate; + uint32_t buckets; + uint32_t orphan_mask; + int pacing; + usec_t ce_threshold_usec; +} FairQueueing; + +DEFINE_QDISC_CAST(FQ, FairQueueing); +extern const QDiscVTable fq_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_u32); +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_size); +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_bool); +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_usec); +CONFIG_PARSER_PROTOTYPE(config_parse_fair_queueing_max_rate); diff --git a/src/network/tc/gred.c b/src/network/tc/gred.c new file mode 100644 index 0000000..2efb02c --- /dev/null +++ b/src/network/tc/gred.c @@ -0,0 +1,185 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "qdisc.h" +#include "string-util.h" + +static int generic_random_early_detection_init(QDisc *qdisc) { + GenericRandomEarlyDetection *gred; + + assert(qdisc); + + gred = GRED(qdisc); + + gred->grio = -1; + + return 0; +} + +static int generic_random_early_detection_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + GenericRandomEarlyDetection *gred; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(gred = GRED(qdisc)); + + const struct tc_gred_sopt opt = { + .DPs = gred->virtual_queues, + .def_DP = gred->default_virtual_queue, + .grio = gred->grio, + }; + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "gred"); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, TCA_GRED_DPS, &opt, sizeof(opt)); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +static int generic_random_early_detection_verify(QDisc *qdisc) { + GenericRandomEarlyDetection *gred = GRED(qdisc); + + if (gred->default_virtual_queue >= gred->virtual_queues) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: DefaultVirtualQueue= must be less than VirtualQueues=. " + "Ignoring [GenericRandomEarlyDetection] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + return 0; +} + +int config_parse_generic_random_early_detection_u32( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + GenericRandomEarlyDetection *gred; + Network *network = ASSERT_PTR(data); + uint32_t *p; + uint32_t v; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_GRED, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + gred = GRED(qdisc); + + if (streq(lvalue, "VirtualQueues")) + p = &gred->virtual_queues; + else if (streq(lvalue, "DefaultVirtualQueue")) + p = &gred->default_virtual_queue; + else + assert_not_reached(); + + if (isempty(rvalue)) { + *p = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (v > MAX_DPs) + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + + *p = v; + TAKE_PTR(qdisc); + + return 0; +} +int config_parse_generic_random_early_detection_bool( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + GenericRandomEarlyDetection *gred; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_GRED, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + gred = GRED(qdisc); + + r = parse_tristate(rvalue, &gred->grio); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +const QDiscVTable 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..d44522f --- /dev/null +++ b/src/network/tc/hhf.c @@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "hhf.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "string-util.h" + +static int heavy_hitter_filter_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + HeavyHitterFilter *hhf; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(hhf = HHF(qdisc)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "hhf"); + if (r < 0) + return r; + + if (hhf->packet_limit > 0) { + r = sd_netlink_message_append_u32(req, TCA_HHF_BACKLOG_LIMIT, hhf->packet_limit); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_heavy_hitter_filter_packet_limit( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + HeavyHitterFilter *hhf; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_HHF, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + hhf = HHF(qdisc); + + if (isempty(rvalue)) { + hhf->packet_limit = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, &hhf->packet_limit); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +const QDiscVTable hhf_vtable = { + .object_size = sizeof(HeavyHitterFilter), + .tca_kind = "hhf", + .fill_message = heavy_hitter_filter_fill_message, +}; diff --git a/src/network/tc/hhf.h b/src/network/tc/hhf.h new file mode 100644 index 0000000..04caaa8 --- /dev/null +++ b/src/network/tc/hhf.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct HeavyHitterFilter { + QDisc meta; + + uint32_t packet_limit; +} HeavyHitterFilter; + +DEFINE_QDISC_CAST(HHF, HeavyHitterFilter); +extern const QDiscVTable hhf_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_heavy_hitter_filter_packet_limit); diff --git a/src/network/tc/htb.c b/src/network/tc/htb.c new file mode 100644 index 0000000..eb2c8cf --- /dev/null +++ b/src/network/tc/htb.c @@ -0,0 +1,487 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netlink-util.h" +#include "networkd-link.h" +#include "parse-util.h" +#include "qdisc.h" +#include "htb.h" +#include "string-util.h" +#include "tc-util.h" + +#define HTB_DEFAULT_RATE_TO_QUANTUM 10 +#define HTB_DEFAULT_MTU 1600 /* Ethernet packet length */ + +static int hierarchy_token_bucket_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + HierarchyTokenBucket *htb; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(htb = HTB(qdisc)); + + struct tc_htb_glob opt = { + .version = 3, + .rate2quantum = htb->rate_to_quantum, + .defcls = htb->default_class, + }; + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "htb"); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, TCA_HTB_INIT, &opt, sizeof(opt)); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + return 0; +} + +int config_parse_hierarchy_token_bucket_default_class( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + HierarchyTokenBucket *htb; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_HTB, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + htb = HTB(qdisc); + + if (isempty(rvalue)) { + htb->default_class = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32_full(rvalue, 16, &htb->default_class); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_hierarchy_token_bucket_u32( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + HierarchyTokenBucket *htb; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_HTB, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + htb = HTB(qdisc); + + if (isempty(rvalue)) { + htb->rate_to_quantum = HTB_DEFAULT_RATE_TO_QUANTUM; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, &htb->rate_to_quantum); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +static int hierarchy_token_bucket_init(QDisc *qdisc) { + HierarchyTokenBucket *htb; + + assert(qdisc); + + htb = HTB(qdisc); + + htb->rate_to_quantum = HTB_DEFAULT_RATE_TO_QUANTUM; + + return 0; +} + +const QDiscVTable htb_vtable = { + .object_size = sizeof(HierarchyTokenBucket), + .tca_kind = "htb", + .fill_message = hierarchy_token_bucket_fill_message, + .init = hierarchy_token_bucket_init, +}; + +static int hierarchy_token_bucket_class_fill_message(Link *link, TClass *tclass, sd_netlink_message *req) { + HierarchyTokenBucketClass *htb; + uint32_t rtab[256], ctab[256]; + int r; + + assert(link); + assert(tclass); + assert(req); + + assert_se(htb = TCLASS_TO_HTB(tclass)); + + struct tc_htb_opt opt = { + .prio = htb->priority, + .quantum = htb->quantum, + .rate.rate = (htb->rate >= (1ULL << 32)) ? ~0U : htb->rate, + .ceil.rate = (htb->ceil_rate >= (1ULL << 32)) ? ~0U : htb->ceil_rate, + .rate.overhead = htb->overhead, + .ceil.overhead = htb->overhead, + }; + + r = tc_transmit_time(htb->rate, htb->buffer, &opt.buffer); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to calculate buffer size: %m"); + + r = tc_transmit_time(htb->ceil_rate, htb->ceil_buffer, &opt.cbuffer); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to calculate ceil buffer size: %m"); + + r = tc_fill_ratespec_and_table(&opt.rate, rtab, htb->mtu); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to calculate rate table: %m"); + + r = tc_fill_ratespec_and_table(&opt.ceil, ctab, htb->mtu); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to calculate ceil rate table: %m"); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "htb"); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, TCA_HTB_PARMS, &opt, sizeof(opt)); + if (r < 0) + return r; + + if (htb->rate >= (1ULL << 32)) { + r = sd_netlink_message_append_u64(req, TCA_HTB_RATE64, htb->rate); + if (r < 0) + return r; + } + + if (htb->ceil_rate >= (1ULL << 32)) { + r = sd_netlink_message_append_u64(req, TCA_HTB_CEIL64, htb->ceil_rate); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_data(req, TCA_HTB_RTAB, rtab, sizeof(rtab)); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, TCA_HTB_CTAB, ctab, sizeof(ctab)); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_hierarchy_token_bucket_class_u32( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL; + HierarchyTokenBucketClass *htb; + Network *network = ASSERT_PTR(data); + uint32_t v; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = tclass_new_static(TCLASS_KIND_HTB, network, filename, section_line, &tclass); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to create traffic control class, ignoring assignment: %m"); + return 0; + } + + htb = TCLASS_TO_HTB(tclass); + + if (isempty(rvalue)) { + htb->priority = 0; + tclass = NULL; + return 0; + } + + r = safe_atou32(rvalue, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + htb->priority = v; + tclass = NULL; + + return 0; +} + +int config_parse_hierarchy_token_bucket_class_size( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL; + HierarchyTokenBucketClass *htb; + Network *network = ASSERT_PTR(data); + uint64_t v; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = tclass_new_static(TCLASS_KIND_HTB, network, filename, section_line, &tclass); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to create traffic control class, ignoring assignment: %m"); + return 0; + } + + htb = TCLASS_TO_HTB(tclass); + + if (isempty(rvalue)) { + if (streq(lvalue, "QuantumBytes")) + htb->quantum = 0; + else if (streq(lvalue, "MTUBytes")) + htb->mtu = HTB_DEFAULT_MTU; + else if (streq(lvalue, "OverheadBytes")) + htb->overhead = 0; + else if (streq(lvalue, "BufferBytes")) + htb->buffer = 0; + else if (streq(lvalue, "CeilBufferBytes")) + htb->ceil_buffer = 0; + else + assert_not_reached(); + + tclass = NULL; + return 0; + } + + r = parse_size(rvalue, 1024, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if ((streq(lvalue, "OverheadBytes") && v > UINT16_MAX) || v > UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (streq(lvalue, "QuantumBytes")) + htb->quantum = v; + else if (streq(lvalue, "OverheadBytes")) + htb->overhead = v; + else if (streq(lvalue, "MTUBytes")) + htb->mtu = v; + else if (streq(lvalue, "BufferBytes")) + htb->buffer = v; + else if (streq(lvalue, "CeilBufferBytes")) + htb->ceil_buffer = v; + else + assert_not_reached(); + + tclass = NULL; + + return 0; +} + +int config_parse_hierarchy_token_bucket_class_rate( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL; + HierarchyTokenBucketClass *htb; + Network *network = ASSERT_PTR(data); + uint64_t *v; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = tclass_new_static(TCLASS_KIND_HTB, network, filename, section_line, &tclass); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to create traffic control class, ignoring assignment: %m"); + return 0; + } + + htb = TCLASS_TO_HTB(tclass); + if (streq(lvalue, "Rate")) + v = &htb->rate; + else if (streq(lvalue, "CeilRate")) + v = &htb->ceil_rate; + else + assert_not_reached(); + + if (isempty(rvalue)) { + *v = 0; + + tclass = NULL; + return 0; + } + + r = parse_size(rvalue, 1000, v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + *v /= 8; + tclass = NULL; + + return 0; +} + +static int hierarchy_token_bucket_class_init(TClass *tclass) { + HierarchyTokenBucketClass *htb; + + assert(tclass); + + htb = TCLASS_TO_HTB(tclass); + + htb->mtu = HTB_DEFAULT_MTU; + + return 0; +} + +static int hierarchy_token_bucket_class_verify(TClass *tclass) { + HierarchyTokenBucketClass *htb; + uint32_t hz; + int r; + + assert(tclass); + + htb = TCLASS_TO_HTB(tclass); + + if (htb->rate == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Rate= is mandatory. " + "Ignoring [HierarchyTokenBucketClass] section from line %u.", + tclass->section->filename, tclass->section->line); + + /* if CeilRate= setting is missing, use the same as Rate= */ + if (htb->ceil_rate == 0) + htb->ceil_rate = htb->rate; + + r = tc_init(NULL, &hz); + if (r < 0) + return log_error_errno(r, "Failed to read /proc/net/psched: %m"); + + if (htb->buffer == 0) + htb->buffer = htb->rate / hz + htb->mtu; + if (htb->ceil_buffer == 0) + htb->ceil_buffer = htb->ceil_rate / hz + htb->mtu; + + return 0; +} + +const TClassVTable htb_tclass_vtable = { + .object_size = sizeof(HierarchyTokenBucketClass), + .tca_kind = "htb", + .fill_message = hierarchy_token_bucket_class_fill_message, + .init = hierarchy_token_bucket_class_init, + .verify = hierarchy_token_bucket_class_verify, +}; diff --git a/src/network/tc/htb.h b/src/network/tc/htb.h new file mode 100644 index 0000000..55644db --- /dev/null +++ b/src/network/tc/htb.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" +#include "tclass.h" + +typedef struct HierarchyTokenBucket { + QDisc meta; + + uint32_t default_class; + uint32_t rate_to_quantum; +} HierarchyTokenBucket; + +DEFINE_QDISC_CAST(HTB, HierarchyTokenBucket); +extern const QDiscVTable htb_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_default_class); +CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_u32); + +typedef struct HierarchyTokenBucketClass { + TClass meta; + + uint32_t priority; + uint32_t quantum; + uint32_t mtu; + uint16_t overhead; + uint64_t rate; + uint32_t buffer; + uint64_t ceil_rate; + uint32_t ceil_buffer; +} HierarchyTokenBucketClass; + +DEFINE_TCLASS_CAST(HTB, HierarchyTokenBucketClass); +extern const TClassVTable htb_tclass_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_class_u32); +CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_class_size); +CONFIG_PARSER_PROTOTYPE(config_parse_hierarchy_token_bucket_class_rate); diff --git a/src/network/tc/netem.c b/src/network/tc/netem.c new file mode 100644 index 0000000..6a63221 --- /dev/null +++ b/src/network/tc/netem.c @@ -0,0 +1,227 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netem.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "parse-util.h" +#include "qdisc.h" +#include "strv.h" +#include "tc-util.h" + +static int network_emulator_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + NetworkEmulator *ne; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(ne = NETEM(qdisc)); + + struct tc_netem_qopt opt = { + .limit = ne->limit > 0 ? ne->limit : 1000, + .loss = ne->loss, + .duplicate = ne->duplicate, + }; + + if (ne->delay != USEC_INFINITY) { + r = tc_time_to_tick(ne->delay, &opt.latency); + if (r < 0) + return log_link_error_errno(link, r, "Failed to calculate latency in TCA_OPTION: %m"); + } + + if (ne->jitter != USEC_INFINITY) { + r = tc_time_to_tick(ne->jitter, &opt.jitter); + if (r < 0) + return log_link_error_errno(link, r, "Failed to calculate jitter in TCA_OPTION: %m"); + } + + r = sd_netlink_message_append_data(req, TCA_OPTIONS, &opt, sizeof(opt)); + if (r < 0) + return r; + + return 0; +} + +int config_parse_network_emulator_delay( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + NetworkEmulator *ne; + usec_t u; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_NETEM, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + ne = NETEM(qdisc); + + if (isempty(rvalue)) { + if (STR_IN_SET(lvalue, "DelaySec", "NetworkEmulatorDelaySec")) + ne->delay = USEC_INFINITY; + else if (STR_IN_SET(lvalue, "DelayJitterSec", "NetworkEmulatorDelayJitterSec")) + ne->jitter = USEC_INFINITY; + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_sec(rvalue, &u); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (STR_IN_SET(lvalue, "DelaySec", "NetworkEmulatorDelaySec")) + ne->delay = u; + else if (STR_IN_SET(lvalue, "DelayJitterSec", "NetworkEmulatorDelayJitterSec")) + ne->jitter = u; + + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_network_emulator_rate( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + NetworkEmulator *ne; + uint32_t rate; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_NETEM, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + ne = NETEM(qdisc); + + if (isempty(rvalue)) { + if (STR_IN_SET(lvalue, "LossRate", "NetworkEmulatorLossRate")) + ne->loss = 0; + else if (STR_IN_SET(lvalue, "DuplicateRate", "NetworkEmulatorDuplicateRate")) + ne->duplicate = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_tc_percent(rvalue, &rate); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (STR_IN_SET(lvalue, "LossRate", "NetworkEmulatorLossRate")) + ne->loss = rate; + else if (STR_IN_SET(lvalue, "DuplicateRate", "NetworkEmulatorDuplicateRate")) + ne->duplicate = rate; + + TAKE_PTR(qdisc); + return 0; +} + +int config_parse_network_emulator_packet_limit( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + NetworkEmulator *ne; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_NETEM, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + ne = NETEM(qdisc); + + if (isempty(rvalue)) { + ne->limit = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou(rvalue, &ne->limit); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + return 0; +} + +const QDiscVTable netem_vtable = { + .object_size = sizeof(NetworkEmulator), + .tca_kind = "netem", + .fill_message = network_emulator_fill_message, +}; diff --git a/src/network/tc/netem.h b/src/network/tc/netem.h new file mode 100644 index 0000000..d58d5ac --- /dev/null +++ b/src/network/tc/netem.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" +#include "time-util.h" + +typedef struct NetworkEmulator { + QDisc meta; + + usec_t delay; + usec_t jitter; + + uint32_t limit; + uint32_t loss; + uint32_t duplicate; +} NetworkEmulator; + +DEFINE_QDISC_CAST(NETEM, NetworkEmulator); +extern const QDiscVTable netem_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_network_emulator_delay); +CONFIG_PARSER_PROTOTYPE(config_parse_network_emulator_rate); +CONFIG_PARSER_PROTOTYPE(config_parse_network_emulator_packet_limit); diff --git a/src/network/tc/pie.c b/src/network/tc/pie.c new file mode 100644 index 0000000..c9b171b --- /dev/null +++ b/src/network/tc/pie.c @@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "pie.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "string-util.h" + +static int pie_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + ProportionalIntegralControllerEnhanced *pie; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(pie = PIE(qdisc)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "pie"); + if (r < 0) + return r; + + if (pie->packet_limit > 0) { + r = sd_netlink_message_append_u32(req, TCA_PIE_LIMIT, pie->packet_limit); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_pie_packet_limit( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + ProportionalIntegralControllerEnhanced *pie; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_PIE, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + pie = PIE(qdisc); + + if (isempty(rvalue)) { + pie->packet_limit = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, &pie->packet_limit); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +const QDiscVTable pie_vtable = { + .object_size = sizeof(ProportionalIntegralControllerEnhanced), + .tca_kind = "pie", + .fill_message = pie_fill_message, +}; diff --git a/src/network/tc/pie.h b/src/network/tc/pie.h new file mode 100644 index 0000000..40a114e --- /dev/null +++ b/src/network/tc/pie.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct ProportionalIntegralControllerEnhanced { + QDisc meta; + + uint32_t packet_limit; +} ProportionalIntegralControllerEnhanced; + +DEFINE_QDISC_CAST(PIE, ProportionalIntegralControllerEnhanced); +extern const QDiscVTable pie_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_pie_packet_limit); diff --git a/src/network/tc/qdisc.c b/src/network/tc/qdisc.c new file mode 100644 index 0000000..f20f410 --- /dev/null +++ b/src/network/tc/qdisc.c @@ -0,0 +1,715 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "in-addr-util.h" +#include "netlink-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "parse-util.h" +#include "qdisc.h" +#include "set.h" +#include "string-util.h" +#include "strv.h" +#include "tc-util.h" + +const QDiscVTable * const qdisc_vtable[_QDISC_KIND_MAX] = { + [QDISC_KIND_BFIFO] = &bfifo_vtable, + [QDISC_KIND_CAKE] = &cake_vtable, + [QDISC_KIND_CODEL] = &codel_vtable, + [QDISC_KIND_DRR] = &drr_vtable, + [QDISC_KIND_ETS] = &ets_vtable, + [QDISC_KIND_FQ] = &fq_vtable, + [QDISC_KIND_FQ_CODEL] = &fq_codel_vtable, + [QDISC_KIND_FQ_PIE] = &fq_pie_vtable, + [QDISC_KIND_GRED] = &gred_vtable, + [QDISC_KIND_HHF] = &hhf_vtable, + [QDISC_KIND_HTB] = &htb_vtable, + [QDISC_KIND_NETEM] = &netem_vtable, + [QDISC_KIND_PIE] = &pie_vtable, + [QDISC_KIND_QFQ] = &qfq_vtable, + [QDISC_KIND_PFIFO] = &pfifo_vtable, + [QDISC_KIND_PFIFO_FAST] = &pfifo_fast_vtable, + [QDISC_KIND_PFIFO_HEAD_DROP] = &pfifo_head_drop_vtable, + [QDISC_KIND_SFB] = &sfb_vtable, + [QDISC_KIND_SFQ] = &sfq_vtable, + [QDISC_KIND_TBF] = &tbf_vtable, + [QDISC_KIND_TEQL] = &teql_vtable, +}; + +static int qdisc_new(QDiscKind kind, QDisc **ret) { + _cleanup_(qdisc_freep) QDisc *qdisc = NULL; + int r; + + if (kind == _QDISC_KIND_INVALID) { + qdisc = new(QDisc, 1); + if (!qdisc) + return -ENOMEM; + + *qdisc = (QDisc) { + .parent = TC_H_ROOT, + .kind = kind, + }; + } else { + assert(kind >= 0 && kind < _QDISC_KIND_MAX); + qdisc = malloc0(qdisc_vtable[kind]->object_size); + if (!qdisc) + return -ENOMEM; + + qdisc->parent = TC_H_ROOT; + qdisc->kind = kind; + + if (QDISC_VTABLE(qdisc)->init) { + r = QDISC_VTABLE(qdisc)->init(qdisc); + if (r < 0) + return r; + } + } + + *ret = TAKE_PTR(qdisc); + + return 0; +} + +int qdisc_new_static(QDiscKind kind, Network *network, const char *filename, unsigned section_line, QDisc **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(qdisc_freep) QDisc *qdisc = NULL; + QDisc *existing; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + existing = hashmap_get(network->qdiscs_by_section, n); + if (existing) { + if (existing->kind != _QDISC_KIND_INVALID && + kind != _QDISC_KIND_INVALID && + existing->kind != kind) + return -EINVAL; + + if (existing->kind == kind || kind == _QDISC_KIND_INVALID) { + *ret = existing; + return 0; + } + } + + r = qdisc_new(kind, &qdisc); + if (r < 0) + return r; + + if (existing) { + qdisc->handle = existing->handle; + qdisc->parent = existing->parent; + qdisc->tca_kind = TAKE_PTR(existing->tca_kind); + + qdisc_free(existing); + } + + qdisc->network = network; + qdisc->section = TAKE_PTR(n); + qdisc->source = NETWORK_CONFIG_SOURCE_STATIC; + + r = hashmap_ensure_put(&network->qdiscs_by_section, &config_section_hash_ops, qdisc->section, qdisc); + if (r < 0) + return r; + + *ret = TAKE_PTR(qdisc); + return 0; +} + +QDisc* qdisc_free(QDisc *qdisc) { + if (!qdisc) + return NULL; + + if (qdisc->network && qdisc->section) + hashmap_remove(qdisc->network->qdiscs_by_section, qdisc->section); + + config_section_free(qdisc->section); + + if (qdisc->link) + set_remove(qdisc->link->qdiscs, qdisc); + + free(qdisc->tca_kind); + return mfree(qdisc); +} + +static const char *qdisc_get_tca_kind(const QDisc *qdisc) { + assert(qdisc); + + return (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->tca_kind) ? + QDISC_VTABLE(qdisc)->tca_kind : qdisc->tca_kind; +} + +static void qdisc_hash_func(const QDisc *qdisc, struct siphash *state) { + assert(qdisc); + assert(state); + + siphash24_compress(&qdisc->handle, sizeof(qdisc->handle), state); + siphash24_compress(&qdisc->parent, sizeof(qdisc->parent), state); + siphash24_compress_string(qdisc_get_tca_kind(qdisc), state); +} + +static int qdisc_compare_func(const QDisc *a, const QDisc *b) { + int r; + + assert(a); + assert(b); + + r = CMP(a->handle, b->handle); + if (r != 0) + return r; + + r = CMP(a->parent, b->parent); + if (r != 0) + return r; + + return strcmp_ptr(qdisc_get_tca_kind(a), qdisc_get_tca_kind(b)); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + qdisc_hash_ops, + QDisc, + qdisc_hash_func, + qdisc_compare_func, + qdisc_free); + +static int qdisc_get(Link *link, const QDisc *in, QDisc **ret) { + QDisc *existing; + + assert(link); + assert(in); + + existing = set_get(link->qdiscs, in); + if (!existing) + return -ENOENT; + + if (ret) + *ret = existing; + return 0; +} + +static int qdisc_add(Link *link, QDisc *qdisc) { + int r; + + assert(link); + assert(qdisc); + + r = set_ensure_put(&link->qdiscs, &qdisc_hash_ops, qdisc); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + qdisc->link = link; + return 0; +} + +static int qdisc_dup(const QDisc *src, QDisc **ret) { + _cleanup_(qdisc_freep) QDisc *dst = NULL; + + assert(src); + assert(ret); + + if (QDISC_VTABLE(src)) + dst = memdup(src, QDISC_VTABLE(src)->object_size); + else + dst = newdup(QDisc, src, 1); + if (!dst) + return -ENOMEM; + + /* clear all pointers */ + dst->network = NULL; + dst->section = NULL; + dst->link = NULL; + dst->tca_kind = NULL; + + if (src->tca_kind) { + dst->tca_kind = strdup(src->tca_kind); + if (!dst->tca_kind) + return -ENOMEM; + } + + *ret = TAKE_PTR(dst); + return 0; +} + +static void log_qdisc_debug(QDisc *qdisc, Link *link, const char *str) { + _cleanup_free_ char *state = NULL; + + assert(qdisc); + assert(str); + + if (!DEBUG_LOGGING) + return; + + (void) network_config_state_to_string_alloc(qdisc->state, &state); + + log_link_debug(link, "%s %s QDisc (%s): handle=%"PRIx32":%"PRIx32", parent=%"PRIx32":%"PRIx32", kind=%s", + str, strna(network_config_source_to_string(qdisc->source)), strna(state), + TC_H_MAJ(qdisc->handle) >> 16, TC_H_MIN(qdisc->handle), + TC_H_MAJ(qdisc->parent) >> 16, TC_H_MIN(qdisc->parent), + strna(qdisc_get_tca_kind(qdisc))); +} + +int link_find_qdisc(Link *link, uint32_t handle, const char *kind, QDisc **ret) { + QDisc *qdisc; + + assert(link); + + SET_FOREACH(qdisc, link->qdiscs) { + if (qdisc->handle != handle) + continue; + + if (!qdisc_exists(qdisc)) + continue; + + if (kind && !streq_ptr(kind, qdisc_get_tca_kind(qdisc))) + continue; + + if (ret) + *ret = qdisc; + return 0; + } + + return -ENOENT; +} + +QDisc* qdisc_drop(QDisc *qdisc) { + TClass *tclass; + Link *link; + + assert(qdisc); + + link = ASSERT_PTR(qdisc->link); + + /* also drop all child classes assigned to the qdisc. */ + SET_FOREACH(tclass, link->tclasses) { + if (TC_H_MAJ(tclass->classid) != qdisc->handle) + continue; + + tclass_drop(tclass); + } + + qdisc_enter_removed(qdisc); + + if (qdisc->state == 0) { + log_qdisc_debug(qdisc, link, "Forgetting"); + qdisc = qdisc_free(qdisc); + } else + log_qdisc_debug(qdisc, link, "Removed"); + + return qdisc; +} + +static int qdisc_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, QDisc *qdisc) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_error_errno(link, m, r, "Could not set QDisc"); + link_enter_failed(link); + return 1; + } + + if (link->tc_messages == 0) { + log_link_debug(link, "Traffic control configured"); + link->tc_configured = true; + link_check_ready(link); + } + + return 1; +} + +static int qdisc_configure(QDisc *qdisc, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(qdisc); + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + assert(link->ifindex > 0); + assert(req); + + log_qdisc_debug(qdisc, link, "Configuring"); + + r = sd_rtnl_message_new_traffic_control(link->manager->rtnl, &m, RTM_NEWQDISC, + link->ifindex, qdisc->handle, qdisc->parent); + if (r < 0) + return r; + + r = sd_netlink_message_append_string(m, TCA_KIND, qdisc_get_tca_kind(qdisc)); + if (r < 0) + return r; + + if (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->fill_message) { + r = QDISC_VTABLE(qdisc)->fill_message(link, qdisc, m); + if (r < 0) + return r; + } + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static bool qdisc_is_ready_to_configure(QDisc *qdisc, Link *link) { + assert(qdisc); + assert(link); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return false; + + /* TC_H_CLSACT == TC_H_INGRESS */ + if (!IN_SET(qdisc->parent, TC_H_ROOT, TC_H_CLSACT)) { + if (TC_H_MIN(qdisc->parent) == 0) { + if (link_find_qdisc(link, qdisc->parent, NULL, NULL) < 0) + return false; + } else { + if (link_find_tclass(link, qdisc->parent, NULL) < 0) + return false; + } + } + + if (QDISC_VTABLE(qdisc) && + QDISC_VTABLE(qdisc)->is_ready && + QDISC_VTABLE(qdisc)->is_ready(qdisc, link) <= 0) + return false; + + return true; +} + +static int qdisc_process_request(Request *req, Link *link, QDisc *qdisc) { + int r; + + assert(req); + assert(link); + assert(qdisc); + + if (!qdisc_is_ready_to_configure(qdisc, link)) + return 0; + + r = qdisc_configure(qdisc, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure QDisc: %m"); + + qdisc_enter_configuring(qdisc); + return 1; +} + +int link_request_qdisc(Link *link, QDisc *qdisc) { + QDisc *existing; + int r; + + assert(link); + assert(qdisc); + + if (qdisc_get(link, qdisc, &existing) < 0) { + _cleanup_(qdisc_freep) QDisc *tmp = NULL; + + r = qdisc_dup(qdisc, &tmp); + if (r < 0) + return log_oom(); + + r = qdisc_add(link, tmp); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to store QDisc: %m"); + + existing = TAKE_PTR(tmp); + } else + existing->source = qdisc->source; + + log_qdisc_debug(existing, link, "Requesting"); + r = link_queue_request_safe(link, REQUEST_TYPE_TC_QDISC, + existing, NULL, + qdisc_hash_func, + qdisc_compare_func, + qdisc_process_request, + &link->tc_messages, + qdisc_handler, + NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request QDisc: %m"); + if (r == 0) + return 0; + + qdisc_enter_requesting(existing); + return 1; +} + +int manager_rtnl_process_qdisc(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) { + _cleanup_(qdisc_freep) QDisc *tmp = NULL; + QDisc *qdisc = NULL; + Link *link; + uint16_t type; + int ifindex, r; + + assert(rtnl); + assert(message); + assert(m); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "rtnl: failed to receive QDisc message, ignoring"); + + return 0; + } + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get message type, ignoring: %m"); + return 0; + } else if (!IN_SET(type, RTM_NEWQDISC, RTM_DELQDISC)) { + log_warning("rtnl: received unexpected message type %u when processing QDisc, ignoring.", type); + return 0; + } + + r = sd_rtnl_message_traffic_control_get_ifindex(message, &ifindex); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m"); + return 0; + } else if (ifindex <= 0) { + log_warning("rtnl: received QDisc message with invalid ifindex %d, ignoring.", ifindex); + return 0; + } + + if (link_get_by_index(m, ifindex, &link) < 0) { + if (!m->enumerating) + log_warning("rtnl: received QDisc for link '%d' we don't know about, ignoring.", ifindex); + return 0; + } + + r = qdisc_new(_QDISC_KIND_INVALID, &tmp); + if (r < 0) + return log_oom(); + + r = sd_rtnl_message_traffic_control_get_handle(message, &tmp->handle); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received QDisc message without handle, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_traffic_control_get_parent(message, &tmp->parent); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received QDisc message without parent, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_string_strdup(message, TCA_KIND, &tmp->tca_kind); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received QDisc message without kind, ignoring: %m"); + return 0; + } + + (void) qdisc_get(link, tmp, &qdisc); + + switch (type) { + case RTM_NEWQDISC: + if (qdisc) { + qdisc_enter_configured(qdisc); + log_qdisc_debug(qdisc, link, "Received remembered"); + } else { + qdisc_enter_configured(tmp); + log_qdisc_debug(tmp, link, "Received new"); + + r = qdisc_add(link, tmp); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to remember QDisc, ignoring: %m"); + return 0; + } + + qdisc = TAKE_PTR(tmp); + } + + if (!m->enumerating) { + /* Some kind of QDisc (e.g. tbf) also create an implicit class under the qdisc, but + * the kernel may not notify about the class. Hence, we need to enumerate classes. */ + r = link_enumerate_tclass(link, qdisc->handle); + if (r < 0) + log_link_warning_errno(link, r, "Failed to enumerate TClass, ignoring: %m"); + } + + break; + + case RTM_DELQDISC: + if (qdisc) + qdisc_drop(qdisc); + else + log_qdisc_debug(tmp, link, "Kernel removed unknown"); + + break; + + default: + assert_not_reached(); + } + + return 1; +} + +static int qdisc_section_verify(QDisc *qdisc, bool *has_root, bool *has_clsact) { + int r; + + assert(qdisc); + assert(has_root); + assert(has_clsact); + + if (section_is_invalid(qdisc->section)) + return -EINVAL; + + if (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->verify) { + r = QDISC_VTABLE(qdisc)->verify(qdisc); + if (r < 0) + return r; + } + + if (qdisc->parent == TC_H_ROOT) { + if (*has_root) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: More than one root qdisc section is defined. " + "Ignoring the qdisc section from line %u.", + qdisc->section->filename, qdisc->section->line); + *has_root = true; + } else if (qdisc->parent == TC_H_CLSACT) { /* TC_H_CLSACT == TC_H_INGRESS */ + if (*has_clsact) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: More than one clsact or ingress qdisc section is defined. " + "Ignoring the qdisc section from line %u.", + qdisc->section->filename, qdisc->section->line); + *has_clsact = true; + } + + return 0; +} + +void network_drop_invalid_qdisc(Network *network) { + bool has_root = false, has_clsact = false; + QDisc *qdisc; + + assert(network); + + HASHMAP_FOREACH(qdisc, network->qdiscs_by_section) + if (qdisc_section_verify(qdisc, &has_root, &has_clsact) < 0) + qdisc_free(qdisc); +} + +int config_parse_qdisc_parent( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(ltype, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + if (streq(rvalue, "root")) + qdisc->parent = TC_H_ROOT; + else if (streq(rvalue, "clsact")) { + qdisc->parent = TC_H_CLSACT; + qdisc->handle = TC_H_MAKE(TC_H_CLSACT, 0); + } else if (streq(rvalue, "ingress")) { + qdisc->parent = TC_H_INGRESS; + qdisc->handle = TC_H_MAKE(TC_H_INGRESS, 0); + } else { + r = parse_handle(rvalue, &qdisc->parent); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse 'Parent=', ignoring assignment: %s", + rvalue); + return 0; + } + } + + if (STR_IN_SET(rvalue, "clsact", "ingress")) { + r = free_and_strdup(&qdisc->tca_kind, rvalue); + if (r < 0) + return log_oom(); + } else + qdisc->tca_kind = mfree(qdisc->tca_kind); + + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_qdisc_handle( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + uint16_t n; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(ltype, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + if (isempty(rvalue)) { + qdisc->handle = TC_H_UNSPEC; + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou16_full(rvalue, 16, &n); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse 'Handle=', ignoring assignment: %s", + rvalue); + return 0; + } + + qdisc->handle = (uint32_t) n << 16; + TAKE_PTR(qdisc); + + return 0; +} diff --git a/src/network/tc/qdisc.h b/src/network/tc/qdisc.h new file mode 100644 index 0000000..a62b941 --- /dev/null +++ b/src/network/tc/qdisc.h @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "networkd-util.h" + +typedef struct Link Link; +typedef struct Manager Manager; +typedef struct Network Network; + +typedef enum QDiscKind { + QDISC_KIND_BFIFO, + QDISC_KIND_CAKE, + QDISC_KIND_CODEL, + QDISC_KIND_DRR, + QDISC_KIND_ETS, + QDISC_KIND_FQ, + QDISC_KIND_FQ_CODEL, + QDISC_KIND_FQ_PIE, + QDISC_KIND_GRED, + QDISC_KIND_HHF, + QDISC_KIND_HTB, + QDISC_KIND_NETEM, + QDISC_KIND_PFIFO, + QDISC_KIND_PFIFO_FAST, + QDISC_KIND_PFIFO_HEAD_DROP, + QDISC_KIND_PIE, + QDISC_KIND_QFQ, + QDISC_KIND_SFB, + QDISC_KIND_SFQ, + QDISC_KIND_TBF, + QDISC_KIND_TEQL, + _QDISC_KIND_MAX, + _QDISC_KIND_INVALID = -EINVAL, +} QDiscKind; + +typedef struct QDisc { + Link *link; + Network *network; + ConfigSection *section; + NetworkConfigSource source; + NetworkConfigState state; + + uint32_t handle; + uint32_t parent; + + char *tca_kind; + QDiscKind kind; +} QDisc; + +typedef struct QDiscVTable { + size_t object_size; + const char *tca_kind; + /* called in qdisc_new() */ + int (*init)(QDisc *qdisc); + int (*fill_message)(Link *link, QDisc *qdisc, sd_netlink_message *m); + int (*verify)(QDisc *qdisc); + int (*is_ready)(QDisc *qdisc, Link *link); +} QDiscVTable; + +extern const QDiscVTable * const qdisc_vtable[_QDISC_KIND_MAX]; + +#define QDISC_VTABLE(q) ((q)->kind != _QDISC_KIND_INVALID ? qdisc_vtable[(q)->kind] : NULL) + +/* For casting a qdisc into the various qdisc kinds */ +#define DEFINE_QDISC_CAST(UPPERCASE, MixedCase) \ + static inline MixedCase* UPPERCASE(QDisc *q) { \ + if (_unlikely_(!q || q->kind != QDISC_KIND_##UPPERCASE)) \ + return NULL; \ + \ + return (MixedCase*) q; \ + } + +DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(QDisc, qdisc); + +QDisc* qdisc_free(QDisc *qdisc); +int qdisc_new_static(QDiscKind kind, Network *network, const char *filename, unsigned section_line, QDisc **ret); + +QDisc* qdisc_drop(QDisc *qdisc); + +int link_find_qdisc(Link *link, uint32_t handle, const char *kind, QDisc **qdisc); + +int link_request_qdisc(Link *link, QDisc *qdisc); + +void network_drop_invalid_qdisc(Network *network); + +int manager_rtnl_process_qdisc(sd_netlink *rtnl, sd_netlink_message *message, Manager *m); + +DEFINE_SECTION_CLEANUP_FUNCTIONS(QDisc, qdisc_free); + +CONFIG_PARSER_PROTOTYPE(config_parse_qdisc_parent); +CONFIG_PARSER_PROTOTYPE(config_parse_qdisc_handle); + +#include "cake.h" +#include "codel.h" +#include "ets.h" +#include "fifo.h" +#include "fq-codel.h" +#include "fq-pie.h" +#include "fq.h" +#include "gred.h" +#include "hhf.h" +#include "htb.h" +#include "pie.h" +#include "qfq.h" +#include "netem.h" +#include "drr.h" +#include "sfb.h" +#include "sfq.h" +#include "tbf.h" +#include "teql.h" diff --git a/src/network/tc/qfq.c b/src/network/tc/qfq.c new file mode 100644 index 0000000..7702e6f --- /dev/null +++ b/src/network/tc/qfq.c @@ -0,0 +1,177 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "parse-util.h" +#include "qdisc.h" +#include "qfq.h" +#include "string-util.h" + +#define QFQ_MAX_WEIGHT (1 << 10) +#define QFQ_MIN_MAX_PACKET 512 +#define QFQ_MAX_MAX_PACKET (1 << 16) + +const QDiscVTable qfq_vtable = { + .object_size = sizeof(QuickFairQueueing), + .tca_kind = "qfq", +}; + +static int quick_fair_queueing_class_fill_message(Link *link, TClass *tclass, sd_netlink_message *req) { + QuickFairQueueingClass *qfq; + int r; + + assert(link); + assert(tclass); + assert(req); + + assert_se(qfq = TCLASS_TO_QFQ(tclass)); + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "qfq"); + if (r < 0) + return r; + + if (qfq->weight > 0) { + r = sd_netlink_message_append_u32(req, TCA_QFQ_WEIGHT, qfq->weight); + if (r < 0) + return r; + } + + if (qfq->max_packet > 0) { + r = sd_netlink_message_append_u32(req, TCA_QFQ_LMAX, qfq->max_packet); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_quick_fair_queueing_weight( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL; + QuickFairQueueingClass *qfq; + Network *network = ASSERT_PTR(data); + uint32_t v; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = tclass_new_static(TCLASS_KIND_QFQ, network, filename, section_line, &tclass); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to create traffic control class, ignoring assignment: %m"); + return 0; + } + + qfq = TCLASS_TO_QFQ(tclass); + + if (isempty(rvalue)) { + qfq->weight = 0; + TAKE_PTR(tclass); + return 0; + } + + r = safe_atou32(rvalue, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (v == 0 || v > QFQ_MAX_WEIGHT) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + qfq->weight = v; + TAKE_PTR(tclass); + + return 0; +} + +int config_parse_quick_fair_queueing_max_packet( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL; + QuickFairQueueingClass *qfq; + Network *network = ASSERT_PTR(data); + uint64_t v; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = tclass_new_static(TCLASS_KIND_QFQ, network, filename, section_line, &tclass); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to create traffic control class, ignoring assignment: %m"); + return 0; + } + + qfq = TCLASS_TO_QFQ(tclass); + + if (isempty(rvalue)) { + qfq->max_packet = 0; + TAKE_PTR(tclass); + return 0; + } + + r = parse_size(rvalue, 1024, &v); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (v < QFQ_MIN_MAX_PACKET || v > QFQ_MAX_MAX_PACKET) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + qfq->max_packet = (uint32_t) v; + TAKE_PTR(tclass); + + return 0; +} + +const TClassVTable qfq_tclass_vtable = { + .object_size = sizeof(QuickFairQueueingClass), + .tca_kind = "qfq", + .fill_message = quick_fair_queueing_class_fill_message, +}; diff --git a/src/network/tc/qfq.h b/src/network/tc/qfq.h new file mode 100644 index 0000000..0f013a9 --- /dev/null +++ b/src/network/tc/qfq.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct QuickFairQueueing { + QDisc meta; +} QuickFairQueueing; + +DEFINE_QDISC_CAST(QFQ, QuickFairQueueing); +extern const QDiscVTable qfq_vtable; + +typedef struct QuickFairQueueingClass { + TClass meta; + + uint32_t weight; + uint32_t max_packet; +} QuickFairQueueingClass; + +DEFINE_TCLASS_CAST(QFQ, QuickFairQueueingClass); +extern const TClassVTable qfq_tclass_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_quick_fair_queueing_weight); +CONFIG_PARSER_PROTOTYPE(config_parse_quick_fair_queueing_max_packet); diff --git a/src/network/tc/sfb.c b/src/network/tc/sfb.c new file mode 100644 index 0000000..861c5fe --- /dev/null +++ b/src/network/tc/sfb.c @@ -0,0 +1,107 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "qdisc.h" +#include "sfb.h" +#include "string-util.h" + +static int stochastic_fair_blue_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + StochasticFairBlue *sfb; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(sfb = SFB(qdisc)); + + const struct tc_sfb_qopt opt = { + .rehash_interval = 600*1000, + .warmup_time = 60*1000, + .penalty_rate = 10, + .penalty_burst = 20, + .increment = (SFB_MAX_PROB + 1000) / 2000, + .decrement = (SFB_MAX_PROB + 10000) / 20000, + .max = 25, + .bin_size = 20, + .limit = sfb->packet_limit, + }; + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "sfb"); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, TCA_SFB_PARMS, &opt, sizeof(opt)); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_stochastic_fair_blue_u32( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + StochasticFairBlue *sfb; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_SFB, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + sfb = SFB(qdisc); + + if (isempty(rvalue)) { + sfb->packet_limit = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou32(rvalue, &sfb->packet_limit); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +const QDiscVTable sfb_vtable = { + .object_size = sizeof(StochasticFairBlue), + .tca_kind = "sfb", + .fill_message = stochastic_fair_blue_fill_message, +}; diff --git a/src/network/tc/sfb.h b/src/network/tc/sfb.h new file mode 100644 index 0000000..628df35 --- /dev/null +++ b/src/network/tc/sfb.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2020 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct StochasticFairBlue { + QDisc meta; + + uint32_t packet_limit; +} StochasticFairBlue; + +DEFINE_QDISC_CAST(SFB, StochasticFairBlue); +extern const QDiscVTable sfb_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_stochastic_fair_blue_u32); diff --git a/src/network/tc/sfq.c b/src/network/tc/sfq.c new file mode 100644 index 0000000..92dbae1 --- /dev/null +++ b/src/network/tc/sfq.c @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "qdisc.h" +#include "sfq.h" +#include "string-util.h" + +static int stochastic_fairness_queueing_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + StochasticFairnessQueueing *sfq; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(sfq = SFQ(qdisc)); + + const struct tc_sfq_qopt_v1 opt = { + .v0.perturb_period = sfq->perturb_period / USEC_PER_SEC, + }; + + r = sd_netlink_message_append_data(req, TCA_OPTIONS, &opt, sizeof(opt)); + if (r < 0) + return r; + + return 0; +} + +int config_parse_stochastic_fairness_queueing_perturb_period( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + StochasticFairnessQueueing *sfq; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_SFQ, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + sfq = SFQ(qdisc); + + if (isempty(rvalue)) { + sfq->perturb_period = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_sec(rvalue, &sfq->perturb_period); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + TAKE_PTR(qdisc); + + return 0; +} + +const QDiscVTable sfq_vtable = { + .object_size = sizeof(StochasticFairnessQueueing), + .tca_kind = "sfq", + .fill_message = stochastic_fairness_queueing_fill_message, +}; diff --git a/src/network/tc/sfq.h b/src/network/tc/sfq.h new file mode 100644 index 0000000..1626775 --- /dev/null +++ b/src/network/tc/sfq.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" +#include "time-util.h" + +typedef struct StochasticFairnessQueueing { + QDisc meta; + + usec_t perturb_period; +} StochasticFairnessQueueing; + +DEFINE_QDISC_CAST(SFQ, StochasticFairnessQueueing); +extern const QDiscVTable sfq_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_stochastic_fairness_queueing_perturb_period); diff --git a/src/network/tc/tbf.c b/src/network/tc/tbf.c new file mode 100644 index 0000000..647fc8c --- /dev/null +++ b/src/network/tc/tbf.c @@ -0,0 +1,343 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include <linux/pkt_sched.h> +#include <math.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "netem.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "parse-util.h" +#include "qdisc.h" +#include "string-util.h" +#include "strv.h" +#include "tc-util.h" + +static int token_bucket_filter_fill_message(Link *link, QDisc *qdisc, sd_netlink_message *req) { + uint32_t rtab[256], ptab[256]; + TokenBucketFilter *tbf; + int r; + + assert(link); + assert(qdisc); + assert(req); + + assert_se(tbf = TBF(qdisc)); + + struct tc_tbf_qopt opt = { + .rate.rate = tbf->rate >= (1ULL << 32) ? ~0U : tbf->rate, + .peakrate.rate = tbf->peak_rate >= (1ULL << 32) ? ~0U : tbf->peak_rate, + .rate.mpu = tbf->mpu, + }; + + if (tbf->limit > 0) + opt.limit = tbf->limit; + else { + double lim, lim2; + + lim = tbf->rate * (double) tbf->latency / USEC_PER_SEC + tbf->burst; + if (tbf->peak_rate > 0) { + lim2 = tbf->peak_rate * (double) tbf->latency / USEC_PER_SEC + tbf->mtu; + lim = MIN(lim, lim2); + } + opt.limit = lim; + } + + r = tc_fill_ratespec_and_table(&opt.rate, rtab, tbf->mtu); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to calculate ratespec: %m"); + + r = tc_transmit_time(opt.rate.rate, tbf->burst, &opt.buffer); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to calculate buffer size: %m"); + + if (opt.peakrate.rate > 0) { + opt.peakrate.mpu = tbf->mpu; + + r = tc_fill_ratespec_and_table(&opt.peakrate, ptab, tbf->mtu); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to calculate ratespec: %m"); + + r = tc_transmit_time(opt.peakrate.rate, tbf->mtu, &opt.mtu); + if (r < 0) + return log_link_debug_errno(link, r, "Failed to calculate mtu size: %m"); + } + + r = sd_netlink_message_open_container_union(req, TCA_OPTIONS, "tbf"); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, TCA_TBF_PARMS, &opt, sizeof(opt)); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, TCA_TBF_BURST, &tbf->burst, sizeof(tbf->burst)); + if (r < 0) + return r; + + if (tbf->rate >= (1ULL << 32)) { + r = sd_netlink_message_append_u64(req, TCA_TBF_RATE64, tbf->rate); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_data(req, TCA_TBF_RTAB, rtab, sizeof(rtab)); + if (r < 0) + return r; + + if (opt.peakrate.rate > 0) { + if (tbf->peak_rate >= (1ULL << 32)) { + r = sd_netlink_message_append_u64(req, TCA_TBF_PRATE64, tbf->peak_rate); + if (r < 0) + return r; + } + + r = sd_netlink_message_append_u32(req, TCA_TBF_PBURST, tbf->mtu); + if (r < 0) + return r; + + r = sd_netlink_message_append_data(req, TCA_TBF_PTAB, ptab, sizeof(ptab)); + if (r < 0) + return r; + } + + r = sd_netlink_message_close_container(req); + if (r < 0) + return r; + + return 0; +} + +int config_parse_token_bucket_filter_size( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + TokenBucketFilter *tbf; + uint64_t k; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_TBF, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + tbf = TBF(qdisc); + + if (isempty(rvalue)) { + if (STR_IN_SET(lvalue, "BurstBytes", "Burst")) + tbf->burst = 0; + else if (STR_IN_SET(lvalue, "LimitBytes", "LimitSize")) + tbf->limit = 0; + else if (streq(lvalue, "MTUBytes")) + tbf->mtu = 0; + else if (streq(lvalue, "MPUBytes")) + tbf->mpu = 0; + else + assert_not_reached(); + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_size(rvalue, 1024, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + if (STR_IN_SET(lvalue, "BurstBytes", "Burst")) + tbf->burst = k; + else if (STR_IN_SET(lvalue, "LimitBytes", "LimitSize")) + tbf->limit = k; + else if (streq(lvalue, "MPUBytes")) + tbf->mpu = k; + else if (streq(lvalue, "MTUBytes")) + tbf->mtu = k; + else + assert_not_reached(); + + TAKE_PTR(qdisc); + + return 0; +} + +int config_parse_token_bucket_filter_rate( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + TokenBucketFilter *tbf; + uint64_t k, *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_TBF, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + tbf = TBF(qdisc); + if (streq(lvalue, "Rate")) + p = &tbf->rate; + else if (streq(lvalue, "PeakRate")) + p = &tbf->peak_rate; + else + assert_not_reached(); + + if (isempty(rvalue)) { + *p = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = parse_size(rvalue, 1000, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + *p = k / 8; + + qdisc = NULL; + + return 0; +} + +int config_parse_token_bucket_filter_latency( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + Network *network = ASSERT_PTR(data); + TokenBucketFilter *tbf; + usec_t u; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_TBF, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + tbf = TBF(qdisc); + + if (isempty(rvalue)) { + tbf->latency = 0; + + qdisc = NULL; + return 0; + } + + r = parse_sec(rvalue, &u); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + tbf->latency = u; + + qdisc = NULL; + + return 0; +} + +static int token_bucket_filter_verify(QDisc *qdisc) { + TokenBucketFilter *tbf = TBF(qdisc); + + if (tbf->limit > 0 && tbf->latency > 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Specifying both LimitBytes= and LatencySec= is not allowed. " + "Ignoring [TokenBucketFilter] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + if (tbf->limit == 0 && tbf->latency == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Either LimitBytes= or LatencySec= is required. " + "Ignoring [TokenBucketFilter] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + if (tbf->rate == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: Rate= is mandatory. " + "Ignoring [TokenBucketFilter] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + if (tbf->burst == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: BurstBytes= is mandatory. " + "Ignoring [TokenBucketFilter] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + if (tbf->peak_rate > 0 && tbf->mtu == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: MTUBytes= is mandatory when PeakRate= is specified. " + "Ignoring [TokenBucketFilter] section from line %u.", + qdisc->section->filename, qdisc->section->line); + + return 0; +} + +const QDiscVTable tbf_vtable = { + .object_size = sizeof(TokenBucketFilter), + .tca_kind = "tbf", + .fill_message = token_bucket_filter_fill_message, + .verify = token_bucket_filter_verify +}; diff --git a/src/network/tc/tbf.h b/src/network/tc/tbf.h new file mode 100644 index 0000000..6b4b017 --- /dev/null +++ b/src/network/tc/tbf.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" +#include "time-util.h" + +typedef struct TokenBucketFilter { + QDisc meta; + + uint64_t rate; + uint64_t peak_rate; + uint32_t burst; + uint32_t mtu; + usec_t latency; + size_t limit; + size_t mpu; +} TokenBucketFilter; + +DEFINE_QDISC_CAST(TBF, TokenBucketFilter); +extern const QDiscVTable tbf_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_token_bucket_filter_latency); +CONFIG_PARSER_PROTOTYPE(config_parse_token_bucket_filter_size); +CONFIG_PARSER_PROTOTYPE(config_parse_token_bucket_filter_rate); diff --git a/src/network/tc/tc-util.c b/src/network/tc/tc-util.c new file mode 100644 index 0000000..3781182 --- /dev/null +++ b/src/network/tc/tc-util.c @@ -0,0 +1,133 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include "alloc-util.h" +#include "extract-word.h" +#include "fileio.h" +#include "parse-util.h" +#include "percent-util.h" +#include "tc-util.h" +#include "time-util.h" + +int tc_init(double *ret_ticks_in_usec, uint32_t *ret_hz) { + static double ticks_in_usec = -1; + static uint32_t hz; + + if (ticks_in_usec < 0) { + uint32_t clock_resolution, ticks_to_usec, usec_to_ticks; + _cleanup_free_ char *line = NULL; + double clock_factor; + int r; + + r = read_one_line_file("/proc/net/psched", &line); + if (r < 0) + return r; + + r = sscanf(line, "%08x%08x%08x%08x", &ticks_to_usec, &usec_to_ticks, &clock_resolution, &hz); + if (r < 4) + return -EIO; + + clock_factor = (double) clock_resolution / USEC_PER_SEC; + ticks_in_usec = (double) ticks_to_usec / usec_to_ticks * clock_factor; + } + + if (ret_ticks_in_usec) + *ret_ticks_in_usec = ticks_in_usec; + if (ret_hz) + *ret_hz = hz; + + return 0; +} + +int tc_time_to_tick(usec_t t, uint32_t *ret) { + double ticks_in_usec; + usec_t a; + int r; + + assert(ret); + + r = tc_init(&ticks_in_usec, NULL); + if (r < 0) + return r; + + a = t * ticks_in_usec; + if (a > UINT32_MAX) + return -ERANGE; + + *ret = a; + return 0; +} + +int parse_tc_percent(const char *s, uint32_t *ret_fraction) { + int r; + + assert(s); + assert(ret_fraction); + + r = parse_permyriad(s); + if (r < 0) + return r; + + *ret_fraction = (double) r / 10000 * UINT32_MAX; + return 0; +} + +int tc_transmit_time(uint64_t rate, uint32_t size, uint32_t *ret) { + return tc_time_to_tick(USEC_PER_SEC * ((double)size / (double)rate), ret); +} + +int tc_fill_ratespec_and_table(struct tc_ratespec *rate, uint32_t *rtab, uint32_t mtu) { + uint32_t cell_log = 0; + int r; + + if (mtu == 0) + mtu = 2047; + + while ((mtu >> cell_log) > 255) + cell_log++; + + for (size_t i = 0; i < 256; i++) { + uint32_t sz; + + sz = (i + 1) << cell_log; + if (sz < rate->mpu) + sz = rate->mpu; + r = tc_transmit_time(rate->rate, sz, &rtab[i]); + if (r < 0) + return r; + } + + rate->cell_align = -1; + rate->cell_log = cell_log; + rate->linklayer = TC_LINKLAYER_ETHERNET; + return 0; +} + +int parse_handle(const char *t, uint32_t *ret) { + _cleanup_free_ char *word = NULL; + uint16_t major, minor; + int r; + + assert(t); + assert(ret); + + /* Extract the major number. */ + r = extract_first_word(&t, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + if (!t) + return -EINVAL; + + r = safe_atou16_full(word, 16, &major); + if (r < 0) + return r; + + r = safe_atou16_full(t, 16, &minor); + if (r < 0) + return r; + + *ret = ((uint32_t) major << 16) | minor; + return 0; +} diff --git a/src/network/tc/tc-util.h b/src/network/tc/tc-util.h new file mode 100644 index 0000000..83bad8e --- /dev/null +++ b/src/network/tc/tc-util.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include <linux/pkt_sched.h> + +#include "time-util.h" + +int tc_init(double *ret_ticks_in_usec, uint32_t *ret_hz); +int tc_time_to_tick(usec_t t, uint32_t *ret); +int parse_tc_percent(const char *s, uint32_t *percent); +int tc_transmit_time(uint64_t rate, uint32_t size, uint32_t *ret); +int tc_fill_ratespec_and_table(struct tc_ratespec *rate, uint32_t *rtab, uint32_t mtu); +int parse_handle(const char *t, uint32_t *ret); diff --git a/src/network/tc/tc.c b/src/network/tc/tc.c new file mode 100644 index 0000000..8a1c5b3 --- /dev/null +++ b/src/network/tc/tc.c @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "macro.h" +#include "networkd-link.h" +#include "networkd-network.h" +#include "qdisc.h" +#include "tc.h" +#include "tclass.h" + +int link_request_traffic_control(Link *link) { + TClass *tclass; + QDisc *qdisc; + int r; + + assert(link); + assert(link->network); + + link->tc_configured = false; + + HASHMAP_FOREACH(qdisc, link->network->qdiscs_by_section) { + r = link_request_qdisc(link, qdisc); + if (r < 0) + return r; + } + + HASHMAP_FOREACH(tclass, link->network->tclasses_by_section) { + r = link_request_tclass(link, tclass); + if (r < 0) + return r; + } + + if (link->tc_messages == 0) { + link->tc_configured = true; + link_check_ready(link); + } else { + log_link_debug(link, "Setting traffic control"); + link_set_state(link, LINK_STATE_CONFIGURING); + } + + return 0; +} diff --git a/src/network/tc/tc.h b/src/network/tc/tc.h new file mode 100644 index 0000000..6226578 --- /dev/null +++ b/src/network/tc/tc.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct Link Link; + +int link_request_traffic_control(Link *link); diff --git a/src/network/tc/tclass.c b/src/network/tc/tclass.c new file mode 100644 index 0000000..0a5fec0 --- /dev/null +++ b/src/network/tc/tclass.c @@ -0,0 +1,639 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ + +#include <linux/pkt_sched.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "in-addr-util.h" +#include "netlink-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "networkd-queue.h" +#include "parse-util.h" +#include "set.h" +#include "string-util.h" +#include "strv.h" +#include "tc-util.h" +#include "tclass.h" + +const TClassVTable * const tclass_vtable[_TCLASS_KIND_MAX] = { + [TCLASS_KIND_DRR] = &drr_tclass_vtable, + [TCLASS_KIND_HTB] = &htb_tclass_vtable, + [TCLASS_KIND_QFQ] = &qfq_tclass_vtable, +}; + +static int tclass_new(TClassKind kind, TClass **ret) { + _cleanup_(tclass_freep) TClass *tclass = NULL; + int r; + + if (kind == _TCLASS_KIND_INVALID) { + tclass = new(TClass, 1); + if (!tclass) + return -ENOMEM; + + *tclass = (TClass) { + .parent = TC_H_ROOT, + .kind = kind, + }; + } else { + assert(kind >= 0 && kind < _TCLASS_KIND_MAX); + tclass = malloc0(tclass_vtable[kind]->object_size); + if (!tclass) + return -ENOMEM; + + tclass->parent = TC_H_ROOT; + tclass->kind = kind; + + if (TCLASS_VTABLE(tclass)->init) { + r = TCLASS_VTABLE(tclass)->init(tclass); + if (r < 0) + return r; + } + } + + *ret = TAKE_PTR(tclass); + + return 0; +} + +int tclass_new_static(TClassKind kind, Network *network, const char *filename, unsigned section_line, TClass **ret) { + _cleanup_(config_section_freep) ConfigSection *n = NULL; + _cleanup_(tclass_freep) TClass *tclass = NULL; + TClass *existing; + int r; + + assert(network); + assert(ret); + assert(filename); + assert(section_line > 0); + + r = config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + existing = hashmap_get(network->tclasses_by_section, n); + if (existing) { + if (existing->kind != kind) + return -EINVAL; + + *ret = existing; + return 0; + } + + r = tclass_new(kind, &tclass); + if (r < 0) + return r; + + tclass->network = network; + tclass->section = TAKE_PTR(n); + tclass->source = NETWORK_CONFIG_SOURCE_STATIC; + + r = hashmap_ensure_put(&network->tclasses_by_section, &config_section_hash_ops, tclass->section, tclass); + if (r < 0) + return r; + + *ret = TAKE_PTR(tclass); + return 0; +} + +TClass* tclass_free(TClass *tclass) { + if (!tclass) + return NULL; + + if (tclass->network && tclass->section) + hashmap_remove(tclass->network->tclasses_by_section, tclass->section); + + config_section_free(tclass->section); + + if (tclass->link) + set_remove(tclass->link->tclasses, tclass); + + free(tclass->tca_kind); + return mfree(tclass); +} + +static const char *tclass_get_tca_kind(const TClass *tclass) { + assert(tclass); + + return (TCLASS_VTABLE(tclass) && TCLASS_VTABLE(tclass)->tca_kind) ? + TCLASS_VTABLE(tclass)->tca_kind : tclass->tca_kind; +} + +static void tclass_hash_func(const TClass *tclass, struct siphash *state) { + assert(tclass); + assert(state); + + siphash24_compress(&tclass->classid, sizeof(tclass->classid), state); + siphash24_compress(&tclass->parent, sizeof(tclass->parent), state); + siphash24_compress_string(tclass_get_tca_kind(tclass), state); +} + +static int tclass_compare_func(const TClass *a, const TClass *b) { + int r; + + assert(a); + assert(b); + + r = CMP(a->classid, b->classid); + if (r != 0) + return r; + + r = CMP(a->parent, b->parent); + if (r != 0) + return r; + + return strcmp_ptr(tclass_get_tca_kind(a), tclass_get_tca_kind(b)); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + tclass_hash_ops, + TClass, + tclass_hash_func, + tclass_compare_func, + tclass_free); + +static int tclass_get(Link *link, const TClass *in, TClass **ret) { + TClass *existing; + + assert(link); + assert(in); + + existing = set_get(link->tclasses, in); + if (!existing) + return -ENOENT; + + if (ret) + *ret = existing; + return 0; +} + +static int tclass_add(Link *link, TClass *tclass) { + int r; + + assert(link); + assert(tclass); + + r = set_ensure_put(&link->tclasses, &tclass_hash_ops, tclass); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + tclass->link = link; + return 0; +} + +static int tclass_dup(const TClass *src, TClass **ret) { + _cleanup_(tclass_freep) TClass *dst = NULL; + + assert(src); + assert(ret); + + if (TCLASS_VTABLE(src)) + dst = memdup(src, TCLASS_VTABLE(src)->object_size); + else + dst = newdup(TClass, src, 1); + if (!dst) + return -ENOMEM; + + /* clear all pointers */ + dst->network = NULL; + dst->section = NULL; + dst->link = NULL; + dst->tca_kind = NULL; + + if (src->tca_kind) { + dst->tca_kind = strdup(src->tca_kind); + if (!dst->tca_kind) + return -ENOMEM; + } + + *ret = TAKE_PTR(dst); + return 0; +} + +int link_find_tclass(Link *link, uint32_t classid, TClass **ret) { + TClass *tclass; + + assert(link); + + SET_FOREACH(tclass, link->tclasses) { + if (tclass->classid != classid) + continue; + + if (!tclass_exists(tclass)) + continue; + + if (ret) + *ret = tclass; + return 0; + } + + return -ENOENT; +} + +static void log_tclass_debug(TClass *tclass, Link *link, const char *str) { + _cleanup_free_ char *state = NULL; + + assert(tclass); + assert(str); + + if (!DEBUG_LOGGING) + return; + + (void) network_config_state_to_string_alloc(tclass->state, &state); + + log_link_debug(link, "%s %s TClass (%s): classid=%"PRIx32":%"PRIx32", parent=%"PRIx32":%"PRIx32", kind=%s", + str, strna(network_config_source_to_string(tclass->source)), strna(state), + TC_H_MAJ(tclass->classid) >> 16, TC_H_MIN(tclass->classid), + TC_H_MAJ(tclass->parent) >> 16, TC_H_MIN(tclass->parent), + strna(tclass_get_tca_kind(tclass))); +} + +TClass* tclass_drop(TClass *tclass) { + QDisc *qdisc; + Link *link; + + assert(tclass); + + link = ASSERT_PTR(tclass->link); + + /* Also drop all child qdiscs assigned to the class. */ + SET_FOREACH(qdisc, link->qdiscs) { + if (qdisc->parent != tclass->classid) + continue; + + qdisc_drop(qdisc); + } + + tclass_enter_removed(tclass); + + if (tclass->state == 0) { + log_tclass_debug(tclass, link, "Forgetting"); + tclass = tclass_free(tclass); + } else + log_tclass_debug(tclass, link, "Removed"); + + return tclass; +} + +static int tclass_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, TClass *tclass) { + int r; + + assert(m); + assert(link); + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_error_errno(link, m, r, "Could not set TClass"); + link_enter_failed(link); + return 1; + } + + if (link->tc_messages == 0) { + log_link_debug(link, "Traffic control configured"); + link->tc_configured = true; + link_check_ready(link); + } + + return 1; +} + +static int tclass_configure(TClass *tclass, Link *link, Request *req) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(tclass); + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + assert(link->ifindex > 0); + assert(req); + + log_tclass_debug(tclass, link, "Configuring"); + + r = sd_rtnl_message_new_traffic_control(link->manager->rtnl, &m, RTM_NEWTCLASS, + link->ifindex, tclass->classid, tclass->parent); + if (r < 0) + return r; + + r = sd_netlink_message_append_string(m, TCA_KIND, TCLASS_VTABLE(tclass)->tca_kind); + if (r < 0) + return r; + + if (TCLASS_VTABLE(tclass)->fill_message) { + r = TCLASS_VTABLE(tclass)->fill_message(link, tclass, m); + if (r < 0) + return r; + } + + return request_call_netlink_async(link->manager->rtnl, m, req); +} + +static bool tclass_is_ready_to_configure(TClass *tclass, Link *link) { + assert(tclass); + assert(link); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return false; + + return link_find_qdisc(link, TC_H_MAJ(tclass->classid), tclass_get_tca_kind(tclass), NULL) >= 0; +} + +static int tclass_process_request(Request *req, Link *link, TClass *tclass) { + int r; + + assert(req); + assert(link); + assert(tclass); + + if (!tclass_is_ready_to_configure(tclass, link)) + return 0; + + r = tclass_configure(tclass, link, req); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to configure TClass: %m"); + + tclass_enter_configuring(tclass); + return 1; +} + +int link_request_tclass(Link *link, TClass *tclass) { + TClass *existing; + int r; + + assert(link); + assert(tclass); + + if (tclass_get(link, tclass, &existing) < 0) { + _cleanup_(tclass_freep) TClass *tmp = NULL; + + r = tclass_dup(tclass, &tmp); + if (r < 0) + return log_oom(); + + r = tclass_add(link, tmp); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to store TClass: %m"); + + existing = TAKE_PTR(tmp); + } else + existing->source = tclass->source; + + log_tclass_debug(existing, link, "Requesting"); + r = link_queue_request_safe(link, REQUEST_TYPE_TC_CLASS, + existing, NULL, + tclass_hash_func, + tclass_compare_func, + tclass_process_request, + &link->tc_messages, + tclass_handler, + NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request TClass: %m"); + if (r == 0) + return 0; + + tclass_enter_requesting(existing); + return 1; +} + +int manager_rtnl_process_tclass(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) { + _cleanup_(tclass_freep) TClass *tmp = NULL; + TClass *tclass = NULL; + Link *link; + uint16_t type; + int ifindex, r; + + assert(rtnl); + assert(message); + assert(m); + + if (sd_netlink_message_is_error(message)) { + r = sd_netlink_message_get_errno(message); + if (r < 0) + log_message_warning_errno(message, r, "rtnl: failed to receive TClass message, ignoring"); + + return 0; + } + + r = sd_netlink_message_get_type(message, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get message type, ignoring: %m"); + return 0; + } else if (!IN_SET(type, RTM_NEWTCLASS, RTM_DELTCLASS)) { + log_warning("rtnl: received unexpected message type %u when processing TClass, ignoring.", type); + return 0; + } + + r = sd_rtnl_message_traffic_control_get_ifindex(message, &ifindex); + if (r < 0) { + log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m"); + return 0; + } else if (ifindex <= 0) { + log_warning("rtnl: received TClass message with invalid ifindex %d, ignoring.", ifindex); + return 0; + } + + if (link_get_by_index(m, ifindex, &link) < 0) { + if (!m->enumerating) + log_warning("rtnl: received TClass for link '%d' we don't know about, ignoring.", ifindex); + return 0; + } + + r = tclass_new(_TCLASS_KIND_INVALID, &tmp); + if (r < 0) + return log_oom(); + + r = sd_rtnl_message_traffic_control_get_handle(message, &tmp->classid); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received TClass message without handle, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_traffic_control_get_parent(message, &tmp->parent); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received TClass message without parent, ignoring: %m"); + return 0; + } + + r = sd_netlink_message_read_string_strdup(message, TCA_KIND, &tmp->tca_kind); + if (r < 0) { + log_link_warning_errno(link, r, "rtnl: received TClass message without kind, ignoring: %m"); + return 0; + } + + (void) tclass_get(link, tmp, &tclass); + + switch (type) { + case RTM_NEWTCLASS: + if (tclass) { + tclass_enter_configured(tclass); + log_tclass_debug(tclass, link, "Received remembered"); + } else { + tclass_enter_configured(tmp); + log_tclass_debug(tmp, link, "Received new"); + + r = tclass_add(link, tmp); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to remember TClass, ignoring: %m"); + return 0; + } + + tclass = TAKE_PTR(tmp); + } + + break; + + case RTM_DELTCLASS: + if (tclass) + (void) tclass_drop(tclass); + else + log_tclass_debug(tmp, link, "Kernel removed unknown"); + + break; + + default: + assert_not_reached(); + } + + return 1; +} + +int link_enumerate_tclass(Link *link, uint32_t parent) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(link); + assert(link->manager); + assert(link->manager->rtnl); + + r = sd_rtnl_message_new_traffic_control(link->manager->rtnl, &req, RTM_GETTCLASS, link->ifindex, 0, parent); + if (r < 0) + return r; + + return manager_enumerate_internal(link->manager, link->manager->rtnl, req, manager_rtnl_process_tclass); +} + +static int tclass_section_verify(TClass *tclass) { + int r; + + assert(tclass); + + if (section_is_invalid(tclass->section)) + return -EINVAL; + + if (TCLASS_VTABLE(tclass)->verify) { + r = TCLASS_VTABLE(tclass)->verify(tclass); + if (r < 0) + return r; + } + + return 0; +} + +void network_drop_invalid_tclass(Network *network) { + TClass *tclass; + + assert(network); + + HASHMAP_FOREACH(tclass, network->tclasses_by_section) + if (tclass_section_verify(tclass) < 0) + tclass_free(tclass); +} + +int config_parse_tclass_parent( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = tclass_new_static(ltype, network, filename, section_line, &tclass); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to create traffic control class, ignoring assignment: %m"); + return 0; + } + + if (streq(rvalue, "root")) + tclass->parent = TC_H_ROOT; + else { + r = parse_handle(rvalue, &tclass->parent); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse 'Parent=', ignoring assignment: %s", + rvalue); + return 0; + } + } + + TAKE_PTR(tclass); + + return 0; +} + +int config_parse_tclass_classid( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(tclass_free_or_set_invalidp) TClass *tclass = NULL; + Network *network = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = tclass_new_static(ltype, network, filename, section_line, &tclass); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to create traffic control class, ignoring assignment: %m"); + return 0; + } + + if (isempty(rvalue)) { + tclass->classid = TC_H_UNSPEC; + TAKE_PTR(tclass); + return 0; + } + + r = parse_handle(rvalue, &tclass->classid); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse 'ClassId=', ignoring assignment: %s", + rvalue); + return 0; + } + + TAKE_PTR(tclass); + + return 0; +} diff --git a/src/network/tc/tclass.h b/src/network/tc/tclass.h new file mode 100644 index 0000000..e73e23c --- /dev/null +++ b/src/network/tc/tclass.h @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright © 2019 VMware, Inc. */ +#pragma once + +#include "conf-parser.h" +#include "networkd-util.h" + +typedef struct Link Link; +typedef struct Manager Manager; +typedef struct Network Network; + +typedef enum TClassKind { + TCLASS_KIND_DRR, + TCLASS_KIND_HTB, + TCLASS_KIND_QFQ, + _TCLASS_KIND_MAX, + _TCLASS_KIND_INVALID = -EINVAL, +} TClassKind; + +typedef struct TClass { + Link *link; + Network *network; + ConfigSection *section; + NetworkConfigSource source; + NetworkConfigState state; + + uint32_t classid; + uint32_t parent; + + TClassKind kind; + char *tca_kind; +} TClass; + +typedef struct TClassVTable { + size_t object_size; + const char *tca_kind; + /* called in tclass_new() */ + int (*init)(TClass *tclass); + int (*fill_message)(Link *link, TClass *tclass, sd_netlink_message *m); + int (*verify)(TClass *tclass); +} TClassVTable; + +extern const TClassVTable * const tclass_vtable[_TCLASS_KIND_MAX]; + +#define TCLASS_VTABLE(t) ((t)->kind != _TCLASS_KIND_INVALID ? tclass_vtable[(t)->kind] : NULL) + +/* For casting a tclass into the various tclass kinds */ +#define DEFINE_TCLASS_CAST(UPPERCASE, MixedCase) \ + static inline MixedCase* TCLASS_TO_##UPPERCASE(TClass *t) { \ + if (_unlikely_(!t || t->kind != TCLASS_KIND_##UPPERCASE)) \ + return NULL; \ + \ + return (MixedCase*) t; \ + } + +DEFINE_NETWORK_CONFIG_STATE_FUNCTIONS(TClass, tclass); + +TClass* tclass_free(TClass *tclass); +int tclass_new_static(TClassKind kind, Network *network, const char *filename, unsigned section_line, TClass **ret); + +TClass* tclass_drop(TClass *tclass); + +int link_find_tclass(Link *link, uint32_t classid, TClass **ret); + +int link_request_tclass(Link *link, TClass *tclass); + +void network_drop_invalid_tclass(Network *network); + +int manager_rtnl_process_tclass(sd_netlink *rtnl, sd_netlink_message *message, Manager *m); +int link_enumerate_tclass(Link *link, uint32_t parent); + +DEFINE_SECTION_CLEANUP_FUNCTIONS(TClass, tclass_free); + +CONFIG_PARSER_PROTOTYPE(config_parse_tclass_parent); +CONFIG_PARSER_PROTOTYPE(config_parse_tclass_classid); + +#include "drr.h" +#include "htb.h" +#include "qfq.h" diff --git a/src/network/tc/teql.c b/src/network/tc/teql.c new file mode 100644 index 0000000..dcb149d --- /dev/null +++ b/src/network/tc/teql.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "macro.h" +#include "networkd-link.h" +#include "parse-util.h" +#include "string-util.h" +#include "teql.h" + +static int trivial_link_equalizer_verify(QDisc *qdisc) { + _cleanup_free_ char *tca_kind = NULL; + TrivialLinkEqualizer *teql; + + teql = TEQL(ASSERT_PTR(qdisc)); + + if (asprintf(&tca_kind, "teql%u", teql->id) < 0) + return log_oom(); + + return free_and_replace(qdisc->tca_kind, tca_kind); +} + +static int trivial_link_equalizer_is_ready(QDisc *qdisc, Link *link) { + Link *teql; + + assert(qdisc); + assert(qdisc->tca_kind); + assert(link); + assert(link->manager); + + if (link_get_by_name(link->manager, qdisc->tca_kind, &teql) < 0) + return false; + + return link_is_ready_to_configure(teql, /* allow_unmanaged = */ true); +} + +const QDiscVTable teql_vtable = { + .object_size = sizeof(TrivialLinkEqualizer), + .verify = trivial_link_equalizer_verify, + .is_ready = trivial_link_equalizer_is_ready, +}; + +int config_parse_trivial_link_equalizer_id( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL; + TrivialLinkEqualizer *teql; + Network *network = ASSERT_PTR(data); + unsigned id; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = qdisc_new_static(QDISC_KIND_TEQL, network, filename, section_line, &qdisc); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "More than one kind of queueing discipline, ignoring assignment: %m"); + return 0; + } + + teql = TEQL(qdisc); + + if (isempty(rvalue)) { + teql->id = 0; + + TAKE_PTR(qdisc); + return 0; + } + + r = safe_atou(rvalue, &id); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse '%s=', ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + if (id > INT_MAX) + log_syntax(unit, LOG_WARNING, filename, line, 0, + "'%s=' is too large, ignoring assignment: %s", + lvalue, rvalue); + + teql->id = id; + + TAKE_PTR(qdisc); + return 0; +} diff --git a/src/network/tc/teql.h b/src/network/tc/teql.h new file mode 100644 index 0000000..8d0085e --- /dev/null +++ b/src/network/tc/teql.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" +#include "qdisc.h" + +typedef struct TrivialLinkEqualizer { + QDisc meta; + + unsigned id; +} TrivialLinkEqualizer; + +DEFINE_QDISC_CAST(TEQL, TrivialLinkEqualizer); +extern const QDiscVTable teql_vtable; + +CONFIG_PARSER_PROTOTYPE(config_parse_trivial_link_equalizer_id); diff --git a/src/network/test-network-tables.c b/src/network/test-network-tables.c new file mode 100644 index 0000000..564ca09 --- /dev/null +++ b/src/network/test-network-tables.c @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bond.h" +#include "dhcp6-internal.h" +#include "dhcp6-protocol.h" +#include "ethtool-util.h" +#include "ipvlan.h" +#include "lldp-rx-internal.h" +#include "macvlan.h" +#include "ndisc-internal.h" +#include "networkd-link.h" +#include "networkd-network.h" +#include "networkd-util.h" +#include "test-tables.h" +#include "tests.h" +#include "tunnel.h" + +int main(int argc, char **argv) { + test_setup_logging(LOG_DEBUG); + + test_table(bond_ad_select, NETDEV_BOND_AD_SELECT); + test_table(bond_arp_all_targets, NETDEV_BOND_ARP_ALL_TARGETS); + test_table(bond_arp_validate, NETDEV_BOND_ARP_VALIDATE); + test_table(bond_fail_over_mac, NETDEV_BOND_FAIL_OVER_MAC); + test_table(bond_lacp_rate, NETDEV_BOND_LACP_RATE); + test_table(bond_mode, NETDEV_BOND_MODE); + test_table(bond_primary_reselect, NETDEV_BOND_PRIMARY_RESELECT); + test_table(bond_xmit_hash_policy, NETDEV_BOND_XMIT_HASH_POLICY); + test_table(dhcp6_message_status, DHCP6_STATUS); + test_table_sparse(dhcp6_message_type, DHCP6_MESSAGE_TYPE); /* enum starts from 1 */ + test_table(dhcp_use_domains, DHCP_USE_DOMAINS); + test_table(duplex, DUP); + test_table(ip6tnl_mode, NETDEV_IP6_TNL_MODE); + test_table(ipv6_privacy_extensions, IPV6_PRIVACY_EXTENSIONS); + test_table(ipvlan_flags, NETDEV_IPVLAN_FLAGS); + test_table(link_operstate, LINK_OPERSTATE); + /* test_table(link_state, LINK_STATE); — not a reversible mapping */ + test_table(lldp_mode, LLDP_MODE); + test_table(netdev_kind, NETDEV_KIND); + test_table(radv_prefix_delegation, RADV_PREFIX_DELEGATION); + test_table(lldp_rx_event, SD_LLDP_RX_EVENT); + test_table(ndisc_event, SD_NDISC_EVENT); + test_table(dhcp_lease_server_type, SD_DHCP_LEASE_SERVER_TYPE); + + test_table_sparse(ipvlan_mode, NETDEV_IPVLAN_MODE); + test_table_sparse(macvlan_mode, NETDEV_MACVLAN_MODE); + test_table_sparse(address_family, ADDRESS_FAMILY); + + assert_cc(sizeof(sd_lldp_rx_event_t) == sizeof(int64_t)); + assert_cc(sizeof(sd_ndisc_event_t) == sizeof(int64_t)); + assert_cc(sizeof(sd_dhcp_lease_server_type_t) == sizeof(int64_t)); + + return EXIT_SUCCESS; +} diff --git a/src/network/test-network.c b/src/network/test-network.c new file mode 100644 index 0000000..5f3b4c0 --- /dev/null +++ b/src/network/test-network.c @@ -0,0 +1,251 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <sys/param.h> + +#include "sd-device.h" + +#include "alloc-util.h" +#include "dhcp-lease-internal.h" +#include "ether-addr-util.h" +#include "hostname-setup.h" +#include "network-internal.h" +#include "networkd-address.h" +#include "networkd-manager.h" +#include "networkd-route-util.h" +#include "string-util.h" +#include "strv.h" +#include "tests.h" + +static void test_deserialize_in_addr(void) { + _cleanup_free_ struct in_addr *addresses = NULL; + _cleanup_free_ struct in6_addr *addresses6 = NULL; + union in_addr_union a, b, c, d, e, f; + int size; + const char *addresses_string = "192.168.0.1 0:0:0:0:0:FFFF:204.152.189.116 192.168.0.2 ::1 192.168.0.3 1:0:0:0:0:0:0:8"; + + assert_se(in_addr_from_string(AF_INET, "0:0:0:0:0:FFFF:204.152.189.116", &a) < 0); + assert_se(in_addr_from_string(AF_INET6, "192.168.0.1", &d) < 0); + + assert_se(in_addr_from_string(AF_INET, "192.168.0.1", &a) >= 0); + assert_se(in_addr_from_string(AF_INET, "192.168.0.2", &b) >= 0); + assert_se(in_addr_from_string(AF_INET, "192.168.0.3", &c) >= 0); + assert_se(in_addr_from_string(AF_INET6, "0:0:0:0:0:FFFF:204.152.189.116", &d) >= 0); + assert_se(in_addr_from_string(AF_INET6, "::1", &e) >= 0); + assert_se(in_addr_from_string(AF_INET6, "1:0:0:0:0:0:0:8", &f) >= 0); + + assert_se((size = deserialize_in_addrs(&addresses, addresses_string)) >= 0); + assert_se(size == 3); + assert_se(in4_addr_equal(&a.in, &addresses[0])); + assert_se(in4_addr_equal(&b.in, &addresses[1])); + assert_se(in4_addr_equal(&c.in, &addresses[2])); + + assert_se((size = deserialize_in6_addrs(&addresses6, addresses_string)) >= 0); + assert_se(size == 3); + assert_se(in6_addr_equal(&d.in6, &addresses6[0])); + assert_se(in6_addr_equal(&e.in6, &addresses6[1])); + assert_se(in6_addr_equal(&f.in6, &addresses6[2])); +} + +static void test_deserialize_dhcp_routes(void) { + size_t size; + + { + _cleanup_free_ struct sd_dhcp_route *routes = NULL; + assert_se(deserialize_dhcp_routes(&routes, &size, "") >= 0); + assert_se(size == 0); + } + + { + /* no errors */ + _cleanup_free_ struct sd_dhcp_route *routes = NULL; + const char *routes_string = "192.168.0.0/16,192.168.0.1 10.1.2.0/24,10.1.2.1 0.0.0.0/0,10.0.1.1"; + + assert_se(deserialize_dhcp_routes(&routes, &size, routes_string) >= 0); + + assert_se(size == 3); + assert_se(routes[0].dst_addr.s_addr == inet_addr("192.168.0.0")); + assert_se(routes[0].gw_addr.s_addr == inet_addr("192.168.0.1")); + assert_se(routes[0].dst_prefixlen == 16); + + assert_se(routes[1].dst_addr.s_addr == inet_addr("10.1.2.0")); + assert_se(routes[1].gw_addr.s_addr == inet_addr("10.1.2.1")); + assert_se(routes[1].dst_prefixlen == 24); + + assert_se(routes[2].dst_addr.s_addr == inet_addr("0.0.0.0")); + assert_se(routes[2].gw_addr.s_addr == inet_addr("10.0.1.1")); + assert_se(routes[2].dst_prefixlen == 0); + } + + { + /* error in second word */ + _cleanup_free_ struct sd_dhcp_route *routes = NULL; + const char *routes_string = "192.168.0.0/16,192.168.0.1 10.1.2.0#24,10.1.2.1 0.0.0.0/0,10.0.1.1"; + + assert_se(deserialize_dhcp_routes(&routes, &size, routes_string) >= 0); + + assert_se(size == 2); + assert_se(routes[0].dst_addr.s_addr == inet_addr("192.168.0.0")); + assert_se(routes[0].gw_addr.s_addr == inet_addr("192.168.0.1")); + assert_se(routes[0].dst_prefixlen == 16); + + assert_se(routes[1].dst_addr.s_addr == inet_addr("0.0.0.0")); + assert_se(routes[1].gw_addr.s_addr == inet_addr("10.0.1.1")); + assert_se(routes[1].dst_prefixlen == 0); + } + + { + /* error in every word */ + _cleanup_free_ struct sd_dhcp_route *routes = NULL; + const char *routes_string = "192.168.0.0/55,192.168.0.1 10.1.2.0#24,10.1.2.1 0.0.0.0/0,10.0.1.X"; + + assert_se(deserialize_dhcp_routes(&routes, &size, routes_string) >= 0); + assert_se(size == 0); + } +} + +static void test_route_tables_one(Manager *manager, const char *name, uint32_t number) { + _cleanup_free_ char *str = NULL, *expected = NULL, *num_str = NULL; + uint32_t t; + + if (!STR_IN_SET(name, "default", "main", "local")) { + assert_se(streq(hashmap_get(manager->route_table_names_by_number, UINT32_TO_PTR(number)), name)); + assert_se(PTR_TO_UINT32(hashmap_get(manager->route_table_numbers_by_name, name)) == number); + } + + assert_se(asprintf(&expected, "%s(%" PRIu32 ")", name, number) >= 0); + assert_se(manager_get_route_table_to_string(manager, number, /* append_num = */ true, &str) >= 0); + assert_se(streq(str, expected)); + + str = mfree(str); + + assert_se(manager_get_route_table_to_string(manager, number, /* append_num = */ false, &str) >= 0); + assert_se(streq(str, name)); + + assert_se(manager_get_route_table_from_string(manager, name, &t) >= 0); + assert_se(t == number); + + assert_se(asprintf(&num_str, "%" PRIu32, number) >= 0); + assert_se(manager_get_route_table_from_string(manager, num_str, &t) >= 0); + assert_se(t == number); +} + +static void test_route_tables(Manager *manager) { + assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "hoge:123 foo:456 aaa:111", manager, manager) >= 0); + assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "bbb:11111 ccc:22222", manager, manager) >= 0); + assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "ddd:22222", manager, manager) >= 0); + + test_route_tables_one(manager, "hoge", 123); + test_route_tables_one(manager, "foo", 456); + test_route_tables_one(manager, "aaa", 111); + test_route_tables_one(manager, "bbb", 11111); + test_route_tables_one(manager, "ccc", 22222); + + assert_se(!hashmap_get(manager->route_table_numbers_by_name, "ddd")); + + test_route_tables_one(manager, "default", 253); + test_route_tables_one(manager, "main", 254); + test_route_tables_one(manager, "local", 255); + + assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "", manager, manager) >= 0); + assert_se(!manager->route_table_names_by_number); + assert_se(!manager->route_table_numbers_by_name); + + /* Invalid pairs */ + assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "main:123 default:333 local:999", manager, manager) >= 0); + assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "xxx:253 yyy:254 local:255", manager, manager) >= 0); + assert_se(config_parse_route_table_names("manager", "filename", 1, "section", 1, "RouteTable", 0, "1234:321 :567 hoge:foo aaa:-888", manager, manager) >= 0); + assert_se(!manager->route_table_names_by_number); + assert_se(!manager->route_table_numbers_by_name); + + test_route_tables_one(manager, "default", 253); + test_route_tables_one(manager, "main", 254); + test_route_tables_one(manager, "local", 255); +} + +static int test_load_config(Manager *manager) { + int r; +/* TODO: should_reload, is false if the config dirs do not exist, so + * so we can't do this test here, move it to a test for paths_check_timestamps + * directly + * + * assert_se(network_should_reload(manager) == true); +*/ + + r = manager_load_config(manager); + if (r == -EPERM) + return r; + assert_se(r >= 0); + + return 0; +} + +static void test_dhcp_hostname_shorten_overlong(void) { + int r; + + { + /* simple hostname, no actions, no errors */ + _cleanup_free_ char *shortened = NULL; + r = shorten_overlong("name1", &shortened); + assert_se(r == 0); + assert_se(streq("name1", shortened)); + } + + { + /* simple fqdn, no actions, no errors */ + _cleanup_free_ char *shortened = NULL; + r = shorten_overlong("name1.example.com", &shortened); + assert_se(r == 0); + assert_se(streq("name1.example.com", shortened)); + } + + { + /* overlong fqdn, cut to first dot, no errors */ + _cleanup_free_ char *shortened = NULL; + r = shorten_overlong("name1.test-dhcp-this-one-here-is-a-very-very-long-domain.example.com", &shortened); + assert_se(r == 1); + assert_se(streq("name1", shortened)); + } + + { + /* overlong hostname, cut to HOST_MAX_LEN, no errors */ + _cleanup_free_ char *shortened = NULL; + r = shorten_overlong("test-dhcp-this-one-here-is-a-very-very-long-hostname-without-domainname", &shortened); + assert_se(r == 1); + assert_se(streq("test-dhcp-this-one-here-is-a-very-very-long-hostname-without-dom", shortened)); + } + + { + /* overlong fqdn, cut to first dot, empty result error */ + _cleanup_free_ char *shortened = NULL; + r = shorten_overlong(".test-dhcp-this-one-here-is-a-very-very-long-hostname.example.com", &shortened); + assert_se(r == -EDOM); + assert_se(shortened == NULL); + } + +} + +int main(void) { + _cleanup_(manager_freep) Manager *manager = NULL; + int r; + + test_setup_logging(LOG_INFO); + + test_deserialize_in_addr(); + test_deserialize_dhcp_routes(); + test_dhcp_hostname_shorten_overlong(); + + assert_se(manager_new(&manager, /* test_mode = */ true) >= 0); + assert_se(manager_setup(manager) >= 0); + + test_route_tables(manager); + + r = test_load_config(manager); + if (r == -EPERM) + log_debug("Cannot load configuration, ignoring."); + else + assert_se(r == 0); + + assert_se(manager_enumerate(manager) >= 0); + return 0; +} diff --git a/src/network/test-networkd-address.c b/src/network/test-networkd-address.c new file mode 100644 index 0000000..a40c571 --- /dev/null +++ b/src/network/test-networkd-address.c @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "networkd-address.h" +#include "tests.h" +#include "time-util.h" + +static void test_FORMAT_LIFETIME_one(usec_t lifetime, const char *expected) { + const char *t = FORMAT_LIFETIME(lifetime); + + log_debug(USEC_FMT " → \"%s\" (expected \"%s\")", lifetime, t, expected); + assert_se(streq(t, expected)); +} + +TEST(FORMAT_LIFETIME) { + usec_t now_usec; + + now_usec = now(CLOCK_BOOTTIME); + + test_FORMAT_LIFETIME_one(now_usec, "for 0"); + test_FORMAT_LIFETIME_one(usec_add(now_usec, 2 * USEC_PER_SEC - 1), "for 1s"); + test_FORMAT_LIFETIME_one(usec_add(now_usec, 3 * USEC_PER_WEEK + USEC_PER_SEC - 1), "for 3w"); + test_FORMAT_LIFETIME_one(USEC_INFINITY, "forever"); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/network/test-networkd-conf.c b/src/network/test-networkd-conf.c new file mode 100644 index 0000000..808db99 --- /dev/null +++ b/src/network/test-networkd-conf.c @@ -0,0 +1,278 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "hexdecoct.h" +#include "log.h" +#include "macro.h" +#include "net-condition.h" +#include "networkd-address.h" +#include "networkd-conf.h" +#include "networkd-manager.h" +#include "networkd-network.h" +#include "strv.h" +#include "tests.h" + +static void test_config_parse_duid_type_one(const char *rvalue, int ret, DUIDType expected, usec_t expected_time) { + DUID actual = {}; + int r; + + r = config_parse_duid_type("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &actual, NULL); + log_info_errno(r, "\"%s\" → %d (%m)", rvalue, actual.type); + assert_se(r == ret); + assert_se(expected == actual.type); + if (expected == DUID_TYPE_LLT) + assert_se(expected_time == actual.llt_time); +} + +TEST(config_parse_duid_type) { + test_config_parse_duid_type_one("", 0, 0, 0); + test_config_parse_duid_type_one("link-layer-time", 0, DUID_TYPE_LLT, 0); + test_config_parse_duid_type_one("link-layer-time:2000-01-01 00:00:00 UTC", 0, DUID_TYPE_LLT, (usec_t) 946684800000000); + test_config_parse_duid_type_one("vendor", 0, DUID_TYPE_EN, 0); + test_config_parse_duid_type_one("vendor:2000-01-01 00:00:00 UTC", 0, 0, 0); + test_config_parse_duid_type_one("link-layer", 0, DUID_TYPE_LL, 0); + test_config_parse_duid_type_one("link-layer:2000-01-01 00:00:00 UTC", 0, 0, 0); + test_config_parse_duid_type_one("uuid", 0, DUID_TYPE_UUID, 0); + test_config_parse_duid_type_one("uuid:2000-01-01 00:00:00 UTC", 0, 0, 0); + test_config_parse_duid_type_one("foo", 0, 0, 0); + test_config_parse_duid_type_one("foo:2000-01-01 00:00:00 UTC", 0, 0, 0); +} + +static void test_config_parse_duid_rawdata_one(const char *rvalue, int ret, const DUID* expected) { + DUID actual = {}; + int r; + _cleanup_free_ char *d = NULL; + + r = config_parse_duid_rawdata("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &actual, NULL); + d = hexmem(actual.raw_data, actual.raw_data_len); + log_info_errno(r, "\"%s\" → \"%s\" (%m)", + rvalue, strnull(d)); + assert_se(r == ret); + if (expected) { + assert_se(actual.raw_data_len == expected->raw_data_len); + assert_se(memcmp(actual.raw_data, expected->raw_data, expected->raw_data_len) == 0); + } +} + +static void test_config_parse_ether_addr_one(const char *rvalue, int ret, const struct ether_addr* expected) { + struct ether_addr *actual = NULL; + int r; + + r = config_parse_ether_addr("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &actual, NULL); + assert_se(ret == r); + if (expected) { + assert_se(actual); + assert_se(ether_addr_equal(expected, actual)); + } else + assert_se(actual == NULL); + + free(actual); +} + +static void test_config_parse_ether_addrs_one(const char *rvalue, const struct ether_addr* list, size_t n) { + _cleanup_set_free_free_ Set *s = NULL; + + assert_se(config_parse_ether_addrs("network", "filename", 1, "section", 1, "lvalue", 0, rvalue, &s, NULL) == 0); + assert_se(set_size(s) == n); + + for (size_t m = 0; m < n; m++) { + _cleanup_free_ struct ether_addr *q = NULL; + + assert_se(q = set_remove(s, &list[m])); + } + + assert_se(set_size(s) == 0); +} + +#define STR_OK \ + "00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:" \ + "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" +#define STR_TOO_LONG \ + "00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:" \ + "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_OK { \ + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f, \ + 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, \ +} + +TEST(config_parse_duid_rawdata) { + test_config_parse_duid_rawdata_one("", 0, &(DUID){}); + test_config_parse_duid_rawdata_one("00:11:22:33:44:55:66:77", 0, + &(DUID){0, 8, {0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77}}); + test_config_parse_duid_rawdata_one("00:11:22:", 0, + &(DUID){0, 3, {0x00,0x11,0x22}}); + test_config_parse_duid_rawdata_one("000:11:22", 0, &(DUID){}); /* error, output is all zeros */ + test_config_parse_duid_rawdata_one("00:111:22", 0, &(DUID){}); + test_config_parse_duid_rawdata_one("0:1:2:3:4:5:6:7", 0, + &(DUID){0, 8, {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7}}); + test_config_parse_duid_rawdata_one("11::", 0, &(DUID){0, 1, {0x11}}); /* FIXME: should this be an error? */ + test_config_parse_duid_rawdata_one("abcdef", 0, &(DUID){}); + test_config_parse_duid_rawdata_one(STR_TOO_LONG, 0, &(DUID){}); + test_config_parse_duid_rawdata_one(STR_OK, 0, &(DUID){0, 128, BYTES_OK}); +} + +TEST(config_parse_ether_addr) { + const struct ether_addr t[] = { + { .ether_addr_octet = { 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff } }, + { .ether_addr_octet = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab } }, + }; + + test_config_parse_ether_addr_one("", 0, NULL); + test_config_parse_ether_addr_one("no:ta:ma:ca:dd:re", 0, NULL); + test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:fx", 0, NULL); + test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:ff", 0, &t[0]); + test_config_parse_ether_addr_one(" aa:bb:cc:dd:ee:ff", 0, NULL); + test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:ff \t\n", 0, NULL); + test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:ff \t\nxxx", 0, NULL); + test_config_parse_ether_addr_one("aa:bb:cc: dd:ee:ff", 0, NULL); + test_config_parse_ether_addr_one("aa:bb:cc:d d:ee:ff", 0, NULL); + test_config_parse_ether_addr_one("aa:bb:cc:dd:ee", 0, NULL); + test_config_parse_ether_addr_one("9:aa:bb:cc:dd:ee:ff", 0, NULL); + test_config_parse_ether_addr_one("aa:bb:cc:dd:ee:ff:gg", 0, NULL); + test_config_parse_ether_addr_one("aa:Bb:CC:dd:ee:ff", 0, &t[0]); + test_config_parse_ether_addr_one("01:23:45:67:89:aB", 0, &t[1]); + test_config_parse_ether_addr_one("1:23:45:67:89:aB", 0, &t[1]); + test_config_parse_ether_addr_one("aa-bb-cc-dd-ee-ff", 0, &t[0]); + test_config_parse_ether_addr_one("AA-BB-CC-DD-EE-FF", 0, &t[0]); + test_config_parse_ether_addr_one("01-23-45-67-89-ab", 0, &t[1]); + test_config_parse_ether_addr_one("aabb.ccdd.eeff", 0, &t[0]); + test_config_parse_ether_addr_one("0123.4567.89ab", 0, &t[1]); + test_config_parse_ether_addr_one("123.4567.89ab.", 0, NULL); + test_config_parse_ether_addr_one("aabbcc.ddeeff", 0, NULL); + test_config_parse_ether_addr_one("aabbccddeeff", 0, NULL); + test_config_parse_ether_addr_one("aabbccddee:ff", 0, NULL); + test_config_parse_ether_addr_one("012345.6789ab", 0, NULL); + test_config_parse_ether_addr_one("123.4567.89ab", 0, &t[1]); + + test_config_parse_ether_addrs_one("", t, 0); + test_config_parse_ether_addrs_one("no:ta:ma:ca:dd:re", t, 0); + test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:fx", t, 0); + test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:ff", t, 1); + test_config_parse_ether_addrs_one(" aa:bb:cc:dd:ee:ff", t, 1); + test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:ff \t\n", t, 1); + test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:ff \t\nxxx", t, 1); + test_config_parse_ether_addrs_one("aa:bb:cc: dd:ee:ff", t, 0); + test_config_parse_ether_addrs_one("aa:bb:cc:d d:ee:ff", t, 0); + test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee", t, 0); + test_config_parse_ether_addrs_one("9:aa:bb:cc:dd:ee:ff", t, 0); + test_config_parse_ether_addrs_one("aa:bb:cc:dd:ee:ff:gg", t, 0); + test_config_parse_ether_addrs_one("aa:Bb:CC:dd:ee:ff", t, 1); + test_config_parse_ether_addrs_one("01:23:45:67:89:aB", &t[1], 1); + test_config_parse_ether_addrs_one("1:23:45:67:89:aB", &t[1], 1); + test_config_parse_ether_addrs_one("aa-bb-cc-dd-ee-ff", t, 1); + test_config_parse_ether_addrs_one("AA-BB-CC-DD-EE-FF", t, 1); + test_config_parse_ether_addrs_one("01-23-45-67-89-ab", &t[1], 1); + test_config_parse_ether_addrs_one("aabb.ccdd.eeff", t, 1); + test_config_parse_ether_addrs_one("0123.4567.89ab", &t[1], 1); + test_config_parse_ether_addrs_one("123.4567.89ab.", t, 0); + test_config_parse_ether_addrs_one("aabbcc.ddeeff", t, 0); + test_config_parse_ether_addrs_one("aabbccddeeff", t, 0); + test_config_parse_ether_addrs_one("aabbccddee:ff", t, 0); + test_config_parse_ether_addrs_one("012345.6789ab", t, 0); + test_config_parse_ether_addrs_one("123.4567.89ab", &t[1], 1); + + test_config_parse_ether_addrs_one("123.4567.89ab aa:bb:cc:dd:ee:ff 01-23-45-67-89-ab aa:Bb:CC:dd:ee:ff", t, 2); + test_config_parse_ether_addrs_one("123.4567.89ab aa:bb:cc:dd:ee:fx hogehoge 01-23-45-67-89-ab aaaa aa:Bb:CC:dd:ee:ff", t, 2); +} + +static void test_config_parse_address_one(const char *rvalue, int family, unsigned n_addresses, const union in_addr_union *u, unsigned char prefixlen) { + _cleanup_(manager_freep) Manager *manager = NULL; + _cleanup_(network_unrefp) Network *network = NULL; + + assert_se(manager_new(&manager, /* test_mode = */ true) >= 0); + assert_se(network = new0(Network, 1)); + network->n_ref = 1; + network->manager = manager; + assert_se(network->filename = strdup("hogehoge.network")); + + assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "*", &network->match.ifname, network) == 0); + assert_se(config_parse_address("network", "filename", 1, "section", 1, "Address", 0, rvalue, network, network) == 0); + assert_se(ordered_hashmap_size(network->addresses_by_section) == 1); + assert_se(network_verify(network) >= 0); + assert_se(ordered_hashmap_size(network->addresses_by_section) == n_addresses); + if (n_addresses > 0) { + Address *a; + + assert_se(a = ordered_hashmap_first(network->addresses_by_section)); + assert_se(a->prefixlen == prefixlen); + assert_se(a->family == family); + assert_se(in_addr_equal(family, &a->in_addr, u)); + /* TODO: check Address.in_addr and Address.broadcast */ + } +} + +TEST(config_parse_address) { + test_config_parse_address_one("", AF_INET, 0, NULL, 0); + test_config_parse_address_one("/", AF_INET, 0, NULL, 0); + test_config_parse_address_one("/8", AF_INET, 0, NULL, 0); + test_config_parse_address_one("1.2.3.4", AF_INET, 1, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 32); + 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 }, 128); + test_config_parse_address_one("::1/0", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 0); + test_config_parse_address_one("::1/1", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 1); + test_config_parse_address_one("::1/2", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 2); + test_config_parse_address_one("::1/32", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 32); + test_config_parse_address_one("::1/33", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 33); + test_config_parse_address_one("::1/64", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 64); + test_config_parse_address_one("::1/128", AF_INET6, 1, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 128); + test_config_parse_address_one("::1/129", AF_INET6, 0, NULL, 0); + test_config_parse_address_one("::1/-1", AF_INET6, 0, NULL, 0); +} + +TEST(config_parse_match_ifnames) { + _cleanup_strv_free_ char **names = NULL; + + assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "!hoge hogehoge foo", &names, NULL) == 0); + assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "!baz", &names, NULL) == 0); + assert_se(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "aaa bbb ccc", &names, NULL) == 0); + + assert_se(strv_equal(names, STRV_MAKE("!hoge", "!hogehoge", "!foo", "!baz", "aaa", "bbb", "ccc"))); +} + +TEST(config_parse_match_strv) { + _cleanup_strv_free_ char **names = NULL; + + assert_se(config_parse_match_strv("network", "filename", 1, "section", 1, "Name", 0, "!hoge hogehoge foo", &names, NULL) == 0); + assert_se(config_parse_match_strv("network", "filename", 1, "section", 1, "Name", 0, "!baz", &names, NULL) == 0); + assert_se(config_parse_match_strv("network", "filename", 1, "section", 1, "Name", 0, + "KEY=val \"KEY2=val with space\" \"KEY3=val with \\\"quotation\\\"\"", &names, NULL) == 0); + + assert_se(strv_equal(names, + STRV_MAKE("!hoge", + "!hogehoge", + "!foo", + "!baz", + "KEY=val", + "KEY2=val with space", + "KEY3=val with \\quotation\\"))); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/network/test-networkd-util.c b/src/network/test-networkd-util.c new file mode 100644 index 0000000..f29ca2c --- /dev/null +++ b/src/network/test-networkd-util.c @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "networkd-util.h" +#include "tests.h" + +TEST(network_config_state_to_string_alloc) { + for (unsigned i = 1; i <= NETWORK_CONFIG_STATE_REMOVING; i <<= 1) { + _cleanup_free_ char *x; + + assert_se(network_config_state_to_string_alloc(i, &x) == 0); + log_debug("%u → %s", i, x); + } + + _cleanup_free_ char *x; + assert_se(network_config_state_to_string_alloc(~0u, &x) == 0); + log_debug("%u → %s", ~0u, x); +}; + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/network/wait-online/link.c b/src/network/wait-online/link.c new file mode 100644 index 0000000..a8ab7f5 --- /dev/null +++ b/src/network/wait-online/link.c @@ -0,0 +1,250 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-network.h" + +#include "alloc-util.h" +#include "format-util.h" +#include "hashmap.h" +#include "link.h" +#include "manager.h" +#include "string-util.h" +#include "strv.h" + +int link_new(Manager *m, Link **ret, int ifindex, const char *ifname) { + _cleanup_(link_freep) Link *l = NULL; + _cleanup_free_ char *n = NULL; + int r; + + assert(m); + assert(ifindex > 0); + assert(ifname); + + n = strdup(ifname); + if (!n) + return -ENOMEM; + + l = new(Link, 1); + if (!l) + return -ENOMEM; + + *l = (Link) { + .manager = m, + .ifname = TAKE_PTR(n), + .ifindex = ifindex, + .required_operstate = LINK_OPERSTATE_RANGE_DEFAULT, + }; + + r = hashmap_ensure_put(&m->links_by_index, NULL, INT_TO_PTR(ifindex), l); + if (r < 0) + return r; + + r = hashmap_ensure_put(&m->links_by_name, &string_hash_ops, l->ifname, l); + if (r < 0) + return r; + + if (ret) + *ret = l; + + TAKE_PTR(l); + return 0; +} + +Link *link_free(Link *l) { + + if (!l) + return NULL; + + if (l->manager) { + hashmap_remove(l->manager->links_by_index, INT_TO_PTR(l->ifindex)); + hashmap_remove(l->manager->links_by_name, l->ifname); + + STRV_FOREACH(n, l->altnames) + hashmap_remove(l->manager->links_by_name, *n); + } + + free(l->state); + free(l->ifname); + strv_free(l->altnames); + return mfree(l); +} + +static int link_update_name(Link *l, sd_netlink_message *m) { + char ifname_from_index[IF_NAMESIZE]; + const char *ifname; + int r; + + assert(l); + assert(l->manager); + assert(m); + + r = sd_netlink_message_read_string(m, IFLA_IFNAME, &ifname); + if (r == -ENODATA) + /* Hmm? But ok. */ + return 0; + if (r < 0) + return r; + + if (streq(ifname, l->ifname)) + return 0; + + /* The kernel sometimes sends wrong ifname change. Let's confirm the received name. */ + r = format_ifname(l->ifindex, ifname_from_index); + if (r < 0) + return r; + + if (!streq(ifname, ifname_from_index)) { + log_link_debug(l, "New interface name '%s' received from the kernel does not correspond " + "with the name currently configured on the actual interface '%s'. Ignoring.", + ifname, ifname_from_index); + return 0; + } + + hashmap_remove(l->manager->links_by_name, l->ifname); + + r = free_and_strdup(&l->ifname, ifname); + if (r < 0) + return r; + + r = hashmap_ensure_put(&l->manager->links_by_name, &string_hash_ops, l->ifname, l); + if (r < 0) + return r; + + return 0; +} + +static int link_update_altnames(Link *l, sd_netlink_message *m) { + _cleanup_strv_free_ char **altnames = NULL; + int r; + + assert(l); + assert(l->manager); + assert(m); + + r = sd_netlink_message_read_strv(m, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &altnames); + if (r == -ENODATA) + /* The message does not have IFLA_PROP_LIST container attribute. It does not mean the + * interface has no alternative name. */ + return 0; + if (r < 0) + return r; + + if (strv_equal(altnames, l->altnames)) + return 0; + + STRV_FOREACH(n, l->altnames) + hashmap_remove(l->manager->links_by_name, *n); + + strv_free_and_replace(l->altnames, altnames); + + STRV_FOREACH(n, l->altnames) { + r = hashmap_ensure_put(&l->manager->links_by_name, &string_hash_ops, *n, l); + if (r < 0) + return r; + } + + return 0; +} + +int link_update_rtnl(Link *l, sd_netlink_message *m) { + 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 = link_update_name(l, m); + if (r < 0) + return r; + + r = link_update_altnames(l, m); + if (r < 0) + return r; + + return 0; +} + +int link_update_monitor(Link *l) { + _cleanup_free_ char *required_operstate = NULL, *required_family = NULL, + *ipv4_address_state = NULL, *ipv6_address_state = NULL, *state = NULL; + int r, ret = 0; + + assert(l); + assert(l->ifname); + + r = sd_network_link_get_required_for_online(l->ifindex); + if (r < 0 && r != -ENODATA) + ret = log_link_debug_errno(l, r, "Failed to determine whether the link is required for online or not, " + "assuming required: %m"); + l->required_for_online = r != 0; + + r = sd_network_link_get_required_operstate_for_online(l->ifindex, &required_operstate); + if (r < 0 && r != -ENODATA) + ret = log_link_debug_errno(l, r, "Failed to get required operational state, ignoring: %m"); + + if (isempty(required_operstate)) + l->required_operstate = LINK_OPERSTATE_RANGE_DEFAULT; + else { + r = parse_operational_state_range(required_operstate, &l->required_operstate); + if (r < 0) + ret = log_link_debug_errno(l, SYNTHETIC_ERRNO(EINVAL), + "Failed to parse required operational state, ignoring: %m"); + } + + r = network_link_get_operational_state(l->ifindex, &l->operational_state); + if (r < 0) + ret = log_link_debug_errno(l, r, "Failed to get operational state, ignoring: %m"); + + r = sd_network_link_get_required_family_for_online(l->ifindex, &required_family); + if (r < 0 && r != -ENODATA) + ret = log_link_debug_errno(l, r, "Failed to get required address family, ignoring: %m"); + + if (isempty(required_family)) + l->required_family = ADDRESS_FAMILY_NO; + else { + AddressFamily f; + + f = link_required_address_family_from_string(required_family); + if (f < 0) + ret = log_link_debug_errno(l, f, "Failed to parse required address family, ignoring: %m"); + else + l->required_family = f; + } + + r = sd_network_link_get_ipv4_address_state(l->ifindex, &ipv4_address_state); + if (r < 0) + ret = log_link_debug_errno(l, r, "Failed to get IPv4 address state, ignoring: %m"); + else { + LinkAddressState s; + + s = link_address_state_from_string(ipv4_address_state); + if (s < 0) + ret = log_link_debug_errno(l, s, "Failed to parse IPv4 address state, ignoring: %m"); + else + l->ipv4_address_state = s; + } + + r = sd_network_link_get_ipv6_address_state(l->ifindex, &ipv6_address_state); + if (r < 0) + ret = log_link_debug_errno(l, r, "Failed to get IPv6 address state, ignoring: %m"); + else { + LinkAddressState s; + + s = link_address_state_from_string(ipv6_address_state); + if (s < 0) + ret = log_link_debug_errno(l, s, "Failed to parse IPv6 address state, ignoring: %m"); + else + l->ipv6_address_state = s; + } + + r = sd_network_link_get_setup_state(l->ifindex, &state); + if (r < 0) + ret = log_link_debug_errno(l, r, "Failed to get setup state, ignoring: %m"); + else + free_and_replace(l->state, state); + + return ret; +} diff --git a/src/network/wait-online/link.h b/src/network/wait-online/link.h new file mode 100644 index 0000000..5dc26d9 --- /dev/null +++ b/src/network/wait-online/link.h @@ -0,0 +1,34 @@ +/* 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; + char **altnames; + unsigned flags; + + bool required_for_online; + LinkOperationalStateRange required_operstate; + LinkOperationalState operational_state; + AddressFamily required_family; + LinkAddressState ipv4_address_state; + LinkAddressState ipv6_address_state; + char *state; +}; + +int link_new(Manager *m, Link **ret, int ifindex, const char *ifname); +Link *link_free(Link *l); +int link_update_rtnl(Link *l, sd_netlink_message *m); +int link_update_monitor(Link *l); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free); diff --git a/src/network/wait-online/manager.c b/src/network/wait-online/manager.c new file mode 100644 index 0000000..40a9fba --- /dev/null +++ b/src/network/wait-online/manager.c @@ -0,0 +1,441 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/ether.h> +#include <linux/if.h> +#include <fnmatch.h> + +#include "alloc-util.h" +#include "link.h" +#include "manager.h" +#include "netlink-util.h" +#include "strv.h" +#include "time-util.h" + +static bool link_in_command_line_interfaces(Link *link, Manager *m) { + assert(link); + assert(m); + + if (hashmap_contains(m->command_line_interfaces_by_name, link->ifname)) + return true; + + STRV_FOREACH(n, link->altnames) + if (hashmap_contains(m->command_line_interfaces_by_name, *n)) + return true; + + return false; +} + +static bool manager_ignore_link(Manager *m, Link *link) { + assert(m); + assert(link); + + /* always ignore the loopback interface */ + if (link->flags & IFF_LOOPBACK) + return true; + + /* if interfaces are given on the command line, ignore all others */ + if (m->command_line_interfaces_by_name && + !link_in_command_line_interfaces(link, m)) + return true; + + if (!link->required_for_online) + return true; + + /* ignore interfaces we explicitly are asked to ignore */ + if (strv_fnmatch(m->ignored_interfaces, link->ifname)) + return true; + + STRV_FOREACH(n, link->altnames) + if (strv_fnmatch(m->ignored_interfaces, *n)) + return true; + + return false; +} + +static int manager_link_is_online(Manager *m, Link *l, LinkOperationalStateRange s) { + AddressFamily required_family; + bool needs_ipv4; + bool needs_ipv6; + + assert(m); + assert(l); + + /* This returns the following: + * -EAGAIN : not processed by udev + * -EBUSY : being processed by networkd + * -EADDRNOTAVAIL: requested conditions (operstate and/or addresses) are not satisfied + * false : unmanaged + * true : online */ + + if (!l->state || streq(l->state, "pending")) + /* If no state string exists, networkd (and possibly also udevd) has not detected the + * interface yet, that mean we cannot determine whether the interface is managed or + * not. Hence, return negative value. + * If the link is in pending state, then udevd has not processed the link, and networkd + * has not tried to find .network file for the link. Hence, return negative value. */ + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EAGAIN), + "link has not yet been processed by udev: setup state is %s.", + strna(l->state)); + + if (streq(l->state, "unmanaged")) { + /* If the link is in unmanaged state, then ignore the interface unless the interface is + * specified in '--interface/-i' option. */ + if (!link_in_command_line_interfaces(l, m)) { + log_link_debug(l, "link is not managed by networkd."); + return false; + } + + } else if (!streq(l->state, "configured")) + /* If the link is in non-configured state, return negative value here. */ + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EBUSY), + "link is being processed by networkd: setup state is %s.", + l->state); + + if (s.min < 0) + s.min = m->required_operstate.min >= 0 ? m->required_operstate.min + : l->required_operstate.min; + + if (s.max < 0) + s.max = m->required_operstate.max >= 0 ? m->required_operstate.max + : l->required_operstate.max; + + if (l->operational_state < s.min || l->operational_state > s.max) + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "Operational state '%s' is not in range ['%s':'%s']", + link_operstate_to_string(l->operational_state), + link_operstate_to_string(s.min), link_operstate_to_string(s.max)); + + required_family = m->required_family > 0 ? m->required_family : l->required_family; + needs_ipv4 = required_family & ADDRESS_FAMILY_IPV4; + needs_ipv6 = required_family & ADDRESS_FAMILY_IPV6; + + if (s.min < LINK_OPERSTATE_ROUTABLE) { + if (needs_ipv4 && l->ipv4_address_state < LINK_ADDRESS_STATE_DEGRADED) + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "No routable or link-local IPv4 address is configured."); + + if (needs_ipv6 && l->ipv6_address_state < LINK_ADDRESS_STATE_DEGRADED) + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "No routable or link-local IPv6 address is configured."); + } else { + if (needs_ipv4 && l->ipv4_address_state < LINK_ADDRESS_STATE_ROUTABLE) + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "No routable IPv4 address is configured."); + + if (needs_ipv6 && l->ipv6_address_state < LINK_ADDRESS_STATE_ROUTABLE) + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "No routable IPv6 address is configured."); + } + + log_link_debug(l, "link is configured by networkd and online."); + return true; +} + +bool manager_configured(Manager *m) { + Link *l; + int r; + + if (!hashmap_isempty(m->command_line_interfaces_by_name)) { + LinkOperationalStateRange *range; + const char *ifname; + + /* wait for all the links given on the command line to appear */ + HASHMAP_FOREACH_KEY(range, ifname, m->command_line_interfaces_by_name) { + + l = hashmap_get(m->links_by_name, ifname); + if (!l) { + if (range->min == LINK_OPERSTATE_MISSING) { + if (m->any) + return true; + } else { + log_debug("still waiting for %s", ifname); + if (!m->any) + return false; + } + continue; + } + + r = manager_link_is_online(m, l, *range); + if (r <= 0 && !m->any) + return false; + if (r > 0 && m->any) + return true; + } + + /* With '--any' : no interface is ready → return false + * Without '--any': all interfaces are ready → return true */ + return !m->any; + } + + /* wait for all links networkd manages */ + bool has_online = false; + HASHMAP_FOREACH(l, m->links_by_index) { + if (manager_ignore_link(m, l)) { + log_link_debug(l, "link is ignored"); + continue; + } + + r = manager_link_is_online(m, l, + (LinkOperationalStateRange) { _LINK_OPERSTATE_INVALID, + _LINK_OPERSTATE_INVALID }); + /* Unlike the above loop, unmanaged interfaces are ignored here. Also, Configured but offline + * interfaces are ignored. See issue #29506. */ + if (r < 0 && r != -EADDRNOTAVAIL && !m->any) + return false; + if (r > 0) { + if (m->any) + return true; + has_online = true; + } + } + + /* With '--any' : no interface is ready → return false + * Without '--any': all interfaces are ready or unmanaged + * + * In this stage, drivers for interfaces may not be loaded yet, and there may be only lo. + * To avoid that wait-online exits earlier than that drivers are loaded, let's request at least one + * managed online interface exists. See issue #27822. */ + return !m->any && has_online; +} + +static int manager_process_link(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + uint16_t type; + Link *l; + const char *ifname; + int ifindex, r; + + assert(rtnl); + assert(mm); + + r = sd_netlink_message_get_type(mm, &type); + if (r < 0) { + log_warning_errno(r, "rtnl: Could not get message type, ignoring: %m"); + return 0; + } + + r = sd_rtnl_message_link_get_ifindex(mm, &ifindex); + if (r < 0) { + log_warning_errno(r, "rtnl: Could not get ifindex from link, ignoring: %m"); + return 0; + } else if (ifindex <= 0) { + log_warning("rtnl: received link message with invalid ifindex %d, ignoring", ifindex); + return 0; + } + + r = sd_netlink_message_read_string(mm, IFLA_IFNAME, &ifname); + if (r < 0) { + log_warning_errno(r, "rtnl: Received link message without ifname, ignoring: %m"); + return 0; + } + + l = hashmap_get(m->links_by_index, INT_TO_PTR(ifindex)); + + switch (type) { + + case RTM_NEWLINK: + if (!l) { + log_debug("Found link %s(%i)", ifname, ifindex); + + r = link_new(m, &l, ifindex, ifname); + if (r < 0) { + log_warning_errno(r, "Failed to create link object for %s(%i), ignoring: %m", ifname, ifindex); + return 0; + } + } + + r = link_update_rtnl(l, mm); + if (r < 0) + log_link_warning_errno(l, r, "Failed to process RTNL link message, ignoring: %m"); + + r = link_update_monitor(l); + if (r < 0) + log_link_full_errno(l, IN_SET(r, -ENODATA, -ENOENT) ? LOG_DEBUG : LOG_WARNING, r, + "Failed to update link state, ignoring: %m"); + + break; + + case RTM_DELLINK: + if (l) { + log_link_debug(l, "Removing link"); + link_free(l); + } + + break; + } + + return 0; +} + +static int on_rtnl_event(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) { + Manager *m = userdata; + int r; + + r = manager_process_link(rtnl, mm, m); + if (r < 0) + return r; + + if (manager_configured(m)) + sd_event_exit(m->event, 0); + + return 1; +} + +static int manager_rtnl_listen(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + int r; + + assert(m); + + /* First, subscribe to interfaces coming and going */ + r = sd_netlink_open(&m->rtnl); + if (r < 0) + return r; + + r = sd_netlink_attach_event(m->rtnl, m->event, 0); + if (r < 0) + return r; + + r = sd_netlink_add_match(m->rtnl, NULL, RTM_NEWLINK, on_rtnl_event, NULL, m, "wait-online-on-NEWLINK"); + if (r < 0) + return r; + + r = sd_netlink_add_match(m->rtnl, NULL, RTM_DELLINK, on_rtnl_event, NULL, m, "wait-online-on-DELLINK"); + if (r < 0) + return r; + + /* Then, enumerate all links */ + r = sd_rtnl_message_new_link(m->rtnl, &req, RTM_GETLINK, 0); + if (r < 0) + return r; + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return r; + + r = sd_netlink_call(m->rtnl, req, 0, &reply); + if (r < 0) + return r; + + for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) { + r = manager_process_link(m->rtnl, i, m); + if (r < 0) + return r; + } + + return r; +} + +static int on_network_event(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + Link *l; + int r; + + sd_network_monitor_flush(m->network_monitor); + + HASHMAP_FOREACH(l, m->links_by_index) { + r = link_update_monitor(l); + if (r < 0) + log_link_full_errno(l, IN_SET(r, -ENODATA, -ENOENT) ? LOG_DEBUG : LOG_WARNING, r, + "Failed to update link state, ignoring: %m"); + } + + if (manager_configured(m)) + sd_event_exit(m->event, 0); + + return 0; +} + +static int manager_network_monitor_listen(Manager *m) { + int r, fd, events; + + assert(m); + + r = sd_network_monitor_new(&m->network_monitor, NULL); + if (r < 0) + return r; + + fd = sd_network_monitor_get_fd(m->network_monitor); + if (fd < 0) + return fd; + + events = sd_network_monitor_get_events(m->network_monitor); + if (events < 0) + return events; + + r = sd_event_add_io(m->event, &m->network_monitor_event_source, + fd, events, &on_network_event, m); + if (r < 0) + return r; + + return 0; +} + +int manager_new(Manager **ret, + Hashmap *command_line_interfaces_by_name, + char **ignored_interfaces, + LinkOperationalStateRange required_operstate, + AddressFamily required_family, + bool any, + usec_t timeout) { + + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + assert(ret); + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .command_line_interfaces_by_name = command_line_interfaces_by_name, + .ignored_interfaces = ignored_interfaces, + .required_operstate = required_operstate, + .required_family = required_family, + .any = any, + }; + + r = sd_event_default(&m->event); + if (r < 0) + return r; + + (void) sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL); + (void) sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL); + + if (timeout > 0) { + r = sd_event_add_time_relative(m->event, NULL, CLOCK_BOOTTIME, timeout, 0, NULL, INT_TO_PTR(-ETIMEDOUT)); + if (r < 0 && r != -EOVERFLOW) + return r; + } + + sd_event_set_watchdog(m->event, true); + + r = manager_network_monitor_listen(m); + if (r < 0) + return r; + + r = manager_rtnl_listen(m); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + + return 0; +} + +Manager* manager_free(Manager *m) { + if (!m) + return NULL; + + hashmap_free_with_destructor(m->links_by_index, link_free); + hashmap_free(m->links_by_name); + + sd_event_source_unref(m->network_monitor_event_source); + sd_network_monitor_unref(m->network_monitor); + sd_event_source_unref(m->rtnl_event_source); + sd_netlink_unref(m->rtnl); + sd_event_unref(m->event); + + return mfree(m); +} diff --git a/src/network/wait-online/manager.h b/src/network/wait-online/manager.h new file mode 100644 index 0000000..01ad18f --- /dev/null +++ b/src/network/wait-online/manager.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-event.h" +#include "sd-netlink.h" +#include "sd-network.h" + +#include "hashmap.h" +#include "network-util.h" +#include "time-util.h" + +typedef struct Manager Manager; +typedef struct Link Link; + +struct Manager { + Hashmap *links_by_index; + Hashmap *links_by_name; + + /* Do not free the two members below. */ + Hashmap *command_line_interfaces_by_name; + char **ignored_interfaces; + + LinkOperationalStateRange required_operstate; + AddressFamily required_family; + bool any; + + sd_netlink *rtnl; + sd_event_source *rtnl_event_source; + + sd_network_monitor *network_monitor; + sd_event_source *network_monitor_event_source; + + sd_event *event; +}; + +Manager* manager_free(Manager *m); +int manager_new(Manager **ret, Hashmap *command_line_interfaces_by_name, char **ignored_interfaces, + LinkOperationalStateRange required_operstate, + AddressFamily required_family, + bool any, usec_t timeout); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +bool manager_configured(Manager *m); diff --git a/src/network/wait-online/wait-online.c b/src/network/wait-online/wait-online.c new file mode 100644 index 0000000..5328bba --- /dev/null +++ b/src/network/wait-online/wait-online.c @@ -0,0 +1,238 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include "sd-daemon.h" + +#include "build.h" +#include "daemon-util.h" +#include "main-func.h" +#include "manager.h" +#include "pretty-print.h" +#include "signal-util.h" +#include "socket-util.h" +#include "strv.h" + +static bool arg_quiet = false; +static usec_t arg_timeout = 120 * USEC_PER_SEC; +static Hashmap *arg_interfaces = NULL; +static char **arg_ignore = NULL; +static LinkOperationalStateRange arg_required_operstate = { _LINK_OPERSTATE_INVALID, _LINK_OPERSTATE_INVALID }; +static AddressFamily arg_required_family = ADDRESS_FAMILY_NO; +static bool arg_any = false; + +STATIC_DESTRUCTOR_REGISTER(arg_interfaces, hashmap_free_free_freep); +STATIC_DESTRUCTOR_REGISTER(arg_ignore, strv_freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-networkd-wait-online.service", "8", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...]\n\n" + "Block until network is configured.\n\n" + " -h --help Show this help\n" + " --version Print version string\n" + " -q --quiet Do not show status information\n" + " -i --interface=INTERFACE[:MIN_OPERSTATE[:MAX_OPERSTATE]]\n" + " Block until at least these interfaces have appeared\n" + " --ignore=INTERFACE Don't take these interfaces into account\n" + " -o --operational-state=MIN_OPERSTATE[:MAX_OPERSTATE]\n" + " Required operational state\n" + " -4 --ipv4 Requires at least one IPv4 address\n" + " -6 --ipv6 Requires at least one IPv6 address\n" + " --any Wait until at least one of the interfaces is online\n" + " --timeout=SECS Maximum time to wait for network connectivity\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + link); + + return 0; +} + +static int parse_interface_with_operstate_range(const char *str) { + _cleanup_free_ char *ifname = NULL; + _cleanup_free_ LinkOperationalStateRange *range = NULL; + const char *p; + int r; + + assert(str); + + range = new(LinkOperationalStateRange, 1); + if (!range) + return log_oom(); + + p = strchr(str, ':'); + if (p) { + r = parse_operational_state_range(p + 1, range); + if (r < 0) + log_error_errno(r, "Invalid operational state range '%s'", p + 1); + + ifname = strndup(optarg, p - optarg); + } else { + range->min = _LINK_OPERSTATE_INVALID; + range->max = _LINK_OPERSTATE_INVALID; + ifname = strdup(str); + } + if (!ifname) + return log_oom(); + + if (!ifname_valid(ifname)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid interface name '%s'", ifname); + + r = hashmap_ensure_put(&arg_interfaces, &string_hash_ops, ifname, TAKE_PTR(range)); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_error_errno(r, "Failed to store interface name: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Interface name %s is already specified", ifname); + + TAKE_PTR(ifname); + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_IGNORE, + ARG_ANY, + ARG_TIMEOUT, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "quiet", no_argument, NULL, 'q' }, + { "interface", required_argument, NULL, 'i' }, + { "ignore", required_argument, NULL, ARG_IGNORE }, + { "operational-state", required_argument, NULL, 'o' }, + { "ipv4", no_argument, NULL, '4' }, + { "ipv6", no_argument, NULL, '6' }, + { "any", no_argument, NULL, ARG_ANY }, + { "timeout", required_argument, NULL, ARG_TIMEOUT }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hi:qo:46", options, NULL)) >= 0) + + switch (c) { + + case 'h': + help(); + return 0; + + case 'q': + arg_quiet = true; + break; + + case ARG_VERSION: + return version(); + + case 'i': + r = parse_interface_with_operstate_range(optarg); + if (r < 0) + return r; + break; + + case ARG_IGNORE: + if (strv_extend(&arg_ignore, optarg) < 0) + return log_oom(); + + break; + + case 'o': { + LinkOperationalStateRange range; + + r = parse_operational_state_range(optarg, &range); + if (r < 0) + return log_error_errno(r, "Invalid operational state range '%s'", optarg); + + arg_required_operstate = range; + + break; + } + + case '4': + arg_required_family |= ADDRESS_FAMILY_IPV4; + break; + + case '6': + arg_required_family |= ADDRESS_FAMILY_IPV6; + break; + + case ARG_ANY: + arg_any = true; + break; + + case ARG_TIMEOUT: + r = parse_sec(optarg, &arg_timeout); + if (r < 0) + return r; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + return 1; +} + +static int run(int argc, char *argv[]) { + _cleanup_(manager_freep) Manager *m = NULL; + _unused_ _cleanup_(notify_on_cleanup) const char *notify_message = NULL; + int r; + + log_setup(); + + umask(0022); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (arg_quiet) + log_set_max_level(LOG_ERR); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0); + + r = manager_new(&m, arg_interfaces, arg_ignore, arg_required_operstate, arg_required_family, arg_any, arg_timeout); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + if (manager_configured(m)) + goto success; + + notify_message = notify_start("READY=1\n" + "STATUS=Waiting for network connections...", + "STATUS=Failed to wait for network connectivity..."); + + r = sd_event_loop(m->event); + if (r == -ETIMEDOUT) + return log_error_errno(r, "Timeout occurred while waiting for network connectivity."); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + +success: + notify_message = "STATUS=All interfaces configured..."; + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); |