using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging; using StellaOps.Router.Common.Models; using StellaOps.Router.Transport.Tcp; namespace StellaOps.Router.Transport.Tls; /// /// Represents a TLS-secured connection to a microservice. /// public sealed class TlsConnection : IAsyncDisposable { private readonly TcpClient _client; private readonly SslStream _sslStream; private readonly SemaphoreSlim _writeLock = new(1, 1); private readonly TlsTransportOptions _options; private readonly ILogger _logger; private readonly CancellationTokenSource _connectionCts = new(); private bool _disposed; /// /// Gets the connection ID. /// public string ConnectionId { get; } /// /// Gets the remote endpoint as a string. /// public string RemoteEndpoint { get; } /// /// Gets a value indicating whether the connection is active. /// public bool IsConnected => _client.Connected && !_disposed; /// /// Gets the connection state. /// public ConnectionState? State { get; set; } /// /// Gets the cancellation token for this connection. /// public CancellationToken ConnectionToken => _connectionCts.Token; /// /// Gets the remote certificate (if mTLS). /// public X509Certificate? RemoteCertificate => _sslStream.RemoteCertificate; /// /// Gets the peer identity extracted from the certificate. /// public string? PeerIdentity { get; } /// /// Event raised when a frame is received. /// public event Action? OnFrameReceived; /// /// Event raised when the connection is closed. /// public event Action? OnDisconnected; /// /// Initializes a new instance of the class. /// public TlsConnection( string connectionId, TcpClient client, SslStream sslStream, TlsTransportOptions options, ILogger logger) { ConnectionId = connectionId; _client = client; _sslStream = sslStream; _options = options; _logger = logger; RemoteEndpoint = client.Client.RemoteEndPoint?.ToString() ?? "unknown"; // Extract peer identity from certificate if (_sslStream.RemoteCertificate is X509Certificate2 cert) { PeerIdentity = ExtractIdentityFromCertificate(cert); } // Configure socket options client.ReceiveBufferSize = options.ReceiveBufferSize; client.SendBufferSize = options.SendBufferSize; client.NoDelay = true; } /// /// Extracts identity from a certificate. /// private static string? ExtractIdentityFromCertificate(X509Certificate2 cert) { // Try to get Common Name (CN) var cn = cert.GetNameInfo(X509NameType.SimpleName, forIssuer: false); if (!string.IsNullOrEmpty(cn)) { return cn; } // Fallback to subject return cert.Subject; } /// /// Starts the read loop to receive frames. /// /// Cancellation token. public async Task ReadLoopAsync(CancellationToken cancellationToken) { using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, _connectionCts.Token); Exception? disconnectException = null; try { while (!linkedCts.Token.IsCancellationRequested) { var frame = await FrameProtocol.ReadFrameAsync( _sslStream, _options.MaxFrameSize, linkedCts.Token); if (frame is null) { _logger.LogDebug("TLS connection {ConnectionId} closed by remote", ConnectionId); break; } OnFrameReceived?.Invoke(this, frame); } } catch (OperationCanceledException) { // Expected on shutdown } catch (IOException ex) when (ex.InnerException is SocketException) { disconnectException = ex; _logger.LogDebug(ex, "TLS connection {ConnectionId} socket error", ConnectionId); } catch (Exception ex) { disconnectException = ex; _logger.LogWarning(ex, "TLS connection {ConnectionId} read error", ConnectionId); } OnDisconnected?.Invoke(this, disconnectException); } /// /// Writes a frame to the connection. /// /// The frame to write. /// Cancellation token. public async Task WriteFrameAsync(Frame frame, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, this); await _writeLock.WaitAsync(cancellationToken); try { await FrameProtocol.WriteFrameAsync(_sslStream, frame, cancellationToken); await _sslStream.FlushAsync(cancellationToken); } finally { _writeLock.Release(); } } /// /// Closes the connection. /// public void Close() { if (_disposed) return; try { _connectionCts.Cancel(); _sslStream.Close(); _client.Close(); } catch (Exception ex) { _logger.LogDebug(ex, "Error closing TLS connection {ConnectionId}", ConnectionId); } } /// public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; try { await _connectionCts.CancelAsync(); } catch { // Ignore } await _sslStream.DisposeAsync(); _client.Dispose(); _writeLock.Dispose(); _connectionCts.Dispose(); } }