]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
network: restart DHCPv6, NDisc, and RADV when tracked IPv6LL is dropped
authorAritra Basu <aritrbas+gh@cisco.com>
Tue, 21 Apr 2026 23:09:20 +0000 (19:09 -0400)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 21 May 2026 08:39:30 +0000 (17:39 +0900)
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>
src/network/networkd-address.c
src/network/networkd-link.c
src/network/networkd-link.h
src/network/networkd-radv.c

index 193c4a04b5f4456a1cad354fcb7edf9f2204dfcb..3880eab47fe9aff78ee00b828a7081e04fc0ab0e 100644 (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);
index a69a5e8979c3c8ab66027b638691c579bacc1d38..16fb529797d42d81b787267eba21d487fa9393fd 100644 (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;
index 456ec99185624cd685b01e48a7c5c594ac67ab39..b51736ae456b81456097f24572ee72abc83d1d1e 100644 (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);
index 02322d4dabad5d1d1102ca7992f9cf6cda0e0a52..1aafc0dd6d429cf83f1920807bb90fa69864085f 100644 (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);
 }