net: arp: introduce arp_evict_nocarrier sysctl parameter
authorJames Prestwood <prestwoj@gmail.com>
Mon, 1 Nov 2021 17:36:28 +0000 (10:36 -0700)
committerJakub Kicinski <kuba@kernel.org>
Tue, 2 Nov 2021 02:57:14 +0000 (19:57 -0700)
This change introduces a new sysctl parameter, arp_evict_nocarrier.
When set (default) the ARP cache will be cleared on a NOCARRIER event.
This new option has been defaulted to '1' which maintains existing
behavior.

Clearing the ARP cache on NOCARRIER is relatively new, introduced by:

commit 859bd2ef1fc1110a8031b967ee656c53a6260a76
Author: David Ahern <dsahern@gmail.com>
Date:   Thu Oct 11 20:33:49 2018 -0700

    net: Evict neighbor entries on carrier down

The reason for this changes is to prevent the ARP cache from being
cleared when a wireless device roams. Specifically for wireless roams
the ARP cache should not be cleared because the underlying network has not
changed. Clearing the ARP cache in this case can introduce significant
delays sending out packets after a roam.

A user reported such a situation here:

https://lore.kernel.org/linux-wireless/CACsRnHWa47zpx3D1oDq9JYnZWniS8yBwW1h0WAVZ6vrbwL_S0w@mail.gmail.com/

After some investigation it was found that the kernel was holding onto
packets until ARP finished which resulted in this 1 second delay. It
was also found that the first ARP who-has was never responded to,
which is actually what caues the delay. This change is more or less
working around this behavior, but again, there is no reason to clear
the cache on a roam anyways.

As for the unanswered who-has, we know the packet made it OTA since
it was seen while monitoring. Why it never received a response is
unknown. In any case, since this is a problem on the AP side of things
all that can be done is to work around it until it is solved.

Some background on testing/reproducing the packet delay:

