VPP中ARP实现第三章:ARP模块详解

目录


文中相关知识可以提前看这些文章

VPP中FIB(转发信息库)和VRF(虚拟路由转发)详解:从设计理念到实际应用
VPP中ARP实现第一章:Adjacency(邻接)模块详解
VPP中ARP实现第二章:IP Neighbor(IP邻居)模块详解

第一章:ARP模块概述

章节意义

本章介绍ARP模块的整体定位、功能和特性,为后续深入分析奠定基础。ARP模块是VPP网络栈中连接L2(以太网)和L3(IP)的关键桥梁,负责将IP地址解析为MAC地址,使得IP数据包能够在以太网上正确传输。

1.1 ARP模块定位

源码位置src/vnet/arp/

VPP的ARP模块是核心网络栈的一部分,不是插件。它实现了IPv4的地址解析协议(Address Resolution Protocol),用于将IP地址映射到MAC地址。

模块在VPP架构中的位置

  • L2/L3边界:位于以太网层和IP层之间,处理地址解析
  • 数据平面核心:作为核心模块,直接参与数据包转发路径
  • 与IP邻居模块集成:学习到的映射关系存储在IP邻居表中,供转发使用

关键文件

  • arp.c:主要的ARP处理逻辑
  • arp_proxy.c:ARP代理功能
  • arp_api.c:API接口
  • arp.h:头文件定义
  • arp_packet.h:ARP数据包结构定义

1.2 ARP模块功能

根据源码分析,VPP的ARP模块提供以下功能:

  1. ARP请求处理:接收并处理ARP请求
  2. ARP响应生成:为本地IP地址生成ARP响应
  3. ARP学习:从接收到的ARP包中学习IP到MAC的映射
  4. ARP代理:为不在本地子网的IP地址提供ARP代理
  5. 接口管理:支持按接口启用/禁用ARP功能

1.3 ARP模块特性

根据src/vnet/arp/FEATURE.yaml文件:

yaml 复制代码
name: Address Resolution Protocol
maintainer: Neale Ranns <nranns@cisco.com>
features:
  - ARP responder
description: "An implementation of the Address resolution protocol (ARP) as described in RFC826"
state: production
properties: [API, CLI, MULTITHREAD]

关键特性

  • MULTITHREAD:支持多线程处理
  • API:提供API接口
  • CLI:提供CLI命令
  • RFC826兼容:符合RFC826标准

1.4 ARP模块整体运行机制

ARP模块的运行机制概述

ARP模块在VPP中的运行遵循以下流程:

  1. 初始化阶段(第四章):

    • 注册ARP节点到以太网输入处理
    • 初始化数据结构
    • 注册IP4接口回调
    • 注册IP邻居VFT
  2. 接口管理(第六章):

    • 接口创建时默认禁用ARP
    • IP4接口启用时自动启用ARP
    • 通过Feature Arc动态启用/禁用ARP功能
  3. 数据包处理流程(第六章、第七章):

    • 接收ARP请求arp-input节点接收并验证
    • Feature Arc路由 :根据接口状态路由到arp-replyarp-disabled
    • FIB查找:判断目标IP是否是本地地址
    • 生成响应:如果是本地地址,生成ARP响应
    • VLAN处理:在响应中包含正确的VLAN标签
    • 学习映射:学习请求者的IP到MAC映射
  4. 学习机制(第九章):

    • 数据平面触发学习请求
    • RPC转发到主线程
    • 主线程更新IP邻居表
    • 更新邻接表供转发使用
  5. 线程安全(第五章):

    • 数据包处理路径无锁(只读操作)
    • FIB查找无锁(只读操作)
    • 配置修改在控制平面(有同步机制)

各章节的关联关系

  • 第二章:定义数据结构和模块骨架,是理解后续章节的基础
  • 第三章:定义节点和Feature Arc,是数据包流转的基础
  • 第四章:初始化流程,建立模块间的连接
  • 第五章:线程调度机制,保证多线程安全
  • 第六章:ARP请求处理,核心功能之一
  • 第七章:ARP响应生成,包括VLAN处理
  • 第八章:ARP代理功能,扩展功能
  • 第九章:ARP学习机制,与IP邻居模块集成

第二章:ARP模块骨架和数据结构

章节意义

本章介绍ARP模块的核心数据结构和模块骨架。数据结构是理解ARP模块运行机制的基础,它们定义了ARP模块如何存储状态、管理接口、处理数据包。理解这些数据结构有助于理解后续章节中的处理流程。

2.1 主数据结构

文件位置src/vnet/arp/arp.c

2.1.1 ethernet_arp_main_t

作用和实现原理

ethernet_arp_main_t是ARP模块的全局主数据结构,用于管理整个ARP模块的状态和配置。它采用单例模式(通过static关键字实现),确保整个系统只有一个ARP主结构实例。

设计原理

  • 单例模式 :使用static全局变量,确保整个VPP进程只有一个ARP主结构
  • 接口级状态管理 :通过数组索引(sw_if_index)快速访问每个接口的ARP状态
  • Feature Arc集成:存储Feature Arc索引,实现动态功能启用/禁用

源码实现

c 复制代码
typedef struct
{
  /* Hash tables mapping name to opcode. */
  uword *opcode_by_name;

  /** Per interface state */
  ethernet_arp_interface_t *ethernet_arp_by_sw_if_index;

  /* ARP feature arc index */
  u8 feature_arc_index;
} ethernet_arp_main_t;

static ethernet_arp_main_t ethernet_arp_main;

字段详解

  1. opcode_by_name:哈希表,将ARP操作码名称映射到数值

    • 作用:用于CLI命令解析和调试输出格式化
    • 实现原理:字符串哈希表,键为操作码名称(如"request"),值为枚举值
    • 使用场景 :CLI输入"request"时,通过哈希表快速查找对应的枚举值ETHERNET_ARP_OPCODE_request
    • 初始化时机 :在ethernet_arp_init函数中填充(见第4章)
  2. ethernet_arp_by_sw_if_index:按接口索引的ARP状态数组

    • 作用:存储每个接口的ARP启用/禁用状态
    • 实现原理 :动态数组,索引为sw_if_index(软件接口索引),值为ethernet_arp_interface_t结构
    • 访问方式am->ethernet_arp_by_sw_if_index[sw_if_index]直接访问
    • 内存管理 :使用VPP的vec_validate动态扩展数组大小
  3. feature_arc_index:ARP Feature Arc的索引

    • 作用:标识ARP Feature Arc在VPP Feature Arc系统中的位置
    • 实现原理 :由VNET_FEATURE_ARC_INIT宏自动填充
    • 使用场景 :调用vnet_feature_arc_start时,需要传入此索引来启动Feature Arc
2.1.2 ethernet_arp_interface_t

作用和实现原理

ethernet_arp_interface_t是每个接口的ARP配置和状态结构。VPP采用接口级(per-interface)的ARP管理策略,允许每个接口独立启用或禁用ARP功能。

设计原理

  • 接口级控制:每个接口可以独立配置ARP功能,满足不同网络场景需求
  • 简单状态标志 :只用一个u32字段存储启用状态,结构简单高效
  • 线程安全:在数据包处理路径中只读,修改在控制平面进行(有同步机制)

源码实现

c 复制代码
/**
 * @brief Per-interface ARP configuration and state
 */
typedef struct ethernet_arp_interface_t_
{
  /**
   * Is ARP enabled on this interface
   */
  u32 enabled;
} ethernet_arp_interface_t;

字段详解

  • enabled :接口上是否启用了ARP
    • 值含义
      • 1:ARP已启用,接口会响应ARP请求并学习ARP映射
      • 0:ARP已禁用,接口忽略ARP数据包
    • 设置时机
      • 接口创建时默认为禁用状态
      • 通过arp_enable函数启用
      • 通过arp_disable函数禁用
    • 检查方式 :通过arp_is_enabled函数检查接口ARP是否启用
2.1.3 ethernet_arp_header_t

文件位置src/vnet/ethernet/arp_packet.h

作用和实现原理

ethernet_arp_header_t是ARP数据包的完整头部结构,严格按照RFC826标准定义。它描述了ARP协议的所有字段,用于解析和构造ARP数据包。

设计原理

  • RFC826兼容:完全符合RFC826标准,确保与其他系统互操作
  • 联合体设计 :使用union支持不同的地址格式(IPv4+以太网、其他硬件类型)
  • 固定字段:前6个字段是固定的,描述协议类型和地址长度
  • 可变地址:地址部分使用联合体,支持不同硬件类型的地址格式

源码实现

c 复制代码
typedef struct
{
  u16 l2_type;                    // 硬件类型(L2层类型)
  u16 l3_type;                    // 协议类型(L3层类型)
  u8 n_l2_address_bytes;          // L2地址字节数
  u8 n_l3_address_bytes;          // L3地址字节数
  u16 opcode;                      // 操作码(请求/响应)
  union
  {
    ethernet_arp_ip4_over_ethernet_address_t ip4_over_ethernet[2];
    /* Others... */
    u8 data[0];
  };
} ethernet_arp_header_t;

字段详解

  • l2_type:硬件类型(Hardware Type)

    • 作用:标识L2层使用的硬件类型
    • :对于以太网为ETHERNET_ARP_HARDWARE_TYPE_ethernet(值为1)
    • 验证 :在arp_input节点中验证,必须是1(以太网)
  • l3_type:协议类型(Protocol Type)

    • 作用:标识L3层使用的协议类型
    • :对于IPv4为ETHERNET_TYPE_IP4(值为0x0800,与以太网类型字段相同)
    • 验证 :在arp_input节点中验证,必须是0x0800(IPv4)
  • n_l2_address_bytes:L2地址字节数

    • 作用:指定L2层地址的长度
    • :以太网为6字节(MAC地址长度)
    • 用途:用于解析和构造ARP包时确定地址字段长度
  • n_l3_address_bytes:L3地址字节数

    • 作用:指定L3层地址的长度
    • :IPv4为4字节
    • 用途:用于解析和构造ARP包时确定地址字段长度
  • opcode:操作码(Operation Code)

    • 作用:标识ARP包的类型(请求或响应)
      • ETHERNET_ARP_OPCODE_request(值为1):ARP请求
      • ETHERNET_ARP_OPCODE_reply(值为2):ARP响应
    • 字节序 :网络字节序(大端),需要使用clib_host_to_net_u16clib_net_to_host_u16转换
  • ip4_over_ethernet[2]:两个IP4+以太网地址对

    • 作用:存储发送者和目标的IP地址和MAC地址
    • 索引含义
      • [0]:发送者(Sender)的IP和MAC地址
      • [1]:目标(Target)的IP和MAC地址
    • ARP请求:发送者填充自己的IP和MAC,目标IP填充要查询的IP,目标MAC为0
    • ARP响应:发送者填充响应者的IP和MAC,目标填充请求者的IP和MAC
2.1.4 ethernet_arp_ip4_over_ethernet_address_t

作用和实现原理

ethernet_arp_ip4_over_ethernet_address_t是ARP数据包中IP地址和MAC地址的组合结构,用于存储ARP协议中的地址对。它使用CLIB_PACKED确保结构体紧密排列,符合网络数据包的字节对齐要求。

设计原理

  • 紧密打包 :使用CLIB_PACKED避免编译器添加填充字节,确保结构体大小固定为10字节
  • 类型安全 :使用VPP的类型系统(mac_address_tip4_address_t)而不是原始字节数组
  • 大小验证 :通过STATIC_ASSERT在编译时验证结构体大小,防止意外修改导致大小变化

源码实现

c 复制代码
typedef CLIB_PACKED (struct {
  mac_address_t mac;
  ip4_address_t ip4;
}) ethernet_arp_ip4_over_ethernet_address_t;

STATIC_ASSERT (sizeof (ethernet_arp_ip4_over_ethernet_address_t) == 10,
               "Packet ethernet address and IP4 address too big");

字段详解

  • mac:MAC地址(6字节)

    • 类型mac_address_t(VPP的MAC地址类型)
    • 大小:6字节(以太网MAC地址标准长度)
    • 字节序:网络字节序(大端)
  • ip4:IPv4地址(4字节)

    • 类型ip4_address_t(VPP的IPv4地址类型)
    • 大小:4字节
    • 字节序:网络字节序(大端)
  • 总大小:10字节(6字节MAC + 4字节IP)

    • 验证机制STATIC_ASSERT在编译时检查,如果大小不是10字节,编译会失败
    • 重要性:确保ARP数据包格式正确,避免解析错误
2.1.5 arp_proxy_main_t

文件位置src/vnet/arp/arp_proxy.c

c 复制代码
typedef struct arp_proxy_main_t_
{
  /** Per interface state */
  bool *enabled_by_sw_if_index;

  /* Proxy arp vector */
  ethernet_proxy_arp_t *proxy_arps;
} arp_proxy_main_t;

arp_proxy_main_t arp_proxy_main;

数据结构说明

  • enabled_by_sw_if_index:按接口索引的代理ARP启用状态
  • proxy_arps:代理ARP条目向量

ethernet_proxy_arp_t结构

c 复制代码
typedef struct
{
  ip4_address_t lo_addr;    // 代理地址范围下限
  ip4_address_t hi_addr;    // 代理地址范围上限
  u32 fib_index;            // FIB索引
} ethernet_proxy_arp_t;

2.2 错误码定义

c 复制代码
typedef enum
{
  ARP_ERROR_REPLIES_SENT,                    // 已发送响应(正常情况)
  ARP_ERROR_DISABLED,                        // ARP已禁用
  ARP_ERROR_L2_TYPE_NOT_ETHERNET,           // L2类型不是以太网
  ARP_ERROR_L3_TYPE_NOT_IP4,                 // L3类型不是IPv4
  ARP_ERROR_L3_SRC_ADDRESS_NOT_LOCAL,        // L3源地址不是本地
  ARP_ERROR_L3_DST_ADDRESS_NOT_LOCAL,        // L3目标地址不是本地
  ARP_ERROR_L3_DST_ADDRESS_UNSET,            // L3目标地址未设置
  ARP_ERROR_L3_SRC_ADDRESS_IS_LOCAL,        // L3源地址是本地
  ARP_ERROR_L3_SRC_ADDRESS_LEARNED,         // L3源地址已学习
  ARP_ERROR_REPLIES_RECEIVED,                // 收到响应
  ARP_ERROR_OPCODE_NOT_REQUEST,              // 操作码不是请求
  ARP_ERROR_PROXY_ARP_REPLIES_SENT,         // 代理ARP响应已发送
  ARP_ERROR_L2_ADDRESS_MISMATCH,            // L2地址不匹配
  ARP_ERROR_GRATUITOUS_ARP,                  // 免费ARP
  ARP_ERROR_INTERFACE_NO_TABLE,              // 接口没有FIB表
  ARP_ERROR_INTERFACE_NOT_IP_ENABLED,        // 接口未启用IP
  ARP_ERROR_UNNUMBERED_MISMATCH,             // 无编号接口不匹配
  ARP_N_ERROR,
} arp_error_t;

第三章:ARP节点注册和Feature Arc

章节意义

本章介绍ARP模块的节点注册和Feature Arc机制。VPP使用节点(Node)和Feature Arc来实现数据包处理的流水线。理解节点注册和Feature Arc是理解ARP数据包如何在VPP中流转的关键。Feature Arc机制使得ARP功能可以按接口动态启用/禁用,这是ARP接口管理的基础。

3.1 ARP节点定义

VPP的ARP模块定义了4个主要节点

3.1.1 arp-input节点

src/vnet/arp/arp.c

c 复制代码
VLIB_REGISTER_NODE (arp_input_node, static) =
{
  .function = arp_input,
  .name = "arp-input",
  .vector_size = sizeof (u32),
  .n_errors = ARP_N_ERROR,
  .error_counters = arp_error_counters,
  .n_next_nodes = ARP_INPUT_N_NEXT,
  .next_nodes = {
    [ARP_INPUT_NEXT_DROP] = "error-drop",
    [ARP_INPUT_NEXT_DISABLED] = "arp-disabled",
  },
  .format_buffer = format_ethernet_arp_header,
  .format_trace = format_ethernet_arp_input_trace,
};

