内核 TLS 卸载

内核 TLS 操作

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

ktls 可以以三种模式运行

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

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

  • 完整 TCP NIC 卸载模式 (TLS_HW_RECORD) - NIC 驱动程序和固件用自己的 TCP 处理取代内核网络堆栈的操作模式,它不适用于利用 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 net_devicetlsdev_ops 成员中安装其 struct tlsdev_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 参数检索连接的五元组和套接字族(IPv4 或 IPv6)。crypto_info 中的加密信息包括密钥、IV、盐以及 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 序列号可能位于分段的任何位置。

正常操作

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

  • 加密密钥 (key, iv, salt)

  • 加密处理状态(部分块,部分认证标签等)

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

  • 预期 TCP 序列号

对记录长度或记录分段没有保证。特别是,分段可能在记录的任何点开始,并包含任意数量的记录。假设分段按顺序接收,设备应该能够执行加密操作和认证,无论分段如何。为了实现这一点,设备必须保留少量分段到分段的状态。这至少包括

  • 部分报头(如果分段仅携带了 TLS 报头的一部分)

  • 部分数据块

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

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

TX

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

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

RX

在数据包 DMA 到主机之前(但在 NIC 的嵌入式交换和数据包转换功能之后),设备验证第 4 层校验和并执行五元组查找,以查找数据包可能属于的任何 TLS 连接(技术上,四元组查找就足够了——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,值 0x03 0x03 的后续字节出现在报头的 SSL/TLS 版本字段中。一旦模式匹配,设备继续尝试在预期位置(根据猜测位置的长度字段)解析报头。每当预期位置不包含有效报头时,扫描就会重新开始。

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

在病态情况下,设备可能会锁定一系列匹配的报头,并且永远不会收到内核的回复(内核没有否定确认)。实现可以选择定期重新启动扫描。然而,考虑到错误匹配流的可能性极低,定期重新启动被认为没有必要。

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

堆栈驱动的重新同步

驱动程序也可以在发现记录不再被解密时,请求堆栈执行重新同步。如果连接配置为这种模式,堆栈会在收到两个完全加密的记录后自动调度重新同步。

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

错误处理

TX

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

传输侧任何设备 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 硬件卸载上下文数量。

  • rx_tls_del - 从设备删除的 TLS RX 硬件卸载上下文数量(连接已完成)。

  • rx_tls_resync_req_pkt - 收到包含重新同步请求的 TLS 数据包数量。

    请求。

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

    已启动。

  • rx_tls_resync_req_end - TLS 异步重新同步请求正确结束并提供了硬件跟踪的 tcp-seq 的次数。

    正确结束并提供了硬件跟踪的 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)。

乱序

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

入口重排序

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

与标准网络卸载功能的共存

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

传输层透明性

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

设备不应依赖于 TLS 卸载严格必要之外的任何数据包报头。

分段丢失

仅在发生灾难性系统错误时才允许丢弃数据包,并且绝不能将其用作正常操作中出现的错误的处理机制。换句话说,依赖 TCP 重传来处理特殊情况是不可接受的。

TLS 设备特性

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

没有校验和计算卸载的设备不能卸载 TLS 加密。因此,TLS TX 设备特性标志要求设置 TX 校验和卸载。禁用后者意味着清除前者。禁用 TX 校验和卸载不应影响旧连接,驱动程序应确保其校验和计算不会中断。类似地,设备卸载的 TLS 解密意味着执行 RXCSUM。如果用户不想启用 RX 校验和卸载,TLS RX 设备特性也会被禁用。