#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Vanara.Extensions; using Vanara.InteropServices; using Vanara.PInvoke; using static Vanara.PInvoke.DnsApi; namespace Vanara.Net; /// Represents a DNS service. /// public class DnsService : IDisposable { private readonly IntPtr ctx; private readonly AutoResetEvent evt = new(false); private readonly SafePDNS_SERVICE_INSTANCE pSvcInst; private bool disposed = false, registering = false; private Win32Error err = 0; private DNS_SERVICE_REGISTER_REQUEST req; /// Initializes a new instance of the class. /// The name of the service. /// The name of the host of the service. /// The port. /// The service priority. /// The service weight. /// The service-associated address and port. /// A dictionary of property keys and values. public DnsService(string serviceName, string hostName, ushort port, [Optional] ushort priority, [Optional] ushort weight, [Optional] IPAddress? address, [Optional] IDictionary? properties) { using SafeCoTaskMemHandle v4 = address is null ? SafeCoTaskMemHandle.Null : new(address.MapToIPv4().GetAddressBytes()); using SafeCoTaskMemHandle v6 = address is null ? SafeCoTaskMemHandle.Null : new(address.MapToIPv6().GetAddressBytes()); pSvcInst = DnsServiceConstructInstance(serviceName, hostName, v4, v6, port, priority, weight, (uint)(properties?.Count ?? 0), properties?.Keys.ToArray(), properties?.Values.ToArray()); ctx = (IntPtr)GCHandle.Alloc(this, GCHandleType.Normal); } internal DnsService(IntPtr pInst) { pSvcInst = new SafePDNS_SERVICE_INSTANCE(pInst); ctx = (IntPtr)GCHandle.Alloc(this, GCHandleType.Normal); } /// A string that represents the name of the host of the service. public string HostName => pSvcInst.pszHostName; /// /// A string that represents the service name. This is a fully qualified domain name that begins with a service name, and ends with /// ".local". It takes the generalized form "<ServiceName>._<ServiceType>._<TransportProtocol>.local". For example, "MyMusicServer._http._tcp.local". /// public string InstanceName => pSvcInst.pszInstanceName; /// A value that contains the interface index on which the service was discovered. public uint InterfaceIndex => pSvcInst.dwInterfaceIndex; /// The service-associated IPv4 address, if defined. public IPAddress? Ip4Address => pSvcInst.ip4Address?.Address.MapToIPv4(); /// The service-associated IPv6 address, if defined. public IPAddress? Ip6Address => pSvcInst.ip6Address?.Address.MapToIPv6(); /// Gets a value indicating whether this instance is registered. /// if this instance is registered; otherwise, . public bool IsRegistered => req.Version != 0; /// A value that represents the port on which the service is running. public ushort Port => pSvcInst.wPort; /// A value that represents the service priority. public ushort Priority => pSvcInst.wPriority; /// The DNS service properties. public IReadOnlyDictionary Properties => pSvcInst.properties; /// A value that represents the service weight. public ushort Weight => pSvcInst.wWeight; /// Used to obtain more information about a service advertised on the local network. /// /// The service name. This is a fully qualified domain name that begins with a service name, and ends with ".local". It takes the /// generalized form "<ServiceName>._<ServiceType>._<TransportProtocol>.local". For example, "MyMusicServer._http._tcp.local". /// /// The interface over which the query is sent. If , then all interfaces will be considered. /// A cancellation token that can be used to cancel a pending asynchronous resolve operation. /// If successful, returns a new instance; otherwise, throws the appropriate DNS-specific exception. /// This function is asynchronous. public static async Task ResolveAsync(string serviceName, NetworkInterface? adapter = null, CancellationToken cancellationToken = default) { using ManualResetEvent evt = new(false); Win32Error err = 0; IntPtr result = default; DNS_SERVICE_RESOLVE_REQUEST res = new() { Version = DNS_QUERY_REQUEST_VERSION1, InterfaceIndex = (uint)(adapter?.GetIPProperties().GetIPv4Properties().Index ?? 0), QueryName = serviceName, pResolveCompletionCallback = ResolveCallback, }; DnsServiceResolve(res, out DNS_SERVICE_CANCEL c).ThrowUnless(Win32Error.DNS_REQUEST_PENDING); await Task.Run(() => { if (WaitHandle.WaitAny(new[] { cancellationToken.WaitHandle, evt }) == 0) DnsServiceResolveCancel(c); }); return result != IntPtr.Zero ? new DnsService(result) : throw err.GetException(); void ResolveCallback(Win32Error Status, IntPtr pQueryContext, IntPtr pInstance) { if ((err = Status).Succeeded) result = pInstance; evt.Set(); } } /// Used to remove a registered service. /// If not successful, throws an appropriate DNS-specific exception. /// This function is asynchronous. public async Task DeRegisterAsync() { if (!IsRegistered) return; if (registering) throw new InvalidOperationException("Service is already being deregistered."); registering = true; DnsServiceDeRegister(req, IntPtr.Zero); await Task.Run(() => evt.WaitOne()); registering = false; req = default; err.ThrowIfFailed(); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { if (disposed) return; disposed = true; pSvcInst?.Dispose(); if (ctx != IntPtr.Zero) GCHandle.FromIntPtr(ctx).Free(); } /// Used to register a discoverable service on this device. /// /// if the DNS protocol should be used to advertise the service; if the mDNS protocol /// should be used. /// /// /// An optional value that contains the network interface over which the service is to be advertised. If , then all /// interfaces will be considered. /// /// A cancellation token that can be used to cancel the asynchonous operation. /// Service is already registered. /// /// This function is asynchronous. To deregister the service, call DnsServiceDeRegister. The registration is tied to the lifetime of the /// calling process. If the process goes away, the service will be automatically deregistered. /// public async Task RegisterAsync(bool unicastEnabled = false, NetworkInterface? adapter = null, CancellationToken cancellationToken = default) { if (IsRegistered) throw new InvalidOperationException("Service is already registered."); if (registering) throw new InvalidOperationException("Service is already being registered."); req = new DNS_SERVICE_REGISTER_REQUEST() { Version = DNS_QUERY_REQUEST_VERSION1, InterfaceIndex = (uint)(adapter?.GetIPProperties().GetIPv4Properties().Index ?? 0), pServiceInstance = pSvcInst, pQueryContext = ctx, pRegisterCompletionCallback = RegCallback, unicastEnabled = unicastEnabled }; registering = true; DnsServiceRegister(req, out DNS_SERVICE_CANCEL svcCancel).ThrowUnless(Win32Error.DNS_REQUEST_PENDING); await Task.Run(() => { if (WaitHandle.WaitAny(new[] { cancellationToken.WaitHandle, evt }) == 0) DnsServiceResolveCancel(svcCancel); }); registering = false; err.ThrowIfFailed(); } private static void RegCallback(Win32Error Status, IntPtr pQueryContext, IntPtr pInstance) { DnsService svc = (DnsService)GCHandle.FromIntPtr(pQueryContext).Target; using SafePDNS_SERVICE_INSTANCE i = new(pInstance); svc.err = Status; svc.evt.Set(); } }