节点说明

  • 函数arp_input
  • 名称arp-input
  • 下一跳节点
    • ARP_INPUT_NEXT_DROP:错误丢弃
    • ARP_INPUT_NEXT_DISABLED:ARP已禁用节点
3.1.2 arp-reply节点
c 复制代码
VLIB_REGISTER_NODE (arp_reply_node, static) =
{
  .function = arp_reply,
  .name = "arp-reply",
  .vector_size = sizeof (u32),
  .n_errors = ARP_N_ERROR,
  .error_counters = arp_error_counters,
  .n_next_nodes = ARP_REPLY_N_NEXT,
  .next_nodes = {
    [ARP_REPLY_NEXT_DROP] = "error-drop",
    [ARP_REPLY_NEXT_REPLY_TX] = "interface-output",
  },
  .format_buffer = format_ethernet_arp_header,
  .format_trace = format_ethernet_arp_input_trace,
};

节点说明

  • 函数arp_reply
  • 名称arp-reply
  • 下一跳节点
    • ARP_REPLY_NEXT_DROP:错误丢弃
    • ARP_REPLY_NEXT_REPLY_TX:发送到interface-output节点
3.1.3 arp-proxy节点

文件位置src/vnet/arp/arp_proxy.c

c 复制代码
VLIB_REGISTER_NODE (arp_proxy_node, static) =
{
  .function = arp_proxy,
  .name = "arp-proxy",
  .vector_size = sizeof (u32),
  .n_errors = ARP_N_ERROR,
  .error_counters = arp_error_counters,
  .n_next_nodes = ARP_REPLY_N_NEXT,
  .next_nodes =
  {
    [ARP_REPLY_NEXT_DROP] = "error-drop",
    [ARP_REPLY_NEXT_REPLY_TX] = "interface-output",
  },
  .format_buffer = format_ethernet_arp_header,
  .format_trace = format_ethernet_arp_input_trace,
};

节点说明

  • 函数arp_proxy
  • 名称arp-proxy
  • 下一跳节点 :与arp-reply相同
3.1.4 arp-disabled节点
c 复制代码
VLIB_REGISTER_NODE (arp_disabled_node, static) =
{
  .function = arp_disabled,
  .name = "arp-disabled",
  .vector_size = sizeof (u32),
  .n_errors = ARP_N_ERROR,
  .error_counters = arp_error_counters,
  .n_next_nodes = ARP_DISABLED_N_NEXT,
  .next_nodes = {
    [ARP_INPUT_NEXT_DROP] = "error-drop",
  },
  .format_buffer = format_ethernet_arp_header,
  .format_trace = format_ethernet_arp_input_trace,
};

节点说明

  • 函数arp_disabled
  • 名称arp-disabled
  • 下一跳节点 :只有error-drop

3.2 ARP Feature Arc

c 复制代码
/* Built-in ARP rx feature path definition */
VNET_FEATURE_ARC_INIT (arp_feat, static) =
{
  .arc_name = "arp",
  .start_nodes = VNET_FEATURES ("arp-input"),
  .last_in_arc = "error-drop",
  .arc_index_ptr = &ethernet_arp_main.feature_arc_index,
};

Feature Arc说明

  • Arc名称"arp"
  • 起始节点arp-input
  • 最后节点error-drop
  • Arc索引 :存储在ethernet_arp_main.feature_arc_index

3.3 Feature节点注册

c 复制代码
VNET_FEATURE_INIT (arp_reply_feat_node, static) =
{
  .arc_name = "arp",
  .node_name = "arp-reply",
  .runs_before = VNET_FEATURES ("arp-disabled"),
};

VNET_FEATURE_INIT (arp_proxy_feat_node, static) =
{
  .arc_name = "arp",
  .node_name = "arp-proxy",
  .runs_after = VNET_FEATURES ("arp-reply"),
  .runs_before = VNET_FEATURES ("arp-disabled"),
};

VNET_FEATURE_INIT (arp_disabled_feat_node, static) =
{
  .arc_name = "arp",
  .node_name = "arp-disabled",
  .runs_before = VNET_FEATURES ("error-drop"),
};

VNET_FEATURE_INIT (arp_drop_feat_node, static) =
{
  .arc_name = "arp",
  .node_name = "error-drop",
  .runs_before = 0,	/* last feature */
};

Feature执行顺序

复制代码
arp-input → arp-reply → arp-proxy → arp-disabled → error-drop

执行逻辑

  1. arp-input:首先处理ARP输入
  2. arp-reply:如果ARP已启用,尝试生成响应
  3. arp-proxy:如果启用了代理ARP,尝试代理响应
  4. arp-disabled:如果ARP已禁用,丢弃数据包
  5. error-drop:最后丢弃错误数据包

第四章:ARP初始化流程

章节意义

本章介绍ARP模块的初始化流程。初始化是模块运行的起点,它建立了ARP模块与其他模块(以太网、IP邻居、FIB)的连接,注册了必要的回调函数,配置了数据包路由。理解初始化流程有助于理解ARP模块如何融入VPP的整体架构。

4.1 初始化函数

src/vnet/arp/arp.c

c 复制代码
static clib_error_t *
ethernet_arp_init (vlib_main_t * vm)
{
  ethernet_arp_main_t *am = &ethernet_arp_main;
  ip4_main_t *im = &ip4_main;
  pg_node_t *pn;

  ethernet_register_input_type (vm, ETHERNET_TYPE_ARP, arp_input_node.index);

  pn = pg_get_node (arp_input_node.index);
  pn->unformat_edit = unformat_pg_arp_header;

  am->opcode_by_name = hash_create_string (0, sizeof (uword));
#define _(o) hash_set_mem (am->opcode_by_name, #o, ETHERNET_ARP_OPCODE_##o);
  foreach_ethernet_arp_opcode;
#undef _

  /* don't trace ARP error packets */
  {
    vlib_node_runtime_t *rt =
      vlib_node_get_runtime (vm, arp_input_node.index);

    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_REPLIES_SENT], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_DISABLED], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_L2_TYPE_NOT_ETHERNET], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_L3_TYPE_NOT_IP4], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_L3_SRC_ADDRESS_NOT_LOCAL], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_L3_DST_ADDRESS_NOT_LOCAL], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_L3_DST_ADDRESS_UNSET], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_L3_SRC_ADDRESS_IS_LOCAL], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_L3_SRC_ADDRESS_LEARNED], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_REPLIES_RECEIVED], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_OPCODE_NOT_REQUEST], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_PROXY_ARP_REPLIES_SENT], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_L2_ADDRESS_MISMATCH], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_GRATUITOUS_ARP], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_INTERFACE_NO_TABLE], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_INTERFACE_NOT_IP_ENABLED], 1);
    vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_UNNUMBERED_MISMATCH], 1);
  }

  {
    ip4_enable_disable_interface_callback_t cb = {
      .function = arp_enable_disable_interface,
    };
    vec_add1 (im->enable_disable_interface_callbacks, cb);
  }

  ip_neighbor_register (AF_IP4, &arp_vft);

  return 0;
}

VLIB_INIT_FUNCTION (ethernet_arp_init) =
{
  .runs_after = VLIB_INITS("ethernet_init",
                           "ip_neighbor_init"),
};

4.2 初始化步骤详解

本章节将详细解释ARP初始化流程中的每一步,包括每个函数内部做了什么、为什么这样做,以及如何工作的。


步骤1:注册以太网输入类型

源码位置src/vnet/arp/arp.c:868

作用和实现原理

这一步的作用是告诉以太网模块:"当收到以太网类型为0x0806(ARP)的数据包时,请把它发送到arp-input节点处理"。

生活化理解

就像"在邮局登记邮件分拣规则":

复制代码
问题:
- 邮局收到各种邮件(以太网数据包)
- 邮件上有类型标签(以太网类型字段)
- 如何知道ARP类型的邮件应该送到哪里?

解决方案:注册输入类型
- 告诉邮局:类型为0x0806的邮件 → 送到ARP处理部门
- 邮局会记住这个规则
- 以后收到ARP邮件,自动送到ARP部门

函数调用

c 复制代码
ethernet_register_input_type (vm, ETHERNET_TYPE_ARP, arp_input_node.index);

参数说明

  • vm:VPP主线程上下文
  • ETHERNET_TYPE_ARP:以太网类型值,0x0806(ARP协议)
  • arp_input_node.index:ARP输入节点的索引

函数内部实现src/vnet/ethernet/node.c:2298):

c 复制代码
void
ethernet_register_input_type (vlib_main_t * vm,
                              ethernet_type_t type, u32 node_index)
{
  ethernet_main_t *em = &ethernet_main;
  ethernet_type_info_t *ti;
  u32 i;

  // 步骤1.1:确保以太网模块已初始化
  {
    clib_error_t *error = vlib_call_init_function (vm, ethernet_init);
    if (error)
      clib_error_report (error);
  }

  // 步骤1.2:获取或创建该以太网类型的类型信息结构
  ti = ethernet_get_type_info (em, type);
  if (ti == 0)
    {
      clib_warning ("type_info NULL for type %d", type);
      return;
    }

  // 步骤1.3:记录ARP输入节点的索引
  ti->node_index = node_index;

  // 步骤1.4:在VLIB图中添加边(edge)
  // 说明:VLIB图是VPP的数据包处理图,节点之间通过边连接
  // 这里添加从ethernet-input节点到arp-input节点的边
  ti->next_index = vlib_node_add_next (vm,
                                       ethernet_input_node.index, node_index);
  
  // 步骤1.5:同样为ethernet-input-type节点添加边
  i = vlib_node_add_next (vm, ethernet_input_type_node.index, node_index);
  ASSERT (i == ti->next_index);

  // 步骤1.6:同样为ethernet-input-not-l2节点添加边
  i = vlib_node_add_next (vm, ethernet_input_not_l2_node.index, node_index);
  ASSERT (i == ti->next_index);

  // 步骤1.7:注册到L3下一跳结构
  // 说明:当以太网输入节点识别出ARP类型时,使用这个next_index
  next_by_ethertype_register (&em->l3_next, type, ti->next_index);

  // 步骤1.8:通知其他需要这个映射的节点(如L2BVI)
  l2bvi_register_input_type (vm, type, node_index);
}

vlib_node_add_next的作用src/vlib/node.c:170):

c 复制代码
uword
vlib_node_add_next_with_slot (vlib_main_t * vm,
                              uword node_index,
                              uword next_node_index, uword slot)
{
  vlib_node_main_t *nm = &vm->node_main;
  vlib_node_t *node, *next;
  
  // 获取源节点和目标节点
  node = vec_elt (nm->nodes, node_index);
  next = vec_elt (nm->nodes, next_node_index);

  // 如果slot未指定,使用下一个可用slot
  if (slot == ~0)
    slot = vec_len (node->next_nodes);

  // 扩展next_nodes数组
  vec_validate_init_empty (node->next_nodes, slot, ~0);

  // 设置next_nodes[slot] = next_node_index
  // 这样node就知道:当选择slot时,下一个节点是next_node_index
  node->next_nodes[slot] = next_node_index;

  // 更新运行时信息
  vlib_node_runtime_update (vm, node_index, slot);

  return slot;
}

next_by_ethertype_register的作用src/vnet/ethernet/node.c:2218):

c 复制代码
clib_error_t *
next_by_ethertype_register (next_by_ethertype_t * l3_next,
                            u32 ethertype, u32 next_index)
{
  u16 *n;
  
  // 步骤1.7.1:在稀疏向量中设置映射
  // 说明:稀疏向量用于存储以太网类型到next_index的映射
  // 例如:0x0806 (ARP) → next_index (指向arp-input节点)
  n = sparse_vec_validate (l3_next->input_next_by_type, ethertype);
  n[0] = next_index;

  // 步骤1.7.2:重建反向映射
  // 说明:用于从next_index查找对应的以太网类型
  vec_validate (l3_next->sparse_index_by_input_next_index, next_index);
  // ... 重建映射 ...

  // 步骤1.7.3:缓存常见以太网类型(性能优化)
  if (ethertype == ETHERNET_TYPE_IP4)
    {
      l3_next->input_next_ip4 = next_index;  // 直接缓存,避免查找
    }
  else if (ethertype == ETHERNET_TYPE_IP6)
    {
      l3_next->input_next_ip6 = next_index;
    }
  else if (ethertype == ETHERNET_TYPE_MPLS)
    {
      l3_next->input_next_mpls = next_index;
    }
  // 注意:ARP类型不会被缓存,因为不常见

  return 0;
}

数据包处理流程

当数据包到达ethernet-input节点时:

c 复制代码
// 在ethernet-input节点中(简化版)
if (l->type == ETHERNET_TYPE_ARP)
  {
    // 查找ARP类型对应的next_index
    l->next = eth_input_next_by_type (l->type);
    // 返回:arp-input节点的next_index
  }

总结

  • 作用:建立以太网类型0x0806到ARP输入节点的映射关系
  • 结果 :当收到ARP数据包时,自动路由到arp-input节点
  • 实现:在VLIB图中添加边,在稀疏向量中注册映射

步骤2:初始化PG节点

源码位置src/vnet/arp/arp.c:870-871

作用和实现原理

PG(Packet Generator)是VPP的测试工具,用于生成和解析测试数据包。这一步设置ARP数据包的解析函数,使得PG工具可以正确解析和显示ARP数据包。

生活化理解

就像"给测试工具安装解析器":

复制代码
问题:
- VPP有一个测试工具(PG),可以生成和显示数据包
- 当生成ARP测试数据包时,如何知道如何解析和显示?

解决方案:设置解析函数
- 告诉PG工具:ARP数据包用这个函数解析
- PG工具生成ARP包时,会调用这个函数
- 可以正确显示ARP包的内容

函数调用

c 复制代码
pn = pg_get_node (arp_input_node.index);
pn->unformat_edit = unformat_pg_arp_header;

代码详解

c 复制代码
// 步骤2.1:获取ARP输入节点的PG节点结构
// 说明:PG节点结构包含数据包生成和解析相关的函数指针
pn = pg_get_node (arp_input_node.index);

// 步骤2.2:设置ARP头的解析函数
// 说明:unformat_edit函数用于从CLI命令解析ARP头
// 例如:CLI命令 "arp request 192.168.1.1" 会被解析成ARP请求包
pn->unformat_edit = unformat_pg_arp_header;

unformat_pg_arp_header的作用

这个函数用于从CLI命令解析ARP头,例如:

bash 复制代码
# CLI命令示例
packet-generator new {
  name arp-test
  node arp-input
  size 60
  data {
    # 这里会调用unformat_pg_arp_header解析
    arp request 192.168.1.1
  }
}

总结

  • 作用:设置ARP数据包的CLI解析函数
  • 用途:用于测试和调试,可以手动生成ARP数据包
  • 影响:不影响正常数据包处理,只影响测试工具

步骤3:初始化操作码哈希表

源码位置src/vnet/arp/arp.c:873-876

作用和实现原理

创建字符串到ARP操作码的映射表,用于CLI命令解析。当用户在CLI中输入"request"或"reply"时,可以快速查找对应的操作码值。

生活化理解

就像"创建字典":

复制代码
问题:
- CLI命令需要解析ARP操作码
- 用户输入:"request"(字符串)
- 代码需要:1(操作码值)
- 如何快速转换?

解决方案:创建哈希表
- 建立映射:{"request" → 1, "reply" → 2}
- 查找时直接查表,速度快

函数调用

c 复制代码
am->opcode_by_name = hash_create_string (0, sizeof (uword));
#define _(o) hash_set_mem (am->opcode_by_name, #o, ETHERNET_ARP_OPCODE_##o);
  foreach_ethernet_arp_opcode;
#undef _

代码详解

c 复制代码
// 步骤3.1:创建字符串哈希表
// 说明:hash_create_string创建以字符串为键的哈希表
// 参数:0 = 初始大小(自动扩展),sizeof(uword) = 值的大小
am->opcode_by_name = hash_create_string (0, sizeof (uword));

// 步骤3.2:使用宏展开填充哈希表
// 说明:foreach_ethernet_arp_opcode会展开成所有ARP操作码
#define _(o) hash_set_mem (am->opcode_by_name, #o, ETHERNET_ARP_OPCODE_##o);
  foreach_ethernet_arp_opcode;  // 展开成多个_(...)调用
