diff --git a/Source/NETworkManager.Converters/FirewallInterfaceTypeToStringConverter.cs b/Source/NETworkManager.Converters/FirewallInterfaceTypeToStringConverter.cs
new file mode 100644
index 0000000000..f15e2ac52b
--- /dev/null
+++ b/Source/NETworkManager.Converters/FirewallInterfaceTypeToStringConverter.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 FirewallInterfaceTypeToStringConverter : IValueConverter
+{
+ ///
+ /// Convert to translated .
+ ///
+ /// Object from type .
+ ///
+ ///
+ ///
+ /// Translated .
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not FirewallInterfaceType interfaceType
+ ? "-/-"
+ : ResourceTranslator.Translate(ResourceIdentifier.FirewallInterfaceType, interfaceType);
+ }
+
+ ///
+ /// !!! Method not implemented !!!
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/FirewallNetworkProfilesToStringConverter.cs b/Source/NETworkManager.Converters/FirewallNetworkProfilesToStringConverter.cs
new file mode 100644
index 0000000000..10ce69e7e3
--- /dev/null
+++ b/Source/NETworkManager.Converters/FirewallNetworkProfilesToStringConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Windows.Data;
+using NETworkManager.Localization.Resources;
+
+namespace NETworkManager.Converters;
+
+///
+/// Convert a array (Domain, Private, Public) representing firewall network
+/// profiles to a localized , or vice versa.
+///
+public sealed class FirewallNetworkProfilesToStringConverter : IValueConverter
+{
+ ///
+ /// Convert a array (Domain, Private, Public) to a localized .
+ /// Returns null when all three profiles are active so that a TargetNullValue binding
+ /// can supply the translated "Any" label.
+ ///
+ /// A array with exactly three elements.
+ ///
+ ///
+ ///
+ /// Localized, comma-separated profile list (e.g. "Domain, Private, Public").
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is not bool[] { Length: 3 } profiles)
+ return "-/-";
+
+ var names = new List(3);
+ if (profiles[0]) names.Add(Strings.Domain);
+ if (profiles[1]) names.Add(Strings.Private);
+ if (profiles[2]) names.Add(Strings.Public);
+
+ return names.Count == 0 ? "-" : string.Join(", ", names);
+ }
+
+ ///
+ /// !!! Method not implemented !!!
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Source/NETworkManager.Converters/FirewallProtocolToStringConverter.cs b/Source/NETworkManager.Converters/FirewallProtocolToStringConverter.cs
new file mode 100644
index 0000000000..daced724af
--- /dev/null
+++ b/Source/NETworkManager.Converters/FirewallProtocolToStringConverter.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 FirewallProtocolToStringConverter : IValueConverter
+{
+ ///
+ /// Convert to translated .
+ ///
+ /// Object from type .
+ ///
+ ///
+ ///
+ /// Translated .
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not FirewallProtocol protocol
+ ? "-/-"
+ : ResourceTranslator.Translate(ResourceIdentifier.FirewallProtocol, protocol);
+ }
+
+ ///
+ /// !!! Method not implemented !!!
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/FirewallRuleActionToStringConverter.cs b/Source/NETworkManager.Converters/FirewallRuleActionToStringConverter.cs
new file mode 100644
index 0000000000..052932ab47
--- /dev/null
+++ b/Source/NETworkManager.Converters/FirewallRuleActionToStringConverter.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 FirewallRuleActionToStringConverter : IValueConverter
+{
+ ///
+ /// Convert to translated .
+ ///
+ /// Object from type .
+ ///
+ ///
+ ///
+ /// Translated .
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not FirewallRuleAction action
+ ? "-/-"
+ : ResourceTranslator.Translate(ResourceIdentifier.FirewallRuleAction, action);
+ }
+
+ ///
+ /// !!! Method not implemented !!!
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/FirewallRuleDirectionToStringConverter.cs b/Source/NETworkManager.Converters/FirewallRuleDirectionToStringConverter.cs
new file mode 100644
index 0000000000..a180ff65c7
--- /dev/null
+++ b/Source/NETworkManager.Converters/FirewallRuleDirectionToStringConverter.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 FirewallRuleDirectionToStringConverter : IValueConverter
+{
+ ///
+ /// Convert to translated .
+ ///
+ /// Object from type .
+ ///
+ ///
+ ///
+ /// Translated .
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not FirewallRuleDirection direction
+ ? "-/-"
+ : ResourceTranslator.Translate(ResourceIdentifier.FirewallRuleDirection, direction);
+ }
+
+ ///
+ /// !!! Method not implemented !!!
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/NetworkProfileToStringConverter.cs b/Source/NETworkManager.Converters/NetworkProfileToStringConverter.cs
new file mode 100644
index 0000000000..841e3e4177
--- /dev/null
+++ b/Source/NETworkManager.Converters/NetworkProfileToStringConverter.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using NETworkManager.Localization.Resources;
+using NETworkManager.Models.Network;
+
+namespace NETworkManager.Converters;
+
+///
+/// Convert to a localized or vice versa.
+///
+public sealed class NetworkProfileToStringConverter : IValueConverter
+{
+ ///
+ /// Convert to a localized .
+ ///
+ /// Object from type .
+ ///
+ ///
+ ///
+ /// Localized representing the network profile.
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not NetworkProfile profile
+ ? "-/-"
+ : profile switch
+ {
+ NetworkProfile.Domain => Strings.Domain,
+ NetworkProfile.Private => Strings.Private,
+ NetworkProfile.Public => Strings.Public,
+ _ => "-/-"
+ };
+ }
+
+ ///
+ /// !!! 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 0cecf42520..1f36bc0209 100644
--- a/Source/NETworkManager.Localization/ResourceIdentifier.cs
+++ b/Source/NETworkManager.Localization/ResourceIdentifier.cs
@@ -22,5 +22,9 @@ public enum ResourceIdentifier
TcpState,
Theme,
TimeUnit,
- WiFiConnectionStatus
+ WiFiConnectionStatus,
+ FirewallProtocol,
+ FirewallInterfaceType,
+ FirewallRuleDirection,
+ FirewallRuleAction
}
\ 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 35c35d1075..533505c677 100644
--- a/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
@@ -734,5 +734,23 @@ public static string XML {
return ResourceManager.GetString("XML", resourceCulture);
}
}
+
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die MyApp - HTTP ähnelt.
+ ///
+ public static string ExampleFirewallRuleName {
+ get {
+ return ResourceManager.GetString("ExampleFirewallRuleName", resourceCulture);
+ }
+ }
+
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die 10.0.0.0/8; LocalSubnet ähnelt.
+ ///
+ public static string ExampleFirewallAddresses {
+ get {
+ return ResourceManager.GetString("ExampleFirewallAddresses", resourceCulture);
+ }
+ }
}
}
diff --git a/Source/NETworkManager.Localization/Resources/StaticStrings.resx b/Source/NETworkManager.Localization/Resources/StaticStrings.resx
index 957880db82..006de73953 100644
--- a/Source/NETworkManager.Localization/Resources/StaticStrings.resx
+++ b/Source/NETworkManager.Localization/Resources/StaticStrings.resx
@@ -342,4 +342,10 @@
borntoberoot.net or 1.1.1.1
+
+ MyApp - HTTP
+
+
+ 10.0.0.0/8; LocalSubnet
+
\ No newline at end of file
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index 68086ee533..8a56008b17 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -4128,7 +4128,16 @@ public static string Firewall {
return ResourceManager.GetString("Firewall", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to Read-only mode. Modifying firewall rules requires elevated rights!
+ ///
+ public static string FirewallAdminMessage {
+ get {
+ return ResourceManager.GetString("FirewallAdminMessage", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Firewall rules.
///
@@ -4813,6 +4822,15 @@ public static string HostsFileEditorAdminMessage {
return ResourceManager.GetString("HostsFileEditorAdminMessage", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Open hosts file.
+ ///
+ public static string OpenHostsFile {
+ get {
+ return ResourceManager.GetString("OpenHostsFile", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to The entry was not found in the "hosts" file! Maybe the file has been modified..
@@ -7343,7 +7361,25 @@ public static string Program {
return ResourceManager.GetString("Program", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to Private.
+ ///
+ public static string Private {
+ get {
+ return ResourceManager.GetString("Private", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Public.
+ ///
+ public static string Public {
+ get {
+ return ResourceManager.GetString("Public", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Protocol.
///
@@ -11553,6 +11589,15 @@ public static string Block {
}
}
+ ///
+ /// Looks up a localized string similar to Network profile.
+ ///
+ public static string NetworkProfile {
+ get {
+ return ResourceManager.GetString("NetworkProfile", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Network profiles.
///
@@ -11579,7 +11624,16 @@ public static string FailedToLoadFirewallRulesMessage {
return ResourceManager.GetString("FailedToLoadFirewallRulesMessage", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to The selected firewall rule is permanently deleted: {0}.
+ ///
+ public static string DeleteFirewallRuleMessage {
+ get {
+ return ResourceManager.GetString("DeleteFirewallRuleMessage", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to WPS.
///
@@ -11714,5 +11768,50 @@ public static string ZipCode {
return ResourceManager.GetString("ZipCode", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Local ports.
+ ///
+ public static string LocalPorts {
+ get {
+ return ResourceManager.GetString("LocalPorts", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Remote ports.
+ ///
+ public static string RemotePorts {
+ get {
+ return ResourceManager.GetString("RemotePorts", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Local addresses.
+ ///
+ public static string LocalAddresses {
+ get {
+ return ResourceManager.GetString("LocalAddresses", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Remote addresses.
+ ///
+ public static string RemoteAddresses {
+ get {
+ return ResourceManager.GetString("RemoteAddresses", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Enter a valid IP address, subnet (e.g. 10.0.0.0/8) or keyword (e.g. LocalSubnet, Internet).
+ ///
+ public static string EnterValidFirewallAddress {
+ get {
+ return ResourceManager.GetString("EnterValidFirewallAddress", resourceCulture);
+ }
+ }
}
}
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 789507cf64..be263b510f 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -2151,6 +2151,12 @@ is disabled!
Program
+
+ Private
+
+
+ Public
+
The selected custom command will be deleted permanently.
@@ -3796,6 +3802,9 @@ Right-click for more options.
Read-only mode. Modifying the hosts file requires elevated rights!
+
+ Open hosts file
+
Comment
@@ -4037,6 +4046,9 @@ You can copy your profile files from “{0}” to “{1}” to migrate your exis
Block
+
+ Network profile
+
Network profiles
@@ -4046,4 +4058,96 @@ You can copy your profile files from “{0}” to “{1}” to migrate your exis
Failed to load firewall rules. {0}
+
+ The selected firewall rule is permanently deleted:
+
+{0}
+
+
+ Any
+
+
+ TCP
+
+
+ UDP
+
+
+ ICMPv4
+
+
+ ICMPv6
+
+
+ HOPOPT
+
+
+ GRE
+
+
+ IPv6
+
+
+ IPv6-Route
+
+
+ IPv6-Frag
+
+
+ IPv6-NoNxt
+
+
+ IPv6-Opts
+
+
+ VRRP
+
+
+ PGM
+
+
+ L2TP
+
+
+ Any
+
+
+ Wired
+
+
+ Wireless
+
+
+ Remote access
+
+
+ Read-only mode. Modifying firewall rules requires elevated rights!
+
+
+ Inbound
+
+
+ Outbound
+
+
+ Block
+
+
+ Allow
+
+
+ Local ports
+
+
+ Remote ports
+
+
+ Local addresses
+
+
+ Remote addresses
+
+
+ Enter a valid IP address, subnet (e.g. 10.0.0.0/8) or keyword (e.g. LocalSubnet, Internet)
+
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Export/ExportManager.FirewallRule.cs b/Source/NETworkManager.Models/Export/ExportManager.FirewallRule.cs
new file mode 100644
index 0000000000..ec8cb81f8f
--- /dev/null
+++ b/Source/NETworkManager.Models/Export/ExportManager.FirewallRule.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml.Linq;
+using NETworkManager.Models.Firewall;
+using NETworkManager.Utilities;
+using Newtonsoft.Json;
+
+namespace NETworkManager.Models.Export;
+
+public static partial class ExportManager
+{
+ ///
+ /// Method to export objects from type to a file.
+ ///
+ /// Path to the export file.
+ /// Allowed are CSV, XML or JSON.
+ /// Objects as to export.
+ public static void Export(string filePath, ExportFileType fileType, IReadOnlyList collection)
+ {
+ switch (fileType)
+ {
+ case ExportFileType.Csv:
+ CreateCsv(collection, filePath);
+ break;
+ case ExportFileType.Xml:
+ CreateXml(collection, filePath);
+ break;
+ case ExportFileType.Json:
+ CreateJson(collection, filePath);
+ break;
+ case ExportFileType.Txt:
+ default:
+ throw new ArgumentOutOfRangeException(nameof(fileType), fileType, null);
+ }
+ }
+
+ ///
+ /// Creates a CSV file from the given collection.
+ ///
+ private static void CreateCsv(IEnumerable collection, string filePath)
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine(
+ $"{nameof(FirewallRule.Name)}," +
+ $"{nameof(FirewallRule.IsEnabled)}," +
+ $"{nameof(FirewallRule.Direction)}," +
+ $"{nameof(FirewallRule.Action)}," +
+ $"{nameof(FirewallRule.Protocol)}," +
+ $"{nameof(FirewallRule.LocalPorts)}," +
+ $"{nameof(FirewallRule.RemotePorts)}," +
+ $"{nameof(FirewallRule.LocalAddresses)}," +
+ $"{nameof(FirewallRule.RemoteAddresses)}," +
+ $"{nameof(FirewallRule.NetworkProfiles)}," +
+ $"{nameof(FirewallRule.InterfaceType)}," +
+ $"{nameof(FirewallRule.Program)}," +
+ $"{nameof(FirewallRule.Description)}");
+
+ foreach (var rule in collection)
+ sb.AppendLine(
+ $"{CsvHelper.QuoteString(rule.Name)}," +
+ $"{rule.IsEnabled}," +
+ $"{rule.Direction}," +
+ $"{rule.Action}," +
+ $"{rule.Protocol}," +
+ $"{CsvHelper.QuoteString(rule.LocalPortsDisplay)}," +
+ $"{CsvHelper.QuoteString(rule.RemotePortsDisplay)}," +
+ $"{CsvHelper.QuoteString(rule.LocalAddressesDisplay)}," +
+ $"{CsvHelper.QuoteString(rule.RemoteAddressesDisplay)}," +
+ $"{CsvHelper.QuoteString(rule.NetworkProfilesDisplay)}," +
+ $"{rule.InterfaceType}," +
+ $"{CsvHelper.QuoteString(rule.ProgramDisplay)}," +
+ $"{CsvHelper.QuoteString(rule.Description)}");
+
+ File.WriteAllText(filePath, sb.ToString());
+ }
+
+ ///
+ /// Creates an XML file from the given collection.
+ ///
+ private static void CreateXml(IEnumerable collection, string filePath)
+ {
+ var document = new XDocument(DefaultXDeclaration,
+ new XElement(nameof(ApplicationName.Firewall),
+ new XElement(nameof(FirewallRule) + "s",
+ 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.Direction), rule.Direction),
+ new XElement(nameof(FirewallRule.Action), rule.Action),
+ new XElement(nameof(FirewallRule.Protocol), rule.Protocol),
+ new XElement(nameof(FirewallRule.LocalPorts), rule.LocalPortsDisplay),
+ new XElement(nameof(FirewallRule.RemotePorts), rule.RemotePortsDisplay),
+ new XElement(nameof(FirewallRule.LocalAddresses), rule.LocalAddressesDisplay),
+ new XElement(nameof(FirewallRule.RemoteAddresses), rule.RemoteAddressesDisplay),
+ new XElement(nameof(FirewallRule.NetworkProfiles), rule.NetworkProfilesDisplay),
+ new XElement(nameof(FirewallRule.InterfaceType), rule.InterfaceType),
+ new XElement(nameof(FirewallRule.Program), rule.ProgramDisplay),
+ new XElement(nameof(FirewallRule.Description), rule.Description)))));
+
+ document.Save(filePath);
+ }
+
+ ///
+ /// Creates a JSON file from the given collection.
+ ///
+ private static void CreateJson(IReadOnlyList collection, string filePath)
+ {
+ var jsonData = new object[collection.Count];
+
+ for (var i = 0; i < collection.Count; i++)
+ {
+ var rule = collection[i];
+ jsonData[i] = new
+ {
+ rule.Name,
+ rule.IsEnabled,
+ Direction = rule.Direction.ToString(),
+ Action = rule.Action.ToString(),
+ Protocol = rule.Protocol.ToString(),
+ LocalPorts = rule.LocalPortsDisplay,
+ RemotePorts = rule.RemotePortsDisplay,
+ LocalAddresses = rule.LocalAddressesDisplay,
+ RemoteAddresses = rule.RemoteAddressesDisplay,
+ NetworkProfiles = rule.NetworkProfilesDisplay,
+ InterfaceType = rule.InterfaceType.ToString(),
+ Program = rule.ProgramDisplay,
+ rule.Description
+ };
+ }
+
+ File.WriteAllText(filePath, JsonConvert.SerializeObject(jsonData, Formatting.Indented));
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Firewall/Firewall.cs b/Source/NETworkManager.Models/Firewall/Firewall.cs
new file mode 100644
index 0000000000..58ef612239
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/Firewall.cs
@@ -0,0 +1,533 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management.Automation.Runspaces;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using SMA = System.Management.Automation;
+using log4net;
+
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Provides static methods to read and modify Windows Firewall rules via PowerShell.
+/// All operations share a single that is initialized once with
+/// the required execution policy and the NetSecurity module, reducing per-call overhead.
+/// A serializes access so the runspace is never used concurrently.
+///
+public class Firewall
+{
+ #region Variables
+
+ ///
+ /// The logger for this class.
+ ///
+ private static readonly ILog Log = LogManager.GetLogger(typeof(Firewall));
+
+ ///
+ /// Prefix applied to the DisplayName of every rule managed by NETworkManager.
+ /// Used to scope Get-NetFirewallRule queries to only our own rules.
+ ///
+ private const string RuleIdentifier = "NETworkManager_";
+
+ ///
+ /// Ensures that only one PowerShell pipeline runs on at a time.
+ ///
+ private static readonly SemaphoreSlim Lock = new(1, 1);
+
+ ///
+ /// Shared PowerShell runspace, initialized once in the static constructor with
+ /// Set-ExecutionPolicy Bypass and Import-Module NetSecurity.
+ ///
+ private static readonly Runspace SharedRunspace;
+
+ ///
+ /// Opens and runs the one-time initialization script
+ /// so that subsequent operations do not need to repeat the module import.
+ ///
+ static Firewall()
+ {
+ SharedRunspace = RunspaceFactory.CreateRunspace();
+ SharedRunspace.Open();
+
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+ ps.AddScript(@"
+Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
+Import-Module NetSecurity -ErrorAction Stop").Invoke();
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Retrieves all Windows Firewall rules whose display name starts with
+ /// and maps each one to a object.
+ /// 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.
+ ///
+ ///
+ /// A list of objects representing the matching rules.
+ ///
+ public static async Task> GetRulesAsync()
+ {
+ await Lock.WaitAsync();
+ try
+ {
+ return await Task.Run(() =>
+ {
+ var rules = new List();
+
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+
+ 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
+ [PSCustomObject]@{{
+ Id = $rule.ID
+ DisplayName = $rule.DisplayName
+ Enabled = ($rule.Enabled -eq 'True')
+ Description = $rule.Description
+ Direction = [string]$rule.Direction
+ Action = [string]$rule.Action
+ Protocol = $portFilter.Protocol
+ LocalPort = ($portFilter.LocalPort -join ',')
+ RemotePort = ($portFilter.RemotePort -join ',')
+ LocalAddress = ($addressFilter.LocalAddress -join ',')
+ RemoteAddress = ($addressFilter.RemoteAddress -join ',')
+ Profile = [string]$rule.Profile
+ InterfaceType = [string]$ifTypeFilter.InterfaceType
+ Program = $appFilter.Program
+ }}
+}}");
+
+ var results = ps.Invoke();
+
+ if (ps.Streams.Error.Count > 0)
+ {
+ foreach (var error in ps.Streams.Error)
+ Log.Warn($"PowerShell error: {error}");
+ }
+
+ foreach (var result in results)
+ {
+ try
+ {
+ var displayName = result.Properties["DisplayName"]?.Value?.ToString() ?? string.Empty;
+
+ var rule = new FirewallRule
+ {
+ Id = result.Properties["Id"]?.Value?.ToString() ?? string.Empty,
+ IsEnabled = result.Properties["Enabled"]?.Value as bool? == true,
+ Name = displayName.StartsWith(RuleIdentifier, StringComparison.Ordinal)
+ ? displayName[RuleIdentifier.Length..]
+ : displayName,
+ Description = result.Properties["Description"]?.Value?.ToString() ?? string.Empty,
+ Direction = ParseDirection(result.Properties["Direction"]?.Value?.ToString()),
+ Action = ParseAction(result.Properties["Action"]?.Value?.ToString()),
+ Protocol = ParseProtocol(result.Properties["Protocol"]?.Value?.ToString()),
+ LocalPorts = ParsePorts(result.Properties["LocalPort"]?.Value?.ToString()),
+ RemotePorts = ParsePorts(result.Properties["RemotePort"]?.Value?.ToString()),
+ LocalAddresses = ParseAddresses(result.Properties["LocalAddress"]?.Value?.ToString()),
+ RemoteAddresses = ParseAddresses(result.Properties["RemoteAddress"]?.Value?.ToString()),
+ NetworkProfiles = ParseProfile(result.Properties["Profile"]?.Value?.ToString()),
+ InterfaceType = ParseInterfaceType(result.Properties["InterfaceType"]?.Value?.ToString()),
+ };
+
+ var program = result.Properties["Program"]?.Value as string;
+
+ if (!string.IsNullOrWhiteSpace(program) && !program.Equals("Any", StringComparison.OrdinalIgnoreCase))
+ rule.Program = new FirewallRuleProgram(program);
+
+ rules.Add(rule);
+ }
+ catch (Exception ex)
+ {
+ Log.Warn($"Failed to parse firewall rule: {ex.Message}");
+ }
+ }
+
+ return rules;
+ });
+ }
+ finally
+ {
+ Lock.Release();
+ }
+ }
+
+ ///
+ /// Enables or disables the given by running
+ /// Enable-NetFirewallRule or Disable-NetFirewallRule against
+ /// the rule's internal .
+ ///
+ ///
+ /// The firewall rule to modify.
+ ///
+ ///
+ /// to enable the rule; to disable it.
+ ///
+ ///
+ /// Thrown when the PowerShell pipeline reports one or more errors.
+ ///
+ public static async Task SetRuleEnabledAsync(FirewallRule rule, bool enabled)
+ {
+ await Lock.WaitAsync();
+ try
+ {
+ await Task.Run(() =>
+ {
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+
+ ps.AddScript($@"{(enabled ? "Enable" : "Disable")}-NetFirewallRule -Name '{rule.Id}'");
+ ps.Invoke();
+
+ if (ps.Streams.Error.Count > 0)
+ throw new Exception(string.Join("; ", ps.Streams.Error));
+ });
+ }
+ finally
+ {
+ Lock.Release();
+ }
+ }
+
+ ///
+ /// Permanently removes the given by running
+ /// Remove-NetFirewallRule against the rule's internal .
+ ///
+ ///
+ /// The firewall rule to delete.
+ ///
+ ///
+ /// Thrown when the PowerShell pipeline reports one or more errors.
+ ///
+ public static async Task DeleteRuleAsync(FirewallRule rule)
+ {
+ await Lock.WaitAsync();
+ try
+ {
+ await Task.Run(() =>
+ {
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+
+ ps.AddScript($@"Remove-NetFirewallRule -Name '{rule.Id}'");
+ ps.Invoke();
+
+ if (ps.Streams.Error.Count > 0)
+ throw new Exception(string.Join("; ", ps.Streams.Error));
+ });
+ }
+ finally
+ {
+ Lock.Release();
+ }
+ }
+
+ ///
+ /// Creates a new Windows Firewall rule with the properties specified in .
+ /// The rule's is prefixed with so
+ /// it is picked up by on the next refresh.
+ ///
+ ///
+ /// The firewall rule to create.
+ ///
+ ///
+ /// Thrown when the PowerShell pipeline reports one or more errors.
+ ///
+ public static async Task AddRuleAsync(FirewallRule rule)
+ {
+ await Lock.WaitAsync();
+ try
+ {
+ await Task.Run(() =>
+ {
+ using var ps = SMA.PowerShell.Create();
+ ps.Runspace = SharedRunspace;
+
+ ps.AddScript(BuildAddScript(rule));
+ ps.Invoke();
+
+ if (ps.Streams.Error.Count > 0)
+ throw new Exception(string.Join("; ", ps.Streams.Error));
+ });
+ }
+ finally
+ {
+ Lock.Release();
+ }
+ }
+
+ ///
+ /// Builds the PowerShell script that calls New-NetFirewallRule with all
+ /// properties from .
+ ///
+ ///
+ /// The firewall rule whose properties are used to build the script.
+ ///
+ private static string BuildAddScript(FirewallRule rule)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("$params = @{");
+ sb.AppendLine($" DisplayName = '{RuleIdentifier}{EscapePs(rule.Name)}'");
+ sb.AppendLine($" Enabled = '{(rule.IsEnabled ? "True" : "False")}'");
+ sb.AppendLine($" Direction = '{rule.Direction}'");
+ sb.AppendLine($" Action = '{rule.Action}'");
+ sb.AppendLine($" Protocol = '{GetProtocolString(rule.Protocol)}'");
+ sb.AppendLine($" InterfaceType = '{GetInterfaceTypeString(rule.InterfaceType)}'");
+ sb.AppendLine($" Profile = '{GetProfileString(rule.NetworkProfiles)}'");
+ sb.AppendLine("}");
+
+ if (!string.IsNullOrWhiteSpace(rule.Description))
+ sb.AppendLine($"$params['Description'] = '{EscapePs(rule.Description)}'");
+
+ if (rule.Protocol is FirewallProtocol.TCP or FirewallProtocol.UDP)
+ {
+ if (rule.LocalPorts.Count > 0)
+ sb.AppendLine($"$params['LocalPort'] = '{FirewallRule.PortsToString(rule.LocalPorts, ',', false)}'");
+
+ if (rule.RemotePorts.Count > 0)
+ sb.AppendLine($"$params['RemotePort'] = '{FirewallRule.PortsToString(rule.RemotePorts, ',', false)}'");
+ }
+
+ if (rule.LocalAddresses.Count > 0)
+ sb.AppendLine($"$params['LocalAddress'] = '{string.Join(',', rule.LocalAddresses.Select(EscapePs))}'");
+
+ if (rule.RemoteAddresses.Count > 0)
+ sb.AppendLine($"$params['RemoteAddress'] = '{string.Join(',', rule.RemoteAddresses.Select(EscapePs))}'");
+
+ if (rule.Program != null && !string.IsNullOrWhiteSpace(rule.Program.Name))
+ sb.AppendLine($"$params['Program'] = '{EscapePs(rule.Program.Name)}'");
+
+ sb.AppendLine("New-NetFirewallRule @params");
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Escapes a string for embedding inside a PowerShell single-quoted string by
+ /// doubling any single-quote characters.
+ ///
+ /// The raw string value to escape.
+ private static string EscapePs(string value) => value.Replace("'", "''");
+
+ ///
+ /// Maps a value to the string accepted by
+ /// New-NetFirewallRule -Protocol.
+ ///
+ /// The protocol to convert.
+ private static string GetProtocolString(FirewallProtocol protocol) => protocol switch
+ {
+ FirewallProtocol.Any => "Any",
+ FirewallProtocol.TCP => "TCP",
+ FirewallProtocol.UDP => "UDP",
+ FirewallProtocol.ICMPv4 => "ICMPv4",
+ FirewallProtocol.ICMPv6 => "ICMPv6",
+ FirewallProtocol.GRE => "GRE",
+ FirewallProtocol.L2TP => "L2TP",
+ _ => ((int)protocol).ToString()
+ };
+
+ ///
+ /// Maps a value to the string accepted by
+ /// New-NetFirewallRule -InterfaceType.
+ ///
+ /// The interface type to convert.
+ private static string GetInterfaceTypeString(FirewallInterfaceType interfaceType) => interfaceType switch
+ {
+ FirewallInterfaceType.Wired => "Wired",
+ FirewallInterfaceType.Wireless => "Wireless",
+ FirewallInterfaceType.RemoteAccess => "RemoteAccess",
+ _ => "Any"
+ };
+
+ ///
+ /// Converts the three-element network-profile boolean array (Domain, Private, Public)
+ /// to the comma-separated profile string accepted by New-NetFirewallRule -Profile.
+ /// All-false or all-true both map to "Any".
+ ///
+ /// Three-element boolean array (Domain=0, Private=1, Public=2).
+ private static string GetProfileString(bool[] profiles)
+ {
+ if (profiles == null || profiles.Length < 3 || profiles.All(p => p) || profiles.All(p => !p))
+ return "Any";
+
+ var parts = new List(3);
+ if (profiles[0]) parts.Add("Domain");
+ if (profiles[1]) parts.Add("Private");
+ if (profiles[2]) parts.Add("Public");
+
+ return parts.Count == 0 ? "Any" : string.Join(",", parts);
+ }
+
+ ///
+ /// Parses a PowerShell direction string (e.g. "Outbound") to a
+ /// value. Defaults to .
+ ///
+ ///
+ /// The raw string value returned by PowerShell.
+ ///
+ private static FirewallRuleDirection ParseDirection(string value)
+ {
+ return value switch
+ {
+ "Outbound" => FirewallRuleDirection.Outbound,
+ _ => FirewallRuleDirection.Inbound,
+ };
+ }
+
+ ///
+ /// Parses a PowerShell action string (e.g. "Allow") to a
+ /// value. Defaults to .
+ ///
+ ///
+ /// The raw string value returned by PowerShell.
+ ///
+ private static FirewallRuleAction ParseAction(string value)
+ {
+ return value switch
+ {
+ "Allow" => FirewallRuleAction.Allow,
+ _ => FirewallRuleAction.Block,
+ };
+ }
+
+ ///
+ /// Parses a PowerShell protocol string (e.g. "TCP", "Any") to a
+ /// value. Numeric protocol numbers are also accepted.
+ /// Defaults to for unrecognized values.
+ ///
+ ///
+ /// The raw string value returned by PowerShell.
+ ///
+ private static FirewallProtocol ParseProtocol(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value) || value.Equals("Any", StringComparison.OrdinalIgnoreCase))
+ return FirewallProtocol.Any;
+
+ return value.ToUpperInvariant() switch
+ {
+ "TCP" => FirewallProtocol.TCP,
+ "UDP" => FirewallProtocol.UDP,
+ "ICMPV4" => FirewallProtocol.ICMPv4,
+ "ICMPV6" => FirewallProtocol.ICMPv6,
+ "GRE" => FirewallProtocol.GRE,
+ "L2TP" => FirewallProtocol.L2TP,
+ _ => int.TryParse(value, out var proto) ? (FirewallProtocol)proto : FirewallProtocol.Any,
+ };
+ }
+
+ ///
+ /// Parses a comma-separated port string (e.g. "80,443,8080-8090") to a list of
+ /// objects.
+ /// Returns an empty list when the value is blank or "Any".
+ ///
+ ///
+ /// The raw comma-separated port string returned by PowerShell.
+ ///
+ private static List ParsePorts(string value)
+ {
+ var list = new List();
+
+ if (string.IsNullOrWhiteSpace(value) || value.Equals("Any", StringComparison.OrdinalIgnoreCase))
+ return list;
+
+ foreach (var token in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ var dashIndex = token.IndexOf('-');
+
+ if (dashIndex > 0 &&
+ int.TryParse(token[..dashIndex], out var start) &&
+ int.TryParse(token[(dashIndex + 1)..], out var end))
+ {
+ list.Add(new FirewallPortSpecification(start, end));
+ }
+ else if (int.TryParse(token, out var port))
+ {
+ list.Add(new FirewallPortSpecification(port));
+ }
+ }
+
+ return list;
+ }
+
+ ///
+ /// Parses a PowerShell profile string (e.g. "Domain, Private") to a
+ /// three-element boolean array in the order Domain, Private, Public.
+ /// "Any" and "All" set all three entries to .
+ ///
+ ///
+ /// The raw profile string returned by PowerShell.
+ ///
+ private static bool[] ParseProfile(string value)
+ {
+ var profiles = new bool[3];
+
+ if (string.IsNullOrWhiteSpace(value))
+ return profiles;
+
+ if (value.Equals("Any", StringComparison.OrdinalIgnoreCase) ||
+ value.Equals("All", StringComparison.OrdinalIgnoreCase))
+ {
+ profiles[0] = profiles[1] = profiles[2] = true;
+ return profiles;
+ }
+
+ foreach (var token in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ switch (token)
+ {
+ case "Domain": profiles[0] = true; break;
+ case "Private": profiles[1] = true; break;
+ case "Public": profiles[2] = true; break;
+ }
+ }
+
+ return profiles;
+ }
+
+ ///
+ /// Parses a comma-separated address string (e.g. "192.168.1.0/24,LocalSubnet") to a
+ /// list of address strings.
+ /// Returns an empty list when the value is blank or "Any".
+ ///
+ ///
+ /// The raw comma-separated address string returned by PowerShell.
+ ///
+ private static List ParseAddresses(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value) || value.Equals("Any", StringComparison.OrdinalIgnoreCase))
+ return [];
+
+ return [.. value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
+ }
+
+ ///
+ /// Parses a PowerShell interface-type string (e.g. "Wired") to a
+ /// value. Defaults to .
+ ///
+ ///
+ /// The raw string value returned by PowerShell.
+ ///
+ private static FirewallInterfaceType ParseInterfaceType(string value)
+ {
+ return value switch
+ {
+ "Wired" => FirewallInterfaceType.Wired,
+ "Wireless" => FirewallInterfaceType.Wireless,
+ "RemoteAccess" => FirewallInterfaceType.RemoteAccess,
+ _ => FirewallInterfaceType.Any,
+ };
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Firewall/FirewallInterfaceType.cs b/Source/NETworkManager.Models/Firewall/FirewallInterfaceType.cs
new file mode 100644
index 0000000000..438291b6d9
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallInterfaceType.cs
@@ -0,0 +1,27 @@
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Defines the types of network interfaces that can be used in firewall rules.
+///
+public enum FirewallInterfaceType
+{
+ ///
+ /// Any interface type.
+ ///
+ Any = -1,
+
+ ///
+ /// Wired interface types, e.g. Ethernet.
+ ///
+ Wired,
+
+ ///
+ /// Wireless interface types, e.g. Wi-Fi.
+ ///
+ Wireless,
+
+ ///
+ /// Remote interface types, e.g. VPN, L2TP, OpenVPN, etc.
+ ///
+ RemoteAccess
+}
diff --git a/Source/NETworkManager.Models/Firewall/FirewallPortLocation.cs b/Source/NETworkManager.Models/Firewall/FirewallPortLocation.cs
new file mode 100644
index 0000000000..549cb35744
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallPortLocation.cs
@@ -0,0 +1,17 @@
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Ports of local host or remote host.
+///
+public enum FirewallPortLocation
+{
+ ///
+ /// Ports of local host.
+ ///
+ LocalPorts,
+
+ ///
+ /// Ports of remote host.
+ ///
+ RemotePorts
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Firewall/FirewallPortSpecification.cs b/Source/NETworkManager.Models/Firewall/FirewallPortSpecification.cs
new file mode 100644
index 0000000000..ae26b65d61
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallPortSpecification.cs
@@ -0,0 +1,66 @@
+// ReSharper disable MemberCanBePrivate.Global
+// Needed for serialization.
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Represents a specification for defining and validating firewall ports.
+///
+///
+/// This class is used to encapsulate rules and configurations for
+/// managing firewall port restrictions or allowances. It provides
+/// properties and methods to define a range of acceptable ports or
+/// individual port specifications.
+///
+public class FirewallPortSpecification
+{
+ ///
+ /// Gets or sets the start point or initial value of a process, range, or operation.
+ ///
+ ///
+ /// The Start property typically represents the beginning state or position for sequential
+ /// processing or iteration. The exact usage of this property may vary depending on the context of
+ /// the class or object it belongs to.
+ ///
+ public int Start { get; set; }
+
+ ///
+ /// Gets or sets the endpoint or final state of a process, range, or operation.
+ ///
+ ///
+ /// This property typically represents the termination position, time, or value
+ /// in a sequence, operation, or any bounded context. Its specific meaning may vary
+ /// depending on the context in which it is used.
+ ///
+ public int End { get; set; }
+
+ ///
+ /// For serializing.
+ ///
+ public FirewallPortSpecification()
+ {
+ Start = -1;
+ End = -1;
+ }
+
+ ///
+ /// Represents the specification for a firewall port, detailing its configuration
+ /// and rules for inbound or outbound network traffic.
+ ///
+ public FirewallPortSpecification(int start, int end = -1)
+ {
+ Start = start;
+ End = end;
+ }
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current instance of the object.
+ public override string ToString()
+ {
+ if (Start is 0)
+ return string.Empty;
+
+ return End is -1 or 0 ? $"{Start}" : $"{Start}-{End}";
+ }
+}
diff --git a/Source/NETworkManager.Models/Firewall/FirewallProtocol.cs b/Source/NETworkManager.Models/Firewall/FirewallProtocol.cs
new file mode 100644
index 0000000000..da54f95e6e
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallProtocol.cs
@@ -0,0 +1,122 @@
+// ReSharper disable InconsistentNaming
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Specifies the network protocols supported by the firewall configuration.
+/// Each protocol is represented by its respective protocol number as defined in
+/// the Internet Assigned Numbers Authority (IANA) protocol registry.
+/// This enumeration is used to identify traffic based on its protocol type
+/// for filtering or access control purposes in the firewall.
+///
+public enum FirewallProtocol
+{
+ ///
+ /// Denotes the Transmission Control Protocol (TCP) used in firewall configurations.
+ /// TCP is a fundamental protocol within the Internet Protocol Suite, ensuring reliable
+ /// communication by delivering a stream of data packets in sequence with error checking
+ /// between networked devices.
+ ///
+ TCP = 6,
+
+ ///
+ /// Represents the User Datagram Protocol (UDP) in the context of firewall rules.
+ /// UDP is a connectionless protocol within the Internet Protocol (IP) suite that
+ /// allows for minimal latency by transmitting datagrams without guaranteeing delivery,
+ /// order, or error recovery.
+ ///
+ UDP = 17,
+
+ ///
+ /// Represents the Internet Control Message Protocol (ICMPv4) in the context of firewall rules.
+ /// ICMP is used by network devices, such as routers, to send error messages and operational
+ /// information, indicating issues like unreachable network destinations.
+ ///
+ ICMPv4 = 1,
+
+ ///
+ /// Represents the Internet Control Message Protocol for IPv6 (ICMPv6) in the context of firewall rules.
+ /// ICMPv6 is a supporting protocol in the Internet Protocol version 6 (IPv6) suite and is used for
+ /// diagnostic and error-reporting purposes, as well as for functions such as Neighbor Discovery Protocol (NDP).
+ ///
+ ICMPv6 = 58,
+
+ ///
+ /// Represents the IPv6 Hop-by-Hop Option (HOPOPT) protocol in the context of firewall rules.
+ /// HOPOPT is a special protocol used in IPv6 for carrying optional information that must be examined
+ /// by each node along the packet's delivery path.
+ ///
+ HOPOPT = 0,
+
+ ///
+ /// Represents the Generic Routing Encapsulation (GRE) protocol in the context of firewall rules.
+ /// GRE is a tunneling protocol developed to encapsulate a wide variety of network layer protocols
+ /// inside virtual point-to-point links. It is commonly used in creating VPNs and enabling the
+ /// transport of multicast traffic and non-IP protocols across IP networks.
+ ///
+ GRE = 47,
+
+ ///
+ /// Represents the Internet Protocol Version 6 (IPv6) in the context of firewall rules.
+ /// IPv6 is the most recent version of the Internet Protocol (IP) and provides identification
+ /// and location addressing for devices across networks, enabling communication over the internet.
+ ///
+ IPv6 = 41,
+
+ ///
+ /// Represents the IPv6-Route protocol in the context of firewall rules.
+ /// IPv6-Route is used for routing header information in IPv6 packets, which
+ /// specifies the list of one or more intermediate nodes a packet should pass
+ /// through before reaching its destination.
+ ///
+ IPv6_Route = 43,
+
+ ///
+ /// Represents the IPv6 Fragmentation Header (IPv6_Frag) in the context of firewall rules.
+ /// The IPv6 Fragmentation Header is used to support fragmentation and reassembly of
+ /// packets in IPv6 networks. It facilitates handling packets that are too large to
+ /// fit in the path MTU (Maximum Transmission Unit) of the network segment.
+ ///
+ IPv6_Frag = 44,
+
+ ///
+ /// Represents the IPv6 No Next Header protocol in the context of firewall rules.
+ /// This protocol indicates that there is no next header following the current header in the IPv6 packet.
+ /// It is primarily used in cases where the payload does not require a specific transport protocol header.
+ ///
+ IPv6_NoNxt = 59,
+
+ ///
+ /// Represents the IPv6 Options (IPv6_Opts) protocol in the context of firewall rules.
+ /// IPv6 Options is a part of the IPv6 suite used for carrying optional internet-layer information
+ /// and additional headers for specific purposes, providing extensibility in IPv6 communication.
+ ///
+ IPv6_Opts = 60,
+
+ ///
+ /// Represents the Virtual Router Redundancy Protocol (VRRP) in the context of firewall rules.
+ /// VRRP is a network protocol that provides automatic assignment of available routers to
+ /// participating hosts, ensuring redundancy and high availability of router services.
+ ///
+ VRRP = 112,
+
+ ///
+ /// Represents the Pragmatic General Multicast (PGM) protocol in the context of firewall rules.
+ /// PGM is a reliable multicast transport protocol that ensures ordered, duplicate-free,
+ /// and scalable delivery of data in multicast-enabled networks.
+ ///
+ PGM = 113,
+
+ ///
+ /// Represents the Layer 2 Tunneling Protocol (L2TP) in the context of firewall rules.
+ /// L2TP is a tunneling protocol used to support virtual private networks (VPNs) or
+ /// as part of the delivery of services by Internet Service Providers (ISPs).
+ ///
+ L2TP = 115,
+
+ ///
+ /// Represents a wildcard protocol option to match any protocol in the context of firewall rules.
+ /// The "Any" value can be used to specify that the rule applies to all network protocols
+ /// without restriction or specificity.
+ ///
+ Any = 255
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Firewall/FirewallRule.cs b/Source/NETworkManager.Models/Firewall/FirewallRule.cs
new file mode 100644
index 0000000000..91c373b3e3
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallRule.cs
@@ -0,0 +1,216 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Represents a security rule used within a firewall to control network traffic based on
+/// specific conditions such as IP addresses, ports, and protocols.
+///
+public class FirewallRule
+{
+ #region Variables
+
+ ///
+ /// Internal unique identifier of the rule (i.e. the value of $rule.Id in PowerShell).
+ /// Used to target the rule in Set-NetFirewallRule / Remove-NetFirewallRule calls.
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// Human-readable display name of the firewall rule. Used for display purposes
+ /// or as an identifier in various contexts.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Indicates whether the firewall rule is enabled.
+ ///
+ public bool IsEnabled { get; set; } = true;
+
+ ///
+ /// Represents a text-based explanation or information associated with an object.
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Represents the communication protocol to be used in the network configuration.
+ ///
+ public FirewallProtocol Protocol { get; set; } = FirewallProtocol.TCP;
+
+ ///
+ /// Defines the direction of traffic impacted by the rule or configuration.
+ ///
+ public FirewallRuleDirection Direction { get; set; } = FirewallRuleDirection.Inbound;
+
+ ///
+ /// Represents the entry point and core execution logic for an application.
+ ///
+ public FirewallRuleProgram Program { get; set; }
+
+ ///
+ /// Local IP addresses or address specifiers (e.g. "192.168.1.0/24", "LocalSubnet").
+ /// An empty list means "Any".
+ ///
+ public List LocalAddresses
+ {
+ get;
+ set
+ {
+ if (value is null)
+ {
+ field = [];
+ return;
+ }
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// Remote IP addresses or address specifiers (e.g. "10.0.0.0/8", "Internet").
+ /// An empty list means "Any".
+ ///
+ public List RemoteAddresses
+ {
+ get;
+ set
+ {
+ if (value is null)
+ {
+ field = [];
+ return;
+ }
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// Defines the local ports associated with the firewall rule.
+ ///
+ public List LocalPorts
+ {
+ get;
+ set
+ {
+ if (value is null)
+ {
+ field = [];
+ return;
+ }
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// Defines the range of remote ports associated with the firewall rule.
+ ///
+ public List RemotePorts
+ {
+ get;
+ set
+ {
+ if (value is null)
+ {
+ field = [];
+ return;
+ }
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// Network profiles in order Domain, Private, Public.
+ ///
+ public bool[] NetworkProfiles
+ {
+ get;
+ set
+ {
+ if (value?.Length is not 3)
+ return;
+ field = value;
+ }
+ } = new bool[3];
+
+ public FirewallInterfaceType InterfaceType { get; set; } = FirewallInterfaceType.Any;
+
+ ///
+ /// Represents the operation to be performed or executed.
+ ///
+ public FirewallRuleAction Action { get; set; } = FirewallRuleAction.Block;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Represents a rule within the firewall configuration.
+ /// Used to control network traffic based on specified criteria, such as
+ /// ports, protocols, the interface type, network profiles, and the used programs.
+ ///
+ public FirewallRule()
+ {
+
+ }
+ #endregion
+
+ #region Display properties
+
+ /// Program path, or null when no program restriction is set.
+ public string ProgramDisplay => Program?.ToString();
+
+ /// Local addresses as a human-readable string (e.g. "192.168.1.0/24; LocalSubnet"). Returns null when unrestricted.
+ public string LocalAddressesDisplay => LocalAddresses.Count == 0 ? null : string.Join("; ", LocalAddresses);
+
+ /// Local ports as a human-readable string (e.g. "80; 443; 8080-8090"). Returns null when unrestricted.
+ public string LocalPortsDisplay => LocalPorts.Count == 0 ? null : PortsToString(LocalPorts);
+
+ /// Remote addresses as a human-readable string (e.g. "10.0.0.0/8"). Returns null when unrestricted.
+ public string RemoteAddressesDisplay => RemoteAddresses.Count == 0 ? null : string.Join("; ", RemoteAddresses);
+
+ /// Remote ports as a human-readable string (e.g. "80; 443"). Returns null when unrestricted.
+ public string RemotePortsDisplay => RemotePorts.Count == 0 ? null : PortsToString(RemotePorts);
+
+ ///
+ /// Network profiles (Domain / Private / Public) as a comma-separated string.
+ /// Returns "Any" when all three are set.
+ ///
+ public string NetworkProfilesDisplay
+ {
+ get
+ {
+ if (NetworkProfiles.Length == 3 && NetworkProfiles.All(x => x))
+ return "Any";
+
+ var names = new List(3);
+ if (NetworkProfiles.Length > 0 && NetworkProfiles[0]) names.Add("Domain");
+ if (NetworkProfiles.Length > 1 && NetworkProfiles[1]) names.Add("Private");
+ if (NetworkProfiles.Length > 2 && NetworkProfiles[2]) names.Add("Public");
+
+ return names.Count == 0 ? "-" : string.Join(", ", names);
+ }
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Converts a collection of port numbers to a single, comma-separated string representation.
+ ///
+ /// A collection of integers representing port numbers.
+ /// Separator character to use
+ /// Separate entries with a space.
+ /// A separated string containing all the port numbers from the input collection.
+ public static string PortsToString(List ports, char separator = ';', bool spacing = true)
+ {
+ if (ports.Count is 0)
+ return string.Empty;
+
+ var delimiter = spacing ? $"{separator} " : separator.ToString();
+
+ return string.Join(delimiter, ports.Select(port => port.ToString()));
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Firewall/FirewallRuleAction.cs b/Source/NETworkManager.Models/Firewall/FirewallRuleAction.cs
new file mode 100644
index 0000000000..eaaa63cfc4
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallRuleAction.cs
@@ -0,0 +1,20 @@
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Represents the action, if the rule filter applies.
+///
+public enum FirewallRuleAction
+{
+ ///
+ /// Represents the action to block network traffic in a firewall rule.
+ ///
+ Block,
+
+ ///
+ /// Represents the action to allow network traffic.
+ ///
+ Allow,
+
+ // Unsupported for now
+ //AllowIPsec
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Firewall/FirewallRuleDirection.cs b/Source/NETworkManager.Models/Firewall/FirewallRuleDirection.cs
new file mode 100644
index 0000000000..ddd72c116f
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallRuleDirection.cs
@@ -0,0 +1,18 @@
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Represents a firewall rule direction that allows or processes network traffic
+/// incoming to the system or network from external sources.
+///
+public enum FirewallRuleDirection
+{
+ ///
+ /// Inbound packets.
+ ///
+ Inbound,
+
+ ///
+ /// Outbound packets.
+ ///
+ Outbound
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Firewall/FirewallRuleProgram.cs b/Source/NETworkManager.Models/Firewall/FirewallRuleProgram.cs
new file mode 100644
index 0000000000..12133b1344
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallRuleProgram.cs
@@ -0,0 +1,100 @@
+using System;
+using System.IO;
+using System.Text.Json.Serialization;
+using System.Xml.Serialization;
+
+namespace NETworkManager.Models.Firewall;
+
+///
+/// Represents a program associated with a firewall rule.
+///
+public class FirewallRuleProgram : ICloneable
+{
+ #region Variables
+ ///
+ /// Program to apply rule to.
+ ///
+ [JsonIgnore]
+ [XmlIgnore]
+ public FileInfo Executable {
+ private set;
+ get
+ {
+ if (field is null && Name is not null)
+ field = new FileInfo(Name);
+
+ return field;
+ }
+ }
+
+ ///
+ /// Represents the name associated with the object.
+ ///
+ public string Name
+ {
+ get;
+ // Public modifier required for deserialization
+ // ReSharper disable once MemberCanBePrivate.Global
+ // ReSharper disable once PropertyCanBeMadeInitOnly.Global
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return;
+
+ Executable = new FileInfo(value);
+ field = value;
+ }
+ }
+ #endregion
+
+ #region Constructor
+ ///
+ /// Public empty constructor is required for de-/serialization.
+ ///
+ // ReSharper disable once MemberCanBePrivate.Global
+ public FirewallRuleProgram()
+ {
+ }
+
+ ///
+ /// Construct program reference for firewall rule.
+ ///
+ ///
+ public FirewallRuleProgram(string pathToExe)
+ {
+ ArgumentNullException.ThrowIfNull(pathToExe);
+ var exe = new FileInfo(pathToExe);
+ Executable = exe;
+ Name = exe.FullName;
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// Convert the full file path to string.
+ ///
+ ///
+ public override string ToString()
+ {
+ return Executable?.FullName;
+ }
+
+ ///
+ /// Clone instance.
+ ///
+ /// An instance clone.
+ public object Clone()
+ {
+ try
+ {
+ return new FirewallRuleProgram(Executable?.FullName);
+ }
+ catch (ArgumentNullException)
+ {
+ return new FirewallRuleProgram();
+ }
+
+ }
+
+ #endregion
+}
diff --git a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs
index 82e1817247..2a211c638c 100644
--- a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs
+++ b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs
@@ -37,7 +37,7 @@ private static void OnHostsFileChanged()
///
/// Path to the hosts file.
///
- private static string HostsFilePath => Path.Combine(HostsFolderPath, "hosts");
+ public static string HostsFilePath => Path.Combine(HostsFolderPath, "hosts");
///
/// Identifier for the hosts file backup.
diff --git a/Source/NETworkManager.Models/NETworkManager.Models.csproj b/Source/NETworkManager.Models/NETworkManager.Models.csproj
index 6ba6c74b39..ae85cf3cb6 100644
--- a/Source/NETworkManager.Models/NETworkManager.Models.csproj
+++ b/Source/NETworkManager.Models/NETworkManager.Models.csproj
@@ -61,4 +61,8 @@
PreserveNewest
+
+
+
+
diff --git a/Source/NETworkManager.Models/Network/NetworkInterface.cs b/Source/NETworkManager.Models/Network/NetworkInterface.cs
index 6fbc000e2b..ae641531d2 100644
--- a/Source/NETworkManager.Models/Network/NetworkInterface.cs
+++ b/Source/NETworkManager.Models/Network/NetworkInterface.cs
@@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Microsoft.Win32;
using NETworkManager.Utilities;
+using SMA = System.Management.Automation;
namespace NETworkManager.Models.Network;
@@ -72,6 +73,37 @@ public static List GetNetworkInterfaces()
{
List listNetworkInterfaceInfo = [];
+ // Query network profiles (Domain/Private/Public) for all connected interfaces via PowerShell.
+ // Keyed by InterfaceAlias which matches networkInterface.Name in the .NET API.
+ var profileByAlias = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ try
+ {
+ using var ps = SMA.PowerShell.Create();
+ ps.AddScript("Get-NetConnectionProfile | Select-Object InterfaceAlias, NetworkCategory");
+
+ foreach (var result in ps.Invoke())
+ {
+ var alias = result.Properties["InterfaceAlias"]?.Value?.ToString();
+ var category = result.Properties["NetworkCategory"]?.Value?.ToString();
+
+ if (string.IsNullOrEmpty(alias))
+ continue;
+
+ profileByAlias[alias] = category switch
+ {
+ "DomainAuthenticated" => NetworkProfile.Domain,
+ "Private" => NetworkProfile.Private,
+ "Public" => NetworkProfile.Public,
+ _ => NetworkProfile.NotConfigured
+ };
+ }
+ }
+ catch
+ {
+ // Profile lookup is best-effort; proceed without profile information on error.
+ }
+
foreach (var networkInterface in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
{
// NetworkInterfaceType 53 is proprietary virtual/internal interface
@@ -194,7 +226,10 @@ public static List GetNetworkInterfaces()
IPv6Gateway = [.. listIPv6Gateway],
DNSAutoconfigurationEnabled = dnsAutoconfigurationEnabled,
DNSSuffix = ipProperties.DnsSuffix,
- DNSServer = [.. ipProperties.DnsAddresses]
+ DNSServer = [.. ipProperties.DnsAddresses],
+ Profile = profileByAlias.TryGetValue(networkInterface.Name, out var profile)
+ ? profile
+ : NetworkProfile.NotConfigured
});
}
diff --git a/Source/NETworkManager.Models/Network/NetworkInterfaceInfo.cs b/Source/NETworkManager.Models/Network/NetworkInterfaceInfo.cs
index ced21944f1..f5130f49a5 100644
--- a/Source/NETworkManager.Models/Network/NetworkInterfaceInfo.cs
+++ b/Source/NETworkManager.Models/Network/NetworkInterfaceInfo.cs
@@ -120,8 +120,8 @@ public class NetworkInterfaceInfo
public IPAddress[] DNSServer { get; set; }
///
- /// Firewall network category (Private, Public, Domain)
+ /// Network category assigned by Windows (Domain, Private, Public).
+ /// when the interface has no active connection profile.
///
- // NOT IMPLEMENTED YET
- //public NetworkProfiles Profiles { get; set; }
+ public NetworkProfile Profile { get; set; } = NetworkProfile.NotConfigured;
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Network/NetworkProfiles.cs b/Source/NETworkManager.Models/Network/NetworkProfiles.cs
new file mode 100644
index 0000000000..00dd4d9124
--- /dev/null
+++ b/Source/NETworkManager.Models/Network/NetworkProfiles.cs
@@ -0,0 +1,27 @@
+namespace NETworkManager.Models.Network;
+
+///
+/// Defines the network profile detected by Windows.
+///
+public enum NetworkProfile
+{
+ ///
+ /// Network profile is not configured.
+ ///
+ NotConfigured = -1,
+
+ ///
+ /// Network has an Active Directory (AD) controller and you are authenticated.
+ ///
+ Domain,
+
+ ///
+ /// Network is private. Firewall will allow most connections.
+ ///
+ Private,
+
+ ///
+ /// Network is public. Firewall will block most connections.
+ ///
+ Public
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Profiles/ProfileViewManager.cs b/Source/NETworkManager.Profiles/ProfileViewManager.cs
index 0435fec045..5c58170cba 100644
--- a/Source/NETworkManager.Profiles/ProfileViewManager.cs
+++ b/Source/NETworkManager.Profiles/ProfileViewManager.cs
@@ -22,41 +22,43 @@ public static class ProfileViewManager
public static List List =>
[
// General
- new ProfileViewInfo(ProfileName.General, new PackIconModern { Kind = PackIconModernKind.Box },
+ new(ProfileName.General, new PackIconModern { Kind = PackIconModernKind.Box },
ProfileGroup.General),
// Applications
- new ProfileViewInfo(ProfileName.NetworkInterface, ApplicationManager.GetIcon(ApplicationName.NetworkInterface),
+ new(ProfileName.NetworkInterface, ApplicationManager.GetIcon(ApplicationName.NetworkInterface),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.IPScanner, ApplicationManager.GetIcon(ApplicationName.IPScanner),
+ new(ProfileName.IPScanner, ApplicationManager.GetIcon(ApplicationName.IPScanner),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner),
+ new(ProfileName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.PingMonitor, ApplicationManager.GetIcon(ApplicationName.PingMonitor),
+ new(ProfileName.PingMonitor, ApplicationManager.GetIcon(ApplicationName.PingMonitor),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.Traceroute, ApplicationManager.GetIcon(ApplicationName.Traceroute),
+ new(ProfileName.Traceroute, ApplicationManager.GetIcon(ApplicationName.Traceroute),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.DNSLookup, ApplicationManager.GetIcon(ApplicationName.DNSLookup),
+ new(ProfileName.DNSLookup, ApplicationManager.GetIcon(ApplicationName.DNSLookup),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.RemoteDesktop, ApplicationManager.GetIcon(ApplicationName.RemoteDesktop),
+ new(ProfileName.RemoteDesktop, ApplicationManager.GetIcon(ApplicationName.RemoteDesktop),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.PowerShell, ApplicationManager.GetIcon(ApplicationName.PowerShell),
+ new(ProfileName.PowerShell, ApplicationManager.GetIcon(ApplicationName.PowerShell),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.PuTTY, ApplicationManager.GetIcon(ApplicationName.PuTTY),
+ new(ProfileName.PuTTY, ApplicationManager.GetIcon(ApplicationName.PuTTY),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.TigerVNC, ApplicationManager.GetIcon(ApplicationName.TigerVNC),
+ new(ProfileName.TigerVNC, ApplicationManager.GetIcon(ApplicationName.TigerVNC),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.WebConsole, ApplicationManager.GetIcon(ApplicationName.WebConsole),
+ new(ProfileName.WebConsole, ApplicationManager.GetIcon(ApplicationName.WebConsole),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.SNMP, ApplicationManager.GetIcon(ApplicationName.SNMP),
+ new(ProfileName.SNMP, ApplicationManager.GetIcon(ApplicationName.SNMP),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.Firewall, ApplicationManager.GetIcon(ApplicationName.Firewall),
+ /*
+ new(ProfileName.Firewall, ApplicationManager.GetIcon(ApplicationName.Firewall),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.WakeOnLAN, ApplicationManager.GetIcon(ApplicationName.WakeOnLAN),
+ */
+ new(ProfileName.WakeOnLAN, ApplicationManager.GetIcon(ApplicationName.WakeOnLAN),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.Whois, ApplicationManager.GetIcon(ApplicationName.Whois),
+ new(ProfileName.Whois, ApplicationManager.GetIcon(ApplicationName.Whois),
ProfileGroup.Application),
- new ProfileViewInfo(ProfileName.IPGeolocation, ApplicationManager.GetIcon(ApplicationName.IPGeolocation),
+ new(ProfileName.IPGeolocation, ApplicationManager.GetIcon(ApplicationName.IPGeolocation),
ProfileGroup.Application),
];
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
index 61260da4e5..ac6442df1e 100644
--- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
+++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
@@ -234,6 +234,9 @@ public static class GlobalStaticConfiguration
// Application: Hosts File Editor
public static ExportFileType HostsFileEditor_ExportFileType => ExportFileType.Csv;
+ // Application: Firewall
+ public static ExportFileType Firewall_ExportFileType => ExportFileType.Csv;
+
// Application: Discovery Protocol
public static DiscoveryProtocol DiscoveryProtocol_Protocol => DiscoveryProtocol.LldpCdp;
public static int DiscoveryProtocol_Duration => 60;
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index bbe4f5a968..930fd23061 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -3255,6 +3255,32 @@ public double Firewall_ProfileWidth
}
} = GlobalStaticConfiguration.Profile_DefaultWidthExpanded;
+ public string Firewall_ExportFilePath
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public ExportFileType Firewall_ExportFileType
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = GlobalStaticConfiguration.Firewall_ExportFileType;
+
#endregion
#region Discovery Protocol
diff --git a/Source/NETworkManager.Settings/SettingsViewManager.cs b/Source/NETworkManager.Settings/SettingsViewManager.cs
index 0e050bf4f4..480ccc89b6 100644
--- a/Source/NETworkManager.Settings/SettingsViewManager.cs
+++ b/Source/NETworkManager.Settings/SettingsViewManager.cs
@@ -21,62 +21,64 @@ public static class SettingsViewManager
public static List List =>
[
// General
- new SettingsViewInfo(SettingsName.General, new PackIconModern { Kind = PackIconModernKind.Layer },
+ new(SettingsName.General, new PackIconModern { Kind = PackIconModernKind.Layer },
SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Window, new PackIconOcticons { Kind = PackIconOcticonsKind.Browser },
+ new(SettingsName.Window, new PackIconOcticons { Kind = PackIconOcticonsKind.Browser },
SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Appearance, new PackIconMaterial { Kind = PackIconMaterialKind.Palette },
+ new(SettingsName.Appearance, new PackIconMaterial { Kind = PackIconMaterialKind.Palette },
SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Language, new PackIconMaterial { Kind = PackIconMaterialKind.Translate },
+ new(SettingsName.Language, new PackIconMaterial { Kind = PackIconMaterialKind.Translate },
SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Network, new PackIconModern { Kind = PackIconModernKind.Network },
+ new(SettingsName.Network, new PackIconModern { Kind = PackIconModernKind.Network },
SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Status, new PackIconMaterial { Kind = PackIconMaterialKind.Pulse },
+ new(SettingsName.Status, new PackIconMaterial { Kind = PackIconMaterialKind.Pulse },
SettingsGroup.General),
- new SettingsViewInfo(SettingsName.HotKeys,
+ new(SettingsName.HotKeys,
new PackIconFontAwesome { Kind = PackIconFontAwesomeKind.KeyboardRegular },
SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Autostart, new PackIconMaterial { Kind = PackIconMaterialKind.Power },
+ new(SettingsName.Autostart, new PackIconMaterial { Kind = PackIconMaterialKind.Power },
SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Update,
+ new(SettingsName.Update,
new PackIconMaterial { Kind = PackIconMaterialKind.RocketLaunchOutline }, SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Profiles,
+ new(SettingsName.Profiles,
new PackIconFontAwesome { Kind = PackIconFontAwesomeKind.ServerSolid }, SettingsGroup.General),
- new SettingsViewInfo(SettingsName.Settings, new PackIconMaterialLight { Kind = PackIconMaterialLightKind.Cog },
+ new(SettingsName.Settings, new PackIconMaterialLight { Kind = PackIconMaterialLightKind.Cog },
SettingsGroup.General),
// Applications
- new SettingsViewInfo(SettingsName.Dashboard, ApplicationManager.GetIcon(ApplicationName.Dashboard),
+ new(SettingsName.Dashboard, ApplicationManager.GetIcon(ApplicationName.Dashboard),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.IPScanner, ApplicationManager.GetIcon(ApplicationName.IPScanner),
+ new(SettingsName.IPScanner, ApplicationManager.GetIcon(ApplicationName.IPScanner),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner),
+ new(SettingsName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.PingMonitor, ApplicationManager.GetIcon(ApplicationName.PingMonitor),
+ new(SettingsName.PingMonitor, ApplicationManager.GetIcon(ApplicationName.PingMonitor),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.Traceroute, ApplicationManager.GetIcon(ApplicationName.Traceroute),
+ new(SettingsName.Traceroute, ApplicationManager.GetIcon(ApplicationName.Traceroute),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.DNSLookup, ApplicationManager.GetIcon(ApplicationName.DNSLookup),
+ new(SettingsName.DNSLookup, ApplicationManager.GetIcon(ApplicationName.DNSLookup),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.RemoteDesktop, ApplicationManager.GetIcon(ApplicationName.RemoteDesktop),
+ new(SettingsName.RemoteDesktop, ApplicationManager.GetIcon(ApplicationName.RemoteDesktop),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.PowerShell, ApplicationManager.GetIcon(ApplicationName.PowerShell),
+ new(SettingsName.PowerShell, ApplicationManager.GetIcon(ApplicationName.PowerShell),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.PuTTY, ApplicationManager.GetIcon(ApplicationName.PuTTY),
+ new(SettingsName.PuTTY, ApplicationManager.GetIcon(ApplicationName.PuTTY),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.TigerVNC, ApplicationManager.GetIcon(ApplicationName.TigerVNC),
+ new(SettingsName.TigerVNC, ApplicationManager.GetIcon(ApplicationName.TigerVNC),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.WebConsole, ApplicationManager.GetIcon(ApplicationName.WebConsole),
+ new(SettingsName.WebConsole, ApplicationManager.GetIcon(ApplicationName.WebConsole),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.SNMP, ApplicationManager.GetIcon(ApplicationName.SNMP),
+ new(SettingsName.SNMP, ApplicationManager.GetIcon(ApplicationName.SNMP),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.SNTPLookup, ApplicationManager.GetIcon(ApplicationName.SNTPLookup),
+ new(SettingsName.SNTPLookup, ApplicationManager.GetIcon(ApplicationName.SNTPLookup),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.Firewall, ApplicationManager.GetIcon(ApplicationName.Firewall),
+ /*
+ new(SettingsName.Firewall, ApplicationManager.GetIcon(ApplicationName.Firewall),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.WakeOnLAN, ApplicationManager.GetIcon(ApplicationName.WakeOnLAN),
+ */
+ new(SettingsName.WakeOnLAN, ApplicationManager.GetIcon(ApplicationName.WakeOnLAN),
SettingsGroup.Application),
- new SettingsViewInfo(SettingsName.BitCalculator, ApplicationManager.GetIcon(ApplicationName.BitCalculator),
+ new(SettingsName.BitCalculator, ApplicationManager.GetIcon(ApplicationName.BitCalculator),
SettingsGroup.Application),
];
}
diff --git a/Source/NETworkManager.Validators/EmptyOrFirewallAddressValidator.cs b/Source/NETworkManager.Validators/EmptyOrFirewallAddressValidator.cs
new file mode 100644
index 0000000000..6e528d5c51
--- /dev/null
+++ b/Source/NETworkManager.Validators/EmptyOrFirewallAddressValidator.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Windows.Controls;
+using NETworkManager.Localization.Resources;
+
+namespace NETworkManager.Validators;
+
+///
+/// Validates that the input is empty (meaning "Any") or contains semicolon-separated
+/// valid IPv4/IPv6 addresses, CIDR subnets, or recognized Windows Firewall keywords
+/// (e.g. LocalSubnet, Internet, Intranet, DNS, DHCP, WINS, DefaultGateway).
+///
+public class EmptyOrFirewallAddressValidator : ValidationRule
+{
+ private static readonly string[] Keywords =
+ [
+ "Any", "LocalSubnet", "Internet", "Intranet", "DNS", "DHCP", "WINS", "DefaultGateway"
+ ];
+
+ ///
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ if (string.IsNullOrEmpty(value as string))
+ return ValidationResult.ValidResult;
+
+ foreach (var entry in ((string)value).Split(';'))
+ {
+ var token = entry.Trim();
+
+ if (string.IsNullOrEmpty(token))
+ continue;
+
+ if (Array.Exists(Keywords, k => k.Equals(token, StringComparison.OrdinalIgnoreCase)))
+ continue;
+
+ var slashIndex = token.IndexOf('/');
+ var addressPart = slashIndex > 0 ? token[..slashIndex] : token;
+
+ if (!IPAddress.TryParse(addressPart, out _))
+ return new ValidationResult(false, Strings.EnterValidFirewallAddress);
+ }
+
+ return ValidationResult.ValidResult;
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager/ViewModels/DiscoveryProtocolViewModel.cs b/Source/NETworkManager/ViewModels/DiscoveryProtocolViewModel.cs
index 6b57463e7d..7b917a8e3d 100644
--- a/Source/NETworkManager/ViewModels/DiscoveryProtocolViewModel.cs
+++ b/Source/NETworkManager/ViewModels/DiscoveryProtocolViewModel.cs
@@ -295,7 +295,9 @@ private async Task RestartAsAdminAction()
///
/// Gets the command to start the capture.
///
- public ICommand CaptureCommand => new RelayCommand(_ => CaptureAction().ConfigureAwait(false));
+ public ICommand CaptureCommand => new RelayCommand(_ => CaptureAction().ConfigureAwait(false), Capture_CanExecute);
+
+ private bool Capture_CanExecute(object _) => ConfigurationManager.Current.IsAdmin && !IsCapturing;
///
/// Action to start the capture.
diff --git a/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs b/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs
new file mode 100644
index 0000000000..de562b5984
--- /dev/null
+++ b/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs
@@ -0,0 +1,360 @@
+using NETworkManager.Models.Firewall;
+using NETworkManager.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Windows.Input;
+
+namespace NETworkManager.ViewModels;
+
+///
+/// ViewModel for adding or editing a firewall rule in the FirewallRule dialog.
+///
+public class FirewallRuleViewModel : ViewModelBase
+{
+ ///
+ /// Creates a new instance of for adding or
+ /// editing a firewall rule.
+ ///
+ /// OK command to save the rule.
+ /// Cancel command to discard changes.
+ /// Existing rule to edit; to add a new rule.
+ public FirewallRuleViewModel(Action okCommand,
+ Action cancelHandler, FirewallRule entry = null)
+ {
+ OKCommand = new RelayCommand(_ => okCommand(this));
+ CancelCommand = new RelayCommand(_ => cancelHandler(this));
+
+ Entry = entry;
+
+ if (entry == null)
+ {
+ IsEnabled = true;
+ Direction = FirewallRuleDirection.Inbound;
+ Action = FirewallRuleAction.Allow;
+ Protocol = FirewallProtocol.Any;
+ InterfaceType = FirewallInterfaceType.Any;
+ NetworkProfileDomain = true;
+ NetworkProfilePrivate = true;
+ NetworkProfilePublic = true;
+ }
+ else
+ {
+ Name = entry.Name;
+ IsEnabled = entry.IsEnabled;
+ Description = entry.Description ?? string.Empty;
+ Direction = entry.Direction;
+ Action = entry.Action;
+ Protocol = entry.Protocol;
+ LocalPorts = FirewallRule.PortsToString(entry.LocalPorts);
+ RemotePorts = FirewallRule.PortsToString(entry.RemotePorts);
+ LocalAddresses = entry.LocalAddresses.Count > 0 ? string.Join("; ", entry.LocalAddresses) : string.Empty;
+ RemoteAddresses = entry.RemoteAddresses.Count > 0 ? string.Join("; ", entry.RemoteAddresses) : string.Empty;
+ Program = entry.Program?.Name ?? string.Empty;
+ InterfaceType = entry.InterfaceType;
+ NetworkProfileDomain = entry.NetworkProfiles.Length > 0 && entry.NetworkProfiles[0];
+ NetworkProfilePrivate = entry.NetworkProfiles.Length > 1 && entry.NetworkProfiles[1];
+ NetworkProfilePublic = entry.NetworkProfiles.Length > 2 && entry.NetworkProfiles[2];
+ }
+ }
+
+ ///
+ /// OK command to save the rule.
+ ///
+ public ICommand OKCommand { get; }
+
+ ///
+ /// Cancel command to discard changes.
+ ///
+ public ICommand CancelCommand { get; }
+
+ ///
+ /// The original firewall rule being edited, or when adding a new rule.
+ ///
+ public FirewallRule Entry { get; }
+
+ ///
+ /// Protocols available in the protocol drop-down.
+ ///
+ public IEnumerable Protocols { get; } =
+ [
+ FirewallProtocol.Any,
+ FirewallProtocol.TCP,
+ FirewallProtocol.UDP,
+ FirewallProtocol.ICMPv4,
+ FirewallProtocol.ICMPv6,
+ FirewallProtocol.GRE,
+ FirewallProtocol.L2TP
+ ];
+
+ ///
+ /// Directions available in the direction drop-down.
+ ///
+ public IEnumerable Directions { get; } = Enum.GetValues();
+
+ ///
+ /// Actions available in the action drop-down.
+ ///
+ public IEnumerable Actions { get; } = Enum.GetValues();
+
+ ///
+ /// Interface types available in the interface type drop-down.
+ ///
+ public IEnumerable InterfaceTypes { get; } = Enum.GetValues();
+
+ ///
+ /// Human-readable display name of the rule (without the NETworkManager_ prefix).
+ ///
+ public string Name
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Indicates whether the rule is enabled.
+ ///
+ public bool IsEnabled
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Optional description of the rule.
+ ///
+ public string Description
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = string.Empty;
+
+ ///
+ /// Traffic direction (Inbound or Outbound).
+ ///
+ public FirewallRuleDirection Direction
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Rule action (Allow or Block).
+ ///
+ public FirewallRuleAction Action
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Network protocol. When changed away from TCP/UDP, local and remote port fields are cleared.
+ ///
+ public FirewallProtocol Protocol
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+
+ if (value is not (FirewallProtocol.TCP or FirewallProtocol.UDP))
+ {
+ LocalPorts = string.Empty;
+ RemotePorts = string.Empty;
+ }
+
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(PortsVisible));
+ }
+ }
+
+ ///
+ /// when the current protocol supports port filtering (TCP or UDP).
+ ///
+ public bool PortsVisible => Protocol is FirewallProtocol.TCP or FirewallProtocol.UDP;
+
+ ///
+ /// Semicolon-separated local port numbers or ranges (e.g. "80; 443; 8080-8090").
+ /// Only relevant when is TCP or UDP.
+ ///
+ public string LocalPorts
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = string.Empty;
+
+ ///
+ /// Semicolon-separated remote port numbers or ranges.
+ /// Only relevant when is TCP or UDP.
+ ///
+ public string RemotePorts
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = string.Empty;
+
+ ///
+ /// Semicolon-separated local addresses (IPs, CIDR subnets, or keywords such as LocalSubnet).
+ /// Empty means "Any".
+ ///
+ public string LocalAddresses
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = string.Empty;
+
+ ///
+ /// Semicolon-separated remote addresses.
+ /// Empty means "Any".
+ ///
+ public string RemoteAddresses
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = string.Empty;
+
+ ///
+ /// Full path to the executable this rule applies to. Empty means "Any program".
+ ///
+ public string Program
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = string.Empty;
+
+ ///
+ /// Network interface type filter.
+ ///
+ public FirewallInterfaceType InterfaceType
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Whether the rule applies to the Domain network profile.
+ ///
+ public bool NetworkProfileDomain
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Whether the rule applies to the Private network profile.
+ ///
+ public bool NetworkProfilePrivate
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Whether the rule applies to the Public network profile.
+ ///
+ public bool NetworkProfilePublic
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager/ViewModels/FirewallViewModel.cs b/Source/NETworkManager/ViewModels/FirewallViewModel.cs
index a11d89993d..13ef72216e 100644
--- a/Source/NETworkManager/ViewModels/FirewallViewModel.cs
+++ b/Source/NETworkManager/ViewModels/FirewallViewModel.cs
@@ -1,22 +1,29 @@
-using System.Threading.Tasks;
+using log4net;
+using MahApps.Metro.Controls;
+using MahApps.Metro.SimpleChildWindow;
using NETworkManager.Localization.Resources;
-
-namespace NETworkManager.ViewModels;
-
-using System.Windows;
-using System.Windows.Data;
-using System.Windows.Threading;
+using NETworkManager.Models.Export;
+using NETworkManager.Models.Firewall;
+using NETworkManager.Settings;
+using NETworkManager.Utilities;
+using NETworkManager.Views;
+using System;
+using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
-using System;
using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Data;
using System.Windows.Input;
+using System.Windows.Threading;
+
+namespace NETworkManager.ViewModels;
+
using Controls;
using Profiles;
using Models;
-using Settings;
-using Utilities;
///
/// ViewModel for the Firewall application.
@@ -24,12 +31,125 @@ namespace NETworkManager.ViewModels;
public class FirewallViewModel : ViewModelBase, IProfileManager
{
#region Variables
-
+
+ private static readonly ILog Log = LogManager.GetLogger(typeof(FirewallViewModel));
+
private readonly DispatcherTimer _searchDispatcherTimer = new();
private bool _searchDisabled;
private readonly bool _isLoading;
private bool _isViewActive = true;
+ #region Rules
+
+ ///
+ /// Gets the loaded firewall rules.
+ ///
+ public ObservableCollection Results { get; } = [];
+
+ ///
+ /// Gets the filtered/sorted view over .
+ ///
+ public ICollectionView ResultsView { get; }
+
+ ///
+ /// Gets or sets the currently selected firewall rule.
+ ///
+ public FirewallRule SelectedResult
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the list of selected firewall rules (multi-select).
+ ///
+ public IList SelectedResults
+ {
+ get;
+ set
+ {
+ if (Equals(value, field))
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = new ArrayList();
+
+ ///
+ /// Gets or sets the search text for filtering rules.
+ ///
+ public string RulesSearch
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ ResultsView.Refresh();
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets whether a refresh is currently running.
+ ///
+ public bool IsRefreshing
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets whether the status message bar is shown.
+ ///
+ public bool IsStatusMessageDisplayed
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the status message text.
+ ///
+ public string StatusMessage
+ {
+ get;
+ private set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ #endregion
+
#region Profiles
///
@@ -243,6 +363,25 @@ public FirewallViewModel()
{
_isLoading = true;
+ // Rules
+ ResultsView = CollectionViewSource.GetDefaultView(Results);
+ ResultsView.Filter = o =>
+ {
+ if (string.IsNullOrEmpty(RulesSearch))
+ return true;
+
+ 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;
+ };
+
+ // Load firewall rules
+ Refresh(true).ConfigureAwait(false);
+
// Profiles
CreateTags();
@@ -281,16 +420,273 @@ private void LoadSettings()
#region ICommand & Actions
///
- /// Gets the command to add a new firewall entry.
+ /// Gets the command to refresh the list of firewall rules from the system.
+ /// Disabled while a refresh is already in progress.
+ ///
+ public ICommand RefreshCommand => new RelayCommand(_ => RefreshAction().ConfigureAwait(false), Refresh_CanExecute);
+
+ ///
+ /// Returns when no refresh is currently running.
+ ///
+ private bool Refresh_CanExecute(object _) => !IsRefreshing;
+
+ ///
+ /// Delegates to to reload the firewall rules.
+ ///
+ private async Task RefreshAction() => await Refresh();
+
+ ///
+ /// Gets the command to open the dialog for adding a new firewall rule.
+ /// Only enabled when the application is running as administrator.
///
- public ICommand AddEntryCommand => new RelayCommand(_ => AddEntryAction());
+ public ICommand AddEntryCommand => new RelayCommand(_ => AddEntry().ConfigureAwait(false), _ => ModifyEntry_CanExecute());
///
- /// Action to add a new firewall entry.
+ /// Opens the add-firewall-rule dialog. On confirmation, creates the rule via PowerShell
+ /// and refreshes the rule list.
///
- private void AddEntryAction()
+ private async Task AddEntry()
{
- MessageBox.Show("Not implemented");
+ var childWindow = new FirewallRuleChildWindow();
+
+ var childWindowViewModel = new FirewallRuleViewModel(async instance =>
+ {
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
+
+ try
+ {
+ await Firewall.AddRuleAsync(BuildRule(instance));
+ await Refresh();
+ }
+ catch (Exception ex)
+ {
+ Log.Error("Error while adding firewall rule", ex);
+
+ StatusMessage = ex.Message;
+ IsStatusMessageDisplayed = true;
+ }
+ }, _ =>
+ {
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
+ });
+
+ childWindow.Title = Strings.AddEntry;
+ childWindow.DataContext = childWindowViewModel;
+
+ ConfigurationManager.Current.IsChildWindowOpen = true;
+
+ await Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
+ }
+
+ ///
+ /// Gets the command to enable the selected firewall rule.
+ /// Only executable when the rule is currently disabled and modification is allowed.
+ ///
+ public ICommand EnableEntryCommand => new RelayCommand(_ => SetRuleEnabled(SelectedResult, true).ConfigureAwait(false), _ => ModifyEntry_CanExecute() && SelectedResult is { IsEnabled: false });
+
+ ///
+ /// Gets the command to disable the selected firewall rule.
+ /// Only executable when the rule is currently enabled and modification is allowed.
+ ///
+ public ICommand DisableEntryCommand => new RelayCommand(_ => SetRuleEnabled(SelectedResult, false).ConfigureAwait(false), _ => ModifyEntry_CanExecute() && SelectedResult is { IsEnabled: true });
+
+ ///
+ /// Enables or disables the given via PowerShell,
+ /// then reloads the rule list to reflect the updated state.
+ /// Any PowerShell error is written to the log and shown in the status bar.
+ ///
+ ///
+ /// The firewall rule to modify.
+ ///
+ ///
+ /// to enable the rule; to disable it.
+ ///
+ private async Task SetRuleEnabled(FirewallRule rule, bool enabled)
+ {
+ try
+ {
+ await Firewall.SetRuleEnabledAsync(rule, enabled);
+ await Refresh();
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Error while {(enabled ? "enabling" : "disabling")} firewall rule", ex);
+
+ StatusMessage = ex.Message;
+ IsStatusMessageDisplayed = true;
+ }
+ }
+
+ ///
+ /// Gets the command to open the dialog for editing the selected firewall rule.
+ /// Only executable when a rule is selected and modification is allowed.
+ ///
+ public ICommand EditEntryCommand => new RelayCommand(_ => EditEntry().ConfigureAwait(false), _ => ModifyEntry_CanExecute() && SelectedResult != null);
+
+ ///
+ /// Opens the edit-firewall-rule dialog pre-filled with the selected rule's properties.
+ /// On confirmation, deletes the old rule, creates the updated rule via PowerShell,
+ /// and refreshes the rule list.
+ ///
+ private async Task EditEntry()
+ {
+ var childWindow = new FirewallRuleChildWindow();
+
+ var childWindowViewModel = new FirewallRuleViewModel(async instance =>
+ {
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
+
+ try
+ {
+ await Firewall.DeleteRuleAsync(instance.Entry);
+ await Firewall.AddRuleAsync(BuildRule(instance));
+ await Refresh();
+ }
+ catch (Exception ex)
+ {
+ Log.Error("Error while editing firewall rule", ex);
+
+ StatusMessage = ex.Message;
+ IsStatusMessageDisplayed = true;
+ }
+ }, _ =>
+ {
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
+ }, SelectedResult);
+
+ childWindow.Title = Strings.EditEntry;
+ childWindow.DataContext = childWindowViewModel;
+
+ ConfigurationManager.Current.IsChildWindowOpen = true;
+
+ await Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
+ }
+
+ ///
+ /// Gets the command to permanently delete the selected firewall rule.
+ /// Only executable when a rule is selected and modification is allowed.
+ ///
+ public ICommand DeleteEntryCommand => new RelayCommand(_ => DeleteEntry().ConfigureAwait(false), _ => ModifyEntry_CanExecute() && SelectedResult != null);
+
+ ///
+ /// Shows a confirmation dialog and, if confirmed, deletes the selected firewall rule
+ /// via PowerShell and reloads the rule list.
+ /// Any PowerShell error is written to the log and shown in the status bar.
+ ///
+ private async Task DeleteEntry()
+ {
+ var result = await DialogHelper.ShowConfirmationMessageAsync(
+ Application.Current.MainWindow,
+ Strings.DeleteEntry,
+ string.Format(Strings.DeleteFirewallRuleMessage, SelectedResult.Name),
+ ChildWindowIcon.Info,
+ Strings.Delete);
+
+ if (!result)
+ return;
+
+ try
+ {
+ await Firewall.DeleteRuleAsync(SelectedResult);
+ await Refresh();
+ }
+ catch (Exception ex)
+ {
+ Log.Error("Error while deleting firewall rule", ex);
+
+ StatusMessage = ex.Message;
+ IsStatusMessageDisplayed = true;
+ }
+ }
+
+ ///
+ /// Returns when the application is running as administrator,
+ /// no dialog is open, and no child window is open — i.e. it is safe to modify a rule.
+ ///
+ private static bool ModifyEntry_CanExecute()
+ {
+ return ConfigurationManager.Current.IsAdmin &&
+ Application.Current.MainWindow != null &&
+ !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
+ !ConfigurationManager.Current.IsChildWindowOpen;
+ }
+
+ ///
+ /// Gets the command to restart the application with administrator privileges.
+ ///
+ public ICommand RestartAsAdminCommand => new RelayCommand(_ => RestartAsAdminAction().ConfigureAwait(false));
+
+ ///
+ /// Restarts the application elevated. Shows an error dialog if the restart fails.
+ ///
+ private async Task RestartAsAdminAction()
+ {
+ try
+ {
+ (Application.Current.MainWindow as MainWindow)?.RestartApplication(true);
+ }
+ catch (Exception ex)
+ {
+ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Error, ex.Message,
+ ChildWindowIcon.Error);
+ }
+ }
+
+ ///
+ /// Gets the command to export the current firewall rule list to a file.
+ ///
+ public ICommand ExportCommand => new RelayCommand(_ => ExportAction().ConfigureAwait(false));
+
+ ///
+ /// Opens the export child window and writes the selected or all firewall rules to the
+ /// chosen file format (CSV, XML, or JSON). Shows an error dialog if the export fails.
+ ///
+ private Task ExportAction()
+ {
+ var childWindow = new ExportChildWindow();
+
+ var childWindowViewModel = new ExportViewModel(async instance =>
+ {
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
+
+ try
+ {
+ ExportManager.Export(instance.FilePath, instance.FileType,
+ instance.ExportAll
+ ? Results
+ : new ObservableCollection(SelectedResults.Cast().ToArray()));
+ }
+ catch (Exception ex)
+ {
+ Log.Error("Error while exporting data as " + instance.FileType, ex);
+
+ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Error,
+ Strings.AnErrorOccurredWhileExportingTheData + Environment.NewLine +
+ Environment.NewLine + ex.Message, ChildWindowIcon.Error);
+ }
+
+ SettingsManager.Current.Firewall_ExportFileType = instance.FileType;
+ SettingsManager.Current.Firewall_ExportFilePath = instance.FilePath;
+ }, _ =>
+ {
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
+ }, [
+ ExportFileType.Csv, ExportFileType.Xml, ExportFileType.Json
+ ], true, SettingsManager.Current.Firewall_ExportFileType,
+ SettingsManager.Current.Firewall_ExportFilePath);
+
+ childWindow.Title = Strings.Export;
+ childWindow.DataContext = childWindowViewModel;
+
+ ConfigurationManager.Current.IsChildWindowOpen = true;
+
+ return Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
}
///
@@ -493,6 +889,119 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro
#endregion
#region Methods
+
+ ///
+ /// Loads all NETworkManager firewall rules from the system via PowerShell and
+ /// replaces the contents of with the new list.
+ /// Updates throughout to reflect loading progress.
+ ///
+ ///
+ /// When the initial UI delay is skipped so the first load
+ /// on startup feels immediate.
+ ///
+ private async Task Refresh(bool init = false)
+ {
+ if (IsRefreshing)
+ return;
+
+ IsRefreshing = true;
+ StatusMessage = Strings.RefreshingDots;
+ IsStatusMessageDisplayed = true;
+
+ if (!init)
+ await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
+
+ try
+ {
+ var rules = await Firewall.GetRulesAsync();
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Results.Clear();
+
+ foreach (var rule in rules)
+ Results.Add(rule);
+ });
+
+ StatusMessage = string.Format(Strings.ReloadedAtX, DateTime.Now.ToShortTimeString());
+ IsStatusMessageDisplayed = true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error("Error while loading firewall rules", ex);
+
+ StatusMessage = string.Format(Strings.FailedToLoadFirewallRulesMessage, ex.Message);
+ IsStatusMessageDisplayed = true;
+ }
+
+ IsRefreshing = false;
+ }
+
+ ///
+ /// Builds a from the values the user entered in the dialog.
+ ///
+ /// The dialog ViewModel containing the user's input.
+ private static FirewallRule BuildRule(FirewallRuleViewModel vm) => new()
+ {
+ Name = vm.Name,
+ IsEnabled = vm.IsEnabled,
+ Description = vm.Description ?? string.Empty,
+ Direction = vm.Direction,
+ Action = vm.Action,
+ Protocol = vm.Protocol,
+ LocalPorts = ParsePortsString(vm.LocalPorts),
+ RemotePorts = ParsePortsString(vm.RemotePorts),
+ LocalAddresses = ParseAddressesString(vm.LocalAddresses),
+ RemoteAddresses = ParseAddressesString(vm.RemoteAddresses),
+ Program = string.IsNullOrWhiteSpace(vm.Program) ? null : new FirewallRuleProgram(vm.Program),
+ InterfaceType = vm.InterfaceType,
+ NetworkProfiles = [vm.NetworkProfileDomain, vm.NetworkProfilePrivate, vm.NetworkProfilePublic]
+ };
+
+ ///
+ /// Parses a semicolon-separated port string (e.g. "80; 443; 8080-8090") into a
+ /// list of objects.
+ ///
+ /// The semicolon-separated port string from the dialog.
+ private static List ParsePortsString(string value)
+ {
+ var list = new List();
+
+ if (string.IsNullOrWhiteSpace(value))
+ return list;
+
+ foreach (var token in value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ var dash = token.IndexOf('-');
+
+ if (dash > 0 &&
+ int.TryParse(token[..dash], out var start) &&
+ int.TryParse(token[(dash + 1)..], out var end))
+ {
+ list.Add(new FirewallPortSpecification(start, end));
+ }
+ else if (int.TryParse(token, out var port))
+ {
+ list.Add(new FirewallPortSpecification(port));
+ }
+ }
+
+ return list;
+ }
+
+ ///
+ /// Parses a semicolon-separated address string (e.g. "192.168.1.0/24; LocalSubnet")
+ /// into a list of address strings.
+ ///
+ /// The semicolon-separated address string from the dialog.
+ private static List ParseAddressesString(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return [];
+
+ return [.. value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
+ }
+
///
/// Sets the IsExpanded property for all profile groups.
///
diff --git a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs
index 2502a4da06..3abb776105 100644
--- a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs
+++ b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs
@@ -454,14 +454,13 @@ private async Task DeleteEntryAction()
{
IsModifying = true;
- var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow,
+ var result = await DialogHelper.ShowConfirmationMessageAsync(
+ Application.Current.MainWindow,
Strings.DeleteEntry,
string.Format(Strings.DeleteHostsFileEntryMessage, SelectedResult.IPAddress, SelectedResult.Hostname,
string.IsNullOrEmpty(SelectedResult.Comment) ? "" : $"# {SelectedResult.Comment}"),
ChildWindowIcon.Info,
- Strings.Delete
- );
-
+ Strings.Delete);
if (!result)
{
@@ -531,6 +530,28 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro
}
}
+ ///
+ /// Gets the command to open the hosts file with the system default editor.
+ ///
+ public ICommand OpenHostsFileCommand => new RelayCommand(_ => OpenHostsFileAction().ConfigureAwait(false));
+
+ ///
+ /// Opens the hosts file with the system default editor.
+ /// Shows an error dialog if the process cannot be started.
+ ///
+ private async Task OpenHostsFileAction()
+ {
+ try
+ {
+ ExternalProcessStarter.RunProcess(HostsFileEditor.HostsFilePath);
+ }
+ catch (Exception ex)
+ {
+ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Error, ex.Message,
+ ChildWindowIcon.Error);
+ }
+ }
+
#endregion
#region Methods
diff --git a/Source/NETworkManager/Views/ARPTableView.xaml b/Source/NETworkManager/Views/ARPTableView.xaml
index 797a97bc4e..d32fab70d1 100644
--- a/Source/NETworkManager/Views/ARPTableView.xaml
+++ b/Source/NETworkManager/Views/ARPTableView.xaml
@@ -186,19 +186,23 @@
MinWidth="100" />
-
-
+
+
+
+
+
-
-
+
diff --git a/Source/NETworkManager/Views/DiscoveryProtocolView.xaml b/Source/NETworkManager/Views/DiscoveryProtocolView.xaml
index 47de679080..ffe9eb0929 100644
--- a/Source/NETworkManager/Views/DiscoveryProtocolView.xaml
+++ b/Source/NETworkManager/Views/DiscoveryProtocolView.xaml
@@ -16,21 +16,24 @@
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -61,16 +64,8 @@
Text="{x:Static localization:Strings.DurationS}" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml b/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml
new file mode 100644
index 0000000000..c62896560c
--- /dev/null
+++ b/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml.cs b/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml.cs
new file mode 100644
index 0000000000..7e6b1d5685
--- /dev/null
+++ b/Source/NETworkManager/Views/FirewallRuleChildWindow.xaml.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Windows;
+using System.Windows.Threading;
+
+namespace NETworkManager.Views;
+
+public partial class FirewallRuleChildWindow
+{
+ public FirewallRuleChildWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e)
+ {
+ Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate
+ {
+ TextBoxName.Focus();
+ }));
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager/Views/FirewallView.xaml b/Source/NETworkManager/Views/FirewallView.xaml
index 656a84922b..229248b289 100644
--- a/Source/NETworkManager/Views/FirewallView.xaml
+++ b/Source/NETworkManager/Views/FirewallView.xaml
@@ -12,6 +12,7 @@
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings"
xmlns:controls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls"
+ xmlns:firewall="clr-namespace:NETworkManager.Models.Firewall;assembly=NETworkManager.Models"
xmlns:dialogs="clr-namespace:MahApps.Metro.Controls.Dialogs;assembly=MahApps.Metro"
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
xmlns:networkManager="clr-namespace:NETworkManager"
@@ -20,49 +21,502 @@
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
-
@@ -74,13 +528,15 @@
-
-
@@ -93,608 +549,647 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Views/FirewallView.xaml.cs b/Source/NETworkManager/Views/FirewallView.xaml.cs
index bc108f1bf9..2793e4ad87 100644
--- a/Source/NETworkManager/Views/FirewallView.xaml.cs
+++ b/Source/NETworkManager/Views/FirewallView.xaml.cs
@@ -1,6 +1,7 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
+using System.Windows.Media;
using NETworkManager.ViewModels;
namespace NETworkManager.Views;
@@ -35,6 +36,23 @@ private void ContextMenu_Opened(object sender, RoutedEventArgs e)
if (sender is ContextMenu menu)
menu.DataContext = _viewModel;
}
+
+ ///
+ /// Toggles the row details visibility when the expand/collapse chevron is clicked.
+ ///
+ private void ExpandRowDetails_OnClick(object sender, RoutedEventArgs e)
+ {
+ for (var visual = sender as Visual; visual != null; visual = VisualTreeHelper.GetParent(visual) as Visual)
+ {
+ if (visual is not DataGridRow row)
+ continue;
+
+ row.DetailsVisibility =
+ row.DetailsVisibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
+
+ break;
+ }
+ }
private void ListBoxItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
diff --git a/Source/NETworkManager/Views/HostsFileEditorView.xaml b/Source/NETworkManager/Views/HostsFileEditorView.xaml
index afdd720a2b..f70d7c1830 100644
--- a/Source/NETworkManager/Views/HostsFileEditorView.xaml
+++ b/Source/NETworkManager/Views/HostsFileEditorView.xaml
@@ -30,9 +30,12 @@
-
+
+
+
+
@@ -75,6 +78,7 @@
Style="{StaticResource ResourceKey=SearchTextBox}" />
+
-
+
+
+
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
@@ -247,13 +277,14 @@
-
-
-
+
+
diff --git a/Source/NETworkManager/Views/IPScannerView.xaml b/Source/NETworkManager/Views/IPScannerView.xaml
index 93591d34e8..5ec9d99894 100644
--- a/Source/NETworkManager/Views/IPScannerView.xaml
+++ b/Source/NETworkManager/Views/IPScannerView.xaml
@@ -606,7 +606,8 @@
+ Style="{StaticResource BoldTextBlock}"
+ Foreground="{DynamicResource MahApps.Brushes.Gray5}" />
@@ -696,7 +697,8 @@
+ Style="{StaticResource BoldTextBlock}"
+ Foreground="{DynamicResource MahApps.Brushes.Gray5}" />
+ Style="{StaticResource BoldTextBlock}"
+ Foreground="{DynamicResource MahApps.Brushes.Gray5}" />
+ Style="{StaticResource BoldTextBlock}"
+ Foreground="{DynamicResource MahApps.Brushes.Gray5}" />
+ Style="{StaticResource BoldTextBlock}"
+ Foreground="{DynamicResource MahApps.Brushes.Gray5}" />
-
-
+
+
+
+
+
-
-
+
diff --git a/Source/NETworkManager/Views/NetworkInterfaceView.xaml b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
index d65bc0e61a..2230bcfebb 100644
--- a/Source/NETworkManager/Views/NetworkInterfaceView.xaml
+++ b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
@@ -37,6 +37,7 @@
x:Key="IPAddressSubnetmaskTupleArrayToStringConverter" />
+
@@ -222,8 +223,9 @@
+
-
@@ -252,6 +254,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1527,7 +1545,7 @@
IsChecked="{Binding Path=ProfileFilterTagsMatchAll}"
Margin="10,0,0,0" />
-