diff --git a/Source/NETworkManager.Converters/FirewallRuleHiddenPropertiesToStringConverter.cs b/Source/NETworkManager.Converters/FirewallRuleHiddenPropertiesToStringConverter.cs
new file mode 100644
index 0000000000..46a2d8d3ae
--- /dev/null
+++ b/Source/NETworkManager.Converters/FirewallRuleHiddenPropertiesToStringConverter.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Windows.Data;
+using NETworkManager.Localization.Resources;
+using NETworkManager.Models.Firewall;
+
+namespace NETworkManager.Converters;
+
+///
+/// Convert to a translated, comma-separated
+/// (e.g. "ICMP types, Service").
+///
+public sealed class FirewallRuleHiddenPropertiesToStringConverter : IValueConverter
+{
+ ///
+ /// Convert to a translated, comma-separated .
+ ///
+ /// Object from type .
+ ///
+ ///
+ ///
+ /// Translated .
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not FirewallRuleHiddenProperties properties
+ ? "-/-"
+ : string.Join(", ", GetNames(properties));
+ }
+
+ ///
+ /// !!! Method not implemented !!!
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Gets the translated names of the hidden properties (e.g. "ICMP types", "Service").
+ /// Also used for the warning shown before a rule is copied.
+ ///
+ /// The hidden properties.
+ /// The translated names.
+ public static List GetNames(FirewallRuleHiddenProperties properties)
+ {
+ var names = new List();
+
+ if (properties.HasFlag(FirewallRuleHiddenProperties.IcmpTypes))
+ names.Add(Strings.IcmpTypes);
+
+ if (properties.HasFlag(FirewallRuleHiddenProperties.Service))
+ names.Add(Strings.Service);
+
+ if (properties.HasFlag(FirewallRuleHiddenProperties.Package))
+ names.Add(Strings.AppPackage);
+
+ if (properties.HasFlag(FirewallRuleHiddenProperties.InterfaceAlias))
+ names.Add(Strings.NetworkAdapters);
+
+ if (properties.HasFlag(FirewallRuleHiddenProperties.Security))
+ names.Add(Strings.UsersAndAuthentication);
+
+ return names;
+ }
+}
diff --git a/Source/NETworkManager.Converters/FirewallRuleSourceToStringConverter.cs b/Source/NETworkManager.Converters/FirewallRuleSourceToStringConverter.cs
new file mode 100644
index 0000000000..c6d54af95c
--- /dev/null
+++ b/Source/NETworkManager.Converters/FirewallRuleSourceToStringConverter.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using NETworkManager.Localization;
+using NETworkManager.Models.Firewall;
+
+namespace NETworkManager.Converters;
+
+///
+/// Convert to translated or vice versa.
+///
+public sealed class FirewallRuleSourceToStringConverter : IValueConverter
+{
+ ///
+ /// Convert to translated .
+ ///
+ /// Object from type .
+ ///
+ ///
+ ///
+ /// Translated .
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not FirewallRuleSource source
+ ? "-/-"
+ : ResourceTranslator.Translate(ResourceIdentifier.FirewallRuleSource, source);
+ }
+
+ ///
+ /// !!! Method not implemented !!!
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Source/NETworkManager.Localization/ResourceIdentifier.cs b/Source/NETworkManager.Localization/ResourceIdentifier.cs
index bedd20874c..108a10f3bc 100644
--- a/Source/NETworkManager.Localization/ResourceIdentifier.cs
+++ b/Source/NETworkManager.Localization/ResourceIdentifier.cs
@@ -28,5 +28,6 @@ public enum ResourceIdentifier
FirewallInterfaceType,
FirewallRuleDirection,
FirewallRuleAction,
+ FirewallRuleSource,
NeighborState
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs b/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
index 9086aaf302..6f5be43ab0 100644
--- a/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
@@ -159,6 +159,24 @@ public static string ExampleFirewallAddresses {
}
}
+ ///
+ /// Looks up a localized string similar to 192.168.1.10; 10.0.0.0/8.
+ ///
+ public static string ExampleFirewallLocalAddresses {
+ get {
+ return ResourceManager.GetString("ExampleFirewallLocalAddresses", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to C:\Program Files\GameServer\server.exe.
+ ///
+ public static string ExampleFirewallProgram {
+ get {
+ return ResourceManager.GetString("ExampleFirewallProgram", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to MyApp - HTTP.
///
diff --git a/Source/NETworkManager.Localization/Resources/StaticStrings.resx b/Source/NETworkManager.Localization/Resources/StaticStrings.resx
index 64d770e8d1..452cbac3a1 100644
--- a/Source/NETworkManager.Localization/Resources/StaticStrings.resx
+++ b/Source/NETworkManager.Localization/Resources/StaticStrings.resx
@@ -351,6 +351,12 @@
10.0.0.0/8; LocalSubnet
+
+ 192.168.1.10; 10.0.0.0/8
+
+
+ C:\Program Files\GameServer\server.exe
+
OU=Computers,DC=borntoberoot,DC=net
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index f009997dd5..854862d06a 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -4210,6 +4210,15 @@ public static string EnterValidFirewallAddress {
}
}
+ ///
+ /// Looks up a localized string similar to Enter a valid IP address, subnet (e.g. 10.0.0.0/8), range (e.g. 10.0.0.1-10.0.0.10), or the keyword Any. Other keywords (e.g. LocalSubnet) are only valid for remote addresses..
+ ///
+ public static string EnterValidFirewallLocalAddress {
+ get {
+ return ResourceManager.GetString("EnterValidFirewallLocalAddress", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Enter a valid folder path!.
///
@@ -5119,6 +5128,15 @@ public static string FirewallRules {
}
}
+ ///
+ /// Looks up a localized string similar to Reloaded at {0}, but not all firewall rules or their properties could be loaded, so the list may be incomplete (see log file for details). {1}.
+ ///
+ public static string FirewallRulesLoadedWithErrorsMessage {
+ get {
+ return ResourceManager.GetString("FirewallRulesLoadedWithErrorsMessage", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to At least one of the selected rules is managed by a policy (e.g. Group Policy or MDM) or created by Windows at runtime and can't be changed locally..
///
@@ -5148,6 +5166,42 @@ public static string FirewallRulesNotManagedWarningMessage {
}
}
+ ///
+ /// Looks up a localized string similar to Group Policy.
+ ///
+ public static string FirewallRuleSource_GroupPolicy {
+ get {
+ return ResourceManager.GetString("FirewallRuleSource_GroupPolicy", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Local.
+ ///
+ public static string FirewallRuleSource_Local {
+ get {
+ return ResourceManager.GetString("FirewallRuleSource_Local", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Mobile Device Management (MDM).
+ ///
+ public static string FirewallRuleSource_MDM {
+ get {
+ return ResourceManager.GetString("FirewallRuleSource_MDM", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Windows (runtime).
+ ///
+ public static string FirewallRuleSource_Other {
+ get {
+ return ResourceManager.GetString("FirewallRuleSource_Other", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to This rule uses values that are not supported by NETworkManager (e.g. port keywords like RPC, a combination of interface types, a protocol like IGMP or the program System). Editing or copying the rule is not supported (yet), because these values would be lost and the new rule could apply to more traffic (e.g. to all ports)..
///
@@ -13145,6 +13199,15 @@ public static string UnlockTheProfileFileMessage {
}
}
+ ///
+ /// Looks up a localized string similar to Unsupported properties.
+ ///
+ public static string UnsupportedProperties {
+ get {
+ return ResourceManager.GetString("UnsupportedProperties", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Untray / Bring window to foreground.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 8a727ed0af..077f2c3bb6 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -4415,6 +4415,9 @@ The new rule may apply to more traffic than the original rule (e.g. to all servi
Failed to load firewall rules. {0}
+
+ Reloaded at {0}, but not all firewall rules or their properties could be loaded, so the list may be incomplete (see log file for details). {1}
+
The selected firewall rule is permanently deleted:
@@ -4492,6 +4495,21 @@ The new rule may apply to more traffic than the original rule (e.g. to all servi
Allow
+
+ Local
+
+
+ Group Policy
+
+
+ Mobile Device Management (MDM)
+
+
+ Windows (runtime)
+
+
+ Unsupported properties
+
Local ports
@@ -4504,6 +4522,9 @@ The new rule may apply to more traffic than the original rule (e.g. to all servi
Remote addresses
+
+ Enter a valid IP address, subnet (e.g. 10.0.0.0/8), range (e.g. 10.0.0.1-10.0.0.10), or the keyword Any. Other keywords (e.g. LocalSubnet) are only valid for remote addresses.
+
Enter a valid IP address, subnet (e.g. 10.0.0.0/8), range (e.g. 10.0.0.1-10.0.0.10), or keyword (Any, LocalSubnet, DNS, DHCP, WINS, DefaultGateway, Internet, Intranet, IntranetRemoteAccess, PlayToDevice, CaptivePortal)
diff --git a/Source/NETworkManager.Models/Export/ExportManager.FirewallRule.cs b/Source/NETworkManager.Models/Export/ExportManager.FirewallRule.cs
index 589c32cac7..a38710469e 100644
--- a/Source/NETworkManager.Models/Export/ExportManager.FirewallRule.cs
+++ b/Source/NETworkManager.Models/Export/ExportManager.FirewallRule.cs
@@ -47,6 +47,8 @@ private static void CreateCsv(IEnumerable collection, string fileP
sb.AppendLine(
$"{nameof(FirewallRule.Name)}," +
$"{nameof(FirewallRule.IsEnabled)}," +
+ $"{nameof(FirewallRule.IsManaged)}," +
+ $"{nameof(FirewallRule.Source)}," +
$"{nameof(FirewallRule.Direction)}," +
$"{nameof(FirewallRule.Action)}," +
$"{nameof(FirewallRule.Protocol)}," +
@@ -63,6 +65,8 @@ private static void CreateCsv(IEnumerable collection, string fileP
sb.AppendLine(
$"{CsvHelper.QuoteString(rule.Name)}," +
$"{rule.IsEnabled}," +
+ $"{rule.IsManaged}," +
+ $"{rule.Source}," +
$"{rule.Direction}," +
$"{rule.Action}," +
$"{rule.Protocol}," +
@@ -90,6 +94,8 @@ from rule in collection
select new XElement(nameof(FirewallRule),
new XElement(nameof(FirewallRule.Name), rule.Name),
new XElement(nameof(FirewallRule.IsEnabled), rule.IsEnabled),
+ new XElement(nameof(FirewallRule.IsManaged), rule.IsManaged),
+ new XElement(nameof(FirewallRule.Source), rule.Source),
new XElement(nameof(FirewallRule.Direction), rule.Direction),
new XElement(nameof(FirewallRule.Action), rule.Action),
new XElement(nameof(FirewallRule.Protocol), rule.Protocol),
@@ -119,6 +125,8 @@ private static void CreateJson(IReadOnlyList collection, string fi
{
rule.Name,
rule.IsEnabled,
+ rule.IsManaged,
+ Source = rule.Source.ToString(),
Direction = rule.Direction.ToString(),
Action = rule.Action.ToString(),
Protocol = rule.Protocol.ToString(),
diff --git a/Source/NETworkManager.Models/Firewall/Firewall.cs b/Source/NETworkManager.Models/Firewall/Firewall.cs
index 70d32bae36..751a95e6fb 100644
--- a/Source/NETworkManager.Models/Firewall/Firewall.cs
+++ b/Source/NETworkManager.Models/Firewall/Firewall.cs
@@ -41,26 +41,50 @@ public class Firewall
private static readonly SemaphoreSlim RunspaceLock = new(1, 1);
///
- /// Lazily initialized PowerShell runspace. Created and configured on first access so that
- /// simply navigating to the Firewall view does not start a PowerShell process unless a
- /// modifying operation is actually performed.
+ /// Shared PowerShell runspace, see .
///
- private static readonly Lazy _sharedRunspace = new(() =>
+ private static Runspace _sharedRunspace;
+
+ ///
+ /// Returns the shared runspace. It is created and configured on first access, which is the initial load
+ /// of the rules when the Firewall view is opened for the first time. If the runspace could not be created
+ /// (e.g. the NetSecurity module failed to load) or is broken, it is created again on the next access,
+ /// so a temporary error doesn't break the Firewall until the application is restarted.
+ /// Must only be accessed while is held.
+ ///
+ private static Runspace SharedRunspace
{
- var runspace = RunspaceFactory.CreateRunspace();
- runspace.Open();
+ get
+ {
+ if (_sharedRunspace?.RunspaceStateInfo.State == RunspaceState.Opened)
+ return _sharedRunspace;
+
+ _sharedRunspace?.Dispose();
+ _sharedRunspace = null;
- using var ps = SMA.PowerShell.Create();
- ps.Runspace = runspace;
- ps.AddScript(@"
+ var runspace = RunspaceFactory.CreateRunspace();
+
+ try
+ {
+ runspace.Open();
+
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = runspace;
+ ps.AddScript(@"
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
Import-Module NetSecurity -ErrorAction Stop").Invoke();
+ }
+ catch
+ {
+ runspace.Dispose();
+ throw;
+ }
- return runspace;
- });
+ _sharedRunspace = runspace;
- /// Returns the shared runspace, initializing it on first access.
- private static Runspace SharedRunspace => _sharedRunspace.Value;
+ return runspace;
+ }
+ }
#endregion
@@ -100,64 +124,51 @@ public class Firewall
#region Methods
///
- /// Retrieves Windows Firewall rules and maps each one to a object.
- /// When is , only rules from the local store whose display name
- /// starts with are returned; otherwise all active rules on the system are returned
- /// (including rules from Group Policy, MDM or created by Windows at runtime, see ).
- /// PowerShell errors during the query are logged as warnings; errors for individual
- /// rules are caught so a single malformed rule does not abort the entire load.
+ /// Number of rules up to which the filters (ports, addresses, ...) are queried per rule. For more rules, the
+ /// filters of all rules in the policy store are queried once and matched by InstanceID, which is faster
+ /// than one query per rule and filter type, but has a fixed cost (all rules on the system, often 500+).
+ ///
+ private const int BulkFilterQueryThreshold = 10;
+
+ ///
+ /// Builds the PowerShell script that queries the rules and their filters and maps them with
+ /// .
///
///
- /// to query only rules managed by NETworkManager; to query all system firewall rules.
+ /// to query only rules managed by NETworkManager (local store); to
+ /// query all active rules.
///
- ///
- /// A list of objects representing the matching rules.
- ///
- public static async Task> GetRulesAsync(bool onlyManagedRules = true)
+ private static string BuildGetRulesScript(bool onlyManagedRules)
{
- await RunspaceLock.WaitAsync();
- try
- {
- return await Task.Run(() =>
- {
- var rules = new List();
-
- using var ps = SMA.PowerShell.Create();
- ps.Runspace = SharedRunspace;
-
- if (onlyManagedRules)
- {
- ps.AddScript($@"
-Get-NetFirewallRule -DisplayName '{RuleIdentifier}*' | ForEach-Object {{
- $rule = $_
- $portFilter = $rule | Get-NetFirewallPortFilter
- $addressFilter = $rule | Get-NetFirewallAddressFilter
- $appFilter = $rule | Get-NetFirewallApplicationFilter
- $ifTypeFilter = $rule | Get-NetFirewallInterfaceTypeFilter
- $svcFilter = $rule | Get-NetFirewallServiceFilter
- $ifFilter = $rule | Get-NetFirewallInterfaceFilter
- $secFilter = $rule | Get-NetFirewallSecurityFilter
- {RuleProjectionScript}
-}}");
- }
- else
- {
- ps.AddScript($@"
-$rules = Get-NetFirewallRule -PolicyStore ActiveStore
-$pf = @{{}}; Get-NetFirewallPortFilter -PolicyStore ActiveStore | ForEach-Object {{ $pf[$_.InstanceID] = $_ }}
-$af = @{{}}; Get-NetFirewallAddressFilter -PolicyStore ActiveStore | ForEach-Object {{ $af[$_.InstanceID] = $_ }}
-$app = @{{}}; Get-NetFirewallApplicationFilter -PolicyStore ActiveStore | ForEach-Object {{ $app[$_.InstanceID] = $_ }}
-$if = @{{}}; Get-NetFirewallInterfaceTypeFilter -PolicyStore ActiveStore | ForEach-Object {{ $if[$_.InstanceID] = $_ }}
-$sf = @{{}}; Get-NetFirewallServiceFilter -PolicyStore ActiveStore | ForEach-Object {{ $sf[$_.InstanceID] = $_ }}
-$inf = @{{}}; Get-NetFirewallInterfaceFilter -PolicyStore ActiveStore | ForEach-Object {{ $inf[$_.InstanceID] = $_ }}
-$sec = @{{}}; Get-NetFirewallSecurityFilter -PolicyStore ActiveStore | ForEach-Object {{ $sec[$_.InstanceID] = $_ }}
-# The filters are matched by InstanceID. If an ID occurs more than once (e.g. a local and a Group Policy rule
-# with the same ID), the filters of these rules are queried per rule, so they are not mixed up.
-$count = @{{}}; foreach ($rule in $rules) {{ $count[$rule.InstanceID]++ }}
+ var rulesQuery = onlyManagedRules
+ ? $"Get-NetFirewallRule -DisplayName '{RuleIdentifier}*'"
+ : "Get-NetFirewallRule -PolicyStore ActiveStore";
+
+ // Get-NetFirewallRule uses the PersistentStore (local rules) by default
+ var policyStore = onlyManagedRules ? "PersistentStore" : "ActiveStore";
+
+ // All rules are always many rules, so the filters are always queried at once
+ var threshold = onlyManagedRules ? BulkFilterQueryThreshold : 0;
+
+ return $@"
+$rules = @({rulesQuery})
+$bulk = $rules.Count -gt {threshold}
+if ($bulk) {{
+ $pf = @{{}}; Get-NetFirewallPortFilter -PolicyStore {policyStore} | ForEach-Object {{ $pf[$_.InstanceID] = $_ }}
+ $af = @{{}}; Get-NetFirewallAddressFilter -PolicyStore {policyStore} | ForEach-Object {{ $af[$_.InstanceID] = $_ }}
+ $app = @{{}}; Get-NetFirewallApplicationFilter -PolicyStore {policyStore} | ForEach-Object {{ $app[$_.InstanceID] = $_ }}
+ $if = @{{}}; Get-NetFirewallInterfaceTypeFilter -PolicyStore {policyStore} | ForEach-Object {{ $if[$_.InstanceID] = $_ }}
+ $sf = @{{}}; Get-NetFirewallServiceFilter -PolicyStore {policyStore} | ForEach-Object {{ $sf[$_.InstanceID] = $_ }}
+ $inf = @{{}}; Get-NetFirewallInterfaceFilter -PolicyStore {policyStore} | ForEach-Object {{ $inf[$_.InstanceID] = $_ }}
+ $sec = @{{}}; Get-NetFirewallSecurityFilter -PolicyStore {policyStore} | ForEach-Object {{ $sec[$_.InstanceID] = $_ }}
+ # The filters are matched by InstanceID. If an ID occurs more than once (e.g. a local and a Group Policy rule
+ # with the same ID), the filters of these rules are queried per rule, so they are not mixed up.
+ $count = @{{}}; foreach ($rule in $rules) {{ $count[$rule.InstanceID]++ }}
+}}
$list = [System.Collections.Generic.List[PSCustomObject]]::new($rules.Count)
foreach ($rule in $rules) {{
$id = $rule.InstanceID
- if ($count[$id] -gt 1) {{
+ if (-not $bulk -or $count[$id] -gt 1) {{
$portFilter = $rule | Get-NetFirewallPortFilter
$addressFilter = $rule | Get-NetFirewallAddressFilter
$appFilter = $rule | Get-NetFirewallApplicationFilter
@@ -176,16 +187,57 @@ public static async Task> GetRulesAsync(bool onlyManagedRules
}}
$list.Add({RuleProjectionScript})
}}
-$list");
- }
+$list";
+ }
+
+ ///
+ /// Retrieves Windows Firewall rules and maps each one to a object.
+ /// When is , only rules from the local store whose display name
+ /// starts with are returned; otherwise all active rules on the system are returned
+ /// (including rules from Group Policy, MDM or created by Windows at runtime, see ).
+ /// If the query fails completely, an exception is thrown. If only parts of it fail (e.g. a filter could not be
+ /// queried or a single rule could not be parsed), the loaded rules are returned together with the errors, so the
+ /// user can be told that the list may be incomplete.
+ ///
+ ///
+ /// to query only rules managed by NETworkManager; to query all system firewall rules.
+ ///
+ ///
+ /// The objects representing the matching rules, and the errors that occurred
+ /// (empty if the rules were loaded without errors).
+ ///
+ ///
+ /// Thrown when PowerShell reports errors and no rule was returned (e.g. the firewall service is not running).
+ ///
+ public static async Task<(List Rules, List Errors)> GetRulesAsync(bool onlyManagedRules = true)
+ {
+ await RunspaceLock.WaitAsync();
+ try
+ {
+ return await Task.Run(() =>
+ {
+ var rules = new List();
+
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+
+ ps.AddScript(BuildGetRulesScript(onlyManagedRules));
var results = ps.Invoke();
- if (ps.Streams.Error.Count > 0)
- {
- foreach (var error in ps.Streams.Error)
- Log.Warn($"PowerShell error: {error}");
- }
+ // No rule found (e.g. no rules managed by NETworkManager yet) is not an error. Only for the rule query:
+ // a filter that is not found (e.g. the rule was deleted while loading) would show the rule as unrestricted.
+ var errors = ps.Streams.Error
+ .Where(error => !(error.FullyQualifiedErrorId?.StartsWith("CmdletizationQuery_NotFound", StringComparison.Ordinal) == true &&
+ error.InvocationInfo?.MyCommand?.Name == "Get-NetFirewallRule"))
+ .Select(error => error.ToString())
+ .ToList();
+
+ foreach (var error in errors)
+ Log.Warn($"PowerShell error: {error}");
+
+ if (errors.Count > 0 && results.Count == 0)
+ throw new Exception(string.Join("; ", errors));
foreach (var result in results)
{
@@ -232,10 +284,12 @@ public static async Task> GetRulesAsync(bool onlyManagedRules
catch (Exception ex)
{
Log.Warn($"Failed to parse firewall rule: {ex.Message}");
+
+ errors.Add(ex.Message);
}
}
- return rules;
+ return (rules, errors);
});
}
finally
diff --git a/Source/NETworkManager.Models/Firewall/FirewallRule.cs b/Source/NETworkManager.Models/Firewall/FirewallRule.cs
index 6fd01c5a90..c180c9c2b6 100644
--- a/Source/NETworkManager.Models/Firewall/FirewallRule.cs
+++ b/Source/NETworkManager.Models/Firewall/FirewallRule.cs
@@ -36,6 +36,13 @@ public class FirewallRule
///
public bool IsManagedByPolicy => Source != FirewallRuleSource.Local;
+ ///
+ /// Sort order of the rule by where it comes from: managed by NETworkManager (0), not managed by NETworkManager (1),
+ /// Group Policy (2), MDM (3) or created by Windows at runtime (4). Used to sort the rule list, so rules managed
+ /// by a policy are not mixed up with rules that are only not managed by NETworkManager.
+ ///
+ public int SourceSortOrder => IsManaged ? 0 : IsManagedByPolicy ? 1 + (int)Source : 1;
+
///
/// Human-readable display name of the firewall rule. Used for display purposes
/// or as an identifier in various contexts.
diff --git a/Source/NETworkManager.Profiles/ProfileInfo.cs b/Source/NETworkManager.Profiles/ProfileInfo.cs
index b2e54fcf1b..05f61c63bb 100644
--- a/Source/NETworkManager.Profiles/ProfileInfo.cs
+++ b/Source/NETworkManager.Profiles/ProfileInfo.cs
@@ -8,7 +8,6 @@
using NETworkManager.Settings;
using System.Security;
using System.Xml.Serialization;
-//using NETworkManager.Models.Firewall;
// ReSharper disable InconsistentNaming
// ReSharper disable PropertyCanBeMadeInitOnly.Global
diff --git a/Source/NETworkManager.Profiles/ProfileName.cs b/Source/NETworkManager.Profiles/ProfileName.cs
index db8684b24c..0dd9424f4f 100644
--- a/Source/NETworkManager.Profiles/ProfileName.cs
+++ b/Source/NETworkManager.Profiles/ProfileName.cs
@@ -15,7 +15,6 @@ public enum ProfileName
TigerVNC,
WebConsole,
SNMP,
- Firewall,
WakeOnLAN,
RDAP,
Whois,
diff --git a/Source/NETworkManager.Profiles/ProfileViewManager.cs b/Source/NETworkManager.Profiles/ProfileViewManager.cs
index 08c2a0078e..9924f7387d 100644
--- a/Source/NETworkManager.Profiles/ProfileViewManager.cs
+++ b/Source/NETworkManager.Profiles/ProfileViewManager.cs
@@ -50,10 +50,6 @@ public static class ProfileViewManager
ProfileGroup.Application),
new(ProfileName.SNMP, ApplicationManager.GetIcon(ApplicationName.SNMP),
ProfileGroup.Application),
- /*
- new(ProfileName.Firewall, ApplicationManager.GetIcon(ApplicationName.Firewall),
- ProfileGroup.Application),
- */
new(ProfileName.WakeOnLAN, ApplicationManager.GetIcon(ApplicationName.WakeOnLAN),
ProfileGroup.Application),
new(ProfileName.RDAP, ApplicationManager.GetIcon(ApplicationName.RDAP),
diff --git a/Source/NETworkManager.Validators/EmptyOrFirewallAddressValidator.cs b/Source/NETworkManager.Validators/EmptyOrFirewallAddressValidator.cs
index 8deb67b336..585acf5cec 100644
--- a/Source/NETworkManager.Validators/EmptyOrFirewallAddressValidator.cs
+++ b/Source/NETworkManager.Validators/EmptyOrFirewallAddressValidator.cs
@@ -13,11 +13,18 @@ namespace NETworkManager.Validators;
/// valid IPv4/IPv6 addresses, IPv4/IPv6 CIDR subnets, IPv4 subnets in subnet-mask
/// notation (e.g. 10.0.0.0/255.0.0.0), IPv4/IPv6 ranges (e.g. 1.2.3.4-1.2.3.7),
/// or recognized Windows Firewall keywords (Any, LocalSubnet, DNS, DHCP, WINS, DefaultGateway,
-/// Internet, Intranet, IntranetRemoteAccess, PlayToDevice, CaptivePortal). Keywords may be
+/// Internet, Intranet, IntranetRemoteAccess, PlayToDevice, CaptivePortal). Keywords except Any may be
/// suffixed with 4 or 6 to restrict matching to IPv4 or IPv6 (e.g. LocalSubnet4).
+/// For local addresses (see ), Windows only accepts the keyword Any.
///
public class EmptyOrFirewallAddressValidator : ValidationRule
{
+ ///
+ /// Validate a local address. New-NetFirewallRule / Set-NetFirewallRule reject all keywords
+ /// except Any for -LocalAddress (e.g. LocalSubnet is only valid for -RemoteAddress).
+ ///
+ public bool IsLocalAddress { get; set; }
+
private static readonly string[] Keywords =
[
"Any", "LocalSubnet", "DNS", "DHCP", "WINS", "DefaultGateway",
@@ -37,7 +44,7 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
if (string.IsNullOrEmpty(token))
continue;
- if (IsKeyword(token))
+ if (IsLocalAddress ? token.Equals("Any", StringComparison.OrdinalIgnoreCase) : IsKeyword(token))
continue;
if (!token.Contains('/') && token.Contains('-'))
@@ -45,14 +52,14 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
if (IsValidRange(token))
continue;
- return new ValidationResult(false, Strings.EnterValidFirewallAddress);
+ return InvalidResult();
}
var slashIndex = token.IndexOf('/');
var addressPart = slashIndex > 0 ? token[..slashIndex] : token;
if (!IPAddress.TryParse(addressPart, out var ip))
- return new ValidationResult(false, Strings.EnterValidFirewallAddress);
+ return InvalidResult();
if (slashIndex <= 0)
continue;
@@ -65,12 +72,15 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
var maxPrefix = ip.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
if (!int.TryParse(suffix, out var prefix) || prefix < 0 || prefix > maxPrefix)
- return new ValidationResult(false, Strings.EnterValidFirewallAddress);
+ return InvalidResult();
}
return ValidationResult.ValidResult;
}
+ private ValidationResult InvalidResult() => new(false,
+ IsLocalAddress ? Strings.EnterValidFirewallLocalAddress : Strings.EnterValidFirewallAddress);
+
private static bool IsKeyword(string token)
{
if (Array.Exists(Keywords, k => k.Equals(token, StringComparison.OrdinalIgnoreCase)))
@@ -79,8 +89,10 @@ private static bool IsKeyword(string token)
if (token.Length < 2 || (token[^1] != '4' && token[^1] != '6'))
return false;
+ // "Any4" / "Any6" are not accepted by Windows
var bare = token[..^1];
- return Array.Exists(Keywords, k => k.Equals(bare, StringComparison.OrdinalIgnoreCase));
+ return !bare.Equals("Any", StringComparison.OrdinalIgnoreCase) &&
+ Array.Exists(Keywords, k => k.Equals(bare, StringComparison.OrdinalIgnoreCase));
}
private static bool IsValidRange(string token)
diff --git a/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs b/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs
index 1419beaac3..7ba97fe4a5 100644
--- a/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs
+++ b/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs
@@ -1,9 +1,11 @@
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
using NETworkManager.Models.Firewall;
+using NETworkManager.Settings;
using NETworkManager.Utilities;
using System;
using System.Collections.Generic;
+using System.Windows.Forms;
using System.Windows.Input;
namespace NETworkManager.ViewModels;
@@ -75,6 +77,25 @@ public FirewallRuleViewModel(Action okCommand,
///
public ICommand CancelCommand { get; }
+ ///
+ /// Command to select the with a file dialog.
+ ///
+ public ICommand BrowseFileCommand => new RelayCommand(_ => BrowseFileAction());
+
+ ///
+ /// Opens a file dialog to select the (executable) the rule applies to.
+ ///
+ private void BrowseFileAction()
+ {
+ var openFileDialog = new OpenFileDialog
+ {
+ Filter = GlobalStaticConfiguration.ApplicationFileExtensionFilter
+ };
+
+ if (openFileDialog.ShowDialog() == DialogResult.OK)
+ Program = openFileDialog.FileName;
+ }
+
///
/// Mode of the dialog (add, edit or copy).
///
diff --git a/Source/NETworkManager/ViewModels/FirewallViewModel.cs b/Source/NETworkManager/ViewModels/FirewallViewModel.cs
index 8232fdab9f..413e95e447 100644
--- a/Source/NETworkManager/ViewModels/FirewallViewModel.cs
+++ b/Source/NETworkManager/ViewModels/FirewallViewModel.cs
@@ -2,6 +2,8 @@
using MahApps.Metro.Controls;
using MahApps.Metro.SimpleChildWindow;
using NETworkManager.Controls;
+using NETworkManager.Converters;
+using NETworkManager.Localization;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
using NETworkManager.Models.Export;
@@ -19,6 +21,7 @@
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
+using System.Windows.Threading;
namespace NETworkManager.ViewModels;
@@ -101,8 +104,15 @@ public IList SelectedResults
///
private bool _hasRulesManagedByPolicy;
+ ///
+ /// Delays filtering the rules while the user is typing (see ),
+ /// so the view (can be 1000+ rules if all rules are shown) is not filtered on every key press.
+ ///
+ private readonly DispatcherTimer _searchDispatcherTimer = new();
+
///
/// Gets or sets the search text for filtering rules.
+ /// The rules are filtered after a short delay (see ).
///
public string RulesSearch
{
@@ -113,7 +123,10 @@ public string RulesSearch
return;
field = value;
- ResultsView.Refresh();
+
+ _searchDispatcherTimer.Stop();
+ _searchDispatcherTimer.Start();
+
OnPropertyChanged();
}
}
@@ -240,12 +253,28 @@ public FirewallViewModel()
if (o is not FirewallRule rule)
return false;
- return rule.Name.IndexOf(RulesSearch, StringComparison.OrdinalIgnoreCase) > -1 ||
- rule.Protocol.ToString().IndexOf(RulesSearch, StringComparison.OrdinalIgnoreCase) > -1 ||
- rule.Action.ToString().IndexOf(RulesSearch, StringComparison.OrdinalIgnoreCase) > -1 ||
- rule.Direction.ToString().IndexOf(RulesSearch, StringComparison.OrdinalIgnoreCase) > -1;
+ // Display values can be null (e.g. no ports / addresses / program)
+ bool Matches(string value) => value?.Contains(RulesSearch, StringComparison.OrdinalIgnoreCase) == true;
+
+ // Protocol, action and direction are shown translated, so the translated text is searched as well
+ return Matches(rule.Name) ||
+ Matches(rule.ProgramDisplay) ||
+ Matches(rule.Protocol.ToString()) ||
+ Matches(ResourceTranslator.Translate(ResourceIdentifier.FirewallProtocol, rule.Protocol)) ||
+ Matches(rule.Action.ToString()) ||
+ Matches(ResourceTranslator.Translate(ResourceIdentifier.FirewallRuleAction, rule.Action)) ||
+ Matches(rule.Direction.ToString()) ||
+ Matches(ResourceTranslator.Translate(ResourceIdentifier.FirewallRuleDirection, rule.Direction)) ||
+ Matches(rule.LocalAddressesDisplay) ||
+ Matches(rule.LocalPortsDisplay) ||
+ Matches(rule.RemoteAddressesDisplay) ||
+ Matches(rule.RemotePortsDisplay) ||
+ Matches(rule.Description);
};
+ _searchDispatcherTimer.Interval = GlobalStaticConfiguration.SearchDispatcherTimerTimeSpan;
+ _searchDispatcherTimer.Tick += SearchDispatcherTimer_Tick;
+
// Load firewall rules
_ = Refresh(true);
@@ -546,27 +575,8 @@ private IEnumerable GetSelectedRules()
/// Converts the hidden properties of a rule to a localized, bulleted list (e.g. "- ICMP types", "- Service").
///
/// The hidden properties.
- private static string GetHiddenPropertiesString(FirewallRuleHiddenProperties properties)
- {
- var names = new List();
-
- if (properties.HasFlag(FirewallRuleHiddenProperties.IcmpTypes))
- names.Add(Strings.IcmpTypes);
-
- if (properties.HasFlag(FirewallRuleHiddenProperties.Service))
- names.Add(Strings.Service);
-
- if (properties.HasFlag(FirewallRuleHiddenProperties.Package))
- names.Add(Strings.AppPackage);
-
- if (properties.HasFlag(FirewallRuleHiddenProperties.InterfaceAlias))
- names.Add(Strings.NetworkAdapters);
-
- if (properties.HasFlag(FirewallRuleHiddenProperties.Security))
- names.Add(Strings.UsersAndAuthentication);
-
- return DialogHelper.FormatItemList(names);
- }
+ private static string GetHiddenPropertiesString(FirewallRuleHiddenProperties properties) =>
+ DialogHelper.FormatItemList(FirewallRuleHiddenPropertiesToStringConverter.GetNames(properties));
///
/// Builds a warning which lists the given rules that are not managed by NETworkManager
@@ -748,7 +758,10 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro
/// When the initial UI delay is skipped so the first load
/// on startup feels immediate.
///
- /// if the rules were loaded successfully; otherwise, .
+ ///
+ /// if the rules were loaded successfully; otherwise (also if the rules were only loaded
+ /// partially), .
+ ///
private async Task Refresh(bool init = false)
{
if (IsRefreshing)
@@ -770,21 +783,48 @@ private async Task Refresh(bool init = false)
// Reset right before the setting is read, so only a change during the query triggers another refresh (see finally)
_refreshPending = false;
- var rules = await Firewall.GetRulesAsync(onlyManagedRules: !ShowAllRules);
+ var (rules, errors) = await Firewall.GetRulesAsync(onlyManagedRules: !ShowAllRules);
Application.Current.Dispatcher.Invoke(() =>
{
+ // Remember the selected rule, because replacing the rules clears the selection. The ID is not unique
+ // if all rules are shown (e.g. a local and a Group Policy rule with the same ID), so the source is compared too.
+ var selectedRule = SelectedResult;
+
// Replace all rules with a single notification, so the view is filtered / sorted only once
// (can be 1000+ rules if all rules are shown). ResultsView.DeferRefresh() can't be used, because
// the DataGrid accesses the view (selection / current item) while the items are changed.
Results.ReplaceAll(rules);
_hasRulesManagedByPolicy = rules.Any(rule => rule.IsManagedByPolicy);
+
+ // Select the rule again (e.g. after it was enabled or edited), so the user doesn't have to search for it.
+ // If several rules were selected, only this rule is selected again. The view scrolls it into view.
+ // Not if the rule is hidden by the search now (e.g. renamed), otherwise actions (e.g. Del) would
+ // apply to a rule that is not visible.
+ if (selectedRule != null)
+ {
+ var reloadedRule = rules.FirstOrDefault(rule =>
+ rule.Id == selectedRule.Id && rule.Source == selectedRule.Source);
+
+ if (reloadedRule != null && ResultsView.Contains(reloadedRule))
+ SelectedResult = reloadedRule;
+ }
});
- StatusMessage = string.Format(Strings.ReloadedAtX, DateTime.Now.ToShortTimeString());
IsStatusMessageDisplayed = true;
+ // Only parts of the query failed (e.g. a single rule or a filter), show that the list may be incomplete
+ if (errors.Count > 0)
+ {
+ StatusMessage = string.Format(Strings.FirewallRulesLoadedWithErrorsMessage,
+ DateTime.Now.ToShortTimeString(), errors[0]);
+
+ return false;
+ }
+
+ StatusMessage = string.Format(Strings.ReloadedAtX, DateTime.Now.ToShortTimeString());
+
return true;
}
catch (Exception ex)
@@ -872,6 +912,16 @@ private static List ParseAddressesString(string value)
return [.. value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
}
+ ///
+ /// Filters the rules once the user stopped typing (see ).
+ ///
+ private void SearchDispatcherTimer_Tick(object sender, EventArgs e)
+ {
+ _searchDispatcherTimer.Stop();
+
+ ResultsView.Refresh();
+ }
+
///
/// Called when a context menu is opened. Updates the selection dependent properties,
/// because is changed in place (no change notification).
diff --git a/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml b/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml
index e01a0e94b4..488ebf637c 100644
--- a/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml
+++ b/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml
@@ -173,11 +173,11 @@
Text="{x:Static localization:Strings.LocalAddresses}" />
+ mah:TextBoxHelper.Watermark="{x:Static localization:StaticStrings.ExampleFirewallLocalAddresses}">
-
+
@@ -203,7 +203,9 @@
Text="{x:Static localization:Strings.Program}" />
+ mah:TextBoxHelper.Watermark="{x:Static localization:StaticStrings.ExampleFirewallProgram}"
+ Style="{StaticResource BrowseFolderTextBox}"
+ mah:TextBoxHelper.ButtonCommand="{Binding BrowseFileCommand}">
diff --git a/Source/NETworkManager/Views/FirewallView.xaml b/Source/NETworkManager/Views/FirewallView.xaml
index 4218037b4f..a0f1d51e5a 100644
--- a/Source/NETworkManager/Views/FirewallView.xaml
+++ b/Source/NETworkManager/Views/FirewallView.xaml
@@ -25,8 +25,13 @@
+
+
+
+
+
@@ -385,7 +390,7 @@
-
@@ -641,6 +646,8 @@
+
+
@@ -687,6 +694,21 @@
Style="{StaticResource InfoTextBlock}" />
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Views/FirewallView.xaml.cs b/Source/NETworkManager/Views/FirewallView.xaml.cs
index 1e3ba55bdf..d07b388d0b 100644
--- a/Source/NETworkManager/Views/FirewallView.xaml.cs
+++ b/Source/NETworkManager/Views/FirewallView.xaml.cs
@@ -1,9 +1,11 @@
using System;
+using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
+using System.Windows.Threading;
using NETworkManager.ViewModels;
namespace NETworkManager.Views;
@@ -25,9 +27,31 @@ public FirewallView()
{
InitializeComponent();
DataContext = _viewModel;
+
+ _viewModel.PropertyChanged += ViewModel_PropertyChanged;
}
#region Events
+ ///
+ /// Scrolls the selected rule into view, e.g. when it is selected again after the rules were reloaded
+ /// (see ).
+ ///
+ private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(FirewallViewModel.SelectedResult) || _viewModel.SelectedResult == null)
+ return;
+
+ // Wait until the DataGrid has processed the new items and the selection
+ Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
+ {
+ var rule = _viewModel.SelectedResult;
+
+ // The rule may be hidden by the search
+ if (rule != null && DataGridFirewallRules.Items.Contains(rule))
+ DataGridFirewallRules.ScrollIntoView(rule);
+ }));
+ }
+
///
/// Set data context for menus.
///
diff --git a/Website/docs/application/firewall.md b/Website/docs/application/firewall.md
index d1a5106ea1..5123129acf 100644
--- a/Website/docs/application/firewall.md
+++ b/Website/docs/application/firewall.md
@@ -24,6 +24,12 @@ Without administrator privileges, the view is read-only. Use the **Restart as ad
The **Program** column shows the file name of the program the rule applies to (e.g. `putty.exe`). The full path (e.g. `%ProgramFiles%\PuTTY\putty.exe`) is shown as tooltip and in the row details.
+Click the chevron in front of a rule to show its details: program (full path), interface type, network profiles, description, source (`Local`, `Group Policy`, `Mobile Device Management (MDM)` or `Windows (runtime)`) and, if set, the [unsupported properties](#rules-not-managed-by-networkmanager) the rule uses (e.g. ICMP types or a service).
+
+The selected rule stays selected (and is scrolled into view) when the list is reloaded, e.g. after it was enabled, disabled or edited. If several rules were selected, only one of them stays selected.
+
+The search above the list finds rules by name, program, direction, action, protocol, local / remote addresses, local / remote ports and description. The list is filtered after a short delay while typing.
+
### Toolbar
| Button | Description |
@@ -35,7 +41,7 @@ The **Program** column shows the file name of the program the rule applies to (e
By default, only rules managed by NETworkManager (display name prefixed with `NETworkManager_`) are shown. If [**Show all rules**](#show-all-rules) is enabled in the settings, all active Windows Firewall rules are shown (e.g. Windows built-in rules, rules created by other applications or from Group Policy).
-When all rules are shown, an additional column shows where a rule comes from. Click the column header to sort by it.
+When all rules are shown, an additional column shows where a rule comes from. Click the column header to sort by it (managed by NETworkManager, not managed by NETworkManager, then managed by a policy).
| Icon | Description |
|------|-------------|
@@ -71,7 +77,7 @@ Multiple rules can be selected (`Ctrl` / `Shift` + click) to enable, disable or
| **Copy as** | Opens the dialog pre-filled with the selected rule to create a new rule based on it (single rule only, not for rules with [unsupported values](#rules-not-managed-by-networkmanager)) |
| **Delete** | Removes the selected rule or rules (after confirmation) |
| **Copy** | Copies the rule information to the clipboard (single rule only) |
-| **Export** | Exports the selected rules (or all rules) to a file |
+| **Export** | Exports the selected rules (or all rules) to a file (CSV, XML or JSON, incl. whether the rule is managed by NETworkManager and its source) |
### Keyboard shortcuts
@@ -85,6 +91,8 @@ Multiple rules can be selected (`Ctrl` / `Shift` + click) to enable, disable or
Before enabling, disabling or deleting, it is checked that all selected rules still exist. If one of them was removed in the meantime (e.g. in the Windows Firewall management console), none of the selected rules is changed and an error is shown. If another error occurs while the rules are changed (e.g. access denied), some of the selected rules may already be changed. The rule list is reloaded after every action (also after an error), so it always shows the current state.
+If the rules can't be loaded (e.g. because the Windows Firewall service is not running), an error is shown instead of an empty list. If only some rules or properties can't be loaded, the loaded rules are shown together with a message that the list may be incomplete (details are written to the log file).
+
:::
## Add rule
@@ -241,7 +249,7 @@ One or more local addresses the rule applies to. Multiple entries are separated
- `192.168.1.0/24`
- `10.0.0.0/255.0.0.0`
- `192.168.1.10-192.168.1.50`
-- `LocalSubnet; 192.168.10.0/24`
+- `192.168.1.10; 10.0.0.0/8`
:::note
@@ -253,9 +261,9 @@ The following formats are accepted:
- IPv4 / IPv6 subnet by prefix length (e.g. `1.2.3.0/24`, `fe80::/48`)
- IPv4 subnet by subnet mask (e.g. `10.0.0.0/255.0.0.0`)
- IPv4 / IPv6 range (e.g. `1.2.3.4-1.2.3.10`, `fe80::1-fe80::9`)
-- Keyword: `Any`, `LocalSubnet`, `DNS`, `DHCP`, `WINS`, `DefaultGateway`, `Internet`, `Intranet`, `IntranetRemoteAccess`, `PlayToDevice`, `CaptivePortal`. Keywords can be restricted to IPv4 or IPv6 by appending `4` or `6` (e.g. `LocalSubnet4`, `Intranet6`).
+- Keyword: `Any`
-According to Microsoft, the only keyword officially supported by `LocalAddress` is `Any`. The other keywords are intended for use with [Remote addresses](#remote-addresses).
+Other keywords (e.g. `LocalSubnet` or `DNS`) are not accepted by Windows for local addresses, they can only be used for [Remote addresses](#remote-addresses).
:::
@@ -279,7 +287,13 @@ One or more remote addresses the rule applies to. Multiple entries are separated
An empty value means **Any** address.
-The same formats as [Local addresses](#local-addresses) are accepted (single address, subnet, range, or keyword).
+The following formats are accepted:
+
+- Single IPv4 / IPv6 address (e.g. `1.2.3.4`, `fe80::1`)
+- IPv4 / IPv6 subnet by prefix length (e.g. `1.2.3.0/24`, `fe80::/48`)
+- IPv4 subnet by subnet mask (e.g. `10.0.0.0/255.0.0.0`)
+- IPv4 / IPv6 range (e.g. `1.2.3.4-1.2.3.10`, `fe80::1-fe80::9`)
+- Keyword: `Any`, `LocalSubnet`, `DNS`, `DHCP`, `WINS`, `DefaultGateway`, `Internet`, `Intranet`, `IntranetRemoteAccess`, `PlayToDevice`, `CaptivePortal`. Keywords except `Any` can be restricted to IPv4 or IPv6 by appending `4` or `6` (e.g. `LocalSubnet4`, `Intranet6`).
:::
@@ -295,7 +309,7 @@ Full path to an executable the rule applies to. When set, the rule only matches
:::note
-If a path is provided, the file must exist on the local computer. Environment variables (e.g. `%SystemRoot%\System32\svchost.exe`) are supported.
+If a path is provided, the file must exist on the local computer. Environment variables (e.g. `%SystemRoot%\System32\svchost.exe`) are supported. Use the button in the text box to select the program.
An empty value means **Any** program.
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 5a7db496e3..896d1066b2 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -94,6 +94,14 @@ Release date: **xx.xx.2026**
- Added a **Program** column that shows the file name of the program (full path as tooltip). [#3630](https://github.com/BornToBeRoot/NETworkManager/pull/3630)
- New **Settings > Firewall** page. [#3630](https://github.com/BornToBeRoot/NETworkManager/pull/3630)
- Added the **Show all rules** option (**Settings > Firewall**) to show all active Windows Firewall rules (including rules from Group Policy / MDM) instead of only the rules managed by NETworkManager. A new column with an icon shows whether a rule is managed by NETworkManager, not managed, or managed by a policy. Rules managed by a policy can only be copied. Rules not managed by NETworkManager can be enabled, disabled, deleted (after confirming a warning) or used as a template via **Copy as...** (with a warning if the rule uses properties that are not copied, e.g. ICMP types, a service, an app package, specific network adapters or users / required authentication), but editing them is not supported (yet). Rules with values that are not supported by NETworkManager (e.g. port keywords like `RPC`, a combination of interface types, a protocol like `IGMP` or the program `System`) are shown correctly, but can't be edited or copied. [#3630](https://github.com/BornToBeRoot/NETworkManager/pull/3630)
+- The search now also finds rules by program, ports, addresses and description, and by the translated direction, action and protocol. The list is filtered after a short delay while typing, so typing stays smooth with many rules. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- Sorting by the rule source column (**Show all rules**) now groups rules managed by NETworkManager, rules not managed by NETworkManager and rules from Group Policy / MDM / Windows, instead of mixing the last two. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- The **Program** field in the add / edit / copy rule dialog now shows an example path of a program instead of the path to PowerShell. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- The program of a rule can now be selected with a file dialog in the add / edit / copy rule dialog. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- The selected rule stays selected and visible when the list is reloaded (e.g. after enabling, disabling or editing it), instead of jumping back to the top of the list. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- The row details now show the source of a rule (local, Group Policy, MDM or Windows) and the unsupported properties it uses (e.g. ICMP types or a service), which are not copied via **Copy as...**. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- The export now contains whether a rule is managed by NETworkManager and its source. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- Faster loading of the rules managed by NETworkManager if there are many of them (the ports, addresses, programs etc. of all rules are queried at once instead of per rule). [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
## Bug Fixes
@@ -101,6 +109,10 @@ Release date: **xx.xx.2026**
**Firewall**
+- Fixed `F5` not refreshing the rule list, although it is documented as shortcut. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- Fixed an empty rule list with "Reloaded at ..." being shown if the rules could not be loaded (e.g. the Windows Firewall service is not running). An error is shown now, and a message if the rules could only be loaded partially. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- Fixed the Firewall not working until the application was restarted if loading the PowerShell module `NetSecurity` failed once. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
+- Fixed keywords like `LocalSubnet` or `DNS` being accepted for **Local addresses** in the add / edit / copy rule dialog, although Windows only accepts `Any` there (saving the rule failed). The same applies to `Any4` / `Any6` for local and remote addresses. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637)
- Fixed a horizontal scrollbar appearing in the add / edit / copy rule dialog when the name or description is very long. [#3630](https://github.com/BornToBeRoot/NETworkManager/pull/3630)
- Fixed port keywords (e.g. `RPC`) and combinations of interface types being shown as **Any**. Editing a rule that was changed outside of NETworkManager to use such values no longer silently removes them (the rule can't be edited in this case). [#3630](https://github.com/BornToBeRoot/NETworkManager/pull/3630)
- Fixed the program of a rule not being shown in the row details and the edit dialog. Saving the edit dialog then removed the program from the rule, so the rule applied to all programs. [#3630](https://github.com/BornToBeRoot/NETworkManager/pull/3630)