diff --git a/Source/NETworkManager.Converters/ProfileGroupToTranslatedStringConverter.cs b/Source/NETworkManager.Converters/ProfileGroupToTranslatedStringConverter.cs index 63b684a9cf..91fe342144 100644 --- a/Source/NETworkManager.Converters/ProfileGroupToTranslatedStringConverter.cs +++ b/Source/NETworkManager.Converters/ProfileGroupToTranslatedStringConverter.cs @@ -23,7 +23,8 @@ public object Convert(object value, Type targetType, object parameter, CultureIn { return value is not GroupViewName name ? "-/-" - : ResourceTranslator.Translate(ResourceIdentifier.ProfileGroup, name); + : ResourceTranslator.Translate(new[] { ResourceIdentifier.ProfileGroup, ResourceIdentifier.ApplicationName }, + name); } /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 94b162c792..f4e3005577 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -951,6 +951,15 @@ public static string ApplicationName_DNSLookup { } } + /// + /// Looks up a localized string similar to Firewall. + /// + public static string ApplicationName_Firewall { + get { + return ResourceManager.GetString("ApplicationName_Firewall", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hosts File Editor. /// @@ -3686,7 +3695,7 @@ public static string DNSServer { } /// - /// Looks up a localized string similar to A DNS server of this profile was not found, the DNS servers saved with the profile are used. DNS servers are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile again (edit the profile).. + /// Looks up a localized string similar to A DNS server of this profile or its group was not found, the DNS servers saved with the profile or group are used. DNS servers are saved in the settings and the profile or group refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile or group again (edit it).. /// public static string DNSServerOfProfileNotFoundMessage { get { @@ -4697,6 +4706,15 @@ public static string ErrorMessage_PortProfileNameInvalid { } } + /// + /// Looks up a localized string similar to PTR records can only be looked up for IP addresses.. + /// + public static string ErrorMessage_PTRRequiresIPAddresses { + get { + return ResourceManager.GetString("ErrorMessage_PTRRequiresIPAddresses", resourceCulture); + } + } + /// /// Looks up a localized string similar to The settings on this page contain errors. Correct them to be able to save.. /// @@ -5967,7 +5985,7 @@ public static string HelpMessage_PasswordNotDisplayedCanBeOverwritten { } /// - /// Looks up a localized string similar to Only hosts that answer a ping (ICMP echo, with the timeout and attempts above) are scanned. Faster for ranges with many unused IP addresses. Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned.. + /// Looks up a localized string similar to Only hosts that answer a ping (ICMP echo, with the attempts, timeout and buffer below) are scanned. Faster for ranges with many unused IP addresses. Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned.. /// public static string HelpMessage_PingHostsBeforeScan { get { @@ -9239,7 +9257,7 @@ public static string PortProfile { } /// - /// Looks up a localized string similar to A port profile of this profile was not found, the ports saved with the profile are used. Port profiles are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the port profile and then select it in the profile again (edit the profile).. + /// Looks up a localized string similar to A port profile of this profile or its group was not found, the ports saved with the profile or group are used. Port profiles are saved in the settings and the profile or group refers to them by an internal ID - if the profile file was shared from another computer, create the port profile and then select it in the profile or group again (edit it).. /// public static string PortProfileOfProfileNotFoundMessage { get { @@ -9653,6 +9671,15 @@ public static string PTRRecord { } } + /// + /// Looks up a localized string similar to PTR records can only be looked up for IP addresses, "{0}" is skipped.. + /// + public static string PTRRequiresIPAddressX { + get { + return ResourceManager.GetString("PTRRequiresIPAddressX", resourceCulture); + } + } + /// /// Looks up a localized string similar to Public. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 3fadda001b..4bee6b1c4b 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -5123,13 +5123,13 @@ Do you want to start the scan? The name must not contain ";" and must not be a port or port range. - A port profile of this profile was not found, the ports saved with the profile are used. Port profiles are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the port profile and then select it in the profile again (edit the profile). + A port profile of this profile or its group was not found, the ports saved with the profile or group are used. Port profiles are saved in the settings and the profile or group refers to them by an internal ID - if the profile file was shared from another computer, create the port profile and then select it in the profile or group again (edit it). Ping hosts before the scan - Only hosts that answer a ping (ICMP echo, with the timeout and attempts above) are scanned. Faster for ranges with many unused IP addresses. Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned. + Only hosts that answer a ping (ICMP echo, with the attempts, timeout and buffer below) are scanned. Faster for ranges with many unused IP addresses. Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned. {0} host(s) did not answer the ping and were not scanned. @@ -5141,6 +5141,15 @@ Do you want to start the scan? The name must not contain ";". - A DNS server of this profile was not found, the DNS servers saved with the profile are used. DNS servers are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile again (edit the profile). + A DNS server of this profile or its group was not found, the DNS servers saved with the profile or group are used. DNS servers are saved in the settings and the profile or group refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile or group again (edit it). + + + Firewall + + + PTR records can only be looked up for IP addresses, "{0}" is skipped. + + + PTR records can only be looked up for IP addresses. \ No newline at end of file diff --git a/Source/NETworkManager.Models/Network/DNSServer.cs b/Source/NETworkManager.Models/Network/DNSServer.cs index 034ca1ea08..2c38f410d8 100644 --- a/Source/NETworkManager.Models/Network/DNSServer.cs +++ b/Source/NETworkManager.Models/Network/DNSServer.cs @@ -153,6 +153,48 @@ public static bool TryDecodeReferences(string inputWithReferences, return NamedReferenceInput.TryDecodeReferences(inputWithReferences, profiles, out input); } + /// + /// Converts a DNS server input (e.g. of a profile or group) to the values that are saved: the servers (DNS + /// server profiles replaced by their servers, see , used if a DNS server + /// profile can't be found later, e.g. a profile file from another computer) and the input with references (see + /// ). + /// + /// DNS server input. + /// DNS server profiles. + /// Servers and DNS server input with references (null if it contains no DNS server profile). + public static (string DNSServer, string DNSServerInput) Encode(string input, + IEnumerable profiles) + { + var profileList = profiles.ToList(); + + return (ReplaceProfilesByServers(input?.Trim(), profileList), EncodeReferences(input, profileList)); + } + + /// + /// Converts the saved values (see ) back to the DNS server input: the DNS server profiles + /// with their current name (also after a rename), or the saved servers if a DNS server profile can't be found. + /// + /// Saved servers. + /// Saved DNS server input with references. + /// DNS server profiles. + /// True if a DNS server profile can't be found. + /// DNS server input. + public static string Decode(string dnsServer, string dnsServerInput, + IEnumerable profiles, out bool dnsServerNotFound) + { + dnsServerNotFound = false; + + if (string.IsNullOrEmpty(dnsServerInput)) + return dnsServer; + + if (TryDecodeReferences(dnsServerInput, profiles, out var input)) + return input; + + dnsServerNotFound = true; + + return dnsServer; + } + /// /// Replaces the names of DNS server profiles in a DNS server input by their servers (e.g. "Cloudflare; 9.9.9.9" /// -> "1.1.1.1:53; 1.0.0.1:53; 9.9.9.9"), so the input still works without the DNS server profiles (e.g. on another diff --git a/Source/NETworkManager.Models/Network/PortProfile.cs b/Source/NETworkManager.Models/Network/PortProfile.cs index bbc0297057..e94673cc06 100644 --- a/Source/NETworkManager.Models/Network/PortProfile.cs +++ b/Source/NETworkManager.Models/Network/PortProfile.cs @@ -104,6 +104,47 @@ public static bool TryDecodeReferences(string inputWithReferences, IEnumerable

+ /// Converts a ports input (e.g. of a profile or group) to the values that are saved: the ports (port profiles + /// replaced by their ports, used if a port profile can't be found later, e.g. a profile file from another + /// computer) and the input with references to the port profiles (see ). + ///

