VPP中ARP实现第二章:IP Neighbor(IP邻居)模块详解

目录

  1. 模块概述
  2. 模块在节点流中的位置
  3. 核心数据结构
  4. 模块初始化
  5. 邻居表管理
  6. 邻居学习机制
  7. 邻居老化机制
  8. 邻居监控机制
  9. 与Adjacency的交互
  10. [IPv4 ARP处理](#IPv4 ARP处理)
  11. [IPv6 ND处理](#IPv6 ND处理)
  12. API接口

1. 模块概述

1.1 模块作用

IP Neighbor模块是VPP中负责管理IP邻居表的核心模块,它的主要作用是:

  • IP到MAC地址映射管理:维护IP地址与MAC地址的对应关系,类似于传统网络设备中的ARP表(IPv4)和ND表(IPv6)
  • 邻居发现:当需要转发数据包但不知道目标MAC地址时,触发ARP请求(IPv4)或ND请求(IPv6)
  • 邻居学习:从接收到的ARP响应或ND响应中学习邻居的MAC地址
  • 邻居老化:定期清理过期的邻居条目,保持邻居表的有效性
  • 与转发平面集成:将学习到的邻居信息更新到Adjacency中,供数据包转发使用

通俗理解:IP Neighbor模块就像是VPP的"通讯录",记录着"谁(IP地址)住在哪里(MAC地址)"。当VPP需要发送数据包时,它会先查这个"通讯录"找到目标的MAC地址,如果找不到,就会主动询问(发送ARP/ND请求)。

1.2 模块架构

IP Neighbor模块采用分层架构:

  • 通用层ip_neighbor.c):提供IPv4和IPv6共用的邻居表管理功能
  • IPv4层ip4_neighbor.c):实现ARP协议相关功能
  • IPv6层ip6_neighbor.c):实现ND(Neighbor Discovery)协议相关功能
  • 数据平面接口ip_neighbor_dp.c):提供数据平面学习接口
  • 监控机制ip_neighbor_watch.c):提供邻居变化监控功能
  • API接口ip_neighbor_api.c):提供外部API接口

1.3 关键文件说明

文件 作用
ip_neighbor.c 核心模块,管理IP邻居表(哈希表、链表、pool)
ip_neighbor.h 模块头文件,定义对外接口
ip_neighbor_types.h 类型定义,包括ip_neighbor_tip_neighbor_key_t
ip4_neighbor.c IPv4 ARP处理节点实现
ip6_neighbor.c IPv6 ND处理节点实现
ip_neighbor_dp.c 数据平面学习接口(通过RPC调用主线程)
ip_neighbor_watch.c 邻居监控机制实现
ip_neighbor_api.c API消息处理

2. 模块在节点流中的位置

2.1 IPv4节点流

IPv4转发路径中的邻居相关节点

复制代码
ip4-input
  └─> ip4-lookup
       ├─> IP_LOOKUP_NEXT_REWRITE → ip4-rewrite (已知邻居,直接转发)
       ├─> IP_LOOKUP_NEXT_GLEAN → ip4-glean (未知邻居,触发ARP请求)
       └─> IP_LOOKUP_NEXT_ARP → ip4-arp (处理ARP请求/响应)

节点说明

  • ip4-glean节点

    • 作用:当FIB查找返回Glean类型的Adjacency时,触发ARP请求
    • 触发条件:数据包目标IP在同一网段,但邻居表中没有对应的MAC地址
    • 处理流程:从数据包中提取目标IP地址,构造ARP请求包并发送
  • ip4-arp节点

    • 作用:处理接收到的ARP请求和响应
    • 处理ARP请求:如果请求的是本机IP,发送ARP响应
    • 处理ARP响应:学习邻居的MAC地址,更新邻居表和Adjacency

2.2 IPv6节点流

IPv6转发路径中的邻居相关节点

复制代码
ip6-input
  └─> ip6-lookup
       ├─> IP_LOOKUP_NEXT_REWRITE → ip6-rewrite (已知邻居,直接转发)
       └─> IP_LOOKUP_NEXT_GLEAN → ip6-glean (未知邻居,触发ND请求)
            └─> ip6-discover-neighbor (处理ND请求/响应)

节点说明

  • ip6-glean节点 :类似于IPv4的ip4-glean,触发ND请求
  • ip6-discover-neighbor节点:处理IPv6的ND请求和响应

2.3 节点注册

源码位置src/vnet/ip-neighbor/ip4_neighbor.c:277src/vnet/ip-neighbor/ip6_neighbor.c:267


3. 核心数据结构

3.1 ip_neighbor_t结构体详解

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:59

3.1.1 结构体的主要用途

通俗理解ip_neighbor_t就像"通讯录条目"一样,记录了每个邻居的详细信息:

  • 联系方式(IP地址和MAC地址)
  • 身份标识(静态还是动态学习)
  • 状态信息(是否过期、探测次数)
  • 时间戳(最后联系时间)

核心作用

  1. 存储邻居信息:存储IP地址到MAC地址的映射关系
  2. 状态管理:管理邻居的状态(静态/动态、完整/过期)
  3. 老化支持:支持基于时间的邻居老化机制
  4. 关联FIB:关联到FIB条目,用于更新Adjacency
3.1.2 结构体完整定义
c 复制代码
typedef struct ip_neighbor_t_
{
  /**
   * The idempotent key
   */
  ip_neighbor_key_t *ipn_key;

  /**
   * The learned MAC address of the neighbour
   */
  mac_address_t ipn_mac;

  /**
   * Falgs for this object
   */
  ip_neighbor_flags_t ipn_flags;

  /**
   * Aging related data
   *  - last time the neighbour was probed
   *  - number of probes - 3 and it's dead
   */
  f64 ipn_time_last_updated;
  u8 ipn_n_probes;
  index_t ipn_elt;

  /**
   * The index of the adj fib created for this neighbour
   */
  fib_node_index_t ipn_fib_entry_index;
} ip_neighbor_t;
3.1.3 主要字段详解
字段1:ipn_key(邻居键值指针)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:64

类型ip_neighbor_key_t *(指针)

作用

  • 唯一标识:指向邻居的唯一标识(IP地址 + 接口索引)
  • 哈希查找:作为哈希表的键,用于快速查找邻居
  • 内存分离:键值单独分配内存,可以共享或独立管理

为什么使用指针

  • 内存优化:键值结构较小,使用指针可以避免结构体过大
  • 灵活性:可以独立分配和释放键值内存
  • 哈希效率:哈希表可以直接使用键值指针作为键

内存管理

c 复制代码
// 分配键值
ipn->ipn_key = clib_mem_alloc(sizeof(ip_neighbor_key_t));

// 释放键值
clib_mem_free(ipn->ipn_key);

通俗理解:就像"身份证号码"一样,唯一标识这个邻居,通过"身份证号码"可以快速找到对应的"个人信息"(邻居条目)。

字段2:ipn_mac(MAC地址)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:69

类型mac_address_t(结构体,6字节)

MAC地址结构src/vnet/ethernet/mac_address.h:21):

c 复制代码
typedef struct mac_address_t_
{
  union
  {
    u8 bytes[6];           // 6字节数组
    struct
    {
      u32 first_4;         // 前4字节
      u16 last_2;          // 后2字节
    } __clib_packed u;
  };
} mac_address_t;

作用

  • 链路层地址:存储邻居的MAC地址(链路层地址)
  • 数据包封装:用于构造Rewrite字符串,封装数据包
  • 地址解析结果:ARP/ND解析的结果

MAC地址格式

复制代码
字节0-5: XX:XX:XX:XX:XX:XX
         ↑
      6字节以太网MAC地址

使用场景

  • 构造以太网头部(目标MAC地址)
  • 更新Adjacency的Rewrite字符串
  • 验证MAC地址是否变化

通俗理解:就像"门牌号"一样,告诉系统数据包应该发送到哪个"门牌号"(MAC地址)。

字段3:ipn_flags(标志位)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:74

类型ip_neighbor_flags_t(位标志枚举)

标志定义src/vnet/ip-neighbor/ip_neighbor_types.h:25):

c 复制代码
#define foreach_ip_neighbor_flag                 \
  _(STATIC, 1 << 0, "static", "S")               \
  _(DYNAMIC, 1 << 1, "dynamic", "D")             \
  _(NO_FIB_ENTRY, 1 << 2, "no-fib-entry", "N")   \
  _(PENDING, 1 << 3, "pending", "P")             \
  _(STALE, 1 << 4, "stale", "A")

typedef enum ip_neighbor_flags_t_
{
  IP_NEIGHBOR_FLAG_NONE = 0,
#define _(a,b,c,d) IP_NEIGHBOR_FLAG_##a = b,
  foreach_ip_neighbor_flag
#undef _
} __clib_packed ip_neighbor_flags_t;

标志说明

标志 含义 说明
IP_NEIGHBOR_FLAG_STATIC 1 << 0 静态邻居 手动配置,不会被老化删除
IP_NEIGHBOR_FLAG_DYNAMIC 1 << 1 动态邻居 从ARP/ND响应学习,参与老化
IP_NEIGHBOR_FLAG_NO_FIB_ENTRY 1 << 2 无FIB条目 不创建FIB条目
IP_NEIGHBOR_FLAG_PENDING 1 << 3 待定状态 已发送ARP/ND请求,等待响应
IP_NEIGHBOR_FLAG_STALE 1 << 4 过期状态 需要重新探测

标志组合

  • 静态邻居IP_NEIGHBOR_FLAG_STATIC
  • 动态邻居IP_NEIGHBOR_FLAG_DYNAMIC
  • 待定动态邻居IP_NEIGHBOR_FLAG_DYNAMIC | IP_NEIGHBOR_FLAG_PENDING
  • 过期动态邻居IP_NEIGHBOR_FLAG_DYNAMIC | IP_NEIGHBOR_FLAG_STALE

标志操作

c 复制代码
// 设置标志
ipn->ipn_flags |= IP_NEIGHBOR_FLAG_STATIC;

// 清除标志
ipn->ipn_flags &= ~IP_NEIGHBOR_FLAG_STALE;

// 检查标志
if (ipn->ipn_flags & IP_NEIGHBOR_FLAG_STATIC)
    // 静态邻居处理

通俗理解:就像"标签"一样,标识这个邻居的"身份"(静态/动态)和"状态"(待定/过期)。

字段4:ipn_time_last_updated(最后更新时间)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:81

类型f64(双精度浮点数,秒)

作用

  • 时间戳:记录邻居最后更新的时间(秒,浮点数)
  • 老化判断:用于判断邻居是否过期
  • LRU排序:用于LRU链表的排序

时间获取

c 复制代码
ipn->ipn_time_last_updated = vlib_time_now(vlib_get_main());

老化判断

c 复制代码
f64 ttl = now - ipn->ipn_time_last_updated;
if (ttl > ipndb_age)
    // 邻居已过期

时间精度 :秒级精度,使用vlib_time_now获取,返回自系统启动以来的秒数(浮点数)。

通俗理解:就像"最后联系时间"一样,记录"最后联系"这个邻居的时间,用于判断是否"很久没联系"(过期)。

字段5:ipn_n_probes(探测次数)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:82

类型u8(1字节,0-255)

作用

  • 探测计数:记录对邻居的探测次数
  • 失效判断:如果探测3次都没有响应,认为邻居已失效
  • 老化机制:配合老化机制使用

探测流程

复制代码
邻居过期 → 发送第1次探测 → ipn_n_probes = 1
         → 发送第2次探测 → ipn_n_probes = 2
         → 发送第3次探测 → ipn_n_probes = 3
         → 如果仍无响应 → 删除邻居

重置时机

  • 收到ARP/ND响应时,重置为0
  • 邻居更新时,重置为0

重置代码

c 复制代码
ipn->ipn_n_probes = 0;  // 重置探测计数

失效判断

c 复制代码
if (ipn->ipn_n_probes > 2)
    // 3次探测失败,删除邻居
    ip_neighbor_destroy(ipn);

通俗理解:就像"敲门次数"一样,记录"敲了几次门"(探测次数),如果"敲了3次都没人开门"(3次探测失败),就认为"家里没人"(邻居失效)。

字段6:ipn_elt(链表元素索引)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:83

类型index_tu32,索引)

作用

  • LRU链表:指向LRU链表中的元素索引
  • 链表操作:用于在链表中添加、删除、移动邻居
  • 时间排序:链表按时间排序,最新的在头部,最旧的在尾部

链表元素结构src/vnet/ip-neighbor/ip_neighbor.c:104):

c 复制代码
typedef struct ip_neighbor_elt_t_
{
  clib_llist_anchor_t ipne_anchor;  // 链表锚点
  index_t ipne_index;               // 邻居索引
} ip_neighbor_elt_t;

链表关系

复制代码
ip_neighbor_t
    │
    ├── ipn_elt (索引)
    │       ↓
    │   ip_neighbor_elt_t
    │       ├── ipne_anchor (链表锚点)
    │       └── ipne_index (指向ip_neighbor_t)
    │
    └── 双向链表
        最新 ← → 最旧

特殊值

  • ~0(0xFFFFFFFF):无效索引,表示不在链表中

使用场景

  • LRU淘汰:从链表尾部删除最旧的邻居
  • 老化扫描:从链表尾部开始扫描,检查过期邻居
  • 时间更新:更新邻居时,移动到链表头部

通俗理解:就像"排队号码"一样,标识这个邻居在"时间队列"中的位置,用于"按时间顺序"管理邻居。

字段7:ipn_fib_entry_index(FIB条目索引)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:88

类型fib_node_index_tu32,索引)

作用

  • FIB关联:关联到FIB条目,用于更新Adjacency
  • 路由管理:当FIB条目变化时,可以更新相关的邻居
  • Adjacency更新:当邻居变化时,可以更新相关的Adjacency

FIB条目类型

  • 连接到接口的前缀(Connected prefix)
  • 用于驱动ARP/ND的Glean Adj

关联流程

复制代码
添加邻居 → 创建/查找FIB条目 → 存储索引 → 更新Adjacency

使用场景

  • 邻居添加时,创建FIB条目
  • 邻居删除时,删除FIB条目
  • 接口删除时,清理所有相关的FIB条目

通俗理解:就像"关联记录"一样,记录这个邻居关联到哪个"路由条目"(FIB条目),用于"同步更新"路由和邻居信息。

3.1.4 结构体的内存布局

内存布局

复制代码
ip_neighbor_t (总大小约40-50字节)
├── ipn_key (8字节指针)
├── ipn_mac (6字节)
├── ipn_flags (1字节)
├── ipn_time_last_updated (8字节)
├── ipn_n_probes (1字节)
├── ipn_elt (4字节)
└── ipn_fib_entry_index (4字节)

内存分配

c 复制代码
// 从Pool分配
ip_neighbor_t *ipn;
pool_get(ip_neighbor_pool, ipn);

// 分配键值
ipn->ipn_key = clib_mem_alloc(sizeof(ip_neighbor_key_t));

内存释放

c 复制代码
// 释放键值
clib_mem_free(ipn->ipn_key);

// 释放邻居条目
pool_put(ip_neighbor_pool, ipn);

3.2 ip_neighbor_key_t结构体详解

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:49

3.2.1 结构体的主要用途

通俗理解ip_neighbor_key_t就像"身份证号码"一样,唯一标识一个邻居:

  • IP地址:邻居的IP地址(IPv4或IPv6)
  • 接口索引:邻居所在的接口

核心作用

  1. 唯一标识:作为邻居的唯一标识(键)
  2. 哈希查找:作为哈希表的键,用于快速查找
  3. 区分接口:同一个IP地址在不同接口上被视为不同的邻居
3.2.2 结构体完整定义
c 复制代码
typedef struct ip_neighbor_key_t_
{
  ip_address_t ipnk_ip;
  u8 __pad[3];
  u32 ipnk_sw_if_index;
} __clib_packed ip_neighbor_key_t;
3.2.3 主要字段详解
字段1:ipnk_ip(IP地址)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:51

类型ip_address_t(结构体)

IP地址结构src/vnet/ip/ip_types.h:78):

c 复制代码
typedef struct ip_address
{
  ip46_address_t ip;              // IPv4或IPv6地址(联合体)
  ip_address_family_t version;   // 地址族(AF_IP4或AF_IP6)
} __clib_packed ip_address_t;

ip46_address_t结构src/vnet/ip/ip46_address.h:37):

c 复制代码
typedef CLIB_PACKED (union ip46_address_t_ {
  struct {
    u32 pad[3];           // 填充(IPv4时使用)
    ip4_address_t ip4;    // IPv4地址(4字节)
  };
  ip6_address_t ip6;     // IPv6地址(16字节)
  u8 as_u8[16];          // 16字节数组
  u64 as_u64[2];         // 2个64位整数
}) ip46_address_t;

IPv4地址结构src/vnet/ip/ip4_packet.h:49):

c 复制代码
typedef union
{
  u8 data[4];
  u32 data_u32;
  u8 as_u8[4];
  u16 as_u16[2];
  u32 as_u32;
} ip4_address_t;

IPv6地址结构src/vnet/ip/ip6_packet.h:47):

c 复制代码
typedef union
{
  u8 as_u8[16];
  u16 as_u16[8];
  u32 as_u32[4];
  u64 as_u64[2];
  u64x2 as_u128;
  uword as_uword[16 / sizeof (uword)];
} __clib_packed ip6_address_t;

作用

  • 地址存储:存储邻居的IP地址(IPv4或IPv6)
  • 地址族标识 :通过version字段区分IPv4和IPv6
  • 地址操作:支持地址比较、复制、格式化等操作

地址大小

  • IPv4:4字节
  • IPv6:16字节
  • ip46_address_t:16字节(联合体,IPv4时前12字节为0)

使用示例

c 复制代码
// 设置IPv4地址
ip_address_t ip;
ip.version = AF_IP4;
ip.ip.ip4.as_u32 = clib_host_to_net_u32(0xC0A80164);  // 192.168.1.100

// 设置IPv6地址
ip.version = AF_IP6;
ip.ip.ip6.as_u64[0] = ...;
ip.ip.ip6.as_u64[1] = ...;

通俗理解:就像"IP地址"一样,标识邻居的"网络地址"(IP地址),可以是"IPv4地址"(4字节)或"IPv6地址"(16字节)。

字段2:__pad[3](填充字节)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:52

类型u8 __pad[3](3字节填充)

作用

  • 内存对齐 :用于内存对齐,确保ipnk_sw_if_index字段4字节对齐
  • 结构体大小:不影响功能,但影响结构体大小

对齐原因

  • ip_address_t的大小可能不是4的倍数
  • u32ipnk_sw_if_index)需要4字节对齐
  • 填充3字节可以确保对齐

通俗理解:就像"填充物"一样,用于"对齐"内存,不影响功能。

字段3:ipnk_sw_if_index(接口索引)

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:53

类型u32(4字节)

作用

  • 接口标识:标识邻居所在的接口
  • 接口区分:同一个IP地址在不同接口上被视为不同的邻居
  • 哈希表索引:作为哈希表的第一级索引

特殊值

  • ~0(0xFFFFFFFF):无效接口索引

使用场景

  • 哈希表查找:ipndb_hash[sw_if_index]
  • 接口遍历:遍历特定接口上的所有邻居
  • 接口删除:删除接口时,清理该接口上的所有邻居

通俗理解:就像"门牌号"一样,标识这个邻居在哪个"门"(接口)上,同一个"地址"(IP)在不同"门"上是不同的邻居。

3.2.4 键值的唯一性

唯一性规则

复制代码
键值 = {IP地址, 接口索引}

唯一性示例

复制代码
邻居1: {192.168.1.100, 接口1}  ✓ 唯一
邻居2: {192.168.1.100, 接口2}  ✓ 唯一(不同接口)
邻居3: {192.168.1.100, 接口1}  ✗ 重复(相同键值)

哈希表使用

c 复制代码
// 构造键值
ip_neighbor_key_t key = {
  .ipnk_ip = *ip,
  .ipnk_sw_if_index = sw_if_index,
};

// 哈希表查找
ipn = ip_neighbor_db_find(&key);

通俗理解:就像"身份证号码"一样,由"地址"(IP)和"门牌号"(接口)组成,唯一标识一个邻居。


3.3 ip_neighbor_db_t结构体详解

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

3.3.1 结构体的主要用途

通俗理解ip_neighbor_db_t就像"通讯录数据库"一样,管理所有邻居:

  • 存储结构:使用哈希表快速查找邻居
  • 限制管理:限制最大邻居数量,防止内存耗尽
  • 老化管理:管理邻居的老化时间
  • 统计信息:统计当前邻居数量

核心作用

  1. 邻居存储:使用多级哈希表存储邻居
  2. 快速查找:通过接口索引和IP地址快速查找邻居
  3. 资源限制:限制最大邻居数量,防止内存耗尽
  4. 老化支持:支持基于时间的邻居老化
3.3.2 结构体完整定义
c 复制代码
typedef struct ip_neighbor_db_t_
{
  /** per interface hash */
  uword **ipndb_hash;
  /** per-protocol limit - max number of neighbors*/
  u32 ipndb_limit;
  /** max age of a neighbor before it's forcibly evicted */
  u32 ipndb_age;
  /** when the limit is reached and new neighbors are created, should
   * we recycle an old one */
  bool ipndb_recycle;
  /** per-protocol number of elements */
  u32 ipndb_n_elts;
  /** per-protocol number of elements per-fib-index*/
  u32 *ipndb_n_elts_per_fib;
} ip_neighbor_db_t;
3.3.3 主要字段详解
字段1:ipndb_hash(哈希表)

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

类型uword **(二级指针,多级哈希表)

作用

  • 快速查找:通过接口索引和IP地址快速查找邻居
  • 多级索引:第一级按接口索引,第二级按IP地址

哈希表结构

复制代码
ipndb_hash (第一级:按接口索引)
    │
    ├── [0] → hash_table (接口0的哈希表)
    │         ├── key1 → index1
    │         ├── key2 → index2
    │         └── ...
    │
    ├── [1] → hash_table (接口1的哈希表)
    │         ├── key1 → index3
    │         └── ...
    │
    └── [N] → hash_table (接口N的哈希表)

哈希表创建src/vnet/ip-neighbor/ip_neighbor.c:266):

c 复制代码
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));

哈希表查找src/vnet/ip-neighbor/ip_neighbor.c:303):

c 复制代码
p = hash_get_mem(ip_neighbor_db[af].ipndb_hash[key->ipnk_sw_if_index], key);
if (p)
    return ip_neighbor_get(p[0]);  // 返回邻居索引

哈希表插入src/vnet/ip-neighbor/ip_neighbor.c:270):

c 复制代码
hash_set_mem(ip_neighbor_db[af].ipndb_hash[sw_if_index],
             ipn->ipn_key, ip_neighbor_get_index(ipn));

哈希表删除src/vnet/ip-neighbor/ip_neighbor.c:287):

c 复制代码
hash_unset_mem(ip_neighbor_db[af].ipndb_hash[sw_if_index], ipn->ipn_key);

为什么使用多级哈希表

  • 接口隔离:不同接口的邻居隔离,减少哈希冲突
  • 查找效率:先按接口索引,再按IP地址,查找效率高
  • 内存优化:只为有邻居的接口创建哈希表

通俗理解:就像"分类书架"一样,先按"房间"(接口)分类,再按"书名"(IP地址)查找,提高"找书"(查找邻居)的效率。

字段2:ipndb_limit(最大邻居数量限制)

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

类型u32(4字节)

作用

  • 资源限制:限制最大邻居数量,防止内存耗尽
  • DoS防护:防止恶意攻击导致邻居表无限增长

默认值

c 复制代码
[AF_IP4] = {
  .ipndb_limit = 50000,  // IPv4默认50000个
},
[AF_IP6] = {
  .ipndb_limit = 50000,  // IPv6默认50000个
}

限制检查src/vnet/ip-neighbor/ip_neighbor.c:505):

c 复制代码
if (ip_neighbor_db[af].ipndb_n_elts >= ip_neighbor_db[af].ipndb_limit)
{
    if (ip_neighbor_db[af].ipndb_recycle)
        ip_neighbor_force_reuse(af);  // 回收最旧的邻居
    else
        return VNET_API_ERROR_LIMIT_EXCEEDED;  // 拒绝新邻居
}

配置方式

bash 复制代码
set ip neighbor-config ip4 limit 10000

通俗理解:就像"通讯录容量限制"一样,限制"通讯录"最多能存多少个"联系人"(邻居),防止"通讯录"太大。

字段3:ipndb_age(老化时间)

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

类型u32(4字节,秒)

作用

  • 老化时间:设置邻居的老化时间(秒)
  • 自动清理:超过老化时间的邻居会被自动清理

默认值

c 复制代码
[AF_IP4] = {
  .ipndb_age = 0,  // 0表示禁用老化
},
[AF_IP6] = {
  .ipndb_age = 0,  // 0表示禁用老化
}

老化判断src/vnet/ip-neighbor/ip_neighbor.c:1577):

c 复制代码
f64 ttl = now - ipn->ipn_time_last_updated;
if (ttl > ipndb_age)
    // 邻居已过期

配置方式

bash 复制代码
set ip neighbor-config ip4 age 300  # 300秒老化

特殊值

  • 0:禁用老化(默认)

通俗理解:就像"联系时间限制"一样,如果"很久没联系"(超过老化时间),就认为"联系不上"(邻居失效),从"通讯录"中删除。

字段4:ipndb_recycle(回收标志)

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

类型bool(1字节布尔值)

作用

  • 回收策略:当达到限制时,是否回收最旧的邻居
  • 内存管理:控制内存使用策略

默认值

c 复制代码
[AF_IP4] = {
  .ipndb_recycle = false,  // 默认不回收
},
[AF_IP6] = {
  .ipndb_recycle = false,  // 默认不回收
}

回收机制src/vnet/ip-neighbor/ip_neighbor.c:486):

c 复制代码
static bool
ip_neighbor_force_reuse (ip_address_family_t af)
{
    if (!ip_neighbor_db[af].ipndb_recycle)
        return false;

    // 从LRU链表尾部删除最旧的邻居
    ip_neighbor_elt_t *elt, *head;
    head = pool_elt_at_index(ip_neighbor_elt_pool, ip_neighbor_list_head[af]);
    elt = clib_llist_prev(ip_neighbor_elt_pool, ipne_anchor, head);
    ip_neighbor_destroy(ip_neighbor_get(elt->ipne_index));

    return true;
}

