SSH 隧道:原理、方法与实践

技术 · 约 13 分钟

本文基于一次真实的生产部署整理:一台香港服务器 + 一台家庭服务器(无公网 IP), 用一条 SSH 隧道同时实现了「家庭出口走香港 IP」和「外网经香港 IP 访问家庭内网服务」。


一、SSH 隧道是什么

SSH 隧道(SSH Tunneling)就是把任意网络流量塞进一条已经建立好的 SSH 连接里传输。

SSH 协议本身支持多个”通道”(channel),这些通道在一条加密的 TCP 连接里多路复用。 隧道的本质,就是让远端的 sshd 进程充当”流量搬运工”:

  • 它天然加密——流量在 SSH 的加密层里传输,外部看到的只是普通的 SSH 流量
  • 它只需要一个端口——SSH 通了,隧道就通,不需要开放任何额外端口
  • 这两点让它成为”端口受限环境”下的穿墙利器
┌────────┐    加密的 SSH 连接(TCP)    ┌────────┐
│ 客户端  │ ═══════════════════════════▶│ sshd   │
│  ssh   │◀─────────────────────────── ═│ 服务器 │
└────────┘      通道多路复用             └────────┘

二、四种隧道类型

1. 本地转发 -L:把远端端口”搬到”本地

ssh -L [本地地址:]本地端口:目标地址:目标端口 user@server

原理:ssh 在本地监听一个端口;访问这个端口的流量经 SSH 连接送到远端, 由远端 sshd 转发给”目标地址:目标端口”。

你 ──▶ 本地:8080 ──┐         ┌──▶ 数据库:3306
                   ║ SSH 加密 ║
                sshd ────────┘

典型场景:访问跳板机背后的内网服务。

# 通过跳板机访问内网数据库,本地直接连 localhost:3306
ssh -L 3306:10.0.0.5:3306 user@jumpserver

关键细节:目标地址 是由远端 sshd 解析和发起连接的, 所以填 localhost 指的是远端自己,不是你的本机。

2. 远程转发 -R:把本地端口”发布”到远端

ssh -R [远端地址:]远端端口:目标地址:目标端口 user@server

原理:与 -L 正好相反——sshd 在远端监听端口, 访问远端端口的流量经隧道送回本地,由本地 ssh 转发给目标。

访客 ──▶ 服务器:8080 ──┐         ┌──▶ 你本机的服务:3000
                      ║ SSH 加密 ║
                   ssh ◀────────┘

典型场景:内网机器(无公网 IP)主动连出去,把自己的服务发布到公网服务器上。 这就是”内网穿透”最朴素的形态。

# 在家里的机器上执行,把家里的 Web 服务挂到公网服务器的 8080
ssh -R 8080:localhost:3000 user@公网服务器

注意:-R 挂的是”端口级”转发,只支持 TCP 应用层流量; 公网服务器若要把端口开放给其他访客,sshd 需要 GatewayPorts yes。

3. 动态转发 -D:随身 SOCKS5 代理

ssh -D [本地地址:]本地端口 user@server

原理:本地 ssh 监听的端口是一个 SOCKS5 代理, 每个经过的应用层请求由远端 sshd 代为发起。流量按”会话”动态决定目标。

# 建立代理后,浏览器/支持 SOCKS5 的程序指向 localhost:1080
ssh -D 1080 user@server

典型场景:临时科学上网、让指定程序走远端网络出口。 相当于一条”按需建立”的隧道集合。

4. 三层隧道 -w:把 SSH 变成一条”虚拟网线” ⭐

ssh -w 本地TUN编号:远端TUN编号 user@server

原理:与上面三种本质不同——-L/-R/-D 是应用层端口转发(sshd 代替你连接目标), 而 -w 会在两端各创建一个 TUN 虚拟网卡,把 SSH 变成一条点对点的 IP 层(三层)隧道。两端可以通过虚拟 IP 互访,配合路由和 NAT 还能转发任意流量。

┌─────────────┐                        ┌─────────────┐
│  客户端      │                        │  服务器      │
│  10.200.0.2 │ ════ SSH 加密 ════════▶ │  10.200.0.1 │
│    [tun0]   │      TUN 隧道           │    [tun0]   │
└──────┬──────┘                        └──────┬──────┘
       │ ip route                              │ iptables NAT
   改默认路由                            转发到公网 / 内网服务

这是最强大的形态:ICMP、UDP、任意端口全都能跑, -L/-R 做不到的事(如整段子网路由、非 TCP 协议)它都能做。

前提条件:

  • 两端都要有 /dev/net/tun(modprobe tun)
  • sshd 开启 PermitTunnel yes(默认是 no)
  • 需要内核开启 IP 转发(net.ipv4.ip_forward=1)才能跨网卡路由

三、进阶原理

TCP-over-TCP 问题

SSH 隧道本身跑在 TCP 上,如果隧道里再装 TCP 流量,就出现 TCP 套 TCP:

  • 内层 TCP 丢包重传时,外层 TCP 也在丢包重传,重传被放大
  • 网络质量差时性能急剧劣化(缓慢、卡顿)

这是 SSH 隧道(以及任何 TCP 承载的隧道)的先天短板。 WireGuard 等基于 UDP 的隧道没有这个问题——这也是为什么网络条件允许时优先选 WireGuard。

MTU 注意事项

隧道会封装加头,可用 MTU 变小。TUN 隧道通常设 1380~1400, 太大导致分片或黑洞(表现为”能 ping 通但大网页打不开”),太小浪费带宽。

保活与自动重连

SSH 连接断了隧道就断。生产环境必备三件套:

# ~/.ssh/config 客户端保活
ServerAliveInterval 15      # 每 15 秒发心跳
ServerAliveCountMax 4       # 4 次无响应判定断线

