]>
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 | ||
bc67342e ZJS |
65 | STRV_FOREACH(s, (char **) optval) { |
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 | ||
81 | STRV_FOREACH(s, (char **) 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; |
524cf458 | 282 | |
32008a96 | 283 | while (offset < buflen) { |
f693e9b3 | 284 | code = options[offset ++]; |
32008a96 | 285 | |
f693e9b3 | 286 | switch (code) { |
22805d92 | 287 | case SD_DHCP_OPTION_PAD: |
f693e9b3 | 288 | continue; |
524cf458 | 289 | |
22805d92 | 290 | case SD_DHCP_OPTION_END: |
524cf458 | 291 | return 0; |
f693e9b3 | 292 | } |
524cf458 | 293 | |
f693e9b3 TG |
294 | if (buflen < offset + 1) |
295 | return -ENOBUFS; | |
296 | ||
297 | len = options[offset ++]; | |
524cf458 | 298 | |
f693e9b3 TG |
299 | if (buflen < offset + len) |
300 | return -EINVAL; | |
301 | ||
302 | option = &options[offset]; | |
303 | ||
304 | switch (code) { | |
22805d92 | 305 | case SD_DHCP_OPTION_MESSAGE_TYPE: |
32008a96 | 306 | if (len != 1) |
524cf458 PF |
307 | return -EINVAL; |
308 | ||
309 | if (message_type) | |
f693e9b3 | 310 | *message_type = *option; |
524cf458 PF |
311 | |
312 | break; | |
313 | ||
22805d92 | 314 | case SD_DHCP_OPTION_ERROR_MESSAGE: |
f693e9b3 | 315 | if (len == 0) |
524cf458 PF |
316 | return -EINVAL; |
317 | ||
f693e9b3 TG |
318 | if (error_message) { |
319 | _cleanup_free_ char *string = NULL; | |
524cf458 | 320 | |
f693e9b3 TG |
321 | /* Accept a trailing NUL byte */ |
322 | if (memchr(option, 0, len - 1)) | |
323 | return -EINVAL; | |
524cf458 | 324 | |
79cd22d6 | 325 | string = memdup_suffix0((const char *) option, len); |
f693e9b3 TG |
326 | if (!string) |
327 | return -ENOMEM; | |
524cf458 | 328 | |
f693e9b3 TG |
329 | if (!ascii_is_valid(string)) |
330 | return -EINVAL; | |
524cf458 | 331 | |
f9ecfd3b | 332 | free_and_replace(*error_message, string); |
f693e9b3 | 333 | } |
524cf458 | 334 | |
f693e9b3 | 335 | break; |
22805d92 | 336 | case SD_DHCP_OPTION_OVERLOAD: |
f693e9b3 | 337 | if (len != 1) |
524cf458 | 338 | return -EINVAL; |
524cf458 | 339 | |
f693e9b3 TG |
340 | if (overload) |
341 | *overload = *option; | |
524cf458 | 342 | |
f693e9b3 TG |
343 | break; |
344 | ||
345 | default: | |
346 | if (cb) | |
347 | cb(code, len, option, userdata); | |
524cf458 PF |
348 | |
349 | break; | |
350 | } | |
f693e9b3 TG |
351 | |
352 | offset += len; | |
524cf458 PF |
353 | } |
354 | ||
32008a96 | 355 | if (offset < buflen) |
524cf458 PF |
356 | return -EINVAL; |
357 | ||
358 | return 0; | |
359 | } | |
360 | ||
ccf86354 | 361 | int dhcp_option_parse(DHCPMessage *message, size_t len, dhcp_option_callback_t cb, void *userdata, char **_error_message) { |
f693e9b3 | 362 | _cleanup_free_ char *error_message = NULL; |
524cf458 PF |
363 | uint8_t overload = 0; |
364 | uint8_t message_type = 0; | |
32008a96 | 365 | int r; |
524cf458 PF |
366 | |
367 | if (!message) | |
368 | return -EINVAL; | |
369 | ||
3b7ca119 | 370 | if (len < sizeof(DHCPMessage)) |
524cf458 PF |
371 | return -EINVAL; |
372 | ||
3b7ca119 | 373 | len -= sizeof(DHCPMessage); |
524cf458 | 374 | |
f693e9b3 | 375 | r = parse_options(message->options, len, &overload, &message_type, &error_message, cb, userdata); |
32008a96 TG |
376 | if (r < 0) |
377 | return r; | |
524cf458 PF |
378 | |
379 | if (overload & DHCP_OVERLOAD_FILE) { | |
f693e9b3 | 380 | r = parse_options(message->file, sizeof(message->file), NULL, &message_type, &error_message, cb, userdata); |
32008a96 TG |
381 | if (r < 0) |
382 | return r; | |
524cf458 PF |
383 | } |
384 | ||
385 | if (overload & DHCP_OVERLOAD_SNAME) { | |
f693e9b3 | 386 | r = parse_options(message->sname, sizeof(message->sname), NULL, &message_type, &error_message, cb, userdata); |
32008a96 TG |
387 | if (r < 0) |
388 | return r; | |
524cf458 PF |
389 | } |
390 | ||
f693e9b3 TG |
391 | if (message_type == 0) |
392 | return -ENOMSG; | |
393 | ||
ae2a15bc LP |
394 | if (_error_message && IN_SET(message_type, DHCP_NAK, DHCP_DECLINE)) |
395 | *_error_message = TAKE_PTR(error_message); | |
524cf458 | 396 | |
f693e9b3 | 397 | return message_type; |
524cf458 | 398 | } |
461dbb2f YW |
399 | |
400 | static sd_dhcp_option* dhcp_option_free(sd_dhcp_option *i) { | |
401 | if (!i) | |
402 | return NULL; | |
403 | ||
404 | free(i->data); | |
405 | return mfree(i); | |
406 | } | |
407 | ||
408 | int sd_dhcp_option_new(uint8_t option, const void *data, size_t length, sd_dhcp_option **ret) { | |
409 | assert_return(ret, -EINVAL); | |
410 | assert_return(length == 0 || data, -EINVAL); | |
411 | ||
412 | _cleanup_free_ void *q = memdup(data, length); | |
413 | if (!q) | |
414 | return -ENOMEM; | |
415 | ||
416 | sd_dhcp_option *p = new(sd_dhcp_option, 1); | |
417 | if (!p) | |
418 | return -ENOMEM; | |
419 | ||
420 | *p = (sd_dhcp_option) { | |
421 | .n_ref = 1, | |
422 | .option = option, | |
423 | .length = length, | |
424 | .data = TAKE_PTR(q), | |
425 | }; | |
426 | ||
427 | *ret = TAKE_PTR(p); | |
428 | return 0; | |
429 | } | |
430 | ||
431 | DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_option, sd_dhcp_option, dhcp_option_free); | |
432 | DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( | |
433 | dhcp_option_hash_ops, | |
434 | void, | |
435 | trivial_hash_func, | |
436 | trivial_compare_func, | |
437 | sd_dhcp_option, | |
438 | sd_dhcp_option_unref); |