diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index ccfb7aaabe..2b6f10c2e1 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -7238,7 +7238,7 @@ public static string NetworkInterfaceConfigurationAppliedSuccessfully {
}
///
- /// Looks up a localized string similar to Configuring the network interface requires elevated rights!.
+ /// Looks up a localized string similar to Configuring the network interface requires elevated rights (administrator or member of the "Network Configuration Operators" group)!.
///
public static string NetworkInterfaceConfigureAdminMessage {
get {
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index bdf839e1c5..df3e0efaf2 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -316,7 +316,7 @@
Configuration applied
- Configuring the network interface requires elevated rights!
+ Configuring the network interface requires elevated rights (administrator or member of the "Network Configuration Operators" group)!
New tab
diff --git a/Source/NETworkManager.Models/Network/NetworkInterface.cs b/Source/NETworkManager.Models/Network/NetworkInterface.cs
index ff8905bb83..8481df1fa4 100644
--- a/Source/NETworkManager.Models/Network/NetworkInterface.cs
+++ b/Source/NETworkManager.Models/Network/NetworkInterface.cs
@@ -4,6 +4,7 @@
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
+using System.Security.Principal;
using System.Threading.Tasks;
using log4net;
using Microsoft.Win32;
@@ -172,12 +173,7 @@ public static List GetNetworkInterfaces()
}
}
- // Check if autoconfiguration for DNS is enabled (only possible via registry key)
- var nameServerKey =
- Registry.LocalMachine.OpenSubKey(
- $@"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{networkInterface.Id}");
- var dnsAutoconfigurationEnabled = nameServerKey?.GetValue("NameServer") != null &&
- string.IsNullOrEmpty(nameServerKey.GetValue("NameServer")?.ToString());
+ var dnsAutoconfigurationEnabled = IsDNSAutoconfigurationEnabled(networkInterface.Id) == true;
// Check if IPv4 protocol is available
var ipv4ProtocolAvailable = true;
@@ -420,10 +416,13 @@ private static void ConfigureNetworkInterface(NetworkInterfaceConfig config)
var name = PowerShellHelper.EscapeSingleQuotes(config.Name);
var commands = new List();
+ // netsh returns an error if DHCP is already enabled. Skip these commands, so every error can be reported.
+ var (dhcpEnabled, dnsAutoconfigurationEnabled) = GetCurrentDhcpState(config.Name);
+
// IP
if (config.EnableStaticIPAddress)
commands.Add($"& netsh interface ipv4 set address name='{name}' source=static address={config.IPAddress} mask={config.Subnetmask} gateway={config.Gateway}");
- else
+ else if (dhcpEnabled != true)
commands.Add($"& netsh interface ipv4 set address name='{name}' source=dhcp");
// DNS
@@ -434,7 +433,7 @@ private static void ConfigureNetworkInterface(NetworkInterfaceConfig config)
if (!string.IsNullOrEmpty(config.SecondaryDNSServer))
commands.Add($"& netsh interface ipv4 add dnsservers name='{name}' address={config.SecondaryDNSServer} index=2 validate=no");
}
- else
+ else if (dnsAutoconfigurationEnabled != true)
{
commands.Add($"& netsh interface ipv4 set dnsservers name='{name}' source=dhcp");
}
@@ -442,6 +441,67 @@ private static void ConfigureNetworkInterface(NetworkInterfaceConfig config)
RunCommands(commands);
}
+ ///
+ /// Checks whether the DNS servers of a network interface are assigned automatically (DHCP). This can only be
+ /// detected via the registry: the "NameServer" value of the interface is empty, if no static DNS servers are set.
+ ///
+ /// ID (GUID) of the network interface.
+ /// Whether DNS autoconfiguration is enabled, or if it cannot be determined.
+ private static bool? IsDNSAutoconfigurationEnabled(string interfaceId)
+ {
+ using var interfaceKey = Registry.LocalMachine.OpenSubKey(
+ $@"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{interfaceId}");
+
+ var nameServer = interfaceKey?.GetValue("NameServer");
+
+ return nameServer == null ? null : string.IsNullOrEmpty(nameServer.ToString());
+ }
+
+ ///
+ /// Reads the current DHCP state of a network interface right before it is configured (the information shown in the
+ /// UI can be outdated). Returns for values that cannot be determined, in which case the
+ /// corresponding command is not skipped.
+ ///
+ /// Name of the network interface.
+ /// Whether DHCP is enabled for the IPv4 address and for the DNS servers.
+ private static (bool? DhcpEnabled, bool? DNSAutoconfigurationEnabled) GetCurrentDhcpState(string name)
+ {
+ try
+ {
+ var networkInterface = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()
+ .FirstOrDefault(x => x.Name == name);
+
+ if (networkInterface == null)
+ return (null, null);
+
+ return (networkInterface.GetIPProperties().GetIPv4Properties()?.IsDhcpEnabled,
+ IsDNSAutoconfigurationEnabled(networkInterface.Id));
+ }
+ catch (Exception ex)
+ {
+ Log.Warn($"Could not read the DHCP state of network interface \"{name}\".", ex);
+ return (null, null);
+ }
+ }
+
+ ///
+ /// Checks whether the current process is allowed to change the network configuration (IP addresses, DNS servers,
+ /// release/renew, flush DNS cache). This is the case if it is running as administrator or as member of the
+ /// "Network Configuration Operators" group, which may modify the TCP/IP settings without being an administrator.
+ ///
+ /// only returns if the
+ /// group is enabled in the access token. With UAC, both groups are only enabled if the application is running
+ /// elevated, so a non-elevated process is correctly reported as not allowed.
+ /// if the network configuration can be changed.
+ public static bool HasPermissionToConfigure()
+ {
+ var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
+
+ return principal.IsInRole(WindowsBuiltInRole.Administrator) ||
+ principal.IsInRole(new SecurityIdentifier(WellKnownSidType.BuiltinNetworkConfigurationOperatorsSid,
+ null));
+ }
+
///
/// Asynchronously flushes the system DNS resolver cache.
///
@@ -459,7 +519,7 @@ public static Task FlushDnsAsync()
/// Requires the application to be running with administrator privileges.
private static void FlushDns()
{
- RunCommands(["& ipconfig /flushdns"], checkExitCode: true);
+ RunCommands(["& ipconfig /flushdns"]);
}
///
@@ -496,7 +556,7 @@ private static void ReleaseRenew(IPConfigReleaseRenewMode mode, string adapterNa
if (mode is IPConfigReleaseRenewMode.ReleaseRenew6 or IPConfigReleaseRenewMode.Renew6)
commands.Add($"& ipconfig /renew6 '{name}'");
- RunCommands(commands, checkExitCode: true);
+ RunCommands(commands);
}
///
@@ -551,28 +611,47 @@ private static void RemoveIPAddressFromNetworkInterface(NetworkInterfaceConfig c
}
///
- /// Runs one or more native commands via a PowerShell runspace.
- /// When is , a non-zero
- /// $LASTEXITCODE is treated as an error via Write-Error.
- /// Use for netsh, whose exit codes are unreliable
- /// for idempotent operations (e.g. returns 1 when already on DHCP).
+ /// Runs one or more native commands (netsh, ipconfig) one after another via a PowerShell runspace.
+ /// Stops at the first command that fails (non-zero exit code) and throws an exception with the output of the
+ /// command (e.g. "The requested operation requires elevation."), which is localized by Windows and shown to the
+ /// user. Commands that would not change anything (and therefore fail with netsh) must not be passed.
///
- private static void RunCommands(IEnumerable commands, bool checkExitCode = false)
+ /// Commands to run, e.g. & netsh interface ipv4 ....
+ /// If a command fails.
+ private static void RunCommands(IEnumerable commands)
{
- string script;
+ foreach (var command in commands)
+ {
+ using var ps = SMA.PowerShell.Create();
+
+ ps.AddScript($"$output = {command} 2>&1 | Out-String{Environment.NewLine}" +
+ "[PSCustomObject]@{ Output = $output; ExitCode = $LASTEXITCODE }");
+
+ var result = ps.Invoke().LastOrDefault();
- if (checkExitCode)
- script = string.Join(Environment.NewLine, commands.Select(cmd =>
- $"{cmd}{Environment.NewLine}if ($LASTEXITCODE -ne 0) {{ Write-Error \"Command failed with exit code $LASTEXITCODE\" }}"));
- else
- script = string.Join(Environment.NewLine, commands);
+ // The script itself failed and did not return a result.
+ if (result == null)
+ throw new Exception(ps.HadErrors
+ ? string.Join("; ", ps.Streams.Error.Select(e => e.ToString()))
+ : $"\"{command}\" did not return a result.");
- using var ps = SMA.PowerShell.Create();
- ps.AddScript(script);
- ps.Invoke();
+ // Each command runs in a new runspace, so the exit code is only set if the native command was started.
+ var exitCode = result.Properties["ExitCode"]?.Value as int?;
+
+ if (exitCode == 0)
+ continue;
- if (checkExitCode && ps.HadErrors)
- throw new Exception(string.Join("; ", ps.Streams.Error.Select(e => e.ToString())));
+ var output = result.Properties["Output"]?.Value?.ToString()?.Trim();
+
+ Log.Error($"Command \"{command}\" failed with exit code {exitCode?.ToString() ?? "-/-"}: {output}");
+
+ if (!string.IsNullOrEmpty(output))
+ throw new Exception(output);
+
+ throw new Exception(exitCode == null
+ ? $"\"{command.TrimStart('&', ' ')}\" could not be run."
+ : $"\"{command.TrimStart('&', ' ')}\" failed with exit code {exitCode}.");
+ }
}
#endregion
diff --git a/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs b/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
index c3e1f1532c..dc48aa71b0 100644
--- a/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
+++ b/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
@@ -60,6 +60,12 @@ public bool IsNetworkInterfaceLoading
}
}
+ ///
+ /// Gets a value indicating whether the application has the permission to change the network configuration
+ /// (running as administrator or as member of the "Network Configuration Operators" group).
+ ///
+ public bool HasPermissionToConfigure { get; } = NetworkInterface.HasPermissionToConfigure();
+
///
/// Gets or sets a value indicating whether configuration is allowed.
///
@@ -846,7 +852,7 @@ private bool AdditionalCommands_CanExecute(object parameter)
!ConfigurationManager.Current.IsChildWindowOpen;
}
- private bool ConfigureCommands_CanExecute(object parameter) => ConfigurationManager.Current.IsAdmin &&
+ private bool ConfigureCommands_CanExecute(object parameter) => HasPermissionToConfigure &&
Application.Current.MainWindow != null &&
!((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
!ConfigurationManager.Current.IsChildWindowOpen;
diff --git a/Source/NETworkManager/Views/NetworkInterfaceView.xaml b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
index aa1da2d67a..ab6069335a 100644
--- a/Source/NETworkManager/Views/NetworkInterfaceView.xaml
+++ b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
@@ -63,7 +63,10 @@