/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Dns.Caching; using ProtonVPN.Dns.Contracts; using ProtonVPN.Dns.Contracts.Resolvers; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.DnsLogs; namespace ProtonVPN.Dns; public class AlternativeHostsManager : IAlternativeHostsManager { private readonly IDnsOverHttpsTxtRecordsResolver _dnsOverHttpsTxtRecordsResolver; private readonly ISettings _settings; private readonly ILogger _logger; private readonly IDnsCacheManager _dnsCacheManager; private readonly SemaphoreSlim _semaphore = new(1, 1); private readonly ConcurrentDictionary _failedRequestsCache = new(); private readonly TimeSpan _failedDnsRequestTimeout; private readonly TimeSpan _newCacheTimeToLiveOnResolveError; public AlternativeHostsManager(IDnsOverHttpsTxtRecordsResolver dnsOverHttpsTxtRecordsResolver, ISettings settings, IConfiguration config, ILogger logger, IDnsCacheManager dnsCacheManager) { _dnsOverHttpsTxtRecordsResolver = dnsOverHttpsTxtRecordsResolver; _settings = settings; _logger = logger; _dnsCacheManager = dnsCacheManager; _failedDnsRequestTimeout = config.FailedDnsRequestTimeout; _newCacheTimeToLiveOnResolveError = config.NewCacheTimeToLiveOnResolveError; } public async Task> GetAsync(string host, CancellationToken cancellationToken) { IList alternativeHosts = GetFreshAlternativeHostsFromCache(host); if (alternativeHosts.Count == 0) { alternativeHosts = await ResolveOrGetFromCacheAsync(host, cancellationToken); } return alternativeHosts; } private async Task> ResolveOrGetFromCacheAsync(string host, CancellationToken cancellationToken) { try { await _semaphore.WaitAsync(cancellationToken); } catch { _logger.Warn($"Alternative hosts resolve of host '{host}' was cancelled while waiting."); return new List(); } IList alternativeHosts; try { alternativeHosts = GetFreshAlternativeHostsFromCache(host); if (alternativeHosts.Count == 0) { if (_failedRequestsCache.TryGetValue(host, out DateTime timeoutEndDateUtc) && timeoutEndDateUtc > DateTime.UtcNow) { _logger.Debug($"Skipping alternative hosts resolve of host '{host}' because its under timeout."); alternativeHosts = GetAlternativeHostsFromCache(host); } else { _logger.Info($"No fresh alternative hosts for host '{host}' were found in the cache. " + $"Triggering a refresh."); alternativeHosts = await ResolveHostAsync(host, cancellationToken); if (alternativeHosts.Count == 0) { alternativeHosts = await GetAlternativeHostsFromCacheAndSetNewTtlAsync(host); } } } else { _logger.Debug($"Locked re-check for a fresh alternative " + $"hosts cache of host '{host}' was successful."); } } finally { _semaphore.Release(); _logger.Debug($"Released semaphore of alternative hosts DNS resolve of host '{host}'."); } return alternativeHosts; } private IList GetFreshAlternativeHostsFromCache(string host) { IList alternativeHosts = new List(); DateTime currentDateTimeUtc = DateTime.UtcNow; if (_settings.DnsCache.TryGetValueIfDictionaryIsNotNull(host, out DnsResponse dnsResponse) && dnsResponse.ExpirationDateTimeUtc > currentDateTimeUtc) { alternativeHosts = dnsResponse.AlternativeHosts; } return alternativeHosts; } private IList GetAlternativeHostsFromCache(string host) { IList alternativeHosts = new List(); if (_settings.DnsCache.TryGetValueIfDictionaryIsNotNull(host, out DnsResponse dnsResponse)) { alternativeHosts = dnsResponse.AlternativeHosts; } return alternativeHosts ?? new List(); } private async Task> GetAlternativeHostsFromCacheAndSetNewTtlAsync(string host) { IList alternativeHosts = GetAlternativeHostsFromCache(host); if (alternativeHosts.Any()) { DnsResponse newDnsResponse = await _dnsCacheManager.UpdateAsync(host, SetDatesAndTimeToLiveFactory); _logger.Info($"Returning cached alternative hosts for host '{host}'. " + $"New TTL of {newDnsResponse.TimeToLive} resulting in a " + $"new expiration date of {newDnsResponse.ExpirationDateTimeUtc}."); } else { DateTime timeoutEndDateUtc = DateTime.UtcNow + _failedDnsRequestTimeout; _failedRequestsCache.AddOrUpdate(host, timeoutEndDateUtc, (_, _) => timeoutEndDateUtc); _logger.Warn($"No cached alternative hosts exist for host '{host}'. " + $"Next resolve can only be made after '{timeoutEndDateUtc}'."); } return alternativeHosts; } private DnsResponse SetDatesAndTimeToLiveFactory(DnsResponse dnsResponse) { dnsResponse.SetDatesAndTimeToLive(_newCacheTimeToLiveOnResolveError); return dnsResponse; } private async Task> ResolveHostAsync(string host, CancellationToken cancellationToken) { try { _logger.Info($"Attempting a HTTPS DNS request for TXT records of host '{host}'."); DnsResponse dnsResponse = await _dnsOverHttpsTxtRecordsResolver.ResolveAsync(host, cancellationToken); if (dnsResponse != null && dnsResponse.AlternativeHosts.Any()) { _logger.Info($"The HTTPS DNS request for TXT records of host '{host}' was successful. " + "Saving to cache."); IList alternativeHosts = dnsResponse.AlternativeHosts; await _dnsCacheManager.AddOrReplaceAsync(host, dnsResponse); return alternativeHosts; } _logger.Error($"The HTTPS DNS request for TXT records of host '{host}' was unsuccessful."); } catch (Exception e) { _logger.Error($"An unexpected error as occurred when resolving " + $"HTTPS DNS for TXT records of host '{host}'.", e); } return new List(); } }