Skip to content

Commit d7f2b07

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

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
@@ -285,16 +285,27 @@ enum ServiceState
285285
long _cookieBadcookieSent;
286286
long _cookieClientOnly;
287287
long _cookieInvalidDropped;
288+
long _cookieRateLimited;
288289

289290
// Hard-coded initial policy for invalid DNS cookie abuse handling.
290291
// Iteration 2 can make these values configurable.
291292
const int COOKIE_FAILURE_WINDOW_SECONDS = 60;
292293
const int COOKIE_FAILURE_SOFT_THRESHOLD = 20;
294+
const int COOKIE_FAILURE_BADCOOKIE_SLIP_THRESHOLD = 40;
295+
const int COOKIE_FAILURE_BADCOOKIE_SLIP_FACTOR = 4; // send BADCOOKIE for every Nth request after slip threshold
293296
const int COOKIE_FAILURE_HARD_THRESHOLD = 100;
294297
const int COOKIE_FAILURE_RETENTION_SECONDS = 300;
298+
const int COOKIE_BOOTSTRAP_WINDOW_SECONDS = 10;
299+
const int COOKIE_BOOTSTRAP_HARD_THRESHOLD = 30;
300+
const int COOKIE_BOOTSTRAP_REFILL_RATE_PER_SECOND = 3; // 30 tokens per 10 seconds
301+
const int COOKIE_BOOTSTRAP_IDLE_EVICT_SECONDS = 120;
302+
const int COOKIE_BOOTSTRAP_BUCKET_COUNT = 16384; // power-of-two for fast masking
303+
const int COOKIE_BOOTSTRAP_MAX_PROBES = 8;
295304

296305
readonly Dictionary<IPAddress, (long WindowStart, int Count)> _cookieFailureByClient = new();
297306
readonly object _cookieFailureLock = new();
307+
readonly object _cookieBootstrapLock = new();
308+
readonly CookieBootstrapBucket[] _cookieBootstrapBuckets = new CookieBootstrapBucket[COOKIE_BOOTSTRAP_BUCKET_COUNT];
298309

299310
#endregion
300311

@@ -1582,6 +1593,14 @@ private string ConvertToAbsolutePath(string path)
15821593

15831594
#region cookie
15841595

1596+
private struct CookieBootstrapBucket
1597+
{
1598+
public ulong ClientHash;
1599+
public uint LastRefillSecond;
1600+
public uint LastSeenSecond;
1601+
public int Tokens;
1602+
}
1603+
15851604
private void InitDnsCookies()
15861605
{
15871606
lock (_saveLock)
@@ -1596,6 +1615,10 @@ private void InitDnsCookies()
15961615

15971616
lock (_cookieFailureLock)
15981617
_cookieFailureByClient.Clear();
1618+
lock (_cookieBootstrapLock)
1619+
{
1620+
Array.Clear(_cookieBootstrapBuckets, 0, _cookieBootstrapBuckets.Length);
1621+
}
15991622

16001623
return;
16011624
}
@@ -1988,7 +2011,124 @@ private bool ShouldDropInvalidCookie(IPAddress clientAddress, out int failureCou
19882011
}
19892012
}
19902013