配置方式

bash 复制代码
set ip neighbor-config ip4 recycle      # 启用回收
set ip neighbor-config ip4 norecycle   # 禁用回收

两种策略

策略 recycle=false recycle=true
达到限制时 拒绝新邻居 回收最旧邻居
优点 保护现有邻居 允许新邻居
缺点 可能拒绝合法邻居 可能删除活跃邻居

通俗理解:就像"通讯录满了怎么办"一样,如果"通讯录满了"(达到限制),可以选择"拒绝新联系人"(recycle=false)或"删除最旧的联系人"(recycle=true)。

字段5:ipndb_n_elts(当前邻居数量)

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

类型u32(4字节)

作用

  • 统计信息:统计当前邻居数量
  • 限制检查:用于检查是否达到限制

更新时机

c 复制代码
// 添加邻居时
ip_neighbor_db[af].ipndb_n_elts++;

// 删除邻居时
ip_neighbor_db[af].ipndb_n_elts--;

使用场景

  • 限制检查:if (ipndb_n_elts >= ipndb_limit)
  • 统计显示:show ip neighbor显示邻居数量

通俗理解:就像"通讯录当前联系人数量"一样,记录"通讯录"中现在有多少个"联系人"(邻居)。

字段6:ipndb_n_elts_per_fib(每个FIB的邻居数量)

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

类型u32 *(向量,每个FIB索引一个计数)

作用

  • FIB统计:统计每个FIB的邻居数量
  • FIB管理:用于FIB相关的统计和管理

向量结构

复制代码
ipndb_n_elts_per_fib[0] = 10  // FIB 0有10个邻居
ipndb_n_elts_per_fib[1] = 5   // FIB 1有5个邻居
...

使用场景

  • FIB删除时,统计该FIB的邻居数量
  • FIB统计显示

通俗理解:就像"每个房间的联系人数量"一样,统计每个"房间"(FIB)有多少个"联系人"(邻居)。

3.3.4 数据库的初始化

初始化位置src/vnet/ip-neighbor/ip_neighbor.c:133

初始化代码

c 复制代码
static ip_neighbor_db_t ip_neighbor_db[N_AF] = {
  [AF_IP4] = {
    .ipndb_limit = 50000,
    .ipndb_age = 0,
    .ipndb_recycle = false,
  },
  [AF_IP6] = {
    .ipndb_limit = 50000,
    .ipndb_age = 0,
    .ipndb_recycle = false,
  }
};

说明

  • 按地址族分离:IPv4和IPv6使用独立的数据库
  • 默认配置:默认限制50000,禁用老化,禁用回收

3.4 ip_neighbor_elt_t结构体详解

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

3.4.1 结构体的主要用途

通俗理解ip_neighbor_elt_t就像"排队号码牌"一样,用于在"时间队列"中标识邻居的位置:

  • 队列位置:标识邻居在LRU链表中的位置
  • 时间排序:链表按时间排序,最新的在头部,最旧的在尾部

核心作用

  1. LRU链表节点:作为LRU链表的节点
  2. 时间排序:维护邻居的时间顺序
  3. 快速访问:通过链表快速访问最旧或最新的邻居
3.4.2 结构体完整定义
c 复制代码
typedef struct ip_neighbor_elt_t_
{
  clib_llist_anchor_t ipne_anchor;
  index_t ipne_index;
} ip_neighbor_elt_t;
3.4.3 主要字段详解
字段1:ipne_anchor(链表锚点)

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

类型clib_llist_anchor_t(双向链表的锚点)

作用

  • 链表连接:用于连接双向链表的节点
  • 前后指针:包含指向前一个和后一个节点的指针

链表结构

复制代码
链表头 (ip_neighbor_list_head[af])
    │
    ├── ipne_anchor (prev/next)
    │       ↓
    │   ip_neighbor_elt_t
    │       ├── ipne_anchor (prev/next)
    │       └── ipne_index → ip_neighbor_t
    │
    └── ipne_anchor (prev/next)
            ↓
        ip_neighbor_elt_t
            ├── ipne_anchor (prev/next)
            └── ipne_index → ip_neighbor_t

链表操作

c 复制代码
// 添加到链表头部(最新)
clib_llist_add(ip_neighbor_elt_pool, ipne_anchor, elt, head);

// 从链表中删除
clib_llist_remove(ip_neighbor_elt_pool, ipne_anchor, elt);

// 遍历链表(从新到旧)
clib_llist_foreach(ip_neighbor_elt_pool, ipne_anchor, head, elt, ({
    // 处理每个邻居
}));

// 反向遍历(从旧到新)
clib_llist_foreach_reverse(ip_neighbor_elt_pool, ipne_anchor, head, elt, ({
    // 处理每个邻居
}));

通俗理解:就像"链条"一样,把"排队号码牌"(链表元素)连接起来,形成"时间队列"。

字段2:ipne_index(邻居索引)

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

类型index_tu32,索引)

作用

  • 邻居引用 :指向对应的ip_neighbor_t条目
  • 双向关联ip_neighbor_t.ipn_elt指向链表元素,链表元素的ipne_index指向邻居

关联关系

复制代码
ip_neighbor_t
    │
    ├── ipn_elt (索引)
    │       ↓
    │   ip_neighbor_elt_t
    │       └── ipne_index (索引)
    │               ↓
    └─────────── ip_neighbor_t (循环引用)

索引获取

c 复制代码
elt->ipne_index = ip_neighbor_get_index(ipn);

邻居获取

c 复制代码
ipn = ip_neighbor_get(elt->ipne_index);

通俗理解:就像"指向联系人的指针"一样,指向对应的"联系人"(邻居条目)。

3.4.4 链表的时间排序

排序规则

  • 最新在头部:新添加或更新的邻居放在链表头部
  • 最旧在尾部:最旧的邻居在链表尾部

链表结构

复制代码
链表头
    │
    ├── [最新] ← ipne_anchor → [较新] ← ipne_anchor → [较旧] ← ipne_anchor → [最旧]
    │
    └── 时间顺序:最新 → 最旧

更新操作src/vnet/ip-neighbor/ip_neighbor.c:223):

c 复制代码
static void
ip_neighbor_refresh (ip_neighbor_t * ipn)
{
    // 步骤1:如果已在链表中,先删除
    if (~0 != ipn->ipn_elt)
    {
        elt = pool_elt_at_index(ip_neighbor_elt_pool, ipn->ipn_elt);
        clib_llist_remove(ip_neighbor_elt_pool, ipne_anchor, elt);
    }
    
    // 步骤2:添加到链表头部(最新位置)
    head = pool_elt_at_index(ip_neighbor_elt_pool, 
                              ip_neighbor_list_head[af]);
    clib_llist_add(ip_neighbor_elt_pool, ipne_anchor, elt, head);
}

LRU淘汰src/vnet/ip-neighbor/ip_neighbor.c:499):

c 复制代码
// 从链表尾部获取最旧的邻居
elt = clib_llist_prev(ip_neighbor_elt_pool, ipne_anchor, head);
ip_neighbor_destroy(ip_neighbor_get(elt->ipne_index));

老化扫描src/vnet/ip-neighbor/ip_neighbor.c:1653):

c 复制代码
// 从链表尾部开始扫描(从最旧的开始)
clib_llist_foreach_reverse(ip_neighbor_elt_pool, ipne_anchor, head, elt, ({
    // 检查每个邻居是否过期
}));

通俗理解:就像"按时间排队"一样,最新"联系"的邻居排在"队伍前面"(链表头部),最久"联系"的邻居排在"队伍后面"(链表尾部)。


3.5 数据结构之间的关系

关系图

复制代码
ip_neighbor_db_t (数据库)
    │
    ├── ipndb_hash[sw_if_index] (哈希表)
    │       │
    │       └── key → index → ip_neighbor_t
    │                           │
    │                           ├── ipn_key → ip_neighbor_key_t
    │                           ├── ipn_mac (MAC地址)
    │                           ├── ipn_elt (索引)
    │                           │       ↓
    │                           └── ip_neighbor_elt_t (链表元素)
    │                                   ├── ipne_anchor (链表锚点)
    │                                   └── ipne_index (指向ip_neighbor_t)
    │
    └── ip_neighbor_list_head[af] (链表头)
            │
            └── 双向链表 (按时间排序)
                最新 ← → 最旧

数据流

复制代码
添加邻居
    ↓
分配ip_neighbor_t
    ↓
分配ip_neighbor_key_t
    ↓
插入哈希表 (ipndb_hash[sw_if_index])
    ↓
添加到LRU链表 (ip_neighbor_list_head[af])
    ↓
分配ip_neighbor_elt_t
    ↓
建立双向关联 (ipn_elt ↔ ipne_index)

查找流程

复制代码
查找邻居
    ↓
构造ip_neighbor_key_t
    ↓
哈希表查找 (ipndb_hash[sw_if_index])
    ↓
返回index
    ↓
获取ip_neighbor_t

删除流程

复制代码
删除邻居
    ↓
从哈希表删除 (ipndb_hash[sw_if_index])
    ↓
从LRU链表删除 (ip_neighbor_list_head[af])
    ↓
释放ip_neighbor_key_t
    ↓
释放ip_neighbor_t
    ↓
释放ip_neighbor_elt_t

通俗理解

  • ip_neighbor_db_t:就像"通讯录数据库",管理所有"联系人"
  • ip_neighbor_t:就像"联系人信息",包含"联系方式"和"状态"
  • ip_neighbor_key_t:就像"身份证号码",唯一标识"联系人"
  • ip_neighbor_elt_t:就像"排队号码牌",标识"联系人"在"时间队列"中的位置

3.6 总结

ip_neighbor_t的关键点

  1. 主要用途:存储邻居信息,管理邻居状态
  2. 核心字段
    • ipn_key:唯一标识
    • ipn_mac:MAC地址
    • ipn_flags:状态标志
    • ipn_time_last_updated:时间戳
    • ipn_n_probes:探测次数
    • ipn_elt:链表索引
    • ipn_fib_entry_index:FIB条目索引

ip_neighbor_key_t的关键点

  1. 主要用途:唯一标识邻居
  2. 核心字段
    • ipnk_ip:IP地址
    • ipnk_sw_if_index:接口索引
  3. 唯一性:由IP地址和接口索引组成唯一键

ip_neighbor_db_t的关键点

  1. 主要用途:管理所有邻居,提供快速查找
  2. 核心字段
    • ipndb_hash:多级哈希表
    • ipndb_limit:最大数量限制
    • ipndb_age:老化时间
    • ipndb_recycle:回收策略
    • ipndb_n_elts:当前数量
  3. 设计特点:多级哈希表,按接口索引和IP地址查找

ip_neighbor_elt_t的关键点

  1. 主要用途:LRU链表的节点
  2. 核心字段
    • ipne_anchor:链表锚点
    • ipne_index:邻居索引
  3. 设计特点:双向链表,按时间排序

数据结构的协作

  • 哈希表:提供O(1)的查找性能
  • LRU链表:提供时间排序,支持LRU淘汰和老化扫描
  • 双向关联:邻居和链表元素相互引用,方便操作

4. 模块初始化

4.1 初始化时机:什么时候开始初始化?

关键点 :IP Neighbor模块的初始化发生在VPP启动时,而不是在网口up或设定IP时。

通俗理解:就像建房子一样,IP Neighbor模块的初始化是在"打地基"阶段完成的,而不是等到"装修"(配置网口)时才做。这样做的目的是让模块提前准备好"基础设施"(比如注册回调函数),以便后续能够响应各种事件。

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

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

初始化顺序说明

  1. VPP启动流程

    • VPP启动时,会按照依赖关系依次调用各个模块的初始化函数
    • IP Neighbor模块的初始化函数ip_neighbor_initip_main_init(IP主模块)初始化之后执行
    • 这意味着当IP Neighbor模块初始化时,IP主模块已经准备好了
  2. 为什么要在IP主模块之后初始化?

    • IP Neighbor模块需要向IP主模块注册回调函数
    • 如果IP主模块还没初始化,就无法注册回调函数
    • 这就像你要给某人留电话号码,必须先确保对方已经准备好了接收系统
  3. 初始化时机总结

    • VPP启动时:模块初始化,注册回调函数
    • 不是网口up时:网口up只是触发回调函数,不是初始化
    • 不是设定IP时:设定IP只是触发回调函数,不是初始化

4.2 初始化函数详解:都做了哪些操作?

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

让我们逐步分析ip_neighbor_init函数做了什么:

4.2.1 注册IPv4接口地址变化回调

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

c 复制代码
{
  ip4_add_del_interface_address_callback_t cb = {
    .function = ip_neighbor_add_del_interface_address_v4,
  };
  vec_add1 (ip4_main.add_del_interface_address_callbacks, cb);
}

作用:向IP主模块注册一个回调函数,当接口的IPv4地址被添加或删除时,IP主模块会调用这个回调函数。

通俗理解:就像在邮局登记"地址变更通知服务",当你的地址(接口IP)发生变化时,邮局(IP主模块)会自动通知你(IP Neighbor模块)。

后续功能铺垫

  • 当接口IP地址被删除时,需要清理该网段内的所有动态邻居条目
  • 例如:如果接口IP从192.168.1.1/24改为10.0.0.1/24,那么192.168.1.x网段的所有邻居都应该被删除,因为它们已经不在同一个网段了

回调函数实现ip_neighbor_add_del_interface_address_v4src/vnet/ip-neighbor/ip_neighbor.c:1431

4.2.2 注册IPv6接口地址变化回调

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

c 复制代码
{
  ip6_add_del_interface_address_callback_t cb = {
    .function = ip_neighbor_add_del_interface_address_v6,
  };
  vec_add1 (ip6_main.add_del_interface_address_callbacks, cb);
}

作用:类似于IPv4,注册IPv6接口地址变化的回调函数。

说明:IPv6和IPv4使用不同的回调函数,因为它们的地址格式和处理方式不同。

4.2.3 注册IPv4 FIB表绑定回调

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

c 复制代码
{
  ip4_table_bind_callback_t cb = {
    .function = ip_neighbor_table_bind_v4,
  };
  vec_add1 (ip4_main.table_bind_callbacks, cb);
}

作用:当接口绑定到不同的FIB表(VRF)时,IP主模块会调用这个回调函数。

通俗理解:就像搬家时,你需要把通讯录(邻居表)中的地址信息更新到新的地址簿(FIB表)中。当接口从一个VRF切换到另一个VRF时,该接口上的所有邻居都需要更新它们关联的FIB条目。

后续功能铺垫

  • 当接口从一个VRF切换到另一个VRF时,需要更新所有邻居的FIB条目
  • 例如:接口从VRF 0切换到VRF 1,那么该接口上的所有邻居都需要在VRF 1中创建FIB条目,并删除VRF 0中的FIB条目

回调函数实现ip_neighbor_table_bind_v4src/vnet/ip-neighbor/ip_neighbor.c:1527

4.2.4 注册IPv6 FIB表绑定回调

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

c 复制代码
{
  ip6_table_bind_callback_t cb = {
    .function = ip_neighbor_table_bind_v6,
  };
  vec_add1 (ip6_main.table_bind_callbacks, cb);
}

作用:类似于IPv4,注册IPv6 FIB表绑定的回调函数。

4.2.5 注册日志类

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

c 复制代码
ipn_logger = vlib_log_register_class ("ip", "neighbor");

作用:注册一个日志类,用于记录IP Neighbor模块的日志信息。

通俗理解:就像给模块分配一个"日志记录本",后续所有的日志信息都会记录在这个"本子"里,方便调试和问题排查。

日志分类

  • 主类:"ip"(IP相关)
  • 子类:"neighbor"(邻居相关)
  • 完整日志路径:ip.neighbor
4.2.6 初始化邻居链表头

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

c 复制代码
ip_address_family_t af;

FOR_EACH_IP_ADDRESS_FAMILY (af)
  ip_neighbor_list_head[af] =
  clib_llist_make_head (ip_neighbor_elt_pool, ipne_anchor);

作用:为IPv4和IPv6分别创建邻居链表的头节点。

通俗理解:就像创建一个"空文件夹",用来存放后续添加的邻居条目。IPv4和IPv6各自有一个"文件夹"。

数据结构说明

  • ip_neighbor_list_head[AF_IP4]:IPv4邻居链表的头节点
  • ip_neighbor_list_head[AF_IP6]:IPv6邻居链表的头节点
  • 链表用于维护邻居的时间顺序,方便LRU淘汰和老化扫描

后续功能铺垫

  • 当添加邻居时,会插入到这个链表中
  • 当需要淘汰邻居时,可以从链表尾部(最旧的)开始删除
  • 当需要老化扫描时,可以按时间顺序遍历链表

4.3 其他自动注册的回调函数

除了在ip_neighbor_init中注册的回调函数,IP Neighbor模块还通过宏自动注册了其他回调函数:

4.3.1 接口up/down回调

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

c 复制代码
VNET_SW_INTERFACE_ADMIN_UP_DOWN_FUNCTION (ip_neighbor_interface_admin_change);

作用:当接口被设置为up或down时,VPP会自动调用这个函数。

回调函数实现ip_neighbor_interface_admin_changesrc/vnet/ip-neighbor/ip_neighbor.c:1333

处理逻辑

  • 接口up时 :调用ip_neighbor_populate,恢复该接口上的所有邻居(更新Adjacency)
  • 接口down时 :调用ip_neighbor_flush,清理该接口上的所有动态邻居

通俗理解:就像"开关灯"一样,当接口up时,把之前保存的邻居信息"点亮"(恢复),当接口down时,把邻居信息"关掉"(清理)。

4.3.2 接口添加/删除回调

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

c 复制代码
VNET_SW_INTERFACE_ADD_DEL_FUNCTION (ip_neighbor_add_del_sw_interface);

作用:当接口被添加或删除时,VPP会自动调用这个函数。

回调函数实现ip_neighbor_add_del_sw_interfacesrc/vnet/ip-neighbor/ip_neighbor.c:1362

处理逻辑

  • 接口添加时:为该接口分配统计计数器
  • 接口删除时:清理该接口上的所有邻居

4.4 初始化总结:为后续什么功能做铺垫?

初始化操作总结表

初始化操作 作用 为后续功能做铺垫
注册IPv4地址变化回调 监听接口IP地址变化 当接口IP变化时,自动清理无效的邻居条目
注册IPv6地址变化回调 监听接口IP地址变化 同上,但针对IPv6
注册IPv4 FIB绑定回调 监听接口VRF切换 当接口切换VRF时,自动更新邻居的FIB条目
注册IPv6 FIB绑定回调 监听接口VRF切换 同上,但针对IPv6
注册日志类 提供日志记录功能 方便调试和问题排查
初始化邻居链表头 创建邻居链表结构 支持LRU淘汰和老化扫描

关键设计思想

  1. 事件驱动架构

    • IP Neighbor模块采用事件驱动架构,通过注册回调函数来响应各种事件
    • 这样设计的好处是:模块不需要主动轮询,而是被动响应事件,效率更高
  2. 提前准备基础设施

    • 在VPP启动时就注册好所有回调函数,而不是等到需要时才注册
    • 这样确保后续的任何操作都能得到及时响应
  3. 自动维护一致性

    • 通过回调函数自动维护邻居表的一致性
    • 例如:当接口IP变化时,自动清理无效邻居;当接口切换VRF时,自动更新FIB条目

实际应用场景举例

场景1:接口IP地址变化

复制代码
1. 用户执行:set interface ip address GigabitEthernet0/0/0 192.168.1.1/24
2. IP主模块检测到IP地址变化,调用注册的回调函数
3. IP Neighbor模块自动清理192.168.1.x网段的所有动态邻居
4. 用户执行:set interface ip address GigabitEthernet0/0/0 10.0.0.1/24
5. IP Neighbor模块自动清理10.0.0.x网段的所有动态邻居

场景2:接口切换VRF

复制代码
1. 用户执行:set interface ip table GigabitEthernet0/0/0 1
2. IP主模块检测到VRF变化,调用注册的回调函数
3. IP Neighbor模块自动将该接口上的所有邻居的FIB条目从VRF 0迁移到VRF 1

场景3:接口up/down

复制代码
1. 用户执行:set interface state GigabitEthernet0/0/0 down
2. VPP检测到接口down,调用注册的回调函数
3. IP Neighbor模块自动清理该接口上的所有动态邻居
4. 用户执行:set interface state GigabitEthernet0/0/0 up
5. IP Neighbor模块自动恢复该接口上的所有邻居(更新Adjacency)

通过这些初始化操作,IP Neighbor模块建立了一个完整的"事件响应机制",能够自动维护邻居表的一致性,无需用户手动干预。


5. 邻居表管理

5.1 添加邻居(ip_neighbor_add)

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

功能:添加或更新IP邻居条目,这是IP Neighbor模块最核心的函数之一。

通俗理解:就像在通讯录中添加或更新联系人信息。如果联系人已存在,就更新他的信息;如果不存在,就新建一个联系人条目。

5.1.1 函数入口和参数检查

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

c 复制代码
int
ip_neighbor_add (const ip_address_t * ip,
		 const mac_address_t * mac,
		 u32 sw_if_index,
		 ip_neighbor_flags_t flags, u32 * stats_index)
{
  fib_protocol_t fproto;
  ip_neighbor_t *ipn;

  /* main thread only */
  ASSERT (0 == vlib_get_thread_index ());

  fproto = ip_address_family_to_fib_proto (ip_addr_version (ip));

  const ip_neighbor_key_t key = {
    .ipnk_ip = *ip,
    .ipnk_sw_if_index = sw_if_index,
  };

步骤说明

  1. 线程检查ASSERT (0 == vlib_get_thread_index ())

    • 确保只在主线程(thread 0)执行
    • 因为需要修改全局数据结构(哈希表、链表),必须保证线程安全
    • 如果worker线程调用,会通过RPC机制转发到主线程
  2. 确定协议类型fproto = ip_address_family_to_fib_proto (ip_addr_version (ip))

    • 根据IP地址版本(IPv4或IPv6)确定FIB协议类型
    • IPv4 → FIB_PROTOCOL_IP4
    • IPv6 → FIB_PROTOCOL_IP6
  3. 构建查找键值ip_neighbor_key_t key

    • 键值包含两个字段:IP地址和接口索引
    • 这是邻居的唯一标识,同一个IP地址在不同接口上被视为不同的邻居
5.1.2 查找现有邻居

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

c 复制代码
ipn = ip_neighbor_db_find (&key);

步骤说明

  • 调用ip_neighbor_db_find在哈希表中查找是否已存在该邻居
  • 如果找到,ipn指向现有邻居条目;如果没找到,ipnNULL

查找实现细节ip_neighbor_db_findsrc/vnet/ip-neighbor/ip_neighbor.c:292):

c 复制代码
static ip_neighbor_t *
ip_neighbor_db_find (const ip_neighbor_key_t * key)
{
  ip_address_family_t af;
  uword *p;

  af = ip_addr_version (&key->ipnk_ip);

  if (key->ipnk_sw_if_index >= vec_len (ip_neighbor_db[af].ipndb_hash))
    return NULL;

  p = hash_get_mem (ip_neighbor_db[af].ipndb_hash
		    [key->ipnk_sw_if_index], key);

  if (p)
    return ip_neighbor_get (p[0]);

  return (NULL);
}

查找流程

  1. 确定地址族:根据IP地址版本确定是IPv4还是IPv6
  2. 边界检查:检查接口索引是否超出哈希表范围
  3. 哈希查找
    • 使用hash_get_mem在对应接口的哈希表中查找
    • 哈希表结构:ip_neighbor_db[af].ipndb_hash[sw_if_index]
    • 第一级索引:地址族(IPv4/IPv6)
    • 第二级索引:接口索引
    • 第三级:哈希表本身,键值为ip_neighbor_key_t
  4. 返回结果 :如果找到,返回邻居条目指针;否则返回NULL

通俗理解:就像在一个多层的文件柜中查找文件。先确定是IPv4还是IPv6的柜子,再找到对应接口的抽屉,最后在抽屉的哈希表中查找。

5.1.3 更新现有邻居(如果已存在)

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

如果ipn != NULL,说明邻居已存在,需要更新。

步骤1:清除过期标志

c 复制代码
ip_neighbor_touch (ipn);

作用 :清除IP_NEIGHBOR_FLAG_STALE标志,表示邻居是"新鲜的"。

步骤2:静态邻居保护

c 复制代码
/* Refuse to over-write static neighbor entry. */
if (!(flags & IP_NEIGHBOR_FLAG_STATIC) &&
    (ipn->ipn_flags & IP_NEIGHBOR_FLAG_STATIC))
{
  /* if MAC address match, still check to send event */
  if (0 == mac_address_cmp (&ipn->ipn_mac, mac))
    goto check_customers;
  return -2;
}

作用:防止动态更新覆盖静态邻居。

逻辑说明

  • 如果现有邻居是静态的(手动配置),而新请求是动态的(从ARP/ND学习),则拒绝更新
  • 例外情况 :如果MAC地址相同,只是刷新时间戳,允许继续(跳转到check_customers
  • 如果MAC地址不同,返回错误-2

通俗理解:就像保护"重要联系人"一样。如果某个邻居是手动配置的(静态),就不能被自动学习的(动态)覆盖,除非MAC地址没变。

步骤3:动态转静态

c 复制代码
/* A dynamic entry can become static, but not vice-versa. */
if ((flags & IP_NEIGHBOR_FLAG_STATIC) &&
    !(ipn->ipn_flags & IP_NEIGHBOR_FLAG_STATIC))
{
  ip_neighbor_list_remove (ipn);
  ipn->ipn_flags |= IP_NEIGHBOR_FLAG_STATIC;
  ipn->ipn_flags &= ~IP_NEIGHBOR_FLAG_DYNAMIC;
}

作用:允许动态邻居升级为静态邻居,但不允许反向操作。

逻辑说明

  • 如果现有邻居是动态的,新请求是静态的,则升级为静态
  • 从链表中移除(静态邻居不参与LRU淘汰)
  • 更新标志位:设置IP_NEIGHBOR_FLAG_STATIC,清除IP_NEIGHBOR_FLAG_DYNAMIC

通俗理解:就像"临时联系人"可以升级为"重要联系人",但"重要联系人"不能降级为"临时联系人"。

步骤4:DoS防护 - MAC地址相同检查

c 复制代码
/*
 * prevent a DoS attack from the data-plane that
 * spams us with no-op updates to the MAC address
 */
if (0 == mac_address_cmp (&ipn->ipn_mac, mac))
{
  ip_neighbor_refresh (ipn);
  goto check_customers;
}

作用:防止数据平面频繁发送相同MAC地址的更新请求(DoS攻击)。

逻辑说明

  • 如果MAC地址相同,只是刷新时间戳,不更新MAC地址
  • 调用ip_neighbor_refresh刷新时间戳和链表位置
  • 跳转到check_customers,跳过MAC地址更新

通俗理解:就像防止"骚扰电话"一样。如果对方一直发送相同的信息,就只更新一下时间戳,不做其他操作。

步骤5:更新MAC地址

c 复制代码
mac_address_copy (&ipn->ipn_mac, mac);

作用:如果MAC地址不同,更新MAC地址。

5.1.4 创建新邻居(如果不存在)

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

如果ipn == NULL,说明邻居不存在,需要创建新条目。

c 复制代码
else
{
  IP_NEIGHBOR_INFO ("add: %U, %U", ...);

  ipn = ip_neighbor_alloc (&key, mac, flags);

  if (NULL == ipn)
    return VNET_API_ERROR_LIMIT_EXCEEDED;
}

步骤说明

  1. 记录日志:记录添加新邻居的日志信息
  2. 分配邻居条目 :调用ip_neighbor_alloc分配新的邻居条目
  3. 检查分配结果:如果分配失败(达到限制且不允许回收),返回错误

分配邻居条目的详细流程ip_neighbor_allocsrc/vnet/ip-neighbor/ip_neighbor.c:505):

