diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 01297c397f..b6bf993adc 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= @@ -2110,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 7a769988ec..0c0a76d362 100644 --- a/src/network/networkd-dhcp-common.c +++ b/src/network/networkd-dhcp-common.c @@ -1267,3 +1267,81 @@ 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; + bool accept_none = true; + int *index, r; + char **name; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(userdata); + + 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 (streq(section, "DHCPv6PrefixDelegation")) { + index = &network->dhcp6_pd_uplink_index; + name = &network->dhcp_server_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 index 2573247db2..d45b92e1a9 100644 --- a/src/network/networkd-dhcp-common.h +++ b/src/network/networkd-dhcp-common.h @@ -9,6 +9,11 @@ #include "set.h" #include "time-util.h" +/* Special values for *_uplink_index. */ +#define UPLINK_INDEX_AUTO 0 /* uplink will be selected automatically */ +#define UPLINK_INDEX_NONE -1 /* uplink will not be selected automatically */ +#define UPLINK_INDEX_SELF -2 /* the interface itself is uplink */ + #define DHCP_ROUTE_METRIC 1024 #define DHCP6PD_ROUTE_METRIC 256 @@ -100,3 +105,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-dhcp6.c b/src/network/networkd-dhcp6.c index 76f4833b4c..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) @@ -968,6 +994,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; @@ -1320,78 +1399,71 @@ 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(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; + + /* 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"); - } - - /* 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) { @@ -1423,25 +1495,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 +1612,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..ccbf02b99d 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 @@ -324,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.c b/src/network/networkd-network.c index 3b8e508ee8..c3c792eb49 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, @@ -1244,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 b23f70b356..bd9758c899 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, @@ -169,6 +165,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; @@ -244,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; @@ -384,7 +383,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); diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network index ea02d4c640..985c2fc0d3 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= @@ -150,6 +151,7 @@ DUIDType= DUIDRawData= RouteTable= [DHCPv6PrefixDelegation] +UplinkInterface= SubnetId= Announce= Assign= 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',