Coverage Report

Created: 2026-02-26 06:38

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}