/* * Copyright (c) 2026 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.Linq; using System.Diagnostics; using System.Threading.Tasks; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using FlaUI.Core.Tools; using NUnit.Framework; namespace ProtonVPN.UI.Tests.TestsHelper; public class DnsHelper { private const string DNS_LEAK_TEST_URL = "https://bash.ws/"; private static readonly HttpClient _httpClient = new(); private static List WireGuardDnsAddress => GetDnsAddresses("ProtonVPN"); private static List ProTunDnsAddress => GetDnsAddresses(TestConstants.IsProTunVersion ? "ProTUN" : "ProtonVPN"); private static List OpenVpnDnsAddress => GetDnsAddresses("ProtonVPN TUN"); [DllImport("dnsapi.dll", EntryPoint = "DnsFlushResolverCache")] public static extern uint DnsFlushResolverCache(); public static List GetDnsAddresses(string adapterName) { RetryResult> retry = Retry.WhileEmpty( () => { return GetDnsAddressesForAdapterByName(adapterName); }, TestConstants.FiveSecondsTimeout, TestConstants.RetryInterval); return retry.Result ?? []; } public static void FlushDns() { DnsFlushResolverCache(); } public static void IsCustomDnsAddressSet(string dnsAddress, int order = 0) { RetryResult retry = Retry.WhileFalse( () => { return ContainsDnsAddress(dnsAddress, order); }, TestConstants.FiveSecondsTimeout, TestConstants.RetryInterval); if (!retry.Success) { throw new Exception(DnsAdressErrorMessage(dnsAddress)); } } public static void IsCustomDnsAddressNotSet(string dnsAddress) { RetryResult retry = Retry.WhileTrue( () => { return ContainsDnsAddressAnywhere(dnsAddress); }, TestConstants.FiveSecondsTimeout, TestConstants.RetryInterval); if (!retry.Success) { throw new Exception(DnsAdressErrorMessage(dnsAddress)); } } private static string DnsAdressErrorMessage(string expectedDnsAddress) { return $"WireGuard dns address: {WireGuardDnsAddress.FirstOrDefault()}." + $" OpenVPN dns address: {OpenVpnDnsAddress.FirstOrDefault()}." + $" ProTUN dns address: {ProTunDnsAddress.FirstOrDefault()}." + $" Expected dns value: {expectedDnsAddress}"; } private static bool ContainsDnsAddressAnywhere(string expectedDnsAddress) { return WireGuardDnsAddress.Contains(expectedDnsAddress) || OpenVpnDnsAddress.Contains(expectedDnsAddress) || ProTunDnsAddress.Contains(expectedDnsAddress); } private static bool ContainsDnsAddress(string expectedDnsAddress, int order) { return WireGuardDnsAddress.ElementAtOrDefault(order) == expectedDnsAddress || OpenVpnDnsAddress.ElementAtOrDefault(order) == expectedDnsAddress || ProTunDnsAddress.ElementAtOrDefault(order) == expectedDnsAddress; } public static void VerifyDnsIsNotLeaking(List dnsListNotConnected) { List currentDnsList = GetDnsServers(); bool isLeaking = AnalyzeIsLeaking(currentDnsList, dnsListNotConnected); Assert.That(isLeaking, Is.False, "DNS Requests are being leaked while connected to VPN server."); } public static List GetDnsServers() { RetryResult> retry = Retry.WhileEmpty( () => { return GetDnsServersAsync().Result; }, TestConstants.OneMinuteTimeout, TestConstants.RetryInterval, ignoreException: true); return retry.Result ?? throw new HttpRequestException("Failed to get DNS servers."); } private static async Task> GetDnsServersAsync() { string leakId = await GetTestIdAsync(); for (int i = 1; i <= 10; i++) { PingDomain($"{i}.{leakId}.bash.ws"); } string dnsTestResults = await FetchTestResultsAsync(leakId); List dnsServers = dnsTestResults .Split('\n') .SkipLast(2) .Select(line => line.Split('|')[3]) .ToList(); return dnsServers; } private static async Task GetTestIdAsync() { return await _httpClient.GetStringAsync($"{DNS_LEAK_TEST_URL}id"); } private static void PingDomain(string domain) { using (Process process = new()) { process.StartInfo.FileName = "ping"; process.StartInfo.Arguments = $"-n 1 {domain}"; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; process.Start(); process.WaitForExit(TestConstants.TenSecondsTimeout); } } private static async Task FetchTestResultsAsync(string leakId) { string url = $"{DNS_LEAK_TEST_URL}dnsleak/test/{leakId}?txt"; return await _httpClient.GetStringAsync(url); } // It checks if NOT CONNECTED DNS server list, does not contain same DNS server names when CONNECTRED. private static bool AnalyzeIsLeaking(List currentDnsList, List dnsListToCompare) { foreach (string dns in currentDnsList) { foreach (string test in dnsListToCompare) { if (currentDnsList.Contains(test)) { return true; } } } return false; } private static List GetDnsAddressesForAdapterByName(string adapterName) { List dnsAddresses = []; NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces(); foreach (NetworkInterface adapter in adapters) { IPInterfaceProperties adapterProperties = adapter.GetIPProperties(); IPAddressCollection dnsServers = adapterProperties.DnsAddresses; if (adapter.Name.Equals(adapterName)) { foreach (IPAddress dns in dnsServers) { dnsAddresses.Add(dns.ToString()); } } } return dnsAddresses; } }