Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connecting to IPAddress.Any via HTTP/3 #108259

Closed
amcasey opened this issue Sep 25, 2024 · 15 comments
Closed

Connecting to IPAddress.Any via HTTP/3 #108259

amcasey opened this issue Sep 25, 2024 · 15 comments

Comments

@amcasey
Copy link
Member

amcasey commented Sep 25, 2024

Description

This is more of a question than a bug, so please let me know if this is by design.

We did find #95487 (which points to microsoft/msquic#2704) but, frankly, it was hard to follow and apply to our scenario.

TL;DR: When a (kestrel) server is listening on IPAddress.Any, HttpClient can connect to localhost with HTTP/1.1 and HTTP/2.0, but not with HTTP/3.0. You seem to have to either use 127.0.0.1 or switch to IPv6Any (which also works for 1.1 and 2.0).

This seems to happen in both dotnet 8.0 and 9.0.

Reproduction Steps

Server

using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(IPAddress.Any, 5001, listenOptions => // H3 to localhost doesn't work
    //options.Listen(IPAddress.IPv6Any, 5001, listenOptions => // H3 to localhost works
    {
        listenOptions.UseHttps();
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});

var app = builder.Build();
 
app.MapGet("/", () => "Hello, world!");

app.Run();

Client

var handler = new SocketsHttpHandler(); 
handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true;

using var client = new HttpClient(handler)
{
    //BaseAddress = new Uri("https://127.0.0.1:5001"), // Works with ipv4 and ipv6
    //BaseAddress = new Uri("https://[::1]:5001"), // Works with ipv6
    BaseAddress = new Uri("https://localhost:5001"), // Works with ipv6
    DefaultRequestVersion = new Version(3, 0),
    DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
};

try
{
    Console.WriteLine("Requesting /");
    var response = await client.GetAsync("/");
    response.EnsureSuccessStatusCode();

    var data = await response.Content.ReadAsStringAsync();
    Console.WriteLine(data);
}
catch (HttpRequestException e)
{
    Console.WriteLine($"Request error: {e.Message}");
}

Expected behavior

Requesting /
Hello, world!

Actual behavior

Requesting /
Request error: Application layer protocol negotiation error was encountered. (localhost:5001)

Regression?

Not since 8.0, anyway. Possibly not even a bug.

Known Workarounds

In kestrel, it's more convenient to call ListenAnyIP anyway.

Configuration

Win11 x64. I appear to have dotnet 8.0.108 and 9.0.100-rc.1.24452.12, though the client may have been using whatever preview of 9.0 is in VS and the server would have been running aspnetcore main. I don't believe the specific dotnet versions matter, though the OS might.

Other information

No response

@amcasey
Copy link
Member Author

amcasey commented Sep 25, 2024

FYI @BrennanConroy

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Sep 25, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@rzikm
Copy link
Member

rzikm commented Sep 26, 2024

I expect that problem is that localhost resolves to IPv6 address at the DNS layer

var addresses = Dns.GetHostAddresses("localhost");
foreach (var addr in addresses)
{
    System.Console.WriteLine(addr);
}

prints on my machine

::1
127.0.0.1

We attempt a connection only via the first IP address returned from Dns.GetHostAddresses

// Given just a ServerName to connect to, MsQuic would also use the first address after the resolution
// (https://github.com/microsoft/msquic/issues/1181) and it would not return a well-known error code
// for resolution failures we could rely on. By doing the resolution in managed code, we can guarantee
// that a SocketException will surface to the user if the name resolution fails.
IPAddress[] addresses = await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
if (addresses.Length == 0)
{
throw new SocketException((int)SocketError.HostNotFound);
}
address = addresses[0];

Related: #82404

A possible workaround can be disabling IPv6, either system-wide, or for .NET only via AppCtx switch or env variable

@amcasey
Copy link
Member Author

amcasey commented Sep 26, 2024

Assuming I understand correctly that #82404 tracks (indirectly) making HTTP/3.0 consistent with HTTP/2.0, that answers my question and we can close this issue. Thanks!

@amcasey amcasey closed this as completed Sep 26, 2024
@dotnet-policy-service dotnet-policy-service bot removed the untriaged New issue has not been triaged by the area owner label Sep 26, 2024
@wfurt
Copy link
Member

wfurt commented Sep 26, 2024

while HTTP 1&2 can connect there is still probably performance penalty for trying IPv6 and falling back to IP4. You should probably investigate @amcasey. As mentioned above HTTP 3 does not have the fall-back at the moment.

@amcasey
Copy link
Member Author

amcasey commented Sep 26, 2024

@wfurt Sorry, I'm not following. I agree that reaching ipv4 as a fallback is probably slower than reaching ipv6 directly, but I'm not sure what you want me to investigate.

@wfurt
Copy link
Member

wfurt commented Sep 26, 2024

can you try packet capture for the case when it seems to work with HTTP 1? e.g. when you bind on IPAddress.Any does it connect on the first try or are there two separate attempts?

@amcasey
Copy link
Member Author

amcasey commented Sep 26, 2024

Sure, though I did provide complete repro steps. 😛

@amcasey
Copy link
Member Author

amcasey commented Sep 26, 2024

Two attempts:
image

@wfurt
Copy link
Member

wfurt commented Sep 26, 2024

yes, this is what I expected. So even if it "works" the setup is not correct IMHO. And the fallback masks the misconfiguration.

@amcasey
Copy link
Member Author

amcasey commented Sep 26, 2024

Are you proposing removing the fallback from the socket client?

@wfurt
Copy link
Member

wfurt commented Sep 26, 2024

no. That is still useful IMHO in general case and it should work for HTTP3 as well at some point. But we should be careful with asp.net docs and examples, perhaps make some explicit comments. We had several cases in the past when people complained about slowness caused by this very issue.

@amcasey
Copy link
Member Author

amcasey commented Sep 26, 2024

So the proposed guidance is that, if your server's OS supports ipv6, your server should listen on IPv6Any to reduce the number of retries?

@wfurt
Copy link
Member

wfurt commented Sep 26, 2024

yes, that would be great. It will always work on Windows and since IPv6 is prevalent now it will likely work on most Unixes as well.

@amcasey
Copy link
Member Author

amcasey commented Sep 30, 2024

Updated the comment and suggested an analyzer.

@github-actions github-actions bot locked and limited conversation to collaborators Oct 31, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants