SSH 不只是登录工具
绝大多数人对 SSH 的认知停留在"用它登服务器改文件、跑命令"。但实际上,SSH 从设计之初就不只是一个远程登录协议,它是一条全加密的隧道。你在 ssh 命令后面加上几个小小的参数,就能把本地的端口、远端的端口、甚至整个网络代理,安全地穿过防火墙送到另一头。
对个人站长来说,SSH 隧道解决的是一类特别现实的问题:数据库不想直接暴露公网、内网服务需要在没有 VPN 的情况下临时访问、本地开发环境要连生产环境的 Redis 调试、身处受限网络想访问被封锁的服务。这些场景用 VPN 是杀鸡用牛刀,用 frp 或 WireGuard 又要额外部署,而 SSH 隧道零成本、零依赖,服务器上早就有了。
本文把 SSH 的三类端口转发讲透:本地转发、远程转发、动态转发,各自解决什么问题、怎么配、以及最关键的——如何让它稳定常驻并在断线后自动重连。
原理:三类转发的方向判断
SSH 转发的难点不是命令本身,而是被 -L、-R、-D 这三个参数的方向绕晕。一个简单可靠的记忆法则是:看流量从哪里出发,往哪里去。
-L(Local,本地转发):在本地开一个监听端口,流量从本地经过 SSH 隧道送到远端能访问的地方。用于"访问远端内网服务"。-R(Remote,远程转发):在远端开一个监听端口,流量从远端经过隧道送到本地能访问的地方。用于"把本地服务暴露到远端"。-D(Dynamic,动态转发):在本地开一个 SOCKS 代理端口,流量经由远端发出。用于"把远端当跳板,代理所有流量"。
再补一个判断关键点:-L 和 -R 后面第一个地址是监听端,第二个地址是目标端,而目标端是从隧道的哪一端去解析和连接的,才是方向问题的核心。
一、本地转发 -L:把远端服务搬到本地
这是最常用的场景。假设你的 MySQL 跑在服务器上,只监听 127.0.0.1:3306,不开放公网。你想在本地用 Navicat 或 mysql 客户端连上去调试。
ssh -L 13306:127.0.0.1:3306 -N -f root@your-server.com
逐段拆解:
-L 13306:127.0.0.1:3306:本地监听127.0.0.1:13306,把收到的流量通过隧道送到服务器的127.0.0.1:3306。注意这里的127.0.0.1是服务器视角的地址——也就是服务器本机。-N:不执行远程命令,只做转发。加上它就不会给你一个 shell,纯粹当隧道用。-f:转入后台运行,这样终端不会被占住。
连好后,本地直接连 127.0.0.1:13306 就等于连上了服务器的 MySQL:
mysql -h 127.0.0.1 -P 13306 -u root -p
如果目标不是服务器本机,而是服务器内网里的另一台机器(比如服务器能访问 10.0.0.5:6379 的 Redis),那目标段就写内网地址:
ssh -L 16379:10.0.0.5:6379 -N -f root@your-server.com
这时服务器就成了一台跳板机,帮你把请求送进内网。
一个安全提醒:默认 -L 只在本地回环地址上监听,这没问题。但如果你写成 -L 0.0.0.0:13306:... 或者加上 -g 参数让其他机器也能用,就等于把你的数据库间接开放给了整个局域网,谨慎使用。
二、远程转发 -R:把本地服务暴露出去
这是最能体现 SSH 威力、也最容易被忽略的一类。假设你的开发机在办公室内网、没有公网 IP,但你想让外部的同事或测试服务器访问你本地跑的 localhost:8080 服务。
ssh -R 9000:127.0.0.1:8080 -N -f root@your-server.com
这条命令的意思是:在服务器上监听 9000 端口,任何连到服务器 9000 的流量,都会通过隧道送回你本地的 127.0.0.1:8080。于是外部只需访问 your-server.com:9000,就能用上你办公室内网跑的服务。
但这里有一个大坑:默认情况下,远程监听只绑定在服务器的 127.0.0.1 上,外部根本连不进来。要做到真正对外,必须在服务端的 sshd_config 里打开:
GatewayPorts yes
改完重启 sshd。然后远程转发时指定绑定地址:
ssh -R 0.0.0.0:9000:127.0.0.1:8080 -N -f root@your-server.com
GatewayPorts yes 让远程转发可以绑定到非回环地址。如果不方便改全局配置,也可以在命令行里直接写 -R *:9000:...,但是否生效依然取决于服务端有没有放开这个权限。
端口占用问题:如果服务端 9000 已经被占用(或者之前的隧道没退干净),SSH 会报 Warning: remote port forwarding failed for listen port 9000。解决办法是换端口,或者在服务端 sshd_config 里开启 ClientAliveInterval 并定期清理残留连接。
三、动态转发 -D:一个搬进 ssh 的 SOCKS5 代理
动态转发是三类里最"魔法"的。它不指定具体目标,而是把你的 SSH 连接变成一个 SOCKS5 代理:
ssh -D 1080 -N -f root@your-server.com
执行后,本地 127.0.0.1:1080 就是一个 SOCKS5 代理。你在浏览器或任何支持 SOCKS5 的软件里把代理设成它,所有流量就会从服务器那头发出,服务器成了你的出口。
用 curl 测试一下:
curl --socks5 127.0.0.1:1080 https://ifconfig.me
返回的应该是服务器的公网 IP,而不是你本地的 IP。这个方案常用于:在受限网络里访问资源、临时切换出口地域做测试、让某些只认固定 IP 的接口从服务器发起请求。
四、组合与跳板:ProxyJump 的现代写法
除了转发,SSH 还有一类常用能力是"穿过跳板机连目标"。老写法是 -o ProxyCommand,新写法简单得多:
ssh -J user@jump-host user@target-internal
-J(等价于 ProxyJump)让 SSH 先连跳板,再从跳板连目标,全程加密,无需在跳板上手动再敲一次 ssh。如果想固定配置,写进 ~/.ssh/config:
Host jump
HostName jump.example.com
User root
Port 22
Host internal-db
HostName 10.0.0.5
User root
ProxyJump jump
Host mysql-tunnel
HostName jump.example.com
User root
LocalForward 13306 10.0.0.5:3306
ServerAliveInterval 30
ServerAliveCountMax 3
这样以后只要 ssh mysql-tunnel,就会自动建立一条到内网数据库的本地转发隧道,配置永久生效,比每次手敲长命令可靠得多。
五、让隧道稳定常驻:autossh 与 keepalive
手动敲的 ssh -L ... -f -N 有个致命问题:网络抖动、NAT 超时、服务器重启之后,隧道就断了,而且 -f 后台进程可能悄无声息地死掉。
两种解决方式。第一种是启用 SSH 自己的保活机制,在 ~/.ssh/config 里加:
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
TCPKeepAlive yes
ServerAliveInterval 30 表示每 30 秒向服务器发一个保活包,连续 3 次没回应就断开重连(由外层管理)。ExitOnForwardFailure yes 特别重要——如果端口转发没建立成功,直接退出进程,而不是留一个"看着活着其实没转发"的僵尸隧道。
第二种方式是用 autossh,它是专门为"隧道断了自动重连"而生的:
apt-get install -y autossh
autossh -M 0 -f -N -L 13306:127.0.0.1:3306 \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes root@your-server.com
-M 0 表示关闭 autossh 自己的监控端口,改用 SSH 的 ServerAliveInterval 来检测——这是现在推荐的做法,因为 autossh 自带的监控端口在 NAT 环境下经常误判。
把这条命令做成 systemd 服务,开机自启、崩溃重启,才算真正可靠。以 /etc/systemd/system/ssh-tunnel.service 为例:
[Unit]
Description=Persistent SSH Tunnel to MySQL
After=network-online.target
Wants=network-online.target
[Service]
User=root
ExecStart=/usr/bin/autossh -M 0 -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=accept-new \
-L 13306:127.0.0.1:3306 \
root@your-server.com
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
然后在目标机上配置好 SSH 免密登录(通过密钥),systemctl enable --now ssh-tunnel 即可。这样隧道永远在线,断了 10 秒内自动重连。
真实场景复盘:一次隧道"假活"引发的数据错乱
我遇到过这样一个案例。一位站长用 ssh -f -N -L 长期挂着一台跳板隧道,本地脚本每五分钟通过 127.0.0.1:13306 连远端数据库跑一次数据同步。某天开始,同步脚本开始报"连接被拒绝",但检查进程列表,那个 ssh 进程明明还活着。
排查过程:先用 ss -tlnp | grep 13306 看本地监听,发现端口在;再 ssh -v 重连一次观察,注意到原隧道对应的底层 TCP 已经处于 CLOSE_WAIT 状态,但 ssh 客户端没有退出——因为最初的启动命令没有加 ExitOnForwardFailure,也没开保活。真正的根因是:服务器重启过一次,隧道对端没了,但本地 ssh 进程卡在一个失效连接上,既不转发也不退出,成了一个"假活的僵尸隧道"。
修复方案就是本文反复强调的两点:加上 ServerAliveInterval 保活 + ExitOnForwardFailure yes 让失败即退出,外面再套 autossh 或 systemd 的 Restart=always 做重连。改造之后,隧道在服务器重启后 10 秒内自动恢复,同步脚本再没因为这个断过。
这个案例的教训是:任何"后台常驻"的东西,只要没有退出机制和重启机制,就一定会在某个时刻变成僵尸。隧道如此,其他守护进程也一样。稳定性不是靠"它平时能跑",而是靠"它坏的时候能被发现并被拉起来"。
安全边界:别让隧道变成后门
SSH 隧道能力强大,用不好就是在给自己开后门。几条务必遵守的纪律:
- 密钥优先,禁用密码登录。隧道依赖的 SSH 登录本身要够硬,
PasswordAuthentication no+ 密钥认证是底线。 - 限制转发权限。如果某个账号只用来做跳板而不该有转发能力,在
authorized_keys里给对应的密钥加上no-port-forwarding。反过来,如果某台机器只用来做隧道,可以给密钥加no-pty,no-agent-forwarding,no-X11-forwarding,command="/bin/false",把权限收得死死的。 - 不要绑 0.0.0.0。除非明确知道自己在做什么、且防火墙已经收口,否则监听地址一律用
127.0.0.1。 - 记得善后。临时隧道用完就
pkill掉,尤其是-R远程转发,别让它一直挂在那里,哪天忘了就成了永久入口。
总结
SSH 端口转发的三类方向,一句话概括:-L 是"把远方拉过来",-R 是"把近处推出去",-D 是"把远方当出口"。掌握了方向判断,剩下的就是参数细节。
对个人站长而言,SSH 隧道最大的价值是零部署成本地解决"临时访问、安全调试、内网穿透"这三类高频需求。不需要装 VPN,不需要架 frp,服务器上现成的 sshd 就能用。真正要花心思的不是命令怎么写,而是让它稳定常驻(autossh + systemd)和安全收口(密钥认证 + 转发权限限制)。把这两点做好,SSH 隧道就是一个既方便又安全的运维利器。