/* * 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; using System.Threading.Tasks; using ProtonVPN.Common.Legacy; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Common.Legacy.OS.Processes; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.ConnectionLogs; using ProtonVPN.Logging.Contracts.Events.ProcessLogs; using ProtonVPN.Vpn.OpenVpn.Arguments; namespace ProtonVPN.Vpn.OpenVpn; /// /// Starts and stops OpenVPN process. /// public class OpenVpnProcess : IOpenVpnProcess { private const string TAP_DRIVER = "tap-windows6"; private static readonly TimeSpan _waitAfterSignalingExit = TimeSpan.FromSeconds(6); private readonly ILogger _logger; private readonly IOsProcesses _processes; private readonly IOpenVpnExitEvent _processExitEvent; private readonly IStaticConfiguration _staticConfig; private readonly IOsProcess _nullProcess; private IOsProcess _process; private TaskCompletionSource? _startCompletionSource; public OpenVpnProcess( ILogger logger, IOsProcesses processes, IOpenVpnExitEvent processExitEvent, IStaticConfiguration staticConfig) { _logger = logger; _processes = processes; _processExitEvent = processExitEvent; _staticConfig = staticConfig; _process = _nullProcess = new NullOsProcess(); } public Task Start(OpenVpnProcessParams processParams) { _startCompletionSource?.TrySetCanceled(); _startCompletionSource = new TaskCompletionSource(); string arguments = GetCommandLineArguments(processParams); _process = _processes.Process(_staticConfig.OpenVpn.ExePath, arguments); AddEventHandlers(); _process.Start(); _logger.Info("OpenVPN <- Management channel password"); _process.StandardInput.WriteLine(processParams.Password); return _startCompletionSource.Task; } public void Stop() { _startCompletionSource?.TrySetCanceled(); SignalProcessToExit(); WaitForProcessToExit(_waitAfterSignalingExit); KillNotExitedProcesses(); Cleanup(); } private string GetCommandLineArguments(OpenVpnProcessParams processParams) { CommandLineArguments arguments = new CommandLineArguments() .Add(new BasicArguments(_staticConfig.OpenVpn)) .Add(new ManagementArguments(_staticConfig.OpenVpn, processParams.ManagementPort)) .Add(new OpenVpnEndpointArguments(processParams.Endpoint)) .Add(new BindArguments(new BestLocalOpenVpnEndpoint(processParams.Endpoint).Ip())) .Add(new CustomDnsArguments(processParams.CustomDns)) .Add(new TlsVerifyArguments(_staticConfig.OpenVpn, processParams.Endpoint.Server.Name)) .Add(new BaseRouteArgument(processParams.SplitTunnelMode)); switch (processParams.OpenVpnAdapter) { case OpenVpnAdapter.Tun: arguments.Add(new NetworkDriverArgument(processParams.InterfaceGuid, processParams.OpenVpnAdapter)); break; case OpenVpnAdapter.Tap: arguments.Add(new WindowsDriverArgument(TAP_DRIVER)); break; } return arguments; } private void AddEventHandlers() { _process.OutputDataReceived += Process_OutputDataReceived; _process.ErrorDataReceived += Process_ErrorDataReceived; _process.Exited += Process_Exited; } private void SignalProcessToExit() { _processExitEvent.Signal(); } private void WaitForProcessToExit(TimeSpan duration) { _process.WaitForExit(duration); } private void KillNotExitedProcesses() { IOsProcess[] processes = _processes.ProcessesByPath(_staticConfig.OpenVpn.ExePath); foreach (IOsProcess process in processes) { process.Kill(); process.Dispose(); } } private void Process_OutputDataReceived(object? sender, EventArgs e) { _logger.Info($"OpenVPN -> {e.Data}"); if (e.Data.StartsWithIgnoringCase("MANAGEMENT: TCP Socket listening on")) { _startCompletionSource?.TrySetResult(true); } else if (e.Data == null) { _startCompletionSource?.TrySetResult(false); } } private void Process_ErrorDataReceived(object? sender, EventArgs e) { string message = $"OpenVPN -> {e.Data}"; if (e.Data.StartsWithIgnoringCase("Enter Management Password:")) { _logger.Info(message); } else { _logger.Warn(message); } } private void Process_Exited(object? sender, EventArgs e) { _logger.Info("OpenVPN process exited"); _startCompletionSource?.TrySetResult(false); } private void Cleanup() { _process?.Dispose(); _process = _nullProcess; } }