远程控制场景下的协议应用 - 第4节
SSH 连接层协议:会话的骨架
上一讲我们聊了 SSH 的传输层,说白了就是怎么把数据加密扔到网线上。但光有加密通道还不够——你想想看,一个 SSH 连接里可能要同时跑好几个东西:你在敲命令、后台在传文件、还有个端口转发在跑。怎么把这些东西隔离开?这就是连接层协议要干的事。
连接层在传输层之上,负责建立、管理和复用多个逻辑通道。我习惯把它比作一个多路复用器:一条加密的 TCP 连接,上面可以跑 N 个独立的通道,每个通道干自己的事,互不干扰。
核心概念:SSH 连接层通过 Channel ID 来区分不同的逻辑通道。每个通道有独立的窗口控制和流量控制,一个通道卡住了不会影响其他通道。
通道的建立与关闭
通道的建立过程其实挺有意思。客户端和服务端之间通过全局请求和通道请求来协商。我简单梳理一下流程:
- 打开通道:客户端发送 SSH_MSG_CHANNEL_OPEN,带上通道类型(比如 session、direct-tcpip)和初始窗口大小。
- 服务端确认:如果服务端同意,回复 SSH_MSG_CHANNEL_OPEN_CONFIRMATION,带上服务端的通道 ID。
- 数据传输:双方通过 SSH_MSG_CHANNEL_DATA 发送数据,每个数据包都携带通道 ID。
- 关闭通道:任何一方都可以发送 SSH_MSG_CHANNEL_CLOSE,表示不再发送数据。收到对方的 CLOSE 后,再发送 SSH_MSG_CHANNEL_CLOSE 确认关闭。
嗯,这里要注意:通道关闭是半双工的。你发了 CLOSE 只是表示你不再发了,但还能收数据,直到对方也发 CLOSE。这个设计我在排查一个连接卡死的问题时深有体会——当时就是有一方发了 CLOSE 但没等对方确认就直接断开了 TCP,导致数据丢失。
流量控制:别让数据撑爆了
每个通道都有独立的窗口控制。初始窗口大小在打开通道时协商好,每发一段数据,窗口就减小一点。当窗口快用完时,接收方会发 SSH_MSG_CHANNEL_WINDOW_ADJUST 来增加窗口。
说白了就是:你发多少,得看我能不能接得住。我曾经在调试一个高并发远程控制工具时,发现通道窗口设得太小,导致数据传输频繁等待窗口调整,吞吐量上不去。后来把初始窗口从 64KB 调到了 1MB,性能立马就上来了。
实战建议:如果你在写 SSH 客户端或做二次开发,窗口大小别设太小。64KB 是默认值,但对于大文件传输或高延迟链路,建议至少 256KB 起步。
通道机制:SSH 的多任务核心
SSH 定义了多种通道类型,每种干不同的事。最常见的就两种:session 通道和直接 TCP/IP 转发通道。
| 通道类型 | 用途 | 典型场景 |
|---|---|---|
| session | 执行远程命令、启动 shell | ssh user@host 登录后敲命令 |
| direct-tcpip | 本地端口转发 | ssh -L 本地端口:目标:目标端口 user@host |
| forwarded-tcpip | 远程端口转发 | ssh -R 远程端口:目标:目标端口 user@host |
| x11 | 转发 X11 图形界面 | ssh -X user@host 运行 GUI 程序 |
每个通道内部还可以嵌套子通道?不,SSH 的通道是扁平的。但你可以同时打开多个 session 通道,每个通道对应一个 shell 或一个命令执行。这就是为什么你可以用 SSH 的 ControlMaster 功能复用连接——多个终端窗口共享同一条 TCP 连接,但各自走不同的通道。
避坑指南:我曾经遇到过一个案例,某运维工具同时打开了几百个 session 通道,结果服务端默认的最大通道数只有 100,后面的请求全部被拒绝。解决方案是在 sshd_config 里调大 MaxSessions 参数。记住,默认值不一定够用。
端口转发:SSH 的隧道魔法
端口转发是 SSH 最实用的功能之一,也是远程控制场景下的核心武器。说白了就是:把本地某个端口的数据,通过 SSH 加密隧道,转发到远程的某个目标。有三种模式:本地、远程、动态。
本地端口转发(-L)
这是最常用的模式。你在本地监听一个端口,所有发到这个端口的数据,都通过 SSH 隧道转发到远程的某个目标地址。
# 把本地的 8080 端口,转发到远程的 internal-web:80
ssh -L 8080:internal-web:80 user@jump-server
这个命令的意思是:你连上 jump-server,然后在 jump-server 上发起对 internal-web:80 的连接。数据流向是:
- 你的浏览器访问 localhost:8080
- SSH 客户端把数据加密,发给 jump-server
- jump-server 解密后,连接到 internal-web:80
- 响应原路返回
我在项目中经常用这个来访问内网的管理后台。公司内网有个 Grafana 面板,但只允许内网访问。我在笔记本上执行一条 -L 命令,就能在本地浏览器安全地看监控数据了。
远程端口转发(-R)
这个和本地转发正好相反。你在远程服务器上监听一个端口,所有发到那个端口的数据,都通过 SSH 隧道转发到你本地的某个服务。
# 让远程的 8080 端口,转发到本地的 3000
ssh -R 8080:localhost:3000 user@public-server
这个场景很实用:你本地开发了一个 Web 服务(端口 3000),想让远程的同事帮忙看看。你不需要部署到公网,一条 -R 命令就搞定了。数据流向是:
- 同事访问 public-server:8080
- public-server 把数据通过 SSH 隧道发给你的笔记本
- 你的 SSH 客户端把数据转发到 localhost:3000
- 响应原路返回
关键区别:-L 是「别人访问我本地,我去远程拉数据」;-R 是「别人访问远程,我去本地拉数据」。方向不同,但本质都是建立一条加密隧道。
动态端口转发(-D)
动态转发是最灵活的模式。它不指定目标地址,而是让你本地变成一个 SOCKS 代理服务器。任何支持 SOCKS 协议的应用程序,都可以通过这个代理访问远程网络。
# 在本地 1080 端口启动 SOCKS 代理
ssh -D 1080 user@jump-server
然后你在浏览器里设置 SOCKS 代理为 localhost:1080,所有流量都会通过 jump-server 转发。目标地址由浏览器决定,SSH 只负责加密传输。
这个功能我几乎天天用。出差时连上公司内网的跳板机,浏览器设个 SOCKS 代理,就能像在公司一样访问内网资源。而且所有流量都加密,在公共 WiFi 上也不怕被嗅探。
三种转发对比
| 类型 | 监听位置 | 目标地址 | 典型用途 |
|---|---|---|---|
| 本地转发 (-L) | 本地 | 远程可达的目标 | 访问内网服务 |
| 远程转发 (-R) | 远程 | 本地可达的目标 | 暴露本地服务到公网 |
| 动态转发 (-D) | 本地 | 由应用程序决定 | 全局代理、科学上网 |
小技巧:如果你需要长期保持端口转发,建议加上 -N 参数(不执行远程命令)和 -f 参数(后台运行)。比如:ssh -Nf -L 8080:internal-web:80 user@jump-server。这样隧道就在后台默默工作了。
SSH 连接层与端口转发的整体架构
为了让你更直观地理解这些概念之间的关系,我画了一张架构图。它展示了传输层、连接层、通道和端口转发之间的层次关系。
从这张图你可以看到,整个 SSH 协议栈是分层的。传输层负责加密和完整性,连接层在上面做多路复用,每个通道独立工作。端口转发本质上是利用了 direct-tcpip 或 forwarded-tcpip 通道类型,在通道内部建立了一条到目标地址的 TCP 连接。
安全提醒:端口转发虽然方便,但也可能被滥用。我曾经在渗透测试中遇到过,攻击者通过 SSH 远程转发把内网的服务暴露到公网,绕过了防火墙。作为运维人员,建议在 sshd_config 中设置 AllowTcpForwarding no 来禁用不必要的转发功能,或者用 PermitOpen 限制可转发的目标地址。