Hardware:
 - 2 access points configured for Fast BSS Transition (Though I don't
   see why regular reassociation wouldn't have the same behavior)
 - Wireless station running IWD as supplicant
 - A device on network able to respond to pings (I used one of the APs)

Procedure:
 - Connect to first AP
 - Ping once to establish an ARP entry
 - Start a tcpdump
 - Roam to second AP
 - Wait for operstate UP event, and note the timestamp
 - Start pinging

Results:

Below is the tcpdump after UP. It was recorded the interface went UP at
10:42:01.432875.

10:42:01.461871 ARP, Request who-has 192.168.254.1 tell 192.168.254.71, length 28
10:42:02.497976 ARP, Request who-has 192.168.254.1 tell 192.168.254.71, length 28
10:42:02.507162 ARP, Reply 192.168.254.1 is-at ac:86:74:55:b0:20, length 46
10:42:02.507185 IP 192.168.254.71 > 192.168.254.1: ICMP echo request, id 52792, seq 1, length 64
10:42:02.507205 IP 192.168.254.71 > 192.168.254.1: ICMP echo request, id 52792, seq 2, length 64
10:42:02.507212 IP 192.168.254.71 > 192.168.254.1: ICMP echo request, id 52792, seq 3, length 64
10:42:02.507219 IP 192.168.254.71 > 192.168.254.1: ICMP echo request, id 52792, seq 4, length 64
10:42:02.507225 IP 192.168.254.71 > 192.168.254.1: ICMP echo request, id 52792, seq 5, length 64
10:42:02.507232 IP 192.168.254.71 > 192.168.254.1: ICMP echo request, id 52792, seq 6, length 64
10:42:02.515373 IP 192.168.254.1 > 192.168.254.71: ICMP echo reply, id 52792, seq 1, length 64
10:42:02.521399 IP 192.168.254.1 > 192.168.254.71: ICMP echo reply, id 52792, seq 2, length 64
10:42:02.521612 IP 192.168.254.1 > 192.168.254.71: ICMP echo reply, id 52792, seq 3, length 64
10:42:02.521941 IP 192.168.254.1 > 192.168.254.71: ICMP echo reply, id 52792, seq 4, length 64
10:42:02.522419 IP 192.168.254.1 > 192.168.254.71: ICMP echo reply, id 52792, seq 5, length 64
10:42:02.523085 IP 192.168.254.1 > 192.168.254.71: ICMP echo reply, id 52792, seq 6, length 64

You can see the first ARP who-has went out very quickly after UP, but
was never responded to. Nearly a second later the kernel retries and
gets a response. Only then do the ping packets go out. If an ARP entry
is manually added prior to UP (after the cache is cleared) it is seen
that the first ping is never responded to, so its not only an issue with
ARP but with data packets in general.

As mentioned prior, the wireless interface was also monitored to verify
the ping/ARP packet made it OTA which was observed to be true.

Signed-off-by: James Prestwood <prestwoj@gmail.com>
Reviewed-by: David Ahern <dsahern@kernel.org>
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Documentation/networking/ip-sysctl.rst
include/linux/inetdevice.h
include/uapi/linux/ip.h
include/uapi/linux/sysctl.h
net/ipv4/arp.c
net/ipv4/devinet.c

index 16b8bf72feaf42753121c368530a4f2e864a658f..18fde4ed7a5e2b204a678379ed2f1caf56cbd7ae 100644 (file)
@@ -1611,6 +1611,15 @@ arp_accept - BOOLEAN
        gratuitous arp frame, the arp table will be updated regardless
        if this setting is on or off.
 
+arp_evict_nocarrier - BOOLEAN
+       Clears the ARP cache on NOCARRIER events. This option is important for
+       wireless devices where the ARP cache should not be cleared when roaming
+       between access points on the same network. In most cases this should
+       remain as the default (1).
+
+       - 1 - (default): Clear the ARP cache on NOCARRIER events
+       - 0 - Do not clear ARP cache on NOCARRIER events
+
 mcast_solicit - INTEGER
        The maximum number of multicast probes in INCOMPLETE state,
        when the associated hardware address is unknown.  Defaults
index a038feb63f23edcfe036d8117ffa419632952bf4..518b484a7f07ebfa75cb8e4b41d78c15157f9aac 100644 (file)
@@ -133,6 +133,8 @@ static inline void ipv4_devconf_setall(struct in_device *in_dev)
 #define IN_DEV_ARP_ANNOUNCE(in_dev)    IN_DEV_MAXCONF((in_dev), ARP_ANNOUNCE)
 #define IN_DEV_ARP_IGNORE(in_dev)      IN_DEV_MAXCONF((in_dev), ARP_IGNORE)
 #define IN_DEV_ARP_NOTIFY(in_dev)      IN_DEV_MAXCONF((in_dev), ARP_NOTIFY)
+#define IN_DEV_ARP_EVICT_NOCARRIER(in_dev) IN_DEV_ANDCONF((in_dev), \
+                                                         ARP_EVICT_NOCARRIER)
 
 struct in_ifaddr {
        struct hlist_node       hash;
index e42d13b55cf3acafd90f0ec6d13ebd685ad3f684..e00bbb9c47bb49bb52db91d9744d7825f645bfee 100644 (file)
@@ -169,6 +169,7 @@ enum
        IPV4_DEVCONF_DROP_UNICAST_IN_L2_MULTICAST,
        IPV4_DEVCONF_DROP_GRATUITOUS_ARP,
        IPV4_DEVCONF_BC_FORWARDING,
+       IPV4_DEVCONF_ARP_EVICT_NOCARRIER,
        __IPV4_DEVCONF_MAX
 };
 