#undef _

宏展开示例

假设foreach_ethernet_arp_opcode定义为:

c 复制代码
#define foreach_ethernet_arp_opcode \
  _(request) \
  _(reply)

那么会展开成:

c 复制代码
hash_set_mem (am->opcode_by_name, "request", ETHERNET_ARP_OPCODE_request);
hash_set_mem (am->opcode_by_name, "reply", ETHERNET_ARP_OPCODE_reply);

hash_set_mem的作用

c 复制代码
// 在哈希表中设置键值对
// 键:"request"(字符串)
// 值:ETHERNET_ARP_OPCODE_request(通常是1)
hash_set_mem (am->opcode_by_name, "request", ETHERNET_ARP_OPCODE_request);

使用场景

在CLI命令解析时:

c 复制代码
// CLI命令:show arp opcode request
unformat_user (input, unformat_ethernet_arp_opcode_host_byte_order, &opcode);

// 内部实现(简化版)
if (unformat (input, "request"))
  {
    // 查找哈希表
    uword *p = hash_get_mem (am->opcode_by_name, "request");
    if (p)
      *opcode = p[0];  // 返回1
  }

总结

  • 作用:创建字符串到操作码的快速查找表
  • 用途:CLI命令解析,将用户输入的字符串转换为操作码值
  • 性能:O(1)查找时间,比字符串比较快得多

步骤4:配置错误包过滤

源码位置src/vnet/arp/arp.c:878-900

作用和实现原理

配置pcap过滤器,告诉VPP不要跟踪(trace)ARP错误包。这样可以减少跟踪开销,提高性能。

生活化理解

就像"设置监控系统的过滤规则":

复制代码
问题:
- VPP有数据包跟踪功能(类似tcpdump)
- 可以记录所有数据包的处理过程
- 但ARP错误包很多,记录它们会浪费资源
- 如何过滤掉不需要跟踪的包?

解决方案:配置过滤器
- 告诉跟踪系统:这些错误类型不要跟踪
- 减少跟踪开销
- 提高性能

函数调用

c 复制代码
vlib_node_runtime_t *rt = vlib_node_get_runtime (vm, arp_input_node.index);

vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_REPLIES_SENT], 1);
vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_DISABLED], 1);
// ... 其他错误码

代码详解

c 复制代码
// 步骤4.1:获取ARP输入节点的运行时结构
// 说明:运行时结构包含节点的错误统计和跟踪配置
vlib_node_runtime_t *rt = vlib_node_get_runtime (vm, arp_input_node.index);

// 步骤4.2:为每个ARP错误类型配置过滤器
// 说明:vnet_pcap_drop_trace_filter_add_del添加或删除跟踪过滤器
// 参数1:错误索引(rt->errors[错误码])
// 参数2:1 = 添加过滤器(不跟踪),0 = 删除过滤器(跟踪)
vnet_pcap_drop_trace_filter_add_del (rt->errors[ARP_ERROR_REPLIES_SENT], 1);

vnet_pcap_drop_trace_filter_add_del的作用src/vnet/interface_output.c:1189):

c 复制代码
void
vnet_pcap_drop_trace_filter_add_del (u32 error_index, int is_add)
{
  // 在全局过滤器中添加或删除错误索引
  // 说明:当数据包被标记为这个错误时,跟踪系统会跳过它
  if (is_add)
    {
      // 添加到过滤器:不跟踪这个错误
      clib_bitmap_set (pcap_drop_trace_filter, error_index, 1);
    }
  else
    {
      // 从过滤器删除:跟踪这个错误
      clib_bitmap_set (pcap_drop_trace_filter, error_index, 0);
    }
}

错误类型说明

c 复制代码
// ARP_ERROR_REPLIES_SENT:已发送ARP响应(正常情况,不需要跟踪)
// ARP_ERROR_DISABLED:ARP功能已禁用(错误,但很常见,不需要跟踪)
// ARP_ERROR_L3_SRC_ADDRESS_LEARNED:已学习源地址(正常情况,不需要跟踪)
// ... 等等

跟踪系统的工作流程

c 复制代码
// 在数据包处理时(简化版)
if (数据包有错误)
  {
    u32 error_index = b->error;
    
    // 检查是否在过滤器中
    if (clib_bitmap_get (pcap_drop_trace_filter, error_index))
      {
        // 在过滤器中,不跟踪
        return;  // 跳过跟踪
      }
    else
      {
        // 不在过滤器中,正常跟踪
        trace_packet (b);
      }
  }

总结

  • 作用:配置跟踪过滤器,减少不必要的跟踪开销
  • 影响:ARP错误包不会被跟踪,但正常ARP包仍会被跟踪
  • 性能:减少跟踪开销,提高性能

步骤5:注册IP4接口回调

源码位置src/vnet/arp/arp.c:902-906

作用和实现原理

注册回调函数,当IP4接口启用或禁用时,自动启用或禁用该接口上的ARP功能。这实现了ARP功能的自动管理。

生活化理解

就像"订阅通知服务":

复制代码
问题:
- IP4接口可以启用/禁用
- 当接口启用时,ARP功能也应该启用
- 当接口禁用时,ARP功能也应该禁用
- 如何自动同步?

解决方案:注册回调函数
- 告诉IP4模块:接口状态变化时,通知我
- IP4模块会调用回调函数
- 回调函数自动启用/禁用ARP

函数调用

c 复制代码
ip4_enable_disable_interface_callback_t cb = {
  .function = arp_enable_disable_interface,
};
vec_add1 (im->enable_disable_interface_callbacks, cb);

代码详解

c 复制代码
// 步骤5.1:创建回调函数结构
// 说明:ip4_enable_disable_interface_callback_t包含回调函数指针
ip4_enable_disable_interface_callback_t cb = {
  .function = arp_enable_disable_interface,  // 回调函数
  .function_opaque = 0,  // 可选的不透明数据(这里未使用)
};

// 步骤5.2:将回调函数添加到IP4主结构的回调列表
// 说明:vec_add1在向量末尾添加一个元素
vec_add1 (im->enable_disable_interface_callbacks, cb);

回调函数的定义src/vnet/arp/arp.c:829):

c 复制代码
static void
arp_enable_disable_interface (ip4_main_t * im,
                              uword opaque, 
                              u32 sw_if_index, 
                              u32 is_enable)
{
  ethernet_arp_main_t *am = &ethernet_arp_main;

  // 步骤5.2.1:根据is_enable参数决定启用或禁用
  if (is_enable)
    {
      // 接口启用:启用ARP功能
      arp_enable (am, sw_if_index);
    }
  else
    {
      // 接口禁用:禁用ARP功能
      arp_disable (am, sw_if_index);
    }
}

arp_enable的实现src/vnet/arp/arp.c:138):

c 复制代码
static void
arp_enable (ethernet_arp_main_t * am, u32 sw_if_index)
{
  // 步骤5.2.1.1:检查是否已经启用
  if (arp_is_enabled (am, sw_if_index))
    return;  // 已经启用,直接返回

  // 步骤5.2.1.2:扩展接口状态数组(如果需要)
  vec_validate (am->ethernet_arp_by_sw_if_index, sw_if_index);

  // 步骤5.2.1.3:标记接口已启用ARP
  am->ethernet_arp_by_sw_if_index[sw_if_index].enabled = 1;

  // 步骤5.2.1.4:启用ARP响应Feature
  // 说明:Feature Arc是VPP的特性链,arp-reply是ARP响应特性
  vnet_feature_enable_disable ("arp", "arp-reply", sw_if_index, 1, NULL, 0);

  // 步骤5.2.1.5:禁用ARP禁用Feature
  // 说明:arp-disabled是当ARP禁用时的处理节点
  vnet_feature_enable_disable ("arp", "arp-disabled", sw_if_index, 0, NULL, 0);
}

arp_disable的实现src/vnet/arp/arp.c:152):

c 复制代码
static void
arp_disable (ethernet_arp_main_t * am, u32 sw_if_index)
{
  // 步骤5.2.2.1:检查是否已经禁用
  if (!arp_is_enabled (am, sw_if_index))
    return;  // 已经禁用,直接返回

  // 步骤5.2.2.2:启用ARP禁用Feature
  vnet_feature_enable_disable ("arp", "arp-disabled", sw_if_index, 1, NULL, 0);

  // 步骤5.2.2.3:禁用ARP响应Feature
  vnet_feature_enable_disable ("arp", "arp-reply", sw_if_index, 0, NULL, 0);

  // 步骤5.2.2.4:标记接口已禁用ARP
  am->ethernet_arp_by_sw_if_index[sw_if_index].enabled = 0;
}

回调函数的调用时机src/vnet/ip/ip4_forward.c:631):

c 复制代码
void
ip4_sw_interface_enable_disable (u32 sw_if_index, u32 is_enable)
{
  ip4_main_t *im = &ip4_main;
  
  // ... 其他处理 ...

  // 步骤5.3:遍历所有注册的回调函数并调用
  {
    ip4_enable_disable_interface_callback_t *cb;
    vec_foreach (cb, im->enable_disable_interface_callbacks)
      {
        // 调用回调函数
        // 参数1:ip4_main_t指针
        // 参数2:不透明数据(回调函数注册时提供)
        // 参数3:接口索引
        // 参数4:是否启用(1=启用,0=禁用)
        cb->function (im, cb->function_opaque, sw_if_index, is_enable);
      }
  }
}

实际工作流程

复制代码
1. 用户执行CLI命令:set interface ip address GigabitEthernet0/0/0 192.168.1.1/24
2. IP4模块检测到接口需要启用IP4
3. IP4模块调用:ip4_sw_interface_enable_disable (sw_if_index, 1)
4. IP4模块遍历回调列表,调用所有回调函数
5. ARP的回调函数被调用:arp_enable_disable_interface (im, 0, sw_if_index, 1)
6. ARP模块自动启用该接口的ARP功能
7. 结果:接口启用IP4时,ARP功能自动启用

总结

  • 作用:实现ARP功能的自动管理,与IP4接口状态同步
  • 机制:通过回调函数实现事件通知
  • 好处:用户无需手动管理ARP,系统自动处理

步骤6:注册IP邻居VFT

源码位置src/vnet/arp/arp.c:854-859, 925

作用和实现原理

注册ARP的VFT(Virtual Function Table,虚拟函数表)到IP邻居模块。VFT是一种函数指针表,用于实现多态。IP邻居模块通过VFT调用ARP模块的代理ARP功能。

生活化理解

就像"注册服务提供商":

复制代码
问题:
- IP邻居模块需要代理ARP功能
- 但代理ARP功能由ARP模块实现
- 如何让IP邻居模块调用ARP模块的函数?

解决方案:VFT(虚拟函数表)
- ARP模块提供函数指针表(VFT)
- IP邻居模块保存这个表
- 需要时通过函数指针调用
- 实现模块间的解耦

VFT定义src/vnet/arp/arp.c:854):

c 复制代码
const static ip_neighbor_vft_t arp_vft = {
  .inv_proxy4_add = arp_proxy_add,        // 添加IPv4代理ARP范围
  .inv_proxy4_del = arp_proxy_del,        // 删除IPv4代理ARP范围
  .inv_proxy4_enable = arp_proxy_enable,  // 在接口上启用代理ARP
  .inv_proxy4_disable = arp_proxy_disable, // 在接口上禁用代理ARP
};

VFT结构定义src/vnet/ip-neighbor/ip_neighbor.h:104):

c 复制代码
typedef struct ip_neighbor_vft_t_
{
  // IPv4代理ARP配置函数
  ip4_neighbor_proxy_cfg_t inv_proxy4_enable;   // 启用函数指针
  ip4_neighbor_proxy_cfg_t inv_proxy4_disable;  // 禁用函数指针
  
  // IPv4代理ARP地址范围函数
  ip4_neighbor_proxy_addr_t inv_proxy4_add;     // 添加函数指针
  ip4_neighbor_proxy_addr_t inv_proxy4_del;     // 删除函数指针
  
  // IPv6代理ND函数(ARP模块不实现,为NULL)
  ip6_neighbor_proxy_cfg_t inv_proxy6_add;
  ip6_neighbor_proxy_cfg_t inv_proxy6_del;
} ip_neighbor_vft_t;

函数指针类型定义src/vnet/ip-neighbor/ip_neighbor.h:94):

c 复制代码
// IPv4代理ARP配置函数类型(启用/禁用)
typedef int (*ip4_neighbor_proxy_cfg_t) (u32 sw_if_index);

// IPv4代理ARP地址范围函数类型(添加/删除)
typedef int (*ip4_neighbor_proxy_addr_t) (u32 fib_index,
                                          const ip4_address_t * start,
                                          const ip4_address_t * end);

注册函数调用

c 复制代码
ip_neighbor_register (AF_IP4, &arp_vft);

ip_neighbor_register的实现src/vnet/ip-neighbor/ip_neighbor.c:1089):

c 复制代码
// 全局VFT数组,每个地址族一个VFT
static ip_neighbor_vft_t ip_nbr_vfts[N_AF];

void
ip_neighbor_register (ip_address_family_t af, const ip_neighbor_vft_t * vft)
{
  // 步骤6.1:将VFT复制到全局数组
  // 说明:af是地址族(AF_IP4或AF_IP6)
  // 对于IPv4,af = AF_IP4 = 0
  ip_nbr_vfts[af] = *vft;
}

VFT的使用src/vnet/ip-neighbor/ip_neighbor.c:1158):

c 复制代码
int
ip4_neighbor_proxy_add (u32 fib_index,
                        const ip4_address_t * start,
                        const ip4_address_t * end)
{
  // 步骤6.2:检查IPv4的VFT是否已注册
  if (ip_nbr_vfts[AF_IP4].inv_proxy4_add)
    {
      // 步骤6.3:通过函数指针调用ARP模块的函数
      return (ip_nbr_vfts[AF_IP4].inv_proxy4_add (fib_index, start, end));
    }

  // 如果没有注册VFT,返回错误
  return (-1);
}

实际调用流程

复制代码
1. 用户执行CLI命令:set ip neighbor proxy 192.168.1.0 192.168.1.255
2. IP邻居模块解析命令,调用:ip4_neighbor_proxy_add (fib_index, start, end)
3. IP邻居模块查找VFT:ip_nbr_vfts[AF_IP4].inv_proxy4_add
4. 找到函数指针:arp_proxy_add
5. 通过函数指针调用:arp_proxy_add (fib_index, start, end)
6. ARP模块执行实际的代理ARP配置
7. 返回结果给IP邻居模块

VFT的优势

  1. 解耦:IP邻居模块不需要知道ARP模块的具体实现
  2. 可扩展:可以注册不同的实现(如IPv6的ND模块)
  3. 可选功能:如果VFT未注册,功能不可用但不报错

总结

  • 作用:注册ARP模块的函数指针表到IP邻居模块
  • 机制:通过VFT实现模块间的函数调用
  • 好处:实现模块解耦,支持可选功能

4.3 初始化依赖

c 复制代码
VLIB_INIT_FUNCTION (ethernet_arp_init) =
{
  .runs_after = VLIB_INITS("ethernet_init",
                           "ip_neighbor_init"),
};

说明

  • ARP初始化必须在ethernet_init之后(需要以太网模块)
  • ARP初始化必须在ip_neighbor_init之后(需要IP邻居模块)

第五章:ARP多线程冲突处理机制

章节意义

VPP采用多线程架构,多个worker线程并发处理数据包。ARP模块需要解决两个关键问题:

  1. 写入冲突:多个worker线程需要更新IP邻居表,如何避免冲突?
  2. 读取冲突:多个worker线程需要读取FIB表,如何保证线程安全?

本章详细讲解ARP如何通过RPC机制和RCU机制解决这些线程冲突问题。

5.1 多线程冲突问题

生活化理解

就像"多个收银员同时工作":

复制代码
问题1:写入冲突
- 多个收银员(worker线程)同时收到ARP响应
- 都需要更新客户信息表(IP邻居表)
- 如果同时写入,会冲突!

问题2:读取冲突
- 多个收银员需要查询商品目录(FIB表)
- 管理员(主线程)在更新目录
- 如何保证查询时不会读到错误的数据?

