SslStream,禁用会话缓存

SslStream, disable session caching

MSDN documentation

The Framework caches SSL sessions as they are created and attempts to reuse a cached session for a new request, if possible. When attempting to reuse an SSL session, the Framework uses the first element of ClientCertificates (if there is one), or tries to reuse an anonymous sessions if ClientCertificates is empty.

如何禁用此缓存?

目前我在重新连接到服务器时遇到问题(即,第一次连接工作正常,但在尝试重新连接服务器时中断了会话)。重新启动应用程序会有所帮助(但当然仅适用于第一次连接尝试)。我认为问题的根源是缓存。

我用嗅探器检查了数据包,不同之处仅在于客户端问候消息的一个地方:

首次连接服务器(成功):

第二次连接尝试(没有程序重启,失败):

区别似乎只是会话标识符。

P.S。我想避免使用第 3 方 SSL 客户端。有没有合理的解决办法?

这是 this question 来自 ru.Whosebug

的翻译

缓存在 SecureChannel 内部处理 - 内部 class 包装 SSPI 并由 SslStream 使用。我没有在其中看到任何可用于禁用客户端连接的会话缓存的要点。

您可以使用反射清除连接之间的缓存:

var sslAssembly = Assembly.GetAssembly(typeof(SslStream));

var sslSessionCacheClass = sslAssembly.GetType("System.Net.Security.SslSessionsCache");

var cachedCredsInfo = sslSessionCacheClass.GetField("s_CachedCreds", BindingFlags.NonPublic | BindingFlags.Static);
var cachedCreds = (Hashtable)cachedCredsInfo.GetValue(null);

cachedCreds.Clear();

但这是非常糟糕的做法。考虑修复服务器端。

所以我以不同的方式解决了这个问题。我真的不喜欢反射出这个私有静态方法来转储缓存的想法,因为你真的不知道这样做会带来什么;您基本上是在规避封装,这可能会导致无法预料的问题。但实际上,我担心转储缓存和发送请求之前的竞争条件,其他线程进入并建立新会话,因此我的第一个线程无意中劫持了该会话。坏消息...无论如何,这就是我所做的。

我停下来思考是否有某种方法可以隔离进程,然后我的一位 Android 同事回忆起 AppDomains 的可用性。我们都同意旋转一个应该允许 Tcp/Ssl 调用 运行,与其他一切隔离开来。这将允许缓存逻辑保持完整,而不会导致 SSL 会话之间发生冲突。

基本上,我最初将我的 SSL 客户端编写为在一个单独的库内部。然后在该库中,我有一个 public 服务充当该客户端的 proxy/mediator。在应用程序层,我希望能够根据硬件类型在服务(在我的例子中是 HSM 服务)之间切换,所以我将其包装到适配器中并将其与工厂连接。好的,那有什么关系呢?好吧,它只是让干净地完成这个 AppDomain 事情变得更容易,而不会强迫 public 服务(我提到的 proxy/mediator )的任何其他消费者采取这种行为。你不必遵循这个抽象,我只是喜欢在我找到好的抽象示例时分享它们:)

现在,在适配器中,我主要是创建域,而不是直接调用服务。这是ctor:

public VCRklServiceAdapter(
    string hostname, 
    int port,  
    IHsmLogger logger)
{
    Ensure.IsNotNullOrEmpty(hostname, nameof(hostname));
    Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})");
    Ensure.IsNotNull(logger, nameof(logger));

    ClientId = Guid.NewGuid();

    _logger = logger;
    _hostname = hostname;
    _port = port;

    // configure the domain
    _instanceDomain = AppDomain.CreateDomain(
        $"vcrypt_rkl_instance_{ClientId}",
        null, 
        AppDomain.CurrentDomain.SetupInformation);

    // using the configured domain, grab a command instance from which we can
    // marshall in some data
    _rklServiceRuntime = (IRklServiceRuntime)_instanceDomain.CreateInstanceAndUnwrap(
        typeof(VCServiceRuntime).Assembly.FullName,
        typeof(VCServiceRuntime).FullName);
}

