SSH 端口转发(隧道)功能强大、用途广泛,仅仅一行代码便可以将两台主机联系起来,对特定的访问请求进行代理。正确书写 SSH 命令是实现一切的基础。然而许多关于端口转发命令参数的介绍都是浅入深出:罗列名词却不加解释,给出实例却不讲如何举一反三。因而在此尝试整理一份更清晰的使用指引,不涉及原理。不足之处欢迎评论区交流。
一 基本连接
SSH 基本的连接命令是:
ssh username@hostname
这里牵扯到了两台主机,一是执行命令、运行 SSH 客户端的主机,我们称为本地主机 A【Host A】;二是接收连接请求、运行 SSH 服务器的主机,我们称为远程主机 B【Host B】。通过密码或密钥等方式验证后,SSH 连接建立,主机 A 可以使用命令行对主机 B 实施远程控制。
以上命令中,username 是主机 B 上已登录的用户名,hostname 则是主机 B 的设备名、域名或 IP 等可以在网络(局域网或互联网)上定位的名称。
端口转发涉及的主机较多,这是引起名词混乱的原因之一。在此不深究用词问题,仅以字母代表之。如无特殊说明,SSH 连接都建立在由主机 A 到主机 B 间,SSH 命令都在主机 A 上被执行。
二 本地端口转发
顾名思义,本地端口转发是将应用【application client】对于本地主机 A 指定端口 X 的访问请求转发给主机 B,交由主机 B 对另一指定主机 C 的指定端口 Z 发起访问。命令如下:
ssh -L 主机A端口X:主机C:主机C端口Z username@hostname
# 简单理解为:将对A:X的访问转变成对C:Z的访问
客户端在执行端口转发命令的同时,实际上也执行了基本的连接命令。多出来的部分中,「-L」旗标表示使用「本地端口转发」选项,之后是用冒号分隔开的三个需要指定的项。原理上,主机 C 可以是任何能够被主机 B 识别到的设备,也可以是主机 B 自身。
当主机 C 在其某端口提供某服务【application server】,主机 A 需要使用该服务却无法直接访问主机 C 或该端口时,如果发现有 SSH:A→B 的连接,且主机 B 能够直接访问主机 C 的该端口,本地端口转发就派上用场。
此时,访问请求在主机 A 一侧发生,可以来自于主机 A 自身,也可以是其他与 A 连接的设备。图中 Host A 或 Host B 的阴影指代主机 A 或主机 B 一侧的网络系统。
- 补充说明
实际上 ssh 本地端口转发命令的「-L」旗标后可以填写四个参数,完整格式为:
ssh -L [收听接口:]收听端口:目标主机:目标端口 username@hostname
命令中方括号内的部分,即第一个参数可以不写;它的默认值一般是 0.0.0.0(OpenSSH 客户端配置文件「ssh_config」中「GatewayPorts」选项的值一般为「yes」),意味着 SSH 隧道会收听所有接口,接受来自任何地址的应用访问请求并进行转发。而如果在此处填写了绑定地址(bind address),SSH 隧道连接就会只处理来自绑定地址的应用请求,而对其他地址发来的请求置之不理;如同在(真实世界的)隧道入口设立哨卡,只对白名单牌号的车辆放行。例如在此处填写 127.0.0.1,即可实现只有来自主机 A 本机的应用请求才被 SSH 隧道转发的效果。
需留意,收听接口是站在主机 A 的视角上去规定允许与 A 连接的设备,解决「能够使用 SSH 端口转发的应用请求从何处来」的问题,类似防火墙的入站;收听端口则依旧是主机 A 上的那个端口 X,不能够跑到别的主机上去。
类似地,远程端口转发和动态端口转发也具有「收听接口」这一可不指明的参数,下文不再赘述。从安全或控制流量的角度,规定绑定地址是一项实用的功能。
场景①
主机 B 与主机 C 处于同一内网中,主机 B 能够与外界联系而主机 C 不能。这时不处于内网中的主机 A 如果想要访问主机 C,就可以通过 SSH 连接主机 B+端口转发来进行。
台式机 B 上运行着虚拟机 C,虚拟机使用虚拟机软件搭建的虚拟网络与宿主主机 B 相连接,但在主机 B 以外无法直接访问该虚拟网络。想要通过 SSH,用与台式机 B 处于同一 WiFi 下的笔记本 A 来远程控制虚拟机 C,(在 A 上)执行端口转发命令:
ssh -L 22022:10.0.2.15:22 desktop_user@192.168.1.11 # cmd.1-1
其中,22022 号端口是随便选的一个没被占用的端口;192.168.1.11 是台式机 B 在 WiFi 中的 IP;desktop_user 是主机 B 上的用户名;10.0.2.15 是虚拟机 C 在主机 B 为其搭建的虚拟网络中的 IP;22 号端口是默认的 SSH 端口。已知 virtual_user 是虚拟机 C 上的用户名,这时在笔记本 A 上执行应用的访问请求命令:
ssh -p 22022 virtual_user@localhost # cmd.1-2
我们在笔记本 A 上以 SSH 协议访问本机(localhost)的 22022 号端口,这个请求就像通过了隧道(SSH 隧道)一样抵达台式机 B,台式机 B 则把这个请求变为对虚拟机 C 的 22 号端口的访问,并为 A 返回结果。其中,使用「-p」旗标是为了访问主机 A 的特定端口而不是 SSH 默认的 22 号端口;由于我们在主机 A 上执行命令,A 管自己叫 localhost,假如在其他主机上执行则需相应地改为主机 A 的域名或 IP 等他们对 A 的称呼。
cmd.1-2 中我们是将 SSH 当作普通应用使用的。参考 Fig.1,cmd.1-1 在 A 与 B 之间建立 SSH 隧道,此时 A 上的 SSH 客户端和 B 上的 SSH 服务器对应图中的 SSH Client 和 SSH Server;cmd.1-2 则表达应用的访问请求,此时 A 上的 SSH 客户端和 C 上的 SSH 服务器对应图中的 application client 和 application server。
以上 cmd.1-1 和 cmd.1-2 合起来实际是想(在 A 上)进行:
ssh -p 22 virtual_user@10.0.2.15 # cmd.1-3
当然,如果这 cmd.1-3 能被成功执行的话,就不需要端口转发了。
场景②
防火墙阻止了主机 A 对主机 B 一些端口的连接,但主机 B 仍有部分端口是对主机 A 开放的。这时主机 A 如果需要访问主机 B 上被防火墙阻挡的端口,就可以通过 SSH 连接主机 B+端口转发来进行。需注意,这时所谓的主机 C 就是主机 B。
某某云的云服务器 B 默认的防火墙设置仅开放了 22 号端口,其他入方向的访问都被屏蔽了。我们为云服务器 B 安装了桌面环境,现在想要在自己的计算机 A 上,通过 VNC 远程控制云服务器 B 的桌面。(在 A 上)执行端口转发命令:
ssh -L 5920:localhost:5901 cloud_user@server.example.com # cmd.2-1
因为 C 就是 B 自己,所以 C 的位置填 localhost;5920 随便选;5901 是云服务器 B 上 VNC 服务进程收听的端口;cloud_user 是 B 上的用户名;http://server.example.com 是 B 的域名,换成公网 IP 也行。
下面在计算机 A 上打开 RealVNC VNC Viewer(VNC 客户端),输入 VNC 服务器地址:
localhost:20
20=5920−5900,这是采用 5901 到 5999 之间端口时 RealVNC 的特殊设定。开始使用优雅(或许吧)的 GUI 来操作云服务器吧!
三 远程端口转发
当主机 C 在其某端口提供某服务,主机 B 需要使用该服务却无法直接访问主机 C 或该端口时,如果发现有 SSH:A→B 的连接,且主机 A 能够直接访问主机 C 的该端口,远程端口转发就派上用场。
需注意,此时访问请求在主机 B 一侧发生,而 SSH 连接的方向却没有变化,仍是由 A 到 B 的。因此「本地与远程端口转发互为镜像」的说法并不完全准确;严格意义上的镜像,SSH 连接也要变为由 B 到 A,那时则应该是在 B 上采用本地端口转发。可以看出,采取哪种端口转发主要取决于 SSH 连接建立的方向。
与本地端口转发的流动方向相反,远程端口转发是将对于远程主机 B 指定端口 Y 的访问请求转发给主机 A,交由主机 A 对另一指定主机 C 的指定端口 Z 发起访问。命令如下:
ssh -R 主机B端口Y:主机C:主机C端口Z username@hostname
# 简单理解为:将对B:Y的访问转变成对C:Z的访问
username@hostname 不变,因为我们仍然以从主机 A 对主机 B 发起 SSH 连接为基础;「-R」旗标表示使用「远程端口转发」选项,之后是用冒号分隔开的三个需要指定的项。原理上,主机 C 可以是任何能够被主机 A 识别到的设备,也可以是主机 A 自身。
场景③
主机 A 与主机 C 处于同一内网中,主机 A 能够与外界联系而主机 C 不能。这时(在主机 A 上)如果想让不处于内网中的主机 B 访问主机 C,就可以通过 SSH 连接主机 B+端口转发来进行。
台式机 A 上运行着虚拟机 C,虚拟机使用虚拟机软件搭建的虚拟网络与宿主主机 A 相连接,但在主机 A 以外无法直接访问该虚拟网络。想要通过 SFTP,用与台式机 A 处于同一 WiFi 下的笔记本 B 来向虚拟机 C 传输文件,(在 A 上)执行端口转发命令:
ssh -R 22122:10.0.2.16:22 laptop_user@192.168.1.233 # cmd.3-1
其中,22122 号端口是随便选的一个没被占用的端口;192.168.1.233 是笔记本 B 在 WiFi 中的 IP;laptop_user 是主机 B 上的用户名;10.0.2.16 是虚拟机 C 在主机 A 为其搭建的虚拟网络中的 IP;22 号端口是默认的 SFTP 端口。已知 virtual_user 是虚拟机 C 上的用户名,这时在笔记本 B 上执行应用的访问请求命令:
sftp -P 22122 virtual_user@localhost # cmd.3-2
请注意这是一条运行在 B 上的应用命令;B 上的 SFTP 客户端这时充当 Fig.2 中的 application client。此处 localhost 是主机 B 对自己的称呼。对 B 的 22122 号端口的访问被转发至 A,A 访问 C,即 10.0.2.16 的 22 号端口并将结果返回给 B。于是 B 就通过远程端口转发成功访问了 C 上的 SFTP 服务器。
以上 cmd.3-1 和 cmd.3-2 合起来实际是想(在 B 上)进行:
sftp -P 22 virtual_user@10.0.2.15 # cmd.3-3
当然,这 cmd.3-3 也是不能被直接成功执行的。
场景④
处于内网之中的主机 A 可以访问公网,但不具有公网 IP;公网中的主机 B 无法找到 A,但为 A 开放各个端口的访问(A 可以直接连接 B,反之则不行)。这时 A 想要让 B 访问自己,就可以通过 SSH 连接主机 B+端口转发来进行。需注意,这时所谓的主机 C 就是主机 A。
注意:OpenSSH 服务器对于远程端口转发的设定,默认只接受远程主机 B 本机上的应用发起的请求。想要从其他连接到 B 的设备发起请求,需将「sshd_config」文件中「GatewayPorts」选项后的「no」修改为「yes」。
手头上计算机 A 运行着 http 服务,但 A 没有公网 IP,其他设备不能使用该服务。恰好云服务器 B 有公网 IP(甚至域名),便于被访问。在不将 http 服务迁移至云服务器 B 的前提下,可以使用 SSH 端口转发使其他设备通过访问 B 的方式访问 A 上的 http 服务。(在 A 上)执行端口转发命令:
ssh -R 80:localhost:80 cloud_user@server.example.com # cmd.4-1
这时 C 便是 A 自己(localhost);80 号端口是 http 默认端口,为简便两个都用默认;cloud_user 还是 B 上的用户名;http://server.example.com 还是 B 的域名。
接下来在其他设备上打开浏览器,输入地址:
http://server.example.com/
于是大家可以通过访问 http://server.example.com 来访问本地计算机 A 提供的 http 服务了。
四 动态端口转发
动态端口转发可以把本地主机 A 上运行的 SSH 客户端转变成一个 SOCKS 代理服务器;实际上它是一种特殊的本地端口转发,或者说叫它「动态本地端口转发」更科学。这个动态,就动在这种转发不规定目标地址(主机 C)和目标端口(端口 Z),而是去读取应用发起的请求,从请求中获取目标信息。
ssh -D 主机A端口X username@hostname
好像很强,但有一个问题:之前使用固定的端口转发时,应用的访问请求都是指向被转发的那个端口 X 的,但现在应用的访问请求必须指向目标,以指定动态端口转发的目标。可如果不指向端口 X,如何让数据走 SSH 隧道呢?这就要求我们在系统或应用(浏览器等)中设置一个使用 SOCKS5 协议、服务器为 localhost、端口为 X 的代理,利用代理使请求走端口 X。
这样应用的请求就从 X 进入隧道,抵达 B 后其中的目标信息被解析出来,B 访问目标后再将结果通过隧道返回给 A。比如在开启代理的 A 上的浏览器中访问 http://zhihu.com,经过端口转发,相当于是 B 在帮 A 访问 http://zhihu.com。
五 端口转发的停止
SSH 端口转发完全基于基本的 SSH 连接,因此,通过在远程终端上执行 exit 命令、暴力关闭本地终端窗口、远程主机 B 关机、本地主机 A 关机等可以切断 SSH 连接的方式,即可停止 SSH 端口转发。就是这样。
上次修改於 2019-11-14