diff --git a/CMakeLists.txt b/CMakeLists.txt index f7ada65a..50bd621c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,11 +2,11 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.0.6.1 +project(${PROJECT} VERSION 4.0.7.1 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) -set(RELEASE_DATE "2023-09-17") +set(RELEASE_DATE "2023-09-21") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index bc450df2..e8795854 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit bc450df2418540869e97a31c4dd76839989b1fa6 +Subproject commit e8795854a5cf27004fe78caecc90a961688d1d41 diff --git a/client/daemon/daemonlocalserver.cpp b/client/daemon/daemonlocalserver.cpp index 02a12cb9..9b8e9504 100644 --- a/client/daemon/daemonlocalserver.cpp +++ b/client/daemon/daemonlocalserver.cpp @@ -12,7 +12,7 @@ #include "leakdetector.h" #include "logger.h" -#ifdef MZ_MACOS +#if defined(MZ_MACOS) || defined(MZ_LINUX) # include # include # include @@ -68,7 +68,8 @@ bool DaemonLocalServer::initialize() { QString DaemonLocalServer::daemonPath() const { #if defined(MZ_WINDOWS) return "\\\\.\\pipe\\amneziavpn"; -#elif defined(MZ_MACOS) +#endif +#if defined(MZ_MACOS) || defined(MZ_LINUX) QDir dir("/var/run"); if (!dir.exists()) { logger.warning() << "/var/run doesn't exist. Fallback /tmp."; @@ -92,7 +93,5 @@ QString DaemonLocalServer::daemonPath() const { } return VAR_PATH; -#else -# error Unsupported platform #endif } diff --git a/client/mozilla/networkwatcher.cpp b/client/mozilla/networkwatcher.cpp index 54beb11c..47fdb622 100644 --- a/client/mozilla/networkwatcher.cpp +++ b/client/mozilla/networkwatcher.cpp @@ -19,7 +19,7 @@ #endif #ifdef MZ_LINUX -//# include "platforms/linux/linuxnetworkwatcher.h" +# include "platforms/linux/linuxnetworkwatcher.h" #endif #ifdef MZ_MACOS @@ -56,7 +56,7 @@ void NetworkWatcher::initialize() { #if defined(MZ_WINDOWS) m_impl = new WindowsNetworkWatcher(this); #elif defined(MZ_LINUX) -// m_impl = new LinuxNetworkWatcher(this); + m_impl = new LinuxNetworkWatcher(this); #elif defined(MZ_MACOS) m_impl = new MacOSNetworkWatcher(this); #elif defined(MZ_WASM) diff --git a/client/mozilla/shared/ipaddress.cpp b/client/mozilla/shared/ipaddress.cpp index 7c337755..95a2f2d0 100644 --- a/client/mozilla/shared/ipaddress.cpp +++ b/client/mozilla/shared/ipaddress.cpp @@ -29,8 +29,6 @@ IPAddress::IPAddress(const QString& ip) { if (m_prefixLength >= 128) { m_prefixLength = 128; } - } else { - Q_ASSERT(false); } } diff --git a/client/platforms/linux/daemon/dbustypeslinux.h b/client/platforms/linux/daemon/dbustypeslinux.h new file mode 100644 index 00000000..1a5e44e2 --- /dev/null +++ b/client/platforms/linux/daemon/dbustypeslinux.h @@ -0,0 +1,170 @@ +/* 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 DBUSTYPESLINUX_H +#define DBUSTYPESLINUX_H + +#include + +#include +#include +#include +#include + +/* D-Bus metatype for marshalling arguments to the SetLinkDNS method */ +class DnsResolver : public QHostAddress { + public: + DnsResolver(const QHostAddress& address = QHostAddress()) + : QHostAddress(address) {} + + friend QDBusArgument& operator<<(QDBusArgument& args, const DnsResolver& ip) { + args.beginStructure(); + if (ip.protocol() == QAbstractSocket::IPv6Protocol) { + Q_IPV6ADDR addrv6 = ip.toIPv6Address(); + args << AF_INET6; + args << QByteArray::fromRawData((const char*)&addrv6, sizeof(addrv6)); + } else { + quint32 addrv4 = ip.toIPv4Address(); + QByteArray data(4, 0); + data[0] = (addrv4 >> 24) & 0xff; + data[1] = (addrv4 >> 16) & 0xff; + data[2] = (addrv4 >> 8) & 0xff; + data[3] = (addrv4 >> 0) & 0xff; + args << AF_INET; + args << data; + } + args.endStructure(); + return args; + } + friend const QDBusArgument& operator>>(const QDBusArgument& args, + DnsResolver& ip) { + int family; + QByteArray data; + args.beginStructure(); + args >> family >> data; + args.endStructure(); + if (family == AF_INET6) { + ip.setAddress(data.constData()); + } else if (data.count() >= 4) { + quint32 addrv4 = 0; + addrv4 |= (data[0] << 24); + addrv4 |= (data[1] << 16); + addrv4 |= (data[2] << 8); + addrv4 |= (data[3] << 0); + ip.setAddress(addrv4); + } + return args; + } +}; +typedef QList DnsResolverList; +Q_DECLARE_METATYPE(DnsResolver); +Q_DECLARE_METATYPE(DnsResolverList); + +/* D-Bus metatype for marshalling arguments to the SetLinkDomains method */ +class DnsLinkDomain { + public: + DnsLinkDomain(const QString d = "", bool s = false) { + domain = d; + search = s; + }; + QString domain; + bool search; + + friend QDBusArgument& operator<<(QDBusArgument& args, + const DnsLinkDomain& data) { + args.beginStructure(); + args << data.domain << data.search; + args.endStructure(); + return args; + } + friend const QDBusArgument& operator>>(const QDBusArgument& args, + DnsLinkDomain& data) { + args.beginStructure(); + args >> data.domain >> data.search; + args.endStructure(); + return args; + } + bool operator==(const DnsLinkDomain& other) const { + return (domain == other.domain) && (search == other.search); + } + bool operator==(const QString& other) const { return (domain == other); } +}; +typedef QList DnsLinkDomainList; +Q_DECLARE_METATYPE(DnsLinkDomain); +Q_DECLARE_METATYPE(DnsLinkDomainList); + +/* D-Bus metatype for marshalling the Domains property */ +class DnsDomain { + public: + DnsDomain() {} + int ifindex = 0; + QString domain = ""; + bool search = false; + + friend QDBusArgument& operator<<(QDBusArgument& args, const DnsDomain& data) { + args.beginStructure(); + args << data.ifindex << data.domain << data.search; + args.endStructure(); + return args; + } + friend const QDBusArgument& operator>>(const QDBusArgument& args, + DnsDomain& data) { + args.beginStructure(); + args >> data.ifindex >> data.domain >> data.search; + args.endStructure(); + return args; + } +}; +typedef QList DnsDomainList; +Q_DECLARE_METATYPE(DnsDomain); +Q_DECLARE_METATYPE(DnsDomainList); + +/* D-Bus metatype for marshalling the freedesktop login manager data. */ +class UserData { + public: + QString name; + uint userid; + QDBusObjectPath path; + + friend QDBusArgument& operator<<(QDBusArgument& args, const UserData& data) { + args.beginStructure(); + args << data.userid << data.name << data.path; + args.endStructure(); + return args; + } + friend const QDBusArgument& operator>>(const QDBusArgument& args, + UserData& data) { + args.beginStructure(); + args >> data.userid >> data.name >> data.path; + args.endStructure(); + return args; + } +}; +typedef QList UserDataList; +Q_DECLARE_METATYPE(UserData); +Q_DECLARE_METATYPE(UserDataList); + +class DnsMetatypeRegistrationProxy { + public: + DnsMetatypeRegistrationProxy() { + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + } +}; + +#endif // DBUSTYPESLINUX_H diff --git a/client/platforms/linux/daemon/dnsutilslinux.cpp b/client/platforms/linux/daemon/dnsutilslinux.cpp new file mode 100644 index 00000000..cc47202b --- /dev/null +++ b/client/platforms/linux/daemon/dnsutilslinux.cpp @@ -0,0 +1,212 @@ +/* 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 "dnsutilslinux.h" + +#include + +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +constexpr const char* DBUS_RESOLVE_SERVICE = "org.freedesktop.resolve1"; +constexpr const char* DBUS_RESOLVE_PATH = "/org/freedesktop/resolve1"; +constexpr const char* DBUS_RESOLVE_MANAGER = "org.freedesktop.resolve1.Manager"; +constexpr const char* DBUS_PROPERTY_INTERFACE = + "org.freedesktop.DBus.Properties"; + +namespace { +Logger logger("DnsUtilsLinux"); +} + +DnsUtilsLinux::DnsUtilsLinux(QObject* parent) : DnsUtils(parent) { + MZ_COUNT_CTOR(DnsUtilsLinux); + logger.debug() << "DnsUtilsLinux created."; + + QDBusConnection conn = QDBusConnection::systemBus(); + m_resolver = new QDBusInterface(DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH, + DBUS_RESOLVE_MANAGER, conn, this); +} + +DnsUtilsLinux::~DnsUtilsLinux() { + MZ_COUNT_DTOR(DnsUtilsLinux); + + for (auto iterator = m_linkDomains.constBegin(); + iterator != m_linkDomains.constEnd(); ++iterator) { + QList argumentList; + argumentList << QVariant::fromValue(iterator.key()); + argumentList << QVariant::fromValue(iterator.value()); + m_resolver->asyncCallWithArgumentList(QStringLiteral("SetLinkDomains"), + argumentList); + } + + if (m_ifindex > 0) { + m_resolver->asyncCall(QStringLiteral("RevertLink"), m_ifindex); + } + + logger.debug() << "DnsUtilsLinux destroyed."; +} + +bool DnsUtilsLinux::updateResolvers(const QString& ifname, + const QList& resolvers) { + m_ifindex = if_nametoindex(qPrintable(ifname)); + if (m_ifindex <= 0) { + logger.error() << "Unable to resolve ifindex for" << ifname; + return false; + } + + setLinkDNS(m_ifindex, resolvers); + setLinkDefaultRoute(m_ifindex, true); + updateLinkDomains(); + return true; +} + +bool DnsUtilsLinux::restoreResolvers() { + for (auto iterator = m_linkDomains.constBegin(); + iterator != m_linkDomains.constEnd(); ++iterator) { + setLinkDomains(iterator.key(), iterator.value()); + } + m_linkDomains.clear(); + + /* Revert the VPN interface's DNS configuration */ + if (m_ifindex > 0) { + QList argumentList = {QVariant::fromValue(m_ifindex)}; + QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList( + QStringLiteral("RevertLink"), argumentList); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsCallCompleted(QDBusPendingCallWatcher*))); + + m_ifindex = 0; + } + + return true; +} + +void DnsUtilsLinux::dnsCallCompleted(QDBusPendingCallWatcher* call) { + QDBusPendingReply<> reply = *call; + if (reply.isError()) { + logger.error() << "Error received from the DBus service"; + } + delete call; +} + +void DnsUtilsLinux::setLinkDNS(int ifindex, + const QList& resolvers) { + QList resolverList; + char ifnamebuf[IF_NAMESIZE]; + const char* ifname = if_indextoname(ifindex, ifnamebuf); + for (const auto& ip : resolvers) { + resolverList.append(ip); + if (ifname) { + logger.debug() << "Adding DNS resolver" << ip.toString() << "via" + << ifname; + } + } + + QList argumentList; + argumentList << QVariant::fromValue(ifindex); + argumentList << QVariant::fromValue(resolverList); + QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList( + QStringLiteral("SetLinkDNS"), argumentList); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsCallCompleted(QDBusPendingCallWatcher*))); +} + +void DnsUtilsLinux::setLinkDomains(int ifindex, + const QList& domains) { + char ifnamebuf[IF_NAMESIZE]; + const char* ifname = if_indextoname(ifindex, ifnamebuf); + if (ifname) { + for (const auto& d : domains) { + // The DNS search domains often winds up revealing user's ISP which + // can correlate back to their location. + logger.debug() << "Setting DNS domain:" << logger.sensitive(d.domain) + << "via" << ifname << (d.search ? "search" : ""); + } + } + + QList argumentList; + argumentList << QVariant::fromValue(ifindex); + argumentList << QVariant::fromValue(domains); + QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList( + QStringLiteral("SetLinkDomains"), argumentList); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsCallCompleted(QDBusPendingCallWatcher*))); +} + +void DnsUtilsLinux::setLinkDefaultRoute(int ifindex, bool enable) { + QList argumentList; + argumentList << QVariant::fromValue(ifindex); + argumentList << QVariant::fromValue(enable); + QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList( + QStringLiteral("SetLinkDefaultRoute"), argumentList); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsCallCompleted(QDBusPendingCallWatcher*))); +} + +void DnsUtilsLinux::updateLinkDomains() { + /* Get the list of search domains, and remove any others that might conspire + * to satisfy DNS resolution. Unfortunately, this is a pain because Qt doesn't + * seem to be able to demarshall complex property types. + */ + QDBusMessage message = QDBusMessage::createMethodCall( + DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH, DBUS_PROPERTY_INTERFACE, "Get"); + message << QString(DBUS_RESOLVE_MANAGER); + message << QString("Domains"); + QDBusPendingReply reply = + m_resolver->connection().asyncCall(message); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsDomainsReceived(QDBusPendingCallWatcher*))); +} + +void DnsUtilsLinux::dnsDomainsReceived(QDBusPendingCallWatcher* call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + logger.error() << "Error retrieving the DNS domains from the DBus service"; + delete call; + return; + } + + /* Update the state of the DNS domains */ + m_linkDomains.clear(); + QDBusArgument args = qvariant_cast(reply.value()); + QList list = qdbus_cast>(args); + for (const auto& d : list) { + if (d.ifindex == 0) { + continue; + } + m_linkDomains[d.ifindex].append(DnsLinkDomain(d.domain, d.search)); + } + + /* Drop any competing root search domains. */ + DnsLinkDomain root = DnsLinkDomain(".", true); + for (auto iterator = m_linkDomains.constBegin(); + iterator != m_linkDomains.constEnd(); ++iterator) { + if (!iterator.value().contains(root)) { + continue; + } + QList newlist = iterator.value(); + newlist.removeAll(root); + setLinkDomains(iterator.key(), newlist); + } + + /* Add a root search domain for the new interface. */ + QList newlist = {root}; + setLinkDomains(m_ifindex, newlist); + delete call; +} + +static DnsMetatypeRegistrationProxy s_dnsMetatypeProxy; diff --git a/client/platforms/linux/daemon/dnsutilslinux.h b/client/platforms/linux/daemon/dnsutilslinux.h new file mode 100644 index 00000000..e4bbd273 --- /dev/null +++ b/client/platforms/linux/daemon/dnsutilslinux.h @@ -0,0 +1,41 @@ +/* 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 DNSUTILSLINUX_H +#define DNSUTILSLINUX_H + +#include +#include + +#include "daemon/dnsutils.h" +#include "dbustypeslinux.h" + +class DnsUtilsLinux final : public DnsUtils { + Q_OBJECT + Q_DISABLE_COPY_MOVE(DnsUtilsLinux) + + public: + DnsUtilsLinux(QObject* parent); + ~DnsUtilsLinux(); + bool updateResolvers(const QString& ifname, + const QList& resolvers) override; + bool restoreResolvers() override; + + private: + void setLinkDNS(int ifindex, const QList& resolvers); + void setLinkDomains(int ifindex, const QList& domains); + void setLinkDefaultRoute(int ifindex, bool enable); + void updateLinkDomains(); + + private slots: + void dnsCallCompleted(QDBusPendingCallWatcher*); + void dnsDomainsReceived(QDBusPendingCallWatcher*); + + private: + int m_ifindex = 0; + QMap m_linkDomains; + QDBusInterface* m_resolver = nullptr; +}; + +#endif // DNSUTILSLINUX_H diff --git a/client/platforms/linux/daemon/iputilslinux.cpp b/client/platforms/linux/daemon/iputilslinux.cpp new file mode 100644 index 00000000..9a51caad --- /dev/null +++ b/client/platforms/linux/daemon/iputilslinux.cpp @@ -0,0 +1,150 @@ +/* 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 "iputilslinux.h" + +#include +#include +#include +#include + +#include +#include + +#include "daemon/wireguardutils.h" +#include "leakdetector.h" +#include "logger.h" + +constexpr uint32_t ETH_MTU = 1500; +constexpr uint32_t WG_MTU_OVERHEAD = 80; + +namespace { +Logger logger("IPUtilsLinux"); +} + +IPUtilsLinux::IPUtilsLinux(QObject* parent) : IPUtils(parent) { + MZ_COUNT_CTOR(IPUtilsLinux); + logger.debug() << "IPUtilsLinux created."; +} + +IPUtilsLinux::~IPUtilsLinux() { + MZ_COUNT_DTOR(IPUtilsLinux); + logger.debug() << "IPUtilsLinux destroyed."; +} + +bool IPUtilsLinux::addInterfaceIPs(const InterfaceConfig& config) { + return addIP4AddressToDevice(config) && addIP6AddressToDevice(config); +} + +bool IPUtilsLinux::setMTUAndUp(const InterfaceConfig& config) { + Q_UNUSED(config); + + // 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); }); + + // Setup the interface to interact with + struct ifreq ifr; + strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ); + + // MTU + // FIXME: We need to know how many layers deep this particular + // interface is into a tunnel to work effectively. Otherwise + // we will run into fragmentation issues. + ifr.ifr_mtu = ETH_MTU - WG_MTU_OVERHEAD; + int ret = ioctl(sockfd, SIOCSIFMTU, &ifr); + if (ret) { + logger.error() << "Failed to set MTU -- Return code: " << ret; + return false; + } + + // Up + ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); + ret = ioctl(sockfd, SIOCSIFFLAGS, &ifr); + if (ret) { + logger.error() << "Failed to set device up -- Return code: " << ret; + return false; + } + + return true; +} + +bool IPUtilsLinux::addIP4AddressToDevice(const InterfaceConfig& config) { + struct ifreq ifr; + struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifr_addr; + + // Name the interface and set family + strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ); + ifr.ifr_addr.sa_family = AF_INET; + + // Get the device address to add to interface + QPair parsedAddr = + QHostAddress::parseSubnet(config.m_deviceIpv4Address); + QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit(); + char* deviceAddr = _deviceAddr.data(); + inet_pton(AF_INET, deviceAddr, &ifrAddr->sin_addr); + + // Create 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, SIOCSIFADDR, &ifr); + if (ret) { + logger.error() << "Failed to set IPv4: " << logger.sensitive(deviceAddr) + << "error:" << strerror(errno); + return false; + } + return true; +} + +bool IPUtilsLinux::addIP6AddressToDevice(const InterfaceConfig& config) { + // Set up the ifr and the companion ifr6 + struct in6_ifreq ifr6; + ifr6.prefixlen = 64; + + // Get the device address to add to ifr6 interface + QPair parsedAddr = + QHostAddress::parseSubnet(config.m_deviceIpv6Address); + QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit(); + char* deviceAddr = _deviceAddr.data(); + inet_pton(AF_INET6, deviceAddr, &ifr6.addr); + + // Create IPv6 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); }); + + // Get the index of named ifr and link with ifr6 + struct ifreq ifr; + strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ); + ifr.ifr_addr.sa_family = AF_INET6; + int ret = ioctl(sockfd, SIOGIFINDEX, &ifr); + if (ret) { + logger.error() << "Failed to get ifindex. Return code: " << ret; + return false; + } + ifr6.ifindex = ifr.ifr_ifindex; + + // Set ifr6 to the interface + ret = ioctl(sockfd, SIOCSIFADDR, &ifr6); + if (ret && (errno != EEXIST)) { + logger.error() << "Failed to set IPv6: " << logger.sensitive(deviceAddr) + << "error:" << strerror(errno); + return false; + } + + return true; +} diff --git a/client/platforms/linux/daemon/iputilslinux.h b/client/platforms/linux/daemon/iputilslinux.h new file mode 100644 index 00000000..38edf177 --- /dev/null +++ b/client/platforms/linux/daemon/iputilslinux.h @@ -0,0 +1,31 @@ +/* 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 IPUTILSLINUX_H +#define IPUTILSLINUX_H + +#include + +#include "daemon/iputils.h" + +class IPUtilsLinux final : public IPUtils { + public: + IPUtilsLinux(QObject* parent); + ~IPUtilsLinux(); + bool addInterfaceIPs(const InterfaceConfig& config) override; + bool setMTUAndUp(const InterfaceConfig& config) override; + + private: + bool addIP4AddressToDevice(const InterfaceConfig& config); + bool addIP6AddressToDevice(const InterfaceConfig& config); + + private: + struct in6_ifreq { + struct in6_addr addr; + uint32_t prefixlen; + unsigned int ifindex; + }; +}; + +#endif // IPUTILSLINUX_H \ No newline at end of file diff --git a/client/platforms/linux/daemon/linuxdaemon.cpp b/client/platforms/linux/daemon/linuxdaemon.cpp new file mode 100644 index 00000000..7c2d95db --- /dev/null +++ b/client/platforms/linux/daemon/linuxdaemon.cpp @@ -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/. */ + +#include "linuxdaemon.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +namespace { +Logger logger("LinuxDaemon"); +LinuxDaemon* s_daemon = nullptr; +} // namespace + +LinuxDaemon::LinuxDaemon() : Daemon(nullptr) { + MZ_COUNT_CTOR(LinuxDaemon); + + logger.debug() << "Daemon created"; + + m_wgutils = new WireguardUtilsLinux(this); + m_dnsutils = new DnsUtilsLinux(this); + m_iputils = new IPUtilsLinux(this); + + Q_ASSERT(s_daemon == nullptr); + s_daemon = this; +} + +LinuxDaemon::~LinuxDaemon() { + MZ_COUNT_DTOR(LinuxDaemon); + + logger.debug() << "Daemon released"; + + Q_ASSERT(s_daemon == this); + s_daemon = nullptr; +} + +// static +LinuxDaemon* LinuxDaemon::instance() { + Q_ASSERT(s_daemon); + return s_daemon; +} diff --git a/client/platforms/linux/daemon/linuxdaemon.h b/client/platforms/linux/daemon/linuxdaemon.h new file mode 100644 index 00000000..7f5d27b7 --- /dev/null +++ b/client/platforms/linux/daemon/linuxdaemon.h @@ -0,0 +1,36 @@ +/* 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 LINUXDAEMON_H +#define LINUXDAEMON_H + + +#include "daemon/daemon.h" +#include "dnsutilslinux.h" +#include "iputilslinux.h" +#include "wireguardutilslinux.h" + +class LinuxDaemon final : public Daemon { + friend class IPUtilsMacos; + + public: + LinuxDaemon(); + ~LinuxDaemon(); + + static LinuxDaemon* instance(); + + 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: + WireguardUtilsLinux* m_wgutils = nullptr; + DnsUtilsLinux* m_dnsutils = nullptr; + IPUtilsLinux* m_iputils = nullptr; +}; + +#endif // LINUXDAEMON_H diff --git a/client/platforms/linux/daemon/linuxroutemonitor.cpp b/client/platforms/linux/daemon/linuxroutemonitor.cpp new file mode 100644 index 00000000..f0c49eb6 --- /dev/null +++ b/client/platforms/linux/daemon/linuxroutemonitor.cpp @@ -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 "linuxroutemonitor.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +namespace { +Logger logger("LinuxRouteMonitor"); +} // namespace + + +typedef struct wg_allowedip { + uint16_t family; + union { + struct in_addr ip4; + struct in6_addr ip6; + }; + uint8_t cidr; + struct wg_allowedip *next_allowedip; +} wg_allowedip; + +constexpr const char* WG_INTERFACE = "amn0"; + +static void nlmsg_append_attr(struct nlmsghdr* nlmsg, size_t maxlen, + int attrtype, const void* attrdata, + size_t attrlen); +static void nlmsg_append_attr32(struct nlmsghdr* nlmsg, size_t maxlen, + int attrtype, uint32_t value); + +static bool buildAllowedIp(wg_allowedip* ip, const IPAddress& prefix); + + +LinuxRouteMonitor::LinuxRouteMonitor(const QString& ifname, QObject* parent) + : QObject(parent), m_ifname(ifname) { + MZ_COUNT_CTOR(LinuxRouteMonitor); + logger.debug() << "LinuxRouteMonitor created."; + + m_nlsock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE); + if (m_nlsock < 0) { + logger.warning() << "Failed to create netlink socket:" << strerror(errno); + } + + struct sockaddr_nl nladdr; + memset(&nladdr, 0, sizeof(nladdr)); + nladdr.nl_family = AF_NETLINK; + nladdr.nl_pid = getpid(); + if (bind(m_nlsock, (struct sockaddr*)&nladdr, sizeof(nladdr)) != 0) { + logger.warning() << "Failed to bind netlink socket:" << strerror(errno); + } + + m_notifier = new QSocketNotifier(m_nlsock, QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, this, + &LinuxRouteMonitor::nlsockReady); +} + +LinuxRouteMonitor::~LinuxRouteMonitor() { + MZ_COUNT_DTOR(LinuxRouteMonitor); + if (m_nlsock >= 0) { + close(m_nlsock); + } + logger.debug() << "WireguardUtilsLinux destroyed."; +} + +// Compare memory against zero. +static int memcmpzero(const void* data, size_t len) { + const quint8* ptr = static_cast(data); + while (len--) { + if (*ptr++) return 1; + } + return 0; +} + +bool LinuxRouteMonitor::insertRoute(const IPAddress& prefix) { + logger.debug() << "Adding route to" << prefix.toString(); + + const int flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK; + return rtmSendRoute(RTM_NEWROUTE, flags, RTN_UNICAST, prefix); +} + +bool LinuxRouteMonitor::deleteRoute(const IPAddress& prefix) { + logger.debug() << "Removing route to" << prefix.toString(); + + const int flags = NLM_F_REQUEST | NLM_F_ACK; + return rtmSendRoute(RTM_DELROUTE, flags, RTN_UNICAST, prefix); +} + +bool LinuxRouteMonitor::addExclusionRoute(const IPAddress& prefix) { + logger.debug() << "Adding exclusion route for" + << prefix.toString(); + const int flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK; + return rtmSendRoute(RTM_NEWROUTE, flags, RTN_THROW, prefix); +} + +bool LinuxRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { + logger.debug() << "Removing exclusion route for" + << prefix.toString(); + const int flags = NLM_F_REQUEST | NLM_F_ACK; + return rtmSendRoute(RTM_DELROUTE, flags, RTN_THROW, prefix); +} + +bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type, + const IPAddress& prefix) { + constexpr size_t rtm_max_size = sizeof(struct rtmsg) + + 2 * RTA_SPACE(sizeof(uint32_t)) + + RTA_SPACE(sizeof(struct in6_addr)); + wg_allowedip ip; + if (!buildAllowedIp(&ip, prefix)) { + logger.warning() << "Invalid destination prefix"; + return false; + } + + char buf[NLMSG_SPACE(rtm_max_size)]; + struct nlmsghdr* nlmsg = reinterpret_cast(buf); + struct rtmsg* rtm = static_cast(NLMSG_DATA(nlmsg)); + + memset(buf, 0, sizeof(buf)); + nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nlmsg->nlmsg_type = action; + nlmsg->nlmsg_flags = flags; + nlmsg->nlmsg_pid = getpid(); + nlmsg->nlmsg_seq = m_nlseq++; + rtm->rtm_dst_len = ip.cidr; + rtm->rtm_family = ip.family; + rtm->rtm_type = type; + rtm->rtm_table = RT_TABLE_UNSPEC; + rtm->rtm_protocol = RTPROT_BOOT; + rtm->rtm_scope = RT_SCOPE_UNIVERSE; + + if (rtm->rtm_family == AF_INET6) { + nlmsg_append_attr(nlmsg, sizeof(buf), RTA_DST, &ip.ip6, sizeof(ip.ip6)); + } else { + nlmsg_append_attr(nlmsg, sizeof(buf), RTA_DST, &ip.ip4, sizeof(ip.ip4)); + } + + if (rtm->rtm_type == RTN_UNICAST) { + int index = if_nametoindex(WG_INTERFACE); + + if (index <= 0) { + logger.error() << "if_nametoindex() failed:" << strerror(errno); + return false; + } + nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_OIF, index); + } + + if (rtm->rtm_type == RTN_THROW) { + int index = if_nametoindex(getgatewayandiface().toUtf8()); + if (index <= 0) { + logger.error() << "if_nametoindex() failed:" << strerror(errno); + return false; + } + nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_OIF, index); + } + + struct sockaddr_nl nladdr; + memset(&nladdr, 0, sizeof(nladdr)); + nladdr.nl_family = AF_NETLINK; + size_t result = sendto(m_nlsock, buf, nlmsg->nlmsg_len, 0, + (struct sockaddr*)&nladdr, sizeof(nladdr)); + + return (result == nlmsg->nlmsg_len); +} + +static void nlmsg_append_attr(struct nlmsghdr* nlmsg, size_t maxlen, + int attrtype, const void* attrdata, + size_t attrlen) { + size_t newlen = NLMSG_ALIGN(nlmsg->nlmsg_len) + RTA_SPACE(attrlen); + if (newlen <= maxlen) { + char* buf = reinterpret_cast(nlmsg) + NLMSG_ALIGN(nlmsg->nlmsg_len); + struct rtattr* attr = reinterpret_cast(buf); + attr->rta_type = attrtype; + attr->rta_len = RTA_LENGTH(attrlen); + memcpy(RTA_DATA(attr), attrdata, attrlen); + nlmsg->nlmsg_len = newlen; + } +} + +static void nlmsg_append_attr32(struct nlmsghdr* nlmsg, size_t maxlen, + int attrtype, uint32_t value) { + nlmsg_append_attr(nlmsg, maxlen, attrtype, &value, sizeof(value)); +} + +void LinuxRouteMonitor::nlsockReady() { + char buf[1024]; + ssize_t len = recv(m_nlsock, buf, sizeof(buf), MSG_DONTWAIT); + if (len <= 0) { + return; + } + + struct nlmsghdr* nlmsg = (struct nlmsghdr*)buf; + while (NLMSG_OK(nlmsg, len)) { + if (nlmsg->nlmsg_type == NLMSG_DONE) { + return; + } + if (nlmsg->nlmsg_type != NLMSG_ERROR) { + nlmsg = NLMSG_NEXT(nlmsg, len); + continue; + } + struct nlmsgerr* err = static_cast(NLMSG_DATA(nlmsg)); + if (err->error != 0) { + logger.debug() << "Netlink request failed:" << strerror(-err->error); + } + nlmsg = NLMSG_NEXT(nlmsg, len); + } +} + +#define BUFFER_SIZE 4096 + +QString LinuxRouteMonitor::getgatewayandiface() +{ + int received_bytes = 0, msg_len = 0, route_attribute_len = 0; + int sock = -1, msgseq = 0; + struct nlmsghdr *nlh, *nlmsg; + struct rtmsg *route_entry; + // This struct contain route attributes (route type) + struct rtattr *route_attribute; + char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE]; + char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE]; + char *ptr = buffer; + struct timeval tv; + + if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) { + perror("socket failed"); + return ""; + } + + memset(msgbuf, 0, sizeof(msgbuf)); + memset(gateway_address, 0, sizeof(gateway_address)); + memset(interface, 0, sizeof(interface)); + memset(buffer, 0, sizeof(buffer)); + + /* point the header and the msg structure pointers into the buffer */ + nlmsg = (struct nlmsghdr *)msgbuf; + + /* Fill in the nlmsg header*/ + nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nlmsg->nlmsg_type = RTM_GETROUTE; // Get the routes from kernel routing table . + nlmsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump. + nlmsg->nlmsg_seq = msgseq++; // Sequence of the message packet. + nlmsg->nlmsg_pid = getpid(); // PID of process sending the request. + + /* 1 Sec Timeout to avoid stall */ + tv.tv_sec = 1; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (struct timeval *)&tv, sizeof(struct timeval)); + /* send msg */ + if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) { + perror("send failed"); + return ""; + } + + /* receive response */ + do + { + received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0); + if (received_bytes < 0) { + perror("Error in recv"); + return ""; + } + + nlh = (struct nlmsghdr *) ptr; + + /* Check if the header is valid */ + if((NLMSG_OK(nlmsg, received_bytes) == 0) || + (nlmsg->nlmsg_type == NLMSG_ERROR)) + { + perror("Error in received packet"); + return ""; + } + + /* If we received all data break */ + if (nlh->nlmsg_type == NLMSG_DONE) + break; + else { + ptr += received_bytes; + msg_len += received_bytes; + } + + /* Break if its not a multi part message */ + if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0) + break; + } + while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid())); + + /* parse response */ + for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes)) + { + /* Get the route data */ + route_entry = (struct rtmsg *) NLMSG_DATA(nlh); + + /* We are just interested in main routing table */ + if (route_entry->rtm_table != RT_TABLE_MAIN) + continue; + + route_attribute = (struct rtattr *) RTM_RTA(route_entry); + route_attribute_len = RTM_PAYLOAD(nlh); + + /* Loop through all attributes */ + for ( ; RTA_OK(route_attribute, route_attribute_len); + route_attribute = RTA_NEXT(route_attribute, route_attribute_len)) + { + switch(route_attribute->rta_type) { + case RTA_OIF: + if_indextoname(*(int *)RTA_DATA(route_attribute), interface); + break; + case RTA_GATEWAY: + inet_ntop(AF_INET, RTA_DATA(route_attribute), + gateway_address, sizeof(gateway_address)); + break; + default: + break; + } + } + + if ((*gateway_address) && (*interface)) { + logger.debug() << "Gateway " << gateway_address << " for interface " << interface; + break; + } + } + close(sock); + return interface; +} + +static bool buildAllowedIp(wg_allowedip* ip, + const IPAddress& prefix) { + const char* addrString = qPrintable(prefix.address().toString()); + if (prefix.type() == QAbstractSocket::IPv4Protocol) { + ip->family = AF_INET; + ip->cidr = prefix.prefixLength(); + return inet_pton(AF_INET, addrString, &ip->ip4) == 1; + } + if (prefix.type() == QAbstractSocket::IPv6Protocol) { + ip->family = AF_INET6; + ip->cidr = prefix.prefixLength(); + return inet_pton(AF_INET6, addrString, &ip->ip6) == 1; + } + return false; +} diff --git a/client/platforms/linux/daemon/linuxroutemonitor.h b/client/platforms/linux/daemon/linuxroutemonitor.h new file mode 100644 index 00000000..f1c3ac1d --- /dev/null +++ b/client/platforms/linux/daemon/linuxroutemonitor.h @@ -0,0 +1,46 @@ +/* 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 LINUXROUTEMONITOR_H +#define LINUXROUTEMONITOR_H + +#include +#include +#include +#include +#include + +#include "ipaddress.h" + + +class LinuxRouteMonitor final : public QObject { + Q_OBJECT + + public: + LinuxRouteMonitor(const QString& ifname, QObject* parent = nullptr); + ~LinuxRouteMonitor(); + + bool insertRoute(const IPAddress& prefix); + bool deleteRoute(const IPAddress& prefix); + + bool addExclusionRoute(const IPAddress& prefix); + bool deleteExclusionRoute(const IPAddress& prefix); + private: + static QString addrToString(const struct sockaddr* sa); + static QString addrToString(const QByteArray& data); + bool rtmSendRoute(int action, int flags, int type, + const IPAddress& prefix); + QString getgatewayandiface(); + QString m_ifname; + unsigned int m_ifindex = 0; + int m_nlsock = -1; + int m_nlseq = 0; + QSocketNotifier* m_notifier = nullptr; + + private slots: + void nlsockReady(); + +}; + +#endif // LINUXROUTEMONITOR_H diff --git a/client/platforms/linux/daemon/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp new file mode 100644 index 00000000..a8b7b04a --- /dev/null +++ b/client/platforms/linux/daemon/wireguardutilslinux.cpp @@ -0,0 +1,369 @@ +/* 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 "wireguardutilslinux.h" + +#include + +#include +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +constexpr const int WG_TUN_PROC_TIMEOUT = 5000; +constexpr const char* WG_RUNTIME_DIR = "/var/run/wireguard"; + +namespace { +Logger logger("WireguardUtilsLinux"); +Logger logwireguard("WireguardGo"); +}; // namespace + +WireguardUtilsLinux::WireguardUtilsLinux(QObject* parent) + : WireguardUtils(parent), m_tunnel(this) { + MZ_COUNT_CTOR(WireguardUtilsLinux); + logger.debug() << "WireguardUtilsLinux created."; + + connect(&m_tunnel, SIGNAL(readyReadStandardOutput()), this, + SLOT(tunnelStdoutReady())); + connect(&m_tunnel, SIGNAL(errorOccurred(QProcess::ProcessError)), this, + SLOT(tunnelErrorOccurred(QProcess::ProcessError))); +} + +WireguardUtilsLinux::~WireguardUtilsLinux() { + MZ_COUNT_DTOR(WireguardUtilsLinux); + logger.debug() << "WireguardUtilsLinux destroyed."; +} + +void WireguardUtilsLinux::tunnelStdoutReady() { + for (;;) { + QByteArray line = m_tunnel.readLine(); + if (line.length() <= 0) { + break; + } + logwireguard.debug() << QString::fromUtf8(line); + } +} + +void WireguardUtilsLinux::tunnelErrorOccurred(QProcess::ProcessError error) { + logger.warning() << "Tunnel process encountered an error:" << error; + emit backendFailure(); +} + +bool WireguardUtilsLinux::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) + ".sock"); + pe.insert("WG_TUN_NAME_FILE", wgNameFile); +#ifdef MZ_DEBUG + pe.insert("LOG_LEVEL", "debug"); +#endif + m_tunnel.setProcessEnvironment(pe); + + QDir appPath(QCoreApplication::applicationDirPath()); + QStringList wgArgs = {"-f", "amn0"}; + m_tunnel.start(appPath.filePath("../../client/bin/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 LinuxRouteMonitor(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 WireguardUtilsLinux::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 WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) { + QByteArray publicKey = + QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + + QByteArray pskKey = QByteArray::fromBase64(qPrintable(config.m_serverPskKey)); + + logger.debug() << "Configuring peer" << config.m_serverPublicKey << "via" << config.m_serverIpv4AddrIn; + + // Update/create the peer config + QString message; + QTextStream out(&message); + out << "set=1\n"; + out << "public_key=" << QString(publicKey.toHex()) << "\n"; + out << "preshared_key=" << QString(pskKey.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"; + out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n"; + for (const IPAddress& ip : config.m_allowedIPAddressRanges) { + out << "allowed_ip=" << ip.toString() << "\n"; + } + + // Exclude the server address, except for multihop exit servers. + if ((config.m_hopType != InterfaceConfig::MultiHopExit) && + (m_rtmonitor != nullptr)) { + m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + } + + int err = uapiErrno(uapiCommand(message)); + if (err != 0) { + logger.error() << "Peer configuration failed:" << strerror(err); + } + return (err == 0); +} + +bool WireguardUtilsLinux::deletePeer(const InterfaceConfig& config) { + QByteArray publicKey = + QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + + // Clear exclustion routes for this peer. + if ((config.m_hopType != InterfaceConfig::MultiHopExit) && + (m_rtmonitor != nullptr)) { + m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + } + + 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); +} + +QList WireguardUtilsLinux::getPeerStatus() { + QString reply = uapiCommand("get=1"); + PeerStatus status; + QList peerList; + 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") { + if (!status.m_pubkey.isEmpty()) { + peerList.append(status); + } + QByteArray pubkey = QByteArray::fromHex(value.toUtf8()); + status = PeerStatus(pubkey.toBase64()); + } + + if (name == "tx_bytes") { + status.m_txBytes = value.toDouble(); + } + if (name == "rx_bytes") { + status.m_rxBytes = value.toDouble(); + } + if (name == "last_handshake_time_sec") { + status.m_handshake += value.toLongLong() * 1000; + } + if (name == "last_handshake_time_nsec") { + status.m_handshake += value.toLongLong() / 1000000; + } + } + if (!status.m_pubkey.isEmpty()) { + peerList.append(status); + } + + return peerList; +} + +bool WireguardUtilsLinux::updateRoutePrefix(const IPAddress& prefix) { + if (!m_rtmonitor) { + return false; + } + if (prefix.prefixLength() > 0) { + return m_rtmonitor->insertRoute(prefix); + } + + // Ensure that we do not replace the default route. + if (prefix.type() == QAbstractSocket::IPv4Protocol) { + return m_rtmonitor->insertRoute(IPAddress("0.0.0.0/1")) && + m_rtmonitor->insertRoute(IPAddress("128.0.0.0/1")); + } + if (prefix.type() == QAbstractSocket::IPv6Protocol) { + return m_rtmonitor->insertRoute(IPAddress("::/1")) && + m_rtmonitor->insertRoute(IPAddress("8000::/1")); + } + + return false; +} + +bool WireguardUtilsLinux::deleteRoutePrefix(const IPAddress& prefix) { + if (!m_rtmonitor) { + return false; + } + if (prefix.prefixLength() > 0) { + return m_rtmonitor->insertRoute(prefix); + } + + // Ensure that we do not replace the default route. + if (prefix.type() == QAbstractSocket::IPv4Protocol) { + return m_rtmonitor->deleteRoute(IPAddress("0.0.0.0/1")) && + m_rtmonitor->deleteRoute(IPAddress("128.0.0.0/1")); + } else if (prefix.type() == QAbstractSocket::IPv6Protocol) { + return m_rtmonitor->deleteRoute(IPAddress("::/1")) && + m_rtmonitor->deleteRoute(IPAddress("8000::/1")); + } else { + return false; + } +} + +bool WireguardUtilsLinux::addExclusionRoute(const IPAddress& prefix) { + if (!m_rtmonitor) { + return false; + } + return m_rtmonitor->addExclusionRoute(prefix); +} + +bool WireguardUtilsLinux::deleteExclusionRoute(const IPAddress& prefix) { + if (!m_rtmonitor) { + return false; + } + return m_rtmonitor->deleteExclusionRoute(prefix); +} + +QString WireguardUtilsLinux::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 WireguardUtilsLinux::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 WireguardUtilsLinux::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); + QString ifname = "amn0"; + + // 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(); +} diff --git a/client/platforms/linux/daemon/wireguardutilslinux.h b/client/platforms/linux/daemon/wireguardutilslinux.h new file mode 100644 index 00000000..a8320c95 --- /dev/null +++ b/client/platforms/linux/daemon/wireguardutilslinux.h @@ -0,0 +1,55 @@ +/* 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 WIREGUARDUTILSLINUX_H +#define WIREGUARDUTILSLINUX_H + +#include +#include + +#include "daemon/wireguardutils.h" +#include "linuxroutemonitor.h" + +class WireguardUtilsLinux final : public WireguardUtils { + Q_OBJECT + +public: + WireguardUtilsLinux(QObject* parent); + ~WireguardUtilsLinux(); + + 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 InterfaceConfig& config) override; + QList getPeerStatus() override; + + bool updateRoutePrefix(const IPAddress& prefix) override; + bool deleteRoutePrefix(const IPAddress& prefix) override; + + bool addExclusionRoute(const IPAddress& prefix) override; + bool deleteExclusionRoute(const IPAddress& prefix) 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; + LinuxRouteMonitor* m_rtmonitor = nullptr; +}; + +#endif // WIREGUARDUTILSLINUX_H diff --git a/client/platforms/linux/interfaceconfig.h b/client/platforms/linux/interfaceconfig.h new file mode 100644 index 00000000..bd4e383a --- /dev/null +++ b/client/platforms/linux/interfaceconfig.h @@ -0,0 +1,47 @@ +/* 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 INTERFACECONFIG_H +#define INTERFACECONFIG_H + +#include +#include + +#include "ipaddress.h" + +class QJsonObject; + +class InterfaceConfig { + Q_GADGET + + public: + InterfaceConfig() {} + + enum HopType { SingleHop, MultiHopEntry, MultiHopExit }; + Q_ENUM(HopType) + + HopType m_hopType; + QString m_privateKey; + QString m_deviceIpv4Address; + QString m_deviceIpv6Address; + QString m_serverIpv4Gateway; + QString m_serverIpv6Gateway; + QString m_serverPublicKey; + QString m_serverIpv4AddrIn; + QString m_serverIpv6AddrIn; + QString m_dnsServer; + int m_serverPort = 0; + QList m_allowedIPAddressRanges; + QStringList m_excludedAddresses; + QStringList m_vpnDisabledApps; +#if defined(MZ_ANDROID) || defined(MZ_IOS) + QString m_installationId; +#endif + + QJsonObject toJson() const; + QString toWgConf( + const QMap& extra = QMap()) const; +}; + +#endif // INTERFACECONFIG_H diff --git a/client/platforms/linux/linuxdependencies.cpp b/client/platforms/linux/linuxdependencies.cpp new file mode 100644 index 00000000..3a4cb259 --- /dev/null +++ b/client/platforms/linux/linuxdependencies.cpp @@ -0,0 +1,139 @@ +/* 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 "linuxdependencies.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +//#include "dbusclient.h" +#include "logger.h" + +namespace { + +Logger logger("LinuxDependencies"); + +void showAlert(const QString& message) { + logger.debug() << "Show alert:" << message; + + QMessageBox alert; + alert.setText(message); + alert.exec(); +} + +bool checkDaemonVersion() { + logger.debug() << "Check Daemon Version"; + + bool completed = false; + bool value = false; + + while (!completed) { + QCoreApplication::processEvents(); + } + + return value; +} + +} // namespace + +// static +bool LinuxDependencies::checkDependencies() { + char* path = getenv("PATH"); + if (!path) { + showAlert("No PATH env found."); + return false; + } + + if (!checkDaemonVersion()) { + showAlert("mozillavpn linuxdaemon needs to be updated or restarted."); + return false; + } + + return true; +} + +// static +QString LinuxDependencies::findCgroupPath(const QString& type) { + struct mntent entry; + char buf[PATH_MAX]; + + FILE* fp = fopen("/etc/mtab", "r"); + if (fp == NULL) { + return QString(); + } + + while (getmntent_r(fp, &entry, buf, sizeof(buf)) != NULL) { + if (strcmp(entry.mnt_type, "cgroup") != 0) { + continue; + } + if (hasmntopt(&entry, type.toLocal8Bit().constData()) != NULL) { + fclose(fp); + return QString(entry.mnt_dir); + } + } + fclose(fp); + + return QString(); +} + +// static +QString LinuxDependencies::findCgroup2Path() { + struct mntent entry; + char buf[PATH_MAX]; + + FILE* fp = fopen("/etc/mtab", "r"); + if (fp == NULL) { + return QString(); + } + + while (getmntent_r(fp, &entry, buf, sizeof(buf)) != NULL) { + if (strcmp(entry.mnt_type, "cgroup2") != 0) { + continue; + } + return QString(entry.mnt_dir); + } + fclose(fp); + + return QString(); +} + +// static +QString LinuxDependencies::gnomeShellVersion() { + QDBusInterface iface("org.gnome.Shell", "/org/gnome/Shell", + "org.gnome.Shell"); + if (!iface.isValid()) { + return QString(); + } + + QVariant shellVersion = iface.property("ShellVersion"); + if (!shellVersion.isValid()) { + return QString(); + } + return shellVersion.toString(); +} + +// static +QString LinuxDependencies::kdeFrameworkVersion() { + QProcess proc; + proc.start("kf5-config", QStringList{"--version"}, QIODeviceBase::ReadOnly); + if (!proc.waitForFinished()) { + return QString(); + } + + QByteArray result = proc.readAllStandardOutput(); + for (const QByteArray& line : result.split('\n')) { + if (line.startsWith("KDE Frameworks: ")) { + return QString::fromUtf8(line.last(line.size() - 16)); + } + } + + return QString(); +} diff --git a/client/platforms/linux/linuxdependencies.h b/client/platforms/linux/linuxdependencies.h new file mode 100644 index 00000000..70ff6f18 --- /dev/null +++ b/client/platforms/linux/linuxdependencies.h @@ -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 LINUXDEPENDENCIES_H +#define LINUXDEPENDENCIES_H + +#include + +class LinuxDependencies final { + public: + static bool checkDependencies(); + static QString findCgroupPath(const QString& type); + static QString findCgroup2Path(); + static QString gnomeShellVersion(); + static QString kdeFrameworkVersion(); + + private: + LinuxDependencies() = default; + ~LinuxDependencies() = default; + + Q_DISABLE_COPY(LinuxDependencies) +}; + +#endif // LINUXDEPENDENCIES_H diff --git a/client/platforms/linux/linuxnetworkwatcher.cpp b/client/platforms/linux/linuxnetworkwatcher.cpp new file mode 100644 index 00000000..c8ae0fea --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcher.cpp @@ -0,0 +1,57 @@ +/* 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 "linuxnetworkwatcher.h" + +#include + +#include "leakdetector.h" +#include "linuxnetworkwatcherworker.h" +#include "logger.h" + +namespace { +Logger logger("LinuxNetworkWatcher"); +} + +LinuxNetworkWatcher::LinuxNetworkWatcher(QObject* parent) + : NetworkWatcherImpl(parent) { + MZ_COUNT_CTOR(LinuxNetworkWatcher); + + m_thread.start(); +} + +LinuxNetworkWatcher::~LinuxNetworkWatcher() { + MZ_COUNT_DTOR(LinuxNetworkWatcher); + + delete m_worker; + + m_thread.quit(); + m_thread.wait(); +} + +void LinuxNetworkWatcher::initialize() { + logger.debug() << "initialize"; + + m_worker = new LinuxNetworkWatcherWorker(&m_thread); + + connect(this, &LinuxNetworkWatcher::checkDevicesInThread, m_worker, + &LinuxNetworkWatcherWorker::checkDevices); + + connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this, + &LinuxNetworkWatcher::unsecuredNetwork); + + // Let's wait a few seconds to allow the UI to be fully loaded and shown. + // This is not strictly needed, but it's better for user experience because + // it makes the UI faster to appear, plus it gives a bit of delay between the + // UI to appear and the first notification. + QTimer::singleShot(2000, this, [this]() { + QMetaObject::invokeMethod(m_worker, "initialize", Qt::QueuedConnection); + }); +} + +void LinuxNetworkWatcher::start() { + logger.debug() << "actived"; + NetworkWatcherImpl::start(); + emit checkDevicesInThread(); +} diff --git a/client/platforms/linux/linuxnetworkwatcher.h b/client/platforms/linux/linuxnetworkwatcher.h new file mode 100644 index 00000000..ed8c76ba --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcher.h @@ -0,0 +1,38 @@ +/* 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 LINUXNETWORKWATCHER_H +#define LINUXNETWORKWATCHER_H + +#include + +#include "networkwatcherimpl.h" + +class LinuxNetworkWatcherWorker; + +class LinuxNetworkWatcher final : public NetworkWatcherImpl { + Q_OBJECT + + public: + explicit LinuxNetworkWatcher(QObject* parent); + ~LinuxNetworkWatcher(); + + void initialize() override; + + void start() override; + + NetworkWatcherImpl::TransportType getTransportType() { + // TODO: Find out how to do that on linux generally. (VPN-2382) + return NetworkWatcherImpl::TransportType_Unknown; + }; + + signals: + void checkDevicesInThread(); + + private: + LinuxNetworkWatcherWorker* m_worker = nullptr; + QThread m_thread; +}; + +#endif // LINUXNETWORKWATCHER_H diff --git a/client/platforms/linux/linuxnetworkwatcherworker.cpp b/client/platforms/linux/linuxnetworkwatcherworker.cpp new file mode 100644 index 00000000..19ed3251 --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcherworker.cpp @@ -0,0 +1,177 @@ +/* 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 "linuxnetworkwatcherworker.h" + +#include + +#include "leakdetector.h" +#include "logger.h" + +// https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMDeviceType +#ifndef NM_DEVICE_TYPE_WIFI +# define NM_DEVICE_TYPE_WIFI 2 +#endif + +// https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NM80211ApFlags +// Wifi network has no security +#ifndef NM_802_11_AP_SEC_NONE +# define NM_802_11_AP_SEC_NONE 0x00000000 +#endif + +// Wifi network has WEP (40 bits) +#ifndef NM_802_11_AP_SEC_PAIR_WEP40 +# define NM_802_11_AP_SEC_PAIR_WEP40 0x00000001 +#endif + +// Wifi network has WEP (104 bits) +#ifndef NM_802_11_AP_SEC_PAIR_WEP104 +# define NM_802_11_AP_SEC_PAIR_WEP104 0x00000002 +#endif + +#define NM_802_11_AP_SEC_WEAK_CRYPTO \ + (NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104) + +constexpr const char* DBUS_NETWORKMANAGER = "org.freedesktop.NetworkManager"; + +namespace { +Logger logger("LinuxNetworkWatcherWorker"); +} + +static inline bool checkUnsecureFlags(int rsnFlags, int wpaFlags) { + // If neither WPA nor WPA2/RSN are supported, then the network is unencrypted + if (rsnFlags == NM_802_11_AP_SEC_NONE && wpaFlags == NM_802_11_AP_SEC_NONE) { + return false; + } + + // Consider the user of weak cryptography to be unsecure + if ((rsnFlags & NM_802_11_AP_SEC_WEAK_CRYPTO) || + (wpaFlags & NM_802_11_AP_SEC_WEAK_CRYPTO)) { + return false; + } + // Otherwise, the network is secured with reasonable cryptography + return true; +} + +LinuxNetworkWatcherWorker::LinuxNetworkWatcherWorker(QThread* thread) { + MZ_COUNT_CTOR(LinuxNetworkWatcherWorker); + moveToThread(thread); +} + +LinuxNetworkWatcherWorker::~LinuxNetworkWatcherWorker() { + MZ_COUNT_DTOR(LinuxNetworkWatcherWorker); +} + +void LinuxNetworkWatcherWorker::initialize() { + logger.debug() << "initialize"; + + logger.debug() + << "Retrieving the list of wifi network devices from NetworkManager"; + + // To know the NeworkManager DBus methods and properties, read the official + // documentation: + // https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html + + QDBusInterface nm(DBUS_NETWORKMANAGER, "/org/freedesktop/NetworkManager", + DBUS_NETWORKMANAGER, QDBusConnection::systemBus()); + if (!nm.isValid()) { + logger.error() + << "Failed to connect to the network manager via system dbus"; + return; + } + + QDBusMessage msg = nm.call("GetDevices"); + QDBusArgument arg = msg.arguments().at(0).value(); + if (arg.currentType() != QDBusArgument::ArrayType) { + logger.error() << "Expected an array of devices"; + return; + } + + QList paths = qdbus_cast >(arg); + for (const QDBusObjectPath& path : paths) { + QString devicePath = path.path(); + QDBusInterface device(DBUS_NETWORKMANAGER, devicePath, + "org.freedesktop.NetworkManager.Device", + QDBusConnection::systemBus()); + if (device.property("DeviceType").toInt() != NM_DEVICE_TYPE_WIFI) { + continue; + } + + logger.debug() << "Found a wifi device:" << devicePath; + m_devicePaths.append(devicePath); + + // Here we monitor the changes. + QDBusConnection::systemBus().connect( + DBUS_NETWORKMANAGER, devicePath, "org.freedesktop.DBus.Properties", + "PropertiesChanged", this, + SLOT(propertyChanged(QString, QVariantMap, QStringList))); + } + + if (m_devicePaths.isEmpty()) { + logger.warning() << "No wifi devices found"; + return; + } + + // We could be already be activated. + checkDevices(); +} + +void LinuxNetworkWatcherWorker::propertyChanged(QString interface, + QVariantMap properties, + QStringList list) { + Q_UNUSED(list); + + logger.debug() << "Properties changed for interface" << interface; + + if (!properties.contains("ActiveAccessPoint")) { + logger.debug() << "Access point did not changed. Ignoring the changes"; + return; + } + + checkDevices(); +} + +void LinuxNetworkWatcherWorker::checkDevices() { + logger.debug() << "Checking devices"; + + for (const QString& devicePath : m_devicePaths) { + QDBusInterface wifiDevice(DBUS_NETWORKMANAGER, devicePath, + "org.freedesktop.NetworkManager.Device.Wireless", + QDBusConnection::systemBus()); + + // Check the access point path + QString accessPointPath = wifiDevice.property("ActiveAccessPoint") + .value() + .path(); + if (accessPointPath.isEmpty()) { + logger.warning() << "No access point found"; + continue; + } + + QDBusInterface ap(DBUS_NETWORKMANAGER, accessPointPath, + "org.freedesktop.NetworkManager.AccessPoint", + QDBusConnection::systemBus()); + + QVariant rsnFlags = ap.property("RsnFlags"); + QVariant wpaFlags = ap.property("WpaFlags"); + if (!rsnFlags.isValid() || !wpaFlags.isValid()) { + // We are probably not connected. + continue; + } + + if (!checkUnsecureFlags(rsnFlags.toInt(), wpaFlags.toInt())) { + QString ssid = ap.property("Ssid").toString(); + QString bssid = ap.property("HwAddress").toString(); + + // We have found 1 unsecured network. We don't need to check other wifi + // network devices. + logger.warning() << "Unsecured AP detected!" + << "rsnFlags:" << rsnFlags.toInt() + << "wpaFlags:" << wpaFlags.toInt() + << "ssid:" << logger.sensitive(ssid); + emit unsecuredNetwork(ssid, bssid); + break; + } + } +} diff --git a/client/platforms/linux/linuxnetworkwatcherworker.h b/client/platforms/linux/linuxnetworkwatcherworker.h new file mode 100644 index 00000000..cc4c6a36 --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcherworker.h @@ -0,0 +1,41 @@ +/* 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 LINUXNETWORKWATCHERWORKER_H +#define LINUXNETWORKWATCHERWORKER_H + +#include +#include +#include + +class QThread; + +class LinuxNetworkWatcherWorker final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(LinuxNetworkWatcherWorker) + + public: + explicit LinuxNetworkWatcherWorker(QThread* thread); + ~LinuxNetworkWatcherWorker(); + + void checkDevices(); + + signals: + void unsecuredNetwork(const QString& networkName, const QString& networkId); + + public slots: + void initialize(); + + private slots: + void propertyChanged(QString interface, QVariantMap properties, + QStringList list); + + private: + // We collect the list of DBus wifi network device paths during the + // initialization. When a property of them changes, we check if the access + // point is active and unsecure. + QStringList m_devicePaths; +}; + +#endif // LINUXNETWORKWATCHERWORKER_H diff --git a/client/protocols/wireguardprotocol.cpp b/client/protocols/wireguardprotocol.cpp index 7466d1af..a4005efb 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -17,9 +17,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject * writeWireguardConfiguration(configuration); // MZ -#if defined(MZ_LINUX) - //m_impl.reset(new LinuxController()); -#elif defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) m_impl.reset(new LocalSocketController()); connect(m_impl.get(), &ControllerImpl::connected, this, [this](const QString &pubkey, const QDateTime &connectionTimestamp) { @@ -39,7 +37,7 @@ WireguardProtocol::~WireguardProtocol() void WireguardProtocol::stop() { -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) stopMzImpl(); return; #endif @@ -100,7 +98,7 @@ void WireguardProtocol::stop() setConnectionState(Vpn::ConnectionState::Disconnected); } -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) ErrorCode WireguardProtocol::startMzImpl() { @@ -128,7 +126,7 @@ void WireguardProtocol::writeWireguardConfiguration(const QJsonObject &configura m_configFile.write(jConfig.value(config_key::config).toString().toUtf8()); m_configFile.close(); -#ifdef Q_OS_LINUX +#if 0 if (IpcClient::Interface()) { QRemoteObjectPendingReply result = IpcClient::Interface()->copyWireguardConfig(m_configFile.fileName()); if (result.returnValue()) { @@ -174,7 +172,7 @@ ErrorCode WireguardProtocol::start() return lastError(); } -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) return startMzImpl(); #endif diff --git a/client/protocols/wireguardprotocol.h b/client/protocols/wireguardprotocol.h index 6f530758..dea8d6d9 100644 --- a/client/protocols/wireguardprotocol.h +++ b/client/protocols/wireguardprotocol.h @@ -23,7 +23,7 @@ public: ErrorCode start() override; void stop() override; -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) ErrorCode startMzImpl(); ErrorCode stopMzImpl(); #endif @@ -47,7 +47,7 @@ private: bool m_isConfigLoaded = false; -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) QScopedPointer m_impl; #endif }; diff --git a/deploy/PrivacyTechAppleCertDeveloperId.p12 b/deploy/PrivacyTechAppleCertDeveloperId.p12 deleted file mode 100755 index a04ec85a..00000000 Binary files a/deploy/PrivacyTechAppleCertDeveloperId.p12 and /dev/null differ diff --git a/deploy/PrivacyTechAppleCertInstallerId.p12 b/deploy/PrivacyTechAppleCertInstallerId.p12 deleted file mode 100755 index ee9a34e9..00000000 Binary files a/deploy/PrivacyTechAppleCertInstallerId.p12 and /dev/null differ diff --git a/deploy/PrivacyTechWindowsCert.pfx b/deploy/PrivacyTechWindowsCert.pfx deleted file mode 100644 index 42c49a33..00000000 Binary files a/deploy/PrivacyTechWindowsCert.pfx and /dev/null differ diff --git a/deploy/WWDRCA.cer b/deploy/WWDRCA.cer deleted file mode 100644 index d2bb1da6..00000000 Binary files a/deploy/WWDRCA.cer and /dev/null differ diff --git a/deploy/build_ios.sh b/deploy/build_ios.sh index c4c0691a..7f16b916 100755 --- a/deploy/build_ios.sh +++ b/deploy/build_ios.sh @@ -34,7 +34,7 @@ clang -v # Generate XCodeProj $QT_BIN_DIR/qt-cmake . -B $BUILD_DIR -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -KEYCHAIN=amnezia.build.keychain +KEYCHAIN=amnezia.build.ios.keychain KEYCHAIN_FILE=$HOME/Library/Keychains/${KEYCHAIN}-db # Setup keychain diff --git a/deploy/build_macos.sh b/deploy/build_macos.sh index 54b6dbe3..700198e7 100755 --- a/deploy/build_macos.sh +++ b/deploy/build_macos.sh @@ -79,7 +79,7 @@ if [ "${MAC_CERT_PW+x}" ]; then CERTIFICATE_P12=$DEPLOY_DIR/PrivacyTechAppleCertDeveloperId.p12 WWDRCA=$DEPLOY_DIR/WWDRCA.cer - KEYCHAIN=amnezia.build.keychain + KEYCHAIN=amnezia.build.macos.keychain TEMP_PASS=tmp_pass security create-keychain -p $TEMP_PASS $KEYCHAIN || true diff --git a/deploy/build_windows.bat b/deploy/build_windows.bat index b6892f3d..7ae3e9f6 100644 --- a/deploy/build_windows.bat +++ b/deploy/build_windows.bat @@ -14,41 +14,31 @@ set PROJECT_DIR=%cd% set SCRIPT_DIR=%PROJECT_DIR:"=%\deploy set WORK_DIR=%SCRIPT_DIR:"=%\build_%BUILD_ARCH:"=% -rmdir /Q /S %WORK_DIR% -mkdir %WORK_DIR% - - set APP_NAME=AmneziaVPN set APP_FILENAME=%APP_NAME:"=%.exe set APP_DOMAIN=org.amneziavpn.package -set RELEASE_DIR=%WORK_DIR:"=% -set OUT_APP_DIR=%RELEASE_DIR:"=%\client\release -set PREBILT_DEPLOY_DATA_DIR=%SCRIPT_DIR:"=%\data\deploy-prebuilt\windows\x%BUILD_ARCH:"=% +set OUT_APP_DIR=%WORK_DIR:"=%\client\release +set PREBILT_DEPLOY_DATA_DIR=%PROJECT_DIR:"=%\client\3rd-prebuilt\deploy-prebuilt\windows\x%BUILD_ARCH:"=% set DEPLOY_DATA_DIR=%SCRIPT_DIR:"=%\data\windows\x%BUILD_ARCH:"=% -set INSTALLER_DATA_DIR=%RELEASE_DIR:"=%\installer\packages\%APP_DOMAIN:"=%\data +set INSTALLER_DATA_DIR=%WORK_DIR:"=%\installer\packages\%APP_DOMAIN:"=%\data set TARGET_FILENAME=%PROJECT_DIR:"=%\%APP_NAME:"=%_x%BUILD_ARCH:"=%.exe echo "Environment:" +echo "WORK_DIR: %WORK_DIR%" echo "APP_FILENAME: %APP_FILENAME%" echo "PROJECT_DIR: %PROJECT_DIR%" echo "SCRIPT_DIR: %SCRIPT_DIR%" -echo "RELEASE_DIR: %RELEASE_DIR%" echo "OUT_APP_DIR: %OUT_APP_DIR%" echo "DEPLOY_DATA_DIR: %DEPLOY_DATA_DIR%" echo "INSTALLER_DATA_DIR: %INSTALLER_DATA_DIR%" -echo "QMAKE_STASH_FILE: %QMAKE_STASH_FILE%" echo "TARGET_FILENAME: %TARGET_FILENAME%" -rem Signing staff -powershell Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope LocalMachine -powershell Get-ExecutionPolicy -List - -powershell Import-PfxCertificate -FilePath %SCRIPT_DIR:"=%\PrivacyTechWindowsCert.pfx -CertStoreLocation Cert:\LocalMachine\My -Password $(ConvertTo-SecureString -String $Env:WIN_CERT_PW -AsPlainText -Force) - echo "Cleanup..." -Rmdir /Q /S %RELEASE_DIR% +rmdir /Q /S %WORK_DIR% Del %TARGET_FILENAME% +mkdir %WORK_DIR% + call "%QT_BIN_DIR:"=%\qt-cmake" --version "%QT_BIN_DIR:"=%\windeployqt" -v cmake --version @@ -69,11 +59,11 @@ copy "%WORK_DIR:"=%\service\server\release\%APP_NAME:"=%-service.exe" %OUT_APP_D echo "Signing exe" cd %OUT_APP_DIR% -signtool sign /v /sm /s My /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe - +signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe + "%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%" -signtool sign /v /sm /s My /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll +signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll echo "Copying deploy data..." xcopy %DEPLOY_DATA_DIR% %OUT_APP_DIR% /s /e /y /i /f @@ -81,7 +71,7 @@ xcopy %PREBILT_DEPLOY_DATA_DIR% %OUT_APP_DIR% /s /e /y /i /f copy "%WORK_DIR:"=%\service\wireguard-service\release\wireguard-service.exe" %OUT_APP_DIR%\wireguard\ cd %SCRIPT_DIR% -xcopy %SCRIPT_DIR:"=%\installer %RELEASE_DIR:"=%\installer /s /e /y /i /f +xcopy %SCRIPT_DIR:"=%\installer %WORK_DIR:"=%\installer /s /e /y /i /f mkdir %INSTALLER_DATA_DIR% echo "Deploy finished, content:" @@ -91,14 +81,14 @@ cd %OUT_APP_DIR% echo "Compressing data..." "%QIF_BIN_DIR:"=%\archivegen" -c 9 %INSTALLER_DATA_DIR:"=%\%APP_NAME:"=%.7z . -cd "%RELEASE_DIR:"=%\installer" +cd "%WORK_DIR:"=%\installer" echo "Creating installer..." "%QIF_BIN_DIR:"=%\binarycreator" --offline-only -v -c config\windows.xml -p packages -f %TARGET_FILENAME% timeout 5 cd %PROJECT_DIR% -signtool sign /v /sm /s My /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 "%TARGET_FILENAME%" +signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 "%TARGET_FILENAME%" echo "Finished, see %TARGET_FILENAME%" exit 0 diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index bce653fd..e34f5ca3 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -6,9 +6,10 @@ project(${PROJECT}) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Core Network Widgets RemoteObjects Core5Compat) +find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat) qt_standard_project_setup() + configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) set(HEADERS @@ -91,7 +92,7 @@ if(UNIX) ) endif() -if (WIN32 OR APPLE) +if (WIN32 OR APPLE OR LINUX) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/../../client/daemon/daemon.h ${CMAKE_CURRENT_LIST_DIR}/../../client/daemon/daemonlocalserver.h @@ -198,12 +199,30 @@ if(APPLE) endif() if(LINUX) + set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/router_linux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcher.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcherworker.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxdependencies.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/iputilslinux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dbustypeslinux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxdaemon.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dnsutilslinux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/wireguardutilslinux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxroutemonitor.h ) set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/router_linux.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcherworker.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxdependencies.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dnsutilslinux.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/iputilslinux.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxdaemon.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/wireguardutilslinux.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxroutemonitor.cpp ) endif() @@ -217,7 +236,7 @@ include_directories( ) add_executable(${PROJECT} ${SOURCES} ${HEADERS}) -target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat ${LIBS}) +target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus ${LIBS}) target_compile_definitions(${PROJECT} PRIVATE "MZ_$") if(CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/service/server/localserver.cpp b/service/server/localserver.cpp index 709ad693..3e1b0954 100644 --- a/service/server/localserver.cpp +++ b/service/server/localserver.cpp @@ -40,12 +40,16 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), } }); -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) // Init Mozilla Wireguard Daemon if (!server.initialize()) { logger.error() << "Failed to initialize the server"; return; } + +#ifdef Q_OS_LINUX + // Signal handling for a proper shutdown. + QObject::connect(qApp, &QCoreApplication::aboutToQuit, + []() { LinuxDaemon::instance()->deactivate(); }); #endif #ifdef Q_OS_MAC diff --git a/service/server/localserver.h b/service/server/localserver.h index b5264120..4a6648a5 100644 --- a/service/server/localserver.h +++ b/service/server/localserver.h @@ -10,13 +10,18 @@ #include "ipcserver.h" -#ifdef Q_OS_WIN #include "../../client/daemon/daemonlocalserver.h" + + +#ifdef Q_OS_WIN #include "windows/daemon/windowsdaemon.h" #endif +#ifdef Q_OS_LINUX +#include "linux/daemon/linuxdaemon.h" +#endif + #ifdef Q_OS_MAC -#include "../../client/daemon/daemonlocalserver.h" #include "macos/daemon/macosdaemon.h" #endif @@ -31,13 +36,14 @@ class LocalServer : public QObject public: explicit LocalServer(QObject* parent = nullptr); ~LocalServer(); - QSharedPointer m_server; - IpcServer m_ipcServer; QRemoteObjectHost m_serverNode; bool m_isRemotingEnabled = false; - +#ifdef Q_OS_LINUX + DaemonLocalServer server{qApp}; + LinuxDaemon daemon; +#endif #ifdef Q_OS_WIN DaemonLocalServer server{qApp}; WindowsDaemon daemon; diff --git a/service/server/router_linux.cpp b/service/server/router_linux.cpp index 3517f8e6..9410b146 100644 --- a/service/server/router_linux.cpp +++ b/service/server/router_linux.cpp @@ -54,9 +54,9 @@ bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const if (int err = ioctl(sock, SIOCADDRT, &route) < 0) { qDebug().noquote() << "route add error: gw " - << ((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr - << " ip " << ((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr - << " mask " << ((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr << " " << err; + << ((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr + << " ip " << ((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr + << " mask " << ((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr << " " << err; return false; } diff --git a/service/server/router_linux.h b/service/server/router_linux.h index 6da20b7d..5b4897bd 100644 --- a/service/server/router_linux.h +++ b/service/server/router_linux.h @@ -27,6 +27,7 @@ public: bool clearSavedRoutes(); bool routeDelete(const QString &ip, const QString &gw, const int &sock); bool routeDeleteList(const QString &gw, const QStringList &ips); + QString getgatewayandiface(); void flushDns(); public slots: