基于 SSH Tunnel 打造自动化全链路代理网络
在终端中使用 AI 工具(如 OpenCode CLI)时,由于网络原因,我们经常需要配置代理。然而,OpenCode 底层的运行时对长连接、流式传输(Streaming)以及代理协议有极为严苛的要求。如果你直接使用 ssh -D 建立的 SOCKS5 代理,或者在环境变量中误加了 http:// 前缀,就会频繁遭遇以下底层报错:
Cannot connect to API: The socket connection was closed unexpectedly
UnsupportedProxyProtocol fetching "https://moonshot.cn"
本文将带你一步步排查这些深坑,并利用 Linux 的 Systemd 将 SSH 动态转发与 HTTP-to-SOCKS 转换器打造成一个高可用、全自动、开箱即用的系统级服务链。
🛠️ 第一部分:痛点分析与核心避坑原理
1. 为什么 SSH 代理容易闪断?
OpenCode 与 AI 接口通信采用的是 HTTP Chunked Stream(流式传输)。当模型在思考(如 DeepSeek-R1 或复杂 Tool Call)时,通道会有数十秒没有任何数据。默认的 SSH 连接没有保活机制,中间的路由器或防火墙会判定连接超时,直接发送 RST 包断开 Socket。
2. 为什么会报 UnsupportedProxyProtocol?
OpenCode 底层依赖的特定运行时,其内置的 fetch() 代理逻辑不接受带协议头的环境变量(例如 http://127.0.0.1:8080 会被判定为非法协议)。正确的做法是只提供纯主机名与端口(127.0.0.1:8080)。
3. 网络拓扑设计
为了完美的兼容性,我们需要搭建如下的转发链路:
OpenCode CLI
│ (环境变量: 127.0.0.1:8080)
▼
[本地 8080 端口] ── (http-proxy-to-socks 转换)
▼
[本地 1080 端口] ── (SSH -D 动态转发通道)
▼
[远程 SSH 服务器] ── (直连) ──> AI API (如 Moonshot / OpenAI)
🚀 第二部分:全链路自动化服务配置指南
前提条件
在开始前,请确保你已经配置好了 SSH 免密公钥登录。在终端执行 ssh user@remote_server_ip 时不需要手动输入密码。
步骤一:创建基础 SSH 常驻服务
首先,我们将普通的 SSH 动态转发命令打造成一个具备自动心跳保活、死服自动重启的 Systemd 系统服务。
创建服务文件:
sudo vim /etc/systemd/system/ssh-proxy.service
粘贴以下配置(请根据注释将 your_username 等信息替换为你本机的真实信息):
[Unit]
Description=SSH SOCKS5 Proxy Service
After=network.target
[Service]
Type=simple
# 替换为你本机的真实 Linux 用户名
User=your_username
# -N: 不执行远程命令; -D: 开启 SOCKS5 端口; ServerAliveInterval: 每15秒发送心跳防止断连
ExecStart=/usr/bin/ssh -N -D 1080 -o ServerAliveInterval=15 -o StrictHostKeyChecking=no -i /home/your_username/.ssh/id_rsa user@remote_server_ip
# 核心:网络波动断开后,5秒后无限自动重连
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
步骤二:安装并创建 HTTP 协议转换服务
为了让不支持 SOCKS5 协议或对协议头敏感的工具完美运行,我们需要将 SOCKS5 转换为纯 HTTP 代理。
全局安装转换工具(避免在 Systemd 中使用不稳定的 npx):
sudo npm install -g http-proxy-to-socks
检查安装路径:
which hpts
# 假设输出为 /usr/local/bin/hpts
创建转换服务文件:
sudo vim /etc/systemd/system/http-to-socks.service
粘贴以下配置,利用 Requires 和 After 声明服务依赖,让两个服务形成捆绑:
[Unit]
Description=HTTP to SOCKS Proxy Converter
# 强依赖:声明必须在 SSH 代理服务启动成功后,本服务才启动
After=ssh-proxy.service
Requires=ssh-proxy.service
[Service]
Type=simple
User=your_username
# 将本地 1080 的 SOCKS5 代理桥接到本地 8080 的 HTTP 代理
ExecStart=/usr/local/bin/hpts -s 127.0.0.1:1080 -p 8080
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
步骤三:一键激活服务生态链
得益于 Systemd 的依赖机制,你现在只需要启动最上层的 http-to-socks 服务,系统就会自动拉起底层的 SSH 代理。
# 刷新 Systemd 守护进程配置
sudo systemctl daemon-reload
# 允许开机自启并立即运行 HTTP 转换服务
sudo systemctl enable --now http-to-socks
# 检查运行状态(看到绿色 active (running) 即代表全链路打通)
sudo systemctl status http-to-socks
💻 第三部分:终端环境配置与测试
现在,底层的代理网络已经默默在后台守候。我们最后需要让 OpenCode CLI 认得这条路径。
打开你的终端配置文件(如 ~/.bashrc 或 ~/.zshrc):
vim ~/.bashrc
在文件末尾追加以下环境变量。注意:绝对不要在 HTTPS_PROXY 里加 http:// 前缀!
# 规避 UnsupportedProxyProtocol 报错的正确写法
export HTTPS_PROXY=127.0.0.1:8080
export HTTP_PROXY=127.0.0.1:8080
# 必须为本地 TUI 通信设置绕过,防止路由回环
export NO_PROXY=localhost,127.0.0.1
# 针对大模型思考耗时,延长本地客户端超时容错(单位:毫秒,此处为10分钟)
export API_TIMEOUT_MS=600000
保存退出,并刷新终端:
source ~/.bashrc
接下来,直接在终端里输入 opencode 启动。你会发现,无论是流式打字输出,还是长时间的复杂推理,都变得如丝般顺滑,再也不会弹出恼人的 Socket 关闭报错。
🔍 第四部分:日常维护与故障排查
查看链路是否正常通畅:
curl -I --proxy 127.0.0.1:8080 https://moonshot.cn
如果返回了 API 服务的 HTTP 状态码(如 401 或 405 均可,只要不是 Connection Refused),说明代理完全成功。
查看 SSH 是否断线或在重连:
sudo journalctl -u ssh-proxy -f
彻底关闭整个代理集群:
sudo systemctl stop http-to-socks
通过这套方案,我们不仅解决了当前工具的报错,还为本地终端搭建了一个极其稳健的代理基础设施,后续任何对网络环境挑剔的 CLI 工具,都可以直接复用这个 127.0.0.1:8080 端口。