/*
* 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.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Timers;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Common.Legacy.Vpn;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ConnectLogs;
using ProtonVPN.Logging.Contracts.Events.DisconnectLogs;
using ProtonVPN.Logging.Contracts.Events.ProtocolLogs;
using ProtonVPN.OperatingSystems.Network.Contracts;
using ProtonVPN.OperatingSystems.Network.Contracts.Monitors;
using ProtonVPN.Vpn.Common;
using ProtonVPN.Vpn.Gateways;
using Timer = System.Timers.Timer;
namespace ProtonVPN.Vpn.WireGuard;
public class WireGuardConnection: IWireGuardConnection
{
private const int MIN_CONNECTION_TIMEOUT = 5000;
private const int MAX_CONNECTION_TIMEOUT = 30000;
private readonly ILogger _logger;
private readonly IConfiguration _config;
private readonly IGatewayCache _gatewayCache;
private readonly IWireGuardService _wireGuardService;
private readonly IWireGuardConfigFileCreator _wireGuardConfigFileCreator;
private readonly INtTrafficManager _ntTrafficManager;
private readonly IWintunTrafficManager _wintunTrafficManager;
private readonly IWireGuardStateMonitor _wireGuardStateMonitor;
private readonly IRouteChangeMonitor _routeChangeMonitor;
private readonly ISystemNetworkInterfaces _networkInterfaces;
private readonly IInterfaceForwardingMonitor _interfaceForwardingMonitor;
private readonly INetworkInterfacePolicyManager _interfacePolicyManager;
private readonly IWireGuardServerRouteManager _serverRouteManager;
private readonly SemaphoreSlim _serviceSemaphore = new(1, 1);
private readonly Channel _stateChannel = Channel.CreateUnbounded();
private CancellationTokenSource _cts = new();
public VpnError LastError { get; private set; }
public string LocalIpv4Address => _config.WireGuard.DefaultClientIpv4Address;
public NetworkTraffic NetworkTraffic { get; private set; } = NetworkTraffic.Zero;
private TaskCompletionSource? _connectionTaskCompletionSource;
private volatile bool _isConnected;
private VpnCredentials _credentials;
private VpnEndpoint? _endpoint;
private VpnConfig? _vpnConfig;
private INetworkInterfacePolicyLease? _interfacePolicyLease;
private readonly Timer _serviceHealthCheckTimer = new();
private bool IsWireGuardServerRouteEnabled => _vpnConfig?.IsWireGuardServerRouteEnabled == true;
public WireGuardConnection(
ILogger logger,
IConfiguration config,
IGatewayCache gatewayCache,
IWireGuardService wireGuardService,
IWireGuardConfigFileCreator wireGuardConfigFileCreator,
INtTrafficManager ntTrafficManager,
IWintunTrafficManager wintunTrafficManager,
IWireGuardStateMonitor wireGuardStateMonitor,
IRouteChangeMonitor routeChangeMonitor,
ISystemNetworkInterfaces networkInterfaces,
IInterfaceForwardingMonitor interfaceForwardingMonitor,
INetworkInterfacePolicyManager interfacePolicyManager,
IWireGuardServerRouteManager serverRouteManager)
{
_logger = logger;
_config = config;
_gatewayCache = gatewayCache;
_wireGuardService = wireGuardService;
_wireGuardConfigFileCreator = wireGuardConfigFileCreator;
_ntTrafficManager = ntTrafficManager;
_wintunTrafficManager = wintunTrafficManager;
_wireGuardStateMonitor = wireGuardStateMonitor;
_routeChangeMonitor = routeChangeMonitor;
_networkInterfaces = networkInterfaces;
_interfaceForwardingMonitor = interfaceForwardingMonitor;
_interfacePolicyManager = interfacePolicyManager;
_serverRouteManager = serverRouteManager;
_routeChangeMonitor.RouteChanged += OnRouteChanged;
_interfaceForwardingMonitor.ForwardingEnabled += OnInterfaceForwardingEnabledAsync;
_serviceHealthCheckTimer.Interval = config.ServiceCheckInterval.TotalMilliseconds;
_serviceHealthCheckTimer.Elapsed += CheckIfServiceIsRunningAsync;
}
public async Task ConnectAsync(VpnEndpoint endpoint, VpnCredentials credentials,
VpnConfig config, CancellationToken cancellationToken)
{
_credentials = credentials;
_endpoint = endpoint;
_vpnConfig = config;
_connectionTaskCompletionSource = new();
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_isConnected = false;
NetworkTraffic = NetworkTraffic.Zero;
LastError = VpnError.None;
bool isWireGuardServerRouteEnabled = IsWireGuardServerRouteEnabled;
INetworkInterface bestInterface = GetBestInterface();
if (!isWireGuardServerRouteEnabled)
{
if (bestInterface.IsIPv4ForwardingEnabled)
{
_logger.Warn($"Triggering disconnect due to active interface forwarding " +
$"on interface {bestInterface.Name} with index {bestInterface.Index}.");
return VpnError.InterfaceHasForwardingEnabled;
}
}
_wireGuardConfigFileCreator.Create(_endpoint, _credentials, _vpnConfig);
UpdateGatewayCache();
if (isWireGuardServerRouteEnabled)
{
_serverRouteManager.CleanupPersistedRoutes();
_serverRouteManager.CreateServerRoute(_endpoint, _vpnConfig);
}
else
{
ApplyInterfacePolicy(bestInterface);
}
await RunWithServiceLockAsync(_wireGuardService.StopAsync, _cts.Token);
StartMonitoringVpnStateAsync(_cts.Token);
await RunWithServiceLockAsync(() => _wireGuardService.StartAsync(_cts.Token, _vpnConfig.VpnProtocol), _cts.Token);
int timeout = Math.Clamp((int)_vpnConfig.WireGuardConnectionTimeout.TotalMilliseconds, MIN_CONNECTION_TIMEOUT, MAX_CONNECTION_TIMEOUT);
// cancellationToken instead of _cts.Token to avoid cancelling the delay when disconnecting
Task timeoutTask = Task.Delay(timeout, cancellationToken);
Task completedTask = await Task.WhenAny(timeoutTask, _connectionTaskCompletionSource.Task);
cancellationToken.ThrowIfCancellationRequested();
if (completedTask == timeoutTask)
{
_logger.Warn($"{timeout}ms timeout reached, disconnecting.");
return VpnError.AdapterTimeoutError;
}
if (!_connectionTaskCompletionSource.Task.IsCompleted || !_connectionTaskCompletionSource.Task.Result)
{
return LastError;
}
StartMonitoringNetworkTrafficAsync(_cts.Token);
return VpnError.None;
}
private INetworkInterface GetBestInterface()
{
return _vpnConfig is null
? new NullNetworkInterface()
: _networkInterfaces.GetBestInterfaceExcludingHardwareId(_config.GetWireGuardHardwareId());
}
private void ApplyInterfacePolicy(INetworkInterface bestInterface)
{
ReleaseInterfacePolicy();
if (_vpnConfig is null || !_vpnConfig.ShouldDisableWeakHostSetting)
{
return;
}
if (bestInterface.Index == 0)
{
_logger.Warn("Skipping interface policy application because no active interface was resolved.");
return;
}
try
{
_interfacePolicyLease = _interfacePolicyManager.Apply(bestInterface);
}
catch (Exception ex)
{
_logger.Warn("Failed to apply interface policy.", ex);
}
}
private void ReleaseInterfacePolicy()
{
try
{
_interfacePolicyLease?.Dispose();
_interfacePolicyLease = null;
}
catch (Exception e)
{
_logger.Warn("Failed to dispose interface policy lease.", e);
}
}
private void UpdateGatewayCache()
{
_gatewayCache.Save(IPAddress.Parse(_config.WireGuard.DefaultServerGatewayIpv4Address));
}
public async Task DisconnectAsync()
{
_isConnected = false;
ReleaseInterfacePolicy();
_serviceHealthCheckTimer.Stop();
await RunWithServiceLockAsync(_wireGuardService.StopAsync);
if (IsWireGuardServerRouteEnabled)
{
if (_endpoint is not null)
{
_serverRouteManager.DeleteServerRoutes(_endpoint);
}
}
else
{
_interfaceForwardingMonitor.Stop();
}
SetConnectionTaskResult(false);
}
public async IAsyncEnumerable ObserveStatesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
yield return await _stateChannel.Reader.ReadAsync(cancellationToken);
}
}
private void SetConnectionTaskResult(bool result)
{
if (_connectionTaskCompletionSource?.Task.IsCompletedSuccessfully == false)
{
_connectionTaskCompletionSource?.SetResult(result);
}
}
private void StartMonitoringVpnStateAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () => await MonitorVpnStateAsync(cancellationToken), cancellationToken);
}
private async Task MonitorVpnStateAsync(CancellationToken cancellationToken)
{
try
{
await foreach (VpnState state in _wireGuardStateMonitor.WatchStatesAsync(cancellationToken))
{
if (state.Status == VpnStatus.Connected)
{
_isConnected = true;
SetConnectionTaskResult(true);
UpdateGatewayCache();
_serviceHealthCheckTimer.Start();
if (IsWireGuardServerRouteEnabled)
{
_routeChangeMonitor.Start();
}
else
{
_interfaceForwardingMonitor.Start();
}
}
else
{
if (state.Error != VpnError.None)
{
LastError = state.Error;
SetConnectionTaskResult(false);
if (!_isConnected)
{
_cts.Cancel();
return;
}
}
await _stateChannel.Writer.WriteAsync(state, cancellationToken);
}
}
}
catch (OperationCanceledException)
{
// expected on cancellation
}
catch (Exception ex)
{
_logger.Error("Status monitor failed.", ex);
}
}
private void StartMonitoringNetworkTrafficAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () => await MonitorNetworkTrafficAsync(cancellationToken), cancellationToken);
}
private async Task MonitorNetworkTrafficAsync(CancellationToken cancellationToken)
{
try
{
IAsyncEnumerable trafficStream =
_vpnConfig?.VpnProtocol == VpnProtocol.WireGuardUdp
? _ntTrafficManager.WatchTrafficAsync(cancellationToken)
: _wintunTrafficManager.WatchTrafficAsync(cancellationToken);
await foreach (NetworkTraffic traffic in trafficStream.WithCancellation(cancellationToken))
{
NetworkTraffic = traffic;
}
}
catch (OperationCanceledException)
{
// expected on cancellation
}
catch (Exception ex)
{
_logger.Error("Traffic monitor failed.", ex);
}
}
private async Task RunWithServiceLockAsync(Func action, CancellationToken cancellationToken = default)
{
await _serviceSemaphore.WaitAsync(cancellationToken);
try
{
await action();
}
finally
{
_serviceSemaphore.Release();
}
}
private async void OnInterfaceForwardingEnabledAsync(object? sender, InterfaceForwardingEventArgs e)
{
if (IsWireGuardServerRouteEnabled || !_isConnected || _endpoint is null)
{
return;
}
try
{
INetworkInterface bestInterface = GetBestInterface();
if (bestInterface.Index != e.InterfaceIndex)
{
return;
}
_logger.Warn(
$"Detected active interface forwarding on interface {bestInterface.Name} with index {e.InterfaceIndex}.");
await _stateChannel.Writer.WriteAsync(new VpnState(VpnStatus.Connected, VpnError.InterfaceHasForwardingEnabled, _endpoint.VpnProtocol), _cts.Token);
}
catch (Exception ex)
{
_logger.Warn("Failed to handle interface forwarding notification.", ex);
}
}
private void OnRouteChanged(object? sender, RouteChangeEventArgs e)
{
if (!IsWireGuardServerRouteEnabled || !_isConnected || _endpoint is null || _vpnConfig is null)
{
return;
}
_serverRouteManager.CreateServerRoute(_endpoint, _vpnConfig);
}
private async void CheckIfServiceIsRunningAsync(object? sender, ElapsedEventArgs e)
{
if (_isConnected && !_wireGuardService.Running() && !_cts.IsCancellationRequested && _endpoint is not null)
{
_logger.Info($"The service {_wireGuardService.Name} is not running. " +
"Sending VpnError.Unknown to get reconnected.");
await _stateChannel.Writer.WriteAsync(new VpnState(VpnStatus.Connected, VpnError.Unknown, _endpoint.VpnProtocol), _cts.Token);
}
}
}