I recently started investigating using HAProxy to ensure that multiple
WebSocket connections from the same browser (or device) end up
communicating with the same application server. Forwarding all connections
from the same origin to the respective application server greatly reduces
the complexity on the application.
My setup is simple: 1 HAProxy in front, 2 App servers on the back. The
browser makes several connections via WebSockets (using ws:// or wss://).
My goal is to have those connections all go to the same app server. When a
new browser is opened on another PC or device then those will go to the
other server (round-robin), etc.
HAProxy was easy enough to configure and I decided to use the cookie
injection persistence method rather than URL parameter, HTTP header field,
or cookie modification (e.g. JSESSIONID) since it seemed the most
transparent and straightforward. However, I noticed it wasn't forwarding
the subsequent WebSocket connections to the same application server. I
started to sniff the packets to see what the HTTP response from HAProxy
looked like and found that the cookie wasn't present. I did notice,
however, that the cookie was present with non-WebSocket HTTP responses.
This led me to the source code proto_http.c to see if there was any code
that treated WebSocket handshakes differently. Nothing really stood out.
However, I did notice that around line 6305 there is a check for status
codes less than 200. In the event that the status code is less than 200 it
skips the header mangling. Note that WebSocket HTTP responses are usually
'101 Switching Protocols'. This means that the initial response from the
application server will not have the cookie injected into the response via
HAProxy. If I comment out the goto, things work as I would expect.
My question is, is this line really necessary? I understand that the HTTP
RFC indicates that for 1xx responses that the following header fields are
optional, but does that mean that the injection should not be done? What if
I were to update the code so that it was a little more stateful and
WebSocket aware so that in the event that the request/response was
WebSockets it would perform the header mangling?
Just as an aside, I did try the URL parameter method, and if I have the
browsers ws:// connection use a URL parameter things appear to work. That
may have to be my backup option, but it isn't as attractive given that I
have to modify the clients to use additional parameters and make sure they
are random or unique values. Again, it is an option, but not my first
choice. I suspect HTTP header field based persistence would work similar to
the URL parameter method. Note that IP-based persistence isn't attractive
because most addresses will be NAT'd and look as though they come from the
same address.
I am looking for some guidance and some history on this logic. Is this code
something that should be re-evaluated with the newer WebSocket protocol
gaining popularity?
Here is my configuration:
global
log 127.0.0.1 local0 debug
maxconn 4096
#debug
#quiet
user haproxy
group haproxy
defaults
log global
mode http
retries 3
timeout client 50s
timeout connect 5s
timeout server 50s
option dontlognull
option httplog
option redispatch
option logasap
option http-server-close
balance roundrobin
# Set up application listeners here.
listen admin
bind 0.0.0.0:22002
mode http
stats uri /
frontend http
maxconn 2000
bind 0.0.0.0:8080
default_backend servers-http
capture cookie SSNID len 63
backend servers-http
cookie SSNID insert nocache
server 03PM1 127.0.0.1:8081 cookie 03PM1 check
server 03PM2 127.0.0.1:8082 cookie 03PM2 check
Here is the code I am referring to in proto_http.c:
/* OK that's all we can do for 1xx responses */
if (unlikely(txn->status < 200))
goto skip_header_mangling;