隧道(端口转发)

在隧道或端口转发中,本地端口连接到远程主机的端口,反之亦然。因此,连接到一台机器上的端口实际上是连接到另一台机器上的端口。

ssh(1) 的选项 -f(后台运行),-N(不执行远程程序)和 -T(禁用伪终端分配)对于仅用于创建隧道的连接非常有用。

基本 SSH 端口转发

在常规的端口转发中,本地端口的连接被转发到远程机器的端口。这是一种保护不安全协议或让远程服务看起来像本地服务的方法。以下示例中,我们将 VNC 转发分为两步,首先创建隧道:

$ ssh -L 5901:localhost:5901 -l fred desktop.example.org

通过这种方式,本地机器上连接到转发端口的连接实际上会连接到远程机器。

可以同时指定多个隧道,隧道类型可以是任何形式,而不仅仅是常规的端口转发。下一节将介绍反向隧道,动态转发请参见“代理和跳转主机”部分。

$ ssh -L 5901:localhost:5901 \
      -L 5432:localhost:5432 \
      -l fred desktop.example.org

如果某个连接仅用于创建隧道,则可以告诉 SSH 不执行任何远程程序(-N),使其成为非交互式会话,并且还可以让其在后台运行(-f)。

$ ssh -fN -L 3128:localhost:3128 -l fred server.example.org

注意,-N 即使在 authorized_keys 强制使用 command="..." 选项时也会有效。所以,使用 -N 的连接将保持打开状态,而不是执行程序然后退出。

上述三个连接可以保存在 SSH 客户端的配置文件 ~/.ssh/config 中,并可以为其设置快捷方式。

Host desktop desktop.example.org
        HostName desktop.example.org
        User fred
        LocalForward 5901 localhost:5901

Host postgres
        HostName desktop.example.org
        User fred
        LocalForward 5901 localhost:5901
        LocalForward 5432 localhost:5432

Host server server.example.org
        HostName server.example.org
        User fred
        ExitOnForwardFailure no
        LocalForward 3128 localhost:3128

Host *
        ExitOnForwardFailure yes

有了这些设置,在连接到 desktopdesktop.example.orgpostgresserverserver.example.org 时,列出的隧道将自动添加。最后的通配符配置适用于任何未明确设置 ExitOnForwardFailure 为 'no' 的主机,并且如果隧道无法建立,客户端将拒绝连接。对于任何给定配置指令,将使用首次获取的值,但文件内容可以通过命令行运行时选项进行覆盖。

通过单个中介主机进行隧道

隧道可以通过一个中介主机转发到第二台主机,并且第二台主机不需要处于公共可访问网络中。然而,第二台远程机器上的目标端口必须能够在与第一台主机相同的网络上访问。在以下示例中,192.168.0.101bastion.example.org 必须在同一网络中,并且 bastion.example.org 必须可以直接访问运行 ssh(1)客户端机器。因此,192.168.0.101 上的端口 80 必须对 bastion.example.org 可访问。

$ ssh -fN -L 1880:192.168.0.101:80 -l fred bastion.example.org

这样,一旦隧道建立,连接到 localhost 上的端口 1880 实际上会转发到 192.168.0.101 上的端口 80。这种方法适用于一个或两个主机,也可以将多个主机链接起来,使用不同的方法。

保护跳转主机,使用一个或多个中介主机进行隧道

在这里,目标是限制一组用户的权限,尽量减少他们通过跳转主机传递的权限,但仍然能够将端口转发到其他机器。如果账户被充分锁定,则跳转主机只能用于端口转发,而不能访问 shell、脚本或 SFTP。以下是 sshd_config(5) 中的设置,防止通过跳转主机进行 shell 访问或 SFTP,但仍允许端口转发。

Match Group tunnelers
        ForceCommand /bin/false
        PasswordAuthentication no
        ChrootDirectory %h
        PermitTTY no
        X11Forwarding no
        AllowTcpForwarding yes
        PermitTunnel no
        Banner none

请注意,由于 ChrootDirectory 配置指令,用户的主目录(但不包括目录中的文件)必须由 root 拥有,并且只能由 root 写入。此外,鉴于 PasswordAuthentication 配置指令,如果没有配置替代位置,则必须在 ~/.ssh/authorized_keys 中为其设置密钥。

$ ssh -N -L 9980:localhost:80 -J fred@bastion.example.org fred@192.168.79.124

通过这种方式,客户端的端口 9980 会通过 bastion.example.org 转发到 192.168.79.124 上的端口 80。

反向隧道

在需要从内部主机到跳转主机建立反向隧道的情况下,采用与前述相同的方法,但前提是首先需要在内部主机和跳转主机之间建立反向隧道。

有关通过中介计算机转发的更多信息,请参见《烹饪书》部分中的“代理和跳转主机”。

查找后台隧道的进程 ID(PID)

当隧道连接通过客户端-f 选项发送到后台执行时,目前没有自动的方法可以找到发送到后台的任务的进程 ID(PID)。后台进程通常用于端口转发或反向端口转发。下面是一个端口转发的示例,也叫做隧道。连接建立后,客户端会退出,将隧道留在后台,使本地主机上的 2194 端口连接到远程系统的本地主机上的 194 端口。

$ ssh -Nf -L 2194:localhost:194 fred@203.0.113.214

即使 $! 变量为空,$? 显示操作的成功或失败,也无法自动获取进程 ID。原因在于 shell 的作业控制并未将客户端置于后台。相反,客户端会在前台运行一会儿,然后正常退出,之后通过 fork 启动另一个进程在后台运行。由于原始客户端已经退出,进程 ID 也随之消失。

查找进程 ID 通常需要至少两步。有些方法可以通过尝试 ps(1) 并查看该账户的所有进程输出来追溯进程,但这不必要。若后台的 SSH 客户端是最新的,可以使用 pgrep(1),否则需要将输出用逗号分隔,并通过 xargs(1) 或进程替代将其传递给 ps(1)

$ ps uw | less

$ pgrep -n -x ssh

$ pgrep -d, -x ssh | xargs ps -p

$ ps -p $(pgrep -d, -x ssh)

如果 -d 选项不受支持,可能需要对上述命令进行一些修改,具体取决于操作系统。

$ pgrep -x ssh | xargs -n 1 ps -o user,pid,ppid,args -p | awk 'NR==1 || $3==1'
USER       PID  PPID COMMAND
fred     97778     1 ssh -fN -L 8008:localhost:80 fred@203.0.113.8
fred     14026     1 ssh -fN -L 8183:localhost:80 fred@203.0.113.183
fred     79522     1 ssh -fN -L 8228:localhost:80 fred@203.0.113.228
fred     49773     1 ssh -fN -L 8205:localhost:80 203.0.113.205

无论如何,请注意,所有在后台运行的连接都有一个父进程 ID(PPID)为 1,这是操作系统的进程控制初始化系统。

鉴于这一缺点,可以采取一种积极的方式,使用 ControlMasterControlPath 配置指令,在后台任务的进程 ID 上留一个 socket 来读取。

$ ssh -M -S ~/.ssh/sockets/pid.%C -fN -L 5901:localhost:5901 fred@203.0.113.122

$ ssh -S ~/.ssh/sockets/pid.%C -O check 203.0.113.122

-M 选项会使客户端进入主模式以使用 -S 选项指定的 socket 进行复用。然后,在 -f客户端转到后台后,check 控制命令会使用该 socket 来检查主进程是否在运行,并报告进程 ID。

建议将 socket 存放在一个隔离的目录中,该目录对其他账户不可读写。除了复杂性之外,一个明显的缺点是,该 socket 可能被用于其他连接。有关风险和其他用途的更多信息,请参见《烹饪书》部分中的“多路复用”。

反向端口转发

基本 SSH 反向转发

反向隧道将连接从常规隧道的方向反转,即从 SSH 会话发起的方向反向。通过远程转发(也称为反向端口转发),SSH 会话从本地主机开始,到远程主机时,远程主机上的端口会被转发到本地主机的端口。使用反向隧道时有两个阶段:首先,使用启用远程转发的 SSH 连接从端点 A 连接到端点 B;然后,其他系统连接到端点 B 上指定的端口,并将该端口转发到端点 A。因此,虽然系统 A 发起了到系统 B 的 SSH 连接,但对 B 上指定端口的连接会通过反向隧道发送回 A。一旦 SSH 连接建立,反向隧道可以像常规隧道一样在端点 B 上使用,尽管是由端点 A 发起的 SSH 连接。

远程转发是一种通过 SSH 转发 SSH 连接的方式,适用于需要访问通常无法访问的系统,例如家用路由器后面的始终在线 SBC。首先,从不可访问的系统通过指定的反向隧道与另一个可访问的系统建立 SSH 会话。在这个示例中,SSH 连接是从本地系统(端点 A)到远程系统(端点 B),该连接包含一个从远程系统(端点 B)上的端口 2022 到本地系统端口 22 的反向隧道:

$ ssh -fN -R 2022:localhost:22 -l fred server.example.org

最后,在远程主机 server.example.org 上,连接到端口 2022 的反向隧道连接。虽然连接是在 server.example.org 上的 localhost 上的端口 2022 进行的,但数据包最终会发送到发起 SSH 初始连接的系统上的端口 22,携带反向隧道。

$ ssh -p 2022 -l fred localhost

这样,即使系统通常无法访问,通过 SSH 连接,也可以访问该系统。SSH 发起连接,建立反向隧道,然后第二个系统可以随时通过 SSH 连接到第一个系统,只要隧道保持有效。如果使用密钥和循环生成带远程转发的 SSH 连接,则可以自动保持反向隧道。

下一个示例演示如何通过 SSH 从另一个系统 server.example.org 使 VNC 从无法访问的系统上可用。首先,从运行 VNC 服务器的系统反向转发端口,将第一个 VNC 显示的端口本地转发到 server.example.org 上的第三个 VNC 显示:

$ ssh -fNT -R 5903:localhost:5901 -l fred server.example.org

然后,在 server.example.org 上,可以连接到该系统的第三个 VNC 显示并通过反向隧道连接到源系统:

$ xvncviewer :3

这也是一个例子,展示了转发的端口不必是相同的。

远程转发可以通过 SSH 客户端的配置文件中的 RemoteForward 指令来配置。详情请参见下一小节。

反向隧道的常见用例

反向隧道的一个常见应用场景是,当您需要访问一个位于 NAT 或防火墙后面的系统或服务,因此无法接受来自外部的 SSH 连接,但您可以直接访问一个位于防火墙外的第二个系统,这个系统可以接受传入的连接。在这种情况下,可以轻松地从防火墙后的内部系统建立一个反向隧道连接到外部的第二个系统。一旦建立了 SSH 连接并创建了反向隧道,其他系统就可以通过连接到远程系统的转发端口来访问内部系统。外部的远程系统充当中继服务器,将连接转发到内部系统。

使用客户端配置文件配置远程转发

RemoteForward 客户端配置指令可以在 ssh_config(5) 文件中使用,以建立来自另一系统的反向隧道。ForkAfterAuthentication 对应于运行时参数 -fSessionType 对应于运行时参数 -N,在此示例中,这些都是可选的,和上述子小节中的第一个示例相同:

Host server server.example.org
        User fred
        HostName server.example.org
        ForkAfterAuthentication yes
        SessionType none
        RemoteForward 2022 localhost:22

通过在另一台系统上配置上述内容,可以通过输入 ssh server 来轻松地建立反向隧道。然后,在 server.example.org 上连接到端口 2022 将会通过反向隧道回传到原始系统的端口 22。

Host server server.example.org
        User fred
        HostName server.example.org
        ForkAfterAuthentication yes
        SessionType none
        RequestTTY no
        RemoteForward 5903 localhost:5901

这与前述小节中的第二个示例类似,但在客户端配置文件中使用,而不是使用运行时参数。有关更多详细信息,请参见关于客户端配置文件的章节。

在已建立的连接中添加或删除隧道

可以通过转义序列向已建立的连接中添加或删除隧道、反向隧道以及 SOCKS 代理。默认的转义字符是波浪号(~),所有选项在 ssh(1) 的手册页中都有描述。转义序列仅在它们是行首字符并且后面跟有回车时才有效。当向已建立的连接中添加或删除隧道时,使用 ~C 命令行。

要在活动的 SSH 会话中添加隧道,请使用转义序列打开 SSH 命令行,然后输入隧道的参数:

~C
L 2022:localhost:22

要从活动的 SSH 会话中删除隧道,几乎相同。只需将 -L-R-D 更改为 -KL-KR-KD,然后加上端口号。使用转义序列打开 SSH 命令行并输入删除隧道的参数:

~C
KL2022

在多路复用连接中添加或删除隧道

对于多路复用连接,还有一个额外的端口转发选项。可以通过单个 TCP 连接复用多个 SSH 连接。可以将控制命令传递给主进程,以添加或删除端口转发。

首先,建立主连接并分配一个 socket 路径:

$ ssh -S '/home/fred/.ssh/%h:%p' -M server.example.org

