SSH Tunnel (Port Forwarding)

Table of Contents

1. SSH Tunnel

SSH 往往工作在 TCP 之上,SSH Connection 中的数据是加密后传递的。SSH 常常用于登录远程服务器,不过本文将介绍 SSH 的另外一个强大功能——端口转发,使用这个功能可以对其它应用程序的 TCP 连接进行加密。 比如,Telnet, SMTP, NNTP, IMAP 等协议是不安全的,利用 SSH 的端口转发功能可以使它们变得安全。由于 SSH Connection 提供了一个安全的隧道,以使其它应用程序的 TCP 连接可以通过, 端口转发又被称为隧道(tunnel)。

SSH 中端口转发有 Local Port Forwarding (-L), Remote Port Forwarding (-R), Dynamic Port Forwarding (-D),后面将介绍前面两个。

参考:
SSH, The Secure Shell: The Definitive Guide, 9.2 Port Forwarding

2. Local port forwarding (-L)

假设 Host B 上有一个服务程序(提供服务的 TCP 端口为 W),Host A 的客户端直接连接它时的示意图如 1 图所示。

ssh_direct_conn.gif

Figure 1: 直接连接

注意,在这种方式下,Host A 和 Host B 之间数据传送是不安全的(未加密的)。 我们利用 SSH 的端口转发功能,可以使 Host A 和 Host B 之间数据传送变得安全 ,如图 2 所示,当隧道建立好后 Host A 的客户端连接“Host A 上的端口 P”就相当于连接上了“Host B 上的端口 W”。注:应用服务器启用 TLS 也可以为 TCP 提示安全的连接,这里不讨论它。

ssh_port_forward.gif

Figure 2: 通过 SSH 隧道进行连接

如何建立图 2 所示的 SSH 隧道呢?这可以通过 ssh 的 -L 选项来实现端口转发功能。

下面假设端口 W 为 80,而端口 P 为 8080。首先你需要有一个可以登录 Host B 的 ssh 账号(假设用户名为 user1),然后在 Host A 上执行下面命令即可:

$ ssh -L 8080:HostB:80 user1@HostB
$ ssh -L 8080:localhost:80 user1@HostB   # 当ssh服务器和HostB是同一台机器时,这样写也行

执行上面命令时可能(如果你没有配置私钥登录方式的话)提示你输入 user1 在 HostB 上的密码。当 ssh 连接建立后,客户端连接“Host A 上的端口 8080”就相当于连接“Host B 上的端口 80”了,而且 Host A 和 Host B 之间的数据传送是安全的。

执行上面命令有两个功能:一是获得了 Host B 上一个交互式 shell(可执行远程命令),二是开启了端口转发功能。有时,我们不需要执行远程命令,仅仅想使用其端口转发功能。通过 -N 可以放弃执行远程命令:

$ ssh -N -L 8080:localhost:80 user1@HostB       # -N 表示不执行远程命令,不会返回交互式shell了

上面命令会在前端执行,使用 & 或者 ssh 的选项 -g 可以把它放入后台执行:

$ ssh -N -L 8080:localhost:80 user1@HostB &    # 命令最后加上 & 可把它放入后台
$ ssh -f -N -L 8080:localhost:80 user1@HostB   # -f 也可以把ssh放入后台执行,它比 & 更可靠

2.1. Forwarding Off-Host(可能涉及 4 台机器)

前面描述的端口转发中,应用程序服务器和 ssh 服务器在一个机器上,而应用程序客户端和 ssh 客户端在一个机器上,所以只涉及 2 台机器。事实上,一次转发可能涉及 4 台机器,如图 3 所示。

ssh_off_host_forward.gif

Figure 3: Off-host port forwarding

为安全起见默认只有位于 Host A 上的客户端才能连接“Host A 上的端口 8080”(和前一样,假设端口 P 为 8080,端口 W 为 80),如果你想在另外一台机器(比如 Host C)上连接“Host A 上的端口 8080”,则需要设置 GatewayPortsyes (参考 man ssh_config ),也可以通过 -g 选项(和设置 GatewayPortsyes 等效)来实现监听在 Host A 上的所有地址(不仅仅是本地地址):

$ ssh -f -N -L 8080:localhost:80 user1@HostB         # 默认仅监听在本地,即 127.0.0.1:8080
$ ssh -g -f -N -L 8080:localhost:80 user1@HostB      # -g 使其监听在所有地址,即 *:8080

应用程序服务器(Host S)和 ssh 服务器(Host B)可以是不同的机器,通过 -L 的参数的第二个字段指定即可:

$ ssh -g -f -N -L 8080:localhost:80 user1@HostB   # ssh服务器会连接localhost:80
$ ssh -g -f -N -L 8080:HostS:80 user1@HostB       # ssh服务器会连接HostS:80

2.2. Troubleshooting: administratively prohibited: open failed

如果启用 Local port forwarding 后(如在 Host A 上执行下面命令):

$ ssh -L 8080:localhost:80 user1@HostB

连接 Host A 上 8080 端口时,出现了错误“administratively prohibited: open failed”。

你需要检测 Host B 上的配置文件“/etc/ssh/sshd_config”,确保:

  1. AllowTCPForwarding 被配置为 yes ,或者被注释掉(它的默认值为 yes ,所以可以不配置);
  2. PermitOpen 被配置为 any ,或者被注释掉(它的默认值为 any ,所以可以不配置)。

参考:
man sshd_config
https://unix.stackexchange.com/questions/14160/ssh-tunneling-error-channel-1-open-failed-administratively-prohibited-open

3. Remote port forwarding (-R)

上一节介绍的是 Local port forwarding,这一节介绍 Remote port forwarding。

假设你在自己电脑上开发了一个网站,想分享给朋友看一下。但你的 ISP(Internet service provider)一般都不会分配固定的公网 IP 给你,从而你的朋友是无法通过 Internet 访问你的网站。

这时,可以通过 Remote port forwarding 来解决。注:你需要有一台在公网上可访问的服务器。

假设你电脑为 Host A,且你的网站服务器端口为 8000。公网上可访问的服务器为 Host B。则在 Host A 上执行下面命令即可:

$ ssh -R 9000:localhost:8000 user1@HostB

这时,你朋友访问 HostB:9000 时,就相当于访问你本地搭建的网站。

特别说明,默认 Host B 上的 9000 端口仅监听本地地址(只能从 Host B 上访问,没什么意义),要使其监听在任意地址,你需要把 GatewayPorts yes 加入到 Host B 中 ssh 服务器配置文件“/etc/ssh/sshd_config”中。为使配置生效,你需要重启 ssh 服务器: sudo service sshd restart

如果你不需要交互式 shell,则可以指定参数 -N ,如果你想把它放入后台执行,则可以指定参数 -f 。如:

$ ssh -N -R 9000:localhost:8000 user1@HostB       # 没有交互式shell,在前台执行
$ ssh -N -R 9000:localhost:8000 user1@HostB &     # 没有交互式shell,且后台执行
$ ssh -f -N -R 9000:localhost:8000 user1@HostB    # 没有交互式shell,且后台执行

4. Local VS. Remote port forwarding

Local port forwarding 和 Remote port forwarding 很相似,它们的区别如图 45 所示。

ssh_local_port_forwarding.gif

Figure 4: SSH local port forwarding

ssh_remote_port_forwarding.gif

Figure 5: SSH remote port forwarding

Author: cig01

Created: <2018-04-28 Sat>

Last updated: <2018-07-19 Thu>

Creator: Emacs 27.1 (Org mode 9.4)