VPP的解决方案

  1. 写入冲突:使用RPC机制,worker线程通过RPC将更新请求发送给主线程,由主线程统一处理
  2. 读取冲突:使用RCU机制,FIB表支持无锁只读,多个worker线程可以并发读取

5.2 RPC机制:解决写入冲突

5.2.1 问题场景

场景:多个worker线程同时收到ARP响应,需要更新IP邻居表

复制代码
Worker线程1:收到ARP响应 192.168.1.1 → MAC1
Worker线程2:收到ARP响应 192.168.1.2 → MAC2
Worker线程3:收到ARP响应 192.168.1.1 → MAC1(重复)

问题:
- IP邻居表是共享的全局数据结构
- 多个线程同时写入会冲突
- 需要加锁?但加锁会降低性能

解决方案:RPC机制

  • Worker线程不直接更新IP邻居表
  • 通过RPC将更新请求发送给主线程
  • 主线程统一处理所有更新请求
  • 避免多线程写入冲突
5.2.2 RPC机制实现

源码位置src/vnet/ip-neighbor/ip_neighbor_dp.c

作用和实现原理

ip_neighbor_learn_dp是数据平面的学习入口,它使用RPC机制将学习请求发送给主线程。

c 复制代码
void
ip_neighbor_learn_dp (const ip_neighbor_learn_t * l)
{
  // 步骤1:通过RPC调用主线程的函数
  // 参数1:主线程要执行的函数(ip_neighbor_learn)
  // 参数2:要传递的数据(学习信息)
  // 参数3:数据大小
  vl_api_rpc_call_main_thread (ip_neighbor_learn, (u8 *) l, sizeof (*l));
}

vl_api_rpc_call_main_thread的实现src/vlibmemory/memclnt_api.c:617):

c 复制代码
always_inline void
vl_api_rpc_call_main_thread_inline (void *fp, u8 *data, u32 data_length,
                                    u8 force_rpc)
{
  vl_api_rpc_call_t *mp;
  vlib_main_t *vm_global = vlib_get_first_main ();  // 主线程
  vlib_main_t *vm = vlib_get_main ();               // 当前线程

  // 步骤1:检查是否在主线程
  // 说明:如果已经在主线程,可以直接调用函数,不需要RPC
  if ((force_rpc == 0) && (vlib_get_thread_index () == 0))
    {
      void (*call_fp) (void *);

      // 同步所有worker线程(确保一致性)
      vlib_worker_thread_barrier_sync (vm);

      // 直接调用函数
      call_fp = fp;
      call_fp (data);

      // 释放同步
      vlib_worker_thread_barrier_release (vm);
      return;
    }

  // 步骤2:在worker线程中,需要真正的RPC调用
  // 步骤2.1:分配RPC消息
  mp = vl_msg_api_alloc_as_if_client (sizeof (*mp) + data_length);

  // 步骤2.2:填充RPC消息
  clib_memset (mp, 0, sizeof (*mp));
  clib_memcpy_fast (mp->data, data, data_length);  // 复制数据
  mp->_vl_msg_id = ntohs (VL_API_RPC_CALL);
  mp->function = pointer_to_uword (fp);            // 函数指针
  mp->need_barrier_sync = 1;                        // 需要同步

  // 步骤2.3:将RPC请求添加到主线程的待处理队列
  // 说明:pending_rpc_requests是主线程的RPC请求队列
  if (vm == vm_global)
    clib_spinlock_lock_if_init (&vm_global->pending_rpc_lock);
  vec_add1 (vm->pending_rpc_requests, (uword) mp);
  if (vm == vm_global)
    clib_spinlock_unlock_if_init (&vm_global->pending_rpc_lock);
}

RPC消息处理src/vlibmemory/memory_api.c:924):

c 复制代码
int
vl_mem_api_handle_rpc (vlib_main_t * vm, vlib_node_runtime_t * node)
{
  api_main_t *am = vlibapi_get_main ();
  int i;
  uword *tmp, mp;

  // 步骤1:交换待处理队列和处理队列
  // 说明:避免在处理时接收新的RPC请求
  clib_spinlock_lock_if_init (&vm->pending_rpc_lock);
  tmp = vm->processing_rpc_requests;
  vec_reset_length (tmp);
  vm->processing_rpc_requests = vm->pending_rpc_requests;  // 交换
  vm->pending_rpc_requests = tmp;                          // 清空待处理队列
  clib_spinlock_unlock_if_init (&vm->pending_rpc_lock);

  // 步骤2:处理所有RPC请求
  if (PREDICT_TRUE (vec_len (vm->processing_rpc_requests)))
    {
      // 步骤2.1:同步所有worker线程
      // 说明:确保在处理RPC时,所有worker线程都暂停
      vl_msg_api_barrier_sync ();

      // 步骤2.2:逐个处理RPC请求
      for (i = 0; i < vec_len (vm->processing_rpc_requests); i++)
        {
          mp = vm->processing_rpc_requests[i];
          // 调用RPC消息中指定的函数
          vl_mem_api_handler_with_vm_node (am, am->vlib_rp, (void *) mp, vm,
                                           node, 0);
        }

      // 步骤2.3:释放同步
      vl_msg_api_barrier_release ();
    }

  return 0;
}

RPC消息处理函数src/vlibmemory/memclnt_api.c:567):

c 复制代码
static void
vl_api_rpc_call_t_handler (vl_api_rpc_call_t *mp)
{
  int (*fp) (void *);
  i32 rv = 0;
  vlib_main_t *vm = vlib_get_main ();

  // 步骤1:获取函数指针
  fp = uword_to_pointer (mp->function, int (*) (void *));

  // 步骤2:如果需要同步,先同步所有worker线程
  if (mp->need_barrier_sync)
    vlib_worker_thread_barrier_sync (vm);

  // 步骤3:调用函数(在主线程中执行)
  rv = fp (mp->data);

  // 步骤4:如果需要同步,释放同步
  if (mp->need_barrier_sync)
    vlib_worker_thread_barrier_release (vm);
}
5.2.3 ARP学习流程中的RPC

完整流程

复制代码
1. Worker线程收到ARP响应
   ↓
2. 调用 arp_learn() → ip_neighbor_learn_dp()
   ↓
3. ip_neighbor_learn_dp() 调用 vl_api_rpc_call_main_thread()
   ↓
4. RPC消息添加到主线程的 pending_rpc_requests 队列
   ↓
5. 主线程在处理循环中调用 vl_mem_api_handle_rpc()
   ↓
6. 主线程同步所有worker线程(barrier_sync)
   ↓
7. 主线程调用 ip_neighbor_learn()(实际的学习函数)
   ↓
8. 主线程更新IP邻居表(单线程,无冲突)
   ↓
9. 主线程释放同步(barrier_release)
   ↓
10. Worker线程继续处理数据包

源码证据

数据平面入口src/vnet/ip-neighbor/ip_neighbor_dp.c:28):

c 复制代码
void
ip_neighbor_learn_dp (const ip_neighbor_learn_t * l)
{
  // 通过RPC调用主线程
  vl_api_rpc_call_main_thread (ip_neighbor_learn, (u8 *) l, sizeof (*l));
}

主线程处理函数src/vnet/ip-neighbor/ip_neighbor.c:784):

c 复制代码
void
ip_neighbor_learn (const ip_neighbor_learn_t * l)
{
  // 在主线程中执行,更新IP邻居表
  ip_neighbor_add (&l->ip, &l->mac, l->sw_if_index,
                   IP_NEIGHBOR_FLAG_DYNAMIC, NULL);
}

RPC机制的优势

  1. 避免写入冲突:所有更新都在主线程执行,单线程写入,无冲突
  2. 批量处理:多个RPC请求可以批量处理,提高效率
  3. 同步保证:使用barrier同步,确保更新时worker线程暂停

RPC机制的开销

  1. 延迟:RPC请求需要等待主线程处理,有一定延迟
  2. 序列化:需要复制数据到RPC消息
  3. 同步开销:barrier同步会暂停所有worker线程

设计权衡

  • 写入操作少:ARP学习是低频操作,RPC开销可接受
  • 数据包处理快:worker线程不需要等待,可以继续处理数据包
  • 一致性保证:单线程更新保证数据一致性

5.3 RCU机制:解决读取冲突

5.3.1 问题场景

场景:多个worker线程需要查询FIB表

复制代码
Worker线程1:查询 192.168.1.1 的路由
Worker线程2:查询 192.168.1.2 的路由
Worker线程3:查询 192.168.1.3 的路由
主线程:正在更新FIB表(添加/删除路由)

问题:
- 如果worker线程读取时,主线程正在修改,会读到错误数据
- 如果加锁,多个worker线程会竞争锁,性能下降
- 如何保证读取的正确性和性能?

解决方案:RCU(Read-Copy-Update)机制

  • 只读操作:Worker线程的FIB查找是纯只读操作,不修改FIB表
  • 无锁读取:多个worker线程可以并发读取,无需加锁
  • 原子更新:主线程更新时使用原子操作,worker线程读取到的是旧版本或新版本,不会读到中间状态
5.3.2 FIB查找的只读特性

源码位置src/vnet/fib/ip4_fib.h

作用和实现原理

FIB查找函数是纯只读的,不修改任何FIB表结构。

c 复制代码
always_inline u32
ip4_fib_lookup (ip4_main_t * im, u32 sw_if_index, ip4_address_t * dst)
{
    // 只读操作:查找FIB表,返回DPO索引
    return (ip4_fib_table_lookup_lb(
        ip4_fib_get(vec_elt (im->fib_index_by_sw_if_index, sw_if_index)),
        dst));
}

mtrie查找的只读特性src/vnet/ip/ip4_mtrie.h):

c 复制代码
always_inline index_t
ip4_fib_forwarding_lookup (u32 fib_index,
                           const ip4_address_t * addr)
{
    ip4_mtrie_leaf_t leaf;
    ip4_mtrie_16_t * mtrie;

    // 步骤1:获取mtrie(只读)
    mtrie = &ip4_fib_get(fib_index)->mtrie;

    // 步骤2:查找(只读,不修改mtrie)
    leaf = ip4_mtrie_16_lookup_step_one (mtrie, addr);
    leaf = ip4_mtrie_16_lookup_step (leaf, addr, 2);
    leaf = ip4_mtrie_16_lookup_step (leaf, addr, 3);

    // 步骤3:返回结果(只读)
    return (ip4_mtrie_leaf_get_adj_index(leaf));
}

只读操作的特点

  1. 不修改数据结构:查找过程只读取mtrie,不修改任何节点
  2. 无副作用:多次查找不会改变FIB表的状态
  3. 可并发:多个线程可以同时执行查找,互不干扰
5.3.3 RCU机制在FIB更新中的实现

源码位置src/vnet/ip/ip4_mtrie.c

作用和实现原理

当主线程更新FIB表时,使用原子操作更新mtrie节点,实现RCU机制。

原子更新叶子节点

c 复制代码
// 在set_leaf函数中(简化版)
static void
set_leaf (const ip4_mtrie_set_unset_leaf_args_t *a, ...)
{
  ip4_mtrie_8_ply_t *old_ply;
  
  // ... 准备新叶子 ...
  
  // 使用原子操作更新叶子
  // 说明:clib_atomic_store_rel_n使用release语义
  // release语义确保:更新操作之前的所有内存操作对读取者可见
  clib_atomic_store_rel_n (&old_ply->leaves[i], new_leaf);
  
  // 更新前缀长度(也需要原子操作)
  old_ply->dst_address_bits_of_leaves[i] = a->dst_address_length;
}

原子操作的内存语义

c 复制代码
// Release语义(写入端)
clib_atomic_store_rel_n (&old_ply->leaves[i], new_leaf);

// 等价于:
// 1. 确保所有之前的写操作完成
// 2. 原子地更新leaves[i]
// 3. 其他线程可以看到更新后的值

读取端的无锁读取

c 复制代码
// Worker线程读取(无锁)
leaf = old_ply->leaves[i];  // 普通读取即可

// 说明:
// - 如果读取发生在更新之前,读到旧值
// - 如果读取发生在更新之后,读到新值
// - 不会读到中间状态(因为原子操作)
5.3.4 RCU机制在IPv6 FIB中的实现

源码位置src/vnet/fib/ip6_fib.c:282

作用和实现原理

IPv6 FIB使用双缓冲机制实现RCU,更清晰地展示了RCU的工作原理。

c 复制代码
static void
compute_prefix_lengths_in_search_order (ip6_fib_fwding_table_instance_t *table)
{
    u8 *old, *prefix_lengths_in_search_order = NULL;
    int i;

    /*
     * build the list in a scratch space then cutover so the workers
     * can continue uninterrupted.
     */
    // 步骤1:保存旧指针
    old = table->prefix_lengths_in_search_order;

    // 步骤2:在临时空间创建新数据
    clib_bitmap_foreach (i, table->non_empty_dst_address_length_bitmap)
     {
        int dst_address_length = 128 - i;
        vec_add1(prefix_lengths_in_search_order, dst_address_length);
    }

    // 步骤3:原子切换指针(RCU的关键步骤)
    // 说明:原子地切换指针,worker线程立即看到新数据
    table->prefix_lengths_in_search_order = prefix_lengths_in_search_order;

    /*
     * let the workers go once round the track before we free the old set
     */
    // 步骤4:等待所有worker线程完成当前循环
    // 说明:确保没有worker线程还在使用旧数据
    vlib_worker_wait_one_loop();

    // 步骤5:安全删除旧数据
    vec_free(old);
}

RCU的三个阶段

  1. Read(读):Worker线程读取旧数据(无锁)
  2. Copy(复制):主线程创建新数据的副本
  3. Update(更新):主线程原子地切换到新数据,等待后删除旧数据

生活化理解

复制代码
就像"图书馆更新目录":

传统方式(加锁):
- 更新目录时,所有读者等待(加锁)
- 更新完成后,读者才能查询(释放锁)
- 问题:读者被阻塞,效率低

RCU方式:
- 读者可以继续使用旧目录(无锁读取)
- 管理员创建新目录(Copy)
- 管理员原子切换指针,指向新目录(Update)
- 等待所有读者完成当前查询后,删除旧目录
- 优势:读者不被阻塞,效率高
5.3.5 ARP中的FIB查找

源码位置src/vnet/arp/arp.c(在arp_reply节点中)

作用和实现原理

ARP模块使用FIB查找来判断IP地址是否是本地地址。

c 复制代码
// 在arp_reply节点中
dst_fei = ip4_fib_table_lookup (ip4_fib_get (fib_index0),
                                &arp0->ip4_over_ethernet[1].ip4, 32);

线程安全保证

  1. 无锁查找ip4_fib_table_lookup是纯只读操作,不需要锁
  2. 并发安全:多个worker线程可以同时执行查找
  3. 一致性保证:RCU机制保证读取到的是完整的数据(旧版本或新版本)

性能优势

  1. 无锁竞争:避免了锁竞争,提高了并发性能
  2. 缓存友好:只读操作对CPU缓存友好
  3. 可扩展性:性能随worker线程数量线性扩展

5.4 两种机制的对比

特性 RPC机制(写入) RCU机制(读取)
用途 解决写入冲突 解决读取冲突
操作类型 写入操作(更新IP邻居表) 读取操作(查询FIB表)
频率 低频(ARP学习不频繁) 高频(每个数据包都可能查询)
延迟 有延迟(需要RPC) 无延迟(直接读取)
同步 需要barrier同步 无需同步
性能 可接受(写入操作少) 高性能(无锁读取)

5.5 总结

ARP模块的线程安全策略

  1. 写入操作:使用RPC机制,worker线程通过RPC将更新请求发送给主线程

    • 优势:避免多线程写入冲突,保证数据一致性
    • 开销:RPC延迟和同步开销,但写入操作少,可接受
  2. 读取操作:使用RCU机制,worker线程无锁读取FIB表

    • 优势:无锁读取,高性能,可扩展
    • 保证:RCU机制保证读取的一致性

设计原则

  • 写入集中化:所有写入操作在主线程执行,避免多线程冲突
  • 读取并行化:所有读取操作在worker线程执行,无锁并发
  • 性能优化:高频操作(读取)无锁,低频操作(写入)使用RPC

