/src/freeradius-server/src/protocols/dhcpv4/packet.c
Line | Count | Source |
1 | | /* |
2 | | * This library is free software; you can redistribute it and/or |
3 | | * modify it under the terms of the GNU Lesser General Public |
4 | | * License as published by the Free Software Foundation; either |
5 | | * version 2.1 of the License, or (at your option) any later version. |
6 | | * |
7 | | * This library is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
10 | | * Lesser General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU Lesser General Public |
13 | | * License along with this library; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA |
15 | | */ |
16 | | |
17 | | /** |
18 | | * $Id: 313213ebf0be056284f81c4271e594606f9d1b49 $ |
19 | | * |
20 | | * @file protocols/dhcpv4/packet.c |
21 | | * @brief Functions to encode/decode DHCP packets. |
22 | | * |
23 | | * @copyright 2008,2017 The FreeRADIUS server project |
24 | | * @copyright 2008 Alan DeKok (aland@deployingradius.com) |
25 | | */ |
26 | | #include <freeradius-devel/util/pair.h> |
27 | | #include <freeradius-devel/util/rand.h> |
28 | | #include <freeradius-devel/protocol/dhcpv4/rfc2131.h> |
29 | | |
30 | | #include "dhcpv4.h" |
31 | | #include "attrs.h" |
32 | | |
33 | | /** Retrieve a DHCP option from a raw packet buffer |
34 | | * |
35 | | * |
36 | | */ |
37 | | uint8_t const *fr_dhcpv4_packet_get_option(dhcp_packet_t const *packet, size_t packet_size, fr_dict_attr_t const *da) |
38 | 463 | { |
39 | 463 | int overload = 0; |
40 | 463 | int field = DHCP_OPTION_FIELD; |
41 | 463 | size_t where, size; |
42 | 463 | uint8_t const *data; |
43 | | |
44 | 463 | if (packet_size < MIN_PACKET_SIZE) return NULL; |
45 | | |
46 | | /* |
47 | | * This is needed for UBSAN on MacOS, that doesn't |
48 | | * allow misaligned accesses. Because the packet |
49 | | * structure is flat, we don't need to deref the |
50 | | * packet pointer at any point, we just need to |
51 | | * calculate the offsets relative to the pointer |
52 | | * value and use those... Whatever actually deals |
53 | | * with the option is just expecting a uint8_t *. |
54 | | */ |
55 | 463 | #define ALIGNED_ACCESS(packet, field) \ |
56 | 771 | (uint8_t const *)packet + offsetof(dhcp_packet_t, field) |
57 | | |
58 | 463 | where = 0; |
59 | 463 | size = packet_size - offsetof(dhcp_packet_t, options); |
60 | | |
61 | | /* |
62 | | * Alignment fix. We can't just deref a pointer |
63 | | */ |
64 | 463 | data = ALIGNED_ACCESS(packet, options); |
65 | 9.43k | while (where < size) { |
66 | 9.42k | if (data[0] == 0) { /* padding */ |
67 | 2.44k | where++; |
68 | 2.44k | data++; |
69 | 2.44k | continue; |
70 | 2.44k | } |
71 | | |
72 | 6.98k | if (data[0] == 255) { /* end of options */ |
73 | 309 | if ((field == DHCP_OPTION_FIELD) && (overload & DHCP_FILE_FIELD)) { |
74 | 290 | data = ALIGNED_ACCESS(packet, file); |
75 | 290 | where = 0; |
76 | 290 | size = sizeof(packet->file); |
77 | 290 | field = DHCP_FILE_FIELD; |
78 | 290 | continue; |
79 | | |
80 | 290 | } else if ((field == DHCP_FILE_FIELD || field == DHCP_OPTION_FIELD) && (overload & DHCP_SNAME_FIELD)) { |
81 | 18 | data = ALIGNED_ACCESS(packet, sname); |
82 | 18 | where = 0; |
83 | 18 | size = sizeof(packet->sname); |
84 | 18 | field = DHCP_SNAME_FIELD; |
85 | 18 | continue; |
86 | 18 | } |
87 | | |
88 | 1 | return NULL; |
89 | 309 | } |
90 | | |
91 | | /* |
92 | | * We MUST have a real option here. |
93 | | */ |
94 | 6.67k | if ((where + 2) > size) { |
95 | 1 | fr_strerror_printf("Options overflow field at %u", |
96 | 1 | (unsigned int) (data - (uint8_t const *) packet)); |
97 | 1 | return NULL; |
98 | 1 | } |
99 | | |
100 | 6.67k | if ((where + 2 + data[1]) > size) { |
101 | 11 | fr_strerror_printf("Option length overflows field at %u", |
102 | 11 | (unsigned int) (data - (uint8_t const *) packet)); |
103 | 11 | return NULL; |
104 | 11 | } |
105 | | |
106 | 6.65k | if (data[0] == da->attr) return data; |
107 | | |
108 | 6.21k | if ((data[0] == 52) && (data[1] > 0)) { /* overload sname and/or file */ |
109 | 567 | overload = data[2]; |
110 | 567 | } |
111 | | |
112 | 6.21k | where += data[1] + 2; |
113 | 6.21k | data += data[1] + 2; |
114 | 6.21k | } |
115 | | |
116 | 9 | return NULL; |
117 | 463 | } |
118 | | |
119 | | int fr_dhcpv4_decode(TALLOC_CTX *ctx, fr_pair_list_t *out, uint8_t const *data, size_t data_len, unsigned int *code) |
120 | 200 | { |
121 | 200 | size_t i; |
122 | 200 | uint8_t const *p = data; |
123 | 200 | uint32_t giaddr; |
124 | 200 | fr_pair_list_t tmp; |
125 | 200 | fr_pair_t *vp; |
126 | 200 | fr_pair_t *maxms, *mtu, *netaddr; |
127 | 200 | fr_value_box_t box; |
128 | 200 | fr_dhcpv4_ctx_t *packet_ctx; |
129 | | |
130 | 200 | fr_pair_list_init(&tmp); |
131 | | |
132 | 200 | if (data[1] > 1) { |
133 | 0 | fr_strerror_printf("Packet is not Ethernet: %u", |
134 | 0 | data[1]); |
135 | 0 | return -1; |
136 | 0 | } |
137 | | |
138 | 200 | packet_ctx = talloc_zero(ctx, fr_dhcpv4_ctx_t); |
139 | 200 | if (!packet_ctx) return -1; |
140 | 200 | packet_ctx->tmp_ctx = talloc(packet_ctx, uint8_t); |
141 | 200 | packet_ctx->root = fr_dict_root(dict_dhcpv4); |
142 | | |
143 | | /* |
144 | | * Decode the header. |
145 | | */ |
146 | 3.00k | for (i = 0; i < dhcp_header_attrs_len; i++) { |
147 | 2.80k | fr_dict_attr_t const *da = *dhcp_header_attrs[i]; |
148 | | |
149 | 2.80k | vp = fr_pair_afrom_da(ctx, da); |
150 | 2.80k | if (!vp) { |
151 | 0 | fr_strerror_const_push("Cannot decode packet due to internal error"); |
152 | 0 | error_vp: |
153 | 0 | talloc_free(vp); |
154 | 0 | error: |
155 | 0 | fr_pair_list_free(&tmp); |
156 | 0 | talloc_free(packet_ctx); |
157 | 0 | return -1; |
158 | 0 | } |
159 | | |
160 | 2.80k | switch (vp->vp_type) { |
161 | 400 | case FR_TYPE_STRING: |
162 | | /* |
163 | | * According to RFC 2131, these are null terminated strings. |
164 | | * We don't trust everyone to abide by the RFC, though. |
165 | | */ |
166 | 400 | if (*p != '\0') { |
167 | 323 | uint8_t *q; |
168 | | |
169 | 323 | q = memchr(p, '\0', dhcp_header_sizes[i]); |
170 | 323 | fr_pair_value_bstrndup(vp, (char const *)p, q ? q - p : dhcp_header_sizes[i], true); |
171 | 323 | } else { |
172 | 77 | TALLOC_FREE(vp); |
173 | 77 | } |
174 | 400 | break; |
175 | | |
176 | | /* |
177 | | * The DHCP header size for CHADDR is not |
178 | | * 6, so the value_box function doesn't |
179 | | * like it. Just do the copy manually. |
180 | | */ |
181 | 200 | case FR_TYPE_ETHERNET: |
182 | 200 | if ((data[1] != 1) || (data[2] != 6)) { |
183 | 180 | TALLOC_FREE(vp); |
184 | 180 | break; |
185 | 180 | } |
186 | | |
187 | 20 | memcpy(vp->vp_ether, p, sizeof(vp->vp_ether)); |
188 | 20 | break; |
189 | | |
190 | 2.20k | default: |
191 | 2.20k | if (fr_value_box_from_network(vp, &vp->data, vp->vp_type, vp->da, |
192 | 2.20k | &FR_DBUFF_TMP(p, (size_t)dhcp_header_sizes[i]), |
193 | 2.20k | dhcp_header_sizes[i], true) < 0) goto error_vp; |
194 | 2.20k | break; |
195 | 2.80k | } |
196 | 2.80k | p += dhcp_header_sizes[i]; |
197 | | |
198 | 2.80k | if (!vp) continue; |
199 | | |
200 | 2.54k | fr_pair_append(&tmp, vp); |
201 | 2.54k | } |
202 | | |
203 | | /* |
204 | | * Nothing uses tail after this call, if it does in the future |
205 | | * it'll need to find the new tail... |
206 | | */ |
207 | 200 | { |
208 | 200 | uint8_t const *end; |
209 | 200 | ssize_t len; |
210 | | |
211 | 200 | p = data + 240; |
212 | 200 | end = p + (data_len - 240); |
213 | | |
214 | | /* |
215 | | * Loop over all the options data |
216 | | */ |
217 | 2.67k | while (p < end) { |
218 | 2.49k | len = fr_dhcpv4_decode_option(ctx, &tmp, p, (end - p), packet_ctx); |
219 | 2.49k | if (len <= 0) { |
220 | 75 | fail: |
221 | 75 | fr_pair_list_free(&tmp); |
222 | 75 | talloc_free(packet_ctx); |
223 | 75 | return -1; |
224 | 18 | } |
225 | 2.47k | p += len; |
226 | 2.47k | } |
227 | | |
228 | 182 | if (code) { |
229 | 182 | vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_message_type); |
230 | 182 | if (vp) { |
231 | 100 | *code = vp->vp_uint8; |
232 | 100 | } |
233 | 182 | } |
234 | | |
235 | | /* |
236 | | * If option Overload is present in the 'options' field, then fields 'file' and/or 'sname' |
237 | | * are used to hold more options. They are partitioned and must be interpreted in sequence. |
238 | | */ |
239 | 182 | vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_overload); |
240 | 182 | if (vp) { |
241 | 133 | if ((vp->vp_uint8 & 1) == 1) { |
242 | | /* |
243 | | * The 'file' field is used to hold options. |
244 | | * It must be interpreted before 'sname'. |
245 | | */ |
246 | 133 | p = data + offsetof(dhcp_packet_t, file); |
247 | 133 | end = p + DHCP_FILE_LEN; |
248 | 3.05k | while (p < end) { |
249 | 2.97k | len = fr_dhcpv4_decode_option(ctx, &tmp, |
250 | 2.97k | p, end - p, packet_ctx); |
251 | 2.97k | if (len <= 0) goto fail; |
252 | 2.92k | p += len; |
253 | 2.92k | } |
254 | 79 | fr_pair_delete_by_da(&tmp, attr_dhcp_boot_filename); |
255 | 79 | } |
256 | 79 | if ((vp->vp_uint8 & 2) == 2) { |
257 | | /* |
258 | | * The 'sname' field is used to hold options. |
259 | | */ |
260 | 78 | p = data + offsetof(dhcp_packet_t, sname); |
261 | 78 | end = p + DHCP_SNAME_LEN; |
262 | 965 | while (p < end) { |
263 | 890 | len = fr_dhcpv4_decode_option(ctx, &tmp, |
264 | 890 | p, end - p, packet_ctx); |
265 | 890 | if (len <= 0) goto fail; |
266 | 887 | p += len; |
267 | 887 | } |
268 | 75 | fr_pair_delete_by_da(&tmp, attr_dhcp_server_host_name); |
269 | 75 | } |
270 | 79 | } |
271 | 182 | } |
272 | | |
273 | | /* |
274 | | * If DHCP request, set ciaddr to zero. |
275 | | */ |
276 | | |
277 | | /* |
278 | | * Set broadcast flag for broken vendors, but only if |
279 | | * giaddr isn't set. |
280 | | */ |
281 | 125 | memcpy(&giaddr, data + 24, sizeof(giaddr)); |
282 | 125 | if (giaddr == htonl(INADDR_ANY)) { |
283 | | /* |
284 | | * DHCP Opcode is request |
285 | | */ |
286 | 6 | vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_opcode); |
287 | 6 | if (vp && vp->vp_uint8 == 1) { |
288 | | /* |
289 | | * Vendor is "MSFT 98" |
290 | | */ |
291 | 0 | vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_vendor_class_identifier); |
292 | 0 | if (vp && (vp->vp_length == 7) && (memcmp(vp->vp_strvalue, "MSFT 98", 7) == 0)) { |
293 | 0 | vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_flags); |
294 | | |
295 | | /* |
296 | | * Reply should be broadcast. |
297 | | */ |
298 | 0 | if (vp) vp->vp_uint16 |= 0x8000; |
299 | 0 | } |
300 | 0 | } |
301 | 6 | } |
302 | | |
303 | | /* |
304 | | * Determine the address to use in looking up which subnet the |
305 | | * client belongs to based on packet data. The sequence here |
306 | | * is based on ISC DHCP behaviour and RFCs 3527 and 3011. We |
307 | | * store the found address in an internal attribute of |
308 | | * Network-Subnet |
309 | | * |
310 | | * |
311 | | * All of these options / fields are type "ipv4addr", so |
312 | | * we need to decode them as that. And then cast it to |
313 | | * "ipv4prefix". |
314 | | */ |
315 | 125 | vp = fr_pair_afrom_da(ctx, attr_dhcp_network_subnet); |
316 | 125 | if (!vp) goto error; |
317 | | |
318 | | /* |
319 | | * First look for Relay-Link-Selection |
320 | | */ |
321 | 125 | netaddr = fr_pair_find_by_da_nested(&tmp, NULL, attr_dhcp_relay_link_selection); |
322 | 125 | if (!netaddr) { |
323 | | /* |
324 | | * Next try Subnet-Selection-Option |
325 | | */ |
326 | 125 | netaddr = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_subnet_selection_option); |
327 | 125 | } |
328 | | |
329 | 125 | if (netaddr) { |
330 | | /* |
331 | | * Store whichever address we found from options and ensure |
332 | | * the data type matches the pair, i.e address to prefix |
333 | | * conversion. |
334 | | */ |
335 | 0 | if (fr_value_box_cast(vp, &vp->data, vp->vp_type, vp->da, &netaddr->data) < 0) goto error_vp; |
336 | |
|
337 | 125 | } else if (giaddr != htonl(INADDR_ANY)) { |
338 | | /* |
339 | | * Gateway address is set - use that one |
340 | | */ |
341 | 119 | if (fr_value_box_from_network(vp, &box, FR_TYPE_IPV4_ADDR, NULL, |
342 | 119 | &FR_DBUFF_TMP(data + 24, 4), 4, true) < 0) goto error_vp; |
343 | 119 | if (fr_value_box_cast(vp, &vp->data, vp->vp_type, vp->da, &box) < 0) goto error_vp; |
344 | | |
345 | 119 | } else { |
346 | | /* |
347 | | * else, store client address whatever it is |
348 | | */ |
349 | 6 | if (fr_value_box_from_network(vp, &box, FR_TYPE_IPV4_ADDR, NULL, |
350 | 6 | &FR_DBUFF_TMP(data + 12, 4), 4, true) < 0) goto error_vp; |
351 | 6 | if (fr_value_box_cast(vp, &vp->data, vp->vp_type, vp->da, &box) < 0) goto error_vp; |
352 | 6 | } |
353 | | |
354 | 125 | fr_pair_append(&tmp, vp); |
355 | | |
356 | | /* |
357 | | * Client can request a LARGER size, but not a smaller |
358 | | * one. They also cannot request a size larger than MTU. |
359 | | */ |
360 | 125 | maxms = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_dhcp_maximum_msg_size); |
361 | 125 | mtu = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_interface_mtu_size); |
362 | | |
363 | 125 | if (mtu && (mtu->vp_uint16 < DEFAULT_PACKET_SIZE)) { |
364 | 0 | fr_strerror_const("Client says MTU is smaller than minimum permitted by the specification"); |
365 | 0 | goto error; |
366 | 0 | } |
367 | | |
368 | | /* |
369 | | * Client says maximum message size is smaller than minimum permitted |
370 | | * by the specification: fixing it. |
371 | | */ |
372 | 125 | if (maxms && (maxms->vp_uint16 < DEFAULT_PACKET_SIZE)) maxms->vp_uint16 = DEFAULT_PACKET_SIZE; |
373 | | |
374 | | /* |
375 | | * Client says MTU is smaller than maximum message size: fixing it |
376 | | */ |
377 | 125 | if (maxms && mtu && (maxms->vp_uint16 > mtu->vp_uint16)) maxms->vp_uint16 = mtu->vp_uint16; |
378 | | |
379 | | /* |
380 | | * FIXME: Nuke attributes that aren't used in the normal |
381 | | * header for discover/requests. |
382 | | */ |
383 | 125 | fr_pair_list_append(out, &tmp); |
384 | | |
385 | 125 | return 0; |
386 | 125 | } |
387 | | |
388 | | int fr_dhcpv4_packet_encode(fr_packet_t *packet, fr_pair_list_t *list) |
389 | 0 | { |
390 | 0 | ssize_t len; |
391 | 0 | fr_pair_t *vp; |
392 | |
|
393 | 0 | if (packet->data) return 0; |
394 | | |
395 | 0 | packet->data_len = MAX_PACKET_SIZE; |
396 | 0 | packet->data = talloc_zero_array(packet, uint8_t, packet->data_len); |
397 | | |
398 | | /* XXX Ugly ... should be set by the caller */ |
399 | 0 | if (packet->code == 0) packet->code = FR_DHCP_NAK; |
400 | | |
401 | | /* store xid */ |
402 | 0 | if ((vp = fr_pair_find_by_da(list, NULL, attr_dhcp_transaction_id))) { |
403 | 0 | packet->id = vp->vp_uint32; |
404 | 0 | } else { |
405 | 0 | packet->id = fr_rand(); |
406 | 0 | } |
407 | |
|
408 | 0 | len = fr_dhcpv4_encode(packet->data, packet->data_len, NULL, packet->code, packet->id, list); |
409 | 0 | if (len < 0) return -1; |
410 | | |
411 | 0 | packet->data_len = len; |
412 | |
|
413 | 0 | return 0; |
414 | 0 | } |
415 | | |
416 | | fr_packet_t *fr_dhcpv4_packet_alloc(uint8_t const *data, ssize_t data_len) |
417 | 0 | { |
418 | 0 | fr_packet_t *packet; |
419 | 0 | uint32_t magic; |
420 | 0 | uint8_t const *code; |
421 | |
|
422 | 0 | code = fr_dhcpv4_packet_get_option((dhcp_packet_t const *) data, data_len, attr_dhcp_message_type); |
423 | 0 | if (!code) return NULL; |
424 | | |
425 | 0 | if (data_len < MIN_PACKET_SIZE) return NULL; |
426 | | |
427 | | /* Now that checks are done, allocate packet */ |
428 | 0 | packet = fr_packet_alloc(NULL, false); |
429 | 0 | if (!packet) { |
430 | 0 | fr_strerror_const("Failed allocating packet"); |
431 | 0 | return NULL; |
432 | 0 | } |
433 | | |
434 | | /* |
435 | | * Get XID. |
436 | | */ |
437 | 0 | memcpy(&magic, data + 4, 4); |
438 | |
|
439 | 0 | packet->data_len = data_len; |
440 | 0 | packet->code = code[2]; |
441 | 0 | packet->id = ntohl(magic); |
442 | | |
443 | | /* |
444 | | * FIXME: for DISCOVER / REQUEST: src_port == dst_port + 1 |
445 | | * FIXME: for OFFER / ACK : src_port = dst_port - 1 |
446 | | */ |
447 | | |
448 | | /* |
449 | | * Unique keys are xid, client mac, and client ID? |
450 | | */ |
451 | 0 | return packet; |
452 | 0 | } |