Websockify & noVNC behind an NGINX Proxy

At Fathom Data we are developing a framework which will enable remote access to Linux desktops via a browser. There’s nothing new to this idea. However, we have a very specific application in mind, so we need to roll our own solution. Importantly, there need to be multiple independent connections catering for a group of users. In this post I’ll show how we used the following tools to make this possible:

  • XFCE — a lightweight desktop environment;
  • TigerVNC — an efficient VNC server;
  • noVNC — a VNC client library written in JavaScript;
  • Websockify — a bridge between TCP and WebSockets; and
  • NGINX — a web server and reverse proxy.

What is VNC? 

VNC (Virtual Network Computing) is a system for desktop sharing, allowing remote access to a desktop session. Typically a VNC server would be run on the machine which is sharing the desktop. A VNC client would be run on another machine (although, in principle, it could also be run on the same machine as the VNC server). The client would then remotely access the desktop on the server.

VNC does not cater for independent users. Each user who wants to share their desktop must launch their own VNC server.

Communication between the server and client happens over TCP using the VNC or RFB (Remote FrameBuffer) protocol (default port is 5900). Although not a significant hurdle, in order to access a remote desktop, VNC requires that you have a suitable VNC client installed. We wanted to avoid this potential obstacle.

What about noVNC? 

noVNC is a HTML client for VNC. Using noVNC it’s possible to connect to a VNC server from a browser. This removes the requirement of a dedicated VNC client.

This is a vital component of our system because we can be fairly confident that every user should have access to a browser, either on their computer or phone.

Why do I Need Websockify? 

We also need to factor in Websockify. Why? Well, first we need to understand the difference between HTTP and WebSocket connections. HTTP and WebSocket are both communication protocols that are used to transfer information between a client and a server. However, there are some important differences.

HTTP is a stateless, unidirectional protocol. The client sends a request to the server. The server then sends a response back to the client. Then the connection between client and server is closed. Each request and response happens on a new connection (this is the “stateless” characteristic).

WebSocket, by contrast, is a stateful (or persistent), bidirectional (or full-duplex) protocol. Once a connection is established between the client and the server, an unlimited number of requests and responses can be relayed. Because of its persistence, a WebSocket connection is generally used when there is going to be a consistent stream of data moving back and forth between the client and server. Because there is no overhead with creating new connections, a WebSocket connection is faster than an HTTP connection and so is often used for real-time applications.

The URL scheme (the first part of the URL) associated with HTTP is http (or https for HTTPS, the encrypted version of HTTP). The corresponding scheme for WebSocket is ws (or wss for the encrypted flavour).

But I digress. We need Websockify because it acts as a proxy between noVNC (which runs in the browser) and the VNC server. Specifically, noVNC uses WebSocket to communicate. However, the VNC server doesn’t understand WebSocket. Websockify acts as a WebSocket translator for the VNC server.

noVNC is a web application written in HTML, CSS and JavaScript. It communicates using the WebSocket protocol. VNC is a desktop sharing application. It communicates using the VNC protocol. Websockify acts as a proxy, translating between these two protocols.

Setup 

We’re going to build the system as a series of layers, starting with a simple VNC server, then adding Websockify, and finally putting it all behind an NGINX proxy.

First install the components.

apt-get update
apt-get install xfce4 tigervnc-standalone-server novnc websockify nginx

Just VNC 

The first step is to start the VNC server. You’ll need to create a ~/.vnc/xstartup file which tells VNC what desktop to launch.

#!/bin/bash

startxfce4 &

We’re going to be running XFCE, but you could equally launch Gnome, KDE or LXDE. Whatever your personal preference. It does, however, make sense to use a lightweight desktop, especially if you are planning on catering for a large number of users.

Make that file executable.

chmod u+x ~/.vnc/xstartup

Now launch the VNC server.

vncserver \
    -localhost no \
    -geometry 1024x768 \
    -SecurityTypes None --I-KNOW-THIS-IS-INSECURE \
    :0

Let’s unpack those options:

  • -localhost no — Accept connections from sources other than 127.0.0.1.
  • -geometry 1024x768 — The size of the window.
  • -SecurityTypes None — Don’t prompt for a password. (Remove this in production!)

The :0 at the end of the command specifies that the VNC server will be connected to display 0. You can launch multiple instances of VNC, each connected to a different display. For example, :1 for display 1 and :2 for display 2. Connections to the VNC server on display 0 are via port 5900. The port number is found by adding 5900 to the display number. So displays 1 and 2 would be accessible via ports 5901 and 5902.

Once the VNC server is running you can connect to the desktop at 127.0.0.1:5900 using any VNC client. 💡 The desktop below is for user alice (see top-left corner of desktop).

Layering on noVNC & Websockify 

It’d be much more convenient to access the desktop via a browser. This is where noVNC and Websockify come into the picture. We don’t invoke noVNC directly. Rather we kick off Websockify and point it to the folder that contains noVNC. If you take a look into /usr/share/novnc/ you’ll find two HTML files, vnc.html and vnc_lite.html, along with a bunch of JavaScript and CSS. We’re not “running” noVNC as such. It’s just a web site which is going to be served by Websockify.

websockify -D \
    --web /usr/share/novnc/ \
    6080 \
    localhost:5900

