当前位置:   article > 正文

Linux内核邻接子系统(arp协议)的工作原理_ndisc

ndisc

主要参考了《深入linux内核架构》和《精通Linux内核网络》相关章节

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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进行的
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 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状态机非常复杂。

    下面这张状态机图描述的很清楚(状态变化)。

    image.png

    • 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;  // 邻接代理散列表
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • **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 *);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

邻居创建是由_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
  • 1
  • 2
  • 3

​ 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

​ **要添加邻居条目,可使用命令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 <IPAddress> <MacAddress>。静态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事件。

ARP协议(工作在三层)

​ 地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。地址解析协议是建立在网络中各个主机互相信任的基础上的,局域网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存;由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。相关协议有RARP代理ARPNDP用于在IPv6中代替地址解析协议。

代理ARP(proxy ARP):对于没有配置缺省网关的计算机要和其他网络中的计算机实现通信,网关收到源计算机的 ARP 请求会使用自己的 MAC 地址与目标计算机的 IP地址对源计算机进行应答。**代理ARP就是将一个主机作为对另一个主机ARP进行应答。它能使得在不影响路由表的情况下添加一个新的Router,使得子网对该主机来说变得更透明化。**同时也会带来巨大的风险,除了ARP欺骗,和某个网段内的ARP增加,最重要的就是无法对网络拓扑进行网络概括。代理ARP的使用一般是使用在没有配置默认网关和路由策略的网络上的。

​ ARP协议是在RFC 826中定义的。在以太网中,硬件地址称为MAC地址,长48位。MAC地址必须是独一无二的,但必须考虑这样的情形,即可能会遇到并非独一无二的MAC地址。导致这种情形的一种常见原因是,在大多数网络接口上,系统管理员都可使用诸如ifconfig或ip等用户空间工具配置MAC地址。
​ 发送IPv4数据包时,目标IPv4地址是已知的,但需要创建以太网报头,其中包含目标MAC地址。根据给定IPv4地址确定MAC地址的工作由ARP协议完成,稍后你讲看到这一点。如果MAC地址未知,就以广播方式发送ARP请求,其中包含已知的IPv4地址。如果有主机配置了这个IPv4地址,它将使用单播ARP响应进行应答。ARP表( arp_tbl)是一个neigh_table结构实例。ARP报头用结构arphdr表示。

arphdr

include\linux\if_arp.h

struct arphdr {
	__be16		ar_hrd;		/* format of hardware address	*/
	__be16		ar_pro;		/* format of protocol address	*/
	unsigned char	ar_hln;		/* length of hardware address	*/
	unsigned char	ar_pln;		/* length of protocol address	*/
	__be16		ar_op;		/* ARP opcode (command)		*/

#if 0
	 /*
	  *	在以太网中,类似于下面这样,但这部分的长度并不是困定的
	  */
	unsigned char		ar_sha[ETH_ALEN];	/* sender hardware address	*/
	unsigned char		ar_sip[4];		/* sender IP address		*/
	unsigned char		ar_tha[ETH_ALEN];	/* target hardware address	*/
	unsigned char		ar_tip[4];		/* target IP address		*/
#endif

};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • ar_hrd是硬件地址类型,对于以太网来说,其为0x01。关于可在ARP报头中使用的硬件地址标识符完整列表,请参阅include/uapi/linux/if_arp.h中的ARPHRD_XXX定义。
  • ar_pro是协议ID,对于IPv4来说,其为0x80。关于可使用的协议ID完整列表,请参阅include/uapi/linux/if_ether.h中的ETH_P_XXX定义。
  • ar_hln是硬件地址长度,单位为字节。对于以太网地址来说,其为6字节。
  • ar_pln是协议地址长度,单位为字节。对于IPv4地址来说,其为4字节。
  • ar_op是操作码。ARP请求表示为ARPOP_REQUEST,ARP应答表示为ARPOP_REPLY。

紧跟在ar_op后面的是发送方的硬件(MAC)地址和IPv4地址,以及目标硬件(MAC)地址和IPv4地址。这些地址并非ARP报头(结构arphdr )的组成部分。在方法arp_process()中,通过读取ARP报头相应的偏移量来提取它们。在讨论方法arp_process()时,你将看到这一点。下图显示了ARP以太网数据包的ARP报头。

