Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .claude/skills/feature-review/references/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,19 @@ Reference: `HostsFileEditorView.xaml`, `FirewallView.xaml`.
`SelectedResults` is changed in place (no change notification): selection-dependent properties are raised
in `OnContextMenuOpened()` called from the view's `ContextMenu_Opened`.
- Two context menus: `RowContextMenu` (one row) and `MultiRowContextMenu` (several rows), switched by a
`DataTrigger` on `SelectedItems.Count`. `Opened="ContextMenu_Opened"` sets the menu's `DataContext`.
`DataTrigger` on `SelectedItems.Count`.
- A context menu is not part of the visual tree and doesn't inherit the `DataContext`: the menu gets
`DataContext="{Binding Data, Source={StaticResource BindingProxy}}"` in XAML, with
`<wpfHelpers:BindingProxy x:Key="BindingProxy" Data="{Binding}" />` in the view's own resources, declared before
the menus. The bindings inside the menu are plain bindings (`{Binding EditEntryCommand}`). Don't set the
`DataContext` in `ContextMenu_Opened` (until the menu is opened, its bindings fail and are logged as XAML binding
errors). `Opened` is only for extra work, e.g. `OnContextMenuOpened()` or the custom commands of the IP Scanner.
- No bindings that have no element to resolve against: no `VisualBrush` / `BitmapImage` with a binding in a style
setter or as a freezable resource. Bind the property of the element with a converter instead
(`VisualToVisualBrushConverter`, `ApplicationNameToIconBrushConverter`, `Image Source="{Binding Uri}"`). A static
`VisualBrush` (`Visual="{iconPacks:Material Kind=...}"`) is fine. `MenuItem`s created outside a menu (e.g. extra
items in an `x:Array`) are fine: the implicit `DefaultMenuItem` style sets the content alignment directly instead
of the MahApps binding to the parent. New `MenuItem` styles are `BasedOn="{StaticResource DefaultMenuItem}"`.
- Order: Enable / Disable (only the applicable one) β†’ Edit... β†’ Copy as... β†’ Delete β†’ separator β†’ Copy
(submenu, `CopyMenuItem` style) β†’ Export...
- Icons (16Γ—16 `Rectangle`, `MahApps.Brushes.Gray3`, `VisualBrush`): Enable `Material ToggleSwitchOutline`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
ο»Ώusing System;
using System.Globalization;
using System.Windows.Data;
using NETworkManager.Profiles;

namespace NETworkManager.Converters;

/// <summary>
/// Convert a <see cref="ProfileInfo" /> to the ports the Port Scanner scans for it (own ports, otherwise the ports
/// of its group, port profiles with their current name).
/// </summary>
public sealed class ProfileToPortScannerPortsConverter : IValueConverter
{
/// <summary>
/// Convert a <see cref="ProfileInfo" /> to the ports to scan (see
/// <see cref="Profiles.Application.PortScanner.GetPorts" />).
/// </summary>
/// <param name="value">Object from type <see cref="ProfileInfo" />.</param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns>Ports to scan, empty if neither the profile nor its group has ports.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is not ProfileInfo profile
? string.Empty
: Profiles.Application.PortScanner.GetPorts(profile, out _);
}

/// <summary>
/// !!! Method not implemented !!!
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
5 changes: 4 additions & 1 deletion Source/NETworkManager.Models/Network/IPScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public sealed class IPScanner(IPScannerOptions options)
/// </summary>
private static readonly TimeSpan MaxHostStartInterval = TimeSpan.FromMilliseconds(10);

// Data sent with the ping, created once (see IPScannerOptions.ICMPBufferSize)
private readonly byte[] _icmpBuffer = new byte[options.ICMPBufferSize];

private int _progressValue;
private readonly InterlockedCounter _hostsUp = new();
private readonly InterlockedCounter _hostsDown = new();
Expand Down Expand Up @@ -211,7 +214,7 @@ private async Task ScanHostAsync(IPAddress ipAddress, ParallelOptions portScanPa
var portAnswered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

var pingTask = PingProbe.PingAsync(ipAddress, options.ICMPTimeout, options.ICMPAttempts,
options.ICMPBuffer, cancellationToken);
_icmpBuffer, cancellationToken);

var portScanTask = options.PortScanEnabled
? PortScanAsync(ipAddress, portScanParallelOptions, () => portAnswered.TrySetResult(), cancellationToken)
Expand Down
5 changes: 3 additions & 2 deletions Source/NETworkManager.Models/Network/IPScannerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ public record IPScannerOptions
public required int ICMPTimeout { get; init; }

