Coverage Report

Created: 2026-08-08 07:10

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: be5011bf051d31eab6af075efd45770b875c691c $
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
1.06k
{
39
1.06k
  int overload = 0;
40
1.06k
  int field = DHCP_OPTION_FIELD;
41
1.06k
  size_t where, size;
42
1.06k
  uint8_t const *data;
43
44
1.06k
  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
1.06k
#define ALIGNED_ACCESS(packet, field) \
56
1.74k
    (uint8_t const *)packet + offsetof(dhcp_packet_t, field)
57
58
1.06k
  where = 0;
59
1.06k
  size = packet_size - offsetof(dhcp_packet_t, options);
60
61
  /*
62
   *  Alignment fix.  We can't just deref a pointer
63
   */
64
1.06k
  data = ALIGNED_ACCESS(packet, options);
65
18.4k
  while (where < size) {
66
18.4k
    if (data[0] == 0) { /* padding */
67
4.61k
      where++;
68
4.61k
      data++;
69
4.61k
      continue;
70
4.61k
    }
71
72
13.8k
    if (data[0] == 255) { /* end of options */
73
693
      if ((field == DHCP_OPTION_FIELD) && (overload & DHCP_FILE_FIELD)) {
74
644
        data = ALIGNED_ACCESS(packet, file);
75
644
        where = 0;
76
644
        size = sizeof(packet->file);
77
644
        field = DHCP_FILE_FIELD;
78
644
        continue;
79
80
644
      } else if ((field == DHCP_FILE_FIELD || field == DHCP_OPTION_FIELD) && (overload & DHCP_SNAME_FIELD)) {
81
45
        data = ALIGNED_ACCESS(packet, sname);
82
45
        where = 0;
83
45
        size = sizeof(packet->sname);
84
45
        field = DHCP_SNAME_FIELD;
85
45
        continue;
86
45
      }
87
88
4
      return NULL;
89
693
    }
90
91
    /*
92
     *  We MUST have a real option here.
93
     */
94
13.1k
    if ((where + 2) > size) {
95
5
      fr_strerror_printf("Options overflow field at %u",
96
5
             (unsigned int) (data - (uint8_t const *) packet));
97
5
      return NULL;
98
5
    }
99
100
13.1k
    if ((where + 2 + data[1]) > size) {
101
15
      fr_strerror_printf("Option length overflows field at %u",
102
15
             (unsigned int) (data - (uint8_t const *) packet));
103
15
      return NULL;
104
15
    }
105
106
13.1k
    if (data[0] == da->attr) return data;
107
108
12.1k
    if ((data[0] == 52) && (data[1] > 0)) { /* overload sname and/or file */
109
1.01k
      overload = data[2];
110
1.01k
    }
111
112
12.1k
    where += data[1] + 2;
113
12.1k
    data += data[1] + 2;
114
12.1k
  }
115
116
6
  return NULL;
117
1.06k
}
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
1.01k
{
121
1.01k
  size_t    i;
122
1.01k
  uint8_t const   *p = data;
123
1.01k
  uint32_t  giaddr;
124
1.01k
  fr_pair_list_t  tmp;
125
1.01k
  fr_pair_t *vp;
126
1.01k
  fr_pair_t *maxms, *mtu, *netaddr;
127
1.01k
  fr_value_box_t  box;
128
1.01k
  fr_dhcpv4_ctx_t *packet_ctx;
129
130
1.01k
  fr_pair_list_init(&tmp);
131
132
1.01k
  fr_assert(data_len >= MIN_PACKET_SIZE); /* fr_dhcpv4_ok() MUST be called first */
133
134
1.01k
  if (data[1] > 1) {
135
0
    fr_strerror_printf("Packet is not Ethernet: %u",
136
0
          data[1]);
137
0
    return -1;
138
0
  }
139
140
1.01k
  packet_ctx = talloc_zero(ctx, fr_dhcpv4_ctx_t);
141
1.01k
  if (!packet_ctx) return -1;
142
1.01k
  packet_ctx->tmp_ctx = talloc(packet_ctx, uint8_t);
143
1.01k
  packet_ctx->root = fr_dict_root(dict_dhcpv4);
144
145
  /*
146
   *  Decode the header.
147
   */
148
15.2k
  for (i = 0; i < dhcp_header_attrs_len; i++) {
149
14.2k
    fr_dict_attr_t const *da = *dhcp_header_attrs[i];
150
151
14.2k
    vp = fr_pair_afrom_da(ctx, da);
152
14.2k
    if (!vp) {
153
0
      fr_strerror_const_push("Cannot decode packet due to internal error");
154
0
    error_vp:
155
0
      talloc_free(vp);
156
8
    error:
157
8
      fr_pair_list_free(&tmp);
158
8
      talloc_free(packet_ctx);
159
8
      return -1;
160
0
    }
161
162
14.2k
    switch (vp->vp_type) {
163
2.03k
    case FR_TYPE_STRING:
164
      /*
165
       *  According to RFC 2131, these are null terminated strings.
166
       *  We don't trust everyone to abide by the RFC, though.
167
       */
168
2.03k
      if (*p != '\0') {
169
1.48k
        uint8_t const *q;
170
171
1.48k
        q = memchr(p, '\0', dhcp_header_sizes[i]);
172
1.48k
        fr_pair_value_bstrndup(vp, (char const *)p, q ? q - p : dhcp_header_sizes[i], true);
173
1.48k
      } else {
174
553
        TALLOC_FREE(vp);
175
553
      }
176
2.03k
      break;
177
178
      /*
179
       *  The DHCP header size for CHADDR is not
180
       *  6, so the value_box function doesn't
181
       *  like it.  Just do the copy manually.
182
       */
183
1.01k
    case FR_TYPE_ETHERNET:
184
1.01k
      if ((data[1] != 1) || (data[2] != 6)) {
185
976
        TALLOC_FREE(vp);
186
976
        break;
187
976
      }
188
189
43
      memcpy(vp->vp_ether, p, sizeof(vp->vp_ether));
190
43
      break;
191
192
11.2k
    default:
193
11.2k
      if (fr_value_box_from_network(vp, &vp->data, vp->vp_type, vp->da,
194
11.2k
                  &FR_DBUFF_TMP(p, (size_t)dhcp_header_sizes[i]),
195
11.2k
                  dhcp_header_sizes[i], true) < 0) goto error_vp;
196
11.2k
      break;
197
14.2k
    }
198
14.2k
    p += dhcp_header_sizes[i];
199
200
14.2k
    if (!vp) continue;
201
202
12.7k
    fr_pair_append(&tmp, vp);
203
12.7k
  }
204
205
  /*
206
   *  Nothing uses tail after this call, if it does in the future
207
   *  it'll need to find the new tail...
208
   */
209
1.01k
  {
210
1.01k
    uint8_t const   *end;
211
1.01k
    ssize_t     len;
212
213
1.01k
    p = data + 240;
214
1.01k
    end = p + (data_len - 240);
215
216
    /*
217
     *  Loop over all the options data
218
     */
219
16.6k
    while (p < end) {
220
15.7k
      len = fr_dhcpv4_decode_option(ctx, &tmp, p, (end - p), packet_ctx);
221
15.7k
      if (len <= 0) {
222
414
      fail:
223
414
        fr_pair_list_free(&tmp);
224
414
        talloc_free(packet_ctx);
225
414
        return -1;
226
46
      }
227
15.6k
      p += len;
228
15.6k
    }
229
230
973
    if (code) {
231
973
      vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_message_type);
232
973
      if (vp) {
233
353
        *code = vp->vp_uint8;
234
353
      }
235
973
    }
236
237
    /*
238
     *  If option Overload is present in the 'options' field, then fields 'file' and/or 'sname'
239
     *  are used to hold more options. They are partitioned and must be interpreted in sequence.
240
     */
241
973
    vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_overload);
