Cloudflare Argo Tunnel with Rust+Raspberry Pi

Yesterday Cloudflare launched Argo Tunnel. In the words of the product team:

Argo Tunnel exposes applications running on your local web server, on any network with an Internet connection, without adding DNS records or configuring a firewall or router. It just works.

Once I grokked this, the first thing that came to mind was that I could actually use one of my Raspberry Pi’s sitting around to serve a website, without:

  • A flaky DDNS running on my router
  • Exposing my home network to the world
  • A cloud VM

Ooooh… so exciting.

The Rig

I’ll assume you already have a Raspberry Pi with Raspbian on it.

Cloudflare Argo Tunnel with Rust+Raspberry Pi

Plug the Pi into your router. It should now have an IP address. Look that up in your router’s admin UI:

Cloudflare Argo Tunnel with Rust+Raspberry Pi

OK, that’s promising. Let’s connect to that IP using the default pi/raspberry credentials:

$ ssh 192.168.8.26 -l pi
[email protected]'s password: 

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Mar 18 23:24:11 2018 from stevens-air-2.lan
[email protected]:~ $ 

We’re in!

Pro tip: quick way to figure it out which type you have is

[email protected]:~ $ cat /proc/cpuinfo | grep 'Revision' | awk '{print $3}' | sed 's/^1000//'
a22082

Then look up the value in the Raspbery Pi revision history. I have Raspberry Pi 3 Model B

Internet connectivity

OK, so we have a Pi connected to our router. Let’s make 100% sure it can connect to the Internet.

[email protected]:~$ $ curl -I https://www.cloudflare.com
HTTP/2 200
date: Tue, 20 Mar 2018 22:54:20 GMT
content-type: text/html; charset=utf-8
set-cookie: __cfduid=dfb9c369ae12fe6eace48ed9b51aedbb01521586460; expires=Wed, 20-Mar-19 22:54:20 GMT; path=/; domain=.cloudflare.com; HttpOnly
x-powered-by: Express
cache-control: no-cache
x-xss-protection: 1; mode=block
strict-transport-security: max-age=15780000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
served-in-seconds: 0.025
set-cookie: __cflb=3128081942; path=/; expires=Wed, 21-Mar-18 21:54:20 GMT
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 3febc2914beb7f06-SFO-DOG

That first line HTTP/2 200 is the OK status code, which is enough to tell us we can connect out to the Internet. Normally this wouldn’t be particularly exciting, as it’s allowing connections in that causes problems. That’s the promise of Argo Tunnels however, it says on the tin we don’t need to poke any firewall holes or configure any DNS. Big claim, let’s test it.

Install the Agent

Go to https://developers.cloudflare.com/argo-tunnel/downloads/ to get the url for the ARM build for your Pi. At the time of writing it was https://bin.equinox.io/c/VdrWdbjqyF/cloudflared-stable-linux-arm.tgz

$ wget https://bin.equinox.io/c/VdrWdbjqyF/cloudflared-stable-linux-arm.tgz
Resolving bin.equinox.io (bin.equinox.io)... 54.243.137.45, 107.22.233.132, 50.19.252.69, ...
Connecting to bin.equinox.io (bin.equinox.io)|54.243.137.45|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 5293773 (5.0M) [application/octet-stream]
Saving to: ‘cloudflared-stable-linux-arm.tgz'
...

Untar it

$ mkdir argo-tunnel
$ tar -xvzf cloudflared-stable-linux-arm.tgz -C ./argo-tunnel
cloudflared
$ cd argo-tunnel

Check you can execute it.

$ ./cloudflared --version
cloudflared version 2018.3.0 (built 2018-03-02-1820 UTC)

Looks OK. Now, we’re hoping that the agent will magically connect from the Pi out to the nearest Cloudflare POP. We obviously want that to be secure. Furthermore, we’re expecting that when a request comes inbound, it magically gets routed through Cloudflare’s network and back to my Raspberry Pi.

Seems unlikely, but let’s have faith. Here is my mental model of what’s happening:

Cloudflare Argo Tunnel with Rust+Raspberry Pi

So let’s create that secure tunnel. I guess we need some sort of certificate or credentials…

$ ./cloudflared login

You’ll see output in the command window similar to this:

A browser window should have opened at the following URL:

https://www.cloudflare.com/a/warp?callback=<some token>

If the browser failed to open, open it yourself and visit the URL above.

Our headless Pi doesn’t have a web browser, so let’s copy the url from the console into the browser on our host dev machine.

This part assumes you already have a domain on Cloudflare If you don’t go to the setup guide to get started.

Cloudflare Argo Tunnel with Rust+Raspberry Pi

We’re being asked which domain we want this tunnel to sit behind. I’ve chosen pacman.wiki. Click Authorize.

Cloudflare Argo Tunnel with Rust+Raspberry Pi

Cloudflare Argo Tunnel with Rust+Raspberry Pi

You should now see this back on your pi:

You have successfully logged in.
If you wish to copy your credentials to a server, they have been saved to:
/home/pi/.cloudflared/cert.pem

Aha! That answers how the tunnel gets secured. The agent has created a certificate and will use that to secure the connection back to Cloudflare. Now let’s create the tunnel and serve some content!

$ cloudflared --hostname [hostname] --hello-world

hostname is a fully-qualified domain name under the domain you chose to Authorize for Argo Tunnels earlier. I’m going to use tunnel.pacman.wiki

$ ./cloudflared --hostname tunnel.pacman.wiki --hello-world
INFO[0002] Proxying tunnel requests to https://127.0.0.1:46727 
INFO[0000] Starting Hello World server at 127.0.0.1:53030 
INFO[0000] Starting metrics server                       addr="127.0.0.1:53031"
INFO[0005] Connected to LAX                             
INFO[0010] Connected to SFO-DOG                         
INFO[0012] Connected to LAX                             
INFO[0012] Connected to SFO-DOG  

