Skip to content

Commit 652b401

Browse files
committed
Feature: Restart as admin & use New-NetNeighbor / Remove-NetNeighbor
1 parent a5cfb44 commit 652b401

6 files changed

Lines changed: 243 additions & 119 deletions

File tree

Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Source/NETworkManager.Localization/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@
141141
<data name="ARPTable" xml:space="preserve">
142142
<value>ARP Table</value>
143143
</data>
144+
<data name="ARPTableAdminMessage" xml:space="preserve">
145+
<value>Read-only mode. Modifying the ARP table requires elevated rights!</value>
146+
</data>
144147
<data name="Authentication" xml:space="preserve">
145148
<value>Authentication</value>
146149
</data>

Source/NETworkManager.Models/Network/ARP.cs

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
1-
// Contains code from: https://stackoverflow.com/a/1148861/4986782
1+
// Contains code from: https://stackoverflow.com/a/1148861/4986782
22
// Modified by BornToBeRoot
33

44
using System;
55
using System.Collections.Generic;
66
using System.ComponentModel;
77
using System.Linq;
8+
using System.Management.Automation.Runspaces;
89
using System.Net;
910
using System.Net.NetworkInformation;
1011
using System.Runtime.InteropServices;
12+
using System.Threading;
1113
using System.Threading.Tasks;
1214
using NETworkManager.Utilities;
15+
using SMA = System.Management.Automation;
16+
using log4net;
1317

1418
namespace NETworkManager.Models.Network;
1519

