内核 TLS 卸载

内核 TLS 操作

Linux 内核提供了 TLS 连接卸载基础设施。一旦 TCP 连接处于 ESTABLISHED 状态,用户空间可以启用 TLS 上层协议(ULP)并安装加密连接状态。有关面向用户的接口的详细信息,请参阅 Documentation/networking/tls.rst 中的 TLS 文档。

ktls 可以以三种模式运行

  • 软件加密模式 (TLS_SW) - CPU 处理加密。在最基本的情况下,只能使用与 CPU 同步的加密操作,但根据调用上下文,CPU 可以利用异步加密加速器。使用加速器会在套接字读取时引入额外的延迟(解密仅在发出读取系统调用时才开始),并增加系统的 I/O 负载。

  • 基于数据包的网卡卸载模式 (TLS_HW) - 网卡逐个数据包地处理加密,前提是数据包按顺序到达。此模式与内核堆栈的最佳集成,并在本文档的其余部分详细描述(ethtool 标志 tls-hw-tx-offloadtls-hw-rx-offload)。

  • 全 TCP 网卡卸载模式 (TLS_HW_RECORD) - 网卡驱动程序和固件取代内核网络堆栈的运行模式,它在生产环境中不可用,例如使用 Linux 网络堆栈的任何防火墙功能或 QoS 和数据包调度(ethtool 标志 tls-hw-record)。

操作模式是根据设备配置自动选择的,目前不支持基于每个连接的卸载选择或退出。

TX

在较高的层面上,用户写入请求被转换为分散列表,TLS ULP 拦截它们,插入记录帧,执行加密(在 TLS_SW 模式下),然后将修改后的分散列表交给 TCP 层。从这时起,TCP 堆栈像正常一样继续运行。

TLS_HW 模式下,加密不在 TLS ULP 中执行。相反,数据包到达设备驱动程序,驱动程序将根据数据包所附加的套接字标记数据包以进行加密卸载,并将它们发送到设备以进行加密和传输。

RX

在接收端,如果设备成功处理了解密和身份验证,驱动程序将在关联的 struct sk_buff 中设置解密位。数据包到达 TCP 堆栈并正常处理。ktls 在数据排队到套接字时会收到通知,并使用 strparser 机制来描绘记录。在读取请求时,记录将从套接字中检索并传递到解密例程。如果设备解密了记录的所有段,则跳过解密,否则软件路径将处理解密。

TLS offload layers

内核 TLS 堆栈层

设备配置

在驱动程序初始化期间,设备会设置 NETIF_F_HW_TLS_RXNETIF_F_HW_TLS_TX 功能,并将 struct tlsdev_ops 指针安装到 struct net_devicetlsdev_ops 成员中。

当 TLS 加密连接状态安装到 ktls 套接字上时(请注意,它会执行两次,一次用于 RX,一次用于 TX 方向,并且两者完全独立),内核会检查底层网络设备是否支持卸载并尝试卸载。如果卸载失败,则使用与从未尝试过卸载的情况相同的机制,完全在软件中处理连接。

卸载请求是通过 struct tlsdev_opstls_dev_add 回调执行的

int (*tls_dev_add)(struct net_device *netdev, struct sock *sk,
                   enum tls_offload_ctx_dir direction,
                   struct tls_crypto_info *crypto_info,
                   u32 start_offload_tcp_sn);

direction 指示加密信息是用于接收的数据包还是发送的数据包。驱动程序使用 sk 参数检索连接的 5 元组和套接字系列(IPv4 与 IPv6)。crypto_info 中的加密信息包括密钥、iv、salt 以及 TLS 记录序列号。start_offload_tcp_sn 指示哪个 TCP 序列号对应于 crypto_info 中的序列号的记录的开头。驱动程序可以在内核结构的末尾添加其状态(请参阅 include/net/tls.h 中的 driver_state 成员),以避免额外的分配和指针取消引用。

TX

安装 TX 状态后,堆栈会保证流的第一个段将恰好从 start_offload_tcp_sn 序列号开始,从而简化 TCP 序列号匹配。

TX 卸载完全初始化并不意味着通过驱动程序且属于卸载套接字的所有段都将位于预期序列号之后,并且将具有内核记录信息。特别是,在内核中安装连接状态之前,可能已经将已加密的数据排队到套接字。

RX

在 RX 方向上,本地网络堆栈对分段的控制很少,因此初始记录的 TCP 序列号可能位于段内的任何位置。

正常操作

设备至少在每个方向上为每个连接维护以下状态

  • 加密密钥(密钥、iv、salt)

  • 加密处理状态(部分块、部分身份验证标签等)

  • 记录元数据(序列号、处理偏移量和长度)

  • 预期 TCP 序列号

记录长度或记录分段没有任何保证。特别是,段可以从记录的任何点开始,并且可以包含任意数量的记录。假设段是按顺序接收的,则设备应能够执行加密操作和身份验证,而与分段无关。为了实现这一点,设备必须保持少量段到段的状态。这至少包括

  • 部分标头(如果段仅携带部分 TLS 标头)

  • 部分数据块

  • 部分身份验证标签(已看到所有数据,但必须从后续段写入或读取部分身份验证标签)

TLS 卸载不需要记录重组。如果数据包按顺序到达,则设备应能够单独处理它们并取得进展。

TX

内核堆栈执行记录帧,为身份验证标签保留空间并填充所有其他 TLS 标头和尾部字段。

由于重新传输的可能性以及数据包到达设备后缺乏软件回退,设备和驱动程序都会维护预期的 TCP 序列号。对于按顺序传递的段,驱动程序使用连接标识符标记数据包(请注意,5 元组查找不足以识别需要硬件卸载的数据包,请参阅 5 元组匹配限制 部分)并将它们交给设备。设备将数据包识别为需要 TLS 处理并确认序列号与其期望值匹配。设备执行记录数据的加密和身份验证。它将身份验证标签和 TCP 校验和替换为正确的值。

RX

在将数据包 DMA 到主机之前(但在网卡嵌入式交换和数据包转换功能之后),设备会验证第 4 层校验和并执行 5 元组查找,以查找该数据包可能属于的任何 TLS 连接(从技术上讲,4 元组查找就足够了 - IP 地址和 TCP 端口号,因为协议始终为 TCP)。如果数据包与连接匹配,则设备会确认 TCP 序列号是否是预期的序列号,然后继续进行 TLS 处理(数据包中每个记录的记录描绘、解密、身份验证)。设备保持记录帧不变,堆栈负责记录解封装。设备指示在传递给主机的每个数据包上下文(描述符)中成功处理了 TLS 卸载。

在接收到 TLS 卸载的数据包后,驱动程序会在与段对应的 struct sk_buff 中设置 decrypted 标记。网络堆栈确保解密段和未解密段不会合并(例如,通过 GRO 或套接字层),并处理部分解密。

重新同步处理

在出现数据包丢失或网络数据包重新排序的情况下,设备可能会失去与 TLS 流的同步,并且需要与内核的 TCP 堆栈重新同步。

请注意,仅当连接成功添加到设备表且处于 TLS_HW 模式时,才会尝试重新同步。例如,如果在内核中安装加密状态时表已满,则此类连接将永远不会被卸载。因此,重新同步请求不携带任何加密连接状态。

TX

从卸载的套接字传输的段可能会以与接收端重新传输类似的方式失去同步 - 可能会发生本地丢包,但不会发生网络重排序。目前有两种机制来处理乱序段。

加密状态重建

每当传输乱序段时,驱动程序都会向设备提供足够的信息来执行加密操作。这很可能意味着当前段之前的记录部分必须作为数据包上下文的一部分传递给设备,同时还要传递其 TCP 序列号和 TLS 记录号。然后,设备可以初始化其加密状态,处理并丢弃先前的数据(以便能够插入身份验证标签),并继续处理实际的数据包。

在这种模式下,根据实现方式,驱动程序可以要求使用加密状态和新的序列号继续(下一个预期段是乱序段之后的段),或者使用之前的流状态继续 - 假设乱序段只是重新传输。前者更简单,不需要重新传输检测,因此在证明其效率低下之前,它是推荐的方法。

下一个记录同步

每当检测到乱序段时,驱动程序会请求 ktls 软件回退代码对其进行加密。如果段的序列号低于预期,驱动程序会假定是重新传输,并且不会更改设备状态。如果段在未来,则可能意味着本地丢包,驱动程序会要求堆栈将设备同步到下一个记录状态并回退到软件。

重新同步请求用以下内容指示:

void tls_offload_tx_resync_request(struct sock *sk, u32 got_seq, u32 exp_seq)

在重新同步完成之前,驱动程序不应访问其预期的 TCP 序列号(因为它将从不同的上下文更新)。应使用以下辅助函数来测试重新同步是否完成

bool tls_offload_tx_resync_pending(struct sock *sk)

下次 ktls 推送记录时,它会首先将其 TCP 序列号和 TLS 记录号发送给驱动程序。堆栈还会确保新记录将在段边界上开始(就像最初添加连接时一样)。

RX

少量的 RX 重排序事件可能不需要完全重新同步。特别是,当记录边界可以恢复时,设备不应失去同步

reorder of non-header segment

非标头段的重排序

绿色段已成功解密,蓝色段按接收到的线路传递,红色条纹标记新记录的开始。

