Coverage Report

Created: 2026-08-31 06:56

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openssl/ssl/dtls_conn_lookup.c
Line
Count
Source
1
/*
2
 * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License 2.0 (the "License").  You may not use
5
 * this file except in compliance with the License.  You can obtain a copy
6
 * in the file LICENSE in the source distribution or at
7
 * https://www.openssl.org/source/license.html
8
 */
9
10
#include <openssl/crypto.h>
11
#include <openssl/bio.h>
12
#include <openssl/ssl.h>
13
#include <openssl/rand.h>
14
#include "internal/common.h"
15
#include "internal/dgram_conn_lookup.h"
16
#include "crypto/siphash.h"
17
18
#ifndef OPENSSL_NO_DTLS
19
20
/*
21
 * Internal entry structure for address-based connection lookup.
22
 * Stores a connection keyed by peer address.
23
 */
24
typedef struct dgram_conn_entry_st {
25
    BIO_ADDR peer; /* Peer address (key) */
26
    SSL *ssl; /* The SSL connection object */
27
    uint8_t *hashkey; /* Pre-built hash key (family + port + addr) */
28
    size_t hashkey_len; /* Length of the hash key */
29
    uint64_t *siphash_key; /* Pointer to shared SipHash key (hash-flooding defense) */
30
} DGRAM_CONN_ENTRY;
31
32
DEFINE_LHASH_OF_EX(DGRAM_CONN_ENTRY);
33
34
/*
35
 * Wrapper structure for address-based lookup implementation data.
36
 * Contains the hash table and SipHash key for hash-flooding defense.
37
 */
38
typedef struct addr_lookup_data_st {
39
    LHASH_OF(DGRAM_CONN_ENTRY) *htable;
40
    uint64_t hash_key[2]; /* 128-bit SipHash key */
41
} ADDR_LOOKUP_DATA;
42
43
/*
44
 * Build a hash key from a BIO_ADDR.
45
 * The key format is: family (int) + port (uint16_t) + raw address bytes.
46
 *
47
 * Returns allocated hashkey buffer on success, NULL on failure.
48
 */
49
static uint8_t *build_hashkey(const BIO_ADDR *peer, size_t *out_len)
50
0
{
51
0
    size_t hashkey_len = 0;
52
0
    size_t addr_len = 0;
53
0
    int family;
54
0
    uint16_t port;
55
0
    uint8_t *hashkey;
56
0
    int *famptr;
57
0
    uint16_t *portptr;
58
0
    uint8_t *addrptr;
59
60
0
    family = BIO_ADDR_family(peer);
61
62
    /* For AF_UNSPEC (no peer address), use a minimal key */
63
0
    if (family == AF_UNSPEC) {
64
0
        hashkey_len = sizeof(int);
65
0
        hashkey = OPENSSL_zalloc(hashkey_len);
66
0
        if (hashkey == NULL)
67
0
            return NULL;
68
0
        famptr = (int *)hashkey;
69
0
        *famptr = family;
70
0
        *out_len = hashkey_len;
71
0
        return hashkey;
72
0
    }
73
74
0
    if (!BIO_ADDR_rawaddress(peer, NULL, &addr_len))
75
0
        return NULL;
76
77
0
    port = BIO_ADDR_rawport(peer);
78
79
0
    hashkey_len += sizeof(int); /* family */
80
0
    hashkey_len += sizeof(uint16_t); /* port */
81
0
    hashkey_len += addr_len; /* address */
82
83
0
    hashkey = OPENSSL_zalloc(hashkey_len);
84
0
    if (hashkey == NULL)
85
0
        return NULL;
86
87
0
    famptr = (int *)hashkey;
88
0
    portptr = (uint16_t *)(famptr + 1);
89
0
    addrptr = (uint8_t *)(portptr + 1);
90
91
0
    *famptr = family;
92
0
    *portptr = port;
93
0
    if (!BIO_ADDR_rawaddress(peer, addrptr, NULL)) {
94
0
        OPENSSL_free(hashkey);
95
0
        return NULL;
96
0
    }
97
98
0
    *out_len = hashkey_len;
99
0
    return hashkey;
100
0
}
101
102
/*
103
 * Hash function for DGRAM_CONN_ENTRY.
104
 * Uses SipHash with a per-instance random key to defend against
105
 * hash-flooding CPU DoS attacks
106
 */
