api: add driver management
Signed-off-by: Simon Rozman <simon@rozman.si>
This commit is contained in:
parent
f316c13b3e
commit
c324d07ffb
@ -143,14 +143,18 @@
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions Condition="'$(Platform)'=='Win32' Or '$(Platform)'=='x64' Or '$(Configuration)|$(Platform)'=='Debug|ARM64'">HAVE_EV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions Condition="Exists('..\$(WintunPlatform)\$(Configuration)\whql\')">HAVE_WHQL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
<AdditionalIncludeDirectories>..\$(WintunPlatform)\$(Configuration);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions Condition="'$(Platform)'=='Win32' Or '$(Platform)'=='x64' Or '$(Configuration)|$(Platform)'=='Debug|ARM64'">HAVE_EV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions Condition="Exists('..\$(WintunPlatform)\$(Configuration)\whql\')">HAVE_WHQL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ResourceCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Bcrypt.lib;Cfgmgr32.lib;Iphlpapi.lib;Setupapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Bcrypt.lib;Cfgmgr32.lib;Crypt32.lib;Iphlpapi.lib;newdev.lib;ntdll.lib;Setupapi.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<ModuleDefinitionFile>exports.def</ModuleDefinitionFile>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
</Link>
|
||||
@ -193,6 +197,7 @@
|
||||
<ClInclude Include="nci.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="registry.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="api.c" />
|
||||
@ -205,6 +210,7 @@
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="registry.c" />
|
||||
<ClCompile Include="resource.c" />
|
||||
<ClCompile Include="rundll32.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
@ -46,6 +46,9 @@
|
||||
<ClInclude Include="logger.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="driver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@ -75,6 +78,9 @@
|
||||
<ClCompile Include="logger.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="resource.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="driver.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
559
api/driver.c
559
api/driver.c
@ -5,6 +5,14 @@
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#pragma warning(disable : 4221) /* nonstandard: address of automatic in initializer */
|
||||
|
||||
typedef struct _SP_DEVINFO_DATA_LIST
|
||||
{
|
||||
SP_DEVINFO_DATA Data;
|
||||
struct _SP_DEVINFO_DATA_LIST *Next;
|
||||
} SP_DEVINFO_DATA_LIST;
|
||||
|
||||
/**
|
||||
* Retrieves driver information detail for a device information set or a particular device information element in the
|
||||
* device information set.
|
||||
@ -54,6 +62,308 @@ out:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Wintun driver is loaded.
|
||||
*
|
||||
* Note: This function does not log any errors, not to flood the log when called from the EnsureDriverUnloaded() loop.
|
||||
*
|
||||
* @return non-zero when loaded; zero when not loaded or error - use GetLastError().
|
||||
*/
|
||||
static BOOL IsDriverLoaded(VOID)
|
||||
{
|
||||
VOID *StackBuffer[0x80];
|
||||
VOID **Drivers = StackBuffer;
|
||||
DWORD Size = 0;
|
||||
if (!EnumDeviceDrivers(Drivers, sizeof(StackBuffer), &Size))
|
||||
return FALSE;
|
||||
if (Size > sizeof(StackBuffer))
|
||||
{
|
||||
HANDLE Heap = GetProcessHeap();
|
||||
Drivers = HeapAlloc(Heap, 0, Size);
|
||||
if (!Drivers)
|
||||
{
|
||||
SetLastError(ERROR_OUTOFMEMORY);
|
||||
return FALSE;
|
||||
}
|
||||
if (!EnumDeviceDrivers(Drivers, Size, &Size))
|
||||
{
|
||||
DWORD Result = GetLastError();
|
||||
HeapFree(Heap, 0, Drivers);
|
||||
SetLastError(Result);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
BOOL Found = FALSE;
|
||||
for (DWORD i = Size / sizeof(Drivers[0]); i-- > 0;)
|
||||
{
|
||||
WCHAR MaybeWintun[11];
|
||||
if (GetDeviceDriverBaseNameW(Drivers[i], MaybeWintun, _countof(MaybeWintun)) == 10 &&
|
||||
!_wcsicmp(MaybeWintun, L"wintun.sys"))
|
||||
{
|
||||
Found = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Drivers != StackBuffer)
|
||||
HeapFree(GetProcessHeap(), 0, Drivers);
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
return Found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls for 15 sec until the Wintun driver is unloaded.
|
||||
*
|
||||
* @return non-zero if the driver unloaded; zero on error or timeout - use GetLastError().
|
||||
*/
|
||||
static BOOL EnsureDriverUnloaded(VOID)
|
||||
{
|
||||
BOOL Loaded;
|
||||
for (int i = 0; (Loaded = IsDriverLoaded()) != 0 && i < 300; ++i)
|
||||
Sleep(50);
|
||||
return !Loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs code-signing certificate to the computer's Trusted Publishers certificate store.
|
||||
*
|
||||
* @param SignedResource ID of the RT_RCDATA resource containing the signed binary to extract the code-signing
|
||||
* certificate from.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
static WINTUN_STATUS
|
||||
InstallCertificate(_In_z_ const WCHAR *SignedResource)
|
||||
{
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Trusting code signing certificate");
|
||||
HRSRC FoundResource = FindResourceW(ResourceModule, SignedResource, RT_RCDATA);
|
||||
if (!FoundResource)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to find resource");
|
||||
DWORD SizeResource = SizeofResource(ResourceModule, FoundResource);
|
||||
if (!SizeResource)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to size resource");
|
||||
HGLOBAL LoadedResource = LoadResource(ResourceModule, FoundResource);
|
||||
if (!LoadedResource)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to load resource");
|
||||
LPVOID LockedResource = LockResource(LoadedResource);
|
||||
if (!LockedResource)
|
||||
{
|
||||
WINTUN_LOGGER(WINTUN_LOG_ERR, L"Failed to lock resource");
|
||||
return ERROR_LOCK_FAILED;
|
||||
}
|
||||
const CERT_BLOB CertBlob = { .cbData = SizeResource, .pbData = LockedResource };
|
||||
HCERTSTORE QueriedStore;
|
||||
if (!CryptQueryObject(
|
||||
CERT_QUERY_OBJECT_BLOB,
|
||||
&CertBlob,
|
||||
CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
|
||||
CERT_QUERY_FORMAT_FLAG_ALL,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
&QueriedStore,
|
||||
0,
|
||||
NULL))
|
||||
return WINTUN_LOGGER_LAST_ERROR("Failed to find certificate");
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
HCERTSTORE TrustedStore =
|
||||
CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_LOCAL_MACHINE, L"TrustedPublisher");
|
||||
if (!TrustedStore)
|
||||
{
|
||||
Result = WINTUN_LOGGER_LAST_ERROR(L"Failed to open store");
|
||||
goto cleanupQueriedStore;
|
||||
}
|
||||
LPSTR CodeSigningOid[] = { szOID_PKIX_KP_CODE_SIGNING };
|
||||
CERT_ENHKEY_USAGE EnhancedUsage = { .cUsageIdentifier = 1, .rgpszUsageIdentifier = CodeSigningOid };
|
||||
for (const CERT_CONTEXT *CertContext = NULL; (CertContext = CertFindCertificateInStore(
|
||||
QueriedStore,
|
||||
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
|
||||
CERT_FIND_EXT_ONLY_ENHKEY_USAGE_FLAG,
|
||||
CERT_FIND_ENHKEY_USAGE,
|
||||
&EnhancedUsage,
|
||||
CertContext)) != NULL;)
|
||||
{
|
||||
CERT_EXTENSION *Ext = CertFindExtension(
|
||||
szOID_BASIC_CONSTRAINTS2, CertContext->pCertInfo->cExtension, CertContext->pCertInfo->rgExtension);
|
||||
CERT_BASIC_CONSTRAINTS2_INFO Constraints;
|
||||
DWORD Size = sizeof(Constraints);
|
||||
if (Ext &&
|
||||
CryptDecodeObjectEx(
|
||||
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
|
||||
szOID_BASIC_CONSTRAINTS2,
|
||||
Ext->Value.pbData,
|
||||
Ext->Value.cbData,
|
||||
0,
|
||||
NULL,
|
||||
&Constraints,
|
||||
&Size) &&
|
||||
!Constraints.fCA)
|
||||
if (!CertAddCertificateContextToStore(TrustedStore, CertContext, CERT_STORE_ADD_REPLACE_EXISTING, NULL))
|
||||
{
|
||||
WINTUN_LOGGER_LAST_ERROR(L"Failed to add certificate to store");
|
||||
Result = Result != ERROR_SUCCESS ? Result : GetLastError();
|
||||
}
|
||||
}
|
||||
CertCloseStore(TrustedStore, 0);
|
||||
cleanupQueriedStore:
|
||||
CertCloseStore(QueriedStore, 0);
|
||||
return Result;
|
||||
}
|
||||
|
||||
/* We can't use RtlGetVersion, because appcompat's aclayers.dll shims it to report Vista
|
||||
* when run from legacy contexts. So, we instead use the undocumented RtlGetNtVersionNumbers.
|
||||
*
|
||||
* Another way would be reading from the PEB directly:
|
||||
* ((DWORD *)NtCurrentTeb()->ProcessEnvironmentBlock)[sizeof(void *) == 8 ? 70 : 41]
|
||||
* Or just read from KUSER_SHARED_DATA the same way on 32-bit and 64-bit:
|
||||
* *(DWORD *)0x7FFE026C
|
||||
*/
|
||||
extern VOID NTAPI
|
||||
RtlGetNtVersionNumbers(_Out_opt_ DWORD *MajorVersion, _Out_opt_ DWORD *MinorVersion, _Out_opt_ DWORD *BuildNumber);
|
||||
|
||||
/**
|
||||
* Installs Wintun driver to the Windows driver store and updates existing adapters to use it.
|
||||
*
|
||||
* @param UpdateExisting Set to non-zero when existing adapters should be upgraded to the newest driver.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
static WINTUN_STATUS
|
||||
InstallDriver(_In_ BOOL UpdateExisting)
|
||||
{
|
||||
WCHAR WindowsDirectory[MAX_PATH];
|
||||
if (!GetWindowsDirectoryW(WindowsDirectory, _countof(WindowsDirectory)))
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to get Windows folder");
|
||||
WCHAR WindowsTempDirectory[MAX_PATH];
|
||||
if (!PathCombineW(WindowsTempDirectory, WindowsDirectory, L"Temp"))
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
UCHAR RandomBytes[32] = { 0 };
|
||||
#pragma warning(suppress : 6387)
|
||||
if (!RtlGenRandom(RandomBytes, sizeof(RandomBytes)))
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to generate random");
|
||||
WCHAR RandomSubDirectory[sizeof(RandomBytes) * 2 + 1];
|
||||
for (int i = 0; i < sizeof(RandomBytes); ++i)
|
||||
swprintf_s(&RandomSubDirectory[i * 2], 3, L"%02x", RandomBytes[i]);
|
||||
WCHAR RandomTempSubDirectory[MAX_PATH];
|
||||
if (!PathCombineW(RandomTempSubDirectory, WindowsTempDirectory, RandomSubDirectory))
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
SECURITY_ATTRIBUTES SecurityAttributes = { .nLength = sizeof(SecurityAttributes) };
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
L"O:SYD:P(A;;GA;;;SY)", SDDL_REVISION_1, &SecurityAttributes.lpSecurityDescriptor, NULL))
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to convert security descriptor");
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
if (!CreateDirectoryW(RandomTempSubDirectory, &SecurityAttributes))
|
||||
{
|
||||
Result = WINTUN_LOGGER_LAST_ERROR(L"Failed to create temporary folder");
|
||||
goto cleanupFree;
|
||||
}
|
||||
|
||||
WCHAR CatPath[MAX_PATH] = { 0 };
|
||||
WCHAR SysPath[MAX_PATH] = { 0 };
|
||||
WCHAR InfPath[MAX_PATH] = { 0 };
|
||||
if (!PathCombineW(CatPath, RandomTempSubDirectory, L"wintun.cat") ||
|
||||
!PathCombineW(SysPath, RandomTempSubDirectory, L"wintun.sys") ||
|
||||
!PathCombineW(InfPath, RandomTempSubDirectory, L"wintun.inf"))
|
||||
{
|
||||
Result = ERROR_BUFFER_OVERFLOW;
|
||||
goto cleanupFree;
|
||||
}
|
||||
|
||||
BOOL UseWHQL = FALSE;
|
||||
#if defined(HAVE_EV) && defined(HAVE_WHQL)
|
||||
DWORD MajorVersion;
|
||||
RtlGetNtVersionNumbers(&MajorVersion, NULL, NULL);
|
||||
UseWHQL = MajorVersion >= 10;
|
||||
#elif defined(HAVE_EV)
|
||||
UseWHQL = FALSE;
|
||||
#elif defined(HAVE_WHQL)
|
||||
UseWHQL = TRUE;
|
||||
#else
|
||||
# error No driver available
|
||||
#endif
|
||||
if (!UseWHQL && (Result = InstallCertificate(L"wintun.sys")) != ERROR_SUCCESS)
|
||||
WINTUN_LOGGER_ERROR(L"Unable to install code signing certificate", Result);
|
||||
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Copying resources to temporary path");
|
||||
if ((Result = CopyResource(CatPath, &SecurityAttributes, UseWHQL ? L"wintun-whql.cat" : L"wintun.cat")) !=
|
||||
ERROR_SUCCESS ||
|
||||
(Result = CopyResource(SysPath, &SecurityAttributes, UseWHQL ? L"wintun-whql.sys" : L"wintun.sys")) !=
|
||||
ERROR_SUCCESS ||
|
||||
(Result = CopyResource(InfPath, &SecurityAttributes, UseWHQL ? L"wintun-whql.inf" : L"wintun.inf")) !=
|
||||
ERROR_SUCCESS)
|
||||
{
|
||||
Result = WINTUN_LOGGER_LAST_ERROR(L"Failed to copy resources");
|
||||
goto cleanupDelete;
|
||||
}
|
||||
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Installing driver");
|
||||
if (!SetupCopyOEMInfW(InfPath, NULL, SPOST_PATH, 0, NULL, 0, NULL, NULL))
|
||||
Result = WINTUN_LOGGER_LAST_ERROR(L"Could not install driver to store");
|
||||
BOOL RebootRequired = FALSE;
|
||||
if (UpdateExisting &&
|
||||
!UpdateDriverForPlugAndPlayDevicesW(
|
||||
NULL, L"Wintun", InfPath, INSTALLFLAG_FORCE | INSTALLFLAG_NONINTERACTIVE, &RebootRequired))
|
||||
WINTUN_LOGGER_LAST_ERROR(L"Could not update existing adapters");
|
||||
if (RebootRequired)
|
||||
WINTUN_LOGGER(WINTUN_LOG_WARN, L"A reboot might be required, which really should not be the case");
|
||||
|
||||
cleanupDelete:
|
||||
DeleteFileW(CatPath);
|
||||
DeleteFileW(SysPath);
|
||||
DeleteFileW(InfPath);
|
||||
RemoveDirectoryW(RandomTempSubDirectory);
|
||||
cleanupFree:
|
||||
LocalFree(SecurityAttributes.lpSecurityDescriptor);
|
||||
return Result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes Wintun driver from the Windows driver store.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
static WINTUN_STATUS RemoveDriver(VOID)
|
||||
{
|
||||
HDEVINFO DevInfo = SetupDiGetClassDevsW(&GUID_DEVCLASS_NET, NULL, NULL, 0);
|
||||
if (!DevInfo)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to request device information");
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
if (!SetupDiBuildDriverInfoList(DevInfo, NULL, SPDIT_CLASSDRIVER))
|
||||
{
|
||||
Result = WINTUN_LOGGER_LAST_ERROR(L"Failed to build list of drivers");
|
||||
goto cleanupDeviceInfoSet;
|
||||
}
|
||||
HANDLE Heap = GetProcessHeap();
|
||||
for (DWORD EnumIndex = 0;; ++EnumIndex)
|
||||
{
|
||||
SP_DRVINFO_DATA_W DrvInfoData = { .cbSize = sizeof(DrvInfoData) };
|
||||
if (!SetupDiEnumDriverInfoW(DevInfo, NULL, SPDIT_CLASSDRIVER, EnumIndex, &DrvInfoData))
|
||||
{
|
||||
if (GetLastError() == ERROR_NO_MORE_ITEMS)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
SP_DRVINFO_DETAIL_DATA_W *DrvInfoDetailData = DriverGetDrvInfoDetail(DevInfo, NULL, &DrvInfoData);
|
||||
if (!DrvInfoDetailData)
|
||||
continue;
|
||||
if (!_wcsicmp(DrvInfoDetailData->HardwareID, L"wintun"))
|
||||
{
|
||||
PathStripPathW(DrvInfoDetailData->InfFileName);
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Removing existing driver");
|
||||
if (!SetupUninstallOEMInfW(DrvInfoDetailData->InfFileName, SUOI_FORCEDELETE, NULL))
|
||||
{
|
||||
WINTUN_LOGGER_LAST_ERROR(L"Unable to remove existing driver");
|
||||
Result = Result != ERROR_SUCCESS ? Result : GetLastError();
|
||||
}
|
||||
}
|
||||
HeapFree(Heap, 0, DrvInfoDetailData);
|
||||
}
|
||||
SetupDiDestroyDriverInfoList(DevInfo, NULL, SPDIT_CLASSDRIVER);
|
||||
cleanupDeviceInfoSet:
|
||||
SetupDiDestroyDeviceInfoList(DevInfo);
|
||||
return Result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the device (i.e. network adapter) is using Wintun driver.
|
||||
*
|
||||
@ -147,3 +457,252 @@ cleanupBuf:
|
||||
SetLastError(Result);
|
||||
return Handle;
|
||||
}
|
||||
|
||||
#define TUN_IOCTL_FORCE_CLOSE_HANDLES CTL_CODE(51820U, 0x971U, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)
|
||||
|
||||
/**
|
||||
* Closes all client handles to the Wintun adapter.
|
||||
*
|
||||
* @param DevInfo A handle to the device information set that contains a device information element that
|
||||
* represents the device.
|
||||
*
|
||||
* @param DevInfoData A pointer to a structure that specifies the device information element in DevInfo.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
static WINTUN_STATUS
|
||||
ForceCloseWintunAdapterHandle(_In_ HDEVINFO DevInfo, _In_ SP_DEVINFO_DATA *DevInfoData)
|
||||
{
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
DWORD RequiredBytes;
|
||||
if (SetupDiGetDeviceInstanceIdW(DevInfo, DevInfoData, NULL, 0, &RequiredBytes) ||
|
||||
(Result = GetLastError()) != ERROR_INSUFFICIENT_BUFFER)
|
||||
return WINTUN_LOGGER_ERROR(L"Failed to query device instance ID size", Result);
|
||||
HANDLE Heap = GetProcessHeap();
|
||||
WCHAR *InstanceId = HeapAlloc(Heap, HEAP_ZERO_MEMORY, sizeof(*InstanceId) * RequiredBytes);
|
||||
if (!InstanceId)
|
||||
return ERROR_OUTOFMEMORY;
|
||||
if (!SetupDiGetDeviceInstanceIdW(DevInfo, DevInfoData, InstanceId, RequiredBytes, &RequiredBytes))
|
||||
{
|
||||
Result = WINTUN_LOGGER_LAST_ERROR(L"Failed to get device instance ID");
|
||||
goto out;
|
||||
}
|
||||
HANDLE NdisHandle = DriverGetAdapterDeviceObject(InstanceId);
|
||||
if (NdisHandle == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
Result = GetLastError();
|
||||
goto out;
|
||||
}
|
||||
Result = DeviceIoControl(NdisHandle, TUN_IOCTL_FORCE_CLOSE_HANDLES, NULL, 0, NULL, 0, &RequiredBytes, NULL)
|
||||
? ERROR_SUCCESS
|
||||
: WINTUN_LOGGER_LAST_ERROR(L"Failed to perform ioctl");
|
||||
CloseHandle(NdisHandle);
|
||||
out:
|
||||
HeapFree(Heap, 0, InstanceId);
|
||||
return Result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables Wintun adapters.
|
||||
*
|
||||
* @param DevInfo A handle to the device information set.
|
||||
*
|
||||
* @param DisabledAdapters Output list of disabled adapters. The adapters disabled are inserted in the list head.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
static WINTUN_STATUS
|
||||
DisableWintunAdapters(_In_ HDEVINFO DevInfo, _Inout_ SP_DEVINFO_DATA_LIST **DisabledAdapters)
|
||||
{
|
||||
SP_PROPCHANGE_PARAMS Params = { .ClassInstallHeader = { .cbSize = sizeof(SP_CLASSINSTALL_HEADER),
|
||||
.InstallFunction = DIF_PROPERTYCHANGE },
|
||||
.StateChange = DICS_DISABLE,
|
||||
.Scope = DICS_FLAG_GLOBAL };
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
HANDLE Heap = GetProcessHeap();
|
||||
for (DWORD EnumIndex = 0;; ++EnumIndex)
|
||||
{
|
||||
SP_DEVINFO_DATA_LIST *DeviceNode = HeapAlloc(Heap, 0, sizeof(SP_DEVINFO_DATA_LIST));
|
||||
if (!DeviceNode)
|
||||
return ERROR_OUTOFMEMORY;
|
||||
DeviceNode->Data.cbSize = sizeof(SP_DEVINFO_DATA);
|
||||
if (!SetupDiEnumDeviceInfo(DevInfo, EnumIndex, &DeviceNode->Data))
|
||||
{
|
||||
if (GetLastError() == ERROR_NO_MORE_ITEMS)
|
||||
{
|
||||
HeapFree(Heap, 0, DeviceNode);
|
||||
break;
|
||||
}
|
||||
goto cleanupDeviceInfoData;
|
||||
}
|
||||
if (!DriverIsWintunAdapter(DevInfo, &DeviceNode->Data))
|
||||
goto cleanupDeviceInfoData;
|
||||
|
||||
ULONG Status, ProblemCode;
|
||||
if (CM_Get_DevNode_Status(&Status, &ProblemCode, DeviceNode->Data.DevInst, 0) != CR_SUCCESS ||
|
||||
((Status & DN_HAS_PROBLEM) && ProblemCode == CM_PROB_DISABLED))
|
||||
goto cleanupDeviceInfoData;
|
||||
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Force closing all open handles for existing adapter");
|
||||
if (ForceCloseWintunAdapterHandle(DevInfo, &DeviceNode->Data) != ERROR_SUCCESS)
|
||||
WINTUN_LOGGER(WINTUN_LOG_WARN, L"Failed to force close adapter handles");
|
||||
Sleep(200);
|
||||
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Disabling existing adapter");
|
||||
if (!SetupDiSetClassInstallParamsW(DevInfo, &DeviceNode->Data, &Params.ClassInstallHeader, sizeof(Params)) ||
|
||||
!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, DevInfo, &DeviceNode->Data))
|
||||
{
|
||||
WINTUN_LOGGER_LAST_ERROR(L"Unable to disable existing adapter");
|
||||
Result = Result != ERROR_SUCCESS ? Result : GetLastError();
|
||||
goto cleanupDeviceInfoData;
|
||||
}
|
||||
|
||||
DeviceNode->Next = *DisabledAdapters;
|
||||
*DisabledAdapters = DeviceNode;
|
||||
continue;
|
||||
|
||||
cleanupDeviceInfoData:
|
||||
HeapFree(Heap, 0, &DeviceNode->Data);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all Wintun adapters.
|
||||
*
|
||||
* @param DevInfo A handle to the device information set.
|
||||
*
|
||||
* @param DisabledAdapters Output list of disabled adapters.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
static WINTUN_STATUS
|
||||
RemoveWintunAdapters(_In_ HDEVINFO DevInfo)
|
||||
{
|
||||
SP_REMOVEDEVICE_PARAMS Params = { .ClassInstallHeader = { .cbSize = sizeof(SP_CLASSINSTALL_HEADER),
|
||||
.InstallFunction = DIF_REMOVE },
|
||||
.Scope = DI_REMOVEDEVICE_GLOBAL };
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
for (DWORD EnumIndex = 0;; ++EnumIndex)
|
||||
{
|
||||
SP_DEVINFO_DATA DevInfoData = { .cbSize = sizeof(SP_DEVINFO_DATA) };
|
||||
if (!SetupDiEnumDeviceInfo(DevInfo, EnumIndex, &DevInfoData))
|
||||
{
|
||||
if (GetLastError() == ERROR_NO_MORE_ITEMS)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
if (!DriverIsWintunAdapter(DevInfo, &DevInfoData))
|
||||
continue;
|
||||
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Force closing all open handles for existing adapter");
|
||||
if (ForceCloseWintunAdapterHandle(DevInfo, &DevInfoData) != ERROR_SUCCESS)
|
||||
WINTUN_LOGGER(WINTUN_LOG_WARN, L"Failed to force close adapter handles");
|
||||
Sleep(200);
|
||||
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Removing existing adapter");
|
||||
if (!SetupDiSetClassInstallParamsW(DevInfo, &DevInfoData, &Params.ClassInstallHeader, sizeof(Params)) ||
|
||||
!SetupDiCallClassInstaller(DIF_REMOVE, DevInfo, &DevInfoData))
|
||||
{
|
||||
WINTUN_LOGGER_LAST_ERROR(L"Unable to remove existing adapter");
|
||||
Result = Result != ERROR_SUCCESS ? Result : GetLastError();
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables Wintun adapters.
|
||||
*
|
||||
* @param DevInfo A handle to the device information set.
|
||||
*
|
||||
* @param AdaptersToEnable Input list of adapters to enable.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
static WINTUN_STATUS
|
||||
EnableWintunAdapters(_In_ HDEVINFO DevInfo, _In_ SP_DEVINFO_DATA_LIST *AdaptersToEnable)
|
||||
{
|
||||
SP_PROPCHANGE_PARAMS Params = { .ClassInstallHeader = { .cbSize = sizeof(SP_CLASSINSTALL_HEADER),
|
||||
.InstallFunction = DIF_PROPERTYCHANGE },
|
||||
.StateChange = DICS_ENABLE,
|
||||
.Scope = DICS_FLAG_GLOBAL };
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
for (SP_DEVINFO_DATA_LIST *DeviceNode = AdaptersToEnable; DeviceNode; DeviceNode = DeviceNode->Next)
|
||||
{
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Enabling existing adapter");
|
||||
if (!SetupDiSetClassInstallParamsW(DevInfo, &DeviceNode->Data, &Params.ClassInstallHeader, sizeof(Params)) ||
|
||||
!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, DevInfo, &DeviceNode->Data))
|
||||
{
|
||||
WINTUN_LOGGER_LAST_ERROR(L"Unable to enable existing adapter");
|
||||
Result = Result != ERROR_SUCCESS ? Result : GetLastError();
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs or updates Wintun driver.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
WINTUN_STATUS DriverInstallOrUpdate(VOID)
|
||||
{
|
||||
HANDLE Heap = GetProcessHeap();
|
||||
HDEVINFO DevInfo = SetupDiGetClassDevsExW(&GUID_DEVCLASS_NET, NULL, NULL, DIGCF_PRESENT, NULL, NULL, NULL);
|
||||
if (DevInfo == INVALID_HANDLE_VALUE)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to get present class devices");
|
||||
SP_DEVINFO_DATA_LIST *ExistingAdapters = NULL;
|
||||
if (IsDriverLoaded())
|
||||
{
|
||||
DisableWintunAdapters(DevInfo, &ExistingAdapters);
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Waiting for driver to unload from kernel");
|
||||
if (!EnsureDriverUnloaded())
|
||||
WINTUN_LOGGER(WINTUN_LOG_WARN, L"Unable to unload driver, which means a reboot will likely be required");
|
||||
}
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
if ((Result = RemoveDriver()) != ERROR_SUCCESS)
|
||||
{
|
||||
WINTUN_LOGGER_ERROR(L"Failed to uninstall old drivers", Result);
|
||||
goto cleanupAdapters;
|
||||
}
|
||||
if ((Result = InstallDriver(!!ExistingAdapters)) != ERROR_SUCCESS)
|
||||
{
|
||||
WINTUN_LOGGER_ERROR(L"Failed to install driver", Result);
|
||||
goto cleanupAdapters;
|
||||
}
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Installation successful");
|
||||
|
||||
cleanupAdapters:;
|
||||
if (ExistingAdapters)
|
||||
{
|
||||
EnableWintunAdapters(DevInfo, ExistingAdapters);
|
||||
while (ExistingAdapters)
|
||||
{
|
||||
SP_DEVINFO_DATA_LIST *Next = ExistingAdapters->Next;
|
||||
HeapFree(Heap, 0, ExistingAdapters);
|
||||
ExistingAdapters = Next;
|
||||
}
|
||||
}
|
||||
SetupDiDestroyDeviceInfoList(DevInfo);
|
||||
return Result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls Wintun driver.
|
||||
*
|
||||
* @return ERROR_SUCCESS on success; Win32 error code otherwise.
|
||||
*/
|
||||
WINTUN_STATUS DriverUninstall(VOID)
|
||||
{
|
||||
HDEVINFO DevInfo = SetupDiGetClassDevsExW(&GUID_DEVCLASS_NET, NULL, NULL, DIGCF_PRESENT, NULL, NULL, NULL);
|
||||
if (DevInfo == INVALID_HANDLE_VALUE)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to get present class devices");
|
||||
RemoveWintunAdapters(DevInfo);
|
||||
DWORD Result = RemoveDriver();
|
||||
if (Result != ERROR_SUCCESS)
|
||||
WINTUN_LOGGER_ERROR(L"Failed to uninstall driver", Result);
|
||||
else
|
||||
WINTUN_LOGGER(WINTUN_LOG_INFO, L"Uninstallation successful");
|
||||
return Result;
|
||||
}
|
||||
|
@ -19,3 +19,7 @@ DriverIsWintunAdapter(_In_ HDEVINFO DevInfo, _In_opt_ SP_DEVINFO_DATA *DevInfoDa
|
||||
|
||||
_Return_type_success_(return != INVALID_HANDLE_VALUE) HANDLE
|
||||
DriverGetAdapterDeviceObject(_In_opt_z_ const WCHAR *InstanceId);
|
||||
|
||||
WINTUN_STATUS DriverInstallOrUpdate(VOID);
|
||||
|
||||
WINTUN_STATUS DriverUninstall(VOID);
|
||||
|
@ -12,14 +12,20 @@
|
||||
#include "namespace.h"
|
||||
#include "nci.h"
|
||||
#include "registry.h"
|
||||
#include "resource.h"
|
||||
|
||||
#include <bcrypt.h>
|
||||
#include <cfgmgr32.h>
|
||||
#include <devguid.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <locale.h>
|
||||
#include <ndisguid.h>
|
||||
#include <newdev.h>
|
||||
#include <NTSecAPI.h>
|
||||
#include <objbase.h>
|
||||
#include <Psapi.h>
|
||||
#include <sddl.h>
|
||||
#include <SetupAPI.h>
|
||||
#include <Shlwapi.h>
|
||||
#include <string.h>
|
||||
#include <wchar.h>
|
||||
|
50
api/resource.c
Normal file
50
api/resource.c
Normal file
@ -0,0 +1,50 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2018-2020 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
WINTUN_STATUS
|
||||
CopyResource(
|
||||
_In_z_ const WCHAR *DestinationPath,
|
||||
_In_opt_ SECURITY_ATTRIBUTES *SecurityAttributes,
|
||||
_In_z_ const WCHAR *ResourceName)
|
||||
{
|
||||
HRSRC FoundResource = FindResourceW(ResourceModule, ResourceName, RT_RCDATA);
|
||||
if (!FoundResource)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to find resource");
|
||||
DWORD SizeResource = SizeofResource(ResourceModule, FoundResource);
|
||||
if (!SizeResource)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to size resource");
|
||||
HGLOBAL LoadedResource = LoadResource(ResourceModule, FoundResource);
|
||||
if (!LoadedResource)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to load resource");
|
||||
LPVOID LockedResource = LockResource(LoadedResource);
|
||||
if (!LockedResource)
|
||||
{
|
||||
WINTUN_LOGGER(WINTUN_LOG_ERR, L"Failed to lock resource");
|
||||
return ERROR_LOCK_FAILED;
|
||||
}
|
||||
HANDLE DestinationHandle = CreateFileW(
|
||||
DestinationPath,
|
||||
GENERIC_WRITE,
|
||||
0,
|
||||
SecurityAttributes,
|
||||
CREATE_NEW,
|
||||
FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_TEMPORARY,
|
||||
NULL);
|
||||
if (DestinationHandle == INVALID_HANDLE_VALUE)
|
||||
return WINTUN_LOGGER_LAST_ERROR(L"Failed to create file");
|
||||
DWORD BytesWritten;
|
||||
DWORD Result = ERROR_SUCCESS;
|
||||
if (!WriteFile(DestinationHandle, LockedResource, SizeResource, &BytesWritten, NULL))
|
||||
Result = WINTUN_LOGGER_LAST_ERROR(L"Failed to write file");
|
||||
if (BytesWritten != SizeResource)
|
||||
{
|
||||
WINTUN_LOGGER(WINTUN_LOG_ERR, L"Incomplete write");
|
||||
Result = Result != ERROR_SUCCESS ? Result : ERROR_WRITE_FAULT;
|
||||
}
|
||||
CloseHandle(DestinationHandle);
|
||||
return Result;
|
||||
}
|
15
api/resource.h
Normal file
15
api/resource.h
Normal file
@ -0,0 +1,15 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2018-2020 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "api.h"
|
||||
#include <Windows.h>
|
||||
|
||||
WINTUN_STATUS
|
||||
CopyResource(
|
||||
_In_z_ const WCHAR *DestinationPath,
|
||||
_In_opt_ SECURITY_ATTRIBUTES *SecurityAttributes,
|
||||
_In_z_ const WCHAR *ResourceName);
|
@ -6,6 +6,18 @@
|
||||
#include <windows.h>
|
||||
#include <ntverp.h>
|
||||
|
||||
#ifdef HAVE_EV
|
||||
wintun.cat RCDATA "wintun\\wintun.cat"
|
||||
wintun.inf RCDATA "wintun\\wintun.inf"
|
||||
wintun.sys RCDATA "wintun\\wintun.sys"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_WHQL
|
||||
wintun-whql.cat RCDATA "whql\\wintun.cat"
|
||||
wintun-whql.inf RCDATA "whql\\wintun.inf"
|
||||
wintun-whql.sys RCDATA "whql\\wintun.sys"
|
||||
#endif
|
||||
|
||||
#define STRINGIZE(x) #x
|
||||
#define EXPAND(x) STRINGIZE(x)
|
||||
|
||||
|
31
wintun.sln
31
wintun.sln
@ -3,6 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
VisualStudioVersion = 16.0.28922.388
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "api", "api\api.vcxproj", "{897F02E3-3EAA-40AF-A6DC-17EB2376EDAF}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422} = {F7679B65-2FEC-469A-8BAC-B07BF4439422}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "installer", "installer\installer.vcxproj", "{D19E6354-A643-4ACC-82D5-B2780BB83475}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
@ -47,20 +50,6 @@ Global
|
||||
{897F02E3-3EAA-40AF-A6DC-17EB2376EDAF}.Release|arm64.Build.0 = Release|ARM64
|
||||
{897F02E3-3EAA-40AF-A6DC-17EB2376EDAF}.Release|x86.ActiveCfg = Release|Win32
|
||||
{897F02E3-3EAA-40AF-A6DC-17EB2376EDAF}.Release|x86.Build.0 = Release|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|amd64.ActiveCfg = Debug|x64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|amd64.Build.0 = Debug|x64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|arm.ActiveCfg = Debug|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|arm64.ActiveCfg = Debug|ARM64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|arm64.Build.0 = Debug|ARM64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|x86.Build.0 = Debug|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|amd64.ActiveCfg = Release|x64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|amd64.Build.0 = Release|x64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|arm.ActiveCfg = Release|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|arm64.ActiveCfg = Release|ARM64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|arm64.Build.0 = Release|ARM64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|x86.ActiveCfg = Release|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|x86.Build.0 = Release|Win32
|
||||
{D19E6354-A643-4ACC-82D5-B2780BB83475}.Debug|amd64.ActiveCfg = Debug|x64
|
||||
{D19E6354-A643-4ACC-82D5-B2780BB83475}.Debug|amd64.Build.0 = Debug|x64
|
||||
{D19E6354-A643-4ACC-82D5-B2780BB83475}.Debug|arm.ActiveCfg = Debug|Win32
|
||||
@ -75,6 +64,20 @@ Global
|
||||
{D19E6354-A643-4ACC-82D5-B2780BB83475}.Release|arm64.Build.0 = Release|ARM64
|
||||
{D19E6354-A643-4ACC-82D5-B2780BB83475}.Release|x86.ActiveCfg = Release|Win32
|
||||
{D19E6354-A643-4ACC-82D5-B2780BB83475}.Release|x86.Build.0 = Release|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|amd64.ActiveCfg = Debug|x64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|amd64.Build.0 = Debug|x64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|arm.ActiveCfg = Debug|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|arm64.ActiveCfg = Debug|ARM64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|arm64.Build.0 = Debug|ARM64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Debug|x86.Build.0 = Debug|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|amd64.ActiveCfg = Release|x64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|amd64.Build.0 = Release|x64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|arm.ActiveCfg = Release|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|arm64.ActiveCfg = Release|ARM64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|arm64.Build.0 = Release|ARM64
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|x86.ActiveCfg = Release|Win32
|
||||
{F7679B65-2FEC-469A-8BAC-B07BF4439422}.Release|x86.Build.0 = Release|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
Loading…
Reference in New Issue
Block a user