⬆️ feat: upgrade tunnel-doctor to v1.1.0 with SSH tunnel SOP and Makefile patterns
Add remote development SOP: proxy-safe Makefile patterns (--noproxy localhost), SSH tunnel targets (tunnel/tunnel-bg with autossh), multi-port tunnels, and end-to-end workflow with pre-flight checklist. Add diagnostic steps 2D (auth redirect via SSH forwarding) and 2E (localhost proxy interception). Fix step ordering, third-person description, and replace hardcoded IPs with placeholders. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,8 @@
|
|||||||
"email": "daymadev89@gmail.com"
|
"email": "daymadev89@gmail.com"
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Professional Claude Code skills for GitHub operations, document conversion, diagram generation, statusline customization, Teams communication, repomix utilities, skill creation, CLI demo generation, LLM icon access, Cloudflare troubleshooting, UI design system extraction, professional presentation creation, YouTube video downloading, secure repomix packaging, ASR transcription correction, video comparison quality analysis, comprehensive QA testing infrastructure, prompt optimization with EARS methodology, session history recovery, documentation cleanup, format-controlled deep research report generation with evidence tracking, PDF generation with Chinese font support, CLAUDE.md progressive disclosure optimization, CCPM skill registry search and management, Promptfoo LLM evaluation framework, iOS app development with XcodeGen and SwiftUI, fact-checking with automated corrections, Twitter/X content fetching, intelligent macOS disk space recovery, skill quality review and improvement, GitHub contribution strategy, complete internationalization/localization setup, plugin/skill troubleshooting with diagnostic tools, evidence-based competitor analysis with source citations, and Windows Remote Desktop (AVD/W365) connection quality diagnosis with transport protocol analysis and log parsing",
|
"description": "Professional Claude Code skills for GitHub operations, document conversion, diagram generation, statusline customization, Teams communication, repomix utilities, skill creation, CLI demo generation, LLM icon access, Cloudflare troubleshooting, UI design system extraction, professional presentation creation, YouTube video downloading, secure repomix packaging, ASR transcription correction, video comparison quality analysis, comprehensive QA testing infrastructure, prompt optimization with EARS methodology, session history recovery, documentation cleanup, format-controlled deep research report generation with evidence tracking, PDF generation with Chinese font support, CLAUDE.md progressive disclosure optimization, CCPM skill registry search and management, Promptfoo LLM evaluation framework, iOS app development with XcodeGen and SwiftUI, fact-checking with automated corrections, Twitter/X content fetching, intelligent macOS disk space recovery, skill quality review and improvement, GitHub contribution strategy, complete internationalization/localization setup, plugin/skill troubleshooting with diagnostic tools, evidence-based competitor analysis with source citations, Windows Remote Desktop (AVD/W365) connection quality diagnosis with transport protocol analysis and log parsing, and Tailscale+proxy conflict diagnosis with SSH tunnel SOP for remote development",
|
||||||
"version": "1.32.1",
|
"version": "1.33.0",
|
||||||
"homepage": "https://github.com/daymade/claude-code-skills"
|
"homepage": "https://github.com/daymade/claude-code-skills"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -732,10 +732,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tunnel-doctor",
|
"name": "tunnel-doctor",
|
||||||
"description": "Diagnose and fix route conflicts between Tailscale and proxy/VPN tools (Shadowrocket, Clash, Surge) on macOS. Use when Tailscale ping works but SSH/TCP times out, when proxy tools hijack the 100.64.0.0/10 CGNAT range, or when setting up Tailscale SSH to WSL instances",
|
"description": "Diagnoses and fixes conflicts between Tailscale and proxy/VPN tools (Shadowrocket, Clash, Surge) on macOS. Covers three conflict layers: route hijacking, HTTP proxy env var interception, and system proxy bypass. Includes SOP for remote development via SSH tunnels with proxy-safe Makefile patterns. Use when Tailscale ping works but SSH/HTTP times out, when browser returns 503 but curl works, when setting up Tailscale SSH to WSL instances, or when bootstrapping remote dev environments over Tailscale",
|
||||||
"source": "./",
|
"source": "./",
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"category": "developer-tools",
|
"category": "developer-tools",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tailscale",
|
"tailscale",
|
||||||
@@ -746,9 +746,13 @@
|
|||||||
"route-conflict",
|
"route-conflict",
|
||||||
"wsl",
|
"wsl",
|
||||||
"ssh",
|
"ssh",
|
||||||
|
"ssh-tunnel",
|
||||||
|
"autossh",
|
||||||
"proxy",
|
"proxy",
|
||||||
"networking",
|
"networking",
|
||||||
"macos"
|
"macos",
|
||||||
|
"makefile",
|
||||||
|
"remote-development"
|
||||||
],
|
],
|
||||||
"skills": [
|
"skills": [
|
||||||
"./tunnel-doctor"
|
"./tunnel-doctor"
|
||||||
|
|||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -10,6 +10,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
- None
|
- None
|
||||||
|
|
||||||
|
## [1.33.0] - 2026-02-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **tunnel-doctor** v1.0.0 → v1.1.0: Added remote development SOP with SSH tunnel and Makefile patterns
|
||||||
|
- New SOP section: proxy-safe Makefile pattern (`--noproxy localhost` for all health checks)
|
||||||
|
- New SOP section: SSH tunnel Makefile targets (`tunnel`/`tunnel-bg` with autossh)
|
||||||
|
- New SOP section: multi-port tunnel configuration
|
||||||
|
- New SOP section: SSH non-login shell setup (deduped, references proxy_fixes.md)
|
||||||
|
- New SOP section: end-to-end workflow (first-time setup + daily workflow)
|
||||||
|
- New SOP section: pre-flight checklist (10 verification items)
|
||||||
|
- New diagnostic step 2D: auth redirect fix via SSH local port forwarding
|
||||||
|
- New diagnostic step 2E: localhost proxy interception in Makefiles/scripts
|
||||||
|
- Fixed step ordering: 2A→2B→2C→2D→2E (was 2A→2C→2D→2E→2B)
|
||||||
|
- Fixed description to third-person voice per skill best practices
|
||||||
|
- Replaced hardcoded IP with `<tailscale-ip>` placeholder (5 occurrences)
|
||||||
|
- Added SSH non-login shell pitfall to references/proxy_fixes.md
|
||||||
|
- Added localhost proxy interception section to references/proxy_fixes.md
|
||||||
|
- Strengthened `--data-binary` vs `-d` warning in references/proxy_fixes.md
|
||||||
|
- New keywords: ssh-tunnel, autossh, makefile, remote-development
|
||||||
|
- Updated marketplace version from 1.32.1 to 1.33.0
|
||||||
|
|
||||||
## [1.32.0] - 2026-02-09
|
## [1.32.0] - 2026-02-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Security scan passed
|
Security scan passed
|
||||||
Scanned at: 2026-02-07T13:48:30.502686
|
Scanned at: 2026-02-16T02:22:59.043527
|
||||||
Tool: gitleaks + pattern-based validation
|
Tool: gitleaks + pattern-based validation
|
||||||
Content hash: 47400fa8c80000831683a9fffb470312077582721a64f6223b1506ce270bb616
|
Content hash: 14e09e78c62e5e85ec17a99df6248d46b3fd4bd50bd156d6fe3be6346628734e
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: tunnel-doctor
|
name: tunnel-doctor
|
||||||
description: Diagnose and fix conflicts between Tailscale and proxy/VPN tools (Shadowrocket, Clash, Surge) on macOS. Covers two conflict types - route hijacking (proxy TUN overrides Tailscale routes) and HTTP proxy env var interception (http_proxy/NO_PROXY misconfiguration). Use when Tailscale ping works but SSH/HTTP times out, when curl to Tailscale IPs returns empty/timeout, or when setting up Tailscale SSH to WSL instances.
|
description: Diagnoses and fixes conflicts between Tailscale and proxy/VPN tools (Shadowrocket, Clash, Surge) on macOS. Covers three conflict layers - (1) route hijacking (proxy TUN overrides Tailscale routes), (2) HTTP proxy env var interception (http_proxy/NO_PROXY misconfiguration), and (3) system proxy bypass (browser goes through VPN proxy, DIRECT rule can't reach Tailscale utun). Includes SOP for remote development via SSH tunnels with proxy-safe Makefile patterns. Use when Tailscale ping works but SSH/HTTP times out, when browser returns 503 but curl works, when setting up Tailscale SSH to WSL instances, or when bootstrapping remote dev environments over Tailscale.
|
||||||
allowed-tools: Read, Grep, Edit, Bash
|
allowed-tools: Read, Grep, Edit, Bash
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -8,18 +8,34 @@ allowed-tools: Read, Grep, Edit, Bash
|
|||||||
|
|
||||||
Diagnose and fix conflicts when Tailscale coexists with proxy/VPN tools on macOS, with specific guidance for SSH access to WSL instances.
|
Diagnose and fix conflicts when Tailscale coexists with proxy/VPN tools on macOS, with specific guidance for SSH access to WSL instances.
|
||||||
|
|
||||||
|
## Three Conflict Layers
|
||||||
|
|
||||||
|
Tailscale + proxy tools can conflict at three independent layers. Each has different symptoms:
|
||||||
|
|
||||||
|
| Layer | What breaks | What still works | Root cause |
|
||||||
|
|-------|-------------|------------------|------------|
|
||||||
|
| 1. Route table | Everything (SSH, curl, browser) | `tailscale ping` | `tun-excluded-routes` adds `en0` route overriding Tailscale utun |
|
||||||
|
| 2. HTTP env vars | `curl`, Python requests, Node.js fetch | SSH, browser | `http_proxy` set without `NO_PROXY` for Tailscale |
|
||||||
|
| 3. System proxy (browser) | Browser only (HTTP 503) | SSH, `curl` (both with/without proxy) | Browser uses VPN system proxy; DIRECT rule routes via Wi-Fi, not Tailscale utun |
|
||||||
|
|
||||||
## Diagnostic Workflow
|
## Diagnostic Workflow
|
||||||
|
|
||||||
### Step 1: Identify the Symptom
|
### Step 1: Identify the Symptom
|
||||||
|
|
||||||
Determine which scenario applies:
|
Determine which scenario applies:
|
||||||
|
|
||||||
|
- **Browser returns HTTP 503, but `curl` and SSH both work** → System proxy bypass conflict (Step 2C)
|
||||||
- **Tailscale ping works, SSH works, but curl/HTTP times out** → HTTP proxy env var conflict (Step 2A)
|
- **Tailscale ping works, SSH works, but curl/HTTP times out** → HTTP proxy env var conflict (Step 2A)
|
||||||
- **Tailscale ping works, SSH/TCP times out** → Route conflict (Step 2B)
|
- **Tailscale ping works, SSH/TCP times out** → Route conflict (Step 2B)
|
||||||
|
- **Remote dev server auth redirects to `localhost` → browser can't follow** → SSH tunnel needed (Step 2D)
|
||||||
|
- **`make status` / scripts curl to localhost fail with proxy** → localhost proxy interception (Step 2E)
|
||||||
- **SSH connects but `operation not permitted`** → Tailscale SSH config issue (Step 4)
|
- **SSH connects but `operation not permitted`** → Tailscale SSH config issue (Step 4)
|
||||||
- **SSH connects but `be-child ssh` exits code 1** → WSL snap sandbox issue (Step 5)
|
- **SSH connects but `be-child ssh` exits code 1** → WSL snap sandbox issue (Step 5)
|
||||||
|
|
||||||
**Key distinction**: SSH does NOT use `http_proxy`/`NO_PROXY` env vars, but curl/wget/Python requests/Node.js fetch do. If SSH works but HTTP doesn't, it's almost always a proxy env var issue, not a route issue.
|
**Key distinctions**:
|
||||||
|
- SSH does NOT use `http_proxy`/`NO_PROXY` env vars. If SSH works but HTTP doesn't → Layer 2.
|
||||||
|
- `curl` uses `http_proxy` env var, NOT the system proxy. Browser uses system proxy (set by VPN). If `curl` works but browser doesn't → Layer 3.
|
||||||
|
- If `tailscale ping` works but regular `ping` doesn't → Layer 1 (route table corrupted).
|
||||||
|
|
||||||
### Step 2A: Fix HTTP Proxy Environment Variables
|
### Step 2A: Fix HTTP Proxy Environment Variables
|
||||||
|
|
||||||
@@ -97,6 +113,89 @@ Two competing routes indicate a conflict:
|
|||||||
|
|
||||||
**Root cause**: On macOS, `UGSc` (Static Gateway) takes priority over `UCSI` (Cloned Static Interface) for the same prefix length.
|
**Root cause**: On macOS, `UGSc` (Static Gateway) takes priority over `UCSI` (Cloned Static Interface) for the same prefix length.
|
||||||
|
|
||||||
|
### Step 2C: Fix System Proxy Bypass (Browser 503)
|
||||||
|
|
||||||
|
**Symptom**: Browser shows HTTP 503 for `http://<tailscale-ip>:<port>`, but both `curl --noproxy '*'` and `curl` (with proxy env var) return 200. SSH also works.
|
||||||
|
|
||||||
|
**Root cause**: The browser uses the system proxy configured by the VPN profile (Shadowrocket/Clash/Surge). The proxy matches `IP-CIDR,100.64.0.0/10,DIRECT` and tries to connect directly — but "directly" means via the Wi-Fi interface (en0), NOT through Tailscale's utun interface. The proxy process itself doesn't have a route to Tailscale IPs, so the connection fails with 503.
|
||||||
|
|
||||||
|
**Diagnosis**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# curl with proxy env var works (curl connects to proxy port, but traffic flows differently)
|
||||||
|
curl -s -o /dev/null -w "%{http_code}" http://<tailscale-ip>:<port>/
|
||||||
|
# → 200
|
||||||
|
|
||||||
|
# Browser gets 503 because it goes through the VPN system proxy, not http_proxy env var
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix** — add Tailscale CGNAT range to `skip-proxy` in the proxy tool config:
|
||||||
|
|
||||||
|
For Shadowrocket, in `[General]`:
|
||||||
|
```
|
||||||
|
skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, localhost, *.local, captive.apple.com
|
||||||
|
```
|
||||||
|
|
||||||
|
`skip-proxy` tells the system "bypass the proxy entirely for these addresses." The browser then connects directly through the OS network stack, where Tailscale's routing table correctly handles the traffic.
|
||||||
|
|
||||||
|
**Why `skip-proxy` works but `tun-excluded-routes` doesn't**:
|
||||||
|
- `skip-proxy`: Bypasses the HTTP proxy layer only. Traffic still flows through the TUN interface and Tailscale utun handles it. Safe.
|
||||||
|
- `tun-excluded-routes`: Removes the CIDR from the TUN routing entirely. This creates a competing `en0` route that overrides Tailscale. Breaks everything.
|
||||||
|
|
||||||
|
### Step 2D: Fix Auth Redirect for Remote Dev (SSH Tunnel)
|
||||||
|
|
||||||
|
**Symptom**: Dev server runs on a remote machine (e.g., Mac Mini via Tailscale). You access `http://<tailscale-ip>:3010` in the browser. Login/signup works, but after auth, the app redirects to `http://localhost:3010/` which fails — `localhost` on your machine isn't running the dev server.
|
||||||
|
|
||||||
|
**Root cause**: The app's `APP_URL` (or equivalent) is set to `http://localhost:3010`. Auth libraries (Better-Auth, NextAuth, etc.) use this URL for callback redirects. Changing `APP_URL` to the Tailscale IP introduces Shadowrocket proxy conflicts and breaks local development on the remote machine.
|
||||||
|
|
||||||
|
**Fix** — SSH local port forwarding. This avoids all three conflict layers entirely:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Forward local port 3010 to remote machine's localhost:3010
|
||||||
|
ssh -NL 3010:localhost:3010 <tailscale-ip>
|
||||||
|
|
||||||
|
# Or with autossh for auto-reconnect (recommended for long sessions)
|
||||||
|
autossh -M 0 -f -N -L 3010:localhost:3010 \
|
||||||
|
-o "ServerAliveInterval=30" \
|
||||||
|
-o "ServerAliveCountMax=3" \
|
||||||
|
-o "ExitOnForwardFailure=yes" \
|
||||||
|
<tailscale-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now access `http://localhost:3010` in the browser. Auth redirects to `localhost:3010` → tunnel → remote dev server → works correctly.
|
||||||
|
|
||||||
|
**Why this is the best approach**:
|
||||||
|
- No `.env` changes needed — `APP_URL=http://localhost:3010` works everywhere
|
||||||
|
- No Shadowrocket conflicts — `localhost` is always in `skip-proxy`
|
||||||
|
- No code changes — same behavior as local development
|
||||||
|
- Industry standard — VS Code Remote SSH, GitHub Codespaces use the same pattern
|
||||||
|
|
||||||
|
**Install autossh**: `brew install autossh` (macOS) or `apt install autossh` (Linux)
|
||||||
|
|
||||||
|
**Kill background tunnel**: `pkill -f 'autossh.*<tailscale-ip>'`
|
||||||
|
|
||||||
|
### Step 2E: Fix localhost Proxy Interception in Scripts
|
||||||
|
|
||||||
|
**Symptom**: Makefile targets or scripts that `curl` localhost (health checks, warmup routes) fail or timeout when `http_proxy` is set globally in the shell.
|
||||||
|
|
||||||
|
**Root cause**: `http_proxy=http://127.0.0.1:1082` is set in `~/.zshrc` but `no_proxy` doesn't include `localhost`. All curl commands send localhost requests through the proxy.
|
||||||
|
|
||||||
|
**Fix** — add `--noproxy localhost` to all localhost curl commands in scripts:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# WRONG — fails when http_proxy is set
|
||||||
|
@curl -sf http://localhost:9000/minio/health/live && echo "OK"
|
||||||
|
|
||||||
|
# CORRECT — always bypasses proxy for localhost
|
||||||
|
@curl --noproxy localhost -sf http://localhost:9000/minio/health/live && echo "OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, set `no_proxy` globally in `~/.zshrc`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export no_proxy=localhost,127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
### Step 3: Fix Proxy Tool Configuration
|
### Step 3: Fix Proxy Tool Configuration
|
||||||
|
|
||||||
Identify the proxy tool and apply the appropriate fix. See [references/proxy_fixes.md](references/proxy_fixes.md) for detailed instructions per tool.
|
Identify the proxy tool and apply the appropriate fix. See [references/proxy_fixes.md](references/proxy_fixes.md) for detailed instructions per tool.
|
||||||
@@ -188,6 +287,170 @@ ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no <user>@<tailscale-ip> 'echo
|
|||||||
|
|
||||||
All three must pass. If step 1 fails, revisit Step 3. If step 2 fails, check WSL sshd or firewall. If step 3 fails, revisit Steps 4-5.
|
All three must pass. If step 1 fails, revisit Step 3. If step 2 fails, check WSL sshd or firewall. If step 3 fails, revisit Steps 4-5.
|
||||||
|
|
||||||
|
## SOP: Remote Development via Tailscale
|
||||||
|
|
||||||
|
Proactive setup guide for remote development over Tailscale with proxy tools. Follow these steps **before** encountering problems.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Tailscale installed and running on both machines
|
||||||
|
- Proxy tool (Shadowrocket/Clash/Surge) configured with Tailscale compatibility (see Step 3 above)
|
||||||
|
- SSH access working: `ssh <tailscale-ip> 'echo ok'`
|
||||||
|
|
||||||
|
### 1. Proxy-Safe Makefile Pattern
|
||||||
|
|
||||||
|
Any Makefile target that curls `localhost` must use `--noproxy localhost`. This is required because `http_proxy` is often set globally in `~/.zshrc` (common in China), and Make inherits shell environment variables.
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
## ── Health Checks ─────────────────────────────────────
|
||||||
|
|
||||||
|
status: ## Health check dashboard
|
||||||
|
@echo "=== Dev Infrastructure ==="
|
||||||
|
@docker exec my-postgres pg_isready -U postgres 2>/dev/null && echo "PostgreSQL: OK" || echo "PostgreSQL: FAIL"
|
||||||
|
@curl --noproxy localhost -sf http://localhost:9000/minio/health/live >/dev/null 2>&1 && echo "MinIO: OK" || echo "MinIO: FAIL"
|
||||||
|
@curl --noproxy localhost -sf http://localhost:3001/api/status >/dev/null 2>&1 && echo "API: OK" || echo "API: FAIL"
|
||||||
|
|
||||||
|
## ── Route Warmup ──────────────────────────────────────
|
||||||
|
|
||||||
|
warmup: ## Pre-compile key routes (run after dev server is ready)
|
||||||
|
@echo "Warming up dev server routes..."
|
||||||
|
@echo -n " /api/health → " && curl --noproxy localhost -s -o /dev/null -w '%{http_code} (%{time_total}s)\n' http://localhost:3010/api/health
|
||||||
|
@echo -n " / → " && curl --noproxy localhost -s -o /dev/null -w '%{http_code} (%{time_total}s)\n' http://localhost:3010/
|
||||||
|
@echo "Warmup complete."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules**:
|
||||||
|
- Every `curl http://localhost` call MUST include `--noproxy localhost`
|
||||||
|
- Docker commands (`docker exec`) are unaffected by `http_proxy` — no fix needed
|
||||||
|
- `redis-cli`, `pg_isready` connect via TCP directly — no fix needed
|
||||||
|
|
||||||
|
### 2. SSH Tunnel Makefile Targets
|
||||||
|
|
||||||
|
Add these targets for remote development via Tailscale SSH tunnels:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
## ── Remote Development ────────────────────────────────
|
||||||
|
|
||||||
|
REMOTE_HOST ?= <tailscale-ip>
|
||||||
|
TUNNEL_FORWARD ?= -L 3010:localhost:3010
|
||||||
|
|
||||||
|
tunnel: ## SSH tunnel to remote machine (foreground)
|
||||||
|
ssh -N $(TUNNEL_FORWARD) $(REMOTE_HOST)
|
||||||
|
|
||||||
|
tunnel-bg: ## SSH tunnel to remote machine (background, auto-reconnect)
|
||||||
|
autossh -M 0 -f -N $(TUNNEL_FORWARD) \
|
||||||
|
-o "ServerAliveInterval=30" \
|
||||||
|
-o "ServerAliveCountMax=3" \
|
||||||
|
-o "ExitOnForwardFailure=yes" \
|
||||||
|
$(REMOTE_HOST)
|
||||||
|
@echo "Tunnel running in background. Kill with: pkill -f 'autossh.*$(REMOTE_HOST)'"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Design decisions**:
|
||||||
|
|
||||||
|
| Choice | Rationale |
|
||||||
|
|--------|-----------|
|
||||||
|
| `?=` (conditional assign) | Allows override: `make tunnel REMOTE_HOST=100.x.x.x` |
|
||||||
|
| `TUNNEL_FORWARD` as variable | Supports multi-port: `make tunnel TUNNEL_FORWARD="-L 3010:localhost:3010 -L 9000:localhost:9000"` |
|
||||||
|
| `autossh -M 0` | Disables autossh's own monitoring port; relies on `ServerAliveInterval` instead (more reliable through NAT) |
|
||||||
|
| `ExitOnForwardFailure=yes` | Fails immediately if port is already bound, instead of silently running without tunnel |
|
||||||
|
| Kill hint uses `autossh.*$(REMOTE_HOST)` | Precise pattern — won't accidentally kill other SSH sessions |
|
||||||
|
|
||||||
|
**Install autossh**: `brew install autossh` (macOS) or `apt install autossh` (Linux/WSL)
|
||||||
|
|
||||||
|
### 3. Multi-Port Tunnels
|
||||||
|
|
||||||
|
When the project requires multiple services (dev server + object storage + API gateway):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Forward multiple ports in one tunnel
|
||||||
|
make tunnel TUNNEL_FORWARD="-L 3010:localhost:3010 -L 9000:localhost:9000 -L 3001:localhost:3001"
|
||||||
|
|
||||||
|
# Or define a project-specific default in Makefile
|
||||||
|
TUNNEL_FORWARD ?= -L 3010:localhost:3010 -L 9000:localhost:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `-L` flag is independent. If one port is already bound locally, `ExitOnForwardFailure=yes` will abort the entire tunnel — fix the port conflict first.
|
||||||
|
|
||||||
|
### 4. SSH Non-Login Shell Setup
|
||||||
|
|
||||||
|
SSH non-login shells don't load `~/.zshrc`, so nvm/Homebrew tools and proxy env vars are unavailable. Prefix all remote commands with `source ~/.zshrc 2>/dev/null;`. See [references/proxy_fixes.md § SSH Non-Login Shell Pitfall](references/proxy_fixes.md) for details and examples.
|
||||||
|
|
||||||
|
For Makefile targets that run remote commands:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
REMOTE_CMD = ssh $(REMOTE_HOST) 'source ~/.zshrc 2>/dev/null; $(1)'
|
||||||
|
|
||||||
|
remote-status: ## Check remote dev server status
|
||||||
|
$(call REMOTE_CMD,curl --noproxy localhost -sf http://localhost:3010/api/health && echo "OK" || echo "FAIL")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. End-to-End Workflow
|
||||||
|
|
||||||
|
#### First-time setup (remote machine)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone repo and install dependencies
|
||||||
|
ssh <tailscale-ip>
|
||||||
|
cd /path/to/project
|
||||||
|
git clone git@github.com:user/repo.git && cd repo
|
||||||
|
pnpm install # Add --registry https://registry.npmmirror.com if in China
|
||||||
|
|
||||||
|
# 2. Copy .env from local machine (run on local)
|
||||||
|
scp .env <tailscale-ip>:/path/to/project/repo/.env
|
||||||
|
|
||||||
|
# 3. Start Docker infrastructure
|
||||||
|
make up && make status
|
||||||
|
|
||||||
|
# 4. Run database migrations
|
||||||
|
bun run db:migrate
|
||||||
|
|
||||||
|
# 5. Start dev server
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Daily workflow (local machine)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Start tunnel
|
||||||
|
make tunnel-bg
|
||||||
|
|
||||||
|
# 2. Open browser
|
||||||
|
open http://localhost:3010
|
||||||
|
|
||||||
|
# 3. Auth, coding, testing — everything works as if local
|
||||||
|
|
||||||
|
# 4. When done, kill tunnel
|
||||||
|
pkill -f 'autossh.*<tailscale-ip>'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Why this works
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser → localhost:3010 → SSH tunnel → Remote localhost:3010 → Dev server
|
||||||
|
↓
|
||||||
|
Auth redirects to localhost:3010
|
||||||
|
↓
|
||||||
|
Browser follows redirect → same tunnel → works
|
||||||
|
```
|
||||||
|
|
||||||
|
The key insight: `APP_URL=http://localhost:3010` in `.env` is correct for **both** local and remote development. The SSH tunnel makes the remote server's localhost accessible as the local machine's localhost. Auth callback redirects to `localhost:3010` always resolve correctly.
|
||||||
|
|
||||||
|
### 6. Checklist
|
||||||
|
|
||||||
|
Before starting remote development, verify:
|
||||||
|
|
||||||
|
- [ ] Tailscale connected: `tailscale status`
|
||||||
|
- [ ] SSH works: `ssh <tailscale-ip> 'echo ok'`
|
||||||
|
- [ ] Proxy tool configured: `[Rule]` has `IP-CIDR,100.64.0.0/10,DIRECT`
|
||||||
|
- [ ] `skip-proxy` includes `100.64.0.0/10`
|
||||||
|
- [ ] `tun-excluded-routes` does NOT include `100.64.0.0/10`
|
||||||
|
- [ ] `NO_PROXY` includes `.ts.net,100.64.0.0/10`
|
||||||
|
- [ ] `autossh` installed: `which autossh`
|
||||||
|
- [ ] Makefile curl commands have `--noproxy localhost`
|
||||||
|
- [ ] Remote dev server running: `ssh <ip> 'source ~/.zshrc 2>/dev/null; curl --noproxy localhost -sf http://localhost:3010/'`
|
||||||
|
- [ ] Tunnel works: `make tunnel-bg && curl -sf http://localhost:3010/`
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [references/proxy_fixes.md](references/proxy_fixes.md) — Detailed fix instructions for Shadowrocket, Clash, and Surge
|
- [references/proxy_fixes.md](references/proxy_fixes.md) — Detailed fix instructions for Shadowrocket, Clash, and Surge
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ Detailed instructions for making each proxy tool coexist with Tailscale on macOS
|
|||||||
|
|
||||||
Shadowrocket's `tun-excluded-routes` adds a system route `100.64/10 → default gateway (en0)` for each excluded CIDR. This route has higher priority (`UGSc`) than Tailscale's route (`UCSI`), hijacking all Tailscale traffic.
|
Shadowrocket's `tun-excluded-routes` adds a system route `100.64/10 → default gateway (en0)` for each excluded CIDR. This route has higher priority (`UGSc`) than Tailscale's route (`UCSI`), hijacking all Tailscale traffic.
|
||||||
|
|
||||||
### The Fix
|
### The Fix (Three Settings)
|
||||||
|
|
||||||
1. **Remove** `100.64.0.0/10` from `tun-excluded-routes` in `[General]`
|
Three Shadowrocket settings work together to handle Tailscale traffic correctly:
|
||||||
2. **Add** a DIRECT rule in `[Rule]` section:
|
|
||||||
|
#### 1. `[Rule]` — Add DIRECT rule (handles TUN-level routing)
|
||||||
|
|
||||||
```
|
```
|
||||||
IP-CIDR,100.64.0.0/10,DIRECT
|
IP-CIDR,100.64.0.0/10,DIRECT
|
||||||
@@ -27,6 +28,26 @@ IP-CIDR,100.64.0.0/10,DIRECT
|
|||||||
|
|
||||||
This lets Tailscale traffic enter the Shadowrocket TUN interface, where the DIRECT rule passes it through without proxying. The system route table remains clean.
|
This lets Tailscale traffic enter the Shadowrocket TUN interface, where the DIRECT rule passes it through without proxying. The system route table remains clean.
|
||||||
|
|
||||||
|
#### 2. `skip-proxy` — Add Tailscale CGNAT range (fixes browser 503)
|
||||||
|
|
||||||
|
In `[General]`, add `100.64.0.0/10` to `skip-proxy`:
|
||||||
|
|
||||||
|
```
|
||||||
|
skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, localhost, *.local, captive.apple.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is needed**: Browsers (Chrome, Safari) use the system proxy set by the VPN profile, not `http_proxy` env vars. Without `skip-proxy`, the browser sends Tailscale requests to Shadowrocket's proxy process. The DIRECT rule tells the proxy to connect "directly" — but the proxy connects via Wi-Fi (en0), not Tailscale's utun, resulting in HTTP 503.
|
||||||
|
|
||||||
|
With `skip-proxy`, the system bypasses the proxy entirely for these IPs. The browser connects through the normal OS network stack where Tailscale's routing works correctly.
|
||||||
|
|
||||||
|
#### 3. `tun-excluded-routes` — Do NOT add `100.64.0.0/10`
|
||||||
|
|
||||||
|
**Never** add `100.64.0.0/10` to `tun-excluded-routes`. This breaks Tailscale completely:
|
||||||
|
- Shadowrocket adds `100.64/10 → en0 (UGSc)` to the system route table
|
||||||
|
- This overrides Tailscale's `100.64/10 → utun (UCSI)` route
|
||||||
|
- Result: `tailscale ping` works (Tailscale-layer), but SSH, ping, curl, browser all fail (OS-layer)
|
||||||
|
- Reverting and restarting Shadowrocket VPN restores the routes
|
||||||
|
|
||||||
### Config API
|
### Config API
|
||||||
|
|
||||||
Shadowrocket exposes a config editor API when the **Edit Plain Text** view is open:
|
Shadowrocket exposes a config editor API when the **Edit Plain Text** view is open:
|
||||||
@@ -39,6 +60,24 @@ NO_PROXY="<shadowrocket-ip>" curl -s "http://<shadowrocket-ip>:8080/api/read"
|
|||||||
NO_PROXY="<shadowrocket-ip>" curl -s -X POST "http://<shadowrocket-ip>:8080/api/save" --data-binary @config.txt
|
NO_PROXY="<shadowrocket-ip>" curl -s -X POST "http://<shadowrocket-ip>:8080/api/save" --data-binary @config.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Detect Shadowrocket IP**: The device IP changes with DHCP. Do not hardcode it. Detect it before use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If you know the device is on the same subnet
|
||||||
|
# Check common ports or use mDNS
|
||||||
|
curl --noproxy '*' -s --connect-timeout 2 "http://192.168.31.110:8080/api/read" | head -1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical**: Use `--data-binary`, NOT `-d`. The `-d` flag URL-encodes the content, corrupting `#`, `=`, `&` and other characters in the config. This **destroys the entire configuration** — all rules, settings, and proxy groups are lost. The user must restore from backup.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CORRECT — preserves raw content
|
||||||
|
curl -s -X POST "http://<ip>:8080/api/save" --data-binary @config.txt
|
||||||
|
|
||||||
|
# WRONG — URL-encodes special chars, destroys config
|
||||||
|
curl -s -X POST "http://<ip>:8080/api/save" -d @config.txt
|
||||||
|
```
|
||||||
|
|
||||||
**Important**: The API `save` only writes to the editor buffer. The user must click **Save** in the Shadowrocket UI to persist changes. After saving, the VPN connection must be restarted for route changes to take effect.
|
**Important**: The API `save` only writes to the editor buffer. The user must click **Save** in the Shadowrocket UI to persist changes. After saving, the VPN connection must be restarted for route changes to take effect.
|
||||||
|
|
||||||
### Example tun-excluded-routes (correct)
|
### Example tun-excluded-routes (correct)
|
||||||
@@ -88,7 +127,7 @@ IP-CIDR,fd7a:115c:a1e0::/48,DIRECT
|
|||||||
|
|
||||||
In Surge's **TUN Excluded Routes** (if available), the same caveat applies as Shadowrocket: excluding `100.64.0.0/10` may add an `en0` route. Test with `route -n get` to confirm.
|
In Surge's **TUN Excluded Routes** (if available), the same caveat applies as Shadowrocket: excluding `100.64.0.0/10` may add an `en0` route. Test with `route -n get` to confirm.
|
||||||
|
|
||||||
Surge also supports `skip-proxy` and `always-real-ip` which may help:
|
Surge also supports `skip-proxy` and `always-real-ip`. Adding `skip-proxy` is **required** to fix browser 503 (same mechanism as Shadowrocket):
|
||||||
|
|
||||||
```
|
```
|
||||||
[General]
|
[General]
|
||||||
@@ -141,6 +180,18 @@ tailscale dns status
|
|||||||
|
|
||||||
## General Principles
|
## General Principles
|
||||||
|
|
||||||
|
### Three Conflict Layers
|
||||||
|
|
||||||
|
Proxy tools and Tailscale can conflict at three independent layers on macOS:
|
||||||
|
|
||||||
|
| Layer | Setting | What it controls | Symptom when wrong |
|
||||||
|
|-------|---------|------------------|--------------------|
|
||||||
|
| 1. Route table | `tun-excluded-routes` | OS-level IP routing | Everything broken (SSH, curl, browser). `tailscale ping` works but `ping` doesn't |
|
||||||
|
| 2. HTTP env vars | `http_proxy` / `NO_PROXY` | CLI tools (curl, wget, Python, Node.js) | `curl` times out, SSH works, browser works |
|
||||||
|
| 3. System proxy | `skip-proxy` | Browser and system HTTP clients | Browser 503, `curl` works (both with/without proxy), SSH works |
|
||||||
|
|
||||||
|
**Each layer is independent.** A fix at one layer doesn't help the others. You may need fixes at multiple layers simultaneously.
|
||||||
|
|
||||||
### Why tun-excluded-routes Breaks Tailscale
|
### Why tun-excluded-routes Breaks Tailscale
|
||||||
|
|
||||||
On macOS, when a VPN tool excludes a CIDR from its TUN interface, it typically adds a system route pointing that CIDR to the default gateway via `en0`. For `100.64.0.0/10`:
|
On macOS, when a VPN tool excludes a CIDR from its TUN interface, it typically adds a system route pointing that CIDR to the default gateway via `en0`. For `100.64.0.0/10`:
|
||||||
@@ -152,9 +203,27 @@ On macOS, when a VPN tool excludes a CIDR from its TUN interface, it typically a
|
|||||||
|
|
||||||
macOS route priority: `UGSc` > `UCSI` for same prefix length. Result: Tailscale traffic goes to the router, which has no route to 100.x addresses.
|
macOS route priority: `UGSc` > `UCSI` for same prefix length. Result: Tailscale traffic goes to the router, which has no route to 100.x addresses.
|
||||||
|
|
||||||
|
### Why skip-proxy Is Needed for Browsers
|
||||||
|
|
||||||
|
Even with correct routes and a DIRECT rule, browsers can still get 503. The flow:
|
||||||
|
|
||||||
|
1. Browser sends request to Shadowrocket's system proxy (set by VPN profile)
|
||||||
|
2. Shadowrocket matches `IP-CIDR,100.64.0.0/10,DIRECT`
|
||||||
|
3. Shadowrocket tries to connect "directly" — but from its own process context, via Wi-Fi (en0)
|
||||||
|
4. `100.x.x.x` is unreachable via en0 → 503
|
||||||
|
|
||||||
|
`curl` works because it uses the `http_proxy` env var (or no proxy with `--noproxy`), going through the OS network stack where Tailscale routing works. Browsers don't use `http_proxy` — they use the system proxy.
|
||||||
|
|
||||||
|
Adding `100.64.0.0/10` to `skip-proxy` makes the system bypass the proxy entirely for those IPs. The browser connects directly through the OS network stack → Tailscale utun handles routing → connection succeeds.
|
||||||
|
|
||||||
### The Correct Approach
|
### The Correct Approach
|
||||||
|
|
||||||
Let Tailscale traffic enter the VPN TUN interface, then use an application-layer DIRECT/bypass rule to send it out without proxying. This avoids polluting the system route table.
|
For full Tailscale compatibility with proxy tools, apply all three:
|
||||||
|
|
||||||
|
1. **`[Rule]`**: `IP-CIDR,100.64.0.0/10,DIRECT` — handles TUN-level traffic
|
||||||
|
2. **`skip-proxy`**: Add `100.64.0.0/10` — fixes browser access
|
||||||
|
3. **`NO_PROXY` env var**: Add `100.64.0.0/10,.ts.net` — fixes CLI HTTP tools
|
||||||
|
4. **`tun-excluded-routes`**: Do NOT add `100.64.0.0/10` — this breaks everything
|
||||||
|
|
||||||
### Quick Verification
|
### Quick Verification
|
||||||
|
|
||||||
@@ -166,16 +235,62 @@ route -n get <tailscale-ip>
|
|||||||
|
|
||||||
# Should show only one 100.64/10 route (Tailscale's)
|
# Should show only one 100.64/10 route (Tailscale's)
|
||||||
netstat -rn | grep 100.64
|
netstat -rn | grep 100.64
|
||||||
|
|
||||||
|
# SSH must work
|
||||||
|
ssh -o ConnectTimeout=5 <user>@<tailscale-ip> 'echo ok'
|
||||||
|
|
||||||
|
# curl must work (with and without proxy)
|
||||||
|
curl --noproxy '*' -s -o /dev/null -w "%{http_code}" http://<tailscale-ip>:<port>/
|
||||||
|
curl -s -o /dev/null -w "%{http_code}" http://<tailscale-ip>:<port>/
|
||||||
|
|
||||||
|
# Browser must work (open in Chrome, no 503)
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH Non-Login Shell Pitfall
|
||||||
|
|
||||||
|
When SSHing to a remote macOS machine, non-login shells don't load `~/.zshrc`. Tools installed via nvm, Homebrew, or other shell-level managers won't be in `$PATH`. Proxy env vars set in `~/.zshrc` also won't be loaded.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# FAILS — non-login shell, nvm/proxy not loaded
|
||||||
|
ssh <tailscale-ip> 'node --version'
|
||||||
|
# → command not found
|
||||||
|
|
||||||
|
# WORKS — explicitly source shell config
|
||||||
|
ssh <tailscale-ip> 'source ~/.zshrc 2>/dev/null; node --version'
|
||||||
|
# → v22.18.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: `bash -lc` loads `.bash_profile` but NOT `.zshrc`. On macOS (default shell is zsh), always use `source ~/.zshrc` or `zsh -ic` for interactive shell initialization.
|
||||||
|
|
||||||
|
### localhost Proxy Interception in Scripts
|
||||||
|
|
||||||
|
When `http_proxy` is set globally (common in China), any script or Makefile that curls `localhost` will fail unless it bypasses the proxy. This affects health checks, warmup scripts, and test harnesses.
|
||||||
|
|
||||||
|
**Fix**: Add `--noproxy localhost` to every localhost curl call in Makefiles and scripts:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Health check that works regardless of proxy settings
|
||||||
|
@curl --noproxy localhost -sf http://localhost:9000/minio/health/live && echo "OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or set `no_proxy` in `~/.zshrc` alongside `http_proxy`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export http_proxy=http://127.0.0.1:1082
|
||||||
|
export https_proxy=http://127.0.0.1:1082
|
||||||
|
export no_proxy=localhost,127.0.0.1 # Always add this alongside proxy vars
|
||||||
```
|
```
|
||||||
|
|
||||||
### Emergency Rollback
|
### Emergency Rollback
|
||||||
|
|
||||||
If a proxy config change breaks other connectivity:
|
If a proxy config change breaks Tailscale connectivity:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Restart the proxy tool (Shadowrocket/Clash/Surge)
|
# Revert the config change and restart Shadowrocket VPN
|
||||||
# This restores its default routes
|
# This restores the original routes
|
||||||
|
|
||||||
# Or manually delete a conflicting route:
|
# Or manually delete a conflicting route:
|
||||||
sudo route delete -net 100.64.0.0/10 <gateway-ip>
|
sudo route delete -net 100.64.0.0/10 <gateway-ip>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `tun-excluded-routes` was modified, reverting it and restarting Shadowrocket will restore Tailscale's routing immediately.
|
||||||
|
|||||||
Reference in New Issue
Block a user