emme coverage


Directory: src/
File: src/ip_limiter.c
Date: 2026-05-14 14:35:13
Exec Total Coverage
Lines: 98 144 68.1%
Functions: 10 10 100.0%
Branches: 42 81 51.9%

Line Branch Exec Source
1 #include "ip_limiter.h"
2 #include "log.h"
3 #include <string.h>
4 #include <time.h>
5 #include <stdatomic.h>
6
7 #define CACHE_LINE_SIZE 64
8 #define KNUTH_MULTIPLICATIVE_CONSTANT 2654435761UL
9 #define COMPACTION_INTERVAL_NS 5000000000ULL /* 5 seconds */
10 #define ENTRY_EXPIRY_NS 60000000000ULL /* 60 seconds */
11
12 3033 static uint32_t hash_ip(uint32_t ip)
13 {
14 3033 ip *= KNUTH_MULTIPLICATIVE_CONSTANT;
15 3033 ip ^= (ip >> 16);
16 3033 ip *= KNUTH_MULTIPLICATIVE_CONSTANT;
17 3033 ip ^= (ip >> 16);
18 3033 ip *= KNUTH_MULTIPLICATIVE_CONSTANT;
19 3033 return ip;
20 }
21
22 3034 static size_t get_shard_index(uint32_t ip)
23 {
24 3034 return hash_ip(ip) & (IP_LIMITER_SHARDS - 1);
25 }
26
27 2145 static uint64_t get_time_ns(void)
28 {
29 struct timespec ts;
30 2145 clock_gettime(CLOCK_MONOTONIC, &ts);
31 2145 return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
32 }
33
34 23 int ip_limiter_init(ip_limiter_t *limiter, uint32_t default_limit)
35 {
36
6/6
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 20 times.
23 if (!limiter || default_limit == 0 || default_limit > IP_LIMITER_MAX_CONNECTIONS_PER_IP) {
37 3 return -1;
38 }
39
40 20 memset(limiter, 0, sizeof(*limiter));
41 20 limiter->default_limit = default_limit;
42
43
2/2
✓ Branch 0 taken 5120 times.
✓ Branch 1 taken 20 times.
5140 for (size_t i = 0; i < IP_LIMITER_SHARDS; i++) {
44
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 5120 times.
5120 if (pthread_mutex_init(&limiter->shards[i].lock, NULL) != 0) {
45 for (size_t j = 0; j < i; j++) {
46 pthread_mutex_destroy(&limiter->shards[j].lock);
47 }
48 return -1;
49 }
50 5120 limiter->shards[i].count = 0;
51 }
52
53 20 limiter->last_compaction = get_time_ns();
54 20 atomic_store(&limiter->total_entries, 0);
55 20 atomic_store(&limiter->rejections_total, 0);
56
57 20 log_message(LOG_LEVEL_INFO, "IP limiter initialized: %d shards, %d max entries, default limit %d",
58 IP_LIMITER_SHARDS, IP_LIMITER_MAX_ENTRIES, default_limit);
59
60 20 return 0;
61 }
62
63 20 void ip_limiter_destroy(ip_limiter_t *limiter)
64 {
65
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (!limiter) {
66 return;
67 }
68
69
2/2
✓ Branch 0 taken 5120 times.
✓ Branch 1 taken 20 times.
5140 for (size_t i = 0; i < IP_LIMITER_SHARDS; i++) {
70 5120 pthread_mutex_destroy(&limiter->shards[i].lock);
71 }
72
73 20 log_message(LOG_LEVEL_INFO, "IP limiter destroyed: final rejections %lu",
74 20 atomic_load(&limiter->rejections_total));
75 }
76
77 3020 ip_limiter_result_t ip_limiter_check_and_increment(ip_limiter_t *limiter, uint32_t ip, uint32_t *current_count)
78 {
79
2/3
✓ Branch 0 taken 3020 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 3023 times.
3020 if (!limiter || !current_count) {
80 return IP_LIMITER_ERROR;
81 }
82
83 3023 size_t shard_idx = get_shard_index(ip);
84 3025 ip_shard_t *shard = &limiter->shards[shard_idx];
85
86 3025 pthread_mutex_lock(&shard->lock);
87
88 3026 ip_entry_t *found_entry = NULL;
89 3026 ip_entry_t *empty_slot = NULL;
90
91
2/2
✓ Branch 0 taken 3914 times.
✓ Branch 1 taken 1019 times.
4933 for (size_t i = 0; i < shard->count; i++) {
92
2/2
✓ Branch 0 taken 2007 times.
✓ Branch 1 taken 1907 times.
3914 if (shard->entries[i].ip == ip) {
93 2007 found_entry = &shard->entries[i];
94 2007 break;
95 }
96
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1907 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1907 if (shard->entries[i].count == 0 && !empty_slot) {
97 empty_slot = &shard->entries[i];
98 }
99 }
100
101
4/6
✓ Branch 0 taken 1019 times.
✓ Branch 1 taken 2007 times.
✓ Branch 2 taken 1019 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1019 times.
✗ Branch 5 not taken.
3026 if (!found_entry && !empty_slot && shard->count < IP_LIMITER_MAX_ENTRIES_PER_SHARD) {
102 1019 empty_slot = &shard->entries[shard->count];
103 1019 shard->count++;
104 }
105
106
2/2
✓ Branch 0 taken 2007 times.
✓ Branch 1 taken 1019 times.
3026 if (found_entry) {
107
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2006 times.
2007 if (found_entry->marked_for_deletion) {
108 1 found_entry->marked_for_deletion = false;
109 1 atomic_store(&found_entry->count, 0);
110 }
111
112 2007 uint32_t limit = limiter->default_limit;
113 2007 uint32_t current = atomic_load(&found_entry->count);
114
115
2/2
✓ Branch 0 taken 902 times.
✓ Branch 1 taken 1105 times.
2007 if (current >= limit) {
116 902 *current_count = current;
117 902 atomic_fetch_add(&limiter->rejections_total, 1);
118 902 pthread_mutex_unlock(&shard->lock);
119 901 return IP_LIMITER_REJECTED;
120 }
121
122 1105 atomic_fetch_add(&found_entry->count, 1);
123 1105 found_entry->last_seen = get_time_ns();
124 1105 *current_count = current + 1;
125 1105 pthread_mutex_unlock(&shard->lock);
126 1105 return IP_LIMITER_OK;
127 }
128
129
1/2
✓ Branch 0 taken 1019 times.
✗ Branch 1 not taken.
1019 if (empty_slot) {
130 1019 empty_slot->ip = ip;
131 1019 atomic_store(&empty_slot->count, 1);
132 1019 empty_slot->last_seen = get_time_ns();
133 1019 empty_slot->marked_for_deletion = false;
134 1019 *current_count = 1;
135 1019 atomic_fetch_add(&limiter->total_entries, 1);
136 1019 pthread_mutex_unlock(&shard->lock);
137 1019 return IP_LIMITER_OK;
138 }
139
140 ip_entry_t *oldest = NULL;
141 uint64_t oldest_time = UINT64_MAX;
142
143 for (size_t i = 0; i < shard->count; i++) {
144 if (shard->entries[i].count == 0 && shard->entries[i].last_seen < oldest_time) {
145 oldest = &shard->entries[i];
146 oldest_time = shard->entries[i].last_seen;
147 }
148 }
149
150 if (oldest) {
151 oldest->ip = ip;
152 atomic_store(&oldest->count, 1);
153 oldest->last_seen = get_time_ns();
154 oldest->marked_for_deletion = false;
155 *current_count = 1;
156 pthread_mutex_unlock(&shard->lock);
157 return IP_LIMITER_OK;
158 }
159
160 *current_count = limiter->default_limit;
161 atomic_fetch_add(&limiter->rejections_total, 1);
162 pthread_mutex_unlock(&shard->lock);
163 return IP_LIMITER_REJECTED;
164 }
165
166 14 void ip_limiter_decrement(ip_limiter_t *limiter, uint32_t ip)
167 {
168
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 13 times.
14 if (!limiter) {
169 1 return;
170 }
171
172 13 size_t shard_idx = get_shard_index(ip);
173 13 ip_shard_t *shard = &limiter->shards[shard_idx];
174
175 13 pthread_mutex_lock(&shard->lock);
176
177
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 for (size_t i = 0; i < shard->count; i++) {
178
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (shard->entries[i].ip == ip) {
179 13 uint32_t old_count = atomic_fetch_sub(&shard->entries[i].count, 1);
180
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 2 times.
13 if (old_count <= 1) {
181 11 shard->entries[i].marked_for_deletion = true;
182 }
183 13 break;
184 }
185 }
186
187 13 pthread_mutex_unlock(&shard->lock);
188 }
189
190 1 void ip_limiter_compact(ip_limiter_t *limiter)
191 {
192
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!limiter) {
193 return;
194 }
195
196 1 uint64_t now = get_time_ns();
197 1 uint64_t elapsed_ns = now - limiter->last_compaction;
198
199
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (elapsed_ns < COMPACTION_INTERVAL_NS) {
200 1 return;
201 }
202
203 limiter->last_compaction = now;
204
205 size_t total_removed = 0;
206
207 for (size_t i = 0; i < IP_LIMITER_SHARDS; i++) {
208 ip_shard_t *shard = &limiter->shards[i];
209
210 pthread_mutex_lock(&shard->lock);
211
212 size_t removed = 0;
213 for (size_t j = 0; j < shard->count; j++) {
214 if (shard->entries[j].marked_for_deletion && shard->entries[j].count == 0) {
215 uint64_t entry_age = now - shard->entries[j].last_seen;
216 if (entry_age > ENTRY_EXPIRY_NS) {
217 shard->entries[j].ip = 0;
218 shard->entries[j].last_seen = 0;
219 shard->entries[j].marked_for_deletion = false;
220 removed++;
221 }
222 }
223 }
224
225 if (removed > 0 && removed == shard->count) {
226 shard->count = 0;
227 }
228
229 total_removed += removed;
230 pthread_mutex_unlock(&shard->lock);
231 }
232
233 if (total_removed > 0) {
234 atomic_fetch_sub(&limiter->total_entries, total_removed);
235 log_message(LOG_LEVEL_DEBUG, "IP limiter compacted: removed %zu entries", total_removed);
236 }
237 }
238
239 5 uint64_t ip_limiter_get_total_entries(ip_limiter_t *limiter)
240 {
241
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (!limiter) {
242 1 return 0;
243 }
244 4 return atomic_load(&limiter->total_entries);
245 }
246
247 3 uint64_t ip_limiter_get_rejections_total(ip_limiter_t *limiter)
248 {
249
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (!limiter) {
250 1 return 0;
251 }
252 2 return atomic_load(&limiter->rejections_total);
253 }
254