WCF: SSL offloading in load balancer - a simple approach

Problem statement
1. There are two machines behind load balancer.
2. These machines host same version of the WCF service with wsHttpBinding and no security.
3. It means WCF services are available over http behind the load balancer.
4. Load balancer is configured with a server certificate (i.e. SSL).
5. It means to the outside world, it will be a SSL configured communication.
6. Though client manages to create proxy classes, it could not make the method level communication with the above configuration setup.

Why?

Flow
Outside (https) -> LB -> Inside (http)

In past, I have posted another blog related to the same problem on https://blogs.msdn.microsoft.com/dsnotes/2014/10/03/ssl-offloading-in-load-balancer-scenario/.

Assessment
1. Service contract is IService1
2. Service implementation class is Service1

         public class Service1 : IService1
        {

        }

3. The url of the WCF service outside load balancer as the following:
https://www.MyCompany.com/MyService/Service1.svc
4. Let’s browse to the service wsdl on client machine
5. Scroll to the bottom
6. It will have:

       <wsdl:service name="Service1">
              <wsdl:port name="WSHttpBinding_IService1" binding="tns:WSHttpBinding_IService1">
                    <soap12:address location="www.MyCompany.com/MyService/Service1.svc"/>
                    <wsa10:EndpointReference>
                             <wsa10:Address>www.MyCompany.com/MyService/Service1.svc</wsa10:Address>
                    </wsa10:EndpointReference>
              </wsdl:port>
      </wsdl:service>

7. We can see there is a clear cut difference between the browsed address and endpoint address in wsdl.
8. When client adds service reference, we can see it comes with endpoint addressed value in app.config.

Solution
As a solution to this problem, I would recommend the following:

WCF Service | Service1.cs (Implementation file)

 // InstanceContextMode or ConcurrencyMode values can also be provided here
/* AddressFilterMode.Any 
* Indicates a filter that matches on any address of an incoming message
* allows to effectively call https url and have this processed by http url in backend
*/
[ServiceBehavior(AddressFilterMode=AddressFilterMode.Any)]
public class Service1 : IService1
{

}

WCF Service | Web.config

    <system.serviceModel>
        <services>
              <service name="WcfService1.Service1" behaviorConfiguration="mybehavior">
                     <endpoint address="" binding="wsHttpBinding" contract="WcfService1.IService1"
                                bindingConfiguration="noSecurity"/>
              </service>
        </services>
        <behaviors>
              <serviceBehaviors>
                     <behavior name="mybehavior">
                           <serviceMetadata httpGetEnabled="true"/>
                           <serviceDebug includeExceptionDetailInFaults="false" />
                           <!-- Enables the retrieval of metadata address information from the request message headers -->
                           <useRequestHeadersForMetadataAddress/>
                    </behavior>
              </serviceBehaviors>
        </behaviors>
        <bindings>
              <wsHttpBinding>
                    <binding name="noSecurity">
                            <security mode="None" />
                    </binding>
              </wsHttpBinding>
        </bindings>
    </system.serviceModel>

Client
1. Add service reference will create an endpoint as per wsdl -> wsa10:EndpointReference -> wsa10:Address value.
2. In client endpoint, the endpoint address will have http based url.
3. So, update the client endpoint address with the externally available address.
4. Also - update client endpoint to use wsHttpBinding with transport client credential type as None.
5. Reason: Client is going to use WCF pipeline to communicate over https. For https, we are going the basic requirement of SSL communication over transport in wsHttpBinding.

Client | App.config

     <system.serviceModel>
          <bindings>
                <wsHttpBinding>
                       <binding name="WSHttpBinding_IService1">
                                <security mode="Transport">
                                       <transport clientCredentialType="None" />
                                </security>
                       </binding>
                </wsHttpBinding>
          </bindings>
          <client>
                <endpoint address="https://www.MyCompany.com/MyService/Service1.svc"
                            binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1"
                            contract=" WcfService1.IService1" name=" WSHttpBinding_IService1" />
          </client>
    </system.serviceModel>

Another solution
Just in case you do not want to do a manual override on the client endpoint address and security configuration, then WCF extensibility is at rescue.

On the service side - implement IwsdlExportExtension, and write your logic in ExportEndpoint method for the new url. This will ensure whenever a client application will add service reference, it will generate the expected endpoint address on client side.

Service side | Create a file - Bahevior.cs

 using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace WcfService1
{
      public class HostNameAddressBehavior : Attribute, IWsdlExportExtension, IEndpointBehavior, IServiceBehavior
      {
           public HostNameAddressBehavior()
           {
                 // If you have some operations, which need to be handled on the global level, please provide your logic.
           }

            public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
            {
            }

            public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
            {
            }

            public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
            {
            }

            public void Validate(ServiceEndpoint endpoint)
            {
            }

            public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
            {
                  foreach (ServiceEndpoint endpoint in endpoints)
                  {
                       if (endpoint.EndpointBehaviors.Count == 0)
                       {
                            endpoint.Behaviors.Add(this);
                       }
                  }
            }

            public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
            {
            }

            public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
            {
            }

            public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
            {
            }

            public void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
            {
                 /*
                  * // Get hold of the endpoint address
                  * EndpointAddress address = context.Endpoint.Address;
                  * string absoluteUri = address.Uri.AbsoluteUri;
                  * // If you are a regex fan, you can use here as well on the absoluteUri
                  */
          
                 string scheme = "https";
                 string hostName = "www.MyCompany.com";
                 string path = "/MyService/Service1.svc";

                 EndpointAddress address = context.Endpoint.Address;
                 string absoluteUri = address.Uri.AbsoluteUri;

                 // Update base address
                 Uri newAbsoluteUri = new Uri(string.Format("{0}://{1}{2}", scheme, hostName, path));
                 context.Endpoint.Address = new EndpointAddress(newAbsoluteUri, address.Identity, address.Headers, 
                                                 address.GetReaderAtMetadata(), address.GetReaderAtExtensions());
            }
     }

     public class HostNameAddressBehaviorExtension : BehaviorExtensionElement
     {
             public override Type BehaviorType
             {
                  get
                     {
                           return typeof(HostNameAddressBehavior);
                     }
             }

             protected override object CreateBehavior()
             {
                   return new HostNameAddressBehavior();
             }
      }
}

 

Service | web.config

     <system.serviceModel>
    ...
       <behaviors>
           <serviceBehaviors>
                <behavior name="behavior1">
                      <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
                      <serviceDebug includeExceptionDetailInFaults="false"/>
                      <hostNameAddress />
                </behavior>
           </serviceBehaviors>
       </behaviors>
       <extensions>
            <behaviorExtensions>
                  <add name="hostNameAddress" type="WcfService1.HostNameAddressBehaviorExtension, WcfService1" />
            </behaviorExtensions>
       </extensions>
    ...
    </system.ServiceModel>

 

Client
1. Add service reference.
2. No changes are required to be done - client would be auto populated with https endpoint address and security mode transport with no client credential type.

 

 

I hope this helps - happy coding!