diff --git a/src/libsystemd-network/dhcp-client-internal.h b/src/libsystemd-network/dhcp-client-internal.h index f75ca71b43..fab4ff24aa 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,64 @@ 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; + + 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; + OrderedHashmap *extra_options; + OrderedHashmap *vendor_options; + sd_event_source *timeout_t1; + sd_event_source *timeout_t2; + sd_event_source *timeout_expire; + 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, @@ -30,6 +90,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 19f85e09df..9742ab833b 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -7,28 +7,22 @@ #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-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 "ether-addr-util.h" #include "event-util.h" -#include "fd-util.h" #include "hostname-util.h" #include "iovec-util.h" #include "memory-util.h" #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" @@ -41,71 +35,11 @@ #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. */ -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, @@ -140,16 +74,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); @@ -683,6 +607,30 @@ 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. */ + ; + } + + // 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); } @@ -702,21 +650,21 @@ 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); +} - client->discover_attempt = 0; - client->request_attempt = 0; +static void client_initialize(sd_dhcp_client *client) { + assert(client); + + client_disable_event_sources(client); client_set_state(client, DHCP_STATE_STOPPED); client->xid = 0; @@ -963,18 +911,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; @@ -1051,7 +987,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) @@ -1089,12 +1024,11 @@ 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; log_dhcp_client(client, "DISCOVER"); - return 0; } @@ -1104,7 +1038,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) @@ -1127,7 +1060,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; @@ -1135,6 +1068,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(client->state == 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; @@ -1169,10 +1111,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. @@ -1198,16 +1139,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); @@ -1220,13 +1155,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; @@ -1236,8 +1167,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: @@ -1249,14 +1180,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, @@ -1284,39 +1213,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 >= client->max_request_attempts) - goto error; + if (client->request_attempt >= MAX_REQUEST_ATTEMPTS) + 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(); } @@ -1330,64 +1263,45 @@ static int client_timeout_resend( goto error; switch (client->state) { - case DHCP_STATE_INIT: - if (client->bootp) - r = client_send_bootp_discover(client); - else - r = client_send_dhcp_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: - 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; + + 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 >= client->max_request_attempts) - goto error; - - if (client->state == DHCP_STATE_INIT_REBOOT) - client_set_state(client, DHCP_STATE_REBOOTING); + if (r < 0 && client->request_attempt >= MAX_REQUEST_ATTEMPTS) + 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 >= client->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 @@ -1395,40 +1309,10 @@ 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); - (void) event_source_disable(client->timeout_ipv6_only_mode); - return event_reset_time_relative( client->event, &client->timeout_resend, @@ -1442,45 +1326,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(client); + DHCP_CLIENT_DONT_DESTROY(client); - 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_disable_event_sources(client); + client->lease = sd_dhcp_lease_unref(client->lease); 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; + if (client->state != DHCP_STATE_INIT_REBOOT) + client_set_state(client, 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) { @@ -1490,6 +1349,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); @@ -1499,16 +1386,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; } @@ -1516,49 +1399,24 @@ 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->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); - 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 = 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->discover_attempt = 0; - client->request_attempt = 0; + client_set_state(client, DHCP_STATE_RENEWING); - 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( @@ -1750,39 +1608,15 @@ 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); - (void) event_source_disable(client->timeout_resend); + client_disable_event_sources(client); - if (client->lease->ipv6_only_preferred_usec > 0) { + client_set_state(client, DHCP_STATE_REQUESTING); + + 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."); @@ -1792,16 +1626,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) { @@ -1954,108 +1791,28 @@ 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; client->last_addr = client->lease->address; r = client_set_lease_timeouts(client); 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; - } + return 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->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 - * 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); - - client_notify(client, SD_DHCP_CLIENT_EVENT_EXPIRED); - - client_initialize(client); - - 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; } @@ -2165,7 +1922,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, @@ -2218,7 +1975,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, @@ -2286,11 +2043,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); } @@ -2305,11 +2060,8 @@ int sd_dhcp_client_start(sd_dhcp_client *client) { int r; assert_return(client, -EINVAL); - - /* Note, do not reset the flag in client_initialize(), as it is also called on expire. */ - client->ipv6_acquired = false; - - client_initialize(client); + assert_return(client->event, -EINVAL); + assert_return(client->ifindex > 0, -EINVAL); /* If no client identifier exists, construct an RFC 4361-compliant one */ if (!sd_dhcp_client_id_is_set(&client->client_id)) { @@ -2397,15 +2149,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(); @@ -2451,32 +2198,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); - assert_return(client->fd >= 0, -EINVAL); - - if (sd_event_source_get_enabled(client->timeout_ipv6_only_mode, NULL) <= 0) - return 0; - - client_initialize(client); - return client_start(client); -} - int sd_dhcp_client_attach_event(sd_dhcp_client *client, sd_event *event, int64_t priority) { int r; @@ -2530,7 +2296,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); @@ -2564,13 +2329,11 @@ 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, .anonymize = !!anonymize, .max_discover_attempts = UINT64_MAX, - .max_request_attempts = 5, .ip_service_type = -1, }; /* NOTE: this could be moved to a function. */ 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);