From 0e07870f5944f4d4a6fda79e2bd6086f91a0b5a8 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 4 May 2026 20:42:27 +0900 Subject: [PATCH 01/10] test-dhcp-server: several cleanups No effective change, just rafactoring. --- src/libsystemd-network/test-dhcp-server.c | 79 +++++++---------------- 1 file changed, 23 insertions(+), 56 deletions(-) diff --git a/src/libsystemd-network/test-dhcp-server.c b/src/libsystemd-network/test-dhcp-server.c index 34263f044d..bbfb4f1c05 100644 --- a/src/libsystemd-network/test-dhcp-server.c +++ b/src/libsystemd-network/test-dhcp-server.c @@ -11,20 +11,7 @@ #include "dhcp-server-internal.h" #include "tests.h" -static void test_pool(struct in_addr *address, unsigned size, int ret) { - _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; - - ASSERT_OK(sd_dhcp_server_new(&server, 1)); - - if (ret >= 0) - ASSERT_RETURN_IS_CRITICAL(true, ASSERT_OK_EQ(sd_dhcp_server_configure_pool(server, address, 8, 0, size), ret)); - else - ASSERT_RETURN_IS_CRITICAL(false, ASSERT_ERROR(sd_dhcp_server_configure_pool(server, address, 8, 0, size), -ret)); -} - -static int test_basic(void) { - _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; - _cleanup_(sd_event_unrefp) sd_event *event = NULL; +TEST(basic) { struct in_addr address_lo = { .s_addr = htobe32(INADDR_LOOPBACK), }; @@ -33,21 +20,21 @@ static int test_basic(void) { }; int r; - log_debug("/* %s */", __func__); - + _cleanup_(sd_event_unrefp) sd_event *event = NULL; ASSERT_OK(sd_event_new(&event)); /* attach to loopback interface */ + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; ASSERT_OK(sd_dhcp_server_new(&server, 1)); ASSERT_NOT_NULL(server); - ASSERT_OK(sd_dhcp_server_attach_event(server, event, 0)); - ASSERT_RETURN_EXPECTED(ASSERT_ERROR(sd_dhcp_server_attach_event(server, event, 0), EBUSY)); - ASSERT_TRUE(sd_dhcp_server_get_event(server) == event); /* ASSERT_EQ() doesn't work here. */ + ASSERT_OK(sd_dhcp_server_attach_event(server, event, SD_EVENT_PRIORITY_NORMAL)); + ASSERT_RETURN_EXPECTED(ASSERT_ERROR(sd_dhcp_server_attach_event(server, event, SD_EVENT_PRIORITY_NORMAL), EBUSY)); + ASSERT_PTR_EQ(sd_dhcp_server_get_event(server), event); ASSERT_OK(sd_dhcp_server_detach_event(server)); ASSERT_NULL(sd_dhcp_server_get_event(server)); - ASSERT_OK(sd_dhcp_server_attach_event(server, NULL, 0)); - ASSERT_RETURN_EXPECTED(ASSERT_ERROR(sd_dhcp_server_attach_event(server, NULL, 0), EBUSY)); + ASSERT_OK(sd_dhcp_server_attach_event(server, NULL, SD_EVENT_PRIORITY_NORMAL)); + ASSERT_RETURN_EXPECTED(ASSERT_ERROR(sd_dhcp_server_attach_event(server, NULL, SD_EVENT_PRIORITY_NORMAL), EBUSY)); ASSERT_TRUE(sd_dhcp_server_ref(server) == server); ASSERT_NULL(sd_dhcp_server_unref(server)); @@ -58,27 +45,23 @@ static int test_basic(void) { ASSERT_RETURN_EXPECTED(ASSERT_ERROR(sd_dhcp_server_configure_pool(server, &address_lo, 38, 0, 0), ERANGE)); ASSERT_OK(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 0)); ASSERT_OK(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 0)); - - test_pool(&address_any, 1, -EINVAL); - test_pool(&address_lo, 1, 0); + ASSERT_RETURN_EXPECTED(ASSERT_ERROR(sd_dhcp_server_configure_pool(server, &address_any, 8, 0, 1), EINVAL)); + ASSERT_OK(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 1)); r = sd_dhcp_server_start(server); /* skip test if running in an environment with no full networking support, CONFIG_PACKET not * compiled in kernel, nor af_packet module available. */ if (IN_SET(r, -EPERM, -EAFNOSUPPORT)) - return r; + return (void) log_tests_skipped_errno(r, "cannot start dhcp server"); ASSERT_OK(r); ASSERT_OK(sd_dhcp_server_start(server)); ASSERT_OK(sd_dhcp_server_stop(server)); ASSERT_OK(sd_dhcp_server_stop(server)); ASSERT_OK(sd_dhcp_server_start(server)); - - return 0; } -static void test_message_handler(void) { - _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; +TEST(dhcp_server_handle_message) { struct { DHCPMessageHeader header; struct { @@ -130,8 +113,10 @@ static void test_message_handler(void) { static uint8_t static_lease_client_id[7] = {0x01, 'A', 'B', 'C', 'D', 'E', 'G' }; int r; - log_debug("/* %s */", __func__); + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + ASSERT_OK(sd_event_new(&event)); + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; ASSERT_OK(sd_dhcp_server_new(&server, 1)); ASSERT_OK(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 0)); ASSERT_OK(sd_dhcp_server_set_static_lease( @@ -140,8 +125,11 @@ static void test_message_handler(void) { static_lease_client_id, ELEMENTSOF(static_lease_client_id), /* hostname= */ NULL)); - ASSERT_OK(sd_dhcp_server_attach_event(server, NULL, 0)); - ASSERT_OK(sd_dhcp_server_start(server)); + ASSERT_OK(sd_dhcp_server_attach_event(server, event, SD_EVENT_PRIORITY_NORMAL)); + r = sd_dhcp_server_start(server); + if (IN_SET(r, -EPERM, -EAFNOSUPPORT)) + return (void) log_tests_skipped_errno(r, "cannot start dhcp server"); + ASSERT_OK(r); r = dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL); if (r == -ENETDOWN) @@ -282,11 +270,8 @@ static void test_message_handler(void) { ASSERT_OK_EQ(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL), DHCP_ACK); } -static void test_static_lease(void) { +TEST(sd_dhcp_server_set_static_lease) { _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; - - log_debug("/* %s */", __func__); - ASSERT_OK(sd_dhcp_server_new(&server, 1)); ASSERT_OK(sd_dhcp_server_set_static_lease( @@ -363,11 +348,8 @@ static void test_static_lease(void) { /* hostname= */ NULL)); } -static void test_domain_name(void) { +TEST(sd_dhcp_server_set_domain_name) { _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; - - log_debug("/* %s */", __func__); - ASSERT_OK(sd_dhcp_server_new(&server, 1)); /* Test setting domain name */ @@ -401,19 +383,4 @@ static void test_domain_name(void) { ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, "local")); } -int main(int argc, char *argv[]) { - int r; - - test_setup_logging(LOG_DEBUG); - - test_static_lease(); - test_domain_name(); - - r = test_basic(); - if (r < 0) - return log_tests_skipped_errno(r, "cannot start dhcp server"); - - test_message_handler(); - - return 0; -} +DEFINE_TEST_MAIN(LOG_DEBUG); From 3f8d0004e9467daeed21b392bfabd53e65912191 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 4 May 2026 13:24:33 +0900 Subject: [PATCH 02/10] sd-dhcp-server: split into small pieaces No functional change, just several functions are moved/split/renamed. --- src/libsystemd-network/dhcp-server-internal.h | 30 +- .../dhcp-server-lease-internal.h | 1 + src/libsystemd-network/dhcp-server-request.c | 536 +++++++++ src/libsystemd-network/dhcp-server-request.h | 31 + src/libsystemd-network/dhcp-server-send.c | 552 +++++++++ src/libsystemd-network/dhcp-server-send.h | 14 + src/libsystemd-network/meson.build | 2 + src/libsystemd-network/sd-dhcp-server.c | 1016 +---------------- src/libsystemd-network/test-dhcp-server.c | 1 + 9 files changed, 1147 insertions(+), 1036 deletions(-) create mode 100644 src/libsystemd-network/dhcp-server-request.c create mode 100644 src/libsystemd-network/dhcp-server-request.h create mode 100644 src/libsystemd-network/dhcp-server-send.c create mode 100644 src/libsystemd-network/dhcp-server-send.h diff --git a/src/libsystemd-network/dhcp-server-internal.h b/src/libsystemd-network/dhcp-server-internal.h index d4caae66d4..c03ce7dddb 100644 --- a/src/libsystemd-network/dhcp-server-internal.h +++ b/src/libsystemd-network/dhcp-server-internal.h @@ -5,13 +5,11 @@ Copyright © 2013 Intel Corporation. All rights reserved. ***/ -#include "sd-dhcp-lease.h" #include "sd-dhcp-server.h" -#include "dhcp-client-id-internal.h" #include "dhcp-option.h" -#include "sd-forward.h" #include "network-common.h" +#include "sd-forward.h" #include "sparse-endian.h" #include "tlv-util.h" @@ -74,32 +72,12 @@ typedef struct sd_dhcp_server { char *lease_file; } sd_dhcp_server; -typedef struct DHCPRequest { - /* received message */ - DHCPMessage *message; - - /* options */ - sd_dhcp_client_id client_id; - size_t max_optlen; - be32_t server_id; - be32_t requested_ip; - usec_t lifetime; - const uint8_t *agent_info_option; - char *hostname; - const uint8_t *parameter_request_list; - size_t parameter_request_list_len; - bool rapid_commit; - triple_timestamp timestamp; -} DHCPRequest; - int dhcp_server_set_extra_options(sd_dhcp_server *server, TLV *options); int dhcp_server_set_vendor_options(sd_dhcp_server *server, TLV *options); -int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, - size_t length, const triple_timestamp *timestamp); -int dhcp_server_send_packet(sd_dhcp_server *server, - DHCPRequest *req, DHCPPacket *packet, - int type, size_t optoffset); +void dhcp_server_on_lease_change(sd_dhcp_server *server); +bool dhcp_server_address_is_in_pool(sd_dhcp_server *server, be32_t address); +bool dhcp_server_address_available(sd_dhcp_server *server, be32_t address); #define log_dhcp_server_errno(server, error, fmt, ...) \ log_interface_prefix_full_errno( \ diff --git a/src/libsystemd-network/dhcp-server-lease-internal.h b/src/libsystemd-network/dhcp-server-lease-internal.h index aeb099ff81..6d7eb4c213 100644 --- a/src/libsystemd-network/dhcp-server-lease-internal.h +++ b/src/libsystemd-network/dhcp-server-lease-internal.h @@ -5,6 +5,7 @@ #include "dhcp-client-id-internal.h" #include "dhcp-server-internal.h" +#include "dhcp-server-request.h" #include "sd-forward.h" typedef struct sd_dhcp_server_lease { diff --git a/src/libsystemd-network/dhcp-server-request.c b/src/libsystemd-network/dhcp-server-request.c new file mode 100644 index 0000000000..5cccc7c2f6 --- /dev/null +++ b/src/libsystemd-network/dhcp-server-request.c @@ -0,0 +1,536 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-event.h" + +#include "alloc-util.h" +#include "dhcp-network.h" +#include "dhcp-protocol.h" +#include "dhcp-server-internal.h" +#include "dhcp-server-lease-internal.h" +#include "dhcp-server-request.h" +#include "dhcp-server-send.h" +#include "errno-util.h" +#include "hashmap.h" +#include "iovec-util.h" +#include "memory-util.h" +#include "siphash24.h" +#include "socket-util.h" +#include "string-util.h" +#include "unaligned.h" + +static DHCPRequest* dhcp_request_free(DHCPRequest *req) { + if (!req) + return NULL; + + free(req->hostname); + return mfree(req); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(DHCPRequest*, dhcp_request_free); + +static void dhcp_request_set_timestamp(DHCPRequest *req, const triple_timestamp *timestamp) { + assert(req); + + if (timestamp && triple_timestamp_is_set(timestamp)) + req->timestamp = *timestamp; + else + triple_timestamp_now(&req->timestamp); +} + +int dhcp_request_get_lifetime_timestamp(DHCPRequest *req, clockid_t clock, usec_t *ret) { + assert(req); + assert(TRIPLE_TIMESTAMP_HAS_CLOCK(clock)); + assert(clock_supported(clock)); + assert(ret); + + if (req->lifetime <= 0) + return -ENODATA; + + if (!triple_timestamp_is_set(&req->timestamp)) + return -ENODATA; + + *ret = usec_add(triple_timestamp_by_clock(&req->timestamp, clock), req->lifetime); + return 0; +} + +static int ensure_sane_request(sd_dhcp_server *server, DHCPRequest *req, DHCPMessage *message) { + int r; + + assert(req); + assert(message); + + req->message = message; + + if (message->hlen > sizeof(message->chaddr)) + return -EBADMSG; + + /* set client id based on MAC address if client did not send an explicit one */ + if (!sd_dhcp_client_id_is_set(&req->client_id)) { + if (!client_id_data_size_is_valid(message->hlen)) + return -EBADMSG; + + r = sd_dhcp_client_id_set(&req->client_id, /* type= */ 1, message->chaddr, message->hlen); + if (r < 0) + return r; + } + + if (message->hlen == 0 || memeqzero(message->chaddr, message->hlen)) { + uint8_t type; + const void *data; + size_t size; + + /* See RFC2131 section 4.1.1. + * hlen and chaddr may not be set for non-ethernet interface. + * Let's try to retrieve it from the client ID. */ + + if (!sd_dhcp_client_id_is_set(&req->client_id)) + return -EBADMSG; + + r = sd_dhcp_client_id_get(&req->client_id, &type, &data, &size); + if (r < 0) + return r; + + if (type != 1) + return -EBADMSG; + + if (size > sizeof(message->chaddr)) + return -EBADMSG; + + memcpy(message->chaddr, data, size); + message->hlen = size; + } + + if (req->max_optlen < DHCP_MIN_OPTIONS_SIZE) + req->max_optlen = DHCP_MIN_OPTIONS_SIZE; + + if (req->lifetime <= 0) + req->lifetime = MAX(USEC_PER_SEC, server->default_lease_time); + + if (server->max_lease_time > 0 && req->lifetime > server->max_lease_time) + req->lifetime = server->max_lease_time; + + return 0; +} + +static int parse_request(uint8_t code, uint8_t len, const void *option, void *userdata) { + DHCPRequest *req = ASSERT_PTR(userdata); + int r; + + switch (code) { + case SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME: + if (len == 4) + req->lifetime = unaligned_be32_sec_to_usec(option, /* max_as_infinity= */ true); + + break; + case SD_DHCP_OPTION_REQUESTED_IP_ADDRESS: + if (len == 4) + memcpy(&req->requested_ip, option, sizeof(be32_t)); + + break; + case SD_DHCP_OPTION_SERVER_IDENTIFIER: + if (len == 4) + memcpy(&req->server_id, option, sizeof(be32_t)); + + break; + case SD_DHCP_OPTION_CLIENT_IDENTIFIER: + if (client_id_size_is_valid(len)) + (void) sd_dhcp_client_id_set_raw(&req->client_id, option, len); + + break; + case SD_DHCP_OPTION_MAXIMUM_MESSAGE_SIZE: + + if (len == 2 && unaligned_read_be16(option) >= sizeof(DHCPPacket)) + req->max_optlen = unaligned_read_be16(option) - sizeof(DHCPPacket); + + break; + case SD_DHCP_OPTION_RELAY_AGENT_INFORMATION: + req->agent_info_option = (uint8_t*)option - 2; + + break; + case SD_DHCP_OPTION_HOST_NAME: { + _cleanup_free_ char *p = NULL; + + r = dhcp_option_parse_hostname(option, len, &p); + if (r < 0) + log_debug_errno(r, "Failed to parse hostname, ignoring: %m"); + else + free_and_replace(req->hostname, p); + break; + } + case SD_DHCP_OPTION_PARAMETER_REQUEST_LIST: + req->parameter_request_list = option; + req->parameter_request_list_len = len; + break; + + case SD_DHCP_OPTION_RAPID_COMMIT: + req->rapid_commit = true; + break; + } + + return 0; +} + +static int dhcp_server_parse_message(sd_dhcp_server *server, DHCPMessage *message, size_t length, DHCPRequest **ret, char **ret_error_message) { + int r; + + assert(server); + assert(message); + assert(ret); + assert(ret_error_message); + + _cleanup_(dhcp_request_freep) DHCPRequest *req = new0(DHCPRequest, 1); + if (!req) + return -ENOMEM; + + _cleanup_free_ char *error_message = NULL; + r = dhcp_option_parse(message, length, parse_request, req, &error_message); + if (r < 0) + return r; + int type = r; + + r = ensure_sane_request(server, req, message); + if (r < 0) + return r; + + *ret = TAKE_PTR(req); + *ret_error_message = TAKE_PTR(error_message); + return type; +} + +static int dhcp_server_ack(sd_dhcp_server *server, DHCPRequest *req, be32_t address) { + usec_t expiration; + int r; + + assert(server); + assert(req); + assert(address != 0); + + r = dhcp_request_get_lifetime_timestamp(req, CLOCK_BOOTTIME, &expiration); + if (r < 0) + return r; + + r = dhcp_server_set_lease(server, address, req, expiration); + if (r < 0) + return log_dhcp_server_errno(server, r, "Failed to create new lease: %m"); + + r = server_send_offer_or_ack(server, req, address, DHCP_ACK); + if (r < 0) + return log_dhcp_server_errno(server, r, "Could not send ACK: %m"); + + log_dhcp_server(server, "ACK (0x%x)", be32toh(req->message->xid)); + + dhcp_server_on_lease_change(server); + + return DHCP_ACK; +} + +static int dhcp_server_process_discover(sd_dhcp_server *server, DHCPRequest *req) { + int r; + + assert(server); + assert(req); + + sd_dhcp_server_lease + *existing_lease = hashmap_get(server->bound_leases_by_client_id, &req->client_id), + *static_lease = dhcp_server_get_static_lease(server, req); + + log_dhcp_server(server, "DISCOVER (0x%x)", be32toh(req->message->xid)); + + if (server->pool_size == 0) + /* no pool allocated */ + return 0; + + be32_t address = INADDR_ANY; + + /* for now pick a random free address from the pool */ + if (static_lease) { + sd_dhcp_server_lease *l = hashmap_get(server->bound_leases_by_address, UINT32_TO_PTR(static_lease->address)); + if (l && l != existing_lease) + /* The address is already assigned to another host. Refusing. */ + return 0; + + /* Found a matching static lease. */ + address = static_lease->address; + + } else if (existing_lease && dhcp_server_address_is_in_pool(server, existing_lease->address)) + + /* If we previously assigned an address to the host, then reuse it. */ + address = existing_lease->address; + + else { + struct siphash state; + uint64_t hash; + + /* Even with no persistence of leases, we try to offer the same client the same IP address. + * We do this by using the hash of the client ID as the offset into the pool of leases when + * finding the next free one. */ + +#define HASH_KEY SD_ID128_MAKE(0d,1d,fe,bd,f1,24,bd,b3,47,f1,dd,6e,73,21,93,30) + + siphash24_init(&state, HASH_KEY.bytes); + client_id_hash_func(&req->client_id, &state); + hash = htole64(siphash24_finalize(&state)); + + for (unsigned i = 0; i < server->pool_size; i++) { + be32_t a = server->subnet | htobe32(server->pool_offset + (hash + i) % server->pool_size); + if (dhcp_server_address_available(server, a)) { + address = a; + break; + } + } + } + + if (address == INADDR_ANY) + /* no free addresses left */ + return 0; + + if (server->rapid_commit && req->rapid_commit) + return dhcp_server_ack(server, req, address); + + r = server_send_offer_or_ack(server, req, address, DHCP_OFFER); + if (r < 0) + /* this only fails on critical errors */ + return log_dhcp_server_errno(server, r, "Could not send offer: %m"); + + log_dhcp_server(server, "OFFER (0x%x)", be32toh(req->message->xid)); + return DHCP_OFFER; +} + +static int dhcp_server_process_request(sd_dhcp_server *server, DHCPRequest *req) { + assert(server); + assert(req); + + sd_dhcp_server_lease + *existing_lease = hashmap_get(server->bound_leases_by_client_id, &req->client_id), + *static_lease = dhcp_server_get_static_lease(server, req); + + be32_t address; + bool init_reboot = false; + + /* see RFC 2131, section 4.3.2 */ + + if (req->server_id != 0) { + log_dhcp_server(server, "REQUEST (selecting) (0x%x)", + be32toh(req->message->xid)); + + /* SELECTING */ + if (req->server_id != server->address) + /* client did not pick us */ + return 0; + + if (req->message->ciaddr != 0) + /* this MUST be zero */ + return 0; + + if (req->requested_ip == 0) + /* this must be filled in with the yiaddr + from the chosen OFFER */ + return 0; + + address = req->requested_ip; + } else if (req->requested_ip != 0) { + log_dhcp_server(server, "REQUEST (init-reboot) (0x%x)", + be32toh(req->message->xid)); + + /* INIT-REBOOT */ + if (req->message->ciaddr != 0) + /* this MUST be zero */ + return 0; + + /* TODO: check more carefully if IP is correct */ + address = req->requested_ip; + init_reboot = true; + } else { + log_dhcp_server(server, "REQUEST (rebinding/renewing) (0x%x)", + be32toh(req->message->xid)); + + /* REBINDING / RENEWING */ + if (req->message->ciaddr == 0) + /* this MUST be filled in with clients IP address */ + return 0; + + address = req->message->ciaddr; + } + + /* Silently ignore Rapid Commit option in REQUEST message. */ + req->rapid_commit = false; + + if (static_lease) { + if (static_lease->address != address) + /* The client requested an address which is different from the static lease. Refusing. */ + return server_send_nak_or_ignore(server, init_reboot, req); + + sd_dhcp_server_lease *l = hashmap_get(server->bound_leases_by_address, UINT32_TO_PTR(address)); + if (l && l != existing_lease) + /* The requested address is already assigned to another host. Refusing. */ + return server_send_nak_or_ignore(server, init_reboot, req); + + /* Found a static lease for the client ID. */ + return dhcp_server_ack(server, req, address); + } + + if (dhcp_server_address_is_in_pool(server, address)) + /* The requested address is in the pool. */ + return dhcp_server_ack(server, req, address); + + /* Refuse otherwise. */ + return server_send_nak_or_ignore(server, init_reboot, req); +} + +static int dhcp_server_process_decline(sd_dhcp_server *server, DHCPRequest *req, const char *error_message) { + assert(server); + assert(req); + + log_dhcp_server(server, "DECLINE (0x%x): %s", be32toh(req->message->xid), strna(error_message)); + + /* TODO: make sure we don't offer this address again */ + + return 0; +} + +static int dhcp_server_process_release(sd_dhcp_server *server, DHCPRequest *req) { + assert(server); + assert(req); + + log_dhcp_server(server, "RELEASE (0x%x)", + be32toh(req->message->xid)); + + sd_dhcp_server_lease *existing_lease = hashmap_get(server->bound_leases_by_client_id, &req->client_id); + if (!existing_lease) + return 0; + + if (existing_lease->address != req->message->ciaddr) + return 0; + + sd_dhcp_server_lease_unref(existing_lease); + dhcp_server_on_lease_change(server); + + return 0; +} + +int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, size_t length, const triple_timestamp *timestamp) { + int r; + + assert(server); + assert(message); + + if (message->op != BOOTREQUEST) + return 0; + + _cleanup_(dhcp_request_freep) DHCPRequest *req = NULL; + _cleanup_free_ char *error_message = NULL; + r = dhcp_server_parse_message(server, message, length, &req, &error_message); + if (r < 0) + return r; + int type = r; + + dhcp_request_set_timestamp(req, timestamp); + + r = dhcp_server_cleanup_expired_leases(server); + if (r < 0) + return r; + + switch (type) { + case DHCP_DISCOVER: + return dhcp_server_process_discover(server, req); + case DHCP_REQUEST: + return dhcp_server_process_request(server, req); + case DHCP_DECLINE: + return dhcp_server_process_decline(server, req, error_message); + case DHCP_RELEASE: + return dhcp_server_process_release(server, req); + default: + return 0; /* Unsupported DHCP message type? Ignore the message silently. */ + } +} + +static int server_receive_message(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_free_ DHCPMessage *message = NULL; + /* This needs to be initialized with zero. See #20741. + * The issue is fixed on glibc-2.35 (8fba672472ae0055387e9315fc2eddfa6775ca79). */ + CMSG_BUFFER_TYPE(CMSG_SPACE_TIMEVAL + + CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; + sd_dhcp_server *server = ASSERT_PTR(userdata); + struct iovec iov = {}; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + ssize_t datagram_size, len; + int r; + + datagram_size = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(datagram_size) || ERRNO_IS_NEG_DISCONNECT(datagram_size)) + return 0; + if (datagram_size < 0) { + log_dhcp_server_errno(server, datagram_size, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + size_t buflen = datagram_size; + message = malloc0(buflen); + if (!message) + return -ENOMEM; + + iov = IOVEC_MAKE(message, datagram_size); + + len = recvmsg_safe(fd, &msg, 0); + if (ERRNO_IS_NEG_TRANSIENT(len) || ERRNO_IS_NEG_DISCONNECT(len)) + return 0; + if (len < 0) { + log_dhcp_server_errno(server, len, "Could not receive message, ignoring: %m"); + return 0; + } + + if ((size_t) len < sizeof(DHCPMessage)) + return 0; + + /* TODO: figure out if this can be done as a filter on the socket, like for IPv6 */ + struct in_pktinfo *info = CMSG_FIND_DATA(&msg, IPPROTO_IP, IP_PKTINFO, struct in_pktinfo); + if (info && info->ipi_ifindex != server->ifindex) + return 0; + + r = dhcp_server_handle_message(server, message, (size_t) len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg)); + if (r < 0) + log_dhcp_server_errno(server, r, "Couldn't process incoming message, ignoring: %m"); + + return 0; +} + +int dhcp_server_setup_io_event_source(sd_dhcp_server *server) { + int r; + + assert(server); + assert(server->event); + + r = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (r < 0) { + r = -errno; + goto on_error; + } + server->fd_raw = r; + + r = dhcp_network_bind_udp_socket(server->ifindex, INADDR_ANY, DHCP_PORT_SERVER, -1); + if (r < 0) + goto on_error; + server->fd = r; + + r = sd_event_add_io(server->event, &server->receive_message, + server->fd, EPOLLIN, + server_receive_message, server); + if (r < 0) + goto on_error; + + r = sd_event_source_set_priority(server->receive_message, + server->event_priority); + if (r < 0) + goto on_error; + + return 0; + + on_error: + sd_dhcp_server_stop(server); + return r; +} diff --git a/src/libsystemd-network/dhcp-server-request.h b/src/libsystemd-network/dhcp-server-request.h new file mode 100644 index 0000000000..f1e827a0e1 --- /dev/null +++ b/src/libsystemd-network/dhcp-server-request.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "dhcp-client-id-internal.h" +#include "dhcp-protocol.h" +#include "sd-forward.h" +#include "sparse-endian.h" +#include "time-util.h" + +typedef struct DHCPRequest { + /* received message */ + DHCPMessage *message; + triple_timestamp timestamp; + + /* options */ + sd_dhcp_client_id client_id; + size_t max_optlen; + be32_t server_id; + be32_t requested_ip; + usec_t lifetime; + const uint8_t *agent_info_option; + char *hostname; + const uint8_t *parameter_request_list; + size_t parameter_request_list_len; + bool rapid_commit; +} DHCPRequest; + +int dhcp_request_get_lifetime_timestamp(DHCPRequest *req, clockid_t clock, usec_t *ret); + +int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, size_t length, const triple_timestamp *timestamp); +int dhcp_server_setup_io_event_source(sd_dhcp_server *server); diff --git a/src/libsystemd-network/dhcp-server-send.c b/src/libsystemd-network/dhcp-server-send.c new file mode 100644 index 0000000000..67fc390bca --- /dev/null +++ b/src/libsystemd-network/dhcp-server-send.c @@ -0,0 +1,552 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "dhcp-network.h" +#include "dhcp-option.h" +#include "dhcp-packet.h" +#include "dhcp-server-lease-internal.h" +#include "dhcp-server-send.h" +#include "dns-domain.h" +#include "errno-util.h" +#include "hashmap.h" +#include "in-addr-util.h" +#include "iovec-util.h" +#include "iovec-wrapper.h" +#include "socket-util.h" + +static int dhcp_server_send_unicast_raw( + sd_dhcp_server *server, + uint8_t hlen, + const uint8_t *chaddr, + DHCPPacket *packet, + size_t len) { + + union sockaddr_union link = { + .ll.sll_family = AF_PACKET, + .ll.sll_protocol = htobe16(ETH_P_IP), + .ll.sll_ifindex = server->ifindex, + .ll.sll_halen = hlen, + }; + int r; + + assert(server); + assert(server->ifindex > 0); + assert(server->address != 0); + assert(hlen > 0); + assert(chaddr); + assert(packet); + assert(len > sizeof(DHCPPacket)); + + memcpy(link.ll.sll_addr, chaddr, hlen); + + if (len > UINT16_MAX) + return -EOVERFLOW; + + r = dhcp_packet_append_ip_headers( + packet, + server->address, + DHCP_PORT_SERVER, + packet->dhcp.yiaddr, + DHCP_PORT_CLIENT, + len, + /* ip_service_type= */ -1); + if (r < 0) + return r; + + return dhcp_network_send_raw_socket( + server->fd_raw, + &link, + &(struct iovec_wrapper) { + .iovec = &IOVEC_MAKE(packet, len), + .count = 1, + }); +} + +static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination, + uint16_t destination_port, + DHCPMessage *message, size_t len) { + union sockaddr_union dest = { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(destination_port), + .in.sin_addr.s_addr = destination, + }; + struct iovec iov = { + .iov_base = message, + .iov_len = len, + }; + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; + struct msghdr msg = { + .msg_name = &dest, + .msg_namelen = sizeof(dest.in), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + struct cmsghdr *cmsg; + struct in_pktinfo *pktinfo; + + assert(server); + assert(server->fd >= 0); + assert(message); + assert(len >= sizeof(DHCPMessage)); + + msg.msg_control = &control; + msg.msg_controllen = sizeof(control); + + cmsg = CMSG_FIRSTHDR(&msg); + assert(cmsg); + + cmsg->cmsg_level = IPPROTO_IP; + cmsg->cmsg_type = IP_PKTINFO; + cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); + + pktinfo = CMSG_TYPED_DATA(cmsg, struct in_pktinfo); + assert(pktinfo); + + pktinfo->ipi_ifindex = server->ifindex; + pktinfo->ipi_spec_dst.s_addr = server->address; + + if (sendmsg(server->fd, &msg, 0) < 0) + return -errno; + + return 0; +} + +static bool requested_broadcast(DHCPMessage *message) { + assert(message); + return message->flags & htobe16(0x8000); +} + +static int dhcp_server_send( + sd_dhcp_server *server, + uint8_t hlen, + const uint8_t *chaddr, + be32_t destination, + uint16_t destination_port, + DHCPPacket *packet, + size_t optoffset, + bool l2_broadcast) { + + if (destination != INADDR_ANY) + return dhcp_server_send_udp(server, destination, + destination_port, &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + else if (l2_broadcast) + return dhcp_server_send_udp(server, INADDR_BROADCAST, + destination_port, &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + else + /* we cannot send UDP packet to specific MAC address when the + address is not yet configured, so must fall back to raw + packets */ + return dhcp_server_send_unicast_raw(server, hlen, chaddr, packet, + sizeof(DHCPPacket) + optoffset); +} + +static int dhcp_server_send_packet(sd_dhcp_server *server, + DHCPRequest *req, DHCPPacket *packet, + int type, size_t optoffset) { + be32_t destination = INADDR_ANY; + uint16_t destination_port = DHCP_PORT_CLIENT; + int r; + + assert(server); + assert(req); + assert(req->max_optlen > 0); + assert(req->message); + assert(optoffset <= req->max_optlen); + assert(packet); + + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0, + SD_DHCP_OPTION_SERVER_IDENTIFIER, + 4, &server->address); + if (r < 0) + return r; + + if (req->agent_info_option) { + size_t opt_full_length = *(req->agent_info_option + 1) + 2; + /* there must be space left for SD_DHCP_OPTION_END */ + if (optoffset + opt_full_length < req->max_optlen) { + memcpy(packet->dhcp.options + optoffset, req->agent_info_option, opt_full_length); + optoffset += opt_full_length; + } + } + + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0, + SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + /* RFC 2131 Section 4.1 + + If the ’giaddr’ field in a DHCP message from a client is non-zero, + the server sends any return messages to the ’DHCP server’ port on the + BOOTP relay agent whose address appears in ’giaddr’. If the ’giaddr’ + field is zero and the ’ciaddr’ field is nonzero, then the server + unicasts DHCPOFFER and DHCPACK messages to the address in ’ciaddr’. + If ’giaddr’ is zero and ’ciaddr’ is zero, and the broadcast bit is + set, then the server broadcasts DHCPOFFER and DHCPACK messages to + 0xffffffff. If the broadcast bit is not set and ’giaddr’ is zero and + ’ciaddr’ is zero, then the server unicasts DHCPOFFER and DHCPACK + messages to the client’s hardware address and ’yiaddr’ address. In + all cases, when ’giaddr’ is zero, the server broadcasts any DHCPNAK + messages to 0xffffffff. + + Section 4.3.2 + + If ’giaddr’ is set in the DHCPREQUEST message, the client is on a + different subnet. The server MUST set the broadcast bit in the + DHCPNAK, so that the relay agent will broadcast the DHCPNAK to the + client, because the client may not have a correct network address + or subnet mask, and the client may not be answering ARP requests. + */ + if (req->message->giaddr != 0) { + destination = req->message->giaddr; + destination_port = DHCP_PORT_SERVER; + if (type == DHCP_NAK) + packet->dhcp.flags = htobe16(0x8000); + } else if (req->message->ciaddr != 0 && type != DHCP_NAK) + destination = req->message->ciaddr; + + bool l2_broadcast = requested_broadcast(req->message) || type == DHCP_NAK; + return dhcp_server_send(server, req->message->hlen, req->message->chaddr, + destination, destination_port, packet, optoffset, l2_broadcast); +} + +static int server_message_init( + sd_dhcp_server *server, + DHCPPacket **ret, + uint8_t type, + size_t *ret_optoffset, + DHCPRequest *req) { + + _cleanup_free_ DHCPPacket *packet = NULL; + size_t optoffset = 0; + int r; + + assert(server); + assert(ret); + assert(ret_optoffset); + assert(IN_SET(type, DHCP_OFFER, DHCP_ACK, DHCP_NAK)); + assert(req); + + packet = malloc0(sizeof(DHCPPacket) + req->max_optlen); + if (!packet) + return -ENOMEM; + + r = dhcp_message_init(&packet->dhcp, BOOTREPLY, + be32toh(req->message->xid), + req->message->htype, req->message->hlen, req->message->chaddr, + type, req->max_optlen, &optoffset); + if (r < 0) + return r; + + packet->dhcp.flags = req->message->flags; + packet->dhcp.giaddr = req->message->giaddr; + + *ret_optoffset = optoffset; + *ret = TAKE_PTR(packet); + + return 0; +} + +static int dhcp_server_append_static_hostname( + sd_dhcp_server *server, + DHCPPacket *packet, + size_t *offset, + DHCPRequest *req) { + + sd_dhcp_server_lease *static_lease; + int r; + + assert(server); + assert(packet); + assert(offset); + assert(req); + + static_lease = dhcp_server_get_static_lease(server, req); + if (!static_lease || !static_lease->hostname) + return 0; + + if (dns_name_is_single_label(static_lease->hostname)) + /* Option 12 */ + return dhcp_option_append( + &packet->dhcp, + req->max_optlen, + offset, + /* overload= */ 0, + SD_DHCP_OPTION_HOST_NAME, + strlen(static_lease->hostname), + static_lease->hostname); + + + /* Option 81 */ + uint8_t buffer[DHCP_MAX_FQDN_LENGTH + 3]; + + /* Flags: S=0 (will not update RR), O=1 (are overriding client), + * E=1 (using DNS wire format), N=1 (will not update DNS) */ + buffer[0] = DHCP_FQDN_FLAG_O | DHCP_FQDN_FLAG_E | DHCP_FQDN_FLAG_N; + + /* RFC 4702: A server SHOULD set these to 255 when sending the option and MUST ignore them on + * receipt. */ + buffer[1] = 255; + buffer[2] = 255; + + r = dns_name_to_wire_format(static_lease->hostname, buffer + 3, sizeof(buffer) - 3, false); + if (r < 0) + return log_dhcp_server_errno(server, r, "Failed to encode FQDN for static lease: %m"); + if (r > DHCP_MAX_FQDN_LENGTH) + return log_dhcp_server_errno(server, SYNTHETIC_ERRNO(EINVAL), "FQDN for static lease too long"); + + return dhcp_option_append( + &packet->dhcp, + req->max_optlen, + offset, + /* overload= */ 0, + SD_DHCP_OPTION_FQDN, + 3 + r, + buffer); +} + +static bool dhcp_request_contains(DHCPRequest *req, uint8_t option) { + assert(req); + + if (!req->parameter_request_list) + return false; + + return memchr(req->parameter_request_list, option, req->parameter_request_list_len); +} + +int server_send_offer_or_ack( + sd_dhcp_server *server, + DHCPRequest *req, + be32_t address, + uint8_t type) { + + static const uint8_t option_map[_SD_DHCP_LEASE_SERVER_TYPE_MAX] = { + [SD_DHCP_LEASE_DNS] = SD_DHCP_OPTION_DOMAIN_NAME_SERVER, + [SD_DHCP_LEASE_NTP] = SD_DHCP_OPTION_NTP_SERVER, + [SD_DHCP_LEASE_SIP] = SD_DHCP_OPTION_SIP_SERVER, + [SD_DHCP_LEASE_POP3] = SD_DHCP_OPTION_POP3_SERVER, + [SD_DHCP_LEASE_SMTP] = SD_DHCP_OPTION_SMTP_SERVER, + [SD_DHCP_LEASE_LPR] = SD_DHCP_OPTION_LPR_SERVER, + }; + + _cleanup_free_ DHCPPacket *packet = NULL; + be32_t lease_time; + size_t offset; + int r; + + assert(server); + assert(req); + assert(IN_SET(type, DHCP_OFFER, DHCP_ACK)); + + r = server_message_init(server, &packet, type, &offset, req); + if (r < 0) + return r; + + packet->dhcp.yiaddr = address; + packet->dhcp.siaddr = server->boot_server_address.s_addr; + + lease_time = usec_to_be32_sec(req->lifetime); + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME, 4, + &lease_time); + if (r < 0) + return r; + + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_SUBNET_MASK, 4, &server->netmask); + if (r < 0) + return r; + + if (server->emit_router) { + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_ROUTER, 4, + in4_addr_is_set(&server->router_address) ? + &server->router_address.s_addr : + &server->address); + if (r < 0) + return r; + } + + if (server->boot_server_name) { + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_BOOT_SERVER_NAME, + strlen(server->boot_server_name), server->boot_server_name); + if (r < 0) + return r; + } + + if (server->boot_filename) { + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_BOOT_FILENAME, + strlen(server->boot_filename), server->boot_filename); + if (r < 0) + return r; + } + + for (sd_dhcp_lease_server_type_t k = 0; k < _SD_DHCP_LEASE_SERVER_TYPE_MAX; k++) { + if (server->servers[k].size <= 0) + continue; + + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + option_map[k], + sizeof(struct in_addr) * server->servers[k].size, + server->servers[k].addr); + if (r < 0) + return r; + } + + if (server->timezone) { + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_TZDB_TIMEZONE, + strlen(server->timezone), server->timezone); + if (r < 0) + return r; + } + + if (server->domain_name) { + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_DOMAIN_NAME, + strlen(server->domain_name), server->domain_name); + if (r < 0) + return r; + } + + /* RFC 8925 section 3.3. DHCPv4 Server Behavior + * The server MUST NOT include the IPv6-Only Preferred option in the DHCPOFFER or DHCPACK message if + * the option was not present in the Parameter Request List sent by the client. */ + if (dhcp_request_contains(req, SD_DHCP_OPTION_IPV6_ONLY_PREFERRED) && + server->ipv6_only_preferred_usec > 0) { + be32_t sec = usec_to_be32_sec(server->ipv6_only_preferred_usec); + + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_IPV6_ONLY_PREFERRED, + sizeof(sec), &sec); + if (r < 0) + return r; + } + + if (server->extra_options) { + void *key; + struct iovec_wrapper *iovw; + HASHMAP_FOREACH_KEY(iovw, key, server->extra_options->entries) { + uint32_t tag = PTR_TO_UINT32(key); + + FOREACH_ARRAY(iov, iovw->iovec, iovw->count) { + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + tag, iov->iov_len, iov->iov_base); + if (r < 0) + return r; + } + } + } + + if (!tlv_isempty(server->vendor_options)) { + _cleanup_(iovec_done) struct iovec iov = {}; + r = tlv_build(server->vendor_options, &iov); + if (r < 0) + return r; + + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_VENDOR_SPECIFIC_INFORMATION, + iov.iov_len, iov.iov_base); + if (r < 0) + return r; + } + + if (server->rapid_commit && req->rapid_commit && type == DHCP_ACK) { + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_RAPID_COMMIT, + 0, NULL); + if (r < 0) + return r; + } + + r = dhcp_server_append_static_hostname(server, packet, &offset, req); + if (r < 0) + return r; + + return dhcp_server_send_packet(server, req, packet, type, offset); +} + +int server_send_nak_or_ignore(sd_dhcp_server *server, bool init_reboot, DHCPRequest *req) { + _cleanup_free_ DHCPPacket *packet = NULL; + size_t offset; + int r; + + /* When a request is refused, RFC 2131, section 4.3.2 mentioned we should send NAK when the + * client is in INITREBOOT. If the client is in other state, there is nothing mentioned in the + * RFC whether we should send NAK or not. Hence, let's silently ignore the request. */ + + if (!init_reboot) + return 0; + + r = server_message_init(server, &packet, DHCP_NAK, &offset, req); + if (r < 0) + return log_dhcp_server_errno(server, r, "Failed to create NAK message: %m"); + + r = dhcp_server_send_packet(server, req, packet, DHCP_NAK, offset); + if (r < 0) + return log_dhcp_server_errno(server, r, "Could not send NAK message: %m"); + + log_dhcp_server(server, "NAK (0x%x)", be32toh(req->message->xid)); + return DHCP_NAK; +} + +static int server_send_forcerenew( + sd_dhcp_server *server, + be32_t address, + be32_t gateway, + uint8_t htype, + uint8_t hlen, + const uint8_t *chaddr) { + + _cleanup_free_ DHCPPacket *packet = NULL; + size_t optoffset = 0; + int r; + + assert(server); + assert(address != INADDR_ANY); + assert(chaddr); + + packet = malloc0(sizeof(DHCPPacket) + DHCP_MIN_OPTIONS_SIZE); + if (!packet) + return -ENOMEM; + + r = dhcp_message_init(&packet->dhcp, BOOTREPLY, 0, + htype, hlen, chaddr, DHCP_FORCERENEW, + DHCP_MIN_OPTIONS_SIZE, &optoffset); + if (r < 0) + return r; + + r = dhcp_option_append(&packet->dhcp, DHCP_MIN_OPTIONS_SIZE, + &optoffset, 0, SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + return dhcp_server_send_udp(server, address, DHCP_PORT_CLIENT, + &packet->dhcp, + sizeof(DHCPMessage) + optoffset); +} + +int sd_dhcp_server_forcerenew(sd_dhcp_server *server) { + sd_dhcp_server_lease *lease; + int r = 0; + + assert_return(server, -EINVAL); + + log_dhcp_server(server, "FORCERENEW"); + + HASHMAP_FOREACH(lease, server->bound_leases_by_client_id) + RET_GATHER(r, + server_send_forcerenew(server, lease->address, lease->gateway, + lease->htype, lease->hlen, lease->chaddr)); + return r; +} diff --git a/src/libsystemd-network/dhcp-server-send.h b/src/libsystemd-network/dhcp-server-send.h new file mode 100644 index 0000000000..80fd9a6b94 --- /dev/null +++ b/src/libsystemd-network/dhcp-server-send.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-forward.h" + +#include "dhcp-server-request.h" + +int server_send_offer_or_ack( + sd_dhcp_server *server, + DHCPRequest *req, + be32_t address, + uint8_t type); + +int server_send_nak_or_ignore(sd_dhcp_server *server, bool init_reboot, DHCPRequest *req); diff --git a/src/libsystemd-network/meson.build b/src/libsystemd-network/meson.build index 9b36f6edf2..3981ecd7d5 100644 --- a/src/libsystemd-network/meson.build +++ b/src/libsystemd-network/meson.build @@ -13,6 +13,8 @@ libsystemd_network_sources = files( 'dhcp-relay-interface.c', 'dhcp-relay-upstream.c', 'dhcp-route.c', + 'dhcp-server-request.c', + 'dhcp-server-send.c', 'dhcp6-network.c', 'dhcp6-option.c', 'dhcp6-protocol.c', diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c index 07f15ef6dc..5267137540 100644 --- a/src/libsystemd-network/sd-dhcp-server.c +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -5,33 +5,25 @@ #include "sd-dhcp-server.h" #include "sd-event.h" -#include "sd-id128.h" #include "alloc-util.h" -#include "dhcp-network.h" #include "dhcp-option.h" -#include "dhcp-packet.h" #include "dhcp-server-internal.h" #include "dhcp-server-lease-internal.h" +#include "dhcp-server-request.h" #include "dns-domain.h" -#include "errno-util.h" #include "fd-util.h" #include "hashmap.h" #include "in-addr-util.h" -#include "iovec-util.h" -#include "iovec-wrapper.h" -#include "memory-util.h" #include "network-common.h" #include "path-util.h" -#include "siphash24.h" #include "socket-util.h" #include "string-util.h" -#include "unaligned.h" #define DHCP_DEFAULT_LEASE_TIME_USEC USEC_PER_HOUR #define DHCP_MAX_LEASE_TIME_USEC (USEC_PER_HOUR*12) -static void server_on_lease_change(sd_dhcp_server *server) { +void dhcp_server_on_lease_change(sd_dhcp_server *server) { int r; assert(server); @@ -288,681 +280,7 @@ int sd_dhcp_server_stop(sd_dhcp_server *server) { return 0; } -static bool dhcp_request_contains(DHCPRequest *req, uint8_t option) { - assert(req); - - if (!req->parameter_request_list) - return false; - - return memchr(req->parameter_request_list, option, req->parameter_request_list_len); -} - -static int dhcp_server_send_unicast_raw( - sd_dhcp_server *server, - uint8_t hlen, - const uint8_t *chaddr, - DHCPPacket *packet, - size_t len) { - - union sockaddr_union link = { - .ll.sll_family = AF_PACKET, - .ll.sll_protocol = htobe16(ETH_P_IP), - .ll.sll_ifindex = server->ifindex, - .ll.sll_halen = hlen, - }; - int r; - - assert(server); - assert(server->ifindex > 0); - assert(server->address != 0); - assert(hlen > 0); - assert(chaddr); - assert(packet); - assert(len > sizeof(DHCPPacket)); - - memcpy(link.ll.sll_addr, chaddr, hlen); - - if (len > UINT16_MAX) - return -EOVERFLOW; - - r = dhcp_packet_append_ip_headers( - packet, - server->address, - DHCP_PORT_SERVER, - packet->dhcp.yiaddr, - DHCP_PORT_CLIENT, - len, - /* ip_service_type= */ -1); - if (r < 0) - return r; - - return dhcp_network_send_raw_socket( - server->fd_raw, - &link, - &(struct iovec_wrapper) { - .iovec = &IOVEC_MAKE(packet, len), - .count = 1, - }); -} - -static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination, - uint16_t destination_port, - DHCPMessage *message, size_t len) { - union sockaddr_union dest = { - .in.sin_family = AF_INET, - .in.sin_port = htobe16(destination_port), - .in.sin_addr.s_addr = destination, - }; - struct iovec iov = { - .iov_base = message, - .iov_len = len, - }; - CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; - struct msghdr msg = { - .msg_name = &dest, - .msg_namelen = sizeof(dest.in), - .msg_iov = &iov, - .msg_iovlen = 1, - }; - struct cmsghdr *cmsg; - struct in_pktinfo *pktinfo; - - assert(server); - assert(server->fd >= 0); - assert(message); - assert(len >= sizeof(DHCPMessage)); - - msg.msg_control = &control; - msg.msg_controllen = sizeof(control); - - cmsg = CMSG_FIRSTHDR(&msg); - assert(cmsg); - - cmsg->cmsg_level = IPPROTO_IP; - cmsg->cmsg_type = IP_PKTINFO; - cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); - - pktinfo = CMSG_TYPED_DATA(cmsg, struct in_pktinfo); - assert(pktinfo); - - pktinfo->ipi_ifindex = server->ifindex; - pktinfo->ipi_spec_dst.s_addr = server->address; - - if (sendmsg(server->fd, &msg, 0) < 0) - return -errno; - - return 0; -} - -static bool requested_broadcast(DHCPMessage *message) { - assert(message); - return message->flags & htobe16(0x8000); -} - -static int dhcp_server_send( - sd_dhcp_server *server, - uint8_t hlen, - const uint8_t *chaddr, - be32_t destination, - uint16_t destination_port, - DHCPPacket *packet, - size_t optoffset, - bool l2_broadcast) { - - if (destination != INADDR_ANY) - return dhcp_server_send_udp(server, destination, - destination_port, &packet->dhcp, - sizeof(DHCPMessage) + optoffset); - else if (l2_broadcast) - return dhcp_server_send_udp(server, INADDR_BROADCAST, - destination_port, &packet->dhcp, - sizeof(DHCPMessage) + optoffset); - else - /* we cannot send UDP packet to specific MAC address when the - address is not yet configured, so must fall back to raw - packets */ - return dhcp_server_send_unicast_raw(server, hlen, chaddr, packet, - sizeof(DHCPPacket) + optoffset); -} - -int dhcp_server_send_packet(sd_dhcp_server *server, - DHCPRequest *req, DHCPPacket *packet, - int type, size_t optoffset) { - be32_t destination = INADDR_ANY; - uint16_t destination_port = DHCP_PORT_CLIENT; - int r; - - assert(server); - assert(req); - assert(req->max_optlen > 0); - assert(req->message); - assert(optoffset <= req->max_optlen); - assert(packet); - - r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0, - SD_DHCP_OPTION_SERVER_IDENTIFIER, - 4, &server->address); - if (r < 0) - return r; - - if (req->agent_info_option) { - size_t opt_full_length = *(req->agent_info_option + 1) + 2; - /* there must be space left for SD_DHCP_OPTION_END */ - if (optoffset + opt_full_length < req->max_optlen) { - memcpy(packet->dhcp.options + optoffset, req->agent_info_option, opt_full_length); - optoffset += opt_full_length; - } - } - - r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0, - SD_DHCP_OPTION_END, 0, NULL); - if (r < 0) - return r; - - /* RFC 2131 Section 4.1 - - If the ’giaddr’ field in a DHCP message from a client is non-zero, - the server sends any return messages to the ’DHCP server’ port on the - BOOTP relay agent whose address appears in ’giaddr’. If the ’giaddr’ - field is zero and the ’ciaddr’ field is nonzero, then the server - unicasts DHCPOFFER and DHCPACK messages to the address in ’ciaddr’. - If ’giaddr’ is zero and ’ciaddr’ is zero, and the broadcast bit is - set, then the server broadcasts DHCPOFFER and DHCPACK messages to - 0xffffffff. If the broadcast bit is not set and ’giaddr’ is zero and - ’ciaddr’ is zero, then the server unicasts DHCPOFFER and DHCPACK - messages to the client’s hardware address and ’yiaddr’ address. In - all cases, when ’giaddr’ is zero, the server broadcasts any DHCPNAK - messages to 0xffffffff. - - Section 4.3.2 - - If ’giaddr’ is set in the DHCPREQUEST message, the client is on a - different subnet. The server MUST set the broadcast bit in the - DHCPNAK, so that the relay agent will broadcast the DHCPNAK to the - client, because the client may not have a correct network address - or subnet mask, and the client may not be answering ARP requests. - */ - if (req->message->giaddr != 0) { - destination = req->message->giaddr; - destination_port = DHCP_PORT_SERVER; - if (type == DHCP_NAK) - packet->dhcp.flags = htobe16(0x8000); - } else if (req->message->ciaddr != 0 && type != DHCP_NAK) - destination = req->message->ciaddr; - - bool l2_broadcast = requested_broadcast(req->message) || type == DHCP_NAK; - return dhcp_server_send(server, req->message->hlen, req->message->chaddr, - destination, destination_port, packet, optoffset, l2_broadcast); -} - -static int server_message_init( - sd_dhcp_server *server, - DHCPPacket **ret, - uint8_t type, - size_t *ret_optoffset, - DHCPRequest *req) { - - _cleanup_free_ DHCPPacket *packet = NULL; - size_t optoffset = 0; - int r; - - assert(server); - assert(ret); - assert(ret_optoffset); - assert(IN_SET(type, DHCP_OFFER, DHCP_ACK, DHCP_NAK)); - assert(req); - - packet = malloc0(sizeof(DHCPPacket) + req->max_optlen); - if (!packet) - return -ENOMEM; - - r = dhcp_message_init(&packet->dhcp, BOOTREPLY, - be32toh(req->message->xid), - req->message->htype, req->message->hlen, req->message->chaddr, - type, req->max_optlen, &optoffset); - if (r < 0) - return r; - - packet->dhcp.flags = req->message->flags; - packet->dhcp.giaddr = req->message->giaddr; - - *ret_optoffset = optoffset; - *ret = TAKE_PTR(packet); - - return 0; -} - -static int dhcp_server_append_static_hostname( - sd_dhcp_server *server, - DHCPPacket *packet, - size_t *offset, - DHCPRequest *req) { - - sd_dhcp_server_lease *static_lease; - int r; - - assert(server); - assert(packet); - assert(offset); - assert(req); - - static_lease = dhcp_server_get_static_lease(server, req); - if (!static_lease || !static_lease->hostname) - return 0; - - if (dns_name_is_single_label(static_lease->hostname)) - /* Option 12 */ - return dhcp_option_append( - &packet->dhcp, - req->max_optlen, - offset, - /* overload= */ 0, - SD_DHCP_OPTION_HOST_NAME, - strlen(static_lease->hostname), - static_lease->hostname); - - - /* Option 81 */ - uint8_t buffer[DHCP_MAX_FQDN_LENGTH + 3]; - - /* Flags: S=0 (will not update RR), O=1 (are overriding client), - * E=1 (using DNS wire format), N=1 (will not update DNS) */ - buffer[0] = DHCP_FQDN_FLAG_O | DHCP_FQDN_FLAG_E | DHCP_FQDN_FLAG_N; - - /* RFC 4702: A server SHOULD set these to 255 when sending the option and MUST ignore them on - * receipt. */ - buffer[1] = 255; - buffer[2] = 255; - - r = dns_name_to_wire_format(static_lease->hostname, buffer + 3, sizeof(buffer) - 3, false); - if (r < 0) - return log_dhcp_server_errno(server, r, "Failed to encode FQDN for static lease: %m"); - if (r > DHCP_MAX_FQDN_LENGTH) - return log_dhcp_server_errno(server, SYNTHETIC_ERRNO(EINVAL), "FQDN for static lease too long"); - - return dhcp_option_append( - &packet->dhcp, - req->max_optlen, - offset, - /* overload= */ 0, - SD_DHCP_OPTION_FQDN, - 3 + r, - buffer); -} - -static int server_send_offer_or_ack( - sd_dhcp_server *server, - DHCPRequest *req, - be32_t address, - uint8_t type) { - - static const uint8_t option_map[_SD_DHCP_LEASE_SERVER_TYPE_MAX] = { - [SD_DHCP_LEASE_DNS] = SD_DHCP_OPTION_DOMAIN_NAME_SERVER, - [SD_DHCP_LEASE_NTP] = SD_DHCP_OPTION_NTP_SERVER, - [SD_DHCP_LEASE_SIP] = SD_DHCP_OPTION_SIP_SERVER, - [SD_DHCP_LEASE_POP3] = SD_DHCP_OPTION_POP3_SERVER, - [SD_DHCP_LEASE_SMTP] = SD_DHCP_OPTION_SMTP_SERVER, - [SD_DHCP_LEASE_LPR] = SD_DHCP_OPTION_LPR_SERVER, - }; - - _cleanup_free_ DHCPPacket *packet = NULL; - be32_t lease_time; - size_t offset; - int r; - - assert(server); - assert(req); - assert(IN_SET(type, DHCP_OFFER, DHCP_ACK)); - - r = server_message_init(server, &packet, type, &offset, req); - if (r < 0) - return r; - - packet->dhcp.yiaddr = address; - packet->dhcp.siaddr = server->boot_server_address.s_addr; - - lease_time = usec_to_be32_sec(req->lifetime); - r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME, 4, - &lease_time); - if (r < 0) - return r; - - r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_SUBNET_MASK, 4, &server->netmask); - if (r < 0) - return r; - - if (server->emit_router) { - r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_ROUTER, 4, - in4_addr_is_set(&server->router_address) ? - &server->router_address.s_addr : - &server->address); - if (r < 0) - return r; - } - - if (server->boot_server_name) { - r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_BOOT_SERVER_NAME, - strlen(server->boot_server_name), server->boot_server_name); - if (r < 0) - return r; - } - - if (server->boot_filename) { - r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_BOOT_FILENAME, - strlen(server->boot_filename), server->boot_filename); - if (r < 0) - return r; - } - - for (sd_dhcp_lease_server_type_t k = 0; k < _SD_DHCP_LEASE_SERVER_TYPE_MAX; k++) { - if (server->servers[k].size <= 0) - continue; - - r = dhcp_option_append( - &packet->dhcp, req->max_optlen, &offset, 0, - option_map[k], - sizeof(struct in_addr) * server->servers[k].size, - server->servers[k].addr); - if (r < 0) - return r; - } - - if (server->timezone) { - r = dhcp_option_append( - &packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_TZDB_TIMEZONE, - strlen(server->timezone), server->timezone); - if (r < 0) - return r; - } - - if (server->domain_name) { - r = dhcp_option_append( - &packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_DOMAIN_NAME, - strlen(server->domain_name), server->domain_name); - if (r < 0) - return r; - } - - /* RFC 8925 section 3.3. DHCPv4 Server Behavior - * The server MUST NOT include the IPv6-Only Preferred option in the DHCPOFFER or DHCPACK message if - * the option was not present in the Parameter Request List sent by the client. */ - if (dhcp_request_contains(req, SD_DHCP_OPTION_IPV6_ONLY_PREFERRED) && - server->ipv6_only_preferred_usec > 0) { - be32_t sec = usec_to_be32_sec(server->ipv6_only_preferred_usec); - - r = dhcp_option_append( - &packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_IPV6_ONLY_PREFERRED, - sizeof(sec), &sec); - if (r < 0) - return r; - } - - if (server->extra_options) { - void *key; - struct iovec_wrapper *iovw; - HASHMAP_FOREACH_KEY(iovw, key, server->extra_options->entries) { - uint32_t tag = PTR_TO_UINT32(key); - - FOREACH_ARRAY(iov, iovw->iovec, iovw->count) { - r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, - tag, iov->iov_len, iov->iov_base); - if (r < 0) - return r; - } - } - } - - if (!tlv_isempty(server->vendor_options)) { - _cleanup_(iovec_done) struct iovec iov = {}; - r = tlv_build(server->vendor_options, &iov); - if (r < 0) - return r; - - r = dhcp_option_append( - &packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_VENDOR_SPECIFIC_INFORMATION, - iov.iov_len, iov.iov_base); - if (r < 0) - return r; - } - - if (server->rapid_commit && req->rapid_commit && type == DHCP_ACK) { - r = dhcp_option_append( - &packet->dhcp, req->max_optlen, &offset, 0, - SD_DHCP_OPTION_RAPID_COMMIT, - 0, NULL); - if (r < 0) - return r; - } - - r = dhcp_server_append_static_hostname(server, packet, &offset, req); - if (r < 0) - return r; - - return dhcp_server_send_packet(server, req, packet, type, offset); -} - -static int server_send_nak_or_ignore(sd_dhcp_server *server, bool init_reboot, DHCPRequest *req) { - _cleanup_free_ DHCPPacket *packet = NULL; - size_t offset; - int r; - - /* When a request is refused, RFC 2131, section 4.3.2 mentioned we should send NAK when the - * client is in INITREBOOT. If the client is in other state, there is nothing mentioned in the - * RFC whether we should send NAK or not. Hence, let's silently ignore the request. */ - - if (!init_reboot) - return 0; - - r = server_message_init(server, &packet, DHCP_NAK, &offset, req); - if (r < 0) - return log_dhcp_server_errno(server, r, "Failed to create NAK message: %m"); - - r = dhcp_server_send_packet(server, req, packet, DHCP_NAK, offset); - if (r < 0) - return log_dhcp_server_errno(server, r, "Could not send NAK message: %m"); - - log_dhcp_server(server, "NAK (0x%x)", be32toh(req->message->xid)); - return DHCP_NAK; -} - -static int server_send_forcerenew( - sd_dhcp_server *server, - be32_t address, - be32_t gateway, - uint8_t htype, - uint8_t hlen, - const uint8_t *chaddr) { - - _cleanup_free_ DHCPPacket *packet = NULL; - size_t optoffset = 0; - int r; - - assert(server); - assert(address != INADDR_ANY); - assert(chaddr); - - packet = malloc0(sizeof(DHCPPacket) + DHCP_MIN_OPTIONS_SIZE); - if (!packet) - return -ENOMEM; - - r = dhcp_message_init(&packet->dhcp, BOOTREPLY, 0, - htype, hlen, chaddr, DHCP_FORCERENEW, - DHCP_MIN_OPTIONS_SIZE, &optoffset); - if (r < 0) - return r; - - r = dhcp_option_append(&packet->dhcp, DHCP_MIN_OPTIONS_SIZE, - &optoffset, 0, SD_DHCP_OPTION_END, 0, NULL); - if (r < 0) - return r; - - return dhcp_server_send_udp(server, address, DHCP_PORT_CLIENT, - &packet->dhcp, - sizeof(DHCPMessage) + optoffset); -} - -static int parse_request(uint8_t code, uint8_t len, const void *option, void *userdata) { - DHCPRequest *req = ASSERT_PTR(userdata); - int r; - - switch (code) { - case SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME: - if (len == 4) - req->lifetime = unaligned_be32_sec_to_usec(option, /* max_as_infinity= */ true); - - break; - case SD_DHCP_OPTION_REQUESTED_IP_ADDRESS: - if (len == 4) - memcpy(&req->requested_ip, option, sizeof(be32_t)); - - break; - case SD_DHCP_OPTION_SERVER_IDENTIFIER: - if (len == 4) - memcpy(&req->server_id, option, sizeof(be32_t)); - - break; - case SD_DHCP_OPTION_CLIENT_IDENTIFIER: - if (client_id_size_is_valid(len)) - (void) sd_dhcp_client_id_set_raw(&req->client_id, option, len); - - break; - case SD_DHCP_OPTION_MAXIMUM_MESSAGE_SIZE: - - if (len == 2 && unaligned_read_be16(option) >= sizeof(DHCPPacket)) - req->max_optlen = unaligned_read_be16(option) - sizeof(DHCPPacket); - - break; - case SD_DHCP_OPTION_RELAY_AGENT_INFORMATION: - req->agent_info_option = (uint8_t*)option - 2; - - break; - case SD_DHCP_OPTION_HOST_NAME: { - _cleanup_free_ char *p = NULL; - - r = dhcp_option_parse_hostname(option, len, &p); - if (r < 0) - log_debug_errno(r, "Failed to parse hostname, ignoring: %m"); - else - free_and_replace(req->hostname, p); - break; - } - case SD_DHCP_OPTION_PARAMETER_REQUEST_LIST: - req->parameter_request_list = option; - req->parameter_request_list_len = len; - break; - - case SD_DHCP_OPTION_RAPID_COMMIT: - req->rapid_commit = true; - break; - } - - return 0; -} - -static DHCPRequest* dhcp_request_free(DHCPRequest *req) { - if (!req) - return NULL; - - free(req->hostname); - return mfree(req); -} - -DEFINE_TRIVIAL_CLEANUP_FUNC(DHCPRequest*, dhcp_request_free); - -static int ensure_sane_request(sd_dhcp_server *server, DHCPRequest *req, DHCPMessage *message) { - int r; - - assert(req); - assert(message); - - req->message = message; - - if (message->hlen > sizeof(message->chaddr)) - return -EBADMSG; - - /* set client id based on MAC address if client did not send an explicit one */ - if (!sd_dhcp_client_id_is_set(&req->client_id)) { - if (!client_id_data_size_is_valid(message->hlen)) - return -EBADMSG; - - r = sd_dhcp_client_id_set(&req->client_id, /* type= */ 1, message->chaddr, message->hlen); - if (r < 0) - return r; - } - - if (message->hlen == 0 || memeqzero(message->chaddr, message->hlen)) { - uint8_t type; - const void *data; - size_t size; - - /* See RFC2131 section 4.1.1. - * hlen and chaddr may not be set for non-ethernet interface. - * Let's try to retrieve it from the client ID. */ - - if (!sd_dhcp_client_id_is_set(&req->client_id)) - return -EBADMSG; - - r = sd_dhcp_client_id_get(&req->client_id, &type, &data, &size); - if (r < 0) - return r; - - if (type != 1) - return -EBADMSG; - - if (size > sizeof(message->chaddr)) - return -EBADMSG; - - memcpy(message->chaddr, data, size); - message->hlen = size; - } - - if (req->max_optlen < DHCP_MIN_OPTIONS_SIZE) - req->max_optlen = DHCP_MIN_OPTIONS_SIZE; - - if (req->lifetime <= 0) - req->lifetime = MAX(USEC_PER_SEC, server->default_lease_time); - - if (server->max_lease_time > 0 && req->lifetime > server->max_lease_time) - req->lifetime = server->max_lease_time; - - return 0; -} - -static void request_set_timestamp(DHCPRequest *req, const triple_timestamp *timestamp) { - assert(req); - - if (timestamp && triple_timestamp_is_set(timestamp)) - req->timestamp = *timestamp; - else - triple_timestamp_now(&req->timestamp); -} - -static int request_get_lifetime_timestamp(DHCPRequest *req, clockid_t clock, usec_t *ret) { - assert(req); - assert(TRIPLE_TIMESTAMP_HAS_CLOCK(clock)); - assert(clock_supported(clock)); - assert(ret); - - if (req->lifetime <= 0) - return -ENODATA; - - if (!triple_timestamp_is_set(&req->timestamp)) - return -ENODATA; - - *ret = usec_add(triple_timestamp_by_clock(&req->timestamp, clock), req->lifetime); - return 0; -} - -static bool address_is_in_pool(sd_dhcp_server *server, be32_t address) { +bool dhcp_server_address_is_in_pool(sd_dhcp_server *server, be32_t address) { assert(server); if (server->pool_size == 0) @@ -981,34 +299,7 @@ static bool address_is_in_pool(sd_dhcp_server *server, be32_t address) { return true; } -static int server_ack_request(sd_dhcp_server *server, DHCPRequest *req, be32_t address) { - usec_t expiration; - int r; - - assert(server); - assert(req); - assert(address != 0); - - r = request_get_lifetime_timestamp(req, CLOCK_BOOTTIME, &expiration); - if (r < 0) - return r; - - r = dhcp_server_set_lease(server, address, req, expiration); - if (r < 0) - return log_dhcp_server_errno(server, r, "Failed to create new lease: %m"); - - r = server_send_offer_or_ack(server, req, address, DHCP_ACK); - if (r < 0) - return log_dhcp_server_errno(server, r, "Could not send ACK: %m"); - - log_dhcp_server(server, "ACK (0x%x)", be32toh(req->message->xid)); - - server_on_lease_change(server); - - return DHCP_ACK; -} - -static bool address_available(sd_dhcp_server *server, be32_t address) { +bool dhcp_server_address_available(sd_dhcp_server *server, be32_t address) { assert(server); if (hashmap_contains(server->bound_leases_by_address, UINT32_TO_PTR(address)) || @@ -1019,262 +310,6 @@ static bool address_available(sd_dhcp_server *server, be32_t address) { return true; } -#define HASH_KEY SD_ID128_MAKE(0d,1d,fe,bd,f1,24,bd,b3,47,f1,dd,6e,73,21,93,30) - -int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, size_t length, const triple_timestamp *timestamp) { - _cleanup_(dhcp_request_freep) DHCPRequest *req = NULL; - _cleanup_free_ char *error_message = NULL; - sd_dhcp_server_lease *existing_lease, *static_lease; - int type, r; - - assert(server); - assert(message); - - if (message->op != BOOTREQUEST) - return 0; - - req = new0(DHCPRequest, 1); - if (!req) - return -ENOMEM; - - type = dhcp_option_parse(message, length, parse_request, req, &error_message); - if (type < 0) - return type; - - r = ensure_sane_request(server, req, message); - if (r < 0) - return r; - - request_set_timestamp(req, timestamp); - - r = dhcp_server_cleanup_expired_leases(server); - if (r < 0) - return r; - - existing_lease = hashmap_get(server->bound_leases_by_client_id, &req->client_id); - static_lease = dhcp_server_get_static_lease(server, req); - - switch (type) { - - case DHCP_DISCOVER: { - be32_t address = INADDR_ANY; - - log_dhcp_server(server, "DISCOVER (0x%x)", be32toh(req->message->xid)); - - if (server->pool_size == 0) - /* no pool allocated */ - return 0; - - /* for now pick a random free address from the pool */ - if (static_lease) { - sd_dhcp_server_lease *l = hashmap_get(server->bound_leases_by_address, UINT32_TO_PTR(static_lease->address)); - if (l && l != existing_lease) - /* The address is already assigned to another host. Refusing. */ - return 0; - - /* Found a matching static lease. */ - address = static_lease->address; - - } else if (existing_lease && address_is_in_pool(server, existing_lease->address)) - - /* If we previously assigned an address to the host, then reuse it. */ - address = existing_lease->address; - - else { - struct siphash state; - uint64_t hash; - - /* even with no persistence of leases, we try to offer the same client - the same IP address. we do this by using the hash of the client id - as the offset into the pool of leases when finding the next free one */ - - siphash24_init(&state, HASH_KEY.bytes); - client_id_hash_func(&req->client_id, &state); - hash = htole64(siphash24_finalize(&state)); - - for (unsigned i = 0; i < server->pool_size; i++) { - be32_t tmp_address; - - tmp_address = server->subnet | htobe32(server->pool_offset + (hash + i) % server->pool_size); - if (address_available(server, tmp_address)) { - address = tmp_address; - break; - } - } - } - - if (address == INADDR_ANY) - /* no free addresses left */ - return 0; - - if (server->rapid_commit && req->rapid_commit) - return server_ack_request(server, req, address); - - r = server_send_offer_or_ack(server, req, address, DHCP_OFFER); - if (r < 0) - /* this only fails on critical errors */ - return log_dhcp_server_errno(server, r, "Could not send offer: %m"); - - log_dhcp_server(server, "OFFER (0x%x)", be32toh(req->message->xid)); - return DHCP_OFFER; - } - case DHCP_DECLINE: - log_dhcp_server(server, "DECLINE (0x%x): %s", be32toh(req->message->xid), strna(error_message)); - - /* TODO: make sure we don't offer this address again */ - - return 1; - - case DHCP_REQUEST: { - be32_t address; - bool init_reboot = false; - - /* see RFC 2131, section 4.3.2 */ - - if (req->server_id != 0) { - log_dhcp_server(server, "REQUEST (selecting) (0x%x)", - be32toh(req->message->xid)); - - /* SELECTING */ - if (req->server_id != server->address) - /* client did not pick us */ - return 0; - - if (req->message->ciaddr != 0) - /* this MUST be zero */ - return 0; - - if (req->requested_ip == 0) - /* this must be filled in with the yiaddr - from the chosen OFFER */ - return 0; - - address = req->requested_ip; - } else if (req->requested_ip != 0) { - log_dhcp_server(server, "REQUEST (init-reboot) (0x%x)", - be32toh(req->message->xid)); - - /* INIT-REBOOT */ - if (req->message->ciaddr != 0) - /* this MUST be zero */ - return 0; - - /* TODO: check more carefully if IP is correct */ - address = req->requested_ip; - init_reboot = true; - } else { - log_dhcp_server(server, "REQUEST (rebinding/renewing) (0x%x)", - be32toh(req->message->xid)); - - /* REBINDING / RENEWING */ - if (req->message->ciaddr == 0) - /* this MUST be filled in with clients IP address */ - return 0; - - address = req->message->ciaddr; - } - - /* Silently ignore Rapid Commit option in REQUEST message. */ - req->rapid_commit = false; - - if (static_lease) { - if (static_lease->address != address) - /* The client requested an address which is different from the static lease. Refusing. */ - return server_send_nak_or_ignore(server, init_reboot, req); - - sd_dhcp_server_lease *l = hashmap_get(server->bound_leases_by_address, UINT32_TO_PTR(address)); - if (l && l != existing_lease) - /* The requested address is already assigned to another host. Refusing. */ - return server_send_nak_or_ignore(server, init_reboot, req); - - /* Found a static lease for the client ID. */ - return server_ack_request(server, req, address); - } - - if (address_is_in_pool(server, address)) - /* The requested address is in the pool. */ - return server_ack_request(server, req, address); - - /* Refuse otherwise. */ - return server_send_nak_or_ignore(server, init_reboot, req); - } - - case DHCP_RELEASE: { - log_dhcp_server(server, "RELEASE (0x%x)", - be32toh(req->message->xid)); - - if (!existing_lease) - return 0; - - if (existing_lease->address != req->message->ciaddr) - return 0; - - sd_dhcp_server_lease_unref(existing_lease); - - server_on_lease_change(server); - - return 0; - }} - - return 0; -} - -static int server_receive_message(sd_event_source *s, int fd, - uint32_t revents, void *userdata) { - _cleanup_free_ DHCPMessage *message = NULL; - /* This needs to be initialized with zero. See #20741. - * The issue is fixed on glibc-2.35 (8fba672472ae0055387e9315fc2eddfa6775ca79). */ - CMSG_BUFFER_TYPE(CMSG_SPACE_TIMEVAL + - CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; - sd_dhcp_server *server = ASSERT_PTR(userdata); - struct iovec iov = {}; - struct msghdr msg = { - .msg_iov = &iov, - .msg_iovlen = 1, - .msg_control = &control, - .msg_controllen = sizeof(control), - }; - ssize_t datagram_size, len; - int r; - - datagram_size = next_datagram_size_fd(fd); - if (ERRNO_IS_NEG_TRANSIENT(datagram_size) || ERRNO_IS_NEG_DISCONNECT(datagram_size)) - return 0; - if (datagram_size < 0) { - log_dhcp_server_errno(server, datagram_size, "Failed to determine datagram size to read, ignoring: %m"); - return 0; - } - - size_t buflen = datagram_size; - message = malloc0(buflen); - if (!message) - return -ENOMEM; - - iov = IOVEC_MAKE(message, datagram_size); - - len = recvmsg_safe(fd, &msg, 0); - if (ERRNO_IS_NEG_TRANSIENT(len) || ERRNO_IS_NEG_DISCONNECT(len)) - return 0; - if (len < 0) { - log_dhcp_server_errno(server, len, "Could not receive message, ignoring: %m"); - return 0; - } - - if ((size_t) len < sizeof(DHCPMessage)) - return 0; - - /* TODO figure out if this can be done as a filter on the socket, like for IPv6 */ - struct in_pktinfo *info = CMSG_FIND_DATA(&msg, IPPROTO_IP, IP_PKTINFO, struct in_pktinfo); - if (info && info->ipi_ifindex != server->ifindex) - return 0; - - r = dhcp_server_handle_message(server, message, (size_t) len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg)); - if (r < 0) - log_dhcp_server_errno(server, r, "Couldn't process incoming message, ignoring: %m"); - - return 0; -} - static void dhcp_server_update_lease_servers(sd_dhcp_server *server) { assert(server); assert(server->address != 0); @@ -1303,55 +338,16 @@ int sd_dhcp_server_start(sd_dhcp_server *server) { dhcp_server_update_lease_servers(server); - r = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); - if (r < 0) { - r = -errno; - goto on_error; - } - server->fd_raw = r; - - r = dhcp_network_bind_udp_socket(server->ifindex, INADDR_ANY, DHCP_PORT_SERVER, -1); + r = dhcp_server_setup_io_event_source(server); if (r < 0) - goto on_error; - server->fd = r; - - r = sd_event_add_io(server->event, &server->receive_message, - server->fd, EPOLLIN, - server_receive_message, server); - if (r < 0) - goto on_error; - - r = sd_event_source_set_priority(server->receive_message, - server->event_priority); - if (r < 0) - goto on_error; + return r; r = dhcp_server_load_leases(server); if (r < 0) log_dhcp_server_errno(server, r, "Failed to load lease file %s, ignoring: %m", strna(server->lease_file)); log_dhcp_server(server, "STARTED"); - return 0; - -on_error: - sd_dhcp_server_stop(server); - return r; -} - -int sd_dhcp_server_forcerenew(sd_dhcp_server *server) { - sd_dhcp_server_lease *lease; - int r = 0; - - assert_return(server, -EINVAL); - - log_dhcp_server(server, "FORCERENEW"); - - HASHMAP_FOREACH(lease, server->bound_leases_by_client_id) - RET_GATHER(r, - server_send_forcerenew(server, lease->address, lease->gateway, - lease->htype, lease->hlen, lease->chaddr)); - return r; } int sd_dhcp_server_set_timezone(sd_dhcp_server *server, const char *tz) { diff --git a/src/libsystemd-network/test-dhcp-server.c b/src/libsystemd-network/test-dhcp-server.c index bbfb4f1c05..86face3406 100644 --- a/src/libsystemd-network/test-dhcp-server.c +++ b/src/libsystemd-network/test-dhcp-server.c @@ -9,6 +9,7 @@ #include "sd-event.h" #include "dhcp-server-internal.h" +#include "dhcp-server-request.h" #include "tests.h" TEST(basic) { From 45a3a3b92c9d62613b23a1a18c2e60bf33b2abf4 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 4 May 2026 16:59:04 +0900 Subject: [PATCH 03/10] dhcp-server-request: modernize server_receive_message() --- src/libsystemd-network/dhcp-server-request.c | 40 +++++++++----------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/libsystemd-network/dhcp-server-request.c b/src/libsystemd-network/dhcp-server-request.c index 5cccc7c2f6..555796d8a1 100644 --- a/src/libsystemd-network/dhcp-server-request.c +++ b/src/libsystemd-network/dhcp-server-request.c @@ -445,38 +445,34 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, siz } static int server_receive_message(sd_event_source *s, int fd, uint32_t revents, void *userdata) { - _cleanup_free_ DHCPMessage *message = NULL; + sd_dhcp_server *server = ASSERT_PTR(userdata); + int r; + + ssize_t buflen = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(buflen) || ERRNO_IS_NEG_DISCONNECT(buflen)) + return 0; + if (buflen < 0) { + log_dhcp_server_errno(server, buflen, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + _cleanup_free_ void *buf = malloc0(buflen); + if (!buf) + return -ENOMEM; + /* This needs to be initialized with zero. See #20741. * The issue is fixed on glibc-2.35 (8fba672472ae0055387e9315fc2eddfa6775ca79). */ CMSG_BUFFER_TYPE(CMSG_SPACE_TIMEVAL + CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; - sd_dhcp_server *server = ASSERT_PTR(userdata); - struct iovec iov = {}; + struct iovec iov = IOVEC_MAKE(buf, buflen); struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1, .msg_control = &control, .msg_controllen = sizeof(control), }; - ssize_t datagram_size, len; - int r; - datagram_size = next_datagram_size_fd(fd); - if (ERRNO_IS_NEG_TRANSIENT(datagram_size) || ERRNO_IS_NEG_DISCONNECT(datagram_size)) - return 0; - if (datagram_size < 0) { - log_dhcp_server_errno(server, datagram_size, "Failed to determine datagram size to read, ignoring: %m"); - return 0; - } - - size_t buflen = datagram_size; - message = malloc0(buflen); - if (!message) - return -ENOMEM; - - iov = IOVEC_MAKE(message, datagram_size); - - len = recvmsg_safe(fd, &msg, 0); + ssize_t len = recvmsg_safe(fd, &msg, 0); if (ERRNO_IS_NEG_TRANSIENT(len) || ERRNO_IS_NEG_DISCONNECT(len)) return 0; if (len < 0) { @@ -492,7 +488,7 @@ static int server_receive_message(sd_event_source *s, int fd, uint32_t revents, if (info && info->ipi_ifindex != server->ifindex) return 0; - r = dhcp_server_handle_message(server, message, (size_t) len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg)); + r = dhcp_server_handle_message(server, buf, (size_t) len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg)); if (r < 0) log_dhcp_server_errno(server, r, "Couldn't process incoming message, ignoring: %m"); From 27d99475b647022b498218715326c55bfac0c878 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 5 May 2026 06:57:33 +0900 Subject: [PATCH 04/10] dhcp-serve-request: move message size check to dhcp_server_handle_message() --- src/libsystemd-network/dhcp-server-request.c | 6 +++--- src/libsystemd-network/fuzz-dhcp-server.c | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/libsystemd-network/dhcp-server-request.c b/src/libsystemd-network/dhcp-server-request.c index 555796d8a1..78d641141c 100644 --- a/src/libsystemd-network/dhcp-server-request.c +++ b/src/libsystemd-network/dhcp-server-request.c @@ -414,6 +414,9 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, siz assert(server); assert(message); + if (length < sizeof(DHCPMessage)) + return 0; + if (message->op != BOOTREQUEST) return 0; @@ -480,9 +483,6 @@ static int server_receive_message(sd_event_source *s, int fd, uint32_t revents, return 0; } - if ((size_t) len < sizeof(DHCPMessage)) - return 0; - /* TODO: figure out if this can be done as a filter on the socket, like for IPv6 */ struct in_pktinfo *info = CMSG_FIND_DATA(&msg, IPPROTO_IP, IP_PKTINFO, struct in_pktinfo); if (info && info->ipi_ifindex != server->ifindex) diff --git a/src/libsystemd-network/fuzz-dhcp-server.c b/src/libsystemd-network/fuzz-dhcp-server.c index 037acdec07..9d927472e0 100644 --- a/src/libsystemd-network/fuzz-dhcp-server.c +++ b/src/libsystemd-network/fuzz-dhcp-server.c @@ -63,9 +63,6 @@ static int add_static_lease(sd_dhcp_server *server, uint8_t i) { int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { struct in_addr address = { .s_addr = htobe32(UINT32_C(10) << 24 | UINT32_C(1))}; - if (size < sizeof(DHCPMessage)) - return 0; - fuzz_setup_logging(); _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL; From 0bb26684d042d92eaadd1ceb713c76ad6f8ba75b Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 4 May 2026 19:57:49 +0900 Subject: [PATCH 05/10] sd-dhcp-server: refactoring for socket fd handling This makes - UDP socket fd is owned by IO event source, - open RAW socket fd just before sending first packet, - set TOS and socket priority, - use AF_UNIX soxket pair in the unit test and fuzzer, so the unit test can now run by unprivileged user. --- src/libsystemd-network/dhcp-server-internal.h | 7 +- src/libsystemd-network/dhcp-server-request.c | 96 +++++++++++---- src/libsystemd-network/dhcp-server-send.c | 114 +++++++++++------- src/libsystemd-network/fuzz-dhcp-server.c | 6 +- src/libsystemd-network/sd-dhcp-server.c | 30 ++--- src/libsystemd-network/test-dhcp-server.c | 37 +++--- 6 files changed, 184 insertions(+), 106 deletions(-) diff --git a/src/libsystemd-network/dhcp-server-internal.h b/src/libsystemd-network/dhcp-server-internal.h index c03ce7dddb..76db80b77d 100644 --- a/src/libsystemd-network/dhcp-server-internal.h +++ b/src/libsystemd-network/dhcp-server-internal.h @@ -29,9 +29,10 @@ typedef struct sd_dhcp_server { sd_event *event; int event_priority; - sd_event_source *receive_message; - int fd; - int fd_raw; + sd_event_source *io_event_source; + uint8_t ip_service_type; + int socket_fd; /* socket fd set externally, used by unit tests */ + int raw_socket_fd; /* send-only raw socket fd, used on sending L2 unicast message. */ int ifindex; char *ifname; diff --git a/src/libsystemd-network/dhcp-server-request.c b/src/libsystemd-network/dhcp-server-request.c index 78d641141c..39c11e69c8 100644 --- a/src/libsystemd-network/dhcp-server-request.c +++ b/src/libsystemd-network/dhcp-server-request.c @@ -3,13 +3,13 @@ #include "sd-event.h" #include "alloc-util.h" -#include "dhcp-network.h" #include "dhcp-protocol.h" #include "dhcp-server-internal.h" #include "dhcp-server-lease-internal.h" #include "dhcp-server-request.h" #include "dhcp-server-send.h" #include "errno-util.h" +#include "fd-util.h" #include "hashmap.h" #include "iovec-util.h" #include "memory-util.h" @@ -495,38 +495,92 @@ static int server_receive_message(sd_event_source *s, int fd, uint32_t revents, return 0; } +static int server_open_socket(sd_dhcp_server *server) { + int r; + + assert(server); + + _cleanup_close_ int fd = RET_NERRNO(socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)); + if (fd < 0) + return fd; + + r = socket_bind_to_ifindex(fd, server->ifindex); + if (r < 0) + return r; + + r = setsockopt_int(fd, SOL_SOCKET, SO_TIMESTAMP, true); + if (r < 0) + return r; + + r = setsockopt_int(fd, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return r; + + r = setsockopt_int(fd, SOL_SOCKET, SO_BROADCAST, true); + if (r < 0) + return r; + + r = setsockopt_int(fd, SOL_SOCKET, SO_PRIORITY, tos_to_priority(server->ip_service_type)); + if (r < 0) + return r; + + r = setsockopt_int(fd, IPPROTO_IP, IP_TOS, server->ip_service_type); + if (r < 0) + return r; + + r = setsockopt_int(fd, IPPROTO_IP, IP_PKTINFO, true); + if (r < 0) + return r; + + union sockaddr_union sa = { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(DHCP_PORT_SERVER), + .in.sin_addr.s_addr = htobe32(INADDR_ANY), + }; + + if (bind(fd, &sa.sa, sizeof(sa.in)) < 0) + return -errno; + + return TAKE_FD(fd); +} + int dhcp_server_setup_io_event_source(sd_dhcp_server *server) { int r; assert(server); assert(server->event); - r = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); - if (r < 0) { - r = -errno; - goto on_error; + _cleanup_close_ int fd_close = -EBADF; + int fd; + if (server->socket_fd >= 0) + /* When a socket fd is given externally, unconditionally use it and do not close the socket + * even if we fail to set up the event source. */ + fd = server->socket_fd; + else { + fd = fd_close = server_open_socket(server); + if (fd < 0) + return fd; } - server->fd_raw = r; - r = dhcp_network_bind_udp_socket(server->ifindex, INADDR_ANY, DHCP_PORT_SERVER, -1); + _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; + r = sd_event_add_io(server->event, &s, fd, EPOLLIN, server_receive_message, server); if (r < 0) - goto on_error; - server->fd = r; + return r; - r = sd_event_add_io(server->event, &server->receive_message, - server->fd, EPOLLIN, - server_receive_message, server); + r = sd_event_source_set_priority(s, server->event_priority); if (r < 0) - goto on_error; + return r; - r = sd_event_source_set_priority(server->receive_message, - server->event_priority); - if (r < 0) - goto on_error; + (void) sd_event_source_set_description(s, "dhcp-server-io"); + if (fd_close >= 0) { + r = sd_event_source_set_io_fd_own(s, true); + if (r < 0) + return r; + TAKE_FD(fd_close); + } + + sd_event_source_disable_unref(server->io_event_source); + server->io_event_source = TAKE_PTR(s); return 0; - - on_error: - sd_dhcp_server_stop(server); - return r; } diff --git a/src/libsystemd-network/dhcp-server-send.c b/src/libsystemd-network/dhcp-server-send.c index 67fc390bca..11d553404c 100644 --- a/src/libsystemd-network/dhcp-server-send.c +++ b/src/libsystemd-network/dhcp-server-send.c @@ -1,19 +1,47 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-event.h" + #include "alloc-util.h" -#include "dhcp-network.h" #include "dhcp-option.h" #include "dhcp-packet.h" #include "dhcp-server-lease-internal.h" #include "dhcp-server-send.h" #include "dns-domain.h" #include "errno-util.h" +#include "fd-util.h" #include "hashmap.h" #include "in-addr-util.h" #include "iovec-util.h" #include "iovec-wrapper.h" #include "socket-util.h" +static int server_acquire_raw_socket(sd_dhcp_server *server) { + int r; + + assert(server); + + if (server->socket_fd >= 0) + /* When a socket fd is given externally, unconditionally use it and do not close the socket. */ + return server->socket_fd; + + if (server->raw_socket_fd >= 0) + /* Already opened. */ + return server->raw_socket_fd; + + /* This is a send-only socket, hence it is opened with protocol=0, and do not call bind(). + * The interface binding will be done on send. */ + _cleanup_close_ int fd = RET_NERRNO(socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)); + if (fd < 0) + return fd; + + r = setsockopt_int(fd, SOL_SOCKET, SO_PRIORITY, tos_to_priority(server->ip_service_type)); + if (r < 0) + return r; + + return server->raw_socket_fd = TAKE_FD(fd); +} + static int dhcp_server_send_unicast_raw( sd_dhcp_server *server, uint8_t hlen, @@ -21,12 +49,6 @@ static int dhcp_server_send_unicast_raw( DHCPPacket *packet, size_t len) { - union sockaddr_union link = { - .ll.sll_family = AF_PACKET, - .ll.sll_protocol = htobe16(ETH_P_IP), - .ll.sll_ifindex = server->ifindex, - .ll.sll_halen = hlen, - }; int r; assert(server); @@ -37,11 +59,13 @@ static int dhcp_server_send_unicast_raw( assert(packet); assert(len > sizeof(DHCPPacket)); - memcpy(link.ll.sll_addr, chaddr, hlen); - if (len > UINT16_MAX) return -EOVERFLOW; + int fd = server_acquire_raw_socket(server); + if (fd < 0) + return fd; + r = dhcp_packet_append_ip_headers( packet, server->address, @@ -49,63 +73,69 @@ static int dhcp_server_send_unicast_raw( packet->dhcp.yiaddr, DHCP_PORT_CLIENT, len, - /* ip_service_type= */ -1); + server->ip_service_type); if (r < 0) return r; - return dhcp_network_send_raw_socket( - server->fd_raw, - &link, - &(struct iovec_wrapper) { - .iovec = &IOVEC_MAKE(packet, len), - .count = 1, - }); + union sockaddr_union sa = { + .ll.sll_family = AF_PACKET, + .ll.sll_protocol = htobe16(ETH_P_IP), + .ll.sll_ifindex = server->ifindex, + .ll.sll_halen = hlen, + }; + + memcpy(sa.ll.sll_addr, chaddr, hlen); + + struct msghdr mh = { + .msg_name = &sa.sa, + .msg_namelen = sockaddr_ll_len(&sa.ll), + .msg_iov = &IOVEC_MAKE(packet, len), + .msg_iovlen = 1, + }; + + if (sendmsg(fd, &mh, MSG_NOSIGNAL) < 0) + return -errno; + + return 0; } static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination, uint16_t destination_port, DHCPMessage *message, size_t len) { - union sockaddr_union dest = { + + assert(server); + assert(message); + assert(len >= sizeof(DHCPMessage)); + + int fd = sd_event_source_get_io_fd(server->io_event_source); + if (fd < 0) + return fd; + + union sockaddr_union sa = { .in.sin_family = AF_INET, .in.sin_port = htobe16(destination_port), .in.sin_addr.s_addr = destination, }; - struct iovec iov = { - .iov_base = message, - .iov_len = len, - }; CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; struct msghdr msg = { - .msg_name = &dest, - .msg_namelen = sizeof(dest.in), - .msg_iov = &iov, + .msg_name = &sa, + .msg_namelen = sizeof(sa.in), + .msg_iov = &IOVEC_MAKE(message, len), .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), }; - struct cmsghdr *cmsg; - struct in_pktinfo *pktinfo; - - assert(server); - assert(server->fd >= 0); - assert(message); - assert(len >= sizeof(DHCPMessage)); - - msg.msg_control = &control; - msg.msg_controllen = sizeof(control); - - cmsg = CMSG_FIRSTHDR(&msg); - assert(cmsg); + struct cmsghdr *cmsg = ASSERT_PTR(CMSG_FIRSTHDR(&msg)); cmsg->cmsg_level = IPPROTO_IP; cmsg->cmsg_type = IP_PKTINFO; cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); - pktinfo = CMSG_TYPED_DATA(cmsg, struct in_pktinfo); - assert(pktinfo); - + struct in_pktinfo *pktinfo = ASSERT_PTR(CMSG_TYPED_DATA(cmsg, struct in_pktinfo)); pktinfo->ipi_ifindex = server->ifindex; pktinfo->ipi_spec_dst.s_addr = server->address; - if (sendmsg(server->fd, &msg, 0) < 0) + if (sendmsg(fd, &msg, MSG_NOSIGNAL) < 0) return -errno; return 0; diff --git a/src/libsystemd-network/fuzz-dhcp-server.c b/src/libsystemd-network/fuzz-dhcp-server.c index 9d927472e0..ac006bc2aa 100644 --- a/src/libsystemd-network/fuzz-dhcp-server.c +++ b/src/libsystemd-network/fuzz-dhcp-server.c @@ -68,13 +68,16 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL; _cleanup_close_ int dir_fd = ASSERT_OK(mkdtemp_open(NULL, 0, &tmpdir)); + _cleanup_close_pair_ int socket_fd[2] = EBADF_PAIR; + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, socket_fd)); + _cleanup_(sd_event_unrefp) sd_event *event = NULL; ASSERT_OK(sd_event_new(&event)); _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; ASSERT_OK(sd_dhcp_server_new(&server, 1)); ASSERT_OK(sd_dhcp_server_attach_event(server, event, SD_EVENT_PRIORITY_NORMAL)); - server->fd = ASSERT_OK_ERRNO(open("/dev/null", O_RDWR|O_CLOEXEC|O_NOCTTY)); + server->socket_fd = TAKE_FD(socket_fd[0]); ASSERT_OK(sd_dhcp_server_set_lease_file(server, dir_fd, "leases")); ASSERT_OK(sd_dhcp_server_configure_pool(server, &address, 24, 0, 0)); @@ -87,6 +90,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { ASSERT_OK(add_static_lease(server, 4)); _cleanup_free_ uint8_t *duped = ASSERT_NOT_NULL(memdup(data, size)); + ASSERT_OK(sd_dhcp_server_start(server)); (void) dhcp_server_handle_message(server, (DHCPMessage*) duped, size, NULL); ASSERT_OK(dhcp_server_save_leases(server)); diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c index 5267137540..772bba4838 100644 --- a/src/libsystemd-network/sd-dhcp-server.c +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -3,6 +3,8 @@ Copyright © 2013 Intel Corporation. All rights reserved. ***/ +#include + #include "sd-dhcp-server.h" #include "sd-event.h" @@ -99,10 +101,7 @@ int sd_dhcp_server_configure_pool( } int sd_dhcp_server_is_running(sd_dhcp_server *server) { - if (!server) - return false; - - return !!server->receive_message; + return server && sd_event_source_get_enabled(server->io_event_source, /* ret= */ NULL) > 0; } static sd_dhcp_server* dhcp_server_free(sd_dhcp_server *server) { @@ -111,6 +110,8 @@ static sd_dhcp_server* dhcp_server_free(sd_dhcp_server *server) { sd_dhcp_server_stop(server); sd_event_unref(server->event); + safe_close(server->socket_fd); + safe_close(server->raw_socket_fd); free(server->boot_server_name); free(server->boot_filename); @@ -149,8 +150,9 @@ int sd_dhcp_server_new(sd_dhcp_server **ret, int ifindex) { *server = (sd_dhcp_server) { .n_ref = 1, - .fd_raw = -EBADF, - .fd = -EBADF, + .ip_service_type = IPTOS_CLASS_CS6, + .socket_fd = -EBADF, + .raw_socket_fd = -EBADF, .address = htobe32(INADDR_ANY), .netmask = htobe32(INADDR_ANY), .ifindex = ifindex, @@ -262,17 +264,13 @@ int sd_dhcp_server_set_boot_filename(sd_dhcp_server *server, const char *filenam } int sd_dhcp_server_stop(sd_dhcp_server *server) { - bool running; - if (!server) return 0; - running = sd_dhcp_server_is_running(server); + bool running = sd_dhcp_server_is_running(server); - server->receive_message = sd_event_source_disable_unref(server->receive_message); - - server->fd_raw = safe_close(server->fd_raw); - server->fd = safe_close(server->fd); + server->raw_socket_fd = safe_close(server->raw_socket_fd); + server->io_event_source = sd_event_source_disable_unref(server->io_event_source); if (running) log_dhcp_server(server, "STOPPED"); @@ -327,15 +325,11 @@ int sd_dhcp_server_start(sd_dhcp_server *server) { assert_return(server, -EINVAL); assert_return(server->event, -EINVAL); + assert_return(server->address != INADDR_ANY, -EUNATCH); if (sd_dhcp_server_is_running(server)) return 0; - assert_return(!server->receive_message, -EBUSY); - assert_return(server->fd_raw < 0, -EBUSY); - assert_return(server->fd < 0, -EBUSY); - assert_return(server->address != htobe32(INADDR_ANY), -EUNATCH); - dhcp_server_update_lease_servers(server); r = dhcp_server_setup_io_event_source(server); diff --git a/src/libsystemd-network/test-dhcp-server.c b/src/libsystemd-network/test-dhcp-server.c index 86face3406..bc9ab1e9ac 100644 --- a/src/libsystemd-network/test-dhcp-server.c +++ b/src/libsystemd-network/test-dhcp-server.c @@ -10,6 +10,7 @@ #include "dhcp-server-internal.h" #include "dhcp-server-request.h" +#include "fd-util.h" #include "tests.h" TEST(basic) { @@ -19,14 +20,12 @@ TEST(basic) { struct in_addr address_any = { .s_addr = htobe32(INADDR_ANY), }; - int r; _cleanup_(sd_event_unrefp) sd_event *event = NULL; ASSERT_OK(sd_event_new(&event)); - /* attach to loopback interface */ _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; - ASSERT_OK(sd_dhcp_server_new(&server, 1)); + ASSERT_OK(sd_dhcp_server_new(&server, 4242)); ASSERT_NOT_NULL(server); ASSERT_OK(sd_dhcp_server_attach_event(server, event, SD_EVENT_PRIORITY_NORMAL)); @@ -49,13 +48,12 @@ TEST(basic) { ASSERT_RETURN_EXPECTED(ASSERT_ERROR(sd_dhcp_server_configure_pool(server, &address_any, 8, 0, 1), EINVAL)); ASSERT_OK(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 1)); - r = sd_dhcp_server_start(server); - /* skip test if running in an environment with no full networking support, CONFIG_PACKET not - * compiled in kernel, nor af_packet module available. */ - if (IN_SET(r, -EPERM, -EAFNOSUPPORT)) - return (void) log_tests_skipped_errno(r, "cannot start dhcp server"); - ASSERT_OK(r); + _cleanup_close_pair_ int socket_fd[2] = EBADF_PAIR; + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, socket_fd)); + server->socket_fd = TAKE_FD(socket_fd[0]); + + ASSERT_OK(sd_dhcp_server_start(server)); ASSERT_OK(sd_dhcp_server_start(server)); ASSERT_OK(sd_dhcp_server_stop(server)); ASSERT_OK(sd_dhcp_server_stop(server)); @@ -112,13 +110,15 @@ TEST(dhcp_server_handle_message) { .s_addr = htobe32(INADDR_LOOPBACK + 42), }; static uint8_t static_lease_client_id[7] = {0x01, 'A', 'B', 'C', 'D', 'E', 'G' }; - int r; + + _cleanup_close_pair_ int socket_fd[2] = EBADF_PAIR; + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, socket_fd)); _cleanup_(sd_event_unrefp) sd_event *event = NULL; ASSERT_OK(sd_event_new(&event)); _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; - ASSERT_OK(sd_dhcp_server_new(&server, 1)); + ASSERT_OK(sd_dhcp_server_new(&server, 4242)); ASSERT_OK(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 0)); ASSERT_OK(sd_dhcp_server_set_static_lease( server, @@ -127,15 +127,10 @@ TEST(dhcp_server_handle_message) { ELEMENTSOF(static_lease_client_id), /* hostname= */ NULL)); ASSERT_OK(sd_dhcp_server_attach_event(server, event, SD_EVENT_PRIORITY_NORMAL)); - r = sd_dhcp_server_start(server); - if (IN_SET(r, -EPERM, -EAFNOSUPPORT)) - return (void) log_tests_skipped_errno(r, "cannot start dhcp server"); - ASSERT_OK(r); + server->socket_fd = TAKE_FD(socket_fd[0]); + ASSERT_OK(sd_dhcp_server_start(server)); - r = dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL); - if (r == -ENETDOWN) - return (void) log_tests_skipped("Network is not available"); - ASSERT_OK_EQ(r, DHCP_OFFER); + ASSERT_OK_EQ(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL), DHCP_OFFER); test.end = 0; /* TODO, shouldn't this fail? */ @@ -273,7 +268,7 @@ TEST(dhcp_server_handle_message) { TEST(sd_dhcp_server_set_static_lease) { _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; - ASSERT_OK(sd_dhcp_server_new(&server, 1)); + ASSERT_OK(sd_dhcp_server_new(&server, 4242)); ASSERT_OK(sd_dhcp_server_set_static_lease( server, @@ -351,7 +346,7 @@ TEST(sd_dhcp_server_set_static_lease) { TEST(sd_dhcp_server_set_domain_name) { _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; - ASSERT_OK(sd_dhcp_server_new(&server, 1)); + ASSERT_OK(sd_dhcp_server_new(&server, 4242)); /* Test setting domain name */ ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, "example.com")); From 29b662184be0debbd54f097423a8e39dca1c9a37 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 7 May 2026 12:41:38 +0900 Subject: [PATCH 06/10] sd-dhcp-server: make IP service type (TOS) configurable --- src/libsystemd-network/sd-dhcp-server.c | 8 ++++++++ src/systemd/sd-dhcp-server.h | 1 + 2 files changed, 9 insertions(+) diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c index 772bba4838..274f0af988 100644 --- a/src/libsystemd-network/sd-dhcp-server.c +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -225,6 +225,14 @@ sd_event* sd_dhcp_server_get_event(sd_dhcp_server *server) { return server->event; } +int sd_dhcp_server_set_ip_service_type(sd_dhcp_server *server, uint8_t type) { + assert_return(server, -EINVAL); + assert_return(!sd_dhcp_server_is_running(server), -EBUSY); + + server->ip_service_type = type; + return 0; +} + int sd_dhcp_server_set_boot_server_address(sd_dhcp_server *server, const struct in_addr *address) { assert_return(server, -EINVAL); diff --git a/src/systemd/sd-dhcp-server.h b/src/systemd/sd-dhcp-server.h index 8c3dbab9b4..15741f96b6 100644 --- a/src/systemd/sd-dhcp-server.h +++ b/src/systemd/sd-dhcp-server.h @@ -53,6 +53,7 @@ int sd_dhcp_server_start(sd_dhcp_server *server); int sd_dhcp_server_stop(sd_dhcp_server *server); int sd_dhcp_server_configure_pool(sd_dhcp_server *server, const struct in_addr *address, unsigned char prefixlen, uint32_t offset, uint32_t size); +int sd_dhcp_server_set_ip_service_type(sd_dhcp_server *server, uint8_t type); int sd_dhcp_server_set_boot_server_address(sd_dhcp_server *server, const struct in_addr *address); int sd_dhcp_server_set_boot_server_name(sd_dhcp_server *server, const char *name); From dae51cc08e2a273481f8563ea56a52083cee53ae Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 4 May 2026 22:08:40 +0900 Subject: [PATCH 07/10] dhcp-server-send: rework sending DHCP reply message This should mostly not change anything, except for some corner cases. Just refactoring. --- src/libsystemd-network/dhcp-server-send.c | 128 +++++++++++----------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/src/libsystemd-network/dhcp-server-send.c b/src/libsystemd-network/dhcp-server-send.c index 11d553404c..a7b827f56e 100644 --- a/src/libsystemd-network/dhcp-server-send.c +++ b/src/libsystemd-network/dhcp-server-send.c @@ -141,42 +141,79 @@ static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination, return 0; } -static bool requested_broadcast(DHCPMessage *message) { - assert(message); - return message->flags & htobe16(0x8000); -} - -static int dhcp_server_send( +static int dhcp_server_send_message( sd_dhcp_server *server, - uint8_t hlen, - const uint8_t *chaddr, - be32_t destination, - uint16_t destination_port, + DHCPRequest *req, + uint8_t type, DHCPPacket *packet, - size_t optoffset, - bool l2_broadcast) { + size_t optoffset) { - if (destination != INADDR_ANY) - return dhcp_server_send_udp(server, destination, - destination_port, &packet->dhcp, - sizeof(DHCPMessage) + optoffset); - else if (l2_broadcast) - return dhcp_server_send_udp(server, INADDR_BROADCAST, - destination_port, &packet->dhcp, - sizeof(DHCPMessage) + optoffset); - else - /* we cannot send UDP packet to specific MAC address when the - address is not yet configured, so must fall back to raw - packets */ - return dhcp_server_send_unicast_raw(server, hlen, chaddr, packet, - sizeof(DHCPPacket) + optoffset); + assert(server); + assert(req); + assert(req->message); + assert(packet); + + /* RFC 2131 Section 4.1 */ + + /* If the ’giaddr’ field in a DHCP message from a client is non-zero, the server sends any + * return messages to the ’DHCP server’ port on the BOOTP relay agent whose address appears + * in ’giaddr’. */ + if (req->message->giaddr != INADDR_ANY) + return dhcp_server_send_udp( + server, + req->message->giaddr, + DHCP_PORT_SERVER, + &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + + /* when ’giaddr’ is zero, the server broadcasts any DHCPNAK messages to 0xffffffff. */ + if (type == DHCP_NAK) + return dhcp_server_send_udp( + server, + INADDR_BROADCAST, + DHCP_PORT_CLIENT, + &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + + /* If the ’giaddr’ field is zero and the ’ciaddr’ field is nonzero, then the server unicasts + * DHCPOFFER and DHCPACK messages to the address in ’ciaddr’. */ + if (req->message->ciaddr != INADDR_ANY) + return dhcp_server_send_udp( + server, + req->message->ciaddr, + DHCP_PORT_CLIENT, + &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + + /* If ’giaddr’ is zero and ’ciaddr’ is zero, and the broadcast bit is set, then the server + * broadcasts DHCPOFFER and DHCPACK messages to 0xffffffff. + * + * (Note, even the broadcast flag is unset, we may not know the client hardware address, e.g. + * InfiniBand. In that case, we cannot unicast in the below, so need to broadcast. Also, broadcast + * the message if 'yiaddr' is zero.) */ + if (FLAGS_SET(be16toh(req->message->flags), 0x8000) || + req->message->hlen == 0 || memeqzero(req->message->chaddr, req->message->hlen) || + packet->dhcp.yiaddr == INADDR_ANY) + return dhcp_server_send_udp( + server, + INADDR_BROADCAST, + DHCP_PORT_CLIENT, + &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + + /* If the broadcast bit is not set and ’giaddr’ is zero and ’ciaddr’ is zero, then the server + * unicasts DHCPOFFER and DHCPACK messages to the client’s hardware address and ’yiaddr’ address. */ + return dhcp_server_send_unicast_raw( + server, + req->message->hlen, + req->message->chaddr, + packet, + sizeof(DHCPPacket) + optoffset); } static int dhcp_server_send_packet(sd_dhcp_server *server, DHCPRequest *req, DHCPPacket *packet, int type, size_t optoffset) { - be32_t destination = INADDR_ANY; - uint16_t destination_port = DHCP_PORT_CLIENT; int r; assert(server); @@ -206,40 +243,7 @@ static int dhcp_server_send_packet(sd_dhcp_server *server, if (r < 0) return r; - /* RFC 2131 Section 4.1 - - If the ’giaddr’ field in a DHCP message from a client is non-zero, - the server sends any return messages to the ’DHCP server’ port on the - BOOTP relay agent whose address appears in ’giaddr’. If the ’giaddr’ - field is zero and the ’ciaddr’ field is nonzero, then the server - unicasts DHCPOFFER and DHCPACK messages to the address in ’ciaddr’. - If ’giaddr’ is zero and ’ciaddr’ is zero, and the broadcast bit is - set, then the server broadcasts DHCPOFFER and DHCPACK messages to - 0xffffffff. If the broadcast bit is not set and ’giaddr’ is zero and - ’ciaddr’ is zero, then the server unicasts DHCPOFFER and DHCPACK - messages to the client’s hardware address and ’yiaddr’ address. In - all cases, when ’giaddr’ is zero, the server broadcasts any DHCPNAK - messages to 0xffffffff. - - Section 4.3.2 - - If ’giaddr’ is set in the DHCPREQUEST message, the client is on a - different subnet. The server MUST set the broadcast bit in the - DHCPNAK, so that the relay agent will broadcast the DHCPNAK to the - client, because the client may not have a correct network address - or subnet mask, and the client may not be answering ARP requests. - */ - if (req->message->giaddr != 0) { - destination = req->message->giaddr; - destination_port = DHCP_PORT_SERVER; - if (type == DHCP_NAK) - packet->dhcp.flags = htobe16(0x8000); - } else if (req->message->ciaddr != 0 && type != DHCP_NAK) - destination = req->message->ciaddr; - - bool l2_broadcast = requested_broadcast(req->message) || type == DHCP_NAK; - return dhcp_server_send(server, req->message->hlen, req->message->chaddr, - destination, destination_port, packet, optoffset, l2_broadcast); + return dhcp_server_send_message(server, req, type, packet, optoffset); } static int server_message_init( From 252f3855fe8e571f76954d106fb6b5a2d8d84fb0 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 5 May 2026 04:55:03 +0900 Subject: [PATCH 08/10] sd-dhcp-server: use struct hw_addr_data to manage client hardware address parsed from DHCP message Then, this drops garbage in DHCP server lease in DBus and Varlink message. This also drops fallback to use client ID as hardware address when chaddr field is not set. In that case, we should broadcast reply. --- .../dhcp-server-lease-internal.h | 4 +- src/libsystemd-network/dhcp-server-request.c | 51 +++---- src/libsystemd-network/dhcp-server-request.h | 4 + src/libsystemd-network/dhcp-server-send.c | 36 ++--- src/libsystemd-network/fuzz-dhcp-server.c | 4 +- src/libsystemd-network/sd-dhcp-server-lease.c | 128 +++++++++--------- src/libsystemd-network/test-dhcp-server.c | 3 +- src/network/networkd-dhcp-server-bus.c | 2 +- 8 files changed, 106 insertions(+), 126 deletions(-) diff --git a/src/libsystemd-network/dhcp-server-lease-internal.h b/src/libsystemd-network/dhcp-server-lease-internal.h index 6d7eb4c213..024279ccbd 100644 --- a/src/libsystemd-network/dhcp-server-lease-internal.h +++ b/src/libsystemd-network/dhcp-server-lease-internal.h @@ -6,6 +6,7 @@ #include "dhcp-client-id-internal.h" #include "dhcp-server-internal.h" #include "dhcp-server-request.h" +#include "ether-addr-util.h" #include "sd-forward.h" typedef struct sd_dhcp_server_lease { @@ -16,10 +17,9 @@ typedef struct sd_dhcp_server_lease { sd_dhcp_client_id client_id; uint8_t htype; /* e.g. ARPHRD_ETHER */ - uint8_t hlen; /* e.g. ETH_ALEN */ + struct hw_addr_data hw_addr; be32_t address; be32_t gateway; - uint8_t chaddr[16]; usec_t expiration; char *hostname; } sd_dhcp_server_lease; diff --git a/src/libsystemd-network/dhcp-server-request.c b/src/libsystemd-network/dhcp-server-request.c index 39c11e69c8..768677e65e 100644 --- a/src/libsystemd-network/dhcp-server-request.c +++ b/src/libsystemd-network/dhcp-server-request.c @@ -54,8 +54,6 @@ int dhcp_request_get_lifetime_timestamp(DHCPRequest *req, clockid_t clock, usec_ } static int ensure_sane_request(sd_dhcp_server *server, DHCPRequest *req, DHCPMessage *message) { - int r; - assert(req); assert(message); @@ -64,41 +62,26 @@ static int ensure_sane_request(sd_dhcp_server *server, DHCPRequest *req, DHCPMes if (message->hlen > sizeof(message->chaddr)) return -EBADMSG; - /* set client id based on MAC address if client did not send an explicit one */ - if (!sd_dhcp_client_id_is_set(&req->client_id)) { - if (!client_id_data_size_is_valid(message->hlen)) - return -EBADMSG; + req->hw_addr.length = req->message->hlen; + memcpy_safe(req->hw_addr.bytes, message->chaddr, message->hlen); - r = sd_dhcp_client_id_set(&req->client_id, /* type= */ 1, message->chaddr, message->hlen); - if (r < 0) - return r; - } + /* Fake client ID generated from the DHCP header. + * The client ID type 0 and 255 are special. So do not use if htype is 0 or 255. + * Note, Some hardware type (e.g. Infiniband) may not set chaddr field. */ + if (!IN_SET(req->message->htype, 0, UINT8_MAX)) + (void) sd_dhcp_client_id_set( + &req->client_id_by_header, + req->message->htype, + req->message->chaddr, + req->message->hlen); - if (message->hlen == 0 || memeqzero(message->chaddr, message->hlen)) { - uint8_t type; - const void *data; - size_t size; + /* If Client Identifier option is unspecified, use the generated one. */ + if (!sd_dhcp_client_id_is_set(&req->client_id)) + req->client_id = req->client_id_by_header; - /* See RFC2131 section 4.1.1. - * hlen and chaddr may not be set for non-ethernet interface. - * Let's try to retrieve it from the client ID. */ - - if (!sd_dhcp_client_id_is_set(&req->client_id)) - return -EBADMSG; - - r = sd_dhcp_client_id_get(&req->client_id, &type, &data, &size); - if (r < 0) - return r; - - if (type != 1) - return -EBADMSG; - - if (size > sizeof(message->chaddr)) - return -EBADMSG; - - memcpy(message->chaddr, data, size); - message->hlen = size; - } + /* We manage bound leases by client ID. Hence, at least one of them is necessary. */ + if (!sd_dhcp_client_id_is_set(&req->client_id)) + return -EBADMSG; if (req->max_optlen < DHCP_MIN_OPTIONS_SIZE) req->max_optlen = DHCP_MIN_OPTIONS_SIZE; diff --git a/src/libsystemd-network/dhcp-server-request.h b/src/libsystemd-network/dhcp-server-request.h index f1e827a0e1..c9ffe2aba8 100644 --- a/src/libsystemd-network/dhcp-server-request.h +++ b/src/libsystemd-network/dhcp-server-request.h @@ -3,6 +3,7 @@ #include "dhcp-client-id-internal.h" #include "dhcp-protocol.h" +#include "ether-addr-util.h" #include "sd-forward.h" #include "sparse-endian.h" #include "time-util.h" @@ -10,10 +11,13 @@ typedef struct DHCPRequest { /* received message */ DHCPMessage *message; + /* sender hardware address, may not be set for non-ethernet interface */ + struct hw_addr_data hw_addr; triple_timestamp timestamp; /* options */ sd_dhcp_client_id client_id; + sd_dhcp_client_id client_id_by_header; size_t max_optlen; be32_t server_id; be32_t requested_ip; diff --git a/src/libsystemd-network/dhcp-server-send.c b/src/libsystemd-network/dhcp-server-send.c index a7b827f56e..3fd548392d 100644 --- a/src/libsystemd-network/dhcp-server-send.c +++ b/src/libsystemd-network/dhcp-server-send.c @@ -44,8 +44,7 @@ static int server_acquire_raw_socket(sd_dhcp_server *server) { static int dhcp_server_send_unicast_raw( sd_dhcp_server *server, - uint8_t hlen, - const uint8_t *chaddr, + const struct hw_addr_data *hw_addr, DHCPPacket *packet, size_t len) { @@ -54,8 +53,7 @@ static int dhcp_server_send_unicast_raw( assert(server); assert(server->ifindex > 0); assert(server->address != 0); - assert(hlen > 0); - assert(chaddr); + assert(hw_addr); assert(packet); assert(len > sizeof(DHCPPacket)); @@ -81,10 +79,10 @@ static int dhcp_server_send_unicast_raw( .ll.sll_family = AF_PACKET, .ll.sll_protocol = htobe16(ETH_P_IP), .ll.sll_ifindex = server->ifindex, - .ll.sll_halen = hlen, + .ll.sll_halen = hw_addr->length, }; - memcpy(sa.ll.sll_addr, chaddr, hlen); + memcpy_safe(sa.ll.sll_addr, hw_addr->bytes, hw_addr->length); struct msghdr mh = { .msg_name = &sa.sa, @@ -192,7 +190,7 @@ static int dhcp_server_send_message( * InfiniBand. In that case, we cannot unicast in the below, so need to broadcast. Also, broadcast * the message if 'yiaddr' is zero.) */ if (FLAGS_SET(be16toh(req->message->flags), 0x8000) || - req->message->hlen == 0 || memeqzero(req->message->chaddr, req->message->hlen) || + hw_addr_is_null(&req->hw_addr) || packet->dhcp.yiaddr == INADDR_ANY) return dhcp_server_send_udp( server, @@ -205,8 +203,7 @@ static int dhcp_server_send_message( * unicasts DHCPOFFER and DHCPACK messages to the client’s hardware address and ’yiaddr’ address. */ return dhcp_server_send_unicast_raw( server, - req->message->hlen, - req->message->chaddr, + &req->hw_addr, packet, sizeof(DHCPPacket) + optoffset); } @@ -269,7 +266,7 @@ static int server_message_init( r = dhcp_message_init(&packet->dhcp, BOOTREPLY, be32toh(req->message->xid), - req->message->htype, req->message->hlen, req->message->chaddr, + req->message->htype, req->hw_addr.length, req->hw_addr.bytes, type, req->max_optlen, &optoffset); if (r < 0) return r; @@ -534,28 +531,23 @@ int server_send_nak_or_ignore(sd_dhcp_server *server, bool init_reboot, DHCPRequ return DHCP_NAK; } -static int server_send_forcerenew( +static int dhcp_server_send_forcerenew( sd_dhcp_server *server, - be32_t address, - be32_t gateway, - uint8_t htype, - uint8_t hlen, - const uint8_t *chaddr) { + sd_dhcp_server_lease *lease) { _cleanup_free_ DHCPPacket *packet = NULL; size_t optoffset = 0; int r; assert(server); - assert(address != INADDR_ANY); - assert(chaddr); + assert(lease); packet = malloc0(sizeof(DHCPPacket) + DHCP_MIN_OPTIONS_SIZE); if (!packet) return -ENOMEM; r = dhcp_message_init(&packet->dhcp, BOOTREPLY, 0, - htype, hlen, chaddr, DHCP_FORCERENEW, + lease->htype, lease->hw_addr.length, lease->hw_addr.bytes, DHCP_FORCERENEW, DHCP_MIN_OPTIONS_SIZE, &optoffset); if (r < 0) return r; @@ -565,7 +557,7 @@ static int server_send_forcerenew( if (r < 0) return r; - return dhcp_server_send_udp(server, address, DHCP_PORT_CLIENT, + return dhcp_server_send_udp(server, lease->address, DHCP_PORT_CLIENT, &packet->dhcp, sizeof(DHCPMessage) + optoffset); } @@ -579,8 +571,6 @@ int sd_dhcp_server_forcerenew(sd_dhcp_server *server) { log_dhcp_server(server, "FORCERENEW"); HASHMAP_FOREACH(lease, server->bound_leases_by_client_id) - RET_GATHER(r, - server_send_forcerenew(server, lease->address, lease->gateway, - lease->htype, lease->hlen, lease->chaddr)); + RET_GATHER(r, dhcp_server_send_forcerenew(server, lease)); return r; } diff --git a/src/libsystemd-network/fuzz-dhcp-server.c b/src/libsystemd-network/fuzz-dhcp-server.c index ac006bc2aa..34c1f8371a 100644 --- a/src/libsystemd-network/fuzz-dhcp-server.c +++ b/src/libsystemd-network/fuzz-dhcp-server.c @@ -27,10 +27,10 @@ static int add_lease(sd_dhcp_server *server, const struct in_addr *server_addres *lease = (sd_dhcp_server_lease) { .n_ref = 1, .address = htobe32(UINT32_C(10) << 24 | i), - .chaddr = { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 }, + .hw_addr.length = ETH_ALEN, + .hw_addr.bytes = { 3, 3, 3, 3, 3, 3, }, .expiration = usec_add(now(CLOCK_BOOTTIME), USEC_PER_DAY), .gateway = server_address->s_addr, - .hlen = ETH_ALEN, .htype = ARPHRD_ETHER, .client_id.size = 2, diff --git a/src/libsystemd-network/sd-dhcp-server-lease.c b/src/libsystemd-network/sd-dhcp-server-lease.c index a7d9947c29..58b5831e44 100644 --- a/src/libsystemd-network/sd-dhcp-server-lease.c +++ b/src/libsystemd-network/sd-dhcp-server-lease.c @@ -16,7 +16,6 @@ #include "iovec-util.h" #include "json-util.h" #include "mkdir.h" -#include "string-util.h" #include "tmpfile-util.h" static sd_dhcp_server_lease* dhcp_server_lease_free(sd_dhcp_server_lease *lease) { @@ -103,12 +102,11 @@ int dhcp_server_set_lease(sd_dhcp_server *server, be32_t address, DHCPRequest *r .address = address, .client_id = req->client_id, .htype = req->message->htype, - .hlen = req->message->hlen, .gateway = req->message->giaddr, .expiration = expiration, }; - memcpy(lease->chaddr, req->message->chaddr, req->message->hlen); + lease->hw_addr = req->hw_addr; if (req->hostname) { lease->hostname = strdup(req->hostname); @@ -146,27 +144,17 @@ int dhcp_server_cleanup_expired_leases(sd_dhcp_server *server) { sd_dhcp_server_lease* dhcp_server_get_static_lease(sd_dhcp_server *server, const DHCPRequest *req) { sd_dhcp_server_lease *static_lease; - sd_dhcp_client_id client_id; assert(server); assert(req); static_lease = hashmap_get(server->static_leases_by_client_id, &req->client_id); - if (static_lease) - goto verify; - - /* when no lease is found based on the client id fall back to chaddr */ - if (!client_id_data_size_is_valid(req->message->hlen)) - return NULL; - - if (sd_dhcp_client_id_set(&client_id, /* type= */ 1, req->message->chaddr, req->message->hlen) < 0) - return NULL; - - static_lease = hashmap_get(server->static_leases_by_client_id, &client_id); + if (!static_lease && sd_dhcp_client_id_is_set(&req->client_id_by_header)) + /* when no lease is found, fall back to use the fake client ID generated from the header. */ + static_lease = hashmap_get(server->static_leases_by_client_id, &req->client_id_by_header); if (!static_lease) return NULL; -verify: /* Check if the address is in the same subnet. */ if ((static_lease->address & server->netmask) != server->subnet) return NULL; @@ -247,8 +235,8 @@ static int dhcp_server_lease_build_json(sd_dhcp_server_lease *lease, sd_json_var JSON_BUILD_PAIR_IN4_ADDR_WITH_STRING_NON_NULL("Address", &(struct in_addr) { .s_addr = lease->address }), JSON_BUILD_PAIR_STRING_NON_EMPTY("Hostname", lease->hostname), SD_JSON_BUILD_PAIR_UNSIGNED("HardwareAddressType", lease->htype), - SD_JSON_BUILD_PAIR_UNSIGNED("HardwareAddressLength", lease->hlen), - SD_JSON_BUILD_PAIR_BYTE_ARRAY("HardwareAddress", lease->chaddr, sizeof(lease->chaddr))); + SD_JSON_BUILD_PAIR_UNSIGNED("HardwareAddressLength", lease->hw_addr.length), + SD_JSON_BUILD_PAIR_BYTE_ARRAY("HardwareAddress", lease->hw_addr.bytes, lease->hw_addr.length)); } int dhcp_server_bound_leases_append_json(sd_dhcp_server *server, sd_json_variant **v) { @@ -376,70 +364,67 @@ int dhcp_server_save_leases(sd_dhcp_server *server) { return 0; } -static int json_dispatch_chaddr(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { - uint8_t* address = ASSERT_PTR(userdata); - _cleanup_(iovec_done) struct iovec iov = {}; - int r; +typedef struct LeaseParam { + sd_dhcp_client_id client_id; + uint8_t htype; + uint8_t hlen; + struct iovec hw_addr; + struct in_addr address; + usec_t exp_b; + usec_t exp_r; + char *hostname; +} LeaseParam; - r = json_dispatch_byte_array_iovec(name, variant, flags, &iov); - if (r < 0) - return r; +static void lease_param_done(LeaseParam *p) { + assert(p); - if (iov.iov_len != 16) - return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is array of unexpected size.", strna(name)); - - memcpy(address, iov.iov_base, iov.iov_len); - return 0; + iovec_done(&p->hw_addr); + free(p->hostname); } static int json_dispatch_dhcp_lease(sd_dhcp_server *server, sd_json_variant *v, bool use_boottime) { - static const sd_json_dispatch_field dispatch_table_boottime[] = { - { "ClientId", SD_JSON_VARIANT_ARRAY, json_dispatch_client_id, offsetof(sd_dhcp_server_lease, client_id), SD_JSON_MANDATORY }, - { "Address", SD_JSON_VARIANT_ARRAY, json_dispatch_in_addr, offsetof(sd_dhcp_server_lease, address), SD_JSON_MANDATORY }, - { "Hostname", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(sd_dhcp_server_lease, hostname), 0 }, - { "HardwareAddressType", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint8, offsetof(sd_dhcp_server_lease, htype), 0 }, - { "HardwareAddressLength", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint8, offsetof(sd_dhcp_server_lease, hlen), 0 }, - { "HardwareAddress", SD_JSON_VARIANT_ARRAY, json_dispatch_chaddr, offsetof(sd_dhcp_server_lease, chaddr), 0 }, - { "ExpirationUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(sd_dhcp_server_lease, expiration), SD_JSON_MANDATORY }, - { "ExpirationRealtimeUSec", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, SD_JSON_MANDATORY }, - {} - }, dispatch_table_realtime[] = { - { "ClientId", SD_JSON_VARIANT_ARRAY, json_dispatch_client_id, offsetof(sd_dhcp_server_lease, client_id), SD_JSON_MANDATORY }, - { "Address", SD_JSON_VARIANT_ARRAY, json_dispatch_in_addr, offsetof(sd_dhcp_server_lease, address), SD_JSON_MANDATORY }, - { "Hostname", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(sd_dhcp_server_lease, hostname), 0 }, - { "HardwareAddressType", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint8, offsetof(sd_dhcp_server_lease, htype), 0 }, - { "HardwareAddressLength", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint8, offsetof(sd_dhcp_server_lease, hlen), 0 }, - { "HardwareAddress", SD_JSON_VARIANT_ARRAY, json_dispatch_chaddr, offsetof(sd_dhcp_server_lease, chaddr), 0 }, - { "ExpirationUSec", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, SD_JSON_MANDATORY }, - { "ExpirationRealtimeUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(sd_dhcp_server_lease, expiration), SD_JSON_MANDATORY }, + static const sd_json_dispatch_field dispatch_table[] = { + { "ClientId", SD_JSON_VARIANT_ARRAY, json_dispatch_client_id, offsetof(LeaseParam, client_id), SD_JSON_MANDATORY }, + { "HardwareAddressType", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint8, offsetof(LeaseParam, htype), 0 }, + { "HardwareAddressLength", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint8, offsetof(LeaseParam, hlen), 0 }, + { "HardwareAddress", SD_JSON_VARIANT_ARRAY, json_dispatch_byte_array_iovec, offsetof(LeaseParam, hw_addr), 0 }, + { "Address", SD_JSON_VARIANT_ARRAY, json_dispatch_in_addr, offsetof(LeaseParam, address), SD_JSON_MANDATORY }, + { "AddressString", SD_JSON_VARIANT_STRING, NULL, 0, 0 }, + { "ExpirationUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(LeaseParam, exp_b), SD_JSON_MANDATORY }, + { "ExpirationRealtimeUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(LeaseParam, exp_r), SD_JSON_MANDATORY }, + { "Hostname", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(LeaseParam, hostname), 0 }, {} }; - _cleanup_(sd_dhcp_server_lease_unrefp) sd_dhcp_server_lease *lease = NULL; - usec_t now_b; int r; assert(server); assert(v); - lease = new(sd_dhcp_server_lease, 1); - if (!lease) - return -ENOMEM; - - *lease = (sd_dhcp_server_lease) { - .n_ref = 1, - }; - - r = sd_json_dispatch(v, use_boottime ? dispatch_table_boottime : dispatch_table_realtime, SD_JSON_ALLOW_EXTENSIONS, lease); + _cleanup_(lease_param_done) LeaseParam p = {}; + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) return r; + if (p.hlen > HW_ADDR_MAX_SIZE) + return -EINVAL; + + if (p.hlen > p.hw_addr.iov_len) + return -EINVAL; + + if (!in4_addr_is_set(&p.address)) + return -EINVAL; + + if (!sd_dhcp_client_id_is_set(&p.client_id)) + return -EINVAL; + + usec_t now_b; r = sd_event_now(server->event, CLOCK_BOOTTIME, &now_b); if (r < 0) return r; if (use_boottime) { - if (lease->expiration < now_b) + if (p.exp_b < now_b) return 0; /* already expired */ } else { usec_t now_r; @@ -448,12 +433,29 @@ static int json_dispatch_dhcp_lease(sd_dhcp_server *server, sd_json_variant *v, if (r < 0) return r; - if (lease->expiration < now_r) + if (p.exp_r < now_r) return 0; /* already expired */ - lease->expiration = map_clock_usec_raw(lease->expiration, now_r, now_b); + p.exp_b = map_clock_usec_raw(p.exp_r, now_r, now_b); } + _cleanup_(sd_dhcp_server_lease_unrefp) sd_dhcp_server_lease *lease = new(sd_dhcp_server_lease, 1); + if (!lease) + return -ENOMEM; + + *lease = (sd_dhcp_server_lease) { + .n_ref = 1, + + .client_id = p.client_id, + .htype = p.htype, + .hw_addr.length = p.hlen, + .address = p.address.s_addr, + .expiration = p.exp_b, + .hostname = TAKE_PTR(p.hostname), + }; + + memcpy_safe(lease->hw_addr.bytes, p.hw_addr.iov_base, p.hlen); + r = dhcp_server_put_lease(server, lease, /* is_static= */ false); if (r == -EEXIST) return 0; diff --git a/src/libsystemd-network/test-dhcp-server.c b/src/libsystemd-network/test-dhcp-server.c index bc9ab1e9ac..a4f025bd41 100644 --- a/src/libsystemd-network/test-dhcp-server.c +++ b/src/libsystemd-network/test-dhcp-server.c @@ -152,8 +152,9 @@ TEST(dhcp_server_handle_message) { test.header.op = BOOTREQUEST; ASSERT_OK_EQ(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL), DHCP_OFFER); + /* Neither client ID nor hardware type is set. There is no way to manage the bound lease for the request. */ test.header.htype = 0; - ASSERT_OK_EQ(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL), DHCP_OFFER); + ASSERT_ERROR(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL), EBADMSG); test.header.htype = ARPHRD_ETHER; ASSERT_OK_EQ(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL), DHCP_OFFER); diff --git a/src/network/networkd-dhcp-server-bus.c b/src/network/networkd-dhcp-server-bus.c index 58e0a26f22..d5829b6dfb 100644 --- a/src/network/networkd-dhcp-server-bus.c +++ b/src/network/networkd-dhcp-server-bus.c @@ -55,7 +55,7 @@ static int property_get_leases( if (r < 0) return r; - r = sd_bus_message_append_array(reply, 'y', &lease->chaddr, sizeof(lease->chaddr)); + r = sd_bus_message_append_array(reply, 'y', &lease->hw_addr.bytes, lease->hw_addr.length); if (r < 0) return r; From e359c6df6589da33c0b7b0448355a95dcb70a8bd Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 7 May 2026 11:59:23 +0900 Subject: [PATCH 09/10] sd-dhcp-server: store more information in DHCPRequest This makes DHCPRequest stores - the message type of the received message, - acquired address, - found static DHCP lease, This also moves call of dhcp_request_get_lifetime_timestamp() from dhcp_server_ack() to dhcp_server_set_lease(), and rename DHCPRequest.server_id -> .server_address. No functional change, just refactoring. --- .../dhcp-server-lease-internal.h | 2 +- src/libsystemd-network/dhcp-server-request.c | 53 +++++++++---------- src/libsystemd-network/dhcp-server-request.h | 9 +++- src/libsystemd-network/dhcp-server-send.c | 15 +++--- src/libsystemd-network/dhcp-server-send.h | 1 - src/libsystemd-network/sd-dhcp-server-lease.c | 16 +++--- 6 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/libsystemd-network/dhcp-server-lease-internal.h b/src/libsystemd-network/dhcp-server-lease-internal.h index 024279ccbd..f347837bd8 100644 --- a/src/libsystemd-network/dhcp-server-lease-internal.h +++ b/src/libsystemd-network/dhcp-server-lease-internal.h @@ -28,7 +28,7 @@ extern const struct hash_ops dhcp_server_lease_hash_ops; int dhcp_server_put_lease(sd_dhcp_server *server, sd_dhcp_server_lease *lease, bool is_static); -int dhcp_server_set_lease(sd_dhcp_server *server, be32_t address, DHCPRequest *req, usec_t expiration); +int dhcp_server_set_lease(sd_dhcp_server *server, DHCPRequest *req); int dhcp_server_cleanup_expired_leases(sd_dhcp_server *server); sd_dhcp_server_lease* dhcp_server_get_static_lease(sd_dhcp_server *server, const DHCPRequest *req); diff --git a/src/libsystemd-network/dhcp-server-request.c b/src/libsystemd-network/dhcp-server-request.c index 768677e65e..202a8b888c 100644 --- a/src/libsystemd-network/dhcp-server-request.c +++ b/src/libsystemd-network/dhcp-server-request.c @@ -112,7 +112,7 @@ static int parse_request(uint8_t code, uint8_t len, const void *option, void *us break; case SD_DHCP_OPTION_SERVER_IDENTIFIER: if (len == 4) - memcpy(&req->server_id, option, sizeof(be32_t)); + memcpy(&req->server_address, option, sizeof(be32_t)); break; case SD_DHCP_OPTION_CLIENT_IDENTIFIER: @@ -169,7 +169,7 @@ static int dhcp_server_parse_message(sd_dhcp_server *server, DHCPMessage *messag r = dhcp_option_parse(message, length, parse_request, req, &error_message); if (r < 0) return r; - int type = r; + req->type = r; r = ensure_sane_request(server, req, message); if (r < 0) @@ -177,26 +177,21 @@ static int dhcp_server_parse_message(sd_dhcp_server *server, DHCPMessage *messag *ret = TAKE_PTR(req); *ret_error_message = TAKE_PTR(error_message); - return type; + return 0; } -static int dhcp_server_ack(sd_dhcp_server *server, DHCPRequest *req, be32_t address) { - usec_t expiration; +static int dhcp_server_ack(sd_dhcp_server *server, DHCPRequest *req) { int r; assert(server); assert(req); - assert(address != 0); + assert(req->address != INADDR_ANY); - r = dhcp_request_get_lifetime_timestamp(req, CLOCK_BOOTTIME, &expiration); - if (r < 0) - return r; - - r = dhcp_server_set_lease(server, address, req, expiration); + r = dhcp_server_set_lease(server, req); if (r < 0) return log_dhcp_server_errno(server, r, "Failed to create new lease: %m"); - r = server_send_offer_or_ack(server, req, address, DHCP_ACK); + r = server_send_offer_or_ack(server, req, DHCP_ACK); if (r < 0) return log_dhcp_server_errno(server, r, "Could not send ACK: %m"); @@ -223,8 +218,6 @@ static int dhcp_server_process_discover(sd_dhcp_server *server, DHCPRequest *req /* no pool allocated */ return 0; - be32_t address = INADDR_ANY; - /* for now pick a random free address from the pool */ if (static_lease) { sd_dhcp_server_lease *l = hashmap_get(server->bound_leases_by_address, UINT32_TO_PTR(static_lease->address)); @@ -233,12 +226,13 @@ static int dhcp_server_process_discover(sd_dhcp_server *server, DHCPRequest *req return 0; /* Found a matching static lease. */ - address = static_lease->address; + req->static_lease = static_lease; + req->address = static_lease->address; } else if (existing_lease && dhcp_server_address_is_in_pool(server, existing_lease->address)) /* If we previously assigned an address to the host, then reuse it. */ - address = existing_lease->address; + req->address = existing_lease->address; else { struct siphash state; @@ -257,20 +251,20 @@ static int dhcp_server_process_discover(sd_dhcp_server *server, DHCPRequest *req for (unsigned i = 0; i < server->pool_size; i++) { be32_t a = server->subnet | htobe32(server->pool_offset + (hash + i) % server->pool_size); if (dhcp_server_address_available(server, a)) { - address = a; + req->address = a; break; } } } - if (address == INADDR_ANY) + if (req->address == INADDR_ANY) /* no free addresses left */ return 0; if (server->rapid_commit && req->rapid_commit) - return dhcp_server_ack(server, req, address); + return dhcp_server_ack(server, req); - r = server_send_offer_or_ack(server, req, address, DHCP_OFFER); + r = server_send_offer_or_ack(server, req, DHCP_OFFER); if (r < 0) /* this only fails on critical errors */ return log_dhcp_server_errno(server, r, "Could not send offer: %m"); @@ -292,12 +286,12 @@ static int dhcp_server_process_request(sd_dhcp_server *server, DHCPRequest *req) /* see RFC 2131, section 4.3.2 */ - if (req->server_id != 0) { + if (req->server_address != INADDR_ANY) { log_dhcp_server(server, "REQUEST (selecting) (0x%x)", be32toh(req->message->xid)); /* SELECTING */ - if (req->server_id != server->address) + if (req->server_address != server->address) /* client did not pick us */ return 0; @@ -348,13 +342,19 @@ static int dhcp_server_process_request(sd_dhcp_server *server, DHCPRequest *req) /* The requested address is already assigned to another host. Refusing. */ return server_send_nak_or_ignore(server, init_reboot, req); + req->static_lease = static_lease; + req->address = address; + /* Found a static lease for the client ID. */ - return dhcp_server_ack(server, req, address); + return dhcp_server_ack(server, req); } - if (dhcp_server_address_is_in_pool(server, address)) + if (dhcp_server_address_is_in_pool(server, address)) { /* The requested address is in the pool. */ - return dhcp_server_ack(server, req, address); + req->address = address; + + return dhcp_server_ack(server, req); + } /* Refuse otherwise. */ return server_send_nak_or_ignore(server, init_reboot, req); @@ -408,7 +408,6 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, siz r = dhcp_server_parse_message(server, message, length, &req, &error_message); if (r < 0) return r; - int type = r; dhcp_request_set_timestamp(req, timestamp); @@ -416,7 +415,7 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, siz if (r < 0) return r; - switch (type) { + switch (req->type) { case DHCP_DISCOVER: return dhcp_server_process_discover(server, req); case DHCP_REQUEST: diff --git a/src/libsystemd-network/dhcp-server-request.h b/src/libsystemd-network/dhcp-server-request.h index c9ffe2aba8..e5738978a2 100644 --- a/src/libsystemd-network/dhcp-server-request.h +++ b/src/libsystemd-network/dhcp-server-request.h @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "sd-dhcp-server-lease.h" + #include "dhcp-client-id-internal.h" #include "dhcp-protocol.h" #include "ether-addr-util.h" @@ -16,10 +18,11 @@ typedef struct DHCPRequest { triple_timestamp timestamp; /* options */ + uint8_t type; sd_dhcp_client_id client_id; sd_dhcp_client_id client_id_by_header; size_t max_optlen; - be32_t server_id; + be32_t server_address; be32_t requested_ip; usec_t lifetime; const uint8_t *agent_info_option; @@ -27,6 +30,10 @@ typedef struct DHCPRequest { const uint8_t *parameter_request_list; size_t parameter_request_list_len; bool rapid_commit; + + /* acquired data */ + sd_dhcp_server_lease *static_lease; + be32_t address; } DHCPRequest; int dhcp_request_get_lifetime_timestamp(DHCPRequest *req, clockid_t clock, usec_t *ret); diff --git a/src/libsystemd-network/dhcp-server-send.c b/src/libsystemd-network/dhcp-server-send.c index 3fd548392d..9f52ff4cc8 100644 --- a/src/libsystemd-network/dhcp-server-send.c +++ b/src/libsystemd-network/dhcp-server-send.c @@ -286,7 +286,6 @@ static int dhcp_server_append_static_hostname( size_t *offset, DHCPRequest *req) { - sd_dhcp_server_lease *static_lease; int r; assert(server); @@ -294,11 +293,10 @@ static int dhcp_server_append_static_hostname( assert(offset); assert(req); - static_lease = dhcp_server_get_static_lease(server, req); - if (!static_lease || !static_lease->hostname) + if (!req->static_lease || !req->static_lease->hostname) return 0; - if (dns_name_is_single_label(static_lease->hostname)) + if (dns_name_is_single_label(req->static_lease->hostname)) /* Option 12 */ return dhcp_option_append( &packet->dhcp, @@ -306,8 +304,8 @@ static int dhcp_server_append_static_hostname( offset, /* overload= */ 0, SD_DHCP_OPTION_HOST_NAME, - strlen(static_lease->hostname), - static_lease->hostname); + strlen(req->static_lease->hostname), + req->static_lease->hostname); /* Option 81 */ @@ -322,7 +320,7 @@ static int dhcp_server_append_static_hostname( buffer[1] = 255; buffer[2] = 255; - r = dns_name_to_wire_format(static_lease->hostname, buffer + 3, sizeof(buffer) - 3, false); + r = dns_name_to_wire_format(req->static_lease->hostname, buffer + 3, sizeof(buffer) - 3, false); if (r < 0) return log_dhcp_server_errno(server, r, "Failed to encode FQDN for static lease: %m"); if (r > DHCP_MAX_FQDN_LENGTH) @@ -350,7 +348,6 @@ static bool dhcp_request_contains(DHCPRequest *req, uint8_t option) { int server_send_offer_or_ack( sd_dhcp_server *server, DHCPRequest *req, - be32_t address, uint8_t type) { static const uint8_t option_map[_SD_DHCP_LEASE_SERVER_TYPE_MAX] = { @@ -375,7 +372,7 @@ int server_send_offer_or_ack( if (r < 0) return r; - packet->dhcp.yiaddr = address; + packet->dhcp.yiaddr = req->address; packet->dhcp.siaddr = server->boot_server_address.s_addr; lease_time = usec_to_be32_sec(req->lifetime); diff --git a/src/libsystemd-network/dhcp-server-send.h b/src/libsystemd-network/dhcp-server-send.h index 80fd9a6b94..27801b64ec 100644 --- a/src/libsystemd-network/dhcp-server-send.h +++ b/src/libsystemd-network/dhcp-server-send.h @@ -8,7 +8,6 @@ int server_send_offer_or_ack( sd_dhcp_server *server, DHCPRequest *req, - be32_t address, uint8_t type); int server_send_nak_or_ignore(sd_dhcp_server *server, bool init_reboot, DHCPRequest *req); diff --git a/src/libsystemd-network/sd-dhcp-server-lease.c b/src/libsystemd-network/sd-dhcp-server-lease.c index 58b5831e44..4ab7897264 100644 --- a/src/libsystemd-network/sd-dhcp-server-lease.c +++ b/src/libsystemd-network/sd-dhcp-server-lease.c @@ -64,21 +64,25 @@ int dhcp_server_put_lease(sd_dhcp_server *server, sd_dhcp_server_lease *lease, b return 0; } -int dhcp_server_set_lease(sd_dhcp_server *server, be32_t address, DHCPRequest *req, usec_t expiration) { +int dhcp_server_set_lease(sd_dhcp_server *server, DHCPRequest *req) { _cleanup_(sd_dhcp_server_lease_unrefp) sd_dhcp_server_lease *lease = NULL; int r; assert(server); - assert(address != 0); assert(req); - assert(expiration != 0); + assert(req->address != INADDR_ANY); + + usec_t expiration; + r = dhcp_request_get_lifetime_timestamp(req, CLOCK_BOOTTIME, &expiration); + if (r < 0) + return r; /* If a lease for the host already exists, update it. */ lease = hashmap_get(server->bound_leases_by_client_id, &req->client_id); if (lease) { - if (lease->address != address) { + if (lease->address != req->address) { hashmap_remove_value(server->bound_leases_by_address, UINT32_TO_PTR(lease->address), lease); - lease->address = address; + lease->address = req->address; r = hashmap_ensure_put(&server->bound_leases_by_address, NULL, UINT32_TO_PTR(lease->address), lease); if (r < 0) @@ -99,7 +103,7 @@ int dhcp_server_set_lease(sd_dhcp_server *server, be32_t address, DHCPRequest *r *lease = (sd_dhcp_server_lease) { .n_ref = 1, - .address = address, + .address = req->address, .client_id = req->client_id, .htype = req->message->htype, .gateway = req->message->giaddr, From d40d021ca5e2b178ac542c70052b6621e7baec97 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 7 May 2026 12:19:25 +0900 Subject: [PATCH 10/10] dhcp-server-request: check server address in DHCPDECLINE and DHCPRELEASE Otherwise, we may do something wrong by messages for another DHCP server. Let's silently ignore messages with unmatching server identifier. Also, logs something when we receive DHCPRELEASE but found lease does not match the reported address. --- src/libsystemd-network/dhcp-server-request.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libsystemd-network/dhcp-server-request.c b/src/libsystemd-network/dhcp-server-request.c index 202a8b888c..6ce68dab40 100644 --- a/src/libsystemd-network/dhcp-server-request.c +++ b/src/libsystemd-network/dhcp-server-request.c @@ -364,10 +364,12 @@ static int dhcp_server_process_decline(sd_dhcp_server *server, DHCPRequest *req, assert(server); assert(req); + if (req->server_address != server->address) + return 0; /* The message is not for us. Let's silently ignore the packet. */ + + /* TODO: make sure we don't offer this address again for a while. */ + log_dhcp_server(server, "DECLINE (0x%x): %s", be32toh(req->message->xid), strna(error_message)); - - /* TODO: make sure we don't offer this address again */ - return 0; } @@ -375,19 +377,20 @@ static int dhcp_server_process_release(sd_dhcp_server *server, DHCPRequest *req) assert(server); assert(req); - log_dhcp_server(server, "RELEASE (0x%x)", - be32toh(req->message->xid)); + if (req->server_address != server->address) + return 0; /* The message is not for us. Let's silently ignore the packet. */ sd_dhcp_server_lease *existing_lease = hashmap_get(server->bound_leases_by_client_id, &req->client_id); if (!existing_lease) return 0; if (existing_lease->address != req->message->ciaddr) - return 0; + return -EBADMSG; sd_dhcp_server_lease_unref(existing_lease); dhcp_server_on_lease_change(server); + log_dhcp_server(server, "RELEASE (0x%x)", be32toh(req->message->xid)); return 0; }