diff --git a/agents.md b/AGENTS.md
similarity index 100%
rename from agents.md
rename to AGENTS.md
diff --git a/Source/AGENTS.md b/Source/AGENTS.md
new file mode 100644
index 0000000000..3005b066e2
--- /dev/null
+++ b/Source/AGENTS.md
@@ -0,0 +1 @@
+See [AGENTS.md](../AGENTS.md) in the repository root for project guidelines and conventions.
\ No newline at end of file
diff --git a/Source/GlobalAssemblyInfo.cs b/Source/GlobalAssemblyInfo.cs
index e66a24bb05..7f5aa87dfd 100644
--- a/Source/GlobalAssemblyInfo.cs
+++ b/Source/GlobalAssemblyInfo.cs
@@ -6,5 +6,5 @@
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
-[assembly: AssemblyVersion("2026.4.26.0")]
-[assembly: AssemblyFileVersion("2026.4.26.0")]
+[assembly: AssemblyVersion("2026.5.3.0")]
+[assembly: AssemblyFileVersion("2026.5.3.0")]
diff --git a/Source/NETworkManager.Converters/NeighborStateToStringConverter.cs b/Source/NETworkManager.Converters/NeighborStateToStringConverter.cs
new file mode 100644
index 0000000000..d738ac7c59
--- /dev/null
+++ b/Source/NETworkManager.Converters/NeighborStateToStringConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using NETworkManager.Localization;
+using NETworkManager.Models.Network;
+
+namespace NETworkManager.Converters;
+
+///
+/// Convert to a translated .
+///
+public sealed class NeighborStateToStringConverter : IValueConverter
+{
+ ///
+ /// Convert to a translated .
+ ///
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not NeighborState state
+ ? "-/-"
+ : ResourceTranslator.Translate(ResourceIdentifier.NeighborState, state);
+ }
+
+ ///
+ /// !!! Method not implemented !!!
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Source/NETworkManager.Documentation/DocumentationIdentifier.cs b/Source/NETworkManager.Documentation/DocumentationIdentifier.cs
index fde0414a35..6ae1c6c742 100644
--- a/Source/NETworkManager.Documentation/DocumentationIdentifier.cs
+++ b/Source/NETworkManager.Documentation/DocumentationIdentifier.cs
@@ -141,9 +141,9 @@ public enum DocumentationIdentifier
ApplicationListeners,
///
- /// ARP Table documentation page.
+ /// Neighbor Table documentation page.
///
- ApplicationArpTable,
+ ApplicationNeighborTable,
///
/// Settings\General documentation page.
diff --git a/Source/NETworkManager.Documentation/DocumentationManager.cs b/Source/NETworkManager.Documentation/DocumentationManager.cs
index 3907c3b97b..6347394f27 100644
--- a/Source/NETworkManager.Documentation/DocumentationManager.cs
+++ b/Source/NETworkManager.Documentation/DocumentationManager.cs
@@ -99,8 +99,8 @@ public static class DocumentationManager
new DocumentationInfo(DocumentationIdentifier.ApplicationListeners,
@"docs/application/listeners"),
- new DocumentationInfo(DocumentationIdentifier.ApplicationArpTable,
- @"docs/application/arp-table"),
+ new DocumentationInfo(DocumentationIdentifier.ApplicationNeighborTable,
+ @"docs/application/neighbor-table"),
new DocumentationInfo(DocumentationIdentifier.SettingsGeneral,
@"docs/settings/general"),
@@ -228,7 +228,7 @@ public static DocumentationIdentifier GetIdentifierByApplicationName(Application
ApplicationName.Lookup => DocumentationIdentifier.ApplicationLookup,
ApplicationName.Connections => DocumentationIdentifier.ApplicationConnections,
ApplicationName.Listeners => DocumentationIdentifier.ApplicationListeners,
- ApplicationName.ARPTable => DocumentationIdentifier.ApplicationArpTable,
+ ApplicationName.NeighborTable => DocumentationIdentifier.ApplicationNeighborTable,
ApplicationName.None => DocumentationIdentifier.Default,
_ => DocumentationIdentifier.Default
};
diff --git a/Source/NETworkManager.Localization/ResourceIdentifier.cs b/Source/NETworkManager.Localization/ResourceIdentifier.cs
index 1f36bc0209..a174e82a2d 100644
--- a/Source/NETworkManager.Localization/ResourceIdentifier.cs
+++ b/Source/NETworkManager.Localization/ResourceIdentifier.cs
@@ -26,5 +26,6 @@ public enum ResourceIdentifier
FirewallProtocol,
FirewallInterfaceType,
FirewallRuleDirection,
- FirewallRuleAction
+ FirewallRuleAction,
+ NeighborState
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index 5d00425692..8075252124 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -581,7 +580,7 @@ public static string AddProfileFile {
return ResourceManager.GetString("AddProfileFile", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Add rule.
///
@@ -590,7 +589,7 @@ public static string AddRule {
return ResourceManager.GetString("AddRule", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Add rule....
///
@@ -599,7 +598,7 @@ public static string AddRuleDots {
return ResourceManager.GetString("AddRuleDots", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Add server.
///
@@ -762,15 +761,6 @@ public static string Appearance {
}
}
- ///
- /// Looks up a localized string similar to ARP Table.
- ///
- public static string ApplicationName_ARPTable {
- get {
- return ResourceManager.GetString("ApplicationName_ARPTable", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Bit Calculator.
///
@@ -870,6 +860,15 @@ public static string ApplicationName_Lookup {
}
}
+ ///
+ /// Looks up a localized string similar to Neighbor Table.
+ ///
+ public static string ApplicationName_NeighborTable {
+ get {
+ return ResourceManager.GetString("ApplicationName_NeighborTable", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Network Interface.
///
@@ -1104,15 +1103,6 @@ public static string ARP {
}
}
- ///
- /// Looks up a localized string similar to ARP Table.
- ///
- public static string ARPTable {
- get {
- return ResourceManager.GetString("ARPTable", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to ASN.
///
@@ -2682,16 +2672,7 @@ public static string DeleteEntry {
return ResourceManager.GetString("DeleteEntry", resourceCulture);
}
}
-
- ///
- /// Looks up a localized string similar to Delete rule.
- ///
- public static string DeleteRule {
- get {
- return ResourceManager.GetString("DeleteRule", resourceCulture);
- }
- }
-
+
///
/// Looks up a localized string similar to The selected firewall rule is permanently deleted:
///
@@ -2822,6 +2803,15 @@ public static string DeleteProfilesMessage {
}
}
+ ///
+ /// Looks up a localized string similar to Delete rule.
+ ///
+ public static string DeleteRule {
+ get {
+ return ResourceManager.GetString("DeleteRule", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Delete SNTP server.
///
@@ -3019,7 +3009,7 @@ public static string DisableEntry {
return ResourceManager.GetString("DisableEntry", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Disable rule.
///
@@ -3028,7 +3018,7 @@ public static string DisableRule {
return ResourceManager.GetString("DisableRule", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Disclaimer.
///
@@ -3344,25 +3334,7 @@ public static string EditEntryDots {
return ResourceManager.GetString("EditEntryDots", resourceCulture);
}
}
-
- ///
- /// Looks up a localized string similar to Edit rule.
- ///
- public static string EditRule {
- get {
- return ResourceManager.GetString("EditRule", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Edit rule....
- ///
- public static string EditRuleDots {
- get {
- return ResourceManager.GetString("EditRuleDots", resourceCulture);
- }
- }
-
+
///
/// Looks up a localized string similar to Edit group.
///
@@ -3417,6 +3389,24 @@ public static string EditProfileFile {
}
}
+ ///
+ /// Looks up a localized string similar to Edit rule.
+ ///
+ public static string EditRule {
+ get {
+ return ResourceManager.GetString("EditRule", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Edit rule....
+ ///
+ public static string EditRuleDots {
+ get {
+ return ResourceManager.GetString("EditRuleDots", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Edit SNTP server.
///
@@ -3509,16 +3499,7 @@ public static string EnableEntry {
return ResourceManager.GetString("EnableEntry", resourceCulture);
}
}
-
- ///
- /// Looks up a localized string similar to Enable rule.
- ///
- public static string EnableRule {
- get {
- return ResourceManager.GetString("EnableRule", resourceCulture);
- }
- }
-
+
///
/// Looks up a localized string similar to Enable gateway server.
///
@@ -3537,6 +3518,15 @@ public static string EnableLog {
}
}
+ ///
+ /// Looks up a localized string similar to Enable rule.
+ ///
+ public static string EnableRule {
+ get {
+ return ResourceManager.GetString("EnableRule", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Encryption.
///
@@ -4690,11 +4680,11 @@ public static string Group {
}
///
- /// Looks up a localized string similar to Group / domain name.
+ /// Looks up a localized string similar to Group / domain.
///
- public static string GroupDomainName {
+ public static string GroupDomain {
get {
- return ResourceManager.GetString("GroupDomainName", resourceCulture);
+ return ResourceManager.GetString("GroupDomain", resourceCulture);
}
}
@@ -6473,6 +6463,87 @@ public static string Name {
}
}
+ ///
+ /// Looks up a localized string similar to Delay.
+ ///
+ public static string NeighborState_Delay {
+ get {
+ return ResourceManager.GetString("NeighborState_Delay", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Incomplete.
+ ///
+ public static string NeighborState_Incomplete {
+ get {
+ return ResourceManager.GetString("NeighborState_Incomplete", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Permanent.
+ ///
+ public static string NeighborState_Permanent {
+ get {
+ return ResourceManager.GetString("NeighborState_Permanent", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Probe.
+ ///
+ public static string NeighborState_Probe {
+ get {
+ return ResourceManager.GetString("NeighborState_Probe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Reachable.
+ ///
+ public static string NeighborState_Reachable {
+ get {
+ return ResourceManager.GetString("NeighborState_Reachable", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Stale.
+ ///
+ public static string NeighborState_Stale {
+ get {
+ return ResourceManager.GetString("NeighborState_Stale", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unreachable.
+ ///
+ public static string NeighborState_Unreachable {
+ get {
+ return ResourceManager.GetString("NeighborState_Unreachable", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Neighbor Table.
+ ///
+ public static string NeighborTable {
+ get {
+ return ResourceManager.GetString("NeighborTable", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Read-only mode. Modifying the neighbor table requires elevated rights!.
+ ///
+ public static string NeighborTableAdminMessage {
+ get {
+ return ResourceManager.GetString("NeighborTableAdminMessage", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to NetBIOS.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index a7445c649c..b49e9c2d35 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -138,8 +138,32 @@
ARP
-
- ARP Table
+
+ Neighbor Table
+
+
+ Read-only mode. Modifying the neighbor table requires elevated rights!
+
+
+ Unreachable
+
+
+ Incomplete
+
+
+ Probe
+
+
+ Delay
+
+
+ Stale
+
+
+ Reachable
+
+
+ Permanent
Authentication
@@ -2463,8 +2487,8 @@ $$hostname$$ --> Hostname
Microsoft.Windows.SDK.Contracts is required for this feature but not available on this system (e.g. on Windows Server).
-
- ARP Table
+
+ Neighbor Table
Connections
@@ -3718,8 +3742,8 @@ Try again in a few seconds.
Warning
-
- Group / domain name
+
+ Group / domain
NetBIOS reachable
diff --git a/Source/NETworkManager.Models/ApplicationManager.cs b/Source/NETworkManager.Models/ApplicationManager.cs
index 8270954762..f4dac25830 100644
--- a/Source/NETworkManager.Models/ApplicationManager.cs
+++ b/Source/NETworkManager.Models/ApplicationManager.cs
@@ -27,7 +27,9 @@ public static IEnumerable GetNames()
/// IEnumerable with .
public static IEnumerable GetDefaultList()
{
- return [.. GetNames().Where(x => x != ApplicationName.None && x != ApplicationName.AWSSessionManager).Select(name => new ApplicationInfo(name, true, name == ApplicationName.Dashboard))];
+#pragma warning disable CS0618
+ return [.. GetNames().Where(x => x != ApplicationName.None && x != ApplicationName.AWSSessionManager && x != ApplicationName.ARPTable).Select(name => new ApplicationInfo(name, true, name == ApplicationName.Dashboard))];
+#pragma warning restore CS0618
}
///
@@ -118,13 +120,17 @@ public static Canvas GetIcon(ApplicationName name)
case ApplicationName.Listeners:
canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.Wan });
break;
+ case ApplicationName.NeighborTable:
+#pragma warning disable CS0618
case ApplicationName.ARPTable:
+#pragma warning restore CS0618
canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.TableOfContents });
break;
case ApplicationName.Firewall:
canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.WallFire });
break;
case ApplicationName.None:
+ case ApplicationName.AWSSessionManager:
default:
canvas.Children.Add(new PackIconModern { Kind = PackIconModernKind.SmileyFrown });
break;
diff --git a/Source/NETworkManager.Models/ApplicationName.cs b/Source/NETworkManager.Models/ApplicationName.cs
index def934f22a..20bd63710c 100644
--- a/Source/NETworkManager.Models/ApplicationName.cs
+++ b/Source/NETworkManager.Models/ApplicationName.cs
@@ -148,7 +148,14 @@ public enum ApplicationName
Listeners,
///
- /// ARP table application.
+ /// Neighbor table application (IPv4 ARP + IPv6 NDP).
///
+ NeighborTable,
+
+ ///
+ /// Obsolete: renamed to . Kept so that old settings files
+ /// with "ARPTable" can still be deserialized; migrated in SettingsManager.Upgrade().
+ ///
+ [System.Obsolete("Use NeighborTable instead.")]
ARPTable
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Export/ExportManager.ARPInfo.cs b/Source/NETworkManager.Models/Export/ExportManager.ARPInfo.cs
deleted file mode 100644
index 8077c59631..0000000000
--- a/Source/NETworkManager.Models/Export/ExportManager.ARPInfo.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Xml.Linq;
-using NETworkManager.Models.Network;
-using Newtonsoft.Json;
-
-namespace NETworkManager.Models.Export;
-
-public static partial class ExportManager
-{
- ///
- /// Method to export objects from type to a file.
- ///
- /// Path to the export file.
- /// Allowed are CSV, XML or JSON.
- /// Objects as to export.
- public static void Export(string filePath, ExportFileType fileType, IReadOnlyList collection)
- {
- switch (fileType)
- {
- case ExportFileType.Csv:
- CreateCsv(collection, filePath);
- break;
- case ExportFileType.Xml:
- CreateXml(collection, filePath);
- break;
- case ExportFileType.Json:
- CreateJson(collection, filePath);
- break;
- case ExportFileType.Txt:
- default:
- throw new ArgumentOutOfRangeException(nameof(fileType), fileType, null);
- }
- }
-
- ///
- /// Creates a CSV file from the given collection.
- ///
- /// Objects as to export.
- /// Path to the export file.
- private static void CreateCsv(IEnumerable collection, string filePath)
- {
- var stringBuilder = new StringBuilder();
-
- stringBuilder.AppendLine(
- $"{nameof(ARPInfo.IPAddress)},{nameof(ARPInfo.MACAddress)},{nameof(ARPInfo.IsMulticast)}");
-
- foreach (var info in collection)
- stringBuilder.AppendLine($"{info.IPAddress},{info.MACAddress},{info.IsMulticast}");
-
- File.WriteAllText(filePath, stringBuilder.ToString());
- }
-
- ///
- /// Creates a XML file from the given collection.
- ///
- /// Objects as to export.
- /// Path to the export file.
- private static void CreateXml(IEnumerable collection, string filePath)
- {
- var document = new XDocument(DefaultXDeclaration,
- new XElement(ApplicationName.ARPTable.ToString(),
- new XElement(nameof(ARPInfo) + "s",
- from info in collection
- select
- new XElement(nameof(ARPInfo),
- new XElement(nameof(ARPInfo.IPAddress), info.IPAddress),
- new XElement(nameof(ARPInfo.MACAddress), info.MACAddress),
- new XElement(nameof(ARPInfo.IsMulticast), info.IsMulticast)))));
-
- document.Save(filePath);
- }
-
- ///
- /// Creates a JSON file from the given collection.
- ///
- /// Objects as to export.
- /// Path to the export file.
- private static void CreateJson(IReadOnlyList collection, string filePath)
- {
- var jsonData = new object[collection.Count];
-
- for (var i = 0; i < collection.Count; i++)
- jsonData[i] = new
- {
- IPAddress = collection[i].IPAddress.ToString(),
- MACAddress = collection[i].MACAddress.ToString(),
- collection[i].IsMulticast
- };
-
- File.WriteAllText(filePath, JsonConvert.SerializeObject(jsonData, Formatting.Indented));
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Export/ExportManager.IPScannerHostInfo.cs b/Source/NETworkManager.Models/Export/ExportManager.IPScannerHostInfo.cs
index 6fb6b1d5aa..07ab949f82 100644
--- a/Source/NETworkManager.Models/Export/ExportManager.IPScannerHostInfo.cs
+++ b/Source/NETworkManager.Models/Export/ExportManager.IPScannerHostInfo.cs
@@ -66,9 +66,7 @@ private static void CreateCsv(IEnumerable collection, string
$"NetBIOSMACAddress," +
$"NetBIOSVendor," +
$"{nameof(IPScannerHostInfo.MACAddress)}," +
- $"{nameof(IPScannerHostInfo.Vendor)}," +
- $"{nameof(IPScannerHostInfo.ARPMACAddress)}," +
- $"{nameof(IPScannerHostInfo.ARPVendor)}"
+ $"{nameof(IPScannerHostInfo.Vendor)}"
);
foreach (var info in collection)
@@ -105,9 +103,7 @@ private static void CreateCsv(IEnumerable collection, string
$"{info.NetBIOSInfo?.MACAddress}," +
$"{CsvHelper.QuoteString(info.NetBIOSInfo?.Vendor)}," +
$"{info.MACAddress}," +
- $"{CsvHelper.QuoteString(info.Vendor)}," +
- $"{info.ARPMACAddress}," +
- $"{CsvHelper.QuoteString(info.ARPVendor)}"
+ $"{CsvHelper.QuoteString(info.Vendor)}"
);
}
@@ -156,9 +152,7 @@ from info in collection
new XElement("NetBIOSMACAddress", info.NetBIOSInfo?.MACAddress),
new XElement("NetBIOSVendor", info.NetBIOSInfo?.Vendor),
new XElement(nameof(IPScannerHostInfo.MACAddress), info.MACAddress),
- new XElement(nameof(IPScannerHostInfo.Vendor), info.Vendor),
- new XElement(nameof(IPScannerHostInfo.ARPMACAddress), info.ARPMACAddress),
- new XElement(nameof(IPScannerHostInfo.ARPVendor), info.ARPVendor)
+ new XElement(nameof(IPScannerHostInfo.Vendor), info.Vendor)
)
)
)
@@ -221,9 +215,7 @@ private static void CreateJson(IReadOnlyList collection, stri
NetBIOSMACAddress = info.NetBIOSInfo?.MACAddress,
NetBIOSVendor = info.NetBIOSInfo?.Vendor,
info.MACAddress,
- info.Vendor,
- info.ARPMACAddress,
- info.ARPVendor
+ info.Vendor
};
}
diff --git a/Source/NETworkManager.Models/Export/ExportManager.NeighborInfo.cs b/Source/NETworkManager.Models/Export/ExportManager.NeighborInfo.cs
new file mode 100644
index 0000000000..af6e501d11
--- /dev/null
+++ b/Source/NETworkManager.Models/Export/ExportManager.NeighborInfo.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml.Linq;
+using NETworkManager.Models.Network;
+using NETworkManager.Utilities;
+using Newtonsoft.Json;
+
+namespace NETworkManager.Models.Export;
+
+public static partial class ExportManager
+{
+ ///
+ /// Method to export objects from type to a file.
+ ///
+ /// Path to the export file.
+ /// Allowed are CSV, XML or JSON.
+ /// Objects as to export.
+ public static void Export(string filePath, ExportFileType fileType, IReadOnlyList collection)
+ {
+ switch (fileType)
+ {
+ case ExportFileType.Csv:
+ CreateCsv(collection, filePath);
+ break;
+ case ExportFileType.Xml:
+ CreateXml(collection, filePath);
+ break;
+ case ExportFileType.Json:
+ CreateJson(collection, filePath);
+ break;
+ case ExportFileType.Txt:
+ default:
+ throw new ArgumentOutOfRangeException(nameof(fileType), fileType, null);
+ }
+ }
+
+ ///
+ /// Creates a CSV file from the given collection.
+ ///
+ private static void CreateCsv(IEnumerable collection, string filePath)
+ {
+ var stringBuilder = new StringBuilder();
+
+ stringBuilder.AppendLine(
+ $"{nameof(NeighborInfo.IPAddress)},{nameof(NeighborInfo.MACAddress)},{nameof(NeighborInfo.InterfaceAlias)},{nameof(NeighborInfo.InterfaceIndex)},{nameof(NeighborInfo.State)},{nameof(NeighborInfo.AddressFamily)},{nameof(NeighborInfo.IsMulticast)}");
+
+ foreach (var info in collection)
+ stringBuilder.AppendLine(
+ $"{info.IPAddress},{info.MACAddress},{CsvHelper.QuoteString(info.InterfaceAlias)},{info.InterfaceIndex},{info.State},{info.AddressFamily},{info.IsMulticast}");
+
+ File.WriteAllText(filePath, stringBuilder.ToString());
+ }
+
+ ///
+ /// Creates an XML file from the given collection.
+ ///
+ private static void CreateXml(IEnumerable collection, string filePath)
+ {
+ var document = new XDocument(DefaultXDeclaration,
+ new XElement(ApplicationName.NeighborTable.ToString(),
+ new XElement(nameof(NeighborInfo) + "s",
+ from info in collection
+ select
+ new XElement(nameof(NeighborInfo),
+ new XElement(nameof(NeighborInfo.IPAddress), info.IPAddress),
+ new XElement(nameof(NeighborInfo.MACAddress), info.MACAddress),
+ new XElement(nameof(NeighborInfo.InterfaceAlias), info.InterfaceAlias),
+ new XElement(nameof(NeighborInfo.InterfaceIndex), info.InterfaceIndex),
+ new XElement(nameof(NeighborInfo.State), info.State),
+ new XElement(nameof(NeighborInfo.AddressFamily), info.AddressFamily),
+ new XElement(nameof(NeighborInfo.IsMulticast), info.IsMulticast)))));
+
+ document.Save(filePath);
+ }
+
+ ///
+ /// Creates a JSON file from the given collection.
+ ///
+ private static void CreateJson(IReadOnlyList collection, string filePath)
+ {
+ var jsonData = new object[collection.Count];
+
+ for (var i = 0; i < collection.Count; i++)
+ jsonData[i] = new
+ {
+ IPAddress = collection[i].IPAddress.ToString(),
+ MACAddress = collection[i].MACAddress.ToString(),
+ collection[i].InterfaceAlias,
+ collection[i].InterfaceIndex,
+ State = collection[i].State.ToString(),
+ AddressFamily = collection[i].AddressFamily.ToString(),
+ collection[i].IsMulticast
+ };
+
+ File.WriteAllText(filePath, JsonConvert.SerializeObject(jsonData, Formatting.Indented));
+ }
+}
diff --git a/Source/NETworkManager.Models/Firewall/Firewall.cs b/Source/NETworkManager.Models/Firewall/Firewall.cs
index cbf683b843..915692d24c 100644
--- a/Source/NETworkManager.Models/Firewall/Firewall.cs
+++ b/Source/NETworkManager.Models/Firewall/Firewall.cs
@@ -15,9 +15,10 @@ namespace NETworkManager.Models.Firewall;
///
/// Provides static methods to read and modify Windows Firewall rules via PowerShell.
-/// All operations share a single that is initialized once with
-/// the required execution policy and the NetSecurity module, reducing per-call overhead.
-/// A serializes access so the runspace is never used concurrently.
+/// All operations share a single that is lazily initialized on
+/// first use with the required execution policy and the NetSecurity module imported,
+/// reducing per-call overhead. A serializes access so the
+/// runspace is never used concurrently.
///
public class Firewall
{
@@ -37,29 +38,29 @@ public class Firewall
///
/// Ensures that only one PowerShell pipeline runs on at a time.
///
- private static readonly SemaphoreSlim Lock = new(1, 1);
+ private static readonly SemaphoreSlim RunspaceLock = new(1, 1);
///
- /// Shared PowerShell runspace, initialized once in the static constructor with
- /// Set-ExecutionPolicy Bypass and Import-Module NetSecurity.
+ /// Lazily initialized PowerShell runspace. Created and configured on first access so that
+ /// simply navigating to the Firewall view does not start a PowerShell process unless a
+ /// modifying operation is actually performed.
///
- private static readonly Runspace SharedRunspace;
-
- ///
- /// Opens and runs the one-time initialization script
- /// so that subsequent operations do not need to repeat the module import.
- ///
- static Firewall()
+ private static readonly Lazy _sharedRunspace = new(() =>
{
- SharedRunspace = RunspaceFactory.CreateRunspace();
- SharedRunspace.Open();
+ var runspace = RunspaceFactory.CreateRunspace();
+ runspace.Open();
using var ps = SMA.PowerShell.Create();
- ps.Runspace = SharedRunspace;
+ ps.Runspace = runspace;
ps.AddScript(@"
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
Import-Module NetSecurity -ErrorAction Stop").Invoke();
- }
+
+ return runspace;
+ });
+
+ /// Returns the shared runspace, initializing it on first access.
+ private static Runspace SharedRunspace => _sharedRunspace.Value;
#endregion
@@ -76,7 +77,7 @@ static Firewall()
///
public static async Task> GetRulesAsync()
{
- await Lock.WaitAsync();
+ await RunspaceLock.WaitAsync();
try
{
return await Task.Run(() =>
@@ -162,7 +163,7 @@ public static async Task> GetRulesAsync()
}
finally
{
- Lock.Release();
+ RunspaceLock.Release();
}
}
@@ -182,7 +183,7 @@ public static async Task> GetRulesAsync()
///
public static async Task SetRuleEnabledAsync(FirewallRule rule, bool enabled)
{
- await Lock.WaitAsync();
+ await RunspaceLock.WaitAsync();
try
{
await Task.Run(() =>
@@ -199,7 +200,7 @@ await Task.Run(() =>
}
finally
{
- Lock.Release();
+ RunspaceLock.Release();
}
}
@@ -215,7 +216,7 @@ await Task.Run(() =>
///
public static async Task DeleteRuleAsync(FirewallRule rule)
{
- await Lock.WaitAsync();
+ await RunspaceLock.WaitAsync();
try
{
await Task.Run(() =>
@@ -232,7 +233,7 @@ await Task.Run(() =>
}
finally
{
- Lock.Release();
+ RunspaceLock.Release();
}
}
@@ -249,7 +250,7 @@ await Task.Run(() =>
///
public static async Task AddRuleAsync(FirewallRule rule)
{
- await Lock.WaitAsync();
+ await RunspaceLock.WaitAsync();
try
{
await Task.Run(() =>
@@ -266,7 +267,7 @@ await Task.Run(() =>
}
finally
{
- Lock.Release();
+ RunspaceLock.Release();
}
}
@@ -281,7 +282,7 @@ private static string BuildAddScript(FirewallRule rule)
{
var sb = new StringBuilder();
sb.AppendLine("$params = @{");
- sb.AppendLine($" DisplayName = '{RuleIdentifier}{EscapePs(rule.Name)}'");
+ sb.AppendLine($" DisplayName = '{RuleIdentifier}{PowerShellHelper.EscapeSingleQuotes(rule.Name)}'");
sb.AppendLine($" Enabled = '{(rule.IsEnabled ? "True" : "False")}'");
sb.AppendLine($" Direction = '{rule.Direction}'");
sb.AppendLine($" Action = '{rule.Action}'");
@@ -291,7 +292,7 @@ private static string BuildAddScript(FirewallRule rule)
sb.AppendLine("}");
if (!string.IsNullOrWhiteSpace(rule.Description))
- sb.AppendLine($"$params['Description'] = '{EscapePs(rule.Description)}'");
+ sb.AppendLine($"$params['Description'] = '{PowerShellHelper.EscapeSingleQuotes(rule.Description)}'");
if (rule.Protocol is FirewallProtocol.TCP or FirewallProtocol.UDP)
{
@@ -309,20 +310,13 @@ private static string BuildAddScript(FirewallRule rule)
sb.AppendLine($"$params['RemoteAddress'] = {ToPsArray(rule.RemoteAddresses)}");
if (rule.Program != null && !string.IsNullOrWhiteSpace(rule.Program.Name))
- sb.AppendLine($"$params['Program'] = '{EscapePs(rule.Program.Name)}'");
+ sb.AppendLine($"$params['Program'] = '{PowerShellHelper.EscapeSingleQuotes(rule.Program.Name)}'");
sb.AppendLine("New-NetFirewallRule @params");
return sb.ToString();
}
- ///
- /// Escapes a string for embedding inside a PowerShell single-quoted string by
- /// doubling any single-quote characters.
- ///
- /// The raw string value to escape.
- private static string EscapePs(string value) => value.Replace("'", "''");
-
///
/// Builds a PowerShell array literal (e.g. @('80','443','8080-8090')) from the given values.
/// New-NetFirewallRule parameters such as -LocalPort and -LocalAddress are typed as
@@ -331,7 +325,7 @@ private static string BuildAddScript(FirewallRule rule)
///
/// The values to embed into the array literal.
private static string ToPsArray(IEnumerable values) =>
- $"@({string.Join(",", values.Select(v => $"'{EscapePs(v)}'"))})";
+ $"@({string.Join(",", values.Select(v => $"'{PowerShellHelper.EscapeSingleQuotes(v)}'"))})";
///
/// Maps a value to the string accepted by
diff --git a/Source/NETworkManager.Models/Network/ARP.cs b/Source/NETworkManager.Models/Network/ARP.cs
deleted file mode 100644
index 9532fb3971..0000000000
--- a/Source/NETworkManager.Models/Network/ARP.cs
+++ /dev/null
@@ -1,214 +0,0 @@
-// Contains code from: https://stackoverflow.com/a/1148861/4986782
-// Modified by BornToBeRoot
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-using NETworkManager.Utilities;
-
-namespace NETworkManager.Models.Network;
-
-public class ARP
-{
- #region Variables
-
- // The max number of physical addresses.
- private const int MAXLEN_PHYSADDR = 8;
-
- // Define the MIB_IPNETROW structure.
- [StructLayout(LayoutKind.Sequential)]
- private struct MIB_IPNETROW
- {
- [MarshalAs(UnmanagedType.U4)] public int dwIndex;
- [MarshalAs(UnmanagedType.U4)] public int dwPhysAddrLen;
- [MarshalAs(UnmanagedType.U1)] public byte mac0;
- [MarshalAs(UnmanagedType.U1)] public byte mac1;
- [MarshalAs(UnmanagedType.U1)] public byte mac2;
- [MarshalAs(UnmanagedType.U1)] public byte mac3;
- [MarshalAs(UnmanagedType.U1)] public byte mac4;
- [MarshalAs(UnmanagedType.U1)] public byte mac5;
- [MarshalAs(UnmanagedType.U1)] public byte mac6;
- [MarshalAs(UnmanagedType.U1)] public byte mac7;
- [MarshalAs(UnmanagedType.U4)] public int dwAddr;
- [MarshalAs(UnmanagedType.U4)] public int dwType;
- }
-
- // Declare the GetIpNetTable function.
- [DllImport("IpHlpApi.dll")]
- [return: MarshalAs(UnmanagedType.U4)]
- private static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(UnmanagedType.U4)] ref int pdwSize,
- bool bOrder);
-
- [DllImport("IpHlpApi.dll", SetLastError = true, CharSet = CharSet.Auto)]
- internal static extern int FreeMibTable(IntPtr plpNetTable);
-
- // The insufficient buffer error.
- private const int ERROR_INSUFFICIENT_BUFFER = 122;
-
- #endregion
-
- #region Events
-
- public event EventHandler UserHasCanceled;
-
- protected virtual void OnUserHasCanceled()
- {
- UserHasCanceled?.Invoke(this, EventArgs.Empty);
- }
-
- #endregion
-
- #region Methods
-
- public static Task> GetTableAsync()
- {
- return Task.Run(GetTable);
- }
-
- private static List GetTable()
- {
- var list = new List();
-
- // The number of bytes needed.
- var bytesNeeded = 0;
-
- // The result from the API call.
- var result = GetIpNetTable(IntPtr.Zero, ref bytesNeeded, false);
-
- // Call the function, expecting an insufficient buffer.
- if (result != ERROR_INSUFFICIENT_BUFFER)
- // Throw an exception.
- throw new Win32Exception(result);
-
- // Allocate the memory, do it in a try/finally block, to ensure
- // that it is released.
- var buffer = IntPtr.Zero;
-
- // Try/finally.
- try
- {
- // Allocate the memory.
- buffer = Marshal.AllocCoTaskMem(bytesNeeded);
-
- // Make the call again. If it did not succeed, then
- // raise an error.
- result = GetIpNetTable(buffer, ref bytesNeeded, false);
-
- // If the result is not 0 (no error), then throw an exception.
- if (result != 0)
- // Throw an exception.
- throw new Win32Exception(result);
-
- // Now we have the buffer, we have to marshal it. We can read
- // the first 4 bytes to get the length of the buffer.
- var entries = Marshal.ReadInt32(buffer);
-
- // Increment the memory pointer by the size of the int.
- var currentBuffer = new IntPtr(buffer.ToInt64() +
- Marshal.SizeOf(typeof(int)));
-
- // Allocate an array of entries.
- var table = new MIB_IPNETROW[entries];
-
- // Cycle through the entries.
- for (var i = 0; i < entries; i++)
- // Call PtrToStructure, getting the structure information.
- table[i] = (MIB_IPNETROW)Marshal.PtrToStructure(new
- IntPtr(currentBuffer.ToInt64() + i * Marshal.SizeOf(typeof(MIB_IPNETROW))), typeof(MIB_IPNETROW));
-
- var virtualMAC = new PhysicalAddress(new byte[] { 0, 0, 0, 0, 0, 0 });
- var broadcastMAC = new PhysicalAddress(new byte[] { 255, 255, 255, 255, 255, 255 });
-
- for (var i = 0; i < entries; i++)
- {
- var row = table[i];
-
- var ipAddress = new IPAddress(BitConverter.GetBytes(row.dwAddr));
- var macAddress = new PhysicalAddress(new[]
- { row.mac0, row.mac1, row.mac2, row.mac3, row.mac4, row.mac5 });
-
- // Filter 0.0.0.0.0.0, 255.255.255.255.255.255
- if (!macAddress.Equals(virtualMAC) && !macAddress.Equals(broadcastMAC))
- list.Add(new ARPInfo(ipAddress, macAddress,
- ipAddress.IsIPv6Multicast || IPv4Address.IsMulticast(ipAddress)));
- }
-
- return list;
- }
- finally
- {
- // Release the memory.
- FreeMibTable(buffer);
- }
- }
-
- public static string GetMACAddress(IPAddress ipAddress)
- {
- var arpInfo = GetTable().FirstOrDefault(x => x.IPAddress.Equals(ipAddress));
-
- return arpInfo?.MACAddress.ToString();
- }
-
- private void RunPowerShellCommand(string command)
- {
- try
- {
- PowerShellHelper.ExecuteCommand(command, true);
- }
- catch (Win32Exception win32Ex)
- {
- switch (win32Ex.NativeErrorCode)
- {
- case 1223:
- OnUserHasCanceled();
- break;
- default:
- throw;
- }
- }
- }
-
- // MAC separated with "-"
- public Task AddEntryAsync(string ipAddress, string macAddress)
- {
- return Task.Run(() => AddEntry(ipAddress, macAddress));
- }
-
- private void AddEntry(string ipAddress, string macAddress)
- {
- var command = $"arp -s {ipAddress} {macAddress}";
-
- RunPowerShellCommand(command);
- }
-
- public Task DeleteEntryAsync(string ipAddress)
- {
- return Task.Run(() => DeleteEntry(ipAddress));
- }
-
- private void DeleteEntry(string ipAddress)
- {
- var command = $"arp -d {ipAddress}";
-
- RunPowerShellCommand(command);
- }
-
- public Task DeleteTableAsync()
- {
- return Task.Run(() => DeleteTable());
- }
-
- private void DeleteTable()
- {
- const string command = "netsh interface ip delete arpcache";
-
- RunPowerShellCommand(command);
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Network/ARPInfo.cs b/Source/NETworkManager.Models/Network/ARPInfo.cs
deleted file mode 100644
index 9b0929414e..0000000000
--- a/Source/NETworkManager.Models/Network/ARPInfo.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Net;
-using System.Net.NetworkInformation;
-
-namespace NETworkManager.Models.Network;
-
-///
-/// Class to store information about an ARP entry.
-///
-public class ARPInfo
-{
- ///
- /// Creates a new instance of with the given parameters.
- ///
- /// IP address of the ARP entry.
- /// Physical address of the ARP entry.
- /// Indicates if the ARP entry is a multicast address.
- public ARPInfo(IPAddress ipAddress, PhysicalAddress macAddress, bool isMulticast)
- {
- IPAddress = ipAddress;
- MACAddress = macAddress;
- IsMulticast = isMulticast;
- }
-
- ///
- /// IP address of the ARP entry.
- ///
- public IPAddress IPAddress { get; set; }
-
- ///
- /// Physical address of the ARP entry.
- ///
- public PhysicalAddress MACAddress { get; set; }
-
- ///
- /// Indicates if the ARP entry is a multicast address.
- ///
- public bool IsMulticast { get; set; }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Network/IPScanner.cs b/Source/NETworkManager.Models/Network/IPScanner.cs
index 7ff2e412a0..7fb4e8b698 100644
--- a/Source/NETworkManager.Models/Network/IPScanner.cs
+++ b/Source/NETworkManager.Models/Network/IPScanner.cs
@@ -161,8 +161,8 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
if (options.ResolveMACAddress)
{
- // Get info from arp table
- arpMACAddress = ARP.GetMACAddress(host.ipAddress);
+ // Get info from neighbor table
+ arpMACAddress = NeighborTable.GetMACAddress(host.ipAddress);
// Check if it is the local mac
if (string.IsNullOrEmpty(arpMACAddress))
@@ -199,15 +199,13 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
isAnyPortOpen,
portScanResults.OrderBy(x => x.Port).ToList(),
netBIOSInfo,
- // ARP is default, fallback to netbios
+ // ARP/NDP is preferred, fallback to NetBIOS
!string.IsNullOrEmpty(arpMACAddress)
? arpMACAddress
: netBIOSInfo?.MACAddress ?? string.Empty,
!string.IsNullOrEmpty(arpMACAddress)
? arpVendor
- : netBIOSInfo?.Vendor ?? string.Empty,
- arpMACAddress,
- arpVendor
+ : netBIOSInfo?.Vendor ?? string.Empty
)
)
);
diff --git a/Source/NETworkManager.Models/Network/IPScannerHostInfo.cs b/Source/NETworkManager.Models/Network/IPScannerHostInfo.cs
index 87047ca556..4e579d77c0 100644
--- a/Source/NETworkManager.Models/Network/IPScannerHostInfo.cs
+++ b/Source/NETworkManager.Models/Network/IPScannerHostInfo.cs
@@ -17,13 +17,11 @@ public class IPScannerHostInfo
/// Indicates whether any port is open.
/// List of open ports.
/// NetBIOS information about the host.
- /// MAC address of the host (ARP or netbios).
+ /// MAC address of the host (ARP/NDP preferred, NetBIOS as fallback).
/// Vendor of the host based on the MAC address.
- /// MAC address of the host received from ARP.
- /// Vendor of the host based on the MAC address received from ARP.
public IPScannerHostInfo(bool isReachable, PingInfo pingInfo, string hostname, string dnsHostname,
bool isAnyPortOpen, List ports,
- NetBIOSInfo netBIOSInfo, string macAddress, string vendor, string arpMacAddress, string arpVendor)
+ NetBIOSInfo netBIOSInfo, string macAddress, string vendor)
{
IsReachable = isReachable;
PingInfo = pingInfo;
@@ -34,8 +32,6 @@ public IPScannerHostInfo(bool isReachable, PingInfo pingInfo, string hostname, s
NetBIOSInfo = netBIOSInfo;
MACAddress = macAddress;
Vendor = vendor;
- ARPMACAddress = arpMacAddress;
- ARPVendor = arpVendor;
}
///
@@ -74,7 +70,7 @@ public IPScannerHostInfo(bool isReachable, PingInfo pingInfo, string hostname, s
public NetBIOSInfo NetBIOSInfo { get; set; }
///
- /// MAC address of the host (ARP or netbios).
+ /// MAC address of the host (ARP/NDP preferred, NetBIOS as fallback).
///
public string MACAddress { get; set; }
@@ -82,14 +78,4 @@ public IPScannerHostInfo(bool isReachable, PingInfo pingInfo, string hostname, s
/// Vendor of the host based on the MAC address.
///
public string Vendor { get; set; }
-
- ///
- /// MAC address of the host received from ARP.
- ///
- public string ARPMACAddress { get; set; }
-
- ///
- /// Vendor of the host based on the MAC address received from ARP.
- ///
- public string ARPVendor { get; set; }
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Network/IPv4Address.cs b/Source/NETworkManager.Models/Network/IPv4Address.cs
index c6cd342f84..77a2069c11 100644
--- a/Source/NETworkManager.Models/Network/IPv4Address.cs
+++ b/Source/NETworkManager.Models/Network/IPv4Address.cs
@@ -89,7 +89,7 @@ public static bool IsMulticast(IPAddress ipAddress)
{
var ip = ToInt32(ipAddress);
- return ip >= IPv4MulticastStart && ip <= IPv4MulticastEnd;
+ return ip is >= IPv4MulticastStart and <= IPv4MulticastEnd;
}
///
diff --git a/Source/NETworkManager.Models/Network/NeighborInfo.cs b/Source/NETworkManager.Models/Network/NeighborInfo.cs
new file mode 100644
index 0000000000..a38e725f25
--- /dev/null
+++ b/Source/NETworkManager.Models/Network/NeighborInfo.cs
@@ -0,0 +1,63 @@
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+
+namespace NETworkManager.Models.Network;
+
+///
+/// Class to store information about an IP neighbor entry (IPv4 ARP / IPv6 NDP).
+///
+public class NeighborInfo
+{
+ ///
+ /// Creates a new instance of with the given parameters.
+ ///
+ public NeighborInfo(IPAddress ipAddress, PhysicalAddress macAddress, bool isMulticast, int interfaceIndex,
+ string interfaceAlias, NeighborState state, AddressFamily addressFamily)
+ {
+ IPAddress = ipAddress;
+ MACAddress = macAddress;
+ IsMulticast = isMulticast;
+ InterfaceIndex = interfaceIndex;
+ InterfaceAlias = interfaceAlias;
+ State = state;
+ AddressFamily = addressFamily;
+ }
+
+ ///
+ /// IP address of the neighbor entry (IPv4 or IPv6).
+ ///
+ public IPAddress IPAddress { get; set; }
+
+ ///
+ /// Physical (link-layer) address of the neighbor entry. May be empty for
+ /// / entries.
+ ///
+ public PhysicalAddress MACAddress { get; set; }
+
+ ///
+ /// Indicates whether the IP address is a multicast address.
+ ///
+ public bool IsMulticast { get; set; }
+
+ ///
+ /// Index of the network interface this neighbor entry belongs to.
+ ///
+ public int InterfaceIndex { get; set; }
+
+ ///
+ /// Human-readable name of the network interface (e.g. "Ethernet", "Wi-Fi").
+ ///
+ public string InterfaceAlias { get; set; }
+
+ ///
+ /// Reachability state of the neighbor entry.
+ ///
+ public NeighborState State { get; set; }
+
+ ///
+ /// Address family ( for IPv4,
+ /// for IPv6).
+ ///
+ public AddressFamily AddressFamily { get; set; }
+}
diff --git a/Source/NETworkManager.Models/Network/NeighborState.cs b/Source/NETworkManager.Models/Network/NeighborState.cs
new file mode 100644
index 0000000000..761e5339ad
--- /dev/null
+++ b/Source/NETworkManager.Models/Network/NeighborState.cs
@@ -0,0 +1,17 @@
+namespace NETworkManager.Models.Network;
+
+///
+/// Reachability state of an IP neighbor entry.
+/// Values match the State field of the MIB_IPNET_ROW2 structure
+/// returned by GetIpNetTable2, so a direct cast is safe.
+///
+public enum NeighborState
+{
+ Unreachable = 1,
+ Incomplete = 2,
+ Probe = 3,
+ Delay = 4,
+ Stale = 5,
+ Reachable = 6,
+ Permanent = 7
+}
diff --git a/Source/NETworkManager.Models/Network/NeighborTable.cs b/Source/NETworkManager.Models/Network/NeighborTable.cs
new file mode 100644
index 0000000000..7b0c13bcad
--- /dev/null
+++ b/Source/NETworkManager.Models/Network/NeighborTable.cs
@@ -0,0 +1,411 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Management.Automation.Runspaces;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Linq;
+using NETworkManager.Utilities;
+using SMA = System.Management.Automation;
+using log4net;
+
+namespace NETworkManager.Models.Network;
+
+///
+/// Provides static methods to read and modify the Windows IP neighbor table
+/// (IPv4 ARP and IPv6 NDP). Read access uses the GetIpNetTable2 Win32 API.
+/// Modifying operations (add/delete entries, clear table) run via PowerShell in a
+/// shared that is lazily initialized on first use with the
+/// required execution policy and the NetTCPIP module imported. A
+/// serializes access so the runspace is never used
+/// concurrently. Modifying operations require the application to run with elevated rights.
+///
+public class NeighborTable
+{
+ #region Variables
+
+ ///
+ /// The logger for this class.
+ ///
+ private static readonly ILog Log = LogManager.GetLogger(typeof(NeighborTable));
+
+ // Address family constants for SOCKADDR_INET / GetIpNetTable2.
+ private const ushort AF_UNSPEC = 0;
+ private const ushort AF_INET = 2;
+ private const ushort AF_INET6 = 23;
+
+ // Maximum length of a physical address inside MIB_IPNET_ROW2.
+ private const int IF_MAX_PHYS_ADDRESS_LENGTH = 32;
+
+ // Size of SOCKADDR_INET (matches sizeof(SOCKADDR_IN6) = 28).
+ private const int SOCKADDR_INET_SIZE = 28;
+
+ ///
+ /// Mirror of the native MIB_IPNET_ROW2 structure. The first 28 bytes hold
+ /// a SOCKADDR_INET union; we read the address family from the first two
+ /// bytes and parse the IPv4 / IPv6 address from the union accordingly.
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 8)]
+ private struct MIB_IPNET_ROW2
+ {
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = SOCKADDR_INET_SIZE)]
+ public byte[] Address;
+
+ public uint InterfaceIndex;
+ public ulong InterfaceLuid;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = IF_MAX_PHYS_ADDRESS_LENGTH)]
+ public byte[] PhysicalAddress;
+
+ public uint PhysicalAddressLength;
+ public uint State;
+ public uint Flags;
+ public uint ReachabilityTime;
+ }
+
+ /// Retrieves all entries from the IPv4/IPv6 neighbor cache. Returns 0 on success.
+ [DllImport("Iphlpapi.dll")]
+ private static extern uint GetIpNetTable2(ushort family, out IntPtr table);
+
+ /// Frees a MIB table buffer allocated by GetIpNetTable2.
+ [DllImport("Iphlpapi.dll")]
+ private static extern void FreeMibTable(IntPtr memory);
+
+ ///
+ /// Ensures that only one PowerShell pipeline runs on at a time.
+ ///
+ private static readonly SemaphoreSlim RunspaceLock = new(1, 1);
+
+ /// Protects reads and writes to and .
+ private static readonly Lock InterfaceAliasCacheLock = new();
+
+ /// Cached result of ; until first use.
+ private static Dictionary _interfaceAliasCache;
+
+ /// UTC time after which must be rebuilt.
+ private static DateTime _interfaceAliasCacheExpiry = DateTime.MinValue;
+
+ /// How long the interface alias cache is considered fresh before being rebuilt (5 minutes).
+ private static readonly TimeSpan InterfaceAliasCacheDuration = TimeSpan.FromMinutes(5);
+
+ ///
+ /// Lazily initialized PowerShell runspace. Created and configured on first access so that
+ /// read-only paths (e.g. MAC address lookup in IP Scanner) do not start a PowerShell
+ /// process unless a modifying operation is actually performed.
+ ///
+ private static readonly Lazy _sharedRunspace = new(() =>
+ {
+ var runspace = RunspaceFactory.CreateRunspace();
+ runspace.Open();
+
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = runspace;
+ ps.AddScript(@"
+Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
+Import-Module NetTCPIP -ErrorAction Stop").Invoke();
+
+ return runspace;
+ });
+
+ /// Returns the shared runspace, initializing it on first access.
+ private static Runspace SharedRunspace => _sharedRunspace.Value;
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Returns all entries from the IPv4 and IPv6 neighbor cache asynchronously.
+ ///
+ public static Task> GetTableAsync()
+ {
+ return Task.Run(GetTable);
+ }
+
+ ///
+ /// Returns a list of available network interfaces as index/name pairs, sorted by name.
+ /// Uses the cached alias map; see .
+ ///
+ public static Task>> GetInterfacesAsync()
+ {
+ return Task.Run(() => GetCachedInterfaceAliasMap()
+ .OrderBy(kv => kv.Value)
+ .ToList());
+ }
+
+ ///
+ /// Reads the full neighbor table via GetIpNetTable2 and returns it as a list
+ /// of objects. IPv4 entries with a virtual or broadcast MAC
+ /// are suppressed.
+ ///
+ private static List GetTable()
+ {
+ var list = new List();
+
+ var virtualMAC = new PhysicalAddress([0, 0, 0, 0, 0, 0]);
+ var broadcastMAC = new PhysicalAddress([255, 255, 255, 255, 255, 255]);
+
+ var aliasMap = GetCachedInterfaceAliasMap();
+
+ var table = IntPtr.Zero;
+
+ try
+ {
+ var result = GetIpNetTable2(AF_UNSPEC, out table);
+
+ if (result != 0)
+ throw new Win32Exception((int)result);
+
+ // First 4 bytes hold NumEntries; the array of MIB_IPNET_ROW2 starts after
+ // 4 bytes of padding (the row contains a ULONG64, requiring 8-byte alignment).
+ var numEntries = Marshal.ReadInt32(table);
+ var rowSize = Marshal.SizeOf();
+ var arrayPtr = IntPtr.Add(table, 8);
+
+ for (var i = 0; i < numEntries; i++)
+ {
+ var row = Marshal.PtrToStructure(IntPtr.Add(arrayPtr, i * rowSize));
+
+ var family = BitConverter.ToUInt16(row.Address, 0);
+
+ IPAddress ipAddress;
+ AddressFamily addressFamily;
+
+ switch (family)
+ {
+ case AF_INET:
+ {
+ // SOCKADDR_IN: family(2)+port(2)+addr(4)+zero(8) — addr at offset 4
+ var addrBytes = new byte[4];
+ Buffer.BlockCopy(row.Address, 4, addrBytes, 0, 4);
+ ipAddress = new IPAddress(addrBytes);
+ addressFamily = AddressFamily.InterNetwork;
+ break;
+ }
+ case AF_INET6:
+ {
+ // SOCKADDR_IN6: family(2)+port(2)+flowinfo(4)+addr(16)+scope_id(4) — addr at offset 8
+ var addrBytes = new byte[16];
+ Buffer.BlockCopy(row.Address, 8, addrBytes, 0, 16);
+ var scopeId = BitConverter.ToUInt32(row.Address, 24);
+ ipAddress = new IPAddress(addrBytes, scopeId);
+ addressFamily = AddressFamily.InterNetworkV6;
+ break;
+ }
+ default:
+ continue;
+ }
+
+ var macLen = (int)row.PhysicalAddressLength;
+
+ if (macLen is < 0 or > IF_MAX_PHYS_ADDRESS_LENGTH)
+ macLen = 0;
+
+ var macBytes = new byte[macLen];
+
+ if (macLen > 0)
+ Buffer.BlockCopy(row.PhysicalAddress, 0, macBytes, 0, macLen);
+
+ var macAddress = new PhysicalAddress(macBytes);
+
+ // Suppress virtual/broadcast MAC for IPv4 to match legacy behavior.
+ if (addressFamily == AddressFamily.InterNetwork &&
+ (macAddress.Equals(virtualMAC) || macAddress.Equals(broadcastMAC)))
+ continue;
+
+ aliasMap.TryGetValue((int)row.InterfaceIndex, out var alias);
+
+ list.Add(new NeighborInfo(
+ ipAddress,
+ macAddress,
+ addressFamily == AddressFamily.InterNetworkV6
+ ? ipAddress.IsIPv6Multicast
+ : IPv4Address.IsMulticast(ipAddress),
+ (int)row.InterfaceIndex,
+ alias ?? string.Empty,
+ (NeighborState)row.State,
+ addressFamily));
+ }
+
+ return list;
+ }
+ finally
+ {
+ if (table != IntPtr.Zero)
+ FreeMibTable(table);
+ }
+ }
+
+ ///
+ /// Returns the interface alias map, rebuilding it via
+ /// when the cache has expired or has not yet been populated.
+ ///
+ private static Dictionary GetCachedInterfaceAliasMap()
+ {
+ lock (InterfaceAliasCacheLock)
+ {
+ if (DateTime.UtcNow < _interfaceAliasCacheExpiry && _interfaceAliasCache != null)
+ return _interfaceAliasCache;
+
+ _interfaceAliasCache = BuildInterfaceAliasMap();
+ _interfaceAliasCacheExpiry = DateTime.UtcNow.Add(InterfaceAliasCacheDuration);
+ return _interfaceAliasCache;
+ }
+ }
+
+ ///
+ /// Builds a dictionary that maps an interface index (IPv4 or IPv6) to the
+ /// human-readable interface name (e.g. "Ethernet", "Wi-Fi").
+ ///
+ private static Dictionary BuildInterfaceAliasMap()
+ {
+ var map = new Dictionary();
+
+ foreach (var ni in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
+ {
+ try
+ {
+ var props = ni.GetIPProperties();
+
+ if (ni.Supports(NetworkInterfaceComponent.IPv4))
+ map[props.GetIPv4Properties().Index] = ni.Name;
+
+ if (ni.Supports(NetworkInterfaceComponent.IPv6))
+ map[props.GetIPv6Properties().Index] = ni.Name;
+ }
+ catch (Exception ex)
+ {
+ Log.Warn($"Failed to read interface properties for '{ni.Name}': {ex.Message}");
+ }
+ }
+
+ return map;
+ }
+
+ ///
+ /// Returns the MAC address for by scanning the neighbor
+ /// cache, or when no entry exists. Supports both IPv4 and IPv6.
+ ///
+ public static string GetMACAddress(IPAddress ipAddress)
+ {
+ var entry = GetTable().FirstOrDefault(x => x.IPAddress.Equals(ipAddress));
+ return entry?.MACAddress.ToString();
+ }
+
+ ///
+ /// Adds a permanent neighbor entry for with the given
+ /// by running New-NetNeighbor through the shared
+ /// PowerShell runspace. Requires the application to run with elevated rights.
+ ///
+ /// The IP address (IPv4 or IPv6) of the entry.
+ /// The link-layer address of the entry, separated with -.
+ /// The index of the network interface to add the entry on.
+ ///
+ /// Thrown when the PowerShell pipeline reports one or more errors.
+ ///
+ public static async Task AddEntryAsync(string ipAddress, string macAddress, int interfaceIndex)
+ {
+ await RunspaceLock.WaitAsync();
+ try
+ {
+ await Task.Run(() =>
+ {
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+
+ ps.AddScript($@"New-NetNeighbor -InterfaceIndex {interfaceIndex} -IPAddress '{PowerShellHelper.EscapeSingleQuotes(ipAddress)}' -LinkLayerAddress '{PowerShellHelper.EscapeSingleQuotes(macAddress)}' -State Permanent -ErrorAction Stop | Out-Null");
+ ps.Invoke();
+
+ ThrowOnError(ps);
+ });
+ }
+ finally
+ {
+ RunspaceLock.Release();
+ }
+ }
+
+ ///
+ /// Removes the neighbor entry for by running
+ /// Remove-NetNeighbor through the shared PowerShell runspace. Requires the
+ /// application to run with elevated rights.
+ ///
+ /// The IP address of the entry to remove.
+ /// The index of the network interface the entry belongs to.
+ ///
+ /// Thrown when the PowerShell pipeline reports one or more errors.
+ ///
+ public static async Task DeleteEntryAsync(string ipAddress, int interfaceIndex)
+ {
+ await RunspaceLock.WaitAsync();
+ try
+ {
+ await Task.Run(() =>
+ {
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+
+ ps.AddScript($@"Remove-NetNeighbor -InterfaceIndex {interfaceIndex} -IPAddress '{PowerShellHelper.EscapeSingleQuotes(ipAddress)}' -Confirm:$false -ErrorAction Stop | Out-Null");
+ ps.Invoke();
+
+ ThrowOnError(ps);
+ });
+ }
+ finally
+ {
+ RunspaceLock.Release();
+ }
+ }
+
+ ///
+ /// Clears all dynamic neighbor entries (IPv4 + IPv6) by piping
+ /// Get-NetNeighbor into Remove-NetNeighbor, excluding entries whose
+ /// state is Permanent. Requires the application to run with elevated rights.
+ ///
+ ///
+ /// Thrown when the PowerShell pipeline reports one or more errors.
+ ///
+ public static async Task DeleteTableAsync()
+ {
+ await RunspaceLock.WaitAsync();
+ try
+ {
+ await Task.Run(() =>
+ {
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+
+ ps.AddScript(@"Get-NetNeighbor -ErrorAction SilentlyContinue | Where-Object { $_.State -ne 'Permanent' } | Remove-NetNeighbor -Confirm:$false -ErrorAction Stop | Out-Null");
+ ps.Invoke();
+
+ ThrowOnError(ps);
+ });
+ }
+ finally
+ {
+ RunspaceLock.Release();
+ }
+ }
+
+ ///
+ /// Throws an whose message is the joined PowerShell error
+ /// stream when reported one or more errors.
+ ///
+ private static void ThrowOnError(SMA.PowerShell ps)
+ {
+ if (ps.Streams.Error.Count == 0)
+ return;
+
+ var message = string.Join(Environment.NewLine, ps.Streams.Error);
+
+ Log.Warn($"PowerShell error: {message}");
+
+ throw new Exception(message);
+ }
+
+ #endregion
+}
diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
index 51623e151f..5303a341df 100644
--- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
+++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
@@ -275,10 +275,10 @@ public static class GlobalStaticConfiguration
public static AutoRefreshTimeInfo Listeners_AutoRefreshTime =>
AutoRefreshTime.GetDefaults.First(x => x.Value == 30 && x.TimeUnit == TimeUnit.Second);
- // Application: ARP Table
- public static ExportFileType ARPTable_ExportFileType => ExportFileType.Csv;
+ // Application: Neighbor Table
+ public static ExportFileType NeighborTable_ExportFileType => ExportFileType.Csv;
- public static AutoRefreshTimeInfo ARPTable_AutoRefreshTime =>
+ public static AutoRefreshTimeInfo NeighborTable_AutoRefreshTime =>
AutoRefreshTime.GetDefaults.First(x => x.Value == 30 && x.TimeUnit == TimeUnit.Second);
#endregion
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index 7d0caae60e..83a2568dc5 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -3917,9 +3917,9 @@ public ExportFileType Listeners_ExportFileType
#endregion
- #region ARPTable
+ #region NeighborTable
- public bool ARPTable_AutoRefreshEnabled
+ public bool NeighborTable_AutoRefreshEnabled
{
get;
set
@@ -3932,7 +3932,7 @@ public bool ARPTable_AutoRefreshEnabled
}
}
- public AutoRefreshTimeInfo ARPTable_AutoRefreshTime
+ public AutoRefreshTimeInfo NeighborTable_AutoRefreshTime
{
get;
set
@@ -3943,9 +3943,9 @@ public AutoRefreshTimeInfo ARPTable_AutoRefreshTime
field = value;
OnPropertyChanged();
}
- } = GlobalStaticConfiguration.ARPTable_AutoRefreshTime;
+ } = GlobalStaticConfiguration.NeighborTable_AutoRefreshTime;
- public string ARPTable_ExportFilePath
+ public string NeighborTable_ExportFilePath
{
get;
set
@@ -3958,7 +3958,7 @@ public string ARPTable_ExportFilePath
}
}
- public ExportFileType ARPTable_ExportFileType
+ public ExportFileType NeighborTable_ExportFileType
{
get;
set
@@ -3969,7 +3969,20 @@ public ExportFileType ARPTable_ExportFileType
field = value;
OnPropertyChanged();
}
- } = GlobalStaticConfiguration.ARPTable_ExportFileType;
+ } = GlobalStaticConfiguration.NeighborTable_ExportFileType;
+
+ public string NeighborTable_InterfaceName
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
#endregion
diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs
index 7b8a446e48..dc3493591a 100644
--- a/Source/NETworkManager.Settings/SettingsManager.cs
+++ b/Source/NETworkManager.Settings/SettingsManager.cs
@@ -680,6 +680,24 @@ private static void UpgradeToLatest(Version version)
Current.General_ApplicationList.Insert(
ApplicationManager.GetDefaultList().ToList().FindIndex(x => x.Name == ApplicationName.Firewall),
ApplicationManager.GetDefaultList().First(x => x.Name == ApplicationName.Firewall));
+
+ // Replace ARPTable with NeighborTable
+#pragma warning disable CS0618
+ var arpTableEntry = Current.General_ApplicationList.FirstOrDefault(x => x.Name == ApplicationName.ARPTable);
+#pragma warning restore CS0618
+
+ if (arpTableEntry != null)
+ {
+ Log.Info("Replacing obsolete \"ARPTable\" app entry with \"NeighborTable\"...");
+
+ var index = Current.General_ApplicationList.IndexOf(arpTableEntry);
+ var neighborTableEntry = ApplicationManager.GetDefaultList().First(x => x.Name == ApplicationName.NeighborTable);
+
+ neighborTableEntry.IsVisible = arpTableEntry.IsVisible;
+ neighborTableEntry.IsDefault = arpTableEntry.IsDefault;
+
+ Current.General_ApplicationList[index] = neighborTableEntry;
+ }
}
#endregion
}
diff --git a/Source/NETworkManager.Utilities/PowerShellHelper.cs b/Source/NETworkManager.Utilities/PowerShellHelper.cs
index a888690556..dea4df89db 100644
--- a/Source/NETworkManager.Utilities/PowerShellHelper.cs
+++ b/Source/NETworkManager.Utilities/PowerShellHelper.cs
@@ -84,4 +84,10 @@ public static void ExecuteCommand(string command, bool asAdmin = false, ProcessW
}
}
}
+
+ ///
+ /// Escapes a string for safe embedding inside a PowerShell single-quoted string
+ /// by doubling any single-quote characters.
+ ///
+ public static string EscapeSingleQuotes(string value) => value.Replace("'", "''");
}
diff --git a/Source/NETworkManager/MainWindow.xaml.cs b/Source/NETworkManager/MainWindow.xaml.cs
index 24862c1e4f..264269aec4 100644
--- a/Source/NETworkManager/MainWindow.xaml.cs
+++ b/Source/NETworkManager/MainWindow.xaml.cs
@@ -667,7 +667,7 @@ private void LoadApplicationList()
private IPGeolocationHostView _ipGeolocationHostView;
private ConnectionsView _connectionsView;
private ListenersView _listenersView;
- private ARPTableView _arpTableView;
+ private NeighborTableView _neighborTableView;
///
/// Method when the application view becomes visible (again). Either when switching the applications
@@ -886,13 +886,13 @@ private void OnApplicationViewVisible(ApplicationName name)
ContentControlApplication.Content = _listenersView;
break;
- case ApplicationName.ARPTable:
- if (_arpTableView == null)
- _arpTableView = new ARPTableView();
+ case ApplicationName.NeighborTable:
+ if (_neighborTableView == null)
+ _neighborTableView = new NeighborTableView();
else
- _arpTableView.OnViewVisible();
+ _neighborTableView.OnViewVisible();
- ContentControlApplication.Content = _arpTableView;
+ ContentControlApplication.Content = _neighborTableView;
break;
default:
Log.Error("Cannot show unknown application view: " + name);
@@ -982,8 +982,8 @@ private void OnApplicationViewHide(ApplicationName name)
case ApplicationName.Listeners:
_listenersView?.OnViewHide();
break;
- case ApplicationName.ARPTable:
- _arpTableView?.OnViewHide();
+ case ApplicationName.NeighborTable:
+ _neighborTableView?.OnViewHide();
break;
default:
Log.Error("Cannot hide unknown application view: " + name);
@@ -1083,7 +1083,7 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro
case ApplicationName.Lookup:
case ApplicationName.Connections:
case ApplicationName.Listeners:
- case ApplicationName.ARPTable:
+ case ApplicationName.NeighborTable:
break;
default:
Log.Error($"Cannot redirect data to unknown application: {data.Application}");
diff --git a/Source/NETworkManager/ViewModels/ARPTableAddEntryViewModel.cs b/Source/NETworkManager/ViewModels/ARPTableAddEntryViewModel.cs
deleted file mode 100644
index bb93a36fa0..0000000000
--- a/Source/NETworkManager/ViewModels/ARPTableAddEntryViewModel.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using System;
-using System.Windows.Input;
-using NETworkManager.Utilities;
-
-namespace NETworkManager.ViewModels;
-
-///
-/// View model for adding an ARP table entry.
-///
-public class ARPTableAddEntryViewModel : ViewModelBase
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The action to execute when the add command is invoked.
- /// The action to execute when the cancel command is invoked.
- public ARPTableAddEntryViewModel(Action addCommand,
- Action cancelHandler)
- {
- AddCommand = new RelayCommand(_ => addCommand(this));
- CancelCommand = new RelayCommand(_ => cancelHandler(this));
- }
-
- ///
- /// Gets the command to add the entry.
- ///
- public ICommand AddCommand { get; }
-
- ///
- /// Gets the command to cancel the operation.
- ///
- public ICommand CancelCommand { get; }
-
- ///
- /// Gets or sets the IP address.
- ///
- public string IPAddress
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
- ///
- /// Gets or sets the MAC address.
- ///
- public string MACAddress
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager/ViewModels/NeighborTableAddEntryViewModel.cs b/Source/NETworkManager/ViewModels/NeighborTableAddEntryViewModel.cs
new file mode 100644
index 0000000000..2ae3608504
--- /dev/null
+++ b/Source/NETworkManager/ViewModels/NeighborTableAddEntryViewModel.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Input;
+using NETworkManager.Settings;
+using NETworkManager.Utilities;
+
+namespace NETworkManager.ViewModels;
+
+///
+/// View model for adding a neighbor table entry.
+///
+public class NeighborTableAddEntryViewModel : ViewModelBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The action to execute when the add command is invoked.
+ /// The action to execute when the cancel command is invoked.
+ /// Available network interfaces to select from.
+ public NeighborTableAddEntryViewModel(Action addCommand,
+ Action cancelHandler,
+ List> interfaces)
+ {
+ AddCommand = new RelayCommand(_ => addCommand(this));
+ CancelCommand = new RelayCommand(_ => cancelHandler(this));
+
+ Interfaces = interfaces;
+
+ if (interfaces.Count > 0)
+ {
+ var lastUsed = interfaces.FirstOrDefault(x =>
+ x.Value == SettingsManager.Current.NeighborTable_InterfaceName);
+
+ SelectedInterface = lastUsed.Value != null ? lastUsed : interfaces[0];
+ }
+ }
+
+ ///
+ /// Gets the command to add the entry.
+ ///
+ public ICommand AddCommand { get; }
+
+ ///
+ /// Gets the command to cancel the operation.
+ ///
+ public ICommand CancelCommand { get; }
+
+ ///
+ /// Gets or sets the IP address.
+ ///
+ public string IPAddress
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the MAC address.
+ ///
+ public string MACAddress
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets the list of available network interfaces.
+ ///
+ public List> Interfaces { get; }
+
+ ///
+ /// Gets or sets the selected network interface.
+ ///
+ public KeyValuePair SelectedInterface
+ {
+ get;
+ set
+ {
+ if (value.Equals(field))
+ return;
+
+ field = value;
+
+ SettingsManager.Current.NeighborTable_InterfaceName = value.Value;
+
+ OnPropertyChanged();
+ }
+ }
+}
diff --git a/Source/NETworkManager/ViewModels/ARPTableViewModel.cs b/Source/NETworkManager/ViewModels/NeighborTableViewModel.cs
similarity index 58%
rename from Source/NETworkManager/ViewModels/ARPTableViewModel.cs
rename to Source/NETworkManager/ViewModels/NeighborTableViewModel.cs
index d507a38899..48bdbe7371 100644
--- a/Source/NETworkManager/ViewModels/ARPTableViewModel.cs
+++ b/Source/NETworkManager/ViewModels/NeighborTableViewModel.cs
@@ -22,24 +22,23 @@
namespace NETworkManager.ViewModels;
///
-/// View model for the ARP table view.
+/// View model for the neighbor table view (IPv4 ARP + IPv6 NDP).
///
-public class ARPTableViewModel : ViewModelBase
+public class NeighborTableViewModel : ViewModelBase
{
#region Contructor, load settings
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// The dialog coordinator instance.
- public ARPTableViewModel()
+ public NeighborTableViewModel()
{
_isLoading = true;
// Result view + search
ResultsView = CollectionViewSource.GetDefaultView(Results);
- ((ListCollectionView)ResultsView).CustomSort = Comparer.Create((x, y) =>
+ ((ListCollectionView)ResultsView).CustomSort = Comparer.Create((x, y) =>
IPAddressHelper.CompareIPAddresses(x.IPAddress, y.IPAddress));
ResultsView.Filter = o =>
@@ -47,18 +46,22 @@ public ARPTableViewModel()
if (string.IsNullOrEmpty(Search))
return true;
- if (o is not ARPInfo info)
+ if (o is not NeighborInfo info)
return false;
- // Search by IPAddress and MACAddress
+ var stateLocalized = ResourceTranslate(info.State);
+
return info.IPAddress.ToString().IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 ||
info.MACAddress.ToString().IndexOf(Search.Replace("-", "").Replace(":", ""),
StringComparison.OrdinalIgnoreCase) > -1 ||
+ (info.InterfaceAlias ?? string.Empty).IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 ||
+ info.State.ToString().IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 ||
+ stateLocalized.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 ||
(info.IsMulticast ? Strings.Yes : Strings.No).IndexOf(
Search, StringComparison.OrdinalIgnoreCase) > -1;
};
- // Get ARP table
+ // Get neighbor table
Refresh(true).ConfigureAwait(false);
// Auto refresh
@@ -66,32 +69,27 @@ public ARPTableViewModel()
AutoRefreshTimes = CollectionViewSource.GetDefaultView(AutoRefreshTime.GetDefaults);
SelectedAutoRefreshTime = AutoRefreshTimes.SourceCollection.Cast().FirstOrDefault(x =>
- x.Value == SettingsManager.Current.ARPTable_AutoRefreshTime.Value &&
- x.TimeUnit == SettingsManager.Current.ARPTable_AutoRefreshTime.TimeUnit);
- AutoRefreshEnabled = SettingsManager.Current.ARPTable_AutoRefreshEnabled;
+ x.Value == SettingsManager.Current.NeighborTable_AutoRefreshTime.Value &&
+ x.TimeUnit == SettingsManager.Current.NeighborTable_AutoRefreshTime.TimeUnit);
+ AutoRefreshEnabled = SettingsManager.Current.NeighborTable_AutoRefreshEnabled;
_isLoading = false;
}
+ private static string ResourceTranslate(NeighborState state)
+ {
+ return Localization.ResourceTranslator.Translate(Localization.ResourceIdentifier.NeighborState, state);
+ }
#endregion
#region Variables
- private static readonly ILog Log = LogManager.GetLogger(typeof(ARPTableViewModel));
+ private static readonly ILog Log = LogManager.GetLogger(typeof(NeighborTableViewModel));
- ///
- /// Indicates whether the view model is loading.
- ///
private readonly bool _isLoading;
- ///
- /// The timer for auto-refresh.
- ///
private readonly DispatcherTimer _autoRefreshTimer = new();
- ///
- /// Gets or sets the search text.
- ///
public string Search
{
get;
@@ -108,10 +106,7 @@ public string Search
}
}
- ///
- /// Gets or sets the collection of ARP results.
- ///
- public ObservableCollection Results
+ public ObservableCollection Results
{
get;
set
@@ -124,15 +119,9 @@ public ObservableCollection Results
}
} = [];
- ///
- /// Gets the collection view for the ARP results.
- ///
public ICollectionView ResultsView { get; }
- ///
- /// Gets or sets the currently selected ARP result.
- ///
- public ARPInfo SelectedResult
+ public NeighborInfo SelectedResult
{
get;
set
@@ -145,9 +134,6 @@ public ARPInfo SelectedResult
}
}
- ///
- /// Gets or sets the list of selected ARP results.
- ///
public IList SelectedResults
{
get;
@@ -161,9 +147,6 @@ public IList SelectedResults
}
} = new ArrayList();
- ///
- /// Gets or sets a value indicating whether auto-refresh is enabled.
- ///
public bool AutoRefreshEnabled
{
get;
@@ -173,11 +156,10 @@ public bool AutoRefreshEnabled
return;
if (!_isLoading)
- SettingsManager.Current.ARPTable_AutoRefreshEnabled = value;
+ SettingsManager.Current.NeighborTable_AutoRefreshEnabled = value;
field = value;
- // Start timer to refresh automatically
if (value)
{
_autoRefreshTimer.Interval = AutoRefreshTime.CalculateTimeSpan(SelectedAutoRefreshTime);
@@ -192,14 +174,8 @@ public bool AutoRefreshEnabled
}
}
- ///
- /// Gets the collection view for the auto-refresh times.
- ///
public ICollectionView AutoRefreshTimes { get; }
- ///
- /// Gets or sets the selected auto-refresh time.
- ///
public AutoRefreshTimeInfo SelectedAutoRefreshTime
{
get;
@@ -209,7 +185,7 @@ public AutoRefreshTimeInfo SelectedAutoRefreshTime
return;
if (!_isLoading)
- SettingsManager.Current.ARPTable_AutoRefreshTime = value;
+ SettingsManager.Current.NeighborTable_AutoRefreshTime = value;
field = value;
@@ -223,9 +199,6 @@ public AutoRefreshTimeInfo SelectedAutoRefreshTime
}
}
- ///
- /// Gets or sets a value indicating whether the view model is currently refreshing.
- ///
public bool IsRefreshing
{
get;
@@ -239,9 +212,19 @@ public bool IsRefreshing
}
}
- ///
- /// Gets or sets a value indicating whether the status message is displayed.
- ///
+ public bool IsModifying
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
public bool IsStatusMessageDisplayed
{
get;
@@ -255,9 +238,6 @@ public bool IsStatusMessageDisplayed
}
}
- ///
- /// Gets the status message.
- ///
public string StatusMessage
{
get;
@@ -275,28 +255,18 @@ private set
#region ICommands & Actions
- ///
- /// Gets the command to refresh the ARP table.
- ///
public ICommand RefreshCommand => new RelayCommand(_ => RefreshAction().ConfigureAwait(false), Refresh_CanExecute);
- ///
- /// Checks if the refresh command can be executed.
- ///
- /// The command parameter.
- /// true if the command can be executed; otherwise, false.
private bool Refresh_CanExecute(object parameter)
{
return Application.Current.MainWindow != null &&
!((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
!ConfigurationManager.Current.IsChildWindowOpen &&
!IsRefreshing &&
+ !IsModifying &&
!AutoRefreshEnabled;
}
- ///
- /// Action to refresh the ARP table.
- ///
private async Task RefreshAction()
{
IsStatusMessageDisplayed = false;
@@ -304,38 +274,17 @@ private async Task RefreshAction()
await Refresh();
}
- ///
- /// Gets the command to delete the ARP table.
- ///
public ICommand DeleteTableCommand =>
- new RelayCommand(_ => DeleteTableAction().ConfigureAwait(false), DeleteTable_CanExecute);
+ new RelayCommand(_ => DeleteTableAction().ConfigureAwait(false), ModifyEntry_CanExecute);
- ///
- /// Checks if the delete table command can be executed.
- ///
- /// The command parameter.
- /// true if the command can be executed; otherwise, false.
- private bool DeleteTable_CanExecute(object parameter)
- {
- return Application.Current.MainWindow != null &&
- !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
- !ConfigurationManager.Current.IsChildWindowOpen;
- }
-
- ///
- /// Action to delete the ARP table.
- ///
private async Task DeleteTableAction()
{
+ IsModifying = true;
IsStatusMessageDisplayed = false;
try
{
- var arpTable = new ARP();
-
- arpTable.UserHasCanceled += ArpTable_UserHasCanceled;
-
- await arpTable.DeleteTableAsync();
+ await NeighborTable.DeleteTableAsync();
await Refresh();
}
@@ -344,40 +293,23 @@ private async Task DeleteTableAction()
StatusMessage = ex.Message;
IsStatusMessageDisplayed = true;
}
+ finally
+ {
+ IsModifying = false;
+ }
}
- ///
- /// Gets the command to delete an ARP entry.
- ///
public ICommand DeleteEntryCommand =>
- new RelayCommand(_ => DeleteEntryAction().ConfigureAwait(false), DeleteEntry_CanExecute);
+ new RelayCommand(_ => DeleteEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute);
- ///
- /// Checks if the delete entry command can be executed.
- ///
- /// The command parameter.
- /// true if the command can be executed; otherwise, false.
- private bool DeleteEntry_CanExecute(object parameter)
- {
- return Application.Current.MainWindow != null &&
- !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
- !ConfigurationManager.Current.IsChildWindowOpen;
- }
-
- ///
- /// Action to delete an ARP entry.
- ///
private async Task DeleteEntryAction()
{
+ IsModifying = true;
IsStatusMessageDisplayed = false;
try
{
- var arpTable = new ARP();
-
- arpTable.UserHasCanceled += ArpTable_UserHasCanceled;
-
- await arpTable.DeleteEntryAsync(SelectedResult.IPAddress.ToString());
+ await NeighborTable.DeleteEntryAsync(SelectedResult.IPAddress.ToString(), SelectedResult.InterfaceIndex);
await Refresh();
}
@@ -386,48 +318,32 @@ private async Task DeleteEntryAction()
StatusMessage = ex.Message;
IsStatusMessageDisplayed = true;
}
+ finally
+ {
+ IsModifying = false;
+ }
}
- ///
- /// Gets the command to add an ARP entry.
- ///
public ICommand AddEntryCommand =>
- new RelayCommand(_ => AddEntryAction().ConfigureAwait(false), AddEntry_CanExecute);
-
- ///
- /// Checks if the add entry command can be executed.
- ///
- /// The command parameter.
- /// true if the command can be executed; otherwise, false.
- private bool AddEntry_CanExecute(object parameter)
- {
- return Application.Current.MainWindow != null &&
- !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
- !ConfigurationManager.Current.IsChildWindowOpen;
- }
+ new RelayCommand(_ => AddEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute);
- ///
- /// Action to add an ARP entry.
- ///
private async Task AddEntryAction()
{
+ IsModifying = true;
IsStatusMessageDisplayed = false;
- var childWindow = new ARPTableAddEntryChildWindow();
+ var interfaces = await NeighborTable.GetInterfacesAsync();
+ var childWindow = new NeighborTableAddEntryChildWindow();
- var childWindowViewModel = new ARPTableAddEntryViewModel(async instance =>
+ var childWindowViewModel = new NeighborTableAddEntryViewModel(async instance =>
{
childWindow.IsOpen = false;
ConfigurationManager.Current.IsChildWindowOpen = false;
try
{
- var arpTable = new ARP();
-
- arpTable.UserHasCanceled += ArpTable_UserHasCanceled;
-
- await arpTable.AddEntryAsync(instance.IPAddress, MACAddressHelper.Format(instance.MACAddress, "-"));
+ await NeighborTable.AddEntryAsync(instance.IPAddress, MACAddressHelper.Format(instance.MACAddress, "-"), instance.SelectedInterface.Key);
await Refresh();
}
@@ -436,11 +352,17 @@ private async Task AddEntryAction()
StatusMessage = ex.Message;
IsStatusMessageDisplayed = true;
}
+ finally
+ {
+ IsModifying = false;
+ }
}, _ =>
{
childWindow.IsOpen = false;
ConfigurationManager.Current.IsChildWindowOpen = false;
- });
+
+ IsModifying = false;
+ }, interfaces);
childWindow.Title = Strings.AddEntry;
@@ -451,14 +373,33 @@ private async Task AddEntryAction()
await Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
}
- ///
- /// Gets the command to export the ARP table.
- ///
+ private bool ModifyEntry_CanExecute(object parameter)
+ {
+ return ConfigurationManager.Current.IsAdmin &&
+ Application.Current.MainWindow != null &&
+ !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
+ !ConfigurationManager.Current.IsChildWindowOpen &&
+ !IsRefreshing &&
+ !IsModifying;
+ }
+
+ public ICommand RestartAsAdminCommand => new RelayCommand(_ => RestartAsAdminAction().ConfigureAwait(false));
+
+ private async Task RestartAsAdminAction()
+ {
+ try
+ {
+ (Application.Current.MainWindow as MainWindow)?.RestartApplication(true);
+ }
+ catch (Exception ex)
+ {
+ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Error, ex.Message,
+ ChildWindowIcon.Error);
+ }
+ }
+
public ICommand ExportCommand => new RelayCommand(_ => ExportAction().ConfigureAwait(false));
- ///
- /// Action to export the ARP table.
- ///
private Task ExportAction()
{
var childWindow = new ExportChildWindow();
@@ -473,7 +414,7 @@ private Task ExportAction()
ExportManager.Export(instance.FilePath, instance.FileType,
instance.ExportAll
? Results
- : new ObservableCollection(SelectedResults.Cast().ToArray()));
+ : new ObservableCollection(SelectedResults.Cast().ToArray()));
}
catch (Exception ex)
{
@@ -484,16 +425,16 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro
Environment.NewLine + ex.Message, ChildWindowIcon.Error);
}
- SettingsManager.Current.ARPTable_ExportFileType = instance.FileType;
- SettingsManager.Current.ARPTable_ExportFilePath = instance.FilePath;
+ SettingsManager.Current.NeighborTable_ExportFileType = instance.FileType;
+ SettingsManager.Current.NeighborTable_ExportFilePath = instance.FilePath;
}, _ =>
{
childWindow.IsOpen = false;
ConfigurationManager.Current.IsChildWindowOpen = false;
}, [
ExportFileType.Csv, ExportFileType.Xml, ExportFileType.Json
- ], true, SettingsManager.Current.ARPTable_ExportFileType,
- SettingsManager.Current.ARPTable_ExportFilePath);
+ ], true, SettingsManager.Current.NeighborTable_ExportFileType,
+ SettingsManager.Current.NeighborTable_ExportFilePath);
childWindow.Title = Strings.Export;
@@ -508,10 +449,6 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro
#region Methods
- ///
- /// Refreshes the ARP table.
- ///
- /// Indicates whether this is the initial refresh.
private async Task Refresh(bool init = false)
{
IsRefreshing = true;
@@ -519,12 +456,12 @@ private async Task Refresh(bool init = false)
StatusMessage = Strings.RefreshingDots;
IsStatusMessageDisplayed = true;
- if (init == false)
+ if (!init)
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
Results.Clear();
- (await ARP.GetTableAsync()).ForEach(Results.Add);
+ (await NeighborTable.GetTableAsync()).ForEach(Results.Add);
StatusMessage = string.Format(Strings.ReloadedAtX, DateTime.Now.ToShortTimeString());
IsStatusMessageDisplayed = true;
@@ -532,22 +469,14 @@ private async Task Refresh(bool init = false)
IsRefreshing = false;
}
- ///
- /// Called when the view becomes visible.
- ///
public void OnViewVisible()
{
- // Restart timer...
if (AutoRefreshEnabled)
_autoRefreshTimer.Start();
}
- ///
- /// Called when the view is hidden.
- ///
public void OnViewHide()
{
- // Temporarily stop timer...
if (AutoRefreshEnabled)
_autoRefreshTimer.Stop();
}
@@ -556,29 +485,17 @@ public void OnViewHide()
#region Events
- ///
- /// Handles the UserHasCanceled event from the ARP table.
- ///
- private void ArpTable_UserHasCanceled(object sender, EventArgs e)
- {
- StatusMessage = Strings.CanceledByUserMessage;
- IsStatusMessageDisplayed = true;
- }
-
- ///
- /// Handles the Tick event of the auto-refresh timer.
- ///
private async void AutoRefreshTimer_Tick(object sender, EventArgs e)
{
- // Stop timer...
_autoRefreshTimer.Stop();
- // Refresh
- await Refresh();
+ // Skip refresh while a modify operation (add/delete) is in progress to avoid
+ // clearing the table while the user is interacting with it.
+ if (!IsModifying)
+ await Refresh();
- // Restart timer...
_autoRefreshTimer.Start();
}
#endregion
-}
\ No newline at end of file
+}
diff --git a/Source/NETworkManager/Views/FirewallView.xaml.cs b/Source/NETworkManager/Views/FirewallView.xaml.cs
index 4565a26677..7b89181733 100644
--- a/Source/NETworkManager/Views/FirewallView.xaml.cs
+++ b/Source/NETworkManager/Views/FirewallView.xaml.cs
@@ -1,6 +1,5 @@
using System.Windows;
using System.Windows.Controls;
-using System.Windows.Input;
using System.Windows.Media;
using NETworkManager.ViewModels;
diff --git a/Source/NETworkManager/Views/IPScannerSettingsView.xaml b/Source/NETworkManager/Views/IPScannerSettingsView.xaml
index 9f6aaf738f..3d3cc946a3 100644
--- a/Source/NETworkManager/Views/IPScannerSettingsView.xaml
+++ b/Source/NETworkManager/Views/IPScannerSettingsView.xaml
@@ -54,7 +54,7 @@
Margin="0,0,0,10" />
-
+
diff --git a/Source/NETworkManager/Views/IPScannerView.xaml b/Source/NETworkManager/Views/IPScannerView.xaml
index 5ec9d99894..d157d218c7 100644
--- a/Source/NETworkManager/Views/IPScannerView.xaml
+++ b/Source/NETworkManager/Views/IPScannerView.xaml
@@ -581,13 +581,11 @@
-
-
-
+
-
+
-
-
+
+
-
-
@@ -631,7 +626,7 @@
-
+
@@ -644,15 +639,15 @@
-
+
-
+
-
+
@@ -663,43 +658,43 @@
-
+
-
-
-
+
-
-
-
-
+
+
-
-
@@ -709,7 +704,7 @@
Style="{StaticResource InfoTextBlock}"
Margin="10,0,0,0" />
-
@@ -773,18 +764,18 @@
-
-
+
+
-
-
@@ -794,19 +785,16 @@
Style="{StaticResource InfoTextBlock}"
Margin="10,0,0,0" />
-
@@ -818,7 +806,7 @@
-
+
@@ -831,12 +819,12 @@
-
+
+ Text="{x:Static Member=localization:Strings.GroupDomain}" />
@@ -844,7 +832,7 @@
-
+
@@ -854,155 +842,6 @@
Text="{Binding Path=(network:IPScannerHostInfo.NetBIOSInfo).UserName, Converter={StaticResource ResourceKey=StringNullOrEmptyToStringConverter}}" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/NETworkManager/Views/ARPTableAddEntryChildWindow.xaml b/Source/NETworkManager/Views/NeighborTableAddEntryChildWindow.xaml
similarity index 84%
rename from Source/NETworkManager/Views/ARPTableAddEntryChildWindow.xaml
rename to Source/NETworkManager/Views/NeighborTableAddEntryChildWindow.xaml
index 60bc100268..3990e8a26c 100644
--- a/Source/NETworkManager/Views/ARPTableAddEntryChildWindow.xaml
+++ b/Source/NETworkManager/Views/NeighborTableAddEntryChildWindow.xaml
@@ -1,4 +1,4 @@
-
+ mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:NeighborTableAddEntryViewModel}">
@@ -33,6 +32,8 @@
+
+
-
+
+ mah:TextBoxHelper.Watermark="{x:Static localization:StaticStrings.ExampleMACAddress}">
@@ -58,6 +59,12 @@
+
+
-
\ No newline at end of file
+
diff --git a/Source/NETworkManager/Views/ARPTableAddEntryChildWindow.xaml.cs b/Source/NETworkManager/Views/NeighborTableAddEntryChildWindow.xaml.cs
similarity index 75%
rename from Source/NETworkManager/Views/ARPTableAddEntryChildWindow.xaml.cs
rename to Source/NETworkManager/Views/NeighborTableAddEntryChildWindow.xaml.cs
index 1589e99192..2e7d9815ae 100644
--- a/Source/NETworkManager/Views/ARPTableAddEntryChildWindow.xaml.cs
+++ b/Source/NETworkManager/Views/NeighborTableAddEntryChildWindow.xaml.cs
@@ -1,12 +1,12 @@
-using System;
+using System;
using System.Windows;
using System.Windows.Threading;
namespace NETworkManager.Views;
-public partial class ARPTableAddEntryChildWindow
+public partial class NeighborTableAddEntryChildWindow
{
- public ARPTableAddEntryChildWindow()
+ public NeighborTableAddEntryChildWindow()
{
InitializeComponent();
}
@@ -18,4 +18,4 @@ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e)
TextBoxIPAddress.Focus();
}));
}
-}
\ No newline at end of file
+}
diff --git a/Source/NETworkManager/Views/ARPTableView.xaml b/Source/NETworkManager/Views/NeighborTableView.xaml
similarity index 77%
rename from Source/NETworkManager/Views/ARPTableView.xaml
rename to Source/NETworkManager/Views/NeighborTableView.xaml
index d32fab70d1..0497fa3e5a 100644
--- a/Source/NETworkManager/Views/ARPTableView.xaml
+++ b/Source/NETworkManager/Views/NeighborTableView.xaml
@@ -1,4 +1,4 @@
-
+
+
-
+
@@ -35,6 +38,7 @@
+
@@ -43,7 +47,7 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Views/ARPTableView.xaml.cs b/Source/NETworkManager/Views/NeighborTableView.xaml.cs
similarity index 69%
rename from Source/NETworkManager/Views/ARPTableView.xaml.cs
rename to Source/NETworkManager/Views/NeighborTableView.xaml.cs
index d65d071412..096a60667e 100644
--- a/Source/NETworkManager/Views/ARPTableView.xaml.cs
+++ b/Source/NETworkManager/Views/NeighborTableView.xaml.cs
@@ -1,4 +1,4 @@
-using System.Collections;
+using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
@@ -9,11 +9,11 @@
namespace NETworkManager.Views;
-public partial class ARPTableView
+public partial class NeighborTableView
{
- private readonly ARPTableViewModel _viewModel = new();
+ private readonly NeighborTableViewModel _viewModel = new();
- public ARPTableView()
+ public NeighborTableView()
{
InitializeComponent();
DataContext = _viewModel;
@@ -43,12 +43,18 @@ private void DataGrid_OnSorting(object sender, DataGridSortingEventArgs e)
switch (column.SortMemberPath)
{
- case nameof(ARPInfo.IPAddress):
+ case nameof(NeighborInfo.IPAddress):
selectedComparer = 0;
break;
- case nameof(ARPInfo.MACAddress):
+ case nameof(NeighborInfo.MACAddress):
selectedComparer = 1;
break;
+ case nameof(NeighborInfo.InterfaceAlias):
+ selectedComparer = 2;
+ break;
+ case nameof(NeighborInfo.State):
+ selectedComparer = 3;
+ break;
default:
return;
}
@@ -80,10 +86,9 @@ public int Compare(object x, object y)
return 0;
// Get data from objects
- if (x is not ARPInfo first || y is not ARPInfo second)
+ if (x is not NeighborInfo first || y is not NeighborInfo second)
return 0;
- // Compare the data
return comparer switch
{
// IP address
@@ -94,8 +99,16 @@ public int Compare(object x, object y)
1 => direction == ListSortDirection.Ascending
? MACAddressHelper.CompareMACAddresses(first.MACAddress, second.MACAddress)
: MACAddressHelper.CompareMACAddresses(second.MACAddress, first.MACAddress),
+ // Interface alias
+ 2 => direction == ListSortDirection.Ascending
+ ? string.Compare(first.InterfaceAlias, second.InterfaceAlias, System.StringComparison.OrdinalIgnoreCase)
+ : string.Compare(second.InterfaceAlias, first.InterfaceAlias, System.StringComparison.OrdinalIgnoreCase),
+ // State
+ 3 => direction == ListSortDirection.Ascending
+ ? ((int)first.State).CompareTo((int)second.State)
+ : ((int)second.State).CompareTo((int)first.State),
_ => 0
};
}
}
-}
\ No newline at end of file
+}
diff --git a/Website/blog/2026-04-30-introducing-firewall/firewall-rule.png b/Website/blog/2026-04-30-introducing-firewall/firewall-rule.png
index 676c751182..f3155ecf43 100644
Binary files a/Website/blog/2026-04-30-introducing-firewall/firewall-rule.png and b/Website/blog/2026-04-30-introducing-firewall/firewall-rule.png differ
diff --git a/Website/blog/2026-04-30-introducing-firewall/firewall.png b/Website/blog/2026-04-30-introducing-firewall/firewall.png
index 692510b0f6..f4ad75b745 100644
Binary files a/Website/blog/2026-04-30-introducing-firewall/firewall.png and b/Website/blog/2026-04-30-introducing-firewall/firewall.png differ
diff --git a/Website/docs/application/arp-table.md b/Website/docs/application/arp-table.md
deleted file mode 100644
index 2cada783e9..0000000000
--- a/Website/docs/application/arp-table.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-sidebar_position: 27
-description: "View and manage IP-to-MAC address mappings in the ARP cache with NETworkManager. Add or delete ARP entries and understand ARP protocol behavior."
-keywords: [NETworkManager, ARP table, ARP cache, IP to MAC, MAC address, ARP protocol, address resolution]
----
-
-# ARP Table
-
-The **ARP table** shows you the IP address and MAC address of all devices on your network with which the computer has already established a connection.
-
-:::info
-
-ARP (Address Resolution Protocol) is a layer 2 protocol for mapping IP addresses to MAC addresses. The ARP table is a list of all IP addresses and the corresponding MAC addresses of the devices on the network. When a device needs to send data to a specific IP address, it first checks its ARP table to see if it already has the MAC address for that IP address. If the MAC address is not found in the ARP table, the device will send a broadcast message called an ARP request to the network asking which device owns that IP address. The device that owns the IP address will then respond with its MAC address, and the requesting device will update its ARP table with the new mapping. ARP cache poisoning attacks can manipulate the contents of the ARP table, leading to security issues.
-
-:::
-
-
-
-:::note
-
-In addition, further actions can be performed using the buttons below:
-
-- **Add entry...** - Opens a dialog to add an entry to the ARP table.
-- **Delete table** - Delete all entries from the ARP table.
-
-:::
-
-:::note
-
-With `F5` you can refresh the ARP table.
-
-Right-click on the result to delete an entry, or to copy or export the information.
-
-:::
-
-## Add entry
-
-The **Add entry** dialog is opened by clicking the **Add entry...** button below the table. It creates a new static ARP entry that maps an IP address to a MAC address.
-
-
-
-### IP address
-
-IPv4 address of the device.
-
-**Type:** `String`
-
-**Default:** `Empty`
-
-**Example:** `10.0.0.10`
-
-:::note
-
-Only IPv4 addresses are accepted. The field is required and validated for a correct address format.
-
-:::
-
-### MAC address
-
-MAC address of the device the [IP address](#ip-address) should be mapped to.
-
-**Type:** `String`
-
-**Default:** `Empty`
-
-**Example:**
-
-- `00:1A:2B:3C:4D:5E`
-- `00-1A-2B-3C-4D-5E`
-
-:::note
-
-The field is required and validated for a correct MAC address format.
-
-:::
-
-:::note
-
-Adding a static ARP entry requires administrator privileges and runs `arp -s` under the hood.
-
-:::
diff --git a/Website/docs/application/ip-scanner.md b/Website/docs/application/ip-scanner.md
index b70edca3bb..35b50a875a 100644
--- a/Website/docs/application/ip-scanner.md
+++ b/Website/docs/application/ip-scanner.md
@@ -183,7 +183,7 @@ Resolve the MAC address and vendor for each IP address.
:::note
-Due to the fact that the MAC address is resolved via ARP, the device must be in the same subnet as the IP address.
+The MAC address is resolved via ARP (IPv4) or NDP (IPv6) from the neighbor cache. If no entry is found there, NetBIOS is used as a fallback. Because ARP and NDP are link-layer protocols, the device must be in the same subnet as the local machine.
:::
diff --git a/Website/docs/application/neighbor-table.md b/Website/docs/application/neighbor-table.md
new file mode 100644
index 0000000000..72bed9eadf
--- /dev/null
+++ b/Website/docs/application/neighbor-table.md
@@ -0,0 +1,121 @@
+---
+sidebar_position: 27
+description: "View and manage IP-to-MAC address mappings for both IPv4 (ARP) and IPv6 (NDP) with NETworkManager. Add or delete neighbor entries, filter by state, and inspect interface assignments."
+keywords: [NETworkManager, neighbor table, ARP table, NDP, IPv4, IPv6, ARP cache, NDP cache, IP to MAC, MAC address, address resolution, neighbor discovery]
+---
+
+# Neighbor Table
+
+The **Neighbor Table** shows the IP-to-MAC address mappings that the operating system has cached for both IPv4 (ARP) and IPv6 (NDP). It lists all devices on the local network with which the computer has recently communicated.
+
+:::info
+
+**IPv4 – ARP (Address Resolution Protocol)** is a layer-2 protocol that maps IPv4 addresses to MAC addresses. When a device needs to send data to an IPv4 address, it first checks the ARP cache. If no entry is found it broadcasts an ARP request; the target replies with its MAC address and the entry is added to the cache.
+
+**IPv6 – NDP (Neighbor Discovery Protocol)** fulfills the same purpose for IPv6. Instead of broadcasts, NDP uses ICMPv6 Neighbor Solicitation and Advertisement messages sent to a solicited-node multicast address.
+
+Both protocols are susceptible to spoofing/poisoning attacks that can manipulate the cached mappings, which can lead to man-in-the-middle scenarios.
+
+:::
+
+:::note
+
+Adding and deleting neighbor entries requires administrator privileges. If the application is not running as administrator, the view is in read-only mode. Use the **Restart as administrator** button to relaunch the application with elevated rights.
+
+:::
+
+
+
+:::note
+
+Additional actions are available via the buttons below the table:
+
+- **Add entry...** – Opens a dialog to add a permanent static neighbor entry.
+- **Delete table** – Removes all dynamic entries (permanent entries are preserved).
+
+:::
+
+:::note
+
+Press `F5` to refresh the neighbor table.
+
+Right-click on a row to copy or export individual values, or to delete the selected entry.
+
+:::
+
+## Columns
+
+| Column | Description |
+| --- | --- |
+| **IP Address** | IPv4 or IPv6 address of the cached neighbor. |
+| **Interface** | Human-readable name of the network interface the entry belongs to (e.g. `Ethernet`, `Wi-Fi`). |
+| **MAC Address** | Link-layer (MAC) address associated with the IP address. |
+| **State** | Current reachability state of the entry (see [States](#states) below). |
+| **Multicast** | Indicates whether the IP address is a multicast address. |
+
+## States
+
+| State | Description |
+| --- | --- |
+| `Unreachable` | The neighbor is no longer reachable and the entry is about to be removed. |
+| `Incomplete` | An ARP/NDP request has been sent but no reply has been received yet. |
+| `Probe` | The system is actively probing the neighbor to verify reachability. |
+| `Delay` | Waiting for an upper-layer protocol to confirm reachability before sending a probe. |
+| `Stale` | The entry exists but has not been recently confirmed; will be verified on next use. |
+| `Reachable` | The neighbor has been confirmed reachable within the last reachability timeout. |
+| `Permanent` | A static entry that was manually added and will not expire. |
+
+## Add entry
+
+The **Add entry** dialog opens by clicking the **Add entry...** button. It creates a new permanent static neighbor entry that maps an IP address to a MAC address.
+
+
+
+### IP address
+
+IPv4 or IPv6 address of the device.
+
+**Type:** `String`
+
+**Default:** `Empty`
+
+**Example:** `10.0.0.10`, `2001:db8::1`
+
+:::note
+
+Both IPv4 and IPv6 addresses are accepted. The field is required and validated for the correct address format.
+
+:::
+
+### MAC address
+
+MAC address of the device the [IP address](#ip-address) should be mapped to.
+
+**Type:** `String`
+
+**Default:** `Empty`
+
+**Example:**
+
+- `00:1A:2B:3C:4D:5E`
+- `00-1A-2B-3C-4D-5E`
+
+:::note
+
+The field is required and validated for a correct MAC address format.
+
+:::
+
+### Interface
+
+The network interface on which the entry is created.
+
+**Type:** `Dropdown`
+
+**Default:** Last used interface, or the first available interface.
+
+:::note
+
+Adding a static entry requires administrator privileges. Internally, NETworkManager uses `New-NetNeighbor` (PowerShell `NetTCPIP` module) to create the entry with state `Permanent`.
+
+:::
\ No newline at end of file
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 08b76c6bd2..5c527e9422 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -27,8 +27,18 @@ Release date: **xx.xx.2025**
## Breaking Changes
+- **ARP Table** has been renamed to **[Neighbor Table](../application/neighbor-table.md)**. The application list entry is automatically migrated on first launch. Other view settings (auto-refresh interval, export file type/path) reset to their defaults. [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
+- **IP Scanner** export: The `ARPMACAddress` and `ARPVendor` columns have been removed from CSV, XML and JSON exports. Use `MACAddress` and `Vendor` instead, which contain the same value (ARP/NDP preferred, NetBIOS as fallback). [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
+
## What's new?
+**[Neighbor Table](../application/neighbor-table.md)** (formerly ARP Table)
+
+- IPv6 (NDP) neighbor entries are now shown in addition to IPv4 (ARP). [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
+- New **Interface** and **State** columns (sortable, searchable). [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
+- Add entry dialog now accepts IPv4 and IPv6 addresses. [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
+- View is read-only when not running elevated; modifying the table requires elevated rights. [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
+
**Firewall**
- New feature to quickly add, edit, enable, disable and delete NETworkManager-owned firewall rules. Managed rules are prefixed with `NETworkManager_` in the Windows Firewall. (See the [documentation](https://borntoberoot.net/NETworkManager/docs/application/firewall) for more details) [#3383](https://github.com/BornToBeRoot/NETworkManager/pull/3383)
@@ -43,6 +53,10 @@ Release date: **xx.xx.2025**
## Improvements
+**IP Scanner**
+
+- MAC address resolution now uses ARP (IPv4) or NDP (IPv6) from the neighbor cache, with NetBIOS as fallback. The detail panel shows a single **MAC Address** section instead of separate ARP and NetBIOS entries. [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
+
**Dashboard**
- Redesign Status Window to make it more compact. [#3359](https://github.com/BornToBeRoot/NETworkManager/pull/3359)
diff --git a/Website/docs/img/arp-table-entry.png b/Website/docs/img/arp-table-entry.png
deleted file mode 100644
index 0d28897d9c..0000000000
Binary files a/Website/docs/img/arp-table-entry.png and /dev/null differ
diff --git a/Website/docs/img/arp-table.png b/Website/docs/img/arp-table.png
deleted file mode 100644
index 12456682c9..0000000000
Binary files a/Website/docs/img/arp-table.png and /dev/null differ
diff --git a/Website/docs/img/firewall-rule.png b/Website/docs/img/firewall-rule.png
index 676c751182..f3155ecf43 100644
Binary files a/Website/docs/img/firewall-rule.png and b/Website/docs/img/firewall-rule.png differ
diff --git a/Website/docs/img/firewall.png b/Website/docs/img/firewall.png
index 692510b0f6..f4ad75b745 100644
Binary files a/Website/docs/img/firewall.png and b/Website/docs/img/firewall.png differ
diff --git a/Website/docs/img/neighbor-table-entry.png b/Website/docs/img/neighbor-table-entry.png
new file mode 100644
index 0000000000..4e2fbbb1d6
Binary files /dev/null and b/Website/docs/img/neighbor-table-entry.png differ
diff --git a/Website/docs/img/neighbor-table.png b/Website/docs/img/neighbor-table.png
new file mode 100644
index 0000000000..30539db6de
Binary files /dev/null and b/Website/docs/img/neighbor-table.png differ
diff --git a/Website/docs/introduction.mdx b/Website/docs/introduction.mdx
index 5117d4ec48..0f092830ed 100644
--- a/Website/docs/introduction.mdx
+++ b/Website/docs/introduction.mdx
@@ -39,7 +39,7 @@ NETworkManager comes with various built-in tools. See the documentation for deta
- [Lookup](./application/lookup) - OUI, Port
- [Connections](./application/connections)
- [Listeners](./application/listeners)
-- [ARP Table](./application/arp-table)
+- [Neighbor Table](./application/neighbor-table)
## Groups and Profiles
diff --git a/Website/docusaurus.config.js b/Website/docusaurus.config.js
index c7cc9e2890..70a056f254 100644
--- a/Website/docusaurus.config.js
+++ b/Website/docusaurus.config.js
@@ -232,6 +232,13 @@ const config = {
],
to: "/docs/changelog/2025-8-10-0",
},
+ // Redirect legacy ARP Table to Neighbor Table
+ {
+ from: [
+ "/docs/application/arp-table",
+ ],
+ to: "/docs/application/neighbor-table",
+ },
],
},
],