啊哈!SSH 解锁远程开发新技能
一直以来,我都将 SSH 作为一个简单的远程登录工具使用,除此之外,从未想过它还能做些什么。直到最近,我在 Ubuntu 上开发一个 Web 应用时,使用了 VSCode 的 Remote-SSH 扩展,这使我能够从 Windows 桌面远程开发 Ubuntu 上的程序。这个功能为我的开发工作提供了极大的便利,更让我惊讶的是,我可以直接在 Windows 本地访问仅在 Ubuntu 上提供的本地 Web 服务。
后来我了解到这是 SSH 端口转发的一种应用。这次偶然的发现,让我重新审视这个看似古老但至今仍被广泛使用的协议,并希望对其有更深入的了解。
SSH 基本原理
SSH(Secure Shell)是一种网络安全协议,通过加密和认证机制提供安全的访问和文件传输服务。SSH 最常用的场景是远程登录和文件传输。在 SSH 出现之前,远程登录和网络设备管理主要使用 Telnet。但 Telnet 的一个问题是数据明文传输,存在极大的安全风险。为了解决这一问题,SSH 使用了加密和认证机制,实现了安全的远程登录。
SSH 基于客户端-服务器架构,用户所在的系统是客户端,所管理的远程系统是服务端。为了建立安全的 SSH 通道,双方需要先建立 TCP 连接,协商使用的协议版本和加密算法,并生成相同的会话密钥以进行后续的对称加密。用户认证完成后,双方即可建立会话并进行数据交换。
除了安全的远程登录和文件传输外,SSH 还提供了强大的端口转发功能,这使得它在网络管理和安全领域有着广泛的应用。
SSH 隧道/端口转发
SSH 端口转发1,也称为 SSH 隧道,是通过 SSH 协议将网络流量加密后转发到另一台计算机的技术。SSH 端口转发分为三种主要类型:本地端口转发、远程端口转发和动态端口转发。
本地端口转发
本地端口转发适用于某个服务仅仅监听本机地址或私有接口,但用户只能通过 SSH 访问该机器的情况。例如:
- 期待使用数据库界面管理工具来管理数据库
- 想要在浏览器中查看某个仅在本地运行的远程服务
- 想要访问某个未暴露公共地址的容器
这些问题都可以通过以下的命令来解决:
ssh -L [local_addr:]local_port:remote_addr:remote_port ssh_serv_addr
local_addr
: 本地地址,如果不指定,默认为localhostlocal_port
: 本地监听端口remote_addr
: 服务所在地址remote_port
: 服务暴露的接口ssh_serv_addr
: SSH 服务器地址
示例场景
假设在服务器端运行以下命令,启动一个监听本地地址和 8000 端口的 Web 服务:
python3 -m http.server --bind 127.0.0.1 8000
直接从 client 端无法访问这个 Web 服务。
在 client 端执行以下命令,输入口令后建立 SSH 隧道:
ssh -L 9999:127.0.0.1:8000 user@10.0.21.8
该命令执行后,SSH 会建立一个从本机到服务器的 SSH 隧道,同时监听本机的 9999 端口。通过 curl 127.0.0.1:9999
访问本机的 9999 端口时,数据会通过 SSH 隧道传输到 server 端的 SSH 服务器,server 端的 SSH 服务器再将数据转发给 Web 服务器的 8000 端口。

通过这种方式,client 端可以通过仅开放的 SSH 端口访问 server 端仅提供本地服务的程序。
这里不要求 remote 地址和 ssh server 在同一台机器上,二者可以不同:
ssh server 的角色类似于跳板机或者堡垒机,由于安全策略的限制,remote 端只可以通过 server 访问。所以,client 要想访问 remote 端的服务,就要借助 SSH 本地端口转发实现。