第六章:ARP请求处理流程

章节意义

本章介绍ARP请求的处理流程,这是ARP模块的核心功能之一。当VPP收到ARP请求时,需要判断是否应该响应,以及如何生成响应。本章详细说明了从数据包接收到响应生成的完整流程,包括VLAN处理、接口管理、FIB查找等关键步骤。

6.1 arp-input节点处理

文件位置src/vnet/arp/arp.c

作用和实现原理

arp_input是ARP模块的入口节点,负责接收所有ARP数据包并进行初步验证。它是ARP Feature Arc的起始节点,所有ARP数据包处理都从这里开始。

设计原理

  • 批量处理:使用VPP标准的向量化处理模式,一次处理多个数据包,提高性能
  • 快速验证:只进行最基本的格式验证(L2类型、L3类型、目标地址),复杂验证交给后续节点
  • Feature Arc启动:验证通过后启动Feature Arc,根据接口配置动态路由到不同的处理节点
  • 错误处理:验证失败时直接设置错误码,不进入Feature Arc

处理流程

  1. 从frame中获取数据包向量
  2. 对每个数据包进行基本验证
  3. 验证通过:启动Feature Arc,进入后续处理
  4. 验证失败:设置错误码,直接丢弃

源码实现

c 复制代码
static uword
arp_input (vlib_main_t * vm, vlib_node_runtime_t * node, vlib_frame_t * frame)
{
  u32 n_left_from, next_index, *from, *to_next, n_left_to_next;
  ethernet_arp_main_t *am = &ethernet_arp_main;

  // 步骤1:获取frame中的数据包索引向量
  from = vlib_frame_vector_args (frame);
  n_left_from = frame->n_vectors;  // 剩余待处理的数据包数量
  next_index = node->cached_next_index;  // 缓存的下一跳节点索引

  // 步骤2:批量处理数据包(外层循环处理frame,内层循环处理向量)
  while (n_left_from > 0)
    {
      // 获取下一跳frame的缓冲区
      vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);

      // 步骤3:逐个处理数据包
      while (n_left_from > 0 && n_left_to_next > 0)
        {
          const ethernet_arp_header_t *arp0;
          arp_input_next_t next0;
          vlib_buffer_t *p0;
          u32 pi0, error0;

          // 从输入向量获取数据包索引,放入输出向量
          pi0 = to_next[0] = from[0];
          from += 1;
          to_next += 1;
          n_left_from -= 1;
          n_left_to_next -= 1;

          // 获取数据包缓冲区和ARP头
          p0 = vlib_get_buffer (vm, pi0);
          arp0 = vlib_buffer_get_current (p0);

          // 步骤4:初始化错误码和下一跳(默认丢弃)
          error0 = ARP_ERROR_REPLIES_SENT;  // 默认无错误
          next0 = ARP_INPUT_NEXT_DROP;

          // 步骤5:验证L2类型(必须是以太网)
          error0 = (arp0->l2_type != clib_net_to_host_u16 (
                               ETHERNET_ARP_HARDWARE_TYPE_ethernet) ?
                        ARP_ERROR_L2_TYPE_NOT_ETHERNET :
                        error0);
          // 步骤6:验证L3类型(必须是IPv4)
          error0 = (arp0->l3_type != clib_net_to_host_u16 (ETHERNET_TYPE_IP4) ?
                        ARP_ERROR_L3_TYPE_NOT_IP4 :
                        error0);
          // 步骤7:验证目标地址(不能为0)
          error0 = (0 == arp0->ip4_over_ethernet[0].ip4.as_u32 ?
                        ARP_ERROR_L3_DST_ADDRESS_UNSET :
                        error0);

          // 步骤8:根据验证结果决定下一步
          if (ARP_ERROR_REPLIES_SENT == error0)
            {
              // 验证通过:启动Feature Arc
              next0 = ARP_INPUT_NEXT_DISABLED;  // 初始下一跳(会被Feature Arc修改)
              vnet_feature_arc_start (am->feature_arc_index,
                                      vnet_buffer (p0)->sw_if_index[VLIB_RX],
                                      &next0, p0);
            }
          else
            {
              // 验证失败:设置错误码
              p0->error = node->errors[error0];
            }

          // 步骤9:将数据包加入下一跳frame
          vlib_validate_buffer_enqueue_x1 (vm, node, next_index, to_next,
                                           n_left_to_next, pi0, next0);
        }

      // 将frame返回给调度器
      vlib_put_next_frame (vm, node, next_index, n_left_to_next);
    }

  return frame->n_vectors;
}

关键实现细节

  1. 向量化处理

    • n_left_from:剩余待处理的输入数据包数量
    • n_left_to_next:当前输出frame的剩余空间
    • 内层循环同时检查两个条件,确保输入和输出都有空间
  2. 错误码链式检查

    • 使用三元运算符链式检查多个条件
    • 如果前面的检查失败,error0会被设置为错误码
    • 如果所有检查通过,error0保持为ARP_ERROR_REPLIES_SENT
  3. Feature Arc启动

    • vnet_feature_arc_start会根据接口配置修改next0
    • 如果接口启用了ARP,next0会被修改为arp-reply节点
    • 如果接口禁用了ARP,next0保持为arp-disabled节点

6.2 验证步骤

验证1:L2类型检查

c 复制代码
error0 = (arp0->l2_type != clib_net_to_host_u16 (
                     ETHERNET_ARP_HARDWARE_TYPE_ethernet) ?
              ARP_ERROR_L2_TYPE_NOT_ETHERNET :
              error0);

说明

  • 检查硬件类型是否为以太网(值为1)
  • 如果不是,设置错误码

验证2:L3类型检查

c 复制代码
error0 = (arp0->l3_type != clib_net_to_host_u16 (ETHERNET_TYPE_IP4) ?
              ARP_ERROR_L3_TYPE_NOT_IP4 :
              error0);

说明

  • 检查协议类型是否为IPv4(值为0x0800)
  • 如果不是,设置错误码

验证3:目标地址检查

c 复制代码
error0 = (0 == arp0->ip4_over_ethernet[0].ip4.as_u32 ?
              ARP_ERROR_L3_DST_ADDRESS_UNSET :
              error0);

说明

  • 检查目标IP地址是否已设置
  • 如果为0,设置错误码

6.3 Feature Arc启动

c 复制代码
if (ARP_ERROR_REPLIES_SENT == error0)
  {
    next0 = ARP_INPUT_NEXT_DISABLED;
    vnet_feature_arc_start (am->feature_arc_index,
                            vnet_buffer (p0)->sw_if_index[VLIB_RX],
                            &next0, p0);
  }

说明

  • 如果验证通过,启动ARP Feature Arc
  • 根据接口配置,路由到相应的Feature节点
  • 初始下一跳设置为arp-disabled,但Feature Arc会根据接口状态修改

6.4 arp-reply节点处理

文件位置src/vnet/arp/arp.c

作用和实现原理

arp_reply节点是ARP Feature Arc中的核心处理节点,负责判断是否需要响应ARP请求。它通过FIB(Forwarding Information Base)查找来判断目标IP是否是本地地址,只有本地地址才会生成ARP响应。

设计原理

  • FIB集成:使用FIB模块判断IP地址是否属于本地接口,而不是简单的IP地址比较
  • 支持无编号接口:通过FIB可以正确处理无编号接口(unnumbered interface)场景
  • 支持多接口:一个IP地址可能配置在多个接口上,FIB可以找到正确的接口
  • 安全验证:验证源地址和目标地址的合法性,防止ARP欺骗

处理流程

  1. 获取接口的FIB索引
  2. 在FIB中查找目标IP地址
  3. 检查目标IP是否是本地地址(通过FIB条目标志)
  4. 如果是本地地址且是ARP请求,生成响应
  5. 学习请求者的IP到MAC映射

源码实现

c 复制代码
static uword
arp_reply (vlib_main_t * vm, vlib_node_runtime_t * node, vlib_frame_t * frame)
{
  vnet_main_t *vnm = vnet_get_main ();
  // ... 省略frame处理循环的初始部分
  
  while (n_left_from > 0 && n_left_to_next > 0)
    {
      vlib_buffer_t *p0;
      ethernet_arp_header_t *arp0;
      ethernet_header_t *eth_rx;
      const ip4_address_t *if_addr0;
      u32 pi0, error0, next0, sw_if_index0, conn_sw_if_index0, fib_index0;
      u8 dst_is_local0, is_vrrp_reply0;
      fib_node_index_t dst_fei, src_fei;
      const fib_prefix_t *pfx0;
      fib_entry_flag_t src_flags, dst_flags;

      // ... 获取数据包和ARP头的代码
      
      // 步骤1:初始化变量
      next0 = ARP_REPLY_NEXT_DROP;  // 默认丢弃
      error0 = ARP_ERROR_REPLIES_SENT;  // 默认无错误
      sw_if_index0 = vnet_buffer (p0)->sw_if_index[VLIB_RX];  // 接收接口

      // 步骤2:获取接口的FIB索引
      // 说明:每个接口关联一个FIB表,用于路由查找
      fib_index0 = ip4_fib_table_get_index_for_sw_if_index (sw_if_index0);
      if (~0 == fib_index0)
        {
          // 接口没有关联FIB表,无法处理ARP
          error0 = ARP_ERROR_INTERFACE_NO_TABLE;
          goto drop;
        }

      // 步骤3:在FIB中查找目标IP地址(被查询的IP)
      // 说明:查找ARP请求中要查询的IP地址(ip4_over_ethernet[1]是目标)
      // 32表示查找精确匹配(/32前缀)
      dst_fei = ip4_fib_table_lookup (ip4_fib_get (fib_index0),
                                      &arp0->ip4_over_ethernet[1].ip4, 32);
      // 获取解析该FIB条目的接口(可能不是接收接口,支持无编号接口)
      conn_sw_if_index0 = fib_entry_get_any_resolving_interface (dst_fei);

      // 步骤4:在FIB中查找源IP地址(请求者的IP)
      // 说明:用于验证请求者的合法性
      src_fei = ip4_fib_table_lookup (ip4_fib_get (fib_index0),
                                      &arp0->ip4_over_ethernet[0].ip4, 32);
      
      // 步骤5:检查目标IP是否是本地地址
      // 说明:通过FIB条目标志判断,支持连接路由(CONNECTED)和邻接路由(ADJ)
      dst_flags = fib_entry_get_flags_for_source (dst_fei, FIB_SOURCE_ADJ);
      dst_is_local0 = (arp_dst_fib_check (dst_fei, &dst_flags) != ARP_DST_FIB_NONE);

      // 步骤6:检查源IP是否是本地地址
      // 说明:用于验证请求者是否在本地子网
      src_flags = fib_entry_get_flags_for_source (src_fei, FIB_SOURCE_ADJ);
      
      // ... 更多验证和处理逻辑(VRRP检查、地址匹配检查等)
      
      // 步骤7:判断是否需要生成ARP响应
      // 条件1:必须是ARP请求(不是响应)
      // 条件2:目标IP必须是本地地址
      if (arp0->opcode == clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_request) &&
          dst_is_local0)
        {
          // 步骤8:获取接口的第一个IP地址
          // 说明:接口可能有多个IP地址,使用第一个作为响应源地址
          if_addr0 = ip4_interface_first_address (sw_if_index0, fib_index0);
          
          // 步骤9:生成ARP响应
          // 说明:修改原始数据包,将其转换为ARP响应
          next0 = arp_mk_reply (vnm, p0, sw_if_index0, if_addr0, arp0, eth_rx);
          
          // 步骤10:学习请求者的IP到MAC映射
          // 说明:从ARP请求中学习请求者的IP和MAC地址对应关系
          // ip4_over_ethernet[1]是请求者的信息(在ARP请求中,[1]是发送者)
          if (!error0)
            error0 = arp_learn (sw_if_index0, &arp0->ip4_over_ethernet[1]);
        }
    }
}

关键实现细节

  1. FIB查找机制

    • ip4_fib_table_lookup在FIB中查找IP地址
    • 返回FIB条目索引(fib_node_index_t),用于后续查询
    • /32前缀表示精确匹配,查找主机路由
  2. 本地地址判断

    • arp_dst_fib_check检查FIB条目类型
    • ARP_DST_FIB_ADJ:邻接路由(直接连接)
    • ARP_DST_FIB_CONN:连接路由(接口上配置的地址)
    • 只有这两种类型才认为是本地地址
  3. 无编号接口支持

    • conn_sw_if_index0可能不同于sw_if_index0
    • 支持无编号接口场景(一个接口借用另一个接口的IP地址)
  4. 学习时机

    • 在生成响应后立即学习请求者的映射
    • 使用ip4_over_ethernet[1](请求者的信息)
    • 即使响应生成失败,也尝试学习(可能学到部分信息)

6.5 ARP接口管理机制

问题:ARP是如何管理本机接口的?

解答

VPP的ARP模块采用接口级(per-interface)管理策略,通过Feature Arc机制动态启用/禁用ARP功能。

6.5.1 接口ARP状态管理

源码位置src/vnet/arp/arp.c

c 复制代码
static void
arp_enable (ethernet_arp_main_t * am, u32 sw_if_index)
{
  if (arp_is_enabled (am, sw_if_index))
    return;

  vec_validate (am->ethernet_arp_by_sw_if_index, sw_if_index);
  am->ethernet_arp_by_sw_if_index[sw_if_index].enabled = 1;

  // 启用arp-reply节点,禁用arp-disabled节点
  vnet_feature_enable_disable ("arp", "arp-reply", sw_if_index, 1, NULL, 0);
  vnet_feature_enable_disable ("arp", "arp-disabled", sw_if_index, 0, NULL, 0);
}

static void
arp_disable (ethernet_arp_main_t * am, u32 sw_if_index)
{
  if (!arp_is_enabled (am, sw_if_index))
    return;

  // 禁用arp-reply节点,启用arp-disabled节点
  vnet_feature_enable_disable ("arp", "arp-disabled", sw_if_index, 1, NULL, 0);
  vnet_feature_enable_disable ("arp", "arp-reply", sw_if_index, 0, NULL, 0);

  am->ethernet_arp_by_sw_if_index[sw_if_index].enabled = 0;
}

管理机制

  • 接口级控制:每个接口独立管理ARP启用/禁用状态
  • Feature Arc切换:通过启用/禁用Feature节点实现功能切换
  • 状态存储 :状态存储在ethernet_arp_by_sw_if_index数组中
6.5.2 接口创建时的默认行为

源码位置src/vnet/arp/arp.c

c 复制代码
static clib_error_t *
vnet_arp_add_del_sw_interface (vnet_main_t * vnm, u32 sw_if_index, u32 is_add)
{
  ethernet_arp_main_t *am = &ethernet_arp_main;
  if (is_add)
    arp_disable (am, sw_if_index);  // 接口创建时默认禁用ARP
  return (NULL);
}

设计原理

  • 默认禁用:新接口创建时,ARP默认禁用
  • 安全考虑:避免未配置的接口响应ARP请求
  • 按需启用:需要时通过IP4接口回调自动启用
6.5.3 IP4接口回调机制

源码位置src/vnet/arp/arp.c

c 复制代码
static void
arp_enable_disable_interface (ip4_main_t * im,
			      uword opaque, u32 sw_if_index, u32 is_enable)
{
  ethernet_arp_main_t *am = &ethernet_arp_main;

  if (is_enable)
    arp_enable (am, sw_if_index);   // IP4接口启用时,自动启用ARP
  else
    arp_disable (am, sw_if_index);  // IP4接口禁用时,自动禁用ARP
}

自动管理机制

  • 回调注册:在初始化时注册IP4接口启用/禁用回调
  • 自动同步:IP4接口状态变化时,自动同步ARP状态
  • 简化配置:用户无需手动配置ARP,系统自动管理

运行流程

  1. IP4接口启用 → 触发回调 → 自动启用ARP
  2. IP4接口禁用 → 触发回调 → 自动禁用ARP
  3. 接口删除 → 自动禁用ARP

6.6 关键处理逻辑

6.6.1 FIB查找
c 复制代码
dst_fei = ip4_fib_table_lookup (ip4_fib_get (fib_index0),
                                &arp0->ip4_over_ethernet[1].ip4, 32);
