| File: System\Net\Security\TlsSession.OpenSsl.cs | Web Access |
| Project: src\runtime\src\libraries\System.Net.Security\src\System.Net.Security.csproj (System.Net.Security) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Authentication; using Microsoft.Win32.SafeHandles; namespace System.Net.Security { public partial class TlsSession { // When true, socket-bound I/O delegates ciphertext directly to OpenSSL via // SSL_set_fd / SSL_do_handshake / SSL_read / SSL_write, bypassing the // managed ProcessHandshake/Encrypt/Decrypt loop and its scratch buffers. private bool _useFdMode; // Socket that will be bound to OpenSSL once server options are resolved. // Non-null only in the deferred-server socket-bound flow: the session // returns nativeBindingEnabled=false so the managed pre-fetch loop can // parse the ClientHello and surface NeedsServerOptions. OnServerContextSet // then activates fd-mode with the peeked bytes replayed via the socket BIO. private SafeSocketHandle? _pendingFdSocket; // Socket-replay BIO populated by TryPeekClientHello via BioPeekTlsFrame. Holds // the ClientHello record buffered off the fd; the same BIO becomes the SSL's // read BIO once OnServerContextSet transfers ownership to _options.PreallocatedReadBio. // Freed by OnDispose if the session is disposed before that handoff runs. private SafeBioHandle? _peekBio; // Bind the socket directly to the SSL object so OpenSSL drives ciphertext // I/O itself. AllocateSslHandle inspects options.SocketHandle and skips // the ManagedSpanBio installation when set. With fd-mode active, no // managed Socket wrapper is needed - OpenSSL calls recv/send on the fd. // // Server sessions created without options up front (SNI-driven callback) // cannot go fd-mode immediately: SSL_set_fd would let OpenSSL consume the // ClientHello before managed code sees SNI. Defer binding until // OnServerContextSet runs; until then the session uses the managed loop. partial void EnableNativeSocketBinding(SafeSocketHandle socket, ref bool nativeBindingEnabled) { // Defer binding to fd-mode whenever we need to see the ClientHello managed-side // first: either because options aren't set yet (SNI-driven callback flow) or // because ClientHello capture is on (the default). Callers can disable capture // via the System.Net.Security.CaptureClientHello AppContext switch to skip the // peek and take the SSL_set_fd fast path when options are already supplied. if (_context!.IsServer && (!_hasServerOptions || LocalAppContextSwitches.CaptureClientHello)) { _pendingFdSocket = socket; nativeBindingEnabled = false; return; } _options.SocketHandle = socket; _useFdMode = true; nativeBindingEnabled = true; } // Activated when the caller supplies server options in response to // NeedsServerOptions. In the deferred socket-bound flow, hand the peeked // ClientHello bytes to a socket-replay BIO so OpenSSL sees them, then // switch subsequent Handshake/Read/Write calls onto the fd-mode fast path. partial void OnServerContextSet() { if (_pendingFdSocket is null) { return; } if (_peekBio is not null) { // Native-peek path (TryPeekClientHello ran): hand the pre-populated // BIO to the options bag; SafeSslHandle.Create adopts it as the read // BIO. No managed byte[] copy, no ReplayPrefix. We keep _peekBio // referenced after transfer so GetClientHelloBytes can span the // retained peek buffer via BioGetReplayPrefix; the SafeBioHandle stays // valid (SSL* is the real owner, our reference DangerousReleases // the parent on session Dispose()). _options.PreallocatedReadBio = _peekBio; } else if (_socketInBuffer.ActiveLength > 0) { // Legacy managed pre-fetch path: still exercised e.g. by a caller // driving ProcessHandshake directly rather than Handshake(). Copy the // peeked bytes so SafeSslHandle.Create's BioNewSocketReplay-with-prefix // branch can seed the replay BIO. _options.ReplayPrefix = _socketInBuffer.ActiveReadOnlySpan.ToArray(); } _options.SocketHandle = _pendingFdSocket; _pendingFdSocket = null; // Discard any managed pre-fetch bytes; ownership transfers to the native BIO now. if (_socketInBuffer.ActiveLength > 0) { _socketInBuffer.Discard(_socketInBuffer.ActiveLength); } _useFdMode = true; } // Native ClientHello peek for the deferred socket-bound flow AND the always-capture // path (server session with options up front + CaptureClientHello switch on). // Creates a socket-replay BIO on the fd, buffers a full TLS record via // BioPeekTlsFrame, parses via TlsFrameHelper, and populates ClientHelloInfo / // TargetHostName from the SNI extension. In the deferred flow returns // NeedsServerOptions so the caller resolves via SetContext; in the capture // flow transfers the peek BIO to the pending SSL* and falls through to fd-mode. // Either way the BIO stays alive so GetClientHelloBytes can span its retained // prefix until Dispose(). partial void TryPeekClientHello(ref TlsOperationStatus? result) { if (_pendingFdSocket is null) { return; } // Deferred flow: caller hasn't resolved NeedsServerOptions yet - re-surface // without re-reading the fd. Not reached on the capture-only path because we // transition to fd-mode on the same TryPeekClientHello call that populates it. if (_clientHelloInfo is not null && !_hasServerOptions) { result = TlsOperationStatus.NeedsTlsContext; return; } if (_peekBio is null) { _peekBio = Interop.Ssl.BioNewSocketReplay(_pendingFdSocket, ReadOnlySpan<byte>.Empty); if (_peekBio.IsInvalid) { _peekBio.Dispose(); _peekBio = null; throw Interop.OpenSsl.CreateSslException(SR.net_ssl_read_bio_failed_error); } } unsafe { int rc = Interop.Ssl.BioPeekTlsFrame(_peekBio, out byte* framePtr, out int frameLen); if (rc == 0) { // Need more bytes off the socket. Caller polls SelectRead and retries. result = TlsOperationStatus.NeedMoreData; return; } if (rc < 0) { throw new IOException(SR.net_ssl_read_bio_failed_error); } ReadOnlySpan<byte> frame = new ReadOnlySpan<byte>(framePtr, frameLen); SslClientHelloInfo? parsed = TryParseClientHello(frame, out _); if (parsed is null) { // TlsFrameHelper couldn't parse the record as a ClientHello. throw new IOException(SR.net_io_decrypt); } _clientHelloInfo = parsed; if (!string.IsNullOrEmpty(parsed.Value.ServerName)) { _sessionTargetHost = parsed.Value.ServerName; } } if (!_hasServerOptions) { // Deferred / SNI-callback flow: caller inspects ClientHelloInfo and // resolves via SetContext; OnServerContextSet then transfers the // peek BIO to _options.PreallocatedReadBio. result = TlsOperationStatus.NeedsTlsContext; return; } // Always-capture flow: options are already supplied. Transfer the peek BIO // to the pending SSL* now and drive the fast-path handshake step so the // caller sees the same behavior as the pre-capture SSL_set_fd path. // See OnServerContextSet: _peekBio stays referenced after transfer. _options.PreallocatedReadBio = _peekBio; _options.SocketHandle = _pendingFdSocket; _pendingFdSocket = null; _useFdMode = true; TryFastHandshake(ref result); } // Called from TlsSession.Dispose. If the caller disposed the session before // OnServerContextSet transferred the peek BIO to _options.PreallocatedReadBio, // release it here so the native buffer / fd reference are freed. partial void OnDispose() { _peekBio?.Dispose(); _peekBio = null; } // Returns a ReadOnlySpan over the socket-replay BIO's retained peek buffer. // Valid as long as the SafeBioHandle is open. Consumers reach here through // GetClientHelloBytes, which does ThrowIfDisposed() first. partial void TryGetNativeClientHelloBytes(ref ReadOnlySpan<byte> bytes) { if (_peekBio is null || _peekBio.IsInvalid) { return; } unsafe { if (Interop.Ssl.BioGetReplayPrefix(_peekBio, out byte* ptr, out int len) == 1 && len > 0) { bytes = new ReadOnlySpan<byte>(ptr, len); } } } partial void TryFastHandshake(ref TlsOperationStatus? result) { if (!_useFdMode) { return; } SafeSslHandle ssl = EnsureFdSslHandle(); int ret = Interop.Ssl.SslDoHandshake(ssl, out Interop.Ssl.SslErrorCode err); if (ret == 1) { OnHandshakeCompleted(); result = TlsOperationStatus.Complete; return; } result = MapSslError(err, SR.net_ssl_handshake_failed_error); } partial void TryFastRead(Span<byte> buffer, ref int bytesRead, ref TlsOperationStatus? result) { if (!_useFdMode) { return; } if (buffer.IsEmpty) { result = TlsOperationStatus.Complete; return; } SafeSslHandle ssl = (SafeSslHandle)_securityContext!; int ret = Interop.Ssl.SslRead(ssl, ref MemoryMarshal.GetReference(buffer), buffer.Length, out Interop.Ssl.SslErrorCode err); if (ret > 0) { bytesRead = ret; result = TlsOperationStatus.Complete; return; } result = MapSslError(err, SR.net_ssl_decrypt_failed); } partial void TryFastWrite(ReadOnlySpan<byte> buffer, ref int bytesWritten, ref TlsOperationStatus? result) { if (!_useFdMode) { return; } if (buffer.IsEmpty) { result = TlsOperationStatus.Complete; return; } SafeSslHandle ssl = (SafeSslHandle)_securityContext!; int ret = Interop.Ssl.SslWrite(ssl, ref MemoryMarshal.GetReference(buffer), buffer.Length, out Interop.Ssl.SslErrorCode err); if (ret > 0) { bytesWritten = ret; result = TlsOperationStatus.Complete; return; } result = MapSslError(err, SR.net_ssl_encrypt_failed); } private SafeSslHandle EnsureFdSslHandle() { if (_securityContext is SafeSslHandle existing && !existing.IsInvalid) { return existing; } SafeSslHandle handle = Interop.OpenSsl.AllocateSslHandle(_options); _securityContext = handle; return handle; } // Translates a non-progress SslErrorCode into either a status the caller // can act on (NeedMoreData / DestinationTooSmall / Closed) or, for a real // failure, an AuthenticationException whose inner SslException carries the // OpenSSL error-queue reason (formatted via SR-backed template). The template // is picked per operation so exceptions surface consistently with SslStream's // OpenSSL error handling. private static TlsOperationStatus MapSslError(Interop.Ssl.SslErrorCode error, string sslErrorTemplate) { return error switch { Interop.Ssl.SslErrorCode.SSL_ERROR_WANT_READ => TlsOperationStatus.NeedMoreData, Interop.Ssl.SslErrorCode.SSL_ERROR_WANT_WRITE => TlsOperationStatus.DestinationTooSmall, Interop.Ssl.SslErrorCode.SSL_ERROR_ZERO_RETURN => TlsOperationStatus.Closed, _ => throw new AuthenticationException(SR.net_auth_SSPI, Interop.OpenSsl.CreateSslException(sslErrorTemplate)), }; } } }