295 lines
No EOL
9.6 KiB
C#
295 lines
No EOL
9.6 KiB
C#
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<T> where T : Target
|
|
{
|
|
public ScannerState State => _state;
|
|
|
|
private CancellationTokenSource? _cts;
|
|
|
|
private readonly ScannerState _state = new();
|
|
private readonly static Dictionary<string, string>? _vendors = BuildVendorDictionary();
|
|
|
|
public async Task<ScannerState> ScanAsync(
|
|
SynchronizedCollection<T> 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<Task>(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<string, string> BuildVendorDictionary()
|
|
{
|
|
var dictionary = new Dictionary<string, string>();
|
|
|
|
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<PingReply?> 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<string?> 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<string?> ResolveHostnameAsync(IPAddress address, List<IPAddress> 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*(?<Name>[^\s]+)")]
|
|
private static partial Regex NameRegex();
|
|
} |