在上述情况下,段 1 被成功接收和解密。段 2 被丢弃,因此 3 乱序到达。设备根据段 1 中的记录长度知道下一个记录在 3 中开始。段 3 被原样传递,因为由于缺少段 2 的数据,无法处理段 3 内的先前记录的其余部分。但是,设备可以从段 3 中收集身份验证算法的状态和新记录的部分块,并在 4 和 5 到达时继续解密。最后,当 2 到达时,它完全超出了设备的预期窗口,因此原样传递,无需特殊处理。ktls 软件回退处理跨越段 1、2 和 3 的记录的解密。即使有两个段未被解密,设备也没有失去同步。

如果丢失的段包含记录标头并且在下一个记录标头已经传递后到达,则可能需要内核同步

reorder of header segment

带有 TLS 标头的段的重排序

在此示例中,段 2 被丢弃,并且包含记录标头。仅当设备知道段 2 中先前记录的长度时,设备才能检测到段 4 也包含 TLS 标头。在这种情况下,设备将失去与流的同步。

流扫描重新同步

当设备失去同步且流到达的 TCP 序列号超过预期 TCP 序列号的最大记录大小时,设备会开始扫描已知的标头模式。例如,对于 TLS 1.2 和 TLS 1.3,标头的 SSL/TLS 版本字段中会出现连续的值 0x03 0x03。一旦模式匹配,设备就会继续尝试在预期位置(基于猜测位置的长度字段)解析标头。每当预期位置不包含有效的标头时,扫描都会重新启动。

当标头匹配时,设备会向内核发送确认请求,询问猜测的位置是否正确(TLS 记录是否真的从那里开始),以及给定标头的记录序列号是多少。内核确认猜测的位置正确,并告诉设备记录序列号。同时,设备一直在解析和计数自刚刚确认的记录以来的所有记录,它将它看到的记录数添加到内核提供的记录号中。此时,设备处于同步状态,可以在下一个段边界恢复解密。

在病态情况下,设备可能会锁定一系列匹配的标头,并且永远不会收到内核的回复(内核没有负面确认)。实现可以选择定期重新启动扫描。但是,考虑到错误匹配的流是多么不可能,因此不认为有必要定期重新启动。

如果确认请求异步传递到数据包流,并且记录可能在确认请求之前由内核处理,则必须特别注意。

堆栈驱动的重新同步

每当设备看到记录不再被解密时,驱动程序也可以请求堆栈执行重新同步。如果连接配置为此模式,则堆栈会在收到两个完全加密的记录后自动安排重新同步。

堆栈会等待套接字排空,并告知设备下一个预期的记录号及其 TCP 序列号。如果记录继续以完全加密的方式接收,则堆栈会以指数退避的方式重试同步(首先在 2 个加密记录后,然后在 4 个记录后,然后在 8 个记录后,在 16 个记录后...最多每 128 个记录)。

错误处理

TX

数据包可能会被堆栈重定向或重新路由到与所选 TLS 卸载设备不同的设备。堆栈将使用 sk_validate_xmit_skb() 辅助函数处理这种情况(TLS 卸载代码在此挂钩处安装 tls_validate_xmit_skb())。卸载会维护有关所有记录的信息,直到数据完全确认,因此,如果 skb 到达错误的设备,则可以通过软件回退来处理它们。

传输端的任何设备 TLS 卸载处理错误都必须导致数据包被丢弃。例如,如果由于堆栈或设备中的错误导致数据包乱序,到达设备并且无法加密,则必须丢弃此类数据包。

RX

如果设备在接收端遇到任何 TLS 卸载问题,则应将数据包作为在网络上接收到的数据包传递到主机的网络堆栈。

例如,段中任何记录的身份验证失败都应导致将未修改的数据包传递给软件回退。这意味着不应“就地”修改数据包。不建议拆分段来处理部分解密。换句话说,数据包中的所有记录都已成功处理和验证,或者必须将数据包作为在网络上的数据包传递给主机的堆栈(如果设备提供精确的错误,则在驱动程序中恢复原始数据包就足够了)。

Linux 网络堆栈没有提供报告每个数据包的解密和身份验证错误的方法,带有错误的数据包不能简单地设置 decrypted 标记。

如果数据包包含不正确的校验和,则也不应由 TLS 卸载处理。

性能指标

TLS 卸载可以通过以下基本指标来表征

  • 最大连接数

  • 连接安装速率

  • 连接安装延迟

  • 总加密性能

请注意,每个 TCP 连接都需要双向的 TLS 会话,可以分别处理每个方向来报告性能。

最大连接数

设备可以支持的连接数可以通过 devlink resource API 公开。

总加密性能

