DiscoveryService.cs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. using System.Net;
  2. using System.Net.Sockets;
  3. using System.Runtime.InteropServices;
  4. using System.Text;
  5. using System.Text.Json;
  6. using System.Text.Json.Serialization;
  7. using NetworkTool.Client.Models;
  8. namespace NetworkTool.Client.Services;
  9. public sealed class DiscoveryService
  10. {
  11. private const int DiscoveryPort = 50000;
  12. [DllImport("iphlpapi.dll", ExactSpelling = true)]
  13. private static extern int SendARP(int destinationIp, int sourceIp, byte[] macAddress, ref int physicalAddressLength);
  14. public async Task<IReadOnlyList<DiscoveredDevice>> DiscoverManyAsync(string localIPv4, CancellationToken cancellationToken = default)
  15. {
  16. if (string.IsNullOrWhiteSpace(localIPv4))
  17. {
  18. return [];
  19. }
  20. using var client = new UdpClient(AddressFamily.InterNetwork)
  21. {
  22. EnableBroadcast = true,
  23. };
  24. client.Client.Bind(new IPEndPoint(IPAddress.Parse(localIPv4), 0));
  25. var request = new
  26. {
  27. protocol_version = 1,
  28. message_type = "discover",
  29. request_id = Guid.NewGuid().ToString(),
  30. client_name = Environment.MachineName,
  31. };
  32. var payload = JsonSerializer.SerializeToUtf8Bytes(request);
  33. await client.SendAsync(payload, payload.Length, new IPEndPoint(IPAddress.Broadcast, DiscoveryPort));
  34. using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  35. timeoutCts.CancelAfter(TimeSpan.FromSeconds(3));
  36. var devicesByIp = new Dictionary<string, DiscoveredDevice>(StringComparer.OrdinalIgnoreCase);
  37. while (!timeoutCts.IsCancellationRequested)
  38. {
  39. try
  40. {
  41. var result = await client.ReceiveAsync(timeoutCts.Token);
  42. var response = JsonSerializer.Deserialize<DiscoveryResponse>(result.Buffer);
  43. if (response is null
  44. || response.MessageType != "discover_response"
  45. || string.IsNullOrWhiteSpace(response.Lan2Ip)
  46. || !response.Lan2Ip.StartsWith("169.254.", StringComparison.Ordinal))
  47. {
  48. continue;
  49. }
  50. var mac = response.Mac;
  51. if (string.IsNullOrWhiteSpace(mac))
  52. {
  53. mac = ResolveMacAddress(result.RemoteEndPoint.Address);
  54. }
  55. devicesByIp[response.Lan2Ip] = new DiscoveredDevice
  56. {
  57. DeviceId = response.DeviceId ?? string.Empty,
  58. Hostname = response.Hostname ?? string.Empty,
  59. ServerVersion = response.ServerVersion ?? string.Empty,
  60. Mac = mac ?? string.Empty,
  61. Lan2Ip = response.Lan2Ip,
  62. AuthRequired = response.AuthRequired,
  63. };
  64. }
  65. catch (OperationCanceledException)
  66. {
  67. break;
  68. }
  69. }
  70. return devicesByIp.Values.OrderBy(device => device.Lan2Ip).ToList();
  71. }
  72. private static string ResolveMacAddress(IPAddress ipAddress)
  73. {
  74. if (ipAddress.AddressFamily != AddressFamily.InterNetwork)
  75. {
  76. return string.Empty;
  77. }
  78. var macAddress = new byte[6];
  79. var macAddressLength = macAddress.Length;
  80. var destinationIp = BitConverter.ToInt32(ipAddress.GetAddressBytes(), 0);
  81. if (SendARP(destinationIp, 0, macAddress, ref macAddressLength) != 0 || macAddressLength <= 0)
  82. {
  83. return string.Empty;
  84. }
  85. return string.Join(":", macAddress.Take(macAddressLength).Select(value => value.ToString("X2")));
  86. }
  87. private sealed class DiscoveryResponse
  88. {
  89. [JsonPropertyName("message_type")]
  90. public string? MessageType { get; set; }
  91. [JsonPropertyName("device_id")]
  92. public string? DeviceId { get; set; }
  93. [JsonPropertyName("hostname")]
  94. public string? Hostname { get; set; }
  95. [JsonPropertyName("server_version")]
  96. public string? ServerVersion { get; set; }
  97. [JsonPropertyName("mac")]
  98. public string? Mac { get; set; }
  99. [JsonPropertyName("lan2_ip")]
  100. public string? Lan2Ip { get; set; }
  101. [JsonPropertyName("auth_required")]
  102. public bool AuthRequired { get; set; }
  103. }
  104. }