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

Android duplex client not working with TCP transport security #5031

Closed
JD-FM opened this issue Mar 3, 2023 · 2 comments
Closed

Android duplex client not working with TCP transport security #5031

JD-FM opened this issue Mar 3, 2023 · 2 comments
Labels

Comments

@JD-FM
Copy link

JD-FM commented Mar 3, 2023

I am working a prototype for a phone app, using MAUI on .NET 6, System.ServiceModel.NetTcp 4.10.0. The following code works for a Windows client, and using an iPhone simulator, but fails on an Android simulator.

public string Connect(string sUserName, string sPassword, string sHost, string sPort, IELM_Process_AdviseList process_AdviseList)
        {
            string sResult = string.Empty;

            try
            {
                IsInConnectProcess = true;
                if (this.Binding == null)
                {
                    this.Binding = new NetTcpBinding(SecurityMode.Transport);
                    this.Binding.Security.Message.ClientCredentialType = MessageCredentialType.None;
                    this.Binding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Certificate;
                    this.Binding.Security.Transport.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
                    this.Binding.SendTimeout = new TimeSpan(72, 0, 0); // 72 hours to cover long weekend down time
                    this.Binding.ReceiveTimeout = new TimeSpan(72, 0, 0); // 72 hours to cover long weekend down time
                    this.Binding.MaxReceivedMessageSize = 500 * 1024 * 1024;//  500 MB
                    this.Binding.MaxBufferSize = (int)Binding.MaxReceivedMessageSize;
                    this.Binding.ReaderQuotas.MaxStringContentLength = (int)Binding.MaxReceivedMessageSize;
                }

                if (this.MauiProxy == null)
                {
                    WcfUserName = sUserName;
                    InstanceContext context = new InstanceContext(this);
                    Uri uriOperation = new Uri($"net.tcp://{sHost}:{sPort}/{typeof(IWCFMaui).Name}");
                    string sPhoneCertName = "ElmPhoneCert";
                    DnsEndpointIdentity dnsEndpointIdentity = new DnsEndpointIdentity(sPhoneCertName);
                    MauiProxy = new WCFMaui_DuplexClient(context, this.Binding, new EndpointAddress(uriOperation, dnsEndpointIdentity));
                    if (MauiProxy != null)
                    {
                        MauiProxy.ClientCredentials.ClientCertificate.Certificate = MauiCertificate();
                        MauiProxy.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None;
                        MauiProxy.Open(); // Exception thrown here on Android simulator

                        //enable events first
                        EnableEvents(true);
                        MauiProxy.MauiConnect(sUserName, sPassword);
                        AdviseProcessor = process_AdviseList;
                        WCF_Client_Maui = this;
                    }
                }
                else
                {
                    HandleDuplex();
                }                
            }
            catch (Exception ex)
            {
                sResult = ex.Message;
                throw;
            }
            finally
            {
                if (sResult.Length > 0)
                    Disconnect();
            }

            return sResult;
        }

The exception thrown is:

