improve ARP cache logic
The old ARP cache implementation is very simple, and keeps entries of
reachable but unused neighbors forever in the cache. Also it never probes
if entries are still valid before the timeout is reached, which is by
default a very long 20 minutes. Combined with the fact that gratuitous ARP
requests for existing entries are accepted, this makes a simple DoS attack
possible, that only recovers after these 20 minutes.
This is an improved implementation of the ARP cache logic, which handles
dynamic network changes a lot better. It is inspired by the NUD of IPv6,
but a lot simpler. It only introduces two more cache entry states: Stale
and Probing. After the `refresh` interval (default: 1 minute) a Dynamic
entry goes into Stale state. If an entry changes its MAC address because of
a gratuitous ARP request, it goes into Stale immediately. If a Stale entry
is queried, it goes into Probing state, where it sends ARP requests similar
to the Pending state, but as unicasts to the last known MAC address. If the
probing is not successful, the entry is removed and further queries cause a
normal broadcast request, otherwise it is replaced with a fresh Dynamic
entry. If a Stale entry reaches `timeout` age (default 20 minutes), because
it never has been queried, it is removed without further probing.
Compared to the old implentation the default values create on the happy path
only one more unicast ARP request per minute and ARP cache entry.