Web server 和 SSH server 位于不同机器上的时候,创建 SSH 本地端口转发有所变化。
区别是 remote_addr 变为了 Web server 所在的独立地址 “10.0.21.7”,即
ssh -L 9999:10.0.21.7:8000 user@10.0.21.8
虽然这里给出的例子中 server 和 remote 在同一个网段,但只要 server 能够与 remote 通信,本地端口转发就能正常工作。
远程端口转发
远程端口转发使 SSH 连接的服务器端应用程序能够访问驻留在 SSH 客户端的服务。简单来说,远程端口转发允许用户从隧道的服务器端连接到位于隧道客户端的网络服务。这种方式常用于将内网服务通过公开网关暴露给外部访问。
例如,当需要将某台内网服务器上的服务暴露给外部所有人访问时,可以利用远程端口转发。这时,需要一个公开的网关服务器,将远程服务器端口转发到本地机器的指定端口可以通过以下命令实现:
ssh -R [remote_addr:]remote_port:local_addr:local_port [user@]gateway_addr
和本地端口转发相比,远程端口转发交换了两端的地址和端口位置。同时,需要在服务器端的 SSH 配置文件中将 GatewayPorts 选项设置为 yes。

这里位于内网的 Web server 希望为组织内所有人提供访问服务,通过命令 ssh -R 10.0.21.8:9999:127.0.0.1:8000 user@10.0.21.8
建立一个 SSH 隧道实现远程端口转发。这样,组织内所有人可以通过访问网关接口来访问内网的 Web 服务。
使用远程端口转发时,需要知道目标服务器(即隧道客户端)的地址和端口号,端口号取决于提供服务的应用程序。
远程端口转发的应用场景包括:
- 安全地公开本地服务:如果希望网络上的其他人可以访问本地服务,通过将远程服务器上的端口转发到本地计算机上的特定端口,只有授权用户才能访问远程服务器。
- 访问内部网络资源:当在远程工作并需要访问组织内部网络上的资源时,可以使用 SSH 远程端口转发建立从组织网关到内部网络服务器的隧道,从而安全地连接到这些内部资源。
- 将开发中服务公开用于产品演示:这是常用的远程端口转发场景之一。
和本地端口转发类似,SSH client 和 local server 也可以在不同机器上。

如何区分本地端口转发和远程端口转发?以下是我的理解:
谁来执行 SSH 命令,谁就是客户端角色,另一端就是服务器角色。如果请求从客户端进入隧道,则为本地端口转发;如果请求从服务器端进入隧道,则为远程端口转发。远程端口转发的实现允许从任意位置访问指定的服务,为各种网络拓扑提供了灵活的解决方案。
动态端口转发
动态端口转发旨在使客户端能够安全地连接到受信任的服务器,这些服务器充当中介,以便向一个或多个目标服务器发送或接收数据。
SSH 动态端口转发是通过将 SSH 客户端设置为 SOCKS 代理服务器来实现。可以配置 Web 浏览器使用 SOCKS 代理服务器进行通信。该代理服务器充当通往另一台服务器的安全隧道的一端。一旦连接建立,动态端口转发就能够为连接到不受信任网络的用户提供额外的安全性。由于数据必须通过安全隧道到达另一台服务器,然后才能转发到其原始目的地,因此可以保护用户免受局域网中可能发生的数据包嗅探的威胁。
那些通过不受信任的网络(如咖啡店、车站或其他安全性较低的网络)连接到互联网的用户可以考虑使用动态端口转发来保护数据。

如图所示,通过以下命令,创建一个 SOCKS 代理服务器。
ssh -D localhost:8000 user@remote_addr
在 Edge 浏览器安装 “SwitchOmega” 扩展,将浏览器配置为使用 localhost:8000 作为 SOCKS 代理服务器,从而通过远程服务器代理访问互联网。该命令执行完成后 SOCKS 代理服务器会监听本机的8000端口,当浏览器通过 SwitchOmega 发送请求时,数据会被 SOCKS 代理服务器接收,然后经由 SSH 隧道传输到 server 服务器的 SSH server,server 服务器的 SSH server 再将数据解包,帮我们将数据转发到对应的目标地址。
SSH 端口转发应用
这里介绍两个我在开发过程中使用到的 SSH 端口转发应用,这二者极大的方便了我的开发过程。也是因为遇到了这个应用,让我对 SSH 端口转发功能产生了兴趣。
Remote-SSH 扩展
VSCode 中提供了 Remote-SSH2扩展,让我能够在本地(Windows)直接编写、调试运行在远端(Ubuntu)的代码,这使得 VSCode 提供了不错的本地开发体验。

