Haproxy is a pretty nifty product. Probably not the least due to the fact that it’s author, Willy Tarreau spends hours of his life helping others in setting it up the way they want, sometimes fixing a bug in the process. The latest release marks the end to a 4-year long bugfix that finally went live. It is this sort of things, features that have been wanted for years, now incorporated into the software helping sysadmins and programmers around the world in fulfilling corporate IT needs. Haprpxy to me is more than a loadbalancer, you can piece together solutions as a workaround for more fundamental problems.

That’s what I recently did. A case of a failing network (NAT problems) that wasn’t easily solvable due to the need of having a box around with 2 patched network cards. NAT using the same interface is just not working right. We just needed some outgoing SSL calls to go to the internet and back, through some portforwarded upstream firewall setup. That setup sucks by being limited to only having a private IP range, we have public IP’s that map to the internal one but that is a firewall hell as packets arrive at the port from a range that doesn’t really belong to the network cards IP range.

So in comes HAPROXY, let’s reverse the thinking and consider putting haproxy ‘in front’ of those SSL requests at port 443. We will just hack up the /etc/hosts file or the local DNS to make sure they endup on haproxy instead of directly to the target public IP:

global
   log 127.0.0.1  local0
   log 127.0.0.1  local1 notice
   #log loghost   local0 info
   maxconn 4096
   # chroot /usr/share/haproxy
   user haproxy
   group haproxy
   daemon
   #debug
   #quiet

defaults
   log   global
   mode  http
   option   httplog
   option   dontlognull
   retries  3
   option redispatch
   maxconn  2000
   contimeout  5000
   clitimeout  50000
   srvtimeout  50000

# Host HA-Proxy web stats on Port 3306 (that will confuse those script kiddies)
listen HAProxy-Statistics *:3306
    mode http
    option httplog
    option httpclose
    stats enable
    stats uri /haproxy?stats
    stats refresh 20s
    stats show-node
    stats show-legends
    stats show-desc Workaround haproxy for SSL
    stats auth admin:ifIruledTheWorld

frontend ssl_relay 192.168.128.21:443
    # this only works with 1.5 haproxy
    mode tcp
    option tcplog
    option socket-stats
    # option nolinger
    maxconn  300

    # use tcp content accepts to detects ssl client and server hello.
    # acl clienthello req_ssl_hello_type 1 -> seems to not work

    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }

    use_backend ssl_testdomain_prod if { req_ssl_sni -i www.testdomain.nl }
    use_backend ssl_testdomain_stag if { req_ssl_sni -i test.testdomain.nl }

    default_backend ssl_testdomain_stag

backend ssl_testdomain_stag
   mode tcp
   #option nolinger
   option tcplog
   balance roundrobin
   hash-type consistent
   option srvtcpka

    # maximum SSL session ID length is 32 bytes.
    stick-table type binary len 32 size 30k expire 30m

    # make sure we cover type 1 (fallback)
    acl clienthello req_ssl_hello_type 1
    acl serverhello rep_ssl_hello_type 2

    # use tcp content accepts to detects ssl client and server hello.
    tcp-request inspect-delay 5s
    tcp-request content accept if clienthello

    # no timeout on response inspect delay by default.
    tcp-response content accept if serverhello

    # SSL session ID (SSLID) may be present on a client or server hello.
    # Its length is coded on 1 byte at offset 43 and its value starts
    # at offset 44.
    # Match and learn on request if client hello.
    stick on payload_lv(43,1) if clienthello

    # Learn on response if server hello.
    stick store-response payload_lv(43,1) if serverhello

    #option ssl-hello-chk

    server x_testdomain_stag 123.123.123.123:443


backend ssl_testdomain_prod
   mode tcp
   #option nolinger
   option tcplog
   balance roundrobin
   hash-type consistent
   option srvtcpka

    # maximum SSL session ID length is 32 bytes.
    stick-table type binary len 32 size 30k expire 30m

    # make sure we cover type 1 (fallback)
    acl clienthello req_ssl_hello_type 1
    acl serverhello rep_ssl_hello_type 2

    # use tcp content accepts to detects ssl client and server hello.
    tcp-request inspect-delay 5s
    tcp-request content accept if clienthello

    # no timeout on response inspect delay by default.
    tcp-response content accept if serverhello

    # SSL session ID (SSLID) may be present on a client or server hello.
    # Its length is coded on 1 byte at offset 43 and its value starts
    # at offset 44.
    # Match and learn on request if client hello.
    stick on payload_lv(43,1) if clienthello

    # Learn on response if server hello.
    stick store-response payload_lv(43,1) if serverhello

    #option ssl-hello-chk

    server x_testdomain_prod 123.123.111.111:443

So what does it do? We will send all request to our internal IP (frontend .21) , who will split this up according to SNI information , if that fails it will go to the ‘staging’ backend. This should only happen to software which doesn’t support this feature. Curl for example knows how to use SNI. Wget also knows. All modern browsers do in fact.

It will analyse the SNI information and play SSL proxy for us. Haproxy is a good puppy! And as sugar on the pie, you can check the stats to see how much has been routed. It’s the other way around, an internal IP is the frontend, while the backends reside on a public IP.

This works for me on the following version

root@server:/usr/sbin# haproxy -V
HA-Proxy version 1.5-dev19 2013/06/17

The only thing left to do is trick DNS by overriding the official target IP’s by your internal IP address.