c 复制代码
static ip_neighbor_t *
ip_neighbor_alloc (const ip_neighbor_key_t * key,
		   const mac_address_t * mac, ip_neighbor_flags_t flags)
{
  ip_address_family_t af;
  ip_neighbor_t *ipn;

  af = ip_addr_version (&key->ipnk_ip);

  // 步骤1:检查是否达到限制
  if (ip_neighbor_db[af].ipndb_limit &&
      (ip_neighbor_db[af].ipndb_n_elts >= ip_neighbor_db[af].ipndb_limit))
    {
      if (!ip_neighbor_force_reuse (af))
	return (NULL);
    }

  // 步骤2:从pool中分配内存
  pool_get_zero (ip_neighbor_pool, ipn);

  // 步骤3:分配并复制键值
  ipn->ipn_key = clib_mem_alloc (sizeof (*ipn->ipn_key));
  clib_memcpy (ipn->ipn_key, key, sizeof (*ipn->ipn_key));

  // 步骤4:初始化字段
  ipn->ipn_fib_entry_index = FIB_NODE_INDEX_INVALID;
  ipn->ipn_flags = flags;
  ipn->ipn_elt = ~0;  // 链表元素索引,~0表示未插入链表
  mac_address_copy (&ipn->ipn_mac, mac);

  // 步骤5:添加到哈希表
  ip_neighbor_db_add (ipn);

  // 步骤6:创建FIB条目(如果需要)
  if (!(ipn->ipn_flags & IP_NEIGHBOR_FLAG_NO_FIB_ENTRY))
    ip_neighbor_adj_fib_add
      (ipn, fib_table_get_index_for_sw_if_index
       (ip_address_family_to_fib_proto (af), ipn->ipn_key->ipnk_sw_if_index));

  return (ipn);
}

详细步骤说明

步骤1:检查限制

c 复制代码
if (ip_neighbor_db[af].ipndb_limit &&
    (ip_neighbor_db[af].ipndb_n_elts >= ip_neighbor_db[af].ipndb_limit))
{
  if (!ip_neighbor_force_reuse (af))
    return (NULL);
}
  • 检查是否达到最大邻居数限制(默认50000)
  • 如果达到限制:
    • 如果启用了回收(ipndb_recycle = true),调用ip_neighbor_force_reuse回收最旧的邻居
    • 如果未启用回收,返回NULL,分配失败

步骤2:分配内存

c 复制代码
pool_get_zero (ip_neighbor_pool, ipn);
  • ip_neighbor_pool中分配一个ip_neighbor_t结构体
  • pool_get_zero会将内存清零

步骤3:分配并复制键值

c 复制代码
ipn->ipn_key = clib_mem_alloc (sizeof (*ipn->ipn_key));
clib_memcpy (ipn->ipn_key, key, sizeof (*ipn->ipn_key));
  • 为键值分配独立的内存(因为键值会被用作哈希表的键)
  • 复制键值内容

步骤4:初始化字段

c 复制代码
ipn->ipn_fib_entry_index = FIB_NODE_INDEX_INVALID;  // FIB条目索引,初始为无效
ipn->ipn_flags = flags;                              // 标志位(静态/动态等)
ipn->ipn_elt = ~0;                                   // 链表元素索引,~0表示未插入
mac_address_copy (&ipn->ipn_mac, mac);               // 复制MAC地址

步骤5:添加到哈希表

c 复制代码
ip_neighbor_db_add (ipn);

添加到哈希表的详细流程ip_neighbor_db_addsrc/vnet/ip-neighbor/ip_neighbor.c:256):

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;

  // 步骤1:确保哈希表数组足够大
  vec_validate (ip_neighbor_db[af].ipndb_hash, sw_if_index);

  // 步骤2:如果该接口的哈希表不存在,创建它
  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));

  // 步骤3:将邻居插入哈希表
  hash_set_mem (ip_neighbor_db[af].ipndb_hash[sw_if_index],
		ipn->ipn_key, ip_neighbor_get_index (ipn));

  // 步骤4:增加计数
  ip_neighbor_db[af].ipndb_n_elts++;
}

详细说明

  1. 扩展数组vec_validate确保哈希表数组足够大,能容纳该接口索引
  2. 创建哈希表 :如果该接口还没有哈希表,创建一个新的哈希表
    • 键值类型:ip_neighbor_key_t
    • 值类型:index_t(邻居在pool中的索引)
  3. 插入条目 :使用hash_set_mem将邻居插入哈希表
    • 键:ipn->ipn_key(IP地址+接口索引)
    • 值:ip_neighbor_get_index (ipn)(邻居在pool中的索引)
  4. 更新计数:增加该地址族的邻居总数

步骤6:创建FIB条目

c 复制代码
if (!(ipn->ipn_flags & IP_NEIGHBOR_FLAG_NO_FIB_ENTRY))
  ip_neighbor_adj_fib_add
    (ipn, fib_table_get_index_for_sw_if_index
     (ip_address_family_to_fib_proto (af), ipn->ipn_key->ipnk_sw_if_index));
  • 如果邻居没有IP_NEIGHBOR_FLAG_NO_FIB_ENTRY标志,创建FIB条目
  • FIB条目用于路由解析,当路由指向该邻居时,可以使用该FIB条目
5.1.5 刷新邻居信息

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

无论是更新现有邻居还是创建新邻居,都需要刷新信息:

c 复制代码
/* Update time stamp and flags. */
ip_neighbor_refresh (ipn);

刷新邻居的详细流程ip_neighbor_refreshsrc/vnet/ip-neighbor/ip_neighbor.c:223):

c 复制代码
static void
ip_neighbor_refresh (ip_neighbor_t * ipn)
{
  ip_neighbor_elt_t *elt, *head;

  // 步骤1:清除过期标志
  ip_neighbor_touch (ipn);
  
  // 步骤2:更新时间和探测次数
  ipn->ipn_time_last_updated = vlib_time_now (vlib_get_main ());
  ipn->ipn_n_probes = 0;

  // 步骤3:更新链表位置(仅动态邻居)
  if (ip_neighbor_is_dynamic (ipn))
    {
      if (~0 == ipn->ipn_elt)
	/* first time insertion */
	pool_get_zero (ip_neighbor_elt_pool, elt);
      else
	{
	  /* already inserted - extract first */
	  elt = pool_elt_at_index (ip_neighbor_elt_pool, ipn->ipn_elt);
	  clib_llist_remove (ip_neighbor_elt_pool, ipne_anchor, elt);
	}
      head = pool_elt_at_index (ip_neighbor_elt_pool,
				ip_neighbor_list_head[ip_neighbor_get_af (ipn)]);

      elt->ipne_index = ip_neighbor_get_index (ipn);
      clib_llist_add (ip_neighbor_elt_pool, ipne_anchor, elt, head);
      ipn->ipn_elt = elt - ip_neighbor_elt_pool;
    }
}

详细步骤说明

  1. 清除过期标志ip_neighbor_touch (ipn)清除STALE标志
  2. 更新时间戳:记录当前时间,用于老化判断
  3. 重置探测次数ipn_n_probes = 0,表示邻居是"新鲜的"
  4. 更新链表位置 (仅动态邻居):
    • 链表的作用:维护邻居的时间顺序,最新的在头部,最旧的在尾部
    • 首次插入 :如果ipn->ipn_elt == ~0,说明还没插入链表,分配新的链表元素
    • 重新插入:如果已经在链表中,先从链表中移除,再插入到头部
    • 插入到头部clib_llist_add将元素插入到链表头部,表示这是最新的邻居

通俗理解:就像"最近联系人"列表一样。每次更新邻居时,把它移到列表最前面,表示这是最近使用的。

5.1.6 更新Adjacency

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

c 复制代码
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);

作用:更新所有指向该邻居的Adjacency,将它们的rewrite字符串更新为完整的(包含MAC地址)。

详细说明

  • adj_nbr_walk_nh:遍历所有指向该邻居的Adjacency
  • ip_neighbor_mk_complete_walk:回调函数,将Adjacency标记为完整
  • 这会触发Adjacency的rewrite字符串更新,使用barrier同步所有worker线程
5.1.7 发布事件和返回统计索引

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

c 复制代码
check_customers:
  /* Customer(s) requesting event for this address? */
  ip_neighbor_publish (ip_neighbor_get_index (ipn), IP_NEIGHBOR_EVENT_ADDED);

  if (stats_index)
    *stats_index = adj_nbr_find (fproto,
				 fib_proto_to_link (fproto),
				 &ip_addr_46 (&ipn->ipn_key->ipnk_ip),
				 ipn->ipn_key->ipnk_sw_if_index);
  return 0;

步骤说明

  1. 发布事件ip_neighbor_publish通知所有注册的监控器,邻居已添加/更新
  2. 返回统计索引 :如果stats_index不为空,查找Adjacency索引并返回
  3. 返回成功 :返回0表示成功

5.2 删除邻居(ip_neighbor_del)

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

功能:删除IP邻居条目。

通俗理解:就像从通讯录中删除联系人,需要清理所有相关信息。

5.2.1 函数入口和参数检查

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

c 复制代码
int
ip_neighbor_del (const ip_address_t * ip, u32 sw_if_index)
{
  ip_neighbor_t *ipn;

  /* main thread only */
  ASSERT (0 == vlib_get_thread_index ());

  IP_NEIGHBOR_INFO ("delete: %U, %U", ...);

  const ip_neighbor_key_t key = {
    .ipnk_ip = *ip,
    .ipnk_sw_if_index = sw_if_index,
  };

  ipn = ip_neighbor_db_find (&key);

步骤说明

  1. 线程检查:确保只在主线程执行
  2. 记录日志:记录删除操作的日志
  3. 构建查找键值:与添加函数相同
  4. 查找邻居:在哈希表中查找
5.2.2 检查邻居是否存在

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

c 复制代码
if (NULL == ipn)
  return (VNET_API_ERROR_NO_SUCH_ENTRY);

如果邻居不存在,返回错误。

5.2.3 销毁邻居

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

c 复制代码
ip_neighbor_destroy (ipn);

销毁邻居的详细流程ip_neighbor_destroysrc/vnet/ip-neighbor/ip_neighbor.c:456):

c 复制代码
static void
ip_neighbor_destroy (ip_neighbor_t * ipn)
{
  ip_address_family_t af;

  af = ip_neighbor_get_af (ipn);

  IP_NEIGHBOR_DBG ("free: %U", format_ip_neighbor, ...);

  // 步骤1:发布删除事件
  ip_neighbor_publish (ip_neighbor_get_index (ipn),
		       IP_NEIGHBOR_EVENT_REMOVED);

  // 步骤2:更新Adjacency为不完整
  adj_nbr_walk_nh (ipn->ipn_key->ipnk_sw_if_index,
		   ip_address_family_to_fib_proto (af),
		   &ip_addr_46 (&ipn->ipn_key->ipnk_ip),
		   ip_neighbor_mk_incomplete_walk, ipn);

  // 步骤3:删除FIB条目
  ip_neighbor_adj_fib_remove
    (ipn,
     fib_table_get_index_for_sw_if_index
     (ip_address_family_to_fib_proto (af), ipn->ipn_key->ipnk_sw_if_index));

  // 步骤4:从链表中移除
  ip_neighbor_list_remove (ipn);

  // 步骤5:从哈希表中移除
  ip_neighbor_db_remove (ipn);

  // 步骤6:释放键值内存
  clib_mem_free (ipn->ipn_key);

  // 步骤7:释放邻居条目
  pool_put (ip_neighbor_pool, ipn);
}

详细步骤说明

步骤1:发布删除事件

c 复制代码
ip_neighbor_publish (ip_neighbor_get_index (ipn),
		     IP_NEIGHBOR_EVENT_REMOVED);
  • 通知所有注册的监控器,邻居已删除

步骤2:更新Adjacency为不完整

c 复制代码
adj_nbr_walk_nh (ipn->ipn_key->ipnk_sw_if_index,
		 ip_address_family_to_fib_proto (af),
		 &ip_addr_46 (&ipn->ipn_key->ipnk_ip),
		 ip_neighbor_mk_incomplete_walk, ipn);
  • 遍历所有指向该邻居的Adjacency
  • 将它们的rewrite字符串更新为不完整的(用于发送ARP/ND请求)
  • 这会触发barrier同步

步骤3:删除FIB条目

c 复制代码
ip_neighbor_adj_fib_remove
  (ipn,
   fib_table_get_index_for_sw_if_index
   (ip_address_family_to_fib_proto (af), ipn->ipn_key->ipnk_sw_if_index));
  • 删除该邻居关联的FIB条目(/32/128路由)

步骤4:从链表中移除

c 复制代码
ip_neighbor_list_remove (ipn);

从链表移除的详细流程ip_neighbor_list_removesrc/vnet/ip-neighbor/ip_neighbor.c:206):

c 复制代码
static void
ip_neighbor_list_remove (ip_neighbor_t * ipn)
{
  ip_neighbor_elt_t *elt;

  if (~0 != ipn->ipn_elt)
    {
      elt = pool_elt_at_index (ip_neighbor_elt_pool, ipn->ipn_elt);
      clib_llist_remove (ip_neighbor_elt_pool, ipne_anchor, elt);
      ipn->ipn_elt = ~0;
    }
}
  • 如果邻居在链表中(ipn->ipn_elt != ~0),从链表中移除
  • ipn->ipn_elt设置为~0,表示不在链表中

步骤5:从哈希表中移除

c 复制代码
ip_neighbor_db_remove (ipn);

从哈希表移除的详细流程ip_neighbor_db_removesrc/vnet/ip-neighbor/ip_neighbor.c:277):

c 复制代码
static void
ip_neighbor_db_remove (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);

  hash_unset_mem (ip_neighbor_db[af].ipndb_hash[sw_if_index], ipn->ipn_key);

  ip_neighbor_db[af].ipndb_n_elts--;
}
  • 使用hash_unset_mem从哈希表中删除条目
  • 减少邻居总数计数

步骤6:释放键值内存

c 复制代码
clib_mem_free (ipn->ipn_key);
  • 释放键值占用的内存(在ip_neighbor_alloc中分配)

步骤7:释放邻居条目

c 复制代码
pool_put (ip_neighbor_pool, ipn);
  • 将邻居条目返回到pool中,供后续重用

5.3 查找邻居(ip_neighbor_db_find)

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

功能:根据IP地址和接口索引查找邻居。

实现方式:使用多级哈希表查找。

详细流程已在5.1.2节中说明,这里不再重复。

关键点

  • 时间复杂度:O(1)平均情况,O(n)最坏情况(哈希冲突)
  • 线程安全:只在主线程调用,不需要加锁
  • 返回值 :找到返回邻居指针,未找到返回NULL

5.4 邻居表限制和回收

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

功能:当邻居表达到限制时,回收最旧的邻居条目。

触发时机 :在ip_neighbor_alloc中,当达到限制且启用回收时调用。

详细流程ip_neighbor_force_reusesrc/vnet/ip-neighbor/ip_neighbor.c:485):

c 复制代码
static bool
ip_neighbor_force_reuse (ip_address_family_t af)
{
  // 步骤1:检查是否启用回收
  if (!ip_neighbor_db[af].ipndb_recycle)
    return false;

  // 步骤2:获取链表头
  ip_neighbor_elt_t *elt, *head;
  head = pool_elt_at_index (ip_neighbor_elt_pool, ip_neighbor_list_head[af]);

  // 步骤3:检查链表是否为空
  if (clib_llist_is_empty (ip_neighbor_elt_pool, ipne_anchor, head))
    return (false);

  // 步骤4:获取最旧的邻居(链表尾部)
  elt = clib_llist_prev (ip_neighbor_elt_pool, ipne_anchor, head);

  // 步骤5:删除最旧的邻居
  ip_neighbor_destroy (ip_neighbor_get (elt->ipne_index));

  return (true);
}

详细步骤说明

  1. 检查回收标志 :如果未启用回收(ipndb_recycle = false),返回false
  2. 获取链表头:获取该地址族的链表头节点
  3. 检查链表是否为空 :如果链表为空,无法回收,返回false
  4. 获取最旧的邻居clib_llist_prev获取链表尾部的前一个节点(最旧的邻居)
    • 链表结构:头节点 → 最新 → ... → 最旧 → 头节点(循环链表)
    • 最旧的邻居在链表尾部
  5. 删除最旧的邻居 :调用ip_neighbor_destroy删除,释放空间
  6. 返回成功 :返回true表示成功回收

通俗理解:就像"最近联系人"列表满了,需要删除最久没联系的人,为新联系人腾出空间。

配置方式

bash 复制代码
# 启用回收
set ip neighbor-config ip4 limit 10000 recycle

# 禁用回收(默认)
set ip neighbor-config ip4 limit 10000 norecycle

关键设计

  • LRU策略:使用链表维护时间顺序,最旧的邻居在尾部,方便快速淘汰
  • 仅动态邻居参与回收:静态邻居不在链表中,不会被回收
  • 原子操作:删除和分配是原子操作,保证一致性

6. 邻居学习机制

6.1 概述:什么是邻居学习?

通俗理解:邻居学习就像"认识新朋友"的过程。当VPP收到ARP响应或ND响应时,它会从响应中提取"对方是谁(IP地址)"和"对方的联系方式(MAC地址)",然后把这些信息记录到"通讯录"(邻居表)中。

学习时机

  • ARP响应:当收到ARP响应时,学习发送方的IP和MAC地址映射
  • ND响应:当收到Neighbor Advertisement(NA)时,学习目标IP和MAC地址映射
  • ND请求:当收到Neighbor Solicitation(NS)时,也可以学习发送方的IP和MAC地址映射

关键设计

  • 数据平面触发:学习请求在数据平面(worker线程)触发
  • 控制平面执行:实际的学习操作在主线程(控制平面)执行
  • RPC机制:通过RPC将学习请求从worker线程转发到主线程

6.2 数据平面学习接口(ip_neighbor_learn_dp)

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

功能:数据平面学习邻居的入口函数,这是worker线程可以安全调用的唯一接口。

通俗理解:就像"提交学习申请"一样。worker线程收到ARP/ND响应后,不能直接更新邻居表(因为线程不安全),而是通过这个函数"提交申请",让主线程来处理。

6.2.1 函数实现

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

c 复制代码
void
ip_neighbor_learn_dp (const ip_neighbor_learn_t * l)
{
  vl_api_rpc_call_main_thread (ip_neighbor_learn, (u8 *) l, sizeof (*l));
}

步骤说明

  1. 参数ip_neighbor_learn_t * l包含学习信息

    • ip:要学习的IP地址
    • mac:对应的MAC地址
    • sw_if_index:接口索引
  2. RPC调用vl_api_rpc_call_main_thread

    • 目标函数ip_neighbor_learn(主线程的学习函数)
    • 数据(u8 *) l(学习信息的字节流)
    • 数据长度sizeof (*l)(学习信息结构体的大小)

RPC机制详解vl_api_rpc_call_main_threadsrc/vlibmemory/memclnt_api.c:660):

c 复制代码
void
vl_api_rpc_call_main_thread (void *fp, u8 *data, u32 data_length)
{
  vl_api_rpc_call_main_thread_inline (fp, data, data_length, 0);
}

RPC内部实现vl_api_rpc_call_main_thread_inlinesrc/vlibmemory/memclnt_api.c:616):

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 ();

  /* Main thread and not a forced RPC: call the function directly */
  if ((force_rpc == 0) && (vlib_get_thread_index () == 0))
    {
      void (*call_fp) (void *);

      vlib_worker_thread_barrier_sync (vm);
      call_fp = fp;
      call_fp (data);
      vlib_worker_thread_barrier_release (vm);
      return;
    }

  /* Otherwise, actually do an RPC */
  mp = vl_msg_api_alloc_as_if_client (sizeof (*mp) + data_length);

  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;  // 标记需要barrier同步

  /* Add to the pending vector. Thread 0 requires locking. */
  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流程说明

  1. 检查线程

    • 如果已经在主线程(thread_index == 0)且不是强制RPC,直接调用函数
    • 否则,创建RPC消息
  2. 创建RPC消息

    • 分配消息内存:vl_msg_api_alloc_as_if_client
    • 复制数据:clib_memcpy_fast将学习信息复制到消息中
    • 设置函数指针:mp->function = pointer_to_uword (fp)
    • 标记需要barrier:mp->need_barrier_sync = 1
  3. 添加到待处理队列

    • 加锁保护(主线程需要加锁)
    • 添加到vm->pending_rpc_requests向量
    • 解锁

通俗理解:就像"提交工单"一样。worker线程把学习请求放到"待处理工单箱"中,主线程会定期处理这些工单。

6.2.2 RPC批量处理机制

源码位置src/vlibmemory/memory_api.c:923

处理时机 :主线程在主循环中定期调用vl_mem_api_handle_rpc处理待处理的RPC请求。

批量处理流程

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:交换待处理和正在处理的向量
  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请求(使用barrier同步)
  if (PREDICT_TRUE (vec_len (vm->processing_rpc_requests)))
    {
      vl_msg_api_barrier_sync ();  // Barrier同步:暂停所有worker线程
      for (i = 0; i < vec_len (vm->processing_rpc_requests); i++)
	{
	  mp = vm->processing_rpc_requests[i];
	  vl_mem_api_handler_with_vm_node (am, am->vlib_rp, (void *) mp, vm,
					   node, 0 /* is_private */);
	}
      vl_msg_api_barrier_release ();  // Barrier释放:恢复worker线程
    }

  return 0;
}

详细步骤说明

  1. 交换向量

    • pending_rpc_requests(待处理)和processing_rpc_requests(正在处理)交换
    • 这样worker线程可以继续添加新的RPC请求,而不会影响正在处理的请求
  2. Barrier同步

    • vl_msg_api_barrier_sync ():暂停所有worker线程
    • 确保在处理RPC时,worker线程不会读取正在更新的数据结构
  3. 批量处理

    • 遍历所有待处理的RPC请求
    • 调用vl_mem_api_handler_with_vm_node处理每个请求
    • 对于ip_neighbor_learn,会调用ip_neighbor_learn函数
  4. Barrier释放

    • vl_msg_api_barrier_release ():恢复所有worker线程
    • worker线程可以继续处理数据包

通俗理解:就像"批量处理工单"一样。主线程收集一批工单,然后"暂停所有工作"(barrier),一次性处理完所有工单,再"恢复工作"。

性能优化

  • 批量处理:一次barrier处理多个RPC请求,减少barrier次数
  • 向量交换:避免在处理时阻塞新的RPC请求
  • Barrier hold-down timer:如果worker线程很忙,会等待一段时间再barrier

6.3 主线程学习函数(ip_neighbor_learn)

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

功能:主线程处理邻居学习请求,这是RPC的目标函数。

通俗理解:就像"处理工单"一样。主线程收到学习请求后,调用这个函数来实际执行学习操作。

6.3.1 函数实现

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

c 复制代码
void
ip_neighbor_learn (const ip_neighbor_learn_t * l)
{
  ip_neighbor_add (&l->ip, &l->mac, l->sw_if_index,
		   IP_NEIGHBOR_FLAG_DYNAMIC, NULL);
}

步骤说明

  1. 提取学习信息 :从ip_neighbor_learn_t结构体中提取IP、MAC和接口索引
  2. 调用添加函数 :调用ip_neighbor_add添加邻居
  3. 标记为动态 :使用IP_NEIGHBOR_FLAG_DYNAMIC标志,表示这是从协议响应中学习的

关键点

  • 主线程执行:这个函数只在主线程执行(通过RPC机制保证)
  • 动态标志:学习到的邻居标记为动态,可以被老化机制清理
  • 调用链ip_neighbor_learnip_neighbor_addip_neighbor_allocip_neighbor_db_add

学习信息结构ip_neighbor_learn_tsrc/vnet/ip-neighbor/ip_neighbor_types.h:97):

c 复制代码
typedef struct ip_neighbor_learn_t_
{
  ip_address_t ip;        // 要学习的IP地址
  mac_address_t mac;      // 对应的MAC地址
  u32 sw_if_index;        // 接口索引
} ip_neighbor_learn_t;

6.4 ARP响应处理中的学习

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

功能:从ARP响应中学习邻居的IP和MAC地址映射。

通俗理解:就像"收到回信"一样。当VPP发送ARP请求后,收到ARP响应,从响应中提取对方的IP和MAC地址,然后学习这个映射关系。

6.4.1 ARP学习函数(arp_learn)

源码位置src/vnet/arp/arp.c:190-206

c 复制代码
always_inline u32
arp_learn (u32 sw_if_index,
	   const ethernet_arp_ip4_over_ethernet_address_t * addr)
{
  ip_neighbor_learn_t l = {
    .ip = {
      .ip.ip4 = addr->ip4,
      .version = AF_IP4,
    },
    .mac = addr->mac,
    .sw_if_index = sw_if_index,
  };

  ip_neighbor_learn_dp (&l);

  return (ARP_ERROR_L3_SRC_ADDRESS_LEARNED);
}

