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.
This commit is contained in:
Yu Watanabe
2026-05-16 08:04:45 +09:00
parent f39c071f31
commit 26b7c5ff3b
2 changed files with 442 additions and 1 deletions
+6 -1
View File
@@ -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)
+436
View File
@@ -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);
}
}