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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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}" /> + - + + + - + + + + + + + + + - - - + - - - + + 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" /> -