amnezia-client/client/platforms/windows/daemon/windowstunnelservice.cpp
Mykola Baibuz 07c38e9b6c
WireGuard rework for MacOS and Windows (#314)
WireGuard rework for MacOS and Windows
2023-09-14 17:44:17 +01:00

345 lines
9.7 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "WindowsTunnelService.h"
#include <Windows.h>
#include <QDateTime>
#include <QScopeGuard>
#include "leakdetector.h"
#include "logger.h"
#include "platforms/windows/windowscommons.h"
#include "platforms/windows/windowsutils.h"
#include "windowsdaemon.h"
#define TUNNEL_NAMED_PIPE \
"\\\\." \
"\\pipe\\ProtectedPrefix\\Administrators\\WireGuard\\AmneziaVPN"
constexpr uint32_t WINDOWS_TUNNEL_MONITOR_TIMEOUT_MSEC = 2000;
namespace {
Logger logger("WindowsTunnelService");
} // namespace
static bool stopAndDeleteTunnelService(SC_HANDLE service);
static bool waitForServiceStatus(SC_HANDLE service, DWORD expectedStatus);
WindowsTunnelService::WindowsTunnelService(QObject* parent) : QObject(parent) {
MZ_COUNT_CTOR(WindowsTunnelService);
m_scm = OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (m_scm == nullptr) {
WindowsUtils::windowsLog("Failed to open SCManager");
}
connect(&m_timer, &QTimer::timeout, this, &WindowsTunnelService::timeout);
}
WindowsTunnelService::~WindowsTunnelService() {
MZ_COUNT_CTOR(WindowsTunnelService);
stop();
CloseServiceHandle((SC_HANDLE)m_scm);
}
void WindowsTunnelService::stop() {
SC_HANDLE service = (SC_HANDLE)m_service;
if (service) {
stopAndDeleteTunnelService(service);
CloseServiceHandle(service);
m_service = nullptr;
}
m_timer.stop();
if (m_logworker) {
m_logthread.quit();
m_logthread.wait();
delete m_logworker;
m_logworker = nullptr;
}
}
bool WindowsTunnelService::isRunning() {
if (m_service == nullptr) {
return false;
}
SERVICE_STATUS status;
if (!QueryServiceStatus((SC_HANDLE)m_service, &status)) {
return false;
}
return status.dwCurrentState == SERVICE_RUNNING;
}
void WindowsTunnelService::timeout() {
if (m_service == nullptr) {
logger.error() << "The service doesn't exist";
emit backendFailure();
return;
}
SERVICE_STATUS status;
if (!QueryServiceStatus((SC_HANDLE)m_service, &status)) {
WindowsUtils::windowsLog("Failed to retrieve the service status");
emit backendFailure();
return;
}
if (status.dwCurrentState == SERVICE_RUNNING) {
// The service is active
return;
}
logger.debug() << "The service is not active";
emit backendFailure();
}
bool WindowsTunnelService::start(const QString& configData) {
logger.debug() << "Starting the tunnel service";
m_logworker = new WindowsTunnelLogger(WindowsCommons::tunnelLogFile());
m_logworker->moveToThread(&m_logthread);
m_logthread.start();
SC_HANDLE scm = (SC_HANDLE)m_scm;
SC_HANDLE service = nullptr;
auto guard = qScopeGuard([&] {
if (service) {
CloseServiceHandle(service);
}
m_logthread.quit();
m_logthread.wait();
delete m_logworker;
m_logworker = nullptr;
});
// Let's see if we have to delete a previous instance.
service = OpenService(scm, TUNNEL_SERVICE_NAME, SERVICE_ALL_ACCESS);
if (service) {
logger.debug() << "An existing service has been detected. Let's close it.";
if (!stopAndDeleteTunnelService(service)) {
return false;
}
CloseServiceHandle(service);
service = nullptr;
}
QString serviceCmdline;
{
QTextStream out(&serviceCmdline);
out << "\"" << qApp->applicationFilePath() << "\" tunneldaemon \""
<< configData << "\"";
}
logger.debug() << "Service:" << qApp->applicationFilePath();
service = CreateService(scm, TUNNEL_SERVICE_NAME, L"Amezia VPN (tunnel)",
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
(const wchar_t*)serviceCmdline.utf16(), nullptr, 0,
TEXT("Nsi\0TcpIp\0"), nullptr, nullptr);
if (!service) {
WindowsUtils::windowsLog("Failed to create the tunnel service");
return false;
}
SERVICE_DESCRIPTION sd = {
(wchar_t*)L"Manages the Amnezia VPN tunnel connection"};
if (!ChangeServiceConfig2(service, SERVICE_CONFIG_DESCRIPTION, &sd)) {
WindowsUtils::windowsLog(
"Failed to set the description to the tunnel service");
return false;
}
SERVICE_SID_INFO ssi;
ssi.dwServiceSidType = SERVICE_SID_TYPE_UNRESTRICTED;
if (!ChangeServiceConfig2(service, SERVICE_CONFIG_SERVICE_SID_INFO, &ssi)) {
WindowsUtils::windowsLog("Failed to set the SID to the tunnel service");
return false;
}
if (!StartService(service, 0, nullptr)) {
WindowsUtils::windowsLog("Failed to start the service");
return false;
}
if (waitForServiceStatus(service, SERVICE_RUNNING)) {
logger.debug() << "The tunnel service is up and running";
guard.dismiss();
m_service = service;
m_timer.start(WINDOWS_TUNNEL_MONITOR_TIMEOUT_MSEC);
return true;
}
logger.error() << "Failed to run the tunnel service";
SERVICE_STATUS status;
if (!QueryServiceStatus(service, &status)) {
WindowsUtils::windowsLog("Failed to retrieve the service status");
return false;
}
logger.debug() << "The tunnel service exited with status:"
<< status.dwWin32ExitCode << "-" << exitCodeToFailure(&status);
emit backendFailure();
return false;
}
static bool stopAndDeleteTunnelService(SC_HANDLE service) {
SERVICE_STATUS status;
if (!QueryServiceStatus(service, &status)) {
WindowsUtils::windowsLog("Failed to retrieve the service status");
return false;
}
logger.debug() << "The current service is stopped:"
<< (status.dwCurrentState == SERVICE_STOPPED);
if (status.dwCurrentState != SERVICE_STOPPED) {
logger.debug() << "The service is not stopped yet.";
if (!ControlService(service, SERVICE_CONTROL_STOP, &status)) {
WindowsUtils::windowsLog("Failed to control the service");
return false;
}
if (!waitForServiceStatus(service, SERVICE_STOPPED)) {
logger.error() << "Unable to stop the service";
return false;
}
}
logger.debug() << "Proceeding with the deletion";
if (!DeleteService(service)) {
WindowsUtils::windowsLog("Failed to delete the service");
return false;
}
return true;
}
QString WindowsTunnelService::uapiCommand(const QString& command) {
// Create a pipe to the tunnel service.
LPTSTR tunnelName = (LPTSTR)TEXT(TUNNEL_NAMED_PIPE);
HANDLE pipe = CreateFile(tunnelName, GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, 0, nullptr);
if (pipe == INVALID_HANDLE_VALUE) {
return QString();
}
auto guard = qScopeGuard([&] { CloseHandle(pipe); });
if (!WaitNamedPipe(tunnelName, 1000)) {
WindowsUtils::windowsLog("Failed to wait for named pipes");
return QString();
}
DWORD mode = PIPE_READMODE_BYTE;
if (!SetNamedPipeHandleState(pipe, &mode, nullptr, nullptr)) {
WindowsUtils::windowsLog("Failed to set the read-mode on pipe");
return QString();
}
// Write the UAPI command into the pipe.
QByteArray message = command.toLocal8Bit();
DWORD written;
while (!message.endsWith("\n\n")) {
message.append('\n');
}
if (!WriteFile(pipe, message.constData(), message.length(), &written,
nullptr)) {
WindowsUtils::windowsLog("Failed to write into the pipe");
return QString();
}
// Receive the response from the pipe.
QByteArray reply;
while (!reply.contains("\n\n")) {
char buffer[512];
DWORD read = 0;
if (!ReadFile(pipe, buffer, sizeof(buffer), &read, nullptr)) {
break;
}
reply.append(buffer, read);
}
return QString::fromUtf8(reply).trimmed();
}
// static
static bool waitForServiceStatus(SC_HANDLE service, DWORD expectedStatus) {
int tries = 0;
while (tries < 30) {
SERVICE_STATUS status;
if (!QueryServiceStatus(service, &status)) {
WindowsUtils::windowsLog("Failed to retrieve the service status");
return false;
}
if (status.dwCurrentState == expectedStatus) {
return true;
}
logger.warning() << "The service is not in the right status yet.";
Sleep(1000);
++tries;
}
return false;
}
// static
QString WindowsTunnelService::exitCodeToFailure(const void* status) {
const SERVICE_STATUS* st = static_cast<const SERVICE_STATUS*>(status);
if (st->dwWin32ExitCode != ERROR_SERVICE_SPECIFIC_ERROR) {
return WindowsUtils::getErrorMessage(st->dwWin32ExitCode);
}
// The order of this error code is taken from wireguard.
switch (st->dwServiceSpecificExitCode) {
case 0:
return "No error";
case 1:
return "Error when opening the ringlogger log file";
case 2:
return "Error while loading the WireGuard configuration file from "
"path.";
case 3:
return "Error while creating a WinTun device.";
case 4:
return "Error while listening on a named pipe.";
case 5:
return "Error while resolving DNS hostname endpoints.";
case 6:
return "Error while manipulating firewall rules.";
case 7:
return "Error while setting the device configuration.";
case 8:
return "Error while binding sockets to default routes.";
case 9:
return "Unable to set interface addresses, routes, dns, and/or "
"interface settings.";
case 10:
return "Error while determining current executable path.";
case 11:
return "Error while opening the NUL file.";
case 12:
return "Error while attempting to track tunnels.";
case 13:
return "Error while attempting to enumerate current sessions.";
case 14:
return "Error while dropping privileges.";
case 15:
return "Windows internal error.";
default:
return QString("Unknown error (%1)").arg(st->dwServiceSpecificExitCode);
}
}