107
static unsigned long conn_entry_hash(const DGRAM_CONN_ENTRY *e)
108
0
{
109
0
    SIPHASH siphash = { 0 };
110
0
    unsigned long hashval = 0;
111
112
0
    if (e->hashkey == NULL || e->hashkey_len == 0 || e->siphash_key == NULL)
113
0
        return 0;
114
115
0
    if (!SipHash_set_hash_size(&siphash, sizeof(unsigned long)))
116
0
        return 0;
117
0
    if (!SipHash_Init(&siphash, (const unsigned char *)e->siphash_key, 0, 0))
118
0
        return 0;
119
0
    SipHash_Update(&siphash, e->hashkey, e->hashkey_len);
120
0
    if (!SipHash_Final(&siphash, (unsigned char *)&hashval, sizeof(unsigned long)))
121
0
        return 0;
122
123
0
    return hashval;
124
0
}
125
126
/*
127
 * Compare function for DGRAM_CONN_ENTRY - compares pre-built hashkeys.
128
 * Returns 0 if equal, non-zero otherwise.
129
 */
130
static int conn_entry_cmp(const DGRAM_CONN_ENTRY *a, const DGRAM_CONN_ENTRY *b)
131
0
{
132
0
    if (a->hashkey_len != b->hashkey_len)
133
0
        return 1;
134
0
    if (a->hashkey == NULL || b->hashkey == NULL)
135
0
        return (a->hashkey == b->hashkey) ? 0 : 1;
136
0
    return memcmp(a->hashkey, b->hashkey, a->hashkey_len);
137
0
}
138
139
/*
140
 * Free a connection entry (but not the connection itself).
141
 */
142
static void conn_entry_free(DGRAM_CONN_ENTRY *e)
143
0
{
144
0
    if (e == NULL)
145
0
        return;
146
0
    OPENSSL_free(e->hashkey);
147
0
    OPENSSL_free(e);
148
0
}
149
150
/*
151
 * Callback for lh_DGRAM_CONN_ENTRY_doall to free all entries.
152
 */
153
static void conn_entry_free_cb(DGRAM_CONN_ENTRY *e)
154
0
{
155
0
    conn_entry_free(e);
156
0
}
157
158
/*
159
 * Lookup a connection by peer address from URXE.
160
 */
161
static SSL *addr_lookup(DGRAM_CONN_LOOKUP *lookup, const DGRAM_URXE *e)
162
0
{
163
0
    ADDR_LOOKUP_DATA *data;
164
0
    DGRAM_CONN_ENTRY key;
165
0
    DGRAM_CONN_ENTRY *result;
166
167
0
    if (lookup == NULL || lookup->impl_data == NULL || e == NULL)
168
0
        return NULL;
169
170
0
    data = (ADDR_LOOKUP_DATA *)lookup->impl_data;
171
172
0
    memset(&key, 0, sizeof(key));
173
0
    BIO_ADDR_copy(&key.peer, &e->peer);
174
175
    /* Build hashkey for the lookup key */
176
0
    key.hashkey = build_hashkey(&e->peer, &key.hashkey_len);
177
0
    if (key.hashkey == NULL)
178
0
        return NULL;
179
180
    /* Set SipHash key pointer for hash function */
181
0
    key.siphash_key = data->hash_key;
182
183
0
    result = lh_DGRAM_CONN_ENTRY_retrieve(data->htable, &key);
184
185
0
    OPENSSL_free(key.hashkey);
186
187
0
    if (result == NULL)
188
0
        return NULL;
189
190
0
    return result->ssl;
191
0
}
192
193
/*
194
 * Register a connection with peer address from URXE.
195
 */
196
static int addr_register_conn(DGRAM_CONN_LOOKUP *lookup, const DGRAM_URXE *e,
197
    SSL *ssl)
