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

Type and content inconsistency between TcpClient.Client.LocalEndPoint/RemoteEndPoint and TcpConnectionInformation.LocalEndPoint/RemoteEndPoint #91137

Closed
jchristn opened this issue Aug 25, 2023 · 16 comments
Assignees
Labels
area-System.Net question Answer questions and provide assistance, not an issue with source code or documentation.
Milestone

Comments

@jchristn
Copy link

jchristn commented Aug 25, 2023

Description

When attempting to identify a TCP connection from TcpClient.Client.LocalEndPoint or RemoteEndPoint using the TcpConnectionInformation[] returned from IPGlobalProperties.GetIPGlobalProperties.GetActiveTcpConnections(), there is a material difference between the contents of LocalEndPoint and RemoteEndPoint.

On a Windows 11 host with both IPv4 and IPv6 enabled, .ToString() for the TcpClient returns this:

Console.WriteLine(client.Client.LocalEndPoint.ToString());
// [::ffff:127.0.0.1]:55853

Whereas after retrieving the connection information, it returns something different:

Console.WriteLine(conn.LocalEndPoint.ToString() + " to " + conn.RemoteEndPoint.ToString() + ": " + conn.State.ToString());
// 127.0.0.1:55853 to 127.0.0.1:8000: Established

It appears TcpClient contains a hybrid IPv6/IPv4 address whereas TcpConnectionInformation only contains IPv4.

Is there a way to identify the connection from TcpConnectionInformation in a consistent manner, preferably across runtimes and across platforms?

Reproduction Steps

Documented here: https://stackoverflow.com/questions/76979491/matching-tcp-connections-between-tcpclient-and-tcpconnectioninformation and also above

Expected behavior

LocalEndPoint and RemoteEndPoint would contain values of similar structure and be comparable, between TcpClient and TcpConnectionInformation

Actual behavior

TcpClient contains a hybrid IPv6/IPv4 address whereas TcpConnectionInformation contains only an IPv4 address.

Regression?

Unknown

Known Workarounds

For localhost connections, just comparing by source port, which must be unique.

Configuration

.NET 7
Windows 11
x64
Unknown
No Blazor

Other information

NA

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Aug 25, 2023
@ghost
Copy link

ghost commented Aug 25, 2023

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

Issue Details

Description

When attempting to identify a TCP connection from TcpClient.Client.LocalEndPoint or RemoteEndPoint using the TcpConnectionInformation[] returned from IPGlobalProperties.GetIPGlobalProperties.GetActiveTcpConnections(), there is a material difference between the contents of LocalEndPoint and RemoteEndPoint.

On a Windows 11 host with both IPv4 and IPv6 enabled, .ToString() for the TcpClient returns this:

Console.WriteLine(client.Client.LocalEndPoint.ToString());
// [::ffff:127.0.0.1]:55853

Whereas after retrieving the connection information, it returns something different:

Console.WriteLine(conn.LocalEndPoint.ToString() + " to " + conn.RemoteEndPoint.ToString() + ": " + conn.State.ToString());
// 127.0.0.1:55853 to 127.0.0.1:8000: Established

It appears TcpClient contains a hybrid IPv6/IPv4 address whereas TcpConnectionInformation only contains IPv4.

Is there a way to identify the connection from TcpConnectionInformation in a consistent manner, preferably across runtimes and across platforms?

Reproduction Steps

Documented here: https://stackoverflow.com/questions/76979491/matching-tcp-connections-between-tcpclient-and-tcpconnectioninformation and also above

Expected behavior

LocalEndPoint and RemoteEndPoint would contain values of similar structure and be comparable, between TcpClient and TcpConnectionInformation

Actual behavior

TcpClient contains a hybrid IPv6/IPv4 address whereas TcpConnectionInformation contains only an IPv4 address.

Regression?

Unknown

Known Workarounds

For localhost connections, just comparing by source port, which must be unique.

Configuration

.NET 7
Windows 11
x64
Unknown
No Blazor

Other information

