本篇文章主要讲解第四部分,数据平面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.1 ACL插件的文件组织结构
- 2.2 各文件的功能和职责
- 2.3 模块间的依赖关系
- 2.4 模块与外部系统的关系
- 2.5 文件组织的设计原则
- 2.6 总结:文件组织的"设计哲学"
-
- 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 数据结构之间的关系小结
- 3.1
第三部分:ACL插件的初始化和控制平面
-
第4章:模块初始化------ACL插件是如何"开机启动"的?
- 4.1 插件注册:告诉VPP"我是谁"
- 4.2 初始化函数注册:告诉VPP"什么时候叫我"
- 4.3 初始化主函数:acl_init 的完整流程
- 4.4 初始化流程总结:从"冷启动"到"就绪"
- 4.5 关键概念深入理解
- 4.6 初始化完成后的状态
- 4.7 本章小结
-
- 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.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_inline与acl_plugin_match_5tuple_inline - 20.5 本章小结
- 20.1
第六部分:多核和性能优化
-
第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.1 边界防火墙配置
- 26.2 内部安全区隔离
- 26.3 机房接入控制
- 26.4 多ACL串联配置
- 26.5 有状态ACL配置
- 26.6 本章总结
-
- 27.1 Hash匹配启用建议
- 27.2 TupleMerge参数调优
- 27.3 会话超时配置建议
- 27.4 多核配置优化
- 27.5 规则组织最佳实践
- 27.6 本章总结
-
- 28.1 常见问题诊断
- 28.2 规则匹配问题排查
- 28.3 会话表问题排查
- 28.4 性能问题排查
- 28.5 调试工具使用
- 28.6 本章总结
-
- 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的结束节点
// 类比:就像"安检点"在"安检路径"中的"位置"(在路径结束之前)
};
节点注册总结:
- 节点定义 :使用
VLIB_REGISTER_NODE宏定义节点 - Feature注册 :使用
VNET_FEATURE_INIT宏将节点注册到Feature Arc - 运行顺序 :通过
runs_before定义节点在Feature Arc中的运行顺序
类比:就像在"机场安检路径"中注册"安检点":
- 定义安检点:确定"安检点"的名称、类型、错误处理等
- 注册到路径:将"安检点"注册到"安检路径"中
- 确定位置:确定"安检点"在"安检路径"中的"位置"(在哪个步骤之前)
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路径的安检点"调用"核心安检函数"
}
节点函数参数:
vlib_main_t * vm:VPP主结构体(包含全局状态)vlib_node_runtime_t * node:节点运行时结构体(包含节点状态)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匹配、会话管理等)
// 类比:就像"执行安检"(检查旅客、管理会话等)
}
函数总结:
- 检查Trace:是否启用Trace(用于调试)
- 获取配置:获取会话重分类标志
- 准备数据:调用准备函数(提取5-tuple、计算Hash等)
- 处理数据包:调用内部处理函数(执行ACL匹配、会话管理等)
类比:就像"安检流程":
- 检查记录:是否启用"安检记录"
- 获取配置:获取"会话重分类"配置
- 准备检查:提取"旅客信息"、计算"Hash"等
- 执行检查:检查"旅客"、管理"会话"等
8.4 5-tuple提取:如何"读取旅客信息"?
8.4.1 什么是5-tuple?
5-tuple(五元组):用于标识网络连接的五元组,包括:
- 源IP地址(Source IP Address)
- 目的IP地址(Destination IP Address)
- 协议号(Protocol Number,如TCP=6、UDP=17)
- 源端口(Source Port)
- 目的端口(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层数据(我们下面详细讲解)
// 类比:就像"提取协议和端口"(协议号、源端口、目的端口)
}
函数总结:
- 计算L3偏移量:根据路径类型(L2路径或IP路径)和方向(输入或输出)计算L3层偏移量
- 提取L3数据:提取源IP地址和目的IP地址
- 提取L4数据:提取协议号、源端口、目的端口等
类比:就像"读取旅客信息":
- 确定位置:确定"IP头"在"数据包"中的位置
- 读取IP地址:读取"源IP地址"和"目的IP地址"
- 读取端口和协议:读取"协议号"、"源端口"、"目的端口"
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地址"(源地址、目的地址)
}
}
函数总结:
- 判断地址族 :根据
is_ip6判断是IPv4还是IPv6 - 提取IP地址:从IP头中提取源IP地址和目的IP地址
- 处理填充:IPv4需要清零填充字段(用于对齐)
类比:就像"读取旅客的出发地和目的地":
- 判断类型:判断是"国内航班"(IPv4)还是"国际航班"(IPv6)
- 读取地址:读取"出发地"和"目的地"
- 处理格式:根据类型处理不同的格式
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:数据包信息(地址族、片段标志等)
}
函数总结:
- 提取协议号:从IP头中提取协议号
- 处理IPv6扩展头:如果是IPv6,可能需要跳过扩展头
- 处理Fragment:如果是Fragment,需要特殊处理(非首片段没有L4信息)
- 提取端口:根据协议类型提取端口(TCP/UDP)或类型/代码(ICMP)
- 提取TCP标志:如果是TCP,提取TCP标志位
类比:就像"读取旅客的交通方式和端口信息":
- 读取协议:读取"交通方式"(TCP、UDP、ICMP等)
- 处理扩展:如果是"国际航班"(IPv6),可能需要处理"中转信息"(扩展头)
- 处理片段:如果是"分片运输"(Fragment),需要特殊处理
- 读取端口:读取"出发地端口"和"目的地端口"
- 读取标志:如果是"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语义(找到第一个匹配的规则就返回):
- 检查ACL存在性:如果ACL不存在,拒绝流量
- 遍历规则:按顺序检查ACL中的每个规则
- 地址族匹配:检查数据包的地址族是否与规则一致
- IP地址匹配:检查源IP和目的IP是否匹配(支持前缀匹配)
- 协议匹配:如果规则指定了协议,检查协议是否匹配
- 端口匹配:如果规则指定了端口,检查源端口和目的端口是否匹配
- TCP标志匹配:如果是TCP协议,检查TCP标志是否匹配
- 返回结果:如果所有检查都通过,返回匹配结果(动作、ACL索引、规则索引)
类比:就像"安检员检查旅客":
- 检查规则表:确保"安检规则表"存在
- 遍历规则:按顺序检查"安检规则表"中的每个规则
- 检查类型:检查"旅客类型"是否匹配(国内/国际)
- 检查地址:检查"出发地"和"目的地"是否匹配
- 检查交通方式:如果规则指定了"交通方式",检查是否匹配
- 检查端口:如果规则指定了"端口",检查是否匹配
- 检查标志:如果是"TCP航班",检查"TCP标志"是否匹配
- 返回结果:如果所有检查都通过,返回"放行"或"拒绝"
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(匹配)
// 类比:就像"前缀完全匹配,返回匹配"
}
}
函数总结:
-
IPv4地址匹配:
- 如果前缀长度为0,总是匹配
- 否则,计算前缀掩码,只比较前缀部分
-
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(不匹配)
// 类比:就像"如果没有'安检规则表'定义,拒绝"
}
函数总结:
- 获取Lookup Context:从Lookup Context中获取ACL索引向量
- 遍历ACL:按顺序检查每个ACL
- 匹配ACL :调用
single_acl_match_5tuple匹配当前ACL - 返回结果:如果找到匹配,返回匹配结果(动作、ACL位置等)
类比:就像"检查多个安检规则表":
- 获取规则表列表:从"安检规则表集合"中获取"安检规则表列表"
- 遍历规则表:按顺序检查每个"安检规则表"
- 匹配规则表:调用"单个规则表匹配函数"匹配当前"安检规则表"
- 返回结果:如果找到匹配,返回匹配结果(放行/拒绝、规则表位置等)
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(不匹配)
// 类比:就像"如果没有找到匹配,拒绝"
}
函数总结:
- Hash查找:从Hash表中查找匹配的ACE索引
- 获取ACE条目:如果找到匹配,获取ACE条目
- 返回结果:返回匹配结果(动作、ACL索引、规则索引等)
类比:就像"使用索引卡快速查找":
- 查找索引卡:从"索引卡"中快速查找匹配的"规则"
- 获取规则条目:如果找到匹配,获取"规则条目"
- 返回结果:返回匹配结果(放行/拒绝、规则表索引、规则索引等)
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规则。
关键概念:
- 会话(Session):一个网络连接的状态信息(5-tuple、超时时间等)
- 会话表(Session Table):存储所有会话的Hash表
- 会话超时:会话在空闲一段时间后自动删除
类比:就像"常客系统":
- 会话:就像"常客信息"(旅客信息、访问记录等)
- 会话表:就像"常客数据库"(存储所有常客信息)
- 会话超时:就像"常客过期"(长时间不访问,从数据库中删除)
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表中查找匹配的会话
// 类比:就像"从'常客数据库'中查找匹配的'常客信息'"
会话查找逻辑:
- 计算Hash值:根据5-tuple和接口索引计算Hash值
- Hash表查找:使用Hash值在会话Hash表中查找
- 返回会话ID:如果找到匹配,返回会话ID;否则返回无效ID
类比:就像"查找常客信息":
- 计算Hash值:根据"旅客信息"计算"Hash值"
- 数据库查找:使用"Hash值"在"常客数据库"中查找
- 返回结果:如果找到匹配,返回"常客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:丢弃
类比:就像完整的"安检流程":
- 旅客到达(数据包到达节点)
- 准备检查(提取5-tuple、计算Hash)
- 查找常客(查找会话)
- 执行安检(ACL匹配)
- 记录信息(创建会话)
- 决定去向(设置动作)
- 返回结果(继续转发或丢弃)
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 本章小结
通过这一章的详细讲解,我们了解了:
- 数据平面ACL匹配的概念:什么是数据平面,VPP的架构
- 数据平面节点注册:如何将ACL节点插入到数据包处理流程
- 5-tuple提取:如何从数据包中提取5-tuple(源IP、目的IP、协议、源端口、目的端口)
- ACL匹配逻辑:如何匹配单个ACL(First-match语义)
- 多ACL匹配:如何匹配多个ACL(线性匹配和Hash匹配)
- 会话管理:如何管理Flow-aware会话(查找、创建、超时)
- 性能优化技术:向量化处理、预取等
核心要点:
- 数据平面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规则。
关键概念:
-
会话(Session):一个网络连接的状态信息
- 5-tuple:源IP、目的IP、协议、源端口、目的端口
- 状态信息:TCP标志、最后活跃时间、超时类型等
- 类比:就像"常客信息"(旅客信息、访问记录等)
-
会话表(Session Table):存储所有会话的Hash表
- IPv4会话表 :使用
clib_bihash_16_8_t(16字节key,8字节value) - IPv6会话表 :使用
clib_bihash_40_8_t(40字节key,8字节value) - 类比:就像"常客数据库"(存储所有常客信息)
- IPv4会话表 :使用
-
会话超时(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) - 类比:就像"常客过期"(长时间不访问,从数据库中删除)
- TCP Established :24小时(
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;
结构体字段总结:
- 基本信息:5-tuple、接口索引、线程索引、地址族
- 状态信息:TCP标志、最后活跃时间
- 链表信息:链表前后节点、链表ID、入队时间
- 管理信息:删除标志、保留字段
类比:就像"常客档案":
- 基本信息:姓名、身份证号、出发地、目的地、通道、地址族
- 状态信息:访问记录、最后访问时间
- 链表信息:在"待检查列表"中的位置、列表类型、加入时间
- 管理信息:是否已标记删除、保留字段
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'"
}
函数总结:
这个函数完成了以下工作:
- 验证参数:检查会话ID是否有效
- 分配内存:从pool中分配会话内存(缓存行对齐)
- 填充基本信息:复制5-tuple、设置接口索引、线程索引、Policy Epoch
- 初始化状态:初始化TCP标志、最后活跃时间、链表信息等
- 添加到链表:将会话添加到超时链表(根据超时类型)
- 添加到Hash表:将会话添加到Hash表(用于快速查找)
- 创建反向会话:创建反向会话(用于匹配反向流量)
- 更新统计:更新会话添加统计信息
类比:就像完整的"新常客登记流程":
- 验证信息:检查"常客信息"是否有效
- 分配档案:从"档案柜"中分配新的"常客档案夹"
- 填写基本信息:填写"姓名"、"身份证号"、"出发地"、"目的地"等
- 初始化状态:初始化"访问记录"、"最后访问时间"等
- 加入列表:将"常客"添加到"待检查列表"(根据类型)
- 加入数据库:将"常客"添加到"常客数据库"(用于快速查找)
- 创建反向记录:创建"反向常客记录"(用于匹配返回流量)
- 更新统计:更新"常客添加统计"
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=未找到)
// 类比:就像"返回'查找结果'"(找到或未找到)
}
函数总结:
- 判断地址族 :根据
is_ip6选择IPv4或IPv6 Hash表 - Hash表查找:使用Hash值和5-tuple在Hash表中查找
- 返回结果:如果找到,返回会话ID;否则返回无效值
类比:就像"在'常客数据库'中查找'常客'":
- 判断类型:判断是"国内航班"(IPv4)还是"国际航班"(IPv6)
- 数据库查找:在对应的"常客数据库"中查找
- 返回结果:如果找到,返回"常客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(允许通过)
// 类比:就像"返回'放行'"
}
函数总结:
- 更新最后活跃时间:将会话的最后活跃时间更新为当前时间
- 更新TCP标志位:如果TCP标志有效且发生变化,更新TCP标志位
- 返回动作:返回permit(允许通过)
类比:就像"更新常客访问记录":
- 更新访问时间:更新"常客"的"最后访问时间"
- 更新访问记录:如果"访问记录"发生变化,更新"访问记录"
- 返回结果:返回"放行"
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超时类型(默认)
// 类比:就像"如果'常客'使用'其他航班',返回'默认类型'"
}
}
函数总结:
- 提取TCP标志:从会话的TCP标志位中提取关键标志(RST、FIN、ACK、SYN)
- 判断协议类型:根据协议类型(TCP、UDP、其他)选择超时类型
- 判断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; // 返回超时时间
// 类比:就像"返回'过期时间'"
}
函数总结:
- 获取时钟周期:获取每秒时钟周期数(用于时间转换)
- 判断链表类型:如果会话在Purgatory链表中,使用Purgatory超时时间(10微秒)
- 计算超时时间:根据超时类型(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:获取会话的超时时间(我们上面已经讲解过)
// 类比:就像"设置'列表头'的'过期时间'为当前时间 + '常客过期时间'"
}
}
函数总结:
这个函数实现了双向链表的尾部插入:
- 选择链表:根据删除标志和超时类型选择链表(Purgatory、TCP Established、TCP Transient、UDP Idle)
- 验证线程:确保会话属于正确的线程
- 设置链表信息:设置会话的链表信息(前一个节点、下一个节点、链表ID、入队时间)
- 更新链表结构:更新双向链表的结构(前一个节点的下一个节点、链表尾)
- 更新链表头:如果链表为空,设置链表头和过期时间
类比:就像"将常客添加到待检查列表":
- 选择列表:根据"常客类型"选择"待检查列表"(VIP列表、普通列表等)
- 验证通道:确保"常客"属于正确的"安检通道"
- 设置位置:设置"常客"在"列表"中的位置(前一个常客、下一个常客、列表类型、加入时间)
- 更新列表:更新"列表"的结构(前一个常客的下一个常客、列表尾)
- 更新列表头:如果"列表"为空,设置"列表头"和"过期时间"
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表
// 类比:就像"将'反向常客记录'添加到'常客数据库'"
}
}
函数总结:
- 交换IP地址:交换源IPv6地址和目的IPv6地址
- 复制会话ID:反向会话和正向会话使用相同的会话ID
- 交换端口和方向:根据协议类型(TCP/UDP或ICMP)交换端口和方向标志
- 添加到Hash表:将反向会话添加到Hash表
类比:就像"创建反向常客记录":
- 交换地址:交换"出发地"和"目的地"
- 复制档案:反向记录和正向记录指向同一个"常客档案"
- 交换端口:交换"出发地端口"和"目的地端口",并切换"方向标志"
- 加入数据库:将"反向常客记录"添加到"常客数据库"
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)
// 类比:就像"返回'动作'"(放行或拒绝)
}
函数总结:
- 获取会话:从pool中获取会话结构体指针
- 跟踪状态:更新会话的最后活跃时间和TCP标志位
- 处理类型变化:如果超时类型发生变化,重启会话定时器
- 验证一致性:验证会话的接口索引与数据包的接口索引一致(防止Hash冲突)
- 返回动作:返回permit(允许通过)
类比:就像"处理已有常客的数据包":
- 获取档案:从"档案柜"中获取"常客档案夹"
- 更新记录:更新"常客"的"访问记录"
- 处理类型变化:如果"常客类型"变化,从"旧列表"移到"新列表"
- 验证一致性:验证"常客"的"通道编号"与"数据包"的"通道编号"一致
- 返回结果:返回"放行"
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; // 返回成功
// 类比:就像"返回'成功'"
}
两阶段删除的原因:
-
第一阶段(停用):从Hash表移除会话,标记为删除,但保留会话结构体
- 原因:正在处理的数据包可能还在使用会话结构体
- 类比:就像"从'常客数据库'中移除'常客记录',但保留'档案夹'(因为可能还在使用)"
-
第二阶段(删除):从Purgatory链表中删除会话,释放内存
- 原因:等待一段时间(10微秒),确保没有数据包在使用会话结构体
- 类比:就像"等待一段时间后,释放'档案夹'"
类比:就像"两阶段注销常客":
- 第一阶段:从"常客数据库"中移除"常客记录",但保留"档案夹"(因为可能还在使用)
- 第二阶段:等待一段时间后,释放"档案夹"
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:全局会话停用计数
// 类比:就像"原子增加'全局常客停用计数'"
}
函数总结:
- 验证线程:确保会话属于当前线程
- 从Hash表移除:从IPv4或IPv6会话Hash表中移除会话(包括反向会话)
- 标记删除:设置会话的删除标志为1
- 更新统计:更新会话停用统计信息
类比:就像"停用常客":
- 验证通道:确保"常客"属于当前"安检通道"
- 移除记录:从"常客数据库"中移除"常客记录"(包括反向记录)
- 标记删除:标记"常客"为"已删除"
- 更新统计:更新"常客停用统计"
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:全局会话删除计数
// 类比:就像"原子增加'全局常客删除计数'"
}
函数总结:
- 获取线程数据:获取当前线程的Per-worker数据
- 释放内存:将会话索引归还给pool(释放内存)
- 更新统计:更新会话删除统计信息
类比:就像"释放常客档案":
- 获取数据:获取当前"安检通道"的"数据"
- 释放档案:将"常客档案夹"放回"档案柜",标记为可用
- 更新统计:更新"常客删除统计"
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; // 返回总过期会话数量
// 类比:就像"返回'总过期常客数量'"
}
函数总结:
这个函数实现了会话清理的核心逻辑:
- 处理变更请求:处理会话变更请求(如重新调度)
- 检查超时链表:遍历所有超时链表,检查是否有过期会话
- 处理过期会话:对于过期会话,执行两阶段删除;对于未过期会话,重新添加到链表
清理策略:
- 批量清理 :每次最多清理
fa_max_deleted_sessions_per_interval个会话(避免一次性清理太多) - 时间检查:如果链表头节点未到过期时间,不继续检查(优化性能)
- 两阶段删除:使用两阶段删除机制,确保安全
类比:就像"清理过期常客":
- 处理请求:处理"常客变更请求"(如重新调度)
- 检查列表:遍历所有"待检查列表",检查是否有"过期常客"
- 处理过期:对于"过期常客",执行"两阶段删除";对于"未过期常客",重新添加到"列表"
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"(进程持续运行)
}
函数总结:
这个函数实现了会话清理进程的核心逻辑:
- 处理事件:处理清理事件(如按接口删除)
- 计算唤醒时间:遍历所有Worker线程,计算最早的过期时间
- 唤醒Worker线程:唤醒所有Worker线程进行清理
- 等待:等待事件或超时,然后继续循环
清理进程的工作流程:
- 主进程:定期检查所有Worker线程的会话链表,计算最早的过期时间
- Worker线程 :被唤醒后,执行
acl_fa_check_idle_sessions清理过期会话 - 循环:主进程等待一段时间后,再次检查并唤醒Worker线程
类比:就像"定期清理过期常客":
- 主管理员:定期检查所有"安检通道"的"待检查列表",计算最早的"过期时间"
- 安检通道:被唤醒后,执行"清理过期常客"操作
- 循环:主管理员等待一段时间后,再次检查并唤醒"安检通道"
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(未删除)
// 类比:就像"返回'未删除'"
}
过期会话检测逻辑:
- 获取当前Policy Epoch:从接口获取当前Policy Epoch
- 比较Policy Epoch :
- 方向标志匹配(MSB匹配):确保是同一方向(输入或输出)
- 版本号不匹配(LSB不匹配):说明规则已变化
- 删除会话:如果检测到过期会话,执行两阶段删除
为什么需要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; // 返回成功
// 类比:就像"返回'成功'"
}
函数总结:
这个函数实现了双向链表的节点删除:
- 验证线程:确保会话属于当前线程
- 更新前一个节点:更新前一个节点的下一个节点索引
- 更新下一个节点:更新下一个节点的前一个节点索引,并更新链表头节点的过期时间(如果需要)
- 重置会话信息:重置会话的链表信息
类比:就像"从待检查列表中删除常客":
- 验证通道:确保"常客"属于当前"安检通道"
- 更新前一个:更新"前一个常客"的"下一个常客"
- 更新下一个:更新"下一个常客"的"前一个常客",并更新"列表头"的"过期时间"(如果需要)
- 重置信息:重置"常客"的"前一个常客"和"下一个常客"
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:最大会话数(在初始化时配置)
// 类比:就像"检查'当前常客数量'是否小于'最大常客数'"
}
函数总结:
- 计算当前会话数:当前会话数 = 添加计数 - 删除计数
- 检查限制:检查当前会话数是否小于最大会话数
类比:就像"检查是否可以添加新常客":
- 计算数量:计算"当前常客数量" = "添加计数" - "删除计数"
- 检查限制:检查"当前常客数量"是否小于"最大常客数"
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; // 返回成功
// 类比:就像"返回'成功'"
}
函数总结:
- 回收Purgatory会话:尝试从Purgatory链表中回收已过期的会话
- 回收TCP Transient会话:如果Purgatory链表为空,尝试回收TCP Transient会话(优先级较低)
回收策略:
- 优先级1:Purgatory会话(已标记为删除,可以立即回收)
- 优先级2:TCP Transient会话(未建立连接的TCP会话,优先级较低)
类比:就像"回收常客档案":
- 回收待删除:尝试回收"待删除常客"(已标记为删除,可以立即回收)
- 回收未建立连接:如果"待删除常客"为空,尝试回收"未建立连接常客"(优先级较低)
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检查:如果启用会话重分类,检查会话是否过期
类比:就像完整的"新常客登记和管理流程":
- 旅客到达(数据包到达节点)
- 准备检查(提取5-tuple、计算Hash)
- 查找常客(查找会话)
- 执行安检(ACL匹配)
- 检查限制(检查是否可以添加新常客)
- 登记常客(创建会话)
- 更新记录(跟踪会话状态)
- 返回结果(放行)
- 定期清理(清理过期常客)
9.18 本章小结
通过这一章的详细讲解,我们了解了:
- Flow-aware ACL的概念:什么是有状态ACL,为什么需要它
- 会话结构体:如何存储会话信息(5-tuple、状态信息、链表信息等)
- 会话创建 :
acl_fa_add_session的完整流程(逐行注释) - 会话查找:如何使用Hash表快速查找会话
- 会话跟踪:如何更新会话状态(最后活跃时间、TCP标志等)
- 会话超时:如何判断会话的超时类型和超时时间
- 会话链表管理:如何管理超时链表(添加、删除)
- 会话删除:两阶段删除机制(停用、释放)
- 会话清理:如何定期清理过期会话
- Policy Epoch机制:如何检测过期会话
- 反向会话:如何匹配返回流量
核心要点:
- 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是不同掩码组合的数量)。
关键概念详解:
-
线性匹配(Linear Matching):逐条检查ACL规则,直到找到匹配的规则
- 时间复杂度:O(M),M是规则数量
- 优点:实现简单,小规模规则时性能好
- 缺点:规则数量多时,性能下降明显
- 类比:就像"一本一本地翻书"(从第一本开始,逐本查找)
-
Hash匹配(Hash Matching):使用Hash表存储规则,通过计算Hash值直接定位规则
- 时间复杂度:O(1)平均情况(如果Hash冲突少)
- 优点:规则数量多时,性能稳定
- 缺点:需要额外的内存和构建时间
- 类比:就像"使用字典"(根据书名计算索引,直接定位到书架位置)
-
掩码类型(Mask Type):具有相同掩码模式的规则集合
- 掩码(Mask):用于指定哪些字段需要匹配(1表示需要匹配,0表示不关心)
- 掩码类型:具有相同掩码模式的规则被归类为同一个掩码类型
- 为什么需要?:减少Hash表的数量,提高查找效率
- 类比:就像"书籍分类"(相同类型的书放在同一个书架)
-
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;
结构体字段总结:
- 匹配值:规则的5-tuple匹配值(用于构建Hash表key)
- 索引信息:ACL索引和ACE索引(用于追踪规则的来源)
- 掩码类型:基础掩码类型索引(用于掩码管理)
- 动作:规则的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;
结构体字段总结:
- Lookup Context列表:应用此ACL的Lookup Context索引列表
- 规则数组:ACL中所有规则的Hash表示信息
- 初始化标志: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;
结构体字段总结:
- 原始信息:ACL索引、ACE索引(用于追踪规则的来源)
- Hash信息:Hash ACE信息索引、掩码类型索引(用于Hash表查找)
- 冲突处理:冲突头条目索引、冲突规则向量(用于处理Hash冲突)
- 统计信息:命中计数(用于性能统计)
- 优先级信息:ACL位置(用于确定规则优先级)
- 动作: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;
结构体字段总结:
- 原始规则:完整的ACL规则(用于精确匹配)
- 索引信息:ACL索引、ACE索引(用于追踪规则的来源)
- 优先级信息:ACL位置(用于确定规则优先级)
- 应用条目索引:规则在应用条目数组中的索引(用于快速定位)
类比:就像"冲突书籍的详细信息卡片":
- 详细信息:书籍的完整信息(原始规则)
- 书架信息:所属书架编号、位置编号(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;
结构体字段总结:
- 应用条目索引:规则在应用条目数组中的索引(用于快速定位)
- 保留字段:用于未来扩展
类比:就像"索引卡片的编号":
- 位置编号:书籍在索引卡片数组中的位置(应用条目索引)
- 保留字段:用于未来扩展(如添加更多信息)
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;
结构体字段总结:
- 掩码类型索引:掩码类型索引(用于查找掩码类型)
- 第一个规则索引:使用此掩码类型的第一个规则索引(用于优化查找顺序)
- 统计信息:条目数量、最大冲突数(用于性能分析)
类比:就像"书籍分类的统计信息":
- 分类编号:书籍分类的编号(掩码类型索引)
- 第一本书:此分类中第一本书的位置(第一个规则索引)
- 统计信息:书籍数量、最大冲突数(条目数量、最大冲突数)
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:重建掩码信息向量
// 作用:根据当前的应用条目,重新构建掩码信息向量(统计信息)
// 类比:就像"重建'分类统计信息'"
}
函数总结:
这个函数完成了以下工作:
- 初始化Hash表:如果Hash表未初始化,初始化它
- 验证和准备数据结构:确保所有必要的向量有足够的空间
- 记录ACL应用信息:将ACL添加到应用列表,防止重复应用
- 准备掩码信息向量:预分配空间,提高性能
- 处理每个规则 :
- 创建应用条目
- 填充应用条目信息
- 分配掩码类型索引(可能使用TupleMerge优化)
- 激活Hash表条目(添加到Hash表,处理Hash冲突)
- 检查冲突数量并可能分割(如果使用TupleMerge)
- 重建掩码信息向量:根据当前的应用条目,重新构建掩码信息向量
类比:就像完整的"建立字典索引系统"流程:
- 初始化系统:初始化"字典索引系统"
- 准备空间:确保所有"数组"有足够的空间
- 记录信息:将"书架"添加到"应用列表"
- 预分配空间:预分配"索引卡片数组"的空间
- 处理每本书 :
- 创建"索引卡片"
- 填写"索引卡片"信息
- 分配"分类编号"(可能使用合并优化)
- 添加到"字典索引系统"(处理冲突)
- 检查冲突并可能分割(如果冲突太多)
- 重建统计信息:重建"分类统计信息"
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:直接分配掩码类型索引
// 作用:查找或创建掩码类型索引(不进行放松)
// 类比:就像"直接分配'分类编号'(不进行合并优化)"
}
}
函数总结:
- 获取基础掩码:从规则的基础掩码类型条目中获取掩码
- 选择分配方式 :
- 使用TupleMerge:使用TupleMerge分配掩码类型索引(可能放松掩码)
- 不使用TupleMerge:直接分配掩码类型索引(不放松掩码)
类比:就像"给书籍分配分类编号":
- 获取原始分类:从"原始分类信息"中获取"分类掩码"
- 选择分配方式 :
- 使用合并优化:使用"合并优化"分配"分类编号"(可能合并相似分类)
- 不使用合并优化:直接分配"分类编号"(不合并)
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; // 返回掩码类型索引
// 类比:就像"返回'分类编号'"
}
函数总结:
- 查找掩码类型:在掩码类型pool中查找是否存在相同的掩码类型
- 创建新掩码类型:如果未找到,创建新的掩码类型条目
- 增加引用计数:增加掩码类型的引用计数(表示有多少规则使用此掩码类型)
- 返回索引:返回掩码类型索引
类比:就像"给书籍分配分类编号":
- 查找分类:在"分类信息池"中查找是否存在相同的"分类"
- 创建新分类:如果未找到,创建新的"分类信息"
- 增加使用计数:增加"分类"的"使用计数"(表示有多少书籍使用此分类)
- 返回编号:返回"分类编号"
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; // 返回新索引
// 类比:就像"返回'新索引卡片位置'"
}
}
函数总结:
- 填充Hash表key-value对:根据应用条目,构建Hash表的key和value
- 在Hash表中查找:检查是否存在Hash冲突
- 处理查找结果 :
- 如果找到(Hash冲突):将新规则添加到冲突规则向量,返回第一个条目索引
- 如果未找到(无Hash冲突):添加到Hash表,将自己添加到冲突规则向量,返回新索引
类比:就像"将书籍添加到字典索引系统":
- 填充键值对:根据"索引卡片",构建"字典索引的键值对"
- 查找键:检查"字典索引系统"中是否存在相同的"键"
- 处理结果 :
- 如果找到(冲突):将"新索引卡片"添加到"冲突书籍列表",返回"第一个索引卡片位置"
- 如果未找到(无冲突):添加到"字典索引系统",将自己添加到"冲突书籍列表",返回"新索引卡片位置"
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中)
// 为什么需要?用于快速定位应用条目(在查找时)
// 类比:就像"设置'索引卡片位置'到'字典索引的值'"
}
函数总结:
- 获取指针:获取匹配值、掩码、Hash表key的指针
- 应用掩码:将掩码应用到匹配值(构建Hash表key)
- 设置元数据:设置掩码类型索引和Lookup Context索引(存储在key的元数据中)
- 设置value:设置应用条目索引(存储在value中)
类比:就像"填充字典索引的键值对":
- 获取指针:获取"书籍匹配值"、"分类掩码"、"字典索引的键"的指针
- 应用掩码:将"分类掩码"应用到"书籍匹配值"(构建"字典索引的键")
- 设置元数据:设置"分类编号"和"书架编号"(存储在"键"的元数据中)
- 设置值:设置"索引卡片位置"(存储在"值"中)
10.6 TupleMerge算法:如何"合并相似分类"?
10.6.1 TupleMerge算法概述
TupleMerge算法:一种动态优化掩码的算法,通过"放松"掩码来合并相似的规则,减少Hash表的数量,提高内存利用率。
核心思想:
- 掩码放松(Mask Relaxation):减少掩码的精确度,使更多规则可以共享同一个Hash表
- 掩码包含检查(Mask Containment Check):检查一个掩码是否可以包含另一个掩码
- 动态分割(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; // 返回掩码类型索引
// 类比:就像"返回'分类编号'"
}
函数总结:
- 查找兼容掩码:从后往前遍历掩码信息向量,查找可以包含输入掩码的现有掩码类型
- 创建新掩码类型:如果未找到,创建新的放松掩码类型
- 添加到掩码信息向量:将新掩码类型添加到掩码信息向量
类比:就像"使用合并优化分配分类编号":
- 查找兼容分类:从后往前遍历"分类信息数组",查找可以包含"新分类"的现有分类
- 创建新分类:如果未找到,创建新的"放松分类"(减少精确度)
- 添加到数组:将新分类添加到"分类信息数组"
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(未匹配)
// 类比:就像"返回'未匹配'"
}
函数总结:
- 获取应用条目向量:根据Lookup Context索引获取应用条目向量
- 查找匹配规则 :调用
multi_acl_match_get_applied_ace_index在Hash表中查找匹配的规则 - 返回结果:如果匹配成功,设置输出参数并返回1;否则返回0
类比:就像"使用字典查找书籍":
- 获取索引卡片数组:根据"书架编号"获取"索引卡片数组"
- 查找匹配书籍:在"字典索引系统"中查找匹配的"索引卡片位置"
- 返回结果:如果匹配成功,设置输出参数并返回"匹配成功";否则返回"未匹配"
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匹配逻辑:
- 获取Lookup Context相关数据:获取应用条目向量和掩码信息向量
- 遍历所有掩码类型:按优先级排序,从高到低遍历
- 构建Hash表key:应用掩码到匹配值,设置元数据
- 在Hash表中查找:使用Hash表查找匹配的规则
- 处理Hash冲突:如果Hash命中,遍历冲突规则向量,查找最佳匹配(优先级最高的规则)
匹配优先级:
- 掩码类型优先级:掩码信息向量按优先级排序(位置越靠前,优先级越高)
- 规则优先级:冲突规则向量按优先级排序(索引越小,优先级越高)
- 最佳匹配:选择优先级最高的匹配规则(索引最小的规则)
类比:就像完整的"使用字典查找书籍"流程:
- 获取数据:获取"索引卡片数组"和"分类信息数组"
- 遍历分类:按优先级排序,从高到低遍历"分类"
- 构建键:应用"分类掩码"到"书籍匹配值",设置元数据
- 查找键:在"字典索引系统"中查找"键"
- 处理冲突:如果找到,遍历"冲突书籍列表",查找最佳匹配(优先级最高的书籍)
10.8 本章小结
通过这一章的详细讲解,我们了解了:
- Hash匹配引擎的概念:什么是Hash匹配,为什么需要它
- 数据结构:Hash ACE信息、Hash ACL信息、应用条目、冲突规则等
- Hash表构建:如何将ACL规则添加到Hash表
- 掩码类型分配:如何为规则分配掩码类型索引
- Hash表条目激活:如何将规则添加到Hash表,处理Hash冲突
- TupleMerge算法:如何通过放松掩码来合并相似规则
- Hash匹配流程:如何使用Hash表快速查找匹配的规则
核心要点:
- Hash匹配将匹配时间复杂度从O(M)降低到O(1)平均情况
- 掩码类型用于减少Hash表的数量,提高查找效率
- TupleMerge算法通过放松掩码来合并相似规则,提高内存利用率
- Hash冲突处理通过冲突规则向量来存储Hash冲突的规则
- 匹配优先级通过掩码类型和规则索引来确定(索引越小,优先级越高)
下一步:在下一章,我们会看到ACL-as-a-service的详细实现,包括Lookup Context管理、导出方法等。