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.