Skip to content

Smdn.Net.AddressResolution version 1.0.0-preview4

Pre-release
Pre-release
Compare
Choose a tag to compare
@smdn smdn released this 14 Mar 16:25
· 199 commits to main since this release
ff17241

Released package

Release notes

The full release notes are available at gist.

Change log

Change log in this release:

API changes

API changes in this release:
diff --git a/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-net6.0.apilist.cs b/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-net6.0.apilist.cs
index 0fcaecd..8de9632 100644
--- a/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-net6.0.apilist.cs
+++ b/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-net6.0.apilist.cs
@@ -1,77 +1,88 @@
-// Smdn.Net.AddressResolution.dll (Smdn.Net.AddressResolution-1.0.0-preview3)
+// Smdn.Net.AddressResolution.dll (Smdn.Net.AddressResolution-1.0.0-preview4)
 //   Name: Smdn.Net.AddressResolution
 //   AssemblyVersion: 1.0.0.0
-//   InformationalVersion: 1.0.0-preview3+b0d0efec19a7bb78ed612cabd93d8e805a1a32f8
+//   InformationalVersion: 1.0.0-preview4+f17248a683dc95a5f8a1d3f3ec79fb49b8b2852f
 //   TargetFramework: .NETCoreApp,Version=v6.0
 //   Configuration: Release
 //   Referenced assemblies:
 //     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+//     System.Collections.Concurrent, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.ComponentModel, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Diagnostics.Process, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Linq, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Memory, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 //     System.Net.NetworkInformation, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Net.Primitives, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Runtime, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Runtime.InteropServices.RuntimeInformation, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 #nullable enable annotations
 
 using System;
 using System.Net;
 using System.Net.NetworkInformation;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 using Smdn.Net.AddressResolution;
 
 namespace Smdn.Net {
   public static class PhysicalAddressExtensions {
     public static string ToMacAddressString(this PhysicalAddress hardwareAddress, char delimiter = ':') {}
   }
 }
 
 namespace Smdn.Net.AddressResolution {
   public interface IAddressResolver<TAddress, TResolvedAddress> {
+    void Invalidate(TResolvedAddress resolvedAddress);
     ValueTask<TResolvedAddress> ResolveAsync(TAddress address, CancellationToken cancellationToken);
   }
 
   public abstract class MacAddressResolver :
     IAddressResolver<IPAddress, PhysicalAddress>,
     IAddressResolver<PhysicalAddress, IPAddress>,
     IDisposable
   {
     protected static readonly PhysicalAddress AllZeroMacAddress; // = "000000000000"
 
     public static MacAddressResolver Null { get; }
 
     public static MacAddressResolver Create(MacAddressResolverOptions? options = null, IServiceProvider? serviceProvider = null) {}
 
     protected MacAddressResolver(ILogger? logger = null) {}
 
+    public abstract bool HasInvalidated { get; }
     protected ILogger? Logger { get; }
 
     protected virtual void Dispose(bool disposing) {}
     public void Dispose() {}
+    public void Invalidate(IPAddress resolvedIPAddress) {}
+    public void Invalidate(PhysicalAddress resolvedMacAddress) {}
+    protected abstract void InvalidateCore(IPAddress resolvedIPAddress);
+    protected abstract void InvalidateCore(PhysicalAddress resolvedMacAddress);
     public ValueTask RefreshCacheAsync(CancellationToken cancellationToken = default) {}
     protected virtual ValueTask RefreshCacheAsyncCore(CancellationToken cancellationToken) {}
+    public ValueTask RefreshInvalidatedCacheAsync(CancellationToken cancellationToken = default) {}
+    protected virtual ValueTask RefreshInvalidatedCacheAsyncCore(CancellationToken cancellationToken) {}
     public ValueTask<PhysicalAddress?> ResolveIPAddressToMacAddressAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) {}
     protected abstract ValueTask<PhysicalAddress?> ResolveIPAddressToMacAddressAsyncCore(IPAddress ipAddress, CancellationToken cancellationToken);
     public ValueTask<IPAddress?> ResolveMacAddressToIPAddressAsync(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
     protected abstract ValueTask<IPAddress?> ResolveMacAddressToIPAddressAsyncCore(PhysicalAddress macAddress, CancellationToken cancellationToken);
+    void IAddressResolver<IPAddress, PhysicalAddress>.Invalidate(PhysicalAddress resolvedAddress) {}
     ValueTask<PhysicalAddress?> IAddressResolver<IPAddress, PhysicalAddress>.ResolveAsync(IPAddress address, CancellationToken cancellationToken) {}
+    void IAddressResolver<PhysicalAddress, IPAddress>.Invalidate(IPAddress resolvedAddress) {}
     ValueTask<IPAddress?> IAddressResolver<PhysicalAddress, IPAddress>.ResolveAsync(PhysicalAddress address, CancellationToken cancellationToken) {}
     protected void ThrowIfDisposed() {}
   }
 
   public sealed class MacAddressResolverOptions {
     public static readonly MacAddressResolverOptions Default; // = "Smdn.Net.AddressResolution.MacAddressResolverOptions"
 
     public MacAddressResolverOptions() {}
 
     public string? NmapTargetSpecification { get; init; }
-    public TimeSpan ProcfsArpScanInterval { get; init; }
+    public TimeSpan ProcfsArpFullScanInterval { get; init; }
   }
 }
 // API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.2.1.0.
 // Smdn.Reflection.ReverseGenerating.ListApi.Core v1.2.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-netstandard2.0.apilist.cs b/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-netstandard2.0.apilist.cs
