]>
Commit | Line | Data |
---|---|---|
db9ecf05 | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
524cf458 | 2 | /*** |
810adae9 | 3 | Copyright © 2013 Intel Corporation. All rights reserved. |
524cf458 PF |
4 | ***/ |
5 | ||
524cf458 | 6 | #include <errno.h> |
cf0fbc49 | 7 | #include <stdint.h> |
524cf458 PF |
8 | #include <stdio.h> |
9 | ||
f693e9b3 | 10 | #include "alloc-util.h" |
524cf458 | 11 | #include "dhcp-internal.h" |
564ca984 | 12 | #include "dhcp-server-internal.h" |
0a970718 LP |
13 | #include "memory-util.h" |
14 | #include "strv.h" | |
15 | #include "utf8.h" | |
524cf458 | 16 | |
11c38d3e YA |
17 | /* Append type-length value structure to the options buffer */ |
18 | static int dhcp_option_append_tlv(uint8_t options[], size_t size, size_t *offset, uint8_t code, size_t optlen, const void *optval) { | |
19 | assert(options); | |
20 | assert(size > 0); | |
21 | assert(offset); | |
22 | assert(optlen <= UINT8_MAX); | |
23 | assert(*offset < size); | |
24 | ||
25 | if (*offset + 2 + optlen > size) | |
26 | return -ENOBUFS; | |
27 | ||
28 | options[*offset] = code; | |
29 | options[*offset + 1] = optlen; | |
30 | ||
31 | memcpy_safe(&options[*offset + 2], optval, optlen); | |
32 | *offset += 2 + optlen; | |
33 | return 0; | |
34 | } | |
35 | ||
04b28be1 TG |
36 | static int option_append(uint8_t options[], size_t size, size_t *offset, |
37 | uint8_t code, size_t optlen, const void *optval) { | |
20b958bf | 38 | assert(options); |
828b603a | 39 | assert(size > 0); |
20b958bf | 40 | assert(offset); |
524cf458 | 41 | |
11c38d3e YA |
42 | int r; |
43 | ||
22805d92 | 44 | if (code != SD_DHCP_OPTION_END) |
2688ef60 | 45 | /* always make sure there is space for an END option */ |
313cefa1 | 46 | size--; |
2688ef60 | 47 | |
524cf458 PF |
48 | switch (code) { |
49 | ||
22805d92 BG |
50 | case SD_DHCP_OPTION_PAD: |
51 | case SD_DHCP_OPTION_END: | |
bc67342e | 52 | if (*offset + 1 > size) |
524cf458 PF |
53 | return -ENOBUFS; |
54 | ||
20b958bf TG |
55 | options[*offset] = code; |
56 | *offset += 1; | |
524cf458 PF |
57 | break; |
58 | ||
af1c0de0 | 59 | case SD_DHCP_OPTION_USER_CLASS: { |
bc67342e | 60 | size_t total = 0; |
af1c0de0 | 61 | |
e4336c0a YW |
62 | if (strv_isempty((char **) optval)) |
63 | return -EINVAL; | |
64 | ||
c9c3b81f | 65 | STRV_FOREACH(s, (const char* const*) optval) { |
bc67342e ZJS |
66 | size_t len = strlen(*s); |
67 | ||
e4336c0a YW |
68 | if (len > 255 || len == 0) |
69 | return -EINVAL; | |
bc67342e ZJS |
70 | |
71 | total += 1 + len; | |
72 | } | |
af1c0de0 | 73 | |
bc67342e | 74 | if (*offset + 2 + total > size) |
af1c0de0 SS |
75 | return -ENOBUFS; |
76 | ||
77 | options[*offset] = code; | |
e4336c0a | 78 | options[*offset + 1] = total; |
af1c0de0 SS |
79 | *offset += 2; |
80 | ||
c9c3b81f | 81 | STRV_FOREACH(s, (const char* const*) optval) { |
bc67342e | 82 | size_t len = strlen(*s); |
af1c0de0 SS |
83 | |
84 | options[*offset] = len; | |
bc67342e ZJS |
85 | memcpy(&options[*offset + 1], *s, len); |
86 | *offset += 1 + len; | |
af1c0de0 SS |
87 | } |
88 | ||
89 | break; | |
90 | } | |
299d578f SS |
91 | case SD_DHCP_OPTION_SIP_SERVER: |
92 | if (*offset + 3 + optlen > size) | |
93 | return -ENOBUFS; | |
94 | ||
95 | options[*offset] = code; | |
96 | options[*offset + 1] = optlen + 1; | |
97 | options[*offset + 2] = 1; | |
98 | ||
99 | memcpy_safe(&options[*offset + 3], optval, optlen); | |
100 | *offset += 3 + optlen; | |
101 | ||
102 | break; | |
564ca984 | 103 | case SD_DHCP_OPTION_VENDOR_SPECIFIC: { |
ebffea2a | 104 | OrderedSet *s = (OrderedSet *) optval; |
461dbb2f | 105 | struct sd_dhcp_option *p; |
564ca984 | 106 | size_t l = 0; |
564ca984 | 107 | |
ebffea2a | 108 | ORDERED_SET_FOREACH(p, s) |
564ca984 SS |
109 | l += p->length + 2; |
110 | ||
111 | if (*offset + l + 2 > size) | |
112 | return -ENOBUFS; | |
113 | ||
114 | options[*offset] = code; | |
115 | options[*offset + 1] = l; | |
564ca984 SS |
116 | *offset += 2; |
117 | ||
ebffea2a | 118 | ORDERED_SET_FOREACH(p, s) { |
11c38d3e YA |
119 | r = dhcp_option_append_tlv(options, size, offset, p->option, p->length, p->data); |
120 | if (r < 0) | |
121 | return r; | |
122 | } | |
123 | break; | |
124 | } | |
125 | case SD_DHCP_OPTION_RELAY_AGENT_INFORMATION: { | |
126 | sd_dhcp_server *server = (sd_dhcp_server *) optval; | |
127 | size_t current_offset = *offset + 2; | |
128 | ||
129 | if (server->agent_circuit_id) { | |
130 | r = dhcp_option_append_tlv(options, size, ¤t_offset, SD_DHCP_RELAY_AGENT_CIRCUIT_ID, | |
131 | strlen(server->agent_circuit_id), server->agent_circuit_id); | |
132 | if (r < 0) | |
133 | return r; | |
134 | } | |
135 | if (server->agent_remote_id) { | |
136 | r = dhcp_option_append_tlv(options, size, ¤t_offset, SD_DHCP_RELAY_AGENT_REMOTE_ID, | |
137 | strlen(server->agent_remote_id), server->agent_remote_id); | |
138 | if (r < 0) | |
139 | return r; | |
564ca984 SS |
140 | } |
141 | ||
11c38d3e YA |
142 | options[*offset] = code; |
143 | options[*offset + 1] = current_offset - *offset - 2; | |
144 | assert(current_offset - *offset - 2 <= UINT8_MAX); | |
145 | *offset = current_offset; | |
564ca984 SS |
146 | break; |
147 | } | |
524cf458 | 148 | default: |
11c38d3e YA |
149 | return dhcp_option_append_tlv(options, size, offset, code, optlen, optval); |
150 | } | |
151 | return 0; | |
152 | } | |
524cf458 | 153 | |
11c38d3e YA |
154 | static int option_length(uint8_t *options, size_t length, size_t offset) { |
155 | assert(options); | |
156 | assert(offset < length); | |
04b28be1 | 157 | |
11c38d3e YA |
158 | if (IN_SET(options[offset], SD_DHCP_OPTION_PAD, SD_DHCP_OPTION_END)) |
159 | return 1; | |
160 | if (length < offset + 2) | |
161 | return -ENOBUFS; | |
524cf458 | 162 | |
11c38d3e YA |
163 | /* validating that buffer is long enough */ |
164 | if (length < offset + 2 + options[offset + 1]) | |
165 | return -ENOBUFS; | |
166 | ||
167 | return options[offset + 1] + 2; | |
168 | } | |
169 | ||
170 | int dhcp_option_find_option(uint8_t *options, size_t length, uint8_t code, size_t *ret_offset) { | |
171 | int r; | |
172 | ||
173 | assert(options); | |
174 | assert(ret_offset); | |
175 | ||
176 | for (size_t offset = 0; offset < length; offset += r) { | |
177 | r = option_length(options, length, offset); | |
178 | if (r < 0) | |
179 | return r; | |
180 | ||
181 | if (code == options[offset]) { | |
182 | *ret_offset = offset; | |
183 | return r; | |
184 | } | |
524cf458 | 185 | } |
11c38d3e YA |
186 | return -ENOENT; |
187 | } | |
524cf458 | 188 | |
11c38d3e YA |
189 | int dhcp_option_remove_option(uint8_t *options, size_t length, uint8_t option_code) { |
190 | int r; | |
191 | size_t offset; | |
192 | ||
193 | assert(options); | |
194 | ||
195 | r = dhcp_option_find_option(options, length, option_code, &offset); | |
196 | if (r < 0) | |
197 | return r; | |
198 | ||
199 | memmove(options + offset, options + offset + r, length - offset - r); | |
200 | return length - r; | |
524cf458 PF |
201 | } |
202 | ||
04b28be1 TG |
203 | int dhcp_option_append(DHCPMessage *message, size_t size, size_t *offset, |
204 | uint8_t overload, | |
205 | uint8_t code, size_t optlen, const void *optval) { | |
bc67342e ZJS |
206 | const bool use_file = overload & DHCP_OVERLOAD_FILE; |
207 | const bool use_sname = overload & DHCP_OVERLOAD_SNAME; | |
04b28be1 TG |
208 | int r; |
209 | ||
210 | assert(message); | |
211 | assert(offset); | |
212 | ||
bc67342e ZJS |
213 | /* If *offset is in range [0, size), we are writing to ->options, |
214 | * if *offset is in range [size, size + sizeof(message->file)) and use_file, we are writing to ->file, | |
215 | * if *offset is in range [size + use_file*sizeof(message->file), size + use_file*sizeof(message->file) + sizeof(message->sname)) | |
216 | * and use_sname, we are writing to ->sname. | |
217 | */ | |
04b28be1 TG |
218 | |
219 | if (*offset < size) { | |
220 | /* still space in the options array */ | |
221 | r = option_append(message->options, size, offset, code, optlen, optval); | |
222 | if (r >= 0) | |
223 | return 0; | |
bc67342e | 224 | else if (r == -ENOBUFS && (use_file || use_sname)) { |
04b28be1 TG |
225 | /* did not fit, but we have more buffers to try |
226 | close the options array and move the offset to its end */ | |
22805d92 | 227 | r = option_append(message->options, size, offset, SD_DHCP_OPTION_END, 0, NULL); |
04b28be1 TG |
228 | if (r < 0) |
229 | return r; | |
230 | ||
231 | *offset = size; | |
232 | } else | |
233 | return r; | |
234 | } | |
235 | ||
bc67342e ZJS |
236 | if (use_file) { |
237 | size_t file_offset = *offset - size; | |
04b28be1 TG |
238 | |
239 | if (file_offset < sizeof(message->file)) { | |
240 | /* still space in the 'file' array */ | |
241 | r = option_append(message->file, sizeof(message->file), &file_offset, code, optlen, optval); | |
242 | if (r >= 0) { | |
243 | *offset = size + file_offset; | |
244 | return 0; | |
bc67342e | 245 | } else if (r == -ENOBUFS && use_sname) { |
04b28be1 TG |
246 | /* did not fit, but we have more buffers to try |
247 | close the file array and move the offset to its end */ | |
828b603a | 248 | r = option_append(message->file, sizeof(message->file), &file_offset, SD_DHCP_OPTION_END, 0, NULL); |
04b28be1 TG |
249 | if (r < 0) |
250 | return r; | |
251 | ||
252 | *offset = size + sizeof(message->file); | |
253 | } else | |
254 | return r; | |
255 | } | |
256 | } | |
257 | ||
bc67342e ZJS |
258 | if (use_sname) { |
259 | size_t sname_offset = *offset - size - use_file*sizeof(message->file); | |
04b28be1 TG |
260 | |
261 | if (sname_offset < sizeof(message->sname)) { | |
262 | /* still space in the 'sname' array */ | |
263 | r = option_append(message->sname, sizeof(message->sname), &sname_offset, code, optlen, optval); | |
264 | if (r >= 0) { | |
bc67342e | 265 | *offset = size + use_file*sizeof(message->file) + sname_offset; |
04b28be1 | 266 | return 0; |
bc67342e | 267 | } else |
04b28be1 TG |
268 | /* no space, or other error, give up */ |
269 | return r; | |
04b28be1 TG |
270 | } |
271 | } | |
272 | ||
273 | return -ENOBUFS; | |
274 | } | |
275 | ||
32008a96 | 276 | static int parse_options(const uint8_t options[], size_t buflen, uint8_t *overload, |
ccf86354 | 277 | uint8_t *message_type, char **error_message, dhcp_option_callback_t cb, |
89ca10c6 | 278 | void *userdata) { |
32008a96 | 279 | uint8_t code, len; |
f693e9b3 | 280 | const uint8_t *option; |
32008a96 | 281 | size_t offset = 0; |
7153213e | 282 | int r; |
524cf458 | 283 | |
32008a96 | 284 | while (offset < buflen) { |
f693e9b3 | 285 | code = options[offset ++]; |
32008a96 | 286 | |
f693e9b3 | 287 | switch (code) { |
22805d92 | 288 | case SD_DHCP_OPTION_PAD: |
f693e9b3 | 289 | continue; |
524cf458 | 290 | |
22805d92 | 291 | case SD_DHCP_OPTION_END: |
524cf458 | 292 | return 0; |
f693e9b3 | 293 | } |
524cf458 | 294 | |
f693e9b3 TG |
295 | if (buflen < offset + 1) |
296 | return -ENOBUFS; | |
297 | ||
298 | len = options[offset ++]; | |
524cf458 | 299 | |
f693e9b3 TG |
300 | if (buflen < offset + len) |
301 | return -EINVAL; | |
302 | ||
303 | option = &options[offset]; | |
304 | ||
305 | switch (code) { | |
22805d92 | 306 | case SD_DHCP_OPTION_MESSAGE_TYPE: |
32008a96 | 307 | if (len != 1) |
524cf458 PF |
308 | return -EINVAL; |
309 | ||
310 | if (message_type) | |
f693e9b3 | 311 | *message_type = *option; |
524cf458 PF |
312 | |
313 | break; | |
314 | ||
22805d92 | 315 | case SD_DHCP_OPTION_ERROR_MESSAGE: |
f693e9b3 | 316 | if (len == 0) |
524cf458 PF |
317 | return -EINVAL; |
318 | ||
f693e9b3 TG |
319 | if (error_message) { |
320 | _cleanup_free_ char *string = NULL; | |
524cf458 | 321 | |
7153213e LP |
322 | r = make_cstring((const char*) option, len, MAKE_CSTRING_ALLOW_TRAILING_NUL, &string); |
323 | if (r < 0) | |
324 | return r; | |
524cf458 | 325 | |
f693e9b3 TG |
326 | if (!ascii_is_valid(string)) |
327 | return -EINVAL; | |
524cf458 | 328 | |
f9ecfd3b | 329 | free_and_replace(*error_message, string); |
f693e9b3 | 330 | } |
524cf458 | 331 | |
f693e9b3 | 332 | break; |
22805d92 | 333 | case SD_DHCP_OPTION_OVERLOAD: |
f693e9b3 | 334 | if (len != 1) |
524cf458 | 335 | return -EINVAL; |
524cf458 | 336 | |
f693e9b3 TG |
337 | if (overload) |
338 | *overload = *option; | |
524cf458 | 339 | |
f693e9b3 TG |
340 | break; |
341 | ||
342 | default: | |
343 | if (cb) | |
344 | cb(code, len, option, userdata); | |
524cf458 PF |
345 | |
346 | break; | |
347 | } | |
f693e9b3 TG |
348 | |
349 | offset += len; | |
524cf458 PF |
350 | } |
351 | ||
32008a96 | 352 | if (offset < buflen) |
524cf458 PF |
353 | return -EINVAL; |
354 | ||
355 | return 0; | |
356 | } | |
357 | ||
7b1fac1e | 358 | int dhcp_option_parse(DHCPMessage *message, size_t len, dhcp_option_callback_t cb, void *userdata, char **ret_error_message) { |
f693e9b3 | 359 | _cleanup_free_ char *error_message = NULL; |
524cf458 PF |
360 | uint8_t overload = 0; |
361 | uint8_t message_type = 0; | |
32008a96 | 362 | int r; |
524cf458 PF |
363 | |
364 | if (!message) | |
365 | return -EINVAL; | |
366 | ||
3b7ca119 | 367 | if (len < sizeof(DHCPMessage)) |
524cf458 PF |
368 | return -EINVAL; |
369 | ||
3b7ca119 | 370 | len -= sizeof(DHCPMessage); |
524cf458 | 371 | |
f693e9b3 | 372 | r = parse_options(message->options, len, &overload, &message_type, &error_message, cb, userdata); |
32008a96 TG |
373 | if (r < 0) |
374 | return r; | |
524cf458 PF |
375 | |
376 | if (overload & DHCP_OVERLOAD_FILE) { | |
f693e9b3 | 377 | r = parse_options(message->file, sizeof(message->file), NULL, &message_type, &error_message, cb, userdata); |
32008a96 TG |
378 | if (r < 0) |
379 | return r; | |
524cf458 PF |
380 | } |
381 | ||
382 | if (overload & DHCP_OVERLOAD_SNAME) { | |
f693e9b3 | 383 | r = parse_options(message->sname, sizeof(message->sname), NULL, &message_type, &error_message, cb, userdata); |
32008a96 TG |
384 | if (r < 0) |
385 | return r; | |
524cf458 PF |
386 | } |
387 | ||
f693e9b3 TG |
388 | if (message_type == 0) |
389 | return -ENOMSG; | |
390 | ||
7b1fac1e YW |
391 | if (ret_error_message && IN_SET(message_type, DHCP_NAK, DHCP_DECLINE)) |
392 | *ret_error_message = TAKE_PTR(error_message); | |
524cf458 | 393 | |
f693e9b3 | 394 | return message_type; |
524cf458 | 395 | } |
461dbb2f | 396 | |
930133d5 RH |
397 | int dhcp_option_parse_string(const uint8_t *option, size_t len, char **ret) { |
398 | int r; | |
399 | ||
400 | assert(option); | |
401 | assert(ret); | |
402 | ||
403 | if (len <= 0) | |
404 | *ret = mfree(*ret); | |
405 | else { | |
406 | char *string; | |
407 | ||
408 | /* | |
409 | * One trailing NUL byte is OK, we don't mind. See: | |
410 | * https://github.com/systemd/systemd/issues/1337 | |
411 | */ | |
412 | r = make_cstring((const char *) option, len, MAKE_CSTRING_ALLOW_TRAILING_NUL, &string); | |
413 | if (r < 0) | |
414 | return r; | |
415 | ||
416 | free_and_replace(*ret, string); | |
417 | } | |
418 | ||
419 | return 0; | |
420 | } | |
421 | ||
461dbb2f YW |
422 | static sd_dhcp_option* dhcp_option_free(sd_dhcp_option *i) { |
423 | if (!i) | |
424 | return NULL; | |
425 | ||
426 | free(i->data); | |
427 | return mfree(i); | |
428 | } | |
429 | ||
430 | int sd_dhcp_option_new(uint8_t option, const void *data, size_t length, sd_dhcp_option **ret) { | |
431 | assert_return(ret, -EINVAL); | |
432 | assert_return(length == 0 || data, -EINVAL); | |
433 | ||
434 | _cleanup_free_ void *q = memdup(data, length); | |
435 | if (!q) | |
436 | return -ENOMEM; | |
437 | ||
438 | sd_dhcp_option *p = new(sd_dhcp_option, 1); | |
439 | if (!p) | |
440 | return -ENOMEM; | |
441 | ||
442 | *p = (sd_dhcp_option) { | |
443 | .n_ref = 1, | |
444 | .option = option, | |
445 | .length = length, | |
446 | .data = TAKE_PTR(q), | |
447 | }; | |
448 | ||
449 | *ret = TAKE_PTR(p); | |
450 | return 0; | |
451 | } | |
452 | ||
453 | DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_option, sd_dhcp_option, dhcp_option_free); | |
454 | DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( | |
455 | dhcp_option_hash_ops, | |
456 | void, | |
457 | trivial_hash_func, | |
458 | trivial_compare_func, | |
459 | sd_dhcp_option, | |
460 | sd_dhcp_option_unref); |