SSH隧道
1 背景
现有三台机器,网络关系如下:
| 机器 | 地址 | SSH 端口 | 说明 |
|---|---|---|---|
| 本地 PC | - | - | 开发机(Windows) |
| 服务器 | 10.0.206.124 | 32354 | 远程 Linux 服务器 |
| 台架 | 10.13.193.69 | 31936 | 测试台架,运行 SSH 服务 |
连通性:
- 本地 PC → 服务器:可以,
ssh -p 32354 root@10.0.206.124 - 本地 PC → 台架:可以,
ssh -p 31936 <台架用户名>@10.13.193.69 - 服务器 → 台架:不可以,服务器路由不到台架所在的网段
现在的需求是:在服务器上操作台架。台架不能被服务器直接访问,怎么办?
2 解决方案
核心思路:在本地 PC 上建立一条 SSH 远程转发隧道,把服务器的 54321 端口映射到台架的 31936 端口。之后在服务器上访问自己的 127.0.0.1:54321,流量就会经本地 PC 中转,最终到达台架。
1 | 服务器 10.0.206.124 本地 PC 台架 10.13.193.69 |
2.1 在本地 PC 上建立隧道
1 | ssh -N -o ServerAliveInterval=15 -o ServerAliveCountMax=8 -R 54321:10.13.193.69:31936 -p 32354 -i "C:\Users\username\.ssh\id_rsa_6v_a3" root@10.0.206.124 |
命令保持运行,隧道就保持有效。各参数作用如下:
| 参数 | 作用 |
|---|---|
-N |
不执行远程命令,只做端口转发 |
-o ServerAliveInterval=15 |
每 15 秒向服务器发送一次保活探测 |
-o ServerAliveCountMax=8 |
连续 8 次探测无响应就断开(约 120 秒) |
-R 54321:10.13.193.69:31936 |
让服务器监听 54321,流量经本地 PC 转发到台架的 31936 |
-p 32354 |
登录服务器的 SSH 端口 |
-i "C:\Users\username\.ssh\id_rsa_6v_a3" |
登录服务器使用的私钥文件 |
注意 -p 是小写,指定的是 SSH 端口;scp 中对应的参数是大写 -P。私钥路径不要漏掉 .ssh 前面的反斜杠,常见错误是写成 C:\Users\username.ssh\...,这样指向的是另一个目录。
2.2 在服务器上访问台架
隧道建立后,在服务器上这样连接台架:
1 | ssh -p 54321 <台架用户名>@127.0.0.1 |
传输文件:
1 | scp -P 54321 local_file <台架用户名>@127.0.0.1:/tmp/ |
必须连 127.0.0.1,不能连台架的 IP。服务器访问台架 IP 依然是不通的,能连的只有服务器本机的 54321 端口。
也可以写进服务器上的 ~/.ssh/config,把隧道端口当成一台“别名主机”:
1 | Host bench |
之后在服务器上 ssh bench 即可。
2.3 方案为什么能成立
关键在于 -R 的语法:
1 | -R [bind_address:]port:host:hostport |
其中 host:hostport(这里是 10.13.193.69:31936)是由本地 PC 去解析和连接的,不是在服务器上连接。所以服务器能不能访问台架并不重要,只要本地 PC 能访问台架,隧道就能通。本地 PC 在这里扮演的是跳板机的角色。
3 原理:远程转发 -R
3.1 数据流向
以本次命令 -R 54321:10.13.193.69:31936 为例,一条连接完整经过以下几步:
1 | [服务器] 应用 ──► 127.0.0.1:54321 |
从各方的视角看:
- 服务器只是在访问自己的
127.0.0.1:54321,它并不需要知道台架的存在; - 台架看到的连接来自本地 PC;
- 本地 PC 是中间的跳板,负责把两边接起来。
3.2 保活参数
ServerAliveInterval 和 ServerAliveCountMax 是客户端的探活设置:
ServerAliveInterval=15:每 15 秒向服务器发送一次保活探测;ServerAliveCountMax=8:连续 8 次探测没有响应,就认为连接已经断开并退出。
也就是最多约 15 × 8 = 120 秒无响应后,ssh 会主动结束。
为什么要设置:本地 PC 和服务器之间通常隔着 NAT 或防火墙,长时间没有数据的 TCP 连接会被中间设备悄悄回收,表现就是“隧道看起来还在,实际已经不通了”。定时发送保活报文可以让连接保持活跃,也能让 ssh 及时发现死连接。
注意区分:ServerAlive* 是客户端探活,对应的服务端参数是 ClientAlive*。
3.3 监听地址与 GatewayPorts
默认情况下,-R 只在服务器的回环地址上监听,这是 sshd 出于安全考虑的默认行为。在服务器上可以看到:
1 | ss -lntp | grep 54321 |
127.0.0.1:54321 表示只有服务器本机能使用这条隧道,其他机器连不上。
如果确实需要让服务器所在网段的其他机器也能使用,需要修改服务器的 /etc/ssh/sshd_config:
1 | GatewayPorts yes |
重启 sshd 后,用下面的形式显式指定监听所有地址:
1 | ssh -R 0.0.0.0:54321:10.13.193.69:31936 ... |
但大多数场景(包括本次)只需要服务器自己用,保持默认的回环监听即可,暴露面越小越安全。
3.4 常见的失败原因
remote port forwarding failed for listen port 54321:服务器上 54321 已被占用,或者 sshd 配置了AllowTcpForwarding no;- 服务器上连 54321 被拒绝:本地 PC 的 ssh 进程已经退出,或隧道没有建立成功(用
ssh -v可以看到remote forward success for: listen 54321); - 连上了但马上断开:本地 PC 到台架不通,或台架防火墙拦截。可以先在本地 PC 上直接
ssh -p 31936 <台架用户名>@10.13.193.69验证; bind: Permission denied:非 root 用户绑定 1024 以下端口需要权限,换一个高位端口即可。
另外,如果希望端口转发失败时 ssh 直接退出,而不是留下一条“看起来正常”的登录会话,可以加上:
1 | -o ExitOnForwardFailure=yes |
4 SSH 的其他转发方式与应用场景
SSH 隧道本质是把其他 TCP 连接封装在一条已经建立的 SSH 连接里传输。对外看只是普通的 SSH 流量,里面跑的可以是 SSH、HTTP、数据库等任意 TCP 协议。除了上面用到的 -R,SSH 还提供 -L 和 -D 两种转发方式。
4.1 -L 本地转发
语法:
1 | -L [bind_address:]port:host:hostport |
含义:在本地监听 port,本地收到的连接通过 SSH 服务器转发到 host:hostport。与 -R 相反,这里的 host:hostport 是由服务器侧连接的。
假设情况反过来:服务器能访问台架,本地 PC 访问不到台架,那么在本地 PC 上执行:
1 | ssh -N -L 54321:10.13.193.69:31936 -p 32354 root@10.0.206.124 |
之后在本地 PC 上执行 ssh -p 54321 <台架用户名>@127.0.0.1,就相当于访问台架。
-L 更常见的用法是访问服务器内网的服务。例如服务器本机跑了一个 Web 服务,端口 80:
1 | ssh -N -L 8080:127.0.0.1:80 -p 32354 root@10.0.206.124 |
本地浏览器访问 http://127.0.0.1:8080 就能看到服务器的页面。注意这里的 127.0.0.1 是相对于服务器解析的,指的是服务器自己。
一句话概括:-L 是“把远端网络拉到本地”。
4.2 -D 动态转发
语法:
1 | -D [bind_address:]port |
在本地监听一个 SOCKS5 代理端口:
1 | ssh -N -D 1080 -p 32354 root@10.0.206.124 |
这条命令在本地 127.0.0.1:1080 起了一个 SOCKS5 代理。任何把流量交给这个代理的程序,其连接都会经 SSH 服务器发出,而目标地址由程序在运行时通过 SOCKS 协议告诉代理,不需要在命令行里写死 host:hostport。
使用方式:
1 | # curl 走代理 |
浏览器中把代理设置为 SOCKS5、地址 127.0.0.1、端口 1080 即可。
与 -L 的区别:-L 一次只能把本地一个端口映射到一个固定目标;-D 只开一个端口,却能代理到服务器能访问的任意地址,目标由每个连接动态指定。可以把它理解为“目标可变的 -L”。
OpenSSH 7.6 及以上版本还支持远程动态转发 -R 1080(不带目标地址),在远端开一个 SOCKS 代理,效果与 -D 镜像。
4.3 三者的区别与联系
| 对比项 | -L 本地转发 |
-R 远程转发 |
-D 动态转发 |
|---|---|---|---|
| 监听端 | 本地 | 远端(SSH 服务器) | 本地 |
| 目标地址 | 命令行写死 | 命令行写死 | 不写死,由 SOCKS 请求决定 |
| 目标由谁连接 | 服务器 | 本地 | 服务器 |
| 承载协议 | 任意 TCP | 任意 TCP | SOCKS5 代理 |
| 典型场景 | 通过跳板机访问内网服务 | 把本地/内网服务暴露给远端 | 让程序整体走服务器的网络出口 |
| 一句话 | 把远端拉到本地 | 把本地推到远端 | 本地开一个能去远端的代理 |
三者的联系:
- 底层都复用同一条 SSH 连接,靠 SSH 的通道(channel)多路复用同时承载多个转发;
-L和-R是一对镜像:同一组映射,在哪一端执行决定了监听端和目标端的角色;-D可以看作-L的动态版本:只监听一个端口,但目标由 SOCKS 协议在运行时决定,一个端口服务所有地址;- 三个参数可以同时出现在一条命令里,也可以重复使用(例如同时开多条
-L或-R)。
4.4 如何选择
| 需求 | 选择 |
|---|---|
| 服务器要访问本地 PC 或本地 PC 能到达的地址 | -R |
| 本地要访问服务器或服务器内网的服务 | -L |
| 让浏览器、程序整体走服务器的网络出口 | -D |
回到本次场景:台架的服务地址是固定的 10.13.193.69:31936,所以用 -R 最直接。如果希望服务器能访问“本地 PC 能到达的任意地址”,则可以改用远程动态转发 -R 1080,在服务器上把它当作 SOCKS5 代理使用。
5 实用技巧
5.1 后台运行
Linux / macOS 上可以加 -f 让 ssh 转入后台:
1 | ssh -f -N -R 54321:10.13.193.69:31936 ... |
Windows 自带 OpenSSH 的部分版本不支持 -f,更稳妥的做法是单独开一个终端窗口,或者用任务计划程序在登录时启动。需要断线自动重连时,Linux 下可以用 autossh:
1 | autossh -M 0 -N -o ServerAliveInterval=15 -o ServerAliveCountMax=8 \ |
-M 0 表示不使用 autossh 自己的监控端口,而依赖 SSH 的 ServerAlive* 参数判断连接状态。
5.2 调试
加 -v 可以看到转发是否成功:
1 | ssh -v -N -R 54321:10.13.193.69:31936 ... |
关键日志:
1 | debug1: remote forward success for: listen 54321, connect 10.13.193.69:31936 |
出现 remote forward failure 时,结合前面的失败原因逐项排查。
5.3 安全提醒
- 隧道一旦建立,能访问服务器
54321的人就等价于能访问台架,注意服务器上的账户权限和访问控制; - 不要随意开启
GatewayPorts yes,默认只监听回环地址是最安全的; - 使用密钥认证,不要图省事使用弱密码;
- 隧道只应该开给需要的端口,转发范围越小越好。