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层和以太网层的桥梁。

相关推荐
Elastic 中国社区官方博客2 小时前
使用 Elastic Cloud Serverless 扩展批量索引
大数据·运维·数据库·elasticsearch·搜索引擎·云原生·serverless
超龄超能程序猿2 小时前
Docker GPU插件(NVIDIA Container Toolkit)安装
运维·docker·容器
岁岁种桃花儿3 小时前
Nginx 站点垂直扩容(单机性能升级)全攻略
网络·nginx·dns
Xの哲學3 小时前
Linux SMP 实现机制深度剖析
linux·服务器·网络·算法·边缘计算
2501_906150563 小时前
私有部署问卷系统操作实战记录-DWSurvey
java·运维·服务器·spring·开源
better_liang3 小时前
每日Java面试场景题知识点之-TCP/IP协议栈与Socket编程
java·tcp/ip·计算机网络·网络编程·socket·面试题
岳来3 小时前
docker 从 Path 值看容器启动命令
运维·docker·容器
一颗青果4 小时前
公网构建全流程与参与主体深度解析
网络
RisunJan4 小时前
Linux命令-ifconfig命令(配置和显示网络接口的信息)
linux·运维·服务器
杭州泽沃电子科技有限公司4 小时前
面对风霜雨雪雷电:看在线监测如何为架空线路筑牢安全网
运维·人工智能·在线监测·智能监测