Skip to content

Commit f052d00

Browse files
committed
Implemented DNS Cookie based rate limiter with performance tricks, i.e. drop without responding beyond a threshold
1 parent 4a5750e commit f052d00

1 file changed

Lines changed: 170 additions & 1 deletion

File tree

DnsServerCore/Dns/DnsServer.cs

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,16 +289,27 @@ enum ServiceState
289289
long _cookieBadcookieSent;
290290
long _cookieClientOnly;
291291
long _cookieInvalidDropped;
292+
long _cookieRateLimited;
292293

293294
// Hard-coded initial policy for invalid DNS cookie abuse handling.
294295
// Iteration 2 can make these values configurable.
295296
const int COOKIE_FAILURE_WINDOW_SECONDS = 60;
296297
const int COOKIE_FAILURE_SOFT_THRESHOLD = 20;
298+
const int COOKIE_FAILURE_BADCOOKIE_SLIP_THRESHOLD = 40;
299+
const int COOKIE_FAILURE_BADCOOKIE_SLIP_FACTOR = 4; // send BADCOOKIE for every Nth request after slip threshold
297300
const int COOKIE_FAILURE_HARD_THRESHOLD = 100;
298301
const int COOKIE_FAILURE_RETENTION_SECONDS = 300;
302+
const int COOKIE_BOOTSTRAP_WINDOW_SECONDS = 10;
303+
const int COOKIE_BOOTSTRAP_HARD_THRESHOLD = 30;
304+
const int COOKIE_BOOTSTRAP_REFILL_RATE_PER_SECOND = 3; // 30 tokens per 10 seconds
305+
const int COOKIE_BOOTSTRAP_IDLE_EVICT_SECONDS = 120;
306+
const int COOKIE_BOOTSTRAP_BUCKET_COUNT = 16384; // power-of-two for fast masking
307+
const int COOKIE_BOOTSTRAP_MAX_PROBES = 8;
299308

300309
readonly Dictionary<IPAddress, (long WindowStart, int Count)> _cookieFailureByClient = new();
301310
readonly object _cookieFailureLock = new();
311+
readonly object _cookieBootstrapLock = new();
312+
readonly CookieBootstrapBucket[] _cookieBootstrapBuckets = new CookieBootstrapBucket[COOKIE_BOOTSTRAP_BUCKET_COUNT];
302313

303314
#endregion
304315

@@ -1633,6 +1644,14 @@ private string ConvertToAbsolutePath(string path)
16331644

16341645
#region cookie
16351646

1647+
private struct CookieBootstrapBucket
1648+
{
1649+
public ulong ClientHash;
1650+
public uint LastRefillSecond;
1651+
public uint LastSeenSecond;
1652+
public int Tokens;
1653+
}
1654+
16361655
private void InitDnsCookies()
16371656
{
16381657
lock (_saveLock)
@@ -1647,6 +1666,10 @@ private void InitDnsCookies()
16471666

16481667
lock (_cookieFailureLock)
16491668
_cookieFailureByClient.Clear();
1669+
lock (_cookieBootstrapLock)
1670+
{
1671+
Array.Clear(_cookieBootstrapBuckets, 0, _cookieBootstrapBuckets.Length);
1672+
}
16501673

16511674
return;
16521675
}
@@ -2039,7 +2062,124 @@ private bool ShouldDropInvalidCookie(IPAddress clientAddress, out int failureCou
20392062
}
20402063
}
20412064

