用 SSH 给自己开一条代理通道:把本地代理反向借给远程服务器
摘要
远程服务器上 pip install、git clone、huggingface-cli download 全都卡住,装个代理客户端却发现节点全部超时——而本地电脑上的代理明明好得很。本文从这次真实排查讲起,用 SSH 反向端口转发(ssh -R) 把本地代理”借”给服务器,让服务器的出口 IP 变成你本地的出口 IP。文中的 IP、端口、耗时数据都来自一次真实排查,可以直接对照复现。
一、从一个真实的尴尬问题说起
场景很典型:
- 你有一台本地电脑,上面跑着代理软件(Clash / mihomo / sing-box 之类),本地 HTTP 代理端口
7897,上网一切正常。 - 你还有一台远程服务器(云主机、公司跳板机、超算容器、实验室工作站)。你在上面跑
pip install、git clone、huggingface-cli download,全都卡住。 - 于是你在服务器上也装了个代理客户端,配了订阅,结果节点全部超时。
排查时的输出大概长这样:
1 | $ curl -s -m 12 -x http://127.0.0.1:7890 https://pypi.org |
ICMP 能通,TCP 443 不通。而同一时刻,服务器直连 github.com:443、pypi.org:443 都正常。
也就是说:
- 不是服务器没网,普通 HTTPS 出网没问题;
- 不是代理软件坏了,配置校验通过、端口正常监听;
- 是这台服务器到那几个代理节点 IP 的 TCP 连接被挡住了(机房出网策略、云厂商防火墙、节点侧白名单,都可能)。
这类情况在共享集群、IDC 机房、企业内网里极其常见。你甚至可能在本地用得好好的同一个节点,在服务器上就是连不上。
更扎心的一点
很多人第一反应是:那我在服务器上再买个节点/再换个机场。可你会发现:
- 换一个节点,可能又连不上;
- 就算连上了,还要在服务器上长期维护一份订阅和配置文件;
- 订阅到期、节点被墙、IP 被拉黑……这些烂事你本地已经踩过一遍,不想在服务器上再踩第二遍。
而你本地明明已经有一条活的、出口干净的代理链路。
那问题就变成了:
能不能把「本地电脑上的代理」借给远程服务器用?
答案是:能,而且只用一条 SSH 命令。原理就是 SSH 反向端口转发(Reverse Port Forwarding)。
二、先建立直觉:SSH 隧道不是「VPN」,它是「管子」
在讲 -L / -R / -D 之前,先纠正一个常见误解。
很多人以为 SSH 隧道是「把网络整根搬过去」,其实更准确的比喻是:
SSH 会在客户端和服务器之间拉一根「虚拟的网线」。你可以指定这根网线的两头分别插在哪个 IP:端口 上。
这根虚拟网线的学名叫 SSH channel(会话通道),它是纯 TCP 字节流——你在里面塞什么协议,SSH 完全不管。
- 塞 HTTP → 就是网页流量;
- 塞 TLS → 就是 HTTPS;
- 塞 MySQL 协议 → 就是数据库流量;
- 塞 SOCKS5 → 就成了一个 SOCKS 代理;
- 塞
HTTP CONNECT→ 就成了一个 HTTP 代理。
这一点极其关键,因为它解释了后面所有的「魔法」:代理协议只是跑在隧道里的货物,SSH 只负责运货。
一根网线的两端:
1 | [A端: IP:端口] ============================== [B端: IP:端口] |
- 如果 A 端在服务器、B 端在你本地,就是
-R(反向转发)——本文的主角; - 如果 A 端在你本地、B 端在服务器,就是
-L(本地转发)。
三、-L 本地转发:把「服务器的服务」搬到你本地
先讲最好理解的 -L,它是 -R 的镜像。
命令
1 | ssh -L 6379:127.0.0.1:6379 user@server |
翻译成人话:在你本地开一个 127.0.0.1:6379,谁连它,就经由 SSH 隧道,让服务器去连 127.0.0.1:6379。
注意这两个 6379 含义不同:
1 | -L {本地监听地址}:{本地监听端口}:{从服务器视角看的目标地址}:{目标端口} |
数据是怎么走的
假设服务器上跑着一个只监听 127.0.0.1:6379 的 Redis(外网根本连不到),你在本地执行了上面的命令,然后用本地 redis-cli -p 6379 去连:
1 | 本地 redis-cli |
关键点
- 一个 SSH 连接可以复用出无数条隧道。②里没有新开网络连接,只是在已有连接里开了一个”子通道”。所以哪怕防火墙只放行 22 端口,你也能同时穿十个服务过去。
- 「目标地址」是站在服务器视角解析的。写
127.0.0.1:6379就是”服务器自己的 6379”;写10.0.0.5:3306就是”服务器内网那台 10.0.0.5 的 3306”。这是-L最有价值的地方:访问服务器内网里的私有服务。 -L的监听默认只绑在本地127.0.0.1,别人连不到,比较安全。
「把服务器当跳板访问第三台机器」
1 | # 通过 server 访问它内网里的 10.0.0.5:80 |
顺便:-L 也能直接当一个简易 SOCKS 代理
这就是 -D(动态转发):
1 | ssh -D 1080 user@server |
-D 的特别之处:目标地址不再是写死的,而是由 SOCKS5 协议在每次请求里动态告诉 ssh。所以它是”通用代理”,能把任意目的地都从服务器出口发出去。这在”借用服务器网络出口”的场景里非常常见。
但要注意本文的场景是反向:我们想借本地的网。-D 只能绑本地,帮不上忙;这时候就得请 -R 出场。
四、-R 反向转发:把本地代理反向借出去(本文核心)
4.1 命令
1 | # 在【本地机】执行,注意仍然是连服务器,只是加了 -R |
翻译成人话:
在【服务器】上开一个
127.0.0.1:7897,谁连它,就经由 SSH 隧道,让【本地机】去连127.0.0.1:7897。
再看一次参数结构,这次是反的:
1 | -R {服务器上的监听地址}:{服务器上的监听端口}:{从本地机视角看的目标地址}:{目标端口} |
而本地机上的 127.0.0.1:7897 正好就是你的 Clash/mihomo 的 HTTP 代理端口。
于是达成了这个效果:
1 | ┌──────────────────────────── 你的本地电脑 ────────────────────────────┐ |
4.2 一次请求,七步旅程
还是用上面那张图,标出每一步到底发生了什么。假设服务器上执行 curl -x http://127.0.0.1:7897 https://pypi.org:
| 步骤 | 发生位置 | 发生了什么 |
|---|---|---|
| ① | 服务器 | curl 建立到 127.0.0.1:7897 的 TCP 连接。这个监听是 sshd 提供的(-R 的功劳) |
| ② | 服务器 | curl 发出 HTTP 代理请求:CONNECT pypi.org:443 HTTP/1.1 + 认证头(如果有) |
| ③ | 服务器 sshd | 把这段字节塞进 SSH 连接的 channel,通过真正的 TCP 连接(22 端口)送回本地 |
| ④ | 本地 ssh 客户端 | 收到 channel 数据,按 -R 的约定,去连本地 127.0.0.1:7897,然后把字节原样吐出;两个方向同时搬运 |
| ⑤ | 本地 Clash | 收到 CONNECT pypi.org:443,这才真正开始干活:按自己的规则选节点、加密、发出 |
| ⑥ | 本地 Clash | 回 HTTP/1.1 200 Connection established,之后就是纯粹的 TLS 双向字节流 |
| ⑦ | 回到服务器 | curl 收到 200,开始 TLS 握手;此后所有 TLS 记录都在”curl ↔ sshd ↔ SSH ↔ 本地 ssh ↔ Clash ↔ 节点 ↔ pypi”这条链上跑 |
关键洞察:
服务器从头到尾没有直接连过 pypi.org,也没有直接连过任何代理节点。
它只连了127.0.0.1——一个自己会跳到本地机的回环地址。
所有”出网”这件事,都发生在你本地电脑上。
这就是为什么:
- 服务器到节点 IP 的 443 被封?无所谓,服务器根本不碰那些 IP。
- 服务器 HTTPS 出网基本被封也能救(因为只需要 22 端口连本地/跳板)。
- 节点、订阅、规则全部由本地维护,服务器上零代理配置。
4.3 为什么「代理协议」能跑在「纯字节流隧道」上
这是新手最容易犯迷糊的地方,值得单独说清楚。
SSH 隧道不解析里面的内容,它只是一根可以双向搬运字节的管子。而 HTTP 代理协议(尤其是 CONNECT 方法)恰好是为”在一条普通 TCP 连接上承载另一条 TCP 连接”而设计的。
所以两者天然契合:
1 | 管子里的货物 = HTTP 代理协议 = 请求头 "CONNECT host:port" + 之后的裸字节流 |
更妙的是:代理协议是无状态的、逐请求的,所以只要 SSH 隧道双方把字节对齐地搬运,多个并发请求、长连接、WebSocket、SSE 流式响应,全都能正常工作。你不需要为每种协议做特殊适配。
4.4 这个方案的本质:一条”会呼吸的 TCP-over-TCP”
必须提醒一个物理事实:
1 | 应用数据 |
TCP 套 TCP(TCP-over-TCP)在丢包严重时会有”熔断”效应:内层 TCP 的重传被外层 TCP 的重传掩盖,内层超时、外层也超时,雪球越滚越大,表现为”越卡越慢、最后卡死”。
实践中的对策:
- 别在这条链上跑大流量长传。拿来做
pip install、git clone、API 调用、huggingface小文件下载,体验很好;拿它当日常视频/大模型权重通道,就会难受。 - 如果确实要跑大流量,优先考虑:本地开 SOCKS5 用
-R(同一条管子,但少一层 HTTP 语义),或者上 WireGuard / Tailscale(UDP,无 TCP-over-TCP 问题)。 - 保持隧道链路质量:
ServerAliveInterval保活,尽量少跳板。
顺便给出本文案例的实测数据(服务器 → 本地 → 洛杉矶出口):
| 目标 | 结果 |
|---|---|
https://api.ipify.org |
200 / 1.22s |
https://ipinfo.io/json |
200 / 1.70s |
https://pypi.org |
200 / 1.63s |
https://github.com |
200 / 3.00s |
https://huggingface.co |
200 / 2.76s |
https://www.google.com |
200 / 2.71s |
(对比:直连节点时代是 5 秒超时 + HTTP 000。延迟确实比本地用高,因为多了”服务器→你本地”这一跳,但”能用”和”不能用”是质的差别。)
五、动手:从零到能跑
5.1 最简版(一条命令)
本地机执行:
1 | ssh -N -R 7897:127.0.0.1:7897 user@server |
-N:不要给我 shell,只做转发(省一个交互终端)- 这个终端要一直开着,按
Ctrl+C就断隧道
服务器上验证:
1 | curl -x http://127.0.0.1:7897 https://api.ipify.org |
这一条命令是判断”通没通”的黄金标准:出口 IP 变了 = 隧道成了。
5.2 工程化版(写进 ~/.ssh/config)
长期用就别手敲了。本地 ~/.ssh/config:
1 | Host qz |
以后 ssh qz 就自带隧道。要”只开隧道”:
1 | ssh -N qz |
后台常驻(本地):
1 | ssh -f -N qz # -f 转后台 |
5.3 服务器侧最好也做”自适应”
隧道依赖你本地机器在线。你本地一关机,127.0.0.1:7897 就没人监听了。
如果服务器上的 http_proxy 环境变量还傻乎乎指着 7897,那所有请求都会卡到超时——这比没有代理更糟糕(本文开头那个”每个请求都卡 5 秒”就是这么来的)。
所以服务器侧建议做成”隧道在就用,不在就直连”:
1 | # ~/.bashrc |
/dev/tcp/127.0.0.1/7897 是 bash 内建能力,几十毫秒就能判断端口在不在,不需要 nc。
5.4 给特定工具单独指定(不想污染全局 env)
1 | pip install xxx --proxy http://127.0.0.1:7897 |
六、踩坑清单(都是血泪)
| 坑 | 现象 | 原因 / 解法 |
|---|---|---|
忘了 -N |
命令执行后卡在那儿,像死机 | 没有 -N 就会开个 shell,转发仍在但终端被占用。加 -N 或 -f |
忘了 ExitOnForwardFailure |
连接成功,但服务器上端口没开 | 端口被占用/被禁时 ssh 默认只警告不退。加 -o ExitOnForwardFailure=yes |
| 两个客户端抢同一端口 | 后连的那个转发失败 | 典型场景:VS Code Remote 和裸 ssh 同时连,都 RemoteForward 7897。改成不同端口,或只保留一个 |
服务器只允许绑 127.0.0.1 |
想从容器内/其他机器访问却不行 | sshd 的 GatewayPorts no(默认)只允许绑回环。要对外开放需 GatewayPorts yes(有安全风险,慎用) |
| 管理员禁用了转发 | Administratively prohibited |
sshd 的 AllowTcpForwarding no。无解,换思路(见第七节) |
| 端口选错方向 | 以为 -R 7897:127.0.0.1:7897 里左边是本地 |
记死:-R 左边是远端(服务器),右边是”本地机视角”的目标 |
| 本地代理监听地址不匹配 | 隧道通了但代理连不上 | 隧道右侧目标写的是”本地机视角”,127.0.0.1:7897 就对;若代理只监听局域网 IP,就写那个 IP |
| 代理端口搞混 | 明明隧道在,curl -x 报 000 |
本地可能有多个端口:7890 HTTP、7891 SOCKS、7897 混拨(mixed)。确认你转的是真实在监听的那个 |
no_proxy 没设 |
连 127.0.0.1 的服务也走代理,报错 |
一定要 no_proxy=127.0.0.1,localhost,::1 |
| 死代理污染环境变量 | 每个请求都卡 5 秒 | 见 5.3:做存活探测,隧道不在就 unset |
| shell 里定义了同名函数互相覆盖 | proxy_on 行为跟你写的代码不一致 |
系统 /etc/profile.d/*.sh 可能在你 ~/.bashrc 之后加载并覆盖同名函数。要改就两处一起改(type proxy_on 查来源) |
| 长连接/SSE 突然断 | 流式接口中途停 | 中间设备空闲超时。用 ServerAliveInterval,或在应用层加心跳 |
七、和别的方案比一比:什么时候不该用 SSH 隧道
| 方案 | 原理 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
ssh -R(本文) |
复用 SSH 连接,反向暴露端口 | 零额外软件(服务器侧)、加密、按需、一根连接可复用多条隧道 | 依赖本地机在线;TCP-over-TCP;一个客户端端口冲突会打架 | 应急、长期轻量使用、已有 SSH 通道的场景 |
ssh -D / -L |
正向转发 | 同上,且能借服务器出口 | 方向相反,解决不了”借本地网”的问题 | 借服务器网、访问内网私有服务 |
| frp / ngrok / cloudflared | 独立反向代理进程 + 公网中继 | 不依赖本地 SSH、可多端、可做域名/鉴权 | 需要在中继侧部署或付费;多一个常驻进程;暴露面更大 | 长期稳定的远程接入 |
| Tailscale / WireGuard / ZeroTier | 虚拟二层/三层组网 | UDP 传输、无 TCP-over-TCP、点对点直连、延迟低 | 需要装客户端、要过 NAT 打洞、权限模型更复杂 | 需要整网互通、大流量传输 |
| 服务器直接跑代理客户端 | 服务器自己连节点 | 不依赖本地机 | 节点被封就抓瞎(本文开头就是)、订阅要维护 | 服务器出网没限制时才可行 |
一句话总结:
ssh -R是成本最低、见效最快的”借网”手段;需要 7×24 稳定、大带宽时,再升级到 frp / WireGuard。
八、安全与合规提醒
用隧道本身是中性技术,但请务必注意:
RemoteForward默认只绑回环,这是好事。不要为了”方便别人用”随手开GatewayPorts yes——那等于把你本地代理暴露给整个服务器网段。- 代理端口要加认证。如果你的本地代理是”免密开放”的(很多 Clash 默认如此),一旦隧道被同机器上的其他用户/容器访问到,别人就能白嫖你的出口,甚至你的账号。建议给代理开用户名密码,或用 iptables 限制来源。
- 遵守服务器/机房的网络使用规定。企业内网、学校集群通常有明确的外网访问政策,绕过管控可能有合规风险。本文的技术讨论以”在允许的环境中解决可达性问题”为前提。
- 别把私钥和订阅链接贴到公开的地方。本文示例里的 IP、token 都为演示,实际使用时请自行替换。
九、30 秒排错流程
隧道”看起来起了但用不了”时,按这个顺序敲:
1 | # 0) 本地:隧道进程在吗?(本地终端) |
其中 第 4 步是唯一可信的判据。前面的步骤只证明”管子接上了”,只有出口 IP 变了才说明”货真的运到了”。
十、总结
把整篇文章压缩成几句话:
- SSH 隧道就是一根能指定两头的虚拟网线,里面跑什么协议它不管。
-L把远端的服务搬到你本地;-R把你本地的服务搬到远端;-D是-L的动态版(SOCKS)。- 要把本地代理借给服务器用,就用
-R 端口:127.0.0.1:端口——服务器上从此多了一个”出网口”,而那个口子的真实出口在你家里。 - 判断成功与否不看日志,看出口 IP。
- 生产使用要配三件事:
ExitOnForwardFailure(别假装成功)、ServerAliveInterval(别被掐断)、服务器侧存活探测(别让死代理拖慢一切)。 - 它是救急和轻量长期方案的最佳选择;要大流量、要 7×24,请升级到 WireGuard / frp。
最后回到开头那个场景:你在服务器上敲 pip install,数据包其实绕了一圈——从机房出发,穿过 SSH 的一根管子,落到你的笔记本上,再从你的家庭宽带出去,访问世界,然后原路返回。
技术上它有点”暴力”,但在”服务器出网被限制”这类让人抓狂的时刻,这一根管子就是最优雅的解法。
附:命令速查
1 | # 反向:把本地 7897 借给服务器 |