1991-
return failureCount > COOKIE_FAILURE_HARD_THRESHOLD;
2014+
if (failureCount > COOKIE_FAILURE_HARD_THRESHOLD)
2015+
return true;
2016+
2017+
if (failureCount > COOKIE_FAILURE_BADCOOKIE_SLIP_THRESHOLD)
2018+
return (failureCount % COOKIE_FAILURE_BADCOOKIE_SLIP_FACTOR) != 0;
2019+
2020+
return false;
2021+
}
2022+
2023+
private static ulong GetCookieBootstrapClientHash(IPAddress clientAddress)
2024+
{
2025+
Span<byte> addressBytes = stackalloc byte[16];
2026+
if (!clientAddress.TryWriteBytes(addressBytes, out int written))
2027+
written = 0;
2028+
2029+
int offset = 0;
2030+
if (written == 16)
2031+
{
2032+
bool isV4Mapped =
2033+
addressBytes[0] == 0 && addressBytes[1] == 0 && addressBytes[2] == 0 && addressBytes[3] == 0 &&
2034+
addressBytes[4] == 0 && addressBytes[5] == 0 && addressBytes[6] == 0 && addressBytes[7] == 0 &&
2035+
addressBytes[8] == 0 && addressBytes[9] == 0 &&
2036+
addressBytes[10] == 0xff && addressBytes[11] == 0xff;
2037+
2038+
if (isV4Mapped)
2039+
{
2040+
offset = 12;
2041+
written = 4;
2042+
}
2043+
}
2044+
2045+
// FNV-1a 64-bit.
2046+
ulong hash = 1469598103934665603UL;
2047+
for (int i = 0; i < written; i++)
2048+
{
2049+
hash ^= addressBytes[offset + i];
2050+
hash *= 1099511628211UL;
2051+
}
2052+
2053+
// Reserve zero as "empty bucket" marker.
2054+
return hash == 0 ? 1UL : hash;
2055+
}
2056+
2057+
private bool ShouldRateLimitCookieBootstrap(IPAddress clientAddress, out int requestCount)
2058+
{
2059+
uint now = unchecked((uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds());
2060+
ulong clientHash = GetCookieBootstrapClientHash(clientAddress);
2061+
int mask = COOKIE_BOOTSTRAP_BUCKET_COUNT - 1;
2062+
int start = (int)(clientHash & (uint)mask);
2063+
2064+
lock (_cookieBootstrapLock)
2065+
{
2066+
int selectedIndex = -1;
2067+
int staleIndex = -1;
2068+
2069+
for (int probe = 0; probe < COOKIE_BOOTSTRAP_MAX_PROBES; probe++)
2070+
{
2071+
int index = (start + probe) & mask;
2072+
ref CookieBootstrapBucket bucket = ref _cookieBootstrapBuckets[index];
2073+
2074+
if (bucket.ClientHash == clientHash)
2075+
{
2076+
selectedIndex = index;
2077+
break;
2078+
}
2079+
2080+
if (bucket.ClientHash == 0)
2081+
{
2082+
selectedIndex = index;
2083+
break;
2084+
}
2085+
2086+
if ((now - bucket.LastSeenSecond) >= COOKIE_BOOTSTRAP_IDLE_EVICT_SECONDS && staleIndex < 0)
2087+
staleIndex = index;
2088+
}
2089+
2090+
if (selectedIndex < 0)
2091+
selectedIndex = staleIndex;
2092+
2093+
if (selectedIndex < 0)
2094+
{
2095+
requestCount = int.MaxValue; // table pressure fail-closed
2096+
return true;
2097+
}
2098+
2099+
ref CookieBootstrapBucket state = ref _cookieBootstrapBuckets[selectedIndex];
2100+
2101+
if (state.ClientHash != clientHash)
2102+
{
2103+
state.ClientHash = clientHash;
2104+
state.LastRefillSecond = now;
2105+
state.LastSeenSecond = now;
2106+
state.Tokens = COOKIE_BOOTSTRAP_HARD_THRESHOLD;
2107+
}
2108+
else
2109+
{
2110+
uint elapsed = now - state.LastRefillSecond;
2111+
if (elapsed > 0)
2112+
{
2113+
int refill = (int)Math.Min(int.MaxValue, elapsed * (uint)COOKIE_BOOTSTRAP_REFILL_RATE_PER_SECOND);
2114+
state.Tokens = Math.Min(COOKIE_BOOTSTRAP_HARD_THRESHOLD, state.Tokens + refill);
2115+
state.LastRefillSecond = now;
2116+
}
2117+
2118+
state.LastSeenSecond = now;
2119+
}
2120+
2121+
if (state.Tokens <= 0)
2122+
{
2123+
requestCount = COOKIE_BOOTSTRAP_HARD_THRESHOLD + 1;
2124+
return true;
2125+
}
2126+
2127+
state.Tokens--;
2128+
}
2129+
2130+
requestCount = 0;
2131+
return false;
19922132
}
19932133

19942134
private DnsDatagram HandleInvalidCookieRequest(
@@ -2011,6 +2151,11 @@ private DnsDatagram HandleInvalidCookieRequest(
20112151
{
20122152
Interlocked.Increment(ref _cookieInvalidDropped);
20132153

2154+
if (failuresInWindow == (COOKIE_FAILURE_BADCOOKIE_SLIP_THRESHOLD + 1))
2155+
{
2156+
_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}.");
2157+
}
2158+
20142159
if (failuresInWindow == (COOKIE_FAILURE_HARD_THRESHOLD + 1))
20152160
{
20162161
_log.Write($"Client '{remoteEP.Address}' exceeded DNS cookie invalid hard threshold ({COOKIE_FAILURE_HARD_THRESHOLD}/{COOKIE_FAILURE_WINDOW_SECONDS}s); dropping invalid-cookie requests.");
@@ -2956,6 +3101,18 @@ private async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPo
29563101
if (requestCookie == null)
29573102
{
29583103
Interlocked.Increment(ref _cookieMissing);
3104+
3105+
if (ShouldRateLimitCookieBootstrap(remoteEP.Address, out int requestsInWindow))
3106+
{
3107+
Interlocked.Increment(ref _cookieRateLimited);
3108+
3109+
if (requestsInWindow == (COOKIE_BOOTSTRAP_HARD_THRESHOLD + 1))
3110+
{
3111+
_log.Write($"Client '{remoteEP.Address}' exceeded DNS cookie bootstrap threshold ({COOKIE_BOOTSTRAP_HARD_THRESHOLD}/{COOKIE_BOOTSTRAP_WINDOW_SECONDS}s); dropping UDP requests without cookie.");
3112+
}
3113+
3114+
return null;
3115+
}
29593116
}
29603117
else
29613118
{
@@ -3000,6 +3157,18 @@ private async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPo
30003157
if (scLen == 0)
30013158
{
30023159
Interlocked.Increment(ref _cookieClientOnly);
3160+
3161+
if (ShouldRateLimitCookieBootstrap(remoteEP.Address, out int requestsInWindow))
3162+
{
3163+
Interlocked.Increment(ref _cookieRateLimited);
3164+
3165+
if (requestsInWindow == (COOKIE_BOOTSTRAP_HARD_THRESHOLD + 1))
3166+
{
3167+
_log.Write($"Client '{remoteEP.Address}' exceeded DNS cookie bootstrap threshold ({COOKIE_BOOTSTRAP_HARD_THRESHOLD}/{COOKIE_BOOTSTRAP_WINDOW_SECONDS}s); dropping client-cookie-only requests.");
3168+
}
3169+
3170+
return null;
3171+
}
30033172
}
30043173
else
30053174
{

0 commit comments

Comments
 (0)