Huh, interesting. So, we’ve connected to my nearest POP(s). I’m in the San Francisco Bay Area, so SJC and LAX seems reasonable. What now though? Surely that’s not it? If I’m reading this right, I can go to my browser, enter https://tunnel.pacman.wiki and I’ll get a hello world page… surely not.

Cloudflare Argo Tunnel with Rust+Raspberry Pi
And back on the Pi

INFO[0615] GET https://127.0.0.1:62627/ HTTP/1.1 CF-RAY=4067701b598e8184-LAX
INFO[0615] 200 OK  CF-RAY=4067701b598e8184-LAX

Mind. Blown. So what happened here exactly…

  1. The agent on the Pi created a secure tunnel (a persistent http2 connection) back to the nearest Cloudflare Argo Tunnels server
  2. The tunnel was secured with the certificate generated by the agent.
  3. A request for https://tunnel.pacman.wiki went from my browser out through the Internet and was routed to the nearest Cloudflare datacenter
  4. Cloudflare received the request, saw the domain was Cloudflare managed and saw a tunnel set up to that hostname
  5. The request got routed over that http2 connection back to my Pi

I’m serving traffic over the Internet, from my Pi, with no ports opened on my home router. That is so cool.

More than hello world

If you’re reading this, I’ve won my battle with the Cloudflare blog editing team about long form vs short form content :p

Serving hello world is great, but I want to expose a real web server. If you’re like me, if you can find any vaguely relevant reason to use Rust, then you use Rust. If you’re also like me, you want to try one of these async web servers the cool kids talk about on /r/rust like gotham. Let’s do it.

First, install rust using rustup.

$ curl https://sh.rustup.rs -sSf | sh

When prompted, just hit enter

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
...
  stable installed - rustc 1.24.1 (d3ae9a9e0 2018-02-27)
...

OK, Rust is installed. Now clone Gotham and build the hello_world example:

$ git clone https://github.com/gotham-rs/gotham
$ cd gotham/examples/hello_world
$ cargo build

Pro tip: if cargo is not found, run source $HOME/.cargo/env. It will be automatic in future sessions.

As cargo does its magic, you can think to yourself about how it’s a great package manager, how there really are a lot of dependencies and how OSS really is standing on the shoulders of giants of giants of giants of giants—eventually you’ll have the example built.

...
Compiling gotham_examples_hello_world v0.0.0 (file:///home/pi/argo-tunnel/gotham/examples/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 502.83 secs
    
$ cd ../../target/debug
$ ./gotham_examples_hello_world 
Listening for requests at http://127.0.0.1:7878

We have a rust web server listening on a local port. Let’s connect the tunnel to that.

./cloudflared --hostname gotham.pacman.wiki http://127.0.0.1:7878

Type gotham.pacman.wiki into your web browser and you’ll see those glorious words, “Hello, world”.

Wait, this post was meant to be more than hello world.

OK, challenge accepted. Rust being fancy and modern is OK with Unicode. Let’s serve some of that.

$ cd examples/hello_world/src/
$ nano src/main.rs 

Replace the hello world string:

Some((String::from("Hello World!").into_bytes(), mime::TEXT_PLAIN)),

with some Unicode and a content-type hint so the browser know how to render it:

Some((String::from("Pᗣᗧ•••MᗣN").into_bytes(), mime::TEXT_HTML)),

Build and run

$ cargo build
...
./gotham_examples_hello_world 
Listening for requests at http://127.0.0.1:7878

$ ./cloudflared --hostname gotham.pacman.wiki http://127.0.0.1:7878

Cloudflare Argo Tunnel with Rust+Raspberry Pi

And now we have some unicode served from our Pi at home over the Internet by a highly asynchronous web server written in a fast, safe, high-level language. Cool.

Are we done?

We should probably auto start both the agent and the web server on boot so they don’t die when we end our ssh session.

$ sudo ./cloudflared service install
INFO[0000] Failed to copy user configuration. Before running the service, 
ensure that /etc/cloudflared contains two files, cert.pem and config.yml  
error="open cert.pem: no such file or directory"

Nice error! OK, the product team have helpfully documented what to put in that file here

$ sudo cp ~/.cloudflared/cert.pem /etc/cloudflared
$ sudo nano /etc/cloudflared/config.yml
#config.yml
hostname: gotham.pacman.wiki
url: http://127.0.0.1:7878

Autostart for the Agent

$ sudo ./cloudflared service install
INFO[0000] Using Systemd                                
ERRO[0000] systemctl: Created symlink from /etc/systemd/system/multi-user.target.wants/cloudflared.service to /etc/systemd/system/cloudflared.service.
INFO[0000] systemctl daemon-reload       

Autostart for the Web Server

Copy the web server executable somewhere outside the gotham source tree so you can play around with the source code. I copied mine to /home/pi/argo-tunnel/server/bin/

nano /etc/rc.local

Add line: /home/pi/argo-tunnel/server/bin/gotham_examples_hello_world & just before exit 0

sudo reboot

On restart, ssh back in again and check both the agent and server are running.

$ sudo ps -aux | grep tunnel
root       501  0.1  0.2  37636  1976 ?        Sl   06:30   0:00 /home/pi/argo-tunnel/server/bin/gotham_examples_hello_world
root       977 15.7  1.4 801292 13972 ?        Ssl  06:30   0:01 /home/pi/argo-tunnel/cloudflared --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --no-autoupdate

Profit.

Read more here:: CloudFlare

Cloudflare Argo Tunnel with Rust+Raspberry Pi