NA

Author: jchristn
Assignees: -
Labels:

area-System.Net

Milestone: -

@wfurt
Copy link
Member

wfurt commented Aug 25, 2023

That depends on how you create the TcpClient. If you use new TcpClient(AddressFamily. InterNetwork) you will get traditional IPv4 addresses. If you use dual mode IPV6 socket (default) you will get mapped addresses.
You can also use IPAddress.MapToIPv4 before Turing into string if you want to.

@jchristn
Copy link
Author

jchristn commented Aug 25, 2023

What if the TcpClient was retrieved from a server listener?

E.g.

TcpClient tcpClient = await _Listener.AcceptTcpClientAsync().ConfigureAwait(false);

@wfurt
Copy link
Member

wfurt commented Aug 26, 2023

I would expect the socket AddressFamily to match the listener. So it will depend on what address (family) you are listening on. As I mentioned the LocalEndPoint and RemoteEndPoint will honor socket's AddressFamily.

The TCP list just dumps what ever connections are out there. If you cannot control the listener you would need to convert the addresses as needed.

@jchristn
Copy link
Author

jchristn commented Aug 26, 2023

Sorry to keep bugging you with this; I'm trying to find a way to match connections in a reliable way across platforms, regardless of whether the address is IPv4 or IPv6, and hopefully without having to do a bunch of .ToString() gymnastics (which consume CPU time, et al).

I have control over the TcpListener. However, I only see three constructors:

TcpListener(int port);
TcpListener(IPEndPoint localEP);
TcpListener(IPAddress localaddr, int port);

I don't see any way to influence whether the listener is IPv4 or IPv6 (directly), unless the IPEndPoint and IPAddress constructor variants take the address family of the supplied values. Is that the case?

What about situations where you want to listen on multiple interfaces and choose a listener of IPAddress.Any? This seems to imply that it will accept both IPv6 and IPv4, and is what I was using to produce the example above where the TcpClient.Client.LocalEndPoint appeared to be a merged [_ipv6_:ipv4]:[port]

There has to be some means of accomplishing this :)

@fbrosseau
Copy link

TcpListener.Create(int port) is dualmode. Ignore the legacy TcpListener(int port) constructor, as that was before dualmode support and it had to keep meaning ipv4-only for backward compatibility.

unless the IPEndPoint and IPAddress constructor variants take the address family of the supplied values. Is that the case?

yes

What about situations where you want to listen on multiple interfaces and choose a listener of IPAddress.Any?

IPAddress.Any means IPv4. IPAddress.Ipv6Any means IPV6.
Neither are what you are looking for, what you want is TcpListener.Create.

Do note that in dualmode all IPAddresses you will get back from dotnet will be IPv6, which means for IPv4 clients they will be IsIPv4MappedToIPv6=true. If you want your application to be dualmode yet still have pretty IPv4 addresses+endpoints, your application code should normalize mapped IPs back to ipv4. Another option is to have two distinct listeners, one on Any and one on IPv6Any, but the whole point of dualmode is to accomplish this optimally.

@jchristn
Copy link
Author

Hi @fbrosseau sorry if this is a dumb question, but IsIPv4MappedToIPv6 is an IPAddress property and does not appear to be a property of LocalEndPoint (Endpoint, IPEndPoint).

@wfurt
Copy link
Member

wfurt commented Aug 28, 2023

That is property of IPAddres e.g. conn.LocalEndPoint.Address

@jchristn
Copy link
Author

The Address property is only present on the LocalEndPoint and RemoteEndPoint properties coming from TcpConnectionInformation.

TcpClient.LocalEndPoint does not have Address, but only AddressFamily.

How about for the TcpClient object (not from TcpConnectionInformation)?

@wfurt
Copy link
Member

wfurt commented Aug 28, 2023