198
0
{
199
0
    ADDR_LOOKUP_DATA *data;
200
0
    DGRAM_CONN_ENTRY *entry, *old;
201
202
0
    if (lookup == NULL || lookup->impl_data == NULL || e == NULL || ssl == NULL)
203
0
        return 0;
204
205
0
    data = (ADDR_LOOKUP_DATA *)lookup->impl_data;
206
207
0
    entry = OPENSSL_zalloc(sizeof(*entry));
208
0
    if (entry == NULL)
209
0
        return 0;
210
211
0
    BIO_ADDR_copy(&entry->peer, &e->peer);
212
0
    entry->ssl = ssl;
213
0
    entry->hashkey = build_hashkey(&e->peer, &entry->hashkey_len);
214
0
    if (entry->hashkey == NULL) {
215
0
        OPENSSL_free(entry);
216
0
        return 0;
217
0
    }
218
219
    /* Set SipHash key pointer for hash function */
220
0
    entry->siphash_key = data->hash_key;
221
222
0
    old = lh_DGRAM_CONN_ENTRY_insert(data->htable, entry);
223
224
    /* Check if insert failed due to allocation error */
225
0
    if (lh_DGRAM_CONN_ENTRY_error(data->htable)) {
226
0
        conn_entry_free(entry);
227
        /* Don't free old since it is still in the hash table since insert failed */
228
0
        return 0;
229
0
    }
230
231
    /*
232
     * A non-NULL return from lh_DGRAM_CONN_ENTRY_insert means an entry with the
233
     * same key (peer address) was already present and has just been replaced.
234
     * This must not happen: callers always look up an address and only register
235
     * when no entry exists, all while holding the listener mutex, so the same peer
236
     * is never registered twice. We therefore expect old == NULL here.
237
     *
238
     * We deliberately do NOT free old->ssl: this lookup table is an
239
     * ownership-agnostic index and does not own the SSL objects it stores.
240
     * The owning layer (the DTLS listener) is responsible for the lifecycle of
241
     * those SSL objects. Freeing one here would be a use-after-free for the
242
     * non-owning established_conns table.
243
     */
244
0
    if (!ossl_assert(old == NULL))
245
0
        conn_entry_free(old);
246
247
0
    return 1;
248
0
}
249
250
/*
251
 * Register a connection by peer address from BIO_ADDR directly.
252
 * This is used when we don't have a URXE available.
253
 */
254
static int addr_register_conn_addr(DGRAM_CONN_LOOKUP *lookup, const BIO_ADDR *peer,
255
    SSL *ssl)
256
0
{
257
0
    ADDR_LOOKUP_DATA *data;
258
0
    DGRAM_CONN_ENTRY *entry, *old;
259
260
0
    if (lookup == NULL || lookup->impl_data == NULL || peer == NULL || ssl == NULL)
261
0
        return 0;
262
263
0
    data = (ADDR_LOOKUP_DATA *)lookup->impl_data;
264
265
0
    entry = OPENSSL_zalloc(sizeof(*entry));
266
0
    if (entry == NULL)
267
0
        return 0;
268
269
0
    BIO_ADDR_copy(&entry->peer, peer);
270
0
    entry->ssl = ssl;
271
0
    entry->hashkey = build_hashkey(peer, &entry->hashkey_len);
272
0
    if (entry->hashkey == NULL) {
273
0
        OPENSSL_free(entry);
274
0
        return 0;
275
0
    }
276
277
    /* Set SipHash key pointer for hash function */
278
0
    entry->siphash_key = data->hash_key;
279
280
0
    old = lh_DGRAM_CONN_ENTRY_insert(data->htable, entry);
281
282
    /* Check if insert failed due to allocation error */
283
0
    if (lh_DGRAM_CONN_ENTRY_error(data->htable)) {
284
0
        conn_entry_free(entry);
285
        /* Don't free old since it is still in the hash table since insert failed */
286
0
        return 0;
287
0
    }
288
289
    /*
290
     * A non-NULL return from lh_DGRAM_CONN_ENTRY_insert means an entry with the
291
     * same key(peer address) was already present and has just been replaced. This
292
     * must not happen: callers always look up an address and only register when no
293
     * entry exists, all while holding the listener mutex, so the same peer is
294
     * never registered twice. We therefore expect old == NULL here.
295
     *
296
     * We deliberately do NOT free old->ssl: this lookup table is an
297
     * ownership-agnostic index and does not own the SSL objects it stores.
298
     * The owning layer (the DTLS listener) is responsible for the lifecycle of
299
     * those SSL objects. Freeing one here would be a use-after-free for the
300
     * non-owning established_conns table.
301
     */
302
0
    if (!ossl_assert(old == NULL))
303
0
        conn_entry_free(old);
304
305
0
    return 1;
306
0
}
307
308
/*
309
 * Unregister a connection by peer address.
310
 */
