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/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..803af9f1ec 100644 --- a/Source/NETworkManager.Localization/ResourceIdentifier.cs +++ b/Source/NETworkManager.Localization/ResourceIdentifier.cs @@ -22,5 +22,7 @@ public enum ResourceIdentifier TcpState, Theme, TimeUnit, - WiFiConnectionStatus + WiFiConnectionStatus, + FirewallProtocol, + FirewallInterfaceType } \ 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..e3a4e842f5 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -7343,7 +7343,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 +11571,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. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 789507cf64..21fc7e296c 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. @@ -4037,6 +4043,9 @@ You can copy your profile files from “{0}” to “{1}” to migrate your exis Block + + Network profile + Network profiles @@ -4046,4 +4055,61 @@ You can copy your profile files from “{0}” to “{1}” to migrate your exis Failed to load firewall rules. {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 + \ 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..5222e34eda --- /dev/null +++ b/Source/NETworkManager.Models/Firewall/Firewall.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using SMA = System.Management.Automation; +using System.Threading.Tasks; +using log4net; + +namespace NETworkManager.Models.Firewall; + +/// +/// Represents a firewall configuration and management class that provides functionalities +/// for applying and managing firewall rules based on a specified profile. +/// +public class Firewall +{ + #region Variables + + /// + /// The Logger. + /// + private static readonly ILog Log = LogManager.GetLogger(typeof(Firewall)); + + private const string RuleIdentifier = "NETworkManager_"; + + #endregion + + #region Methods + + /// + /// Reads all Windows Firewall rules whose display name starts with + /// and maps them to objects. + /// + /// A task that resolves to the list of matching firewall rules. + public static async Task> GetRulesAsync() + { + return await Task.Run(() => + { + var rules = new List(); + + using var ps = SMA.PowerShell.Create(); + + ps.AddScript($@" +Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process +Import-Module NetSecurity -ErrorAction Stop +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; + }); + } + + /// Parses a PowerShell direction string to . + private static FirewallRuleDirection ParseDirection(string value) + { + return value switch + { + "Outbound" => FirewallRuleDirection.Outbound, + _ => FirewallRuleDirection.Inbound, + }; + } + + /// Parses a PowerShell action string to . + private static FirewallRuleAction ParseAction(string value) + { + return value switch + { + "Allow" => FirewallRuleAction.Allow, + _ => FirewallRuleAction.Block, + }; + } + + /// Parses a PowerShell protocol string to . + 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 for Any or blank input. + /// + 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. + /// + 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 for Any or blank input. + /// + 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 to . + private static FirewallInterfaceType ParseInterfaceType(string value) + { + return value switch + { + "Wired" => FirewallInterfaceType.Wired, + "Wireless" => FirewallInterfaceType.Wireless, + "RemoteAccess" => FirewallInterfaceType.RemoteAccess, + _ => FirewallInterfaceType.Any, + }; + } + #endregion +} 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..1af34b849e --- /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); + + /// 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); + + /// 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 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/NETworkManager.Models.csproj b/Source/NETworkManager.Models/NETworkManager.Models.csproj index c913dd5488..d5123a011d 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.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/ViewModels/FirewallViewModel.cs b/Source/NETworkManager/ViewModels/FirewallViewModel.cs index a11d89993d..4a5173afe6 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(); @@ -280,17 +419,98 @@ private void LoadSettings() #region ICommand & Actions - /// - /// Gets the command to add a new firewall entry. - /// + /// Gets the command to refresh the firewall rules. + public ICommand RefreshCommand => new RelayCommand(_ => RefreshAction().ConfigureAwait(false), Refresh_CanExecute); + + private bool Refresh_CanExecute(object _) => !IsRefreshing; + + private async Task RefreshAction() => await Refresh(); + + /// Gets the command to add a new firewall entry. public ICommand AddEntryCommand => new RelayCommand(_ => AddEntryAction()); - /// - /// Action to add a new firewall entry. - /// private void AddEntryAction() { - MessageBox.Show("Not implemented"); + // TODO: open AddFirewallRuleDialog + } + + /// Gets the command to enable the selected firewall entry. + public ICommand EnableEntryCommand => new RelayCommand(_ => EnableEntryAction(), _ => SelectedResult is { IsEnabled: false }); + + private void EnableEntryAction() + { + // TODO: call Firewall.SetRuleEnabledAsync(SelectedResult, true) + } + + /// Gets the command to disable the selected firewall entry. + public ICommand DisableEntryCommand => new RelayCommand(_ => DisableEntryAction(), _ => SelectedResult is { IsEnabled: true }); + + private void DisableEntryAction() + { + // TODO: call Firewall.SetRuleEnabledAsync(SelectedResult, false) + } + + /// Gets the command to edit the selected firewall entry. + public ICommand EditEntryCommand => new RelayCommand(_ => EditEntryAction(), _ => SelectedResult != null); + + private void EditEntryAction() + { + // TODO: open EditFirewallRuleDialog + } + + /// Gets the command to delete the selected firewall entry. + public ICommand DeleteEntryCommand => new RelayCommand(_ => DeleteEntryAction(), _ => SelectedResult != null); + + private void DeleteEntryAction() + { + // TODO: confirm and call Firewall.DeleteRuleAsync + } + + /// Gets the command to export the firewall rules. + public ICommand ExportCommand => new RelayCommand(_ => ExportAction().ConfigureAwait(false)); + + 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 +713,48 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro #endregion #region Methods + + /// + /// Loads firewall rules from Windows via PowerShell and populates . + /// + 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; + } + /// /// Sets the IsExpanded property for all profile groups. /// diff --git a/Source/NETworkManager/Views/FirewallView.xaml b/Source/NETworkManager/Views/FirewallView.xaml index 656a84922b..db23e12392 100644 --- a/Source/NETworkManager/Views/FirewallView.xaml +++ b/Source/NETworkManager/Views/FirewallView.xaml @@ -12,6 +12,7 @@ xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings" xmlns:controls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls" + xmlns:firewall="clr-namespace:NETworkManager.Models.Firewall;assembly=NETworkManager.Models" xmlns:dialogs="clr-namespace:MahApps.Metro.Controls.Dialogs;assembly=MahApps.Metro" xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF" xmlns:networkManager="clr-namespace:NETworkManager" @@ -22,6 +23,9 @@ + + + @@ -37,32 +41,461 @@ + + + - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - @@ -102,7 +538,7 @@ - + /// 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/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" /> -