/* 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 "wireguardutilsmacos.h" #include "leakdetector.h" #include "logger.h" #include #include #include #include #include #include constexpr const int WG_TUN_PROC_TIMEOUT = 5000; constexpr const char* WG_RUNTIME_DIR = "/var/run/wireguard"; namespace { Logger logger(LOG_MACOS, "WireguardUtilsMacos"); Logger logwireguard(LOG_MACOS, "WireguardGo"); }; // namespace WireguardUtilsMacos::WireguardUtilsMacos(QObject* parent) : WireguardUtils(parent), m_tunnel(this) { MVPN_COUNT_CTOR(WireguardUtilsMacos); logger.debug() << "WireguardUtilsMacos created."; connect(&m_tunnel, SIGNAL(readyReadStandardOutput()), this, SLOT(tunnelStdoutReady())); connect(&m_tunnel, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(tunnelErrorOccurred(QProcess::ProcessError))); } WireguardUtilsMacos::~WireguardUtilsMacos() { MVPN_COUNT_DTOR(WireguardUtilsMacos); logger.debug() << "WireguardUtilsMacos destroyed."; } void WireguardUtilsMacos::tunnelStdoutReady() { for (;;) { QByteArray line = m_tunnel.readLine(); if (line.length() <= 0) { break; } logwireguard.debug() << QString::fromUtf8(line); } } void WireguardUtilsMacos::tunnelErrorOccurred(QProcess::ProcessError error) { logger.warning() << "Tunnel process encountered an error:" << error; emit backendFailure(); } bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) { Q_UNUSED(config); if (m_tunnel.state() != QProcess::NotRunning) { logger.warning() << "Unable to start: tunnel process already running"; return false; } QDir wgRuntimeDir(WG_RUNTIME_DIR); if (!wgRuntimeDir.exists()) { wgRuntimeDir.mkpath("."); } QProcessEnvironment pe = QProcessEnvironment::systemEnvironment(); QString wgNameFile = wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name"); pe.insert("WG_TUN_NAME_FILE", wgNameFile); #ifdef QT_DEBUG pe.insert("LOG_LEVEL", "debug"); #endif m_tunnel.setProcessEnvironment(pe); QDir appPath(QCoreApplication::applicationDirPath()); appPath.cdUp(); appPath.cd("Resources"); appPath.cd("utils"); QStringList wgArgs = {"-f", "utun"}; m_tunnel.start(appPath.filePath("wireguard-go"), wgArgs); if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) { logger.error() << "Unable to start tunnel process due to timeout"; m_tunnel.kill(); return false; } m_ifname = waitForTunnelName(wgNameFile); if (m_ifname.isNull()) { logger.error() << "Unable to read tunnel interface name"; m_tunnel.kill(); return false; } logger.debug() << "Created wireguard interface" << m_ifname; // Start the routing table monitor. m_rtmonitor = new MacosRouteMonitor(m_ifname, this); // Send a UAPI command to configure the interface QString message("set=1\n"); QByteArray privateKey = QByteArray::fromBase64(config.m_privateKey.toUtf8()); QTextStream out(&message); out << "private_key=" << QString(privateKey.toHex()) << "\n"; out << "replace_peers=true\n"; int err = uapiErrno(uapiCommand(message)); if (err != 0) { logger.error() << "Interface configuration failed:" << strerror(err); } return (err == 0); } bool WireguardUtilsMacos::deleteInterface() { if (m_rtmonitor) { delete m_rtmonitor; m_rtmonitor = nullptr; } if (m_tunnel.state() == QProcess::NotRunning) { return false; } // Attempt to terminate gracefully. m_tunnel.terminate(); if (!m_tunnel.waitForFinished(WG_TUN_PROC_TIMEOUT)) { m_tunnel.kill(); m_tunnel.waitForFinished(WG_TUN_PROC_TIMEOUT); } // Garbage collect. QDir wgRuntimeDir(WG_RUNTIME_DIR); QFile::remove(wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name")); return true; } // dummy implementations for now bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) { QByteArray publicKey = QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); // Update/create the peer config QString message; QTextStream out(&message); out << "set=1\n"; out << "public_key=" << QString(publicKey.toHex()) << "\n"; if (!config.m_serverIpv4AddrIn.isNull()) { out << "endpoint=" << config.m_serverIpv4AddrIn << ":"; } else if (!config.m_serverIpv6AddrIn.isNull()) { out << "endpoint=[" << config.m_serverIpv6AddrIn << "]:"; } else { logger.warning() << "Failed to create peer with no endpoints"; return false; } out << config.m_serverPort << "\n"; out << "replace_allowed_ips=true\n"; for (const IPAddressRange& ip : config.m_allowedIPAddressRanges) { out << "allowed_ip=" << ip.toString() << "\n"; } int err = uapiErrno(uapiCommand(message)); if (err != 0) { logger.error() << "Peer configuration failed:" << strerror(err); } return (err == 0); } bool WireguardUtilsMacos::deletePeer(const QString& pubkey) { QByteArray publicKey = QByteArray::fromBase64(qPrintable(pubkey)); QString message; QTextStream out(&message); out << "set=1\n"; out << "public_key=" << QString(publicKey.toHex()) << "\n"; out << "remove=true\n"; int err = uapiErrno(uapiCommand(message)); if (err != 0) { logger.error() << "Peer deletion failed:" << strerror(err); } return (err == 0); } WireguardUtils::peerStatus WireguardUtilsMacos::getPeerStatus( const QString& pubkey) { peerStatus status = {0, 0}; QString hexkey = QByteArray::fromBase64(pubkey.toUtf8()).toHex(); QString reply = uapiCommand("get=1"); bool match = false; for (const QString& line : reply.split('\n')) { int eq = line.indexOf('='); if (eq <= 0) { continue; } QString name = line.left(eq); QString value = line.mid(eq + 1); if (name == "public_key") { match = (value == hexkey); continue; } else if (!match) { continue; } if (name == "tx_bytes") { status.txBytes = value.toDouble(); } if (name == "rx_bytes") { status.rxBytes = value.toDouble(); } } return status; } bool WireguardUtilsMacos::updateRoutePrefix(const IPAddressRange& prefix, int hopindex) { Q_UNUSED(hopindex); if (!m_rtmonitor) { return false; } return m_rtmonitor->insertRoute(prefix); } bool WireguardUtilsMacos::deleteRoutePrefix(const IPAddressRange& prefix, int hopindex) { Q_UNUSED(hopindex); if (!m_rtmonitor) { return false; } return m_rtmonitor->deleteRoute(prefix); } QString WireguardUtilsMacos::uapiCommand(const QString& command) { QLocalSocket socket; QTimer uapiTimeout; QDir wgRuntimeDir(WG_RUNTIME_DIR); QString wgSocketFile = wgRuntimeDir.filePath(m_ifname + ".sock"); uapiTimeout.setSingleShot(true); uapiTimeout.start(WG_TUN_PROC_TIMEOUT); socket.connectToServer(wgSocketFile, QIODevice::ReadWrite); if (!socket.waitForConnected(WG_TUN_PROC_TIMEOUT)) { logger.error() << "QLocalSocket::waitForConnected() failed:" << socket.errorString(); return QString(); } // Send the message to the UAPI socket. QByteArray message = command.toLocal8Bit(); while (!message.endsWith("\n\n")) { message.append('\n'); } socket.write(message); QByteArray reply; while (!reply.contains("\n\n")) { if (!uapiTimeout.isActive()) { logger.error() << "UAPI command timed out"; return QString(); } QCoreApplication::processEvents(QEventLoop::AllEvents, 100); reply.append(socket.readAll()); } return QString::fromUtf8(reply).trimmed(); } // static int WireguardUtilsMacos::uapiErrno(const QString& reply) { for (const QString& line : reply.split("\n")) { int eq = line.indexOf('='); if (eq <= 0) { continue; } if (line.left(eq) == "errno") { return line.mid(eq + 1).toInt(); } } return EINVAL; } QString WireguardUtilsMacos::waitForTunnelName(const QString& filename) { QTimer timeout; timeout.setSingleShot(true); timeout.start(WG_TUN_PROC_TIMEOUT); QFile file(filename); while ((m_tunnel.state() == QProcess::Running) && timeout.isActive()) { QCoreApplication::processEvents(QEventLoop::AllEvents, 100); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { continue; } QString ifname = QString::fromLocal8Bit(file.readLine()).trimmed(); file.close(); // Test-connect to the UAPI socket. QLocalSocket sock; QDir wgRuntimeDir(WG_RUNTIME_DIR); QString sockName = wgRuntimeDir.filePath(ifname + ".sock"); sock.connectToServer(sockName, QIODevice::ReadWrite); if (sock.waitForConnected(100)) { return ifname; } } return QString(); }