VPP中ACL源码详解第二篇:ACL数据平面处理

本篇文章主要讲解第四部分,数据平面ACL处理,其他部分请阅读专栏其他篇幅

目录

第一部分:ACL插件的作用和意义

  • [第1章:从生活中的"门禁系统"说起------ACL 插件整体认知](#第1章:从生活中的"门禁系统"说起——ACL 插件整体认知)
    • 1.1 先不管 VPP:现代 ACL 系统应该会做哪些事?
    • 1.2 再看 VPP:ACL 插件具体提供了哪些能力?
    • 1.3 ACL 插件在 VPP 里的大致位置:插在哪些弧上?
    • 1.4 ACL 与其他模块的关系:谁先谁后、谁配合谁?
    • 1.5 常见场景:ACL 插件在工程中的几种典型用法

第二部分:ACL插件的整体架构

  • 第2章:模块架构和文件组织

    • 2.1 ACL插件的文件组织结构
    • 2.2 各文件的功能和职责
    • 2.3 模块间的依赖关系
    • 2.4 模块与外部系统的关系
    • 2.5 文件组织的设计原则
    • 2.6 总结:文件组织的"设计哲学"
  • 第3章:核心数据结构

    • 3.1 acl_rule_t:一条 IP ACL 规则长什么样?
    • 3.2 acl_list_t:一张 IP ACL 表(很多规则 + 一个标签)
    • 3.3 macip_acl_rule_t:一条 MAC+IP 绑定规则
    • 3.4 macip_acl_list_t:一张 MACIP ACL 表
    • 3.5 acl_main_t:ACL 插件的全局大脑
    • 3.6 会话相关结构体:Flow-aware ACL 的骨架
    • 3.7 Hash 查找相关结构体:高性能匹配的"索引卡片"
    • 3.8 Lookup Context 与对外导出方法:ACL 作为服务的"接口面"
    • 3.9 数据结构之间的关系小结

第三部分:ACL插件的初始化和控制平面

  • 第4章:模块初始化------ACL插件是如何"开机启动"的?

    • 4.1 插件注册:告诉VPP"我是谁"
    • 4.2 初始化函数注册:告诉VPP"什么时候叫我"
    • 4.3 初始化主函数:acl_init 的完整流程
    • 4.4 初始化流程总结:从"冷启动"到"就绪"
    • 4.5 关键概念深入理解
    • 4.6 初始化完成后的状态
    • 4.7 本章小结
  • 第5章:ACL规则管理------如何"登记员工名单"?

    • 5.1 从API消息到规则存储:完整的流程概览
    • 5.2 API消息处理:接收"入职申请"
    • 5.3 核心函数:acl_add_list 的完整实现
    • 5.4 ACL规则删除:如何"办理离职手续"
    • 5.5 规则替换机制:add_replace 的语义
    • 5.6 规则验证详解:为什么需要这么多检查?
    • 5.7 规则转换详解:API格式 vs 内部格式
    • 5.8 通知机制:为什么需要通知相关系统?
    • 5.9 完整流程总结:从API到存储
    • 5.10 本章小结
  • 第6章:接口ACL绑定------如何"给门装上锁"?

    • 6.1 接口ACL绑定的概念:什么是"绑定"?
    • 6.2 从API消息到绑定完成:完整的流程概览
    • 6.3 API消息处理:接收"安装门禁申请"
    • 6.4 核心函数:acl_interface_add_del_inout_acl
    • 6.5 核心函数:acl_interface_set_inout_acl_list(完整实现)
    • 6.6 Policy Epoch机制:什么是"策略纪元"?
    • 6.7 Feature Arc启用/禁用:如何"打开/关闭门禁"?
    • 6.8 多ACL串联处理:如何"安装多个门禁系统"?
    • 6.9 反向索引的维护:为什么需要"反向查找"?
    • 6.10 完整流程总结:从API到Feature Arc启用
    • 6.11 关键概念深入理解
    • 6.12 本章小结
  • [第7章:MACIP ACL管理------如何"绑定IP和MAC地址"?](#第7章:MACIP ACL管理——如何"绑定IP和MAC地址"?)

    • 7.1 MACIP ACL的概念:什么是"MAC+IP绑定"?
    • 7.2 MACIP ACL的用途:为什么需要"MAC+IP绑定"?
    • 7.3 从API消息到MACIP ACL创建:完整的流程概览
    • 7.4 API消息处理:接收"建立身份验证系统申请"
    • 7.5 核心函数:macip_acl_add_list 的完整实现
    • 7.6 L2 Classify表构建:macip_create_classify_tables 的核心逻辑
    • 7.7 MACIP ACL接口绑定:如何"安装身份验证系统"?
    • 7.8 MACIP ACL删除:如何"销毁身份验证系统"?
    • 7.9 API消息处理:macip_acl_interface_add_del
    • 7.10 MACIP ACL vs IP ACL:关键区别总结
    • 7.11 完整流程总结:从API到Classify表应用
    • 7.12 本章小结

第四部分:数据平面ACL处理

  • 第8章:数据平面ACL匹配流程------数据包如何"过安检"?

    • 8.1 数据平面ACL匹配的概念:什么是"数据平面"?
    • 8.2 数据平面节点:ACL插件如何"插入"到数据包处理流程?
    • 8.3 数据包处理流程:从节点调用到ACL匹配
    • 8.4 5-tuple提取:如何"读取旅客信息"?
    • 8.5 ACL匹配逻辑:如何"检查旅客是否符合规则"?
    • 8.6 多ACL匹配:如何"检查多个安检规则表"?
    • 8.7 会话管理:如何"记录常客信息"?
    • 8.8 完整流程总结:从数据包到达到ACL匹配完成
    • 8.9 性能优化技术
    • 8.10 本章小结
  • [第9章:Flow-aware ACL详细实现------如何"记录和管理常客信息"?](#第9章:Flow-aware ACL详细实现——如何"记录和管理常客信息"?)

    • 9.1 Flow-aware ACL的概念:什么是"有状态ACL"?
    • 9.2 会话结构体:如何"存储常客信息"?
    • 9.3 会话创建:如何"登记新常客"?
    • 9.4 会话查找:如何"查找常客信息"?
    • 9.5 会话跟踪:如何"更新常客访问记录"?
    • 9.6 会话超时类型:如何"判断常客类型"?
    • 9.7 会话超时计算:如何"计算常客过期时间"?
    • 9.8 会话链表管理:如何"管理待检查列表"?
    • 9.9 反向会话:如何"匹配返回流量"?
    • 9.10 会话处理:如何"处理已有会话的数据包"?
    • 9.11 会话删除:如何"注销常客信息"?
    • 9.12 会话清理:如何"清理过期常客"?
    • 9.13 会话清理进程:如何"定期清理过期常客"?
    • 9.14 Policy Epoch机制:如何"检测过期会话"?
    • 9.15 会话链表删除:如何"从待检查列表中删除常客"?
    • 9.16 会话创建条件:如何"判断是否可以创建新会话"?
    • 9.17 完整流程总结:从数据包到会话管理完成
    • 9.18 本章小结
  • 第10章:Hash匹配引擎------如何"用字典快速查找规则"?

    • 10.1 Hash匹配引擎的概念:什么是"字典查找"?
    • 10.2 数据结构:如何"组织规则信息"?
    • 10.3 Hash表构建:如何"建立字典索引"?
    • 10.4 掩码类型分配:如何"给规则分配分类"?
    • 10.5 Hash表条目激活:如何"将规则添加到字典"?
    • 10.6 TupleMerge算法:如何"合并相似分类"?
    • 10.7 Hash匹配流程:如何"使用字典查找规则"?
    • 10.8 本章小结

第五部分:ACL作为服务与内部机制扩展(ACL-as-a-service)

  • 第11章:ACL-as-a-service------如何"让其他插件使用ACL引擎"?

    • 11.1 ACL-as-a-service的概念:什么是"ACL作为服务"?
    • 11.2 数据结构:如何"组织用户和上下文信息"?
    • 11.3 用户模块注册:如何"注册为ACL服务用户"?
    • 11.4 Lookup Context创建:如何"创建安检规则集合"?
    • 11.5 ACL列表设置:如何"为规则集合添加规则"?
    • 11.6 Lookup Context释放:如何"销毁安检规则集合"?
    • 11.7 ACL锁定和解锁:如何"追踪ACL的使用情况"?
    • 11.8 ACL应用和取消应用:如何"将规则添加到Hash表"?
    • 11.9 ACL变更通知:如何"通知上下文ACL规则变化"?
    • 11.10 方法导出和初始化:如何"让外部插件调用ACL方法"?
    • 11.11 5-tuple填充和匹配:如何"使用ACL匹配引擎"?
    • 11.12 完整使用流程:从注册到匹配
    • 11.13 本章小结
  • 第12章:会话表管理------如何"建立和管理常客数据库"?

    • 12.1 会话表管理的概念:什么是"会话表管理"?
    • 12.2 会话表初始化:如何"建立常客数据库"?
    • 12.3 Per-worker数据结构:如何"为每个安检通道建立档案系统"?
    • 12.4 会话表容量管理:如何"管理数据库容量"?
    • 12.5 会话表性能优化:如何"优化数据库性能"?
    • 12.6 会话表监控和调试:如何"监控和调试数据库"?
    • 12.7 会话表清理机制:如何"定期清理过期记录"?
    • 12.8 会话表容量管理:如何"管理数据库容量"?
    • 12.9 会话表性能优化:如何"优化数据库性能"?
    • 12.10 会话表监控和调试:如何"监控和调试数据库"?
    • 12.11 会话表CLI命令:如何"使用命令行管理数据库"?
    • 12.12 会话表初始化时机:何时"建立数据库"?
    • 12.13 会话表容量限制:如何"防止数据库溢出"?
    • 12.14 会话表性能调优:如何"优化数据库性能"?
    • 12.15 会话表监控指标:如何"监控数据库健康状态"?
    • 12.16 本章小结
  • 第13章:ACL插件性能优化------如何"让安检更快更高效"?

    • 13.1 性能优化的概念:什么是"性能优化"?
    • 13.2 批量处理优化:如何"一次处理多个数据包"?
    • 13.3 预取优化:如何"提前准备好数据"?
    • 13.4 流水线处理:如何"同时处理多个数据包"?
    • 13.5 缓存优化:如何"优化内存访问"?
    • 13.6 分支预测优化:如何"帮助CPU预测分支"?
    • 13.7 性能调优建议:如何"优化ACL插件性能"?
    • 13.8 性能监控:如何"监控ACL插件性能"?
    • 13.9 本章小结
  • 第14章:ACL插件调试工具------如何"诊断和排查问题"?

    • 14.1 调试工具的概念:什么是"调试工具"?
    • 14.2 Trace功能:如何"跟踪数据包处理过程"?
    • 14.3 ELOG功能:如何"记录系统事件"?
    • 14.4 Show命令:如何"显示系统状态"?
    • 14.5 调试宏:如何"输出调试信息"?
    • 14.6 故障排查方法:如何"诊断和解决问题"?
    • 14.7 本章小结
  • 第15章:ACL插件高级功能------如何"处理非IP数据包"?

    • 15.1 高级功能的概念:什么是"非IP数据包处理"?
    • 15.2 Ethertype白名单:如何"管理特殊旅客白名单"?
    • 15.3 非IP数据包处理节点:如何"检查特殊旅客"?
    • 15.4 节点注册和Feature Arc集成:如何"注册特殊旅客检查点"?
    • 15.5 Trace格式化:如何"格式化检查记录"?
    • 15.6 本章小结
  • [第16章:MACIP ACL数据面处理------如何"检查MAC+IP绑定"?](#第16章:MACIP ACL数据面处理——如何"检查MAC+IP绑定"?)

    • 16.1 MACIP ACL数据面处理的概念:什么是"MAC+IP绑定检查"?
    • 16.2 MACIP ACL数据结构:如何"存储MAC+IP绑定规则"?
    • 16.3 匹配类型管理:如何"组织MAC+IP绑定规则"?
    • 16.4 L2 Classify表构建:如何"建立快速查找表"?
    • 16.5 接口应用:如何"将MACIP ACL应用到接口"?
    • 16.6 MACIP ACL在L2输入弧中的位置:如何"集成到数据平面"?
    • 16.7 本章小结
  • 第17章:Hash查找引擎------如何"用字典快速查找规则"?

    • 17.1 Hash查找的必要性:为什么需要"字典查找"?
    • 17.2 掩码类型(Mask Type)概念:什么是"字典分类"?
    • 17.3 掩码类型池管理:如何"管理字典分类"?
    • 17.4 ACL规则到Hash条目的转换:如何"将规则转换为字典条目"?
    • 17.5 Hash表构建:如何"建立字典"?
    • 17.6 Hash查找流程:如何"使用字典查找规则"?
    • 17.7 本章小结
  • [第19章:Lookup Context机制------ACL作为服务的核心设计](#第19章:Lookup Context机制——ACL作为服务的核心设计)

    • 19.1 ACL-as-a-Service的概念:为什么需要Lookup Context?
    • 19.2 用户模块注册:acl_lookup_context_user_t
    • 19.3 Lookup Context创建:acl_lookup_context_t
    • 19.4 Context与ACL的绑定:set_acl_vec_for_context
    • 19.5 多插件复用ACL引擎:exports.h和exported_types.h
    • 19.6 Lookup Context的释放:put_lookup_context_index
    • 19.7 ACL变更通知机制:notify_acl_change
    • 19.8 调试和展示:show_lookup_context和show_lookup_user
    • 19.9 本章小结
  • 第20章:ACL插件方法导出------让其他插件也能"用上这套门禁系统"

    • 20.1 acl_plugin_methods_t:ACL服务"菜单"的结构
    • 20.2 方法注册和导出:ACL插件如何填充方法表?
    • 20.3 外部插件如何使用这些方法?(调用流程示意)
    • 20.4 内联匹配函数:acl_plugin_fill_5tuple_inlineacl_plugin_match_5tuple_inline
    • 20.5 本章小结

第六部分:多核和性能优化

  • 第21章:多核会话管理------如何让多个"安检员"协同工作?

    • 21.1 Per-worker 数据结构:为每个"安检员"准备独立的办公桌
    • 21.2 会话表分布策略:如何决定旅客档案放在哪个安检通道?
    • 21.3 线程间同步机制:如何让多个"安检员"协同工作而不冲突?
    • 21.4 本章小结
  • 第22章:性能优化技术------如何让"安检系统"快如闪电?

    • 22.1 批量处理优化:如何"批量安检"提高效率?
    • 22.2 预取(Prefetch)优化:如何"提前准备材料"减少等待时间?
    • 22.3 缓存行对齐:如何避免"伪共享"导致的性能问题?
    • 22.4 分支预测优化:如何帮助CPU"猜对"程序执行路径?
    • 22.5 向量化处理:如何一次处理多个数据包?
    • 22.6 本章小结

第七部分:可观测性和调试

  • 第23章:错误处理、日志记录与调试追踪机制------如何"记录安检日志和排查问题"?

    • 23.1 错误处理机制:如何"标记和处理安检异常"?
    • 23.2 日志记录机制:如何"记录安检工作日志"?
    • 23.3 数据包追踪机制:如何"回放特定旅客的安检过程"?
    • 23.4 计数器统计机制:如何"统计安检工作数据"?
    • 23.5 事件日志追踪(ELOG):如何"记录详细的操作日志"?
    • 23.6 本章小结
  • [第24章:日志与 Trace 实战------如何"从外部视角"排查 ACL 问题?](#第24章:日志与 Trace 实战——如何"从外部视角"排查 ACL 问题?)

    • 24.1 总体排障思路:从"外症状"到"内原因"
    • 24.2 ACL 插件 CLI 命令总览:有什么"观察窗口"?
    • 24.3 show acl-plugin sessions:看清"常客"会话情况
    • 24.4 set acl-plugin ...:开启事件 Trace 和调试开关
    • 24.5 VPP 通用 Packet Trace:抓一条"问题包"的全链路
    • 24.6 本章小结
  • 第25章:CLI和API接口------如何"指挥"ACL插件做事?

    • 25.1 ACL相关CLI命令
    • 25.2 MACIP ACL相关CLI命令
    • 25.3 ACL API消息处理
    • 25.4 API消息格式和编码
    • 25.5 本章小结

第八部分:综合案例和最佳实践

  • 第26章:综合配置案例

    • 26.1 边界防火墙配置
    • 26.2 内部安全区隔离
    • 26.3 机房接入控制
    • 26.4 多ACL串联配置
    • 26.5 有状态ACL配置
    • 26.6 本章总结
  • 第27章:性能调优实践

    • 27.1 Hash匹配启用建议
    • 27.2 TupleMerge参数调优
    • 27.3 会话超时配置建议
    • 27.4 多核配置优化
    • 27.5 规则组织最佳实践
    • 27.6 本章总结
  • 第28章:故障排查指南

    • 28.1 常见问题诊断
    • 28.2 规则匹配问题排查
    • 28.3 会话表问题排查
    • 28.4 性能问题排查
    • 28.5 调试工具使用
    • 28.6 本章总结
  • 第29章:ACL插件总结

    • 29.1 ACL插件的关键特点
    • 29.2 在VPP数据包转发中的作用
    • 29.3 性能优化要点
    • 29.4 与其他模块的关系
    • 29.5 最佳实践和注意事项
    • 29.6 知识体系总结
    • 29.7 本章总结

第8章:数据平面ACL匹配流程------数据包如何"过安检"?

生活类比:想象一下,你是一个机场安检员,需要检查每个旅客(数据包)是否符合安全规定(ACL规则)。

  • 当旅客进入机场时,你需要检查他们的"身份证"(5-tuple:源IP、目的IP、协议、源端口、目的端口)
  • 根据"安检规则表"(ACL规则),决定是"放行"(permit)还是"拒绝"(deny)
  • 如果是"常客"(已有会话),可以快速放行(会话匹配)
  • 如果是"新旅客"(新会话),需要详细检查(ACL匹配)

这一章,我们就跟着ACL插件的代码,看看数据包是如何"过安检"的。每一步我都会详细解释,确保你完全理解。


8.1 数据平面ACL匹配的概念:什么是"数据平面"?

8.1.1 什么是数据平面?

数据平面(Data Plane):网络设备中处理实际数据包转发的部分,与**控制平面(Control Plane)**相对。

控制平面 vs 数据平面

特性 控制平面 数据平面
作用 管理网络状态、配置路由、管理ACL规则 转发数据包、执行ACL匹配
处理速度 较慢(毫秒级) 极快(纳秒级)
处理对象 配置命令、管理消息 数据包
执行位置 主线程 Worker线程(多核并行)

类比

  • 控制平面:就像"机场管理部门"(制定规则、管理设备)
  • 数据平面:就像"安检员"(执行检查、处理旅客)
8.1.2 VPP的数据平面架构

VPP(Vector Packet Processing):Cisco开发的高性能数据包处理框架。

关键特性

  • 向量化处理:一次处理多个数据包(提高缓存命中率)
  • 图节点架构:数据包处理流程由多个节点组成(类似流水线)
  • 多核并行:每个Worker线程独立处理数据包(无锁设计)

类比:就像"机场安检流水线":

  • 向量化处理:就像一次检查多个旅客(提高效率)
  • 图节点架构:就像"身份验证 → 行李检查 → 身体检查"的流水线
  • 多核并行:就像多个安检通道同时工作(互不干扰)

8.2 数据平面节点:ACL插件如何"插入"到数据包处理流程?

8.2.1 VPP节点注册

ACL插件通过注册多个节点来"插入"到数据包处理流程中。让我们看看节点是如何注册的:

c 复制代码
//794:836:src/plugins/acl/dataplane_node.c
VLIB_REGISTER_NODE (acl_in_l2_ip6_node) =  // VLIB_REGISTER_NODE:VPP提供的宏,用于注册节点
{
  .name = "acl-plugin-in-ip6-l2",          // 节点名称:ACL插件IPv6输入L2节点
                                         // 命名规则:acl-plugin-{方向}-{协议}-{路径}
                                         // 方向:in(输入)、out(输出)
                                         // 协议:ip4(IPv4)、ip6(IPv6)
                                         // 路径:l2(L2路径)、fa(Flow-aware,IP路径)
                                         // 类比:就像"IPv6进入门L2通道的安检点"
  
  .vector_size = sizeof (u32),              // 向量大小:每个数据包在向量中占用的空间(u32,4字节)
                                         // 向量:VPP中用于批量处理数据包的数据结构
                                         // 类比:就像"安检通道"的"容量"(一次可以处理多少个旅客)
  
  .format_trace = format_acl_plugin_trace,   // Trace格式化函数:用于调试和追踪
                                         // format_acl_plugin_trace:格式化ACL插件的Trace信息
                                         // 类比:就像"安检记录"的"格式化函数"(用于查看安检记录)
  
  .type = VLIB_NODE_TYPE_INTERNAL,           // 节点类型:内部节点(不直接暴露给用户)
                                         // VLIB_NODE_TYPE_INTERNAL:内部节点类型
                                         // 类比:就像"内部安检点"(不直接对旅客开放,但会处理数据包)
  
  .n_errors = ARRAY_LEN (acl_fa_error_strings),  // 错误数量:错误字符串数组的长度
                                         // acl_fa_error_strings:ACL Flow-aware错误字符串数组
                                         // 类比:就像"安检错误类型"的数量
  
  .error_strings = acl_fa_error_strings,    // 错误字符串数组:用于错误报告
                                         // 类比:就像"安检错误类型"的"说明文字"
  
  .n_next_nodes = ACL_FA_N_NEXT,            // 下一跳节点数量:ACL Flow-aware下一跳节点数量
                                         // ACL_FA_N_NEXT:ACL Flow-aware下一跳节点数量(通常是1,即error-drop)
                                         // 类比:就像"安检后的去向"数量(通常是"拒绝"或"放行")
  
  .next_nodes =                             // 下一跳节点数组:定义数据包处理后的去向
  {
    [ACL_FA_ERROR_DROP] = "error-drop",     // 如果ACL拒绝,跳转到error-drop节点
                                         // ACL_FA_ERROR_DROP:ACL拒绝错误索引
                                         // "error-drop":错误丢弃节点(丢弃数据包)
                                         // 类比:就像"如果安检不通过,送到'拒绝通道'"
  }
};

VNET_FEATURE_INIT (acl_in_l2_ip6_fa_feature, static) =  // VNET_FEATURE_INIT:VPP提供的宏,用于注册Feature
{
  .arc_name = "l2-input-ip6",               // Feature Arc名称:L2输入IPv6弧
                                         // arc_name:Feature Arc名称(数据包处理路径)
                                         // "l2-input-ip6":L2输入IPv6 Feature Arc(数据包从L2进入,是IPv6包)
                                         // 类比:就像"IPv6进入门L2通道"的"安检路径"
  
  .node_name = "acl-plugin-in-ip6-l2",      // 节点名称:ACL插件IPv6输入L2节点(与上面注册的节点名称一致)
                                         // 类比:就像"安检点名称"(与上面注册的节点名称一致)
  
  .runs_before = VNET_FEATURES ("l2-input-feat-arc-end"),  // 运行顺序:在l2-input-feat-arc-end之前运行
                                         // runs_before:定义节点在Feature Arc中的运行顺序
                                         // "l2-input-feat-arc-end":L2输入Feature Arc的结束节点
                                         // 类比:就像"安检点"在"安检路径"中的"位置"(在路径结束之前)
};

节点注册总结

  1. 节点定义 :使用 VLIB_REGISTER_NODE 宏定义节点
  2. Feature注册 :使用 VNET_FEATURE_INIT 宏将节点注册到Feature Arc
  3. 运行顺序 :通过 runs_before 定义节点在Feature Arc中的运行顺序

类比:就像在"机场安检路径"中注册"安检点":

  1. 定义安检点:确定"安检点"的名称、类型、错误处理等
  2. 注册到路径:将"安检点"注册到"安检路径"中
  3. 确定位置:确定"安检点"在"安检路径"中的"位置"(在哪个步骤之前)
8.2.2 ACL插件的节点类型

ACL插件注册了多个节点,用于不同的数据包处理路径:

L2路径节点(数据包从L2进入/离开):

  • acl-plugin-in-ip4-l2:L2输入IPv4节点
  • acl-plugin-in-ip6-l2:L2输入IPv6节点
  • acl-plugin-out-ip4-l2:L2输出IPv4节点
  • acl-plugin-out-ip6-l2:L2输出IPv6节点

IP路径节点(Flow-aware,数据包在IP层处理):

  • acl-plugin-in-ip4-fa:IP输入IPv4 Flow-aware节点
  • acl-plugin-in-ip6-fa:IP输入IPv6 Flow-aware节点
  • acl-plugin-out-ip4-fa:IP输出IPv4 Flow-aware节点
  • acl-plugin-out-ip6-fa:IP输出IPv6 Flow-aware节点

类比:就像"机场安检"的多个"安检点":

  • L2路径节点:就像"进入机场时的安检点"(在L2层检查)
  • IP路径节点:就像"登机前的安检点"(在IP层检查,支持Flow-aware)

8.3 数据包处理流程:从节点调用到ACL匹配

8.3.1 节点函数入口

当数据包经过ACL节点时,VPP会调用节点函数。让我们看看节点函数是如何工作的:

c 复制代码
//773:778:src/plugins/acl/dataplane_node.c
VLIB_NODE_FN (acl_in_fa_ip4_node) (vlib_main_t * vm,      // VLIB_NODE_FN:VPP提供的宏,定义节点函数
				   vlib_node_runtime_t * node,  // 参数:vm(VPP主结构体)、node(节点运行时结构体)
				   vlib_frame_t * frame)        // 参数:frame(数据包帧,包含多个数据包)
{
  return acl_fa_node_fn (vm, node, frame, 0, 1, 0);      // 调用核心处理函数
                                         // acl_fa_node_fn:ACL Flow-aware节点核心处理函数
                                         // 参数:vm(VPP主结构体)、node(节点运行时结构体)、frame(数据包帧)
                                         // 参数:0(is_ip6,0=IPv4,1=IPv6)、1(is_input,1=输入,0=输出)、0(is_l2_path,0=IP路径,1=L2路径)
                                         // 类比:就像"IPv4输入IP路径的安检点"调用"核心安检函数"
}

节点函数参数

  1. vlib_main_t * vm:VPP主结构体(包含全局状态)
  2. vlib_node_runtime_t * node:节点运行时结构体(包含节点状态)
  3. vlib_frame_t * frame:数据包帧(包含多个数据包的索引)

类比:就像"安检点"的"处理函数":

  • vm:就像"机场管理系统"(包含全局状态)
  • node:就像"安检点状态"(包含当前安检点的状态)
  • frame:就像"待检查的旅客列表"(包含多个旅客的索引)
8.3.2 核心处理函数:acl_fa_node_fn

acl_fa_node_fn 是ACL节点的核心处理函数,它调用内部处理函数:

c 复制代码
//720:730:src/plugins/acl/dataplane_node.c
always_inline uword                              // 返回类型:uword(处理的数据包数量)
acl_fa_node_fn (vlib_main_t * vm,                // 参数:vm(VPP主结构体)
		vlib_node_runtime_t * node,          // 参数:node(节点运行时结构体)
		vlib_frame_t * frame,                 // 参数:frame(数据包帧)
		int is_ip6,                          // 参数:is_ip6(是否IPv6,0=IPv4,1=IPv6)
		int is_input,                        // 参数:is_input(是否输入方向,1=输入,0=输出)
		int is_l2_path)                      // 参数:is_l2_path(是否L2路径,0=IP路径,1=L2路径)
{
  int node_trace_on = (node->flags & VLIB_NODE_FLAG_TRACE);  // 检查是否启用Trace
                                         // VLIB_NODE_FLAG_TRACE:节点Trace标志位
                                         // 类比:就像检查"是否启用安检记录"
  
  int reclassify_sessions = acl_main.reclassify_sessions;    // 获取会话重分类标志
                                         // reclassify_sessions:是否启用会话重分类(Policy Epoch机制)
                                         // 类比:就像检查"是否启用会话重分类"(检查旧会话是否仍然有效)
  
  int with_stateful_datapath = 1;                            // 是否启用有状态数据路径(Flow-aware)
                                         // with_stateful_datapath:是否启用Flow-aware(默认启用)
                                         // 类比:就像"是否启用有状态安检"(记录旅客信息,快速放行常客)
  
  // 调用准备函数:提取5-tuple、计算Hash等
  acl_fa_node_common_prepare_fn (vm, node, frame, is_ip6, is_input, is_l2_path, with_stateful_datapath);
                                         // acl_fa_node_common_prepare_fn:准备函数(提取5-tuple、计算Hash等)
                                         // 类比:就像"准备安检"(提取旅客信息、计算Hash等)
  
  // 调用内部处理函数:执行ACL匹配、会话管理等
  return acl_fa_inner_node_fn (vm, node, frame, is_ip6, is_input, is_l2_path, with_stateful_datapath, node_trace_on, reclassify_sessions);
                                         // acl_fa_inner_node_fn:内部处理函数(执行ACL匹配、会话管理等)
                                         // 类比:就像"执行安检"(检查旅客、管理会话等)
}

函数总结

  1. 检查Trace:是否启用Trace(用于调试)
  2. 获取配置:获取会话重分类标志
  3. 准备数据:调用准备函数(提取5-tuple、计算Hash等)
  4. 处理数据包:调用内部处理函数(执行ACL匹配、会话管理等)

类比:就像"安检流程":

  1. 检查记录:是否启用"安检记录"
  2. 获取配置:获取"会话重分类"配置
  3. 准备检查:提取"旅客信息"、计算"Hash"等
  4. 执行检查:检查"旅客"、管理"会话"等

8.4 5-tuple提取:如何"读取旅客信息"?

8.4.1 什么是5-tuple?

5-tuple(五元组):用于标识网络连接的五元组,包括:

  1. 源IP地址(Source IP Address)
  2. 目的IP地址(Destination IP Address)
  3. 协议号(Protocol Number,如TCP=6、UDP=17)
  4. 源端口(Source Port)
  5. 目的端口(Destination Port)

类比:就像"旅客信息":

  • 源IP地址:就像"出发地"
  • 目的IP地址:就像"目的地"
  • 协议号:就像"交通方式"(飞机、火车等)
  • 源端口:就像"出发地端口"
  • 目的端口:就像"目的地端口"
8.4.2 5-tuple提取函数:acl_fill_5tuple

acl_fill_5tuple 是提取5-tuple的核心函数:

c 复制代码
//206:228:src/plugins/acl/public_inlines.h
always_inline void                              // 返回类型:void(无返回值,通过指针参数返回结果)
acl_fill_5tuple (acl_main_t * am,               // 参数:am(ACL插件主结构体)
		 u32 sw_if_index0,                  // 参数:sw_if_index0(接口索引)
		 vlib_buffer_t * b0,                 // 参数:b0(数据包缓冲区指针)
		 int is_ip6,                        // 参数:is_ip6(是否IPv6,0=IPv4,1=IPv6)
		 int is_input,                      // 参数:is_input(是否输入方向,1=输入,0=输出)
		 int is_l2_path,                    // 参数:is_l2_path(是否L2路径,0=IP路径,1=L2路径)
		 fa_5tuple_t * p5tuple_pkt)         // 参数:p5tuple_pkt(5-tuple输出指针)
{
  int l3_offset;                                // L3层偏移量(临时变量)
                                         // l3_offset:L3层(IP层)在数据包中的偏移量
                                         // 类比:就像"IP头在数据包中的位置"

  // ========== 第一部分:计算L3层偏移量 ==========
  
  if (is_l2_path)                               // 如果是L2路径(数据包从L2进入/离开)
    {
      l3_offset = ethernet_buffer_header_size(b0);  // L3偏移量 = 以太网头大小
                                         // ethernet_buffer_header_size:获取以太网头大小(包括VLAN标签)
                                         // 类比:就像"IP头在以太网帧中的位置"(在以太网头之后)
    }
  else                                          // 如果是IP路径(数据包在IP层处理)
    {
      if (is_input)                             // 如果是输入方向
        l3_offset = 0;                          // L3偏移量 = 0(数据包从IP层开始)
                                         // 类比:就像"IP头在数据包的开头"(没有L2头)
      else                                      // 如果是输出方向
        l3_offset = vnet_buffer(b0)->ip.save_rewrite_length;  // L3偏移量 = IP重写长度
                                         // vnet_buffer(b0)->ip.save_rewrite_length:IP重写长度(输出方向可能有L2头)
                                         // 类比:就像"IP头在数据包中的位置"(输出方向可能有L2头)
    }

  // ========== 第二部分:提取L3层数据(IP地址) ==========
  
  /* key[0..3] contains src/dst address and is cleared/set below */
  /* Remainder of the key and per-packet non-key data */
  acl_fill_5tuple_l3_data(am, b0, is_ip6, l3_offset, p5tuple_pkt);  // 提取L3层数据(源IP、目的IP)
                                         // acl_fill_5tuple_l3_data:提取L3层数据(我们下面详细讲解)
                                         // 类比:就像"提取IP地址"(源IP、目的IP)

  // ========== 第三部分:提取L4层数据(协议、端口等) ==========
  
  acl_fill_5tuple_l4_and_pkt_data(am, sw_if_index0, b0, is_ip6, is_input, l3_offset, &p5tuple_pkt->l4, &p5tuple_pkt->pkt);  // 提取L4层数据(协议、端口等)
                                         // acl_fill_5tuple_l4_and_pkt_data:提取L4层数据(我们下面详细讲解)
                                         // 类比:就像"提取协议和端口"(协议号、源端口、目的端口)
}

函数总结

  1. 计算L3偏移量:根据路径类型(L2路径或IP路径)和方向(输入或输出)计算L3层偏移量
  2. 提取L3数据:提取源IP地址和目的IP地址
  3. 提取L4数据:提取协议号、源端口、目的端口等

类比:就像"读取旅客信息":

  1. 确定位置:确定"IP头"在"数据包"中的位置
  2. 读取IP地址:读取"源IP地址"和"目的IP地址"
  3. 读取端口和协议:读取"协议号"、"源端口"、"目的端口"
8.4.3 L3层数据提取:acl_fill_5tuple_l3_data

acl_fill_5tuple_l3_data 用于提取L3层数据(IP地址):

c 复制代码
//69:89:src/plugins/acl/public_inlines.h
always_inline void                              // 返回类型:void
acl_fill_5tuple_l3_data (acl_main_t * am,      // 参数:am(ACL插件主结构体,未使用)
		 vlib_buffer_t * b0,                 // 参数:b0(数据包缓冲区指针)
		 int is_ip6,                        // 参数:is_ip6(是否IPv6,0=IPv4,1=IPv6)
		 int l3_offset,                     // 参数:l3_offset(L3层偏移量)
		 fa_5tuple_t * p5tuple_pkt)         // 参数:p5tuple_pkt(5-tuple输出指针)
{
  if (is_ip6)                                   // 如果是IPv6
    {
      ip6_header_t *ip6 = vlib_buffer_get_current (b0) + l3_offset;  // 获取IPv6头指针
                                         // vlib_buffer_get_current:获取数据包当前指针(指向数据包开始)
                                         // l3_offset:L3层偏移量(IP头在数据包中的位置)
                                         // 类比:就像"获取IPv6头指针"(指向IP头)
      
      p5tuple_pkt->ip6_addr[0] = ip6->src_address;  // 提取源IPv6地址
      p5tuple_pkt->ip6_addr[1] = ip6->dst_address;  // 提取目的IPv6地址
                                         // ip6->src_address:IPv6源地址(128位)
                                         // ip6->dst_address:IPv6目的地址(128位)
                                         // 类比:就像"读取IPv6地址"(源地址、目的地址)
    }
  else                                          // 如果是IPv4
    {
      int ii;
      for(ii=0; ii<6; ii++) {                   // 清零IPv6地址字段(IPv4不需要)
        p5tuple_pkt->l3_zero_pad[ii] = 0;      // 清零填充字段
                                         // l3_zero_pad:IPv4地址的填充字段(用于对齐)
                                         // 类比:就像"清零不需要的字段"
      }
      
      ip4_header_t *ip4 = vlib_buffer_get_current (b0) + l3_offset;  // 获取IPv4头指针
                                         // 类比:就像"获取IPv4头指针"(指向IP头)
      
      p5tuple_pkt->ip4_addr[0] = ip4->src_address;  // 提取源IPv4地址
      p5tuple_pkt->ip4_addr[1] = ip4->dst_address;  // 提取目的IPv4地址
                                         // ip4->src_address:IPv4源地址(32位)
                                         // ip4->dst_address:IPv4目的地址(32位)
                                         // 类比:就像"读取IPv4地址"(源地址、目的地址)
    }
}

函数总结

  1. 判断地址族 :根据 is_ip6 判断是IPv4还是IPv6
  2. 提取IP地址:从IP头中提取源IP地址和目的IP地址
  3. 处理填充:IPv4需要清零填充字段(用于对齐)

类比:就像"读取旅客的出发地和目的地":

  1. 判断类型:判断是"国内航班"(IPv4)还是"国际航班"(IPv6)
  2. 读取地址:读取"出发地"和"目的地"
  3. 处理格式:根据类型处理不同的格式

8.4.4 L4层数据提取:acl_fill_5tuple_l4_and_pkt_data

acl_fill_5tuple_l4_and_pkt_data 用于提取L4层数据(协议、端口等)。这个函数比较复杂,我们重点讲解关键逻辑:

c 复制代码
//91:204:src/plugins/acl/public_inlines.h
always_inline void                              // 返回类型:void
acl_fill_5tuple_l4_and_pkt_data (acl_main_t * am, u32 sw_if_index0, vlib_buffer_t * b0, int is_ip6, int is_input,
		 int l3_offset, fa_session_l4_key_t *p5tuple_l4, fa_packet_info_t *p5tuple_pkt)  // 参数说明见函数签名
{
  /* IP4 and IP6 protocol numbers of ICMP */
  static u8 icmp_protos_v4v6[] = { IP_PROTOCOL_ICMP, IP_PROTOCOL_ICMP6 };  // ICMP协议号数组(IPv4和IPv6)
                                         // IP_PROTOCOL_ICMP:IPv4 ICMP协议号(1)
                                         // IP_PROTOCOL_ICMP6:IPv6 ICMPv6协议号(58)
                                         // 类比:就像"ICMP协议号列表"(IPv4和IPv6)

  int l4_offset;                                // L4层偏移量(临时变量)
                                         // l4_offset:L4层(传输层)在数据包中的偏移量
                                         // 类比:就像"传输层头在数据包中的位置"

  u16 ports[2] = { 0 };                         // 端口数组(源端口、目的端口)
                                         // ports[0]:源端口
                                         // ports[1]:目的端口
                                         // 类比:就像"端口数组"(源端口、目的端口)

  u8 proto;                                     // 协议号(临时变量)
                                         // 类比:就像"协议号"(TCP、UDP、ICMP等)

  u8 tmp_l4_flags = 0;                          // L4标志位(临时变量)
                                         // tmp_l4_flags:L4层标志位(用于标记L4层信息是否有效)
                                         // 类比:就像"L4层信息有效性标志"

  fa_packet_info_t tmp_pkt = { .is_ip6 = is_ip6, .mask_type_index_lsb = ~0 };  // 数据包信息(临时变量)
                                         // is_ip6:是否IPv6
                                         // mask_type_index_lsb:掩码类型索引(用于Hash匹配)
                                         // 类比:就像"数据包信息"(地址族、掩码类型等)

  // ========== 第一部分:提取协议号和计算L4偏移量 ==========
  
  if (is_ip6)                                   // 如果是IPv6
    {
      ip6_header_t *ip6 = vlib_buffer_get_current (b0) + l3_offset;  // 获取IPv6头指针
      proto = ip6->protocol;                    // 提取协议号(IPv6的next header字段)
                                         // ip6->protocol:IPv6的next header字段(协议号)
                                         // 类比:就像"读取IPv6协议号"

      l4_offset = l3_offset + sizeof (ip6_header_t);  // L4偏移量 = L3偏移量 + IPv6头大小
                                         // sizeof (ip6_header_t):IPv6头大小(40字节)
                                         // 类比:就像"计算L4层位置"(在IPv6头之后)

      /* IP6 EH handling is here, increment l4_offset if needs to, update the proto */
      int need_skip_eh = clib_bitmap_get (am->fa_ipv6_known_eh_bitmap, proto);  // 检查是否需要跳过扩展头
                                         // clib_bitmap_get:获取位图中指定位的值
                                         // fa_ipv6_known_eh_bitmap:IPv6已知扩展头位图(在初始化时设置)
                                         // 为什么需要?IPv6可能有扩展头(如Fragment、Routing等),需要跳过
                                         // 类比:就像"检查是否需要跳过扩展头"(IPv6可能有扩展头)
      
      if (PREDICT_FALSE (need_skip_eh))         // 如果需要跳过扩展头(PREDICT_FALSE表示不常见)
	{
	  while (need_skip_eh && offset_within_packet (b0, l4_offset))  // 循环跳过扩展头
	    {
	      /* Fragment header needs special handling */
	      if (PREDICT_FALSE(ACL_EH_FRAGMENT == proto))  // 如果是Fragment扩展头
	        {
	          proto = *(u8 *) get_ptr_to_offset (b0, l4_offset);  // 读取下一个协议号
		  u16 frag_offset = *(u16 *) get_ptr_to_offset (b0, 2 + l4_offset);  // 读取Fragment偏移量
		  frag_offset = clib_net_to_host_u16(frag_offset) >> 3;  // 转换字节序并右移3位(单位转换)
		  
		  if (frag_offset)                    // 如果不是首片段(frag_offset != 0)
		    {
                      tmp_pkt.is_nonfirst_fragment = 1;  // 标记为非首片段
                                         // is_nonfirst_fragment:非首片段标志(用于特殊处理)
                                         // 类比:就像"标记为非首片段"(后续片段需要特殊处理)
                      
                      /* invalidate L4 offset so we don't try to find L4 info */
                      l4_offset += b0->current_length;  // 使L4偏移量无效(超出数据包长度)
                                         // 为什么需要?非首片段没有L4层信息,不能提取端口
                                         // 类比:就像"使L4偏移量无效"(非首片段没有L4层信息)
		    }
		  else                                // 如果是首片段(frag_offset == 0)
		    {
		      /* First fragment: skip the frag header and move on. */
		      l4_offset += 8;                  // 跳过Fragment头(8字节)
                                         // 类比:就像"跳过Fragment头"(首片段有L4层信息)
		    }
		}
              else                                // 如果是其他扩展头
                {
	          u8 nwords = *(u8 *) get_ptr_to_offset (b0, 1 + l4_offset);  // 读取扩展头长度(以8字节为单位)
	          proto = *(u8 *) get_ptr_to_offset (b0, l4_offset);  // 读取下一个协议号
	          l4_offset += 8 * (1 + (u16) nwords);  // 跳过扩展头(8字节 × (1 + 长度))
                                         // 类比:就像"跳过扩展头"(其他扩展头需要跳过)
                }
	      
	      need_skip_eh = clib_bitmap_get (am->fa_ipv6_known_eh_bitmap, proto);  // 检查下一个协议号是否还是扩展头
                                         // 类比:就像"检查下一个协议号是否还是扩展头"(可能有多个扩展头)
	    }
	}
    }
  else                                          // 如果是IPv4
    {
      ip4_header_t *ip4 = vlib_buffer_get_current (b0) + l3_offset;  // 获取IPv4头指针
      proto = ip4->protocol;                    // 提取协议号
                                         // ip4->protocol:IPv4协议号字段
                                         // 类比:就像"读取IPv4协议号"

      l4_offset = l3_offset + (ip4->ip_version_and_header_length & 0x0F) * 4;  // L4偏移量 = L3偏移量 + IPv4头长度
                                         // ip4->ip_version_and_header_length:IPv4版本和头长度字段
                                         // & 0x0F:取低4位(头长度,以4字节为单位)
                                         // * 4:转换为字节数
                                         // 类比:就像"计算L4层位置"(在IPv4头之后,考虑可变长度)
    }

  // ========== 第二部分:提取端口和TCP标志 ==========
  
  // 根据协议类型提取端口和TCP标志
  // 这部分代码较长,主要逻辑是:
  // 1. 如果是TCP/UDP,提取源端口和目的端口
  // 2. 如果是ICMP/ICMPv6,提取类型和代码(作为端口使用)
  // 3. 如果是TCP,提取TCP标志位
  // 由于代码较长,我们重点讲解关键逻辑
  
  // ... (端口提取代码,根据协议类型不同而不同) ...
  
  // ========== 第三部分:填充L4层数据 ==========
  
  // 填充L4层数据到输出结构体
  // p5tuple_l4:L4层数据(协议、端口、标志等)
  // p5tuple_pkt:数据包信息(地址族、片段标志等)
}

函数总结

  1. 提取协议号:从IP头中提取协议号
  2. 处理IPv6扩展头:如果是IPv6,可能需要跳过扩展头
  3. 处理Fragment:如果是Fragment,需要特殊处理(非首片段没有L4信息)
  4. 提取端口:根据协议类型提取端口(TCP/UDP)或类型/代码(ICMP)
  5. 提取TCP标志:如果是TCP,提取TCP标志位

类比:就像"读取旅客的交通方式和端口信息":

  1. 读取协议:读取"交通方式"(TCP、UDP、ICMP等)
  2. 处理扩展:如果是"国际航班"(IPv6),可能需要处理"中转信息"(扩展头)
  3. 处理片段:如果是"分片运输"(Fragment),需要特殊处理
  4. 读取端口:读取"出发地端口"和"目的地端口"
  5. 读取标志:如果是"TCP航班",读取"TCP标志"(SYN、ACK等)

8.5 ACL匹配逻辑:如何"检查旅客是否符合规则"?

8.5.1 单个ACL匹配:single_acl_match_5tuple

single_acl_match_5tuple 是匹配单个ACL的核心函数。它按顺序检查ACL中的每个规则,找到第一个匹配的规则就返回:

c 复制代码
//289:399:src/plugins/acl/public_inlines.h
always_inline int                              // 返回类型:int(1=匹配,0=不匹配)
single_acl_match_5tuple (acl_main_t * am,      // 参数:am(ACL插件主结构体)
		  u32 acl_index,                  // 参数:acl_index(ACL索引)
		  fa_5tuple_t * pkt_5tuple,      // 参数:pkt_5tuple(数据包的5-tuple)
		  int is_ip6,                    // 参数:is_ip6(是否IPv6,0=IPv4,1=IPv6)
		  u8 * r_action,                  // 参数:r_action(输出:动作,0=deny,1=permit)
		  u32 * r_acl_match_p,            // 参数:r_acl_match_p(输出:匹配的ACL索引)
		  u32 * r_rule_match_p,           // 参数:r_rule_match_p(输出:匹配的规则索引)
		  u32 * trace_bitmap)             // 参数:trace_bitmap(输出:Trace位图)
{
  int i;                                       // 循环计数器
  acl_rule_t *r;                               // ACL规则指针(临时变量)
  acl_rule_t *acl_rules;                       // ACL规则数组指针(临时变量)

  // ========== 第一部分:检查ACL是否存在 ==========
  
  if (pool_is_free_index (am->acls, acl_index))  // 检查ACL是否存在
    {
      if (r_acl_match_p)                        // 如果输出参数不为空
	*r_acl_match_p = acl_index;            // 设置匹配的ACL索引
      if (r_rule_match_p)                       // 如果输出参数不为空
	*r_rule_match_p = -1;                  // 设置匹配的规则索引为-1(无效)
      /* the ACL does not exist but is used for policy. Block traffic. */
      return 0;                                 // 返回0(不匹配,拒绝)
                                         // 为什么需要?如果ACL不存在,应该拒绝流量(安全策略)
                                         // 类比:就像"如果安检规则不存在,拒绝旅客"(安全策略)
    }
  
  acl_rules = am->acls[acl_index].rules;       // 获取ACL规则数组指针
                                         // am->acls[acl_index].rules:ACL的规则向量
                                         // 类比:就像"获取安检规则列表"

  // ========== 第二部分:遍历ACL规则,查找匹配 ==========
  
  for (i = 0; i < vec_len(acl_rules); i++)     // 遍历ACL的所有规则
    {
      r = &acl_rules[i];                       // 获取当前规则的指针
                                         // 类比:就像"获取当前安检规则"

      // 检查1:地址族是否匹配
      if (is_ip6 != r->is_ipv6)                // 如果数据包的地址族与规则的地址族不匹配
	{
	  continue;                            // 跳过当前规则,检查下一个规则
                                         // 为什么需要?IPv4规则不能匹配IPv6数据包,反之亦然
                                         // 类比:就像"如果规则是'国内航班',不能匹配'国际航班'"
	}
      
      // 检查2:目的IP地址是否匹配
      if (is_ip6)                              // 如果是IPv6
        {
          if (!fa_acl_match_ip6_addr            // 检查目的IPv6地址是否匹配
	      (&pkt_5tuple->ip6_addr[1], &r->dst.ip6, r->dst_prefixlen))  // 参数:数据包目的IP、规则目的IP、规则前缀长度
	    continue;                          // 如果不匹配,跳过当前规则
                                         // fa_acl_match_ip6_addr:IPv6地址匹配函数(我们下面详细讲解)
                                         // 类比:就像"检查'目的地'是否匹配"
        }
      else                                      // 如果是IPv4
        {
          if (!fa_acl_match_ip4_addr            // 检查目的IPv4地址是否匹配
	      (&pkt_5tuple->ip4_addr[1], &r->dst.ip4, r->dst_prefixlen))  // 参数:数据包目的IP、规则目的IP、规则前缀长度
	    continue;                          // 如果不匹配,跳过当前规则
                                         // fa_acl_match_ip4_addr:IPv4地址匹配函数(我们下面详细讲解)
                                         // 类比:就像"检查'目的地'是否匹配"
        }
      
      // 检查3:源IP地址是否匹配
      if (is_ip6)                              // 如果是IPv6
        {
          if (!fa_acl_match_ip6_addr            // 检查源IPv6地址是否匹配
	      (&pkt_5tuple->ip6_addr[0], &r->src.ip6, r->src_prefixlen))  // 参数:数据包源IP、规则源IP、规则前缀长度
	    continue;                          // 如果不匹配,跳过当前规则
                                         // 类比:就像"检查'出发地'是否匹配"
        }
      else                                      // 如果是IPv4
        {
          if (!fa_acl_match_ip4_addr            // 检查源IPv4地址是否匹配
	      (&pkt_5tuple->ip4_addr[0], &r->src.ip4, r->src_prefixlen))  // 参数:数据包源IP、规则源IP、规则前缀长度
	    continue;                          // 如果不匹配,跳过当前规则
                                         // 类比:就像"检查'出发地'是否匹配"
        }

      // 检查4:协议是否匹配
      if (r->proto)                             // 如果规则指定了协议(r->proto != 0)
	{
	  if (pkt_5tuple->l4.proto != r->proto)  // 如果数据包的协议与规则的协议不匹配
	    continue;                            // 跳过当前规则
                                         // 类比:就像"如果规则是'飞机',不能匹配'火车'"

          // 检查4.1:非首片段处理
          if (PREDICT_FALSE (pkt_5tuple->pkt.is_nonfirst_fragment &&  // 如果是非首片段
                     am->l4_match_nonfirst_fragment))                 // 且启用了非首片段L4匹配
          {
            /* non-initial fragment with frag match configured - match this rule */
            *trace_bitmap |= 0x80000000;        // 设置Trace位图标志
            *r_action = r->is_permit;           // 设置动作(允许或拒绝)
            if (r_acl_match_p)                  // 如果输出参数不为空
	      *r_acl_match_p = acl_index;        // 设置匹配的ACL索引
            if (r_rule_match_p)                 // 如果输出参数不为空
	      *r_rule_match_p = i;                // 设置匹配的规则索引
            return 1;                           // 返回1(匹配)
                                         // 为什么需要?非首片段没有L4层信息,但可以匹配L3规则
                                         // 类比:就像"如果规则只检查'出发地'和'目的地',非首片段也可以匹配"
          }

	  /* A sanity check just to ensure we are about to match the ports extracted from the packet */
	  if (PREDICT_FALSE (!pkt_5tuple->pkt.l4_valid))  // 如果L4层信息无效(非首片段等)
	    continue;                            // 跳过当前规则(不能匹配端口)
                                         // l4_valid:L4层信息有效性标志
                                         // 类比:就像"如果L4层信息无效,不能匹配端口"

	  // 检查4.2:源端口是否匹配
	  if (!fa_acl_match_port                   // 检查源端口是否匹配
	      (pkt_5tuple->l4.port[0], r->src_port_or_type_first,  // 参数:数据包源端口、规则源端口起始值
	       r->src_port_or_type_last, is_ip6))   // 参数:规则源端口结束值、是否IPv6
	    continue;                            // 如果不匹配,跳过当前规则
                                         // fa_acl_match_port:端口匹配函数(我们下面详细讲解)
                                         // 类比:就像"检查'出发地端口'是否匹配"

	  // 检查4.3:目的端口是否匹配
	  if (!fa_acl_match_port                   // 检查目的端口是否匹配
	      (pkt_5tuple->l4.port[1], r->dst_port_or_code_first,  // 参数:数据包目的端口、规则目的端口起始值
	       r->dst_port_or_code_last, is_ip6))   // 参数:规则目的端口结束值、是否IPv6
	    continue;                            // 如果不匹配,跳过当前规则
                                         // 类比:就像"检查'目的地端口'是否匹配"
	  
	  // 检查4.4:TCP标志是否匹配(仅TCP协议)
	  if (pkt_5tuple->pkt.tcp_flags_valid      // 如果TCP标志有效
	      && ((pkt_5tuple->pkt.tcp_flags & r->tcp_flags_mask) !=  // 如果数据包的TCP标志与规则的TCP标志不匹配
		  r->tcp_flags_value))              // (使用掩码进行匹配)
	    continue;                            // 跳过当前规则
                                         // tcp_flags_valid:TCP标志有效性标志
                                         // tcp_flags_mask:TCP标志掩码(指定要检查的标志位)
                                         // tcp_flags_value:TCP标志值(指定标志位的期望值)
                                         // 类比:就像"检查'TCP标志'是否匹配"(如SYN、ACK等)
	}
      
      // ========== 第三部分:所有检查都通过,规则匹配 ==========
      
      /* everything matches! */
      *r_action = r->is_permit;                 // 设置动作(允许或拒绝)
                                         // r->is_permit:规则动作(1=permit,0=deny)
                                         // 类比:就像"设置动作"(放行或拒绝)
      
      if (r_acl_match_p)                        // 如果输出参数不为空
	*r_acl_match_p = acl_index;            // 设置匹配的ACL索引
      if (r_rule_match_p)                       // 如果输出参数不为空
	*r_rule_match_p = i;                   // 设置匹配的规则索引
      
      return 1;                                 // 返回1(匹配)
                                         // 类比:就像"所有检查都通过,规则匹配"
    }
  
  return 0;                                     // 如果没有规则匹配,返回0(不匹配)
                                         // 类比:就像"没有规则匹配,拒绝"
}

函数总结

这个函数实现了First-match语义(找到第一个匹配的规则就返回):

  1. 检查ACL存在性:如果ACL不存在,拒绝流量
  2. 遍历规则:按顺序检查ACL中的每个规则
  3. 地址族匹配:检查数据包的地址族是否与规则一致
  4. IP地址匹配:检查源IP和目的IP是否匹配(支持前缀匹配)
  5. 协议匹配:如果规则指定了协议,检查协议是否匹配
  6. 端口匹配:如果规则指定了端口,检查源端口和目的端口是否匹配
  7. TCP标志匹配:如果是TCP协议,检查TCP标志是否匹配
  8. 返回结果:如果所有检查都通过,返回匹配结果(动作、ACL索引、规则索引)

类比:就像"安检员检查旅客":

  1. 检查规则表:确保"安检规则表"存在
  2. 遍历规则:按顺序检查"安检规则表"中的每个规则
  3. 检查类型:检查"旅客类型"是否匹配(国内/国际)
  4. 检查地址:检查"出发地"和"目的地"是否匹配
  5. 检查交通方式:如果规则指定了"交通方式",检查是否匹配
  6. 检查端口:如果规则指定了"端口",检查是否匹配
  7. 检查标志:如果是"TCP航班",检查"TCP标志"是否匹配
  8. 返回结果:如果所有检查都通过,返回"放行"或"拒绝"

8.5.2 IP地址匹配函数:fa_acl_match_ip4_addr 和 fa_acl_match_ip6_addr

IP地址匹配函数用于检查数据包的IP地址是否匹配规则的IP地址(支持前缀匹配):

c 复制代码
//240:253:src/plugins/acl/public_inlines.h
always_inline int                              // 返回类型:int(1=匹配,0=不匹配)
fa_acl_match_ip4_addr (ip4_address_t * addr1, // 参数:addr1(数据包的IP地址)
		   ip4_address_t * addr2,        // 参数:addr2(规则的IP地址)
		   int prefixlen)                 // 参数:prefixlen(规则的前缀长度,0表示匹配任意)
{
  if (prefixlen == 0)                          // 如果前缀长度为0(匹配任意)
    {
      /* match any always succeeds */
      return 1;                                // 返回1(匹配)
                                         // 类比:就像"如果规则是'任意地址',总是匹配"
    }
  
  // 前缀匹配:只比较前缀部分
  uint32_t a1 = clib_net_to_host_u32 (addr1->as_u32);  // 将数据包IP地址转换为主机字节序
  uint32_t a2 = clib_net_to_host_u32 (addr2->as_u32);  // 将规则IP地址转换为主机字节序
                                         // clib_net_to_host_u32:网络字节序转主机字节序(32位整数)
                                         // 类比:就像"将IP地址转换为'可比较格式'"
  
  uint32_t mask0 = 0xffffffff - ((1 << (32 - prefixlen)) - 1);  // 计算前缀掩码
                                         // 0xffffffff:全1掩码(32位)
                                         // (1 << (32 - prefixlen)) - 1:后缀掩码(低32-prefixlen位为1)
                                         // 0xffffffff - ...:前缀掩码(高prefixlen位为1)
                                         // 例如:prefixlen=24,mask0=0xffffff00(高24位为1,低8位为0)
                                         // 类比:就像"计算'前缀掩码'"(用于只比较前缀部分)
  
  return (a1 & mask0) == a2;                    // 比较前缀部分是否相等
                                         // a1 & mask0:数据包IP地址的前缀部分
                                         // == a2:与规则IP地址比较(规则IP地址应该已经是前缀部分)
                                         // 类比:就像"比较'前缀部分'是否相等"(如192.168.1.0/24匹配192.168.1.100)
}

IPv6地址匹配函数

c 复制代码
//255:281:src/plugins/acl/public_inlines.h
always_inline int                              // 返回类型:int(1=匹配,0=不匹配)
fa_acl_match_ip6_addr (ip6_address_t * addr1, // 参数:addr1(数据包的IPv6地址)
		   ip6_address_t * addr2,        // 参数:addr2(规则的IPv6地址)
		   int prefixlen)                 // 参数:prefixlen(规则的前缀长度,0表示匹配任意)
{
  if (prefixlen == 0)                          // 如果前缀长度为0(匹配任意)
    {
      /* match any always succeeds */
      return 1;                                // 返回1(匹配)
                                         // 类比:就像"如果规则是'任意地址',总是匹配"
    }
  
  // 前缀匹配:先比较完整字节,再比较部分字节
  if (memcmp (addr1, addr2, prefixlen / 8))    // 比较前缀的完整字节部分
	{
	  /* If the starting full bytes do not match, no point in bittwidling the thumbs further */
	  return 0;                            // 如果不匹配,返回0(不匹配)
                                         // memcmp:内存比较函数
                                         // prefixlen / 8:前缀的完整字节数
                                         // 类比:就像"比较'前缀的完整字节部分'"(如果这部分不匹配,直接返回不匹配)
	}
  
  if (prefixlen % 8)                            // 如果前缀长度不是8的倍数(有部分字节需要比较)
	{
	  u8 b1 = *((u8 *) addr1 + prefixlen / 8);  // 获取数据包IP地址的部分字节
	  u8 b2 = *((u8 *) addr2 + prefixlen / 8);  // 获取规则IP地址的部分字节
	  u8 mask0 = (0xff - ((1 << (8 - prefixlen % 8)) - 1));  // 计算部分字节掩码
                                         // 0xff:全1掩码(8位)
                                         // (1 << (8 - prefixlen % 8)) - 1:后缀掩码(低8-prefixlen%8位为1)
                                         // 0xff - ...:前缀掩码(高prefixlen%8位为1)
                                         // 例如:prefixlen=20,prefixlen%8=4,mask0=0xf0(高4位为1,低4位为0)
                                         // 类比:就像"计算'部分字节掩码'"(用于只比较部分字节的前缀部分)
	  
	  return (b1 & mask0) == b2;            // 比较部分字节的前缀部分是否相等
                                         // 类比:就像"比较'部分字节的前缀部分'是否相等"
	}
  else                                          // 如果前缀长度是8的倍数(没有部分字节需要比较)
	{
	  /* The prefix fits into integer number of bytes, so nothing left to do */
	  return 1;                            // 返回1(匹配)
                                         // 类比:就像"前缀完全匹配,返回匹配"
	}
}

函数总结

  1. IPv4地址匹配

    • 如果前缀长度为0,总是匹配
    • 否则,计算前缀掩码,只比较前缀部分
  2. IPv6地址匹配

    • 如果前缀长度为0,总是匹配
    • 否则,先比较完整字节部分,再比较部分字节(如果有)

类比:就像"检查地址是否匹配":

  • IPv4:就像"检查'国内地址'是否匹配"(如192.168.1.0/24匹配192.168.1.100)
  • IPv6:就像"检查'国际地址'是否匹配"(如2001:db8::/32匹配2001:db8::1)

8.5.3 端口匹配函数:fa_acl_match_port

端口匹配函数用于检查数据包的端口是否在规则的端口范围内:

c 复制代码
//283:287:src/plugins/acl/public_inlines.h
always_inline int                              // 返回类型:int(1=匹配,0=不匹配)
fa_acl_match_port (u16 port,                  // 参数:port(数据包的端口)
		   u16 port_first,              // 参数:port_first(规则端口范围的起始值)
		   u16 port_last,               // 参数:port_last(规则端口范围的结束值)
		   int is_ip6)                  // 参数:is_ip6(是否IPv6,未使用)
{
  return ((port >= port_first) && (port <= port_last));  // 检查端口是否在范围内
                                         // port >= port_first:端口大于等于起始值
                                         // port <= port_last:端口小于等于结束值
                                         // 类比:就像"检查'端口'是否在'端口范围'内"(如80-100匹配90)
}

函数总结

简单的范围检查:端口是否在 [port_first, port_last] 范围内。

类比:就像"检查'端口'是否在'端口范围'内"(如80-100匹配90)。


8.6 多ACL匹配:如何"检查多个安检规则表"?

8.6.1 线性多ACL匹配:linear_multi_acl_match_5tuple

当一个接口绑定了多个ACL时,需要按顺序检查每个ACL。linear_multi_acl_match_5tuple 实现了线性匹配:

c 复制代码
//412:449:src/plugins/acl/public_inlines.h
always_inline int                              // 返回类型:int(1=匹配,0=不匹配)
linear_multi_acl_match_5tuple (void *p_acl_main, u32 lc_index, fa_5tuple_t * pkt_5tuple,  // 参数说明见函数签名
		       int is_ip6, u8 *r_action, u32 *acl_pos_p, u32 * acl_match_p,
		       u32 * rule_match_p, u32 * trace_bitmap)
{
  acl_main_t *am = p_acl_main;                 // 获取ACL插件主结构体
  int i;                                       // 循环计数器
  u32 *acl_vector;                             // ACL索引向量指针(临时变量)
  u8 action = 0;                               // 动作(临时变量)
  acl_lookup_context_t *acontext = pool_elt_at_index(am->acl_lookup_contexts, lc_index);  // 获取Lookup Context结构体指针
                                         // pool_elt_at_index:从pool中获取指定索引的元素指针
                                         // acl_lookup_contexts:Lookup Context pool
                                         // 类比:就像"获取'安检规则表集合'结构体"

  acl_vector = acontext->acl_indices;           // 获取ACL索引向量
                                         // acontext->acl_indices:Lookup Context的ACL索引向量
                                         // 类比:就像"获取'安检规则表列表'"

  // ========== 遍历所有ACL,查找匹配 ==========
  
  for (i = 0; i < vec_len (acl_vector); i++)  // 遍历Lookup Context中的所有ACL
    {
      // 尝试匹配当前ACL
      if (single_acl_match_5tuple              // 调用单个ACL匹配函数
	  (am, acl_vector[i], pkt_5tuple, is_ip6, &action,  // 参数:am、ACL索引、5-tuple、地址族、动作输出
	   acl_match_p, rule_match_p, trace_bitmap))  // 参数:ACL匹配输出、规则匹配输出、Trace位图输出
	{
	  *r_action = action;                  // 设置动作(允许或拒绝)
          *acl_pos_p = i;                        // 设置ACL在列表中的位置
	  return 1;                            // 返回1(匹配)
                                         // 类比:就像"如果'安检规则表'匹配,返回匹配结果"
	}
    }
  
  // ========== 如果没有ACL匹配 ==========
  
  if (vec_len (acl_vector) > 0)                 // 如果Lookup Context中有ACL(但都不匹配)
    {
      return 0;                                 // 返回0(不匹配,拒绝)
                                         // 类比:就像"如果'安检规则表列表'不为空但都不匹配,拒绝"
    }
  
  /* If there are no ACLs defined we should not be here. */
  return 0;                                     // 如果没有ACL定义,返回0(不匹配)
                                         // 类比:就像"如果没有'安检规则表'定义,拒绝"
}

函数总结

  1. 获取Lookup Context:从Lookup Context中获取ACL索引向量
  2. 遍历ACL:按顺序检查每个ACL
  3. 匹配ACL :调用 single_acl_match_5tuple 匹配当前ACL
  4. 返回结果:如果找到匹配,返回匹配结果(动作、ACL位置等)

类比:就像"检查多个安检规则表":

  1. 获取规则表列表:从"安检规则表集合"中获取"安检规则表列表"
  2. 遍历规则表:按顺序检查每个"安检规则表"
  3. 匹配规则表:调用"单个规则表匹配函数"匹配当前"安检规则表"
  4. 返回结果:如果找到匹配,返回匹配结果(放行/拒绝、规则表位置等)

8.6.2 Hash多ACL匹配:hash_multi_acl_match_5tuple

如果启用了Hash匹配,可以使用Hash表进行快速匹配。hash_multi_acl_match_5tuple 实现了Hash匹配:

c 复制代码
//631:642:src/plugins/acl/public_inlines.h
always_inline int                              // 返回类型:int(1=匹配,0=不匹配)
hash_multi_acl_match_5tuple (void *p_acl_main, u32 lc_index, fa_5tuple_t * pkt_5tuple,  // 参数说明见函数签名
                       int is_ip6, u8 *action, u32 *acl_pos_p, u32 * acl_match_p,
                       u32 * rule_match_p, u32 * trace_bitmap)
{
  acl_main_t *am = p_acl_main;                 // 获取ACL插件主结构体
  u32 match_index = multi_acl_match_get_applied_ace_index(am, is_ip6, pkt_5tuple);  // 从Hash表中查找匹配的ACE索引
                                         // multi_acl_match_get_applied_ace_index:从Hash表中查找匹配的ACE索引
                                         // 作用:使用Hash表快速查找匹配的ACE(Access Control Entry,访问控制条目)
                                         // 类比:就像"从'索引卡'中快速查找匹配的'规则'"

  if (match_index != ~0)                       // 如果找到了匹配(match_index != ~0)
    {
      applied_hash_ace_entry_t *pae = pool_elt_at_index(am->applied_hash_ace_entries, match_index);  // 获取匹配的ACE条目
                                         // applied_hash_ace_entries:应用的Hash ACE条目pool
                                         // 类比:就像"获取匹配的'规则条目'"
      
      *action = pae->is_permit;                 // 设置动作(允许或拒绝)
      *acl_match_p = pae->acl_index;            // 设置匹配的ACL索引
      *rule_match_p = pae->rule_index;          // 设置匹配的规则索引
      *acl_pos_p = pae->acl_pos;                // 设置ACL在列表中的位置
      return 1;                                 // 返回1(匹配)
                                         // 类比:就像"如果找到匹配,返回匹配结果"
    }
  
  return 0;                                     // 如果没有找到匹配,返回0(不匹配)
                                         // 类比:就像"如果没有找到匹配,拒绝"
}

函数总结

  1. Hash查找:从Hash表中查找匹配的ACE索引
  2. 获取ACE条目:如果找到匹配,获取ACE条目
  3. 返回结果:返回匹配结果(动作、ACL索引、规则索引等)

类比:就像"使用索引卡快速查找":

  1. 查找索引卡:从"索引卡"中快速查找匹配的"规则"
  2. 获取规则条目:如果找到匹配,获取"规则条目"
  3. 返回结果:返回匹配结果(放行/拒绝、规则表索引、规则索引等)

Hash匹配 vs 线性匹配

特性 线性匹配 Hash匹配
时间复杂度 O(n×m)(n=ACL数量,m=规则数量) O(1)(平均情况)
内存占用 高(需要构建Hash表)
适用场景 ACL数量少、规则数量少 ACL数量多、规则数量多
性能 较慢 较快

类比

  • 线性匹配:就像"人工查找"(慢,但不需要额外资源)
  • Hash匹配:就像"索引卡查找"(快,但需要建立索引卡)

8.7 会话管理:如何"记录常客信息"?

8.7.1 Flow-aware ACL的概念

Flow-aware ACL(有状态ACL):记录网络连接的状态,对于已建立的连接,可以快速放行,无需重复检查ACL规则。

关键概念

  1. 会话(Session):一个网络连接的状态信息(5-tuple、超时时间等)
  2. 会话表(Session Table):存储所有会话的Hash表
  3. 会话超时:会话在空闲一段时间后自动删除

类比:就像"常客系统":

  • 会话:就像"常客信息"(旅客信息、访问记录等)
  • 会话表:就像"常客数据库"(存储所有常客信息)
  • 会话超时:就像"常客过期"(长时间不访问,从数据库中删除)
8.7.2 会话查找:acl_fa_find_session_with_hash

当数据包到达时,首先尝试查找现有会话:

c 复制代码
//372:374:src/plugins/acl/dataplane_node.c
  /* find the "next" session so we can kickstart the pipeline */
  if (with_stateful_datapath)                 // 如果启用了有状态数据路径
    acl_fa_find_session_with_hash (am, is_ip6, sw_if_index[0], hash[0],  // 查找会话
				   &fa_5tuple[0], &f_sess_id_next.as_u64);  // 参数:am、地址族、接口索引、Hash值、5-tuple、会话ID输出
                                         // acl_fa_find_session_with_hash:使用Hash值查找会话
                                         // 作用:从会话Hash表中查找匹配的会话
                                         // 类比:就像"从'常客数据库'中查找匹配的'常客信息'"

会话查找逻辑

  1. 计算Hash值:根据5-tuple和接口索引计算Hash值
  2. Hash表查找:使用Hash值在会话Hash表中查找
  3. 返回会话ID:如果找到匹配,返回会话ID;否则返回无效ID

类比:就像"查找常客信息":

  1. 计算Hash值:根据"旅客信息"计算"Hash值"
  2. 数据库查找:使用"Hash值"在"常客数据库"中查找
  3. 返回结果:如果找到匹配,返回"常客ID";否则返回"无效ID"

8.8 完整流程总结:从数据包到达到ACL匹配完成

让我们用一个完整的例子来总结整个流程:

场景:一个IPv4 TCP数据包从接口0进入,需要经过ACL检查

步骤1:数据包到达节点

复制代码
数据包 → acl-plugin-in-ip4-l2 节点
  - 触发 acl_in_l2_ip4_node 函数
  - 调用 acl_fa_node_fn

步骤2:准备数据

复制代码
acl_fa_node_common_prepare_fn:
  - 提取接口索引(sw_if_index)
  - 提取5-tuple(源IP、目的IP、协议、源端口、目的端口)
  - 计算会话Hash值(如果启用Flow-aware)

步骤3:查找会话(如果启用Flow-aware)

复制代码
acl_fa_find_session_with_hash:
  - 使用Hash值在会话Hash表中查找
  - 如果找到匹配,使用会话动作(快速路径)
  - 如果未找到,继续ACL匹配(慢速路径)

步骤4:ACL匹配(如果未找到会话)

复制代码
acl_plugin_match_5tuple_inline:
  - 获取Lookup Context索引
  - 根据配置选择线性匹配或Hash匹配
  - 调用 linear_multi_acl_match_5tuple 或 hash_multi_acl_match_5tuple

步骤5:单个ACL匹配

复制代码
single_acl_match_5tuple:
  - 遍历ACL的所有规则
  - 检查地址族、IP地址、协议、端口、TCP标志
  - 找到第一个匹配的规则,返回动作

步骤6:创建会话(如果匹配且启用Flow-aware)

复制代码
acl_fa_can_add_session:
  - 检查是否可以创建新会话
  - 如果可以,创建新会话并添加到会话表

步骤7:设置数据包动作

复制代码
b[0]->error = error_node->errors[action];
  - 如果action=1(permit),设置错误码为ACL_PERMIT
  - 如果action=0(deny),设置错误码为ACL_DROP

步骤8:返回处理结果

复制代码
返回处理的数据包数量
  - VPP根据错误码决定数据包的下一步处理
  - permit:继续转发
  - deny:丢弃

类比:就像完整的"安检流程":

  1. 旅客到达(数据包到达节点)
  2. 准备检查(提取5-tuple、计算Hash)
  3. 查找常客(查找会话)
  4. 执行安检(ACL匹配)
  5. 记录信息(创建会话)
  6. 决定去向(设置动作)
  7. 返回结果(继续转发或丢弃)

8.9 性能优化技术

8.9.1 向量化处理

向量化处理:一次处理多个数据包,提高缓存命中率。

c 复制代码
//269:297:src/plugins/acl/dataplane_node.c
  n_left = frame->n_vectors;                    // 剩余数据包数量
  while (n_left >= (ACL_PLUGIN_PREFETCH_GAP + 1) * ACL_PLUGIN_VECTOR_SIZE)  // 如果剩余数据包数量足够(用于预取)
    {
      const int vec_sz = ACL_PLUGIN_VECTOR_SIZE;  // 向量大小(4)
      {
	int ii;
	for (ii = ACL_PLUGIN_PREFETCH_GAP * vec_sz;  // 预取循环(预取未来的数据包)
	     ii < (ACL_PLUGIN_PREFETCH_GAP + 1) * vec_sz; ii++)
	  {
	    clib_prefetch_load (b[ii]);            // 预取数据包缓冲区
	    CLIB_PREFETCH (b[ii]->data, 2 * CLIB_CACHE_LINE_BYTES, LOAD);  // 预取数据包数据
                                         // clib_prefetch_load:预取数据包缓冲区(提高缓存命中率)
                                         // CLIB_PREFETCH:预取数据包数据(2个缓存行)
                                         // 类比:就像"预取未来的旅客信息"(提前准备,提高效率)
	  }
      }

      get_sw_if_index_xN (vec_sz, is_input, b, sw_if_index);  // 批量提取接口索引
      fill_5tuple_xN (vec_sz, am, is_ip6, is_input, is_l2_path, &b[0],  // 批量提取5-tuple
		      &sw_if_index[0], &fa_5tuple[0]);
      if (with_stateful_datapath)
	make_session_hash_xN (vec_sz, am, is_ip6, &sw_if_index[0],  // 批量计算会话Hash值
			      &fa_5tuple[0], &hash[0]);

      n_left -= vec_sz;                         // 减少剩余数据包数量
      fa_5tuple += vec_sz;                     // 移动指针
      b += vec_sz;
      sw_if_index += vec_sz;
      hash += vec_sz;
    }

优化效果

  • 提高缓存命中率:批量处理数据包,减少缓存未命中
  • 减少函数调用开销:批量处理,减少函数调用次数

类比:就像"批量处理旅客":

  • 提高效率:一次处理多个旅客,减少"准备时间"
  • 减少开销:批量处理,减少"检查次数"
8.9.2 预取(Prefetch)

预取:提前加载未来需要的数据到CPU缓存,减少等待时间。

c 复制代码
//395:410:src/plugins/acl/dataplane_node.c
      switch (n_left)                           // 根据剩余数据包数量选择预取策略
	{
	default:                                // 如果剩余数据包数量 >= 6
	  acl_fa_prefetch_session_bucket_for_hash (am, is_ip6, hash[5]);  // 预取第5个数据包的会话Hash桶
                                         // acl_fa_prefetch_session_bucket_for_hash:预取会话Hash桶
                                         // 作用:提前加载未来需要的数据到CPU缓存
                                         // 类比:就像"预取未来的'常客数据库'查询"
	  
	  /* fallthrough */
	case 5:
	case 4:
	  acl_fa_prefetch_session_data_for_hash (am, is_ip6, hash[3]);  // 预取第3个数据包的会话数据
                                         // acl_fa_prefetch_session_data_for_hash:预取会话数据
                                         // 类比:就像"预取未来的'常客信息'"
	  
	  /* fallthrough */
	case 3:
	case 2:
	  acl_fa_find_session_with_hash (am, is_ip6, sw_if_index[1],  // 查找第1个数据包的会话
					 hash[1], &fa_5tuple[1],
					 &f_sess_id_next.as_u64);
	  if (f_sess_id_next.as_u64 != ~0ULL)
	    {
	      prefetch_session_entry (am, f_sess_id_next);  // 预取会话条目
                                         // prefetch_session_entry:预取会话条目
                                         // 类比:就像"预取未来的'常客信息'"
	    }
	  /* fallthrough */
	case 1:
	  // 处理当前数据包
	}

优化效果

  • 隐藏内存延迟:提前加载数据,减少CPU等待时间
  • 提高流水线效率:CPU可以在处理当前数据包的同时预取未来数据

类比:就像"提前准备":

  • 隐藏等待时间:提前准备未来的"常客信息",减少等待时间
  • 提高效率:在处理当前旅客的同时,准备未来旅客的信息

8.10 本章小结

通过这一章的详细讲解,我们了解了:

  1. 数据平面ACL匹配的概念:什么是数据平面,VPP的架构
  2. 数据平面节点注册:如何将ACL节点插入到数据包处理流程
  3. 5-tuple提取:如何从数据包中提取5-tuple(源IP、目的IP、协议、源端口、目的端口)
  4. ACL匹配逻辑:如何匹配单个ACL(First-match语义)
  5. 多ACL匹配:如何匹配多个ACL(线性匹配和Hash匹配)
  6. 会话管理:如何管理Flow-aware会话(查找、创建、超时)
  7. 性能优化技术:向量化处理、预取等

核心要点

  • 数据平面ACL匹配是一个多步骤的过程(5-tuple提取 → 会话查找 → ACL匹配 → 会话创建)
  • First-match语义:找到第一个匹配的规则就返回
  • Flow-aware:记录会话状态,快速放行已建立的连接
  • 性能优化:使用向量化处理、预取等技术提高性能

下一步:在下一章,我们会看到Flow-aware ACL的详细实现,包括会话管理、超时处理等。


第9章:Flow-aware ACL详细实现------如何"记录和管理常客信息"?

生活类比:想象一下,你是一个机场安检员,需要建立一个"常客系统"(Flow-aware ACL)。

  • 当新旅客(新会话)第一次通过时,需要详细检查(ACL匹配),并记录他们的信息(创建会话)
  • 当常客(已有会话)再次通过时,可以快速放行(会话匹配),无需重复检查
  • 如果常客长时间不来(会话超时),需要从系统中删除他们的信息(会话清理)
  • 如果安检规则变化(Policy Epoch变化),需要重新检查常客(会话重分类)

这一章,我们就跟着ACL插件的代码,看看它是如何"记录和管理常客信息"的。每一步我都会详细解释,确保你完全理解。


9.1 Flow-aware ACL的概念:什么是"有状态ACL"?

9.1.1 什么是Flow-aware ACL?

Flow-aware ACL(有状态ACL):一种能够记录网络连接状态的ACL,对于已建立的连接,可以快速放行,无需重复检查ACL规则。

关键概念

  1. 会话(Session):一个网络连接的状态信息

    • 5-tuple:源IP、目的IP、协议、源端口、目的端口
    • 状态信息:TCP标志、最后活跃时间、超时类型等
    • 类比:就像"常客信息"(旅客信息、访问记录等)
  2. 会话表(Session Table):存储所有会话的Hash表

    • IPv4会话表 :使用 clib_bihash_16_8_t(16字节key,8字节value)
    • IPv6会话表 :使用 clib_bihash_40_8_t(40字节key,8字节value)
    • 类比:就像"常客数据库"(存储所有常客信息)
  3. 会话超时(Session Timeout):会话在空闲一段时间后自动删除

    • TCP Established :24小时(TCP_SESSION_IDLE_TIMEOUT_SEC
    • TCP Transient :120秒(TCP_SESSION_TRANSIENT_TIMEOUT_SEC
    • UDP Idle :600秒(UDP_SESSION_IDLE_TIMEOUT_SEC
    • 类比:就像"常客过期"(长时间不访问,从数据库中删除)
9.1.2 Flow-aware vs Stateless ACL
特性 Stateless ACL Flow-aware ACL
状态记录 有(记录会话状态)
匹配速度 每次都需要ACL匹配 已有会话快速放行
内存占用 高(需要存储会话)
适用场景 简单规则、低流量 复杂规则、高流量
TCP支持 无法识别TCP状态 可以识别TCP状态(Established/Transient)

类比

  • Stateless ACL:就像"每次都要检查身份证"(无状态,每次都要检查)
  • Flow-aware ACL:就像"常客系统"(有状态,常客可以快速放行)

9.2 会话结构体:如何"存储常客信息"?

9.2.1 会话结构体:fa_session_t

会话结构体 fa_session_t 用于存储一个网络连接的所有状态信息:

c 复制代码
//105:122:src/plugins/acl/fa_node.h
typedef struct {
  fa_5tuple_t info; /* (5+1)*8 = 48 bytes */  // 5-tuple信息(48字节)
                                         // fa_5tuple_t:5-tuple结构体(源IP、目的IP、协议、源端口、目的端口)
                                         // 类比:就像"常客的基本信息"(姓名、身份证号、出发地、目的地等)
  
  u64 last_active_time;   /* +8 bytes = 56 */  // 最后活跃时间(8字节)
                                         // last_active_time:会话最后活跃的时间戳(用于超时判断)
                                         // 类比:就像"常客最后访问时间"(用于判断是否过期)
  
  u32 sw_if_index;        /* +4 bytes = 60 */  // 接口索引(4字节)
                                         // sw_if_index:会话所属的接口索引(用于Policy Epoch检查)
                                         // 类比:就像"常客所属的通道"(用于判断规则是否变化)
  
  union {
    u8 as_u8[2];
    u16 as_u16;
  } tcp_flags_seen; ;     /* +2 bytes = 62 */  // TCP标志位(2字节)
                                         // tcp_flags_seen:已看到的TCP标志位(用于判断TCP状态)
                                         // as_u8[2]:按方向存储([0]=输入方向,[1]=输出方向)
                                         // 类比:就像"常客的访问记录"(记录输入和输出方向的TCP标志)
  
  u16 thread_index;          /* +2 bytes = 64 */  // 线程索引(2字节)
                                         // thread_index:会话所属的Worker线程索引(用于多核处理)
                                         // 类比:就像"常客所属的安检通道"(用于多核处理)
  
  u64 link_enqueue_time;  /* 8 byte = 8 */  // 链表入队时间(8字节)
                                         // link_enqueue_time:会话加入超时链表的时间(用于超时判断)
                                         // 类比:就像"常客加入'待检查列表'的时间"
  
  u32 link_prev_idx;      /* +4 bytes = 12 */  // 链表前一个节点索引(4字节)
                                         // link_prev_idx:超时链表中前一个会话的索引(用于链表遍历)
                                         // 类比:就像"常客在'待检查列表'中的'前一个常客'"
  
  u32 link_next_idx;      /* +4 bytes = 16 */  // 链表下一个节点索引(4字节)
                                         // link_next_idx:超时链表中下一个会话的索引(用于链表遍历)
                                         // 类比:就像"常客在'待检查列表'中的'下一个常客'"
  
  u8 link_list_id;        /* +1 bytes = 17 */  // 链表ID(1字节)
                                         // link_list_id:会话所属的超时链表ID(TCP Established、TCP Transient、UDP Idle等)
                                         // 类比:就像"常客所属的'待检查列表'类型"(VIP列表、普通列表等)
  
  u8 deleted;             /* +1 bytes = 18 */  // 删除标志(1字节)
                                         // deleted:会话是否已标记为删除(用于两阶段删除)
                                         // 类比:就像"常客是否已标记为删除"(准备删除但还未删除)
  
  u8 is_ip6;              /* +1 bytes = 19 */  // 是否IPv6(1字节)
                                         // is_ip6:会话是否使用IPv6(用于区分IPv4和IPv6会话)
                                         // 类比:就像"常客是否使用'国际航班'"(IPv6)
  
  u8 reserved1[5];        /* +5 bytes = 24 */  // 保留字段1(5字节)
                                         // reserved1:保留字段(用于未来扩展)
  
  u64 reserved2[5];       /* +5*8 bytes = 64 */  // 保留字段2(40字节)
                                         // reserved2:保留字段(用于未来扩展)
                                         // 总大小:128字节(缓存行对齐,用于性能优化)
} fa_session_t;

结构体字段总结

  1. 基本信息:5-tuple、接口索引、线程索引、地址族
  2. 状态信息:TCP标志、最后活跃时间
  3. 链表信息:链表前后节点、链表ID、入队时间
  4. 管理信息:删除标志、保留字段

类比:就像"常客档案":

  • 基本信息:姓名、身份证号、出发地、目的地、通道、地址族
  • 状态信息:访问记录、最后访问时间
  • 链表信息:在"待检查列表"中的位置、列表类型、加入时间
  • 管理信息:是否已标记删除、保留字段
9.2.2 会话ID:fa_full_session_id_t

会话ID用于唯一标识一个会话:

c 复制代码
//131:140:src/plugins/acl/fa_node.h
typedef struct {
  union {
    u64 as_u64;                                // 作为64位整数使用(用于Hash表)
                                         // 类比:就像"常客ID"(作为整数使用)
    struct {
      u32 session_index;                       // 会话索引(32位)
                                         // session_index:会话在pool中的索引
                                         // 类比:就像"常客在'档案柜'中的'位置编号'"
      
      u16 thread_index;                        // 线程索引(16位)
                                         // thread_index:会话所属的Worker线程索引
                                         // 类比:就像"常客所属的'安检通道编号'"
      
      u16 intf_policy_epoch;                   // 接口策略纪元(16位)
                                         // intf_policy_epoch:会话创建时的Policy Epoch(用于检测过期会话)
                                         // 类比:就像"常客注册时的'规则版本号'"(用于判断规则是否变化)
    };
  };
} fa_full_session_id_t;

会话ID的编码

  • 低32位:会话索引(在pool中的位置)
  • 中16位:线程索引(所属的Worker线程)
  • 高16位:接口策略纪元(创建时的Policy Epoch)

类比:就像"常客ID"的编码:

  • 低32位:在"档案柜"中的"位置编号"
  • 中16位:所属的"安检通道编号"
  • 高16位:注册时的"规则版本号"

9.3 会话创建:如何"登记新常客"?

9.3.1 会话创建函数:acl_fa_add_session

当数据包匹配ACL规则且需要创建会话时,会调用 acl_fa_add_session 函数:

c 复制代码
//514:579:src/plugins/acl/session_inlines.h
always_inline fa_full_session_id_t              // 返回类型:fa_full_session_id_t(会话ID)
acl_fa_add_session (acl_main_t * am,            // 参数:am(ACL插件主结构体)
		    int is_input,                  // 参数:is_input(是否输入方向,1=输入,0=输出)
		    int is_ip6,                    // 参数:is_ip6(是否IPv6,0=IPv4,1=IPv6)
		    u32 sw_if_index,               // 参数:sw_if_index(接口索引)
		    u64 now,                       // 参数:now(当前时间戳)
		    fa_5tuple_t * p5tuple,         // 参数:p5tuple(数据包的5-tuple)
		    u16 current_policy_epoch)      // 参数:current_policy_epoch(当前Policy Epoch)
{
  fa_full_session_id_t f_sess_id;               // 会话ID(返回值)
  uword thread_index = os_get_thread_index ();  // 获取当前线程索引
                                         // os_get_thread_index:获取当前Worker线程索引
                                         // 类比:就像"获取当前'安检通道编号'"
  
  acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index];  // 获取当前线程的Per-worker数据
                                         // per_worker_data:每个Worker线程的数据(会话pool、链表等)
                                         // 类比:就像"获取当前'安检通道'的'数据'"

  f_sess_id.thread_index = thread_index;        // 设置会话ID的线程索引
                                         // 类比:就像"设置'常客ID'的'安检通道编号'"
  
  fa_session_t *sess;                           // 会话结构体指针(临时变量)

  // ========== 第一部分:验证会话ID ==========
  
  if (f_sess_id.as_u64 == ~0)                   // 如果会话ID为~0(无效值)
    {
      clib_error ("Adding session with invalid value");  // 打印错误并终止程序
                                         // clib_error:VPP的错误输出函数(会终止程序)
                                         // 类比:就像"如果'常客ID'无效,报错并终止"
    }

  // ========== 第二部分:分配会话内存 ==========
  
  pool_get_aligned (pw->fa_sessions_pool, sess, CLIB_CACHE_LINE_BYTES);  // 从pool中分配会话内存(缓存行对齐)
                                         // pool_get_aligned:从pool中分配对齐的内存(缓存行对齐,用于性能优化)
                                         // CLIB_CACHE_LINE_BYTES:缓存行大小(通常是64字节)
                                         // fa_sessions_pool:会话pool(每个Worker线程一个)
                                         // 类比:就像"从'档案柜'中分配新的'常客档案夹'"
  
  f_sess_id.session_index = sess - pw->fa_sessions_pool;  // 计算会话索引(在pool中的位置)
                                         // sess - pw->fa_sessions_pool:计算指针差值,得到索引
                                         // 类比:就像"计算'常客'在'档案柜'中的'位置编号'"
  
  f_sess_id.intf_policy_epoch = current_policy_epoch;  // 设置会话ID的策略纪元
                                         // current_policy_epoch:当前接口的Policy Epoch(从get_current_policy_epoch获取)
                                         // 类比:就像"设置'常客ID'的'规则版本号'"(用于判断规则是否变化)

  // ========== 第三部分:填充会话基本信息 ==========
  
  if (is_ip6)                                   // 如果是IPv6
    {
      // 填充IPv6会话的5-tuple(使用40_8 Hash表)
      sess->info.kv_40_8.key[0] = p5tuple->kv_40_8.key[0];  // 复制Hash表key的第0个u64
      sess->info.kv_40_8.key[1] = p5tuple->kv_40_8.key[1];  // 复制Hash表key的第1个u64
      sess->info.kv_40_8.key[2] = p5tuple->kv_40_8.key[2];  // 复制Hash表key的第2个u64
      sess->info.kv_40_8.key[3] = p5tuple->kv_40_8.key[3];  // 复制Hash表key的第3个u64
      sess->info.kv_40_8.key[4] = p5tuple->kv_40_8.key[4];  // 复制Hash表key的第4个u64
      sess->info.kv_40_8.value = f_sess_id.as_u64;          // 设置Hash表value为会话ID
                                         // kv_40_8:40字节key、8字节value的Hash表结构体
                                         // 类比:就像"复制'常客'的'基本信息'到'档案夹'"
    }
  else                                          // 如果是IPv4
    {
      // 填充IPv4会话的5-tuple(使用16_8 Hash表)
      sess->info.kv_16_8.key[0] = p5tuple->kv_16_8.key[0];  // 复制Hash表key的第0个u64
      sess->info.kv_16_8.key[1] = p5tuple->kv_16_8.key[1];  // 复制Hash表key的第1个u64
      sess->info.kv_16_8.value = f_sess_id.as_u64;          // 设置Hash表value为会话ID
                                         // kv_16_8:16字节key、8字节value的Hash表结构体
                                         // 类比:就像"复制'常客'的'基本信息'到'档案夹'"
    }

  // ========== 第四部分:初始化会话状态信息 ==========
  
  sess->last_active_time = now;                 // 设置最后活跃时间为当前时间
                                         // 类比:就像"设置'常客'的'最后访问时间'为当前时间"
  
  sess->sw_if_index = sw_if_index;              // 设置接口索引
                                         // 类比:就像"设置'常客'的'通道编号'"
  
  sess->tcp_flags_seen.as_u16 = 0;              // 初始化TCP标志位为0
                                         // tcp_flags_seen:已看到的TCP标志位(初始化为0,表示还未看到任何标志)
                                         // 类比:就像"初始化'常客'的'访问记录'为空"
  
  sess->thread_index = thread_index;            // 设置线程索引
                                         // 类比:就像"设置'常客'的'安检通道编号'"
  
  sess->link_list_id = ACL_TIMEOUT_UNUSED;      // 初始化链表ID为未使用
                                         // ACL_TIMEOUT_UNUSED:未使用(0)
                                         // 类比:就像"初始化'常客'的'待检查列表类型'为未使用"
  
  sess->link_prev_idx = FA_SESSION_BOGUS_INDEX; // 初始化链表前一个节点索引为无效
                                         // FA_SESSION_BOGUS_INDEX:无效索引(~0)
                                         // 类比:就像"初始化'常客'的'前一个常客'为无效"
  
  sess->link_next_idx = FA_SESSION_BOGUS_INDEX; // 初始化链表下一个节点索引为无效
                                         // 类比:就像"初始化'常客'的'下一个常客'为无效"
  
  sess->deleted = 0;                            // 初始化删除标志为0(未删除)
                                         // 类比:就像"初始化'常客'的'删除标志'为未删除"
  
  sess->is_ip6 = is_ip6;                        // 设置地址族
                                         // 类比:就像"设置'常客'的'地址族'(IPv4或IPv6)"

  // ========== 第五部分:将会话添加到超时链表 ==========
  
  acl_fa_conn_list_add_session (am, f_sess_id, now);  // 将会话添加到超时链表
                                         // acl_fa_conn_list_add_session:将会话添加到超时链表(我们下面详细讲解)
                                         // 作用:根据会话的超时类型,将会话添加到对应的超时链表中
                                         // 类比:就像"将'常客'添加到'待检查列表'"

  // ========== 第六部分:将会话添加到Hash表 ==========
  
  ASSERT (am->fa_sessions_hash_is_initialized == 1);  // 断言Hash表已初始化
                                         // ASSERT:断言(如果条件不满足,程序终止)
                                         // fa_sessions_hash_is_initialized:Hash表初始化标志
                                         // 类比:就像"确保'常客数据库'已初始化"
  
  if (is_ip6)                                   // 如果是IPv6
    {
      clib_bihash_add_del_40_8 (&am->fa_ip6_sessions_hash, &sess->info.kv_40_8, 1);  // 添加到IPv6会话Hash表
                                         // clib_bihash_add_del_40_8:添加或删除40_8 Hash表条目
                                         // fa_ip6_sessions_hash:IPv6会话Hash表
                                         // 参数:Hash表指针、key-value对、1(添加)
                                         // 类比:就像"将'常客'添加到'IPv6常客数据库'"
      
      reverse_session_add_del_ip6 (am, &sess->info.kv_40_8, 1);  // 添加反向会话(用于反向流量匹配)
                                         // reverse_session_add_del_ip6:添加或删除IPv6反向会话
                                         // 作用:创建反向会话(交换源IP和目的IP、源端口和目的端口),用于匹配反向流量
                                         // 类比:就像"创建'反向常客记录'"(用于匹配返回流量)
    }
  else                                          // 如果是IPv4
    {
      clib_bihash_add_del_16_8 (&am->fa_ip4_sessions_hash, &sess->info.kv_16_8, 1);  // 添加到IPv4会话Hash表
                                         // clib_bihash_add_del_16_8:添加或删除16_8 Hash表条目
                                         // fa_ip4_sessions_hash:IPv4会话Hash表
                                         // 类比:就像"将'常客'添加到'IPv4常客数据库'"
      
      reverse_session_add_del_ip4 (am, &sess->info.kv_16_8, 1);  // 添加反向会话(用于反向流量匹配)
                                         // reverse_session_add_del_ip4:添加或删除IPv4反向会话
                                         // 类比:就像"创建'反向常客记录'"(用于匹配返回流量)
    }

  // ========== 第七部分:更新统计信息 ==========
  
  vec_validate (pw->fa_session_adds_by_sw_if_index, sw_if_index);  // 确保统计向量有足够的空间
                                         // fa_session_adds_by_sw_if_index:按接口索引统计会话添加次数
                                         // 类比:就像"确保'通道统计表'有足够的空间"
  
  pw->fa_session_adds_by_sw_if_index[sw_if_index]++;  // 增加接口的会话添加计数
                                         // 类比:就像"增加'通道'的'常客添加计数'"
  
  clib_atomic_fetch_add (&am->fa_session_total_adds, 1);  // 原子增加全局会话添加计数
                                         // clib_atomic_fetch_add:原子操作(线程安全)
                                         // fa_session_total_adds:全局会话添加计数
                                         // 类比:就像"原子增加'全局常客添加计数'"

  return f_sess_id;                             // 返回会话ID
                                         // 类比:就像"返回'常客ID'"
}

函数总结

这个函数完成了以下工作:

  1. 验证参数:检查会话ID是否有效
  2. 分配内存:从pool中分配会话内存(缓存行对齐)
  3. 填充基本信息:复制5-tuple、设置接口索引、线程索引、Policy Epoch
  4. 初始化状态:初始化TCP标志、最后活跃时间、链表信息等
  5. 添加到链表:将会话添加到超时链表(根据超时类型)
  6. 添加到Hash表:将会话添加到Hash表(用于快速查找)
  7. 创建反向会话:创建反向会话(用于匹配反向流量)
  8. 更新统计:更新会话添加统计信息

类比:就像完整的"新常客登记流程":

  1. 验证信息:检查"常客信息"是否有效
  2. 分配档案:从"档案柜"中分配新的"常客档案夹"
  3. 填写基本信息:填写"姓名"、"身份证号"、"出发地"、"目的地"等
  4. 初始化状态:初始化"访问记录"、"最后访问时间"等
  5. 加入列表:将"常客"添加到"待检查列表"(根据类型)
  6. 加入数据库:将"常客"添加到"常客数据库"(用于快速查找)
  7. 创建反向记录:创建"反向常客记录"(用于匹配返回流量)
  8. 更新统计:更新"常客添加统计"

9.4 会话查找:如何"查找常客信息"?

9.4.1 会话Hash值计算:acl_fa_make_session_hash

在查找会话之前,需要先计算会话的Hash值:

c 复制代码
//605:613:src/plugins/acl/session_inlines.h
always_inline u64                                // 返回类型:u64(Hash值)
acl_fa_make_session_hash (acl_main_t * am,      // 参数:am(ACL插件主结构体,未使用)
			  int is_ip6,              // 参数:is_ip6(是否IPv6,0=IPv4,1=IPv6)
			  u32 sw_if_index0,        // 参数:sw_if_index0(接口索引,未使用)
			  fa_5tuple_t * p5tuple)   // 参数:p5tuple(数据包的5-tuple)
{
  if (is_ip6)                                    // 如果是IPv6
    return clib_bihash_hash_40_8 (&p5tuple->kv_40_8);  // 计算IPv6会话Hash值
                                         // clib_bihash_hash_40_8:计算40_8 Hash表的Hash值
                                         // 作用:根据5-tuple计算Hash值(用于快速查找)
                                         // 类比:就像"根据'常客信息'计算'Hash值'"(用于快速查找)
  else                                          // 如果是IPv4
    return clib_bihash_hash_16_8 (&p5tuple->kv_16_8);  // 计算IPv4会话Hash值
                                         // clib_bihash_hash_16_8:计算16_8 Hash表的Hash值
                                         // 类比:就像"根据'常客信息'计算'Hash值'"
}

函数总结

根据地址族(IPv4或IPv6)选择对应的Hash函数计算Hash值。

类比:就像"根据'常客信息'计算'Hash值'"(用于快速查找)。


9.4.2 会话查找函数:acl_fa_find_session_with_hash

acl_fa_find_session_with_hash 使用Hash值在Hash表中查找会话:

c 复制代码
//634:659:src/plugins/acl/session_inlines.h
always_inline int                                // 返回类型:int(1=找到,0=未找到)
acl_fa_find_session_with_hash (acl_main_t * am,  // 参数:am(ACL插件主结构体)
			       int is_ip6,          // 参数:is_ip6(是否IPv6,0=IPv4,1=IPv6)
			       u32 sw_if_index0,    // 参数:sw_if_index0(接口索引,未使用)
			       u64 hash,            // 参数:hash(会话Hash值)
			       fa_5tuple_t * p5tuple,  // 参数:p5tuple(数据包的5-tuple)
			       u64 * pvalue_sess)   // 参数:pvalue_sess(输出:会话ID)
{
  int res = 0;                                   // 返回值(0=未找到,1=找到)
  
  if (is_ip6)                                    // 如果是IPv6
    {
      clib_bihash_kv_40_8_t kv_result;          // Hash表查找结果(临时变量)
      kv_result.value = ~0ULL;                  // 初始化value为无效值
                                         // ~0ULL:64位全1(无效值)
                                         // 类比:就像"初始化'查找结果'为无效"
      
      res = (clib_bihash_search_inline_2_with_hash_40_8  // 在IPv6会话Hash表中查找
	     (&am->fa_ip6_sessions_hash, hash, &p5tuple->kv_40_8,  // 参数:Hash表指针、Hash值、key
	      &kv_result) == 0);                                  // 参数:输出结果
                                         // clib_bihash_search_inline_2_with_hash_40_8:使用Hash值在40_8 Hash表中查找
                                         // 返回值:0=找到,非0=未找到
                                         // 类比:就像"在'IPv6常客数据库'中查找'常客'"
      
      *pvalue_sess = kv_result.value;           // 设置输出参数为查找结果
                                         // kv_result.value:Hash表的value(会话ID)
                                         // 类比:就像"设置'查找结果'为'常客ID'"
    }
  else                                          // 如果是IPv4
    {
      clib_bihash_kv_16_8_t kv_result;          // Hash表查找结果(临时变量)
      kv_result.value = ~0ULL;                  // 初始化value为无效值
                                         // 类比:就像"初始化'查找结果'为无效"
      
      res = (clib_bihash_search_inline_2_with_hash_16_8  // 在IPv4会话Hash表中查找
	     (&am->fa_ip4_sessions_hash, hash, &p5tuple->kv_16_8,  // 参数:Hash表指针、Hash值、key
	      &kv_result) == 0);                                  // 参数:输出结果
                                         // clib_bihash_search_inline_2_with_hash_16_8:使用Hash值在16_8 Hash表中查找
                                         // 类比:就像"在'IPv4常客数据库'中查找'常客'"
      
      *pvalue_sess = kv_result.value;           // 设置输出参数为查找结果
                                         // 类比:就像"设置'查找结果'为'常客ID'"
    }
  
  return res;                                    // 返回查找结果(1=找到,0=未找到)
                                         // 类比:就像"返回'查找结果'"(找到或未找到)
}

函数总结

  1. 判断地址族 :根据 is_ip6 选择IPv4或IPv6 Hash表
  2. Hash表查找:使用Hash值和5-tuple在Hash表中查找
  3. 返回结果:如果找到,返回会话ID;否则返回无效值

类比:就像"在'常客数据库'中查找'常客'":

  1. 判断类型:判断是"国内航班"(IPv4)还是"国际航班"(IPv6)
  2. 数据库查找:在对应的"常客数据库"中查找
  3. 返回结果:如果找到,返回"常客ID";否则返回"无效ID"

9.5 会话跟踪:如何"更新常客访问记录"?

9.5.1 会话跟踪函数:acl_fa_track_session

当数据包匹配已有会话时,需要更新会话的状态信息(最后活跃时间、TCP标志等):

c 复制代码
//276:292:src/plugins/acl/session_inlines.h
always_inline u8                                 // 返回类型:u8(动作,3表示permit)
acl_fa_track_session (acl_main_t * am,           // 参数:am(ACL插件主结构体,未使用)
		      int is_input,                 // 参数:is_input(是否输入方向,1=输入,0=输出)
		      u32 sw_if_index,              // 参数:sw_if_index(接口索引,未使用)
		      u64 now,                      // 参数:now(当前时间戳)
		      fa_session_t * sess,          // 参数:sess(会话结构体指针)
		      fa_5tuple_t * pkt_5tuple,    // 参数:pkt_5tuple(数据包的5-tuple)
		      u32 pkt_len)                  // 参数:pkt_len(数据包长度,未使用)
{
  sess->last_active_time = now;                  // 更新最后活跃时间为当前时间
                                         // 类比:就像"更新'常客'的'最后访问时间'为当前时间"
  
  u8 old_flags = sess->tcp_flags_seen.as_u8[is_input];  // 获取旧TCP标志位
                                         // tcp_flags_seen.as_u8[is_input]:按方向存储TCP标志位
                                         // [0]:输入方向的TCP标志位
                                         // [1]:输出方向的TCP标志位
                                         // 类比:就像"获取'常客'的'旧访问记录'"
  
  u8 new_flags = old_flags | pkt_5tuple->pkt.tcp_flags;  // 计算新TCP标志位(位或操作)
                                         // old_flags | pkt_5tuple->pkt.tcp_flags:将旧标志位与新标志位合并(位或操作)
                                         // 作用:记录已看到的TCP标志位(如SYN、ACK、FIN等)
                                         // 类比:就像"合并'旧访问记录'和'新访问记录'"

  int flags_need_update = pkt_5tuple->pkt.tcp_flags_valid  // 检查是否需要更新TCP标志位
    && (old_flags != new_flags);                            // 条件:TCP标志有效且标志位发生变化
                                         // tcp_flags_valid:TCP标志有效性标志
                                         // old_flags != new_flags:标志位是否发生变化
                                         // 类比:就像"检查是否需要更新'访问记录'"
  
  if (PREDICT_FALSE (flags_need_update))        // 如果需要更新(PREDICT_FALSE表示不常见)
    {
      sess->tcp_flags_seen.as_u8[is_input] = new_flags;  // 更新TCP标志位
                                         // 类比:就像"更新'常客'的'访问记录'"
    }
  
  return 3;                                      // 返回动作3(permit)
                                         // 为什么是3?3表示permit(允许通过)
                                         // 类比:就像"返回'放行'"
}

函数总结

  1. 更新最后活跃时间:将会话的最后活跃时间更新为当前时间
  2. 更新TCP标志位:如果TCP标志有效且发生变化,更新TCP标志位
  3. 返回动作:返回permit(允许通过)

类比:就像"更新常客访问记录":

  1. 更新访问时间:更新"常客"的"最后访问时间"
  2. 更新访问记录:如果"访问记录"发生变化,更新"访问记录"
  3. 返回结果:返回"放行"

9.6 会话超时类型:如何"判断常客类型"?

9.6.1 超时类型判断:fa_session_get_timeout_type

根据会话的协议和TCP标志,判断会话的超时类型:

c 复制代码
//70:95:src/plugins/acl/session_inlines.h
always_inline int                                // 返回类型:int(超时类型)
fa_session_get_timeout_type (acl_main_t * am,    // 参数:am(ACL插件主结构体,未使用)
			     fa_session_t * sess)  // 参数:sess(会话结构体指针)
{
  /* seen both SYNs and ACKs but not FINs means we are in established state */
  u16 masked_flags =                             // 掩码后的TCP标志位(临时变量)
    sess->tcp_flags_seen.as_u16 & ((TCP_FLAGS_RSTFINACKSYN << 8) +  // 输入方向标志位掩码
				   TCP_FLAGS_RSTFINACKSYN);          // 输出方向标志位掩码
                                         // TCP_FLAGS_RSTFINACKSYN:TCP标志位掩码(RST、FIN、ACK、SYN)
                                         // << 8:左移8位(输入方向标志位)
                                         // &:位与操作(只保留RST、FIN、ACK、SYN标志位)
                                         // 类比:就像"提取'访问记录'中的'关键标志'"
  
  switch (sess->info.l4.proto)                   // 根据协议类型判断
    {
    case IPPROTO_TCP:                            // 如果是TCP协议
      if (((TCP_FLAGS_ACKSYN << 8) + TCP_FLAGS_ACKSYN) == masked_flags)  // 如果已看到SYN和ACK(两个方向)
	{
	  return ACL_TIMEOUT_TCP_IDLE;            // 返回TCP Established超时类型
                                         // ACL_TIMEOUT_TCP_IDLE:TCP Established超时类型(24小时)
                                         // 条件:输入方向和输出方向都看到了SYN和ACK(连接已建立)
                                         // 类比:就像"如果'常客'已建立连接,返回'VIP类型'"
	}
      else
	{
	  return ACL_TIMEOUT_TCP_TRANSIENT;       // 返回TCP Transient超时类型
                                         // ACL_TIMEOUT_TCP_TRANSIENT:TCP Transient超时类型(120秒)
                                         // 条件:连接未建立(未看到SYN和ACK,或只看到一个方向)
                                         // 类比:就像"如果'常客'未建立连接,返回'普通类型'"
	}
      break;
    
    case IPPROTO_UDP:                            // 如果是UDP协议
      return ACL_TIMEOUT_UDP_IDLE;               // 返回UDP Idle超时类型
                                         // ACL_TIMEOUT_UDP_IDLE:UDP Idle超时类型(600秒)
                                         // UDP是无状态协议,没有连接建立过程
                                         // 类比:就像"如果'常客'使用'UDP航班',返回'UDP类型'"
      break;
    
    default:                                      // 如果是其他协议
      return ACL_TIMEOUT_UDP_IDLE;               // 返回UDP Idle超时类型(默认)
                                         // 类比:就像"如果'常客'使用'其他航班',返回'默认类型'"
    }
}

函数总结

  1. 提取TCP标志:从会话的TCP标志位中提取关键标志(RST、FIN、ACK、SYN)
  2. 判断协议类型:根据协议类型(TCP、UDP、其他)选择超时类型
  3. 判断TCP状态:如果是TCP,根据是否已建立连接(看到SYN和ACK)选择超时类型

超时类型

  • ACL_TIMEOUT_TCP_IDLE:TCP Established(24小时)
  • ACL_TIMEOUT_TCP_TRANSIENT:TCP Transient(120秒)
  • ACL_TIMEOUT_UDP_IDLE:UDP Idle(600秒)

类比:就像"判断常客类型":

  • VIP类型(TCP Established):已建立连接的常客(24小时超时)
  • 普通类型(TCP Transient):未建立连接的常客(120秒超时)
  • UDP类型(UDP Idle):使用UDP的常客(600秒超时)

9.7 会话超时计算:如何"计算常客过期时间"?

9.7.1 超时时间计算:fa_session_get_timeout

根据会话的超时类型,计算会话的超时时间:

c 复制代码
//101:115:src/plugins/acl/session_inlines.h
always_inline u64                                // 返回类型:u64(超时时间,单位:时钟周期)
fa_session_get_timeout (acl_main_t * am,         // 参数:am(ACL插件主结构体)
			fa_session_t * sess)     // 参数:sess(会话结构体指针)
{
  u64 timeout = (am->vlib_main->clib_time.clocks_per_second);  // 获取每秒时钟周期数
                                         // clocks_per_second:每秒时钟周期数(用于时间转换)
                                         // 类比:就像"获取'每秒时间单位数'"
  
  if (sess->link_list_id == ACL_TIMEOUT_PURGATORY)  // 如果会话在Purgatory链表中
    {
      timeout /= (1000000 / SESSION_PURGATORY_TIMEOUT_USEC);  // 计算Purgatory超时时间
                                         // SESSION_PURGATORY_TIMEOUT_USEC:Purgatory超时时间(10微秒)
                                         // 1000000 / SESSION_PURGATORY_TIMEOUT_USEC:每秒的Purgatory周期数
                                         // timeout /= ...:计算Purgatory超时时间(单位:时钟周期)
                                         // 类比:就像"如果'常客'在'待删除列表'中,计算'待删除超时时间'"
    }
  else                                          // 如果会话在正常超时链表中
    {
      int timeout_type = fa_session_get_timeout_type (am, sess);  // 获取超时类型
                                         // fa_session_get_timeout_type:获取会话的超时类型(我们上面已经讲解过)
                                         // 类比:就像"获取'常客类型'"
      
      timeout *= am->session_timeout_sec[timeout_type];  // 计算超时时间(秒 × 每秒时钟周期数)
                                         // session_timeout_sec[timeout_type]:超时时间(秒)
                                         // timeout *= ...:计算超时时间(单位:时钟周期)
                                         // 类比:就像"根据'常客类型'计算'过期时间'"
    }
  
  return timeout;                                // 返回超时时间
                                         // 类比:就像"返回'过期时间'"
}

函数总结

  1. 获取时钟周期:获取每秒时钟周期数(用于时间转换)
  2. 判断链表类型:如果会话在Purgatory链表中,使用Purgatory超时时间(10微秒)
  3. 计算超时时间:根据超时类型(TCP Established、TCP Transient、UDP Idle)计算超时时间

超时时间配置

  • TCP Established :24小时(TCP_SESSION_IDLE_TIMEOUT_SEC
  • TCP Transient :120秒(TCP_SESSION_TRANSIENT_TIMEOUT_SEC
  • UDP Idle :600秒(UDP_SESSION_IDLE_TIMEOUT_SEC
  • Purgatory :10微秒(SESSION_PURGATORY_TIMEOUT_USEC

类比:就像"计算常客过期时间":

  • VIP类型:24小时后过期
  • 普通类型:120秒后过期
  • UDP类型:600秒后过期
  • 待删除类型:10微秒后过期

9.8 会话链表管理:如何"管理待检查列表"?

9.8.1 会话链表添加:acl_fa_conn_list_add_session

会话需要根据超时类型添加到对应的超时链表中,用于超时检查:

c 复制代码
//146:190:src/plugins/acl/session_inlines.h
always_inline void                              // 返回类型:void(无返回值)
acl_fa_conn_list_add_session (acl_main_t * am,  // 参数:am(ACL插件主结构体)
			      fa_full_session_id_t sess_id,  // 参数:sess_id(会话ID)
			      u64 now)              // 参数:now(当前时间戳)
{
  fa_session_t *sess =                          // 获取会话结构体指针
    get_session_ptr (am, sess_id.thread_index, sess_id.session_index);  // 从pool中获取会话指针
                                         // get_session_ptr:从pool中获取会话指针(带边界检查)
                                         // 类比:就像"从'档案柜'中获取'常客档案夹'"
  
  u8 list_id =                                   // 链表ID(临时变量)
    sess->deleted ? ACL_TIMEOUT_PURGATORY : fa_session_get_timeout_type (am, sess);  // 根据删除标志和超时类型选择链表ID
                                         // sess->deleted:会话是否已标记为删除
                                         // ACL_TIMEOUT_PURGATORY:Purgatory链表ID(用于待删除会话)
                                         // fa_session_get_timeout_type:获取会话的超时类型
                                         // 类比:就像"根据'常客'的'删除标志'和'类型'选择'待检查列表'"
  
  uword thread_index = os_get_thread_index ();  // 获取当前线程索引
  acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index];  // 获取当前线程的Per-worker数据
                                         // 类比:就像"获取当前'安检通道'的'数据'"

  // ========== 第一部分:验证线程一致性 ==========
  
  /* the retrieved session thread index must be necessarily the same as the one in the key */
  ASSERT (sess->thread_index == sess_id.thread_index);  // 断言会话的线程索引与会话ID的线程索引一致
                                         // 为什么需要?确保会话属于正确的线程
                                         // 类比:就像"确保'常客'属于正确的'安检通道'"
  
  /* the retrieved session thread index must be the same as current thread */
  ASSERT (sess->thread_index == thread_index);  // 断言会话的线程索引与当前线程索引一致
                                         // 为什么需要?确保在当前线程中操作会话
                                         // 类比:就像"确保'常客'在当前'安检通道'中操作"

  // ========== 第二部分:设置会话的链表信息 ==========
  
  sess->link_enqueue_time = now;                 // 设置链表入队时间为当前时间
                                         // 类比:就像"设置'常客'加入'待检查列表'的时间"
  
  sess->link_list_id = list_id;                  // 设置链表ID
                                         // 类比:就像"设置'常客'的'待检查列表类型'"
  
  sess->link_next_idx = FA_SESSION_BOGUS_INDEX; // 初始化链表下一个节点索引为无效
                                         // 类比:就像"初始化'常客'的'下一个常客'为无效"
  
  sess->link_prev_idx = pw->fa_conn_list_tail[list_id];  // 设置链表前一个节点索引为链表尾
                                         // fa_conn_list_tail[list_id]:链表的尾节点索引
                                         // 类比:就像"设置'常客'的'前一个常客'为'列表尾'"

  // ========== 第三部分:更新链表结构(双向链表) ==========
  
  if (FA_SESSION_BOGUS_INDEX != pw->fa_conn_list_tail[list_id])  // 如果链表不为空
    {
      fa_session_t *prev_sess =                  // 获取前一个会话的指针
	get_session_ptr (am, thread_index, pw->fa_conn_list_tail[list_id]);  // 从pool中获取前一个会话指针
                                         // 类比:就像"获取'列表尾'的'常客档案夹'"
      
      prev_sess->link_next_idx = sess_id.session_index;  // 设置前一个会话的下一个节点索引为当前会话
                                         // 类比:就像"设置'列表尾'的'下一个常客'为当前'常客'"
      
      /* We should never try to link with a session on another thread */
      ASSERT (prev_sess->thread_index == sess->thread_index);  // 断言前一个会话的线程索引与当前会话一致
                                         // 为什么需要?确保所有会话属于同一个线程
                                         // 类比:就像"确保所有'常客'属于同一个'安检通道'"
    }
  
  pw->fa_conn_list_tail[list_id] = sess_id.session_index;  // 更新链表尾节点索引为当前会话
                                         // 类比:就像"更新'列表尾'为当前'常客'"

  // ========== 第四部分:更新链表头(如果是第一个节点) ==========
  
  if (FA_SESSION_BOGUS_INDEX == pw->fa_conn_list_head[list_id])  // 如果链表为空(头节点为无效)
    {
      pw->fa_conn_list_head[list_id] = sess_id.session_index;  // 设置链表头节点索引为当前会话
                                         // fa_conn_list_head[list_id]:链表的头节点索引
                                         // 类比:就像"如果'列表'为空,设置'列表头'为当前'常客'"
      
      /* set the head expiry time because it is the first element */
      pw->fa_conn_list_head_expiry_time[list_id] =  // 设置链表头节点的过期时间
	now + fa_session_get_timeout (am, sess);     // 当前时间 + 会话超时时间
                                         // fa_conn_list_head_expiry_time[list_id]:链表头节点的过期时间
                                         // fa_session_get_timeout:获取会话的超时时间(我们上面已经讲解过)
                                         // 类比:就像"设置'列表头'的'过期时间'为当前时间 + '常客过期时间'"
    }
}

函数总结

这个函数实现了双向链表的尾部插入

  1. 选择链表:根据删除标志和超时类型选择链表(Purgatory、TCP Established、TCP Transient、UDP Idle)
  2. 验证线程:确保会话属于正确的线程
  3. 设置链表信息:设置会话的链表信息(前一个节点、下一个节点、链表ID、入队时间)
  4. 更新链表结构:更新双向链表的结构(前一个节点的下一个节点、链表尾)
  5. 更新链表头:如果链表为空,设置链表头和过期时间

类比:就像"将常客添加到待检查列表":

  1. 选择列表:根据"常客类型"选择"待检查列表"(VIP列表、普通列表等)
  2. 验证通道:确保"常客"属于正确的"安检通道"
  3. 设置位置:设置"常客"在"列表"中的位置(前一个常客、下一个常客、列表类型、加入时间)
  4. 更新列表:更新"列表"的结构(前一个常客的下一个常客、列表尾)
  5. 更新列表头:如果"列表"为空,设置"列表头"和"过期时间"

9.9 反向会话:如何"匹配返回流量"?

9.9.1 什么是反向会话?

反向会话(Reverse Session):交换源IP和目的IP、源端口和目的端口的会话,用于匹配返回流量。

为什么需要反向会话?

  • 问题:当客户端发送数据包时,创建了正向会话(client → server)
  • 问题:当服务器返回数据包时,源IP和目的IP、源端口和目的端口都交换了(server → client)
  • 解决方案:创建反向会话(交换源和目的),用于匹配返回流量

类比:就像"双向常客记录":

  • 正向记录:客户端 → 服务器(用于匹配客户端发送的流量)
  • 反向记录:服务器 → 客户端(用于匹配服务器返回的流量)
9.9.2 反向会话创建:reverse_session_add_del_ip6

当创建IPv6会话时,需要同时创建反向会话:

c 复制代码
//358:379:src/plugins/acl/session_inlines.h
always_inline void                              // 返回类型:void
reverse_session_add_del_ip6 (acl_main_t * am,   // 参数:am(ACL插件主结构体)
			     clib_bihash_kv_40_8_t * pkv,  // 参数:pkv(正向会话的key-value对)
			     int is_add)            // 参数:is_add(是否添加,1=添加,0=删除)
{
  clib_bihash_kv_40_8_t kv2;                    // 反向会话的key-value对(临时变量)
  
  // ========== 第一部分:交换IP地址 ==========
  
  kv2.key[0] = pkv->key[2];                     // 反向key[0] = 正向key[2](交换源IP和目的IP)
  kv2.key[1] = pkv->key[3];                     // 反向key[1] = 正向key[3]
  kv2.key[2] = pkv->key[0];                     // 反向key[2] = 正向key[0]
  kv2.key[3] = pkv->key[1];                     // 反向key[3] = 正向key[1]
                                         // 为什么这样交换?
                                         // key[0-1]:源IPv6地址(128位,2个u64)
                                         // key[2-3]:目的IPv6地址(128位,2个u64)
                                         // 交换后:key[0-1] = 目的IPv6地址,key[2-3] = 源IPv6地址
                                         // 类比:就像"交换'出发地'和'目的地'"
  
  /* the last u64 needs special treatment (ports, etc.) so we do it last */
  kv2.value = pkv->value;                       // 反向value = 正向value(会话ID相同)
                                         // 为什么相同?反向会话和正向会话指向同一个会话结构体
                                         // 类比:就像"反向记录和正向记录指向同一个'常客档案'"

  // ========== 第二部分:交换端口和方向标志 ==========
  
  if (PREDICT_FALSE (is_session_l4_key_u64_slowpath (pkv->key[4])))  // 如果是慢路径(ICMP等)
    {
      if (reverse_l4_u64_slowpath_valid (pkv->key[4], 1, &kv2.key[4]))  // 如果反向L4 key有效
	clib_bihash_add_del_40_8 (&am->fa_ip6_sessions_hash, &kv2, is_add);  // 添加到Hash表
                                         // reverse_l4_u64_slowpath_valid:计算反向L4 key(ICMP需要特殊处理)
                                         // 类比:就像"如果'ICMP航班',需要特殊处理"
    }
  else                                          // 如果是快路径(TCP/UDP)
    {
      kv2.key[4] = reverse_l4_u64_fastpath (pkv->key[4], 1);  // 计算反向L4 key(交换端口和方向标志)
                                         // reverse_l4_u64_fastpath:快速计算反向L4 key(交换端口和方向标志)
                                         // 类比:就像"交换'出发地端口'和'目的地端口',并切换'方向标志'"
      
      clib_bihash_add_del_40_8 (&am->fa_ip6_sessions_hash, &kv2, is_add);  // 添加到Hash表
                                         // 类比:就像"将'反向常客记录'添加到'常客数据库'"
    }
}

函数总结

  1. 交换IP地址:交换源IPv6地址和目的IPv6地址
  2. 复制会话ID:反向会话和正向会话使用相同的会话ID
  3. 交换端口和方向:根据协议类型(TCP/UDP或ICMP)交换端口和方向标志
  4. 添加到Hash表:将反向会话添加到Hash表

类比:就像"创建反向常客记录":

  1. 交换地址:交换"出发地"和"目的地"
  2. 复制档案:反向记录和正向记录指向同一个"常客档案"
  3. 交换端口:交换"出发地端口"和"目的地端口",并切换"方向标志"
  4. 加入数据库:将"反向常客记录"添加到"常客数据库"

9.10 会话处理:如何"处理已有会话的数据包"?

9.10.1 已有会话处理:process_established_session

当数据包匹配已有会话时,需要更新会话状态并返回动作:

c 复制代码
//180:226:src/plugins/acl/dataplane_node.c
always_inline u8                                // 返回类型:u8(动作,0=deny,非0=permit)
process_established_session (vlib_main_t * vm,   // 参数:vm(VPP主结构体)
			     acl_main_t * am,      // 参数:am(ACL插件主结构体)
			     u32 counter_node_index,  // 参数:counter_node_index(计数器节点索引)
			     int is_input,          // 参数:is_input(是否输入方向,1=输入,0=输出)
			     u64 now,               // 参数:now(当前时间戳)
			     fa_full_session_id_t f_sess_id,  // 参数:f_sess_id(会话ID)
			     u32 * sw_if_index,     // 参数:sw_if_index(接口索引数组)
			     fa_5tuple_t * fa_5tuple,  // 参数:fa_5tuple(数据包的5-tuple)
			     u32 pkt_len,           // 参数:pkt_len(数据包长度)
			     int node_trace_on,      // 参数:node_trace_on(是否启用Trace)
			     u32 * trace_bitmap)    // 参数:trace_bitmap(Trace位图输出)
{
  u8 action = 0;                                 // 动作(初始化为0,deny)
  fa_session_t *sess = get_session_ptr_no_check (am, f_sess_id.thread_index,  // 获取会话结构体指针
						 f_sess_id.session_index);      // 从pool中获取会话指针(不带边界检查)
                                         // get_session_ptr_no_check:从pool中获取会话指针(不带边界检查,性能优化)
                                         // 类比:就像"从'档案柜'中获取'常客档案夹'"

  // ========== 第一部分:跟踪会话状态 ==========
  
  int old_timeout_type = fa_session_get_timeout_type (am, sess);  // 获取旧的超时类型
                                         // fa_session_get_timeout_type:获取会话的超时类型(我们上面已经讲解过)
                                         // 类比:就像"获取'常客'的'旧类型'"
  
  action = acl_fa_track_session (am, is_input, sw_if_index[0], now,  // 跟踪会话状态
				 sess, &fa_5tuple[0], pkt_len);        // 更新最后活跃时间、TCP标志等
                                         // acl_fa_track_session:跟踪会话状态(我们上面已经讲解过)
                                         // 类比:就像"更新'常客'的'访问记录'"
  
  int new_timeout_type = fa_session_get_timeout_type (am, sess);  // 获取新的超时类型
                                         // 类比:就像"获取'常客'的'新类型'"

  // ========== 第二部分:处理超时类型变化 ==========
  
  /* Tracking might have changed the session timeout type, e.g. from transient to established */
  if (PREDICT_FALSE (old_timeout_type != new_timeout_type))  // 如果超时类型发生变化(PREDICT_FALSE表示不常见)
    {
      acl_fa_restart_timer_for_session (am, now, f_sess_id);  // 重启会话定时器
                                         // acl_fa_restart_timer_for_session:重启会话定时器(从旧链表移除,添加到新链表)
                                         // 为什么需要?超时类型变化时,需要将会话从旧链表移到新链表
                                         // 类比:就像"如果'常客类型'变化,需要从'旧列表'移到'新列表'"
      
      vlib_node_increment_counter (vm, counter_node_index,  // 增加计数器
				   ACL_FA_ERROR_ACL_RESTART_SESSION_TIMER, 1);  // 重启会话定时器计数器
                                         // vlib_node_increment_counter:增加节点计数器
                                         // 类比:就像"增加'重启定时器计数'"
      
      if (node_trace_on)                        // 如果启用Trace
	{
	  *trace_bitmap |=                        // 设置Trace位图标志
	    0x00010000 + ((0xff & old_timeout_type) << 8) +  // 旧超时类型(8-15位)
	    (0xff & new_timeout_type);            // 新超时类型(0-7位)
                                         // 类比:就像"记录'类型变化'到'Trace位图'"
	}
    }

  // ========== 第三部分:验证接口索引一致性 ==========
  
  /*
   * I estimate the likelihood to be very low - the VPP needs
   * to have >64K interfaces to start with and then on
   * exactly 64K indices apart needs to be exactly the same
   * 5-tuple... Anyway, since this probability is nonzero -
   * print an error and drop the unlucky packet.
   * If this shows up in real world, we would need to bump
   * the hash key length.
   */
  if (PREDICT_FALSE (sess->sw_if_index != sw_if_index[0]))  // 如果会话的接口索引与数据包的接口索引不一致(PREDICT_FALSE表示不常见)
    {
      clib_warning                              // 打印警告
	("BUG: session LSB16(sw_if_index)=%d and 5-tuple=%d collision!",  // 警告信息
	 sess->sw_if_index, sw_if_index[0]);      // 会话的接口索引、数据包的接口索引
                                         // 为什么会出现?Hash冲突(不同的5-tuple和接口索引组合产生相同的Hash值)
                                         // 为什么概率很低?需要>64K接口,且接口索引相差64K,且5-tuple完全相同
                                         // 类比:就像"如果'常客'的'通道编号'与'数据包'的'通道编号'不一致,报错"
      
      action = 0;                                // 设置动作为deny(拒绝)
                                         // 为什么拒绝?Hash冲突可能导致安全问题
                                         // 类比:就像"如果'通道编号'不一致,拒绝"
    }
  
  return action;                                 // 返回动作(3=permit,0=deny)
                                         // 类比:就像"返回'动作'"(放行或拒绝)
}

函数总结

  1. 获取会话:从pool中获取会话结构体指针
  2. 跟踪状态:更新会话的最后活跃时间和TCP标志位
  3. 处理类型变化:如果超时类型发生变化,重启会话定时器
  4. 验证一致性:验证会话的接口索引与数据包的接口索引一致(防止Hash冲突)
  5. 返回动作:返回permit(允许通过)

类比:就像"处理已有常客的数据包":

  1. 获取档案:从"档案柜"中获取"常客档案夹"
  2. 更新记录:更新"常客"的"访问记录"
  3. 处理类型变化:如果"常客类型"变化,从"旧列表"移到"新列表"
  4. 验证一致性:验证"常客"的"通道编号"与"数据包"的"通道编号"一致
  5. 返回结果:返回"放行"


9.11 会话删除:如何"注销常客信息"?

9.11.1 两阶段删除:acl_fa_two_stage_delete_session

会话删除采用两阶段删除机制,确保在删除过程中不会影响正在处理的数据包:

c 复制代码
//446:464:src/plugins/acl/session_inlines.h
always_inline int                                // 返回类型:int(1=成功,0=失败)
acl_fa_two_stage_delete_session (acl_main_t * am, u32 sw_if_index,  // 参数:am、sw_if_index(接口索引)
				 fa_full_session_id_t sess_id, u64 now)  // 参数:sess_id(会话ID)、now(当前时间戳)
{
  fa_session_t *sess =                          // 获取会话结构体指针
    get_session_ptr (am, sess_id.thread_index, sess_id.session_index);  // 从pool中获取会话指针
                                         // 类比:就像"从'档案柜'中获取'常客档案夹'"

  if (sess->deleted)                            // 如果会话已标记为删除
    {
      acl_fa_put_session (am, sw_if_index, sess_id);  // 直接释放会话内存
                                         // acl_fa_put_session:释放会话内存(我们下面详细讲解)
                                         // 为什么需要?如果已标记为删除,说明已经在Purgatory链表中,可以直接删除
                                         // 类比:就像"如果'常客'已标记为删除,直接释放'档案夹'"
    }
  else                                          // 如果会话未标记为删除
    {
      acl_fa_deactivate_session (am, sw_if_index, sess_id);  // 第一阶段:停用会话(从Hash表移除)
                                         // acl_fa_deactivate_session:停用会话(从Hash表移除,标记为删除)
                                         // 作用:从Hash表中移除会话,但保留会话结构体(用于正在处理的数据包)
                                         // 类比:就像"第一阶段:从'常客数据库'中移除'常客记录',但保留'档案夹'"
      
      acl_fa_conn_list_add_session (am, sess_id, now);  // 添加到Purgatory链表
                                         // acl_fa_conn_list_add_session:将会话添加到超时链表
                                         // 作用:将会话添加到Purgatory链表(待删除链表),等待第二阶段删除
                                         // 类比:就像"将'常客'添加到'待删除列表'"
    }
  
  return 1;                                      // 返回成功
                                         // 类比:就像"返回'成功'"
}

两阶段删除的原因

  1. 第一阶段(停用):从Hash表移除会话,标记为删除,但保留会话结构体

    • 原因:正在处理的数据包可能还在使用会话结构体
    • 类比:就像"从'常客数据库'中移除'常客记录',但保留'档案夹'(因为可能还在使用)"
  2. 第二阶段(删除):从Purgatory链表中删除会话,释放内存

    • 原因:等待一段时间(10微秒),确保没有数据包在使用会话结构体
    • 类比:就像"等待一段时间后,释放'档案夹'"

类比:就像"两阶段注销常客":

  1. 第一阶段:从"常客数据库"中移除"常客记录",但保留"档案夹"(因为可能还在使用)
  2. 第二阶段:等待一段时间后,释放"档案夹"

9.11.2 会话停用:acl_fa_deactivate_session

acl_fa_deactivate_session 用于停用会话(从Hash表移除,但保留会话结构体):

c 复制代码
//402:424:src/plugins/acl/session_inlines.h
always_inline void                              // 返回类型:void
acl_fa_deactivate_session (acl_main_t * am,    // 参数:am(ACL插件主结构体)
			   u32 sw_if_index,      // 参数:sw_if_index(接口索引,未使用)
			   fa_full_session_id_t sess_id)  // 参数:sess_id(会话ID)
{
  fa_session_t *sess =                          // 获取会话结构体指针
    get_session_ptr (am, sess_id.thread_index, sess_id.session_index);  // 从pool中获取会话指针
                                         // 类比:就像"从'档案柜'中获取'常客档案夹'"
  
  ASSERT (sess->thread_index == os_get_thread_index ());  // 断言会话的线程索引与当前线程索引一致
                                         // 为什么需要?确保在当前线程中操作会话
                                         // 类比:就像"确保'常客'在当前'安检通道'中操作"

  // ========== 第一部分:从Hash表移除会话 ==========
  
  if (sess->is_ip6)                             // 如果是IPv6
    {
      clib_bihash_add_del_40_8 (&am->fa_ip6_sessions_hash,  // 从IPv6会话Hash表移除
				&sess->info.kv_40_8, 0);        // 参数:Hash表指针、key、0(删除)
                                         // clib_bihash_add_del_40_8:添加或删除40_8 Hash表条目
                                         // 参数:0表示删除
                                         // 类比:就像"从'IPv6常客数据库'中移除'常客记录'"
      
      reverse_session_add_del_ip6 (am, &sess->info.kv_40_8, 0);  // 从Hash表移除反向会话
                                         // reverse_session_add_del_ip6:添加或删除IPv6反向会话
                                         // 参数:0表示删除
                                         // 类比:就像"从'常客数据库'中移除'反向常客记录'"
    }
  else                                          // 如果是IPv4
    {
      clib_bihash_add_del_16_8 (&am->fa_ip4_sessions_hash,  // 从IPv4会话Hash表移除
				&sess->info.kv_16_8, 0);        // 参数:Hash表指针、key、0(删除)
                                         // 类比:就像"从'IPv4常客数据库'中移除'常客记录'"
      
      reverse_session_add_del_ip4 (am, &sess->info.kv_16_8, 0);  // 从Hash表移除反向会话
                                         // 类比:就像"从'常客数据库'中移除'反向常客记录'"
    }

  // ========== 第二部分:标记会话为删除 ==========
  
  sess->deleted = 1;                            // 设置删除标志为1(已删除)
                                         // 类比:就像"标记'常客'为'已删除'"
  
  clib_atomic_fetch_add (&am->fa_session_total_deactivations, 1);  // 原子增加全局会话停用计数
                                         // clib_atomic_fetch_add:原子操作(线程安全)
                                         // fa_session_total_deactivations:全局会话停用计数
                                         // 类比:就像"原子增加'全局常客停用计数'"
}

函数总结

  1. 验证线程:确保会话属于当前线程
  2. 从Hash表移除:从IPv4或IPv6会话Hash表中移除会话(包括反向会话)
  3. 标记删除:设置会话的删除标志为1
  4. 更新统计:更新会话停用统计信息

类比:就像"停用常客":

  1. 验证通道:确保"常客"属于当前"安检通道"
  2. 移除记录:从"常客数据库"中移除"常客记录"(包括反向记录)
  3. 标记删除:标记"常客"为"已删除"
  4. 更新统计:更新"常客停用统计"

9.11.3 会话释放:acl_fa_put_session

acl_fa_put_session 用于释放会话内存(第二阶段删除):

c 复制代码
//427:443:src/plugins/acl/session_inlines.h
always_inline void                              // 返回类型:void
acl_fa_put_session (acl_main_t * am,           // 参数:am(ACL插件主结构体)
		    u32 sw_if_index,              // 参数:sw_if_index(接口索引)
		    fa_full_session_id_t sess_id)  // 参数:sess_id(会话ID)
{
  uword thread_index = os_get_thread_index ();  // 获取当前线程索引
  acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index];  // 获取当前线程的Per-worker数据
                                         // 类比:就像"获取当前'安检通道'的'数据'"

  pool_put_index (pw->fa_sessions_pool, sess_id.session_index);  // 将会话索引归还给pool
                                         // pool_put_index:将索引归还给pool(标记为可用)
                                         // 作用:释放会话占用的内存,标记为可重用
                                         // 类比:就像"将'常客档案夹'放回'档案柜',标记为可用"

  vec_validate (pw->fa_session_dels_by_sw_if_index, sw_if_index);  // 确保统计向量有足够的空间
                                         // fa_session_dels_by_sw_if_index:按接口索引统计会话删除次数
                                         // 类比:就像"确保'通道统计表'有足够的空间"
  
  pw->fa_session_dels_by_sw_if_index[sw_if_index]++;  // 增加接口的会话删除计数
                                         // 类比:就像"增加'通道'的'常客删除计数'"
  
  clib_atomic_fetch_add (&am->fa_session_total_dels, 1);  // 原子增加全局会话删除计数
                                         // clib_atomic_fetch_add:原子操作(线程安全)
                                         // fa_session_total_dels:全局会话删除计数
                                         // 类比:就像"原子增加'全局常客删除计数'"
}

函数总结

  1. 获取线程数据:获取当前线程的Per-worker数据
  2. 释放内存:将会话索引归还给pool(释放内存)
  3. 更新统计:更新会话删除统计信息

类比:就像"释放常客档案":

  1. 获取数据:获取当前"安检通道"的"数据"
  2. 释放档案:将"常客档案夹"放回"档案柜",标记为可用
  3. 更新统计:更新"常客删除统计"

9.12 会话清理:如何"清理过期常客"?

9.12.1 会话清理函数:acl_fa_check_idle_sessions

acl_fa_check_idle_sessions 用于检查并清理过期的会话:

c 复制代码
//164:319:src/plugins/acl/sess_mgmt_node.c
static int                                      // 返回类型:int(清理的会话数量)
acl_fa_check_idle_sessions (acl_main_t * am,    // 参数:am(ACL插件主结构体)
			    u16 thread_index,     // 参数:thread_index(线程索引)
			    u64 now)              // 参数:now(当前时间戳)
{
  acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index];  // 获取线程的Per-worker数据
                                         // 类比:就像"获取'安检通道'的'数据'"
  
  fa_full_session_id_t fsid;                    // 会话ID(临时变量)
  fsid.thread_index = thread_index;            // 设置会话ID的线程索引
  int total_expired = 0;                        // 总过期会话数量(返回值)

  // ========== 第一部分:处理会话变更请求 ==========
  
  /* let the other threads enqueue more requests while we process, if they like */
  aclp_swap_wip_and_pending_session_change_requests (am, thread_index);  // 交换WIP和Pending会话变更请求
                                         // aclp_swap_wip_and_pending_session_change_requests:交换WIP和Pending会话变更请求
                                         // 作用:允许其他线程在处理过程中继续入队请求(无锁设计)
                                         // 类比:就像"交换'处理中列表'和'待处理列表'"
  
  u64 *psr = NULL;                              // 会话变更请求指针(临时变量)
  vec_foreach (psr, pw->wip_session_change_requests)  // 遍历WIP会话变更请求
    {
      acl_fa_sess_req_t op = *psr >> 32;        // 提取操作类型(高32位)
                                         // acl_fa_sess_req_t:会话变更请求类型(如ACL_FA_REQ_SESS_RESCHEDULE)
                                         // >> 32:右移32位(提取高32位)
                                         // 类比:就像"提取'请求类型'"
      
      fsid.session_index = *psr & 0xffffffff;   // 提取会话索引(低32位)
                                         // & 0xffffffff:位与操作(提取低32位)
                                         // 类比:就像"提取'常客索引'"
      
      switch (op)                                // 根据操作类型处理
	{
	case ACL_FA_REQ_SESS_RESCHEDULE:         // 如果是重新调度请求
	  acl_fa_restart_timer_for_session (am, now, fsid);  // 重启会话定时器
                                         // acl_fa_restart_timer_for_session:重启会话定时器(从旧链表移除,添加到新链表)
                                         // 类比:就像"如果请求是'重新调度',重启'常客定时器'"
	  break;
	default:                                 // 如果是其他操作
	  /* do nothing */
	  break;
	}
    }
  
  if (pw->wip_session_change_requests)          // 如果WIP会话变更请求不为空
    vec_set_len (pw->wip_session_change_requests, 0);  // 清空WIP会话变更请求
                                         // vec_set_len:设置向量长度为0(清空向量)
                                         // 类比:就像"清空'处理中列表'"

  // ========== 第二部分:检查所有超时链表 ==========
  
  {
    u8 tt = 0;                                  // 超时类型(临时变量,用于遍历)
    int n_pending_swipes = 0;                   // 待清理会话数量(临时变量)
    
    for (tt = 0; tt < ACL_N_TIMEOUTS; tt++)     // 遍历所有超时类型
      {
	int n_expired = 0;                        // 当前超时类型的过期会话数量
	while (n_expired < am->fa_max_deleted_sessions_per_interval)  // 如果未达到最大删除数量
	  {
	    fsid.session_index = pw->fa_conn_list_head[tt];  // 获取链表头节点索引
                                         // fa_conn_list_head[tt]:超时链表的头节点索引
                                         // 类比:就像"获取'待检查列表'的'列表头'"
	    
	    if (!acl_fa_conn_time_to_check        // 如果未到检查时间
		(am, pw, now, thread_index, pw->fa_conn_list_head[tt]))  // 检查是否到检查时间
	      {
		break;                              // 跳出循环(未到检查时间,不继续检查)
                                         // acl_fa_conn_time_to_check:检查是否到检查时间
                                         // 作用:如果链表的头节点未到过期时间,不继续检查(优化性能)
                                         // 类比:就像"如果'列表头'未到'过期时间',不继续检查"
	      }
	    
	    if (am->trace_sessions > 3)            // 如果Trace级别足够高
	      {
		elog_acl_maybe_trace_X3 (am,        // 打印Trace信息
					 "acl_fa_check_idle_sessions: expire session %d in list %d on thread %d",
					 "i4i4i4", (u32) fsid.session_index,
					 (u32) tt, (u32) thread_index);
                                         // elog_acl_maybe_trace_X3:打印Trace信息(3个整数参数)
                                         // 类比:就像"记录'过期常客'的Trace信息"
	      }
	    
	    vec_add1 (pw->expired, fsid.session_index);  // 添加到过期会话向量
                                         // vec_add1:向向量添加一个元素
                                         // expired:过期会话向量(用于后续处理)
                                         // 类比:就像"将'过期常客'添加到'过期列表'"
	    
	    n_expired++;                            // 增加过期会话数量
	    acl_fa_conn_list_delete_session (am, fsid, now);  // 从链表中删除会话
                                         // acl_fa_conn_list_delete_session:从链表中删除会话(我们下面详细讲解)
                                         // 类比:就像"从'待检查列表'中删除'过期常客'"
	  }
      }
    
    // 统计待清理会话数量
    for (tt = 0; tt < ACL_N_TIMEOUTS; tt++)     // 遍历所有超时类型
      {
	u32 session_index = pw->fa_conn_list_head[tt];  // 获取链表头节点索引
	if (session_index == FA_SESSION_BOGUS_INDEX)  // 如果链表为空
	  break;                                    // 跳出循环
	fa_session_t *sess =                        // 获取会话结构体指针
	  get_session_ptr (am, thread_index, session_index);
	n_pending_swipes += sess->link_enqueue_time <= pw->swipe_end_time;  // 统计待清理会话数量
                                         // swipe_end_time:清理结束时间(用于批量清理)
                                         // 类比:就像"统计'待清理常客'数量"
      }
    
    if (n_pending_swipes == 0)                  // 如果没有待清理会话
      {
	pw->swipe_end_time = 0;                    // 重置清理结束时间
                                         // 类比:就像"如果没有'待清理常客',重置'清理结束时间'"
      }
  }

  // ========== 第三部分:处理过期会话 ==========
  
  u32 *psid = NULL;                             // 过期会话索引指针(临时变量)
  vec_foreach (psid, pw->expired)               // 遍历过期会话向量
    {
      fsid.session_index = *psid;               // 设置会话索引
      
      if (!pool_is_free_index (pw->fa_sessions_pool, fsid.session_index))  // 如果会话未被释放
	{
	  fa_session_t *sess =                    // 获取会话结构体指针
	    get_session_ptr (am, thread_index, fsid.session_index);
	  u32 sw_if_index = sess->sw_if_index;    // 获取接口索引
	  
	  u64 sess_timeout_time =                 // 计算会话超时时间
	    sess->last_active_time + fa_session_get_timeout (am, sess);  // 最后活跃时间 + 超时时间
                                         // fa_session_get_timeout:获取会话的超时时间(我们上面已经讲解过)
                                         // 类比:就像"计算'常客'的'过期时间'"
	  
	  int timeout_passed = (now >= sess_timeout_time);  // 检查是否已过期
                                         // 类比:就像"检查'常客'是否已过期"
	  
	  int clearing_interface =                // 检查接口是否正在清理
	    clib_bitmap_get (pw->pending_clear_sw_if_index_bitmap, sw_if_index);  // 从位图中获取接口清理标志
                                         // pending_clear_sw_if_index_bitmap:待清理接口位图
                                         // 类比:就像"检查'通道'是否正在清理"
	  
	  if (am->trace_sessions > 3)              // 如果Trace级别足够高
	    {
	      elog_acl_maybe_trace_X2 (am,         // 打印Trace信息
				     "acl_fa_check_idle_sessions: now %lu sess_timeout_time %lu",
				     "i8i8", now, sess_timeout_time);
	      elog_acl_maybe_trace_X4 (am,
				     "acl_fa_check_idle_sessions: session %d sw_if_index %d timeout_passed %d clearing_interface %d",
				     "i4i4i4i4", (u32) fsid.session_index,
				     (u32) sess->sw_if_index,
				     (u32) timeout_passed,
				     (u32) clearing_interface);
                                         // 类比:就像"记录'过期常客'的详细信息"
	    }
	  
	  if (timeout_passed || clearing_interface)  // 如果已过期或接口正在清理
	    {
	      if (acl_fa_two_stage_delete_session (am, sw_if_index, fsid, now))  // 两阶段删除会话
		{
		  if (am->trace_sessions > 3)      // 如果Trace级别足够高
		    {
		      elog_acl_maybe_trace_X2 (am,
					     "acl_fa_check_idle_sessions: deleted session %d sw_if_index %d",
					     "i4i4", (u32) fsid.session_index,
					     (u32) sess->sw_if_index);
                                         // 类比:就像"记录'删除常客'的信息"
		    }
		  /* the session has been put */
		  pw->cnt_deleted_sessions++;      // 增加删除会话计数
                                         // cnt_deleted_sessions:删除会话计数
                                         // 类比:就像"增加'删除常客计数'"
		}
	      else
		{
		  /* the connection marked as deleted and put to purgatory */
		  if (am->trace_sessions > 3)      // 如果Trace级别足够高
		    {
		      elog_acl_maybe_trace_X2 (am,
					     "acl_fa_check_idle_sessions: session %d sw_if_index %d marked as deleted, put to purgatory",
					     "i4i4", (u32) fsid.session_index,
					     (u32) sess->sw_if_index);
                                         // 类比:就像"记录'标记为删除常客'的信息"
		    }
		}
	    }
	  else
	    {
	      if (am->trace_sessions > 3)          // 如果Trace级别足够高
		{
		  elog_acl_maybe_trace_X2 (am,
					 "acl_fa_check_idle_sessions: restart timer for session %d sw_if_index %d",
					 "i4i4", (u32) fsid.session_index,
					 (u32) sess->sw_if_index);
                                         // 类比:就像"记录'重启定时器常客'的信息"
		}
	      /* There was activity on the session, so the idle timeout
		 has not passed. Enqueue for another time period. */
	      
	      acl_fa_conn_list_add_session (am, fsid, now);  // 重新添加到链表(未过期,继续等待)
                                         // acl_fa_conn_list_add_session:将会话添加到超时链表(我们上面已经讲解过)
                                         // 为什么需要?如果会话未过期,需要重新添加到链表,等待下次检查
                                         // 类比:就像"如果'常客'未过期,重新添加到'待检查列表'"
	    }
	}
    }
  
  if (pw->expired)                               // 如果过期会话向量不为空
    vec_set_len (pw->expired, 0);                // 清空过期会话向量
                                         // 类比:就像"清空'过期列表'"

  if (am->trace_sessions > 3)                    // 如果Trace级别足够高
    {
      elog_acl_maybe_trace_X1 (am,
			   "acl_fa_check_idle_sessions: done, total sessions expired: %d",
			   "i4", total_expired);
                                         // 类比:就像"记录'清理完成'的信息"
    }

  return total_expired;                          // 返回总过期会话数量
                                         // 类比:就像"返回'总过期常客数量'"
}

函数总结

这个函数实现了会话清理的核心逻辑

  1. 处理变更请求:处理会话变更请求(如重新调度)
  2. 检查超时链表:遍历所有超时链表,检查是否有过期会话
  3. 处理过期会话:对于过期会话,执行两阶段删除;对于未过期会话,重新添加到链表

清理策略

  • 批量清理 :每次最多清理 fa_max_deleted_sessions_per_interval 个会话(避免一次性清理太多)
  • 时间检查:如果链表头节点未到过期时间,不继续检查(优化性能)
  • 两阶段删除:使用两阶段删除机制,确保安全

类比:就像"清理过期常客":

  1. 处理请求:处理"常客变更请求"(如重新调度)
  2. 检查列表:遍历所有"待检查列表",检查是否有"过期常客"
  3. 处理过期:对于"过期常客",执行"两阶段删除";对于"未过期常客",重新添加到"列表"

9.13 会话清理进程:如何"定期清理过期常客"?

9.13.1 清理进程:acl_fa_session_cleaner_process

VPP使用一个独立的进程定期清理过期的会话。让我们看看清理进程的实现:

c 复制代码
//571:780:src/plugins/acl/sess_mgmt_node.c
acl_fa_session_cleaner_process (vlib_main_t * vm, vlib_node_runtime_t * rt)  // 清理进程函数
{
  acl_main_t *am = &acl_main;                   // 获取ACL插件主结构体
  u64 now = clib_cpu_time_now ();               // 获取当前时间戳
                                         // clib_cpu_time_now:获取当前CPU时间戳
                                         // 类比:就像"获取当前时间"

  am->fa_cleaner_node_index = acl_fa_session_cleaner_process_node.index;  // 保存清理进程节点索引
                                         // fa_cleaner_node_index:清理进程节点索引(用于发送事件)
                                         // 类比:就像"保存'清理进程编号'"

  // ========== 第一部分:处理清理事件 ==========
  
  while (1)                                     // 无限循环(进程持续运行)
    {
      u64 wait_time = ~0ULL;                    // 等待时间(初始化为最大值)
                                         // wait_time:下次唤醒时间(单位:时钟周期)
                                         // ~0ULL:64位全1(最大值,表示无限等待)
                                         // 类比:就像"初始化'等待时间'为'无限等待'"
      
      uword event_type;                         // 事件类型(临时变量)
      u64 *event_data = 0;                      // 事件数据(临时变量)
      
      // 处理所有待处理的事件
      while (vlib_process_get_events (vm, &event_data))  // 获取待处理事件
	{
	  event_type = event_data[0];            // 提取事件类型
                                         // vlib_process_get_events:获取待处理事件
                                         // 类比:就像"获取'待处理事件'"
	  
	  switch (event_type)                    // 根据事件类型处理
	    {
	    case ACL_FA_CLEANER_DELETE_BY_SW_IF_INDEX:  // 如果是按接口删除事件
	      {
		u32 sw_if_index = event_data[1];      // 提取接口索引
                                         // ACL_FA_CLEANER_DELETE_BY_SW_IF_INDEX:按接口删除事件类型
                                         // 作用:删除指定接口的所有会话(当接口删除或ACL变化时)
                                         // 类比:就像"如果事件是'按通道删除',提取'通道编号'"
		
		// 遍历所有Worker线程,删除指定接口的会话
		// ... (删除逻辑) ...
		
		break;
	      }
	    
	    default:                               // 如果是未知事件类型
	      am->fa_cleaner_cnt_unknown_event++;  // 增加未知事件计数
                                         // 类比:就像"如果事件类型未知,增加'未知事件计数'"
	      break;
	    }
	  
	  vec_free (event_data);                  // 释放事件数据
                                         // 类比:就像"释放'事件数据'"
	}

      // ========== 第二部分:计算下次唤醒时间 ==========
      
      uword thread_index;                       // 线程索引(临时变量)
      u64 next_expire_time = ~0ULL;             // 下次过期时间(初始化为最大值)
      
      // 遍历所有Worker线程,计算最早的过期时间
      for (thread_index = 0; thread_index < vec_len (am->per_worker_data); thread_index++)  // 遍历所有Worker线程
	{
	  acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index];  // 获取线程的Per-worker数据
	  
	  u8 tt = 0;                              // 超时类型(临时变量)
	  for (tt = 0; tt < ACL_N_TIMEOUTS; tt++)  // 遍历所有超时类型
	    {
	      if (FA_SESSION_BOGUS_INDEX != pw->fa_conn_list_head[tt])  // 如果链表不为空
		{
		  u64 head_expiry_time = pw->fa_conn_list_head_expiry_time[tt];  // 获取链表头节点的过期时间
                                         // fa_conn_list_head_expiry_time[tt]:链表头节点的过期时间
                                         // 类比:就像"获取'列表头'的'过期时间'"
		  
		  if (head_expiry_time < next_expire_time)  // 如果过期时间更早
		    {
		      next_expire_time = head_expiry_time;  // 更新下次过期时间
                                         // 类比:就像"如果'过期时间'更早,更新'下次过期时间'"
		    }
		}
	    }
	}
      
      // ========== 第三部分:唤醒Worker线程进行清理 ==========
      
      if (next_expire_time != ~0ULL)            // 如果有会话需要清理
	{
	  u64 wait_time_sec = (next_expire_time - now) / am->vlib_main->clib_time.clocks_per_second;  // 计算等待时间(秒)
                                         // 类比:就像"计算'等待时间'(秒)"
	  
	  // 唤醒所有Worker线程进行清理
	  uword thread_index;
	  for (thread_index = 0; thread_index < vec_len (am->per_worker_data); thread_index++)  // 遍历所有Worker线程
	    {
	      vlib_node_set_state (vm, am->per_worker_data[thread_index].fa_worker_cleaner_node_index,  // 设置Worker清理节点状态
				  VLIB_NODE_STATE_INTERRUPT);  // 为INTERRUPT(可中断)
                                         // vlib_node_set_state:设置节点状态
                                         // VLIB_NODE_STATE_INTERRUPT:可中断状态(可以处理事件)
                                         // 类比:就像"唤醒'Worker清理节点'"
	    }
	  
	  wait_time = next_expire_time - now;      // 计算等待时间
                                         // 类比:就像"计算'等待时间'"
	}
      else
	{
	  wait_time = 1.0 * am->vlib_main->clib_time.clocks_per_second;  // 如果没有会话,等待1秒
                                         // 类比:就像"如果没有'会话',等待1秒"
	}

      // ========== 第四部分:等待并继续循环 ==========
      
      vlib_process_wait_for_event_or_clock (vm, wait_time);  // 等待事件或时钟
                                         // vlib_process_wait_for_event_or_clock:等待事件或时钟
                                         // 作用:进程休眠,等待事件或超时
                                         // 类比:就像"等待'事件'或'超时'"
    }
  
  return 0;                                     // 返回0(进程不会退出)
                                         // 类比:就像"返回0"(进程持续运行)
}

函数总结

这个函数实现了会话清理进程的核心逻辑

  1. 处理事件:处理清理事件(如按接口删除)
  2. 计算唤醒时间:遍历所有Worker线程,计算最早的过期时间
  3. 唤醒Worker线程:唤醒所有Worker线程进行清理
  4. 等待:等待事件或超时,然后继续循环

清理进程的工作流程

  1. 主进程:定期检查所有Worker线程的会话链表,计算最早的过期时间
  2. Worker线程 :被唤醒后,执行 acl_fa_check_idle_sessions 清理过期会话
  3. 循环:主进程等待一段时间后,再次检查并唤醒Worker线程

类比:就像"定期清理过期常客":

  1. 主管理员:定期检查所有"安检通道"的"待检查列表",计算最早的"过期时间"
  2. 安检通道:被唤醒后,执行"清理过期常客"操作
  3. 循环:主管理员等待一段时间后,再次检查并唤醒"安检通道"

9.14 Policy Epoch机制:如何"检测过期会话"?

9.14.1 过期会话检测:stale_session_deleted

当启用会话重分类时,需要检测"过期会话"(Policy Epoch不匹配的会话):

c 复制代码
//105:132:src/plugins/acl/dataplane_node.c
always_inline int                                // 返回类型:int(1=已删除,0=未删除)
stale_session_deleted (acl_main_t * am,         // 参数:am(ACL插件主结构体)
		       int is_input,               // 参数:is_input(是否输入方向,1=输入,0=输出)
		       acl_fa_per_worker_data_t * pw,  // 参数:pw(Per-worker数据)
		       u64 now,                    // 参数:now(当前时间戳)
		       u32 sw_if_index0,           // 参数:sw_if_index0(接口索引)
		       fa_full_session_id_t f_sess_id)  // 参数:f_sess_id(会话ID)
{
  u16 current_policy_epoch =                     // 获取当前Policy Epoch
    get_current_policy_epoch (am, is_input, sw_if_index0);  // 从接口获取当前Policy Epoch
                                         // get_current_policy_epoch:获取当前Policy Epoch(我们第6章已经讲解过)
                                         // 类比:就像"获取'通道'的'当前规则版本号'"

  /* if the MSB of policy epoch matches but not the LSB means it is a stale session */
  if ((0 ==                                     // 如果方向标志匹配(MSB匹配)
       ((current_policy_epoch ^                 // 当前Policy Epoch异或会话Policy Epoch
	 f_sess_id.intf_policy_epoch) &           // 提取方向标志位
	FA_POLICY_EPOCH_IS_INPUT))                // FA_POLICY_EPOCH_IS_INPUT:方向标志位(0x8000)
      && (current_policy_epoch != f_sess_id.intf_policy_epoch))  // 且Policy Epoch不匹配(LSB不匹配)
    {
      /* delete session and increment the counter */
      vec_validate (pw->fa_session_epoch_change_by_sw_if_index, sw_if_index0);  // 确保统计向量有足够的空间
                                         // fa_session_epoch_change_by_sw_if_index:按接口索引统计Policy Epoch变化次数
                                         // 类比:就像"确保'通道统计表'有足够的空间"
      
      vec_elt (pw->fa_session_epoch_change_by_sw_if_index, sw_if_index0)++;  // 增加接口的Policy Epoch变化计数
                                         // 类比:就像"增加'通道'的'规则版本号变化计数'"
      
      if (acl_fa_conn_list_delete_session (am, f_sess_id, now))  // 从链表中删除会话
	{
	  /* delete the session only if we were able to unlink it */
	  acl_fa_two_stage_delete_session (am, sw_if_index0, f_sess_id, now);  // 两阶段删除会话
                                         // acl_fa_two_stage_delete_session:两阶段删除会话(我们上面已经讲解过)
                                         // 为什么需要?如果会话在链表中,先删除;然后两阶段删除
                                         // 类比:就像"如果'常客'在'列表'中,先删除;然后两阶段删除"
	}
      
      return 1;                                  // 返回1(已删除)
                                         // 类比:就像"返回'已删除'"
    }
  else
    return 0;                                    // 返回0(未删除)
                                         // 类比:就像"返回'未删除'"
}

过期会话检测逻辑

  1. 获取当前Policy Epoch:从接口获取当前Policy Epoch
  2. 比较Policy Epoch
    • 方向标志匹配(MSB匹配):确保是同一方向(输入或输出)
    • 版本号不匹配(LSB不匹配):说明规则已变化
  3. 删除会话:如果检测到过期会话,执行两阶段删除

为什么需要Policy Epoch?

  • 问题:当ACL规则变化时,现有会话可能不再有效(比如规则从permit变成deny)
  • 解决方案:使用Policy Epoch标记规则变化,会话创建时记录Policy Epoch,匹配时检查是否一致
  • 类比 :就像"规则版本号":
    • 每次换规则,版本号+1
    • 常客注册时,记录"当前版本号"
    • 常客再次访问时,检查"当前版本号"是否与"记录版本号"一致
    • 如果不一致,说明规则变了,需要重新验证

9.15 会话链表删除:如何"从待检查列表中删除常客"?

9.15.1 链表删除函数:acl_fa_conn_list_delete_session

acl_fa_conn_list_delete_session 用于从超时链表中删除会话:

c 复制代码
//192:245:src/plugins/acl/session_inlines.h
static int                                      // 返回类型:int(1=成功,0=失败)
acl_fa_conn_list_delete_session (acl_main_t * am,  // 参数:am(ACL插件主结构体)
				 fa_full_session_id_t sess_id,  // 参数:sess_id(会话ID)
				 u64 now)              // 参数:now(当前时间戳)
{
  uword thread_index = os_get_thread_index ();  // 获取当前线程索引
  acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index];  // 获取当前线程的Per-worker数据
                                         // 类比:就像"获取当前'安检通道'的'数据'"
  
  if (thread_index != sess_id.thread_index)     // 如果当前线程索引与会话ID的线程索引不一致
    {
      /* If another thread attempts to delete the session, fail it. */
      return 0;                                 // 返回失败
                                         // 为什么需要?确保会话删除操作在正确的线程中执行(线程安全)
                                         // 类比:就像"如果'常客'不属于当前'安检通道',返回失败"
    }
  
  fa_session_t *sess =                          // 获取会话结构体指针
    get_session_ptr (am, thread_index, sess_id.session_index);  // 从pool中获取会话指针
                                         // 类比:就像"从'档案柜'中获取'常客档案夹'"

  u8 list_id = sess->link_list_id;             // 获取链表ID
                                         // 类比:就像"获取'常客'的'待检查列表类型'"

  // ========== 第一部分:更新前一个节点的下一个节点 ==========
  
  if (FA_SESSION_BOGUS_INDEX != sess->link_prev_idx)  // 如果前一个节点存在
    {
      fa_session_t *prev_sess =                  // 获取前一个会话的指针
	get_session_ptr (am, thread_index, sess->link_prev_idx);  // 从pool中获取前一个会话指针
                                         // 类比:就像"获取'前一个常客'的'档案夹'"
      
      prev_sess->link_next_idx = sess->link_next_idx;  // 设置前一个会话的下一个节点索引为当前会话的下一个节点
                                         // 类比:就像"设置'前一个常客'的'下一个常客'为当前'常客'的'下一个常客'"
    }
  else                                          // 如果前一个节点不存在(当前节点是链表头)
    {
      pw->fa_conn_list_head[list_id] = sess->link_next_idx;  // 更新链表头节点索引为当前会话的下一个节点
                                         // 类比:就像"如果当前'常客'是'列表头',更新'列表头'为'下一个常客'"
    }

  // ========== 第二部分:更新下一个节点的前一个节点 ==========
  
  if (FA_SESSION_BOGUS_INDEX != sess->link_next_idx)  // 如果下一个节点存在
    {
      fa_session_t *next_sess =                  // 获取下一个会话的指针
	get_session_ptr (am, thread_index, sess->link_next_idx);  // 从pool中获取下一个会话指针
                                         // 类比:就像"获取'下一个常客'的'档案夹'"
      
      next_sess->link_prev_idx = sess->link_prev_idx;  // 设置下一个会话的前一个节点索引为当前会话的前一个节点
                                         // 类比:就像"设置'下一个常客'的'前一个常客'为当前'常客'的'前一个常客'"
      
      // 更新链表头节点的过期时间(如果下一个节点是链表头)
      u64 next_expiry_time = now + fa_session_get_timeout (am, next_sess);  // 计算下一个节点的过期时间
                                         // fa_session_get_timeout:获取会话的超时时间(我们上面已经讲解过)
                                         // 类比:就像"计算'下一个常客'的'过期时间'"
      
      if (FA_SESSION_BOGUS_INDEX == sess->link_prev_idx)  // 如果当前节点是链表头(前一个节点不存在)
	{
	  pw->fa_conn_list_head_expiry_time[list_id] = next_expiry_time;  // 更新链表头节点的过期时间
                                         // fa_conn_list_head_expiry_time[list_id]:链表头节点的过期时间
                                         // 类比:就像"如果当前'常客'是'列表头',更新'列表头'的'过期时间'"
	}
    }
  else                                          // 如果下一个节点不存在(当前节点是链表尾)
    {
      pw->fa_conn_list_tail[list_id] = sess->link_prev_idx;  // 更新链表尾节点索引为当前会话的前一个节点
                                         // 类比:就像"如果当前'常客'是'列表尾',更新'列表尾'为'前一个常客'"
    }

  // ========== 第三部分:重置会话的链表信息 ==========
  
  sess->link_prev_idx = FA_SESSION_BOGUS_INDEX; // 重置链表前一个节点索引为无效
  sess->link_next_idx = FA_SESSION_BOGUS_INDEX; // 重置链表下一个节点索引为无效
                                         // 类比:就像"重置'常客'的'前一个常客'和'下一个常客'为无效"

  return 1;                                      // 返回成功
                                         // 类比:就像"返回'成功'"
}

函数总结

这个函数实现了双向链表的节点删除

  1. 验证线程:确保会话属于当前线程
  2. 更新前一个节点:更新前一个节点的下一个节点索引
  3. 更新下一个节点:更新下一个节点的前一个节点索引,并更新链表头节点的过期时间(如果需要)
  4. 重置会话信息:重置会话的链表信息

类比:就像"从待检查列表中删除常客":

  1. 验证通道:确保"常客"属于当前"安检通道"
  2. 更新前一个:更新"前一个常客"的"下一个常客"
  3. 更新下一个:更新"下一个常客"的"前一个常客",并更新"列表头"的"过期时间"(如果需要)
  4. 重置信息:重置"常客"的"前一个常客"和"下一个常客"

9.16 会话创建条件:如何"判断是否可以创建新会话"?

9.16.1 会话创建检查:acl_fa_can_add_session

在创建新会话之前,需要检查是否可以创建(是否超过最大会话数):

c 复制代码
//465:475:src/plugins/acl/session_inlines.h
always_inline int                                // 返回类型:int(1=可以,0=不可以)
acl_fa_can_add_session (acl_main_t * am,        // 参数:am(ACL插件主结构体)
			int is_input,              // 参数:is_input(是否输入方向,1=输入,0=输出)
			u32 sw_if_index)           // 参数:sw_if_index(接口索引,未使用)
{
  u64 curr_sess_count;                          // 当前会话数量(临时变量)
  curr_sess_count = am->fa_session_total_adds - am->fa_session_total_dels;  // 计算当前会话数量
                                         // fa_session_total_adds:全局会话添加计数
                                         // fa_session_total_dels:全局会话删除计数
                                         // 当前会话数量 = 添加计数 - 删除计数
                                         // 类比:就像"计算'当前常客数量' = '添加计数' - '删除计数'"
  
  return (curr_sess_count < am->fa_conn_table_max_entries);  // 检查是否小于最大会话数
                                         // fa_conn_table_max_entries:最大会话数(在初始化时配置)
                                         // 类比:就像"检查'当前常客数量'是否小于'最大常客数'"
}

函数总结

  1. 计算当前会话数:当前会话数 = 添加计数 - 删除计数
  2. 检查限制:检查当前会话数是否小于最大会话数

类比:就像"检查是否可以添加新常客":

  1. 计算数量:计算"当前常客数量" = "添加计数" - "删除计数"
  2. 检查限制:检查"当前常客数量"是否小于"最大常客数"

9.16.2 会话回收:acl_fa_try_recycle_session

如果当前会话数已达到上限,需要尝试回收一些会话(删除最老的会话):

c 复制代码
//475:510:src/plugins/acl/session_inlines.h
always_inline int                                // 返回类型:int(1=成功回收,0=未回收)
acl_fa_try_recycle_session (acl_main_t * am,    // 参数:am(ACL插件主结构体)
			    int is_input,          // 参数:is_input(是否输入方向,1=输入,0=输出)
			    u16 thread_index,      // 参数:thread_index(线程索引)
			    u32 sw_if_index,       // 参数:sw_if_index(接口索引)
			    u64 now)               // 参数:now(当前时间戳)
{
  acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index];  // 获取线程的Per-worker数据
                                         // 类比:就像"获取'安检通道'的'数据'"
  
  fa_full_session_id_t sess_id;                 // 会话ID(临时变量)
  sess_id.thread_index = thread_index;         // 设置会话ID的线程索引

  // ========== 第一部分:尝试从Purgatory链表中回收 ==========
  
  sess_id.session_index = pw->fa_conn_list_head[ACL_TIMEOUT_PURGATORY];  // 获取Purgatory链表头节点索引
                                         // ACL_TIMEOUT_PURGATORY:Purgatory链表ID(待删除会话)
                                         // 类比:就像"获取'待删除列表'的'列表头'"
  
  while ((FA_SESSION_BOGUS_INDEX != sess_id.session_index)  // 如果链表不为空
	 && (pw->fa_conn_list_head[ACL_TIMEOUT_PURGATORY] != FA_SESSION_BOGUS_INDEX))  // 且链表头节点有效
    {
      fa_session_t *sess =                      // 获取会话结构体指针
	get_session_ptr (am, thread_index, sess_id.session_index);  // 从pool中获取会话指针
                                         // 类比:就像"获取'待删除常客'的'档案夹'"
      
      if (sess->link_enqueue_time + fa_session_get_timeout (am, sess) < now)  // 如果会话已过期
	{
	  acl_fa_conn_list_delete_session (am, sess_id, now);  // 从链表中删除会话
	  acl_fa_put_session (am, sw_if_index, sess_id);  // 释放会话内存
                                         // acl_fa_put_session:释放会话内存(我们上面已经讲解过)
                                         // 类比:就像"如果'待删除常客'已过期,删除并释放'档案夹'"
	}
      
      sess_id.session_index = pw->fa_conn_list_head[ACL_TIMEOUT_PURGATORY];  // 获取下一个节点索引
                                         // 类比:就像"获取'下一个待删除常客'"
    }

  // ========== 第二部分:尝试从TCP Transient链表中回收 ==========
  
  sess_id.session_index = pw->fa_conn_list_head[ACL_TIMEOUT_TCP_TRANSIENT];  // 获取TCP Transient链表头节点索引
                                         // ACL_TIMEOUT_TCP_TRANSIENT:TCP Transient链表ID(未建立连接的TCP会话)
                                         // 类比:就像"获取'TCP未建立连接列表'的'列表头'"
  
  if (FA_SESSION_BOGUS_INDEX != sess_id.session_index)  // 如果链表不为空
    {
      acl_fa_conn_list_delete_session (am, sess_id, now);  // 从链表中删除会话
      acl_fa_deactivate_session (am, sw_if_index, sess_id);  // 停用会话(从Hash表移除)
      acl_fa_conn_list_add_session (am, sess_id, now);  // 添加到Purgatory链表
                                         // 为什么需要?TCP Transient会话优先级较低,可以回收
                                         // 类比:就像"如果'TCP未建立连接常客'存在,删除并添加到'待删除列表'"
    }
  
  return 1;                                      // 返回成功
                                         // 类比:就像"返回'成功'"
}

函数总结

  1. 回收Purgatory会话:尝试从Purgatory链表中回收已过期的会话
  2. 回收TCP Transient会话:如果Purgatory链表为空,尝试回收TCP Transient会话(优先级较低)

回收策略

  • 优先级1:Purgatory会话(已标记为删除,可以立即回收)
  • 优先级2:TCP Transient会话(未建立连接的TCP会话,优先级较低)

类比:就像"回收常客档案":

  1. 回收待删除:尝试回收"待删除常客"(已标记为删除,可以立即回收)
  2. 回收未建立连接:如果"待删除常客"为空,尝试回收"未建立连接常客"(优先级较低)

9.17 完整流程总结:从数据包到会话管理完成

让我们用一个完整的例子来总结整个流程:

场景:一个IPv4 TCP数据包从接口0进入,需要创建新会话

步骤1:数据包到达节点

复制代码
数据包 → acl-plugin-in-ip4-l2 节点
  - 触发 acl_in_l2_ip4_node 函数
  - 调用 acl_fa_node_fn

步骤2:准备数据

复制代码
acl_fa_node_common_prepare_fn:
  - 提取接口索引(sw_if_index = 0)
  - 提取5-tuple(源IP、目的IP、协议、源端口、目的端口)
  - 计算会话Hash值

步骤3:查找会话

复制代码
acl_fa_find_session_with_hash:
  - 使用Hash值在IPv4会话Hash表中查找
  - 如果未找到,继续ACL匹配

步骤4:ACL匹配

复制代码
acl_plugin_match_5tuple_inline:
  - 匹配ACL规则
  - 如果匹配且action=2(permit+reflect),需要创建会话

步骤5:检查是否可以创建会话

复制代码
acl_fa_can_add_session:
  - 检查当前会话数是否小于最大会话数
  - 如果已达到上限,尝试回收会话

步骤6:创建会话

复制代码
acl_fa_add_session:
  - 分配会话内存
  - 填充会话信息(5-tuple、接口索引、Policy Epoch等)
  - 添加到Hash表(包括反向会话)
  - 添加到超时链表

步骤7:跟踪会话状态

复制代码
process_established_session:
  - 更新最后活跃时间
  - 更新TCP标志位
  - 如果超时类型变化,重启定时器

步骤8:返回结果

复制代码
返回动作(permit)
  - 数据包继续转发

后续处理

  • 会话清理进程:定期检查过期会话,执行两阶段删除
  • Policy Epoch检查:如果启用会话重分类,检查会话是否过期

类比:就像完整的"新常客登记和管理流程":

  1. 旅客到达(数据包到达节点)
  2. 准备检查(提取5-tuple、计算Hash)
  3. 查找常客(查找会话)
  4. 执行安检(ACL匹配)
  5. 检查限制(检查是否可以添加新常客)
  6. 登记常客(创建会话)
  7. 更新记录(跟踪会话状态)
  8. 返回结果(放行)
  9. 定期清理(清理过期常客)

9.18 本章小结

通过这一章的详细讲解,我们了解了:

  1. Flow-aware ACL的概念:什么是有状态ACL,为什么需要它
  2. 会话结构体:如何存储会话信息(5-tuple、状态信息、链表信息等)
  3. 会话创建acl_fa_add_session 的完整流程(逐行注释)
  4. 会话查找:如何使用Hash表快速查找会话
  5. 会话跟踪:如何更新会话状态(最后活跃时间、TCP标志等)
  6. 会话超时:如何判断会话的超时类型和超时时间
  7. 会话链表管理:如何管理超时链表(添加、删除)
  8. 会话删除:两阶段删除机制(停用、释放)
  9. 会话清理:如何定期清理过期会话
  10. Policy Epoch机制:如何检测过期会话
  11. 反向会话:如何匹配返回流量

核心要点

  • Flow-aware ACL通过记录会话状态,实现快速放行已建立的连接
  • 两阶段删除机制确保在删除过程中不会影响正在处理的数据包
  • 超时链表用于高效管理会话超时(按超时类型分类)
  • Policy Epoch机制用于检测过期会话(规则变化时)
  • 反向会话用于匹配返回流量(交换源和目的)

下一步:在下一章,我们会看到Hash匹配引擎的详细实现,包括TupleMerge算法、Hash表构建等。


第10章:Hash匹配引擎------如何"用字典快速查找规则"?

生活类比:想象一下,你是一个图书管理员,需要快速找到某本书。

  • 线性查找(Linear Matching):就像"一本一本地翻书"(从第一本开始,逐本查找,直到找到为止)
  • Hash匹配(Hash Matching):就像"使用字典"(根据书名计算索引,直接定位到书架位置)

当规则数量很少时,线性查找可能更快(因为不需要计算Hash值)。但当规则数量很多时,Hash匹配会快得多(因为可以直接定位)。

这一章,我们就跟着ACL插件的代码,看看它是如何"用字典快速查找规则"的。每一步我都会详细解释,确保你完全理解。


10.1 Hash匹配引擎的概念:什么是"字典查找"?

10.1.1 什么是Hash匹配引擎?

Hash匹配引擎(Hash Matching Engine):一种使用Hash表来快速匹配ACL规则的方法,将匹配时间复杂度从O(M)(M是规则数量)降低到O(N)(N是不同掩码组合的数量)。

关键概念详解

  1. 线性匹配(Linear Matching):逐条检查ACL规则,直到找到匹配的规则

    • 时间复杂度:O(M),M是规则数量
    • 优点:实现简单,小规模规则时性能好
    • 缺点:规则数量多时,性能下降明显
    • 类比:就像"一本一本地翻书"(从第一本开始,逐本查找)
  2. Hash匹配(Hash Matching):使用Hash表存储规则,通过计算Hash值直接定位规则

    • 时间复杂度:O(1)平均情况(如果Hash冲突少)
    • 优点:规则数量多时,性能稳定
    • 缺点:需要额外的内存和构建时间
    • 类比:就像"使用字典"(根据书名计算索引,直接定位到书架位置)
  3. 掩码类型(Mask Type):具有相同掩码模式的规则集合

    • 掩码(Mask):用于指定哪些字段需要匹配(1表示需要匹配,0表示不关心)
    • 掩码类型:具有相同掩码模式的规则被归类为同一个掩码类型
    • 为什么需要?:减少Hash表的数量,提高查找效率
    • 类比:就像"书籍分类"(相同类型的书放在同一个书架)
  4. TupleMerge算法:一种动态优化掩码的算法,通过"放松"掩码来合并相似的规则

    • 放松(Relax):减少掩码的精确度,使更多规则可以共享同一个Hash表
    • 为什么需要?:减少Hash表的数量,提高内存利用率
    • 类比:就像"合并相似的书架"(将相似类型的书放在同一个书架,节省空间)
10.1.2 为什么需要Hash匹配?

问题场景

假设有1000条ACL规则,使用线性匹配:

  • 平均查找次数:500次(需要检查一半的规则才能找到匹配)
  • 最坏情况:1000次(需要检查所有规则)
  • 性能影响:每个数据包都需要检查大量规则,CPU占用高

使用Hash匹配:

  • 平均查找次数:1-2次(直接定位到Hash表,然后检查冲突链)
  • 最坏情况:取决于冲突链长度(通常很短)
  • 性能影响:每个数据包只需要检查少量规则,CPU占用低

性能对比

规则数量 线性匹配(平均) Hash匹配(平均) 性能提升
10条 5次 1-2次 2-5倍
100条 50次 1-2次 25-50倍
1000条 500次 1-2次 250-500倍

类比

  • 线性匹配:就像"在1000本书中一本一本地找"(需要检查500本)
  • Hash匹配:就像"使用字典索引"(直接定位到书架,只需要检查1-2本)

10.2 数据结构:如何"组织规则信息"?

10.2.1 Hash ACE信息:hash_ace_info_t

hash_ace_info_t 用于存储单个规则的Hash表示信息:

c 复制代码
//24:33:src/plugins/acl/hash_lookup_types.h
typedef struct {
  fa_5tuple_t match;                              // 5-tuple匹配值(用于Hash表key)
                                         // fa_5tuple_t:5-tuple结构体(源IP、目的IP、协议、源端口、目的端口)
                                         // match:规则的匹配值(用于构建Hash表key)
                                         // 类比:就像"书籍的'索引信息'"(用于定位书架位置)
  
  /* these two entries refer to the original ACL# and rule# within that ACL */
  u32 acl_index;                                  // ACL索引(指向原始ACL)
                                         // acl_index:规则所属的ACL索引(用于调试和追踪)
                                         // 类比:就像"书籍所属的'书架编号'"
  
  u32 ace_index;                                  // ACE索引(指向原始规则)
                                         // ace_index:规则在ACL中的索引(用于调试和追踪)
                                         // 类比:就像"书籍在书架中的'位置编号'"
  
  u32 base_mask_type_index;                       // 基础掩码类型索引
                                         // base_mask_type_index:规则的基础掩码类型索引(未经过TupleMerge优化)
                                         // 为什么需要?用于追踪规则的原始掩码类型
                                         // 类比:就像"书籍的'原始分类编号'"
  
  u8 action;                                      // 动作(permit或deny)
                                         // action:规则的动作(0=deny,非0=permit)
                                         // 类比:就像"书籍的'处理方式'"(允许借阅或禁止借阅)
} hash_ace_info_t;

结构体字段总结

  1. 匹配值:规则的5-tuple匹配值(用于构建Hash表key)
  2. 索引信息:ACL索引和ACE索引(用于追踪规则的来源)
  3. 掩码类型:基础掩码类型索引(用于掩码管理)
  4. 动作:规则的permit/deny动作

类比:就像"书籍的索引卡片":

  • 索引信息:书名、作者(5-tuple匹配值)
  • 书架信息:所属书架编号、位置编号(ACL索引、ACE索引)
  • 分类信息:原始分类编号(基础掩码类型索引)
  • 处理方式:允许借阅或禁止借阅(动作)

10.2.2 Hash ACL信息:hash_acl_info_t

hash_acl_info_t 用于存储整个ACL的Hash匹配信息:

c 复制代码
//35:44:src/plugins/acl/hash_lookup_types.h
/*
 * The structure holding the information necessary for the hash-based ACL operation
 */
typedef struct {
  /* hash ACL applied on these lookup contexts */
  u32 *lc_index_list;                             // Lookup Context索引列表
                                         // lc_index_list:应用此ACL的Lookup Context索引列表
                                         // Lookup Context:一个接口+方向的组合(如"接口0输入方向")
                                         // 为什么需要?同一个ACL可能应用到多个Lookup Context
                                         // 类比:就像"书籍可能被放在多个书架上"
  
  hash_ace_info_t *rules;                         // Hash ACE信息数组
                                         // rules:ACL中所有规则的Hash表示信息数组
                                         // 为什么需要?每个规则都需要转换为Hash表示
                                         // 类比:就像"书架中所有书籍的索引卡片"
  
  /* a boolean flag set when the hash acl info is initialized */
  int hash_acl_exists;                            // Hash ACL是否存在标志
                                         // hash_acl_exists:Hash ACL信息是否已初始化
                                         // 为什么需要?用于检查Hash ACL是否已构建
                                         // 类比:就像"书架是否已建立索引"
} hash_acl_info_t;

结构体字段总结

  1. Lookup Context列表:应用此ACL的Lookup Context索引列表
  2. 规则数组:ACL中所有规则的Hash表示信息
  3. 初始化标志:Hash ACL信息是否已初始化

类比:就像"整个书架的索引系统":

  • 书架列表:书籍可能被放在哪些书架上(Lookup Context列表)
  • 索引卡片:书架中所有书籍的索引卡片(规则数组)
  • 系统状态:书架是否已建立索引(初始化标志)

10.2.3 应用的Hash ACE条目:applied_hash_ace_entry_t

applied_hash_ace_entry_t 用于存储应用到Lookup Context的Hash ACE条目:

c 复制代码
//55:83:src/plugins/acl/hash_lookup_types.h
typedef struct {
  /* original non-compiled ACL */
  u32 acl_index;                                  // ACL索引(指向原始ACL)
                                         // acl_index:规则所属的ACL索引
                                         // 类比:就像"书籍所属的'书架编号'"
  
  u32 ace_index;                                  // ACE索引(指向原始规则)
                                         // ace_index:规则在ACL中的索引
                                         // 类比:就像"书籍在书架中的'位置编号'"
  
  /* the index of the hash_ace_info_t */
  u32 hash_ace_info_index;                        // Hash ACE信息索引
                                         // hash_ace_info_index:规则在hash_acl_info_t->rules数组中的索引
                                         // 为什么需要?用于查找规则的Hash表示信息
                                         // 类比:就像"书籍在索引卡片中的'位置编号'"
  
  /* applied mask type index */
  u32 mask_type_index;                            // 应用的掩码类型索引
                                         // mask_type_index:规则应用的掩码类型索引(可能经过TupleMerge优化)
                                         // 为什么需要?用于查找规则的掩码类型
                                         // 类比:就像"书籍的'当前分类编号'"(可能经过合并优化)
  
  /*
   * index of applied entry, which owns the colliding_rules vector
   */
  u32 collision_head_ae_index;                    // 冲突头条目索引
                                         // collision_head_ae_index:拥有冲突规则向量的条目索引
                                         // 为什么需要?当多个规则Hash到同一个位置时,需要链接它们
                                         // 类比:就像"冲突书籍的'主索引卡片编号'"
  
  /*
   * Collision rule vector for matching - set only on head entry
   */
  collision_match_rule_t *colliding_rules;        // 冲突规则向量(仅头条目设置)
                                         // colliding_rules:Hash冲突的规则列表(仅头条目有)
                                         // 为什么需要?当多个规则Hash到同一个位置时,需要存储所有冲突规则
                                         // 类比:就像"冲突书籍的'索引卡片列表'"
  
  /*
   * number of hits on this entry
   */
  u64 hitcount;                                   // 命中计数
                                         // hitcount:此条目被匹配的次数(用于统计和优化)
                                         // 为什么需要?用于统计规则的匹配频率,优化Hash表结构
                                         // 类比:就像"书籍的'借阅次数'"
  
  /*
   * acl position in vector of ACLs within lookup context
   */
  u32 acl_position;                               // ACL在Lookup Context中的位置
                                         // acl_position:ACL在Lookup Context的ACL列表中的位置
                                         // 为什么需要?用于确定规则的优先级(位置越靠前,优先级越高)
                                         // 类比:就像"书架在图书馆中的'位置编号'"
  
  /*
   * Action of this applied ACE
   */
  u8 action;                                      // 动作(permit或deny)
                                         // action:规则的permit/deny动作
                                         // 类比:就像"书籍的'处理方式'"
} applied_hash_ace_entry_t;

结构体字段总结

  1. 原始信息:ACL索引、ACE索引(用于追踪规则的来源)
  2. Hash信息:Hash ACE信息索引、掩码类型索引(用于Hash表查找)
  3. 冲突处理:冲突头条目索引、冲突规则向量(用于处理Hash冲突)
  4. 统计信息:命中计数(用于性能统计)
  5. 优先级信息:ACL位置(用于确定规则优先级)
  6. 动作:permit/deny动作

类比:就像"应用到书架的书籍索引卡片":

  • 原始信息:所属书架编号、位置编号(ACL索引、ACE索引)
  • Hash信息:索引卡片位置、当前分类编号(Hash ACE信息索引、掩码类型索引)
  • 冲突处理:冲突书籍的主索引卡片、冲突书籍列表(冲突头条目索引、冲突规则向量)
  • 统计信息:借阅次数(命中计数)
  • 优先级信息:书架在图书馆中的位置(ACL位置)
  • 处理方式:允许借阅或禁止借阅(动作)

10.2.4 冲突匹配规则:collision_match_rule_t

collision_match_rule_t 用于存储Hash冲突的规则信息:

c 复制代码
//47:53:src/plugins/acl/hash_lookup_types.h
typedef struct {
  acl_rule_t rule;                                // 原始规则
                                         // rule:完整的ACL规则(用于精确匹配)
                                         // 为什么需要?当Hash冲突时,需要精确匹配规则
                                         // 类比:就像"冲突书籍的'详细信息'"
  
  u32 acl_index;                                  // ACL索引
                                         // acl_index:规则所属的ACL索引
                                         // 类比:就像"书籍所属的'书架编号'"
  
  u32 ace_index;                                  // ACE索引
                                         // ace_index:规则在ACL中的索引
                                         // 类比:就像"书籍在书架中的'位置编号'"
  
  u32 acl_position;                               // ACL位置
                                         // acl_position:ACL在Lookup Context中的位置(用于确定优先级)
                                         // 类比:就像"书架在图书馆中的'位置编号'"
  
  u32 applied_entry_index;                        // 应用的条目索引
                                         // applied_entry_index:规则在applied_hash_ace_entry_t数组中的索引
                                         // 为什么需要?用于快速定位规则的应用条目
                                         // 类比:就像"书籍在索引卡片数组中的'位置编号'"
} collision_match_rule_t;

结构体字段总结

  1. 原始规则:完整的ACL规则(用于精确匹配)
  2. 索引信息:ACL索引、ACE索引(用于追踪规则的来源)
  3. 优先级信息:ACL位置(用于确定规则优先级)
  4. 应用条目索引:规则在应用条目数组中的索引(用于快速定位)

类比:就像"冲突书籍的详细信息卡片":

  • 详细信息:书籍的完整信息(原始规则)
  • 书架信息:所属书架编号、位置编号(ACL索引、ACE索引)
  • 优先级信息:书架在图书馆中的位置(ACL位置)
  • 索引位置:书籍在索引卡片数组中的位置(应用条目索引)

10.2.5 Hash ACL查找值:hash_acl_lookup_value_t

hash_acl_lookup_value_t 用于存储Hash表的value(应用条目索引):

c 复制代码
//92:100:src/plugins/acl/hash_lookup_types.h
typedef union {
  u64 as_u64;                                     // 作为64位整数使用
                                         // as_u64:整个结构体作为64位整数使用(用于Hash表value)
                                         // 为什么需要?Hash表的value必须是固定大小(8字节)
                                         // 类比:就像"索引卡片的'编号'"
  
  struct {
    u32 applied_entry_index;                      // 应用条目索引(32位)
                                         // applied_entry_index:规则在applied_hash_ace_entry_t数组中的索引
                                         // 为什么需要?用于快速定位规则的应用条目
                                         // 类比:就像"书籍在索引卡片数组中的'位置编号'"
    
    u16 reserved_u16;                            // 保留字段(16位)
                                         // reserved_u16:保留字段(用于未来扩展)
    
    u8 reserved_u8;                              // 保留字段(8位)
                                         // reserved_u8:保留字段(用于未来扩展)
    
    u8 reserved_flags:8;                         // 保留标志位(8位)
                                         // reserved_flags:保留标志位(用于未来扩展)
  };
} hash_acl_lookup_value_t;

结构体字段总结

  1. 应用条目索引:规则在应用条目数组中的索引(用于快速定位)
  2. 保留字段:用于未来扩展

类比:就像"索引卡片的编号":

  • 位置编号:书籍在索引卡片数组中的位置(应用条目索引)
  • 保留字段:用于未来扩展(如添加更多信息)

10.2.6 Hash应用的掩码信息:hash_applied_mask_info_t

hash_applied_mask_info_t 用于存储应用到Lookup Context的掩码类型信息:

c 复制代码
//103:110:src/plugins/acl/hash_lookup_types.h
typedef struct {
   u32 mask_type_index;                           // 掩码类型索引
                                         // mask_type_index:掩码类型索引(用于查找掩码类型)
                                         // 类比:就像"书籍分类的'编号'"
  
   /* first rule # for this mask */
   u32 first_rule_index;                           // 第一个规则索引
                                         // first_rule_index:使用此掩码类型的第一个规则索引
                                         // 为什么需要?用于优化查找顺序(按优先级排序)
                                         // 类比:就像"此分类中第一本书的'位置编号'"
  
   /* Debug Information */
   u32 num_entries;                                // 条目数量
                                         // num_entries:使用此掩码类型的规则数量(用于统计)
                                         // 类比:就像"此分类中的'书籍数量'"
  
   u32 max_collisions;                             // 最大冲突数
                                         // max_collisions:此掩码类型的最大Hash冲突数(用于性能分析)
                                         // 为什么需要?用于评估Hash表的质量(冲突越少,性能越好)
                                         // 类比:就像"此分类中的'最大冲突书籍数'"
} hash_applied_mask_info_t;

结构体字段总结

  1. 掩码类型索引:掩码类型索引(用于查找掩码类型)
  2. 第一个规则索引:使用此掩码类型的第一个规则索引(用于优化查找顺序)
  3. 统计信息:条目数量、最大冲突数(用于性能分析)

类比:就像"书籍分类的统计信息":

  • 分类编号:书籍分类的编号(掩码类型索引)
  • 第一本书:此分类中第一本书的位置(第一个规则索引)
  • 统计信息:书籍数量、最大冲突数(条目数量、最大冲突数)

10.3 Hash表构建:如何"建立字典索引"?

10.3.1 Hash ACL应用:hash_acl_apply

hash_acl_apply 用于将ACL应用到Lookup Context,构建Hash表:

c 复制代码
//621:713:src/plugins/acl/hash_lookup.c
void
hash_acl_apply(acl_main_t *am, u32 lc_index, int acl_index, u32 acl_position)
{
  int i;                                          // 循环变量(临时变量)

  DBG0("HASH ACL apply: lc_index %d acl %d", lc_index, acl_index);  // 打印调试信息
                                         // DBG0:调试输出函数(仅在调试模式下输出)
                                         // 类比:就像"记录'开始建立索引'的信息"

  // ========== 第一部分:初始化Hash表 ==========
  
  if (!am->acl_lookup_hash_initialized)          // 如果Hash表未初始化
    {
      BV (clib_bihash_init) (&am->acl_lookup_hash, "ACL plugin rule lookup bihash",  // 初始化Hash表
			     am->hash_lookup_hash_buckets, am->hash_lookup_hash_memory);  // 参数:Hash表指针、名称、桶数量、内存大小
                                         // clib_bihash_init:初始化双向Hash表(bihash)
                                         // acl_lookup_hash:ACL查找Hash表(48字节key,8字节value)
                                         // hash_lookup_hash_buckets:Hash表桶数量(在初始化时配置)
                                         // hash_lookup_hash_memory:Hash表内存大小(在初始化时配置)
                                         // 为什么需要?Hash表需要预先分配内存
                                         // 类比:就像"初始化'字典索引系统'"
      
      am->acl_lookup_hash_initialized = 1;       // 设置初始化标志为1
                                         // 类比:就像"标记'字典索引系统'已初始化"
    }

  // ========== 第二部分:验证和准备数据结构 ==========
  
  vec_validate(am->hash_entry_vec_by_lc_index, lc_index);  // 确保应用条目向量有足够的空间
                                         // hash_entry_vec_by_lc_index:按Lookup Context索引的应用条目向量数组
                                         // vec_validate:确保向量有足够的空间(如果不够,自动扩展)
                                         // 类比:就像"确保'索引卡片数组'有足够的空间"
  
  vec_validate(am->hash_acl_infos, acl_index);   // 确保Hash ACL信息向量有足够的空间
                                         // hash_acl_infos:Hash ACL信息向量数组
                                         // 类比:就像"确保'书架索引信息数组'有足够的空间"
  
  applied_hash_ace_entry_t **applied_hash_aces = get_applied_hash_aces(am, lc_index);  // 获取应用条目向量
                                         // get_applied_hash_aces:获取Lookup Context的应用条目向量
                                         // 类比:就像"获取'索引卡片数组'"
  
  hash_acl_info_t *ha = vec_elt_at_index(am->hash_acl_infos, acl_index);  // 获取Hash ACL信息
                                         // hash_acl_infos:Hash ACL信息向量数组
                                         // 类比:就像"获取'书架索引信息'"
  
  u32 **hash_acl_applied_lc_index = &ha->lc_index_list;  // 获取Lookup Context索引列表指针
                                         // lc_index_list:应用此ACL的Lookup Context索引列表
                                         // 类比:就像"获取'书架应用列表'"

  // ========== 第三部分:记录ACL应用信息 ==========
  
  int base_offset = vec_len(*applied_hash_aces);  // 计算基础偏移量(当前应用条目数量)
                                         // base_offset:新规则在应用条目向量中的起始位置
                                         // 为什么需要?新规则会追加到向量末尾
                                         // 类比:就像"计算'新索引卡片'的起始位置"

  /* Update the bitmap of the mask types with which the lookup
     needs to happen for the ACLs applied to this lc_index */
  applied_hash_acl_info_t **applied_hash_acls = &am->applied_hash_acl_info_by_lc_index;  // 获取应用的Hash ACL信息
                                         // applied_hash_acl_info_by_lc_index:按Lookup Context索引的应用Hash ACL信息数组
                                         // 类比:就像"获取'应用的索引系统信息'"
  
  vec_validate((*applied_hash_acls), lc_index);  // 确保应用Hash ACL信息向量有足够的空间
                                         // 类比:就像"确保'应用的索引系统信息数组'有足够的空间"
  
  applied_hash_acl_info_t *pal = vec_elt_at_index((*applied_hash_acls), lc_index);  // 获取应用Hash ACL信息
                                         // 类比:就像"获取'应用的索引系统信息'"

  /* ensure the list of applied hash acls is initialized and add this acl# to it */
  u32 index = vec_search(pal->applied_acls, acl_index);  // 搜索ACL索引是否已存在
                                         // vec_search:在向量中搜索元素(返回索引,如果未找到返回~0)
                                         // 为什么需要?防止重复应用同一个ACL
                                         // 类比:就像"检查'书架'是否已在'应用列表'中"
  
  if (index != ~0)                                // 如果ACL索引已存在
    {
      clib_warning("BUG: trying to apply twice acl_index %d on lc_index %d, according to lc",  // 打印警告
		   acl_index, lc_index);
                                         // clib_warning:警告输出函数(不会终止程序)
                                         // 类比:就像"如果'书架'已在'应用列表'中,报错"
      
      ASSERT(0);                                  // 断言失败(终止程序)
                                         // ASSERT:断言(如果条件不满足,程序终止)
                                         // 类比:就像"终止程序"
      
      return;                                      // 返回(不继续执行)
                                         // 类比:就像"返回,不继续建立索引"
    }
  
  vec_add1(pal->applied_acls, acl_index);        // 添加ACL索引到应用列表
                                         // vec_add1:向向量添加一个元素
                                         // 类比:就像"将'书架'添加到'应用列表'"
  
  u32 index2 = vec_search((*hash_acl_applied_lc_index), lc_index);  // 搜索Lookup Context索引是否已存在
                                         // 为什么需要?防止重复记录
                                         // 类比:就像"检查'Lookup Context'是否已在'书架应用列表'中"
  
  if (index2 != ~0)                              // 如果Lookup Context索引已存在
    {
      clib_warning("BUG: trying to apply twice acl_index %d on lc_index %d, according to hash h-acl info",  // 打印警告
		   acl_index, lc_index);
                                         // 类比:就像"如果'Lookup Context'已在'书架应用列表'中,报错"
      
      ASSERT(0);                                  // 断言失败(终止程序)
                                         // 类比:就像"终止程序"
      
      return;                                      // 返回(不继续执行)
                                         // 类比:就像"返回,不继续建立索引"
    }
  
  vec_add1((*hash_acl_applied_lc_index), lc_index);  // 添加Lookup Context索引到列表
                                         // 类比:就像"将'Lookup Context'添加到'书架应用列表'"

  // ========== 第四部分:准备掩码信息向量 ==========
  
  /*
   * if the applied ACL is empty, the current code will cause a
   * different behavior compared to current linear search: an empty ACL will
   * simply fallthrough to the next ACL, or the default deny in the end.
   *
   * This is not a problem, because after vpp-dev discussion,
   * the consensus was it should not be possible to apply the non-existent
   * ACL, so the change adding this code also takes care of that.
   */
                                         // 注释:如果ACL为空,行为与线性搜索不同(空ACL会直接跳过)
                                         // 但这不是问题,因为不应该应用不存在的ACL

  vec_validate(am->hash_applied_mask_info_vec_by_lc_index, lc_index);  // 确保掩码信息向量有足够的空间
                                         // hash_applied_mask_info_vec_by_lc_index:按Lookup Context索引的掩码信息向量数组
                                         // 类比:就像"确保'分类信息数组'有足够的空间"

  /* since we know (in case of no split) how much we expand, preallocate that space */
  if (vec_len(ha->rules) > 0)                    // 如果ACL有规则
    {
      int old_vec_len = vec_len(*applied_hash_aces);  // 获取旧向量长度
                                         // 类比:就像"获取'旧索引卡片数组'的长度"
      
      vec_validate((*applied_hash_aces), old_vec_len + vec_len(ha->rules) - 1);  // 预分配空间
                                         // vec_validate:确保向量有足够的空间(预分配,避免频繁扩展)
                                         // 为什么需要?预分配可以提高性能(避免频繁扩展向量)
                                         // 类比:就像"预分配'索引卡片数组'的空间"
      
      vec_set_len ((*applied_hash_aces), old_vec_len);  // 设置向量长度为旧长度(保留预分配的空间)
                                         // vec_set_len:设置向量长度(不释放内存)
                                         // 为什么需要?保留预分配的空间,但保持长度为旧长度
                                         // 类比:就像"保留预分配的空间,但保持长度为旧长度"
    }

  // ========== 第五部分:处理每个规则 ==========
  
  /* add the rules from the ACL to the hash table for lookup and append to the vector*/
  for(i=0; i < vec_len(ha->rules); i++)           // 遍历ACL中的所有规则
    {
      /*
       * Expand the applied aces vector to fit a new entry.
       * One by one not to upset split_partition() if it is called.
       */
                                         // 注释:逐个扩展向量,避免split_partition()调用时出现问题
      
      vec_resize((*applied_hash_aces), 1);        // 扩展向量(增加1个元素)
                                         // vec_resize:扩展向量(增加指定数量的元素)
                                         // 为什么需要?逐个扩展,避免split_partition()调用时出现问题
                                         // 类比:就像"逐个添加'索引卡片'"

      int is_ip6 = ha->rules[i].match.pkt.is_ip6;  // 获取地址族(IPv4或IPv6)
                                         // is_ip6:规则是否使用IPv6(0=IPv4,1=IPv6)
                                         // 类比:就像"获取'书籍'的'地址族'"
      
      u32 new_index = base_offset + i;            // 计算新索引(基础偏移量 + 规则索引)
                                         // new_index:新规则在应用条目向量中的索引
                                         // 类比:就像"计算'新索引卡片'的位置"
      
      applied_hash_ace_entry_t *pae = vec_elt_at_index((*applied_hash_aces), new_index);  // 获取应用条目指针
                                         // 类比:就像"获取'索引卡片'的指针"
      
      // ========== 填充应用条目信息 ==========
      
      pae->acl_index = acl_index;                 // 设置ACL索引
                                         // 类比:就像"设置'书架编号'"
      
      pae->ace_index = ha->rules[i].ace_index;    // 设置ACE索引
                                         // 类比:就像"设置'书籍在书架中的位置编号'"
      
      pae->acl_position = acl_position;          // 设置ACL位置
                                         // 类比:就像"设置'书架在图书馆中的位置'"
      
      pae->action = ha->rules[i].action;          // 设置动作
                                         // 类比:就像"设置'处理方式'"
      
      pae->hitcount = 0;                           // 初始化命中计数为0
                                         // 类比:就像"初始化'借阅次数'为0"
      
      pae->hash_ace_info_index = i;               // 设置Hash ACE信息索引
                                         // 类比:就像"设置'索引卡片在数组中的位置'"
      
      /* we might link it in later */
      pae->collision_head_ae_index = ~0;          // 初始化冲突头条目索引为无效
                                         // 类比:就像"初始化'冲突书籍的主索引卡片编号'为无效"
      
      pae->colliding_rules = NULL;                // 初始化冲突规则向量为NULL
                                         // 类比:就像"初始化'冲突书籍列表'为空"
      
      pae->mask_type_index = ~0;                  // 初始化掩码类型索引为无效
                                         // 类比:就像"初始化'分类编号'为无效"
      
      // ========== 分配掩码类型索引 ==========
      
      assign_mask_type_index_to_pae(am, lc_index, is_ip6, pae);  // 分配掩码类型索引
                                         // assign_mask_type_index_to_pae:分配掩码类型索引(可能使用TupleMerge优化)
                                         // 作用:根据规则的掩码,分配或创建掩码类型索引
                                         // 类比:就像"分配'分类编号'"
      
      // ========== 激活Hash表条目 ==========
      
      u32 first_index = activate_applied_ace_hash_entry(am, lc_index, applied_hash_aces, new_index);  // 激活Hash表条目
                                         // activate_applied_ace_hash_entry:激活Hash表条目(添加到Hash表)
                                         // 返回值:冲突头条目索引(如果Hash冲突,返回头条目索引;否则返回新索引)
                                         // 作用:将规则添加到Hash表,处理Hash冲突
                                         // 类比:就像"将'索引卡片'添加到'字典索引系统'"
      
      // ========== 检查冲突数量并可能分割 ==========
      
      if (am->use_tuple_merge)                    // 如果使用TupleMerge
	{
	  check_collision_count_and_maybe_split(am, lc_index, is_ip6, first_index);  // 检查冲突数量并可能分割
                                         // check_collision_count_and_maybe_split:检查冲突数量并可能分割
                                         // 作用:如果冲突数量超过阈值,分割分区(优化Hash表结构)
                                         // 类比:就像"如果'冲突书籍'太多,分割'分类'"
	}
    }
  
  // ========== 第六部分:重建掩码信息向量 ==========
  
  remake_hash_applied_mask_info_vec(am, applied_hash_aces, lc_index);  // 重建掩码信息向量
                                         // remake_hash_applied_mask_info_vec:重建掩码信息向量
                                         // 作用:根据当前的应用条目,重新构建掩码信息向量(统计信息)
                                         // 类比:就像"重建'分类统计信息'"
}

函数总结

这个函数完成了以下工作:

  1. 初始化Hash表:如果Hash表未初始化,初始化它
  2. 验证和准备数据结构:确保所有必要的向量有足够的空间
  3. 记录ACL应用信息:将ACL添加到应用列表,防止重复应用
  4. 准备掩码信息向量:预分配空间,提高性能
  5. 处理每个规则
    • 创建应用条目
    • 填充应用条目信息
    • 分配掩码类型索引(可能使用TupleMerge优化)
    • 激活Hash表条目(添加到Hash表,处理Hash冲突)
    • 检查冲突数量并可能分割(如果使用TupleMerge)
  6. 重建掩码信息向量:根据当前的应用条目,重新构建掩码信息向量

类比:就像完整的"建立字典索引系统"流程:

  1. 初始化系统:初始化"字典索引系统"
  2. 准备空间:确保所有"数组"有足够的空间
  3. 记录信息:将"书架"添加到"应用列表"
  4. 预分配空间:预分配"索引卡片数组"的空间
  5. 处理每本书
    • 创建"索引卡片"
    • 填写"索引卡片"信息
    • 分配"分类编号"(可能使用合并优化)
    • 添加到"字典索引系统"(处理冲突)
    • 检查冲突并可能分割(如果冲突太多)
  6. 重建统计信息:重建"分类统计信息"


10.4 掩码类型分配:如何"给规则分配分类"?

10.4.1 掩码类型分配函数:assign_mask_type_index_to_pae

assign_mask_type_index_to_pae 用于为应用条目分配掩码类型索引:

c 复制代码
//586:604:src/plugins/acl/hash_lookup.c
static void
assign_mask_type_index_to_pae(acl_main_t *am, u32 lc_index, int is_ip6, applied_hash_ace_entry_t *pae)
{
  hash_acl_info_t *ha = vec_elt_at_index(am->hash_acl_infos, pae->acl_index);  // 获取Hash ACL信息
                                         // 类比:就像"获取'书架索引信息'"
  
  hash_ace_info_t *ace_info = vec_elt_at_index(ha->rules, pae->hash_ace_info_index);  // 获取Hash ACE信息
                                         // 类比:就像"获取'索引卡片信息'"

  ace_mask_type_entry_t *mte;                    // 掩码类型条目指针(临时变量)
  fa_5tuple_t mask;                              // 掩码(临时变量)
  
  /*
   * Start taking base_mask associated to ace, and essentially copy it.
   * With TupleMerge we will assign a relaxed mask here.
   */
                                         // 注释:从ACE的基础掩码开始,复制它。如果使用TupleMerge,会分配一个放松的掩码。
  
  mte = vec_elt_at_index(am->ace_mask_type_pool, ace_info->base_mask_type_index);  // 获取基础掩码类型条目
                                         // ace_mask_type_pool:掩码类型条目pool(所有掩码类型的集合)
                                         // base_mask_type_index:规则的基础掩码类型索引(未经过TupleMerge优化)
                                         // 类比:就像"获取'原始分类信息'"
  
  mask = mte->mask;                              // 复制掩码
                                         // 类比:就像"复制'原始分类掩码'"
  
  // ========== 根据是否使用TupleMerge选择分配方式 ==========
  
  if (am->use_tuple_merge)                       // 如果使用TupleMerge
    {
      pae->mask_type_index = tm_assign_mask_type_index(am, &mask, is_ip6, lc_index);  // 使用TupleMerge分配掩码类型索引
                                         // tm_assign_mask_type_index:使用TupleMerge分配掩码类型索引
                                         // 作用:尝试找到可以包含当前掩码的现有掩码类型,如果找不到,创建新的放松掩码类型
                                         // 类比:就像"使用'合并优化'分配'分类编号'"
    }
  else                                          // 如果不使用TupleMerge
    {
      pae->mask_type_index = assign_mask_type_index(am, &mask);  // 直接分配掩码类型索引
                                         // assign_mask_type_index:直接分配掩码类型索引
                                         // 作用:查找或创建掩码类型索引(不进行放松)
                                         // 类比:就像"直接分配'分类编号'(不进行合并优化)"
    }
}

函数总结

  1. 获取基础掩码:从规则的基础掩码类型条目中获取掩码
  2. 选择分配方式
    • 使用TupleMerge:使用TupleMerge分配掩码类型索引(可能放松掩码)
    • 不使用TupleMerge:直接分配掩码类型索引(不放松掩码)

类比:就像"给书籍分配分类编号":

  1. 获取原始分类:从"原始分类信息"中获取"分类掩码"
  2. 选择分配方式
    • 使用合并优化:使用"合并优化"分配"分类编号"(可能合并相似分类)
    • 不使用合并优化:直接分配"分类编号"(不合并)

10.4.2 直接分配掩码类型索引:assign_mask_type_index

assign_mask_type_index 用于直接分配掩码类型索引(不进行TupleMerge优化):

c 复制代码
//272:294:src/plugins/acl/hash_lookup.c
static u32
assign_mask_type_index(acl_main_t *am, fa_5tuple_t *mask)
{
  u32 mask_type_index = find_mask_type_index(am, mask);  // 查找掩码类型索引
                                         // find_mask_type_index:查找掩码类型索引(如果存在)
                                         // 返回值:掩码类型索引(如果找到),~0(如果未找到)
                                         // 类比:就像"查找'分类编号'(如果存在)"
  
  ace_mask_type_entry_t *mte;                    // 掩码类型条目指针(临时变量)
  
  if(~0 == mask_type_index)                      // 如果未找到掩码类型索引
    {
      pool_get_aligned (am->ace_mask_type_pool, mte, CLIB_CACHE_LINE_BYTES);  // 从pool中分配掩码类型条目
                                         // pool_get_aligned:从pool中分配对齐的内存(缓存行对齐,用于性能优化)
                                         // ace_mask_type_pool:掩码类型条目pool
                                         // CLIB_CACHE_LINE_BYTES:缓存行大小(通常是64字节)
                                         // 类比:就像"从'分类信息池'中分配新的'分类信息'"
      
      mask_type_index = mte - am->ace_mask_type_pool;  // 计算掩码类型索引(在pool中的位置)
                                         // 类比:就像"计算'分类编号'(在池中的位置)"
      
      clib_memcpy_fast(&mte->mask, mask, sizeof(mte->mask));  // 复制掩码到掩码类型条目
                                         // clib_memcpy_fast:快速内存复制(使用优化的memcpy)
                                         // 类比:就像"复制'分类掩码'到'分类信息'"
      
      mte->refcount = 0;                          // 初始化引用计数为0
                                         // refcount:引用计数(有多少规则使用此掩码类型)
                                         // 类比:就像"初始化'使用计数'为0"
      
      /*
       * We can use only 16 bits, since in the match there is only u16 field.
       * Realistically, once you go to 64K of mask types, it is a huge
       * problem anyway, so we might as well stop half way.
       */
                                         // 注释:只能使用16位,因为匹配中只有u16字段。实际上,一旦达到64K掩码类型,就是个大问题,所以最好在半路停止。
      
      ASSERT(mask_type_index < 32768);            // 断言掩码类型索引小于32768(16位最大值的一半)
                                         // 为什么需要?确保掩码类型索引不会超过16位
                                         // 类比:就像"确保'分类编号'不会超过限制"
    }
  
  mte = am->ace_mask_type_pool + mask_type_index;  // 获取掩码类型条目指针
                                         // 类比:就像"获取'分类信息'指针"
  
  mte->refcount++;                                // 增加引用计数
                                         // 类比:就像"增加'使用计数'"
  
  DBG0("ASSIGN MTE index %d new refcount %d", mask_type_index, mte->refcount);  // 打印调试信息
                                         // 类比:就像"记录'分配分类编号'的信息"
  
  return mask_type_index;                         // 返回掩码类型索引
                                         // 类比:就像"返回'分类编号'"
}

函数总结

  1. 查找掩码类型:在掩码类型pool中查找是否存在相同的掩码类型
  2. 创建新掩码类型:如果未找到,创建新的掩码类型条目
  3. 增加引用计数:增加掩码类型的引用计数(表示有多少规则使用此掩码类型)
  4. 返回索引:返回掩码类型索引

类比:就像"给书籍分配分类编号":

  1. 查找分类:在"分类信息池"中查找是否存在相同的"分类"
  2. 创建新分类:如果未找到,创建新的"分类信息"
  3. 增加使用计数:增加"分类"的"使用计数"(表示有多少书籍使用此分类)
  4. 返回编号:返回"分类编号"

10.5 Hash表条目激活:如何"将规则添加到字典"?

10.5.1 Hash表条目激活函数:activate_applied_ace_hash_entry

activate_applied_ace_hash_entry 用于激活Hash表条目(将规则添加到Hash表):

c 复制代码
//547:583:src/plugins/acl/hash_lookup.c
static u32
activate_applied_ace_hash_entry(acl_main_t *am,
                            u32 lc_index,
                            applied_hash_ace_entry_t **applied_hash_aces,
                            u32 new_index)
{
  clib_bihash_kv_48_8_t kv;                       // Hash表key-value对(临时变量)
                                         // clib_bihash_kv_48_8_t:48字节key、8字节value的Hash表key-value对
                                         // 类比:就像"字典索引的'键值对'"
  
  ASSERT(new_index != ~0);                       // 断言新索引不为无效值
                                         // 类比:就像"确保'新索引卡片位置'不为无效"
  
  DBG("activate_applied_ace_hash_entry lc_index %d new_index %d", lc_index, new_index);  // 打印调试信息
                                         // 类比:就像"记录'激活Hash表条目'的信息"

  // ========== 第一部分:填充Hash表key-value对 ==========
  
  fill_applied_hash_ace_kv(am, applied_hash_aces, lc_index, new_index, &kv);  // 填充Hash表key-value对
                                         // fill_applied_hash_ace_kv:填充Hash表key-value对
                                         // 作用:根据应用条目,构建Hash表的key(应用掩码后的5-tuple)和value(应用条目索引)
                                         // 类比:就像"填充'字典索引的键值对'"

  DBG("APPLY ADD KY: %016llx %016llx %016llx %016llx %016llx %016llx",
      kv.key[0], kv.key[1], kv.key[2],
      kv.key[3], kv.key[4], kv.key[5]);  // 打印Hash表key(调试信息)
                                         // 类比:就像"记录'字典索引的键'的信息"

  // ========== 第二部分:在Hash表中查找 ==========
  
  clib_bihash_kv_48_8_t result;                  // Hash表查找结果(临时变量)
                                         // 类比:就像"字典查找结果"
  
  hash_acl_lookup_value_t *result_val = (hash_acl_lookup_value_t *)&result.value;  // 获取查找结果的value指针
                                         // 类比:就像"获取查找结果的'值'指针"
  
  int res = BV (clib_bihash_search) (&am->acl_lookup_hash, &kv, &result);  // 在Hash表中查找
                                         // clib_bihash_search:在双向Hash表中查找
                                         // 返回值:0=找到,非0=未找到
                                         // 类比:就像"在'字典索引系统'中查找'键'"
  
  ASSERT(new_index != ~0);                       // 断言新索引不为无效值
                                         // 类比:就像"确保'新索引卡片位置'不为无效"
  
  ASSERT(new_index < vec_len((*applied_hash_aces)));  // 断言新索引在有效范围内
                                         // 类比:就像"确保'新索引卡片位置'在有效范围内"
  
  // ========== 第三部分:处理查找结果 ==========
  
  if (res == 0)                                  // 如果找到(Hash冲突)
    {
      u32 first_index = result_val->applied_entry_index;  // 获取第一个条目索引
                                         // applied_entry_index:Hash表value中的应用条目索引
                                         // 类比:就像"获取'第一个索引卡片位置'"
      
      ASSERT(first_index != ~0);                 // 断言第一个索引不为无效值
                                         // 类比:就像"确保'第一个索引卡片位置'不为无效"
      
      ASSERT(first_index < vec_len((*applied_hash_aces)));  // 断言第一个索引在有效范围内
                                         // 类比:就像"确保'第一个索引卡片位置'在有效范围内"
      
      /* There already exists an entry or more. Append at the end. */
                                         // 注释:已存在条目或多个条目。追加到末尾。
      
      DBG("A key already exists, with applied entry index: %d", first_index);  // 打印调试信息
                                         // 类比:就像"记录'键已存在'的信息"
      
      add_colliding_rule(am, applied_hash_aces, first_index, new_index);  // 添加冲突规则
                                         // add_colliding_rule:添加冲突规则
                                         // 作用:将新规则添加到冲突规则向量(链接到第一个条目)
                                         // 类比:就像"将'新索引卡片'添加到'冲突书籍列表'"
      
      return first_index;                         // 返回第一个条目索引
                                         // 类比:就像"返回'第一个索引卡片位置'"
    }
  else                                          // 如果未找到(无Hash冲突)
    {
      /* It's the very first entry */
                                         // 注释:这是第一个条目
      
      hashtable_add_del(am, &kv, 1);             // 添加到Hash表
                                         // hashtable_add_del:添加或删除Hash表条目
                                         // 参数:1表示添加
                                         // 类比:就像"将'索引卡片'添加到'字典索引系统'"
      
      ASSERT(new_index != ~0);                   // 断言新索引不为无效值
                                         // 类比:就像"确保'新索引卡片位置'不为无效"
      
      add_colliding_rule(am, applied_hash_aces, new_index, new_index);  // 添加冲突规则(自己作为第一个)
                                         // add_colliding_rule:添加冲突规则
                                         // 作用:将自己添加到冲突规则向量(作为第一个条目)
                                         // 类比:就像"将'索引卡片'添加到'冲突书籍列表'(自己作为第一个)"
      
      return new_index;                           // 返回新索引
                                         // 类比:就像"返回'新索引卡片位置'"
    }
}

函数总结

  1. 填充Hash表key-value对:根据应用条目,构建Hash表的key和value
  2. 在Hash表中查找:检查是否存在Hash冲突
  3. 处理查找结果
    • 如果找到(Hash冲突):将新规则添加到冲突规则向量,返回第一个条目索引
    • 如果未找到(无Hash冲突):添加到Hash表,将自己添加到冲突规则向量,返回新索引

类比:就像"将书籍添加到字典索引系统":

  1. 填充键值对:根据"索引卡片",构建"字典索引的键值对"
  2. 查找键:检查"字典索引系统"中是否存在相同的"键"
  3. 处理结果
    • 如果找到(冲突):将"新索引卡片"添加到"冲突书籍列表",返回"第一个索引卡片位置"
    • 如果未找到(无冲突):添加到"字典索引系统",将自己添加到"冲突书籍列表",返回"新索引卡片位置"

10.5.2 填充Hash表key-value对:fill_applied_hash_ace_kv

fill_applied_hash_ace_kv 用于填充Hash表的key-value对:

c 复制代码
//376:406:src/plugins/acl/hash_lookup.c
static void
fill_applied_hash_ace_kv(acl_main_t *am,
                            applied_hash_ace_entry_t **applied_hash_aces,
                            u32 lc_index,
                            u32 new_index, clib_bihash_kv_48_8_t *kv)
{
  fa_5tuple_t *kv_key = (fa_5tuple_t *)kv->key;  // 获取Hash表key指针(转换为5-tuple指针)
                                         // kv->key:Hash表key(48字节,6个u64)
                                         // 类比:就像"获取'字典索引的键'指针"
  
  hash_acl_lookup_value_t *kv_val = (hash_acl_lookup_value_t *)&kv->value;  // 获取Hash表value指针
                                         // kv->value:Hash表value(8字节)
                                         // 类比:就像"获取'字典索引的值'指针"
  
  applied_hash_ace_entry_t *pae = vec_elt_at_index((*applied_hash_aces), new_index);  // 获取应用条目指针
                                         // 类比:就像"获取'索引卡片'指针"
  
  hash_acl_info_t *ha = vec_elt_at_index(am->hash_acl_infos, pae->acl_index);  // 获取Hash ACL信息
                                         // 类比:就像"获取'书架索引信息'"

  /* apply the mask to ace key */
                                         // 注释:将掩码应用到ACE key
  
  hash_ace_info_t *ace_info = vec_elt_at_index(ha->rules, pae->hash_ace_info_index);  // 获取Hash ACE信息
                                         // 类比:就像"获取'索引卡片信息'"
  
  ace_mask_type_entry_t *mte = vec_elt_at_index(am->ace_mask_type_pool, pae->mask_type_index);  // 获取掩码类型条目
                                         // 类比:就像"获取'分类信息'"
  
  u64 *pmatch = (u64 *) &ace_info->match;  // 获取匹配值指针(转换为u64指针)
                                         // ace_info->match:规则的匹配值(5-tuple)
                                         // 类比:就像"获取'书籍的匹配值'指针"
  
  u64 *pmask = (u64 *)&mte->mask;        // 获取掩码指针(转换为u64指针)
                                         // mte->mask:掩码类型条目的掩码(5-tuple)
                                         // 类比:就像"获取'分类的掩码'指针"
  
  u64 *pkey = (u64 *)kv->key;            // 获取Hash表key指针(转换为u64指针)
                                         // 类比:就像"获取'字典索引的键'指针"

  // ========== 应用掩码到匹配值(构建Hash表key) ==========
  
  *pkey++ = *pmatch++ & *pmask++;        // 应用掩码到匹配值(第0个u64)
                                         // &:位与操作(应用掩码)
                                         // 作用:只保留掩码为1的位(忽略掩码为0的位)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第0部分)"
  
  *pkey++ = *pmatch++ & *pmask++;        // 应用掩码到匹配值(第1个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第1部分)"
  
  *pkey++ = *pmatch++ & *pmask++;        // 应用掩码到匹配值(第2个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第2部分)"
  
  *pkey++ = *pmatch++ & *pmask++;        // 应用掩码到匹配值(第3个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第3部分)"
  
  *pkey++ = *pmatch++ & *pmask++;        // 应用掩码到匹配值(第4个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第4部分)"
  
  *pkey++ = *pmatch++ & *pmask++;        // 应用掩码到匹配值(第5个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第5部分)"

  // ========== 设置Hash表key的元数据 ==========
  
  kv_key->pkt.mask_type_index_lsb = pae->mask_type_index;  // 设置掩码类型索引(低16位)
                                         // mask_type_index_lsb:掩码类型索引的低16位(存储在key的元数据中)
                                         // 为什么需要?用于快速识别掩码类型(在查找时)
                                         // 类比:就像"设置'分类编号'到'字典索引的键'的元数据"
  
  kv_key->pkt.lc_index = lc_index;      // 设置Lookup Context索引
                                         // lc_index:Lookup Context索引(存储在key的元数据中)
                                         // 为什么需要?用于区分不同的Lookup Context(不同接口+方向)
                                         // 类比:就像"设置'书架编号'到'字典索引的键'的元数据"

  // ========== 设置Hash表value ==========
  
  kv_val->as_u64 = 0;                            // 初始化value为0
                                         // 类比:就像"初始化'字典索引的值'为0"
  
  kv_val->applied_entry_index = new_index;       // 设置应用条目索引
                                         // applied_entry_index:应用条目索引(存储在value中)
                                         // 为什么需要?用于快速定位应用条目(在查找时)
                                         // 类比:就像"设置'索引卡片位置'到'字典索引的值'"
}

函数总结

  1. 获取指针:获取匹配值、掩码、Hash表key的指针
  2. 应用掩码:将掩码应用到匹配值(构建Hash表key)
  3. 设置元数据:设置掩码类型索引和Lookup Context索引(存储在key的元数据中)
  4. 设置value:设置应用条目索引(存储在value中)

类比:就像"填充字典索引的键值对":

  1. 获取指针:获取"书籍匹配值"、"分类掩码"、"字典索引的键"的指针
  2. 应用掩码:将"分类掩码"应用到"书籍匹配值"(构建"字典索引的键")
  3. 设置元数据:设置"分类编号"和"书架编号"(存储在"键"的元数据中)
  4. 设置值:设置"索引卡片位置"(存储在"值"中)

10.6 TupleMerge算法:如何"合并相似分类"?

10.6.1 TupleMerge算法概述

TupleMerge算法:一种动态优化掩码的算法,通过"放松"掩码来合并相似的规则,减少Hash表的数量,提高内存利用率。

核心思想

  1. 掩码放松(Mask Relaxation):减少掩码的精确度,使更多规则可以共享同一个Hash表
  2. 掩码包含检查(Mask Containment Check):检查一个掩码是否可以包含另一个掩码
  3. 动态分割(Dynamic Splitting):当Hash冲突过多时,分割分区(创建更精确的掩码)

类比:就像"合并相似的书架分类":

  • 放松分类:减少分类的精确度,使更多书籍可以共享同一个分类
  • 包含检查:检查一个分类是否可以包含另一个分类
  • 动态分割:当冲突书籍太多时,分割分类(创建更精确的分类)

10.6.2 TupleMerge掩码类型分配:tm_assign_mask_type_index

tm_assign_mask_type_index 用于使用TupleMerge算法分配掩码类型索引:

c 复制代码
//321:373:src/plugins/acl/hash_lookup.c
static u32
tm_assign_mask_type_index(acl_main_t *am, fa_5tuple_t *mask, int is_ip6, u32 lc_index)
{
	u32 mask_type_index = ~0;                     // 掩码类型索引(初始化为无效值)
                                         // 类比:就像"初始化'分类编号'为无效"
	
	u32 for_mask_type_index = ~0;                 // 用于查找的掩码类型索引(临时变量)
                                         // 类比:就像"用于查找的'分类编号'"
	
	ace_mask_type_entry_t *mte = 0;               // 掩码类型条目指针(临时变量)
                                         // 类比:就像"分类信息指针"
	
	int order_index;                              // 顺序索引(临时变量,用于遍历)
	
	/* look for existing mask comparable with the one in input */
                                         // 注释:查找与输入掩码兼容的现有掩码

	hash_applied_mask_info_t **hash_applied_mask_info_vec = vec_elt_at_index(am->hash_applied_mask_info_vec_by_lc_index, lc_index);  // 获取掩码信息向量
                                         // hash_applied_mask_info_vec_by_lc_index:按Lookup Context索引的掩码信息向量数组
                                         // 类比:就像"获取'分类信息数组'"
	
	hash_applied_mask_info_t *minfo;              // 掩码信息指针(临时变量)
                                         // 类比:就像"分类信息指针"

        if (vec_len(*hash_applied_mask_info_vec) > 0)  // 如果掩码信息向量不为空
	    {
		// ========== 从后往前遍历掩码信息向量(按优先级排序) ==========
		
		for(order_index = vec_len((*hash_applied_mask_info_vec)) -1; order_index >= 0; order_index--)  // 从后往前遍历
		    {
			minfo = vec_elt_at_index((*hash_applied_mask_info_vec), order_index);  // 获取掩码信息
                                         // 类比:就像"获取'分类信息'"
			
			for_mask_type_index = minfo->mask_type_index;  // 获取掩码类型索引
                                         // 类比:就像"获取'分类编号'"
			
			mte = vec_elt_at_index(am->ace_mask_type_pool, for_mask_type_index);  // 获取掩码类型条目
                                         // 类比:就像"获取'分类信息'"
			
			if(first_mask_contains_second_mask(is_ip6, &mte->mask, mask))  // 如果现有掩码可以包含输入掩码
			    {
				mask_type_index = (mte - am->ace_mask_type_pool);  // 计算掩码类型索引
                                         // 类比:就像"计算'分类编号'"
				
				lock_mask_type_index(am, mask_type_index);  // 锁定掩码类型索引(增加引用计数)
                                         // lock_mask_type_index:锁定掩码类型索引(增加引用计数)
                                         // 作用:表示有新的规则使用此掩码类型
                                         // 类比:就像"锁定'分类编号'(增加使用计数)"
				
				break;                                  // 跳出循环(找到兼容的掩码类型)
                                         // 类比:就像"跳出循环(找到兼容的分类)"
			    }
		    }
	    }

	// ========== 如果未找到兼容的掩码类型,创建新的放松掩码类型 ==========
	
	if(~0 == mask_type_index)                     // 如果未找到兼容的掩码类型索引
	    {
		/* if no mask is found, then let's use a relaxed version of the original one, in order to be used by new ace_entries */
                                         // 注释:如果未找到掩码,则使用原始掩码的放松版本,以便新的ACE条目可以使用
		
		DBG( "TM-assigning mask type index-new one");  // 打印调试信息
                                         // 类比:就像"记录'分配新分类编号'的信息"
		
		fa_5tuple_t relaxed_mask = *mask;        // 复制掩码(用于放松)
                                         // 类比:就像"复制'分类掩码'(用于放松)"
		
		relax_tuple(&relaxed_mask, is_ip6, 0);   // 放松掩码
                                         // relax_tuple:放松掩码(TupleMerge算法的核心)
                                         // 参数:0表示第一次放松(relax2=0)
                                         // 作用:减少掩码的精确度,使更多规则可以共享同一个Hash表
                                         // 类比:就像"放松'分类掩码'(减少精确度)"
		
		mask_type_index = assign_mask_type_index(am, &relaxed_mask);  // 分配掩码类型索引
                                         // assign_mask_type_index:分配掩码类型索引(查找或创建)
                                         // 类比:就像"分配'分类编号'"
		
		// ========== 将新掩码类型添加到掩码信息向量 ==========
		
		hash_applied_mask_info_t **hash_applied_mask_info_vec = vec_elt_at_index(am->hash_applied_mask_info_vec_by_lc_index, lc_index);  // 获取掩码信息向量
                                         // 类比:就像"获取'分类信息数组'"

		int spot = vec_len((*hash_applied_mask_info_vec));  // 计算新位置(向量末尾)
                                         // 类比:就像"计算'新位置'(数组末尾)"
		
		vec_validate((*hash_applied_mask_info_vec), spot);  // 确保向量有足够的空间
                                         // 类比:就像"确保'数组'有足够的空间"
		
		minfo = vec_elt_at_index((*hash_applied_mask_info_vec), spot);  // 获取掩码信息指针
                                         // 类比:就像"获取'分类信息'指针"
		
		minfo->mask_type_index = mask_type_index;  // 设置掩码类型索引
                                         // 类比:就像"设置'分类编号'"
		
		minfo->num_entries = 0;                   // 初始化条目数量为0
                                         // 类比:就像"初始化'书籍数量'为0"
		
		minfo->max_collisions = 0;                 // 初始化最大冲突数为0
                                         // 类比:就像"初始化'最大冲突数'为0"
		
		minfo->first_rule_index = ~0;              // 初始化第一个规则索引为无效
                                         // 类比:就像"初始化'第一本书位置'为无效"
		
		/*
		 * We can use only 16 bits, since in the match there is only u16 field.
		 * Realistically, once you go to 64K of mask types, it is a huge
		 * problem anyway, so we might as well stop half way.
		 */
                                         // 注释:只能使用16位,因为匹配中只有u16字段。实际上,一旦达到64K掩码类型,就是个大问题,所以最好在半路停止。
		
		ASSERT(mask_type_index < 32768);           // 断言掩码类型索引小于32768
                                         // 类比:就像"确保'分类编号'不会超过限制"
	    }
	
	mte = am->ace_mask_type_pool + mask_type_index;  // 获取掩码类型条目指针
                                         // 类比:就像"获取'分类信息'指针"
	
	DBG0("TM-ASSIGN MTE index %d new refcount %d", mask_type_index, mte->refcount);  // 打印调试信息
                                         // 类比:就像"记录'分配分类编号'的信息"
	
	return mask_type_index;                        // 返回掩码类型索引
                                         // 类比:就像"返回'分类编号'"
}

函数总结

  1. 查找兼容掩码:从后往前遍历掩码信息向量,查找可以包含输入掩码的现有掩码类型
  2. 创建新掩码类型:如果未找到,创建新的放松掩码类型
  3. 添加到掩码信息向量:将新掩码类型添加到掩码信息向量

类比:就像"使用合并优化分配分类编号":

  1. 查找兼容分类:从后往前遍历"分类信息数组",查找可以包含"新分类"的现有分类
  2. 创建新分类:如果未找到,创建新的"放松分类"(减少精确度)
  3. 添加到数组:将新分类添加到"分类信息数组"

10.7 Hash匹配流程:如何"使用字典查找规则"?

10.7.1 Hash匹配入口:hash_multi_acl_match_5tuple

hash_multi_acl_match_5tuple 是Hash匹配的入口函数:

c 复制代码
//630:648:src/plugins/acl/public_inlines.h
always_inline int
hash_multi_acl_match_5tuple (void *p_acl_main, u32 lc_index, fa_5tuple_t * pkt_5tuple,
                       int is_ip6, u8 *action, u32 *acl_pos_p, u32 * acl_match_p,
                       u32 * rule_match_p, u32 * trace_bitmap)
{
  acl_main_t *am = p_acl_main;                   // 获取ACL插件主结构体
                                         // 类比:就像"获取'图书馆系统'"
  
  applied_hash_ace_entry_t **applied_hash_aces = vec_elt_at_index(am->hash_entry_vec_by_lc_index, lc_index);  // 获取应用条目向量
                                         // 类比:就像"获取'索引卡片数组'"
  
  u32 match_index = multi_acl_match_get_applied_ace_index(am, is_ip6, pkt_5tuple);  // 获取匹配的应用条目索引
                                         // multi_acl_match_get_applied_ace_index:获取匹配的应用条目索引
                                         // 作用:在Hash表中查找匹配的规则,返回应用条目索引
                                         // 类比:就像"在'字典索引系统'中查找匹配的'索引卡片位置'"
  
  if (match_index < vec_len((*applied_hash_aces)))  // 如果匹配索引有效
    {
      applied_hash_ace_entry_t *pae = vec_elt_at_index((*applied_hash_aces), match_index);  // 获取应用条目指针
                                         // 类比:就像"获取'索引卡片'指针"
      
      pae->hitcount++;                            // 增加命中计数
                                         // 类比:就像"增加'借阅次数'"
      
      *acl_pos_p = pae->acl_position;            // 设置ACL位置输出参数
                                         // 类比:就像"设置'书架位置'输出参数"
      
      *acl_match_p = pae->acl_index;              // 设置ACL索引输出参数
                                         // 类比:就像"设置'书架编号'输出参数"
      
      *rule_match_p = pae->ace_index;             // 设置ACE索引输出参数
                                         // 类比:就像"设置'书籍位置'输出参数"
      
      *action = pae->action;                       // 设置动作输出参数
                                         // 类比:就像"设置'处理方式'输出参数"
      
      return 1;                                   // 返回1(匹配成功)
                                         // 类比:就像"返回'匹配成功'"
    }
  
  return 0;                                       // 返回0(未匹配)
                                         // 类比:就像"返回'未匹配'"
}

函数总结

  1. 获取应用条目向量:根据Lookup Context索引获取应用条目向量
  2. 查找匹配规则 :调用 multi_acl_match_get_applied_ace_index 在Hash表中查找匹配的规则
  3. 返回结果:如果匹配成功,设置输出参数并返回1;否则返回0

类比:就像"使用字典查找书籍":

  1. 获取索引卡片数组:根据"书架编号"获取"索引卡片数组"
  2. 查找匹配书籍:在"字典索引系统"中查找匹配的"索引卡片位置"
  3. 返回结果:如果匹配成功,设置输出参数并返回"匹配成功";否则返回"未匹配"

10.7.2 Hash匹配核心:multi_acl_match_get_applied_ace_index

multi_acl_match_get_applied_ace_index 是Hash匹配的核心函数,实现了完整的匹配逻辑:

c 复制代码
//534:628:src/plugins/acl/public_inlines.h
always_inline u32
multi_acl_match_get_applied_ace_index (acl_main_t * am, int is_ip6, fa_5tuple_t * match)
{
  clib_bihash_kv_48_8_t kv;                       // Hash表key-value对(临时变量)
                                         // 类比:就像"字典索引的'键值对'"
  
  clib_bihash_kv_48_8_t result;                  // Hash表查找结果(临时变量)
                                         // 类比:就像"字典查找结果"
  
  fa_5tuple_t *kv_key = (fa_5tuple_t *) kv.key;  // 获取Hash表key指针(转换为5-tuple指针)
                                         // 类比:就像"获取'字典索引的键'指针"
  
  hash_acl_lookup_value_t *result_val =
    (hash_acl_lookup_value_t *) & result.value;  // 获取查找结果的value指针
                                         // 类比:就像"获取查找结果的'值'指针"
  
  u64 *pmatch = (u64 *) match;                   // 获取匹配值指针(转换为u64指针)
                                         // match:数据包的5-tuple(用于匹配)
                                         // 类比:就像"获取'书籍匹配值'指针"
  
  u64 *pmask;                                     // 掩码指针(临时变量)
                                         // 类比:就像"分类掩码指针"
  
  u64 *pkey;                                      // Hash表key指针(临时变量)
                                         // 类比:就像"字典索引的键指针"
  
  int mask_type_index, order_index;               // 掩码类型索引、顺序索引(临时变量)
                                         // 类比:就像"分类编号、顺序编号"
  
  u32 curr_match_index = (~0 - 1);                // 当前匹配索引(初始化为最大值-1)
                                         // curr_match_index:当前找到的最佳匹配索引(优先级最高的规则)
                                         // 为什么初始化为~0-1?确保第一个匹配的规则会被选中
                                         // 类比:就像"当前找到的最佳'索引卡片位置'"

  // ========== 第一部分:获取Lookup Context相关数据 ==========
  
  u32 lc_index = match->pkt.lc_index;            // 获取Lookup Context索引
                                         // lc_index:Lookup Context索引(从数据包的元数据中获取)
                                         // 类比:就像"获取'书架编号'"
  
  applied_hash_ace_entry_t **applied_hash_aces =
    vec_elt_at_index (am->hash_entry_vec_by_lc_index, lc_index);  // 获取应用条目向量
                                         // 类比:就像"获取'索引卡片数组'"

  hash_applied_mask_info_t **hash_applied_mask_info_vec =
    vec_elt_at_index (am->hash_applied_mask_info_vec_by_lc_index, lc_index);  // 获取掩码信息向量
                                         // 类比:就像"获取'分类信息数组'"

  hash_applied_mask_info_t *minfo;                // 掩码信息指针(临时变量)
                                         // 类比:就像"分类信息指针"

  DBG ("TRYING TO MATCH: %016llx %016llx %016llx %016llx %016llx %016llx",
       pmatch[0], pmatch[1], pmatch[2], pmatch[3], pmatch[4], pmatch[5]);  // 打印调试信息
                                         // 类比:就像"记录'尝试匹配'的信息"

  // ========== 第二部分:遍历所有掩码类型(按优先级排序) ==========
  
  for (order_index = 0; order_index < vec_len ((*hash_applied_mask_info_vec));
       order_index++)                             // 遍历所有掩码类型
    {
      minfo = vec_elt_at_index ((*hash_applied_mask_info_vec), order_index);  // 获取掩码信息
                                         // 类比:就像"获取'分类信息'"
      
      if (minfo->first_rule_index > curr_match_index)  // 如果第一个规则索引大于当前匹配索引
	{
	  /* Index in this and following (by construction) partitions are greater than our candidate, Avoid trying to match! */
                                         // 注释:此分区及后续分区(按构造)中的索引都大于我们的候选,避免尝试匹配!
	  
	  break;                              // 跳出循环(优化:后续分区优先级更低,无需检查)
                                         // 为什么需要?掩码信息向量按优先级排序,如果第一个规则索引大于当前匹配索引,说明后续分区优先级更低
                                         // 类比:就像"如果'第一本书位置'大于当前匹配位置,跳出循环(后续分类优先级更低)"
	}

      // ========== 第三部分:构建Hash表key ==========
      
      mask_type_index = minfo->mask_type_index;   // 获取掩码类型索引
                                         // 类比:就像"获取'分类编号'"
      
      ace_mask_type_entry_t *mte =
	vec_elt_at_index (am->ace_mask_type_pool, mask_type_index);  // 获取掩码类型条目
                                         // 类比:就像"获取'分类信息'"
      
      pmatch = (u64 *) match;                      // 重置匹配值指针
                                         // 类比:就像"重置'书籍匹配值'指针"
      
      pmask = (u64 *) & mte->mask;                // 获取掩码指针
                                         // 类比:就像"获取'分类掩码'指针"
      
      pkey = (u64 *) kv.key;                      // 获取Hash表key指针
                                         // 类比:就像"获取'字典索引的键'指针"
      
      /*
       * unrolling the below loop results in a noticeable performance increase.
       * int i;
       * for(i=0; i<6; i++) {
       *   kv.key[i] = pmatch[i] & pmask[i];
       * }
       */
                                         // 注释:展开下面的循环可以显著提高性能。
      
      // ========== 应用掩码到匹配值(构建Hash表key) ==========
      
      *pkey++ = *pmatch++ & *pmask++;             // 应用掩码到匹配值(第0个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第0部分)"
      
      *pkey++ = *pmatch++ & *pmask++;             // 应用掩码到匹配值(第1个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第1部分)"
      
      *pkey++ = *pmatch++ & *pmask++;             // 应用掩码到匹配值(第2个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第2部分)"
      
      *pkey++ = *pmatch++ & *pmask++;             // 应用掩码到匹配值(第3个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第3部分)"
      
      *pkey++ = *pmatch++ & *pmask++;             // 应用掩码到匹配值(第4个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第4部分)"
      
      *pkey++ = *pmatch++ & *pmask++;             // 应用掩码到匹配值(第5个u64)
                                         // 类比:就像"应用'分类掩码'到'书籍匹配值'(第5部分)"

      /*
       * The use of temporary variable convinces the compiler
       * to make a u64 write, avoiding the stall on crc32 operation
       * just a bit later.
       */
                                         // 注释:使用临时变量可以说服编译器进行u64写入,避免稍后在crc32操作上停顿。
      
      // ========== 设置Hash表key的元数据 ==========
      
      fa_packet_info_t tmp_pkt = kv_key->pkt;     // 获取数据包信息(临时变量)
                                         // 类比:就像"获取'书籍信息'(临时变量)"
      
      tmp_pkt.mask_type_index_lsb = mask_type_index;  // 设置掩码类型索引(低16位)
                                         // 类比:就像"设置'分类编号'到'书籍信息'"
      
      kv_key->pkt.as_u64 = tmp_pkt.as_u64;        // 设置数据包信息(使用临时变量,优化性能)
                                         // 类比:就像"设置'书籍信息'(使用临时变量,优化性能)"

      // ========== 第四部分:在Hash表中查找 ==========
      
      int res =
	clib_bihash_search_inline_2_48_8 (&am->acl_lookup_hash, &kv, &result);  // 在Hash表中查找
                                         // clib_bihash_search_inline_2_48_8:在双向Hash表中查找(内联优化版本)
                                         // 返回值:0=找到,非0=未找到
                                         // 类比:就像"在'字典索引系统'中查找'键'"

      if (res == 0)                                // 如果找到(Hash命中)
	{
	  /* There is a hit in the hash, so check the collision vector */
                                         // 注释:Hash中有命中,所以检查冲突向量
	  
	  u32 curr_index = result_val->applied_entry_index;  // 获取当前条目索引
                                         // applied_entry_index:Hash表value中的应用条目索引
                                         // 类比:就像"获取'索引卡片位置'"
	  
	  applied_hash_ace_entry_t *pae =
	    vec_elt_at_index ((*applied_hash_aces), curr_index);  // 获取应用条目指针
                                         // 类比:就像"获取'索引卡片'指针"
	  
	  collision_match_rule_t *crs = pae->colliding_rules;  // 获取冲突规则向量指针
                                         // colliding_rules:冲突规则向量(Hash冲突的规则列表)
                                         // 类比:就像"获取'冲突书籍列表'指针"
	  
	  // ========== 第五部分:遍历冲突规则向量,查找最佳匹配 ==========
	  
	  int i;
	  for (i = 0; i < vec_len (crs); i++)         // 遍历冲突规则向量
	    {
	      if (crs[i].applied_entry_index >= curr_match_index)  // 如果冲突规则索引大于等于当前匹配索引
		{
		  continue;                            // 跳过(优先级更低,无需检查)
                                         // 为什么需要?冲突规则向量按优先级排序,如果索引大于等于当前匹配索引,说明优先级更低
                                         // 类比:就像"如果'冲突书籍位置'大于等于当前匹配位置,跳过(优先级更低)"
		}
	      
	      if (single_rule_match_5tuple (&crs[i].rule, is_ip6, match))  // 如果冲突规则匹配
		{
		  curr_match_index = crs[i].applied_entry_index;  // 更新当前匹配索引
                                         // 为什么需要?选择优先级最高的匹配规则(索引越小,优先级越高)
                                         // 类比:就像"更新当前匹配位置(选择优先级最高的'冲突书籍')"
		}
	    }
	}
    }
  
  DBG ("MATCH-RESULT: %d", curr_match_index);      // 打印调试信息
                                         // 类比:就像"记录'匹配结果'的信息"
  
  return curr_match_index;                         // 返回当前匹配索引
                                         // 类比:就像"返回当前匹配位置"
}

函数总结

这个函数实现了完整的Hash匹配逻辑:

  1. 获取Lookup Context相关数据:获取应用条目向量和掩码信息向量
  2. 遍历所有掩码类型:按优先级排序,从高到低遍历
  3. 构建Hash表key:应用掩码到匹配值,设置元数据
  4. 在Hash表中查找:使用Hash表查找匹配的规则
  5. 处理Hash冲突:如果Hash命中,遍历冲突规则向量,查找最佳匹配(优先级最高的规则)

匹配优先级

  • 掩码类型优先级:掩码信息向量按优先级排序(位置越靠前,优先级越高)
  • 规则优先级:冲突规则向量按优先级排序(索引越小,优先级越高)
  • 最佳匹配:选择优先级最高的匹配规则(索引最小的规则)

类比:就像完整的"使用字典查找书籍"流程:

  1. 获取数据:获取"索引卡片数组"和"分类信息数组"
  2. 遍历分类:按优先级排序,从高到低遍历"分类"
  3. 构建键:应用"分类掩码"到"书籍匹配值",设置元数据
  4. 查找键:在"字典索引系统"中查找"键"
  5. 处理冲突:如果找到,遍历"冲突书籍列表",查找最佳匹配(优先级最高的书籍)

10.8 本章小结

通过这一章的详细讲解,我们了解了:

  1. Hash匹配引擎的概念:什么是Hash匹配,为什么需要它
  2. 数据结构:Hash ACE信息、Hash ACL信息、应用条目、冲突规则等
  3. Hash表构建:如何将ACL规则添加到Hash表
  4. 掩码类型分配:如何为规则分配掩码类型索引
  5. Hash表条目激活:如何将规则添加到Hash表,处理Hash冲突
  6. TupleMerge算法:如何通过放松掩码来合并相似规则
  7. Hash匹配流程:如何使用Hash表快速查找匹配的规则

核心要点

  • Hash匹配将匹配时间复杂度从O(M)降低到O(1)平均情况
  • 掩码类型用于减少Hash表的数量,提高查找效率
  • TupleMerge算法通过放松掩码来合并相似规则,提高内存利用率
  • Hash冲突处理通过冲突规则向量来存储Hash冲突的规则
  • 匹配优先级通过掩码类型和规则索引来确定(索引越小,优先级越高)

下一步:在下一章,我们会看到ACL-as-a-service的详细实现,包括Lookup Context管理、导出方法等。


相关推荐
qq_339191142 小时前
ubuntu 配置ulimit -n , ubuntu配置文件描述符数量, ubuntu优化,ubuntu系统调优
linux·运维·ubuntu
上河雨滴2 小时前
win11 环境下,有线网络识别问题bug
网络
老蒋新思维2 小时前
创客匠人推演:当知识IP成为“数字心智”的架构师——论下一代认知服务的形态
网络·人工智能·网络协议·tcp/ip·机器学习·创始人ip·创客匠人
xiufeia2 小时前
(5)应用层
计算机网络
逆流°只是风景-bjhxcc3 小时前
【网络】ipv4和ipv6的区别
网络
Yan-英杰3 小时前
从Free Tier到Serverless:用亚马逊云科技打造零门槛AI应用
服务器·开发语言·科技·ai·大模型
Web极客码3 小时前
Wordpress如何调整区块高度与宽度
服务器·主题·wordpress
WG_173 小时前
Linux:基础IO(18+19)+文件描述符
linux·运维·服务器