|
Linux内核邻接子系统(二层到三层) 邻接子系统的核心
邻居子系统,提供三层地址到二层地址之间的映射,提供二层首部缓存加速二层头的封装,提供二层报文头的封装。
在IPv4当中,实现这种转换的协议为地址解析协议(Address ResolutionProtocol,ARP),而在IPv6则为邻居发现协议(Neighbour Discovery Protocol,NDISC或ND),邻接子系统为执行L3到L2映射提供了独立于协议的基础设施。
ipv6 NDP: 邻居发现协议(Neighbour Discovery Protocol, NDP),邻居子系统为了执行L3到L2的映射提供了独立于协议的基础设施。请求和应答分别为邻居请求和邻居应答。。 ND(Neighbor Discovery,邻居发现)协议是IPv6的一个关键协议,它综合了IPv4中的ARP,ICMP路由发现和ICMP重定向等协议,并对他们做了改进。作为IPv6的基础性协议,ND协议还提供了前缀发现,邻居不可达检测,重复地址检测,地址自动配置等功能 NDP 加强了地址解析协议与底层链路的独立性、增强了安全性、减小了报文传播范围 **在第2层发送数据包时,为创建L2报头,需要使用L2目标地址,使用邻接系统进行请示和应答,便可根据主机的L3地址获悉其L2地址(或获悉这样的L3地址不存在)。在最常用的数据链路层(L2)–以太网中,主机的L2地址为MAC地址。传输当前主机生成的外出数据包或转发当前主机收到的数据包。**有时不需要邻接子系统的帮助也能够获悉目标地址。比如发送广播时,在这种情况L2目标地址是固定的,例如,在以太网中为FF:FF:FF:FF:FF:FF,有时目标地址是组播地址,L3组播地址和L2组播地址的映射关系是固定的。
邻居子系统,提供三层地址到二层地址之间的映射,提供二层首部缓存加速二层头的封装,提供二层报文头的封装。
如下,邻居表信息,表达了IP地址是x.x.x.x的下一跳,它的mac地址是xx:xx:xx:xx:xx:xx,通过出接口ethx能够到达。
#ip neigh 172.16.10.34 dev eth1 lladdr 52:54:00:8f:77:cd STALE 172.16.100.2 dev eth1 lladdr 00:1e:08:0a:53:01 STALE 192.168.122.1 dev eth2 lladdr 52:54:00:7a:39:1c STALE 172.16.100.3 dev eth1 lladdr 00:1e:08:0a:b2:f7 STALE 172.16.0.2 dev eth1 lladdr 00:1e:08:15:18:65 STALE 172.16.0.1 dev eth1 lladdr 50:c5:8d:b4:3e:81 REACHABLE 1.1.1.1 dev eth0 lladdr 52:54:00:e4:f7:11 PERMANENT 192.168.121.1 dev eth0 lladdr 52:54:00:8a:20:74 STALE 20.1.1.10 dev eth2.100 lladdr 52:54:00:e4:f7:2a STALE
Linux邻接系统的基本数据结构是邻居,表示与当前链路相连的网络结点,用结构neighbour来表示。
struct neighbour
如果一台主机和你的计算机连接在同一个LAN上(也就是说,你和这台主机通过一个共享介质相连或点对点直接相连),那么它就是你的邻居(neighbor),而且它有相同的L3网络配置。
具体内核源码分析如下: include\net\neighbour.h
struct neighbour { struct neighbour __rcu *next; // 指向散列表的同一个桶中的下一个邻居 struct neigh_table *tbl; // 与邻居相关联的邻接表(ARP表/arp缓存) struct neigh_parms *parms; // 与邻居相关联的neigh_parms对象,由相关联的邻接表的构造函数对其进行初始化 unsigned long confirmed; // 确认时间戳 unsigned long updated; rwlock_t lock; atomic_t refcnt; // 引用计数器 struct sk_buff_head arp_queue; // arp_queue:一个未解析SKB队列 unsigned int arp_queue_len_bytes; struct timer_list timer; // 定时器 unsigned long used; atomic_t probes; __u8 flags; __u8 nud_state; __u8 type; __u8 dead; seqlock_t ha_lock; // 邻居对象的硬件地址。在以太网中,它为邻居的MAC地址。 unsigned char ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))]; struct hh_cache hh; // L2报头的硬件报头缓存(一个hh_cache对象) int (*output)(struct neighbour *, struct sk_buff *); // 一个指向传输方法的指针 const struct neigh_ops *ops; struct rcu_head rcu; struct net_device *dev; u8 primary_key[0]; // primary_key:邻居的IP地址(L3地址)邻接表查找是根据primary_key进行的 };
parms: 与邻居相关联的neigh_parms对象,由相关联的邻接表的构造函数对其进行初始化。例如,在ipv4中,方法arp_constructor将parms初始化为相关联的网络设备的arp_parms。不要将其与邻接表的neigh_parms对象混为一谈。
refcnt: 引用计数器。neigh_hold宏会将其加1,而neigh_release宏则将其减1。仅当这个引用计数器的值被减为0时,方法neigh_release才会调用方法neigh_destroy来释放邻居对象。
timer: 每个neighbour对象都有一个定时器。定时器回调函数为方法neigh_timer_handler,它可以修改邻居的网络不可达检测(NUD)状态。在发送请求时,邻居的状态为NUD_INCOMPLETE或NUD_PROBE。如果请求数达到或超过neigh_max_probes的值,将把邻居的状态设置为NUD_FAILED,并调用方法neigh_invalidate。
ha_lock: 对邻居硬件地址( ha)提供访问保护。
ha: 邻居对象的硬件地址。在以太网中,它为邻居的MAC地址。
hh: L2报头的硬件报头缓存(一个hh_cache对象)。
output: 一个指向传输方法(如方法neigh_resolve_output或neigh_direct_output )的指针。其值取决于NUD状态,因此在邻居的生命周期内可赋给其不同的值。在方法neigh_alloc中初始化邻居对象时,会将其设置为方法neigh_blackhole。这个方法将丢弃数据包并返回-ENETDOWN。下面是设置output回调函数的辅助方法。
void neigh_connect(struct neighbour *neigh):将指定邻居的output回调函数设置为neigh->ops->connected_output。
void neigh_suspect(struct neighbour *neigh):将指定邻居的output回调函数设置为neigh->ops->output。
**nud_state: 邻居的NUD状态。**在邻居的生命周期内,可动态地修改nud_state的值。在7.5节中,表7-1描述了基本的NUD状态及其Linux符号。NUD状态机非常复杂。
更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.
Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂
下面这张状态机图描述的很清楚(状态变化)。
NUD_INCOMPLETE:该状态是请求报文已发送,但尚未收到应答的状态。该状态下还没解析到硬件地址,因此尚无可用硬件地址,如果有报文要输出到该邻居,会将其缓存起来。
这个状态会启动一个定时器,如果在定时器到期时还没有接收到邻居的回应,则会重复发送请求报文,否则发送请求报文的次数打到上限,便会进入NUD_FAILED。
NUD_REACHABLE :该状态以及得到并缓存了邻居的硬件地址。进入该状态首先设置邻居项相关的output函数(该状态使用neighbors_ops结构的connectd_outpt),然后查看是否存在要发送给该邻居的报文。如果在该状态下闲置时间达到上限,便会进入NUD_STATLE。
NUD_STALE :该状态一旦有报文要输出到该邻居,则会进入NUD_DELAY并将该报文输出。如果在该状态下闲置时间达到上限,且此时的引用计数为1,则通过垃圾回收机制将其删除,在该状态下,报文的输出不收限制,使用慢速发送过程
NUD_DELAY :该状态下表示NUD_STATE状态下发送的报文已经发出,需得到邻居的可达性确认的状态。在为接收到邻居的应答或确认时也会定时地重发请求,如果发送请求报文的次数到上限,如果收到邻居的应答,进入NUD_REACHABLE,否则进入NUD_FAILED,在该状态下,报文的输出不收限制,使用慢速发送过程。
NUD_PROBE :过渡状态,和NUD_INCOMPLETE 状态类似,在未收到邻居状态的应答或者确认时,也会定时的重发请求,直到收到邻居的应答、确认、或者尝试发送请求报文的次数达到上限,如果收到应答或者确认就会进入NUD_REACHABLE,如果尝试发送请求到达上限,则进入NUD_FAILD状态,在该状态,报文的输出也不受限制,使用慢速发送过程。
NUD_FAILED:由于没有收到应答报文而无法访问状态,
NUD_NOARP:标识邻居无需将三层地址协议映射到二层地址协议。如一些三层overlay的虚拟接口,loopback等。
NUD_PERMANENT: 设置邻居表项的硬件地址为静态。
dead: 一个标志**,在邻居对象处于活动状态时被设置。**创建邻居对象时,在方法_neigh_create末尾将其设置为0。对于dead标志未被设置的邻居对象,调用方法neigh_destroy将会失败。方法neigh_flush_dev将dead标志设置为1,但不会删除邻居。被标记为失效( dead标志被设置)的邻居由垃圾收集器删除。
primary_key: 邻居的IP地址(L3地址),邻接表查找是根据primary_key进行的。primary_key的长度因协议而异。例如,对于IPv4来说,其长度为4字节;对于IPv6来说,其长度为sizeof(struct in6_addr),因为结构in6_addr表示IPv6地址。因此,primary_key被定义为0字节的数组,分配邻居时,必须考虑使用的协议。详情请参阅后面描述结构neigh_table的成员时,对entry_size和key_len的解释。
**为避免在每次传输数据包时都发送请求,内核将L3地址和L2地址之间的映射存储在了被称为邻接表的数据结构中。**在IPv4中,这个表就是ARP表,有时被称为ARP缓存,但它们指的是一回事。在IPv6中,邻接表就是NDISC表(也叫NDISC缓存)。ARP表( arp_tbl )和NDISC表( nd_tbl)都是结构neigh_table的实例。下面就来看看结构neigh_table。
struct neigh_table { int family; // ipv4\ipv6 ipv4= arp_tbl, ipv6=nd_tbl int entry_size; // 邻居表项结构的大小,包括邻居表项和其key的信息,对于ipv4,是根据ipv4地址查询neighbor表项的,所以=sizeof(neighbour)+4 int key_len; // 查找键长度,就是上面用到的neighbor表项key长度,三层地址,arp就是ipv4地址 __be16 protocol; // 三层协议类型,ETH_P_IP 或者 ETH_P_IPV6 __u32 (*hash)(const void *pkey, const struct net_device *dev, __u32 *hash_rnd); // 表项hash函数,eg arp_hash bool (*key_eq)(const struct neighbour *, const void *pkey); int (*constructor)(struct neighbour *); // 创建邻居对象 int (*pconstructor)(struct pneigh_entry *); // 创建邻居代理对象(arp不使用,NDSIC使用) void (*pdestructor)(struct pneigh_entry *); void (*proxy_redo)(struct sk_buff *skb); char *id; // 邻接表名称,、arp_tabl为arp_cache struct neigh_parms parms; // 存储与协议相关的可调节参数 struct list_head parms_list; int gc_interval; // 这四个是垃圾回收的时间参数 int gc_thresh1; int gc_thresh2; int gc_thresh3; unsigned long last_flush; // 最近一次运行方法neigh_forced_gc的时间 struct delayed_work gc_work; // 垃圾回收的工作队列,异步垃圾回收层处理程序 struct timer_list proxy_timer; struct sk_buff_head proxy_queue; // 由SKB组成的代理ARP队列。SKB是使用方法pneigh_enqueue添加的。 atomic_t entries; // 所有邻居项的数目 rwlock_t lock; unsigned long last_rand; struct neigh_statistics __perCPU *stats; // 邻居统计信息( neigh_statistics )对象 struct neigh_hash_table __rcu *nht; // 邻居散列表(neigh_hash_table对象) struct pneigh_entry **phash_buckets; // 邻接代理散列表 };
**proxy_timer:主机被配置为ARP代理时,它可能不会立即处理请求,而是过一段时间再处理。这是因为,对于ARP代理主机来说,可能有大量的请求需要处理(这不同于不是ARP代理的主机,通常它们需要处理的ARP请求较少)。有时候,你可能希望延迟对这种广播做出应答,让拥有要解析的IP地址的主机先收到请求。**这种延迟是随机的,最长不超过参数proxy_delay的值。对于ARP来说,代理定时器处理程序为方法neigh proxy_process。proxy_timer由方法neigh_table_init_no_netlink进行初始化。
stats:邻居统计信息( neigh_statistics )对象,包含针对每个CPU的计数器,如allocs(方法neigh_alloc分配的邻居对象数)、destroys(方法neigh_destroy释放的邻居对象数)等。邻居统计信息计数器由NEIGH_CACHE_STAT_INC宏进行递增操作。请注意,由于这些统计信息是针对每个CPU的计数器的,因此NEIGH_CACHE_STAT_INC宏将调用this_cpu_inc宏。要显示ARP统计信息和NDISC统计信息,可分别使用cat /proc/netstat/arp_cache和cat/proc/net/stat/ndisc_cache。在7.5节中,描述了结neigh_statistics,并指出了每个计数器的递增方法。
phash_buckets:邻接代理散列表,是在方法neigh_table_init_no_netlink中分配的。邻接表的初始化工作是使用方neigh_table_init完成的。
在IPv4中,ARP模块定义了ARP表(一个名为arp_tbl的neigh_table结构实例),并将其作为参数传递给方法neigh_table_init(参见net/ipv4/arp.c中的方法arp_init ).
在IPv6中,NDISC模块定义了NDSIC表(一个名为nd_tbl的neigh_table结构实例),并将其作为参数传递给方法neigh_table_init(参见net/ipv6/ndisc.c中的方法ndisc_init )。方法neigh_table_init还可调用方法neigh_table_init_no_netlink,后者将调用方法neigh_hash_alloc创建邻接散列表(对象nht ),以便为8个散列条目分配空间。
使用邻接子系统的每种L3协议都还注册一个协议处理程序。对于IPv4来讲,ARP数据包处理程序方法为arp_rcv arp.c
struct neigh_ops
每个邻居对象结构neigh_ops中定义一组方法,它包含一个协议簇成员和4个函数指针,具体内核源码如下
struct neigh_ops { int family; // 发送请求报文函数。在发送一个报文时,需要更新邻居表项, // 发送报文会缓存到arp_queue中,然后调用solicit函数发送请求报文。 void (*solicit)(struct neighbour *, struct sk_buff *); // 在邻居状态为NUD_FAILED时,将在方法neigh_invalidate调用该方法,例如在请求应答时间超时后就将出现这种情况。 // 邻居项缓存着未发送的报文,而该邻居项又不可达时, 被调用来向三层报告错误的函数。 void (*error_report)(struct neighbour *, struct sk_buff *); // 在下一跳的L3地址已知,但未能解析出L2地址时,应将output设置为neigh_resolve_output。 // 通用输出报文函数,做邻居状态等校验,流程上会比connected_output 慢一些 int (*output)(struct neighbour *, struct sk_buff *); // 当邻居可达NUD_CONNECT的时候,肯定处于邻居可用状态,直接构造和封装二层头发送。 // 邻居状态为NUD_REACHABLE或NUD_CONNECTED时,应将output方法设置为connected_output指定的方法。 int (*connected_output)(struct neighbour *, struct sk_buff *); };邻居创建是由_neigh_create处理方法_neigh_create首先调用方法neigh_alloc,以分配一个邻居对象并执行各种初始化。在某些情况下,方法neigh_alloc还将调用同步垃圾收集器(方法neigh_forced_gc ).
接下来,方法__neigh_create将调用指定邻接表的constructor方法(对于ARP来说,该方法为arp_constructor;对于NDISC来说,该方法为ndisc_constructor ),以执行因协议而异的设置工作。
在constructor方法中,将处理组播地址和环回地址等特殊情况。例如,在方法arp_constructor中,需调用方法arp_mc_map,来根据邻居的IPv4 primary_key地址设置邻居的硬件地址( ha ),再将nud_state设置为NUD_NOARP,因为组播地址不需要ARP。
对于广播地址也需特殊对待。例如,在方法arp_constructor中,如果邻居类型为RTN_BROADCAST,就将其硬件地址( ha)设置为网络设备的广播地址( net_device对象的broadcast字段)、并将nud_state设置为NUD_NOARP。
在方法_neigh_create的最后,将dead标志初始化为0,并将邻居对象添加到邻居散列表中。
邻居删除是由neigh_release处理
方法neigh_release将邻居的引用计数器减1。如果它变成了0,就调用方法neigh_destroy将邻居对象释放。方法neigh_destroy会检查邻居的dead标志。如果该标志为0,就不会将邻居删除。
用户空间和邻接子系统之间的交互
管理ARP表,可使用iproute2包中的命令ip neigh,也可以使用net_tools包中的命令arp
arp: 由net/ipv4/arp.c中的万法arp_seq_show(处理)。
ip neigh show(或ip neighbour show ):由net/ core/neighbour.c中的方法neigh_dump_info处理。
请注意,命令ip neigh show显示邻接表条目的NUD状态,如NUD_REACHABLE或NUD_STALE。另外,命令arp只显示IPv4邻接表(ARP表),而命令ip显示IPv4 ARP表和IPv6邻接表。如果只想显示IPv6邻接表,可使用命令ip -6 neigh show。
root@XYF:~# arp Address HWtype HWaddress Flags Mask Iface _gateway ether ee:ff:ff:ff:ff:ff C eth0
ARP和NDISC模块还可通过procfs导出数据。这意味着,要显示ARP表,还可执行命令cat/proc/net/arp(这个procfs条目由方法arp_seq_show处理,该方法也用于处理前面提到的命令arp )。要显示ARP统计信息,可使用命令cat/proc/net/stat/ arp_cache;而要显示NDISC统计信息,可使用命令cat /proc/net/stat/ndisc_cache(这两个命令都由方法neigh_stat_seq_show处理)。
net\ipv4\arp.c
static int arp_seq_show(struct seq_file *seq, void *v) { if (v == SEQ_START_TOKEN) { seq_puts(seq, "IP address HW type Flags " "HW address Mask Device\n"); } else { struct neigh_seq_state *state = seq->private; if (state->flags & NEIGH_SEQ_IS_PNEIGH) arp_format_pneigh_entry(seq, v); else arp_format_neigh_entry(seq, v); } return 0; }
**要添加邻居条目,可使用命令ip neigh add。**这个命令由方法neigh_add处理。执行命令ipneigh add时,可指定要添加的邻居条目的状态(如NUD_PERMANENT、NUD_STALE、NUD_REACHABLE等),如下所示。
ip neigh add 192.168.0.121 dev etho lladdr 00:30:48:5b:cc:45 nud permanent
要删除邻居条目,可使用命令ip neigh del(这个命令由方法neigh_delete处理),如下所示。
ip neigh del 192.168.0.121 dev etho
要在代理ARP表中添加条目,可使用命令ip neigh add proxy,如下所示。ip neigh add proxy 192.168.2.11 dew etho
这种添加工作也可由方法neigh_add进行处理,但该方法将在从用户空间传递而来的数据中设置标志NTF_PROXY(参见对象ndm的ndm_flags字段),因此将调用方法pneigh_lookup在代理邻接表( phash_buckets )中执行查找。如果没有找到,方法pneigh_lookup将在代理邻接散列表中添加一个条目。
要从代理ARP表中删除条目,可使用命令ip neigh del proxy,如下所示。ip neigh del proxy 192.168.2.11 dev etho
这种删除工作由方法neigh_delete处理。同样,在这种情况下,将在从用户空间传递而来的数据中设置NTF_PROXY标志(参见对象ndm的ndm_flags字段),因此将调用方法pneigh_delete.将条目从代理邻接表中删除。
使用命令ip ntable可显示和控制邻接表的参数,如下所示。ip ntable show:显示所有邻接表的参数。
ip ntable change:修改邻接表参数的值,由方法neightbl_set处理,如ip ntable change name arp_cache queue 20 dev etho。
还可以使用命令arp add在ARP表中添加条目。另外,还可以像下面这样在ARP表中添加静态条目: arp -s 。静态ARP条目不会被邻接子系统垃圾收集器删除,但会在重启后消失。
邻接核心不会使用方法register_netdevice_notifier注册任何事件,而ARP和NDISC模块则会注册网络事件。在ARP中,方法arp_netdev_event将被注册为netdev事件的回调函数,它调用通用方法neigh_changeaddr以及方法rt_cache_flush来处理MAC地址变更事件。从内核3.11起,在IFF_NOARP标志发生变化时,将调用方法neigh_changeaddr来处理NETDEV_CHANGE事件。当设备使用方法_dev_notify_flags修改其标志或使用方法netdev_state_change修改其状态时,都将触发NETDEV_CHANGE事件。在NDISC中,方法ndisc_netdev_event被注册为netdev事件的回调函数,它处理NETDEV_CHANGEADDR、NETDEV_DOwN和NETDEV_NOTIFY_PEERS事件。 - - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛
转载地址:Linux内核邻接子系统(arp协议) 的工作原理(一) - 圈点 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛
来源:http://www.yidianzixun.com/article/0ii24kf1
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|