using System.Net; using System.Net.Sockets; using System.Text; namespace Inspectron.Epson; /// /// Bonjour/mDNS (Multicast DNS) Service Discovery /// Implements RFC 6762 (mDNS) and RFC 6763 (DNS-SD) /// Port 5353, Multicast 224.0.0.251 (IPv4) / FF02::FB (IPv6) /// public class BonjourDiscovery { private const int MDNS_PORT = 5353; private const string MDNS_MULTICAST_IPV4 = "224.0.0.251"; //private const string MDNS_MULTICAST_IPV4 = "255.255.255.255"; private const string MDNS_MULTICAST_IPV6 = "FF02::FB"; private const int DEFAULT_TIMEOUT_MS = 10000; private static ushort _nextTransactionId = 1; /// /// Well-known service types for Bonjour/mDNS /// public static class ServiceTypes { public const string PRINTER_IPP = "_ipp._tcp.local."; public const string PRINTER_IPP_TLS = "_ipps._tcp.local."; public const string PRINTER_LPD = "_printer._tcp.local."; public const string PRINTER_PDL_DATASTREAM = "_pdl-datastream._tcp.local."; public const string SCANNER = "_scanner._tcp.local."; public const string HTTP = "_http._tcp.local."; public const string HTTPS = "_https._tcp.local."; public const string SSH = "_ssh._tcp.local."; public const string FTP = "_ftp._tcp.local."; public const string SFTP = "_sftp-ssh._tcp.local."; public const string SMB = "_smb._tcp.local."; public const string AFP = "_afpovertcp._tcp.local."; public const string NFS = "_nfs._tcp.local."; public const string AIRPLAY = "_airplay._tcp.local."; public const string AIRPRINT = "_ipp._tcp.local."; public const string RAOP = "_raop._tcp.local."; public const string HOMEKIT = "_hap._tcp.local."; public const string CHROMECAST = "_googlecast._tcp.local."; public const string SPOTIFY_CONNECT = "_spotify-connect._tcp.local."; public const string MQTT = "_mqtt._tcp.local."; public const string WORKSTATION = "_workstation._tcp.local."; public const string SERVICES_DNS_SD = "_services._dns-sd._udp.local."; } // _dosvc._tcp.local - discovery of all devices /// /// Discovers services of specified type via mDNS/Bonjour /// public static List DiscoverServices( string serviceType = "_ipp._tcp.local.", int timeoutMs = DEFAULT_TIMEOUT_MS, bool useIPv6 = false) { var discoveredServices = new List(); var serviceKeys = new HashSet(); using (var udpClient = new UdpClient()) { try { // Configure socket udpClient.Client.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); if (useIPv6) { udpClient.Client.Bind(new IPEndPoint(IPAddress.IPv6Any, MDNS_PORT)); var multicastAddress = IPAddress.Parse(MDNS_MULTICAST_IPV6); udpClient.JoinMulticastGroup(multicastAddress); } else { udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, MDNS_PORT)); var multicastAddress = IPAddress.Parse(MDNS_MULTICAST_IPV4); udpClient.JoinMulticastGroup(multicastAddress); } udpClient.Client.ReceiveTimeout = timeoutMs; // Build and send mDNS query var transactionId = _nextTransactionId++; byte[] query = BuildMdnsQuery(serviceType, transactionId); var multicastEndpoint = new IPEndPoint( useIPv6 ? IPAddress.Parse(MDNS_MULTICAST_IPV6) : IPAddress.Parse(MDNS_MULTICAST_IPV4), MDNS_PORT); Console.WriteLine($"[mDNS] Querying: {serviceType}"); udpClient.Send(query, query.Length, multicastEndpoint); // Collect responses var startTime = DateTime.Now; while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs) { try { IPEndPoint remoteEndpoint = null; byte[] responseData = udpClient.Receive(ref remoteEndpoint); if (IsValidMdnsResponse(responseData)) { var services = ParseMdnsResponse(responseData, remoteEndpoint.Address.ToString()); foreach (var service in services) { string key = $"{service.ServiceName}.{service.ServiceType}"; if (!serviceKeys.Contains(key)) { discoveredServices.Add(service); serviceKeys.Add(key); Console.WriteLine($"[mDNS] ✓ Found: {service.ToString()}"); } } } } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut) { break; } } Console.WriteLine($"[mDNS] Found {discoveredServices.Count} service(s)"); } catch (Exception ex) { Console.WriteLine($"[mDNS] Error: {ex.Message}"); } } return discoveredServices; } /// /// Discovers services asynchronously /// public static async Task> DiscoverServicesAsync( string serviceType = "_ipp._tcp.local.", int timeoutMs = DEFAULT_TIMEOUT_MS, bool useIPv6 = false) { return await Task.Run(() => DiscoverServices(serviceType, timeoutMs, useIPv6)); } /// /// Discovers all available service types on the network /// public static List DiscoverServiceTypes(int timeoutMs = DEFAULT_TIMEOUT_MS) { Console.WriteLine("[mDNS] Discovering available service types..."); var services = DiscoverServices("_services._dns-sd._udp.local.", timeoutMs); var serviceTypes = new HashSet(); foreach (var service in services) { if (!string.IsNullOrEmpty(service.ServiceType)) { serviceTypes.Add(service.ServiceType); } } return serviceTypes.ToList(); } /// /// Builds an mDNS query packet /// private static byte[] BuildMdnsQuery(string serviceType, ushort transactionId) { using (var ms = new System.IO.MemoryStream()) using (var writer = new System.IO.BinaryWriter(ms)) { // DNS Header writer.Write((byte)((transactionId >> 8) & 0xFF)); writer.Write((byte)(transactionId & 0xFF)); // Flags: Standard query (0x0000) writer.Write((byte)0x00); writer.Write((byte)0x00); // Question count: 1 writer.Write((byte)0x00); writer.Write((byte)0x01); // Answer count: 0 writer.Write((byte)0x00); writer.Write((byte)0x00); // Authority count: 0 writer.Write((byte)0x00); writer.Write((byte)0x00); // Additional count: 0 writer.Write((byte)0x00); writer.Write((byte)0x00); // Question section WriteQName(writer, serviceType); // Type: PTR (12) writer.Write((byte)0x00); writer.Write((byte)0x0C); // Class: IN (1) writer.Write((byte)0x00); writer.Write((byte)0x01); return ms.ToArray(); } } /// /// Writes a DNS name in QNAME format /// private static void WriteQName(System.IO.BinaryWriter writer, string name) { var labels = name.TrimEnd('.').Split('.'); foreach (var label in labels) { if (string.IsNullOrEmpty(label)) continue; writer.Write((byte)label.Length); writer.Write(Encoding.ASCII.GetBytes(label)); } writer.Write((byte)0x00); // Null terminator } /// /// Validates if response is a proper mDNS response /// private static bool IsValidMdnsResponse(byte[] response) { if (response == null || response.Length < 12) return false; // Check if it's a response (QR bit set) return (response[2] & 0x80) != 0; } /// /// Parses mDNS response packet /// private static List ParseMdnsResponse(byte[] response, string sourceIp) { var services = new List(); try { int offset = 0; // Parse header ushort transactionId = ReadUInt16(response, ref offset); ushort flags = ReadUInt16(response, ref offset); ushort questionCount = ReadUInt16(response, ref offset); ushort answerCount = ReadUInt16(response, ref offset); ushort authorityCount = ReadUInt16(response, ref offset); ushort additionalCount = ReadUInt16(response, ref offset); // Skip questions for (int i = 0; i < questionCount; i++) { SkipQName(response, ref offset); offset += 4; // Type and Class } // Parse answers var ptrRecords = new Dictionary>(); var srvRecords = new Dictionary(); var txtRecords = new Dictionary>(); var aRecords = new Dictionary(); // Parse all record sections int totalRecords = answerCount + authorityCount + additionalCount; for (int i = 0; i < totalRecords; i++) { try { string name = ReadQName(response, ref offset); ushort type = ReadUInt16(response, ref offset); ushort rclass = ReadUInt16(response, ref offset); uint ttl = ReadUInt32(response, ref offset); ushort dataLength = ReadUInt16(response, ref offset); int dataStart = offset; switch (type) { case 12: // PTR string ptrTarget = ReadQName(response, ref offset); if (!ptrRecords.ContainsKey(name)) ptrRecords[name] = new List(); ptrRecords[name].Add(ptrTarget); break; case 33: // SRV ushort priority = ReadUInt16(response, ref offset); ushort weight = ReadUInt16(response, ref offset); ushort port = ReadUInt16(response, ref offset); string target = ReadQName(response, ref offset); srvRecords[name] = new SrvRecord { Port = port, Target = target, Priority = priority, Weight = weight, TTL = ttl }; break; case 16: // TXT var txtData = ParseTxtRecord(response, offset, dataLength); txtRecords[name] = txtData; offset += dataLength; break; case 1: // A (IPv4) if (dataLength == 4) { var ip = new IPAddress(new[] { response[offset], response[offset + 1], response[offset + 2], response[offset + 3] }); aRecords[name] = ip.ToString(); offset += 4; } break; case 28: // AAAA (IPv6) if (dataLength == 16) { byte[] ipBytes = new byte[16]; Array.Copy(response, offset, ipBytes, 0, 16); var ip = new IPAddress(ipBytes); aRecords[name] = ip.ToString(); offset += 16; } break; default: offset += dataLength; break; } // Ensure we're at the right position offset = dataStart + dataLength; } catch { break; } } // Build service objects from collected records foreach (var ptrEntry in ptrRecords) { foreach (var instanceName in ptrEntry.Value) { var service = new BonjourService { DiscoveredAt = DateTime.Now, RawResponse = response }; // Parse instance name (e.g., "My Printer._ipp._tcp.local.") ParseServiceName(instanceName, service); // Get SRV record if (srvRecords.ContainsKey(instanceName)) { var srv = srvRecords[instanceName]; service.Port = srv.Port; service.HostName = srv.Target; service.TTL = srv.TTL; // Get IP address from A/AAAA record if (aRecords.ContainsKey(srv.Target)) { service.IPAddress = aRecords[srv.Target]; } } // Get TXT record if (txtRecords.ContainsKey(instanceName)) { service.TxtRecords = txtRecords[instanceName]; } services.Add(service); } } } catch (Exception ex) { Console.WriteLine($"[mDNS Parser] Error: {ex.Message}"); } return services; } /// /// Parses service name components /// private static void ParseServiceName(string fullName, BonjourService service) { service.FullServiceName = fullName; var parts = fullName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 3) { service.ServiceName = parts[0]; service.ServiceType = $"{parts[1]}.{parts[2]}"; if (parts.Length >= 4) { service.Domain = string.Join(".", parts.Skip(3)); } else { service.Domain = "local"; } } } /// /// Parses TXT record key-value pairs /// private static Dictionary ParseTxtRecord(byte[] data, int offset, int length) { var result = new Dictionary(); int end = offset + length; while (offset < end) { byte len = data[offset++]; if (len == 0 || offset + len > end) break; string txt = Encoding.UTF8.GetString(data, offset, len); offset += len; var eqIndex = txt.IndexOf('='); if (eqIndex > 0) { string key = txt.Substring(0, eqIndex); string value = txt.Substring(eqIndex + 1); result[key] = value; } else { result[txt] = ""; } } return result; } /// /// Reads a DNS name from the packet (handles compression) /// private static string ReadQName(byte[] data, ref int offset) { var labels = new List(); int jumps = 0; int maxJumps = 10; bool jumped = false; int returnOffset = -1; while (offset < data.Length) { byte length = data[offset]; // Check for compression pointer if ((length & 0xC0) == 0xC0) { if (!jumped) returnOffset = offset + 2; int pointer = ((length & 0x3F) << 8) | data[offset + 1]; offset = pointer; jumped = true; jumps++; if (jumps > maxJumps) break; continue; } offset++; if (length == 0) break; if (offset + length > data.Length) break; string label = Encoding.ASCII.GetString(data, offset, length); labels.Add(label); offset += length; } if (jumped && returnOffset != -1) offset = returnOffset; return string.Join(".", labels); } /// /// Skips a DNS name in the packet /// private static void SkipQName(byte[] data, ref int offset) { while (offset < data.Length) { byte length = data[offset++]; if (length == 0) break; if ((length & 0xC0) == 0xC0) { offset++; break; } offset += length; } } /// /// Reads 16-bit unsigned integer (big-endian) /// private static ushort ReadUInt16(byte[] data, ref int offset) { ushort value = (ushort)((data[offset] << 8) | data[offset + 1]); offset += 2; return value; } /// /// Reads 32-bit unsigned integer (big-endian) /// private static uint ReadUInt32(byte[] data, ref int offset) { uint value = (uint)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]); offset += 4; return value; } /// /// SRV record data structure /// private class SrvRecord { public ushort Priority { get; set; } public ushort Weight { get; set; } public ushort Port { get; set; } public string Target { get; set; } public uint TTL { get; set; } } }