using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace LanDiscovery.Services { public class NetBiosResult { public string Hostname { get; set; } = ""; public string MacAddress { get; set; } = ""; public bool Found { get; set; } } public class NetBiosScanner { private const int Port = 137; private const int TimeoutMs = 2000; // 2 seconds timeout // NetBIOS Node Status Query for "*" // Transaction ID (2) + Flags (2) + Questions (2) + Answers (2) + Auth (2) + Add (2) // + Name (34 bytes: CKAAAAAAAAAAAA... + NULL) + Type (2) + Class (2) private static readonly byte[] NodeStatusRequest = new byte[] { 0x00, 0x00, // Transaction ID 0x00, 0x00, // Flags (Query) 0x00, 0x01, // Questions: 1 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Others: 0 // Name: "*" encoded (CKAAAAAAAAAAAAAA...) 0x20, 0x43, 0x4B, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, // Null terminator 0x00, 0x21, // Type: NBSTAT (33) 0x00, 0x01 // Class: IN (1) }; public async Task ScanAsync(IPAddress ip) { using var udp = new UdpClient(); udp.Client.ReceiveTimeout = TimeoutMs; udp.Client.SendTimeout = TimeoutMs; // Generate random Transaction ID var request = (byte[])NodeStatusRequest.Clone(); new Random().NextBytes(request.AsSpan(0, 2)); try { await udp.SendAsync(request, request.Length, new IPEndPoint(ip, Port)); // Wait for receive with timeout (using Task.WhenAny trick or UdpClient.ReceiveAsync) // Receiving async doesn't support timeout natively easily without cancellation token logic var receiveTask = udp.ReceiveAsync(); var completedTask = await Task.WhenAny(receiveTask, Task.Delay(TimeoutMs)); if (completedTask == receiveTask) { var result = await receiveTask; return ParseResponse(result.Buffer); } } catch { } return new NetBiosResult { Found = false }; } private NetBiosResult ParseResponse(byte[] data) { try { // Basic validation: Transaction ID matches? (Skip for now) // Valid header length if (data.Length < 56) return new NetBiosResult { Found = false }; // Locate the MAC address. In NBSTAT response, it's at the end of the data section usually. // Structure: Header (12) + Name (34) + Type (2) + Class (2) + TTL (4) + RdLength (2) + NumNames (1) // + Names (18 * NumNames) + UnitId (6 bytes = MAC) // Offset calculation: // Header (12) // RR Name (skip until 0x00 or fixed length?) Response echoes query usually. // Actually simplest way is to find the Unit ID at the end of the statistics field. // Let's parse properly. // Question section (usually echoed): Name (34) + Type (2) + Class (2) = 38 bytes // Total so far: 12 + 38 = 50. // Answer section: // Name (1 byte pointer 0xC00C or similar, OR 34 bytes) // Let's assume offset 12 starts Answer 1 if question is stripped? No, standard DNS format. // But NBSTAT response typically has 0 Questions if successful? Or 1? // Let's try to jump to the stats. // The 'Unit ID' (MAC) is the last 6 bytes of the data. // However, the Name Table is variable length. // NumNames is at offset [Header + Question + 10 bytes of RR fixed fields + 1 byte NumNames] // Wait, let's use a simpler heuristic or just check the last part? No. // Let's try parsing the sections. int offset = 12; // Skip Questions int qCount = (data[4] << 8) | data[5]; for (int i = 0; i < qCount; i++) { // Skip name (label sequence) // NetBIOS names are 32 bytes + lengths? // Usually simple "20 + ..." // Just scan for 0x00? while (offset < data.Length && data[offset] != 0) offset++; offset++; // Skip null offset += 4; // Skip Type, Class } // Answer parsing int ansCount = (data[6] << 8) | data[7]; if (ansCount > 0) { // Name (Compression 0xC0 or sequence) if ((data[offset] & 0xC0) == 0xC0) offset += 2; // Pointer else { while (offset < data.Length && data[offset] != 0) offset++; offset++; } // Type (2), Class (2), TTL (4), RdLen (2) offset += 8; int rdLen = (data[offset] << 8) | data[offset+1]; offset += 2; int dataStart = offset; // Inside RDATA: NumNames (1) int numNames = data[offset]; offset += 1; string bestName = ""; // Parse Names (18 bytes each: 15 Name + 1 Type + 2 Flags) for (int n = 0; n < numNames; n++) { string name = Encoding.ASCII.GetString(data, offset, 15).Trim(); // Type is at offset+15 // 0x00 = Workstation, 0x20 = Server byte type = data[offset+15]; if (type == 0x00 || type == 0x20) { if (string.IsNullOrEmpty(bestName) || !bestName.StartsWith("IS~")) bestName = name; } offset += 18; } // Unit ID (MAC) is next 6 bytes // Statistics (variable?) No, Unit ID is usually at the end of the RDATA block // Actually, Unit ID is MAC address. byte[] macBytes = new byte[6]; Array.Copy(data, offset, macBytes, 0, 6); string mac = BitConverter.ToString(macBytes); return new NetBiosResult { Found = true, MacAddress = mac, Hostname = bestName }; } } catch {} return new NetBiosResult { Found = false }; } } }