]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sd-dhcp-client: simplify the implementation of IPv6 Only mode support
authorYu Watanabe <watanabe.yu+github@gmail.com>
Tue, 14 Apr 2026 23:47:42 +0000 (08:47 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 23 Apr 2026 22:40:08 +0000 (07:40 +0900)
This drop delay after ACK, as it has many problems. See comment in
sd_dhcp_client_is_waiting_for_ipv6_connectivity() for more details.
This way, the logic becomes much much simpler.

Also, do not restart the client if we lost IPv6 connectivity in
sd_dhcp_client side, but restart the client by networkd. As,
sd_dhcp_client does not know if we can start the client or not,
e.g., the interface may be currently down.

src/libsystemd-network/dhcp-client-internal.h
src/libsystemd-network/sd-dhcp-client.c
src/network/networkd-dhcp4.c
src/systemd/sd-dhcp-client.h

index e08ea3deda0ff819207402d0c801c9b3215bc2cf..fab4ff24aaf99d2dd8a83cc83a78172cc9f11703 100644 (file)
@@ -70,7 +70,6 @@ struct sd_dhcp_client {
         sd_event_source *timeout_t1;
         sd_event_source *timeout_t2;
         sd_event_source *timeout_expire;
-        sd_event_source *timeout_ipv6_only_mode;
         sd_dhcp_client_callback_t callback;
         void *userdata;
         sd_dhcp_client_callback_t state_callback;
index 7a08861eb819003b919cb427d8efbc45df2a6bd1..76558bdd72e04ec5df87bac36bec8ed72c757e93 100644 (file)
@@ -635,7 +635,6 @@ static void client_disable_event_sources(sd_dhcp_client *client) {
         (void) event_source_disable(client->timeout_t1);
         (void) event_source_disable(client->timeout_t2);
         (void) event_source_disable(client->timeout_expire);
-        (void) event_source_disable(client->timeout_ipv6_only_mode);
 }
 
 static void client_initialize(sd_dhcp_client *client) {
@@ -1293,8 +1292,6 @@ static int client_initialize_time_events(sd_dhcp_client *client) {
         assert(client);
         assert(client->event);
 
-        (void) event_source_disable(client->timeout_ipv6_only_mode);
-
         return event_reset_time_relative(
                         client->event,
                         &client->timeout_resend,
@@ -1566,39 +1563,17 @@ static int client_handle_offer_or_rapid_ack(sd_dhcp_client *client, DHCPMessage
         return 0;
 }
 
-static int client_enter_requesting_now(sd_dhcp_client *client) {
-        assert(client);
-
-        client_set_state(client, DHCP_STATE_REQUESTING);
-        client->discover_attempt = 0;
-        client->request_attempt = 0;
-
-        return event_reset_time(client->event, &client->timeout_resend,
-                                CLOCK_BOOTTIME, 0, 0,
-                                client_timeout_resend, client,
-                                client->event_priority, "dhcp4-resend-timer",
-                                /* force_reset= */ true);
-}
-
-static int client_enter_requesting_delayed(sd_event_source *s, uint64_t usec, void *userdata) {
-        sd_dhcp_client *client = ASSERT_PTR(userdata);
-        DHCP_CLIENT_DONT_DESTROY(client);
-        int r;
-
-        r = client_enter_requesting_now(client);
-        if (r < 0)
-                client_stop(client, r);
-
-        return 0;
-}
-
 static int client_enter_requesting(sd_dhcp_client *client) {
         assert(client);
         assert(client->lease);
 
         client_disable_event_sources(client);
 
-        if (client->lease->ipv6_only_preferred_usec > 0) {
+        client_set_state(client, DHCP_STATE_REQUESTING);
+        client->discover_attempt = 0;
+        client->request_attempt = 0;
+
+        if (sd_dhcp_client_is_waiting_for_ipv6_connectivity(client)) {
                 if (client->ipv6_acquired) {
                         log_dhcp_client(client,
                                         "Received an OFFER with IPv6-only preferred option, and the host already acquired IPv6 connectivity, stopping DHCPv4 client.");
@@ -1608,16 +1583,19 @@ static int client_enter_requesting(sd_dhcp_client *client) {
                 log_dhcp_client(client,
                                 "Received an OFFER with IPv6-only preferred option, delaying to send REQUEST with %s.",
                                 FORMAT_TIMESPAN(client->lease->ipv6_only_preferred_usec, USEC_PER_SEC));
-
-                return event_reset_time_relative(client->event, &client->timeout_ipv6_only_mode,
-                                                 CLOCK_BOOTTIME,
-                                                 client->lease->ipv6_only_preferred_usec, 0,
-                                                 client_enter_requesting_delayed, client,
-                                                 client->event_priority, "dhcp4-ipv6-only-mode-timer",
-                                                 /* force_reset= */ true);
         }
 
-        return client_enter_requesting_now(client);
+        return event_reset_time_relative(
+                        client->event,
+                        &client->timeout_resend,
+                        CLOCK_BOOTTIME,
+                        client->lease->ipv6_only_preferred_usec,
+                        /* accuracy= */ 0,
+                        client_timeout_resend,
+                        client,
+                        client->event_priority,
+                        "dhcp4-resend-timer",
+                        /* force_reset= */ true);
 }
 
 static bool lease_equal(const sd_dhcp_lease *a, const sd_dhcp_lease *b) {
@@ -1770,14 +1748,19 @@ static int client_set_lease_timeouts(sd_dhcp_client *client) {
         return 0;
 }
 
-static int client_enter_bound_now(sd_dhcp_client *client, int notify_event) {
+static int client_enter_bound(sd_dhcp_client *client, int notify_event) {
         int r;
 
         assert(client);
+        assert(client->lease);
 
         if (IN_SET(client->state, DHCP_STATE_REQUESTING, DHCP_STATE_REBOOTING))
                 notify_event = SD_DHCP_CLIENT_EVENT_IP_ACQUIRE;
 
+        client_disable_event_sources(client);
+
+        client->start_delay = 0;
+
         client_set_state(client, DHCP_STATE_BOUND);
         client->discover_attempt = 0;
         client->request_attempt = 0;
@@ -1786,61 +1769,12 @@ static int client_enter_bound_now(sd_dhcp_client *client, int notify_event) {
 
         r = client_set_lease_timeouts(client);
         if (r < 0)
-                log_dhcp_client_errno(client, r, "could not set lease timeouts: %m");
+                log_dhcp_client_errno(client, r, "Failed to set lease timeouts: %m");
 
         client_notify(client, notify_event);
         return 0;
 }
 
-static int client_enter_bound_delayed(sd_event_source *s, uint64_t usec, void *userdata) {
-        sd_dhcp_client *client = ASSERT_PTR(userdata);
-        DHCP_CLIENT_DONT_DESTROY(client);
-        int r;
-
-        r = client_enter_bound_now(client, SD_DHCP_CLIENT_EVENT_IP_ACQUIRE);
-        if (r < 0)
-                client_stop(client, r);
-
-        return 0;
-}
-
-static int client_enter_bound(sd_dhcp_client *client, int notify_event) {
-        assert(client);
-        assert(client->lease);
-
-        client_disable_event_sources(client);
-
-        client->start_delay = 0;
-
-        /* RFC 8925 section 3.2
-         * If the client is in the INIT-REBOOT state, it SHOULD stop the DHCPv4 configuration process or
-         * disable the IPv4 stack completely for V6ONLY_WAIT seconds or until the network attachment event,
-         * whichever happens first.
-         *
-         * In the below, the condition uses REBOOTING, instead of INIT-REBOOT, as the client state has
-         * already transitioned from INIT-REBOOT to REBOOTING after sending a DHCPREQUEST message. */
-        if (client->state == DHCP_STATE_REBOOTING && client->lease->ipv6_only_preferred_usec > 0) {
-                if (client->ipv6_acquired) {
-                        log_dhcp_client(client,
-                                        "Received an ACK with IPv6-only preferred option, and the host already acquired IPv6 connectivity, stopping DHCPv4 client.");
-                        return sd_dhcp_client_stop(client);
-                }
-
-                log_dhcp_client(client,
-                                "Received an ACK with IPv6-only preferred option, delaying to enter bound state with %s.",
-                                FORMAT_TIMESPAN(client->lease->ipv6_only_preferred_usec, USEC_PER_SEC));
-
-                return event_reset_time_relative(client->event, &client->timeout_ipv6_only_mode,
-                                                 CLOCK_BOOTTIME,
-                                                 client->lease->ipv6_only_preferred_usec, 0,
-                                                 client_enter_bound_delayed, client,
-                                                 client->event_priority, "dhcp4-ipv6-only-mode",
-                                                 /* force_reset= */ true);
-        }
-
-        return client_enter_bound_now(client, notify_event);
-}
-
 static int client_restart(sd_dhcp_client *client) {
         int r;
         assert(client);
@@ -2107,9 +2041,6 @@ int sd_dhcp_client_start(sd_dhcp_client *client) {
         assert_return(client->event, -EINVAL);
         assert_return(client->ifindex > 0, -EINVAL);
 
-        /* Note, do not reset the flag in client_initialize(), as it is also called on expire. */
-        client->ipv6_acquired = false;
-
         /* If no client identifier exists, construct an RFC 4361-compliant one */
         if (!sd_dhcp_client_id_is_set(&client->client_id)) {
                 r = sd_dhcp_client_set_iaid_duid_en(client, /* iaid_set= */ false, /* iaid= */ 0);
@@ -2245,28 +2176,49 @@ int sd_dhcp_client_stop(sd_dhcp_client *client) {
         return 0;
 }
 
+int sd_dhcp_client_is_waiting_for_ipv6_connectivity(sd_dhcp_client *client) {
+        /* Note that we intentionally do not implement the following behavior:
+         *
+         * RFC 8925, section 3.2:
+         *   If the client is in the INIT-REBOOT state, it SHOULD stop the DHCPv4 configuration process or
+         *   disable the IPv4 stack completely for V6ONLY_WAIT seconds or until the next network attachment
+         *   event, whichever occurs first.
+         *
+         * Delaying the application of an acquired IPv4 address after DHCPACK introduces several issues:
+         *
+         * - If T1 is reached before the address is assigned to the interface, the client cannot send a
+         *   unicast DHCPREQUEST during RENEWING.
+         *
+         * - If the client is stopped before the address is configured, it cannot send a DHCPRELEASE message,
+         *   which also requires a valid source address.
+         *
+         * While these issues could be worked around, doing so would significantly complicate the
+         * implementation and violate assumptions in the DHCP state machine as defined in RFC 2131.
+         *
+         * Instead, we only honor the IPv6-Only Preferred delay (Option 108) in the REQUESTING state, i.e.
+         * before any DHCPREQUEST has been sent. */
+
+        return
+                client &&
+                client->state == DHCP_STATE_REQUESTING &&
+                client->request_attempt == 0 &&
+                client->lease &&
+                client->lease->ipv6_only_preferred_usec > 0;
+}
+
 int sd_dhcp_client_set_ipv6_connectivity(sd_dhcp_client *client, int have) {
         if (!client)
                 return 0;
 
-        /* We have already received a message with IPv6-Only preferred option, and are waiting for IPv6
-         * connectivity or timeout, let's stop the client. */
-        if (have && sd_event_source_get_enabled(client->timeout_ipv6_only_mode, NULL) > 0)
-                return sd_dhcp_client_stop(client);
-
-        /* Otherwise, save that the host already has IPv6 connectivity. */
         client->ipv6_acquired = have;
-        return 0;
-}
-
-int sd_dhcp_client_interrupt_ipv6_only_mode(sd_dhcp_client *client) {
-        assert_return(client, -EINVAL);
-        assert_return(sd_dhcp_client_is_running(client), -ESTALE);
 
-        if (sd_event_source_get_enabled(client->timeout_ipv6_only_mode, NULL) <= 0)
-                return 0;
+        if (have && sd_dhcp_client_is_waiting_for_ipv6_connectivity(client)) {
+                log_dhcp_client(client,
+                                "Acquired IPv6 connectivity before sending REQUEST, stopping DHCPv4 client.");
+                return sd_dhcp_client_stop(client);
+        }
 
-        return client_start(client);
+        return 0;
 }
 
 int sd_dhcp_client_attach_event(sd_dhcp_client *client, sd_event *event, int64_t priority) {
@@ -2322,7 +2274,6 @@ static sd_dhcp_client* dhcp_client_free(sd_dhcp_client *client) {
         sd_event_source_unref(client->timeout_t1);
         sd_event_source_unref(client->timeout_t2);
         sd_event_source_unref(client->timeout_expire);
-        sd_event_source_unref(client->timeout_ipv6_only_mode);
 
         sd_dhcp_client_detach_event(client);
 
index f274a0c4d94a096214741badf6e8d678b0397d5a..dc6b78e326c34a30e045194482b43618d4507965 100644 (file)
@@ -1733,6 +1733,8 @@ int dhcp4_update_mac(Link *link) {
 }
 
 int dhcp4_update_ipv6_connectivity(Link *link) {
+        int r;
+
         assert(link);
 
         if (!link->network)
@@ -1744,16 +1746,20 @@ int dhcp4_update_ipv6_connectivity(Link *link) {
         if (!link->dhcp_client)
                 return 0;
 
-        /* If the client is running, set the current connectivity. */
-        if (sd_dhcp_client_is_running(link->dhcp_client))
-                return sd_dhcp_client_set_ipv6_connectivity(link->dhcp_client, link_has_ipv6_connectivity(link));
+        bool have = link_has_ipv6_connectivity(link);
+        r = sd_dhcp_client_set_ipv6_connectivity(link->dhcp_client, have);
+        if (r < 0)
+                return r;
 
-        /* If the client has been already stopped or not started yet, let's check the current connectivity
-         * and start the client if necessary. */
-        if (link_has_ipv6_connectivity(link))
-                return 0;
+        /* If we do not have IPv6 connectivity, and the client has been already stopped or not started yet,
+         * let's start the client if possible. */
+        if (!have && !sd_dhcp_client_is_running(link->dhcp_client)) {
+                r = dhcp4_start_full(link, /* set_ipv6_connectivity= */ false);
+                if (r < 0)
+                        return r;
+        }
 
-        return dhcp4_start_full(link, /* set_ipv6_connectivity= */ false);
+        return 0;
 }
 
 int dhcp4_start_full(Link *link, bool set_ipv6_connectivity) {
@@ -1805,8 +1811,10 @@ int dhcp4_renew(Link *link) {
                 return dhcp4_start(link);
 
         /* The client may be waiting for IPv6 connectivity. Let's restart the client in that case. */
-        if (dhcp_client_get_state(link->dhcp_client) != DHCP_STATE_BOUND)
-                return sd_dhcp_client_interrupt_ipv6_only_mode(link->dhcp_client);
+        if (sd_dhcp_client_is_waiting_for_ipv6_connectivity(link->dhcp_client)) {
+                sd_dhcp_client_stop(link->dhcp_client);
+                return dhcp4_start(link);
+        }
 
         /* Otherwise, send a RENEW command. */
         return sd_dhcp_client_send_renew(link->dhcp_client);
index 033d5ad894ec34585d935e2495a461327f16914e..378271aef885dfba76055b4f6a4469a96768bd83 100644 (file)
@@ -159,7 +159,7 @@ int sd_dhcp_client_start(sd_dhcp_client *client);
 int sd_dhcp_client_send_decline(sd_dhcp_client *client);
 int sd_dhcp_client_send_renew(sd_dhcp_client *client);
 int sd_dhcp_client_set_ipv6_connectivity(sd_dhcp_client *client, int have);
-int sd_dhcp_client_interrupt_ipv6_only_mode(sd_dhcp_client *client);
+int sd_dhcp_client_is_waiting_for_ipv6_connectivity(sd_dhcp_client *client);
 
 _SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_client);