+ /// Ports input. + /// Port profiles. + /// Ports and ports input with references (null if it contains no port profile). + public static (string Ports, string PortsInput) Encode(string input, IEnumerable portProfiles) + { + var portProfileList = portProfiles.ToList(); + + return (TryResolvePorts(input, portProfileList, out var ports) ? ports : input?.Trim(), + EncodeReferences(input, portProfileList)); + } + + /// + /// Converts the saved values (see ) back to the ports input: the port profiles with their + /// current name (also after a rename), or the saved ports if a port profile can't be found. + /// + /// Saved ports. + /// Saved ports input with references. + /// Port profiles. + /// True if a port profile can't be found. + /// Ports input. + public static string Decode(string ports, string portsInput, IEnumerable portProfiles, + out bool portProfileNotFound) + { + portProfileNotFound = false; + + if (string.IsNullOrEmpty(portsInput)) + return ports; + + if (TryDecodeReferences(portsInput, portProfiles, out var input)) + return input; + + portProfileNotFound = true; + + return ports; + } + public static List GetDefaultList() { return new List diff --git a/Source/NETworkManager.Models/Network/PortScanner.cs b/Source/NETworkManager.Models/Network/PortScanner.cs index 0aa38b6cb4..72be0ce9a0 100644 --- a/Source/NETworkManager.Models/Network/PortScanner.cs +++ b/Source/NETworkManager.Models/Network/PortScanner.cs @@ -63,12 +63,6 @@ public PortScanner(PortScannerOptions options) private readonly InterlockedCounter _portsTimedOut = new(); private readonly InterlockedCounter _hostsSkipped = new(); - /// - /// Data of the ping before the scan (see ), like the - /// default buffer of the IP Scanner. - /// - private static readonly byte[] PingBuffer = new byte[32]; - // Closed and timed out ports per host, see GetClosedAndTimedOutPortsPerHost private readonly ConcurrentDictionary _portsPerHost = new(); @@ -399,8 +393,8 @@ await Parallel.ForEachAsync(hosts, pingParallelOptions, async (host, ct) => /// True if the host answered the ping. private async Task IsReachableAsync(IPAddress ipAddress, int portCount, CancellationToken cancellationToken) { - var pingInfo = await PingProbe.PingAsync(ipAddress, _options.Timeout, _options.Attempts, PingBuffer, - cancellationToken).ConfigureAwait(false); + var pingInfo = await PingProbe.PingAsync(ipAddress, _options.ICMPTimeout, _options.ICMPAttempts, + _options.ICMPBuffer, cancellationToken).ConfigureAwait(false); if (pingInfo.Status == IPStatus.Success) return true; diff --git a/Source/NETworkManager.Models/Network/PortScannerOptions.cs b/Source/NETworkManager.Models/Network/PortScannerOptions.cs index 3aefedf054..961e868fdb 100644 --- a/Source/NETworkManager.Models/Network/PortScannerOptions.cs +++ b/Source/NETworkManager.Models/Network/PortScannerOptions.cs @@ -32,11 +32,27 @@ public record PortScannerOptions public required int Attempts { get; init; } /// - /// Ping every host before its ports are scanned (with and ), a host - /// that doesn't answer is not scanned. + /// Ping every host before its ports are scanned (with , and + /// ), a host that doesn't answer is not scanned. /// public required bool PingHostsBeforeScan { get; init; } + /// + /// Number of pings before the scan if there was no answer (see ). + /// + public required int ICMPAttempts { get; init; } + + /// + /// Timeout in milliseconds after which a ping before the scan is considered lost (see + /// ). + /// + public required int ICMPTimeout { get; init; } + + /// + /// Data sent with the ping before the scan (see ). + /// + public required byte[] ICMPBuffer { get; init; } + /// /// Resolve the hostname for an IP address. /// diff --git a/Source/NETworkManager.Profiles/Application/DNSLookup.cs b/Source/NETworkManager.Profiles/Application/DNSLookup.cs new file mode 100644 index 0000000000..3dc2f3805d --- /dev/null +++ b/Source/NETworkManager.Profiles/Application/DNSLookup.cs @@ -0,0 +1,56 @@ +using DnsClient; +using NETworkManager.Models.Network; +using NETworkManager.Settings; +using NETworkManager.Utilities; + +namespace NETworkManager.Profiles.Application; + +public static class DNSLookup +{ + /// + /// Gets the DNS servers for a profile: its own DNS servers (if overridden), otherwise the DNS servers of its + /// group (if overridden), otherwise none (the DNS server selected in the DNS Lookup is used). DNS servers by name + /// with their current name (also after a rename); if a DNS server can't be found (deleted, or a profile file + /// shared from another computer - DNS servers are saved in the settings), the servers saved with the profile or + /// group. + /// + /// Profile to get the DNS servers for. + /// True if a DNS server can't be found. + /// DNS servers, null if neither the profile nor its group has DNS servers. + public static string GetDNSServer(ProfileInfo profile, out bool dnsServerNotFound) + { + // Get group info + var group = ProfileManager.GetGroupByName(profile.Group); + + var (dnsServer, dnsServerInput) = profile.DNSLookup_OverrideDNSServer + ? (profile.DNSLookup_DNSServer, profile.DNSLookup_DNSServerInput) + : group.DNSLookup_OverrideDNSServer + ? (group.DNSLookup_DNSServer, group.DNSLookup_DNSServerInput) + : (null, null); + + if (string.IsNullOrWhiteSpace(dnsServer)) + { + dnsServerNotFound = false; + + return null; + } + + return DNSServer.Decode(dnsServer, dnsServerInput, SettingsManager.Current.DNSLookup_DNSServers, + out dnsServerNotFound); + } + + /// + /// Gets the query type for a profile: its own type (if overridden), otherwise PTR for IP addresses and A for + /// hostnames. + /// + /// Profile to get the query type for. + /// Query type. + public static QueryType GetQueryType(ProfileInfo profile) + { + return profile.DNSLookup_OverrideQueryType + ? profile.DNSLookup_QueryType + : IPAddressHelper.AreIPAddresses(profile.DNSLookup_Host) + ? QueryType.PTR + : QueryType.A; + } +} diff --git a/Source/NETworkManager.Profiles/Application/PortScanner.cs b/Source/NETworkManager.Profiles/Application/PortScanner.cs new file mode 100644 index 0000000000..24380f052a --- /dev/null +++ b/Source/NETworkManager.Profiles/Application/PortScanner.cs @@ -0,0 +1,31 @@ +using NETworkManager.Models.Network; +using NETworkManager.Settings; + +namespace NETworkManager.Profiles.Application; + +public static class PortScanner +{ + /// + /// Gets the ports to scan for a profile: its own ports (if overridden), otherwise the ports of its group (if + /// overridden), otherwise none. Port profiles with their current name (also after a rename); if a port profile + /// can't be found (deleted, or a profile file shared from another computer - port profiles are saved in the + /// settings), the ports saved with the profile or group. + /// + /// Profile to get the ports for. + /// True if a port profile can't be found. + /// Ports to scan, empty if neither the profile nor its group has ports. + public static string GetPorts(ProfileInfo profile, out bool portProfileNotFound) + { + // Get group info + var group = ProfileManager.GetGroupByName(profile.Group); + + var (ports, portsInput) = profile.PortScanner_OverridePorts + ? (profile.PortScanner_Ports, profile.PortScanner_PortsInput) + : group.PortScanner_OverridePorts + ? (group.PortScanner_Ports, group.PortScanner_PortsInput) + : (string.Empty, null); + + return PortProfile.Decode(ports, portsInput, SettingsManager.Current.PortScanner_PortProfiles, + out portProfileNotFound); + } +} diff --git a/Source/NETworkManager.Profiles/GroupInfo.cs b/Source/NETworkManager.Profiles/GroupInfo.cs index 4842bcccc2..1f9289df58 100644 --- a/Source/NETworkManager.Profiles/GroupInfo.cs +++ b/Source/NETworkManager.Profiles/GroupInfo.cs @@ -41,6 +41,16 @@ public GroupInfo(GroupInfo group) : this(group.Name) // Profiles Profiles = group.Profiles; + // Port Scanner + PortScanner_OverridePorts = group.PortScanner_OverridePorts; + PortScanner_Ports = group.PortScanner_Ports; + PortScanner_PortsInput = group.PortScanner_PortsInput; + + // DNS Lookup + DNSLookup_OverrideDNSServer = group.DNSLookup_OverrideDNSServer; + DNSLookup_DNSServer = group.DNSLookup_DNSServer; + DNSLookup_DNSServerInput = group.DNSLookup_DNSServerInput; + // Remote Desktop RemoteDesktop_UseCredentials = group.RemoteDesktop_UseCredentials; RemoteDesktop_Username = group.RemoteDesktop_Username; @@ -175,6 +185,32 @@ public GroupInfo(GroupInfo group) : this(group.Name) [XmlIgnore] public List Profiles { get; set; } + public bool PortScanner_OverridePorts { get; set; } + + /// + /// Ports to scan (ports and port ranges) for the profiles of the group, see + /// . + /// + public string PortScanner_Ports { get; set; } + + /// + /// Ports as entered, if they contain port profiles, see . + /// + public string PortScanner_PortsInput { get; set; } + + public bool DNSLookup_OverrideDNSServer { get; set; } + + /// + /// DNS servers for the profiles of the group, see . + /// + public string DNSLookup_DNSServer { get; set; } + + /// + /// DNS servers as entered, if they contain DNS servers by name, see + /// . + /// + public string DNSLookup_DNSServerInput { get; set; } + public bool RemoteDesktop_UseCredentials { get; set; } public string RemoteDesktop_Username { get; set; } public string RemoteDesktop_Domain { get; set; } diff --git a/Source/NETworkManager.Profiles/GroupViewManager.cs b/Source/NETworkManager.Profiles/GroupViewManager.cs index 4d3a910e75..7758b9c6f6 100644 --- a/Source/NETworkManager.Profiles/GroupViewManager.cs +++ b/Source/NETworkManager.Profiles/GroupViewManager.cs @@ -13,6 +13,8 @@ public static class GroupViewManager new GroupViewInfo(GroupViewName.General, new PackIconModern { Kind = PackIconModernKind.Box }), // Applications + new GroupViewInfo(GroupViewName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner)), + new GroupViewInfo(GroupViewName.DNSLookup, ApplicationManager.GetIcon(ApplicationName.DNSLookup)), new GroupViewInfo(GroupViewName.RemoteDesktop, ApplicationManager.GetIcon(ApplicationName.RemoteDesktop)), new GroupViewInfo(GroupViewName.PowerShell, ApplicationManager.GetIcon(ApplicationName.PowerShell)), new GroupViewInfo(GroupViewName.PuTTY, ApplicationManager.GetIcon(ApplicationName.PuTTY)), diff --git a/Source/NETworkManager.Profiles/GroupViewName.cs b/Source/NETworkManager.Profiles/GroupViewName.cs index b36d3bffe6..153ef3d5cf 100644 --- a/Source/NETworkManager.Profiles/GroupViewName.cs +++ b/Source/NETworkManager.Profiles/GroupViewName.cs @@ -3,6 +3,8 @@ public enum GroupViewName { General, + PortScanner, + DNSLookup, RemoteDesktop, PowerShell, PuTTY, diff --git a/Source/NETworkManager.Profiles/ProfileFileData.cs b/Source/NETworkManager.Profiles/ProfileFileData.cs index 989a642627..d6b0057f02 100644 --- a/Source/NETworkManager.Profiles/ProfileFileData.cs +++ b/Source/NETworkManager.Profiles/ProfileFileData.cs @@ -41,7 +41,19 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null) public bool ProfilesChanged { get; set; } /// - /// Schema version for handling future migrations. + /// Current schema version of the profile file (see ). Version history: + /// + /// 1: Initial version. + /// + /// 2: Port Scanner ports can be inherited from the group, a profile only uses its own ports if + /// is set. + /// + /// + /// + public const int CurrentVersion = 2; + + /// + /// Schema version for handling migrations (see ). /// public int Version { @@ -54,7 +66,7 @@ public int Version field = value; OnPropertyChanged(); } - } = 1; + } = CurrentVersion; /// /// Date of the last backup (used for daily backup tracking). @@ -131,6 +143,9 @@ public List GroupsSerializable { Profiles = [.. (gs.Profiles ?? []).Select(ps => new ProfileInfo(ps) { + // A profile is saved in its group: repairs profiles of a group that was renamed only by case + // (e.g. "servers" -> "Servers"), which kept the old name and couldn't find their group + Group = gs.Name, TagsCollection = (ps.TagsCollection == null || ps.TagsCollection.Count == 0) && !string.IsNullOrEmpty(ps.Tags) ? new Controls.ObservableSetCollection(ps.Tags.Split([';'], StringSplitOptions.RemoveEmptyEntries)) : ps.TagsCollection, diff --git a/Source/NETworkManager.Profiles/ProfileInfo.cs b/Source/NETworkManager.Profiles/ProfileInfo.cs index ade5fa6376..2a7a123263 100644 --- a/Source/NETworkManager.Profiles/ProfileInfo.cs +++ b/Source/NETworkManager.Profiles/ProfileInfo.cs @@ -1,4 +1,5 @@ //using System.Collections.Generic; +using DnsClient; using NETworkManager.Controls; using NETworkManager.Models; using NETworkManager.Models.Network; @@ -62,6 +63,7 @@ public ProfileInfo(ProfileInfo profile) PortScanner_Enabled = profile.PortScanner_Enabled; PortScanner_InheritHost = profile.PortScanner_InheritHost; PortScanner_Host = profile.PortScanner_Host; + PortScanner_OverridePorts = profile.PortScanner_OverridePorts; PortScanner_Ports = profile.PortScanner_Ports; PortScanner_PortsInput = profile.PortScanner_PortsInput; @@ -82,6 +84,8 @@ public ProfileInfo(ProfileInfo profile) DNSLookup_OverrideDNSServer = profile.DNSLookup_OverrideDNSServer; DNSLookup_DNSServer = profile.DNSLookup_DNSServer; DNSLookup_DNSServerInput = profile.DNSLookup_DNSServerInput; + DNSLookup_OverrideQueryType = profile.DNSLookup_OverrideQueryType; + DNSLookup_QueryType = profile.DNSLookup_QueryType; // Remote Desktop RemoteDesktop_Enabled = profile.RemoteDesktop_Enabled; @@ -315,6 +319,11 @@ public ProfileInfo(ProfileInfo profile) public bool PortScanner_InheritHost { get; set; } = true; public string PortScanner_Host { get; set; } + /// + /// Use of the profile instead of the ports of the group. + /// + public bool PortScanner_OverridePorts { get; set; } + /// /// Ports to scan (ports and port ranges). If port profiles were entered, their ports at the time the profile was /// saved - used if a port profile of can't be found (e.g. a profile file @@ -361,6 +370,13 @@ public ProfileInfo(ProfileInfo profile) /// public string DNSLookup_DNSServerInput { get; set; } + /// + /// Use instead of the automatic type (PTR for IP addresses, otherwise A). + /// + public bool DNSLookup_OverrideQueryType { get; set; } + + public QueryType DNSLookup_QueryType { get; set; } = QueryType.A; + public bool RemoteDesktop_Enabled { get; set; } public bool RemoteDesktop_InheritHost { get; set; } = true; public string RemoteDesktop_Host { get; set; } diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 38ed36f408..c4bb3ad038 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -1014,6 +1014,7 @@ private static void DeserializeFromJson(string jsonString) if (profileFileData != null) { LoadedProfileFileData = profileFileData; + UpgradeLoadedProfileFileData(); return; } } @@ -1031,9 +1032,12 @@ private static void DeserializeFromJson(string jsonString) // Create ProfileFileData wrapper for legacy format LoadedProfileFileData = new ProfileFileData { + Version = 1, GroupsSerializable = groupsSerializable }; + UpgradeLoadedProfileFileData(); + Log.Info("Successfully loaded profile file in legacy format. It will be migrated to new format on next save."); } @@ -1053,8 +1057,33 @@ private static void DeserializeFromXmlStream(Stream stream) LoadedProfileFileData = new ProfileFileData { + Version = 1, GroupsSerializable = groupsSerializable }; + + UpgradeLoadedProfileFileData(); + } + + /// + /// Migrates the loaded profile data from an older schema version to + /// . The file is updated the next time it is saved. + /// + private static void UpgradeLoadedProfileFileData() + { + if (LoadedProfileFileData.Version >= ProfileFileData.CurrentVersion) + return; + + Log.Info($"Upgrade profile file data from version {LoadedProfileFileData.Version} to {ProfileFileData.CurrentVersion}..."); + + // Version 2: Port Scanner ports can be inherited from the group - profiles keep their own ports + if (LoadedProfileFileData.Version < 2) + { + foreach (var profile in LoadedProfileFileData.Groups.SelectMany(x => x.Profiles) + .Where(x => !string.IsNullOrEmpty(x.PortScanner_Ports))) + profile.PortScanner_OverridePorts = true; + } + + LoadedProfileFileData.Version = ProfileFileData.CurrentVersion; } /// diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index 9ddfd821b1..d3ea08f0bf 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -43,6 +43,9 @@ public static class GlobalStaticConfiguration // known constant) that are expected to be "close enough" rather than bit-for-bit equal. public static double FloatPointFix => 1.0; + // ICMP config: maximum buffer of a ping (.NET's Ping throws for more than 65500 bytes) + public static int ICMPBufferMaximum => 65500; + // Profile config public static bool Profile_TagsMatchAny => true; public static bool Profile_ExpandProfileView => true; @@ -142,7 +145,7 @@ public static class GlobalStaticConfiguration public static int IPScanner_ICMPBuffer => 32; public static bool IPScanner_ResolveHostname => true; public static bool IPScanner_PortScanEnabled => true; - public static string IPScanner_PortScanPorts => "22; 53; 80; 135; 139; 389; 636; 443; 445; 3389; 9100"; + public static string IPScanner_PortScanPorts => "21; 22; 23; 53; 80; 135; 139; 389; 443; 445; 554; 631; 636; 3389; 5900; 5985; 7000; 8009; 8080; 8443; 9100; 62078"; public static int IPScanner_PortScanTimeout => 2000; public static int IPScanner_PortScanAttempts => 1; public static int IPScanner_MaxHostThreads => 128; @@ -167,6 +170,9 @@ public static class GlobalStaticConfiguration public static int PortScanner_Timeout => 2000; public static int PortScanner_Attempts => 1; public static bool PortScanner_PingHostsBeforeScan => false; + public static int PortScanner_ICMPAttempts => 2; + public static int PortScanner_ICMPTimeout => 2000; + public static int PortScanner_ICMPBuffer => 32; public static ExportFileType PortScanner_ExportFileType => ExportFileType.Csv; // Larger scans (hosts x ports) need a confirmation, even larger ones are refused public static long PortScanner_ConfirmPortCount => 65536; diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index a460ab433b..b3892ce80d 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -1554,6 +1554,45 @@ public bool PortScanner_PingHostsBeforeScan } } = GlobalStaticConfiguration.PortScanner_PingHostsBeforeScan; + public int PortScanner_ICMPAttempts + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = GlobalStaticConfiguration.PortScanner_ICMPAttempts; + + public int PortScanner_ICMPTimeout + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = GlobalStaticConfiguration.PortScanner_ICMPTimeout; + + public int PortScanner_ICMPBuffer + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = GlobalStaticConfiguration.PortScanner_ICMPBuffer; + public bool PortScanner_AutoAdjustThreads { get; diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 2dc5a326ff..3a76a0af05 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -856,29 +856,31 @@ private static void UpgradeToLatest(Version version) Current.PortScanner_Timeout = GlobalStaticConfiguration.PortScanner_Timeout; } - if (Current.IPScanner_PortScanPorts == "22; 53; 80; 139; 389; 636; 443; 445; 3389") + // Old defaults (until 2026.7.7.0 and of the pre-releases since 2026.9.7.0) + if (Current.IPScanner_PortScanPorts is "22; 53; 80; 139; 389; 636; 443; 445; 3389" + or "22; 53; 80; 135; 139; 389; 636; 443; 445; 3389; 9100") { Log.Info($"Update \"IPScanner_PortScanPorts\" to \"{GlobalStaticConfiguration.IPScanner_PortScanPorts}\"..."); Current.IPScanner_PortScanPorts = GlobalStaticConfiguration.IPScanner_PortScanPorts; } // ICMP buffer - .NET's Ping throws for more than 65500 bytes, the settings allowed up to 65535 - if (Current.IPScanner_ICMPBuffer > 65500) + if (Current.IPScanner_ICMPBuffer > GlobalStaticConfiguration.ICMPBufferMaximum) { - Log.Info($"Update \"IPScanner_ICMPBuffer\" from {Current.IPScanner_ICMPBuffer} to 65500..."); - Current.IPScanner_ICMPBuffer = 65500; + Log.Info($"Update \"IPScanner_ICMPBuffer\" from {Current.IPScanner_ICMPBuffer} to {GlobalStaticConfiguration.ICMPBufferMaximum}..."); + Current.IPScanner_ICMPBuffer = GlobalStaticConfiguration.ICMPBufferMaximum; } - if (Current.PingMonitor_Buffer > 65500) + if (Current.PingMonitor_Buffer > GlobalStaticConfiguration.ICMPBufferMaximum) { - Log.Info($"Update \"PingMonitor_Buffer\" from {Current.PingMonitor_Buffer} to 65500..."); - Current.PingMonitor_Buffer = 65500; + Log.Info($"Update \"PingMonitor_Buffer\" from {Current.PingMonitor_Buffer} to {GlobalStaticConfiguration.ICMPBufferMaximum}..."); + Current.PingMonitor_Buffer = GlobalStaticConfiguration.ICMPBufferMaximum; } - if (Current.Traceroute_Buffer > 65500) + if (Current.Traceroute_Buffer > GlobalStaticConfiguration.ICMPBufferMaximum) { - Log.Info($"Update \"Traceroute_Buffer\" from {Current.Traceroute_Buffer} to 65500..."); - Current.Traceroute_Buffer = 65500; + Log.Info($"Update \"Traceroute_Buffer\" from {Current.Traceroute_Buffer} to {GlobalStaticConfiguration.ICMPBufferMaximum}..."); + Current.Traceroute_Buffer = GlobalStaticConfiguration.ICMPBufferMaximum; } // Add new Port Scanner port profiles diff --git a/Source/NETworkManager.Tests/Network/DNSServerTests.cs b/Source/NETworkManager.Tests/Network/DNSServerTests.cs index 054722ce6b..cc764182d7 100644 --- a/Source/NETworkManager.Tests/Network/DNSServerTests.cs +++ b/Source/NETworkManager.Tests/Network/DNSServerTests.cs @@ -70,6 +70,27 @@ public void DecodeReferences_FalseIfProfileIsMissing() Assert.Null(decoded); } + [Fact] + public void EncodeAndDecode() + { + var cloudflare = Profiles.First(x => x.Name == "Cloudflare"); + + var (dnsServer, dnsServerInput) = DNSServer.Encode("Cloudflare; 9.9.9.9", Profiles); + + Assert.Equal("1.1.1.1:53; 1.0.0.1:53; 9.9.9.9", dnsServer); + Assert.Equal($"{cloudflare.Id:B}; 9.9.9.9", dnsServerInput); + Assert.Equal("Cloudflare; 9.9.9.9", DNSServer.Decode(dnsServer, dnsServerInput, Profiles, out var notFound)); + Assert.False(notFound); + } + + [Fact] + public void Decode_SavedServersIfProfileIsMissing() + { + Assert.Equal("1.1.1.1:53", DNSServer.Decode("1.1.1.1:53", $"{System.Guid.NewGuid():B}", Profiles, + out var notFound)); + Assert.True(notFound); + } + [Fact] public void ReplaceProfilesByServers() { diff --git a/Source/NETworkManager.Tests/Network/PortProfileTests.cs b/Source/NETworkManager.Tests/Network/PortProfileTests.cs index a20fccd9a2..df52ad2fc0 100644 --- a/Source/NETworkManager.Tests/Network/PortProfileTests.cs +++ b/Source/NETworkManager.Tests/Network/PortProfileTests.cs @@ -89,4 +89,49 @@ public void IsValidName(string name, bool isValid) { Assert.Equal(isValid, PortProfile.IsValidName(name)); } + + [Fact] + public void Encode_PortsAndReferences() + { + var (ports, portsInput) = PortProfile.Encode("Webserver; 8080", PortProfiles); + + Assert.Equal("80; 443; 8080", ports); + Assert.Equal($"{PortProfiles[0].Id:B}; 8080", portsInput); + } + + [Fact] + public void Encode_WithoutPortProfile() + { + var (ports, portsInput) = PortProfile.Encode(" 22; 80 ", PortProfiles); + + Assert.Equal("22; 80", ports); + Assert.Null(portsInput); + } + + [Fact] + public void Decode_CurrentNameAfterRename() + { + var portProfile = new PortProfileInfo("Web", "80"); + var (ports, portsInput) = PortProfile.Encode("Web; 22", [portProfile]); + + portProfile.Name = "Web (renamed)"; + + Assert.Equal("Web (renamed); 22", PortProfile.Decode(ports, portsInput, [portProfile], out var notFound)); + Assert.False(notFound); + } + + [Fact] + public void Decode_SavedPortsWithoutReferences() + { + Assert.Equal("22; 80", PortProfile.Decode("22; 80", null, PortProfiles, out var notFound)); + Assert.False(notFound); + } + + [Fact] + public void Decode_SavedPortsIfPortProfileIsMissing() + { + Assert.Equal("80; 22", PortProfile.Decode("80; 22", $"{Guid.NewGuid():B}; 22", PortProfiles, + out var notFound)); + Assert.True(notFound); + } } diff --git a/Source/NETworkManager.Tests/Utilities/IPAddressHelperTests.cs b/Source/NETworkManager.Tests/Utilities/IPAddressHelperTests.cs new file mode 100644 index 0000000000..a3467d7e0b --- /dev/null +++ b/Source/NETworkManager.Tests/Utilities/IPAddressHelperTests.cs @@ -0,0 +1,36 @@ +using NETworkManager.Utilities; +using Xunit; + +namespace NETworkManager.Tests.Utilities; + +public class IPAddressHelperTests +{ + [Theory] + [InlineData("10.0.0.1", true)] + [InlineData(" 10.0.0.1 ", true)] + [InlineData("2001:db8::1", true)] + [InlineData("::1", true)] + [InlineData("10", false)] + [InlineData("127.1", false)] + [InlineData("server-01.borntoberoot.net", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void IsIPAddress(string host, bool expected) + { + Assert.Equal(expected, IPAddressHelper.IsIPAddress(host)); + } + + [Theory] + [InlineData("10.0.0.1", true)] + [InlineData("10.0.0.1; 2001:db8::1", true)] + [InlineData("10.0.0.1; 10.0.0.2;", true)] + [InlineData("10.0.0.1;; 10.0.0.2", true)] + [InlineData("10.0.0.1; example.com", false)] + [InlineData(";", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void AreIPAddresses(string hosts, bool expected) + { + Assert.Equal(expected, IPAddressHelper.AreIPAddresses(hosts)); + } +} diff --git a/Source/NETworkManager.Utilities/IPAddressHelper.cs b/Source/NETworkManager.Utilities/IPAddressHelper.cs index 988289f0da..a4bfbdca2f 100644 --- a/Source/NETworkManager.Utilities/IPAddressHelper.cs +++ b/Source/NETworkManager.Utilities/IPAddressHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using System.Net.Sockets; @@ -6,6 +7,34 @@ namespace NETworkManager.Utilities; public static class IPAddressHelper { + /// + /// Checks if a host is an IP address: an IPv4 address in the full form (shortened forms like "10" are host + /// names) or an IPv6 address. + /// + /// Host to check. + /// True if the host is an IP address. + public static bool IsIPAddress(string host) + { + return !string.IsNullOrWhiteSpace(host) && + (RegexHelper.IPv4AddressRegex().IsMatch(host.Trim()) || + host.Contains(':') && IPAddress.TryParse(host.Trim(), out var ipAddress) && + ipAddress.AddressFamily == AddressFamily.InterNetworkV6); + } + + /// + /// Checks if all hosts (separated by ";", empty entries like a trailing ";" are ignored) are IP addresses (see + /// ). + /// + /// Hosts to check. + /// True if there is at least one host and all hosts are IP addresses. + public static bool AreIPAddresses(string hosts) + { + var entries = (hosts ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries | + StringSplitOptions.TrimEntries); + + return entries.Length > 0 && entries.All(IsIPAddress); + } + /// /// Test if an IP address is a private address. /// diff --git a/Source/NETworkManager/ProfileDialogManager.cs b/Source/NETworkManager/ProfileDialogManager.cs index 2c62478873..035faa2749 100644 --- a/Source/NETworkManager/ProfileDialogManager.cs +++ b/Source/NETworkManager/ProfileDialogManager.cs @@ -24,6 +24,13 @@ public static class ProfileDialogManager private static ProfileInfo ParseProfileInfo(ProfileViewModel instance) { + // Port profiles and DNS servers by name are referenced by their ID (found after a rename), their ports / servers + // are saved as well (used if one can't be found, e.g. a profile file from another computer) + var (portScannerPorts, portScannerPortsInput) = PortProfile.Encode(instance.PortScanner_Ports, + SettingsManager.Current.PortScanner_PortProfiles); + var (dnsLookupDNSServer, dnsLookupDNSServerInput) = DNSServer.Encode(instance.DNSLookup_DNSServer, + SettingsManager.Current.DNSLookup_DNSServers); + return new ProfileInfo { Name = instance.Name.Trim(), @@ -55,14 +62,9 @@ private static ProfileInfo ParseProfileInfo(ProfileViewModel instance) PortScanner_Host = instance.PortScanner_InheritHost ? instance.Host?.Trim() : instance.PortScanner_Host?.Trim(), - // Port profiles are referenced by their ID (found after a rename), their ports are saved as well (used if - // a port profile can't be found, e.g. a profile file from another computer) - PortScanner_Ports = PortProfile.TryResolvePorts(instance.PortScanner_Ports, - SettingsManager.Current.PortScanner_PortProfiles, out var portScannerPorts) - ? portScannerPorts - : instance.PortScanner_Ports?.Trim(), - PortScanner_PortsInput = PortProfile.EncodeReferences(instance.PortScanner_Ports, - SettingsManager.Current.PortScanner_PortProfiles), + PortScanner_OverridePorts = instance.PortScanner_OverridePorts, + PortScanner_Ports = portScannerPorts, + PortScanner_PortsInput = portScannerPortsInput, // Ping Monitor PingMonitor_Enabled = instance.PingMonitor_Enabled, @@ -82,12 +84,10 @@ private static ProfileInfo ParseProfileInfo(ProfileViewModel instance) DNSLookup_InheritHost = instance.DNSLookup_InheritHost, DNSLookup_Host = instance.DNSLookup_InheritHost ? instance.Host?.Trim() : instance.DNSLookup_Host?.Trim(), DNSLookup_OverrideDNSServer = instance.DNSLookup_OverrideDNSServer, - // DNS servers by name are referenced by their ID (found after a rename), their servers are saved as well - // (used if a DNS server can't be found, e.g. a profile file from another computer) - DNSLookup_DNSServer = DNSServer.ReplaceProfilesByServers(instance.DNSLookup_DNSServer?.Trim(), - SettingsManager.Current.DNSLookup_DNSServers), - DNSLookup_DNSServerInput = DNSServer.EncodeReferences(instance.DNSLookup_DNSServer, - SettingsManager.Current.DNSLookup_DNSServers), + DNSLookup_DNSServer = dnsLookupDNSServer, + DNSLookup_DNSServerInput = dnsLookupDNSServerInput, + DNSLookup_OverrideQueryType = instance.DNSLookup_OverrideQueryType, + DNSLookup_QueryType = instance.DNSLookup_QueryType, // Remote Desktop RemoteDesktop_Enabled = instance.RemoteDesktop_Enabled, @@ -306,14 +306,22 @@ private static GroupInfo ParseGroupInfo(GroupViewModel instance) var name = instance.Name.Trim(); - // Update group in profiles + // Update group in profiles - also if only the case changed (e.g. "servers" -> "Servers"), groups are looked up + // case-sensitive (see ProfileManager.GetGroupByName) if (profiles.Count > 0) if (!string.IsNullOrEmpty(instance.Group.Name) && - !string.Equals(instance.Group.Name, name, StringComparison.OrdinalIgnoreCase)) + !string.Equals(instance.Group.Name, name, StringComparison.Ordinal)) foreach (var profile in profiles) profile.Group = name; //else // Debug.WriteLine("Cannot update group in profiles"); + + // Like the profile (see ParseProfileInfo) + var (portScannerPorts, portScannerPortsInput) = PortProfile.Encode(instance.PortScanner_Ports, + SettingsManager.Current.PortScanner_PortProfiles); + var (dnsLookupDNSServer, dnsLookupDNSServerInput) = DNSServer.Encode(instance.DNSLookup_DNSServer, + SettingsManager.Current.DNSLookup_DNSServers); + return new GroupInfo { Name = name, @@ -321,6 +329,16 @@ private static GroupInfo ParseGroupInfo(GroupViewModel instance) Profiles = profiles, + // Port Scanner + PortScanner_OverridePorts = instance.PortScanner_OverridePorts, + PortScanner_Ports = portScannerPorts, + PortScanner_PortsInput = portScannerPortsInput, + + // DNS Lookup + DNSLookup_OverrideDNSServer = instance.DNSLookup_OverrideDNSServer, + DNSLookup_DNSServer = dnsLookupDNSServer, + DNSLookup_DNSServerInput = dnsLookupDNSServerInput, + // Remote Desktop RemoteDesktop_UseCredentials = instance.RemoteDesktop_UseCredentials, RemoteDesktop_Username = diff --git a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs index 8ed0d47689..5ca5fe8f9a 100644 --- a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs @@ -3,17 +3,12 @@ using NETworkManager.Controls; using NETworkManager.Localization.Resources; using NETworkManager.Models; -using NETworkManager.Models.Network; using NETworkManager.Profiles; -using NETworkManager.Settings; using NETworkManager.Utilities; using NETworkManager.Views; using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Windows.Input; namespace NETworkManager.ViewModels; @@ -163,7 +158,7 @@ private static void CloseItemAction(ItemActionCallbackArgs args) public void AddTab(string host = null, string dnsServer = null, QueryType? queryType = null, bool start = true, string message = null) { - if (queryType == null && IsIPAddresses(host)) + if (queryType == null && IPAddressHelper.AreIPAddresses(host)) queryType = QueryType.PTR; var tabId = Guid.NewGuid(); @@ -174,17 +169,6 @@ public void AddTab(string host = null, string dnsServer = null, QueryType? query SelectedTabIndex = TabItems.Count - 1; } - /// - /// Checks if the host(s) are only IP addresses (shortened IPv4 forms like "10" are host names). - /// - private static bool IsIPAddresses(string hosts) - { - return !string.IsNullOrWhiteSpace(hosts) && hosts.Split(';').Select(x => x.Trim()).All(x => - RegexHelper.IPv4AddressRegex().IsMatch(x) || - x.Contains(':') && IPAddress.TryParse(x, out var ipAddress) && - ipAddress.AddressFamily == AddressFamily.InterNetworkV6); - } - /// /// Adds a new tab for the host of the specified profile. /// @@ -192,28 +176,12 @@ private static bool IsIPAddresses(string hosts) /// Starts it on the first load. If false, the input is only filled in. public void AddTab(ProfileInfo profile, bool start = true) { - // DNS server of DNS Lookup (last selected) - if (!profile.DNSLookup_OverrideDNSServer || string.IsNullOrWhiteSpace(profile.DNSLookup_DNSServer)) - { - AddTab(profile.DNSLookup_Host, start: start); - return; - } - - // DNS servers by name with their current name (also after a rename) - if (!string.IsNullOrEmpty(profile.DNSLookup_DNSServerInput) && - DNSServer.TryDecodeReferences(profile.DNSLookup_DNSServerInput, - SettingsManager.Current.DNSLookup_DNSServers, out var dnsServer)) - { - AddTab(profile.DNSLookup_Host, dnsServer, start: start); - return; - } + // DNS servers of the profile or its group (null: the selected DNS server). A DNS server that can't be found: + // the saved servers, with a note. + var dnsServer = NETworkManager.Profiles.Application.DNSLookup.GetDNSServer(profile, out var dnsServerNotFound); - // No DNS server by name, or one can't be found (deleted, or a profile file shared from another computer - DNS - // servers are saved in the settings): the servers saved with the profile, with a note if one wasn't found - AddTab(profile.DNSLookup_Host, profile.DNSLookup_DNSServer, start: start, - message: string.IsNullOrEmpty(profile.DNSLookup_DNSServerInput) - ? null - : Strings.DNSServerOfProfileNotFoundMessage); + AddTab(profile.DNSLookup_Host, dnsServer, NETworkManager.Profiles.Application.DNSLookup.GetQueryType(profile), + start, dnsServerNotFound ? Strings.DNSServerOfProfileNotFoundMessage : null); } #endregion diff --git a/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs b/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs index 26499769c3..1fe380227e 100644 --- a/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs +++ b/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs @@ -394,6 +394,27 @@ private async Task QueryAsync() DragablzTabItem.SetTabHeader(_tabId, Host); + List hosts = [.. Host.Split(';').Select(x => x.Trim())]; + + // PTR needs IP addresses (e.g. from a profile or the run command): hostnames are skipped with a message + // instead of the error of the lookup ("An invalid IP address was specified"). Checked before the DNS servers + // are resolved and the history is changed, so nothing is done if no host is left. Every address the lookup + // can parse is valid (also shortened IPv4 forms like "127.1"). + if (QueryType == QueryType.PTR) + { + foreach (var host in hosts.Where(x => x.Length > 0 && !IPAddress.TryParse(x, out _))) + AppendStatusMessage(string.Format(Strings.PTRRequiresIPAddressX, host)); + + hosts = [.. hosts.Where(x => IPAddress.TryParse(x, out _))]; + + if (hosts.Count == 0) + { + IsRunning = false; + + return; + } + } + ListHelper.Modify(SettingsManager.Current.DNSLookup_HostHistory, Host, SettingsManager.Current.General_HistoryListEntries); @@ -484,7 +505,7 @@ private async Task QueryAsync() dnsLookup.LookupError += DNSLookup_LookupError; dnsLookup.LookupComplete += DNSLookup_LookupComplete; - dnsLookup.ResolveAsync([.. Host.Split(';').Select(x => x.Trim())]); + dnsLookup.ResolveAsync(hosts); } /// diff --git a/Source/NETworkManager/ViewModels/GroupViewModel.cs b/Source/NETworkManager/ViewModels/GroupViewModel.cs index a3debe5d79..b54f4cf08d 100644 --- a/Source/NETworkManager/ViewModels/GroupViewModel.cs +++ b/Source/NETworkManager/ViewModels/GroupViewModel.cs @@ -3,6 +3,7 @@ using NETworkManager.Models.PuTTY; using NETworkManager.Models.RemoteDesktop; using NETworkManager.Profiles; +using NETworkManager.Settings; using NETworkManager.Utilities; using System; using System.Collections.Generic; @@ -107,6 +108,90 @@ public string Description #endregion + #region Port Scanner + + public bool PortScanner_OverridePorts + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + private readonly InputSuggestions _portScannerPortsSuggestions = InputSuggestions.ForPortScannerPorts(); + + /// + /// Gets the port profiles and the ports history of the Port Scanner for the ports input (grouped). + /// + public ICollectionView PortScanner_PortsSuggestions => _portScannerPortsSuggestions.View; + + /// + /// Gets the names of the port profiles for the Tab completion of the ports input of the Port Scanner. + /// + public IEnumerable PortScanner_PortProfileNames => _portScannerPortsSuggestions.ProfileNames; + + public string PortScanner_Ports + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + #endregion + + #region DNS Lookup + + public bool DNSLookup_OverrideDNSServer + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + public string DNSLookup_DNSServer + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + private readonly InputSuggestions _dnsLookupDNSServerSuggestions = InputSuggestions.ForDNSLookupDNSServers(); + + /// + /// Gets the DNS servers and the DNS server history of DNS Lookup for the DNS server input (grouped). + /// + public ICollectionView DNSLookup_DNSServerSuggestions => _dnsLookupDNSServerSuggestions.View; + + /// + /// Gets the names of the DNS servers for the Tab completion of the DNS server input of DNS Lookup. + /// + public IEnumerable DNSLookup_DNSServerNames => _dnsLookupDNSServerSuggestions.ProfileNames; + + #endregion + #region Remote Desktop /// @@ -1590,6 +1675,16 @@ public GroupViewModel(Action saveCommand, Action Name = groupInfo.Name; Description = groupInfo.Description; + // Port Scanner + PortScanner_OverridePorts = groupInfo.PortScanner_OverridePorts; + PortScanner_Ports = PortProfile.Decode(groupInfo.PortScanner_Ports, groupInfo.PortScanner_PortsInput, + SettingsManager.Current.PortScanner_PortProfiles, out _); + + // DNS Lookup + DNSLookup_OverrideDNSServer = groupInfo.DNSLookup_OverrideDNSServer; + DNSLookup_DNSServer = DNSServer.Decode(groupInfo.DNSLookup_DNSServer, groupInfo.DNSLookup_DNSServerInput, + SettingsManager.Current.DNSLookup_DNSServers, out _); + // Remote Desktop RemoteDesktop_UseCredentials = groupInfo.RemoteDesktop_UseCredentials; RemoteDesktop_Username = groupInfo.RemoteDesktop_Username; diff --git a/Source/NETworkManager/ViewModels/IPScannerViewModel.cs b/Source/NETworkManager/ViewModels/IPScannerViewModel.cs index c51439abb6..c390d379af 100644 --- a/Source/NETworkManager/ViewModels/IPScannerViewModel.cs +++ b/Source/NETworkManager/ViewModels/IPScannerViewModel.cs @@ -901,7 +901,9 @@ private static IPScannerOptions CreateScanOptions(int[] ports) MaxPortThreads = SettingsManager.Current.IPScanner_MaxPortThreads, ICMPAttempts = SettingsManager.Current.IPScanner_ICMPAttempts, ICMPTimeout = SettingsManager.Current.IPScanner_ICMPTimeout, - ICMPBuffer = new byte[SettingsManager.Current.IPScanner_ICMPBuffer], + // Limits of the input (a value edited in the settings file would throw or make every ping fail) + ICMPBuffer = new byte[Math.Clamp(SettingsManager.Current.IPScanner_ICMPBuffer, 1, + GlobalStaticConfiguration.ICMPBufferMaximum)], ResolveHostname = SettingsManager.Current.IPScanner_ResolveHostname, PortScanEnabled = SettingsManager.Current.IPScanner_PortScanEnabled, PortScanPorts = ports, diff --git a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs index ab7395266f..1a91956a2b 100644 --- a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs +++ b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs @@ -571,7 +571,9 @@ public void Start() var ping = new Ping { Timeout = SettingsManager.Current.PingMonitor_Timeout, - Buffer = new byte[SettingsManager.Current.PingMonitor_Buffer], + // Limits of the input (a value edited in the settings file would throw or make every ping fail) + Buffer = new byte[Math.Clamp(SettingsManager.Current.PingMonitor_Buffer, 1, + GlobalStaticConfiguration.ICMPBufferMaximum)], TTL = SettingsManager.Current.PingMonitor_TTL, DontFragment = SettingsManager.Current.PingMonitor_DontFragment, WaitTime = SettingsManager.Current.PingMonitor_WaitTime diff --git a/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs index e095857c2c..2c881f7a51 100644 --- a/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs @@ -2,9 +2,7 @@ using NETworkManager.Controls; using NETworkManager.Localization.Resources; using NETworkManager.Models; -using NETworkManager.Models.Network; using NETworkManager.Profiles; -using NETworkManager.Settings; using NETworkManager.Utilities; using NETworkManager.Views; using System; @@ -152,23 +150,11 @@ public void AddTab(string host = null, string ports = null, bool start = true, s public void AddTab(ProfileInfo profile, bool start = true) { - // Port profiles with their current name and ports. A port profile that can't be found (deleted, or a profile - // file shared from another computer - port profiles are saved in the settings): the ports saved with the - // profile, with a note. - if (string.IsNullOrEmpty(profile.PortScanner_PortsInput)) - { - AddTab(profile.PortScanner_Host, profile.PortScanner_Ports, start); - } - else if (PortProfile.TryDecodeReferences(profile.PortScanner_PortsInput, - SettingsManager.Current.PortScanner_PortProfiles, out var ports)) - { - AddTab(profile.PortScanner_Host, ports, start); - } - else - { - AddTab(profile.PortScanner_Host, profile.PortScanner_Ports, start, - Strings.PortProfileOfProfileNotFoundMessage); - } + // Ports of the profile or its group. A port profile that can't be found: the saved ports, with a note. + var ports = NETworkManager.Profiles.Application.PortScanner.GetPorts(profile, out var portProfileNotFound); + + AddTab(profile.PortScanner_Host, ports, start, + portProfileNotFound ? Strings.PortProfileOfProfileNotFoundMessage : null); } #endregion diff --git a/Source/NETworkManager/ViewModels/PortScannerSettingsViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerSettingsViewModel.cs index 66b7e11f67..65221ec213 100644 --- a/Source/NETworkManager/ViewModels/PortScannerSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerSettingsViewModel.cs @@ -156,6 +156,63 @@ public bool PingHostsBeforeScan } } + /// + /// Gets or sets the number of ICMP attempts of the ping before the scan. + /// + public int ICMPAttempts + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PortScanner_ICMPAttempts = value; + + field = value; + OnPropertyChanged(); + } + } + + /// + /// Gets or sets the ICMP timeout in milliseconds of the ping before the scan. + /// + public int ICMPTimeout + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PortScanner_ICMPTimeout = value; + + field = value; + OnPropertyChanged(); + } + } + + /// + /// Gets or sets the ICMP buffer size of the ping before the scan. + /// + public int ICMPBuffer + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PortScanner_ICMPBuffer = value; + + field = value; + OnPropertyChanged(); + } + } + /// /// Gets or sets whether the number of hosts and ports scanned at the same time is adjusted to the scan (instead of /// and ). @@ -259,6 +316,9 @@ private void LoadSettings() Timeout = SettingsManager.Current.PortScanner_Timeout; Attempts = SettingsManager.Current.PortScanner_Attempts; PingHostsBeforeScan = SettingsManager.Current.PortScanner_PingHostsBeforeScan; + ICMPAttempts = SettingsManager.Current.PortScanner_ICMPAttempts; + ICMPTimeout = SettingsManager.Current.PortScanner_ICMPTimeout; + ICMPBuffer = SettingsManager.Current.PortScanner_ICMPBuffer; ResolveHostname = SettingsManager.Current.PortScanner_ResolveHostname; AutoAdjustThreads = SettingsManager.Current.PortScanner_AutoAdjustThreads; MaxHostThreads = SettingsManager.Current.PortScanner_MaxHostThreads; diff --git a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs index d92c6fc70c..37c0164617 100644 --- a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs @@ -36,9 +36,16 @@ public class PortScannerViewModel : ViewModelBase, IProfileManagerMinimal private static readonly ILog Log = LogManager.GetLogger(typeof(PortScannerViewModel)); /// - /// Maximum effort (changed rows x all rows) to remove / add the rows one by one when the results of hosts are - /// replaced (rescan, see ) or buffered results are added (see - /// ). E.g. 1,000 rows of 10,000 or 10 rows of 1,000,000. Above, with one reset. + /// Maximum effort to remove / add rows one by one when the results of hosts are replaced (rescan, see + /// ) or buffered results are added (see ). Above, the + /// results are replaced with one reset (sorts and groups all rows once, but collapsed groups and the scroll + /// position are lost). + /// + /// The effort is a product (changed rows x all rows), not a row count: WPF inserts / removes every single row in + /// its group of the sorted view with a linear search. Even one host with 1-65535 (max. 65,535 rows) exceeds it, + /// e.g. adding 500 rows to 20,000 rows (10.25M) is done with a reset, 100 rows to 60,000 (6M) one by one. With + /// multiple hosts, the results can have millions of rows (e.g. a /24 with 1-65535 and closed ports shown). + /// /// private const long MaxRowChangesOneByOne = 10_000_000; @@ -730,6 +737,7 @@ private async void AddProfileSelectedHostAction(PortScannerPortInfo row) Host = row.IPAddress.ToString(), // Additional data + PortScanner_OverridePorts = true, PortScanner_Ports = _lastPortsInput }; @@ -1276,6 +1284,11 @@ private static PortScannerOptions CreateScanOptions() Timeout = SettingsManager.Current.PortScanner_Timeout, Attempts = SettingsManager.Current.PortScanner_Attempts, PingHostsBeforeScan = SettingsManager.Current.PortScanner_PingHostsBeforeScan, + ICMPAttempts = SettingsManager.Current.PortScanner_ICMPAttempts, + ICMPTimeout = SettingsManager.Current.PortScanner_ICMPTimeout, + // Limits of the input (a value edited in the settings file would throw or make every ping fail) + ICMPBuffer = new byte[Math.Clamp(SettingsManager.Current.PortScanner_ICMPBuffer, 1, + GlobalStaticConfiguration.ICMPBufferMaximum)], ResolveHostname = SettingsManager.Current.PortScanner_ResolveHostname, ShowAllResults = SettingsManager.Current.PortScanner_ShowAllResults }; diff --git a/Source/NETworkManager/ViewModels/ProfileViewModel.cs b/Source/NETworkManager/ViewModels/ProfileViewModel.cs index ca4c44aced..b810aef2ee 100644 --- a/Source/NETworkManager/ViewModels/ProfileViewModel.cs +++ b/Source/NETworkManager/ViewModels/ProfileViewModel.cs @@ -1,3 +1,4 @@ +using DnsClient; using NETworkManager.Controls; using NETworkManager.Localization.Resources; using NETworkManager.Models; @@ -89,12 +90,9 @@ public ProfileViewModel(Action saveCommand, Action saveCommand, Action x.ToString())]; + DNSLookup_QueryType = DNSLookup_QueryTypes.Contains(profileInfo.DNSLookup_QueryType) + ? profileInfo.DNSLookup_QueryType + : QueryType.A; // Remote Desktop RemoteDesktop_Enabled = editMode == ProfileEditMode.Add @@ -436,6 +435,8 @@ public string Host field = value; OnPropertyChanged(); + + ValidateDNSLookupQueryType(); } } @@ -747,6 +748,19 @@ public string PortScanner_Host } } + public bool PortScanner_OverridePorts + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + private readonly InputSuggestions _portScannerPortsSuggestions = InputSuggestions.ForPortScannerPorts(); /// @@ -888,6 +902,8 @@ public bool DNSLookup_InheritHost field = value; OnPropertyChanged(); + + ValidateDNSLookupQueryType(); } } @@ -901,6 +917,8 @@ public string DNSLookup_Host field = value; OnPropertyChanged(); + + ValidateDNSLookupQueryType(); } } @@ -942,6 +960,64 @@ public string DNSLookup_DNSServer /// public IEnumerable DNSLookup_DNSServerNames => _dnsLookupDNSServerSuggestions.ProfileNames; + public bool DNSLookup_OverrideQueryType + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + + ValidateDNSLookupQueryType(); + } + } + + public List DNSLookup_QueryTypes { get; } + + public QueryType DNSLookup_QueryType + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + + ValidateDNSLookupQueryType(); + } + } + + /// + /// Gets a value indicating whether the query type fits the host: PTR can only be looked up for IP addresses. + /// + public bool DNSLookup_QueryTypeIsValid + { + get; + private set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = true; + + /// + /// Checks if the query type of the profile fits the host (own or inherited), see + /// . + /// + private void ValidateDNSLookupQueryType() + { + DNSLookup_QueryTypeIsValid = !DNSLookup_OverrideQueryType || DNSLookup_QueryType != QueryType.PTR || + IPAddressHelper.AreIPAddresses(DNSLookup_InheritHost ? Host : DNSLookup_Host); + } + #endregion #region Remote Desktop diff --git a/Source/NETworkManager/ViewModels/TracerouteViewModel.cs b/Source/NETworkManager/ViewModels/TracerouteViewModel.cs index 1cb383e31e..748b20836a 100644 --- a/Source/NETworkManager/ViewModels/TracerouteViewModel.cs +++ b/Source/NETworkManager/ViewModels/TracerouteViewModel.cs @@ -420,7 +420,9 @@ await DNSClientHelper.ResolveAorAaaaAsync(Host, { var traceroute = new Traceroute(new TracerouteOptions( SettingsManager.Current.Traceroute_Timeout, - new byte[SettingsManager.Current.Traceroute_Buffer], + // Limits of the input (a value edited in the settings file would throw or make every ping fail) + new byte[Math.Clamp(SettingsManager.Current.Traceroute_Buffer, 1, + GlobalStaticConfiguration.ICMPBufferMaximum)], SettingsManager.Current.Traceroute_MaximumHops, true, SettingsManager.Current.Traceroute_ResolveHostname, diff --git a/Source/NETworkManager/Views/GroupChildWindow.xaml b/Source/NETworkManager/Views/GroupChildWindow.xaml index 38676ab0b2..de893a01c5 100644 --- a/Source/NETworkManager/Views/GroupChildWindow.xaml +++ b/Source/NETworkManager/Views/GroupChildWindow.xaml @@ -11,6 +11,7 @@ xmlns:network="clr-namespace:NETworkManager.Models.Network;assembly=NETworkManager.Models" xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters" xmlns:validators="clr-namespace:NETworkManager.Validators;assembly=NETworkManager.Validators" + xmlns:controls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls" 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" @@ -148,6 +149,40 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -569,12 +604,14 @@ + + SelectedValuePath="Tag" + SelectedValue="{Binding ElementName=ListBoxViews, Path=SelectedItem.(profiles:GroupViewInfo.Name), Mode=OneWay}"> - + + + - - + @@ -1899,6 +1929,12 @@ + + + + + + @@ -1942,13 +1978,18 @@ - - + + + - + + + + + +