服务器上的 MySQL 只监听 127.0.0.1,你用 Navicat 连不上;Grafana 面板不想暴露到公网,但你又想在外面看看;本地开发的接口要让同事测一下,可他连不上你的笔记本。
这三件事看起来不相干,其实是同一个需求:把网络那一头的某个端口,安全地搬到这一头来。SSH 隧道就是干这个的,而且不需要额外装任何软件 —— 你配 SSH 密钥 时用的那个 ssh 命令本身就带这个能力。
先建立直觉,细节后面再说:
| 选项 | 方向 | 一句话 |
|---|---|---|
-L | 本地 ← 远程 | 把服务器上的端口拉到我本机 |
-R | 本地 → 远程 | 把我本机的端口推到服务器上 |
-D | 动态 | 在本机开一个 SOCKS 代理,流量走服务器出去 |
记法:L 是 Local(拉到本地),R 是 Remote(推到远程),D 是 Dynamic(动态代理)。
三个选项可以同时用,也可以叠加多个。下面逐个说。
这是最常用的一个,先看格式:
ssh -L [本地地址:]本地端口:目标主机:目标端口 用户@跳板服务器
这里有个最容易搞混的点:目标主机 是从服务器视角解析的,不是从你本机。
ssh -N -L 3306:127.0.0.1:3306 [email protected]
这条命令的意思是:在我本机监听 3306,任何连上来的请求,都由服务器转发到服务器自己的 127.0.0.1:3306。
所以 127.0.0.1 指的是服务器上的回环地址,不是你的电脑。理解这一点,后面所有用法都能推出来。
-N 表示不执行远程命令,纯粹做转发 —— 加不加都能用,但加上更清晰,也不会因为误操作在服务器上开个 shell。
这是 SSH 隧道最经典也最有价值的用法。
前提是你的数据库已经按 VPS 数据库不要直接暴露公网 里的做法,只监听 127.0.0.1。这样公网扫不到,但你自己的客户端可以通过隧道连上去。
MySQL / MariaDB:
# 本机 3307 转发到服务器的 3306(用 3307 避免和本机已有 MySQL 冲突)
ssh -N -L 3307:127.0.0.1:3306 [email protected]
然后在 Navicat / DBeaver / TablePlus 里连:
- 主机:
127.0.0.1 - 端口:
3307 - 用户名密码:数据库自己的账号
PostgreSQL:
ssh -N -L 5433:127.0.0.1:5432 [email protected]
Redis(用 redis-cli 连隧道):
ssh -N -L 6380:127.0.0.1:6379 [email protected]
# 另开一个终端
redis-cli -h 127.0.0.1 -p 6380
注意 127.0.0.1 和 localhost 的区别:在 MySQL 客户端的语境里,localhost 常常意味着"走 Unix socket"而不是 TCP。隧道场景一定要写 127.0.0.1,写 localhost 可能会去连本机的 socket 而完全绕过隧道。
Grafana、Portainer、Adminer、Metabase、pgAdmin 这类工具,绑在 127.0.0.1:3000 上,需要时开个隧道:
ssh -N -L 3000:127.0.0.1:3000 [email protected]
然后浏览器打开 http://localhost:3000。
这比给面板配域名 + HTTPS + 登录加固要安全得多 —— 因为面板压根没有暴露在公网,攻击者连它的存在都发现不了。本来要折腾一堆认证配置,现在一条命令解决。
缺点是每次用都要开隧道。如果经常用,后面讲 ~/.ssh/config 那节可以固化下来。
ssh -N -L 8888:127.0.0.1:8888 [email protected]
Jupyter 默认就只绑定本地,配合隧道用最合适。同样的方式也适用于 Streamlit、Gradio、TensorBoard 这些跑在服务器上的开发工具。
上面都用 -N,适合"开一个终端挂着"。如果你还想同时进去操作服务器,可以不加 -N,直接:
ssh -L 3307:127.0.0.1:3306 [email protected]
# 进去之后隧道一直有效,直到你退出这个会话
想让它自己在后台跑,加 -f(先 fork 到后台):
ssh -f -N -L 3307:127.0.0.1:3306 [email protected]
但不推荐长期用 -f —— 进程在后台,断了你不知道,用完也容易忘记杀。要长期保持的隧道,用后面讲的 systemd 方案。
方向反过来。你在本地写了点东西想让人看到,但笔记本没有公网 IP:
ssh -N -R 8080:127.0.0.1:3000 [email protected]
意思是:在服务器上监听 8080,请求转发回你本机的 3000。
这样跑完之后,你去服务器上 curl 127.0.0.1:8080 能通,但从外网访问 203.0.113.10:8080 不通。
原因是服务端 sshd_config 里这一项默认是关的:
#GatewayPorts no ← 官方默认值
GatewayPorts no 表示远程转发的端口只绑定回环地址,外部访问不到。这个默认值是出于安全考虑 —— 否则任何能 SSH 上来的人都能在服务器上开放端口给公网。
要让它对外可用,服务器上改:
sudo sed -i 's/^#\?GatewayPorts.*/GatewayPorts clientspecified/' /etc/ssh/sshd_config
sudo sshd -t && sudo systemctl reload ssh
然后转发时明确指定绑定地址:
ssh -N -R 0.0.0.0:8080:127.0.0.1:3000 [email protected]
但先说清楚风险:0.0.0.0 意味着任何能访问这台服务器 8080 的人,都能访问到你笔记本上的服务。如果你的笔记本在咖啡厅的 WiFi 上,那个服务还带着本地的鉴权信息,这就是把内网敞开。
更稳的做法是保持 GatewayPorts no,需要时通过本地转发绕一圈:先用 -R 推到服务器回环,再用 -L 从服务器拉出来,或者干脆让同事也建一条 -L 隧道。
-R 也可以用来从没有公网 IP 的内网机器反向连出来,让公网服务器能访问内网服务。这是 FRP 这类工具做的事,但一次性的临时需求用 SSH 就够了。长期、多服务的场景还是用专门工具:FRP 内网穿透、Cloudflare Tunnel 或 Pangolin。
一次转发解决所有端口:
ssh -N -D 1080 [email protected]
本机的 1080 就成了一个 SOCKS5 代理,把浏览器的代理设置指向 socks5://127.0.0.1:1080,所有流量就从服务器出去了。
和 -L 的区别:-L 要一个个端口指定目标,-D 是"你来决定连哪,我帮你转发"。
两个必须注意的点:
第一,DNS 可能泄漏。 普通 socks5:// 会在本地解析域名,你访问了哪些域名本地网络能看到。用 socks5h://,让代理解析域名:
socks5h://127.0.0.1:1080
Firefox 里则是在 about:config 把 network.proxy.socks_remote_dns 设为 true。
第二,-D 只是个 SOCKS 代理,凭据仍在明文里(HTTPS 的加密不变,但 HTTP 流量是裸的)。它不提供加密,只提供"从另一个出口出去"。这个区别在涉及敏感操作时要清楚。
每次都敲一长串命令很烦。写进 ~/.ssh/config:
Host db-tunnel
HostName 203.0.113.10
User root
IdentityFile ~/.ssh/id_ed25519
LocalForward 3307 127.0.0.1:3306
LocalForward 5433 127.0.0.1:5432
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
SessionType none
之后只要 ssh db-tunnel 一条命令,两条隧道一起开好了。
几个配置项的作用:
LocalForward可以写多行,一次开多条隧道RemoteForward对应-RSessionType none相当于-N,不分配终端ServerAliveInterval 30每 30 秒发保活包ExitOnForwardFailure yes端口绑定失败就直接退出,而不是留下一个"看着连接着、其实转发没生效"的假状态
最后这项特别值得加。 不加的话,如果本地端口被占用导致绑定失败,SSH 连接照常建立,你只会觉得"连上了但连不通",很难想到是转发根本没起来。
隧道会断 —— 网络抖动、NAT 超时、服务器重启都会。要长期保持,需要一个能自动重连的方案。
关于 autossh:这是过去最常见的答案,但它的最后一次提交是 2024 年 7 月,两年多没更新了。功能上还能用,但既然 systemd 已经能做同样的事,没必要再引入一个维护停滞的工具。
用 systemd 托管隧道:
/etc/systemd/system/db-tunnel.service:
[Unit]
Description=SSH Tunnel for remote database access
After=network-online.target
Wants=network-online.target
[Service]
User=tunnel
ExecStart=/usr/bin/ssh -NT \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=accept-new \
-i /home/tunnel/.ssh/id_ed25519 \
-L 127.0.0.1:3307:127.0.0.1:3306 \
[email protected]
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now db-tunnel
-L 127.0.0.1:3307:... 里的 127.0.0.1 别漏。 不写的话默认也是绑回环,但显式写出来能防止以后误改成 0.0.0.0 —— 那等于把数据库端口在局域网里公开了。
network-online.target 那两个也别省,否则机器启动时网络还没就绪,隧道建了又断,无限重试。
有时候目标服务器不直接开 SSH,要先经过一台跳板机。过去要建两次隧道(先在跳板机上开端口,再从本机连过去),现在一条命令:
ssh -J [email protected]:22 [email protected]
-J 是 OpenSSH 7.3 开始支持的参数。在 config 里写更省事:
Host internal-server
HostName 10.0.0.5
User deploy
ProxyJump [email protected]
IdentityFile ~/.ssh/id_ed25519
隧道也能穿过跳板机:
ssh -J [email protected] -N -L 3307:127.0.0.1:3306 [email protected]
这里的 127.0.0.1 是从最终目标机(10.0.0.5)的视角解析的,跳板机只是中转。理解了前面那条规则,这里就不会绕晕。
隧道不会给你额外的权限。 它只是把你已经有的 SSH 访问权,延伸成"访问服务器上某个端口"的能力。你能连的数据库,取决于你 SSH 登进去之后能不能连 —— 隧道不改变这一点。
但有几件事要注意:
别随便开 GatewayPorts yes(或 clientspecified + 0.0.0.0)。 那等于把服务器变成一个转发器,任何能连上的人都可能借道进入你的内网。只在明确知道自己在做什么时开。
关注服务器上的其他用户。 AllowTcpForwarding 默认是开的(官方默认值 yes),意味着任何能 SSH 上你服务器的人,都能建隧道。如果是多人共用的机器,这可能是你不想要的 —— 可以在 sshd_config 里按用户或用户组限制:
AllowTcpForwarding no
Match Group tunnelusers
AllowTcpForwarding yes
用专门的受限账号做隧道。 长期隧道不要用 root。建一个只能转发、不能登录 shell 的账号更安全。做法是给这个用户设置 nologin shell,同时在 authorized_keys 里用 command= 和 permitopen= 限制它能转发到哪:
command="/bin/false",no-pty,no-agent-forwarding,permitopen="127.0.0.1:3306" ssh-ed25519 AAAA... tunnel-key
permitopen 限制这把密钥只能转发到 3306,就算密钥泄露也做不了别的。
隧道不是 VPN。 它只转发你明确指定的端口(或 SOCKS 范围内的连接),不给你一个"进入内网"的网络接口,也不能访问服务器所在网段的其他机器。要那种效果用 WireGuard 或 Tailscale / Headscale。
bind: Address already in use
本地端口被占了。换个本地端口(比如 3306 换成 3307),或者查是谁占着:
# Linux / macOS
sudo ss -lntp 'sport = :3307'
# Windows
netstat -ano | findstr :3307
其他排查见 VPS 端口被占用怎么办。
channel 2: open failed: connect failed: Connection refused
隧道本身通了,但服务器上那个目标端口没人监听。检查服务是否在跑、监听地址对不对:
# 在服务器上执行
sudo ss -lntp 'sport = :3306'
如果是 Docker 容器,还要注意端口有没有映射出来。
channel 3: open failed: administratively prohibited
服务端禁用了转发。检查服务器 sshd_config 的 AllowTcpForwarding,以及 authorized_keys 里有没有 permitopen / no-port-forwarding 限制。
连上了但访问不通
按顺序确认:
ExitOnForwardFailure yes加上,看是不是绑定失败被吞了- 服务器上确认目标服务在监听(上面那条
ss) - 客户端连的是
127.0.0.1而不是localhost - 客户端端口写对了没(转发到 3307 就得连 3307)
隧道莫名其妙断了
先看是不是空闲被清理了,加 ServerAliveInterval 30。频繁断线的其他原因见 SSH 总是断开怎么办。
Permission denied (publickey)
隧道建立阶段的认证失败,和转发无关。见 SSH Permission denied 怎么办。
隧道里的流量加密吗?
加密。整条 SSH 连接都是加密的,所以隧道里的 MySQL 明文协议也是安全的 —— 这也是为什么可以不用配数据库 TLS 就通过隧道访问。但记住加密到的是服务器,从服务器再往外就不在隧道保护范围里了。
一条隧道能给多个人用吗?
技术上可以(本地绑 0.0.0.0,局域网内共享),但等于把你的访问权借给别人,而且审计时无法区分是谁在操作。别这么做。
会不会拖慢速度?
有额外开销,但通常可以忽略。SSH 的加解密开销在现代 CPU 上很小,真正的瓶颈是网络。传大文件时 rsync over SSH 可能比裸传输慢 10-20%,一般可以接受。
隧道能转发 UDP 吗?
不能。 SSH 只转发 TCP。要转发 UDP(比如 DNS、游戏协议、WireGuard 本身)得用 VPN 方案。
服务器重启后隧道还在吗?
用 systemd 托管的话在(前提是 enable 了)。手动 ssh -L 起的不会自动恢复。
Windows 上怎么用?
Win10 1809+ 自带的 OpenSSH 客户端用法完全一样,命令照抄。想图形化可以用 MobaXterm、Termius 这类带隧道管理界面的工具。
和 frp / Cloudflare Tunnel 怎么选?
- 临时的、个人的、几个端口 → SSH 隧道,零配置零部署
- 长期的、要给外部用户访问的、多服务的 → FRP 或 Cloudflare Tunnel
- 要让多台机器组成一个内网 → WireGuard / Tailscale
SSH 隧道最大的优势是不需要在服务器上部署任何东西,随手就用;劣势是不适合做服务、没有图形界面、管理多台机器时配置会散落各处。
- VPS 配置 SSH 密钥登录 — 隧道最好配密钥用,别用密码
- VPS 数据库不要直接暴露公网 — 隧道是这套方案的前置条件
- SSH 总是断开怎么办 — 隧道频繁断线的原因
- VPS 搭建 FRP 内网穿透 — 需要长期稳定穿透时的方案
- VPS 搭建 WireGuard VPN — 要让多台机器互通,隧道就不够了
