Post

Linux neighbour subsystem notes

Linux neighbour subsystem notes

关于neighbour邻居子系统的一些新知(内核版本6.18.2)

  1. 主机如果接收到arp request报文并发送arp reply报文后,这个时候neigh的状态只会更新成stale而不是reachable,当该主机之后发送arp request并收到arp reply之后,才会将neigh状态从stale更新为reachable。 这个现象可以使用arping来验证。

  2. 主机上的reachable状态的neigh在经过base reachable的时间(实际是base reachable * (0.5-1.5之间的随机值))之后,会从reachable变为stale状态。如果在这期间neigh的个数不超过tbl配置的gc_thresh1,则不会触发gc,也就是stale状态的neigh一直会存在而不会删除。这里的gc_thresh1是table级别,对于docker等应用来说,因为netns是基于dev,所以实际上是所有docker共享arp table的。如果需要让stale状态不一直保持,则需要降低gc_thresh1到一个很小的值,比如0。

  3. gc_interval这个参数在最新的内核,或者说很早的内核中就已经不再使用了。目前看2.6.29的内核就已经不使用该参数更新gc的处理周期了,而是使用base_reachable_time/2作为gc的处理周期。具体可以参考neigh_periodic_timer/neigh_periodic_work这个函数最后更新timer的时间。

  4. 当neigh处于stale状态的时候,如果只收到对端的arp request并发送arp reply,这个时候依然保持stale状态而不更新到reachable状态,可以参考上面1中的解释。如果本端有报文需要发送,比如ping包,这个时候会先发送ping报文,直到第一次发送DELAY_PROBE_TIME(默认是5秒)之后,才会触发arp请求报文的发送(这里是单播探测),这可以从wireshark/tcpdump抓包来看出。具体流程可以参考sock_confirm_neigh,neigh_output和neigh_timer_handler。之所以有这种现象,是因为linux的neighbour思想就是尽量减少arp探测报文的数量。同时内核还为raw/udp socket提供了MSG_CONFIRM选项,当应用层确认发送的报文是对接收报文的回复的时候,可以设置该标志,则内核不会触发上述DELAY_PROBE_TIME之后的arp探测报文发送,而是直接从delay状态进入reachable,具体可以参考neigh_timer_handler中对于NUD_DELAY状态的处理。

  5. 对于tcp来说,如果设置了FLAG_FORWARD_PROGRESS标志或者没有设置FLAG_NOT_DUP(上面二者都是TCP自动设置的标志,不需要用户设置)的时候,当收到ack报文的时候,会调用sk_dst_confirm更新sock的confirm状态。这个时候如果有报文发送,且neigh处理stale状态,则也不会触发arp探测流程。具体可以参考函数tcp_ack(tcp_input.c),__tcp_transmit_skb(tcp_output.c)。

  6. NUD_CONNECTED状态包含NUD_PERMANENT,NUD_NOARP,NUD_REACHABLE,__neigh_event_send中只处理非,NUD_CONNECTED ,NUD_DELAY ,NUD_PROBE状态。 NUD_IN_TIMER状态包含NUD_INCOMPLETE,NUD_REACHABLE,NUD_DELAY,NUD_PROBE,neigh_timer_handler中只处理NUD_IN_TIMER状态。综上所述,NUD_PERMANENT,NUD_NOARP状态不会在__neigh_event_send和neigh_timer_handler状态中处理,NUD_REACHABLE会在neigh_timer_handler状态中处理。NUD_DELAY , NUD_PROBE , NUD_INCOMPLETE会在neigh_timer_handler状态中处理,NUD_STALE , NUD_INCOMPLETE会在__neigh_event_send状态中处理。

  7. 当发送报文的时候,只有处于NUD_CONNECTED状态的neigh才会使用hh缓存接口neigh_hh_output发送报文,其他neigh状态通过neigh_resolve_output走慢速流程。 neigh_resolve_output中会首先通过neigh_event_send/neigh_event_send_probe发送探测报文,具体是先更新used时戳,然后再次判断状态不是NUD_CONNECTED , NUD_DELAY, NUD_PROBE,才会进入__neigh_event_send状态。在__neigh_event_send中,即使设置了immediate_ok为TRUE,这个时候如果是stale状态也不会立即探测,具体参考上述4的描述。__neigh_event_send的返回值是0表示状态正常,比如NUD_STALE(这里可能只有NUD_STALE会返回0),返回1表示neigh状态不可用,比如NUD_FAILED,NUD_INCOMPLETE,NUD_NONE,以及已经标记为dead的neigh。

  8. neigh_hh_output缓存发送流程中,使用了一种seqlock顺序锁机制。这种锁具有写的优先级高于读优先级的特点,当neigh_hh_output中从hh中拷贝hh_data的时候,会通过read_seqbegin获取锁标志,然后获取的是hh_data最新数据。如果在获取过程中hh_data有更新,会根据read_seqretry返回的标志再次读取。是的,当通过seqlock访问数据的时候,在读的过程中保护的数据是可以被更新的,所以在结束的时候,需要read_seqretry返回的结果来看是否重读读取。因为对于neigh来说,read的频率远远大于write的频率,所以read之间实际也是没有互斥的,read和write之间也没有互斥,而是通过seqlock来保证及时使用最新的数据。write之间是有锁保护的,具体参考__neigh_update->neigh_update_hhs中的更新流程。

  9. ip -s neigh显示中的used之后的三列的数据分别对应代码中的used/confirmed/updated。used主要是发送方向的时间戳更新,如果有上述7中的慢速转发流程,则used会被更新。当在STALE状态的时候,会后在DELAY_PROBE_TIME时间后触发ARP单播探测。当其他状态会走ARP正常探测。当使用hh缓存的时候,不会更新used时间戳。confirmed时间戳主要是接收方向对于neigh的确认,以及上述MSG_CONFIRM和TCP的间接确认机制。updated是neigh状态更新时间戳,用来表示neigh状态的变化情况。

  10. 对于以太设备来说,因为在ether_setup(eth.c)中都设置了header_ops指向eth_header_ops,而eth_header_ops中cache指向eth_header_cache,所以arp_constructor中的neigh->ops会指向arp_hh_ops。所以neigh->output不论是NUD_VLIAD还是非NUD_VALID,其实output和connected_output都指向了neigh_resolve_output。然后neigh_resolve_output中再根据neigh的状态决定是否发送探测报文。arp_generic_ops为不支持hh缓存的device提供的,arp_direct_ops是为了直接发送device提供的。对于loopback设备来说,因为loopback_setup(loopback.c)中使用eth_header_ops初始化device,所以其header_ops还是有效的,但是因为接口设置了IFF_LOOPBACK标志,所以nud_state状态是NUD_NOARP,output使用的是connected_output(neigh_resolve_output)。

  11. 组播,广播报文都有对应的neigh,需要使用ip neigh show nud all查看,这些neigh的状态都是NOARP。注意在上面4中stale状态的gc流程中,比较的个数是table->entries和gc_thresh1,也就是所有arp entry个数和gc_thresh1,这里的table->entries是包含所有单播,组播和广播entry的。

This post is licensed under CC BY 4.0 by the author.