conn_sw_if_index0 = fib_entry_get_any_resolving_interface (dst_fei);

说明

  • 在FIB中查找目标IP地址(ip4_over_ethernet[1].ip4
  • 获取解析接口
6.6.2 本地地址检查
c 复制代码
dst_flags = fib_entry_get_flags_for_source (dst_fei, FIB_SOURCE_ADJ);
dst_is_local0 = (arp_dst_fib_check (dst_fei, &dst_flags) != ARP_DST_FIB_NONE);

说明

  • 检查目标IP是否是本地地址
  • 通过FIB条目标志判断
6.6.3 生成ARP响应
c 复制代码
if_addr0 = ip4_interface_first_address (sw_if_index0, fib_index0);
next0 = arp_mk_reply (vnm, p0, sw_if_index0, if_addr0, arp0, eth_rx);

说明

  • 获取接口的第一个IP地址
  • 调用arp_mk_reply生成ARP响应

第七章:ARP响应处理流程

章节意义

本章介绍ARP响应的生成和处理流程。当ARP模块决定响应ARP请求时,需要构造ARP响应数据包。本章详细说明了响应生成的每个步骤,包括VLAN标签的处理、以太网头的构建、地址交换等。理解响应生成流程有助于理解ARP协议的具体实现细节。

7.1 ARP响应生成函数

文件位置src/vnet/arp/arp_packet.h

作用和实现原理

arp_mk_reply是ARP响应生成的核心函数,负责将接收到的ARP请求转换为ARP响应。它通过修改原始ARP请求数据包来生成响应,避免重新分配内存,提高性能。

设计原理

  • 原地修改:直接在接收到的数据包上修改,不分配新缓冲区
  • 重写字符串:使用以太网重写字符串机制构建完整的以太网头(包括VLAN标签等)
  • 地址交换:将请求的发送者信息复制到响应的目标位置,填充响应者信息
  • 接口复用:响应通过接收接口发送回去

处理流程

  1. 构建以太网重写字符串(包含目标MAC、VLAN等)
  2. 调整缓冲区位置,为以太网头留出空间
  3. 修改ARP操作码为响应
  4. 交换发送者和目标地址
  5. 填充响应者的IP和MAC地址
  6. 复制以太网头到缓冲区

源码实现

c 复制代码
static_always_inline u32
arp_mk_reply (vnet_main_t * vnm,
              vlib_buffer_t * p0,
              u32 sw_if_index0,
              const ip4_address_t * if_addr0,
              ethernet_arp_header_t * arp0, ethernet_header_t * eth_rx)
{
  vnet_hw_interface_t *hw_if0;
  u8 *rewrite0, rewrite0_len;
  ethernet_header_t *eth_tx;
  u32 next0;

  /* 步骤1:构建以太网重写字符串
   * 说明:由于发送者可能没有邻接表项,我们使用接口来构建重写字符串
   * 重写字符串包含:目标MAC地址、VLAN标签等所有必要的标签 */
  rewrite0 = ethernet_build_rewrite (vnm, sw_if_index0,
                                     VNET_LINK_ARP, eth_rx->src_address);
  rewrite0_len = vec_len (rewrite0);

  /* 步骤2:调整缓冲区位置
   * 说明:将缓冲区指针向前移动,为以太网头留出空间
   * 负数表示向前移动(向缓冲区开始方向) */
  vlib_buffer_advance (p0, -rewrite0_len);
  eth_tx = vlib_buffer_get_current (p0);

  /* 步骤3:设置发送接口和获取硬件接口信息 */
  vnet_buffer (p0)->sw_if_index[VLIB_TX] = sw_if_index0;
  hw_if0 = vnet_get_sup_hw_interface (vnm, sw_if_index0);

  /* 步骤4:设置下一跳节点(发送到interface-output) */
  next0 = ARP_REPLY_NEXT_REPLY_TX;

  /* 步骤5:修改ARP操作码为响应 */
  arp0->opcode = clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_reply);

  /* 步骤6:交换发送者和目标地址
   * 说明:将原请求的发送者([0])复制到响应的目标([1])位置 */
  arp0->ip4_over_ethernet[1] = arp0->ip4_over_ethernet[0];

  /* 步骤7:填充响应者信息(发送者位置[0])
   * 说明:填充响应者的MAC地址(接口的硬件地址)和IP地址(接口的IP地址) */
  mac_address_from_bytes (&arp0->ip4_over_ethernet[0].mac,
                          hw_if0->hw_address);
  clib_mem_unaligned (&arp0->ip4_over_ethernet[0].ip4.data_u32, u32) =
    if_addr0->data_u32;

  /* 步骤8:标记数据包为本地生成
   * 说明:这个标志用于统计和调试,表示数据包是VPP本地生成的 */
  p0->flags |= VNET_BUFFER_F_LOCALLY_ORIGINATED;

  /* 步骤9:断言硬件地址长度为6字节(以太网标准) */
  ASSERT (vec_len (hw_if0->hw_address) == 6);

  /* 步骤10:复制以太网头到缓冲区
   * 说明:将构建的重写字符串(包含完整的以太网头)复制到数据包缓冲区
   * 注意:在某些情况下(如收到VLAN=0的tagged包但发送untagged),
   * 接收和发送的以太网头可能会重叠 */
  clib_memcpy_fast (eth_tx, rewrite0, vec_len (rewrite0));
  vec_free (rewrite0);

  return (next0);
}

关键实现细节

  1. 重写字符串机制

    • ethernet_build_rewrite构建完整的以太网头,包括:
      • 目标MAC地址(请求者的源MAC)
      • 源MAC地址(接口的MAC)
      • 以太网类型(0x0806,ARP)
      • VLAN标签(如果接口配置了VLAN)
    • 长度可能因VLAN配置而变化
  2. 缓冲区位置调整

    • vlib_buffer_advance(p0, -rewrite0_len)向前移动指针
    • 为以太网头留出空间,因为原始数据包可能没有以太网头(已被剥离)
  3. 地址交换逻辑

    • 请求格式:发送者[0] = 请求者IP+MAC,目标[1] = 被查询IP+0
    • 响应格式:发送者[0] = 响应者IP+MAC,目标[1] = 请求者IP+MAC
    • 通过复制[0][1],然后填充新的[0]实现交换
  4. 性能优化

    • 使用clib_memcpy_fast快速内存复制
    • 使用static_always_inline内联函数,减少函数调用开销
    • 原地修改数据包,避免内存分配

7.2 ARP的VLAN处理机制

问题:ARP的VLAN是什么,VPP是怎么实现区分VLAN的?

解答

VPP通过**以太网重写字符串(rewrite string)**机制处理VLAN标签,确保ARP响应包包含正确的VLAN信息。

7.2.1 VLAN标签的作用

**VLAN(Virtual LAN)**是IEEE 802.1Q标准定义的虚拟局域网技术:

  • 网络隔离:在同一物理网络上创建多个逻辑网络
  • 标签标识:通过VLAN ID(12位)标识不同的虚拟网络
  • 标签位置:位于以太网头和IP头之间
7.2.2 VPP的VLAN处理实现

源码位置src/vnet/ethernet/interface.c

c 复制代码
u8 *
ethernet_build_rewrite (vnet_main_t * vnm, u32 sw_if_index,
			vnet_link_t link_type, const void *dst_address)
{
  vnet_sw_interface_t *sub_sw = vnet_get_sw_interface (vnm, sw_if_index);
  vnet_sw_interface_t *sup_sw = vnet_get_sup_hw_interface (vnm, sw_if_index);
  uword n_bytes = sizeof (ethernet_header_t);
  u8 *rewrite = NULL;

  // 检查是否是子接口(sub-interface),子接口可能有VLAN标签
  if (sub_sw != sup_sw)
    {
      // 单标签VLAN(802.1Q)
      if (sub_sw->sub.eth.flags.one_tag)
	{
	  n_bytes += sizeof (ethernet_vlan_header_t);
	}
      // 双标签VLAN(Q-in-Q,802.1ad)
      else if (sub_sw->sub.eth.flags.two_tags)
	{
	  n_bytes += 2 * (sizeof (ethernet_vlan_header_t));
	}
    }

  // 分配重写字符串空间
  vec_validate (rewrite, n_bytes - 1);
  h = (ethernet_header_t *) rewrite;

  // 填充以太网头
  clib_memcpy (h->src_address, &ei->address, sizeof (h->src_address));
  if (dst_address)
    clib_memcpy (h->dst_address, dst_address, sizeof (h->dst_address));

  // 构建单标签VLAN头
  if (sub_sw->sub.eth.flags.one_tag)
    {
      ethernet_vlan_header_t *outer = (void *) (h + 1);
      
      // 设置以太网类型为VLAN(0x8100)或Q-in-Q(0x88A8)
      h->type = sub_sw->sub.eth.flags.dot1ad ?
	clib_host_to_net_u16 (ETHERNET_TYPE_DOT1AD) :
	clib_host_to_net_u16 (ETHERNET_TYPE_VLAN);
      
      // 填充VLAN ID和优先级
      outer->priority_cfi_and_id =
	clib_host_to_net_u16 (sub_sw->sub.eth.outer_vlan_id);
      
      // VLAN头后的协议类型(ARP为0x0806)
      outer->type = clib_host_to_net_u16 (type);
    }
  
  // 构建双标签VLAN头(Q-in-Q)
  else if (sub_sw->sub.eth.flags.two_tags)
    {
      ethernet_vlan_header_t *outer = (void *) (h + 1);
      ethernet_vlan_header_t *inner = (void *) (outer + 1);

      h->type = sub_sw->sub.eth.flags.dot1ad ?
	clib_host_to_net_u16 (ETHERNET_TYPE_DOT1AD) :
	clib_host_to_net_u16 (ETHERNET_TYPE_VLAN);
      
      // 外层VLAN标签
      outer->priority_cfi_and_id =
	clib_host_to_net_u16 (sub_sw->sub.eth.outer_vlan_id);
      outer->type = clib_host_to_net_u16 (ETHERNET_TYPE_VLAN);
      
      // 内层VLAN标签
      inner->priority_cfi_and_id =
	clib_host_to_net_u16 (sub_sw->sub.eth.inner_vlan_id);
      inner->type = clib_host_to_net_u16 (type);
    }
  else
    {
      // 无VLAN标签,直接设置协议类型
      h->type = clib_host_to_net_u16 (type);
    }

  return (rewrite);
}
7.2.3 ARP响应中的VLAN处理

arp_mk_reply函数中,VLAN处理通过ethernet_build_rewrite自动完成:

c 复制代码
// 构建包含VLAN标签的以太网重写字符串
rewrite0 = ethernet_build_rewrite (vnm, sw_if_index0,
                                   VNET_LINK_ARP, eth_rx->src_address);
rewrite0_len = vec_len (rewrite0);

// 调整缓冲区位置,为VLAN头留出空间
vlib_buffer_advance (p0, -rewrite0_len);

// 复制包含VLAN标签的完整以太网头
clib_memcpy_fast (eth_tx, rewrite0, vec_len (rewrite0));

处理流程

  1. 检查接口类型 :判断接口是否有VLAN配置(one_tagtwo_tags
  2. 计算头部长度:根据VLAN配置计算以太网头长度
  3. 构建VLAN标签:如果接口配置了VLAN,在重写字符串中包含VLAN头
  4. 复制到数据包:将完整的以太网头(含VLAN)复制到ARP响应包

VLAN区分机制

  • 接口级配置 :每个接口的VLAN配置存储在vnet_sw_interface_t结构中
  • 自动识别ethernet_build_rewrite根据接口配置自动添加VLAN标签
  • 透明处理:ARP模块无需关心VLAN细节,由以太网层统一处理

关键点

  • 接收VLAN:ARP请求包可能包含VLAN标签,VPP在接收时已剥离
  • 发送VLAN:ARP响应包需要根据接口配置添加VLAN标签
  • 标签匹配:响应包的VLAN ID必须与请求包的VLAN ID匹配

7.3 ARP响应生成步骤

步骤1:构建以太网重写字符串
c 复制代码
rewrite0 = ethernet_build_rewrite (vnm, sw_if_index0,
                                   VNET_LINK_ARP, eth_rx->src_address);
rewrite0_len = vec_len (rewrite0);

说明

  • 构建以太网重写字符串
  • 包含目标MAC地址(请求者的源MAC)
  • 包含VLAN标签等
步骤2:调整缓冲区位置
c 复制代码
vlib_buffer_advance (p0, -rewrite0_len);
eth_tx = vlib_buffer_get_current (p0);

说明

  • 将缓冲区指针向前移动,为以太网头留出空间
  • 获取新的以太网头位置
步骤3:设置发送接口
c 复制代码
vnet_buffer (p0)->sw_if_index[VLIB_TX] = sw_if_index0;
hw_if0 = vnet_get_sup_hw_interface (vnm, sw_if_index0);

说明

  • 设置发送接口索引
  • 获取硬件接口信息
步骤4:修改ARP操作码
c 复制代码
arp0->opcode = clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_reply);

说明

  • 将操作码从请求改为响应
步骤5:交换发送者和目标
c 复制代码
arp0->ip4_over_ethernet[1] = arp0->ip4_over_ethernet[0];

说明

  • 将原请求的发送者([0])复制到响应的目标([1]
步骤6:填充响应者信息
c 复制代码
mac_address_from_bytes (&arp0->ip4_over_ethernet[0].mac,
                        hw_if0->hw_address);
clib_mem_unaligned (&arp0->ip4_over_ethernet[0].ip4.data_u32, u32) =
  if_addr0->data_u32;

说明

  • 填充响应者的MAC地址(接口的硬件地址)
  • 填充响应者的IP地址(接口的IP地址)
步骤7:复制以太网头
c 复制代码
clib_memcpy_fast (eth_tx, rewrite0, vec_len (rewrite0));
vec_free (rewrite0);

说明

  • 将构建的以太网重写字符串复制到数据包
  • 释放重写字符串

7.3 ARP响应接收处理

src/vnet/arp/arp.c

c 复制代码
/* Learn or update sender's mapping only for replies to addresses
 * that are local to the subnet */
if (arp0->opcode ==
    clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_reply))
  {
    if (dst_is_local0)
      error0 =
        arp_learn (sw_if_index0, &arp0->ip4_over_ethernet[0]);
    else
      /* a reply for a non-local destination could be a GARP.
       * GARPs for hosts we know were handled above, so this one
       * we drop */
      error0 = ARP_ERROR_L3_DST_ADDRESS_NOT_LOCAL;

    goto next_feature;
  }

说明

  • 如果收到ARP响应,且目标是本地地址
  • 学习发送者的IP到MAC映射
  • 如果目标不是本地地址,可能是GARP(免费ARP),丢弃

第八章:ARP代理功能

章节意义

本章介绍ARP代理功能。ARP代理允许VPP为不在本地子网的IP地址响应ARP请求,这在某些网络场景(如路由器、负载均衡、网络虚拟化)中非常有用。理解ARP代理的实现有助于理解VPP如何扩展ARP协议的功能。

8.1 ARP代理概述

ARP代理允许VPP为不在本地子网的IP地址响应ARP请求。这在某些网络场景中很有用,例如:

  • 路由器的代理ARP
  • 负载均衡场景
  • 网络虚拟化场景

8.2 ARP代理节点

文件位置src/vnet/arp/arp_proxy.c

