From 1e24865e5ae382362b24cf6c9178c5d13f05f1c7 Mon Sep 17 00:00:00 2001 From: David Hall Date: Fri, 16 Sep 2022 10:49:26 -0600 Subject: [PATCH] Added new Vanara.Net assembly with support for DNS services and DHCP clients. --- Net/DhcpClient.cs | 578 ++++++++++++++++++++++++++++++++++++ Net/DnsService.cs | 205 +++++++++++++ Net/Vanara.Net.csproj | 18 ++ Net/pkgreadme.md | 21 ++ Net/readme.md | 7 + System/Computer/Sessions.cs | 1 + UnitTests/Net/Net.csproj | 8 + UnitTests/Net/NetTests.cs | 74 +++++ Vanara.sln | 43 ++- 9 files changed, 954 insertions(+), 1 deletion(-) create mode 100644 Net/DhcpClient.cs create mode 100644 Net/DnsService.cs create mode 100644 Net/Vanara.Net.csproj create mode 100644 Net/pkgreadme.md create mode 100644 Net/readme.md create mode 100644 UnitTests/Net/Net.csproj create mode 100644 UnitTests/Net/NetTests.cs diff --git a/Net/DhcpClient.cs b/Net/DhcpClient.cs new file mode 100644 index 000000000..b0dc093ee --- /dev/null +++ b/Net/DhcpClient.cs @@ -0,0 +1,578 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using Vanara.Extensions; +using Vanara.InteropServices; +using Vanara.PInvoke; +using static Vanara.PInvoke.Dhcp; +using static Vanara.PInvoke.Kernel32; + +namespace Vanara.Net; + +/// Encapsulates the functions and properties for a DHCP client. +/// +public class DhcpClient : IDisposable +{ +#pragma warning disable IDE0052 // Remove unread private members + private static readonly DhcpInit init = new(); +#pragma warning restore IDE0052 // Remove unread private members + + private readonly SafeEventHandle closing = CreateEvent(default, false, false), updateList = CreateEvent(default, false, false); + private readonly SafeHTHREAD hThread; + private readonly Dictionary paramChgEvents = new(); + + /// Initializes a new instance of the class. + public DhcpClient() + { + var h = GCHandle.Alloc(this, GCHandleType.Normal); + hThread = CreateThread(default, 0, ThreadProc, (IntPtr)h, CREATE_THREAD_FLAGS.RUN_IMMEDIATELY, out _); + } + + /// + /// Occurs when the value related to a has changed. + /// Use the to set the list of identifiers that are watched. + /// + public event Action ParamChanged; + + /// + /// Specifies whether or not the client may assume that all subnets of the IP network to which the client is connected use the same MTU + /// as the subnet of that network to which the client is directly connected. A value of true indicates that all subnets share the same + /// MTU. A value of false means that the client should assume that some subnets of the directly connected network may have smaller MTUs. + /// + public bool AllSubnetsMTU => GetParam(DHCP_OPTION_ID.OPTION_ALL_SUBNETS_MTU); + + /// Specifies the timeout in seconds for ARP cache entries. + public TimeSpan ARPCacheTimeout => TimeSpan.FromSeconds(GetParam(DHCP_OPTION_ID.OPTION_ARP_CACHE_TIMEOUT)); + + /// + /// Specifies whether or not the client should respond to subnet mask requests using ICMP. A value of false indicates that the client + /// should not respond. A value of true means that the client should respond. + /// + public bool BeAMaskSupplier => GetParam(DHCP_OPTION_ID.OPTION_BE_A_MASK_SUPPLIER); + + /// + /// Specifies whether or not the client should solicit routers using the Router Discovery mechanism defined in RFC 1256. A value of false + /// indicates that the client should not perform router discovery. A value of true means that the client should perform router discovery. + /// + public bool BeARouter => GetParam(DHCP_OPTION_ID.OPTION_BE_A_ROUTER); + + /// + /// Identifies a bootstrap file. If supported by the client, it should have the same effect as the filename declaration. BOOTP clients + /// are unlikely to support this option. Some DHCP clients will support it, and others actually require it. + /// + public string BootfileName => GetParam(DHCP_OPTION_ID.OPTION_BOOTFILE_NAME); + + /// This option specifies the length in 512-octet blocks of the default boot image for the client. + public ushort BootFileSize => GetParam(DHCP_OPTION_ID.OPTION_BOOT_FILE_SIZE); + + /// + /// This option specifies the broadcast address in use on the client’s subnet. Legal values for broadcast addresses are specified in + /// section 3.2.1.3 of STD 3 (RFC1122). + /// + public IPAddress BroadcastAddress => new(GetParam(DHCP_OPTION_ID.OPTION_BROADCAST_ADDRESS)); + + /// + /// Gets or sets the list of values that, when changed, will fire the event. + /// + /// The list of values that, when changed, will fire the event. + public DHCP_OPTION_ID[] ChangeEventIds + { + get => paramChgEvents.Values.ToArray(); + set + { + ClearListeners(); + updateList.Set(); + if (value is null || value.Length == 0) return; + string adapter = Adapter; + foreach (DHCP_OPTION_ID id in value) + { + if (DhcpRegisterParamChange(DHCPCAPI_REGISTER_HANDLE_EVENT, default, adapter, default, DHCPCAPI_PARAMS_ARRAY.Make(out _, id), out HEVENT hEvt).Succeeded) + { + paramChgEvents.Add(hEvt, id); + } + } + updateList.Set(); + } + } + + /// + /// Class identifier (ID) that should be used if DHCP INFORM messages are being transmitted onto the network. This value is optional. + /// + public byte[] ClassId { get; set; } + + /// + /// This option is used by some DHCP clients as a way for users to specify identifying information to the client. This can be used in a + /// similar way to the vendor-class-identifier option, but the value of the option is specified by the user, not the vendor. Most recent + /// DHCP clients have a way in the user interface to specify the value for this identifier, usually as a text string. + /// + public string ClientClassInfo => GetParam(DHCP_OPTION_ID.OPTION_CLIENT_CLASS_INFO); + + /// + /// This option can be used to specify a DHCP client identifier in a host declaration, so that dhcpd can find the host record by matching + /// against the client identifier. + /// + public string ClientId => GetParam(DHCP_OPTION_ID.OPTION_CLIENT_ID); + + /// + /// The cookie server option specifies a list of RFC 865 cookie servers available to the client. Servers should be listed in order of preference. + /// + public IPAddress[] CookieServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_COOKIE_SERVERS)); + + /// This option specifies the default time-to-live that the client should use on outgoing datagrams. + public byte DefaultTTL => GetParam(DHCP_OPTION_ID.OPTION_DEFAULT_TTL); + + /// This option specifies the domain name that client should use when resolving hostnames via the Domain Name System. + public string DomainName => GetParam(DHCP_OPTION_ID.OPTION_DOMAIN_NAME); + + /// + /// The domain-name-servers option specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to the client. + /// Servers should be listed in order of preference. + /// + public IPAddress[] DomainNameServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_DOMAIN_NAME_SERVERS)); + + /// + /// This option specifies whether or not the client should use Ethernet Version 2 (RFC 894) or IEEE 802.3 (RFC 1042) encapsulation if the + /// interface is an Ethernet. A value of false indicates that the client should use RFC 894 encapsulation. A value of true means that the + /// client should use RFC 1042 encapsulation. + /// + public bool EthernetEncapsulation => GetParam(DHCP_OPTION_ID.OPTION_ETHERNET_ENCAPSULATION); + + /// + /// This option specifies the name of a file containing additional options to be interpreted according to the DHCP option format as + /// specified in RFC2132. + /// + public string ExtensionsPath => GetParam(DHCP_OPTION_ID.OPTION_EXTENSIONS_PATH); + + /// + /// This option specifies the name of the client. The name may or may not be qualified with the local domain name (it is preferable to + /// use the domain-name option to specify the domain name). See RFC 1035 for character set restrictions. This option is only honored by + /// dhclient-script(8) if the hostname for the client machine is not set. + /// + public string HostName => GetParam(DHCP_OPTION_ID.OPTION_HOST_NAME); + + /// + /// The ien116-name-servers option specifies a list of IEN 116 name servers available to the client. Servers should be listed in order of preference. + /// + public IPAddress[] IEN116NameServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_IEN116_NAME_SERVERS)); + + /// The Internet Explorer proxy. + public string IEProxy => GetParam(DHCP_OPTION_ID.OPTION_MSFT_IE_PROXY); + + /// + /// The impress-server option specifies a list of Imagen Impress servers available to the client. Servers should be listed in order of preference. + /// + public IPAddress[] ImpressServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_IMPRESS_SERVERS)); + + /// + public uint KeepAliveDataSize => GetParam(DHCP_OPTION_ID.OPTION_KEEP_ALIVE_DATA_SIZE); + + /// + /// This option specifies the interval (in seconds) that the client TCP should wait before sending a keepalive message on a TCP + /// connection. The time is specified as a 32-bit unsigned integer. A value of zero indicates that the client should not generate + /// keepalive messages on connections unless specifically requested by an application. + /// + public TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(GetParam(DHCP_OPTION_ID.OPTION_KEEP_ALIVE_INTERVAL)); + + /// + /// This option is used in a client request (DHCPDISCOVER or DHCPREQUEST) to allow the client to request a lease time for the IP address. + /// In a server reply (DHCPOFFER), a DHCP server uses this option to specify the lease time it is willing to offer. + /// + public TimeSpan LeaseTime => new(GetParam(DHCP_OPTION_ID.OPTION_LEASE_TIME)); + + /// + /// The log-server option specifies a list of MIT-LCS UDP log servers available to the client. Servers should be listed in order of preference. + /// + public IPAddress[] LogServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_LOG_SERVERS)); + + /// + /// The LPR server option specifies a list of RFC 1179 line printer servers available to the client. Servers should be listed in order of preference. + /// + public IPAddress[] LPRServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_LPR_SERVERS)); + + /// + /// This option specifies the maximum size datagram that the client should be prepared to reassemble. The minimum legal value is 576. + /// + public ushort MaxReassemblySize => GetParam(DHCP_OPTION_ID.OPTION_MAX_REASSEMBLY_SIZE); + + /// + /// This option specifies the path-name of a file to which the client’s core image should be dumped in the event the client crashes. The + /// path is formatted as a character string consisting of characters from the NVT ASCII character set. + /// + public string MeritDumpFile => GetParam(DHCP_OPTION_ID.OPTION_MERIT_DUMP_FILE); + + /// + /// This option is used by a DHCP server to provide an error message to a DHCP client in a DHCPNAK message in the event of a failure. A + /// client may use this option in a DHCPDECLINE message to indicate why the client declined the offered parameters. + /// + public string Message => GetParam(DHCP_OPTION_ID.OPTION_MESSAGE); + + /// + /// This option, when sent by the client, specifies the maximum size of any response that the server sends to the client. When specified + /// on the server, if the client did not send a dhcp-max-message-size option, the size specified on the server is used. This works for + /// BOOTP as well as DHCP responses. + /// + public ushort MessageLength => GetParam(DHCP_OPTION_ID.OPTION_MESSAGE_LENGTH); + + /// This option, sent by both client and server, specifies the type of DHCP message contained in the DHCP packet. + public DhcpMessageType MessageType => GetParam(DHCP_OPTION_ID.OPTION_MESSAGE_TYPE); + + /// This option specifies the MTU to use on this interface. The minimum legal value for the MTU is 68. + public ushort MTU => GetParam(DHCP_OPTION_ID.OPTION_MTU); + + /// + /// The NetBIOS datagram distribution server (NBDD) option specifies a list of RFC 1001/1002 NBDD servers listed in order of preference. + /// + public IPAddress[] NetBIOSDatagramServer => ToIP(GetParam(DHCP_OPTION_ID.OPTION_NETBIOS_DATAGRAM_SERVER)); + + /// + /// The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers listed in order of preference. NetBIOS Name + /// Service is currently more commonly referred to as WINS. WINS servers can be specified using the netbios-name-servers option. + /// + public IPAddress[] NetBIOSNameServer => ToIP(GetParam(DHCP_OPTION_ID.OPTION_NETBIOS_NAME_SERVER)); + + /// + /// The NetBIOS node type option allows NetBIOS over TCP/IP clients which are configurable to be configured as described in RFC + /// 1001/1002. The value is specified as a single octet which identifies the client type. + /// + public NetBIOSNodeType NetBIOSNodeType => GetParam(DHCP_OPTION_ID.OPTION_NETBIOS_NODE_TYPE); + + /// + /// The NetBIOS scope option specifies the NetBIOS over TCP/IP scope parameter for the client as specified in RFC 1001/1002. See RFC1001, + /// RFC1002, and RFC1035 for character-set restrictions. + /// + public string NetBIOSScopeOption => GetParam(DHCP_OPTION_ID.OPTION_NETBIOS_SCOPE_OPTION); + + /// + /// The netinfo-server-address option has not been described in any RFC, but has been allocated (and is claimed to be in use) by Apple + /// Computers. It’s hard to say if the above is the correct format, or what clients might be expected to do if values were configured. + /// Use at your own risk. + /// + public IPAddress[] NetworkInfoServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_NETWORK_INFO_SERVERS)); + + /// + /// This option specifies the name of the client’s NIS (Sun Network Information Services) domain. The domain is formatted as a character + /// string consisting of characters from the NVT ASCII character set. + /// + public string NetworkInfoServiceDomain => GetParam(DHCP_OPTION_ID.OPTION_NETWORK_INFO_SERVICE_DOM); + + /// + /// The NNTP server option specifies a list of NNTP servers available to the client. Servers should be listed in order of preference. + /// + public IPAddress[] NetworkTimeServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_NETWORK_TIME_SERVERS)); + + /// + /// This option specifies whether the client should configure its IP layer to allow forwarding of datagrams with non-local source routes + /// (see Section 3.3.5 of [4] for a discussion of this topic). A value of false means disallow forwarding of such datagrams, and a value + /// of true means allow forwarding. + /// + public bool NonLocalSourceRouting => GetParam(DHCP_OPTION_ID.OPTION_NON_LOCAL_SOURCE_ROUTING); + + /// + public bool OkToOverlay => GetParam(DHCP_OPTION_ID.OPTION_OK_TO_OVERLAY); + + /// + /// This option, when sent by the client, specifies which options the client wishes the server to return. Normally, in the ISC DHCP + /// client, this is done using the request statement. If this option is not specified by the client, the DHCP server will normally return + /// every option that is valid in scope and that fits into the reply. When this option is specified on the server, the server returns the + /// specified options. This can be used to force a client to take options that it hasn’t requested, and it can also be used to tailor the + /// response of the DHCP server for clients that may need a more limited set of options than those the server would normally return. + /// + public byte[] ParameterRequestList => GetParam(DHCP_OPTION_ID.OPTION_PARAMETER_REQUEST_LIST); + + /// + /// This option specifies the timeout (in seconds) to use when aging Path MTU values discovered by the mechanism defined in RFC 1191. + /// + public TimeSpan PathMTUAgingTimeout => TimeSpan.FromSeconds(GetParam(DHCP_OPTION_ID.OPTION_PMTU_AGING_TIMEOUT)); + + /// + /// This option specifies a table of MTU sizes to use when performing Path MTU Discovery as defined in RFC 1191. The table is formatted + /// as a list of 16-bit unsigned integers, ordered from smallest to largest. The minimum MTU value cannot be smaller than 68. + /// + public ushort[] PathMTUPlateauTable => GetParam(DHCP_OPTION_ID.OPTION_PMTU_PLATEAU_TABLE); + + /// + /// This option specifies whether or not the client should perform subnet mask discovery using ICMP. A value of false indicates that the + /// client should not perform mask discovery. A value of true means that the client should perform mask discovery. + /// + public bool PerformMaskDiscovery => GetParam(DHCP_OPTION_ID.OPTION_PERFORM_MASK_DISCOVERY); + + /// + /// This option specifies whether or not the client should solicit routers using the Router Discovery mechanism defined in RFC 1256. A + /// value of false indicates that the client should not perform router discovery. A value of true means that the client should perform + /// router discovery. + /// + public bool PerformRouterDiscovery => GetParam(DHCP_OPTION_ID.OPTION_PERFORM_ROUTER_DISCOVERY); + + /// + /// This option specifies policy filters for non-local source routing. The filters consist of a list of IP addresses and masks which + /// specify destination/mask pairs with which to filter incoming source routes. + /// Any source routed datagram whose next-hop address does not match one of the filters should be discarded by the client. + /// + public IPAddress[] PolicyFilter => ToIP(GetParam(DHCP_OPTION_ID.OPTION_POLICY_FILTER_FOR_NLSR)); + + /// + /// + /// This option specifies the number of seconds from the time a client gets an address until the client transitions to the REBINDING state. + /// + /// This option is user configurable, but it will be ignored if the value is greater than or equal to the lease time. + /// + /// To make DHCPv4+DHCPv6 migration easier in the future, any value configured in this option is also used as a DHCPv6 "T1" (renew) time. + /// + /// + public TimeSpan RebindTime => TimeSpan.FromSeconds(GetParam(DHCP_OPTION_ID.OPTION_REBIND_TIME)); + + /// + /// + /// This option specifies the number of seconds from the time a client gets an address until the client transitions to the RENEWING state. + /// + /// + /// This option is user configurable, but it will be ignored if the value is greater than or equal to the rebinding time, or lease time. + /// + /// To make DHCPv4+DHCPv6 migration easier in the future, + /// + public TimeSpan RenewalTime => TimeSpan.FromSeconds(GetParam(DHCP_OPTION_ID.OPTION_RENEWAL_TIME)); + + /// + /// This option is used by the client in a DHCPDISCOVER to request that a particular IP address be assigned. + /// This option is not user configurable. + /// + public IPAddress RequestedAddress => new(GetParam(DHCP_OPTION_ID.OPTION_REQUESTED_ADDRESS)); + + /// + public IPAddress[] RlpServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_RLP_SERVERS)); + + /// + /// This option specifies the path-name that contains the client’s root disk. The path is formatted as a character string consisting of + /// characters from the NVT ASCII character set. + /// + public string RootDisk => GetParam(DHCP_OPTION_ID.OPTION_ROOT_DISK); + + /// + /// The routers option specifies a list of IP addresses for routers on the client’s subnet. Routers should be listed in order of preference. + /// + public IPAddress RouterAddress => new(GetParam(DHCP_OPTION_ID.OPTION_ROUTER_ADDRESS)); + + /// This option specifies the address to which the client should transmit router solicitation requests. + public IPAddress RouterSolicitationAddress => new(GetParam(DHCP_OPTION_ID.OPTION_ROUTER_SOLICITATION_ADDR)); + + /// + /// GUID of the adapter on which requested data is being made. Must be under 256 characters. If this value is , the + /// first adapter with an address supplied via DHCP will be used. + /// + public string SelectedAdapterId { get; set; } + + /// + /// + /// This option is used in DHCPOFFER and DHCPREQUEST messages, and may optionally be included in the DHCPACK and DHCPNAK messages. DHCP + /// servers include this option in the DHCPOFFER in order to allow the client to distinguish between lease offers. DHCP clients use the + /// contents of the ´server identifier´ field as the destination address for any DHCP messages unicast to the DHCP server. DHCP clients + /// also indicate which of several lease offers is being accepted by including this option in a DHCPREQUEST message. + /// + /// The value of this option is the IP address of the server. + /// This option is not directly user configurable. See the server-identifier server option in dhcpd.conf(5). + /// + public IPAddress ServerIdentifier => new(GetParam(DHCP_OPTION_ID.OPTION_SERVER_IDENTIFIER)); + + /// + /// + /// This option specifies a list of static routes that the client should install in its routing cache. If multiple routes to the same + /// destination are specified, they are listed in descending order of priority. + /// + /// + /// The routes consist of a list of IP address pairs. The first address is the destination address, and the second address is the router + /// for the destination. + /// + /// + /// The default route (0.0.0.0) is an illegal destination for a static route. To specify the default route, use the routers option. Also, + /// please note that this option is not intended for classless IP routing - it does not include a subnet mask. Since classless IP routing + /// is now the most widely deployed routing standard, this option is virtually useless, and is not implemented by any of the popular DHCP + /// clients, for example the Microsoft DHCP client. + /// + /// + public IPAddress[] StaticRoutes => ToIP(GetParam(DHCP_OPTION_ID.OPTION_STATIC_ROUTES)); + + /// + /// The subnet mask option specifies the client’s subnet mask as per RFC 950. If no subnet mask option is provided anywhere in scope, as + /// a last resort dhcpd will use the subnet mask from the subnet declaration for the network on which an address is being assigned. + /// However, any subnet-mask option declaration that is in scope for the address being assigned will override the subnet mask specified + /// in the subnet declaration. + /// + public IPAddress SubnetMask => new(GetParam(DHCP_OPTION_ID.OPTION_SUBNET_MASK)); + + /// This specifies the IP address of the client’s swap server. + public IPAddress SwapServer => new(GetParam(DHCP_OPTION_ID.OPTION_SWAP_SERVER)); + + /// + /// This option is used to identify a TFTP server and, if supported by the client, should have the same effect as the server-name + /// declaration. BOOTP clients are unlikely to support this option. Some DHCP clients will support it, and others actually require it. + /// + public string TFTPServerName => GetParam(DHCP_OPTION_ID.OPTION_TFTP_SERVER_NAME); + + /// The time-offset option specifies the offset of the client’s subnet in seconds from Coordinated Universal Time (UTC). + public DateTimeOffset TimeOffset => new(0, TimeSpan.FromSeconds(GetParam(DHCP_OPTION_ID.OPTION_TIME_OFFSET))); + + /// + /// The time-server option specifies a list of RFC 868 time servers available to the client. Servers should be listed in order of preference. + /// + public IPAddress[] TimeServers => ToIP(GetParam(DHCP_OPTION_ID.OPTION_TIME_SERVERS)); + + /// + /// This option specifies whether or not the client should negotiate the use of trailers (RFC 893 [14]) when using the ARP protocol. A + /// value of false indicates that the client should not attempt to use trailers. A value of true means that the client should attempt to + /// use trailers. + /// + public bool Trailers => GetParam(DHCP_OPTION_ID.OPTION_TRAILERS); + + /// This option specifies the default TTL that the client should use when sending TCP segments. The minimum value is 1. + public byte TTL => GetParam(DHCP_OPTION_ID.OPTION_TTL); + + /// + /// + /// This option is used by some DHCP clients to identify the vendor type and possibly the configuration of a DHCP client. The information + /// is a string of bytes whose contents are specific to the vendor and are not specified in a standard. To see what vendor class + /// identifier clients are sending, you can write the following in your DHCP server configuration file: + /// + /// set vendor-string = option vendor-class-identifier; + /// + /// This will result in all entries in the DHCP server lease database file for clients that sent vendor-class-identifier options having a + /// set statement that looks something like this: + /// + /// set vendor-string = "SUNW.Ultra-5_10"; + /// + /// The vendor-class-identifier option is normally used by the DHCP server to determine the options that are returned in the + /// vendor-encapsulated-options option. Please see the VENDOR ENCAPSULATED OPTIONS section later in this manual page for further information. + /// + /// + public string VendorSpecInfo => GetParam(DHCP_OPTION_ID.OPTION_VENDOR_SPEC_INFO); + + /// + /// This option specifies a list of systems that are running the X Window System Display Manager and are available to the client. + /// Addresses should be listed in order of preference. + /// + public IPAddress[] XwindowDisplayManager => ToIP(GetParam(DHCP_OPTION_ID.OPTION_XWINDOW_DISPLAY_MANAGER)); + + /// + /// This option specifies a list of X Window System Font servers available to the client. Servers should be listed in order of preference. + /// + public IPAddress[] XwindowFontServer => ToIP(GetParam(DHCP_OPTION_ID.OPTION_XWINDOW_FONT_SERVER)); + + internal static NetworkInterface CurrentAdapter => NetworkInterface.GetAllNetworkInterfaces(). + Where(i => i.NetworkInterfaceType == NetworkInterfaceType.Ethernet && i.OperationalStatus == OperationalStatus.Up && + i.Supports(NetworkInterfaceComponent.IPv4) && (i.GetIPProperties()?.GetIPv4Properties().IsDhcpEnabled ?? false)). + FirstOrDefault(); + + internal string Adapter => SelectedAdapterId ?? CurrentAdapter?.Id; + + /// Gets the original subnet mask. + /// The retrieved subnet mask. + public IPAddress GetOriginalSubnetMask() + { + DhcpGetOriginalSubnetMask(Adapter, out DHCP_IP_ADDRESS mask); + return new(mask.value); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + ClearListeners(); + closing.Set(); + hThread.Close(); + } + + /// Gets the current DHCP servers for this client. + /// A sequence of addresses of DHCP servers. + public IEnumerable GetDhcpServers() => NetworkInterface.GetAllNetworkInterfaces().Where(i => i.OperationalStatus == OperationalStatus.Up).SelectMany(i => i.GetIPProperties().DhcpServerAddresses).Distinct(); + + /// The DhcpRemoveDNSRegistrations function removes all DHCP-initiated DNS registrations for the client. + public void RemoveDNSRegistrations() => DhcpRemoveDNSRegistrations().ThrowIfFailed(); + + private void ClearListeners() + { + foreach (KeyValuePair kv in paramChgEvents) + { + DhcpDeRegisterParamChange(0, default, kv.Key); + } + paramChgEvents.Clear(); + } + + private T GetParam(DHCP_OPTION_ID optionId) + { + using SafeCoTaskMemHandle pClassIdData = new(ClassId); + using SafeCoTaskMemStruct pClass = (DHCPCAPI_CLASSID?)(ClassId is null ? null : new DHCPCAPI_CLASSID() { nBytesData = (uint)ClassId.Length, Data = pClassIdData }); + + DHCPCAPI_PARAMS_ARRAY sendParams = new(); + DHCPCAPI_PARAMS_ARRAY reqParams = DHCPCAPI_PARAMS_ARRAY.Make(out SafeNativeArray pparam, optionId); + + uint sz = 0; + string adapter = Adapter; + DhcpRequestParams(DHCPCAPI_REQUEST.DHCPCAPI_REQUEST_SYNCHRONOUS, default, adapter, pClass, sendParams, reqParams, IntPtr.Zero, ref sz, null).ThrowUnless(Win32Error.ERROR_MORE_DATA); + + using SafeCoTaskMemHandle buffer = new(sz); + Guid appId = Guid.NewGuid(); + DhcpRequestParams(DHCPCAPI_REQUEST.DHCPCAPI_REQUEST_SYNCHRONOUS, default, adapter, pClass, sendParams, reqParams, buffer, ref sz, appId.ToString("N")).ThrowIfFailed(); + if (sz == 0) + return default; + + DHCPAPI_PARAMS p = pparam[0]; + if (typeof(T).IsArray) + { + Type elemType = typeof(T).GetElementType(); + System.Diagnostics.Debug.WriteLine($"Array: type={elemType.Name}, elemSz={InteropExtensions.SizeOf(elemType)}, memSz={sz}"); + return (T)(object)p.Data.ToArray(elemType, sz / InteropExtensions.SizeOf(elemType), 0, sz); + } + else + { + System.Diagnostics.Debug.WriteLine(typeof(T) == typeof(string) ? $"String: memSz={sz}" : $"Value: type={typeof(T).Name}, sz={InteropExtensions.SizeOf()}, memSz={sz}"); + return p.Data.Convert(p.nBytesData, CharSet.Ansi); + } + } + + private static uint ThreadProc(IntPtr hgc) + { + var c = (DhcpClient)GCHandle.FromIntPtr(hgc).Target; + HEVENT[] hevts; + RebuildList(); + do + { + WAIT_STATUS state = WaitForMultipleObjects(hevts, false, INFINITE); + if (state == (WAIT_STATUS)c.paramChgEvents.Count) + { + break; + } + else if (state == (WAIT_STATUS)c.paramChgEvents.Count + 1) + { + RebuildList(); + } + else + { + if (hevts.Length > (int)state && c.paramChgEvents.TryGetValue(hevts[(int)state], out var id)) + c.ParamChanged?.Invoke(id); + } + } while (true); + return 0; + + void RebuildList() => hevts = c.paramChgEvents.Keys.Concat(new HEVENT[] { c.closing, c.updateList }).ToArray(); + } + + private IPAddress[] ToIP(uint[] ips) => ips is null ? new IPAddress[0] : Array.ConvertAll(ips, i => new IPAddress(i)); + + internal class DhcpInit + { + public readonly uint DhcpVersion, DhcpV6Version; + + public DhcpInit() + { + DhcpCApiInitialize(out DhcpVersion).ThrowIfFailed(); + Dhcpv6CApiInitialize(out DhcpV6Version); + } + + ~DhcpInit() + { + Dhcpv6CApiCleanup(); + DhcpCApiCleanup(); + } + } +} \ No newline at end of file diff --git a/Net/DnsService.cs b/Net/DnsService.cs new file mode 100644 index 000000000..28e769ca4 --- /dev/null +++ b/Net/DnsService.cs @@ -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; + +/// 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(); + } +} \ No newline at end of file diff --git a/Net/Vanara.Net.csproj b/Net/Vanara.Net.csproj new file mode 100644 index 000000000..d2174b5a1 --- /dev/null +++ b/Net/Vanara.Net.csproj @@ -0,0 +1,18 @@ + + + + Abstracted classes around Win32 networking functions to provide simplified and object-oriented access to key networking capabilities like DNS, DHCP, filtering, access, and discovery. + $(AssemblyName) + Vanara.Net + $(AssemblyName) + vanara;net-extensions;networking + + pkgreadme.md + Vanara.Net + + + + + + + \ No newline at end of file diff --git a/Net/pkgreadme.md b/Net/pkgreadme.md new file mode 100644 index 000000000..f6b3dc0ba --- /dev/null +++ b/Net/pkgreadme.md @@ -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 diff --git a/Net/readme.md b/Net/readme.md new file mode 100644 index 000000000..ac64ee9cd --- /dev/null +++ b/Net/readme.md @@ -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`. diff --git a/System/Computer/Sessions.cs b/System/Computer/Sessions.cs index f0f8ee641..ec926f5f9 100644 --- a/System/Computer/Sessions.cs +++ b/System/Computer/Sessions.cs @@ -279,6 +279,7 @@ public IPAddress SessionAddressV4 /// /// Indicates whether the operation is synchronous. Specify TRUE to wait for the operation to complete, or FALSE to /// return immediately. + /// /// /// If the function succeeds, the return value is a nonzero value. /// If the function fails, the return value is zero. To get extended error information, call GetLastError. diff --git a/UnitTests/Net/Net.csproj b/UnitTests/Net/Net.csproj new file mode 100644 index 000000000..fb2110203 --- /dev/null +++ b/UnitTests/Net/Net.csproj @@ -0,0 +1,8 @@ + + + UnitTest.PInvoke.Net + + + + + \ No newline at end of file diff --git a/UnitTests/Net/NetTests.cs b/UnitTests/Net/NetTests.cs new file mode 100644 index 000000000..7be0e12b3 --- /dev/null +++ b/UnitTests/Net/NetTests.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Vanara.sln b/Vanara.sln index 61fef8fd8..33978e179 100644 --- a/Vanara.sln +++ b/Vanara.sln @@ -399,7 +399,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DOSvc", "UnitTests\PInvoke\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vanara.PInvoke.MsRdc", "PInvoke\MsRdc\Vanara.PInvoke.MsRdc.csproj", "{FE46691C-F788-471B-B538-B24F81C5579E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MsRdc", "UnitTests\PInvoke\MsRdc\MsRdc.csproj", "{454FEEDE-D1C5-474F-BDE6-801CE6C78088}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MsRdc", "UnitTests\PInvoke\MsRdc\MsRdc.csproj", "{454FEEDE-D1C5-474F-BDE6-801CE6C78088}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vanara.Net", "Net\Vanara.Net.csproj", "{55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Net", "UnitTests\Net\Net.csproj", "{F713978B-5EA5-459B-9D26-62DCC6305B78}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -2634,6 +2638,42 @@ Global {454FEEDE-D1C5-474F-BDE6-801CE6C78088}.Release|x64.Build.0 = Release|Any CPU {454FEEDE-D1C5-474F-BDE6-801CE6C78088}.Release|x86.ActiveCfg = Release|Any CPU {454FEEDE-D1C5-474F-BDE6-801CE6C78088}.Release|x86.Build.0 = Release|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Debug|x64.ActiveCfg = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Debug|x64.Build.0 = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Debug|x86.ActiveCfg = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Debug|x86.Build.0 = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.DebugNoTests|Any CPU.ActiveCfg = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.DebugNoTests|Any CPU.Build.0 = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.DebugNoTests|x64.ActiveCfg = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.DebugNoTests|x64.Build.0 = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.DebugNoTests|x86.ActiveCfg = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.DebugNoTests|x86.Build.0 = Debug|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Release|Any CPU.Build.0 = Release|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Release|x64.ActiveCfg = Release|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Release|x64.Build.0 = Release|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Release|x86.ActiveCfg = Release|Any CPU + {55D566F7-DF56-4C18-A8FF-5BBD6F672BC9}.Release|x86.Build.0 = Release|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Debug|x64.ActiveCfg = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Debug|x64.Build.0 = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Debug|x86.ActiveCfg = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Debug|x86.Build.0 = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.DebugNoTests|Any CPU.ActiveCfg = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.DebugNoTests|Any CPU.Build.0 = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.DebugNoTests|x64.ActiveCfg = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.DebugNoTests|x64.Build.0 = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.DebugNoTests|x86.ActiveCfg = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.DebugNoTests|x86.Build.0 = Debug|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Release|Any CPU.Build.0 = Release|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Release|x64.ActiveCfg = Release|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Release|x64.Build.0 = Release|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Release|x86.ActiveCfg = Release|Any CPU + {F713978B-5EA5-459B-9D26-62DCC6305B78}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2809,6 +2849,7 @@ Global {83DD9317-2B20-4AD3-A167-250D9AF2F1A5} = {385CAD2D-0A5E-4F80-927B-D5499D126B90} {FE46691C-F788-471B-B538-B24F81C5579E} = {212ABBD0-B724-4CFA-9D6D-E3891547FA90} {454FEEDE-D1C5-474F-BDE6-801CE6C78088} = {385CAD2D-0A5E-4F80-927B-D5499D126B90} + {F713978B-5EA5-459B-9D26-62DCC6305B78} = {3EC6B40D-71D3-4E59-A0E0-544EC605FE11} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {543FAC75-2AF1-4EF1-9609-B242B63FEED4}