程序级方案:autossh(自动重连 ssh),或用 systemd 服务 + 循环脚本自行守护。


四、实战:香港服务器 IP 中转家庭 NAS(完整案例)

注:文中 203.0.113.10 为文档专用示例 IP(RFC 5737),非真实地址,已替换脱敏。

需求

  • 家庭服务器(内网 192.168.1.202,无公网 IP)
  • 香港服务器(公网 203.0.113.10,SSH 在非标端口 5079)
  • 目标①:家庭服务器的出站流量走香港 IP
  • 目标②:外网访客通过香港 IP 访问家庭内网的 Web 服务

架构总览

                互联网
                  │
      ┌───────────┴───────────┐
      │  香港服务器 203.0.113.10  │◀──── 外网访客
      │  sshd(:5079) PermitTunnel │      http://203.0.113.10:5666
      │  tun0: 10.200.0.1       │                │
      │  iptables DNAT 5666───┐ │                │
      └──────────────┬────────┼─┘                │
                SSH 加密隧道   │ (转发)           │
                (TCP 5079)    │                  │
      ┌──────────────┴────────▼──────────────────┘
      │  家庭服务器 (内网)
      │  tun0: 10.200.0.2
      │  默认路由 → 10.200.0.1        ← 出站走香港
      │  飞牛OS :5666 等 Web 服务
      └──────────────┬──────────────
                  家用局域网

第 1 步:香港端开启隧道支持

# 加载 TUN 模块
modprobe tun

# /etc/ssh/sshd_config 中开启
PermitTunnel yes

# 重启 sshd(注意别把自己锁外面,先开个备用会话)
systemctl restart sshd

第 2 步:家庭端免密登录

ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519
ssh-copy-id -p 5079 root@203.0.113.10
# 香港端确认 PubkeyAuthentication yes(有些镜像默认关闭)

第 3 步:建立三层隧道(核心命令)

家庭端执行:

ssh -w 0:0 -p 5079 root@203.0.113.10 \
    'ip addr add 10.200.0.1/30 dev tun0; ip link set tun0 up; \
     ip forward 打开; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE'
# 本地脚本随后配置:
ip addr add 10.200.0.2/30 dev tun0
ip link set tun0 mtu 1380 up
ip route change default via 10.200.0.1    # 默认路由切进隧道

要点:

  • -w 0:0:两端各用 0 号 TUN 设备
  • 隧道内网用 /30(10.200.0.1 ↔ 10.200.0.2)或 /24 均可
  • SSH 端点的路由要固定走原网关(否则隧道自己套自己,瞬间断网): ip route add 203.0.113.10 via <原网关>
  • 香港端开 net.ipv4.ip_forward=1 + MASQUERADE,家庭流量即可从香港 IP 出去

第 4 步:入站——香港端口转发进家庭

在香港端用 iptables DNAT,把公网端口”投递”进隧道对端:

iptables -t nat -A PREROUTING -p tcp --dport 5666 -j DNAT --to-destination 10.200.0.2:5666
iptables -I FORWARD 1 -d 10.200.0.2 -p tcp --dport 5666 -j ACCEPT
iptables -I FORWARD 1 -s 10.200.0.2 -j ACCEPT
iptables -t nat -A POSTROUTING -d 10.200.0.2 -p tcp --dport 5666 -j MASQUERADE

外网访客访问 http://203.0.113.10:5666 → DNAT 改写目标为 10.200.0.2 → 经 tun0 隧道送到家庭服务器 → 飞牛OS 响应原路返回。

第 5 步:守护与持久化

两端各一个 systemd 服务:

位置服务职责
家庭端hk-tunnel.service循环执行 ssh -w,断线重连;停止时恢复原路由
香港端home-relay.service开机重放 DNAT 转发规则(幂等脚本)

家庭端守护脚本骨架:

#!/bin/bash
GW=$(ip route show default | awk '{print $3}')   # 备份原网关
ip route add 203.0.113.10 via $GW               # SSH 端点走原路
while true; do
    ssh -w 0:0 -p 5079 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 \
        -o StrictHostKeyChecking=no root@203.0.113.10 \
        '远端初始化命令'
    # ssh 退出(断线)后循环重试
    sleep 5
done

五、方法选型速查

需求方案
临时访问远端/内网某个端口-L 本地转发
把内网服务发布到公网服务器(单端口)-R 远程转发
让浏览器等程序走远端出口-D 动态转发(SOCKS5)
两台机器组网、互访任意端口/子网、双向流量-w 三层隧道
高性能长期组网、UDP 载荷WireGuard(UDP 承载,无 TCP 套 TCP 问题)
免开端口、要域名和 HTTPS、有 Cloudflare 账号Cloudflare Tunnel

SSH 隧道的最佳定位:端口受限(只有 SSH 通)、临时性、或不想在两端装任何额外软件的场景。 一条 SSH 能干的事,就别引第三方。


六、常见坑

  1. PermitTunnel 默认是 no——-w 报 command not allowed 先查它
  2. 隧道递归——把默认路由切进隧道前,必须给 SSH 服务器 IP 加一条走原网关的静态路由
  3. PubkeyAuthentication no——不少云镜像默认关闭公钥登录,守护脚本必须免密
  4. MTU 黑洞——小包(ping/ssh)通、大包(网页/大文件)卡,九成是 MTU 没降
  5. iptables 规则不持久——重启即失,务必做成 systemd oneshot 服务重放
  6. nc/超时测试的假象——验证端口通不通,要用真实 HTTP 请求走一个来回, 单纯 TCP 握手成功(尤其经过代理时)不代表数据能通
阳熙

Java / 保险业务系统开发 · 无锡

← 返回文章列表

评论

还没有评论,来留第一条吧。

首次评论会做人机校验