iOS Wireguard

This commit is contained in:
pokamest 2021-10-23 04:26:47 -07:00
parent 421f665e85
commit 7701efc704
117 changed files with 6577 additions and 0 deletions

View file

@ -0,0 +1,222 @@
/* 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 "dnsutilsmacos.h"
#include "leakdetector.h"
#include "logger.h"
#include <QScopeGuard>
#include <systemconfiguration/scpreferences.h>
#include <systemconfiguration/scdynamicstore.h>
namespace {
Logger logger(LOG_MACOS, "DnsUtilsMacos");
}
DnsUtilsMacos::DnsUtilsMacos(QObject* parent) : DnsUtils(parent) {
MVPN_COUNT_CTOR(DnsUtilsMacos);
m_scStore = SCDynamicStoreCreate(kCFAllocatorSystemDefault,
CFSTR("mozillavpn"), nullptr, nullptr);
if (m_scStore == nullptr) {
logger.error() << "Failed to create system configuration store ref";
}
logger.debug() << "DnsUtilsMacos created.";
}
DnsUtilsMacos::~DnsUtilsMacos() {
MVPN_COUNT_DTOR(DnsUtilsMacos);
restoreResolvers();
logger.debug() << "DnsUtilsMacos destroyed.";
}
static QString cfParseString(CFTypeRef ref) {
if (CFGetTypeID(ref) != CFStringGetTypeID()) {
return QString();
}
CFStringRef stringref = (CFStringRef)ref;
CFRange range;
range.location = 0;
range.length = CFStringGetLength(stringref);
if (range.length <= 0) {
return QString();
}
UniChar* buf = (UniChar*)malloc(range.length * sizeof(UniChar));
if (!buf) {
return QString();
}
auto guard = qScopeGuard([&] { free(buf); });
CFStringGetCharacters(stringref, range, buf);
return QString::fromUtf16(buf, range.length);
}
static QStringList cfParseStringList(CFTypeRef ref) {
if (CFGetTypeID(ref) != CFArrayGetTypeID()) {
return QStringList();
}
CFArrayRef array = (CFArrayRef)ref;
QStringList result;
for (CFIndex i = 0; i < CFArrayGetCount(array); i++) {
CFTypeRef value = CFArrayGetValueAtIndex(array, i);
result.append(cfParseString(value));
}
return result;
}
static void cfDictSetString(CFMutableDictionaryRef dict, CFStringRef name,
const QString& value) {
if (value.isNull()) {
return;
}
CFStringRef cfValue = CFStringCreateWithCString(
kCFAllocatorSystemDefault, qUtf8Printable(value), kCFStringEncodingUTF8);
CFDictionarySetValue(dict, name, cfValue);
CFRelease(cfValue);
}
static void cfDictSetStringList(CFMutableDictionaryRef dict, CFStringRef name,
const QStringList& valueList) {
if (valueList.isEmpty()) {
return;
}
CFMutableArrayRef array;
array = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0,
&kCFTypeArrayCallBacks);
if (array == nullptr) {
return;
}
for (const QString& rstring : valueList) {
CFStringRef cfAddr = CFStringCreateWithCString(kCFAllocatorSystemDefault,
qUtf8Printable(rstring),
kCFStringEncodingUTF8);
CFArrayAppendValue(array, cfAddr);
CFRelease(cfAddr);
}
CFDictionarySetValue(dict, name, array);
CFRelease(array);
}
bool DnsUtilsMacos::updateResolvers(const QString& ifname,
const QList<QHostAddress>& resolvers) {
Q_UNUSED(ifname);
// Get the list of current network services.
CFArrayRef netServices = SCDynamicStoreCopyKeyList(
m_scStore, CFSTR("Setup:/Network/Service/[0-9A-F-]+"));
if (netServices == nullptr) {
return false;
}
auto serviceGuard = qScopeGuard([&] { CFRelease(netServices); });
// Prepare the DNS configuration.
CFMutableDictionaryRef dnsConfig = CFDictionaryCreateMutable(
kCFAllocatorSystemDefault, 0, &kCFCopyStringDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
auto configGuard = qScopeGuard([&] { CFRelease(dnsConfig); });
QStringList list;
for (const QHostAddress& addr : resolvers) {
list.append(addr.toString());
}
cfDictSetStringList(dnsConfig, kSCPropNetDNSServerAddresses, list);
cfDictSetString(dnsConfig, kSCPropNetDNSDomainName, "lan");
// Backup each network service's DNS config, and replace it with ours.
for (CFIndex i = 0; i < CFArrayGetCount(netServices); i++) {
QString service = cfParseString(CFArrayGetValueAtIndex(netServices, i));
QString uuid = service.section('/', 3, 3);
if (uuid.isEmpty()) {
continue;
}
backupService(uuid);
logger.debug() << "Setting DNS config for" << uuid;
CFStringRef dnsPath = CFStringCreateWithFormat(
kCFAllocatorSystemDefault, nullptr,
CFSTR("Setup:/Network/Service/%s/DNS"), qPrintable(uuid));
if (!dnsPath) {
continue;
}
SCDynamicStoreSetValue(m_scStore, dnsPath, dnsConfig);
CFRelease(dnsPath);
}
return true;
}
bool DnsUtilsMacos::restoreResolvers() {
for (const QString& uuid : m_prevServices.keys()) {
CFStringRef path = CFStringCreateWithFormat(
kCFAllocatorSystemDefault, nullptr,
CFSTR("Setup:/Network/Service/%s/DNS"), qPrintable(uuid));
logger.debug() << "Restoring DNS config for" << uuid;
const DnsBackup& backup = m_prevServices[uuid];
if (backup.isValid()) {
CFMutableDictionaryRef config;
config = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0,
&kCFCopyStringDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
cfDictSetString(config, kSCPropNetDNSDomainName, backup.m_domain);
cfDictSetStringList(config, kSCPropNetDNSSearchDomains, backup.m_search);
cfDictSetStringList(config, kSCPropNetDNSServerAddresses,
backup.m_servers);
cfDictSetStringList(config, kSCPropNetDNSSortList, backup.m_sortlist);
SCDynamicStoreSetValue(m_scStore, path, config);
CFRelease(config);
} else {
SCDynamicStoreRemoveValue(m_scStore, path);
}
CFRelease(path);
}
m_prevServices.clear();
return true;
}
void DnsUtilsMacos::backupService(const QString& uuid) {
DnsBackup backup;
CFStringRef path = CFStringCreateWithFormat(
kCFAllocatorSystemDefault, nullptr,
CFSTR("Setup:/Network/Service/%s/DNS"), qPrintable(uuid));
CFDictionaryRef config =
(CFDictionaryRef)SCDynamicStoreCopyValue(m_scStore, path);
auto serviceGuard = qScopeGuard([&] {
if (config) {
CFRelease(config);
}
CFRelease(path);
});
// Parse the DNS protocol entry and save it for later.
if (config) {
CFTypeRef value;
value = CFDictionaryGetValue(config, kSCPropNetDNSDomainName);
if (value) {
backup.m_domain = cfParseString(value);
}
value = CFDictionaryGetValue(config, kSCPropNetDNSServerAddresses);
if (value) {
backup.m_servers = cfParseStringList(value);
}
value = CFDictionaryGetValue(config, kSCPropNetDNSSearchDomains);
if (value) {
backup.m_search = cfParseStringList(value);
}
value = CFDictionaryGetValue(config, kSCPropNetDNSSortList);
if (value) {
backup.m_sortlist = cfParseStringList(value);
}
}
m_prevServices[uuid] = backup;
}

View file

@ -0,0 +1,51 @@
/* 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 DNSUTILSMACOS_H
#define DNSUTILSMACOS_H
#include "dnsutils.h"
#include <QHostAddress>
#include <QMap>
#include <QString>
#include <systemconfiguration/scdynamicstore.h>
#include <systemconfiguration/systemconfiguration.h>
class DnsUtilsMacos final : public DnsUtils {
Q_OBJECT
Q_DISABLE_COPY_MOVE(DnsUtilsMacos)
public:
explicit DnsUtilsMacos(QObject* parent);
virtual ~DnsUtilsMacos();
bool updateResolvers(const QString& ifname,
const QList<QHostAddress>& resolvers) override;
bool restoreResolvers() override;
private:
void backupResolvers();
void backupService(const QString& uuid);
private:
class DnsBackup {
public:
DnsBackup() {}
bool isValid() const {
return !m_domain.isEmpty() || !m_search.isEmpty() ||
!m_servers.isEmpty() || !m_sortlist.isEmpty();
}
QString m_domain;
QStringList m_search;
QStringList m_servers;
QStringList m_sortlist;
};
SCDynamicStoreRef m_scStore = nullptr;
QMap<QString, DnsBackup> m_prevServices;
};
#endif // DNSUTILSMACOS_H

View file

@ -0,0 +1,180 @@
/* 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 "iputilsmacos.h"
#include "leakdetector.h"
#include "logger.h"
#include "macosdaemon.h"
#include "daemon/wireguardutils.h"
#include <QHostAddress>
#include <QScopeGuard>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/if_var.h>
#include <netinet/in.h>
#include <netinet/in_var.h>
#include <sys/ioctl.h>
#include <unistd.h>
constexpr uint32_t ETH_MTU = 1500;
constexpr uint32_t WG_MTU_OVERHEAD = 80;
namespace {
Logger logger(LOG_MACOS, "IPUtilsMacos");
}
IPUtilsMacos::IPUtilsMacos(QObject* parent) : IPUtils(parent) {
MVPN_COUNT_CTOR(IPUtilsMacos);
logger.debug() << "IPUtilsMacos created.";
}
IPUtilsMacos::~IPUtilsMacos() {
MVPN_COUNT_DTOR(IPUtilsMacos);
logger.debug() << "IPUtilsMacos destroyed.";
}
bool IPUtilsMacos::addInterfaceIPs(const InterfaceConfig& config) {
if (!addIP4AddressToDevice(config)) {
return false;
}
if (config.m_ipv6Enabled) {
if (!addIP6AddressToDevice(config)) {
return false;
}
}
return true;
}
bool IPUtilsMacos::setMTUAndUp(const InterfaceConfig& config) {
Q_UNUSED(config);
QString ifname = MacOSDaemon::instance()->m_wgutils->interfaceName();
struct ifreq ifr;
// Create socket file descriptor to perform the ioctl operations on
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sockfd < 0) {
logger.error() << "Failed to create ioctl socket.";
return false;
}
auto guard = qScopeGuard([&] { close(sockfd); });
// MTU
strncpy(ifr.ifr_name, qPrintable(ifname), IFNAMSIZ);
ifr.ifr_mtu = ETH_MTU - WG_MTU_OVERHEAD;
int ret = ioctl(sockfd, SIOCSIFMTU, &ifr);
if (ret) {
logger.error() << "Failed to set MTU:" << strerror(errno);
return false;
}
// Get the interface flags
strncpy(ifr.ifr_name, qPrintable(ifname), IFNAMSIZ);
ret = ioctl(sockfd, SIOCGIFFLAGS, &ifr);
if (ret) {
logger.error() << "Failed to get interface flags:" << strerror(errno);
return false;
}
// Up
ifr.ifr_flags |= (IFF_UP | IFF_RUNNING);
ret = ioctl(sockfd, SIOCSIFFLAGS, &ifr);
if (ret) {
logger.error() << "Failed to set device up:" << strerror(errno);
return false;
}
return true;
}
bool IPUtilsMacos::addIP4AddressToDevice(const InterfaceConfig& config) {
Q_UNUSED(config);
QString ifname = MacOSDaemon::instance()->m_wgutils->interfaceName();
struct ifaliasreq ifr;
struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifra_addr;
struct sockaddr_in* ifrMask = (struct sockaddr_in*)&ifr.ifra_mask;
struct sockaddr_in* ifrBcast = (struct sockaddr_in*)&ifr.ifra_broadaddr;
// Name the interface and set family
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifra_name, qPrintable(ifname), IFNAMSIZ);
// Get the device address to add to interface
QPair<QHostAddress, int> parsedAddr =
QHostAddress::parseSubnet(config.m_deviceIpv4Address);
QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit();
char* deviceAddr = _deviceAddr.data();
ifrAddr->sin_family = AF_INET;
ifrAddr->sin_len = sizeof(struct sockaddr_in);
inet_pton(AF_INET, deviceAddr, &ifrAddr->sin_addr);
// Set the netmask to /32
ifrMask->sin_family = AF_INET;
ifrMask->sin_len = sizeof(struct sockaddr_in);
memset(&ifrMask->sin_addr, 0xff, sizeof(ifrMask->sin_addr));
// Set the broadcast address.
ifrBcast->sin_family = AF_INET;
ifrBcast->sin_len = sizeof(struct sockaddr_in);
ifrBcast->sin_addr.s_addr =
(ifrAddr->sin_addr.s_addr | ~ifrMask->sin_addr.s_addr);
// Create an IPv4 socket to perform the ioctl operations on
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sockfd < 0) {
logger.error() << "Failed to create ioctl socket.";
return false;
}
auto guard = qScopeGuard([&] { close(sockfd); });
// Set ifr to interface
int ret = ioctl(sockfd, SIOCAIFADDR, &ifr);
if (ret) {
logger.error() << "Failed to set IPv4: " << deviceAddr
<< "error:" << strerror(errno);
return false;
}
return true;
}
bool IPUtilsMacos::addIP6AddressToDevice(const InterfaceConfig& config) {
Q_UNUSED(config);
QString ifname = MacOSDaemon::instance()->m_wgutils->interfaceName();
struct in6_aliasreq ifr6;
// Name the interface and set family
memset(&ifr6, 0, sizeof(ifr6));
strncpy(ifr6.ifra_name, qPrintable(ifname), IFNAMSIZ);
ifr6.ifra_addr.sin6_family = AF_INET6;
ifr6.ifra_addr.sin6_len = sizeof(ifr6.ifra_addr);
ifr6.ifra_lifetime.ia6t_vltime = ifr6.ifra_lifetime.ia6t_pltime = 0xffffffff;
ifr6.ifra_prefixmask.sin6_family = AF_INET6;
ifr6.ifra_prefixmask.sin6_len = sizeof(ifr6.ifra_prefixmask);
memset(&ifr6.ifra_prefixmask.sin6_addr, 0xff, sizeof(struct in6_addr));
// Get the device address to add to interface
QPair<QHostAddress, int> parsedAddr =
QHostAddress::parseSubnet(config.m_deviceIpv6Address);
QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit();
char* deviceAddr = _deviceAddr.data();
inet_pton(AF_INET6, deviceAddr, &ifr6.ifra_addr.sin6_addr);
// Create IPv4 socket to perform the ioctl operations on
int sockfd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_IP);
if (sockfd < 0) {
logger.error() << "Failed to create ioctl socket.";
return false;
}
auto guard = qScopeGuard([&] { close(sockfd); });
// Set ifr to interface
int ret = ioctl(sockfd, SIOCAIFADDR_IN6, &ifr6);
if (ret) {
logger.error() << "Failed to set IPv6: " << deviceAddr
<< "error:" << strerror(errno);
return false;
}
return true;
}

View file

@ -0,0 +1,28 @@
/* 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 IPUTILSMACOS_H
#define IPUTILSMACOS_H
#include "daemon/iputils.h"
#include <arpa/inet.h>
class IPUtilsMacos final : public IPUtils {
public:
IPUtilsMacos(QObject* parent);
~IPUtilsMacos();
bool addInterfaceIPs(const InterfaceConfig& config) override;
bool setMTUAndUp(const InterfaceConfig& config) override;
void setIfname(const QString& ifname) { m_ifname = ifname; }
private:
bool addIP4AddressToDevice(const InterfaceConfig& config);
bool addIP6AddressToDevice(const InterfaceConfig& config);
private:
QString m_ifname;
};
#endif // IPUTILSMACOS_H

View file

@ -0,0 +1,74 @@
/* 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 "macosdaemon.h"
#include "leakdetector.h"
#include "logger.h"
#include "wgquickprocess.h"
#include <QCoreApplication>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QLocalSocket>
#include <QProcess>
#include <QSettings>
#include <QTextStream>
#include <QtGlobal>
namespace {
Logger logger(LOG_MACOS, "MacOSDaemon");
MacOSDaemon* s_daemon = nullptr;
} // namespace
MacOSDaemon::MacOSDaemon() : Daemon(nullptr) {
MVPN_COUNT_CTOR(MacOSDaemon);
logger.debug() << "Daemon created";
m_wgutils = new WireguardUtilsMacos(this);
m_dnsutils = new DnsUtilsMacos(this);
m_iputils = new IPUtilsMacos(this);
Q_ASSERT(s_daemon == nullptr);
s_daemon = this;
}
MacOSDaemon::~MacOSDaemon() {
MVPN_COUNT_DTOR(MacOSDaemon);
logger.debug() << "Daemon released";
Q_ASSERT(s_daemon == this);
s_daemon = nullptr;
}
// static
MacOSDaemon* MacOSDaemon::instance() {
Q_ASSERT(s_daemon);
return s_daemon;
}
QByteArray MacOSDaemon::getStatus() {
logger.debug() << "Status request";
bool connected = m_connections.contains(0);
QJsonObject obj;
obj.insert("type", "status");
obj.insert("connected", connected);
if (connected) {
const ConnectionState& state = m_connections.value(0).m_config;
WireguardUtils::peerStatus status =
m_wgutils->getPeerStatus(state.m_config.m_serverPublicKey);
obj.insert("serverIpv4Gateway", state.m_config.m_serverIpv4Gateway);
obj.insert("deviceIpv4Address", state.m_config.m_deviceIpv4Address);
obj.insert("date", state.m_date.toString());
obj.insert("txBytes", QJsonValue(status.txBytes));
obj.insert("rxBytes", QJsonValue(status.rxBytes));
}
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
}

View file

@ -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 MACOSDAEMON_H
#define MACOSDAEMON_H
#include "daemon.h"
#include "dnsutilsmacos.h"
#include "iputilsmacos.h"
#include "wireguardutilsmacos.h"
class MacOSDaemon final : public Daemon {
friend class IPUtilsMacos;
public:
MacOSDaemon();
~MacOSDaemon();
static MacOSDaemon* instance();
QByteArray getStatus() override;
protected:
WireguardUtils* wgutils() const override { return m_wgutils; }
bool supportDnsUtils() const override { return true; }
DnsUtils* dnsutils() override { return m_dnsutils; }
bool supportIPUtils() const override { return true; }
IPUtils* iputils() override { return m_iputils; }
private:
WireguardUtilsMacos* m_wgutils = nullptr;
DnsUtilsMacos* m_dnsutils = nullptr;
IPUtilsMacos* m_iputils = nullptr;
};
#endif // MACOSDAEMON_H

View file

@ -0,0 +1,60 @@
/* 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 "macosdaemonserver.h"
#include "commandlineparser.h"
#include "daemon/daemonlocalserver.h"
#include "leakdetector.h"
#include "logger.h"
#include "macosdaemon.h"
#include "mozillavpn.h"
#include "signalhandler.h"
#include <QCoreApplication>
namespace {
Logger logger(LOG_MACOS, "MacOSDaemonServer");
}
MacOSDaemonServer::MacOSDaemonServer(QObject* parent)
: Command(parent, "macosdaemon", "Activate the macos daemon") {
MVPN_COUNT_CTOR(MacOSDaemonServer);
}
MacOSDaemonServer::~MacOSDaemonServer() { MVPN_COUNT_DTOR(MacOSDaemonServer); }
int MacOSDaemonServer::run(QStringList& tokens) {
Q_ASSERT(!tokens.isEmpty());
QString appName = tokens[0];
QCoreApplication app(CommandLineParser::argc(), CommandLineParser::argv());
QCoreApplication::setApplicationName("Mozilla VPN Daemon");
QCoreApplication::setApplicationVersion(APP_VERSION);
if (tokens.length() > 1) {
QList<CommandLineParser::Option*> options;
return CommandLineParser::unknownOption(this, appName, tokens[1], options,
false);
}
MacOSDaemon daemon;
DaemonLocalServer server(qApp);
if (!server.initialize()) {
logger.error() << "Failed to initialize the server";
return 1;
}
// Signal handling for a proper shutdown.
SignalHandler sh;
QObject::connect(&sh, &SignalHandler::quitRequested,
[]() { MacOSDaemon::instance()->deactivate(); });
QObject::connect(&sh, &SignalHandler::quitRequested, &app,
&QCoreApplication::quit, Qt::QueuedConnection);
return app.exec();
}
static Command::RegistrationProxy<MacOSDaemonServer> s_commandMacOSDaemon;

View file

@ -0,0 +1,18 @@
/* 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 MACOSDAEMONSERVER_H
#define MACOSDAEMONSERVER_H
#include "command.h"
class MacOSDaemonServer final : public Command {
public:
explicit MacOSDaemonServer(QObject* parent);
~MacOSDaemonServer();
int run(QStringList& tokens) override;
};
#endif // MACOSDAEMONSERVER_H

View file

@ -0,0 +1,354 @@
/* 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 "macosroutemonitor.h"
#include "leakdetector.h"
#include "logger.h"
#include <QCoreApplication>
#include <QHostAddress>
#include <QScopeGuard>
#include <QTimer>
#include <QProcess>
#include <arpa/inet.h>
#include <errno.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <net/if_types.h>
#include <net/route.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
namespace {
Logger logger(LOG_MACOS, "MacosRouteMonitor");
template <typename T>
static T* sockaddr_cast(QByteArray& data) {
const struct sockaddr* sa = (const struct sockaddr*)data.constData();
Q_ASSERT(sa->sa_len <= data.length());
if (data.length() >= (int)sizeof(T)) {
return (T*)data.data();
}
return nullptr;
}
} // namespace
MacosRouteMonitor::MacosRouteMonitor(const QString& ifname, QObject* parent)
: QObject(parent), m_ifname(ifname) {
MVPN_COUNT_CTOR(MacosRouteMonitor);
logger.debug() << "MacosRouteMonitor created.";
m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0);
if (m_rtsock < 0) {
logger.error() << "Failed to create routing socket:" << strerror(errno);
return;
}
// Disable replies to our own messages.
int off = 0;
setsockopt(m_rtsock, SOL_SOCKET, SO_USELOOPBACK, &off, sizeof(off));
m_notifier = new QSocketNotifier(m_rtsock, QSocketNotifier::Read, this);
connect(m_notifier, &QSocketNotifier::activated, this,
&MacosRouteMonitor::rtsockReady);
}
MacosRouteMonitor::~MacosRouteMonitor() {
MVPN_COUNT_DTOR(MacosRouteMonitor);
if (m_rtsock >= 0) {
close(m_rtsock);
}
logger.debug() << "MacosRouteMonitor destroyed.";
}
void MacosRouteMonitor::handleRtmAdd(const struct rt_msghdr* rtm,
const QByteArray& payload) {
QList<QByteArray> addrlist = parseAddrList(payload);
// Ignore routing changes on the tunnel interfaces
if ((rtm->rtm_addrs & RTA_DST) && (rtm->rtm_addrs & RTA_GATEWAY)) {
if (m_ifname == addrToString(addrlist[1])) {
return;
}
}
QStringList list;
for (auto addr : addrlist) {
list.append(addrToString(addr));
}
logger.debug() << "Route added by" << rtm->rtm_pid
<< QString("addrs(%1):").arg(rtm->rtm_addrs) << list.join(" ");
}
void MacosRouteMonitor::handleRtmDelete(const struct rt_msghdr* rtm,
const QByteArray& payload) {
QList<QByteArray> addrlist = parseAddrList(payload);
// Ignore routing changes on the tunnel interfaces
if ((rtm->rtm_addrs & RTA_DST) && (rtm->rtm_addrs & RTA_GATEWAY)) {
if (m_ifname == addrToString(addrlist[1])) {
return;
}
}
QStringList list;
for (auto addr : addrlist) {
list.append(addrToString(addr));
}
logger.debug() << "Route deleted by" << rtm->rtm_pid
<< QString("addrs(%1):").arg(rtm->rtm_addrs) << list.join(" ");
}
void MacosRouteMonitor::handleRtmChange(const struct rt_msghdr* rtm,
const QByteArray& payload) {
QList<QByteArray> addrlist = parseAddrList(payload);
// Ignore routing changes on the tunnel interfaces
if ((rtm->rtm_addrs & RTA_DST) && (rtm->rtm_addrs & RTA_GATEWAY)) {
if (m_ifname == addrToString(addrlist[1])) {
return;
}
}
QStringList list;
for (auto addr : addrlist) {
list.append(addrToString(addr));
}
logger.debug() << "Route chagned by" << rtm->rtm_pid
<< QString("addrs(%1):").arg(rtm->rtm_addrs) << list.join(" ");
}
void MacosRouteMonitor::handleIfaceInfo(const struct if_msghdr* ifm,
const QByteArray& payload) {
QList<QByteArray> addrlist = parseAddrList(payload);
if (ifm->ifm_index != if_nametoindex(qPrintable(m_ifname))) {
return;
}
m_ifflags = ifm->ifm_flags;
QStringList list;
for (auto addr : addrlist) {
list.append(addrToString(addr));
}
logger.debug() << "Interface " << ifm->ifm_index
<< "chagned flags:" << ifm->ifm_flags
<< QString("addrs(%1):").arg(ifm->ifm_addrs) << list.join(" ");
}
void MacosRouteMonitor::rtsockReady() {
char buf[1024];
ssize_t len = recv(m_rtsock, buf, sizeof(buf), MSG_DONTWAIT);
if (len <= 0) {
return;
}
#ifndef RTMSG_NEXT
# define RTMSG_NEXT(_rtm_) \
(struct rt_msghdr*)((char*)(_rtm_) + (_rtm_)->rtm_msglen)
#endif
struct rt_msghdr* rtm = (struct rt_msghdr*)buf;
struct rt_msghdr* end = (struct rt_msghdr*)(&buf[len]);
while (rtm < end) {
// Ensure the message fits within the buffer
if (RTMSG_NEXT(rtm) > end) {
break;
}
// Handle the routing message.
QByteArray message((char*)rtm, rtm->rtm_msglen);
switch (rtm->rtm_type) {
case RTM_ADD:
message.remove(0, sizeof(struct rt_msghdr));
handleRtmAdd(rtm, message);
break;
case RTM_DELETE:
message.remove(0, sizeof(struct rt_msghdr));
handleRtmDelete(rtm, message);
break;
case RTM_CHANGE:
message.remove(0, sizeof(struct rt_msghdr));
handleRtmChange(rtm, message);
break;
case RTM_IFINFO:
message.remove(0, sizeof(struct if_msghdr));
handleIfaceInfo((struct if_msghdr*)rtm, message);
break;
default:
logger.debug() << "Unknown routing message:" << rtm->rtm_type;
break;
}
rtm = RTMSG_NEXT(rtm);
}
}
void MacosRouteMonitor::rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen,
int rtaddr, const void* sa) {
size_t sa_len = ((struct sockaddr*)sa)->sa_len;
Q_ASSERT((rtm->rtm_addrs & rtaddr) == 0);
if ((rtm->rtm_msglen + sa_len) > maxlen) {
return;
}
memcpy((char*)rtm + rtm->rtm_msglen, sa, sa_len);
rtm->rtm_addrs |= rtaddr;
rtm->rtm_msglen += sa_len;
if (rtm->rtm_msglen % sizeof(uint32_t)) {
rtm->rtm_msglen += sizeof(uint32_t) - (rtm->rtm_msglen % sizeof(uint32_t));
}
}
bool MacosRouteMonitor::rtmSendRoute(int action, const IPAddressRange& prefix) {
constexpr size_t rtm_max_size = sizeof(struct rt_msghdr) +
sizeof(struct sockaddr_in6) * 2 +
sizeof(struct sockaddr_dl);
char buf[rtm_max_size];
struct rt_msghdr* rtm = (struct rt_msghdr*)buf;
memset(rtm, 0, rtm_max_size);
rtm->rtm_msglen = sizeof(struct rt_msghdr);
rtm->rtm_version = RTM_VERSION;
rtm->rtm_type = action;
rtm->rtm_index = if_nametoindex(qPrintable(m_ifname));
rtm->rtm_flags = RTF_STATIC | RTF_UP;
rtm->rtm_addrs = 0;
rtm->rtm_pid = 0;
rtm->rtm_seq = m_rtseq++;
rtm->rtm_errno = 0;
rtm->rtm_inits = 0;
memset(&rtm->rtm_rmx, 0, sizeof(rtm->rtm_rmx));
// Append RTA_DST
if (prefix.type() == IPAddressRange::IPv6) {
struct sockaddr_in6 sin6;
Q_IPV6ADDR dst = QHostAddress(prefix.ipAddress()).toIPv6Address();
memset(&sin6, 0, sizeof(sin6));
sin6.sin6_family = AF_INET6;
sin6.sin6_len = sizeof(sin6);
memcpy(&sin6.sin6_addr, &dst, 16);
rtmAppendAddr(rtm, rtm_max_size, RTA_DST, &sin6);
} else {
struct sockaddr_in sin;
quint32 dst = QHostAddress(prefix.ipAddress()).toIPv4Address();
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_len = sizeof(sin);
sin.sin_addr.s_addr = htonl(dst);
rtmAppendAddr(rtm, rtm_max_size, RTA_DST, &sin);
}
// Append RTA_GATEWAY
if (action != RTM_DELETE) {
struct sockaddr_dl sdl;
memset(&sdl, 0, sizeof(sdl));
sdl.sdl_family = AF_LINK;
sdl.sdl_len = offsetof(struct sockaddr_dl, sdl_data) + m_ifname.length();
sdl.sdl_index = rtm->rtm_index;
sdl.sdl_type = IFT_OTHER;
sdl.sdl_nlen = m_ifname.length();
sdl.sdl_alen = 0;
sdl.sdl_slen = 0;
memcpy(&sdl.sdl_data, qPrintable(m_ifname), sdl.sdl_nlen);
rtmAppendAddr(rtm, rtm_max_size, RTA_GATEWAY, &sdl);
}
// Append RTA_NETMASK
int plen = prefix.range();
if (prefix.type() == IPAddressRange::IPv6) {
struct sockaddr_in6 sin6;
memset(&sin6, 0, sizeof(sin6));
sin6.sin6_family = AF_INET6;
sin6.sin6_len = sizeof(sin6);
memset(&sin6.sin6_addr.s6_addr, 0xff, plen / 8);
if (plen % 8) {
sin6.sin6_addr.s6_addr[plen / 8] = 0xFF ^ (0xFF >> (plen % 8));
}
rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &sin6);
} else if (prefix.type() == IPAddressRange::IPv4) {
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_len = sizeof(struct sockaddr_in);
sin.sin_addr.s_addr = 0xffffffff;
if (plen < 32) {
sin.sin_addr.s_addr ^= htonl(0xffffffff >> plen);
}
rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &sin);
}
// Send the routing message to the kernel.
int len = write(m_rtsock, rtm, rtm->rtm_msglen);
if (len == rtm->rtm_msglen) {
return true;
}
if ((action == RTM_ADD) && (errno == EEXIST)) {
return true;
}
if ((action == RTM_DELETE) && (errno == ESRCH)) {
return true;
}
logger.warning() << "Failed to send routing message:" << strerror(errno);
return false;
}
bool MacosRouteMonitor::insertRoute(const IPAddressRange& prefix) {
return rtmSendRoute(RTM_ADD, prefix);
}
bool MacosRouteMonitor::deleteRoute(const IPAddressRange& prefix) {
return rtmSendRoute(RTM_DELETE, prefix);
}
// static
QList<QByteArray> MacosRouteMonitor::parseAddrList(const QByteArray& payload) {
QList<QByteArray> list;
int offset = 0;
constexpr int minlen = offsetof(struct sockaddr, sa_len) + sizeof(u_short);
while ((offset + minlen) <= payload.length()) {
struct sockaddr* sa = (struct sockaddr*)(payload.constData() + offset);
int paddedSize = sa->sa_len;
if (!paddedSize || (paddedSize % sizeof(uint32_t))) {
paddedSize += sizeof(uint32_t) - (paddedSize % sizeof(uint32_t));
}
if ((offset + paddedSize) > payload.length()) {
break;
}
list.append(payload.mid(offset, paddedSize));
offset += paddedSize;
}
return list;
}
// static
QString MacosRouteMonitor::addrToString(const struct sockaddr* sa) {
if (sa->sa_family == AF_INET) {
const struct sockaddr_in* sin = (const struct sockaddr_in*)sa;
return QString(inet_ntoa(sin->sin_addr));
}
if (sa->sa_family == AF_INET6) {
const struct sockaddr_in6* sin6 = (const struct sockaddr_in6*)sa;
char buf[INET6_ADDRSTRLEN];
return QString(inet_ntop(AF_INET6, &sin6->sin6_addr, buf, sizeof(buf)));
}
if (sa->sa_family == AF_LINK) {
const struct sockaddr_dl* sdl = (const struct sockaddr_dl*)sa;
return QString(link_ntoa(sdl));
}
return QString("unknown(af=%1)").arg(sa->sa_family);
}
// static
QString MacosRouteMonitor::addrToString(const QByteArray& data) {
const struct sockaddr* sa = (const struct sockaddr*)data.constData();
Q_ASSERT(sa->sa_len <= data.length());
return addrToString(sa);
}

View file

@ -0,0 +1,54 @@
/* 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 MACOSROUTEMONITOR_H
#define MACOSROUTEMONITOR_H
#include "ipaddressrange.h"
#include <QByteArray>
#include <QList>
#include <QObject>
#include <QSocketNotifier>
struct if_msghdr;
struct rt_msghdr;
struct sockaddr;
class MacosRouteMonitor final : public QObject {
Q_OBJECT
public:
MacosRouteMonitor(const QString& ifname, QObject* parent = nullptr);
~MacosRouteMonitor();
bool insertRoute(const IPAddressRange& prefix);
bool deleteRoute(const IPAddressRange& prefix);
int interfaceFlags() { return m_ifflags; }
private:
void handleRtmAdd(const struct rt_msghdr* msg, const QByteArray& payload);
void handleRtmDelete(const struct rt_msghdr* msg, const QByteArray& payload);
void handleRtmChange(const struct rt_msghdr* msg, const QByteArray& payload);
void handleIfaceInfo(const struct if_msghdr* msg, const QByteArray& payload);
bool rtmSendRoute(int action, const IPAddressRange& prefix);
static void rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, int rtaddr,
const void* sa);
static QList<QByteArray> parseAddrList(const QByteArray& data);
private slots:
void rtsockReady();
private:
static QString addrToString(const struct sockaddr* sa);
static QString addrToString(const QByteArray& data);
QString m_ifname;
int m_ifflags = 0;
int m_rtsock = -1;
int m_rtseq = 0;
QSocketNotifier* m_notifier = nullptr;
};
#endif // MACOSROUTEMONITOR_H

View file

@ -0,0 +1,309 @@
/* 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 <QByteArray>
#include <QDir>
#include <QFile>
#include <QLocalSocket>
#include <QTimer>
#include <errno.h>
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();
}

View file

@ -0,0 +1,52 @@
/* 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 WIREGUARDUTILSMACOS_H
#define WIREGUARDUTILSMACOS_H
#include "daemon/wireguardutils.h"
#include "macosroutemonitor.h"
#include <QObject>
#include <QProcess>
class WireguardUtilsMacos final : public WireguardUtils {
Q_OBJECT
public:
WireguardUtilsMacos(QObject* parent);
~WireguardUtilsMacos();
bool interfaceExists() override {
return m_tunnel.state() == QProcess::Running;
}
QString interfaceName() override { return m_ifname; }
bool addInterface(const InterfaceConfig& config) override;
bool deleteInterface() override;
bool updatePeer(const InterfaceConfig& config) override;
bool deletePeer(const QString& pubkey) override;
peerStatus getPeerStatus(const QString& pubkey) override;
bool updateRoutePrefix(const IPAddressRange& prefix, int hopindex) override;
bool deleteRoutePrefix(const IPAddressRange& prefix, int hopindex) override;
signals:
void backendFailure();
private slots:
void tunnelStdoutReady();
void tunnelErrorOccurred(QProcess::ProcessError error);
private:
QString uapiCommand(const QString& command);
static int uapiErrno(const QString& command);
QString waitForTunnelName(const QString& filename);
QString m_ifname;
QProcess m_tunnel;
MacosRouteMonitor* m_rtmonitor = nullptr;
};
#endif // WIREGUARDUTILSMACOS_H

View file

@ -0,0 +1,136 @@
/* 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 "cryptosettings.h"
#include "logger.h"
#include <QRandomGenerator>
constexpr const NSString* SERVICE = @"Mozilla VPN";
#import <Foundation/Foundation.h>
namespace {
Logger logger({LOG_MACOS, LOG_MAIN}, "MacOSCryptoSettings");
bool initialized = false;
QByteArray key;
} // anonymous
// static
void CryptoSettings::resetKey() {
logger.debug() << "Reset the key in the keychain";
NSData* service = [SERVICE dataUsingEncoding:NSUTF8StringEncoding];
NSString* appId = [[NSBundle mainBundle] bundleIdentifier];
NSMutableDictionary* query = [[NSMutableDictionary alloc] init];
[query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[query setObject:service forKey:(id)kSecAttrGeneric];
[query setObject:service forKey:(id)kSecAttrAccount];
[query setObject:appId forKey:(id)kSecAttrService];
SecItemDelete((CFDictionaryRef)query);
[query release];
initialized = false;
}
// static
bool CryptoSettings::getKey(uint8_t output[CRYPTO_SETTINGS_KEY_SIZE]) {
#if defined(MVPN_IOS) || defined(MVPN_MACOS_NETWORKEXTENSION) || defined(MVPN_MACOS_DAEMON)
if (!initialized) {
initialized = true;
logger.debug() << "Retrieving the key from the keychain";
NSData* service = [SERVICE dataUsingEncoding:NSUTF8StringEncoding];
NSString* appId = [[NSBundle mainBundle] bundleIdentifier];
NSMutableDictionary* query = [[NSMutableDictionary alloc] init];
[query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[query setObject:service forKey:(id)kSecAttrGeneric];
[query setObject:service forKey:(id)kSecAttrAccount];
[query setObject:appId forKey:(id)kSecAttrService];
[query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[query setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
NSData* keyData = NULL;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef*)(void*)&keyData);
[query release];
if (status == noErr) {
key = QByteArray::fromNSData(keyData);
logger.debug() << "Key found with length:" << key.length();
if (key.length() == CRYPTO_SETTINGS_KEY_SIZE) {
memcpy(output, key.data(), CRYPTO_SETTINGS_KEY_SIZE);
return true;
}
}
logger.warning() << "Key not found. Let's create it. Error:" << status;
key = QByteArray(CRYPTO_SETTINGS_KEY_SIZE, 0x00);
QRandomGenerator* rg = QRandomGenerator::system();
for (int i = 0; i < CRYPTO_SETTINGS_KEY_SIZE; ++i) {
key[i] = rg->generate() & 0xFF;
}
query = [[NSMutableDictionary alloc] init];
[query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[query setObject:service forKey:(id)kSecAttrGeneric];
[query setObject:service forKey:(id)kSecAttrAccount];
[query setObject:appId forKey:(id)kSecAttrService];
SecItemDelete((CFDictionaryRef)query);
keyData = key.toNSData();
[query setObject:keyData forKey:(id)kSecValueData];
status = SecItemAdd((CFDictionaryRef)query, NULL);
if (status != noErr) {
logger.error() << "Failed to store the key. Error:" << status;
key = QByteArray();
}
[query release];
}
if (key.length() == CRYPTO_SETTINGS_KEY_SIZE) {
memcpy(output, key.data(), CRYPTO_SETTINGS_KEY_SIZE);
return true;
}
logger.error() << "Invalid key";
#else
Q_UNUSED(output);
#endif
return false;
}
// static
CryptoSettings::Version CryptoSettings::getSupportedVersion() {
logger.debug() << "Get supported settings method";
#if defined(MVPN_IOS) || defined(MVPN_MACOS_NETWORKEXTENSION) || defined(MVPN_MACOS_DAEMON)
uint8_t key[CRYPTO_SETTINGS_KEY_SIZE];
if (getKey(key)) {
logger.debug() << "Encryption supported!";
return CryptoSettings::EncryptionChachaPolyV1;
}
#endif
logger.debug() << "No encryption";
return CryptoSettings::NoEncryption;
}

View file

@ -0,0 +1,106 @@
/* 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 "macosmenubar.h"
#include "leakdetector.h"
#include "logger.h"
#include "mozillavpn.h"
#include "qmlengineholder.h"
#ifdef MVPN_MACOS
# include "platforms/macos/macosutils.h"
#endif
#include <QAction>
#include <QMenu>
#include <QMenuBar>
namespace {
Logger logger(LOG_MACOS, "MacOSManuBar");
MacOSMenuBar* s_instance = nullptr;
} // namespace
MacOSMenuBar::MacOSMenuBar() {
MVPN_COUNT_CTOR(MacOSMenuBar);
Q_ASSERT(!s_instance);
s_instance = this;
}
MacOSMenuBar::~MacOSMenuBar() {
MVPN_COUNT_DTOR(MacOSMenuBar);
Q_ASSERT(s_instance == this);
s_instance = nullptr;
}
// static
MacOSMenuBar* MacOSMenuBar::instance() {
Q_ASSERT(s_instance);
return s_instance;
}
void MacOSMenuBar::initialize() {
logger.debug() << "Creating menubar";
AmneziaVPN* vpn = AmneziaVPN::instance();
m_menuBar = new QMenuBar(nullptr);
//% "File"
QMenu* fileMenu = m_menuBar->addMenu(qtTrId("menubar.file.title"));
// Do not use qtTrId here!
QAction* quit =
fileMenu->addAction("quit", vpn->controller(), &Controller::quit);
quit->setMenuRole(QAction::QuitRole);
// Do not use qtTrId here!
m_aboutAction =
fileMenu->addAction("about.vpn", vpn, &AmneziaVPN::requestAbout);
m_aboutAction->setMenuRole(QAction::AboutRole);
m_aboutAction->setVisible(vpn->state() == AmneziaVPN::StateMain);
// Do not use qtTrId here!
m_preferencesAction =
fileMenu->addAction("preferences", vpn, &AmneziaVPN::requestSettings);
m_preferencesAction->setMenuRole(QAction::PreferencesRole);
m_preferencesAction->setVisible(vpn->state() == AmneziaVPN::StateMain);
m_closeAction = fileMenu->addAction("close", []() {
QmlEngineHolder::instance()->hideWindow();
#ifdef MVPN_MACOS
MacOSUtils::hideDockIcon();
#endif
});
m_closeAction->setShortcut(QKeySequence::Close);
m_helpMenu = m_menuBar->addMenu("");
retranslate();
};
void MacOSMenuBar::controllerStateChanged() {
AmneziaVPN* vpn = AmneziaVPN::instance();
m_preferencesAction->setVisible(vpn->state() == AmneziaVPN::StateMain);
m_aboutAction->setVisible(vpn->state() == AmneziaVPN::StateMain);
}
void MacOSMenuBar::retranslate() {
logger.debug() << "Retranslate";
//% "Close"
m_closeAction->setText(qtTrId("menubar.file.close"));
//% "Help"
m_helpMenu->setTitle(qtTrId("menubar.help.title"));
for (QAction* action : m_helpMenu->actions()) {
m_helpMenu->removeAction(action);
}
AmneziaVPN* vpn = AmneziaVPN::instance();
vpn->helpModel()->forEach([&](const char* nameId, int id) {
m_helpMenu->addAction(qtTrId(nameId),
[help = vpn->helpModel(), id]() { help->open(id); });
});
}

View file

@ -0,0 +1,42 @@
/* 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 MACOSMENUBAR_H
#define MACOSMENUBAR_H
#include <QObject>
class QAction;
class QMenu;
class QMenuBar;
class MacOSMenuBar final : public QObject {
Q_OBJECT
Q_DISABLE_COPY_MOVE(MacOSMenuBar)
public:
MacOSMenuBar();
~MacOSMenuBar();
static MacOSMenuBar* instance();
void initialize();
void retranslate();
QMenuBar* menuBar() const { return m_menuBar; }
public slots:
void controllerStateChanged();
private:
QMenuBar* m_menuBar = nullptr;
QAction* m_aboutAction = nullptr;
QAction* m_preferencesAction = nullptr;
QAction* m_closeAction = nullptr;
QMenu* m_helpMenu = nullptr;
};
#endif // MACOSMENUBAR_H

View file

@ -0,0 +1,25 @@
/* 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 MACOSNETWORKWATCHER_H
#define MACOSNETWORKWATCHER_H
#include "networkwatcherimpl.h"
class MacOSNetworkWatcher final : public NetworkWatcherImpl {
public:
MacOSNetworkWatcher(QObject* parent);
~MacOSNetworkWatcher();
void initialize() override;
void start() override;
void checkInterface();
private:
void* m_delegate = nullptr;
};
#endif // MACOSNETWORKWATCHER_H

View file

@ -0,0 +1,131 @@
/* 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 "macosnetworkwatcher.h"
#include "leakdetector.h"
#include "logger.h"
#import <CoreWLAN/CoreWLAN.h>
namespace {
Logger logger(LOG_MACOS, "MacOSNetworkWatcher");
}
@interface MacOSNetworkWatcherDelegate : NSObject <CWEventDelegate> {
MacOSNetworkWatcher* m_watcher;
}
@end
@implementation MacOSNetworkWatcherDelegate
- (id)initWithObject:(MacOSNetworkWatcher*)watcher {
self = [super init];
if (self) {
m_watcher = watcher;
}
return self;
}
- (void)bssidDidChangeForWiFiInterfaceWithName:(NSString*)interfaceName {
logger.debug() << "BSSID changed!" << QString::fromNSString(interfaceName);
if (m_watcher) {
m_watcher->checkInterface();
}
}
@end
MacOSNetworkWatcher::MacOSNetworkWatcher(QObject* parent) : NetworkWatcherImpl(parent) {
MVPN_COUNT_CTOR(MacOSNetworkWatcher);
}
MacOSNetworkWatcher::~MacOSNetworkWatcher() {
MVPN_COUNT_DTOR(MacOSNetworkWatcher);
if (m_delegate) {
CWWiFiClient* client = CWWiFiClient.sharedWiFiClient;
if (!client) {
logger.debug() << "Unable to retrieve the CWWiFiClient shared instance";
return;
}
[client stopMonitoringAllEventsAndReturnError:nullptr];
[static_cast<MacOSNetworkWatcherDelegate*>(m_delegate) dealloc];
m_delegate = nullptr;
}
}
void MacOSNetworkWatcher::initialize() {
// Nothing to do here
}
void MacOSNetworkWatcher::start() {
NetworkWatcherImpl::start();
checkInterface();
if (m_delegate) {
logger.debug() << "Delegate already registered";
return;
}
CWWiFiClient* client = CWWiFiClient.sharedWiFiClient;
if (!client) {
logger.error() << "Unable to retrieve the CWWiFiClient shared instance";
return;
}
logger.debug() << "Registering delegate";
m_delegate = [[MacOSNetworkWatcherDelegate alloc] initWithObject:this];
[client setDelegate:static_cast<MacOSNetworkWatcherDelegate*>(m_delegate)];
[client startMonitoringEventWithType:CWEventTypeBSSIDDidChange error:nullptr];
}
void MacOSNetworkWatcher::checkInterface() {
logger.debug() << "Checking interface";
if (!isActive()) {
logger.debug() << "Feature disabled";
return;
}
CWWiFiClient* client = CWWiFiClient.sharedWiFiClient;
if (!client) {
logger.debug() << "Unable to retrieve the CWWiFiClient shared instance";
return;
}
CWInterface* interface = [client interface];
if (!interface) {
logger.debug() << "No default wifi interface";
return;
}
if (![interface powerOn]) {
logger.debug() << "The interface is off";
return;
}
NSString* ssidNS = [interface ssid];
if (!ssidNS) {
logger.debug() << "WiFi is not in used";
return;
}
QString ssid = QString::fromNSString(ssidNS);
if (ssid.isEmpty()) {
logger.debug() << "WiFi doesn't have a valid SSID";
return;
}
CWSecurity security = [interface security];
if (security == kCWSecurityNone || security == kCWSecurityWEP) {
logger.debug() << "Unsecured network found!";
emit unsecuredNetwork(ssid, ssid);
return;
}
logger.debug() << "Secure WiFi interface";
}

View file

@ -0,0 +1,122 @@
/* 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 "macospingsender.h"
#include "leakdetector.h"
#include "logger.h"
#include <QSocketNotifier>
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <unistd.h>
namespace {
Logger logger({LOG_MACOS, LOG_NETWORKING}, "MacOSPingSender");
int identifier() { return (getpid() & 0xFFFF); }
}; // namespace
MacOSPingSender::MacOSPingSender(const QString& source, QObject* parent)
: PingSender(parent) {
MVPN_COUNT_CTOR(MacOSPingSender);
if (getuid()) {
m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
} else {
m_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
}
if (m_socket < 0) {
logger.error() << "Socket creation failed";
return;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_len = sizeof(addr);
if (inet_aton(source.toLocal8Bit().constData(), &addr.sin_addr) == 0) {
logger.error() << "source address error";
return;
}
if (bind(m_socket, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
logger.error() << "bind error:" << strerror(errno);
return;
}
m_notifier = new QSocketNotifier(m_socket, QSocketNotifier::Read, this);
connect(m_notifier, &QSocketNotifier::activated, this,
&MacOSPingSender::socketReady);
}
MacOSPingSender::~MacOSPingSender() {
MVPN_COUNT_DTOR(MacOSPingSender);
if (m_socket >= 0) {
close(m_socket);
}
}
void MacOSPingSender::sendPing(const QString& dest, quint16 sequence) {
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_len = sizeof(addr);
if (inet_aton(dest.toLocal8Bit().constData(), &addr.sin_addr) == 0) {
logger.error() << "DNS lookup failed";
return;
}
struct icmp packet;
bzero(&packet, sizeof packet);
packet.icmp_type = ICMP_ECHO;
packet.icmp_id = identifier();
packet.icmp_seq = htons(sequence);
packet.icmp_cksum = inetChecksum(&packet, sizeof(packet));
if (sendto(m_socket, (char*)&packet, sizeof(packet), 0,
(struct sockaddr*)&addr, sizeof(addr)) != sizeof(packet)) {
logger.error() << "ping sending failed:" << strerror(errno);
return;
}
}
void MacOSPingSender::socketReady() {
struct msghdr msg;
bzero(&msg, sizeof(msg));
struct sockaddr_in addr;
msg.msg_name = (caddr_t)&addr;
msg.msg_namelen = sizeof(addr);
struct iovec iov;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
u_char packet[IP_MAXPACKET];
iov.iov_base = packet;
iov.iov_len = IP_MAXPACKET;
ssize_t rc = recvmsg(m_socket, &msg, MSG_DONTWAIT);
if (rc <= 0) {
logger.error() << "Recvmsg failed";
return;
}
struct ip* ip = (struct ip*)packet;
int hlen = ip->ip_hl << 2;
struct icmp* icmp = (struct icmp*)(((char*)packet) + hlen);
if (icmp->icmp_type == ICMP_ECHOREPLY && icmp->icmp_id == identifier()) {
emit recvPing(htons(icmp->icmp_seq));
}
}

View file

@ -0,0 +1,30 @@
/* 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 MACOSPINGSENDER_H
#define MACOSPINGSENDER_H
#include "pingsender.h"
class QSocketNotifier;
class MacOSPingSender final : public PingSender {
Q_OBJECT
Q_DISABLE_COPY_MOVE(MacOSPingSender)
public:
MacOSPingSender(const QString& source, QObject* parent = nullptr);
~MacOSPingSender();
void sendPing(const QString& dest, quint16 sequence) override;
private slots:
void socketReady();
private:
QSocketNotifier* m_notifier = nullptr;
int m_socket = -1;
};
#endif // MACOSPINGSENDER_H

View file

@ -0,0 +1,28 @@
/* 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 "macosstartatbootwatcher.h"
#include "leakdetector.h"
#include "logger.h"
#include "macosutils.h"
namespace {
Logger logger(LOG_MACOS, "MacOSStartAtBootWatcher");
}
MacOSStartAtBootWatcher::MacOSStartAtBootWatcher(bool startAtBoot) {
MVPN_COUNT_CTOR(MacOSStartAtBootWatcher);
logger.debug() << "StartAtBoot watcher";
MacOSUtils::enableLoginItem(startAtBoot);
}
MacOSStartAtBootWatcher::~MacOSStartAtBootWatcher() {
MVPN_COUNT_DTOR(MacOSStartAtBootWatcher);
}
void MacOSStartAtBootWatcher::startAtBootChanged(bool startAtBoot) {
logger.debug() << "StartAtBoot changed:" << startAtBoot;
MacOSUtils::enableLoginItem(startAtBoot);
}

View file

@ -0,0 +1,22 @@
/* 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 MACOSSTARTATBOOTWATCHER_H
#define MACOSSTARTATBOOTWATCHER_H
#include <QObject>
class MacOSStartAtBootWatcher final : public QObject {
Q_OBJECT
Q_DISABLE_COPY_MOVE(MacOSStartAtBootWatcher)
public:
explicit MacOSStartAtBootWatcher(bool startAtBoot);
~MacOSStartAtBootWatcher();
public slots:
void startAtBootChanged(bool value);
};
#endif // MACOSSTARTATBOOTWATCHER_H

View file

@ -0,0 +1,23 @@
/* 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 MACOSUTILS_H
#define MACOSUTILS_H
#include <QObject>
#include <QString>
class MacOSUtils final {
public:
static QString computerName();
static void enableLoginItem(bool startAtBoot);
static void setDockClickHandler();
static void hideDockIcon();
static void showDockIcon();
};
#endif // MACOSUTILS_H

View file

@ -0,0 +1,91 @@
/* 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 "macosutils.h"
#include "logger.h"
#include "models/helpmodel.h"
#include "qmlengineholder.h"
#include <objc/message.h>
#include <objc/objc.h>
#include <QFile>
#include <QMenuBar>
#import <Cocoa/Cocoa.h>
#import <ServiceManagement/ServiceManagement.h>
namespace {
Logger logger(LOG_MACOS, "MacOSUtils");
}
// static
QString MacOSUtils::computerName() {
NSString* name = [[NSHost currentHost] localizedName];
return QString::fromNSString(name);
}
// static
void MacOSUtils::enableLoginItem(bool startAtBoot) {
logger.debug() << "Enabling login-item";
NSString* appId = [[NSBundle mainBundle] bundleIdentifier];
NSString* loginItemAppId =
QString("%1.login-item").arg(QString::fromNSString(appId)).toNSString();
CFStringRef cfs = (__bridge CFStringRef)loginItemAppId;
Boolean ok = SMLoginItemSetEnabled(cfs, startAtBoot ? YES : NO);
logger.debug() << "Result: " << ok;
}
namespace {
bool dockClickHandler(id self, SEL cmd, ...) {
Q_UNUSED(self);
Q_UNUSED(cmd);
logger.debug() << "Dock icon clicked.";
QmlEngineHolder::instance()->showWindow();
return FALSE;
}
} // namespace
// static
void MacOSUtils::setDockClickHandler() {
NSApplication* app = [NSApplication sharedApplication];
if (!app) {
logger.debug() << "No sharedApplication";
return;
}
id delegate = [app delegate];
if (!delegate) {
logger.debug() << "No delegate";
return;
}
Class delegateClass = [delegate class];
if (!delegateClass) {
logger.debug() << "No delegate class";
return;
}
SEL shouldHandle = sel_registerName("applicationShouldHandleReopen:hasVisibleWindows:");
if (class_getInstanceMethod(delegateClass, shouldHandle)) {
if (!class_replaceMethod(delegateClass, shouldHandle, (IMP)dockClickHandler, "B@:")) {
logger.error() << "Failed to replace the dock click handler";
}
} else if (!class_addMethod(delegateClass, shouldHandle, (IMP)dockClickHandler, "B@:")) {
logger.error() << "Failed to register the dock click handler";
}
}
void MacOSUtils::hideDockIcon() {
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
}
void MacOSUtils::showDockIcon() {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}