2042-
return failureCount > COOKIE_FAILURE_HARD_THRESHOLD;
2065+
if (failureCount > COOKIE_FAILURE_HARD_THRESHOLD)
2066+
return true;
2067+
2068+
if (failureCount > COOKIE_FAILURE_BADCOOKIE_SLIP_THRESHOLD)
2069+
return (failureCount % COOKIE_FAILURE_BADCOOKIE_SLIP_FACTOR) != 0;
2070+
2071+
return false;
2072+
}
2073+
2074+
private static ulong GetCookieBootstrapClientHash(IPAddress clientAddress)
2075+
{
2076+
Span<byte> addressBytes = stackalloc byte[16];
2077+
if (!clientAddress.TryWriteBytes(addressBytes, out int written))
2078+
written = 0;
2079+
2080+
int offset = 0;
2081+
if (written == 16)
2082+
{
2083+
bool isV4Mapped =
2084+
addressBytes[0] == 0 && addressBytes[1] == 0 && addressBytes[2] == 0 && addressBytes[3] == 0 &&
2085+
addressBytes[4] == 0 && addressBytes[5] == 0 && addressBytes[6] == 0 && addressBytes[7] == 0 &&
2086+
addressBytes[8] == 0 && addressBytes[9] == 0 &&
2087+
addressBytes[10] == 0xff && addressBytes[11] == 0xff;
2088+
2089+
if (isV4Mapped)
2090+
{
2091+
offset = 12;
2092+
written = 4;
2093+
}
2094+
}
2095+
2096+
// FNV-1a 64-bit.
2097+
ulong hash = 1469598103934665603UL;
2098+
for (int i = 0; i < written; i++)
2099+
{
2100+
hash ^= addressBytes[offset + i];
2101+
hash *= 1099511628211UL;
2102+
}
2103+
2104+
// Reserve zero as "empty bucket" marker.
2105+
return hash == 0 ? 1UL : hash;
2106+
}
2107+
2108+
private bool ShouldRateLimitCookieBootstrap(IPAddress clientAddress, out int requestCount)
2109+
{
2110+
uint now = unchecked((uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds());
2111+
ulong clientHash = GetCookieBootstrapClientHash(clientAddress);
2112+
int mask = COOKIE_BOOTSTRAP_BUCKET_COUNT - 1;
2113+
int start = (int)(clientHash & (uint)mask);
2114+
2115+
lock (_cookieBootstrapLock)
2116+
{
2117+
int selectedIndex = -1;
2118+
int staleIndex = -1;
2119+
2120+
for (int probe = 0; probe < COOKIE_BOOTSTRAP_MAX_PROBES; probe++)
2121+
{
2122+
int index = (start + probe) & mask;
2123+
ref CookieBootstrapBucket bucket = ref _cookieBootstrapBuckets[index];
2124+
2125+
if (bucket.ClientHash == clientHash)
2126+
{
2127+
selectedIndex = index;
2128+
break;
2129+
}
2130+
2131+
if (bucket.ClientHash == 0)
2132+
{
2133+
selectedIndex = index;
2134+
break;
2135+
}
2136+
2137+
if ((now - bucket.LastSeenSecond) >= COOKIE_BOOTSTRAP_IDLE_EVICT_SECONDS && staleIndex < 0)
2138+
staleIndex = index;
2139+
}
2140+
2141+
if (selectedIndex < 0)
2142+
selectedIndex = staleIndex;
2143+
2144+
if (selectedIndex < 0)
2145+
{
2146+
requestCount = int.MaxValue; // table pressure fail-closed
2147+
return true;
2148+
}
2149+
2150+
ref CookieBootstrapBucket state = ref _cookieBootstrapBuckets[selectedIndex];
2151+
2152+
if (state.ClientHash != clientHash)
2153+
{
2154+
state.ClientHash = clientHash;
2155+
state.LastRefillSecond = now;
2156+
state.LastSeenSecond = now;
2157+
state.Tokens = COOKIE_BOOTSTRAP_HARD_THRESHOLD;
2158+
}
2159+
else
2160+
{
2161+
uint elapsed = now - state.LastRefillSecond;
2162+
if (elapsed > 0)
2163+
{
2164+
int refill = (int)Math.Min(int.MaxValue, elapsed * (uint)COOKIE_BOOTSTRAP_REFILL_RATE_PER_SECOND);
2165+
state.Tokens = Math.Min(COOKIE_BOOTSTRAP_HARD_THRESHOLD, state.Tokens + refill);
2166+
state.LastRefillSecond = now;
2167+
}
2168+
2169+
state.LastSeenSecond = now;
2170+
}
2171+
2172+
if (state.Tokens <= 0)
2173+
{
2174+
requestCount = COOKIE_BOOTSTRAP_HARD_THRESHOLD + 1;
2175+
return true;
2176+
}
2177+
2178+
state.Tokens--;
2179+
}
2180+
2181+
requestCount = 0;
2182+
return false;
20432183
}
20442184

20452185
private DnsDatagram HandleInvalidCookieRequest(
@@ -2062,6 +2202,11 @@ private DnsDatagram HandleInvalidCookieRequest(
20622202
{
20632203
Interlocked.Increment(ref _cookieInvalidDropped);
20642204

2205+
if (failuresInWindow == (COOKIE_FAILURE_BADCOOKIE_SLIP_THRESHOLD + 1))
2206+
{
2207+
_log.Write($"Client '{remoteEP.Address}' crossed DNS cookie invalid slip threshold ({COOKIE_FAILURE_BADCOOKIE_SLIP_THRESHOLD}/{COOKIE_FAILURE_WINDOW_SECONDS}s); BADCOOKIE responses are now throttled to 1/{COOKIE_FAILURE_BADCOOKIE_SLIP_FACTOR}.");
2208+
}
2209+
20652210
if (failuresInWindow == (COOKIE_FAILURE_HARD_THRESHOLD + 1))
20662211
{
20672212
_log.Write($"Client '{remoteEP.Address}' exceeded DNS cookie invalid hard threshold ({COOKIE_FAILURE_HARD_THRESHOLD}/{COOKIE_FAILURE_WINDOW_SECONDS}s); dropping invalid-cookie requests.");
@@ -3204,6 +3349,18 @@ private async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPo
32043349
if (requestCookie == null)
32053350
{
32063351
Interlocked.Increment(ref _cookieMissing);
3352+
3353+
if (ShouldRateLimitCookieBootstrap(remoteEP.Address, out int requestsInWindow))
3354+
{
3355+
Interlocked.Increment(ref _cookieRateLimited);
3356+
3357+
if (requestsInWindow == (COOKIE_BOOTSTRAP_HARD_THRESHOLD + 1))
3358+
{
3359+
_log.Write($"Client '{remoteEP.Address}' exceeded DNS cookie bootstrap threshold ({COOKIE_BOOTSTRAP_HARD_THRESHOLD}/{COOKIE_BOOTSTRAP_WINDOW_SECONDS}s); dropping UDP requests without cookie.");
3360+
}
3361+
3362+
return null;
3363+
}
32073364
}
32083365
else
32093366
{
@@ -3248,6 +3405,18 @@ private async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPo
32483405
if (scLen == 0)
32493406
{
32503407
Interlocked.Increment(ref _cookieClientOnly);
3408+
3409+
if (ShouldRateLimitCookieBootstrap(remoteEP.Address, out int requestsInWindow))
3410+
{
3411+
Interlocked.Increment(ref _cookieRateLimited);
3412+
3413+
if (requestsInWindow == (COOKIE_BOOTSTRAP_HARD_THRESHOLD + 1))
3414+
{
3415+
_log.Write($"Client '{remoteEP.Address}' exceeded DNS cookie bootstrap threshold ({COOKIE_BOOTSTRAP_HARD_THRESHOLD}/{COOKIE_BOOTSTRAP_WINDOW_SECONDS}s); dropping client-cookie-only requests.");
3416+
}
3417+
3418+
return null;
3419+
}
32513420
}
32523421
else
32533422
{

0 commit comments

Comments
 (0)