为 iTerm2 Buddy 自托管中继服务器,加速手机连接 Mac 终端速度

Wait 5 sec.

iTerm2 Buddy 是著名的开源 macOS 终端工具 iTerm2 发布的 iPhone 应用,它能在异地通过 iOS 连接回 macOS 上已经打开的 iTerm2 标签页,实时同步两端操作,效果大概是这个样子:公共中继服务器默认情况下,iTerm2 Buddy 使用 iTerm2 的公共中继服务器,来帮助用户在手机上连接回电脑:relay1.iterm2.comrelay2.iterm2.com所以,速度很慢。青小蛙的感受是,在外面通过 5G 连回家里的 iTerm2,从打开手机上的 iTerm2 Buddy 算起,需要经过连接、加密两个步骤,最少都需要5秒钟,才能打开终端。真的很慢,慢到不行。自托管中继服务器于是,我想,能不能自托管中继服务器,这样就可以不用在地球上绕一圈,才连回家。甚至在局域网中,iTerm2 Buddy 也需要连中继服务器…还好,中继服务器也是开源的:iterm2-companion-relay,原理是 Mac 和手机分别主动建立一条 WebSocket 连接,中继把两条连接接起来,转发加密数据,中继服务器看不到内容。连接时,用 Ed25519 签名验证配对身份,用 Apple App Attest 验证手机上的应用。配对记录保存在 SQLite 数据库中,重启中继后仍然有效。说干就干:我把想法交给了 AI,是的,现在怎么能自己动手呢。必备条件不过你需要给 AI 提供:公网IP一台 Linux 服务器(可以是群晖等NAS)权限一个域名(用了创建 TLS 证书 )然后就不管了,我也不知道(没看)AI 怎么操作的,反正就建好了。最后,它会让你回到 iTerm2 所在的 macOS 上,运行两条命令:再使用 iTerm2 Buddy 扫码配对,就好了。再次连回去,这次从家中连回家中,对比测试:这还测个啥,完全没有可比性啊 😂(结束)以下内容是 AI 帮我总结的搭建过程,如果需要可以自取:这套方案已经在 x86_64 群晖上构建运行,验证了证书、WebSocket 升级和 Mac 端握手。其他 NAS 架构尚未验证;实际 Buddy 连接是否变快,需要在自己的网络下比较。自建 Relay 也不会给 Buddy 增加新建 iTerm2 标签页等客户端功能。准备条件群晖已安装 Container Manager,并支持 Docker Compose v2。能通过 SSH 登录群晖,使用具备 Docker 和证书目录访问权限的账号。本文的 NAS 命令按 root 环境编写。一个可以修改 DNS 的域名,本文统一使用 relay.example.com,实际部署时全部替换成自己的域名。能从外部访问 NAS 的 TCP 18443,例如有公网 IP,并配置路由器端口转发。一张覆盖 Relay 域名、受 iOS/macOS 信任的 TLS 证书。Mac 安装支持 Companion 功能的 iTerm2,手机安装 iTerm2 Buddy。本次使用的 iTerm2 版本是 3.7.3。本文使用 /volume1/docker/iterm2-relay-nas 作为部署目录。若你的存储卷不同,修改后面所有对应路径。1. 建立目录,下载官方源码SSH 登录群晖后执行。先确认这个目录没有其他用途,再创建:export PATH=/usr/local/bin:/usr/bin:/bin:$PATHdocker --versiondocker compose versionmkdir -p /volume1/docker/iterm2-relay-nascd /volume1/docker/iterm2-relay-nasmkdir -p certschmod 700 certsgit clone https://github.com/gnachman/iterm2-companion-relay.git upstreamgit -C upstream checkout --detach f1c3371cdab8e7d361db9e8b439551f2fea1d95b这里固定到本次验证过的 commit,便于复现。NAS 没有 Git 的话,也可以在电脑上下载对应源码,再复制到 NAS 的 upstream 目录。2. 创建部署文件在部署目录下创建以下文件,注意文件名大小写。Dockerfile使用 Node 24 构建依赖。编译工具留在构建阶段,最终容器使用普通 node 用户运行。FROM node:24-bookworm-slim AS depsWORKDIR /appRUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/*COPY upstream/package.json upstream/package-lock.json ./RUN npm ci --omit=devFROM node:24-bookworm-slimENV NODE_ENV=productionWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY upstream/package.json ./COPY upstream/bin ./binCOPY upstream/host ./hostCOPY upstream/src ./srcRUN mkdir /data && chown node:node /dataUSER nodeCMD ["node", "bin/relay.js"]compose.yaml只把 Caddy 的 TCP 18443 发布到 NAS,Relay 的 8787 留在 Docker 网络内。配对数据放在命名卷里,重启服务不会删除。name: iterm2-relay-nasservices: relay: build: . restart: unless-stopped init: true environment: RELAY_HOST: 0.0.0.0 RELAY_PORT: "8787" RELAY_DB: /data/relay.db RELAY_ORIGIN: ${RELAY_ORIGIN:?Set the public HTTPS origin including port} ATTEST_REQUIRED: "true" APP_ID: H7V7XYVQ7D.com.googlecode.iterm2.companion APPATTEST_ENV: production RELAY_TRUST_PROXY: "true" RELAY_LOG: "false" RELAY_DAILY_BYTE_QUOTA: "8589934592" volumes: - relay-data:/data healthcheck: test: [CMD, node, -e, "fetch('http://127.0.0.1:8787/metrics').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 30s timeout: 5s retries: 3 start_period: 20s tls: image: caddy:2-alpine@sha256:d8542f48d34a9cf4e4c11a478865229840e87e4c96ea3f439101f31a5d35f75f restart: unless-stopped environment: RELAY_ORIGIN: ${RELAY_ORIGIN:?Set the public HTTPS origin including port} ports: - "18443:18443/tcp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ${RELAY_CERT_DIR:-./certs}:/certs:ro - caddy-data:/data - caddy-config:/config depends_on: relay: condition: service_healthyvolumes: relay-data: caddy-data: caddy-config:Caddy 使用本次验证过的镜像 digest。环境变量中的 App ID 对应官方 Buddy,App Attest 保持开启。Caddyfile加载外部证书,反向代理 Relay,同时阻止公网访问 /metrics。{ admin off auto_https off}{$RELAY_ORIGIN} { tls /certs/fullchain.pem /certs/privkey.pem @metrics path /metrics /metrics/* respond @metrics 404 reverse_proxy relay:8787 { header_up X-Forwarded-For {remote_host} header_up -CF-Connecting-IP } log { output discard }}.envRELAY_ORIGIN=https://relay.example.com:18443RELAY_CERT_DIR=./certsRELAY_ORIGIN 必须与 Mac 和手机实际连接的地址一致,包括 https://、域名和端口,末尾不要加 /。.dockerignore避免把证书、密钥和续期配置送进 Docker 构建上下文:upstream/.git**/node_modulescertsacmeacme-srcacme-state.env*.log*.zip*.tar.gz3. 准备 TLS 证书已有证书可以复用群晖现有证书,但必须确认它覆盖 relay.example.com,且仍在有效期内。证书签给了别的域名,不能直接拿来用。将证书链和私钥放到:/volume1/docker/iterm2-relay-nas/certs/fullchain.pem/volume1/docker/iterm2-relay-nas/certs/privkey.pem也可以将 .env 中的 RELAY_CERT_DIR 改成现有证书目录的绝对路径。目录内的文件名仍需为 fullchain.pem 和 privkey.pem。查看证书:openssl x509 -in certs/fullchain.pem -noout -subject -issuer -dates -ext subjectAltName如果使用了其他证书目录,对应修改这个检查路径。直连服务要使用客户端信任的证书;Cloudflare Origin CA 证书不适用于本教程的直连方式。没有证书:用 DNSPod 签发 Let’s EncryptDNS-01 通过添加 DNS TXT 记录验证域名控制权,不需要开放公网 80、443。这里以 DNSPod 为例,其他 DNS 服务商可参考 acme.sh DNS API 文档。以下是一套独立安装示例,不会依赖其他已有证书任务的目录。已经安装 acme.sh 的用户,也可以使用自己的安装路径,单独指定 --config-home。cd /volume1/docker/iterm2-relay-nasumask 077mkdir -p acme acme-statechmod 700 acme acme-stategit clone --depth 1 https://github.com/acmesh-official/acme.sh.git acme-src(cd acme-src && ./acme.sh --install \ --home /volume1/docker/iterm2-relay-nas/acme \ --config-home /volume1/docker/iterm2-relay-nas/acme-state \ --accountemail your-email@example.com \ --nocron)替换邮箱。这里使用 --nocron,后面在 DSM 中建立定时任务,不重复安装 cron。安装参数说明在 NAS 上设置自己的 DNSPod Token ID 和 Token,再申请证书:export DP_Id='你的 DNSPod Token ID'export DP_Key='你的 DNSPod Token'./acme/acme.sh --issue \ --home /volume1/docker/iterm2-relay-nas/acme \ --config-home /volume1/docker/iterm2-relay-nas/acme-state \ --server letsencrypt \ --dns dns_dp \ -d relay.example.com \ --keylength ec-256unset DP_Id DP_Key使用的是 DNSPod Token ID/Token 这套接口,不是把腾讯云 SecretId/SecretKey 填进去。acme.sh 会将续期所需的配置保存在 NAS 的 acme-state 目录。签发成功后,先创建 reload-tls.sh:#!/bin/shset -euexport PATH=/usr/local/bin:/usr/bin:/bin:$PATHcd /volume1/docker/iterm2-relay-nasif [ -n "$(docker compose ps -q tls)" ]; then docker compose restart tlsfi然后安装证书到 Caddy 挂载的目录,并登记续期后的重载命令:chmod 700 reload-tls.sh./acme/acme.sh --install-cert \ --home /volume1/docker/iterm2-relay-nas/acme \ --config-home /volume1/docker/iterm2-relay-nas/acme-state \ -d relay.example.com --ecc \ --key-file /volume1/docker/iterm2-relay-nas/certs/privkey.pem \ --fullchain-file /volume1/docker/iterm2-relay-nas/certs/fullchain.pem \ --reloadcmd /volume1/docker/iterm2-relay-nas/reload-tls.shchmod 600 certs/privkey.pem certs/fullchain.pem首次安装时 Caddy 还没启动,重载脚本会跳过重启;之后续期成功会重新加载证书。4. 配置 DNS 和端口转发将 relay.example.com 的 A 记录指向 NAS 所在网络的公网 IPv4。若公网地址会变化,需要有对应的 DDNS 更新机制。在路由器上设置:公网 TCP 18443 → NAS 内网 IP 的 TCP 18443有防火墙的话,也要允许这个端口。本教程只发布 TCP,不需要转发 UDP 18443。使用 Cloudflare 管理 DNS 时,本教程按 DNS-only 直连配置。如果添加 AAAA 记录,需要同时确认 IPv6 的端口和防火墙能正常访问,避免客户端选择了一条无法连接的路径。5. 构建并启动cd /volume1/docker/iterm2-relay-nasexport PATH=/usr/local/bin:/usr/bin:/bin:$PATHdocker compose config --quietdocker compose up -d --builddocker compose psdocker compose logs --tail=50 relay tls正常情况下,relay 显示 healthy,tls 显示运行中。首次构建需要下载 Node 镜像、安装依赖,耗时取决于 NAS 和网络。先从另一台电脑检查 HTTPS:curl -I --connect-timeout 10 https://relay.example.com:18443/根路径返回 HTTP 400 并不一定是异常:Relay 的普通请求也需要协议头。这里先确认 TLS 验证没有报错、能收到 HTTP 响应,不要求根路径返回 200。还可以检查 WebSocket 升级,在有 curl 和 openssl 的电脑上执行:probe_room=$(openssl rand -hex 32)curl --http1.1 -i -N --max-time 5 \ -H 'Connection: Upgrade' \ -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' \ -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \ -H "x-relay-room: $probe_room" \ https://relay.example.com:18443/收到 101 Switching Protocols 表示 WebSocket 升级成功。命令随后可能因 5 秒超时退出,因为这里没有继续发送客户端握手消息。这一步仍不等于真实 Buddy 配对成功。公网测试尽量从外部网络进行。NAS 所在局域网访问自己的公网地址,可能受路由器 NAT 回环限制影响。6. 修改 Mac 的 iTerm2 设置并重新配对先保存工作,确认没有需要继续运行的下载或任务,再完全退出 iTerm2。 然后在 macOS 自带「终端」中执行。如果之前改过这些设置,先记下原值,便于恢复:defaults read com.googlecode.iterm2 CompanionRelayOrigindefaults read com.googlecode.iterm2 CompanionResolverURL首次使用时提示找不到键是正常的。写入新地址:defaults write com.googlecode.iterm2 CompanionRelayOrigin -string 'https://relay.example.com:18443'defaults write com.googlecode.iterm2 CompanionResolverURL -string ''第二项设为空,让客户端使用这台固定 Relay,而不是默认的 Relay 解析服务。再执行两条 defaults read 检查:第一条应输出自己的 HTTPS 地址;第二条应为空。重新打开 iTerm2,在 iTerm2 → Companion Device Settings 中生成新二维码,在 Buddy 中扫描并确认配对码。已有配对会记住原来的 Relay,因此需要建立新配对。官方配对说明配对后分别测试 Wi-Fi 和蜂窝网络,确认终端可以访问;再测试网络切换和 Relay 重启后的重连。合成握手的耗时与 Buddy 打开到可用的耗时不是同一个指标。7. 设置自动续期如果使用了上面的独立 acme.sh 安装方式,在 DSM 的 控制面板 → 任务计划 中创建用户定义脚本:用户:root。周期:每天一次,例如 04:27。脚本:umask 077/volume1/docker/iterm2-relay-nas/acme/acme.sh --cron \ --home /volume1/docker/iterm2-relay-nas/acme \ --config-home /volume1/docker/iterm2-relay-nas/acme-state创建后手动运行一次,检查执行结果。每天运行只是检查是否需要续期,不会每天申请一张证书;成功续期后会调用之前登记的 reload-tls.sh。重启 Caddy 时连接会短暂中断,实际重连效果需要用自己的设备验证。如果复用的是 DSM 或其他工具管理的证书,应沿用原来的续期机制,并在更新后重载 Caddy,不要重复配置这套 acme.sh 任务。常见问题证书报错检查域名是否在证书 SAN 中、证书是否过期、fullchain.pem 是否包含完整链。用别的域名的证书会导致校验失败。容器正常,公网仍连不上检查域名解析、公网地址、端口转发和防火墙。relay healthy 只能证明 NAS 内部服务在运行,不能证明公网入口已打通。能连接,但配对失败先核对 .env 和 Mac 设置中的 RELAY_ORIGIN,域名、HTTPS、端口必须一致;确认修改后完全重启了 iTerm2,并重新扫码建立配对。真实 iOS App Attest 的验证也只有实际配对时才会完整经过。更新证书后客户端仍看到旧证书证书文件更新后执行:docker compose restart tls如果证书管理工具把整个挂载目录替换了,而不是更新目录中的文件,可以重新创建 TLS 容器以刷新挂载:docker compose up -d --force-recreate tls想恢复原来的 Relay先退出 iTerm2,恢复之前记下的两项设置。如果两项原本都没有设置,可以删除自定义键:defaults delete com.googlecode.iterm2 CompanionRelayOrigindefaults delete com.googlecode.iterm2 CompanionResolverURL重新打开 iTerm2,并按原来的 Relay 建立配对。升级和数据保留升级官方源码后,可以执行 docker compose up -d --build 重新构建。升级前先查看上游变更并备份配对数据库。配对数据库在 relay-data 命名卷里,备份时应考虑 SQLite 正在写入的情况。不要随手执行 docker compose down -v,它会删除卷里的配对数据,之后需要重新配对。这份教程是部署记录,并不是官方 Docker 镜像,也没有改动 Buddy 的功能。官方源码、协议和客户端后续可能变化,复现时可以先使用文中固定的版本。原文:https://www.appinn.com/iterm2-buddy-self-hosted-relay-server/相关阅读iTerm2 发布 iOS 客户端 iTerm2 Buddy,但它不是 iPhone 版终端macOS 开源终端 iTerm2 的进化:终于可以自己上网查资料了,新增浏览器与 AI 聊天功能Firefox Relay – 免费提供 5 个临时邮箱地址,用来转发邮件,扩展算半成品?Session Buddy – 帮你节省95%内存开销的 Chrome 扩展iTerm2 – 增强命令行终端 [Mac]©2021 青小蛙 for 小众软件 | 加入我们 | 投稿 | 订阅指南 3659b075e72a5b7b1b87ea74aa7932ff 点击这里留言、和原作者一起评论[ 点击前往获取链接 ]