OpenSSH
多路复用
多路复用是指通过单一线路或连接发送多个信号的能力。在 OpenSSH 中,多路复用允许重用现有的外向 TCP 连接来进行多个并发的 SSH 会话,从而避免了每次都创建新 TCP 连接并重新认证的开销。
多路复用的优势
SSH 多路复用的一个主要优势是消除了创建新 TCP 连接和协商安全连接的开销。一个机器能够接受的连接数量是有限的,这一限制在某些机器上更为明显,而且会根据负载和使用情况变化。在打开新连接时也会有显著的延迟。需要重复打开新连接的活动,使用多路复用后可以显著加速。
通过对比下面的表格,可以看到多路复用和独立会话的区别。两者都是通过 netstat -nt
输出的内容,稍作编辑以便清晰对比。在表格 1 中,"SSH 连接,独立" 显示没有使用多路复用时,每次新登录都会创建一个新的 TCP 连接,每个登录会话都有一个 TCP 连接。而在表格 2 中,"SSH 连接,多路复用" 显示使用多路复用时,新的登录会通过已建立的 TCP 连接进行。
表格 1:SSH 连接,独立
# 一个连接
tcp 0 0 192.168.x.y:45050 192.168.x.z:22 ESTABLISHED
# 两个独立连接
tcp 0 0 192.168.x.y:45050 192.168.x.z:22 ESTABLISHED
tcp 0 0 192.168.x.y:45051 192.168.x.z:22 ESTABLISHED
# 三个独立连接
tcp 0 0 192.168.x.y:45050 192.168.x.z:22 ESTABLISHED
tcp 0 0 192.168.x.y:45051 192.168.x.z:22 ESTABLISHED
tcp 0 0 192.168.x.y:45052 192.168.x.z:22 ESTABLISHED
表格 2:SSH 连接,多路复用
# 一个连接
tcp 0 0 192.168.x.y:58913 192.168.x.z:22 ESTABLISHED
# 两个多路复用连接
tcp 0 0 192.168.x.y:58913 192.168.x.z:22 ESTABLISHED
# 三个多路复用连接
tcp 0 0 192.168.x.y:58913 192.168.x.z:22 ESTABLISHED
可以看到,在多路复用的情况下,无论有多少个 SSH 会话,都只建立一个 TCP 连接并且重复使用它。
另外,我们也可以通过在一个慢速的远程服务器上运行 true(1)
命令,并使用 time(1)
测量时间,来比较使用与不使用多路复用的差异。以下是两个命令的对比:
- 没有多路复用的连接时间:
real 0m0.658s
user 0m0.016s
sys 0m0.008s
- 使用多路复用的连接时间:
real 0m0.029s
user 0m0.004s
sys 0m0.004s
可以看到,差异非常明显,对于需要频繁快速连接的活动,速度提升尤为显著。多路复用连接的加速效果并不体现在首次建立主连接的速度上(这部分速度正常),而是体现在第二次及随后的多路复用连接上。新的 SSH 连接的开销依然存在,但新 TCP 连接的开销得到了避免。第二次及以后的连接会重复使用已建立的 TCP 连接,而不需要为每个新的 SSH 连接创建新的 TCP 连接。
配置多路复用
OpenSSH 客户端从 3.9 版本(2004年8月18日)开始支持多路复用,可以通过在 ssh_config(5)
配置文件中设置 ControlMaster
、ControlPath
和 ControlPersist
来实现。客户端配置文件通常默认为 ~/.ssh/config
。这三个指令在 ssh_config(5)
的手册页中有详细描述。你也可以在 "TOKENS" 部分查看可用的令牌列表,这些令牌会在运行时被扩展。
- ControlMaster 决定是否让
ssh(1)
监听控制连接,以及如何处理这些连接。 - ControlPath 设置用于多路复用会话的控制套接字的位置。它们可以在
ssh_config(5)
中全局设置或在运行时指定。控制套接字会在主连接结束时自动删除。 - ControlPersist 可以与
ControlMaster
一起使用。如果ControlPersist
设置为 'yes',则主连接会在后台保持开启,直到显式终止或超时关闭。如果ControlPersist
设置为具体时间,则主连接会保持打开,直到指定的时间或直到最后一个多路复用会话关闭,以较长者为准。
以下是一个配置示例:
Host machine1
HostName machine1.example.org
ControlPath ~/.ssh/controlmasters/%r@%h:%p
ControlMaster auto
ControlPersist 10m
在这个配置下,第一次连接到 machine1
会在 ~/.ssh/controlmasters/
目录下创建一个控制套接字。然后,所有后续的连接(最多 10 个,默认由 SSH 服务器的 MaxSessions
设置)会自动重用该控制路径作为多路复用会话。如果将 ControlMaster
设置为 autoask
,则每个新连接时会要求确认。
请注意,在上述配置中,控制套接字会被放置在 ~/.ssh/controlmasters/
文件夹下。如果该文件夹不存在,SSH 客户端会退出,并显示一个关于 unix_listener
的错误,指出路径或文件不存在:
unix_listener: cannot bind to path /home/fred/.ssh/controlmasters/fred@machine1.example.org:22.EOWsvm5xFt30O6CB: No such file or directory
使用 ssh -O
可以管理连接并使用相同的快捷配置。要取消所有现有连接,包括主连接,可以使用 exit
而不是 stop
。
$ ssh -O check machine1
Master running (pid=14379)
$ ssh -O stop machine1
Stop listening request sent
$ ssh -O check machine1
Control socket connect(/Users/Username/.ssh/sockets/machine1): No such file or directory
在这个示例中,首先检查连接的状态。然后,告诉主连接不再接受多路复用请求,最后再次检查控制套接字是否不可用。
手动建立多路复用连接
多路复用会话需要一个控制主连接来建立。运行时参数 -M
和 -S
分别对应 ControlMaster
和 ControlPath
。因此,首先通过 -M
和指定控制套接字路径 -S
来建立一个初始的主连接。
$ ssh -M -S /home/fred/.ssh/controlmasters/fred@server.example.org:22 server.example.org
接着,在其他终端中,后续的多路复用连接会使用 ControlPath
或 -S
指向控制套接字。
$ ssh -S /home/fred/.ssh/controlmasters/fred@server.example.org:22 server.example.org
需要注意的是,控制套接字的命名为 fred@server.example.org:22
,或者使用 %r@%h:%p
来使名称唯一。%r
、%h
和 %p
代表远程用户名、远程主机和远程主机的端口。控制套接字应当具有唯一的名称。
多个 -M
参数会将 ssh(1)
置于主连接模式,在接受从属连接之前需要确认。这与 ControlMaster=ask
相同,都需要用户确认。
以下是请求确认新多路复用会话的主连接:
$ ssh -MM -S ~/.ssh/controlmasters/%r@%h:%p server.example.org
而后续的多路复用连接如下所示:
$ ssh -S ~/.ssh/controlmasters/%r@%h:%p server.example.org
可以使用 -O check
来查询控制主连接的状态,查看它是否正在运行。
$ ssh -O check -S ~/.ssh/controlmasters/%r@%h:%p server.example.org
如果控制会话已停止,查询会返回关于 "No such file or directory" 的错误,尽管仍有多路复用会话正在运行,因为套接字已不存在。
另外,除了将 -M
和 -S
作为运行时参数,也可以通过 -o
来完全指定配置选项,这样更方便将其转移到客户端配置文件中。
下面的方式通过运行时参数设置配置选项,首先启动一个控制主连接:
$ ssh -o "ControlMaster=yes" -o "ControlPath=/home/fred/.ssh/controlmasters/%r@%h:%p" server.example.org
然后后续会话通过套接字连接到控制主连接的控制路径。
$ ssh -o "ControlPath=/home/fred/.ssh/controlmasters/%r@%h:%p" server.example.org
当然,所有这些配置可以放入 ssh_config(5)
中,正如前面一节所示。从 6.7 版本开始,%r@%h:%p
及其变体可以用 %C
来替代,它会自动生成一个基于 %l%h%p%r
连接字符串的 SHA1 哈希。
$ ssh -S ~/.ssh/controlmasters/%C server.example.org
这样做有两个优势。一是哈希值可能比组合元素的名称短,但仍然可以唯一标识连接;二是它对连接信息进行了混淆,避免了在套接字名称中显示这些信息。
结束多路复用连接
结束多路复用会话的一种方式是退出所有相关的 SSH 会话,包括控制主连接。如果控制主连接通过 ControlPersist
被置于后台,那么需要使用 -O
和 stop
或 exit
来停止它。如果没有在 ssh_config(5)
中定义快捷方式,那么还需要知道控制套接字的完整路径和文件名。
$ ssh -O stop server1
$ ssh -O stop -S ~/.ssh/controlmasters/%C server1.example.org
命令 -O stop
会优雅地关闭多路复用。发出该命令后,控制套接字会被移除,并且不再接受新的多路复用会话。现有连接会继续,直到最后一个多路复用连接关闭,主连接会持续存在。
相比之下,命令 -O exit
会立即移除控制套接字,并终止所有现有连接。
另外,ControlPersist
指令也可以设置为在一段时间不使用后超时。该时间间隔以 sshd_config(5)
中列出的时间格式写出,若没有单位则默认为秒。这会导致主连接在指定时间内没有客户端连接时自动关闭。
Host server1
HostName server1.example.org
ControlPath ~/.ssh/controlmasters/%C
ControlMaster yes
ControlPersist 2h
上述配置示例表示如果主连接在 2 小时内没有活动,控制主连接会超时关闭。需要小心使用持久控制套接字。具有读写控制套接字权限的用户可以在没有进一步认证的情况下建立新连接。
多路复用选项
用于配置会话多路复用的值可以在用户特定的 ssh_config(5)
文件中设置,或者在全局的 /etc/ssh/ssh_config
文件中,或者在从 shell 或脚本运行时通过参数设置。当 ControlMaster
设置为 yes
时,可以通过运行时参数覆盖此设置,显式地将其设置为 no
,以便重新使用现有的主连接:
$ ssh -o "ControlMaster=no" server.example.org
ControlMaster
接受五个不同的值:'no'、'yes'、'ask'、'auto' 和 'autoask'。
'no'
是默认值。新的会话不会尝试连接到已建立的主连接,但仍然可以通过显式连接到现有套接字来实现多路复用。'yes'
每次都会创建一个新的主连接,除非显式覆盖。新的主连接将监听连接请求。'ask'
每次都会创建一个新的主连接,除非被覆盖,这些连接会监听连接请求。如果被覆盖,ssh-askpass(1)
会在 X 环境中弹出提示,询问主连接的拥有者是否批准或拒绝请求。如果请求被拒绝,则创建的会话会回退为常规的独立会话。'auto'
自动创建主连接,但如果已有主连接存在,后续会话将自动进行多路复用。'autoask'
假设如果已有主连接,后续会话应进行多路复用,但会在添加会话之前先询问用户。
拒绝的连接会被记录到主连接中。
Host *
ControlMaster ask
ControlPath
可以是一个固定字符串,或者包含在 ssh_config(5)
中描述的多个预定义变量。%L
代表本地主机名的第一个组成部分,%l
代表完整的本地主机名。%h
是目标主机名,%n
是原始目标主机名,%p
是远程服务器的目标端口,%r
是远程用户名,%u
是运行 ssh(1)
的用户名。它们也可以组合成 %C
,它是从 %l%h%p%r
连接字符串生成的 SHA1 哈希。
Host *
ControlMaster ask
ControlPath ~/.ssh/controlmasters/%C
ControlPersist
可以设置为 'yes'、'no' 或一个时间间隔。如果给定时间间隔,则默认为秒,单位可以扩展为分钟、小时、天、周或它们的组合。如果设置为 'yes',则主连接会无限期地保持在后台。
Host *
ControlMaster ask
ControlPath ~/.ssh/controlmasters/%C
ControlPersist 10m
事后端口转发
可以在不建立新连接的情况下请求端口转发。这里我们使用 -L
将本地主机的 8080 端口转发到远程主机的 80 端口:
$ ssh -O forward -L 8080:localhost:80 -S ~/.ssh/controlmasters/fred@server.example.org:22 server.example.org
对于远程转发,可以使用 -R
,但是对于多路复用会话,转义序列 ~C
不可用,因此只能使用 -O forward
来动态添加端口转发。
端口转发可以在不关闭任何会话的情况下取消,使用 -O cancel
:
$ ssh -O cancel -L 8080:localhost:80 -S ~/.ssh/controlmasters/fred@server.example.org:22 server.example.org
取消端口转发的语法与转发时相同。然而,目前没有办法查看哪些端口正在被转发,以及它们是如何被转发的。
关于多路复用的附加说明
切勿将控制路径套接字放置在任何公共可访问的目录中。应将这些套接字放置在其他位置的目录中,该目录仅限于您的帐户访问。例如,~/.ssh/socket/
会是一个更安全的选择,而 /tmp/
是一个不安全的选择。
在需要保持连接的情况下,始终可以将多路复用与其他选项结合使用,例如 -f
或 -N
,使控制主连接在连接后进入后台,并且不加载 shell。
在 sshd_config(5)
中,MaxSessions
指令指定每个网络连接允许的最大打开会话数。这在通过单个 TCP 连接进行多路复用时使用。将 MaxSessions
设置为 1 会禁用多路复用,将其设置为 0 会完全禁用登录/外壳/子系统会话。默认值为 10。MaxSessions
指令还可以在 Match
条件块下设置,以便在不同条件下提供不同的设置。
防止多路复用的错误
用于保存控制套接字的目录必须位于允许创建套接字的文件系统上。例如,AFS 就不支持此操作,某些 HFS+ 实现也是如此。如果尝试在不允许创建套接字的文件系统上创建套接字,将会出现类似以下的错误:
$ ssh -M -S /home/fred/.ssh/mux 192.0.2.57
muxserver_listen: link mux listener /home/fred/.ssh/mux.vjfeIFFzHnhgHoOV => /home/fred/.ssh/mux: Operation not permitted
类似的问题也曾出现在试图在 OverlayFS 文件系统上创建 Unix 域套接字时,这些文件系统通常与 Docker 一起使用,特别是在 Linux 4.7 之前的内核版本中。
如果无法重新配置文件系统以允许创建套接字,那么唯一的选择就是将控制路径套接字放在其他支持创建套接字的文件系统中。
观察多路复用
可以进行一些粗略的测量,展示多路复用连接与独立连接之间的差异,如上面的表格和图形所示。
测量 TCP 连接数
以下脚本使用 netstat(8)
和 awk(1)
显示与 SSH 连接数相对应的 TCP 连接数:
#!/bin/sh
netstat -nt | awk 'NR == 2'
ssh -f server.example.org sleep 60
echo # 一个连接
netstat -nt | awk '$5 ~ /:22$/'
ssh -f server.example.org sleep 60
echo # 两个连接
netstat -nt | awk '$5 ~ /:22$/'
ssh -f server.example.org sleep 60
echo # 三个连接
netstat -nt | awk '$5 ~ /:22$/'
echo 表格 1
#!/bin/sh
netstat -nt | awk 'NR == 2'
ssh -f -M -S ~/.ssh/demo server.example.org sleep 60
echo # 一个连接
netstat -nt | awk '$5 ~ /:22$/'
ssh -f -S ~/.ssh/demo server.example.org sleep 60 &
echo # 两个连接
netstat -nt | awk '$5 ~ /:22$/'
ssh -f -S ~/.ssh/demo server.example.org sleep 60 &
echo # 三个连接
netstat -nt | awk '$5 ~ /:22$/'
echo 表格 2
测量响应时间
测量响应时间需要首先设置使用密钥的代理,以便代理处理认证,消除认证作为延迟来源。以下的响应时间测试都依赖于使用密钥进行认证。
对于独立连接,只需添加 time(1)
来检查访问所需的时间:
$ time ssh -i ~/.ssh/rsakey server.example.org true
对于多路复用连接,必须首先建立一个控制主会话。然后,后续连接将显示响应速度的提升。
$ ssh -f -M -S ~/.ssh/demo -i ~/.ssh/rsakey server.example.org
$ time ssh -S ~/.ssh/demo -i ~/.ssh/rsakey server.example.org true
$ time ssh -S ~/.ssh/demo -i ~/.ssh/rsakey server.example.org true
这些响应时间大致相同,但独立连接和多路复用连接之间的差异已经足够明显。
保持会话打开
可能还希望在会话不活动时保持会话打开。可以使用 ServerAliveInterval
和 ServerAliveCountMax
选项来进行配置,详细信息请参见 ssh_config(5)
。
使用 sslh 多路复用 HTTPS 和 SSH
一种不同类型的多路复用是将多个协议通过同一端口传输。sslh
就是实现这一功能的工具,它支持 SSL 和 SSH 协议。它能够自动识别传入连接的类型,并将其转发到相应的服务。因此,它允许服务器同时接收 HTTPS 和 SSH 请求,通过同一个端口,从而在某些情况下使得从受限防火墙后连接到服务器成为可能。请注意,sslh
并不会隐藏 SSH,甚至通过监听端口快速扫描(例如使用 scanssh(1)
)也能发现 SSH 的存在。值得注意的是,这种方法只适用于较为简单的包过滤器,如 PF 或 nftables(8) 及其前端工具 firewalld 和 UFW,这些过滤器基于目标端口进行过滤,而不会绕过协议分析和应用层过滤器(如 Zorp),这些过滤器是基于实际使用的协议进行过滤的。以下是 sslh(8)
安装的四个步骤:
-
安装 Web 服务器并配置其接受 HTTPS 请求:确保它仅在本地主机上监听 HTTPS。如果服务器监听的是非标准端口(例如 2443 而不是 443),则更有助于防火墙规则的通过。
-
配置 SSH 服务器接受本地主机端口 22 的连接:其实可以使用任何端口,但端口 22 是 SSH 的标准端口。
-
创建一个无特权用户来运行 sslh(8):在下面的示例中,我们使用用户 'sslh' 来作为无特权帐户。
-
安装并启动 sslh(8),使其监听端口 443 并将 HTTPS 和 SSH 请求转发到本地主机的相应端口。将外部 IP 地址替换为您的机器的 IP 地址。执行文件名和路径可能因系统而异:
$ /usr/local/sbin/sslh-fork -u sslh -p xx.yy.zz.aa:443 --tls 127.0.0.1:2443 --ssh 127.0.0.1:22
另一种选择是使用配置文件配置 sslh(8),而不是在运行时传递任何参数。安装包时应该至少包含一个样本配置文件,如 basic.cfg
或 example.cfg
。完成的配置文件应如下所示:
user: "sslh";
listen: ( { host: "xx.yy.zz.aa"; port: "443" } );
on-timeout: "ssl";
protocols:
(
{ name: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
{ name: "ssl"; host: "localhost"; port: "2443"; probe: "builtin"; }
);
请注意引号、逗号和分号的使用。
如果使用的是旧版的 SSH,并结合现在已弃用的 TCP Wrappers(如 hosts_access(5)
中描述的),则可以使用 service:
选项来提供它们所需的服务名称。
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
如果没有使用 TCP Wrappers(大多数情况下是这样),则不需要 service:
选项。
运行时参数将覆盖配置文件中可能设置的任何选项。sslh(8)
支持 HTTP、SSL、SSH、OpenVPN、tinc 和 XMPP 等协议。实际上,任何可以通过正则表达式模式匹配识别的协议都可以使用。sslh
有两个版本:一个是分叉版本(sslh
或 sslh-fork
),另一个是单线程版本(sslh-select
)。更多详细信息请参阅项目网站:sslh README。