Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
573a424
Added basic DNS cookie support
zbalkan Feb 18, 2026
a8e5076
Added cookie initialization to path when no config file exists
zbalkan Feb 19, 2026
5c2b3ae
Merged null check in MergeCookieOption
zbalkan Feb 19, 2026
06fcdfd
Fixed endianness of timestamp
zbalkan Feb 19, 2026
fc3e83b
Hardened ComputeServerCookie
zbalkan Feb 19, 2026
07c78da
Hardened ValidateServerCookieWithSecret, and removed nested streams
zbalkan Feb 19, 2026
740837e
Hardened CreateResponseCookie
zbalkan Feb 19, 2026
8332944
Rewrite of DnsCookieValidator
zbalkan Feb 19, 2026
5b15704
Cloned current secret in rotation
zbalkan Feb 19, 2026
22b3408
Used System.Threading.Lock instead of object
zbalkan Feb 19, 2026
655389a
TC is necessary now; removed _dnsCookiesSetTcOnBadCookie setting from…
zbalkan Feb 19, 2026
f756ba8
Used ReadOnlySpan<byte>instead of mutable byte arrays
zbalkan Feb 19, 2026
3f84da7
Made cookies enabled by default and hardcoded
zbalkan Feb 19, 2026
224475f
Added guard clauses for secret file loading
zbalkan Feb 19, 2026
3af1fd3
Rewrite locking in DnsCookieSecretManager
zbalkan Feb 19, 2026
f07627d
Moved cookie code under separate region
zbalkan Feb 19, 2026
8a0384f
Added UDP check for cookies
zbalkan Feb 19, 2026
9348e90
Made a difference between a parsing error and a bad cookie
zbalkan Feb 19, 2026
f5a0926
Removed unnecessary allocations
zbalkan Feb 19, 2026
9a8c3e2
Used an internal snapshot to solve concurrency issues on rotation
zbalkan Feb 20, 2026
4f0130f
Used siphash again
zbalkan Feb 20, 2026
68e44ad
USed tmp file during writes
zbalkan Feb 20, 2026
8809691
Improved cookie handling
zbalkan Feb 20, 2026
ea6755a
Improved cookie validation
zbalkan Feb 20, 2026
1b3210a
Fixed RFC non-compliant logic
zbalkan Feb 20, 2026
f9fae36
Standardized FORMERR
zbalkan Feb 20, 2026
9e4738c
Used lock for DNS cookie initialization
zbalkan Feb 20, 2026
c8fd59f
Refactored UpsertOptRecord
zbalkan Feb 20, 2026
1042ad9
Reused cookie variable for clarity
zbalkan Feb 20, 2026
ba61e3f
Optimized IPv6 handling in cookie calculation hot path
zbalkan Feb 20, 2026
779089a
Added null checks
zbalkan Feb 20, 2026
95f7f3c
Used ArgumentNullException.ThrowIfNull where applicable
zbalkan Feb 20, 2026
932c72c
Formatting
zbalkan Feb 20, 2026
4647c04
Hardened RFC7873/9018 COOKIE handling (robust parse/rebuild, reliable…
zbalkan Apr 24, 2026
d7f2b07
Implemented DNS Cookie based rate limiter with performance tricks, i.…
zbalkan Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions DnsServerCore/Dns/DnsServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,22 @@ enum ServiceState
readonly Timer _saveTimer;
const int SAVE_TIMER_INITIAL_INTERVAL = 5000;

// DNS Cookies (RFC 7873)
readonly bool _dnsCookiesEnabled = true;
readonly string _dnsCookiesSecretFile = "dns.cookies.state";
readonly int _dnsCookiesRotationPeriodHours = 1;
Comment thread
zbalkan marked this conversation as resolved.
readonly bool _dnsCookiesAlwaysEcho = true;

Security.DnsCookieSecretManager _cookieSecrets;
Security.DnsCookieValidator _cookieValidator;
Timer _cookieRotationTimer;

// Optional observability counters
long _cookieValid;
long _cookieInvalid;
long _cookieMissing;
long _cookieBadcookieSent;

#endregion

#region constructor
Expand Down Expand Up @@ -431,6 +447,9 @@ public async ValueTask DisposeAsync()
}
}

_cookieRotationTimer?.Dispose();
_cookieRotationTimer = null;

_disposed = true;
GC.SuppressFinalize(this);
}
Expand Down Expand Up @@ -566,6 +585,8 @@ public void LoadConfigFile()
_statsManager.MaxStatFileDays = 365;

SaveConfigFileInternal();