/// <summary>
/// Size of the buffer used in the ping in bytes.
/// Size of the buffer used in the ping in bytes. A size, not the buffer, so options with the same values are
/// equal (records compare arrays by reference).
/// </summary>
public required byte[] ICMPBuffer { get; init; }
public required int ICMPBufferSize { get; init; }

/// <summary>
/// Resolve the hostname for an IP address.
Expand Down
20 changes: 7 additions & 13 deletions Source/NETworkManager.Models/Network/Ping.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
ο»Ώusing System;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using NETworkManager.Utilities;
Expand All @@ -26,9 +25,9 @@ public sealed class Ping
public int Timeout = 4000;

/// <summary>
/// The buffer to send with the ping request. Default is 32 bytes.
/// The size in bytes of the buffer to send with the ping request. Default is 32 bytes.
/// </summary>
public byte[] Buffer = new byte[32];
public int BufferSize = 32;

/// <summary>
/// The Time to Live (TTL) value for the ping request. Default is 64.
Expand Down Expand Up @@ -123,6 +122,8 @@ public void SendAsync(IPAddress ipAddress, CancellationToken cancellationToken)

var errorCount = 0;

var buffer = new byte[BufferSize];

var options = new PingOptions
{
Ttl = TTL,
Expand All @@ -139,7 +140,7 @@ public void SendAsync(IPAddress ipAddress, CancellationToken cancellationToken)
var timestamp = DateTime.Now;

// Send ping
var pingReply = ping.Send(ipAddress, Timeout, Buffer, options);
var pingReply = ping.Send(ipAddress, Timeout, buffer, options);

// Reset the error count (if no exception was thrown)
errorCount = 0;
Expand All @@ -152,15 +153,8 @@ public void SendAsync(IPAddress ipAddress, CancellationToken cancellationToken)
}
else
{
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
OnPingReceived(new PingReceivedArgs(
new PingInfo(timestamp, pingReply.Address, hostname,
pingReply.Buffer.Length, pingReply.RoundtripTime, pingReply.Options!.Ttl,
pingReply.Status)));
else
OnPingReceived(new PingReceivedArgs(
new PingInfo(timestamp, pingReply.Address, hostname,
pingReply.Buffer.Length, pingReply.RoundtripTime, pingReply.Status)));
OnPingReceived(new PingReceivedArgs(
PingInfo.FromSuccessfulReply(timestamp, pingReply, hostname)));
}
}
catch (PingException ex)
Expand Down
16 changes: 16 additions & 0 deletions Source/NETworkManager.Models/Network/PingInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ public PingInfo(DateTime timestamp, IPAddress ipAddress, string hostname, int by
Status = status;
}

/// <summary>
/// Creates a <see cref="PingInfo" /> from a successful ping reply. The TTL is only available for IPv4.
/// </summary>
/// <param name="timestamp">Timestamp when the ping was sent.</param>
/// <param name="pingReply">Successful reply of the ping.</param>
/// <param name="hostname">Hostname of the host.</param>
/// <returns>Information of the ping.</returns>
internal static PingInfo FromSuccessfulReply(DateTime timestamp, PingReply pingReply, string hostname = null)
{
return pingReply.Address.AddressFamily == AddressFamily.InterNetwork
? new PingInfo(timestamp, pingReply.Address, hostname, pingReply.Buffer.Length, pingReply.RoundtripTime,
pingReply.Options!.Ttl, pingReply.Status)
: new PingInfo(timestamp, pingReply.Address, hostname, pingReply.Buffer.Length, pingReply.RoundtripTime,
pingReply.Status);
}

