Search code examples
grpc.net-8.0

.Net 8 Grpc CLient unable to establish http/2 connection Exception:


Ok I'm at a loss here for what I need to be doing I have a WPF client that I've written in .net 8 which is supposed to connect to a Go gRPC server, but every time I try and connect I'm getting the following exception

 [ALERT] Connection Status: Connection error: Status(StatusCode="Internal", Detail="Error starting gRPC call. HttpRequestException: Requesting HTTP version 2.0 with version policy RequestVersionOrHigher while unable to establish HTTP/2 connection.", DebugException="System.Net.Http.HttpRequestException: Requesting HTTP version 2.0 with version policy RequestVersionOrHigher while unable to establish HTTP/2 connection.")

The issue is that when I wire shark the connection I can clearly see that the TLS 1.2 handshake completes AND it is using HTTP2

ALPN2

I do see that there is later an encrypted alert but that happens several seconds after the exception has been thrown so I'm not sure why.

either way I thought it could be that it's a self signed certificate so I added into the mainwindow()

AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

and for good measures just incase AppContext.SetSwitch("System.Net.Http.UseSocketsHttpHandler", false);

my init channel function looks like

        internal async Task InitializeChannel(string ip)
        {
            Console.WriteLine("Initializing Channel with IP: " + ip);
            Logger.Log("Initializing Channel with IP: " + ip, 1);

            if (!string.IsNullOrEmpty(ip))
            {
                var parts = ip.Split(':');
                if (parts.Length == 2 && int.TryParse(parts[1], out int port))
                {
                    try
                    {

                        
                        var httpClientHandler = new HttpClientHandler
                        {
                            ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
                            {
                                return true; 
                            },
                            SslProtocols = System.Security.Authentication.SslProtocols.Tls12
                            
                        };


                        var loggingHandler = new LoggingHandler(httpClientHandler);

                        
                        var httpClient = new HttpClient(loggingHandler)
                        {
                            DefaultRequestVersion =  HttpVersion.Version20,
                            DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
                        };

                        Logger.Log("Creating GrpcChannel", 0);

                        // Create the GrpcChannel using the HttpClient
                        Channel = GrpcChannel.ForAddress($"https://{parts[0]}:{port}", new GrpcChannelOptions
                        {
                            HttpClient = httpClient
                        });


                    }
                    catch (Exception ex)
                    {
                        Logger.Log($"Exception in InitializeChannel: {ex.Message}", 3);
                        throw;
                    }
                }
                else
                {
                    TeamServerStatus = "Invalid IP format";
                    Logger.Log("Invalid IP format", 2);
                }
            }
            else
            {
                Logger.Log("IP is null or empty", 2);
            }
        }

        // Define the custom HttpMessageHandler with logging
        public class LoggingHandler : DelegatingHandler
        {
            public LoggingHandler(HttpMessageHandler innerHandler)
                : base(innerHandler)
            {
            }

            protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
            {
                // Log the request details
                Logger.Log($"Request: {request.Method} {request.RequestUri}", 0);
                Logger.Log($"Request Headers: {request.Headers}", 0);
                Logger.Log($"Request Version: {request.Version}", 0);

                // Send the request
                var response = await base.SendAsync(request, cancellationToken);

                // Log the response details
                Logger.Log($"Response: {response.StatusCode}", 0);
                Logger.Log($"Response Headers: {response.Headers}", 0);
                Logger.Log($"Response Version: {response.Version}", 0);

                return response;
            }
        }

and here is the log it produces when I attempt to connect

[06/24 08:54] [Debug] TS Connect initatied
[06/24 08:54] [Debug] Attempted to connect to TS
[06/24 08:54] [Info] Initializing Channel with IP: 127.0.0.1:5001
[06/24 08:54] [Debug] Creating GrpcChannel
[06/24 08:54] [Info] Attempting to connect to Team Server at: 127.0.0.1:5001
[06/24 08:54] [Debug] Request: POST https://127.0.0.1:5001/App.Auth/ConnectToTeamServer
[06/24 08:54] [Debug] Request Headers: User-Agent: grpc-dotnet/2.59.0 (.NET 8.0.0; CLR 8.0.0; net8.0; windows; x64)
TE: trailers
grpc-accept-encoding: identity,gzip,deflate

[06/24 08:54] [Debug] Request Version: 2.0
[06/24 08:54] [Critical] Connection error: Status(StatusCode="Internal", Detail="Error starting gRPC call. HttpRequestException: Requesting HTTP version 2.0 with version policy RequestVersionOrHigher while unable to establish HTTP/2 connection.", DebugException="System.Net.Http.HttpRequestException: Requesting HTTP version 2.0 with version policy RequestVersionOrHigher while unable to establish HTTP/2 connection.")
[06/24 08:54] [Debug] Connection Status recived: Connection error: Status(StatusCode="Internal", Detail="Error starting gRPC call. HttpRequestException: Requesting HTTP version 2.0 with version policy RequestVersionOrHigher while unable to establish HTTP/2 connection.", DebugException="System.Net.Http.HttpRequestException: Requesting HTTP version 2.0 with version policy RequestVersionOrHigher while unable to establish HTTP/2 connection."

As best I can tell its not actually http2? but that wouldn't make sense since it has the ALPN. Any help would be greatly appreciated


Solution

  • So it turned out that it was the Go side after all. After a ton of research it turns out that by default the server was not advertising the ALPN so it was failing at the negotiation phase, despite being hard configured to use HTTP2. So what appears was happening was the .Net client was sending a ClienttHello with the h2 ALPN extension but the GO server's ServerHello did not include its ALPN in the response. Since the negotiation failed in this regards gRPC would kick back an error with it can't establish a connection over Http2. This is very similar to what would happen if the certificates were not accepted as well which is why I assumed it was cert based, but we can see that the cert was valid from Wireshark as it did pas the TLS check so something else was failing.

    TL;DR the fix action is you must force the Go server's TLS configuration to include the ALPN of "h2" in the build something like this

        tlsConfig := &tls.Config{
            Certificates: []tls.Certificate{
                cert,
            },
            NextProtos: []string{"h2"},
        }
    return tlsConfig, nil
    }
    

    hope this helps anyone who has run into this stupid issue as well.