242
973
    if (vp) {
243
806
      if ((vp->vp_uint8 & 1) == 1) {
244
        /*
245
         *  The 'file' field is used to hold options.
246
         *  It must be interpreted before 'sname'.
247
         */
248
803
        p = data + offsetof(dhcp_packet_t, file);
249
803
        end = p + DHCP_FILE_LEN;
250
20.0k
        while (p < end) {
251
19.3k
          len = fr_dhcpv4_decode_option(ctx, &tmp,
252
19.3k
                      p, end - p, packet_ctx);
253
19.3k
          if (len <= 0) goto fail;
254
19.2k
          p += len;
255
19.2k
        }
256
699
        fr_pair_delete_by_da(&tmp, attr_dhcp_boot_filename);
257
699
      }
258
702
      if ((vp->vp_uint8 & 2) == 2) {
259
        /*
260
         *  The 'sname' field is used to hold options.
261
         */
262
691
        p = data + offsetof(dhcp_packet_t, sname);
263
691
        end = p + DHCP_SNAME_LEN;
264
8.72k
        while (p < end) {
265
8.29k
          len = fr_dhcpv4_decode_option(ctx, &tmp,
266
8.29k
                      p, end - p, packet_ctx);
267
8.29k
          if (len <= 0) goto fail;
268
8.03k
          p += len;
269
8.03k
        }
270
427
        fr_pair_delete_by_da(&tmp, attr_dhcp_server_host_name);
271
427
      }
272
702
    }
