#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"
assert(lease);
+ sd_dhcp_message_unref(lease->message);
+
while ((option = LIST_POP(options, lease->private_options))) {
free(option->data);
free(option);
*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);
+ }
+}