|
|
/*++
Copyright (c) 1997 Microsoft Corporation
Module Name:
entry.c
Abstract:
This module contains the entry-code for the IP Network Address Translator.
Author:
Abolade Gbadegesin (t-abolag) 11-July-1997
Revision History:
William Ingle (billi) 12-May-2001 NULL security descriptor check
--*/
#include "precomp.h"
#pragma hdrstop
//
// GLOBAL DATA DEFINITIONS
//
COMPONENT_REFERENCE ComponentReference;
//
// Win32 device-name
//
WCHAR ExternalName[] = L"\\DosDevices\\IPNAT";
//
// Device- and file-object for the IP driver
//
extern PDEVICE_OBJECT IpDeviceObject = NULL; extern PFILE_OBJECT IpFileObject = NULL;
//
// Device-object for the NAT driver
//
extern PDEVICE_OBJECT NatDeviceObject = NULL;
//
// Registry parameters key name
//
WCHAR ParametersName[] = L"Parameters";
//
// Name of value holding reserved ports
//
WCHAR ReservedPortsName[] = L"ReservedPorts";
//
// Start and end of reserved-port range
//
USHORT ReservedPortsLowerRange = DEFAULT_START_PORT; USHORT ReservedPortsUpperRange = DEFAULT_END_PORT;
//
// Device- and file-object for the TCP driver
//
extern PDEVICE_OBJECT TcpDeviceObject = NULL; extern PFILE_OBJECT TcpFileObject = NULL; extern HANDLE TcpDeviceHandle = NULL;
//
// Registry path for the driver's parameters
//
const WCHAR IpNatParametersPath[] = L"\\Registry\\Machine\\System\\CurrentControlSet\\Services" L"\\IpNat\\Parameters";
//
// Timeout interval for TCP session mappings
//
ULONG TcpTimeoutSeconds = DEFAULT_TCP_TIMEOUT;
//
// Bitmap of enabled tracing message classes
//
ULONG TraceClassesEnabled = 0;
//
// Registry trace-class value name
//
WCHAR TraceClassesEnabledName[] = L"TraceClassesEnabled";
//
// Timeout interval for UDP and other message-oriented session mappings
//
ULONG UdpTimeoutSeconds = DEFAULT_UDP_TIMEOUT;
#if NAT_WMI
//
// Copy of our registry path for WMI use.
//
UNICODE_STRING NatRegistryPath; #endif
//
// Name of value for allowing inbound non-unicast
//
WCHAR AllowInboundNonUnicastTrafficName[] = L"AllowInboundNonUnicastTraffic";
//
// If true, non-unicast traffic will not be dropped
// when recevied on a firewalled interface.
//
BOOLEAN AllowInboundNonUnicastTraffic = FALSE;
//
// FUNCTION PROTOTYPES (alphabetically)
//
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath );
NTSTATUS NatAdjustSecurityDescriptor( VOID );
VOID NatCleanupDriver( VOID );
VOID NatCreateExternalNaming( IN PUNICODE_STRING DeviceString );
VOID NatDeleteExternalNaming( VOID );
NTSTATUS NatInitializeDriver( VOID );
NTSTATUS NatSetFirewallHook( BOOLEAN Install );
VOID NatUnloadDriver( IN PDRIVER_OBJECT DriverObject );
#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(INIT, NatAdjustSecurityDescriptor)
#pragma alloc_text(PAGE, NatCreateExternalNaming)
#pragma alloc_text(PAGE, NatDeleteExternalNaming)
#endif
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) /*++
Routine Description:
Performs driver-initialization for NAT.
Arguments:
Return Value:
STATUS_SUCCESS if initialization succeeded, error code otherwise.
--*/
{ WCHAR DeviceName[] = DD_IP_NAT_DEVICE_NAME; UNICODE_STRING DeviceString; LONG i; OBJECT_ATTRIBUTES ObjectAttributes; HANDLE ParametersKey; HANDLE ServiceKey; NTSTATUS status; UNICODE_STRING String;
PAGED_CODE();
CALLTRACE(("DriverEntry\n"));
#if DBG
//
// Open the registry key
//
InitializeObjectAttributes( &ObjectAttributes, RegistryPath, OBJ_CASE_INSENSITIVE, NULL, NULL ); status = ZwOpenKey(&ServiceKey, KEY_READ, &ObjectAttributes); if (NT_SUCCESS(status)) { RtlInitUnicodeString(&String, ParametersName); InitializeObjectAttributes( &ObjectAttributes, &String, OBJ_CASE_INSENSITIVE, ServiceKey, NULL ); status = ZwOpenKey(&ParametersKey, KEY_READ, &ObjectAttributes); ZwClose(ServiceKey); if (NT_SUCCESS(status)) { UCHAR Buffer[32]; ULONG BytesRead; PKEY_VALUE_PARTIAL_INFORMATION Value; RtlInitUnicodeString(&String, TraceClassesEnabledName); status = ZwQueryValueKey( ParametersKey, &String, KeyValuePartialInformation, (PKEY_VALUE_PARTIAL_INFORMATION)Buffer, sizeof(Buffer), &BytesRead ); ZwClose(ParametersKey); if (NT_SUCCESS(status) && ((PKEY_VALUE_PARTIAL_INFORMATION)Buffer)->Type == REG_DWORD) { TraceClassesEnabled = *(PULONG)((PKEY_VALUE_PARTIAL_INFORMATION)Buffer)->Data; } } } #endif
#if NAT_WMI
//
// Record our registry path for WMI use
//
NatRegistryPath.Length = 0; NatRegistryPath.MaximumLength = RegistryPath->MaximumLength + sizeof( UNICODE_NULL ); NatRegistryPath.Buffer = ExAllocatePoolWithTag( PagedPool, NatRegistryPath.MaximumLength, NAT_TAG_WMI );
if( NatRegistryPath.Buffer ) { RtlCopyUnicodeString( &NatRegistryPath, RegistryPath ); } else { ERROR(("NAT: Unable to allocate string for RegistryPath\n")); return STATUS_NO_MEMORY; } #endif
//
// Create the device's object.
//
RtlInitUnicodeString(&DeviceString, DeviceName);
status = IoCreateDevice( DriverObject, 0, &DeviceString, FILE_DEVICE_NETWORK, FILE_DEVICE_SECURE_OPEN, FALSE, &NatDeviceObject );
if (!NT_SUCCESS(status)) { ERROR(("IoCreateDevice failed (0x%08X)\n", status)); return status; }
//
// Adjust the security descriptor on the device object.
//
status = NatAdjustSecurityDescriptor();
if (!NT_SUCCESS(status)) { ERROR(("NatAdjustSecurityDescriptor failed (0x%08x)\n", status)); return status; }
//
// Initialize file-object tracking items
//
KeInitializeSpinLock(&NatFileObjectLock); NatOwnerProcessId = NULL; NatFileObjectCount = 0;
//
// Setup the driver object
//
DriverObject->DriverUnload = NatUnloadDriver; DriverObject->FastIoDispatch = &NatFastIoDispatch; DriverObject->DriverStartIo = NULL;
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) { DriverObject->MajorFunction[i] = NatDispatch; }
//
// Create a Win32-accessible device object
//
NatCreateExternalNaming(&DeviceString);
//
// Initialize the driver's structures
//
status = NatInitializeDriver();
return status;
} // DriverEntry
NTSTATUS NatAdjustSecurityDescriptor( VOID )
/*++
Routine Description:
Modifies the security descriptor on the NAT's device object so that only SYSTEM has any permissions.
Arguments:
none.
Return Value:
NTSTATUS - success/error code.
--*/
{ PACE_HEADER AceHeader; PSID AceSid; PACL Dacl; BOOLEAN DaclDefaulted; BOOLEAN DaclPresent; DWORD i; BOOLEAN MemoryAllocated; PSECURITY_DESCRIPTOR NatSD = NULL; PACL NewDacl = NULL; SECURITY_DESCRIPTOR NewSD; SECURITY_INFORMATION SecurityInformation; ULONG Size; NTSTATUS status;
do { //
// Get our original security descriptor
//
status = ObGetObjectSecurity( NatDeviceObject, &NatSD, &MemoryAllocated );
// ObGetObjectSecurity can return a NULL security descriptor
// even with NT_SUCCESS status code
if (!NT_SUCCESS(status) || (NULL==NatSD)) { break; }
//
// Obtain the Dacl from the security descriptor
//
status = RtlGetDaclSecurityDescriptor( NatSD, &DaclPresent, &Dacl, &DaclDefaulted ); if (!NT_SUCCESS(status)) { break; }
ASSERT(FALSE != DaclPresent);
//
// Make a copy of the Dacl so that we can modify it.
//
NewDacl = ExAllocatePoolWithTag( PagedPool, Dacl->AclSize, NAT_TAG_SD );
if (NULL == NewDacl) { status = STATUS_NO_MEMORY; break; }
RtlCopyMemory(NewDacl, Dacl, Dacl->AclSize);
//
// Loop through the DACL, removing any access allowed
// entries that aren't for SYSTEM
//
for (i = 0; i < NewDacl->AceCount; i++) {
status = RtlGetAce(NewDacl, i, &AceHeader);
if (NT_SUCCESS(status)) { if (ACCESS_ALLOWED_ACE_TYPE == AceHeader->AceType) {
AceSid = (PSID) &((ACCESS_ALLOWED_ACE*)AceHeader)->SidStart;
if (!RtlEqualSid(AceSid, SeExports->SeLocalSystemSid)) { status = RtlDeleteAce(NewDacl, i); if (NT_SUCCESS(status)) { i -= 1; } } } } }
ASSERT(NewDacl->AceCount > 0);
//
// Create a new security descriptor to hold the new Dacl.
//
status = RtlCreateSecurityDescriptor( &NewSD, SECURITY_DESCRIPTOR_REVISION );
if (!NT_SUCCESS(status)) { break; }
//
// Place the new Dacl into the new SD
//
status = RtlSetDaclSecurityDescriptor( &NewSD, TRUE, NewDacl, FALSE );
if (!NT_SUCCESS(status)) { break; }
//
// Set the new SD into our device object. Only the Dacl from the
// SD will be set.
//
SecurityInformation = DACL_SECURITY_INFORMATION; status = ObSetSecurityObjectByPointer( NatDeviceObject, SecurityInformation, &NewSD );
} while (FALSE);
if (NULL != NatSD) { ObReleaseObjectSecurity(NatSD, MemoryAllocated); }
if (NULL != NewDacl) { ExFreePool(NewDacl); }
return status; } // NatAdjustSecurityDescriptor
VOID NatCleanupDriver( VOID )
/*++
Routine Description:
This routine is invoked when the last reference to the NAT driver is released.
Arguments:
none.
Return Value:
none.
--*/
{ CALLTRACE(("NatCleanupDriver\n"));
} // NatCleanupDriver
VOID NatCreateExternalNaming( IN PUNICODE_STRING DeviceString )
/*++
Routine Description:
Creates a symbolic-link to the NAT's device-object so the NAT can be opened by a user-mode process.
Arguments:
DeviceString - Unicode name of the NAT's device-object.
Return Value:
none.
--*/
{ UNICODE_STRING symLinkString; PAGED_CODE(); RtlInitUnicodeString(&symLinkString, ExternalName); IoCreateSymbolicLink(&symLinkString, DeviceString);
} // NatCreateExternalNaming
VOID NatDeleteExternalNaming( VOID )
/*++
Routine Description:
Deletes the Win32 symbolic-link to the NAT's device-object
Arguments:
Return Value:
none.
--*/
{ UNICODE_STRING symLinkString; PAGED_CODE(); RtlInitUnicodeString(&symLinkString, ExternalName); IoDeleteSymbolicLink(&symLinkString);
} // NatDeleteExternalNaming
NTSTATUS NatInitializeDriver( VOID )
/*++
Routine Description:
Performs initialization of the driver's structures.
Arguments:
none.
Return Value:
NTSTATUS - success/error code.
--*/
{ OBJECT_ATTRIBUTES ObjectAttributes; HANDLE ParametersKey; NTSTATUS status; NTSTATUS status2; UNICODE_STRING UnicodeString; IO_STATUS_BLOCK IoStatus;
CALLTRACE(("NatInitializeDriver\n"));
//
// Set up global synchronization objects
//
InitializeComponentReference(&ComponentReference, NatCleanupDriver);
//
// Obtain the IP and TCP driver device-objects
//
RtlInitUnicodeString(&UnicodeString, DD_IP_DEVICE_NAME); status = IoGetDeviceObjectPointer( &UnicodeString, SYNCHRONIZE|GENERIC_READ|GENERIC_WRITE, &IpFileObject, &IpDeviceObject ); if (!NT_SUCCESS(status)) { ERROR(("NatInitializeDriver: error %X getting IP object\n", status)); return status; }
RtlInitUnicodeString(&UnicodeString, DD_TCP_DEVICE_NAME); status = IoGetDeviceObjectPointer( &UnicodeString, SYNCHRONIZE|GENERIC_READ|GENERIC_WRITE, &TcpFileObject, &TcpDeviceObject ); if (!NT_SUCCESS(status)) { ERROR(("NatInitializeDriver: error %X getting TCP object\n", status)); return status; }
//
// Open Tcp Kernel Device
//
InitializeObjectAttributes( &ObjectAttributes, &UnicodeString, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
status = ZwCreateFile( &TcpDeviceHandle, GENERIC_READ, &ObjectAttributes, &IoStatus, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN_IF, 0, NULL, 0);
if ( !NT_SUCCESS(status) ) { ERROR(("ZwCreateFile failed (0x%08X)\n", status)); }
ObReferenceObject(IpDeviceObject); ObReferenceObject(TcpDeviceObject);
//
// Initialize all object-modules
//
NatInitializeTimerManagement(); NatInitializeMappingManagement(); NatInitializeDirectorManagement(); NatInitializeEditorManagement(); NatInitializeRedirectManagement(); NatInitializeDynamicTicketManagement(); NatInitializeIcmpManagement(); NatInitializeRawIpManagement(); NatInitializeInterfaceManagement(); #if 0
status = NatInitializeAddressManagement(); if (!NT_SUCCESS(status)) { return status; } #endif
NatInitializePacketManagement(); NatInitializeNotificationManagement();
#if NAT_WMI
NatInitializeWMI(); #endif
//
// Initialize NAT-provided editors.
//
status = NatInitializePptpManagement(); if (!NT_SUCCESS(status)) { return status; }
//
// Commence translation of packets, and start the periodic timer.
//
status = NatInitiateTranslation();
//
// Read optional registry settings.
// The user may customize the range of ports used by modifying
// the reserved-ports setting in the registry.
// We now check to see if there is such a value,
// and if so, we use it as our reserved-port range.
//
// The user may also specify that inbound non-unicast traffic
// is allowed on a firewalled interface.
//
// N.B. Failures here are not returned to the caller.
//
RtlInitUnicodeString(&UnicodeString, IpNatParametersPath); InitializeObjectAttributes( &ObjectAttributes, &UnicodeString, OBJ_CASE_INSENSITIVE, NULL, NULL );
status2 = ZwOpenKey(&ParametersKey, KEY_READ, &ObjectAttributes);
if (NT_SUCCESS(status2)) {
UCHAR Buffer[sizeof(KEY_VALUE_PARTIAL_INFORMATION)]; ULONG EndPort; PWCHAR p; ULONG StartPort; PKEY_VALUE_PARTIAL_INFORMATION Value = NULL; ULONG ValueLength;
//
// First check for allowed non-unicast traffic.
//
RtlInitUnicodeString( &UnicodeString, AllowInboundNonUnicastTrafficName );
status2 = ZwQueryValueKey( ParametersKey, &UnicodeString, KeyValuePartialInformation, (PKEY_VALUE_PARTIAL_INFORMATION)Buffer, sizeof(Buffer), &ValueLength ); if (NT_SUCCESS(status2) && REG_DWORD == ((PKEY_VALUE_PARTIAL_INFORMATION)Buffer)->Type) { AllowInboundNonUnicastTraffic = 1 == *((PULONG)((PKEY_VALUE_PARTIAL_INFORMATION)Buffer)->Data); }
//
// Check for reserved ports
//
do {
RtlInitUnicodeString(&UnicodeString, ReservedPortsName);
status2 = ZwQueryValueKey( ParametersKey, &UnicodeString, KeyValuePartialInformation, (PKEY_VALUE_PARTIAL_INFORMATION)Buffer, sizeof(Buffer), &ValueLength ); if (status2 != STATUS_BUFFER_OVERFLOW) { break; }
Value = (PKEY_VALUE_PARTIAL_INFORMATION) ExAllocatePoolWithTag( PagedPool, ValueLength, NAT_TAG_RANGE_ARRAY ); if (!Value) { break; }
status2 = ZwQueryValueKey( ParametersKey, &UnicodeString, KeyValuePartialInformation, (PKEY_VALUE_PARTIAL_INFORMATION)Value, ValueLength, &ValueLength ); if (!NT_SUCCESS(status2) || REG_SZ != Value->Type || L'\0' != *(PWCHAR) (Value->Data + (Value->DataLength - sizeof(WCHAR)))) {
break; }
//
// The value should be in the format "xxx-yyy\0\0";
// read the first number
//
p = (PWCHAR)Value->Data; RtlInitUnicodeString(&UnicodeString, p); status2 = RtlUnicodeStringToInteger(&UnicodeString, 10, &StartPort); if (!NT_SUCCESS(status2)) { break; }
//
// Advance past '-'
//
while (*p && *p != L'-') { ++p; } if (*p != L'-') { break; } else { ++p; }
//
// Read second number
//
RtlInitUnicodeString(&UnicodeString, p); status2 = RtlUnicodeStringToInteger(&UnicodeString, 10, &EndPort); if (!NT_SUCCESS(status2)) { break; }
//
// Validate the resulting range
//
if (StartPort > 0 && StartPort < 65535 && EndPort > 0 && EndPort < 65535 && StartPort <= EndPort ) { ReservedPortsLowerRange = NTOHS((USHORT)StartPort); ReservedPortsUpperRange = NTOHS((USHORT)EndPort); }
} while(FALSE);
if (Value) { ExFreePool(Value); } ZwClose(ParametersKey); }
return status; } // NatInitializeDriver
NTSTATUS NatInitiateTranslation( VOID )
/*++
Routine Description:
This routine is invoked on creation of the first interface, to launch the periodic timer and install the firewall hook.
Arguments:
none.
Return Value:
STATUS_SUCCESS if successful, error code otherwise.
--*/
{ CALLTRACE(("NatInitiateTranslation\n"));
//
// Launch the timer
//
NatStartTimer();
//
// Install 'NatTranslate' as the firewall hook
//
return NatSetFirewallHook(TRUE);
} // NatInitiateTranslation
NTSTATUS NatSetFirewallHook( BOOLEAN Install )
/*++
Routine Description:
This routine is called to set (Install==TRUE) or clear (Install==FALSE) the value of the firewall-callout function pointer in the IP driver.
Arguments:
Install - indicates whether to install or remove the hook.
Return Value:
NTSTATUS - indicates success/failure
Environment:
The routine assumes the caller is executing at PASSIVE_LEVEL.
--*/
{ IP_SET_FIREWALL_HOOK_INFO HookInfo; IO_STATUS_BLOCK IoStatus; PIRP Irp; TCP_RESERVE_PORT_RANGE PortRange; KEVENT LocalEvent; NTSTATUS status;
CALLTRACE(("NatSetFirewallHook\n"));
//
// Register (or deregister) as a firewall
//
HookInfo.FirewallPtr = (IPPacketFirewallPtr)NatTranslatePacket; HookInfo.Priority = 1; HookInfo.Add = Install;
KeInitializeEvent(&LocalEvent, SynchronizationEvent, FALSE); Irp = IoBuildDeviceIoControlRequest( IOCTL_IP_SET_FIREWALL_HOOK, IpDeviceObject, (PVOID)&HookInfo, sizeof(HookInfo), NULL, 0, FALSE, &LocalEvent, &IoStatus );
if (!Irp) { ERROR(("NatSetFirewallHook: IoBuildDeviceIoControlRequest=0\n")); return STATUS_UNSUCCESSFUL; }
status = IoCallDriver(IpDeviceObject, Irp); if (status == STATUS_PENDING) { KeWaitForSingleObject(&LocalEvent, Executive, KernelMode, FALSE, NULL); KeResetEvent(&LocalEvent); status = IoStatus.Status; }
if (!NT_SUCCESS(status)) { ERROR(("NatSetFirewallHook: IpSetFirewallHook=0x%08X\n", status)); return status; }
if (ReservedPortsLowerRange != DEFAULT_START_PORT || ReservedPortsUpperRange != DEFAULT_END_PORT ) { return STATUS_SUCCESS; }
//
// Reserve (or unreserve) our port-range
//
// N.B. The IOCTL expects host-order numbers and we store the range
// in network order, so do a swap before reserving the ports.
//
PortRange.LowerRange = NTOHS(DEFAULT_START_PORT); PortRange.UpperRange = NTOHS(DEFAULT_END_PORT); Irp = IoBuildDeviceIoControlRequest( Install ? IOCTL_TCP_RESERVE_PORT_RANGE : IOCTL_TCP_UNRESERVE_PORT_RANGE, TcpDeviceObject, (PVOID)&PortRange, sizeof(PortRange), NULL, 0, FALSE, &LocalEvent, &IoStatus ); if (!Irp) { ERROR(("NatSetFirewallHook: IoBuildDeviceIoControlRequest(2)=0\n")); return STATUS_UNSUCCESSFUL; }
status = IoCallDriver(TcpDeviceObject, Irp); if (status == STATUS_PENDING) { KeWaitForSingleObject(&LocalEvent, Executive, KernelMode, FALSE, NULL); status = IoStatus.Status; }
if (!NT_SUCCESS(status)) { ERROR(("NatSetFirewallHook: Tcp(Un)ReservePortRange=0x%08X\n", status)); }
return status;
} // NatSetFirewallHook
VOID NatTerminateTranslation( VOID )
/*++
Routine Description:
On cleanup of the last interface, this routine is invoked to stop the periodic timer and de-install the firewall hook.
Arguments:
none.
Return Value:
none.
--*/
{ CALLTRACE(("NatTerminateTranslation\n")); NatSetFirewallHook(FALSE);
} // NatTerminateTranslation
VOID NatUnloadDriver( IN PDRIVER_OBJECT DriverObject )
/*++
Routine Description:
Performs cleanup for the NAT.
Arguments:
DriverObject - reference to the NAT's driver-object
Return Value:
--*/
{ PNAT_EDITOR Editor; PLIST_ENTRY List;
CALLTRACE(("NatUnloadDriver\n"));
//
// Stop translation and clear the periodic timer
//
NatTerminateTranslation();
//
// Stop the route-change-notification in our packet-management and
// address-management modules.
// This forces completion of the route-change and address-change IRPs,
// which in turn releases component-references which would otherwise not
// drop until a route-change and address-change occurred.
//
NatShutdownNotificationManagement(); NatShutdownPacketManagement(); #if 0
NatShutdownAddressManagement(); #endif
//
// Drop our self-reference and wait for all activity to cease.
//
ReleaseInitialComponentReference(&ComponentReference, TRUE);
//
// Tear down our Win32-namespace symbolic link
//
NatDeleteExternalNaming();
//
// Delete the NAT's device object
//
IoDeleteDevice(DriverObject->DeviceObject);
//
// Shutdown object modules
//
#if NAT_WMI
NatShutdownWMI();
if( NatRegistryPath.Buffer ) { ExFreePool( NatRegistryPath.Buffer ); RtlInitUnicodeString( &NatRegistryPath, NULL ); } #endif
NatShutdownPptpManagement(); NatShutdownTimerManagement(); NatShutdownMappingManagement(); NatShutdownEditorManagement(); NatShutdownDirectorManagement(); NatShutdownDynamicTicketManagement(); NatShutdownRawIpManagement(); NatShutdownIcmpManagement(); NatShutdownInterfaceManagement();
//
// Release references to the IP and TCP driver objects
//
ObDereferenceObject((PVOID)IpFileObject); ObDereferenceObject(IpDeviceObject); ObDereferenceObject((PVOID)TcpFileObject); ObDereferenceObject(TcpDeviceObject);
if (TcpDeviceHandle) {
ZwClose(TcpDeviceHandle); TcpDeviceHandle = NULL; } DeleteComponentReference(&ComponentReference);
} // NatUnloadDriver
|