Skip to content

Refactor ARP Table → Neighbor Table (IPv4 + IPv6) #3402

@BornToBeRoot

Description

@BornToBeRoot

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

  1. 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.

  1. 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_….

  1. 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).


  1. Converters — NETworkManager.Converters/

New converter: NeighborStateToStringConverter.cs

  • Maps NeighborState enum to Strings.NeighborState_ for display.
  • Mirror the pattern of FirewallRuleDirectionToStringConverter.

  1. 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.


  1. 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.


  1. 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).

  1. 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).

  1. 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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions