SSH 隧道转发深度指南:本地转发、远程转发与动态代理的原理和实战
SSH 隧道转发深度指南:本地转发、远程转发与动态代理的原理和实战
摘要
SSH 隧道转发是运维、后端开发、安全测试和跨网络访问中极其常见的一项基础能力。很多人知道 ssh -L、ssh -R、ssh -D 这些命令能“打通网络”,但真正进入生产环境后,经常会遇到方向搞反、端口绑定错误、服务暴露范围失控、跳板机链路混乱、数据库访问异常等问题。
本文系统梳理 SSH 隧道转发的工作原理、三种核心模式、本地与远端视角的区别、跳板机场景、多级链路、配置项限制、排错方法与安全建议。读完后,你应该能够准确判断该用哪一种转发,知道流量到底从哪里进、到哪里出,也能在真实项目中稳定地用它访问数据库、调试内网服务、临时暴露本地端口,或者构建 SOCKS 代理。
目录
- #ssh 隧道转发解决的到底是什么问题
- #先建立一个正确的网络视角
- #本地转发 ssh -L
- #远程转发 ssh -R
- #动态转发 ssh -D
- #跳板机与多级链路
- #数据库内网服务与远程调试实战
- #服务端配置与限制项
- #常见错误与排查方法
- #安全边界与最佳实践
- #总结
正文
SSH 隧道转发解决的到底是什么问题
SSH 最常见的用途是远程登录:
ssh user@server
但 SSH 不只是一个登录协议,它还可以在加密连接里承载其他 TCP 流量。所谓“隧道转发”,本质上就是:
把某个入口端口收到的 TCP 数据,通过 SSH 连接,转交给另一端去访问目标地址和端口。
因此,SSH 隧道常用来解决这些问题:
- 本机无法直连内网数据库,但能 SSH 到跳板机。
- 内网服务没有公网入口,但需要临时从外部访问。
- 本地开发服务要暴露给远端机器验证。
- 浏览器或 CLI 需要借助 SOCKS 代理访问另一侧网络。
- 不希望明文暴露数据库、Redis、管理面板等高风险端口。
它不是魔法,也不是 VPN 的等价替代品。它做的事情很具体:转发一个或一组 TCP 连接。
先建立一个正确的网络视角
理解 SSH 隧道最容易出错的地方,不是命令格式,而是“站在谁的视角看地址”。
假设你在本机执行:
ssh user@bastion
则至少存在三个角色:
- 本机客户端:你执行
ssh命令的机器。 - SSH 服务端:
bastion。 - 最终目标服务:例如 MySQL、Redis、HTTP 服务。
关键原则:
ssh命令总是在本机发起。-L监听端口默认开在本机。-R监听端口默认开在远端 SSH 服务端。- 最终目标地址由“转发出口所在一侧”去解析和访问。
这句话很关键,举个例子:
ssh -L 13306:db.internal:3306 user@bastion
这里的 db.internal:3306 不是由你的本机去访问,而是由 bastion 那一侧去访问。也就是说:
- 入口:本机
127.0.0.1:13306 - 出口:
bastion去连db.internal:3306
把这个视角搞清楚,后面三种转发就不容易混。
本地转发:ssh -L
1. 语义
本地转发的意思是:
在本机打开一个监听端口,把收到的流量通过 SSH 送到远端,再由远端访问目标地址。
基本格式:
ssh -L [本地监听地址:]本地端口:目标主机:目标端口 user@ssh-server
例子:
ssh -L 13306:127.0.0.1:3306 user@db-gateway
含义:
- 本机监听
127.0.0.1:13306 - SSH 连接建立到
db-gateway - 到达
13306的流量,经db-gateway转发到它自己看到的127.0.0.1:3306
这常用于数据库只监听远端本地回环地址时。
2. 高价值场景
场景一:访问内网 MySQL
ssh -L 13306:mysql.internal:3306 ops@bastion
然后本机连接:
mysql -h 127.0.0.1 -P 13306 -u app -p
这是非常典型的用法。你的电脑无法直连 mysql.internal,但 bastion 可以,于是通过 SSH 把链路接过去。
场景二:访问远端仅内网开放的 Web 管理页面
ssh -L 18080:10.0.2.15:8080 dev@bastion
然后本机浏览器访问:
http://127.0.0.1:18080
场景三:让命令行工具误以为服务就在本机
很多工具只接受 localhost + port,而不关心后面真实是谁。-L 正好适合作为这类适配层。
3. 常见误区
最常见误区是把目标主机理解成本机视角。例如:
ssh -L 8080:localhost:8080 user@server
这里的 localhost 指的是 server 自己,不是你本机。如果你原本想把远端服务映射回本机,这是对的;如果你以为它会回到本机,就完全错了。
4. 常用参数组合
不想进入交互 shell 时:
ssh -N -L 13306:mysql.internal:3306 ops@bastion
-N:不执行远程命令,只做转发。
放到后台:
ssh -f -N -L 13306:mysql.internal:3306 ops@bastion
-f:认证完成后转入后台。
启动前先检查端口绑定是否成功:
ssh -v -N -L 13306:mysql.internal:3306 ops@bastion
-v:输出详细调试日志。
远程转发:ssh -R
1. 语义
远程转发的意思是:
在 SSH 服务端那一侧打开监听端口,把收到的流量通过 SSH 回送到本机,再由本机访问目标地址。
基本格式:
ssh -R [远端监听地址:]远端端口:目标主机:目标端口 user@ssh-server
例子:
ssh -R 18080:127.0.0.1:8080 user@vps
含义:
- 在
vps上监听18080 - 有人访问
vps:18080时 - 流量经 SSH 回送到你的本机
- 再由你的本机访问
127.0.0.1:8080
这相当于把本地服务临时暴露给远端。
2. 高价值场景
场景一:把本地开发服务暴露给远端测试机
你的本机跑了一个前端或 API:
npm run dev
本地监听 127.0.0.1:3000,你想让远端机器访问:
ssh -R 13000:127.0.0.1:3000 user@vps
此时在 vps 上可以访问:
http://127.0.0.1:13000
场景二:让远端服务器回调你本地服务
例如你正在调试 webhook,需要远端环境把请求打到你本机服务,可以用 -R 临时接通。
3. 远程暴露范围问题
默认情况下,ssh -R 在服务端通常只绑定回环地址,也就是只有远端机器自己能访问。如果希望其他机器也能访问,常见写法是:
ssh -R 0.0.0.0:13000:127.0.0.1:3000 user@vps
但这是否真的生效,取决于 SSH 服务端配置中的 GatewayPorts。如果服务器配置不允许,即使你写了 0.0.0.0,也不会真正对外开放。
这也是 -R 最容易被误判的地方之一:客户端命令写对了,不代表服务端策略允许。
动态转发:ssh -D
1. 语义
动态转发本质上是:
在本机启动一个 SOCKS 代理端口,由应用按需告诉它目标地址,再通过 SSH 从远端网络发起连接。
基本格式:
ssh -D [本地监听地址:]本地端口 user@ssh-server
例子:
ssh -D 1080 user@bastion
这会在本机启动一个 SOCKS5 代理:
127.0.0.1:1080
浏览器、curl、包管理器或其他支持 SOCKS 代理的工具,都可以通过它借道 bastion 那一侧网络。
2. 典型使用方式
curl 示例:
curl --socks5-hostname 127.0.0.1:1080 http://service.internal
浏览器则可以把代理配置成:
SOCKS5 127.0.0.1:1080
3. 为什么 -D 比 -L 更灵活
-L 是固定映射,一个本地端口对应一个目标地址和端口。
-D 则是通用代理,最终目标由应用在每次连接时动态指定。所以:
- 适合浏览器、多服务调试、命令行代理。
- 不适合那些必须写死目标地址的单用途场景。
跳板机与多级链路
很多生产网络不是“你直接 SSH 到目标机”,而是:
本机 -> 跳板机 -> 业务机 -> 内网服务
这时 SSH 隧道依然很好用。
1. 使用 -J 指定跳板机
ssh -J jumpuser@bastion appuser@app-server
如果要做本地转发:
ssh -J jumpuser@bastion -L 13306:mysql.internal:3306 appuser@app-server
这里真正访问 mysql.internal:3306 的,是最终 SSH 落点 app-server 那一侧,而不是本机,也不是跳板机。
2. 使用 SSH 配置简化命令
可以在 ~/.ssh/config 中定义:
Host bastion
HostName bastion.example.com
User jumpuser
Host app-prod
HostName 10.0.10.23
User appuser
ProxyJump bastion
之后命令就可以简化为:
ssh -L 13306:mysql.internal:3306 app-prod
这对维护多个环境非常有价值,否则命令行会越来越长,越来越容易写错。
数据库、内网服务与远程调试实战
实战一:通过跳板机访问生产 Redis
ssh -N -L 16379:redis.internal:6379 ops@bastion
然后本机连接:
redis-cli -h 127.0.0.1 -p 16379
如果你连不上,先区分三种可能:
- 本地
16379没监听成功。 bastion无法访问redis.internal:6379。- Redis 本身只允许特定来源访问。
实战二:安全访问只开放回环地址的远端 PostgreSQL
远端数据库配置为仅监听:
127.0.0.1:5432
这时命令通常是:
ssh -N -L 15432:127.0.0.1:5432 dbadmin@db-host
注意这里目标主机必须写远端视角的 127.0.0.1,因为数据库就运行在 db-host 自己身上。
实战三:把本地调试接口暴露给远端 Linux 主机
ssh -N -R 19090:127.0.0.1:9090 user@remote-host
然后在远端主机执行:
curl http://127.0.0.1:19090/actuator/health
实战四:临时构建 SOCKS 代理访问另一侧网络
ssh -N -D 1080 user@bastion
适合这些用途:
- 浏览器访问内网站点。
curl、git、包管理器借道测试另一侧网络。- 安全测试时验证某段网络上的可达性。
服务端配置与限制项
光会写客户端命令还不够,服务端 SSH 配置也会直接影响转发是否可用。典型配置在 /etc/ssh/sshd_config。
1. AllowTcpForwarding
AllowTcpForwarding yes
如果该项关闭,-L、-R、-D 都可能被拒绝。
2. GatewayPorts
GatewayPorts no
它主要影响远程转发监听地址的对外暴露能力:
no:通常仅允许绑定回环地址。yes:允许绑定更广泛地址。clientspecified:允许客户端指定监听地址。
如果你发现 ssh -R 0.0.0.0:13000:... 仍然只能本机访问,多半要检查这里。
3. PermitOpen
PermitOpen 127.0.0.1:3306 10.0.0.5:6379
它可以限制客户端允许转发到哪些目标地址和端口,属于比较细粒度的安全控制。
常见错误与排查方法
错误一:bind: Address already in use
本地或远端要监听的端口已经被占用。先检查端口:
netstat -ano | findstr 13306
或者在 Linux 上:
ss -lntp | grep 13306
错误二:channel open failed: connect failed
这通常表示 SSH 隧道本身建立了,但出口一侧访问目标服务失败。要检查:
- 目标主机名是否由出口那一侧可解析。
- 目标端口是否真的在监听。
- 安全组、防火墙、白名单是否放通。
错误三:本地能连 SSH,但转发后的服务仍然访问失败
这种情况常见于把目标地址写错成了错误一侧的 localhost,或者服务只监听了 Unix socket、指定网卡地址,没有监听你写的那个地址。
错误四:远程转发已经执行,但其他机器仍然不能访问
优先检查两件事:
- 命令是否写了
0.0.0.0或指定了非回环地址。 - 服务端
GatewayPorts是否允许。
错误五:命令执行后卡住不返回
这通常不是异常。SSH 在前台维持隧道,本来就会保持连接。若只是做转发,推荐:
ssh -N -L 13306:mysql.internal:3306 ops@bastion
或者后台运行:
ssh -f -N -L 13306:mysql.internal:3306 ops@bastion
安全边界与最佳实践
SSH 隧道很方便,但如果不控制边界,会把“临时调试”变成“长期暴露”。
建议遵循这些原则:
- 默认只监听
127.0.0.1,除非你明确需要对外暴露。 - 数据库、Redis、管理后台优先使用本地转发,不直接开公网端口。
- 远程转发只做临时用途,用完立刻关闭。
- 为跳板机和生产机分离账号与密钥,不要混用超权账号。
- 把常用目标写进
~/.ssh/config,减少命令输错概率。 - 对长期使用的隧道配合
ServerAliveInterval、ServerAliveCountMax提高稳定性。 - 在服务端启用
AllowTcpForwarding、PermitOpen等策略做最小授权。 - 用
-v、-vv、-vvv观察链路建立过程,不靠猜测排错。
一个比较稳妥的 SSH 配置片段如下:
Host bastion
HostName bastion.example.com
User ops
ServerAliveInterval 30
ServerAliveCountMax 3
这样在网络抖动时,客户端更容易及时发现断链,而不是长时间挂着一个失效隧道。
总结
SSH 隧道转发并不复杂,复杂的是网络视角和边界控制。只要你始终先问自己两个问题,很多错误都能提前避免:
- 监听端口到底开在哪一侧?
- 目标地址到底由哪一侧去解析和访问?
围绕这个判断:
-L适合把远端服务映射到本机。-R适合把本地服务暴露给远端。-D适合做通用 SOCKS 代理。
真正进入生产实践后,SSH 隧道最有价值的不是“能不能连通”,而是你是否清楚地知道链路怎么走、暴露到了什么范围、出了问题该怎么定位。把这些基本功掌握住,SSH 隧道就会从一个临时技巧,变成你日常排障、调试和安全访问中的稳定工具。
延伸阅读
man sshman ssh_configman sshd_config- OpenSSH 官方文档
一句话记忆
SSH 隧道转发的本质,就是把一侧监听到的 TCP 流量,经加密 SSH 连接,交给另一侧去访问目标服务;先分清入口在哪,才能分清命令该怎么写。