From 0f5ef9b62a8f454208db69f3219c7484f87a068a Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 13 Oct 2021 16:26:09 +0900 Subject: [PATCH 1/6] network: dhcp6: introduce UseDelegatedPrefix= setting and enable by default Previously, the prefix delegation is enabled when at least one downstream interfaces request it. But, when the DHCPv6 client on the upstream interface is configured, some downstream interfaces may not exist yet, nor have .network file assigned. Also, if a system has thousands of interfaces, then the previous logic introduce O(n^2) search. This makes the prefix delegation is always enabled, except when it is explicitly disabled. Hopefully, that should not break anything, as the DHCPv6 server should ignore the prefix delegation request if the server do not have any prefix to delegate. --- man/systemd.network.xml | 13 +++++++++ src/network/networkd-dhcp6.c | 28 +++---------------- src/network/networkd-network-gperf.gperf | 1 + src/network/networkd-network.c | 1 + src/network/networkd-network.h | 1 + .../fuzz-network-parser/directives.network | 1 + 6 files changed, 21 insertions(+), 24 deletions(-) diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 01297c397f..cdb6f4eb87 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2063,6 +2063,19 @@ Table=1234 + + UseDelegatedPrefix= + + When true (the default), the client will request the DHCPv6 server to delegate + prefixes. If the server provides prefixes to be delegated, then subnets of the prefixes are + assigned to the interfaces which enables DHCPv6PrefixDelegation=. + See also DHCPv6PrefixDelegation= in [Network] section, + [DHCPv6PrefixDelegation] section, and + RFC 8415. + + + + RouteMetric= UseDNS= diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c index 76f4833b4c..1deaed1347 100644 --- a/src/network/networkd-dhcp6.c +++ b/src/network/networkd-dhcp6.c @@ -1423,25 +1423,6 @@ static int dhcp6_set_hostname(sd_dhcp6_client *client, Link *link) { return 0; } -static bool dhcp6_enable_prefix_delegation(Link *dhcp6_link) { - Link *link; - - assert(dhcp6_link); - assert(dhcp6_link->manager); - - HASHMAP_FOREACH(link, dhcp6_link->manager->links_by_index) { - if (link == dhcp6_link) - continue; - - if (!link_dhcp6_pd_is_enabled(link)) - continue; - - return true; - } - - return false; -} - static int dhcp6_set_identifier(Link *link, sd_dhcp6_client *client) { const DUID *duid; int r; @@ -1559,11 +1540,10 @@ static int dhcp6_configure(Link *link) { if (r < 0) return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set callback: %m"); - if (dhcp6_enable_prefix_delegation(link)) { - r = sd_dhcp6_client_set_prefix_delegation(client, true); - if (r < 0) - return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to set prefix delegation: %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 prefix delegation: %m", + enable_disable(link->network->dhcp6_use_pd_prefix)); if (link->network->dhcp6_pd_prefix_length > 0) { r = sd_dhcp6_client_set_prefix_delegation_hint(client, diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index a6415c2311..758aa8ee19 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -238,6 +238,7 @@ DHCPv4.SendVendorOption, config_parse_dhcp_send_option, DHCPv4.RouteMTUBytes, config_parse_mtu, AF_INET, offsetof(Network, dhcp_route_mtu) DHCPv4.FallbackLeaseLifetimeSec, config_parse_dhcp_fallback_lease_lifetime, 0, 0 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 diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index 3b8e508ee8..be3c087c8a 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -400,6 +400,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .dhcp_broadcast = -1, .dhcp6_use_address = true, + .dhcp6_use_pd_prefix = true, .dhcp6_use_dns = true, .dhcp6_use_hostname = true, .dhcp6_use_ntp = true, diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index b23f70b356..dd9804ea2d 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -169,6 +169,7 @@ struct Network { /* DHCPv6 Client support */ bool dhcp6_use_address; + bool dhcp6_use_pd_prefix; bool dhcp6_use_dns; bool dhcp6_use_dns_set; bool dhcp6_use_hostname; diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network index ea02d4c640..dae13a243c 100644 --- a/test/fuzz/fuzz-network-parser/directives.network +++ b/test/fuzz/fuzz-network-parser/directives.network @@ -130,6 +130,7 @@ RouteMTUBytes= FallbackLeaseLifetimeSec= [DHCPv6] UseAddress= +UseDelegatedPrefix= UseNTP= UseDNS= UseHostname= From 079f94edfa369480a158f715fecf12cd4b3c9451 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 13 Oct 2021 16:52:24 +0900 Subject: [PATCH 2/6] network: dhcp6: do not reconfigure/restart DHCPv6 clients when a new downstream appears Previously, when a downstream appears which requests delegated prefix to be assigned, then possibly restart all DHCPv6 clients for another interfaces. This makes networkd always honor the UseDelegatedPrefix= setting, and not restart DHCPv6 clients. --- src/network/networkd-dhcp6.c | 105 ++++++++++++++++------------------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c index 1deaed1347..a7106050c9 100644 --- a/src/network/networkd-dhcp6.c +++ b/src/network/networkd-dhcp6.c @@ -1320,72 +1320,63 @@ int dhcp6_start(Link *link) { return 1; } -int dhcp6_request_prefix_delegation(Link *link) { +static bool dhcp6_pd_uplink_is_ready(Link *link) { + assert(link); + + if (!link->network) + return false; + + if (!link->network->dhcp6_use_pd_prefix) + return false; + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return false; + + if (!link->dhcp6_client) + return false; + + if (sd_dhcp6_client_is_running(link->dhcp6_client) <= 0) + return false; + + if (!link->dhcp6_lease) + return false; + + return dhcp6_lease_has_pd_prefix(link->dhcp6_lease); +} + +static int dhcp6_pd_find_uplink(Link *link, Link **ret) { Link *l; assert(link); assert(link->manager); + assert(ret); + + HASHMAP_FOREACH(l, link->manager->links_by_index) { + if (!dhcp6_pd_uplink_is_ready(l)) + continue; + + /* Assume that there exists at most one link which acquired delegated prefixes. */ + *ret = l; + return 0; + } + + return -ENODEV; +} + +int dhcp6_request_prefix_delegation(Link *link) { + Link *uplink; + + assert(link); if (!link_dhcp6_pd_is_enabled(link)) return 0; - log_link_debug(link, "Requesting DHCPv6 prefixes to be delegated for new link"); + if (dhcp6_pd_find_uplink(link, &uplink) < 0) + return 0; - HASHMAP_FOREACH(l, link->manager->links_by_index) { - int r, enabled; - - if (l == link) - continue; - - if (!l->dhcp6_client) - continue; - - r = sd_dhcp6_client_get_prefix_delegation(l->dhcp6_client, &enabled); - if (r < 0) { - log_link_warning_errno(l, r, "Cannot get prefix delegation when adding new link: %m"); - link_enter_failed(l); - continue; - } - - if (enabled == 0) { - r = sd_dhcp6_client_set_prefix_delegation(l->dhcp6_client, 1); - if (r < 0) { - log_link_warning_errno(l, r, "Cannot enable prefix delegation when adding new link: %m"); - link_enter_failed(l); - continue; - } - } - - r = sd_dhcp6_client_is_running(l->dhcp6_client); - if (r <= 0) - continue; - - if (enabled != 0) { - if (dhcp6_lease_has_pd_prefix(l->dhcp6_lease)) { - log_link_debug(l, "Requesting re-assignment of delegated prefixes after adding new link"); - r = dhcp6_pd_prefix_acquired(l); - if (r < 0) - link_enter_failed(l); - } - continue; - } - - r = sd_dhcp6_client_stop(l->dhcp6_client); - if (r < 0) { - log_link_warning_errno(l, r, "Cannot stop DHCPv6 prefix delegation client after adding new link: %m"); - link_enter_failed(l); - continue; - } - - r = sd_dhcp6_client_start(l->dhcp6_client); - if (r < 0) { - log_link_warning_errno(l, r, "Cannot restart DHCPv6 prefix delegation client after adding new link: %m"); - link_enter_failed(l); - continue; - } - - log_link_debug(l, "Restarted DHCPv6 client to acquire prefix delegations after adding new link"); - } + log_link_debug(uplink, "Requesting re-assignment of delegated prefixes after adding new link %s", link->ifname); + if (dhcp6_pd_prefix_acquired(uplink) < 0) + link_enter_failed(uplink); /* dhcp6_pd_prefix_acquired() may make the link in failed state. */ if (link->state == LINK_STATE_FAILED) From 47e9c655b4b79bb03e20648b53c227e62cdd6e74 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 21 Oct 2021 17:49:08 +0900 Subject: [PATCH 3/6] network: dhcp6pd: do not trigger prefix reassignment for all downstreams when a new downstream appears It is not necessary to reconfigure other interfaces. --- src/network/networkd-dhcp6.c | 64 +++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c index a7106050c9..01850d49c5 100644 --- a/src/network/networkd-dhcp6.c +++ b/src/network/networkd-dhcp6.c @@ -968,6 +968,59 @@ static int dhcp6_pd_prefix_acquired(Link *dhcp6_link) { return 0; } +static int dhcp6_pd_assign_prefixes(Link *link, Link *uplink) { + usec_t timestamp_usec; + int r; + + assert(link); + assert(uplink); + assert(uplink->dhcp6_lease); + + /* This is similar to dhcp6_pd_prefix_acquired(), but called when a downstream interface + * appears later or reconfiguring the interface. */ + + r = sd_dhcp6_lease_get_timestamp(uplink->dhcp6_lease, clock_boottime_or_monotonic(), ×tamp_usec); + if (r < 0) + return r; + + r = dhcp6_pd_prepare(link); + if (r < 0) + return r; + + for (sd_dhcp6_lease_reset_pd_prefix_iter(uplink->dhcp6_lease);;) { + uint32_t lifetime_preferred_sec, lifetime_valid_sec; + usec_t lifetime_preferred_usec, lifetime_valid_usec; + struct in6_addr pd_prefix; + uint8_t pd_prefix_len; + + r = sd_dhcp6_lease_get_pd(uplink->dhcp6_lease, &pd_prefix, &pd_prefix_len, + &lifetime_preferred_sec, &lifetime_valid_sec); + if (r < 0) + break; + + lifetime_preferred_usec = usec_add(lifetime_preferred_sec * USEC_PER_SEC, timestamp_usec); + lifetime_valid_usec = usec_add(lifetime_valid_sec * USEC_PER_SEC, timestamp_usec); + + 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 = dhcp6_pd_assign_prefix(link, &pd_prefix, pd_prefix_len, lifetime_preferred_usec, lifetime_valid_usec); + if (r < 0) + return r; + } + + r = dhcp6_pd_finalize(link); + if (r < 0) + return r; + + return 0; +} + static int dhcp6_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { int r; @@ -1374,15 +1427,8 @@ int dhcp6_request_prefix_delegation(Link *link) { if (dhcp6_pd_find_uplink(link, &uplink) < 0) return 0; - log_link_debug(uplink, "Requesting re-assignment of delegated prefixes after adding new link %s", link->ifname); - if (dhcp6_pd_prefix_acquired(uplink) < 0) - link_enter_failed(uplink); - - /* dhcp6_pd_prefix_acquired() may make the link in failed state. */ - if (link->state == LINK_STATE_FAILED) - return -ENOANO; - - return 0; + log_link_debug(link, "Requesting subnets of delegated prefixes acquired by %s", uplink->ifname); + return dhcp6_pd_assign_prefixes(link, uplink); } static int dhcp6_set_hostname(sd_dhcp6_client *client, Link *link) { From 2cba2146ac70bf48b46d9e1a72c576ad94580085 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 13 Oct 2021 17:15:10 +0900 Subject: [PATCH 4/6] network: move config_parse_uplink() to networkd-dhcp-common.[ch] --- src/network/networkd-dhcp-common.c | 66 +++++++++++++++++++++++++++++ src/network/networkd-dhcp-common.h | 5 +++ src/network/networkd-network.c | 67 ------------------------------ src/network/networkd-network.h | 5 --- 4 files changed, 71 insertions(+), 72 deletions(-) diff --git a/src/network/networkd-dhcp-common.c b/src/network/networkd-dhcp-common.c index 7a769988ec..3a77a45780 100644 --- a/src/network/networkd-dhcp-common.c +++ b/src/network/networkd-dhcp-common.c @@ -1267,3 +1267,69 @@ int config_parse_network_duid_rawdata( /* 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 = userdata; + 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 + assert_not_reached(); + + if (isempty(rvalue) || streq(rvalue, ":auto")) { + *index = UPLINK_INDEX_AUTO; + *name = mfree(*name); + return 0; + } + + if (streq(rvalue, ":none")) { + *index = UPLINK_INDEX_NONE; + *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 index 2573247db2..176f6c04fe 100644 --- a/src/network/networkd-dhcp-common.h +++ b/src/network/networkd-dhcp-common.h @@ -9,6 +9,10 @@ #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 DHCP_ROUTE_METRIC 1024 #define DHCP6PD_ROUTE_METRIC 256 @@ -100,3 +104,4 @@ 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-network.c b/src/network/networkd-network.c index be3c087c8a..c3c792eb49 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -1245,73 +1245,6 @@ int config_parse_link_group( return 0; } - -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 = userdata; - 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 - assert_not_reached(); - - if (isempty(rvalue) || streq(rvalue, ":auto")) { - *index = UPLINK_INDEX_AUTO; - *name = mfree(*name); - return 0; - } - - if (streq(rvalue, ":none")) { - *index = UPLINK_INDEX_NONE; - *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; -} - DEFINE_CONFIG_PARSE_ENUM(config_parse_required_family_for_online, link_required_address_family, AddressFamily, "Failed to parse RequiredFamilyForOnline= setting"); diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index dd9804ea2d..3972fc5c89 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -26,10 +26,6 @@ #include "resolve-util.h" #include "socket-netlink.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 */ - typedef enum KeepConfiguration { KEEP_CONFIGURATION_NO = 0, KEEP_CONFIGURATION_DHCP_ON_START = 1 << 0, @@ -385,7 +381,6 @@ CONFIG_PARSER_PROTOTYPE(config_parse_keep_configuration); CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_link_local_address_gen_mode); CONFIG_PARSER_PROTOTYPE(config_parse_activation_policy); CONFIG_PARSER_PROTOTYPE(config_parse_link_group); -CONFIG_PARSER_PROTOTYPE(config_parse_uplink); const struct ConfigPerfItem* network_network_gperf_lookup(const char *key, GPERF_LEN_TYPE length); From dc5cae6c9ddc0b94e1cdd2b0980aca75e5d31f8e Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 13 Oct 2021 18:00:52 +0900 Subject: [PATCH 5/6] network: dhcp6: introduce UplinkInterface= for DHCP6 prefix delegation --- man/systemd.network.xml | 11 ++++++ src/network/networkd-dhcp-common.c | 14 +++++++- src/network/networkd-dhcp-common.h | 1 + src/network/networkd-dhcp6.c | 35 +++++++++++++++++++ src/network/networkd-network-gperf.gperf | 1 + src/network/networkd-network.h | 2 ++ .../fuzz-network-parser/directives.network | 1 + 7 files changed, 64 insertions(+), 1 deletion(-) diff --git a/man/systemd.network.xml b/man/systemd.network.xml index cdb6f4eb87..b6bf993adc 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2123,6 +2123,17 @@ Table=1234 is enabled. + + UplinkInterface= + + Specifies the name or the index of the uplink interface, or one of the special values + :self and :auto. When :self, the + interface itself is considered the uplink interface. When :auto, the first + link which acquired prefixes to be delegated from the DHCPv6 server is selected. Defaults to + :auto. + + + SubnetId= diff --git a/src/network/networkd-dhcp-common.c b/src/network/networkd-dhcp-common.c index 3a77a45780..0c0a76d362 100644 --- a/src/network/networkd-dhcp-common.c +++ b/src/network/networkd-dhcp-common.c @@ -1281,6 +1281,7 @@ int config_parse_uplink( void *userdata) { Network *network = userdata; + bool accept_none = true; int *index, r; char **name; @@ -1288,6 +1289,7 @@ int config_parse_uplink( assert(section); assert(lvalue); assert(rvalue); + assert(userdata); if (streq(section, "DHCPServer")) { index = &network->dhcp_server_uplink_index; @@ -1295,6 +1297,10 @@ int config_parse_uplink( } else if (streq(section, "IPv6SendRA")) { index = &network->router_uplink_index; name = &network->router_uplink_name; + } else if (streq(section, "DHCPv6PrefixDelegation")) { + index = &network->dhcp6_pd_uplink_index; + name = &network->dhcp_server_uplink_name; + accept_none = false; } else assert_not_reached(); @@ -1304,12 +1310,18 @@ int config_parse_uplink( return 0; } - if (streq(rvalue, ":none")) { + 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; diff --git a/src/network/networkd-dhcp-common.h b/src/network/networkd-dhcp-common.h index 176f6c04fe..d45b92e1a9 100644 --- a/src/network/networkd-dhcp-common.h +++ b/src/network/networkd-dhcp-common.h @@ -12,6 +12,7 @@ /* Special values for *_uplink_index. */ #define UPLINK_INDEX_AUTO 0 /* uplink will be selected automatically */ #define UPLINK_INDEX_NONE -1 /* uplink will not be selected automatically */ +#define UPLINK_INDEX_SELF -2 /* the interface itself is uplink */ #define DHCP_ROUTE_METRIC 1024 #define DHCP6PD_ROUTE_METRIC 256 diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c index 01850d49c5..52ff5eb3aa 100644 --- a/src/network/networkd-dhcp6.c +++ b/src/network/networkd-dhcp6.c @@ -42,6 +42,22 @@ bool link_dhcp6_pd_is_enabled(Link *link) { return link->network->dhcp6_pd; } +static int dhcp6_pd_resolve_uplink(Link *link, Link **ret) { + if (link->network->dhcp6_pd_uplink_name) + return link_get_by_name(link->manager, link->network->dhcp6_pd_uplink_name, ret); + + if (link->network->dhcp6_pd_uplink_index > 0) + return link_get_by_index(link->manager, link->network->dhcp6_pd_uplink_index, ret); + + if (link->network->dhcp6_pd_uplink_index == UPLINK_INDEX_SELF) { + *ret = link; + return 0; + } + + assert(link->network->dhcp6_pd_uplink_index == UPLINK_INDEX_AUTO); + return -ENOENT; +} + static bool dhcp6_lease_has_pd_prefix(sd_dhcp6_lease *lease) { uint32_t lifetime_preferred_sec, lifetime_valid_sec; struct in6_addr pd_prefix; @@ -575,6 +591,7 @@ static int dhcp6_pd_distribute_prefix( assert(pd_prefix_len <= 64); HASHMAP_FOREACH(link, dhcp6_link->manager->links_by_index) { + Link *uplink; if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) continue; @@ -591,6 +608,15 @@ static int dhcp6_pd_distribute_prefix( if (assign_preferred_subnet_id != link_has_preferred_subnet_id(link)) continue; + r = dhcp6_pd_resolve_uplink(link, &uplink); + if (r != -ENOENT) { + if (r < 0) /* The uplink interface does not exist yet. */ + continue; + + if (uplink != dhcp6_link) + continue; + } + r = dhcp6_pd_assign_prefix(link, pd_prefix, pd_prefix_len, lifetime_preferred_usec, lifetime_valid_usec); if (r < 0) { if (link == dhcp6_link) @@ -1402,8 +1428,17 @@ static int dhcp6_pd_find_uplink(Link *link, Link **ret) { assert(link); assert(link->manager); + assert(link->network); assert(ret); + if (dhcp6_pd_resolve_uplink(link, &l) >= 0) { + if (!dhcp6_pd_uplink_is_ready(l)) + return -EBUSY; + + *ret = l; + return 0; + } + HASHMAP_FOREACH(l, link->manager->links_by_index) { if (!dhcp6_pd_uplink_is_ready(l)) continue; diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 758aa8ee19..ccbf02b99d 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -325,6 +325,7 @@ BridgeMDB.VLANId, config_parse_mdb_vlan_id, BridgeVLAN.PVID, config_parse_brvlan_pvid, 0, 0 BridgeVLAN.VLAN, config_parse_brvlan_vlan, 0, 0 BridgeVLAN.EgressUntagged, config_parse_brvlan_untagged, 0, 0 +DHCPv6PrefixDelegation.UplinkInterface, config_parse_uplink, 0, 0 DHCPv6PrefixDelegation.SubnetId, config_parse_dhcp6_pd_subnet_id, 0, offsetof(Network, dhcp6_pd_subnet_id) DHCPv6PrefixDelegation.Announce, config_parse_bool, 0, offsetof(Network, dhcp6_pd_announce) DHCPv6PrefixDelegation.Assign, config_parse_bool, 0, offsetof(Network, dhcp6_pd_assign) diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index 3972fc5c89..bd9758c899 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -241,6 +241,8 @@ struct Network { int64_t dhcp6_pd_subnet_id; uint32_t dhcp6_pd_route_metric; Set *dhcp6_pd_tokens; + int dhcp6_pd_uplink_index; + char *dhcp6_pd_uplink_name; /* Bridge Support */ int use_bpdu; diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network index dae13a243c..985c2fc0d3 100644 --- a/test/fuzz/fuzz-network-parser/directives.network +++ b/test/fuzz/fuzz-network-parser/directives.network @@ -151,6 +151,7 @@ DUIDType= DUIDRawData= RouteTable= [DHCPv6PrefixDelegation] +UplinkInterface= SubnetId= Announce= Assign= From caad88a22bcbf3062c062f01105a8959b809f2ad Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 15 Oct 2021 02:51:18 +0900 Subject: [PATCH 6/6] test-network: add test cases for DHCPv6 prefix delegation --- test/test-network/conf/13-dummy.netdev | 4 + .../conf/25-veth-downstream.netdev | 9 + .../conf/dhcp6pd-downstream-dummy97.network | 16 ++ .../conf/dhcp6pd-downstream-dummy98.network | 16 ++ .../conf/dhcp6pd-downstream-dummy99.network | 14 ++ .../conf/dhcp6pd-downstream-test1.network | 16 ++ .../dhcp6pd-downstream-veth98-peer.network | 11 + .../conf/dhcp6pd-downstream-veth98.network | 22 ++ test/test-network/conf/dhcp6pd-server.network | 7 + .../conf/dhcp6pd-upstream.network | 19 ++ test/test-network/conf/isc-dhcpd-dhcp6pd.conf | 21 ++ test/test-network/systemd-networkd-tests.py | 193 ++++++++++++++++++ 12 files changed, 348 insertions(+) create mode 100644 test/test-network/conf/13-dummy.netdev create mode 100644 test/test-network/conf/25-veth-downstream.netdev create mode 100644 test/test-network/conf/dhcp6pd-downstream-dummy97.network create mode 100644 test/test-network/conf/dhcp6pd-downstream-dummy98.network create mode 100644 test/test-network/conf/dhcp6pd-downstream-dummy99.network create mode 100644 test/test-network/conf/dhcp6pd-downstream-test1.network create mode 100644 test/test-network/conf/dhcp6pd-downstream-veth98-peer.network create mode 100644 test/test-network/conf/dhcp6pd-downstream-veth98.network create mode 100644 test/test-network/conf/dhcp6pd-server.network create mode 100644 test/test-network/conf/dhcp6pd-upstream.network create mode 100644 test/test-network/conf/isc-dhcpd-dhcp6pd.conf diff --git a/test/test-network/conf/13-dummy.netdev b/test/test-network/conf/13-dummy.netdev new file mode 100644 index 0000000000..5f34b2f564 --- /dev/null +++ b/test/test-network/conf/13-dummy.netdev @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[NetDev] +Name=dummy99 +Kind=dummy diff --git a/test/test-network/conf/25-veth-downstream.netdev b/test/test-network/conf/25-veth-downstream.netdev new file mode 100644 index 0000000000..1329d915c7 --- /dev/null +++ b/test/test-network/conf/25-veth-downstream.netdev @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[NetDev] +Name=veth98 +Kind=veth +MACAddress=12:34:56:78:9a:be + +[Peer] +Name=veth98-peer +MACAddress=12:34:56:78:9a:bf diff --git a/test/test-network/conf/dhcp6pd-downstream-dummy97.network b/test/test-network/conf/dhcp6pd-downstream-dummy97.network new file mode 100644 index 0000000000..01174f3bb0 --- /dev/null +++ b/test/test-network/conf/dhcp6pd-downstream-dummy97.network @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=dummy97 + +[Network] +IPv6PrivacyExtensions=yes +IPv6AcceptRA=no +DHCP=no +DHCPv6PrefixDelegation=yes + +[DHCPv6PrefixDelegation] +UplinkInterface=veth99 +SubnetId=6 +Announce=no +Token=eui64 +Token=::1a:2b:3c:4d diff --git a/test/test-network/conf/dhcp6pd-downstream-dummy98.network b/test/test-network/conf/dhcp6pd-downstream-dummy98.network new file mode 100644 index 0000000000..8260e0cc7c --- /dev/null +++ b/test/test-network/conf/dhcp6pd-downstream-dummy98.network @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=dummy98 + +[Network] +IPv6PrivacyExtensions=yes +IPv6AcceptRA=no +DHCP=no +DHCPv6PrefixDelegation=yes + +[DHCPv6PrefixDelegation] +UplinkInterface=veth99 +SubnetId=3 +Announce=no +Token=eui64 +Token=::1a:2b:3c:4d diff --git a/test/test-network/conf/dhcp6pd-downstream-dummy99.network b/test/test-network/conf/dhcp6pd-downstream-dummy99.network new file mode 100644 index 0000000000..ad56f26262 --- /dev/null +++ b/test/test-network/conf/dhcp6pd-downstream-dummy99.network @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=dummy99 + +[Network] +IPv6PrivacyExtensions=yes +IPv6AcceptRA=no +DHCP=no +DHCPv6PrefixDelegation=yes + +[DHCPv6PrefixDelegation] +UplinkInterface=veth99 +Assign=no +Announce=no diff --git a/test/test-network/conf/dhcp6pd-downstream-test1.network b/test/test-network/conf/dhcp6pd-downstream-test1.network new file mode 100644 index 0000000000..78ddc72e2d --- /dev/null +++ b/test/test-network/conf/dhcp6pd-downstream-test1.network @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=test1 + +[Network] +IPv6PrivacyExtensions=yes +IPv6AcceptRA=no +DHCP=no +DHCPv6PrefixDelegation=yes + +[DHCPv6PrefixDelegation] +UplinkInterface=veth99 +SubnetId=0 +Announce=no +Token=eui64 +Token=::1a:2b:3c:4d diff --git a/test/test-network/conf/dhcp6pd-downstream-veth98-peer.network b/test/test-network/conf/dhcp6pd-downstream-veth98-peer.network new file mode 100644 index 0000000000..db0aec1809 --- /dev/null +++ b/test/test-network/conf/dhcp6pd-downstream-veth98-peer.network @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth98-peer + +[Network] +IPv6PrivacyExtensions=yes +IPv6AcceptRA=yes + +[IPv6AcceptRA] +Token=eui64 +Token=::1a:2b:3c:4e diff --git a/test/test-network/conf/dhcp6pd-downstream-veth98.network b/test/test-network/conf/dhcp6pd-downstream-veth98.network new file mode 100644 index 0000000000..14c543d1bc --- /dev/null +++ b/test/test-network/conf/dhcp6pd-downstream-veth98.network @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth98 + +[Network] +IPv6PrivacyExtensions=yes +IPv6AcceptRA=no +DHCP=no +DHCPv6PrefixDelegation=yes +IPv6SendRA=yes + +[DHCPv6PrefixDelegation] +UplinkInterface=veth99 +SubnetId=9 +Announce=yes +Token=eui64 +Token=::1a:2b:3c:4d + +[IPv6SendRA] +UplinkInterface=:none +EmitDNS=no +EmitDomains=no diff --git a/test/test-network/conf/dhcp6pd-server.network b/test/test-network/conf/dhcp6pd-server.network new file mode 100644 index 0000000000..f7a852665e --- /dev/null +++ b/test/test-network/conf/dhcp6pd-server.network @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth-peer + +[Network] +IPv6AcceptRA=no +Address=3ffe:501:ffff:100::1/64 diff --git a/test/test-network/conf/dhcp6pd-upstream.network b/test/test-network/conf/dhcp6pd-upstream.network new file mode 100644 index 0000000000..dd26f2ae5f --- /dev/null +++ b/test/test-network/conf/dhcp6pd-upstream.network @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth99 + +[Network] +IPv6PrivacyExtensions=yes +IPv6AcceptRA=no +DHCP=ipv6 +DHCPv6PrefixDelegation=yes + +[DHCPv6] +WithoutRA=solicit + +[DHCPv6PrefixDelegation] +UplinkInterface=:self +SubnetId=10 +Announce=no +Token=eui64 +Token=::1a:2b:3c:4d diff --git a/test/test-network/conf/isc-dhcpd-dhcp6pd.conf b/test/test-network/conf/isc-dhcpd-dhcp6pd.conf new file mode 100644 index 0000000000..c95b00e004 --- /dev/null +++ b/test/test-network/conf/isc-dhcpd-dhcp6pd.conf @@ -0,0 +1,21 @@ +default-lease-time 2592000; +preferred-lifetime 604800; + +option dhcp-renewal-time 3600; +option dhcp-rebinding-time 7200; + +# Enable RFC 5007 support (same than for DHCPv4) +allow leasequery; + +option dhcp6.name-servers 3ffe:501:ffff:100:200:ff:fe00:3f3e; +option dhcp6.domain-search "test.example.com","example.com"; + +option dhcp6.info-refresh-time 21600; + +subnet6 3ffe:501:ffff:100::/64 { + # Addresses available to clients + range6 3ffe:501:ffff:100::10 3ffe:501:ffff:100::100; + + # Some /64 prefixes available for Prefix Delegation (RFC 3633) + prefix6 3ffe:501:ffff:200:: 3ffe:501:ffff:f00:: /56; +} diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index ac5447b9ff..fc46c31c5d 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -14,6 +14,7 @@ import sys import time import unittest from shutil import copytree +from pathlib import Path network_unit_file_path='/run/systemd/network' networkd_runtime_directory='/run/systemd/netif' @@ -26,6 +27,9 @@ dnsmasq_pid_file='/run/networkd-ci/test-dnsmasq.pid' dnsmasq_log_file='/run/networkd-ci/test-dnsmasq.log' dnsmasq_lease_file='/run/networkd-ci/test-dnsmasq.lease' +isc_dhcpd_pid_file='/run/networkd-ci/test-isc-dhcpd.pid' +isc_dhcpd_lease_file='/run/networkd-ci/test-isc-dhcpd.lease' + systemd_lib_paths=['/usr/lib/systemd', '/lib/systemd'] which_paths=':'.join(systemd_lib_paths + os.getenv('PATH', os.defpath).lstrip(':').split(':')) @@ -531,6 +535,17 @@ def remove_dnsmasq_log_file(): if os.path.exists(dnsmasq_log_file): os.remove(dnsmasq_log_file) +def start_isc_dhcpd(interface, conf_file): + conf_file_path = os.path.join(networkd_ci_path, conf_file) + isc_dhcpd_command = f'dhcpd -6 -cf {conf_file_path} -lf {isc_dhcpd_lease_file} -pf {isc_dhcpd_pid_file} {interface}' + Path(isc_dhcpd_lease_file).touch() + check_output(isc_dhcpd_command) + +def stop_isc_dhcpd(): + stop_by_pid_file(isc_dhcpd_pid_file) + if os.path.exists(isc_dhcpd_lease_file): + os.remove(isc_dhcpd_lease_file) + def remove_networkd_state_files(): if os.path.exists(os.path.join(networkd_runtime_directory, 'state')): os.remove(os.path.join(networkd_runtime_directory, 'state')) @@ -4846,6 +4861,184 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): print(output) self.assertRegex(output, 'inet 192.168.5.[0-9]*/24 metric 1024 brd 192.168.5.255 scope global dynamic veth99') +class NetworkdDHCP6PDTests(unittest.TestCase, Utilities): + links = [ + 'dummy97', + 'dummy98', + 'dummy99', + 'test1', + 'veth98', + 'veth99', + ] + + units = [ + '11-dummy.netdev', + '12-dummy.netdev', + '13-dummy.netdev', + '25-veth.netdev', + '25-veth-downstream.netdev', + 'dhcp6pd-downstream-dummy97.network', + 'dhcp6pd-downstream-dummy98.network', + 'dhcp6pd-downstream-dummy99.network', + 'dhcp6pd-downstream-test1.network', + 'dhcp6pd-downstream-veth98.network', + 'dhcp6pd-downstream-veth98-peer.network', + 'dhcp6pd-server.network', + 'dhcp6pd-upstream.network', + ] + + def setUp(self): + stop_isc_dhcpd() + remove_links(self.links) + stop_networkd(show_logs=False) + + def tearDown(self): + stop_isc_dhcpd() + remove_links(self.links) + remove_unit_from_networkd_path(self.units) + stop_networkd(show_logs=True) + + def test_dhcp6pd(self): + copy_unit_to_networkd_unit_path('25-veth.netdev', 'dhcp6pd-server.network', 'dhcp6pd-upstream.network', + '25-veth-downstream.netdev', 'dhcp6pd-downstream-veth98.network', 'dhcp6pd-downstream-veth98-peer.network', + '11-dummy.netdev', 'dhcp6pd-downstream-test1.network', + 'dhcp6pd-downstream-dummy97.network', + '12-dummy.netdev', 'dhcp6pd-downstream-dummy98.network', + '13-dummy.netdev', 'dhcp6pd-downstream-dummy99.network') + + start_networkd() + self.wait_online(['veth-peer:carrier']) + start_isc_dhcpd('veth-peer', 'isc-dhcpd-dhcp6pd.conf') + self.wait_online(['veth-peer:routable', 'veth99:routable', 'test1:routable', 'dummy98:routable', 'dummy99:degraded', + 'veth98:routable', 'veth98-peer:routable']) + + print('### ip -6 address show dev veth-peer scope global') + output = check_output('ip -6 address show dev veth-peer scope global') + print(output) + self.assertIn('inet6 3ffe:501:ffff:100::1/64 scope global', output) + + print('### ip -6 address show dev veth99 scope global') + output = check_output('ip -6 address show dev veth99 scope global') + print(output) + # IA_NA + self.assertRegex(output, 'inet6 3ffe:501:ffff:100::[0-9]*/128 scope global (dynamic noprefixroute|noprefixroute dynamic)') + # address in IA_PD (Token=static) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]10:1a:2b:3c:4d/64 (metric 256 |)scope global dynamic mngtmpaddr') + # address in IA_PD (Token=eui64) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]10:1034:56ff:fe78:9abc/64 (metric 256 |)scope global dynamic mngtmpaddr') + # address in IA_PD (temporary) + # Note that the temporary addresses may appear after the link enters configured state + self.wait_address('veth99', 'inet6 3ffe:501:ffff:[2-9a-f]10:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*/64 (metric 256 |)scope global temporary dynamic', ipv='-6') + + print('### ip -6 address show dev test1 scope global') + output = check_output('ip -6 address show dev test1 scope global') + print(output) + # address in IA_PD (Token=static) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]00:1a:2b:3c:4d/64 (metric 256 |)scope global dynamic mngtmpaddr') + # address in IA_PD (temporary) + self.wait_address('test1', 'inet6 3ffe:501:ffff:[2-9a-f]00:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*/64 (metric 256 |)scope global temporary dynamic', ipv='-6') + + print('### ip -6 address show dev dummy98 scope global') + output = check_output('ip -6 address show dev dummy98 scope global') + print(output) + # address in IA_PD (Token=static) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]03:1a:2b:3c:4d/64 (metric 256 |)scope global dynamic mngtmpaddr') + # address in IA_PD (temporary) + self.wait_address('dummy98', 'inet6 3ffe:501:ffff:[2-9a-f]03:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*/64 (metric 256 |)scope global temporary dynamic', ipv='-6') + + print('### ip -6 address show dev veth98 scope global') + output = check_output('ip -6 address show dev veth98 scope global') + print(output) + # address in IA_PD (Token=static) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]09:1a:2b:3c:4d/64 (metric 256 |)scope global dynamic mngtmpaddr') + # address in IA_PD (Token=eui64) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]09:1034:56ff:fe78:9abe/64 (metric 256 |)scope global dynamic mngtmpaddr') + # address in IA_PD (temporary) + self.wait_address('veth98', 'inet6 3ffe:501:ffff:[2-9a-f]09:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*/64 (metric 256 |)scope global temporary dynamic', ipv='-6') + + print('### ip -6 address show dev veth98-peer scope global') + output = check_output('ip -6 address show dev veth98-peer scope global') + print(output) + # NDisc address (Token=static) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]09:1a:2b:3c:4e/64 (metric 256 |)scope global dynamic mngtmpaddr') + # NDisc address (Token=eui64) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]09:1034:56ff:fe78:9abf/64 (metric 256 |)scope global dynamic mngtmpaddr') + # NDisc address (temporary) + self.wait_address('veth98-peer', 'inet6 3ffe:501:ffff:[2-9a-f]09:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*/64 (metric 256 |)scope global temporary dynamic', ipv='-6') + + print('### ip -6 address show dev dummy99 scope global') + output = check_output('ip -6 address show dev dummy98 scope global') + print(output) + + print('### ip -6 route show type unreachable') + output = check_output('ip -6 route show type unreachable') + print(output) + self.assertRegex(output, 'unreachable 3ffe:501:ffff:[2-9a-f]00::/56 dev lo proto dhcp') + + print('### ip -6 route show dev veth99') + output = check_output('ip -6 route show dev veth99') + print(output) + self.assertRegex(output, '3ffe:501:ffff:[2-9a-f]10::/64 proto kernel metric [0-9]* expires') + + print('### ip -6 route show dev test1') + output = check_output('ip -6 route show dev test1') + print(output) + self.assertRegex(output, '3ffe:501:ffff:[2-9a-f]00::/64 proto kernel metric [0-9]* expires') + + print('### ip -6 route show dev dummy98') + output = check_output('ip -6 route show dev dummy98') + print(output) + self.assertRegex(output, '3ffe:501:ffff:[2-9a-f]03::/64 proto kernel metric [0-9]* expires') + + print('### ip -6 route show dev dummy99') + output = check_output('ip -6 route show dev dummy99') + print(output) + self.assertRegex(output, '3ffe:501:ffff:[2-9a-f]01::/64 proto dhcp metric [0-9]* expires') + + print('### ip -6 route show dev veth98') + output = check_output('ip -6 route show dev veth98') + print(output) + self.assertRegex(output, '3ffe:501:ffff:[2-9a-f]09::/64 proto kernel metric [0-9]* expires') + + print('### ip -6 route show dev veth98-peer') + output = check_output('ip -6 route show dev veth98-peer') + print(output) + self.assertRegex(output, '3ffe:501:ffff:[2-9a-f]09::/64 proto ra metric [0-9]* expires') + + # Test case for a downstream which appears later + check_output('ip link add dummy97 type dummy') + self.wait_online(['dummy97:routable']) + + print('### ip -6 address show dev dummy97 scope global') + output = check_output('ip -6 address show dev dummy97 scope global') + print(output) + # address in IA_PD (Token=static) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]06:1a:2b:3c:4d/64 (metric 256 |)scope global dynamic mngtmpaddr') + # address in IA_PD (temporary) + self.wait_address('dummy97', 'inet6 3ffe:501:ffff:[2-9a-f]06:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*/64 (metric 256 |)scope global temporary dynamic', ipv='-6') + + print('### ip -6 route show dev dummy97') + output = check_output('ip -6 route show dev dummy97') + print(output) + self.assertRegex(output, '3ffe:501:ffff:[2-9a-f]06::/64 proto kernel metric [0-9]* expires') + + # Test case for reconfigure + check_output(*networkctl_cmd, 'reconfigure', 'dummy98', env=env) + self.wait_online(['dummy98:routable']) + + print('### ip -6 address show dev dummy98 scope global') + output = check_output('ip -6 address show dev dummy98 scope global') + print(output) + # address in IA_PD (Token=static) + self.assertRegex(output, 'inet6 3ffe:501:ffff:[2-9a-f]03:1a:2b:3c:4d/64 (metric 256 |)scope global dynamic mngtmpaddr') + # address in IA_PD (temporary) + self.wait_address('dummy98', 'inet6 3ffe:501:ffff:[2-9a-f]03:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*/64 (metric 256 |)scope global temporary dynamic', ipv='-6') + + print('### ip -6 route show dev dummy98') + output = check_output('ip -6 route show dev dummy98') + print(output) + self.assertRegex(output, '3ffe:501:ffff:[2-9a-f]03::/64 proto kernel metric [0-9]* expires') + class NetworkdIPv6PrefixTests(unittest.TestCase, Utilities): links = [ 'dummy98',