File: System\Net\Quic\Internal\MsQuicConfiguration.Cache.cs
Web Access
Project: src\src\libraries\System.Net.Quic\src\System.Net.Quic.csproj (System.Net.Quic)
// 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;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Security.Authentication;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using Microsoft.Quic;
 
namespace System.Net.Quic;
 
internal static partial class MsQuicConfiguration
{
    private const int CheckExpiredModulo = 32;
 
    private const string DisableCacheEnvironmentVariable = "DOTNET_SYSTEM_NET_QUIC_DISABLE_CONFIGURATION_CACHE";
    private const string DisableCacheCtxSwitch = "System.Net.Quic.DisableConfigurationCache";
 
    internal static bool ConfigurationCacheEnabled { get; } = GetConfigurationCacheEnabled();
 
    private static bool GetConfigurationCacheEnabled()
    {
        // AppContext switch takes precedence
        if (AppContext.TryGetSwitch(DisableCacheCtxSwitch, out bool value))
        {
            return !value;
        }
        // check environment variable second
        else if (Environment.GetEnvironmentVariable(DisableCacheEnvironmentVariable) is string envVar)
        {
            return !(envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
        }
 
        // enabled by default
        return true;
    }
    private static readonly ConcurrentDictionary<CacheKey, MsQuicConfigurationSafeHandle> s_configurationCache = new();
 
    private readonly struct CacheKey : IEquatable<CacheKey>
    {
        public readonly List<byte[]> CertificateThumbprints;
        public readonly QUIC_CREDENTIAL_FLAGS Flags;
        public readonly QUIC_SETTINGS Settings;
        public readonly List<SslApplicationProtocol> ApplicationProtocols;
        public readonly QUIC_ALLOWED_CIPHER_SUITE_FLAGS AllowedCipherSuites;
 
        public CacheKey(QUIC_SETTINGS settings, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, ReadOnlyCollection<X509Certificate2>? intermediates, List<SslApplicationProtocol> alpnProtocols, QUIC_ALLOWED_CIPHER_SUITE_FLAGS allowedCipherSuites)
        {
            CertificateThumbprints = certificate == null ? new List<byte[]>() : new List<byte[]> { certificate.GetCertHash() };
 
            if (intermediates != null)
            {
                foreach (X509Certificate2 intermediate in intermediates)
                {
                    CertificateThumbprints.Add(intermediate.GetCertHash());
                }
            }
 
            Flags = flags;
            Settings = settings;
            // make defensive copy to prevent modification (the list comes from user code)
            ApplicationProtocols = new List<SslApplicationProtocol>(alpnProtocols);
            AllowedCipherSuites = allowedCipherSuites;
        }
 
        public override bool Equals(object? obj) => obj is CacheKey key && Equals(key);
 
        public bool Equals(CacheKey other)
        {
            if (CertificateThumbprints.Count != other.CertificateThumbprints.Count)
            {
                return false;
            }
 
            for (int i = 0; i < CertificateThumbprints.Count; i++)
            {
                if (!CertificateThumbprints[i].AsSpan().SequenceEqual(other.CertificateThumbprints[i]))
                {
                    return false;
                }
            }
 
            if (ApplicationProtocols.Count != other.ApplicationProtocols.Count)
            {
                return false;
            }
 
            for (int i = 0; i < ApplicationProtocols.Count; i++)
            {
                if (ApplicationProtocols[i] != other.ApplicationProtocols[i])
                {
                    return false;
                }
            }
 
            return
                Flags == other.Flags &&
                Settings.Equals(other.Settings) &&
                AllowedCipherSuites == other.AllowedCipherSuites;
        }
 
        public override int GetHashCode()
        {
            HashCode hash = default;
 
            foreach (var thumbprint in CertificateThumbprints)
            {
                hash.AddBytes(thumbprint);
            }
 
            hash.Add(Flags);
            hash.Add(Settings);
 
            foreach (var protocol in ApplicationProtocols)
            {
                hash.AddBytes(protocol.Protocol.Span);
            }
 
            hash.Add(AllowedCipherSuites);
 
            return hash.ToHashCode();
        }
    }
 
    private static MsQuicConfigurationSafeHandle GetCachedCredentialOrCreate(QUIC_SETTINGS settings, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, ReadOnlyCollection<X509Certificate2>? intermediates, List<SslApplicationProtocol> alpnProtocols, QUIC_ALLOWED_CIPHER_SUITE_FLAGS allowedCipherSuites)
    {
        CacheKey key = new CacheKey(settings, flags, certificate, intermediates, alpnProtocols, allowedCipherSuites);
 
        MsQuicConfigurationSafeHandle? handle;
 
        if (s_configurationCache.TryGetValue(key, out handle) && handle.TryAddRentCount())
        {
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(null, $"Found cached MsQuicConfiguration: {handle}.");
            }
            return handle;
        }
 
        // if we get here, the handle is either not in the cache, or we lost the race between
        // TryAddRentCount on this thread and MarkForDispose on another thread doing cache cleanup.
        // In either case, we need to create a new handle.
 
        if (NetEventSource.Log.IsEnabled())
        {
            NetEventSource.Info(null, $"MsQuicConfiguration not found in cache, creating new.");
        }
 
        handle = CreateInternal(settings, flags, certificate, intermediates, alpnProtocols, allowedCipherSuites);
        handle.TryAddRentCount(); // we are the first renter
 
        MsQuicConfigurationSafeHandle cached;
        do
        {
            cached = s_configurationCache.GetOrAdd(key, handle);
        }
        // If we get the same handle back, we successfully added it to the cache and we are done.
        // If we get a different handle back, we need to increase the rent count.
        // If we fail to add the rent count, then the existing/cached handle is in process of
        // being removed from the cache and we can try again, eventually either succeeding to add our
        // new handle or getting a fresh handle inserted by another thread meanwhile.
        while (cached != handle && !cached.TryAddRentCount());
 
        if (cached != handle)
        {
            // we lost a race with another thread to insert new handle into the cache
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(null, $"Discarding MsQuicConfiguration {handle} (preferring cached {cached}).");
            }
 
            // First dispose decrements the rent count we added before attempting the cache insertion
            // and second closes the handle
            handle.Dispose();
            handle.Dispose();
            Debug.Assert(handle.IsClosed);
 
            return cached;
        }
 