image-20220712193432061

ARP包是前面是MAC首部 —— 目地MAC地址(8字节)源MAC地址(8字节)类型(2字节)

ARP 解析 MAC 地址

  1. 首先,每台主机都会在自己的ARP缓冲区(ARP Cache)中建立一个 ARP列表,以表示IP地址和MAC地址的对应关系。

  2. 当源主机需要将一个数据包要发送到目的主机时,会首先检查自己 ARP列表中是否存在该 IP地址对应的MAC地址,如果有﹐就直接将数据包发送到这个MAC地址;如果没有,就向本地网段发起一个ARP请求的广播包,查询此目的主机对应的MAC地址。此ARP请求数据包里包括源主机的IP地址、硬件地址、以及目的主机的IP地址。

  3. 网络中所有的主机收到这个ARP请求后,会检查数据包中的目的IP是否和自己的IP地址一致。如果不相同就忽略此数据包;如果相同,该主机首先将发送端的MAC地址和IP地址添加到自己的ARP列表中,如果ARP表中已经存在该IP的信息,则将其覆盖,然后给源主机发送一个 ARP响应数据包,告诉对方自己是它需要查找的MAC地址;

  4. 源主机收到这个ARP响应数据包后,将得到的目的主机的IP地址和MAC地址添加到自己的ARP列表中,并利用此信息开始数据的传输。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。

发送 ARP报文

ARP 发送请求

  • 请求是在哪里发送的呢?最常见的场景是在传输路径中。在离开网络层(L3)进人数据链路层(L2)之前。在方法ip_finish_output2()中,首先调用方法_ipv4_neigh_lookup_noref(),在ARP表中查找下一跳IPv4地址。如果没有找到匹配的邻居条目,就调用方法_neigh_create()来创建一个。
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct dst_entry *dst = skb_dst(skb);
	struct rtable *rt = (struct rtable *)dst;
	struct net_device *dev = dst->dev;
	unsigned int hh_len = LL_RESERVED_SPACE(dev);
	struct neighbour *neigh;
	u32 nexthop;
...
	neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
	if (unlikely(!neigh))
		neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

ARP发送报文

  • arp_solicit函数,邻居子系统调用solicit函数指针 (neigh_ops) 发送solicitation请求,在arp指针中这个函数指针被初始化为arp_solicit函数。

    static void arp_solicit(struct neighbour *neigh, struct sk_buff *skb);
    
    • 1
    • 最终调用方法arp_send()来发送ARP请求。我们注意到最后一个参数(target_hw)为NULL,因为还不知道目标硬件(MAC)地址。在调用arp_send()时,如果参数target_hw为NULL,将以广播方式发送ARP请求。

      void arp_send(int type, int ptype, __be32 dest_ip,
      	      struct net_device *dev, __be32 src_ip,
      	      const unsigned char *dest_hw, const unsigned char *src_hw,
      	      const unsigned char *target_hw)
      {
      	arp_send_dst(type, ptype, dest_ip, dev, src_ip, dest_hw, src_hw,
      		     target_hw, NULL);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

接收 ARP报文

  • arp_rcv函数被注册到内核中,当有ARP协议报文调用arp_rcv处理。
    某些情况下收到一个ARP报文可能导致发出一个ARP报文,这些情况是:
    • 配置了网桥,网桥只是转发报文到其他接口。
    • 邻居子系统对请求报文做出应答。
static int arp_rcv(struct sk_buff *skb, struct net_device *dev,
		   struct packet_type *pt, struct net_device *orig_dev)
{
	const struct arphdr *arp;

	/* do not tweak dropwatch on an ARP we will ignore */
    // 如果收到ARP数据包的网络设备设置标志为IFF_NOARP,
    //   或者数据包不是发送给当前主机,或数据包是发送给环回设备的,就必须将数据包丢弃。
	if (dev->flags & IFF_NOARP ||
	    skb->pkt_type == PACKET_OTHERHOST ||
	    skb->pkt_type == PACKET_LOOPBACK)
		goto consumeskb;
    
	// 如果SKB是共享的,就必须复制它,因为在方法arp_rcv()进行处理期间,它可能被其他人
	// 修改。如果SKB是共享的,方法skb_share_check()就将创建其副本
	skb = skb_share_check(skb, GFP_ATOMIC);
	if (!skb)
		goto out_of_mem;

	/* ARP报头,两个设备地址,两个IP地址  */
	if (!pskb_may_pull(skb, arp_hdr_len(dev)))
		goto freeskb;

	arp = arp_hdr(skb);
    // ARP报头的ar_hln表示硬件地址的长度。对于以太网报头来说,其应为6字节,
    // 并与net_device对象的addr_len相等。ARP报头的ar_pln表示协议地址的长度,它应与IPv4地址的长度相等,即为4字节。
	if (arp->ar_hln != dev->addr_len || arp->ar_pln != 4)
		goto freeskb;

	memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));

    // 如果一切正常,就接着调用方法arp_process(),由它执行处理ARP数据包的实际工作。
	return NF_HOOK(NFPROTO_ARP, NF_ARP_IN,
		       dev_net(dev), NULL, skb, dev, NULL,
		       arp_process);

