Repository navigation
Accept IPv6 literal hosts in the managed HttpListener - #135937
pcshrosbree wants to merge 1 commit into
Conversation
On Linux and macOS, adding a prefix such as http://[::1]:8080/ makes Start throw HttpListenerException "Invalid port in prefix", while http.sys on Windows accepts it. The managed implementation finds the port by searching for the first ':' after the scheme, and for an IPv6 literal, that is a colon inside the brackets. The same search also splits IPv6 request hosts at a colon inside the brackets, so a request with "Host: [::1]:8080" gets 400 Bad Request before it reaches the application. The same split also breaks absolute-form requests for [::1] and HTTP/1.0 requests without a Host header that arrive over IPv6. For a host that starts with '[', find the port separator by looking for the first ':' after the closing ']'. Apply this rule in HttpEndPointManager.AddPrefixInternal, ListenerPrefix.Parse and HttpListenerRequest.FinishInitialization. Hosts that do not start with '[' are split as before, and the port checks are unchanged. A malformed bracketed host such as [::1]x can now be reported as an invalid host rather than an invalid port; both are 400. ListenerPrefix now normalizes IPv6 literals to match Uri.Host, so a prefix containing [0:0:0:0:0:0:0:1] matches a request for [::1]. Scope IDs remain unsupported and now report an invalid-host error. The [::] literal is now rejected with error 50, like 0.0.0.0, instead of 400. Binding needed no change: Dns.GetHostAddresses returns a literal such as "[::1]" without a lookup, so the listener binds to ::1. This change does not make localhost listen on both address families on Linux, as it does on Windows. Add tests for IPv6 loopback prefixes and requests, and for prefixes that must still be rejected: bad ports, an unclosed '[' and scoped literals. The full System.Net.HttpListener.Tests project passes in Release on linux-x64 and osx-arm64; on windows-x64 the new tests pass, and the one failing test fails on main too. Fixes dotnet#34399
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @karelz, @dotnet/ncl |
|
I'm inclined to say won't-fix here. This never worked so it's not a regression, and a clear better alternative exists - Kestrel. See also
|
Thanks for looking at it. Some background on why I opened it. This fix isn't something my own work needs. I hit the bug while writing docs for the OpenTelemetry Prometheus exporter that is built on Your points stand: it never worked on Linux or macOS, so it isn't a regression, and Kestrel is the recommended alternative. Would you prefer me to close this PR? If so, closing #34399 as not planned would tell the next person who finds it where things stand. |
Fixes #34399
On Linux and macOS,
HttpListenerrejects a prefix with an IPv6 literal such ashttp://[::1]:8080/:StartthrowsHttpListenerException (400): Invalid port in prefix. On Windows, http.sys accepts it.What was wrong
The managed implementation finds the port of a host by looking for the first
:. In an IPv6 literal that colon is inside the brackets. This affected both prefix registration and request parsing:HttpEndPointManager.AddPrefixInternalandListenerPrefix.Parseread:1]:8080as the port.HttpListenerRequest.FinishInitializationalso splits the host at the first:. A request withHost: [::1]:8080, which is what a client sends to an[::1]prefix, therefore gets400 Bad Requestbefore it reaches the application. The same problem affects absolute-form requests for[::1]and HTTP/1.0 requests without a Host header that arrive over IPv6.What changed
[, the new helperListenerPrefix.IndexOfPortSeparatorfinds the port separator at the first:after the closing]. All three methods above use it. Hosts that don't start with[are split as before, and the port checks are unchanged.ListenerPrefixnormalizes IPv6 literals to matchUri.Host, so a prefix containing[0:0:0:0:0:0:0:1]matches a request for[::1].[fe80::1%25eth0], are still rejected.Uri.Hostdrops the scope ID while binding needs it, so supporting them needs a decision of its own.Dns.GetHostAddressesreturns the address of an IP literal without a DNS lookup, so the listener binds to::1.localhostlistens on both address families on Windows but not on Linux, is a separate change and isn't in this PR.A malformed bracketed host such as
[::1]x:5can now be reported as an invalid host instead of an invalid port; both errors return 400. The[::]literal now fails with error 50, like0.0.0.0, instead of an invalid-port error.On Windows the library uses http.sys and doesn't compile the three source files I changed.
Tests
HttpListenerIPv6Testschecks that both[::1]and[0:0:0:0:0:0:0:1]prefixes serve requests over IPv6 loopback with the expectedRequest.Url,UserHostNameandLocalEndPoint. It also checksRequest.Urlfor origin-form, absolute-form and HTTP/1.0 requests, and that a+prefix accepts a request withHost: [::1]:port.[::1]added toHosts_TestData, so the existing prefix registration tests run with it.[, or a scoped literal are still rejected.[::1]prefix run only where::1can be bound. With http.sys, they also require elevation, because registering an IP literal prefix may require it. The wildcard test uses the existing check for wildcard prefixes.I ran the whole
System.Net.HttpListener.Testsproject (Release) on main, on main with the new tests, and with this change:In the
main + new testsruns, the 12 failures are from the new IPv6 tests; the new rejected-prefix tests pass. The Windows runs were elevated. The Windows failure isExtendedProtectionSelectorDelegate_IncreasesPolicyBetweenNtlmLegs_AuthenticationFails, which fails on that machine on main as well; all the new tests pass there. I also undid each part of the fix on its own and checked that at least one of the new tests fails each time.Note
This PR was prepared with AI assistance (Anthropic Claude, checked by OpenAI Codex) at my request. I reviewed it before opening it.