卸载性能可能取决于段和记录大小。

设备的加密子系统过载不应对非卸载流产生显著的性能影响。

统计信息

驱动程序应报告以下 TLS 相关统计信息的最小集合

  • rx_tls_decrypted_packets - 成功解密的属于 TLS 流的 RX 数据包数。

  • rx_tls_decrypted_bytes - 成功解密的 RX 数据包中的 TLS 负载字节数。

  • rx_tls_ctx - 添加到设备以进行解密的 TLS RX HW 卸载上下文数。

  • rx_tls_del - 从设备中删除的 TLS RX HW 卸载上下文数(连接已结束)。

  • rx_tls_resync_req_pkt - 接收到的带有重新同步的 TLS 数据包数

    请求。

  • rx_tls_resync_req_start - TLS 异步重新同步请求的启动次数

    已启动。

  • rx_tls_resync_req_end - TLS 异步重新同步请求的结束次数

    正确结束,并提供了硬件跟踪的 tcp-seq。

  • rx_tls_resync_req_skip - TLS 异步重新同步请求

    过程启动但未正确结束的次数。

  • rx_tls_resync_res_ok - TLS 重新同步响应调用驱动程序

    成功处理的次数。

  • rx_tls_resync_res_skip - TLS 重新同步响应调用驱动程序

    未成功终止的次数。

  • rx_tls_err - 由于状态机中出现意外错误而未解密的 TLS 流的一部分的 RX 数据包数量。

  • tx_tls_encrypted_packets - 传递给设备以加密其 TLS 负载的 TX 数据包数量。

  • tx_tls_encrypted_bytes - 传递给设备进行加密的 TX 数据包中的 TLS 负载字节数。

  • tx_tls_ctx - 为加密添加到设备的 TLS TX 硬件卸载上下文的数量。

  • tx_tls_ooo - 作为 TLS 流一部分但未按预期顺序到达的 TX 数据包数量。

  • tx_tls_skip_no_sync_data - 作为 TLS 流一部分且乱序到达的 TX 数据包数量,但跳过了硬件卸载例程并进入常规传输流程,因为它们是连接握手的重传。

  • tx_tls_drop_no_sync_data - 作为 TLS 流一部分而被丢弃的 TX 数据包数量,因为它们是乱序到达并且找不到关联的记录。

  • tx_tls_drop_bypass_req - 作为 TLS 流一部分而被丢弃的 TX 数据包数量,因为它们包含由软件加密的数据和期望硬件加密卸载的数据。

值得注意的极端情况、异常和附加要求

五元组匹配限制

该设备只能根据套接字的五元组识别接收到的数据包。当前的 ktls 实现不会卸载通过软件接口路由的套接字,例如用于隧道或虚拟网络的套接字。但是,网络堆栈执行的许多数据包转换(最值得注意的是任何 BPF 逻辑)不需要任何中间软件设备,因此五元组匹配可能会在设备级别持续失败。在这种情况下,设备仍然应该能够执行 TX 卸载(加密),并且应该干净地回退到软件解密 (RX)。

乱序

在网卡中引入额外的处理不应导致数据包乱序传输或接收,例如,纯 ACK 数据包不应相对于数据段重新排序。

入口重新排序

允许设备对连续的 TCP 段执行数据包重新排序(即按正确的顺序放置数据包),但任何形式的额外缓冲都是不允许的。

与标准网络卸载功能共存

卸载的 ktls 套接字应该透明地支持标准的 TCP 堆栈功能。启用设备 TLS 卸载不应导致在线路上看到的数据包有任何差异。

传输层透明度

为了简化 TLS 卸载,设备不应修改任何数据包头。

设备不应依赖任何数据包头,超出 TLS 卸载严格要求的范围。

分段丢弃

仅在发生灾难性系统错误时才可以丢弃数据包,并且在正常操作中出现的错误处理情况下绝不应使用它作为错误处理机制。换句话说,依赖 TCP 重传来处理极端情况是不可接受的。

TLS 设备特性

驱动程序应忽略对 TLS 设备特性标志的更改。这些标志将由核心 ktls 代码相应地处理。TLS 设备特性标志仅控制添加新的 TLS 连接卸载,旧的连接将在标志清除后保持活动状态。

在没有校验和计算卸载的情况下,TLS 加密无法卸载到设备。因此,TLS TX 设备特性标志要求设置 TX 校验和卸载。禁用后者意味着清除前者。禁用 TX 校验和卸载不应影响旧的连接,驱动程序应确保校验和计算不会破坏这些连接。类似地,设备卸载的 TLS 解密意味着执行 RXCSUM。如果用户不想启用 RX 校验和卸载,则也会禁用 TLS RX 设备特性。