/*
* 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.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ConnectLogs;
using ProtonVPN.Logging.Contracts.Events.DisconnectLogs;
namespace ProtonVPN.Vpn.Management;
///
/// TCP connection to OpenVPN management interface.
///
internal class TcpManagementChannel : IManagementChannel, ITcpManagementChannel
{
private readonly ILogger _logger;
private readonly string _host;
private StreamReader? _streamReader;
private StreamWriter? _streamWriter;
private TcpClient? _tcpClient;
public TcpManagementChannel(ILogger logger, IStaticConfiguration config)
{
_logger = logger;
_host = config.OpenVpn.ManagementHost;
}
public async Task Connect(int port)
{
Disconnect();
TcpClient tcpClient = new();
_logger.Info($"Connecting to OpenVPN management interface on {_host}:{port}");
await tcpClient.ConnectAsync(_host, port);
_streamReader = new StreamReader(tcpClient.GetStream());
_streamWriter = new StreamWriter(tcpClient.GetStream())
{
AutoFlush = true
};
_tcpClient = tcpClient;
}
public async Task WriteLineAsync(string message, CancellationToken cancellationToken)
{
try
{
await WriteLineInternalAsync(message, cancellationToken);
}
catch (ObjectDisposedException ex)
{
throw new IOException("The NetworkStream is closed", ex);
}
}
public async Task ReadLineAsync(CancellationToken cancellationToken)
{
try
{
return await ReadLineInternalAsync(cancellationToken);
}
catch (OperationCanceledException)
{
return null; // treat as disconnect
}
catch (ObjectDisposedException ex)
{
throw new IOException("The NetworkStream is closed", ex);
}
}
public void Disconnect()
{
if (_tcpClient != null)
{
if (_tcpClient.Connected)
{
_logger.Info("Disconnecting from OpenVPN management interface");
}
_tcpClient.Close();
SafeDispose(ref _tcpClient);
}
SafeDispose(ref _streamReader);
SafeDispose(ref _streamWriter);
}
private async Task WriteLineInternalAsync(string message, CancellationToken cancellationToken)
{
StreamWriter? writer = _streamWriter
?? throw new IOException("OpenVPN management interface is not connected");
await writer.WriteLineAsync(message.ToCharArray(), cancellationToken);
}
private async Task ReadLineInternalAsync(CancellationToken cancellationToken)
{
StreamReader reader = _streamReader
?? throw new IOException("OpenVPN management interface is not connected");
string? message;
try
{
message = await reader.ReadLineAsync(cancellationToken);
}
catch (OperationCanceledException)
{
return null;
}
if (message == null)
{
_logger.Info("Disconnected from OpenVPN management interface");
}
return message;
}
private void SafeDispose(ref T? disposable) where T: class, IDisposable
{
T? item = disposable;
disposable = null;
try
{
item?.Dispose();
}
catch (InvalidOperationException ex)
{
_logger.Error($"Failed to disconnect from OpenVPN management interface: {ex.Message}");
}
}
}