eb9a0c90ca
Install is already idempotent-upgrade (apt install caddy / xray install script / get.hy2.sh all fetch latest); upgrade mode additionally requires an existing setup.env (dies instead of accidentally installing) and never prompts. Legacy setup.env without HY2_CERT_MODE infers dns when CF_API_TOKEN present, so upgrades keep wildcard certs instead of silently switching to http mode. Co-Authored-By: Claude <noreply@anthropic.com>
8.1 KiB
8.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this repo is
Two self-contained bash scripts:
setup_warp_zerotrust.sh— gives an IPv6-only Debian/Ubuntu VPS an IPv4 egress via Cloudflare WARP as a plain WireGuard interface (wg0) — no official client. Two modes, selected by environment variables:
- Consumer mode (default): anonymous free-WARP registration.
- Team mode (
CF_TEAM+CF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRET, orWARP_JWT): enrolls as a Cloudflare One (Zero Trust) organization device via service-token → JWT exchange athttps://<team>.cloudflareaccess.com/warp, then registers with headerCF-Access-Jwt-Assertion.
setup_caddy_xray_hysteria.sh— deploys Caddy + Xray (VLESS+REALITY/WS/XHTTP) + Hysteria2 + nftables port-hopping on a Debian/Ubuntu VPS. Xray:443 takes REALITY traffic and falls back to Caddy on127.0.0.1:8003(PROXY protocol); Caddy serves the camouflage reverse-proxy and routes/scilad(WS:54442) and a random path (XHTTP:8080). Hysteria2:8443/UDP gets its cert viaHY2_CERT_MODE(defaulthttp): built-in certmagic ACME HTTP-01 listening only on127.0.0.1:9180(altPort), with Caddy's port-80 block reverse-proxying/.well-known/acme-challenge/to it — no public port opened, no DNS token needed, non-wildcardDOMAINcert, auto-renewed and hot-applied (TLS-ALPN is unusable: the CA only connects to TCP 443, which Xray owns). Fallbackdns: Cloudflare DNS-01 for a*.BASE_DOMAINwildcard (needsCF_API_TOKEN). nftables redirects UDP 20000-30000 → 8443. Config templates are embedded as quoted heredocs with__VAR__placeholders (rendered via bash parameter expansion, never sed); credentials auto-generate and persist to/etc/caddy-xray-hy2/setup.env(mode 600) which is sourced on re-run.DRY_RUN=1renders everything to./dry-run-output/without system changes — this is howtests/run_tests.shvalidates the script on a dev machine. Actions:ACTION=install(default) /upgrade(same idempotent pipeline — apt/script installs already pull latest — but requires an existing setup.env and never prompts) /uninstall. Legacy setup.env files withoutHY2_CERT_MODEinferdnswhenCF_API_TOKENis present, so upgrades don't silently switch wildcard certs to single-domain. Unlike the WARP script, this one intentionally uses the official cloudsmith apt repo (Caddy) and the official Xray/Hysteria2 install scripts; original hand-written templates are archived indocs/templates-original/.
There is no build system or package manifest; tests are tests/run_tests.sh (DRY_RUN + grep/json.tool assertions) for the caddy/xray/hysteria script only.
Commands
- Syntax check:
bash -n setup_warp_zerotrust.sh setup_caddy_xray_hysteria.sh - Lint:
shellcheck setup_warp_zerotrust.sh setup_caddy_xray_hysteria.sh - Test (caddy/xray/hysteria script):
bash tests/run_tests.sh - Run (only on a disposable target VPS, never on a dev machine):
sudo bash setup_warp_zerotrust.sh- With no env vars it interactively prompts for team name + service token (reads from
/dev/tty, sowget|bashpiping still works); empty team name or incomplete credentials silently fall back to consumer mode. Env vars fully set → no prompts. No controlling tty → prompts skipped, env-only. - Team mode:
sudo CF_TEAM=<team> CF_ACCESS_CLIENT_ID=<id> CF_ACCESS_CLIENT_SECRET=<secret> bash setup_warp_zerotrust.sh - Uninstall + free the device seat:
sudo ACTION=delete bash setup_warp_zerotrust.sh - Optional overrides:
ALLOWED_IPS(default0.0.0.0/0),MTU(default1280)
- With no env vars it interactively prompts for team name + service token (reads from
- It requires root,
apt-installs packages, overwrites/etc/resolv.conf, writes/etc/wireguard/wg0.conf+/etc/wireguard/warp-account.json, and enables thewg-quick@wg0systemd unit. - Verification is built into the script's final output: IPv4 exit IP, unchanged IPv6 exit, and
warp=from cloudflare.com/cdn-cgi/trace.
Architecture and invariants
The script is a one-shot bootstrap sequence; its correctness depends on a few deliberate constraints:
- Temporary DNS64 for bootstrap only, with two takeover modes chosen by resolv.conf's actual disposition. If
/etc/resolv.confresolves (viareadlink -f) into/run/systemd/resolve/*and systemd-resolved is active, the script never touches resolv.conf — it injects global DNS64 via an/etc/systemd/resolved.conf.d/zz-warp-dns64.confdrop-in (DNS=+Domains=~.) and restarts resolved; restore just deletes the drop-in (writing through the Ubuntu-default stub symlink would clobber a resolved-managed file in /run that resolved never reads, silently breaking the bootstrap). Otherwise it backs up the file's content and overwrites resolv.conf; restore writes the backup back in place (cat > file, never whole-filecp) so the inode and any symlink survive and systemd-resolved's inotify watch keeps working (acp-style inode replacement leaves resolved blind to the file while its contents look correct). Atrap cleanup EXIT INT TERMguarantees restoration even on failure — any new early-exit path must preserve this guarantee, or the VPS is left with broken/wrong DNS. - IPv6 transport, IPv4 payload. Registration uses
curl -6(the API has native AAAA) and the WireGuard endpoint is Cloudflare's IPv6 anycast ([2606:4700:d0::a29f:c001]:2408), butAllowedIPs = 0.0.0.0/0by default — IPv6 traffic must stay on the native connection. Dashboard split-tunnel settings do NOT apply to a plain WireGuard client;ALLOWED_IPSis the only routing control. - The reg API returns a FLAT JSON object (
.id,.token,.config.interface.addresses.{v4,v6},.config.peers[0]) — there is no.resultwrapper (that's the v4 dashboard API convention, not this one). ACF-Client-Versionheader is mandatory or registration is rejected; known-good pairings (as of 2026-08):v0a4471+a-6.35-4471(Aether) andv0a1922+a-6.3-1922(wgcf v2.2.32). The script tries both. Registration curls pin--tlsv1.2 --tls-max 1.2 --http1.1because Cloudflare rejects non-app TLS fingerprints with 403 error 1020. HTTP 403 otherwise means the VPS IP is flagged; 429 means rate-limited. - Idempotency via
/etc/wireguard/warp-account.json. Re-runs PATCH the existing device's key (PATCH /reg/<id>with its Bearer token) instead of registering anew — Zero Trust free plans cap at 50 devices, so repeated fresh registrations would burn seats. The PATCH path is only taken when the saved team matches the currentCF_TEAM(switching free ↔ team always re-registers). Fall back to fresh registration on 401/404/410. - MTU 1280 survives the double overhead of NAT64/DNS64 plus the WireGuard tunnel; don't raise it without testing on the target network.
- Kernel-module probe with wireguard-go fallback. The script probes
ip link add wgprobe type wireguard(aftermodprobe wireguard); on failure it installswireguard-go(noble universe, ~1MB) and relies on wg-quick's built-in userspace fallback (add_if()falls back towireguard-gowhen the kernel module is absent — covers LXC containers and custom old kernels, works for boot autostart too). Requires/dev/net/tun; if absent (some LXC), the script errors out with a hint. - Supply-chain constraint (user requirement). Install only via the distro's signed apt repos; never add third-party apt sources, download binaries, or pipe remote code. Outbound calls go to Cloudflare-owned domains only (registration API, team Access endpoint, cdn-cgi/trace for the connectivity check — ip.sb kept only as fallback). The bootstrap DNS64 resolvers are overridable via
DNS64_SERVERS. Keys/credentials stay local under/etc/wireguard/(mode 600); only the WireGuard public key leaves the machine.
Conventions
- Script comments are written in Chinese; match that when editing.
README.md(Chinese) documents the 2026 dashboard paths for Zero Trust service-token enrollment — keep its UI paths in sync with the script header comment when Cloudflare reorganizes navigation.- The repo has no CI; commits are small single-file updates (historically made via the GitHub web UI).