如图所示,这主要得益于 Remote-SSH 在本地和远端之间建立了一个 SSH 隧道。当使用 Remote-SSH 连接远端的时候,会自动在远端下载安装 VSCode Server。由 VSCode Server 提供对代码编辑、执行的支持。
不仅如此,如果远端开发的是网络服务,Remote-SSH 会自动为远端服务端口映射一个本地端口,这样在本地端就可以直接访问原本需要在远端本地访问的内容。

这里的 Port 就是远端开放的本地端口,Forwarded Address 就是 Remote-SSH 创建的本地转发地址和端口。从行为上看,Remote-SSH 会优先保持二者端口一致,但如果本地端端口被占用了,它就会选择另一个有效的端口。而我可以直接使用这个 Forwarded Address 在本地的浏览器访问远端拉起的服务。
有了这个贴心的功能,就不需要我在外部手动执行 ssh -L 9999:127.0.0.1:9999 sboy@10.0.21.150
的命令来设置本地端口转发了。
X11 转发
在开发过程中,我遇到过两种需要利用 X11 转发来帮助工作的场景。
第一种场景是开发机上安装了虚拟机,但由于硬件资源有限,使用桌面版的 Ubuntu 会导致系统卡顿,因此选择了 server 版本。然而,有时需要运行需要图形环境的程序,如使用 matplotlib 绘制数据分析结果或运行其他需要图形界面的程序。如果在 server 上运行这些程序,不可避免地会遭遇失败。
第二种情况是有一台独立的物理机可以安装 Ubuntu 桌面版,但这台机器离我很远。如果每次遇到涉及图像显示的程序都要跑过去运行调试,效率会非常低。现在,很多时候为快速验证一个功能,会采用云主机。这时候就更需要在必要的时候能够将远程的图形信息显示在本地。
这里我用到了 VcxSrv3 作为 X11 server,配合 SSH 的转发功能,就能够实现将远端的图形界面转发至本地端显示。它的底层实现也利用了 SSH 的端口转发。

工作过程:
- 建立 SSH 连接:首先,在本地计算机上使用 putty 工具建立与远程服务器的 SSH 连接。
- 启用 X11 转发:在建立 SSH 连接时,勾选 putty 工具中的"启用 X11 转发功能"。这会指示 SSH 客户端将来自远程服务器的 X11 流量转发到本地 X11 服务器(VcXsrv)。
- 本地 X11 服务器处理:本地 VcXsrv 服务器会接收来自 SSH 客户端转发的 X11 流量,并解析这些数据以呈现图形界面。
使用 X11 转发技术后,无论是在开发机上运行虚拟机,还是在远程物理机或云主机上运行需要图形界面的程序,都可以方便地将这些图形界面安全地转发到本地显示,大大提高了工作效率和便利性。
小结
SSH 不仅仅是一个用于远程登录的工具,它所支持的三种端口转发方式——本地端口转发、远程端口转发和动态端口转发——在日常开发工作中提供了极大的便利。例如,通过本地端口转发,可以安全地访问远程服务器上的本地服务;通过远程端口转发,可以将本地服务暴露给外部访问;而动态端口转发则可以通过 SOCKS 代理保护用户在不安全网络中的数据传输。这些功能极大地扩展了 SSH 的应用范围,使其在开发和运维中成为不可或缺的工具。
而且,自 Windows 1809 版本起,Windows 系统内置了 OpenSSH4,这对开发者而言意义重大。首先,它增强了 Windows 对 WSL(Windows Subsystem for Linux)的支持,使开发者能够在 Windows 上无缝地开发和运行 Linux 应用。其次,对于需要在 Windows 上进行开发并在 Linux 上部署运行的开发模式,内置的 OpenSSH 提供了便利和高效的解决方案。个人理解,这一改变不仅体现了 SSH 本身的重要性,也反映了跨平台开发和操作系统互操作性的趋势。总之,SSH 在现代开发环境中扮演了至关重要的角色,其广泛应用为远程开发提供了更多的灵活性和效率。