-
-
Notifications
You must be signed in to change notification settings - Fork 672
Added basic DNS cookie support with rate limiting #1737
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zbalkan
wants to merge
35
commits into
TechnitiumSoftware:master
Choose a base branch
from
zbalkan:feat/add-dns-cookie-support
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 a8e5076
Added cookie initialization to path when no config file exists
zbalkan 5c2b3ae
Merged null check in MergeCookieOption
zbalkan 06fcdfd
Fixed endianness of timestamp
zbalkan fc3e83b
Hardened ComputeServerCookie
zbalkan 07c78da
Hardened ValidateServerCookieWithSecret, and removed nested streams
zbalkan 740837e
Hardened CreateResponseCookie
zbalkan 8332944
Rewrite of DnsCookieValidator
zbalkan 5b15704
Cloned current secret in rotation
zbalkan 22b3408
Used System.Threading.Lock instead of object
zbalkan 655389a
TC is necessary now; removed _dnsCookiesSetTcOnBadCookie setting from…
zbalkan f756ba8
Used ReadOnlySpan<byte>instead of mutable byte arrays
zbalkan 3f84da7
Made cookies enabled by default and hardcoded
zbalkan 224475f
Added guard clauses for secret file loading
zbalkan 3af1fd3
Rewrite locking in DnsCookieSecretManager
zbalkan f07627d
Moved cookie code under separate region
zbalkan 8a0384f
Added UDP check for cookies
zbalkan 9348e90
Made a difference between a parsing error and a bad cookie
zbalkan f5a0926
Removed unnecessary allocations
zbalkan 9a8c3e2
Used an internal snapshot to solve concurrency issues on rotation
zbalkan 4f0130f
Used siphash again
zbalkan 68e44ad
USed tmp file during writes
zbalkan 8809691
Improved cookie handling
zbalkan ea6755a
Improved cookie validation
zbalkan 1b3210a
Fixed RFC non-compliant logic
zbalkan f9fae36
Standardized FORMERR
zbalkan 9e4738c
Used lock for DNS cookie initialization
zbalkan c8fd59f
Refactored UpsertOptRecord
zbalkan 1042ad9
Reused cookie variable for clarity
zbalkan ba61e3f
Optimized IPv6 handling in cookie calculation hot path
zbalkan 779089a
Added null checks
zbalkan 95f7f3c
Used ArgumentNullException.ThrowIfNull where applicable
zbalkan 932c72c
Formatting
zbalkan 4647c04
Hardened RFC7873/9018 COOKIE handling (robust parse/rebuild, reliable…
zbalkan d7f2b07
Implemented DNS Cookie based rate limiter with performance tricks, i.…
zbalkan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -431,6 +447,9 @@ public async ValueTask DisposeAsync() | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| _cookieRotationTimer?.Dispose(); | ||||||||||||||||||||||||||||||||||||||||
| _cookieRotationTimer = null; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| _disposed = true; | ||||||||||||||||||||||||||||||||||||||||
| GC.SuppressFinalize(this); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -566,6 +585,8 @@ public void LoadConfigFile() | |||||||||||||||||||||||||||||||||||||||
| _statsManager.MaxStatFileDays = 365; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| SaveConfigFileInternal(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| InitDnsCookiesIfEnabled(); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| catch (Exception ex) | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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(); | ||||||||||||||||||||||||||||||||||||||||
|
zbalkan marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
zbalkan marked this conversation as resolved.
zbalkan marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private void WriteConfigTo(Stream s) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||||||||
|
zbalkan marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #endregion | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
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)); | ||||||||||||||||||||||||||||||||||||||||
|
zbalkan marked this conversation as resolved.
Outdated
zbalkan marked this conversation as resolved.
Outdated
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 | ||||||||||||||||||||||||||||||||||||||||
|
zbalkan marked this conversation as resolved.
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); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
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); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| 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); |
zbalkan marked this conversation as resolved.
Outdated
zbalkan marked this conversation as resolved.
Outdated
zbalkan marked this conversation as resolved.
Outdated
zbalkan marked this conversation as resolved.
Outdated
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.