所有这一切都是创建一个命名域,我的实际服务将 运行 从中隔离。现在,我遇到的大多数关于如何在域内实际执行的文章都过于简化了它的工作方式。这些示例通常涉及调用 myDomain.DoCallback(() => ...); 这并没有错,但是尝试将数据传入和传出该域可能会出现问题,因为序列化可能会使您陷入困境。简而言之,在 DoCallback() 外部实例化的对象与从 DoCallback 内部调用的对象不同,因为它们是在该域外部创建的(请参阅对象编组)。所以你可能会遇到各种序列化错误。如果 运行 整个操作、输入和输出都可以从内部发生,这不是问题 myDomain.DoCallback() 但如果您需要使用外部参数和 return此 AppDomain 返回到原始域。

我在 SO 上遇到了一个不同的模式,它对我有效并解决了这个问题。看看我的示例 ctor 中的 _rklServiceRuntime =。这实际上是在要求域为您实例化一个对象,以充当该域的代理。这将允许您编组一些对象进出它。这是我对 IRklServiceRuntime:

的实现
public interface IRklServiceRuntime
{       
    RklResponse Run(RklRequest request, string hostname, int port, Guid clientId, IHsmLogger logger);
}

public class VCServiceRuntime : MarshalByRefObject, IRklServiceRuntime
{
    public RklResponse Run(
        RklRequest request, 
        string hostname, 
        int port, 
        Guid clientId,
        IHsmLogger logger)
    {
        Ensure.IsNotNull(request, nameof(request));
        Ensure.IsNotNullOrEmpty(hostname, nameof(hostname));
        Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})");
        Ensure.IsNotNull(logger, nameof(logger));

        // these are set here instead of passed in because they are not
        // serializable
        var clientCert = ApplicationValues.VCClientCertificate;
        var clientCerts = new X509Certificate2Collection(clientCert);

        using (var client = new VCServiceClient(hostname, port, clientCerts, clientId, logger))
        {
            var response = client.RetrieveDeviceKeys(request);
            return response;
        }
    }
}

它继承自 MarshallByRefObject,它允许它跨越 AppDomain 边界,并且有一个方法可以获取您的外部参数并从实例化它的域内执行您的逻辑。

现在回到服务适配器:所有服务适配器现在要做的就是调用 _rklServiceRuntime.Run(...) 并输入必要的可序列化参数。现在,我只需根据需要创建尽可能多的服务适配器实例,它们都 运行 在自己的域中。这对我有用,因为我的 SSL 调用很小而且很简短,并且这些请求是在内部 Web 服务内部发出的,在这种情况下,实例化这样的请求非常重要。这是完整的适配器:

public class VCRklServiceAdapter : IRklService
{
    private readonly string _hostname;
    private readonly int _port;
    private readonly IHsmLogger _logger;
    private readonly AppDomain _instanceDomain;
    private readonly IRklServiceRuntime _rklServiceRuntime;

    public Guid ClientId { get; }

    public VCRklServiceAdapter(
        string hostname, 
        int port,  
        IHsmLogger logger)
    {
        Ensure.IsNotNullOrEmpty(hostname, nameof(hostname));
        Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})");
        Ensure.IsNotNull(logger, nameof(logger));

        ClientId = Guid.NewGuid();

        _logger = logger;
        _hostname = hostname;
        _port = port;

        // configure the domain
        _instanceDomain = AppDomain.CreateDomain(
            $"vc_rkl_instance_{ClientId}",
            null, 
            AppDomain.CurrentDomain.SetupInformation);

        // using the configured domain, grab a command instance from which we can
        // marshall in some data
        _rklServiceRuntime = (IRklServiceRuntime)_instanceDomain.CreateInstanceAndUnwrap(
            typeof(VCServiceRuntime).Assembly.FullName,
            typeof(VCServiceRuntime).FullName);
    }

    public RklResponse GetKeys(RklRequest rklRequest)
    {
        Ensure.IsNotNull(rklRequest, nameof(rklRequest));

        var response = _rklServiceRuntime.Run(
            rklRequest, 
            _hostname, 
            _port, 
            ClientId, 
            _logger);

        return response;
    }

    /// <summary>
    /// Releases unmanaged and - optionally - managed resources.
    /// </summary>
    public void Dispose()
    {
        AppDomain.Unload(_instanceDomain);
    }
}

注意处理方法。不要忘记卸载域。该服务实现了实现 IDisposable 的 IRklService,因此当我使用它时,它与 using 语句一起使用。

这似乎有点做作,但事实并非如此,现在逻辑将 运行 在它自己的域中,隔离,因此缓存逻辑保持完整但没有问题。比干预 SSLSessionCache 好多了!

请原谅任何命名不一致的地方,因为我在写完 post 后快速清理了实际名称。希望这对某人有所帮助!