OpenBSD version: 7.1 (Or any other thing that runs nginx)
Arch:            Any
NSFP:            Yes please, everyone.

The murder of Mahsa Amini in Iran ignited a fight against opression by the women of Iran. The Iranian government now tries to–as regimes usually do in these situations–stiffle its populations’ ability to organize and coordinate, while also limiting the ability to document atrocities commited in the pursuit of staying in power. In our modern times, this translates to ‘turn off the Internet’ and–if it is left on–censor/block any form of non-auditable communication.

The Signal messenger team hence asked for help in running signal proxies $somewhere on the Internet. Looking at the “code”, it is actually relatively simple: An nginx TCP proxy, listening on tcp/443, with a couple of static proxy statements for all important Signal endpoints. As TLS easily chains, it even means that the proxy can not snoop on the contents, and the clients can still validate certificates, which is nice.

On the other hand, setting up a proxy to just be, well, a proxy has some obvious draw-backs. First and foremost, you usually want to kind of hide what you are doing. The out-of-the-docker proxy, however, does not host any site. This is somewhat difficult, especially given that i’d assume that Iranian censorship partially works on rather manual inspection of heavy-hitter stuff (or Searle’s Manual AI… as i like to call it.) If you see a lot of TLS traffic for tcp/443 to a host that is not even hosting a website, you don’t really have to know what is going over that connection… surprise, metadata is data.

Co-location

What is far more feasible is seeing a lot of tcp/443 to some harmless website. Or rather, a ton of different harmless websites. I guess no censor bats an eye if users are having a lot of traffic to some stupid browser game (Solitair anyone?), funny cat videos, or whatever one might find. In addition, if any remote might be a proxy… that hiding becomes a lot easier (unless, of course, censors turn the Internet into an intranet… ) Also, usually people already do have websites and services around.

Adding a Signal reverse proxy to an existing site

If we now want to add a Signal reverse proxy to an existing site–i am assuming you’re running some kind of debian/ubuntu like image here–we can actually quiet easily do this by relying on the configs from the official Signal repo.

First, we have to add some stuff to /etc/nginx/nginx.conf; locate the stream { section, or (if you don’t have one yet) just make the additions below; Watch for the comments inline.

stream {

    upstream relay {
         server 127.0.0.1:4433;
    }

    # Main service listening for "HTTPs" connections
    server {
        # Listen for IPv6 and Legacy IPv4
        listen                443 ssl;
        listen                [::]:443 ssl;
        proxy_pass            relay;

        access_log            off;
        error_log             /dev/null;

        # Make sure that this is one certificate for all domains you are 
        # running on this server, i.e., using DNSAltNames; Change based
        # on how your system looks.
        ssl_certificate /etc/letsencrypt/active/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/active/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
     }

    map $ssl_preread_server_name $name {
        chat.signal.org                         signal-service;
        ud-chat.signal.org                      signal-service;
        textsecure-service.whispersystems.org   signal-service;
        storage.signal.org                      storage-service;
        cdn.signal.org                          signal-cdn;
        cdn2.signal.org                         signal-cdn2;
        api.directory.signal.org                directory;
        cdsi.signal.org                         cdsi;
        contentproxy.signal.org                 content-proxy;
        uptime.signal.org                       uptime;
        api.backup.signal.org                   backup;
        sfu.voip.signal.org                     sfu;
        updates.signal.org                      updates;
        updates2.signal.org                     updates2;
        default                                 websites;
    }

    upstream signal-service {
         server chat.signal.org:443;
    }

    upstream storage-service {
        server storage.signal.org:443;
    }

    upstream signal-cdn {
        server cdn.signal.org:443;
    }

    upstream signal-cdn2 {
        server cdn2.signal.org:443;
    }

    upstream directory {
        server api.directory.signal.org:443;
    }

    upstream cdsi {
        server cdsi.signal.org:443;
    }

    upstream content-proxy {
        server contentproxy.signal.org:443;
    }

    upstream backup {
        server api.backup.signal.org:443;
    }

    upstream sfu {
        server sfu.voip.signal.org:443;
    }

    upstream updates {
        server updates.signal.org:443;
    }

    upstream updates2 {
        server updates2.signal.org:443;
    }

    # This might have to be different for you; We will make additional changes
    # to your vhosts later, and this has to be the address _they_ are listening
    # on.
    upstream websites {
        server 127.0.0.1:80;
    }

    # Make sure this listens on local host.
    server {
        listen                127.0.0.1:4433;
        proxy_pass            $name;
        ssl_preread           on;
        error_log             /dev/null;
        access_log            off;
     }
}

So, what does this do? We create two stream servers. The first one publicly listens on tcp/443 and will terminate TLS for us. Anything connecting here gets its outer TLS shell stripped, and is then forwarded to the second stream server listening on 127.0.0.1:4433. This server will then look at the second (encapsulated) TLS stream’s SNI (Server Name Indication) to figure out where to redirect things. If either SNI is none of the pre-configured names for signal, or if the stream is not a TLS stream, it gets forwarded to 127.0.0.1:80.

This allows us to run our actualy websites on 127.0.0.1:80. Even virtual hosts will work just fine, given that HTTPS is just HTTP in a TLS tunnel (which we just stripped). Connections to signal services remain properly forwarded and secured by TLS directly between the client and server, without our proxy laying its hands on the content.

Adjusting vHosts

If you had already been running something on your nginx, you most likely already have some virtual hosts configured which look somewhat like this:

http {
        ...
        server {
                listen       443;
                server_name  www.example.com example.com;

                ssl_certificate /etc/letsencrypt/active/fullchain.pem;
                ssl_certificate_key /etc/letsencrypt/active/privkey.pem;
                include /etc/letsencrypt/options-ssl-nginx.conf;
                ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

                root         /var/www/www.example.com/htdocs;
                ...
        }
        ...
}

As we now have another service listening on tcp/443, our virtual hosts obviously can’t continue to do so. So, what we do is just drop TLS for these (will be done by the stream server we setup earlier; really make sure nothing else listens on :443), and make them listen on 127.0.0.1:80:

http {
        ...
        server {
                #listen       443;
                listen        127.0.0.1:80;
                server_name  www.example.com example.com;

                #ssl_certificate /etc/letsencrypt/active/fullchain.pem;
                #ssl_certificate_key /etc/letsencrypt/active/privkey.pem;
                #include /etc/letsencrypt/options-ssl-nginx.conf;
                #ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

                root         /var/www/www.example.com/htdocs;
                ...
        }
        ...
}

Make sure to do this for all vhosts which you may have configured.

And that’s it; Do a quick service nginx reload, and your Signal proxy should be ready to go, while your site remains accessible (test that, though…).

Testing the proxy

Before communicating your proxy to the rest of the world (or rather not), you should test it. For that, fire up your Signal app, and go to "Settings" -> "Data and storage" -> "Use proxy", activate it, and enter (one of your) configured FQDN(s). If signal can connect through the proxy, it should give you a visual indication of things working.

In addition, you can always fire up tcpdump on your machine to see whether sending messages on your phone actually directs traffic over your box.

Summary

So, that’s it. You have a Signal proxy next to a benign website. Of course, technically, that proxy can still be rather easily probed. Nevertheless, it should be a bit less of a public lighthouse…