20+
/// <summary>
21+
/// Provides static methods to read and modify the Windows ARP table.
22+
/// Read access uses the <c>IpHlpApi</c> Win32 API. Modifying operations
23+
/// (add/delete entries, clear table) run via PowerShell in a shared
24+
/// <see cref="Runspace"/> that is initialized once with the required
25+
/// execution policy. A <see cref="SemaphoreSlim"/> serializes access so
26+
/// the runspace is never used concurrently. Modifying operations require
27+
/// the application to run with elevated rights.
28+
/// </summary>
1629
public class ARP
1730
{
1831
#region Variables
1932

33+
/// <summary>
34+
/// The logger for this class.
35+
/// </summary>
36+
private static readonly ILog Log = LogManager.GetLogger(typeof(ARP));
37+
2038
// The max number of physical addresses.
2139
private const int MAXLEN_PHYSADDR = 8;
2240

@@ -50,15 +68,29 @@ private static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(Unmanaged
5068
// The insufficient buffer error.
5169
private const int ERROR_INSUFFICIENT_BUFFER = 122;
5270

53-
#endregion
54-
55-
#region Events
56-
57-
public event EventHandler UserHasCanceled;
58-
59-
protected virtual void OnUserHasCanceled()
71+
/// <summary>
72+
/// Ensures that only one PowerShell pipeline runs on <see cref="SharedRunspace"/> at a time.
73+
/// </summary>
74+
private static readonly SemaphoreSlim Lock = new(1, 1);
75+
76+
/// <summary>
77+
/// Shared PowerShell runspace, initialized once in the static constructor with
78+
/// <c>Set-ExecutionPolicy Bypass</c> so subsequent operations can run without
79+
/// repeating the policy change.
80+
/// </summary>
81+
private static readonly Runspace SharedRunspace;
82+
83+
/// <summary>
84+
/// Opens <see cref="SharedRunspace"/> and runs the one-time initialization script.
85+
/// </summary>
86+
static ARP()
6087
{
61-
UserHasCanceled?.Invoke(this, EventArgs.Empty);
88+
SharedRunspace = RunspaceFactory.CreateRunspace();
89+
SharedRunspace.Open();
90+
91+
using var ps = SMA.PowerShell.Create();
92+
ps.Runspace = SharedRunspace;
93+
ps.AddScript("Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process").Invoke();
6294
}
6395

6496
#endregion
@@ -154,61 +186,92 @@ public static string GetMACAddress(IPAddress ipAddress)
154186
return arpInfo?.MACAddress.ToString();
155187
}
156188

157-
private void RunPowerShellCommand(string command)
189+
/// <summary>
190+
/// Adds a static ARP entry by running <c>arp -s</c> through the shared PowerShell
191+
/// runspace. Requires the application to run with elevated rights.
192+
/// </summary>
193+
/// <param name="ipAddress">The IP address of the entry.</param>
194+
/// <param name="macAddress">The MAC address of the entry, separated with <c>-</c>.</param>
195+
/// <exception cref="Exception">
196+
/// Thrown when the PowerShell pipeline reports one or more errors.
197+
/// </exception>
198+
public static async Task AddEntryAsync(string ipAddress, string macAddress)
158199
{
159-
try
160-
{
161-
PowerShellHelper.ExecuteCommand(command, true);
162-
}
163-
catch (Win32Exception win32Ex)
164-
{
165-
switch (win32Ex.NativeErrorCode)
166-
{
167-
case 1223:
168-
OnUserHasCanceled();
169-
break;
170-
default:
171-
throw;
172-
}
173-
}
200+
await InvokeAsync($"arp -s '{EscapePs(ipAddress)}' '{EscapePs(macAddress)}' 2>&1 | Out-String");
174201
}
175202

176-
// MAC separated with "-"
177-
public Task AddEntryAsync(string ipAddress, string macAddress)
203+
/// <summary>
204+
/// Removes a single ARP entry by running <c>arp -d</c> through the shared PowerShell
205+
/// runspace. Requires the application to run with elevated rights.
206+
/// </summary>
207+
/// <param name="ipAddress">The IP address of the entry to remove.</param>
208+
/// <exception cref="Exception">
209+
/// Thrown when the PowerShell pipeline reports one or more errors.
210+
/// </exception>
211+
public static async Task DeleteEntryAsync(string ipAddress)
178212
{
179-
return Task.Run(() => AddEntry(ipAddress, macAddress));
213+
await InvokeAsync($"arp -d '{EscapePs(ipAddress)}' 2>&1 | Out-String");
180214
}
181215

182-
private void AddEntry(string ipAddress, string macAddress)
216+
/// <summary>
217+
/// Clears the entire ARP cache by running <c>netsh interface ip delete arpcache</c>
218+
/// through the shared PowerShell runspace. Requires the application to run with
219+
/// elevated rights.
220+
/// </summary>
221+
/// <exception cref="Exception">
222+
/// Thrown when the PowerShell pipeline reports one or more errors.
223+
/// </exception>
224+
public static async Task DeleteTableAsync()
183225
{
184-
var command = $"arp -s {ipAddress} {macAddress}";
185-
186-
RunPowerShellCommand(command);
226+
await InvokeAsync("netsh interface ip delete arpcache 2>&1 | Out-String");
187227
}
188228

189-
public Task DeleteEntryAsync(string ipAddress)
229+
/// <summary>
230+
/// Runs <paramref name="script"/> on the shared runspace and throws when the
231+
/// command exits with a non-zero exit code or writes to the PowerShell error stream.
232+
/// </summary>
233+
/// <param name="script">The PowerShell script to execute.</param>
234+
private static async Task InvokeAsync(string script)
190235
{
191-
return Task.Run(() => DeleteEntry(ipAddress));
192-
}
193-
194-
private void DeleteEntry(string ipAddress)
195-
{
196-
var command = $"arp -d {ipAddress}";
197-
198-
RunPowerShellCommand(command);
199-
}
200-
201-
public Task DeleteTableAsync()
202-
{
203-
return Task.Run(() => DeleteTable());
236+
await Lock.WaitAsync();
237+
try
238+
{
239+
await Task.Run(() =>
240+
{
241+
using var ps = SMA.PowerShell.Create();
242+
ps.Runspace = SharedRunspace;
243+
244+
ps.AddScript(script + @"
245+
if ($LASTEXITCODE -ne 0) { Write-Error ""Exit code: $LASTEXITCODE"" }");
246+
var results = ps.Invoke();
247+
248+
if (ps.Streams.Error.Count > 0)
249+
{
250+
var output = string.Join(Environment.NewLine,
251+
results.Select(r => r?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)));
252+
var errors = string.Join(Environment.NewLine, ps.Streams.Error);
253+
254+
var message = string.IsNullOrWhiteSpace(output)
255+
? errors
256+
: $"{output.Trim()}{Environment.NewLine}{errors}";
257+
258+
Log.Warn($"PowerShell error: {message}");
259+
throw new Exception(message);
260+
}
261+
});
262+
}
263+
finally
264+
{
265+
Lock.Release();
266+
}
204267
}
205268

206-
private void DeleteTable()
207-
{
208-
const string command = "netsh interface ip delete arpcache";
209-
210-
RunPowerShellCommand(command);
211-
}
269+
/// <summary>
270+
/// Escapes a string for embedding inside a PowerShell single-quoted string by
271+
/// doubling any single-quote characters.
272+
/// </summary>
273+
/// <param name="value">The raw string value to escape.</param>
274+
private static string EscapePs(string value) => value.Replace("'", "''");
212275

213276
#endregion
214277
}

0 commit comments

Comments
 (0)