Piggy-back on top of NDIS' device object instead of adding our own
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
bf51c91e3e
commit
58ce3c5000
@ -99,7 +99,8 @@ StatementMacros: [
|
|||||||
'__drv_allocatesMem',
|
'__drv_allocatesMem',
|
||||||
'__drv_freesMem',
|
'__drv_freesMem',
|
||||||
'_Field_size_bytes_',
|
'_Field_size_bytes_',
|
||||||
'_Function_class_'
|
'_Function_class_',
|
||||||
|
'_Dispatch_type_'
|
||||||
]
|
]
|
||||||
TabWidth: '4'
|
TabWidth: '4'
|
||||||
UseTab: Never
|
UseTab: Never
|
||||||
|
@ -81,7 +81,7 @@ Note: due to the use of SHA256 signatures throughout, Windows 7 users who would
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
After loading the driver and creating a network interface the typical way using [SetupAPI](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/setupapi), open `\\.\Global\WINTUN%d` as Local System, where `%d` is the [LUID](https://docs.microsoft.com/en-us/windows/desktop/api/ifdef/ns-ifdef-_net_luid_lh) index (`NetLuidIndex` member) of the network device.
|
After loading the driver and creating a network interface the typical way using [SetupAPI](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/setupapi), open the NDIS device object associated with the PnPInstanceId.
|
||||||
|
|
||||||
### Ring layout
|
### Ring layout
|
||||||
|
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
/* SPDX-License-Identifier: GPL-2.0
|
|
||||||
*
|
|
||||||
* Copyright (C) 2018-2019 WireGuard LLC. All Rights Reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <wdm.h>
|
|
||||||
|
|
||||||
typedef enum
|
|
||||||
{
|
|
||||||
SystemExtendedHandleInformation = 0x40
|
|
||||||
} SYSTEM_INFORMATION_CLASS;
|
|
||||||
|
|
||||||
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX
|
|
||||||
{
|
|
||||||
PVOID Object;
|
|
||||||
HANDLE UniqueProcessId;
|
|
||||||
HANDLE HandleValue;
|
|
||||||
ACCESS_MASK GrantedAccess;
|
|
||||||
USHORT CreatorBackTraceIndex;
|
|
||||||
USHORT ObjectTypeIndex;
|
|
||||||
ULONG HandleAttributes;
|
|
||||||
ULONG Reserved;
|
|
||||||
} SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX;
|
|
||||||
|
|
||||||
typedef struct _SYSTEM_HANDLE_INFORMATION_EX
|
|
||||||
{
|
|
||||||
ULONG_PTR NumberOfHandles;
|
|
||||||
ULONG_PTR Reserved;
|
|
||||||
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[ANYSIZE_ARRAY];
|
|
||||||
} SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;
|
|
||||||
|
|
||||||
extern NTSTATUS
|
|
||||||
ZwQuerySystemInformation(
|
|
||||||
SYSTEM_INFORMATION_CLASS SystemInformationClass,
|
|
||||||
PVOID SystemInformation,
|
|
||||||
ULONG SystemInformationLength,
|
|
||||||
ULONG *ReturnLength);
|
|
||||||
|
|
||||||
extern NDIS_HANDLE
|
|
||||||
NdisWdfGetAdapterContextFromAdapterHandle(PVOID DeviceExtension);
|
|
||||||
|
|
||||||
extern POBJECT_TYPE *IoDeviceObjectType;
|
|
362
wintun.c
362
wintun.c
@ -8,7 +8,6 @@
|
|||||||
#include <wdmsec.h>
|
#include <wdmsec.h>
|
||||||
#include <ndis.h>
|
#include <ndis.h>
|
||||||
#include <ntstrsafe.h>
|
#include <ntstrsafe.h>
|
||||||
#include "undocumented.h"
|
|
||||||
|
|
||||||
#pragma warning(disable : 4100) /* unreferenced formal parameter */
|
#pragma warning(disable : 4100) /* unreferenced formal parameter */
|
||||||
#pragma warning(disable : 4200) /* nonstandard: zero-sized array in struct/union */
|
#pragma warning(disable : 4200) /* nonstandard: zero-sized array in struct/union */
|
||||||
@ -19,9 +18,6 @@
|
|||||||
#define NDIS_MINIPORT_VERSION_MIN ((NDIS_MINIPORT_MINIMUM_MAJOR_VERSION << 16) | NDIS_MINIPORT_MINIMUM_MINOR_VERSION)
|
#define NDIS_MINIPORT_VERSION_MIN ((NDIS_MINIPORT_MINIMUM_MAJOR_VERSION << 16) | NDIS_MINIPORT_MINIMUM_MINOR_VERSION)
|
||||||
#define NDIS_MINIPORT_VERSION_MAX ((NDIS_MINIPORT_MAJOR_VERSION << 16) | NDIS_MINIPORT_MINOR_VERSION)
|
#define NDIS_MINIPORT_VERSION_MAX ((NDIS_MINIPORT_MAJOR_VERSION << 16) | NDIS_MINIPORT_MINOR_VERSION)
|
||||||
|
|
||||||
/* Data device name */
|
|
||||||
#define TUN_DEVICE_NAME L"WINTUN%u"
|
|
||||||
|
|
||||||
#define TUN_VENDOR_NAME "Wintun Tunnel"
|
#define TUN_VENDOR_NAME "Wintun Tunnel"
|
||||||
#define TUN_VENDOR_ID 0xFFFFFF00
|
#define TUN_VENDOR_ID 0xFFFFFF00
|
||||||
#define TUN_LINK_SPEED 100000000000ULL /* 100gbps */
|
#define TUN_LINK_SPEED 100000000000ULL /* 100gbps */
|
||||||
@ -110,8 +106,8 @@ typedef struct _TUN_REGISTER_RINGS
|
|||||||
|
|
||||||
typedef enum _TUN_FLAGS
|
typedef enum _TUN_FLAGS
|
||||||
{
|
{
|
||||||
TUN_FLAGS_RUNNING = 1 << 0, /* Toggles between paused and running state */
|
TUN_FLAGS_RUNNING = 1 << 0, /* Toggles between paused and running state */
|
||||||
TUN_FLAGS_PRESENT = 1 << 1, /* Toggles between removal pending and being present */
|
TUN_FLAGS_PRESENT = 1 << 1, /* Toggles between removal pending and being present */
|
||||||
} TUN_FLAGS;
|
} TUN_FLAGS;
|
||||||
|
|
||||||
typedef struct _TUN_CTX
|
typedef struct _TUN_CTX
|
||||||
@ -124,13 +120,11 @@ typedef struct _TUN_CTX
|
|||||||
EX_SPIN_LOCK TransitionLock;
|
EX_SPIN_LOCK TransitionLock;
|
||||||
|
|
||||||
NDIS_HANDLE MiniportAdapterHandle; /* This is actually a pointer to NDIS_MINIPORT_BLOCK struct. */
|
NDIS_HANDLE MiniportAdapterHandle; /* This is actually a pointer to NDIS_MINIPORT_BLOCK struct. */
|
||||||
|
DEVICE_OBJECT *FunctionalDeviceObject;
|
||||||
NDIS_STATISTICS_INFO Statistics;
|
NDIS_STATISTICS_INFO Statistics;
|
||||||
|
|
||||||
struct
|
struct
|
||||||
{
|
{
|
||||||
NDIS_HANDLE Handle;
|
|
||||||
DEVICE_OBJECT *Object;
|
|
||||||
IO_REMOVE_LOCK RemoveLock;
|
|
||||||
FILE_OBJECT *volatile Owner;
|
FILE_OBJECT *volatile Owner;
|
||||||
KEVENT Disconnected;
|
KEVENT Disconnected;
|
||||||
|
|
||||||
@ -141,7 +135,8 @@ typedef struct _TUN_CTX
|
|||||||
ULONG Capacity;
|
ULONG Capacity;
|
||||||
KEVENT *TailMoved;
|
KEVENT *TailMoved;
|
||||||
KSPIN_LOCK Lock;
|
KSPIN_LOCK Lock;
|
||||||
ULONG RingTail; /* We need a private tail offset to keep track of ring allocation without disturbing the client. */
|
ULONG RingTail; /* We need a private tail offset to keep track of ring allocation without disturbing the
|
||||||
|
* client. */
|
||||||
struct
|
struct
|
||||||
{
|
{
|
||||||
NET_BUFFER_LIST *Head, *Tail;
|
NET_BUFFER_LIST *Head, *Tail;
|
||||||
@ -163,8 +158,8 @@ typedef struct _TUN_CTX
|
|||||||
|
|
||||||
static UINT NdisVersion;
|
static UINT NdisVersion;
|
||||||
static NDIS_HANDLE NdisMiniportDriverHandle;
|
static NDIS_HANDLE NdisMiniportDriverHandle;
|
||||||
static DRIVER_DISPATCH *NdisDispatchPnP;
|
static DRIVER_DISPATCH *NdisDispatchPnP, *NdisDispatchDeviceControl, *NdisDispatchClose;
|
||||||
static volatile LONG64 TunAdapterCount;
|
static ERESOURCE TunCtxDispatchGuard;
|
||||||
|
|
||||||
static __forceinline ULONG
|
static __forceinline ULONG
|
||||||
InterlockedExchangeU(_Inout_ _Interlocked_operand_ ULONG volatile *Target, _In_ ULONG Value)
|
InterlockedExchangeU(_Inout_ _Interlocked_operand_ ULONG volatile *Target, _In_ ULONG Value)
|
||||||
@ -444,7 +439,8 @@ TunProcessReceiveData(_Inout_ TUN_CTX *Ctx)
|
|||||||
RingTail = InterlockedGetU(&Ring->Tail);
|
RingTail = InterlockedGetU(&Ring->Tail);
|
||||||
if (RingHead == RingTail)
|
if (RingHead == RingTail)
|
||||||
{
|
{
|
||||||
KeWaitForMultipleObjects(RTL_NUMBER_OF(Events), Events, WaitAny, Executive, KernelMode, FALSE, NULL, NULL);
|
KeWaitForMultipleObjects(
|
||||||
|
RTL_NUMBER_OF(Events), Events, WaitAny, Executive, KernelMode, FALSE, NULL, NULL);
|
||||||
InterlockedExchange(&Ring->Alertable, FALSE);
|
InterlockedExchange(&Ring->Alertable, FALSE);
|
||||||
Irql = ExAcquireSpinLockShared(&Ctx->TransitionLock);
|
Irql = ExAcquireSpinLockShared(&Ctx->TransitionLock);
|
||||||
continue;
|
continue;
|
||||||
@ -526,34 +522,16 @@ TunProcessReceiveData(_Inout_ TUN_CTX *Ctx)
|
|||||||
ExReleaseSpinLockShared(&Ctx->TransitionLock, Irql);
|
ExReleaseSpinLockShared(&Ctx->TransitionLock, Irql);
|
||||||
}
|
}
|
||||||
|
|
||||||
_IRQL_requires_max_(APC_LEVEL)
|
|
||||||
_Must_inspect_result_
|
|
||||||
static NTSTATUS
|
|
||||||
TunDispatchCreate(_Inout_ TUN_CTX *Ctx, _Inout_ IRP *Irp)
|
|
||||||
{
|
|
||||||
NTSTATUS Status;
|
|
||||||
|
|
||||||
KIRQL Irql = ExAcquireSpinLockShared(&Ctx->TransitionLock);
|
|
||||||
LONG Flags = InterlockedGet(&Ctx->Flags);
|
|
||||||
if (Status = STATUS_DELETE_PENDING, !(Flags & TUN_FLAGS_PRESENT))
|
|
||||||
goto cleanupReleaseSpinLock;
|
|
||||||
|
|
||||||
IO_STACK_LOCATION *Stack = IoGetCurrentIrpStackLocation(Irp);
|
|
||||||
Status = IoAcquireRemoveLock(&Ctx->Device.RemoveLock, Stack->FileObject);
|
|
||||||
|
|
||||||
cleanupReleaseSpinLock:
|
|
||||||
ExReleaseSpinLockShared(&Ctx->TransitionLock, Irql);
|
|
||||||
return Status;
|
|
||||||
}
|
|
||||||
|
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
_Must_inspect_result_
|
_Must_inspect_result_
|
||||||
static NTSTATUS
|
static NTSTATUS
|
||||||
TunDispatchRegisterBuffers(_Inout_ TUN_CTX *Ctx, _Inout_ IRP *Irp)
|
TunRegisterBuffers(_Inout_ TUN_CTX *Ctx, _Inout_ IRP *Irp)
|
||||||
{
|
{
|
||||||
NTSTATUS Status;
|
NTSTATUS Status;
|
||||||
IO_STACK_LOCATION *Stack = IoGetCurrentIrpStackLocation(Irp);
|
IO_STACK_LOCATION *Stack = IoGetCurrentIrpStackLocation(Irp);
|
||||||
|
|
||||||
|
/* TODO TODO TODO: SeCaptureSubjectContext, SeAccessCheck */
|
||||||
|
|
||||||
if (InterlockedCompareExchangePointer(&Ctx->Device.Owner, Stack->FileObject, NULL) != NULL)
|
if (InterlockedCompareExchangePointer(&Ctx->Device.Owner, Stack->FileObject, NULL) != NULL)
|
||||||
return STATUS_ALREADY_INITIALIZED;
|
return STATUS_ALREADY_INITIALIZED;
|
||||||
|
|
||||||
@ -667,7 +645,7 @@ cleanupResetOwner:
|
|||||||
|
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
static void
|
static void
|
||||||
TunDispatchUnregisterBuffers(_Inout_ TUN_CTX *Ctx, _In_ FILE_OBJECT *Owner)
|
TunUnregisterBuffers(_Inout_ TUN_CTX *Ctx, _In_ FILE_OBJECT *Owner)
|
||||||
{
|
{
|
||||||
if (InterlockedCompareExchangePointer(&Ctx->Device.Owner, NULL, Owner) != Owner)
|
if (InterlockedCompareExchangePointer(&Ctx->Device.Owner, NULL, Owner) != Owner)
|
||||||
return;
|
return;
|
||||||
@ -699,83 +677,72 @@ TunDispatchUnregisterBuffers(_Inout_ TUN_CTX *Ctx, _In_ FILE_OBJECT *Owner)
|
|||||||
ObDereferenceObject(Ctx->Device.Send.TailMoved);
|
ObDereferenceObject(Ctx->Device.Send.TailMoved);
|
||||||
}
|
}
|
||||||
|
|
||||||
static DRIVER_DISPATCH_PAGED TunDispatch;
|
_Dispatch_type_(IRP_MJ_DEVICE_CONTROL)
|
||||||
|
static DRIVER_DISPATCH TunDispatchDeviceControl;
|
||||||
_Use_decl_annotations_
|
_Use_decl_annotations_
|
||||||
static NTSTATUS
|
static NTSTATUS
|
||||||
TunDispatch(DEVICE_OBJECT *DeviceObject, IRP *Irp)
|
TunDispatchDeviceControl(DEVICE_OBJECT *DeviceObject, IRP *Irp)
|
||||||
{
|
{
|
||||||
NTSTATUS Status;
|
|
||||||
|
|
||||||
Irp->IoStatus.Information = 0;
|
|
||||||
TUN_CTX *Ctx = NdisGetDeviceReservedExtension(DeviceObject);
|
|
||||||
if (Status = STATUS_INVALID_HANDLE, !Ctx)
|
|
||||||
goto cleanupCompleteRequest;
|
|
||||||
|
|
||||||
IO_STACK_LOCATION *Stack = IoGetCurrentIrpStackLocation(Irp);
|
IO_STACK_LOCATION *Stack = IoGetCurrentIrpStackLocation(Irp);
|
||||||
switch (Stack->MajorFunction)
|
if (Stack->Parameters.DeviceIoControl.IoControlCode != TUN_IOCTL_REGISTER_RINGS)
|
||||||
{
|
return NdisDispatchDeviceControl(DeviceObject, Irp);
|
||||||
case IRP_MJ_CREATE:
|
ExAcquireResourceSharedLite(&TunCtxDispatchGuard, TRUE);
|
||||||
if (!NT_SUCCESS(Status = IoAcquireRemoveLock(&Ctx->Device.RemoveLock, Irp)))
|
#pragma warning(suppress : 28175)
|
||||||
goto cleanupCompleteRequest;
|
TUN_CTX *Ctx = DeviceObject->Reserved;
|
||||||
Status = TunDispatchCreate(Ctx, Irp);
|
NTSTATUS Status = NDIS_STATUS_ADAPTER_NOT_READY;
|
||||||
IoReleaseRemoveLock(&Ctx->Device.RemoveLock, Irp);
|
if (Ctx)
|
||||||
break;
|
Status = TunRegisterBuffers(Ctx, Irp);
|
||||||
|
ExReleaseResourceLite(&TunCtxDispatchGuard);
|
||||||
case IRP_MJ_DEVICE_CONTROL:
|
|
||||||
if ((Status = STATUS_INVALID_PARAMETER,
|
|
||||||
Stack->Parameters.DeviceIoControl.IoControlCode != TUN_IOCTL_REGISTER_RINGS) ||
|
|
||||||
!NT_SUCCESS(Status = IoAcquireRemoveLock(&Ctx->Device.RemoveLock, Irp)))
|
|
||||||
goto cleanupCompleteRequest;
|
|
||||||
Status = TunDispatchRegisterBuffers(Ctx, Irp);
|
|
||||||
IoReleaseRemoveLock(&Ctx->Device.RemoveLock, Irp);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case IRP_MJ_CLOSE:
|
|
||||||
Status = STATUS_SUCCESS;
|
|
||||||
TunDispatchUnregisterBuffers(Ctx, Stack->FileObject);
|
|
||||||
IoReleaseRemoveLock(&Ctx->Device.RemoveLock, Stack->FileObject);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Status = STATUS_INVALID_PARAMETER;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupCompleteRequest:
|
|
||||||
Irp->IoStatus.Status = Status;
|
Irp->IoStatus.Status = Status;
|
||||||
|
Irp->IoStatus.Information = 0;
|
||||||
IoCompleteRequest(Irp, IO_NO_INCREMENT);
|
IoCompleteRequest(Irp, IO_NO_INCREMENT);
|
||||||
return Status;
|
return Status;
|
||||||
}
|
}
|
||||||
|
|
||||||
_Dispatch_type_(IRP_MJ_PNP) static DRIVER_DISPATCH TunDispatchPnP;
|
_Dispatch_type_(IRP_MJ_CLOSE)
|
||||||
|
static DRIVER_DISPATCH TunDispatchClose;
|
||||||
|
_Use_decl_annotations_
|
||||||
|
static NTSTATUS
|
||||||
|
TunDispatchClose(DEVICE_OBJECT *DeviceObject, IRP *Irp)
|
||||||
|
{
|
||||||
|
ExAcquireResourceSharedLite(&TunCtxDispatchGuard, TRUE);
|
||||||
|
#pragma warning(suppress : 28175)
|
||||||
|
TUN_CTX *Ctx = DeviceObject->Reserved;
|
||||||
|
if (Ctx)
|
||||||
|
TunUnregisterBuffers(Ctx, IoGetCurrentIrpStackLocation(Irp)->FileObject);
|
||||||
|
ExReleaseResourceLite(&TunCtxDispatchGuard);
|
||||||
|
return NdisDispatchClose(DeviceObject, Irp);
|
||||||
|
}
|
||||||
|
|
||||||
|
_Dispatch_type_(IRP_MJ_PNP)
|
||||||
|
static DRIVER_DISPATCH TunDispatchPnP;
|
||||||
_Use_decl_annotations_
|
_Use_decl_annotations_
|
||||||
static NTSTATUS
|
static NTSTATUS
|
||||||
TunDispatchPnP(DEVICE_OBJECT *DeviceObject, IRP *Irp)
|
TunDispatchPnP(DEVICE_OBJECT *DeviceObject, IRP *Irp)
|
||||||
{
|
{
|
||||||
IO_STACK_LOCATION *Stack = IoGetCurrentIrpStackLocation(Irp);
|
ExAcquireResourceSharedLite(&TunCtxDispatchGuard, TRUE);
|
||||||
if (Stack->MajorFunction == IRP_MJ_PNP)
|
|
||||||
{
|
|
||||||
#pragma warning(suppress : 28175)
|
#pragma warning(suppress : 28175)
|
||||||
TUN_CTX *Ctx = DeviceObject->Reserved;
|
TUN_CTX *Ctx = DeviceObject->Reserved;
|
||||||
if (!Ctx)
|
if (!Ctx)
|
||||||
return NdisDispatchPnP(DeviceObject, Irp);
|
goto cleanup;
|
||||||
|
|
||||||
switch (Stack->MinorFunction)
|
switch (IoGetCurrentIrpStackLocation(Irp)->MinorFunction)
|
||||||
{
|
{
|
||||||
case IRP_MN_QUERY_REMOVE_DEVICE:
|
case IRP_MN_QUERY_REMOVE_DEVICE:
|
||||||
case IRP_MN_SURPRISE_REMOVAL:
|
case IRP_MN_SURPRISE_REMOVAL:
|
||||||
InterlockedAnd(&Ctx->Flags, ~TUN_FLAGS_PRESENT);
|
InterlockedAnd(&Ctx->Flags, ~TUN_FLAGS_PRESENT);
|
||||||
ExReleaseSpinLockExclusive(
|
ExReleaseSpinLockExclusive(
|
||||||
&Ctx->TransitionLock,
|
&Ctx->TransitionLock,
|
||||||
ExAcquireSpinLockExclusive(&Ctx->TransitionLock)); /* Ensure above change is visible to all readers. */
|
ExAcquireSpinLockExclusive(&Ctx->TransitionLock)); /* Ensure above change is visible to all readers. */
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IRP_MN_CANCEL_REMOVE_DEVICE:
|
case IRP_MN_CANCEL_REMOVE_DEVICE:
|
||||||
InterlockedOr(&Ctx->Flags, TUN_FLAGS_PRESENT);
|
InterlockedOr(&Ctx->Flags, TUN_FLAGS_PRESENT);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
ExReleaseResourceLite(&TunCtxDispatchGuard);
|
||||||
return NdisDispatchPnP(DeviceObject, Irp);
|
return NdisDispatchPnP(DeviceObject, Irp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -825,63 +792,14 @@ TunInitializeEx(
|
|||||||
|
|
||||||
if (!MiniportAdapterHandle)
|
if (!MiniportAdapterHandle)
|
||||||
return NDIS_STATUS_FAILURE;
|
return NDIS_STATUS_FAILURE;
|
||||||
|
TUN_CTX *Ctx = ExAllocatePoolWithTag(NonPagedPoolNx, sizeof(*Ctx), TUN_MEMORY_TAG);
|
||||||
/* Register data device first. Having only one device per adapter allows us to store adapter context inside device
|
if (!Ctx)
|
||||||
* extension. */
|
|
||||||
WCHAR DeviceName[sizeof(L"\\Device\\" TUN_DEVICE_NAME L"4294967295") / sizeof(WCHAR) + 1] = { 0 };
|
|
||||||
UNICODE_STRING UnicodeDeviceName;
|
|
||||||
TunInitUnicodeString(&UnicodeDeviceName, DeviceName);
|
|
||||||
RtlUnicodeStringPrintf(
|
|
||||||
&UnicodeDeviceName, L"\\Device\\" TUN_DEVICE_NAME, (ULONG)MiniportInitParameters->NetLuid.Info.NetLuidIndex);
|
|
||||||
|
|
||||||
WCHAR SymbolicName[sizeof(L"\\DosDevices\\" TUN_DEVICE_NAME L"4294967295") / sizeof(WCHAR) + 1] = { 0 };
|
|
||||||
UNICODE_STRING UnicodeSymbolicName;
|
|
||||||
TunInitUnicodeString(&UnicodeSymbolicName, SymbolicName);
|
|
||||||
RtlUnicodeStringPrintf(
|
|
||||||
&UnicodeSymbolicName,
|
|
||||||
L"\\DosDevices\\" TUN_DEVICE_NAME,
|
|
||||||
(ULONG)MiniportInitParameters->NetLuid.Info.NetLuidIndex);
|
|
||||||
|
|
||||||
static PDRIVER_DISPATCH DispatchTable[IRP_MJ_MAXIMUM_FUNCTION + 1] = {
|
|
||||||
TunDispatch, /* IRP_MJ_CREATE */
|
|
||||||
NULL, /* IRP_MJ_CREATE_NAMED_PIPE */
|
|
||||||
TunDispatch, /* IRP_MJ_CLOSE */
|
|
||||||
NULL, /* IRP_MJ_READ */
|
|
||||||
NULL, /* IRP_MJ_WRITE */
|
|
||||||
NULL, /* IRP_MJ_QUERY_INFORMATION */
|
|
||||||
NULL, /* IRP_MJ_SET_INFORMATION */
|
|
||||||
NULL, /* IRP_MJ_QUERY_EA */
|
|
||||||
NULL, /* IRP_MJ_SET_EA */
|
|
||||||
NULL, /* IRP_MJ_FLUSH_BUFFERS */
|
|
||||||
NULL, /* IRP_MJ_QUERY_VOLUME_INFORMATION */
|
|
||||||
NULL, /* IRP_MJ_SET_VOLUME_INFORMATION */
|
|
||||||
NULL, /* IRP_MJ_DIRECTORY_CONTROL */
|
|
||||||
NULL, /* IRP_MJ_FILE_SYSTEM_CONTROL */
|
|
||||||
TunDispatch, /* IRP_MJ_DEVICE_CONTROL */
|
|
||||||
};
|
|
||||||
NDIS_DEVICE_OBJECT_ATTRIBUTES DeviceObjectAttributes = {
|
|
||||||
.Header = { .Type = NDIS_OBJECT_TYPE_DEVICE_OBJECT_ATTRIBUTES,
|
|
||||||
.Revision = NDIS_DEVICE_OBJECT_ATTRIBUTES_REVISION_1,
|
|
||||||
.Size = NDIS_SIZEOF_DEVICE_OBJECT_ATTRIBUTES_REVISION_1 },
|
|
||||||
.DeviceName = &UnicodeDeviceName,
|
|
||||||
.SymbolicName = &UnicodeSymbolicName,
|
|
||||||
.MajorFunctions = DispatchTable,
|
|
||||||
.ExtensionSize = sizeof(TUN_CTX),
|
|
||||||
.DefaultSDDLString = &SDDL_DEVOBJ_SYS_ALL /* Kernel, and SYSTEM: full control. Others: none */
|
|
||||||
};
|
|
||||||
NDIS_HANDLE DeviceObjectHandle;
|
|
||||||
DEVICE_OBJECT *DeviceObject;
|
|
||||||
if (!NT_SUCCESS(
|
|
||||||
Status = NdisRegisterDeviceEx(
|
|
||||||
NdisMiniportDriverHandle, &DeviceObjectAttributes, &DeviceObject, &DeviceObjectHandle)))
|
|
||||||
return NDIS_STATUS_FAILURE;
|
return NDIS_STATUS_FAILURE;
|
||||||
|
NdisZeroMemory(Ctx, sizeof(*Ctx));
|
||||||
|
|
||||||
TUN_CTX *Ctx = NdisGetDeviceReservedExtension(DeviceObject);
|
Ctx->MiniportAdapterHandle = MiniportAdapterHandle;
|
||||||
if (Status = NDIS_STATUS_FAILURE, !Ctx)
|
|
||||||
goto cleanupDeregisterDevice;
|
|
||||||
DEVICE_OBJECT *FunctionalDeviceObject;
|
|
||||||
NdisMGetDeviceProperty(MiniportAdapterHandle, NULL, &FunctionalDeviceObject, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
|
NdisMGetDeviceProperty(MiniportAdapterHandle, NULL, &Ctx->FunctionalDeviceObject, NULL, NULL, NULL);
|
||||||
/* Reverse engineering indicates that we'd be better off calling
|
/* Reverse engineering indicates that we'd be better off calling
|
||||||
* NdisWdfGetAdapterContextFromAdapterHandle(functional_device),
|
* NdisWdfGetAdapterContextFromAdapterHandle(functional_device),
|
||||||
* which points to our TUN_CTX object directly, but this isn't
|
* which points to our TUN_CTX object directly, but this isn't
|
||||||
@ -889,12 +807,9 @@ TunInitializeEx(
|
|||||||
* this reserved field. Revisit this when we drop support for old
|
* this reserved field. Revisit this when we drop support for old
|
||||||
* Windows versions. */
|
* Windows versions. */
|
||||||
#pragma warning(suppress : 28175)
|
#pragma warning(suppress : 28175)
|
||||||
ASSERT(!FunctionalDeviceObject->Reserved);
|
ASSERT(!Ctx->FunctionalDeviceObject->Reserved);
|
||||||
#pragma warning(suppress : 28175)
|
#pragma warning(suppress : 28175)
|
||||||
FunctionalDeviceObject->Reserved = Ctx;
|
Ctx->FunctionalDeviceObject->Reserved = Ctx;
|
||||||
|
|
||||||
NdisZeroMemory(Ctx, sizeof(*Ctx));
|
|
||||||
Ctx->MiniportAdapterHandle = MiniportAdapterHandle;
|
|
||||||
|
|
||||||
Ctx->Statistics.Header.Type = NDIS_OBJECT_TYPE_DEFAULT;
|
Ctx->Statistics.Header.Type = NDIS_OBJECT_TYPE_DEFAULT;
|
||||||
Ctx->Statistics.Header.Revision = NDIS_STATISTICS_INFO_REVISION_1;
|
Ctx->Statistics.Header.Revision = NDIS_STATISTICS_INFO_REVISION_1;
|
||||||
@ -909,10 +824,6 @@ TunInitializeEx(
|
|||||||
NDIS_STATISTICS_FLAGS_VALID_DIRECTED_BYTES_RCV | NDIS_STATISTICS_FLAGS_VALID_MULTICAST_BYTES_RCV |
|
NDIS_STATISTICS_FLAGS_VALID_DIRECTED_BYTES_RCV | NDIS_STATISTICS_FLAGS_VALID_MULTICAST_BYTES_RCV |
|
||||||
NDIS_STATISTICS_FLAGS_VALID_BROADCAST_BYTES_RCV | NDIS_STATISTICS_FLAGS_VALID_DIRECTED_BYTES_XMIT |
|
NDIS_STATISTICS_FLAGS_VALID_BROADCAST_BYTES_RCV | NDIS_STATISTICS_FLAGS_VALID_DIRECTED_BYTES_XMIT |
|
||||||
NDIS_STATISTICS_FLAGS_VALID_MULTICAST_BYTES_XMIT | NDIS_STATISTICS_FLAGS_VALID_BROADCAST_BYTES_XMIT;
|
NDIS_STATISTICS_FLAGS_VALID_MULTICAST_BYTES_XMIT | NDIS_STATISTICS_FLAGS_VALID_BROADCAST_BYTES_XMIT;
|
||||||
|
|
||||||
Ctx->Device.Handle = DeviceObjectHandle;
|
|
||||||
Ctx->Device.Object = DeviceObject;
|
|
||||||
IoInitializeRemoveLock(&Ctx->Device.RemoveLock, TUN_MEMORY_TAG, 0, 0);
|
|
||||||
KeInitializeEvent(&Ctx->Device.Disconnected, NotificationEvent, TRUE);
|
KeInitializeEvent(&Ctx->Device.Disconnected, NotificationEvent, TRUE);
|
||||||
KeInitializeSpinLock(&Ctx->Device.Send.Lock);
|
KeInitializeSpinLock(&Ctx->Device.Send.Lock);
|
||||||
|
|
||||||
@ -928,7 +839,7 @@ TunInitializeEx(
|
|||||||
#pragma warning(suppress : 6014)
|
#pragma warning(suppress : 6014)
|
||||||
Ctx->NblPool = NdisAllocateNetBufferListPool(MiniportAdapterHandle, &NblPoolParameters);
|
Ctx->NblPool = NdisAllocateNetBufferListPool(MiniportAdapterHandle, &NblPoolParameters);
|
||||||
if (Status = NDIS_STATUS_FAILURE, !Ctx->NblPool)
|
if (Status = NDIS_STATUS_FAILURE, !Ctx->NblPool)
|
||||||
goto cleanupDeregisterDevice;
|
goto cleanupFreeCtx;
|
||||||
|
|
||||||
NDIS_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES AdapterRegistrationAttributes = {
|
NDIS_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES AdapterRegistrationAttributes = {
|
||||||
.Header = { .Type = NDIS_OBJECT_TYPE_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES,
|
.Header = { .Type = NDIS_OBJECT_TYPE_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES,
|
||||||
@ -1015,118 +926,16 @@ TunInitializeEx(
|
|||||||
* registration attributes even if the driver is still in the context
|
* registration attributes even if the driver is still in the context
|
||||||
* of the MiniportInitializeEx function. */
|
* of the MiniportInitializeEx function. */
|
||||||
TunIndicateStatus(Ctx->MiniportAdapterHandle, MediaConnectStateDisconnected);
|
TunIndicateStatus(Ctx->MiniportAdapterHandle, MediaConnectStateDisconnected);
|
||||||
InterlockedIncrement64(&TunAdapterCount);
|
|
||||||
InterlockedOr(&Ctx->Flags, TUN_FLAGS_PRESENT);
|
InterlockedOr(&Ctx->Flags, TUN_FLAGS_PRESENT);
|
||||||
return NDIS_STATUS_SUCCESS;
|
return NDIS_STATUS_SUCCESS;
|
||||||
|
|
||||||
cleanupFreeNblPool:
|
cleanupFreeNblPool:
|
||||||
NdisFreeNetBufferListPool(Ctx->NblPool);
|
NdisFreeNetBufferListPool(Ctx->NblPool);
|
||||||
cleanupDeregisterDevice:
|
cleanupFreeCtx:
|
||||||
NdisDeregisterDeviceEx(DeviceObjectHandle);
|
ExFreePoolWithTag(Ctx, TUN_MEMORY_TAG);
|
||||||
return Status;
|
return Status;
|
||||||
}
|
}
|
||||||
|
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
|
||||||
static NTSTATUS
|
|
||||||
TunDeviceSetDenyAllDacl(_In_ DEVICE_OBJECT *DeviceObject)
|
|
||||||
{
|
|
||||||
NTSTATUS Status;
|
|
||||||
SECURITY_DESCRIPTOR Sd;
|
|
||||||
ACL Acl;
|
|
||||||
HANDLE DeviceObjectHandle;
|
|
||||||
|
|
||||||
if (!NT_SUCCESS(Status = RtlCreateSecurityDescriptor(&Sd, SECURITY_DESCRIPTOR_REVISION)) ||
|
|
||||||
!NT_SUCCESS(Status = RtlCreateAcl(&Acl, sizeof(ACL), ACL_REVISION)) ||
|
|
||||||
!NT_SUCCESS(Status = RtlSetDaclSecurityDescriptor(&Sd, TRUE, &Acl, FALSE)) ||
|
|
||||||
!NT_SUCCESS(
|
|
||||||
Status = ObOpenObjectByPointer(
|
|
||||||
DeviceObject,
|
|
||||||
OBJ_KERNEL_HANDLE,
|
|
||||||
NULL,
|
|
||||||
WRITE_DAC,
|
|
||||||
*IoDeviceObjectType,
|
|
||||||
KernelMode,
|
|
||||||
&DeviceObjectHandle)))
|
|
||||||
return Status;
|
|
||||||
|
|
||||||
Status = ZwSetSecurityObject(DeviceObjectHandle, DACL_SECURITY_INFORMATION, &Sd);
|
|
||||||
ZwClose(DeviceObjectHandle);
|
|
||||||
return Status;
|
|
||||||
}
|
|
||||||
|
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
|
||||||
static void
|
|
||||||
TunForceHandlesClosed(_Inout_ TUN_CTX *Ctx)
|
|
||||||
{
|
|
||||||
NTSTATUS Status;
|
|
||||||
PEPROCESS Process;
|
|
||||||
KAPC_STATE ApcState;
|
|
||||||
PVOID Object = NULL;
|
|
||||||
ULONG VerifierFlags = 0;
|
|
||||||
OBJECT_HANDLE_INFORMATION HandleInfo;
|
|
||||||
SYSTEM_HANDLE_INFORMATION_EX *HandleTable = NULL;
|
|
||||||
|
|
||||||
MmIsVerifierEnabled(&VerifierFlags);
|
|
||||||
|
|
||||||
for (ULONG Size = 0, RequestedSize;
|
|
||||||
(Status = ZwQuerySystemInformation(SystemExtendedHandleInformation, HandleTable, Size, &RequestedSize)) ==
|
|
||||||
STATUS_INFO_LENGTH_MISMATCH;
|
|
||||||
Size = RequestedSize)
|
|
||||||
{
|
|
||||||
if (HandleTable)
|
|
||||||
ExFreePoolWithTag(HandleTable, TUN_MEMORY_TAG);
|
|
||||||
HandleTable = ExAllocatePoolWithTag(PagedPool, RequestedSize, TUN_MEMORY_TAG);
|
|
||||||
if (!HandleTable)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!NT_SUCCESS(Status) || !HandleTable)
|
|
||||||
goto cleanup;
|
|
||||||
|
|
||||||
for (ULONG_PTR Index = 0; Index < HandleTable->NumberOfHandles; ++Index)
|
|
||||||
{
|
|
||||||
/* XXX: We should perhaps first look at table->Handles[i].ObjectTypeIndex, but
|
|
||||||
* the value changes lots between NT versions, and it should be implicit anyway. */
|
|
||||||
FILE_OBJECT *FileObject = HandleTable->Handles[Index].Object;
|
|
||||||
if (!FileObject || FileObject->Type != 5 || FileObject->DeviceObject != Ctx->Device.Object)
|
|
||||||
continue;
|
|
||||||
Status = PsLookupProcessByProcessId(HandleTable->Handles[Index].UniqueProcessId, &Process);
|
|
||||||
if (!NT_SUCCESS(Status))
|
|
||||||
continue;
|
|
||||||
KeStackAttachProcess(Process, &ApcState);
|
|
||||||
if (!VerifierFlags)
|
|
||||||
Status = ObReferenceObjectByHandle(
|
|
||||||
HandleTable->Handles[Index].HandleValue, 0, NULL, UserMode, &Object, &HandleInfo);
|
|
||||||
if (NT_SUCCESS(Status))
|
|
||||||
{
|
|
||||||
if (VerifierFlags || Object == FileObject)
|
|
||||||
ObCloseHandle(HandleTable->Handles[Index].HandleValue, UserMode);
|
|
||||||
if (!VerifierFlags)
|
|
||||||
ObfDereferenceObject(Object);
|
|
||||||
}
|
|
||||||
KeUnstackDetachProcess(&ApcState);
|
|
||||||
ObfDereferenceObject(Process);
|
|
||||||
}
|
|
||||||
cleanup:
|
|
||||||
if (HandleTable)
|
|
||||||
ExFreePoolWithTag(HandleTable, TUN_MEMORY_TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
_IRQL_requires_max_(APC_LEVEL)
|
|
||||||
static void
|
|
||||||
TunWaitForReferencesToDropToZero(_In_ DEVICE_OBJECT *DeviceObject)
|
|
||||||
{
|
|
||||||
/* The sleep loop isn't pretty, but we don't have a choice. This is an NDIS bug we're working around. */
|
|
||||||
enum
|
|
||||||
{
|
|
||||||
SleepTime = 50,
|
|
||||||
TotalTime = 2 * 60 * 1000,
|
|
||||||
MaxTries = TotalTime / SleepTime
|
|
||||||
};
|
|
||||||
#pragma warning(suppress : 28175)
|
|
||||||
for (INT Try = 0; Try < MaxTries && InterlockedGet(&DeviceObject->ReferenceCount); ++Try)
|
|
||||||
NdisMSleep(SleepTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
static MINIPORT_HALT TunHaltEx;
|
static MINIPORT_HALT TunHaltEx;
|
||||||
_Use_decl_annotations_
|
_Use_decl_annotations_
|
||||||
static void
|
static void
|
||||||
@ -1138,26 +947,14 @@ TunHaltEx(NDIS_HANDLE MiniportAdapterContext, NDIS_HALT_ACTION HaltAction)
|
|||||||
ExReleaseSpinLockExclusive(
|
ExReleaseSpinLockExclusive(
|
||||||
&Ctx->TransitionLock,
|
&Ctx->TransitionLock,
|
||||||
ExAcquireSpinLockExclusive(&Ctx->TransitionLock)); /* Ensure above change is visible to all readers. */
|
ExAcquireSpinLockExclusive(&Ctx->TransitionLock)); /* Ensure above change is visible to all readers. */
|
||||||
|
|
||||||
/* Setting a deny-all DACL we prevent userspace to open the data device by symlink after TunForceHandlesClosed(). */
|
|
||||||
TunDeviceSetDenyAllDacl(Ctx->Device.Object);
|
|
||||||
TunForceHandlesClosed(Ctx);
|
|
||||||
|
|
||||||
/* Wait for processing IRP(s) to complete. */
|
|
||||||
IoAcquireRemoveLock(&Ctx->Device.RemoveLock, NULL);
|
|
||||||
IoReleaseRemoveLockAndWait(&Ctx->Device.RemoveLock, NULL);
|
|
||||||
NdisFreeNetBufferListPool(Ctx->NblPool);
|
NdisFreeNetBufferListPool(Ctx->NblPool);
|
||||||
|
|
||||||
/* MiniportAdapterHandle must not be used in TunDispatch(). After TunHaltEx() returns it is invalidated. */
|
|
||||||
InterlockedExchangePointer(&Ctx->MiniportAdapterHandle, NULL);
|
InterlockedExchangePointer(&Ctx->MiniportAdapterHandle, NULL);
|
||||||
|
#pragma warning(suppress : 28175)
|
||||||
ASSERT(InterlockedGet64(&TunAdapterCount) > 0);
|
InterlockedExchangePointer(&Ctx->FunctionalDeviceObject->Reserved, NULL);
|
||||||
if (InterlockedDecrement64(&TunAdapterCount) <= 0)
|
ExAcquireResourceExclusiveLite(&TunCtxDispatchGuard, TRUE); /* Ensure above change is visible to all readers. */
|
||||||
TunWaitForReferencesToDropToZero(Ctx->Device.Object);
|
ExReleaseResourceLite(&TunCtxDispatchGuard);
|
||||||
|
ExFreePoolWithTag(Ctx, TUN_MEMORY_TAG);
|
||||||
/* Deregister data device _after_ we are done using Ctx not to risk an UaF. The Ctx is hosted by device extension.
|
|
||||||
*/
|
|
||||||
NdisDeregisterDeviceEx(Ctx->Device.Handle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static MINIPORT_SHUTDOWN TunShutdownEx;
|
static MINIPORT_SHUTDOWN TunShutdownEx;
|
||||||
@ -1406,6 +1203,7 @@ static VOID
|
|||||||
TunUnload(PDRIVER_OBJECT DriverObject)
|
TunUnload(PDRIVER_OBJECT DriverObject)
|
||||||
{
|
{
|
||||||
NdisMDeregisterMiniportDriver(NdisMiniportDriverHandle);
|
NdisMDeregisterMiniportDriver(NdisMiniportDriverHandle);
|
||||||
|
ExDeleteResourceLite(&TunCtxDispatchGuard);
|
||||||
}
|
}
|
||||||
|
|
||||||
DRIVER_INITIALIZE DriverEntry;
|
DRIVER_INITIALIZE DriverEntry;
|
||||||
@ -1421,6 +1219,8 @@ DriverEntry(DRIVER_OBJECT *DriverObject, UNICODE_STRING *RegistryPath)
|
|||||||
if (NdisVersion > NDIS_MINIPORT_VERSION_MAX)
|
if (NdisVersion > NDIS_MINIPORT_VERSION_MAX)
|
||||||
NdisVersion = NDIS_MINIPORT_VERSION_MAX;
|
NdisVersion = NDIS_MINIPORT_VERSION_MAX;
|
||||||
|
|
||||||
|
ExInitializeResourceLite(&TunCtxDispatchGuard);
|
||||||
|
|
||||||
NDIS_MINIPORT_DRIVER_CHARACTERISTICS miniport = {
|
NDIS_MINIPORT_DRIVER_CHARACTERISTICS miniport = {
|
||||||
.Header = { .Type = NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS,
|
.Header = { .Type = NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS,
|
||||||
.Revision = NdisVersion < NDIS_RUNTIME_VERSION_680
|
.Revision = NdisVersion < NDIS_RUNTIME_VERSION_680
|
||||||
@ -1457,7 +1257,11 @@ DriverEntry(DRIVER_OBJECT *DriverObject, UNICODE_STRING *RegistryPath)
|
|||||||
return Status;
|
return Status;
|
||||||
|
|
||||||
NdisDispatchPnP = DriverObject->MajorFunction[IRP_MJ_PNP];
|
NdisDispatchPnP = DriverObject->MajorFunction[IRP_MJ_PNP];
|
||||||
|
NdisDispatchDeviceControl = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL];
|
||||||
|
NdisDispatchClose = DriverObject->MajorFunction[IRP_MJ_CLOSE];
|
||||||
DriverObject->MajorFunction[IRP_MJ_PNP] = TunDispatchPnP;
|
DriverObject->MajorFunction[IRP_MJ_PNP] = TunDispatchPnP;
|
||||||
|
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = TunDispatchDeviceControl;
|
||||||
|
DriverObject->MajorFunction[IRP_MJ_CLOSE] = TunDispatchClose;
|
||||||
|
|
||||||
return STATUS_SUCCESS;
|
return STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -176,9 +176,6 @@
|
|||||||
<Inf Include="wintun.inf" />
|
<Inf Include="wintun.inf" />
|
||||||
<FilesToPackage Include="$(TargetPath)" Condition="'$(ConfigurationType)'=='Driver' or '$(ConfigurationType)'=='DynamicLibrary'" />
|
<FilesToPackage Include="$(TargetPath)" Condition="'$(ConfigurationType)'=='Driver' or '$(ConfigurationType)'=='DynamicLibrary'" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<ClInclude Include="undocumented.h" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
<ImportGroup Label="ExtensionTargets" />
|
<ImportGroup Label="ExtensionTargets" />
|
||||||
</Project>
|
</Project>
|
@ -29,9 +29,4 @@
|
|||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</Inf>
|
</Inf>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<ClInclude Include="undocumented.h">
|
|
||||||
<Filter>Header Files</Filter>
|
|
||||||
</ClInclude>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
Loading…
Reference in New Issue
Block a user