步骤说明

  1. 构建学习信息

    • 从ARP响应中提取IP地址(addr->ip4
    • 从ARP响应中提取MAC地址(addr->mac
    • 使用接收ARP响应的接口索引(sw_if_index
  2. 调用学习接口 :调用ip_neighbor_learn_dp提交学习请求

  3. 返回错误码 :返回ARP_ERROR_L3_SRC_ADDRESS_LEARNED,表示已学习到L3源地址

6.4.2 ARP响应处理流程

源码位置src/vnet/arp/arp.c:594-609

ARP输入节点处理ARP响应的流程

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;
}

详细步骤说明

  1. 检查ARP操作码

    • 如果是ARP响应(ETHERNET_ARP_OPCODE_reply),继续处理
    • 如果是ARP请求,不学习(因为请求中没有目标MAC地址)
  2. 检查目标地址

    • dst_is_local0:检查ARP响应的目标IP是否是本机或本网段的
    • 为什么要检查?:只学习对本机或本网段的ARP响应,避免学习到无关的邻居
  3. 调用学习函数

    • 如果目标地址是本地的,调用arp_learn学习发送方的IP和MAC地址
    • &arp0->ip4_over_ethernet[0]:ARP响应中发送方的IP和MAC地址
  4. 处理非本地响应

    • 如果目标地址不是本地的,可能是GARP(Gratuitous ARP)
    • 对于未知的GARP,直接丢弃

通俗理解:就像"只接受给自己的回信"一样。只有当ARP响应是发给自己的(或本网段的),才会学习发送方的信息。

学习条件总结

  • ARP响应:必须是ARP响应(不是请求)
  • 目标地址本地:响应的目标IP必须是本机或本网段的
  • MAC地址匹配:ARP响应中的MAC地址必须与以太网帧的源MAC地址匹配(在ARP输入节点中已检查)

6.5 IPv6 ND响应处理中的学习

源码位置src/vnet/ip6-nd/ip6_nd.c:145

功能:从IPv6的ND(Neighbor Discovery)消息中学习邻居的IP和MAC地址映射。

通俗理解:类似于ARP,但使用ICMPv6协议。当收到Neighbor Advertisement(NA)或Neighbor Solicitation(NS)时,可以学习邻居信息。

6.5.1 ND学习场景

场景1:从Neighbor Advertisement学习

源码位置src/vnet/ip6-nd/ip6_nd.c:145-163

c 复制代码
/* If src address unspecified or link local, donot learn neighbor MAC */
if (PREDICT_TRUE (error0 == ICMP6_ERROR_NONE && o0 != 0 &&
		  !ip6_sadd_unspecified))
{
  ip_neighbor_learn_t learn = {
    .sw_if_index = sw_if_index0,
    .ip = {
      .version = AF_IP6,
      .ip.ip6 = (is_solicitation ?
                 ip0->src_address :
                 h0->target_address),
    }
  };
  memcpy (&learn.mac, o0->ethernet_address, sizeof (learn.mac));
  ip_neighbor_learn_dp (&learn);
}

步骤说明

  1. 检查条件

    • error0 == ICMP6_ERROR_NONE:ICMPv6消息解析成功
    • o0 != 0:存在链路层地址选项(包含MAC地址)
    • !ip6_sadd_unspecified:源地址不是未指定地址(::
  2. 确定IP地址

    • 如果是NS(请求) :使用IPv6报文的源地址(ip0->src_address
    • 如果是NA(响应) :使用NA消息的目标地址(h0->target_address
  3. 提取MAC地址

    • 从链路层地址选项中提取MAC地址(o0->ethernet_address
  4. 调用学习接口 :调用ip_neighbor_learn_dp提交学习请求

场景2:从Neighbor Solicitation学习

说明:当收到NS请求时,也可以学习发送方的IP和MAC地址(如果NS中包含源链路层地址选项)。

场景3:从Router Advertisement学习

源码位置src/vnet/ip6-nd/ip6_ra.c:304

说明:当收到Router Advertisement(RA)消息时,也可以学习路由器的IP和MAC地址。

6.5.2 ND学习条件总结

学习条件

  • ICMPv6消息有效:消息解析成功,没有错误
  • 存在链路层地址选项:消息中包含MAC地址信息
  • 源地址有效 :源地址不是未指定地址(::
  • 地址类型
    • NS:使用IPv6报文的源地址
    • NA:使用NA消息的目标地址

与ARP学习的区别

特性 ARP ND
协议 ARP ICMPv6
学习时机 仅响应 响应和请求
IP地址来源 ARP响应中的发送方IP NS的源地址或NA的目标地址
MAC地址来源 ARP响应中的发送方MAC 链路层地址选项

6.6 完整学习流程

完整学习流程:从数据包接收到邻居表更新的全过程。

6.6.1 流程图
复制代码
数据包接收(Worker线程)
    ↓
ARP/ND输入节点处理
    ↓
提取IP和MAC地址
    ↓
调用 ip_neighbor_learn_dp
    ↓
创建RPC消息,添加到待处理队列
    ↓
主线程批量处理RPC(vl_mem_api_handle_rpc)
    ↓
Barrier同步:暂停所有worker线程
    ↓
调用 ip_neighbor_learn
    ↓
调用 ip_neighbor_add
    ↓
更新邻居表(哈希表、链表)
    ↓
更新Adjacency(rewrite字符串)
    ↓
Barrier释放:恢复所有worker线程
    ↓
学习完成
6.6.2 详细步骤说明

步骤1:数据包接收(Worker线程)

  • Worker线程从网卡接收ARP响应或ND响应
  • 数据包进入ARP输入节点(arp-input)或ICMPv6输入节点

步骤2:协议处理

  • ARP响应

    • 解析ARP响应包
    • 检查操作码(必须是响应)
    • 检查目标地址(必须是本地的)
    • 提取发送方的IP和MAC地址
  • ND响应

    • 解析ICMPv6消息
    • 检查消息类型(NS或NA)
    • 提取链路层地址选项
    • 确定IP地址(NS的源地址或NA的目标地址)

步骤3:调用学习接口

  • 调用ip_neighbor_learn_dp(或arp_learn
  • 构建ip_neighbor_learn_t结构体
  • 通过RPC发送到主线程

步骤4:RPC消息处理

  • Worker线程将RPC消息添加到pending_rpc_requests队列
  • 主线程在主循环中调用vl_mem_api_handle_rpc

步骤5:Barrier同步

  • 主线程调用vl_msg_api_barrier_sync ()
  • 所有worker线程暂停,等待barrier释放

步骤6:执行学习操作

  • 主线程调用ip_neighbor_learn
  • 调用ip_neighbor_add添加邻居
  • 更新哈希表和链表
  • 更新Adjacency的rewrite字符串

步骤7:Barrier释放

  • 主线程调用vl_msg_api_barrier_release ()
  • 所有worker线程恢复,可以继续处理数据包

步骤8:学习完成

  • 邻居信息已更新到邻居表
  • Adjacency已更新,数据包可以正常转发
6.6.3 时间线示例

假设场景 :Worker线程1收到ARP响应,需要学习邻居192.168.1.100aa:bb:cc:dd:ee:ff

复制代码
时间   线程        操作
----   ----        ----
T0     Worker1     接收ARP响应
T1     Worker1     解析ARP响应,提取IP和MAC
T2     Worker1     调用 ip_neighbor_learn_dp
T3     Worker1     创建RPC消息,添加到队列
T4     Worker1     继续处理其他数据包
T5     Main        调用 vl_mem_api_handle_rpc
T6     Main        Barrier同步:暂停所有worker
T7     Main        调用 ip_neighbor_learn
T8     Main        调用 ip_neighbor_add
T9     Main        更新哈希表和链表
T10    Main        更新Adjacency(rewrite字符串)
T11    Main        Barrier释放:恢复所有worker
T12    Worker1     可以继续处理数据包

关键时间点

  • T2-T3:RPC消息创建和入队(Worker线程,无阻塞)
  • T6-T11:Barrier期间(所有worker线程暂停,主线程执行学习)
  • Barrier时间:通常< 1ms,取决于需要更新的Adjacency数量

6.7 学习机制的关键设计

6.7.1 为什么使用RPC?

原因1:线程安全

  • 邻居表的更新操作(哈希表、链表)不是线程安全的
  • Worker线程不能直接更新邻居表,必须通过主线程

原因2:数据一致性

  • 使用barrier确保更新时worker线程不会读取不一致的数据
  • 保证Adjacency更新和数据包转发的原子性

原因3:性能优化

  • RPC批量处理:一次barrier处理多个学习请求
  • 减少barrier次数,提高性能
6.7.2 为什么需要Barrier?

原因1:Adjacency更新不是原子的

  • Rewrite字符串太长(128字节),无法原子更新
  • Worker线程在转发数据包时会读取rewrite字符串
  • 必须暂停worker线程,确保读取一致性

原因2:FIB条目更新

  • 邻居学习会创建FIB条目(/32/128
  • FIB更新需要barrier同步
6.7.3 学习性能考虑

优化措施

  1. 批量处理:一次barrier处理多个RPC请求
  2. 异步处理:Worker线程不阻塞,继续处理其他数据包
  3. Barrier hold-down timer:如果worker线程很忙,延迟barrier
  4. 向量交换:处理时不影响新的RPC请求入队

性能影响

  • Barrier时间:通常< 1ms
  • 学习频率:ARP/ND响应是低频操作
  • 实际影响:对数据包转发的影响很小

6.8 学习机制总结

关键函数调用链

复制代码
ip_neighbor_learn_dp (数据平面入口)
    ↓
vl_api_rpc_call_main_thread (RPC机制)
    ↓
vl_mem_api_handle_rpc (批量处理)
    ↓
vl_msg_api_barrier_sync (Barrier同步)
    ↓
ip_neighbor_learn (主线程学习函数)
    ↓
ip_neighbor_add (添加邻居)
    ↓
ip_neighbor_db_add (更新哈希表)
    ↓
ip_neighbor_refresh (更新链表)
    ↓
ip_neighbor_mk_complete (更新Adjacency)
    ↓
vl_msg_api_barrier_release (Barrier释放)

关键数据结构

  • ip_neighbor_learn_t:学习信息结构体
  • vm->pending_rpc_requests:待处理的RPC请求队列
  • vm->processing_rpc_requests:正在处理的RPC请求队列

关键设计原则

  1. 数据平面触发,控制平面执行:学习请求在worker线程触发,在主线程执行
  2. RPC机制:通过RPC实现线程间通信
  3. Barrier同步:确保数据一致性
  4. 批量处理:提高性能,减少barrier次数

7. 邻居老化机制

7.1 概述:什么是邻居老化?

通俗理解:邻居老化就像"定期清理通讯录"一样。如果某个联系人很久没有联系(超过设定的时间),系统会主动"打电话"确认他是否还在,如果连续3次都联系不上,就把他从通讯录中删除。

老化的目的

  1. 清理无效邻居:删除已经失效的邻居(设备已下线、MAC地址已变化等)
  2. 节省内存:防止邻居表无限增长
  3. 保持数据准确性:确保邻居表中的信息是最新的

老化机制的特点

  • 默认禁用 :默认情况下,老化机制是禁用的(age = 0
  • 仅动态邻居:只有动态学习的邻居参与老化,静态邻居不会被老化
  • 探测机制:过期邻居不会立即删除,而是先发送ARP/ND请求探测
  • 三次探测:如果连续3次探测都没有响应,才删除邻居

7.2 老化进程节点

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

节点说明

  • ip4-neighbor-age-process:IPv4邻居老化进程节点
  • ip6-neighbor-age-process:IPv6邻居老化进程节点

节点注册

c 复制代码
VLIB_REGISTER_NODE (ip4_neighbor_age_process_node,static) = {
  .function = ip4_neighbor_age_process,
  .type = VLIB_NODE_TYPE_PROCESS,
  .name = "ip4-neighbor-age-process",
};
VLIB_REGISTER_NODE (ip6_neighbor_age_process_node,static) = {
  .function = ip6_neighbor_age_process,
  .type = VLIB_NODE_TYPE_PROCESS,
  .name = "ip6-neighbor-age-process",
};

节点类型说明

  • VLIB_NODE_TYPE_PROCESS:进程节点,不是数据包处理节点
  • 特点:可以睡眠、等待事件、使用定时器
  • 执行时机:在主线程中执行,不参与数据包转发

通俗理解:就像"后台定时任务"一样。这个进程节点在后台运行,定期检查邻居是否过期,而不是在处理数据包时检查。

7.3 老化循环(ip_neighbor_age_loop)

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

功能:老化进程的主循环,负责定期扫描和清理过期邻居。

通俗理解:就像"定时闹钟"一样。每隔一段时间(根据配置),这个循环就会"醒来",检查邻居是否过期。

7.3.1 循环初始化

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

c 复制代码
static uword
ip_neighbor_age_loop (vlib_main_t * vm,
		      vlib_node_runtime_t * rt,
		      vlib_frame_t * f, ip_address_family_t af)
{
  uword event_type, *event_data = NULL;
  f64 timeout;

  /* Set the timeout to an effectively infinite value when the process starts */
  timeout = IP_NEIGHBOR_PROCESS_SLEEP_LONG;

步骤说明

  1. 初始化变量

    • event_type:事件类型
    • event_data:事件数据
    • timeout:超时时间
  2. 初始超时时间

    • IP_NEIGHBOR_PROCESS_SLEEP_LONG = 0:表示无限期等待
    • 如果老化未启用,进程会一直睡眠,直到收到唤醒事件
7.3.2 主循环结构

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

c 复制代码
while (1)
{
  f64 now;

  if (!timeout)
    vlib_process_wait_for_event (vm);
  else
    vlib_process_wait_for_event_or_clock (vm, timeout);

  event_type = vlib_process_get_events (vm, &event_data);
  vec_reset_length (event_data);

  now = vlib_time_now (vm);

步骤说明

  1. 等待事件或超时

    • timeout == 0 :无限期等待事件(vlib_process_wait_for_event
    • timeout > 0 :等待事件或超时(vlib_process_wait_for_event_or_clock
    • 如果超时,event_type会被设置为~0(表示定时器到期)
  2. 获取事件

    • vlib_process_get_events:获取所有待处理的事件
    • vec_reset_length:重置事件数据向量
  3. 获取当前时间

    • vlib_time_now:获取当前时间(秒,浮点数)

通俗理解:就像"等待闹钟响"一样。进程会睡眠,直到:

  • 收到唤醒事件(配置改变)
  • 定时器到期(需要检查邻居)
7.3.3 定时器到期处理(case ~0)

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

触发时机 :当定时器到期时,event_type == ~0

处理流程

c 复制代码
case ~0:
{
  /* timer expired */
  ip_neighbor_elt_t *elt, *head;
  f64 wait;

  timeout = ip_neighbor_db[af].ipndb_age;
  head = pool_elt_at_index (ip_neighbor_elt_pool,
			    ip_neighbor_list_head[af]);

  /* the list is time sorted, newest first, so start from the back
   * and work forwards. Stop when we get to one that is alive */
  restart:
  clib_llist_foreach_reverse(ip_neighbor_elt_pool,
                             ipne_anchor, head, elt,
  ({
    ip_neighbor_age_state_t res;

    res = ip_neighbour_age_out(elt->ipne_index, now, &wait);

    if (IP_NEIGHBOR_AGE_ALIVE == res) {
      /* the oldest neighbor has not yet expired, go back to sleep */
      timeout = clib_min (wait, timeout);
      break;
    }
    else if (IP_NEIGHBOR_AGE_DEAD == res) {
      /* the oldest neighbor is dead, pop it, then restart the walk
       * again from the back */
      ip_neighbor_destroy (ip_neighbor_get(elt->ipne_index));
      goto restart;
    }

    timeout = clib_min (wait, timeout);
  }));
  break;
}

详细步骤说明

步骤1:初始化超时时间

c 复制代码
timeout = ip_neighbor_db[af].ipndb_age;
  • 设置超时时间为老化时间(秒)
  • 如果老化未启用(age = 0),这个值不会被使用

步骤2:获取链表头

c 复制代码
head = pool_elt_at_index (ip_neighbor_elt_pool,
			  ip_neighbor_list_head[af]);
  • 获取该地址族的邻居链表头节点
  • 链表按时间排序:最新的在头部,最旧的在尾部

步骤3:反向遍历链表

c 复制代码
clib_llist_foreach_reverse(ip_neighbor_elt_pool,
                           ipne_anchor, head, elt,
({
  // 处理每个邻居
}));
  • 反向遍历:从链表尾部(最旧的)开始,向头部(最新的)遍历
  • 为什么反向?:最旧的邻居最可能过期,先检查它们
  • 停止条件:遇到一个"活着"的邻居就停止(因为更新的邻居肯定也活着)

步骤4:检查每个邻居

c 复制代码
res = ip_neighbour_age_out(elt->ipne_index, now, &wait);
  • 调用ip_neighbour_age_out检查邻居是否过期
  • 返回邻居状态:ALIVE(活着)、PROBE(探测中)、DEAD(已死)

步骤5:处理不同状态

  • IP_NEIGHBOR_AGE_ALIVE

    • 邻居还活着,停止遍历
    • 更新超时时间:timeout = clib_min (wait, timeout)
    • wait表示该邻居还有多久过期
  • IP_NEIGHBOR_AGE_DEAD

    • 邻居已死(探测3次失败),删除它
    • 调用ip_neighbor_destroy删除邻居
    • goto restart:重新从链表尾部开始遍历(因为删除后链表结构改变)
  • IP_NEIGHBOR_AGE_PROBE

    • 邻居过期,正在探测中
    • 更新超时时间:timeout = clib_min (wait, timeout)
    • 继续遍历下一个邻居

通俗理解:就像"检查过期食品"一样。从最旧的开始检查,如果发现一个还没过期,就停止检查(因为更新的肯定也没过期)。如果发现一个已过期且确认不能吃(探测3次失败),就扔掉它。

7.3.4 唤醒事件处理(case IP_NEIGHBOR_AGE_PROCESS_WAKEUP)

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

触发时机:当配置改变时,会发送唤醒事件。

处理流程

c 复制代码
case IP_NEIGHBOR_AGE_PROCESS_WAKEUP:
{
  if (!ip_neighbor_db[af].ipndb_age)
    {
      /* aging has been disabled */
      timeout = 0;
      break;
    }
  ip_neighbor_elt_t *elt, *head;

  head = pool_elt_at_index (ip_neighbor_elt_pool,
			    ip_neighbor_list_head[af]);
  /* no neighbors yet */
  if (clib_llist_is_empty (ip_neighbor_elt_pool, ipne_anchor, head))
    {
      timeout = ip_neighbor_db[af].ipndb_age;
      break;
    }

  /* poke the oldset neighbour for aging, which returns how long we sleep for */
  elt = clib_llist_prev (ip_neighbor_elt_pool, ipne_anchor, head);
  ip_neighbour_age_out (elt->ipne_index, now, &timeout);
  break;
}

详细步骤说明

  1. 检查老化是否启用

    • 如果ipndb_age == 0(老化已禁用),设置timeout = 0,无限期等待
  2. 检查链表是否为空

    • 如果链表为空,设置timeout = ipndb_age,等待一个老化周期
  3. 检查最旧的邻居

    • 获取链表尾部的前一个节点(最旧的邻居)
    • 调用ip_neighbour_age_out检查该邻居
    • 根据返回的wait时间设置timeout

通俗理解:就像"被叫醒后先看看最旧的食品"一样。当配置改变时,进程被唤醒,先检查最旧的邻居,然后根据情况决定下次什么时候再检查。

7.4 老化判断函数(ip_neighbour_age_out)

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

功能:判断邻居是否过期,如果过期则发送探测请求。

通俗理解:就像"检查食品是否过期"一样。如果过期了,先"尝一口"(发送探测),如果连续3次都"尝不出味道"(没有响应),就认为它"坏了"(删除)。

7.4.1 函数入口和参数

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

c 复制代码
static ip_neighbor_age_state_t
ip_neighbour_age_out (index_t ipni, f64 now, f64 * wait)
{
  ip_address_family_t af;
  ip_neighbor_t *ipn;
  u32 ipndb_age;
  u32 ttl;

  ipn = ip_neighbor_get (ipni);
  af = ip_neighbor_get_af (ipn);
  ipndb_age = ip_neighbor_db[af].ipndb_age;
  ttl = now - ipn->ipn_time_last_updated;
  *wait = ipndb_age;

步骤说明

  1. 获取邻居信息

    • ip_neighbor_get:根据索引获取邻居条目
    • ip_neighbor_get_af:获取地址族(IPv4或IPv6)
  2. 获取老化配置

    • ipndb_age:老化时间(秒)
  3. 计算存活时间(TTL)

    • ttl = now - ipn->ipn_time_last_updated
    • 当前时间减去最后更新时间,得到邻居的"年龄"
  4. 初始化等待时间

    • *wait = ipndb_age:默认等待一个老化周期
7.4.2 判断是否过期

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

c 复制代码
if (ttl > ipndb_age)
{
  IP_NEIGHBOR_DBG ("aged: %U @%f - %f > %d", format_ip_neighbor, now, ipni,
		   now, ipn->ipn_time_last_updated, ipndb_age);
  if (ipn->ipn_n_probes > 2)
    {
      /* 3 strikes and yea-re out */
      IP_NEIGHBOR_DBG ("dead: %U", format_ip_neighbor, now, ipni);
      *wait = 1;
      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;
    }
}
else
{
  /* here we are sure that ttl <= ipndb_age */
  *wait = ipndb_age - ttl + 1;
  return (IP_NEIGHBOR_AGE_ALIVE);
}

return (IP_NEIGHBOR_AGE_PROBE);

详细步骤说明

情况1:邻居未过期(ttl <= ipndb_age

c 复制代码
else
{
  *wait = ipndb_age - ttl + 1;
  return (IP_NEIGHBOR_AGE_ALIVE);
}
  • 计算剩余时间ipndb_age - ttl + 1
    • 表示该邻居还有多久过期
    • +1是为了确保在过期前检查
  • 返回状态IP_NEIGHBOR_AGE_ALIVE(邻居还活着)

情况2:邻居已过期(ttl > ipndb_age

步骤2.1:检查探测次数

c 复制代码
if (ipn->ipn_n_probes > 2)
{
  /* 3 strikes and yea-re out */
  *wait = 1;
  return (IP_NEIGHBOR_AGE_DEAD);
}
  • 探测次数检查 :如果ipn_n_probes > 2(已经探测了3次),认为邻居已死
  • 返回状态IP_NEIGHBOR_AGE_DEAD(邻居已死)
  • 等待时间*wait = 1(1秒后删除)

步骤2.2:发送探测请求

c 复制代码
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;
}
  • 发送探测 :调用ip_neighbor_probe_dst发送ARP/ND请求
  • 增加探测计数ipn_n_probes++
  • 等待时间*wait = 1(1秒后再次检查)
  • 返回状态IP_NEIGHBOR_AGE_PROBE(正在探测中)

通俗理解:就像"检查食品保质期"一样:

  • 未过期:计算还有多久过期,设置下次检查时间
  • 已过期但未探测:先"尝一口"(发送探测),1秒后再检查
  • 已过期且探测3次失败:确认"坏了",标记为已死
7.4.3 探测机制详解

探测函数ip_neighbor_probe_dstsrc/vnet/ip-neighbor/ip_neighbor.h:59

IPv4探测ip4_neighbor_probe_dstsrc/vnet/ip-neighbor/ip4_neighbor.c:59):

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;

  /* any glean will do, it's just for the rewrite */
  ai = adj_glean_get (FIB_PROTOCOL_IP4, sw_if_index, NULL);

  if (ADJ_INDEX_INVALID != ai &&
      (fib_sas4_get (sw_if_index, dst, &src) ||
       ip4_sas_by_sw_if_index (sw_if_index, dst, &src)))
    ip4_neighbor_probe (vlib_get_main (),
			vnet_get_main (), adj_get (ai), &src, dst);
}

步骤说明

  1. 获取Glean Adjacency:用于构建ARP请求的rewrite字符串
  2. 获取源IP地址:从接口配置中获取源IP地址
  3. 发送ARP请求 :调用ip4_neighbor_probe构造并发送ARP请求

IPv6探测ip6_neighbor_probe_dst):类似,但发送Neighbor Solicitation(NS)请求。

探测包构造ip4_neighbor_probesrc/vnet/ip-neighbor/ip4_neighbor.h:30):

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)
{
  // 步骤1:获取ARP包模板
  h0 = vlib_packet_template_get_packet (vm,
					&ip4_main.ip4_arp_request_packet_template,
					&bi0);

  // 步骤2:填充ARP头部
  mac_address_from_bytes (&h0->ip4_over_ethernet[0].mac, hw_if0->hw_address);
  h0->ip4_over_ethernet[0].ip4 = *src;  // 源IP
  h0->ip4_over_ethernet[1].ip4 = *dst;  // 目标IP(要探测的邻居)

  // 步骤3:发送ARP请求包
  vlib_put_frame_to_node (vm, hw_if0->output_node_index, f);
}

探测机制说明

  • 探测目的:确认邻居是否还在线
  • 探测方式:发送ARP请求(IPv4)或NS请求(IPv6)
  • 探测响应 :如果邻居在线,会发送ARP响应或NA响应,触发学习机制,重置ipn_time_last_updatedipn_n_probes
  • 探测失败:如果3次探测都没有响应,认为邻居已死

7.5 老化配置机制

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

功能:配置邻居表的老化参数。

7.5.1 配置函数(ip_neighbor_config)

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

c 复制代码
int
ip_neighbor_config (ip_address_family_t af, u32 limit, u32 age, bool recycle)
{
  ip_neighbor_db[af].ipndb_limit = limit;
  ip_neighbor_db[af].ipndb_recycle = recycle;
  ip_neighbor_db[af].ipndb_age = age;

  vlib_process_signal_event (vlib_get_main (),
			     (AF_IP4 == af ?
			      ip4_neighbor_age_process_node.index :
			      ip6_neighbor_age_process_node.index),
			     IP_NEIGHBOR_AGE_PROCESS_WAKEUP, 0);

  return (0);
}

步骤说明

  1. 更新配置

    • ipndb_limit:最大邻居数限制
    • ipndb_recycle:是否启用回收
    • ipndb_age:老化时间(秒)
  2. 唤醒老化进程

    • vlib_process_signal_event:发送唤醒事件给老化进程
    • 老化进程收到事件后,会立即检查最旧的邻居

配置参数说明

参数 说明 默认值
limit 最大邻居数 50000
age 老化时间(秒) 0(禁用)
recycle 是否回收旧邻居 false
7.5.2 CLI配置命令

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

配置命令

bash 复制代码
# 设置IPv4邻居老化配置
set ip neighbor-config ip4 limit 10000 age 300 recycle

# 设置IPv6邻居老化配置
set ip neighbor-config ip6 limit 10000 age 300 recycle

# 禁用老化
set ip neighbor-config ip4 age 0

# 查看配置
show ip neighbor-config

命令参数说明

  • limit <number>:设置最大邻居数
  • age <seconds> :设置老化时间(秒)
    • age = 0:禁用老化
    • age > 0:启用老化,邻居超过这个时间未更新就会被探测
  • recycle:启用回收(达到限制时回收最旧的邻居)
  • norecycle:禁用回收(达到限制时拒绝新邻居)

配置示例

bash 复制代码
# 示例1:启用老化,300秒未更新的邻居会被探测
set ip neighbor-config ip4 age 300

# 示例2:启用老化并设置限制和回收
set ip neighbor-config ip4 limit 5000 age 600 recycle

# 示例3:禁用老化
set ip neighbor-config ip4 age 0

7.6 完整老化流程

完整老化流程:从配置到删除的全过程。

7.6.1 流程图
复制代码
配置老化参数(ip_neighbor_config)
    ↓
唤醒老化进程(发送WAKEUP事件)
    ↓
老化进程被唤醒
    ↓
检查最旧的邻居(ip_neighbour_age_out)
    ↓
判断邻居状态
    ├─> ALIVE:计算剩余时间,设置下次检查时间
    ├─> PROBE:发送ARP/ND请求,1秒后再次检查
    └─> DEAD:删除邻居,继续检查下一个
    ↓
等待定时器到期或收到唤醒事件
    ↓
重复上述流程
7.6.2 详细时间线示例

假设场景 :配置老化时间为300秒,邻居192.168.1.100在T0时刻学习,之后一直没有更新。

复制代码
时间    事件                    操作
----    ----                   ----
T0      学习邻居                ipn_time_last_updated = T0
T0      配置老化 age=300        唤醒老化进程
T0      老化进程检查            最旧邻居:T0,ttl=0,ALIVE
T0      设置超时                timeout = 300秒
T300    定时器到期              检查最旧邻居:T0,ttl=300,过期
T300    发送第1次探测           ipn_n_probes = 1,PROBE状态
T300    设置超时                timeout = 1秒
T301    定时器到期              检查:ttl=301,过期,探测次数=1
T301    发送第2次探测           ipn_n_probes = 2,PROBE状态
T301    设置超时                timeout = 1秒
T302    定时器到期              检查:ttl=302,过期,探测次数=2
T302    发送第3次探测           ipn_n_probes = 3,PROBE状态
T302    设置超时                timeout = 1秒
T303    定时器到期              检查:ttl=303,过期,探测次数=3
T303    删除邻居                ip_neighbor_destroy,DEAD状态

关键时间点

  • T0:学习邻居,开始计时
  • T300:邻居过期,开始探测
  • T301-T302:继续探测(第2、3次)
  • T303:3次探测失败,删除邻居

如果邻居响应探测

复制代码
时间    事件                    操作
----    ----                   ----
T300    定时器到期              检查:过期
T300    发送第1次探测           ipn_n_probes = 1
T300.5  收到ARP响应             ip_neighbor_learn_dp
T300.5  更新邻居                ipn_time_last_updated = T300.5
T300.5  重置探测计数            ipn_n_probes = 0
T300.5  邻居状态                ALIVE(重新激活)
7.6.3 老化状态转换图
复制代码
        [ALIVE]
          |
          | ttl > age
          ↓
      [PROBE] (第1次探测)
          |
          | 无响应,1秒后
          ↓
      [PROBE] (第2次探测)
          |
          | 无响应,1秒后
          ↓
      [PROBE] (第3次探测)
          |
          | 无响应,1秒后
          ↓
       [DEAD] → 删除邻居

      如果收到响应:
      [PROBE] → [ALIVE] (重置时间戳和探测计数)

7.7 老化机制的关键设计

7.7.1 为什么使用进程节点而不是数据包节点?

原因1:定时器支持

  • 进程节点可以使用vlib_process_wait_for_event_or_clock等待定时器
  • 数据包节点无法使用定时器

原因2:不阻塞数据包处理

  • 老化扫描是后台任务,不应该影响数据包转发性能
  • 进程节点可以睡眠,不占用CPU资源

原因3:事件驱动

  • 可以响应配置改变事件(WAKEUP)
  • 可以响应定时器到期事件
7.7.2 为什么使用链表而不是哈希表?

原因1:时间顺序

  • 链表维护时间顺序,最新的在头部,最旧的在尾部
  • 哈希表无法维护时间顺序

原因2:高效扫描

  • 从尾部开始扫描,遇到第一个"活着"的邻居就停止
  • 不需要扫描所有邻居

原因3:LRU淘汰

  • 链表尾部是最旧的邻居,方便LRU淘汰
7.7.3 为什么需要三次探测?

原因1:网络抖动

  • 网络可能暂时不稳定,一次探测失败不代表邻居真的死了
  • 三次探测可以避免误删

原因2:协议延迟

  • ARP/ND响应可能有延迟
  • 给邻居足够的时间响应

原因3:平衡准确性和效率

  • 太少:可能误删活跃邻居
  • 太多:浪费资源,延迟清理

三次探测的设计

  • 第1次:邻居可能暂时无响应
  • 第2次:确认邻居可能有问题
  • 第3次:最终确认,删除邻居

7.8 老化机制总结

关键函数调用链

复制代码
ip_neighbor_config (配置老化参数)
    ↓
vlib_process_signal_event (唤醒老化进程)
    ↓
ip_neighbor_age_loop (老化循环)
    ↓
vlib_process_wait_for_event_or_clock (等待定时器)
    ↓
ip_neighbour_age_out (检查邻居)
    ↓
ip_neighbor_probe_dst (发送探测)
    ↓
ip4_neighbor_probe / ip6_neighbor_probe (构造探测包)
    ↓
ip_neighbor_destroy (删除失效邻居)

关键数据结构

  • ip_neighbor_list_head[af]:邻居链表头(按时间排序)
  • ipn->ipn_time_last_updated:最后更新时间
  • ipn->ipn_n_probes:探测次数
  • ip_neighbor_db[af].ipndb_age:老化时间配置

关键设计原则

  1. 仅动态邻居参与老化:静态邻居不会被老化
  2. 探测机制:过期邻居先探测,确认失效后才删除
  3. 三次探测:避免误删,平衡准确性和效率
  4. 链表优化:从最旧的开始扫描,遇到活着的就停止
  5. 事件驱动:可以响应配置改变,立即检查

8. 邻居监控机制

8.1 概述:什么是邻居监控?

通俗理解:邻居监控就像"订阅通知"一样。外部程序可以"订阅"某个邻居的变化事件,当邻居被添加或删除时,VPP会自动"推送"通知给订阅者。

监控机制的目的

  1. 实时通知:外部程序可以实时知道邻居表的变化
  2. 解耦设计:数据平面(VPP)和控制平面(外部程序)解耦
  3. 事件驱动:基于事件驱动,而不是轮询查询

监控机制的特点

  • 精确匹配:可以监控特定IP地址、特定接口,或所有邻居
  • 事件类型:支持添加事件和删除事件
  • 异步通知:通过进程节点异步处理,不阻塞数据包转发
  • 多订阅者:同一个邻居可以有多个订阅者

8.2 监控机制架构

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

架构组件

  1. 监控器数据库:存储所有注册的监控器
  2. 事件发布函数ip_neighbor_publish(在邻居添加/删除时调用)
  3. 事件处理进程ip_neighbor_event_process_node(异步处理事件)
  4. 事件处理函数ip_neighbor_handle_event(发送API消息给客户端)

架构图

复制代码
邻居添加/删除 (ip_neighbor_add/del)
    ↓
发布事件 (ip_neighbor_publish)
    ↓
查找监控器 (ipnw_db.ipnwdb_hash)
    ↓
发送事件信号 (ip_neighbor_signal)
    ↓
事件处理进程 (ip_neighbor_event_process_node)
    ↓
处理事件 (ip_neighbor_handle_event)
    ↓
发送API消息 (vl_api_send_msg)
    ↓
外部客户端接收通知

8.3 监控器数据结构

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:40

监控器结构

c 复制代码
typedef struct ip_neighbor_watcher_t_
{
  u32 ipw_pid;          // 客户端进程ID
  u32 ipw_client;       // API客户端索引
  int ipw_api_version;  // API版本(1或2)
} ip_neighbor_watcher_t;

字段说明

  • ipw_pid:客户端进程ID,用于标识订阅者
  • ipw_client:API客户端索引,用于发送消息
  • ipw_api_version:API版本号,支持v1和v2

监控器数据库

源码位置src/vnet/ip-neighbor/ip_neighbor_watch.c:33

c 复制代码
typedef struct ip_neighbor_watch_db_t_
{
  mhash_t ipnwdb_hash;  // 哈希表:key = ip_neighbor_key_t, value = watcher向量
} ip_neighbor_watch_db_t;

static ip_neighbor_watch_db_t ipnw_db;

数据库说明

  • 键(Key)ip_neighbor_key_t(IP地址 + 接口索引)
  • 值(Value)ip_neighbor_watcher_t向量(可以有多个监控器)
  • 特殊值
    • ip = 0.0.0.0:监控所有IP地址
    • sw_if_index = ~0:监控所有接口

8.4 监控事件类型

源码位置src/vnet/ip-neighbor/ip_neighbor_types.h:105

事件类型枚举

c 复制代码
typedef enum ip_neighbor_event_flags_t_
{
  IP_NEIGHBOR_EVENT_ADDED = (1 << 0),    // 邻居添加事件
  IP_NEIGHBOR_EVENT_REMOVED = (1 << 1),  // 邻居删除事件
} ip_neighbor_event_flags_t;

事件类型说明

  • IP_NEIGHBOR_EVENT_ADDED

    • 触发时机:邻居被添加或更新时
    • 调用位置:ip_neighbor_add函数末尾(src/vnet/ip-neighbor/ip_neighbor.c:630
  • IP_NEIGHBOR_EVENT_REMOVED

    • 触发时机:邻居被删除时
    • 调用位置:ip_neighbor_destroy函数中(src/vnet/ip-neighbor/ip_neighbor.c:466

8.5 注册监控器(ip_neighbor_watch)

源码位置src/vnet/ip-neighbor/ip_neighbor_watch.c:113

功能:注册一个监控器,监听特定邻居的变化。

函数签名

c 复制代码
void
ip_neighbor_watch (const ip_address_t * ip,
		   u32 sw_if_index,
                   const ip_neighbor_watcher_t * watch)

实现详解

c 复制代码
void
ip_neighbor_watch (const ip_address_t * ip,
		   u32 sw_if_index,
                   const ip_neighbor_watcher_t * watch)
{
  ip_neighbor_key_t key = {
    .ipnk_ip = *ip,
    .ipnk_sw_if_index = (sw_if_index == 0 ? ~0 : sw_if_index),
  };
  ip_neighbor_watcher_t *ipws = NULL;
  uword *p;

  // 步骤1:构造查找键
  p = mhash_get (&ipnw_db.ipnwdb_hash, &key);

  // 步骤2:如果键已存在,获取现有的监控器向量
  if (p)
    {
      ipws = (ip_neighbor_watcher_t*) p[0];

      // 步骤3:检查是否重复注册
      if (~0 != vec_search_with_function (ipws, watch,
                                          ip_neighbor_watch_cmp))
        /* duplicate */
        return;
    }

  // 步骤4:添加新的监控器到向量
  vec_add1 (ipws, *watch);

  // 步骤5:更新哈希表
  mhash_set (&ipnw_db.ipnwdb_hash, &key, (uword) ipws, NULL);
}

步骤说明

  1. 构造查找键

    • 如果sw_if_index == 0,设置为~0(表示所有接口)
  2. 查找现有监控器

    • 在哈希表中查找该键对应的监控器向量
  3. 检查重复

    • 如果监控器已存在,直接返回(避免重复注册)
  4. 添加监控器

    • 将新监控器添加到向量中
  5. 更新哈希表

    • 将更新后的向量存储回哈希表

通俗理解:就像"订阅邮件列表"一样。如果某个IP地址的"邮件列表"已存在,就把新订阅者加入列表;如果不存在,就创建一个新列表。

8.6 取消注册监控器(ip_neighbor_unwatch)

源码位置src/vnet/ip-neighbor/ip_neighbor_watch.c:142

功能:取消注册监控器,停止监听邻居变化。

函数签名

c 复制代码
void
ip_neighbor_unwatch (const ip_address_t * ip,
		     u32 sw_if_index,
                     const ip_neighbor_watcher_t * watch)

实现详解

c 复制代码
void
ip_neighbor_unwatch (const ip_address_t * ip,
		     u32 sw_if_index,
                     const ip_neighbor_watcher_t * watch)
{
  ip_neighbor_key_t key = {
    .ipnk_ip = *ip,
    .ipnk_sw_if_index = (sw_if_index == 0 ? ~0 : sw_if_index),
  };
  ip_neighbor_watcher_t *ipws = NULL;
  uword *p;
  u32 pos;

  // 步骤1:查找监控器向量
  p = mhash_get (&ipnw_db.ipnwdb_hash, &key);

  if (!p)
    return;

  ipws = (ip_neighbor_watcher_t*) p[0];

  // 步骤2:查找要删除的监控器
  pos = vec_search_with_function (ipws, watch, ip_neighbor_watch_cmp);

  if (~0 == pos)
    return;  // 未找到,直接返回

  // 步骤3:删除监控器
  vec_del1 (ipws, pos);

  // 步骤4:如果向量为空,删除哈希表项
  if (vec_len(ipws) == 0)
    mhash_unset (&ipnw_db.ipnwdb_hash, &key, NULL);
}

步骤说明

  1. 查找监控器向量:在哈希表中查找对应的监控器向量
  2. 查找要删除的监控器:在向量中查找匹配的监控器
  3. 删除监控器:从向量中删除该监控器
  4. 清理空向量:如果向量为空,删除哈希表项

8.7 发布事件(ip_neighbor_publish)

源码位置src/vnet/ip-neighbor/ip_neighbor_watch.c:192

功能:当邻居添加或删除时,发布事件通知所有相关的监控器。

函数签名

c 复制代码
void
ip_neighbor_publish (index_t ipni,
                     ip_neighbor_event_flags_t flags)

实现详解

c 复制代码
void
ip_neighbor_publish (index_t ipni,
                     ip_neighbor_event_flags_t flags)
{
  const ip_neighbor_t *ipn;
  ip_neighbor_key_t key;
  uword *p;

  // 步骤1:获取邻居信息
  ipn = ip_neighbor_get (ipni);
  clib_memcpy (&key, ipn->ipn_key, sizeof (key));

  // 步骤2:查找精确匹配的监控器(IP + 接口)
  p = mhash_get (&ipnw_db.ipnwdb_hash, &key);
  if (p) {
    ip_neighbor_signal ((ip_neighbor_watcher_t*) p[0], ipni, flags);
  }

  // 步骤3:查找接口级别的监控器(所有IP,特定接口)
  ip_address_reset (&key.ipnk_ip);
  p = mhash_get (&ipnw_db.ipnwdb_hash, &key);
  if (p) {
    ip_neighbor_signal ((ip_neighbor_watcher_t*) p[0], ipni, flags);
  }

  // 步骤4:查找全局监控器(所有IP,所有接口)
  key.ipnk_sw_if_index = ~0;
  p = mhash_get (&ipnw_db.ipnwdb_hash, &key);
  if (p) {
    ip_neighbor_signal ((ip_neighbor_watcher_t*) p[0], ipni, flags);
  }
}

步骤说明

  1. 获取邻居信息:根据索引获取邻居条目,提取键(IP + 接口)

  2. 查找精确匹配

    • 键:{IP地址, 接口索引}
    • 匹配:监控特定IP地址和特定接口的监控器
  3. 查找接口级别匹配

    • 键:{0.0.0.0, 接口索引}
    • 匹配:监控特定接口上所有IP地址的监控器
  4. 查找全局匹配

    • 键:{0.0.0.0, ~0}
    • 匹配:监控所有接口上所有IP地址的监控器

匹配优先级:从最精确到最宽泛,确保所有相关的监控器都能收到通知。

通俗理解:就像"广播通知"一样。先通知"订阅特定地址"的用户,再通知"订阅整个接口"的用户,最后通知"订阅所有"的用户。

8.8 事件信号发送(ip_neighbor_signal)

源码位置src/vnet/ip-neighbor/ip_neighbor_watch.c:173

功能:为每个监控器创建事件并发送给事件处理进程。

函数签名

c 复制代码
static void
ip_neighbor_signal (ip_neighbor_watcher_t *watchers,
                    index_t ipni,
                    ip_neighbor_event_flags_t flags)

实现详解

c 复制代码
static void
ip_neighbor_signal (ip_neighbor_watcher_t *watchers,
                    index_t ipni,
                    ip_neighbor_event_flags_t flags)
{
  ip_neighbor_watcher_t *watcher;

  vec_foreach (watcher, watchers) {
    ip_neighbor_event_t *ipne;

    // 步骤1:为每个监控器创建事件
    ipne = vlib_process_signal_event_data (vlib_get_main(),
                                           ip_neighbor_event_process_node.index,
                                           0, 1, sizeof(*ipne));
    
    // 步骤2:填充事件信息
    ipne->ipne_watch = *watcher;
    ipne->ipne_flags = flags;
    ip_neighbor_clone(ip_neighbor_get(ipni), &ipne->ipne_nbr);
  }
}

步骤说明

  1. 遍历监控器:为每个监控器创建一个事件

  2. 创建事件数据

    • vlib_process_signal_event_data:创建事件数据并发送给进程节点
    • 事件发送给ip_neighbor_event_process_node
  3. 填充事件信息

    • ipne_watch:监控器信息(客户端索引、进程ID等)
    • ipne_flags:事件类型(添加/删除)
    • ipne_nbr:邻居信息(克隆,避免引用已释放的内存)

通俗理解:就像"给每个订阅者发邮件"一样。为每个监控器创建一封"邮件"(事件),然后发送给"邮件处理中心"(事件处理进程)。

8.9 事件处理进程(ip_neighbor_event_process_node)

源码位置src/vnet/ip-neighbor/ip_neighbor_watch.c:40

功能:异步处理邻居事件,调用事件处理函数。

进程节点注册

c 复制代码
VLIB_REGISTER_NODE (ip_neighbor_event_process_node) = {
  .function = ip_neighbor_event_process,
  .type = VLIB_NODE_TYPE_PROCESS,
  .name = "ip-neighbor-event",
};

事件处理函数

c 复制代码
static uword
ip_neighbor_event_process (vlib_main_t * vm,
			   vlib_node_runtime_t * rt, vlib_frame_t * f)
{
  ip_neighbor_event_t *ipne, *ipnes = NULL;
  uword event_type = ~0;

  while (1)
    {
      // 步骤1:等待事件
      vlib_process_wait_for_event (vm);

      // 步骤2:获取事件数据
      ipnes = vlib_process_get_event_data (vm, &event_type);

      switch (event_type)
	{
	default:
	  // 步骤3:处理每个事件
	  vec_foreach (ipne, ipnes) 
	    ip_neighbor_handle_event (ipne);
	  break;

	case ~0:
	  /* timeout - */
	  break;
	}

      // 步骤4:重置事件向量
      vec_reset_length (ipnes);
    }
  return 0;
}

步骤说明

  1. 等待事件:进程睡眠,等待事件到达
  2. 获取事件数据:获取所有待处理的事件
  3. 处理事件 :遍历每个事件,调用ip_neighbor_handle_event处理
  4. 重置向量:清空事件向量,准备接收新事件

通俗理解:就像"邮件处理中心"一样。不断等待"邮件"(事件)到达,然后逐个处理。

8.10 事件处理函数(ip_neighbor_handle_event)

源码位置src/vnet/ip-neighbor/ip_neighbor_api.c:64

功能:处理单个事件,构造API消息并发送给客户端。

函数签名

c 复制代码
void
ip_neighbor_handle_event (ip_neighbor_event_t * ipne)

实现详解

c 复制代码
void
ip_neighbor_handle_event (ip_neighbor_event_t * ipne)
{
  vl_api_registration_t *reg;
  ip_neighbor_t *ipn;

  ipn = &ipne->ipne_nbr;

  if (NULL == ipn)
    /* Client can cancel, die, etc. */
    return;

  // 步骤1:获取客户端注册信息
  reg = vl_api_client_index_to_registration (ipne->ipne_watch.ipw_client);
  if (!reg)
    return;

  // 步骤2:检查是否可以发送消息
  if (vl_api_can_send_msg (reg))
    {
      if (1 == ipne->ipne_watch.ipw_api_version)
	{
	  // 步骤3:构造v1 API消息
	  vl_api_ip_neighbor_event_t *mp;
	  mp = vl_msg_api_alloc (sizeof (*mp));
	  clib_memset (mp, 0, sizeof (*mp));
	  mp->_vl_msg_id = ntohs (VL_API_IP_NEIGHBOR_EVENT + REPLY_MSG_ID_BASE);
	  mp->client_index = ipne->ipne_watch.ipw_client;
	  mp->pid = ipne->ipne_watch.ipw_pid;
	  ip_neighbor_encode (&mp->neighbor, ipn);
	  vl_api_send_msg (reg, (u8 *) mp);
	}
      else if (2 == ipne->ipne_watch.ipw_api_version)
	{
	  // 步骤4:构造v2 API消息
	  vl_api_ip_neighbor_event_v2_t *mp;
	  mp = vl_msg_api_alloc (sizeof (*mp));
	  clib_memset (mp, 0, sizeof (*mp));
	  mp->_vl_msg_id = ntohs (VL_API_IP_NEIGHBOR_EVENT_V2 + REPLY_MSG_ID_BASE);
	  mp->client_index = ipne->ipne_watch.ipw_client;
	  mp->pid = ipne->ipne_watch.ipw_pid;
	  mp->flags = clib_host_to_net_u32 (ipne->ipne_flags);
	  ip_neighbor_encode (&mp->neighbor, ipn);
	  vl_api_send_msg (reg, (u8 *) mp);
	}
    }
  else
    {
      // 步骤5:如果消息队列满,记录警告(限流)
      static f64 last_time;
      if (vlib_time_now (vlib_get_main ()) > last_time + 10.0)
	{
	  clib_warning ("neighbor event for %U to pid %d: queue stuffed!",
			format_ip_address, &ipn->ipn_key->ipnk_ip,
			ipne->ipne_watch.ipw_pid);
	  last_time = vlib_time_now (vlib_get_main ());
	}
    }

  // 步骤6:释放克隆的邻居信息
  ip_neighbor_free (ipn);
}

步骤说明

  1. 获取客户端注册:根据客户端索引获取注册信息
  2. 检查消息队列:检查是否可以发送消息
  3. 构造v1消息 :如果API版本为1,构造ip_neighbor_event消息
  4. 构造v2消息 :如果API版本为2,构造ip_neighbor_event_v2消息(包含flags)
  5. 限流处理:如果消息队列满,记录警告(每10秒一次)
  6. 释放内存:释放克隆的邻居信息

API版本区别

  • v1:只包含邻居信息,不包含事件类型标志
  • v2 :包含事件类型标志(IP_NEIGHBOR_API_EVENT_FLAG_ADDEDIP_NEIGHBOR_API_EVENT_FLAG_REMOVED

8.11 API接口

源码位置src/vnet/ip-neighbor/ip_neighbor.api:263

注册监控API

c 复制代码
autoreply define want_ip_neighbor_events_v2
{
  u32 client_index;
  u32 context;
  bool enable;          // 1=注册,0=取消注册
  u32 pid;             // 客户端进程ID
  vl_api_address_t ip; // IP地址(0.0.0.0表示所有)
  vl_api_interface_index_t sw_if_index [default=0xffffffff]; // 接口索引(~0表示所有)
};

事件通知API

c 复制代码
define ip_neighbor_event_v2
{
  u32 client_index;
  u32 pid;
  vl_api_ip_neighbor_event_flags_t flags;  // 事件类型
  vl_api_ip_neighbor_t neighbor;            // 邻居信息
};

API处理函数

源码位置src/vnet/ip-neighbor/ip_neighbor_api.c:270

c 复制代码
static void
vl_api_want_ip_neighbor_events_v2_t_handler
  (vl_api_want_ip_neighbor_events_v2_t * mp)
{
  vl_api_want_ip_neighbor_events_reply_t *rmp;
  ip_address_t ip;
  int rv = 0;

  if (mp->sw_if_index != ~0)
    VALIDATE_SW_IF_INDEX (mp);
  ip_address_decode2 (&mp->ip, &ip);

  ip_neighbor_watcher_t watch = {
    .ipw_client = mp->client_index,
    .ipw_pid = mp->pid,
    .ipw_api_version = 2,
  };

  if (mp->enable)
    ip_neighbor_watch (&ip, ntohl (mp->sw_if_index), &watch);
  else
    ip_neighbor_unwatch (&ip, ntohl (mp->sw_if_index), &watch);

  BAD_SW_IF_INDEX_LABEL;
  REPLY_MACRO (VL_API_WANT_IP_NEIGHBOR_EVENTS_V2_REPLY);
}

8.12 客户端清理机制(Reaper)

源码位置src/vnet/ip-neighbor/ip_neighbor_watch.c:76

功能:当客户端断开连接时,自动清理该客户端的所有监控器注册。

Reaper函数

c 复制代码
static clib_error_t *
want_ip_neighbor_events_reaper (u32 client_index)
{
  ip_neighbor_key_t *key, *empty_keys = NULL;
  ip_neighbor_watcher_t *watchers;
  uword *v;
  i32 pos;

  // 步骤1:遍历所有监控器注册
  mhash_foreach(key, v, &ipnw_db.ipnwdb_hash,
  ({
    watchers = (ip_neighbor_watcher_t*) *v;

    // 步骤2:删除该客户端的所有监控器
    vec_foreach_index_backwards (pos, watchers) {
      if (watchers[pos].ipw_client == client_index)
        vec_del1(watchers, pos);
    }

    // 步骤3:如果向量为空,标记为删除
    if (vec_len(watchers) == 0)
      vec_add1 (empty_keys, *key);
  }));

  // 步骤4:删除空的哈希表项
  vec_foreach (key, empty_keys)
    mhash_unset (&ipnw_db.ipnwdb_hash, key, NULL);
  vec_free (empty_keys);
  return (NULL);
}

VL_MSG_API_REAPER_FUNCTION (want_ip_neighbor_events_reaper);

步骤说明

  1. 遍历所有注册:遍历监控器数据库中的所有键值对
  2. 删除客户端监控器 :删除匹配client_index的所有监控器
  3. 标记空向量:如果向量为空,标记该键为删除
  4. 清理哈希表:删除所有空的哈希表项

通俗理解:就像"清理僵尸订阅"一样。当用户"退订"(断开连接)时,自动清理他的所有订阅记录。

8.13 完整监控流程

完整流程图

复制代码
外部客户端
    ↓
发送 want_ip_neighbor_events_v2 API
    ↓
API处理函数 (vl_api_want_ip_neighbor_events_v2_t_handler)
    ↓
注册监控器 (ip_neighbor_watch)
    ↓
存储到监控器数据库 (ipnw_db.ipnwdb_hash)
    ↓
[等待邻居变化...]
    ↓
邻居添加/删除 (ip_neighbor_add/del)
    ↓
发布事件 (ip_neighbor_publish)
    ↓
查找监控器 (3级匹配:精确/接口/全局)
    ↓
发送事件信号 (ip_neighbor_signal)
    ↓
事件处理进程 (ip_neighbor_event_process_node)
    ↓
处理事件 (ip_neighbor_handle_event)
    ↓
构造API消息 (ip_neighbor_event_v2)
    ↓
发送给客户端 (vl_api_send_msg)
    ↓
外部客户端接收通知

时间线示例

复制代码
T0: 客户端注册监控 192.168.1.100
T1: 邻居 192.168.1.100 被添加
T2: ip_neighbor_publish 发布 ADDED 事件
T3: 查找监控器(精确匹配)
T4: ip_neighbor_signal 创建事件
T5: 事件处理进程处理事件
T6: ip_neighbor_handle_event 构造消息
T7: 发送 ip_neighbor_event_v2 给客户端
T8: 客户端收到通知

8.14 VPP中需要利用邻居监控的场景

8.14.1 外部控制平面(SDN控制器)

应用场景:SDN控制器需要实时知道VPP的邻居表变化,以便:

  • 拓扑发现:发现网络中的新设备
  • 路径计算:根据邻居信息计算最优路径
  • 故障检测:检测邻居失效,触发路径切换

实现方式

python 复制代码
# Python示例:SDN控制器订阅邻居事件
import vpp_papi

# 连接VPP
vpp = vpp_papi.connect()

# 注册监控所有邻居
vpp.want_ip_neighbor_events_v2(
    enable=True,
    pid=os.getpid(),
    ip=vpp_papi.address(0, "0.0.0.0"),  # 所有IP
    sw_if_index=0xffffffff  # 所有接口
)

# 处理事件
while True:
    msg = vpp.wait_for_event()
    if msg.__class__.__name__ == "ip_neighbor_event_v2":
        if msg.flags & IP_NEIGHBOR_API_EVENT_FLAG_ADDED:
            print(f"新邻居: {msg.neighbor.ip_address}")
            # 更新拓扑数据库
            update_topology(msg.neighbor)
        elif msg.flags & IP_NEIGHBOR_API_EVENT_FLAG_REMOVED:
            print(f"邻居删除: {msg.neighbor.ip_address}")
            # 触发路径重计算
            recalculate_paths(msg.neighbor)

实际应用

  • ONOS控制器:使用邻居监控实现网络拓扑自动发现
  • OpenDaylight:通过邻居事件更新网络视图
  • 自定义SDN控制器:实现基于邻居状态的动态路由
8.14.2 网络管理工具(SNMP/NetConf)

应用场景:网络管理工具需要监控邻居状态,以便:

  • 状态监控:实时监控网络设备状态
  • 告警系统:邻居失效时触发告警
  • 性能统计:统计邻居添加/删除频率

实现方式

c 复制代码
// C示例:SNMP代理订阅邻居事件
void snmp_neighbor_monitor_init(void)
{
    vl_api_want_ip_neighbor_events_v2_t *mp;
    
    mp = vl_msg_api_alloc(sizeof(*mp));
    mp->_vl_msg_id = htons(VL_API_WANT_IP_NEIGHBOR_EVENTS_V2);
    mp->client_index = api_client_index;
    mp->enable = 1;
    mp->pid = getpid();
    mp->ip.af = ADDRESS_IP4;
    clib_memcpy(mp->ip.un.ip4, &all_zeros_ip4, sizeof(ip4_address_t));
    mp->sw_if_index = htonl(~0);  // 所有接口
    
    vl_api_send_msg(api_reg, (u8 *)mp);
}

void handle_neighbor_event(vl_api_ip_neighbor_event_v2_t *msg)
{
    if (msg->flags & IP_NEIGHBOR_API_EVENT_FLAG_ADDED) {
        // 更新SNMP MIB
        snmp_update_neighbor_table(msg->neighbor);
    } else if (msg->flags & IP_NEIGHBOR_API_EVENT_FLAG_REMOVED) {
        // 发送SNMP trap
        snmp_send_neighbor_down_trap(msg->neighbor);
    }
}

实际应用

  • SNMP代理:将邻居事件转换为SNMP trap
  • NetConf服务器:通过YANG模型暴露邻居状态
  • REST API:提供RESTful接口查询邻居状态
8.14.3 故障诊断和调试工具

应用场景:网络工程师需要实时监控邻居变化,以便:

  • 故障定位:快速定位邻居失效问题
  • 调试分析:分析邻居学习过程
  • 日志记录:记录所有邻居变化事件

实现方式

python 复制代码
# Python示例:邻居事件日志工具
class NeighborLogger:
    def __init__(self):
        self.vpp = vpp_papi.connect()
        self.log_file = open("neighbor_events.log", "w")
        
    def start_monitoring(self):
        # 监控所有邻居
        self.vpp.want_ip_neighbor_events_v2(
            enable=True,
            pid=os.getpid(),
            ip=vpp_papi.address(0, "0.0.0.0"),
            sw_if_index=0xffffffff
        )
        
    def handle_event(self, msg):
        timestamp = datetime.now().isoformat()
        if msg.flags & IP_NEIGHBOR_API_EVENT_FLAG_ADDED:
            self.log_file.write(
                f"{timestamp} ADDED: {msg.neighbor.ip_address} -> "
                f"{msg.neighbor.mac_address} on interface {msg.neighbor.sw_if_index}\n"
            )
        else:
            self.log_file.write(
                f"{timestamp} REMOVED: {msg.neighbor.ip_address} "
                f"on interface {msg.neighbor.sw_if_index}\n"
            )
        self.log_file.flush()

实际应用

  • VPP调试工具:实时显示邻居表变化
  • 网络分析工具:分析邻居学习模式
  • 故障诊断脚本:自动检测邻居异常
8.14.4 安全监控系统

应用场景:安全系统需要监控邻居变化,以便:

  • MAC地址欺骗检测:检测MAC地址变化
  • ARP攻击检测:检测异常的ARP请求
  • 设备认证:验证新设备的合法性

实现方式

c 复制代码
// C示例:安全监控系统
void security_monitor_init(void)
{
    // 监控特定关键设备的邻居
    ip4_address_t critical_ip = {.as_u8 = {192, 168, 1, 1}};
    
    vl_api_want_ip_neighbor_events_v2_t *mp;
    mp = vl_msg_api_alloc(sizeof(*mp));
    mp->_vl_msg_id = htons(VL_API_WANT_IP_NEIGHBOR_EVENTS_V2);
    mp->client_index = api_client_index;
    mp->enable = 1;
    mp->pid = getpid();
    mp->ip.af = ADDRESS_IP4;
    clib_memcpy(mp->ip.un.ip4, critical_ip.as_u8, 4);
    mp->sw_if_index = htonl(1);  // 特定接口
    
    vl_api_send_msg(api_reg, (u8 *)mp);
}

void security_check_neighbor(vl_api_ip_neighbor_event_v2_t *msg)
{
    if (msg->flags & IP_NEIGHBOR_API_EVENT_FLAG_ADDED) {
        // 检查MAC地址是否在白名单中
        if (!is_mac_whitelisted(msg->neighbor.mac_address)) {
            security_alert("Unauthorized device detected!");
            // 可以选择删除该邻居
            delete_neighbor(msg->neighbor);
        }
    }
}

实际应用

  • 入侵检测系统(IDS):检测MAC地址欺骗
  • 网络访问控制(NAC):验证设备身份
  • 安全审计系统:记录所有邻居变化
8.14.5 网络拓扑发现工具

应用场景:网络管理工具需要自动发现网络拓扑,以便:

  • 自动发现:自动发现网络中的设备
  • 拓扑映射:构建网络拓扑图
  • 设备清单:维护设备清单

实现方式

python 复制代码
# Python示例:拓扑发现工具
class TopologyDiscovery:
    def __init__(self):
        self.vpp = vpp_papi.connect()
        self.topology = {}
        
    def discover_topology(self):
        # 监控所有邻居
        self.vpp.want_ip_neighbor_events_v2(
            enable=True,
            pid=os.getpid(),
            ip=vpp_papi.address(0, "0.0.0.0"),
            sw_if_index=0xffffffff
        )
        
    def update_topology(self, msg):
        if msg.flags & IP_NEIGHBOR_API_EVENT_FLAG_ADDED:
            device = {
                'ip': msg.neighbor.ip_address,
                'mac': msg.neighbor.mac_address,
                'interface': msg.neighbor.sw_if_index,
                'timestamp': time.time()
            }
            self.topology[msg.neighbor.ip_address] = device
            self.visualize_topology()
        elif msg.flags & IP_NEIGHBOR_API_EVENT_FLAG_REMOVED:
            if msg.neighbor.ip_address in self.topology:
                del self.topology[msg.neighbor.ip_address]
                self.visualize_topology()

实际应用

  • 网络可视化工具:实时显示网络拓扑
  • CMDB系统:自动更新配置管理数据库
  • 网络监控平台:集成到监控大屏

8.15 监控机制的关键设计

8.15.1 为什么使用进程节点处理事件?

原因1:异步处理

  • 事件处理不阻塞数据包转发
  • 避免影响转发性能

原因2:批量处理

  • 可以批量处理多个事件
  • 提高处理效率

原因3:错误隔离

  • 事件处理错误不影响数据平面
  • 提高系统稳定性
8.15.2 为什么使用三级匹配(精确/接口/全局)?

原因1:灵活性

  • 支持精确监控(特定IP)
  • 支持接口级别监控(接口上所有IP)
  • 支持全局监控(所有邻居)

原因2:效率

  • 精确匹配优先,减少不必要的通知
  • 避免重复通知

原因3:扩展性

  • 可以轻松添加新的匹配级别
  • 支持复杂的监控策略
8.15.3 为什么需要Reaper机制?

原因1:资源清理

  • 客户端断开时自动清理注册
  • 避免内存泄漏

原因2:一致性

  • 确保监控器数据库的一致性
  • 避免僵尸注册

原因3:自动化

  • 不需要客户端显式取消注册
  • 简化客户端实现

8.16 监控机制总结

关键函数调用链

复制代码
外部客户端
    ↓
want_ip_neighbor_events_v2 API
    ↓
ip_neighbor_watch (注册监控器)
    ↓
[邻居变化]
    ↓
ip_neighbor_publish (发布事件)
    ↓
ip_neighbor_signal (发送信号)
    ↓
ip_neighbor_event_process_node (事件处理进程)
    ↓
ip_neighbor_handle_event (处理事件)
    ↓
vl_api_send_msg (发送API消息)
    ↓
外部客户端接收通知

关键数据结构

  • ip_neighbor_watcher_t:监控器结构
  • ip_neighbor_watch_db_t:监控器数据库
  • ip_neighbor_event_t:事件结构

关键设计原则

  1. 异步处理:使用进程节点异步处理事件
  2. 三级匹配:支持精确/接口/全局三级匹配
  3. 自动清理:Reaper机制自动清理断开连接的客户端
  4. 版本兼容:支持v1和v2两个API版本
  5. 限流保护:消息队列满时进行限流保护

9. 与Adjacency的交互

章节意义

IP_Neighbor模块与Adj模块是VPP中紧密协作的两个模块。IP_Neighbor负责管理邻居的IP地址到MAC地址的映射,而Adj负责管理数据包转发的Rewrite字符串。当IP_Neighbor学习到或更新邻居的MAC地址时,需要同步更新Adj的Rewrite字符串,确保数据包能够正确转发。

本章将详细讲解:

  • IP_Neighbor如何更新Adj的Rewrite字符串
  • 完整Adj和不完整Adj的更新机制
  • Adj FIB条目的创建和管理
  • 交互的时机和触发条件

9.1 IP_Neighbor与Adj的交互概述

作用和实现原理

IP_Neighbor模块和Adj模块的交互是双向的:

  1. IP_Neighbor → Adj:当IP_Neighbor学习到或更新邻居的MAC地址时,更新Adj的Rewrite字符串
  2. Adj → IP_Neighbor:当Adj需要MAC地址但IP_Neighbor中没有时,触发ARP/ND请求

交互的关键函数

函数 作用 调用时机
ip_neighbor_mk_complete 将Adj标记为完整(已知MAC地址) IP_Neighbor学习到MAC地址时
ip_neighbor_mk_incomplete 将Adj标记为不完整(未知MAC地址) IP_Neighbor删除或MAC地址失效时
ip_neighbor_update 根据Adj状态更新IP_Neighbor Adj需要更新时(如接口MAC变化)

通俗理解:就像"快递站和地址簿"的关系一样:

  • IP_Neighbor:就像"地址簿",记录"IP地址 → MAC地址"的映射
  • Adj:就像"快递单模板",包含"如何封装数据包"的信息
  • 交互:当地址簿更新时,同步更新快递单模板

9.2 完整Adj更新机制(ip_neighbor_mk_complete)

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

作用和实现原理

ip_neighbor_mk_complete用于将Adj标记为完整(Complete),即Adj的Rewrite字符串包含完整的MAC地址信息,可以用于数据包转发。

完整代码src/vnet/ip-neighbor/ip_neighbor.c:413):

c 复制代码
static void
ip_neighbor_mk_complete (adj_index_t ai, ip_neighbor_t * ipn)
{
    // 步骤1:构建以太网Rewrite字符串(包含目标MAC地址)
    u8 *rewrite = ethernet_build_rewrite (vnet_get_main (),
                                          ipn->ipn_key->ipnk_sw_if_index,
                                          adj_get_link_type (ai),
                                          ipn->ipn_mac.bytes);
    
    // 步骤2:更新Adj的Rewrite字符串(标记为COMPLETE)
    adj_nbr_update_rewrite (ai, ADJ_NBR_REWRITE_FLAG_COMPLETE, rewrite);
}

关键步骤详解

步骤1:构建以太网Rewrite字符串

ethernet_build_rewrite函数src/vnet/ethernet/interface.c):

作用:根据接口类型、链路类型和目标MAC地址,构建以太网Rewrite字符串。

Rewrite字符串内容

复制代码
以太网头部(14字节):
    ├─ 目标MAC地址(6字节):从ipn->ipn_mac.bytes获取
    ├─ 源MAC地址(6字节):从接口获取
    └─ 以太网类型(2字节):从链路类型获取(如0x0800表示IPv4)

示例

复制代码
目标MAC地址: AA:BB:CC:DD:EE:FF
源MAC地址: 11:22:33:44:55:66
以太网类型: 0x0800 (IPv4)

Rewrite字符串(14字节):
    [AA BB CC DD EE FF] [11 22 33 44 55 66] [08 00]
步骤2:更新Adj的Rewrite字符串

adj_nbr_update_rewrite函数 (参考VPP_Adj模块详解.md第5.2节):

作用:更新Adj的Rewrite字符串,将其从Incomplete状态转换为Complete状态。

更新流程 (参考VPP_Adj模块详解.md第9.3节):

  1. Barrier同步:暂停所有worker线程
  2. 更新Rewrite字符串:将新的Rewrite字符串复制到Adj结构中
  3. 更新VLIB图 :更新Adj的next_index,从IP_LOOKUP_NEXT_ARP变为IP_LOOKUP_NEXT_REWRITE
  4. Barrier释放:恢复所有worker线程
  5. 回退遍历:通知所有子节点Adj已更新

状态转换

复制代码
初始状态:Incomplete Adj
    ├─ lookup_next_index = IP_LOOKUP_NEXT_ARP
    ├─ Rewrite字符串 = [广播MAC地址](用于发送ARP请求)
    └─ 数据包转发:发送到ARP节点

更新后:Complete Adj
    ├─ lookup_next_index = IP_LOOKUP_NEXT_REWRITE
    ├─ Rewrite字符串 = [目标MAC地址](用于数据包转发)
    └─ 数据包转发:直接应用Rewrite字符串,发送到接口TX节点

调用时机

  1. IP_Neighbor添加时ip_neighbor_add):

    c 复制代码
    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);
  2. IP_Neighbor学习时ip_neighbor_learn):

    c 复制代码
    adj_nbr_walk_nh (ipn->ipn_key->ipnk_sw_if_index,
                     ip_address_family_to_fib_proto (ip_neighbor_get_af (ipn)),
                     &ip_addr_46 (&ipn->ipn_key->ipnk_ip),
                     ip_neighbor_mk_complete_walk, ipn);
  3. Adj更新时ip_neighbor_update):

    c 复制代码
    if (NULL != ipn)
        ip_neighbor_mk_complete (ai, ipn);

ip_neighbor_mk_complete_walk函数src/vnet/ip-neighbor/ip_neighbor.c:437):

作用:遍历所有匹配的Adj,将它们标记为完整。

c 复制代码
static adj_walk_rc_t
ip_neighbor_mk_complete_walk (adj_index_t ai, void *ctx)
{
    ip_neighbor_t *ipn = ctx;
    
    // 对每个Adj调用ip_neighbor_mk_complete
    ip_neighbor_mk_complete (ai, ipn);
    
    return (ADJ_WALK_RC_CONTINUE);
}

为什么需要遍历?

同一个IP地址可能有多个Adj(不同链路类型,如IPv4、IPv6、MPLS),需要更新所有匹配的Adj。

通俗理解:就像"更新所有相关的快递单模板"一样,当地址簿更新时,需要更新所有使用该地址的快递单模板。


9.3 不完整Adj更新机制(ip_neighbor_mk_incomplete)

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

作用和实现原理

ip_neighbor_mk_incomplete用于将Adj标记为不完整(Incomplete),即Adj的Rewrite字符串不包含目标MAC地址,需要发送ARP/ND请求来学习。

完整代码src/vnet/ip-neighbor/ip_neighbor.c:424):

c 复制代码
static void
ip_neighbor_mk_incomplete (adj_index_t ai)
{
    ip_adjacency_t *adj = adj_get (ai);

    // 步骤1:构建ARP Rewrite字符串(使用广播MAC地址)
    u8 *rewrite = ethernet_build_rewrite (vnet_get_main (),
                                          adj->rewrite_header.sw_if_index,
                                          VNET_LINK_ARP,  // 链路类型为ARP
                                          VNET_REWRITE_FOR_SW_INTERFACE_ADDRESS_BROADCAST);  // 广播地址

    // 步骤2:更新Adj的Rewrite字符串(标记为INCOMPLETE)
    adj_nbr_update_rewrite (ai,
                            ADJ_NBR_REWRITE_FLAG_INCOMPLETE,
                            rewrite);
}

关键步骤详解

步骤1:构建ARP Rewrite字符串

广播MAC地址VNET_REWRITE_FOR_SW_INTERFACE_ADDRESS_BROADCAST表示使用广播MAC地址(FF:FF:FF:FF:FF:FF)。

Rewrite字符串内容

复制代码
以太网头部(14字节):
    ├─ 目标MAC地址(6字节):FF:FF:FF:FF:FF:FF(广播地址)
    ├─ 源MAC地址(6字节):从接口获取
    └─ 以太网类型(2字节):0x0806(ARP)或0x86DD(IPv6 ND)

为什么使用广播地址?

ARP/ND请求需要发送到广播地址,以便同一网段内的所有主机都能收到。

步骤2:更新Adj的Rewrite字符串

状态转换

复制代码
初始状态:Complete Adj
    ├─ lookup_next_index = IP_LOOKUP_NEXT_REWRITE
    ├─ Rewrite字符串 = [目标MAC地址](用于数据包转发)
    └─ 数据包转发:直接应用Rewrite字符串

更新后:Incomplete Adj
    ├─ lookup_next_index = IP_LOOKUP_NEXT_ARP
    ├─ Rewrite字符串 = [广播MAC地址](用于发送ARP请求)
    └─ 数据包转发:发送到ARP节点,触发ARP请求

调用时机

  1. IP_Neighbor删除时ip_neighbor_destroy):

    c 复制代码
    adj_nbr_walk_nh (ipn->ipn_key->ipnk_sw_if_index,
                     ip_address_family_to_fib_proto (af),
                     &ip_addr_46 (&ipn->ipn_key->ipnk_ip),
                     ip_neighbor_mk_incomplete_walk, ipn);
  2. MAC地址失效时:当邻居的MAC地址失效或变化时,将Adj标记为不完整。

ip_neighbor_mk_incomplete_walk函数src/vnet/ip-neighbor/ip_neighbor.c:447):

作用:遍历所有匹配的Adj,将它们标记为不完整。

c 复制代码
static adj_walk_rc_t
ip_neighbor_mk_incomplete_walk (adj_index_t ai, void *ctx)
{
    // 对每个Adj调用ip_neighbor_mk_incomplete
    ip_neighbor_mk_incomplete (ai);
    
    return (ADJ_WALK_RC_CONTINUE);
}

通俗理解:就像"快递单模板失效"一样,当地址簿中的地址失效时,需要将快递单模板标记为"需要重新查询地址"。


9.4 Adj FIB条目的创建和管理

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

作用和实现原理

当IP_Neighbor添加一个邻居时,会为该邻居创建一个FIB条目(/32用于IPv4,/128用于IPv6),该FIB条目指向该邻居的Adj。这样,当路由解析时,如果匹配到该FIB条目,会直接使用该Adj进行转发。

9.4.1 FIB条目创建(ip_neighbor_adj_fib_add)

完整代码src/vnet/ip-neighbor/ip_neighbor.c:350):

c 复制代码
static void
ip_neighbor_adj_fib_add (ip_neighbor_t * ipn, u32 fib_index)
{
    ip_address_family_t af;
    fib_protocol_t fproto;

    af = ip_neighbor_get_af (ipn);
    fproto = ip_address_family_to_fib_proto (af);

    // 步骤1:构造FIB前缀(/32用于IPv4,/128用于IPv6)
    if (AF_IP6 == af &&
        ip6_address_is_link_local_unicast (&ip_addr_v6 (&ipn->ipn_key->ipnk_ip)))
    {
        // IPv6链路本地地址:使用特殊的链路本地前缀表
        ip6_ll_prefix_t pfx = {
            .ilp_addr = ip_addr_v6 (&ipn->ipn_key->ipnk_ip),
            .ilp_sw_if_index = ipn->ipn_key->ipnk_sw_if_index,
        };
        ip6_ll_table_entry_add (&pfx);
    }
    else
    {
        // 普通地址:创建/32或/128的FIB条目
        fib_prefix_t pfx = {
            .fp_len = ip_af_type_pfx_len (af),  // 32(IPv4)或128(IPv6)
            .fp_proto = fproto,
            .fp_addr = ip_addr_46 (&ipn->ipn_key->ipnk_ip),
        };

        // 步骤2:添加FIB条目路径
        fib_table_entry_path_add (fib_index,
                                  &pfx,
                                  FIB_SOURCE_ADJ,  // 来源:Adj
                                  fib_proto_to_dpo (fproto),
                                  &pfx.fp_addr,    // 下一跳地址
                                  ipn->ipn_key->ipnk_sw_if_index,  // 接口索引
                                  ~0, 1, FIB_ROUTE_PATH_FLAG_NONE);
    }

    // 步骤3:锁定FIB表(防止删除)
    if (0 == ip_neighbor_db[af].ipndb_n_elts_per_fib[fib_index])
    {
        fib_table_lock (fib_index, fproto, FIB_SOURCE_ADJ);
    }
    
    // 步骤4:更新统计计数
    ip_neighbor_db[af].ipndb_n_elts_per_fib[fib_index]++;
}

关键步骤详解

  1. 构造FIB前缀

    • IPv4:/32(主机路由)
    • IPv6:/128(主机路由)
    • IPv6链路本地地址:使用特殊的链路本地前缀表
  2. 添加FIB条目路径

    • 来源FIB_SOURCE_ADJ(表示该FIB条目由Adj模块创建)
    • 下一跳地址:邻居的IP地址
    • 接口索引:邻居所在的接口
  3. 锁定FIB表:防止FIB表被删除

  4. 更新统计计数:记录该FIB表中的邻居数量

调用时机src/vnet/ip-neighbor/ip_neighbor.c:505):

c 复制代码
static ip_neighbor_t *
ip_neighbor_alloc (const ip_neighbor_key_t * key,
                   const mac_address_t * mac, ip_neighbor_flags_t flags)
{
    // ... 分配和初始化 ...
    
    // 创建FIB条目
    ip_neighbor_adj_fib_add
        (ipn,
         fib_table_get_index_for_sw_if_index
         (ip_address_family_to_fib_proto (ip_neighbor_get_af (ipn)),
          ipn->ipn_key->ipnk_sw_if_index));
    
    // ...
}
9.4.2 FIB条目删除(ip_neighbor_adj_fib_remove)

完整代码src/vnet/ip-neighbor/ip_neighbor.c:365):

c 复制代码
static void
ip_neighbor_adj_fib_remove (ip_neighbor_t * ipn, u32 fib_index)
{
    ip_address_family_t af;
    fib_protocol_t fproto;

    af = ip_neighbor_get_af (ipn);

    if (FIB_NODE_INDEX_INVALID != ipn->ipn_fib_entry_index)
    {
        if (AF_IP6 == af &&
            ip6_address_is_link_local_unicast (&ip_addr_v6 (&ipn->ipn_key->ipnk_ip)))
        {
            // IPv6链路本地地址:从链路本地前缀表删除
            ip6_ll_prefix_t pfx = {
                .ilp_addr = ip_addr_v6 (&ipn->ipn_key->ipnk_ip),
                .ilp_sw_if_index = ipn->ipn_key->ipnk_sw_if_index,
            };
            ip6_ll_table_entry_delete (&pfx);
        }
        else
        {
            // 普通地址:删除FIB条目路径
            fproto = ip_address_family_to_fib_proto (af);

            fib_prefix_t pfx = {
                .fp_len = ip_af_type_pfx_len (af),
                .fp_proto = fproto,
                .fp_addr = ip_addr_46 (&ipn->ipn_key->ipnk_ip),
            };

            fib_table_entry_path_remove (fib_index,
                                         &pfx,
                                         FIB_SOURCE_ADJ,
                                         fib_proto_to_dpo (fproto),
                                         &pfx.fp_addr,
                                         ipn->ipn_key->ipnk_sw_if_index,
                                         ~0, 1, FIB_ROUTE_PATH_FLAG_NONE);
        }

        // 更新统计计数
        ip_neighbor_db[af].ipndb_n_elts_per_fib[fib_index]--;

        // 如果FIB表中没有邻居了,解锁FIB表
        if (0 == ip_neighbor_db[af].ipndb_n_elts_per_fib[fib_index])
        {
            fib_table_unlock (fib_index, fproto, FIB_SOURCE_ADJ);
        }
    }
}

调用时机src/vnet/ip-neighbor/ip_neighbor.c:456):