This is because Socket has generic EndPoint that may or may not be IPEndPoint. For TCP you can do something like

        IPEndPoint lep = tcpClient.Client.LocalEndPoint as IPEndPoint;
        IPEndPoint rep = tcpClient.Client.RemoteEndPoint as IPEndPoint;

        Console.WriteLine("Client = {0} {1}", tcpClient.Client.LocalEndPoint, tcpClient.Client.RemoteEndPoint);
        Console.WriteLine("Client = {0} {1}", lep.Address.IsIPv4MappedToIPv6 ?
                                                new IPEndPoint(lep.Address.MapToIPv4(), lep.Port) : lep,
                                              rep.Address.IsIPv4MappedToIPv6 ?
                                                new IPEndPoint(rep.Address.MapToIPv4(), lep.Port) : rep);

and it would produce something like

Client = [::ffff:127.0.0.1]:47868 [::ffff:127.0.0.1]:36935
Client = 127.0.0.1:47868 127.0.0.1:47868

TcpConnectionInformation already gives you IPEndPoint so the cast is not needed. Once converted, IPEndPoint.Equals should just work without need to convert to string. If you compare Address and Port separately, you can avoid allocation of the new IPEndPoint but that may be small benefit.

I hope this make some sense @jchristn.

@wfurt wfurt added the question Answer questions and provide assistance, not an issue with source code or documentation. label Aug 28, 2023
@jchristn
Copy link
Author

Thank you @wfurt this makes it perfectly clear. Is there any effort underway to ensure that TcpConnectionInformation represents in the same way?

@karelz
Copy link
Member

karelz commented Aug 29, 2023

@jchristn what kind of effort do you expect here?
It seems to me that you can use IPEndPoint.Equals to solve your primary problem. The only difference is in pretty-printing (ToString) for EndPoint vs. IPEndPoint, which you can solve by casting to IPEndPoint to have everything consistent.

@jchristn
Copy link
Author

jchristn commented Aug 29, 2023

Hi @karelz unfortunately .Equals does not work. This code does not return a connection state, it returns null, when the address is mapped.

IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties();
TcpConnectionInformation[] connections = properties.GetActiveTcpConnections();
var state = connections.FirstOrDefault(x =>
                x.LocalEndPoint.Equals(client.Client.LocalEndPoint)
                && x.RemoteEndPoint.Equals(client.Client.RemoteEndPoint));

@karelz
Copy link
Member

karelz commented Aug 29, 2023

@jchristn can you try to debug why it does not work?
What are the differences?

@jchristn
Copy link
Author

jchristn commented Aug 29, 2023

Hi @karelz yes I have tried. It seems that when the TcpClient from TcpClient tcpClient = await _Listener.AcceptTcpClientAsync().ConfigureAwait(false); has a mapped address, you cannot use .Equals() against the TcpClient.Client.[Local|Remote]EndPoint and the one returned from TcpConnectionInformation.[Local|Remote]EndPoint because it is not a mapped address.

e.g. if I use the Linq statement above, it returns null.

If I iterate the array of TcpConnectionInformation using Console.WriteLine, I can see the connection in the list. Since the TcpClient is mapped, and the TcpClient inside of TcpConnectionInformation is not, Equals() fails.

@wfurt
Copy link
Member

wfurt commented Aug 29, 2023

There is no effort to make them same and I feel there should not be. They are different for good reasons. As I mentioned above, the endpoints on Socket do match Socket AddressFamily. Changing that would be major breaking change. Also the endpoint needs to stay as it is because there can be other types than TCP/IP e.g. casting is needed when appropriate.

Only one hope for the future would be #63162 IMHO. That already uses IPEndPoint and since that is new API we would have more freedom to remap - but I'm not sure if we really want to. For you case, I think using MapToIPv4 with combination of Equals is best approach. (to avoid allocation of strings for comparison). Or as @fbrosseau suggested, change your code to avoid dual mode sockets as that eliminates the need for re-mapping.

@wfurt wfurt closed this as completed Aug 29, 2023
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Aug 29, 2023
@karelz karelz added this to the 9.0.0 milestone Sep 14, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Oct 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Net question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
None yet
Development

No branches or pull requests

4 participants