/* * 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 System.Net; using System.Runtime.InteropServices; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.NetworkLogs; using ProtonVPN.OperatingSystems.Network.Contracts; using Vanara.PInvoke; using static Vanara.PInvoke.IpHlpApi; using static Vanara.PInvoke.Ws2_32; namespace ProtonVPN.OperatingSystems.Network; public class NetworkUtilities : INetworkUtilities { public const uint ERROR_SUCCESS = 0; private readonly ILogger _logger; public NetworkUtilities( ILogger logger) { _logger = logger; } public void EnableIPv6OnAllAdapters(string appName, string excludeId) { AssertSuccess(() => PInvoke.EnableIPv6OnAllAdapters(appName, excludeId)); } public void DisableIPv6OnAllAdapters(string appName, string excludeId) { AssertSuccess(() => PInvoke.DisableIPv6OnAllAdapters(appName, excludeId)); } public void EnableIPv6(string appName, string interfaceId) { AssertSuccess(() => PInvoke.EnableIPv6(appName, interfaceId)); } public IPAddress GetBestInterfaceIPv4Address(string excludedIfaceHwid) { byte[] bytes = new byte[4]; GCHandle pinnedBytes = GCHandle.Alloc(bytes, GCHandleType.Pinned); AssertSuccess(() => PInvoke.GetBestInterfaceIp(pinnedBytes.AddrOfPinnedObject(), excludedIfaceHwid)); pinnedBytes.Free(); return new IPAddress(bytes); } public void SetLowestTapMetric(uint index) { AssertSuccess(() => PInvoke.SetLowestTapMetric(index)); } public void RestoreDefaultTapMetric(uint index) { AssertSuccess(() => PInvoke.RestoreDefaultTapMetric(index)); } private void AssertSuccess(Func function) { uint status; try { status = function(); } catch (SEHException ex) { throw new NetworkUtilException(ex.ErrorCode, ex); } switch (status) { case ERROR_SUCCESS: return; default: throw new NetworkUtilException(status); } } public NetworkAddress? GetDefaultIpv6Gateway(INetworkInterface tunnelInterface, INetworkInterface[] networkInterfaces) { List interfacesWithGlobalUnicastAddresses = GetInterfaceIndexesWithGlobalUnicastAddress(tunnelInterface, networkInterfaces); if (interfacesWithGlobalUnicastAddresses.Count == 0) { _logger.Warn("No interface found with global unicast address."); return null; } List ipForwardRows = GetIpv6DefaultRoutes(interfacesWithGlobalUnicastAddresses); if (ipForwardRows.Count == 0) { _logger.Error("No IPv6 route found."); return null; } Dictionary interfaceMetrics = GetIpv6InterfaceMetrics(); byte[]? nextHop = GetNextHopWithBestEffectiveMetric(ipForwardRows, interfaceMetrics); return nextHop is not null && NetworkAddress.TryParse(new IPAddress(nextHop).ToString(), out NetworkAddress ipv6DefaultRoute) ? ipv6DefaultRoute : null; } private static byte[]? GetNextHopWithBestEffectiveMetric(List ipForwardRows, Dictionary interfaceMetrics) { byte[]? nextHop = null; uint bestEffectiveMetric = uint.MaxValue; foreach (MIB_IPFORWARD_ROW2 row in ipForwardRows) { if (!interfaceMetrics.TryGetValue(row.InterfaceIndex, out uint interfaceMetric)) { continue; } uint effectiveMetric = row.Metric + interfaceMetric; if (effectiveMetric < bestEffectiveMetric) { bestEffectiveMetric = effectiveMetric; nextHop = row.NextHop.Ipv6.sin6_addr.bytes; } } return nextHop; } private List GetInterfaceIndexesWithGlobalUnicastAddress(INetworkInterface tunnelInterface, INetworkInterface[] networkInterfaces) { return networkInterfaces .Where(i => !i.Equals(tunnelInterface)) .Where(i => i.GetUnicastAddresses().Any(a => a.IsGlobalUnicastAddress())) .Where(i => i.Index != 0) .Select(i => i.Index) .ToList(); } private List GetIpv6DefaultRoutes(List interfacesWithGlobalUnicastAddresses) { Win32Error result = GetIpForwardTable2(ADDRESS_FAMILY.AF_INET6, out MIB_IPFORWARD_TABLE2 interfaces); if (result.Failed) { _logger.Error("Failed to retrieve IP forward table.", result.GetException()); return []; } return interfaces?.Table?.Where(row => IsDefaultIpv6Route(row, interfacesWithGlobalUnicastAddresses)).ToList() ?? []; } private static bool IsDefaultIpv6Route(MIB_IPFORWARD_ROW2 row, List interfacesWithGlobalUnicastAddresses) { return row.DestinationPrefix.PrefixLength == 0 && new IPAddress(row.DestinationPrefix.Prefix.Ipv6.sin6_addr.bytes).Equals(IPAddress.IPv6None) && interfacesWithGlobalUnicastAddresses.Contains(row.InterfaceIndex); } private Dictionary GetIpv6InterfaceMetrics() { Win32Error result = GetIpInterfaceTable(ADDRESS_FAMILY.AF_INET6, out MIB_IPINTERFACE_TABLE interfaces); if (result.Failed || interfaces?.Table is null) { _logger.Error("Failed to retrieve IP interface table.", result.GetException()); return []; } Dictionary metrics = []; foreach (MIB_IPINTERFACE_ROW interfaceRow in interfaces.Table) { metrics.Add(interfaceRow.InterfaceIndex, interfaceRow.Metric); } return metrics; } }