Goal
Replace the current ARP Table feature with a more capable Neighbor Table that covers both IPv4 (ARP) and IPv6 (NDP). Improve modify operations to run in-process via SMA.PowerShell and gate them
behind an admin check (banner + disabled commands when not elevated), matching the existing pattern from Firewall and HostsFileEditor.
Motivation
- The current implementation only shows IPv4 entries (GetIpNetTable) and uses externally elevated PowerShell processes (PowerShellHelper.ExecuteCommand → UAC prompt per click) for arp -s, arp
-d, netsh interface ip delete arpcache. This is inconsistent with how Firewall and HostsFileEditor handle elevation.
- Output of arp.exe / netsh.exe is fully localized (e.g., German error messages on de-DE Windows), making error handling unreliable.
- IPv6 neighbors (NDP) are invisible today.
- No metadata about neighbor reachability state (Reachable/Stale/Permanent) is exposed.
Scope
Rename + restructure: ARP Table becomes Neighbor Table. No backwards-compatible migration of settings keys — just rename. Users will lose their auto-refresh preference for this single view
(acceptable).
Detailed Implementation Steps
- Models — NETworkManager.Models/Network/
1.1 Rename and extend the model
- ARPInfo.cs → NeighborInfo.cs
- New properties:
- IPAddress IPAddress (existing)
- PhysicalAddress MACAddress (existing, may be empty for Incomplete/Unreachable states)
- bool IsMulticast (existing — computed from IP, NOT from State)
- int InterfaceIndex (new)
- string InterfaceAlias (new — human-readable, e.g., "Ethernet", "Wi-Fi")
- NeighborState State (new — enum)
- AddressFamily AddressFamily (new — InterNetwork / InterNetworkV6, from System.Net.Sockets)
1.2 New NeighborState enum
public enum NeighborState
{
Unreachable = 1,
Incomplete = 2,
Probe = 3,
Delay = 4,
Stale = 5,
Reachable = 6,
Permanent = 7
}
Values match the MIB_IPNET_ROW2.State field returned by GetIpNetTable2, so direct cast works.
1.3 Rename ARP.cs → NeighborTable.cs
Read path — use GetIpNetTable2 (Win32 P/Invoke) for performance + language independence:
[DllImport("Iphlpapi.dll")]
private static extern uint GetIpNetTable2(ushort family, out IntPtr table);
[DllImport("Iphlpapi.dll")]
private static extern void FreeMibTable(IntPtr memory);
- Pass family = AF_UNSPEC (0) to get IPv4 + IPv6 in one call.
- Marshal MIB_IPNET_TABLE2 (header: NumEntries + variadic array of MIB_IPNET_ROW2).
- For each MIB_IPNET_ROW2:
- Address is a SOCKADDR_INET union — read si_family first (2 = AF_INET, 23 = AF_INET6), then parse IPv4 (4 bytes at offset 4 in SOCKADDR_IN) or IPv6 (16 bytes at offset 8 in SOCKADDR_IN6).
- PhysicalAddressLength tells how many of the 32 PhysicalAddress bytes are valid. Build PhysicalAddress from those bytes (may be 0 → leave MAC empty).
- InterfaceIndex → assign directly.
- InterfaceLuid → resolve to alias via ConvertInterfaceLuidToAlias (P/Invoke) or lookup once via NetworkInterface.GetAllNetworkInterfaces() keyed by InterfaceIndex (simpler, uses managed
API).
- State → cast UInt8 to NeighborState enum.
- Filter: keep the existing virtual MAC (00:00:00:00:00:00) / broadcast MAC (FF:FF:FF:FF:FF:FF) suppression for IPv4 to match current behavior. IPv6 NDP doesn't have a "broadcast MAC" concept,
but Incomplete entries with empty MACs should still be filterable — decide whether to show them (recommend: show but with empty MAC column).
Write path — use SMA.PowerShell.Create() with a shared runspace (mirror of Firewall.cs pattern):
private static readonly Runspace SharedRunspace; // initialized in static ctor with ExecutionPolicy Bypass + Import-Module NetTCPIP
private static readonly SemaphoreSlim Lock = new(1, 1);
- AddEntryAsync(string ipAddress, string macAddress):
- Resolve InterfaceIndex via Find-NetRoute -RemoteIPAddress '' | Select -First 1 -ExpandProperty InterfaceIndex inside the same script.
- Then New-NetNeighbor -InterfaceIndex $idx -IPAddress '' -LinkLayerAddress '' -State Permanent
- MAC must be passed in aa-bb-cc-dd-ee-ff format (use MACAddressHelper.Format(mac, "-") in the ViewModel, as today).
- DeleteEntryAsync(string ipAddress):
- Remove-NetNeighbor -IPAddress '' -Confirm:$false
- DeleteTableAsync():
- Get-NetNeighbor -AddressFamily IPv4 | Where-Object State -ne 'Permanent' | Remove-NetNeighbor -Confirm:$false
- This matches the behavior of netsh interface ip delete arpcache: clear dynamic entries, keep static ones.
- Decision needed: clear IPv4 only (matches old behavior) or clear IPv4 + IPv6 (more consistent with the new Neighbor Table scope). Recommendation: IPv4 + IPv6 — match the rename. Implement
as Get-NetNeighbor | Where-Object State -ne 'Permanent' | Remove-NetNeighbor -Confirm:$false.
- Error handling: check ps.HadErrors / ps.Streams.Error.Count, throw an Exception with the joined error stream messages (the messages may be localized — that's OK, we just display them as
status messages, never pattern-match them).
- Properly escape single quotes in IP/MAC strings (EscapePs).
1.4 Drop the legacy UserHasCanceled event
- No longer needed: we no longer launch external elevated PowerShell, so there's no UAC cancel scenario.
- Remove RunPowerShellCommand and the dependency on PowerShellHelper.ExecuteCommand.
- ViewModel — NETworkManager/ViewModels/
- Rename ARPTableViewModel.cs → NeighborTableViewModel.cs
- Rename ARPTableAddEntryViewModel.cs → NeighborTableAddEntryViewModel.cs
- Inside NeighborTableViewModel:
- Results → ObservableCollection
- Update sort comparer (IPAddressHelper.CompareIPAddresses already handles v4 + v6).
- Update search filter to also match against InterfaceAlias and State (State.ToString() — invariant English) and the localized state string (Strings.NeighborState_X).
- Replace arpTable.UserHasCanceled += … event subscription pattern with simple try/catch around static NeighborTable.AddEntryAsync/DeleteEntryAsync/DeleteTableAsync.
- Add IsModifying flag (gates modify commands while a write is in flight; pattern matches HostsFileEditorViewModel).
- Add common ModifyEntry_CanExecute method:
return ConfigurationManager.Current.IsAdmin
&& Application.Current.MainWindow != null
&& !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen
&& !ConfigurationManager.Current.IsChildWindowOpen
&& !IsRefreshing
&& !IsModifying;
- Wire AddEntryCommand, DeleteEntryCommand, DeleteTableCommand to ModifyEntry_CanExecute.
- Add RestartAsAdminCommand → (Application.Current.MainWindow as MainWindow)?.RestartApplication(true).
- Update settings references: ARPTable_AutoRefreshTime → NeighborTable_AutoRefreshTime, ARPTable_AutoRefreshEnabled → NeighborTable_AutoRefreshEnabled, ARPTable_ExportFileType/FilePath →
NeighborTable_….
- View — NETworkManager/Views/
3.1 Rename
- ARPTableView.xaml / .xaml.cs → NeighborTableView.xaml / .xaml.cs
- ARPTableAddEntryChildWindow.xaml / .xaml.cs → NeighborTableAddEntryChildWindow.xaml / .xaml.cs
3.2 New columns in MultiSelectDataGrid
- IP Address (existing — works for both v4 and v6)
- Interface (InterfaceAlias)
- MAC Address (existing — may be empty for Incomplete states)
- State (use a converter NeighborStateToStringConverter that maps the enum to localized resx strings)
- Multicast (existing — computed from IP)
Update sort handlers in NeighborTableView.xaml.cs for the new columns.
3.3 Admin banner row
- Add xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings"
- Add BooleanReverseToVisibilityCollapsedConverter to UserControl.Resources
- Add a final row with Visibility bound to ConfigurationManager.Current.IsAdmin (reverse) showing the message + "Restart as Admin" button — copy from HostsFileEditorView.xaml /
FirewallView.xaml.
3.4 Update copy-to-clipboard menu items for new columns (Interface, State).
- Converters — NETworkManager.Converters/
New converter: NeighborStateToStringConverter.cs
- Maps NeighborState enum to Strings.NeighborState_ for display.
- Mirror the pattern of FirewallRuleDirectionToStringConverter.
- Settings — NETworkManager.Settings/
Rename in SettingsInfo.cs:
- ARPTable_AutoRefreshEnabled → NeighborTable_AutoRefreshEnabled
- ARPTable_AutoRefreshTime → NeighborTable_AutoRefreshTime
- ARPTable_ExportFileType → NeighborTable_ExportFileType
- ARPTable_ExportFilePath → NeighborTable_ExportFilePath
Defaults stay the same. No migration code — old keys get dropped.
- Localization — NETworkManager.Localization/
Edit only Strings.resx (Transifex handles all other languages). Update Strings.Designer.cs accordingly.
Strings to rename (search/replace):
- ARPTable → NeighborTable ("Neighbor Table")
- ARPTableAdminMessage → NeighborTableAdminMessage ("Read-only mode. Modifying the neighbor table requires elevated rights!")
- ApplicationName_ARPTable → ApplicationName_NeighborTable
Strings to add:
- Interface (column header) — likely already exists, reuse if present
- State (column header) — likely already exists, reuse
- NeighborState_Unreachable, _Incomplete, _Probe, _Delay, _Stale, _Reachable, _Permanent (one per enum value)
Search the codebase for any other ARPTable / ARP Table user-facing strings and rename.
- Application registry / routing
- NETworkManager.Models/ApplicationName.cs enum: rename ARPTable → NeighborTable.
- Search for ApplicationName.ARPTable references — at minimum: MainWindow.xaml.cs (view loading switch), ApplicationView registry, sidebar/icon assets, profile manager (does ARP show up
there?), search index.
- Update icon/image asset name if there's an arp_table.png or similar.
- Settings page references in SettingsViewModel if ARP Table has a settings sub-page (likely not, but check).
- Other call sites
- ARP.GetMACAddress(IPAddress) is called from elsewhere (check usages — at least IPScanner likely uses it). Either:
- Keep it as a static helper on NeighborTable with the same signature, or
- Inline the lookup at the call site.
- Any other consumers of ARPInfo / ARP — update types.
Edge Cases / Notes
- Marshalling SOCKADDR_INET: it's a union. Read si_family (UShort at offset 0). For AF_INET (2) → IPv4 sockaddr_in (16 bytes total, IP at offset 4). For AF_INET6 (23) → IPv6 sockaddr_in6 (28
bytes total, IP at offset 8). The full MIB_IPNET_ROW2 structure is larger than the IPv4-only MIB_IPNETROW.
- Empty MACs: Incomplete and Unreachable neighbors may have a zero-length physical address. Display as empty string, do not crash on PhysicalAddress construction.
- Localization in error stream: New-NetNeighbor / Remove-NetNeighbor errors will be in the OS UI language. We only display them as status messages — we do not pattern-match. Fine.
- InterfaceAlias lookup: prefer NetworkInterface.GetAllNetworkInterfaces() keyed by Index — pure managed code, no extra P/Invoke. Cache the lookup per GetTable() call (don't enumerate per row).
- AF_UNSPEC: passing 0 to GetIpNetTable2 returns both IPv4 and IPv6 in one call. Single API call, single allocation.
- MAXLEN_PHYSADDR: in MIB_IPNET_ROW2 this is 32 (vs 8 in the old MIB_IPNETROW).
- DeleteTable scope: implement as IPv4 + IPv6 dynamic entries (Permanent excluded). Document in the method's XML doc.
- No backwards compatibility: settings keys are dropped, not migrated. Users see auto-refresh defaults reset for this view on first launch after upgrade.
- NETworkManager.Models/ApplicationName.cs enum: rename ARPTable → NeighborTable.
- Search for ApplicationName.ARPTable references — at minimum: MainWindow.xaml.cs (view loading switch), ApplicationView registry, sidebar/icon assets, profile manager (does ARP show up
there?), search index.
- Update icon/image asset name if there's an arp_table.png or similar.
- Settings page references in SettingsViewModel if ARP Table has a settings sub-page (likely not, but check).
- Other call sites
- ARP.GetMACAddress(IPAddress) is called from elsewhere (check usages — at least IPScanner likely uses it). Either:
- Keep it as a static helper on NeighborTable with the same signature, or
- Inline the lookup at the call site.
- Any other consumers of ARPInfo / ARP — update types.
Edge Cases / Notes
- Marshalling SOCKADDR_INET: it's a union. Read si_family (UShort at offset 0). For AF_INET (2) → IPv4 sockaddr_in (16 bytes total, IP at offset 4). For AF_INET6 (23) → IPv6 sockaddr_in6 (28
bytes total, IP at offset 8). The full MIB_IPNET_ROW2 structure is larger than the IPv4-only MIB_IPNETROW.
- Empty MACs: Incomplete and Unreachable neighbors may have a zero-length physical address. Display as empty string, do not crash on PhysicalAddress construction.
- Localization in error stream: New-NetNeighbor / Remove-NetNeighbor errors will be in the OS UI language. We only display them as status messages — we do not pattern-match. Fine.
- InterfaceAlias lookup: prefer NetworkInterface.GetAllNetworkInterfaces() keyed by Index — pure managed code, no extra P/Invoke. Cache the lookup per GetTable() call (don't enumerate per row).
- AF_UNSPEC: passing 0 to GetIpNetTable2 returns both IPv4 and IPv6 in one call. Single API call, single allocation.
- MAXLEN_PHYSADDR: in MIB_IPNET_ROW2 this is 32 (vs 8 in the old MIB_IPNETROW).
- DeleteTable scope: implement as IPv4 + IPv6 dynamic entries (Permanent excluded). Document in the method's XML doc.
- No backwards compatibility: settings keys are dropped, not migrated. Users see auto-refresh defaults reset for this view on first launch after upgrade.
Out of Scope
- Ability to edit existing neighbor entries (today there's only add/delete — keep that)
- Filtering by AddressFamily or State in the UI (search box covers it; filter UI can be a follow-up)
- Showing Incomplete / Unreachable differently (e.g., greyed out) — could be a follow-up
- Migration of the old ARPTable_* settings keys
Acceptance Criteria
- App builds with 0 errors
- Running as standard user: Neighbor Table loads & auto-refreshes; Add/Delete/Delete Table buttons + context menu items are disabled; admin banner is visible at the bottom with a working
"Restart as Admin" button
- Running as admin: banner is hidden; modify operations work end-to-end without UAC prompts
- IPv4 and IPv6 neighbors both appear in the table
- Interface column shows readable adapter names ("Ethernet", "Wi-Fi", etc.)
- State column shows localized text and is sortable
- Search box matches against IP, MAC, Interface, State, and Multicast Yes/No
- Export (CSV/XML/JSON) includes the new columns
- Errors from New-NetNeighbor / Remove-NetNeighbor are surfaced in the status message line, not silently swallowed
Goal
Replace the current ARP Table feature with a more capable Neighbor Table that covers both IPv4 (ARP) and IPv6 (NDP). Improve modify operations to run in-process via SMA.PowerShell and gate them
behind an admin check (banner + disabled commands when not elevated), matching the existing pattern from Firewall and HostsFileEditor.
Motivation
-d, netsh interface ip delete arpcache. This is inconsistent with how Firewall and HostsFileEditor handle elevation.
Scope
Rename + restructure: ARP Table becomes Neighbor Table. No backwards-compatible migration of settings keys — just rename. Users will lose their auto-refresh preference for this single view
(acceptable).
Detailed Implementation Steps
1.1 Rename and extend the model
1.2 New NeighborState enum
public enum NeighborState
{
Unreachable = 1,
Incomplete = 2,
Probe = 3,
Delay = 4,
Stale = 5,
Reachable = 6,
Permanent = 7
}
Values match the MIB_IPNET_ROW2.State field returned by GetIpNetTable2, so direct cast works.
1.3 Rename ARP.cs → NeighborTable.cs
Read path — use GetIpNetTable2 (Win32 P/Invoke) for performance + language independence:
[DllImport("Iphlpapi.dll")]
private static extern uint GetIpNetTable2(ushort family, out IntPtr table);
[DllImport("Iphlpapi.dll")]
private static extern void FreeMibTable(IntPtr memory);
API).
but Incomplete entries with empty MACs should still be filterable — decide whether to show them (recommend: show but with empty MAC column).
Write path — use SMA.PowerShell.Create() with a shared runspace (mirror of Firewall.cs pattern):
private static readonly Runspace SharedRunspace; // initialized in static ctor with ExecutionPolicy Bypass + Import-Module NetTCPIP
private static readonly SemaphoreSlim Lock = new(1, 1);
as Get-NetNeighbor | Where-Object State -ne 'Permanent' | Remove-NetNeighbor -Confirm:$false.
status messages, never pattern-match them).
1.4 Drop the legacy UserHasCanceled event
return ConfigurationManager.Current.IsAdmin
&& Application.Current.MainWindow != null
&& !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen
&& !ConfigurationManager.Current.IsChildWindowOpen
&& !IsRefreshing
&& !IsModifying;
NeighborTable_….
3.1 Rename
3.2 New columns in MultiSelectDataGrid
Update sort handlers in NeighborTableView.xaml.cs for the new columns.
3.3 Admin banner row
FirewallView.xaml.
3.4 Update copy-to-clipboard menu items for new columns (Interface, State).
New converter: NeighborStateToStringConverter.cs
Rename in SettingsInfo.cs:
Defaults stay the same. No migration code — old keys get dropped.
Edit only Strings.resx (Transifex handles all other languages). Update Strings.Designer.cs accordingly.
Strings to rename (search/replace):
Strings to add:
Search the codebase for any other ARPTable / ARP Table user-facing strings and rename.
there?), search index.
Edge Cases / Notes
bytes total, IP at offset 8). The full MIB_IPNET_ROW2 structure is larger than the IPv4-only MIB_IPNETROW.
there?), search index.
Edge Cases / Notes
bytes total, IP at offset 8). The full MIB_IPNET_ROW2 structure is larger than the IPv4-only MIB_IPNETROW.
Out of Scope
Acceptance Criteria
"Restart as Admin" button