From: Yu Watanabe Date: Tue, 31 Mar 2026 16:45:16 +0000 (+0900) Subject: sd-dhcp-lease: introduce dhcp_client_parse_message() X-Git-Tag: v261-rc1~145 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=26b7c5ff3b944aa3a16d4e859e9c84ce7e968a5a;p=thirdparty%2Fsystemd.git sd-dhcp-lease: introduce dhcp_client_parse_message() It parses a received DHCP message and create corresponding sd_dhcp_lease object for the message. Internally, it uses sd_dhcp_message object. Note, the new parser is not used yet. The currently used one will be replaced in later commit. --- diff --git a/src/libsystemd-network/dhcp-lease-internal.h b/src/libsystemd-network/dhcp-lease-internal.h index bc411a4f36c..6a562901142 100644 --- a/src/libsystemd-network/dhcp-lease-internal.h +++ b/src/libsystemd-network/dhcp-lease-internal.h @@ -6,10 +6,11 @@ ***/ #include "sd-dhcp-lease.h" +#include "sd-forward.h" #include "dhcp-client-id-internal.h" +#include "dhcp-message.h" #include "dhcp-option.h" -#include "sd-forward.h" #include "list.h" struct sd_dhcp_raw_option { @@ -23,6 +24,8 @@ struct sd_dhcp_raw_option { struct sd_dhcp_lease { unsigned n_ref; + sd_dhcp_message *message; + /* each 0 if unset */ usec_t t1; usec_t t2; @@ -87,5 +90,7 @@ void dhcp_lease_set_timestamp(sd_dhcp_lease *lease, const triple_timestamp *time int dhcp_lease_set_default_subnet_mask(sd_dhcp_lease *lease); int dhcp_lease_set_client_id(sd_dhcp_lease *lease, const sd_dhcp_client_id *client_id); +int dhcp_client_parse_message(sd_dhcp_client *client, const struct iovec *iov, sd_dhcp_lease **ret); + #define dhcp_lease_unref_and_replace(a, b) \ free_and_replace_full(a, b, sd_dhcp_lease_unref) diff --git a/src/libsystemd-network/sd-dhcp-lease.c b/src/libsystemd-network/sd-dhcp-lease.c index 8003f37fd31..9df1a4818df 100644 --- a/src/libsystemd-network/sd-dhcp-lease.c +++ b/src/libsystemd-network/sd-dhcp-lease.c @@ -9,6 +9,7 @@ #include "sd-dhcp-lease.h" #include "alloc-util.h" +#include "dhcp-client-internal.h" #include "dhcp-lease-internal.h" #include "dhcp-option.h" #include "dhcp-route.h" @@ -22,9 +23,11 @@ #include "hexdecoct.h" #include "hostname-util.h" #include "in-addr-util.h" +#include "ip-util.h" #include "network-common.h" #include "network-internal.h" #include "parse-util.h" +#include "set.h" #include "sort-util.h" #include "stdio-util.h" #include "string-util.h" @@ -419,6 +422,8 @@ static sd_dhcp_lease *dhcp_lease_free(sd_dhcp_lease *lease) { assert(lease); + sd_dhcp_message_unref(lease->message); + while ((option = LIST_POP(options, lease->private_options))) { free(option->data); free(option); @@ -1803,3 +1808,434 @@ int sd_dhcp_lease_get_timezone(sd_dhcp_lease *lease, const char **ret) { *ret = lease->timezone; return 0; } + +static int dhcp_lease_new_from_message(sd_dhcp_client *client, sd_dhcp_message *message, sd_dhcp_lease **ret) { + int r; + + assert(client); + assert(message); + assert(ret); + + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + r = dhcp_lease_new(&lease); + if (r < 0) + return r; + + /* acquired address: mandatory */ + if (message->header.yiaddr == INADDR_ANY) + return -EBADMSG; + lease->address = message->header.yiaddr; + + /* subnet mask: mandatory */ + if (dhcp_message_get_option_be32(message, SD_DHCP_OPTION_SUBNET_MASK, &lease->subnet_mask) < 0) { + /* fall back to the default subnet masks based on address class */ + struct in_addr mask; + r = in4_addr_default_subnet_mask( + &(struct in_addr) { + .s_addr = message->header.yiaddr, + }, + &mask); + if (r < 0) + return r; + + lease->subnet_mask = mask.s_addr; + } + + /* DHCP server address: mandatory */ + r = dhcp_message_get_option_be32(message, SD_DHCP_OPTION_SERVER_IDENTIFIER, &lease->server_address); + if (r < 0) { + if (!client->bootp) + return log_dhcp_client_errno(client, r, "Failed to read %s option: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_SERVER_IDENTIFIER)); + + /* BOOTP typically does not use Server Identifier option, but uses the siaddr field. */ + lease->server_address = message->header.siaddr; + } + + /* lifetime: mandatory */ + if (client->bootp) + lease->lifetime = USEC_INFINITY; /* BOOTP does not support lifetime. */ + else { + r = dhcp_message_get_option_sec(message, SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME, /* max_as_infinity= */ true, &lease->lifetime); + if (r < 0 || lease->lifetime == 0) { + if (client->fallback_lease_lifetime == 0) { + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to read %s option: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME)); + + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "The %s option set to 0 second.", + dhcp_option_code_to_string(SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME)); + } + + lease->lifetime = client->fallback_lease_lifetime; + } + + /* There is nothing mentioned about the valid range of the lifetime in RFC, but if it is too + * short, then the network connection easily become unstable. Let's bump to 30 seconds in + * that case. + * TODO: filter short lifetime in selecting state. */ + if (lease->lifetime <= 30 * USEC_PER_SEC) { + log_dhcp_client(client, "The %s option is too short (%s), bumping lease lifetime to 30 seconds.", + dhcp_option_code_to_string(SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME), + FORMAT_TIMESPAN(lease->lifetime, USEC_PER_SEC)); + lease->lifetime = 30 * USEC_PER_SEC; + } + + if (lease->lifetime != USEC_INFINITY) { + /* T2 */ + r = dhcp_message_get_option_sec(message, SD_DHCP_OPTION_REBINDING_TIME, /* max_as_infinity= */ true, &lease->t2); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to read %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_REBINDING_TIME)); + + /* verify that 0 < t2 < lifetime */ + if (lease->t2 <= 0 || lease->t2 >= lease->lifetime) + /* RFC2131 section 4.4.5: T2 defaults to (0.875 * duration_of_lease). */ + lease->t2 = lease->lifetime * 7 / 8; + + /* T1 */ + r = dhcp_message_get_option_sec(message, SD_DHCP_OPTION_RENEWAL_TIME, /* max_as_infinity= */ true, &lease->t1); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to read %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_RENEWAL_TIME)); + + /* verify that 0 < t1 < t2 */ + if (lease->t1 <= 0 || lease->t1 >= lease->t2) + /* RFC2131 section 4.4.5: T1 defaults to (0.5 * duration_of_lease). */ + lease->t1 = lease->lifetime / 2; + + /* For the case when T2 is too small compared with lifetime. */ + if (lease->t1 >= lease->t2) + /* RFC2131 section 4.4.5: T2 defaults to (0.875 * duration_of_lease). */ + lease->t2 = lease->lifetime * 7 / 8; + + assert(lease->t1 > 0); + assert(lease->t1 < lease->t2); + assert(lease->t2 < lease->lifetime); + } + } + + r = dhcp_message_get_option_be32(message, SD_DHCP_OPTION_BROADCAST, &lease->broadcast); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_BROADCAST)); + + r = dhcp_message_get_option_addresses( + message, + SD_DHCP_OPTION_ROUTER, + &lease->router_size, + &lease->router); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_ROUTER)); + + r = dhcp_message_get_option_addresses( + message, + SD_DHCP_OPTION_DOMAIN_NAME_SERVER, + &lease->servers[SD_DHCP_LEASE_DNS].size, + &lease->servers[SD_DHCP_LEASE_DNS].addr); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_DOMAIN_NAME_SERVER)); + + r = dhcp_message_get_option_addresses( + message, + SD_DHCP_OPTION_NTP_SERVER, + &lease->servers[SD_DHCP_LEASE_NTP].size, + &lease->servers[SD_DHCP_LEASE_NTP].addr); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_NTP_SERVER)); + + r = dhcp_message_get_option_addresses( + message, + SD_DHCP_OPTION_SIP_SERVER, + &lease->servers[SD_DHCP_LEASE_SIP].size, + &lease->servers[SD_DHCP_LEASE_SIP].addr); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_SIP_SERVER)); + + r = dhcp_message_get_option_routes( + message, + SD_DHCP_OPTION_STATIC_ROUTE, + &lease->n_static_routes, + &lease->static_routes); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_STATIC_ROUTE)); + + r = dhcp_message_get_option_routes( + message, + SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE, + &lease->n_classless_routes, + &lease->classless_routes); + if (r < 0) { + if (r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE)); + + r = dhcp_message_get_option_routes( + message, + SD_DHCP_OPTION_PRIVATE_CLASSLESS_STATIC_ROUTE, + &lease->n_classless_routes, + &lease->classless_routes); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_PRIVATE_CLASSLESS_STATIC_ROUTE)); + } + + r = dhcp_message_get_option_6rd( + message, + &lease->sixrd_ipv4masklen, + &lease->sixrd_prefixlen, + &lease->sixrd_prefix, + &lease->sixrd_n_br_addresses, + &lease->sixrd_br_addresses); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_6RD)); + + r = dhcp_message_get_option_dns_name(message, SD_DHCP_OPTION_DOMAIN_NAME, &lease->domainname); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_DOMAIN_NAME)); + + r = dhcp_message_get_option_hostname(message, &lease->hostname); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s and/or %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_FQDN), + dhcp_option_code_to_string(SD_DHCP_OPTION_HOST_NAME)); + + r = dhcp_message_get_option_domains(message, SD_DHCP_OPTION_DOMAIN_SEARCH, &lease->search_domains); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_DOMAIN_SEARCH)); + + r = dhcp_message_get_option_dnr(message, &lease->n_dnr, &lease->dnr); + if (r < 0 && r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_V4_DNR)); + + _cleanup_free_ char *captive_portal = NULL; + r = dhcp_message_get_option_string(message, SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL, &captive_portal); + if (r >= 0) { + if (!in_charset(captive_portal, URI_VALID)) + log_dhcp_client(client, "Received invalid %s, ignoring: %s", + dhcp_option_code_to_string(SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL), + captive_portal); + else + lease->captive_portal = TAKE_PTR(captive_portal); + } else if (r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL)); + + _cleanup_free_ char *tz = NULL; + r = dhcp_message_get_option_string(message, SD_DHCP_OPTION_TZDB_TIMEZONE, &tz); + if (r >= 0) { + if (!timezone_is_valid(tz, LOG_DEBUG)) + log_dhcp_client(client, "Received invalid %s, ignoring: %s", + dhcp_option_code_to_string(SD_DHCP_OPTION_TZDB_TIMEZONE), tz); + else + lease->timezone = TAKE_PTR(tz); + } else if (r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_TZDB_TIMEZONE)); + + uint16_t mtu; + r = dhcp_message_get_option_u16(message, SD_DHCP_OPTION_MTU_INTERFACE, &mtu); + if (r >= 0) { + /* RFC 2132 section 5.1 permits MTU values down to 68 bytes, which corresponds to the minimum + * IPv4 datagram size defined in RFC 791. + * + * Such a small MTU is not generally usable for normal IP communication. RFC 791 and RFC 1122 + * require hosts to be able to reassemble datagrams of at least 576 bytes, which is treated + * as the minimum safe size for IPv4 interoperability. + * + * Ignore MTU values smaller than 576 bytes. */ + if (mtu < IPV4_MIN_REASSEMBLY_SIZE) + log_dhcp_client(client, "Received too small %s, ignoring: %u", + dhcp_option_code_to_string(SD_DHCP_OPTION_MTU_INTERFACE), mtu); + else + lease->mtu = mtu; + } else if (r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_MTU_INTERFACE)); + + /* RFC 8925 section 3.2 + * If the client did not include the IPv6-Only Preferred option code in the Parameter Request List in + * the DHCPDISCOVER or DHCPREQUEST message, it MUST ignore the IPv6-Only Preferred option in any + * messages received from the server. */ + if (!client->anonymize && + set_contains(client->req_opts, UINT_TO_PTR(SD_DHCP_OPTION_IPV6_ONLY_PREFERRED))) { + usec_t t; + r = dhcp_message_get_option_sec( + message, + SD_DHCP_OPTION_IPV6_ONLY_PREFERRED, + /* max_as_infinity= */ false, + &t); + if (r >= 0) { + /* RFC 8925 section 3.4 + * MIN_V6ONLY_WAIT: The lower boundary for V6ONLY_WAIT. */ + if (t < MIN_V6ONLY_WAIT_USEC && !network_test_mode_enabled()) + lease->ipv6_only_preferred_usec = MIN_V6ONLY_WAIT_USEC; + else + lease->ipv6_only_preferred_usec = t; + } else if (r != -ENODATA) + log_dhcp_client_errno(client, r, "Failed to parse %s option, ignoring: %m", + dhcp_option_code_to_string(SD_DHCP_OPTION_IPV6_ONLY_PREFERRED)); + } + + lease->message = sd_dhcp_message_ref(message); + *ret = TAKE_PTR(lease); + return 0; +} + +static int client_parse_bootreply(sd_dhcp_client *client, sd_dhcp_message *message, sd_dhcp_lease **ret) { + int r; + + assert(client); + assert(message); + assert(ret); + + if (client->state != DHCP_STATE_SELECTING) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), "Received unexpected BOOTREPLY."); + + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + r = dhcp_lease_new_from_message(client, message, &lease); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to create BOOTP lease: %m"); + + log_dhcp_client(client, "Received BOOTREPLY from %s", IN4_ADDR_TO_STRING(&(struct in_addr) { .s_addr = lease->server_address })); + + *ret = TAKE_PTR(lease); + return DHCP_ACK; +} + +static int client_parse_ack(sd_dhcp_client *client, sd_dhcp_message *message, sd_dhcp_lease **ret) { + int r; + + assert(client); + assert(message); + assert(ret); + + switch (client->state) { + case DHCP_STATE_SELECTING: + if (!client->rapid_commit) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), "Received unexpected DHCPACK."); + + r = dhcp_message_get_option_flag(message, SD_DHCP_OPTION_RAPID_COMMIT); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to get Rapid Commit option: %m"); + + break; + case DHCP_STATE_REBOOTING: + case DHCP_STATE_REQUESTING: + case DHCP_STATE_RENEWING: + case DHCP_STATE_REBINDING: + break; + default: + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), "Received unexpected DHCPACK."); + } + + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + r = dhcp_lease_new_from_message(client, message, &lease); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to create DHCP lease: %m"); + + log_dhcp_client(client, "Received DHCPACK from %s", IN4_ADDR_TO_STRING(&(struct in_addr) { .s_addr = lease->server_address })); + + *ret = TAKE_PTR(lease); + return DHCP_ACK; +} + +static int client_parse_offer(sd_dhcp_client *client, sd_dhcp_message *message, sd_dhcp_lease **ret) { + int r; + + assert(client); + assert(message); + assert(ret); + + if (client->state != DHCP_STATE_SELECTING) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), "Received unexpected DHCPOFFER."); + + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + r = dhcp_lease_new_from_message(client, message, &lease); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to create DHCP lease: %m"); + + log_dhcp_client(client, "Received DHCPOFFER from %s", IN4_ADDR_TO_STRING(&(struct in_addr) { .s_addr = lease->server_address })); + + *ret = TAKE_PTR(lease); + return DHCP_OFFER; +} + +static int client_parse_nak(sd_dhcp_client *client, sd_dhcp_message *message, sd_dhcp_lease **ret) { + int r; + + assert(client); + assert(message); + assert(ret); + + /* DHCPNAK is a valid reply when we sent DHCPREQUEST. When we receive it after sending + * DHCPDISCOVER (or even we sent nothing), we should ignore the message. */ + if (!IN_SET(client->state, DHCP_STATE_REBOOTING, DHCP_STATE_REQUESTING, DHCP_STATE_RENEWING, DHCP_STATE_REBINDING)) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), "Received unexpected DHCPNAK."); + + /* Always ignore DHCPNAK without Server Identifier option. */ + struct in_addr a; + r = dhcp_message_get_option_address(message, SD_DHCP_OPTION_SERVER_IDENTIFIER, &a); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to read Server Identifier option in DHCPNAK: %m"); + + if (client->lease && client->lease->server_address != a.s_addr) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "Received DHCPNAK from unexpected server (%s).", + IN4_ADDR_TO_STRING(&a)); + + _cleanup_free_ char *e = NULL; + (void) dhcp_message_get_option_string(message, SD_DHCP_OPTION_ERROR_MESSAGE, &e); + log_dhcp_client(client, "Received DHCPNAK: %s", strna(e)); + + *ret = NULL; + return DHCP_NAK; +} + +int dhcp_client_parse_message(sd_dhcp_client *client, const struct iovec *iov, sd_dhcp_lease **ret) { + int r; + + assert(client); + assert(iov); + assert(ret); + + _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *message = NULL; + r = dhcp_message_parse( + iov, + BOOTREPLY, + &client->xid, + client->arp_type, + &client->hw_addr, + &message); + if (r < 0) + return r; + + if (client->bootp) + return client_parse_bootreply(client, message, ret); + + uint8_t type; + r = dhcp_message_get_option_u8(message, SD_DHCP_OPTION_MESSAGE_TYPE, &type); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to read Message Type option: %m"); + + switch (type) { + case DHCP_OFFER: + return client_parse_offer(client, message, ret); + case DHCP_ACK: + return client_parse_ack(client, message, ret); + case DHCP_NAK: + return client_parse_nak(client, message, ret); + default: + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), "Received message with unexpected type (%u).", type); + } +}