using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using RackPeek.Domain.Resources.Services.Networking; namespace RackPeek.Domain.Discovery; /// The real network IO. Deliberately dumb; see . public sealed class NetworkProbe : INetworkProbe { public bool IsSupported => !OperatingSystem.IsBrowser(); public async Task PingAsync(string ip, TimeSpan timeout, CancellationToken cancellationToken = default) { try { using var ping = new Ping(); PingReply reply = await ping.SendPingAsync(ip, timeout, cancellationToken: cancellationToken); return reply.Status == IPStatus.Success; } catch { // No ICMP privilege, unreachable network, bad address — all mean "no answer". return false; } } public async Task TryConnectAsync( string ip, int port, TimeSpan timeout, CancellationToken cancellationToken = default) { try { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(timeout); await socket.ConnectAsync(IPAddress.Parse(ip), port, cts.Token); return true; } catch { // Refused, timed out, filtered — for liveness they are all the same "no". return false; } } public async Task ReadArpAsync(CancellationToken cancellationToken = default) { // Linux reads the kernel's file; BSD/macOS answer `arp -an`; Windows' arp.exe // only knows `-a`. A source only wins if it yields entries the parser can use — // an exit code alone is not proof (arp.exe printing usage text could exit 0), // and trusting one would silently cost every host its MAC identity. foreach (Func> read in new Func>[] { () => SystemProbeCommon.TryReadFileAsync("/proc/net/arp", cancellationToken), () => SystemProbeCommon.TryRunAsync("arp", "-an", cancellationToken), () => SystemProbeCommon.TryRunAsync("arp", "-a", cancellationToken) }) { var text = await read(); if (text != null && ArpTableParser.Parse(text).Count > 0) return text; } return null; } public async Task ReverseDnsAsync( string ip, TimeSpan timeout, CancellationToken cancellationToken = default) { try { // A resolver with a dead PTR zone can sit on the query far longer than the // whole sweep took; the cap keeps a pile of dead lookups from stalling it. using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(timeout); IPHostEntry entry = await Dns.GetHostEntryAsync(ip, cts.Token); // Some resolvers answer a PTR miss by echoing the address back. return string.IsNullOrWhiteSpace(entry.HostName) || entry.HostName == ip ? null : entry.HostName; } catch { return null; } } public Cidr? LocalSubnet() { try { foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.OperationalStatus != OperationalStatus.Up || nic.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue; IPInterfaceProperties properties = nic.GetIPProperties(); var hasGateway = properties.GatewayAddresses.Any(g => g.Address.AddressFamily == AddressFamily.InterNetwork && !g.Address.Equals(IPAddress.Any)); if (!hasGateway) continue; // Skip 169.254/16 self-assigned addresses: a NIC mid-DHCP-renewal can // carry one alongside its real address, and sweeping that block finds // nothing by definition. UnicastIPAddressInformation? address = properties.UnicastAddresses.FirstOrDefault(a => a.Address.AddressFamily == AddressFamily.InterNetwork && !IsLinkLocal(a.Address)); if (address == null) continue; return Cidr.Parse($"{address.Address}/{address.PrefixLength}"); } } catch { // Fall through: the caller asks the user for --cidr instead. } return null; } private static bool IsLinkLocal(IPAddress address) { var bytes = address.GetAddressBytes(); return bytes.Length == 4 && bytes[0] == 169 && bytes[1] == 254; } }