Openrsync:由OpenBSD团队开发的rsync实现
摘要
Openrsync是OpenBSD团队采用BSD许可重新实现的rsync,兼容现代rsync协议并支持Unix系统。
查看缓存全文
缓存时间: 2026/05/30 13:26
kristapsdz/openrsync
来源:https://github.com/kristapsdz/openrsync
简介
该系统已合并到 OpenBSD 基础系统中。如果您希望为 openrsync 做出贡献,请将补丁邮件发送至 [email protected]。此仓库仅为 OpenBSD 版本加上一些可移植性粘合剂。
这是一个带有 BSD(ISC)许可证的 rsync(https://rsync.samba.org/)实现。它与现代 rsync(测试使用 3.1.3 版本,但任何支持协议 27 的版本均可)兼容,但仅接受 rsync 命令行参数的一个子集。
其官方支持的操作系统是 OpenBSD,但也可以在其他 UNIX 系统上编译运行。详情请参见可移植性。
openrsync 的规范文档是其手册页。协议细节请参阅 rsync(5)(https://github.com/kristapsdz/openrsync/blob/master/rsync.5)和 rsyncd(5)(https://github.com/kristapsdz/openrsync/blob/master/rsyncd.5);实用工具文档请参考 openrsync(1)(https://github.com/kristapsdz/openrsync/blob/master/openrsync.1)。如果您想编写自己的 rsync 实现,协议手册页应包含所有必要信息。
本页的架构和算法部分旨在为开发者介绍源代码。它们不是规范文档。
项目背景
openrsync 是 rpki-client(1)(https://medium.com/@jobsnijders/a-proposal-for-a-new-rpki-validator-openbsd-rpki-client-1-15b74e7a3f65)项目的一部分,该项目是 OpenBSD 的 RPKI(https://en.wikipedia.org/wiki/Resource_Public_Key_Infrastructure)验证器。openrsync 由 NetNod(https://www.netnod.se)、IIS.SE(https://www.iis.se)、SUNET(https://www.sunet.se)和 6connect(https://www.6connect.com)资助。
安装
在最新的 UNIX 系统上,只需下载并运行:
`` % ./configure % make
make install
``
这将安装 openrsync 工具和手册页。同时安装 rsync 也是可以的:两者不会以任何方式冲突。
如果升级源码并希望重新安装,只需重复上述步骤。如果要卸载:
``
make uninstall
``
如果希望 openrsync 作为服务器运行,可以执行以下命令:
% rsync --rsync-path=openrsync src/* dst % openrsync --rsync-path=openrsync src/* dst
如果希望 openrsync 和 rsync 交互,请务必使用两者都支持的命令行选项。参见 openrsync(1)(https://github.com/kristapsdz/openrsync/blob/master/openrsync.1)获取列表。
算法
关于 rsync 算法的详细描述,请参阅 Andrew Tridgell 和 Paul Mackerras 的“The rsync algorithm(https://rsync.samba.org/tech_report/)”。Andrew Tridgell 的博士论文“Efficient Algorithms for Sorting and Synchronization(https://www.samba.org/~tridge/phd_thesis.pdf)”更详细地涵盖了该主题。以下内容适合深入理解源代码。
rsync 算法包含两个组件:发送方和接收方。发送方管理源文件;接收方管理目标文件。在以下调用中,第一个例子发送方是主机 remote,接收方是本地主机;第二个例子则相反。
% openrsync -lrtp remote:foo/bar ~/baz/xyzzy % openrsync -lrtp ~/foo/bar remote:baz/xyzzy
该算法依赖于一个包含文件名和元数据(如 mode、mtime 等)的文件列表,该列表在组件之间共享。文件列表描述了所有源文件,由发送方生成。共享过程在 flist.c(https://github.com/kristapsdz/openrsync/blob/master/flist.c)中实现。
共享列表后,接收方和发送方独立按照文件名的字典序对条目进行排序。这允许文件列表以无序方式发送和接收。排序保持目录优先顺序,因此目录会在其包含的文件之前处理。此外,排序后,发送方和接收方都可以通过排序数组中的位置引用文件条目。
接收方读取列表后,遍历列表中的每个文件,将信息传递给发送方,以便发送方发回更新文件的指令。这称为“块交换”,是 rsync 算法的主要部分。在块交换期间,发送方等待接收更新请求或序列结束消息;收到请求后,它会扫描新的块并发送给接收方。
块交换完成后,所有文件都更新到最新状态。
接收方在 receiver.c(https://github.com/kristapsdz/openrsync/blob/master/receiver.c)中实现;发送方在 sender.c(https://github.com/kristapsdz/openrsync/blob/master/sender.c)中实现。块交换的大部分逻辑在 blocks.c(https://github.com/kristapsdz/openrsync/blob/master/blocks.c)中完成。
块交换
块交换序列根据文件类型(目录、符号链接或普通文件)而不同。
对于符号链接,接收方所需的信息已经编码在文件列表元数据中。符号链接会被更新以指向正确的目标。无需向发送方请求更新。
对于目录,如果目录不存在则创建。无需向发送方请求更新。
普通文件的处理如下。首先,检查文件是否已是最新。如果文件大小和最后修改时间相同,则说明已是最新,无需向发送方请求更新。
否则,接收方以固定大小的块检查每个文件。参见块大小获取详细信息。(如果文件大小不能被块大小整除,最后一块可能较小。)如果文件为空或不存在,则块数为零。每个块进行两次哈希:首先,使用快速的 Adler-32 类型 4 字节哈希;其次,使用较慢的 MD4 16 字节哈希。这些哈希在 hash.c(https://github.com/kristapsdz/openrsync/blob/master/hash.c)中实现。接收方将文件的块哈希发送给发送方。
发送方收到后,使用给定的块检查对应的源文件。对于源文件中的每个字节,发送方根据块大小计算快速哈希。然后它在接收到的块信息中查找匹配的快速哈希。如果找到匹配,则计算并检查慢速哈希。如果没有匹配,则继续到下一个字节。匹配(以及所有块操作)在 block.c(https://github.com/kristapsdz/openrsync/blob/master/block.c)中实现。
找到匹配时,首先将匹配前的数据作为字节流发送给接收方。然后发送找到的块标识符,如果没有更多数据则发送零。
接收方首先写入字节流,然后根据指定标识符复制块中的数据。此过程持续到文件末尾,此时文件完全重建。
如果接收方侧文件不存在(基础情况),则整个文件作为字节流发送。
之后,对整个文件使用 MD4 哈希进行哈希计算。然后比较这些哈希;如果成功,算法继续处理下一个文件。
块大小
块大小算法对协议效率至关重要。通常,块大小是文件总大小的四舍五入平方根。但最小块大小为 700 B。否则,平方根计算仅使用 sqrt(3)(https://man.openbsd.org/sqrt.3)后跟 ceil(3)(https://man.openbsd.org/ceil.3)。
出于未知原因,平方根结果向上取整到最接近的八的倍数。
架构
每个 openrsync 会话分为一个运行的服务器和客户端进程。客户端 openrsync 进程由用户启动。
% openrsync -rlpt host:path/to/source dest
服务器 openrsync 在远程主机上执行,可以按需通过 ssh(1)(https://man.openbsd.org/ssh.1)启动,也可以作为持久网络守护进程。如果通过 ssh(1)(https://man.openbsd.org/ssh.1)执行,服务器 openrsync 会通过 –server 标志与客户端(用户启动的 openrsync)区分。
一旦客户端或服务器 openrsync 进程启动,它会检查命令行参数以确定是处于接收方还是发送方模式。(守护进程以协议特定方式接收命令行参数,如 rsyncd(5)(https://github.com/kristapsdz/openrsync/blob/master/rsyncd.5)所述,但其他方面相同。)接收方是文件的目标端;发送方是源端。总是存在一个接收方和一个发送方。
服务器进程通过 –sender 命令行标志明确指定为发送方,否则为接收方。客户端进程通过检查命令行中传递的文件是本地还是远程来隐式确定其状态。
openrsync path/to/source host:destination openrsync host:source path/to/destination
在第一个例子中,客户端是发送方:它将数据发送到服务器。在第二个例子中,情况相反,它接收数据。
客户端的命令行文件可以有以下主机规范,用于确定本地性。
- 本地:../path/to/source ../another
- 远程服务器:host:path/to/source :path/to/another
- 远程守护进程:rsync://host/module/path ::another
主机规范必须一致:所有源必须都是本地或都在同一远程主机上。两者不能同时为远程。(说明:技术上可以做到,但我不清楚为什么 GPL rsync 限制为只能选其一。)
如果源或目标位于远程服务器上,客户端随后 fork(2)(https://man.openbsd.org/fork.2)并通过 ssh(1)(https://man.openbsd.org/ssh.1)在远程主机上启动服务器 openrsync。随后客户端和服务器通过 socketpair(2)(https://man.openbsd.org/socketpair.2)管道通信。如果位于远程守护进程,客户端不进行 fork,而是通过网络 socket(2)(https://man.openbsd.org/socket.2)连接到独立服务器。
服务器的命令行(无论是通过 ssh(1)(https://man.openbsd.org/ssh.1)会话按需生成的 openrsync 还是传递给守护进程的)与客户端不同。
openrsync --server [--sender] . files...
给出的文件要么是接收方模式下的单个目标目录,要么是发送方模式下的源列表。独立的句点(.)对我来说是个谜。
本地性检测以及通往客户端和服务器运行时的路由在 main.c(https://github.com/kristapsdz/openrsync/blob/master/main.c)中处理。用于服务器的客户端在 client.c(https://github.com/kristapsdz/openrsync/blob/master/client.c)中实现,服务器在 server.c(https://github.com/kristapsdz/openrsync/blob/master/server.c)中实现。用于网络守护进程的客户端在 socket.c(https://github.com/kristapsdz/openrsync/blob/master/socket.c)中实现。远程服务器 openrsync 的调用在 child.c(https://github.com/kristapsdz/openrsync/blob/master/child.c)中管理。
一旦客户端和服务器开始运行,它们开始协商通过已连接的套接字传输文件。使用的协议在 rsync(5)(https://github.com/kristapsdz/openrsync/blob/master/rsync.5)中指定。对于守护进程连接,rsyncd(5)(https://github.com/kristapsdz/openrsync/blob/master/rsyncd.5)协议也用于握手。
接收方侧在 receiver.c(https://github.com/kristapsdz/openrsync/blob/master/receiver.c)中管理,发送方侧在 sender.c(https://github.com/kristapsdz/openrsync/blob/master/sender.c)中管理。
接收方侧技术上有两个功能:不仅必须向发送方上传块元数据,还必须处理发送方发送的数据写入。rsync 协议设计为发送方接收块请求并持续向接收方发送数据。
为此,接收方同时作为上传器和下载器执行多任务。这些角色分别在 uploader.c(https://github.com/kristapsdz/openrsync/blob/master/uploader.c)和 downloader.c(https://github.com/kristapsdz/openrsync/blob/master/downloader.c)中实现。多任务通过一个由发送方数据驱动并基于磁盘文件就绪进行校验和上传的有限状态机实现。
上传器扫描文件列表,异步打开文件以处理块。在等待文件打开时,它将控制权交还给事件循环。当文件可用时,它对块进行哈希和校验和并上传给发送方。
下载器等待来自发送方的数据。当数据准备好(并带有它将更新的文件前缀)时,下载器异步打开现有文件以进行任何块复制。当文件可读时,它继续从发送方读取数据并从现有文件中复制。
与 rsync 的差异
rsync 的设计涉及与接收方并行的另一个模式:生成器。它作为从接收方 fork(2)(https://man.openbsd.org/fork.2)的另一个进程实现,并与接收方和发送方通信。
在 openrsync 中,生成器和接收方是同一进程,使用事件循环实现快速响应读写请求。
安全性
除了常规的防御性编程外,openrsync 还大量使用原生安全特性。
可执行代码可用的系统操作首先受限于 OpenBSD 的 pledge(2)(https://man.openbsd.org/pledge.2)。承诺(pledge)取决于操作模式。例如,接收方需要对磁盘的写访问——但仅在非干运行模式(-n)下。守护进程客户端需要 DNS 和网络访问,但仅限于一定程度。pledge(2)(https://man.openbsd.org/pledge.2)允许在操作过程中限制可用资源。
第二个工具是 OpenBSD 的 unveil(2)(https://man.openbsd.org/unveil.2),它限制对文件系统的访问。这可以防止恶意尝试“逃逸”目标目录。它是 chroot(2)(https://man.openbsd.org/chroot.2)的一个有吸引力的替代方案,因为不需要 root 权限即可执行。
在接收方侧,文件系统通过 unveil(2)(https://man.openbsd.org/unveil.2)限制在目标目录及其子目录。创建目标目录后,只能访问或修改该目录内的目标。
最后,MD4 哈希使用 arc4random(3)(https://man.openbsd.org/arc4random.3)生成种子,而不是 time(3)(https://man.openbsd.org/time.3)。这仅在 openrsync 以服务器模式运行时适用,因为服务器生成种子。
可移植性
许多人询问过可移植性。
唯一官方支持的操作系统是 OpenBSD,因为它具有相当的安全特性。不过,openrsync 使用 oconfigure(https://github.com/kristapsdz/oconfigure)以便在非 OpenBSD 系统上编译。这是为了鼓励移植。
目前它在 Linux(glibc 和 musl)、FreeBSD、NetBSD、Mac OS X 和 OmniOS 上是可移植的。这由 GitHub CI 机制强制执行,该系统在这些平台上进行测试。测试的架构包括 x86_64、aarch64 和 s390x。
移植的实际工作是对应 OpenBSD 的 pledge(2)(https://man.openbsd.org/pledge.2)和 unveil(2)(https://man.openbsd.org/unveil.2)提供的安全特性。这些是系统功能的关键元素。没有它们,您的系统将接受来自公共网络的任意数据。
这(我认为)在 FreeBSD 的 Capsicum(https://man.freebsd.org/capsicum(4))上是可能的,但 Linux 的安全设施一团糟,需要专家之手才能正确保护。
rsync 有特定的超级用户运行模式。它还将来自网络的任意数据注入到您的文件系统中。
相似文章
我的极简、内存安全的Go rsync如何规避漏洞
深入探讨极简、内存安全的Go实现的rsync如何避免原始C版本中存在的十几个漏洞,并与OpenBSD的openrsync及纵深防御技术进行对比。
rsync 与愤怒
Rsync 维护者 Andrew Tridgell 为其使用 AI 工具(Claude, Codex, Gemini)以提升安全性并重写测试套件的行为进行辩护,回应了来自开源社区的强烈反对。
LibreOps
LibreOps 是一个黑客集体,基于开源软件提供去中心化的、自由(自由软件意义上的自由)服务,是 Librehosters 网络的一部分,致力于提供尊重隐私的替代方案,以对抗企业服务。
Scrcpy v4.0
Scrcpy v4.0 已发布,为 Linux、Windows 和 macOS 系统提供了通过 USB 或 TCP/IP 镜像和控制 Android 设备的轻量级开源解决方案。
OpenBSD 7.9 发布
OpenBSD 7.9 已发布,包含针对 arm64、amd64、luna88k、riscv64 等架构的平台特定改进,以及各种错误修复和增强的硬件支持。