InitDnsCookiesIfEnabled();
}
catch (Exception ex)
{
Expand Down Expand Up @@ -1078,6 +1099,14 @@ private void ReadConfigFrom(Stream s, bool isConfigTransfer)
int maxStatFileDays = bR.ReadInt32();
if (!isConfigTransfer)
_statsManager.MaxStatFileDays = maxStatFileDays;

if (!isConfigTransfer)
{
_cookieRotationTimer?.Dispose();
_cookieRotationTimer = null;

InitDnsCookiesIfEnabled();
Comment thread
zbalkan marked this conversation as resolved.
Outdated
}
Comment thread
zbalkan marked this conversation as resolved.
Comment thread
zbalkan marked this conversation as resolved.
}

private void WriteConfigTo(Stream s)
Expand Down Expand Up @@ -1359,6 +1388,12 @@ private void WriteConfigTo(Stream s)
bW.Write(_queryLog is not null); //log all queries
bW.Write(_statsManager.EnableInMemoryStats);
bW.Write(_statsManager.MaxStatFileDays);

// DNS cookies
bW.Write(_dnsCookiesEnabled);
bW.Write(_dnsCookiesSecretFile ?? "dns.cookies.state");
bW.Write(_dnsCookiesRotationPeriodHours);
bW.Write(_dnsCookiesAlwaysEcho);
Comment thread
zbalkan marked this conversation as resolved.
Outdated
}

#endregion
Expand Down Expand Up @@ -1526,6 +1561,146 @@ private string ConvertToAbsolutePath(string path)
return Path.Combine(_configFolder, path);
}

private void InitDnsCookiesIfEnabled()
{
if (!_dnsCookiesEnabled)
return;

string secretPath = Path.IsPathRooted(_dnsCookiesSecretFile)
? _dnsCookiesSecretFile
: Path.Combine(_configFolder, _dnsCookiesSecretFile);

_cookieSecrets = new Security.DnsCookieSecretManager(secretPath);
_cookieValidator = new Security.DnsCookieValidator(_cookieSecrets);

Comment thread
zbalkan marked this conversation as resolved.
_cookieRotationTimer?.Dispose();
if (_dnsCookiesRotationPeriodHours > 0)
{
_cookieRotationTimer = new Timer(
_ =>
{
try { _cookieSecrets.Rotate(); }
catch (Exception ex) { _log.Write(ex); }
},
null,
dueTime: TimeSpan.FromMinutes(5),
period: TimeSpan.FromHours(_dnsCookiesRotationPeriodHours));
Comment thread
zbalkan marked this conversation as resolved.
Outdated
Comment thread
zbalkan marked this conversation as resolved.
Outdated
Comment thread
zbalkan marked this conversation as resolved.
Outdated
}
}

private static EDnsCookieOptionData TryGetCookieOption(DnsDatagram request)
{
DnsDatagramEdns edns = request.EDNS;
if (edns is null)
return null;

foreach (EDnsOption opt in edns.Options)
{
if (opt.Code == EDnsOptionCode.COOKIE && opt.Data is EDnsCookieOptionData c)
return c;
}

return null;
}

private DnsDatagram BuildBadCookieResponse(
DnsDatagram request,
IPEndPoint remoteEP,
bool isRecursionAllowed,
EDnsCookieOptionData responseCookie)
{
IReadOnlyList<EDnsOption> options =
MergeCookieOption(request.EDNS?.Options, responseCookie);

ushort udpPayload = request.EDNS?.UdpPayloadSize ?? 512;
EDnsHeaderFlags flags = request.EDNS?.Flags ?? EDnsHeaderFlags.None;

return new DnsDatagram(
request.Identifier,
true,
request.OPCODE,
false,
truncation: true, // REQUIRED by RFC 7873 §5.2.3
Comment thread
zbalkan marked this conversation as resolved.
Comment thread
zbalkan marked this conversation as resolved.
recursionDesired: request.RecursionDesired,
recursionAvailable: isRecursionAllowed,
authenticData: false,
checkingDisabled: request.CheckingDisabled,
DnsResponseCode.BADCOOKIE,
request.Question,
null,
null,
null,
udpPayload,
flags,
options
)
{
Tag = DnsServerResponseType.Authoritative
};
}

private static IReadOnlyList<EDnsOption> MergeCookieOption(
IReadOnlyList<EDnsOption> existing,
EDnsCookieOptionData cookie)
{
List<EDnsOption> list;

if (existing == null)
{
list = new List<EDnsOption>(1);
}
else
{
list = new List<EDnsOption>(existing.Count + 1);
foreach (var opt in existing)
{
if (opt.Code != EDnsOptionCode.COOKIE)
list.Add(opt);
}
}
Comment thread
zbalkan marked this conversation as resolved.

list.Add(new EDnsOption(EDnsOptionCode.COOKIE, cookie));

return list;
}