/// <summary>
/// Timestamp when the ping was sent.
/// </summary>
Expand Down
22 changes: 1 addition & 21 deletions Source/NETworkManager.Models/Network/PingProbe.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -43,26 +42,7 @@ public static async Task<PingInfo> PingAsync(IPAddress ipAddress, int timeoutMs,

// Success
if (pingReply is { Status: IPStatus.Success })
{
switch (ipAddress.AddressFamily)
{
case AddressFamily.InterNetwork:
return new PingInfo(
timestamp,
pingReply.Address,
pingReply.Buffer.Length,
pingReply.RoundtripTime,
pingReply.Options!.Ttl,
pingReply.Status);
case AddressFamily.InterNetworkV6:
return new PingInfo(
timestamp,
pingReply.Address,
pingReply.Buffer.Length,
pingReply.RoundtripTime,
pingReply.Status);
}
}
return PingInfo.FromSuccessfulReply(timestamp, pingReply);

// Failed
pingInfo = new PingInfo(timestamp, ipAddress, pingReply.Status);
Expand Down
6 changes: 5 additions & 1 deletion Source/NETworkManager.Models/Network/PortScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public sealed class PortScanner
public PortScanner(PortScannerOptions options)
{
_options = options;
_icmpBuffer = new byte[options.ICMPBufferSize];
}

#endregion
Expand Down Expand Up @@ -69,6 +70,9 @@ public PortScanner(PortScannerOptions options)

private readonly PortScannerOptions _options;

// Data sent with the ping before the scan, created once (see PortScannerOptions.ICMPBufferSize)
private readonly byte[] _icmpBuffer;

/// <summary>
/// Gets the number of ports scanned so far. Thread-safe; may be read from any thread while the scan is
/// running.
Expand Down Expand Up @@ -394,7 +398,7 @@ await Parallel.ForEachAsync(hosts, pingParallelOptions, async (host, ct) =>
private async Task<bool> IsReachableAsync(IPAddress ipAddress, int portCount, CancellationToken cancellationToken)
{
var pingInfo = await PingProbe.PingAsync(ipAddress, _options.ICMPTimeout, _options.ICMPAttempts,
_options.ICMPBuffer, cancellationToken).ConfigureAwait(false);
_icmpBuffer, cancellationToken).ConfigureAwait(false);

if (pingInfo.Status == IPStatus.Success)
return true;
Expand Down
7 changes: 4 additions & 3 deletions Source/NETworkManager.Models/Network/PortScannerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public record PortScannerOptions

/// <summary>
/// Ping every host before its ports are scanned (with <see cref="ICMPAttempts" />, <see cref="ICMPTimeout" /> and
/// <see cref="ICMPBuffer" />), a host that doesn't answer is not scanned.
/// <see cref="ICMPBufferSize" />), a host that doesn't answer is not scanned.
/// </summary>
public required bool PingHostsBeforeScan { get; init; }

Expand All @@ -49,9 +49,10 @@ public record PortScannerOptions
public required int ICMPTimeout { get; init; }

/// <summary>
/// Data sent with the ping before the scan (see <see cref="PingHostsBeforeScan" />).
/// Size in bytes of the data sent with the ping before the scan (see <see cref="PingHostsBeforeScan" />). A size,
/// not the buffer, so options with the same values are equal (records compare arrays by reference).
/// </summary>
public required byte[] ICMPBuffer { get; init; }
public required int ICMPBufferSize { get; init; }

/// <summary>
/// Resolve the hostname for an IP address.
Expand Down
6 changes: 5 additions & 1 deletion Source/NETworkManager.Models/Network/Traceroute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ public sealed class Traceroute

private readonly TracerouteOptions _options;

// Data sent with the ping, created once (see TracerouteOptions.BufferSize)
private readonly byte[] _buffer;

#endregion

#region Constructor

public Traceroute(TracerouteOptions options)
{
_options = options;
_buffer = new byte[options.BufferSize];
}

#endregion
Expand Down Expand Up @@ -55,7 +59,7 @@ public void TraceAsync(IPAddress ipAddress, CancellationToken cancellationToken)
{
stopwatch.Start();

pingReply = ping.Send(ipAddress, _options.Timeout, _options.Buffer,
pingReply = ping.Send(ipAddress, _options.Timeout, _buffer,
new PingOptions { Ttl = i1, DontFragment = _options.DontFragment });

stopwatch.Stop();
Expand Down
8 changes: 4 additions & 4 deletions Source/NETworkManager.Models/Network/TracerouteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ public class TracerouteOptions
/// Create an instance of <see cref="TracerouteOptions" /> with parameters.
/// </summary>
/// <param name="timeout">Timeout in milliseconds after which a ping is considered lost.</param>
/// <param name="buffer">Size of the buffer used in the ping in bytes.</param>
/// <param name="bufferSize">Size of the buffer used in the ping in bytes.</param>
/// <param name="maximumHops">Maximum number of hops between the local and remote computer.</param>
/// <param name="dontFragment">Do not fragment the ping packet.</param>
/// <param name="resolveHostname">Resolve the hostname for an IP address.</param>
/// <param name="checkIPApiIPGeolocation">Check IP geolocation via ip-api.com.</param>
public TracerouteOptions(int timeout, byte[] buffer, int maximumHops, bool dontFragment, bool resolveHostname,
public TracerouteOptions(int timeout, int bufferSize, int maximumHops, bool dontFragment, bool resolveHostname,
bool checkIPApiIPGeolocation)
{
Timeout = timeout;
Buffer = buffer;
BufferSize = bufferSize;
MaximumHops = maximumHops;
DontFragment = dontFragment;
ResolveHostname = resolveHostname;
Expand All @@ -33,7 +33,7 @@ public TracerouteOptions(int timeout, byte[] buffer, int maximumHops, bool dontF
/// <summary>
/// Size of the buffer used in the ping in bytes.
/// </summary>
public byte[] Buffer { get; }
public int BufferSize { get; }

/// <summary>
/// Maximum number of hops between the local and remote computer.
Expand Down
8 changes: 8 additions & 0 deletions Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public static class GlobalStaticConfiguration
// ICMP config: maximum buffer of a ping (.NET's Ping throws for more than 65500 bytes)
public static int ICMPBufferMaximum => 65500;

/// <summary>
/// Limits the buffer size of a ping from the settings to the limits of the input (1 to
/// <see cref="ICMPBufferMaximum" />). A value edited in the settings file would throw or make every ping fail.
/// </summary>
/// <param name="size">Buffer size in bytes from the settings.</param>
/// <returns>Buffer size in bytes within the limits.</returns>
public static int LimitICMPBuffer(int size) => Math.Clamp(size, 1, ICMPBufferMaximum);

// Profile config
public static bool Profile_TagsMatchAny => true;
public static bool Profile_ExpandProfileView => true;
Expand Down
4 changes: 1 addition & 3 deletions Source/NETworkManager/ViewModels/IPScannerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -901,9 +901,7 @@ private static IPScannerOptions CreateScanOptions(int[] ports)
MaxPortThreads = SettingsManager.Current.IPScanner_MaxPortThreads,
ICMPAttempts = SettingsManager.Current.IPScanner_ICMPAttempts,
ICMPTimeout = SettingsManager.Current.IPScanner_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.IPScanner_ICMPBuffer, 1,
GlobalStaticConfiguration.ICMPBufferMaximum)],
ICMPBufferSize = GlobalStaticConfiguration.LimitICMPBuffer(SettingsManager.Current.IPScanner_ICMPBuffer),
ResolveHostname = SettingsManager.Current.IPScanner_ResolveHostname,
PortScanEnabled = SettingsManager.Current.IPScanner_PortScanEnabled,
PortScanPorts = ports,
Expand Down
4 changes: 1 addition & 3 deletions Source/NETworkManager/ViewModels/PingMonitorViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,7 @@ public void Start()
var ping = new Ping
{
Timeout = SettingsManager.Current.PingMonitor_Timeout,
// 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)],
BufferSize = GlobalStaticConfiguration.LimitICMPBuffer(SettingsManager.Current.PingMonitor_Buffer),
TTL = SettingsManager.Current.PingMonitor_TTL,
DontFragment = SettingsManager.Current.PingMonitor_DontFragment,
WaitTime = SettingsManager.Current.PingMonitor_WaitTime
Expand Down
4 changes: 1 addition & 3 deletions Source/NETworkManager/ViewModels/PortScannerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1286,9 +1286,7 @@ private static PortScannerOptions CreateScanOptions()
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)],
ICMPBufferSize = GlobalStaticConfiguration.LimitICMPBuffer(SettingsManager.Current.PortScanner_ICMPBuffer),
ResolveHostname = SettingsManager.Current.PortScanner_ResolveHostname,
ShowAllResults = SettingsManager.Current.PortScanner_ShowAllResults
};
Expand Down
16 changes: 13 additions & 3 deletions Source/NETworkManager/ViewModels/ProfileViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Security;
using System.Threading.Tasks;
using System.Windows.Data;
Expand Down Expand Up @@ -1010,12 +1011,21 @@ private set

/// <summary>
/// Checks if the query type of the profile fits the host (own or inherited), see
/// <see cref="DNSLookup_QueryTypeIsValid"/>.
/// <see cref="DNSLookup_QueryTypeIsValid"/>. Every address the lookup can parse is valid (also shortened IPv4
/// forms like "127.1"), like in the DNS Lookup.
/// </summary>
private void ValidateDNSLookupQueryType()
{
DNSLookup_QueryTypeIsValid = !DNSLookup_OverrideQueryType || DNSLookup_QueryType != QueryType.PTR ||
IPAddressHelper.AreIPAddresses(DNSLookup_InheritHost ? Host : DNSLookup_Host);
if (!DNSLookup_OverrideQueryType || DNSLookup_QueryType != QueryType.PTR)
{
DNSLookup_QueryTypeIsValid = true;
return;
}

var hosts = ((DNSLookup_InheritHost ? Host : DNSLookup_Host) ?? string.Empty).Split(';',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

DNSLookup_QueryTypeIsValid = hosts.Length > 0 && hosts.All(x => IPAddress.TryParse(x, out _));
}

#endregion
Expand Down
Loading
Loading