OpenSSH/Cookbook/隧道
在隧道或端口转发中,本地端口连接到远程主机的端口,反之亦然。因此,对一台机器上端口的连接实际上是对另一台机器上端口的连接。
ssh(1) 的选项 -f (转到后台)、-N (不执行远程程序) 和 -T (禁用伪终端分配) 对于仅用于创建隧道的连接很有用。
在常规端口转发中,对本地端口的连接将转发到远程机器上的端口。这是一种保护不安全协议或使远程服务看起来像本地服务的方法。这里我们分两步转发了 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
如果连接仅用于创建隧道,则可以指示它不执行任何远程程序 (-N),使其成为非交互式会话,并将其置于后台 (-f)。
$ ssh -fN -L 3128:localhost:3128 -l fred server.example.org
请注意,即使 authorized_keys 使用 command="..." 选项强制执行程序,-N 也会起作用。因此,使用 -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
使用这些设置,连接到 desktop、desktop.example.org、postgres、server 或 server.example.org 时,会自动添加列出的隧道。最后面的通配符配置适用于上述所有尚未将 ExitOnForwardFailure 设置为 'no' 的主机,如果无法建立隧道,客户端将拒绝连接。将使用任何给定配置指令的第一个获得的值,但文件的内容可以通过命令行传递的运行时选项覆盖。
隧道可以通过一个中间主机到达第二个主机,后者不需要在公开可访问的网络上。但是,第二个远程机器上的目标端口必须在与第一个主机相同的网络上可访问。这里,192.168.0.101 和 bastion.example.org 必须在同一个网络上,并且 bastion.example.org 必须直接可访问运行 ssh(1) 的客户端机器。因此,192.168.2.101 上的端口 80 必须对机器 bastion.example.org 可用。
$ ssh -fN -L 1880:192.168.0.101:80 -l fred bastion.example.org
因此,一旦隧道建立,要通过主机 bastion.example.org 连接到 192.168.2.101 上的端口 80,请连接到 localhost 上的端口 1880。这种方式适用于一两个主机。也可以通过不同的方法将多个主机链接在一起。
这里,想法是将一组用户的权限限制为通过跃点主机所需的最低限度,但仍然能够将端口转发到其他机器。如果帐户被充分锁定,那么堡垒只能用于转发,而不能用于 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
请注意,由于 sshd_config(5) 中的 ChrootDirectory 配置指令,它们的主目录(但目录中的文件除外)必须由 root 拥有,并且只能由 root 写入。另外,由于 PasswordAuthentication 配置指令,如果尚未配置备用位置,则必须在 ~/.ssh/authorized_keys 中的主目录中设置密钥。
$ ssh -N -L 9980:localhost:80 -J [email protected] [email protected]
这样,客户端上的端口 9980 将通过 bastion.example.org 指向 192.168.79.124 上的端口 80。
在堡垒必须从内部主机建立反向隧道才能到达它的情况下,可以使用相同的方法,但前提是从内部主机到堡垒首先建立反向隧道。
有关通过中间计算机的更多信息,请参见 Cookbook 中有关 代理和跳跃主机 的部分。
当使用客户端的 -f 选项将隧道连接发送到后台执行时,目前还没有自动方法来查找发送到后台的任务的进程 ID (PID)。后台进程通常用于端口转发或反向端口转发。以下是一个端口转发的示例,也称为隧道。连接建立后,客户端会离开,将隧道保留在后台,将本地主机的端口 2194 连接到远程系统本地主机的端口 194。
$ ssh -Nf -L 2194:localhost:194 [email protected]
特殊变量 $! 仍然为空,即使 $? 报告操作成功或失败。原因是 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 [email protected]
fred 14026 1 ssh -fN -L 8183:localhost:80 [email protected]
fred 79522 1 ssh -fN -L 8228:localhost:80 [email protected]
fred 49773 1 ssh -fN -L 8205:localhost:80 203.0.113.205
无论哪种方式,请注意,所有在后台运行的连接的父进程 ID (PPID) 都是 1,即操作系统的进程控制初始化系统。
意识到这一缺点,可以使用 ControlMaster 和 ControlPath 配置指令采用主动方法,以便留下一个可读的套接字以获取后台任务的进程 ID。
$ ssh -M -S ~/.ssh/sockets/pid.%C -fN -L 5901:localhost:5901 [email protected]
$ ssh -S ~/.ssh/sockets/pid.%C -O check 203.0.113.122
-M 选项使客户端进入使用 -S 选项指定的套接字进行多路复用的主模式。然后,在 -f 将客户端分叉到后台后,控制命令 check 将使用套接字检查主进程是否正在运行,并报告进程 ID。
最好将套接字放在其他帐户不可读或不可写的隔离目录中。除了复杂性之外,一个明显的缺点是,套接字可以重复使用用于其他连接。有关风险和额外用途的更多信息,请参见 Cookbook 中有关 多路复用 的部分。
反向隧道与普通隧道方向相反,即与 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 上的 locahost 上的端口 2022,数据包最终也会通过承载反向隧道的 SSH 初始连接系统上的端口 22 传输。
$ ssh -p 2022 -l fred localhost
因此,这个例子允许通过 SSH 访问原本无法访问的系统。SSH 连接出去,建立一个反向隧道,然后第二个系统可以随时通过 SSH 连接到第一个系统,只要隧道一直存在。如果使用密钥和循环来生成带有远程转发的 SSH 连接,那么反向隧道可以自动维护。
下一个例子展示了如何通过第二个系统 server.example.org 通过 SSH 从无法直接访问的系统访问 VNC。从运行 VNC 服务器的系统开始,将第一个 VNC 显示的端口反向转发到 server.example.org 上的第三个 VNC 显示。
$ ssh -fNT -R 5903:localhost:5901 -l fred server.example.org
然后,在系统 server.example.org 上,用户可以连接到该系统 localhost 地址上的第三个 VNC 显示,并通过隧道连接到源系统。
$ xvncviewer :3
这也是一个例子,说明转发端口不需要相同。
远程转发可以包含在 SSH 客户端的配置文件中,使用 RemoteForward 指令。有关详细信息,请参阅下一节。
反向隧道的常见用例是,当您需要访问位于 NAT 或防火墙(或两者)后面的系统或服务时,而 SSH 连接被阻止,但您可以直接访问防火墙外部的第二个系统,该系统可以接受传入连接。在这种情况下,很容易从防火墙后面的内部系统建立到外部第二个系统的反向隧道。一旦 SSH 连接建立了反向隧道,就可以从外部连接到内部系统,其他系统可以连接到远程系统上的转发端口。外部的远程系统充当中继服务器,将连接转发到内部的启动系统。
使用客户端配置文件配置远程转发
[edit | edit source]RemoteForward 客户端配置指令可以在 ssh_config(5) 中使用,以从另一个系统建立反向隧道。ForkAfterAuthentication 将为 -f 和 SessionType 将为 -N,作为运行时参数,当然,在本例中是可选的,本例与上面小节中的第一个例子相同。
Host server server.example.org
User fred
HostName server.example.org
ForkAfterAuthentication yes
SessionType none
RemoteForward 2022 localhost:22
有了这些设置,在另一个系统上,只需输入 ssh server
连接到 server 即可建立反向隧道。然后,在 server.example.org 上,连接到 localhost 上的端口 2022 将会传回原始系统上的端口 22。
Host server server.example.org
User fred
HostName server.example.org
ForkAfterAuthentication yes
SessionType none
RequestTTY no
RemoteForward 5903 localhost:5901
同样,这复制了上面小节中的第二个例子,只是在客户端配置文件中使用,而不是使用运行时参数。有关更多信息,请参阅关于使用 客户端配置文件 的章节。
在已建立的连接中添加或删除隧道
[edit | edit source]可以使用转义序列在现有连接中添加或删除隧道、反向隧道和 SOCKS 代理。默认的转义字符是波浪号 (~),完整选项范围在 ssh(1) 的手册页中描述。转义序列只有在作为一行中的第一个字符输入并后跟回车时才有效。当向现有连接添加或删除隧道时,将使用 ~C,即命令行。
要在活动的 SSH 会话中添加隧道,请使用转义序列在 SSH 中打开命令行,然后输入隧道的参数。
~C L 2022:localhost:22
从活动的 SSH 会话中删除隧道几乎相同。我们使用 -KL、-KR 和 -KD 而不是 -L、-R 或 -D,再加上端口号。使用转义序列在 SSH 中打开命令行,然后输入删除隧道的参数。
~C KL2022
在多路复用连接中添加或删除隧道
[edit | edit source]在多路复用时,有一个额外的转发选项。多个 SSH 连接可以多路复用到单个 TCP 连接上。可以将控制命令传递给主进程,以向主进程添加或删除端口转发。
首先建立一个主连接,并分配一个套接字路径。
$ ssh -S '/home/fred/.ssh/%h:%p' -M server.example.org
然后,使用套接字路径,可以添加端口转发。
$ ssh -O forward -L 2022:localhost:22 -S '/home/fred/.ssh/%h:%p' [email protected]
从 OpenSSH 6.0 开始,可以使用控制命令取消特定的端口转发。
$ ssh -S "/home/fred/.ssh/%h:%p" -O cancel -L 2022:localhost:22 [email protected]
有关多路复用的更多信息,请参阅 Cookbook 部分中的 多路复用。
将隧道限制到特定端口
[edit | edit source]默认情况下,端口转发将允许转发到任何端口(如果允许)。限制用户可以在转发中使用的端口的方法是在服务器端应用 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
PermitOpen 或 PermitListen 选项可以作为一或多个 Match 块的一部分使用,如果转发选项需要根据用户、组、客户端地址或网络、服务器地址或网络以及 sshd(8) 使用的监听端口的各种组合而变化。如果使用 Match 条件来选择性地应用端口转发的规则,也可以通过将 PermitTTY 设置为 no 来阻止帐户获得交互式 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,客户端上会显示警告“PTY allocation request failed on channel n”,其中 n 是通道号,但连接会正常成功,没有远程 shell,端口仍然会转发。包括 shell 在内的各种程序仍然可以由客户端指定,只是它们不会获得 PTY。因此,要真正阻止除转发之外对系统的访问,需要使用强制命令。工具 true(1) 非常适合这个用途。请注意,true(1) 可能在不同系统上的不同位置。
仅使用密钥限制端口转发请求
[edit | edit source]以下 authorized_keys 行展示了 PermitOpen 选项前置到密钥中,以便使用该特定密钥连接的用户只能转发到端口 8765
permitopen="localhost:8765" ssh-ed25519 AAAAC3NzaC1lZDI1NT...
如果用逗号分隔,则可以将多个 PermitOpen 选项应用于同一个公钥,因此一个密钥可以允许多个端口。
默认情况下,允许 shell 访问。使用 shell 访问,仍然可以运行其他端口转发器。如果与使用 command 选项的适当强制命令相结合,密钥内部的 no-pty 选项可以帮助创建仅允许转发而不允许交互式 shell 的密钥。以下是在 authorized_keys 中列出的此类密钥的示例。
no-pty,permitopen="localhost:9876",command="/usr/bin/true" ssh-ed25519 AAAAC3NzaC1lZDI1NT...
no-pty 选项会阻止交互式 shell。客户端仍然可以连接到远程服务器并允许转发,但会返回一个错误信息,其中包含“PTY 分配请求在通道 n 上失败”的消息。但如前文所述,仍然有很多方法可以绕过它,添加 command 选项会阻碍它们。
这种方法很难锁定。如果帐户以任何方式(直接或间接)对 authorized_keys 文件具有写入权限,那么用户就可以添加新密钥或用更宽松的选项覆盖现有密钥。为了防止这种情况,服务器必须被配置为在用户无法访问的位置查找该文件。有关此方面的详细信息,请参见关于 公钥认证 的部分。在许多情况下,使用 sshd_config(5) 在上一节中描述的方法可能更实用。