index 44fe7a4..f9d6a20 100644
--- a/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-netstandard2.0.apilist.cs
+++ b/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-netstandard2.0.apilist.cs
@@ -1,72 +1,82 @@
-// Smdn.Net.AddressResolution.dll (Smdn.Net.AddressResolution-1.0.0-preview3)
+// Smdn.Net.AddressResolution.dll (Smdn.Net.AddressResolution-1.0.0-preview4)
 //   Name: Smdn.Net.AddressResolution
 //   AssemblyVersion: 1.0.0.0
-//   InformationalVersion: 1.0.0-preview3+b0d0efec19a7bb78ed612cabd93d8e805a1a32f8
+//   InformationalVersion: 1.0.0-preview4+f17248a683dc95a5f8a1d3f3ec79fb49b8b2852f
 //   TargetFramework: .NETStandard,Version=v2.0
 //   Configuration: Release
 //   Referenced assemblies:
 //     Microsoft.Bcl.AsyncInterfaces, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 //     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 //     netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 #nullable enable annotations
 
 using System;
 using System.Net;
 using System.Net.NetworkInformation;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 using Smdn.Net.AddressResolution;
 
 namespace Smdn.Net {
   public static class PhysicalAddressExtensions {
     public static string ToMacAddressString(this PhysicalAddress hardwareAddress, char delimiter = ':') {}
   }
 }
 
 namespace Smdn.Net.AddressResolution {
   public interface IAddressResolver<TAddress, TResolvedAddress> {
+    void Invalidate(TResolvedAddress resolvedAddress);
     ValueTask<TResolvedAddress> ResolveAsync(TAddress address, CancellationToken cancellationToken);
   }
 
   public abstract class MacAddressResolver :
     IAddressResolver<IPAddress, PhysicalAddress>,
     IAddressResolver<PhysicalAddress, IPAddress>,
     IDisposable
   {
     protected static readonly PhysicalAddress AllZeroMacAddress; // = "000000000000"
 
     public static MacAddressResolver Null { get; }
 
     public static MacAddressResolver Create(MacAddressResolverOptions? options = null, IServiceProvider? serviceProvider = null) {}
 
     protected MacAddressResolver(ILogger? logger = null) {}
 
+    public abstract bool HasInvalidated { get; }
     protected ILogger? Logger { get; }
 
     protected virtual void Dispose(bool disposing) {}
     public void Dispose() {}
+    public void Invalidate(IPAddress resolvedIPAddress) {}
+    public void Invalidate(PhysicalAddress resolvedMacAddress) {}
+    protected abstract void InvalidateCore(IPAddress resolvedIPAddress);
+    protected abstract void InvalidateCore(PhysicalAddress resolvedMacAddress);
     public ValueTask RefreshCacheAsync(CancellationToken cancellationToken = default) {}
     protected virtual ValueTask RefreshCacheAsyncCore(CancellationToken cancellationToken) {}
+    public ValueTask RefreshInvalidatedCacheAsync(CancellationToken cancellationToken = default) {}
+    protected virtual ValueTask RefreshInvalidatedCacheAsyncCore(CancellationToken cancellationToken) {}
     public ValueTask<PhysicalAddress?> ResolveIPAddressToMacAddressAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) {}
     protected abstract ValueTask<PhysicalAddress?> ResolveIPAddressToMacAddressAsyncCore(IPAddress ipAddress, CancellationToken cancellationToken);
     public ValueTask<IPAddress?> ResolveMacAddressToIPAddressAsync(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
     protected abstract ValueTask<IPAddress?> ResolveMacAddressToIPAddressAsyncCore(PhysicalAddress macAddress, CancellationToken cancellationToken);
+    void IAddressResolver<IPAddress, PhysicalAddress>.Invalidate(PhysicalAddress resolvedAddress) {}
     ValueTask<PhysicalAddress?> IAddressResolver<IPAddress, PhysicalAddress>.ResolveAsync(IPAddress address, CancellationToken cancellationToken) {}
+    void IAddressResolver<PhysicalAddress, IPAddress>.Invalidate(IPAddress resolvedAddress) {}
     ValueTask<IPAddress?> IAddressResolver<PhysicalAddress, IPAddress>.ResolveAsync(PhysicalAddress address, CancellationToken cancellationToken) {}
     protected void ThrowIfDisposed() {}
   }
 
   public sealed class MacAddressResolverOptions {
     public static readonly MacAddressResolverOptions Default; // = "Smdn.Net.AddressResolution.MacAddressResolverOptions"
 
     public MacAddressResolverOptions() {}
 
     public string? NmapTargetSpecification { get; init; }
-    public TimeSpan ProcfsArpScanInterval { get; init; }
+    public TimeSpan ProcfsArpFullScanInterval { get; init; }
   }
 }
 // API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.2.1.0.
 // Smdn.Reflection.ReverseGenerating.ListApi.Core v1.2.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-netstandard2.1.apilist.cs b/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-netstandard2.1.apilist.cs
