diff --git a/src/content/unix/reverse-proxy.md b/src/content/unix/reverse-proxy.md
index ac257d2..80263e8 100644
--- a/src/content/unix/reverse-proxy.md
+++ b/src/content/unix/reverse-proxy.md
@@ -24,25 +24,25 @@ easily get *inward* access to any port.
The easiest method, and probably recommended for most users, is using Software
Driven WAN (SDWAN). This is similar to a VPN, but unlike Wireguard, ZeroTier One
-doesn't [[1]] require a centralized server.
+doesn't1 require a centralized server.
Advantages:
- - Free version is good enough for most small groups
- Very simple setup
- - Very fast through UDP hole-punching
+ - Very low latency through UDP hole-punching
+ - Free (for up to 25 clients)
Drawbacks:
- - Limit of 25 clients (on the [free
- version](https://www.zerotier.com/pricing/))
- Requires installing separate software on all clients
- Sometimes fails to connect for up to an hour... very hard to debug when it
happens
- Can only be used by clients on the VPN. For example, a public webserver won't
be able to use this
+ - Limit of 25 clients (on the [free
+ version](https://www.zerotier.com/pricing/))
To use ZeroTier:
1. Sign up for an account at [zerotier.com](https://www.zerotier.com/)
- 2. Under "Networks" create a network and give it a name. Ensure access control
+ 2. Under "Networks" create a network and give it a name. Ensure Access Control
is private.
3. Install ZeroTier One on all clients you'd like to connect. You can add more
later.
@@ -168,3 +168,163 @@ ssh -p 8022 emiliko@172.27.10.10
```
This will be forwarded to port 22 on the server!
+
+### Reverse Proxy without a Static IP
+
+If you're planning to use a home computer, you'll quickly find that most
+internet service providers do not offer static IPs for consumer plans. Luckily,
+there's a very simple way around this: Domain Name Servers (DNS).
+
+You will need a domain to achieve this. Domains should not cost more than
+$20/year. Here I'll use the domain `example.com` as an example.
+
+The idea is that a domain will point to a specific IP, but this IP is determined
+through a lookup to the DNS. This means that if we change the IP the DNS has
+every time our computer's IP changes, we'll appear to have a static IP!
+
+First, put your nameservers on a good DNS provider. I use
+[Cloudflare](https://pages.cloudflare.com/), it's free and fast. You'll need to
+find the DNS page. The URL will look something like:
+
+```
+https://dash.cloudflare.com//example.com/dns/records
+```
+
+There, you'll want to add an "A Record". The name will be the subdomain. So if
+my computer is called `mycomputer` and that's in the name field, it'll be
+accessible at `mycomputer.example.com`.
+
+Now you need to identify your IP address. This is your PUBLIC IP address, not
+your LOCAL IP address. One easy way to do this is `curl -q
+https://ifconfig.me/ip`.
+
+Make sure "Proxy Status" is OFF. Proxying the connection appears to make this
+whole idea break down very quickly, so don't.
+
+With that "A Record" set, try `host mycomputer.example.com` to see when the DNS
+updates. This can take up to 4 hour, but usually takes under a minute in
+practice. With this, you should be able to access your computer using the
+domain! Of course, make sure your router's ports are forwarding to your
+computer.
+
+We now need to make your computer update Cloudflare's DNS, whenever the IP
+changes. I use the script below to do this. Fill in the `HOST`, `DOMAIN`,
+`TOKEN`, `ZONE_ID`. The `TOKEN` is your Cloudflare application token:
+
+```bash
+#!/usr/bin/env bash
+declare wan_ip_record wan_ip cf_records host_record cf_host_ip cf_rec_id
+
+declare -r HOST='mycomputer'
+declare -r DOMAIN='example.com'
+declare -r TOKEN='...'
+declare -r ZONE_ID='...'
+
+#╔─────────────────────────────────────────────────────────────────────────────╗
+#│ Gετ WΛN IP |
+#╚─────────────────────────────────────────────────────────────────────────────╝
+if ! wan_ip_record="$(curl ifconfig.me)"; then
+ echo "Hosts timed out" >&2
+ exit 1
+fi
+
+wan_ip="$wan_ip_record"
+#wan_ip="$(echo "$wan_ip_record" | tail -n1 | awk '{ split($0, a, " "); print a[4] }')"
+
+#╔─────────────────────────────────────────────────────────────────────────────╗
+#│ Gετ Λ rεcδrd δη Clδμdflαrε |
+#╚─────────────────────────────────────────────────────────────────────────────╝
+if ! cf_records="$(curl -s --request GET \
+ --url https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records \
+ --header 'Content-Type: application/json' \
+ --header "Authorization: Bearer $TOKEN")"
+then
+ echo "Failed to retrive cloudflare zone records" >&2
+ exit 1
+fi
+
+for i in {0..4000}; do # Assuming 4000 is enough
+ record="$(echo "$cf_records" | jq --raw-output ".result[${i}].name")"
+
+ if [[ "$record" == "${HOST}.${DOMAIN}" ]]; then
+ host_record="$(echo "$cf_records" | jq -r ".result[${i}]")"
+ break
+ elif [[ "$record" == null ]]; then
+ echo "No record found for ${HOST}.${DOMAIN}" >&2
+ exit 1
+ fi
+done
+
+#╔─────────────────────────────────────────────────────────────────────────────╗
+#│ Sετ Λ rεcδrd τδ cμrrεητ WΛN |
+#╚─────────────────────────────────────────────────────────────────────────────╝
+cf_host_ip="$(echo "$host_record" | jq -r '.content')"
+cf_rec_id="$(echo "$host_record" | jq -r '.id')"
+
+if [[ -z "$cf_host_ip" || "$cf_host_ip" == null ]]; then
+ echo "Failed to find content of A record for ${HOST}.${DOMAIN}" >&2
+ exit 1
+elif [[ -z "$cf_rec_id" || "$cf_rec_id" == null ]]; then
+ echo "Failed to find A record ID for ${HOST}.${DOMAIN}" >&2
+ exit 1
+fi
+
+if [[ "$cf_host_ip" == "$wan_ip" ]]; then
+ echo "Cloudflare is up to date @ $(date)" >&2
+else
+ echo "Updating Cloudflare's A record from $cf_host_ip to $wan_ip" >&2
+
+ patch_response="$(curl -s --request PATCH \
+ --url "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${cf_rec_id}" \
+ --header 'Content-Type: application/json' \
+ --header "Authorization: Bearer $TOKEN" \
+ --data '{
+ "comment": "'"${HOST} @ $(date)"'",
+ "content": "'"$wan_ip"'",
+ "name": "'"${HOST}.${DOMAIN}"'",
+ "proxied": false,
+ "ttl": 1
+ }')"
+
+ if [[ "$(echo "$patch_response" | jq -r '.success')" == true ]]; then
+ echo "Update to $wan_ip succeeded @ $(date)" >&2
+ else
+ echo "Failed to update A record. DUMP:"
+ echo "$patch_response"
+ exit 1
+ fi
+fi
+```
+
+Now we need a systemd-timer to run this script. I run it once every 15 minutes.
+Please refer to the [systemd-timers]() blog for more information, but breifly I
+use:
+
+```ini
+[Unit]
+Wants=update_a_record.timer
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/bin/set_a_records.sh
+```
+
+Timer:
+
+```ini
+[Unit]
+Requires=update_a_record.service
+
+[Timer]
+Unit=update_a_record.service
+OnCalendar=*-*-* *:00,15,30,45:00
+RandomizedDelaySec=15min
+
+[Install]
+WantedBy=timers.target
+```
+
+Then start it with `systemctl enable update_a_record.service`. The name of the
+service will be different based on what you called the files.