SSH 反向隧道:让服务器通过客户端代理访问互联网

很多服务器不能直接访问互联网。常见原因不是机器坏了,而是它处在内网、受出口策略限制,或者云上环境没有配置公网出口。

这时很多人的第一反应是:给服务器装代理、改防火墙、开公网出口、加 NAT 网关。

这些办法都可以,但它们改变的是服务器所在网络的出口结构。权限不够时,往往做不了;临时排障时,也太重。

还有一种更轻的办法:如果你的客户端机器已经能访问互联网,并且本地已经有一个代理端口,比如 127.0.0.1:10808,就可以用 SSH 反向端口转发,把这个代理能力挂到服务器上。服务器访问自己的 127.0.0.1:18080,实际流量会穿过 SSH 隧道,转到客户端的 127.0.0.1:10808

先假设有这样一组配置:

服务器地址:203.0.113.10
服务器 SSH 端口:2222
服务器 SSH 用户:ops
客户端本地代理地址:127.0.0.1
客户端本地代理端口:10808
服务器侧代理入口:127.0.0.1:18080

基于这组配置,核心命令如下:

ssh -p 2222 \
    -R 18080:127.0.0.1:10808 \
    ops@203.0.113.10

这条命令的重点在 -R

这条命令真正做了什么

先看完整结构:

ssh -p 2222 \
    -R 18080:127.0.0.1:10808 \
    ops@203.0.113.10

含义是:

  • -p 2222:连接服务器 203.0.113.10 的 SSH 端口 2222
  • ops@203.0.113.10:以用户 ops 登录服务器
  • -R 18080:127.0.0.1:10808:在服务器侧监听 18080,并把连接转发到客户端侧的 127.0.0.1:10808

连接建立后,链路变成这样:

服务器上的程序
    -> 127.0.0.1:18080
        -> SSH 反向隧道
            -> 客户端 127.0.0.1:10808
                -> 客户端本地代理
                    -> 互联网

注意,这不是让客户端访问服务器的端口,而是让服务器借用客户端的端口。

更准确地说,-R 的格式是:

-R [远端监听地址:]远端端口:本地地址:本地端口

所以:

-R 18080:127.0.0.1:10808

可以理解成:

服务器 127.0.0.1:18080 -> 客户端 127.0.0.1:10808

SSH 只负责转发 TCP 连接,并不关心里面跑的是 HTTP 代理、SOCKS5 代理,还是别的协议。只要客户端的 127.0.0.1:10808 上确实有一个可用代理,服务器就能通过这个隧道使用它。

为什么这个方式容易被忽略

大部分人熟悉的是正向端口转发:

ssh -L 本地端口:目标地址:目标端口 user@server

它解决的是“我的电脑怎么访问远端内网服务”。

比如把服务器内网里的 MySQL 映射到本机,这很常见。

反向端口转发刚好反过来:

ssh -R 远端端口:本地地址:本地端口 user@server

它解决的是“远端服务器怎么访问我本机能访问的服务”。

这个方向一换,价值就变了。它不再只是调试端口,而是可以临时改变服务器的出口路径。

最小可用配置

假设你的客户端已经有一个本地代理:

127.0.0.1:10808

这个代理可能是 SOCKS5,也可能是 HTTP。先确认它在客户端可用。

如果是 SOCKS5 代理,可以在客户端测试:

curl --socks5-hostname 127.0.0.1:10808 https://ifconfig.me

如果是 HTTP 代理,可以测试:

curl -x http://127.0.0.1:10808 https://ifconfig.me

确认客户端代理可用后,再从客户端连接服务器:

ssh -p 2222 \
    -R 18080:127.0.0.1:10808 \
    ops@203.0.113.10

登录服务器后,在服务器上测试:

curl --socks5-hostname 127.0.0.1:18080 https://ifconfig.me

如果你的本地代理是 HTTP 代理,就用:

curl -x http://127.0.0.1:18080 https://ifconfig.me

如果能返回公网 IP,说明服务器已经通过客户端代理访问互联网。

更适合长期挂着的写法

如果只是临时验证,前面的命令就够了。

如果只是建立隧道,不需要登录 shell,可以加上 -N

ssh -N -p 2222 \
    -R 18080:127.0.0.1:10808 \
    ops@203.0.113.10

为了避免隧道建立失败但 SSH 仍然连上,可以加:

ssh -N -p 2222 \
    -o ExitOnForwardFailure=yes \
    -R 18080:127.0.0.1:10808 \
    ops@203.0.113.10

为了让连接更容易发现断线,可以加 keepalive:

ssh -N -p 2222 \
    -o ExitOnForwardFailure=yes \
    -o ServerAliveInterval=30 \
    -o ServerAliveCountMax=3 \
    -R 18080:127.0.0.1:10808 \
    ops@203.0.113.10

如果要长期运行,可以再配合 autossh 或 systemd user service。这里先不展开。核心不是守护进程,而是先确认这条链路本身成立。

让命令行工具使用这个代理

隧道建立后,服务器上会出现一个本地代理入口:

127.0.0.1:18080

如果客户端的 10808 是 SOCKS5 代理,服务器上的命令可以通过服务器侧的 18080 入口走代理:

curl --socks5-hostname 127.0.0.1:18080 https://github.com

也可以临时设置环境变量:

export ALL_PROXY=socks5h://127.0.0.1:18080
export HTTP_PROXY=socks5h://127.0.0.1:18080
export HTTPS_PROXY=socks5h://127.0.0.1:18080

socks5hh 很重要。它表示域名解析也交给代理端完成。否则服务器如果本身 DNS 不通,仍然会失败。

如果客户端的 10808 是 HTTP 代理,则在服务器上使用:

export HTTP_PROXY=http://127.0.0.1:18080
export HTTPS_PROXY=http://127.0.0.1:18080

Git 也可以单独配置:

git config --global http.proxy socks5h://127.0.0.1:18080
git config --global https.proxy socks5h://127.0.0.1:18080

用完后取消:

git config --global --unset http.proxy
git config --global --unset https.proxy

常见问题

服务器上访问 127.0.0.1:18080 失败

先确认 SSH 命令还在运行。反向端口转发依赖这条 SSH 连接,连接断了,服务器上的 18080 也就没了。

再检查服务器上端口是否存在:

ss -lntp | grep 18080

如果端口没有监听,可能是远端 SSH 服务不允许 TCP 转发。服务器的 sshd_config 里需要允许:

AllowTcpForwarding yes

修改后需要重载或重启 sshd。

远端端口被占用

如果服务器上已经有程序占用了 18080,可以换一个远端端口:

ssh -N -p 2222 \
    -R 28080:127.0.0.1:10808 \
    ops@203.0.113.10

这时服务器使用:

curl --socks5-hostname 127.0.0.1:28080 https://ifconfig.me

链路变成:

服务器 127.0.0.1:28080 -> 客户端 127.0.0.1:10808

curl 可以,git 或 npm 不行

这通常不是隧道问题,而是工具没有使用代理,或者代理协议写错了。

先用 curl 确认最小链路:

curl -v --socks5-hostname 127.0.0.1:18080 https://github.com

再分别配置具体工具。不要一开始就从复杂工具排查。

DNS 仍然失败

如果使用 SOCKS5,优先使用 socks5h://--socks5-hostname。这会把域名解析交给代理侧。

如果写成 socks5://,很多工具会先在服务器本地解析域名。服务器 DNS 不通时,代理还没开始工作,请求已经失败。

其他机器能不能访问服务器上的 18080

默认情况下,OpenSSH 的反向端口通常只监听服务器的 loopback 地址,也就是服务器自己访问:

127.0.0.1:18080

这正是推荐方式。它只把代理暴露给这台服务器自己,风险较小。

如果希望服务器所在网络的其他机器也访问这个反向代理,需要显式绑定地址,并且服务器 SSH 配置还要允许 GatewayPorts。例如:

ssh -N -p 2222 \
    -R 0.0.0.0:18080:127.0.0.1:10808 \
    ops@203.0.113.10

这会扩大暴露面。除非你非常清楚访问控制边界,否则不建议这么做。

安全边界

这类隧道很方便,但它本质上是在让服务器借用客户端的网络出口。

需要注意几点:

  • 不要把远端监听地址开放到 0.0.0.0,除非你已经做好访问控制
  • 不要在多人共享服务器上随便暴露代理端口
  • 不要把这个方式当成长期网络架构,长期使用应该有正式出口、审计和权限边界
  • 不要忽略客户端代理的访问规则,服务器发出的请求最终会从客户端代理出口出去

它最适合的场景是临时排障、临时下载依赖、访问受限 API、在无公网出口的服务器上做一次可控操作。

这件事的关键变化

通常我们以为服务器访问互联网,出口一定在服务器所在网络里。

SSH 反向隧道改变的是这个控制点:服务器不直接拥有出口,但它可以通过一条已经建立的 SSH 连接,借用客户端的出口能力。

这不是代理软件的新功能,而是 SSH 端口转发方向带来的系统变化。

正向转发解决“客户端进入远端网络”。

反向转发解决“远端服务器借用客户端能力”。

只要理解这个方向,很多临时网络问题就不用从防火墙、NAT、网关开始改。先挂一条 SSH 反向隧道,确认请求链路能不能跑通,再决定是否需要正式改网络架构。