c 复制代码
static void
ip_neighbor_destroy (ip_neighbor_t * ipn)
{
    // ... 其他清理工作 ...
    
    // 删除FIB条目
    ip_neighbor_adj_fib_remove
        (ipn,
         fib_table_get_index_for_sw_if_index
         (ip_address_family_to_fib_proto (af), ipn->ipn_key->ipnk_sw_if_index));
    
    // ...
}

通俗理解:就像"在地址簿中添加地址时,同时在快递站的路由表中添加一条直达路线"一样,当IP_Neighbor添加邻居时,会在FIB中创建一条主机路由,指向该邻居的Adj。


9.5 Adj触发IP_Neighbor更新(ip_neighbor_update)

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

作用和实现原理

ip_neighbor_update是Adj模块调用IP_Neighbor模块的函数,用于根据Adj的状态更新IP_Neighbor。这通常发生在接口MAC地址变化等场景。

完整代码src/vnet/ip-neighbor/ip_neighbor.c:702):

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

    adj = adj_get (ai);

    // 步骤1:构造查找键值
    ip_neighbor_key_t key = {
        .ipnk_sw_if_index = adj->rewrite_header.sw_if_index,
    };
    ip_address_from_46 (&adj->sub_type.nbr.next_hop, &key.ipnk_ip);

    // 步骤2:查找IP_Neighbor
    ipn = ip_neighbor_db_find (&key);

    // 步骤3:根据Adj状态处理
    switch (adj->lookup_next_index)
    {
    case IP_LOOKUP_NEXT_ARP:
        // Adj是Incomplete状态
        if (NULL != ipn)
        {
            // 如果IP_Neighbor中有该邻居,更新Adj为Complete
            adj_nbr_walk_nh (adj->rewrite_header.sw_if_index,
                             adj->ia_nh_proto,
                             &adj->sub_type.nbr.next_hop,
                             ip_neighbor_mk_complete_walk, ipn);
        }
        else
        {
            // 如果IP_Neighbor中没有该邻居,保持Incomplete状态,并发送ARP请求
            adj_nbr_update_rewrite
                (ai,
                 ADJ_NBR_REWRITE_FLAG_INCOMPLETE,
                 ethernet_build_rewrite
                 (vnm,
                  adj->rewrite_header.sw_if_index,
                  VNET_LINK_ARP,
                  VNET_REWRITE_FOR_SW_INTERFACE_ADDRESS_BROADCAST));

            // 发送推测性ARP请求
            adj = adj_get (ai);
            ip_neighbor_probe (adj);
        }
        break;
        
    case IP_LOOKUP_NEXT_REWRITE:
        // Adj是Complete状态
        if (NULL != ipn)
        {
            // 如果IP_Neighbor中有该邻居,更新Adj的Rewrite字符串(如接口MAC变化)
            ip_neighbor_mk_complete (ai, ipn);
        }
        break;
        
    default:
        ASSERT (0);
        break;
    }
}

