Skip to content

Commit

Permalink
Linux: Re-introduce D-Bus API Authorization (#7110)
Browse files Browse the repository at this point in the history
* Check for CAP_NET_ADMIN to auth D-Bus calls.
* Add libcap as a build dependency
* Require D-Bus authorization for controller APIs
* Permit API calls originating from the daemon itself.
* Drop daemon permissions after launch.
* Apply auth check to split tunneling APIs too
* Restrict API access by UID
  • Loading branch information
oskirby authored and lesleyjanenorton committed Aug 4, 2023
1 parent 461dc5e commit 5240754
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 2 deletions.
1 change: 1 addition & 0 deletions linux/debian/control
Expand Up @@ -13,6 +13,7 @@ Build-Depends: debhelper (>= 11.2),
python3-yaml,
python3-jinja2,
python3-click,
libcap-dev,
libgl-dev,
libopengl-dev (>= 1.3.0~),
libqt6core5compat6-dev (>=6.2.0~),
Expand Down
1 change: 1 addition & 0 deletions linux/mozillavpn.spec
Expand Up @@ -17,6 +17,7 @@ Requires: wireguard-tools

BuildRequires: cargo
BuildRequires: golang >= 1.18
BuildRequires: libcap-devel
BuildRequires: libsecret-devel
BuildRequires: openssl-devel
BuildRequires: python3-yaml
Expand Down
3 changes: 2 additions & 1 deletion src/apps/vpn/cmake/linux.cmake
Expand Up @@ -7,7 +7,8 @@ target_link_libraries(mozillavpn PRIVATE Qt6::DBus)

find_package(PkgConfig REQUIRED)
pkg_check_modules(libsecret REQUIRED IMPORTED_TARGET libsecret-1)
target_link_libraries(mozillavpn PRIVATE PkgConfig::libsecret)
pkg_check_modules(libcap REQUIRED IMPORTED_TARGET libcap)
target_link_libraries(mozillavpn PRIVATE PkgConfig::libsecret PkgConfig::libcap)

# Linux platform source files
target_sources(mozillavpn PRIVATE
Expand Down
136 changes: 136 additions & 0 deletions src/apps/vpn/platforms/linux/daemon/dbusservice.cpp
Expand Up @@ -4,11 +4,15 @@

#include "dbusservice.h"

#include <sys/capability.h>
#include <unistd.h>

#include <QCoreApplication>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QJsonDocument>
#include <QJsonObject>
#include <QScopeGuard>
#include <QtDBus/QtDBus>

#include "dbus_adaptor.h"
Expand Down Expand Up @@ -57,6 +61,9 @@ DBusService::DBusService(QObject* parent) : Daemon(parent) {
QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this,
SLOT(userListCompleted(QDBusPendingCallWatcher*)));

// Drop as many root permissions as we are able.
dropRootPermissions();
}

DBusService::~DBusService() { MZ_COUNT_DTOR(DBusService); }
Expand Down Expand Up @@ -99,6 +106,11 @@ QString DBusService::version() {
bool DBusService::activate(const QString& jsonConfig) {
logger.debug() << "Activate";

if (!isCallerAuthorized()) {
logger.error() << "Insufficient caller permissions";
return false;
}

QJsonDocument json = QJsonDocument::fromJson(jsonConfig.toLocal8Bit());
if (!json.isObject()) {
logger.error() << "Invalid input";
Expand Down Expand Up @@ -131,6 +143,12 @@ bool DBusService::activate(const QString& jsonConfig) {

bool DBusService::deactivate(bool emitSignals) {
logger.debug() << "Deactivate";
if (!isCallerAuthorized()) {
logger.error() << "Insufficient caller permissions";
return false;
}

m_sessionUid = 0;
firewallClear();
return Daemon::deactivate(emitSignals);
}
Expand All @@ -141,6 +159,11 @@ QString DBusService::status() {

QString DBusService::getLogs() {
logger.debug() << "Log request";
if (!isCallerAuthorized()) {
logger.error() << "Insufficient caller permissions";
return QString();
}

return Daemon::logs();
}

Expand Down Expand Up @@ -211,6 +234,11 @@ QString DBusService::runningApps() {

/* Update the firewall for running applications matching the application ID. */
bool DBusService::firewallApp(const QString& appName, const QString& state) {
if (!isCallerAuthorized()) {
logger.error() << "Insufficient caller permissions";
return false;
}

logger.debug() << "Setting" << appName << "to firewall state" << state;

// Update the split tunnelling state for any running apps.
Expand All @@ -237,6 +265,11 @@ bool DBusService::firewallApp(const QString& appName, const QString& state) {

/* Update the firewall for the application matching the desired PID. */
bool DBusService::firewallPid(int rootpid, const QString& state) {
if (!isCallerAuthorized()) {
logger.error() << "Insufficient caller permissions";
return false;
}

#if 0
ProcessGroup* group = m_pidtracker->group(rootpid);
if (!group) {
Expand All @@ -258,8 +291,111 @@ bool DBusService::firewallPid(int rootpid, const QString& state) {

/* Clear the firewall and return all applications to the active state */
bool DBusService::firewallClear() {
if (!isCallerAuthorized()) {
logger.error() << "Insufficient caller permissions";
return false;
}

logger.debug() << "Clearing excluded app list";
m_wgutils->resetAllCgroups();
m_excludedApps.clear();
return true;
}

/* Drop root permissions from the daemon. */
void DBusService::dropRootPermissions() {
logger.debug() << "Dropping root permissions";

cap_t caps = cap_get_proc();
if (caps == nullptr) {
logger.warning() << "Failed to retrieve process capabilities";
return;
}
auto guard = qScopeGuard([&] { cap_free(caps); });

// Clear the capability set, which effectively makes us an unpriveleged user.
cap_clear(caps);

// Acquire CAP_NET_ADMIN, we need it to perform bringup and management
// of the network interfaces and Wireguard tunnel.
//
// Acquire CAP_SETUID, we need it to masquerade as other users on their
// session busses for application tracking.
//
// NOTE: ptrace is a dangerous permission to hold. If it may be safer to
// relent on the executable check and grant CAP_NET_ADMIN to the client
// process during installation.
//
// Clear all other capabilities, effectively discarding our root permissions.
cap_value_t newcaps[] = {CAP_NET_ADMIN, CAP_SETUID};
const int numcaps = sizeof(newcaps) / sizeof(cap_value_t);
if (cap_set_flag(caps, CAP_EFFECTIVE, numcaps, newcaps, CAP_SET) ||
cap_set_flag(caps, CAP_PERMITTED, numcaps, newcaps, CAP_SET)) {
logger.warning() << "Failed to set process capability flags";
return;
}
if (cap_set_proc(caps) != 0) {
logger.warning() << "Failed to update process capabilities";
return;
}
}

/* Checks to see if the caller has sufficient authorization */
bool DBusService::isCallerAuthorized() {
if (!calledFromDBus()) {
// If this is not a D-Bus call, it came from the daemon itself.
return true;
}
const QDBusConnectionInterface* iface =
QDBusConnection::systemBus().interface();

// If the VPN is active, and we know the UID that turned it on, as a special
// case we permit that user full access to the D-Bus API in order to manage
// the connection.
if (m_sessionUid != 0) {
const QDBusReply<uint> reply = iface->serviceUid(message().service());
if (reply.isValid() && m_sessionUid == reply.value()) {
return true;
}
}
// Otherwise, if this is the activate method, we permit any non-root user to
// activate the VPN, but we will remember their UID for later authorization
// checks.
else if ((message().type() == QDBusMessage::MethodCallMessage) &&
(message().member() == "activate")) {
const QDBusReply<uint> reply = iface->serviceUid(message().service());
const uint senderuid = reply.value();
if (reply.isValid() && senderuid != 0) {
m_sessionUid = senderuid;
return true;
}
}
// In all other cases, the use of this D-Bus API requires the CAP_NET_ADMIN
// permission, which we can check by examining the PID of the sender. Note
// that a zero UID (root) is used as a guard value to fall back to this case.

// Get the PID of the D-Bus message sender.
const QDBusReply<uint> reply = iface->servicePid(message().service());
const uint senderpid = reply.value();
if (!reply.isValid() || (senderpid == 0)) {
// Could not lookup the sender's PID. Rejected!
logger.warning() << "Failed to resolve sender PID";
return false;
}

// Get the capabilties of the sender process.
cap_t caps = cap_get_pid(senderpid);
if (caps == nullptr) {
logger.warning() << "Failed to retrieve process capabilities";
return false;
}
auto guard = qScopeGuard([&] { cap_free(caps); });

// Check if the calling process has CAP_NET_ADMIN.
cap_flag_value_t flag;
if (cap_get_flag(caps, CAP_NET_ADMIN, CAP_EFFECTIVE, &flag) != 0) {
logger.warning() << "Failed to retrieve process cap_net_admin flags";
return false;
}
return (flag == CAP_SET);
}
8 changes: 7 additions & 1 deletion src/apps/vpn/platforms/linux/daemon/dbusservice.h
Expand Up @@ -5,6 +5,8 @@
#ifndef DBUSSERVICE_H
#define DBUSSERVICE_H

#include <QDBusContext>

#include "apptracker.h"
#include "daemon/daemon.h"
#include "dnsutilslinux.h"
Expand All @@ -14,7 +16,7 @@

class DbusAdaptor;

class DBusService final : public Daemon {
class DBusService final : public Daemon, protected QDBusContext {
Q_OBJECT
Q_DISABLE_COPY_MOVE(DBusService)
Q_CLASSINFO("D-Bus Interface", "org.mozilla.vpn.dbus")
Expand Down Expand Up @@ -51,6 +53,8 @@ class DBusService final : public Daemon {

private:
bool removeInterfaceIfExists();
bool isCallerAuthorized();
void dropRootPermissions();

private slots:
void appLaunched(const QString& cgroup, const QString& appId, int rootpid);
Expand All @@ -68,6 +72,8 @@ class DBusService final : public Daemon {

AppTracker* m_appTracker = nullptr;
QList<QString> m_excludedApps;

uint m_sessionUid = 0;
};

#endif // DBUSSERVICE_H

0 comments on commit 5240754

Please sign in to comment.