DiscoveryService.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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. private static readonly TimeSpan DiscoveryTimeout = TimeSpan.FromSeconds(5);
  13. private static readonly TimeSpan DiscoveryRetryInterval = TimeSpan.FromSeconds(1);
  14. [DllImport("iphlpapi.dll", ExactSpelling = true)]
  15. private static extern int SendARP(int destinationIp, int sourceIp, byte[] macAddress, ref int physicalAddressLength);
  16. public async Task<IReadOnlyList<DiscoveredDevice>> DiscoverManyAsync(
  17. string localIPv4,
  18. Action<DiscoveredDevice>? onDeviceDiscovered = null,
  19. CancellationToken cancellationToken = default)
  20. {
  21. if (string.IsNullOrWhiteSpace(localIPv4))
  22. {
  23. return [];
  24. }
  25. using var client = new UdpClient(AddressFamily.InterNetwork)
  26. {
  27. EnableBroadcast = true,
  28. };
  29. client.Client.Bind(new IPEndPoint(IPAddress.Parse(localIPv4), 0));
  30. var request = new
  31. {
  32. protocol_version = 1,
  33. message_type = "discover",
  34. request_id = Guid.NewGuid().ToString(),
  35. client_name = Environment.MachineName,
  36. };
  37. var payload = JsonSerializer.SerializeToUtf8Bytes(request);
  38. var broadcastEndPoint = new IPEndPoint(IPAddress.Broadcast, DiscoveryPort);
  39. await client.SendAsync(payload, payload.Length, broadcastEndPoint);
  40. using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  41. timeoutCts.CancelAfter(DiscoveryTimeout);
  42. var devicesByIp = new Dictionary<string, DiscoveredDevice>(StringComparer.OrdinalIgnoreCase);
  43. var nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
  44. while (!timeoutCts.IsCancellationRequested)
  45. {
  46. try
  47. {
  48. var retryDelay = nextRetryAt - DateTimeOffset.UtcNow;
  49. if (retryDelay <= TimeSpan.Zero)
  50. {
  51. await client.SendAsync(payload, payload.Length, broadcastEndPoint);
  52. nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
  53. continue;
  54. }
  55. using var receiveCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token);
  56. var receiveTask = client.ReceiveAsync(receiveCts.Token).AsTask();
  57. var retryDelayTask = Task.Delay(retryDelay, timeoutCts.Token);
  58. var completedTask = await Task.WhenAny(receiveTask, retryDelayTask);
  59. if (completedTask == retryDelayTask)
  60. {
  61. receiveCts.Cancel();
  62. try
  63. {
  64. await receiveTask;
  65. }
  66. catch (OperationCanceledException)
  67. {
  68. }
  69. if (timeoutCts.IsCancellationRequested)
  70. {
  71. break;
  72. }
  73. await client.SendAsync(payload, payload.Length, broadcastEndPoint);
  74. nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
  75. continue;
  76. }
  77. var result = await receiveTask;
  78. var response = JsonSerializer.Deserialize<DiscoveryResponse>(result.Buffer);
  79. if (response is null
  80. || response.MessageType != "discover_response"
  81. || !TryGetDiscoveredLinkLocalIp(response.Lan2Ip, result.RemoteEndPoint.Address, out var discoveredIp))
  82. {
  83. continue;
  84. }
  85. var mac = response.Mac;
  86. if (string.IsNullOrWhiteSpace(mac))
  87. {
  88. mac = ResolveMacAddress(result.RemoteEndPoint.Address);
  89. }
  90. var device = new DiscoveredDevice
  91. {
  92. DeviceId = response.DeviceId ?? string.Empty,
  93. Hostname = response.Hostname ?? string.Empty,
  94. ServerVersion = response.ServerVersion ?? string.Empty,
  95. Mac = mac ?? string.Empty,
  96. Lan2Ip = discoveredIp,
  97. HttpPort = response.HttpPort,
  98. AuthRequired = response.AuthRequired,
  99. };
  100. devicesByIp[discoveredIp] = device;
  101. onDeviceDiscovered?.Invoke(device);
  102. }
  103. catch (OperationCanceledException)
  104. {
  105. break;
  106. }
  107. }
  108. return devicesByIp.Values.OrderBy(device => device.Lan2Ip).ToList();
  109. }
  110. private static bool TryGetDiscoveredLinkLocalIp(string? lan2Ip, IPAddress remoteAddress, out string discoveredIp)
  111. {
  112. if (IsLinkLocalIPv4(lan2Ip))
  113. {
  114. discoveredIp = lan2Ip!.Trim();
  115. return true;
  116. }
  117. if (IsLinkLocalIPv4(remoteAddress))
  118. {
  119. discoveredIp = remoteAddress.ToString();
  120. return true;
  121. }
  122. discoveredIp = string.Empty;
  123. return false;
  124. }
  125. private static bool IsLinkLocalIPv4(string? ipAddress)
  126. {
  127. return IPAddress.TryParse(ipAddress, out var parsed) && IsLinkLocalIPv4(parsed);
  128. }
  129. private static bool IsLinkLocalIPv4(IPAddress ipAddress)
  130. {
  131. var bytes = ipAddress.GetAddressBytes();
  132. return ipAddress.AddressFamily == AddressFamily.InterNetwork && bytes[0] == 169 && bytes[1] == 254;
  133. }
  134. private static string ResolveMacAddress(IPAddress ipAddress)
  135. {
  136. if (ipAddress.AddressFamily != AddressFamily.InterNetwork)
  137. {
  138. return string.Empty;
  139. }
  140. var macAddress = new byte[6];
  141. var macAddressLength = macAddress.Length;
  142. var destinationIp = BitConverter.ToInt32(ipAddress.GetAddressBytes(), 0);
  143. if (SendARP(destinationIp, 0, macAddress, ref macAddressLength) != 0 || macAddressLength <= 0)
  144. {
  145. return string.Empty;
  146. }
  147. return string.Join(":", macAddress.Take(macAddressLength).Select(value => value.ToString("X2")));
  148. }
  149. private sealed class DiscoveryResponse
  150. {
  151. [JsonPropertyName("message_type")]
  152. public string? MessageType { get; set; }
  153. [JsonPropertyName("device_id")]
  154. public string? DeviceId { get; set; }
  155. [JsonPropertyName("hostname")]
  156. public string? Hostname { get; set; }
  157. [JsonPropertyName("server_version")]
  158. public string? ServerVersion { get; set; }
  159. [JsonPropertyName("mac")]
  160. public string? Mac { get; set; }
  161. [JsonPropertyName("lan2_ip")]
  162. public string? Lan2Ip { get; set; }
  163. [JsonPropertyName("http_port")]
  164. public int HttpPort { get; set; }
  165. [JsonPropertyName("auth_required")]
  166. public bool AuthRequired { get; set; }
  167. }
  168. }