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

Inconsistent behavior in SslStream.AuthenticateAsClient across win/linux #81590

Closed
CharlieEriksen opened this issue Feb 3, 2023 · 5 comments · Fixed by #81631
Closed

Inconsistent behavior in SslStream.AuthenticateAsClient across win/linux #81590

CharlieEriksen opened this issue Feb 3, 2023 · 5 comments · Fixed by #81631
Labels
area-System.Net.Security good first issue Issue should be easy to implement, good for first-time contributors help wanted [up-for-grabs] Good issue for external contributors
Milestone

Comments

@CharlieEriksen
Copy link

CharlieEriksen commented Feb 3, 2023

Description

On .NET 7.0, SslStream.AuthenticateAsClient behaves differently on Windows and Linux. When TargetHost is set for the SslClientAuthenticationOptions parameter, in some cases on Linux it will lead to the connection not establishing successfully, while it does on Windows.

While this appears to be host specific and is dependent on the parameters to AuthenticateAsClient, the fact that the very same code does not function the same across OS is problematic, leading to a lot of wasted time.

On Linux, you will simply get a connection reset:

Unhandled exception. System.IO.IOException: Unable to read data from the transport connection: Connection reset by peer.
 ---> System.Net.Sockets.SocketException (104): Connection reset by peer
   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   --- End of inner exception stack trace ---
   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   at System.Net.Security.SyncReadWriteAdapter.ReadAsync(Stream stream, Memory`1 buffer, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReceiveBlobAsync[TIOAdapter](CancellationToken cancellationToken)
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.AuthenticateAsClient(SslClientAuthenticationOptions sslClientAuthenticationOptions)
   at Program.Main() in /mnt/c/Users/Charlie/RiderProjects/SslStreamTest/SslStreamTest/Program.cs:line 17

Reproduction Steps

Run this code across both Windows and Linux:

using System.Net.Security;
using System.Net.Sockets;
					
public class Program
{
    public static void Main()
    {
        TcpClient client = new TcpClient("143.166.83.22", 443);
        SslStream sslStream = new SslStream(client.GetStream(), false);

        sslStream.AuthenticateAsClient(new SslClientAuthenticationOptions(){TargetHost = "143.166.83.22",RemoteCertificateValidationCallback = (sender, x509Certificate, chain, errors) => true});
    }
}

Expected behavior

As seen on Windows: A connection is established.

Actual behavior

As seen on Linux: Connection is reset:

Unhandled exception. System.IO.IOException: Unable to read data from the transport connection: Connection reset by peer.
 ---> System.Net.Sockets.SocketException (104): Connection reset by peer
   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   --- End of inner exception stack trace ---
   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   at System.Net.Security.SyncReadWriteAdapter.ReadAsync(Stream stream, Memory`1 buffer, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReceiveBlobAsync[TIOAdapter](CancellationToken cancellationToken)
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.AuthenticateAsClient(SslClientAuthenticationOptions sslClientAuthenticationOptions)
   at Program.Main() in /mnt/c/Users/Charlie/RiderProjects/SslStreamTest/SslStreamTest/Program.cs:line 17

Regression?

No response

Known Workarounds

Not setting TargetHost can help in some cases.

Configuration

Windows:

.NET SDK:
 Version:   7.0.102
 Commit:    4bbdd14480

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22000
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\7.0.102\

Host:
  Version:      7.0.2
  Architecture: x64
  Commit:       d037e070eb

.NET SDKs installed:
  6.0.402 [C:\Program Files\dotnet\sdk]
  7.0.102 [C:\Program Files\dotnet\sdk]

Linux, wsl:

.NET SDK:
 Version:   7.0.102
 Commit:    4bbdd14480

Runtime Environment:
 OS Name:     ubuntu
 OS Version:  20.04
 OS Platform: Linux
 RID:         ubuntu.20.04-x64
 Base Path:   /usr/share/dotnet/sdk/7.0.102/

Host:
  Version:      7.0.2
  Architecture: x64
  Commit:       d037e070eb

.NET SDKs installed:
  7.0.102 [/usr/share/dotnet/sdk]

Other information

No response

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

ghost commented Feb 3, 2023

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

Issue Details

Description

On .NET 7.0, SslStream.AuthenticateAsClient behaves differently on Windows and Linux. When TargetHost is set for the SslClientAuthenticationOptions parameter, in some cases on Linux it will lead to the connection not establishing successfully, while it does on Windows.

While this appears to be host specific and is dependent on the parameters to AuthenticateAsClient, the fact that the very same code does not function the same across OS is problematic, leading to a lot of wasted time.

On Linux, you will simply get a connection reset:

Unhandled exception. System.IO.IOException: Unable to read data from the transport connection: Connection reset by peer.
 ---> System.Net.Sockets.SocketException (104): Connection reset by peer
   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   --- End of inner exception stack trace ---
   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   at System.Net.Security.SyncReadWriteAdapter.ReadAsync(Stream stream, Memory`1 buffer, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReceiveBlobAsync[TIOAdapter](CancellationToken cancellationToken)
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.AuthenticateAsClient(SslClientAuthenticationOptions sslClientAuthenticationOptions)
   at Program.Main() in /mnt/c/Users/Charlie/RiderProjects/SslStreamTest/SslStreamTest/Program.cs:line 17

Reproduction Steps

Run this code across both Windows and Linux:

using System.Net.Security;
using System.Net.Sockets;
					
public class Program
{
    public static void Main()
    {
        TcpClient client = new TcpClient("143.166.83.22", 443);
        SslStream sslStream = new SslStream(client.GetStream(), false);

        sslStream.AuthenticateAsClient(new SslClientAuthenticationOptions(){TargetHost = "143.166.83.22",RemoteCertificateValidationCallback = (sender, x509Certificate, chain, errors) => true});
    }
}

Expected behavior

As seen on Windows: A connection is established.

Actual behavior

As seen on Linux: Connection is reset:

Unhandled exception. System.IO.IOException: Unable to read data from the transport connection: Connection reset by peer.
 ---> System.Net.Sockets.SocketException (104): Connection reset by peer
   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   --- End of inner exception stack trace ---
   at System.Net.Sockets.NetworkStream.Read(Span`1 buffer)
   at System.Net.Security.SyncReadWriteAdapter.ReadAsync(Stream stream, Memory`1 buffer, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReceiveBlobAsync[TIOAdapter](CancellationToken cancellationToken)
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.AuthenticateAsClient(SslClientAuthenticationOptions sslClientAuthenticationOptions)
   at Program.Main() in /mnt/c/Users/Charlie/RiderProjects/SslStreamTest/SslStreamTest/Program.cs:line 17

Regression?

No response

Known Workarounds

Not settings TargetHost can help in some cases.

Configuration

Windows:

.NET SDK:
 Version:   7.0.102
 Commit:    4bbdd14480

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22000
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\7.0.102\

Host:
  Version:      7.0.2
  Architecture: x64
  Commit:       d037e070eb

.NET SDKs installed:
  6.0.402 [C:\Program Files\dotnet\sdk]
  7.0.102 [C:\Program Files\dotnet\sdk]

Linux, wsl:

.NET SDK:
 Version:   7.0.102
 Commit:    4bbdd14480

Runtime Environment:
 OS Name:     ubuntu
 OS Version:  20.04
 OS Platform: Linux
 RID:         ubuntu.20.04-x64
 Base Path:   /usr/share/dotnet/sdk/7.0.102/

Host:
  Version:      7.0.2
  Architecture: x64
  Commit:       d037e070eb

.NET SDKs installed:
  7.0.102 [/usr/share/dotnet/sdk]

Other information

No response

Author: CharlieEriksen
Assignees: -
Labels:

area-System.Net.Security, untriaged

Milestone: -

@rzikm
Copy link
Member

rzikm commented Feb 3, 2023

This is kinda interesting. I can replicate this using openssl cli

openssl s_client -connect 143.166.83.22:443 -servername "143.166.83.22"

If I omit the -servername parameter (equivalent to not specifying TargetHost in .NET), then everything works.

The difference in behavior is caused by difference between SChannel and OpenSSL. SChannel will not send the SNI extension when you specify the TargetHost as IP address, while OpenSSL will send whatever you tell it to.

From RFC 6066 - transport Layer Security (TLS) Extensions: Extension Definitions

Literal IPv4 and IPv6 addresses are not permitted in "HostName".

The specific server in question (pc-premierce80.us.dell.com) is kinda picky about SNI and will close the connection when SNI can be interpreted as IP address. Not all servers behave like this btw.

We can probably check if the TaretHost parses as IP address and avoid sending it to be consistent across platforms. An obvious workaround for the user in the meantime is not setting TargetHost to an IP literal.

Not critical for 8.0, moving to future. This should also be relatively simple, so it is a good candidate for external contribution.

@rzikm rzikm added good first issue Issue should be easy to implement, good for first-time contributors help wanted [up-for-grabs] Good issue for external contributors and removed untriaged New issue has not been triaged by the area owner labels Feb 3, 2023
@rzikm rzikm added this to the Future milestone Feb 3, 2023
@wfurt
Copy link
Member

wfurt commented Feb 3, 2023

https://www.rfc-editor.org/rfc/rfc6066#page-7

   "HostName" contains the fully qualified DNS hostname of the server,
   as understood by the client.  The hostname is represented as a byte
   string using ASCII encoding without a trailing dot.  This allows the
   support of internationalized domain names through the use of A-labels
   defined in [[RFC5890](https://www.rfc-editor.org/rfc/rfc5890)].  DNS hostnames are case-insensitive.  The
   algorithm to compare hostnames is described in [[RFC5890](https://www.rfc-editor.org/rfc/rfc5890)], [Section](https://www.rfc-editor.org/rfc/rfc6066#section-2.3.2.4)
   [2.3.2.4](https://www.rfc-editor.org/rfc/rfc6066#section-2.3.2.4).

   Literal IPv4 and IPv6 addresses are not permitted in "HostName".

I think we should fix it @rzikm even if the server behavior is finicky.
(and also make sure HttpClient does not try to set it)

@wfurt wfurt modified the milestones: Future, 8.0.0 Feb 3, 2023
@rzikm
Copy link
Member

rzikm commented Feb 3, 2023

I did not realize the issue cannot be avoided with HttpClient. So fixing the issue in 8.0 makes more sense.

I think fixing it on SslStream level (not passing TargetHost to underlying platform crypto) should be enough, no need to do the same on HttpClient level.

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Feb 4, 2023
@CharlieEriksen
Copy link
Author

Thanks for the quick triage and for figuring out what was wrong @rzikm and @wfurt. I hadn't actually caught the fact that it also was an issue with HttpClient, but I had been wondering why my HTTP responses against IP addresses were rather flaky. So that explains that!

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Feb 8, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Mar 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Net.Security good first issue Issue should be easy to implement, good for first-time contributors help wanted [up-for-grabs] Good issue for external contributors
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants