Introduction
Halibut is an open source, secure communication stack for .NET and Mono. It uses a JSON-RPC style protocol over SSL, and is a lightweight alternative to WCF.
The source code and samples in this article are available on GitHub.
Background
Halibut came about as an attempt to build a secure alternative to WCF for use in Octopus Deploy, an automated deployment product for .NET developers.
As an automated deployment tool, Octopus needs to be able to push packages and configuration information to machines that might be on the local network or in the cloud. To do this securely, users of Octopus establish a two-way trust relationship:
- The Octopus server, which co-ordinates deployments, needs to know it is sending packages and configuration information to a machine that it trusts, not an imposter.
- The Tentacle agent, which receives and installs the packages, needs to know that it is receiving packages from an Octopus server that it trusts, and not an imposter.
To do this, Octopus uses public/private key cryptography. On installation, the Octopus and Tentacle servers both generate an X.509 certificate. The administrator who sets up each machine then pastes the thumbprint of the public key of each machine to the other, establishing the trust.
In Octopus Deploy, we use those X.509 certificates with WCF's wsHttpBinding stack. When the connection is established, each side verifies the thumbprint of the certificate presented by the other party. If the public key isn't what we expect, we reject the connection.
(You can read more about how this works on our reference page.)
In a future release, we'd love to add Mono support to Octopus, so that we can deploy packages to Linux and other Unix-like operating systems. But there's a problem: WCF's wsHttpBinding isn't supported on Mono!
Faced with this problem, we needed to come up with an alternative communication stack.
Goals of Halibut
When building Halibut, we had a few goals in mind:
- Open source: We felt that making the code for our communication stack open to the world would help to increase the security of such a critical piece of our architecture.
- Secure: We still wanted public/private key cryptography to be the cornerstone of our secure communication stack.
- Simple: We wanted to write the least code possible, and to make it super easy to get started with.
- Friendly: Error messages can be bad enough, but security types are especially good at writing convoluted error messages that users can never understand. We wanted to make sure that any errors coming out of our stack would be easy to understand.
- Stand on the shoulders of giants: We wanted to leverage code written by people who know far more about implementing secure communication stacks than ourselves. Our goal would just be to join a few pieces together into an easy to use package.
- Works on Mono and .NET: We needed to choose technologies that would work on both platforms.
After lots of experimentation, we ended up building Halibut on top of:
- TcpListener/TcpClient, to bind to the TCP sockets. We use raw TCP rather than HTTP to avoid relying on HTTP.sys under Windows, since it requires additional permissions to work with.
- SslStream. This class is baked into .NET and Mono, and takes care of encrypting the connections between the two services using SSL. It handles presenting the certificates and negotiating the encryption levels to use. We use TLS 1.0.
- Json.NET: This has become the standard for JSON on .NET and Mono, so it was the logical choice when it came to serializing the objects to send on the wire. Using JSON also means that we can more easily handle backwards compatibility and versioning than if we'd chosen a proprietary binary format.
Once a secure connection is established, Halibut uses a protocol based on
JSON-RPC. A request is sent, containing the method name to call and arguments. The server processes the request and returns a response object. If an error occurs, Halibut catches it and returns an error object, and the error is rethrown on the client.
Show us the code!
To get started with Halibut, first define an interface for your service:
public interface ICalculatorService
{
long Add(long a, long b);
long Subtract(long a, long b);
}
There's no need to decorate it with anything, Halibut assumes all methods on the interface are available for RPC. On your server, implement the interface:
public class CalculatorService : ICalculatorService
{
public long Add(long a, long b)
{
return a + b;
}
public long Subtract(long a, long b)
{
return a - b;
}
}
Next, load a certificate which your server will use to identify itself. The certificate needs to have a public key as well as a private key. To make it easy to create certificates, the sample code comes with an executable (Halibut.CertificateGenerator.exe) which you can call like this:
Halibut.CertificateGenerator.exe CN=YourApplication YourApplication.pfx
This creates a file containing the public and private key of a certificate identifying YourApplication in the example above.
Load the certificate in your server:
var certificate = new X509Certificate2("HalibutServer.pfx");
Finally, start your service. Here we are listening on port 8433:
var endPoint = new IPEndPoint(IPAddress.Any, 8433);
var server = new HalibutServer(endPoint, certificate);
server.Services.Register<ICalculatorService, CalculatorService>();
server.Start();
Now we need to connect our client. Again, use Halibut.CertificateGenerator.exe to generate a certificate, and load it on your client:
var certificate = new X509Certificate2("HalibutClient.pfx");
Now connect to the server:
var serverThumbprint = "EF3A7A69AFE0D13130370B44A228F5CD15C069BC";
var client = new HalibutClient(certificate);
var calculator = client.Create<ICalculatorService>(new Uri("rpc://" + hostName + ":8433/"), serverThumbprint);
var result = calculator.Add(12, 18);
Note that in the example above, our client specifies the thumbprint of the public key of the server. This way, our client will abort the connection if the server identifies itself as a server we weren't expecting.
Our server is currently accepting connections from any valid client however. We can change this by adding a validation callback to the server before calling Start():
server.Options.ClientCertificateValidator = ValidateClientCertificate;
...
static CertificateValidationResult ValidateClientCertificate(X509Certificate2 clientcertificate)
{
return clientcertificate.Thumbprint == "2074529C99D93D5955FEECA859AEAC6092741205"
? CertificateValidationResult.Valid
: CertificateValidationResult.Rejected;
}
Clients
The HalibutClient class is a factory that creates instances of proxies that connect to a Halibut server. When you call:
var calculator = halibutClient.Create<ICalculatorService>(...);
HalibutClient returns a proxy (implemented using RealProxy). When a method is called on the proxy, Halibut connects to the server, serializes the JSON requests, and sends them. It deserializes the result from the server, and returns the result as the result of the method call. If the server returns an error, or the connection fails, the proxy will throw.
Halibut doesn't use sessions and doesn't keep connections open, so each call is completely stateless.
Extension points
The HalibutServer class provides an Options property which can be used to customize and extend Halibut:
- Options.ServiceFactory: If you want to control how services are constructed (e.g., to call an IOC container), implement IServiceFactory. Construct the object, returning it in a disposable object that will be disposed when the service call is complete.
- Options.Serializer: returns the Json.NET serializer that Halibut uses. Use it to customize serialization settings.
- Options.ServiceInvoker: control how methods on the service are invoked.
Logging
Halibut automatically logs messages using trace sources, and they are compatible with the WCF service trace viewer. To enable logging, add the following to your configuration file:
="1.0"="utf-8"
<configuration>
<system.diagnostics>
<sources>
<source name="Halibut.Client" switchValue="Information, ActivityTracing">
<listeners>
<add name="xml"/>
<add name="console" />
</listeners>
</source>
<source name="Halibut.Server" switchValue="Information, ActivityTracing">
<listeners>
<add name="xml"/>
<add name="console" />
</listeners>
</source>
</sources>
<switches>
<add name="Halibut.Client" value="Verbose" />
<add name="Halibut.Server" value="Verbose" />
</switches>
<sharedListeners>
<add name="xml" type="System.Diagnostics.XmlWriterTraceListener" initializeData="starling.e2e" />
<add name="console" type="System.Diagnostics.ConsoleTraceListener" />
</sharedListeners>
<trace autoflush="true" />
</system.diagnostics>
</configuration>
Halibut sends trace activity ID's over the wire too, so if you use the XML logger above, you can open both the client and server log messages in the same Service Trace Viewer instance to see the log messages joined together, giving you beautiful end-to-end tracing.
Summary
Halibut was built as an alternative to using WCF's wsHttpBinding, primarily because wsHttpBinding isn't supported on Mono, and we needed something to use. It is open source, simple, and secure. I hope you find a use for it in your projects too, or at least, you enjoy reading the code. Thanks!