From 62cc6b9333a484fd386661e90005ecaeffadfc4c Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 13 Nov 2024 14:45:26 -0800 Subject: [PATCH] VPN-6626: Install socks proxy as a service (#9952) * Add curlpipe tool to test named pipe support on Windows * Add code to set our own Windows firewall rules * Implement Windows service thread * Add file logging when started as a service * Install the socks proxy as a Windows service * Split log levels and always write debug to file * Add logfile option to enable logging manually * Add WFP policy to restrict proxy access only to browsers * Split windows firewall stuff into its own class * Install socksproxy as a systemd service * Install/enable/start proxy service on debian installation * Add proxy service assets to RPM package too * Automatically start proxy on installation * Add a WinFwPolicy::create() method to handle WFP setup failure * Don't install systemd service in containerized environment --- extension/socks5proxy/bin/CMakeLists.txt | 32 +- extension/socks5proxy/bin/main.cpp | 80 ++++- extension/socks5proxy/bin/sockslogger.cpp | 219 ++++++++++++ .../bin/{verboselogger.h => sockslogger.h} | 36 +- .../socks5proxy/bin/socksproxy.service.in | 10 + extension/socks5proxy/bin/verboselogger.cpp | 126 ------- extension/socks5proxy/bin/windowsbypass.cpp | 21 +- extension/socks5proxy/bin/windowsbypass.h | 1 - extension/socks5proxy/bin/winfwpolicy.cpp | 327 ++++++++++++++++++ extension/socks5proxy/bin/winfwpolicy.h | 33 ++ extension/socks5proxy/bin/winsvcthread.cpp | 128 +++++++ extension/socks5proxy/bin/winsvcthread.h | 37 ++ extension/socks5proxy/bin/winutils.cpp | 21 ++ extension/socks5proxy/bin/winutils.h | 14 + extension/socks5proxy/src/CMakeLists.txt | 4 +- .../socks5proxy/src/socks5local_windows.cpp | 37 ++ extension/socks5proxy/tests/CMakeLists.txt | 12 +- extension/socks5proxy/tests/curlpipe.cpp | 107 ++++++ linux/debian/rules | 3 + linux/mozillavpn.spec | 2 + windows/installer/MozillaVPN.wxs | 27 +- 21 files changed, 1104 insertions(+), 173 deletions(-) create mode 100644 extension/socks5proxy/bin/sockslogger.cpp rename extension/socks5proxy/bin/{verboselogger.h => sockslogger.h} (66%) create mode 100644 extension/socks5proxy/bin/socksproxy.service.in delete mode 100644 extension/socks5proxy/bin/verboselogger.cpp create mode 100644 extension/socks5proxy/bin/winfwpolicy.cpp create mode 100644 extension/socks5proxy/bin/winfwpolicy.h create mode 100644 extension/socks5proxy/bin/winsvcthread.cpp create mode 100644 extension/socks5proxy/bin/winsvcthread.h create mode 100644 extension/socks5proxy/bin/winutils.cpp create mode 100644 extension/socks5proxy/bin/winutils.h create mode 100644 extension/socks5proxy/src/socks5local_windows.cpp create mode 100644 extension/socks5proxy/tests/curlpipe.cpp diff --git a/extension/socks5proxy/bin/CMakeLists.txt b/extension/socks5proxy/bin/CMakeLists.txt index bac50d79e9..9189555833 100644 --- a/extension/socks5proxy/bin/CMakeLists.txt +++ b/extension/socks5proxy/bin/CMakeLists.txt @@ -4,8 +4,8 @@ qt_add_executable(socksproxy main.cpp - verboselogger.cpp - verboselogger.h + sockslogger.cpp + sockslogger.h ) target_link_libraries(socksproxy PUBLIC @@ -15,15 +15,23 @@ target_link_libraries(socksproxy PUBLIC ) if(WIN32) + target_compile_definitions(socksproxy PRIVATE PROXY_OS_WIN) target_sources(socksproxy PRIVATE windowsbypass.cpp - windowsbypass.h) + windowsbypass.h + winfwpolicy.cpp + winfwpolicy.h + winsvcthread.cpp + winsvcthread.h + winutils.cpp + winutils.h) target_link_libraries(socksproxy PRIVATE Iphlpapi.lib) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/socksproxy.exe DESTINATION .) elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + target_compile_definitions(socksproxy PRIVATE PROXY_OS_LINUX) target_sources(socksproxy PRIVATE linuxbypass.cpp linuxbypass.h) @@ -32,9 +40,21 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") pkg_check_modules(LIBCAP REQUIRED IMPORTED_TARGET libcap) target_link_libraries(socksproxy PRIVATE PkgConfig::LIBCAP) - # TODO: not install that yet. - #install(FILES ${CMAKE_CURRENT_BINARY_DIR}/socksproxy - # DESTINATION ${CMAKE_INSTALL_DATADIR}/socksproxy) + # Install a Systemd service to run the proxy, if supported. + pkg_check_modules(SYSTEMD systemd) + if("${SYSTEMD_FOUND}" EQUAL 1) + pkg_get_variable(SYSTEMD_UNIT_DIR systemd systemdsystemunitdir) + elseif(NOT DEFINED ENV{container}) + set(SYSTEMD_UNIT_DIR /lib/systemd/system) + endif() + if(SYSTEMD_UNIT_DIR) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/socksproxy.service.in + ${CMAKE_CURRENT_BINARY_DIR}/socksproxy.service) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/socksproxy.service + DESTINATION ${SYSTEMD_UNIT_DIR}) + endif() + + install(TARGETS socksproxy) else() # TODO: This is currently pointless on macos, # so no point in shipping it. diff --git a/extension/socks5proxy/bin/main.cpp b/extension/socks5proxy/bin/main.cpp index ca25a22316..c6784fcb46 100644 --- a/extension/socks5proxy/bin/main.cpp +++ b/extension/socks5proxy/bin/main.cpp @@ -4,20 +4,23 @@ #include #include +#include #include #include +#include #include #include #include #include "socks5.h" -#include "verboselogger.h" +#include "sockslogger.h" -#ifdef __linux__ +#if defined(PROXY_OS_LINUX) # include "linuxbypass.h" -#endif -#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +#elif defined(PROXY_OS_WIN) # include "windowsbypass.h" +# include "winfwpolicy.h" +# include "winsvcthread.h" #endif struct CliOptions { @@ -27,6 +30,10 @@ struct CliOptions { QString username = {}; QString password = {}; bool verbose = false; + bool logfile = false; +#if defined(PROXY_OS_WIN) + bool service = false; +#endif }; static CliOptions parseArgs(const QCoreApplication& app) { @@ -48,11 +55,25 @@ static CliOptions parseArgs(const QCoreApplication& app) { QCommandLineOption passOption({"P", "password"}, "The password", "password"); parser.addOption(passOption); - QCommandLineOption localOption({"l", "local"}, "Local socket name", "name"); +#if defined(PROXY_OS_WIN) + QCommandLineOption localOption({"n", "pipe"}, "SOCKS proxy over named pipe", + "name"); +#else + QCommandLineOption localOption({"n", "unix"}, + "SOCKS proxy over UNIX domain socket", "path"); +#endif parser.addOption(localOption); + QCommandLineOption logfileOption({"l", "logfile"}, "Save logs to file"); + parser.addOption(logfileOption); + QCommandLineOption verboseOption({"v", "verbose"}, "Verbose"); parser.addOption(verboseOption); + +#if defined(PROXY_OS_WIN) + QCommandLineOption serviceOption({"s", "service"}, "Windows service mode"); + parser.addOption(serviceOption); +#endif parser.process(app); CliOptions out = {}; @@ -77,9 +98,19 @@ static CliOptions parseArgs(const QCoreApplication& app) { if (parser.isSet(localOption)) { out.localSocketName = parser.value(localOption); } + if (parser.isSet(logfileOption)) { + out.logfile = true; + } if (parser.isSet(verboseOption)) { out.verbose = true; } +#if defined(PROXY_OS_WIN) + if (parser.isSet(serviceOption)) { + out.service = true; + // Enforce logging when started as a service. + out.logfile = true; + } +#endif return out; }; @@ -95,10 +126,29 @@ int main(int argc, char** argv) { return 1; } + auto* logger = new SocksLogger(&app); + logger->setVerbose(config.verbose); + if (config.logfile) { + auto location = QStandardPaths::AppLocalDataLocation; + QDir logdir(QStandardPaths::writableLocation(location)); + logger->setLogfile(logdir.filePath("socksproxy.log")); + } + +#if defined(PROXY_OS_WIN) + if (config.service) { + WinSvcThread* svc = new WinSvcThread("Mozilla VPN Proxy"); + svc->start(); + } +#endif + Socks5* socks5; if (!config.localSocketName.isEmpty()) { - QLocalServer* server = new QLocalServer(); + QLocalServer* server = new QLocalServer(&app); + QObject::connect(&app, &QCoreApplication::aboutToQuit, server, + &QLocalServer::close); + socks5 = new Socks5(server); + server->setSocketOptions(QLocalServer::WorldAccessOption); if (server->listen(config.localSocketName)) { qDebug() << "Starting on local socket" << server->fullServerName(); } else if ((server->serverError() == QAbstractSocket::AddressInUseError) && @@ -112,24 +162,26 @@ int main(int argc, char** argv) { return 1; } } else { - QTcpServer* server = new QTcpServer(); + QTcpServer* server = new QTcpServer(&app); + QObject::connect(&app, &QCoreApplication::aboutToQuit, server, + &QTcpServer::close); + socks5 = new Socks5(server); if (server->listen(config.addr, config.port)) { - qDebug() << "Starting on port" << config.port; + qDebug() << "Starting on port" << server->serverPort(); } else { qWarning() << "Unable to listen to the proxy port" << config.port; return 1; } } + QObject::connect(socks5, &Socks5::incomingConnection, logger, + &SocksLogger::incomingConnection); - if (config.verbose) { - new VerboseLogger(socks5); - } - -#ifdef __linux__ +#if defined(PROXY_OS_LINUX) new LinuxBypass(socks5); -#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +#elif defined(PROXY_OS_WIN) new WindowsBypass(socks5); + WinFwPolicy::create(socks5); #endif return app.exec(); diff --git a/extension/socks5proxy/bin/sockslogger.cpp b/extension/socks5proxy/bin/sockslogger.cpp new file mode 100644 index 0000000000..b8cfb2d038 --- /dev/null +++ b/extension/socks5proxy/bin/sockslogger.cpp @@ -0,0 +1,219 @@ +/* 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 "sockslogger.h" + +#include +#include +#include +#include + +#include "socks5.h" + +// 4MB of log data ought to be enough for anyone. +constexpr const qsizetype LOGFILE_MAX_SIZE = 4 * 1024 * 1024; + +SocksLogger* SocksLogger::s_instance = nullptr; + +// static +QString SocksLogger::bytesToString(qint64 bytes) { + if (bytes < 1024) { + return QString("%1b").arg(bytes); + } + + if (bytes < 1024 * 1024) { + return QString("%1Kb").arg(bytes / 1024); + } + + if (bytes < 1024 * 1024 * 1024) { + return QString("%1Mb").arg(bytes / (1024 * 1024)); + } + + return QString("%1Gb").arg(bytes / (1024 * 1024 * 1024)); +} + +SocksLogger::SocksLogger(QObject* parent) : QObject(parent) { + connect(&m_timer, &QTimer::timeout, this, &SocksLogger::tick); + m_timer.setSingleShot(false); + m_timer.start(1000); + + // Store our singleton reference. + s_instance = this; + + // Install a custom log handler that plays nicely with the status. + qInstallMessageHandler(logHandler); +} + +SocksLogger::~SocksLogger() { + qInstallMessageHandler(nullptr); + s_instance = nullptr; +} + +void SocksLogger::incomingConnection(Socks5Connection* conn) { + connect(conn, &Socks5Connection::dataSentReceived, this, + &SocksLogger::dataSentReceived); + connect(conn, &Socks5Connection::stateChanged, this, + &SocksLogger::connectionStateChanged); + connect(conn, &QObject::destroyed, this, [this]() { m_numConnections--; }); + + m_events.append( + Event{conn->clientName(), QDateTime::currentMSecsSinceEpoch()}); + m_numConnections++; + printStatus(); +} + +void SocksLogger::tick() { + // Update the boxcar average. + m_tx_bytes.advance(); + m_rx_bytes.advance(); + + // Drop entries from the event queue. + qint64 now = QDateTime::currentMSecsSinceEpoch(); + QMutableListIterator i(m_events); + while (i.hasNext()) { + if ((now - i.next().m_when) > 1000) { + i.remove(); + } + } + + // Update the status. + printStatus(); + + // Handle logfile rotation. + QMutexLocker lock(&m_logFileMutex); + if (m_logFileDevice && (m_logFileDevice->size() > LOGFILE_MAX_SIZE)) { + // Truncate the file. + m_logFileDevice->seek(0); + m_logFileDevice->resize(0); + } +} + +void SocksLogger::printStatus() { + QString output; + { + QTextStream out(&output); + out << "Connections: " << m_numConnections; + + QStringList addresses; + for (const Event& event : m_events) { + if (!event.m_newConnection.isEmpty() && + !addresses.contains(event.m_newConnection)) { + addresses.append(event.m_newConnection); + } + } + out << " [" << addresses.join(", ") << "]"; + out << " Up: " << bytesToString(m_tx_bytes.average()) << "/s"; + out << " Down: " << bytesToString(m_rx_bytes.average()) << "/s"; + } + + output.truncate(80); + while (output.length() < 80) output.append(' '); + QTextStream out(stdout); + out << output << '\r'; + + m_lastStatus = output; +} + +void SocksLogger::dataSentReceived(qint64 sent, qint64 received) { + m_tx_bytes.addSample(sent); + m_rx_bytes.addSample(received); +} + +void SocksLogger::connectionStateChanged() { + Socks5Connection* conn = qobject_cast(QObject::sender()); + if (conn->state() == Socks5Connection::Proxy) { + auto msg = qDebug() << "Connecting" << conn->clientName() << "to"; + for (const QString& hostname : conn->dnsLookupStack()) { + msg << hostname << "->"; + } + msg << conn->destAddress().toString(); + } +} + +// static and recursive! +bool SocksLogger::makeLogDir(const QDir& dir) { + if (dir.exists()) { + return true; + } + // Recursively make the parent. + QDir parent(dir); + if (!parent.cdUp()) { + return false; + } + if (!makeLogDir(parent)) { + return false; + } + // Make ourself. + return parent.mkdir(dir.dirName()); +} + +void SocksLogger::setVerbose(bool enabled) { + // Enable debug logging when logging to a file, or the verbose option is set. + auto* c = QLoggingCategory::defaultCategory(); + c->setEnabled(QtMsgType::QtDebugMsg, enabled || !m_logFileName.isEmpty()); + m_verbose = enabled; +} + +void SocksLogger::setLogfile(const QString& logfile) { + qInfo() << "Logging to:" << logfile; + if (!makeLogDir(QFileInfo(logfile).dir())) { + qInfo() << "Failed to create directory"; + return; + } + m_logFileName = logfile; + setVerbose(m_verbose); + + QMutexLocker lock(&m_logFileMutex); + + // Close the log file, if any. + if (m_logFileDevice) { + m_logFileDevice->flush(); + m_logFileDevice->close(); + m_logFileDevice->deleteLater(); + m_logFileDevice = nullptr; + } + + // Open the new log file, if any. + if (!m_logFileName.isEmpty()) { + m_logFileDevice = new QFile(m_logFileName, this); + const auto mode = + QIODeviceBase::WriteOnly | QIODeviceBase::Text | QIODeviceBase::Append; + const auto perms = QFileDevice::ReadOwner | QFileDevice::WriteOwner | + QFileDevice::ReadGroup; + m_logFileDevice->open(mode); + m_logFileDevice->setPermissions(perms); + } +} + +void SocksLogger::logfileHandler(QtMsgType type, const QMessageLogContext& ctx, + const QString& msg) { + QMutexLocker lock(&m_logFileMutex); + if (m_logFileDevice) { + // Write the line into the logfile. + m_logFileDevice->write(msg.toUtf8() + '\n'); + m_logFileDevice->flush(); + } +} + +// static +void SocksLogger::logHandler(QtMsgType type, const QMessageLogContext& ctx, + const QString& msg) { + QTextStream out(stdout); + if (!s_instance) { + // No logger? Just dump everything to console. + out << msg << "\n"; + return; + } + + if (s_instance->m_verbose || (type != QtMsgType::QtDebugMsg)) { + // A message logger that plays nicely with the status output. + // Clears the current line - prints the log message - reprints the status. + out << QString(80, ' ') << '\r'; + out << msg << "\r\n"; + out << s_instance->m_lastStatus << '\r'; + } + + // Handle logging to file. + s_instance->logfileHandler(type, ctx, msg); +} diff --git a/extension/socks5proxy/bin/verboselogger.h b/extension/socks5proxy/bin/sockslogger.h similarity index 66% rename from extension/socks5proxy/bin/verboselogger.h rename to extension/socks5proxy/bin/sockslogger.h index 43420ba88a..1264214014 100644 --- a/extension/socks5proxy/bin/verboselogger.h +++ b/extension/socks5proxy/bin/sockslogger.h @@ -2,14 +2,17 @@ * 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/. */ -#ifndef VERBOSELOGGER_H -#define VERBOSELOGGER_H +#ifndef SOCKSLOGGER_H +#define SOCKSLOGGER_H +#include #include #include #include #include +class QDir; +class QFile; class Socks5; class Socks5Connection; @@ -43,18 +46,27 @@ class BoxcarAverage final { QVector m_data; }; -class VerboseLogger final : public QObject { +class SocksLogger final : public QObject { Q_OBJECT public: - explicit VerboseLogger(Socks5* proxy); - ~VerboseLogger() = default; + explicit SocksLogger(QObject* parent = nullptr); + ~SocksLogger(); static QString bytesToString(qint64 value); - void printStatus(); + const QString& logfile() const { return m_logFileName; } + void setLogfile(const QString& filename); + void setVerbose(bool enabled); + + public slots: + void incomingConnection(Socks5Connection* conn); + private: + static bool makeLogDir(const QDir& dir); + void logfileHandler(QtMsgType type, const QMessageLogContext& ctx, + const QString& msg); static void logHandler(QtMsgType type, const QMessageLogContext& ctx, const QString& msg); void dataSentReceived(qint64 sent, qint64 received); @@ -62,19 +74,25 @@ class VerboseLogger final : public QObject { void tick(); private: - Socks5* m_socks = nullptr; + static SocksLogger* s_instance; + + bool m_verbose = false; + QString m_logFileName; + QMutex m_logFileMutex; + QFile* m_logFileDevice = nullptr; struct Event { QString m_newConnection; qint64 m_when; }; QList m_events; + qsizetype m_numConnections = 0; QTimer m_timer; - static QString s_lastStatus; + QString m_lastStatus; BoxcarAverage m_rx_bytes; BoxcarAverage m_tx_bytes; }; -#endif // VERBOSELOGGER_H +#endif // SOCKSLOGGER_H diff --git a/extension/socks5proxy/bin/socksproxy.service.in b/extension/socks5proxy/bin/socksproxy.service.in new file mode 100644 index 0000000000..3b0f398749 --- /dev/null +++ b/extension/socks5proxy/bin/socksproxy.service.in @@ -0,0 +1,10 @@ +[Unit] +Description=MozillaVPN SOCKS Proxy service + +[Service] +Type=simple +Restart=on-failure +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/socksproxy --unix /var/run/mozillavpn.proxy + +[Install] +WantedBy=multi-user.target diff --git a/extension/socks5proxy/bin/verboselogger.cpp b/extension/socks5proxy/bin/verboselogger.cpp deleted file mode 100644 index 1490689857..0000000000 --- a/extension/socks5proxy/bin/verboselogger.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/* 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 "verboselogger.h" - -#include -#include - -#include "socks5.h" - -// static -QString VerboseLogger::bytesToString(qint64 bytes) { - if (bytes < 1024) { - return QString("%1b").arg(bytes); - } - - if (bytes < 1024 * 1024) { - return QString("%1Kb").arg(bytes / 1024); - } - - if (bytes < 1024 * 1024 * 1024) { - return QString("%1Mb").arg(bytes / (1024 * 1024)); - } - - return QString("%1Gb").arg(bytes / (1024 * 1024 * 1024)); -} - -VerboseLogger::VerboseLogger(Socks5* socks5) - : QObject(socks5), m_socks(socks5) { - connect(&m_timer, &QTimer::timeout, this, &VerboseLogger::tick); - m_timer.setSingleShot(false); - m_timer.start(1000); - - connect(socks5, &Socks5::connectionsChanged, this, - &VerboseLogger::printStatus); - - connect(socks5, &Socks5::incomingConnection, this, - [this](Socks5Connection* conn) { - connect(conn, &Socks5Connection::dataSentReceived, this, - &VerboseLogger::dataSentReceived); - connect(conn, &Socks5Connection::stateChanged, this, - &VerboseLogger::connectionStateChanged); - - m_events.append( - Event{conn->clientName(), QDateTime::currentMSecsSinceEpoch()}); - printStatus(); - }); - - // Install a custom log handler that plays nicely with the status. - QLoggingCategory::defaultCategory()->setEnabled(QtMsgType::QtDebugMsg, true); - qInstallMessageHandler(logHandler); -} - -void VerboseLogger::tick() { - // Update the boxcar average. - m_tx_bytes.advance(); - m_rx_bytes.advance(); - - // Drop entries from the event queue. - qint64 now = QDateTime::currentMSecsSinceEpoch(); - QMutableListIterator i(m_events); - while (i.hasNext()) { - if ((now - i.next().m_when) > 1000) { - i.remove(); - } - } - - // Update the status. - printStatus(); -} - -QString VerboseLogger::s_lastStatus; - -void VerboseLogger::printStatus() { - QString output; - { - QTextStream out(&output); - out << "Connections: " << m_socks->connections(); - - QStringList addresses; - for (const Event& event : m_events) { - if (!event.m_newConnection.isEmpty() && - !addresses.contains(event.m_newConnection)) { - addresses.append(event.m_newConnection); - } - } - out << " [" << addresses.join(", ") << "]"; - out << " Up: " << bytesToString(m_tx_bytes.average()) << "/s"; - out << " Down: " << bytesToString(m_rx_bytes.average()) << "/s"; - } - - output.truncate(80); - while (output.length() < 80) output.append(' '); - QTextStream out(stdout); - out << output << '\r'; - - s_lastStatus = output; -} - -void VerboseLogger::dataSentReceived(qint64 sent, qint64 received) { - m_tx_bytes.addSample(sent); - m_rx_bytes.addSample(received); -} - -void VerboseLogger::connectionStateChanged() { - Socks5Connection* conn = qobject_cast(QObject::sender()); - if (conn->state() == Socks5Connection::Proxy) { - auto msg = qDebug() << "Connecting" << conn->clientName() << "to"; - for (const QString& hostname : conn->dnsLookupStack()) { - msg << hostname << "->"; - } - msg << conn->destAddress().toString(); - } -} - -// static -void VerboseLogger::logHandler(QtMsgType type, const QMessageLogContext& ctx, - const QString& msg) { - // A message logger that plays nicely with the status output. - // Clears the current line - prints the log message - reprints the status. - QTextStream out(stdout); - out << QString(80, ' ') << '\r'; - out << msg << "\r\n"; - out << s_lastStatus << '\r'; -} diff --git a/extension/socks5proxy/bin/windowsbypass.cpp b/extension/socks5proxy/bin/windowsbypass.cpp index 795ce398a3..5c6751b292 100644 --- a/extension/socks5proxy/bin/windowsbypass.cpp +++ b/extension/socks5proxy/bin/windowsbypass.cpp @@ -5,16 +5,20 @@ #include "windowsbypass.h" #include +#include #include #include #include #include +#include #include #include +#include #include #include "socks5.h" +#include "winutils.h" // Fixed GUID of the Wireguard NT driver. constexpr const QUuid WIREGUARD_NT_GUID(0xf64063ab, 0xbfee, 0x4881, 0xbf, 0x79, @@ -144,17 +148,6 @@ void WindowsBypass::outgoingConnection(QAbstractSocket* s, } // static -QString WindowsBypass::win32strerror(unsigned long code) { - LPWSTR buffer = nullptr; - DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS; - DWORD size = FormatMessageW(flags, nullptr, code, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPWSTR)&buffer, 0, nullptr); - QString result = QString::fromWCharArray(buffer, size); - LocalFree(buffer); - return result; -} quint64 WindowsBypass::getVpnLuid() const { // Get the LUID of the wireguard interface, if it's up. @@ -171,7 +164,8 @@ void WindowsBypass::refreshAddresses() { MIB_UNICASTIPADDRESS_TABLE* table; DWORD result = GetUnicastIpAddressTable(AF_UNSPEC, &table); if (result != NO_ERROR) { - qWarning() << "GetUnicastIpAddressTable() failed:" << win32strerror(result); + qWarning() << "GetUnicastIpAddressTable() failed:" + << WinUtils::win32strerror(result); return; } auto guard = qScopeGuard([table]() { FreeMibTable(table); }); @@ -340,7 +334,8 @@ void WindowsBypass::updateTable(QVector& table, MIB_IPFORWARD_TABLE2* mib; DWORD result = GetIpForwardTable2(family, &mib); if (result != NO_ERROR) { - qWarning() << "GetIpForwardTable2() failed:" << win32strerror(result); + qWarning() << "GetIpForwardTable2() failed:" + << WinUtils::win32strerror(result); return; } auto mibGuard = qScopeGuard([mib] { FreeMibTable(mib); }); diff --git a/extension/socks5proxy/bin/windowsbypass.h b/extension/socks5proxy/bin/windowsbypass.h index f380299d32..bd9b3bca5d 100644 --- a/extension/socks5proxy/bin/windowsbypass.h +++ b/extension/socks5proxy/bin/windowsbypass.h @@ -24,7 +24,6 @@ class WindowsBypass final : public QObject { ~WindowsBypass(); private: - static QString win32strerror(unsigned long code); quint64 getVpnLuid() const; void updateTable(QVector& table, int family); const struct _MIB_IPFORWARD_ROW2* lookupRoute(const QHostAddress& dest) const; diff --git a/extension/socks5proxy/bin/winfwpolicy.cpp b/extension/socks5proxy/bin/winfwpolicy.cpp new file mode 100644 index 0000000000..be338dcf79 --- /dev/null +++ b/extension/socks5proxy/bin/winfwpolicy.cpp @@ -0,0 +1,327 @@ +/* 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 "winfwpolicy.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "socks5.h" +#include "winutils.h" + +#pragma comment(lib, "Fwpuclnt") + +// Fixed GUID of the VPN Killswitch firewall sublayer +constexpr const QUuid KILLSWITCH_FW_GUID(0xc78056ff, 0x2bc1, 0x4211, 0xaa, 0xdd, + 0x7f, 0x35, 0x8d, 0xef, 0x20, 0x2d); + +// Fixed GUID of our own firewall sublayer +constexpr const QUuid LOCALPROXY_FW_GUID(0x0555706c, 0x4468, 0x4ec6, 0xb4, 47, + 0x20, 0xe7, 0x4a, 0x10, 0x06, 0xe7); + +WinFwPolicy* WinFwPolicy::create(Socks5* proxy) { + WinFwPolicy* fwPolicy = new WinFwPolicy(proxy); + if (!fwPolicy->isValid()) { + delete fwPolicy; + return nullptr; + } + + // If listening on a local TCP port, permit access only by web browsers. + QTcpServer* tcpServer = qobject_cast(proxy->parent()); + if ((tcpServer != nullptr) && tcpServer->serverAddress().isLoopback()) { + fwPolicy->restrictProxyPort(tcpServer->serverPort()); + } + + return fwPolicy; +} + +WinFwPolicy::WinFwPolicy(QObject* parent) : QObject(parent) { + // Create the firewall engine handle + FWPM_SESSION0 session; + memset(&session, 0, sizeof(session)); + session.flags = FWPM_SESSION_FLAG_DYNAMIC; + DWORD result = FwpmEngineOpen0(nullptr, RPC_C_AUTHN_WINNT, nullptr, &session, + &m_fwEngineHandle); + if (result != ERROR_SUCCESS) { + qDebug() << "Failed to open firewall engine:" + << WinUtils::win32strerror(result); + m_fwEngineHandle = nullptr; + return; + } + + auto subscribe = [](void* ctx, const FWPM_SUBLAYER_CHANGE0* change) { + auto fw = reinterpret_cast(ctx); + fw->fwpmSublayerChanged(change->changeType, change->subLayerKey); + }; + + // Watch for changes to the firewall + GUID fwguid = KILLSWITCH_FW_GUID; + FWPM_SUBLAYER_ENUM_TEMPLATE0 fwmatch{.providerKey = &fwguid}; + FWPM_SUBLAYER_SUBSCRIPTION0 fwsub = {0}; + fwsub.enumTemplate = &fwmatch; + fwsub.flags = FWPM_SUBSCRIPTION_FLAG_NOTIFY_ON_ADD; + fwsub.sessionKey = session.sessionKey; + result = FwpmSubLayerSubscribeChanges0(m_fwEngineHandle, &fwsub, subscribe, + this, &m_fwChangeHandle); + if (result != ERROR_SUCCESS) { + qDebug() << "Failed to create firewall subscription:" + << WinUtils::win32strerror(result); + FwpmEngineClose0(m_fwEngineHandle); + m_fwEngineHandle = nullptr; + return; + } + + // If the sublayer already exists - immediately generate a notification. + FWPM_SUBLAYER0* fwlayer; + result = FwpmSubLayerGetByKey0(m_fwEngineHandle, &fwguid, &fwlayer); + if (result == ERROR_SUCCESS) { + fwpmSublayerChanged(FWPM_CHANGE_ADD, KILLSWITCH_FW_GUID); + FwpmFreeMemory0((void**)&fwlayer); + } +} + +WinFwPolicy::~WinFwPolicy() { + if (m_fwEngineHandle) { + GUID localProxyGuid = LOCALPROXY_FW_GUID; + FwpmSubLayerUnsubscribeChanges0(m_fwEngineHandle, m_fwChangeHandle); + FwpmSubLayerDeleteByKey0(m_fwEngineHandle, &localProxyGuid); + FwpmEngineClose0(m_fwEngineHandle); + } +} + +void WinFwPolicy::fwpmSublayerChanged(uint changeType, + const QUuid& subLayerKey) { + // Ignore everything except sublayer creation. + if (changeType != FWPM_CHANGE_ADD) { + return; + } + if (subLayerKey != KILLSWITCH_FW_GUID) { + return; + } + + // Get the AppID for the current executable; + FWP_BYTE_BLOB* appID = NULL; + WCHAR filePath[MAX_PATH]; + GetModuleFileNameW(nullptr, filePath, MAX_PATH); + DWORD result = FwpmGetAppIdFromFileName0(filePath, &appID); + if (result != ERROR_SUCCESS) { + qDebug() << "Firewall setup failed:" << WinUtils::win32strerror(result); + return; + } + auto appGuard = qScopeGuard([appID]() { FwpmFreeMemory0((void**)&appID); }); + + // Condition: Request must come from the .exe + FWPM_FILTER_CONDITION0 conds; + conds.fieldKey = FWPM_CONDITION_ALE_APP_ID; + conds.matchType = FWP_MATCH_EQUAL; + conds.conditionValue.type = FWP_BYTE_BLOB_TYPE; + conds.conditionValue.byteBlob = appID; + + // Assemble the Filter base + WCHAR filterName[] = L"Permit socksproxy bypass traffic"; + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = &conds; + filter.numFilterConditions = 1; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = 15; + filter.subLayerKey = KILLSWITCH_FW_GUID; + filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT; + filter.displayData.name = filterName; + + // Start a transaction so that the firewall changes can be made atomically. + FwpmTransactionBegin0(m_fwEngineHandle, 0); + auto txnGuard = + qScopeGuard([this]() { FwpmTransactionAbort0(m_fwEngineHandle); }); + + WCHAR descv4out[] = L"Permit outbound IPv4 traffic from proxy"; + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + filter.displayData.description = descv4out; + result = FwpmFilterAdd0(m_fwEngineHandle, &filter, nullptr, nullptr); + if (result != ERROR_SUCCESS) { + return; + } + + WCHAR descv4in[] = L"Permit inbound IPv4 traffic to proxy"; + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4; + filter.displayData.description = descv4in; + result = FwpmFilterAdd0(m_fwEngineHandle, &filter, nullptr, nullptr); + if (result != ERROR_SUCCESS) { + return; + } + + WCHAR descv6out[] = L"Permit outbound IPv6 traffic from proxy"; + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6; + filter.displayData.description = descv6out; + FwpmFilterAdd0(m_fwEngineHandle, &filter, nullptr, nullptr); + if (result != ERROR_SUCCESS) { + return; + } + + WCHAR descv6in[] = L"Permit inbound IPv6 traffic to proxy"; + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; + filter.displayData.description = descv6in; + FwpmFilterAdd0(m_fwEngineHandle, &filter, nullptr, nullptr); + if (result != ERROR_SUCCESS) { + return; + } + + // Commit the transaction + if (FwpmTransactionCommit0(m_fwEngineHandle) == ERROR_SUCCESS) { + txnGuard.dismiss(); + } else { + qDebug() << "Firewall setup failed:" << WinUtils::win32strerror(result); + } +} + +void WinFwPolicy::restrictProxyPort(quint16 port) { + // Start a transaction so that the firewall changes can be made atomically. + FwpmTransactionBegin0(m_fwEngineHandle, 0); + auto txnGuard = + qScopeGuard([this]() { FwpmTransactionAbort0(m_fwEngineHandle); }); + + // Check if the Layer Already Exists + GUID proxyguid = LOCALPROXY_FW_GUID; + FWPM_SUBLAYER0* sublayer = nullptr; + DWORD result = FwpmSubLayerGetByKey0(m_fwEngineHandle, &proxyguid, &sublayer); + if (result == ERROR_SUCCESS) { + FwpmFreeMemory0((void**)&sublayer); + sublayer = nullptr; + } else { + FWPM_SUBLAYER0 newlayer; + memset(&newlayer, 0, sizeof(newlayer)); + newlayer.subLayerKey = LOCALPROXY_FW_GUID; + newlayer.displayData.name = (PWSTR)L"MozillaVPN-Proxy-Sublayer"; + newlayer.displayData.description = + (PWSTR)L"Restrict application access to the proxy"; + newlayer.weight = 0xFFFF; + result = FwpmSubLayerAdd0(m_fwEngineHandle, &newlayer, nullptr); + if (result != ERROR_SUCCESS) { + qDebug() << "Firewall setup failed:" << WinUtils::win32strerror(result); + return; + } + } + + // Block TCP traffic sent the localhost port. + FWPM_FILTER_CONDITION0 conds[4]; + conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[0].matchType = FWP_MATCH_EQUAL; + conds[0].conditionValue.type = FWP_UINT8; + conds[0].conditionValue.uint8 = IPPROTO_TCP; + + conds[1].fieldKey = FWPM_CONDITION_LOCAL_INTERFACE_TYPE; + conds[1].matchType = FWP_MATCH_EQUAL; + conds[1].conditionValue.type = FWP_UINT32; + conds[1].conditionValue.uint32 = IF_TYPE_SOFTWARE_LOOPBACK; + + conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT; + conds[2].matchType = FWP_MATCH_EQUAL; + conds[2].conditionValue.type = FWP_UINT16; + conds[2].conditionValue.uint16 = port; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = conds; + filter.numFilterConditions = 3; + filter.action.type = FWP_ACTION_BLOCK; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = 1; + filter.subLayerKey = LOCALPROXY_FW_GUID; + filter.flags = FWPM_FILTER_FLAG_NONE; + + WCHAR descv4block[] = L"Block local IPv4 connections to proxy"; + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + filter.displayData.name = descv4block; + filter.displayData.description = descv4block; + result = FwpmFilterAdd0(m_fwEngineHandle, &filter, nullptr, nullptr); + if (result != ERROR_SUCCESS) { + qDebug() << "Firewall setup failed:" << WinUtils::win32strerror(result); + return; + } + WCHAR descv6block[] = L"Block local IPv6 connections to proxy"; + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6; + filter.displayData.name = descv6block; + filter.displayData.description = descv6block; + result = FwpmFilterAdd0(m_fwEngineHandle, &filter, nullptr, nullptr); + if (result != ERROR_SUCCESS) { + qDebug() << "Firewall setup failed:" << WinUtils::win32strerror(result); + return; + } + + // Now for the fun part - permit all browsers! + filter.weight.uint8 = 15; + filter.action.type = FWP_ACTION_PERMIT; + filter.numFilterConditions = 4; + QSettings hklm("HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\StartMenuInternet", + QSettings::Registry64Format); + for (const QString& name : hklm.childGroups()) { + hklm.beginGroup(name); + auto hklmGuard = qScopeGuard([&]() { hklm.endGroup(); }); + QVariant value = hklm.value("shell/open/command/Default"); + if (!value.isValid()) { + continue; + } + // Let's just pretend Internet Explorer doesn't exist. + // It's bundled into all kinds of Windows internals, to the point where I + // don't think we can trust it to be a real user browser anymore. + if (name.startsWith("iexplore", Qt::CaseInsensitive)) { + continue; + } + + // Strip unnecessary quotations by removing an equal number of leading and + // trailing quotation marks. + QString command = value.toString(); + while ((command.front() == '"') && (command.back() == '"')) { + command = command.mid(1, command.size() - 2); + } + qDebug() << "Permitting browser traffic for:" << name; + + // Build the final condition to match the application ID. + FWP_BYTE_BLOB* appID = NULL; + result = FwpmGetAppIdFromFileName0((PCWSTR)command.utf16(), &appID); + if (result != ERROR_SUCCESS) { + qDebug() << "Failed to get appid for:" << name; + continue; + } + auto appGuard = qScopeGuard([&]() { FwpmFreeMemory0((void**)&appID); }); + conds[3].fieldKey = FWPM_CONDITION_ALE_APP_ID; + conds[3].matchType = FWP_MATCH_EQUAL; + conds[3].conditionValue.type = FWP_BYTE_BLOB_TYPE; + conds[3].conditionValue.byteBlob = appID; + + QString descv4allow = QString("Permit IPv4 connections from %1").arg(name); + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + filter.displayData.name = (LPWSTR)descv4allow.utf16(); + filter.displayData.description = (LPWSTR)descv4allow.utf16(); + result = FwpmFilterAdd0(m_fwEngineHandle, &filter, nullptr, nullptr); + if (result != ERROR_SUCCESS) { + return; + } + + QString descv6allow = QString("Permit IPv4 connections from %1").arg(name); + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6; + filter.displayData.name = (LPWSTR)descv6allow.utf16(); + filter.displayData.description = (LPWSTR)descv6allow.utf16(); + result = FwpmFilterAdd0(m_fwEngineHandle, &filter, nullptr, nullptr); + if (result != ERROR_SUCCESS) { + return; + } + } + + // Commit the transaction + if (FwpmTransactionCommit0(m_fwEngineHandle) == ERROR_SUCCESS) { + txnGuard.dismiss(); + } else { + qDebug() << "Firewall setup failed:" << WinUtils::win32strerror(result); + } +} diff --git a/extension/socks5proxy/bin/winfwpolicy.h b/extension/socks5proxy/bin/winfwpolicy.h new file mode 100644 index 0000000000..20b4684dcd --- /dev/null +++ b/extension/socks5proxy/bin/winfwpolicy.h @@ -0,0 +1,33 @@ +/* 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/. */ + +#ifndef WINFWPOLICY_H +#define WINFWPOLICY_H + +#include +#include + +class Socks5; + +class WinFwPolicy final : public QObject { + Q_OBJECT + + public: + static WinFwPolicy* create(Socks5* proxy); + ~WinFwPolicy(); + + bool isValid() const { return m_fwEngineHandle != nullptr; } + + private: + WinFwPolicy(QObject* parent = nullptr); + + void restrictProxyPort(quint16 port); + void fwpmSublayerChanged(uint changeType, const QUuid& subLayerKey); + + private: + void* m_fwEngineHandle = nullptr; + void* m_fwChangeHandle = nullptr; +}; + +#endif // WINFWPOLICY_H diff --git a/extension/socks5proxy/bin/winsvcthread.cpp b/extension/socks5proxy/bin/winsvcthread.cpp new file mode 100644 index 0000000000..b5db1a7d54 --- /dev/null +++ b/extension/socks5proxy/bin/winsvcthread.cpp @@ -0,0 +1,128 @@ +/* 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 "winsvcthread.h" + +#include +#include + +#include +#include + +// static +WinSvcThread* WinSvcThread::s_instance = nullptr; + +WinSvcThread::WinSvcThread(const QString& name, QObject* parent) + : QThread(parent), m_serviceName(name) { + Q_ASSERT(s_instance == nullptr); + s_instance = this; + + m_serviceStatus = new SERVICE_STATUS; + ZeroMemory(m_serviceStatus, sizeof(SERVICE_STATUS)); + m_serviceStatus->dwServiceType = SERVICE_WIN32_OWN_PROCESS; + m_serviceStatus->dwCurrentState = SERVICE_START_PENDING; + + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, + &WinSvcThread::aboutToQuit); +} + +void WinSvcThread::run() { + auto lambdaServiceMain = [](DWORD argc, LPWSTR* argv) { + Q_UNUSED(argc); + Q_UNUSED(argv); + QStringList arguments; + for (DWORD i = 0; i < argc; i++) { + arguments.append(QString::fromWCharArray(argv[i])); + } + s_instance->svcMain(arguments); + }; + + SERVICE_TABLE_ENTRYW serviceTable[] = { + {(LPWSTR)s_instance->m_serviceName.utf16(), lambdaServiceMain}, + {nullptr, nullptr}, + }; + + // The service dispatcher blocks until the service is stopped. + StartServiceCtrlDispatcherW(serviceTable); +} + +WinSvcThread::~WinSvcThread() { + if (m_svcCtrlHandle) { + m_serviceStatus->dwControlsAccepted = 0; + m_serviceStatus->dwCurrentState = SERVICE_STOPPED; + m_serviceStatus->dwCheckPoint = 1; + SetServiceStatus(m_svcCtrlHandle, m_serviceStatus); + } + delete m_serviceStatus; +} + +void WinSvcThread::svcMain(const QStringList& arguments) { + LPCWSTR wname = (LPCWSTR)m_serviceName.utf16(); + m_svcCtrlHandle = RegisterServiceCtrlHandlerEx(wname, svcCtrlHandler, this); + if (!m_svcCtrlHandle) { + qWarning() << "Failed to register the service handler"; + return; + } + if (!SetServiceStatus(m_svcCtrlHandle, m_serviceStatus)) { + m_serviceStatus->dwWin32ExitCode = GetLastError(); + qWarning() << "SetServiceStatus failed"; + return; + } + + // Set the service as running. + m_serviceStatus->dwControlsAccepted = SERVICE_ACCEPT_STOP; + m_serviceStatus->dwCurrentState = SERVICE_RUNNING; + m_serviceStatus->dwWin32ExitCode = 0; + m_serviceStatus->dwCheckPoint = 0; + if (SetServiceStatus(m_svcCtrlHandle, m_serviceStatus) == FALSE) { + qWarning() << "SetServiceStatus failed"; + return; + } +} + +void WinSvcThread::aboutToQuit() { + m_serviceStatus->dwControlsAccepted = 0; + m_serviceStatus->dwCurrentState = SERVICE_STOPPED; + m_serviceStatus->dwWin32ExitCode = GetLastError(); + m_serviceStatus->dwCheckPoint = 1; + if (SetServiceStatus(m_svcCtrlHandle, m_serviceStatus) == FALSE) { + qWarning() << "SetServiceStatus failed"; + } +} + +void WinSvcThread::svcCtrlStop() { + if (m_serviceStatus->dwCurrentState != SERVICE_RUNNING) { + return; + } + m_serviceStatus->dwControlsAccepted = 0; + m_serviceStatus->dwCurrentState = SERVICE_STOP_PENDING; + m_serviceStatus->dwWin32ExitCode = 0; + m_serviceStatus->dwCheckPoint = 4; + if (SetServiceStatus(m_svcCtrlHandle, m_serviceStatus) == FALSE) { + qWarning() << "SetServiceStatus failed"; + } + + // Request the application to exit. + QCoreApplication::instance()->quit(); +} + +ulong WinSvcThread::svcCtrlHandler(ulong code, ulong type, void* evdata, + void* context) { + Q_UNUSED(type); + Q_UNUSED(evdata); + + WinSvcThread* svc = reinterpret_cast(context); + switch (code) { + case SERVICE_CONTROL_STOP: + svc->svcCtrlStop(); + return NO_ERROR; + + case SERVICE_CONTROL_INTERROGATE: + // Always report that we support interrogation. + return NO_ERROR; + + default: + return ERROR_CALL_NOT_IMPLEMENTED; + } +} diff --git a/extension/socks5proxy/bin/winsvcthread.h b/extension/socks5proxy/bin/winsvcthread.h new file mode 100644 index 0000000000..6d3c10fcf2 --- /dev/null +++ b/extension/socks5proxy/bin/winsvcthread.h @@ -0,0 +1,37 @@ +/* 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/. */ + +#ifndef WINSVCTHREAD_H +#define WINSVCTHREAD_H + +#include +#include + +struct _SERVICE_STATUS; +struct SERVICE_STATUS_HANDLE__; + +class WinSvcThread final : public QThread { + public: + WinSvcThread(const QString& name, QObject* parent = nullptr); + ~WinSvcThread(); + + private slots: + void aboutToQuit(); + + private: + void run() override; + void svcMain(const QStringList& arguments); + void svcCtrlStop(); + + static WinSvcThread* s_instance; + static ulong svcCtrlHandler(ulong control, ulong type, void* event, + void* context); + + struct SERVICE_STATUS_HANDLE__* m_svcCtrlHandle; + struct _SERVICE_STATUS* m_serviceStatus; + + const QString m_serviceName; +}; + +#endif // WINSVCTHREAD_H diff --git a/extension/socks5proxy/bin/winutils.cpp b/extension/socks5proxy/bin/winutils.cpp new file mode 100644 index 0000000000..7208f455f3 --- /dev/null +++ b/extension/socks5proxy/bin/winutils.cpp @@ -0,0 +1,21 @@ +/* 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 "winutils.h" + +#include + +#include + +QString WinUtils::win32strerror(unsigned long code) { + LPWSTR buffer = nullptr; + DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS; + DWORD size = FormatMessageW(flags, nullptr, code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&buffer, 0, nullptr); + QString result = QString::fromWCharArray(buffer, size); + LocalFree(buffer); + return result; +} diff --git a/extension/socks5proxy/bin/winutils.h b/extension/socks5proxy/bin/winutils.h new file mode 100644 index 0000000000..9c0b39377d --- /dev/null +++ b/extension/socks5proxy/bin/winutils.h @@ -0,0 +1,14 @@ +/* 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/. */ + +#ifndef WINUTILS_H +#define WINUTILS_H + +#include + +namespace WinUtils { +QString win32strerror(unsigned long code); +} + +#endif // WINUTILS_H diff --git a/extension/socks5proxy/src/CMakeLists.txt b/extension/socks5proxy/src/CMakeLists.txt index 71a51dc96f..ccf230910e 100644 --- a/extension/socks5proxy/src/CMakeLists.txt +++ b/extension/socks5proxy/src/CMakeLists.txt @@ -13,7 +13,9 @@ target_sources(libSocks5proxy PRIVATE socks5connection.h ) -if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") +if(WIN32) + target_sources(libSocks5proxy PRIVATE socks5local_windows.cpp) +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") target_sources(libSocks5proxy PRIVATE socks5local_linux.cpp) else() target_sources(libSocks5proxy PRIVATE socks5local_default.cpp) diff --git a/extension/socks5proxy/src/socks5local_windows.cpp b/extension/socks5proxy/src/socks5local_windows.cpp new file mode 100644 index 0000000000..a4f4f2d721 --- /dev/null +++ b/extension/socks5proxy/src/socks5local_windows.cpp @@ -0,0 +1,37 @@ +/* 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/. */ + +// psapi.h must be included after windows.h +// clang-format off +#include +#include +// clang-format on + +#include +#include + +#include "socks5connection.h" + +// static +QString Socks5Connection::localClientName(QLocalSocket* s) { + // Get the process at the other end of the socket. + HANDLE pipe = reinterpret_cast(s->socketDescriptor()); + ULONG pid; + if (!GetNamedPipeClientProcessId(pipe, &pid)) { + return QString(); + } + HANDLE handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid); + if (handle == nullptr) { + return QString(); + } + auto guard = qScopeGuard([handle]() { CloseHandle(handle); }); + + // Read the process filename. + WCHAR filename[MAX_PATH]; + DWORD len = GetProcessImageFileNameW(handle, filename, MAX_PATH); + if (len > 0) { + return QString::fromWCharArray(filename, len); + } + return QString(); +} diff --git a/extension/socks5proxy/tests/CMakeLists.txt b/extension/socks5proxy/tests/CMakeLists.txt index 474ab28aba..a761412423 100644 --- a/extension/socks5proxy/tests/CMakeLists.txt +++ b/extension/socks5proxy/tests/CMakeLists.txt @@ -2,7 +2,6 @@ # 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/. - mz_add_test_target(testSocks5connection SOURCES testsocks5connection.cpp @@ -18,3 +17,14 @@ mz_add_test_target(testSocks5 DEPENDENCIES libSocks5proxy ) + +# For testing named pipe support, add a little wrapper tool to run curl through +# a named pipe. This should let us debug while we try to figure out how to get +# Firefox to use them. +if(WIN32) + qt_add_executable(curlpipe curlpipe.cpp) + target_link_libraries(curlpipe PUBLIC + Qt6::Core + Qt6::Network + ) +endif() diff --git a/extension/socks5proxy/tests/curlpipe.cpp b/extension/socks5proxy/tests/curlpipe.cpp new file mode 100644 index 0000000000..63ec85458a --- /dev/null +++ b/extension/socks5proxy/tests/curlpipe.cpp @@ -0,0 +1,107 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include + +class MockWorker final : public QObject { + Q_OBJECT + public: + MockWorker(QTcpSocket* s, const QString& name, QObject* parent = nullptr); + + private: + void pipeRead(); + void socketRead(); + + const QString m_name; + QLocalSocket* m_pipe = nullptr; + QTcpSocket* m_socket; +}; +#include "curlpipe.moc" + +MockWorker::MockWorker(QTcpSocket* s, const QString& name, QObject* parent) + : QObject(parent), m_name(name), m_socket(s) { + s->setParent(this); + + qInfo() << "Connection started:" << m_socket->peerAddress().toString(); + m_pipe = new QLocalSocket(this); + connect(m_pipe, &QLocalSocket::connected, this, [&]() { + connect(m_pipe, &QLocalSocket::readyRead, this, &MockWorker::pipeRead); + connect(m_socket, &QLocalSocket::readyRead, this, &MockWorker::socketRead); + }); + + connect(m_pipe, &QLocalSocket::disconnected, this, &QObject::deleteLater); + connect(m_pipe, &QLocalSocket::errorOccurred, this, &QObject::deleteLater); + connect(m_socket, &QTcpSocket::disconnected, this, &QObject::deleteLater); + connect(m_socket, &QTcpSocket::errorOccurred, this, &QObject::deleteLater); + + m_pipe->connectToServer(m_name); +} + +void MockWorker::pipeRead() { + while (m_pipe->bytesAvailable()) { + QByteArray data = m_pipe->read(4096); + m_socket->write(data); + } +} + +void MockWorker::socketRead() { + while (m_socket->bytesAvailable()) { + QByteArray data = m_socket->read(4096); + m_pipe->write(data); + } +} + +int main(int argc, char** argv) { + QCoreApplication app(argc, argv); + QString pipeName = "\\\\.\\pipe\\chrome.vpn.proxy"; + QStringList curlArgs = app.arguments().mid(1); + auto i = curlArgs.begin(); + while (i != curlArgs.end()) { + if (i->startsWith("--pipe=")) { + pipeName = i->mid(7); + i = curlArgs.erase(i); + } else if (*i == "--pipe") { + i = curlArgs.erase(i); + if (i == curlArgs.end()) { + qFatal() << "Argument --pipe requires a value."; + } + pipeName = *i; + i = curlArgs.erase(i); + } else { + i++; + } + } + + // Setup a QTcpServer to broker connection to the named pipes. + QTcpServer server; + QObject::connect(&server, &QTcpServer::newConnection, [&]() { + while (server.hasPendingConnections()) { + auto* s = server.nextPendingConnection(); + if (!s) { + break; + } + auto* worker = new MockWorker(s, pipeName); + } + }); + server.listen(QHostAddress::LocalHost); + QString proxyUrl = QString("socks5://127.0.0.1:%1").arg(server.serverPort()); + + // Launch curl with all the arguments we have been given. + QProcess curl; + curl.setProgram("curl"); + curl.setArguments(QStringList({"--proxy", proxyUrl}) + curlArgs); + curl.setProcessChannelMode(QProcess::ForwardedChannels); + + // Start curl and run the event loop until it exits. + QObject::connect(&curl, &QProcess::finished, &app, &QCoreApplication::exit); + curl.start(); + app.exec(); +} diff --git a/linux/debian/rules b/linux/debian/rules index bdfdd9bb21..9c4e0795a6 100755 --- a/linux/debian/rules +++ b/linux/debian/rules @@ -27,12 +27,15 @@ override_dh_installinfo: override_dh_installsystemd: dh_installsystemd $(CMAKE_BUILD_DIR)/src/mozillavpn.service + dh_installsystemd $(CMAKE_BUILD_DIR)/extension/socks5proxy/bin/socksproxy.service override_dh_systemd_start: dh_systemd_start $(CMAKE_BUILD_DIR)/src/mozillavpn.service + dh_systemd_start $(CMAKE_BUILD_DIR)/extension/socks5proxy/bin/socksproxy.service override_dh_systemd_enable: dh_systemd_enable $(CMAKE_BUILD_DIR)/src/mozillavpn.service + dh_systemd_enable $(CMAKE_BUILD_DIR)/extension/socks5proxy/bin/socksproxy.service override_dh_builddeb: dh_builddeb -- -Zgzip diff --git a/linux/mozillavpn.spec b/linux/mozillavpn.spec index e033fbfe6b..1c2639de5f 100644 --- a/linux/mozillavpn.spec +++ b/linux/mozillavpn.spec @@ -57,7 +57,9 @@ install %{_srcdir}/LICENSE.md %{buildroot}/%{_licensedir}/%{name}/ %{_sysconfdir}/chromium/native-messaging-hosts/mozillavpn.json %{_sysconfdir}/opt/chrome/native-messaging-hosts/mozillavpn.json %{_unitdir}/mozillavpn.service +%{_unitdir}/socksproxy.service %{_bindir}/mozillavpn +%{_bindir}/socksproxy %{_prefix}/lib/mozillavpn/mozillavpnnp %{_prefix}/lib/mozilla/native-messaging-hosts/mozillavpn.json %{_datadir}/applications/org.mozilla.vpn.desktop diff --git a/windows/installer/MozillaVPN.wxs b/windows/installer/MozillaVPN.wxs index 13d52dc090..26ccafd765 100644 --- a/windows/installer/MozillaVPN.wxs +++ b/windows/installer/MozillaVPN.wxs @@ -84,8 +84,6 @@ - - + + + + + + + + + + +