Add project files.
This commit is contained in:
579
Inspectron.Epson/BonjourDiscovery.cs
Normal file
579
Inspectron.Epson/BonjourDiscovery.cs
Normal file
@@ -0,0 +1,579 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known service types for Bonjour/mDNS
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Discovers services of specified type via mDNS/Bonjour
|
||||
/// </summary>
|
||||
public static List<BonjourService> DiscoverServices(
|
||||
string serviceType = "_ipp._tcp.local.",
|
||||
int timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
bool useIPv6 = false)
|
||||
{
|
||||
var discoveredServices = new List<BonjourService>();
|
||||
var serviceKeys = new HashSet<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers services asynchronously
|
||||
/// </summary>
|
||||
public static async Task<List<BonjourService>> DiscoverServicesAsync(
|
||||
string serviceType = "_ipp._tcp.local.",
|
||||
int timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
bool useIPv6 = false)
|
||||
{
|
||||
return await Task.Run(() => DiscoverServices(serviceType, timeoutMs, useIPv6));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers all available service types on the network
|
||||
/// </summary>
|
||||
public static List<string> 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<string>();
|
||||
|
||||
foreach (var service in services)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(service.ServiceType))
|
||||
{
|
||||
serviceTypes.Add(service.ServiceType);
|
||||
}
|
||||
}
|
||||
|
||||
return serviceTypes.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an mDNS query packet
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a DNS name in QNAME format
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if response is a proper mDNS response
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses mDNS response packet
|
||||
/// </summary>
|
||||
private static List<BonjourService> ParseMdnsResponse(byte[] response, string sourceIp)
|
||||
{
|
||||
var services = new List<BonjourService>();
|
||||
|
||||
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<string, List<string>>();
|
||||
var srvRecords = new Dictionary<string, SrvRecord>();
|
||||
var txtRecords = new Dictionary<string, Dictionary<string, string>>();
|
||||
var aRecords = new Dictionary<string, string>();
|
||||
|
||||
// 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<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses service name components
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses TXT record key-value pairs
|
||||
/// </summary>
|
||||
private static Dictionary<string, string> ParseTxtRecord(byte[] data, int offset, int length)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DNS name from the packet (handles compression)
|
||||
/// </summary>
|
||||
private static string ReadQName(byte[] data, ref int offset)
|
||||
{
|
||||
var labels = new List<string>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips a DNS name in the packet
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads 16-bit unsigned integer (big-endian)
|
||||
/// </summary>
|
||||
private static ushort ReadUInt16(byte[] data, ref int offset)
|
||||
{
|
||||
ushort value = (ushort)((data[offset] << 8) | data[offset + 1]);
|
||||
offset += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads 32-bit unsigned integer (big-endian)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SRV record data structure
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user