From 6a3b0e847509fd3281989d3f3db1fb5104815d72 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 18 Mar 2026 19:26:22 +0900 Subject: [PATCH 01/13] sd-dhcp-client: move the object definition to the header Then, we can split the long sd-dhcp-client.c into small pieces later. This also drops redundant typedef, which is also in sd-dhcp-client.h. --- src/libsystemd-network/dhcp-client-internal.h | 67 ++++++++++++++++++- src/libsystemd-network/sd-dhcp-client.c | 67 ------------------- 2 files changed, 65 insertions(+), 69 deletions(-) diff --git a/src/libsystemd-network/dhcp-client-internal.h b/src/libsystemd-network/dhcp-client-internal.h index f75ca71b43..07e61b0d76 100644 --- a/src/libsystemd-network/dhcp-client-internal.h +++ b/src/libsystemd-network/dhcp-client-internal.h @@ -3,8 +3,11 @@ #include "sd-dhcp-client.h" -#include "sd-forward.h" +#include "dhcp-client-id-internal.h" +#include "ether-addr-util.h" #include "network-common.h" +#include "sd-forward.h" +#include "socket-util.h" typedef enum DHCPState { DHCP_STATE_STOPPED, @@ -22,7 +25,67 @@ typedef enum DHCPState { DECLARE_STRING_TABLE_LOOKUP_TO_STRING(dhcp_state, DHCPState); -typedef struct sd_dhcp_client sd_dhcp_client; +struct sd_dhcp_client { + unsigned n_ref; + + DHCPState state; + sd_event *event; + int event_priority; + sd_event_source *timeout_resend; + + int ifindex; + char *ifname; + + sd_device *dev; + + int fd; + uint16_t port; + uint16_t server_port; + union sockaddr_union link; + sd_event_source *receive_message; + bool request_broadcast; + Set *req_opts; + bool anonymize; + bool rapid_commit; + be32_t last_addr; + struct hw_addr_data hw_addr; + struct hw_addr_data bcast_addr; + uint16_t arp_type; + sd_dhcp_client_id client_id; + char *hostname; + char *vendor_class_identifier; + char *mudurl; + char **user_class; + uint32_t mtu; + usec_t fallback_lease_lifetime; + uint32_t xid; + usec_t start_time; + usec_t t1_time; + usec_t t2_time; + usec_t expire_time; + uint64_t discover_attempt; + uint64_t request_attempt; + uint64_t max_discover_attempts; + uint64_t max_request_attempts; + OrderedHashmap *extra_options; + OrderedHashmap *vendor_options; + 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; + void *state_userdata; + sd_dhcp_lease *lease; + usec_t start_delay; + int ip_service_type; + int socket_priority; + bool socket_priority_set; + bool ipv6_acquired; + bool bootp; + bool send_release; +}; int dhcp_client_set_state_callback( sd_dhcp_client *client, diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 19f85e09df..c530e6c222 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -7,11 +7,8 @@ #include #include -#include "sd-dhcp-client.h" - #include "alloc-util.h" #include "device-util.h" -#include "dhcp-client-id-internal.h" #include "dhcp-client-internal.h" #include "dhcp-lease-internal.h" #include "dhcp-network.h" @@ -19,7 +16,6 @@ #include "dhcp-packet.h" #include "dns-domain.h" #include "errno-util.h" -#include "ether-addr-util.h" #include "event-util.h" #include "fd-util.h" #include "hostname-util.h" @@ -28,7 +24,6 @@ #include "network-common.h" #include "random-util.h" #include "set.h" -#include "socket-util.h" #include "sort-util.h" #include "string-table.h" #include "string-util.h" @@ -44,68 +39,6 @@ #define TRANSIENT_FAILURE_ATTEMPTS 3 /* Arbitrary limit: how many attempts are considered enough to report * transient failure. */ -struct sd_dhcp_client { - unsigned n_ref; - - DHCPState state; - sd_event *event; - int event_priority; - sd_event_source *timeout_resend; - - int ifindex; - char *ifname; - - sd_device *dev; - - int fd; - uint16_t port; - uint16_t server_port; - union sockaddr_union link; - sd_event_source *receive_message; - bool request_broadcast; - Set *req_opts; - bool anonymize; - bool rapid_commit; - be32_t last_addr; - struct hw_addr_data hw_addr; - struct hw_addr_data bcast_addr; - uint16_t arp_type; - sd_dhcp_client_id client_id; - char *hostname; - char *vendor_class_identifier; - char *mudurl; - char **user_class; - uint32_t mtu; - usec_t fallback_lease_lifetime; - uint32_t xid; - usec_t start_time; - usec_t t1_time; - usec_t t2_time; - usec_t expire_time; - uint64_t discover_attempt; - uint64_t request_attempt; - uint64_t max_discover_attempts; - uint64_t max_request_attempts; - OrderedHashmap *extra_options; - OrderedHashmap *vendor_options; - 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; - void *state_userdata; - sd_dhcp_lease *lease; - usec_t start_delay; - int ip_service_type; - int socket_priority; - bool socket_priority_set; - bool ipv6_acquired; - bool bootp; - bool send_release; -}; - static const uint8_t default_req_opts[] = { SD_DHCP_OPTION_SUBNET_MASK, SD_DHCP_OPTION_ROUTER, From 3817ab9e1412bb47e3b47cdba663eed4985dae2b Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 24 Apr 2026 07:28:30 +0900 Subject: [PATCH 02/13] sd-dhcp-client: add one missing assertion Found and suggested by Claude. Nice! --- src/libsystemd-network/sd-dhcp-client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index c530e6c222..6bc96a47e7 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -1476,7 +1476,7 @@ static int client_timeout_t2(sd_event_source *s, uint64_t usec, void *userdata) } static int client_timeout_t1(sd_event_source *s, uint64_t usec, void *userdata) { - sd_dhcp_client *client = userdata; + sd_dhcp_client *client = ASSERT_PTR(userdata); DHCP_CLIENT_DONT_DESTROY(client); int r; From 16d08897bbe1c6c9357bf1a845dd3efe2a5fd316 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 10 Mar 2026 08:57:15 +0900 Subject: [PATCH 03/13] sd-dhcp-client: open socket when necessary and close it when unnecessary To make gracefully ignore unexpected messages from outside at unexpected timing. This potentially reduces work load to handle such messages, and slightly reduces attack surface by malicious DHCP messages. This also makes the socket fd is owned by the relevant IO event source. Except for the performance optimization and security hardening, this should not change any behaviors. So, just refactoring. --- src/libsystemd-network/dhcp-client-internal.h | 12 +- src/libsystemd-network/dhcp-client-send.c | 187 ++++++++++++++++++ src/libsystemd-network/dhcp-client-send.h | 18 ++ src/libsystemd-network/fuzz-dhcp-client.c | 1 + src/libsystemd-network/meson.build | 1 + src/libsystemd-network/sd-dhcp-client.c | 151 ++------------ 6 files changed, 239 insertions(+), 131 deletions(-) create mode 100644 src/libsystemd-network/dhcp-client-send.c create mode 100644 src/libsystemd-network/dhcp-client-send.h diff --git a/src/libsystemd-network/dhcp-client-internal.h b/src/libsystemd-network/dhcp-client-internal.h index 07e61b0d76..b59f0e632d 100644 --- a/src/libsystemd-network/dhcp-client-internal.h +++ b/src/libsystemd-network/dhcp-client-internal.h @@ -38,7 +38,6 @@ struct sd_dhcp_client { sd_device *dev; - int fd; uint16_t port; uint16_t server_port; union sockaddr_union link; @@ -93,6 +92,17 @@ int dhcp_client_set_state_callback( void *userdata); int dhcp_client_get_state(sd_dhcp_client *client); +int client_receive_message_raw( + sd_event_source *s, + int fd, + uint32_t revents, + void *userdata); +int client_receive_message_udp( + sd_event_source *s, + int fd, + uint32_t revents, + void *userdata); + /* If we are invoking callbacks of a dhcp-client, ensure unreffing the * client from the callback doesn't destroy the object we are working * on */ diff --git a/src/libsystemd-network/dhcp-client-send.c b/src/libsystemd-network/dhcp-client-send.c new file mode 100644 index 0000000000..a802cb5e86 --- /dev/null +++ b/src/libsystemd-network/dhcp-client-send.c @@ -0,0 +1,187 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-event.h" + +#include "dhcp-client-internal.h" +#include "dhcp-client-send.h" +#include "dhcp-lease-internal.h" /* IWYU pragma: keep */ +#include "dhcp-network.h" +#include "dhcp-packet.h" +#include "fd-util.h" +#include "socket-util.h" + +static int client_get_socket(sd_dhcp_client *client, int domain) { + int r, d, fd; + + assert(client); + assert(IN_SET(domain, AF_PACKET, AF_INET)); + + if (!client->receive_message) + return -EBADF; + + fd = sd_event_source_get_io_fd(client->receive_message); + if (fd < 0) + return fd; + + r = getsockopt_int(fd, SOL_SOCKET, SO_DOMAIN, &d); + if (r < 0) + return r; + + if (d != domain) + return -EBADF; + + return fd; +} + +static int client_setup_io_event( + sd_dhcp_client *client, + int fd, + sd_event_io_handler_t callback, + const char *description) { + + int r; + + assert(client); + assert(fd >= 0); + assert(callback); + assert(description); + + _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; + r = sd_event_add_io(client->event, &s, fd, EPOLLIN, callback, client); + if (r < 0) + return r; + + r = sd_event_source_set_priority(s, client->event_priority); + if (r < 0) + return r; + + r = sd_event_source_set_description(s, description); + if (r < 0) + return r; + + r = sd_event_source_set_io_fd_own(s, true); + if (r < 0) + return r; + + sd_event_source_disable_unref(client->receive_message); + client->receive_message = TAKE_PTR(s); + return 0; +} + +int dhcp_client_send_raw( + sd_dhcp_client *client, + bool expect_reply, + DHCPPacket *packet, + size_t optoffset) { + + _cleanup_close_ int fd_close = -EBADF; + int r, fd; + + assert(client); + assert(packet); + + fd = client_get_socket(client, AF_PACKET); + if (fd < 0) { + fd = dhcp_network_bind_raw_socket( + client->ifindex, + &client->link, + client->xid, + &client->hw_addr, + &client->bcast_addr, + client->arp_type, + client->port, + client->socket_priority_set, + client->socket_priority); + if (fd < 0) + return fd; + + fd_close = fd; + } + + dhcp_packet_append_ip_headers( + packet, + INADDR_ANY, + client->port, + INADDR_BROADCAST, + client->server_port, + sizeof(DHCPPacket) + optoffset, + client->ip_service_type); + + r = dhcp_network_send_raw_socket( + fd, + &client->link, + packet, + sizeof(DHCPPacket) + optoffset); + if (r < 0) + return r; + + if (!expect_reply) { + /* We do not expect any replies, hence stop the IO event source if enabled. */ + client->receive_message = sd_event_source_disable_unref(client->receive_message); + return 0; + } + + if (fd_close < 0) + return 0; /* Already opened socket is reused. Not necessary to setup new IO event source. */ + + r = client_setup_io_event(client, fd, client_receive_message_raw, "dhcp4-receive-message-raw"); + if (r < 0) + return r; + + TAKE_FD(fd_close); + return 0; +} + +int dhcp_client_send_udp( + sd_dhcp_client *client, + bool expect_reply, + DHCPPacket *packet, + size_t optoffset) { + + _cleanup_close_ int fd_close = -EBADF; + int r, fd; + + assert(client); + assert(packet); + + if (!client->lease || client->lease->address == 0) + return -EADDRNOTAVAIL; + + fd = client_get_socket(client, AF_INET); + if (fd < 0) { + fd = dhcp_network_bind_udp_socket( + client->ifindex, + client->lease->address, + client->port, + client->ip_service_type); + if (fd < 0) + return fd; + + fd_close = fd; + } + + r = dhcp_network_send_udp_socket( + fd, + client->lease->server_address, + client->server_port, + &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + if (r < 0) + return r; + + if (!expect_reply) { + /* We do not expect any replies, hence stop the IO event source if enabled. */ + client->receive_message = sd_event_source_disable_unref(client->receive_message); + return 0; + } + + if (fd_close < 0) + return 0; /* Already opened socket is reused. Not necessary to setup new IO event source. */ + + r = client_setup_io_event(client, fd, client_receive_message_udp, "dhcp4-receive-message-udp"); + if (r < 0) + return r; + + TAKE_FD(fd_close); + return 0; +} diff --git a/src/libsystemd-network/dhcp-client-send.h b/src/libsystemd-network/dhcp-client-send.h new file mode 100644 index 0000000000..2dbb6a0878 --- /dev/null +++ b/src/libsystemd-network/dhcp-client-send.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-forward.h" + +#include "dhcp-protocol.h" + +int dhcp_client_send_raw( + sd_dhcp_client *client, + bool expect_reply, + DHCPPacket *packet, + size_t optoffset); + +int dhcp_client_send_udp( + sd_dhcp_client *client, + bool expect_reply, + DHCPPacket *packet, + size_t optoffset); diff --git a/src/libsystemd-network/fuzz-dhcp-client.c b/src/libsystemd-network/fuzz-dhcp-client.c index 7a59faff63..21f693c487 100644 --- a/src/libsystemd-network/fuzz-dhcp-client.c +++ b/src/libsystemd-network/fuzz-dhcp-client.c @@ -3,6 +3,7 @@ #include #include "dhcp-network.h" +#include "fd-util.h" #include "fuzz.h" #include "network-internal.h" #include "sd-dhcp-client.c" diff --git a/src/libsystemd-network/meson.build b/src/libsystemd-network/meson.build index 2eeabc075e..d1e13d99b5 100644 --- a/src/libsystemd-network/meson.build +++ b/src/libsystemd-network/meson.build @@ -2,6 +2,7 @@ libsystemd_network_sources = files( 'arp-util.c', + 'dhcp-client-send.c', 'dhcp-network.c', 'dhcp-option.c', 'dhcp-packet.c', diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 6bc96a47e7..88a831e9ce 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -10,14 +10,13 @@ #include "alloc-util.h" #include "device-util.h" #include "dhcp-client-internal.h" +#include "dhcp-client-send.h" #include "dhcp-lease-internal.h" -#include "dhcp-network.h" #include "dhcp-option.h" #include "dhcp-packet.h" #include "dns-domain.h" #include "errno-util.h" #include "event-util.h" -#include "fd-util.h" #include "hostname-util.h" #include "iovec-util.h" #include "memory-util.h" @@ -73,16 +72,6 @@ static const uint8_t default_req_opts_anonymize[] = { SD_DHCP_OPTION_PRIVATE_PROXY_AUTODISCOVERY, /* 252 */ }; -static int client_receive_message_raw( - sd_event_source *s, - int fd, - uint32_t revents, - void *userdata); -static int client_receive_message_udp( - sd_event_source *s, - int fd, - uint32_t revents, - void *userdata); static void client_stop(sd_dhcp_client *client, int error); static int client_restart(sd_dhcp_client *client); @@ -635,18 +624,22 @@ static int client_notify(sd_dhcp_client *client, int event) { return 0; } -static void client_initialize(sd_dhcp_client *client) { +static void client_disable_event_sources(sd_dhcp_client *client) { assert(client); client->receive_message = sd_event_source_disable_unref(client->receive_message); - client->fd = safe_close(client->fd); - (void) event_source_disable(client->timeout_resend); (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) { + assert(client); + + client_disable_event_sources(client); client->discover_attempt = 0; client->request_attempt = 0; @@ -896,18 +889,6 @@ static int client_append_fqdn_option( return r; } -static int dhcp_client_send_raw( - sd_dhcp_client *client, - DHCPPacket *packet, - size_t len) { - - dhcp_packet_append_ip_headers(packet, INADDR_ANY, client->port, - INADDR_BROADCAST, client->server_port, len, client->ip_service_type); - - return dhcp_network_send_raw_socket(client->fd, &client->link, - packet, len); -} - static int client_append_common_discover_request_options(sd_dhcp_client *client, DHCPPacket *packet, size_t *optoffset, size_t optlen) { sd_dhcp_option *j; int r; @@ -1022,7 +1003,7 @@ static int client_send_dhcp_discover(sd_dhcp_client *client) { if (r < 0) return r; - r = dhcp_client_send_raw(client, discover, sizeof(DHCPPacket) + optoffset); + r = dhcp_client_send_raw(client, /* expect_reply= */ true, discover, optoffset); if (r < 0) return r; @@ -1060,7 +1041,7 @@ static int client_send_bootp_discover(sd_dhcp_client *client) { optoffset = 60; } - r = dhcp_client_send_raw(client, discover, sizeof(DHCPPacket) + optoffset); + r = dhcp_client_send_raw(client, /* expect_reply= */ true, discover, optoffset); if (r < 0) return r; @@ -1153,13 +1134,9 @@ static int client_send_request(sd_dhcp_client *client) { return r; if (client->state == DHCP_STATE_RENEWING) - r = dhcp_network_send_udp_socket(client->fd, - client->lease->server_address, - client->server_port, - &request->dhcp, - sizeof(DHCPMessage) + optoffset); + r = dhcp_client_send_udp(client, /* expect_reply= */ true, request, optoffset); else - r = dhcp_client_send_raw(client, request, sizeof(DHCPPacket) + optoffset); + r = dhcp_client_send_raw(client, /* expect_reply= */ true, request, optoffset); if (r < 0) return r; @@ -1328,34 +1305,6 @@ error: return 0; } -static int client_initialize_io_events( - sd_dhcp_client *client, - sd_event_io_handler_t io_callback) { - - int r; - - assert(client); - assert(client->event); - assert(io_callback); - - _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; - r = sd_event_add_io(client->event, &s, client->fd, EPOLLIN, io_callback, client); - if (r < 0) - return r; - - r = sd_event_source_set_priority(s, client->event_priority); - if (r < 0) - return r; - - r = sd_event_source_set_description(s, "dhcp4-receive-message"); - if (r < 0) - return r; - - sd_event_source_disable_unref(client->receive_message); - client->receive_message = TAKE_PTR(s); - return 0; -} - static int client_initialize_time_events(sd_dhcp_client *client) { assert(client); assert(client->event); @@ -1375,45 +1324,20 @@ static int client_initialize_time_events(sd_dhcp_client *client) { /* force_reset= */ true); } -static int client_initialize_events(sd_dhcp_client *client, sd_event_io_handler_t io_callback) { - int r; - - assert(client); - assert(io_callback); - - r = client_initialize_io_events(client, io_callback); - if (r < 0) - return r; - - return client_initialize_time_events(client); -} - static int client_start_delayed(sd_dhcp_client *client) { - int r; - assert_return(client, -EINVAL); assert_return(client->event, -EINVAL); assert_return(client->ifindex > 0, -EINVAL); - assert_return(client->fd < 0, -EBUSY); assert_return(client->xid == 0, -EINVAL); assert_return(IN_SET(client->state, DHCP_STATE_STOPPED, DHCP_STATE_INIT_REBOOT), -EBUSY); client->xid = random_u32(); - - r = dhcp_network_bind_raw_socket(client->ifindex, &client->link, client->xid, - &client->hw_addr, &client->bcast_addr, - client->arp_type, client->port, - client->socket_priority_set, client->socket_priority); - if (r < 0) - return r; - client->fd = r; - client->start_time = now(CLOCK_BOOTTIME); if (client->state == DHCP_STATE_STOPPED) client->state = DHCP_STATE_INIT; - return client_initialize_events(client, client_receive_message_raw); + return client_initialize_time_events(client); } static int client_start(sd_dhcp_client *client) { @@ -1452,23 +1376,12 @@ static int client_timeout_t2(sd_event_source *s, uint64_t usec, void *userdata) int r; client->receive_message = sd_event_source_disable_unref(client->receive_message); - client->fd = safe_close(client->fd); client_set_state(client, DHCP_STATE_REBINDING); client->discover_attempt = 0; client->request_attempt = 0; - r = dhcp_network_bind_raw_socket(client->ifindex, &client->link, client->xid, - &client->hw_addr, &client->bcast_addr, - client->arp_type, client->port, - client->socket_priority_set, client->socket_priority); - if (r < 0) { - client_stop(client, r); - return 0; - } - client->fd = r; - - r = client_initialize_events(client, client_receive_message_raw); + r = client_initialize_time_events(client); if (r < 0) client_stop(client, r); @@ -1713,7 +1626,7 @@ static int client_enter_requesting(sd_dhcp_client *client) { assert(client); assert(client->lease); - (void) event_source_disable(client->timeout_resend); + client_disable_event_sources(client); if (client->lease->ipv6_only_preferred_usec > 0) { if (client->ipv6_acquired) { @@ -1905,23 +1818,7 @@ static int client_enter_bound_now(sd_dhcp_client *client, int notify_event) { if (r < 0) log_dhcp_client_errno(client, r, "could not set lease timeouts: %m"); - if (client->bootp) { - client->receive_message = sd_event_source_disable_unref(client->receive_message); - client->fd = safe_close(client->fd); - } else { - r = dhcp_network_bind_udp_socket(client->ifindex, client->lease->address, client->port, client->ip_service_type); - if (r < 0) - return log_dhcp_client_errno(client, r, "could not bind UDP socket: %m"); - - client->receive_message = sd_event_source_disable_unref(client->receive_message); - close_and_replace(client->fd, r); - r = client_initialize_io_events(client, client_receive_message_udp); - if (r < 0) - return r; - } - client_notify(client, notify_event); - return 0; } @@ -1941,8 +1838,9 @@ 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; - (void) event_source_disable(client->timeout_resend); /* RFC 8925 section 3.2 * If the client is in the INIT-REBOOT state, it SHOULD stop the DHCPv4 configuration process or @@ -2098,7 +1996,7 @@ static int client_handle_message(sd_dhcp_client *client, DHCPMessage *message, s return 0; } -static int client_receive_message_udp( +int client_receive_message_udp( sd_event_source *s, int fd, uint32_t revents, @@ -2151,7 +2049,7 @@ static int client_receive_message_udp( return 0; } -static int client_receive_message_raw( +int client_receive_message_raw( sd_event_source *s, int fd, uint32_t revents, @@ -2330,15 +2228,10 @@ static int client_send_release_or_decline(sd_dhcp_client *client, uint8_t type) switch (type) { case DHCP_RELEASE: - r = dhcp_network_send_udp_socket( - client->fd, - client->lease->server_address, - client->server_port, - &packet->dhcp, - sizeof(DHCPMessage) + optoffset); + r = dhcp_client_send_udp(client, /* expect_reply= */ false, packet, optoffset); break; case DHCP_DECLINE: - r = dhcp_client_send_raw(client, packet, sizeof(DHCPPacket) + optoffset); + r = dhcp_client_send_raw(client, /* expect_reply= */ false, packet, optoffset); break; default: assert_not_reached(); @@ -2401,7 +2294,6 @@ 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) { assert_return(client, -EINVAL); assert_return(sd_dhcp_client_is_running(client), -ESTALE); - assert_return(client->fd >= 0, -EINVAL); if (sd_event_source_get_enabled(client->timeout_ipv6_only_mode, NULL) <= 0) return 0; @@ -2497,7 +2389,6 @@ int sd_dhcp_client_new(sd_dhcp_client **ret, int anonymize) { .n_ref = 1, .state = DHCP_STATE_STOPPED, .ifindex = -1, - .fd = -EBADF, .mtu = DHCP_MIN_PACKET_SIZE, .port = DHCP_PORT_CLIENT, .server_port = DHCP_PORT_SERVER, From c32105081fc25dd6fdd9378e221479ac7509c779 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 13 Mar 2026 14:23:06 +0900 Subject: [PATCH 04/13] sd-dhcp-client: introduce client_send_discover() No functional change, just refactoring. --- src/libsystemd-network/sd-dhcp-client.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 88a831e9ce..720f23cba8 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -965,7 +965,6 @@ static int client_send_dhcp_discover(sd_dhcp_client *client) { int r; assert(client); - assert(IN_SET(client->state, DHCP_STATE_INIT, DHCP_STATE_SELECTING)); r = client_message_init(client, DHCP_DISCOVER, &discover, &optlen, &optoffset); if (r < 0) @@ -1008,7 +1007,6 @@ static int client_send_dhcp_discover(sd_dhcp_client *client) { return r; log_dhcp_client(client, "DISCOVER"); - return 0; } @@ -1018,7 +1016,6 @@ static int client_send_bootp_discover(sd_dhcp_client *client) { int r; assert(client); - assert(IN_SET(client->state, DHCP_STATE_INIT, DHCP_STATE_SELECTING)); r = client_message_init(client, DHCP_DISCOVER, &discover, &optlen, &optoffset); if (r < 0) @@ -1049,6 +1046,15 @@ static int client_send_bootp_discover(sd_dhcp_client *client) { return 0; } +static int client_send_discover(sd_dhcp_client *client) { + assert(client); + assert(IN_SET(client->state, DHCP_STATE_INIT, DHCP_STATE_SELECTING)); + + return client->bootp ? + client_send_bootp_discover(client) : + client_send_dhcp_discover(client); +} + static int client_send_request(sd_dhcp_client *client) { _cleanup_free_ DHCPPacket *request = NULL; size_t optoffset, optlen; @@ -1241,10 +1247,7 @@ static int client_timeout_resend( switch (client->state) { case DHCP_STATE_INIT: - if (client->bootp) - r = client_send_bootp_discover(client); - else - r = client_send_dhcp_discover(client); + r = client_send_discover(client); if (r >= 0) { client_set_state(client, DHCP_STATE_SELECTING); client->discover_attempt = 0; @@ -1253,10 +1256,7 @@ static int client_timeout_resend( break; case DHCP_STATE_SELECTING: - if (client->bootp) - r = client_send_bootp_discover(client); - else - r = client_send_dhcp_discover(client); + r = client_send_discover(client); if (r < 0 && client->discover_attempt >= client->max_discover_attempts) goto error; break; From ffafefbcceb8a4afada4cb1172b9057e1a7c56cc Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 14 Mar 2026 06:34:19 +0900 Subject: [PATCH 05/13] sd-dhcp-client: replace max_request_attempts with constant macro --- src/libsystemd-network/dhcp-client-internal.h | 1 - src/libsystemd-network/sd-dhcp-client.c | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libsystemd-network/dhcp-client-internal.h b/src/libsystemd-network/dhcp-client-internal.h index b59f0e632d..e08ea3deda 100644 --- a/src/libsystemd-network/dhcp-client-internal.h +++ b/src/libsystemd-network/dhcp-client-internal.h @@ -65,7 +65,6 @@ struct sd_dhcp_client { uint64_t discover_attempt; uint64_t request_attempt; uint64_t max_discover_attempts; - uint64_t max_request_attempts; OrderedHashmap *extra_options; OrderedHashmap *vendor_options; sd_event_source *timeout_t1; diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 720f23cba8..7cfebcb596 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -35,6 +35,7 @@ #define RESTART_AFTER_NAK_MIN_USEC (1 * USEC_PER_SEC) #define RESTART_AFTER_NAK_MAX_USEC (30 * USEC_PER_MINUTE) +#define MAX_REQUEST_ATTEMPTS 5 #define TRANSIENT_FAILURE_ATTEMPTS 3 /* Arbitrary limit: how many attempts are considered enough to report * transient failure. */ @@ -1222,7 +1223,7 @@ static int client_timeout_resend( break; case DHCP_STATE_REQUESTING: case DHCP_STATE_BOUND: - if (client->request_attempt >= client->max_request_attempts) + if (client->request_attempt >= MAX_REQUEST_ATTEMPTS) goto error; client->request_attempt++; @@ -1266,7 +1267,7 @@ static int client_timeout_resend( case DHCP_STATE_RENEWING: case DHCP_STATE_REBINDING: r = client_send_request(client); - if (r < 0 && client->request_attempt >= client->max_request_attempts) + if (r < 0 && client->request_attempt >= MAX_REQUEST_ATTEMPTS) goto error; if (client->state == DHCP_STATE_INIT_REBOOT) @@ -1292,7 +1293,7 @@ error: /* Avoid REQUEST infinite loop. Per RFC 2131 section 3.1.5: if the client receives neither a DHCPACK or a DHCPNAK message after employing the retransmission algorithm, the client reverts to INIT state and restarts the initialization process */ - if (client->request_attempt >= client->max_request_attempts) { + if (client->request_attempt >= MAX_REQUEST_ATTEMPTS) { log_dhcp_client(client, "Max REQUEST attempts reached. Restarting..."); r = client_restart(client); if (r >= 0) @@ -2394,7 +2395,6 @@ int sd_dhcp_client_new(sd_dhcp_client **ret, int anonymize) { .server_port = DHCP_PORT_SERVER, .anonymize = !!anonymize, .max_discover_attempts = UINT64_MAX, - .max_request_attempts = 5, .ip_service_type = -1, }; /* NOTE: this could be moved to a function. */ From 0b3cbfd1bc247c7323ba0286b6df8961a68f2fbd Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 13 Mar 2026 14:33:42 +0900 Subject: [PATCH 06/13] sd-dhcp-client: enter the SELECTING state before sending DHCPDISCOVER Similarly, enter the REBOOTING state before sending DHCPREQUEST on reboot. Also, this makes DHCPREQUEST message is sent several times also in REBOOTING state. Previously, we wait about 4 seconds after DHCPREQUEST on reboot, and entered the init state if no response. Now we wait 1 second after the first DHCPREQUEST, resend another DHCPREQUEST, wait 2 seconds, then enter the init state if no response. So, even in the worst case, we have slight speed up. --- src/libsystemd-network/sd-dhcp-client.c | 113 ++++++++++-------------- 1 file changed, 48 insertions(+), 65 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 7cfebcb596..f037f80cdf 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -35,6 +35,7 @@ #define RESTART_AFTER_NAK_MIN_USEC (1 * USEC_PER_SEC) #define RESTART_AFTER_NAK_MAX_USEC (30 * USEC_PER_MINUTE) +#define MAX_REQUEST_ATTEMPTS_ON_REBOOTING 2 #define MAX_REQUEST_ATTEMPTS 5 #define TRANSIENT_FAILURE_ATTEMPTS 3 /* Arbitrary limit: how many attempts are considered enough to report * transient failure. */ @@ -1049,7 +1050,7 @@ static int client_send_bootp_discover(sd_dhcp_client *client) { static int client_send_discover(sd_dhcp_client *client) { assert(client); - assert(IN_SET(client->state, DHCP_STATE_INIT, DHCP_STATE_SELECTING)); + assert(client->state == DHCP_STATE_SELECTING); return client->bootp ? client_send_bootp_discover(client) : @@ -1090,10 +1091,9 @@ static int client_send_request(sd_dhcp_client *client) { 4, &client->lease->address); if (r < 0) return r; - break; - case DHCP_STATE_INIT_REBOOT: + case DHCP_STATE_REBOOTING: /* ’server identifier’ MUST NOT be filled in, ’requested IP address’ option MUST be filled in with client’s notion of its previously assigned address. ’ciaddr’ MUST be zero. @@ -1119,16 +1119,10 @@ static int client_send_request(sd_dhcp_client *client) { This message MUST be broadcast to the 0xffffffff IP broadcast address. */ request->dhcp.ciaddr = client->lease->address; - break; - case DHCP_STATE_INIT: - case DHCP_STATE_SELECTING: - case DHCP_STATE_REBOOTING: - case DHCP_STATE_BOUND: - case DHCP_STATE_STOPPED: default: - return -EINVAL; + assert_not_reached(); } r = client_append_common_discover_request_options(client, request, &optoffset, optlen); @@ -1153,8 +1147,8 @@ static int client_send_request(sd_dhcp_client *client) { log_dhcp_client(client, "REQUEST (requesting)"); break; - case DHCP_STATE_INIT_REBOOT: - log_dhcp_client(client, "REQUEST (init-reboot)"); + case DHCP_STATE_REBOOTING: + log_dhcp_client(client, "REQUEST (rebooting)"); break; case DHCP_STATE_RENEWING: @@ -1166,14 +1160,12 @@ static int client_send_request(sd_dhcp_client *client) { break; default: - log_dhcp_client(client, "REQUEST (invalid)"); + assert_not_reached(); } return 0; } -static int client_start(sd_dhcp_client *client); - static int client_timeout_resend( sd_event_source *s, uint64_t usec, @@ -1201,39 +1193,43 @@ static int client_timeout_resend( next_timeout = client_compute_reacquisition_timeout(time_now, client->expire_time); break; - case DHCP_STATE_REBOOTING: - /* start over as we did not receive a timely ack or nak */ - client_initialize(client); - - r = client_start(client); - if (r < 0) - goto error; - - log_dhcp_client(client, "REBOOTED"); - return 0; - case DHCP_STATE_INIT: - case DHCP_STATE_INIT_REBOOT: + client_set_state(client, DHCP_STATE_SELECTING); + _fallthrough_; + case DHCP_STATE_SELECTING: - if (client->discover_attempt >= client->max_discover_attempts) + if (client->discover_attempt >= client->max_discover_attempts) { + r = -ETIMEDOUT; goto error; + } client->discover_attempt++; next_timeout = client_compute_request_timeout(client->discover_attempt); break; + + case DHCP_STATE_INIT_REBOOT: + client_set_state(client, DHCP_STATE_REBOOTING); + _fallthrough_; + + case DHCP_STATE_REBOOTING: + /* There is nothing explicitly mentioned about retry interval on reboot. Let's reuse the same + * algorithm as in the requesting state below, but slightly speed up for faster reboot. */ + + if (client->request_attempt >= MAX_REQUEST_ATTEMPTS_ON_REBOOTING) + goto restart; + + client->request_attempt++; + next_timeout = client_compute_request_timeout(client->request_attempt) / 4; + break; + case DHCP_STATE_REQUESTING: - case DHCP_STATE_BOUND: if (client->request_attempt >= MAX_REQUEST_ATTEMPTS) - goto error; + goto restart; client->request_attempt++; next_timeout = client_compute_request_timeout(client->request_attempt); break; - case DHCP_STATE_STOPPED: - r = -EINVAL; - goto error; - default: assert_not_reached(); } @@ -1247,58 +1243,45 @@ static int client_timeout_resend( goto error; switch (client->state) { - case DHCP_STATE_INIT: - r = client_send_discover(client); - if (r >= 0) { - client_set_state(client, DHCP_STATE_SELECTING); - client->discover_attempt = 0; - } else if (client->discover_attempt >= client->max_discover_attempts) - goto error; - break; - case DHCP_STATE_SELECTING: r = client_send_discover(client); if (r < 0 && client->discover_attempt >= client->max_discover_attempts) goto error; + + if (client->discover_attempt >= TRANSIENT_FAILURE_ATTEMPTS) + client_notify(client, SD_DHCP_CLIENT_EVENT_TRANSIENT_FAILURE); + break; + + case DHCP_STATE_REBOOTING: + r = client_send_request(client); + if (r < 0 && client->request_attempt >= MAX_REQUEST_ATTEMPTS_ON_REBOOTING) + goto restart; break; - case DHCP_STATE_INIT_REBOOT: case DHCP_STATE_REQUESTING: case DHCP_STATE_RENEWING: case DHCP_STATE_REBINDING: r = client_send_request(client); if (r < 0 && client->request_attempt >= MAX_REQUEST_ATTEMPTS) - goto error; - - if (client->state == DHCP_STATE_INIT_REBOOT) - client_set_state(client, DHCP_STATE_REBOOTING); + goto restart; break; - case DHCP_STATE_REBOOTING: - case DHCP_STATE_BOUND: - break; - - case DHCP_STATE_STOPPED: default: - r = -EINVAL; - goto error; + assert_not_reached(); } - if (client->discover_attempt >= TRANSIENT_FAILURE_ATTEMPTS) - client_notify(client, SD_DHCP_CLIENT_EVENT_TRANSIENT_FAILURE); - return 0; -error: +restart: /* Avoid REQUEST infinite loop. Per RFC 2131 section 3.1.5: if the client receives neither a DHCPACK or a DHCPNAK message after employing the retransmission algorithm, the client reverts to INIT state and restarts the initialization process */ - if (client->request_attempt >= MAX_REQUEST_ATTEMPTS) { - log_dhcp_client(client, "Max REQUEST attempts reached. Restarting..."); - r = client_restart(client); - if (r >= 0) - return 0; - } + log_dhcp_client(client, "Max REQUEST attempts reached. Restarting..."); + r = client_restart(client); + if (r >= 0) + return 0; + +error: client_stop(client, r); /* Errors were dealt with when stopping the client, don't spill From ffc3d5f812e899cf32d2dc610988cabb2f3f5810 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 14 Mar 2026 01:15:22 +0900 Subject: [PATCH 07/13] sd-dhcp-client: initialize event source and so on in client_start_delayed() When we start the client, any previous state/configuration should be cleaned. Let's effectively do the same thing as client_initialize() in that function. This also several assertions in client_start_delayed() to sd_dhcp_client_start(). These kind of checks should be done earlier. --- src/libsystemd-network/sd-dhcp-client.c | 35 ++++++++++--------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index f037f80cdf..f301018d4f 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -1309,17 +1309,17 @@ static int client_initialize_time_events(sd_dhcp_client *client) { } static int client_start_delayed(sd_dhcp_client *client) { - assert_return(client, -EINVAL); - assert_return(client->event, -EINVAL); - assert_return(client->ifindex > 0, -EINVAL); - assert_return(client->xid == 0, -EINVAL); - assert_return(IN_SET(client->state, DHCP_STATE_STOPPED, DHCP_STATE_INIT_REBOOT), -EBUSY); + assert(client); + DHCP_CLIENT_DONT_DESTROY(client); + + client_disable_event_sources(client); + client->lease = sd_dhcp_lease_unref(client->lease); client->xid = random_u32(); client->start_time = now(CLOCK_BOOTTIME); - if (client->state == DHCP_STATE_STOPPED) - client->state = DHCP_STATE_INIT; + if (client->state != DHCP_STATE_INIT_REBOOT) + client_set_state(client, DHCP_STATE_INIT); return client_initialize_time_events(client); } @@ -1340,16 +1340,12 @@ static int client_timeout_expire(sd_event_source *s, uint64_t usec, void *userda client_notify(client, SD_DHCP_CLIENT_EVENT_EXPIRED); - /* lease was lost, start over if not freed or stopped in callback */ - if (client->state != DHCP_STATE_STOPPED) { - client_initialize(client); + if (client->state == DHCP_STATE_STOPPED) + return 0; /* The notify callback stopped the client. */ - r = client_start(client); - if (r < 0) { - client_stop(client, r); - return 0; - } - } + r = client_start(client); + if (r < 0) + client_stop(client, r); return 0; } @@ -1861,8 +1857,6 @@ static int client_restart(sd_dhcp_client *client) { client_notify(client, SD_DHCP_CLIENT_EVENT_EXPIRED); - client_initialize(client); - r = client_start_delayed(client); if (r < 0) return r; @@ -2120,12 +2114,12 @@ int sd_dhcp_client_start(sd_dhcp_client *client) { int r; assert_return(client, -EINVAL); + 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; - client_initialize(client); - /* 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); @@ -2282,7 +2276,6 @@ int sd_dhcp_client_interrupt_ipv6_only_mode(sd_dhcp_client *client) { if (sd_event_source_get_enabled(client->timeout_ipv6_only_mode, NULL) <= 0) return 0; - client_initialize(client); return client_start(client); } From f0b9b679ffb30de3d2b3ef3623912a65ac157b67 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 14 Mar 2026 01:26:45 +0900 Subject: [PATCH 08/13] sd-dhcp-client: simply enter renewing/rebinding state send DHCPREQUEST on T1/T2 It is not necessary to enable another timer event source to send DHCPREQUEST from the T1/T2 timer event source. Just call the callback function for sending message. Also, T1 hits only we have a bound lease. Drop spurious conditions. --- src/libsystemd-network/sd-dhcp-client.c | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index f301018d4f..7a08861eb8 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -1353,38 +1353,28 @@ static int client_timeout_expire(sd_event_source *s, uint64_t usec, void *userda static int client_timeout_t2(sd_event_source *s, uint64_t usec, void *userdata) { sd_dhcp_client *client = ASSERT_PTR(userdata); DHCP_CLIENT_DONT_DESTROY(client); - int r; + /* Explicitly close the unicast socket opened during renewing. On success path, the socket will be + * closed anyway on sending broadcast DHCPREQUEST, but let's explicitly close it here for failure + * path to ignore all unicast replies from now on. */ client->receive_message = sd_event_source_disable_unref(client->receive_message); client_set_state(client, DHCP_STATE_REBINDING); client->discover_attempt = 0; client->request_attempt = 0; - r = client_initialize_time_events(client); - if (r < 0) - client_stop(client, r); - - return 0; + return client_timeout_resend(s, usec, userdata); } static int client_timeout_t1(sd_event_source *s, uint64_t usec, void *userdata) { sd_dhcp_client *client = ASSERT_PTR(userdata); DHCP_CLIENT_DONT_DESTROY(client); - int r; - if (client->lease) - client_set_state(client, DHCP_STATE_RENEWING); - else if (client->state != DHCP_STATE_INIT) - client_set_state(client, DHCP_STATE_INIT_REBOOT); + client_set_state(client, DHCP_STATE_RENEWING); client->discover_attempt = 0; client->request_attempt = 0; - r = client_initialize_time_events(client); - if (r < 0) - client_stop(client, r); - - return 0; + return client_timeout_resend(s, usec, userdata); } static int dhcp_option_parse_and_verify( From bb5b2f9e5a340407394226a60af21658e88c64a2 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 15 Apr 2026 08:47:42 +0900 Subject: [PATCH 09/13] sd-dhcp-client: simplify the implementation of IPv6 Only mode support 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 | 1 - src/libsystemd-network/sd-dhcp-client.c | 169 +++++++----------- src/network/networkd-dhcp4.c | 28 +-- src/systemd/sd-dhcp-client.h | 2 +- 4 files changed, 79 insertions(+), 121 deletions(-) diff --git a/src/libsystemd-network/dhcp-client-internal.h b/src/libsystemd-network/dhcp-client-internal.h index e08ea3deda..fab4ff24aa 100644 --- a/src/libsystemd-network/dhcp-client-internal.h +++ b/src/libsystemd-network/dhcp-client-internal.h @@ -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; diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 7a08861eb8..76558bdd72 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -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,30 +2176,51 @@ 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; + + 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 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; - - return client_start(client); -} - int sd_dhcp_client_attach_event(sd_dhcp_client *client, sd_event *event, int64_t priority) { int r; @@ -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); diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c index f274a0c4d9..dc6b78e326 100644 --- a/src/network/networkd-dhcp4.c +++ b/src/network/networkd-dhcp4.c @@ -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); diff --git a/src/systemd/sd-dhcp-client.h b/src/systemd/sd-dhcp-client.h index 033d5ad894..378271aef8 100644 --- a/src/systemd/sd-dhcp-client.h +++ b/src/systemd/sd-dhcp-client.h @@ -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); From b831cd00e6777b32b08cc1848b5ff9c6c981f128 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sun, 19 Apr 2026 08:37:53 +0900 Subject: [PATCH 10/13] sd-dhcp-client: propagate failure in setting timer and stop the client If we fail to setup timer event sources about the lease lifetime or T1/T2, then the lease will be never updated, and the user (networkd) will not receive any notification about the expire. The situation is terrible. Let's stop the client with error code earlier, and notify the failure to networkd. --- src/libsystemd-network/sd-dhcp-client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 76558bdd72..f6ab4b34df 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -1769,7 +1769,7 @@ static int client_enter_bound(sd_dhcp_client *client, int notify_event) { r = client_set_lease_timeouts(client); if (r < 0) - log_dhcp_client_errno(client, r, "Failed to set lease timeouts: %m"); + return log_dhcp_client_errno(client, r, "Failed to set lease timeouts: %m"); client_notify(client, notify_event); return 0; From 2dff63a31ce33e4525f68351b47516a69b27334c Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 14 Mar 2026 01:33:54 +0900 Subject: [PATCH 11/13] sd-dhcp-client: notify SD_DHCP_CLIENT_EVENT_EXPIRED only when we already have a bound lease Otherwise, if we emit the notification without a valid bound lease, networkd may be confused (of course should not, but for safety). Also, increment the delay before calling client_start_delayed(). Otherwise, the first reboot is done instantaneously. --- src/libsystemd-network/sd-dhcp-client.c | 45 +++++++++++++++---------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index f6ab4b34df..6ff6329ccf 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -1328,6 +1328,34 @@ static int client_start(sd_dhcp_client *client) { return client_start_delayed(client); } +static int client_restart(sd_dhcp_client *client) { + assert(client); + DHCP_CLIENT_DONT_DESTROY(client); + + /* This is called when we receive a DHCPNAK or could not receive any replies. */ + + /* First, if we have a bound lease, then notify it is expired. */ + if (IN_SET(client->state, DHCP_STATE_BOUND, DHCP_STATE_RENEWING, DHCP_STATE_REBINDING)) { + client_notify(client, SD_DHCP_CLIENT_EVENT_EXPIRED); + + if (client->state == DHCP_STATE_STOPPED) + return 0; /* The notify callback stopped the client. */ + } + + /* On reboot, DHCPNAK or no reply suggests that the network is changed or the address is already + * used by another host. Let's restart the client immediately without any delay to speed up the + * reboot process. */ + if (client->state == DHCP_STATE_REBOOTING) + return client_start(client); + + /* Otherwise, we should restart the client with a short delay. */ + client->start_delay = CLAMP(client->start_delay * 2, + RESTART_AFTER_NAK_MIN_USEC, RESTART_AFTER_NAK_MAX_USEC); + + log_dhcp_client(client, "REBOOT in %s", FORMAT_TIMESPAN(client->start_delay, USEC_PER_SEC)); + return client_start_delayed(client); +} + static int client_timeout_expire(sd_event_source *s, uint64_t usec, void *userdata) { sd_dhcp_client *client = userdata; DHCP_CLIENT_DONT_DESTROY(client); @@ -1775,23 +1803,6 @@ static int client_enter_bound(sd_dhcp_client *client, int notify_event) { return 0; } -static int client_restart(sd_dhcp_client *client) { - int r; - assert(client); - - client_notify(client, SD_DHCP_CLIENT_EVENT_EXPIRED); - - r = client_start_delayed(client); - if (r < 0) - return r; - - log_dhcp_client(client, "REBOOT in %s", FORMAT_TIMESPAN(client->start_delay, USEC_PER_SEC)); - - client->start_delay = CLAMP(client->start_delay * 2, - RESTART_AFTER_NAK_MIN_USEC, RESTART_AFTER_NAK_MAX_USEC); - return 0; -} - static int client_verify_message_header(sd_dhcp_client *client, DHCPMessage *message, size_t len) { const uint8_t *expected_chaddr = NULL; uint8_t expected_hlen = 0; From 6ee065ed9980056264369c6da8d3401f9b583e17 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 14 Mar 2026 16:45:53 +0900 Subject: [PATCH 12/13] sd-dhcp-client: rework discover/request_attempts counter discover_attempts should be reset only when - the client is stopped, to make the counter starts from zero on the next invocation. - we acquire a bound lease, to make the counter starts from zero when the lease is expired. request_attempts should be reset only when the client enter a new state that sends DHCPREQUEST, that is, when enter one of the REBOOTING, REQUESTING, RENEWING, and REBINDING state. This moves resetting counter to client_set_state() as it should happen only when the state transition. --- src/libsystemd-network/sd-dhcp-client.c | 37 +++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 6ff6329ccf..ad7714d596 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -607,6 +607,28 @@ static void client_set_state(sd_dhcp_client *client, DHCPState state) { client->state = state; + switch (state) { + case DHCP_STATE_STOPPED: + case DHCP_STATE_BOUND: + /* In these cases, the next DHCPDISCOVER message will be sent in a new cycle. + * Hence, clear the counter for DHCPDISCOVER messages. */ + client->discover_attempt = 0; + break; + + case DHCP_STATE_REBOOTING: + case DHCP_STATE_REQUESTING: + case DHCP_STATE_RENEWING: + case DHCP_STATE_REBINDING: + /* In these cases, the next DHCPREQUEST message will be the first message in this new state. + * Hence, clear the counter for DHCPREQUEST messages. */ + client->request_attempt = 0; + break; + + default: + /* otherwise, do not reset the counters. */ + ; + } + if (client->state_callback) client->state_callback(client, state, client->state_userdata); } @@ -642,9 +664,6 @@ static void client_initialize(sd_dhcp_client *client) { client_disable_event_sources(client); - client->discover_attempt = 0; - client->request_attempt = 0; - client_set_state(client, DHCP_STATE_STOPPED); client->xid = 0; @@ -1385,8 +1404,6 @@ static int client_timeout_t2(sd_event_source *s, uint64_t usec, void *userdata) client->receive_message = sd_event_source_disable_unref(client->receive_message); client_set_state(client, DHCP_STATE_REBINDING); - client->discover_attempt = 0; - client->request_attempt = 0; return client_timeout_resend(s, usec, userdata); } @@ -1396,8 +1413,6 @@ static int client_timeout_t1(sd_event_source *s, uint64_t usec, void *userdata) DHCP_CLIENT_DONT_DESTROY(client); client_set_state(client, DHCP_STATE_RENEWING); - client->discover_attempt = 0; - client->request_attempt = 0; return client_timeout_resend(s, usec, userdata); } @@ -1598,8 +1613,6 @@ static int client_enter_requesting(sd_dhcp_client *client) { client_disable_event_sources(client); 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) { @@ -1790,8 +1803,6 @@ static int client_enter_bound(sd_dhcp_client *client, int notify_event) { client->start_delay = 0; client_set_state(client, DHCP_STATE_BOUND); - client->discover_attempt = 0; - client->request_attempt = 0; client->last_addr = client->lease->address; @@ -2030,11 +2041,9 @@ int sd_dhcp_client_send_renew(sd_dhcp_client *client) { if (!sd_dhcp_client_is_running(client) || client->state != DHCP_STATE_BOUND || client->bootp) return 0; /* do nothing */ - client->start_delay = 0; - client->discover_attempt = 1; - client->request_attempt = 1; client_set_state(client, DHCP_STATE_RENEWING); + client->start_delay = 0; return client_initialize_time_events(client); } From e06115cb8b372d7e20e600a7c917d30c48c349fd Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 16 Apr 2026 04:07:25 +0900 Subject: [PATCH 13/13] sd-dhcp-client: add FIXME comment about the state callback At least currently, it is a theoretical concern, as networkd does not change the client state in the callback. --- src/libsystemd-network/sd-dhcp-client.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index ad7714d596..9742ab833b 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -629,6 +629,8 @@ static void client_set_state(sd_dhcp_client *client, DHCPState state) { ; } + // FIXME: If the state callback changes the state, we may not safely free/stop the client, and the + // state machine diagram becomes needlessly complicated. Introduce a guard to avoid that. */ if (client->state_callback) client->state_callback(client, state, client->state_userdata); }