11/*
22Technitium DNS Server
3- Copyright (C) 2024 Shreyas Zare ([email protected] ) 3+ Copyright (C) 2025 Shreyas Zare ([email protected] ) 44Copyright (C) 2025 Zafer Balkan ([email protected] ) 55
66This program is free software: you can redistribute it and/or modify
@@ -45,28 +45,41 @@ public sealed class App : IDnsApplication, IDnsRequestBlockingHandler
4545 {
4646 #region variables
4747
48- string _cacheFilePath ;
48+ string _domainCacheFilePath ;
4949 Config _config ;
5050 IDnsServer _dnsServer ;
51- private FrozenSet < string > _globalBlocklist = FrozenSet < string > . Empty ;
51+ FrozenSet < string > _domainBlocklist = FrozenSet < string > . Empty ;
5252 HttpClient _httpClient ;
5353
5454 Uri _mispApiUrl ;
5555
5656 DnsSOARecordData _soaRecord ;
5757 TimeSpan _updateInterval ;
58+ Task _updateLoopTask ;
5859
5960 CancellationTokenSource _appShutdownCts ;
60-
6161 #endregion variables
6262
6363 #region IDisposable
6464
6565 public void Dispose ( )
6666 {
6767 _appShutdownCts ? . Cancel ( ) ;
68- _appShutdownCts ? . Dispose ( ) ;
69- _httpClient ? . Dispose ( ) ;
68+ try
69+ {
70+ if ( _updateLoopTask != null )
71+ {
72+ _ = Task . WhenAny ( _updateLoopTask , Task . Delay ( TimeSpan . FromSeconds ( 2 ) ) ) . GetAwaiter ( ) . GetResult ( ) ;
73+ }
74+ }
75+ catch
76+ {
77+ }
78+ finally
79+ {
80+ _appShutdownCts ? . Dispose ( ) ;
81+ _httpClient ? . Dispose ( ) ;
82+ }
7083 }
7184
7285 #endregion IDisposable
@@ -78,17 +91,17 @@ public async Task InitializeAsync(IDnsServer dnsServer, string config)
7891 _dnsServer = dnsServer ;
7992 try
8093 {
81- string configDir = _dnsServer . ApplicationFolder ;
82- Directory . CreateDirectory ( configDir ) ;
83- _cacheFilePath = Path . Combine ( configDir , "misp_domain_cache.txt" ) ;
84-
8594 _soaRecord = new DnsSOARecordData ( _dnsServer . ServerDomain , _dnsServer . ResponsiblePerson . Address , 1 , 14400 , 3600 , 604800 , 60 ) ;
8695
8796 JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true } ;
8897 _config = JsonSerializer . Deserialize < Config > ( config , options ) ;
8998
9099 Validator . ValidateObject ( _config , new ValidationContext ( _config ) , validateAllProperties : true ) ;
91100
101+ string configDir = _dnsServer . ApplicationFolder ;
102+ Directory . CreateDirectory ( configDir ) ;
103+ _domainCacheFilePath = Path . Combine ( configDir , "misp_domain_cache.txt" ) ;
104+
92105 _updateInterval = ParseUpdateInterval ( _config . UpdateInterval ) ;
93106
94107 Uri mispServerUrl = new Uri ( _config . MispServerUrl ) ;
@@ -98,9 +111,16 @@ public async Task InitializeAsync(IDnsServer dnsServer, string config)
98111 await LoadBlocklistFromCacheAsync ( ) ;
99112 _appShutdownCts = new CancellationTokenSource ( ) ;
100113
101- // 2. Start the new, long-running update loop task.
102114 // We do not await this, as it's designed to run for the lifetime of the app.
103- _ = StartUpdateLoopAsync ( _appShutdownCts . Token ) ;
115+ _updateLoopTask = StartUpdateLoopAsync ( _appShutdownCts . Token ) ;
116+ Task _ = _updateLoopTask . ContinueWith ( t =>
117+ {
118+ if ( t . IsFaulted )
119+ {
120+ _dnsServer . WriteLog ( $ "FATAL: Update loop terminated unexpectedly: { t . Exception ? . GetBaseException ( ) . Message } ") ;
121+ _dnsServer . WriteLog ( t . Exception ) ;
122+ }
123+ } , TaskContinuationOptions . OnlyOnFaulted ) ;
104124 }
105125 catch ( Exception ex )
106126 {
@@ -117,24 +137,28 @@ public Task<bool> IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP)
117137 public Task < DnsDatagram > ProcessRequestAsync ( DnsDatagram request , IPEndPoint remoteEP )
118138 {
119139 if ( _config ? . EnableBlocking != true )
140+ {
120141 return Task . FromResult < DnsDatagram > ( null ) ;
142+ }
121143
122144 DnsQuestionRecord question = request . Question [ 0 ] ;
123- if ( ! IsDomainBlocked ( question . Name , out string blockedDomain ) )
145+ bool domainBlocked = IsDomainBlocked ( question . Name , out string blockedDomain ) ;
146+ if ( ! domainBlocked )
124147 {
125148 return Task . FromResult < DnsDatagram > ( null ) ;
126149 }
127150
128151 string blockingReport = $ "source=misp-connector;domain={ blockedDomain } ";
152+
129153 EDnsOption [ ] options = null ;
130154 if ( _config . AddExtendedDnsError && request . EDNS is not null )
131155 {
132- options = new EDnsOption [ ] { new EDnsOption ( EDnsOptionCode . EXTENDED_DNS_ERROR , new EDnsExtendedDnsErrorOptionData ( EDnsExtendedDnsErrorCode . Blocked , blockingReport ) ) } ;
156+ options = new EDnsOption [ ] { new EDnsOption ( EDnsOptionCode . EXTENDED_DNS_ERROR , new EDnsExtendedDnsErrorOptionData ( EDnsExtendedDnsErrorCode . Blocked , string . Empty ) ) } ;
133157 }
134158
135159 if ( _config . AllowTxtBlockingReport && question . Type == DnsResourceRecordType . TXT )
136160 {
137- DnsResourceRecord [ ] answer = new DnsResourceRecord [ ] { new DnsResourceRecord ( question . Name , DnsResourceRecordType . TXT , question . Class , 60 , new DnsTXTRecordData ( blockingReport ) ) } ;
161+ DnsResourceRecord [ ] answer = new DnsResourceRecord [ ] { new DnsResourceRecord ( question . Name , DnsResourceRecordType . TXT , question . Class , 60 , new DnsTXTRecordData ( string . Empty ) ) } ;
138162 return Task . FromResult ( new DnsDatagram (
139163 ID : request . Identifier ,
140164 isResponse : true ,
@@ -156,7 +180,7 @@ public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint rem
156180 ) ) ;
157181 }
158182
159- DnsResourceRecord [ ] authority = { new DnsResourceRecord ( blockedDomain , DnsResourceRecordType . SOA , question . Class , 60 , _soaRecord ) } ;
183+ DnsResourceRecord [ ] authority = { new DnsResourceRecord ( question . Name , DnsResourceRecordType . SOA , question . Class , 60 , _soaRecord ) } ;
160184 return Task . FromResult ( new DnsDatagram (
161185 ID : request . Identifier ,
162186 isResponse : true ,
@@ -184,26 +208,27 @@ public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint rem
184208 private async Task StartUpdateLoopAsync ( CancellationToken cancellationToken )
185209 {
186210 await Task . Delay ( TimeSpan . FromSeconds ( Random . Shared . Next ( 5 , 30 ) ) , cancellationToken ) ;
187- using var timer = new PeriodicTimer ( _updateInterval ) ;
188-
189- while ( ! cancellationToken . IsCancellationRequested )
211+ using ( PeriodicTimer timer = new PeriodicTimer ( _updateInterval ) )
190212 {
191- try
192- {
193- await UpdateIocsAsync ( cancellationToken ) ;
194- }
195- catch ( OperationCanceledException )
213+ while ( ! cancellationToken . IsCancellationRequested )
196214 {
197- _dnsServer . WriteLog ( "Update loop is shutting down gracefully." ) ;
198- break ;
199- }
200- catch ( Exception ex )
201- {
202- _dnsServer . WriteLog ( $ "FATAL: The MispConnector update task failed unexpectedly. Error: { ex . Message } ") ;
203- _dnsServer . WriteLog ( ex ) ;
204- }
215+ try
216+ {
217+ await UpdateIocsAsync ( cancellationToken ) ;
218+ }
219+ catch ( OperationCanceledException )
220+ {
221+ _dnsServer . WriteLog ( "Update loop is shutting down gracefully." ) ;
222+ break ;
223+ }
224+ catch ( Exception ex )
225+ {
226+ _dnsServer . WriteLog ( $ "FATAL: The MispConnector update task failed unexpectedly. Error: { ex . Message } ") ;
227+ _dnsServer . WriteLog ( ex ) ;
228+ }
205229
206- await timer . WaitForNextTickAsync ( cancellationToken ) ;
230+ await timer . WaitForNextTickAsync ( cancellationToken ) ;
231+ }
207232 }
208233 }
209234 private static TimeSpan ParseUpdateInterval ( string interval )
@@ -247,10 +272,11 @@ private async Task<bool> CheckTcpPortAsync(Uri serverUri, CancellationToken canc
247272
248273 try
249274 {
250- using var cts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , new CancellationTokenSource ( timeout ) . Token ) ;
251- using TcpClient client = new TcpClient ( ) ;
252-
253- await client . ConnectAsync ( host , port , cts . Token ) ;
275+ using ( CancellationTokenSource cts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , new CancellationTokenSource ( timeout ) . Token ) )
276+ using ( TcpClient client = new TcpClient ( ) )
277+ {
278+ await client . ConnectAsync ( host , port , cts . Token ) ;
279+ }
254280
255281 _dnsServer . WriteLog ( $ "Pre-flight TCP check successful for { host } :{ port } .") ;
256282 return true ;
@@ -284,19 +310,16 @@ private HttpClient CreateHttpClient(Uri serverUrl, bool disableTlsValidation)
284310
285311 if ( disableTlsValidation )
286312 {
287- handler . SslOptions . RemoteCertificateValidationCallback = ( sender , certificate , chain , sslPolicyErrors ) =>
288- {
289- return true ;
290- } ;
313+ handler . SslOptions . RemoteCertificateValidationCallback = ( sender , certificate , chain , sslPolicyErrors ) => true ;
291314 _dnsServer . WriteLog ( $ "WARNING: TLS certificate validation is DISABLED for MISP server: { serverUrl } ") ;
292315 }
293316
294317 return new HttpClient ( new HttpClientNetworkHandler ( handler , _dnsServer . PreferIPv6 ? HttpClientNetworkType . PreferIPv6 : HttpClientNetworkType . Default , _dnsServer ) ) ;
295318 }
296319
297- private async Task < FrozenSet < string > > FetchDomainsFromMispAsync ( CancellationToken cancellationToken )
320+ private async Task < HashSet < string > > FetchIocFromMispAsync ( CancellationToken cancellationToken )
298321 {
299- HashSet < string > domains = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
322+ HashSet < string > iocSet = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
300323 int page = 1 ;
301324 int limit = _config . PaginationLimit ;
302325 bool hasMorePages = true ;
@@ -330,18 +353,18 @@ private async Task<FrozenSet<string>> FetchDomainsFromMispAsync(CancellationToke
330353 request . Headers . Add ( "Accept" , "application/json" ) ;
331354
332355 _dnsServer . WriteLog ( $ "Fetching page { page } , attempt { attempt } /{ maxRetries } ...") ;
333- using HttpResponseMessage response = await _httpClient . SendAsync ( request ) ;
356+ using HttpResponseMessage response = await _httpClient . SendAsync ( request , cancellationToken ) ;
334357
335358 if ( ! response . IsSuccessStatusCode )
336359 {
337360 // This is a definitive failure from the server (e.g., 403, 500).
338361 // We should not retry this. Abort immediately.
339- string errorBody = await response . Content . ReadAsStringAsync ( ) ;
362+ string errorBody = await response . Content . ReadAsStringAsync ( cancellationToken ) ;
340363 throw new HttpRequestException ( $ "MISP API returned a non-success status code: { ( int ) response . StatusCode } . Body: { errorBody } ", null , response . StatusCode ) ;
341364 }
342365
343- await using Stream responseStream = await response . Content . ReadAsStreamAsync ( ) ;
344- mispResponse = await JsonSerializer . DeserializeAsync < MispResponse > ( responseStream ) ;
366+ await using ( Stream responseStream = await response . Content . ReadAsStreamAsync ( cancellationToken ) )
367+ mispResponse = await JsonSerializer . DeserializeAsync < MispResponse > ( responseStream , cancellationToken : cancellationToken ) ;
345368
346369 break ;
347370 }
@@ -373,10 +396,13 @@ private async Task<FrozenSet<string>> FetchDomainsFromMispAsync(CancellationToke
373396
374397 foreach ( MispAttribute attribute in attributes )
375398 {
376- string domain = attribute . Value ? . Trim ( ) . ToLowerInvariant ( ) ;
377- if ( ! string . IsNullOrEmpty ( domain ) && DnsClient . IsDomainNameValid ( domain ) )
399+ string ioc = attribute . Value ? . Trim ( ) . ToLowerInvariant ( ) ;
400+ if ( ! string . IsNullOrEmpty ( ioc ) )
378401 {
379- domains . Add ( domain ) ;
402+ if ( DnsClient . IsDomainNameValid ( ioc ) )
403+ {
404+ iocSet . Add ( ioc ) ;
405+ }
380406 }
381407 }
382408
@@ -391,13 +417,13 @@ private async Task<FrozenSet<string>> FetchDomainsFromMispAsync(CancellationToke
391417 }
392418 }
393419
394- _dnsServer . WriteLog ( $ "Finished paginated fetch. Freezing { domains . Count } domains for optimal read performance...") ;
395- return domains . ToFrozenSet ( StringComparer . OrdinalIgnoreCase ) ;
420+ _dnsServer . WriteLog ( $ "Finished paginated fetch. Freezing { iocSet . Count } IOCs for optimal read performance...") ;
421+ return iocSet ;
396422 }
397423
398424 private bool IsDomainBlocked ( string domain , out string foundZone )
399425 {
400- FrozenSet < string > currentBlocklist = _globalBlocklist ;
426+ FrozenSet < string > currentBlocklist = _domainBlocklist ;
401427
402428 ReadOnlySpan < char > currentSpan = domain . AsSpan ( ) ;
403429
@@ -426,24 +452,21 @@ private bool IsDomainBlocked(string domain, out string foundZone)
426452
427453 private async Task LoadBlocklistFromCacheAsync ( )
428454 {
429- if ( ! File . Exists ( _cacheFilePath ) ) return ;
430- try
455+ if ( File . Exists ( _domainCacheFilePath ) )
431456 {
432- FrozenSet < string > domains = ( await File . ReadAllLinesAsync ( _cacheFilePath ) ) . ToHashSet ( StringComparer . OrdinalIgnoreCase ) . ToFrozenSet ( StringComparer . OrdinalIgnoreCase ) ;
433- ReloadBlocklist ( domains ) ;
434- _dnsServer . WriteLog ( $ "MISP Connector: Loaded { domains . Count } domains from cache.") ;
435- }
436- catch ( IOException ex )
437- {
438- _dnsServer . WriteLog ( $ "ERROR: Failed to read cache file '{ _cacheFilePath } '. Error: { ex . Message } ") ;
457+ try
458+ {
459+ FrozenSet < string > domains = ( await File . ReadAllLinesAsync ( _domainCacheFilePath ) ) . ToHashSet ( StringComparer . OrdinalIgnoreCase ) . ToFrozenSet ( StringComparer . OrdinalIgnoreCase ) ;
460+ Interlocked . Exchange ( ref _domainBlocklist , domains ) ;
461+ _dnsServer . WriteLog ( $ "MISP Connector: Loaded { domains . Count } domains from cache.") ;
462+ }
463+ catch ( IOException ex )
464+ {
465+ _dnsServer . WriteLog ( $ "ERROR: Failed to read cache file '{ _domainCacheFilePath } '. Error: { ex . Message } ") ;
466+ }
439467 }
440468 }
441469
442- private void ReloadBlocklist ( FrozenSet < string > newBlocklist )
443- {
444- Interlocked . Exchange ( ref _globalBlocklist , newBlocklist ) ;
445- }
446-
447470 private async Task UpdateIocsAsync ( CancellationToken cancellationToken )
448471 {
449472 if ( ! await CheckTcpPortAsync ( new Uri ( _config . MispServerUrl ) , cancellationToken ) )
@@ -452,13 +475,15 @@ private async Task UpdateIocsAsync(CancellationToken cancellationToken)
452475 }
453476
454477 _dnsServer . WriteLog ( "MISP Connector: Starting IOC update..." ) ;
455- FrozenSet < string > domains = await FetchDomainsFromMispAsync ( cancellationToken ) ;
478+
479+ HashSet < string > tmpDomains = await FetchIocFromMispAsync ( cancellationToken ) ;
456480 cancellationToken . ThrowIfCancellationRequested ( ) ;
481+ FrozenSet < string > domains = tmpDomains . ToFrozenSet ( StringComparer . OrdinalIgnoreCase ) ;
457482
458- if ( ! domains . SetEquals ( _globalBlocklist ) )
483+ if ( ! domains . SetEquals ( _domainBlocklist ) )
459484 {
460- await WriteDomainsToCacheAsync ( domains , cancellationToken ) ;
461- ReloadBlocklist ( domains ) ;
485+ await WriteIocsToCacheAsync ( domains , cancellationToken ) ;
486+ Interlocked . Exchange ( ref _domainBlocklist , domains ) ;
462487 _dnsServer . WriteLog ( $ "MISP Connector: Successfully updated blocklist with { domains . Count } domains.") ;
463488 }
464489 else
@@ -467,11 +492,11 @@ private async Task UpdateIocsAsync(CancellationToken cancellationToken)
467492 }
468493 }
469494
470- private async Task WriteDomainsToCacheAsync ( FrozenSet < string > domains , CancellationToken cancellationToken )
495+ private async Task WriteIocsToCacheAsync ( FrozenSet < string > iocs , CancellationToken cancellationToken )
471496 {
472- string tempPath = _cacheFilePath + ".tmp" ;
473- await File . WriteAllLinesAsync ( tempPath , domains , cancellationToken ) ;
474- File . Move ( tempPath , _cacheFilePath , true ) ;
497+ string tempPath = _domainCacheFilePath + ".tmp" ;
498+ await File . WriteAllLinesAsync ( tempPath , iocs , cancellationToken ) ;
499+ File . Move ( tempPath , _domainCacheFilePath , true ) ;
475500 }
476501
477502 #endregion private
0 commit comments