SocketsHttpHandler: Add workaround for ipv6 LLA address#28971
SocketsHttpHandler: Add workaround for ipv6 LLA address#28971caesar-chen merged 4 commits intodotnet:masterfrom
Conversation
| // Since scope is mandatory for LLA, we will use uri.IdnHost for IPv6 LLA address. | ||
| // From RFC 4291, IPv6 LLA has formate like: fe80::/10. | ||
| bool isNonLLAIPv6Address = (uri.HostNameType == UriHostNameType.IPv6 && | ||
| uri.IdnHost.IndexOf("fe80") != 0); |
There was a problem hiding this comment.
Alternatively, we can use IPAddress.IsIPv6LinkLocal Property, but that requirs convert uri to IPAddress first, which is an expensive operation.
The logic here to determine isNonLLAIPv6Address is similar to IPAddress.IsIPv6LinkLocal implementation. (I should change to case insensitive IndexOf).
There was a problem hiding this comment.
If anything it must start with the prefix: fe80::/10 The IndexOf will try to find it anywhere in the string, right?
The documentation says uri.IdnHost is string suitable for DNS lookup.
It should not include [] IMHO but the constructed URL should e.g. "[fe80::1%22]"
There was a problem hiding this comment.
The IndexOf will try to find it anywhere in the string, right?
Yes, but here is intentionally checked for the index != 0 case, to make sure that it's not IPv6 LLA.
It should not include [] IMHO but the constructed URL should e.g. "[fe80::1%22]"
I think so. Will make URL to contain [] and add more test cases in next commit.
There was a problem hiding this comment.
Yes, but here is intentionally checked for the index != 0 case, to make sure that it's not IPv6 LLA.
But that still does the extra work to find it anywhere in the string, whereas StartsWith will bail if it doesn't match the beginning.
| // From RFC 4291, IPv6 LLA has formate like: fe80::/10. | ||
| await WriteAsciiStringAsync((uri.HostNameType == UriHostNameType.IPv6 && | ||
| uri.IdnHost.IndexOf("fe80") != 0) ? | ||
| uri.Host : uri.IdnHost).ConfigureAwait(false); |
There was a problem hiding this comment.
Why are we limiting it to fe80? If the user specifies a scope for something else, we want to ignore it? e.g.
using System;
class Program
{
static void Main()
{
Uri uri = new Uri("http://[fec0::1%123]:81/");
Console.WriteLine(uri.Host);
Console.WriteLine(uri.IdnHost);
}
}There was a problem hiding this comment.
Why not just do:
if (uri.HostNameType == UriHostNameType.IPv6)
{
await WriteByteAsync((byte)'[').ConfigureAwait(false);
await WriteAsciiStringAsync(uri.IdnHost).ConfigureAwait(false);
await WriteByteAsync((byte)']').ConfigureAwait(false);
}
else
{
await WriteAsciiStringAsync(uri.IdnHost).ConfigureAwait(false);
}?
There was a problem hiding this comment.
Why are we limiting it to fe80? If the user specifies a scope for something else, we want to ignore it?
I don't know, I will investigate it.
Why not just do:
This is definitely a better way. I didn't do that first because I want to make the change consistent, like below isNonLLAIPv6Address logic.
There was a problem hiding this comment.
According to the RFC for IPv6, "interface id" can be present for non LLA IPv6 addresses.
https://tools.ietf.org/html/rfc4291
It is required to be present for LLA because fe80:: addresses can be reused across multiple interfaces on a machine. So, the interface-id is used to differentiate them.
Most common uses of IPv6 don't include the interface-id, though, for non-LLA addresses.
| { | ||
| Debug.Assert(uri == proxyUri); | ||
| return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, isIPv6Address ? uri.Host : uri.IdnHost, uri.Port, null, proxyUri); | ||
| return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, isNonLLAIPv6Address ? uri.Host : uri.IdnHost, uri.Port, null, proxyUri); |
There was a problem hiding this comment.
Can you remind me why we need to distinguish between Host and IdnHost in these connection keys? Is the host from the connection key ever written out anywhere? It looks like "no", in which case why does it matter whether it's wrapped in []?
There was a problem hiding this comment.
Can you remind me why we need to distinguish between Host and IdnHost in these connection keys? Is the host from the connection key ever written out anywhere?
The host from the connection key will be written out to the Host header.
We need to distinguish because IdnHost doesn't include [] around IPv6 address, thus the Host header we are sending out for IPv6 address will be missing []. (#28557)
in which case why does it matter whether it's wrapped in []?
As the example in #28557 showed, if it not wrapped in [], for example, Host: ::1:5001. For IPv6 address, it will be hard to tell which part is address, which part is port number.
There was a problem hiding this comment.
The host from the connection key will be written out to the Host header.
It doesn't have to be though. This code is executed for every request. In contrast, this code is executed only when we create a new pool:
https://github.com/caesar1995/corefx/blob/22633ac67e1b59ddc058b7e1d700e90e5c202934/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs#L211
and it's that ctor that creates the bytes to be written out. We can pass whatever we want into that ctor, such as the host bytes to use or something the ctor can use to compute the host bytes. We should be keeping the connection key logic as lean as possible.
There was a problem hiding this comment.
We should be keeping the connection key logic as lean as possible.
I agree. But the tricky part here is to determine the value we want to pass into that ctor without additional allocation. I will explore this approach to see if able to find efficient way to do so, so that we can make a trade-off decision.
There was a problem hiding this comment.
An extra string allocation when constructing the pool is fine if there's no better way. It's not fine when constructing a key.
| else | ||
| { | ||
| return new HttpConnectionKey(HttpConnectionKind.Http, isIPv6Address ? uri.Host : uri.IdnHost, uri.Port, null, null); | ||
| return new HttpConnectionKey(HttpConnectionKind.Http, isNonLLAIPv6Address ? uri.Host : uri.IdnHost, uri.Port, null, null); |
There was a problem hiding this comment.
What about other places we're using IdnHost, like:
https://github.com/caesar1995/corefx/blob/0c08d53b12c4f93e224b447e850746fd1a9b6660/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs#L385-L389
Do those need to be updated as well?
There was a problem hiding this comment.
That's the content in the URL case Tomas has mentioned, I will update them all.
|
|
||
| // TODO: #28863 Part 2: Uri.Host LLA (Link-local address) for IPv6 address doesn't contain %number part. | ||
| // Since scope is mandatory for LLA, we will use uri.IdnHost for IPv6 LLA address. | ||
| // From RFC 4291, IPv6 LLA has formate like: fe80::/10. |
| // From RFC 4291, IPv6 LLA has formate like: fe80::/10. | ||
| await WriteAsciiStringAsync((uri.HostNameType == UriHostNameType.IPv6 && | ||
| uri.IdnHost.IndexOf("fe80") != 0) ? | ||
| uri.Host : uri.IdnHost).ConfigureAwait(false); |
There was a problem hiding this comment.
According to the RFC for IPv6, "interface id" can be present for non LLA IPv6 addresses.
https://tools.ietf.org/html/rfc4291
It is required to be present for LLA because fe80:: addresses can be reused across multiple interfaces on a machine. So, the interface-id is used to differentiate them.
Most common uses of IPv6 don't include the interface-id, though, for non-LLA addresses.
|
The new commit redo the product change in pr: #28578 #28740 (while keep the tests of course, nothing is regressed). The change in above old PRs for SocketsHttpHandler is to use To satisfy both cases, the best way is that we will always use uri.IdnHost for all IPv6 address (as the original code does before PR #28578 and #28740), and append [] around it. The only extra allocation is needed when a new connection pool is created (we don't need to allocate/detect if IPv6 per request, and leave connection key logic untouched). @dotnet-bot test Outerloop Linux x64 Debug Build please |
|
I re-enable the disabled For example, we will use a IPv6 LLA address (with interface-id/scope) to establish the connection, on Framework/Linux; however, content sent to network will not have the scope in it: The |
|
Unrelated failure & build stuck. @dotnet-bot test Outerloop OSX x64 Debug Build please |
|
Failures not related. Ping reviewers @dotnet/ncl |
| { | ||
| Debug.Assert(_pool.UsingProxy); | ||
|
|
||
| // TODO: #28863 Part 1: Uri.IdnHost should include [] around IPv6 address. |
There was a problem hiding this comment.
I would simplify these comments (which are repeated in a few places in this PR).
I would simply say:
// TODO: #28863 Uri.IdnHost is missing '[', ']' characters around IPv6 address. So, we need to add them manually for now.I would remove all the text in the comments talking about a bug in Uri.Host. It's not necessary to talk about that here because the handler should only be using .IdnHost anyways. The bug with Uri.Host not including %number for LLA already describes that problem in that GitHub issue. It's not relevant for SocketsHttpHandler.
There was a problem hiding this comment.
Will change comment.
|
@dotnet-bot test Outerloop Linux x64 Debug Build please |
|
Resolve merge conflict #29081: https://github.com/dotnet/corefx/pull/29081/files#diff-2675d24b3a3482c87ea2b9a41e2bdfb7R220 @dotnet-bot test Outerloop Linux x64 Debug Build please |
|
@dotnet-bot test Outerloop OSX x64 Debug Build please |
…x#28971) * add workaround for ipv6 LLA address * address comment * fix comment Commit migrated from dotnet/corefx@0698997
Uri.Host LLA (Link-local address) IPv6 address doesn't contain %number (scope) part, and scope is mandatory for LLA. We will use uri.IdnHost for IPv6 LLA address on SocketsHttpHandler.
This PR is a workaround for #28863, and should be reverted when #28863 is fixed.
Close #28749