/*
* 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.Threading.Channels;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ConnectionLogs;
using ProtonVPN.Logging.Contracts.Events.DisconnectLogs;
using ProtonVPN.ProTun.Adapters;
using ProtonVPN.ProTun.Contracts;
using ProtonVPN.ProTun.Contracts.Adapters;
using ProtonVPN.ProTun.Contracts.ConnectionArguments;
using ProtonVPN.ProTun.Generated;
using ProtonVPN.ProTun.Logging;
using ProtonVPN.ProTun.StateChanges;
using ProtonVPN.ProTun.StatsResponses;
using ProTunApi = ProtonVPN.ProTun.Generated.ProTun;
using ProTunConnection = ProtonVPN.ProTun.Generated.Connection;
using ProTunWindowsConnection = ProtonVPN.ProTun.Generated.WindowsConnection;
namespace ProtonVPN.ProTun;
public class ProTunManager : IProTunManager
{
private const LogLevel LOG_LEVEL = LogLevel.Info;
private const ushort MTU = 1420;
private const uint UDP_SEND_BUFFER_SIZE = 2 * 1024 * 1024; // 2 MiB
private const uint UDP_RECEIVE_BUFFER_SIZE = 4 * 1024 * 1024; // 4 MiB
private const uint WINTUN_BUFFER_SIZE = 4 * 1024 * 1024; // 4 MiB (Needs to be a power of two between 131072 and 67108864 inclusive)
private readonly IProTunLogger _proTunLogger;
private readonly IProTunStateChangeHandler _proTunStateChangeHandler;
private readonly IProTunEventsResponseHandler _proTunEventsResponseHandler;
private readonly IAdapterDetailsCache _adapterDetailsCache;
private readonly ILogger _logger;
private readonly SemaphoreSlim _protunSemaphore = new(1, 1);
private readonly SemaphoreSlim _connectionSemaphore = new(1, 1);
private ProTunApi? _protun;
private ProTunWindowsConnection? _windowsConnection;
private ProTunConnection? _connection;
public Channel StateChannel { get; }
public Channel TrafficChannel { get; }
public ProTunManager(IProTunLogger proTunLogger,
IProTunStateChangeHandler proTunStateChangeHandler,
IProTunEventsResponseHandler proTunEventsResponseHandler,
IAdapterDetailsCache adapterDetailsCache,
ILogger logger)
{
_proTunLogger = proTunLogger;
_proTunStateChangeHandler = proTunStateChangeHandler;
_proTunEventsResponseHandler = proTunEventsResponseHandler;
_adapterDetailsCache = adapterDetailsCache;
_logger = logger;
ProTunDllLoader.Register();
StateChannel = _proTunStateChangeHandler.StateChannel;
TrafficChannel = _proTunEventsResponseHandler.TrafficChannel;
}
public async Task InitializeAsync()
{
await _protunSemaphore.WaitAsync();
try
{
if (_protun is null)
{
_protun = ProTunApi.Initialize(LOG_LEVEL, _proTunLogger);
if (_protun is null)
{
_logger.Error("Failed to initializing ProTUN");
}
}
}
catch (Exception ex)
{
_logger.Error("Error when initializing ProTUN", ex);
}
finally
{
_protunSemaphore.Release();
}
}
public async Task ConnectAsync(ConnectionArgs args, CancellationToken cancellationToken)
{
await _connectionSemaphore.WaitAsync();
VpnState? disconnectState = null;
try
{
TryDisconnect();
await InitializeAsync();
if (_protun is null)
{
_logger.Error("Cannot connect because ProTUN object doesn't exist");
}
else
{
InitialConnectionConfig initialConnectionConfig = CreateInitialConnectionConfig(args);
NetworkConfig networkConfig = CreateNetworkConfig(args);
_proTunStateChangeHandler.SetCancellationToken(cancellationToken);
_proTunEventsResponseHandler.SetCancellationToken(cancellationToken);
_windowsConnection = ProTunWindowsConnection.Connect(initialConnectionConfig, networkConfig,
_proTunStateChangeHandler, _proTunEventsResponseHandler);
AdapterDetails adapterDetails = _windowsConnection.GetAdapterDetails().Map();
_adapterDetailsCache.Set(adapterDetails);
_connection = _windowsConnection.GetConnection();
}
}
catch (Exception ex)
{
_logger.Error("Error when connecting with ProTUN", ex);
disconnectState = TryDisconnect(VpnError.AdapterTimeoutError);
}
finally
{
_connectionSemaphore.Release();
}
if (disconnectState != null)
{
await InvokeStateChangeAsync(disconnectState, cancellationToken);
}
}
private async Task InvokeStateChangeAsync(VpnState vpnState, CancellationToken cancellationToken)
{
await StateChannel.Writer.WriteAsync(vpnState, cancellationToken);
}
private void TryDisconnect()
{
Disconnect();
DestroyConnection();
}
private VpnState TryDisconnect(VpnError vpnError)
{
Disconnect();
DestroyConnection();
return new(VpnStatus.Disconnected, vpnError, VpnProtocol.Smart);
}
private void Disconnect()
{
try
{
_connection?.DisconnectAndWait();
}
catch (Exception ex)
{
_logger.Error("Error when disconnecting with ProTUN", ex);
}
_connection = null;
}
private void DestroyConnection()
{
try
{
_windowsConnection?.Destroy();
}
catch (Exception ex)
{
_logger.Error("Error when destroying ProTUN connection", ex);
}
_windowsConnection = null;
}
private static InitialConnectionConfig CreateInitialConnectionConfig(ConnectionArgs args)
{
return new InitialConnectionConfig(
wgPrivateKey: args.WireGuardPrivateKey,
peers: MapPeers(args.Peers).ToArray(),
networkAvailable: true,
pcapFile: null
);
}
private static IEnumerable MapPeers(List peers)
{
foreach (ConnectionPeer peer in peers)
{
if (peer is not null)
{
yield return MapPeer(peer);
}
}
}
private static PeerInfo MapPeer(ConnectionPeer peer)
{
return new(
peerId: peer.PeerId,
serverIp: peer.ServerIp,
serverPublicKey: peer.ServerPublicKey,
udpPorts: peer.UdpPorts,
tcpPorts: peer.TcpPorts,
tlsPorts: peer.TlsPorts,
priority: peer.Priority
);
}
private static NetworkConfig CreateNetworkConfig(ConnectionArgs args)
{
return new(CreateAdapterConfig(args), CreateUdpSocketConfig());
}
private static AdapterConfig CreateAdapterConfig(ConnectionArgs args)
{
return new AdapterConfig(
customDnsServerIps: args.CustomDnsServers.ToArray(),
isIpv6Enabled: args.IsIpv6Enabled,
mtu: MTU,
bufferSizeBytes: WINTUN_BUFFER_SIZE
);
}
private static SocketConfig CreateUdpSocketConfig()
{
return new SocketConfig(
sendBufferSizeBytes: UDP_SEND_BUFFER_SIZE,
receiveBufferSizeBytes: UDP_RECEIVE_BUFFER_SIZE
);
}
public async Task DisconnectAsync()
{
await _connectionSemaphore.WaitAsync();
try
{
TryDisconnect();
}
finally
{
_connectionSemaphore.Release();
}
}
public async Task RequestStatsAsync()
{
await _connectionSemaphore.WaitAsync();
try
{
_connection?.GetStats();
}
catch (Exception ex)
{
_logger.Error("Error when requesting stats from ProTUN", ex);
}
finally
{
_connectionSemaphore.Release();
}
}
}