Linux 内核网络教程(三)
协议:CC BY-NC-SA 4.0
六、高级路由
第五章讲述了 IPv4 路由子系统。本章继续讲述路由子系统,并讨论高级 IPv4 路由主题,如多播路由、多路径路由、策略路由等。这本书讨论的是 Linux 内核网络实现——它没有深入研究用户空间多播路由守护进程实现的内部,这些实现非常复杂,超出了本书的范围。然而,我确实在某种程度上讨论了用户空间多播路由守护进程和内核中多播层之间的交互。我还简要讨论了互联网组管理协议(IGMP)协议,这是组播组成员管理的基础;添加和删除多播组成员是由 IGMP 协议完成的。要理解多播主机和多播路由器之间的交互,需要一些 IGMP 的基本知识。
多路径路由能够在一条路由中添加多个下一跳。策略路由支持配置不仅仅基于目的地址的路由策略。我从描述多播路由开始。
多播路由
第四章在“接收 IPv4 组播数据包”一节中简要提到了组播路由。我现在将更深入地讨论它。发送多播流量意味着向多个接收者发送相同的数据包。此功能在流媒体、音频/视频会议等方面非常有用。在节省网络带宽方面,它比单播流量有明显的优势。多播地址被定义为 D 类地址。该组的无类域间路由(CIDR)前缀是 224.0.0.0/4。IPv4 多播地址的范围是从 224.0.0.0 到 239.255.255.255。处理多播路由必须结合与内核交互的用户空间路由守护进程来完成。根据 Linux 实现,与单播路由相反,如果没有这个用户空间路由守护进程,多播路由不能仅由内核代码处理。有各种各样的多播守护进程:例如:mrouted,它基于距离矢量多播路由协议(DVMRP)的实现,或者pimd,它基于与协议无关的多播协议(PIM)。RFC 1075 中定义了 DVMRP 协议,它是第一个多播路由协议。它基于路由信息协议(RIP)协议。
PIM 协议有两个版本,,内核都支持(配置 _IP_PIMSM_V1 和配置 _IP_PIMSM_V2)。PIM 有四种不同的模式:PIM-SM (PIM 稀疏模式)、PIM-DM (PIM 密集模式)、PIM 源特定多播(PIM-SSM)和双向 PIM。该协议被称为协议独立,因为它不依赖于任何特定的路由协议进行拓扑发现。本节讨论用户空间守护进程和内核多播路由层之间的交互。深入研究 PIM 协议或 DVMRP 协议(或任何其他多播路由协议)的内部已经超出了本书的范围。通常,多播路由查找基于源地址和目的地址。有一个“多播策略路由”内核特性,它与第五章中提到的单播策略路由内核特性类似,也将在本章中讨论。多播策略路由协议是使用策略路由 API 实现的(例如,它调用fib_rules_lookup()方法来执行查找,创建fib_rules_ops对象,并用fib_rules_register()方法注册它,等等)。使用多播策略路由,路由可以基于附加标准,如入口网络接口。此外,您可以使用多个多播路由表。为了使用多播策略路由,必须设置 IP_MROUTE_MULTIPLE_TABLES。
图 6-1 显示了一个简单的 IPv4 组播路由设置。拓扑结构非常简单:左边的笔记本电脑通过发送一个 IGMP 数据包(IP_ADD_MEMBERSHIP)加入一个组播组(224.225.0.1)。IGMP 协议将在下一节“IGMP 协议”中讨论中间的 AMD 服务器被配置为组播路由器,用户空间组播路由守护进程(如pimd或mrouted)在上面运行。右边的 Windows 服务器的 IP 地址为 192.168.2.10,它向 224.225.0.1 发送组播流量;该流量通过多播路由器转发到笔记本电脑。请注意,Windows 服务器本身没有加入 224.225.0.1 多播组。运行ip route add 224.0.0.0/4 dev <networkDeviceName>告诉内核通过指定的网络设备发送所有多播流量。
图 6-1 。简单多播路由设置
下一节讨论 IGMP 协议,它用于管理多播组成员。
《IGMP 议定书》
IGMP 协议是 IPv4 多播不可分割的一部分。它必须在支持 IPv4 多播的每个节点上实现。在 IPv6 中,组播管理由 MLD(组播监听发现)协议处理,该协议使用 ICMPv6 消息,在第八章的中讨论。使用 IGMP 协议,可以建立和管理多播组成员。IGMP 有三个版本:
-
*igmp v1(RFC 1112)**:*有两种类型的消息—主机成员报告和主机成员查询。当主机想要加入多播组时,它发送成员报告消息。多播路由器发送成员资格查询,以发现哪些主机多播组在其连接的本地网络上有成员。查询被发送到所有主机组地址(224.0.0.1,IGMP 所有主机)并携带 TTL 1,以便成员资格查询不会传播到 LAN 之外。
-
IGMPv2 (RFC 2236)**: This is an extension of IGMPv1. The IGMPv2 protocol adds three new messages:
- 成员资格查询(0x11):有两个子类型的成员资格查询消息:一般查询,用于了解哪些组在所连接的网络上具有成员,以及特定于组的查询,用于了解特定组在所连接的网络上是否具有任何成员。
- 版本 2 成员报告(0x16)。
- 离开组(0x17)。
注意 IGMPv2 也支持版本 1 成员报告消息,以向后兼容 IGMPv1。参见 RFC 2236 第 2.1 节。
-
IGMPv3 (RFC 3376,由 RFC 4604 更新) : 此次协议重大修订增加了一项名为源过滤的功能。这意味着当主机加入多播组时,它可以指定一组源地址,从这些地址接收多播流量。源过滤器也可以排除源地址。为了支持源过滤特性,socket API 被扩展;请参见 RFC 3678,“多播源过滤器的套接字接口扩展”我还应该提到,多播路由器定期(大约每两分钟)向所有主机多播组地址 224.0.0.1 发送成员查询。接收成员资格查询的主机用成员资格报告来响应。这是在内核中由
igmp_rcv()方法实现的:获取 IGMP _ 主机 _ 成员资格 _ 查询消息由igmp_heard_query()方法处理。
注IP v4 IGMP 的内核实现在
net/core/igmp.c、include/linux/igmp.h、include/uapi/linux/igmp.h。
下一节研究 IPv4 多播路由的基本数据结构、多播路由表及其 Linux 实现。
多播路由表
多播路由表由名为mr_table的结构表示。我们来看看:
struct mr_table {
struct list_head list;
#ifdef CONFIG_NET_NS
struct net *net;
#endif
u32 id;
struct sock __rcu *mroute_sk;
struct timer_list ipmr_expire_timer;
struct list_head mfc_unres_queue;
struct list_head mfc_cache_array[MFC_LINES];
struct vif_device vif_table[MAXVIFS];
. . .
};
(net/ipv4/ipmr.c)
以下是对mr_table结构中一些成员的描述:
net:组播路由表关联的网络名称空间;默认情况下,它是初始网络名称空间init_net。网络名称空间在第十四章中讨论。id:组播路由表 id;当使用单个表时,它是 RT_TABLE_DEFAULT (253)。mroute_sk:这个指针代表内核保存的用户空间套接字的引用。通过使用MRT_INIT套接字选项从用户空间调用setsockopt()来初始化mroute_sk指针,并通过使用 MRT_DONE 套接字选项调用setsockopt()来使其无效。用户空间和内核之间的交互是基于调用setsockopt()方法、从用户空间发送 IOCTLs、构建 IGMP 包并通过从内核调用sock_queue_rcv_skb()方法将它们传递给多播路由守护进程。ipmr_expire_timer:清理未解析组播路由条目的定时器。这个定时器在用ipmr_new_table()方法创建组播路由表时被初始化,在用ipmr_free_table()方法删除组播路由表时被删除。mfc_unres_queue:未解析的路由条目队列。mfc_cache_array:一个路由条目的缓存,有 64 个(MFC_LINES)条目,将在下一节中简要讨论。vif_table[MAXVIFS]:32 个(MAXVIFS)vif_device对象的数组。通过vif_add()方法添加条目,通过vif_delete()方法删除条目。vif_device结构代表一个虚拟组播路由网络接口;它可以基于物理设备或 IPIP (IP over IP)隧道。vif_device结构将在后面的“Vif 装置”章节中讨论。
我已经介绍了组播路由表,提到了它的重要成员,比如组播转发缓存(MFC) 和未解析路由条目的队列。接下来我将看看 MFC,它嵌入在多播路由表对象中,在多播路由中起着重要的作用。
组播转发缓存(MFC)
多播路由表中最重要的数据结构是 MFC,它实际上是缓存条目(mfc_cache对象)的数组。这个名为mfc_cache_array的数组嵌入在组播路由表(mr_table)对象中。它有 64 个(MFC_LINES)元素。这个数组的索引是散列值(散列函数接受两个参数—多播组地址和源 IP 地址;请参见本章末尾“快速参考”一节中对 MFC_HASH 宏的描述)。
通常只有一个多播路由表,它是mr_table结构的一个实例,对它的引用保存在 IPv4 网络名称空间(net->ipv4.mrt)中。该表是由ipmr_rules_init()方法创建的,该方法还指定net->ipv4.mrt指向创建的组播路由表。当使用前面提到的多播策略路由功能时,可以有多个多播策略路由表。在这两种情况下,你用同样的方法得到路由表,ipmr_fib_lookup()。ipmr_fib_lookup()方法获得三个参数作为输入:网络名称空间、流和指向它应该填充的mr_table对象的指针。正常情况下,它只是将指定的mr_table指针设置为net->ipv4.mrt;当处理多个表时(设置了 IP_MROUTE_MULTIPLE_TABLES),实现更加复杂。让我们来看看mfc_cache的结构:
struct mfc_cache {
struct list_head list;
__be32 mfc_mcastgrp;
__be32 mfc_origin;
vifi_t mfc_parent;
int mfc_flags;
union {
struct {
unsigned long expires;
struct sk_buff_head unresolved; /* Unresolved buffers */
} unres;
struct {
unsigned long last_assert;
int minvif;
int maxvif;
unsigned long bytes;
unsigned long pkt;
unsigned long wrong_if;
unsigned char ttls[MAXVIFS]; /* TTL thresholds */
} res;
} mfc_un;
struct rcu_head rcu;
};
(include/linux/mroute.h)
以下是对mfc_cache结构中一些成员的描述:
-
mfc_mcastgrp:该条目所属组播组的地址。 -
mfc_origin:路由的源地址。 -
mfc_parent:源接口。 -
mfc_flags:条目的标志。可以有下列值之一: -
MFC_STATIC:当路由是静态添加的,而不是由多播路由守护进程添加的。
-
MFC_NOTIFY:路由条目的 RTM_F_NOTIFY 标志设置的时间。更多细节见
rt_fill_info()方法和ipmr_get_route()方法。 -
mfc_un联合由两部分组成: -
unres:未解析的缓存条目。 -
res:解析的缓存条目。
某个流的 SKB 第一次到达内核时,它被添加到未解析条目的队列中(mfc_un.unres.unresolved),其中最多可以保存三个 skb。如果队列中有三个 skb,那么数据包不会被添加到队列中,而是被释放,ipmr_cache_unresolved()方法返回-ENOBUFS("没有可用的缓冲区空间"):
static int ipmr_cache_unresolved(struct mr_table *mrt, vifi_t vifi, struct sk_buff *skb)
{
. . .
if (c->mfc_un.unres.unresolved.qlen > 3) {
kfree_skb(skb);
err = -ENOBUFS;
} else {
. . .
}
(net/ipv4/ipmr.c)
本节描述了 MFC 及其重要成员,包括已解析条目队列和未解析条目队列。下一节将简要描述什么是多播路由器,以及它在 Linux 中是如何配置的。
多播路由器
为了将机器配置为多播路由器,您应该设置 CONFIG_IP_MROUTE 内核配置选项。您还应该运行一些路由守护进程,如前面提到的pimd或mrouted。这些路由守护进程创建一个套接字来与内核通信。例如,在pimd中,您通过调用socket(AF_INET, SOCK_RAW, IPPROTO_IGMP)创建一个原始的 IGMP 套接字。在这个套接字上调用setsockopt()会触发向内核发送命令,这些命令由ip_mroute_setsockopt()方法处理。当使用 MRT_INIT 从路由守护进程调用这个套接字上的setsockopt()时,内核被设置为在所使用的mr_table对象的mroute_sk字段中保存对用户空间套接字的引用,并且通过调用 IPV4_DEVCONF_ALL(net,MC_FORWARDING)++来设置mc_forwarding procfs条目(/proc/sys/net/ipv4/conf/all/mc_forwarding)。注意,mc_forwarding procfs条目是一个只读条目,不能从用户空间设置。您不能创建多播路由守护进程的另一个实例:当处理 MRT_INIT 选项时,ip_mroute_setsockopt()方法检查mr_table对象的mroute_sk字段是否已初始化,如果是,则返回-EADDRINUSE。添加网络接口是通过在这个套接字上用 MRT_ADD_VIF 调用setsockopt()完成的,删除网络接口是通过在这个套接字上用 MRT_DEL_VIF 调用setsockopt()完成的。您可以通过传递一个vifctl对象作为setsockopt()系统调用的optval参数,将网络接口的参数传递给这些setsockopt()调用。让我们来看看vifctl的结构:
struct vifctl {
vifi_t vifc_vifi; /* Index of VIF */
unsigned char vifc_flags; /* VIFF_ flags */
unsigned char vifc_threshold; /* ttl limit */
unsigned int vifc_rate_limit; /* Rate limiter values (NI) */
union {
struct in_addr vifc_lcl_addr; /* Local interface address */
int vifc_lcl_ifindex; /* Local interface index */
};
struct in_addr vifc_rmt_addr; /* IPIP tunnel addr */
};
(include/uapi/linux/mroute.h)
以下是对vifctl结构中一些成员的描述:
-
vifc_flags可以是: -
当你想使用 IPIP 隧道时。
-
VIFF_REGISTER:当你想注册接口的时候。
-
VIFF_USE_IFINDEX:当你想使用本地接口索引而不是本地接口 IP 地址时;在这种情况下,您将把
vifc_lcl_ifindex设置为本地接口索引。VIFF_USE_IFINDEX 标志适用于 2.6.33 及更高版本的内核。 -
vifc_lcl_addr:本地接口 IP 地址。(这是默认设置,不应该为使用它设置任何标志)。 -
vifc_lcl_ifindex:本地接口索引。当vifc_flags中的 VIFF_USE_IFINDEX 标志置位时,应置位该位。 -
vifc_rmt_addr:隧道的远程节点的地址。
当多播路由守护进程关闭时,使用 MRT_DONE 选项调用setsockopt()方法 。这将触发调用mrtsock_destruct()方法来使所使用的mr_table对象的mroute_sk字段无效,并执行各种清理。
本节讲述了什么是多播路由器,以及在 Linux 中如何配置它。我还检查了vifctl结构。接下来,我看一下 Vif 设备,它代表一个多播网络接口。
Vif 设备
多播路由支持两种模式:直接多播和封装在隧道上的单播包中的多播。在这两种情况下,使用相同的对象(vif_device结构的一个实例)来表示网络接口。在隧道上工作时,将设置 VIFF_TUNNEL 标志。添加和删除多播接口分别由vif_add()方法和vif_delete()方法完成。vif_add()方法还通过调用dev_set_allmulti(dev, 1)方法将设备设置为支持组播,该方法递增指定网络设备(net_device对象)的allmulti计数器。vif_delete()方法调用dev_set_allmulti(dev, -1)来递减指定网络设备的allmulti计数器(net_device对象)。关于dev_set_allmulti()方法的更多细节,参见附录 A 。我们来看看vif_device的结构;它的成员是不言自明的:
struct vif_device {
struct net_device *dev; /* Device we are using */
unsigned long bytes_in,bytes_out;
unsigned long pkt_in,pkt_out; /* Statistics */
unsigned long rate_limit; /* Traffic shaping (NI) */
unsigned char threshold; /* TTL threshold */
unsigned short flags; /* Control flags */
__be32 local,remote; /* Addresses(remote for tunnels)*/
int link; /* Physical interface index */
};
(include/linux/mroute.h)
为了接收多播流量,主机必须加入多播组。这是通过在用户空间中创建一个套接字,并使用 IPPROTO_IP 和 IP_ADD_MEMBERSHIP 套接字选项调用setsockopt()来完成的。用户空间应用还创建了一个ip_mreq对象,在这里它初始化请求参数,比如所需的组多播地址和主机的源 IP 地址(参见netinet/in.h用户空间头)。在net/ipv4/igmp.c中,setsockopt()调用由ip_mc_join_group()方法在内核中处理。最终,ip_mc_join_group()方法将组播地址添加到组播地址列表(mc_list)中,该列表是in_device对象的成员。主机可以通过使用 IPPROTO_IP 和 IP_DROP_MEMBERSHIP 套接字选项调用setsockopt()来离开多播组。这是在内核中由net/ipv4/igmp.c中的ip_mc_leave_group()方法处理的。单个套接字可以加入多达 20 个多播组(sysctl_igmp_max_memberships)。试图通过同一个套接字加入 20 个以上的多播组将会失败,并出现-ENOBUFS 错误(“没有可用的缓冲区空间”))参见net/ipv4/igmp.c中的ip_mc_join_group()方法实现。
IPv4 组播接收路径
第四章的“接收 IPv4 多播数据包”一节简要讨论了如何处理多播数据包。我现在将更深入地描述这一点。我的讨论假设我们的机器被配置为多播路由器;正如前面提到的,这意味着 CONFIG_IP_MROUTE 被设置,并且像pimd或mrouted这样的路由守护进程在这个主机上运行。多播数据包由ip_route_input_mc()方法处理,其中分配并初始化路由表条目(一个rtable对象),并且在设置 CONFIG_IP_MROUTE 的情况下,将dst对象的input回调设置为ip_mr_input()。我们来看看ip_mr_input()的方法:
int ip_mr_input(struct sk_buff *skb)
{
struct mfc_cache *cache;
struct net *net = dev_net(skb->dev);
首先,如果数据包打算用于本地传送,则将local标志设置为true,因为ip_mr_input()方法也处理本地多播数据包。
int local = skb_rtable(skb)->rt_flags & RTCF_LOCAL;
struct mr_table *mrt;
/* Packet is looped back after forward, it should not be
* forwarded second time, but still can be delivered locally.
*/
if (IPCB(skb)->flags & IPSKB_FORWARDED)
goto dont_forward;
通常,当使用单个多播路由表时,ipmr_rt_fib_lookup()方法简单地返回net->ipv4.mrt对象:
mrt = ipmr_rt_fib_lookup(net, skb);
if (IS_ERR(mrt)) {
kfree_skb(skb);
return PTR_ERR(mrt);
}
if (!local) {
当发送加入或离开数据包时,IGMPv3 和一些 IGMPv2 实现在 IPv4 报头中设置路由器警报选项(IPOPT_RA)。参见net/ipv4/igmp.c中的igmpv3_newpack()方法:
if (IPCB(skb)->opt.router_alert) {
ip_call_ra_chain()方法 ( net/ipv4/ip_input.c)调用raw_rcv()方法将包传递给用户空间原始套接字,该套接字进行监听。ip_ra_chain对象包含对多播路由套接字的引用,该引用作为参数传递给raw_rcv()方法。更多细节,请看net/ipv4/ip_input.c中的ip_call_ra_chain()方法实现:
if (ip_call_ra_chain(skb))
return 0;
存在未设置路由器警报选项的实现,如以下注释中所解释的;这些情况也必须通过直接调用raw_rcv()方法来处理:
} else if (ip_hdr(skb)->protocol == IPPROTO_IGMP) {
/* IGMPv1 (and broken IGMPv2 implementations sort of
* Cisco IOS <= 11.2(8)) do not put router alert
* option to IGMP packets destined to routable
* groups. It is very bad, because it means
* that we can forward NO IGMP messages.
*/
struct sock *mroute_sk;
mrt->mroute_sk套接字是组播路由用户空间应用创建的套接字内核中的一个副本:
mroute_sk = rcu_dereference(mrt->mroute_sk);
if (mroute_sk) {
nf_reset(skb);
raw_rcv(mroute_sk, skb);
return 0;
}
}
}
首先,通过调用ipmr_cache_find()方法在多播路由缓存mfc_cache_array中执行查找。哈希键是数据包的目的多播组地址和源 IP 地址,取自 IPv4 报头:
cache = ipmr_cache_find(mrt, ip_hdr(skb)->saddr, ip_hdr(skb)->daddr);
if (cache == NULL) {
在虚拟设备阵列中执行查找(vif_table)以查看是否存在与输入网络设备匹配的对应条目(skb->dev):
int vif = ipmr_find_vif(mrt, skb->dev);
ipmr_cache_find_any()方法处理多播代理支持的高级特性(本书不讨论):
if (vif >= 0)
cache = ipmr_cache_find_any(mrt, ip_hdr(skb)->daddr,
vif);
}
/*
* No usable cache entry
*/
if (cache == NULL) {
int vif;
如果数据包的目的地是本地主机,则传送它:
if (local) {
struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);
ip_local_deliver(skb);
if (skb2 == NULL)
return -ENOBUFS;
skb = skb2;
}
read_lock(&mrt_lock);
vif = ipmr_find_vif(mrt, skb->dev);
if (vif >= 0) {
ipmr_cache_unresolved()方法通过调用ipmr_cache_alloc_unres()方法创建一个多播路由条目(mfc_cache对象)。该方法创建一个缓存条目(mfc_cache对象),并初始化其到期时间间隔(通过设置mfc_un.unres.expires)。让我们来看看这个非常短的方法,ipmr_cache_alloc_unres() :
static struct mfc_cache *ipmr_cache_alloc_unres(void)
{
struct mfc_cache *c = kmem_cache_zalloc(mrt_cachep, GFP_ATOMIC);
if (c) {
skb_queue_head_init(&c->mfc_un.unres.unresolved);
设置到期时间间隔:
c->mfc_un.unres.expires = jiffies + 10*HZ;
}
return c;
}
如果路由守护程序未在其到期时间间隔内解析路由条目,则该条目将从未解析条目的队列中删除。当创建多播路由表时(通过ipmr_new_table()方法),其定时器(ipmr_expire_timer)被设置。这个定时器周期性地调用ipmr_expire_process()方法。ipmr_expire_process()方法遍历未解析条目队列中的所有未解析缓存条目(mrtable对象的mfc_unres_queue,并移除过期的未解析缓存条目。
在创建了未解析的高速缓存条目之后,ipmr_cache_unresolved()方法将其添加到未解析条目的队列中(多播表的mfc_unres_queue,mrtable,并且将未解析队列长度加 1(多播表的cache_resolve_queue_len,mrtable)。它还调用ipmr_cache_report()方法,该方法构建 IGMP 消息(IGMPMSG_NOCACHE ),并通过最终调用sock_queue_rcv_skb()方法将其交付给用户空间多播路由守护进程。
我提到过用户空间路由守护进程应该在某个时间间隔内解析路由。我不会深究这是如何在用户空间中实现的。但是,请注意,一旦路由守护程序决定它应该解析一个未解析的条目,它就构建缓存条目参数(在一个mfcctl对象中)并使用 MRT_ADD_MFC 套接字选项调用setsockopt(),然后它传递嵌入在setsockopt()系统调用的optval参数中的mfcctl对象;这在内核中由ipmr_mfc_add()方法处理:
int err2 = ipmr_cache_unresolved(mrt, vif, skb);
read_unlock(&mrt_lock);
return err2;
}
read_unlock(&mrt_lock);
kfree_skb(skb);
return -ENODEV;
}
read_lock(&mrt_lock);
如果在 MFC 中找到缓存条目,调用ip_mr_forward()方法继续包遍历:
ip_mr_forward(net, mrt, skb, cache, local);
read_unlock(&mrt_lock);
if (local)
return ip_local_deliver(skb);
return 0;
dont_forward:
if (local)
return ip_local_deliver(skb);
kfree_skb(skb);
return 0;
}
本节详细介绍了 IPv4 多播接收路径以及与该路径中的路由守护程序的交互。下一节描述多播路由转发方法,ip_mr_forward()。
ip_mr_forward()方法
我们来看看ip_mr_forward()的方法:
static int ip_mr_forward(struct net *net, struct mr_table *mrt,
struct sk_buff *skb, struct mfc_cache *cache,
int local)
{
int psend = -1;
int vif, ct;
int true_vifi = ipmr_find_vif(mrt, skb->dev);
vif = cache->mfc_parent;
在这里,您可以看到已解析的缓存对象(mfc_un.res)的更新统计信息:
cache->mfc_un.res.pkt++;
cache->mfc_un.res.bytes += skb->len;
if (cache->mfc_origin == htonl(INADDR_ANY) && true_vifi >= 0) {
struct mfc_cache *cache_proxy;
表达式(*, G)表示从任何源发送到组 G 的业务:
/* For an (*,G) entry, we only check that the incomming
* interface is part of the static tree.
*/
cache_proxy = ipmr_cache_find_any_parent(mrt, vif);
if (cache_proxy &&
cache_proxy->mfc_un.res.ttls[true_vifi] < 255)
goto forward;
}
/*
* Wrong interface: drop packet and (maybe) send PIM assert.
*/
if (mrt->vif_table[vif].dev != skb->dev) {
if (rt_is_output_route(skb_rtable(skb))) {
/* It is our own packet, looped back.
* Very complicated situation...
*
* The best workaround until routing daemons will be
* fixed is not to redistribute packet, if it was
* send through wrong interface. It means, that
* multicast applications WILL NOT work for
* (S,G), which have default multicast route pointing
* to wrong oif. In any case, it is not a good
* idea to use multicasting applications on router.
*/
goto dont_forward;
}
cache->mfc_un.res.wrong_if++;
if (true_vifi >= 0 && mrt->mroute_do_assert &&
/* pimsm uses asserts, when switching from RPT to SPT,
* so that we cannot check that packet arrived on an oif.
* It is bad, but otherwise we would need to move pretty
* large chunk of pimd to kernel. Ough... --ANK
*/
(mrt->mroute_do_pim ||
cache->mfc_un.res.ttls[true_vifi] < 255) &&
time_after(jiffies,
cache->mfc_un.res.last_assert + MFC_ASSERT_THRESH)) {
cache->mfc_un.res.last_assert = jiffies;
调用ipmr_cache_report()方法构建 IGMP 消息(IGMPMSG_WRONGVIF ),并通过调用sock_queue_rcv_skb()方法将其交付给用户空间多播路由守护进程:
ipmr_cache_report(mrt, skb, true_vifi, IGMPMSG_WRONGVIF);
}
goto dont_forward;
}
该帧现在可以转发了:
forward:
mrt->vif_table[vif].pkt_in++;
mrt->vif_table[vif].bytes_in += skb->len;
/*
* Forward the frame
*/
if (cache->mfc_origin == htonl(INADDR_ANY) &&
cache->mfc_mcastgrp == htonl(INADDR_ANY)) {
if (true_vifi >= 0 &&
true_vifi != cache->mfc_parent &&
ip_hdr(skb)->ttl >
cache->mfc_un.res.ttls[cache->mfc_parent]) {
/* It's an (*,*) entry and the packet is not coming from
* the upstream: forward the packet to the upstream
* only.
*/
psend = cache->mfc_parent;
goto last_forward;
}
goto dont_forward;
}
for (ct = cache->mfc_un.res.maxvif - 1;
ct >= cache->mfc_un.res.minvif; ct--) {
/* For (*,G) entry, don't forward to the incoming interface */
if ((cache->mfc_origin != htonl(INADDR_ANY) ||
ct != true_vifi) &&
ip_hdr(skb)->ttl > cache->mfc_un.res.ttls[ct]) {
if (psend != -1) {
struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);
调用ipmr_queue_xmit()方法继续转发数据包:
if (skb2)
ipmr_queue_xmit(net, mrt, skb2, cache,
psend);
}
psend = ct;
}
}
last_forward:
if (psend != -1) {
if (local) {
struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);
if (skb2)
ipmr_queue_xmit(net, mrt, skb2, cache, psend);
} else {
ipmr_queue_xmit(net, mrt, skb, cache, psend);
return 0;
}
}
dont_forward:
if (!local)
kfree_skb(skb);
return 0;
}
既然我已经介绍了多播路由转发方法ip_mr_forward(),那么是时候检查一下ipmr_queue_xmit()方法了。
ipmr_queue_xmit()方法
我们来看看ipmr_queue_xmit()的方法:
static void ipmr_queue_xmit(struct net *net, struct mr_table *mrt,
struct sk_buff *skb, struct mfc_cache *c, int vifi)
{
const struct iphdr *iph = ip_hdr(skb);
struct vif_device *vif = &mrt->vif_table[vifi];
struct net_device *dev;
struct rtable *rt;
struct flowi4 fl4;
使用隧道时使用encap字段:
int encap = 0;
if (vif->dev == NULL)
goto out_free;
#ifdef CONFIG_IP_PIMSM
if (vif->flags & VIFF_REGISTER) {
vif->pkt_out++;
vif->bytes_out += skb->len;
vif->dev->stats.tx_bytes += skb->len;
vif->dev->stats.tx_packets++;
ipmr_cache_report(mrt, skb, vifi, IGMPMSG_WHOLEPKT);
goto out_free;
}
#endif
使用隧道时,使用分别代表目的地址和本地地址的vif->remote和vif->local进行路由查找。这些地址是隧道的端点。当使用代表物理设备的vif_device对象时,使用 IPv4 报头的目的地和作为源地址的 0 来执行路由查找:
if (vif->flags & VIFF_TUNNEL) {
rt = ip_route_output_ports(net, &fl4, NULL,
vif->remote, vif->local,
0, 0,
IPPROTO_IPIP,
RT_TOS(iph->tos), vif->link);
if (IS_ERR(rt))
goto out_free;
encap = sizeof(struct iphdr);
} else {
rt = ip_route_output_ports(net, &fl4, NULL, iph->daddr, 0,
0, 0,
IPPROTO_IPIP,
RT_TOS(iph->tos), vif->link);
if (IS_ERR(rt))
goto out_free;
}
dev = rt->dst.dev;
注意,如果分组大小高于 MTU,则不发送 ICMPv4 消息(如在单播转发的这种情况下所做的);只有统计数据被更新,数据包被丢弃:
if (skb->len+encap > dst_mtu(&rt->dst) && (ntohs(iph->frag_off) & IP_DF)) {
/* Do not fragment multicasts. Alas, IPv4 does not
* allow to send ICMP, so that packets will disappear
* to blackhole.
*/
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
ip_rt_put(rt);
goto out_free;
}
encap += LL_RESERVED_SPACE(dev) + rt->dst.header_len;
if (skb_cow(skb, encap)) {
ip_rt_put(rt);
goto out_free;
}
vif->pkt_out++;
vif->bytes_out += skb->len;
skb_dst_drop(skb);
skb_dst_set(skb, &rt->dst);
TTL 减小,转发数据包时重新计算 IPv4 报头校验和(因为 TTL 是 IPv4 字段之一);单播数据包的ip_forward()方法也是如此:
ip_decrease_ttl(ip_hdr(skb));
/* FIXME: forward and output firewalls used to be called here.
* What do we do with netfilter? -- RR
*/
if (vif->flags & VIFF_TUNNEL) {
ip_encap(skb, vif->local, vif->remote);
/* FIXME: extra output firewall step used to be here. --RR */
vif->dev->stats.tx_packets++;
vif->dev->stats.tx_bytes += skb->len;
}
IPCB(skb)->flags |= IPSKB_FORWARDED;
/*
* RFC1584 teaches, that DVMRP/PIM router must deliver packets locally
* not only before forwarding, but after forwarding on all output
* interfaces. It is clear, if mrouter runs a multicasting
* program, it should receive packets not depending to what interface
* program is joined.
* If we will not make it, the program will have to join on all
* interfaces. On the other hand, multihoming host (or router, but
* not mrouter) cannot join to more than one interface - it will
* result in receiving multiple packets.
*/
调用 NF_INET_FORWARD 钩子:
NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev, dev,
ipmr_forward_finish);
return;
out_free:
kfree_skb(skb);
}
ipmr_forward_finish()方法
让我们来看看ipmr_forward_finish()方法,,这是一个非常简短的方法——它实际上与ip_forward()方法相同:
static inline int ipmr_forward_finish(struct sk_buff *skb)
{
struct ip_options *opt = &(IPCB(skb)->opt);
IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTFORWDATAGRAMS);
IP_ADD_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTOCTETS, skb->len);
处理 IPv4 选项,如果设置的话(见第四章):
if (unlikely(opt->optlen))
ip_forward_options(skb);
return dst_output(skb);
}
最终,dst_output()通过调用ip_finish_output()方法的ip_mc_output()方法发送数据包(两个方法都在net/ipv4/route.c中)。
现在我已经介绍了这些多播方法,让我们更好地理解 TTL 字段的值是如何在多播流量中使用的。
多播流量中的 TTL
在讨论多播流量时,IPv4 报头的 TTL 字段具有双重含义。第一个与单播 IPV4 流量中的相同:TTL 表示一个跳计数器,在转发数据包的每个设备上该计数器减 1。当它达到 0 时,数据包被丢弃。这样做是为了避免由于某些错误而导致数据包的无休止传输。TTL 的第二个含义是阈值,这是多播流量所特有的。TTL 值分为几个范围。路由器的每个接口都有一个 TTL 阈值,只有 TTL 大于接口阈值的数据包才会被转发。以下是这些阈值的值:
- 0: 限于同一主机(不能通过任何接口发出)
- 1: 限制在同一个子网(不会被路由器转发)
- 32: 限制在同一个地点
- 64: 局限于同一地区
- 128: 限于同一个大陆
- 范围不受限制(全局)
参见史蒂夫·迪林的“4.3BSD UNIX 和相关系统的 IP 多播扩展”,可在www.kohala.com/start/mcast.api.txt获得。
注 IPv4 组播路由在
net/ipv4/ipmr.c、include/linux/mroute.h、include/uapi/linux/mroute.h实现。
我对多播路由的讨论到此结束。本章现在转到策略路由,使您能够配置不仅仅基于目的地址的路由策略。
策略路由
使用策略路由, 一个系统管理员最多可以定义 255 个路由表。本节讨论 IPv4 策略路由;IPv6 策略路由在第八章中讨论。在本节中,我使用术语策略或规则来表示由策略路由创建的条目,以避免将普通路由条目(在第五章中讨论)与策略规则混淆。
策略路由管理
策略路由管理是通过iproute2包的ip rule命令完成的(策略路由管理不能与route命令并行)。让我们看看如何添加、删除和转储所有策略路由规则:
- 你用
ip rule add命令添加一条规则;比如:ip rule add tos 0x04 table 252。插入此规则后,将根据表 252 的路由规则处理 IPv4 TOS 字段匹配 0x04 的每个数据包。在添加路由时,您可以通过指定表的编号将路由条目添加到这个表中;例如:ip route add default via 192.168.2.10 table 252。这个命令在内核中由net/core/fib_rules.c中的fib_nl_newrule()方法处理。先前ip rule命令中的tos修饰符是ip rule命令可用的选择器修饰符之一;参见man 8 ip rule,以及本章末尾“快速参考”部分的表 6-1 。 - 使用
ip rule del命令删除一个规则;比如:ip rule del tos 0x04 table 252。这个命令在内核中由net/core/fib_rules.c中的fib_nl_delrule()方法处理。 - 使用
ip rule list命令或ip rule show命令转储所有规则。这两个命令都在内核中由net/core/fib_rules.c中的fib_nl_dumprule()方法处理。
现在,您已经对策略路由管理的基础有了很好的了解,所以让我们研究一下策略路由的 Linux 实现。
策略路由实现
策略路由的核心基础设施是fib_rules模块、 net/core/fib_rules.c。它由内核网络堆栈的三个协议使用:IPv4(包括多播模块,它具有多播策略路由功能,如本章前面的“多播路由”一节所述)、IPv6 和 DECnet。IPv4 策略路由也在名为fib_rules.c的文件中实现。不要被相同的名字(net/ipv4/fib_rules.c)迷惑。在 IPv6 中,策略路由在net/ipv6/fib6_rules.c中实现。头文件include/net/fib_rules.h包含策略路由核心的数据结构和方法。下面是fib4_rule结构的定义,它是 IPv4 策略路由的基础:
struct fib4_rule {
struct fib_rule common;
u8 dst_len;
u8 src_len;
u8 tos;
__be32 src;
__be32 srcmask;
__be32 dst;
__be32 dstmask;
#ifdef CONFIG_IP_ROUTE_CLASSID
u32 tclassid;
#endif
};
(net/ipv4/fib_rules.c)
缺省情况下,在引导时通过调用fib_default_rules_init()方法创建三个策略: 本地(RT_TABLE_LOCAL)表、主(RT_TABLE_MAIN)表和缺省(RT_TABLE_DEFAULT)表。查找是通过fib_lookup()方法完成的。注意在include/net/ip_fib.h中有两种不同的fib_lookup()方法的实现。第一个封装在#ifndef CONFIG_IP_MULTIPLE_TABLES 块中,用于非策略路由,第二个用于策略路由。使用策略路由时,查找是这样执行的:如果初始策略路由规则没有变化(net->ipv4.fib_has_custom_rules未设置),这意味着规则必须在三个初始路由表之一中。因此,首先在本地表中进行查找,然后在主表中查找,最后在默认表中查找。如果没有相应的条目,则返回网络不可达(-ENETUNREACH)错误。如果在初始策略路由规则中有一些变化(net->ipv4.fib_has_custom_rules被设置),那么 _ fib_lookup()方法 被调用,这是一个更重的方法,因为它遍历规则列表并为每个规则调用fib_rule_match()以决定它是否匹配。参见net/core/fib_rules.c中fib_rules_lookup()方法的实现。(从__fib_lookup()方法调用fib_rules_lookup()方法)。这里我应该提到的是,net->ipv4.fib_has_custom_rules变量在初始化阶段由fib4_rules_init()方法设置为false,在fib4_rule_configure()方法和fib4_rule_delete()方法中设置为true。请注意,应该设置 CONFIG_IP_MULTIPLE_TABLES 来使用策略路由。
我的多播路由讨论到此结束。下一节将讨论多路径路由,即在一条路由中添加多个下一跳的能力。
多路径路由
多路径路由 提供了向路由添加多个下一跳的能力。定义两个 nexthop 节点可以这样做,比如:ip route add default scope global nexthop dev eth0 nexthop dev eth1。系统管理员还可以为每个下一跳分配权重,例如:ip route add 192.168.1.10 nexthop via 192.168.2.1 weight 3 nexthop via 192.168.2.10 weight 5。fib_info结构表示一个 IPv4 路由条目,它可以有多个 FIB 下一跳。fib_info对象的fib_nhs成员代表 FIB nexthop 对象的数量;fib_info对象包含一个名为fib_nh的 FIB nexthop 对象数组。所以在这种情况下,创建了一个单独的fib_info对象,带有两个 FIB nexthop 对象的数组。内核将每个下一跳的权重保存在 FIB nexthop 对象的nh_weight字段中(fib_nh)。如果在添加多路径路径时未指定权重,则在fib_create_info()方法中,权重默认设置为 1。使用多路径路由时,调用fib_select_multipath()方法来确定下一跳。该方法从两个地方调用:Tx 路径中的__ip_route_output_key()方法和 Rx 路径中的ip_mkroute_input()方法。注意,当在流程中设置输出设备时,不会调用fib_select_multipath()方法,因为输出设备是已知的:
struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4) {
. . .
#ifdef CONFIG_IP_ROUTE_MULTIPATH
if (res.fi->fib_nhs > 1 && fl4->flowi4_oif == 0)
fib_select_multipath(&res);
else
#endif
. . .
}
在 Rx 路径中,不需要检查fl4->flowi4_oif是否为 0,因为它在该方法的开始被设置为 0。fib_select_multipath()方法的细节我就不深究了。我只想提一下,这种使用jiffies的方法有一定的随机性,有助于创建公平的加权路由分布,并且每个下一跳的权重都被考虑在内。通过设置指定fib_result对象的 FIB nexthop 选择器(nh_sel)来分配要使用的 FIB nexthop。与由专用模块(net/ipv4/ipmr.c)处理的多播路由相反,多路径路由的代码分散在现有的路由代码中,包含在#ifdef CONFIG_IP_ROUTE_MULTIPATH 条件中,并且没有在源代码中添加单独的模块来支持它。在第五章中提到,曾经有对 IPv4 多路径路由缓存的支持,但是在 2007 年的内核 2.6.23 中被移除了;事实上,它从未很好地工作过,也从未脱离过实验状态。不要将多路径路由缓存的删除与路由缓存的删除相混淆;这是两个不同的缓存。路由缓存的移除发生在五年后的内核 3.6 (2012)中。
注意多路径路由支持需要设置 CONFIG_IP_ROUTE_MULTIPATH。
摘要
本章讲述了高级 IPv4 路由主题,如组播路由、IGMP 协议、策略路由和多路径路由。您了解了多播路由的基本结构,比如多播表(mr_table))、多播转发缓存(MFC)、Vif 设备等等。您还了解了如何将主机设置为多播路由器,以及如何在多播路由中使用ttl字段。第七章处理 Linux 相邻子系统。接下来的“快速参考”部分涵盖了与本章讨论的主题相关的主要方法,按其上下文排序。
快速参考
我用一个重要路由子系统方法的简短列表(其中一些在本章中提到过)、一个宏列表和procfs多播条目和表来结束本章。
方法
让我们从方法开始:
int IP _ mrroute _ setsockopt(struct sock * sk,int optname,char __user *optval,signed int opt len);
这个方法处理来自多播路由守护进程的setsockopt()调用。支持的套接字选项有:MRT_INIT、MRT_DONE、MRT_ADD_VIF、MRT_DEL_VIF、MRT_ADD_MFC、MRT_DEL_MFC、MRT_ADD_MFC_PROXY、MRT_DEL_MFC_PROXY、MRT_ASSERT、MRT_PIM(设置 PIM 支持时)和 MRT_TABLE(设置多播策略路由时)。
int IP _ mrroute _ getsockopt(struct sock * sk、int optname、char __user *optval、int _ _ user * opt len);
这个方法处理来自多播路由守护进程的getsockopt()调用。支持的套接字选项有 MRT_VERSION、MRT_ASSERT 和 MRT_PIM。
struct Mr _ table * ipmr _ new _ table(struct net * net,u32 id);
此方法创建一个新的多播路由表。表格的id将是指定的id.
void ipmr _ free _ table(struct Mr _ table * mrt);
这个方法释放指定的多播路由表和附属于它的资源。
int IP _ MC _ join _ group(struct sock * sk,struct IP _ Mr eqn * IMR);
此方法用于加入多播组。要加入的多播组的地址在给定的ip_mreqn对象中指定。如果成功,该方法返回 0。
静态 struct MFC _ cache * ipmr _ cache _ find(struct Mr _ table * mrt,_be32 origin, _ be32 mcastgrp);
此方法在 IPv4 多播路由缓存中执行查找。当没有找到条目时,它返回 NULL。
bool IP v4 _ is _ multicast(_ _ be32 addr);
如果地址是组播地址,这个方法返回true。
int IP _ Mr _ input(struct sk _ buff * skb);
该方法是主要的 IPv4 组播 Rx 方法(net/ipv4/ipmr.c)。
struct MFC _ cache * IPM _ cache _ alloc(请参阅);
该方法分配多播转发高速缓存(mfc_cache)条目。
静态结构 MFC _ cache * ipmr _ cache _ alloc _ unres(void);
该方法为未解析的高速缓存分配多播路由高速缓存(mfc_cache)条目,并设置未解析条目队列的expires字段。
void fib _ select _ multipath(struct fib _ result * RES);
使用多路径路由时,调用此方法来确定下一跳。
int dev_set_allmulti(结构网络设备*dev,int Inc);
该方法根据指定的增量(增量可以是正数,也可以是负数)递增/递减指定网络设备的allmulti计数器。
int igmp _ rcv(struct sk _ buf * skb);
此方法是 IGMP 数据包的接收处理程序。
static int ipmr _ MFC _ add(struct net * net,struct mr_table *mrt,struct mfcctl *mfc,int mrtsock,int parent);
此方法添加一个多播缓存条目;通过用 MRT_ADD_MFC 从用户空间调用setsockopt()来调用它。
static int ipmr _ MFC _ delete(struct Mr _ table * mrt,struct mfcctl *mfc,int parent);
此方法删除多播缓存条目;通过用 MRT_DEL_MFC 从用户空间调用setsockopt()来调用它。
static int vif_add(struct net *net,struct mr_table *mrt,struct vifctl *vifc,int mrt sock);
此方法添加一个多播虚拟接口;通过使用 MRT_ADD_VIF 从用户空间调用setsockopt()来调用它。
静态 int Vif _ delete(struct Mr _ table * mrt,int vifi,int notify,struct list _ head * head);
此方法删除多播虚拟接口;通过使用 MRT_DEL_VIF 从用户空间调用setsockopt()来调用它。
静态 void ipmr_expire_process(无符号长整型 arg);
此方法从未解析条目队列中移除过期条目。
static int ipmr _ cache _ report(struct Mr _ table * mrt,struct sk_buff *pkt,vifi_t vifi,int assert);
该方法构建一个 IGMP 包,将 IGMP 报头中的类型设置为指定的assert值,并将代码设置为 0。通过调用sock_queue_rcv_skb()方法,这个 IGMP 包被传递给用户空间多播路由守护进程。当一个未解析的缓存条目被添加到未解析条目的队列中,并且想要通知用户空间路由守护进程它应该解析它时,可以给assert参数分配以下值之一:IGMPMSG_NOCACHE、IGMPMSG_WRONGVIF 和 IGMPMSG_WHOLEPKT。
static int ipmr _ device _ event(struct notifier _ block * this,unsigned long event,void * ptr);
此方法是由register_netdevice_notifier()方法注册的通知程序回调;当某个网络设备未注册时,会生成一个 NETDEV_UNREGISTER 事件;这个回调接收这个事件并删除vif_table中的vif_device对象,其设备是未注册的设备。
静态 void mrt sock _ destruct(struct sock * sk);
当用户空间路由守护进程用 MRT_DONE 调用setsockopt()时,这个方法被调用。这个方法使多播路由套接字(多播路由表的mroute_sk)无效,递减mc_forwarding procfs条目,并调用mroute_clean_tables()方法来释放资源。
宏指令
本节描述我们的宏。
MFC_HASH(a,b)
这个宏计算用于向 MFC 缓存添加条目的哈希值。它将组多播地址和源 IPv4 地址作为参数。
VIF_EXISTS(_mrt,_idx)
该宏检查vif_table中条目的存在;如果指定组播路由表(mrt)的组播虚拟设备(vif_table)的数组中有指定索引(_idx)的条目,则返回true。
Procfs 多播条目
以下是对两个重要的procfs多播条目的描述:
S7-1200 可编程控制器
列出所有多播虚拟接口;它显示多播虚拟设备表(vif_table)中的所有vif_device对象。显示/proc/net/ip_mr_vif entry由ipmr_vif_seq_show()方法处理。
/proc/net/ip_mr_cache
多播转发缓存(MFC)的状态。该条目显示了所有缓存条目的以下字段:组组播地址(mfc_mcastgrp)、源 IP 地址(mfc_origin)、输入接口索引(mfc_parent)、转发数据包(mfc_un.res.pkt)、转发字节(mfc_un.res.bytes)、错误接口索引(mfc_un.res.wrong_if)、转发接口索引(vif_table中的一个索引)以及该索引对应的mfc_un.res.ttls数组中的条目。显示/proc/net/ip_mr_cache条目由ipmr_mfc_seq_show()方法处理。
桌子
最后,在表格 6-1 中,是规则选择器的表格。
表 6-1 。IP 规则选择器
七、Linux 相邻子系统
本章讨论了 Linux 相邻子系统及其在 Linux 中的实现。相邻子系统负责发现同一链路上节点的存在,并负责将 L3(网络层)地址转换为 L2(链路层)地址。正如下一节所述,需要 L2 地址来构建外发数据包的 L2 报头。实现这种转换的协议在 IPv4 中称为地址解析协议(ARP ),在 IPv6 中称为邻居发现协议(ndISC 或 ND)。相邻子系统提供了用于执行 L3 到 L2 映射的独立于协议的基础设施。然而,本章的讨论仅限于最常见的情况,即 IPv4 和 IPv6 中相邻子系统的使用。请记住,ARP 协议就像在第三章中讨论的 ICMP 协议一样,容易受到安全威胁——例如 ARP 中毒攻击和 ARP 欺骗攻击(ARP 协议的安全方面超出了本书的范围)。
在这一章中,我首先讨论了常见的相邻数据结构和一些重要的 API 方法,它们在 IPv4 和 IPv6 中都有使用。然后讨论了 ARP 协议和 NDISC 协议的具体实现。您将看到邻居是如何创建和释放的,并且您将了解用户空间和相邻子系统之间的交互。您还将了解 ARP 请求和 ARP 回复、NDISC 邻居请求和 NDISC 邻居通告,以及 NDISC 协议用来避免重复 IPv6 地址的重复地址检测(DAD)机制。
相邻子系统核心
相邻子系统的用途是什么?当数据包通过 L2 层发送时,需要 L2 目的地址来构建 L2 报头。使用相邻子系统请求和请求回复,给定主机的 L3 地址(或者这种 L3 地址不存在的事实),可以找出主机的 L2 地址。在最常用的链路层(L2)以太网中,主机的 L2 地址就是其 MAC 地址。在 IPv4 中,ARP 是相邻协议,请求请求和请求回复分别称为 ARP 请求和 ARP 回复。在 IPv6 中,邻居协议是 NDISC,请求请求和请求回复分别称为邻居请求和邻居通告,。
有些情况下,不需要相邻子系统的任何帮助就可以找到目的地址,例如发送广播时。在这种情况下,目的 L2 地址是固定的(例如,在以太网中是 FF:FF:FF:FF:FF:FF)。或者当目的地址是多播地址时,L3 多播地址与其 L2 地址之间存在固定的映射。我将在本章中讨论这种情况。
Linux 相邻子系统的基本数据结构是邻居。邻居代表连接到同一链路(L2)的网络节点。它由neighbour结构表示。这种表示对于特定的协议不是唯一的。然而,如上所述,neighbour结构的讨论将限于其在 IPv4 和 IPv6 协议中的使用。让我们看看neighbour的结构:
struct neighbour {
struct neighbour __rcu *next;
struct neigh_table *tbl;
struct neigh_parms *parms;
unsigned long confirmed;
unsigned long updated;
rwlock_t lock;
atomic_t refcnt;
struct sk_buff_head arp_queue;
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;
unsigned char ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
struct hh_cache hh;
int (*output)(struct neighbour *, struct sk_buff *);
const struct neigh_ops *ops;
struct rcu_head rcu;
struct net_device *dev;
u8 primary_key[0];
};
(include/net/neighbour.h)
以下是对neighbour结构中一些重要成员的描述:
-
next:指向哈希表中同一桶上的下一个邻居的指针。 -
tbl:与该邻居关联的邻居表。 -
parms:与此neighbour关联的neigh_parms对象 。它由相关邻表的constructor方法初始化。例如,在 IPv4 中,arp_constructor()方法将parms初始化为相关网络设备的arp_parms。不要将其与邻桌的neigh_parms对象混淆。 -
confirmed:确认时间戳(本章稍后讨论)。 -
refcnt:参考计数器。 通过neigh_hold()宏递增,通过neigh_release()方法递减。只有在引用计数器递减后,其值为 0 时,neigh_release()方法才通过调用neigh_destroy()方法来释放邻居对象。 -
arp_queue:未解析 skb 的队列。尽管有这个名字,这个成员并不是 ARP 所独有的,而是被其他协议所使用,例如 NDISC 协议。 -
timer:每个neighbour对象都有一个定时器;计时器回调是neigh_timer_handler()方法。neigh_timer_handler()方法可以改变邻居的网络不可达检测(NUD)状态。发送邀约请求时,邻居状态为 NUD _ 未完成或 NUD _ 探测,且邀约请求探测数大于等于neigh_max_probes(),则邻居状态设置为 NUD _ 失败,调用neigh_invalidate()方法。 -
ha_lock:提供对邻居硬件地址的访问保护(ha)。 -
ha:邻居对象的硬件地址;在以太网的情况下,它是邻居的 MAC 地址。 -
hh:L2 头的硬件头缓存(一个hh_cache对象)。 -
output: A pointer to a transmit method, like theneigh_resolve_output()method or theneigh_direct_output()method. It is dependent on the NUD state and as a result can be assigned to different methods during a neighbour lifetime. When initializing the neighbour object in theneigh_alloc()method, it is set to be theneigh_blackhole()method, which discards the packet and returns -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-1 描述了基本的 NUD 状态及其 Linux 符号。NUD 国家机器非常复杂;在本书中,我没有深入探究它的所有细微差别。 -
dead:当neighbour对象活着时,设置标志 t hat。当创建一个neighbour对象时,在__neigh_create()方法结束时,它被初始化为 0。对于没有设置dead标志的邻居对象,neigh_destroy()方法将失败。neigh_flush_dev()方法将dead标志设置为 1,但是还没有删除邻居条目。被标记为死亡的邻居(它们的dead标志被置位)的移除稍后由垃圾收集器完成。 -
邻居的 IP 地址(L3)。使用
primary_key在相邻表中进行查找。primary_key长度 基于所使用的协议。例如,对于 IPv4,它应该是 4 个字节。对于 IPv6,它应该是sizeof(struct in6_addr),因为in6_addr结构代表一个 IPv6 地址。因此,primary_key被定义为 0 字节的数组,在分配邻居时,应考虑使用哪种协议。参见本章后面关于entry_size和key_len的解释,在neigh_table结构成员的描述中。
为了避免为每个传输的新数据包发送请求,内核将 L3 地址和 L2 地址之间的映射保存在一个称为邻表的数据结构中;在 IPv4 的情况下,它是 ARP 表(有时也称为 ARP 缓存,尽管它们是相同的)——与您在第五章中看到的 IPv4 路由子系统相反:路由缓存在被删除之前,和路由表是两个不同的实体,由两种不同的数据结构表示。在 IPv6 的情况下,相邻表是 NDISC 表(也称为 NDISC 缓存)。ARP 表(arp_tbl)和 NDISC 表(nd_tbl)都是neigh_table结构的实例。我们来看看neigh_table的结构:
struct neigh_table {
struct neigh_table *next;
int family;
int entry_size;
int key_len;
__u32 (*hash)(const void *pkey,
const struct net_device *dev,
__u32 *hash_rnd);
int (*constructor)(struct neighbour *);
int (*pconstructor)(struct pneigh_entry *);
void (*pdestructor)(struct pneigh_entry *);
void (*proxy_redo)(struct sk_buff *skb);
char *id;
struct neigh_parms parms;
/* HACK. gc_* should follow parms without a gap! */
int gc_interval;
int gc_thresh1;
int gc_thresh2;
int gc_thresh3;
unsigned long last_flush;
struct delayed_work gc_work;
struct timer_list proxy_timer;
struct sk_buff_head proxy_queue;
atomic_t entries;
rwlock_t lock;
unsigned long last_rand;
struct neigh_statistics __percpu *stats;
struct neigh_hash_table __rcu *nht;
struct pneigh_entry **phash_buckets;
};
(include/net/neighbour.h)
以下是neigh_table结构中的一些重要成员:
-
next:每个协议创建自己的neigh_table实例。系统中有一个所有相邻表的链表。neigh_tables全局变量是一个指向列表开头的指针。next变量指向列表中的下一项。 -
family:协议族:IPv4 邻居表的 AF _ INET(arp_tbl),IPv6 邻居表的 AF _ INET 6(nd_tbl)。 -
entry_size:通过neigh_alloc()方法分配邻居条目时,分配的大小为tbl->entry_size + dev->neigh_priv_len。通常neigh_priv_len值为 0。在内核 3.3 之前,entry_size被显式初始化为 ARP 的sizeof(struct neighbour) + 4,NDISC 的sizeof(struct neighbour) + sizeof(struct in6_addr)。这种初始化的原因是,在分配邻居时,您还想为primary_key[0]成员分配空间。从内核 3.3 开始,enrty_size被从arp_tbl和ndisc_tbl的静态初始化中移除,entry_size初始化是基于核心相邻层中的key_len通过neigh_table_init_no_netlink()方法完成的。 -
key_len:查找键的大小;对于 IPv4 是 4 字节,因为 IPv4 地址的长度是 4 字节,对于 IPv6 是sizeof(struct in6_addr)。in6_addr结构代表一个 IPv6 地址。 -
hash:用于将关键字(L3 地址)映射到特定散列值的散列函数;对于 ARP,它是arp_hash()方法。对于 NDISC,这是一种ndisc_hash()方法。 -
constructor:该方法在创建neighbour对象时执行 协议特定的初始化。比如 IPv4 中 ARP 的arp_constructor(),IPv6 中 NDISC 的ndisc_constructor()。constructor回调由__neigh_create()方法调用。如果成功,它返回 0。 -
pconstructor:用于创建邻居代理条目的方法;ARP 不用,NDISC 用的是pndisc_constructor。此方法应该在成功时返回 0。如果查找失败,则从pneigh_lookup()方法中调用pconstructor方法,条件是用creat = 1调用了pneigh_lookup()。 -
pdestructor:销毁邻居代理条目的方法。和pconstructor回调一样,pdestructor不是 ARP 用的,是 NDISC 用的pndisc_destructor。pdestructor方法是从pneigh_delete()方法和pneigh_ifdown()方法中调用的。 -
id:表格的名称;IPv4 是arp_cache,IPv6 是ndisc_cache。 -
parms:一个neigh_parms对象:每个相邻的表都有一个关联的neigh_parms对象,它由各种配置设置组成,比如可达性信息、各种超时等等。ARP 表和 NDISC 表中的neigh_parms初始化不同。 -
gc_interval:不被相邻核心直接使用。 -
gc_thresh1、gc_thresh2、gc_thresh3:邻表条目数的阈值。用作激活同步垃圾收集器(neigh_forced_gc)的标准,并在neigh_periodic_work()异步垃圾收集器处理器中使用。请参阅本章后面的“创建和释放邻居”一节中关于分配邻居对象的解释。在 ARP 表中,默认值为:gc_thresh1是 128,gc_thresh2是 512,gc_thresh3是 1024。这些值可以通过procfs设置。IPv6 中的 NDISC 表也使用相同的默认值。IPv4procfs条目是: -
/proc/sys/net/ipv4/neigh/default/gc_thresh1 -
/proc/sys/net/ipv4/neigh/default/gc_thresh2 -
/proc/sys/net/ipv4/neigh/default/gc_thresh3
对于 IPv6,这些是procfs条目:
/proc/sys/net/ipv6/neigh/default/gc_thresh1/proc/sys/net/ipv6/neigh/default/gc_thresh2/proc/sys/net/ipv6/neigh/default/gc_thresh3last_flush:最近一次运行neigh_forced_gc()方法的时间。在neigh_table_init_no_netlink ()方法中被初始化为当前时间(jiffies)。gc_work:异步垃圾收集器处理程序。通过neigh_table_init_no_netlink()方法设置为neigh_periodic_work()定时器。delayed_work struct是一种工作队列。在内核 2.6.32 之前,neigh_periodic_timer()方法是异步垃圾收集器处理程序;它只处理一个桶,而不是整个相邻哈希表。neigh_periodic_work()方法首先检查表中的条目数是否小于gc_thresh1,如果是,则不做任何事情就退出;然后它重新计算可到达时间(parms的reachable_time字段,它是与邻表关联的neigh_parms对象)。然后,它扫描相邻哈希表,并删除其状态不是 NUD _ 永久或 NUD _ 计时器,并且其引用计数为 1 的条目,如果满足这些条件之一:它们处于 NUD _ 失败状态,或者当前时间在它们的used时间戳+ gc_staletime之后(gc_staletime是neighbour parms对象的成员)。通过将dead标志设置为 1 并调用neigh_cleanup_and_release()方法来移除邻居条目。proxy_timer:当一台主机被配置为 ARP 代理时,可以避免立即处理请求,而是延迟处理。这是因为对于 ARP 代理主机,可能会有大量的请求(与主机不是 ARP 代理的情况相反,在这种情况下,您通常会有少量的 ARP 请求)。有时,您可能希望延迟对此类广播的回复,以便让拥有此类 IP 地址的主机优先获得请求。该延迟是达到proxy_delay参数的随机值。ARP 代理定时器处理程序是neigh_proxy_process()方法。proxy_timer是由neigh_table_init_no_netlink()方法初始化的。proxy_queue:SKBs 的代理 ARP 队列。skb 是用pneigh_enqueue()的方法添加的。stats:邻居统计(neigh_statistics)对象;由每 CPU 计数器组成,如allocs,它是由neigh_alloc()方法分配的邻居对象的数量,或destroys,它是由neigh_destroy()方法释放的邻居对象的数量,等等。邻居统计计数器由 NEIGH_CACHE_STAT_INC 宏递增。请注意,因为统计是针对每个 CPU 计数器的,所以这个宏使用了宏this_cpu_inc()。您可以分别用cat /proc/net/stat/arp_cache和cat/proc/net/stat/ndisc_cache显示 ARP 统计和 NDISC 统计。在本章末尾的“快速参考”部分,有一个对neigh_statistics结构的描述,说明了每个计数器递增的方法。nht:邻居哈希表(neigh_hash_table对象)。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对象):
static void neigh_table_init_no_netlink(struct neigh_table *tbl)
{
. . .
RCU_INIT_POINTER(tbl->nht, neigh_hash_alloc(3));
. . .
}
static struct neigh_hash_table *neigh_hash_alloc(unsigned int shift)
{
哈希表的大小是1<< shift (when size <= PAGE_SIZE):
size_t size = (1 << shift) * sizeof(struct neighbour *);
struct neigh_hash_table *ret;
struct neighbour __rcu **buckets;
int i;
ret = kmalloc(sizeof(*ret), GFP_ATOMIC);
if (!ret)
return NULL;
if (size <= PAGE_SIZE)
buckets = kzalloc(size, GFP_ATOMIC);
else
buckets = (struct neighbour __rcu **)
__get_free_pages(GFP_ATOMIC | __GFP_ZERO,
get_order(size));
. . .
}
您可能想知道为什么需要neigh_table_init_no_netlink()方法——为什么不在neigh_table_init()方法中执行所有的初始化?neigh_table_init_no_netlink()方法执行相邻表的所有初始化,除了将它链接到相邻表的全局链表neigh_tables。本来这样的初始化,没有链接到neigh_tables链表,是 ATM 需要的,结果neigh_table_init()方法被拆分,ATM clip 模块调用了neigh_table_init_no_netlink()方法,而不是调用neigh_table_init()方法;然而,随着时间的推移,在 ATM 中发现了不同的解决方案。虽然 ATM clip 模块不再调用neigh_table_init_no_netlink()方法,但是这些方法的分离仍然存在,也许将来会需要。
我应该提到,使用相邻子系统的每个 L3 协议也注册了一个协议处理程序:对于 IPv4,ARP 包(其以太网报头中的类型是 0x0806 的包)的处理程序是arp_rcv()方法:
static struct packet_type arp_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_ARP),
.func = arp_rcv,
};
void __init arp_init(void)
{
. . .
dev_add_pack(&arp_packet_type);
. . .
}
(net/ipv4/arp.c)
对于 IPv6,相邻消息是 ICMPv6 消息,因此它们由 ICMPv6 处理程序icmpv6_rcv()方法处理。有五个 ICMPv6 相邻消息;当(通过icmpv6_rcv()方法)收到它们中的每一个时,调用ndisc_rcv()方法来处理它们(见net/ipv6/icmp.c)。ndisc_rcv()方法将在本章后面的章节中讨论。每个邻居对象通过neigh_ops结构定义了一组方法。这是通过它的constructor方法完成的。neigh_ops结构包含一个协议族成员和四个函数指针:
struct neigh_ops {
int family;
void (*solicit)(struct neighbour *, struct sk_buff *);
void (*error_report)(struct neighbour *, struct sk_buff *);
int (*output)(struct neighbour *, struct sk_buff *);
int (*connected_output)(struct neighbour *, struct sk_buff *);
};
(include/net/neighbour.h)
family:IP v4 的 AF_INET,IPv6 的 AF_INET6。solicit:这个方法负责发送邻居请求:在 ARP 中是arp_solicit()方法,在 NDISC 中是ndisc_solicit()方法。error_report:当邻居状态为 NUD _ 失败时,从neigh_invalidate()方法调用该方法。例如,当请求请求未被回复时,在某个超时之后会发生这种情况。output:下一跳 L3 地址已知,但 L2 地址未解析时,output回调应为neigh_resolve_output()。connected_output:邻居状态为 NUD _ 可达或 NUD _ 已连接时,邻居的输出方式设置为connected_output()。参见neigh_update()方法和neigh_timer_handler()方法中的neigh_connect()调用。
创建和释放邻居
通过__neigh_create()方法创建邻居:
struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev, bool want_ref)
首先,__neigh_create()方法通过调用neigh_alloc()方法分配一个邻居对象,该方法也执行各种初始化。有些情况下,neigh_alloc()方法调用同步垃圾收集器(也就是neigh_forced_gc()方法):
static struct neighbour *neigh_alloc(struct neigh_table *tbl, struct net_device *dev)
{
struct neighbour *n = NULL;
unsigned long now = jiffies;
int entries;
entries = atomic_inc_return(&tbl->entries) - 1;
如果表条目的数量大于gc_thresh3(默认为 1024)或者如果表条目的数量大于gc_thresh2(默认为 512),并且自上次刷新以来经过的时间大于 5 Hz,则调用同步垃圾收集器方法(neigh_forced_gc()方法)。如果在运行了neigh_forced_gc()方法之后,表条目的数量大于gc_thresh3 (1024),你不分配一个邻居对象并返回 NULL:
if (entries >= tbl->gc_thresh3 ||
(entries >= tbl->gc_thresh2 &&
time_after(now, tbl->last_flush + 5 * HZ))) {
if (!neigh_forced_gc(tbl) &&
entries >= tbl->gc_thresh3)
goto out_entries;
}
然后,__neigh_create()方法通过调用指定邻居表的constructor方法来执行特定于协议的设置(ARP 使用arp_constructor(),NDISC 使用ndisc_constructor())。在构造器方法中,处理特殊情况,如多播或回送地址。在arp_constructor()方法中,比如你调用arp_mc_map()方法根据邻居 IPv4 primary_key地址设置邻居的硬件地址(ha),你设置nud_state为 NUD _ 诺阿普,因为组播地址不需要 ARP。例如,在ndisc_constructor()方法中,当处理多播地址时,您做了一些非常类似的事情:您调用ndisc_mc_map()来根据邻居 IPv6 primary_key地址设置邻居的硬件地址ha,并且您再次将nud_state设置为 NUD _ 诺阿普。对于广播地址也有特殊的处理:在arp_constructor()方法中,例如,当邻居类型是 RTN_BROADCAST 时,你设置邻居硬件地址(ha)为网络设备广播地址(net_device对象的broadcast字段),你设置nud_state为 NUD_NOARP。注意,IPv6 协议不实现传统的 IP 广播,因此广播地址的概念是不相关的(尽管在地址ff02::1有一个链路本地所有节点多播组)。有两种特殊情况需要进行额外的设置:
- 当
netdev_ops的ndo_neigh_construct()回调被定义时,它被调用。事实上,这仅在经典的 IP over ATM 代码中完成(clip);参见net/atm/clip.c。 - 当
neigh_parms对象的neigh_setup()回调被定义时,它被调用。例如,这用于绑定驱动程序中;参见drivers/net/bonding/bond_main.c。
当试图通过__neigh_create()方法创建一个neighbour对象时,如果邻居条目的数量超过了哈希表的大小,则必须将其扩大。这是通过调用neigh_hash_grow()方法来完成的,就像这样:
struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey,
struct net_device *dev, bool want_ref)
{
. . .
哈希表大小为1 << nht->hash_shift;如果超过,哈希表必须扩大:
if (atomic_read(&tbl->entries) > (1 << nht->hash_shift))
nht = neigh_hash_grow(tbl, nht->hash_shift + 1);
. . .
}
当want_ref参数为真时,您将在该方法中增加邻居引用计数。您还初始化了neighbour对象的confirmed字段:
n->confirmed = jiffies - (n->parms->base_reachable_time << 1);
它被初始化为略小于当前时间jiffies(原因很简单,您希望更快地要求可达性确认)。在__neigh_create()方法结束时,dead标志被初始化为 0,并且neighbour对象被添加到邻居散列表中。
neigh_release()方法通过调用neigh_destroy()方法,递减邻居的引用计数器,并在到达零时释放它。neigh_destroy()方法将验证邻居被标记为dead:其dead标志为 0 的邻居不会被移除。
在本节中,您了解了创建和释放邻居的内核方法。接下来,您将学习如何从用户空间触发添加和删除邻居条目,以及如何显示邻居表,对于 IPv4 使用arp命令,对于 IPv4/IPv6 使用ip命令。
用户空间和相邻子系统之间的交互
ARP 表的管理是通过iproute2包的ip neigh命令或者net-tools包的arp命令来完成的。因此,您可以通过从命令行运行以下命令之一来显示 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 可达或 NUD 失效)。还要注意,arp命令只能显示 IPv4 邻居表(ARP 表),而使用ip命令可以显示 IPv4 ARP 表和 IPv6 邻居表。如果您想只显示 IPv6 邻居表,您应该运行ip -6 neigh show。
ARP 和 NDISC 模块也通过procfs导出数据。这意味着您可以通过运行cat /proc/net/arp来显示 ARP 表(这个procfs条目由arp_seq_show()方法处理,正如前面提到的,该方法与处理arp命令的方法相同)。或者可以通过cat /proc/net/stat/arp_cache显示 ARP 统计,通过cat /proc/net/stat/ndisc_cache显示 NDISC 统计(两者都是通过neigh_stat_seq_show()方法处理的)。
可以用ip neigh add添加一个条目,由neigh_add()方法处理。运行ip neigh add时,您可以指定正在添加的条目的状态(比如 NUD _ 永久、NUD _ 陈旧、NUD _ 可达等等)。例如:
ip neigh add 192.168.0.121 dev eth0 lladdr 00:30:48:5b:cc:45 nud permanent
删除一个条目可以通过ip neigh del完成,并由neigh_delete()方法处理。例如:
ip neigh del 192.168.0.121 dev eth0
可以使用ip neigh add proxy向代理 ARP 表添加条目。例如:
ip neigh add proxy 192.168.2.11 dev eth0
加法由neigh_add()方法再次处理。在这种情况下,在从用户空间传递的数据中设置 NTF _ 代理标志(参见ndm对象的ndm_flags字段),因此调用pneigh_lookup()方法在代理邻居哈希表(phash_buckets)中执行查找。在查找失败的情况下,pneigh_lookup()方法向代理邻居哈希表添加一个条目。
可以用ip neigh del proxy从代理 ARP 表中删除一个条目。例如:
ip neigh del proxy 192.168.2.11 dev eth0
删除由neigh_delete()方法处理。同样,在这种情况下,在从用户空间传递的数据中设置 NTF _ 代理标志(参见ndm对象的ndm_flags字段),因此调用pneigh_delete()方法从代理邻居表中删除条目。
使用ip ntable命令,您可以控制相邻表的参数。例如:
ip ntable show:显示所有相邻表的参数。ip ntable change:改变邻表的参数值。由neightbl_set()方法处理。例如:ip ntable change name arp_cache queue 20 dev eth0。
您也可以通过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 事件。
在描述了 IPv4 和 IPv6 共有的基本数据结构,如邻居表(neigh_table)和neighbour结构,并讨论了如何创建和释放neighbour对象之后,是时候描述第一个邻居协议 ARP 协议的实现了。
ARP 协议(IPv4)
RFC 826 中定义了 ARP 协议。使用以太网时,这些地址被称为 MAC 地址,是 48 位值。MAC 地址应该是唯一的,但是您必须考虑到您可能会遇到不唯一的 MAC 地址。一个常见的原因是,在大多数网络接口上,系统管理员可以使用像ifconfig或ip这样的用户空间工具来配置 MAC 地址。
发送 IPv4 数据包时,您知道目的 IPv4 地址。您应该构建一个以太网报头,其中应该包含目的 MAC 地址。根据给定的 IPv4 地址查找 MAC 地址是由 ARP 协议完成的,您很快就会看到这一点。如果 MAC 地址未知,您可以通过广播发送 ARP 请求。这个 ARP 请求包含您正在寻找的 IPv4 地址。如果存在具有此类 IPv4 地址的主机,该主机将发送单播 ARP 响应作为回复。ARP 表(arp_tbl)是neigh_table结构的一个实例。ARP 报头由arphdr结构表示:
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
*
* Ethernet looks like this : This bit is variable sized however...
*/
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
};
(include/uapi/linux/if_arp.h)
以下是对arphdr结构中一些重要成员的描述:
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。有关可用 ARP 报头操作码的完整列表,请查看include/uapi/linux/if_arp.h。
紧跟在ar_op之后的是发送方硬件(MAC)地址和 IPv4 地址,以及目标硬件(MAC)地址和 IPv4 地址。这些地址不是 ARP 报头(arphdr)结构的一部分。在arp_process()方法、中,它们是通过读取 ARP 报头的相应偏移量来提取的,您可以在本章后面的“ARP:接收请求和回复”一节中看到关于arp_process()方法的解释。图 7-1 显示了一个 ARP 以太网数据包的 ARP 报头。
图 7-1 。ARP 报头(用于以太网)
在 ARP 中,定义了四个neigh_ops对象:arp_direct_ops、arp_generic_ops、arp_hh_ops和arp_broken_ops。ARP 表neigh_ops对象的初始化由arp_constructor()方法完成,基于网络设备特性:
- 如果
net_device对象的header_ops为空,则neigh_ops对象将被设置为arp_direct_ops。在这种情况下,将使用neigh_direct_output()方法发送数据包,这实际上是对dev_queue_xmit()的包装。然而,在大多数以太网设备中,net_device对象的header_ops被通用的ether_setup()方法初始化为eth_header_ops;参见net/ethernet/eth.c。 - 如果
net_device对象的header_ops包含一个空的cache()回调,那么neigh_ops对象将被设置为arp_generic_ops。 - 如果
net_device对象的header_ops包含一个非空的cache()回调,那么neigh_ops对象将被设置为arp_hh_ops。在使用通用eth_header_ops对象的情况下,cache()回调就是eth_header_cache()回调。 - 对于三种类型的设备,
neigh_ops对象将被设置为arp_broken_ops(当net_device对象的类型为 ARPHRD_ROSE、ARPHRD_AX25 或 ARPHRD_NETROM 时)。
现在我已经介绍了 ARP 协议和 ARP 头(arphdr)对象,让我们看看 ARP 请求是如何发送的。
ARP:发送征求请求
征集请求发送到哪里?最常见的情况是在 Tx 路径中,在实际离开网络层(L3)并移动到链路层(L2)之前。在ip_finish_output2()方法中,首先通过调用__ipv4_neigh_lookup_noref()方法在 ARP 表中查找下一跳 IPv4 地址,如果没有找到任何匹配的邻居条目,则通过调用__neigh_create()方法创建一个条目:
static inline int ip_finish_output2(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;
. . .
. . .
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
if (!IS_ERR(neigh)) {
int res = dst_neigh_output(dst, neigh, skb);
. . .
}
让我们来看看dst_neigh_output()法 :
static inline int dst_neigh_output(struct dst_entry *dst, struct neighbour *n,
struct sk_buff *skb)
{
const struct hh_cache *hh;
if (dst->pending_confirm) {
unsigned long now = jiffies;
dst->pending_confirm = 0;
/* avoid dirtying neighbour */
if (n->confirmed != now)
n->confirmed = now;
}
当你用这个流程第一次到达这个方法的时候,nud_state不是 NUD _ 连接的,输出回调是neigh_resolve_output()方法 :
hh = &n->hh;
if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
return neigh_hh_output(hh, skb);
else
return n->output(n, skb);
}
(include/net/dst.h)
在neigh_resolve_output()方法中,你调用neigh_event_send()方法,最终通过__skb_queue_tail(&neigh->arp_queue, skb)将 SKB 放入邻居的arp_queue;稍后,从邻居定时器处理程序neigh_timer_handler()调用的neigh_probe()方法将通过调用solicit()方法 ( neigh->ops->solicit在我们的例子中是arp_solicit()方法):来发送数据包
static void neigh_probe(struct neighbour *neigh)
__releases(neigh->lock)
{
struct sk_buff *skb = skb_peek(&neigh->arp_queue);
. . .
neigh->ops->solicit(neigh, skb);
atomic_inc(&neigh->probes);
kfree_skb(skb);
}
让我们来看看arp_solicit()方法,它实际上发送了 ARP 请求:
static void arp_solicit(struct neighbour *neigh, struct sk_buff *skb)
{
__be32 saddr = 0;
u8 dst_ha[MAX_ADDR_LEN], *dst_hw = NULL;
struct net_device *dev = neigh->dev;
__be32 target = *(__be32 *)neigh->primary_key;
int probes = atomic_read(&neigh->probes);
struct in_device *in_dev;
rcu_read_lock();
in_dev = __in_dev_get_rcu(dev);
if (!in_dev) {
rcu_read_unlock();
return;
}
使用arp_announce procfs条目,您可以为要发送的 ARP 数据包设置使用本地源 IP 地址的限制:
- 0: 使用在任何接口上配置的任何本地地址。这是默认值。
- 首先尝试使用目标子网上的地址。如果没有这样的地址,请使用第 2 级。
- 2: 使用主 IP 地址。
请注意,使用了这两个条目的最大值:
/proc/sys/net/ipv4/conf/all/arp_announce
/proc/sys/net/ipv4/conf/<netdeviceName>/arp_announce
另请参见本章末尾“快速参考”一节中对 IN_DEV_ARP_ANNOUNCE 宏的描述。
switch (IN_DEV_ARP_ANNOUNCE(in_dev)) {
default:
case 0: /* By default announce any local IP */
if (skb && inet_addr_type(dev_net(dev),
ip_hdr(skb)->saddr) == RTN_LOCAL)
saddr = ip_hdr(skb)->saddr;
break;
case 1: /* Restrict announcements of saddr in same subnet */
if (!skb)
break;
saddr = ip_hdr(skb)->saddr;
if (inet_addr_type(dev_net(dev), saddr) == RTN_LOCAL) {
inet_addr_onlink()方法 检查指定的目标地址和指定的源地址是否在同一个子网内:
/* saddr should be known to target */
if (inet_addr_onlink(in_dev, target, saddr))
break;
}
saddr = 0;
break;
case 2: /* Avoid secondary IPs, get a primary/preferred one */
break;
}
rcu_read_unlock();
if (!saddr)
inet_select_addr()方法返回指定设备的第一个主接口的地址,该设备的作用域小于指定的作用域(在本例中为 RT_SCOPE_LINK),并且与目标位于同一个子网:
saddr = inet_select_addr(dev, target, RT_SCOPE_LINK);
probes -= neigh->parms->ucast_probes;
if (probes < 0) {
if (!(neigh->nud_state & NUD_VALID))
pr_debug("trying to ucast probe in NUD_INVALID\n");
neigh_ha_snapshot(dst_ha, neigh, dev);
dst_hw = dst_ha;
} else {
probes -= neigh->parms->app_probes;
if (probes < 0) {
在使用用户空间 ARP 守护进程时设置配置 ARP 有像 OpenNHRP 这样的项目,它们是基于 ARPD 的。下一跳解析协议(NHRP) 用于提高通过非广播多路访问(NBMA) 网络路由计算机网络流量的效率(我在本书中不讨论 ARPD 用户空间守护进程):
#ifdef CONFIG_ARPD
neigh_app_ns(neigh);
#endif
return;
}
}
现在您调用arp_send()方法来发送一个 ARP 请求。注意,最后一个参数target_hw为空。您还不知道目标硬件(MAC)地址。当target_hw为空呼叫arp_send()时,发送广播 ARP 请求:
arp_send(ARPOP_REQUEST, ETH_P_ARP, target, dev, saddr,
dst_hw, dev->dev_addr, NULL);
}
我们来看看arp_send()法,挺短的:
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)
{
struct sk_buff *skb;
/*
* No arp on this interface.
*/
您必须检查该网络设备是否支持 IFF_NOARP。存在 ARP 被禁用的情况:例如,管理员可以通过ifconfig eth1 –arp或ip link set eth1 arp off禁用 ARP。一些网络设备在创建时设置 IFF_NOARP 标志,例如,IPv4 隧道设备或 PPP 设备,它们不需要 ARP。参见net/ipv4/ipip.c中的ipip_tunnel_setup()方法或drivers/net/ppp_generic.c中的ppp_setup()方法。
if (dev->flags&IFF_NOARP)
return;
arp_create()方法 创建一个带有 ARP 头的 SKB,并根据指定的参数对其进行初始化:
skb = arp_create(type, ptype, dest_ip, dev, src_ip,
dest_hw, src_hw, target_hw);
if (skb == NULL)
return;
arp_xmit()方法唯一做的事情是通过 NF_HOOK()宏 : 调用dev_queue_xmit()
arp_xmit(skb);
}
现在是时候了解如何处理这些 ARP 请求以及如何处理 ARP 回复了。
ARP:接收请求和回复
在 IPv4 中,arp_rcv()方法负责处理 ARP 数据包,如前所述。我们来看看arp_rcv()的方法:
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;
如果接收 ARP 数据包的网络设备设置了 IFF_NOARP 标志,或者如果数据包的目的地不是本地机器,或者是环回设备,那么应该丢弃数据包。您继续进行一些更全面的检查,如果一切正常,您将继续执行arp_process()方法,该方法执行处理 ARP 数据包的实际工作:
if (dev->flags & IFF_NOARP ||
skb->pkt_type == PACKET_OTHERHOST ||
skb->pkt_type == PACKET_LOOPBACK)
goto freeskb;
如果 SKB 是共享的,您必须克隆它,因为它可能在被arp_rcv()方法处理时被其他人更改。如果共享的话,skb_share_check()方法创建 SKB 的克隆(参见附录 A )。
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
goto out_of_mem;
/* ARP header, plus 2 device addresses, plus 2 IP addresses. */
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));
return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);
freeskb:
kfree_skb(skb);
out_of_mem:
return 0;
}
处理 ARP 请求不限于以本地主机为目的地的数据包。当本地主机被配置为代理 ARP 或专用 VLAN 代理 ARP(参见 RFC 3069)时,您还可以处理目的地不是本地主机的数据包。内核 2.6.34 增加了对私有 VLAN 代理 ARP 的支持。
在arp_process()方法中,您只处理 ARP 请求或 ARP 响应。对于 ARP 请求,您通过ip_route_input_noref()方法在路由子系统中执行查找。如果 ARP 数据包是发送给本地主机的(路由条目的rt_type是 RTN_LOCAL),那么您就要检查一些条件(稍后会介绍)。如果所有这些检查都通过了,一个 ARP 回复就会用arp_send()方法发送回来。如果 ARP 包不是给本地主机的,而是应该被转发的(路由条目的rt_type是 RTN_UNICAST),那么您检查一些条件(也将简要描述),如果它们被满足,您通过调用pneigh_lookup()方法在代理 ARP 表中执行查找。
现在您将看到处理 ARP 请求的主要 ARP 方法的实现细节,即arp_process()方法。
arp_process()方法
让我们来看看arp_process()方法,真正的工作是在这里完成的:
static int arp_process(struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
struct in_device *in_dev = __in_dev_get_rcu(dev);
struct arphdr *arp;
unsigned char *arp_ptr;
struct rtable *rt;
unsigned char *sha;
__be32 sip, tip;
u16 dev_type = dev->type;
int addr_type;
struct neighbour *n;
struct net *net = dev_net(dev);
/* arp_rcv below verifies the ARP header and verifies the device
* is ARP'able.
*/
if (in_dev == NULL)
goto out;
从 SKB 获取 ARP 头(是网络头,见arp_hdr()方法):
arp = arp_hdr(skb);
switch (dev_type) {
default:
if (arp->ar_pro != htons(ETH_P_IP) ||
htons(dev_type) != arp->ar_hrd)
goto out;
break;
case ARPHRD_ETHER:
. . .
if ((arp->ar_hrd != htons(ARPHRD_ETHER) &&
arp->ar_hrd != htons(ARPHRD_IEEE802)) ||
arp->ar_pro != htons(ETH_P_IP))
goto out;
break;
. . .
您希望在arp_process()方法中只处理 ARP 请求或 ARP 响应,并丢弃所有其他数据包:
/* Understand only these message types */
if (arp->ar_op != htons(ARPOP_REPLY) &&
arp->ar_op != htons(ARPOP_REQUEST))
goto out;
/*
* Extract fields
*/
arp_ptr = (unsigned char *)(arp + 1);
arp_process()方法—提取报头:
紧接在 ARP 报头之后,有以下字段(参见上面的 ARP 报头定义):
sha:源硬件地址(MAC 地址,6 字节)。sip:源 IPv4 地址(4 字节)。tha:目标硬件地址(MAC 地址,6 字节)。tip:目标 IPv4 地址(4 字节)。
提取sip和tip地址:
sha = arp_ptr;
arp_ptr += dev->addr_len;
将arp_ptr前移相应的偏移量后,将sip设置为源 IPv4 地址:
memcpy(&sip, arp_ptr, 4);
arp_ptr += 4;
switch (dev_type) {
. . .
default:
arp_ptr += dev->addr_len;
}
将arp_ptr前移相应的偏移量后,将tip设置为目标 IPv4 地址;
memcpy(&tip, arp_ptr, 4);
丢弃这两种类型的数据包:
- 多播数据包
- 如果禁用了带有环回地址的本地路由,则为环回设备发送数据包;另请参见本章末尾“快速参考”一节中对 IN_DEV_ROUTE_LOCALNET 宏的描述。
/*
* Check for bad requests for 127.x.x.x and requests for multicast
* addresses. If this is one such, delete it.
*/
if (ipv4_is_multicast(tip) ||
(!IN_DEV_ROUTE_LOCALNET(in_dev) && ipv4_is_loopback(tip)))
goto out;
. . .
使用重复地址检测(DAD)时,源 IP ( sip)为 0。DAD 允许您检测 LAN 上不同主机上是否存在双 L3 地址。DAD 在 IPv6 中作为地址配置过程中不可或缺的一部分实施,但在 IPv4 中则不是。但是,在 IPv4 中支持正确处理 DAD 请求,您很快就会看到这一点。iputils包的arping实用程序是在 IPv4 中使用 DAD 的一个例子。当使用arping –D发送 ARP 请求时,您发送了一个 ARP 请求,其中 ARP 报头的sip为 0。(–D修饰符告诉arping处于 DAD 模式);tip通常是发送方 IPv4 地址(因为你要检查同一个局域网上是否有另一台主机和你的 IPv4 地址相同);如果存在与 DAD ARP 请求的tip具有相同 IP 地址的主机,它将发回一个 ARP 回复(不将发送方添加到其邻居表中):
/* Special case: IPv4 duplicate address detection packet (RFC2131) */
if (sip == 0) {
if (arp->ar_op == htons(ARPOP_REQUEST) &&
arp_process()方法—arp_ignore()和 arp_filter()方法
arp_ignore procfs条目支持发送 ARP 回复作为对 ARP 请求的响应的不同模式。使用的值是/proc/sys/net/ipv4/conf/all/arp_ignore和/proc/sys/net/ipv4/conf/<netDeviceName>/arp_ignore的最大值。默认情况下,arp_ignore procfs条目的值是 0,在这种情况下,arp_ignore() 方法返回 0。您用arp_send()回复 ARP 请求,在下一段代码中可以看到(假设inet_addr_type(net, tip)返回 RTN_LOCAL)。arp_ignore()方法检查 IN_DEV_ARP_IGNORE(in_dev)的值;有关更多详细信息,请参见net/ipv4/arp.c中的arp_ignore() 实现以及本章末尾“快速参考”一节中对 IN_DEV_ARP_IGNORE 宏的描述:
inet_addr_type(net, tip) == RTN_LOCAL &&
!arp_ignore(in_dev, sip, tip))
arp_send(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha,
dev->dev_addr, sha);
goto out;
}
if (arp->ar_op == htons(ARPOP_REQUEST) &&
ip_route_input_noref(skb, tip, sip, 0, dev) == 0) {
rt = skb_rtable(skb);
addr_type = rt->rt_type;
当addr_type等于 RTN_LOCAL 时,数据包用于本地传送:
if (addr_type == RTN_LOCAL) {
int dont_send;
dont_send = arp_ignore(in_dev, sip, tip);
arp_filter()方法在两种情况下失败(返回 1):
- 当使用
ip_route_output()方法在路由表中查找失败时。 - 当路由条目的输出网络设备不同于接收 ARP 请求的网络设备时。
如果成功,arp_filter()方法返回 0(另请参见本章末尾“快速参考”一节中对 IN_DEV_ARPFILTER 宏的描述):
if (!dont_send && IN_DEV_ARPFILTER(in_dev))
dont_send = arp_filter(sip, tip, dev);
if (!dont_send) {
在发送 ARP 回复之前,您希望将发送方添加到您的邻表中或更新它;这是通过neigh_event_ns()方法完成的。neigh_event_ns()方法创建一个新的邻居表条目,并将其状态设置为 NUD 陈旧。如果已经有这样一个条目,它用neigh_update()方法将其状态更新为 NUD 陈旧。以这种方式添加条目被称为被动学习:
n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
if (n) {
arp_send(ARPOP_REPLY, ETH_P_ARP, sip,
dev, tip, sha, dev->dev_addr,
sha);
neigh_release(n);
}
}
goto out;
} else if (IN_DEV_FORWARD(in_dev)) {
当设备可以用作 ARP 代理时,arp_fwd_proxy()方法返回 1;当设备可以用作 ARP VLAN 代理时,arp_fwd_pvlan()方法返回 1:
if (addr_type == RTN_UNICAST &&
(arp_fwd_proxy(in_dev, dev, rt) ||
arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||
(rt->dst.dev != dev &&
pneigh_lookup(&arp_tbl, net, &tip, dev, 0)))) {
再次调用neigh_event_ns()方法,用 NUD _ 陈旧创建发送方的邻居条目,或者如果这样的条目存在,将该条目状态更新为 NUD _ 陈旧:
n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
if (n)
neigh_release(n);
if (NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED ||
skb->pkt_type == PACKET_HOST ||
in_dev->arp_parms->proxy_delay == 0) {
arp_send(ARPOP_REPLY, ETH_P_ARP, sip,
dev, tip, sha, dev->dev_addr,
sha);
} else {
通过将 SKB 放在proxy_queue的尾部,调用pneigh_enqueue()方法,延迟发送 ARP 回复。注意,延迟是随机的,是一个介于 0 和in_dev->arp_parms->proxy_delay之间的数字:
pneigh_enqueue(&arp_tbl,
in_dev->arp_parms, skb);
return 0;
}
goto out;
}
}
}
/* Update our ARP tables */
注意,调用__neigh_lookup() 方法的最后一个参数是 0,这意味着您只在邻居表中执行查找(如果查找失败,不创建新的邻居):
n = __neigh_lookup(&arp_tbl, &sip, dev, 0);
IN_DEV_ARP_ACCEPT 宏告诉您网络设备是否设置为接受 ARP 请求(另请参见本章末尾的“快速参考”部分中对 IN_DEV_ARP_ACCEPT 宏的描述):
if (IN_DEV_ARP_ACCEPT(in_dev)) {
/* Unsolicited ARP is not accepted by default.
It is possible, that this option should be enabled for some
devices (strip is candidate)
*/
未经请求的 ARP 请求仅用于更新邻居表。在这样的请求中,tip等于sip(arping实用程序支持通过arping –U发送未经请求的 ARP 请求):
if (n == NULL &&
(arp->ar_op == htons(ARPOP_REPLY) ||
(arp->ar_op == htons(ARPOP_REQUEST) && tip == sip)) &&
inet_addr_type(net, sip) == RTN_UNICAST)
n = __neigh_lookup(&arp_tbl, &sip, dev, 1);
}
if (n) {
int state = NUD_REACHABLE;
int override;
/* If several different ARP replies follows back-to-back,
use the FIRST one. It is possible, if several proxy
agents are active. Taking the first reply prevents
arp trashing and chooses the fastest router.
*/
override = time_after(jiffies, n->updated + n->parms->locktime);
/* 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()更新邻表:
neigh_update(n, sha, state,
override ? NEIGH_UPDATE_F_OVERRIDE : 0);
neigh_release(n);
}
out:
consume_skb(skb);
return 0;
}
既然您已经了解了 IPv4 ARP 协议的实现,那么是时候转向 IPv6 NDISC 协议的实现了。您将很快注意到 IPv4 和 IPv6 中相邻子系统实现之间的一些差异。
NDISC 协议(IPv6)
邻居发现(NDISC)协议基于 RFC 2461,“IP 版本 6 (IPv6)的邻居发现”,该协议后来在 2007 年被 RFC 4861 废弃。同一链路上的 IPv6 节点(主机或路由器)使用邻居发现协议来发现彼此的存在、发现路由器、确定彼此的 L2 地址以及维护邻居可达性信息。添加了重复地址检测(DAD ),以避免在同一个 LAN 上出现两个 L3 地址。我将讨论 DAD 和处理 NDISC 邻居请求和邻居广告。
接下来,您将了解 IPv6 邻居发现协议如何避免创建重复的 IPv6 地址。
重复地址检测
您如何确定局域网上没有其他相同的 IPv6 地址?这种可能性很低,但如果这样的地址确实存在,它可能会引起麻烦。爸爸是一个解决办法。当主机尝试配置地址时,它首先创建一个链路本地地址(链路本地地址以 FE80 开头)。这个地址是暂定的(IFA _ F _ 暂定),这意味着主机只能与 ND 消息通信。然后主机通过调用addrconf_dad_start()方法 ( net/ipv6/addrconf.c)启动 DAD 进程。主机发送邻居请求 DAD 消息。目标是它的暂定地址,源是全零(未指定的地址)。如果在指定的时间间隔内没有应答,状态将变为永久(IFA _ F _ 永久)。当设置乐观 DAD (CONFIG_IPV6_OPTIMISTIC_DAD)时,您不会等到 DAD 完成,而是允许主机在 DAD 成功完成之前与对等方通信。参见 RFC 4429,“IPv6 的乐观重复地址检测(DAD)”,2006 年。
IPv6 的邻居表称为nd_tbl:
struct neigh_table nd_tbl = {
.family = AF_INET6,
.key_len = sizeof(struct in6_addr),
.hash = ndisc_hash,
.constructor = ndisc_constructor,
.pconstructor = pndisc_constructor,
.pdestructor = pndisc_destructor,
.proxy_redo = pndisc_redo,
.id = "ndisc_cache",
.parms = {
.tbl = &nd_tbl,
.base_reachable_time = ND_REACHABLE_TIME,
.retrans_time = ND_RETRANS_TIMER,
.gc_staletime = 60 * HZ,
.reachable_time = ND_REACHABLE_TIME,
.delay_probe_time = 5 * HZ,
.queue_len_bytes = 64*1024,
.ucast_probes = 3,
.mcast_probes = 3,
.anycast_delay = 1 * HZ,
.proxy_delay = (8 * HZ) / 10,
.proxy_qlen = 64,
},
.gc_interval = 30 * HZ,
.gc_thresh1 = 128,
.gc_thresh2 = 512,
.gc_thresh3 = 1024,
};
(net/ipv6/ndisc.c)
注意,NDISC 表中的一些成员等于 ARP 表中的并行成员,例如,垃圾收集器阈值的值(gc_thresh1、gc_thresh2和gc_thresh3)。
Linux IPv6 邻居发现实现基于 ICMPv6 消息来管理相邻节点之间的交互。邻居发现协议定义了以下五种 ICMPv6 消息类型:
#define NDISC_ROUTER_SOLICITATION 133
#define NDISC_ROUTER_ADVERTISEMENT 134
#define NDISC_NEIGHBOUR_SOLICITATION 135
#define NDISC_NEIGHBOUR_ADVERTISEMENT 136
#define NDISC_REDIRECT 137
(include/net/ndisc.h)
请注意,这五种 ICMPv6 消息类型是信息性消息。值在 0 到 127 范围内的 ICMPv6 消息类型是错误消息,值在 128 到 255 范围内的 ICMPv6 消息类型是信息性消息。关于这方面的更多信息,请参见第三章,其中讨论了 ICMP 协议。本章仅讨论邻居请求和邻居发现消息。
正如本章开头所提到的,因为邻居发现消息是 ICMPv6 消息,所以它们由icmpv6_rcv()方法处理,该方法又为消息类型是前面提到的五种类型之一的 ICMPv6 数据包调用ndisc_rcv()方法(参见net/ipv6/icmp.c)。
在 NDISC 中,有三个neigh_ops对象:ndisc_generic_ops、ndisc_hh_ops和ndisc_direct_ops:
- 如果
net_device对象的header_ops为空,则neigh_ops对象将被设置为ndisc_direct_ops。就像在arp_direct_ops的情况下一样,发送数据包是用neigh_direct_output()方法完成的,这实际上是一个对dev_queue_xmit()的包装。注意,正如前面 ARP 部分提到的,在大多数以太网设备中,net_device对象的header_ops不为空。 - 如果
net_device对象的header_ops包含一个空的cache()回调,那么neigh_ops对象被设置为ndisc_generic_ops。 - 如果
net_device对象的header_ops包含一个非空的cache()回调,那么neigh_ops对象被设置为ndisc_hh_ops。
本节讨论了 DAD 机制以及它如何帮助避免重复地址。下一节描述如何发送征求请求。
NIDSC:发送招标请求
与您在 IPv6 中看到的类似,如果没有找到任何匹配项,您也会执行查找并创建一个条目:
static int ip6_finish_output2(struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb);
struct net_device *dev = dst->dev;
struct neighbour *neigh;
struct in6_addr *nexthop;
int ret;
. . .
. . .
nexthop = rt6_nexthop((struct rt6_info *)dst, &ipv6_hdr(skb)->daddr);
neigh = __ipv6_neigh_lookup_noref(dst->dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&nd_tbl, nexthop, dst->dev, false);
if (!IS_ERR(neigh)) {
ret = dst_neigh_output(dst, neigh, skb);
. . .
最终,就像在 IPv4 Tx 路径中一样,您从neigh_probe()方法中调用 solicit 方法neigh->ops->solicit(neigh, skb)。这种情况下的neigh->ops->solicit就是ndisc_solicit()方法。ndisc_solicit() 是一种很短的方法;事实上,它是对ndisc_send_ns()方法的包装:
static void ndisc_solicit(struct neighbour *neigh, struct sk_buff *skb)
{
struct in6_addr *saddr = NULL;
struct in6_addr mcaddr;
struct net_device *dev = neigh->dev;
struct in6_addr *target = (struct in6_addr *)&neigh->primary_key;
int probes = atomic_read(&neigh->probes);
if (skb && ipv6_chk_addr(dev_net(dev), &ipv6_hdr(skb)->saddr, dev, 1))
saddr = &ipv6_hdr(skb)->saddr;
if ((probes -= neigh->parms->ucast_probes) < 0) {
if (!(neigh->nud_state & NUD_VALID)) {
ND_PRINTK(1, dbg,
"%s: trying to ucast probe in NUD_INVALID: %pI6\n",
__func__, target);
}
ndisc_send_ns(dev, neigh, target, target, saddr);
} else if ((probes -= neigh->parms->app_probes) < 0) {
#ifdef CONFIG_ARPD
neigh_app_ns(neigh);
#endif
} else {
addrconf_addr_solict_mult(target, &mcaddr);
ndisc_send_ns(dev, NULL, target, &mcaddr, saddr);
}
}
为了发送征集请求,我们需要构建一个nd_msg对象:
struct nd_msg {
struct icmp6hdr icmph;
struct in6_addr target;
__u8 opt[0];
};
(include/net/ndisc.h)
对于请求请求,ICMPv6 标头类型应设置为 NDISC _ NEIGHBOUR _ SOLICITATION,对于请求回复,ICMPv6 标头类型应设置为 NDISC _ NEIGHBOUR _ ADVERTISEMENT。请注意,对于邻居广告消息,有时需要在 ICMPv6 标头中设置标志。ICMPv6 报头包括一个名为icmpv6_nd_advt的结构,该结构包括覆盖、请求和路由器标志:
struct icmp6hdr {
__u8 icmp6_type;
__u8 icmp6_code;
__sum16 icmp6_cksum;
union {
. . .
. . .
struct icmpv6_nd_advt {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u32 reserved:5,
override:1,
solicited:1,
router:1,
reserved2:24;
. . .
#endif
} u_nd_advt;
} icmp6_dataun;
. . .
#define icmp6_router icmp6_dataun.u_nd_advt.router
#define icmp6_solicited icmp6_dataun.u_nd_advt.solicited
#define icmp6_override icmp6_dataun.u_nd_advt.override
. . .
(include/uapi/linux/icmpv6.h)
- 当响应邻居请求发送消息时,设置
solicited标志(icmp6_solicited)。 - 当你想要覆盖一个相邻的高速缓存条目(更新 L2 地址)时,你设置
override标志(icmp6_override)。 - 当发送邻居通告消息的主机是路由器时,您设置
router标志(icmp6_router)。
您可以在下面的ndisc_send_na()方法中看到这三个标志的用法。我们来看看ndisc_send_ns()法:
void ndisc_send_ns(struct net_device *dev, struct neighbour *neigh,
const struct in6_addr *solicit,
const struct in6_addr *daddr, const struct in6_addr *saddr)
{
struct sk_buff *skb;
struct in6_addr addr_buf;
int inc_opt = dev->addr_len;
int optlen = 0;
struct nd_msg *msg;
if (saddr == NULL) {
if (ipv6_get_lladdr(dev, &addr_buf,
(IFA_F_TENTATIVE|IFA_F_OPTIMISTIC)))
return;
saddr = &addr_buf;
}
if (ipv6_addr_any(saddr))
inc_opt = 0;
if (inc_opt)
optlen += ndisc_opt_addr_space(dev);
skb = ndisc_alloc_skb(dev, sizeof(*msg) + optlen);
if (!skb)
return;
构建嵌入在nd_msg对象中的 ICMPv6 头:
msg = (struct nd_msg *)skb_put(skb, sizeof(*msg));
*msg = (struct nd_msg) {
.icmph = {
.icmp6_type = NDISC_NEIGHBOUR_SOLICITATION,
},
.target = *solicit,
};
if (inc_opt)
ndisc_fill_addr_option(skb, ND_OPT_SOURCE_LL_ADDR,
dev->dev_addr);
ndisc_send_skb(skb, daddr, saddr);
}
我们来看看ndisc_send_na()的方法:
static void ndisc_send_na(struct net_device *dev, struct neighbour *neigh,
const struct in6_addr *daddr,
const struct in6_addr *solicited_addr,
bool router, bool solicited, bool override, bool inc_opt)
{
struct sk_buff *skb;
struct in6_addr tmpaddr;
struct inet6_ifaddr *ifp;
const struct in6_addr *src_addr;
struct nd_msg *msg;
int optlen = 0;
. . .
skb = ndisc_alloc_skb(dev, sizeof(*msg) + optlen);
if (!skb)
return;
构建嵌入在nd_msg对象中的 ICMPv6 头:
msg = (struct nd_msg *)skb_put(skb, sizeof(*msg));
*msg = (struct nd_msg) {
.icmph = {
.icmp6_type = NDISC_NEIGHBOUR_ADVERTISEMENT,
.icmp6_router = router,
.icmp6_solicited = solicited,
.icmp6_override = override,
},
.target = *solicited_addr,
};
if (inc_opt)
ndisc_fill_addr_option(skb, ND_OPT_TARGET_LL_ADDR,
dev->dev_addr);
ndisc_send_skb(skb, daddr, src_addr);
}
本节描述了如何发送招标请求。下一节将讨论如何处理邻居招揽和广告。
NDISC:接收邻居请求和广告
如上所述,ndisc_rcv()方法处理所有五种邻居发现消息类型;让我们来看看这个方法:
int ndisc_rcv(struct sk_buff *skb)
{
struct nd_msg *msg;
if (skb_linearize(skb))
return 0;
msg = (struct nd_msg *)skb_transport_header(skb);
__skb_push(skb, skb->data - skb_transport_header(skb));
根据 RFC 4861,邻居消息的跳数限制应该是 255;跳数限制长度为 8 位,因此最大跳数限制为 255。值 255 确保数据包没有被转发,这保证您不会受到某种安全攻击。不满足此要求的数据包将被丢弃:
if (ipv6_hdr(skb)->hop_limit != 255) {
ND_PRINTK(2, warn, "NDISC: invalid hop-limit: %d\n",
ipv6_hdr(skb)->hop_limit);
return 0;
}
根据 RFC 4861,邻居消息的 ICMPv6 代码应该为 0,因此丢弃不满足此要求的数据包:
if (msg->icmph.icmp6_code != 0) {
ND_PRINTK(2, warn, "NDISC: invalid ICMPv6 code: %d\n",
msg->icmph.icmp6_code);
return 0;
}
memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));
switch (msg->icmph.icmp6_type) {
case NDISC_NEIGHBOUR_SOLICITATION:
ndisc_recv_ns(skb);
break;
case NDISC_NEIGHBOUR_ADVERTISEMENT:
ndisc_recv_na(skb);
break;
case NDISC_ROUTER_SOLICITATION:
ndisc_recv_rs(skb);
break;
case NDISC_ROUTER_ADVERTISEMENT:
ndisc_router_discovery(skb);
break;
case NDISC_REDIRECT:
ndisc_redirect_rcv(skb);
break;
}
return 0;
}
我不在本章讨论路由器请求和路由器广告,因为它们在第八章中讨论过。我们来看看ndisc_recv_ns()法:
static void ndisc_recv_ns(struct sk_buff *skb)
{
struct nd_msg *msg = (struct nd_msg *)skb_transport_header(skb);
const struct in6_addr *saddr = &ipv6_hdr(skb)->saddr;
const struct in6_addr *daddr = &ipv6_hdr(skb)->daddr;
u8 *lladdr = NULL;
u32 ndoptlen = skb->tail - (skb->transport_header +
offsetof(struct nd_msg, opt));
struct ndisc_options ndopts;
struct net_device *dev = skb->dev;
struct inet6_ifaddr *ifp;
struct inet6_dev *idev = NULL;
struct neighbour *neigh;
当saddr是全零的未指定地址(IPV6_ADDR_ANY)时,ipv6_addr_any()方法返回 1。当源地址是未指定的地址(全零)时,这意味着请求是 DAD:
int dad = ipv6_addr_any(saddr);
bool inc;
int is_router = -1;
执行一些有效性检查:
if (skb->len < sizeof(struct nd_msg)) {
ND_PRINTK(2, warn, "NS: packet too short\n");
return;
}
if (ipv6_addr_is_multicast(&msg->target)) {
ND_PRINTK(2, warn, "NS: multicast target address\n");
return;
}
/*
* RFC2461 7.1.1:
* DAD has to be destined for solicited node multicast address.
*/
if (dad && !ipv6_addr_is_solict_mult(daddr)) {
ND_PRINTK(2, warn, "NS: bad DAD packet (wrong destination)\n");
return;
}
if (!ndisc_parse_options(msg->opt, ndoptlen, &ndopts)) {
ND_PRINTK(2, warn, "NS: invalid ND options\n");
return;
}
if (ndopts.nd_opts_src_lladdr) {
lladdr = ndisc_opt_addr_data(ndopts.nd_opts_src_lladdr, dev);
if (!lladdr) {
ND_PRINTK(2, warn,
"NS: invalid link-layer address length\n");
return;
}
/* RFC2461 7.1.1:
* If the IP source address is the unspecified address,
* there MUST NOT be source link-layer address option
* in the message.
*/
if (dad) {
ND_PRINTK(2, warn,
"NS: bad DAD packet (link-layer address option)\n");
return;
}
}
inc = ipv6_addr_is_multicast(daddr);
ifp = ipv6_get_ifaddr(dev_net(dev), &msg->target, dev, 1);
if (ifp) {
if (ifp->flags & (IFA_F_TENTATIVE|IFA_F_OPTIMISTIC)) {
if (dad) {
/*
* We are colliding with another node
* who is doing DAD
* so fail our DAD process
*/
addrconf_dad_failure(ifp);
return;
} else {
/*
* This is not a dad solicitation.
* If we are an optimistic node,
* we should respond.
* Otherwise, we should ignore it.
*/
if (!(ifp->flags & IFA_F_OPTIMISTIC))
goto out;
}
}
idev = ifp->idev;
} else {
struct net *net = dev_net(dev);
idev = in6_dev_get(dev);
if (!idev) {
/* XXX: count this drop? */
return;
}
if (ipv6_chk_acast_addr(net, dev, &msg->target) ||
(idev->cnf.forwarding &&
(net->ipv6.devconf_all->proxy_ndp || idev->cnf.proxy_ndp) &&
(is_router = pndisc_is_router(&msg->target, dev)) >= 0)) {
if (!(NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED) &&
skb->pkt_type != PACKET_HOST &&
inc != 0 &&
idev->nd_parms->proxy_delay != 0) {
/*
* for anycast or proxy,
* sender should delay its response
* by a random time between 0 and
* MAX_ANYCAST_DELAY_TIME seconds.
* (RFC2461) -- yoshfuji
*/
struct sk_buff *n = skb_clone(skb, GFP_ATOMIC);
if (n)
pneigh_enqueue(&nd_tbl, idev->nd_parms, n);
goto out;
}
} else
goto out;
}
if (is_router < 0)
is_router = idev->cnf.forwarding;
if (dad) {
发送邻居广告消息:
ndisc_send_na(dev, NULL, &in6addr_linklocal_allnodes, &msg->target,
!!is_router, false, (ifp != NULL), true);
goto out;
}
if (inc)
NEIGH_CACHE_STAT_INC(&nd_tbl, rcv_probes_mcast);
else
NEIGH_CACHE_STAT_INC(&nd_tbl, rcv_probes_ucast);
/*
* update / create cache entry
* for the source address
*/
neigh = __neigh_lookup(&nd_tbl, saddr, dev,
!inc || lladdr || !dev->addr_len);
if (neigh)
用发件人的 L2 地址更新你的邻表;nud_state将被设置为 NUD 陈旧:
neigh_update(neigh, lladdr, NUD_STALE,
NEIGH_UPDATE_F_WEAK_OVERRIDE|
NEIGH_UPDATE_F_OVERRIDE);
if (neigh || !dev->header_ops) {
发送邻居广告消息:
ndisc_send_na(dev, neigh, saddr, &msg->target,
!!is_router,
true, (ifp != NULL && inc), inc);
if (neigh)
neigh_release(neigh);
}
out:
if (ifp)
in6_ifa_put(ifp);
else
in6_dev_put(idev);
}
让我们来看看处理邻居广告的方法,ndisc_recv_na() :
static void ndisc_recv_na(struct sk_buff *skb)
{
struct nd_msg *msg = (struct nd_msg *)skb_transport_header(skb);
const struct in6_addr *saddr = &ipv6_hdr(skb)->saddr;
const struct in6_addr *daddr = &ipv6_hdr(skb)->daddr;
u8 *lladdr = NULL;
u32 ndoptlen = skb->tail - (skb->transport_header +
offsetof(struct nd_msg, opt));
struct ndisc_options ndopts;
struct net_device *dev = skb->dev;
struct inet6_ifaddr *ifp;
struct neighbour *neigh;
if (skb->len < sizeof(struct nd_msg)) {
ND_PRINTK(2, warn, "NA: packet too short\n");
return;
}
if (ipv6_addr_is_multicast(&msg->target)) {
ND_PRINTK(2, warn, "NA: target address is multicast\n");
return;
}
if (ipv6_addr_is_multicast(daddr) &&
msg->icmph.icmp6_solicited) {
ND_PRINTK(2, warn, "NA: solicited NA is multicasted\n");
return;
}
if (!ndisc_parse_options(msg->opt, ndoptlen, &ndopts)) {
ND_PRINTK(2, warn, "NS: invalid ND option\n");
return;
}
if (ndopts.nd_opts_tgt_lladdr) {
lladdr = ndisc_opt_addr_data(ndopts.nd_opts_tgt_lladdr, dev);
if (!lladdr) {
ND_PRINTK(2, warn,
"NA: invalid link-layer address length\n");
return;
}
}
ifp = ipv6_get_ifaddr(dev_net(dev), &msg->target, dev, 1);
if (ifp) {
if (skb->pkt_type != PACKET_LOOPBACK
&& (ifp->flags & IFA_F_TENTATIVE)) {
addrconf_dad_failure(ifp);
return;
}
/* What should we make now? The advertisement
is invalid, but ndisc specs say nothing
about it. It could be misconfiguration, or
an smart proxy agent tries to help us :-)
We should not print the error if NA has been
received from loopback - it is just our own
unsolicited advertisement.
*/
if (skb->pkt_type != PACKET_LOOPBACK)
ND_PRINTK(1, warn,
"NA: someone advertises our address %pI6 on %s!\n",
&ifp->addr, ifp->idev->dev->name);
in6_ifa_put(ifp);
return;
}
neigh = neigh_lookup(&nd_tbl, &msg->target, dev);
if (neigh) {
u8 old_flags = neigh->flags;
struct net *net = dev_net(dev);
if (neigh->nud_state & NUD_FAILED)
goto out;
/*
* Don't update the neighbour cache entry on a proxy NA from
* ourselves because either the proxied node is off link or it
* has already sent a NA to us.
*/
if (lladdr && !memcmp(lladdr, dev->dev_addr, dev->addr_len) &&
net->ipv6.devconf_all->forwarding &&
net->ipv6.devconf_all->proxy_ndp &&
pneigh_lookup(&nd_tbl, net, &msg->target, dev, 0)) {
/* XXX: idev->cnf.proxy_ndp */
goto out;
}
更新邻表。当接收到的消息是邻居请求时,icmp6_solicited被设置,因此您想要将状态设置为 NUD _ 可达。当设置了icmp6_override标志时,您希望设置override标志(这意味着用指定的lladdr更新 L2 地址,如果不同的话):
neigh_update(neigh, lladdr,
msg->icmph.icmp6_solicited ? NUD_REACHABLE : NUD_STALE,
NEIGH_UPDATE_F_WEAK_OVERRIDE|
(msg->icmph.icmp6_override ? NEIGH_UPDATE_F_OVERRIDE : 0)|
NEIGH_UPDATE_F_OVERRIDE_ISROUTER|
(msg->icmph.icmp6_router ? NEIGH_UPDATE_F_ISROUTER : 0));
if ((old_flags & ∼neigh->flags) & NTF_ROUTER) {
/*
* Change: router to host
*/
struct rt6_info *rt;
rt = rt6_get_dflt_router(saddr, dev);
if (rt)
ip6_del_rt(rt);
}
out:
neigh_release(neigh);
}
}
摘要
本章描述了 IPv4 和 IPv6 中的相邻子系统。首先,您了解了相邻子系统的目标。然后,您了解了 IPv4 中的 ARP 请求和 ARP 回复,以及 IPv6 中的 NDISC 邻居请求和 NDISC 邻居通告。您还了解了 DAD 实现如何避免重复的 IPv6 地址,并且看到了处理相邻子系统请求和回复的各种方法。第八章讨论了 IPv6 子系统的实现。接下来的“快速参考”部分涵盖了与本章中讨论的主题相关的主要方法和宏,按其上下文排序。我还展示了neigh_statistics结构,它表示相邻子系统收集的统计数据。
快速参考
以下是相邻子系统的一些重要方法和宏,以及对neigh_statistics结构的描述。
注核心邻码在
net/core/neighbour.c、include/net/neighbour.h和include/uapi/linux/neighbour.h。
ARP 代码(IPv4)在net/ipv4/arp.c、include/net/arp.h和include/uapi/linux/if_arp.h中。
NDISC 代码(IPv6)在net/ipv6/ndisc.c和include/net/ndisc.h中。
方法
让我们从介绍方法开始。
void neigh_table_init(结构 neigh_table *tbl)
该方法调用neigh_table_init_no_netlink()方法来执行邻表的初始化,并将该表链接到全局邻表链表(neigh_tables)。
void neigh _ table _ init _ no _ netlink(struct neigh _ table * TBL)
这个方法执行所有的邻居初始化,除了链接到全局邻居表链表,这是由neigh_table_init()完成的,如前所述。
int neigh_table_clear(结构 neigh_table *tbl)
该方法释放指定邻表的资源。
struct neighbor * neigh _ alloc(struct neigh _ table * TBL,struct net_device *dev)
这个方法分配一个邻居对象。
struct neigh _ hash _ table * neigh _ hash _ alloc(无符号整数移位)
这个方法分配一个相邻的哈希表。
struct neighbor * _ _ neigh _ create(struct neigh _ table * TBL,const void *pkey,struct net_device *dev,bool want_ref)
这个方法创建一个邻居对象。
int neigh_add(struct sk_buff *skb,struct nlmsghdr *nlh,void *arg)
此方法添加一个邻居条目;它是 netlink RTM_NEWNEIGH 消息的处理程序。
int neigh _ delete(struct sk _ buff * skb,struct nlmsghdr *nlh,void *arg)
此方法删除邻居条目;它是 netlink RTM_DELNEIGH 消息的处理程序。
void neigh _ probe(struct neighbor * neigh)
这个方法从邻居arp_queue获取一个 SKB,并调用相应的solicit()方法来发送它。在 ARP 的情况下,它将是arp_solicit()。它递增邻居probes计数器并释放数据包。
int neigh _ forced _ GC(struct neigh _ table * TBL)
此方法是同步垃圾收集方法。它移除不处于永久状态(NUD _ 永久)并且其引用计数等于 1 的邻居条目。邻居的移除和清理是通过首先将邻居的失效标志设置为 1,然后调用neigh_cleanup_and_release()方法来完成的,该方法获取一个邻居对象作为参数。在某些情况下,从neigh_alloc()方法调用neigh_forced_gc()方法,如本章前面的“创建和释放邻居”一节所述。如果至少移除了一个邻居对象,则neigh_forced_gc()方法返回 1,否则返回 0。
void neigh _ periodic _ work(struct work _ struct * work)
这个方法是异步垃圾收集器处理程序。
静态 void neigh_timer_handler(无符号长整型参数)
此方法是每邻居定期计时器垃圾收集器处理程序。
struct neighbor * _ _ neigh _ lookup(struct neigh _ table * TBL,const void *pkey,struct net_device *dev,int creat)
此方法通过给定的键在指定的相邻表中执行查找。如果creat参数为 1,并且查找失败,调用neigh_create()方法在指定的邻居表中创建一个邻居条目并返回它。
neigh_hh_init(结构邻居*n,结构 dst_entry *dst)
此方法根据指定的路由缓存条目初始化指定邻居的 L2 缓存(hh_cache对象)。
void __init arp_init(void)
该方法执行 ARP 协议的设置:初始化 ARP 表,将arp_rcv()注册为接收 ARP 数据包的处理程序,初始化procfs条目,注册sysctl条目,注册 ARP netdev通知回调,arp_netdev_event()。
int arp_rcv(结构 sk_buff *skb,结构 net_device *dev,结构 packet_type *pt,结构 net_device *orig_dev)
此方法是 ARP 数据包(类型为 0x0806 的以太网数据包)的 Rx 处理程序。
int ARP _ constructor(struct neighbor * neigh)
此方法执行 ARP 邻居初始化。
int ARP _ process(struct sk _ buff * skb)
这个方法由arp_rcv()方法调用,处理 ARP 请求和 ARP 响应的主要处理。
void ARP _ solicit(struct neighbor * neigh,struct sk_buff *skb)
这个方法通过调用arp_send()方法,在一些检查和初始化之后发送请求(ARPOP_REQUEST)。
请参见 arp_send(int type,int ptype,_ _ _ _ _ 32 dest _ IP,struct net_device *dev,_ _ _ _ _ 32 src _ IP,const unsigned char * dest _ hw,const unsigned char * src _ hw,const unsigned char * target _ hw)
这个方法创建一个 ARP 包,通过调用arp_create()方法用指定的参数初始化它,并通过调用arp_xmit()方法发送它。
请参阅 ARP _ xmit(struct sk _ buf * skb)
这个方法实际上是通过用dev_queue_xmit()调用 NF_HOOK 宏来发送包的。
arphdr *arp_hdr 结构(const struct sk _ buff * skb)
这个方法获取指定 SKB 的 ARP 头。
int arp_mc_map(__be32 addr,u8 *haddr,struct net_device *dev,int dir)
该方法根据网络设备类型将 IPv4 地址转换为 L2(链路层)地址。例如,当设备是以太网设备时,这是通过ip_eth_mc_map()方法完成的;当设备是 Infiniband 设备时,这是通过ip_ib_mc_map()方法完成的。
静态内联 int ARP _ FWD _ proxy(struct in _ device * in _ dev,struct net_device *dev,struct rtable *rt)
如果指定的设备可以对指定的路由条目使用代理 ARP,则此方法返回 1。
静态内联 int ARP _ FWD _ pvlan(struct in _ device * in _ dev,struct net_device *dev,struct rtable *rt,__be32 sip,__be32 tip)
如果指定的设备可以对指定的路由条目和指定的 IPv4 源地址和目的地址使用代理 ARP VLAN,则此方法返回 1。
int ARP _ net dev _ event(struct notifier _ block * this,unsigned long event,void *ptr)
这个方法是用于netdev通知事件的 ARP 处理程序。
int ndisc _ net dev _ event(struct notifier _ block * this,unsigned long event,void *ptr)
这个方法是用于netdev通知事件的 NDISC 处理程序。
int ndis _ rcv(struct sk _ buf * skb)
该方法是接收五种类型请求包之一的主要 NDISC 处理程序。
静态 int neigh _ black hole(struct neighbor * neigh,struct sk_buff *skb)
此方法会丢弃数据包并返回–enet down 错误(网络中断)。
静态 void ndisc _ recv _ ns(struct sk _ buff * skb)和静态 void ndisc _ recv _ na(struct sk _ buff * skb)
这些方法分别处理接收邻居请求和邻居广告。
静态空 ndisc _ recv _ RS(struct sk _ buff * skb)和静态空 ndisc _ router _ discovery(struct sk _ buff * skb)
这些方法分别处理接收路由器请求和路由器广告。
int ndisc _ MC _ map(const struct in 6 _ addr * addr,char *buf,struct net_device *dev,int dir)
该方法根据网络设备类型将 IPv4 地址转换为 L2(链路层)地址。在 IPv6 下的以太网中,这是通过ipv6_eth_mc_map()方法完成的。
int ndisc _ constructor(struct neighbor * neigh)
此方法执行 NDISC 邻居初始化。
void ndisc _ solicit(struct neighbor * neigh,struct sk_buff *skb)
这个方法通过调用ndisc_send_ns()方法,在一些检查和初始化之后发送请求。
int icmpv 6 _ rcv(struct sk _ buf * skb)
此方法是接收 ICMPv6 消息的处理程序。
bool IPv6 _ addr _ any(const struct in 6 _ addr * a)
当给定的 IPv6 地址是全零的未指定地址(IPv6 _ ADDR _ 任意)时,此方法返回 1。
int inet _ addr _ onlink(struct in _ device * in _ dev,__be32 a,__be32 b)
此方法检查两个指定的地址是否在同一子网上。
宏指令
现在,让我们看看宏。
开发者代理地址解析器
如果/proc/sys/net/ipv4/conf/<netDevice>/proxy_arp被置位或/proc/sys/net/ipv4/conf/all/proxy_arp is set,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。
开发者代理地址解析协议 PVLAN
如果/proc/sys/net/ipv4/conf/<netDevice>/proxy_arp_pvlan被设置,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。
开发者地址过滤器(开发者地址)
如果/proc/sys/net/ipv4/conf/<netDevice>/arp_filter被置位或者/proc/sys/net/ipv4/conf/all/arp_filter被置位,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。
输入设备地址解析接受(输入设备)
如果/proc/sys/net/ipv4/conf/<netDevice>/arp_accept被置位或者/proc/sys/net/ipv4/conf/all/arp_accept被置位,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。
开发者地址解析通告
该宏返回/proc/sys/net/ipv4/conf/<netDevice>/arp_announce和/proc/sys/net/ipv4/conf/all/arp_announce的最大值,其中netDevice是与指定的in_dev关联的网络设备。
内部开发地址解析忽略(内部开发)
该宏返回/proc/sys/net/ipv4/conf/<netDevice>/arp_ignore和/proc/sys/net/ipv4/conf/all/arp_ignore的最大值,其中netDevice是与指定的in_dev关联的网络设备。
设备地址解析通知(设备地址)
该宏返回/proc/sys/net/ipv4/conf/<netDevice>/arp_notify和/proc/sys/net/ipv4/conf/all/arp_notify的最大值,其中netDevice是与指定的in_dev关联的网络设备。
开发共享媒体(开发中)
如果/proc/sys/net/ipv4/conf/<netDevice>/shared_media被置位或者/proc/sys/net/ipv4/conf/all/shared_media被置位,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。
开发中路由本地网
如果/proc/sys/net/ipv4/conf/<netDevice>/route_localnet被置位或者/proc/sys/net/ipv4/conf/all/route_localnet被置位,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。
neigh_hold()
此宏递增指定邻居的引用计数。
邻居统计结构
neigh_statistics结构对于监控相邻子系统很重要;正如本章开头提到的,ARP 和 NDISC 都通过procfs(分别是/proc/net/stat/arp_cache和/proc/net/stat/ndisc_cache,)导出这个结构成员。以下是对其成员的描述,并指出它们的增量:
struct neigh_statistics {
unsigned long allocs; /* number of allocated neighs */
unsigned long destroys; /* number of destroyed neighs */
unsigned long hash_grows; /* number of hash resizes */
unsigned long res_failed; /* number of failed resolutions */
unsigned long lookups; /* number of lookups */
unsigned long hits; /* number of hits (among lookups) */
unsigned long rcv_probes_mcast; /* number of received mcast ipv6 */
unsigned long rcv_probes_ucast; /* number of received ucast ipv6 */
unsigned long periodic_gc_runs; /* number of periodic GC runs */
unsigned long forced_gc_runs; /* number of forced GC runs */
unsigned long unres_discards; /* number of unresolved drops */
};
下面是对neigh_statistics结构成员的描述:
allocs:分配的邻居数量;通过neigh_alloc()方法递增。destroys:被摧毁的邻居的数量;通过neigh_destroy()方法递增。hash_grows:哈希调整大小的次数;通过neigh_hash_grow()方法递增。res_failed:解析失败的次数;通过neigh_invalidate()方法递增。lookups:已完成的邻居查找的数量;通过neigh_lookup()方法和neigh_lookup_nodev()方法递增。hits:执行邻居查找时的命中次数;当你命中时,通过neigh_lookup()方法和neigh_lookup_nodev()方法递增。rcv_probes_mcast:接收到的组播探测数(仅限 IPv6 通过ndisc_recv_ns()方法递增。rcv_probes_ucast:接收到的单播探测数(仅限 IPv6 通过ndisc_recv_ns()方法递增。periodic_gc_runs:周期性 GC 调用的次数;通过neigh_periodic_work()方法递增。forced_gc_runs:强制 GC 调用的次数;通过neigh_forced_gc()方法递增。unres_discards:未解决滴数;当丢弃未解析的数据包时,通过__neigh_event_send()方法递增。
桌子
这是被盖住的桌子。
表 7-1。网络不可达检测状态
|
Linux 操作系统
|
标志
|
| --- | --- |
| NUD_INCOMPLETE | 地址解析正在进行中,邻居的链路层地址尚未确定。这意味着已发送请求,您正在等待请求回复或超时。 |
| NUD 可到达 | 已知最近可以联系到该邻居。 |
| NUD 过时了 | 自从收到前向路径运行正常的最后一个肯定确认后,已过去了超过 ReachableTime 毫秒。 |
| NUD 延迟 | 不再知道该邻居是可到达的。暂时延迟发送探测,以便上层协议有机会提供可达性确认。 |
| NUD 探针 | 不再知道邻居是可到达的,并且正在发送单播邻居请求探测以验证可达性。 |
| NUD _ 失败 | 将邻居设置为不可达。删除邻居时,会将其设置为 NUD 失败状态。 |