远程控制场景下的协议应用 - 第4节

SSH 连接层协议:会话的骨架

上一讲我们聊了 SSH 的传输层,说白了就是怎么把数据加密扔到网线上。但光有加密通道还不够——你想想看,一个 SSH 连接里可能要同时跑好几个东西:你在敲命令、后台在传文件、还有个端口转发在跑。怎么把这些东西隔离开?这就是连接层协议要干的事。

连接层在传输层之上,负责建立、管理和复用多个逻辑通道。我习惯把它比作一个多路复用器:一条加密的 TCP 连接,上面可以跑 N 个独立的通道,每个通道干自己的事,互不干扰。

核心概念:SSH 连接层通过 Channel ID 来区分不同的逻辑通道。每个通道有独立的窗口控制和流量控制,一个通道卡住了不会影响其他通道。

通道的建立与关闭

通道的建立过程其实挺有意思。客户端和服务端之间通过全局请求和通道请求来协商。我简单梳理一下流程:

  1. 打开通道:客户端发送 SSH_MSG_CHANNEL_OPEN,带上通道类型(比如 session、direct-tcpip)和初始窗口大小。
  2. 服务端确认:如果服务端同意,回复 SSH_MSG_CHANNEL_OPEN_CONFIRMATION,带上服务端的通道 ID。
  3. 数据传输:双方通过 SSH_MSG_CHANNEL_DATA 发送数据,每个数据包都携带通道 ID。
  4. 关闭通道:任何一方都可以发送 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 的连接。数据流向是:

  1. 你的浏览器访问 localhost:8080
  2. SSH 客户端把数据加密,发给 jump-server
  3. jump-server 解密后,连接到 internal-web:80
  4. 响应原路返回

我在项目中经常用这个来访问内网的管理后台。公司内网有个 Grafana 面板,但只允许内网访问。我在笔记本上执行一条 -L 命令,就能在本地浏览器安全地看监控数据了。

远程端口转发(-R)

这个和本地转发正好相反。你在远程服务器上监听一个端口,所有发到那个端口的数据,都通过 SSH 隧道转发到你本地的某个服务。

# 让远程的 8080 端口,转发到本地的 3000
ssh -R 8080:localhost:3000 user@public-server

这个场景很实用:你本地开发了一个 Web 服务(端口 3000),想让远程的同事帮忙看看。你不需要部署到公网,一条 -R 命令就搞定了。数据流向是:

  1. 同事访问 public-server:8080
  2. public-server 把数据通过 SSH 隧道发给你的笔记本
  3. 你的 SSH 客户端把数据转发到 localhost:3000
  4. 响应原路返回

关键区别:-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 连接层与端口转发架构图 传输层:加密 TCP 连接 连接层:多路复用、通道管理、流量控制 逻辑通道层 Session 通道 direct-tcpip 通道 forwarded-tcpip 通道 其他通道 端口转发应用层 本地转发 (-L) 远程转发 (-R) 动态转发 (-D) 说明:传输层提供加密通道,连接层在其上复用多个逻辑通道,端口转发则利用特定通道类型实现隧道功能

从这张图你可以看到,整个 SSH 协议栈是分层的。传输层负责加密和完整性,连接层在上面做多路复用,每个通道独立工作。端口转发本质上是利用了 direct-tcpip 或 forwarded-tcpip 通道类型,在通道内部建立了一条到目标地址的 TCP 连接。

安全提醒:端口转发虽然方便,但也可能被滥用。我曾经在渗透测试中遇到过,攻击者通过 SSH 远程转发把内网的服务暴露到公网,绕过了防火墙。作为运维人员,建议在 sshd_config 中设置 AllowTcpForwarding no 来禁用不必要的转发功能,或者用 PermitOpen 限制可转发的目标地址。