/*
* 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.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Common.Legacy.Vpn;
using ProtonVPN.Crypto.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ConnectLogs;
using ProtonVPN.Logging.Contracts.Events.ProtocolLogs;
using ProtonVPN.ProTun.Contracts;
using ProtonVPN.ProTun.Contracts.Adapters;
using ProtonVPN.ProTun.Contracts.ConnectionArguments;
using ProtonVPN.ProTun.Contracts.Traffic;
using ProtonVPN.Vpn.Common;
using ProtonVPN.Vpn.Gateways;
namespace ProtonVPN.Vpn.ProTun;
public class ProTunConnection : IProTunConnection
{
private const int MIN_CONNECTION_TIMEOUT = 5000;
private const int MAX_CONNECTION_TIMEOUT = 30000;
private readonly ILogger _logger;
private readonly IGatewayCache _gatewayCache;
private readonly IProTunManager _proTunManager;
private readonly IProTunTrafficManager _proTunTrafficManager;
private readonly IX25519KeyGenerator _x25519KeyGenerator;
private readonly IAdapterDetailsCache _adapterDetailsCache;
private readonly Channel _stateChannel = Channel.CreateUnbounded();
private CancellationTokenSource _cts = new();
private volatile bool _isConnected;
private TaskCompletionSource? _connectionTaskCompletionSource;
private VpnCredentials _credentials;
private VpnEndpoint? _endpoint;
private VpnConfig? _vpnConfig;
public VpnError LastError { get; private set; }
public NetworkTraffic NetworkTraffic { get; private set; } = NetworkTraffic.Zero;
public string LocalIpv4Address => _adapterDetailsCache.ClientIpv4Address;
public ProTunConnection(
ILogger logger,
IGatewayCache gatewayCache,
IProTunManager proTunManager,
IProTunTrafficManager proTunTrafficManager,
IX25519KeyGenerator x25519KeyGenerator,
IAdapterDetailsCache adapterDetailsCache)
{
_logger = logger;
_gatewayCache = gatewayCache;
_proTunManager = proTunManager;
_proTunTrafficManager = proTunTrafficManager;
_x25519KeyGenerator = x25519KeyGenerator;
_adapterDetailsCache = adapterDetailsCache;
}
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;
UpdateGatewayCache();
StartMonitoringVpnStateAsync(_cts.Token);
ConnectionArgs? connectionArgs = CreateConnectionArgs();
if (connectionArgs is null)
{
const string ERR_MSG = "The endpoint or the config are null when creating the ProTun connection args.";
_logger.Error(ERR_MSG);
throw new NotImplementedException(ERR_MSG);
}
_proTunManager.ConnectAsync(connectionArgs, _cts.Token).FireAndForget();
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 void StartMonitoringVpnStateAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () => await MonitorVpnStateAsync(cancellationToken), cancellationToken);
}
private async Task MonitorVpnStateAsync(CancellationToken cancellationToken)
{
try
{
await foreach (VpnState state in WatchStatesAsync(cancellationToken))
{
if (state.Status == VpnStatus.Connected)
{
_isConnected = true;
SetConnectionTaskResult(true);
UpdateGatewayCache();
_proTunTrafficManager.StartAsync(cancellationToken).FireAndForget();
}
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 async IAsyncEnumerable WatchStatesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
yield return await _proTunManager.StateChannel.Reader.ReadAsync(cancellationToken);
}
}
private void UpdateGatewayCache()
{
if (IPAddress.TryParse(_adapterDetailsCache.ServerGatewayIpv4Address, out IPAddress? address) && address is not null)
{
_gatewayCache.Save(address);
}
}
private ConnectionArgs? CreateConnectionArgs()
{
VpnConfig? config = _vpnConfig;
VpnEndpoint? endpoint = _endpoint;
return config == null || endpoint is null ? null: new()
{
WireGuardPrivateKey = GetX25519SecretKey().Bytes,
Peers = CreatePeers(config, endpoint),
IsIpv6Enabled = config.IsIpv6Enabled,
CustomDnsServers = config.CustomDns
};
}
private SecretKey GetX25519SecretKey()
{
return _x25519KeyGenerator.FromEd25519SecretKey(_credentials.ClientKeyPair.SecretKey);
}
private List CreatePeers(VpnConfig config, VpnEndpoint endpoint)
{
return [new()
{
PeerId = $"{endpoint.Server.Ip}@{endpoint.Server.Label}",
ServerIp = endpoint.Server.Ip,
ServerPublicKey = endpoint.Server.X25519PublicKey.Bytes,
UdpPorts = GetPorts(config, endpoint, VpnProtocol.ProTunUdp),
TcpPorts = GetPorts(config, endpoint, VpnProtocol.ProTunTcp),
TlsPorts = GetPorts(config, endpoint, VpnProtocol.ProTunTls),
Priority = 1
}];
}
private ushort[] GetPorts(VpnConfig config, VpnEndpoint endpoint, VpnProtocol protocol)
{
if (endpoint.VpnProtocol is VpnProtocol.Smart || endpoint.VpnProtocol == protocol)
{
bool hasPorts = config.Ports.TryGetValue(protocol, out IReadOnlyCollection? ports);
return hasPorts && ports is not null ? ports.Select(p => (ushort)p).ToArray() : [];
}
return [];
}
private void StartMonitoringNetworkTrafficAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () => await MonitorNetworkTrafficAsync(cancellationToken), cancellationToken);
}
private async Task MonitorNetworkTrafficAsync(CancellationToken cancellationToken)
{
try
{
await foreach (NetworkTraffic traffic in WatchTrafficAsync(cancellationToken))
{
NetworkTraffic = traffic;
}
}
catch (OperationCanceledException)
{
// expected on cancellation
}
catch (Exception ex)
{
_logger.Error("Traffic monitor failed.", ex);
}
}
private async IAsyncEnumerable WatchTrafficAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
yield return await _proTunManager.TrafficChannel.Reader.ReadAsync(cancellationToken);
}
}
public async Task DisconnectAsync()
{
await _proTunManager.DisconnectAsync();
SetConnectionTaskResult(false);
}
private void SetConnectionTaskResult(bool result)
{
if (_connectionTaskCompletionSource?.Task.IsCompletedSuccessfully == false)
{
_connectionTaskCompletionSource?.SetResult(result);
}
}
public async IAsyncEnumerable ObserveStatesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
yield return await _stateChannel.Reader.ReadAsync(cancellationToken);
}
}
}