处理场景

  1. Adj是Incomplete状态IP_LOOKUP_NEXT_ARP):

    • 有IP_Neighbor:更新Adj为Complete
    • 无IP_Neighbor:保持Incomplete状态,发送ARP请求
  2. Adj是Complete状态IP_LOOKUP_NEXT_REWRITE):

    • 有IP_Neighbor:更新Adj的Rewrite字符串(如接口MAC变化)

调用时机

  • 接口MAC地址变化时:当接口的MAC地址变化时,需要更新所有Adj的Rewrite字符串
  • Adj需要更新时:当Adj需要更新但不知道如何更新时,调用此函数

通俗理解:就像"快递单模板需要更新时,查询地址簿"一样,当Adj需要更新时,会查询IP_Neighbor,根据IP_Neighbor的状态更新Adj。


9.6 完整交互流程示例

示例1:IP_Neighbor学习到MAC地址
复制代码
步骤1:ARP响应到达
    └─ 数据包进入ip4-arp节点
    └─ 提取源IP和源MAC地址
    ↓
步骤2:IP_Neighbor学习
    └─ 调用ip_neighbor_learn_dp()(数据平面)
    └─ RPC调用ip_neighbor_learn()(主线程)
    └─ 调用ip_neighbor_add()添加邻居
    ↓
