Coverage Report

Created: 2026-10-28 06:28

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