# 一行命令 Bootstrap 新设备

> **理念**：人类只做最少特权操作，buddy agent 接管后续。

## 概述

这套方法论解决了一个经典问题：如何让一台全新的设备快速接入 Agent 生态，同时把人类需要手动操作的步骤压缩到最低？

核心思路：

1. 人类在新设备上执行一条 bootstrap 命令
2. 脚本自动安装必要组件，建立公网 SSH 隧道
3. 客户端 buddy agent 通过隧道 SSH 进入新设备
4. buddy agent 完成剩余所有配置

**人类只需要：** 运行一行命令，然后把隧道地址告诉 agent。

脚本仓库：[https://github.com/shazhou-ww/oc-bootstrap](https://github.com/shazhou-ww/oc-bootstrap)

---

## 流程图

```mermaid
sequenceDiagram
    participant H as 人类（新设备）
    participant S as Bootstrap 脚本
    participant CF as Cloudflare Quick Tunnel
    participant A as Buddy Agent（客户端）

    H->>S: bash <(curl -fsSL <script_url>)
    S->>S: 安装依赖（cloudflared、OpenClaw 等）
    S->>CF: cloudflared tunnel --url tcp://localhost:22
    CF-->>S: 返回隧道域名 xxx.trycloudflare.com
    S->>H: 显示隧道地址，等待 agent 接管
    H->>A: 把隧道地址发给 agent
    A->>CF: cloudflared access tcp --hostname xxx.trycloudflare.com --url localhost:2222
    A->>A: ssh -p 2222 user@localhost
    A->>A: 执行剩余配置（复制公钥、配置 OpenClaw 等）
```

---

## 快速上手

### 新设备端（人类操作）

在新机器的终端执行：

```bash
bash <(curl -fsSL https://raw.githubusercontent.com/shazhou-ww/oc-bootstrap/main/bootstrap.sh)
```

脚本会：

1. 检测系统类型（macOS / Ubuntu / Debian）
2. 安装 `cloudflared`（如未安装）
3. 启动 Quick Tunnel 暴露本地 22 端口
4. 在终端打印隧道地址，格式类似：

```
✅ Tunnel ready: https://abc-def-ghi.trycloudflare.com
   Tell your agent: ssh -o ProxyCommand="cloudflared access tcp --hostname abc-def-ghi.trycloudflare.com --url localhost:2222" user@localhost
```

5. 脚本保持前台运行，**不要关闭终端**，等待 agent 接管完成

### 客户端 Buddy Agent 操作

收到隧道地址后，agent 执行：

```bash
# 建立本地代理（后台运行）
cloudflared access tcp --hostname abc-def-ghi.trycloudflare.com --url localhost:2222 &

# SSH 进入新设备
ssh -p 2222 username@localhost
```

进入后完成剩余配置：复制 SSH 公钥、安装 OpenClaw、配置 Telegram Bot Token 等。

### 验收标准

Bootstrap 完成的标志：

- [ ] 可以通过正式 SSH 公钥直接登录新设备（无需密码）
- [ ] OpenClaw 已安装并启动
- [ ] Agent 可以通过 Telegram 收到新设备的心跳
- [ ] 关闭 Quick Tunnel 后，通过正式方式（Tailscale / 固定 SSH）仍可访问

---

## 技术方案

### Bootstrap 脚本执行方式

```bash
bash <(curl -fsSL https://raw.githubusercontent.com/shazhou-ww/oc-bootstrap/main/bootstrap.sh)
```

!!! warning "为什么不用 `curl | bash`"
    `curl | bash` 会把 curl 的 stdout 接到 bash 的 stdin，导致脚本内的 `read` 命令无法从终端读取用户输入。即使加 `< /dev/tty` 重定向，在 macOS 上也不稳定。

    **正确做法**：用 `bash <(curl ...)` 进程替换（Process Substitution），脚本的 stdin 仍然连接到终端。

    原理对比：
    ```
    # curl | bash（错误）
    curl 的 stdout ──→ bash 的 stdin
    终端 stdin ──→ 被占用，read 无法读取
    
    # bash <(curl ...) （正确）
    curl 的 stdout ──→ 命名管道 /dev/fd/63 ──→ bash 读取脚本内容
    终端 stdin ──→ 正常连接，read 可以读取
    ```

### 建立公网 SSH 隧道

新设备上通过 Cloudflare Quick Tunnel 暴露 SSH 端口：

```bash
cloudflared tunnel --url tcp://localhost:22 --protocol http2
```

命令执行后，cloudflared 会输出一个类似 `https://xxx-yyy-zzz.trycloudflare.com` 的临时域名。

!!! info "`--protocol http2` 参数"
    较新版本的 cloudflared 已默认使用 http2 协议，此参数可省略。如果你的 VPN/代理环境下遇到连接问题，可以显式加上确保不走 QUIC。

!!! info "Quick Tunnel 的优势"
    - **零依赖**：不需要 Cloudflare 账号、不需要域名、不需要 Named Tunnel 配置
    - **零持久化**：隧道随进程启动/销毁，不留痕迹
    - **http2 协议**：比默认的 QUIC/UDP 更稳定，避免被某些 VPN 劫持

!!! note "隧道地址提取技巧"
    cloudflared 的隧道地址输出在 stderr，可以用以下方式提取：
    ```bash
    cloudflared tunnel --url tcp://localhost:22 --protocol http2 2>&1 | \
      grep -o 'https://[a-z0-9-]*\.trycloudflare\.com'
    ```

### 客户端连接隧道

Buddy agent 在自己的机器上建立本地代理，再 SSH 连入：

```bash
# 第一步：建立本地 TCP 代理（后台运行）
cloudflared access tcp --hostname xxx-yyy-zzz.trycloudflare.com --url localhost:2222 &

# 第二步：SSH 到本地代理端口
ssh -p 2222 user@localhost
```

两步也可以合并为 SSH ProxyCommand：

```bash
ssh -o ProxyCommand="cloudflared access tcp --hostname %h --url localhost:2222" \
    -p 22 user@xxx-yyy-zzz.trycloudflare.com
```

### Bootstrap 脚本的工作内容

脚本按顺序执行以下操作：

| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 检测 OS 类型 | macOS/Ubuntu/Debian，选择对应包管理器 |
| 2 | 安装 cloudflared | brew / apt / 直接下载二进制 |
| 3 | 确认 SSH 服务运行 | macOS 需要手动开启"远程登录"（系统设置 → 通用 → 共享 → 远程登录），Linux 通常已启动 |
| 4 | 启动 Quick Tunnel | `cloudflared tunnel --url tcp://localhost:22 --protocol http2` |
| 5 | 提取并显示隧道地址 | 解析 cloudflared stderr 输出 |
| 6 | 等待 agent 接管 | 保持前台运行，Ctrl+C 可中断 |

---

## 踩过的坑

### 1. `curl | bash` 导致 `read` 失效

**问题**：`curl https://... | bash` 模式下，脚本中的交互式 `read` 命令无法读取用户输入，因为 stdin 已被 curl 的输出占据。

**尝试过的修复**（均不可靠）：
- `read var < /dev/tty`：在 Linux 可能有效，但在 macOS 不稳定
- 提前关闭 stdin：无法从脚本内部做到

**正确方案**：`bash <(curl -fsSL ...)` 进程替换，脚本拿到真正的 tty stdin。

### 2. Quick Tunnel 必须用 `tcp://` 而非 `ssh://`

**问题**：`cloudflared tunnel --url ssh://localhost:22` 命令会失败或无法正常工作。

**原因**：Quick Tunnel 的 `--url` 参数期望 HTTP/HTTPS/TCP URL，SSH 协议需要通过 TCP 隧道承载。

**正确命令**：
```bash
cloudflared tunnel --url tcp://localhost:22 --protocol http2
```

### 3. `cloudflared access ssh` vs `cloudflared access tcp`

| 命令 | 要求 |
|------|------|
| `cloudflared access ssh` | 需要在 Cloudflare Zero Trust Dashboard 配置 SSH Application，绑定域名 |
| `cloudflared access tcp` | **无需任何配置**，直接代理 TCP 流量 |

Quick Tunnel 场景用 `cloudflared access tcp`，不需要任何 Zero Trust 配置。

### 4. Surge VPN 劫持 cloudflared 流量

**问题**：在开启 Surge 等 VPN/代理软件的 macOS 上，cloudflared 的 QUIC/UDP 流量可能被劫持，导致隧道无法建立或连接不稳定。

**解决方案**：

1. 在 Surge 配置中添加 bypass 规则：
   ```
   DOMAIN-SUFFIX,argotunnel.com,DIRECT
   DOMAIN-SUFFIX,cloudflare.com,DIRECT
   DOMAIN-SUFFIX,trycloudflare.com,DIRECT
   ```

2. 或者强制 cloudflared 使用 http2（TCP）而非 QUIC：
   ```bash
   cloudflared tunnel --url tcp://localhost:22 --protocol http2
   ```

### 5. Named Tunnel 需要账号 + 域名

Quick Tunnel（`cloudflared tunnel --url`）和 Named Tunnel（`cloudflared tunnel create`）是两种不同模式：

| 特性 | Quick Tunnel | Named Tunnel |
|------|-------------|--------------|
| 需要 CF 账号 | ❌ 不需要 | ✅ 需要 |
| 需要域名 | ❌ 不需要 | ✅ 需要 |
| 隧道持久化 | ❌ 临时 | ✅ 持久 |
| 适用场景 | 临时访问、bootstrap | 长期服务暴露 |

Bootstrap 场景优先用 Quick Tunnel。

### 6. cert.pem 是 Cloudflare Origin Cert

执行 `cloudflared login` 或某些 Named Tunnel 操作后，cloudflared 会在 `~/.cloudflared/cert.pem` 存放凭证文件。

**注意**：这个 `cert.pem` 是 Cloudflare 专有的 Origin Certificate 格式，**不是**标准 X.509 证书。不能用 `openssl x509 -in cert.pem -text` 提取域名，命令会报错或输出乱码。这个文件只供 cloudflared 自身使用。

### 7. macOS 默认可能禁用密码 SSH

**问题**：部分 macOS 版本默认只允许公钥认证，不允许密码登录。Bootstrap 脚本依赖密码 SSH 进行首次连接。

**解决方案**：

1. 确认 `/etc/ssh/sshd_config` 中 `PasswordAuthentication` 设置（macOS 可能在 `/etc/ssh/sshd_config.d/` 下有覆盖文件，以最后生效的为准）
2. 或者：人类在 bootstrap 前先把 buddy agent 的公钥手动加入 `~/.ssh/authorized_keys`
3. 最可靠：脚本运行时提示人类输入 buddy agent 的公钥，自动写入 authorized_keys，完全绕过密码认证

---

## 安全考虑

### 隧道本身的安全性

- **域名随机**：Quick Tunnel 的域名由 Cloudflare 随机生成（类似 `abc-def-ghi.trycloudflare.com`），无法被猜测或枚举
- **临时性**：隧道与 cloudflared 进程绑定，进程结束隧道立即失效，不留持久入口
- **流量加密**：cloudflared 到 Cloudflare 边缘节点之间全程 TLS 加密

### 脚本安全最佳实践

- 脚本中**不应硬编码**任何密钥、API Token 或密码
- 敏感信息（如 OpenClaw Token）应通过**交互式输入**（`read -s`）或**环境变量**传递
- 脚本公开托管在 GitHub，任何人都可以审计内容

### 最小化暴露时间

- Bootstrap 完成后，立即 `Ctrl+C` 终止 cloudflared 进程
- 改用正式的、持久化的访问方式（Tailscale、固定公网 IP + 防火墙等）
- 不要让 Quick Tunnel 长期运行，它是临时引导工具，不是生产访问方式

---

## 从 Quick Tunnel 毕业到 Named Tunnel

Bootstrap 阶段用 Quick Tunnel 引导新设备，但**长期运行应切换到 Named Tunnel**。Quick Tunnel 的域名随进程随机生成，进程一旦退出隧道即失效，不适合作为持久访问入口。

### Named Tunnel 的优势

| 特性 | Quick Tunnel | Named Tunnel |
|------|-------------|-------------|
| 域名固定 | ❌ 每次随机 | ✅ 绑定你的域名 |
| 可配置自启 | ❌ 手动运行 | ✅ cloudflared 作为系统服务 |
| 不依赖进程存活 | ❌ 进程死则隧道断 | ✅ 服务化后自动重启 |
| 需要 CF 账号 + 域名 | ❌ 不需要 | ✅ 需要 |

### 切换步骤概要

```bash
# 1. 登录 Cloudflare（生成 ~/.cloudflared/cert.pem）
cloudflared login

# 2. 创建 Named Tunnel
cloudflared tunnel create <tunnel-name>

# 3. 编写配置文件
cat > ~/.cloudflared/config.yml << 'EOF'
tunnel: <tunnel-id>
credentials-file: /home/<user>/.cloudflared/<tunnel-id>.json

ingress:
  - hostname: ssh.yourdomain.com
    service: tcp://localhost:22
  - service: http_status:404
EOF

# 4. 在 Cloudflare DNS 添加 CNAME 记录
# ssh.yourdomain.com  CNAME  <tunnel-id>.cfargotunnel.com

# 5. 安装为系统服务（可选，实现开机自启）
sudo cloudflared service install
```

### SSH 场景示例

切换到 Named Tunnel 后，在客户端的 `~/.ssh/config` 中配置 ProxyCommand，即可像普通 SSH 一样连接：

```ssh-config
Host my-server
    HostName ssh.yourdomain.com
    User youruser
    ProxyCommand cloudflared access tcp --hostname %h --url localhost:%p
```

之后直接 `ssh my-server` 即可，无需手动启动代理进程。

### SSH 认证加固

- Bootstrap 过程依赖密码 SSH，完成后应：
    1. 将 agent 的 SSH 公钥写入 `~/.ssh/authorized_keys`
    2. 修改 `/etc/ssh/sshd_config`，禁用密码登录：`PasswordAuthentication no`
    3. 重启 sshd：`sudo systemctl restart sshd`

---

## 参考

- 脚本仓库：[shazhou-ww/oc-bootstrap](https://github.com/shazhou-ww/oc-bootstrap)
- [Cloudflare Tunnel 文档](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)
- [cloudflared GitHub](https://github.com/cloudflare/cloudflared)
- [Process Substitution - Bash 手册](https://www.gnu.org/software/bash/manual/bash.html#Process-Substitution)
