文档说明
本文档详细解析VPP中的DHCP插件实现,从DHCP协议基础到VPP的具体实现和增强特性,系统性地梳理整个DHCP插件的架构和源码细节。此篇文章主要讲解第六部分:DHCP Proxy实现详解,内容较多分为上下篇。
目录大纲
-
第一部分:DHCP协议基础知识 - 系统介绍DHCP协议的基本概念、DORA交互流程、DHCPv4/v6协议差异以及协议在网络中的角色定位,为理解VPP实现奠定理论基础。
-
第二部分:VPP DHCP插件功能概述 - 全面介绍VPP DHCP插件的功能特性、架构定位、API/CLI支持能力以及多线程处理机制,帮助读者建立对插件的整体认知。
-
第三部分:源码目录结构与模块划分 - 详细解析DHCP插件的源码文件组织方式、各模块职责划分、控制平面与数据平面分离设计,以及模块间的依赖关系。
-
第四部分:DHCPv4客户端实现详解 - 深入剖析DHCPv4客户端的核心数据结构、状态机实现、报文构造与解析、地址安装与路由更新等关键实现细节。
-
第五部分:DHCPv6客户端实现详解 - 全面解析DHCPv6客户端的CP/DP分离架构、IA_NA和IA_PD两种模式的实现机制、前缀委派功能以及DUID管理。
-
第六部分:DHCP Proxy实现详解 - 深入讲解DHCP代理/中继的架构设计、多服务器支持策略、VRF隔离机制、Option 82插入以及Cookie去重机制。
-
第七部分:API接口与CLI命令详解 - 系统说明所有DHCP相关的API接口定义、参数说明、使用示例以及CLI命令的完整语法和配置方法。
-
第八部分:调试与故障排查 - 详细介绍日志系统使用、统计计数器查看、调试命令操作、常见问题诊断方法以及数据包捕获分析技巧。
-
第九部分:扩展与定制 - 讲解如何基于源码添加新的DHCP选项支持、定制客户端和Proxy行为、与外部系统集成以及插件开发最佳实践。
-
第十部分:应用场景与配置实践 - 基于实际CLI命令和配置方法,详细说明边缘路由器、企业网络、多租户等典型应用场景的具体配置步骤和实现效果。
-
第十一部分:安全考虑 - 分析DHCP协议面临的安全威胁(欺骗攻击、DoS攻击等)、VPP提供的安全特性以及实际部署中的安全加固建议。
-
第十二部分:当前限制与未来发展方向 - 总结当前实现的限制(如不支持DHCP服务器、选项支持有限等)以及基于协议标准和实际需求的改进方向。
6.4 DHCPv4 Proxy节点实现
DHCPv4 Proxy节点实现是Proxy功能的核心,包括两个主要节点:客户端到服务器方向的节点和服务器到客户端方向的节点。本节详细分析这两个节点的实现原理,包括报文处理流程、Option 82插入、VSS选项处理等。
6.4.1 dhcp4_proxy_node.c 分析
6.4.1.1 节点概述
DHCPv4 Proxy使用两个VPP节点来处理DHCP报文:
-
dhcp-proxy-to-server节点:处理客户端到服务器方向的报文
- 接收客户端的DHCP请求(DISCOVER、REQUEST等)
- 插入Option 82和VSS选项
- 修改源地址和目标地址
- 转发到DHCP服务器
-
dhcp-proxy-to-client节点:处理服务器到客户端方向的报文
- 接收服务器的DHCP回复(OFFER、ACK等)
- 验证Option 82
- 删除Option 82
- 转发到客户端
文件结构:
#include <vlib/vlib.h> /* VPP库函数头文件 */
/* 说明:包含VPP的核心库函数。
*
* 主要功能:
* - 节点注册和管理
* - 缓冲区处理
* - 帧处理 */
#include <dhcp/dhcp_proxy.h> /* DHCP Proxy头文件 */
/* 说明:包含DHCP Proxy的公共定义。
*
* 主要功能:
* - Proxy配置结构体
* - Proxy管理函数
* - VSS配置 */
#include <dhcp/client.h> /* DHCP客户端头文件 */
/* 说明:包含DHCP客户端的定义。
*
* 主要功能:
* - dhcp_client_for_us函数
* - 用于判断报文是否被本地客户端处理 */
#include <vnet/fib/ip4_fib.h> /* IPv4 FIB头文件 */
/* 说明:包含IPv4 FIB的定义。
*
* 主要功能:
* - FIB索引查找
* - 路由查找 */
错误字符串数组:
//23:27:src/plugins/dhcp/dhcp4_proxy_node.c
static char *dhcp_proxy_error_strings[] = { /* DHCP Proxy错误字符串数组 */
/* 说明:dhcp_proxy_error_strings数组存储所有DHCP Proxy错误的字符串描述。
*
* 类型:char *数组
* - 静态数组,在编译时初始化
* - 每个元素是一个错误描述字符串
*
* 用途:
* - 在节点中用于错误统计和日志
* - 在trace中显示错误信息
* - 通过dhcp4_proxy_error.def文件生成 */
#define dhcp_proxy_error(n,s) s, /* 定义宏,展开为字符串 */
/* 说明:宏定义用于从错误定义文件中提取字符串。
*
* 参数:
* - n:错误名称(不使用)
* - s:错误字符串描述
*
* 展开:
* - dhcp_proxy_error(NONE, "no error") 展开为 "no error",
* - dhcp_proxy_error(NO_SERVER, "no dhcp server configured") 展开为 "no dhcp server configured",
*
* 用途:
* - 简化错误字符串数组的初始化
* - 保持错误定义和字符串的一致性 */
#include <dhcp/dhcp4_proxy_error.def> /* 包含错误定义文件 */
/* 说明:包含DHCP Proxy错误定义文件。
*
* 文件内容:
* - 每个错误一行:dhcp_proxy_error(名称, 字符串)
* - 例如:dhcp_proxy_error(NO_SERVER, "no dhcp server configured")
*
* 处理方式:
* - 通过宏展开为字符串数组元素
* - 编译时处理,运行时直接使用 */
#undef dhcp_proxy_error /* 取消宏定义 */
/* 说明:取消宏定义,避免影响后续代码。
*
* 原因:
* - 宏定义只在包含错误定义文件时有效
* - 包含完成后取消定义,避免命名冲突 */
};
/* 说明:错误字符串数组定义完成。
*
* 数组内容:
* - 包含所有DHCP Proxy错误的字符串描述
* - 数组索引对应错误代码
* - 例如:dhcp_proxy_error_strings[DHCP_PROXY_ERROR_NO_SERVER] = "no dhcp server configured" */
节点下一跳枚举:
//29:40:src/plugins/dhcp/dhcp4_proxy_node.c
#define foreach_dhcp_proxy_to_server_input_next \ /* 定义下一跳节点宏 */
/* 说明:foreach_dhcp_proxy_to_server_input_next宏定义客户端到服务器方向节点的下一跳。
*
* 格式:
* - 使用VPP的foreach宏模式
* - 每行定义一个下一跳:_(名称, 节点名)
*
* 下一跳节点:
* - DROP:错误丢弃节点
* - LOOKUP:IP查找节点(转发到服务器)
* - SEND_TO_CLIENT:发送到客户端节点(服务器回复) */
_ (DROP, "error-drop") \ /* 错误丢弃节点 */
/* 说明:DROP下一跳用于丢弃错误的报文。
*
* 节点名:"error-drop"
* - VPP的标准错误丢弃节点
* - 丢弃报文并更新错误统计
*
* 使用场景:
* - 找不到Proxy配置
* - 报文太大
* - Option 82插入失败等 */
_ (LOOKUP, "ip4-lookup") \ /* IP查找节点 */
/* 说明:LOOKUP下一跳用于转发报文到服务器。
*
* 节点名:"ip4-lookup"
* - VPP的IPv4查找节点
* - 根据目标地址查找路由
* - 转发报文到下一跳
*
* 使用场景:
* - 正常转发客户端请求到服务器
* - 报文已插入Option 82
* - 源地址和目标地址已修改 */
_ (SEND_TO_CLIENT, "dhcp-proxy-to-client") /* 发送到客户端节点 */
/* 说明:SEND_TO_CLIENT下一跳用于处理服务器回复。
*
* 节点名:"dhcp-proxy-to-client"
* - DHCP Proxy的服务器到客户端方向节点
* - 处理服务器的回复报文
*
* 使用场景:
* - 检测到服务器回复报文(src_port = 67)
* - 需要转发到客户端节点处理 */
typedef enum
{
#define _(s,n) DHCP_PROXY_TO_SERVER_INPUT_NEXT_##s, /* 定义枚举值 */
/* 说明:宏定义用于生成枚举值。
*
* 参数:
* - s:下一跳名称(DROP、LOOKUP、SEND_TO_CLIENT)
* - n:节点名(不使用)
*
* 展开:
* - _(DROP, "error-drop") 展开为 DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP,
* - _(LOOKUP, "ip4-lookup") 展开为 DHCP_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP,
* - _(SEND_TO_CLIENT, "dhcp-proxy-to-client") 展开为 DHCP_PROXY_TO_SERVER_INPUT_NEXT_SEND_TO_CLIENT,
*
* 用途:
* - 生成枚举值,用于代码中引用下一跳 */
foreach_dhcp_proxy_to_server_input_next /* 展开宏定义 */
/* 说明:展开foreach宏,生成枚举值。
*
* 展开结果:
* - DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP
* - DHCP_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP
* - DHCP_PROXY_TO_SERVER_INPUT_NEXT_SEND_TO_CLIENT */
#undef _ /* 取消宏定义 */
DHCP_PROXY_TO_SERVER_INPUT_N_NEXT, /* 下一跳节点数量 */
/* 说明:DHCP_PROXY_TO_SERVER_INPUT_N_NEXT枚举值表示下一跳节点的数量。
*
* 值:3
* - 表示有3个下一跳节点
* - 用于数组大小定义
* - 用于边界检查 */
} dhcp_proxy_to_server_input_next_t; /* 下一跳枚举类型 */
/* 说明:dhcp_proxy_to_server_input_next_t是客户端到服务器方向节点的下一跳枚举类型。
*
* 用途:
* - 在节点函数中引用下一跳
* - 在节点注册时定义下一跳节点数组
* - 提高代码可读性和类型安全 */
Trace结构体:
//42:54:src/plugins/dhcp/dhcp4_proxy_node.c
typedef struct
{
/* 0 => to server, 1 => to client */
int which; /* 方向标志 */
/* 说明:which字段标识报文方向。
*
* 值:
* - 0:发送到服务器
* - 1:发送到客户端
*
* 用途:
* - 在trace中区分报文方向
* - 用于格式化trace输出 */
ip4_address_t trace_ip4_address; /* IPv4地址(用于trace) */
/* 说明:trace_ip4_address字段存储IPv4地址。
*
* 用途:
* - 在trace中显示服务器地址或客户端接口地址
* - 用于调试和日志 */
u32 error; /* 错误代码 */
/* 说明:error字段存储错误代码。
*
* 类型:u32
* - 错误代码枚举值
* - 如果无错误,值为~0
*
* 用途:
* - 在trace中显示错误信息
* - 用于错误统计 */
u32 sw_if_index; /* 软件接口索引 */
/* 说明:sw_if_index字段存储软件接口索引。
*
* 用途:
* - 在trace中显示接口信息
* - 用于调试 */
u32 original_sw_if_index; /* 原始软件接口索引 */
/* 说明:original_sw_if_index字段存储原始软件接口索引。
*
* 用途:
* - 在trace中显示原始接口信息
* - 用于调试(特别是unnumbered接口的情况) */
/* enough space for the DHCP header plus some options */
u8 packet_data[2 * sizeof (dhcp_header_t)]; /* 报文数据(用于trace) */
/* 说明:packet_data字段存储报文数据。
*
* 大小:2 * sizeof(dhcp_header_t)
* - 足够存储DHCP头部和一些选项
* - 用于trace输出
*
* 用途:
* - 在trace中显示报文内容
* - 用于调试和故障排查 */
}
dhcp_proxy_trace_t; /* DHCP Proxy Trace结构体类型 */
/* 说明:dhcp_proxy_trace_t是DHCP Proxy的trace结构体类型。
*
* 用途:
* - 在节点中记录trace信息
* - 用于调试和故障排查
* - 通过VPP的trace机制输出 */
Option 82大小定义:
//56:61:src/plugins/dhcp/dhcp4_proxy_node.c
#define VPP_DHCP_OPTION82_SUB1_SIZE 6 /* Option 82子选项1(Circuit ID)大小 */
/* 说明:VPP_DHCP_OPTION82_SUB1_SIZE定义Circuit ID子选项的大小。
*
* 大小:6字节
* - 子选项类型:1字节
* - 子选项长度:1字节
* - 子选项数据:4字节(sw_if_index)
*
* 用途:
* - 用于计算Option 82总大小
* - 用于缓冲区空间检查 */
#define VPP_DHCP_OPTION82_SUB5_SIZE 6 /* Option 82子选项5(Remote ID)大小 */
/* 说明:VPP_DHCP_OPTION82_SUB5_SIZE定义Remote ID子选项的大小。
*
* 大小:6字节
* - 子选项类型:5字节
* - 子选项长度:1字节
* - 子选项数据:4字节(接口IP地址)
*
* 用途:
* - 用于计算Option 82总大小
* - 用于缓冲区空间检查 */
#define VPP_DHCP_OPTION82_VSS_SIZE 12 /* Option 82 VSS子选项大小 */
/* 说明:VPP_DHCP_OPTION82_VSS_SIZE定义VSS子选项的大小。
*
* 大小:12字节(最大)
* - 子选项类型:151(1字节)
* - 子选项长度:1字节
* - VSS类型:1字节
* - VSS数据:最多7字节(VPN-ID)或可变(ASCII)
* - 子选项控制:152(1字节)
* - 子选项长度:1字节
*
* 用途:
* - 用于计算Option 82总大小
* - 用于缓冲区空间检查 */
#define VPP_DHCP_OPTION82_SIZE (VPP_DHCP_OPTION82_SUB1_SIZE + \ /* Option 82总大小 */
/* 说明:VPP_DHCP_OPTION82_SIZE定义Option 82的总大小。
*
* 计算方式:
* - Circuit ID子选项:6字节
* - Remote ID子选项:6字节
* - VSS子选项:12字节(最大)
* - Option 82头部:2字节(选项类型+长度)
* - 结束标记:1字节(0xFF)
*
* 总大小:约27字节
*
* 用途:
* - 用于缓冲区空间检查
* - 确保有足够空间插入Option 82 */
VPP_DHCP_OPTION82_SUB5_SIZE + \
VPP_DHCP_OPTION82_VSS_SIZE +3)
/* 说明:计算Option 82的总大小。
*
* 公式:
* - VPP_DHCP_OPTION82_SUB1_SIZE(6)+ VPP_DHCP_OPTION82_SUB5_SIZE(6)+ VPP_DHCP_OPTION82_VSS_SIZE(12)+ 3(头部和结束标记)
* - 总计:27字节
*
* 额外3字节:
* - Option 82类型(1字节)
* - Option 82长度(1字节)
* - 结束标记0xFF(1字节) */
节点变量声明:
//63:64:src/plugins/dhcp/dhcp4_proxy_node.c
static vlib_node_registration_t dhcp_proxy_to_server_node; /* 客户端到服务器方向节点 */
/* 说明:dhcp_proxy_to_server_node变量存储客户端到服务器方向节点的注册信息。
*
* 类型:vlib_node_registration_t
* - VPP节点注册结构体
* - 包含节点函数、名称、下一跳等信息
*
* 用途:
* - 在节点注册时填充
* - 用于节点查找和访问 */
static vlib_node_registration_t dhcp_proxy_to_client_node; /* 服务器到客户端方向节点 */
/* 说明:dhcp_proxy_to_client_node变量存储服务器到客户端方向节点的注册信息。
*
* 类型:vlib_node_registration_t
* - VPP节点注册结构体
*
* 用途:
* - 在节点注册时填充
* - 用于节点查找和访问 */
6.4.1.2 节点注册
客户端到服务器方向节点注册:
//467:488:src/plugins/dhcp/dhcp4_proxy_node.c
VLIB_REGISTER_NODE (dhcp_proxy_to_server_node, static) = { /* 注册客户端到服务器方向节点 */
/* 说明:VLIB_REGISTER_NODE宏用于注册VPP节点。
*
* 参数1:dhcp_proxy_to_server_node
* - 节点注册结构体变量
* - 在编译时初始化
*
* 参数2:static
* - 节点作用域为静态(仅在当前文件可见)
* - 避免与其他文件的节点冲突
*
* 功能:
* - 将节点注册到VPP的节点表
* - 节点在VPP启动时自动注册
* - 可以通过节点名查找节点 */
.function = dhcp_proxy_to_server_input, /* 节点处理函数 */
/* 说明:function字段指定节点的处理函数。
*
* 函数:dhcp_proxy_to_server_input
* - 处理客户端到服务器方向的DHCP报文
* - 接收报文向量,处理每个报文
* - 返回处理的报文数量
*
* 函数签名:
* - static uword dhcp_proxy_to_server_input(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *from_frame)
* - 返回处理的报文数量 */
.name = "dhcp-proxy-to-server", /* 节点名称 */
/* 说明:name字段指定节点的名称。
*
* 名称:"dhcp-proxy-to-server"
* - 用于节点查找和引用
* - 在CLI和trace中显示
* - 必须唯一
*
* 使用场景:
* - 通过vlib_get_node_by_name查找节点
* - 在UDP端口注册时引用节点索引 */
/* Takes a vector of packets. */
.vector_size = sizeof (u32), /* 向量元素大小 */
/* 说明:vector_size字段指定向量元素的大小。
*
* 大小:sizeof(u32)
* - 向量中每个元素是缓冲区索引(u32类型)
* - 4字节
*
* 用途:
* - VPP使用向量传递报文缓冲区索引
* - 每个元素是一个报文的缓冲区索引 */
.n_errors = DHCP_PROXY_N_ERROR, /* 错误数量 */
/* 说明:n_errors字段指定节点支持的错误数量。
*
* 值:DHCP_PROXY_N_ERROR
* - 从dhcp4_proxy_error.def文件生成的错误数量
* - 用于错误统计数组大小
*
* 用途:
* - 定义错误统计数组大小
* - 用于错误计数和显示 */
.error_strings = dhcp_proxy_error_strings, /* 错误字符串数组 */
/* 说明:error_strings字段指定错误字符串数组。
*
* 数组:dhcp_proxy_error_strings
* - 包含所有错误的字符串描述
* - 数组索引对应错误代码
*
* 用途:
* - 在trace中显示错误信息
* - 在CLI中显示错误统计 */
.n_next_nodes = DHCP_PROXY_TO_SERVER_INPUT_N_NEXT, /* 下一跳节点数量 */
/* 说明:n_next_nodes字段指定下一跳节点的数量。
*
* 值:DHCP_PROXY_TO_SERVER_INPUT_N_NEXT
* - 从枚举中获取,值为3
* - 表示有3个下一跳节点
*
* 用途:
* - 定义下一跳节点数组大小
* - 用于边界检查 */
.next_nodes = { /* 下一跳节点数组 */
/* 说明:next_nodes字段指定下一跳节点数组。
*
* 类型:字符串数组
* - 每个元素是下一跳节点的名称
* - 数组索引对应下一跳枚举值
*
* 用途:
* - 在节点处理函数中,通过枚举值索引此数组
* - 获取下一跳节点名称
* - VPP根据节点名称查找节点索引 */
#define _(s,n) [DHCP_PROXY_TO_SERVER_INPUT_NEXT_##s] = n, /* 定义下一跳节点映射 */
/* 说明:宏定义用于生成下一跳节点映射。
*
* 参数:
* - s:下一跳名称(DROP、LOOKUP、SEND_TO_CLIENT)
* - n:节点名称字符串
*
* 展开:
* - _(DROP, "error-drop") 展开为 [DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP] = "error-drop",
* - _(LOOKUP, "ip4-lookup") 展开为 [DHCP_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP] = "ip4-lookup",
* - _(SEND_TO_CLIENT, "dhcp-proxy-to-client") 展开为 [DHCP_PROXY_TO_SERVER_INPUT_NEXT_SEND_TO_CLIENT] = "dhcp-proxy-to-client",
*
* 用途:
* - 建立枚举值到节点名称的映射
* - 在节点处理函数中使用枚举值,自动映射到节点名称 */
foreach_dhcp_proxy_to_server_input_next /* 展开宏定义 */
/* 说明:展开foreach宏,生成下一跳节点映射。
*
* 展开结果:
* - [DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP] = "error-drop"
* - [DHCP_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP] = "ip4-lookup"
* - [DHCP_PROXY_TO_SERVER_INPUT_NEXT_SEND_TO_CLIENT] = "dhcp-proxy-to-client" */
#undef _ /* 取消宏定义 */
},
.format_buffer = format_dhcp_proxy_header_with_length, /* 缓冲区格式化函数 */
/* 说明:format_buffer字段指定缓冲区格式化函数。
*
* 函数:format_dhcp_proxy_header_with_length
* - 用于格式化DHCP Proxy报文的显示
* - 在CLI和trace中使用
*
* 函数签名:
* - static u8 *format_dhcp_proxy_header_with_length(u8 *s, va_list *args)
* - 返回格式化后的字符串 */
.format_trace = format_dhcp_proxy_trace, /* Trace格式化函数 */
/* 说明:format_trace字段指定trace格式化函数。
*
* 函数:format_dhcp_proxy_trace
* - 用于格式化DHCP Proxy的trace信息
* - 在trace输出中使用
*
* 函数签名:
* - static u8 *format_dhcp_proxy_trace(u8 *s, va_list *args)
* - 返回格式化后的字符串 */
#if 0
.unformat_buffer = unformat_dhcp_proxy_header, /* 缓冲区解析函数(未使用) */
/* 说明:unformat_buffer字段指定缓冲区解析函数(当前未使用)。
*
* 功能:
* - 用于从字符串解析DHCP Proxy报文
* - 在CLI中使用(如果启用)
*
* 当前状态:
* - 被#if 0条件编译禁用
* - 未实现或未使用 */
#endif
};
/* 说明:节点注册完成。
*
* 注册后的状态:
* - 节点已注册到VPP节点表
* - 可以通过节点名"dhcp-proxy-to-server"查找
* - 节点索引已分配
* - 可以在UDP端口注册时使用节点索引 */
服务器到客户端方向节点注册:
//785:803:src/plugins/dhcp/dhcp4_proxy_node.c
VLIB_REGISTER_NODE (dhcp_proxy_to_client_node, static) = { /* 注册服务器到客户端方向节点 */
/* 说明:VLIB_REGISTER_NODE宏用于注册VPP节点。
*
* 参数1:dhcp_proxy_to_client_node
* - 节点注册结构体变量
*
* 参数2:static
* - 节点作用域为静态 */
.function = dhcp_proxy_to_client_input, /* 节点处理函数 */
/* 说明:function字段指定节点的处理函数。
*
* 函数:dhcp_proxy_to_client_input
* - 处理服务器到客户端方向的DHCP报文
* - 接收服务器的回复报文
* - 验证和删除Option 82
* - 转发到客户端 */
.name = "dhcp-proxy-to-client", /* 节点名称 */
/* 说明:name字段指定节点的名称。
*
* 名称:"dhcp-proxy-to-client"
* - 用于节点查找和引用 */
/* Takes a vector of packets. */
.vector_size = sizeof (u32), /* 向量元素大小 */
/* 说明:vector_size字段指定向量元素的大小。
*
* 大小:sizeof(u32)
* - 向量中每个元素是缓冲区索引 */
.n_errors = DHCP_PROXY_N_ERROR, /* 错误数量 */
/* 说明:n_errors字段指定节点支持的错误数量。
*
* 值:DHCP_PROXY_N_ERROR
* - 与客户端到服务器方向节点相同
* - 共享错误定义 */
.error_strings = dhcp_proxy_error_strings, /* 错误字符串数组 */
/* 说明:error_strings字段指定错误字符串数组。
*
* 数组:dhcp_proxy_error_strings
* - 与客户端到服务器方向节点共享
* - 使用相同的错误定义 */
.format_buffer = format_dhcp_proxy_header_with_length, /* 缓冲区格式化函数 */
/* 说明:format_buffer字段指定缓冲区格式化函数。
*
* 函数:format_dhcp_proxy_header_with_length
* - 与客户端到服务器方向节点共享 */
.format_trace = format_dhcp_proxy_trace, /* Trace格式化函数 */
/* 说明:format_trace字段指定trace格式化函数。
*
* 函数:format_dhcp_proxy_trace
* - 与客户端到服务器方向节点共享 */
#if 0
.unformat_buffer = unformat_dhcp_proxy_header, /* 缓冲区解析函数(未使用) */
#endif
.n_next_nodes = DHCP4_PROXY_N_NEXT, /* 下一跳节点数量 */
/* 说明:n_next_nodes字段指定下一跳节点的数量。
*
* 值:DHCP4_PROXY_N_NEXT
* - 从枚举中获取,值为2
* - 表示有2个下一跳节点(DROP和TX) */
.next_nodes = { /* 下一跳节点数组 */
/* 说明:next_nodes字段指定下一跳节点数组。
*
* 数组大小:2
* - [DHCP4_PROXY_NEXT_DROP] = "error-drop"
* - [DHCP4_PROXY_NEXT_TX] = "interface-output" */
[DHCP4_PROXY_NEXT_DROP] = "error-drop", /* 错误丢弃节点 */
/* 说明:DROP下一跳用于丢弃错误的报文。
*
* 节点名:"error-drop"
* - VPP的标准错误丢弃节点
*
* 使用场景:
* - Option 82缺失或无效
* - 找不到Proxy配置
* - 接口地址无效等 */
[DHCP4_PROXY_NEXT_TX] = "interface-output", /* 接口输出节点 */
/* 说明:TX下一跳用于转发报文到客户端。
*
* 节点名:"interface-output"
* - VPP的接口输出节点
* - 将报文发送到指定接口
*
* 使用场景:
* - 正常转发服务器回复到客户端
* - 报文已删除Option 82
* - 源地址和目标地址已修改 */
},
};
/* 说明:节点注册完成。
*
* 注册后的状态:
* - 节点已注册到VPP节点表
* - 可以通过节点名"dhcp-proxy-to-client"查找
* - 节点索引已分配
* - 可以在UDP端口注册时使用节点索引 */
节点注册总结:
-
两个节点:
dhcp-proxy-to-server:客户端到服务器方向dhcp-proxy-to-client:服务器到客户端方向
-
共享资源:
- 错误字符串数组
- 格式化函数
- 错误定义
-
不同点:
- 处理函数不同
- 下一跳节点不同
- 处理逻辑不同
#### 6.4.2 UDP端口注册和初始化
**6.4.2.1 UDP端口注册函数**
```c
//805:824:src/plugins/dhcp/dhcp4_proxy_node.c
void
dhcp_maybe_register_udp_ports (dhcp_port_reg_flags_t ports) /* UDP端口注册函数 */
{
/* 说明:dhcp_maybe_register_udp_ports函数用于注册DHCP Proxy节点的UDP端口。
*
* 功能:
* - 根据端口标志注册相应的UDP端口
* - 避免重复注册
* - 将UDP端口与节点索引关联
*
* 参数:
* - ports:端口注册标志(DHCP_PORT_REG_CLIENT或DHCP_PORT_REG_SERVER)
*
* 返回值:无 */
dhcp_proxy_main_t *dm = &dhcp_proxy_main; /* 获取DHCP Proxy主结构体 */
/* 说明:获取全局的DHCP Proxy主结构体。
*
* 用途:
* - 访问VPP主结构体
* - 检查已注册的端口标志 */
vlib_main_t *vm = dm->vlib_main; /* 获取VPP主结构体 */
/* 说明:获取VPP主结构体。
*
* 用途:
* - 传递给UDP端口注册函数
* - 用于节点查找 */
int port_regs_diff = dm->udp_ports_registered ^ ports; /* 计算端口注册差异 */
/* 说明:计算需要注册的端口差异。
*
* 操作:异或运算
* - 如果端口已注册,差异为0
* - 如果端口未注册,差异为非0
*
* 用途:
* - 避免重复注册
* - 只注册新端口 */
if (!port_regs_diff) /* 如果没有差异,直接返回 */
/* 说明:检查是否需要注册新端口。
*
* 条件:
* - port_regs_diff == 0:所有端口已注册
* - 直接返回,避免重复注册 */
return;
if ((port_regs_diff & DHCP_PORT_REG_CLIENT) & ports) /* 注册客户端端口 */
/* 说明:检查是否需要注册客户端端口(UDP 68)。
*
* 条件:
* - port_regs_diff包含DHCP_PORT_REG_CLIENT标志
* - ports参数也包含DHCP_PORT_REG_CLIENT标志
*
* 含义:
* - 需要注册客户端端口
* - 将UDP 68端口与dhcp-proxy-to-client节点关联 */
udp_register_dst_port (vm, UDP_DST_PORT_dhcp_to_client, /* 注册UDP 68端口 */
/* 说明:udp_register_dst_port函数注册UDP目标端口。
*
* 参数1:vm
* - VPP主结构体
*
* 参数2:UDP_DST_PORT_dhcp_to_client
* - UDP端口号68(客户端端口)
* - 客户端发送DHCP请求的端口
*
* 功能:
* - 当UDP报文目标端口为68时,转发到指定节点 */
dhcp_proxy_to_client_node.index, /* 节点索引 */
/* 说明:指定处理该UDP端口的节点索引。
*
* 节点:dhcp-proxy-to-client
* - 处理服务器到客户端方向的DHCP报文
* - 接收服务器的回复报文
*
* 流程:
* - UDP报文目标端口68 -> dhcp-proxy-to-client节点 */
1 /* is_ip4 */ ); /* IPv4标志 */
/* 说明:指定协议类型为IPv4。
*
* 值:1表示IPv4,0表示IPv6 */
if ((port_regs_diff & DHCP_PORT_REG_SERVER) & ports) /* 注册服务器端口 */
/* 说明:检查是否需要注册服务器端口(UDP 67)。
*
* 条件:
* - port_regs_diff包含DHCP_PORT_REG_SERVER标志
* - ports参数也包含DHCP_PORT_REG_SERVER标志
*
* 含义:
* - 需要注册服务器端口
* - 将UDP 67端口与dhcp-proxy-to-server节点关联 */
udp_register_dst_port (vm, UDP_DST_PORT_dhcp_to_server, /* 注册UDP 67端口 */
/* 说明:注册UDP目标端口67(服务器端口)。
*
* 端口:UDP 67
* - 服务器接收DHCP请求的端口
* - 客户端发送DHCP请求的目标端口
*
* 功能:
* - 当UDP报文目标端口为67时,转发到指定节点 */
dhcp_proxy_to_server_node.index, /* 节点索引 */
/* 说明:指定处理该UDP端口的节点索引。
*
* 节点:dhcp-proxy-to-server
* - 处理客户端到服务器方向的DHCP报文
* - 接收客户端的请求报文
*
* 流程:
* - UDP报文目标端口67 -> dhcp-proxy-to-server节点 */
1 /* is_ip4 */ ); /* IPv4标志 */
/* 说明:指定协议类型为IPv4。 */
dm->udp_ports_registered |= ports; /* 更新已注册端口标志 */
/* 说明:更新已注册端口标志。
*
* 操作:按位或运算
* - 将新注册的端口标志添加到已注册标志中
* - 避免重复注册
*
* 用途:
* - 记录已注册的端口
* - 下次调用时检查是否已注册 */
}
UDP端口注册总结:
-
注册时机:
- 在配置DHCP Proxy服务器时调用
- 通过
dhcp4_proxy_set_server函数触发
-
注册的端口:
- UDP 67:服务器端口,关联
dhcp-proxy-to-server节点 - UDP 68:客户端端口,关联
dhcp-proxy-to-client节点
- UDP 67:服务器端口,关联
-
注册机制:
- 使用异或运算检测需要注册的新端口
- 避免重复注册已注册的端口
- 更新已注册端口标志
6.4.2.2 初始化函数
//826:840:src/plugins/dhcp/dhcp4_proxy_node.c
static clib_error_t *
dhcp4_proxy_init (vlib_main_t * vm) /* DHCP Proxy初始化函数 */
{
/* 说明:dhcp4_proxy_init函数在VPP启动时初始化DHCP Proxy模块。
*
* 功能:
* - 查找错误丢弃节点
* - 保存VPP主结构体引用
* - 初始化DHCP Proxy主结构体
*
* 参数:
* - vm:VPP主结构体
*
* 返回值:
* - 成功返回0(NULL)
* - 失败返回错误信息 */
dhcp_proxy_main_t *dm = &dhcp_proxy_main; /* 获取DHCP Proxy主结构体 */
/* 说明:获取全局的DHCP Proxy主结构体。
*
* 用途:
* - 存储节点索引
* - 存储VPP主结构体引用 */
vlib_node_t *error_drop_node; /* 错误丢弃节点 */
/* 说明:错误丢弃节点变量。
*
* 用途:
* - 存储错误丢弃节点的引用
* - 用于后续的错误处理 */
error_drop_node = vlib_get_node_by_name (vm, (u8 *) "error-drop"); /* 查找错误丢弃节点 */
/* 说明:通过节点名称查找错误丢弃节点。
*
* 参数1:vm
* - VPP主结构体
*
* 参数2:"error-drop"
* - 节点名称
* - VPP的标准错误丢弃节点
*
* 功能:
* - 在VPP节点表中查找名为"error-drop"的节点
* - 返回节点结构体指针
*
* 用途:
* - 获取错误丢弃节点的索引
* - 用于后续的错误处理 */
dm->error_drop_node_index = error_drop_node->index; /* 保存错误丢弃节点索引 */
/* 说明:保存错误丢弃节点的索引。
*
* 用途:
* - 在节点处理函数中直接使用索引
* - 避免重复查找节点
* - 提高性能 */
dm->vlib_main = vm; /* 保存VPP主结构体引用 */
/* 说明:保存VPP主结构体的引用。
*
* 用途:
* - 在UDP端口注册函数中使用
* - 在其他需要VPP主结构体的地方使用
* - 避免重复传递参数 */
return 0; /* 返回成功 */
/* 说明:返回0表示初始化成功。
*
* 返回值:
* - 0(NULL):成功
* - 非0:失败,返回错误信息 */
}
VLIB_INIT_FUNCTION (dhcp4_proxy_init); /* 注册初始化函数 */
/* 说明:VLIB_INIT_FUNCTION宏用于注册VPP初始化函数。
*
* 功能:
* - 在VPP启动时自动调用dhcp4_proxy_init函数
* - 在节点注册之后执行
* - 在所有插件初始化之前执行
*
* 执行顺序:
* 1. 节点注册(VLIB_REGISTER_NODE)
* 2. 初始化函数(VLIB_INIT_FUNCTION)
* 3. 配置函数(通过API调用) */
初始化函数总结:
-
初始化内容:
- 查找并保存错误丢弃节点索引
- 保存VPP主结构体引用
-
执行时机:
- VPP启动时自动执行
- 在节点注册之后
- 在配置之前
-
用途:
- 为后续处理准备必要的节点索引
- 避免运行时查找节点的开销
6.4.3 客户端到服务器方向处理函数
6.4.3.1 函数概述
//107:465:src/plugins/dhcp/dhcp4_proxy_node.c
static uword
dhcp_proxy_to_server_input (vlib_main_t * vm, /* VPP主结构体 */
vlib_node_runtime_t * node, /* 节点运行时结构体 */
vlib_frame_t * from_frame) /* 输入帧 */
{
/* 说明:dhcp_proxy_to_server_input函数处理客户端到服务器方向的DHCP报文。
*
* 功能:
* - 接收客户端的DHCP请求报文
* - 查找Proxy配置
* - 插入Option 82选项
* - 修改IP头部(源地址、目标地址)
* - 转发到DHCP服务器
* - 支持多服务器转发(Discover消息)
*
* 参数:
* - vm:VPP主结构体
* - node:节点运行时结构体(包含节点索引、下一跳缓存等)
* - from_frame:输入帧(包含报文缓冲区索引向量)
*
* 返回值:
* - 处理的报文数量(from_frame->n_vectors)
*
* 处理流程:
* 1. 获取报文向量
* 2. 遍历每个报文
* 3. 检查是否为返回流量(服务器到客户端)
* 4. 查找Proxy配置
* 5. 验证缓冲区空间
* 6. 修改IP头部
* 7. 插入Option 82
* 8. 转发到服务器 */
u32 n_left_from, next_index, *from, *to_next; /* 向量处理变量 */
/* 说明:向量处理变量。
*
* n_left_from:剩余待处理报文数量
* next_index:下一跳节点索引(缓存)
* from:输入向量指针
* to_next:输出向量指针 */
dhcp_proxy_main_t *dpm = &dhcp_proxy_main; /* DHCP Proxy主结构体 */
from = vlib_frame_vector_args (from_frame); /* 获取输入向量 */
n_left_from = from_frame->n_vectors; /* 获取报文数量 */
u32 pkts_to_server = 0, pkts_to_client = 0, pkts_no_server = 0; /* 统计计数器 */
/* 说明:统计计数器。
*
* pkts_to_server:转发到服务器的报文数
* pkts_to_client:转发到客户端的报文数(返回流量)
* pkts_no_server:没有找到服务器的报文数 */
u32 pkts_no_interface_address = 0; /* 没有接口地址的报文数 */
u32 pkts_too_big = 0; /* 报文太大的报文数 */
ip4_main_t *im = &ip4_main; /* IPv4主结构体 */
next_index = node->cached_next_index; /* 获取缓存的下一跳索引 */
/* 说明:获取缓存的下一跳节点索引。
*
* 用途:
* - 提高性能,避免重复查找
* - 大多数情况下下一跳节点相同 */
6.4.3.2 报文遍历和初步检查
while (n_left_from > 0) /* 外层循环:处理多个帧 */
/* 说明:外层循环处理多个帧。
*
* 原因:
* - VPP使用向量处理,一次可能处理多个报文
* - 需要处理多个帧的情况 */
{
u32 n_left_to_next; /* 当前帧剩余空间 */
vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next); /* 获取下一帧 */
/* 说明:获取下一跳节点的输出帧。
*
* 功能:
* - 为下一跳节点分配输出帧
* - 获取输出向量指针和剩余空间 */
while (n_left_from > 0 && n_left_to_next > 0) /* 内层循环:处理单个报文 */
/* 说明:内层循环处理单个报文。
*
* 条件:
* - 还有待处理报文(n_left_from > 0)
* - 输出帧还有空间(n_left_to_next > 0) */
{
u32 bi0; /* 缓冲区索引 */
vlib_buffer_t *b0; /* 缓冲区指针 */
udp_header_t *u0; /* UDP头部指针 */
dhcp_header_t *h0; /* DHCP头部指针 */
ip4_header_t *ip0; /* IP头部指针 */
u32 next0; /* 下一跳节点索引 */
u32 old0, new0; /* IP地址更新变量 */
ip_csum_t sum0; /* IP校验和 */
u32 error0 = (u32) ~ 0; /* 错误代码 */
u32 sw_if_index = 0; /* 软件接口索引 */
u32 original_sw_if_index = 0; /* 原始软件接口索引 */
u32 fib_index; /* FIB索引 */
dhcp_proxy_t *proxy; /* Proxy配置指针 */
dhcp_server_t *server; /* 服务器配置指针 */
u32 rx_sw_if_index; /* 接收接口索引 */
dhcp_option_t *o, *end; /* Option指针 */
u32 len = 0; /* Option长度 */
u8 is_discover = 0; /* 是否为Discover消息 */
int space_left; /* 剩余空间 */
bi0 = from[0]; /* 获取缓冲区索引 */
from += 1; /* 移动输入指针 */
n_left_from -= 1; /* 减少剩余计数 */
b0 = vlib_get_buffer (vm, bi0); /* 获取缓冲区 */
h0 = vlib_buffer_get_current (b0); /* 获取DHCP头部 */
/* 说明:获取当前缓冲区位置的DHCP头部。
*
* 当前状态:
* - udp_local节点已将缓冲区指针定位到DHCP头部
* - 需要回退获取UDP和IP头部 */
/*
* udp_local hands us the DHCP header, need udp hdr,
* ip hdr to relay to server
*/
vlib_buffer_advance (b0, -(sizeof (*u0))); /* 回退到UDP头部 */
/* 说明:将缓冲区指针回退到UDP头部。
*
* 操作:向前移动(负值)
* - 从DHCP头部回退到UDP头部
* - 需要UDP头部修改目标端口和校验和 */
u0 = vlib_buffer_get_current (b0); /* 获取UDP头部 */
/* This blows. Return traffic has src_port = 67, dst_port = 67 */
if (u0->src_port == clib_net_to_host_u16 (UDP_DST_PORT_dhcp_to_server)) /* 检查返回流量 */
/* 说明:检查是否为返回流量(服务器到客户端)。
*
* 条件:
* - 源端口为67(服务器端口)
* - 目标端口也为67
*
* 含义:
* - 这是服务器的回复报文
* - 需要转发到客户端节点处理
*
* 原因:
* - DHCP服务器回复时,源端口和目标端口都是67
* - 无法通过目标端口区分,需要通过源端口判断 */
{
vlib_buffer_advance (b0, sizeof (*u0)); /* 前进到DHCP头部 */
next0 = DHCP_PROXY_TO_SERVER_INPUT_NEXT_SEND_TO_CLIENT; /* 设置下一跳 */
/* 说明:设置下一跳为客户端节点。
*
* 下一跳:dhcp-proxy-to-client
* - 处理服务器到客户端方向的报文
* - 删除Option 82
* - 转发到客户端 */
error0 = 0; /* 无错误 */
pkts_to_client++; /* 增加统计 */
goto do_enqueue; /* 跳转到入队 */
/* 说明:直接跳转到入队处理。
*
* 原因:
* - 返回流量不需要插入Option 82
* - 不需要修改IP头部
* - 直接转发到客户端节点 */
}
6.4.3.3 Proxy配置查找和验证
rx_sw_if_index = vnet_buffer (b0)->sw_if_index[VLIB_RX]; /* 获取接收接口索引 */
/* 说明:获取报文的接收接口索引。
*
* 用途:
* - 用于查找对应的Proxy配置
* - 用于获取接口地址 */
fib_index = im->fib_index_by_sw_if_index[rx_sw_if_index]; /* 获取FIB索引 */
/* 说明:根据接口索引获取FIB索引。
*
* 用途:
* - Proxy配置按FIB索引组织
* - 需要FIB索引查找Proxy配置 */
proxy = dhcp_get_proxy (dpm, fib_index, FIB_PROTOCOL_IP4); /* 查找Proxy配置 */
/* 说明:查找对应FIB的Proxy配置。
*
* 参数:
* - dpm:DHCP Proxy主结构体
* - fib_index:FIB索引
* - FIB_PROTOCOL_IP4:IPv4协议
*
* 返回值:
* - 成功:Proxy配置指针
* - 失败:NULL
*
* 用途:
* - 获取服务器地址
* - 获取源地址
* - 获取VSS信息 */
if (PREDICT_FALSE (NULL == proxy)) /* 检查Proxy配置 */
/* 说明:检查是否找到Proxy配置。
*
* PREDICT_FALSE:提示编译器这是不常见的情况
* - 优化分支预测
* - 提高常见路径的性能 */
{
error0 = DHCP_PROXY_ERROR_NO_SERVER; /* 设置错误代码 */
next0 = DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP; /* 设置下一跳为丢弃 */
pkts_no_server++; /* 增加统计 */
goto do_trace; /* 跳转到trace */
/* 说明:如果没有找到Proxy配置,丢弃报文。
*
* 原因:
* - 该接口未配置DHCP Proxy
* - 无法转发到服务器 */
}
if (!vlib_buffer_chain_linearize (vm, b0)) /* 线性化缓冲区链 */
/* 说明:将缓冲区链线性化为单个缓冲区。
*
* 功能:
* - 如果缓冲区是链式的,合并为单个缓冲区
* - 如果已经是单个缓冲区,直接返回成功
*
* 返回值:
* - 成功:非0
* - 失败:0(缓冲区太大无法线性化)
*
* 原因:
* - Option解析需要连续内存
* - 需要插入Option 82,需要连续空间 */
{
error0 = DHCP_PROXY_ERROR_PKT_TOO_BIG; /* 设置错误代码 */
next0 = DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP; /* 设置下一跳为丢弃 */
pkts_too_big++; /* 增加统计 */
goto do_trace; /* 跳转到trace */
}
space_left = vlib_buffer_space_left_at_end (vm, b0); /* 获取缓冲区尾部剩余空间 */
/* 说明:获取缓冲区尾部的剩余空间。
*
* 用途:
* - 检查是否有足够空间插入Option 82
* - Option 82需要约24字节空间 */
/* cant parse chains...
* and we need some space for option 82*/
if ((b0->flags & VLIB_BUFFER_NEXT_PRESENT) != 0 || /* 检查是否有下一个缓冲区 */
space_left < VPP_DHCP_OPTION82_SIZE) /* 检查剩余空间 */
/* 说明:检查缓冲区状态和剩余空间。
*
* 条件1:b0->flags & VLIB_BUFFER_NEXT_PRESENT
* - 如果设置了此标志,说明还有下一个缓冲区
* - 线性化失败或缓冲区链未完全合并
*
* 条件2:space_left < VPP_DHCP_OPTION82_SIZE
* - 剩余空间不足插入Option 82
* - Option 82需要约24字节(可能更多,如果包含VSS)
*
* 如果任一条件满足,丢弃报文 */
{
error0 = DHCP_PROXY_ERROR_PKT_TOO_BIG; /* 设置错误代码 */
next0 = DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP; /* 设置下一跳为丢弃 */
pkts_too_big++; /* 增加统计 */
goto do_trace; /* 跳转到trace */
}
6.4.3.4 IP头部修改
server = &proxy->dhcp_servers[0]; /* 获取第一个服务器 */
/* 说明:获取Proxy配置中的第一个服务器。
*
* 用途:
* - 修改IP目标地址为服务器地址
* - 后续可能复制到多个服务器 */
vlib_buffer_advance (b0, -(sizeof (*ip0))); /* 回退到IP头部 */
/* 说明:将缓冲区指针回退到IP头部。
*
* 操作:向前移动(负值)
* - 从UDP头部回退到IP头部
* - 需要修改IP头部(源地址、目标地址、长度、校验和) */
ip0 = vlib_buffer_get_current (b0); /* 获取IP头部 */
/* disable UDP checksum */
u0->checksum = 0; /* 禁用UDP校验和 */
/* 说明:将UDP校验和设置为0。
*
* 原因:
* - 修改了IP头部,UDP校验和需要重新计算
* - 设置为0表示不校验(可选)
* - 或者可以重新计算UDP校验和 */
sum0 = ip0->checksum; /* 保存原始IP校验和 */
old0 = ip0->dst_address.as_u32; /* 保存原始目标地址 */
new0 = server->dhcp_server.ip4.as_u32; /* 新目标地址(服务器地址) */
ip0->dst_address.as_u32 = server->dhcp_server.ip4.as_u32; /* 修改目标地址 */
/* 说明:修改IP目标地址为DHCP服务器地址。
*
* 操作:
* - 保存原始地址和校验和
* - 修改目标地址
* - 更新校验和 */
sum0 = ip_csum_update (sum0, old0, new0, /* 更新IP校验和 */
ip4_header_t /* structure */ ,
dst_address /* changed member */ );
/* 说明:增量更新IP校验和。
*
* 方法:
* - 不重新计算整个校验和
* - 只更新变化的部分
* - 提高性能
*
* 参数:
* - sum0:原始校验和
* - old0:原始地址
* - new0:新地址
* - 结构体类型和成员名(用于计算偏移) */
ip0->checksum = ip_csum_fold (sum0); /* 折叠校验和 */
/* 说明:将校验和折叠为16位。
*
* 操作:
* - 将32位校验和折叠为16位
* - 处理进位 */
sum0 = ip0->checksum; /* 保存当前IP校验和 */
old0 = ip0->src_address.as_u32; /* 保存原始源地址 */
new0 = proxy->dhcp_src_address.ip4.as_u32; /* 新源地址(Proxy源地址) */
ip0->src_address.as_u32 = new0; /* 修改源地址 */
/* 说明:修改IP源地址为Proxy配置的源地址。
*
* 原因:
* - 客户端发送的源地址可能是0.0.0.0或客户端地址
* - Proxy需要用自己的地址作为源地址
* - 服务器回复时使用此地址 */
sum0 = ip_csum_update (sum0, old0, new0, /* 更新IP校验和 */
ip4_header_t /* structure */ ,
src_address /* changed member */ );
ip0->checksum = ip_csum_fold (sum0); /* 折叠校验和 */
/* Send to DHCP server via the configured FIB */
vnet_buffer (b0)->sw_if_index[VLIB_TX] = server->server_fib_index; /* 设置发送FIB */
/* 说明:设置发送FIB索引。
*
* 用途:
* - 指定发送报文的FIB表
* - 服务器可能在不同的VRF中
* - 路由查找使用指定的FIB表 */
h0->gateway_ip_address = proxy->dhcp_src_address.ip4; /* 设置网关IP地址 */
/* 说明:设置DHCP头部的网关IP地址字段。
*
* 用途:
* - DHCP协议中的giaddr字段
* - 服务器使用此地址识别客户端所在的网段
* - 通常设置为Proxy的源地址 */
pkts_to_server++; /* 增加转发到服务器的统计 */
6.4.3.5 Option解析和Option 82插入
o = h0->options; /* 获取Option起始位置 */
end = (void *) vlib_buffer_get_tail (b0); /* 获取缓冲区尾部 */
/* 说明:获取Option解析的边界。
*
* o:Option起始位置(DHCP头部之后)
* end:缓冲区尾部(防止越界)
*
* 用途:
* - 遍历所有Option
* - 查找Option结束位置
* - 检查消息类型 */
/* TLVs are not performance-friendly... */
while (o->option != DHCP_PACKET_OPTION_END && o < end) /* 遍历Option */
/* 说明:遍历DHCP Option,查找消息类型和结束位置。
*
* 条件:
* - 不是结束Option(0xFF)
* - 未到达缓冲区尾部
*
* 用途:
* - 查找消息类型Option(53)
* - 确定Option结束位置
* - 检查是否为Discover消息 */
{
if (DHCP_PACKET_OPTION_MSG_TYPE == o->option) /* 检查消息类型Option */
/* 说明:检查是否为消息类型Option(53)。
*
* 消息类型:
* - 1:Discover
* - 2:Offer
* - 3:Request
* - 等等 */
{
if (DHCP_PACKET_DISCOVER == o->data[0]) /* 检查是否为Discover */
/* 说明:检查是否为Discover消息。
*
* 用途:
* - Discover消息需要复制到所有服务器
* - 其他消息只发送到第一个服务器 */
{
is_discover = 1; /* 设置Discover标志 */
}
}
o = (dhcp_option_t *) (o->data + o->length); /* 移动到下一个Option */
/* 说明:移动到下一个Option。
*
* 计算:
* - 当前Option的数据部分:o->data
* - 数据长度:o->length
* - 下一个Option位置:o->data + o->length */
}
if (o->option == DHCP_PACKET_OPTION_END && o <= end) /* 检查Option结束位置 */
/* 说明:检查是否找到有效的Option结束位置。
*
* 条件:
* - 找到结束Option(0xFF)
* - 位置在缓冲区范围内
*
* 如果条件满足,可以插入Option 82 */
{
vnet_main_t *vnm = vnet_get_main (); /* 获取VNET主结构体 */
u16 old_l0, new_l0; /* IP长度变量 */
ip4_address_t _ia0, *ia0 = &_ia0; /* 接口地址 */
dhcp_vss_t *vss; /* VSS信息 */
vnet_sw_interface_t *swif; /* 软件接口 */
original_sw_if_index = sw_if_index = /* 获取接收接口索引 */
vnet_buffer (b0)->sw_if_index[VLIB_RX];
swif = vnet_get_sw_interface (vnm, sw_if_index); /* 获取软件接口 */
if (swif->flags & VNET_SW_INTERFACE_FLAG_UNNUMBERED) /* 检查是否为未编号接口 */
/* 说明:检查接口是否为未编号接口。
*
* 未编号接口:
* - 没有自己的IP地址
* - 借用其他接口的IP地址
* - 需要获取被借用接口的地址 */
sw_if_index = swif->unnumbered_sw_if_index; /* 使用被借用接口 */
/* 说明:如果接口是未编号的,使用被借用接口的索引。
*
* 用途:
* - 获取被借用接口的IP地址
* - 用于Option 82的Suboption 5 */
/*
* Get the first ip4 address on the [client-side]
* RX interface, if not unnumbered. otherwise use
* the loopback interface's ip address.
*/
ia0 = ip4_interface_first_address (&ip4_main, sw_if_index, 0); /* 获取接口第一个IP地址 */
/* 说明:获取接口的第一个IPv4地址。
*
* 用途:
* - 用于Option 82的Suboption 5(客户端接收接口地址)
* - 服务器使用此地址识别客户端所在的网段
*
* 返回值:
* - 成功:IP地址指针
* - 失败:NULL(接口没有IP地址) */
if (ia0 == 0) /* 检查接口地址 */
/* 说明:检查是否获取到接口地址。
*
* 如果接口没有IP地址,无法插入Option 82 */
{
error0 = DHCP_PROXY_ERROR_NO_INTERFACE_ADDRESS; /* 设置错误代码 */
next0 = DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP; /* 设置下一跳为丢弃 */
pkts_no_interface_address++; /* 增加统计 */
goto do_trace; /* 跳转到trace */
}
/* Add option 82 */
o->option = 82; /* option 82 */
o->length = 12; /* 12 octets to follow */
/* 说明:设置Option 82头部。
*
* Option 82结构:
* - option:82(Relay Agent Information)
* - length:12(初始长度,可能增加)
*
* 初始包含:
* - Suboption 1(Circuit ID):6字节
* - Suboption 5(Remote ID):6字节
* - 可能添加VSS选项 */
o->data[0] = 1; /* suboption 1, circuit ID (=FIB id) */
o->data[1] = 4; /* length of suboption */
/* 说明:设置Suboption 1(Circuit ID)。
*
* 结构:
* - data[0]:子选项类型(1)
* - data[1]:子选项长度(4)
* - data[2-5]:FIB ID(接口索引) */
u32 *o_ifid = (u32 *) & o->data[2]; /* 获取FIB ID位置 */
*o_ifid = clib_host_to_net_u32 (original_sw_if_index); /* 设置FIB ID */
/* 说明:设置Circuit ID为原始接收接口索引。
*
* 用途:
* - 服务器使用此ID识别客户端所在的接口
* - 回复时根据此ID转发到正确的接口
*
* 字节序:
* - 转换为网络字节序 */
o->data[6] = 5; /* suboption 5 (client RX intfc address) */
o->data[7] = 4; /* length 4 */
/* 说明:设置Suboption 5(Remote ID/客户端接收接口地址)。
*
* 结构:
* - data[6]:子选项类型(5)
* - data[7]:子选项长度(4)
* - data[8-11]:接口IP地址 */
u32 *o_addr = (u32 *) & o->data[8]; /* 获取地址位置 */
*o_addr = ia0->as_u32; /* 设置接口地址 */
/* 说明:设置客户端接收接口的IP地址。
*
* 用途:
* - 服务器使用此地址识别客户端所在的网段
* - 用于地址分配和路由选择 */
o->data[12] = DHCP_PACKET_OPTION_END; /* 设置结束Option(初始) */
/* 说明:在Option 82之后设置结束Option。
*
* 初始位置:data[12]
* - 如果添加VSS,此位置会被覆盖
* - 最终位置会在VSS之后 */
vss = dhcp_get_vss_info (dpm, fib_index, FIB_PROTOCOL_IP4); /* 获取VSS信息 */
/* 说明:获取VSS(Virtual Subnet Selection)信息。
*
* VSS用途:
* - 支持多租户环境
* - 在同一物理网络上区分不同的虚拟网络
* - 服务器根据VSS分配不同的地址池 */
if (vss) /* 如果配置了VSS */
/* 说明:检查是否配置了VSS。
*
* 如果配置了VSS,需要在Option 82中添加VSS子选项 */
{
u32 id_len; /* length of VPN ID */
if (vss->vss_type == VSS_TYPE_VPN_ID) /* VPN ID类型 */
/* 说明:检查VSS类型是否为VPN ID。
*
* VPN ID:
* - 7字节的VPN标识符
* - 用于标识VPN */
{
id_len = sizeof (vss->vpn_id); /* vpn_id is 7 bytes */
memcpy (&o->data[15], vss->vpn_id, id_len); /* 复制VPN ID */
}
else if (vss->vss_type == VSS_TYPE_ASCII) /* ASCII类型 */
/* 说明:检查VSS类型是否为ASCII。
*
* ASCII类型:
* - 可变长度的ASCII字符串
* - 用于标识VPN或租户 */
{
id_len = vec_len (vss->vpn_ascii_id); /* 获取ASCII长度 */
memcpy (&o->data[15], vss->vpn_ascii_id, id_len); /* 复制ASCII ID */
}
else /* must be VSS_TYPE_DEFAULT, no VPN ID */
/* 说明:默认类型,没有VPN ID。
*
* 默认类型:
* - 不包含VPN ID
* - 只包含VSS类型和控制子选项 */
id_len = 0; /* VPN ID长度为0 */
o->data[12] = 151; /* vss suboption */
o->data[13] = id_len + 1; /* length: vss_type + id_len */
o->data[14] = vss->vss_type; /* vss option type */
/* 说明:设置VSS子选项(151)。
*
* 结构:
* - data[12]:子选项类型(151)
* - data[13]:子选项长度(VSS类型1字节 + VPN ID长度)
* - data[14]:VSS类型
* - data[15+]:VPN ID(如果有) */
o->data[15 + id_len] = 152; /* vss control suboption */
o->data[16 + id_len] = 0; /* length */
o->data[17 + id_len] = DHCP_PACKET_OPTION_END; /* "end-of-options" (0xFF) */
/* 说明:设置VSS控制子选项(152)和结束Option。
*
* 结构:
* - data[15+id_len]:子选项类型(152)
* - data[16+id_len]:子选项长度(0)
* - data[17+id_len]:结束Option(0xFF)
*
* VSS控制子选项:
* - 当前未使用,长度为0 */
/* 5 bytes for suboption headers 151+len, 152+len and 0xFF */
o->length += id_len + 5; /* 更新Option 82长度 */
/* 说明:更新Option 82的总长度。
*
* 增加的长度:
* - VSS子选项头部:2字节(151 + 长度)
* - VSS类型:1字节
* - VPN ID:id_len字节
* - VSS控制子选项头部:2字节(152 + 长度)
* - 结束Option:1字节(0xFF)
*
* 总计:id_len + 5字节 */
}
/* 2 bytes for option header 82+len */
len = o->length + 2; /* 计算Option总长度 */
/* 说明:计算Option 82的总长度(包括头部)。
*
* 长度组成:
* - Option类型:1字节(82)
* - Option长度:1字节
* - Option数据:o->length字节
*
* 总计:o->length + 2字节 */
b0->current_length += len; /* 更新缓冲区长度 */
/* 说明:更新缓冲区的当前长度。
*
* 原因:
* - 插入了Option 82,报文变长
* - 需要更新缓冲区长度字段 */
/* Fix IP header length and checksum */
old_l0 = ip0->length; /* 保存原始IP长度 */
new_l0 = clib_net_to_host_u16 (old_l0); /* 转换为主机字节序 */
new_l0 += len; /* 增加长度 */
new_l0 = clib_host_to_net_u16 (new_l0); /* 转换回网络字节序 */
ip0->length = new_l0; /* 更新IP长度 */
/* 说明:更新IP头部的总长度字段。
*
* 操作:
* - 保存原始长度
* - 转换为主机字节序
* - 增加Option 82长度
* - 转换回网络字节序
* - 更新IP头部 */
sum0 = ip0->checksum; /* 保存原始IP校验和 */
sum0 = ip_csum_update (sum0, old_l0, new_l0, ip4_header_t,
length /* changed member */ ); /* 更新校验和 */
ip0->checksum = ip_csum_fold (sum0); /* 折叠校验和 */
/* 说明:更新IP头部的校验和。
*
* 原因:
* - IP长度字段变化,需要更新校验和
* - 使用增量更新方法 */
/* Fix UDP length */
new_l0 = clib_net_to_host_u16 (u0->length); /* 获取UDP长度 */
new_l0 += len; /* 增加长度 */
u0->length = clib_host_to_net_u16 (new_l0); /* 更新UDP长度 */
/* 说明:更新UDP头部的长度字段。
*
* 原因:
* - UDP数据部分增加了Option 82
* - 需要更新UDP长度字段 */
}
else /* Option解析失败 */
/* 说明:如果Option解析失败,无法插入Option 82 */
{
vlib_node_increment_counter /* 增加错误统计 */
(vm, dhcp_proxy_to_server_node.index,
DHCP_PROXY_ERROR_OPTION_82_ERROR, 1);
/* 说明:增加Option 82错误统计。
*
* 错误原因:
* - 未找到结束Option
* - Option格式错误
* - 缓冲区越界 */
}
6.4.3.6 多服务器转发
next0 = DHCP_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP; /* 设置下一跳为查找 */
/* 说明:设置下一跳为IP查找节点。
*
* 下一跳:ip4-lookup
* - 根据目标地址查找路由
* - 转发到服务器 */
/*
* If we have multiple servers configured and this is the
* client's discover message, then send copies to each of
* those servers
*/
if (is_discover && vec_len (proxy->dhcp_servers) > 1) /* 检查多服务器转发 */
/* 说明:检查是否需要多服务器转发。
*
* 条件:
* - 是Discover消息(is_discover == 1)
* - 配置了多个服务器(vec_len > 1)
*
* 原因:
* - Discover消息是广播请求
* - 需要让所有服务器都知道客户端请求
* - 服务器可以竞争响应
* - 客户端选择最快的响应 */
{
u32 ii; /* 循环变量 */
for (ii = 1; ii < vec_len (proxy->dhcp_servers); ii++) /* 遍历其他服务器 */
/* 说明:遍历除第一个服务器外的其他服务器。
*
* 起始:ii = 1(第一个服务器已在主流程中处理)
* 结束:ii < vec_len(所有服务器) */
{
vlib_buffer_t *c0; /* 复制的缓冲区 */
u32 ci0; /* 复制的缓冲区索引 */
c0 = vlib_buffer_copy (vm, b0); /* 复制缓冲区 */
/* 说明:复制原始缓冲区。
*
* 功能:
* - 创建缓冲区的副本
* - 副本包含相同的报文内容
* - 可以独立修改和转发
*
* 返回值:
* - 成功:缓冲区指针
* - 失败:NULL(内存不足) */
if (c0 == NULL) /* 检查复制是否成功 */
/* 说明:检查缓冲区复制是否成功。
*
* 如果失败,跳过此服务器,继续处理下一个 */
{
vlib_node_increment_counter /* 增加分配失败统计 */
(vm, dhcp_proxy_to_server_node.index,
DHCP_PROXY_ERROR_ALLOC_FAIL, 1);
continue; /* 继续下一个服务器 */
}
ci0 = vlib_get_buffer_index (vm, c0); /* 获取缓冲区索引 */
server = &proxy->dhcp_servers[ii]; /* 获取当前服务器 */
ip0 = vlib_buffer_get_current (c0); /* 获取IP头部 */
sum0 = ip0->checksum; /* 保存IP校验和 */
old0 = ip0->dst_address.as_u32; /* 保存原始目标地址 */
new0 = server->dhcp_server.ip4.as_u32; /* 新目标地址 */
ip0->dst_address.as_u32 = server->dhcp_server.ip4.as_u32; /* 修改目标地址 */
/* 说明:修改复制缓冲区的目标地址为当前服务器地址。
*
* 操作:
* - 每个副本发送到不同的服务器
* - 需要修改目标地址 */
sum0 = ip_csum_update (sum0, old0, new0, /* 更新IP校验和 */
ip4_header_t /* structure */ ,
dst_address /* changed member */ );
ip0->checksum = ip_csum_fold (sum0); /* 折叠校验和 */
to_next[0] = ci0; /* 添加到输出向量 */
to_next += 1; /* 移动输出指针 */
n_left_to_next -= 1; /* 减少剩余空间 */
vlib_validate_buffer_enqueue_x1 (vm, node, next_index, /* 验证并入队 */
to_next, n_left_to_next,
ci0, next0);
/* 说明:验证并入队复制的缓冲区。
*
* 功能:
* - 验证缓冲区索引有效
* - 验证下一跳节点有效
* - 将缓冲区添加到输出帧 */
if (PREDICT_FALSE (b0->flags & VLIB_BUFFER_IS_TRACED)) /* 检查是否需要trace */
/* 说明:检查原始缓冲区是否需要trace。
*
* 如果原始缓冲区需要trace,复制的缓冲区也需要trace */
{
dhcp_proxy_trace_t *tr; /* Trace结构体 */
tr = vlib_add_trace (vm, node, c0, sizeof (*tr)); /* 添加trace */
tr->which = 0; /* to server */
tr->error = error0;
tr->original_sw_if_index = original_sw_if_index;
tr->sw_if_index = sw_if_index;
if (next0 == DHCP_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP)
tr->trace_ip4_address.as_u32 =
server->dhcp_server.ip4.as_u32; /* 设置服务器地址 */
clib_memcpy_fast (tr->packet_data, h0,
sizeof (tr->packet_data)); /* 复制报文数据 */
}
if (PREDICT_FALSE (0 == n_left_to_next)) /* 检查输出帧空间 */
/* 说明:检查输出帧是否已满。
*
* 如果已满,需要获取新的输出帧 */
{
vlib_put_next_frame (vm, node, next_index, /* 释放当前帧 */
n_left_to_next);
vlib_get_next_frame (vm, node, next_index, /* 获取新帧 */
to_next, n_left_to_next);
}
}
}
6.4.3.7 Trace和入队
do_trace:
if (PREDICT_FALSE (b0->flags & VLIB_BUFFER_IS_TRACED)) /* 检查是否需要trace */
/* 说明:检查缓冲区是否需要trace。
*
* 条件:
* - 缓冲区设置了VLIB_BUFFER_IS_TRACED标志
* - 通常通过CLI命令启用trace */
{
dhcp_proxy_trace_t *tr = vlib_add_trace (vm, node, /* 添加trace */
b0, sizeof (*tr));
/* 说明:添加trace记录。
*
* 功能:
* - 在缓冲区后添加trace数据
* - 记录处理信息
* - 用于调试和问题排查 */
tr->which = 0; /* to server */
/* 说明:设置方向标志。
*
* 0:客户端到服务器方向
* 1:服务器到客户端方向 */
tr->error = error0; /* 设置错误代码 */
tr->original_sw_if_index = original_sw_if_index; /* 设置原始接口索引 */
tr->sw_if_index = sw_if_index; /* 设置接口索引 */
if (next0 == DHCP_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP) /* 如果转发到服务器 */
tr->trace_ip4_address.as_u32 =
proxy->dhcp_servers[0].dhcp_server.ip4.as_u32; /* 设置服务器地址 */
/* 说明:如果转发到服务器,记录服务器地址。
*
* 用途:
* - 在trace中显示目标服务器地址
* - 用于调试和问题排查 */
clib_memcpy_fast (tr->packet_data, h0, /* 复制报文数据 */
sizeof (tr->packet_data));
/* 说明:复制DHCP报文数据到trace。
*
* 用途:
* - 在trace中显示报文内容
* - 用于调试和问题排查 */
}
do_enqueue:
to_next[0] = bi0; /* 添加到输出向量 */
to_next += 1; /* 移动输出指针 */
n_left_to_next -= 1; /* 减少剩余空间 */
vlib_validate_buffer_enqueue_x1 (vm, node, next_index, /* 验证并入队 */
to_next, n_left_to_next,
bi0, next0);
/* 说明:验证并入队缓冲区。
*
* 功能:
* - 验证缓冲区索引有效
* - 验证下一跳节点有效
* - 将缓冲区添加到输出帧
* - 更新统计信息 */
}
vlib_put_next_frame (vm, node, next_index, n_left_to_next); /* 释放输出帧 */
/* 说明:释放输出帧。
*
* 功能:
* - 将输出帧传递给下一跳节点
* - 更新下一跳节点的输入帧 */
}
vlib_node_increment_counter (vm, dhcp_proxy_to_server_node.index, /* 更新统计 */
DHCP_PROXY_ERROR_RELAY_TO_CLIENT,
pkts_to_client);
vlib_node_increment_counter (vm, dhcp_proxy_to_server_node.index,
DHCP_PROXY_ERROR_RELAY_TO_SERVER,
pkts_to_server);
vlib_node_increment_counter (vm, dhcp_proxy_to_server_node.index,
DHCP_PROXY_ERROR_NO_SERVER, pkts_no_server);
vlib_node_increment_counter (vm, dhcp_proxy_to_server_node.index,
DHCP_PROXY_ERROR_NO_INTERFACE_ADDRESS,
pkts_no_interface_address);
vlib_node_increment_counter (vm, dhcp_proxy_to_server_node.index,
DHCP_PROXY_ERROR_PKT_TOO_BIG, pkts_too_big);
/* 说明:更新节点统计计数器。
*
* 统计项:
* - RELAY_TO_CLIENT:转发到客户端的报文数
* - RELAY_TO_SERVER:转发到服务器的报文数
* - NO_SERVER:没有找到服务器的报文数
* - NO_INTERFACE_ADDRESS:没有接口地址的报文数
* - PKT_TOO_BIG:报文太大的报文数
*
* 用途:
* - 在CLI中显示统计信息
* - 用于监控和问题排查 */
return from_frame->n_vectors; /* 返回处理的报文数量 */
/* 说明:返回处理的报文数量。
*
* 返回值:
* - 处理的报文数量
* - 用于VPP的向量处理机制 */
}
客户端到服务器方向处理函数总结:
-
主要功能:
- 接收客户端DHCP请求
- 查找Proxy配置
- 插入Option 82(Circuit ID、Remote ID、VSS)
- 修改IP头部(源地址、目标地址)
- 转发到DHCP服务器
- 支持多服务器转发(Discover消息)
-
关键步骤:
- 返回流量检测(源端口67)
- Proxy配置查找(按FIB索引)
- 缓冲区线性化和空间检查
- IP头部修改和校验和更新
- Option 82插入(包含VSS支持)
- 多服务器复制(Discover消息)
-
错误处理:
- 没有Proxy配置
- 缓冲区太大无法线性化
- 没有接口地址
- Option解析失败
6.5 DHCPv6 Proxy节点实现
DHCPv6 Proxy与DHCPv4 Proxy在架构上类似,但实现细节有显著差异。DHCPv6使用Relay消息机制,而不是在原始报文中插入选项。本节详细分析DHCPv6 Proxy节点的实现。
6.5.1 节点基础结构
6.5.1.1 错误字符串定义
//25:29:src/plugins/dhcp/dhcp6_proxy_node.c
static char *dhcpv6_proxy_error_strings[] = {
#define dhcpv6_proxy_error(n,s) s,
#include <dhcp/dhcp6_proxy_error.def>
#undef dhcpv6_proxy_error
};
/* 说明:dhcpv6_proxy_error_strings数组包含所有DHCPv6 Proxy错误的字符串描述。
*
* 生成方式:
* - 通过宏定义从dhcp6_proxy_error.def文件生成
* - 每个错误对应一个字符串
*
* 用途:
* - 在trace中显示错误信息
* - 在CLI中显示错误统计
* - 用于调试和问题排查 */
6.5.1.2 下一跳节点枚举
//31:43:src/plugins/dhcp/dhcp6_proxy_node.c
#define foreach_dhcpv6_proxy_to_server_input_next \
_ (DROP, "error-drop") \
_ (LOOKUP, "ip6-lookup") \
_ (SEND_TO_CLIENT, "dhcpv6-proxy-to-client")
/* 说明:定义客户端到服务器方向节点的下一跳节点。
*
* 下一跳节点:
* - DROP:错误丢弃节点
* - LOOKUP:IPv6查找节点(转发到服务器)
* - SEND_TO_CLIENT:客户端节点(处理返回流量)
*
* 与DHCPv4的区别:
* - 使用ip6-lookup而不是ip4-lookup
* - 节点名称使用dhcpv6前缀 */
typedef enum
{
#define _(s,n) DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_##s,
foreach_dhcpv6_proxy_to_server_input_next
#undef _
DHCPV6_PROXY_TO_SERVER_INPUT_N_NEXT,
} dhcpv6_proxy_to_server_input_next_t;
/* 说明:客户端到服务器方向节点的下一跳枚举。
*
* 枚举值:
* - DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_DROP:错误丢弃
* - DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP:IPv6查找
* - DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_SEND_TO_CLIENT:发送到客户端节点
*
* 数量:3个下一跳节点 */
6.5.1.3 Trace结构体
//45:53:src/plugins/dhcp/dhcp6_proxy_node.c
typedef struct
{
/* 0 => to server, 1 => to client */
int which; /* 方向标志 */
/* 说明:which字段标识报文方向。
*
* 值:
* - 0:客户端到服务器方向
* - 1:服务器到客户端方向 */
u8 packet_data[64]; /* 报文数据 */
/* 说明:packet_data字段存储报文数据(用于trace)。
*
* 大小:64字节
* - 通常存储IP地址信息
* - 用于trace显示 */
u32 error; /* 错误代码 */
u32 sw_if_index; /* 软件接口索引 */
u32 original_sw_if_index; /* 原始软件接口索引 */
} dhcpv6_proxy_trace_t;
/* 说明:dhcpv6_proxy_trace_t结构体用于trace信息。
*
* 与DHCPv4的区别:
* - 使用u8 packet_data[64]而不是ip4_address_t
* - 可以存储IPv6地址(16字节)或其他信息 */
6.5.1.4 节点变量声明
//55:56:src/plugins/dhcp/dhcp6_proxy_node.c
static vlib_node_registration_t dhcpv6_proxy_to_server_node; /* 客户端到服务器方向节点 */
static vlib_node_registration_t dhcpv6_proxy_to_client_node; /* 服务器到客户端方向节点 */
/* 说明:声明两个节点的注册结构体。
*
* dhcpv6_proxy_to_server_node:
* - 处理客户端到服务器方向的报文
* - 将客户端消息封装为Relay-Forward消息
*
* dhcpv6_proxy_to_client_node:
* - 处理服务器到客户端方向的报文
* - 解封装Relay-Reply消息并转发到客户端 */
6.5.1.5 DHCPv6服务器地址常量
//58:60:src/plugins/dhcp/dhcp6_proxy_node.c
/* all DHCP servers address */
static ip6_address_t all_dhcpv6_server_address; /* 所有DHCPv6服务器地址 */
static ip6_address_t all_dhcpv6_server_relay_agent_address; /* 所有DHCPv6服务器和Relay代理地址 */
/* 说明:定义DHCPv6服务器地址常量。
*
* all_dhcpv6_server_address:
* - IPv6多播地址:FF05::1:3
* - 用于向所有DHCPv6服务器发送Relay消息
*
* all_dhcpv6_server_relay_agent_address:
* - IPv6多播地址:FF02::1:2
* - 用于向所有DHCPv6服务器和Relay代理发送消息
*
* 初始化:
* - 在dhcp6_proxy_init函数中初始化
* - 根据RFC 3315定义 */
6.5.1.6 Trace格式化函数
//62:82:src/plugins/dhcp/dhcp6_proxy_node.c
static u8 *
format_dhcpv6_proxy_trace (u8 * s, va_list * args)
{
CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *);
CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *);
dhcpv6_proxy_trace_t *t = va_arg (*args, dhcpv6_proxy_trace_t *);
if (t->which == 0) /* 客户端到服务器方向 */
s = format (s, "DHCPV6 proxy: sent to server %U",
format_ip6_address, &t->packet_data, sizeof (ip6_address_t));
/* 说明:格式化客户端到服务器方向的trace信息。
*
* 显示内容:
* - "DHCPV6 proxy: sent to server"
* - 服务器IPv6地址 */
else /* 服务器到客户端方向 */
s = format (s, "DHCPV6 proxy: sent to client from %U",
format_ip6_address, &t->packet_data, sizeof (ip6_address_t));
/* 说明:格式化服务器到客户端方向的trace信息。
*
* 显示内容:
* - "DHCPV6 proxy: sent to client from"
* - 源IPv6地址 */
if (t->error != (u32) ~ 0) /* 如果有错误 */
s = format (s, " error: %s\n", dhcpv6_proxy_error_strings[t->error]);
/* 说明:如果有错误,显示错误信息。 */
s = format (s, " original_sw_if_index: %d, sw_if_index: %d\n",
t->original_sw_if_index, t->sw_if_index);
/* 说明:显示接口索引信息。
*
* original_sw_if_index:原始接收接口索引
* sw_if_index:实际使用的接口索引(可能是未编号接口的被借用接口) */
return s;
}
6.5.1.7 辅助函数
//100:119:src/plugins/dhcp/dhcp6_proxy_node.c
/* get first interface address */
static ip6_address_t *
ip6_interface_first_global_or_site_address (ip6_main_t * im, u32 sw_if_index)
{
/* 说明:获取接口的第一个全局或站点本地IPv6地址。
*
* 功能:
* - 遍历接口的所有IPv6地址
* - 查找全局单播地址(2000::/3)或站点本地地址(FC00::/7)
* - 返回第一个匹配的地址
*
* 参数:
* - im:IPv6主结构体
* - sw_if_index:软件接口索引
*
* 返回值:
* - 成功:IPv6地址指针
* - 失败:NULL
*
* 用途:
* - 用于设置Relay消息的link-address字段 */
ip_lookup_main_t *lm = &im->lookup_main;
ip_interface_address_t *ia = 0;
ip6_address_t *result = 0;
foreach_ip_interface_address (lm, ia, sw_if_index,
1 /* honor unnumbered */,
({
ip6_address_t * a = ip_interface_address_get_address (lm, ia);
if ((a->as_u8[0] & 0xe0) == 0x20 || /* 全局单播地址(2000::/3) */
(a->as_u8[0] & 0xfe) == 0xfc) { /* 站点本地地址(FC00::/7) */
result = a;
break;
}
}));
return result;
}
//121:126:src/plugins/dhcp/dhcp6_proxy_node.c
static inline void
copy_ip6_address (ip6_address_t * dst, ip6_address_t * src)
{
dst->as_u64[0] = src->as_u64[0];
dst->as_u64[1] = src->as_u64[1];
}
/* 说明:复制IPv6地址。
*
* 功能:
* - 将源IPv6地址复制到目标地址
* - 使用两个u64操作,提高性能
*
* 用途:
* - 复制IP地址到Relay消息头部
* - 复制地址到trace结构体 */
6.5.2 节点注册
6.5.2.1 客户端到服务器方向节点注册
//533:554:src/plugins/dhcp/dhcp6_proxy_node.c
VLIB_REGISTER_NODE (dhcpv6_proxy_to_server_node, static) = {
.function = dhcpv6_proxy_to_server_input, /* 节点处理函数 */
.name = "dhcpv6-proxy-to-server", /* 节点名称 */
/* Takes a vector of packets. */
.vector_size = sizeof (u32), /* 向量元素大小 */
.n_errors = DHCPV6_PROXY_N_ERROR, /* 错误数量 */
.error_strings = dhcpv6_proxy_error_strings, /* 错误字符串数组 */
.n_next_nodes = DHCPV6_PROXY_TO_SERVER_INPUT_N_NEXT, /* 下一跳节点数量 */
.next_nodes = {
#define _(s,n) [DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_##s] = n,
foreach_dhcpv6_proxy_to_server_input_next
#undef _
},
.format_buffer = format_dhcpv6_proxy_header_with_length, /* 缓冲区格式化函数 */
.format_trace = format_dhcpv6_proxy_trace, /* Trace格式化函数 */
#if 0
.unformat_buffer = unformat_dhcpv6_proxy_header, /* 缓冲区解析函数(未使用) */
#endif
};
/* 说明:注册客户端到服务器方向的DHCPv6 Proxy节点。
*
* 节点名称:dhcpv6-proxy-to-server
* - 处理客户端到服务器方向的DHCPv6报文
* - 将客户端消息封装为Relay-Forward消息
*
* 下一跳节点:
* - error-drop:错误丢弃
* - ip6-lookup:IPv6查找(转发到服务器)
* - dhcpv6-proxy-to-client:发送到客户端节点(处理返回流量) */
6.5.2.2 服务器到客户端方向节点注册
//825:838:src/plugins/dhcp/dhcp6_proxy_node.c
VLIB_REGISTER_NODE (dhcpv6_proxy_to_client_node, static) = {
.function = dhcpv6_proxy_to_client_input, /* 节点处理函数 */
.name = "dhcpv6-proxy-to-client", /* 节点名称 */
/* Takes a vector of packets. */
.vector_size = sizeof (u32), /* 向量元素大小 */
.n_errors = DHCPV6_PROXY_N_ERROR, /* 错误数量 */
.error_strings = dhcpv6_proxy_error_strings, /* 错误字符串数组 */
.format_buffer = format_dhcpv6_proxy_header_with_length, /* 缓冲区格式化函数 */
.format_trace = format_dhcpv6_proxy_trace, /* Trace格式化函数 */
#if 0
.unformat_buffer = unformat_dhcpv6_proxy_header, /* 缓冲区解析函数(未使用) */
#endif
};
/* 说明:注册服务器到客户端方向的DHCPv6 Proxy节点。
*
* 节点名称:dhcpv6-proxy-to-client
* - 处理服务器到客户端方向的DHCPv6报文
* - 解封装Relay-Reply消息
* - 转发到客户端
*
* 注意:
* - 此节点没有定义.next_nodes字段
* - 使用动态下一跳(通过vlib_get_frame_to_node获取)
* - 直接转发到硬件接口的输出节点 */
6.5.2.3 初始化函数
//840:863:src/plugins/dhcp/dhcp6_proxy_node.c
static clib_error_t *
dhcp6_proxy_init (vlib_main_t * vm) /* DHCPv6 Proxy初始化函数 */
{
dhcp_proxy_main_t *dm = &dhcp_proxy_main; /* 获取DHCP Proxy主结构体 */
vlib_node_t *error_drop_node; /* 错误丢弃节点 */
error_drop_node = vlib_get_node_by_name (vm, (u8 *) "error-drop"); /* 查找错误丢弃节点 */
dm->error_drop_node_index = error_drop_node->index; /* 保存错误丢弃节点索引 */
/* RFC says this is the dhcpv6 server address */
all_dhcpv6_server_address.as_u64[0] =
clib_host_to_net_u64 (0xFF05000000000000);
all_dhcpv6_server_address.as_u64[1] = clib_host_to_net_u64 (0x00010003);
/* 说明:初始化所有DHCPv6服务器地址。
*
* 地址:FF05::1:3
* - FF05:站点本地范围多播地址
* - ::1:3:DHCPv6服务器标识符
*
* 用途:
* - 当服务器地址未配置时,使用此多播地址
* - 向所有DHCPv6服务器发送Relay消息 */
/* RFC says this is the server and agent address */
all_dhcpv6_server_relay_agent_address.as_u64[0] =
clib_host_to_net_u64 (0xFF02000000000000);
all_dhcpv6_server_relay_agent_address.as_u64[1] =
clib_host_to_net_u64 (0x00010002);
/* 说明:初始化所有DHCPv6服务器和Relay代理地址。
*
* 地址:FF02::1:2
* - FF02:链路本地范围多播地址
* - ::1:2:DHCPv6服务器和Relay代理标识符
*
* 用途:
* - 用于MFIB表项配置
* - 接收来自服务器的Relay-Reply消息 */
return 0; /* 返回成功 */
}
VLIB_INIT_FUNCTION (dhcp6_proxy_init); /* 注册初始化函数 */
/* 说明:VLIB_INIT_FUNCTION宏用于注册VPP初始化函数。
*
* 功能:
* - 在VPP启动时自动调用dhcp6_proxy_init函数
* - 初始化错误丢弃节点索引
* - 初始化DHCPv6服务器多播地址 */
6.5.3 DHCPv6 Proxy错误处理
6.5.3.1 错误定义文件分析
//18:31:src/plugins/dhcp/dhcp6_proxy_error.def
dhcpv6_proxy_error (NONE, "no error") /* 无错误 */
dhcpv6_proxy_error (NO_SERVER, "no dhcpv6 server configured") /* 没有配置DHCPv6服务器 */
dhcpv6_proxy_error (RELAY_TO_SERVER, "DHCPV6 packets relayed to the server") /* 转发到服务器的报文 */
dhcpv6_proxy_error (RELAY_TO_CLIENT, "DHCPV6 packets relayed to clients") /* 转发到客户端的报文 */
dhcpv6_proxy_error (NO_INTERFACE_ADDRESS, "DHCPV6 no interface address") /* 没有接口地址 */
dhcpv6_proxy_error (WRONG_MESSAGE_TYPE, "DHCPV6 wrong message type.") /* 错误的消息类型 */
dhcpv6_proxy_error (NO_SRC_ADDRESS, "DHCPV6 no srouce IPv6 address configured.") /* 没有配置源IPv6地址 */
dhcpv6_proxy_error (NO_CIRCUIT_ID_OPTION, "DHCPv6 reply packets without circuit ID option") /* 回复报文没有Circuit ID选项 */
dhcpv6_proxy_error (NO_RELAY_MESSAGE_OPTION, "DHCPv6 reply packets without relay message option") /* 回复报文没有Relay消息选项 */
dhcpv6_proxy_error (BAD_SVR_FIB_OR_ADDRESS, "DHCPv6 packets not from DHCPv6 server or server FIB.") /* 报文不是来自DHCPv6服务器或服务器FIB */
dhcpv6_proxy_error (PKT_TOO_BIG, "DHCPv6 packets which are too big.") /* 报文太大 */
dhcpv6_proxy_error (WRONG_INTERFACE_ID_OPTION, "DHCPv6 reply to invalid interface.") /* 回复到无效接口 */
dhcpv6_proxy_error (ALLOC_FAIL, "DHCPv6 buffer allocation failures.") /* 缓冲区分配失败 */
/* 说明:dhcp6_proxy_error.def文件定义所有DHCPv6 Proxy错误。
*
* 错误类型分类:
* 1. 配置错误:
* - NO_SERVER:没有配置服务器
* - NO_INTERFACE_ADDRESS:没有接口地址
* - NO_SRC_ADDRESS:没有源地址
*
* 2. 报文错误:
* - WRONG_MESSAGE_TYPE:错误的消息类型
* - PKT_TOO_BIG:报文太大
* - NO_CIRCUIT_ID_OPTION:缺少Circuit ID选项
* - NO_RELAY_MESSAGE_OPTION:缺少Relay消息选项
* - WRONG_INTERFACE_ID_OPTION:错误的接口ID选项
* - BAD_SVR_FIB_OR_ADDRESS:错误的服务器FIB或地址
*
* 3. 系统错误:
* - ALLOC_FAIL:内存分配失败
*
* 4. 统计错误(非错误,用于计数):
* - RELAY_TO_SERVER:转发到服务器的报文数
* - RELAY_TO_CLIENT:转发到客户端的报文数 */
6.5.3.2 错误类型详解
-
配置相关错误:
NO_SERVER:没有为接收接口的FIB配置DHCPv6服务器NO_INTERFACE_ADDRESS:接收接口没有全局或站点本地IPv6地址NO_SRC_ADDRESS:没有配置Proxy源地址,且接口也没有IPv6地址
-
报文格式错误:
WRONG_MESSAGE_TYPE:收到不支持的消息类型PKT_TOO_BIG:报文太大,无法添加Relay头部和选项NO_CIRCUIT_ID_OPTION:Relay-Reply消息缺少Interface-ID选项NO_RELAY_MESSAGE_OPTION:Relay-Reply消息缺少Relay-Message选项WRONG_INTERFACE_ID_OPTION:Interface-ID选项中的接口索引无效
-
安全验证错误:
BAD_SVR_FIB_OR_ADDRESS:Relay-Reply消息不是来自配置的服务器或服务器FIB
-
系统资源错误:
ALLOC_FAIL:复制缓冲区失败(多服务器转发时)
6.5.3.3 错误统计计数
错误统计在节点处理函数中通过vlib_node_increment_counter更新:
//513:529:src/plugins/dhcp/dhcp6_proxy_node.c
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_RELAY_TO_CLIENT,
pkts_to_client);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_RELAY_TO_SERVER,
pkts_to_server);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_NO_INTERFACE_ADDRESS,
pkts_no_interface_address);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_WRONG_MESSAGE_TYPE,
pkts_wrong_msg_type);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_NO_SRC_ADDRESS,
pkts_no_src_address);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_PKT_TOO_BIG, pkts_too_big);
/* 说明:更新节点统计计数器。
*
* 统计项:
* - RELAY_TO_CLIENT:转发到客户端的报文数(返回流量)
* - RELAY_TO_SERVER:转发到服务器的报文数
* - NO_INTERFACE_ADDRESS:没有接口地址的报文数
* - WRONG_MESSAGE_TYPE:错误消息类型的报文数
* - NO_SRC_ADDRESS:没有源地址的报文数
* - PKT_TOO_BIG:报文太大的报文数
*
* 用途:
* - 在CLI中显示统计信息(show node counters)
* - 用于监控和问题排查 */
6.5.4 客户端到服务器方向处理函数(RELAY-FORW)
客户端到服务器方向的处理函数dhcpv6_proxy_to_server_input将客户端的DHCPv6消息封装为Relay-Forward消息,并转发到服务器。这是DHCPv6 Proxy的核心功能。
6.5.4.1 函数概述和变量声明
//128:147:src/plugins/dhcp/dhcp6_proxy_node.c
static uword
dhcpv6_proxy_to_server_input (vlib_main_t * vm, /* VPP主结构体 */
vlib_node_runtime_t * node, /* 节点运行时结构体 */
vlib_frame_t * from_frame) /* 输入帧 */
{
/* 说明:dhcpv6_proxy_to_server_input函数处理客户端到服务器方向的DHCPv6报文。
*
* 功能:
* - 接收客户端的DHCPv6消息(Solicit、Request、Renew等)
* - 将消息封装为Relay-Forward消息
* - 插入Interface-ID、Client MAC、VSS等选项
* - 设置Link-address和Peer-address
* - 转发到DHCPv6服务器
* - 支持多服务器转发(Solicit消息)
*
* 与DHCPv4的区别:
* - DHCPv6使用Relay消息机制,而不是在原始报文中插入选项
* - 需要构建新的IP和UDP头部
* - 支持Relay链(多级Relay) */
u32 n_left_from, next_index, *from, *to_next; /* 向量处理变量 */
dhcp_proxy_main_t *dpm = &dhcp_proxy_main; /* DHCP Proxy主结构体 */
from = vlib_frame_vector_args (from_frame); /* 获取输入向量 */
n_left_from = from_frame->n_vectors; /* 获取报文数量 */
u32 pkts_to_server = 0, pkts_to_client = 0; /* 统计计数器 */
u32 pkts_no_interface_address = 0; /* 没有接口地址的报文数 */
u32 pkts_no_src_address = 0; /* 没有源地址的报文数 */
u32 pkts_wrong_msg_type = 0; /* 错误消息类型的报文数 */
u32 pkts_too_big = 0; /* 报文太大的报文数 */
ip6_main_t *im = &ip6_main; /* IPv6主结构体 */
ip6_address_t *src; /* 源地址指针 */
int bogus_length; /* 校验和计算标志 */
dhcp_proxy_t *proxy; /* Proxy配置指针 */
dhcp_server_t *server; /* 服务器配置指针 */
u32 rx_fib_idx = 0, server_fib_idx = 0; /* FIB索引 */
next_index = node->cached_next_index; /* 获取缓存的下一跳索引 */
6.5.4.2 报文遍历和消息类型检查
//151:225:src/plugins/dhcp/dhcp6_proxy_node.c
while (n_left_from > 0) /* 外层循环:处理多个帧 */
{
u32 n_left_to_next;
vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);
while (n_left_from > 0 && n_left_to_next > 0) /* 内层循环:处理单个报文 */
{
vnet_main_t *vnm = vnet_get_main ();
u32 sw_if_index = 0;
u32 rx_sw_if_index = 0;
vnet_sw_interface_t *swif;
u32 bi0;
vlib_buffer_t *b0;
udp_header_t *u0, *u1; /* u0:原始UDP头部,u1:新UDP头部 */
dhcpv6_header_t *h0; // client msg hdr /* 客户端消息头部 */
ip6_header_t *ip0, *ip1; /* ip0:原始IP头部,ip1:新IP头部 */
ip6_address_t _ia0, *ia0 = &_ia0;
u32 next0;
u32 error0 = (u32) ~ 0;
dhcpv6_option_t *fwd_opt; /* Relay-Message选项 */
dhcpv6_relay_hdr_t *r1; /* Relay头部 */
u16 len;
dhcpv6_int_id_t *id1; /* Interface-ID选项 */
dhcpv6_vss_t *vss1; /* VSS选项 */
dhcpv6_client_mac_t *cmac; // client mac /* 客户端MAC选项 */
ethernet_header_t *e_h0; /* 以太网头部 */
u8 client_src_mac[6]; /* 客户端源MAC地址 */
dhcp_vss_t *vss;
u8 is_solicit = 0; /* 是否为Solicit消息 */
bi0 = from[0];
from += 1;
n_left_from -= 1;
b0 = vlib_get_buffer (vm, bi0);
h0 = vlib_buffer_get_current (b0); /* 获取DHCPv6头部 */
/* 说明:获取当前缓冲区位置的DHCPv6头部。
*
* 当前状态:
* - udp_local节点已将缓冲区指针定位到DHCPv6头部
* - 需要回退获取UDP、IP和以太网头部 */
/*
* udp_local hands us the DHCPV6 header.
*/
u0 = (void *) h0 - (sizeof (*u0)); /* 回退到UDP头部 */
ip0 = (void *) u0 - (sizeof (*ip0)); /* 回退到IP头部 */
e_h0 = (void *) ip0 - ethernet_buffer_header_size (b0); /* 回退到以太网头部 */
clib_memcpy (client_src_mac, e_h0->src_address, 6); /* 复制客户端MAC地址 */
/* 说明:从以太网头部复制客户端源MAC地址。
*
* 用途:
* - 用于插入Client Link-Layer Address选项
* - 服务器可以使用MAC地址识别客户端 */
switch (h0->msg_type) /* 检查消息类型 */
/* 说明:根据消息类型决定处理方式。
*
* 支持的消息类型:
* - SOLICIT、REQUEST、CONFIRM、RENEW、REBIND、RELEASE、DECLINE、INFORMATION_REQUEST:转发到服务器
* - RELAY_FORW:已经是Relay消息,继续转发(支持Relay链)
* - RELAY_REPL:服务器回复,转发到客户端节点 */
{
case DHCPV6_MSG_SOLICIT:
case DHCPV6_MSG_REQUEST:
case DHCPV6_MSG_CONFIRM:
case DHCPV6_MSG_RENEW:
case DHCPV6_MSG_REBIND:
case DHCPV6_MSG_RELEASE:
case DHCPV6_MSG_DECLINE:
case DHCPV6_MSG_INFORMATION_REQUEST:
case DHCPV6_MSG_RELAY_FORW:
/* send to server */
break; /* 继续处理,转发到服务器 */
case DHCPV6_MSG_RELAY_REPL:
/* send to client */
next0 = DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_SEND_TO_CLIENT; /* 转发到客户端节点 */
error0 = 0;
pkts_to_client++;
goto do_enqueue; /* 直接跳转到入队 */
/* 说明:如果是Relay-Reply消息,直接转发到客户端节点。
*
* 原因:
* - Relay-Reply是服务器的回复
* - 不需要再次封装
* - 由客户端节点解封装并转发 */
default:
/* drop the packet */
pkts_wrong_msg_type++;
error0 = DHCPV6_PROXY_ERROR_WRONG_MESSAGE_TYPE;
next0 = DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_DROP;
goto do_trace; /* 跳转到trace,然后丢弃 */
/* 说明:不支持的消息类型,丢弃报文。 */
}
6.5.4.3 Proxy配置查找
//227:242:src/plugins/dhcp/dhcp6_proxy_node.c
/* Send to DHCPV6 server via the configured FIB */
rx_sw_if_index = sw_if_index =
vnet_buffer (b0)->sw_if_index[VLIB_RX]; /* 获取接收接口索引 */
rx_fib_idx = im->mfib_index_by_sw_if_index[rx_sw_if_index]; /* 获取MFIB索引 */
/* 说明:根据接收接口获取MFIB索引。
*
* 与DHCPv4的区别:
* - DHCPv6使用MFIB(Multicast FIB)而不是FIB
* - 因为DHCPv6使用多播地址(FF05::1:3) */
proxy = dhcp_get_proxy (dpm, rx_fib_idx, FIB_PROTOCOL_IP6); /* 查找Proxy配置 */
/* 说明:查找对应MFIB的Proxy配置。
*
* 参数:
* - dpm:DHCP Proxy主结构体
* - rx_fib_idx:接收MFIB索引
* - FIB_PROTOCOL_IP6:IPv6协议
*
* 返回值:
* - 成功:Proxy配置指针
* - 失败:NULL */
if (PREDICT_FALSE (NULL == proxy)) /* 检查Proxy配置 */
{
error0 = DHCPV6_PROXY_ERROR_NO_SERVER;
next0 = DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_DROP;
goto do_trace; /* 没有配置,丢弃报文 */
}
server = &proxy->dhcp_servers[0]; /* 获取第一个服务器 */
server_fib_idx = server->server_fib_index; /* 获取服务器FIB索引 */
vnet_buffer (b0)->sw_if_index[VLIB_TX] = server_fib_idx; /* 设置发送FIB */
/* 说明:设置发送FIB索引。
*
* 用途:
* - 指定发送报文的FIB表
* - 服务器可能在不同的VRF中 */
6.5.4.4 Relay消息头部构建
//244:275:src/plugins/dhcp/dhcp6_proxy_node.c
/* relay-option header pointer */
vlib_buffer_advance (b0, -(sizeof (*fwd_opt))); /* 为Relay-Message选项预留空间 */
fwd_opt = vlib_buffer_get_current (b0);
/* 说明:为Relay-Message选项预留空间。
*
* Relay-Message选项:
* - 包含原始的客户端消息
* - 位于Relay头部之后 */
/* relay message header pointer */
vlib_buffer_advance (b0, -(sizeof (*r1))); /* 为Relay头部预留空间 */
r1 = vlib_buffer_get_current (b0);
/* 说明:为Relay头部预留空间。
*
* Relay头部结构:
* - msg_type:消息类型(RELAY_FORW)
* - hop_count:跳数
* - link_addr:链路地址(16字节)
* - peer_addr:对等地址(16字节) */
vlib_buffer_advance (b0, -(sizeof (*u1))); /* 为新UDP头部预留空间 */
u1 = vlib_buffer_get_current (b0);
vlib_buffer_advance (b0, -(sizeof (*ip1))); /* 为新IP头部预留空间 */
ip1 = vlib_buffer_get_current (b0);
/* 说明:为新IP和UDP头部预留空间。
*
* 原因:
* - 需要构建新的IP和UDP头部
* - 目标地址是服务器地址
* - 源地址是Proxy地址 */
/* fill in all that rubbish... */
len = clib_net_to_host_u16 (u0->length) - sizeof (udp_header_t); /* 计算原始DHCPv6消息长度 */
/* 说明:计算原始DHCPv6消息的长度。
*
* 计算:
* - UDP长度 - UDP头部大小 = DHCPv6消息长度 */
copy_ip6_address (&r1->peer_addr, &ip0->src_address); /* 设置Peer-address */
/* 说明:设置Relay头部的Peer-address字段。
*
* Peer-address:
* - 客户端的源IPv6地址
* - 如果客户端使用链路本地地址,设置为该地址
* - 如果客户端使用其他地址,设置为该地址 */
r1->msg_type = DHCPV6_MSG_RELAY_FORW; /* 设置消息类型为Relay-Forward */
fwd_opt->length = clib_host_to_net_u16 (len); /* 设置Relay-Message选项长度 */
fwd_opt->option = clib_host_to_net_u16 (DHCPV6_OPTION_RELAY_MSG); /* 设置选项类型 */
/* 说明:设置Relay-Message选项。
*
* 选项结构:
* - option:9(RELAY_MSG)
* - length:原始DHCPv6消息长度
* - data:原始DHCPv6消息(在缓冲区中) */
r1->hop_count++;
r1->hop_count =
(h0->msg_type != DHCPV6_MSG_RELAY_FORW) ? 0 : r1->hop_count;
/* 说明:设置Hop count字段。
*
* 逻辑:
* - 如果原始消息不是Relay-Forward,hop_count设置为0(第一跳)
* - 如果原始消息是Relay-Forward,hop_count递增(支持Relay链)
*
* 用途:
* - 防止Relay循环
* - 限制Relay跳数 */
if (PREDICT_FALSE (r1->hop_count >= HOP_COUNT_LIMIT)) /* 检查Hop count限制 */
/* 说明:检查Hop count是否超过限制。
*
* HOP_COUNT_LIMIT:32(定义在dhcp6_packet.h中)
*
* 如果超过限制,丢弃报文 */
{
error0 = DHCPV6_RELAY_PKT_DROP_MAX_HOPS;
next0 = DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_DROP;
goto do_trace;
}
6.5.4.5 Link-address设置
//277:311:src/plugins/dhcp/dhcp6_proxy_node.c
/* If relay-fwd and src address is site or global unicast address */
if (h0->msg_type == DHCPV6_MSG_RELAY_FORW &&
((ip0->src_address.as_u8[0] & 0xe0) == 0x20 || /* 全局单播地址(2000::/3) */
(ip0->src_address.as_u8[0] & 0xfe) == 0xfc)) /* 站点本地地址(FC00::/7) */
/* 说明:检查是否为Relay-Forward消息且源地址是全局或站点本地地址。
*
* 条件:
* - 原始消息是Relay-Forward
* - 源地址是全局单播地址(2000::/3)或站点本地地址(FC00::/7)
*
* 含义:
* - 这是来自上游Relay的消息
* - 上游Relay已经设置了Link-address
* - 我们需要将Link-address设置为0(根据RFC 3315) */
{
/* Set link address to zero */
r1->link_addr.as_u64[0] = 0;
r1->link_addr.as_u64[1] = 0;
goto link_address_set; /* 跳转到Link-address已设置 */
}
/* if receiving interface is unnumbered, use receiving interface
* IP address as link address, otherwise use the loopback interface
* IP address as link address.
*/
swif = vnet_get_sw_interface (vnm, rx_sw_if_index); /* 获取软件接口 */
if (swif->flags & VNET_SW_INTERFACE_FLAG_UNNUMBERED) /* 检查是否为未编号接口 */
sw_if_index = swif->unnumbered_sw_if_index; /* 使用被借用接口 */
/* 说明:如果接口是未编号的,使用被借用接口的索引。
*
* 用途:
* - 获取被借用接口的IPv6地址
* - 用于设置Link-address */
ia0 =
ip6_interface_first_global_or_site_address (&ip6_main,
sw_if_index); /* 获取接口地址 */
/* 说明:获取接口的第一个全局或站点本地IPv6地址。
*
* 用途:
* - 用于设置Relay头部的Link-address字段
* - Link-address标识客户端所在的网段 */
if (ia0 == 0) /* 检查接口地址 */
{
error0 = DHCPV6_PROXY_ERROR_NO_INTERFACE_ADDRESS;
next0 = DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_DROP;
pkts_no_interface_address++;
goto do_trace; /* 没有地址,丢弃报文 */
}
copy_ip6_address (&r1->link_addr, ia0); /* 设置Link-address */
/* 说明:将接口地址复制到Relay头部的Link-address字段。
*
* Link-address:
* - 标识客户端所在的网段
* - 服务器使用此地址分配地址池 */
link_address_set:
/* 说明:Link-address设置完成标签。 */
**6.5.4.6 选项插入**
```c
//313:372:src/plugins/dhcp/dhcp6_proxy_node.c
if ((b0->current_data + b0->current_length + sizeof (*id1) +
sizeof (*vss1) + sizeof (*cmac)) >
vlib_buffer_get_default_data_size (vm)) /* 检查缓冲区空间 */
/* 说明:检查缓冲区是否有足够空间插入选项。
*
* 需要空间:
* - Interface-ID选项
* - VSS选项(可能)
* - Client MAC选项(可能)
*
* 如果空间不足,丢弃报文 */
{
error0 = DHCPV6_PROXY_ERROR_PKT_TOO_BIG;
next0 = DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_DROP;
pkts_too_big++;
goto do_trace;
}
id1 = (dhcpv6_int_id_t *) (((uword) ip1) + b0->current_length); /* 获取Interface-ID选项位置 */
b0->current_length += (sizeof (*id1)); /* 更新缓冲区长度 */
id1->opt.option = clib_host_to_net_u16 (DHCPV6_OPTION_INTERFACE_ID); /* 设置选项类型 */
id1->opt.length = clib_host_to_net_u16 (sizeof (rx_sw_if_index)); /* 设置选项长度 */
id1->int_idx = clib_host_to_net_u32 (rx_sw_if_index); /* 设置接口索引 */
/* 说明:插入Interface-ID选项(18)。
*
* 选项结构:
* - option:18(INTERFACE_ID)
* - length:4(接口索引大小)
* - int_idx:接收接口索引
*
* 用途:
* - 服务器使用此ID识别客户端所在的接口
* - 回复时根据此ID转发到正确的接口 */
u1->length = 0; /* 初始化UDP长度(用于计算选项总长度) */
if (h0->msg_type != DHCPV6_MSG_RELAY_FORW) /* 如果不是Relay-Forward消息 */
/* 说明:检查是否为原始客户端消息(不是Relay消息)。
*
* 原因:
* - 只有原始客户端消息才需要插入Client MAC选项
* - Relay消息已经包含此信息 */
{
cmac =
(dhcpv6_client_mac_t *) (((uword) ip1) + b0->current_length); /* 获取Client MAC选项位置 */
b0->current_length += (sizeof (*cmac)); /* 更新缓冲区长度 */
cmac->opt.length = clib_host_to_net_u16 (sizeof (*cmac) -
sizeof (cmac->opt)); /* 设置选项长度 */
cmac->opt.option =
clib_host_to_net_u16
(DHCPV6_OPTION_CLIENT_LINK_LAYER_ADDRESS); /* 设置选项类型 */
cmac->link_type = clib_net_to_host_u16 (1); /* ethernet */ /* 设置链路类型 */
clib_memcpy (cmac->data, client_src_mac, 6); /* 复制客户端MAC地址 */
u1->length += sizeof (*cmac); /* 累加选项长度 */
/* 说明:插入Client Link-Layer Address选项(79)。
*
* 选项结构:
* - option:79(CLIENT_LINK_LAYER_ADDRESS)
* - length:8(链路类型2字节 + MAC地址6字节)
* - link_type:1(以太网)
* - data:客户端MAC地址
*
* 用途:
* - 服务器可以使用MAC地址识别客户端
* - 用于地址绑定和认证 */
}
vss = dhcp_get_vss_info (dpm, rx_fib_idx, FIB_PROTOCOL_IP6); /* 获取VSS信息 */
if (vss) /* 如果配置了VSS */
/* 说明:检查是否配置了VSS(Virtual Subnet Selection)。
*
* VSS用途:
* - 支持多租户环境
* - 在同一物理网络上区分不同的虚拟网络 */
{
u16 id_len; /* length of VPN ID */
u16 type_len = sizeof (vss1->vss_type); /* VSS类型长度(1字节) */
vss1 = (dhcpv6_vss_t *) (((uword) ip1) + b0->current_length); /* 获取VSS选项位置 */
vss1->vss_type = vss->vss_type; /* 设置VSS类型 */
if (vss->vss_type == VSS_TYPE_VPN_ID) /* VPN ID类型 */
/* 说明:检查VSS类型是否为VPN ID。
*
* VPN ID:
* - 7字节的VPN标识符
* - 用于标识VPN */
{
id_len = sizeof (vss->vpn_id); /* vpn_id is 7 bytes */
memcpy (vss1->data, vss->vpn_id, id_len); /* 复制VPN ID */
}
else if (vss->vss_type == VSS_TYPE_ASCII) /* ASCII类型 */
/* 说明:检查VSS类型是否为ASCII。
*
* ASCII类型:
* - 可变长度的ASCII字符串
* - 用于标识VPN或租户 */
{
id_len = vec_len (vss->vpn_ascii_id); /* 获取ASCII长度 */
memcpy (vss1->data, vss->vpn_ascii_id, id_len); /* 复制ASCII ID */
}
else /* must be VSS_TYPE_DEFAULT, no VPN ID */
/* 说明:默认类型,没有VPN ID。
*
* 默认类型:
* - 不包含VPN ID
* - 只包含VSS类型 */
id_len = 0; /* VPN ID长度为0 */
vss1->opt.option = clib_host_to_net_u16 (DHCPV6_OPTION_VSS); /* 设置选项类型 */
vss1->opt.length = clib_host_to_net_u16 (type_len + id_len); /* 设置选项长度 */
u1->length += type_len + id_len + sizeof (vss1->opt); /* 累加选项长度 */
b0->current_length += type_len + id_len + sizeof (vss1->opt); /* 更新缓冲区长度 */
/* 说明:插入VSS选项(68)。
*
* 选项结构:
* - option:68(VSS)
* - length:VSS类型长度 + VPN ID长度
* - vss_type:VSS类型
* - data:VPN ID(如果有)
*
* 用途:
* - 服务器根据VSS分配不同的地址池
* - 支持多租户环境 */
}
6.5.4.7 IP和UDP头部构建
//374:414:src/plugins/dhcp/dhcp6_proxy_node.c
pkts_to_server++; /* 增加转发到服务器的统计 */
u1->checksum = 0; /* 初始化UDP校验和 */
u1->src_port = clib_host_to_net_u16 (UDP_DST_PORT_dhcpv6_to_client); /* 设置UDP源端口 */
/* 说明:设置UDP源端口为546(客户端端口)。
*
* 原因:
* - Proxy作为Relay,源端口使用客户端端口
* - 服务器回复时使用此端口 */
u1->dst_port = clib_host_to_net_u16 (UDP_DST_PORT_dhcpv6_to_server); /* 设置UDP目标端口 */
/* 说明:设置UDP目标端口为547(服务器端口)。 */
u1->length =
clib_host_to_net_u16 (clib_net_to_host_u16 (fwd_opt->length) +
sizeof (*r1) + sizeof (*fwd_opt) +
sizeof (*u1) + sizeof (*id1) + u1->length);
/* 说明:计算UDP长度。
*
* UDP长度组成:
* - Relay-Message选项数据长度(原始DHCPv6消息)
* - Relay头部大小
* - Relay-Message选项头部大小
* - UDP头部大小
* - Interface-ID选项大小
* - 其他选项大小(Client MAC、VSS等) */
clib_memset (ip1, 0, sizeof (*ip1)); /* 清零IP头部 */
ip1->ip_version_traffic_class_and_flow_label = 0x60; /* 设置IPv6版本和流量类别 */
/* 说明:设置IPv6版本字段。
*
* 0x60:
* - 高4位:版本号(6)
* - 低4位:流量类别(0) */
ip1->payload_length = u1->length; /* 设置IP负载长度 */
ip1->protocol = PROTO_UDP; /* 设置协议类型为UDP */
ip1->hop_limit = HOP_COUNT_LIMIT; /* 设置跳数限制 */
/* 说明:设置IPv6头部字段。
*
* payload_length:UDP长度
* protocol:17(UDP)
* hop_limit:32(HOP_COUNT_LIMIT) */
src = ((server->dhcp_server.ip6.as_u64[0] ||
server->dhcp_server.ip6.as_u64[1]) ?
&server->dhcp_server.ip6 : &all_dhcpv6_server_address);
/* 说明:选择目标地址。
*
* 逻辑:
* - 如果服务器地址已配置,使用服务器地址
* - 如果服务器地址未配置(全0),使用多播地址(FF05::1:3) */
copy_ip6_address (&ip1->dst_address, src); /* 设置IP目标地址 */
/* 说明:设置IPv6目标地址。
*
* 地址:
* - 服务器地址(如果配置)
* - 或所有DHCPv6服务器多播地址(FF05::1:3) */
ia0 = ip6_interface_first_global_or_site_address
(&ip6_main, vnet_buffer (b0)->sw_if_index[VLIB_RX]); /* 获取接收接口地址 */
/* 说明:获取接收接口的IPv6地址。
*
* 用途:
* - 如果Proxy源地址未配置,使用接口地址作为源地址 */
src = (proxy->dhcp_src_address.ip6.as_u64[0] ||
proxy->dhcp_src_address.ip6.as_u64[1]) ?
&proxy->dhcp_src_address.ip6 : ia0; /* 选择源地址 */
/* 说明:选择源地址。
*
* 逻辑:
* - 如果Proxy源地址已配置,使用Proxy源地址
* - 如果Proxy源地址未配置,使用接口地址 */
if (ia0 == 0) /* 检查接口地址 */
{
error0 = DHCPV6_PROXY_ERROR_NO_SRC_ADDRESS;
next0 = DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_DROP;
pkts_no_src_address++;
goto do_trace; /* 没有源地址,丢弃报文 */
}
copy_ip6_address (&ip1->src_address, src); /* 设置IP源地址 */
/* 说明:设置IPv6源地址。
*
* 地址:
* - Proxy源地址(如果配置)
* - 或接收接口地址 */
u1->checksum = ip6_tcp_udp_icmp_compute_checksum (vm, b0, ip1,
&bogus_length); /* 计算UDP校验和 */
/* 说明:计算UDP校验和。
*
* 功能:
* - 根据IPv6伪头部和UDP数据计算校验和
* - 包括IPv6源地址、目标地址、协议类型、UDP长度和UDP数据 */
ASSERT (bogus_length == 0); /* 断言校验和计算成功 */
/* 说明:断言校验和计算成功。
*
* bogus_length:
* - 如果为0,表示校验和计算成功
* - 如果非0,表示校验和计算失败(缓冲区问题) */
next0 = DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP; /* 设置下一跳为IPv6查找 */
/* 说明:设置下一跳为IPv6查找节点。
*
* 下一跳:ip6-lookup
* - 根据目标地址查找路由
* - 转发到服务器 */
6.5.4.8 多服务器转发
//418:484:src/plugins/dhcp/dhcp6_proxy_node.c
is_solicit = (DHCPV6_MSG_SOLICIT == h0->msg_type); /* 检查是否为Solicit消息 */
/* 说明:检查是否为Solicit消息。
*
* Solicit消息:
* - 客户端的初始请求
* - 需要让所有服务器都知道
* - 服务器可以竞争响应 */
/*
* If we have multiple servers configured and this is the
* client's discover message, then send copies to each of
* those servers
*/
if (is_solicit && vec_len (proxy->dhcp_servers) > 1) /* 检查多服务器转发 */
/* 说明:检查是否需要多服务器转发。
*
* 条件:
* - 是Solicit消息(is_solicit == 1)
* - 配置了多个服务器(vec_len > 1)
*
* 原因:
* - Solicit消息是广播请求
* - 需要让所有服务器都知道客户端请求
* - 服务器可以竞争响应
* - 客户端选择最快的响应 */
{
u32 ii; /* 循环变量 */
for (ii = 1; ii < vec_len (proxy->dhcp_servers); ii++) /* 遍历其他服务器 */
/* 说明:遍历除第一个服务器外的其他服务器。
*
* 起始:ii = 1(第一个服务器已在主流程中处理)
* 结束:ii < vec_len(所有服务器) */
{
vlib_buffer_t *c0; /* 复制的缓冲区 */
u32 ci0; /* 复制的缓冲区索引 */
c0 = vlib_buffer_copy (vm, b0); /* 复制缓冲区 */
/* 说明:复制原始缓冲区。
*
* 功能:
* - 创建缓冲区的副本
* - 副本包含相同的Relay消息内容
* - 可以独立修改目标地址 */
if (c0 == NULL) /* 检查复制是否成功 */
{
vlib_node_increment_counter
(vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_ALLOC_FAIL, 1);
continue; /* 复制失败,跳过此服务器 */
}
ci0 = vlib_get_buffer_index (vm, c0); /* 获取缓冲区索引 */
server = &proxy->dhcp_servers[ii]; /* 获取当前服务器 */
ip0 = vlib_buffer_get_current (c0); /* 获取IP头部 */
src = ((server->dhcp_server.ip6.as_u64[0] ||
server->dhcp_server.ip6.as_u64[1]) ?
&server->dhcp_server.ip6 :
&all_dhcpv6_server_address); /* 选择目标地址 */
copy_ip6_address (&ip1->dst_address, src); /* 修改目标地址 */
/* 说明:修改复制缓冲区的目标地址为当前服务器地址。
*
* 操作:
* - 每个副本发送到不同的服务器
* - 需要修改目标地址 */
to_next[0] = ci0; /* 添加到输出向量 */
to_next += 1;
n_left_to_next -= 1;
vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
to_next, n_left_to_next,
ci0, next0); /* 验证并入队 */
if (PREDICT_FALSE (b0->flags & VLIB_BUFFER_IS_TRACED)) /* 检查是否需要trace */
{
dhcpv6_proxy_trace_t *tr;
tr = vlib_add_trace (vm, node, c0, sizeof (*tr)); /* 添加trace */
tr->which = 0; /* to server */
tr->error = error0;
tr->original_sw_if_index = rx_sw_if_index;
tr->sw_if_index = sw_if_index;
if (next0 == DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP)
copy_ip6_address ((ip6_address_t *) &
tr->packet_data[0],
&server->dhcp_server.ip6); /* 设置服务器地址 */
}
if (PREDICT_FALSE (0 == n_left_to_next)) /* 检查输出帧空间 */
{
vlib_put_next_frame (vm, node, next_index,
n_left_to_next);
vlib_get_next_frame (vm, node, next_index,
to_next, n_left_to_next);
}
}
}
6.5.4.9 Trace和统计更新
//486:530:src/plugins/dhcp/dhcp6_proxy_node.c
do_trace:
if (PREDICT_FALSE (b0->flags & VLIB_BUFFER_IS_TRACED)) /* 检查是否需要trace */
{
dhcpv6_proxy_trace_t *tr = vlib_add_trace (vm, node,
b0, sizeof (*tr)); /* 添加trace */
tr->which = 0; /* to server */
tr->error = error0;
tr->original_sw_if_index = rx_sw_if_index;
tr->sw_if_index = sw_if_index;
if (DHCPV6_PROXY_TO_SERVER_INPUT_NEXT_LOOKUP == next0)
copy_ip6_address ((ip6_address_t *) & tr->packet_data[0],
&server->dhcp_server.ip6); /* 设置服务器地址 */
}
do_enqueue:
to_next[0] = bi0; /* 添加到输出向量 */
to_next += 1;
n_left_to_next -= 1;
vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
to_next, n_left_to_next,
bi0, next0); /* 验证并入队 */
}
vlib_put_next_frame (vm, node, next_index, n_left_to_next); /* 释放输出帧 */
}
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_RELAY_TO_CLIENT,
pkts_to_client);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_RELAY_TO_SERVER,
pkts_to_server);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_NO_INTERFACE_ADDRESS,
pkts_no_interface_address);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_WRONG_MESSAGE_TYPE,
pkts_wrong_msg_type);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_NO_SRC_ADDRESS,
pkts_no_src_address);
vlib_node_increment_counter (vm, dhcpv6_proxy_to_server_node.index,
DHCPV6_PROXY_ERROR_PKT_TOO_BIG, pkts_too_big);
return from_frame->n_vectors; /* 返回处理的报文数量 */
}
/* 说明:更新节点统计计数器并返回处理的报文数量。
*
* 统计项:
* - RELAY_TO_CLIENT:转发到客户端的报文数
* - RELAY_TO_SERVER:转发到服务器的报文数
* - NO_INTERFACE_ADDRESS:没有接口地址的报文数
* - WRONG_MESSAGE_TYPE:错误消息类型的报文数
* - NO_SRC_ADDRESS:没有源地址的报文数
* - PKT_TOO_BIG:报文太大的报文数 */
客户端到服务器方向处理函数总结:
-
主要功能:
- 接收客户端DHCPv6消息
- 封装为Relay-Forward消息
- 插入Interface-ID、Client MAC、VSS选项
- 设置Link-address和Peer-address
- 构建新的IP和UDP头部
- 转发到DHCPv6服务器
- 支持多服务器转发(Solicit消息)
-
关键步骤:
- 消息类型检查和返回流量检测
- Proxy配置查找(按MFIB索引)
- Relay头部构建(消息类型、Hop count、Link-address、Peer-address)
- 选项插入(Interface-ID、Client MAC、VSS)
- IP和UDP头部构建(源地址、目标地址、校验和)
- 多服务器复制(Solicit消息)
-
与DHCPv4的区别:
- 使用Relay消息机制,而不是在原始报文中插入选项
- 需要构建新的IP和UDP头部
- 支持Relay链(多级Relay)
- 使用MFIB而不是FIB
### 6.6 Proxy与VPP其他模块的集成
> DHCP Proxy作为VPP的一个插件模块,需要与VPP的核心模块进行集成,才能实现完整的DHCP中继功能。
> 这一节详细讲解DHCP Proxy与VPP其他模块的关系和交互方式,包括FIB路由查找、UDP协议栈集成、接口管理、VRF支持等。
> Proxy采用数据平面处理架构,所有数据包处理都在数据平面节点中完成,实现高性能的DHCP消息转发。
#### 6.6.1 与FIB的交互(查找服务器路由)
> FIB(转发信息库)是VPP的核心路由表,负责存储和管理路由信息。
> DHCP Proxy在转发消息到服务器时,需要根据服务器的FIB索引查找路由,确定数据包的转发路径。
> 这一节详细讲解Proxy如何与FIB模块交互,包括FIB索引获取、路由查找、广播路由管理等。
##### 6.6.1.1 FIB索引获取与Proxy查找
**功能说明**:
Proxy在处理客户端消息时,首先需要根据接收接口的FIB索引查找对应的Proxy配置。FIB索引通过`fib_index_by_sw_if_index`数组获取,然后使用FIB索引在Proxy配置表中查找对应的Proxy实例。
**FIB索引获取与Proxy查找源码分析(客户端到服务器方向)**:
```c
//177:179:src/plugins/dhcp/dhcp4_proxy_node.c
rx_sw_if_index = vnet_buffer (b0)->sw_if_index[VLIB_RX];
fib_index = im->fib_index_by_sw_if_index[rx_sw_if_index];
proxy = dhcp_get_proxy (dpm, fib_index, FIB_PROTOCOL_IP4);
/* 说明:根据接收接口的FIB索引查找Proxy配置。
*
* 处理流程:
* 1. 从vnet_buffer中获取接收接口索引(VLIB_RX)
* 2. 通过ip4_main的fib_index_by_sw_if_index数组获取FIB索引
* 3. 调用dhcp_get_proxy根据FIB索引和协议类型查找Proxy配置
*
* FIB索引的作用:
* - 标识客户端所在的VRF(虚拟路由转发域)
* - 每个VRF对应一个FIB索引
* - Proxy配置按FIB索引组织,支持多VRF场景
*
* Proxy查找失败处理:
* - 如果proxy为NULL,说明该VRF未配置Proxy
* - 设置错误码DHCP_PROXY_ERROR_NO_SERVER
* - 丢弃数据包 */
服务器路由查找源码分析(服务器到客户端方向):
//657:677:src/plugins/dhcp/dhcp4_proxy_node.c
fib_index = im->fib_index_by_sw_if_index[sw_if_index];
proxy = dhcp_get_proxy (dpm, fib_index, FIB_PROTOCOL_IP4);
if (PREDICT_FALSE (NULL == proxy))
{
error0 = DHCP_PROXY_ERROR_NO_SERVER;
goto drop_packet;
}
vec_foreach (server, proxy->dhcp_servers)
{
if (ip0->src_address.as_u32 == server->dhcp_server.ip4.as_u32)
{
goto server_found;
}
}
error0 = DHCP_PROXY_ERROR_BAD_SVR_FIB_OR_ADDRESS;
goto drop_packet;
/* 说明:验证服务器地址和FIB索引。
*
* 验证流程:
* 1. 从Option 82中提取客户端接口索引
* 2. 根据接口索引获取客户端FIB索引
* 3. 查找对应的Proxy配置
* 4. 遍历Proxy配置的服务器列表,匹配源IP地址
*
* 服务器匹配条件:
* - 数据包的源IP地址必须匹配配置的服务器地址
* - 确保回复消息来自正确的DHCP服务器
*
* 安全考虑:
* - 防止伪造的DHCP服务器回复
* - 只接受配置列表中服务器的回复 */
FIB索引与Proxy配置的关系:
FIB索引与Proxy配置关系
│
├─→ 1. 配置阶段
│ └─→ dhcp4_proxy_set_server()
│ ├─→ 根据rx_table_id查找或创建FIB索引
│ ├─→ 创建或获取Proxy配置
│ └─→ 将Proxy配置存储在dhcp_server_index_by_rx_fib_index数组中
│
├─→ 2. 运行时查找
│ └─→ dhcp_get_proxy()
│ ├─→ 根据FIB索引在数组中查找Proxy索引
│ ├─→ 从dhcp_servers池中获取Proxy结构
│ └─→ 返回Proxy指针
│
└─→ 3. 数据包处理
├─→ 获取接收接口的FIB索引
├─→ 查找对应的Proxy配置
└─→ 使用Proxy配置中的服务器FIB索引转发数据包
6.6.1.2 服务器FIB索引与路由转发
功能说明:
Proxy在转发消息到服务器时,使用服务器配置的server_fib_index作为数据包的发送FIB索引。这个FIB索引标识服务器所在的VRF,VPP根据此FIB索引查找路由表,确定数据包的转发路径。
服务器FIB索引使用源码分析:
//232:233:src/plugins/dhcp/dhcp4_proxy_node.c
/* Send to DHCP server via the configured FIB */
vnet_buffer (b0)->sw_if_index[VLIB_TX] = server->server_fib_index;
/* 说明:设置数据包的发送FIB索引。
*
* server_fib_index:
* - 存储在dhcp_server_t结构中
* - 在配置服务器时通过server_table_id查找或创建
* - 标识服务器所在的VRF
*
* 转发机制:
* - VPP使用VLIB_TX的sw_if_index字段作为FIB索引
* - 数据包转发时,根据此FIB索引查找路由表
* - 如果FIB索引是接口索引,直接发送到该接口
* - 如果FIB索引是VRF索引,查找路由表确定下一跳
*
* 多VRF支持:
* - 客户端和服务器可以在不同的VRF中
* - 通过rx_fib_index和server_fib_index区分
* - 实现跨VRF的DHCP中继 */
服务器FIB索引配置源码分析:
//218:223:src/plugins/dhcp/dhcp_proxy.c
dhcp_server_t server = {
.dhcp_server = *addr,
.server_fib_index = fib_table_find_or_create_and_lock (proto,
server_table_id,
FIB_SOURCE_DHCP),
};
/* 说明:创建服务器配置时查找或创建FIB索引。
*
* fib_table_find_or_create_and_lock:
* - 根据server_table_id(外部表ID)查找或创建FIB索引
* - 如果FIB表不存在,创建新的FIB表
* - 使用FIB_SOURCE_DHCP作为源标识
* - 锁定FIB表,防止被删除
*
* FIB表管理:
* - 每个VRF对应一个FIB表
* - FIB索引是内部标识,Table ID是外部标识
* - Proxy负责管理FIB表的生命周期 */
6.6.1.3 广播路由管理
功能说明:
Proxy在配置时会添加一个特殊的广播路由(255.255.255.255/32),用于接收客户端的广播DHCP消息。这个路由使用FIB_ENTRY_FLAG_LOCAL标志,表示是本地路由,数据包不会转发出去。
广播路由添加源码分析:
//850:888:src/plugins/dhcp/dhcp4_proxy_node.c
const fib_prefix_t all_1s = {
.fp_len = 32,
.fp_addr.ip4.as_u32 = 0xffffffff,
.fp_proto = FIB_PROTOCOL_IP4,
};
if (ip46_address_is_zero (addr))
return VNET_API_ERROR_INVALID_DST_ADDRESS;
if (ip46_address_is_zero (src_addr))
return VNET_API_ERROR_INVALID_SRC_ADDRESS;
dhcp_maybe_register_udp_ports (DHCP_PORT_REG_CLIENT | DHCP_PORT_REG_SERVER);
rx_fib_index = fib_table_find_or_create_and_lock (FIB_PROTOCOL_IP4,
rx_table_id,
FIB_SOURCE_DHCP);
if (is_del)
{
if (dhcp_proxy_server_del (FIB_PROTOCOL_IP4, rx_fib_index,
addr, server_table_id))
{
fib_table_entry_special_remove (rx_fib_index,
&all_1s, FIB_SOURCE_DHCP);
fib_table_unlock (rx_fib_index, FIB_PROTOCOL_IP4, FIB_SOURCE_DHCP);
}
}
else
{
if (dhcp_proxy_server_add (FIB_PROTOCOL_IP4,
addr, src_addr,
rx_fib_index, server_table_id))
{
fib_table_entry_special_add (rx_fib_index,
&all_1s,
FIB_SOURCE_DHCP, FIB_ENTRY_FLAG_LOCAL);
fib_table_lock (rx_fib_index, FIB_PROTOCOL_IP4, FIB_SOURCE_DHCP);
}
}
/* 说明:添加或删除广播路由。
*
* all_1s前缀:
* - 表示255.255.255.255/32,即IPv4广播地址
* - 用于接收客户端的广播DHCP消息
*
* fib_table_entry_special_add:
* - 添加特殊路由条目
* - FIB_ENTRY_FLAG_LOCAL标志表示本地路由
* - 数据包匹配此路由时,不会转发,而是上送到本地处理
*
* 路由生命周期:
* - 添加第一个服务器时,添加广播路由
* - 删除最后一个服务器时,删除广播路由
* - 与Proxy配置的生命周期绑定 */
广播路由的关键点:
-
本地路由标志:
- 使用
FIB_ENTRY_FLAG_LOCAL标志 - 确保广播数据包上送到Proxy节点处理
- 不会转发到其他接口
- 使用
-
路由管理时机:
- 添加第一个服务器时添加路由
- 删除最后一个服务器时删除路由
- 通过
dhcp_proxy_server_add的返回值判断是否是新Proxy
-
多VRF支持:
- 每个VRF(rx_fib_index)独立管理广播路由
- 不同VRF的广播路由互不影响
6.6.2 与UDP协议栈的集成
UDP协议栈是VPP处理UDP数据包的核心模块。
DHCP Proxy需要注册UDP端口,接收DHCP客户端和服务器的UDP消息。
这一节详细讲解Proxy如何与UDP协议栈集成,包括端口注册、数据包接收、UDP校验和计算等。
6.6.2.1 UDP端口注册
功能说明:
Proxy通过udp_register_dst_port函数注册DHCP客户端和服务器端口,当UDP数据包到达这些端口时,UDP协议栈会将数据包转发到Proxy节点处理。
UDP端口注册源码分析:
//805:824:src/plugins/dhcp/dhcp4_proxy_node.c
void
dhcp_maybe_register_udp_ports (dhcp_port_reg_flags_t ports)
{
dhcp_proxy_main_t *dm = &dhcp_proxy_main;
vlib_main_t *vm = dm->vlib_main;
int port_regs_diff = dm->udp_ports_registered ^ ports;
if (!port_regs_diff)
return;
if ((port_regs_diff & DHCP_PORT_REG_CLIENT) & ports)
udp_register_dst_port (vm, UDP_DST_PORT_dhcp_to_client,
dhcp_proxy_to_client_node.index, 1 /* is_ip4 */ );
if ((port_regs_diff & DHCP_PORT_REG_SERVER) & ports)
udp_register_dst_port (vm, UDP_DST_PORT_dhcp_to_server,
dhcp_proxy_to_server_node.index, 1 /* is_ip4 */ );
dm->udp_ports_registered |= ports;
}
/* 说明:注册UDP端口,接收DHCP消息。
*
* 端口注册机制:
* 1. 检查端口是否已注册(避免重复注册)
* 2. 如果未注册,调用udp_register_dst_port注册
* 3. 将Proxy节点索引传递给UDP协议栈
* 4. 记录已注册的端口标志
*
* 注册的端口:
* - UDP_DST_PORT_dhcp_to_client (68):客户端端口
* * 接收服务器到客户端的消息
* * 转发到dhcp_proxy_to_client_node节点
* - UDP_DST_PORT_dhcp_to_server (67):服务器端口
* * 接收客户端到服务器的消息
* * 转发到dhcp_proxy_to_server_node节点
*
* 注册时机:
* - 在配置第一个Proxy服务器时注册
* - 使用位标志避免重复注册 */
UDP端口注册流程:
UDP端口注册流程
│
├─→ 1. 配置Proxy服务器
│ └─→ dhcp4_proxy_set_server()
│
├─→ 2. 检查端口注册状态
│ └─→ dhcp_maybe_register_udp_ports()
│ ├─→ 计算端口注册差异
│ └─→ 如果已注册,直接返回
│
├─→ 3. 注册客户端端口(68)
│ └─→ udp_register_dst_port(vm, 68, dhcp_proxy_to_client_node.index)
│ ├─→ UDP协议栈记录端口到节点的映射
│ └─→ 目标端口为68的UDP包转发到Proxy节点
│
├─→ 4. 注册服务器端口(67)
│ └─→ udp_register_dst_port(vm, 67, dhcp_proxy_to_server_node.index)
│ ├─→ UDP协议栈记录端口到节点的映射
│ └─→ 目标端口为67的UDP包转发到Proxy节点
│
└─→ 5. 记录注册状态
└─→ dm->udp_ports_registered |= ports
6.6.2.2 UDP数据包接收与处理
功能说明:
UDP协议栈接收到DHCP消息后,根据目标端口将数据包转发到对应的Proxy节点。Proxy节点从UDP头部开始处理,需要访问UDP头部和IP头部来修改数据包。
UDP数据包接收源码分析(客户端到服务器):
//157:164:src/plugins/dhcp/dhcp4_proxy_node.c
h0 = vlib_buffer_get_current (b0);
/*
* udp_local hands us the DHCP header, need udp hdr,
* ip hdr to relay to server
*/
vlib_buffer_advance (b0, -(sizeof (*u0)));
u0 = vlib_buffer_get_current (b0);
/* 说明:调整缓冲区指针,访问UDP头部。
*
* 缓冲区状态:
* - UDP协议栈将数据包传递给Proxy节点时
* - 当前指针指向DHCP头部
* - 需要回退到UDP头部,以便修改UDP和IP头部
*
* 数据包结构:
* [Ethernet Header][IP Header][UDP Header][DHCP Header][Options]
* ↑
* 当前指针位置(调整后) */
UDP校验和处理源码分析:
//212:230:src/plugins/dhcp/dhcp4_proxy_node.c
/* disable UDP checksum */
u0->checksum = 0;
sum0 = ip0->checksum;
old0 = ip0->dst_address.as_u32;
new0 = server->dhcp_server.ip4.as_u32;
ip0->dst_address.as_u32 = server->dhcp_server.ip4.as_u32;
sum0 = ip_csum_update (sum0, old0, new0,
ip4_header_t /* structure */ ,
dst_address /* changed member */ );
ip0->checksum = ip_csum_fold (sum0);
sum0 = ip0->checksum;
old0 = ip0->src_address.as_u32;
new0 = proxy->dhcp_src_address.ip4.as_u32;
ip0->src_address.as_u32 = new0;
sum0 = ip_csum_update (sum0, old0, new0,
ip4_header_t /* structure */ ,
src_address /* changed member */ );
ip0->checksum = ip_csum_fold (sum0);
/* 说明:修改IP头部并更新校验和。
*
* UDP校验和:
* - 设置为0,表示不计算UDP校验和
* - DHCP协议允许UDP校验和为0
*
* IP校验和更新:
* - 使用增量校验和算法(ip_csum_update)
* - 先更新目标地址(改为服务器地址)
* - 再更新源地址(改为Proxy源地址)
* - 使用ip_csum_fold折叠校验和
*
* 增量校验和优势:
* - 不需要重新计算整个IP头部的校验和
* - 只更新变化的字段,性能更好 */
6.6.3 与接口模块的交互
接口模块是VPP管理网络接口的核心模块。
Proxy需要获取接口的IP地址、处理未编号接口、确定数据包的发送接口等。
这一节详细讲解Proxy如何与接口模块交互,包括接口地址获取、未编号接口处理、接口状态查询等。
6.6.3.1 接口地址获取
功能说明:
Proxy在添加Option 82时,需要获取接收接口的IP地址。对于未编号接口,需要获取关联的loopback接口地址。接口地址通过ip4_interface_first_address函数获取。
接口地址获取源码分析(客户端到服务器):
//262:281:src/plugins/dhcp/dhcp4_proxy_node.c
original_sw_if_index = sw_if_index =
vnet_buffer (b0)->sw_if_index[VLIB_RX];
swif = vnet_get_sw_interface (vnm, sw_if_index);
if (swif->flags & VNET_SW_INTERFACE_FLAG_UNNUMBERED)
sw_if_index = swif->unnumbered_sw_if_index;
/*
* Get the first ip4 address on the [client-side]
* RX interface, if not unnumbered. otherwise use
* the loopback interface's ip address.
*/
ia0 = ip4_interface_first_address (&ip4_main, sw_if_index, 0);
if (ia0 == 0)
{
error0 = DHCP_PROXY_ERROR_NO_INTERFACE_ADDRESS;
next0 = DHCP_PROXY_TO_SERVER_INPUT_NEXT_DROP;
pkts_no_interface_address++;
goto do_trace;
}
/* 说明:获取接口的IPv4地址。
*
* 未编号接口处理:
* - 检查接口标志VNET_SW_INTERFACE_FLAG_UNNUMBERED
* - 如果是未编号接口,使用unnumbered_sw_if_index
* - 获取关联loopback接口的地址
*
* ip4_interface_first_address:
* - 获取接口上的第一个IPv4地址
* - 参数0表示获取主地址(非辅助地址)
* - 返回ip4_address_t指针,如果接口没有地址返回NULL
*
* 地址用途:
* - 添加到Option 82的suboption 5(客户端接收接口地址)
* - 用于服务器识别客户端所在的接口
* - 在服务器回复时用于验证 */
接口地址验证源码分析(服务器到客户端):
//680:696:src/plugins/dhcp/dhcp4_proxy_node.c
swif = vnet_get_sw_interface (vnm, sw_if_index);
original_sw_if_index = sw_if_index;
if (swif->flags & VNET_SW_INTERFACE_FLAG_UNNUMBERED)
sw_if_index = swif->unnumbered_sw_if_index;
ia0 = ip4_interface_first_address (&ip4_main, sw_if_index, 0);
if (ia0 == 0)
{
error0 = DHCP_PROXY_ERROR_NO_INTERFACE_ADDRESS;
goto drop_packet;
}
if (relay_addr.as_u32 != ia0->as_u32)
{
error0 = DHCP_PROXY_ERROR_BAD_YIADDR;
goto drop_packet;
}
/* 说明:验证服务器回复中的中继地址。
*
* 验证流程:
* 1. 从Option 82中提取客户端接口索引
* 2. 获取接口的IP地址
* 3. 从DHCP头部的GIADDR字段提取中继地址
* 4. 比较中继地址和接口地址是否匹配
*
* 安全验证:
* - 确保服务器回复是针对正确的接口
* - 防止地址欺骗攻击
* - 如果地址不匹配,丢弃数据包 */
6.6.3.2 接口状态与标志处理
功能说明:
Proxy在处理数据包时,需要查询接口的状态和标志,包括未编号接口标志、接口MAC地址等。这些信息用于确定数据包的转发方式和构造正确的数据包头部。
接口标志查询源码分析:
//264:266:src/plugins/dhcp/dhcp4_proxy_node.c
swif = vnet_get_sw_interface (vnm, sw_if_index);
if (swif->flags & VNET_SW_INTERFACE_FLAG_UNNUMBERED)
sw_if_index = swif->unnumbered_sw_if_index;
/* 说明:查询接口标志并处理未编号接口。
*
* vnet_get_sw_interface:
* - 根据sw_if_index获取软件接口结构
* - 返回vnet_sw_interface_t指针
* - 包含接口的状态、标志、类型等信息
*
* 未编号接口:
* - 没有配置IP地址的接口
* - 使用关联的loopback接口的地址
* - 通过unnumbered_sw_if_index字段关联
*
* 处理逻辑:
* - 如果是未编号接口,切换到关联的loopback接口
* - 后续操作使用loopback接口的地址和配置 */
接口MAC地址获取源码分析(服务器到客户端):
//720:760:src/plugins/dhcp/dhcp4_proxy_node.c
mac0 = vlib_buffer_get_current (b0);
hi0 = vnet_get_sup_hw_interface (vnm, sw_if_index);
ei0 = ethernet_get_interface (&em->interfaces[hi0->hw_if_index]);
clib_memcpy_fast (mac0->src_address.as_u8, ei0->address, 6);
clib_memcpy_fast (mac0->dst_address.as_u8,
vnet_buffer (b0)->l2.bd_age_mac->mac.bytes, 6);
/* 说明:获取接口MAC地址并构造以太网头部。
*
* vnet_get_sup_hw_interface:
* - 获取接口的上层硬件接口
* - 返回vnet_hw_interface_t指针
*
* ethernet_get_interface:
* - 根据硬件接口索引获取以太网接口
* - 返回ethernet_interface_t指针
* - 包含接口的MAC地址
*
* MAC地址设置:
* - 源MAC地址:使用接口的MAC地址
* - 目标MAC地址:从vnet_buffer中获取(L2转发信息)
* - 用于构造完整的以太网数据包 */
6.6.4 与VRF的集成
VRF(虚拟路由转发)是VPP支持多租户网络的核心机制。
Proxy完全支持VRF,客户端和服务器可以在不同的VRF中,通过FIB索引区分。
这一节详细讲解Proxy如何与VRF集成,包括VRF配置、跨VRF转发、VRF生命周期管理等。
6.6.4.1 VRF配置与FIB索引管理
功能说明:
Proxy配置时,需要指定客户端VRF(rx_table_id)和服务器VRF(server_table_id)。这些Table ID会被转换为FIB索引,Proxy使用FIB索引来区分不同的VRF。
VRF配置源码分析:
//864:890:src/plugins/dhcp/dhcp4_proxy_node.c
rx_fib_index = fib_table_find_or_create_and_lock (FIB_PROTOCOL_IP4,
rx_table_id,
FIB_SOURCE_DHCP);
if (is_del)
{
if (dhcp_proxy_server_del (FIB_PROTOCOL_IP4, rx_fib_index,
addr, server_table_id))
{
fib_table_entry_special_remove (rx_fib_index,
&all_1s, FIB_SOURCE_DHCP);
fib_table_unlock (rx_fib_index, FIB_PROTOCOL_IP4, FIB_SOURCE_DHCP);
}
}
else
{
if (dhcp_proxy_server_add (FIB_PROTOCOL_IP4,
addr, src_addr,
rx_fib_index, server_table_id))
{
fib_table_entry_special_add (rx_fib_index,
&all_1s,
FIB_SOURCE_DHCP, FIB_ENTRY_FLAG_LOCAL);
fib_table_lock (rx_fib_index, FIB_PROTOCOL_IP4, FIB_SOURCE_DHCP);
}
}
fib_table_unlock (rx_fib_index, FIB_PROTOCOL_IP4, FIB_SOURCE_DHCP);
/* 说明:配置VRF并管理FIB表生命周期。
*
* fib_table_find_or_create_and_lock:
* - 根据rx_table_id查找或创建FIB表
* - 如果FIB表不存在,创建新的FIB表
* - 使用FIB_SOURCE_DHCP作为源标识
* - 锁定FIB表,防止被其他模块删除
*
* FIB表锁定机制:
* - 添加服务器时锁定FIB表
* - 删除最后一个服务器时解锁FIB表
* - 确保FIB表在使用期间不会被删除
*
* 多VRF支持:
* - 每个rx_table_id对应一个独立的Proxy配置
* - 不同VRF的客户端消息互不干扰
* - 服务器可以配置在不同的VRF中 */
服务器VRF配置源码分析:
//218:223:src/plugins/dhcp/dhcp_proxy.c
dhcp_server_t server = {
.dhcp_server = *addr,
.server_fib_index = fib_table_find_or_create_and_lock (proto,
server_table_id,
FIB_SOURCE_DHCP),
};
/* 说明:为服务器配置独立的VRF。
*
* server_fib_index:
* - 根据server_table_id查找或创建FIB表
* - 存储在dhcp_server_t结构中
* - 用于转发数据包到服务器
*
* 跨VRF转发:
* - 客户端在rx_fib_index对应的VRF中
* - 服务器在server_fib_index对应的VRF中
* - Proxy负责在不同VRF之间转发数据包
* - 通过设置vnet_buffer的sw_if_index[VLIB_TX]实现 */
6.6.4.2 跨VRF数据包转发
功能说明:
Proxy在转发数据包时,通过设置vnet_buffer (b0)->sw_if_index[VLIB_TX]为服务器的FIB索引,实现跨VRF转发。VPP的路由系统根据此FIB索引查找对应的路由表,确定数据包的转发路径。
跨VRF转发源码分析(客户端到服务器):
//232:233:src/plugins/dhcp/dhcp4_proxy_node.c
/* Send to DHCP server via the configured FIB */
vnet_buffer (b0)->sw_if_index[VLIB_TX] = server->server_fib_index;
/* 说明:设置发送FIB索引,实现跨VRF转发。
*
* 转发机制:
* - 将server_fib_index设置为VLIB_TX的sw_if_index
* - VPP路由系统根据此FIB索引查找路由表
* - 如果FIB索引是VRF索引,查找该VRF的路由表
* - 如果FIB索引是接口索引,直接发送到该接口
*
* 数据包路径:
* 1. 客户端在rx_fib_index VRF中发送DHCP消息
* 2. Proxy接收消息,修改IP头部
* 3. 设置VLIB_TX为server_fib_index
* 4. VPP根据server_fib_index查找路由
* 5. 数据包转发到服务器所在的VRF
*
* 多服务器支持:
* - 每个服务器可以配置不同的server_fib_index
* - 支持服务器分布在多个VRF中
* - Proxy根据配置选择正确的FIB索引 */
跨VRF转发流程:
跨VRF数据包转发流程
│
├─→ 1. 客户端发送DHCP消息
│ └─→ 在rx_fib_index VRF中发送
│
├─→ 2. Proxy接收消息
│ └─→ 根据rx_fib_index查找Proxy配置
│
├─→ 3. 修改数据包头部
│ ├─→ 修改IP目标地址为服务器地址
│ ├─→ 修改IP源地址为Proxy源地址
│ └─→ 添加Option 82
│
├─→ 4. 设置发送FIB索引
│ └─→ vnet_buffer->sw_if_index[VLIB_TX] = server_fib_index
│
├─→ 5. VPP路由查找
│ └─→ 根据server_fib_index查找路由表
│ ├─→ 如果server_fib_index是VRF索引
│ │ └─→ 查找该VRF的路由表
│ └─→ 如果server_fib_index是接口索引
│ └─→ 直接发送到该接口
│
└─→ 6. 数据包转发
└─→ 数据包转发到服务器所在的VRF
6.6.4.3 VRF生命周期管理
功能说明:
Proxy负责管理VRF的生命周期,包括FIB表的创建、锁定、解锁、删除等。当配置第一个服务器时创建并锁定FIB表,当删除最后一个服务器时解锁并可能删除FIB表。
VRF生命周期管理关键点:
-
FIB表创建:
- 使用
fib_table_find_or_create_and_lock创建或查找FIB表 - 如果FIB表不存在,自动创建
- 使用
FIB_SOURCE_DHCP标识Proxy创建的FIB表
- 使用
-
FIB表锁定:
- 添加第一个服务器时锁定FIB表
- 防止其他模块删除正在使用的FIB表
- 使用
fib_table_lock函数锁定
-
FIB表解锁:
- 删除最后一个服务器时解锁FIB表
- 使用
fib_table_unlock函数解锁 - 如果FIB表没有其他引用,可能被删除
-
广播路由管理:
- 与FIB表生命周期绑定
- 添加第一个服务器时添加广播路由
- 删除最后一个服务器时删除广播路由
VRF集成的关键点:
-
多VRF支持:
- 每个VRF独立配置Proxy
- 客户端和服务器可以在不同VRF中
- 通过FIB索引区分不同的VRF
-
跨VRF转发:
- 通过设置VLIB_TX的sw_if_index实现
- VPP路由系统自动处理跨VRF转发
- 支持复杂的多租户网络场景
-
VRF隔离:
- 不同VRF的Proxy配置互不干扰
- 客户端消息只在对应的VRF中处理
- 服务器回复只发送到对应的客户端VRF
206

被折叠的 条评论
为什么被折叠?



