]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sd-dhcp-lease: introduce dhcp_client_parse_message()
authorYu Watanabe <watanabe.yu+github@gmail.com>
Tue, 31 Mar 2026 16:45:16 +0000 (01:45 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Fri, 15 May 2026 23:04:45 +0000 (08:04 +0900)
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.

src/libsystemd-network/dhcp-lease-internal.h
src/libsystemd-network/sd-dhcp-lease.c

index bc411a4f36cd3373b96c21c62f4cdf2c716b59c8..6a562901142bb432f714edf428d134c402a1cb73 100644 (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)
index 8003f37fd3122a7a4a641bace8c353218f9e9f62..9df1a4818dfecf39ba74bb93d26dfb037909ad6a 100644 (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"
 #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);
+        }
+}