c 复制代码
static uword
arp_proxy (vlib_main_t * vm, vlib_node_runtime_t * node, vlib_frame_t * frame)
{
  arp_proxy_main_t *am = &arp_proxy_main;
  vnet_main_t *vnm = vnet_get_main ();
  u32 n_left_from, next_index, *from, *to_next;
  u32 n_arp_replies_sent = 0;

  from = vlib_frame_vector_args (frame);
  n_left_from = frame->n_vectors;
  next_index = node->cached_next_index;

  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)
        {
          vlib_buffer_t *p0;
          ethernet_arp_header_t *arp0;
          ethernet_header_t *eth_rx;
          ip4_address_t proxy_src;
          u32 pi0, error0, next0, sw_if_index0, fib_index0;
          u8 is_request0;
          ethernet_proxy_arp_t *pa;

          pi0 = from[0];
          to_next[0] = pi0;
          from += 1;
          to_next += 1;
          n_left_from -= 1;
          n_left_to_next -= 1;

          p0 = vlib_get_buffer (vm, pi0);
          arp0 = vlib_buffer_get_current (p0);
          eth_rx = ethernet_buffer_get_header (p0);

          is_request0 = arp0->opcode
            == clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_request);

          error0 = ARP_ERROR_REPLIES_SENT;
          sw_if_index0 = vnet_buffer (p0)->sw_if_index[VLIB_RX];
          next0 = ARP_REPLY_NEXT_DROP;

          fib_index0 = ip4_fib_table_get_index_for_sw_if_index (sw_if_index0);
          if (~0 == fib_index0)
            {
              error0 = ARP_ERROR_INTERFACE_NO_TABLE;
            }

          if (0 == error0 && is_request0)
            {
              u32 this_addr = clib_net_to_host_u32
                (arp0->ip4_over_ethernet[1].ip4.as_u32);

              vec_foreach (pa, am->proxy_arps)
                {
                  u32 lo_addr = clib_net_to_host_u32 (pa->lo_addr.as_u32);
                  u32 hi_addr = clib_net_to_host_u32 (pa->hi_addr.as_u32);

                  /* an ARP request hit in the proxy-arp table? */
                  if ((this_addr >= lo_addr && this_addr <= hi_addr) &&
                      (fib_index0 == pa->fib_index))
                    {
                      proxy_src.as_u32 =
                        arp0->ip4_over_ethernet[1].ip4.data_u32;

                      /*
                       * change the interface address to the proxied
                       */
                      n_arp_replies_sent++;

                      next0 =
                        arp_mk_reply (vnm, p0, sw_if_index0, &proxy_src, arp0,
                                      eth_rx);
                    }
                }
            }
          else
            {
              p0->error = node->errors[error0];
            }

          vlib_validate_buffer_enqueue_x1 (vm, node, next_index, to_next,
                                           n_left_to_next, pi0, next0);
        }

      vlib_put_next_frame (vm, node, next_index, n_left_to_next);
    }

  vlib_error_count (vm, node->node_index, ARP_ERROR_REPLIES_SENT,
                    n_arp_replies_sent);

  return frame->n_vectors;
}

8.3 ARP代理处理逻辑

步骤1:检查是否是ARP请求
c 复制代码
is_request0 = arp0->opcode
  == clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_request);

说明:只处理ARP请求

步骤2:查找代理ARP表
c 复制代码
u32 this_addr = clib_net_to_host_u32
  (arp0->ip4_over_ethernet[1].ip4.as_u32);

vec_foreach (pa, am->proxy_arps)
  {
    u32 lo_addr = clib_net_to_host_u32 (pa->lo_addr.as_u32);
    u32 hi_addr = clib_net_to_host_u32 (pa->hi_addr.as_u32);

    /* an ARP request hit in the proxy-arp table? */
    if ((this_addr >= lo_addr && this_addr <= hi_addr) &&
        (fib_index0 == pa->fib_index))
      {
        // 找到匹配的代理ARP条目
      }
  }

