/* * 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.Collections.Generic; using System.Linq; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Common.Legacy.Vpn; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Files.Contracts; using ProtonVPN.OperatingSystems.Network.Contracts.Routing; using ProtonVPN.Serialization.Contracts; using ProtonVPN.Vpn.Common; namespace ProtonVPN.Vpn.WireGuard; public class WireGuardServerRouteManager : IWireGuardServerRouteManager { private const Serializers ROUTE_FILE_SERIALIZER = Serializers.Json; private readonly IStaticConfiguration _config; private readonly IRoutingTableHelper _routingTableHelper; private readonly IIpv4GatewayResolver _ipv4GatewayResolver; private readonly IFileReaderWriter _fileReaderWriter; private readonly object _routesFileLock = new(); private string WireGuardServerRoutesFilePath => _config.WireGuardServerRoutesFilePath; public WireGuardServerRouteManager( IStaticConfiguration config, IRoutingTableHelper routingTableHelper, IIpv4GatewayResolver ipv4GatewayResolver, IFileReaderWriter fileReaderWriter) { _config = config; _routingTableHelper = routingTableHelper; _ipv4GatewayResolver = ipv4GatewayResolver; _fileReaderWriter = fileReaderWriter; } public void CleanupPersistedRoutes() { lock (_routesFileLock) { List routes = ReadPersistedServerRoutes(); if (routes.Count == 0) { return; } List remainingRoutes = DeletePersistedRoutes(routes); WritePersistedServerRoutes(remainingRoutes); } } public void CreateServerRoute(VpnEndpoint endpoint, VpnConfig vpnConfig) { if (!TryBuildRoute(endpoint, vpnConfig, out RouteConfiguration? route, out NetworkAddress ipAddress) || route is null) { return; } if (_routingTableHelper.RouteExists(route)) { TrackServerRoute(ipAddress); return; } _routingTableHelper.DeleteRoute(ipAddress.Ip.ToString(), ipAddress.IsIpV6); _routingTableHelper.CreateRoute(route); TrackServerRoute(ipAddress); } public void DeleteServerRoutes(VpnEndpoint endpoint) { DeleteCurrentServerRoute(endpoint); lock (_routesFileLock) { List routes = ReadPersistedServerRoutes(); if (routes.Count > 0) { List remainingRoutes = DeletePersistedRoutes(routes); WritePersistedServerRoutes(remainingRoutes); } } } private bool TryBuildRoute(VpnEndpoint endpoint, VpnConfig vpnConfig, out RouteConfiguration? route, out NetworkAddress ipAddress) { if (endpoint is null || vpnConfig is null || !NetworkAddress.TryParse(endpoint.Server.Ip, out ipAddress) || !_ipv4GatewayResolver.TryGetBestIpv4Gateway( _config.GetWireGuardHardwareId(), out Ipv4GatewayInfo? ipv4GatewayInfo)) { route = null; ipAddress = NetworkAddress.None; return false; } route = new RouteConfiguration { Destination = ipAddress, Gateway = ipv4GatewayInfo.GatewayAddress, InterfaceIndex = ipv4GatewayInfo.Interface.Index, Metric = ipv4GatewayInfo.InterfaceMetric, IsIpv6 = ipAddress.IsIpV6, }; return true; } private void TrackServerRoute(NetworkAddress ipAddress) { lock (_routesFileLock) { List routes = ReadPersistedServerRoutes(); if (ContainsRoute(routes, ipAddress)) { return; } routes.Add(new PersistedServerRoute { DestinationIpAddress = ipAddress.Ip.ToString(), IsIpv6 = ipAddress.IsIpV6, }); WritePersistedServerRoutes(routes); } } private List ReadPersistedServerRoutes() { return _fileReaderWriter .ReadOrNew>(WireGuardServerRoutesFilePath, ROUTE_FILE_SERIALIZER) .Distinct() .ToList(); } private void WritePersistedServerRoutes(List routes) { _fileReaderWriter.Write(routes.Distinct(), WireGuardServerRoutesFilePath, ROUTE_FILE_SERIALIZER); } private List DeletePersistedRoutes(IEnumerable routes) { List remainingRoutes = []; foreach (PersistedServerRoute route in routes) { if (!TryDeletePersistedRoute(route)) { remainingRoutes.Add(route); } } return remainingRoutes; } private static bool ContainsRoute(IEnumerable routes, NetworkAddress ipAddress) { PersistedServerRoute target = new() { DestinationIpAddress = ipAddress.Ip.ToString(), IsIpv6 = ipAddress.IsIpV6, }; return routes.Contains(target); } private bool TryDeletePersistedRoute(PersistedServerRoute route) { if (string.IsNullOrWhiteSpace(route.DestinationIpAddress)) { return false; } return _routingTableHelper.DeleteRoute(route.DestinationIpAddress, route.IsIpv6); } private void DeleteCurrentServerRoute(VpnEndpoint endpoint) { if (endpoint is null || !NetworkAddress.TryParse(endpoint.Server.Ip, out NetworkAddress ipAddress)) { return; } bool isDeleted = _routingTableHelper.DeleteRoute(ipAddress.Ip.ToString(), ipAddress.IsIpV6); if (!isDeleted) { TrackServerRoute(ipAddress); } } }