File: ConnectionTracker.cs
Web Access
Project: src\aspnetcore\src\Servers\Kestrel\Transport.DirectTls\src\Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.csproj (Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls;
 
/// <summary>
/// Listener-level counters bounding the pre-Kestrel work a single listener will do. One tracker is shared by
/// every pump of a listener, so its counters cap that listener across all its pump threads. Today it tracks
/// in-flight TLS handshakes (see <see cref="TryAcquireHandshake"/>); the type is intentionally named generally
/// so additional per-listener limits can be added as further acquire/release pairs.
/// <para>
/// On this transport the TLS handshake runs on the pump thread <em>before</em> the connection is surfaced to
/// Kestrel, so Kestrel's own <c>MaxConcurrentConnections</c> limit - which only counts accepted connections -
/// cannot bound a handshake flood. The handshake counter closes that gap: a pump calls
/// <see cref="TryAcquireHandshake"/> for a freshly accepted connection and, if the cap is already reached,
/// rejects it (closes the fd) before starting the handshake. Each admitted connection frees its slot via
/// <see cref="ReleaseHandshake"/> when it is dropped during handshake or handed to Kestrel. This mirrors
/// Kestrel's <c>MaxConcurrentConnections</c> reject behavior, but pre-handshake so a flood cannot spend server
/// crypto.
/// </para>
/// </summary>
internal sealed class ConnectionTracker
{
    /// <summary>
    /// A shared tracker with no configured limits. Every acquire succeeds and every release is a no-op, so a
    /// disabled tracker holds no state and is safe to share across all listeners and pumps. Used as the default
    /// when no cap is configured, avoiding a null field and per-connection null checks on the accept path.
    /// </summary>
    public static readonly ConnectionTracker Unlimited = new(maxHandshakes: null);
 
    private readonly bool _handshakeLimitEnabled;
    private readonly long _maxHandshakes;
    private long _handshakeCount;
 
    /// <param name="maxHandshakes">
    /// The maximum number of simultaneously in-flight handshakes, or <see langword="null"/> (or a non-positive
    /// value) to leave handshakes unlimited, in which case <see cref="TryAcquireHandshake"/> and
    /// <see cref="ReleaseHandshake"/> are no-ops.
    /// </param>
    public ConnectionTracker(long? maxHandshakes)
    {
        _handshakeLimitEnabled = maxHandshakes is > 0;
        _maxHandshakes = maxHandshakes ?? 0;
    }
 
    /// <summary>
    /// The number of connections currently counted against the handshake cap (handshaking or sitting in the
    /// ready channel awaiting <c>AcceptAsync</c>). Always 0 when the handshake limit is disabled. For tests and
    /// diagnostics.
    /// </summary>
    internal long HandshakeCount => Interlocked.Read(ref _handshakeCount);
 
    /// <summary>
    /// Tries to admit a freshly accepted connection into the handshake stage. Returns <see langword="true"/>
    /// and reserves a slot when the in-flight handshake count is below the cap; returns <see langword="false"/>
    /// when the cap is reached, in which case the caller must reject the connection. Always returns
    /// <see langword="true"/> when the handshake limit is disabled. Lock-free; safe to call concurrently from
    /// every pump.
    /// </summary>
    public bool TryAcquireHandshake()
    {
        if (!_handshakeLimitEnabled)
        {
            return true;
        }
 
        // Reserve a slot optimistically, then hand it straight back if that put us over the cap. Each concurrent
        // caller sees a distinct post-increment value, so at most _maxHandshakes callers ever observe a value
        // within the cap - the admission limit stays exact even though a rejected caller briefly overshoots it.
        if (Interlocked.Increment(ref _handshakeCount) <= _maxHandshakes)
        {
            return true;
        }
 
        Interlocked.Decrement(ref _handshakeCount);
        return false;
    }
 
    /// <summary>
    /// Frees a slot reserved by a prior successful <see cref="TryAcquireHandshake"/>. Called once per admitted
    /// connection when it leaves the pre-Kestrel pipeline - dropped during handshake or handed to Kestrel via
    /// <c>AcceptAsync</c>. A no-op when the handshake limit is disabled.
    /// </summary>
    public void ReleaseHandshake()
    {
        if (!_handshakeLimitEnabled)
        {
            return;
        }
 
        var updated = Interlocked.Decrement(ref _handshakeCount);
        Debug.Assert(updated >= 0, "ConnectionTracker released more handshake slots than were acquired.");
    }
}