/* * Copyright (c) 2025 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 ProtonVPN.Client.Logic.Connection.Contracts.Extensions; using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents; using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Features; using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations; using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations.Countries; using ProtonVPN.Client.Logic.Connection.Contracts.Preferences; using ProtonVPN.Client.Logic.Connection.Contracts.ServerListGenerators; using ProtonVPN.Client.Logic.Servers.Contracts; using ProtonVPN.Client.Logic.Servers.Contracts.Models; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; namespace ProtonVPN.Client.Logic.Connection.ServerListGenerators; public class SmartServerListGenerator : ServerListGeneratorBase, ISmartServerListGenerator { private const int MAX_GENERATED_INTENT_LOGICAL_SERVERS = 3; private const int MAX_GENERATED_BASE_INTENT_LOGICAL_SERVERS = 1; protected override int MaxPhysicalServersPerLogical => 1; protected override int MaxPhysicalServersInTotal => 64; public ServerListDiagnostic Diagnostic { get; private set; } = ServerListDiagnostic.Empty; public SmartServerListGenerator( ISettings settings, IServersLoader serversLoader, IExclusionChecker exclusionChecker, ILogger logger) : base(settings, serversLoader, exclusionChecker, logger) { } public ServerListResult Generate(IConnectionIntent connectionIntent, IList preferredProtocols) { Logger.Debug($"Generating smart servers list for intent: {connectionIntent}"); List servers = GenerateServerList(connectionIntent, preferredProtocols, applyExclusions: true); ServerListDiagnostic diagnostic = DetermineExclusionDiagnostic(servers.Count, () => SelectLogicalServers(connectionIntent, preferredProtocols, applyExclusions: false).Any()); Logger.Debug($"Generated smart servers list: {string.Join(", ", servers.Select(s => s.Name))}"); IReadOnlyList physicalServers = SelectDistinctPhysicalServers(servers, preferredProtocols).ToList(); return new ServerListResult(physicalServers, diagnostic); } private List GenerateServerList(IConnectionIntent connectionIntent, IList preferredProtocols, bool applyExclusions) { List availableServers = GetAvailableServers(connectionIntent, applyExclusions).ToList(); List servers = []; IEnumerable locationIntents = connectionIntent.Location.GetIntentHierarchy(); IEnumerable featureIntents = connectionIntent.Feature.GetIntentHierarchy(); // Phase 1: iterate over all (location, feature) combinations foreach (ILocationIntent locationIntent in locationIntents) { int numberOfServersToPick = GetNumberOfServersToPick(servers.Count); foreach (IFeatureIntent featureIntent in featureIntents) { ConnectionIntent intent = new(locationIntent, featureIntent); List matchingServers = PickThenRemoveServers(availableServers, intent, preferredProtocols, numberOfServersToPick); servers.AddRange(matchingServers); } } // Phase 2: location-only fallback foreach (ILocationIntent locationIntent in locationIntents) { int numberOfServersToPick = GetNumberOfServersToPick(servers.Count); ConnectionIntent intent = new(locationIntent); List matchingServers = PickThenRemoveServers(availableServers, intent, preferredProtocols, numberOfServersToPick); servers.AddRange(matchingServers); } return servers; } /// /// Pick # servers from the available servers list that match the connection intent. /// Then remove all the servers that match the connection intent from the available servers list. /// /// /// /// /// private List PickThenRemoveServers( List availableServers, IConnectionIntent connectionIntent, IList preferredProtocols, int numberOfServersToPick) { // Get all the servers that match the current connection intent and pick the first one(s). List supportedServers = SelectLogicalServers(availableServers, connectionIntent, preferredProtocols).ToList(); List pickedServers = [.. supportedServers.Take(numberOfServersToPick)]; if (connectionIntent.Location is MultiCountryLocationIntent countryintent && countryintent.IsSelectionEmpty) { // Get all the servers that are located in the same countries as the ones picked. List pickedCountries = pickedServers.Select(s => s.ExitCountry).Distinct().ToList(); List serversWithSameLocation = supportedServers.Where(s => pickedCountries.Contains(s.ExitCountry, StringComparer.OrdinalIgnoreCase)).ToList(); // Remove the servers from the same countries as the ones picked from the available servers. foreach (Server server in serversWithSameLocation) { availableServers.Remove(server); } } else { // Remove the servers that were supported at this step from the available servers. foreach (Server server in supportedServers) { availableServers.Remove(server); } } // Return the first # servers from the supported servers list. return pickedServers; } /// /// Get how many logical servers should be picked based on the current number of servers already picked. /// private int GetNumberOfServersToPick(int currentServersCount) { return currentServersCount > 0 ? MAX_GENERATED_BASE_INTENT_LOGICAL_SERVERS : MAX_GENERATED_INTENT_LOGICAL_SERVERS; } }