273
973
  }
274
275
  /*
276
   *  If DHCP request, set ciaddr to zero.
277
   */
278
279
  /*
280
   *  Set broadcast flag for broken vendors, but only if
281
   *  giaddr isn't set.
282
   */
283
605
  memcpy(&giaddr, data + 24, sizeof(giaddr));
284
605
  if (giaddr == htonl(INADDR_ANY)) {
285
    /*
286
     *  DHCP Opcode is request
287
     */
288
113
    vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_opcode);
289
113
    if (vp && vp->vp_uint8 == 1) {
290
      /*
291
       *  Vendor is "MSFT 98"
292
       */
293
73
      vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_vendor_class_identifier);
294
73
      if (vp && (vp->vp_length == 7) && (memcmp(vp->vp_strvalue, "MSFT 98", 7) == 0)) {
295
2
        vp = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_flags);
296
297
        /*
298
         *  Reply should be broadcast.
299
         */
300
2
        if (vp) vp->vp_uint16 |= 0x8000;
301
2
      }
302
73
    }
303
113
  }
304
305
  /*
306
   *  Determine the address to use in looking up which subnet the
307
   *  client belongs to based on packet data.  The sequence here
308
   *  is based on ISC DHCP behaviour and RFCs 3527 and 3011.  We
309
   *  store the found address in an internal attribute of
310
   *  Network-Subnet
311
   *
312
   *
313
   *  All of these options / fields are type "ipv4addr", so
314
   *  we need to decode them as that.  And then cast it to
315
   *  "ipv4prefix".
316
   */
317
605
  vp = fr_pair_afrom_da(ctx, attr_dhcp_network_subnet);
318
605
  if (!vp) goto error;
319
320
  /*
321
   *  First look for Relay-Link-Selection
322
   */
323
605
  netaddr = fr_pair_find_by_da_nested(&tmp, NULL, attr_dhcp_relay_link_selection);
324
605
  if (!netaddr) {
325
    /*
326
     *  Next try Subnet-Selection-Option
327
     */
328
604
    netaddr = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_subnet_selection_option);
329
604
  }
330
331
605
  if (netaddr) {
332
    /*
333
     *  Store whichever address we found from options and ensure
334
     *  the data type matches the pair, i.e address to prefix
335
     *  conversion.
336
     */
337
3
    if (fr_value_box_cast(vp, &vp->data, vp->vp_type, vp->da, &netaddr->data) < 0) goto error_vp;
338
339
602
  } else if (giaddr != htonl(INADDR_ANY)) {
340
    /*
341
     *  Gateway address is set - use that one
342
     */
343
491
    if (fr_value_box_from_network(vp, &box, FR_TYPE_IPV4_ADDR, NULL,
344
491
            &FR_DBUFF_TMP(data + 24, 4), 4, true) < 0) goto error_vp;
345
491
    if (fr_value_box_cast(vp, &vp->data, vp->vp_type, vp->da, &box) < 0) goto error_vp;
346
347
491
  } else {
348
    /*
349
     *  else, store client address whatever it is
350
     */
351
111
    if (fr_value_box_from_network(vp, &box, FR_TYPE_IPV4_ADDR, NULL,
352
111
            &FR_DBUFF_TMP(data + 12, 4), 4, true) < 0) goto error_vp;
353
111
    if (fr_value_box_cast(vp, &vp->data, vp->vp_type, vp->da, &box) < 0) goto error_vp;
354
111
  }
