SSH 隧道与端口转发实战:本地/远程/动态转发三类方向、autossh 常驻与安全收口全流程

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 隧道就是一个既方便又安全的运维利器。

Last modification:October 9th, 2026 at 01:25 pm

Leave a Comment