A Simple HAProxy Setup
2023-03-08I'm fortunate enough to have pretty fast Internet at home, but it comes with a downside: DS-Lite. This means I don't have my own public IPv4 address, only IPv6. It's a non-issue for outgoing connections (at least in my case, I have heard of reliability issues), but incoming connections are required to use IPv6.
In theory, this shouldn't be a problem either. Practically, IPv6 is still broken in many places, often making it impossible to access the things I'm hosting at home. The solution: Setting up a proxy.
Some Considerations
First of all, the proxy should run in TCP, not HTTP mode. It will simply forward all traffic without terminating TLS or changing HTTP headers. Certificate management will stay on the target server in my home network. I want to keep that server directly accessible over IPv6, only IPv4 should be proxied.
Furthermore, this setup allows for easily adding additional services like SSH to be accessible via the proxy.
HAProxy
HAProxy is popular, powerful and relatively easy to set up. It seems like a good fit. Based on an example from the manual, the simplest working haproxy.conf
to forward ports 80 and 443 looks like this:
global daemon user haproxy group haproxy maxconn 10 defaults mode tcp timeout connect 5000ms timeout client 50000ms timeout server 50000ms listen http-in bind *:80 bind *:443 server myserver example.com maxconn 10
This will listen on all addresses and forward all traffic to ports 80 and 443 to example.com
.
However, in this setup, those ports are blocked entirely for the machine hosting the proxy. We need a dedicated IPv4 address specifically for the proxy to use:
listen http-in bind ipv4@1.2.3.4:80 bind ipv4@1.2.3.4:443 server ipv6@myserver example.com maxconn 10
HAProxy also allows for hostnames in bind
-directives:
listen http-in bind ipv4@example.com:80 bind ipv4@example.com:443 server myserver ipv6@example.com maxconn 10
Keep in mind that in TCP mode, HAProxy does not really understand hostnames like it would in HTTP mode. They are simply resolved to IP addresses, it will not look at HTTP headers. Therefore, in TCP mode, it is not possible to sort traffic going to the same IP merely based on hostnames (which would be found in the "Host
" Header).
However, this is still useful for an easier configuration and in case the IP behind the hostname changes, which happens regularly in home networks.
Proxy Protocol
Anyway, the setup is working well, but one problem remains: The target server thinks all the requests are coming from the proxy, it does not see the real client's real IP! Ideally, this would be solved with http headers like X-Forwarded-For
. However, as mentioned above, HAProxy is running in TCP mode, rendering it unable to set http headers.
But there is a solution – HAProxy's own Proxy Protocol:
listen http-in
bind ipv4@example.com:80
bind ipv4@example.com:443
server ipv6@myserver example.com maxconn 10 send-proxy-v2 check
This will also need some reconfiguring on the backend, which in my case is nginx. It already supports the proxy protocol, so only some adjustment of the listen
-directive in nginx.conf
is necessary:
http { server { ... listen [::]:80 http2 proxy_protocol listen [::]:443 ssl http2 proxy_protocol set_real_ip_from my_proxy; # Replace my_proxy with the proxy's IP or hostname real_ip_header proxy_protocol; real_ip_recursive on; ...
The backend server can now see the client's real IP, but we've created a new issue: It cannot listen for connections using both the Proxy Protocol and regular http(s) on the same ports, meaning now it can only be reached via the proxy. Since only IPv4 traffic is supposed to be proxied, this is not acceptable. Fortunately, the solution is simple:
http { server { ... listen [::]:80 http2 listen [::]:8080 http2 proxy_protocol listen [::]:443 ssl http2 listen [::]:8443 ssl http2 proxy_protocol ...
Nginx will now listen for regular http requests on ports 80 and 443 and for requests using the Proxy Protocol on ports 8080 and 8443.
haproxy.conf
has to be adjusted accordingly. Separate listen
-Blocks are required to properly remap each port:
listen http-in timeout queue 10s bind ipv4@example.com:80 server myserver ipv6@example.com:8080 maxconn 10 send-proxy-v2 listen https-in timeout queue 10s bind ipv4@example.com:443 server myserver ipv6@example.com:8443 maxconn 10 send-proxy-v2
The result of this setup: With the correct DNS records set, IPv6 requests to example.com ports 80 and 443 will go directly to the server, whereas IPv4 requests will go the proxy. It will forward them to the target server over IPv6, using ports 8080 and 8443.
Health checks
As mentioned above, using hostnames instead of IP addresses in haproxy.conf
is useful if the IP changes. However, to make HAProxy automatically re-resolve the hostname, some more configuration is needed:
listen http-in timeout queue 10s bind ipv4@example.com:80 server myserver ipv6@example.com:8080 maxconn 10 send-proxy-v2 check resolvers mydns listen https-in timeout queue 10s bind ipv4@example.com:443 server myserver ipv6@example.com:8443 maxconn 10 send-proxy-v2 check resolvers mydns resolvers mydns parse-resolv-conf
check resolvers mydns
tells HAProxy to check if it can still reach the target server at example.com. If it can't, it will use the resolver "mydns" to resolve the hostname again. parse-resolv-conf
tells it to use the nameservers defined in /etc/resolv.conf
, i.e. the system default nameservers.