/* * 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.Servers.Contracts.Models; using ProtonVPN.Client.Logic.Servers.Loads.Native; using ProtonVPN.Common.Core.Geographical; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; namespace ProtonVPN.Client.Logic.Servers.Loads; public class ServerLoadsCalculator : IServerLoadsCalculator { private readonly ILogger _logger; public ServerLoadsCalculator(ILogger logger) { _logger = logger; } /// /// Computes and updates server loads using native binary status processing. /// /// List of servers to compute loads for /// Binary status file data from API /// Optional user device location for geo-optimization public bool UpdateServerLoads(IReadOnlyList servers, byte[] statusFile, DeviceLocation? deviceLocation) { if (servers is null || servers.Count == 0) { return false; } FfiLoad[]? loads = ComputeNative(servers, statusFile, deviceLocation); return loads is not null && ApplyServerMetrics(servers, loads); } private unsafe FfiLoad[]? ComputeNative(IReadOnlyList servers, byte[] statusFile, DeviceLocation? deviceLocation) { ReadOnlySpan logicals = MapServersToLogicals(servers).AsSpan(); if (logicals.Length < servers.Count) { _logger.Error($"Failed to map all servers to logicals. Expected {servers.Count}, but only {logicals.Length} were mapped."); return null; } ReadOnlySpan statusFileSpan = statusFile.AsSpan(); FfiLocation location = new() { Latitude = (float)(deviceLocation?.Latitude ?? 0), Longitude = (float)(deviceLocation?.Longitude ?? 0), }; byte[] countryBytes = new byte[2]; fixed (FfiLogical* logicalsPtr = logicals) fixed (byte* statusFilePtr = statusFileSpan) fixed (byte* countryBytesPtr = countryBytes) { SetCountryCode(countryBytesPtr, deviceLocation?.CountryCode); FfiLoad[] loads = new FfiLoad[logicals.Length]; int result = NativeMethods.ComputeLoads( logicals, (nuint)logicals.Length, statusFileSpan, (nuint)statusFileSpan.Length, in location, !string.IsNullOrEmpty(deviceLocation?.CountryCode) ? countryBytesPtr : null, loads, out ErrorStringHandle error); using (error) { if (result == 0) { return loads; } else { _logger.Error($"Failed to calculate server loads: {error}"); return null; } } } } private unsafe static FfiLogical[] MapServersToLogicals(IReadOnlyList servers) { FfiLogical[] logicals = new FfiLogical[servers.Count]; for (int i = 0; i < servers.Count; i++) { Server server = servers[i]; // There is a theoretical possibiltiy that servers list is from the legacy logicals endpoint // which didn't provide StatusReference, EntryLocation and ExitLocation fields, // so we add this guard to not calculate loads in such cases. This guard should be removed // after removing IsBinaryServerStatusEnabled feature flag. if (server.StatusReference is null || server.EntryLocation is null || server.ExitLocation is null) { continue; } logicals[i] = new FfiLogical { StatusReference = new FfiStatusReference { Index = server.StatusReference.Index, Penalty = server.StatusReference.Penalty, Cost = server.StatusReference.Cost }, EntryLocation = new FfiLocation { Latitude = (float)server.EntryLocation.Latitude, Longitude = (float)server.EntryLocation.Longitude, }, ExitLocation = new FfiLocation { Latitude = (float)server.ExitLocation.Latitude, Longitude = (float)server.ExitLocation.Longitude, }, Features = (uint)server.Features }; fixed (byte* countryPtr = logicals[i].ExitCountry) { SetCountryCode(countryPtr, server.ExitCountry); } } return logicals; } private static unsafe void SetCountryCode(byte* countryPtr, string? countryCode) { if (string.IsNullOrEmpty(countryCode)) { countryPtr[0] = 0; countryPtr[1] = 0; return; } string normalizedCode = countryCode.ToUpperInvariant(); countryPtr[0] = normalizedCode.Length > 0 ? (byte)normalizedCode[0] : (byte)0; countryPtr[1] = normalizedCode.Length > 1 ? (byte)normalizedCode[1] : (byte)0; } private bool ApplyServerMetrics(IReadOnlyList servers, FfiLoad[] loads) { if (servers.Count != loads.Length) { _logger.Error($"Server count ({servers.Count}) does not match load count ({loads.Length}). " + $"Skipping server load update to maintain data integrity."); return false; } for (int i = 0; i < servers.Count; i++) { Server server = servers[i]; FfiLoad load = loads[i]; server.Load = load.Load; server.Score = (float)load.Score; server.Status = (sbyte)(load.IsEnabled ? 1 : 0); server.IsVisible = load.IsVisible; server.IsAutoconnectable = load.IsAutoconnectable; // If the logical server only has one physical server, then the status of the logical and physical server are tied // If the status for the logical is down, it means that all physical servers for this logical are down // If the status for the logical is up, it means that at least one physical server is up, but we can't know which one(s) // -> in that case, we need to wait the update servers call to update the status properly if (server.Status == 0 || server.Servers.Count <= 1) { foreach (PhysicalServer physicalServer in server.Servers) { physicalServer.Status = server.Status; } } } return true; } }