步骤3:创建FIB条目
    └─ 调用ip_neighbor_adj_fib_add()
    └─ 创建/32的FIB条目,指向该邻居的Adj
    ↓
步骤4:更新Adj为Complete
    └─ 调用adj_nbr_walk_nh()遍历所有匹配的Adj
    └─ 对每个Adj调用ip_neighbor_mk_complete_walk()
    └─ 调用ip_neighbor_mk_complete()
    └─ 构建Rewrite字符串(包含目标MAC地址)
    └─ 调用adj_nbr_update_rewrite()更新Adj
    └─ Barrier同步,更新Rewrite字符串
    └─ 将Adj从IP_LOOKUP_NEXT_ARP变为IP_LOOKUP_NEXT_REWRITE
    ↓
步骤5:后续数据包转发
    └─ FIB查找返回Adj索引
    └─ 应用Rewrite字符串(包含目标MAC地址)
    └─ 直接发送到接口TX节点
示例2:IP_Neighbor删除邻居
复制代码
步骤1:IP_Neighbor删除
    └─ 调用ip_neighbor_del()
    └─ 调用ip_neighbor_destroy()
    ↓
步骤2:更新Adj为Incomplete
    └─ 调用adj_nbr_walk_nh()遍历所有匹配的Adj
    └─ 对每个Adj调用ip_neighbor_mk_incomplete_walk()
    └─ 调用ip_neighbor_mk_incomplete()
    └─ 构建ARP Rewrite字符串(使用广播MAC地址)
    └─ 调用adj_nbr_update_rewrite()更新Adj
    └─ Barrier同步,更新Rewrite字符串
    └─ 将Adj从IP_LOOKUP_NEXT_REWRITE变为IP_LOOKUP_NEXT_ARP
    ↓
步骤3:删除FIB条目
    └─ 调用ip_neighbor_adj_fib_remove()
    └─ 删除/32的FIB条目
    ↓
步骤4:后续数据包转发
    └─ FIB查找返回Adj索引
    └─ 数据包发送到ARP节点
    └─ 触发ARP请求

9.7 交互机制总结

IP_Neighbor与Adj交互的关键特点

  1. 双向交互

    • IP_Neighbor → Adj:更新Adj的Rewrite字符串
    • Adj → IP_Neighbor:触发ARP/ND请求
  2. 状态同步

    • Complete状态:Rewrite字符串包含目标MAC地址
    • Incomplete状态:Rewrite字符串使用广播MAC地址
  3. FIB条目管理

    • 每个邻居创建一个/32/128的FIB条目
    • FIB条目指向该邻居的Adj
  4. Barrier同步

    • 更新Rewrite字符串时使用Barrier同步,确保数据一致性

交互的关键函数调用链

复制代码
IP_Neighbor添加/更新:
    ip_neighbor_add()
    └─ ip_neighbor_adj_fib_add()  // 创建FIB条目
    └─ adj_nbr_walk_nh()
        └─ ip_neighbor_mk_complete_walk()
            └─ ip_neighbor_mk_complete()
                └─ ethernet_build_rewrite()  // 构建Rewrite字符串
                └─ adj_nbr_update_rewrite()  // 更新Adj(Barrier同步)

IP_Neighbor删除:
    ip_neighbor_del()
    └─ ip_neighbor_destroy()
        └─ adj_nbr_walk_nh()
            └─ ip_neighbor_mk_incomplete_walk()
                └─ ip_neighbor_mk_incomplete()
                    └─ ethernet_build_rewrite()  // 构建ARP Rewrite字符串
                    └─ adj_nbr_update_rewrite()  // 更新Adj(Barrier同步)
        └─ ip_neighbor_adj_fib_remove()  // 删除FIB条目


10. IPv4 ARP处理

章节意义

IP_Neighbor模块在ARP处理中起到关键作用。它负责:

  1. 触发ARP请求:当需要学习邻居MAC地址时,构造并发送ARP请求
  2. 处理ARP响应:从ARP响应中提取邻居信息,学习MAC地址
  3. 更新转发平面:将学习到的MAC地址更新到Adj的Rewrite字符串中

本章将详细讲解:

  • ARP节点(ip4-arp)和Glean节点(ip4-glean)的作用
  • ARP请求的构造和发送(ip4_neighbor_probe
  • ARP响应的处理和邻居学习(arp_learn
  • ARP限流机制

10.1 IP_Neighbor在ARP中的整体作用

作用和实现原理

IP_Neighbor模块在ARP处理中扮演"学习者和更新者"的角色:

  1. 触发ARP请求:当数据包需要转发但不知道目标MAC地址时,触发ARP请求
  2. 学习邻居信息:从ARP响应中提取源IP和源MAC地址,学习邻居信息
  3. 更新转发平面:将学习到的MAC地址更新到Adj的Rewrite字符串中

完整流程

复制代码
步骤1:数据包需要转发
    └─ FIB查找返回Incomplete Adj或Glean Adj
    └─ 数据包进入ip4-arp节点或ip4-glean节点
    ↓
步骤2:触发ARP请求
    └─ 调用ip4_neighbor_probe()构造ARP请求
    └─ 发送ARP请求到广播地址
    └─ 丢弃原始数据包(等待ARP响应)
    ↓
步骤3:收到ARP响应
    └─ ARP响应进入arp-input节点
    └─ 调用arp_learn()提取邻居信息
    └─ 调用ip_neighbor_learn_dp()(数据平面)
    └─ RPC调用ip_neighbor_learn()(主线程)
    ↓
步骤4:学习邻居信息
    └─ 调用ip_neighbor_add()添加邻居
    └─ 创建FIB条目
    └─ 更新Adj为Complete状态
    ↓
步骤5:后续数据包转发
    └─ FIB查找返回Complete Adj
    └─ 应用Rewrite字符串(包含目标MAC地址)
    └─ 直接转发

通俗理解:就像"快递员查询地址"一样:

  • 触发查询:当不知道地址时,发送查询请求
  • 收到回复:收到地址回复,记录到地址簿
  • 更新快递单:将地址更新到快递单模板中
  • 后续使用:后续快递直接使用更新后的模板

10.2 ARP节点(ip4-arp)

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

作用和实现原理

ip4-arp节点处理命中Incomplete Adj的数据包。当数据包需要转发但Adj的Rewrite字符串不包含目标MAC地址时,数据包会被发送到ip4-arp节点,触发ARP请求。

完整代码流程src/vnet/ip-neighbor/ip4_neighbor.c:137):

c 复制代码
always_inline uword
ip4_arp_inline (vlib_main_t * vm,
                vlib_node_runtime_t * node,
                vlib_frame_t * frame, int is_glean)
{
    vnet_main_t *vnm = vnet_get_main ();
    u32 *from, *to_next_drop;
    uword n_left_from, n_left_to_next_drop, next_index;
    u32 thread_index = vm->thread_index;
    u64 seed;

    // 步骤1:初始化限流种子
    seed = throttle_seed (&arp_throttle, thread_index, vlib_time_now (vm));

    from = vlib_frame_vector_args (frame);
    n_left_from = frame->n_vectors;

    while (n_left_from > 0)
    {
        vlib_get_next_frame (vm, node, IP4_ARP_NEXT_DROP,
                             to_next_drop, n_left_to_next_drop);

        while (n_left_from > 0 && n_left_to_next_drop > 0)
        {
            u32 pi0, adj_index0, sw_if_index0;
            ip4_address_t resolve0, src0;
            vlib_buffer_t *p0, *b0;
            ip_adjacency_t *adj0;
            u64 r0;

            pi0 = from[0];
            p0 = vlib_get_buffer (vm, pi0);

            from += 1;
            n_left_from -= 1;
            to_next_drop[0] = pi0;
            to_next_drop += 1;
            n_left_to_next_drop -= 1;

            // 步骤2:获取Adj
            adj_index0 = vnet_buffer (p0)->ip.adj_index[VLIB_TX];
            adj0 = adj_get (adj_index0);
            sw_if_index0 = adj0->rewrite_header.sw_if_index;

            // 步骤3:确定要解析的IP地址
            if (is_glean)
            {
                // Glean节点:解析数据包的目标IP地址
                ip4_header_t *ip0 = vlib_buffer_get_current (p0);
                resolve0 = ip0->dst_address;
            }
            else
            {
                // ARP节点:解析Incomplete Adj的下一跳地址
                resolve0 = adj0->sub_type.nbr.next_hop.ip4;
            }

            // 步骤4:获取源IP地址
            if (is_glean && adj0->sub_type.glean.rx_pfx.fp_len)
            {
                // Glean节点:从连接前缀获取源IP
                src0 = adj0->sub_type.glean.rx_pfx.fp_addr.ip4;
            }
            else
            {
                // ARP节点:从接口配置获取源IP
                if (!fib_sas4_get (sw_if_index0, &resolve0, &src0) &&
                    !ip4_sas_by_sw_if_index (sw_if_index0, &resolve0, &src0))
                {
                    // 没有可用的源地址
                    p0->error = node->errors[IP4_NEIGHBOR_ERROR_NO_SOURCE_ADDRESS];
                    continue;
                }
            }

            // 步骤5:限流检查
            r0 = (u64) resolve0.data_u32 << 32;
            r0 |= sw_if_index0;

            if (throttle_check (&arp_throttle, thread_index, r0, seed))
            {
                p0->error = node->errors[IP4_NEIGHBOR_ERROR_THROTTLED];
                continue;
            }

            // 步骤6:检查Adj状态
            if (IP_LOOKUP_NEXT_REWRITE == adj0->lookup_next_index)
            {
                // Adj已经更新为Complete,不需要ARP请求
                p0->error = node->errors[IP4_NEIGHBOR_ERROR_RESOLVED];
                continue;
            }

            if ((is_glean && adj0->lookup_next_index != IP_LOOKUP_NEXT_GLEAN) ||
                (!is_glean && adj0->lookup_next_index != IP_LOOKUP_NEXT_ARP))
            {
                // Adj类型不匹配
                p0->error = node->errors[IP4_NEIGHBOR_ERROR_NON_ARP_ADJ];
                continue;
            }

            // 步骤7:发送ARP请求
            b0 = ip4_neighbor_probe (vm, vnm, adj0, &src0, &resolve0);

            if (PREDICT_TRUE (NULL != b0))
            {
                // 复制持久字段(用于跟踪)
                clib_memcpy_fast (b0->opaque2, p0->opaque2,
                                  sizeof (p0->opaque2));
                p0->error = node->errors[IP4_NEIGHBOR_ERROR_REQUEST_SENT];
            }
            else
            {
                p0->error = node->errors[IP4_NEIGHBOR_ERROR_NO_BUFFERS];
                continue;
            }
        }

        vlib_put_next_frame (vm, node, IP4_ARP_NEXT_DROP, n_left_to_next_drop);
    }

    return frame->n_vectors;
}

关键步骤详解

步骤1:确定要解析的IP地址

Glean节点is_glean = 1):

  • 目标IP :从数据包的IP头部提取(ip0->dst_address
  • 源IP :从Glean Adj的连接前缀获取(adj0->sub_type.glean.rx_pfx.fp_addr.ip4

ARP节点is_glean = 0):

  • 目标IP :从Incomplete Adj的下一跳地址获取(adj0->sub_type.nbr.next_hop.ip4
  • 源IP :从接口配置获取(fib_sas4_getip4_sas_by_sw_if_index

通俗理解:就像"确定要查询的地址"一样:

  • Glean节点:查询"数据包要发送到的地址"
  • ARP节点:查询"Incomplete Adj的下一跳地址"
步骤2:限流检查

限流机制src/vnet/ip-neighbor/ip4_neighbor.c:215):

c 复制代码
if (throttle_check (&arp_throttle, thread_index, r0, seed))
{
    p0->error = node->errors[IP4_NEIGHBOR_ERROR_THROTTLED];
    continue;
}

作用:防止ARP请求风暴,限制同一目标的ARP请求频率。

限流键值(目标IP地址 << 32) | 接口索引

限流初始化src/vnet/ip-neighbor/ip4_neighbor.c:328):

c 复制代码
static clib_error_t *
ip4_neighbor_main_loop_enter (vlib_main_t * vm)
{
    vlib_thread_main_t *tm = &vlib_thread_main;
    u32 n_vlib_mains = tm->n_vlib_mains;

    throttle_init (&arp_throttle, n_vlib_mains, THROTTLE_BITS, 1e-3);

    return (NULL);
}

限流参数

  • THROTTLE_BITS:限流位宽(通常为10,即1024个桶)
  • 1e-3:限流时间窗口(1毫秒)

通俗理解:就像"限制查询频率"一样,防止对同一个地址频繁发送查询请求。

步骤3:检查Adj状态

状态检查

  1. Adj已更新 :如果Adj已经更新为Complete(IP_LOOKUP_NEXT_REWRITE),不需要ARP请求
  2. Adj类型匹配 :检查Adj类型是否匹配(Glean节点需要IP_LOOKUP_NEXT_GLEAN,ARP节点需要IP_LOOKUP_NEXT_ARP

通俗理解:就像"检查地址是否已经查询过"一样,如果地址已经知道,就不需要再查询。

步骤4:发送ARP请求

调用ip4_neighbor_probe:构造并发送ARP请求包(详见10.3节)。


10.3 Glean节点(ip4-glean)

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

作用和实现原理

ip4-glean节点处理命中Glean Adj的数据包。当FIB查找返回Glean类型的Adj时,数据包会被发送到ip4-glean节点,触发ARP请求。

与ARP节点的区别

差异项 Glean节点 ARP节点 说明
触发条件 FIB查找返回Glean Adj FIB查找返回Incomplete Adj Glean用于连接前缀,ARP用于特定下一跳
目标IP 从数据包IP头部提取 从Adj的下一跳地址获取 Glean使用数据包的目标IP
源IP 从Glean Adj的连接前缀获取 从接口配置获取 Glean使用连接前缀的地址

通俗理解:就像"两种查询方式"一样:

  • Glean节点:查询"数据包要发送到的地址"(用于连接前缀)
  • ARP节点:查询"路由的下一跳地址"(用于特定路由)

使用场景

  1. 连接前缀 :当接口配置了连接前缀(如192.168.1.0/24)时,该前缀的Glean Adj会触发Glean节点
  2. 同一网段:当数据包要发送到同一网段内的主机时,使用Glean节点

10.4 ARP探测(ip4_neighbor_probe)

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

作用和实现原理

ip4_neighbor_probe用于构造并发送ARP请求包。它从ARP包模板中获取ARP包,填充ARP头部,构建以太网头部,然后发送到输出接口。

完整代码src/vnet/ip-neighbor/ip4_neighbor.h:29):

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:应用Rewrite字符串(以太网头部)
    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地址
    h0->ip4_over_ethernet[1].ip4 = *dst;
    // 目标MAC地址:在ARP请求中为0(待填充)

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

    // 步骤6:调整缓冲区指针(为Rewrite字符串预留空间)
    vlib_buffer_advance (b0, -adj0->rewrite_header.data_bytes);

    // 步骤7:发送ARP请求包
    {
        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;
}

关键步骤详解

步骤1:从ARP包模板获取ARP包

ARP包模板:VPP使用预定义的ARP包模板,包含ARP包的基本结构。

模板内容

复制代码
以太网头部(14字节):
    ├─ 目标MAC地址:FF:FF:FF:FF:FF:FF(广播地址)
    ├─ 源MAC地址:待填充
    └─ 以太网类型:0x0806(ARP)

ARP头部(28字节):
    ├─ 硬件类型:0x0001(以太网)
    ├─ 协议类型:0x0800(IPv4)
    ├─ 硬件地址长度:6
    ├─ 协议地址长度:4
    ├─ 操作码:0x0001(ARP请求)
    ├─ 源MAC地址:待填充
    ├─ 源IP地址:待填充
    ├─ 目标MAC地址:00:00:00:00:00:00(待填充)
    └─ 目标IP地址:待填充
步骤2:应用Rewrite字符串

vnet_rewrite_one_header函数:将Adj的Rewrite字符串(以太网头部)复制到ARP包前面。

Rewrite字符串内容

  • 目标MAC地址:FF:FF:FF:FF:FF:FF(广播地址)
  • 源MAC地址:接口的MAC地址
  • 以太网类型:0x0806(ARP)
步骤3:填充ARP头部

填充字段

字段 说明
源MAC地址 hw_if0->hw_address 接口的MAC地址
源IP地址 *src 从Adj或接口配置获取
目标IP地址 *dst 要查询的IP地址
目标MAC地址 00:00:00:00:00:00 ARP请求中为0

通俗理解:就像"填写查询单"一样,填写"我是谁"(源MAC和源IP)和"我要查询谁"(目标IP)。

步骤4:发送ARP请求包

发送流程

  1. 调整缓冲区 :为Rewrite字符串预留空间(vlib_buffer_advance
  2. 发送到接口TX节点:将ARP请求包发送到接口的输出节点
  3. 更新统计计数:记录发送的ARP请求数量

ARP请求包结构

复制代码
[以太网头部] [ARP头部]
    ├─ 目标MAC: FF:FF:FF:FF:FF:FF(广播)
    ├─ 源MAC: 接口MAC地址
    ├─ 以太网类型: 0x0806
    ├─ 操作码: 0x0001(请求)
    ├─ 源IP: 接口IP地址
    └─ 目标IP: 要查询的IP地址

10.5 ARP响应的处理和邻居学习

作用和实现原理

当收到ARP响应时,ARP输入节点(arp-input)会调用arp_learn函数,提取ARP响应中的源IP和源MAC地址,然后通过RPC调用ip_neighbor_learn学习邻居信息。

10.5.1 ARP响应的处理(arp-input节点)

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

处理流程src/vnet/arp/arp.c:596):

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)
    {
        // 步骤1:调用arp_learn学习邻居信息
        error0 = arp_learn (sw_if_index0, &arp0->ip4_over_ethernet[0]);
    }
    else
    {
        // 非本地目标的响应可能是GARP,丢弃
        error0 = ARP_ERROR_L3_DST_ADDRESS_NOT_LOCAL;
    }

    goto next_feature;
}

关键条件

  1. ARP响应opcode == ETHERNET_ARP_OPCODE_reply
  2. 本地目标dst_is_local0 == 1(响应的是本地地址)

通俗理解:就像"收到地址回复"一样,只有当回复的是本地地址时,才学习邻居信息。

10.5.2 邻居学习(arp_learn)

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

作用和实现原理

arp_learn函数从ARP响应中提取源IP和源MAC地址,构造学习结构,然后通过RPC调用ip_neighbor_learn_dp学习邻居信息。

完整代码src/vnet/arp/arp.c:191):

c 复制代码
arp_learn (u32 sw_if_index,
           const ethernet_arp_ip4_over_ethernet_address_t * addr)
{
    // 步骤1:构造学习结构
    ip_neighbor_learn_t l = {
        .ip = {
            .ip.ip4 = addr->ip4,      // 源IP地址
            .version = AF_IP4,
        },
        .mac = addr->mac,              // 源MAC地址
        .sw_if_index = sw_if_index,    // 接口索引
    };

    // 步骤2:通过RPC调用ip_neighbor_learn_dp(数据平面)
    ip_neighbor_learn_dp (&l);

    return (ARP_ERROR_L3_SRC_ADDRESS_LEARNED);
}

关键步骤详解

步骤1:构造学习结构

ip_neighbor_learn_t结构src/vnet/ip-neighbor/ip_neighbor_types.h):

c 复制代码
typedef struct ip_neighbor_learn_t_
{
    ip_address_t ip;        // 邻居的IP地址(从ARP响应的源IP获取)
    mac_address_t mac;      // 邻居的MAC地址(从ARP响应的源MAC获取)
    u32 sw_if_index;        // 接口索引(接收ARP响应的接口)
} ip_neighbor_learn_t;

数据来源

字段 来源 说明
ip arp0->ip4_over_ethernet[0].ip4 ARP响应的源IP地址
mac arp0->ip4_over_ethernet[0].mac ARP响应的源MAC地址
sw_if_index sw_if_index0 接收ARP响应的接口索引
步骤2:RPC调用ip_neighbor_learn_dp

ip_neighbor_learn_dp函数src/vnet/ip-neighbor/ip_neighbor_dp.c:28):

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发送到主线程,由主线程处理邻居学习(参考第6章:邻居学习机制)。

完整学习流程

复制代码
步骤1:ARP响应到达
    └─ 数据包进入arp-input节点
    └─ 检查是否为ARP响应(opcode == reply)
    └─ 检查目标是否为本地地址
    ↓
步骤2:调用arp_learn
    └─ 提取源IP和源MAC地址
    └─ 构造ip_neighbor_learn_t结构
    └─ 调用ip_neighbor_learn_dp()
    ↓