355
356
605
  fr_pair_append(&tmp, vp);
357
358
  /*
359
   *  Client can request a LARGER size, but not a smaller
360
   *  one.  They also cannot request a size larger than MTU.
361
   */
362
605
  maxms = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_dhcp_maximum_msg_size);
363
605
  mtu = fr_pair_find_by_da(&tmp, NULL, attr_dhcp_interface_mtu_size);
364
365
605
  if (mtu && (mtu->vp_uint16 < DEFAULT_PACKET_SIZE)) {
366
8
    fr_strerror_const("Client says MTU is smaller than minimum permitted by the specification");
367
8
    goto error;
368
8
  }
369
370
  /*
371
   *  Client says maximum message size is smaller than minimum permitted
372
   *  by the specification: fixing it.
373
   */
374
597
  if (maxms && (maxms->vp_uint16 < DEFAULT_PACKET_SIZE)) maxms->vp_uint16 = DEFAULT_PACKET_SIZE;
375
376
  /*
377
   *  Client says MTU is smaller than maximum message size: fixing it
378
   */
379
597
  if (maxms && mtu && (maxms->vp_uint16 > mtu->vp_uint16)) maxms->vp_uint16 = mtu->vp_uint16;
380
381
  /*
382
   *  FIXME: Nuke attributes that aren't used in the normal
383
   *  header for discover/requests.
384
   */
385
597
  fr_pair_list_append(out, &tmp);
386
387
597
  return 0;
388
605
}
389
390
int fr_dhcpv4_packet_encode(fr_packet_t *packet, fr_pair_list_t *list)
391
0
{
392
0
  ssize_t   len;
393
0
  fr_pair_t *vp;
394
395
0
  if (packet->data) return 0;
396
397
0
  packet->data_len = MAX_PACKET_SIZE;
398
0
  packet->data = talloc_zero_array(packet, uint8_t, packet->data_len);
399
400
  /* XXX Ugly ... should be set by the caller */
401
0
  if (packet->code == 0) packet->code = FR_DHCP_NAK;
402
403
  /* store xid */
404
0
  if ((vp = fr_pair_find_by_da(list, NULL, attr_dhcp_transaction_id))) {
405
0
    packet->id = vp->vp_uint32;
406
0
  } else {
407
0
    packet->id = fr_rand();
408
0
  }
409
410
0
  len = fr_dhcpv4_encode(packet->data, packet->data_len, NULL, packet->code, packet->id, list);
411
0
  if (len < 0) return -1;
412
413
0
  packet->data_len = len;
414
415
0
  return 0;
416
0
}
417
418
fr_packet_t *fr_dhcpv4_packet_alloc(uint8_t const *data, size_t data_len)
419
0
{
420
0
  fr_packet_t *packet;
421
0
  uint32_t  magic;
422
0
  uint8_t const *code;
423
424
0
  fr_assert(data_len >= MIN_PACKET_SIZE); /* fr_dhcpv4_ok() MUST be called first */
425
426
0
  code = fr_dhcpv4_packet_get_option((dhcp_packet_t const *) data, data_len, attr_dhcp_message_type);
427
0
  if (!code || (code[1] != 1)) return NULL;
428
429
  /* Now that checks are done, allocate packet */
430
0
  packet = fr_packet_alloc(NULL, false);
431
0
  if (!packet) {
432
0
    fr_strerror_const("Failed allocating packet");
433
0
    return NULL;
434
0
  }
435
436
  /*
437
   *  Get XID.
438
   */
439
0
  memcpy(&magic, data + 4, 4);
440
441
0
  packet->data_len = data_len;
442
0
  packet->code = code[2];
443
0
  packet->id = ntohl(magic);
444
445
  /*
446
   *  FIXME: for DISCOVER / REQUEST: src_port == dst_port + 1
447
   *  FIXME: for OFFER / ACK       : src_port = dst_port - 1
448
   */
449
450
  /*
451
   *  Unique keys are xid, client mac, and client ID?
452
   */
453
0
  return packet;
454
0
}