private static IReadOnlyList<DnsResourceRecord> UpsertOptRecord(
IReadOnlyList<DnsResourceRecord> existingAdditional,
DnsDatagram request,
DnsDatagram response,
IReadOnlyList<EDnsOption> options)
{
var baseEdns = response.EDNS ?? request.EDNS;

ushort udp = baseEdns?.UdpPayloadSize ?? 512;
var flags = baseEdns?.Flags ?? EDnsHeaderFlags.None;

var opt = DnsDatagramEdns.GetOPTFor(
udpPayloadSize: udp,
extendedRCODE: 0,
version: 0,
flags: flags,
options: options);

List<DnsResourceRecord> list =
existingAdditional == null
? new List<DnsResourceRecord>(1)
: new List<DnsResourceRecord>(existingAdditional.Count + 1);

if (existingAdditional != null)
{
foreach (var rr in existingAdditional)
{
if (rr.Type != DnsResourceRecordType.OPT)
list.Add(rr);
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to MergeCookieOption, the null check for existingAdditional is performed twice - once to determine list capacity (line 1687) and again before iterating (line 1691). The second check is redundant and could be removed for cleaner code.

Suggested change
List<DnsResourceRecord> list =
existingAdditional == null
? new List<DnsResourceRecord>(1)
: new List<DnsResourceRecord>(existingAdditional.Count + 1);
if (existingAdditional != null)
{
foreach (var rr in existingAdditional)
{
if (rr.Type != DnsResourceRecordType.OPT)
list.Add(rr);
}
var capacity = (existingAdditional?.Count ?? 0) + 1;
var list = new List<DnsResourceRecord>(capacity);
foreach (var rr in existingAdditional ?? Array.Empty<DnsResourceRecord>())
{
if (rr.Type != DnsResourceRecordType.OPT)
list.Add(rr);

Copilot uses AI. Check for mistakes.
}

list.Add(opt);

return list;
}

#endregion

#region private
Expand Down Expand Up @@ -2396,10 +2571,83 @@ private async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPo
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.BADVERS, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative };
}

// DNS Cookies (RFC 7873)
if (_dnsCookiesEnabled &&
request.EDNS != null &&
_cookieValidator != null)
{
EDnsCookieOptionData cookie =
TryGetCookieOption(request);

if (cookie == null)
{
Interlocked.Increment(ref _cookieMissing);
}
else
{
if (!cookie.ServerCookie.IsEmpty && cookie.ServerCookie.Length > 0)
{
if (!_cookieValidator.Validate(
remoteEP.Address,
cookie))
{
Interlocked.Increment(ref _cookieInvalid);

var respCookie =
_cookieValidator.CreateResponseCookie(
remoteEP.Address,
cookie);
Comment thread
zbalkan marked this conversation as resolved.
Outdated

Interlocked.Increment(
ref _cookieBadcookieSent);

return BuildBadCookieResponse(
request,
remoteEP,
isRecursionAllowed,
respCookie);
Comment thread
zbalkan marked this conversation as resolved.
Outdated
}

Interlocked.Increment(ref _cookieValid);
}
}
}

DnsDatagram response = await ProcessQueryAsync(request, remoteEP, protocol, isRecursionAllowed, false, _clientTimeout, null);
if (response is null)
return null;

// Attach cookie to response if needed
if (_dnsCookiesEnabled && _cookieValidator != null && request.EDNS != null)
{
EDnsCookieOptionData requestCookie = TryGetCookieOption(request);
if (requestCookie != null)
{
bool shouldSendServerCookie =
_dnsCookiesAlwaysEcho ||
requestCookie.ServerCookie.IsEmpty ||
requestCookie.ServerCookie.Length == 0;

if (shouldSendServerCookie)
{
EDnsCookieOptionData responseCookie =
_cookieValidator.CreateResponseCookie(
remoteEP.Address,
requestCookie);
Comment thread
zbalkan marked this conversation as resolved.
Outdated

IReadOnlyList<EDnsOption> mergedOptions =
MergeCookieOption(response.EDNS?.Options, responseCookie);

response = response.Clone(
additional: UpsertOptRecord(
response.Additional,
request,
response,
mergedOptions));
Comment thread
zbalkan marked this conversation as resolved.
Outdated
}
}
}

return await PostProcessQueryAsync(request, remoteEP, protocol, response);
}

Expand Down
Loading
Loading