Files
Print_server/Inspectron.Epson/BonjourDiscovery.cs
2026-01-13 09:06:47 +01:00

579 lines
19 KiB
C#

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; }
}
}