From ef38e76b64acbfb25ca0bc401b40b7d6b1c425b4 Mon Sep 17 00:00:00 2001 From: Trudy Date: Sat, 13 Jan 2024 17:52:40 -0800 Subject: [PATCH] deprecate CryptoBinaryStream --- Tests/VpnHood.Test/Tests/TunnelTest.cs | 8 +- .../ClientProfileStoreLegacy.cs | 3 +- VpnHood.Client.App/VpnHoodApp.cs | 4 +- VpnHood.Client/ClientHost.cs | 4 +- .../ConnectorServices/ConnectorService.cs | 35 +- VpnHood.Client/VpnHoodClient.cs | 11 +- VpnHood.Server/ServerHost.cs | 55 +-- VpnHood.Server/Session.cs | 4 +- .../Channels/Streams/BinaryStream.cs | 40 +-- .../Channels/Streams/CryptoBinaryStream.cs | 330 ++++++++++++++++++ 10 files changed, 416 insertions(+), 78 deletions(-) create mode 100644 VpnHood.Tunneling/Channels/Streams/CryptoBinaryStream.cs diff --git a/Tests/VpnHood.Test/Tests/TunnelTest.cs b/Tests/VpnHood.Test/Tests/TunnelTest.cs index df7234213..0316d5d05 100644 --- a/Tests/VpnHood.Test/Tests/TunnelTest.cs +++ b/Tests/VpnHood.Test/Tests/TunnelTest.cs @@ -209,7 +209,7 @@ private static async Task SimpleLoopback(TcpListener tcpListener, CancellationTo using var client = await tcpListener.AcceptTcpClientAsync(cancellationToken); // Create a memory stream to store the incoming data - ChunkStream binaryStream = new BinaryStream(client.GetStream(), Guid.NewGuid().ToString(), new byte[16]); + ChunkStream binaryStream = new BinaryStream(client.GetStream(), Guid.NewGuid().ToString()); while (true) { using var memoryStream = new MemoryStream(); @@ -267,7 +267,7 @@ public async Task ChunkStream() }; // first stream - ChunkStream binaryStream = new BinaryStream(stream, Guid.NewGuid().ToString(), new byte[16]); + ChunkStream binaryStream = new BinaryStream(stream, Guid.NewGuid().ToString()); await binaryStream.WriteAsync(BitConverter.GetBytes(chunks.Sum(x => x.Length)), cts.Token); foreach (var chunk in chunks) await binaryStream.WriteAsync(Encoding.UTF8.GetBytes(chunk).ToArray(), cts.Token); @@ -279,7 +279,7 @@ public async Task ChunkStream() Assert.AreEqual(string.Join("", chunks), res); // write second stream - binaryStream = (BinaryStream)await binaryStream.CreateReuse(); + binaryStream = await binaryStream.CreateReuse(); await binaryStream.WriteAsync(BitConverter.GetBytes(chunks.Sum(x => x.Length)).ToArray(), cts.Token); foreach (var chunk in chunks) await binaryStream.WriteAsync(Encoding.UTF8.GetBytes(chunk).ToArray(), cts.Token); @@ -321,7 +321,7 @@ public async Task ChunkStream_Binary() random.NextBytes(writeBuffer); // write stream - ChunkStream binaryStream = new BinaryStream(stream, Guid.NewGuid().ToString(), new byte[16]); + ChunkStream binaryStream = new BinaryStream(stream, Guid.NewGuid().ToString()); await binaryStream.WriteAsync(BitConverter.GetBytes(writeBuffer.Length), cts.Token); await binaryStream.WriteAsync((byte[])writeBuffer.Clone(), cts.Token); diff --git a/VpnHood.Client.App/ClientProfileStoreLegacy.cs b/VpnHood.Client.App/ClientProfileStoreLegacy.cs index 3693703a6..d17b67955 100644 --- a/VpnHood.Client.App/ClientProfileStoreLegacy.cs +++ b/VpnHood.Client.App/ClientProfileStoreLegacy.cs @@ -1,13 +1,12 @@ using System.Text.Json; using Microsoft.Extensions.Logging; -using VpnHood.Common; using VpnHood.Common.Logging; using VpnHood.Common.TokenLegacy; using VpnHood.Common.Utils; namespace VpnHood.Client.App; -// deprecated for version 3.3.450 or upper +// deprecated by version 3.2.440 or upper internal static class ClientProfileStoreLegacy { // ReSharper disable once ClassNeverInstantiated.Local diff --git a/VpnHood.Client.App/VpnHoodApp.cs b/VpnHood.Client.App/VpnHoodApp.cs index 78bc63bb9..c41ab58cc 100644 --- a/VpnHood.Client.App/VpnHoodApp.cs +++ b/VpnHood.Client.App/VpnHoodApp.cs @@ -76,7 +76,9 @@ private VpnHoodApp(IAppProvider appProvider, AppOptions? options = default) { if (IsInit) throw new InvalidOperationException("VpnHoodApp is already initialized."); options ??= new AppOptions(); +#pragma warning disable CS0618 // Type or member is obsolete MigrateUserDataFromXamarin(options.AppDataFolderPath); // Deprecated >= 400 +#pragma warning restore CS0618 // Type or member is obsolete Directory.CreateDirectory(options.AppDataFolderPath); //make sure directory exists Resources = options.Resources; @@ -668,7 +670,7 @@ public Task RunJob() : ClientProfileService.FindById(LastActiveClientProfileId); } - // Deprecated >= 400 + [Obsolete("Deprecated >= 400", false)] private static void MigrateUserDataFromXamarin(string folderPath) { try diff --git a/VpnHood.Client/ClientHost.cs b/VpnHood.Client/ClientHost.cs index e5427c904..b1dc6fa3b 100644 --- a/VpnHood.Client/ClientHost.cs +++ b/VpnHood.Client/ClientHost.cs @@ -233,8 +233,10 @@ await Client.AddPassthruTcpStream( var orgTcpClientStream = new TcpClientStream(orgTcpClient, orgTcpClient.GetStream(), request.RequestId + ":host"); // MaxEncryptChunk - if (proxyClientStream.Stream is BinaryStream binaryStream) +#pragma warning disable CS0618 // Type or member is obsolete + if (proxyClientStream.Stream is CryptoBinaryStream binaryStream) binaryStream.MaxEncryptChunk = TunnelDefaults.TcpProxyEncryptChunkCount; +#pragma warning restore CS0618 // Type or member is obsolete channel = new StreamProxyChannel(request.RequestId, orgTcpClientStream, proxyClientStream); Client.Tunnel.AddChannel(channel); diff --git a/VpnHood.Client/ConnectorServices/ConnectorService.cs b/VpnHood.Client/ConnectorServices/ConnectorService.cs index a29e39f21..8d68de49e 100644 --- a/VpnHood.Client/ConnectorServices/ConnectorService.cs +++ b/VpnHood.Client/ConnectorServices/ConnectorService.cs @@ -2,6 +2,9 @@ using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using System.Collections.Concurrent; +using System.Net.Sockets; +using System.Text; using VpnHood.Common.Exceptions; using VpnHood.Common.Logging; using VpnHood.Common.Utils; @@ -9,15 +12,12 @@ using VpnHood.Tunneling.Factory; using VpnHood.Client.Exceptions; using VpnHood.Tunneling.ClientStreams; -using System.Collections.Concurrent; using VpnHood.Common.JobController; using VpnHood.Common.Messaging; using VpnHood.Tunneling.Messaging; using VpnHood.Common.Collections; using VpnHood.Tunneling.Channels.Streams; -using System.Text; using VpnHood.Tunneling.Utils; -using System.Net.Sockets; namespace VpnHood.Client.ConnectorServices; @@ -52,31 +52,44 @@ public void Init(int serverProtocolVersion, TimeSpan tcpRequestTimeout, TimeSpan ServerProtocolVersion = serverProtocolVersion; RequestTimeout = tcpRequestTimeout; TcpReuseTimeout = tcpReuseTimeout; - _apiKey = serverSecret != null ? HttpUtil.GetApiKey(serverSecret, TunnelDefaults.HttpPassCheck) : ""; + _apiKey = serverSecret != null ? HttpUtil.GetApiKey(serverSecret, TunnelDefaults.HttpPassCheck) : string.Empty; } private async Task CreateClientStream(TcpClient tcpClient, Stream sslStream, string streamId, CancellationToken cancellationToken) { - var streamSecret = VhUtil.GenerateKey(128); + var streamSecret = VhUtil.GenerateKey(128); // deprecated by 450 and upper + var version = ServerProtocolVersion >= 5 ? 3 : 2; + var useBinaryStream = version >= 3 && !string.IsNullOrEmpty(_apiKey); // write HTTP request var header = $"POST /{Guid.NewGuid()} HTTP/1.1\r\n" + $"Authorization: ApiKey {_apiKey}\r\n" + - $"X-Version: 2\r\n" + + $"X-Version: {version}\r\n" + $"X-Secret: {Convert.ToBase64String(streamSecret)}\r\n" + + $"X-BinaryStream: {useBinaryStream}\r\n" + "\r\n"; // Send header and wait for its response await sslStream.WriteAsync(Encoding.UTF8.GetBytes(header), cancellationToken); // secret await HttpUtil.ReadHeadersAsync(sslStream, cancellationToken); - if (string.IsNullOrEmpty(_apiKey)) - return new TcpClientStream(tcpClient, sslStream, streamId); + // deprecated legacy by version >= 450 +#pragma warning disable CS0618 // Type or member is obsolete + if (version == 2) + { + if (string.IsNullOrEmpty(_apiKey)) + return new TcpClientStream(tcpClient, sslStream, streamId); + + // dispose Ssl + await sslStream.DisposeAsync(); + return new TcpClientStream(tcpClient, new CryptoBinaryStream(tcpClient.GetStream(), streamId, streamSecret), streamId, ReuseStreamClient); + } +#pragma warning restore CS0618 // Type or member is obsolete - // dispose Ssl - await sslStream.DisposeAsync(); - return new TcpClientStream(tcpClient, new BinaryStream(tcpClient.GetStream(), streamId, streamSecret), streamId, ReuseStreamClient); + return useBinaryStream + ? new TcpClientStream(tcpClient, new BinaryStream(tcpClient.GetStream(), streamId), streamId, ReuseStreamClient) + : new TcpClientStream(tcpClient, sslStream, streamId); } private async Task GetTlsConnectionToServer(string streamId, CancellationToken cancellationToken) diff --git a/VpnHood.Client/VpnHoodClient.cs b/VpnHood.Client/VpnHoodClient.cs index cda55ddcc..4976d6fff 100644 --- a/VpnHood.Client/VpnHoodClient.cs +++ b/VpnHood.Client/VpnHoodClient.cs @@ -45,10 +45,9 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable private ClientUsageTracker? _clientUsageTracker; private DateTime? _initConnectedTime; - private bool IsTcpDatagramLifespanSupported => ServerVersion?.Build >= 345; //will be deprecated private DateTime? _lastConnectionErrorTime; private byte[]? _sessionKey; - private byte[]? _serverKey; + private byte[]? _serverSecret; private bool _useUdpChannel; private ClientState _state = ClientState.None; private int ProtocolVersion { get; } @@ -547,7 +546,7 @@ private async Task ManageDatagramChannels(CancellationToken cancellationToken) private async Task AddUdpChannel() { if (HostTcpEndPoint == null) throw new InvalidOperationException($"{nameof(HostTcpEndPoint)} is not initialized!"); - if (VhUtil.IsNullOrEmpty(_serverKey)) throw new Exception("ServerSecret has not been set."); + if (VhUtil.IsNullOrEmpty(_serverSecret)) throw new Exception("ServerSecret has not been set."); if (VhUtil.IsNullOrEmpty(_sessionKey)) throw new Exception("Server UdpKey has not been set."); if (HostUdpEndPoint == null) { @@ -559,7 +558,7 @@ private async Task AddUdpChannel() var udpChannel = new UdpChannel(SessionId, _sessionKey, false, _connectorService.ServerProtocolVersion); try { - var udpChannelTransmitter = new ClientUdpChannelTransmitter(udpChannel, udpClient, _serverKey); + var udpChannelTransmitter = new ClientUdpChannelTransmitter(udpChannel, udpClient, _serverSecret); udpChannel.SetRemote(udpChannelTransmitter, HostUdpEndPoint); Tunnel.AddChannel(udpChannel); } @@ -645,7 +644,7 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool red // get session id SessionId = sessionResponse.SessionId != 0 ? sessionResponse.SessionId : throw new Exception("Invalid SessionId!"); _sessionKey = sessionResponse.SessionKey; - _serverKey = sessionResponse.ServerSecret; + _serverSecret = sessionResponse.ServerSecret; _helloTraffic = sessionResponse.AccessUsage?.Traffic ?? new Traffic(); SessionStatus.SuppressedTo = sessionResponse.SuppressedTo; PublicAddress = sessionResponse.ClientPublicAddress; @@ -715,7 +714,7 @@ private async Task AddTcpDatagramChannel(CancellationToken cancellationToken) try { // find timespan - var lifespan = !VhUtil.IsInfinite(_maxTcpDatagramLifespan) && IsTcpDatagramLifespanSupported + var lifespan = !VhUtil.IsInfinite(_maxTcpDatagramLifespan) ? TimeSpan.FromSeconds(new Random().Next((int)_minTcpDatagramLifespan.TotalSeconds, (int)_maxTcpDatagramLifespan.TotalSeconds)) : Timeout.InfiniteTimeSpan; diff --git a/VpnHood.Server/ServerHost.cs b/VpnHood.Server/ServerHost.cs index 77180e600..e1d73d469 100644 --- a/VpnHood.Server/ServerHost.cs +++ b/VpnHood.Server/ServerHost.cs @@ -22,7 +22,7 @@ namespace VpnHood.Server; internal class ServerHost : IAsyncDisposable, IJob { private readonly HashSet _clientStreams = []; - private const int ServerProtocolVersion = 4; + private const int ServerProtocolVersion = 5; private CancellationTokenSource _cancellationTokenSource = new(); private readonly SessionManager _sessionManager; private readonly SslCertificateManager _sslCertificateManager; @@ -267,6 +267,16 @@ await sslStream.AuthenticateAsServerAsync( } } + private bool CheckApiKeyAuthorization(string authorization) + { + var parts = authorization.Split(' '); + return + parts.Length >= 2 && + parts[0].Equals("ApiKey", StringComparison.OrdinalIgnoreCase) && + parts[1].Equals(_sessionManager.ApiKey, StringComparison.OrdinalIgnoreCase) && + parts[1] == _sessionManager.ApiKey; + } + private async Task CreateClientStream(TcpClient tcpClient, Stream sslStream, CancellationToken cancellationToken) { // check request version @@ -279,40 +289,41 @@ private async Task CreateClientStream(TcpClient tcpClient, Stream await HttpUtil.ParseHeadersAsync(sslStream, cancellationToken) ?? throw new Exception("Connection has been closed before receiving any request."); + int.TryParse(headers.GetValueOrDefault("X-Version", "0"), out var xVersion); + bool.TryParse(headers.GetValueOrDefault("X-BinaryStream", "false"), out var useBinaryStream); + var authorization = headers.GetValueOrDefault("Authorization", string.Empty); + // read api key - var hasPassChecked = false; - if (headers.TryGetValue("Authorization", out var authorization)) + if (!CheckApiKeyAuthorization(authorization)) { - var parts = authorization.Split(' '); - if (parts.Length >= 2 && - parts[0].Equals("ApiKey", StringComparison.OrdinalIgnoreCase) && - parts[1].Equals(_sessionManager.ApiKey, StringComparison.OrdinalIgnoreCase)) - { - var apiKey = parts[1]; - hasPassChecked = apiKey == _sessionManager.ApiKey; - } + // process hello without api key + if (authorization != "ApiKey") + throw new UnauthorizedAccessException(); + + await sslStream.WriteAsync(HttpResponses.GetUnauthorized(), cancellationToken); + return new TcpClientStream(tcpClient, sslStream, streamId); } - if (hasPassChecked) +#pragma warning disable CS0618 // Type or member is obsolete + if (xVersion == 2) { - if (headers.TryGetValue("X-Version", out var xVersion) && int.Parse(xVersion) == 2 && - headers.TryGetValue("X-Secret", out var xSecret)) + if (headers.TryGetValue("X-Secret", out var xSecret) && !string.IsNullOrEmpty(xSecret)) { await sslStream.WriteAsync(HttpResponses.GetOk(), cancellationToken); var secret = Convert.FromBase64String(xSecret); await sslStream.DisposeAsync(); // dispose Ssl - return new TcpClientStream(tcpClient, new BinaryStream(tcpClient.GetStream(), streamId, secret), streamId, ReuseClientStream); + return new TcpClientStream(tcpClient, new CryptoBinaryStream(tcpClient.GetStream(), streamId, secret), streamId, ReuseClientStream); } - } - // process hello without api key - if (authorization == "ApiKey") - { - await sslStream.WriteAsync(HttpResponses.GetUnauthorized(), cancellationToken); - return new TcpClientStream(tcpClient, sslStream, streamId); + return new TcpClientStream(tcpClient, new CryptoBinaryStream(tcpClient.GetStream(), streamId, Convert.FromBase64String(xSecret)), streamId, ReuseClientStream); } +#pragma warning restore CS0618 // Type or member is obsolete - throw new UnauthorizedAccessException(); + // use binary stream only for authenticated clients + await sslStream.WriteAsync(HttpResponses.GetOk(), cancellationToken); + return useBinaryStream + ? new TcpClientStream(tcpClient, new BinaryStream(tcpClient.GetStream(), streamId), streamId, ReuseClientStream) + : new TcpClientStream(tcpClient, sslStream, streamId); } catch (Exception ex) { diff --git a/VpnHood.Server/Session.cs b/VpnHood.Server/Session.cs index f1dd244ad..c721ac420 100644 --- a/VpnHood.Server/Session.cs +++ b/VpnHood.Server/Session.cs @@ -334,8 +334,10 @@ await VhUtil.RunTask( await StreamUtil.WriteJsonAsync(clientStream.Stream, SessionResponse, cancellationToken); // MaxEncryptChunk - if (clientStream.Stream is BinaryStream binaryStream) +#pragma warning disable CS0618 // Type or member is obsolete + if (clientStream.Stream is CryptoBinaryStream binaryStream) binaryStream.MaxEncryptChunk = TunnelDefaults.TcpProxyEncryptChunkCount; +#pragma warning restore CS0618 // Type or member is obsolete // add the connection VhLogger.Instance.LogTrace(GeneralEventId.StreamProxyChannel, diff --git a/VpnHood.Tunneling/Channels/Streams/BinaryStream.cs b/VpnHood.Tunneling/Channels/Streams/BinaryStream.cs index fa291eda2..12da73dae 100644 --- a/VpnHood.Tunneling/Channels/Streams/BinaryStream.cs +++ b/VpnHood.Tunneling/Channels/Streams/BinaryStream.cs @@ -7,7 +7,7 @@ namespace VpnHood.Tunneling.Channels.Streams; public class BinaryStream : ChunkStream { - private const int ChunkHeaderLength = 5; + private const int ChunkHeaderLength = 4; private int _remainingChunkBytes; private bool _disposed; private bool _finished; @@ -17,26 +17,22 @@ public class BinaryStream : ChunkStream private Task _readTask = Task.FromResult(0); private Task _writeTask = Task.CompletedTask; private bool _isConnectionClosed; - private bool _isCurrentReadingChunkEncrypted; - private readonly StreamCryptor _streamCryptor; private readonly byte[] _readChunkHeaderBuffer = new byte[ChunkHeaderLength]; private byte[] _writeBuffer = Array.Empty(); - public long MaxEncryptChunk { get; set; } = long.MaxValue; public override int PreserveWriteBufferLength => ChunkHeaderLength; - public BinaryStream(Stream sourceStream, string streamId, byte[] secret) + public BinaryStream(Stream sourceStream, string streamId) : base(new ReadCacheStream(sourceStream, false, TunnelDefaults.StreamProxyBufferSize), streamId) { - _streamCryptor = StreamCryptor.Create(SourceStream, secret, leaveOpen: true, encryptInGivenBuffer: true); } - private BinaryStream(Stream sourceStream, string streamId, StreamCryptor streamCryptor, int reusedCount) + private BinaryStream(Stream sourceStream, string streamId, int reusedCount) : base(sourceStream, streamId, reusedCount) { - _streamCryptor = streamCryptor; } + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (_disposed) @@ -68,8 +64,7 @@ private async Task ReadInternalAsync(byte[] buffer, int offset, int count, } var bytesToRead = Math.Min(_remainingChunkBytes, count); - var stream = _isCurrentReadingChunkEncrypted ? _streamCryptor : SourceStream; - var bytesRead = await stream.ReadAsync(buffer, offset, bytesToRead, cancellationToken); + var bytesRead = await SourceStream.ReadAsync(buffer, offset, bytesToRead, cancellationToken); if (bytesRead == 0 && count != 0) // count zero is used for checking the connection throw new Exception("BinaryStream has been closed unexpectedly."); @@ -97,7 +92,7 @@ private void CloseByError(Exception ex) private async Task ReadChunkHeaderAsync(CancellationToken cancellationToken) { // read chunk header by cryptor - if (!await StreamUtil.ReadWaitForFillAsync(_streamCryptor, _readChunkHeaderBuffer, 0, _readChunkHeaderBuffer.Length, cancellationToken)) + if (!await StreamUtil.ReadWaitForFillAsync(SourceStream, _readChunkHeaderBuffer, 0, _readChunkHeaderBuffer.Length, cancellationToken)) { if (!_finished && ReadChunkCount > 0) VhLogger.Instance.LogWarning(GeneralEventId.TcpLife, "BinaryStream has been closed unexpectedly."); @@ -107,7 +102,6 @@ private async Task ReadChunkHeaderAsync(CancellationToken cancellationToken // get chunk size var chunkSize = BitConverter.ToInt32(_readChunkHeaderBuffer); - _isCurrentReadingChunkEncrypted = _readChunkHeaderBuffer[4] == 1; return chunkSize; } @@ -143,19 +137,10 @@ private async Task WriteInternalAsync(byte[] buffer, int offset, int count, Canc { try { - var encryptChunk = MaxEncryptChunk > 0; - if (PreserveWriteBuffer) { - // create the chunk header and encrypt it + // create the chunk header BitConverter.GetBytes(count).CopyTo(buffer, offset - ChunkHeaderLength); - buffer[4] = encryptChunk ? (byte)1 : (byte)0; - _streamCryptor.Encrypt(buffer, 0, ChunkHeaderLength); //header should always be encrypted - - // encrypt chunk - if (encryptChunk) - _streamCryptor.Encrypt(buffer, offset, count); - await SourceStream.WriteAsync(buffer, offset - ChunkHeaderLength, ChunkHeaderLength + count, cancellationToken); } else @@ -164,15 +149,11 @@ private async Task WriteInternalAsync(byte[] buffer, int offset, int count, Canc if (size > _writeBuffer.Length) Array.Resize(ref _writeBuffer, size); - // create the chunk header and encrypt it + // create the chunk header BitConverter.GetBytes(count).CopyTo(_writeBuffer, 0); - _writeBuffer[4] = encryptChunk ? (byte)1 : (byte)0; - _streamCryptor.Encrypt(_writeBuffer, 0, ChunkHeaderLength); //header should always be encrypted - // add buffer to chunk and encrypt + // prepend the buffer to chunk Buffer.BlockCopy(buffer, offset, _writeBuffer, ChunkHeaderLength, count); - if (encryptChunk) - _streamCryptor.Encrypt(_writeBuffer, ChunkHeaderLength, count); // Copy write buffer to output await SourceStream.WriteAsync(_writeBuffer, 0, size, cancellationToken); @@ -181,7 +162,6 @@ private async Task WriteInternalAsync(byte[] buffer, int offset, int count, Canc // make sure chunk is sent await SourceStream.FlushAsync(cancellationToken); WroteChunkCount++; - if (MaxEncryptChunk > 0) MaxEncryptChunk--; } catch (Exception ex) { @@ -209,7 +189,7 @@ public override async Task CreateReuse() // reuse if the stream has been closed gracefully if (_finished && !_hasError) - return new BinaryStream(SourceStream, StreamId, _streamCryptor, ReusedCount + 1); + return new BinaryStream(SourceStream, StreamId, ReusedCount + 1); // dispose and throw the ungraceful BinaryStream await base.DisposeAsync(); diff --git a/VpnHood.Tunneling/Channels/Streams/CryptoBinaryStream.cs b/VpnHood.Tunneling/Channels/Streams/CryptoBinaryStream.cs new file mode 100644 index 000000000..a635d835d --- /dev/null +++ b/VpnHood.Tunneling/Channels/Streams/CryptoBinaryStream.cs @@ -0,0 +1,330 @@ +using Microsoft.Extensions.Logging; +using System.Net.Sockets; +using VpnHood.Common.Logging; +using VpnHood.Common.Utils; + +namespace VpnHood.Tunneling.Channels.Streams; + +// this was used for .NET 6 or earlier as .net didn't use hardware acceleration for encryption in android and SslStream was very slow +[Obsolete("Use BinaryStream instead. Deprecated by 3.2.440 or upper.")] +public class CryptoBinaryStream : ChunkStream +{ + private const int ChunkHeaderLength = 5; + private int _remainingChunkBytes; + private bool _disposed; + private bool _finished; + private bool _hasError; + private readonly CancellationTokenSource _readCts = new(); + private readonly CancellationTokenSource _writeCts = new(); + private Task _readTask = Task.FromResult(0); + private Task _writeTask = Task.CompletedTask; + private bool _isConnectionClosed; + private bool _isCurrentReadingChunkEncrypted; + private readonly StreamCryptor _streamCryptor; + private readonly byte[] _readChunkHeaderBuffer = new byte[ChunkHeaderLength]; + private byte[] _writeBuffer = Array.Empty(); + + public long MaxEncryptChunk { get; set; } = long.MaxValue; + public override int PreserveWriteBufferLength => ChunkHeaderLength; + + public CryptoBinaryStream(Stream sourceStream, string streamId, byte[] secret) + : base(new ReadCacheStream(sourceStream, false, TunnelDefaults.StreamProxyBufferSize), streamId) + { + _streamCryptor = StreamCryptor.Create(SourceStream, secret, leaveOpen: true, encryptInGivenBuffer: true); + } + + private CryptoBinaryStream(Stream sourceStream, string streamId, StreamCryptor streamCryptor, int reusedCount) + : base(sourceStream, streamId, reusedCount) + { + _streamCryptor = streamCryptor; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_disposed) + throw new ObjectDisposedException(GetType().Name); + + // Create CancellationToken + using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(_readCts.Token, cancellationToken); + _readTask = ReadInternalAsync(buffer, offset, count, tokenSource.Token); + return await _readTask; // await needed to dispose tokenSource + } + + private async Task ReadInternalAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + try + { + // check is stream has finished + if (_finished) + return 0; + + // If there are no more in the chunks read the next chunk + if (_remainingChunkBytes == 0) + { + _remainingChunkBytes = await ReadChunkHeaderAsync(cancellationToken); + _finished = _remainingChunkBytes == 0; + + // check last chunk + if (_finished) + return 0; + } + + var bytesToRead = Math.Min(_remainingChunkBytes, count); + var stream = _isCurrentReadingChunkEncrypted ? _streamCryptor : SourceStream; + var bytesRead = await stream.ReadAsync(buffer, offset, bytesToRead, cancellationToken); + if (bytesRead == 0 && count != 0) // count zero is used for checking the connection + throw new Exception("BinaryStream has been closed unexpectedly."); + + // update remaining chunk + _remainingChunkBytes -= bytesRead; + if (_remainingChunkBytes == 0) ReadChunkCount++; + return bytesRead; + } + catch (Exception ex) + { + CloseByError(ex); + throw; + } + } + + private void CloseByError(Exception ex) + { + if (_hasError) return; + _hasError = true; + + VhLogger.LogError(GeneralEventId.TcpLife, ex, "Disposing BinaryStream. StreamId: {StreamId}", StreamId); + _ = DisposeAsync(); + } + + private async Task ReadChunkHeaderAsync(CancellationToken cancellationToken) + { + // read chunk header by cryptor + if (!await StreamUtil.ReadWaitForFillAsync(_streamCryptor, _readChunkHeaderBuffer, 0, _readChunkHeaderBuffer.Length, cancellationToken)) + { + if (!_finished && ReadChunkCount > 0) + VhLogger.Instance.LogWarning(GeneralEventId.TcpLife, "BinaryStream has been closed unexpectedly."); + _isConnectionClosed = true; + return 0; + } + + // get chunk size + var chunkSize = BitConverter.ToInt32(_readChunkHeaderBuffer); + _isCurrentReadingChunkEncrypted = _readChunkHeaderBuffer[4] == 1; + return chunkSize; + + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_disposed) + throw new ObjectDisposedException(GetType().Name); + + if (_finished) + throw new SocketException((int)SocketError.ConnectionAborted); + + // Create CancellationToken + using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(_writeCts.Token, cancellationToken); + + // should not write a zero chunk if caller data is zero + try + { + _writeTask = count == 0 + ? SourceStream.WriteAsync(buffer, offset, count, tokenSource.Token) + : WriteInternalAsync(buffer, offset, count, tokenSource.Token); + + await _writeTask; + } + catch (Exception ex) + { + CloseByError(ex); + throw; + } + } + + private async Task WriteInternalAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + try + { + var encryptChunk = MaxEncryptChunk > 0; + + if (PreserveWriteBuffer) + { + // create the chunk header and encrypt it + BitConverter.GetBytes(count).CopyTo(buffer, offset - ChunkHeaderLength); + buffer[4] = encryptChunk ? (byte)1 : (byte)0; + _streamCryptor.Encrypt(buffer, 0, ChunkHeaderLength); //header should always be encrypted + + // encrypt chunk + if (encryptChunk) + _streamCryptor.Encrypt(buffer, offset, count); + + await SourceStream.WriteAsync(buffer, offset - ChunkHeaderLength, ChunkHeaderLength + count, cancellationToken); + } + else + { + var size = ChunkHeaderLength + count; + if (size > _writeBuffer.Length) + Array.Resize(ref _writeBuffer, size); + + // create the chunk header and encrypt it + BitConverter.GetBytes(count).CopyTo(_writeBuffer, 0); + _writeBuffer[4] = encryptChunk ? (byte)1 : (byte)0; + _streamCryptor.Encrypt(_writeBuffer, 0, ChunkHeaderLength); //header should always be encrypted + + // add buffer to chunk and encrypt + Buffer.BlockCopy(buffer, offset, _writeBuffer, ChunkHeaderLength, count); + if (encryptChunk) + _streamCryptor.Encrypt(_writeBuffer, ChunkHeaderLength, count); + + // Copy write buffer to output + await SourceStream.WriteAsync(_writeBuffer, 0, size, cancellationToken); + } + + // make sure chunk is sent + await SourceStream.FlushAsync(cancellationToken); + WroteChunkCount++; + if (MaxEncryptChunk > 0) MaxEncryptChunk--; + } + catch (Exception ex) + { + CloseByError(ex); + throw; + } + } + + private bool _leaveOpen; + public override bool CanReuse => !_hasError && !_isConnectionClosed; + public override async Task CreateReuse() + { + if (_disposed) + throw new ObjectDisposedException(GetType().Name); + + if (_hasError) + throw new InvalidOperationException($"Could not reuse a BinaryStream that has error. StreamId: {StreamId}"); + + if (_isConnectionClosed) + throw new InvalidOperationException($"Could not reuse a BinaryStream that its underling stream has been closed . StreamId: {StreamId}"); + + // Dispose the stream but keep the original stream open + _leaveOpen = true; + await DisposeAsync(); + + // reuse if the stream has been closed gracefully + if (_finished && !_hasError) + return new CryptoBinaryStream(SourceStream, StreamId, _streamCryptor, ReusedCount + 1); + + // dispose and throw the ungraceful BinaryStream + await base.DisposeAsync(); + throw new InvalidOperationException($"Could not reuse a BinaryStream that has not been closed gracefully. StreamId: {StreamId}"); + } + + private async Task CloseStream(CancellationToken cancellationToken) + { + // cancel writing requests. + _readCts.CancelAfter(TunnelDefaults.TcpGracefulTimeout); + _writeCts.CancelAfter(TunnelDefaults.TcpGracefulTimeout); + + // wait for finishing current write + try + { + await _writeTask; + } + catch (Exception ex) + { + VhLogger.LogError(GeneralEventId.TcpLife, ex, + "Final stream write has not been completed gracefully. StreamId: {StreamId}", + StreamId); + return; + } + _writeCts.Dispose(); + + // finish writing current BinaryStream gracefully + try + { + if (PreserveWriteBuffer) + await WriteInternalAsync(new byte[ChunkHeaderLength], ChunkHeaderLength, 0, cancellationToken); + else + await WriteInternalAsync(Array.Empty(), 0, 0, cancellationToken); + + } + catch (Exception ex) + { + VhLogger.LogError(GeneralEventId.TcpLife, ex, + "Could not write the chunk terminator. StreamId: {StreamId}", + StreamId); + return; + } + + // make sure current caller read has been finished gracefully or wait for cancellation time + try + { + await _readTask; + } + catch (Exception ex) + { + VhLogger.LogError(GeneralEventId.TcpLife, ex, + "Final stream read has not been completed gracefully. StreamId: {StreamId}", + StreamId); + return; + } + + _readCts.Dispose(); + + try + { + if (_hasError) + throw new InvalidOperationException("Could not close a BinaryStream due internal error."); + + if (!_finished) + { + var buffer = new byte[500]; + var trashedLength = 0; + while (true) + { + var read = await ReadInternalAsync(buffer, 0, buffer.Length, cancellationToken); + if (read == 0) + break; + + trashedLength += read; + } + + if (trashedLength > 0) + VhLogger.Instance.LogWarning(GeneralEventId.TcpLife, + "Trashing unexpected binary stream data. StreamId: {StreamId}, TrashedLength: {TrashedLength}", + StreamId, trashedLength); + } + } + catch (Exception ex) + { + VhLogger.LogError(GeneralEventId.TcpLife, ex, + "BinaryStream has not been closed gracefully. StreamId: {StreamId}", + StreamId); + } + } + + private readonly AsyncLock _disposeLock = new(); + private ValueTask? _disposeTask; + public override ValueTask DisposeAsync() + { + lock (_disposeLock) + _disposeTask ??= DisposeAsyncCore(); + return _disposeTask.Value; + } + + private async ValueTask DisposeAsyncCore() + { + // prevent other caller requests + _disposed = true; + + // close stream + if (!_hasError && !_isConnectionClosed) + { + // create a new cancellation token for CloseStream + using var cancellationTokenSource = new CancellationTokenSource(TunnelDefaults.TcpGracefulTimeout); + await CloseStream(cancellationTokenSource.Token); + } + + if (!_leaveOpen) + await base.DisposeAsync(); + } +} \ No newline at end of file