容错 SFTP 脚本编写 - 自动重试失败的传输

引言
现代网络的整体构建于不可靠的媒介之上。路由设备可以自由地丢弃、损坏、重新排序或复制其转发的数据。TCP/IP 中的 IP 层的理解是,不保证准确性。没有 IP 网络可以声称 100% 可靠。
TCP 层充当 IP 之上的守护者,确保其生成的数据是正确的。这通过许多技术来实现,这些技术有时会故意丢失数据,以便确定网络限制。正如大多数人可能知道的那样,TCP 在 IP 无连接网络之上提供了一个基于连接的网络,该网络可以并且确实会随意丢弃流量,并保证交付。
令人好奇的是,我们的文件传输工具在面对断开的 TCP 连接时,并没有类似的鲁棒性。SFTP 协议与其祖先和同类协议相似,都没有努力从导致连接关闭的 TCP 错误中恢复。有一些工具可以解决传输失败的问题(reget
和 reput
),但这些工具不会在重新生成的 TCP 会话中自动触发(那些需要此属性的人通常可能会转向 NFS,但这既需要特权,也需要架构配置)。如果这些工具突然变得普及,用户和网络管理员可能会欣喜若狂。
SFTP 能够提供的是返回状态,当值为零时,它表示成功。它默认不为文件传输返回状态,而仅在以批处理模式调用时才这样做。此返回状态可以被 POSIX shell 捕获,并在非零时重试。即使在 Windows 上,也可以借助 Microsoft 版本的 OpenSSH 和 Busybox(甚至 PowerShell,功能受限)来完成此检查。POSIX shell 脚本看似简单,但并不常见。让我们改变这种情况。
使用 POSIX Shell 进行故障检测
SFTP 容错的核心实现并不特别庞大,但批处理模式保证和标准输入处理增加了一些长度和复杂性,如下面的 Windows 环境所示。
C:\Users\BillG>type sftpft #!/bin/sh set -eu # Shell strict mode tvar=1 for param # Confirm SFTP batch mode do case "$param" in [-]b*) tvar=;; esac done [ -n "$tvar" ] && { printf '%s: must be called with -b\n' "${0##*/}"; exit; } if [ -t 0 ] # Save stdin unless on a terminal then tvar=/dev/null else tvar="$(mktemp -t sftpft-XXXXXX)" cat > "$tvar" if [ -s "$tvar" ] # Save only if stdin isn't empty then trap "rm -v \"$tvar\"" EXIT ABRT INT KILL TERM # Erase at exit else rm "$tvar" tvar=/dev/null fi fi until sftp "$@" < "$tvar" do echo "failed: $? $param" # Report failed transfer and retry sleep 15 done
此 SFTP 包装器的使用中存在一些微妙之处,即可检测错误的返回不是默认设置。为了使 until
在数据错误时触发重试,必须传递 -b
选项,并且可以在关联的批处理命令脚本中使用更多控件来配置错误响应。由于权限不足而导致传输失败的零状态成功报告很容易演示
~ $ echo 'put foobar.txt /var' | sftp -i secret_key billg@macrofirm.com; echo $? Connected to 10.11.12.13. sftp> put foobar.txt /var Uploading foobar.txt to /var/foobar.txt remote open("/var/foobar.txt"): Permission denied 0
检测未发生的传输需要 SFTP 的 -b
选项;如果没有它,只会报告初始连接错误。一个简单的修复方法是添加 -b -
用于标准输入
~ $ echo 'put foobar.txt /var' | sftp -i secret_key -b - billg@macrofirm.com; echo $? sftp> put foobar.txt /var remote open("/var/foobar.txt"): Permission denied 1
该脚本明确确认 -b
参数存在。
在脚本上下文中,大多数 POSIX(和派生)shell 的用户更熟悉上面的 if [
构造来表示条件。但是,大多数 UNIX 系统在 /bin/[
中都有一个程序,该程序将评估 POSIX test
并返回状态。我们可以改为编写 if /bin/[
或 if /bin/test
以使用完整路径直接调用任一程序(原始 Bourne shell 始终这样做,但大多数现代 shell 将 [
实现为“内置”以提高速度)。if
和 until
都可以执行任何程序,包括 SFTP,但 if
用于分支,而 until
用于循环。当出现传输问题时,我们希望循环。
发送到 sftp 的参数与提供给父脚本的参数完全相同,通过 $@
shell 变量,在 Korn shell 文档中对此有最佳描述
$@ Same as $*, unless it is used inside double quotes, in which case a separate word is generated for each positional parameter. If there are no positional parameters, no word is generated. $@ can be used to access arguments, verbatim, without losing NULL argu‐ ments or splitting arguments with spaces.
当 SFTP 会话正常运行时,until
块(在 do
和 done
之间)内的脚本永远不会被触发;只有在初始 TCP 连接失败时,或者 a) SFTP 以批处理模式使用,并且 b) 非忽略命令失败时才会被调用(如下所述)。错误消息结合了 $?
shell 变量中保存的(非零)返回代码和命令行上的最后一个参数。让我们在带有 Busybox 的 Windows 系统上演示,作为测试,我断开服务器的以太网网络电缆,调用传输并等待两次失败,然后重新连接
C:\Users\BillG>type sbatch dir C:\Users\BillG>busybox sh ~ $ ./sftpft -i secret_key -b sbatch billg@sftp.macrofirm.com ssh: connect to host sftp.macrofirm.com port 22: Connection timed out Connection closed failed: 255 billg@sftp.macrofirm.com ssh: connect to host sftp.macrofirm.com port 22: Connection timed out Connection closed failed: 255 billg@sftp.macrofirm.com sftp> dir CP046020-iLO.scexe ... ~ $ exit C:\Users\BillG>ssh -V OpenSSH_for_Windows_8.1p1, LibreSSL 3.0.2
SFTP 的文档(从 RedHat v9 中提取)揭示了会影响错误报告的其他控件和微妙之处
$ man sftp | sed -n '/batchfile$/,/bsd/p' -b batchfile Batch mode reads a series of commands from an input batchfile in‐ stead of stdin. Since it lacks user interaction it should be used in conjunction with non-interactive authentication to obvi‐ ate the need to enter a password at connection time (see sshd(8) and ssh-keygen(1) for details). A batchfile of ‘-’ may be used to indicate standard input. sftp will abort if any of the following commands fail: get, put, reget, reput, rename, ln, rm, mkdir, chdir, ls, lchdir, chmod, chown, chgrp, lpwd, df, symlink, and lmkdir. Termination on error can be suppressed on a command by command basis by prefixing the command with a ‘-’ character (for example, -rm /tmp/blah*). Echo of the command may be suppressed by pre‐ fixing the command with a ‘@’ character. These two prefixes may be combined in any order, for example -@ls /bsd.
我们上面使用的 SFTP 批处理脚本由单个命令 dir
(它是 ls
命令的别名)组成。对于多个 SFTP 命令,我们可以自由地允许一些命令失败,但在更复杂的使用中检测和重试其他命令。
强制我们之前失败的传输成功返回只需要另一个破折号
~ $ echo '-put foobar.txt /var' | sftp -i secret_key -b - billg@macrofirm.com; echo $? sftp> -put foobar.txt /var remote open("/var/foobar.txt"): Permission denied 0
对于那些通过容易发生 TCP 重置的连接传输大型文件的人来说,reget
和 reput
选项将保留成功传输的部分内容,而不是完全重新启动失败的传输。在这种情况下,重要的是要确保 reget
不会针对不匹配的文件发出(确保目标文件在最初调用 sftpft
中的 reget
或 reput
之前不存在)。
这是一个 ISO 镜像传输会话的示例,该会话因断开服务器的网络电缆而反复中断。首先,批处理文件将检查 ISO,然后发出 reget
~ $ cat sbatch !dir OracleLinux-R8-U2-x86_64-dvd.iso reget OracleLinux-R8-U2-x86_64-dvd.iso
此 ISO 文件总共 7.8 GiB。传输在下载了 817 MiB 时被断开网络电缆中断,然后在本地存储了 2.5 GiB 后再次中断。
~ $ ./sftpft -i secret_key -b sbatch billg@sftp.macrofirm.com sftp> !dir OracleLinux-R8-U2-x86_64-dvd.iso Volume in drive C is OSDisk Volume Serial Number is E44B-22EC Directory of C:\Users\BillG File Not Found sftp> reget OracleLinux-R8-U2-x86_64-dvd.iso client_loop: send disconnect: Connection reset Connection closed failed: 255 billg@sftp.macrofirm.com sftp> !dir OracleLinux-R8-U2-x86_64-dvd.iso Volume in drive C is OSDisk Volume Serial Number is E44B-22EC Directory of C:\Users\BillG 12/20/2022 10:14 AM 857,309,184 OracleLinux-R8-U2-x86_64-dvd.iso 1 File(s) 857,309,184 bytes 0 Dir(s) 115,628,781,568 bytes free sftp> reget OracleLinux-R8-U2-x86_64-dvd.iso client_loop: send disconnect: Connection reset Connection closed failed: 255 billg@sftp.macrofirm.com ssh: connect to host 10.11.12.13 port 22: Connection timed out Connection closed failed: 255 billg@sftp.macrofirm.com ssh: connect to host 10.11.12.13 port 22: Connection timed out Connection closed failed: 255 billg@sftp.macrofirm.com sftp> !dir OracleLinux-R8-U2-x86_64-dvd.iso Volume in drive C is OSDisk Volume Serial Number is E44B-22EC Directory of C:\Users\BillG 12/20/2022 10:17 AM 2,638,348,288 OracleLinux-R8-U2-x86_64-dvd.iso 1 File(s) 2,638,348,288 bytes 0 Dir(s) 113,851,338,752 bytes free sftp> reget OracleLinux-R8-U2-x86_64-dvd.iso
传输完成后,计算了两侧的 SHA 校验和,结果匹配。
~ $ ls -l OracleLinux-R8-U2-x86_64-dvd.iso -rw-rw-r-- 1 BillG BillG 8337227776 Dec 20 10:27 OracleLinux-R8-U2-x86_64-dvd.iso ~ $ sha256sum OracleLinux-R8-U2-x86_64-dvd.iso 67568941e976efb26a3d61cdbf98c5a46cd0b3463ec750992f305eee20957a6e OracleLinux-R8-U2-x86_64-dvd.iso ~ $ ssh -i secret_key billg@sftp.macrofirm.com Last login: Mon Dec 19 15:36:37 2022 $ sha256sum OracleLinux-R8-U2-x86_64-dvd.iso 67568941e976efb26a3d61cdbf98c5a46cd0b3463ec750992f305eee20957a6e OracleLinux-R8-U2-x86_64-dvd.iso
一个脚本编写的关注点是标准输入的处理。当使用 -b -
选项时,SFTP 在 until
的第一次迭代中完全消耗 stdin,为后续运行留下 null。为了在标准输入(必须不是交互式终端)上重复使用 SFTP 批处理命令脚本,必须将其保存在临时文件中,在每次运行时应用,然后在退出陷阱中取消链接
~ $ echo 'put foobar.txt' | ./sftpft -i secret_key -b - billg@sftp.macrofirm.com ssh: connect to host sftp.macrofirm.com port 22: Connection timed out Connection closed failed: 255 billg@sftp.macrofirm.com ssh: connect to host sftp.macrofirm.com port 22: Connection timed out Connection closed failed: 255 billg@sftp.macrofirm.com sftp> put foobar.txt removed 'C:/Users/BillG/AppData/Local/Temp/sftpft-a01472'
请注意,Windows 版本的 Busybox 仅处理 EXIT 和 ERR 陷阱;所有其他信号都将被忽略。
另一个问题是执行此脚本的最佳 shell 选择。Debian dash
是一个非常好的选择,它体积小且速度快。MirBSD mksh
在 Android 上拥有庞大的安装基础。ksh-93
和 bash
都相当大,bash
的手册页确实将其描述为“太大且太慢”。所有这些符合 POSIX 标准的 shell 都可以使用(还有更多);选择你喜欢的一个。
对于存在问题的 WAN 连接,这种 SFTP 方法是提高可靠性的福音。它将结束关键数据的下班后支持电话,这些数据将被延迟但不会被丢弃。它还可以使用日期、时间、traceroute 和其他网络诊断工具进行检测,以识别您的提供商的特定故障。
没有 IP 网络是(或可能是)可靠的。TCP 成功地创建了可靠性的表面,但网络会发生故障。包括 SFTP 在内的网络传输通常应该从 TCP 故障中优雅地恢复。您的网络管理员会感谢您广泛使用这些技术。
特别感谢 Ron Yorston,Windows Busybox 端口的维护者,感谢他在本节要素方面的建议。
PowerShell
虽然 POSIX shell 主要存在于 1988 年版本的 Korn shell 的功能中,并在 1992 年的 POSIX.2 标准中完全定义,但 PowerShell 是一种年轻得多的语言,仍在获得核心功能的对等性。
基本的 SFTP 重试可以在 PowerShell 中以以下形式实现
do {sftp -i secret_key -b sbatch billg@sftp.macrofirm.com} while (-not $?)
合并延迟(对于所有传输,即使成功)可以使用以下方法完成
do {sftp -i secret_key -b sbatch billg@sftp.macrofirm.com;$n=$?;sleep 15} while (-not $n)
早期版本的 PowerShell 似乎没有 $@
的实现,因此通用脚本可能更具挑战性。这种努力留给读者作为练习。
$?
(退出状态)也似乎是布尔值而不是整数类型。这足以强制 SFTP 在失败时重试,但不如 POSIX shell 的报告精细。请注意,我们上面已经看到了两个不同的 SFTP 错误代码,1 表示 /var
上的权限失败,255 表示 TCP 连接失败。Microsoft 捆绑的 curl 返回 90 多个不同的退出状态代码,如 RedHat 9 文档中所报告的(其中包括比 Windows 10 中当前存在的 curl 更早的版本),这些代码具有不同的含义,PowerShell 的 $?
状态布尔值对此是不透明的。
也许 PowerShell 维护人员可能会考虑追溯定义 $¿ 为整数类型,以报告已完成程序的实际退出状态。这在各种情况下都很有用。
Microsoft 对当前软件的请求做出了高度响应,他们的 curl 和 OpenSSH 端口证明了这一点。如果最近的努力持续下去,PowerShell 的其他功能肯定会得到仔细考虑。
其他用法
还有许多其他实用程序返回类似的状态,并且可以配置为在失败时重试。考虑到 Windows 用户,主要重点应该是
- Curl,Microsoft 现在将其捆绑到现代版本的 Windows 中,以及
- PuTTY psftp,多年来一直是 Windows 上最流行的 SSH 客户端,并提供了一些 OpenSSH 中没有的功能。
Microsoft 的 curl 端口已经存在一段时间了,支持多种传输协议(但奇怪的是不支持 SFTP)
C:\Users\BillG>curl --version curl 7.83.1 (Windows) libcurl/7.83.1 Schannel Release-Date: 2022-05-13 Protocols: dict file ftp ftps http https imap imaps pop3 pop3s smtp smtps telnet tftp Features: AsynchDNS HSTS IPv6 Kerberos Largefile NTLM SPNEGO SSL SSPI UnixSockets
PuTTY 实用程序也会报告状态,并具有 OpenSSH 明确省略的密码处理功能(这可能会被滥用)
C:\Users\BillG>psftp --version psftp: Release 0.76 Build platform: 64-bit x86 Windows Compiler: clang 13.0.0 (https://github.com/llvm/llvm-project/ ab5ee342b92b4661cfec3cdd647c9a5c18e346dd), emulating Visual Studio 2013 (12.0), _MSC_VER=1800 Source commit: 1fd7baa7344bb38d62a024e5dba3a720c67d05cf
SCP
SCP 实用程序在错误报告方面更直接,默认情况下返回指示传输成功的退出状态。它更容易适应容错脚本编写,并且从历史上看,它比 SFTP 快一些。
但是,出于以下描述的原因,建议在关键传输中避免使用 SCP。
OpenSSH 8.7 首次引入了修改后的 SCP,它使用 SFTP 作为线路协议。这被建议为 OpenSSH 8.8 中 SCP 的弃用,默认处理在 OpenSSH 9.0 中切换(尽管 SCP 客户端的 -O
选项可以在允许的情况下恢复为经典服务器)。
SCP 缺点的部分列表
- 关于 SCP 中历史缺陷的讨论加剧了担忧。
- 多个 CVE,在当前协议的限制范围内无法解决。
- 使用旧客户端和服务器的风险。
- 新旧版本混合的协议歧义。
- 在服务器上将
chroot()
应用于 SFTP 的便利性。
考虑到 PuTTY pscp
实用程序可能比任何形式的 OpenSSH SCP 更可取,这是因为 PuTTY 在 SCP 安全问题方面有着悠久的历史(PuTTY 实用程序套件的 UNIX 端口存在并且运行良好)。PuTTY 在 2002 年的 0.52 版本中为 pscp
实现了 SFTP 后端,并且“只有在找不到 SFTP 时才会回退到旧的 scp1 形式。”在 0.52 版本发布时,更改日志指出“scp1 的服务器端通配符实现本质上是不安全的。”在使用 pscp
时强制 SFTP 模式是最佳实践,并特别避免 -unsafe
模式,该模式会给恶意 SCP 服务器提供自由发挥的空间。
稳定性和安全性有利于 SFTP。不幸的是,这些好处是有代价的。
SFTP 性能基准测试
SFTP 在某些情况下一直受到性能问题的困扰,因为它没有利用TCP 滑动窗口,因此性能可能不如它在很大程度上取代的 FTP 协议。
为了量化这种性能损失,在两台运行 Oracle UEK Linux 内核的 HPE DL380 服务器之间进行了测试。sshd
配置了 Ciphers aes128-gcm@openssh.com
,以将所有传输限制为能够利用服务器 CPU 的 AES-NI 加速的最快 AEAD 密码。传输试验首先在处理测试控制之外的无关流量的企业防火墙上进行,然后通过使用交叉电缆直接连接的以太网进行。
这些性能测试的结果有些令人惊讶。
一个 32 GiB 的文件被用作所有试验的测试传输
# ll backup_file.dat -rw-r--r--. 1 BillG BillG 34344017920 Jan 25 2021 backup_file.dat # ll -h backup_file.dat -rw-r--r--. 1 BillG BillG 32G Jan 25 2021 backup_file.dat
首先,使用 POSIX shell “here document” 执行 SFTP 检索,并记录挂钟时间
time sftp -q billg@sftp.macrofirm.com <<-''EndOfSFTP get backup_file.dat quit EndOfSFTP real 8m35.846s real 8m43.507s real 8m43.883s real 8m44.184s real 8m46.229s
当使用直连网络接口时,传输时间几乎减半
real 4m53.244s real 4m54.488s real 4m54.497s
然后尝试了 SCP,它似乎表现出较小的性能优势
time scp -q billg@sftp.macrofirm.com:backup_file.dat . real 8m4.313s real 8m8.493s real 8m12.369s real 8m14.518s real 8m16.414s
当改用直连接口时,SFTP 和 SCP 之间的小差异消失了
real 4m53.007s real 4m53.578s real 4m53.671s
直接通过标准输入拉取似乎提供了微小的改进
f=backup_file.dat time ssh billg@sftp.macrofirm.com "cat $f" > $f real 7m46.035s real 8m4.932s real 8m6.543s real 8m12.891s real 8m14.345s
这种优势在直接连接上也消失了
real 4m55.404s real 4m55.705s real 4m57.028s
最后,使用一对 Netcat 建立了直接 TCP 连接,这应该具有与明文 FTP 相似的性能,表明 SSH 连接的总体性能成本
nc --send-only -l 65432 < backup_file.dat time nc --recv-only sftp.macrofirm.com 65432 > delme real 7m44.775s real 7m46.638s real 7m49.271s real 7m49.669s real 7m51.181s
这种领先优势也随着直接连接而消失
real 4m53.328s real 4m54.608s real 4m54.577s
对这些模式的最佳猜测可能是 SFTP 更难控制数据流中的延迟,并且这似乎会随着连接距离的增长而影响带宽。
结论
SFTP 似乎并非旨在轻松地从 FTP 或 SCP 迁移。它在许多方面改进了 FTP,但付出了一些性能代价。SFTP 错误报告默认情况下也禁用数据故障,并且检测关键传输中断现在比 SCP 更加繁琐。
它确实提供了强大的安全优势,因为 OpenSSH 已经对其进行了重新设计,使其在 chroot()
中运行,从而将整个 SFTP 服务器引入到主 SSHD 中。结合强大的加密、特权分离(在支持的情况下)和 reget/reput
,SFTP 提供了比它所取代的传统协议更高的安全性和灵活性。
尽管存在缺点,但 SFTP 已获得作为安全文件传输代理的卓越地位。通过添加容错处理技术,几乎没有理由回退到传统协议,尽管会带来一些性能损失。完美是优秀的敌人,行业已认为 SFTP 足够好;逆流而上几乎毫无益处。