/* * 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 System.Threading; using System.Threading.Tasks; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Common.Core.Networking; using ProtonVPN.IPv6.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.IPv6Logs; using ProtonVPN.OperatingSystems.Network.Contracts; using ProtonVPN.OperatingSystems.Processes.Contracts; using ProtonVPN.Service.Firewall; using ProtonVPN.Service.Settings; namespace ProtonVPN.Service.Vpn; internal class IPv6Manager : IIPv6Manager { private const int MAX_FAKE_IPV6_ADDRESSES = 50; private readonly IIpv6 _ipv6; private readonly ILogger _logger; private readonly IFirewall _firewall; private readonly IServiceSettings _serviceSettings; private readonly IFakeIPv6AddressGenerator _fakeIPv6AddressGenerator; private readonly ICommandLineCaller _commandLineCaller; private readonly INetworkInterfaceProvider _networkInterfaceProvider; private readonly ISystemNetworkInterfaces _networkInterfaces; private readonly SemaphoreSlim _networkSemaphore = new(1, 1); private readonly SemaphoreSlim _ipv6Semaphore = new(1, 1); private HashSet _lastGlobalUnicastAddresses = []; private List _lastFakeIpv6Addresses = []; private volatile bool _wereInterfacesAdded; private VpnProtocol? _vpnProtocol; private VpnStatus? _vpnStatus; private OpenVpnAdapter? _openVpnAdapter; public IPv6Manager( IIpv6 ipv6, ILogger logger, IFirewall firewall, IServiceSettings serviceSettings, IFakeIPv6AddressGenerator fakeIPv6AddressGenerator, ICommandLineCaller commandLineCaller, INetworkInterfaceProvider networkInterfaceProvider, ISystemNetworkInterfaces networkInterfaces, IObservableNetworkInterfaces observableNetworkInterfaces) { _ipv6 = ipv6; _logger = logger; _firewall = firewall; _serviceSettings = serviceSettings; _fakeIPv6AddressGenerator = fakeIPv6AddressGenerator; _commandLineCaller = commandLineCaller; _networkInterfaceProvider = networkInterfaceProvider; _networkInterfaces = networkInterfaces; observableNetworkInterfaces.NetworkInterfacesAdded += OnNetworkInterfacesAddedAsync; networkInterfaces.NetworkAddressChanged += OnNetworkAddressChangedAsync; } public async Task HandleIPv6OnConnectAsync(VpnProtocol vpnProtocol, OpenVpnAdapter openVpnAdapter) { _vpnProtocol = vpnProtocol; _openVpnAdapter = openVpnAdapter; if (_serviceSettings.IsIpv6Enabled) { await HandleChaosAlgorithmAsync(vpnProtocol); } else { await DisableIpv6Async(vpnProtocol); } } public async Task OnVpnStatusChangedAsync(VpnStatus vpnStatus) { _vpnStatus = vpnStatus; if (_vpnProtocol is not null) { await HandleIPv6InterfacesAsync(vpnStatus, _vpnProtocol.Value); } if (_serviceSettings.IsIpv6Enabled) { HandleIPv6ChaosAsync(vpnStatus).FireAndForget(); } } private async Task HandleIPv6InterfacesAsync(VpnStatus vpnStatus, VpnProtocol vpnProtocol) { if (vpnStatus == VpnStatus.Disconnected) { _wereInterfacesAdded = false; if ((!_firewall.LeakProtectionEnabled || _serviceSettings.IsIpv6Enabled) && !_ipv6.IsEnabled) { await RunIpv6ActionAsync(() => _ipv6.EnableAsync(vpnProtocol)); } } } private async Task HandleIPv6ChaosAsync(VpnStatus vpnStatus) { switch (vpnStatus) { case VpnStatus.Connected when _lastFakeIpv6Addresses.Count > 0: INetworkInterface? tunnelInterface = GetTunnelInterface(); if (tunnelInterface is not null) { await AddInterfaceIpv6AddressesAsync(_lastFakeIpv6Addresses, tunnelInterface.Index); } break; case VpnStatus.Disconnected: _lastFakeIpv6Addresses.Clear(); break; } } private async Task RunIpv6ActionAsync(Func action) { await _ipv6Semaphore.WaitAsync().ConfigureAwait(false); try { await action().ConfigureAwait(false); } finally { _ipv6Semaphore.Release(); } } private async Task HandleChaosAlgorithmAsync(VpnProtocol vpnProtocol) { if (!_ipv6.IsEnabled) { await RunIpv6ActionAsync(() => _ipv6.EnableAsync(vpnProtocol)); } if (!_serviceSettings.Ipv6LeakProtection) { _lastFakeIpv6Addresses.Clear(); return; } HashSet globalUnicastAddresses = GetGlobalUnicastAddresses(); if (globalUnicastAddresses.Count == 0) { _lastFakeIpv6Addresses.Clear(); return; } LogGuaAddresses(globalUnicastAddresses); _lastGlobalUnicastAddresses = globalUnicastAddresses; List fakeIpv6Addresses = await _fakeIPv6AddressGenerator.GenerateAddressesAsync( _serviceSettings.Ipv6Fragments, globalUnicastAddresses.Select(a => a.ToString()).ToList(), MAX_FAKE_IPV6_ADDRESSES); if (fakeIpv6Addresses.Count == 0) { return; } _lastFakeIpv6Addresses = fakeIpv6Addresses; } private void LogGuaAddresses(HashSet globalUnicastAddresses) { _logger.Debug($"GUA addresses detected: {string.Join(", ", globalUnicastAddresses)}"); } private HashSet GetGlobalUnicastAddresses() { INetworkInterface? tunnelInterface = GetTunnelInterface(); if (tunnelInterface is null) { return []; } return _networkInterfaces .GetInterfaces() .Where(i => !i.Equals(tunnelInterface)) .SelectMany(i => i.GetUnicastAddresses()) .Where(a => a.IsGlobalUnicastAddress()) .ToHashSet(); } private INetworkInterface? GetTunnelInterface() { if (_vpnProtocol is null || _openVpnAdapter is null) { _logger.Error("Failed to get tunnel interface due to missing VPN protocol or OpenVPN adapter."); return null; } return _networkInterfaceProvider.GetByVpnProtocol(_vpnProtocol.Value, _openVpnAdapter.Value); } private async Task DisableIpv6Async(VpnProtocol vpnProtocol) { await _ipv6.EnableOnVPNInterfaceAsync(vpnProtocol); if (_ipv6.IsEnabled && _serviceSettings.Ipv6LeakProtection) { await RunIpv6ActionAsync(() => _ipv6.DisableAsync(vpnProtocol)); } else if (!_ipv6.IsEnabled && !_serviceSettings.Ipv6LeakProtection) { await RunIpv6ActionAsync(() => _ipv6.EnableAsync(vpnProtocol)); } } private async void OnNetworkInterfacesAddedAsync(object? sender, EventArgs e) { if (_wereInterfacesAdded || _vpnProtocol is null || _vpnStatus != VpnStatus.Connected) { return; } _wereInterfacesAdded = true; if (!_ipv6.IsEnabled) { await RunIpv6ActionAsync(() => _ipv6.DisableAsync(_vpnProtocol.Value)); } } private async void OnNetworkAddressChangedAsync(object? sender, EventArgs e) { await _networkSemaphore.WaitAsync(); try { if (_vpnStatus != VpnStatus.Connected) { return; } HashSet globalUnicastAddresses = GetGlobalUnicastAddresses(); if (_lastGlobalUnicastAddresses.SetEquals(globalUnicastAddresses)) { return; } if (globalUnicastAddresses.Count > 0) { LogGuaAddresses(globalUnicastAddresses); } INetworkInterface? tunnelInterface = GetTunnelInterface(); if (tunnelInterface is null) { return; } if (globalUnicastAddresses.Count == 0 && _lastGlobalUnicastAddresses.Count > 0) { _logger.Info("No GUA addresses detected after network addresses changed. Clearing previuos fake IPv6 addresses."); _lastGlobalUnicastAddresses.Clear(); await DeleteInterfaceIpv6AddressesAsync(_lastFakeIpv6Addresses, tunnelInterface.Index); _lastFakeIpv6Addresses.Clear(); return; } _lastGlobalUnicastAddresses = globalUnicastAddresses; List ipv6AddressesToRemove = _lastFakeIpv6Addresses.ToList(); await ApplyFakeIpv6AddressesAsync(tunnelInterface.Index); await DeleteInterfaceIpv6AddressesAsync(ipv6AddressesToRemove, tunnelInterface.Index); } finally { _networkSemaphore.Release(); } } private async Task ApplyFakeIpv6AddressesAsync(uint tunnelInterfaceIndex) { List fakeIpv6Addresses = await _fakeIPv6AddressGenerator.GenerateAddressesAsync( _serviceSettings.Ipv6Fragments, _lastGlobalUnicastAddresses.Select(a => a.ToString()).ToList(), MAX_FAKE_IPV6_ADDRESSES); if (fakeIpv6Addresses.Count > 0) { await AddInterfaceIpv6AddressesAsync(fakeIpv6Addresses, tunnelInterfaceIndex); _lastFakeIpv6Addresses = fakeIpv6Addresses; } } private async Task AddInterfaceIpv6AddressesAsync(List addresses, uint interfaceIndex) { _logger.Info($"Adding {addresses.Count} fake IPv6 addresses to interface with index {interfaceIndex}."); List commands = addresses .ToList() .Select(address => $"netsh interface ipv6 add address {interfaceIndex} {address} skipassource=true") .ToList(); await _commandLineCaller.ExecuteMultipleAsync(commands); } private async Task DeleteInterfaceIpv6AddressesAsync(List addresses, uint interfaceIndex) { if (addresses.Count == 0) { return; } _logger.Info($"Deleting {addresses.Count} fake IPv6 addresses from interface with index {interfaceIndex}."); List commands = addresses .ToList() .Select(address => $"netsh interface ipv6 delete address {interfaceIndex} {address}") .ToList(); await _commandLineCaller.ExecuteMultipleAsync(commands); } }