diff --git a/client/daemon/daemonlocalserver.cpp b/client/daemon/daemonlocalserver.cpp index 02a12cb99..9d8feb68d 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."; @@ -76,12 +77,12 @@ QString DaemonLocalServer::daemonPath() const { } if (dir.exists("amneziavpn")) { - logger.debug() << "/var/run/amneziavpn seems to be usable"; + logger.debug() << "/var/run/amnezia seems to be usable"; return VAR_PATH; } if (!dir.mkdir("amneziavpn")) { - logger.warning() << "Failed to create /var/run/amneziavpn"; + logger.warning() << "Failed to create /var/run/amnezia"; return TMP_PATH; } @@ -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 54beb11c0..47fdb6227 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 7c3377552..1f84ad077 100644 --- a/client/mozilla/shared/ipaddress.cpp +++ b/client/mozilla/shared/ipaddress.cpp @@ -30,7 +30,7 @@ IPAddress::IPAddress(const QString& ip) { m_prefixLength = 128; } } else { - Q_ASSERT(false); + // Q_ASSERT(false); } } diff --git a/client/platforms/linux/daemon/dbustypeslinux.h b/client/platforms/linux/daemon/dbustypeslinux.h new file mode 100644 index 000000000..1a5e44e26 --- /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 000000000..cc47202b5 --- /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 000000000..e4bbd2734 --- /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 000000000..9a51caad1 --- /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 000000000..38edf1778 --- /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 000000000..7c2d95dba --- /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 000000000..7f5d27b7e --- /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 000000000..80f510b79 --- /dev/null +++ b/client/platforms/linux/daemon/linuxroutemonitor.cpp @@ -0,0 +1,133 @@ +/* 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 "router_linux.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +namespace { +Logger logger("LinuxRouteMonitor"); +} // namespace + +LinuxRouteMonitor::LinuxRouteMonitor(const QString& ifname, QObject* parent) + : QObject(parent), m_ifname(ifname) { + MZ_COUNT_CTOR(LinuxRouteMonitor); + logger.debug() << "LinuxRouteMonitor created."; + + m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); + if (m_rtsock < 0) { + logger.error() << "Failed to create routing socket:" << strerror(errno); + return; + } + + RouterLinux &router = RouterLinux::Instance(); + m_defaultGatewayIpv4 = router.getgatewayandiface().toUtf8(); + + m_ifindex = if_nametoindex(qPrintable(ifname)); + m_notifier = new QSocketNotifier(m_rtsock, QSocketNotifier::Read, this); +} + +LinuxRouteMonitor::~LinuxRouteMonitor() { + MZ_COUNT_DTOR(LinuxRouteMonitor); + flushExclusionRoutes(); + if (m_rtsock >= 0) { + close(m_rtsock); + } + logger.debug() << "LinuxRouteMonitor 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) { + int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + + struct ifreq ifc; + int res; + + if(temp_sock < 0) + return -1; + strcpy(ifc.ifr_name, m_ifname.toUtf8()); + + res = ioctl(temp_sock, SIOCGIFADDR, &ifc); + if(res < 0) + return -1; + + RouterLinux &router = RouterLinux::Instance(); + logger.debug() << "prefix.toString() " << prefix.toString() << " m_ifname " << inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr); + return router.routeAdd(prefix.toString(), inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr), temp_sock); +} + +bool LinuxRouteMonitor::deleteRoute(const IPAddress& prefix) { + int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + + struct ifreq ifc; + int res; + if(temp_sock < 0) + temp_sock -1; + strcpy(ifc.ifr_name, m_ifname.toUtf8()); + + res = ioctl(temp_sock, SIOCGIFADDR, &ifc); + if(res < 0) + return -1; + + RouterLinux &router = RouterLinux::Instance(); + logger.debug() << "prefix.toString() " << prefix.toString() << " m_ifname " << inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr); + return router.routeDelete(prefix.toString(), inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr), temp_sock); +} + +bool LinuxRouteMonitor::addExclusionRoute(const IPAddress& prefix) { + logger.debug() << "Adding exclusion route for" + << logger.sensitive(prefix.toString()); + + int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + RouterLinux &router = RouterLinux::Instance(); + logger.debug() << "prefix.toString() " << prefix.toString() << " m_defaultGatewayIpv4 " << m_defaultGatewayIpv4; + return router.routeAdd(prefix.toString(), m_defaultGatewayIpv4, temp_sock); + // Otherwise, the default route isn't known yet. Do nothing. + return true; +} + +bool LinuxRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { + logger.debug() << "Deleting exclusion route for" + << logger.sensitive(prefix.toString()); + + int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + RouterLinux &router = RouterLinux::Instance(); + logger.debug() << "prefix.toString() " << prefix.toString() << " m_defaultGatewayIpv4 " << m_defaultGatewayIpv4; + return router.routeDelete(prefix.toString(), m_defaultGatewayIpv4, temp_sock); +} + +void LinuxRouteMonitor::flushExclusionRoutes() { + RouterLinux &router = RouterLinux::Instance(); + router.clearSavedRoutes(); +} diff --git a/client/platforms/linux/daemon/linuxroutemonitor.h b/client/platforms/linux/daemon/linuxroutemonitor.h new file mode 100644 index 000000000..872fcb7e4 --- /dev/null +++ b/client/platforms/linux/daemon/linuxroutemonitor.h @@ -0,0 +1,53 @@ +/* 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" + +struct if_msghdr; +struct rt_msghdr; +struct sockaddr; + +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); + int interfaceFlags() { return m_ifflags; } + + bool addExclusionRoute(const IPAddress& prefix); + bool deleteExclusionRoute(const IPAddress& prefix); + void flushExclusionRoutes(); + + private: + static QString addrToString(const struct sockaddr* sa); + static QString addrToString(const QByteArray& data); + + QList m_exclusionRoutes; + QByteArray m_defaultGatewayIpv4; + QByteArray m_defaultGatewayIpv6; + unsigned int m_defaultIfindexIpv4 = 0; + unsigned int m_defaultIfindexIpv6 = 0; + + QString m_ifname; + unsigned int m_ifindex = 0; + int m_ifflags = 0; + int m_rtsock = -1; + int m_rtseq = 0; + QSocketNotifier* m_notifier = nullptr; +}; + +#endif // LINUXROUTEMONITOR_H diff --git a/client/platforms/linux/daemon/pidtracker.cpp b/client/platforms/linux/daemon/pidtracker.cpp new file mode 100644 index 000000000..76d53219a --- /dev/null +++ b/client/platforms/linux/daemon/pidtracker.cpp @@ -0,0 +1,228 @@ +/* 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 "pidtracker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +constexpr size_t CN_MCAST_MSG_SIZE = + sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op); + +namespace { +Logger logger("PidTracker"); +} + +PidTracker::PidTracker(QObject* parent) : QObject(parent) { + MZ_COUNT_CTOR(PidTracker); + logger.debug() << "PidTracker created."; + + m_nlsock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR); + if (m_nlsock < 0) { + logger.error() << "Failed to create netlink socket:" << strerror(errno); + return; + } + + struct sockaddr_nl nladdr; + nladdr.nl_family = AF_NETLINK; + nladdr.nl_groups = CN_IDX_PROC; + nladdr.nl_pid = getpid(); + nladdr.nl_pad = 0; + if (bind(m_nlsock, (struct sockaddr*)&nladdr, sizeof(nladdr)) < 0) { + logger.error() << "Failed to bind netlink socket:" << strerror(errno); + close(m_nlsock); + m_nlsock = -1; + return; + } + + char buf[NLMSG_SPACE(CN_MCAST_MSG_SIZE)]; + struct nlmsghdr* nlmsg = (struct nlmsghdr*)buf; + struct cn_msg* cnmsg = (struct cn_msg*)NLMSG_DATA(nlmsg); + enum proc_cn_mcast_op mcast_op = PROC_CN_MCAST_LISTEN; + + memset(buf, 0, sizeof(buf)); + nlmsg->nlmsg_len = NLMSG_LENGTH(CN_MCAST_MSG_SIZE); + nlmsg->nlmsg_type = NLMSG_DONE; + nlmsg->nlmsg_flags = 0; + nlmsg->nlmsg_seq = 0; + nlmsg->nlmsg_pid = getpid(); + + cnmsg->id.idx = CN_IDX_PROC; + cnmsg->id.val = CN_VAL_PROC; + cnmsg->seq = 0; + cnmsg->ack = 0; + cnmsg->len = sizeof(mcast_op); + memcpy(cnmsg->data, &mcast_op, sizeof(mcast_op)); + + if (send(m_nlsock, nlmsg, sizeof(buf), 0) != sizeof(buf)) { + logger.error() << "Failed to send netlink message:" << strerror(errno); + close(m_nlsock); + m_nlsock = -1; + return; + } + + m_socket = new QSocketNotifier(m_nlsock, QSocketNotifier::Read, this); + connect(m_socket, &QSocketNotifier::activated, this, &PidTracker::readData); +} + +PidTracker::~PidTracker() { + MZ_COUNT_DTOR(PidTracker); + logger.debug() << "PidTracker destroyed."; + + m_processTree.clear(); + while (!m_processGroups.isEmpty()) { + ProcessGroup* group = m_processGroups.takeFirst(); + delete group; + } + + if (m_nlsock > 0) { + close(m_nlsock); + } +} + +ProcessGroup* PidTracker::track(const QString& name, int rootpid) { + ProcessGroup* group = m_processTree.value(rootpid, nullptr); + if (group) { + logger.warning() << "Ignoring attempt to track duplicate PID"; + return group; + } + group = new ProcessGroup(name, rootpid); + group->kthreads[rootpid] = 1; + group->refcount = 1; + + m_processGroups.append(group); + m_processTree[rootpid] = group; + + return group; +} + +void PidTracker::handleProcEvent(struct cn_msg* cnmsg) { + struct proc_event* ev = (struct proc_event*)cnmsg->data; + + if (ev->what == proc_event::PROC_EVENT_FORK) { + auto forkdata = &ev->event_data.fork; + /* If the child process already exists, track a new kernel thread. */ + ProcessGroup* group = m_processTree.value(forkdata->child_tgid, nullptr); + if (group) { + group->kthreads[forkdata->child_tgid]++; + return; + } + + /* Track a new userspace process if was forked from a known parent. */ + group = m_processTree.value(forkdata->parent_tgid, nullptr); + if (!group) { + return; + } + m_processTree[forkdata->child_tgid] = group; + group->kthreads[forkdata->child_tgid] = 1; + group->refcount++; + emit pidForked(group->name, forkdata->parent_tgid, forkdata->child_tgid); + } + + if (ev->what == proc_event::PROC_EVENT_EXIT) { + auto exitdata = &ev->event_data.exit; + ProcessGroup* group = m_processTree.value(exitdata->process_tgid, nullptr); + if (!group) { + return; + } + + /* Decrement the number of kernel threads in this userspace process. */ + uint threadcount = group->kthreads.value(exitdata->process_tgid, 0); + if (threadcount == 0) { + return; + } + if (threadcount > 1) { + group->kthreads[exitdata->process_tgid] = threadcount - 1; + return; + } + group->kthreads.remove(exitdata->process_tgid); + + /* A userspace process exits when all of its kernel threads exit. */ + Q_ASSERT(group->refcount > 0); + group->refcount--; + if (group->refcount == 0) { + emit terminated(group->name, group->rootpid); + m_processGroups.removeAll(group); + delete group; + } + } +} + +void PidTracker::readData() { + struct sockaddr_nl src; + socklen_t srclen = sizeof(src); + ssize_t recvlen; + + recvlen = recvfrom(m_nlsock, m_readBuf, sizeof(m_readBuf), MSG_DONTWAIT, + (struct sockaddr*)&src, &srclen); + if (recvlen == ENOBUFS) { + logger.error() + << "Failed to read netlink socket: buffer full, message dropped"; + return; + } + if (recvlen < 0) { + logger.error() << "Failed to read netlink socket:" << strerror(errno); + return; + } + if (srclen != sizeof(src)) { + logger.error() << "Failed to read netlink socket: invalid address length"; + return; + } + + /* We are only interested in process-control messages from the kernel */ + if ((src.nl_groups != CN_IDX_PROC) || (src.nl_pid != 0)) { + return; + } + + /* Handle the process-control messages. */ + struct nlmsghdr* msg; + for (msg = (struct nlmsghdr*)m_readBuf; NLMSG_OK(msg, recvlen); + msg = NLMSG_NEXT(msg, recvlen)) { + struct cn_msg* cnmsg = (struct cn_msg*)NLMSG_DATA(msg); + if (msg->nlmsg_type == NLMSG_NOOP) { + continue; + } + if ((msg->nlmsg_type == NLMSG_ERROR) || + (msg->nlmsg_type == NLMSG_OVERRUN)) { + break; + } + handleProcEvent(cnmsg); + if (msg->nlmsg_type == NLMSG_DONE) { + break; + } + } +} + +bool ProcessGroup::moveToCgroup(const QString& name) { + /* Do nothing if Cgroups are not supported. */ + if (name.isNull()) { + return true; + } + + QString cgProcsFile = name + "/cgroup.procs"; + FILE* fp = fopen(qPrintable(cgProcsFile), "w"); + if (!fp) { + return false; + } + + for (auto iterator = kthreads.constBegin(); iterator != kthreads.constEnd(); + ++iterator) { + fprintf(fp, "%d\n", iterator.key()); + fflush(fp); + } + fclose(fp); + return true; +} diff --git a/client/platforms/linux/daemon/pidtracker.h b/client/platforms/linux/daemon/pidtracker.h new file mode 100644 index 000000000..dc632b8bc --- /dev/null +++ b/client/platforms/linux/daemon/pidtracker.h @@ -0,0 +1,72 @@ +/* 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 PIDTRACKER_H +#define PIDTRACKER_H + +#include +#include +#include +#include + +#include "leakdetector.h" + +struct cn_msg; + +class ProcessGroup { + public: + ProcessGroup(const QString& groupName, int groupRootPid, + const QString& groupState = "active") { + MZ_COUNT_CTOR(ProcessGroup); + name = groupName; + rootpid = groupRootPid; + state = groupState; + refcount = 0; + } + ~ProcessGroup() { MZ_COUNT_DTOR(ProcessGroup); } + + bool moveToCgroup(const QString& name); + + QHash kthreads; + QString name; + QString state; + int rootpid; + int refcount; +}; + +class PidTracker final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(PidTracker) + + public: + explicit PidTracker(QObject* parent); + ~PidTracker(); + + ProcessGroup* track(const QString& name, int rootpid); + + QList pids() { return m_processTree.keys(); } + QList::iterator begin() { return m_processGroups.begin(); } + QList::iterator end() { return m_processGroups.end(); } + ProcessGroup* group(int pid) { return m_processTree.value(pid); } + + signals: + void pidForked(const QString& name, int parent, int child); + void pidExited(const QString& name, int pid); + void terminated(const QString& name, int rootpid); + + private: + void handleProcEvent(struct cn_msg*); + + private slots: + void readData(); + + private: + int m_nlsock; + char m_readBuf[2048]; + QSocketNotifier* m_socket = nullptr; + QHash m_processTree; + QList m_processGroups; +}; + +#endif // PIDTRACKER_H diff --git a/client/platforms/linux/daemon/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp new file mode 100644 index 000000000..32150ad5d --- /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("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 000000000..a8320c95e --- /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 000000000..bd4e383a5 --- /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 000000000..3a4cb2591 --- /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 000000000..70ff6f18b --- /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 000000000..c8ae0fea3 --- /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 000000000..ed8c76bab --- /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 000000000..19ed32516 --- /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 000000000..cc4c6a366 --- /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 3091655d6..91903b2b1 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -16,9 +16,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) { emit connectionStateChanged(VpnProtocol::Connected); @@ -38,7 +36,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 @@ -98,7 +96,7 @@ void WireguardProtocol::stop() setConnectionState(VpnProtocol::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() { @@ -126,7 +124,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()) { @@ -171,7 +169,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 6f5307587..dea8d6d9d 100644 --- a/client/protocols/wireguardprotocol.h +++ b/client/protocols/wireguardprotocol.h @@ -23,7 +23,7 @@ class WireguardProtocol : public VpnProtocol 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 @@ class WireguardProtocol : public VpnProtocol 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/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 72c5b09b3..20ed8cb60 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,32 @@ 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/pidtracker.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/pidtracker.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 +238,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 709ad6939..3e1b09548 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 b5264120c..4a6648a55 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 3517f8e6b..d717ce9c1 100644 --- a/service/server/router_linux.cpp +++ b/service/server/router_linux.cpp @@ -14,6 +14,17 @@ #include #include #include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include RouterLinux &RouterLinux::Instance() @@ -22,6 +33,123 @@ RouterLinux &RouterLinux::Instance() return s; } +#define BUFFER_SIZE 4096 + +QString RouterLinux::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)) { + qDebug().noquote() << "Gateway " << gateway_address << " for interface " << interface; + break; + } + } + close(sock); + return gateway_address; +} + + bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const int &sock) { QString ip = Utils::ipAddressFromIpWithSubnet(ipWithSubnet); @@ -29,7 +157,7 @@ bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const if (!Utils::checkIPv4Format(ip) || !Utils::checkIPv4Format(gw)) { qCritical().noquote() << "Critical, trying to add invalid route: " << ip << gw; - return false; + return true; } struct rtentry route; @@ -53,11 +181,11 @@ 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; - return false; + // 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; + // return false; } m_addedRoutes.append({ipWithSubnet, gw}); @@ -99,7 +227,7 @@ bool RouterLinux::routeDelete(const QString &ipWithSubnet, const QString &gw, co if (!Utils::checkIPv4Format(ip) || !Utils::checkIPv4Format(gw)) { qCritical().noquote() << "Critical, trying to remove invalid route: " << ip << gw; - return false; + return true; } if (ip == "0.0.0.0") { @@ -129,8 +257,8 @@ bool RouterLinux::routeDelete(const QString &ipWithSubnet, const QString &gw, co if (ioctl(sock, SIOCDELRT, &route) < 0) { - qDebug().noquote() << "route delete error: gw " << gw << " ip " << ip << " mask " << mask; - return false; + // qDebug().noquote() << "route delete error: gw " << gw << " ip " << ip << " mask " << mask; + // return false; } return true; } diff --git a/service/server/router_linux.h b/service/server/router_linux.h index 6da20b7d6..5b4897bde 100644 --- a/service/server/router_linux.h +++ b/service/server/router_linux.h @@ -27,6 +27,7 @@ class RouterLinux : public QObject 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: