/* * 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.Net; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Common.Core.Networking.Extensions; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.RoutingTableLogs; using ProtonVPN.OperatingSystems.Network.Contracts.Routing; using Vanara.PInvoke; using static Vanara.PInvoke.IpHlpApi; using static Vanara.PInvoke.Ws2_32; namespace ProtonVPN.OperatingSystems.Network.Routing; public class RoutingTableHelper : IRoutingTableHelper { private const uint DEFAULT_LOOPBACK_INTERFACE_INDEX = 1; private readonly ILogger _logger; public RoutingTableHelper(ILogger logger) { _logger = logger; } public void CreateRoute(RouteConfiguration route) { InitializeIpForwardEntry(out MIB_IPFORWARD_ROW2 row); row.DestinationPrefix = GetDestinationPrefix(route); row.NextHop = GetNextHop(route); row.Metric = route.Metric; row.InterfaceIndex = route.Gateway is null ? DEFAULT_LOOPBACK_INTERFACE_INDEX : route.InterfaceIndex; row.ValidLifetime = uint.MaxValue; row.PreferredLifetime = uint.MaxValue; row.Loopback = route.Gateway is null; CreateIpForwardEntry2(ref row); } public uint? GetLoopbackInterfaceIndex() { Win32Error result = GetIfTable2(out MIB_IF_TABLE2 table); if (result.Succeeded) { MIB_IF_ROW2? interfaceRow = table.Table?.FirstOrDefault(row => row.Type == IFTYPE.IF_TYPE_SOFTWARE_LOOPBACK); return interfaceRow?.InterfaceIndex; } return null; } private IP_ADDRESS_PREFIX GetDestinationPrefix(RouteConfiguration route) { return new() { Prefix = CreateSockAddrInet(route.Destination), PrefixLength = GetDefaultPrefixLength(route.Destination), }; } private SOCKADDR_INET GetNextHop(RouteConfiguration route) { return CreateSockAddrInet(route.Gateway ?? new NetworkAddress(route.IsIpv6 ? IPAddress.IPv6None : IPAddress.None)); } private SOCKADDR_INET CreateSockAddrInet(NetworkAddress address) { SOCKADDR_INET sockAddr = new() { si_family = address.GetFamily(), }; if (address.IsIpV6) { sockAddr.Ipv6 = new SOCKADDR_IN6 { sin6_family = ADDRESS_FAMILY.AF_INET6, sin6_addr = new IN6_ADDR(address.Ip.GetAddressBytes()), }; } else { sockAddr.Ipv4 = new SOCKADDR_IN { sin_family = ADDRESS_FAMILY.AF_INET, sin_addr = new IN_ADDR(address.Ip.GetAddressBytes()), }; } return sockAddr; } private byte GetDefaultPrefixLength(NetworkAddress address) { if (address.Subnet.HasValue) { return (byte)address.Subnet.Value; } if (address.IsIpV6) { return 128; } return address.Ip.Equals(IPAddress.Any) ? (byte)0 : (byte)32; } public void DeleteRoute(RouteConfiguration route) { MIB_IPFORWARD_ROW2 routeToDelete = new() { DestinationPrefix = GetDestinationPrefix(route), NextHop = GetNextHop(route), InterfaceIndex = route.InterfaceIndex, }; DeleteIpForwardEntry2(ref routeToDelete); } public bool DeleteRoute(string destinationIpAddress, bool isIpv6) { IPAddress ipAddress = IPAddress.Parse(destinationIpAddress); ADDRESS_FAMILY family = isIpv6 ? ADDRESS_FAMILY.AF_INET6 : ADDRESS_FAMILY.AF_INET; Win32Error result = GetIpForwardTable2(family, out MIB_IPFORWARD_TABLE2 table); if (result.Failed) { _logger.Error($"Failed to get IP forward table when deleting route with destination {destinationIpAddress}", result.GetException()); return false; } for (int i = 0; i < table.Table?.Length; i++) { if (isIpv6 && table.Table[i].DestinationPrefix.Prefix.Ipv6.sin6_addr.Equals(new IN6_ADDR(ipAddress.GetAddressBytes())) || !isIpv6 && table.Table[i].DestinationPrefix.Prefix.Ipv4.sin_addr.Equals(new IN_ADDR(ipAddress.GetAddressBytes()))) { Win32Error deleteResult = DeleteIpForwardEntry2(ref table.Table[i]); if (deleteResult.Failed) { _logger.Error($"Failed to delete route with destination {destinationIpAddress}", deleteResult.GetException()); return false; } } } return true; } public uint? GetInterfaceMetric(uint interfaceIndex, bool isIpv6) { MIB_IPINTERFACE_ROW row = new() { Family = isIpv6 ? ADDRESS_FAMILY.AF_INET6 : ADDRESS_FAMILY.AF_INET, InterfaceIndex = interfaceIndex, }; Win32Error result = GetIpInterfaceEntry(ref row); return result.Succeeded ? row.Metric : null; } public bool RouteExists(RouteConfiguration route) { ADDRESS_FAMILY family = route.IsIpv6 ? ADDRESS_FAMILY.AF_INET6 : ADDRESS_FAMILY.AF_INET; Win32Error result = GetIpForwardTable2(family, out MIB_IPFORWARD_TABLE2 table); if (result.Failed || table.Table is null) { return false; } IP_ADDRESS_PREFIX expectedPrefix = GetDestinationPrefix(route); SOCKADDR_INET expectedNextHop = GetNextHop(route); foreach (MIB_IPFORWARD_ROW2 row in table.Table) { if (row.InterfaceIndex != route.InterfaceIndex || row.DestinationPrefix.PrefixLength != expectedPrefix.PrefixLength) { continue; } if (route.IsIpv6) { if (!row.DestinationPrefix.Prefix.Ipv6.sin6_addr.Equals(expectedPrefix.Prefix.Ipv6.sin6_addr) || !row.NextHop.Ipv6.sin6_addr.Equals(expectedNextHop.Ipv6.sin6_addr)) { continue; } } else { if (!row.DestinationPrefix.Prefix.Ipv4.sin_addr.Equals(expectedPrefix.Prefix.Ipv4.sin_addr) || !row.NextHop.Ipv4.sin_addr.Equals(expectedNextHop.Ipv4.sin_addr)) { continue; } } return true; } return false; } }