Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Source/NETworkManager.Localization/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@
<value>Configuration applied</value>
</data>
<data name="NetworkInterfaceConfigureAdminMessage" xml:space="preserve">
<value>Configuring the network interface requires elevated rights!</value>
<value>Configuring the network interface requires elevated rights (administrator or member of the "Network Configuration Operators" group)!</value>
</data>
<data name="NewTab" xml:space="preserve">
<value>New tab</value>
Expand Down
133 changes: 106 additions & 27 deletions Source/NETworkManager.Models/Network/NetworkInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -172,12 +173,7 @@ public static List<NetworkInterfaceInfo> 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;
Expand Down Expand Up @@ -420,10 +416,13 @@ private static void ConfigureNetworkInterface(NetworkInterfaceConfig config)
var name = PowerShellHelper.EscapeSingleQuotes(config.Name);
var commands = new List<string>();

// 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
Expand All @@ -434,14 +433,75 @@ 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");
}

RunCommands(commands);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="interfaceId">ID (GUID) of the network interface.</param>
/// <returns>Whether DNS autoconfiguration is enabled, or <see langword="null" /> if it cannot be determined.</returns>
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());
}

/// <summary>
/// Reads the current DHCP state of a network interface right before it is configured (the information shown in the
/// UI can be outdated). Returns <see langword="null" /> for values that cannot be determined, in which case the
/// corresponding command is not skipped.
/// </summary>
/// <param name="name">Name of the network interface.</param>
/// <returns>Whether DHCP is enabled for the IPv4 address and for the DNS servers.</returns>
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);
}
}

/// <summary>
/// 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.
/// </summary>
/// <remarks><see cref="WindowsPrincipal.IsInRole(SecurityIdentifier)" /> only returns <see langword="true" /> 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.</remarks>
/// <returns><see langword="true" /> if the network configuration can be changed.</returns>
public static bool HasPermissionToConfigure()
{
var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());

return principal.IsInRole(WindowsBuiltInRole.Administrator) ||
principal.IsInRole(new SecurityIdentifier(WellKnownSidType.BuiltinNetworkConfigurationOperatorsSid,
null));
}

/// <summary>
/// Asynchronously flushes the system DNS resolver cache.
/// </summary>
Expand All @@ -459,7 +519,7 @@ public static Task FlushDnsAsync()
/// <remarks>Requires the application to be running with administrator privileges.</remarks>
private static void FlushDns()
{
RunCommands(["& ipconfig /flushdns"], checkExitCode: true);
RunCommands(["& ipconfig /flushdns"]);
}

/// <summary>
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -551,28 +611,47 @@ private static void RemoveIPAddressFromNetworkInterface(NetworkInterfaceConfig c
}

/// <summary>
/// Runs one or more native commands via a PowerShell runspace.
/// When <paramref name="checkExitCode"/> is <see langword="true"/>, a non-zero
/// <c>$LASTEXITCODE</c> is treated as an error via <c>Write-Error</c>.
/// Use <see langword="false"/> for <c>netsh</c>, whose exit codes are unreliable
/// for idempotent operations (e.g. returns 1 when already on DHCP).
/// Runs one or more native commands (<c>netsh</c>, <c>ipconfig</c>) 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 <c>netsh</c>) must not be passed.
/// </summary>
private static void RunCommands(IEnumerable<string> commands, bool checkExitCode = false)
/// <param name="commands">Commands to run, e.g. <c>&amp; netsh interface ipv4 ...</c>.</param>
/// <exception cref="Exception">If a command fails.</exception>
private static void RunCommands(IEnumerable<string> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public bool IsNetworkInterfaceLoading
}
}

/// <summary>
/// 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).
/// </summary>
public bool HasPermissionToConfigure { get; } = NetworkInterface.HasPermissionToConfigure();

/// <summary>
/// Gets or sets a value indicating whether configuration is allowed.
/// </summary>
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions Source/NETworkManager/Views/NetworkInterfaceView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@
</DataTemplate>
<x:Array x:Key="NetworkInterfaceProfileContextMenuExtraItems" Type="{x:Type MenuItem}">
<MenuItem Header="{x:Static localization:Strings.Apply}"
Command="{Binding ApplyProfileCommand}">
Command="{Binding ApplyProfileCommand}"
ToolTip="{x:Static localization:Strings.NetworkInterfaceConfigureAdminMessage}"
ToolTipService.ShowOnDisabled="True"
ToolTipService.IsEnabled="{Binding HasPermissionToConfigure, Converter={StaticResource BooleanReverseConverter}}">
<MenuItem.Icon>
<Rectangle Width="16" Height="16"
Fill="{DynamicResource MahApps.Brushes.Gray3}">
Expand Down Expand Up @@ -1464,7 +1467,7 @@
</Grid>
<Grid Grid.Row="5"
Background="{DynamicResource MahApps.Brushes.Gray10}"
Visibility="{Binding Source={x:Static settings:ConfigurationManager.Current}, Path=IsAdmin, Converter={StaticResource BooleanReverseToVisibilityCollapsedConverter}}"
Visibility="{Binding HasPermissionToConfigure, Converter={StaticResource BooleanReverseToVisibilityCollapsedConverter}}"
Margin="0,10,0,0">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Stretch"
Margin="10">
Expand Down
6 changes: 5 additions & 1 deletion Website/docs/application/network-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ The options you can set correspond to the network adapter properties `Internetpr

:::note

All actions on this tab require the application to be running with **administrator privileges**. If the app is not elevated, a banner is shown at the bottom with a **Restart as Administrator** button.
All actions on this tab (and **Apply** from the profile context menu) require the application to be running elevated, either with **administrator privileges** or as a member of the **Network Configuration Operators** group. If the app is not elevated, a banner is shown at the bottom with a **Restart as Administrator** button.

Members of the **Network Configuration Operators** group don't need to be administrators: with User Account Control, the group is only active in an elevated process. Use **Restart as Administrator** and confirm the prompt with your own credentials.

If a change fails (e.g. due to missing permissions), the error message of Windows is shown below the configuration.

:::

Expand Down
8 changes: 8 additions & 0 deletions Website/docs/changelog/next-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Release date: **xx.xx.2026**

## Improvements

**Network Interface**

- Members of the **Network Configuration Operators** group can now change the network configuration (IP address, DNS servers, release/renew) without being an administrator. NETworkManager has to run elevated (**Restart as Administrator**, confirm with your own credentials). The hint is now also shown as tooltip on **Apply** in the profile context menu. [#3606](https://github.com/BornToBeRoot/NETworkManager/issues/3606)

- The collapsed/expanded state of profile groups (e.g. **linux-server**) is now remembered per profile file and shared across all tools, instead of resetting every time you switch tools or restart the application. [#3539](https://github.com/BornToBeRoot/NETworkManager/pull/3539)

**IP Scanner**
Expand Down Expand Up @@ -67,6 +71,10 @@ Release date: **xx.xx.2026**

## Bug Fixes

**Network Interface**

- Fixed errors when changing the network configuration (e.g. missing permissions) not being shown — the change was reported as successful although it failed. The error message of Windows is now shown. [#3606](https://github.com/BornToBeRoot/NETworkManager/issues/3606)

**Dashboard**

- Fixed the **Network Connection** widget running a full connection check on every application startup, even if the Status Window or Dashboard was never opened. It now only checks when actually shown - via the tray icon, on a network change, or when the Dashboard tab is opened. [#3553](https://github.com/BornToBeRoot/NETworkManager/pull/3553)
Expand Down
Loading