#!/usr/bin/env bash # algoblocker — deploy the Cloudflare WARP free-tier VPN and route this device through it. # # Closes the network-layer fingerprint surfaces the browser extension cannot touch: # source IP (shared CF egress pool), ASN (AS13335), DNS (encrypted to 1.1.1.1), # and TLS ClientHello (uniform WARP JA3). See ../ARCHITECTURE.md for the full model. # # This is NOT anonymity. It shifts network-layer trust from your ISP to Cloudflare. # For adversaries who can compel Cloudflare, or for behavioral de-anonymization, # use Tor Browser instead. # # Usage: # ./deploy-vpn.sh # install + register consumer WARP (free), connect, verify # ./deploy-vpn.sh --team NAME # enroll into a Cloudflare Zero Trust org (free, 50 users) # ./deploy-vpn.sh --mode warp+doh # tunnel mode: warp (default) | warp+doh | doh | proxy # ./deploy-vpn.sh --worker # also deploy the /whoami audit Worker (needs wrangler) # ./deploy-vpn.sh --browser-doh # also pin Chrome/Edge/Firefox DNS to Cloudflare DoH # ./deploy-vpn.sh --status # show WARP + egress status and exit # ./deploy-vpn.sh --disconnect # disconnect WARP and exit # # Supports: Debian/Ubuntu, Fedora/RHEL, Arch (AUR note), macOS (Homebrew). set -euo pipefail # ── args ──────────────────────────────────────────────────────────── TEAM="" MODE="warp" DO_WORKER=0 DO_BROWSER_DOH=0 ACTION="deploy" while [ $# -gt 0 ]; do case "$1" in --team) TEAM="${2:?--team needs a name}"; shift 2;; --mode) MODE="${2:?--mode needs a value}"; shift 2;; --worker) DO_WORKER=1; shift;; --browser-doh) DO_BROWSER_DOH=1; shift;; --status) ACTION="status"; shift;; --disconnect) ACTION="disconnect"; shift;; -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0;; *) echo "unknown arg: $1 (try --help)" >&2; exit 2;; esac done DOH_TEMPLATE="https://cloudflare-dns.com/dns-query" c(){ printf '\033[%sm%s\033[0m' "$1" "$2"; } say(){ printf '%s %s\n' "$(c '36;1' '»')" "$1"; } ok(){ printf '%s %s\n' "$(c '32;1' '✓')" "$1"; } warn(){ printf '%s %s\n' "$(c '33;1' '!')" "$1" >&2; } die(){ printf '%s %s\n' "$(c '31;1' '✗')" "$1" >&2; exit 1; } OS="$(uname -s)" SUDO=""; [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1 && SUDO="sudo" # ── locate warp-cli ───────────────────────────────────────────────── warp_bin(){ if command -v warp-cli >/dev/null 2>&1; then command -v warp-cli; return; fi for p in /usr/bin/warp-cli /usr/local/bin/warp-cli \ "/Applications/Cloudflare WARP.app/Contents/Resources/warp-cli"; do [ -x "$p" ] && { echo "$p"; return; } done return 1 } # warp-cli's subcommand grammar changed across releases. Try the modern form, # fall back to the legacy one on an "unrecognized subcommand" style failure. WARP="" warp(){ local out rc out="$("$WARP" "$@" 2>&1)"; rc=$? printf '%s\n' "$out" return $rc } warp_try(){ # warp_try "modern args" "legacy args" local modern="$1" legacy="$2" out rc # shellcheck disable=SC2086 out="$("$WARP" --accept-tos $modern 2>&1)"; rc=$? if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qiE 'unrecognized|unknown|invalid.*subcommand|no such'; then # shellcheck disable=SC2086 out="$("$WARP" $legacy 2>&1)"; rc=$? fi printf '%s\n' "$out" return $rc } public_ip(){ curl -fsS --max-time 8 https://1.1.1.1/cdn-cgi/trace 2>/dev/null | sed -n 's/^ip=//p'; } trace(){ curl -fsS --max-time 8 https://1.1.1.1/cdn-cgi/trace 2>/dev/null; } # ── status / disconnect short-circuits ────────────────────────────── if [ "$ACTION" = "status" ]; then WARP="$(warp_bin)" || die "WARP is not installed. Run without --status to install it." warp status || true echo; say "egress as origin servers see it:"; trace | grep -E '^(ip|warp|gateway|loc)=' || warn "no trace (offline?)" exit 0 fi if [ "$ACTION" = "disconnect" ]; then WARP="$(warp_bin)" || die "WARP is not installed." warp_try "disconnect" "disconnect" >/dev/null && ok "WARP disconnected — traffic is on your ISP again." || warn "disconnect reported an error" exit 0 fi echo "$(c '1' 'algoblocker VPN') $(c '2' '— Cloudflare WARP, network-surface layer')" echo # ── 1. capture the ISP baseline (to prove the change) ─────────────── ISP_IP="$(public_ip || true)" [ -n "$ISP_IP" ] && say "current public IP (your ISP): $(c '33' "$ISP_IP")" || warn "could not read current IP (continuing)" # ── 2. install WARP if absent ─────────────────────────────────────── if WARP="$(warp_bin)"; then ok "WARP client already installed ($WARP)" else say "installing Cloudflare WARP…" case "$OS" in Darwin) command -v brew >/dev/null 2>&1 || die "Homebrew required on macOS: https://brew.sh — then re-run." brew install --cask cloudflare-warp || die "brew install failed" ;; Linux) if command -v apt-get >/dev/null 2>&1; then curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg \ | $SUDO gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(. /etc/os-release; echo "${VERSION_CODENAME:-$(lsb_release -cs 2>/dev/null)}") main" \ | $SUDO tee /etc/apt/sources.list.d/cloudflare-client.list >/dev/null $SUDO apt-get update && $SUDO apt-get install -y cloudflare-warp elif command -v dnf >/dev/null 2>&1; then $SUDO rpm --import https://pkg.cloudflareclient.com/pubkey.gpg || true $SUDO curl -fsSL https://pkg.cloudflareclient.com/cloudflare-warp-ascii.repo -o /etc/yum.repos.d/cloudflare-warp.repo $SUDO dnf install -y cloudflare-warp elif command -v pacman >/dev/null 2>&1; then die "Arch: install the AUR package 'cloudflare-warp-bin' (e.g. 'yay -S cloudflare-warp-bin'), then re-run." else die "no supported package manager found. Install WARP manually: https://pkg.cloudflareclient.com" fi # ensure the daemon is up if command -v systemctl >/dev/null 2>&1; then $SUDO systemctl enable --now warp-svc 2>/dev/null || true fi ;; *) die "unsupported OS: $OS (this script covers Linux + macOS; use deploy-vpn.ps1 on Windows)";; esac WARP="$(warp_bin)" || die "WARP installed but warp-cli not found on PATH; open a new shell and re-run." ok "WARP installed" fi # give the daemon a moment on a cold install for _ in 1 2 3 4 5; do warp status >/dev/null 2>&1 && break; sleep 1; done # ── 3. register (consumer) or enroll (Zero Trust) ─────────────────── if warp status 2>/dev/null | grep -qiE 'registration missing|not registered' || ! warp status >/dev/null 2>&1; then if [ -n "$TEAM" ]; then say "enrolling into Zero Trust org '$TEAM' (a browser will open for one-time-PIN auth)…" warp_try "teams-enroll $TEAM" "teams-enroll $TEAM" || warn "enrollment kicked off; finish auth in the browser, then re-run --status" else say "registering a free consumer WARP device (no account needed)…" warp_try "registration new" "register" >/dev/null || die "registration failed — see output above" fi ok "device registered" else ok "device already registered" fi # ── 4. set tunnel mode + connect ──────────────────────────────────── say "setting tunnel mode to '$MODE' (full-device: browsers and every app route through WARP)…" warp_try "mode $MODE" "set-mode $MODE" >/dev/null || warn "could not set mode $MODE (continuing with current mode)" say "connecting…" warp_try "connect" "connect" >/dev/null || die "connect failed — check 'warp-cli status'" # ── 5. verify egress actually changed ─────────────────────────────── say "verifying egress…" NEW_TRACE="" ; for _ in 1 2 3 4 5 6; do NEW_TRACE="$(trace || true)"; printf '%s' "$NEW_TRACE" | grep -q '^warp=' && break; sleep 2; done WARP_STATE="$(printf '%s' "$NEW_TRACE" | sed -n 's/^warp=//p')" NEW_IP="$(printf '%s' "$NEW_TRACE" | sed -n 's/^ip=//p')" LOC="$(printf '%s' "$NEW_TRACE" | sed -n 's/^loc=//p')" echo case "$WARP_STATE" in on|plus) ok "WARP active (warp=$WARP_STATE)" [ -n "$NEW_IP" ] && ok "egress IP now: $(c '32' "$NEW_IP")${LOC:+ ($LOC)} — shared Cloudflare pool, not yours" [ -n "$ISP_IP" ] && [ "$ISP_IP" != "$NEW_IP" ] && ok "your ISP IP ($ISP_IP) is no longer visible to sites" ;; *) warn "WARP does not report active (warp='${WARP_STATE:-unknown}'). Traffic may still be on your ISP." warn "check 'warp-cli status'; a conflicting VPN or MDM profile can block WARP from taking the route." ;; esac # ── 6. optional: deploy the audit Worker ──────────────────────────── if [ "$DO_WORKER" -eq 1 ]; then echo; say "deploying the /whoami audit Worker…" WDIR="$(cd "$(dirname "$0")/../worker" && pwd)" if command -v wrangler >/dev/null 2>&1; then WRANGLER="wrangler"; elif command -v npx >/dev/null 2>&1; then WRANGLER="npx --yes wrangler"; else warn "wrangler/npx not found — skipping Worker (install Node, then: cd $WDIR && npx wrangler deploy)"; WRANGLER=""; fi if [ -n "$WRANGLER" ]; then ( cd "$WDIR" && $WRANGLER deploy ) \ && ok "Worker deployed — paste its printed https://…workers.dev URL into the extension's Options → VPN health check" \ || warn "wrangler deploy failed (run 'wrangler login' first?) — see output above" fi fi # ── 7. optional: pin browser DNS to Cloudflare DoH ────────────────── if [ "$DO_BROWSER_DOH" -eq 1 ]; then echo; say "pinning Chrome/Edge/Firefox DNS to Cloudflare DoH (defense-in-depth if WARP drops)…" write_json(){ $SUDO mkdir -p "$(dirname "$1")" && printf '%s\n' "$2" | $SUDO tee "$1" >/dev/null && ok "wrote $1"; } CHROME_POLICY='{ "DnsOverHttpsMode": "secure", "DnsOverHttpsTemplates": "'"$DOH_TEMPLATE"'" }' FF_POLICY='{ "policies": { "DNSOverHTTPS": { "Enabled": true, "ProviderURL": "'"$DOH_TEMPLATE"'", "Locked": true } } }' case "$OS" in Linux) write_json /etc/opt/chrome/policies/managed/algoblocker-doh.json "$CHROME_POLICY" write_json /etc/chromium/policies/managed/algoblocker-doh.json "$CHROME_POLICY" write_json /etc/opt/edge/policies/managed/algoblocker-doh.json "$CHROME_POLICY" for d in /etc/firefox/policies /usr/lib/firefox/distribution /etc/firefox-esr/policies; do write_json "$d/policies.json" "$FF_POLICY" done ;; Darwin) for id in com.google.Chrome com.microsoft.Edge; do defaults write "$id" DnsOverHttpsMode -string "secure" 2>/dev/null || true defaults write "$id" DnsOverHttpsTemplates -string "$DOH_TEMPLATE" 2>/dev/null || true done ok "Chrome/Edge DoH set via defaults (restart the browser to apply)" warn "Firefox on macOS: set DoH manually in Settings → Privacy → DNS over HTTPS → Cloudflare" ;; esac warn "restart your browsers for DoH policy to take effect" fi # ── summary (honest) ──────────────────────────────────────────────── echo echo "$(c '1' 'closed by this layer:') source IP, ASN, DNS-to-ISP, TLS JA3 (uniform WARP)." echo "$(c '1' 'still open:') Cloudflare can see your egress (trust shifted from ISP to CF);" echo " TLS content, logins, and behavioral biometrics are unaffected. Not Tor." echo "$(c '2' 'verify anytime:') ./deploy-vpn.sh --status · turn off: ./deploy-vpn.sh --disconnect"