/* SPDX-License-Identifier: LGPL-2.1-or-later */ /*** Copyright © 2013 Intel Corporation. All rights reserved. ***/ #include #include #include #include "alloc-util.h" #include "dhcp-option.h" #include "dhcp-server-internal.h" #include "memory-util.h" #include "ordered-set.h" #include "strv.h" #include "utf8.h" /* Append type-length value structure to the options buffer */ static int dhcp_option_append_tlv(uint8_t options[], size_t size, size_t *offset, uint8_t code, size_t optlen, const void *optval) { assert(options); assert(size > 0); assert(offset); assert(optlen <= UINT8_MAX); assert(*offset < size); if (*offset + 2 + optlen > size) return -ENOBUFS; options[*offset] = code; options[*offset + 1] = optlen; memcpy_safe(&options[*offset + 2], optval, optlen); *offset += 2 + optlen; return 0; } static int option_append(uint8_t options[], size_t size, size_t *offset, uint8_t code, size_t optlen, const void *optval) { assert(options); assert(size > 0); assert(offset); int r; if (code != SD_DHCP_OPTION_END) /* always make sure there is space for an END option */ size--; switch (code) { case SD_DHCP_OPTION_PAD: case SD_DHCP_OPTION_END: if (*offset + 1 > size) return -ENOBUFS; options[*offset] = code; *offset += 1; break; case SD_DHCP_OPTION_USER_CLASS: { size_t total = 0; if (strv_isempty((char **) optval)) return -EINVAL; STRV_FOREACH(s, (const char* const*) optval) { size_t len = strlen(*s); if (len > 255 || len == 0) return -EINVAL; total += 1 + len; } if (*offset + 2 + total > size) return -ENOBUFS; options[*offset] = code; options[*offset + 1] = total; *offset += 2; STRV_FOREACH(s, (const char* const*) optval) { size_t len = strlen(*s); options[*offset] = len; memcpy(&options[*offset + 1], *s, len); *offset += 1 + len; } break; } case SD_DHCP_OPTION_SIP_SERVER: if (*offset + 3 + optlen > size) return -ENOBUFS; options[*offset] = code; options[*offset + 1] = optlen + 1; options[*offset + 2] = 1; memcpy_safe(&options[*offset + 3], optval, optlen); *offset += 3 + optlen; break; case SD_DHCP_OPTION_VENDOR_SPECIFIC: { OrderedSet *s = (OrderedSet *) optval; struct sd_dhcp_option *p; size_t l = 0; ORDERED_SET_FOREACH(p, s) l += p->length + 2; if (*offset + l + 2 > size) return -ENOBUFS; options[*offset] = code; options[*offset + 1] = l; *offset += 2; ORDERED_SET_FOREACH(p, s) { r = dhcp_option_append_tlv(options, size, offset, p->option, p->length, p->data); if (r < 0) return r; } break; } case SD_DHCP_OPTION_RELAY_AGENT_INFORMATION: { sd_dhcp_server *server = (sd_dhcp_server *) optval; size_t current_offset = *offset + 2; if (server->agent_circuit_id) { r = dhcp_option_append_tlv(options, size, ¤t_offset, SD_DHCP_RELAY_AGENT_CIRCUIT_ID, strlen(server->agent_circuit_id), server->agent_circuit_id); if (r < 0) return r; } if (server->agent_remote_id) { r = dhcp_option_append_tlv(options, size, ¤t_offset, SD_DHCP_RELAY_AGENT_REMOTE_ID, strlen(server->agent_remote_id), server->agent_remote_id); if (r < 0) return r; } options[*offset] = code; options[*offset + 1] = current_offset - *offset - 2; assert(current_offset - *offset - 2 <= UINT8_MAX); *offset = current_offset; break; } default: return dhcp_option_append_tlv(options, size, offset, code, optlen, optval); } return 0; } static int option_length(uint8_t *options, size_t length, size_t offset) { assert(options); assert(offset < length); if (IN_SET(options[offset], SD_DHCP_OPTION_PAD, SD_DHCP_OPTION_END)) return 1; if (length < offset + 2) return -ENOBUFS; /* validating that buffer is long enough */ if (length < offset + 2 + options[offset + 1]) return -ENOBUFS; return options[offset + 1] + 2; } int dhcp_option_find_option(uint8_t *options, size_t length, uint8_t code, size_t *ret_offset) { int r; assert(options); assert(ret_offset); for (size_t offset = 0; offset < length; offset += r) { r = option_length(options, length, offset); if (r < 0) return r; if (code == options[offset]) { *ret_offset = offset; return r; } } return -ENOENT; } int dhcp_option_remove_option(uint8_t *options, size_t length, uint8_t option_code) { int r; size_t offset; assert(options); r = dhcp_option_find_option(options, length, option_code, &offset); if (r < 0) return r; memmove(options + offset, options + offset + r, length - offset - r); return length - r; } int dhcp_option_append(DHCPMessage *message, size_t size, size_t *offset, uint8_t overload, uint8_t code, size_t optlen, const void *optval) { const bool use_file = overload & DHCP_OVERLOAD_FILE; const bool use_sname = overload & DHCP_OVERLOAD_SNAME; int r; assert(message); assert(offset); /* If *offset is in range [0, size), we are writing to ->options, * if *offset is in range [size, size + sizeof(message->file)) and use_file, we are writing to ->file, * if *offset is in range [size + use_file*sizeof(message->file), size + use_file*sizeof(message->file) + sizeof(message->sname)) * and use_sname, we are writing to ->sname. */ if (*offset < size) { /* still space in the options array */ r = option_append(message->options, size, offset, code, optlen, optval); if (r >= 0) return 0; else if (r == -ENOBUFS && (use_file || use_sname)) { /* did not fit, but we have more buffers to try close the options array and move the offset to its end */ r = option_append(message->options, size, offset, SD_DHCP_OPTION_END, 0, NULL); if (r < 0) return r; *offset = size; } else return r; } if (use_file) { size_t file_offset = *offset - size; if (file_offset < sizeof(message->file)) { /* still space in the 'file' array */ r = option_append(message->file, sizeof(message->file), &file_offset, code, optlen, optval); if (r >= 0) { *offset = size + file_offset; return 0; } else if (r == -ENOBUFS && use_sname) { /* did not fit, but we have more buffers to try close the file array and move the offset to its end */ r = option_append(message->file, sizeof(message->file), &file_offset, SD_DHCP_OPTION_END, 0, NULL); if (r < 0) return r; *offset = size + sizeof(message->file); } else return r; } } if (use_sname) { size_t sname_offset = *offset - size - use_file*sizeof(message->file); if (sname_offset < sizeof(message->sname)) { /* still space in the 'sname' array */ r = option_append(message->sname, sizeof(message->sname), &sname_offset, code, optlen, optval); if (r >= 0) { *offset = size + use_file*sizeof(message->file) + sname_offset; return 0; } else /* no space, or other error, give up */ return r; } } return -ENOBUFS; } static int parse_options(const uint8_t options[], size_t buflen, uint8_t *overload, uint8_t *message_type, char **error_message, dhcp_option_callback_t cb, void *userdata) { uint8_t code, len; const uint8_t *option; size_t offset = 0; int r; while (offset < buflen) { code = options[offset ++]; switch (code) { case SD_DHCP_OPTION_PAD: continue; case SD_DHCP_OPTION_END: return 0; } if (buflen < offset + 1) return -ENOBUFS; len = options[offset ++]; if (buflen < offset + len) return -EINVAL; option = &options[offset]; switch (code) { case SD_DHCP_OPTION_MESSAGE_TYPE: if (len != 1) return -EINVAL; if (message_type) *message_type = *option; break; case SD_DHCP_OPTION_ERROR_MESSAGE: if (len == 0) return -EINVAL; if (error_message) { _cleanup_free_ char *string = NULL; r = make_cstring((const char*) option, len, MAKE_CSTRING_ALLOW_TRAILING_NUL, &string); if (r < 0) return r; if (!ascii_is_valid(string)) return -EINVAL; free_and_replace(*error_message, string); } break; case SD_DHCP_OPTION_OVERLOAD: if (len != 1) return -EINVAL; if (overload) *overload = *option; break; default: if (cb) cb(code, len, option, userdata); break; } offset += len; } if (offset < buflen) return -EINVAL; return 0; } int dhcp_option_parse(DHCPMessage *message, size_t len, dhcp_option_callback_t cb, void *userdata, char **ret_error_message) { _cleanup_free_ char *error_message = NULL; uint8_t overload = 0; uint8_t message_type = 0; int r; if (!message) return -EINVAL; if (len < sizeof(DHCPMessage)) return -EINVAL; len -= sizeof(DHCPMessage); r = parse_options(message->options, len, &overload, &message_type, &error_message, cb, userdata); if (r < 0) return r; if (overload & DHCP_OVERLOAD_FILE) { r = parse_options(message->file, sizeof(message->file), NULL, &message_type, &error_message, cb, userdata); if (r < 0) return r; } if (overload & DHCP_OVERLOAD_SNAME) { r = parse_options(message->sname, sizeof(message->sname), NULL, &message_type, &error_message, cb, userdata); if (r < 0) return r; } if (message_type == 0) return -ENOMSG; if (ret_error_message && IN_SET(message_type, DHCP_NAK, DHCP_DECLINE)) *ret_error_message = TAKE_PTR(error_message); return message_type; } int dhcp_option_parse_string(const uint8_t *option, size_t len, char **ret) { int r; assert(option); assert(ret); if (len <= 0) *ret = mfree(*ret); else { char *string; /* * One trailing NUL byte is OK, we don't mind. See: * https://github.com/systemd/systemd/issues/1337 */ r = make_cstring((const char *) option, len, MAKE_CSTRING_ALLOW_TRAILING_NUL, &string); if (r < 0) return r; free_and_replace(*ret, string); } return 0; } static sd_dhcp_option* dhcp_option_free(sd_dhcp_option *i) { if (!i) return NULL; free(i->data); return mfree(i); } int sd_dhcp_option_new(uint8_t option, const void *data, size_t length, sd_dhcp_option **ret) { assert_return(ret, -EINVAL); assert_return(length == 0 || data, -EINVAL); _cleanup_free_ void *q = memdup(data, length); if (!q) return -ENOMEM; sd_dhcp_option *p = new(sd_dhcp_option, 1); if (!p) return -ENOMEM; *p = (sd_dhcp_option) { .n_ref = 1, .option = option, .length = length, .data = TAKE_PTR(q), }; *ret = TAKE_PTR(p); return 0; } DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_option, sd_dhcp_option, dhcp_option_free); DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( dhcp_option_hash_ops, void, trivial_hash_func, trivial_compare_func, sd_dhcp_option, sd_dhcp_option_unref);