然后,使用 socket 路径,可以添加端口转发:

$ ssh -O forward -L 2022:localhost:22 -S '/home/fred/.ssh/%h:%p' fred@server.example.org

从 OpenSSH 6.0 开始,可以使用控制命令取消特定的端口转发:

$ ssh -S "/home/fred/.ssh/%h:%p" -O cancel -L 2022:localhost:22 fred@server.example.org

有关多路复用的更多信息,请参见《烹饪书》中的“多路复用”部分。

限制隧道到特定端口

默认情况下,端口转发允许转发到任何端口,只要它是允许的。要限制用户在转发中使用的端口,可以在服务器端使用 PermitOpen 选项,这可以在服务器配置中设置,也可以在用户的公钥中设置(在 authorized_keys 文件中)。例如,在 sshd_config(5) 中设置以下内容,允许所有用户只将端口转发到服务器的 7654 端口:

PermitOpen localhost:7654

如果希望转发多个端口,可以在同一行中使用空格分隔多个端口:

PermitOpen localhost:7654 localhost:3306

如果客户端尝试转发到不允许的端口,将会收到类似 “open failed: administratively prohibited: open failed” 的警告消息,但连接仍会正常进行。然而,即使客户端在其配置中设置了 ExitOnForwardFailure,连接仍会成功,尽管会有警告消息。

如果提供了 shell 访问权限,用户仍然可以运行其他端口转发器。因此,PermitOpen 更像是一个提醒或建议,而不是一种严格的限制。但在许多情况下,这样的提醒可能已经足够。

对于反向隧道,可以使用 PermitListen 选项。它确定远程系统上可用的端口。例如,以下配置允许使用 ssh -R 2022:localhost:xxxx,其中 xxxx 可以是反向隧道源端的任何可用端口,但远端端口只能是 2022:

PermitListen localhost:2022

PermitOpenPermitListen 选项可以作为一个或多个 Match 块的一部分使用,如果需要根据用户、组、客户端地址、网络、服务器地址或网络以及 sshd(8) 使用的监听端口的不同组合来调整转发选项。如果使用 Match 条件选择性地应用端口转发规则,还可以通过设置 PermitTTYno 来防止账户获取交互式 shell。这样将阻止在服务器上分配伪终端(PTY),从而防止 shell 访问,但允许其他程序运行,除非另外指定了强制命令。

Match Group mariadbusers
        PermitOpen localhost:3306
        PermitTTY no
        ForceCommand /usr/bin/true

通过在 sshd_config(5) 中添加这一段配置,只能通过添加 -N 选项来连接,避免执行远程命令:

$ ssh -N -L 3306:localhost:3306 server.example.org

-N 选项可以单独使用,也可以与 -f 选项一起使用,后者会在连接建立后将客户端转到后台。

如果在服务器配置中的 Match 块中没有 ForceCommand 选项,则如果客户端尝试获取一个被阻止的 PTY,客户端会收到警告 “PTY allocation request failed on channel n”,其中 n 是通道号,但连接会成功,不会有远程 shell,端口仍会被转发。尽管如此,客户端仍然可以指定各种程序,但它们不会获取 PTY。因此,为了真正阻止访问系统而只允许端口转发,需要使用强制命令。true(1) 工具在这里非常有用。请注意,true(1) 可能在不同的系统上位于不同的位置。

使用密钥限制端口转发请求

以下是 authorized_keys 中的一个示例,显示了如何将 PermitOpen 选项添加到密钥中,以限制使用该密钥连接的用户只能转发到端口 8765:

permitopen="localhost:8765" ssh-ed25519 AAAAC3NzaC1lZDI1NT...

可以将多个 PermitOpen 选项应用于同一个公钥,如果它们之间用逗号分隔,因此一个密钥可以允许多个端口。

默认情况下,允许 shell 访问。有了 shell 访问,用户仍然可以运行其他端口转发器。如果将 no-pty 选项与适当的强制命令配合使用,则可以创建只允许转发而不允许交互式 shell 的密钥。以下是 authorized_keys 中的一个示例:

no-pty,permitopen="localhost:9876",command="/usr/bin/true" ssh-ed25519 AAAAC3NzaC1lZDI1NT...

no-pty 选项会阻止交互式 shell。客户端仍然可以连接到远程服务器并允许端口转发,但会收到错误消息,其中包括 "PTY allocation request failed on channel n" 的消息。但正如上一小节

最后修改: 2025年01月19日 星期日 21:45