Simple SPDY and NPN Negotiation with HAProxy

SPDY is an experimental protocol developed at Google, designed to reduce the latency of web pages. Specifically, its goal is to address the limitations of HTTP 1.1 and to remove existing bottlenecks: head of line blocking, inefficient use of underlying TCP connections, and header bloat amongst others. However, while all of this sounds great in writing, deploying a new protocol on the web, in practice, is fraught with difficulty.

Specifically, the many layers of HTTP routers, caches, and custom appliances will often behave in unpredictable ways when presented with an alternate protocol, leading to randomly dropped connections and frustrated users. To address this, SPDY is delivered via SSL: the end-to-end encrypted tunnel allows the client and the server to exchange SPDY frames without intervention by intermediate nodes. It's important to note that SPDY does not require SSL, but in practice, SSL is a pragmatic choice to get to a working solution.

Next Protocol Negotiation (NPN)

But how does the client and server know to use SPDY once the SSL tunnel is opened? This is where Next Protocol Negotiation (NPN) enters the picture. NPN is an SSL extension which allows the client and the server to negotiate the application protocol as part of the SSL handshake.

NPN eliminates the extra roundtrip to negotiate the application protocol - contrast this to the current WebSocket handshake, which imposes another roundtrip of latency on top of the SSL negotiation. If you're counting, that is one RTT for the TCP handshake, two for SSL negotiation, and one for WebSocket upgrade - four RTT's before any application data can be exchanged!

Support for NPN was added in OpenSSL (1.0.0d+), NSS, and TLSLite. Chrome, Firefox, and Opera support SPDY (and hence NPN also), as well as many of the popular servers: Apache, nginx, Jetty, and node.js amongst others. That said, the requirement for NPN support has nonetheless been a barrier to wider adoption. That is, until HAProxy decided to make everyone's life a lot easier! Let's take a look at a hands on example.

Building HAProxy with NPN support

Early SSL support landed in HAProxy development builds in early September and has been rapidly improving ever since. A notable and a recent addition is, you guessed it, support for NPN negotiation! To make it work, download the latest development tarball, or do a checkout from git and build the binary against a recent version of OpenSSL:

$> wget http://www.openssl.org/source/openssl-1.0.1c.tar.gz
$> tar -zxvf openssl* && cd openssl*
$> ./configure --prefix=/tmp/openssl --openssldir=/tmp/openssl darwin64-x86_64-cc
$> make && make install

$> git clone http://git.1wt.eu/git/haproxy.git/ && cd haproxy
$> make TARGET=generic USE_OPENSSL=1 ADDINC=-I/tmp/openssl/include ADDLIB=-L/tmp/openssl/lib -lssl
$> ./haproxy -vv
  # Built with OpenSSL version : OpenSSL 1.0.1c 10 May 2012
  # OpenSSL library supports TLS extensions : yes

The above instructions should build the latest version of HAProxy on OSX. If you already have OpenSSL 1.0.0d+, then you can skip the first step, as well as the extra ADDINC and ADDLIB directives. With that, we're almost ready to handle SPDY alongside our regular HTTP and HTTPS traffic!

Configuring HAProxy for HTTP, HTTPS, and SPDY

What we want to do is to configure our HAProxy as an SSL termination proxy. Meaning, HAProxy will be the one serving our SSL certificate back to the client, and all traffic forwarded to our internal servers will flow unencrypted. This also means that HAProxy will need to handle the NPN handshake. In fact, ideally, it should handle and route all types of traffic: HTTP, HTTPS, and SPDY.

defaults
  log 127.0.0.1 local0

# accept connections on port 80, forward requests to "http_cluster"
frontend http
  mode http
  bind :80
  default_backend http_cluster

# accept connections on port 443 (SSL)
# - forward SPDY connections to spdy_cluster
# - forward regular HTTPS connections to http_cluster
# - ha.pem should be a concatenated file: certificate first then the key
frontend secure
  mode tcp

  # advertise spdy/2 support via NPN
  bind :443 ssl crt ./certs/ha.pem npn spdy/2
  use_backend spdy_cluster if { ssl_npn -i spdy/2 }

  default_backend http_cluster

# SPDY server running on port 10000
backend spdy_cluster
  server srv01 127.0.0.1:10000

# HTTP server running on port 8000
backend http_cluster
  mode http
  server srv01 127.0.0.1:8000

Simple as that. HAProxy accepts the SSL connection, performs the SSL handshake and advertises spdy/2 support if client supports NPN. If the client selects spdy/2 then the request is routed to the SPDY server. Otherwise, the HTTPS request is forwarded to the HTTP cluster. The important bit is that in both cases, the forwarded traffic is no longer encrypted - the SPDY server does not need to perform the SSL handshake or worry about NPN, instead it simply has to parse and return the raw SPDY frames back to the proxy.

NPN is not just for SPDY

To see SPDY in action, you can checkout and run the demo SPDY server written in Ruby behind HAProxy. With native NPN support in HAProxy, adding SPDY support has never been easier - you are literally just a few rules away from intelligently routing incoming SPDY requests alongside the rest of your traffic. No need to downgrade your SPDY connection to HTTP, punch holes in your infrastructure to expose NPN capable serves, or deploy parallel infrastructure.

Best of all, while the above example is specific to SPDY, the NPN mechanism works for any new, or existing protocol. As an example, we can eliminate the extra roundtrip in WebSocket negotiation with NPN, or we can deploy other, new and experimental protocols without imposing any additional latency penalties.

Ilya GrigorikIlya Grigorik is a web ecosystem engineer, author of High Performance Browser Networking (O'Reilly), and Principal Engineer at Shopify — follow on Twitter.