using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using NetworkTool.Client.Models; namespace NetworkTool.Client.Services; public sealed class DiscoveryService { private const int DiscoveryPort = 50000; [DllImport("iphlpapi.dll", ExactSpelling = true)] private static extern int SendARP(int destinationIp, int sourceIp, byte[] macAddress, ref int physicalAddressLength); public async Task> DiscoverManyAsync(string localIPv4, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(localIPv4)) { return []; } using var client = new UdpClient(AddressFamily.InterNetwork) { EnableBroadcast = true, }; client.Client.Bind(new IPEndPoint(IPAddress.Parse(localIPv4), 0)); var request = new { protocol_version = 1, message_type = "discover", request_id = Guid.NewGuid().ToString(), client_name = Environment.MachineName, }; var payload = JsonSerializer.SerializeToUtf8Bytes(request); await client.SendAsync(payload, payload.Length, new IPEndPoint(IPAddress.Broadcast, DiscoveryPort)); using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(TimeSpan.FromSeconds(3)); var devicesByIp = new Dictionary(StringComparer.OrdinalIgnoreCase); while (!timeoutCts.IsCancellationRequested) { try { var result = await client.ReceiveAsync(timeoutCts.Token); var response = JsonSerializer.Deserialize(result.Buffer); if (response is null || response.MessageType != "discover_response" || string.IsNullOrWhiteSpace(response.Lan2Ip) || !response.Lan2Ip.StartsWith("169.254.", StringComparison.Ordinal)) { continue; } var mac = response.Mac; if (string.IsNullOrWhiteSpace(mac)) { mac = ResolveMacAddress(result.RemoteEndPoint.Address); } devicesByIp[response.Lan2Ip] = new DiscoveredDevice { DeviceId = response.DeviceId ?? string.Empty, Hostname = response.Hostname ?? string.Empty, ServerVersion = response.ServerVersion ?? string.Empty, Mac = mac ?? string.Empty, Lan2Ip = response.Lan2Ip, AuthRequired = response.AuthRequired, }; } catch (OperationCanceledException) { break; } } return devicesByIp.Values.OrderBy(device => device.Lan2Ip).ToList(); } private static string ResolveMacAddress(IPAddress ipAddress) { if (ipAddress.AddressFamily != AddressFamily.InterNetwork) { return string.Empty; } var macAddress = new byte[6]; var macAddressLength = macAddress.Length; var destinationIp = BitConverter.ToInt32(ipAddress.GetAddressBytes(), 0); if (SendARP(destinationIp, 0, macAddress, ref macAddressLength) != 0 || macAddressLength <= 0) { return string.Empty; } return string.Join(":", macAddress.Take(macAddressLength).Select(value => value.ToString("X2"))); } private sealed class DiscoveryResponse { [JsonPropertyName("message_type")] public string? MessageType { get; set; } [JsonPropertyName("device_id")] public string? DeviceId { get; set; } [JsonPropertyName("hostname")] public string? Hostname { get; set; } [JsonPropertyName("server_version")] public string? ServerVersion { get; set; } [JsonPropertyName("mac")] public string? Mac { get; set; } [JsonPropertyName("lan2_ip")] public string? Lan2Ip { get; set; } [JsonPropertyName("auth_required")] public bool AuthRequired { get; set; } } }