HTTP/3 interop fixes (#41298)
authorCory Nelson <phrosty@gmail.com>
Tue, 25 Aug 2020 17:00:47 +0000 (10:00 -0700)
committerGitHub <noreply@github.com>
Tue, 25 Aug 2020 17:00:47 +0000 (10:00 -0700)
Support DNS-based quic connections for SNI.
Support multiple ALPN values.
Update H3 ALPN from "h3" to "h3-29".

17 files changed:
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/Implementations/Mock/MockConnection.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/Implementations/MsQuic/Internal/MsQuicSession.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/Implementations/MsQuic/Internal/QuicExceptionHelpers.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/Implementations/MsQuic/MsQuicConnection.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/Implementations/MsQuic/MsQuicStream.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/Implementations/QuicConnectionProvider.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/Interop/MsQuicStatusCodes.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/QuicClientConnectionOptions.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/QuicConnection.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Quic/QuicException.cs
src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs

index b97f487..07995dd 100644 (file)
@@ -14,7 +14,7 @@ namespace System.Net.Quic.Implementations.Mock
     {
         private readonly bool _isClient;
         private bool _disposed;
-        private IPEndPoint? _remoteEndPoint;
+        private EndPoint? _remoteEndPoint;
         private IPEndPoint? _localEndPoint;
         private object _syncObject = new object();
         private Socket? _socket;
@@ -24,7 +24,7 @@ namespace System.Net.Quic.Implementations.Mock
         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;
@@ -59,7 +59,7 @@ namespace System.Net.Quic.Implementations.Mock
 
         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();
 
index 5c62f1d..d90b7be 100644 (file)
@@ -2,6 +2,8 @@
 // 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;
@@ -318,26 +320,72 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
             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.");
 
index 962e804..8b0e28e 100644 (file)
@@ -1,5 +1,7 @@
 // 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
 {
@@ -21,7 +23,7 @@ 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);
             }
@@ -36,7 +38,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
             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);
@@ -49,7 +51,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
         {
             if (!_opened)
             {
-                OpenSession(options.ServerAuthenticationOptions!.ApplicationProtocols![0].Protocol.ToArray(),
+                OpenSession(options.ServerAuthenticationOptions!.ApplicationProtocols!,
                                     (ushort)options.MaxBidirectionalStreams,
                                     (ushort)options.MaxUnidirectionalStreams);
             }
index a68cbbe..673e766 100644 (file)
@@ -10,8 +10,13 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
         {
             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);
+        }
     }
 }
index 632c755..fb9a286 100644 (file)
@@ -6,6 +6,7 @@ using System.Diagnostics;
 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;
@@ -32,7 +33,7 @@ namespace System.Net.Quic.Implementations.MsQuic
 
         // Endpoint to either connect to or the endpoint already accepted.
         private IPEndPoint? _localEndPoint;
-        private readonly IPEndPoint _remoteEndPoint;
+        private readonly EndPoint _remoteEndPoint;
 
         private SslApplicationProtocol _negotiatedAlpnProtocol;
 
@@ -92,7 +93,7 @@ namespace System.Net.Quic.Implementations.MsQuic
             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;
 
@@ -155,7 +156,9 @@ namespace System.Net.Quic.Implementations.MsQuic
         {
             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();
@@ -243,12 +246,28 @@ namespace System.Net.Quic.Implementations.MsQuic
         {
             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);
index 48a1357..3b01df5 100644 (file)
@@ -225,6 +225,11 @@ namespace System.Net.Quic.Implementations.MsQuic
                 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)
@@ -474,6 +479,11 @@ namespace System.Net.Quic.Implementations.MsQuic
 
         private uint HandleEvent(ref StreamEvent evt)
         {
+            if (NetEventSource.IsEnabled)
+            {
+                NetEventSource.Info(this, $"[{GetHashCode()}] handling event '{evt.Type}'.");
+            }
+
             uint status = MsQuicStatusCodes.Success;
 
             try
index da51164..7febd47 100644 (file)
@@ -12,7 +12,7 @@ namespace System.Net.Quic.Implementations
 
         internal abstract IPEndPoint LocalEndPoint { get; }
 
-        internal abstract IPEndPoint RemoteEndPoint { get; }
+        internal abstract EndPoint RemoteEndPoint { get; }
 
         internal abstract ValueTask ConnectAsync(CancellationToken cancellationToken = default);
 
index af7fd30..c48fac8 100644 (file)
@@ -26,13 +26,16 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
             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)
@@ -53,11 +56,15 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                     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}"
                 };
             }
         }
@@ -79,9 +86,15 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
             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)
@@ -103,10 +116,16 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                     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}"
                 };
             }
         }
index c4ad344..9852644 100644 (file)
@@ -24,7 +24,7 @@ namespace System.Net.Quic
         /// <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
index 999ce7a..6c82a23 100644 (file)
@@ -22,13 +22,13 @@ namespace System.Net.Quic
         /// <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 })
         {
         }
@@ -50,7 +50,7 @@ namespace System.Net.Quic
 
         public IPEndPoint LocalEndPoint => _provider.LocalEndPoint;
 
-        public IPEndPoint RemoteEndPoint => _provider.RemoteEndPoint;
+        public EndPoint RemoteEndPoint => _provider.RemoteEndPoint;
 
         public SslApplicationProtocol NegotiatedApplicationProtocol => _provider.NegotiatedApplicationProtocol;
 
index 16a26d8..9a2351e 100644 (file)
@@ -1,12 +1,17 @@
 // 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)
         {
         }
     }
index 06f8c46..e203bf6 100644 (file)
@@ -28,7 +28,7 @@ namespace System.Net.Test.Common
             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
             };
index 841cce7..726b8ba 100644 (file)
@@ -150,34 +150,19 @@ namespace System.Net.Http
             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)
index 9ee2bdf..ffde369 100644 (file)
@@ -17,7 +17,7 @@ namespace System.Net.Http
     {
         // 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.
index 3199975..27829f0 100644 (file)
@@ -743,6 +743,11 @@ namespace System.Net.Http
 
                 _recvBuffer.Discard(bytesRead);
 
+                if (NetEventSource.IsEnabled)
+                {
+                    Trace($"Received frame {frameType} of length {payloadLength}.");
+                }
+
                 switch ((Http3FrameType)frameType)
                 {
                     case Http3FrameType.Headers:
index cf921be..fdfbaf9 100644 (file)
@@ -826,7 +826,7 @@ namespace System.Net.Http
                 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
                 {
index 27d4b77..7bb317c 100644 (file)
@@ -21,10 +21,33 @@ namespace System.Net.Http.Functional.Tests
     {
         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);
+        }
     }
 }