From 3eb9b2922dc962a819280e9463546af4ac8cadd6 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Fri, 5 Dec 2025 16:05:46 +0200 Subject: [PATCH 01/19] Added BlockingAnswerTtl with default value of 3600 --- Apps/MispConnectorApp/App.cs | 53 +++++++++++++++-------------- Apps/MispConnectorApp/dnsApp.config | 1 + 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 6f77979f6..4913986e9 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -151,50 +151,47 @@ public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint rem string blockingReport = $"source=misp-connector;domain={blockedDomain}"; + // Add blocking report as EDE to EDNS options for both TXT and other queries if the query datagram has EDNS field EDnsOption[] options = null; if (_config.AddExtendedDnsError && request.EDNS is not null) { - options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, string.Empty)) }; + options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, blockingReport)) }; } + DnsResourceRecord[] answer = null; + DnsResourceRecord[] authority = null; + bool authoritative = false; + DnsResponseCode RCODE; if (_config.AllowTxtBlockingReport && question.Type == DnsResourceRecordType.TXT) { - DnsResourceRecord[] answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData(string.Empty)) }; - return Task.FromResult(new DnsDatagram( - ID: request.Identifier, - isResponse: true, - OPCODE: DnsOpcode.StandardQuery, - authoritativeAnswer: false, - truncation: false, - recursionDesired: request.RecursionDesired, - recursionAvailable: true, - authenticData: false, - checkingDisabled: false, - RCODE: DnsResponseCode.NoError, - question: request.Question, - answer: answer, - authority: null, - additional: null, - udpPayloadSize: request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, - ednsFlags: EDnsHeaderFlags.None, - options: options - )); + answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _config.BlockingAnswerTtl, new DnsTXTRecordData(blockingReport)) }; + RCODE = DnsResponseCode.NoError; } + else + { + authority = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.SOA, question.Class, _config.BlockingAnswerTtl, _soaRecord) }; + RCODE = DnsResponseCode.NxDomain; + authoritative = true; + } + + return BlockResponse(request: request, options: options, authority: authority, answer: answer, authoritativeAnswer: authoritative, rCode: RCODE); + } - DnsResourceRecord[] authority = { new DnsResourceRecord(question.Name, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; + private Task BlockResponse(DnsDatagram request, EDnsOption[] options, DnsResourceRecord[] authority, DnsResourceRecord[] answer, bool authoritativeAnswer, DnsResponseCode rCode) + { return Task.FromResult(new DnsDatagram( ID: request.Identifier, isResponse: true, OPCODE: DnsOpcode.StandardQuery, - authoritativeAnswer: true, + authoritativeAnswer: authoritativeAnswer, truncation: false, recursionDesired: request.RecursionDesired, recursionAvailable: true, authenticData: false, checkingDisabled: false, - RCODE: DnsResponseCode.NxDomain, + RCODE: rCode, question: request.Question, - answer: null, + answer: answer, authority: authority, additional: null, udpPayloadSize: request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, @@ -528,11 +525,16 @@ private class Config [JsonPropertyName("enableBlocking")] public bool EnableBlocking { get; set; } = true; + [JsonPropertyName("maxIocAge")] [Required(ErrorMessage = "maxIocAge is a required configuration property.")] [RegularExpression(@"^\d+[mhd]$", ErrorMessage = "Invalid interval format. Use a number followed by 'm', 'h', or 'd' (e.g., '90m', '2h', '7d').", MatchTimeoutInMilliseconds = 3000)] public string MaxIocAge { get; set; } + [JsonPropertyName("blockingAnswerTtl")] + [Range(30, 86400, ErrorMessage = "The minimum available TTL is usually 30, equivalent to 30 seconds. However, most sites use a default TTL of 3600 (one hour). The maximum TTL that you can apply is 86,400 (24 hours).")] + public uint BlockingAnswerTtl { get; set; } + [JsonPropertyName("mispApiKey")] [Required(ErrorMessage = "mispApiKey is a required configuration property.")] [MinLength(1, ErrorMessage = "mispApiKey cannot be empty.")] @@ -542,6 +544,7 @@ private class Config [Required(ErrorMessage = "mispServerUrl is a required configuration property.")] [Url(ErrorMessage = "mispServerUrl must be a valid URL.")] public string MispServerUrl { get; set; } + [JsonPropertyName("paginationLimit")] public int PaginationLimit { get; set; } = 5000; diff --git a/Apps/MispConnectorApp/dnsApp.config b/Apps/MispConnectorApp/dnsApp.config index e5e76a052..3be30b56c 100644 --- a/Apps/MispConnectorApp/dnsApp.config +++ b/Apps/MispConnectorApp/dnsApp.config @@ -5,6 +5,7 @@ "disableTlsValidation": false, "updateInterval": "2h", "maxIocAge": "30d", + "blockingAnswerTtl": 3600, "allowTxtBlockingReport": true, "paginationLimit": 5000, "addExtendedDnsError": true From ff3b2a354caf2ec10d870f8a1ae80cf0c02254ca Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Fri, 5 Dec 2025 17:51:58 +0200 Subject: [PATCH 02/19] Incremented version --- Apps/MispConnectorApp/MispConnectorApp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apps/MispConnectorApp/MispConnectorApp.csproj b/Apps/MispConnectorApp/MispConnectorApp.csproj index 5ad84658f..85e7c8ff3 100644 --- a/Apps/MispConnectorApp/MispConnectorApp.csproj +++ b/Apps/MispConnectorApp/MispConnectorApp.csproj @@ -3,7 +3,7 @@ net9.0 false - 1.0 + 1.1 false Technitium Technitium DNS Server From b96955706666727f7543fff865952a23a004d907 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Sat, 6 Dec 2025 16:50:44 +0200 Subject: [PATCH 03/19] Response to reviews --- Apps/MispConnectorApp/App.cs | 12 ++++++------ Apps/MispConnectorApp/dnsApp.config | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 4913986e9..9177269cf 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -161,20 +161,20 @@ public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint rem DnsResourceRecord[] answer = null; DnsResourceRecord[] authority = null; bool authoritative = false; - DnsResponseCode RCODE; + DnsResponseCode rCode; if (_config.AllowTxtBlockingReport && question.Type == DnsResourceRecordType.TXT) { answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _config.BlockingAnswerTtl, new DnsTXTRecordData(blockingReport)) }; - RCODE = DnsResponseCode.NoError; + rCode = DnsResponseCode.NoError; } else { authority = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.SOA, question.Class, _config.BlockingAnswerTtl, _soaRecord) }; - RCODE = DnsResponseCode.NxDomain; + rCode = DnsResponseCode.NxDomain; authoritative = true; } - return BlockResponse(request: request, options: options, authority: authority, answer: answer, authoritativeAnswer: authoritative, rCode: RCODE); + return BlockResponse(request: request, options: options, authority: authority, answer: answer, authoritativeAnswer: authoritative, rCode: rCode); } private Task BlockResponse(DnsDatagram request, EDnsOption[] options, DnsResourceRecord[] authority, DnsResourceRecord[] answer, bool authoritativeAnswer, DnsResponseCode rCode) @@ -532,8 +532,8 @@ private class Config public string MaxIocAge { get; set; } [JsonPropertyName("blockingAnswerTtl")] - [Range(30, 86400, ErrorMessage = "The minimum available TTL is usually 30, equivalent to 30 seconds. However, most sites use a default TTL of 3600 (one hour). The maximum TTL that you can apply is 86,400 (24 hours).")] - public uint BlockingAnswerTtl { get; set; } + [Range(30, 86400, ErrorMessage = "blockingAnswerTtl must be between 30 and 86400 seconds.")] + public uint BlockingAnswerTtl { get; set; } = 30; [JsonPropertyName("mispApiKey")] [Required(ErrorMessage = "mispApiKey is a required configuration property.")] diff --git a/Apps/MispConnectorApp/dnsApp.config b/Apps/MispConnectorApp/dnsApp.config index 3be30b56c..d345add79 100644 --- a/Apps/MispConnectorApp/dnsApp.config +++ b/Apps/MispConnectorApp/dnsApp.config @@ -5,7 +5,7 @@ "disableTlsValidation": false, "updateInterval": "2h", "maxIocAge": "30d", - "blockingAnswerTtl": 3600, + "blockingAnswerTtl": 30, "allowTxtBlockingReport": true, "paginationLimit": 5000, "addExtendedDnsError": true From 5398d2591ef8eda752c174696c324fde9dad8b52 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 22 Jan 2026 08:21:35 +0300 Subject: [PATCH 04/19] Add MISP explanation to README Added a section explaining MISP and its relevance to the plugin. --- Apps/MispConnectorApp/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Apps/MispConnectorApp/README.md b/Apps/MispConnectorApp/README.md index 5b8a4a6cd..c54e95b38 100644 --- a/Apps/MispConnectorApp/README.md +++ b/Apps/MispConnectorApp/README.md @@ -4,6 +4,12 @@ A plugin that pulls malicious domain names from MISP feeds and enforces blocking It maintains in-memory blocklists with disk-backed caching and periodically refreshes from the source. +## What is MISP + +MISP is a threat intelligence platform for sharing, storing and correlating Indicators of Compromise (IOC) of targeted attacks, threat intelligence, financial fraud information, vulnerability information or even counter-terrorism information. Refer to the [project documentation for more details](https://www.misp-project.org/documentation/). This plugin assumes an existing MISP instance to connect to. Installation and configuration is out of scope of Technitium DNS Server. + +See [this article](https://zaferbalkan.com/technitium-misp/) for a sample use case. + ## Features - Retrieves indicators of compromise (IOCs) aka. malicious domain names from a MISP server via its REST API. @@ -40,4 +46,4 @@ Supply a JSON configuration like the following: # Acknowledgement -Thanks to everyone who has been part of or contributed to [MISP Project](https://www.misp-project.org/) for being an amazing resource. \ No newline at end of file +Thanks to everyone who has been part of or contributed to [MISP Project](https://www.misp-project.org/) for being an amazing resource. From 66db46afec42a8d11e89562efcfcb9db1ecb2bee Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 10:57:51 +0200 Subject: [PATCH 05/19] Used span-based lookup to prevent new string allocations Signed-off-by: Zafer Balkan --- Apps/MispConnectorApp/App.cs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 9177269cf..cc7aafe0e 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -424,24 +424,20 @@ private bool IsDomainBlocked(string domain, out string foundZone) { FrozenSet currentBlocklist = _domainBlocklist; + // Span-based lookup + FrozenSet.AlternateLookup> lookup = currentBlocklist.GetAlternateLookup>(); + ReadOnlySpan currentSpan = domain.AsSpan(); while (true) { - // To look up in a HashSet, we must provide a string. - string key = new string(currentSpan); - if (currentBlocklist.TryGetValue(key, out foundZone)) - { + if (lookup.TryGetValue(currentSpan, out foundZone)) return true; - } int dotIndex = currentSpan.IndexOf('.'); - if (dotIndex == -1) - { - break; // No more parent domains. - } + if (dotIndex < 0) + break; - // Slice to the parent domain view. No allocation here. currentSpan = currentSpan.Slice(dotIndex + 1); } @@ -483,11 +479,11 @@ private async Task UpdateIocsAsync(CancellationToken cancellationToken) { await WriteIocsToCacheAsync(domains, cancellationToken); Interlocked.Exchange(ref _domainBlocklist, domains); - _dnsServer.WriteLog($"MISP Connector: Successfully updated blocklist with {domains.Count} domains."); + _dnsServer.WriteLog($"MISP Connector: Successfully updated currentBlocklist with {domains.Count} domains."); } else { - _dnsServer.WriteLog("MISP data has not changed. No update to blocklist or cache is necessary."); + _dnsServer.WriteLog("MISP data has not changed. No update to currentBlocklist or cache is necessary."); } } From b62fea4c2b88edb8660e32b940b7d6852b7a7a6b Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 11:00:26 +0200 Subject: [PATCH 06/19] We already use case-insensitive comparison. No need for lowering and allocation of new strings --- Apps/MispConnectorApp/App.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index cc7aafe0e..02e47089a 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -395,7 +395,8 @@ private async Task> FetchIocFromMispAsync(CancellationToken canc foreach (MispAttribute attribute in attributes) { - string ioc = attribute.Value?.Trim().ToLowerInvariant(); + string ioc = attribute.Value?.Trim(); + if (!string.IsNullOrEmpty(ioc)) { if (DnsClient.IsDomainNameValid(ioc)) From af10c48b02957da3cf313ce9cbebc3ab0ebc4b67 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 11:11:02 +0200 Subject: [PATCH 07/19] Don't retry when operation cancelled --- Apps/MispConnectorApp/App.cs | 43 +++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 02e47089a..08406a38a 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -367,23 +367,19 @@ private async Task> FetchIocFromMispAsync(CancellationToken canc break; } - catch (Exception ex) when (ex is HttpRequestException || ex is SocketException || ex is OperationCanceledException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { - // These are likely transient network errors, so we should retry. - _dnsServer.WriteLog($"WARNING: A transient network error occurred on page {page}, attempt {attempt}/{maxRetries}. Error: {ex.Message}"); - if (attempt < maxRetries) - { - TimeSpan delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)) + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); - _dnsServer.WriteLog($"Waiting for {delay.TotalSeconds:F1} seconds before retrying..."); - await Task.Delay(delay, cancellationToken); - } - else - { - // All retries have failed for this page. - _dnsServer.WriteLog($"ERROR: Failed to fetch page {page} after {maxRetries} attempts. Aborting entire update cycle."); - throw; - } + throw; + } + catch (HttpRequestException ex) + { + await HandleRetry(ex, page, maxRetries, attempt, cancellationToken); + } + catch (SocketException ex) + { + await HandleRetry(ex, page, maxRetries, attempt, cancellationToken); } + } List attributes = mispResponse?.Response?.Attribute; @@ -420,6 +416,23 @@ private async Task> FetchIocFromMispAsync(CancellationToken canc _dnsServer.WriteLog($"Finished paginated fetch. Freezing {iocSet.Count} IOCs for optimal read performance..."); return iocSet; } + private async Task HandleRetry(Exception ex, int page, int maxRetries, int attempt, CancellationToken cancellationToken) + { + // These are likely transient network errors, so we should retry. + _dnsServer.WriteLog($"WARNING: A transient network error occurred on page {page}, attempt {attempt}/{maxRetries}. Error: {ex.Message}"); + if (attempt < maxRetries) + { + TimeSpan delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)) + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); + _dnsServer.WriteLog($"Waiting for {delay.TotalSeconds:F1} seconds before retrying..."); + await Task.Delay(delay, cancellationToken); + } + else + { + // All retries have failed for this page. + _dnsServer.WriteLog($"ERROR: Failed to fetch page {page} after {maxRetries} attempts. Aborting entire update cycle."); + throw ex; + } + } private bool IsDomainBlocked(string domain, out string foundZone) { From aa2151e448a378d451e99642a7ab69616149e44a Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 11:13:33 +0200 Subject: [PATCH 08/19] Throw on broken JSON response --- Apps/MispConnectorApp/App.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 08406a38a..53c234304 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -382,8 +382,10 @@ private async Task> FetchIocFromMispAsync(CancellationToken canc } - List attributes = mispResponse?.Response?.Attribute; - if (attributes == null || attributes.Count == 0) + List attributes = (mispResponse?.Response?.Attribute) ?? + throw new InvalidDataException("Invalid or unexpected MISP response schema."); + + if (attributes.Count == 0) { hasMorePages = false; continue; From 88a1eef2dbcd199fd106defe7b1bcaef84781443 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 11:23:42 +0200 Subject: [PATCH 09/19] Added cleanup in hashset generation --- Apps/MispConnectorApp/App.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 53c234304..d0628829e 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -467,7 +467,15 @@ private async Task LoadBlocklistFromCacheAsync() { try { - FrozenSet domains = (await File.ReadAllLinesAsync(_domainCacheFilePath)).ToHashSet(StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase); + var set = new HashSet(StringComparer.OrdinalIgnoreCase); + + await foreach (var line in File.ReadLinesAsync(_domainCacheFilePath)) + { + if (!string.IsNullOrWhiteSpace(line)) + set.Add(line); + } + + FrozenSet domains = set.ToFrozenSet(StringComparer.OrdinalIgnoreCase); Interlocked.Exchange(ref _domainBlocklist, domains); _dnsServer.WriteLog($"MISP Connector: Loaded {domains.Count} domains from cache."); } From d86981c04b9ab2feff7e9473a91b3910b80a4dcc Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 11:36:22 +0200 Subject: [PATCH 10/19] Added a comment to clarify the intent --- Apps/MispConnectorApp/App.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index d0628829e..c35cb5260 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -130,6 +130,8 @@ public async Task InitializeAsync(IDnsServer dnsServer, string config) } } + // No allowlist override in this app. + // ProcessRequestAsync handles blocking. public Task IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP) { return Task.FromResult(false); From c5b0c117461ecba251426d1566184f391fc9e971 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 11:38:50 +0200 Subject: [PATCH 11/19] Timeout without allocationg a new CTS --- Apps/MispConnectorApp/App.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index c35cb5260..7cd0892a6 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -272,11 +272,11 @@ private async Task CheckTcpPortAsync(Uri serverUri, CancellationToken canc try { - using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(timeout).Token)) - using (TcpClient client = new TcpClient()) - { - await client.ConnectAsync(host, port, cts.Token); - } + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(timeout); + + using var client = new TcpClient(); + await client.ConnectAsync(host, port, cts.Token); _dnsServer.WriteLog($"Pre-flight TCP check successful for {host}:{port}."); return true; From 32bb8ff58202f0f74262260a0fc23af817d9cb15 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 11:39:50 +0200 Subject: [PATCH 12/19] Prevented explicit cancellations causing delays on shutdown --- Apps/MispConnectorApp/App.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 7cd0892a6..38e6945a8 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -281,6 +281,10 @@ private async Task CheckTcpPortAsync(Uri serverUri, CancellationToken canc _dnsServer.WriteLog($"Pre-flight TCP check successful for {host}:{port}."); return true; } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (OperationCanceledException) { _dnsServer.WriteLog($"ERROR: Pre-flight TCP check failed: Connection to {host}:{port} timed out after {timeout.TotalSeconds} seconds. Check firewall rules or network route."); From 887b406e605dc6deea9f55352f469f10b1c8b5cd Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 13:27:10 +0200 Subject: [PATCH 13/19] Fixing exception handling --- Apps/MispConnectorApp/App.cs | 48 +++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 38e6945a8..6b66fe729 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -216,18 +216,20 @@ private async Task StartUpdateLoopAsync(CancellationToken cancellationToken) { await UpdateIocsAsync(cancellationToken); } - catch (OperationCanceledException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { _dnsServer.WriteLog("Update loop is shutting down gracefully."); break; } + catch (Exception ex) { _dnsServer.WriteLog($"FATAL: The MispConnector update task failed unexpectedly. Error: {ex.Message}"); _dnsServer.WriteLog(ex); } - await timer.WaitForNextTickAsync(cancellationToken); + if (!await timer.WaitForNextTickAsync(cancellationToken)) + break; } } } @@ -379,11 +381,13 @@ private async Task> FetchIocFromMispAsync(CancellationToken canc } catch (HttpRequestException ex) { - await HandleRetry(ex, page, maxRetries, attempt, cancellationToken); + if (!await HandleRetry(ex, page, maxRetries, attempt, cancellationToken)) + throw; } catch (SocketException ex) { - await HandleRetry(ex, page, maxRetries, attempt, cancellationToken); + if (!await HandleRetry(ex, page, maxRetries, attempt, cancellationToken)) + throw; } } @@ -424,24 +428,38 @@ private async Task> FetchIocFromMispAsync(CancellationToken canc _dnsServer.WriteLog($"Finished paginated fetch. Freezing {iocSet.Count} IOCs for optimal read performance..."); return iocSet; } - private async Task HandleRetry(Exception ex, int page, int maxRetries, int attempt, CancellationToken cancellationToken) + + private async Task HandleRetry( + Exception ex, + int page, + int maxRetries, + int attempt, + CancellationToken cancellationToken) { - // These are likely transient network errors, so we should retry. - _dnsServer.WriteLog($"WARNING: A transient network error occurred on page {page}, attempt {attempt}/{maxRetries}. Error: {ex.Message}"); + _dnsServer.WriteLog( + $"WARNING: A transient network error occurred on page {page}, " + + $"attempt {attempt}/{maxRetries}. Error: {ex.Message}"); + if (attempt < maxRetries) { - TimeSpan delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)) + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); - _dnsServer.WriteLog($"Waiting for {delay.TotalSeconds:F1} seconds before retrying..."); + TimeSpan delay = + TimeSpan.FromSeconds(Math.Pow(2, attempt)) + + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); + + _dnsServer.WriteLog( + $"Waiting for {delay.TotalSeconds:F1} seconds before retrying..."); + await Task.Delay(delay, cancellationToken); + return true; // retry } - else - { - // All retries have failed for this page. - _dnsServer.WriteLog($"ERROR: Failed to fetch page {page} after {maxRetries} attempts. Aborting entire update cycle."); - throw ex; - } + + _dnsServer.WriteLog( + $"ERROR: Failed to fetch page {page} after {maxRetries} attempts."); + + return false; // abort } + private bool IsDomainBlocked(string domain, out string foundZone) { FrozenSet currentBlocklist = _domainBlocklist; From 83aa3f1ba6033cbd5ea33c40be917bbe2571c8cd Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 13:32:21 +0200 Subject: [PATCH 14/19] Fixed IOC trimming --- Apps/MispConnectorApp/App.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 6b66fe729..bcc1b69f4 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -495,8 +495,10 @@ private async Task LoadBlocklistFromCacheAsync() await foreach (var line in File.ReadLinesAsync(_domainCacheFilePath)) { - if (!string.IsNullOrWhiteSpace(line)) - set.Add(line); + var d = line.Trim(); + + if (d.Length > 0) + set.Add(d); } FrozenSet domains = set.ToFrozenSet(StringComparer.OrdinalIgnoreCase); From e5482866814610d3115904a4f6e590c14ac159e4 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 13:35:10 +0200 Subject: [PATCH 15/19] Reused Misp Server URI object --- Apps/MispConnectorApp/App.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index bcc1b69f4..43a823d71 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -24,7 +24,6 @@ You should have received a copy of the GNU General Public License using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; @@ -53,6 +52,7 @@ public sealed class App : IDnsApplication, IDnsRequestBlockingHandler HttpClient _httpClient; Uri _mispApiUrl; + Uri _mispServerUrl; DnsSOARecordData _soaRecord; TimeSpan _updateInterval; @@ -105,9 +105,9 @@ public async Task InitializeAsync(IDnsServer dnsServer, string config) _updateInterval = ParseUpdateInterval(_config.UpdateInterval); - Uri mispServerUrl = new Uri(_config.MispServerUrl); - _mispApiUrl = new Uri(mispServerUrl, "/attributes/restSearch"); - _httpClient = CreateHttpClient(mispServerUrl, _config.DisableTlsValidation); + _mispServerUrl = new Uri(_config.MispServerUrl); + _mispApiUrl = new Uri(_mispServerUrl, "/attributes/restSearch"); + _httpClient = CreateHttpClient(_mispServerUrl, _config.DisableTlsValidation); await LoadBlocklistFromCacheAsync(); _appShutdownCts = new CancellationTokenSource(); @@ -514,7 +514,7 @@ private async Task LoadBlocklistFromCacheAsync() private async Task UpdateIocsAsync(CancellationToken cancellationToken) { - if (!await CheckTcpPortAsync(new Uri(_config.MispServerUrl), cancellationToken)) + if (!await CheckTcpPortAsync(_mispServerUrl, cancellationToken)) { return; } From 579b68e2acafddecf76e368fe0821a29d9e4e848 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 13:37:03 +0200 Subject: [PATCH 16/19] Used a simpler wait --- Apps/MispConnectorApp/App.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index 43a823d71..c65dd0e0d 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -70,7 +70,7 @@ public void Dispose() { if (_updateLoopTask != null) { - _ = Task.WhenAny(_updateLoopTask, Task.Delay(TimeSpan.FromSeconds(2))).GetAwaiter().GetResult(); + _updateLoopTask?.WaitAsync(TimeSpan.FromSeconds(2)).Wait(); } } catch From cc5040e42cfc5eda21442fa89700dabed33d705a Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 13:37:51 +0200 Subject: [PATCH 17/19] Merged if conditions --- Apps/MispConnectorApp/App.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index c65dd0e0d..be2f73da7 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -405,12 +405,9 @@ private async Task> FetchIocFromMispAsync(CancellationToken canc { string ioc = attribute.Value?.Trim(); - if (!string.IsNullOrEmpty(ioc)) + if (!string.IsNullOrEmpty(ioc) && DnsClient.IsDomainNameValid(ioc)) { - if (DnsClient.IsDomainNameValid(ioc)) - { - iocSet.Add(ioc); - } + iocSet.Add(ioc); } } From 34d71ba097637dfea16f1d20dc17e1167c0e7a3f Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 13:42:55 +0200 Subject: [PATCH 18/19] Fixed blocking waiter --- Apps/MispConnectorApp/App.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index be2f73da7..fd3af0ca2 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -70,7 +70,13 @@ public void Dispose() { if (_updateLoopTask != null) { - _updateLoopTask?.WaitAsync(TimeSpan.FromSeconds(2)).Wait(); + try + { + _updateLoopTask?.WaitAsync(TimeSpan.FromSeconds(2)) + .GetAwaiter() + .GetResult(); + } + catch { } } } catch From f14714af81efdeed6ed6c3bdbbd6e2fd7931c684 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Thu, 5 Feb 2026 13:43:54 +0200 Subject: [PATCH 19/19] Simpliefied while loop --- Apps/MispConnectorApp/App.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs index fd3af0ca2..a368e0324 100644 --- a/Apps/MispConnectorApp/App.cs +++ b/Apps/MispConnectorApp/App.cs @@ -216,7 +216,7 @@ private async Task StartUpdateLoopAsync(CancellationToken cancellationToken) await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(5, 30)), cancellationToken); using (PeriodicTimer timer = new PeriodicTimer(_updateInterval)) { - while (!cancellationToken.IsCancellationRequested) + while (true) { try {