andhop.dev lives on a Raspberry Pi 5. Can it automatically scale to handle 1 million TPS? No. Is it distributed across the globe so all customers have sub 10 ms access to it? No, it’s just a Raspberry Pi sitting under my TV.
In the age of cloud computing it’s refreshing to take a step back and look at what it takes to run everything yourself.
Domain name registration
I started by buying andhop.dev from Cloudflare because AWS Route53 doesn’t
support the dev Top Level Domain (TLD). The process to signup and pay was
painless and they seem to have a good API and documentation.
DNS
Okay, I said run everything yourself, but DNS is one place where it’s really
hard to DIY. I could run my own nameserver, but that kind of makes DNS
redundant, once you know where the nameserver is (at my home IP) you know where
andhop.dev is. The much bigger problem is Cloudflare doesn’t support changing
the
nameserver
for domain names registered with Cloudflare. So fine, Cloudflare wins this
round.
Once you have a DNS record you need to point it somewhere. This is where the big
cloud providers will try to lure you toward their fancy services with 9 9’s of
availability and edge locations around the world. Here is where our resistance
begins for real. We’re going to self host everything else and point andhop.dev
at my home network.
I use a simple systemd service run on an interval to check my current IP and if it’s changed update Cloudflare, the basic idea is:
RECORD_NAME="andhop.dev"
CF_ZONE_ID="something_here"
CF_API_TOKEN="something_secret"
RECORD_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${RECORD_NAME}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
NEW_IP=$(curl -s --max-time 10 http://checkip.amazonaws.com/)
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${RECORD_NAME}\",\"content\":\"${NEW_IP}\",\"ttl\":300,\"proxied\":false}"
Home Network Setup
The internet is vast and scary and we’re about to invite strangers in. Getting
traffic from the internet to a Pi safely takes a bit of plumbing. The first step
involves getting all the traffic to my router, thankfully CenturyLink is cool
and put a nice sticker on their router with it’s IP address (192.168.0.1),
username, and password. Then it just took some poking around to find the right
setting. In my case it was a setting called “transparent bridging”.
CenturyLink is now forwarding all packets to my UniFi router, but by default the firewall drops all incoming connections, it only allows data in for established connections that devices on my home network initiated. Let’s change that:
- Create a “hazmat” VLAN for the web server Pi
- Allow the rest of my network to connect to the hazmat vlan, but not the other way around
- Update the router to forward all port 443 traffic to the Pi’s IP, this automatically updates the firewall rules allowing new connections on port 443 in
Pi
The Pi itself is set up to be as boring and secure as possible:
- Simple systemd service running as an untrusted user
- Serve static content that is defined at compile time
- Run the service in a read-only chroot
Server software
The server is written in Rust using hyper for HTTP and rustls for TLS. Blog posts like this one are compiled from Markdown at build time to static HTML pages. The HTML pages are directly included into the rust source code so the Pi just serves static bytes for each request.