法不净空,觉无性也。

Terminal Hole

2026.06.25

Expose your local terminal to a public IP for remote control from a browser.

把本地终端通过公网中继暴露给浏览器,让你离开电脑后仍能用手机操作本地的命令行工具(如 Claude Code、vim、tmux 等)。

工作原理

手机浏览器              公网 VPS                  本地电脑
┌──────────┐         ┌──────────┐            ┌──────────┐
│  xterm.js│  WSS    │          │  WSS out   │          │
│  Web UI  │◄───────►│  Relay   │◄───────────│  Agent   │
└──────────┘         └──────────┘            └────┬─────┘
                                              ┌───────┴──────┐
                                              │ tmux 会话们   │
                                              └──────────────┘
  • Agent(本地):管理 tmux 会话,提供 Web 终端,主动 WSS 连出到 Relay
  • Relay(公网 VPS):HTTPS 入口,认证浏览器,反向代理到 Agent
  • 浏览器:xterm.js + 原生 JS,多 tab 切换不同 tmux 会话

关键设计:tmux 是终端的"持久化层",会话独立于任何网络连接。浏览器关了、网络抖了,会话不丢;只有 Agent 进程死了会话才消失。

前置要求

  • Go 1.25+
  • tmux(macOS:brew install tmux,Linux:apt/dnf install tmux
  • 一台有公网 IP/域名的 VPS(用于跑 Relay)
  • 一个指向 VPS 的域名(用于 ACME 自动签证书)

安装

一键安装(Agent + Relay 通用,推荐)

curl -fsSL https://raw.githubusercontent.com/iannil/terminal-hole/main/scripts/install.sh | sh

装特定版本:

curl -fsSL https://raw.githubusercontent.com/iannil/terminal-hole/main/scripts/install.sh | sh -s -- --version v0.1.0

装到自定义路径:

curl -fsSL https://raw.githubusercontent.com/iannil/terminal-hole/main/scripts/install.sh | sh -s -- --install-dir /opt/bin

行为:

  • root 跑装到 /usr/local/bin,普通用户装到 ~/.local/bin(并提示加 PATH)
  • SHA256 校验 checksums.txt
  • 检测 tmux 缺失时打印安装命令(Relay 不需要 tmux,可忽略)

Docker(仅 Relay,推荐用于 VPS)

# 首次初始化配置(交互式,写入 ./config/config.yaml)
docker run --rm -it \
    -v "$PWD/config:/etc/terminal-hole" \
    ghcr.io/iannil/terminal-hole-relay:latest init --mode relay

# 启动 Relay
docker run -d --name terminal-hole-relay \
    -p 443:443 -p 80:80 \
    -v "$PWD/config:/etc/terminal-hole" \
    -v "$PWD/state:/var/lib/terminal-hole" \
    ghcr.io/iannil/terminal-hole-relay:latest

镜像 tag:v0.1.0(精确版本)、0.1(小版本滚动)、latest(最新)。

注意:

  • 必须把 DNS A/AAAA 记录指向 VPS 后再起容器(ACME 验签)
  • -v state 卷保留 ACME 证书缓存和 signer secret;丢了 → 浏览器 cookie 全失效,要重新登录

从源码构建(开发/贡献者)

git clone https://github.com/iannil/terminal-hole.git
cd terminal-hole
go build -o terminal-hole ./cmd/terminal-hole

需要 Go 1.25+。跑测试还需本机装 tmux。

部署

1. Relay(在 VPS 上)

# 交互式初始化(生成 /etc/terminal-hole/config.yaml)
sudo ./terminal-hole init --mode relay
# > 域名? tp.example.com
# > 管理员账号? alice
# > 管理员密码? 

# 启动(前台;生产可用 systemd 包装)
sudo ./terminal-hole relay

Relay 会:

  • :443 上跑 HTTPS,用 Let's Encrypt 自动签证书
  • :80 上跑 HTTP→HTTPS 重定向(可在 config 用 http_listen: "-" 禁用)
  • 接受 Agent 的 WSS 隧道连接(wss://tp.example.com/agent-tunnel
  • 持久化 HMAC signer secret 到 signer.key,重启后浏览器 cookie 不失效

DNS:先把域名 A/AAAA 记录指向 VPS,再启动 Relay(ACME 签证书时要验签)。

2. Agent(在本地电脑上)

# 交互式初始化(生成 ~/.config/terminal-hole/config.yaml)
./terminal-hole init
# > 选择密码存储方式:
#   1) 明文存 config(0600 权限)
#   2) password_file(自己创建文件,config 引用路径)
#   3) TH_AGENT_PASSWORD 环境变量(config 不存密码)
# > 中继地址? https://tp.example.com
# > 账号? alice
# > [根据选择] 输入密码 / 文件路径

# 启动
./terminal-hole agent

Agent 启动后会:

  • 0.0.0.0:7777 起本地 Web 终端(局域网设备也能访问,需账号密码)
  • 主动 WSS 连出到 Relay,注册自己
  • 指数退避重连(1s → 2s → 4s → ... → 封顶 30s)

日常使用

  1. 本地启动 Agent,让它跑着(不要登出系统,否则 Agent 进程会被杀)
  2. 锁屏离开
  3. 手机浏览器打开 https://tp.example.com,输入账号密码登录
  4. 看到 sidebar 会话清单(首次为空),点 "+" 新建终端
  5. 在终端里跑 claudevimhtop 等命令,跟本地操作一致
  6. 关浏览器/tab 不杀会话;下次登录点回原会话继续

手机键盘:界面底部有特殊键面板(Esc / Tab / Ctrl / Alt / 方向键 / / / - / Enter / Backspace)。Ctrl/Alt 是修饰键,点一下高亮,下次输入组合生效。仅在触摸设备显示。

多端共享:tmux 原生支持多客户端 attach,手机和电脑可同时看同一个会话。

Agent 掉线自动恢复:浏览器订阅 /api/agent-status SSE,Agent 重连后自动刷新会话清单并重 attach 当前会话。

配置参考

Agent (~/.config/terminal-hole/config.yaml)

agent:
  relay:
    url: https://tp.example.com
    username: alice
    password: "secret"            # 或 password_file / TH_AGENT_PASSWORD
  local:
    bind: 0.0.0.0:7777            # 默认值;改 127.0.0.1 可限制只 loopback 访问
    shell: /bin/zsh               # 默认从 $SHELL 推断
    tmux_prefix: th-              # Agent 管理的 tmux 会话前缀

Relay (/etc/terminal-hole/config.yaml)

relay:
  domain: tp.example.com
  listen: ":443"
  http_listen: ":80"              # 设为 "-" 禁用 HTTP 重定向
  acme_cache_dir: /var/lib/terminal-hole/certs
  signer_secret_file: /var/lib/terminal-hole/signer.key
  allowed_origins:                # WS origin allowlist;空则走同源策略
    - https://tp.example.com
  # 自带证书模式(与 domain/ACME 二选一):
  # cert_file: /path/fullchain.pem
  # key_file: /path/privkey.pem
accounts:
  - username: alice
    password_hash: "$2a$10$..."   # bcrypt,init 自动生成

配置优先级:CLI flags > 环境变量(TH_* 前缀)> config 文件 > 默认值

安全

  • 认证:username + password(bcrypt 哈希),浏览器登录用 HMAC 签名 Cookie
  • TLS:Relay 强制 HTTPS(ACME 自动签或自带证书),不支持纯 HTTP 部署
  • 限流:/api/login 5 次/IP/分钟,/agent-tunnel 10 次/IP/分钟,超限 429
  • 会话 ID 校验:仅允许字母数字+下划线+连字符,最长 256 字符
  • 错误消息:上游错误细节不暴露给客户端,仅记录到服务端日志
  • WS origin allowlist:可配置 relay.allowed_origins 做纵深防御 CSRF
  • 稳定 signer secret:Relay 重启后浏览器 cookie 不失效
  • 常量时间认证:登录失败时不区分"用户名错"和"密码错",防用户名枚举

已知限制(v1):

  • 单用户单 Agent(不支持多机/多用户)
  • Agent 密码可明文存 config(建议用 password_fileTH_AGENT_PASSWORD 替代)
  • 无审计日志、无文件传输、无 WebAuthn
  • Agent 前台运行,不自动重启(设计选择,简化使用模型)

子命令

terminal-hole init [--mode agent|relay] [--config PATH]   # 交互式生成 config
terminal-hole agent [--config PATH] [--bind ADDR]         # 跑 Agent
terminal-hole relay [--config PATH]                       # 跑 Relay
terminal-hole version                                     # 打印版本
terminal-hole help                                        # 帮助

状态与路线图

  • Plan 1:本地 MVP(Agent + 本地浏览器终端 + 多 tab + 手机键盘)
  • Plan 2:远程访问(Relay + WSS+yamux 隧道 + ACME)
  • Plan 3:体验打磨(稳定 signer、限流、SSE 状态推送、origin allowlist、ServeMux 模式)
  • Plan 4:分发(GoReleaser、install.sh、Docker、GitHub Actions)

详见 docs/design.md(完整设计)和 docs/superpowers/plans/(实现计划)。

开发

# 跑测试(需要本机装 tmux)
tmux kill-server 2>/dev/null; sleep 1
go test ./... -race

# 构建
go build -o terminal-hole ./cmd/terminal-hole

# 跑 vet
go vet ./...

项目结构:

cmd/terminal-hole/         入口
internal/agent/            Agent(管理 tmux、本地 HTTP、隧道客户端)
internal/relay/            Relay(HTTPS、登录、限流、SSE、代理)
internal/tunnel/           yamux 多路复用隧道
internal/tmux/             tmux wrapper
internal/pty/              creack/pty 封装
internal/auth/             bcrypt + HMAC cookie + 中间件
internal/config/           AgentConfig YAML
internal/protocol/         WS 控制消息
internal/web/              嵌入式静态资源 + 登录端点
internal/version/          ldflags 版本信息
internal/web/assets/       前端源码(HTML/CSS/JS + vendor xterm.js)