diff --git a/Source/NETworkManager.Converters/BoolArrayToFwRuleCategoriesConverter.cs b/Source/NETworkManager.Converters/BoolArrayToFwRuleCategoriesConverter.cs
new file mode 100644
index 0000000000..24beacb641
--- /dev/null
+++ b/Source/NETworkManager.Converters/BoolArrayToFwRuleCategoriesConverter.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Data;
+using NETworkManager.Models.Network;
+using NETworkManager.Localization.Resources;
+
+
+namespace NETworkManager.Converters;
+
+public class BoolArrayToFwRuleCategoriesConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (targetType != typeof(string)) return null;
+ const int expectedLength = 3;
+ var fallback = GetTranslation(Enum.GetName(NetworkProfiles.NotConfigured), false);
+ if (value is not bool[] { Length: expectedLength } boolArray)
+ return fallback;
+ var result = string.Empty;
+ var numSelected = boolArray.CountAny(true);
+ switch (numSelected)
+ {
+ case 0:
+ return fallback;
+ case < 2:
+ return GetTranslation(Enum.GetName(typeof(NetworkProfiles),
+ Array.FindIndex(boolArray, b => b)), false);
+ }
+
+ if (boolArray.All(b => b))
+ return Strings.All;
+
+ for (var i = 0; i < expectedLength; i++)
+ {
+ if (boolArray[i])
+ result += $", {GetTranslation(Enum.GetName(typeof(NetworkProfiles), i), true)}";
+ }
+
+ return result[2..];
+
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static string GetTranslation(string key, bool trimmed)
+ {
+ return Strings.ResourceManager.GetString(trimmed ? $"{key}_Short3" : key, Strings.Culture);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/CollectionPropertyBooleanOrConverter.cs b/Source/NETworkManager.Converters/CollectionPropertyBooleanOrConverter.cs
new file mode 100644
index 0000000000..1b23919988
--- /dev/null
+++ b/Source/NETworkManager.Converters/CollectionPropertyBooleanOrConverter.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Windows.Data;
+using NETworkManager.Interfaces.ViewModels;
+
+namespace NETworkManager.Converters
+{
+ ///
+ /// A generic converter that checks a property of items in a collection.
+ /// If ANY item's property is considered "present" (not null, not empty), it returns Visible.
+ ///
+ /// The type of item in the collection.
+ public class CollectionPropertyBooleanOrConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // 1. Validate inputs
+ if (value is not IEnumerable collection)
+ return false;
+
+ if (parameter is not string propertyName || string.IsNullOrEmpty(propertyName))
+ return false;
+
+ // 2. Get PropertyInfo via Reflection or cache.
+ if (!Cache.TryGetValue(propertyName, out var propertyInfo))
+ {
+ propertyInfo = typeof(T).GetProperty(propertyName);
+ Cache.TryAdd(propertyName, propertyInfo);
+ }
+
+ if (propertyInfo == null)
+ return false;
+
+ // 3. Iterate collection and check property
+ foreach (var item in collection)
+ {
+ if (item == null) continue;
+
+ var propValue = propertyInfo.GetValue(item);
+
+ if (propValue is true)
+ return true;
+ }
+
+ return false;
+ }
+
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private ConcurrentDictionary Cache { get; } = new();
+ }
+
+ // Concrete implementation for XAML usage
+ public class FirewallRuleViewModelBooleanOrConverter : CollectionPropertyBooleanOrConverter;
+}
+
diff --git a/Source/NETworkManager.Converters/CollectionPropertyVisibilityConverter.cs b/Source/NETworkManager.Converters/CollectionPropertyVisibilityConverter.cs
new file mode 100644
index 0000000000..6e069a87c7
--- /dev/null
+++ b/Source/NETworkManager.Converters/CollectionPropertyVisibilityConverter.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Windows;
+using System.Windows.Data;
+using NETworkManager.Interfaces.ViewModels;
+
+namespace NETworkManager.Converters
+{
+ ///
+ /// A generic converter that checks a property of items in a collection.
+ /// If ANY item's property is considered "present" (not null, not empty), it returns Visible.
+ ///
+ /// The type of item in the collection.
+ public class CollectionPropertyVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // 1. Validate inputs
+ if (value is not IEnumerable collection)
+ return Visibility.Collapsed;
+
+ if (parameter is not string propertyName || string.IsNullOrEmpty(propertyName))
+ return Visibility.Collapsed;
+
+ // 2. Get PropertyInfo via Reflection or cache.
+ if (!Cache.TryGetValue(propertyName, out var propertyInfo))
+ {
+ propertyInfo = typeof(T).GetProperty(propertyName);
+ Cache.TryAdd(propertyName, propertyInfo);
+ }
+
+ if (propertyInfo == null)
+ return Visibility.Collapsed;
+
+ // 3. Iterate collection and check property
+ foreach (var item in collection)
+ {
+ if (item == null) continue;
+
+ var propValue = propertyInfo.GetValue(item);
+
+ if (HasContent(propValue))
+ {
+ return Visibility.Visible;
+ }
+ }
+
+ return Visibility.Collapsed;
+ }
+
+ private static bool HasContent(object value)
+ {
+ if (value == null) return false;
+
+ // Handle Strings
+ if (value is string str)
+ return !string.IsNullOrWhiteSpace(str);
+
+ // Handle Collections (Lists, Arrays, etc.)
+ if (value is ICollection col)
+ return col.Count > 0;
+
+ // Handle Generic Enumerable (fallback)
+ if (value is IEnumerable enumValue)
+ {
+ var enumerator = enumValue.GetEnumerator();
+ var result = enumerator.MoveNext(); // Has at least one item
+ (enumerator as IDisposable)?.Dispose();
+ return result;
+ }
+
+ // Default: If it's an object and not null, it's "True"
+ return true;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private ConcurrentDictionary Cache { get; } = new();
+ }
+
+ // Concrete implementation for XAML usage
+ public class FirewallRuleViewModelVisibilityConverter : CollectionPropertyVisibilityConverter;
+}
diff --git a/Source/NETworkManager.Converters/EmptyToIntMaxValueConverter.cs b/Source/NETworkManager.Converters/EmptyToIntMaxValueConverter.cs
new file mode 100644
index 0000000000..01db260ede
--- /dev/null
+++ b/Source/NETworkManager.Converters/EmptyToIntMaxValueConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NETworkManager.Converters;
+
+public class EmptyToIntMaxValueConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ const int fallback = int.MaxValue;
+ if (targetType == typeof(int))
+ {
+ if (value is not string strValue)
+ return fallback;
+ if (string.IsNullOrWhiteSpace(strValue))
+ return fallback;
+ if (!int.TryParse(strValue, out int parsedIntValue))
+ return fallback;
+ return parsedIntValue;
+ }
+
+ if (targetType != typeof(string))
+ return null;
+ if (value is not int intValue)
+ return null;
+ return intValue is fallback ? string.Empty : intValue.ToString();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Convert(value, targetType, parameter, culture);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/EnumToIntConverter.cs b/Source/NETworkManager.Converters/EnumToIntConverter.cs
new file mode 100644
index 0000000000..1dbaa4e5dc
--- /dev/null
+++ b/Source/NETworkManager.Converters/EnumToIntConverter.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+
+namespace NETworkManager.Converters;
+
+///
+/// A value converter that facilitates the conversion between enumeration values and their corresponding integer indices or string names.
+///
+///
+/// This converter is designed to either:
+/// - Convert an enumeration value to its integer index within the enumeration.
+/// - Convert an enumeration value to its string representation.
+/// When converting back, the same logic is applied to handle appropriate conversion.
+///
+public class EnumToIntConverter : IValueConverter
+{
+ ///
+ /// Converts an Enum value to its corresponding integer index or string representation
+ /// based on the target type. If the target type is an Enum, the method attempts
+ /// to return the name of the enum value. If the provided value is an Enum, it
+ /// returns the integer index of the value within the Enum's definition.
+ ///
+ /// The value to convert. This can be an Enum instance or null.
+ /// The target type for the conversion. Typically either Enum or int.
+ /// An optional parameter for additional input, not used in this method.
+ /// The culture information for the conversion process.
+ ///
+ /// If the targetType is an Enum, a string representation of the Enum name is returned.
+ /// If the value is an Enum, the integer index of the Enum value is returned.
+ /// If neither condition is met, null is returned.
+ ///
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (targetType.IsEnum)
+ {
+ return value is null ? string.Empty :
+ Enum.GetName(targetType, value);
+ }
+ if (value is Enum enumVal)
+ {
+ return Array.IndexOf(Enum.GetValues(enumVal.GetType()), enumVal);
+ }
+
+ return null;
+ }
+
+ /// Converts a value back into its corresponding enumeration value or integer representation.
+ /// This method is typically used in two-way data bindings where an integer or string needs
+ /// to be converted back to the corresponding enum value.
+ /// The value to be converted back. This can be an integer or a string representation of an enumeration value.
+ /// The type of the target object, typically the type of the enumeration.
+ /// An optional parameter for the conversion. Not used in this implementation.
+ /// The culture information to use during the conversion process.
+ /// The enumeration value corresponding to the input value.
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Convert(value, targetType, parameter, culture);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/EnumToStringConverter.cs b/Source/NETworkManager.Converters/EnumToStringConverter.cs
new file mode 100644
index 0000000000..5eb327e300
--- /dev/null
+++ b/Source/NETworkManager.Converters/EnumToStringConverter.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections;
+using System.Globalization;
+using System.Resources;
+using System.Windows.Data;
+using NETworkManager.Localization.Resources;
+
+namespace NETworkManager.Converters;
+
+///
+/// A converter used to transform an value into its corresponding string,
+/// using localization resources for string mapping. Also provides functionality to convert
+/// localized or raw string values back to the corresponding value.
+///
+///
+/// This class is not guaranteed to be thread-safe. It should be used only in the context of the application’s
+/// UI thread or with proper synchronization for shared use cases.
+///
+///
+public sealed class EnumToStringConverter : IValueConverter
+{
+ /// Converts an enumeration value to its localized string representation and vice versa.
+ ///
+ /// The value to convert. This can be an enumeration value or a string.
+ ///
+ ///
+ /// The expected type of the target binding, typically the type of the enumeration being converted.
+ ///
+ ///
+ /// An optional parameter to use in the conversion process, if applicable.
+ ///
+ ///
+ /// The culture information to use during the conversion, affecting localization.
+ ///
+ ///
+ /// A string representing the localized name of the enumeration, or the enumeration corresponding
+ /// to the given string. If the conversion fails, a fallback enumeration value or string is returned.
+ ///
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is not Enum enumValue)
+ {
+ string fallback = Enum.ToObject(targetType, 255) as string;
+ if (value is not string strVal)
+ return fallback;
+ fallback = strVal;
+ ResourceSet resourceSet = Strings.ResourceManager.GetResourceSet(Strings.Culture,
+ false, true);
+ string foundKey = null;
+ if (resourceSet is null)
+ return fallback;
+ foreach (DictionaryEntry item in resourceSet)
+ {
+ if (item.Value as string != strVal && item.Key as string != strVal)
+ continue;
+ foundKey = item.Key as string;
+ break;
+ }
+
+ if (foundKey is null || !Enum.TryParse(targetType, foundKey, out var result))
+ return fallback;
+ return result;
+ }
+
+ var enumString = Enum.GetName(enumValue.GetType(), value);
+ if (enumString is null)
+ return string.Empty;
+ return Strings.ResourceManager.GetString(enumString, Strings.Culture) ?? enumString;
+ }
+
+ ///
+ /// Converts a value back from a string representation to its corresponding enumeration value.
+ ///
+ /// The value to be converted back. Expected to be a string representation of an enumeration.
+ /// The type of the enumeration to which the value will be converted.
+ /// An optional parameter that can be used for custom conversion logic.
+ /// The culture information to use during the conversion process.
+ ///
+ /// Returns the enumeration value corresponding to the string representation. If the conversion fails,
+ /// a default value of the enumeration is returned.
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Convert(value, targetType, parameter, culture);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/FirewallRuleProgramConverter.cs b/Source/NETworkManager.Converters/FirewallRuleProgramConverter.cs
new file mode 100644
index 0000000000..accec2a1d1
--- /dev/null
+++ b/Source/NETworkManager.Converters/FirewallRuleProgramConverter.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Windows.Data;
+using NETworkManager.Models.Firewall;
+
+namespace NETworkManager.Converters;
+
+///
+/// Convert a program reference to a string and vice versa.
+///
+public class FirewallRuleProgramConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (targetType == typeof(FirewallRuleProgram))
+ {
+ if (value is not string program)
+ return null;
+ if (string.IsNullOrWhiteSpace(program))
+ return null;
+ try
+ {
+ var exe = new FirewallRuleProgram(program);
+ return exe;
+ }
+ catch (ArgumentNullException)
+ {
+ return null;
+ }
+ }
+
+ if (targetType != typeof(string))
+ return null;
+ return value is not FirewallRuleProgram prog ? null : prog.ToString();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Convert(value, targetType, parameter, culture);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/IntZeroToFalseConverter.cs b/Source/NETworkManager.Converters/IntZeroToFalseConverter.cs
new file mode 100644
index 0000000000..592da208fe
--- /dev/null
+++ b/Source/NETworkManager.Converters/IntZeroToFalseConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NETworkManager.Converters;
+///
+/// Convert a value of 0 to the boolean false.
+///
+public class IntZeroToFalseConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value switch
+ {
+ null => false,
+ int i => i is not 0,
+ bool boolean => boolean ? 1 : 0,
+ _ => false
+ };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Convert(value, targetType, parameter, culture);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/NETworkManager.Converters.csproj b/Source/NETworkManager.Converters/NETworkManager.Converters.csproj
index 0457284fc5..8c721cf425 100644
--- a/Source/NETworkManager.Converters/NETworkManager.Converters.csproj
+++ b/Source/NETworkManager.Converters/NETworkManager.Converters.csproj
@@ -20,6 +20,7 @@
+
diff --git a/Source/NETworkManager.Converters/PortRangeToPortSpecificationConverter.cs b/Source/NETworkManager.Converters/PortRangeToPortSpecificationConverter.cs
new file mode 100644
index 0000000000..fd9e954def
--- /dev/null
+++ b/Source/NETworkManager.Converters/PortRangeToPortSpecificationConverter.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Data;
+using NETworkManager.Models.Firewall;
+using NETworkManager.Settings;
+
+namespace NETworkManager.Converters;
+
+///
+/// Converts a port range or port to an instance of FirewallPortSpecification.
+///
+public class PortRangeToPortSpecificationConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ char portDelimiter = ';';
+ if (targetType == typeof(List))
+ {
+ if (value is not string input)
+ return null;
+ const char rangeDelimiter = '-';
+ List resultList = [];
+ var portList = input.Split(portDelimiter);
+ foreach (var port in portList)
+ {
+ var trimmedPort = port.Trim();
+ if (trimmedPort.Contains(rangeDelimiter))
+ {
+ var portRange = trimmedPort.Split(rangeDelimiter);
+ if (!int.TryParse(portRange[0], out var startPort))
+ return null;
+ if (!int.TryParse(portRange[1], out var endPort))
+ return null;
+ resultList.Add(new FirewallPortSpecification(startPort, endPort));
+ }
+ else
+ {
+ if (!int.TryParse(port, out var portNumber))
+ return null;
+ resultList.Add(new FirewallPortSpecification(portNumber));
+ }
+ }
+ return resultList;
+ }
+
+ if (targetType != typeof(string))
+ return null;
+ if (value is not List portSpecs)
+ return string.Empty;
+ string result = portSpecs
+ .Aggregate("", (current, portSpecification) => current + $"{portSpecification}{portDelimiter} ");
+ return result.Length > 0 ? result[..^2] : string.Empty;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Convert(value, targetType, parameter, culture);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/SizeFactorConverter.cs b/Source/NETworkManager.Converters/SizeFactorConverter.cs
new file mode 100644
index 0000000000..e2a23b1e5a
--- /dev/null
+++ b/Source/NETworkManager.Converters/SizeFactorConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NETworkManager.Converters;
+///
+/// Multiplies a value by a factor given with the parameter.
+///
+///
+/// Useful for setting sizes relative to window size.
+///
+public class SizeFactorConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ try
+ {
+ double theValue = System.Convert.ToDouble(value, CultureInfo.InvariantCulture);
+ double factor = System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture);
+ return theValue * factor;
+ }
+ catch (Exception e) when (e is FormatException or InvalidCastException or OverflowException)
+ {
+ return 0.0;
+ }
+ }
+
+ 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.Interfaces/NETworkManager.Interfaces.csproj b/Source/NETworkManager.Interfaces/NETworkManager.Interfaces.csproj
new file mode 100644
index 0000000000..422d2aaa6d
--- /dev/null
+++ b/Source/NETworkManager.Interfaces/NETworkManager.Interfaces.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net10.0-windows10.0.22621.0
+ enable
+ enable
+ NETworkManager.Interfaces
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager.Interfaces/ViewModels/IFirewallRuleViewModel.cs b/Source/NETworkManager.Interfaces/ViewModels/IFirewallRuleViewModel.cs
new file mode 100644
index 0000000000..0590f8b277
--- /dev/null
+++ b/Source/NETworkManager.Interfaces/ViewModels/IFirewallRuleViewModel.cs
@@ -0,0 +1,30 @@
+using NETworkManager.Models.Firewall;
+
+namespace NETworkManager.Interfaces.ViewModels;
+
+///
+/// Interface to allow converters and validators access to the firewall rule view model,
+/// in this case validating with the other network profile checkboxes.
+///
+public interface IFirewallRuleViewModel
+{
+ public bool NetworkProfileDomain { get; }
+
+ public bool NetworkProfilePrivate { get; }
+
+ public bool NetworkProfilePublic { get; }
+
+ public List? LocalPorts { get; }
+
+ public List? RemotePorts { get; }
+
+ public FirewallRuleProgram? Program { get; }
+
+ public int MaxLengthName { get; }
+
+ public string? UserDefinedName { get; }
+
+ public bool NameHasError { get; set; }
+
+ public bool HasError { get; }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Interfaces/ViewModels/IFirewallViewModel.cs b/Source/NETworkManager.Interfaces/ViewModels/IFirewallViewModel.cs
new file mode 100644
index 0000000000..8daca21889
--- /dev/null
+++ b/Source/NETworkManager.Interfaces/ViewModels/IFirewallViewModel.cs
@@ -0,0 +1,22 @@
+using System.Collections.ObjectModel;
+using System.Windows.Input;
+
+namespace NETworkManager.Interfaces.ViewModels;
+
+public interface IFirewallViewModel
+{
+ public ObservableCollection FirewallRulesInterface { get; }
+
+ public ICommand ExpandAllProfileGroupsCommand { get; }
+
+ public ICommand CollapseAllProfileGroupsCommand { get; }
+
+ public int MaxLengthHistory { get; }
+
+ public static IFirewallViewModel? Instance { get; set; }
+
+ public static void SetInstance(IFirewallViewModel? viewModel)
+ {
+ Instance = viewModel;
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Interfaces/ViewModels/IProfileViewModel.cs b/Source/NETworkManager.Interfaces/ViewModels/IProfileViewModel.cs
new file mode 100644
index 0000000000..5a9f24ccca
--- /dev/null
+++ b/Source/NETworkManager.Interfaces/ViewModels/IProfileViewModel.cs
@@ -0,0 +1,15 @@
+namespace NETworkManager.Interfaces.ViewModels;
+
+// ReSharper disable InconsistentNaming
+public interface IProfileViewModel
+{
+ #region General
+ public string Name { get; }
+ #endregion
+
+ #region Firewall
+ public bool Firewall_Enabled { get; set; }
+
+ public IFirewallViewModel Firewall_IViewModel { get; set; }
+ #endregion
+}
\ 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 5264d10300..4dd707481e 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -6201,6 +6201,15 @@ public static string NetworkPacketsCaptureAdminMessage {
}
}
+ ///
+ /// 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 Networks.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 4e6ebe5412..3789592b0d 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -3945,6 +3945,9 @@ If you click Cancel, the profile file will remain unencrypted.
A restart is required to apply changes such as language settings.
+
+ Network profile
+
Could not parse or resolve any of the specified DNS servers.
diff --git a/Source/NETworkManager.Models/Firewall/Firewall.cs b/Source/NETworkManager.Models/Firewall/Firewall.cs
new file mode 100644
index 0000000000..2214745296
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/Firewall.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using log4net;
+using NETworkManager.Models.Network;
+using NETworkManager.Utilities;
+
+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 readonly ILog Log = LogManager.GetLogger(typeof(Firewall));
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Applies the firewall rules specified in the list to the system's firewall settings.
+ ///
+ /// A list of objects representing the firewall rules to be applied.
+ /// Returns true if the rules were successfully applied; otherwise, false.
+ private bool ApplyRules(List rules)
+ {
+ // If there are no rules to apply, return true as there is nothing to do.
+ if (rules.Count is 0)
+ return true;
+
+ // Start by clearing all existing rules for the current profile to ensure a clean state.
+ var sb = new StringBuilder(GetClearAllRulesCommand());
+ sb.Append("; ");
+
+ foreach (var rule in rules)
+ {
+ try
+ {
+ var ruleSb = new StringBuilder($"New-NetFirewallRule -DisplayName '{SanitizeStringArguments(rule.Name)}'");
+
+ if (!string.IsNullOrEmpty(rule.Description))
+ ruleSb.Append($" -Description '{SanitizeStringArguments(rule.Description)}'");
+
+ ruleSb.Append($" -Direction {Enum.GetName(rule.Direction)}");
+
+ if (rule.LocalPorts.Count > 0 && rule.Protocol is FirewallProtocol.TCP or FirewallProtocol.UDP)
+ ruleSb.Append($" -LocalPort {FirewallRule.PortsToString(rule.LocalPorts, ',', false)}");
+
+ if (rule.RemotePorts.Count > 0 && rule.Protocol is FirewallProtocol.TCP or FirewallProtocol.UDP)
+ ruleSb.Append($" -RemotePort {FirewallRule.PortsToString(rule.RemotePorts, ',', false)}");
+
+ ruleSb.Append(rule.Protocol is FirewallProtocol.Any
+ ? " -Protocol Any"
+ : $" -Protocol {(int)rule.Protocol}");
+
+ if (!string.IsNullOrWhiteSpace(rule.Program?.Name))
+ {
+ if (File.Exists(rule.Program.Name))
+ ruleSb.Append($" -Program '{SanitizeStringArguments(rule.Program.Name)}'");
+ else
+ {
+ Log.Warn($"Program path '{rule.Program.Name}' in rule '{rule.Name}' does not exist. Skipping rule.");
+ continue;
+ }
+ }
+
+ if (rule.InterfaceType is not FirewallInterfaceType.Any)
+ ruleSb.Append($" -InterfaceType {Enum.GetName(rule.InterfaceType)}");
+
+ // If not all network profiles are enabled, specify the ones that are.
+ if (!rule.NetworkProfiles.All(x => x))
+ {
+ var profiles = Enumerable.Range(0, rule.NetworkProfiles.Length)
+ .Where(i => rule.NetworkProfiles[i])
+ .Select(i => Enum.GetName(typeof(NetworkProfiles), i));
+
+ ruleSb.Append($" -Profile {string.Join(',', profiles)}");
+ }
+
+ ruleSb.Append($" -Action {Enum.GetName(rule.Action)}; ");
+
+ sb.Append(ruleSb);
+ }
+ catch (ArgumentException ex)
+ {
+ Log.Warn($"Failed to build firewall rule '{rule.Name}': {ex.Message}");
+ }
+ }
+
+ // Remove the trailing "; " from the last command.
+ sb.Length -= 2;
+
+ var command = sb.ToString();
+
+ Log.Debug($"Applying rules:{Environment.NewLine}{command}");
+
+ try
+ {
+ PowerShellHelper.ExecuteCommand(command, true);
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Removes all existing firewall rules associated with the specified profile.
+ ///
+ ///
+ /// This method clears all configured firewall rules for the current profile.
+ /// It is useful for resetting the rules to a clean state. Errors or exceptions
+ /// during the operation are logged using the configured logging mechanism.
+ ///
+ public static void ClearAllRules()
+ {
+ PowerShellHelper.ExecuteCommand(GetClearAllRulesCommand(), true);
+ }
+
+ ///
+ /// Generates a command string that removes all Windows Firewall rules with a display name starting with 'NwM_'.
+ ///
+ /// A command string that can be executed in PowerShell to remove the specified firewall rules.
+ private static string GetClearAllRulesCommand()
+ {
+ return "Remove-NetFirewallRule -DisplayName 'NwM_*'";
+ }
+
+ ///
+ /// Sanitizes string arguments by replacing single quotes with double single quotes to prevent issues in PowerShell command execution.
+ ///
+ /// The input string to be sanitized.
+ /// A sanitized string with single quotes escaped.
+ private static string SanitizeStringArguments(string value)
+ {
+ return value.Replace("'", "''");
+ }
+
+ ///
+ /// Applies firewall rules asynchronously by invoking the ApplyRules method in a separate task.
+ ///
+ /// A list of firewall rules to apply.
+ /// A task representing the asynchronous operation. The task result contains a boolean indicating whether the rules were successfully applied.
+ public async Task ApplyRulesAsync(List rules)
+ {
+ await Task.Run(() => ApplyRules(rules));
+ }
+ #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..c9273fba28
--- /dev/null
+++ b/Source/NETworkManager.Models/Firewall/FirewallRule.cs
@@ -0,0 +1,140 @@
+using System.Collections.Generic;
+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
+
+ ///
+ /// Represents the name associated with the object.
+ ///
+ ///
+ /// This property is used to identify the object with a descriptive, human-readable name.
+ /// The value of this property is typically a string and can be used for display purposes
+ /// or as an identifier in various contexts.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 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; }
+
+ ///
+ /// 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 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 whitespace = spacing ? " " : string.Empty;
+
+ var builder = new StringBuilder();
+
+ foreach (var port in ports)
+ builder.Append($"{port}{separator}{whitespace}");
+
+ var offset = spacing ? 2 : 1;
+
+ return builder.ToString()[..^offset];
+ }
+ #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/Network/NetworkProfiles.cs b/Source/NETworkManager.Models/Network/NetworkProfiles.cs
new file mode 100644
index 0000000000..4d7d52d203
--- /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 NetworkProfiles
+{
+ ///
+ /// 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.Utilities.WPF/NETworkManager.Utilities.WPF.csproj b/Source/NETworkManager.Utilities.WPF/NETworkManager.Utilities.WPF.csproj
index 06652b0713..98eaffd891 100644
--- a/Source/NETworkManager.Utilities.WPF/NETworkManager.Utilities.WPF.csproj
+++ b/Source/NETworkManager.Utilities.WPF/NETworkManager.Utilities.WPF.csproj
@@ -18,4 +18,7 @@
+
+
+
diff --git a/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/FirewallViewModelProxy.cs b/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/FirewallViewModelProxy.cs
new file mode 100644
index 0000000000..6e99fb16bf
--- /dev/null
+++ b/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/FirewallViewModelProxy.cs
@@ -0,0 +1,36 @@
+using System.Windows;
+using NETworkManager.Interfaces.ViewModels;
+
+namespace NETworkManager.Utilities.WPF.TypedBindingProxies;
+
+///
+/// Binding proxy for s.
+///
+public class FirewallViewModelProxy : Freezable
+{
+ ///
+ /// Dependency property used to hold a generic object.
+ /// This property allows data binding scenarios where a proxy
+ /// is required to transfer data between binding contexts.
+ ///
+ public static readonly DependencyProperty DataProperty =
+ DependencyProperty.Register(nameof(Data), typeof(IFirewallViewModel), typeof(FirewallViewModelProxy));
+
+ ///
+ /// Gets or sets the data object used for binding in WPF applications.
+ ///
+ public IFirewallViewModel Data
+ {
+ get => (IFirewallViewModel)GetValue(DataProperty);
+ set => SetValue(DataProperty, value);
+ }
+
+ /// Creates a new instance of the BindingProxy class.
+ ///
+ /// A new instance of the BindingProxy class.
+ ///
+ protected override Freezable CreateInstanceCore()
+ {
+ return new FirewallViewModelProxy();
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/FrameworkElementProxy.cs b/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/FrameworkElementProxy.cs
new file mode 100644
index 0000000000..0c1d0a1947
--- /dev/null
+++ b/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/FrameworkElementProxy.cs
@@ -0,0 +1,35 @@
+using System.Windows;
+
+namespace NETworkManager.Utilities.WPF.TypedBindingProxies;
+
+///
+/// Binding proxy for s.
+///
+public class FrameworkElementProxy : Freezable
+{
+ ///
+ /// Dependency property used to hold a generic object.
+ /// This property allows data binding scenarios where a proxy
+ /// is required to transfer data between binding contexts.
+ ///
+ public static readonly DependencyProperty DataProperty =
+ DependencyProperty.Register(nameof(Data), typeof(FrameworkElement), typeof(FrameworkElementProxy));
+
+ ///
+ /// Gets or sets the data object used for binding in WPF applications.
+ ///
+ public FrameworkElement Data
+ {
+ get => (FrameworkElement)GetValue(DataProperty);
+ set => SetValue(DataProperty, value);
+ }
+
+ /// Creates a new instance of the BindingProxy class.
+ ///
+ /// A new instance of the BindingProxy class.
+ ///
+ protected override Freezable CreateInstanceCore()
+ {
+ return new FrameworkElementProxy();
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/ProfileViewModelProxy.cs b/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/ProfileViewModelProxy.cs
new file mode 100644
index 0000000000..7099c04c0d
--- /dev/null
+++ b/Source/NETworkManager.Utilities.WPF/TypedBindingProxies/ProfileViewModelProxy.cs
@@ -0,0 +1,36 @@
+using System.Windows;
+using NETworkManager.Interfaces.ViewModels;
+
+namespace NETworkManager.Utilities.WPF.TypedBindingProxies;
+
+///
+/// Binding proxy for s.
+///
+public class ProfileViewModelProxy : Freezable
+{
+ ///
+ /// Dependency property used to hold a generic object.
+ /// This property allows data binding scenarios where a proxy
+ /// is required to transfer data between binding contexts.
+ ///
+ public static readonly DependencyProperty DataProperty =
+ DependencyProperty.Register(nameof(Data), typeof(IProfileViewModel), typeof(ProfileViewModelProxy));
+
+ ///
+ /// Gets or sets the data object used for binding in WPF applications.
+ ///
+ public IProfileViewModel Data
+ {
+ get => (IProfileViewModel)GetValue(DataProperty);
+ set => SetValue(DataProperty, value);
+ }
+
+ /// Creates a new instance of the BindingProxy class.
+ ///
+ /// A new instance of the BindingProxy class.
+ ///
+ protected override Freezable CreateInstanceCore()
+ {
+ return new ProfileViewModelProxy();
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Utilities.WPF/ValidationHelper.cs b/Source/NETworkManager.Utilities.WPF/ValidationHelper.cs
new file mode 100644
index 0000000000..4910a4f715
--- /dev/null
+++ b/Source/NETworkManager.Utilities.WPF/ValidationHelper.cs
@@ -0,0 +1,68 @@
+using System;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace NETworkManager.Utilities.WPF;
+
+///
+/// This allows propagating validation errors to a ViewModel allowing style changes bound
+/// to the view, e.g., red border on a DataGridRow.
+///
+///
+/// Class is AI-generated. See FirewallRuleGrid control for usage.
+///
+public static class ValidationHelper
+{
+ // This property acts as a bridge. We can write to it from a Style Trigger,
+ // and it can push that value to the ViewModel via OneWayToSource binding.
+ public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached(
+ "HasError",
+ typeof(bool),
+ typeof(ValidationHelper),
+ new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
+
+ public static void SetHasError(DependencyObject element, bool value) => element.SetValue(HasErrorProperty, value);
+
+ public static bool GetHasError(DependencyObject element) => (bool)element.GetValue(HasErrorProperty);
+
+ // Observe validation errors directly. Required unless NotifyOnValidationErrors is set.
+ public static readonly DependencyProperty ObserveValidationProperty = DependencyProperty.RegisterAttached(
+ "ObserveValidation",
+ typeof(bool),
+ typeof(ValidationHelper),
+ new PropertyMetadata(false, OnObserveValidationChanged));
+
+ public static void SetObserveValidation(DependencyObject element, bool value) => element.SetValue(ObserveValidationProperty, value);
+
+ public static bool GetObserveValidation(DependencyObject element) => (bool)element.GetValue(ObserveValidationProperty);
+
+ private static void OnObserveValidationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not FrameworkElement element)
+ return;
+
+ if ((bool)e.NewValue)
+ {
+ // Listen to the Validation.HasError property changes directly
+ var descriptor = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, typeof(FrameworkElement));
+ descriptor.AddValueChanged(element, OnValidationHasErrorChanged);
+
+ // Initial sync
+ SetHasError(element, Validation.GetHasError(element));
+ }
+ else
+ {
+ var descriptor = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, typeof(FrameworkElement));
+ descriptor.RemoveValueChanged(element, OnValidationHasErrorChanged);
+ }
+ }
+
+ private static void OnValidationHasErrorChanged(object sender, EventArgs e)
+ {
+ if (sender is DependencyObject d)
+ {
+ SetHasError(d, Validation.GetHasError(d));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/AnyNetworkProfileValidator.cs b/Source/NETworkManager.Validators/AnyNetworkProfileValidator.cs
new file mode 100644
index 0000000000..f104a52e26
--- /dev/null
+++ b/Source/NETworkManager.Validators/AnyNetworkProfileValidator.cs
@@ -0,0 +1,40 @@
+using System.Globalization;
+using System.Linq;
+using System.Windows.Controls;
+using System.Windows.Data;
+using NETworkManager.Localization.Resources;
+using NETworkManager.Interfaces.ViewModels;
+
+namespace NETworkManager.Validators;
+
+///
+/// Checks for any network profile to be selected.
+///
+///
+/// Has to be called in ValidationStep.UpdatedValue or later. This means that the value update cannot be prevented,
+/// however, we can still show the configuration error.
+///
+public class AnyNetworkProfileValidator : ValidationRule
+{
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ var bindingExpression = value as BindingExpression;
+ if (bindingExpression?.DataItem is not IFirewallRuleViewModel viewModel)
+ {
+ return new ValidationResult(false,
+ "No ViewModel could be found. Is the ValidationStep set correctly?");
+ }
+ if (bindingExpression.DataItem is true)
+ return ValidationResult.ValidResult;
+
+ bool[] currentValues =
+ [
+ viewModel.NetworkProfileDomain,
+ viewModel.NetworkProfilePrivate,
+ viewModel.NetworkProfilePublic
+ ];
+
+ return currentValues.Any(x => x) ? ValidationResult.ValidResult
+ : new ValidationResult(false, Strings.AtLeastOneNetworkProfileMustBeSelected);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/EmptyOrFileExistsValidator.cs b/Source/NETworkManager.Validators/EmptyOrFileExistsValidator.cs
index d669e71e4d..e24ce4ffb9 100644
--- a/Source/NETworkManager.Validators/EmptyOrFileExistsValidator.cs
+++ b/Source/NETworkManager.Validators/EmptyOrFileExistsValidator.cs
@@ -1,19 +1,22 @@
using System.Globalization;
-using System.IO;
using System.Windows.Controls;
-using NETworkManager.Localization.Resources;
namespace NETworkManager.Validators;
public class EmptyOrFileExistsValidator : ValidationRule
{
- public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ private static FileExistsValidator FileExistsValidator
{
- if (string.IsNullOrEmpty(value as string))
- return ValidationResult.ValidResult;
+ get
+ {
+ field ??= new FileExistsValidator();
+ return field;
+ }
+ }
- return File.Exists((string)value)
- ? ValidationResult.ValidResult
- : new ValidationResult(false, Strings.FileDoesNotExist);
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ return string.IsNullOrEmpty(value as string)
+ ? ValidationResult.ValidResult : FileExistsValidator.Validate(value, cultureInfo);
}
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/EmptyOrFileIsExeValidator.cs b/Source/NETworkManager.Validators/EmptyOrFileIsExeValidator.cs
new file mode 100644
index 0000000000..69e2dd05f1
--- /dev/null
+++ b/Source/NETworkManager.Validators/EmptyOrFileIsExeValidator.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using System.Windows.Controls;
+using NETworkManager.Localization.Resources;
+
+namespace NETworkManager.Validators;
+
+///
+/// Evaluate whether the file path is a *.exe or *.EXE file.
+///
+public class EmptyOrFileIsExeValidator : ValidationRule
+{
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ if (value is not string strVal || string.IsNullOrEmpty(strVal)
+ || strVal.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
+ return ValidationResult.ValidResult;
+ return new ValidationResult(false, Strings.EnterPathToExe);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/EmptyOrFilePathValidator.cs b/Source/NETworkManager.Validators/EmptyOrFilePathValidator.cs
new file mode 100644
index 0000000000..20896606bc
--- /dev/null
+++ b/Source/NETworkManager.Validators/EmptyOrFilePathValidator.cs
@@ -0,0 +1,23 @@
+using System.Globalization;
+using System.Windows.Controls;
+
+namespace NETworkManager.Validators;
+
+public class EmptyOrFilePathValidator : ValidationRule
+{
+ private static FilePathValidator FilePathValidator
+ {
+ get
+ {
+ field ??= new FilePathValidator();
+ return field;
+ }
+ }
+
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ if (value is not string strValue || string.IsNullOrEmpty(strValue))
+ return ValidationResult.ValidResult;
+ return FilePathValidator.Validate(value, cultureInfo);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/EmptyOrFirewallPortRangeValidator.cs b/Source/NETworkManager.Validators/EmptyOrFirewallPortRangeValidator.cs
new file mode 100644
index 0000000000..3c23da1d67
--- /dev/null
+++ b/Source/NETworkManager.Validators/EmptyOrFirewallPortRangeValidator.cs
@@ -0,0 +1,53 @@
+using System.Globalization;
+using System.Windows.Controls;
+using NETworkManager.Localization.Resources;
+using NETworkManager.Settings;
+
+namespace NETworkManager.Validators;
+
+public class EmptyOrFirewallPortRangeValidator : ValidationRule
+{
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ if (string.IsNullOrEmpty(value as string))
+ return ValidationResult.ValidResult;
+
+ var isValid = true;
+ char portSeparator = ';';
+ var portList = ((string)value).Replace(" ", "").Split(portSeparator);
+ if (portList.Length > 10000)
+ return new ValidationResult(false, Strings.EnterLessThan10001PortsOrPortRanges);
+ foreach (var portOrRange in portList)
+ if (portOrRange.Contains('-'))
+ {
+ var portRange = portOrRange.Split('-');
+
+ if (int.TryParse(portRange[0], out var startPort) && int.TryParse(portRange[1], out var endPort))
+ {
+ if (startPort is < 0 or > 65536 || endPort is < 0 or > 65536 ||
+ startPort > endPort)
+ isValid = false;
+ }
+ else
+ {
+ isValid = false;
+ }
+ }
+ else
+ {
+ if (int.TryParse(portOrRange, out var portNumber))
+ {
+ if (portNumber is <= 0 or >= 65536)
+ isValid = false;
+ }
+ else
+ {
+ isValid = false;
+ }
+ }
+
+ return isValid
+ ? ValidationResult.ValidResult
+ : new ValidationResult(false, Strings.EnterValidPortOrPortRange);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/EmptyOrInt32Validator.cs b/Source/NETworkManager.Validators/EmptyOrInt32Validator.cs
new file mode 100644
index 0000000000..ffb3c3adef
--- /dev/null
+++ b/Source/NETworkManager.Validators/EmptyOrInt32Validator.cs
@@ -0,0 +1,23 @@
+using System.Globalization;
+using System.Windows.Controls;
+
+namespace NETworkManager.Validators;
+
+public class EmptyOrInt32Validator : ValidationRule
+{
+ private static Int32Validator Int32Validator
+ {
+ get
+ {
+ field ??= new Int32Validator();
+ return field;
+ }
+ }
+
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ if (value is string strValue && string.IsNullOrEmpty(strValue))
+ return ValidationResult.ValidResult;
+ return Int32Validator.Validate(value, cultureInfo);
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/FileNameValidator.cs b/Source/NETworkManager.Validators/FileNameValidator.cs
index 7defc70b48..7c44c747b3 100644
--- a/Source/NETworkManager.Validators/FileNameValidator.cs
+++ b/Source/NETworkManager.Validators/FileNameValidator.cs
@@ -10,7 +10,7 @@ namespace NETworkManager.Validators;
/// Check if the filename is a valid file name (like "text.txt"). The file name does not have to exist on the local
/// system.
///
-public class FileNameValidator : ValidationRule
+public partial class FileNameValidator : ValidationRule
{
///
/// Check if the filename is a valid file name (like "text.txt"). The filen ame does not have to exist on the local
@@ -24,8 +24,11 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
var filename = (string)value;
// Check if the filename has valid chars and a dot.
- return filename.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 && new Regex(@"^.+\..+$").IsMatch(filename)
+ return (filename?.IndexOfAny(Path.GetInvalidFileNameChars()) ?? 1) < 0 && FileRegex().IsMatch(filename)
? ValidationResult.ValidResult
: new ValidationResult(false, Strings.EnterValidFileName);
}
+
+ [GeneratedRegex(@"^.+\..+$")]
+ private static partial Regex FileRegex();
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/FirewallRuleNameValidator.cs b/Source/NETworkManager.Validators/FirewallRuleNameValidator.cs
new file mode 100644
index 0000000000..217687a6a3
--- /dev/null
+++ b/Source/NETworkManager.Validators/FirewallRuleNameValidator.cs
@@ -0,0 +1,50 @@
+using System.Globalization;
+using System.Windows.Controls;
+using System.Windows.Data;
+using NETworkManager.Interfaces.ViewModels;
+using NETworkManager.Localization.Resources;
+
+namespace NETworkManager.Validators;
+
+///
+/// Provides validation logic for user-defined names or descriptions used in firewall rules.
+/// Ensures the value meets specific criteria for validity:
+/// - The character '|' is not allowed.
+/// - The string length does not exceed 9999 characters.
+///
+public class FirewallRuleNameValidator : ValidationRule
+{
+ ///
+ /// Validates a string based on the following two conditions:
+ /// - The string must not contain the '|' character.
+ /// - The string must not exceed a length of 9999 characters.
+ ///
+ /// The value to be validated.
+ /// The culture information used during validation.
+ /// A ValidationResult indicating whether the string is valid or not.
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ var bindingExpression = value as BindingExpression;
+ if (bindingExpression?.DataItem is not IFirewallRuleViewModel viewModel)
+ {
+ return new ValidationResult(false,
+ "No ViewModel could be found. Is the ValidationStep set correctly?");
+ }
+
+ if (string.IsNullOrEmpty(viewModel.UserDefinedName))
+ {
+ viewModel.NameHasError = false;
+ return ValidationResult.ValidResult;
+ }
+
+ if (viewModel.UserDefinedName.Length <= viewModel.MaxLengthName)
+ {
+ viewModel.NameHasError = false;
+ return ValidationResult.ValidResult;
+ }
+
+ viewModel.NameHasError = true;
+ return new ValidationResult(false, Strings.InputLengthExceeded);
+ }
+
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/FirewallRuleNoPipeValidator.cs b/Source/NETworkManager.Validators/FirewallRuleNoPipeValidator.cs
new file mode 100644
index 0000000000..0f77c07d30
--- /dev/null
+++ b/Source/NETworkManager.Validators/FirewallRuleNoPipeValidator.cs
@@ -0,0 +1,39 @@
+using System.Globalization;
+using System.Windows.Controls;
+using System.Windows.Data;
+using NETworkManager.Interfaces.ViewModels;
+using NETworkManager.Localization.Resources;
+
+namespace NETworkManager.Validators;
+
+public class FirewallRuleNoPipeValidator : ValidationRule
+{
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ string valueToCheck;
+ bool isProfileValidation = false;
+ if (value is not BindingExpression bindingExpression)
+ {
+ valueToCheck = value?.ToString();
+ }
+ else if (bindingExpression.DataItem is not IProfileViewModel viewModel)
+ {
+ return new ValidationResult(false,
+ "No ViewModel could be found. Is the ValidationStep set correctly?");
+ }
+ else
+ {
+ isProfileValidation = true;
+ if (!viewModel.Firewall_Enabled)
+ return ValidationResult.ValidResult;
+ valueToCheck = viewModel.Name;
+ }
+ if (string.IsNullOrWhiteSpace(valueToCheck))
+ return ValidationResult.ValidResult;
+ return valueToCheck.Contains('|')
+ ? new ValidationResult(false,
+ isProfileValidation ? Strings.PipeNotAllowedWhenFirewallEnabled : Strings.PipeNotAllowed)
+ : ValidationResult.ValidResult;
+
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.Validators/NETworkManager.Validators.csproj b/Source/NETworkManager.Validators/NETworkManager.Validators.csproj
index 2a0c1a9f7d..bc989c92c2 100644
--- a/Source/NETworkManager.Validators/NETworkManager.Validators.csproj
+++ b/Source/NETworkManager.Validators/NETworkManager.Validators.csproj
@@ -21,6 +21,7 @@
+
diff --git a/Source/NETworkManager.Validators/ProgramNameLengthValidator.cs b/Source/NETworkManager.Validators/ProgramNameLengthValidator.cs
new file mode 100644
index 0000000000..d52e61aeb8
--- /dev/null
+++ b/Source/NETworkManager.Validators/ProgramNameLengthValidator.cs
@@ -0,0 +1,16 @@
+using System.Globalization;
+using System.Windows.Controls;
+using NETworkManager.Localization.Resources;
+
+namespace NETworkManager.Validators;
+
+public class ProgramNameLengthValidator : ValidationRule
+{
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ if (value is not string strVal)
+ return ValidationResult.ValidResult;
+ return strVal.Length > 259 ? new ValidationResult(false, Strings.ProgramNameTooLong)
+ : ValidationResult.ValidResult;
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager.sln b/Source/NETworkManager.sln
index bcfaeb362c..fef9fd0592 100644
--- a/Source/NETworkManager.sln
+++ b/Source/NETworkManager.sln
@@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dragablz", "3rdparty\Dragab
EndProject
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "NETworkManager.Setup", "NETworkManager.Setup\NETworkManager.Setup.wixproj", "{B474758D-E657-4DA6-AB2B-63764C5FBAF1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NETworkManager.Interfaces", "NETworkManager.Interfaces\NETworkManager.Interfaces.csproj", "{F25AA52C-7203-4F3D-B9B2-915DFFD25294}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -338,6 +340,26 @@ Global
{B474758D-E657-4DA6-AB2B-63764C5FBAF1}.Release|x64.Build.0 = Release|x64
{B474758D-E657-4DA6-AB2B-63764C5FBAF1}.Release|x86.ActiveCfg = Release|x86
{B474758D-E657-4DA6-AB2B-63764C5FBAF1}.Release|x86.Build.0 = Release|x86
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|ARM.Build.0 = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|x64.Build.0 = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Debug|x86.Build.0 = Debug|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|ARM.ActiveCfg = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|ARM.Build.0 = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|ARM64.Build.0 = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|x64.ActiveCfg = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|x64.Build.0 = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|x86.ActiveCfg = Release|Any CPU
+ {F25AA52C-7203-4F3D-B9B2-915DFFD25294}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Source/NETworkManager/Controls/FirewallRuleEnumTranslations.cs b/Source/NETworkManager/Controls/FirewallRuleEnumTranslations.cs
new file mode 100644
index 0000000000..ddd0b73f20
--- /dev/null
+++ b/Source/NETworkManager/Controls/FirewallRuleEnumTranslations.cs
@@ -0,0 +1,35 @@
+using NETworkManager.Models.Firewall;
+using NETworkManager.ViewModels;
+
+namespace NETworkManager.Controls;
+
+///
+/// Static class to profile strings for Enum translations.
+///
+public static class FirewallRuleEnumTranslation
+{
+ ///
+ /// Names of the firewall rule actions.
+ ///
+ public static string[] ActionNames =>
+ FirewallRuleViewModel.GetEnumTranslation(typeof(FirewallRuleAction));
+
+ ///
+ /// Names of the directions.
+ ///
+ public static string[] DirectionNames =>
+ FirewallRuleViewModel.GetEnumTranslation(typeof(FirewallRuleDirection));
+
+ ///
+ /// Names of the protocols.
+ ///
+ public static string[] ProtocolNames =>
+ FirewallRuleViewModel.GetEnumTranslation(typeof(FirewallProtocol));
+
+ ///
+ /// Names of the interface types.
+ ///
+ public static string[] InterfaceTypeNames =>
+ FirewallRuleViewModel.GetEnumTranslation(typeof(FirewallInterfaceType));
+}
+
diff --git a/Source/NETworkManager/Controls/FirewallRuleGrid.xaml b/Source/NETworkManager/Controls/FirewallRuleGrid.xaml
new file mode 100644
index 0000000000..490ad77058
--- /dev/null
+++ b/Source/NETworkManager/Controls/FirewallRuleGrid.xaml
@@ -0,0 +1,724 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/NETworkManager/Controls/FirewallRuleGrid.xaml.cs b/Source/NETworkManager/Controls/FirewallRuleGrid.xaml.cs
new file mode 100644
index 0000000000..692fa2ed1b
--- /dev/null
+++ b/Source/NETworkManager/Controls/FirewallRuleGrid.xaml.cs
@@ -0,0 +1,393 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using System.Windows.Threading;
+using NETworkManager.Converters;
+using NETworkManager.Localization.Resources;
+using NETworkManager.Models.Firewall;
+using NETworkManager.ViewModels;
+
+namespace NETworkManager.Controls;
+
+///
+/// Code-behind for the firewall rule grid.
+///
+public partial class FirewallRuleGrid
+{
+ ///
+ /// Mutex to prevent spamming the history for every rule.
+ ///
+ ///
+ /// Every rule's local and remote port combo boxes are registered to the
+ /// method. So, if one rule changes its
+ /// port values, every rule's combo boxes will trigger the event. Preventing this
+ /// with the following static variables.
+ ///
+ private static readonly Mutex LocalPortsAdding = new();
+ ///
+ /// Last local port string, which was added.
+ ///
+ private static string _lastLocalPortsString = string.Empty;
+ ///
+ /// Mutex for remote ports being added to the history.
+ ///
+ private static readonly Mutex RemotePortsAdding = new();
+ ///
+ /// Last remote port string, which was added.
+ ///
+ private static string _lastRemotePortsString = string.Empty;
+
+ ///
+ /// List of TextBlock content to ignore on double clicks.
+ ///
+ private static readonly string[] IgnoredTextBlocks = [Strings.Domain, Strings.Private, Strings.Public];
+
+ public FirewallRuleGrid()
+ {
+ InitializeComponent();
+ RestoreRuleGridFocus();
+ }
+
+ #region Events
+ ///
+ /// Handle new valid ports and add them to the history.
+ ///
+ /// The combo box. Not necessarily the one with the change.
+ /// Event arguments. Unused.
+ private void ComboBoxPorts_OnLostFocus(object sender, RoutedEventArgs e)
+ {
+ if (sender is not ComboBox comboBox)
+ return;
+ if (string.IsNullOrWhiteSpace(comboBox.Text))
+ return;
+ if (Validation.GetHasError(comboBox))
+ return;
+ var portType = (FirewallPortLocation)comboBox.Tag;
+ if (comboBox.DataContext is not FirewallRuleViewModel dataContext)
+ return;
+ var converter = new PortRangeToPortSpecificationConverter();
+ var structuredData = converter.Convert(comboBox.Text, typeof(List), null, null);
+ if (structuredData is not List list)
+ return;
+ string formattedText = converter.ConvertBack(list, typeof(string), null, null) as string;
+ if (string.IsNullOrWhiteSpace(formattedText))
+ return;
+
+ // Direct visual update: Find the internal TextBox and force formatting the text.
+ // This bypasses ComboBox.Text property synchronization issues during LostFocus.
+ if (GetVisualChild(comboBox) is { } textBox)
+ {
+ // Only update if visually different to avoid cursor/selection side effects
+ if (textBox.Text != formattedText)
+ textBox.Text = formattedText;
+ }
+ switch (portType)
+ {
+ case FirewallPortLocation.LocalPorts:
+ if (_lastLocalPortsString == formattedText)
+ return;
+
+ try
+ {
+ LocalPortsAdding?.WaitOne();
+ _lastLocalPortsString = formattedText;
+ dataContext.AddPortsToHistory(formattedText, portType);
+ }
+ finally
+ {
+ LocalPortsAdding?.ReleaseMutex();
+ }
+ break;
+ case FirewallPortLocation.RemotePorts:
+ if (_lastRemotePortsString == formattedText)
+ return;
+
+ try
+ {
+ RemotePortsAdding?.WaitOne();
+ _lastRemotePortsString = formattedText;
+ dataContext.AddPortsToHistory(formattedText, portType);
+ }
+ finally
+ {
+ RemotePortsAdding?.ReleaseMutex();
+ }
+ break;
+ }
+ }
+
+ ///
+ /// Toggle row visibility.
+ ///
+ ///
+ ///
+ private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (e.OriginalSource is not Button)
+ return;
+ // Get the row from the sender
+ 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;
+ }
+ }
+
+ ///
+ /// Collapse and open rule details by double clicking.
+ ///
+ ///
+ ///
+ private void Row_OnDoubleClick(object sender, RoutedEventArgs e)
+ {
+ // Avoid collision with ButtonBase behavior
+ if (e.OriginalSource is Button or Rectangle)
+ return;
+ // Get the row from the sender
+ for (var visual = sender as Visual; visual != null; visual = VisualTreeHelper.GetParent(visual) as Visual)
+ {
+ if (visual is not DataGridRow row)
+ continue;
+
+ if (row.DetailsVisibility is Visibility.Visible
+ && e.OriginalSource is not Border and not TextBlock and not CheckBox)
+ return;
+ if (e.OriginalSource is TextBlock textBlock && IgnoredTextBlocks.Contains(textBlock.DataContext as string))
+ return;
+ row.DetailsVisibility =
+ row.DetailsVisibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
+ break;
+ }
+ }
+
+
+ ///
+ /// Set the data context of the context menu.
+ ///
+ ///
+ ///
+ private void ContextMenu_Opened(object sender, RoutedEventArgs e)
+ {
+ if (sender is ContextMenu menu)
+ menu.DataContext = DataContext;
+ }
+
+ ///
+ /// Keyboard control
+ ///
+ ///
+ ///
+ private void DataGridRow_PreviewKeyDown(object sender, KeyEventArgs e)
+ {
+ // Only handle if the focus is on the cell itself (navigation mode),
+ // to avoid interfering with editing (e.g., inside a TextBox).
+ if (e.OriginalSource is TextBox or CheckBox or ComboBox)
+ return;
+
+ if (sender is not MultiSelectDataGrid grid)
+ return;
+
+ if (DataContext is not FirewallViewModel dataContext)
+ return;
+
+ DataGridRow row = null;
+ if (grid.SelectedIndex > -1 && e.OriginalSource is DependencyObject source)
+ row = ItemsControl.ContainerFromElement(grid, source) as DataGridRow;
+
+ switch (e.Key)
+ {
+ case Key.A when Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl):
+ //dataContext.ApplyConfigurationCommand.Execute(null);
+ e.Handled = true;
+ break;
+ case Key.D when Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl):
+ case Key.Delete:
+ int index = RuleGrid.SelectedIndex;
+ //dataContext.DeleteRulesCommand.Execute(null);
+ if (grid.HasItems)
+ {
+ // Select the same index or the last item if we deleted the end
+ index = Math.Min(index, grid.Items.Count - 1);
+ if (index < 0)
+ index = 0;
+ grid.SelectedIndex = index;
+ grid.Focus();
+ }
+
+ e.Handled = true;
+ break;
+ case Key.C when (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
+ && (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
+ && (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)):
+ // FirewallViewModel.DeleteWindowsRulesCommand.Execute(null);
+ e.Handled = true;
+ break;
+ case Key.C when (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
+ && (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)):
+ //dataContext.DeleteAllRulesCommand.Execute(null);
+ e.Handled = true;
+ break;
+ case Key.N when Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl):
+ //dataContext.AddRuleCommand.Execute(null);
+ e.Handled = true;
+ break;
+ case Key.Right when row?.DetailsVisibility == Visibility.Collapsed:
+ row.DetailsVisibility = Visibility.Visible;
+ e.Handled = true;
+ break;
+ case Key.Left when row?.DetailsVisibility == Visibility.Visible:
+ row.DetailsVisibility = Visibility.Collapsed;
+ e.Handled = true;
+ break;
+ // If nothing is selected, start from the bottom or top row.
+ case Key.Down:
+ case Key.Up:
+ if (!grid.IsKeyboardFocused)
+ return;
+ if (row is null && grid.HasItems)
+ {
+ if (grid.SelectedIndex is -1)
+ index = e.Key is Key.Down ? 0 : grid.Items.Count - 1;
+ else
+ index = e.Key is Key.Down ? grid.SelectedIndex + 1 : grid.SelectedIndex - 1;
+ if (index < 0)
+ index = 0;
+ if (index >= grid.Items.Count)
+ index = grid.Items.Count - 1;
+ grid.SelectedIndex = index;
+ var item = grid.Items[grid.SelectedIndex] as FirewallRuleViewModel;
+ if (item is not null)
+ grid.ScrollIntoView(item);
+ grid.UpdateLayout();
+ row = grid.ItemContainerGenerator.ContainerFromIndex(grid.SelectedIndex) as DataGridRow;
+ var viewModel = grid.DataContext as FirewallViewModel;
+ //viewModel?.SelectedRule = item;
+ }
+ FocusManager.SetFocusedElement(FocusManager.GetFocusScope(grid), null);
+ Keyboard.ClearFocus();
+ // DataGridRow is not focusable.
+ if (row is not null)
+ {
+ var cell = GetCell(grid, row, 1);
+ cell?.Focus();
+ }
+ e.Handled = true;
+ break;
+ case Key.W when Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl):
+ //dataContext.OpenWindowsFirewallCommand.Execute(null);
+ e.Handled = true;
+ break;
+ }
+ }
+
+ ///
+ /// Gets a DataGridCell from a given DataGrid, given row and column accounting for virtualization.
+ ///
+ /// The DataGrid to retrieve the cell from.
+ /// The row instance to get the index from.
+ /// The column index.
+ /// A DataGridCell instance.
+ private static DataGridCell GetCell(DataGrid grid, DataGridRow row, int column)
+ {
+ if (row == null) return null;
+
+ var presenter = GetVisualChild(row);
+ if (presenter != null)
+ return presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
+ grid.ScrollIntoView(row, grid.Columns[column]);
+ presenter = GetVisualChild(row);
+
+ return presenter?.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
+ }
+
+ ///
+ /// Get the first child of a Visual, which is not null.
+ ///
+ /// Parent of children.
+ /// Any Visual type.
+ /// First non-null child found or null.
+ private static T GetVisualChild(Visual parent) where T : Visual
+ {
+ T child = null;
+ int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
+ for (int i = 0; i < numVisuals; i++)
+ {
+ var v = (Visual)VisualTreeHelper.GetChild(parent, i);
+ child = v as T ?? GetVisualChild(v);
+ if (child != null)
+ {
+ break;
+ }
+ }
+ return child;
+ }
+
+ ///
+ /// Restore the focus to the RuleGrid, whenever it is lost by column sorting or button clicks.
+ /// Also called on loading.
+ ///
+ public void RestoreRuleGridFocus()
+ {
+ if (RuleGrid is null)
+ return;
+
+ // Post-focus: let WPF finish the click/command that caused focus to move away.
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ if (!IsVisible || !IsEnabled)
+ return;
+
+ if (!RuleGrid.HasItems)
+ {
+ RuleGrid.Focus();
+ return;
+ }
+
+ if (RuleGrid.SelectedIndex < 0)
+ RuleGrid.SelectedIndex = 0;
+
+ RuleGrid.ScrollIntoView(RuleGrid.SelectedItem);
+ RuleGrid.UpdateLayout();
+
+ // Prefer focusing a real cell (more reliable than focusing the DataGrid itself).
+ var row = RuleGrid.ItemContainerGenerator.ContainerFromIndex(RuleGrid.SelectedIndex) as DataGridRow;
+ if (row != null)
+ {
+ // Pick a sensible column (0 = expander button column; 1 = "Name" in your grid).
+ var cell = GetCell(RuleGrid, row, column: 1) ?? GetCell(RuleGrid, row, column: 0);
+ if (cell != null)
+ {
+ cell.Focus();
+ Keyboard.Focus(cell);
+ return;
+ }
+ }
+
+ RuleGrid.Focus();
+ Keyboard.Focus(RuleGrid);
+ }), DispatcherPriority.Input);
+ }
+
+ ///
+ /// Delegate the refocusing to on sorting,
+ /// but with another dispatcher context.
+ ///
+ ///
+ ///
+ private void RuleGrid_OnSorting(object sender, DataGridSortingEventArgs e)
+ {
+ Dispatcher.BeginInvoke(new Action(RestoreRuleGridFocus), DispatcherPriority.ContextIdle);
+ }
+ #endregion
+
+}
\ No newline at end of file
diff --git a/Source/NETworkManager/NETworkManager.csproj b/Source/NETworkManager/NETworkManager.csproj
index 865ff0fab4..df322fba84 100644
--- a/Source/NETworkManager/NETworkManager.csproj
+++ b/Source/NETworkManager/NETworkManager.csproj
@@ -82,6 +82,9 @@
$(TargetDir)\lib\MSTSCLib.dll
+
+ FirewallRuleGrid.xaml
+
@@ -92,6 +95,7 @@
+
@@ -141,6 +145,11 @@
WpfDesigner
+
+ MSBuild:Compile
+ Wpf
+ Designer
+
diff --git a/Source/NETworkManager/Properties/Resources.Designer.cs b/Source/NETworkManager/Properties/Resources.Designer.cs
index 79d39ad2e6..694724d834 100644
--- a/Source/NETworkManager/Properties/Resources.Designer.cs
+++ b/Source/NETworkManager/Properties/Resources.Designer.cs
@@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -70,7 +69,7 @@ public static string MicrosoftEdgeWebView2Runtime_DownloadUrl {
}
///
- /// Looks up a localized string similar to https://github.com/BornToBeRoot/NETworkManager/blob/main/CONTRIBUTORS.
+ /// Looks up a localized string similar to https://github.com/BornToBeRoot/NETworkManager/blob/main/CONTRIBUTORS.md.
///
public static string NETworkManager_Contributors {
get {
diff --git a/Source/NETworkManager/Resources/Styles/CheckBoxStyles.xaml b/Source/NETworkManager/Resources/Styles/CheckBoxStyles.xaml
index 3291de6f49..ef3d375aef 100644
--- a/Source/NETworkManager/Resources/Styles/CheckBoxStyles.xaml
+++ b/Source/NETworkManager/Resources/Styles/CheckBoxStyles.xaml
@@ -4,7 +4,7 @@
diff --git a/Source/NETworkManager/Resources/Templates/ValidationErrorTemplates.xaml b/Source/NETworkManager/Resources/Templates/ValidationErrorTemplates.xaml
index c6bcbd4ac4..9cced1ae93 100644
--- a/Source/NETworkManager/Resources/Templates/ValidationErrorTemplates.xaml
+++ b/Source/NETworkManager/Resources/Templates/ValidationErrorTemplates.xaml
@@ -17,19 +17,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs b/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs
new file mode 100644
index 0000000000..e791497299
--- /dev/null
+++ b/Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs
@@ -0,0 +1,1038 @@
+using System.IO;
+
+namespace NETworkManager.ViewModels;
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using System.Windows.Data;
+using System.Windows.Forms;
+using System.Windows.Input;
+using Localization.Resources;
+using Models.Firewall;
+using NW = Models.Network;
+using NETworkManager.Interfaces.ViewModels;
+using Settings;
+using Utilities;
+
+///
+/// ViewModel for a firewall rule
+///
+public class FirewallRuleViewModel : ViewModelBase, ICloneable, IFirewallRuleViewModel
+{
+ #region Variables
+
+ ///
+ /// Reflected access to converter.
+ ///
+ private static IValueConverter PortRangeToPortSpecificationConverter
+ {
+ get
+ {
+ if (field is not null) return field;
+ var type = Type.GetType(
+ "NETworkManager.Converters.PortRangeToPortSpecificationConverter, NETworkManager.Converters");
+
+ if (type is null) return field;
+ var ctor = Expression.New(type);
+ var lambda = Expression.Lambda>(ctor);
+ field = lambda.Compile().Invoke();
+
+ return field;
+ }
+ }
+
+ ///
+ /// Represents the underlying firewall rule associated with the configuration.
+ ///
+ [NotNull]
+ private readonly FirewallRule _rule = new();
+
+ public bool NameHasError
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasError));
+ }
+ }
+
+ public bool DescriptionHasError
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasError));
+ }
+ }
+
+ public bool ProgramHasError
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasError));
+ }
+ }
+
+ private bool ProfilesHaveError
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasError));
+ }
+ }
+
+ public bool HasError => NameHasError || DescriptionHasError || ProgramHasError || ProfilesHaveError;
+
+ ///
+ /// Represents the name or identifier associated with an entity or object.
+ ///
+ public string Name
+ {
+ get => _rule.Name;
+ set
+ {
+ if (value == _rule.Name)
+ return;
+ _rule.Name = value;
+ ValidateName();
+ OnPropertyChanged();
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Optionally describe the firewall rule with this property
+ ///
+ public string Description
+ {
+ get => _rule.Description;
+ set
+ {
+ if (value == _rule.Description)
+ return;
+ _rule.Description = value;
+ OnRuleChangedEvent();
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Name of the currently loaded profile
+ ///
+ public string ProfileName
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ UpdateRuleName();
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(MaxLengthName));
+ OnPropertyChanged(nameof(UserDefinedName));
+ OnRuleChangedEvent();
+ }
+ }
+
+ private string _userDefineName;
+ ///
+ /// Name override for the firewall DisplayName
+ ///
+ public string UserDefinedName
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ if (value?.Length <= MaxLengthName)
+ _userDefineName = value;
+ ValidateName();
+ OnPropertyChanged();
+ UpdateRuleName();
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Max length of the firewall DisplayName
+ ///
+ public int MaxLengthName =>
+ 9999 - "NwM__".Length - ProfileName?.Length ??
+ 9999 - "NwM__".Length - "Default".Length;
+
+ ///
+ /// Default name shown in the field watermark and used, if no is provided.
+ ///
+ public string DefaultName
+ {
+ get;
+ private set
+ {
+ if (field == value)
+ return;
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Firewall protocol to apply the rule to.
+ ///
+ public FirewallProtocol Protocol
+ {
+ get => _rule.Protocol;
+ set
+ {
+ if (value == _rule.Protocol)
+ return;
+ _rule.Protocol = value;
+ UpdateRuleName();
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(PortsEnabled));
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Specifies the direction of traffic flow for the rule.
+ ///
+ public FirewallRuleDirection Direction
+ {
+ get => _rule.Direction;
+ set
+ {
+ if (value == _rule.Direction)
+ return;
+ _rule.Direction = value;
+ UpdateRuleName();
+ OnPropertyChanged();
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Program for which the rule applies.
+ ///
+ public FirewallRuleProgram Program
+ {
+ get => _rule.Program;
+ set
+ {
+ if (value == _rule.Program)
+ return;
+ _rule.Program = value;
+ ValidateProgramPath();
+ UpdateRuleName();
+ OnPropertyChanged();
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Binding field for the port input fields to be activated.
+ ///
+ public bool PortsEnabled => Protocol is FirewallProtocol.TCP or FirewallProtocol.UDP;
+
+ ///
+ /// Specifies the local ports that the rule applies to.
+ ///
+ public List LocalPorts
+ {
+ get => _rule.LocalPorts;
+ set
+ {
+ _rule.LocalPorts = value;
+ UpdateRuleName();
+ OnPropertyChanged();
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Index of the history combobox.
+ ///
+ public int LocalPortsIndex
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ }
+ } = -1;
+
+ ///
+ /// Specifies the remote ports that the rule applies to.
+ ///
+ public List RemotePorts
+ {
+ get => _rule.RemotePorts;
+ set
+ {
+ _rule.RemotePorts = value;
+ UpdateRuleName();
+ OnPropertyChanged();
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Index of the history combobox.
+ ///
+ public int RemotePortsIndex
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ }
+ } = -1;
+
+ ///
+ /// View for history combobox.
+ ///
+ public ICollectionView LocalPortsHistoryView
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ }
+
+ }
+
+ ///
+ /// View for history combobox.
+ ///
+ public ICollectionView RemotePortsHistoryView
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Local port history.
+ ///
+ public static ObservableCollection LocalPortsHistory
+ {
+ get;
+ set;
+ } = [];
+
+ ///
+ /// Remote port history.
+ ///
+ public static ObservableCollection RemotePortsHistory
+ {
+ get;
+ set;
+ } = [];
+
+ ///
+ /// View for the combination of and history.
+ ///
+ public static ObservableCollection CombinedPortsHistory
+ {
+ get;
+ } = [];
+
+ private string _lastLocalPortValue = string.Empty;
+ private string _lastRemotePortValue = string.Empty;
+ private readonly bool _isInit;
+
+ ///
+ /// Watermark for the port input fields.
+ ///
+ public string PortWatermark
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Checkbox for the domain network profile.
+ ///
+ public bool NetworkProfileDomain
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ if (_isInit)
+ return;
+ // Temporarily apply the change to a copy to check validity
+ var newProfiles = _rule.NetworkProfiles.ToArray();
+ newProfiles[(int)NW.NetworkProfiles.Domain] = value;
+
+ if (newProfiles.Any(x => x))
+ {
+ NetworkProfiles[(int)NW.NetworkProfiles.Domain] = value;
+ NetworkProfiles[(int)NW.NetworkProfiles.Private] = NetworkProfilePrivate;
+ NetworkProfiles[(int)NW.NetworkProfiles.Public] = NetworkProfilePublic;
+ ProfilesHaveError = false;
+ UpdateRuleName();
+ OnRuleChangedEvent();
+ }
+ else
+ {
+ ProfilesHaveError = true;
+ }
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(NetworkProfilePrivate));
+ OnPropertyChanged(nameof(NetworkProfilePublic));
+ OnPropertyChanged(nameof(NetworkProfiles));
+ }
+ }
+
+ ///
+ /// Checkbox for the private network profile.
+ ///
+ public bool NetworkProfilePrivate
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ if (_isInit)
+ return;
+ var newProfiles = _rule.NetworkProfiles.ToArray();
+ newProfiles[(int)NW.NetworkProfiles.Private] = value;
+
+ if (newProfiles.Any(x => x))
+ {
+ NetworkProfiles[(int)NW.NetworkProfiles.Domain] = NetworkProfileDomain;
+ NetworkProfiles[(int)NW.NetworkProfiles.Private] = value;
+ NetworkProfiles[(int)NW.NetworkProfiles.Public] = NetworkProfilePublic;
+ ProfilesHaveError = false;
+ UpdateRuleName();
+ OnRuleChangedEvent();
+ }
+ else
+ {
+ ProfilesHaveError = true;
+ }
+
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(NetworkProfileDomain));
+ OnPropertyChanged(nameof(NetworkProfilePublic));
+ OnPropertyChanged(nameof(NetworkProfiles));
+ }
+ }
+
+ ///
+ /// Checkbox for the public network profile.
+ ///
+ public bool NetworkProfilePublic
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ if (_isInit)
+ return;
+ var newProfiles = _rule.NetworkProfiles.ToArray();
+ newProfiles[(int)NW.NetworkProfiles.Public] = value;
+
+ if (newProfiles.Any(x => x))
+ {
+ NetworkProfiles[(int)NW.NetworkProfiles.Domain] = NetworkProfileDomain;
+ NetworkProfiles[(int)NW.NetworkProfiles.Private] = NetworkProfilePrivate;
+ NetworkProfiles[(int)NW.NetworkProfiles.Public] = value;
+ ProfilesHaveError = false;
+ UpdateRuleName();
+ OnRuleChangedEvent();
+ }
+ else
+ {
+ ProfilesHaveError = true;
+ }
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(NetworkProfileDomain));
+ OnPropertyChanged(nameof(NetworkProfilePrivate));
+ OnPropertyChanged(nameof(NetworkProfiles));
+ }
+ }
+
+ ///
+ /// Combination of all checkboxes for network profiles.
+ ///
+ public bool[] NetworkProfiles
+ {
+ get => _rule.NetworkProfiles;
+ init
+ {
+ if (value == _rule.NetworkProfiles)
+ return;
+ _isInit = true;
+ _rule.NetworkProfiles = value;
+ NetworkProfileDomain = value[(int)NW.NetworkProfiles.Domain];
+ NetworkProfilePrivate = value[(int)NW.NetworkProfiles.Private];
+ NetworkProfilePublic = value[(int)NW.NetworkProfiles.Public];
+ _isInit = false;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(NetworkProfileDomain));
+ OnPropertyChanged(nameof(NetworkProfilePrivate));
+ OnPropertyChanged(nameof(NetworkProfilePublic));
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Interface type filter for the firewall rule.
+ ///
+ public FirewallInterfaceType InterfaceType
+ {
+ get => _rule.InterfaceType;
+ set
+ {
+ if (value == _rule.InterfaceType)
+ return;
+ _rule.InterfaceType = value;
+ OnPropertyChanged();
+ UpdateRuleName();
+ OnRuleChangedEvent();
+ }
+ }
+
+ ///
+ /// Action to execute when the rule is applied.
+ ///
+ public FirewallRuleAction Action
+ {
+ get => _rule.Action;
+ set
+ {
+ if (value == _rule.Action)
+ return;
+ _rule.Action = value;
+ UpdateRuleName();
+ OnPropertyChanged();
+ OnRuleChangedEvent();
+ }
+ }
+
+ public int MaxLengthHistory
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// Represents a view model for a firewall rule that provides details
+ /// about the rule's configuration and state for user interface bindings.
+ ///
+ public FirewallRuleViewModel()
+ {
+ NetworkProfiles = [true, true, true];
+ ProfileName = "Default";
+ PortWatermark = StaticStrings.ExamplePortScanRange;
+
+ // Set the collection views for port histories
+ LocalPortsHistoryView = CollectionViewSource.
+ GetDefaultView(LocalPortsHistory);
+ RemotePortsHistoryView = CollectionViewSource.
+ GetDefaultView(RemotePortsHistory);
+ }
+
+ ///
+ /// Construct a rule from a Firewall rule and a profile name.
+ ///
+ /// The rule to get data from.
+ /// The profile name to use.
+ public FirewallRuleViewModel(FirewallRule rule, string profileName = null) : this()
+ {
+ Direction = rule.Direction;
+ Protocol = rule.Protocol;
+ LocalPorts = rule.LocalPorts;
+ RemotePorts = rule.RemotePorts;
+ /*
+ if (SettingsManager.Current.Firewall_CombinePortHistory)
+ {
+ char separator = SettingsManager.Current.Firewall_UseWindowsPortSyntax ? ',' : ';';
+ LocalPortsIndex = CombinedPortsHistory.IndexOf(FirewallRule.PortsToString(LocalPorts, separator));
+ RemotePortsIndex = CombinedPortsHistory.IndexOf(FirewallRule.PortsToString(RemotePorts, separator));
+ }
+ */
+ Program = rule.Program;
+ Description = rule.Description;
+ Action = rule.Action;
+ InterfaceType = rule.InterfaceType;
+ NetworkProfiles = rule.NetworkProfiles;
+ ProfileName = profileName;
+ UpdateRuleName();
+ string ruleName = rule.Name.Substring("NwM_".Length);
+ ruleName = ruleName.Substring(0, ruleName.LastIndexOf('_'));
+ if (DefaultName != ruleName)
+ UserDefinedName = ruleName;
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// Updates the firewall rule's name based on the current configuration.
+ ///
+ private void UpdateRuleName()
+ {
+ StringBuilder resultBuilder = new();
+ if (!string.IsNullOrWhiteSpace(_userDefineName))
+ {
+ resultBuilder.Append(_userDefineName);
+ }
+ else
+ {
+ string nextToken;
+ var direction = Direction switch
+ {
+ FirewallRuleDirection.Inbound => "in",
+ FirewallRuleDirection.Outbound => "out",
+ _ => null
+ };
+ if (direction is not null)
+ resultBuilder.Append(direction);
+ resultBuilder.Append($"_{Protocol}");
+ if (Protocol is FirewallProtocol.TCP or FirewallProtocol.UDP)
+ {
+ if (LocalPorts?.Count is 0 && RemotePorts?.Count is 0)
+ {
+ resultBuilder.Append("_any");
+ }
+ else
+ {
+ char separator = ';';
+ if (LocalPorts?.Count > 0)
+ {
+ nextToken = $"_loc:{FirewallRule.PortsToString(LocalPorts, separator, false)}";
+ if (nextToken.Length > 20)
+ nextToken = $"{nextToken[..20]}...";
+ resultBuilder.Append(nextToken);
+ }
+
+ if (RemotePorts?.Count > 0)
+ {
+ nextToken = $"_rem:{FirewallRule.PortsToString(RemotePorts, separator, false)}";
+ if (nextToken.Length > 20)
+ nextToken = $"{nextToken[..20]}...";
+ resultBuilder.Append(nextToken);
+ }
+ }
+ }
+
+ if (!string.IsNullOrEmpty(Program?.Executable?.Name))
+ {
+ nextToken = $"_{Program.Executable.Name}";
+ if (nextToken.Length > 30)
+ nextToken = $"{nextToken[..30]}...";
+ resultBuilder.Append(nextToken);
+ }
+
+ if (NetworkProfiles.Any(x => x))
+ {
+ resultBuilder.Append('_');
+ if (NetworkProfiles.All(x => x))
+ {
+ resultBuilder.Append("all");
+ }
+ else
+ {
+ if (NetworkProfiles[(int)NW.NetworkProfiles.Domain])
+ resultBuilder.Append("dom,");
+ if (NetworkProfiles[(int)NW.NetworkProfiles.Private])
+ resultBuilder.Append("prv,");
+ if (NetworkProfiles[(int)NW.NetworkProfiles.Public])
+ resultBuilder.Append("pub");
+ if (resultBuilder[^1] == ',')
+ resultBuilder.Remove(resultBuilder.Length - 1, 1);
+ }
+ }
+ string type = InterfaceType switch
+ {
+ FirewallInterfaceType.RemoteAccess => "vpn",
+ FirewallInterfaceType.Wired => "wire",
+ FirewallInterfaceType.Wireless => "wifi",
+ _ => null
+ };
+ if (type is not null)
+ resultBuilder.Append($"_if:{type}");
+ string action = Action switch
+ {
+ FirewallRuleAction.Allow => "acc",
+ FirewallRuleAction.Block => "blk",
+ _ => null
+ };
+ if (action is not null)
+ resultBuilder.Append($"_{action}");
+ }
+
+ string defaultName = resultBuilder.ToString();
+ if (defaultName.Length > MaxLengthName)
+ defaultName = $"{defaultName[..(MaxLengthName - 3)]}...";
+ Name = $"NwM_{defaultName}_{ProfileName ?? "Default"}";
+ DefaultName = defaultName;
+ }
+
+ ///
+ /// Sets the error state while the view is not in the VisualTree, which can happen
+ /// in the ProfileChildWindow until the tab has been opened.
+ ///
+ private void ValidateName()
+ {
+ if (string.IsNullOrEmpty(UserDefinedName) || UserDefinedName.Length <= MaxLengthName)
+ {
+ NameHasError = false;
+ return;
+ }
+
+ NameHasError = true;
+ }
+
+ private void ValidateProgramPath()
+ {
+ if (Program?.Executable?.FullName is not { } strValue)
+ {
+ ProgramHasError = false;
+ return;
+ }
+ ProgramHasError = !File.Exists(strValue);
+ }
+
+ /// Converts the current instance of the FirewallRuleViewModel to a FirewallRule object.
+ ///
+ /// A object representing the current instance.
+ ///
+ public FirewallRule ToRule(bool toLoadOrSave = false)
+ {
+ ValidateProgramPath();
+ ValidateName();
+ return HasError && !toLoadOrSave ? null : _rule;
+ }
+
+ ///
+ /// Retrieves the localized translation for a given enumeration value.
+ ///
+ /// The enumeration type to translate.
+ /// The localized string corresponding to the provided enumeration value.
+ public static string[] GetEnumTranslation(Type enumType)
+ {
+ if (!enumType.IsEnum)
+ return null;
+
+ var enumStrings = Enum.GetNames(enumType);
+ var transStrings = new string[enumStrings.Length];
+ for (int i = 0; i < enumStrings.Length; i++)
+ transStrings[i] = Strings.ResourceManager.GetString(enumStrings[i], Strings.Culture) ?? enumStrings[i];
+
+ return transStrings;
+ }
+
+ ///
+ /// Store the current port values to spare them from deletion on collection clearing.
+ ///
+ public void StorePortValues()
+ {
+ // Store original port values
+ var converter = PortRangeToPortSpecificationConverter;
+ _lastLocalPortValue = converter.ConvertBack(LocalPorts, typeof(string), null, null) as string;
+ _lastRemotePortValue = converter.ConvertBack(RemotePorts, typeof(string), null, null) as string;
+ }
+
+ ///
+ /// Restore the port values after the history has been modified.
+ ///
+ public void RestorePortValues()
+ {
+ var converter = PortRangeToPortSpecificationConverter;
+ // Restore the original field values
+ if (!string.IsNullOrWhiteSpace(_lastLocalPortValue))
+ {
+ // Find appropriate index
+ int tmpLocalIndex = LocalPortsHistory.IndexOf(_lastLocalPortValue);
+ // Restore field value
+ if (tmpLocalIndex != -1
+ && converter.Convert(_lastLocalPortValue, typeof(List),
+ null, null) is List convertedPorts)
+ LocalPorts = convertedPorts;
+ LocalPortsIndex = tmpLocalIndex;
+
+
+ }
+ // Reset stored value
+ _lastLocalPortValue = string.Empty;
+
+ // Same for remote ports
+ if (!string.IsNullOrWhiteSpace(_lastRemotePortValue))
+ {
+ int tmpRemoteIndex = RemotePortsHistory.IndexOf(_lastRemotePortValue);
+ if (tmpRemoteIndex != -1
+ && converter.Convert(_lastRemotePortValue, typeof(List),
+ null, null) is List convertedPorts)
+ RemotePorts = convertedPorts;
+ RemotePortsIndex = tmpRemoteIndex;
+ }
+ // Reset stored value
+ _lastRemotePortValue = string.Empty;
+ }
+
+ ///
+ /// Add ports to history.
+ ///
+ /// Port list to add.
+ /// Type of port history to add to.
+ public void AddPortsToHistory(string ports, FirewallPortLocation firewallPortType)
+ {
+ OnAddingPortsToHistoryEvent();
+ ObservableCollection portHistory;
+ switch (firewallPortType)
+ {
+ case FirewallPortLocation.LocalPorts:
+ portHistory = LocalPortsHistory;
+ break;
+ case FirewallPortLocation.RemotePorts:
+ portHistory = RemotePortsHistory;
+ break;
+ default:
+ return;
+ }
+
+ // Create the new list
+ var list = ListHelper.Modify(portHistory.ToList(), ports,
+ SettingsManager.Current.General_HistoryListEntries);
+
+ // Clear the old items
+ portHistory.Clear();
+
+ // Raise property changed again after the collection has been cleared
+ switch (firewallPortType)
+ {
+ case FirewallPortLocation.LocalPorts:
+ OnPropertyChanged(nameof(LocalPortsHistoryView));
+ break;
+ case FirewallPortLocation.RemotePorts:
+ OnPropertyChanged(nameof(RemotePortsHistoryView));
+ break;
+ }
+
+ // Fill with the new items
+ list.ForEach(x => portHistory.Add(x));
+
+ // Update history config
+ /*
+ switch (firewallPortType)
+ {
+ case FirewallPortLocation.LocalPorts:
+ LocalPortsHistory = portHistory;
+ SettingsManager.Current.Firewall_LocalPortsHistoryConfig = list;
+ FirewallSettingsViewModel.Instance.LocalPortsHaveItems = true;
+ break;
+ case FirewallPortLocation.RemotePorts:
+ RemotePortsHistory = portHistory;
+ SettingsManager.Current.Firewall_RemotePortsHistoryConfig = list;
+ FirewallSettingsViewModel.Instance.RemotePortsHaveItems = true;
+ break;
+ }
+ */
+
+ // Update the combined history if configured
+
+ OnAddedPortsToHistoryEvent();
+ }
+
+ ///
+ /// Update or create the combined port history.
+ ///
+ public static void UpdateCombinedPortsHistory()
+ {
+ // This will as a side effect reset all unchanged combobox fields in all rules, because its source is empty
+ // StorePorts() and RestorePorts() are required to circumvent this.
+ CombinedPortsHistory.Clear();
+
+ // Refill the combined history alternating between local and remote fields when possible
+ int count = 0;
+ int indexLocal = 0;
+ int indexRemote = 0;
+ bool swap = false;
+ var localPorts = LocalPortsHistory;
+ var remotePorts = RemotePortsHistory;
+ if (localPorts is null | remotePorts is null)
+ return;
+ while (count < SettingsManager.Current.General_HistoryListEntries)
+ {
+ if (indexLocal >= localPorts.Count
+ && indexRemote >= remotePorts.Count)
+ break;
+ if (indexLocal < localPorts.Count && (!swap || indexRemote >= remotePorts.Count))
+ {
+ // Avoid duplicates
+ if (CombinedPortsHistory.Contains(localPorts[indexLocal++]))
+ continue;
+ CombinedPortsHistory.Add(localPorts[indexLocal - 1]);
+ swap = true;
+ count++;
+ continue;
+ }
+ if (indexRemote < remotePorts.Count)
+ {
+ if (CombinedPortsHistory.Contains(remotePorts[indexRemote++]))
+ continue;
+ CombinedPortsHistory.Add(remotePorts[indexRemote - 1]);
+ count++;
+ swap = false;
+ }
+ }
+ }
+
+ ///
+ /// Command for selecting a program.
+ ///
+ public ICommand SelectProgramCommand => new RelayCommand(_ => SelectProgramAction());
+
+ ///
+ /// Select the program using a file dialog.
+ ///
+ private void SelectProgramAction()
+ {
+ var openFileDialog = new OpenFileDialog();
+
+ var fileExtension = "exe";
+
+ openFileDialog.Filter = $@"{Strings.Program} | *.{fileExtension}";
+
+ if (openFileDialog.ShowDialog() == DialogResult.OK)
+ Program = new FirewallRuleProgram(openFileDialog.FileName);
+ }
+
+ ///
+ /// Clone this instance.
+ ///
+ /// Cloned instance.
+ public object Clone()
+ {
+ var clone = new FirewallRuleViewModel
+ {
+ Name = new string(Name ?? string.Empty),
+ Program = Program?.Clone() as FirewallRuleProgram,
+ NetworkProfileDomain = NetworkProfileDomain,
+ NetworkProfilePrivate = NetworkProfilePrivate,
+ NetworkProfilePublic = NetworkProfilePublic,
+ DefaultName = new string(DefaultName ?? string.Empty),
+ Action = Action,
+ InterfaceType = InterfaceType,
+ Protocol = Protocol,
+ Direction = Direction,
+ LocalPorts = LocalPorts?.ToList(),
+ RemotePorts = RemotePorts?.ToList(),
+ NetworkProfiles = NetworkProfiles.ToArray(),
+ PortWatermark = new string(PortWatermark ?? string.Empty),
+ Description = new string(Description ?? string.Empty),
+ UserDefinedName = new string(UserDefinedName ?? string.Empty),
+ LocalPortsIndex = LocalPortsIndex,
+ RemotePortsIndex = RemotePortsIndex,
+ };
+ UpdateCombinedPortsHistory();
+ return clone;
+ }
+ #endregion
+
+ #region Events
+
+ ///
+ /// Event when ports are added to history.
+ ///
+ public event EventHandler OnAddingPortsToHistory;
+
+ ///
+ /// Fire event.
+ ///
+ private void OnAddingPortsToHistoryEvent()
+ {
+ OnAddingPortsToHistory?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Event after ports have been added to history.
+ ///
+ public event EventHandler OnAddedPortsToHistory;
+
+ ///
+ /// Fire event.
+ ///
+ private void OnAddedPortsToHistoryEvent()
+ {
+ OnAddedPortsToHistory?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Event when the rule configuration has changed.
+ ///
+ public event EventHandler OnRuleChanged;
+
+ ///
+ /// Fire event.
+ ///
+ private void OnRuleChangedEvent()
+ {
+ OnRuleChanged?.Invoke(this, EventArgs.Empty);
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/Source/NETworkManager/Views/NetworkInterfaceView.xaml b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
index d65bc0e61a..5f274ec649 100644
--- a/Source/NETworkManager/Views/NetworkInterfaceView.xaml
+++ b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
@@ -103,7 +103,7 @@
+ Foreground="{StaticResource MahApps.Brushes.Gray3}" />
@@ -728,7 +728,7 @@
+ Foreground="{StaticResource MahApps.Brushes.Gray3}" />
@@ -837,7 +837,7 @@
@@ -967,7 +967,7 @@
+ Foreground="{StaticResource MahApps.Brushes.Gray3}" />
@@ -1049,10 +1049,13 @@
+ Style="{DynamicResource DefaultTextBlock}" Margin="0,10,0,0">
+
+
+
+
-
@@ -1495,7 +1498,7 @@
+ Visibility="{Binding Path=ProfileFilterTagsView.IsEmpty, Converter={StaticResource BooleanToVisibilityCollapsedConverter}}">
@@ -1514,7 +1517,7 @@
-
+
diff --git a/Source/NETworkManager/Views/ProfileChildWindow.xaml b/Source/NETworkManager/Views/ProfileChildWindow.xaml
index 151444aa27..9cfea4f4f3 100644
--- a/Source/NETworkManager/Views/ProfileChildWindow.xaml
+++ b/Source/NETworkManager/Views/ProfileChildWindow.xaml
@@ -14,11 +14,13 @@
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
+ xmlns:typedProxies="clr-namespace:NETworkManager.Utilities.WPF.TypedBindingProxies;assembly=NETworkManager.Utilities.WPF"
xmlns:interactivity="http://schemas.microsoft.com/xaml/behaviors"
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
xmlns:profiles="clr-namespace:NETworkManager.Profiles;assembly=NETworkManager.Profiles"
+ xmlns:controls="clr-namespace:NETworkManager.Controls"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
- xmlns:simpleChildWindow="clr-namespace:MahApps.Metro.SimpleChildWindow;assembly=MahApps.Metro.SimpleChildWindow"
+ xmlns:simpleChildWindow="clr-namespace:MahApps.Metro.SimpleChildWindow;assembly=MahApps.Metro.SimpleChildWindow"
CloseButtonCommand="{Binding CancelCommand}"
Style="{StaticResource DefaultChildWindow}"
Loaded="ChildWindow_OnLoaded"
@@ -42,6 +44,8 @@
+
+
@@ -60,6 +64,7 @@
+
@@ -771,7 +776,7 @@
-
+
@@ -1008,6 +1013,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1138,7 +1190,7 @@
Value="{x:Static profiles:ProfileName.PuTTY}">
-
+
+
+
+
-
-
+
+
@@ -2218,7 +2277,7 @@
Value="{Binding RemoteDesktop_AuthenticationLevel}" Maximum="3"
Minimum="0" Interval="1"
IsEnabled="{Binding RemoteDesktop_OverrideAuthenticationLevel}" />
-
@@ -2236,7 +2295,7 @@
-
@@ -3127,7 +3186,7 @@
-
+
@@ -3759,6 +3818,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -4341,7 +4484,7 @@
-
+
@@ -4475,49 +4618,62 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
+
-
-
+
+
diff --git a/Source/NETworkManager/Views/ProfileChildWindow.xaml.cs b/Source/NETworkManager/Views/ProfileChildWindow.xaml.cs
index aa19c9d22f..12505fb2de 100644
--- a/Source/NETworkManager/Views/ProfileChildWindow.xaml.cs
+++ b/Source/NETworkManager/Views/ProfileChildWindow.xaml.cs
@@ -16,8 +16,6 @@ public ProfileChildWindow(Window parentWindow)
InitializeComponent();
// Set the width and height of the child window based on the parent window size
- ChildWindowMaxWidth = 1050;
- ChildWindowMaxHeight = 650;
ChildWindowWidth = parentWindow.ActualWidth * 0.85;
ChildWindowHeight = parentWindow.ActualHeight * 0.85;
@@ -28,7 +26,7 @@ public ProfileChildWindow(Window parentWindow)
ChildWindowHeight = parentWindow.ActualHeight * 0.85;
};
}
-
+
private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e)
{
Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate
@@ -54,4 +52,4 @@ private void ScrollViewer_ManipulationBoundaryFeedback(object sender, Manipulati
{
e.Handled = true;
}
-}
\ No newline at end of file
+}
diff --git a/Source/NETworkManager/Views/ProfilesView.xaml b/Source/NETworkManager/Views/ProfilesView.xaml
index b6e4283a3a..90f15266cd 100644
--- a/Source/NETworkManager/Views/ProfilesView.xaml
+++ b/Source/NETworkManager/Views/ProfilesView.xaml
@@ -414,6 +414,10 @@
Header="{x:Static localization:Strings.WebConsole}"
Binding="{Binding (profiles:ProfileInfo.WebConsole_Enabled)}"
SortMemberPath="WebConsole_Enabled" MinWidth="80" Width="Auto" />
+
-
+
@@ -443,7 +447,7 @@
-
+
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 0687410c26..c00a302a01 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -7,7 +7,7 @@ keywords: [NETworkManager, changelog, release notes, next release, upcoming feat
# Next Release
Version: **Next release**
-Release date: **xx.xx.2025**
+Release date: **xx.xx.2026**
| File | `SHA256` |
| ---- | -------- |
@@ -21,6 +21,10 @@ Release date: **xx.xx.2025**
## What's new?
+**Firewall**
+
+- Firewall application has been added for adding NETworkManager controlled Windows firewall rules with profile support. Special thanks to [@labre-rdc](https://github.com/labre-rdc) [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+
**PowerShell**
- DPI scaling is now applied correctly when NETworkManager is moved to a monitor with a different DPI scaling factor. The embedded PowerShell (conhost) window now rescales its font automatically using the Windows Console API (`AttachConsole` + `SetCurrentConsoleFontEx`), bypassing the OS limitation that prevents `WM_DPICHANGED` from being forwarded to cross-process child windows. [#3352](https://github.com/BornToBeRoot/NETworkManager/pull/3352)
@@ -31,10 +35,20 @@ Release date: **xx.xx.2025**
## Improvements
-- Redesign Status Window to make it more compact [#3359](https://github.com/BornToBeRoot/NETworkManager/pull/3359)
+- Reuse existing validators and converters where applicable. [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+- Support commands exceeding the commandline limit in PowershellHelper. [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+- Fix warnings in NetworkInterfaceView. [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+- Add various converters and validators [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+- Allow to click validation errors out of the way. [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+- Add validation error template on checkboxes. [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+- Allow style changes when ViewModels recognize configuration errors. [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
## Bug Fixes
+**General**
+
+- Fix null dereferences in various validators and converters. [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+
**PowerShell**
- Fixed incorrect initial embedded window size on high-DPI monitors. The `WindowsFormsHost` panel now sets its initial dimensions in physical pixels using the current DPI scale factor, ensuring the PowerShell window fills the panel correctly at startup. [#3352](https://github.com/BornToBeRoot/NETworkManager/pull/3352)
@@ -52,3 +66,5 @@ Release date: **xx.xx.2025**
- Code cleanup & refactoring
- Language files updated via [#transifex](https://github.com/BornToBeRoot/NETworkManager/pulls?q=author%3Aapp%2Ftransifex-integration)
- Dependencies updated via [#dependabot](https://github.com/BornToBeRoot/NETworkManager/pulls?q=author%3Aapp%2Fdependabot)
+- Add code documentation in various places. [#3353](https://github.com/BornToBeRoot/NETworkManager/pull/3353)
+- Refactor ListHelper.Modify as generic method.