feat: wire TLS negotiation into NatsServer accept loop
Integrate TLS support into the server's connection accept path: - Add SslServerAuthenticationOptions and TlsRateLimiter fields to NatsServer - Extract AcceptClientAsync method for TLS negotiation, rate limiting, and TLS state extraction (protocol version, cipher suite, peer certificate) - Add InfoAlreadySent flag to NatsClient to skip redundant INFO when TlsConnectionWrapper already sent it during negotiation - Add TlsServerTests verifying TLS connect+INFO and TLS pub/sub
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NATS.Server.Monitoring;
|
||||
using NATS.Server.Protocol;
|
||||
using NATS.Server.Subscriptions;
|
||||
using NATS.Server.Tls;
|
||||
|
||||
namespace NATS.Server;
|
||||
|
||||
@@ -19,6 +22,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ServerStats _stats = new();
|
||||
private readonly TaskCompletionSource _listeningStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly SslServerAuthenticationOptions? _sslOptions;
|
||||
private readonly TlsRateLimiter? _tlsRateLimiter;
|
||||
private Socket? _listener;
|
||||
private MonitorServer? _monitorServer;
|
||||
private ulong _nextClientId;
|
||||
@@ -48,6 +53,17 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
Port = options.Port,
|
||||
MaxPayload = options.MaxPayload,
|
||||
};
|
||||
|
||||
if (options.HasTls)
|
||||
{
|
||||
_sslOptions = TlsHelper.BuildServerAuthOptions(options);
|
||||
_serverInfo.TlsRequired = !options.AllowNonTls;
|
||||
_serverInfo.TlsAvailable = options.AllowNonTls;
|
||||
_serverInfo.TlsVerify = options.TlsVerify;
|
||||
|
||||
if (options.TlsRateLimit > 0)
|
||||
_tlsRateLimiter = new TlsRateLimiter(options.TlsRateLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken ct)
|
||||
@@ -105,13 +121,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
|
||||
_logger.LogDebug("Client {ClientId} connected from {RemoteEndpoint}", clientId, socket.RemoteEndPoint);
|
||||
|
||||
var clientLogger = _loggerFactory.CreateLogger($"NATS.Server.NatsClient[{clientId}]");
|
||||
var networkStream = new NetworkStream(socket, ownsSocket: false);
|
||||
var client = new NatsClient(clientId, networkStream, socket, _options, _serverInfo, clientLogger, _stats);
|
||||
client.Router = this;
|
||||
_clients[clientId] = client;
|
||||
|
||||
_ = RunClientAsync(client, ct);
|
||||
_ = AcceptClientAsync(socket, clientId, ct);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -120,6 +130,49 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptClientAsync(Socket socket, ulong clientId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Rate limit TLS handshakes
|
||||
if (_tlsRateLimiter != null)
|
||||
await _tlsRateLimiter.WaitAsync(ct);
|
||||
|
||||
var networkStream = new NetworkStream(socket, ownsSocket: false);
|
||||
|
||||
// TLS negotiation (no-op if not configured)
|
||||
var (stream, infoAlreadySent) = await TlsConnectionWrapper.NegotiateAsync(
|
||||
socket, networkStream, _options, _sslOptions, _serverInfo,
|
||||
_loggerFactory.CreateLogger("NATS.Server.Tls"), ct);
|
||||
|
||||
// Extract TLS state
|
||||
TlsConnectionState? tlsState = null;
|
||||
if (stream is SslStream ssl)
|
||||
{
|
||||
tlsState = new TlsConnectionState(
|
||||
ssl.SslProtocol.ToString(),
|
||||
ssl.NegotiatedCipherSuite.ToString(),
|
||||
ssl.RemoteCertificate as X509Certificate2);
|
||||
}
|
||||
|
||||
var clientLogger = _loggerFactory.CreateLogger($"NATS.Server.NatsClient[{clientId}]");
|
||||
var client = new NatsClient(clientId, stream, socket, _options, _serverInfo,
|
||||
clientLogger, _stats);
|
||||
client.Router = this;
|
||||
client.TlsState = tlsState;
|
||||
client.InfoAlreadySent = infoAlreadySent;
|
||||
_clients[clientId] = client;
|
||||
|
||||
await RunClientAsync(client, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to accept client {ClientId}", clientId);
|
||||
try { socket.Shutdown(SocketShutdown.Both); } catch { }
|
||||
socket.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunClientAsync(NatsClient client, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
@@ -199,6 +252,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
{
|
||||
if (_monitorServer != null)
|
||||
_monitorServer.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
_tlsRateLimiter?.Dispose();
|
||||
_listener?.Dispose();
|
||||
foreach (var client in _clients.Values)
|
||||
client.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user