Support DNS-based quic connections for SNI.
Support multiple ALPN values.
Update H3 ALPN from "h3" to "h3-29".
{
private readonly bool _isClient;
private bool _disposed;
- private IPEndPoint? _remoteEndPoint;
+ private EndPoint? _remoteEndPoint;
private IPEndPoint? _localEndPoint;
private object _syncObject = new object();
private Socket? _socket;
private long _nextOutboundUnidirectionalStream;
// Constructor for outbound connections
- internal MockConnection(IPEndPoint? remoteEndPoint, SslClientAuthenticationOptions? sslClientAuthenticationOptions, IPEndPoint? localEndPoint = null)
+ internal MockConnection(EndPoint? remoteEndPoint, SslClientAuthenticationOptions? sslClientAuthenticationOptions, IPEndPoint? localEndPoint = null)
{
_remoteEndPoint = remoteEndPoint;
_localEndPoint = localEndPoint;
internal override IPEndPoint LocalEndPoint => new IPEndPoint(_localEndPoint!.Address, _localEndPoint.Port);
- internal override IPEndPoint RemoteEndPoint => new IPEndPoint(_remoteEndPoint!.Address, _remoteEndPoint.Port);
+ internal override EndPoint RemoteEndPoint => _remoteEndPoint!;
internal override SslApplicationProtocol NegotiatedApplicationProtocol => throw new NotImplementedException();
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
+using System.Buffers;
+using System.Collections.Generic;
using System.IO;
using System.Net.Security;
using System.Runtime.InteropServices;
return secConfig;
}
- public unsafe IntPtr SessionOpen(byte[] alpn)
+ public unsafe IntPtr SessionOpen(List<SslApplicationProtocol> alpnProtocols)
{
- IntPtr sessionPtr = IntPtr.Zero;
- uint status;
+ if (alpnProtocols.Count == 1)
+ {
+ return SessionOpen(alpnProtocols[0]);
+ }
+
+ var memoryHandles = ArrayPool<MemoryHandle>.Shared.Rent(alpnProtocols.Count);
+ var quicBuffers = ArrayPool<MsQuicNativeMethods.QuicBuffer>.Shared.Rent(alpnProtocols.Count);
- fixed (byte* pAlpn = alpn)
+ try
{
- var alpnBuffer = new MsQuicNativeMethods.QuicBuffer
+ for (int i = 0; i < alpnProtocols.Count; ++i)
{
- Length = (uint)alpn.Length,
- Buffer = pAlpn
- };
-
- status = SessionOpenDelegate(
- _registrationContext,
- &alpnBuffer,
- 1,
- IntPtr.Zero,
- ref sessionPtr);
+ ReadOnlyMemory<byte> alpnProtocol = alpnProtocols[i].Protocol;
+ MemoryHandle h = alpnProtocol.Pin();
+
+ memoryHandles[i] = h;
+ quicBuffers[i].Buffer = (byte*)h.Pointer;
+ quicBuffers[i].Length = (uint)alpnProtocol.Length;
+ }
+
+ IntPtr session;
+
+ fixed (MsQuicNativeMethods.QuicBuffer* pQuicBuffers = quicBuffers)
+ {
+ session = SessionOpen(pQuicBuffers, (uint)alpnProtocols.Count);
+ }
+
+ ArrayPool<MsQuicNativeMethods.QuicBuffer>.Shared.Return(quicBuffers);
+ ArrayPool<MemoryHandle>.Shared.Return(memoryHandles);
+
+ return session;
}
+ finally
+ {
+ foreach (MemoryHandle handle in memoryHandles)
+ {
+ handle.Dispose();
+ }
+ }
+ }
+
+ private unsafe IntPtr SessionOpen(SslApplicationProtocol alpnProtocol)
+ {
+ ReadOnlyMemory<byte> memory = alpnProtocol.Protocol;
+ using MemoryHandle h = memory.Pin();
+
+ var quicBuffer = new MsQuicNativeMethods.QuicBuffer()
+ {
+ Buffer = (byte*)h.Pointer,
+ Length = (uint)memory.Length
+ };
+
+ return SessionOpen(&quicBuffer, 1);
+ }
+
+ private unsafe IntPtr SessionOpen(MsQuicNativeMethods.QuicBuffer *alpnBuffers, uint bufferCount)
+ {
+ IntPtr sessionPtr = IntPtr.Zero;
+ uint status = SessionOpenDelegate(
+ _registrationContext,
+ alpnBuffers,
+ bufferCount,
+ IntPtr.Zero,
+ ref sessionPtr);
QuicExceptionHelpers.ThrowIfFailed(status, "Could not open session.");
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Generic;
+using System.Net.Security;
namespace System.Net.Quic.Implementations.MsQuic.Internal
{
{
if (!_opened)
{
- OpenSession(options.ClientAuthenticationOptions!.ApplicationProtocols![0].Protocol.ToArray(),
+ OpenSession(options.ClientAuthenticationOptions!.ApplicationProtocols!,
(ushort)options.MaxBidirectionalStreams,
(ushort)options.MaxUnidirectionalStreams);
}
return connectionPtr;
}
- private void OpenSession(byte[] alpn, ushort bidirectionalStreamCount, ushort undirectionalStreamCount)
+ private void OpenSession(List<SslApplicationProtocol> alpn, ushort bidirectionalStreamCount, ushort undirectionalStreamCount)
{
_opened = true;
_nativeObjPtr = MsQuicApi.Api.SessionOpen(alpn);
{
if (!_opened)
{
- OpenSession(options.ServerAuthenticationOptions!.ApplicationProtocols![0].Protocol.ToArray(),
+ OpenSession(options.ServerAuthenticationOptions!.ApplicationProtocols!,
(ushort)options.MaxBidirectionalStreams,
(ushort)options.MaxUnidirectionalStreams);
}
{
if (!MsQuicStatusHelper.SuccessfulStatusCode(status))
{
- throw new QuicException($"{message} Error Code: {MsQuicStatusCodes.GetError(status)}");
+ throw CreateExceptionForHResult(status, message, innerException);
}
}
+
+ internal static Exception CreateExceptionForHResult(uint status, string? message = null, Exception? innerException = null)
+ {
+ return new QuicException($"{message} Error Code: {MsQuicStatusCodes.GetError(status)}", innerException);
+ }
}
}
using System.IO;
using System.Net.Quic.Implementations.MsQuic.Internal;
using System.Net.Security;
+using System.Net.Sockets;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
// Endpoint to either connect to or the endpoint already accepted.
private IPEndPoint? _localEndPoint;
- private readonly IPEndPoint _remoteEndPoint;
+ private readonly EndPoint _remoteEndPoint;
private SslApplicationProtocol _negotiatedAlpnProtocol;
MsQuicParameterHelpers.SetSecurityConfig(MsQuicApi.Api, _ptr, (uint)QUIC_PARAM_LEVEL.CONNECTION, (uint)QUIC_PARAM_CONN.SEC_CONFIG, _securityConfig!.NativeObjPtr);
}
- internal override IPEndPoint RemoteEndPoint => new IPEndPoint(_remoteEndPoint.Address, _remoteEndPoint.Port);
+ internal override EndPoint RemoteEndPoint => _remoteEndPoint;
internal override SslApplicationProtocol NegotiatedApplicationProtocol => _negotiatedAlpnProtocol;
{
if (!_connected)
{
- _connectTcs.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new IOException("Connection has been shutdown.")));
+ uint hresult = connectionEvent.Data.ShutdownInitiatedByTransport.Status;
+ Exception ex = QuicExceptionHelpers.CreateExceptionForHResult(hresult, "Connection has been shutdown by transport.");
+ _connectTcs.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(ex));
}
_acceptQueue.Writer.Complete();
{
ThrowIfDisposed();
+ (string address, int port) = _remoteEndPoint switch
+ {
+ DnsEndPoint dnsEp => (dnsEp.Host, dnsEp.Port),
+ IPEndPoint ipEp => (ipEp.Address.ToString(), ipEp.Port),
+ _ => throw new Exception($"Unsupported remote endpoint type '{_remoteEndPoint.GetType()}'.")
+ };
+
+ // values taken from https://github.com/microsoft/msquic/blob/main/docs/api/ConnectionStart.md
+ int af = _remoteEndPoint.AddressFamily switch
+ {
+ AddressFamily.Unspecified => 0,
+ AddressFamily.InterNetwork => 2,
+ AddressFamily.InterNetworkV6 => 23,
+ _ => throw new Exception($"Unsupported address family of '{_remoteEndPoint.AddressFamily}' for remote endpoint.")
+ };
+
QuicExceptionHelpers.ThrowIfFailed(
MsQuicApi.Api.ConnectionStartDelegate(
_ptr,
- (ushort)_remoteEndPoint.AddressFamily,
- _remoteEndPoint.Address.ToString(),
- (ushort)_remoteEndPoint.Port),
+ (ushort)af,
+ address,
+ (ushort)port),
"Failed to connect to peer.");
return new ValueTask(_connectTcs.Task);
throw new InvalidOperationException("Reading is not allowed on stream.");
}
+ if (NetEventSource.IsEnabled)
+ {
+ NetEventSource.Info(this, $"[{GetHashCode()}] reading into Memory of '{destination.Length}' bytes.");
+ }
+
lock (_sync)
{
if (_readState == ReadState.ReadsCompleted)
private uint HandleEvent(ref StreamEvent evt)
{
+ if (NetEventSource.IsEnabled)
+ {
+ NetEventSource.Info(this, $"[{GetHashCode()}] handling event '{evt.Type}'.");
+ }
+
uint status = MsQuicStatusCodes.Success;
try
internal abstract IPEndPoint LocalEndPoint { get; }
- internal abstract IPEndPoint RemoteEndPoint { get; }
+ internal abstract EndPoint RemoteEndPoint { get; }
internal abstract ValueTask ConnectAsync(CancellationToken cancellationToken = default);
internal const uint HandshakeFailure = 0x80410000;
internal const uint Aborted = 0x80004004;
internal const uint AddressInUse = 0x80072740;
- internal const uint ConnectionTimeout = 0x800704CF;
- internal const uint ConnectionIdle = 0x800704D4;
- internal const uint InternalError = 0x80004005;
- internal const uint ServerBusy = 0x800704C9;
- internal const uint ProtocolError = 0x800704CD;
+ internal const uint ConnectionTimeout = 0x80410006;
+ internal const uint ConnectionIdle = 0x80410005;
internal const uint HostUnreachable = 0x800704D0;
+ internal const uint InternalError = 0x80410003;
+ internal const uint ConnectionRefused = 0x800704C9;
+ internal const uint ProtocolError = 0x80410004;
internal const uint VerNegError = 0x80410001;
+ internal const uint TlsError = 0x80072B18;
+ internal const uint UserCanceled = 0x80410002;
+ internal const uint AlpnNegotiationFailure = 0x80410007;
// TODO return better error messages here.
public static string GetError(uint status)
AddressInUse => "ADDRESS_IN_USE",
ConnectionTimeout => "CONNECTION_TIMEOUT",
ConnectionIdle => "CONNECTION_IDLE",
+ HostUnreachable => "UNREACHABLE",
InternalError => "INTERNAL_ERROR",
- ServerBusy => "SERVER_BUSY",
+ ConnectionRefused => "CONNECTION_REFUSED",
ProtocolError => "PROTOCOL_ERROR",
VerNegError => "VER_NEG_ERROR",
- _ => status.ToString()
+ TlsError => "TLS_ERROR",
+ UserCanceled => "USER_CANCELED",
+ AlpnNegotiationFailure => "ALPN_NEG_FAILURE",
+ _ => $"0x{status:X8}"
};
}
}
internal const uint ConnectionTimeout = 110;
internal const uint ConnectionIdle = 200000011;
internal const uint InternalError = 200000012;
- internal const uint ServerBusy = 200000007;
+ internal const uint ConnectionRefused = 200000007;
internal const uint ProtocolError = 200000013;
internal const uint VerNegError = 200000014;
+ internal const uint EpollError = 200000015;
+ internal const uint DnsResolutionError = 200000016;
+ internal const uint SocketError = 200000017;
+ internal const uint TlsError = 200000018;
+ internal const uint UserCanceled = 200000019;
+ internal const uint AlpnNegotiationFailure = 200000020;
// TODO return better error messages here.
public static string GetError(uint status)
ConnectionTimeout => "CONNECTION_TIMEOUT",
ConnectionIdle => "CONNECTION_IDLE",
InternalError => "INTERNAL_ERROR",
- ServerBusy => "SERVER_BUSY",
+ ConnectionRefused => "CONNECTION_REFUSED",
ProtocolError => "PROTOCOL_ERROR",
VerNegError => "VER_NEG_ERROR",
- _ => status.ToString()
+ EpollError => "EPOLL_ERROR",
+ DnsResolutionError => "DNS_RESOLUTION_ERROR",
+ SocketError => "SOCKET_ERROR",
+ TlsError => "TLS_ERROR",
+ UserCanceled => "USER_CANCELED",
+ AlpnNegotiationFailure => "ALPN_NEG_FAILURE",
+ _ => $"0x{status:X8}"
};
}
}
/// <summary>
/// The endpoint to connect to.
/// </summary>
- public IPEndPoint? RemoteEndPoint { get; set; }
+ public EndPoint? RemoteEndPoint { get; set; }
/// <summary>
/// Limit on the number of bidirectional streams the peer connection can create
/// <param name="remoteEndPoint">The remote endpoint to connect to.</param>
/// <param name="sslClientAuthenticationOptions">TLS options</param>
/// <param name="localEndPoint">The local endpoint to connect from.</param>
- public QuicConnection(IPEndPoint remoteEndPoint, SslClientAuthenticationOptions? sslClientAuthenticationOptions, IPEndPoint? localEndPoint = null)
+ public QuicConnection(EndPoint remoteEndPoint, SslClientAuthenticationOptions? sslClientAuthenticationOptions, IPEndPoint? localEndPoint = null)
: this(QuicImplementationProviders.Default, remoteEndPoint, sslClientAuthenticationOptions, localEndPoint)
{
}
// !!! TEMPORARY: Remove "implementationProvider" before shipping
- public QuicConnection(QuicImplementationProvider implementationProvider, IPEndPoint remoteEndPoint, SslClientAuthenticationOptions? sslClientAuthenticationOptions, IPEndPoint? localEndPoint = null)
+ public QuicConnection(QuicImplementationProvider implementationProvider, EndPoint remoteEndPoint, SslClientAuthenticationOptions? sslClientAuthenticationOptions, IPEndPoint? localEndPoint = null)
: this(implementationProvider, new QuicClientConnectionOptions() { RemoteEndPoint = remoteEndPoint, ClientAuthenticationOptions = sslClientAuthenticationOptions, LocalEndPoint = localEndPoint })
{
}
public IPEndPoint LocalEndPoint => _provider.LocalEndPoint;
- public IPEndPoint RemoteEndPoint => _provider.RemoteEndPoint;
+ public EndPoint RemoteEndPoint => _provider.RemoteEndPoint;
public SslApplicationProtocol NegotiatedApplicationProtocol => _provider.NegotiatedApplicationProtocol;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable enable
namespace System.Net.Quic
{
internal class QuicException : Exception
{
- public QuicException(string message)
- : base (message)
+ public QuicException(string? message)
+ : base(message)
+ {
+ }
+ public QuicException(string? message, Exception? innerException)
+ : base(message, innerException)
{
}
}
var sslOpts = new SslServerAuthenticationOptions
{
EnabledSslProtocols = options.SslProtocols,
- ApplicationProtocols = new List<SslApplicationProtocol> { new SslApplicationProtocol("h3") },
+ ApplicationProtocols = new List<SslApplicationProtocol> { new SslApplicationProtocol("h3-29") },
//ServerCertificate = _cert,
ClientCertificateRequired = false
};
return sslStream;
}
- public static async ValueTask<QuicConnection> ConnectQuicAsync(string host, int port, SslClientAuthenticationOptions? clientAuthenticationOptions, CancellationToken cancellationToken)
+ public static async ValueTask<QuicConnection> ConnectQuicAsync(DnsEndPoint endPoint, SslClientAuthenticationOptions? clientAuthenticationOptions, CancellationToken cancellationToken)
{
- IPAddress[] addresses = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false);
- Exception? lastException = null;
-
- foreach (IPAddress address in addresses)
+ QuicConnection con = new QuicConnection(endPoint, clientAuthenticationOptions);
+ try
{
- QuicConnection con = new QuicConnection(new IPEndPoint(address, port), clientAuthenticationOptions);
- try
- {
- await con.ConnectAsync(cancellationToken).ConfigureAwait(false);
- return con;
- }
- // TODO: it would be great to catch a specific exception here... QUIC implementation dependent.
- catch (Exception ex) when (!(ex is OperationCanceledException))
- {
- con.Dispose();
- lastException = ex;
- }
+ await con.ConnectAsync(cancellationToken).ConfigureAwait(false);
+ return con;
}
-
- if (lastException != null)
+ catch (Exception ex)
{
- throw CreateWrappedException(lastException, host, port, cancellationToken);
+ con.Dispose();
+ throw CreateWrappedException(ex, endPoint.Host, endPoint.Port, cancellationToken);
}
-
- // TODO: find correct exception to throw here.
- throw new HttpRequestException("No host found.");
}
private static Exception CreateWrappedException(Exception error, string host, int port, CancellationToken cancellationToken)
{
// TODO: once HTTP/3 is standardized, create APIs for these.
public static readonly Version HttpVersion30 = new Version(3, 0);
- public static readonly SslApplicationProtocol Http3ApplicationProtocol = new SslApplicationProtocol("h3");
+ public static readonly SslApplicationProtocol Http3ApplicationProtocol = new SslApplicationProtocol("h3-29");
/// <summary>
/// If we receive a settings frame larger than this, tear down the connection with an error.
_recvBuffer.Discard(bytesRead);
+ if (NetEventSource.IsEnabled)
+ {
+ Trace($"Received frame {frameType} of length {payloadLength}.");
+ }
+
switch ((Http3FrameType)frameType)
{
case Http3FrameType.Headers:
QuicConnection quicConnection;
try
{
- quicConnection = await ConnectHelper.ConnectQuicAsync(authority.IdnHost, authority.Port, _sslOptionsHttp3, cancellationToken).ConfigureAwait(false);
+ quicConnection = await ConnectHelper.ConnectQuicAsync(new DnsEndPoint(authority.IdnHost, authority.Port), _sslOptionsHttp3, cancellationToken).ConfigureAwait(false);
}
catch
{
{
protected override Version UseVersion => HttpVersion30;
- public static bool SupportsAlpn => PlatformDetection.SupportsAlpn;
-
public HttpClientHandlerTest_Http3(ITestOutputHelper output) : base(output)
{
}
+
+ /// <summary>
+ /// These are public interop test servers for various QUIC and HTTP/3 implementations,
+ /// taken from https://github.com/quicwg/base-drafts/wiki/Implementations
+ /// </summary>
+ [OuterLoop]
+ [Theory]
+ [InlineData("https://quic.rocks:4433/")] // Chromium
+ [InlineData("https://www.litespeedtech.com/")] // LiteSpeed
+ [InlineData("https://quic.tech:8443/")] // Cloudflare
+ public async Task Public_Interop_Success(string uri)
+ {
+ using HttpClient client = CreateHttpClient();
+ using HttpRequestMessage request = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri(uri, UriKind.Absolute),
+ Version = HttpVersion30,
+ VersionPolicy = HttpVersionPolicy.RequestVersionExact
+ };
+ using HttpResponseMessage response = await client.SendAsync(request).TimeoutAfter(20_000);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(3, response.Version.Major);
+ }
}
}