Let’s unpack those options too:

  • --web /usr/share/novnc/ — Where to find the source for the noVNC web site.
  • 6080 — Port to access on which the desktop will be served.
  • localhost:5900 — Where to find VNC.

When Websockify has started you can connect to the desktop at 127.0.0.1:6080/vnc.html using a browser. 💡 It’s possible to enable HTTPS by pointing to an SSL certificate via the --cert option to websockify.

NGINX Proxy 

The current setup, with noVNC, Websockify and a VNC server, certainly does the job: we can access the desktop via a browser. However, there’s one major snag: you need to provide a specific port number in the URL. In principle this is not an issue. However, it’s going to get messy if we are running multiple desktops, each of which is accessible via its own port.

To make this more seamless we’ll put everything behind an NGINX reverse proxy. Furthermore, we’ll also use NGINX to serve a simple static page which will act as an index to the desktop(s).

Here’s a massively boiled down version of the index page. It consists of just a single link to Alice’s desktop.

<a href="http://127.0.0.1/novnc/vnc.html?resize=remote&
path=novnc/websockify">Alice</a>

A few things to observe about this link:

  1. The URL doesn’t include a port number. NGINX will proxy requests based on the path /novnc/.
  2. The path parameter tells noVNC where to look for Websockify.
  3. The resize parameter enables dynamic scaling of the desktop size to fit in the browser. 🚀

Now we need to set up NGINX. Here’s the nginx.conf configuration file.

user www-data;
daemon off;

events {
}

http {
  server {
    listen 80 default_server;
    
    location / {
      root /www/data;
      try_files $uri $uri/ /index.html;
    }
    
    location /novnc/ {
      proxy_pass http://127.0.0.1:6080/;
    }
    
    location /novnc/websockify {
      proxy_pass http://127.0.0.1:6080/;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
      proxy_set_header Host $host;
    }
  }
}

NGINX will listen on port 80. The three location blocks determine what happens with a request.

  • location / — Serve the static index page at the root.
  • location /novnc/ — Proxy HTTP requests for /novnc/ to Websockify on port 6080.
  • location /novnc/websockify — Proxy WebSocket requests for /novnc/websockify to Websockify on port 6080 and upgrade connection.

Now start NGINX.

nginx

Visiting 127.0.0.1 now provides a (very) simple index page.

Clicking on the solitary link takes you to the noVNC connection page.

And clicking on the big button then takes you through to the desktop.

This implementation has a couple of improvements:

  • automatic scaling of the desktop size with the size of the browser; and
  • it’s being served on port 80, the standard HTTP port, so doesn’t require any special networking considerations.

Multiple Desktops 

Okay, so we’ve got it working for a single desktop. Nice! But the goal was to accommodate multiple desktops. Luckily we’ve already done most of the hard work. Now it’s just a matter of tweaking the details.

If you wanted to enable multiple desktops then you’d probably be running an instance of VNC and Websockify for each user, which means a bunch of ports that need to be exposed. However, using NGINX and judicially proxying an URI to each Websockify port you need only expose port 80 (or 443 if you’re using SSL).

Suppose that we also wanted to provide a desktop for Bob. First update the index page.

<a href="http://127.0.0.1/novnc/alice/vnc.html?resize=remote&
path=novnc/alice/websockify">Alice</a>
<a href="http://127.0.0.1/novnc/bob/vnc.html?resize=remote&
path=novnc/bob/websockify">Bob</a>

💡 The targets of the <a> tags and the values of the path parameters are now slightly more elaborate and include the usernames for Alice and Bob. NGINX will use this path to dispatch requests to separate instances of noVNC and connect to a distinct Websockify sessions.

We’ll launch a second VNC server, this time on display 1 (so it’s accessible via port 5901).

vncserver \
    -localhost no \
    -geometry 1024x768 \
    -SecurityTypes None --I-KNOW-THIS-IS-INSECURE \
    :1

And we’ll also need a separate instance of Websockify, which will now run on port 6801 and connect to noVNC on port 5901.

websockify -D \
    --web /usr/share/novnc/ \
    --cert /etc/ssl/novnc.pem \
    6081 \
    localhost:5901 \
    2>/dev/null

Now we just need to update the NGINX configuration to handle this setup.

user www-data;
daemon off;

events {
}

http {
  server {
    listen 80 default_server;
    
    location / {
      root /www/data;
      try_files $uri $uri/ /index-both.html;
    }
    
    location /novnc/alice/ {
      proxy_pass http://127.0.0.1:6080/;
    }
    
    location /novnc/alice/websockify {
      proxy_pass http://127.0.0.1:6080/;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
      proxy_set_header Host $host;
    }
    
    location /novnc/bob/ {
      proxy_pass http://127.0.0.1:6081/;
    }
    
    location /novnc/bob/websockify {
      proxy_pass http://127.0.0.1:6081/;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
      proxy_set_header Host $host;
    }
  }
}

There’s now a second pair of location entries for Bob. And the /novnc/ locations now also include the username in the path, which means that they match the URLs specified in the index.

If there are many users then the NGINX configuration can become bulky and difficult to maintain by hand. We’re using Python and the Jinja templating engine to automatically generate our nginx.conf. The same setup is being used to create the static index page, which is populated with a list of accounts for each of the users.

It’s possible that there are simpler ways to do this (in which case I’d love to learn about them!), but this does the job. And I’m going to be pragmatic: if it works, then it’s good enough!