mirror of https://github.com/tongzx/nt5src
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
772 lines
16 KiB
772 lines
16 KiB
/*++
|
|
|
|
Copyright (c) 2001-2002 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
common.c
|
|
|
|
Abstract:
|
|
|
|
This module contains the teredo interface to the IPv6 Helper Service.
|
|
|
|
Author:
|
|
|
|
Mohit Talwar (mohitt) Wed Nov 07 11:27:01 2001
|
|
|
|
Environment:
|
|
|
|
User mode only.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#pragma hdrstop
|
|
|
|
CONST IN6_ADDR TeredoIpv6ServicePrefix = TEREDO_SERVICE_PREFIX;
|
|
CONST IN6_ADDR TeredoIpv6MulticastPrefix = TEREDO_MULTICAST_PREFIX;
|
|
|
|
#define DEVICE_PREFIX L"\\Device\\"
|
|
|
|
LPGUID TeredoWmiEvent[] = {
|
|
(LPGUID) &GUID_NDIS_NOTIFY_ADAPTER_ARRIVAL,
|
|
(LPGUID) &GUID_NDIS_NOTIFY_ADAPTER_REMOVAL,
|
|
(LPGUID) &GUID_NDIS_NOTIFY_DEVICE_POWER_ON,
|
|
(LPGUID) &GUID_NDIS_NOTIFY_DEVICE_POWER_OFF,
|
|
};
|
|
|
|
HANDLE TeredoTimer; // Periodic timer started for the service.
|
|
HANDLE TeredoTimerEvent; // Event signalled upon Timer deletion.
|
|
HANDLE TeredoTimerEventWait; // Wait registered for TimerEvent.
|
|
ULONG TeredoResolveInterval = TEREDO_RESOLVE_INTERVAL;
|
|
|
|
ULONG TeredoClientRefreshInterval = TEREDO_REFRESH_INTERVAL;
|
|
BOOL TeredoClientEnabled = (TEREDO_DEFAULT_TYPE == TEREDO_CLIENT);
|
|
BOOL TeredoServerEnabled = (TEREDO_DEFAULT_TYPE == TEREDO_SERVER);
|
|
WCHAR TeredoServerName[NI_MAXHOST] = TEREDO_SERVER_NAME;
|
|
|
|
BOOL TeredoInitialized = FALSE;
|
|
|
|
|
|
ICMPv6Header *
|
|
TeredoParseIpv6Headers (
|
|
IN PUCHAR Buffer,
|
|
IN ULONG Bytes
|
|
)
|
|
{
|
|
UCHAR NextHeader = IP_PROTOCOL_V6;
|
|
ULONG Length;
|
|
|
|
//
|
|
// Parse up until the ICMPv6 header.
|
|
//
|
|
for (;;) {
|
|
switch (NextHeader) {
|
|
case IP_PROTOCOL_V6:
|
|
if (Bytes < sizeof(IP6_HDR)) {
|
|
return NULL;
|
|
}
|
|
NextHeader = ((PIP6_HDR) Buffer)->ip6_nxt;
|
|
Length = sizeof(IP6_HDR);
|
|
break;
|
|
|
|
case IP_PROTOCOL_HOP_BY_HOP:
|
|
case IP_PROTOCOL_DEST_OPTS:
|
|
case IP_PROTOCOL_ROUTING:
|
|
if (Bytes < sizeof(ExtensionHeader)) {
|
|
return NULL;
|
|
}
|
|
NextHeader = ((ExtensionHeader *) Buffer)->NextHeader;
|
|
Length = ((ExtensionHeader *) Buffer)->HeaderExtLength * 8 + 8;
|
|
break;
|
|
|
|
case IP_PROTOCOL_FRAGMENT:
|
|
if (Bytes < sizeof(FragmentHeader)) {
|
|
return NULL;
|
|
}
|
|
NextHeader = ((FragmentHeader *) Buffer)->NextHeader;
|
|
Length = sizeof(FragmentHeader);
|
|
break;
|
|
|
|
case IP_PROTOCOL_AH:
|
|
if (Bytes < sizeof(AHHeader)) {
|
|
return NULL;
|
|
}
|
|
NextHeader = ((AHHeader *) Buffer)->NextHeader;
|
|
Length = sizeof(AHHeader) +
|
|
((AHHeader *) Buffer)->PayloadLen * 4 + 8;
|
|
break;
|
|
|
|
case IP_PROTOCOL_ICMPv6:
|
|
if (Bytes < sizeof(ICMPv6Header)) {
|
|
return NULL;
|
|
}
|
|
return (ICMPv6Header *) Buffer;
|
|
|
|
default:
|
|
return NULL;
|
|
}
|
|
|
|
if (Bytes < Length) {
|
|
return NULL;
|
|
}
|
|
Buffer += Length;
|
|
Bytes -= Length;
|
|
}
|
|
}
|
|
|
|
|
|
__inline
|
|
VOID
|
|
TeredoStart(
|
|
VOID
|
|
)
|
|
{
|
|
//
|
|
// Both client and server should not be enabled on the same node.
|
|
//
|
|
ASSERT(!TeredoClientEnabled || !TeredoServerEnabled);
|
|
|
|
if (TeredoClientEnabled) {
|
|
//
|
|
// The service might already be running, but that's alright.
|
|
//
|
|
TeredoStartClient();
|
|
}
|
|
|
|
if (TeredoServerEnabled) {
|
|
//
|
|
// The service might already be running, but that's alright.
|
|
//
|
|
TeredoStartServer();
|
|
}
|
|
}
|
|
|
|
|
|
__inline
|
|
VOID
|
|
TeredoStop(
|
|
VOID
|
|
)
|
|
{
|
|
//
|
|
// Both client and server should not be enabled on the same node.
|
|
//
|
|
ASSERT(!TeredoClientEnabled || !TeredoServerEnabled);
|
|
|
|
if (TeredoClientEnabled) {
|
|
//
|
|
// The service might not be running, but that's all right.
|
|
//
|
|
TeredoStopClient();
|
|
}
|
|
|
|
if (TeredoServerEnabled) {
|
|
//
|
|
// The service might not be running, but that's all right.
|
|
//
|
|
TeredoStopServer();
|
|
}
|
|
}
|
|
|
|
|
|
DWORD
|
|
__inline
|
|
TeredoEnableWmiEvent(
|
|
IN LPGUID EventGuid,
|
|
IN BOOLEAN Enable
|
|
)
|
|
{
|
|
return WmiNotificationRegistrationW(
|
|
EventGuid, // Event Type.
|
|
Enable, // Enable or Disable.
|
|
TeredoWmiEventNotification, // Callback.
|
|
0, // Context.
|
|
NOTIFICATION_CALLBACK_DIRECT); // Notification Flags.
|
|
}
|
|
|
|
|
|
VOID
|
|
__inline
|
|
TeredoDeregisterWmiEventNotification(
|
|
VOID
|
|
)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < (sizeof(TeredoWmiEvent) / sizeof(LPGUID)); i++) {
|
|
(VOID) TeredoEnableWmiEvent(TeredoWmiEvent[i], FALSE);
|
|
}
|
|
}
|
|
|
|
|
|
DWORD
|
|
__inline
|
|
TeredoRegisterWmiEventNotification(
|
|
VOID
|
|
)
|
|
{
|
|
DWORD Error;
|
|
int i;
|
|
|
|
for (i = 0; i < (sizeof(TeredoWmiEvent) / sizeof(LPGUID)); i++) {
|
|
Error = TeredoEnableWmiEvent(TeredoWmiEvent[i], TRUE);
|
|
if (Error != NO_ERROR) {
|
|
Trace2(ANY, L"TeredoEnableWmiEvent(%u): Error(%x)", i, Error);
|
|
goto Bail;
|
|
}
|
|
}
|
|
|
|
return NO_ERROR;
|
|
|
|
Bail:
|
|
TeredoDeregisterWmiEventNotification();
|
|
return Error;
|
|
}
|
|
|
|
|
|
VOID
|
|
CALLBACK
|
|
TeredoTimerCallback(
|
|
IN PVOID Parameter,
|
|
IN BOOLEAN TimerOrWaitFired
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Callback routine for TeredoTimer expiration.
|
|
The timer is always active.
|
|
|
|
Arguments:
|
|
|
|
Parameter, TimerOrWaitFired - Ignored.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
ENTER_API();
|
|
TeredoStart();
|
|
LEAVE_API();
|
|
}
|
|
|
|
|
|
VOID
|
|
CALLBACK
|
|
TeredoTimerCleanup(
|
|
IN PVOID Parameter,
|
|
IN BOOLEAN TimerOrWaitFired
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Callback routine for TeredoTimer deletion.
|
|
|
|
Deletion is performed asynchronously since we acquire a lock in
|
|
the callback function that we hold when deleting the timer.
|
|
|
|
Arguments:
|
|
|
|
Parameter, TimerOrWaitFired - Ignored.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
UnregisterWait(TeredoTimerEventWait);
|
|
CloseHandle(TeredoTimerEvent);
|
|
DecEventCount("TeredoCleanupTimer");
|
|
}
|
|
|
|
|
|
DWORD
|
|
TeredoInitializeTimer(
|
|
VOID
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Initializes the timer.
|
|
|
|
Arguments:
|
|
|
|
None.
|
|
|
|
Return Value:
|
|
|
|
NO_ERROR or failure code.
|
|
|
|
--*/
|
|
{
|
|
DWORD Error;
|
|
ULONG ResolveInterval;
|
|
|
|
TeredoTimerEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
|
|
if (TeredoTimerEvent == NULL) {
|
|
Error = GetLastError();
|
|
return Error;
|
|
}
|
|
|
|
if (!RegisterWaitForSingleObject(
|
|
&(TeredoTimerEventWait),
|
|
TeredoTimerEvent,
|
|
TeredoTimerCleanup,
|
|
NULL,
|
|
INFINITE,
|
|
0)) {
|
|
Error = GetLastError();
|
|
CloseHandle(TeredoTimerEvent);
|
|
return Error;
|
|
}
|
|
|
|
//
|
|
// If the service is enabled, we attempt to start it every
|
|
// TeredoResolveInterval seconds. Else we disable its timer.
|
|
//
|
|
ResolveInterval = (TEREDO_DEFAULT_TYPE == TEREDO_DISABLE)
|
|
? INFINITE_INTERVAL
|
|
: (TeredoResolveInterval * 1000);
|
|
if (!CreateTimerQueueTimer(
|
|
&(TeredoTimer),
|
|
NULL,
|
|
TeredoTimerCallback,
|
|
NULL,
|
|
ResolveInterval,
|
|
ResolveInterval,
|
|
0)) {
|
|
Error = GetLastError();
|
|
UnregisterWait(TeredoTimerEventWait);
|
|
CloseHandle(TeredoTimerEvent);
|
|
return Error;
|
|
}
|
|
|
|
IncEventCount("TeredoInitializeTimer");
|
|
return NO_ERROR;
|
|
}
|
|
|
|
|
|
VOID
|
|
TeredoUninitializeTimer(
|
|
VOID
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Uninitializes the timer. Typically invoked upon service stop.
|
|
|
|
Arguments:
|
|
|
|
None.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
DeleteTimerQueueTimer(NULL, TeredoTimer, TeredoTimerEvent);
|
|
TeredoTimer = NULL;
|
|
}
|
|
|
|
|
|
DWORD
|
|
TeredoInitializeGlobals(
|
|
VOID
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Initializes the teredo client and server and attempts to start them.
|
|
|
|
Arguments:
|
|
|
|
None.
|
|
|
|
Return Value:
|
|
|
|
NO_ERROR or failure code.
|
|
|
|
--*/
|
|
{
|
|
DWORD Error;
|
|
BOOL ClientInitialized = FALSE;
|
|
BOOL ServerInitialized = FALSE;
|
|
BOOL TimerInitialized = FALSE;
|
|
|
|
Error = TeredoInitializeClient();
|
|
if (Error != NO_ERROR) {
|
|
goto Bail;
|
|
}
|
|
ClientInitialized = TRUE;
|
|
|
|
Error = TeredoInitializeServer();
|
|
if (Error != NO_ERROR) {
|
|
goto Bail;
|
|
}
|
|
ServerInitialized = TRUE;
|
|
|
|
Error = TeredoInitializeTimer();
|
|
if (Error != NO_ERROR) {
|
|
goto Bail;
|
|
}
|
|
TimerInitialized = TRUE;
|
|
|
|
Error = TeredoRegisterWmiEventNotification();
|
|
if (Error != NO_ERROR) {
|
|
goto Bail;
|
|
}
|
|
|
|
TeredoStart();
|
|
|
|
TeredoInitialized = TRUE;
|
|
|
|
return NO_ERROR;
|
|
|
|
Bail:
|
|
//
|
|
// This can always be safely invoked!
|
|
//
|
|
TeredoDeregisterWmiEventNotification();
|
|
|
|
if (TimerInitialized) {
|
|
TeredoUninitializeTimer();
|
|
}
|
|
|
|
if (ServerInitialized) {
|
|
TeredoUninitializeServer();
|
|
}
|
|
|
|
if (ClientInitialized) {
|
|
TeredoUninitializeClient();
|
|
}
|
|
|
|
return Error;
|
|
}
|
|
|
|
|
|
VOID
|
|
TeredoUninitializeGlobals(
|
|
VOID
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Uninitializes the teredo client and server.
|
|
|
|
Arguments:
|
|
|
|
None.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
if (!TeredoInitialized) {
|
|
return;
|
|
}
|
|
|
|
TeredoDeregisterWmiEventNotification();
|
|
TeredoUninitializeTimer();
|
|
TeredoUninitializeServer();
|
|
TeredoUninitializeClient();
|
|
|
|
TeredoInitialized = FALSE;
|
|
}
|
|
|
|
|
|
VOID
|
|
TeredoAddressChangeNotification(
|
|
IN BOOL Delete,
|
|
IN IN_ADDR Address
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Process an address deletion or addition request.
|
|
|
|
Arguments:
|
|
|
|
Delete - Supplies a boolean. TRUE if the address was deleted, FALSE o/w.
|
|
|
|
Address - Supplies the IPv4 address that was deleted or added.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
Caller LOCK: API.
|
|
|
|
--*/
|
|
{
|
|
if (Delete) {
|
|
//
|
|
// Both client and server should not be running on the same node.
|
|
//
|
|
ASSERT((TeredoClient.State == TEREDO_STATE_OFFLINE) ||
|
|
(TeredoServer.State == TEREDO_STATE_OFFLINE));
|
|
|
|
if (TeredoClient.State != TEREDO_STATE_OFFLINE) {
|
|
TeredoClientAddressDeletionNotification(Address);
|
|
}
|
|
|
|
if (TeredoServer.State != TEREDO_STATE_OFFLINE) {
|
|
TeredoServerAddressDeletionNotification(Address);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Address addition.
|
|
// Attempt to start the service (if it is not already running).
|
|
//
|
|
TeredoStart();
|
|
}
|
|
|
|
|
|
VOID
|
|
TeredoConfigurationChangeNotification(
|
|
VOID
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Process an configuration change request.
|
|
|
|
Arguments:
|
|
|
|
None.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
Caller LOCK: API.
|
|
|
|
--*/
|
|
{
|
|
HKEY Key = INVALID_HANDLE_VALUE;
|
|
TEREDO_TYPE Type;
|
|
BOOL EnableClient, EnableServer;
|
|
ULONG RefreshInterval, ResolveInterval;
|
|
WCHAR OldServerName[NI_MAXHOST];
|
|
BOOL IoStateChange = FALSE;
|
|
|
|
(VOID) RegOpenKeyExW(
|
|
HKEY_LOCAL_MACHINE, KEY_TEREDO, 0, KEY_QUERY_VALUE, &Key);
|
|
//
|
|
// Continue despite errors, reverting to default values.
|
|
//
|
|
|
|
//
|
|
// Get the new configuration parameters.
|
|
//
|
|
RefreshInterval = GetInteger(
|
|
Key, KEY_TEREDO_REFRESH_INTERVAL, TEREDO_REFRESH_INTERVAL);
|
|
if (RefreshInterval == 0) {
|
|
//
|
|
// Invalid value. Revert to default.
|
|
//
|
|
RefreshInterval = TEREDO_REFRESH_INTERVAL;
|
|
}
|
|
TeredoClientRefreshInterval = RefreshInterval;
|
|
TeredoClientRefreshIntervalChangeNotification();
|
|
|
|
Type = GetInteger(Key, KEY_TEREDO_TYPE, TEREDO_DEFAULT_TYPE);
|
|
if ((Type == TEREDO_DEFAULT) || (Type >= TEREDO_MAXIMUM)) {
|
|
//
|
|
// Invalid value. Revert to default.
|
|
//
|
|
Type = TEREDO_DEFAULT_TYPE;
|
|
}
|
|
EnableClient = (Type == TEREDO_CLIENT);
|
|
EnableServer = (Type == TEREDO_SERVER);
|
|
|
|
wcscpy(OldServerName, TeredoServerName);
|
|
GetString(
|
|
Key,
|
|
KEY_TEREDO_SERVER_NAME,
|
|
TeredoServerName,
|
|
NI_MAXHOST,
|
|
TEREDO_SERVER_NAME);
|
|
if (_wcsicmp(TeredoServerName, OldServerName) != 0) {
|
|
IoStateChange = TRUE;
|
|
}
|
|
|
|
RegCloseKey(Key);
|
|
|
|
//
|
|
// Both client and server should not be enabled on the same node.
|
|
//
|
|
ASSERT(!TeredoClientEnabled || !TeredoServerEnabled);
|
|
|
|
//
|
|
// Stop / Start / Reconfigure.
|
|
//
|
|
if (!EnableClient && TeredoClientEnabled) {
|
|
TeredoClientEnabled = FALSE;
|
|
TeredoStopClient();
|
|
}
|
|
|
|
if (!EnableServer && TeredoServerEnabled) {
|
|
TeredoServerEnabled = FALSE;
|
|
TeredoStopServer();
|
|
}
|
|
|
|
if (EnableClient) {
|
|
if (TeredoClient.State != TEREDO_STATE_OFFLINE) {
|
|
if (IoStateChange) {
|
|
//
|
|
// Refresh I/O state.
|
|
//
|
|
TeredoClientAddressDeletionNotification(
|
|
TeredoClient.Io.SourceAddress.sin_addr);
|
|
}
|
|
} else {
|
|
TeredoClientEnabled = TRUE;
|
|
TeredoStartClient();
|
|
}
|
|
}
|
|
|
|
if (EnableServer) {
|
|
if (TeredoServer.State != TEREDO_STATE_OFFLINE) {
|
|
if (IoStateChange) {
|
|
//
|
|
// Refresh I/O state.
|
|
//
|
|
TeredoServerAddressDeletionNotification(
|
|
TeredoServer.Io.SourceAddress.sin_addr);
|
|
}
|
|
} else {
|
|
TeredoServerEnabled = TRUE;
|
|
TeredoStartServer();
|
|
}
|
|
}
|
|
|
|
//
|
|
// If the service is enabled, we attempt to start it every
|
|
// TeredoResolveInterval seconds. Else we disable its timer.
|
|
//
|
|
ResolveInterval = (Type == TEREDO_DISABLE)
|
|
? INFINITE_INTERVAL
|
|
: (TeredoResolveInterval * 1000);
|
|
(VOID) ChangeTimerQueueTimer(
|
|
NULL, TeredoTimer, ResolveInterval, ResolveInterval);
|
|
}
|
|
|
|
|
|
VOID
|
|
WINAPI
|
|
TeredoWmiEventNotification(
|
|
IN PWNODE_HEADER Event,
|
|
IN UINT_PTR Context
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Process a WMI event (specifically adapter arrival or removal).
|
|
|
|
Arguments:
|
|
|
|
Event - Supplies event specific information.
|
|
|
|
Context - Supplies the context registered.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
PWNODE_SINGLE_INSTANCE Instance = (PWNODE_SINGLE_INSTANCE) Event;
|
|
USHORT AdapterNameLength;
|
|
WCHAR AdapterName[MAX_ADAPTER_NAME_LENGTH], *AdapterGuid;
|
|
PTEREDO_IO Io = NULL;
|
|
|
|
if (Instance == NULL) {
|
|
return;
|
|
}
|
|
|
|
ENTER_API();
|
|
|
|
TraceEnter("TeredoWmiEventNotification");
|
|
|
|
//
|
|
// WNODE_SINGLE_INSTANCE is organized thus...
|
|
// +-----------------------------------------------------------+
|
|
// |<--- DataBlockOffset --->| AdapterNameLength | AdapterName |
|
|
// +-----------------------------------------------------------+
|
|
//
|
|
// AdapterName is defined as "\DEVICE\"AdapterGuid
|
|
//
|
|
AdapterNameLength =
|
|
*((PUSHORT) (((PUCHAR) Instance) + Instance->DataBlockOffset));
|
|
RtlCopyMemory(
|
|
AdapterName,
|
|
((PUCHAR) Instance) + Instance->DataBlockOffset + sizeof(USHORT),
|
|
AdapterNameLength);
|
|
AdapterName[AdapterNameLength / sizeof(WCHAR)] = L'\0';
|
|
AdapterGuid = AdapterName + wcslen(DEVICE_PREFIX);
|
|
Trace1(ANY, L"TeredoAdapter: %s", AdapterGuid);
|
|
|
|
|
|
if ((memcmp(
|
|
&(Event->Guid),
|
|
&GUID_NDIS_NOTIFY_ADAPTER_ARRIVAL,
|
|
sizeof(GUID)) == 0) ||
|
|
(memcmp(
|
|
&(Event->Guid),
|
|
&GUID_NDIS_NOTIFY_DEVICE_POWER_ON,
|
|
sizeof(GUID)) == 0)) {
|
|
//
|
|
// Adapter arrival (perhaps TUN).
|
|
// Attempt to start the service (if it is not already running).
|
|
//
|
|
Trace0(ANY, L"Adapter Arrival");
|
|
TeredoStart();
|
|
}
|
|
|
|
if ((memcmp(
|
|
&(Event->Guid),
|
|
&GUID_NDIS_NOTIFY_ADAPTER_REMOVAL,
|
|
sizeof(GUID)) == 0) ||
|
|
(memcmp(
|
|
&(Event->Guid),
|
|
&GUID_NDIS_NOTIFY_DEVICE_POWER_OFF,
|
|
sizeof(GUID)) == 0)) {
|
|
if (TeredoClient.State != TEREDO_STATE_OFFLINE) {
|
|
Io = &(TeredoClient.Io);
|
|
}
|
|
|
|
if (TeredoServer.State != TEREDO_STATE_OFFLINE) {
|
|
Io = &(TeredoServer.Io);
|
|
}
|
|
|
|
if ((Io != NULL) &&
|
|
(_wcsicmp(Io->TunnelInterface, AdapterGuid) == 0)) {
|
|
//
|
|
// TUN adapter removal.
|
|
// Stop the service if it is running.
|
|
//
|
|
Trace0(ANY, L"Adapter Removal");
|
|
TeredoStop();
|
|
}
|
|
}
|
|
|
|
LEAVE_API();
|
|
}
|