说明

  • 遍历代理ARP表
  • 检查请求的IP地址是否在代理范围内(lo_addrhi_addr
  • 检查FIB索引是否匹配
步骤3:生成代理响应
c 复制代码
proxy_src.as_u32 = arp0->ip4_over_ethernet[1].ip4.data_u32;

next0 = arp_mk_reply (vnm, p0, sw_if_index0, &proxy_src, arp0, eth_rx);

说明

  • 使用请求的目标IP作为代理源IP
  • 调用arp_mk_reply生成响应

8.4 ARP代理API

src/vnet/arp/arp.h

c 复制代码
extern int arp_proxy_add (u32 fib_index,
                          const ip4_address_t * lo_addr,
                          const ip4_address_t * hi_addr);
extern int arp_proxy_del (u32 fib_index,
                          const ip4_address_t * lo_addr,
                          const ip4_address_t * hi_addr);

extern int arp_proxy_enable (u32 sw_if_index);
extern int arp_proxy_disable (u32 sw_if_index);

说明

  • arp_proxy_add:添加代理ARP范围
  • arp_proxy_del:删除代理ARP范围
  • arp_proxy_enable:在接口上启用代理ARP
  • arp_proxy_disable:在接口上禁用代理ARP

第九章:ARP主动学习和探测机制

章节意义

本章介绍ARP的主动学习和探测机制。除了被动学习(从接收到的ARP包中学习),VPP还支持主动探测功能,可以在需要时主动发送ARP请求来解析IP地址。理解主动学习机制有助于理解VPP如何优化网络性能,减少数据包丢失。

9.0 ARP主动学习概述

主动学习 vs 被动学习

  • 被动学习:从接收到的ARP请求/响应中学习IP到MAC映射(见第十章)
  • 主动学习:主动发送ARP请求来解析IP地址,无需等待其他主机发送ARP包

主动学习的优势

  • 提前解析:在需要转发数据包之前就解析好MAC地址
  • 减少丢包:避免数据包因为缺少MAC地址而被丢弃
  • 性能优化:减少数据包在incomplete邻接表项上的等待时间

9.1 ARP主动探测的触发场景

VPP的ARP主动探测在以下场景自动触发:

9.1.1 场景1:FIB添加新路由时的推测性ARP(Speculative ARP)

源码位置src/vnet/ip-neighbor/ip_neighbor.c

触发时机:当FIB添加新的邻接表项(adjacency)但没有对应的ARP条目时

c 复制代码
void
ip_neighbor_update (vnet_main_t * vnm, adj_index_t ai)
{
  ip_neighbor_t *ipn;
  ip_adjacency_t *adj = adj_get (ai);

  // 查找是否已有ARP条目
  ipn = ip_neighbor_db_find (&key);

  switch (adj->lookup_next_index)
    {
    case IP_LOOKUP_NEXT_ARP:
      if (NULL != ipn)
        {
          // 已有ARP条目,更新邻接表项
          adj_nbr_walk_nh (...);
        }
      else
        {
          // 没有ARP条目,设置incomplete邻接表项
          adj_nbr_update_rewrite (ai, ADJ_NBR_REWRITE_FLAG_INCOMPLETE, ...);

          /*
           * 发送推测性ARP(Speculative ARP)
           * 注释说明:由于FIB添加了这个邻接表项用于路由,
           * 可能很快就会有流量需要转发。让我们发送一个推测性ARP。
           * 只发送一个。如果要定期发送,需要更多代码,但收益相对较小。
           */
          adj = adj_get (ai);
          ip_neighbor_probe (adj);  // 主动发送ARP请求
        }
      break;
    }
}

设计原理

  • 提前解析:在数据包到达之前就尝试解析MAC地址
  • 一次性探测:只发送一次,不进行周期性重试(注释中说明)
  • 性能优化:减少首次数据包的延迟
9.1.2 场景2:数据包转发时的ARP解析

源码位置src/vnet/ip-neighbor/ip4_neighbor.c

触发时机:当数据包需要转发,但邻接表项是incomplete状态时

c 复制代码
always_inline uword
ip4_arp_inline (vlib_main_t * vm,
                vlib_node_runtime_t * node,
                vlib_frame_t * frame, int is_glean)
{
  // 处理需要ARP解析的数据包
  while (n_left_from > 0 && n_left_to_next > 0)
    {
      ip_adjacency_t *adj0 = adj_get (adj_index0);
      
      // 检查邻接表项是否是ARP类型
      if (adj0->lookup_next_index == IP_LOOKUP_NEXT_ARP)
        {
          // 获取源IP和目标IP
          ip4_address_t src0, resolve0;
          
          // 发送ARP请求
          b0 = ip4_neighbor_probe (vm, vnm, adj0, &src0, &resolve0);
          
          if (PREDICT_TRUE (NULL != b0))
            {
              p0->error = node->errors[IP4_NEIGHBOR_ERROR_REQUEST_SENT];
            }
        }
    }
}

处理流程

  1. 数据包到达ip4-arp节点
  2. 检查邻接表项状态(incomplete)
  3. 发送ARP请求探测
  4. 数据包暂时丢弃(等待ARP响应)

节点说明

  • ip4-arp节点:处理需要ARP解析的数据包
  • ip4-glean节点:处理glean类型的邻接表项(子网路由)
9.1.3 场景3:邻居老化时的探测

源码位置src/vnet/ip-neighbor/ip_neighbor.c

触发时机:当邻居条目超过老化时间时,发送探测验证是否仍然活跃

c 复制代码
static ip_neighbor_age_state_t
ip_neighbour_age_out (index_t ipni, f64 now, f64 * wait)
{
  ip_neighbor_t *ipn = ip_neighbor_get (ipni);
  u32 ttl = now - ipn->ipn_time_last_updated;
  
  if (ttl > ipndb_age)  // 超过老化时间
    {
      if (ipn->ipn_n_probes > 2)
        {
          // 已探测3次,标记为死亡
          return (IP_NEIGHBOR_AGE_DEAD);
        }
      else
        {
          // 发送探测请求
          ip_neighbor_probe_dst (ip_neighbor_get_sw_if_index (ipn),
                                 vlib_get_thread_index (), af,
                                 &ip_addr_46 (&ipn->ipn_key->ipnk_ip));
          
          ipn->ipn_n_probes++;  // 增加探测计数
          *wait = 1;
        }
    }
  
  return (IP_NEIGHBOR_AGE_PROBE);
}

老化机制

  • 老化时间 :由ip_neighbor_db[af].ipndb_age配置
  • 探测次数 :最多探测3次(ipn_n_probes > 2
  • 三次失败:如果3次探测都失败,删除邻居条目

9.2 ARP探测函数实现

9.2.1 ip4_neighbor_probe函数

源码位置src/vnet/ip-neighbor/ip4_neighbor.h

作用和实现原理

ip4_neighbor_probe是ARP主动探测的核心函数,负责构造并发送ARP请求包。

c 复制代码
always_inline vlib_buffer_t *
ip4_neighbor_probe (vlib_main_t *vm, vnet_main_t *vnm,
                    const ip_adjacency_t *adj0, const ip4_address_t *src,
                    const ip4_address_t *dst)
{
  vnet_hw_interface_t *hw_if0;
  ethernet_arp_header_t *h0;
  vlib_buffer_t *b0;
  u32 bi0;

  // 步骤1:获取硬件接口信息
  hw_if0 = vnet_get_sup_hw_interface (vnm, adj0->rewrite_header.sw_if_index);

  // 步骤2:从数据包模板获取ARP包
  // 说明:使用预定义的ARP请求包模板,提高性能
  h0 = vlib_packet_template_get_packet (vm,
                                        &ip4_main.ip4_arp_request_packet_template,
                                        &bi0);
  if (PREDICT_FALSE (!h0))
    return (NULL);  // 缓冲区不足

  b0 = vlib_get_buffer (vm, bi0);

  // 步骤3:添加以太网重写字符串(包含VLAN等)
  vnet_rewrite_one_header (adj0[0], h0, sizeof (ethernet_header_t));

  // 步骤4:填充ARP请求包
  // 发送者MAC地址(接口的硬件地址)
  mac_address_from_bytes (&h0->ip4_over_ethernet[0].mac, hw_if0->hw_address);
  // 发送者IP地址
  h0->ip4_over_ethernet[0].ip4 = *src;
  // 目标IP地址(要查询的IP)
  h0->ip4_over_ethernet[1].ip4 = *dst;

  // 步骤5:设置发送接口
  vnet_buffer (b0)->sw_if_index[VLIB_TX] = adj0->rewrite_header.sw_if_index;
  b0->flags |= VNET_BUFFER_F_LOCALLY_ORIGINATED;

  // 步骤6:调整缓冲区位置
  vlib_buffer_advance (b0, -adj0->rewrite_header.data_bytes);

  // 步骤7:发送到接口输出节点
  {
    vlib_frame_t *f = vlib_get_frame_to_node (vm, hw_if0->output_node_index);
    u32 *to_next = vlib_frame_vector_args (f);
    to_next[0] = bi0;
    f->n_vectors = 1;
    vlib_put_frame_to_node (vm, hw_if0->output_node_index, f);
  }

  // 步骤8:更新统计计数器
  vlib_increment_simple_counter (
    &ip_neighbor_counters[AF_IP4].ipnc[VLIB_TX][IP_NEIGHBOR_CTR_REQUEST],
    vm->thread_index, adj0->rewrite_header.sw_if_index, 1);

  return b0;
}

关键实现细节

  • 数据包模板:使用预定义的ARP请求包模板,避免每次构造
  • VLAN支持 :通过vnet_rewrite_one_header自动处理VLAN标签
  • 直接发送:不经过ARP Feature Arc,直接发送到接口输出节点
9.2.2 ip4_neighbor_probe_dst函数

源码位置src/vnet/ip-neighbor/ip4_neighbor.c

作用和实现原理

ip4_neighbor_probe_dst是面向用户的探测接口,自动获取源IP地址。

c 复制代码
void
ip4_neighbor_probe_dst (u32 sw_if_index, u32 thread_index,
                        const ip4_address_t *dst)
{
  ip4_address_t src;
  adj_index_t ai;

  // 步骤1:获取glean邻接表项(用于重写字符串)
  ai = adj_glean_get (FIB_PROTOCOL_IP4, sw_if_index, NULL);

  if (ADJ_INDEX_INVALID != ai &&
      // 步骤2:获取源IP地址
      (fib_sas4_get (sw_if_index, dst, &src) ||
       ip4_sas_by_sw_if_index (sw_if_index, dst, &src)))
    {
      // 步骤3:调用核心探测函数
      ip4_neighbor_probe (vlib_get_main (),
                          vnet_get_main (), adj_get (ai), &src, dst);
    }
}

设计原理

  • 自动源IP选择 :通过fib_sas4_getip4_sas_by_sw_if_index自动选择源IP
  • 简化接口:用户只需指定目标IP和接口,无需关心源IP

9.3 手动触发ARP探测

9.3.1 arping插件

VPP提供了arping插件,允许用户手动发送ARP探测请求。

插件位置src/plugins/arping/

CLI命令

bash 复制代码
# 发送ARP请求
arping <IP地址> <接口> [repeat <次数>] [interval <间隔秒数>]

# 发送免费ARP(Gratuitous ARP)
arping gratuitous <IP地址> <接口> [repeat <次数>] [interval <间隔秒数>]

使用示例

bash 复制代码
# 向100.1.1.10发送3次ARP请求,间隔1秒
arping 100.1.1.10 VirtualEthernet0/0/0 repeat 3 interval 1

# 发送免费ARP
arping gratuitous 100.1.1.100 VirtualEthernet0/0/0 repeat 2

源码实现src/plugins/arping/arping.c

c 复制代码
static clib_error_t *
arping_ip_address (vlib_main_t *vm, unformat_input_t *input,
                   vlib_cli_command_t *cmd)
{
  arping_args_t args = { 0 };
  
  // 解析参数
  if (unformat (input, "%U", unformat_ip4_address, &args.address.ip.ip4))
    args.address.version = AF_IP4;
  
  // 解析接口
  unformat_user (input, unformat_vnet_sw_interface, vnm, &args.sw_if_index);
  
  // 解析repeat和interval参数
  while (!unformat_eof (input, NULL))
    {
      if (unformat (input, "interval"))
        unformat (input, "%f", &args.interval);
      else if (unformat (input, "repeat"))
        unformat (input, "%u", &args.repeat);
    }
  
  // 执行探测
  arping_run_command (vm, &args);
}

功能特点

  • 支持IPv4和IPv6:可以发送ARP请求或ND请求
  • 可配置次数和间隔:支持重复发送和自定义间隔
  • 统计响应:统计收到的响应数量
9.3.2 API接口

API定义src/plugins/arping/arping.api

c 复制代码
define arping
{
  u32 client_index;
  u32 context;
  vl_api_address_t address;
  vl_api_interface_index_t sw_if_index;
  bool is_garp;
  u32 repeat [default=1];
  f64 interval [default=1.0];
};

使用方式 :通过VPP API客户端调用arping消息

9.4 主动学习的配置和开启

9.4.1 自动探测的开启

默认状态 :VPP的主动探测功能默认开启,无需额外配置。

触发条件

  1. FIB添加新路由时自动发送推测性ARP
  2. 数据包需要转发时自动发送ARP请求
  3. 邻居老化时自动发送探测
9.4.2 邻居老化配置

配置命令

bash 复制代码
# 设置IPv4邻居老化时间(秒)
set ip neighbor age <秒数>

# 查看当前配置
show ip neighbor config

源码位置src/vnet/ip-neighbor/ip_neighbor.c

c 复制代码
// 默认配置
static ip_neighbor_db_t ip_neighbor_db[N_AF] = {
  [AF_IP4] = {
    .ipndb_limit = 50000,      // 最大邻居数量
    .ipndb_age = 0,            // 老化时间(0表示不老化)
    .ipndb_recycle = false,    // 是否回收旧条目
  },
};

说明

  • 默认不老化ipndb_age = 0表示默认不启用老化
  • 启用老化 :设置ipndb_age > 0后,超过该时间的邻居条目会被探测
  • 探测次数:最多探测3次,3次失败后删除条目
9.4.3 手动探测的使用

使用arping插件

  1. 确保插件已加载

    bash 复制代码
    # 检查插件是否加载
    show plugins
  2. 发送ARP探测

    bash 复制代码
    # 基本用法
    arping <IP地址> <接口>
    
    # 完整示例
    arping 192.168.1.1 GigabitEthernet0/0/0 repeat 5 interval 0.5
  3. 发送免费ARP

    bash 复制代码
    arping gratuitous 192.168.1.100 GigabitEthernet0/0/0

9.5 主动学习机制总结

主动学习的三种方式

  1. 推测性ARP

    • 触发:FIB添加新路由时
    • 目的:提前解析MAC地址
    • 特点:只发送一次
  2. 数据包触发ARP

    • 触发:数据包需要转发但缺少MAC地址
    • 目的:解析MAC地址以转发数据包
    • 特点:数据包暂时丢弃,等待ARP响应
  3. 老化探测

    • 触发:邻居条目超过老化时间
    • 目的:验证邻居是否仍然活跃
    • 特点:最多探测3次

手动探测

  • 工具:arping插件
  • 用途:网络诊断、地址冲突检测
  • 方式:CLI命令或API调用


第十章:ARP被动学习机制

章节意义

本章介绍ARP被动学习机制,这是ARP模块的另一个核心功能。当VPP收到ARP请求或响应时,需要学习IP到MAC的映射关系,并将这些关系存储到IP邻居表中供转发使用。本章详细说明了学习机制的实现细节,包括数据平面到控制平面的RPC调用、哈希表操作等。

10.1 ARP被动学习函数

文件位置src/vnet/arp/arp.c

作用和实现原理

arp_learn是ARP学习的核心函数,负责将从ARP数据包中提取的IP到MAC映射关系学习到IP邻居表中。它不直接管理ARP表,而是将学习任务委托给IP邻居模块,实现统一的邻居发现机制。

设计原理

  • 模块分离:ARP模块只负责解析ARP数据包,学习功能由IP邻居模块统一管理
  • 统一接口:IPv4(ARP)和IPv6(ND)都使用相同的IP邻居接口,简化设计
  • 数据平面学习 :使用ip_neighbor_learn_dp(dp表示data plane),在数据包处理路径中直接学习
  • 返回值语义 :返回错误码ARP_ERROR_L3_SRC_ADDRESS_LEARNED,表示已学习(这不是真正的错误)

学习内容

  • IP地址:从ARP数据包中提取的IP地址
  • MAC地址:从ARP数据包中提取的MAC地址
  • 接口索引:接收ARP数据包的接口

源码实现

c 复制代码
always_inline u32
arp_learn (u32 sw_if_index,
           const ethernet_arp_ip4_over_ethernet_address_t * addr)
{
  // 步骤1:构建学习结构体
  // 说明:ip_neighbor_learn_t是IP邻居模块的学习接口结构
  ip_neighbor_learn_t l = {
    .ip = {
      .ip.ip4 = addr->ip4,        // 要学习的IP地址
      .version = AF_IP4,           // 地址族:IPv4
    },
    .mac = addr->mac,              // 要学习的MAC地址
    .sw_if_index = sw_if_index,    // 接口索引
  };

  // 步骤2:调用IP邻居模块的学习函数
  // 说明:ip_neighbor_learn_dp是数据平面学习函数,在数据包处理路径中调用
  // 该函数会:
  // 1. 查找或创建IP邻居条目
  // 2. 更新MAC地址
  // 3. 更新超时时间
  // 4. 触发相关回调(如更新邻接表)
  ip_neighbor_learn_dp (&l);

  // 步骤3:返回学习完成标志
  // 说明:虽然名为ERROR,但这是正常的学习完成标志
  // 用于区分"已学习"和"需要学习"两种状态
  return (ARP_ERROR_L3_SRC_ADDRESS_LEARNED);
}

关键实现细节

  1. IP邻居模块集成

    • ARP模块不维护自己的ARP表
    • 所有IP到MAC的映射由IP邻居模块统一管理
    • 支持IPv4(ARP)和IPv6(ND)的统一接口
  2. 数据平面学习

    • ip_neighbor_learn_dp是数据平面函数,在数据包处理路径中调用
    • 不需要切换到控制平面,性能高
    • 线程安全,支持多线程并发学习
  3. 学习时机

    • 收到ARP请求并生成响应时学习请求者
    • 收到ARP响应时学习响应者
    • 每次学习都会更新超时时间,保持条目活跃
  4. 返回值语义

    • ARP_ERROR_L3_SRC_ADDRESS_LEARNED不是真正的错误
    • 用于表示"已学习"状态,区别于其他错误码
    • 在某些场景下用于控制流程(如避免重复学习)

10.2 ip_neighbor_learn_dp实现细节

问题:ip_neighbor_learn_dp实现细节是如何学习的?

解答

ip_neighbor_learn_dp实际上是一个RPC桥接函数,它将数据平面的学习请求转发到控制平面(主线程)执行。

10.2.1 数据平面到控制平面的RPC调用

源码位置src/vnet/ip-neighbor/ip_neighbor_dp.c

c 复制代码
void
ip_neighbor_learn_dp (const ip_neighbor_learn_t * l)
{
  // 通过RPC调用主线程的ip_neighbor_learn函数
  vl_api_rpc_call_main_thread (ip_neighbor_learn, (u8 *) l, sizeof (*l));
}

设计原理

  • RPC机制:使用VPP的RPC机制将学习请求发送到主线程
  • 线程安全:避免在worker线程中直接操作共享数据结构
  • 异步处理:学习请求异步执行,不阻塞数据包处理
10.2.2 主线程的学习实现

源码位置src/vnet/ip-neighbor/ip_neighbor.c

c 复制代码
int
ip_neighbor_learn (const ip_address_t * ip,
		   const mac_address_t * mac,
		   u32 sw_if_index,
		   ip_neighbor_flags_t flags, u32 * stats_index)
{
  ip_neighbor_t *ipn;
  const ip_neighbor_key_t key = {
    .ipnk_ip = *ip,
    .ipnk_sw_if_index = sw_if_index,
  };

  // 主线程执行(有断言保证)
  ASSERT (0 == vlib_get_thread_index ());

  // 查找是否已存在该邻居条目
  ipn = ip_neighbor_db_find (&key);

  if (ipn)
    {
      // 更新现有条目
      ip_neighbor_touch (ipn);  // 更新时间戳,清除STALE标志
      
      // 如果MAC地址改变,更新MAC地址
      if (mac_address_cmp (&ipn->ipn_mac, mac))
	mac_address_copy (&ipn->ipn_mac, mac);
    }
  else
    {
      // 创建新条目
      ipn = ip_neighbor_alloc (&key, mac, flags);
      
      if (NULL == ipn)
	return VNET_API_ERROR_LIMIT_EXCEEDED;
    }

  // 更新时间戳
  ip_neighbor_refresh (ipn);

  // 更新邻接表(adjacency table)
  adj_nbr_walk_nh (ipn->ipn_key->ipnk_sw_if_index,
		   fproto, &ip_addr_46 (&ipn->ipn_key->ipnk_ip),
		   ip_neighbor_mk_complete_walk, ipn);

  // 发布学习事件(通知订阅者)
  ip_neighbor_publish (ip_neighbor_get_index (ipn), IP_NEIGHBOR_EVENT_ADDED);

  return 0;
}
10.2.3 哈希表存储机制

源码位置src/vnet/ip-neighbor/ip_neighbor.c

c 复制代码
static void
ip_neighbor_db_add (const ip_neighbor_t * ipn)
{
  ip_address_family_t af;
  u32 sw_if_index;

  af = ip_neighbor_get_af (ipn);
  sw_if_index = ipn->ipn_key->ipnk_sw_if_index;

  // 为接口创建哈希表(如果不存在)
  vec_validate (ip_neighbor_db[af].ipndb_hash, sw_if_index);
  if (!ip_neighbor_db[af].ipndb_hash[sw_if_index])
    ip_neighbor_db[af].ipndb_hash[sw_if_index]
      = hash_create_mem (0, sizeof (ip_neighbor_key_t), sizeof (index_t));

  // 将邻居条目添加到哈希表
  hash_set_mem (ip_neighbor_db[af].ipndb_hash[sw_if_index],
		ipn->ipn_key, ip_neighbor_get_index (ipn));

  ip_neighbor_db[af].ipndb_n_elts++;
}

存储结构

  • 两级哈希:第一级按地址族(IPv4/IPv6)和接口索引,第二级按IP地址
  • 键值结构ip_neighbor_key_t包含IP地址和接口索引
  • :邻居条目的索引(index_t),用于访问ip_neighbor_pool中的实际条目
10.2.4 学习流程总结

完整学习流程

  1. 数据平面触发

    • ARP模块在arp_replyarp_learn中调用ip_neighbor_learn_dp
    • 传递IP地址、MAC地址、接口索引
  2. RPC转发

    • ip_neighbor_learn_dp通过RPC将请求发送到主线程
    • 使用vl_api_rpc_call_main_thread实现异步调用
  3. 主线程处理

    • 主线程执行ip_neighbor_learn
    • 查找或创建邻居条目
    • 更新哈希表
    • 更新邻接表
    • 发布学习事件
  4. 存储结构

    • 邻居条目存储在ip_neighbor_pool
    • 哈希表用于快速查找
    • 链表用于老化管理

关键设计特点

  • 线程安全:所有修改操作在主线程执行
  • 异步处理:不阻塞数据包处理路径
  • 事件通知:支持订阅者监听学习事件
  • 老化机制:支持邻居条目老化

10.3 ARP被动学习时机

时机1:收到ARP请求并生成响应时
c 复制代码
/* We are going to reply to this request, so, in the absence of
   errors, learn the sender */
if (!error0)
  error0 = arp_learn (sw_if_index0, &arp0->ip4_over_ethernet[1]);

说明

  • 当收到ARP请求并准备生成响应时
  • 学习请求者的IP到MAC映射(ip4_over_ethernet[1]是请求者)
时机2:收到ARP响应时
c 复制代码
if (arp0->opcode ==
    clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_reply))
  {
    if (dst_is_local0)
      error0 =
        arp_learn (sw_if_index0, &arp0->ip4_over_ethernet[0]);
  }

说明

  • 当收到ARP响应时
  • 如果目标是本地地址,学习响应者的IP到MAC映射(ip4_over_ethernet[0]是响应者)

10.4 IP邻居模块集成

c 复制代码
#include <vnet/ip-neighbor/ip_neighbor.h>
#include <vnet/ip-neighbor/ip4_neighbor.h>
#include <vnet/ip-neighbor/ip_neighbor_dp.h>

说明

  • ARP学习功能集成到IP邻居模块
  • IP邻居模块统一管理IP到MAC的映射
  • 支持IPv4和IPv6(IPv6使用ND)

第十一章:总结

章节意义

本章总结ARP模块的整体架构、设计特点和处理流程,帮助读者建立对ARP模块的完整认识。通过总结,读者可以理解ARP模块在VPP网络栈中的位置和作用,以及各个组件如何协同工作。

11.1 ARP模块架构总结

模块定位

  • VPP核心网络栈的一部分,不是插件
  • 实现RFC826标准的ARP协议

主要组件

  1. 4个节点:arp-input, arp-reply, arp-proxy, arp-disabled
  2. Feature Arc"arp" feature arc
  3. 数据结构:ethernet_arp_main_t, ethernet_arp_interface_t
  4. 代理功能:arp_proxy_main_t

11.2 线程调度总结

多线程支持

  • ✅ 完全支持多线程(MULTITHREAD属性)
  • ✅ 使用vm->thread_index标识线程
  • ✅ 线程特定的计数器
  • ✅ 无锁设计(只读数据包处理路径)

线程安全

  • 数据包处理路径是只读的
  • 配置修改在控制平面进行(有同步机制)
  • 每个线程有独立的vlib_main_t实例

11.3 处理流程总结

ARP请求处理

  1. arp-input节点接收并验证ARP包
  2. 启动arp Feature Arc
  3. arp-reply节点检查目标是否是本地地址
  4. 如果是,生成ARP响应
  5. 学习请求者的IP到MAC映射

ARP响应处理

  1. arp-input节点接收并验证ARP包
  2. arp-reply节点检查目标是否是本地地址
  3. 如果是,学习响应者的IP到MAC映射

ARP代理处理

  1. arp-proxy节点检查请求的IP是否在代理范围内
  2. 如果是,生成代理ARP响应

11.4 关键设计特点

  1. Feature Arc机制:灵活的功能启用/禁用
  2. FIB集成:与FIB模块紧密集成,用于判断本地地址
  3. IP邻居集成:与IP邻居模块集成,统一管理IP到MAC映射
  4. 多线程优化:无锁设计,高性能处理

11.5 源码文件清单

核心文件

  • src/vnet/arp/arp.c:主要ARP处理逻辑
  • src/vnet/arp/arp_proxy.c:ARP代理功能
  • src/vnet/arp/arp_api.c:API接口
  • src/vnet/arp/arp.h:头文件
  • src/vnet/arp/arp_packet.h:ARP数据包结构
  • src/vnet/ethernet/arp_packet.h:ARP数据包定义

相关模块

  • src/vnet/ip-neighbor/:IP邻居模块(ARP学习)
  • src/vnet/fib/:FIB模块(本地地址判断)

文档说明

本文档基于VPP源码(src/vnet/arp/目录)进行100%源码证据支持的分析。所有代码片段、数据结构、函数调用都来自实际源码,确保准确性和可靠性。

关键源码位置

  • 主处理逻辑:src/vnet/arp/arp.c
  • 代理功能:src/vnet/arp/arp_proxy.c
  • 数据包结构:src/vnet/ethernet/arp_packet.h
  • 初始化:src/vnet/arp/arp.c:ethernet_arp_init

版本信息

  • 基于VPP主分支源码
  • 符合RFC826标准
  • 支持多线程处理
相关推荐
中冕—霍格沃兹软件开发测试40 分钟前
测试用例库建设与管理方案
数据库·人工智能·科技·开源·测试用例·bug
The star"'44 分钟前
mysql(4-7)
数据库·mysql·adb
中屹指纹浏览器1 小时前
指纹浏览器抗检测进阶:绕过深度风控的技术实践
服务器·网络·经验分享·笔记·媒体
jiayong231 小时前
Redis面试深度解析
数据库·redis·面试
wu_huashan1 小时前
环路造成的IP/MAC地址漂移说明
网络·yersinia攻击·ip地址漂移·mac地址漂移
思成不止于此1 小时前
【MySQL 零基础入门】DQL 核心语法(四):执行顺序与综合实战 + DCL 预告篇
数据库·笔记·学习·mysql
model20051 小时前
Alibaba linux 3安装LAMP(5)
linux·运维·服务器
weixin_462446232 小时前
SpringBoot切换Redis的DB
数据库·spring boot·redis
哇哈哈&2 小时前
安装wxWidgets3.2.0(编译高版本erlang的时候用,不如用rpm包),而且还需要高版本的gcc++19以上,已基本舍弃
linux·数据库·python
雨中飘荡的记忆2 小时前
HBase实战指南
大数据·数据库·hbase