index 7ac71b4..565bdda 100644
--- a/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-netstandard2.1.apilist.cs
+++ b/doc/api-list/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution-netstandard2.1.apilist.cs
@@ -1,70 +1,80 @@
-// Smdn.Net.AddressResolution.dll (Smdn.Net.AddressResolution-1.0.0-preview3)
+// Smdn.Net.AddressResolution.dll (Smdn.Net.AddressResolution-1.0.0-preview4)
 //   Name: Smdn.Net.AddressResolution
 //   AssemblyVersion: 1.0.0.0
-//   InformationalVersion: 1.0.0-preview3+b0d0efec19a7bb78ed612cabd93d8e805a1a32f8
+//   InformationalVersion: 1.0.0-preview4+f17248a683dc95a5f8a1d3f3ec79fb49b8b2852f
 //   TargetFramework: .NETStandard,Version=v2.1
 //   Configuration: Release
 //   Referenced assemblies:
 //     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 #nullable enable annotations
 
 using System;
 using System.Net;
 using System.Net.NetworkInformation;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 using Smdn.Net.AddressResolution;
 
 namespace Smdn.Net {
   public static class PhysicalAddressExtensions {
     public static string ToMacAddressString(this PhysicalAddress hardwareAddress, char delimiter = ':') {}
   }
 }
 
 namespace Smdn.Net.AddressResolution {
   public interface IAddressResolver<TAddress, TResolvedAddress> {
+    void Invalidate(TResolvedAddress resolvedAddress);
     ValueTask<TResolvedAddress> ResolveAsync(TAddress address, CancellationToken cancellationToken);
   }
 
   public abstract class MacAddressResolver :
     IAddressResolver<IPAddress, PhysicalAddress>,
     IAddressResolver<PhysicalAddress, IPAddress>,
     IDisposable
   {
     protected static readonly PhysicalAddress AllZeroMacAddress; // = "000000000000"
 
     public static MacAddressResolver Null { get; }
 
     public static MacAddressResolver Create(MacAddressResolverOptions? options = null, IServiceProvider? serviceProvider = null) {}
 
     protected MacAddressResolver(ILogger? logger = null) {}
 
+    public abstract bool HasInvalidated { get; }
     protected ILogger? Logger { get; }
 
     protected virtual void Dispose(bool disposing) {}
     public void Dispose() {}
+    public void Invalidate(IPAddress resolvedIPAddress) {}
+    public void Invalidate(PhysicalAddress resolvedMacAddress) {}
+    protected abstract void InvalidateCore(IPAddress resolvedIPAddress);
+    protected abstract void InvalidateCore(PhysicalAddress resolvedMacAddress);
     public ValueTask RefreshCacheAsync(CancellationToken cancellationToken = default) {}
     protected virtual ValueTask RefreshCacheAsyncCore(CancellationToken cancellationToken) {}
+    public ValueTask RefreshInvalidatedCacheAsync(CancellationToken cancellationToken = default) {}
+    protected virtual ValueTask RefreshInvalidatedCacheAsyncCore(CancellationToken cancellationToken) {}
     public ValueTask<PhysicalAddress?> ResolveIPAddressToMacAddressAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) {}
     protected abstract ValueTask<PhysicalAddress?> ResolveIPAddressToMacAddressAsyncCore(IPAddress ipAddress, CancellationToken cancellationToken);
     public ValueTask<IPAddress?> ResolveMacAddressToIPAddressAsync(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
     protected abstract ValueTask<IPAddress?> ResolveMacAddressToIPAddressAsyncCore(PhysicalAddress macAddress, CancellationToken cancellationToken);
+    void IAddressResolver<IPAddress, PhysicalAddress>.Invalidate(PhysicalAddress resolvedAddress) {}
     ValueTask<PhysicalAddress?> IAddressResolver<IPAddress, PhysicalAddress>.ResolveAsync(IPAddress address, CancellationToken cancellationToken) {}
+    void IAddressResolver<PhysicalAddress, IPAddress>.Invalidate(IPAddress resolvedAddress) {}
     ValueTask<IPAddress?> IAddressResolver<PhysicalAddress, IPAddress>.ResolveAsync(PhysicalAddress address, CancellationToken cancellationToken) {}
     protected void ThrowIfDisposed() {}
   }
 
   public sealed class MacAddressResolverOptions {
     public static readonly MacAddressResolverOptions Default; // = "Smdn.Net.AddressResolution.MacAddressResolverOptions"
 
     public MacAddressResolverOptions() {}
 
     public string? NmapTargetSpecification { get; init; }
-    public TimeSpan ProcfsArpScanInterval { get; init; }
+    public TimeSpan ProcfsArpFullScanInterval { get; init; }
   }
 }
 // API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.2.1.0.
 // Smdn.Reflection.ReverseGenerating.ListApi.Core v1.2.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)