index 1e05d3caa712ff9abcb0a1d827e69507808c6228..6a3b194c50fe74bc5a972031bd45f7bec1aa4de4 100644 (file)
@@ -482,6 +482,7 @@ enum
        NET_IPV4_CONF_PROMOTE_SECONDARIES=20,
        NET_IPV4_CONF_ARP_ACCEPT=21,
        NET_IPV4_CONF_ARP_NOTIFY=22,
+       NET_IPV4_CONF_ARP_EVICT_NOCARRIER=23,
 };
 
 /* /proc/sys/net/ipv4/netfilter */
index 922dd73e57406e95f000be2a76fcc8de27bd06d0..857a144b1ea96c1a78874d4e6e4695fe0ff57db7 100644 (file)
@@ -1247,6 +1247,8 @@ static int arp_netdev_event(struct notifier_block *this, unsigned long event,
 {
        struct net_device *dev = netdev_notifier_info_to_dev(ptr);
        struct netdev_notifier_change_info *change_info;
+       struct in_device *in_dev;
+       bool evict_nocarrier;
 
        switch (event) {
        case NETDEV_CHANGEADDR:
@@ -1257,7 +1259,14 @@ static int arp_netdev_event(struct notifier_block *this, unsigned long event,
                change_info = ptr;
                if (change_info->flags_changed & IFF_NOARP)
                        neigh_changeaddr(&arp_tbl, dev);
-               if (!netif_carrier_ok(dev))
+
+               in_dev = __in_dev_get_rtnl(dev);
+               if (!in_dev)
+                       evict_nocarrier = true;
+               else
+                       evict_nocarrier = IN_DEV_ARP_EVICT_NOCARRIER(in_dev);
+
+               if (evict_nocarrier && !netif_carrier_ok(dev))
                        neigh_carrier_down(&arp_tbl, dev);
                break;
        default:
index f4468980b6757dfe9565173adc8f40823f940c45..ec73a0d52d3e3524f36e0c140699cf394696637a 100644 (file)
@@ -75,6 +75,7 @@ static struct ipv4_devconf ipv4_devconf = {
                [IPV4_DEVCONF_SHARED_MEDIA - 1] = 1,
                [IPV4_DEVCONF_IGMPV2_UNSOLICITED_REPORT_INTERVAL - 1] = 10000 /*ms*/,
                [IPV4_DEVCONF_IGMPV3_UNSOLICITED_REPORT_INTERVAL - 1] =  1000 /*ms*/,
+               [IPV4_DEVCONF_ARP_EVICT_NOCARRIER - 1] = 1,
        },
 };
 
@@ -87,6 +88,7 @@ static struct ipv4_devconf ipv4_devconf_dflt = {
                [IPV4_DEVCONF_ACCEPT_SOURCE_ROUTE - 1] = 1,
                [IPV4_DEVCONF_IGMPV2_UNSOLICITED_REPORT_INTERVAL - 1] = 10000 /*ms*/,
                [IPV4_DEVCONF_IGMPV3_UNSOLICITED_REPORT_INTERVAL - 1] =  1000 /*ms*/,
+               [IPV4_DEVCONF_ARP_EVICT_NOCARRIER - 1] = 1,
        },
 };
 
@@ -2532,6 +2534,8 @@ static struct devinet_sysctl_table {
                DEVINET_SYSCTL_RW_ENTRY(ARP_IGNORE, "arp_ignore"),
                DEVINET_SYSCTL_RW_ENTRY(ARP_ACCEPT, "arp_accept"),
                DEVINET_SYSCTL_RW_ENTRY(ARP_NOTIFY, "arp_notify"),
+               DEVINET_SYSCTL_RW_ENTRY(ARP_EVICT_NOCARRIER,
+                                       "arp_evict_nocarrier"),
                DEVINET_SYSCTL_RW_ENTRY(PROXY_ARP_PVLAN, "proxy_arp_pvlan"),
                DEVINET_SYSCTL_RW_ENTRY(FORCE_IGMP_VERSION,
                                        "force_igmp_version"),