311
static int addr_unregister_conn(DGRAM_CONN_LOOKUP *lookup, const BIO_ADDR *peer)
312
0
{
313
0
    ADDR_LOOKUP_DATA *data;
314
0
    DGRAM_CONN_ENTRY lookup_key;
315
0
    DGRAM_CONN_ENTRY *removed;
316
317
0
    if (lookup == NULL || lookup->impl_data == NULL || peer == NULL)
318
0
        return 0;
319
320
0
    data = (ADDR_LOOKUP_DATA *)lookup->impl_data;
321
322
0
    memset(&lookup_key, 0, sizeof(lookup_key));
323
0
    BIO_ADDR_copy(&lookup_key.peer, peer);
324
325
0
    lookup_key.hashkey = build_hashkey(peer, &lookup_key.hashkey_len);
326
0
    if (lookup_key.hashkey == NULL)
327
0
        return 0;
328
329
    /* Set SipHash key pointer for hash function */
330
0
    lookup_key.siphash_key = data->hash_key;
331
332
0
    removed = lh_DGRAM_CONN_ENTRY_delete(data->htable, &lookup_key);
333
334
0
    OPENSSL_free(lookup_key.hashkey);
335
336
0
    if (removed != NULL) {
337
0
        conn_entry_free(removed);
338
0
        return 1;
339
0
    }
340
341
0
    return 0;
342
0
}
343
344
/*
345
 * Free the lookup structure and all entries.
346
 */
347
static void addr_free(DGRAM_CONN_LOOKUP *lookup)
348
0
{
349
0
    ADDR_LOOKUP_DATA *data;
350
351
0
    if (lookup == NULL)
352
0
        return;
353
354
0
    if (lookup->impl_data != NULL) {
355
0
        data = (ADDR_LOOKUP_DATA *)lookup->impl_data;
356
0
        if (data->htable != NULL) {
357
0
            lh_DGRAM_CONN_ENTRY_doall(data->htable, conn_entry_free_cb);
358
0
            lh_DGRAM_CONN_ENTRY_free(data->htable);
359
0
        }
360
0
        OPENSSL_free(data);
361
0
    }
362
363
0
    OPENSSL_free(lookup);
364
0
}
365
366
/*
367
 * Context structure for iteration callback.
368
 */
369
typedef struct {
370
    ossl_dgram_conn_lookup_iter_fn user_cb;
371
    void *user_arg;
372
} ADDR_FOREACH_CTX;
373
374
/*
375
 * Internal callback for lh_doall_arg that invokes the user's callback.
376
 */
377
static void addr_foreach_cb(DGRAM_CONN_ENTRY *e, void *arg)
378
0
{
379
0
    ADDR_FOREACH_CTX *ctx = arg;
380
381
0
    if (ctx->user_cb != NULL)
382
0
        ctx->user_cb(e->ssl, &e->peer, ctx->user_arg);
383
0
}
384
385
IMPLEMENT_LHASH_DOALL_ARG(DGRAM_CONN_ENTRY, void);
386
387
/*
388
 * Iterate over all connections, calling the callback for each.
389
 */
390
static void addr_foreach(DGRAM_CONN_LOOKUP *lookup,
391
    ossl_dgram_conn_lookup_iter_fn cb, void *arg)
392
0
{
393
0
    ADDR_LOOKUP_DATA *data;
394
0
    ADDR_FOREACH_CTX ctx;
395
396
0
    if (lookup == NULL || lookup->impl_data == NULL || cb == NULL)
397
0
        return;
398
399
0
    data = (ADDR_LOOKUP_DATA *)lookup->impl_data;
400
0
    ctx.user_cb = cb;
401
0
    ctx.user_arg = arg;
402
403
0
    lh_DGRAM_CONN_ENTRY_doall_void(data->htable, addr_foreach_cb, &ctx);
404
0
}
405
406
static size_t addr_num_items(const DGRAM_CONN_LOOKUP *lookup)
407
0
{
408
0
    ADDR_LOOKUP_DATA *data;
409
410
0
    if (lookup == NULL || lookup->impl_data == NULL)
411
0
        return 0;
412
413
0
    data = (ADDR_LOOKUP_DATA *)lookup->impl_data;
414
0
    return lh_DGRAM_CONN_ENTRY_num_items(data->htable);
415
0
}
416
417
static const DGRAM_CONN_LOOKUP_METHODS addr_methods = {
418
    addr_lookup,
419
    addr_register_conn,
420
    addr_register_conn_addr,
421
    addr_unregister_conn,
422
    addr_foreach,
423
    addr_free,
424
    addr_num_items
425
};
426
427
/*
428
 * Create a new address-based connection lookup for DTLS.
429
 */