Full changes

Full changes in this release:
diff --git a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.Arp/ProcfsArpMacAddressResolver.cs b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.Arp/ProcfsArpMacAddressResolver.cs
index b5e05af..5693ea8 100644
--- a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.Arp/ProcfsArpMacAddressResolver.cs
+++ b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.Arp/ProcfsArpMacAddressResolver.cs
@@ -1,6 +1,8 @@
 // SPDX-FileCopyrightText: 2022 smdn <smdn@smdn.jp>
 // SPDX-License-Identifier: MIT
 using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
 using System.IO;
 using System.Net;
 using System.Net.NetworkInformation;
@@ -34,13 +36,31 @@ internal partial class ProcfsArpMacAddressResolver : MacAddressResolver {
     );
   }
 
+  private readonly struct None { }
+
+  private class ConcurrentSet<T> : ConcurrentDictionary<T, None>
+    where T : notnull
+  {
+    public ConcurrentSet()
+    {
+    }
+
+    public void Add(T key)
+      => AddOrUpdate(key: key, addValue: default, updateValueFactory: static (key, old) => default);
+  }
+
   /*
    * instance members
    */
-  private DateTime lastArpScanAt = DateTime.MinValue;
-  private readonly TimeSpan arpScanInterval;
+  private DateTime lastArpFullScanAt = DateTime.MinValue;
+  private readonly TimeSpan arpFullScanInterval;
+
+  private bool HasArpFullScanIntervalElapsed => lastArpFullScanAt + arpFullScanInterval <= DateTime.Now;
 
