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',