ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
auth_lockout.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file auth_lockout.cpp
6 * @brief Per-peer brute-force lockout state machine (PC_ENABLE_AUTH_LOCKOUT).
7 *
8 * A bounded BSS table of buckets keyed by the source address (a pc_ip, so IPv4 and IPv6 peers are
9 * each their own bucket - never a lossy hash that a colliding address could poison). Each bucket
10 * holds the consecutive-failure count and, once the threshold is crossed, the start and duration
11 * of the current lockout (exponential backoff, capped). Compiled only when PC_ENABLE_AUTH_LOCKOUT
12 * is set; the host unit tests enable it.
13 */
14
15#include "auth_lockout.h"
16
17#if PC_ENABLE_AUTH_LOCKOUT
18
19namespace
20{
21
22struct LockoutBucket
23{
24 pc_ip addr; ///< source address (family pc_ip_family::PC_IP_NONE marks an empty bucket).
25 uint32_t lock_start_ms; ///< millis() when the current lockout began.
26 uint32_t lock_ms; ///< current lockout duration (0 = not locked).
27 uint32_t last_ms; ///< millis() of the last recorded failure (LRU eviction).
28 uint16_t fails; ///< consecutive failures from this address.
29};
30
31// All lockout state, owned by one instance (internal linkage): the per-peer bucket table,
32// so it is one named owner, unreachable from any other translation unit.
33struct LockoutCtx
34{
35 LockoutBucket buckets[PC_AUTH_LOCKOUT_SLOTS];
36};
37LockoutCtx s_lock;
38
39// Returns a mutable bucket (callers mutate it), so it takes the owner by non-const reference.
40LockoutBucket *find_bucket(LockoutCtx &c, const pc_ip *ip)
41{
42 for (int i = 0; i < PC_AUTH_LOCKOUT_SLOTS; i++)
43 {
44 if (c.buckets[i].addr.family != pc_ip_family::PC_IP_NONE && pc_ip_equal(&c.buckets[i].addr, ip))
45 {
46 return &c.buckets[i];
47 }
48 }
49 return nullptr;
50}
51
52bool bucket_locked(const LockoutBucket *b, uint32_t now_ms)
53{
54 // Unsigned subtraction wraps correctly across the millis() rollover.
55 return b->lock_ms != 0 && (uint32_t)(now_ms - b->lock_start_ms) < b->lock_ms;
56}
57
58} // namespace
59
60uint32_t auth_lockout_remaining_ms(const pc_ip *ip, uint32_t now_ms)
61{
63 {
64 return 0; // untrackable source -> never reported as locked
65 }
66 LockoutBucket *b = find_bucket(s_lock, ip);
67 if (!b || b->lock_ms == 0)
68 {
69 return 0;
70 }
71 uint32_t elapsed = now_ms - b->lock_start_ms; // wraps correctly across rollover
72 if (elapsed >= b->lock_ms)
73 {
74 return 0; // the lockout window has passed
75 }
76 return b->lock_ms - elapsed;
77}
78
79void auth_lockout_fail(const pc_ip *ip, uint32_t now_ms)
80{
82 {
83 return; // untrackable source
84 }
85
86 LockoutBucket *b = find_bucket(s_lock, ip);
87 if (!b)
88 {
89 // Claim a bucket: an empty one first; else evict the least-recently-used
90 // address that is NOT currently locked; only if every bucket is locked do
91 // we evict the overall LRU (so an active attacker cannot evict their own
92 // lockout by flooding from other addresses).
93 int slot = -1;
94 int lru = 0;
95 for (int i = 0; i < PC_AUTH_LOCKOUT_SLOTS; i++)
96 {
97 if (s_lock.buckets[i].addr.family == pc_ip_family::PC_IP_NONE)
98 {
99 slot = i;
100 break;
101 }
102 if ((uint32_t)(now_ms - s_lock.buckets[i].last_ms) > (uint32_t)(now_ms - s_lock.buckets[lru].last_ms))
103 {
104 lru = i;
105 }
106 if (!bucket_locked(&s_lock.buckets[i], now_ms) &&
107 (slot < 0 ||
108 (uint32_t)(now_ms - s_lock.buckets[i].last_ms) > (uint32_t)(now_ms - s_lock.buckets[slot].last_ms)))
109 {
110 slot = i;
111 }
112 }
113 if (slot < 0)
114 {
115 slot = lru; // table full of active lockouts
116 }
117 b = &s_lock.buckets[slot];
118 b->addr = *ip;
119 b->fails = 0;
120 b->lock_ms = 0;
121 b->lock_start_ms = now_ms;
122 }
123
124 b->last_ms = now_ms;
125 if (b->fails < 0xFFFF)
126 {
127 b->fails++;
128 }
129
130 if (b->fails >= PC_AUTH_LOCKOUT_THRESHOLD)
131 {
132 // Exponential backoff: base << (fails - threshold), capped at max. Step the
133 // double so the shift can never overflow: the cap is hit (and the loop
134 // breaks) before dur could exceed MAX_MS, and the config caps MAX_MS at
135 // 0x80000000 so the surviving dur << 1 always fits in a uint32.
136 uint32_t dur = PC_AUTH_LOCKOUT_BASE_MS;
137 for (uint16_t n = (uint16_t)(b->fails - PC_AUTH_LOCKOUT_THRESHOLD); n > 0; n--)
138 {
139 if (dur >= PC_AUTH_LOCKOUT_MAX_MS)
140 {
142 break;
143 }
144 dur <<= 1;
145 }
146 if (dur > PC_AUTH_LOCKOUT_MAX_MS)
147 {
149 }
150 b->lock_ms = dur;
151 b->lock_start_ms = now_ms;
152 }
153}
154
155void auth_lockout_succeed(const pc_ip *ip)
156{
157 if (pc_ip_is_unspecified(ip))
158 {
159 return;
160 }
161 LockoutBucket *b = find_bucket(s_lock, ip);
162 if (b)
163 {
164 b->addr.family = pc_ip_family::PC_IP_NONE;
165 b->fails = 0;
166 b->lock_ms = 0;
167 b->lock_start_ms = 0;
168 b->last_ms = 0;
169 }
170}
171
172void auth_lockout_reset(void)
173{
174 for (int i = 0; i < PC_AUTH_LOCKOUT_SLOTS; i++)
175 {
176 s_lock.buckets[i].addr.family = pc_ip_family::PC_IP_NONE;
177 s_lock.buckets[i].lock_start_ms = 0;
178 s_lock.buckets[i].lock_ms = 0;
179 s_lock.buckets[i].last_ms = 0;
180 s_lock.buckets[i].fails = 0;
181 }
182}
183
184#endif // PC_ENABLE_AUTH_LOCKOUT
Per-peer brute-force lockout for HTTP auth (PC_ENABLE_AUTH_LOCKOUT).
bool pc_ip_equal(const pc_ip *a, const pc_ip *b)
True if a and b are the same family and address.
Definition ip.cpp:585
bool pc_ip_is_unspecified(const pc_ip *ip)
True if ip is empty (pc_ip_family::PC_IP_NONE) or the all-zero unspecified address (0....
Definition ip.cpp:645
@ PC_IP_NONE
empty / unparsed
#define PC_AUTH_LOCKOUT_SLOTS
Number of source IPs the auth lockout tracks (BSS bucket table).
#define PC_AUTH_LOCKOUT_THRESHOLD
Consecutive failed auths from one IP before it is locked out.
#define PC_AUTH_LOCKOUT_MAX_MS
Maximum lockout duration in ms (the exponential backoff cap).
#define PC_AUTH_LOCKOUT_BASE_MS
First lockout duration in ms; doubles on each further failure.
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52