NetworkProbe.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. using System.Net;
  2. using System.Net.NetworkInformation;
  3. using System.Net.Sockets;
  4. using RackPeek.Domain.Resources.Services.Networking;
  5. namespace RackPeek.Domain.Discovery;
  6. /// <summary>The real network IO. Deliberately dumb; see <see cref="INetworkProbe" />.</summary>
  7. public sealed class NetworkProbe : INetworkProbe {
  8. public bool IsSupported => !OperatingSystem.IsBrowser();
  9. public async Task<bool> PingAsync(string ip, TimeSpan timeout, CancellationToken cancellationToken = default) {
  10. try {
  11. using var ping = new Ping();
  12. PingReply reply = await ping.SendPingAsync(ip, timeout, cancellationToken: cancellationToken);
  13. return reply.Status == IPStatus.Success;
  14. }
  15. catch {
  16. // No ICMP privilege, unreachable network, bad address — all mean "no answer".
  17. return false;
  18. }
  19. }
  20. public async Task<bool> TryConnectAsync(
  21. string ip,
  22. int port,
  23. TimeSpan timeout,
  24. CancellationToken cancellationToken = default) {
  25. try {
  26. using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  27. using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  28. cts.CancelAfter(timeout);
  29. await socket.ConnectAsync(IPAddress.Parse(ip), port, cts.Token);
  30. return true;
  31. }
  32. catch {
  33. // Refused, timed out, filtered — for liveness they are all the same "no".
  34. return false;
  35. }
  36. }
  37. public async Task<string?> ReadArpAsync(CancellationToken cancellationToken = default) {
  38. // Linux reads the kernel's file; BSD/macOS answer `arp -an`; Windows' arp.exe
  39. // only knows `-a`. A source only wins if it yields entries the parser can use —
  40. // an exit code alone is not proof (arp.exe printing usage text could exit 0),
  41. // and trusting one would silently cost every host its MAC identity.
  42. foreach (Func<Task<string?>> read in new Func<Task<string?>>[] {
  43. () => SystemProbeCommon.TryReadFileAsync("/proc/net/arp", cancellationToken),
  44. () => SystemProbeCommon.TryRunAsync("arp", "-an", cancellationToken),
  45. () => SystemProbeCommon.TryRunAsync("arp", "-a", cancellationToken)
  46. }) {
  47. var text = await read();
  48. if (text != null && ArpTableParser.Parse(text).Count > 0)
  49. return text;
  50. }
  51. return null;
  52. }
  53. public async Task<string?> ReverseDnsAsync(
  54. string ip,
  55. TimeSpan timeout,
  56. CancellationToken cancellationToken = default) {
  57. try {
  58. // A resolver with a dead PTR zone can sit on the query far longer than the
  59. // whole sweep took; the cap keeps a pile of dead lookups from stalling it.
  60. using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  61. cts.CancelAfter(timeout);
  62. IPHostEntry entry = await Dns.GetHostEntryAsync(ip, cts.Token);
  63. // Some resolvers answer a PTR miss by echoing the address back.
  64. return string.IsNullOrWhiteSpace(entry.HostName) || entry.HostName == ip
  65. ? null
  66. : entry.HostName;
  67. }
  68. catch {
  69. return null;
  70. }
  71. }
  72. public Cidr? LocalSubnet() {
  73. try {
  74. foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) {
  75. if (nic.OperationalStatus != OperationalStatus.Up
  76. || nic.NetworkInterfaceType == NetworkInterfaceType.Loopback)
  77. continue;
  78. IPInterfaceProperties properties = nic.GetIPProperties();
  79. var hasGateway = properties.GatewayAddresses.Any(g =>
  80. g.Address.AddressFamily == AddressFamily.InterNetwork
  81. && !g.Address.Equals(IPAddress.Any));
  82. if (!hasGateway)
  83. continue;
  84. // Skip 169.254/16 self-assigned addresses: a NIC mid-DHCP-renewal can
  85. // carry one alongside its real address, and sweeping that block finds
  86. // nothing by definition.
  87. UnicastIPAddressInformation? address = properties.UnicastAddresses.FirstOrDefault(a =>
  88. a.Address.AddressFamily == AddressFamily.InterNetwork
  89. && !IsLinkLocal(a.Address));
  90. if (address == null)
  91. continue;
  92. return Cidr.Parse($"{address.Address}/{address.PrefixLength}");
  93. }
  94. }
  95. catch {
  96. // Fall through: the caller asks the user for --cidr instead.
  97. }
  98. return null;
  99. }
  100. private static bool IsLinkLocal(IPAddress address) {
  101. var bytes = address.GetAddressBytes();
  102. return bytes.Length == 4 && bytes[0] == 169 && bytes[1] == 254;
  103. }
  104. }