-  private bool HasArpScanIntervalElapsed => lastArpScanAt + arpScanInterval <= DateTime.Now;
+  private readonly ConcurrentSet<IPAddress> invalidatedIPAddressSet = new();
+  private readonly ConcurrentSet<PhysicalAddress> invalidatedMacAddressSet = new();
+
+  public override bool HasInvalidated => !(invalidatedIPAddressSet.IsEmpty && invalidatedMacAddressSet.IsEmpty);
 
   public ProcfsArpMacAddressResolver(
     MacAddressResolverOptions options,
@@ -48,7 +68,7 @@ internal partial class ProcfsArpMacAddressResolver : MacAddressResolver {
   )
     : base(logger)
   {
-    arpScanInterval = options.ProcfsArpScanInterval;
+    arpFullScanInterval = options.ProcfsArpFullScanInterval;
   }
 
   protected override async ValueTask<PhysicalAddress?> ResolveIPAddressToMacAddressAsyncCore(
@@ -56,8 +76,8 @@ internal partial class ProcfsArpMacAddressResolver : MacAddressResolver {
     CancellationToken cancellationToken
   )
   {
-    if (HasArpScanIntervalElapsed)
-      await ArpScanAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+    if (HasArpFullScanIntervalElapsed)
+      await ArpFullScanAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
 
     ArpTableEntry priorCandidate = default;
     ArpTableEntry candidate = default;
@@ -67,6 +87,9 @@ internal partial class ProcfsArpMacAddressResolver : MacAddressResolver {
       Logger,
       cancellationToken
     ).ConfigureAwait(false)) {
+      if (invalidatedMacAddressSet.ContainsKey(entry.HardwareAddress!))
+        continue; // ignore the entry that is marked as invalidated
+
       if (entry.IsPermanentOrComplete) {
         // prefer permanent or complete entry
         priorCandidate = entry;
@@ -88,8 +111,8 @@ internal partial class ProcfsArpMacAddressResolver : MacAddressResolver {
     CancellationToken cancellationToken
   )
   {
-    if (HasArpScanIntervalElapsed)
-      await ArpScanAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+    if (HasArpFullScanIntervalElapsed)
+      await ArpFullScanAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
 
     ArpTableEntry priorCandidate = default;
     ArpTableEntry candidate = default;
@@ -99,6 +122,9 @@ internal partial class ProcfsArpMacAddressResolver : MacAddressResolver {
       Logger,
       cancellationToken
     ).ConfigureAwait(false)) {
+      if (invalidatedIPAddressSet.ContainsKey(entry.IPAddress!))
+        continue; // ignore the entry that is marked as invalidated
+
       if (entry.IsPermanentOrComplete) {
         // prefer permanent or complete entry
         priorCandidate = entry;
@@ -115,6 +141,12 @@ internal partial class ProcfsArpMacAddressResolver : MacAddressResolver {
       : priorCandidate.IPAddress;
   }
 
+  protected override void InvalidateCore(IPAddress resolvedIPAddress)
+    => invalidatedIPAddressSet.Add(resolvedIPAddress);
+
+  protected override void InvalidateCore(PhysicalAddress resolvedMacAddress)
+    => invalidatedMacAddressSet.Add(resolvedMacAddress);
+
   protected override ValueTask RefreshCacheAsyncCore(
     CancellationToken cancellationToken = default
   )
@@ -124,21 +156,69 @@ internal partial class ProcfsArpMacAddressResolver : MacAddressResolver {
         ValueTask.FromCanceled(cancellationToken)
 #else
         ValueTaskShim.FromCanceled(cancellationToken)
+#endif
+      : ArpFullScanAsync(cancellationToken: cancellationToken);
+
+  private async ValueTask ArpFullScanAsync(CancellationToken cancellationToken)
+  {
+    Logger?.LogDebug("Performing ARP full scan");
+
+    await ArpFullScanAsyncCore(cancellationToken: cancellationToken).ConfigureAwait(false);
+
+    invalidatedIPAddressSet.Clear();
+    invalidatedMacAddressSet.Clear();
+
+    lastArpFullScanAt = DateTime.Now;
+  }
+
+  protected virtual ValueTask ArpFullScanAsyncCore(CancellationToken cancellationToken)
+  {
+    Logger?.LogWarning("ARP scan is not supported in this class.");
+
+    return default;
+  }
+
+  protected override ValueTask RefreshInvalidatedCacheAsyncCore(
+    CancellationToken cancellationToken = default
+  )
+    => cancellationToken.IsCancellationRequested
+      ?
+#if SYSTEM_THREADING_TASKS_VALUETASK_FROMCANCELED
+        ValueTask.FromCanceled(cancellationToken)
+#else
+        ValueTaskShim.FromCanceled(cancellationToken)
 #endif
       : ArpScanAsync(cancellationToken: cancellationToken);
 
   private async ValueTask ArpScanAsync(CancellationToken cancellationToken)
   {
-    Logger?.LogDebug("[/proc/net/arp] Performing ARP scan");
+    Logger?.LogDebug("Performing ARP scan for invalidated targets.");
+
+    var invalidatedIPAddresses = invalidatedIPAddressSet.Keys;
+    var invalidatedMacAddresses = invalidatedMacAddressSet.Keys;
 
-    await ArpScanAsyncCore(cancellationToken: cancellationToken).ConfigureAwait(false);
+    Logger?.LogTrace("Invalidated IP addresses: {InvalidatedIPAddresses}", string.Join(" ", invalidatedIPAddresses));
+    Logger?.LogTrace("Invalidated MAC addresses: {InvalidatedMACAddresses}", string.Join(" ", invalidatedMacAddresses));
 
-    lastArpScanAt = DateTime.Now;
+    await ArpScanAsyncCore(
+      invalidatedIPAddresses: invalidatedIPAddresses,
+      invalidatedMacAddresses: invalidatedMacAddresses,
+      cancellationToken: cancellationToken
+    ).ConfigureAwait(false);
+
+    invalidatedIPAddressSet.Clear();
+    invalidatedMacAddressSet.Clear();
+
+    lastArpFullScanAt = DateTime.Now;
   }
 
-  protected virtual ValueTask ArpScanAsyncCore(CancellationToken cancellationToken)
+  protected virtual ValueTask ArpScanAsyncCore(
+    IEnumerable<IPAddress> invalidatedIPAddresses,
+    IEnumerable<PhysicalAddress> invalidatedMacAddresses,
+    CancellationToken cancellationToken
+  )
   {
-    Logger?.LogWarning($"[{nameof(ProcfsArpMacAddressResolver)}] ARP scan is not supported in this class.");
+    Logger?.LogWarning("ARP scan is not supported in this class.");
 
     return default;
   }
diff --git a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.Arp/ProcfsArpNmapScanMacAddressResolver.cs b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.Arp/ProcfsArpNmapScanMacAddressResolver.cs
index 5418064..983f739 100644
--- a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.Arp/ProcfsArpNmapScanMacAddressResolver.cs
+++ b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.Arp/ProcfsArpNmapScanMacAddressResolver.cs
@@ -5,6 +5,8 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
@@ -40,7 +42,45 @@ internal sealed class ProcfsArpNmapScanMacAddressResolver : ProcfsArpMacAddressR
       ?? throw new ArgumentException($"{nameof(options.NmapTargetSpecification)} must be specified with {nameof(MacAddressResolverOptions)}");
   }
 
-  protected override async ValueTask ArpScanAsyncCore(CancellationToken cancellationToken)
+  protected override ValueTask ArpFullScanAsyncCore(CancellationToken cancellationToken)
+    => NmapScanAsync(
+      nmapOptionTargetSpecification: nmapTargetSpecification,
+      logger: Logger,
+      cancellationToken: cancellationToken
+    );
+
+  protected override ValueTask ArpScanAsyncCore(
+    IEnumerable<IPAddress> invalidatedIPAddresses,
+    IEnumerable<PhysicalAddress> invalidatedMacAddresses,
+    CancellationToken cancellationToken
+  )
+  {
+    if (invalidatedMacAddresses.Any()) {
+      // perform full scan
+      return NmapScanAsync(
+        nmapOptionTargetSpecification: nmapTargetSpecification,
+        logger: Logger,
+        cancellationToken: cancellationToken
+      );
+    }
+
+    // perform scan for specific target IPs
+    var nmapOptionTargetSpecification = string.Join(" ", invalidatedIPAddresses);
+
+    return nmapOptionTargetSpecification.Length == 0
+      ? default // do nothing
+      : NmapScanAsync(
+          nmapOptionTargetSpecification: nmapOptionTargetSpecification,
+          logger: Logger,
+          cancellationToken: cancellationToken
+        );
+  }
+
+  private static async ValueTask NmapScanAsync(
+    string nmapOptionTargetSpecification,
+    ILogger? logger,
+    CancellationToken cancellationToken
+  )
   {
     // -sn: Ping Scan - disable port scan
     // -n: Never do DNS resolution
@@ -51,13 +91,13 @@ internal sealed class ProcfsArpNmapScanMacAddressResolver : ProcfsArpMacAddressR
 
     var nmapProcessStartInfo = new ProcessStartInfo() {
       FileName = lazyPathToNmap.Value,
-      Arguments = nmapOptions + nmapTargetSpecification,
+      Arguments = nmapOptions + nmapOptionTargetSpecification,
       RedirectStandardOutput = true,
       RedirectStandardError = true,
       UseShellExecute = false,
     };
 
-    Logger?.LogDebug(
+    logger?.LogDebug(
       "[nmap] {ProcessStartInfoFileName} {ProcessStartInfoArguments}",
       nmapProcessStartInfo.FileName,
       nmapProcessStartInfo.Arguments
@@ -76,7 +116,7 @@ internal sealed class ProcfsArpNmapScanMacAddressResolver : ProcfsArpMacAddressR
       nmapProcess.WaitForExit(); // TODO: cacellation
 #endif
 
-      if (Logger is not null) {
+      if (logger is not null) {
         const LogLevel logLevelForStandardOutput = LogLevel.Trace;
         const LogLevel logLevelForStandardError = LogLevel.Error;
 
@@ -87,7 +127,7 @@ internal sealed class ProcfsArpNmapScanMacAddressResolver : ProcfsArpMacAddressR
         }
 
         foreach (var (stdio, logLevel) in EnumerateLogTarget(nmapProcess.StandardOutput, nmapProcess.StandardError)) {
-          if (!Logger.IsEnabled(logLevel))
+          if (!logger.IsEnabled(logLevel))
             continue;
 
           for (; ;) {
@@ -96,13 +136,13 @@ internal sealed class ProcfsArpNmapScanMacAddressResolver : ProcfsArpMacAddressR
             if (line is null)
               break;
 
-            Logger.Log(logLevel, "[nmap] {Line}", line);
+            logger.Log(logLevel, "[nmap] {Line}", line);
           }
         }
       }
     }
     catch (Exception ex) {
-      Logger?.LogError(ex, "[nmap] failed to perform ARP scanning");
+      logger?.LogError(ex, "[nmap] failed to perform ARP scanning");
     }
   }
 }
diff --git a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.csproj b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.csproj
index afb36d6..c23de68 100644
--- a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.csproj
+++ b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution.csproj
@@ -6,7 +6,7 @@ SPDX-License-Identifier: MIT
   <PropertyGroup>
     <TargetFrameworks>netstandard2.0;netstandard2.1;net6.0</TargetFrameworks>
     <VersionPrefix>1.0.0</VersionPrefix>
-    <VersionSuffix>preview3</VersionSuffix>
+    <VersionSuffix>preview4</VersionSuffix>
     <!-- <PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion> -->
     <Nullable>enable</Nullable>
     <NoWarn>CA1848</NoWarn> <!-- use the LoggerMessage delegates instead -->
diff --git a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/IAddressResolver.cs b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/IAddressResolver.cs
index 51cb4c3..b172e3d 100644
--- a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/IAddressResolver.cs
+++ b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/IAddressResolver.cs
@@ -8,4 +8,5 @@ namespace Smdn.Net.AddressResolution;
 public interface IAddressResolver<TAddress, TResolvedAddress> {
   /// <returns>An resolved address. <see langword="null"/> if address could not be resolved.</returns>
   public ValueTask<TResolvedAddress?> ResolveAsync(TAddress address, CancellationToken cancellationToken);
+  public void Invalidate(TResolvedAddress resolvedAddress);
 }
diff --git a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/MacAddressResolver.cs b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/MacAddressResolver.cs
index 33241a9..b9b01c5 100644
--- a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/MacAddressResolver.cs
+++ b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/MacAddressResolver.cs
@@ -38,6 +38,7 @@ public abstract class MacAddressResolver :
   /*
    * instance members
    */
+  public abstract bool HasInvalidated { get; }
   protected ILogger? Logger { get; }
 
   protected MacAddressResolver(
@@ -81,6 +82,11 @@ public abstract class MacAddressResolver :
       cancellationToken: cancellationToken
     );
 
+  void IAddressResolver<IPAddress, PhysicalAddress>.Invalidate(
+    PhysicalAddress resolvedAddress
+  )
+    => Invalidate(resolvedMacAddress: resolvedAddress);
+
 #pragma warning disable SA1305
   public ValueTask<PhysicalAddress?> ResolveIPAddressToMacAddressAsync(
     IPAddress ipAddress,
@@ -100,8 +106,6 @@ public abstract class MacAddressResolver :
     if (ipAddress is null)
       throw new ArgumentNullException(nameof(ipAddress));
 
-    // TODO: validate IP address
-
     return ResolveAsync();
 
     async ValueTask<PhysicalAddress?> ResolveAsync()
@@ -127,6 +131,20 @@ public abstract class MacAddressResolver :
   );
 #pragma warning restore SA1305
 
+  public void Invalidate(PhysicalAddress resolvedMacAddress)
+  {
+    if (resolvedMacAddress is null)
+      throw new ArgumentNullException(nameof(resolvedMacAddress));
+
+    ThrowIfDisposed();
+
+    Logger?.LogDebug("Invalidating {MacAddress}", resolvedMacAddress.ToMacAddressString());
+
+    InvalidateCore(resolvedMacAddress);
+  }
+
+  protected abstract void InvalidateCore(PhysicalAddress resolvedMacAddress);
+
   /*
    * PhysicalAddress -> IPAddress
    */
@@ -139,6 +157,11 @@ public abstract class MacAddressResolver :
       cancellationToken: cancellationToken
     );
 
+  void IAddressResolver<PhysicalAddress, IPAddress>.Invalidate(
+    IPAddress resolvedAddress
+  )
+    => Invalidate(resolvedIPAddress: resolvedAddress);
+
   public ValueTask<IPAddress?> ResolveMacAddressToIPAddressAsync(
     PhysicalAddress macAddress,
     CancellationToken cancellationToken = default
@@ -163,8 +186,6 @@ public abstract class MacAddressResolver :
       return ValueTaskShim.FromResult<IPAddress?>(null);
 #endif
 
-    // TODO: validate MAC address
-
     return ResolveAsync();
 
     async ValueTask<IPAddress?> ResolveAsync()
@@ -188,6 +209,20 @@ public abstract class MacAddressResolver :
     CancellationToken cancellationToken
   );
 
+  public void Invalidate(IPAddress resolvedIPAddress)
+  {
+    if (resolvedIPAddress is null)
+      throw new ArgumentNullException(nameof(resolvedIPAddress));
+
+    ThrowIfDisposed();
+
+    Logger?.LogDebug("Invalidating {IPAddress}", resolvedIPAddress);
+
+    InvalidateCore(resolvedIPAddress);
+  }
+
+  protected abstract void InvalidateCore(IPAddress resolvedIPAddress);
+
   /*
    * other virtual/abstract members
    */
@@ -217,4 +252,31 @@ public abstract class MacAddressResolver :
 #else
       default;
 #endif
+
+  public ValueTask RefreshInvalidatedCacheAsync(
+    CancellationToken cancellationToken = default
+  )
+  {
+    if (cancellationToken.IsCancellationRequested)
+#if SYSTEM_THREADING_TASKS_VALUETASK_FROMCANCELED
+      return ValueTask.FromCanceled(cancellationToken);
+#else
+      return ValueTaskShim.FromCanceled(cancellationToken);
+#endif
+
+    ThrowIfDisposed();
+
+    return RefreshInvalidatedCacheAsyncCore(cancellationToken);
+  }
+
+  protected virtual ValueTask RefreshInvalidatedCacheAsyncCore(
+    CancellationToken cancellationToken
+  )
+    =>
+      // do nothing in this class
+#if SYSTEM_THREADING_TASKS_VALUETASK_COMPLETEDTASK
+      ValueTask.CompletedTask;
+#else
+      default;
+#endif
 }
diff --git a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/MacAddressResolverOptions.cs b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/MacAddressResolverOptions.cs
index a4f2c46..9525810 100644
--- a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/MacAddressResolverOptions.cs
+++ b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/MacAddressResolverOptions.cs
@@ -12,5 +12,5 @@ public sealed class MacAddressResolverOptions {
   /// </summary>
   public string? NmapTargetSpecification { get; init; }
 
-  public TimeSpan ProcfsArpScanInterval { get; init; } = TimeSpan.FromMinutes(15.0);
+  public TimeSpan ProcfsArpFullScanInterval { get; init; } = TimeSpan.FromMinutes(15.0);
 }
diff --git a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/NullMacAddressResolver.cs b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/NullMacAddressResolver.cs
index 2a3ee21..d78d3e9 100644
--- a/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/NullMacAddressResolver.cs
+++ b/src/Smdn.Net.AddressResolution/Smdn.Net.AddressResolution/NullMacAddressResolver.cs
@@ -6,6 +6,8 @@ using System.Threading.Tasks;
 namespace Smdn.Net.AddressResolution;
 
 internal sealed class NullMacAddressResolver : MacAddressResolver {
+  public override bool HasInvalidated => false;
+
   internal NullMacAddressResolver()
     : base(logger: null)
   {
@@ -39,4 +41,14 @@ internal sealed class NullMacAddressResolver : MacAddressResolver {
 #else
       ValueTaskShim.FromResult<IPAddress?>(null);
 #endif
+
+  protected override void InvalidateCore(IPAddress ipAddress)
+  {
+    // do nothing
+  }
+
+  protected override void InvalidateCore(PhysicalAddress macAddress)
+  {
+    // do nothing
+  }
 }

Notes

Full Changelog: releases/Smdn.Net.AddressResolution-1.0.0-preview3...releases/Smdn.Net.AddressResolution-1.0.0-preview4