network: restart DHCPv6, NDisc, and RADV when tracked IPv6LL is dropped

When the tracked IPv6 link-local address is removed, networkd clears
link->ipv6ll_address, but keeps DHCPv6, NDisc, and RADV running. These
engines keep using a stale source identity which affects the following:
- DHCPv6 client continues to send Solicit/Renew/Rebind from a nonexistent
  source address.
- NDisc continues to send Router Solicitations from a nonexistent source
  address. Router Advertisements cannot be received properly.
- RADV continues to advertise with a stale source address. This can lead
  to downstream hosts configuring invalid routes.
- DHCP-PD prefixes remain configured without a valid upstream DHCPv6 path.

Added link_ipv6ll_lost() to stop IPv6 dynamic engines and related states:
- sd_dhcp6_client_stop()
- ndisc_stop() + ndisc_flush()
- sd_radv_stop()

This is called from address_drop() when the dropped address matches the
tracked IPv6LL. After clearing the tracked address, it scans for another
ready link-local address on the interface. If found, this is set as
link->ipv6ll_address and link_ipv6ll_gained() is called to restart the
engines with the new source identity.

This resolves the FIXME in address_drop().

Signed-off-by: Aritra Basu <aritrbas+gh@cisco.com>
This commit is contained in:
Aritra Basu
2026-05-21 17:39:30 +09:00
committed by Yu Watanabe
parent 9f25feb4ed
commit dadc77c5fb
4 changed files with 81 additions and 6 deletions
+34 -2
View File
@@ -886,11 +886,43 @@ static int address_drop(Address *in, bool removed_by_us) {
address_del_netlabel(address);
/* FIXME: if the IPv6LL address is dropped, stop DHCPv6, NDISC, RADV. */
if (address->family == AF_INET6 &&
in6_addr_equal(&address->in_addr.in6, &link->ipv6ll_address))
in6_addr_equal(&address->in_addr.in6, &link->ipv6ll_address)) {
link->ipv6ll_address = (const struct in6_addr) {};
Address *a;
bool has_replacement;
/* If another ready IPv6LL address exists on this link, use it instead. */
SET_FOREACH(a, link->addresses) {
if (a == address)
continue;
if (a->family != AF_INET6)
continue;
if (!in6_addr_is_link_local(&a->in_addr.in6))
continue;
if (!address_is_ready(a))
continue;
link->ipv6ll_address = a->in_addr.in6;
break;
}
has_replacement = in6_addr_is_set(&link->ipv6ll_address);
/* Stop engines bound to the dropped IPv6LL source address. Do not return early on error.
* address_detach() and link_update_operstate() must run to keep link state consistent. */
r = link_ipv6ll_lost(link, &address->in_addr.in6, has_replacement);
if (r < 0)
log_link_warning_errno(link, r, "Failed to stop IPv6 services after IPv6LL loss, ignoring: %m");
/* If another IPv6LL address is available, restart engines with it. */
if (has_replacement) {
r = link_ipv6ll_gained(link);
if (r < 0)
log_link_warning_errno(link, r, "Failed to restart IPv6 services with alternate IPv6LL address, ignoring: %m");
}
}
ipv4acd_detach(link, address);
address_detach(address);
+40
View File
@@ -813,6 +813,46 @@ int link_ipv6ll_gained(Link *link) {
return 0;
}
int link_ipv6ll_lost(Link *link, const struct in6_addr *dropped_ipv6ll, bool has_replacement) {
int ret = 0, r;
assert(link);
assert(dropped_ipv6ll);
if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
return 0;
log_link_info(link, "Lost IPv6LL address %s%s.",
IN6_ADDR_TO_STRING(dropped_ipv6ll),
has_replacement ? ", switching to alternate IPv6LL source" : "");
r = sd_dhcp6_client_stop(link->dhcp6_client);
if (r < 0)
RET_GATHER(ret, log_link_warning_errno(link, r, "Could not stop DHCPv6 client: %m"));
/* DHCPv6 must be restarted to switch the client's source address, while NDisc and
* RADV can switch to the replacement IPv6LL in link_ipv6ll_gained() without flushing
* learned state. Keep link->ndisc_configured as-is in this path. */
if (has_replacement)
return ret;
r = ndisc_stop(link);
if (r < 0)
RET_GATHER(ret, log_link_warning_errno(link, r, "Could not stop IPv6 Router Discovery: %m"));
link->ndisc_configured = false;
ndisc_flush(link);
r = sd_radv_stop(link->radv);
if (r < 0)
RET_GATHER(ret, log_link_warning_errno(link, r, "Could not stop IPv6 Router Advertisement: %m"));
r = link_request_stacked_netdevs(link, NETDEV_LOCAL_ADDRESS_IPV6LL);
if (r < 0)
RET_GATHER(ret, log_link_warning_errno(link, r, "Could not reconfigure stacked netdevs after IPv6LL loss: %m"));
return ret;
}
int link_handle_bound_to_list(Link *link) {
bool required_up = false;
Link *l;
+1
View File
@@ -233,6 +233,7 @@ bool link_multicast_enabled(Link *link);
bool link_ipv6_enabled(Link *link);
int link_ipv6ll_gained(Link *link);
int link_ipv6ll_lost(Link *link, const struct in6_addr *dropped_ipv6ll, bool has_replacement);
bool link_has_ipv6_connectivity(Link *link);
int link_stop_engines(Link *link, bool may_keep_dynamic);
+6 -4
View File
@@ -697,6 +697,12 @@ int radv_start(Link *link) {
if (in6_addr_is_null(&link->ipv6ll_address))
return 0;
/* Update the source IPv6LL before the running check so replacement IPv6LL handover can
* rebind RADV without requiring stop/start. */
r = sd_radv_set_link_local_address(link->radv, &link->ipv6ll_address);
if (r < 0)
return r;
if (sd_radv_is_running(link->radv))
return 0;
@@ -706,10 +712,6 @@ int radv_start(Link *link) {
return log_link_debug_errno(link, r, "Failed to request DHCP delegated subnet prefix: %m");
}
r = sd_radv_set_link_local_address(link->radv, &link->ipv6ll_address);
if (r < 0)
return r;
log_link_debug(link, "Starting IPv6 Router Advertisements");
return sd_radv_start(link->radv);
}