        // we added a new handle, check if we need to cleanup
        var count = s_configurationCache.Count;
        if (count % CheckExpiredModulo == 0)
        {
            // let only one thread perform cleanup at a time
            lock (s_configurationCache)
            {
                // check again, if another thread just cleaned up (and cached count went down) we are unlikely
                // to clean anything
                if (s_configurationCache.Count >= count)
                {
                    CleanupCache();
                }
            }
        }
 
        return handle;
    }
 
    private static void CleanupCache()
    {
        if (NetEventSource.Log.IsEnabled())
        {
            NetEventSource.Info(null, $"Cleaning up MsQuicConfiguration cache, current size: {s_configurationCache.Count}.");
        }
 
        foreach ((CacheKey key, MsQuicConfigurationSafeHandle handle) in s_configurationCache)
        {
            if (!handle.TryMarkForDispose())
            {
                // handle in use
                continue;
            }
 
            // the handle is not in use and has been marked such that no new rents can be added.
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(null, $"Removing cached MsQuicConfiguration {handle}.");
            }
 
            bool removed = s_configurationCache.TryRemove(key, out _);
            Debug.Assert(removed);
            handle.Dispose();
            Debug.Assert(handle.IsClosed);
        }
 
        if (NetEventSource.Log.IsEnabled())
        {
            NetEventSource.Info(null, $"Cleaning up MsQuicConfiguration cache, new size: {s_configurationCache.Count}.");
        }
    }
}