Skip to content

Commit

Permalink
Added new Vanara.Net assembly with support for DNS services and DHCP …
Browse files Browse the repository at this point in the history
…clients.
  • Loading branch information
dahall committed Sep 16, 2022
1 parent ad56403 commit 1e24865
Show file tree
Hide file tree
Showing 9 changed files with 954 additions and 1 deletion.
578 changes: 578 additions & 0 deletions Net/DhcpClient.cs

Large diffs are not rendered by default.

205 changes: 205 additions & 0 deletions Net/DnsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#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;

/// <summary>Represents a DNS service.</summary>
/// <seealso cref="System.IDisposable"/>
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;

/// <summary>Initializes a new instance of the <see cref="DnsService"/> class.</summary>
/// <param name="serviceName">The name of the service.</param>
/// <param name="hostName">The name of the host of the service.</param>
/// <param name="port">The port.</param>
/// <param name="priority">The service priority.</param>
/// <param name="weight">The service weight.</param>
/// <param name="address">The service-associated address and port.</param>
/// <param name="properties">A dictionary of property keys and values.</param>
public DnsService(string serviceName, string hostName, ushort port, [Optional] ushort priority,
[Optional] ushort weight, [Optional] IPAddress? address, [Optional] IDictionary<string, string>? 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);
}

/// <summary>A string that represents the name of the host of the service.</summary>
public string HostName => pSvcInst.pszHostName;

/// <summary>
/// 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 "&lt;ServiceName&gt;._&lt;ServiceType&gt;._&lt;TransportProtocol&gt;.local". For example, "MyMusicServer._http._tcp.local".
/// </summary>
public string InstanceName => pSvcInst.pszInstanceName;

/// <summary>A value that contains the interface index on which the service was discovered.</summary>
public uint InterfaceIndex => pSvcInst.dwInterfaceIndex;

/// <summary>The service-associated IPv4 address, if defined.</summary>
public IPAddress? Ip4Address => pSvcInst.ip4Address?.Address.MapToIPv4();

/// <summary>The service-associated IPv6 address, if defined.</summary>
public IPAddress? Ip6Address => pSvcInst.ip6Address?.Address.MapToIPv6();

/// <summary>Gets a value indicating whether this instance is registered.</summary>
/// <value><see langword="true"/> if this instance is registered; otherwise, <see langword="false"/>.</value>
public bool IsRegistered => req.Version != 0;

/// <summary>A value that represents the port on which the service is running.</summary>
public ushort Port => pSvcInst.wPort;

/// <summary>A value that represents the service priority.</summary>
public ushort Priority => pSvcInst.wPriority;

/// <summary>The DNS service properties.</summary>
public IReadOnlyDictionary<string, string> Properties => pSvcInst.properties;

/// <summary>A value that represents the service weight.</summary>
public ushort Weight => pSvcInst.wWeight;

/// <summary>Used to obtain more information about a service advertised on the local network.</summary>
/// <param name="serviceName">
/// 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 "&lt;ServiceName&gt;._&lt;ServiceType&gt;._&lt;TransportProtocol&gt;.local". For example, "MyMusicServer._http._tcp.local".
/// </param>
/// <param name="adapter">The interface over which the query is sent. If <see langword="null"/>, then all interfaces will be considered.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel a pending asynchronous resolve operation.</param>
/// <returns>If successful, returns a new <see cref="DnsService"/> instance; otherwise, throws the appropriate DNS-specific exception.</returns>
/// <remarks>This function is asynchronous.</remarks>
public static async Task<DnsService> 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();
}
}

/// <summary>Used to remove a registered service.</summary>
/// <returns>If not successful, throws an appropriate DNS-specific exception.</returns>
/// <remarks>This function is asynchronous.</remarks>
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();
}

/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
if (disposed)
return;

disposed = true;
pSvcInst?.Dispose();
if (ctx != IntPtr.Zero)
GCHandle.FromIntPtr(ctx).Free();
}

