| /* |
| * |
| * Embedded Linux library |
| * |
| * Copyright (C) 2018 Intel Corporation. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <netinet/ip.h> |
| #include <net/ethernet.h> |
| #include <linux/types.h> |
| #include <net/if_arp.h> |
| #include <errno.h> |
| |
| #include "private.h" |
| #include "random.h" |
| #include "time.h" |
| #include "net.h" |
| #include "timeout.h" |
| #include "dhcp.h" |
| #include "dhcp-private.h" |
| |
| #define CLIENT_DEBUG(fmt, args...) \ |
| l_util_debug(client->debug_handler, client->debug_data, \ |
| "%s:%i " fmt, __func__, __LINE__, ## args) |
| #define CLIENT_ENTER_STATE(s) \ |
| l_util_debug(client->debug_handler, client->debug_data, \ |
| "%s:%i Entering state: " #s, \ |
| __func__, __LINE__); \ |
| client->state = (s) |
| |
| #define BITS_PER_LONG (sizeof(unsigned long) * 8) |
| |
| #define DHCP_OPTION_PAD 0 /* RFC 2132, Section 3.1 */ |
| #define DHCP_OPTION_END 255 /* RFC 2132, Section 3.2 */ |
| |
| /* RFC 2132, Section 9.3. Option Overload */ |
| #define DHCP_OPTION_OVERLOAD 52 |
| enum dhcp_option_overload { |
| DHCP_OVERLOAD_FILE = 1, |
| DHCP_OVERLOAD_SNAME = 2, |
| DHCP_OVERLOAD_BOTH = 3, |
| }; |
| |
| /* RFC 2132, Section 9.6. DHCP Message Type */ |
| #define DHCP_OPTION_MESSAGE_TYPE 53 |
| enum dhcp_message_type { |
| DHCP_MESSAGE_TYPE_DISCOVER = 1, |
| DHCP_MESSAGE_TYPE_OFFER = 2, |
| DHCP_MESSAGE_TYPE_REQUEST = 3, |
| DHCP_MESSAGE_TYPE_DECLINE = 4, |
| DHCP_MESSAGE_TYPE_ACK = 5, |
| DHCP_MESSAGE_TYPE_NAK = 6, |
| DHCP_MESSAGE_TYPE_RELEASE = 7, |
| DHCP_MESSAGE_TYPE_INFORM = 8, |
| }; |
| |
| #define DHCP_OPTION_PARAMETER_REQUEST_LIST 55 /* Section 9.8 */ |
| #define DHCP_OPTION_MAXIMUM_MESSAGE_SIZE 57 /* Section 9.10 */ |
| #define DHCP_OPTION_CLIENT_IDENTIFIER 61 /* Section 9.14 */ |
| |
| enum dhcp_state { |
| DHCP_STATE_INIT, |
| DHCP_STATE_SELECTING, |
| DHCP_STATE_INIT_REBOOT, |
| DHCP_STATE_REBOOTING, |
| DHCP_STATE_REQUESTING, |
| DHCP_STATE_BOUND, |
| DHCP_STATE_RENEWING, |
| DHCP_STATE_REBINDING, |
| }; |
| |
| const char *_dhcp_message_type_to_string(uint8_t type) |
| { |
| switch(type) { |
| case DHCP_MESSAGE_TYPE_DISCOVER: |
| return "DHCPDISCOVER"; |
| case DHCP_MESSAGE_TYPE_OFFER: |
| return "DHCPOFFER"; |
| case DHCP_MESSAGE_TYPE_REQUEST: |
| return "DHCPREQUEST"; |
| case DHCP_MESSAGE_TYPE_DECLINE: |
| return "DHCPDECLINE"; |
| case DHCP_MESSAGE_TYPE_ACK: |
| return "DHCPACK"; |
| case DHCP_MESSAGE_TYPE_NAK: |
| return "DHCPNAK"; |
| case DHCP_MESSAGE_TYPE_RELEASE: |
| return "DHCPRELEASE"; |
| default: |
| return "unknown"; |
| } |
| } |
| |
| const char *_dhcp_option_to_string(uint8_t option) |
| { |
| switch (option) { |
| case DHCP_OPTION_PAD: |
| return "Pad"; |
| case L_DHCP_OPTION_SUBNET_MASK: |
| return "Subnet Mask"; |
| case L_DHCP_OPTION_ROUTER: |
| return "Router"; |
| case L_DHCP_OPTION_DOMAIN_NAME_SERVER: |
| return "Domain Name Server"; |
| case L_DHCP_OPTION_HOST_NAME: |
| return "Host Name"; |
| case L_DHCP_OPTION_DOMAIN_NAME: |
| return "Domain Name"; |
| case L_DHCP_OPTION_BROADCAST_ADDRESS: |
| return "Broadcast Address"; |
| case L_DHCP_OPTION_NTP_SERVERS: |
| return "NTP Servers"; |
| case L_DHCP_OPTION_REQUESTED_IP_ADDRESS: |
| return "IP Address"; |
| case L_DHCP_OPTION_IP_ADDRESS_LEASE_TIME: |
| return "IP Address Lease Time"; |
| case DHCP_OPTION_OVERLOAD: |
| return "Overload"; |
| case DHCP_OPTION_MESSAGE_TYPE: |
| return "DHCP Message Type"; |
| case L_DHCP_OPTION_SERVER_IDENTIFIER: |
| return "Server Identifier"; |
| case DHCP_OPTION_PARAMETER_REQUEST_LIST: |
| return "Parameter Request List"; |
| case DHCP_OPTION_MAXIMUM_MESSAGE_SIZE: |
| return "Maximum Message Size"; |
| case L_DHCP_OPTION_RENEWAL_T1_TIME: |
| return "Renewal Time"; |
| case L_DHCP_OPTION_REBINDING_T2_TIME: |
| return "Rebinding Time"; |
| case DHCP_OPTION_CLIENT_IDENTIFIER: |
| return "Client Identifier"; |
| case DHCP_OPTION_END: |
| return "End"; |
| default: |
| return "unknown"; |
| } |
| } |
| |
| bool _dhcp_message_iter_init(struct dhcp_message_iter *iter, |
| const struct dhcp_message *message, size_t len) |
| { |
| if (!message) |
| return false; |
| |
| if (len < sizeof(struct dhcp_message)) |
| return false; |
| |
| if (L_BE32_TO_CPU(message->magic) != DHCP_MAGIC) |
| return false; |
| |
| memset(iter, 0, sizeof(*iter)); |
| iter->message = message; |
| iter->message_len = len; |
| iter->max = len - sizeof(struct dhcp_message); |
| iter->options = message->options; |
| iter->can_overload = true; |
| |
| return true; |
| } |
| |
| static bool next_option(struct dhcp_message_iter *iter, |
| uint8_t *t, uint8_t *l, const void **v) |
| { |
| uint8_t type; |
| uint8_t len; |
| |
| while (iter->pos < iter->max) { |
| type = iter->options[iter->pos]; |
| |
| switch (type) { |
| case DHCP_OPTION_PAD: |
| iter->pos += 1; |
| continue; |
| case DHCP_OPTION_END: |
| return false; |
| default: |
| break; |
| } |
| |
| if (iter->pos + 2 >= iter->max) |
| return false; |
| |
| len = iter->options[iter->pos + 1]; |
| |
| if (iter->pos + 2 + len > iter->max) |
| return false; |
| |
| *t = type; |
| *l = len; |
| *v = &iter->options[iter->pos + 2]; |
| |
| iter->pos += 2 + len; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool _dhcp_message_iter_next(struct dhcp_message_iter *iter, uint8_t *type, |
| uint8_t *len, const void **data) |
| { |
| bool r; |
| uint8_t t, l; |
| const void *v; |
| |
| do { |
| r = next_option(iter, &t, &l, &v); |
| if (!r) { |
| iter->can_overload = false; |
| |
| if (iter->overload_file) { |
| iter->options = iter->message->file; |
| iter->pos = 0; |
| iter->max = sizeof(iter->message->file); |
| iter->overload_file = false; |
| continue; |
| } |
| |
| if (iter->overload_sname) { |
| iter->options = iter->message->sname; |
| iter->pos = 0; |
| iter->max = sizeof(iter->message->sname); |
| iter->overload_sname = false; |
| continue; |
| } |
| |
| return r; |
| } |
| |
| switch (t) { |
| case DHCP_OPTION_OVERLOAD: |
| if (l != 1) |
| continue; |
| |
| if (!iter->can_overload) |
| continue; |
| |
| if (l_get_u8(v) & DHCP_OVERLOAD_FILE) |
| iter->overload_file = true; |
| |
| if (l_get_u8(v) & DHCP_OVERLOAD_SNAME) |
| iter->overload_sname = true; |
| |
| continue; |
| default: |
| if (type) |
| *type = t; |
| |
| if (len) |
| *len = l; |
| |
| if (data) |
| *data = v; |
| return r; |
| } |
| } while (true); |
| |
| return false; |
| } |
| |
| int _dhcp_option_append(uint8_t **buf, size_t *buflen, uint8_t code, |
| size_t optlen, const void *optval) |
| { |
| if (!buf || !buflen) |
| return -EINVAL; |
| |
| switch (code) { |
| |
| case DHCP_OPTION_PAD: |
| case DHCP_OPTION_END: |
| if (*buflen < 1) |
| return -ENOBUFS; |
| |
| (*buf)[0] = code; |
| *buf += 1; |
| *buflen -= 1; |
| break; |
| |
| default: |
| if (*buflen < optlen + 2) |
| return -ENOBUFS; |
| |
| if (!optval) |
| return -EINVAL; |
| |
| (*buf)[0] = code; |
| (*buf)[1] = optlen; |
| memcpy(&(*buf)[2], optval, optlen); |
| |
| *buf += optlen + 2; |
| *buflen -= (optlen + 2); |
| |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static int dhcp_append_prl(const unsigned long *reqopts, |
| uint8_t **buf, size_t *buflen) |
| { |
| uint8_t optlen = 0; |
| unsigned int i; |
| unsigned int j; |
| |
| if (!buf || !buflen) |
| return -EINVAL; |
| |
| for (i = 0; i < 256 / BITS_PER_LONG; i++) |
| optlen += __builtin_popcountl(reqopts[i]); |
| |
| /* |
| * This function assumes that there's enough space to put the PRL |
| * into the buffer without resorting to file or sname overloading |
| */ |
| if (*buflen < optlen + 2U) |
| return -ENOBUFS; |
| |
| i = 0; |
| (*buf)[i++] = DHCP_OPTION_PARAMETER_REQUEST_LIST; |
| (*buf)[i++] = optlen; |
| |
| for (j = 0; j < 256; j++) { |
| if (reqopts[j / BITS_PER_LONG] & 1UL << (j % BITS_PER_LONG)) |
| (*buf)[i++] = j; |
| } |
| |
| *buf += optlen + 2; |
| *buflen -= (optlen + 2); |
| |
| return 0; |
| } |
| |
| static int dhcp_message_init(struct dhcp_message *message, |
| enum dhcp_op_code op, |
| uint8_t type, uint32_t xid, |
| uint8_t **opt, size_t *optlen) |
| { |
| int err; |
| |
| message->op = op; |
| message->xid = L_CPU_TO_BE32(xid); |
| message->magic = L_CPU_TO_BE32(DHCP_MAGIC); |
| *opt = (uint8_t *)(message + 1); |
| |
| err = _dhcp_option_append(opt, optlen, |
| DHCP_OPTION_MESSAGE_TYPE, 1, &type); |
| if (err < 0) |
| return err; |
| |
| return 0; |
| } |
| |
| static void dhcp_message_set_address_type(struct dhcp_message *message, |
| uint8_t addr_type, |
| uint8_t addr_len) |
| { |
| message->htype = addr_type; |
| |
| switch (addr_type) { |
| case ARPHRD_ETHER: |
| message->hlen = addr_len; |
| break; |
| default: |
| message->hlen = 0; |
| } |
| } |
| |
| static inline int dhcp_message_optimize(struct dhcp_message *message, |
| const uint8_t *end) |
| { |
| /* |
| * Don't bother sending a full sized dhcp_message as it is most likely |
| * mostly zeros. Instead truncate it at DHCP_OPTION_END and align to |
| * the nearest 4 byte boundary. Many implementations expect a packet |
| * of a certain size or it is filtered, so we cap the length in |
| * accordance to RFC 1542: |
| * "The IP Total Length and UDP Length must be large enough to contain |
| * the minimal BOOTP header of 300 octets" |
| */ |
| size_t len = align_len(end - (uint8_t *) message, 4); |
| if (len < 300) |
| len = 300; |
| |
| return len; |
| } |
| |
| #define DHCP_MIN_OPTIONS_SIZE 312 |
| |
| struct l_dhcp_client { |
| enum dhcp_state state; |
| unsigned long request_options[256 / BITS_PER_LONG]; |
| uint32_t ifindex; |
| char *ifname; |
| uint8_t addr[6]; |
| uint8_t addr_len; |
| uint8_t addr_type; |
| char *hostname; |
| uint32_t xid; |
| struct dhcp_transport *transport; |
| uint64_t start_t; |
| struct l_timeout *timeout_resend; |
| struct l_timeout *timeout_lease; |
| struct l_dhcp_lease *lease; |
| uint8_t attempt; |
| l_dhcp_client_event_cb_t event_handler; |
| void *event_data; |
| l_dhcp_destroy_cb_t event_destroy; |
| l_dhcp_debug_cb_t debug_handler; |
| l_dhcp_destroy_cb_t debug_destroy; |
| void *debug_data; |
| bool have_addr : 1; |
| bool override_xid : 1; |
| }; |
| |
| static inline void dhcp_enable_option(struct l_dhcp_client *client, |
| uint8_t option) |
| { |
| client->request_options[option / BITS_PER_LONG] |= |
| 1UL << (option % BITS_PER_LONG); |
| } |
| |
| static uint16_t dhcp_attempt_secs(uint64_t start) |
| { |
| uint64_t now = l_time_now(); |
| uint64_t elapsed = l_time_to_secs(now - start); |
| |
| if (elapsed == 0) |
| return 1; |
| |
| if (elapsed > UINT16_MAX) |
| return UINT16_MAX; |
| |
| return elapsed; |
| } |
| |
| /* |
| * Takes a time in seconds and produces a fuzzed value that can be directly |
| * used by l_timeout_modify_ms |
| */ |
| static uint64_t dhcp_fuzz_secs(uint32_t secs) |
| { |
| uint64_t ms = secs * 1000ULL; |
| uint32_t r = l_getrandom_uint32(); |
| |
| /* |
| * RFC2132, Section 4.1: |
| * DHCP clients are responsible for all message retransmission. The |
| * client MUST adopt a retransmission strategy that incorporates a |
| * randomized exponential backoff algorithm to determine the delay |
| * between retransmissions. |
| * |
| * and later in the same paragraph: |
| * For example, in a 10Mb/sec Ethernet internetwork, the delay before |
| * the first retransmission SHOULD be 4 seconds randomized by the |
| * value of a uniform random number chosen from the range -1 to +1. |
| * Clients with clocks that provide resolution granularity of less than |
| * one second may choose a non-integer randomization value. |
| */ |
| if (r & 0x80000000) |
| ms += r & 0x3ff; |
| else |
| ms -= r & 0x3ff; |
| |
| return ms; |
| } |
| |
| /* |
| * Takes a time in milliseconds and produces a fuzzed value that can be directly |
| * used by l_timeout_modify_ms. The fluctuation of the random noise added is |
| * from -63 to 63 milliseconds. |
| */ |
| static uint64_t dhcp_fuzz_msecs(uint64_t ms) |
| { |
| uint32_t r = l_getrandom_uint32(); |
| |
| if (r & 0x80000000) |
| ms += r & 0x3f; |
| else |
| ms -= r & 0x3f; |
| |
| return ms; |
| } |
| |
| static uint32_t dhcp_rebind_renew_retry_time(uint64_t start_t, uint32_t expiry) |
| { |
| uint64_t now = l_time_now(); |
| uint32_t relative_now; |
| uint32_t retry_time; |
| |
| /* |
| * RFC 2131, Section 4.4.5: |
| * " In both RENEWING and REBINDING states, if the client receives no |
| * response to its DHCPREQUEST message, the client SHOULD wait one-half |
| * of the remaining time until T2 (in RENEWING state) and one-half of |
| * the remaining lease time (in REBINDING state), down to a minimum of |
| * 60 seconds, before retransmitting the DHCPREQUEST message. |
| */ |
| relative_now = l_time_to_secs(now - start_t); |
| retry_time = (expiry - relative_now) / 2; |
| |
| if (retry_time < 60) |
| retry_time = 60; |
| |
| return retry_time; |
| } |
| |
| static int client_message_init(struct l_dhcp_client *client, |
| struct dhcp_message *message, |
| uint8_t type, |
| uint8_t **opt, size_t *optlen) |
| { |
| int err; |
| uint16_t max_size; |
| |
| err = dhcp_message_init(message, DHCP_OP_CODE_BOOTREQUEST, |
| type, client->xid, opt, optlen); |
| if (err < 0) |
| return err; |
| |
| dhcp_message_set_address_type(message, client->addr_type, |
| client->addr_len); |
| /* |
| * RFC2132 section 4.1.1: |
| * The client MUST include its hardware address in the ’chaddr’ field, |
| * if necessary for delivery of DHCP reply messages. Non-Ethernet |
| * interfaces will leave 'chaddr' empty and use the client identifier |
| * instead |
| */ |
| if (client->addr_type == ARPHRD_ETHER) |
| memcpy(message->chaddr, &client->addr, client->addr_len); |
| |
| /* |
| * Althrough RFC 2131 says that secs should be initialized to 0, |
| * some servers refuse to give us a lease unless we set this to a |
| * non-zero value |
| */ |
| message->secs = L_CPU_TO_BE16(dhcp_attempt_secs(client->start_t)); |
| |
| err = dhcp_append_prl(client->request_options, opt, optlen); |
| if (err < 0) |
| return err; |
| |
| /* |
| * Set the maximum DHCP message size to the minimum legal value. This |
| * helps some buggy DHCP servers to not send bigger packets |
| */ |
| max_size = L_CPU_TO_BE16(576); |
| err = _dhcp_option_append(opt, optlen, |
| DHCP_OPTION_MAXIMUM_MESSAGE_SIZE, |
| 2, &max_size); |
| if (err < 0) |
| return err; |
| |
| return 0; |
| } |
| |
| static void dhcp_client_event_notify(struct l_dhcp_client *client, |
| enum l_dhcp_client_event event) |
| { |
| if (client->event_handler) |
| client->event_handler(client, event, client->event_data); |
| } |
| |
| static int dhcp_client_send_discover(struct l_dhcp_client *client) |
| { |
| uint8_t *opt; |
| size_t optlen = DHCP_MIN_OPTIONS_SIZE; |
| size_t len = sizeof(struct dhcp_message) + optlen; |
| L_AUTO_FREE_VAR(struct dhcp_message *, discover); |
| int err; |
| |
| CLIENT_DEBUG(""); |
| |
| discover = (struct dhcp_message *) l_new(uint8_t, len); |
| |
| err = client_message_init(client, discover, |
| DHCP_MESSAGE_TYPE_DISCOVER, |
| &opt, &optlen); |
| if (err < 0) |
| return err; |
| |
| if (client->hostname) { |
| err = _dhcp_option_append(&opt, &optlen, |
| L_DHCP_OPTION_HOST_NAME, |
| strlen(client->hostname), |
| client->hostname); |
| if (err < 0) |
| return err; |
| } |
| |
| err = _dhcp_option_append(&opt, &optlen, DHCP_OPTION_END, 0, NULL); |
| if (err < 0) |
| return err; |
| |
| len = dhcp_message_optimize(discover, opt); |
| |
| return client->transport->broadcast(client->transport, |
| INADDR_ANY, DHCP_PORT_CLIENT, |
| INADDR_BROADCAST, DHCP_PORT_SERVER, |
| discover, len); |
| } |
| |
| static int dhcp_client_send_request(struct l_dhcp_client *client) |
| { |
| uint8_t *opt; |
| size_t optlen = DHCP_MIN_OPTIONS_SIZE; |
| size_t len = sizeof(struct dhcp_message) + optlen; |
| L_AUTO_FREE_VAR(struct dhcp_message *, request); |
| int err; |
| |
| CLIENT_DEBUG(""); |
| |
| request = (struct dhcp_message *) l_new(uint8_t, len); |
| |
| err = client_message_init(client, request, |
| DHCP_MESSAGE_TYPE_REQUEST, |
| &opt, &optlen); |
| if (err < 0) |
| return err; |
| |
| switch (client->state) { |
| case DHCP_STATE_REQUESTING: |
| /* |
| * RFC 2131, Section 4.3.2: |
| * "If the DHCPREQUEST message contains a 'server identifier' |
| * option, the message is in response to a DHCPOFFER message." |
| * |
| * and |
| * |
| * "DHCPREQUEST generated during SELECTING state: |
| * Client inserts the address of the selected server in |
| * 'server identifier', 'ciaddr' MUST be zero, 'requested IP |
| * address' MUST be filled in with the yiaddr value from the |
| * chosen DHCPOFFER." |
| * |
| * NOTE: 'SELECTING' is meant to be 'REQUESTING' in the RFC |
| */ |
| err = _dhcp_option_append(&opt, &optlen, |
| L_DHCP_OPTION_SERVER_IDENTIFIER, |
| 4, &client->lease->server_address); |
| if (err < 0) |
| return err; |
| |
| err = _dhcp_option_append(&opt, &optlen, |
| L_DHCP_OPTION_REQUESTED_IP_ADDRESS, |
| 4, &client->lease->address); |
| if (err < 0) |
| return err; |
| break; |
| case DHCP_STATE_RENEWING: |
| case DHCP_STATE_REBINDING: |
| request->ciaddr = client->lease->address; |
| break; |
| |
| case DHCP_STATE_INIT: |
| case DHCP_STATE_SELECTING: |
| case DHCP_STATE_INIT_REBOOT: |
| case DHCP_STATE_REBOOTING: |
| case DHCP_STATE_BOUND: |
| return -EINVAL; |
| } |
| |
| if (client->hostname) { |
| err = _dhcp_option_append(&opt, &optlen, |
| L_DHCP_OPTION_HOST_NAME, |
| strlen(client->hostname), |
| client->hostname); |
| if (err < 0) |
| return err; |
| } |
| |
| err = _dhcp_option_append(&opt, &optlen, DHCP_OPTION_END, 0, NULL); |
| if (err < 0) |
| return err; |
| |
| len = dhcp_message_optimize(request, opt); |
| |
| /* |
| * RFC2131, Section 4.1: |
| * "DHCP clients MUST use the IP address provided in the |
| * 'server identifier' option for any unicast requests to the DHCP |
| * server. |
| */ |
| if (client->state == DHCP_STATE_RENEWING) { |
| struct sockaddr_in si; |
| memset(&si, 0, sizeof(si)); |
| si.sin_family = AF_INET; |
| si.sin_port = L_CPU_TO_BE16(DHCP_PORT_SERVER); |
| si.sin_addr.s_addr = client->lease->server_address; |
| return client->transport->send(client->transport, |
| &si, request, len); |
| } |
| |
| return client->transport->broadcast(client->transport, |
| INADDR_ANY, DHCP_PORT_CLIENT, |
| INADDR_BROADCAST, DHCP_PORT_SERVER, |
| request, len); |
| } |
| |
| static void dhcp_client_timeout_resend(struct l_timeout *timeout, |
| void *user_data) |
| { |
| struct l_dhcp_client *client = user_data; |
| unsigned int next_timeout = 0; |
| |
| CLIENT_DEBUG(""); |
| |
| switch (client->state) { |
| case DHCP_STATE_SELECTING: |
| if (dhcp_client_send_discover(client) < 0) |
| goto error; |
| break; |
| case DHCP_STATE_RENEWING: |
| case DHCP_STATE_REQUESTING: |
| case DHCP_STATE_REBINDING: |
| if (dhcp_client_send_request(client) < 0) |
| goto error; |
| break; |
| case DHCP_STATE_INIT: |
| case DHCP_STATE_INIT_REBOOT: |
| case DHCP_STATE_REBOOTING: |
| case DHCP_STATE_BOUND: |
| break; |
| } |
| |
| switch (client->state) { |
| case DHCP_STATE_RENEWING: |
| next_timeout = dhcp_rebind_renew_retry_time(client->start_t, |
| client->lease->t2); |
| break; |
| case DHCP_STATE_REBINDING: |
| next_timeout = dhcp_rebind_renew_retry_time(client->start_t, |
| client->lease->lifetime); |
| break; |
| case DHCP_STATE_REQUESTING: |
| case DHCP_STATE_SELECTING: |
| /* |
| * RFC 2131 Section 4.1: |
| * "The retransmission delay SHOULD be doubled with subsequent |
| * retransmissions up to a maximum of 64 seconds. |
| */ |
| client->attempt += 1; |
| next_timeout = minsize(2 << client->attempt, 64); |
| break; |
| case DHCP_STATE_INIT: |
| case DHCP_STATE_INIT_REBOOT: |
| case DHCP_STATE_REBOOTING: |
| case DHCP_STATE_BOUND: |
| break; |
| } |
| |
| if (next_timeout) |
| l_timeout_modify_ms(timeout, dhcp_fuzz_secs(next_timeout)); |
| |
| return; |
| |
| error: |
| l_dhcp_client_stop(client); |
| } |
| |
| static void dhcp_client_lease_expired(struct l_timeout *timeout, |
| void *user_data) |
| { |
| struct l_dhcp_client *client = user_data; |
| |
| CLIENT_DEBUG(""); |
| |
| l_dhcp_client_stop(client); |
| dhcp_client_event_notify(client, L_DHCP_CLIENT_EVENT_NO_LEASE); |
| } |
| |
| static void dhcp_client_t2_expired(struct l_timeout *timeout, void *user_data) |
| { |
| struct l_dhcp_client *client = user_data; |
| uint32_t next_timeout = client->lease->lifetime - client->lease->t2; |
| |
| CLIENT_DEBUG(""); |
| |
| /* |
| * If we got here, then resend_timeout is active, with a timeout |
| * set originally for ~60 seconds. So we simply set the new state |
| * and wait for the timer to fire |
| */ |
| CLIENT_ENTER_STATE(DHCP_STATE_REBINDING); |
| |
| l_timeout_modify_ms(client->timeout_lease, |
| dhcp_fuzz_secs(next_timeout)); |
| l_timeout_set_callback(client->timeout_lease, |
| dhcp_client_lease_expired, client, NULL); |
| } |
| |
| static void dhcp_client_t1_expired(struct l_timeout *timeout, void *user_data) |
| { |
| struct l_dhcp_client *client = user_data; |
| uint32_t next_timeout; |
| |
| CLIENT_DEBUG(""); |
| |
| CLIENT_ENTER_STATE(DHCP_STATE_RENEWING); |
| client->attempt = 1; |
| |
| if (dhcp_client_send_request(client) < 0) |
| goto error; |
| |
| next_timeout = client->lease->t2 - client->lease->t1; |
| l_timeout_modify_ms(client->timeout_lease, |
| dhcp_fuzz_secs(next_timeout)); |
| l_timeout_set_callback(client->timeout_lease, dhcp_client_t2_expired, |
| client, NULL); |
| |
| next_timeout = dhcp_rebind_renew_retry_time(client->start_t, |
| client->lease->t2); |
| client->timeout_resend = |
| l_timeout_create_ms(dhcp_fuzz_secs(next_timeout), |
| dhcp_client_timeout_resend, |
| client, NULL); |
| return; |
| |
| error: |
| l_dhcp_client_stop(client); |
| } |
| |
| static int dhcp_client_receive_ack(struct l_dhcp_client *client, |
| const struct dhcp_message *ack, |
| size_t len) |
| { |
| struct dhcp_message_iter iter; |
| struct l_dhcp_lease *lease; |
| int r; |
| |
| CLIENT_DEBUG(""); |
| |
| if (ack->yiaddr == 0) |
| return -ENOMSG; |
| |
| if (!_dhcp_message_iter_init(&iter, ack, len)) |
| return -EINVAL; |
| |
| lease = _dhcp_lease_parse_options(&iter); |
| if (!lease) { |
| CLIENT_DEBUG("Failed to parse DHCP options."); |
| |
| return -ENOMSG; |
| } |
| |
| lease->address = ack->yiaddr; |
| |
| r = L_DHCP_CLIENT_EVENT_LEASE_RENEWED; |
| |
| if (client->lease) { |
| if (client->lease->subnet_mask != lease->subnet_mask || |
| client->lease->address != lease->address || |
| client->lease->router != lease->router) |
| r = L_DHCP_CLIENT_EVENT_IP_CHANGED; |
| |
| _dhcp_lease_free(client->lease); |
| } |
| |
| client->lease = lease; |
| |
| /* In case this is an initial request, override to LEASE_OBTAINED */ |
| if (client->state == DHCP_STATE_REQUESTING || |
| client->state == DHCP_STATE_REBOOTING) |
| r = L_DHCP_CLIENT_EVENT_LEASE_OBTAINED; |
| |
| return r; |
| } |
| |
| static int dhcp_client_receive_offer(struct l_dhcp_client *client, |
| const struct dhcp_message *offer, |
| size_t len) |
| { |
| struct dhcp_message_iter iter; |
| |
| CLIENT_DEBUG(""); |
| |
| if (offer->yiaddr == 0) |
| return -ENOMSG; |
| |
| if (!_dhcp_message_iter_init(&iter, offer, len)) |
| return -EINVAL; |
| |
| client->lease = _dhcp_lease_parse_options(&iter); |
| if (!client->lease) |
| return -ENOMSG; |
| |
| client->lease->address = offer->yiaddr; |
| |
| return 0; |
| } |
| |
| static void dhcp_client_rx_message(const void *data, size_t len, void *userdata) |
| { |
| struct l_dhcp_client *client = userdata; |
| const struct dhcp_message *message = data; |
| struct dhcp_message_iter iter; |
| uint8_t msg_type = 0; |
| uint8_t t, l; |
| const void *v; |
| int r; |
| |
| CLIENT_DEBUG(""); |
| |
| if (len < sizeof(struct dhcp_message)) |
| return; |
| |
| if (message->op != DHCP_OP_CODE_BOOTREPLY) |
| return; |
| |
| if (L_BE32_TO_CPU(message->xid) != client->xid) |
| return; |
| |
| if (memcmp(message->chaddr, client->addr, client->addr_len)) |
| return; |
| |
| if (!_dhcp_message_iter_init(&iter, message, len)) |
| return; |
| |
| while (_dhcp_message_iter_next(&iter, &t, &l, &v) && !msg_type) { |
| switch (t) { |
| case DHCP_OPTION_MESSAGE_TYPE: |
| if (l == 1) |
| msg_type = l_get_u8(v); |
| break; |
| } |
| } |
| |
| switch (client->state) { |
| case DHCP_STATE_INIT: |
| return; |
| case DHCP_STATE_SELECTING: |
| if (msg_type != DHCP_MESSAGE_TYPE_OFFER) |
| return; |
| |
| if (dhcp_client_receive_offer(client, message, len) < 0) |
| return; |
| |
| CLIENT_ENTER_STATE(DHCP_STATE_REQUESTING); |
| client->attempt = 1; |
| |
| if (dhcp_client_send_request(client) < 0) { |
| l_dhcp_client_stop(client); |
| |
| return; |
| } |
| |
| l_timeout_modify_ms(client->timeout_resend, dhcp_fuzz_secs(4)); |
| break; |
| case DHCP_STATE_REQUESTING: |
| case DHCP_STATE_RENEWING: |
| case DHCP_STATE_REBINDING: |
| if (msg_type == DHCP_MESSAGE_TYPE_NAK) { |
| l_dhcp_client_stop(client); |
| |
| dhcp_client_event_notify(client, |
| L_DHCP_CLIENT_EVENT_NO_LEASE); |
| return; |
| } |
| |
| if (msg_type != DHCP_MESSAGE_TYPE_ACK) |
| return; |
| |
| r = dhcp_client_receive_ack(client, message, len); |
| if (r < 0) |
| return; |
| |
| CLIENT_ENTER_STATE(DHCP_STATE_BOUND); |
| l_timeout_remove(client->timeout_resend); |
| client->timeout_resend = NULL; |
| |
| if (client->transport->bind) |
| client->transport->bind(client->transport, |
| client->lease->address); |
| |
| dhcp_client_event_notify(client, r); |
| |
| /* |
| * Start T1, once it expires we will start the T2 timer. If |
| * we renew the lease, we will end up back here. |
| * |
| * RFC2131, Section 4.4.5 states: |
| * "Times T1 and T2 SHOULD be chosen with some random "fuzz" |
| * around a fixed value, to avoid synchronization of client |
| * reacquisition." |
| */ |
| l_timeout_remove(client->timeout_lease); |
| |
| /* Infinite lease, no need to start t1 */ |
| if (client->lease->lifetime != 0xffffffffu) { |
| uint32_t next_timeout = |
| dhcp_fuzz_secs(client->lease->t1); |
| |
| client->timeout_lease = |
| l_timeout_create_ms(next_timeout, |
| dhcp_client_t1_expired, |
| client, NULL); |
| } |
| break; |
| case DHCP_STATE_INIT_REBOOT: |
| case DHCP_STATE_REBOOTING: |
| case DHCP_STATE_BOUND: |
| break; |
| } |
| } |
| |
| LIB_EXPORT struct l_dhcp_client *l_dhcp_client_new(uint32_t ifindex) |
| { |
| struct l_dhcp_client *client; |
| |
| client = l_new(struct l_dhcp_client, 1); |
| |
| client->state = DHCP_STATE_INIT; |
| client->ifindex = ifindex; |
| |
| /* Enable these options by default */ |
| dhcp_enable_option(client, L_DHCP_OPTION_SUBNET_MASK); |
| dhcp_enable_option(client, L_DHCP_OPTION_ROUTER); |
| dhcp_enable_option(client, L_DHCP_OPTION_HOST_NAME); |
| dhcp_enable_option(client, L_DHCP_OPTION_DOMAIN_NAME); |
| dhcp_enable_option(client, L_DHCP_OPTION_DOMAIN_NAME_SERVER); |
| dhcp_enable_option(client, L_DHCP_OPTION_NTP_SERVERS); |
| |
| return client; |
| } |
| |
| LIB_EXPORT void l_dhcp_client_destroy(struct l_dhcp_client *client) |
| { |
| if (unlikely(!client)) |
| return; |
| |
| l_dhcp_client_stop(client); |
| |
| if (client->event_destroy) |
| client->event_destroy(client->event_data); |
| |
| _dhcp_transport_free(client->transport); |
| l_free(client->ifname); |
| l_free(client->hostname); |
| |
| l_free(client); |
| } |
| |
| LIB_EXPORT bool l_dhcp_client_add_request_option(struct l_dhcp_client *client, |
| uint8_t option) |
| { |
| if (unlikely(!client)) |
| return false; |
| |
| if (unlikely(client->state != DHCP_STATE_INIT)) |
| return false; |
| |
| switch (option) { |
| case DHCP_OPTION_PAD: |
| case DHCP_OPTION_END: |
| case DHCP_OPTION_OVERLOAD: |
| case DHCP_OPTION_MESSAGE_TYPE: |
| case DHCP_OPTION_PARAMETER_REQUEST_LIST: |
| return false; |
| } |
| |
| dhcp_enable_option(client, option); |
| |
| return true; |
| } |
| |
| LIB_EXPORT bool l_dhcp_client_set_address(struct l_dhcp_client *client, |
| uint8_t type, |
| const uint8_t *addr, |
| size_t addr_len) |
| { |
| if (unlikely(!client)) |
| return false; |
| |
| switch (type) { |
| case ARPHRD_ETHER: |
| if (addr_len != ETH_ALEN) |
| return false; |
| break; |
| default: |
| return false; |
| } |
| |
| client->addr_len = addr_len; |
| memcpy(client->addr, addr, addr_len); |
| client->addr_type = type; |
| |
| client->have_addr = true; |
| |
| return true; |
| } |
| |
| LIB_EXPORT bool l_dhcp_client_set_interface_name(struct l_dhcp_client *client, |
| const char *ifname) |
| { |
| if (unlikely(!client)) |
| return false; |
| |
| if (unlikely(client->state != DHCP_STATE_INIT)) |
| return false; |
| |
| l_free(client->ifname); |
| client->ifname = l_strdup(ifname); |
| |
| return true; |
| } |
| |
| LIB_EXPORT bool l_dhcp_client_set_hostname(struct l_dhcp_client *client, |
| const char *hostname) |
| { |
| if (unlikely(!client)) |
| return false; |
| |
| if (unlikely(client->state != DHCP_STATE_INIT)) |
| return false; |
| |
| if (!hostname) |
| goto done; |
| |
| if (client->hostname && !strcmp(client->hostname, hostname)) |
| return true; |
| |
| done: |
| l_free(client->hostname); |
| client->hostname = l_strdup(hostname); |
| |
| return true; |
| } |
| |
| bool _dhcp_client_set_transport(struct l_dhcp_client *client, |
| struct dhcp_transport *transport) |
| { |
| if (unlikely(!client)) |
| return false; |
| |
| if (unlikely(client->state != DHCP_STATE_INIT)) |
| return false; |
| |
| if (client->transport) |
| _dhcp_transport_free(client->transport); |
| |
| client->transport = transport; |
| return true; |
| } |
| |
| void _dhcp_client_override_xid(struct l_dhcp_client *client, uint32_t xid) |
| { |
| client->override_xid = true; |
| client->xid = xid; |
| } |
| |
| LIB_EXPORT const struct l_dhcp_lease *l_dhcp_client_get_lease( |
| const struct l_dhcp_client *client) |
| { |
| if (unlikely(!client)) |
| return NULL; |
| |
| return client->lease; |
| } |
| |
| LIB_EXPORT bool l_dhcp_client_start(struct l_dhcp_client *client) |
| { |
| int err; |
| |
| if (unlikely(!client)) |
| return false; |
| |
| if (unlikely(client->state != DHCP_STATE_INIT)) |
| return false; |
| |
| if (!client->have_addr) { |
| uint8_t mac[6]; |
| |
| if (!l_net_get_mac_address(client->ifindex, mac)) |
| return false; |
| |
| l_dhcp_client_set_address(client, ARPHRD_ETHER, mac, 6); |
| } |
| |
| if (!client->ifname) { |
| client->ifname = l_net_get_name(client->ifindex); |
| |
| if (!client->ifname) |
| return false; |
| } |
| |
| if (!client->transport) { |
| client->transport = |
| _dhcp_default_transport_new(client->ifindex, |
| client->ifname, |
| DHCP_PORT_CLIENT); |
| |
| if (!client->transport) |
| return false; |
| } |
| |
| if (!client->override_xid) |
| l_getrandom(&client->xid, sizeof(client->xid)); |
| |
| if (client->transport->open) |
| if (client->transport->open(client->transport, |
| client->xid) < 0) |
| return false; |
| |
| _dhcp_transport_set_rx_callback(client->transport, |
| dhcp_client_rx_message, |
| client); |
| |
| client->start_t = l_time_now(); |
| |
| err = dhcp_client_send_discover(client); |
| if (err < 0) |
| return false; |
| |
| client->timeout_resend = l_timeout_create_ms(dhcp_fuzz_msecs(600), |
| dhcp_client_timeout_resend, |
| client, NULL); |
| CLIENT_ENTER_STATE(DHCP_STATE_SELECTING); |
| client->attempt = 1; |
| |
| return true; |
| } |
| |
| LIB_EXPORT bool l_dhcp_client_stop(struct l_dhcp_client *client) |
| { |
| if (unlikely(!client)) |
| return false; |
| |
| l_timeout_remove(client->timeout_resend); |
| client->timeout_resend = NULL; |
| |
| l_timeout_remove(client->timeout_lease); |
| client->timeout_lease = NULL; |
| |
| if (client->transport && client->transport->close) |
| client->transport->close(client->transport); |
| |
| client->start_t = 0; |
| CLIENT_ENTER_STATE(DHCP_STATE_INIT); |
| |
| _dhcp_lease_free(client->lease); |
| client->lease = NULL; |
| |
| return true; |
| } |
| |
| LIB_EXPORT bool l_dhcp_client_set_event_handler(struct l_dhcp_client *client, |
| l_dhcp_client_event_cb_t handler, |
| void *userdata, |
| l_dhcp_destroy_cb_t destroy) |
| { |
| if (unlikely(!client)) |
| return false; |
| |
| if (client->event_destroy) |
| client->event_destroy(client->event_data); |
| |
| client->event_handler = handler; |
| client->event_data = userdata; |
| client->event_destroy = destroy; |
| |
| return true; |
| } |
| |
| LIB_EXPORT bool l_dhcp_client_set_debug(struct l_dhcp_client *client, |
| l_dhcp_debug_cb_t function, |
| void *user_data, |
| l_dhcp_destroy_cb_t destroy) |
| { |
| if (unlikely(!client)) |
| return false; |
| |
| if (client->debug_destroy) |
| client->debug_destroy(client->debug_data); |
| |
| client->debug_handler = function; |
| client->debug_destroy = destroy; |
| client->debug_data = user_data; |
| |
| return true; |
| } |