feat(scripts): WSL client bootstrap for routing Claude Code through proxy
Some checks failed
CI / lint (push) Successful in 29s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Failing after 19s
Infra CI / zotero (push) Successful in 15s
Infra CI / docs (push) Successful in 1m28s
Infra CI / api (push) Successful in 33s
Infra CI / mc (push) Successful in 14s
Deploy / report (push) Successful in 14s
CI / test (push) Failing after 13m32s
Renovate / renovate (push) Successful in 15s
Package Supply Chain / pkg-supply-chain (push) Failing after 45s
Some checks failed
CI / lint (push) Successful in 29s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Failing after 19s
Infra CI / zotero (push) Successful in 15s
Infra CI / docs (push) Successful in 1m28s
Infra CI / api (push) Successful in 33s
Infra CI / mc (push) Successful in 14s
Deploy / report (push) Successful in 14s
CI / test (push) Failing after 13m32s
Renovate / renovate (push) Successful in 15s
Package Supply Chain / pkg-supply-chain (push) Failing after 45s
setup-proxy-client.sh: one-shot, idempotent setup for a WSL (or any Debian/Ubuntu) machine to route Claude Code through proxy.fhirworx.io. - Installs cloudflared from the official apt repo - Pins proxy.fhirworx.io -> 127.0.0.1 in /etc/hosts (and patches /etc/wsl.conf so the pin survives reboots on WSL) - Drops a systemd --user unit (cf-proxy.service) running `cloudflared access tcp --hostname proxy.fhirworx.io --url 127.0.0.1:18443` - Installs Claude Code if missing - Merges HTTPS_PROXY + NO_PROXY into ~/.claude/settings.json (mode 0600 since the URL embeds the proxy password) - Smoke-tests by hitting api.anthropic.com through the proxy and asserting HTTP 401 (proves auth + allow-list both work) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
161
dev/scripts/setup-proxy-client.sh
Executable file
161
dev/scripts/setup-proxy-client.sh
Executable file
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Set up a WSL (or any Debian/Ubuntu) machine to route Claude Code through
|
||||||
|
# proxy.fhirworx.io via cloudflared TCP access. Idempotent — safe to re-run.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# PROXY_PASSWORD=... ./setup-proxy-client.sh
|
||||||
|
# or
|
||||||
|
# ./setup-proxy-client.sh # will prompt for the password
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOSTNAME_PROXY=proxy.fhirworx.io
|
||||||
|
LOCAL_PORT=18443
|
||||||
|
SERVICE_NAME=cf-proxy
|
||||||
|
|
||||||
|
# ── 0. Pre-flight ─────────────────────────────────────────────────────────────
|
||||||
|
log() { printf '\033[1;34m[setup]\033[0m %s\n' "$*"; }
|
||||||
|
fail() { printf '\033[1;31m[setup] ERROR:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
[[ $EUID -ne 0 ]] || fail "run as your normal user — sudo is invoked per-step"
|
||||||
|
command -v systemctl >/dev/null || fail "systemd not found. Enable it in /etc/wsl.conf ([boot] systemd=true) then 'wsl --shutdown'."
|
||||||
|
command -v sudo >/dev/null || fail "sudo missing"
|
||||||
|
command -v curl >/dev/null || fail "curl missing"
|
||||||
|
command -v python3 >/dev/null || fail "python3 missing"
|
||||||
|
|
||||||
|
if [[ -z "${PROXY_PASSWORD:-}" ]]; then
|
||||||
|
read -rsp "proxy password for user 'claude': " PROXY_PASSWORD; echo
|
||||||
|
fi
|
||||||
|
[[ -n "$PROXY_PASSWORD" ]] || fail "PROXY_PASSWORD is empty"
|
||||||
|
|
||||||
|
# ── 1. cloudflared ────────────────────────────────────────────────────────────
|
||||||
|
if ! command -v cloudflared >/dev/null; then
|
||||||
|
log "installing cloudflared"
|
||||||
|
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||||
|
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
|
||||||
|
| sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||||
|
CODENAME=$(. /etc/os-release; echo "${VERSION_CODENAME:-bookworm}")
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $CODENAME main" \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/cloudflared.list >/dev/null
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y cloudflared
|
||||||
|
else
|
||||||
|
log "cloudflared already installed ($(cloudflared --version 2>&1 | head -1))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. /etc/hosts pin + WSL persistence ───────────────────────────────────────
|
||||||
|
log "pinning $HOSTNAME_PROXY → 127.0.0.1 in /etc/hosts"
|
||||||
|
sudo sed -i "/\b$HOSTNAME_PROXY\b/d" /etc/hosts
|
||||||
|
echo "127.0.0.1 $HOSTNAME_PROXY" | sudo tee -a /etc/hosts >/dev/null
|
||||||
|
|
||||||
|
if grep -qi microsoft /proc/version 2>/dev/null; then
|
||||||
|
if ! grep -q '^\s*generateHosts\s*=\s*false' /etc/wsl.conf 2>/dev/null; then
|
||||||
|
log "patching /etc/wsl.conf so /etc/hosts isn't regenerated on next boot"
|
||||||
|
sudo tee -a /etc/wsl.conf >/dev/null <<'EOF'
|
||||||
|
|
||||||
|
[network]
|
||||||
|
generateHosts = false
|
||||||
|
EOF
|
||||||
|
WSL_REBOOT_NEEDED=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
getent hosts "$HOSTNAME_PROXY" | grep -q '^127\.0\.0\.1' \
|
||||||
|
|| fail "$HOSTNAME_PROXY does not resolve to 127.0.0.1 — check /etc/hosts and /etc/nsswitch.conf"
|
||||||
|
|
||||||
|
# ── 3. systemd user service for the cloudflared shim ──────────────────────────
|
||||||
|
log "writing systemd user unit ~/.config/systemd/user/$SERVICE_NAME.service"
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cat > ~/.config/systemd/user/$SERVICE_NAME.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=cloudflared access tcp shim for $HOSTNAME_PROXY
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/cloudflared access tcp --hostname $HOSTNAME_PROXY --url 127.0.0.1:$LOCAL_PORT
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now "$SERVICE_NAME.service"
|
||||||
|
sudo loginctl enable-linger "$USER" # keep user manager alive across logouts
|
||||||
|
|
||||||
|
# wait briefly for the listener
|
||||||
|
for _ in $(seq 1 20); do
|
||||||
|
ss -tln 2>/dev/null | grep -q ":$LOCAL_PORT\b" && break
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
ss -tln 2>/dev/null | grep -q ":$LOCAL_PORT\b" \
|
||||||
|
|| fail "cloudflared shim didn't open :$LOCAL_PORT — check 'journalctl --user -u $SERVICE_NAME -n 30'"
|
||||||
|
|
||||||
|
# ── 4. Claude Code ────────────────────────────────────────────────────────────
|
||||||
|
if ! command -v claude >/dev/null; then
|
||||||
|
log "installing Claude Code"
|
||||||
|
curl -fsSL https://claude.ai/install.sh | bash
|
||||||
|
# the installer drops it under ~/.local/bin or similar; rehash for this shell
|
||||||
|
hash -r
|
||||||
|
fi
|
||||||
|
command -v claude >/dev/null || log "WARNING: claude not on PATH — may need a new shell"
|
||||||
|
|
||||||
|
# ── 5. settings.json (merge, don't clobber) ───────────────────────────────────
|
||||||
|
log "merging proxy env into ~/.claude/settings.json"
|
||||||
|
mkdir -p ~/.claude
|
||||||
|
SETTINGS=~/.claude/settings.json
|
||||||
|
[[ -f "$SETTINGS" ]] || echo '{}' > "$SETTINGS"
|
||||||
|
|
||||||
|
PROXY_URL="https://claude:${PROXY_PASSWORD}@${HOSTNAME_PROXY}:${LOCAL_PORT}"
|
||||||
|
python3 - "$SETTINGS" "$PROXY_URL" <<'PY'
|
||||||
|
import json, sys, os
|
||||||
|
path, proxy_url = sys.argv[1], sys.argv[2]
|
||||||
|
with open(path) as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
env = cfg.get("env", {}) or {}
|
||||||
|
env["HTTPS_PROXY"] = proxy_url
|
||||||
|
env["NO_PROXY"] = "localhost,127.0.0.1"
|
||||||
|
cfg["env"] = env
|
||||||
|
tmp = path + ".tmp"
|
||||||
|
with open(tmp, "w") as f:
|
||||||
|
json.dump(cfg, f, indent=2)
|
||||||
|
os.replace(tmp, path)
|
||||||
|
os.chmod(path, 0o600) # contains the proxy password
|
||||||
|
PY
|
||||||
|
log "wrote $SETTINGS (mode 0600)"
|
||||||
|
|
||||||
|
# ── 6. smoke test ─────────────────────────────────────────────────────────────
|
||||||
|
log "smoke-testing the full path (expect HTTP 401 from Anthropic)"
|
||||||
|
CODE=$(curl -sS -o /dev/null -w '%{http_code}' -m 30 \
|
||||||
|
-x "$PROXY_URL" \
|
||||||
|
https://api.anthropic.com/v1/messages \
|
||||||
|
-H "x-api-key: dummy" -H "anthropic-version: 2023-06-01" \
|
||||||
|
-H "content-type: application/json" \
|
||||||
|
-d '{"model":"claude-opus-4-7","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' \
|
||||||
|
|| echo "curl-failed")
|
||||||
|
|
||||||
|
case "$CODE" in
|
||||||
|
401) log "✓ smoke test passed (got 401 from api.anthropic.com)" ;;
|
||||||
|
407) fail "got 407 — proxy auth failed. Check PROXY_PASSWORD." ;;
|
||||||
|
403) fail "got 403 — Cloudflare Access is gating the tunnel. Run: cloudflared access login $HOSTNAME_PROXY" ;;
|
||||||
|
"curl-failed"|000) fail "curl couldn't reach the proxy. Check 'systemctl --user status $SERVICE_NAME'." ;;
|
||||||
|
*) fail "unexpected HTTP $CODE — check 'docker compose logs proxy' on the stack host." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -n "${WSL_REBOOT_NEEDED:-}" ]]; then
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────────────
|
||||||
|
One-time follow-up: /etc/wsl.conf was changed so /etc/hosts won't be
|
||||||
|
regenerated on next WSL boot. To make that effective, from PowerShell
|
||||||
|
on the Windows side, run once:
|
||||||
|
|
||||||
|
wsl --shutdown
|
||||||
|
|
||||||
|
Then reopen WSL. (Current session continues to work as-is.)
|
||||||
|
────────────────────────────────────────────────────────────────────
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "done. Try: claude -p 'ping'"
|
||||||
Reference in New Issue
Block a user