Authentication failed, see inner exception
at System.ServiceModel.Channels.SslStreamSecurityUpgradeInitiator.OnInitiateUpgradeAsync(Stream stream, OutWrapper`1 remoteSecurityWrapper) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/SslStreamSecurityUpgradeProvider.cs:line 404
   at System.ServiceModel.Channels.StreamSecurityUpgradeInitiatorBase.InitiateUpgradeAsync(Stream stream) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/StreamSecurityUpgradeInitiatorBase.cs:line 78
   at System.ServiceModel.Channels.ConnectionUpgradeHelper.InitiateUpgradeAsync(StreamUpgradeInitiator upgradeInitiator, OutWrapper`1 connectionWrapper, ClientFramingDecoder decoder, IDefaultCommunicationTimeouts defaultTimeouts, TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/FramingChannels.cs:line 606
   at System.ServiceModel.Channels.ClientFramingDuplexSessionChannel.SendPreambleAsync(IConnection connection, ArraySegment`1 preamble, TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/FramingChannels.cs:line 257
   at System.ServiceModel.Channels.ConnectionPoolHelper.EstablishConnectionAsync(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/ConnectionPoolHelper.cs:line 113
   at System.ServiceModel.Channels.ClientFramingDuplexSessionChannel.OnOpenAsync(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/FramingChannels.cs:line 343
   at System.ServiceModel.Channels.CommunicationObject.OnOpenAsyncInternal(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/CommunicationObject.cs:line 561
   at System.ServiceModel.Channels.CommunicationObject.System.ServiceModel.IAsyncCommunicationObject.OpenAsync(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/CommunicationObject.cs:line 532
   at System.ServiceModel.Channels.ServiceChannel.OnOpenAsync(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/ServiceChannel.cs:line 1351
   at System.ServiceModel.Channels.CommunicationObject.OnOpenAsyncInternal(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/CommunicationObject.cs:line 561
   at System.ServiceModel.Channels.CommunicationObject.System.ServiceModel.IAsyncCommunicationObject.OpenAsync(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/CommunicationObject.cs:line 532
   at System.ServiceModel.Channels.CommunicationObject.OpenAsyncInternal(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/CommunicationObject.cs:line 504
   at System.Runtime.TaskHelpers.WaitForCompletion(Task task) in /_/src/System.Private.ServiceModel/src/Internals/System/Runtime/TaskHelpers.cs:line 255
   at System.ServiceModel.Channels.CommunicationObject.Open(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/CommunicationObject.cs:line 498
   at System.ServiceModel.Channels.ServiceChannelProxy.System.ServiceModel.ICommunicationObject.Open(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/ServiceChannelProxy.cs:line 592
   at System.ServiceModel.ClientBase`1[[Elm.WCF.IWCFMaui, TestApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].System.ServiceModel.ICommunicationObject.Open(TimeSpan timeout) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/ClientBase.cs:line 500
   at System.ServiceModel.ClientBase`1[[Elm.WCF.IWCFMaui, TestApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].Open() in /_/src/System.Private.ServiceModel/src/System/ServiceModel/ClientBase.cs:line 300
   at Elm.WCF.EWCF_Client_Maui.Connect(String sUserName, String sPassword, String sHost, String sPort, IELM_Process_AdviseList process_AdviseList) in C:\Work\ELM80\Elm.Services\WCF\Maui\WCFMauiClient.cs:line 102

The inner\inner exception is:

Exception of type 'Interop+AndroidCrypto+SslException' was thrown
   at System.Net.Security.SslStream.<ForceAuthenticationAsync>d__175`1[[System.Net.Security.AsyncReadWriteAdapter, System.Net.Security, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]].MoveNext()
   at System.ServiceModel.Channels.SslStreamSecurityUpgradeInitiator.OnInitiateUpgradeAsync(Stream stream, OutWrapper`1 remoteSecurityWrapper) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/SslStreamSecurityUpgradeProvider.cs:line 395
@mconnew
Copy link
Member

mconnew commented Mar 7, 2023

It's difficult to know what the problem is exactly as from the WCF perspective as all we do is new up an SslStream and call AuthenticateAsClientAsync and the exception is that SslStream threw an exception. There's a difference in behavior with SslStream between Android and the other platforms. I searched for SslException in the maui android repo and found this comment which references issue dotnet/runtime#45741. In that issue it says the following:

  • Any RemoteCertificateValidationCallback will only get an opportunity to validate certificates that have already been accepted by the system's built-in trust manager.
  • This means that the use cases of the callback for self-signed certificates or custom trust will not work.

It looks like using self signed developer certificates and telling WCF to skip certificate validation isn't an option on Android. If you are going to have a certificate which Android can validate available when you release the phone app and you just want to get past this problem during prototyping, you can set the binding SecurityMode to None. The communication won't be encrypted and the server will have no way of identifying the clients identity, but it will allow you to concentrate on other parts of your prototype later. One option it to get a free certificate which has a publicly valid signature, but that requires owning a domain and maybe hosting a file on that domain (I'm only familiar with Lets Encrypt and I'm several years out of date, but that's how they did it when I looked into them a few years ago).

I hope this is enough info to unblock you. BTW, you might want to consider switching to NetHttpBinding which uses WebSockets when you have a duplex contract. The down side with using NetTcp over high latency links is that it requires an ACK byte sent in response to an incoming message before the sender can send the next thing. NetTcp allows you to have replies sent in a different order than the requests were sent as the request and reply are tied together using a generated GUID added to each request message. This allows you to make a request to the service (or client if using duplex) and before the reply comes back, send a second request to the service. The problem that can happen with the ACK byte is that the second request won't begin to be sent until the ACK byte is returned for the first request. In high latency networks (such as mobile networks which can often have a 300ms latency), this makes it harder to send multiple requests before the reply comes back. A second problem with NetTcp on unreliable networks (such as mobile networks) is difficulty in detecting the connection has died. If the client (or server with duplex contracts) doesn't have an outstanding request that it's waiting on the reply for, there's no way to know that the connection has died. The first you find out is when you next try to send a request and you might have to wait for the Send timeout (default of 1 minute) to elapse before you get an exception.
WebSockets doesn't have either of these problems. Once a request has been sent, a second request can immediately be sent without waiting on an ACK byte. WebSockets also has built in a keep-alive mechanism which sends a ping or pong message on a regular basis and the channel will Fault if the keep-alive packets aren't being received. You can attach a Faulted event handler to your channel so you know pre-emptively that the channel has faulted before you need to send a request and reestablish a new connection. This is especially important with duplex communication because if you aren't sending any client->server requests, you can't tell the difference between the server hasn't sent you anything and the connection has died.
The down side with WebSockets is you can't (currently) use client certificates with it, you would need to use some other authentication mechanism. You could use TransportWithMessageCredentials to still use client certificates. It does an initial handshake to establish a session and the client provides the client certificate over a SOAP message so there are ways to work do it, they just aren't quite as efficient. WebSockets can also be more expensive on the Service side. If you are hosting in IIS it's not too bad, but if your WCF service is self hosted (eg console app), then it's quite a bit more expensive.

@HongGit HongGit added the triaged label Mar 7, 2023
@HongGit
Copy link
Contributor

HongGit commented Mar 7, 2023

Hopefully your issue has been resolved with Matt's comment, close for now.

Feel free to reactivate if you have more questions.

@HongGit HongGit closed this as completed Mar 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants