Skill: darwin7381/frp-tunnel (version 1.0.0)

--- name: frp-tunnel description: Share local development servers via self-hosted frp tunnel with custom domains and auto HTTPS. Use when you need to share localhost with others, demo to clients, or test on mobile. Replaces ngrok/localhost.run with a stable, unlimited, self-hosted solution. --- # frp-tunnel — 自建內網穿透 用自建的 frp + Caddy + Hetzner VPS 分享本地開發中的網站。自訂域名、自動 HTTPS、無限 tunnel。 > 替代方案（無 VPS 時）：見 `old-share-local-site` skill（ngrok/localhost.run） ## ⚙️ Config（自訂區塊） 使用此 skill 前，請將以下值替換成你自己的環境： | 變數 | 目前的值 | 說明 | |------|---------|------| | VPS IP | `5.223.75.160` | 你的 VPS 公網 IP | | 域名 | `*.tunnel.fud.city` | 你的 wildcard 子域名 | | DNS Provider | Cloudflare (`fud.city`) | 管理你域名 DNS 的服務 | | GitHub 備份 | `https://github.com/darwin7381/frp-tunnel` | 你的 private repo（可選） | > **Dashboard 密碼**：不記在這裡。在 VPS 的 `frps.toml` 裡設定你自己的密碼。 ## 基礎資訊 | 項目 | 值 | |------|-----| | VPS IP | 5.223.75.160 | | VPS Provider | Hetzner Cloud, Singapore, CPX12 | | 域名 | *.tunnel.fud.city | | DNS | Cloudflare (fud.city) | | HTTPS 方式 | **Wildcard cert** (DNS-01 challenge via Cloudflare API) | | frp Dashboard | http://5.223.75.160:7500 | | GitHub 備份 | https://github.com/darwin7381/frp-tunnel (private) | | 本地 config | ~/.frp/frpc.toml | | 本地 tmux session | `frpc` | | VPS services | frps (systemd) + caddy (systemd) | ## 目前的 Tunnels | 名稱 | 本地 Port | URL | |------|----------|-----| | news-dashboard | 5173 | https://news.tunnel.fud.city | | oldweb | 8080 | https://oldweb.tunnel.fud.city | | api | 8000 | https://api.tunnel.fud.city | | fin-terminal | 5177 | https://terminal.tunnel.fud.city | | fin-terminal-api | 3002 | https://terminal-api.tunnel.fud.city | ## HTTPS 方式：Wildcard vs Per-Domain VPS 上的 Caddy 有兩種方式為 tunnel 提供 HTTPS。**目前使用 Wildcard（推薦）。** ### 方式 A：Wildcard Certificate（目前使用 ✅） **原理**：一張 `*.tunnel.fud.city` wildcard cert 涵蓋所有子域名。使用 DNS-01 challenge — Caddy 透過 Cloudflare API 自動加 TXT record 驗證。 **Caddyfile：** ```caddyfile *.tunnel.fud.city { tls { dns cloudflare {env.CF_API_TOKEN} } reverse_proxy localhost:8080 } ``` **需要：** - Caddy with Cloudflare DNS plugin（標準版沒有，需重新編譯或下載） - Cloudflare API Token（Zone:DNS:Edit 權限，只限 fud.city） - Token 設定在 `/etc/systemd/system/caddy.service.d/override.conf` **新增 tunnel 只需改本地 frpc.toml，不用動 VPS。** **安裝步驟（已完成，僅供紀錄）：** ```bash # 1. 下載帶 cloudflare plugin 的 Caddy curl -s "https://caddyserver.com/api/download?os=linux&arch=amd64&p=github.com%2Fcaddy-dns%2Fcloudflare" -o /usr/bin/caddy chmod +x /usr/bin/caddy # 2. 驗證 module 存在 caddy list-modules | grep cloudflare # 3. 設定 CF token mkdir -p /etc/systemd/system/caddy.service.d cat > /etc/systemd/system/caddy.service.d/override.conf << EOF [Service] Environment="CF_API_TOKEN=你的token" EOF # 4. 更新 Caddyfile cat > /etc/caddy/Caddyfile << EOF *.tunnel.fud.city { tls { dns cloudflare {env.CF_API_TOKEN} } reverse_proxy localhost:8080 } EOF # 5. 重啟 systemctl daemon-reload systemctl restart caddy ``` ### 方式 B：Per-Domain Certificate（備用） **原理**：每個子域名獨立簽一張 cert。使用 HTTP-01 challenge — Let's Encrypt 訪問 `http://xxx/.well-known/acme-challenge/` 驗證。 **Caddyfile：** ```caddyfile news.tunnel.fud.city { reverse_proxy localhost:8080 } terminal.tunnel.fud.city { reverse_proxy localhost:8080 } # 每加一個 tunnel 就加一段 ``` **需要**：標準版 Caddy 即可，不需要 API token。 **缺點**：每次加 tunnel 都要 SSH 進 VPS 改 Caddyfile 再 reload Caddy。 **適合場景**： - 不想給 Cloudflare API token - 需要不同子域名指向不同 upstream（不只是 frp） - 臨時測試 ### 如何切換 **Wildcard → Per-Domain：** ```bash ssh root@5.223.75.160 # 刪除 override.conf（或保留也無妨） # 把 Caddyfile 改成逐一列出每個域名 systemctl restart caddy ``` **Per-Domain → Wildcard：** 按上面方式 A 的安裝步驟。 ## 日常操作 ### 啟動 frpc（Mac 重啟後需要） ```bash SOCK=/tmp/openclaw-tmux/openclaw.sock tmux -S $SOCK new-session -d -s frpc 'frpc -c ~/.frp/frpc.toml' ``` ### 檢查 frpc 狀態 ```bash SOCK=/tmp/openclaw-tmux/openclaw.sock tmux -S $SOCK capture-pane -t frpc -p -S -10 ``` ### 加新 tunnel（Wildcard 模式 — 只需改本地） **Step 1 — 本地 config** 編輯 `~/.frp/frpc.toml`，加一段： ```toml [[proxies]] name = "new-project" type = "http" localPort = 3000 customDomains = ["new.tunnel.fud.city"] ``` **Step 2 — 重啟本地 frpc** ```bash SOCK=/tmp/openclaw-tmux/openclaw.sock tmux -S $SOCK send-keys -t frpc C-c sleep 2 tmux -S $SOCK send-keys -t frpc 'frpc -c ~/.frp/frpc.toml' Enter ``` **Step 3 — 驗證**（不需要動 VPS！Wildcard cert 自動涵蓋） ```bash curl -sI https://new.tunnel.fud.city | head -5 ``` 確認回傳 `HTTP/2 200` 才告訴用戶。 **Step 4 — 同步 GitHub 備份** ```bash cd ~/Projects/frp-tunnel cp ~/.frp/frpc.toml ./frpc.toml git add -A && git commit -m "Add tunnel: new-project" && git push ``` ### 加新 tunnel（Per-Domain 模式 — 需改 VPS） 同上 Step 1-2，但在 Step 2 和 3 之間多一步： ```bash ssh root@5.223.75.160 # 編輯 /etc/caddy/Caddyfile，加： # new.tunnel.fud.city { # reverse_proxy localhost:8080 # } systemctl reload caddy # 等 30 秒讓 cert 簽發 ``` ### 前端 Vite 的 allowedHosts Vite dev server 預設會拒絕非 localhost 的 Host header。加 tunnel 時前端 `vite.config.ts` 也要加： ```ts server: { allowedHosts: ['xxx.tunnel.fud.city'], } ``` ### 臨時開一條（不改 config） ```bash frpc http --server-addr 5.223.75.160:7000 --local-port 3000 --custom-domain temp.tunnel.fud.city ``` Wildcard 模式下，臨時 tunnel 也自動有 HTTPS。 ## 發送 URL 前必做的檢查（SOP） **每次**發送 tunnel URL 給用戶前： 1. **確認 frpc 在跑** — `tmux capture-pane -t frpc` 2. **確認 proxy 成功** — 看到 `start proxy success` 4. **確認回傳 200** — 不是 502/404/連線失敗 5. **確認內容正確** — `curl -s https://xxx.tunnel.fud.city | grep "<title>"`，title 必須是預期的網站名稱 6. **確認本地 server 是活的，不是僵屍進程** — `ps aux | grep <port>` 檢查進程啟動時間，如果是幾天前啟動的舊進程，很可能已經僵屍化（佔 port 但不正常服務，偶爾回 200 但頁面空白）。殺掉重啟。 7. **瀏覽器驗證時禁止信任 cache** — 用無痕模式或 `curl --resolve` 強制走 VPS IP，不要用已開過的 tab ### ⚠️ 已知 Failure Mode：僵屍進程（2026-03-04 事故） 本地 dev server（如 Vite）長時間不重啟會變成僵屍：佔著 port、偶爾回 HTTP 200，但實際頁面是空白。 curl 檢查看似正常，瀏覽器有 cache 也看似正常，但外部用戶（包含手機）打開是空白。 **排查步驟**： 1. `ps aux | grep <port>` — 看進程啟動時間，超過 1-2 天就該懷疑 2. 殺掉舊進程，在 tmux 裡重新 `npm run dev` 3. 重啟後再 curl + grep title 確認 ## VPS 故障排除 ```bash # SSH 進 VPS ssh root@5.223.75.160 # 檢查 frps systemctl status frps journalctl -u frps --no-pager -n 20 # 檢查 Caddy systemctl status caddy journalctl -u caddy --no-pager -n 20 # 檢查 Caddy modules（確認有 cloudflare） caddy list-modules | grep cloudflare # 檢查 cert 狀態 caddy cert list 2>/dev/null || journalctl -u caddy | grep "certificate obtained" # 重啟 systemctl restart frps systemctl restart caddy ``` ## 防火牆規則（VPS ufw） | Port | 用途 | |------|------| | 22 | SSH | | 80 | HTTP (Caddy + Let's Encrypt) | | 443 | HTTPS (Caddy) | | 7000 | frp client 連線 | | 7500 | frp Web Dashboard | ## 費用 - VPS: $7.59/月（Hetzner CPX12 Singapore + IPv4） - 域名: 已有 fud.city - SSL: 免費（Let's Encrypt via Caddy） - 流量: 0.5 TB/月（實際用量約 10-15 GB/月）

path: setup-guide.md
content: # frp-tunnel 從零建置指南

以 Hetzner Cloud + Cloudflare DNS 為例，從開 VPS 到第一條 tunnel 通。

---

## 概覽

完成後你會有：
- 一台 Hetzner VPS 跑 frps + Caddy
- `*.your-domain.com` wildcard 子域名，自動 HTTPS
- 本地 `frpc` 隨時開 tunnel，不用再動 VPS

預計時間：30-60 分鐘（含等 DNS 生效）

---

## Step 1：註冊 Hetzner Cloud

1. 前往 https://www.hetzner.com/cloud/
2. 註冊帳號（需信用卡或 PayPal）
3. 建立一個 Project（例如 `tunnel`）

### 選機型

| 項目 | 建議 |
|------|------|
| 地區 | 離你最近的（Singapore / Ashburn / Falkenstein） |
| 機型 | **CPX12**（2 vCPU, 2 GB RAM, 40 GB SSD, ~$7.59/月） |
| OS | **Ubuntu 24.04** |
| IPv4 | ✅ 勾選（需要公網 IP） |

> CPX12 是 ARM 架構（Ampere），性價比最高。如果你需要 x86 選 CX22。

---

## Step 2：SSH Key 設定

### 方案 A：讓 AI 幫你操作 VPS（推薦）

讓 AI（OpenClaw / Claude Code）能 SSH 進 VPS 執行指令。

**在你的 Mac 上生成 SSH key：**

```bash
# 生成專用 key（不設 passphrase，AI 才能自動連線）
ssh-keygen -t ed25519 -f ~/.ssh/frp-vps -N "" -C "openclaw-frp-tunnel"
```

**在 Hetzner Console 加入 public key：**

1. 進 Hetzner Cloud Console → Security → SSH Keys
2. 點 Add SSH Key
3. 貼上 `~/.ssh/frp-vps.pub` 的內容
4. 命名為 `openclaw`

**建 VPS 時選這把 key。**

建好後測試：

```bash
ssh -i ~/.ssh/frp-vps root@<VPS_IP>
```

**設定 SSH config 簡化連線（可選但推薦）：**

```bash
cat >> ~/.ssh/config << 'EOF'

Host frp-vps
    HostName <VPS_IP>
    User root
    IdentityFile ~/.ssh/frp-vps
EOF
```

之後 `ssh frp-vps` 就能連。

### 方案 B：自己操作 VPS

如果你不想給 AI SSH 存取，可以自己 SSH 進去，把下面每一步的指令手動貼進去跑。

---

## Step 3：VPS 基礎設定

SSH 進 VPS 後：

```bash
# 更新系統
apt update && apt upgrade -y

# 設定防火牆
ufw allow 22/tcp    # SSH
ufw allow 80/tcp    # HTTP (Let's Encrypt + redirect)
ufw allow 443/tcp   # HTTPS (Caddy)
ufw allow 7000/tcp  # frp client 連線
ufw allow 7500/tcp  # frp Dashboard（可選，不需要可不開）
ufw --force enable
ufw status
```

---

## Step 4：安裝 frps（frp server）

```bash
# 查最新版本：https://github.com/fatedier/frp/releases
FRP_VERSION="0.62.1"

# 判斷架構
ARCH=$(uname -m)
if [ "$ARCH" = "aarch64" ]; then
    FRP_ARCH="arm64"
elif [ "$ARCH" = "x86_64" ]; then
    FRP_ARCH="amd64"
fi

# 下載並安裝
cd /tmp
wget https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz
tar xzf frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz
cp frp_${FRP_VERSION}_linux_${FRP_ARCH}/frps /usr/bin/frps
chmod +x /usr/bin/frps
```

### 建立 frps 設定檔

```bash
mkdir -p /etc/frp

cat > /etc/frp/frps.toml << 'EOF'
bindPort = 7000
vhostHTTPPort = 8080

# Dashboard（可選）
webServer.addr = "0.0.0.0"
webServer.port = 7500
webServer.user = "admin"
webServer.[REDACTED_SECRET]"
EOF
```

> `vhostHTTPPort = 8080`：frps 在 8080 接收所有 HTTP 流量，Caddy 會把 HTTPS 流量反向代理到這裡。

### 建立 systemd service

```bash
cat > /etc/systemd/system/frps.service << 'EOF'
[Unit]
Description=frps server
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable frps
systemctl start frps
systemctl status frps
```

---

## Step 5：設定 DNS（Cloudflare）

1. 登入 Cloudflare Dashboard → 選你的域名
2. DNS → Add Record：

| Type | Name | Content | Proxy |
|------|------|---------|-------|
| A | `*.tunnel` | `<VPS_IP>` | ❌ DNS only（灰雲） |

> **必須關 Proxy（灰雲）**，不然 Caddy 的 HTTPS 會跟 Cloudflare 的衝突。

3. 等 DNS 生效（通常 1-5 分鐘）：

```bash
dig +short anything.tunnel.your-domain.com
# 應該回傳你的 VPS IP
```

---

## Step 6：安裝 Caddy（帶 Cloudflare DNS plugin）

標準版 Caddy 不支援 DNS-01 challenge（wildcard cert 需要）。需要帶 Cloudflare plugin 的版本：

```bash
# 下載帶 cloudflare plugin 的 Caddy（自動偵測架構）
ARCH=$(uname -m)
if [ "$ARCH" = "aarch64" ]; then
    CADDY_ARCH="arm64"
elif [ "$ARCH" = "x86_64" ]; then
    CADDY_ARCH="amd64"
fi

curl -sL "https://caddyserver.com/api/download?os=linux&arch=${CADDY_ARCH}&p=github.com%2Fcaddy-dns%2Fcloudflare" -o /usr/bin/caddy
chmod +x /usr/bin/caddy

# 驗證 cloudflare module 存在
caddy list-modules | grep cloudflare
# 應看到：dns.providers.cloudflare
```

### 建立 Cloudflare API Token

1. Cloudflare Dashboard → My Profile → API Tokens → Create Token
2. 選 **Custom Token**：
   - Permissions: `Zone > DNS > Edit`
   - Zone Resources: `Include > Specific zone > your-domain.com`
3. 建立後複製 token

### 設定 Caddy

```bash
# 設定 Cloudflare API Token
mkdir -p /etc/systemd/system/caddy.service.d
cat > /etc/systemd/system/caddy.service.d/override.conf << EOF
[Service]
Environment="CF_API_TOKEN=貼上你的token"
EOF

# Caddyfile
mkdir -p /etc/caddy
cat > /etc/caddy/Caddyfile << 'EOF'
*.tunnel.your-domain.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    reverse_proxy localhost:8080
}
EOF
```

> 把 `your-domain.com` 換成你的域名。

### 建立 Caddy systemd service

```bash
# 建立 caddy user（如果不存在）
groupadd --system caddy 2>/dev/null
useradd --system --gid caddy --create-home --home-dir /var/lib/caddy --shell /usr/sbin/nologin caddy 2>/dev/null

cat > /etc/systemd/system/caddy.service << 'EOF'
[Unit]
Description=Caddy
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile
TimeoutStopSec=5s
LimitNOFILE=1048576
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable caddy
systemctl start caddy

# 確認 cert 簽發成功（等 10-30 秒）
journalctl -u caddy --no-pager -n 20 | grep -i "certificate obtained"
```

---

## Step 7：本地安裝 frpc（Mac）

```bash
# macOS (Homebrew)
brew install frp

# 或手動下載
# https://github.com/fatedier/frp/releases → darwin_arm64 (Apple Silicon) / darwin_amd64 (Intel)
```

### 建立本地設定檔

```bash
mkdir -p ~/.frp

cat > ~/.frp/frpc.toml << 'EOF'
serverAddr = "<VPS_IP>"
serverPort = 7000

[[proxies]]
name = "my-first-tunnel"
type = "http"
localPort = 3000
customDomains = ["app.tunnel.your-domain.com"]
EOF
```

> 把 `<VPS_IP>` 和 `your-domain.com` 換成你的。`localPort` 改成你本地 dev server 的 port。

---

## Step 8：啟動並驗證

### 啟動本地 frpc

```bash
# 用 tmux 持久化（推薦）
SOCK=/tmp/openclaw-tmux/openclaw.sock
tmux -S $SOCK new-session -d -s frpc 'frpc -c ~/.frp/frpc.toml'

# 或直接跑（關 terminal 就斷）
frpc -c ~/.frp/frpc.toml
```

### 驗證

```bash
# 1. 確認 frpc 連上 server
tmux -S $SOCK capture-pane -t frpc -p -S -5
# 應看到：start proxy success

# 2. 確認本地 server 在跑（例如 port 3000）
curl -s http://localhost:3000 | head -5

# 3. 確認 tunnel 通
curl -sI https://app.tunnel.your-domain.com | head -5
# 應看到：HTTP/2 200
```

🎉 完成！你現在有一條穩定的 HTTPS tunnel。

---

## 之後加新 tunnel

只需要改本地 `~/.frp/frpc.toml`，加一段 `[[proxies]]`，重啟 frpc。不用動 VPS。

詳見主文件 `SKILL.md` 的「加新 tunnel」段落。

---

## 費用總結

| 項目 | 費用 |
|------|------|
| Hetzner CPX12 | ~$7.59/月 |
| 域名 | 你已有的域名（或 ~$10/年） |
| SSL | 免費（Let's Encrypt via Caddy） |
| Cloudflare DNS | 免費 |
| **合計** | **~$7.59/月** |


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)