文章

SSH 隧道打洞原理:怎样穿透内网连上远程服务

SSH 隧道打洞原理:怎样穿透内网连上远程服务

什么是”打洞”

你在云上有一台 VM,上面跑着一个服务(比如 WebSocket server),监听在 8080 端口。你想从本地笔记本连上去。

听起来很简单?但现实是:

  • VM 没有公网 IP,只有 VPC 内网地址(比如 10.128.0.42
  • 你的笔记本在家里的 NAT 后面,也没有公网 IP
  • 两台机器在不同的私有网络里,互相看不见

这就是典型的”内网穿透”问题。所谓”打洞”,就是在两个互不可达的网络之间建立一条通道。

网络基础:为什么连不上

理解打洞之前,先搞清楚为什么连不上。

IP 地址与可达性

互联网的基本规则:你只能主动连接一个你能路由到的 IP 地址

1
2
3
4
5
你的笔记本 (192.168.1.100)
    → 家用路由器 NAT (公网 IP: 203.0.113.5)
        → 互联网
            → GCP VPC 路由器
                → VM (10.128.0.42)  ← 这个地址不在公网路由表里

10.128.0.42 是 RFC 1918 私有地址,全世界的路由器都不会帮你把包发到这里。你在笔记本上执行 curl 10.128.0.42:8080,包出了你的网关就被丢弃了。

三种典型场景

场景能连吗原因
VM 有公网 IP,防火墙开了 8080直接路由可达
VM 有公网 IP,防火墙没开 8080不能包到了但被防火墙丢弃
VM 没有公网 IP不能包根本路由不过去

第三种场景就是我们要解决的。

SSH 隧道:借道已有的通道

核心思想

虽然你不能直接连 VM 的 8080 端口,但你能 SSH 到这台 VM(通过 GCP 的 IAP 隧道或者跳板机)。SSH 连接本身就是一条加密通道。SSH 隧道的思想很简单:

既然 SSH 连接已经通了,那就把别的流量也塞进这条 SSH 连接里传过去。

Local Port Forwarding(本地端口转发)

这是最常用的打洞方式。命令格式:

1
ssh -L [本地地址:]本地端口:目标地址:目标端口 跳板机

具体例子:

1
ssh -NL 18789:localhost:8080 user@jump-host

这条命令做了什么:

1
2
3
4
5
6
7
8
9
10
你的笔记本                          远程 VM
┌─────────────────┐                ┌─────────────────┐
│                 │                │                 │
│  应用程序        │                │  服务 (8080)     │
│    ↓ 连接       │                │    ↑             │
│  localhost:18789 │    SSH 隧道    │  localhost:8080  │
│    ↓            │ ══════════════ │    ↑             │
│  SSH 客户端 ─────┼───加密通道────→┼── SSH 服务端      │
│                 │                │                 │
└─────────────────┘                └─────────────────┘

分解一下数据流:

  1. 你的应用连接 localhost:18789
  2. SSH 客户端在本地监听 18789,收到连接后把数据加密,通过已建立的 SSH 连接发送到远端
  3. 远端的 SSH 服务端收到数据后解密,然后以自己的身份连接 localhost:8080
  4. 远端服务的响应原路返回

关键点:第 3 步的 localhost 是相对于远端 VM 说的,不是你的本机。这是很多人搞混的地方。

-N-L 参数详解

1
ssh -N -L 18789:localhost:8080 user@host
  • -N:不执行远程命令,只做端口转发。没有这个参数你会同时打开一个远程 shell
  • -L:Local forwarding。格式是 本地端口:远端目标:远端端口
  • localhost:这里指的是远端机器的 loopback 地址,不是你本机的

还有一个常见写法:

1
ssh -L 18789:0.0.0.0:8080 user@host

区别在于隧道的远端目标地址。如果远端服务绑定在 0.0.0.0(所有接口),用 localhost0.0.0.0 都能连上。但如果服务只绑定在某个特定接口(比如 eth0 的 IP),你就需要指定那个 IP。

Remote Port Forwarding(远程端口转发)

方向反过来:让远端机器的某个端口转发到你本地的某个端口。

1
ssh -R 9090:localhost:3000 user@remote-host

这会让远端机器的 9090 端口转发到你本机的 3000。适用于你想把本地开发服务暴露给远端的场景。

Dynamic Forwarding(SOCKS 代理)

1
ssh -D 1080 user@remote-host

这会在本地 1080 端口起一个 SOCKS5 代理,所有通过这个代理的流量都会从远端机器出去。相当于把远端机器当作一个出口节点。适用于你需要以远端机器的网络身份访问多个服务的场景。

三种转发方式对比

方式命令数据方向典型场景
Local -Lssh -L 本地:远端目标:远端端口本地 → 远端访问远端内网服务
Remote -Rssh -R 远端:本地目标:本地端口远端 → 本地暴露本地服务给远端
Dynamic -Dssh -D 本地端口本地 → 远端(动态)SOCKS 代理,访问多个远端服务

GCP 场景:没有公网 IP 怎么 SSH

等一下——前面说 VM 没有公网 IP,那 SSH 本身怎么连上去?

GCP 提供了 IAP (Identity-Aware Proxy) TCP Forwarding。当你执行:

1
gcloud compute ssh my-vm --zone=us-west1-a

实际发生的事情:

1
2
3
4
5
你的笔记本
    → gcloud CLI
        → IAP TCP 隧道 (走 HTTPS 到 Google 前端)
            → GCP 内部网络
                → VM 的 22 端口

IAP 隧道走的是 Google 的基础设施,不需要 VM 有公网 IP。它本质上是 Google 帮你做了一层”打洞”。

所以完整的 SSH 隧道命令是:

1
gcloud compute ssh my-vm --zone=us-west1-a -- -NL 18789:localhost:8080

-- 后面的参数会原样传给底层的 ssh 命令。数据流变成了:

1
应用 → localhost:18789 → SSH 客户端 → IAP 隧道 → GCP 内网 → VM SSH → localhost:8080 → 服务

三层嵌套:应用数据被 SSH 加密,SSH 流量又被 IAP 的 HTTPS 加密。

SSH 隧道的局限

SSH 隧道简单好用,但不是万能的。实际使用中会遇到几类问题:

1. 连接稳定性

SSH 隧道本质上是一个长连接的 TCP 会话。网络波动、笔记本休眠、WiFi 切换都会导致隧道断开。虽然可以用 autossh 自动重连,但终究是个 workaround。

2. 应用层兼容性

有些应用协议对底层传输有假设。比如某些 WebSocket 实现会校验连接的来源 IP 或者做设备签名验证。通过 SSH 隧道连接时,远端服务看到的来源是 127.0.0.1(因为是 SSH 服务端在本地发起的连接),这可能跟客户端直连时的行为不一致,导致握手失败。

3. 端口冲突

本地端口是独占的。如果之前有个 SSH 隧道进程没退干净(比如 Ctrl+Z 挂起了而不是 Ctrl+C 终止),新的隧道绑不上同一个端口:

1
bind [127.0.0.1]:18789: Address already in use

排查方法:

1
2
lsof -i :18789        # 找到占用端口的进程
kill <pid>             # 杀掉

4. 性能开销

每一个字节都要走 SSH 的加密/解密流程。对于高吞吐的场景(比如传大文件、视频流),SSH 隧道的开销不可忽略。

其他打洞方案

SSH 隧道是最轻量的方案,但不是唯一的。根据场景不同,还有这些选择:

给 VM 加公网 IP

最直接。GCP 里一条命令:

1
2
3
gcloud compute instances add-access-config my-vm \
  --zone=us-west1-a \
  --access-config-name="external-nat"

加完之后配好防火墙规则就能直连。优点是没有隧道中间层,延迟低、稳定。缺点是暴露了一个公网入口,需要额外的安全措施(防火墙规则、认证、TLS)。

反向代理

在已有公网入口的服务器上做反向代理,把特定路径或子域名转发到内网 VM:

1
用户 → nginx/caddy (公网) → 内网 VM:8080

优点是可以复用已有的 TLS 证书和域名,支持多个后端。缺点是多了一跳,而且反向代理需要能路由到 VM(也就是说代理服务器要和 VM 在同一个 VPC,或者通过 VPN 互通)。

WireGuard / Tailscale

组建一个虚拟的二层网络,让所有节点互相可达。适合长期的、多机器的内网互通需求。但对于”临时连一个端口”这种需求来说,配置成本偏高。

方案选择

方案适合场景配置成本稳定性
SSH 隧道临时调试、开发测试
公网 IP + 防火墙需要直连、低延迟
反向代理已有公网入口、多后端
WireGuard/Tailscale长期多机互通

总结

“打洞”的本质就是在两个不可达的网络之间找到或创造一条可达的路径。SSH 隧道之所以好用,是因为 SSH 几乎是所有 Linux 机器的标配——只要你能 SSH 上去,就能打洞。理解了 -L-R-D 三种转发模式的数据流方向,遇到内网穿透的需求就不会慌了。

本文由作者按照 CC BY 4.0 进行授权