430
DGRAM_CONN_LOOKUP *ossl_dgram_conn_lookup_new_addr(void)
431
0
{
432
0
    DGRAM_CONN_LOOKUP *lookup;
433
0
    ADDR_LOOKUP_DATA *data;
434
435
0
    lookup = OPENSSL_zalloc(sizeof(*lookup));
436
0
    if (lookup == NULL)
437
0
        return NULL;
438
439
0
    data = OPENSSL_zalloc(sizeof(*data));
440
0
    if (data == NULL) {
441
0
        OPENSSL_free(lookup);
442
0
        return NULL;
443
0
    }
444
445
    /* Generate random SipHash key for hash-flooding defense */
446
0
    if (RAND_priv_bytes((unsigned char *)data->hash_key,
447
0
            sizeof(data->hash_key))
448
0
        <= 0) {
449
0
        OPENSSL_free(data);
450
0
        OPENSSL_free(lookup);
451
0
        return NULL;
452
0
    }
453
454
0
    data->htable = lh_DGRAM_CONN_ENTRY_new(conn_entry_hash, conn_entry_cmp);
455
0
    if (data->htable == NULL) {
456
0
        OPENSSL_free(data);
457
0
        OPENSSL_free(lookup);
458
0
        return NULL;
459
0
    }
460
461
0
    lookup->methods = &addr_methods;
462
0
    lookup->impl_data = data;
463
464
0
    return lookup;
465
0
}
466
467
/*
468
 * Public API wrappers - call through methods table.
469
 */
470
SSL *ossl_dgram_conn_lookup_find(DGRAM_CONN_LOOKUP *lookup, const DGRAM_URXE *e)
471
0
{
472
0
    if (lookup == NULL || lookup->methods == NULL
473
0
        || lookup->methods->lookup == NULL)
474
0
        return NULL;
475
0
    return lookup->methods->lookup(lookup, e);
476
0
}
477
478
int ossl_dgram_conn_lookup_register(DGRAM_CONN_LOOKUP *lookup,
479
    const DGRAM_URXE *e, SSL *ssl)
480
0
{
481
0
    if (lookup == NULL || lookup->methods == NULL
482
0
        || lookup->methods->register_conn == NULL)
483
0
        return 0;
484
0
    return lookup->methods->register_conn(lookup, e, ssl);
485
0
}
486
487
int ossl_dgram_conn_lookup_register_addr(DGRAM_CONN_LOOKUP *lookup,
488
    const BIO_ADDR *peer, SSL *ssl)
489
0
{
490
0
    if (lookup == NULL || lookup->methods == NULL
491
0
        || lookup->methods->register_conn_addr == NULL)
492
0
        return 0;
493
0
    return lookup->methods->register_conn_addr(lookup, peer, ssl);
494
0
}
495
496
int ossl_dgram_conn_lookup_unregister(DGRAM_CONN_LOOKUP *lookup,
497
    const BIO_ADDR *peer)
498
0
{
499
0
    if (lookup == NULL || lookup->methods == NULL
500
0
        || lookup->methods->unregister_conn == NULL)
501
0
        return 0;
502
0
    return lookup->methods->unregister_conn(lookup, peer);
503
0
}
504
505
void ossl_dgram_conn_lookup_foreach(DGRAM_CONN_LOOKUP *lookup,
506
    ossl_dgram_conn_lookup_iter_fn cb, void *arg)
507
0
{
508
0
    if (lookup == NULL || lookup->methods == NULL
509
0
        || lookup->methods->foreach == NULL)
510
0
        return;
511
0
    lookup->methods->foreach(lookup, cb, arg);
512
0
}
513
514
void ossl_dgram_conn_lookup_free(DGRAM_CONN_LOOKUP *lookup)
515
0
{
516
0
    if (lookup == NULL || lookup->methods == NULL
517
0
        || lookup->methods->free == NULL)
518
0
        return;
519
0
    lookup->methods->free(lookup);
520
0
}
521
522
/*
523
 * Return the number of entries currently registered.
524
 *
525
 * The lookup is not internally synchronised: like the other lookup
526
 * operations, the caller must hold the lock that serialises access to it.
527
 * Reading the count without that lock held races with concurrent
528
 * register/unregister.
529
 */
530
size_t ossl_dgram_conn_lookup_num_items(const DGRAM_CONN_LOOKUP *lookup)
531
0
{
532
0
    if (lookup == NULL || lookup->methods == NULL
533
0
        || lookup->methods->num_items == NULL)
534
0
        return 0;
535
0
    return lookup->methods->num_items(lookup);
536
0
}
537
538
#endif /* OPENSSL_NO_DTLS */