3

Is it possible to configure pound to handle WebSocket requests? If not, what are best alternatives for a reverse proxy? I'd like to use pound or a equivalent light-weight reverse proxy.

2
  • Doesn't it support websockets already? Websockets mimic normal HTTP communication so that existing proxies can handle them. Jan 26, 2013 at 21:18
  • @JanDvorak - Pound may have support, but it doesn't seem to work. In my experiments (which consisted of tcpdump captures on either side of the proxy), it appeared that Pound allowed the protocol upgrade, sent the first few bytes of payload back to the client, then simply stopped transmitting. I didn't experiment beyond determining that it doesn't work. I needed a solution, not a romp through Pound's source code. :-P
    – ghoti
    Jul 4, 2013 at 6:49

2 Answers 2

1

Pound appears to include code that supports the protocol upgrade, but I've never been able to make it work. Nor have various folks in forums and on the pound mailing list.

There's a pretty detailed post at exratione.com that describes a number of options for load balancing websockets behind SSL, including Pound (which the author also eventually gave up on). The conclusion of this post (which dates from early 2012) is that there is no good solution.

Since that post, nginx may have added websocket proxy support, so that's worth looking at. nginx is a bit more involved in terms of configuration, and IIRC has some limitations with regard to sticky session management, but it is a reliable, fast reverse proxy that supports SSL.

If you don't require SSL for your websocket connections, you might want to try a simple TCP load balancer. There are many to choose from -- HAProxy is well loved by Linux folks, but simple, high quality alternatives exist like Pen, OpenBSD's relayd (or its FreeBSD port), etc.

If you only need a reverse proxy in front of a single back-end server, and don't need to load balance, you can probably just use stunnel to receive front-end HTTPS/WSS connections and connect to an internal back-end. Here's some sample stunnel configuration. Alternately, you might be able to use stunnel in front of pen, but you'd have to experiment -- I haven't done that, and can't tell you if it'll work. (If you try, please let us know your results!)

Update:

HAProxy 1.5.0 was released on June 19th, 2014. This version includes native SSL support on both sides of the connection, which means that this is now my "preferred" solution for a WebSocket proxy. Configuration is incredibly easy:

frontend http-in
    ...
    bind 192.0.2.1:80     # if you want
    bind 192.0.2.1:443 ssl crt /etc/ssl/yadda.pem
    use_backend ws if { hdr(Upgrade) -i WebSocket }

backend ws
    server node1 192.168.1.111:8000
    server node2 192.168.1.112:8000

Or alternately, you could do this via hostname using an ACL:

frontend http-in
    ...
    acl is_ws hdr_end(host) -i ws.example.com
    use_backend ws if is_ws
1

@ghoti's answer worked well and I will probably stick to using stunnel as suggested but this issue nagged me nevertheless so I will expand on the comment of @JanDvorak who claimed having done some experiments without going into further detail.

I used the following simple python websocket server which is derived from https://gist.github.com/jkp/3136208

import struct
import SocketServer
from base64 import b64encode
from hashlib import sha1
from mimetools import Message
from StringIO import StringIO

class WebSocketsHandler(SocketServer.StreamRequestHandler):
    magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

    def setup(self):
        SocketServer.StreamRequestHandler.setup(self)
        print "connection established", self.client_address

    def handle(self):
        data = self.request.recv(1024).strip()
        headers = Message(StringIO(data.split('\r\n', 1)[1]))
        if headers.get("Upgrade", None) != "websocket":
            return
        print 'Handshaking...'
        key = headers['Sec-WebSocket-Key']
        digest = b64encode(sha1(key + self.magic).hexdigest().decode('hex'))
        response = 'HTTP/1.1 101 Switching Protocols\r\n'
        response += 'Upgrade: websocket\r\n'
        response += 'Connection: Upgrade\r\n'
        response += 'Sec-WebSocket-Accept: %s\r\n\r\n' % digest
        self.request.send(response)

        length = ord(self.rfile.read(2)[1]) & 127
        if length == 126:
            length = struct.unpack(">H", self.rfile.read(2))[0]
        elif length == 127:
            length = struct.unpack(">Q", self.rfile.read(8))[0]
        masks = [ord(byte) for byte in self.rfile.read(4)]
        decoded = ""
        for char in self.rfile.read(length):
            decoded += chr(ord(char) ^ masks[len(decoded) % 4])

        print decoded

        self.request.send(chr(129))
        length = len(decoded)
        if length <= 125:
            self.request.send(chr(length))
        elif length >= 126 and length <= 65535:
            self.request.send(126)
            self.request.send(struct.pack(">H", length))
        else:
            self.request.send(127)
            self.request.send(struct.pack(">Q", length))
        self.request.send(decoded)

        self.finish()

if __name__ == "__main__":
    server = SocketServer.TCPServer(
        ("localhost", 9000), WebSocketsHandler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print "Got ^C"
        server.server_close();
        print "bye!"

And I combined it with the following html which was borrowed from http://www.websocket.org/echo.html

<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">
  var wsUri = "ws://localhost:9000/";
  var output;
  function init() {
    output = document.getElementById("output");
    testWebSocket();
  }
  function testWebSocket() {
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }
  function onOpen(evt) {
    writeToScreen("CONNECTED");
    doSend("WebSocket rocks");
  }
  function onClose(evt) {
    writeToScreen("DISCONNECTED");
  }
  function onMessage(evt) {
    writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
    websocket.close();
  }
  function onError(evt) {
    writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
  }
  function doSend(message) {
    writeToScreen("SENT: " + message); 
    websocket.send(message);
  }
  function writeToScreen(message) {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    output.appendChild(pre);
  }
  window.addEventListener("load", init, false);
</script>
<h2>WebSocket Test</h2>
<div id="output"></div>
</html>

This worked well, so I put pound in the middle with the following config entry:

ListenHTTP
    Address 127.0.0.1
    Port    9999
    Service
            BackEnd
                    Address 127.0.0.1
                    Port    9000
            End
    End
End

and changed the port in the html from 9000 to 9999. After that change it stopped working.

Analyzing the traffic with wireshark, I found out that the HTTP 101 request to switch the protocol gets forwarded correctly. But the subsequent first websocket packet never gets forwarded by pound. This is confirmed by the print output of the python server script which never receives the WebSocket rocks message with pound in the middle.

Whenever pound receives a WebSocket message, it drops the message and instead writes e414 headers: request URI too long to syslog. Looking at the pound source code this seems to be because pound tries to parse HTTP headers. To do this it first searches for an EOL which it can't find in the WebSocket message and thus drops the message as invalid.

So it seems that the answer to OP's question indeed is: pound can't do WebSocket.

I wrote an email to the pound list about this issue: http://www.apsis.ch/pound/pound_list/archive/2014/2014-01/1388844924000

0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .