注意

本文档适用于 Ceph 开发版本。

msgr2协议(msgr2.0和msgr2.1)

这是一个对 SimpleMessenger 实现的旧版 Ceph 线上协议的修订版本。它解决了性能和安全性问题。

目标

相对于原始协议,此协议修订版本有几个目标:

  • 灵活的握手. 原始协议没有足够灵活的协议协商,允许不需要的功能。

  • Encryption. 我们将在线路上加入加密。

  • 性能. 我们希望提供协议功能(例如,填充),以尽可能将计算和内存副本保持在快速路径之外。

  • 签名. 我们将允许对流量进行签名(但不一定加密)。这尚未实现。

定义

  • 客户端(C): 初始化(TCP)连接的一方

  • 服务器(S): 接受(TCP)连接的一方

  • connection: 两个进程之间的(TCP)连接实例。

  • entity: 一个 Ceph 实体的实例,例如 ‘osd.0’。每个实体通过“nonce”字段具有一个或多个唯一的 entity_addr_t,通常是一个 pid 或随机值。

  • 会话: 两个实体之间的有状态会话,其中消息交换是有序且无损的。如果发生中断(TCP 连接断开),会话可能会跨越多个连接。

  • : 两个对等方之间发送的离散消息。每个帧由一个标签(类型代码)、有效载荷和(如果启用签名或加密)一些其他字段组成。下面是结构。

  • tag: 与帧相关联的类型代码。标签确定有效载荷的结构。

阶段

连接有四个不同的阶段:

  1. 旗帜

  2. 认证帧交换

  3. 消息流握手帧交换

  4. 消息帧交换

帧格式

交换旗帜后,所有进一步的通信都在帧中进行。帧的确切格式取决于连接模式(msgr2.0-crc、msgr2.0-secure、msgr2.1-crc 或 msgr2.1-secure)。所有连接都以 crc 模式开始(无论是 msgr2.0-crc 还是 msgr2.1-crc,取决于来自旗帜的 peer_supported_features)。

每个帧都有一个 32 字节的预序:

__u8 tag
__u8 number of segments
{
  __le32 segment length
  __le16 segment alignment
} * 4
__u8 flags
reserved (1 byte)
__le32 preamble crc

一个空帧有一个空段。一个非空帧可以有一个到四个段,除了最后一个段之外的所有段都可以为空。

如果段数少于四个,未使用(尾部)段长度和段对齐字段被清零。

### 目前支持的标志

  1. FRAME_EARLY_DATA_COMPRESSED (见 472c1f: 压缩后帧格式压缩后帧格式)

保留字节被清零。

预序校验和是 CRC32-C。它覆盖自身(28 字节)以及自身之前的内容,并且无论连接模式如何(即使帧被加密)都会被计算和验证。

### msgr2.0-crc 模式

一个 msgr2.0-crc 帧的形式如下:

preamble (32 bytes)
{
  segment payload
} * number of segments
epilogue (17 bytes)

其中尾声是:

__u8 late_flags
{
  __le32 segment crc
} * 4

late_flags 用于帧中止。在发送预序和第一个段后,发送者可以用零填充其余的段,并设置一个标志,指示接收者必须丢弃帧。这允许发送者在帧被取消(即从信使中移除)时避免额外的缓冲:有效载荷缓冲区可以立即解除固定并交还给用户,而无需复制或阻塞直到整个帧发送完毕。目前这仅由内核客户端使用,见 ceph_msg_revoke()。

段校验和是 CRC32-C。对于“使用”的空段,它被设置为 (__le32)-1。对于未使用(尾部)段,它被清零。

crc 被计算只是为了防止位错误。与 msgr1 不同,msgr1 尝试通过可选地使用会话密钥对段长度和 crc 进行签名来提供一些真实性保证。

问题:

  1. 作为引入一个具有可变段数的通用帧结构的一部分,该结构适用于控制和消息帧,msgr2.0 将消息帧(ceph_msg_header2)的第一个段的 crc 移动到了尾声。

    结果,ceph_msg_header2 无法在从线路上读取整个帧之前安全地解释。这是从 msgr1 回退,因为为了将有效载荷直接分散到用户提供的缓冲区中,从而在接收消息帧时避免额外的缓冲和复制,ceph_msg_header2 必须提前可用——它存储了用户缓冲区所依据的事务 ID。

  2. late_flags 没有被任何 crc 覆盖。由于它存储了中止标志,一个位的翻转可能会导致完成的帧被丢弃(导致发送者挂起等待回复),或者更糟,可能会导致具有垃圾段有效载荷的中止帧被发送。

    这是 msgr1 的情况,并且被带到 msgr2.0 中。

### msgr2.1-crc 模式

与 msgr2.0-crc 的区别:

  1. 第一个段的 crc 存储在第一个段的末尾,而不是在尾声中。尾声最多存储三个 crc,而不是四个。

    如果第一个段为空,则不生成 (__le32)-1 crc。

  2. 只有当帧有多个段时(即至少第二个到第四个段之一不为空时),才会生成尾声。理由:如果帧只有一个段,它不能被中止,并且没有需要在尾声中存储的 crc。

  3. 未校验的 late_flags 被 late_status 替换,late_status 通过使用每个标志的 4 位字节和两个汉明距离为 4 的代码字来构建内置位错误检测。当然,这以只有一个保留标志为代价。

一些示例帧:

  • 一个 0+0+0+0 帧(空,没有尾声):

    preamble (32 bytes)
    
  • 一个 20+0+0+0 帧(没有尾声):

    preamble (32 bytes)
    segment1 payload (20 bytes)
    __le32 segment1 crc
    
  • 一个 0+70+0+0 帧:

    preamble (32 bytes)
    segment2 payload (70 bytes)
    epilogue (13 bytes)
    
  • 一个 20+70+0+350 帧:

    preamble (32 bytes)
    segment1 payload (20 bytes)
    __le32 segment1 crc
    segment2 payload (70 bytes)
    segment4 payload (350 bytes)
    epilogue (13 bytes)
    

其中尾声是:

__u8 late_status
{
  __le32 segment crc
} * 3

Hello

  • TAG_HELLO: 客户端到服务器和服务器到客户端:

    __u8 entity_type
    entity_addr_t peer_socket_address
    
    • 我们立即共享我们的实体类型和对等方的地址(这可以用于检测我们的有效 IP 地址,尤其是在存在 NAT 的情况下)。

身份验证

  • TAG_AUTH_REQUEST: 客户端到服务器:

    __le32 method;  // CEPH_AUTH_{NONE, CEPHX, ...}
    __le32 num_preferred_modes;
    list<__le32> mode  // CEPH_CON_MODE_*
    method specific payload
    
  • TAG_AUTH_BAD_METHOD 服务器到客户端:拒绝客户端选择的认证方法:

    __le32 method
    __le32 negative error result code
    __le32 num_methods
    list<__le32> allowed_methods // CEPH_AUTH_{NONE, CEPHX, ...}
    __le32 num_modes
    list<__le32> allowed_modes   // CEPH_CON_MODE_*
    
    • 返回尝试的认证方法、错误代码(如果方法是未支持的,则为 -EOPNOTSUPP),以及允许的认证方法列表。

  • TAG_AUTH_REPLY_MORE: 服务器到客户端:

    __le32 len;
    method specific payload
    
  • TAG_AUTH_REQUEST_MORE: 客户端到服务器:

    __le32 len;
    method specific payload
    
  • TAG_AUTH_DONE: (服务器到客户端):

    __le64 global_id
    __le32 connection mode // CEPH_CON_MODE_*
    method specific payload
    
    • 服务器是决定认证是否完成以及最终连接模式的一方。

当客户端使用允许的认证方法时,认证阶段交互的示例:

当客户端作为第一次尝试使用禁止的认证方法时,认证阶段交互的示例:

认证后帧格式

根据从 TAG_AUTH_DONE 协商的连接模式,连接要么保持在 crc 模式,要么切换到相应的安全模式(msgr2.0-secure 或 msgr2.1-secure)。

### msgr2.0-secure 模式

一个 msgr2.0-secure 帧的形式如下:

{
  preamble (32 bytes)
  {
    segment payload
    zero padding (out to 16 bytes)
  } * number of segments
  epilogue (16 bytes)
} ^ AES-128-GCM cipher
auth tag (16 bytes)

其中尾声是:

__u8 late_flags
zero padding (15 bytes)

late_flags 与 msgr2.0-crc 模式中的含义相同。

每个段和尾声都被零填充到 16 字节。技术上,GCM 不需要任何填充,因为计数器模式(GCM 中的 C)本质上将分组密码转换为流密码。但是,如果整体输入长度不是 16 字节的倍数,由于 GCM 用于生成认证标签的 GHASH 函数只对 16 字节块工作,内部会发生一些隐式的零填充。

问题:

  1. 发送者使用单个 nonce 对整个帧进行加密,并生成单个认证标签。由于段长度存储在预序中,接收者别无选择,只能在未经验证认证标签的情况下解密和解释预序——否则它甚至无法告诉从线路上读取多少才能得到认证标签!这创建了一个解密预言机,在与计数器模式的可塑性相结合的情况下,可能导致敏感信息的恢复。

    这也适用于消息帧的第一段。与 msgr2.0-crc 模式一样,ceph_msg_header2 在整个帧从线路上读取之前无法安全地解释。

  2. 使用 4 字节计数器字段后跟 8 字节固定字段构建确定性 nonce。初始值取自连接秘密——在认证阶段生成的随机字节串。由于计数器字段只有四个字节长,它在不到一天内就会回绕并重复,导致 GCM nonce 重复,因此连接的真实性和机密性都可能完全丢失。这个问题通过在计数器重复之前断开连接来解决(CVE-2020-1759)。

### msgr2.1-secure 模式

与 msgr2.0-secure 的区别:

  1. 预序、第一个段和帧的其余部分使用单独的 nonce 和生成单独的认证标签进行加密。这消除了未验证的明文使用,并使 msgr2.1-secure 模式接近 msgr2.1-crc 模式,允许实现以类似的方式接收消息帧(很少或没有缓冲,相同的分散/收集逻辑等)。

    为了减少每帧的加密/解密操作数量,预序通过一个固定大小的内联缓冲区(48 字节)增长,第一个段被内联到其中,完全或部分内联。预序认证标签覆盖预序和内联缓冲区,因此如果第一个段足够小可以完全内联,它将在单个解密操作后可用。

  2. 与 msgr2.1-crc 模式一样,只有在帧有多个段时才会生成尾声。理由更强,因为它需要额外的加密/解密操作。

  3. 为了与 msgr2.1-crc 模式保持一致,late_flags 被替换为 late_status(在安全模式下内置的位错误检测确实不是必需的)。

  4. 根据NIST GCM 建议使用,使用 4 字节固定字段后跟 8 字节计数器字段构建确定性 nonce。8 字节计数器字段永远不会重复,但为 msgr2.0-secure 模式设置的 nonce 重复保护仍然存在。

    初始值与 msgr2.0-secure 模式相同。

与 msgr2.0-secure 模式一样,每个段都被零填充到 16 字节。如果第一个段完全内联,则其填充进入内联缓冲区。否则,填充在剩余部分上。这个推论是内联缓冲区以 16 字节块被消耗。

内联缓冲区的未使用部分被清零。

