/*
* 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.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.Dns.Contracts;
using ProtonVPN.Dns.NameServers;
using ProtonVPN.Dns.Resolvers;
using ProtonVPN.Dns.Resolvers.System;
using ProtonVPN.Dns.Tests.Mocks;
namespace ProtonVPN.Dns.Tests.Resolvers;
[TestClass]
public class DnsOverUdpResolverTest
{
private const string HOST = "api.protonvpn.ch";
private const string NON_EXISTING_HOST = "g5f16gfds1gdsf5g16dsfg15fs5gfds651d61s651g6516gf1s6fdgfs.vhbverhu";
private static readonly TimeSpan DNS_RESOLVE_TIMEOUT = TimeSpan.FromSeconds(30);
private static readonly TimeSpan DEFAULT_DNS_TTL = TimeSpan.FromMinutes(20);
private MockOfLogger _logger;
private CancellationTokenSource _cancellationTokenSource;
private Stopwatch _stopwatch;
private IConfiguration _configuration;
private MockOfNameServersLoader _mockOfNameServersLoader;
private DnsOverUdpResolver _resolver;
private ISystemDnsResolver _mockOfSystemDnsResolver;
[TestInitialize]
public void TestInitialize()
{
_logger = new MockOfLogger();
_cancellationTokenSource = new CancellationTokenSource();
_stopwatch = new Stopwatch();
_configuration = Substitute.For();
_configuration.DnsResolveTimeout.Returns(DNS_RESOLVE_TIMEOUT);
_configuration.DefaultDnsTimeToLive.Returns(DEFAULT_DNS_TTL);
_mockOfSystemDnsResolver = Substitute.For();
}
[TestCleanup]
public void TestCleanup()
{
_logger = null;
_cancellationTokenSource = null;
_stopwatch = null;
_configuration = null;
_mockOfNameServersLoader = null;
_resolver = null;
_mockOfSystemDnsResolver = null;
}
[TestMethod]
public async Task TestResolveAsync()
{
InitializeResolverWithRealNameServersLoader();
DnsResponse response = await ExecuteAsync();
Assert.IsNotEmpty(response.IpAddresses);
Assert.IsTrue(response.TimeToLive > TimeSpan.Zero);
Assert.IsTrue(response.ExpirationDateTimeUtc > DateTime.UtcNow);
Assert.IsTrue(response.ResponseDateTimeUtc <= DateTime.UtcNow);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(5));
}
private void InitializeResolverWithRealNameServersLoader()
{
NameServersResolver nameServersResolver = new();
NameServersLoader nameServersLoader = new(nameServersResolver, _logger);
_resolver = new(nameServersLoader, _mockOfSystemDnsResolver, _configuration, _logger);
}
private async Task ExecuteAsync()
{
return await ExecuteWithStopwatchAsync(ResolveAsync);
}
private async Task ResolveAsync()
{
return await _resolver.ResolveAsync(HOST, _cancellationTokenSource.Token);
}
private async Task ExecuteWithStopwatchAsync(Func> task)
{
_stopwatch.Start();
DnsResponse response = await task();
_stopwatch.Stop();
return response;
}
[TestMethod]
public async Task TestResolveAsync_WithoutNameServers_AndSystemResolverWorks()
{
InitializeWithMockOfNameServersLoader();
List systemResult = new() { IPAddress.Parse("1.2.3.4"), IPAddress.Parse("5.6.7.8") };
AddSystemDnsResolverResult(HOST, systemResult);
DnsResponse response = await ExecuteAsync();
AssertValidSystemResponse(systemResult, response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(1));
}
private void AssertValidSystemResponse(List expectedSystemResult, DnsResponse response)
{
Assert.IsNotNull(response);
Assert.HasCount(expectedSystemResult.Count, response.IpAddresses);
foreach (IPAddress ipAddress in expectedSystemResult)
{
Assert.IsNotNull(response.IpAddresses.Single(ip => ip.ToString() == ipAddress.ToString()));
}
Assert.AreEqual(TimeSpan.FromSeconds(DnsOverUdpResolver.DEFAULT_DNS_TTL_IN_SECONDS), response.TimeToLive);
Assert.IsTrue(response.ExpirationDateTimeUtc > DateTime.UtcNow);
Assert.IsTrue(response.ResponseDateTimeUtc <= DateTime.UtcNow);
Assert.AreEqual(response.TimeToLive, response.ExpirationDateTimeUtc - response.ResponseDateTimeUtc);
}
private void InitializeWithMockOfNameServersLoader()
{
_mockOfNameServersLoader = new MockOfNameServersLoader();
_resolver = new DnsOverUdpResolver(_mockOfNameServersLoader, _mockOfSystemDnsResolver, _configuration, _logger);
}
[TestMethod]
public async Task TestResolveAsync_WithoutNameServers_AndSystemResolverFails()
{
InitializeWithMockOfNameServersLoader();
AddSystemDnsResolverResult(HOST, new List());
DnsResponse response = await ExecuteAsync();
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(1));
}
[TestMethod]
public async Task TestResolveAsync_WithNonWorkingNameServers_AndSystemResolverWorks()
{
InitializeWithMockOfNameServersLoader();
SetNonWorkingNameServers();
List systemResult = new() { IPAddress.Parse("8.8.8.8"), IPAddress.Parse("1.1.1.1") };
AddSystemDnsResolverResult(HOST, systemResult);
DnsResponse response = await ExecuteAsync();
AssertValidSystemResponse(systemResult, response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(20));
}
private void SetNonWorkingNameServers()
{
_mockOfNameServersLoader.Set(IPAddress.Loopback, IPAddress.Parse("192.168.153.153"));
}
[TestMethod]
public async Task TestResolveAsync_WithNonWorkingNameServers_AndSystemResolverFails()
{
InitializeWithMockOfNameServersLoader();
SetNonWorkingNameServers();
AddSystemDnsResolverResult(HOST, new List());
DnsResponse response = await ExecuteAsync();
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(20));
}
[TestMethod]
public async Task TestResolveAsync_WhenCancelled()
{
InitializeWithMockOfNameServersLoader();
SetNonWorkingNameServers();
DnsResponse response = await ExecuteWithStopwatchAsync(StartResolveAndCancelAsync);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed > TimeSpan.FromSeconds(2) && _stopwatch.Elapsed < TimeSpan.FromSeconds(5));
}
private async Task StartResolveAndCancelAsync()
{
Task task = Task.Run(() => _resolver.ResolveAsync(HOST, _cancellationTokenSource.Token));
Task.Delay(TimeSpan.FromSeconds(3)).ContinueWith(_ => _cancellationTokenSource.Cancel());
return await task;
}
[TestMethod]
public async Task TestResolveAsync_WithCancelledToken()
{
InitializeWithMockOfNameServersLoader();
SetNonWorkingNameServers();
_cancellationTokenSource.Cancel();
DnsResponse response = await ExecuteAsync();
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(1));
}
[TestMethod]
public async Task TestResolveAsync_WithNonExistentHost()
{
InitializeResolverWithRealNameServersLoader();
AddSystemDnsResolverResult(NON_EXISTING_HOST, []);
DnsResponse response = await ExecuteWithStopwatchAndCustomHostAsync(NON_EXISTING_HOST);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(20));
}
[TestMethod]
public async Task TestResolveAsync_WithNonPublicIpAddresses()
{
InitializeResolverWithRealNameServersLoader();
AddSystemDnsResolverResult(NON_EXISTING_HOST, [
IPAddress.Parse("127.9.9.9"),
IPAddress.Parse("10.2.0.1")
]);
DnsResponse response = await ExecuteWithStopwatchAndCustomHostAsync(NON_EXISTING_HOST);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(20));
}
private void AddSystemDnsResolverResult(string host, List ipAddresses)
{
_mockOfSystemDnsResolver.ResolveWithSystemAsync(host, Arg.Any())
.ReturnsForAnyArgs(ipAddresses);
}
private async Task ExecuteWithStopwatchAndCustomHostAsync(string host)
{
return await ExecuteWithStopwatchAsync(() => ResolveWithSpecificHostAsync(host));
}
private async Task ResolveWithSpecificHostAsync(string host)
{
return await _resolver.ResolveAsync(host, _cancellationTokenSource.Token);
}
[TestMethod]
public async Task TestResolveAsync_WithEmptyHost()
{
InitializeResolverWithRealNameServersLoader();
DnsResponse response = await ExecuteWithStopwatchAndCustomHostAsync(string.Empty);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(1));
}
[TestMethod]
public async Task TestResolveAsync_WithNullHost()
{
InitializeResolverWithRealNameServersLoader();
DnsResponse response = await ExecuteWithStopwatchAndCustomHostAsync(null);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(1));
}
}