目录
- 模块概述
- 模块在节点流中的位置
- 核心数据结构
- 模块初始化
- 邻居表管理
- 邻居学习机制
- 邻居老化机制
- 邻居监控机制
- 与Adjacency的交互
- [IPv4 ARP处理](#IPv4 ARP处理)
- [IPv6 ND处理](#IPv6 ND处理)
- 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_t、ip_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:277、src/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地址)
- 身份标识(静态还是动态学习)
- 状态信息(是否过期、探测次数)
- 时间戳(最后联系时间)
核心作用:
- 存储邻居信息:存储IP地址到MAC地址的映射关系
- 状态管理:管理邻居的状态(静态/动态、完整/过期)
- 老化支持:支持基于时间的邻居老化机制
- 关联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_t(u32,索引)
作用:
- 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_t(u32,索引)
作用:
- 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)
- 接口索引:邻居所在的接口
核心作用:
- 唯一标识:作为邻居的唯一标识(键)
- 哈希查找:作为哈希表的键,用于快速查找
- 区分接口:同一个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的倍数u32(ipnk_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就像"通讯录数据库"一样,管理所有邻居:
- 存储结构:使用哈希表快速查找邻居
- 限制管理:限制最大邻居数量,防止内存耗尽
- 老化管理:管理邻居的老化时间
- 统计信息:统计当前邻居数量
核心作用:
- 邻居存储:使用多级哈希表存储邻居
- 快速查找:通过接口索引和IP地址快速查找邻居
- 资源限制:限制最大邻居数量,防止内存耗尽
- 老化支持:支持基于时间的邻居老化
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链表中的位置
- 时间排序:链表按时间排序,最新的在头部,最旧的在尾部
核心作用:
- LRU链表节点:作为LRU链表的节点
- 时间排序:维护邻居的时间顺序
- 快速访问:通过链表快速访问最旧或最新的邻居
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_t(u32,索引)
作用:
- 邻居引用 :指向对应的
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的关键点:
- 主要用途:存储邻居信息,管理邻居状态
- 核心字段 :
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的关键点:
- 主要用途:唯一标识邻居
- 核心字段 :
ipnk_ip:IP地址ipnk_sw_if_index:接口索引
- 唯一性:由IP地址和接口索引组成唯一键
ip_neighbor_db_t的关键点:
- 主要用途:管理所有邻居,提供快速查找
- 核心字段 :
ipndb_hash:多级哈希表ipndb_limit:最大数量限制ipndb_age:老化时间ipndb_recycle:回收策略ipndb_n_elts:当前数量
- 设计特点:多级哈希表,按接口索引和IP地址查找
ip_neighbor_elt_t的关键点:
- 主要用途:LRU链表的节点
- 核心字段 :
ipne_anchor:链表锚点ipne_index:邻居索引
- 设计特点:双向链表,按时间排序
数据结构的协作:
- 哈希表:提供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"),
};
初始化顺序说明:
-
VPP启动流程:
- VPP启动时,会按照依赖关系依次调用各个模块的初始化函数
- IP Neighbor模块的初始化函数
ip_neighbor_init在ip_main_init(IP主模块)初始化之后执行 - 这意味着当IP Neighbor模块初始化时,IP主模块已经准备好了
-
为什么要在IP主模块之后初始化?
- IP Neighbor模块需要向IP主模块注册回调函数
- 如果IP主模块还没初始化,就无法注册回调函数
- 这就像你要给某人留电话号码,必须先确保对方已经准备好了接收系统
-
初始化时机总结:
- ✅ 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_v4(src/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_v4(src/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_change(src/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_interface(src/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淘汰和老化扫描 |
关键设计思想:
-
事件驱动架构:
- IP Neighbor模块采用事件驱动架构,通过注册回调函数来响应各种事件
- 这样设计的好处是:模块不需要主动轮询,而是被动响应事件,效率更高
-
提前准备基础设施:
- 在VPP启动时就注册好所有回调函数,而不是等到需要时才注册
- 这样确保后续的任何操作都能得到及时响应
-
自动维护一致性:
- 通过回调函数自动维护邻居表的一致性
- 例如:当接口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,
};
步骤说明:
-
线程检查 :
ASSERT (0 == vlib_get_thread_index ())- 确保只在主线程(thread 0)执行
- 因为需要修改全局数据结构(哈希表、链表),必须保证线程安全
- 如果worker线程调用,会通过RPC机制转发到主线程
-
确定协议类型 :
fproto = ip_address_family_to_fib_proto (ip_addr_version (ip))- 根据IP地址版本(IPv4或IPv6)确定FIB协议类型
- IPv4 →
FIB_PROTOCOL_IP4 - IPv6 →
FIB_PROTOCOL_IP6
-
构建查找键值 :
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指向现有邻居条目;如果没找到,ipn为NULL
查找实现细节 (ip_neighbor_db_find,src/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);
}
查找流程:
- 确定地址族:根据IP地址版本确定是IPv4还是IPv6
- 边界检查:检查接口索引是否超出哈希表范围
- 哈希查找 :
- 使用
hash_get_mem在对应接口的哈希表中查找 - 哈希表结构:
ip_neighbor_db[af].ipndb_hash[sw_if_index] - 第一级索引:地址族(IPv4/IPv6)
- 第二级索引:接口索引
- 第三级:哈希表本身,键值为
ip_neighbor_key_t
- 使用
- 返回结果 :如果找到,返回邻居条目指针;否则返回
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;
}
步骤说明:
- 记录日志:记录添加新邻居的日志信息
- 分配邻居条目 :调用
ip_neighbor_alloc分配新的邻居条目 - 检查分配结果:如果分配失败(达到限制且不允许回收),返回错误
分配邻居条目的详细流程 (ip_neighbor_alloc,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)
{
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_add,src/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++;
}
详细说明:
- 扩展数组 :
vec_validate确保哈希表数组足够大,能容纳该接口索引 - 创建哈希表 :如果该接口还没有哈希表,创建一个新的哈希表
- 键值类型:
ip_neighbor_key_t - 值类型:
index_t(邻居在pool中的索引)
- 键值类型:
- 插入条目 :使用
hash_set_mem将邻居插入哈希表- 键:
ipn->ipn_key(IP地址+接口索引) - 值:
ip_neighbor_get_index (ipn)(邻居在pool中的索引)
- 键:
- 更新计数:增加该地址族的邻居总数
步骤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_refresh,src/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;
}
}
详细步骤说明:
- 清除过期标志 :
ip_neighbor_touch (ipn)清除STALE标志 - 更新时间戳:记录当前时间,用于老化判断
- 重置探测次数 :
ipn_n_probes = 0,表示邻居是"新鲜的" - 更新链表位置 (仅动态邻居):
- 链表的作用:维护邻居的时间顺序,最新的在头部,最旧的在尾部
- 首次插入 :如果
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:遍历所有指向该邻居的Adjacencyip_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;
步骤说明:
- 发布事件 :
ip_neighbor_publish通知所有注册的监控器,邻居已添加/更新 - 返回统计索引 :如果
stats_index不为空,查找Adjacency索引并返回 - 返回成功 :返回
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);
步骤说明:
- 线程检查:确保只在主线程执行
- 记录日志:记录删除操作的日志
- 构建查找键值:与添加函数相同
- 查找邻居:在哈希表中查找
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_destroy,src/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_remove,src/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_remove,src/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_reuse,src/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);
}
详细步骤说明:
- 检查回收标志 :如果未启用回收(
ipndb_recycle = false),返回false - 获取链表头:获取该地址族的链表头节点
- 检查链表是否为空 :如果链表为空,无法回收,返回
false - 获取最旧的邻居 :
clib_llist_prev获取链表尾部的前一个节点(最旧的邻居)- 链表结构:头节点 → 最新 → ... → 最旧 → 头节点(循环链表)
- 最旧的邻居在链表尾部
- 删除最旧的邻居 :调用
ip_neighbor_destroy删除,释放空间 - 返回成功 :返回
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));
}
步骤说明:
-
参数 :
ip_neighbor_learn_t * l包含学习信息ip:要学习的IP地址mac:对应的MAC地址sw_if_index:接口索引
-
RPC调用 :
vl_api_rpc_call_main_thread- 目标函数 :
ip_neighbor_learn(主线程的学习函数) - 数据 :
(u8 *) l(学习信息的字节流) - 数据长度 :
sizeof (*l)(学习信息结构体的大小)
- 目标函数 :
RPC机制详解 (vl_api_rpc_call_main_thread,src/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_inline,src/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流程说明:
-
检查线程:
- 如果已经在主线程(
thread_index == 0)且不是强制RPC,直接调用函数 - 否则,创建RPC消息
- 如果已经在主线程(
-
创建RPC消息:
- 分配消息内存:
vl_msg_api_alloc_as_if_client - 复制数据:
clib_memcpy_fast将学习信息复制到消息中 - 设置函数指针:
mp->function = pointer_to_uword (fp) - 标记需要barrier:
mp->need_barrier_sync = 1
- 分配消息内存:
-
添加到待处理队列:
- 加锁保护(主线程需要加锁)
- 添加到
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;
}
详细步骤说明:
-
交换向量:
- 将
pending_rpc_requests(待处理)和processing_rpc_requests(正在处理)交换 - 这样worker线程可以继续添加新的RPC请求,而不会影响正在处理的请求
- 将
-
Barrier同步:
vl_msg_api_barrier_sync ():暂停所有worker线程- 确保在处理RPC时,worker线程不会读取正在更新的数据结构
-
批量处理:
- 遍历所有待处理的RPC请求
- 调用
vl_mem_api_handler_with_vm_node处理每个请求 - 对于
ip_neighbor_learn,会调用ip_neighbor_learn函数
-
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);
}
步骤说明:
- 提取学习信息 :从
ip_neighbor_learn_t结构体中提取IP、MAC和接口索引 - 调用添加函数 :调用
ip_neighbor_add添加邻居 - 标记为动态 :使用
IP_NEIGHBOR_FLAG_DYNAMIC标志,表示这是从协议响应中学习的
关键点:
- 主线程执行:这个函数只在主线程执行(通过RPC机制保证)
- 动态标志:学习到的邻居标记为动态,可以被老化机制清理
- 调用链 :
ip_neighbor_learn→ip_neighbor_add→ip_neighbor_alloc→ip_neighbor_db_add等
学习信息结构 (ip_neighbor_learn_t,src/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);
}
步骤说明:
-
构建学习信息:
- 从ARP响应中提取IP地址(
addr->ip4) - 从ARP响应中提取MAC地址(
addr->mac) - 使用接收ARP响应的接口索引(
sw_if_index)
- 从ARP响应中提取IP地址(
-
调用学习接口 :调用
ip_neighbor_learn_dp提交学习请求 -
返回错误码 :返回
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;
}
详细步骤说明:
-
检查ARP操作码:
- 如果是ARP响应(
ETHERNET_ARP_OPCODE_reply),继续处理 - 如果是ARP请求,不学习(因为请求中没有目标MAC地址)
- 如果是ARP响应(
-
检查目标地址:
dst_is_local0:检查ARP响应的目标IP是否是本机或本网段的- 为什么要检查?:只学习对本机或本网段的ARP响应,避免学习到无关的邻居
-
调用学习函数:
- 如果目标地址是本地的,调用
arp_learn学习发送方的IP和MAC地址 &arp0->ip4_over_ethernet[0]:ARP响应中发送方的IP和MAC地址
- 如果目标地址是本地的,调用
-
处理非本地响应:
- 如果目标地址不是本地的,可能是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);
}
步骤说明:
-
检查条件:
error0 == ICMP6_ERROR_NONE:ICMPv6消息解析成功o0 != 0:存在链路层地址选项(包含MAC地址)!ip6_sadd_unspecified:源地址不是未指定地址(::)
-
确定IP地址:
- 如果是NS(请求) :使用IPv6报文的源地址(
ip0->src_address) - 如果是NA(响应) :使用NA消息的目标地址(
h0->target_address)
- 如果是NS(请求) :使用IPv6报文的源地址(
-
提取MAC地址:
- 从链路层地址选项中提取MAC地址(
o0->ethernet_address)
- 从链路层地址选项中提取MAC地址(
-
调用学习接口 :调用
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.100 → aa: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 学习性能考虑
优化措施:
- 批量处理:一次barrier处理多个RPC请求
- 异步处理:Worker线程不阻塞,继续处理其他数据包
- Barrier hold-down timer:如果worker线程很忙,延迟barrier
- 向量交换:处理时不影响新的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请求队列
关键设计原则:
- 数据平面触发,控制平面执行:学习请求在worker线程触发,在主线程执行
- RPC机制:通过RPC实现线程间通信
- Barrier同步:确保数据一致性
- 批量处理:提高性能,减少barrier次数
7. 邻居老化机制
7.1 概述:什么是邻居老化?
通俗理解:邻居老化就像"定期清理通讯录"一样。如果某个联系人很久没有联系(超过设定的时间),系统会主动"打电话"确认他是否还在,如果连续3次都联系不上,就把他从通讯录中删除。
老化的目的:
- 清理无效邻居:删除已经失效的邻居(设备已下线、MAC地址已变化等)
- 节省内存:防止邻居表无限增长
- 保持数据准确性:确保邻居表中的信息是最新的
老化机制的特点:
- 默认禁用 :默认情况下,老化机制是禁用的(
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;
步骤说明:
-
初始化变量:
event_type:事件类型event_data:事件数据timeout:超时时间
-
初始超时时间:
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);
步骤说明:
-
等待事件或超时:
timeout == 0:无限期等待事件(vlib_process_wait_for_event)timeout > 0:等待事件或超时(vlib_process_wait_for_event_or_clock)- 如果超时,
event_type会被设置为~0(表示定时器到期)
-
获取事件:
vlib_process_get_events:获取所有待处理的事件vec_reset_length:重置事件数据向量
-
获取当前时间:
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;
}
详细步骤说明:
-
检查老化是否启用:
- 如果
ipndb_age == 0(老化已禁用),设置timeout = 0,无限期等待
- 如果
-
检查链表是否为空:
- 如果链表为空,设置
timeout = ipndb_age,等待一个老化周期
- 如果链表为空,设置
-
检查最旧的邻居:
- 获取链表尾部的前一个节点(最旧的邻居)
- 调用
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;
步骤说明:
-
获取邻居信息:
ip_neighbor_get:根据索引获取邻居条目ip_neighbor_get_af:获取地址族(IPv4或IPv6)
-
获取老化配置:
ipndb_age:老化时间(秒)
-
计算存活时间(TTL):
ttl = now - ipn->ipn_time_last_updated- 当前时间减去最后更新时间,得到邻居的"年龄"
-
初始化等待时间:
*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_dst(src/vnet/ip-neighbor/ip_neighbor.h:59)
IPv4探测 (ip4_neighbor_probe_dst,src/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);
}
步骤说明:
- 获取Glean Adjacency:用于构建ARP请求的rewrite字符串
- 获取源IP地址:从接口配置中获取源IP地址
- 发送ARP请求 :调用
ip4_neighbor_probe构造并发送ARP请求
IPv6探测 (ip6_neighbor_probe_dst):类似,但发送Neighbor Solicitation(NS)请求。
探测包构造 (ip4_neighbor_probe,src/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_updated和ipn_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);
}
步骤说明:
-
更新配置:
ipndb_limit:最大邻居数限制ipndb_recycle:是否启用回收ipndb_age:老化时间(秒)
-
唤醒老化进程:
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:老化时间配置
关键设计原则:
- 仅动态邻居参与老化:静态邻居不会被老化
- 探测机制:过期邻居先探测,确认失效后才删除
- 三次探测:避免误删,平衡准确性和效率
- 链表优化:从最旧的开始扫描,遇到活着的就停止
- 事件驱动:可以响应配置改变,立即检查
8. 邻居监控机制
8.1 概述:什么是邻居监控?
通俗理解:邻居监控就像"订阅通知"一样。外部程序可以"订阅"某个邻居的变化事件,当邻居被添加或删除时,VPP会自动"推送"通知给订阅者。
监控机制的目的:
- 实时通知:外部程序可以实时知道邻居表的变化
- 解耦设计:数据平面(VPP)和控制平面(外部程序)解耦
- 事件驱动:基于事件驱动,而不是轮询查询
监控机制的特点:
- 精确匹配:可以监控特定IP地址、特定接口,或所有邻居
- 事件类型:支持添加事件和删除事件
- 异步通知:通过进程节点异步处理,不阻塞数据包转发
- 多订阅者:同一个邻居可以有多个订阅者
8.2 监控机制架构
源码位置 :src/vnet/ip-neighbor/ip_neighbor_watch.c
架构组件:
- 监控器数据库:存储所有注册的监控器
- 事件发布函数 :
ip_neighbor_publish(在邻居添加/删除时调用) - 事件处理进程 :
ip_neighbor_event_process_node(异步处理事件) - 事件处理函数 :
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);
}
步骤说明:
-
构造查找键:
- 如果
sw_if_index == 0,设置为~0(表示所有接口)
- 如果
-
查找现有监控器:
- 在哈希表中查找该键对应的监控器向量
-
检查重复:
- 如果监控器已存在,直接返回(避免重复注册)
-
添加监控器:
- 将新监控器添加到向量中
-
更新哈希表:
- 将更新后的向量存储回哈希表
通俗理解:就像"订阅邮件列表"一样。如果某个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);
}
步骤说明:
- 查找监控器向量:在哈希表中查找对应的监控器向量
- 查找要删除的监控器:在向量中查找匹配的监控器
- 删除监控器:从向量中删除该监控器
- 清理空向量:如果向量为空,删除哈希表项
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);
}
}
步骤说明:
-
获取邻居信息:根据索引获取邻居条目,提取键(IP + 接口)
-
查找精确匹配:
- 键:
{IP地址, 接口索引} - 匹配:监控特定IP地址和特定接口的监控器
- 键:
-
查找接口级别匹配:
- 键:
{0.0.0.0, 接口索引} - 匹配:监控特定接口上所有IP地址的监控器
- 键:
-
查找全局匹配:
- 键:
{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);
}
}
步骤说明:
-
遍历监控器:为每个监控器创建一个事件
-
创建事件数据:
vlib_process_signal_event_data:创建事件数据并发送给进程节点- 事件发送给
ip_neighbor_event_process_node
-
填充事件信息:
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;
}
步骤说明:
- 等待事件:进程睡眠,等待事件到达
- 获取事件数据:获取所有待处理的事件
- 处理事件 :遍历每个事件,调用
ip_neighbor_handle_event处理 - 重置向量:清空事件向量,准备接收新事件
通俗理解:就像"邮件处理中心"一样。不断等待"邮件"(事件)到达,然后逐个处理。
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);
}
步骤说明:
- 获取客户端注册:根据客户端索引获取注册信息
- 检查消息队列:检查是否可以发送消息
- 构造v1消息 :如果API版本为1,构造
ip_neighbor_event消息 - 构造v2消息 :如果API版本为2,构造
ip_neighbor_event_v2消息(包含flags) - 限流处理:如果消息队列满,记录警告(每10秒一次)
- 释放内存:释放克隆的邻居信息
API版本区别:
- v1:只包含邻居信息,不包含事件类型标志
- v2 :包含事件类型标志(
IP_NEIGHBOR_API_EVENT_FLAG_ADDED或IP_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);
步骤说明:
- 遍历所有注册:遍历监控器数据库中的所有键值对
- 删除客户端监控器 :删除匹配
client_index的所有监控器 - 标记空向量:如果向量为空,标记该键为删除
- 清理哈希表:删除所有空的哈希表项
通俗理解:就像"清理僵尸订阅"一样。当用户"退订"(断开连接)时,自动清理他的所有订阅记录。
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:事件结构
关键设计原则:
- 异步处理:使用进程节点异步处理事件
- 三级匹配:支持精确/接口/全局三级匹配
- 自动清理:Reaper机制自动清理断开连接的客户端
- 版本兼容:支持v1和v2两个API版本
- 限流保护:消息队列满时进行限流保护
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模块的交互是双向的:
- IP_Neighbor → Adj:当IP_Neighbor学习到或更新邻居的MAC地址时,更新Adj的Rewrite字符串
- 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节):
- Barrier同步:暂停所有worker线程
- 更新Rewrite字符串:将新的Rewrite字符串复制到Adj结构中
- 更新VLIB图 :更新Adj的next_index,从
IP_LOOKUP_NEXT_ARP变为IP_LOOKUP_NEXT_REWRITE - Barrier释放:恢复所有worker线程
- 回退遍历:通知所有子节点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节点
调用时机:
-
IP_Neighbor添加时 (
ip_neighbor_add):cadj_nbr_walk_nh (ipn->ipn_key->ipnk_sw_if_index, fproto, &ip_addr_46 (&ipn->ipn_key->ipnk_ip), ip_neighbor_mk_complete_walk, ipn); -
IP_Neighbor学习时 (
ip_neighbor_learn):cadj_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); -
Adj更新时 (
ip_neighbor_update):cif (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请求
调用时机:
-
IP_Neighbor删除时 (
ip_neighbor_destroy):cadj_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); -
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]++;
}
关键步骤详解:
-
构造FIB前缀:
- IPv4:
/32(主机路由) - IPv6:
/128(主机路由) - IPv6链路本地地址:使用特殊的链路本地前缀表
- IPv4:
-
添加FIB条目路径:
- 来源 :
FIB_SOURCE_ADJ(表示该FIB条目由Adj模块创建) - 下一跳地址:邻居的IP地址
- 接口索引:邻居所在的接口
- 来源 :
-
锁定FIB表:防止FIB表被删除
-
更新统计计数:记录该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;
}
}
处理场景:
-
Adj是Incomplete状态 (
IP_LOOKUP_NEXT_ARP):- 有IP_Neighbor:更新Adj为Complete
- 无IP_Neighbor:保持Incomplete状态,发送ARP请求
-
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交互的关键特点:
-
双向交互:
- IP_Neighbor → Adj:更新Adj的Rewrite字符串
- Adj → IP_Neighbor:触发ARP/ND请求
-
状态同步:
- Complete状态:Rewrite字符串包含目标MAC地址
- Incomplete状态:Rewrite字符串使用广播MAC地址
-
FIB条目管理:
- 每个邻居创建一个
/32或/128的FIB条目 - FIB条目指向该邻居的Adj
- 每个邻居创建一个
-
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处理中起到关键作用。它负责:
- 触发ARP请求:当需要学习邻居MAC地址时,构造并发送ARP请求
- 处理ARP响应:从ARP响应中提取邻居信息,学习MAC地址
- 更新转发平面:将学习到的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处理中扮演"学习者和更新者"的角色:
- 触发ARP请求:当数据包需要转发但不知道目标MAC地址时,触发ARP请求
- 学习邻居信息:从ARP响应中提取源IP和源MAC地址,学习邻居信息
- 更新转发平面:将学习到的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_get或ip4_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状态
状态检查:
- Adj已更新 :如果Adj已经更新为Complete(
IP_LOOKUP_NEXT_REWRITE),不需要ARP请求 - 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节点:查询"路由的下一跳地址"(用于特定路由)
使用场景:
- 连接前缀 :当接口配置了连接前缀(如
192.168.1.0/24)时,该前缀的Glean Adj会触发Glean节点 - 同一网段:当数据包要发送到同一网段内的主机时,使用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请求包
发送流程:
- 调整缓冲区 :为Rewrite字符串预留空间(
vlib_buffer_advance) - 发送到接口TX节点:将ARP请求包发送到接口的输出节点
- 更新统计计数:记录发送的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;
}
关键条件:
- ARP响应 :
opcode == ETHERNET_ARP_OPCODE_reply - 本地目标 :
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地址)
通俗理解:就像"收到地址回复后更新地址簿"一样:
- 收到回复:收到ARP响应,包含源IP和源MAC地址
- 记录地址:将地址记录到地址簿(IP_Neighbor表)
- 更新模板:更新快递单模板(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毫秒的时间窗口内,对同一目标(IP地址+接口)只允许发送一次ARP请求
- 多线程:每个线程有独立的限流桶,避免线程间的竞争
- 哈希桶:使用哈希函数将限流键值映射到限流桶
通俗理解:就像"限制查询频率"一样,防止对同一个地址频繁发送查询请求,避免网络拥塞。
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处理中的关键作用:
-
触发ARP请求:
- ARP节点:处理Incomplete Adj,触发ARP请求
- Glean节点:处理Glean Adj,触发ARP请求
-
构造ARP请求:
- 从ARP包模板获取ARP包
- 填充ARP头部(源IP、目标IP、源MAC)
- 应用Rewrite字符串(广播MAC地址)
- 发送到接口TX节点
-
学习邻居信息:
- 从ARP响应中提取源IP和源MAC地址
- 通过RPC调用主线程学习邻居
- 更新IP_Neighbor表
-
更新转发平面:
- 创建FIB条目
- 更新Adj为Complete状态
- 更新Rewrite字符串(包含目标MAC地址)
-
限流保护:
- 防止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节点的区别:
- 源地址选择 :必须使用链路本地地址(
ip6_get_link_local_address) - 目标地址 :使用Solicited-Node组播地址(
ip6_set_solicited_node_multicast_address) - 消息类型:使用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节点的区别:
- 源地址:必须使用链路本地地址
- 目标地址:使用数据包的目标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探测的区别:
- 消息类型:使用ICMPv6的Neighbor Solicitation消息,而不是ARP请求
- 目标地址:使用Solicited-Node组播地址,而不是广播MAC地址
- 包结构:包含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消息。
处理流程:
- 收到Neighbor Advertisement:ICMPv6输入节点收到Neighbor Advertisement消息
- 提取邻居信息:从Neighbor Advertisement中提取目标地址和链路层地址
- 学习邻居 :调用
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的关键区别:
- 协议类型:ND使用ICMPv6,ARP使用独立协议
- 消息类型:ND使用Neighbor Solicitation/Advertisement,ARP使用请求/响应
- 目标地址:ND使用组播地址,ARP使用广播地址
- 源地址:ND必须使用链路本地地址,ARP从接口配置获取
IP_Neighbor模块在ND中的作用:
与ARP处理类似,IP_Neighbor模块在ND处理中负责:
- 触发ND请求:当需要学习邻居MAC地址时,构造并发送Neighbor Solicitation
- 处理ND响应:从Neighbor Advertisement中提取邻居信息,学习MAC地址
- 更新转发平面:将学习到的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) |
使用场景:
- 静态邻居配置:通过控制平面配置静态邻居
- 邻居管理:动态添加或删除邻居条目
内部实现 :调用ip_neighbor_add或ip_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地址、接口、标志等)
使用场景:
- 邻居表查询:查询指定接口或所有接口的邻居表
- 监控和诊断:监控邻居表的状态和变化
内部实现 :遍历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 |
当达到最大数量时,是否回收最老的邻居 |
使用场景:
- 限制邻居数量:防止邻居表过大,占用过多内存
- 配置老化时间:设置邻居的老化时间,自动删除过期的邻居
- 配置回收策略:当达到最大数量时,是否回收最老的邻居
内部实现 :调用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;
};
使用场景:
- 控制平面同步:当控制平面有完整的邻居表时,批量替换VPP的邻居表
- 邻居表重建:重建邻居表,删除未指定的邻居
使用流程:
步骤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表示所有接口) |
使用场景:
- 接口重置:当接口重置时,清空该接口的邻居表
- 故障恢复:当网络故障时,清空邻居表重新学习
内部实现 :调用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:邻居已删除
使用场景:
- 外部控制平面:监控邻居变化,同步到外部系统
- 网络管理工具:实时监控邻居表的变化
- 故障诊断:诊断邻居学习问题
内部实现 :参考第8章:邻居监控机制。
12.2 API处理流程
源码位置 :src/vnet/ip-neighbor/ip_neighbor_api.c
作用和实现原理:
API消息处理函数将API请求转换为内部函数调用,处理流程如下:
- 接收API消息:从共享内存接收API消息
- 参数验证:验证API消息的参数
- 调用内部函数 :调用相应的内部函数(如
ip_neighbor_add、ip_neighbor_del等) - 构造回复消息:构造并发送回复消息
主要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接口:
-
邻居管理:
ip_neighbor_add_del:添加/删除邻居ip_neighbor_flush:清空邻居表
-
邻居查询:
ip_neighbor_dump:导出邻居表
-
配置管理:
ip_neighbor_config:配置邻居表参数ip_neighbor_config_get:获取邻居表配置
-
批量操作:
ip_neighbor_replace_begin/end:批量替换邻居表
-
事件监控:
want_ip_neighbor_events_v2:注册邻居事件监控
API的使用场景:
- 控制平面:通过API管理邻居表,同步邻居信息
- 网络管理工具:查询和监控邻居表状态
- 故障诊断:诊断邻居学习问题,清空和重建邻居表
详细使用方法:请参考VPP API文档和示例代码。
总结
IP Neighbor模块是VPP中负责IP到MAC地址映射管理的核心模块,它:
- 管理邻居表:使用哈希表和链表维护邻居信息
- 处理邻居发现:通过ARP(IPv4)和ND(IPv6)协议发现邻居
- 学习邻居信息:从协议响应中学习邻居MAC地址
- 更新转发平面:将学习到的信息更新到Adjacency中,供数据包转发使用
- 提供监控机制:允许外部模块监控邻居变化
该模块在VPP的数据包转发流程中起到关键作用,是连接IP层和以太网层的桥梁。