步骤3:RPC调用主线程
    └─ 通过RPC调用ip_neighbor_learn()(主线程)
    └─ 调用ip_neighbor_add()添加邻居
    ↓
步骤4:更新转发平面
    └─ 创建FIB条目
    └─ 更新Adj为Complete状态
    └─ 更新Rewrite字符串(包含目标MAC地址)

通俗理解:就像"收到地址回复后更新地址簿"一样:

  1. 收到回复:收到ARP响应,包含源IP和源MAC地址
  2. 记录地址:将地址记录到地址簿(IP_Neighbor表)
  3. 更新模板:更新快递单模板(Adj的Rewrite字符串)

10.6 ARP限流机制

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

作用和实现原理

ARP限流机制用于防止ARP请求风暴。当对同一目标频繁发送ARP请求时,限流机制会限制请求频率,避免网络拥塞。

限流实现src/vnet/ip-neighbor/ip4_neighbor.c:215):

c 复制代码
// 步骤1:构造限流键值
r0 = (u64) resolve0.data_u32 << 32;
r0 |= sw_if_index0;

// 步骤2:限流检查
if (throttle_check (&arp_throttle, thread_index, r0, seed))
{
    p0->error = node->errors[IP4_NEIGHBOR_ERROR_THROTTLED];
    continue;
}

限流键值

  • 高32位 :目标IP地址(resolve0.data_u32
  • 低32位 :接口索引(sw_if_index0

限流初始化src/vnet/ip-neighbor/ip4_neighbor.c:328):

c 复制代码
static clib_error_t *
ip4_neighbor_main_loop_enter (vlib_main_t * vm)
{
    vlib_thread_main_t *tm = &vlib_thread_main;
    u32 n_vlib_mains = tm->n_vlib_mains;

    // 初始化限流器
    throttle_init (&arp_throttle, n_vlib_mains, THROTTLE_BITS, 1e-3);

    return (NULL);
}

限流参数

参数 说明
n_vlib_mains 线程数 每个线程有独立的限流桶
THROTTLE_BITS 10 限流位宽(1024个桶)
1e-3 1毫秒 限流时间窗口

限流机制

  1. 时间窗口:在1毫秒的时间窗口内,对同一目标(IP地址+接口)只允许发送一次ARP请求
  2. 多线程:每个线程有独立的限流桶,避免线程间的竞争
  3. 哈希桶:使用哈希函数将限流键值映射到限流桶

通俗理解:就像"限制查询频率"一样,防止对同一个地址频繁发送查询请求,避免网络拥塞。


10.7 完整ARP处理流程示例

示例1:数据包触发ARP请求
复制代码
步骤1:数据包到达
    └─ 数据包:10.0.0.1 → 192.168.1.100
    └─ FIB查找返回Incomplete Adj(不知道192.168.1.100的MAC地址)
    ↓
步骤2:进入ARP节点
    └─ 数据包进入ip4-arp节点
    └─ 获取Adj的下一跳地址:192.168.1.100
    └─ 获取源IP地址:192.168.1.1(从接口配置获取)
    ↓
步骤3:限流检查
    └─ 构造限流键值:(192.168.1.100 << 32) | sw_if_index
    └─ 检查限流器:如果未限流,继续;否则丢弃数据包
    ↓
步骤4:发送ARP请求
    └─ 调用ip4_neighbor_probe()
    └─ 从ARP包模板获取ARP包
    └─ 填充ARP头部:
        ├─ 源MAC: 11:22:33:44:55:66(接口MAC)
        ├─ 源IP: 192.168.1.1
        ├─ 目标IP: 192.168.1.100
        └─ 目标MAC: 00:00:00:00:00:00(待填充)
    └─ 应用Rewrite字符串(广播MAC地址)
    └─ 发送到接口TX节点
    ↓
步骤5:丢弃原始数据包
    └─ 设置错误码:IP4_NEIGHBOR_ERROR_REQUEST_SENT
    └─ 丢弃数据包(等待ARP响应)
示例2:ARP响应触发邻居学习
复制代码
步骤1:ARP响应到达
    └─ ARP响应包进入arp-input节点
    └─ 检查操作码:opcode == ETHERNET_ARP_OPCODE_reply
    └─ 检查目标地址:dst_is_local0 == 1(响应的是本地地址)
    ↓
步骤2:调用arp_learn
    └─ 提取源IP:192.168.1.100
    └─ 提取源MAC:AA:BB:CC:DD:EE:FF
    └─ 构造ip_neighbor_learn_t结构
    └─ 调用ip_neighbor_learn_dp()
    ↓
步骤3:RPC调用主线程
    └─ 通过RPC调用ip_neighbor_learn()(主线程)
    └─ 调用ip_neighbor_add()添加邻居
    └─ 创建FIB条目:192.168.1.100/32
    └─ 更新Adj为Complete状态
    └─ 更新Rewrite字符串:包含目标MAC地址(AA:BB:CC:DD:EE:FF)
    ↓
步骤4:后续数据包转发
    └─ 数据包:10.0.0.1 → 192.168.1.100
    └─ FIB查找返回Complete Adj
    └─ 应用Rewrite字符串(包含目标MAC地址)
    └─ 直接转发

10.8 IP_Neighbor在ARP中的作用总结

IP_Neighbor模块在ARP处理中的关键作用

  1. 触发ARP请求

    • ARP节点:处理Incomplete Adj,触发ARP请求
    • Glean节点:处理Glean Adj,触发ARP请求
  2. 构造ARP请求

    • 从ARP包模板获取ARP包
    • 填充ARP头部(源IP、目标IP、源MAC)
    • 应用Rewrite字符串(广播MAC地址)
    • 发送到接口TX节点
  3. 学习邻居信息

    • 从ARP响应中提取源IP和源MAC地址
    • 通过RPC调用主线程学习邻居
    • 更新IP_Neighbor表
  4. 更新转发平面

    • 创建FIB条目
    • 更新Adj为Complete状态
    • 更新Rewrite字符串(包含目标MAC地址)
  5. 限流保护

    • 防止ARP请求风暴
    • 限制同一目标的ARP请求频率

关键函数调用链

复制代码
ARP请求触发:
    ip4_arp_inline() / ip4_glean_inline()
    └─ ip4_neighbor_probe()
        └─ vlib_packet_template_get_packet()  // 获取ARP包模板
        └─ vnet_rewrite_one_header()          // 应用Rewrite字符串
        └─ 发送到接口TX节点

ARP响应处理:
    arp-input节点
    └─ arp_learn()
        └─ ip_neighbor_learn_dp()             // RPC调用
            └─ ip_neighbor_learn()            // 主线程
                └─ ip_neighbor_add()          // 添加邻居
                    └─ ip_neighbor_adj_fib_add()  // 创建FIB条目
                    └─ adj_nbr_walk_nh()
                        └─ ip_neighbor_mk_complete_walk()
                            └─ ip_neighbor_mk_complete()
                                └─ adj_nbr_update_rewrite()  // 更新Adj

11. IPv6 ND处理

章节意义

IPv6使用ND(Neighbor Discovery,邻居发现)协议进行邻居发现,而不是ARP。ND协议使用ICMPv6消息(Neighbor Solicitation和Neighbor Advertisement)来实现与ARP类似的功能。

本章将简要讲解:

  • IPv6 ND与IPv4 ARP的主要区别
  • ND节点(ip6-discover-neighbor)和Glean节点(ip6-glean)
  • ND探测(ip6_neighbor_probe)

注意:IPv6 ND处理的详细原理与IPv4 ARP类似,本章主要讲解IPv6的特殊之处。


11.1 IPv6 ND与IPv4 ARP的主要区别

作用和实现原理

IPv6 ND协议与IPv4 ARP协议在功能上类似,但在实现上有以下主要区别:

差异项 IPv4 ARP IPv6 ND 说明
协议类型 ARP(独立协议) ICMPv6(IPv6扩展) ND使用ICMPv6消息
消息类型 ARP请求/响应 Neighbor Solicitation/Advertisement ND使用ICMPv6消息类型
目标地址 广播MAC地址(FF:FF:FF:FF:FF:FF) 组播IPv6地址(Solicited-Node Multicast) ND使用组播地址
源地址选择 从接口配置获取 必须使用链路本地地址 ND要求使用链路本地地址
地址解析 直接查询目标IP 查询目标IP的Solicited-Node组播地址 ND使用组播地址解析

通俗理解:就像"两种查询方式"一样:

  • IPv4 ARP:使用"广播查询"(所有人都能收到)
  • IPv6 ND:使用"组播查询"(只有目标主机能收到)

11.2 ND节点(ip6-discover-neighbor)

源码位置src/vnet/ip-neighbor/ip6_neighbor.c:254

作用和实现原理

ip6-discover-neighbor节点处理命中Incomplete Adj的IPv6数据包,类似于IPv4的ip4-arp节点。

与IPv4 ARP节点的区别

  1. 源地址选择 :必须使用链路本地地址(ip6_get_link_local_address
  2. 目标地址 :使用Solicited-Node组播地址(ip6_set_solicited_node_multicast_address
  3. 消息类型:使用ICMPv6的Neighbor Solicitation消息

关键代码src/vnet/ip-neighbor/ip6_neighbor.c:220):

c 复制代码
// 步骤1:获取链路本地地址(必须)
const ip6_address_t *ll = ip6_get_link_local_address (sw_if_index0);
if (!ll)
{
    p0->error = node->errors[IP6_NEIGHBOR_ERROR_NO_SOURCE_ADDRESS];
    continue;
}
ip6_address_copy (&src, ll);

// 步骤2:发送ND请求
b0 = ip6_neighbor_probe (vm, vnm, sw_if_index0, thread_index, &src,
                         &ip0->dst_address);

通俗理解:就像"IPv6版本的地址查询"一样,使用链路本地地址作为源地址,发送Neighbor Solicitation消息。


11.3 Glean节点(ip6-glean)

源码位置src/vnet/ip-neighbor/ip6_neighbor.c:261

作用和实现原理

ip6-glean节点处理命中Glean Adj的IPv6数据包,类似于IPv4的ip4-glean节点。

与IPv4 Glean节点的区别

  1. 源地址:必须使用链路本地地址
  2. 目标地址:使用数据包的目标IPv6地址的Solicited-Node组播地址

处理流程:与IPv4 Glean节点类似,但使用IPv6地址和ICMPv6消息。


11.4 ND探测(ip6_neighbor_probe)

源码位置src/vnet/ip-neighbor/ip6_neighbor.h:40

作用和实现原理

ip6_neighbor_probe用于构造并发送Neighbor Solicitation消息,类似于IPv4的ip4_neighbor_probe

与IPv4 ARP探测的区别

  1. 消息类型:使用ICMPv6的Neighbor Solicitation消息,而不是ARP请求
  2. 目标地址:使用Solicited-Node组播地址,而不是广播MAC地址
  3. 包结构:包含IPv6头部、ICMPv6头部和链路层选项

关键代码src/vnet/ip-neighbor/ip6_neighbor.h:40):

c 复制代码
always_inline vlib_buffer_t *
ip6_neighbor_probe (vlib_main_t *vm, vnet_main_t *vnm, u32 sw_if_index,
                    u32 thread_index, const ip6_address_t *src,
                    const ip6_address_t *dst)
{
    icmp6_neighbor_solicitation_header_t *h0;
    vnet_hw_interface_t *hw_if0;
    vlib_buffer_t *b0;
    int bogus_length;
    u32 bi0 = 0;

    // 步骤1:从ND包模板获取ND包
    h0 = vlib_packet_template_get_packet
        (vm, &ip6_neighbor_packet_template, &bi0);
    if (!h0)
        return NULL;

    b0 = vlib_get_buffer (vm, bi0);
    hw_if0 = vnet_get_sup_hw_interface (vnm, sw_if_index);

    // 步骤2:填充IPv6头部
    h0->ip.src_address = *src;  // 源地址:链路本地地址
    // 目标地址:Solicited-Node组播地址(低24位来自目标IPv6地址)
    h0->ip.dst_address.as_u8[13] = dst->as_u8[13];
    h0->ip.dst_address.as_u8[14] = dst->as_u8[14];
    h0->ip.dst_address.as_u8[15] = dst->as_u8[15];

    // 步骤3:填充Neighbor Solicitation消息
    h0->neighbor.target_address = *dst;  // 目标地址

    // 步骤4:填充链路层选项(源MAC地址)
    clib_memcpy (h0->link_layer_option.ethernet_address,
                 hw_if0->hw_address, vec_len (hw_if0->hw_address));

    // 步骤5:计算ICMPv6校验和
    h0->neighbor.icmp.checksum = 0;
    h0->neighbor.icmp.checksum =
        ip6_tcp_udp_icmp_compute_checksum (vm, 0, &h0->ip, &bogus_length);

    // 步骤6:发送ND请求包
    vnet_buffer (b0)->sw_if_index[VLIB_TX] = sw_if_index;
    // ... 发送到接口TX节点 ...
}

Solicited-Node组播地址

IPv6的Solicited-Node组播地址格式:FF02::1:FFXX:XXXX,其中XX:XXXX是目标IPv6地址的低24位。

示例

复制代码
目标IPv6地址: 2001:db8::1
Solicited-Node组播地址: FF02::1:FF00:1
    ├─ FF02: 组播前缀
    ├─ ::1:FF: Solicited-Node标识
    └─ 00:1: 目标IPv6地址的低24位

通俗理解:就像"IPv6版本的查询地址"一样,使用组播地址而不是广播地址,只有目标主机能收到。


11.5 ND响应的处理

作用和实现原理

ND响应的处理与ARP响应类似,但使用ICMPv6的Neighbor Advertisement消息。

处理流程

  1. 收到Neighbor Advertisement:ICMPv6输入节点收到Neighbor Advertisement消息
  2. 提取邻居信息:从Neighbor Advertisement中提取目标地址和链路层地址
  3. 学习邻居 :调用ip_neighbor_learn_dp学习邻居信息(与ARP相同)

与ARP响应的区别

差异项 ARP响应 ND响应 说明
消息类型 ARP响应 Neighbor Advertisement ND使用ICMPv6消息
包结构 以太网+ARP IPv6+ICMPv6+选项 ND包含IPv6头部
地址格式 IPv4地址 IPv6地址 ND使用IPv6地址

通俗理解:就像"IPv6版本的地址回复"一样,使用ICMPv6消息而不是ARP消息。


11.6 IPv6 ND处理总结

IPv6 ND与IPv4 ARP的关键区别

  1. 协议类型:ND使用ICMPv6,ARP使用独立协议
  2. 消息类型:ND使用Neighbor Solicitation/Advertisement,ARP使用请求/响应
  3. 目标地址:ND使用组播地址,ARP使用广播地址
  4. 源地址:ND必须使用链路本地地址,ARP从接口配置获取

IP_Neighbor模块在ND中的作用

与ARP处理类似,IP_Neighbor模块在ND处理中负责:

  1. 触发ND请求:当需要学习邻居MAC地址时,构造并发送Neighbor Solicitation
  2. 处理ND响应:从Neighbor Advertisement中提取邻居信息,学习MAC地址
  3. 更新转发平面:将学习到的MAC地址更新到Adj的Rewrite字符串中

详细原理 :请参考[第10章:IPv4 ARP处理](#第10章:IPv4 ARP处理),IPv6 ND处理的详细原理与IPv4 ARP类似。


12. API接口

章节意义

IP_Neighbor模块提供了丰富的API接口,允许外部程序(如控制平面、管理工具)管理邻居表,包括添加/删除邻居、查询邻居信息、配置邻居表参数等。

本章将简要讲解:

  • 主要API消息类型及其功用
  • API消息的处理流程

注意:本章只简要介绍API的功用,详细的使用方法请参考VPP API文档。


12.1 主要API消息类型

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

12.1.1 添加/删除邻居(ip_neighbor_add_del)

功用:添加或删除一个邻居条目。

API定义

c 复制代码
define ip_neighbor_add_del
{
  u32 client_index;
  u32 context;
  bool is_add;              // 1 = 添加, 0 = 删除
  vl_api_ip_neighbor_t neighbor;
};

define ip_neighbor_add_del_reply
{
  u32 context;
  i32 retval;
  u32 stats_index;          // 统计索引
};

参数说明

参数 类型 说明
is_add bool 1表示添加,0表示删除
neighbor.sw_if_index u32 接口索引
neighbor.ip_address address_t 邻居的IP地址(IPv4或IPv6)
neighbor.mac_address mac_address_t 邻居的MAC地址
neighbor.flags ip_neighbor_flags_t 邻居标志(如STATIC、NO_FIB_ENTRY)

使用场景

  1. 静态邻居配置:通过控制平面配置静态邻居
  2. 邻居管理:动态添加或删除邻居条目

内部实现 :调用ip_neighbor_addip_neighbor_del函数。


12.1.2 导出邻居表(ip_neighbor_dump)

功用:导出邻居表的所有条目,用于查询和监控。

API定义

c 复制代码
define ip_neighbor_dump
{
  u32 client_index;
  u32 context;
  vl_api_interface_index_t sw_if_index [default=0xffffffff];  // ~0 = 所有接口
  vl_api_address_family_t af;                                 // IPv4或IPv6
};

define ip_neighbor_details {
  u32 context;
  f64 age;                    // 邻居年龄(秒)
  vl_api_ip_neighbor_t neighbor;
};

参数说明

参数 类型 说明
sw_if_index u32 接口索引(~0表示所有接口)
af address_family_t 地址族(IPv4或IPv6)

返回数据

  • age:邻居年龄(从上次更新到现在的时间,秒)
  • neighbor:邻居信息(IP地址、MAC地址、接口、标志等)

使用场景

  1. 邻居表查询:查询指定接口或所有接口的邻居表
  2. 监控和诊断:监控邻居表的状态和变化

内部实现 :遍历IP_Neighbor表,对每个邻居调用回调函数发送ip_neighbor_details消息。


12.1.3 配置邻居表参数(ip_neighbor_config)

功用:配置邻居表的参数,如最大数量、最大年龄、回收策略等。

API定义

c 复制代码
autoreply define ip_neighbor_config
{
  u32 client_index;
  u32 context;
  vl_api_address_family_t af;      // IPv4或IPv6
  u32 max_number;                  // 最大邻居数量(默认50k)
  u32 max_age;                     // 最大年龄(秒,0 = 永不老化)
  bool recycle;                    // 是否回收最老的邻居
};

参数说明

参数 类型 说明
af address_family_t 地址族(IPv4或IPv6)
max_number u32 最大邻居数量(默认50000)
max_age u32 最大年龄(秒,0表示永不老化)
recycle bool 当达到最大数量时,是否回收最老的邻居

使用场景

  1. 限制邻居数量:防止邻居表过大,占用过多内存
  2. 配置老化时间:设置邻居的老化时间,自动删除过期的邻居
  3. 配置回收策略:当达到最大数量时,是否回收最老的邻居

内部实现 :调用ip_neighbor_config函数,更新ip_neighbor_db_t的配置参数。


12.1.4 批量替换邻居表(ip_neighbor_replace_begin/end)

功用:批量替换邻居表,用于控制平面同步邻居表。

API定义

c 复制代码
autoreply define ip_neighbor_replace_begin
{
  u32 client_index;
  u32 context;
};

autoreply define ip_neighbor_replace_end
{
  u32 client_index;
  u32 context;
};

使用场景

  1. 控制平面同步:当控制平面有完整的邻居表时,批量替换VPP的邻居表
  2. 邻居表重建:重建邻居表,删除未指定的邻居

使用流程

复制代码
步骤1:调用ip_neighbor_replace_begin
    └─ 标记开始批量替换
    ↓
步骤2:调用ip_neighbor_add_del(多次)
    └─ 添加所有需要的邻居
    ↓
步骤3:调用ip_neighbor_replace_end
    └─ 删除未指定的邻居
    └─ 完成批量替换

内部实现 :使用标记机制,在replace_end时删除未标记的邻居。


12.1.5 清空邻居表(ip_neighbor_flush)

功用:清空指定接口或所有接口的邻居表。

API定义

c 复制代码
autoreply define ip_neighbor_flush
{
  u32 client_index;
  u32 context;
  vl_api_address_family_t af;
  vl_api_interface_index_t sw_if_index [default=0xffffffff];  // ~0 = 所有接口
};

参数说明

参数 类型 说明
af address_family_t 地址族(IPv4或IPv6)
sw_if_index u32 接口索引(~0表示所有接口)

使用场景

  1. 接口重置:当接口重置时,清空该接口的邻居表
  2. 故障恢复:当网络故障时,清空邻居表重新学习

内部实现 :调用ip_neighbor_del_all函数,删除指定接口的所有邻居。


12.1.6 邻居事件监控(want_ip_neighbor_events_v2)

功用:注册邻居事件监控,当邻居添加或删除时,接收事件通知。

API定义

c 复制代码
autoreply define want_ip_neighbor_events_v2
{
  u32 client_index;
  u32 context;
  bool enable;                    // 1 = 注册, 0 = 取消注册
  u32 pid;                        // 客户端进程ID
  vl_api_address_t ip;            // 监控的IP地址(可选)
  vl_api_interface_index_t sw_if_index [default=0xffffffff];  // 监控的接口(可选)
};

define ip_neighbor_event_v2
{
  u32 client_index;
  u32 pid;
  vl_api_ip_neighbor_event_flags_t flags;  // ADDED或REMOVED
  vl_api_ip_neighbor_t neighbor;
};

参数说明

参数 类型 说明
enable bool 1表示注册,0表示取消注册
pid u32 客户端进程ID
ip address_t 监控的IP地址(可选,NULL表示监控所有)
sw_if_index u32 监控的接口(可选,~0表示监控所有)

事件类型

  • IP_NEIGHBOR_API_EVENT_FLAG_ADDED:邻居已添加
  • IP_NEIGHBOR_API_EVENT_FLAG_REMOVED:邻居已删除

使用场景

  1. 外部控制平面:监控邻居变化,同步到外部系统
  2. 网络管理工具:实时监控邻居表的变化
  3. 故障诊断:诊断邻居学习问题

内部实现 :参考第8章:邻居监控机制


12.2 API处理流程

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

作用和实现原理

API消息处理函数将API请求转换为内部函数调用,处理流程如下:

  1. 接收API消息:从共享内存接收API消息
  2. 参数验证:验证API消息的参数
  3. 调用内部函数 :调用相应的内部函数(如ip_neighbor_addip_neighbor_del等)
  4. 构造回复消息:构造并发送回复消息

主要API处理函数

API消息 处理函数 内部函数
ip_neighbor_add_del ip_neighbor_add_del_handler ip_neighbor_add / ip_neighbor_del
ip_neighbor_dump ip_neighbor_dump_handler ip_neighbor_walk
ip_neighbor_config ip_neighbor_config_handler ip_neighbor_config
ip_neighbor_replace_begin ip_neighbor_replace_begin_handler 标记开始替换
ip_neighbor_replace_end ip_neighbor_replace_end_handler 删除未标记的邻居
ip_neighbor_flush ip_neighbor_flush_handler ip_neighbor_del_all
want_ip_neighbor_events_v2 want_ip_neighbor_events_v2_handler ip_neighbor_watch

API消息注册src/vnet/ip-neighbor/ip_neighbor_api.c):

c 复制代码
#define vl_msg_name_crc_list
#include <vnet/ip-neighbor/ip_neighbor.api.h>
#undef vl_msg_name_crc_list

static void
setup_message_id_table (api_main_t * am)
{
#define _(id,n,crc) vl_msg_api_add_msg_name_crc (am, #n "_" #crc, id + msg_id_base);
    foreach_vl_msg_name_crc_ip_neighbor;
#undef _
}

通俗理解:就像"快递站的客服系统"一样,API接口允许外部程序通过"电话"(API消息)与IP_Neighbor模块交互,进行各种操作。


12.3 API接口总结

IP_Neighbor模块的主要API接口

  1. 邻居管理

    • ip_neighbor_add_del:添加/删除邻居
    • ip_neighbor_flush:清空邻居表
  2. 邻居查询

    • ip_neighbor_dump:导出邻居表
  3. 配置管理

    • ip_neighbor_config:配置邻居表参数
    • ip_neighbor_config_get:获取邻居表配置
  4. 批量操作

    • ip_neighbor_replace_begin/end:批量替换邻居表
  5. 事件监控

    • want_ip_neighbor_events_v2:注册邻居事件监控

API的使用场景

  1. 控制平面:通过API管理邻居表,同步邻居信息
  2. 网络管理工具:查询和监控邻居表状态
  3. 故障诊断:诊断邻居学习问题,清空和重建邻居表

详细使用方法:请参考VPP API文档和示例代码。


总结

IP Neighbor模块是VPP中负责IP到MAC地址映射管理的核心模块,它:

  1. 管理邻居表:使用哈希表和链表维护邻居信息
  2. 处理邻居发现:通过ARP(IPv4)和ND(IPv6)协议发现邻居
  3. 学习邻居信息:从协议响应中学习邻居MAC地址
  4. 更新转发平面:将学习到的信息更新到Adjacency中,供数据包转发使用
  5. 提供监控机制:允许外部模块监控邻居变化

该模块在VPP的数据包转发流程中起到关键作用,是连接IP层和以太网层的桥梁。

相关推荐
知识分享小能手2 小时前
CentOS Stream 9入门学习教程,从入门到精通,CentOS Stream 9 配置网络功能 —语法详解与实战案例(10)
网络·学习·centos
Joren的学习记录2 小时前
【Linux运维进阶知识】Nginx负载均衡
linux·运维·nginx
专业开发者2 小时前
Wi-Fi®:可持续的优选连接方案
网络·物联网
GIS数据转换器3 小时前
综合安防数智管理平台
大数据·网络·人工智能·安全·无人机
Jtti3 小时前
服务器防御SYN Flood攻击的方法
运维·服务器
2501_941982053 小时前
RPA 的跨平台部署与统一自动化策略
运维·自动化·rpa
b***25113 小时前
电池自动分选机:精密分选保障新能源产业质量核心
运维·自动化·制造
数数科技的数据干货3 小时前
游戏流失分析:一套经实战检验的「流程化操作指南」
大数据·运维·人工智能·游戏
蒟蒻要翻身4 小时前
在同一局域网内共享打印机设置指南
运维
chem41114 小时前
魔百盒 私有网盘seafile搭建
linux·运维·网络