/// <summary>Used to register a discoverable service on this device.</summary>
/// <param name="unicastEnabled">
/// <see langword="true"/> if the DNS protocol should be used to advertise the service; <see langword="false"/> if the mDNS protocol
/// should be used.
/// </param>
/// <param name="adapter">
/// An optional value that contains the network interface over which the service is to be advertised. If <see langword="null"/>, then all
/// interfaces will be considered.
/// </param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchonous operation.</param>
/// <exception cref="System.InvalidOperationException">Service is already registered.</exception>
/// <remarks>
/// 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.
/// </remarks>
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();
}
}
18 changes: 18 additions & 0 deletions Net/Vanara.Net.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Abstracted classes around Win32 networking functions to provide simplified and object-oriented access to key networking capabilities like DNS, DHCP, filtering, access, and discovery.</Description>
<AssemblyTitle>$(AssemblyName)</AssemblyTitle>
<AssemblyName>Vanara.Net</AssemblyName>
<PackageId>$(AssemblyName)</PackageId>
<PackageTags>vanara;net-extensions;networking</PackageTags>
<PackageReleaseNotes />
<PackageReadmeFile>pkgreadme.md</PackageReadmeFile>
<RootNamespace>Vanara.Net</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\PInvoke\Dhcp\Vanara.PInvoke.Dhcp.csproj" />
<ProjectReference Include="..\PInvoke\DnsApi\Vanara.PInvoke.DnsApi.csproj" />
<ProjectReference Include="..\PInvoke\Kernel32\Vanara.PInvoke.Kernel32.csproj" />
</ItemGroup>
</Project>
21 changes: 21 additions & 0 deletions Net/pkgreadme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
![Vanara](https://raw.githubusercontent.com/dahall/Vanara/master/docs/icons/VanaraHeading.png)
### **Vanara.Management NuGet Package**
[![Version](https://img.shields.io/nuget/v/Vanara.Management?label=NuGet&style=flat-square)](https://github.com/dahall/Vanara/releases)
[![Build status](https://img.shields.io/appveyor/build/dahall/vanara?label=AppVeyor%20build&style=flat-square)](https://ci.appveyor.com/project/dahall/vanara)

Extensions and helper classes for System.Management.

### **What is Vanara?**

[Vanara](https://github.com/dahall/Vanara) is a community project that contains various .NET assemblies which have P/Invoke functions, interfaces, enums and structures from Windows libraries. Each assembly is associated with one or a few tightly related libraries.

### **Issues?**

First check if it's already fixed by trying the [AppVeyor build](https://ci.appveyor.com/nuget/vanara-prerelease).
If you're still running into problems, file an [issue](https://github.com/dahall/Vanara/issues).

### **Included in Vanara.Management**

Classes
---
DynamicMgmtObject ManagementExtensions
7 changes: 7 additions & 0 deletions Net/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Assembly report for Vanara.Management.dll
Extensions and helper classes for System.Management.
### Classes
Class | Description
---- | ----
[Vanara.Management.DynamicMgmtObject](https://github.com/dahall/Vanara/search?l=C%23&q=DynamicMgmtObject) | A dynamic object to handle WMI `System.Management.ManagementBaseObject` references.
[Vanara.Management.ManagementExtensions](https://github.com/dahall/Vanara/search?l=C%23&q=ManagementExtensions) | Extension methods to work more easily with `System.Management`.
1 change: 1 addition & 0 deletions System/Computer/Sessions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ public IPAddress SessionAddressV4
/// <param name="wait">
/// Indicates whether the operation is synchronous. Specify <c>TRUE</c> to wait for the operation to complete, or <c>FALSE</c> to
/// return immediately.
/// </param>
/// <returns>
/// <para>If the function succeeds, the return value is a nonzero value.</para>
/// <para>If the function fails, the return value is zero. To get extended error information, call GetLastError.</para>
Expand Down
8 changes: 8 additions & 0 deletions UnitTests/Net/Net.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>UnitTest.PInvoke.Net</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Net\Vanara.Net.csproj" />
</ItemGroup>
</Project>
74 changes: 74 additions & 0 deletions UnitTests/Net/NetTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Microsoft.CodeAnalysis;
using NUnit.Framework;
using NUnit.Framework.Internal;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Vanara.Net;
using static Vanara.PInvoke.Dhcp;

namespace Vanara.PInvoke.Tests;

[TestFixture]
public class NetTests
{
[OneTimeSetUp]
public void _Setup()
{
}

[OneTimeTearDown]
public void _TearDown()
{
}

[Test]
public async Task TestDnsCreateService()
{
const string svcName = "initial._windns-example._udp.local";
using var dnsClient = new DnsService(svcName, "example.com", 1);
await dnsClient.RegisterAsync();
TestContext.WriteLine("Created\r\n=================");
dnsClient.WriteValues();
try
{
Assert.That(dnsClient.IsRegistered, Is.True);
using var dnsLookup = DnsService.ResolveAsync(svcName).Result;
Assert.That(dnsLookup.InstanceName, Is.EqualTo(svcName));
TestContext.WriteLine("Found\r\n=================");
dnsLookup.WriteValues();
}
finally
{
await dnsClient.DeRegisterAsync();
}
}

[Test]
public void TestDhcpProps()
{
using var client = new DhcpClient();
client.WriteValues();
client.GetDhcpServers().WriteValues();
client.GetOriginalSubnetMask().WriteValues();
}

[Test]
public void TestDhcpListeners()
{
var client = new DhcpClient();
try
{
client.ChangeEventIds = new[] { DHCP_OPTION_ID.OPTION_DEFAULT_TTL, DHCP_OPTION_ID.OPTION_LEASE_TIME, DHCP_OPTION_ID.OPTION_MESSAGE };
Thread.Sleep(1000);
client.ChangeEventIds = null;
Thread.Sleep(1000);
}
finally
{
client.Dispose();
}
}
}
Loading

0 comments on commit 1e24865

Please sign in to comment.