一些示例帧:

  • 一个 0+0+0+0 帧(空,没有内联内容,没有尾声):

    {
      preamble (32 bytes)
      zero padding (48 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    
  • 一个 20+0+0+0 帧(第一个段完全内联,没有尾声):

    {
      preamble (32 bytes)
      segment1 payload (20 bytes)
      zero padding (28 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    
  • 一个 0+70+0+0 帧(没有内联内容):

    {
      preamble (32 bytes)
      zero padding (48 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    {
      segment2 payload (70 bytes)
      zero padding (10 bytes)
      epilogue (16 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    
  • 一个 20+70+0+350 帧(第一个段完全内联):

    {
      preamble (32 bytes)
      segment1 payload (20 bytes)
      zero padding (28 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    {
      segment2 payload (70 bytes)
      zero padding (10 bytes)
      segment4 payload (350 bytes)
      zero padding (2 bytes)
      epilogue (16 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    
  • 一个 105+0+0+0 帧(第一个段部分内联,没有尾声):

    {
      preamble (32 bytes)
      segment1 payload (48 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    {
      segment1 payload remainder (57 bytes)
      zero padding (7 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    
  • 一个 105+70+0+350 帧(第一个段部分内联):

    {
      preamble (32 bytes)
      segment1 payload (48 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    {
      segment1 payload remainder (57 bytes)
      zero padding (7 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    {
      segment2 payload (70 bytes)
      zero padding (10 bytes)
      segment4 payload (350 bytes)
      zero padding (2 bytes)
      epilogue (16 bytes)
    } ^ AES-128-GCM cipher
    auth tag (16 bytes)
    

其中尾声是:

__u8 late_status
zero padding (15 bytes)

late_status 与 msgr2.1-crc 模式中的含义相同。

压缩

压缩握手使用基于特性的 msgr2 握手实现。在此阶段,客户端将向服务器指示如果可以用于消息传输,则可以使用线路压缩,除了支持压缩方法的列表之外。如果客户端和服务器都启用了线路压缩,则服务器将根据客户端的请求和自己的偏好选择压缩方法。握手完成后,两个对等方都设置了他们的压缩处理程序(如果需要)。

  • TAG_COMPRESSION_REQUEST (客户端到服务器):声明压缩能力和要求:

    bool  is_compress
    std::vector<uint32_t> preferred_methods
    
    • 如果客户端确定两个对等方都支持压缩功能,它将启动握手。

    • is_compress 标志指示客户端的配置是否要使用压缩。

    • preferred_methods 是客户端支持的压缩算法列表。

  • TAG_COMPRESSION_DONE (服务器到客户端) : 确定压缩设置:

    bool is_compress
    uint32_t  method
    
    • 服务器根据配置确定是否可能进行压缩。

    • 如果可能,它将选择客户端也支持的优先级最高的压缩方法。

    • 如果不存在,它将确定两个对等方之间的会话将不使用压缩处理。

# msgr2.x-secure 模式

将压缩与加密相结合会带来安全影响。除非管理员特别配置,否则在使用安全模式时将无法进行压缩。

压缩后帧格式

根据从 TAG_COMPRESSION_DONE 协商的连接模式,连接能够接受/发送压缩帧或处理所有帧作为解压缩帧。

# msgr2.x-force 模式

通过此连接发送的所有后续帧如果满足压缩要求(例如,帧大小),则将被压缩。

对于压缩帧,发送对等方将启用 FRAME_EARLY_DATA_COMPRESSED 标志,从而允许接受对等方检测并解压缩帧。

# msgr2.x-none 模式

在预序中禁用 FRAME_EARLY_DATA_COMPRESSED 标志。

消息流握手

在此阶段,对等方互相识别,并(如果需要)重新连接到一个已建立的会话。

  • TAG_CLIENT_IDENT (客户端到服务器):标识我们自己:

    __le32 num_addrs
    entity_addrvec_t*num_addrs entity addrs
    entity_addr_t target entity addr
    __le64 gid (numeric part of osd.0, client.123456, ...)
    __le64 global_seq
    __le64 features supported (CEPH_FEATURE_* bitmask)
    __le64 features required (CEPH_FEATURE_* bitmask)
    __le64 flags (CEPH_MSG_CONNECT_* bitmask)
    __le64 cookie
    
    • 客户端将首先发送,服务器将用相同的内容回复。如果这是一个新会话,客户端和服务器可以继续进行消息交换。

    • target_addr 是客户端尝试连接的目标地址to,以便服务器端可以在客户端与错误的守护进程通信时关闭连接。

    • type.gid (entity_name_t) 在这里设置,通过将 hello 帧中共享的类型与这里的 gid 组合。这意味着我们不需要在每条消息的头部中包含它。这也意味着我们不能“从”其他 entity_name_t 发送消息。当前实现将在 _send_message 等顶部设置此内容,因此这不应该破坏任何现有功能。实现可能需要将其与经过认证的凭据允许的内容进行掩码处理。

    • cookie 是用于标识会话的客户端 cookie,并且可以用于重新连接到现有会话。

    • 我们已经从 msgr1 中删除了“protocol_version”字段。

  • TAG_IDENT_MISSING_FEATURES (服务器到客户端):抱怨具有太少特性的 TAG_IDENT:

    __le64 features we require that the peer didn't advertise
    
  • TAG_SERVER_IDENT (服务器到客户端):接受客户端标识并标识服务器:

    __le32 num_addrs
    entity_addrvec_t*num_addrs entity addrs
    __le64 gid (numeric part of osd.0, client.123456, ...)
    __le64 global_seq
    __le64 features supported (CEPH_FEATURE_* bitmask)
    __le64 features required (CEPH_FEATURE_* bitmask)
    __le64 flags (CEPH_MSG_CONNECT_* bitmask)
    __le64 cookie
    
    • 如果服务器 cookie 在稍后断开连接并且客户端希望重新连接并恢复会话,则客户端可以使用它。

  • TAG_RECONNECT (客户端到服务器):重新连接到已建立的会话:

    __le32 num_addrs
    entity_addr_t * num_addrs
    __le64 client_cookie
    __le64 server_cookie
    __le64 global_seq
    __le64 connect_seq
    __le64 msg_seq (the last msg seq received)
    
  • TAG_RECONNECT_OK (服务器到客户端):确认重新连接尝试:

    __le64 msg_seq (last msg seq received)
    
    • 一旦客户端收到此内容,客户端可以继续进行消息交换。

    • 一旦服务器发送此内容,服务器可以继续进行消息交换。

  • TAG_RECONNECT_RETRY_SESSION (仅服务器):由于连接序列过时而重新连接失败:

  • TAG_RECONNECT_RETRY_GLOBAL (仅服务器):由于全局序列过时而重新连接失败

  • TAG_RECONNECT_WAIT (仅服务器):由于连接竞争而重新连接失败。

    • 指示服务器已经正在连接到客户端,并且应该赢得竞争。客户端应该等待该连接完成。

  • TAG_RESET_SESSION (仅服务器):要求客户端重置会话:

    __u8 full
    
    • full_flag 指示对等方是否应该执行完整重置,即,丢弃消息队列。

失败场景示例:

  • 客户端的第一个 client_ident 消息丢失,然后客户端重新连接。

  • 服务器的 server_ident 消息丢失,然后客户端重新连接。

  • 服务器的 server_ident 消息丢失,然后服务器重新连接。

  • 会话建立后连接失败,然后客户端重新连接。

  • 会话建立后由于服务器重置而连接失败,然后客户端重新连接。

RC* 表示重置会话 full_flag 取决于连接的 policy.resetcheck。

  • 会话建立后由于客户端重置而连接失败,然后客户端重新连接。

消息交换

一旦会话建立,我们就可以交换消息。

  • TAG_MSG: 消息:

    ceph_msg_header2
    front
    middle
    data_pre_padding
    data
    
    • ceph_msg_header2 从 ceph_msg_header 修改:
      • 包括 ack_seq。这避免了在大多数情况下需要 TAG_ACK 消息。

      • 移除 src 字段,我们现在从消息流握手(TAG_IDENT)中获取它。

      • 指定 data_pre_padding 长度,可用于调整数据有效载荷的对齐。(注意:这是有用的吗?)

  • TAG_ACK: 确认收到消息(们):

    __le64 seq
    
    • 这仅用于有状态会话。

  • TAG_KEEPALIVE2: 检查连接活性:

    ceph_timespec stamp
    
    • 时间戳是发送者的本地时间。

  • TAG_KEEPALIVE2_ACK: 回复 keepalive2:

    ceph_timestamp stamp
    
    • 时间戳来自我们正在回复的 TAG_KEEPALIVE2。

  • TAG_CLOSE: 终止连接

    指示应终止连接。这相当于挂起或重置(即,应触发 ms_handle_reset)。它并不是严格必要或有用的,因为我们可以直接断开 TCP 连接。

协议交互示例(WIP)

digraph lossy_client {

客户端状态机

digraph lossy_server {

服务器状态机

由 Ceph 基金会带给您

Ceph 文档是一个社区资源,由非盈利的 Ceph 基金会资助和托管Ceph Foundation. 如果您想支持这一点和我们的其他工作,请考虑加入现在加入.