深入理解Linux网络技术内幕 第28章 邻居子系统-ARP协议

    技术2022-07-10  110

    邻居子系统-ARP协议

    ARP报文格式无端ARPL2地址变化重复地址探测虚拟IP 多个网络接口应答初始化ARP协议arp_table 初始化neighbour结构接收和发送ARP报文发送arp报文处理arp报文

    ARP报文格式

    报文格式如下所示。

    硬件类型 硬件类型标识协议类型 L3协议标志硬件地址长度 L2地址长度协议地址长度 L3地址长度操作码 ARP操作类型发送方硬件地址和协议源地址指的是发送方的L2地址和L3地址目的地址和协议目的地址指的是目的地L3地址和L3地址

    ARP两个基本类型

    ARPOP_REQUEST将一个L3地址解析为L2地址,用于发出一个solicitation请求。ARPOP_REPLY报文是ARPOP_REQUEST的应答。通常直接发到请求的主机,有时会发送到广播地址,在主机改变配置后会发送到广播地址,更新其他邻居主机的缓存。

    无端ARP

    无端ARP是指发送方发出ARPOP_REQUEST是为了通知接收方一些信息,而不是为了请求信息的报文。常用于下列情况:

    L2地址变化重复地址探测虚拟IP

    L2地址变化

    L2地址的变化应该使网络中其他节点的neighbour失效,改变地址的节点通过无端ARP完成更新。

    重复地址探测

    主机可以使用无端ARP探测重复L3地址是否有重复。如果发出一个目的地地址是自己的ARP请求,如果收到应答说有一台主机IP和本机相同。 在大型网络中允许主机随机发出ARP报文对整个网络性能不利。DHCP服务器在将一个地址授权给某个主机前通常发出请求,检测这个地址是否重复。

    虚拟IP

    无端IP另外一个使用场景是服务器故障迁移。在这种情况下,有一台活跃的主机还有一定数量的主机处于备用模式。当活跃主机宕机后,心跳机制检测到这个故障并启动新的活跃服务器。这个新的服务器会生成一个无端ARP报文更新网络中所有主机的ARP缓存。该ARP不会被应答,但是所有接收者会更新他们的缓存。

    多个网络接口应答

    Linux认为一个IP地址属于一个主机而不是某个接口,因此会有如下现象:

    Linux主机会对目的地址是该主机任何一个接口上IP的ARP请求做应答,而不管是哪个接口收到的(包括环回口)。通过/proc下的ARP_IGNORE可以改变处理方式。如下图所示: Linux主机的两块网卡连接到同一个LAN中并且拥有不同的L3地址,此时如果对其任意一个IP地址发出ARP请求,会收到两个应答。

    初始化ARP协议

    arp_init负责初始化arp协议。这个函数首先调用neigh_table_init向邻居层注册arp协议。dev_add_pack负责安装arp协议。 arp_proc_init协议在proc文件系统中建立arp接口。 register_netdevice_notifier注册接收设备状态和配置变化的通知链。

    void __init arp_init(void) { neigh_table_init(NEIGH_ARP_TABLE, &arp_tbl); dev_add_pack(&arp_packet_type); arp_proc_init(); #ifdef CONFIG_SYSCTL neigh_sysctl_register(NULL, &arp_tbl.parms, NULL); #endif register_netdevice_notifier(&arp_netdev_notifier); }

    arp_table

    arp_tbl包含ARP协议涉及到的关键变量和参数。

    struct neigh_table arp_tbl = { .family = AF_INET, .key_len = 4, .protocol = cpu_to_be16(ETH_P_IP), .hash = arp_hash, .key_eq = arp_key_eq, .constructor = arp_constructor, .proxy_redo = parp_redo, .id = "arp_cache", .parms = { .tbl = &arp_tbl, .reachable_time = 30 * HZ, .data = { [NEIGH_VAR_MCAST_PROBES] = 3, [NEIGH_VAR_UCAST_PROBES] = 3, [NEIGH_VAR_RETRANS_TIME] = 1 * HZ, [NEIGH_VAR_BASE_REACHABLE_TIME] = 30 * HZ, [NEIGH_VAR_DELAY_PROBE_TIME] = 5 * HZ, [NEIGH_VAR_GC_STALETIME] = 60 * HZ, [NEIGH_VAR_QUEUE_LEN_BYTES] = SK_WMEM_MAX, [NEIGH_VAR_PROXY_QLEN] = 64, [NEIGH_VAR_ANYCAST_DELAY] = 1 * HZ, [NEIGH_VAR_PROXY_DELAY] = (8 * HZ) / 10, [NEIGH_VAR_LOCKTIME] = 1 * HZ, }, }, .gc_interval = 30 * HZ, .gc_thresh1 = 128, .gc_thresh2 = 512, .gc_thresh3 = 1024, };

    初始化neighbour结构

    arp_table的arp_constructor成员函数用于对创建neighbour结构做协议相关的初始化。 函数首先判断dev是不是环路和点对点设备,这两种设备不需要L2信息,因此搜索键都设置为INADDR_ANY。 接下来获取in_dev,这个结构保存网络设备IP层配置信息,包括ARP信息。

    static int arp_constructor(struct neighbour *neigh) { __be32 addr; struct net_device *dev = neigh->dev; struct in_device *in_dev; struct neigh_parms *parms; u32 inaddr_any = INADDR_ANY; if (dev->flags & (IFF_LOOPBACK | IFF_POINTOPOINT)) memcpy(neigh->primary_key, &inaddr_any, arp_tbl.key_len); addr = *(__be32 *)neigh->primary_key; rcu_read_lock(); in_dev = __in_dev_get_rcu(dev); if (!in_dev) { rcu_read_unlock(); return -EINVAL; } neigh->type = inet_addr_type_dev_table(dev_net(dev), dev, addr);

    接下来将arp的参数放入neighbour->parms指针。

    parms = in_dev->arp_parms; __neigh_parms_put(neigh->parms); neigh->parms = neigh_parms_clone(parms); rcu_read_unlock();

    下面代码判断netdevice结构的header_ops如果是NULL说明该设备驱动程序没有提供填充L2报文头的函数,也就是说设备不需要L2信息也就不需要ARP功能。直接将nud_state设置为NUD_NOARP,output函数设置neigh_direct_output函数。ops设置为arp_direct_ops结构,arp_direct_ops中所有函数被初始化neigh_direct_output函数。而neigh_direct_output函数只是简单的调用dev_queue_xmit函数将报文发送出去。

    static const struct neigh_ops arp_direct_ops = { .family = AF_INET, .output = neigh_direct_output, .connected_output = neigh_direct_output, }; if(!dev->header_ops) { neigh->nud_state = NUD_NOARP; neigh->ops = &arp_direct_ops; neigh->output = neigh_direct_output; }

    回环设备和设置为NUD_NOARP的设备不需要ARP解析地址,但是邻居层仍然需要一个地址存放L2报文头,这些函数指定与该设备相关的地址。

    if (neigh->type == RTN_MULTICAST) { neigh->nud_state = NUD_NOARP; arp_mc_map(addr, neigh->ha, dev, 1); } else if (dev->flags & (IFF_NOARP | IFF_LOOPBACK)) { neigh->nud_state = NUD_NOARP; memcpy(neigh->ha, dev->dev_addr, dev->addr_len); } else if (neigh->type == RTN_BROADCAST || (dev->flags & IFF_POINTOPOINT)) { neigh->nud_state = NUD_NOARP; memcpy(neigh->ha, dev->broadcast, dev->addr_len); }

    下面内核根据驱动是否支持L2缓存初始化ops字段,如果驱动支持L2缓存就初始化为arp_hh_ops,否则初始化为arp_generic_ops结构。一个驱动是否是支持L2缓存在和设备相关的xxx_setup函数函数中指定。 最后初始化output 函数,这个函数根据nud_state这个值是NUD_VALID就将output设置为connected_output

    if (dev->header_ops->cache) neigh->ops = &arp_hh_ops; else neigh->ops = &arp_generic_ops; if (neigh->nud_state & NUD_VALID) neigh->output = neigh->ops->connected_output; else neigh->output = neigh->ops->output; }

    接收和发送ARP报文

    arp_solicit函数,邻居子系统调用solicit函数指针发送solicitation请求,在arp指针中这个函数指针被初始化为arp_solicit函数。arp_rcv函数被注册到内核中,当有ARP协议报文调用arp_rcv处理。 某些情况下收到一个ARP报文可能导致发出一个ARP报文,这些情况是:配置了网桥,网桥只是转发报文到其他接口。邻居子系统对请求报文做出应答。

    发送arp报文

    参数neigh是L3地址需要被解析的邻居。 skb是触发solicitation请求的报文。

    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; struct dst_entry *dst = NULL; rcu_read_lock(); in_dev = __in_dev_get_rcu(dev); if (!in_dev) { rcu_read_unlock(); return; }

    如果一个主机有多个IP地址,ARP_ANNOUNCE影响源IP地址的选择。 inet_addr_type_dev_table函数用于返回参数中地址的类型。根据ARP_ANNOUNCE选择源地址,如果ARP_ANNOUNCE为0或1,且没有选择到源地址就使用inet_select_addr函数在设备配置信息中查找这个IP地址位于同一子网上的地址。

    switch (IN_DEV_ARP_ANNOUNCE(in_dev)) { default: case 0: /* By default announce any local IP */ if (skb && inet_addr_type_dev_table(dev_net(dev), 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_table(dev_net(dev), dev, saddr) == RTN_LOCAL) { /* 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; } if (!saddr) saddr = inet_select_addr(dev, target, RT_SCOPE_LINK);

    处理arp报文

    arp_rcv函数首先检查是否应该处理这个报文,如果不需要处理跳转到consumeskb,接下来检查arp报文头是否都在第一个skb中,如果不是则从第二个skb中复制一些数据到第一个skb中,便于访问。

    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 */ if (dev->flags & IFF_NOARP || skb->pkt_type == PACKET_OTHERHOST || skb->pkt_type == PACKET_LOOPBACK) goto consumeskb; 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_process函数进行后续处理。

    arp = arp_hdr(skb); 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, dev_net(dev), NULL, skb, dev, NULL, arp_process);

    arp_process函数用来处理arp报文。 首先验证ARP报文头和设备是否使能ARP功能。 arp_process函数只处理ARPOP_REPLY和ARPOP_REQUEST报文类型。

    /* arp_rcv below verifies the ARP header and verifies the device * is ARP'able. */ if (!in_dev) goto out_free_skb; arp = arp_hdr(skb); switch (dev_type) { default: if (arp->ar_pro != htons(ETH_P_IP) || htons(dev_type) != arp->ar_hrd) goto out_free_skb; break; case ARPHRD_ETHER: case ARPHRD_FDDI: case ARPHRD_IEEE802: /* * ETHERNET, and Fibre Channel (which are IEEE 802 * devices, according to RFC 2625) devices will accept ARP * hardware types of either 1 (Ethernet) or 6 (IEEE 802.2). * This is the case also of FDDI, where the RFC 1390 says that * FDDI devices should accept ARP hardware of (1) Ethernet, * however, to be more robust, we'll accept both 1 (Ethernet) * or 6 (IEEE 802.2) */ if ((arp->ar_hrd != htons(ARPHRD_ETHER) && arp->ar_hrd != htons(ARPHRD_IEEE802)) || arp->ar_pro != htons(ETH_P_IP)) goto out_free_skb; break; case ARPHRD_AX25: if (arp->ar_pro != htons(AX25_P_IP) || arp->ar_hrd != htons(ARPHRD_AX25)) goto out_free_skb; break; case ARPHRD_NETROM: if (arp->ar_pro != htons(AX25_P_IP) || arp->ar_hrd != htons(ARPHRD_NETROM)) goto out_free_skb; break; } /* Understand only these message types */ if (arp->ar_op != htons(ARPOP_REPLY) && arp->ar_op != htons(ARPOP_REQUEST)) goto out_free_skb;

    如果请求地址是环路或者地址是多播就退出,不需要做处理。

    /* * 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_free_skb;
    Processed: 0.050, SQL: 9