When you think things are not possible: WCF duplex callbacks through NATs and firewalls - safe and secure

NATs (Network Address Translation) prevents the private IP address of your home and/or work PC to be exposed to the big wild and bad Internet. The only thing nodes outside of the NAT-shielded network can see is the IP address of the NAT-capable router device. A firewall should prevent unwanted network traffic from both inside and outside of a network - and personal firewalls even scale this wish down to the single computer, again at work or at home.

Cool. This is good and surely not extremely latest news for you. But this fact and these two measurements makes building certain kinds of distributed systems a little bit... well, tough, to say the least. Or as I would put it in my words: "It totally sucks to build duplex communication over the Internet as we all want to be safe and secure.".
I have been facing the wish to call from an Internet-located resource to an enterprise- or home-located program in a secure fashion countless times. There are tons of programs that can do this, like MSN Messenger or Skype, to name just a few very prominent ones. So, what can we do when we want to build a distributed solution based on Windows, based on .NET, based on WCF? Surely, we could build everything that Messenger et. al. does on our own or just hijack Messenger. But I think there should be some more general purpose approach which is potentially highly interoperable (more to come...).

WCF has this neat feature of duplex channels and callback contracts. You can do duplex messaging in WCF over different transports, like TCP, Named Pipes and even HTTP. In the latter case you need to set up (or better: let WCF do it for you) an HTTP-based callback endpoint. This is either done automatically for you (watch out for some interesting security side effect or you can decide which address your consuming application should listen on. Whatever approach you use, you just cannot beat firewalls and you surely lose the battle against NATs - so what to do?

We need some helpers, as always.
This helper shows up in the shape of the Connectivity and the Identity services from the BizTalk Services labs cornucopia. We can use the Connectivity Service as a relay in order to enable the above described callback scenario. There are already some very good philosophical and architectural postings and descriptions of the currently available services.

For duplex communication to happen, the key secret is that we do not only register the service with the relay but also the client's base callback address. Of course we need to model the service and consumer conversation and we decide to go for WCF's opt-in callback contract model. Next we add a duplex-capable binding and should be ready to go.

First, let's have a look at the major ServiceContract for a really simple example. We just want to be able to subscribe and unsubscribe to a service to get notifications when certain topics occur. Yeah, I know, this is far from being a complete and production-ready pub/sub framework - if you look for this please switch over to my friend Juval.

[ServiceContract(

    Name = "PubSub",

    Namespace = "http://thinktecture.com/services/relay/pubsub",

    ConfigurationName="PSC",

    CallbackContract=typeof(IPubSubCallback))]

public interface IPubSub

{

    [OperationContract(IsOneWay=true)]

    void Subscribe(string subMessage);

 

    [OperationContract(IsOneWay = true)]

    void Unsubscribe(string unsubMessage);

}

For sake of completeness, this is the CallbackContract to publish data from the service to the consumers.

[ServiceContract(

    Name = "PubSubCallback",

    Namespace = "http://thinktecture.com/services/relay/pubsub",

    ConfigurationName="PSCC")]

public interface IPubSubCallback

{

    [OperationContract(IsOneWay = true)]

    void Publish(string pubMessage);

}

So far, nothing really new and extraordinary exciting, at least if you are familiar with basic duplex callbback contract modeling.

Second, we now need a binding that a) supports duplex communication and b) of course can do the relaying from the BizTalk Labs Connectivity service. That means that netTcpBinding, netNamedPipeBinding and wsDualHttpBinding will not work - for obvious reasons.
Following is the service-side config for our simple duplex relay sample. This shows a little known but incredibly powerful way to make up a custom binding with duplex capability. We use the relayTransport from the BizTalk Services SDK and stack on top if it a binary message encoder. Then we need a oneWay element in order to be able to take a IDuplexSessionChannel or a IRequestChannel and expose it as a IOutputChannel, or conversely it can take a IDuplexSessionChannel or a IReplyChannel and expose it as a IInputChannel. Last but not least we add a compositeDuplex element.

<configuration>

  <system.serviceModel>

    <bindings>

      <customBinding>

        <binding name="duplexRelay">

          <compositeDuplex />

          <oneWay />

          <binaryMessageEncoding />

          <relayTransport />

        </binding>

      </customBinding>

    </bindings>

 

    <services>

      <service name="PSS">

        <endpoint name="DuplexRelayEndpoint"

          contract="PSC"

          binding="customBinding" bindingNamespace="http://thinktecture.com/services/relay/pubsub"

          bindingConfiguration="duplexRelay" />

      </service>

    </services>

  </system.serviceModel>

</configuration>

I will skip the service hosting code, it is merely about fiddling with the endpoint address setup as I am runnin gthe servive and the client on the same machine and want to dynamically have the machine name in the endpoint address so that the relay service can register a unique address for the relaying.

Half time.
Now on to the consuming side. We similarily need to have the custom duplex binding but now want to explicitly specify the endpoint address for the callback. We will do this inside of the client code.

<configuration>

  <system.serviceModel>

    <bindings>

      <customBinding>

        <binding name ="duplexRelay">

          <compositeDuplex clientBaseAddress="SeeCode" />

          <oneWay />

          <binaryMessageEncoding />

          <relayTransport />

        </binding>

      </customBinding>   

    </bindings>

 

    <client>

      <endpoint name="RelayEndpoint"

                contract="PSC"

                binding="customBinding"

                bindingConfiguration="duplexRelay"

                address="SeeCode" />

    </client>

  </system.serviceModel>

</configuration>

This is the code to set the client's base callback address dynamically in code, derived from what I have shown here.

CustomBinding binding = (CustomBinding)channelFactory.Endpoint.Binding;

CompositeDuplexBindingElement duplex = binding.Elements.Find<CompositeDuplexBindingElement>();

Uri clientAddress = new Uri(

    String.Format(

        "net.relay://{0}/services/{1}/PubSub/callback/",

        RelayBinding.DefaultRelayHostName,

        GetHostName()));

duplex.ClientBaseAddress = clientAddress;

One thing we need to make sure with the custom duplex binding it to have the WS-Addressing ReplyTo header explicitly set on the outgoing message.

using (new OperationContextScope((IContextChannel)channel))

{

    Console.WriteLine("Calling Subscribe...");

 

    OperationContext.Current.OutgoingMessageHeaders.ReplyTo =

        ((IClientChannel)channel).LocalAddress;

 

    channel.Subscribe(input);

}

Likewise, on the service side, we need to add the To header to the outgoing message traveling to the original caller, i.e. the client.

public void Subscribe(string subMessage)

{

    Console.WriteLine("Subscribed...: {0}", subMessage);

 

    IPubSubCallback callback =

        OperationContext.Current.GetCallbackChannel<IPubSubCallback>();

 

    if (callback != null)

    {

        OperationContext.Current.OutgoingMessageHeaders.To =

            OperationContext.Current.IncomingMessageHeaders.ReplyTo.Uri;

        callback.Publish("Huhu!");

    }

}

To make a rather long story short, this is what happens when we run both the service and the client. We also tested this between Sri Lanka and Germany :) Buddhike was running the service and I the client: worked, I could reach his service and his service was calling back into my client - everything behind firewalls and NATs.
Everything happens in a safe fashion as we need to authenticate ourselves with a registered self-issued InfoCard for communication.

Oh, BTW, there is currently one caveat with my approach and the code: the first time the service wants to call back into the client some human being needs to select the appropriate card. This is by design of the current netRelayBinding from the BizTalk Services SDK and surely by design of CardSpace.
I am still investigating.

[Download sample code]