consumeskb:
	consume_skb(skb);
	return NET_RX_SUCCESS;
freeskb:
	kfree_skb(skb);
out_of_mem:
	return NET_RX_DROP;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

在方法arp_process()中,只处理ARP报文请求和响应(回复)

  • 对于ARP请求,将使用方法ip_route_input_noref()执行路由选择子系统查找。
    • 如果ARP数据包是发送给当前主机的(路由选择条目的rt_type为RTN_LOCAL),就接着检查一些条件(这将稍后介绍)。如果这些检查都通过了,就使用方法arp_send()发回ARP应答。
    • 如果ARP数据包不是发送给当前主机但需要进行转发的(路由选择条目的rt_type为RTN_UNICAST),也需要检查一些条件(也将在稍后介绍)。如果这些条件都满足,就调用方法pneigh_lookup()在代理ARP表中进行查找。

比如arp_process() 首先验证ARP报文头和设备是否使能ARP功能。
arp_process函数只处理ARPOP_REPLY和ARPOP_REQUEST报文类型。

...
    /* Understand only these message types */

	if (arp->ar_op != htons(ARPOP_REPLY) &&
	    arp->ar_op != htons(ARPOP_REQUEST))
		goto out_free_skb;
...
if (n) {
		int state = NUD_REACHABLE;
		int override;

		/* 如果连续收到多个ARP应答,将使用第一个应答,如果有多个代理处于活动状态,
		就可能出现这种情况,使用第一个应答可避免ARP受损,并确保选择的是最快的路由器。
		 */
		override = time_after(jiffies,
				      n->updated +
				      NEIGH_VAR(n->parms, LOCKTIME)) ||
			   is_garp;

		/* Broadcast replies and request packets
		   do not assert neighbour reachability.
		 */
		if (arp->ar_op != htons(ARPOP_REPLY) ||
		    skb->pkt_type != PACKET_HOST)
			state = NUD_STALE;
		neigh_update(n, sha, state,
			     override ? NEIGH_UPDATE_F_OVERRIDE : 0, 0);
		neigh_release(n);
	}
out_consume_skb:
	consume_skb(skb);

out_free_dst:
	dst_release(reply_dst);
	return NET_RX_SUCCESS;

out_free_skb:
	kfree_skb(skb);
	return NET_RX_DROP;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 主机还不知道默认网关的MAC地址,它会发起ARP的请求
    • 主机生成一个包含目的地址为网关路由器 IP 地址(DHCP)的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。
  • 默认网关把它自己的MAC地址作为应答回来
  • Host1开始封装它的数据了,目的为默认网关的MAC地址
  • Host1发出的数据通过MAC寻址送到了默认网关
  • 默认网关收到数据之后(解封装、提取出目的IP、查表)然后转发出去(重新封装,源/目的MAC地址都发送了转换),最后就把这个数据发往Host4所在的EE网络(当然有可能也不知道Host4的MAC地址,这个时候网关一样会发送ARP请求)
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/335921
推荐阅读
相关标签
  

闽ICP备14008679号