using Discovery.Models; using System.Diagnostics; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.RegularExpressions; namespace Discovery.Services; public partial class Scanner where T : Target { public ScannerState State => _state; private CancellationTokenSource? _cts; private readonly ScannerState _state = new(); private readonly static Dictionary? _vendors = BuildVendorDictionary(); public async Task ScanAsync( SynchronizedCollection targets, ScannerOptions options, ScannerEvents events, CancellationToken cancellationToken = default) { // cancel if already running if (_state.Operation == ScanOperation.Scanning && _cts != null) { await _cts.CancelAsync(); return _state; } // renew state _state.Operation = ScanOperation.Scanning; _state.Progress = 0; // set cts _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var orderedTargets = targets.InternalList.OrderBy(p => p.Index); var start = Stopwatch.GetTimestamp(); try { var tasks = new List(targets.InternalList.Count); var con = new SemaphoreSlim(options.Concurrency); var pingOptions = new PingOptions { DontFragment = options.DontFragment, Ttl = options.Ttl }; foreach (var target in targets.InternalList) { tasks.Add(Task.Run(async () => { try { await con.WaitAsync(cancellationToken).ConfigureAwait(false); target.Ping = await PingAsync(target.IPAddress, options.PingTimeout, null, pingOptions, cancellationToken).ConfigureAwait(false); target.Mac = await ArpAsync(target.IPAddress, options.ArpTimeout, cancellationToken).ConfigureAwait(false); if (target.Mac != null) { target.Arp = true; target.Vendor = ResolveVendor(target.Mac!); } // local resolve fix string? sAddress = null; if (TryGetPrimaryInterface(out var pInterface, out var pAddress)) sAddress = pAddress?.ToString(); if (target.Address == sAddress) { target.Hostname = Dns.GetHostName(); } else if (options.Nameservers == null) { target.Hostname = "N/A"; } else { if (target.Online || (!target.Online && options.ResolveAll)) { var result = await ResolveHostnameAsync(target.IPAddress, options.Nameservers, cancellationToken).ConfigureAwait(false); if (result != null) target.Hostname = result; } } } catch (OperationCanceledException) { } finally { _state.Progress += (100 / (double)targets.InternalList.Count); if (events.OnStateUpdate != null) await events.OnStateUpdate(_state); con.Release(); } }, cancellationToken)); } await Task.WhenAll(tasks); _state.Operation = ScanOperation.Idle; } catch (OperationCanceledException) { _state.Operation = ScanOperation.Aborted; } finally { _state.Progress = 100; _state.Elapsed = Stopwatch.GetElapsedTime(start); _cts = null; } return _state; } public async Task CancelAsync() { if (_cts == null) return; await _cts.CancelAsync().ConfigureAwait(false); _state.Operation = ScanOperation.Aborted; } public bool TryGetPrimaryInterface(out NetworkInterface? pInterface, out IPAddress? pAddress) { pInterface = null; pAddress = null; try { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0); socket.Connect("8.8.8.8", 65530); var ep = socket.LocalEndPoint as IPEndPoint; if (ep?.Address == null) return false; pAddress = ep.Address; pInterface = NetworkInterface .GetAllNetworkInterfaces() .FirstOrDefault(p => p .GetIPProperties().UnicastAddresses.Any(x => x.Address.ToString() == ep.Address.ToString())); if (pInterface == null) return false; return true; } catch { return false; } } public NetworkInfo? GetPrimaryNetwork() { if (!TryGetPrimaryInterface(out var pInterface, out var pAddress)) return null; var pUnicast = pInterface!.GetIPProperties().UnicastAddresses.Single(p => p.Address.ToString() == pAddress!.ToString()); if (!IPNetwork2.TryParse(pAddress!, pUnicast!.IPv4Mask, out var net)) return null; var nameservers = pInterface!.GetIPProperties().DnsAddresses; return new NetworkInfo(pAddress!, net.Network, net.Cidr, [.. nameservers]); } private static Dictionary BuildVendorDictionary() { var dictionary = new Dictionary(); using var fileStream = new FileStream("vendors.json", FileMode.Open, FileAccess.Read); var vendors = JsonSerializer.Deserialize( fileStream, VendorsJsonContext.Default.VendorArray ) ?? throw new InvalidOperationException("vendors.json"); foreach (var item in vendors) { if (item is null) continue; if (item.MacPrefix == null || item.VendorName == null) continue; dictionary.Add(item.MacPrefix, item.VendorName); } return dictionary; } private static async Task PingAsync(IPAddress address, TimeSpan timeout, byte[]? buffer, PingOptions? options, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) return null; using var ping = new Ping(); var reply = await ping.SendPingAsync(address, timeout, buffer, options, cancellationToken: cancellationToken).ConfigureAwait(false); ping.SendAsyncCancel(); return reply; } private static async Task ArpAsync(IPAddress address, TimeSpan timeout, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) return null; if (address.AddressFamily != AddressFamily.InterNetwork) return null; // IPv4 only // 1) Try ARP cache (GetIpNetTable) if (WinArpInterop.TryGetFromArpCache(address, out var mac)) return mac; // 2) Active probe on a dedicated thread; still honor the timeout using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var probeTask = Task.Factory.StartNew( () => WinArpInterop.ProbeWithSendArp(address), linked.Token, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); var completed = await Task.WhenAny(probeTask, Task.Delay(timeout, linked.Token)).ConfigureAwait(false); if (completed != probeTask) return null; linked.Cancel(); // cancel the delay if still pending return await probeTask.ConfigureAwait(false); } private static async Task ResolveHostnameAsync(IPAddress address, List nameservers, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) return null; string? hostname = null; for (int i = 0; i < nameservers.Count; i++) { using var process = new Process { StartInfo = new ProcessStartInfo { FileName = "nslookup", Arguments = $"{address} {nameservers[i]}", RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true } }; process.Start(); var output = await process.StandardOutput .ReadToEndAsync(cancellationToken) .ConfigureAwait(false); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); var match = NameRegex().Match(output); if (match.Success) hostname = match.Groups["Name"].Value; if (hostname != null) break; } return hostname; } private static string? ResolveVendor(string mac) { if (!_vendors!.TryGetValue(mac[..8].ToUpperInvariant(), out string? vendor)) return null; return vendor; } [GeneratedRegex(@"Name:\s*(?[^\s]+)")] private static partial Regex NameRegex(); }