Files
isolationRunner/DBusConnection.cpp
tomFlowee c0f579ff6d add VPN feature
This allows a jail to have a VPN config associated and as a result we start
a new net namespace, completely isolating the jails networking.
We then start an openVPN client to route between the main network and the
jails' network.

The main limitation here is that we don't setup DNS, which basically means
that the VPN will route DNS calls to the other side, but since we don't
remount resolv.conf this depends on the vpn provider actually mapping the
nameserver we use. For people that use a nameserver like 192.168.100.1,
this most of the time works just fine.

Improvement is possible.
2026-04-11 15:06:44 +02:00

283 lines
10 KiB
C++

#include "DBusConnection.h"
#include "IsolationManager.h"
#include <QDBusConnection>
#include <QDBusError>
#include <QSettings>
#include <QDebug>
#include <qdbusargument.h>
#include <QXmlStreamWriter>
#include <QBuffer>
#include <cstdlib>
constexpr const char *SERVICE_NAME = "org.tom.IsolationRunner";
DBusConnection::DBusConnection(IsolationManager *parent)
: QDBusAbstractAdaptor(parent),
m_parent(parent)
{
/*
* Registering ourself like this means that all public slots will become
* remote-callable.
*/
if (!QDBusConnection::sessionBus().isConnected())
throw std::runtime_error("Failed to find the DBUS session");
QDBusConnection::sessionBus().registerObject("/apps", parent);
if (!QDBusConnection::sessionBus().registerService(SERVICE_NAME))
throw std::runtime_error("Registration with DBus failed, am I already running?");
}
void DBusConnection::run(const QString &fullPath, const QStringList &arguments, const QDBusMessage &message)
{
run2(message);
}
void DBusConnection::run2(const QDBusMessage &message)
{
/*
* The complex run option. With more features.
* 0: app-name
* 1: app-arguments
* 2: executable-path (if different from 0)
* 3: bool: lockdown (minimum rights)
* 4: bool: autodelete
* 5: string: jailId
*/
const auto args = message.arguments();
if (args.size() < 2) {
QDBusConnection::sessionBus().send(message.createErrorReply(QDBusError::InvalidArgs, "Need more args"));
return;
}
QString profileName = args.at(0).toString();
if (!args.at(1).canConvert(QMetaType::QStringList)) {
QDBusConnection::sessionBus().send(message.createErrorReply(QDBusError::InvalidArgs, "invalid args"));
return;
}
QStringList arguments = args.at(1).toStringList();
bool lockdown = false;
bool autoDelete = false;
QString jailId; // to run a specific executable in an existing jailId, this is the 'id'
QString vpnConf, vpnAc;
if (args.size() >= 4)
lockdown = args.at(3).toBool();
if (args.size() >= 5)
autoDelete = args.at(4).toBool(); // auto-delete is not allowed for existing profiles
if (args.size() >= 6)
jailId = args.at(5).toString();
if (args.size() >= 8) {
vpnAc = args.at(6).toString();
vpnConf = args.at(7).toString();
}
// jailId means that the profile we run in is defined by the ID,
// instead of the executable.
if (!jailId.isEmpty()) {
if (autoDelete)
return fail(message, "not valid combination of options");
profileName = jailId;
}
else if (autoDelete) // avoid app namespace collisions.
profileName += QString("_tmp-%1").arg(std::rand());
auto existingAppData = m_parent->lookupApp(profileName, IsolationManager::OnlyExisting);
if (!jailId.isEmpty()) {
int jail = existingAppData.appId;
// is jailId an int based ID?
bool isNumberId = false;
if (existingAppData.appId == -1) {
jail = jailId.toInt(&isNumberId);
if (!isNumberId)
return fail(message, "jail not found");
}
for (const auto &profile : m_parent->listProfiles()) {
if (profile.jailId == jail) {
if (!profile.active)
return fail(message, "jail not running");
if (isNumberId)
existingAppData = m_parent->lookupApp(profile.name, IsolationManager::OnlyExisting);
break;
}
}
assert(jail > 1);
assert(existingAppData.appId > 0);
}
if (existingAppData.appId == -1) {
if (!jailId.isEmpty())
return fail(message, "jail not found");
QString exe;
if (args.size() >= 3)
exe = args.at(2).toString();
if (exe.isEmpty())
exe = args.at(0).toString();
if (!exe.startsWith("/"))
return fail(message, "Invalid path, not absolute");
QFileInfo info(exe);
if (!info.exists())
return fail(message, "File not found");
else if (!info.isExecutable())
return fail(message, "Path doesn't point to an executable");
// create new entry
auto entry = m_parent->startEditApp(profileName, IsolationManager::MaybeCreate);
entry->setValue("path-to-exe", exe);
if (lockdown)
entry->setValue("denied", "homedir dbus git ssh media dbus-system audio");
if (!vpnAc.isEmpty())
entry->setValue("vpnAc", vpnAc);
if (!vpnConf.isEmpty())
entry->setValue("vpnConf", vpnConf);
}
else if (!jailId.isEmpty()) {
auto appData = existingAppData;
// no init script should be run
appData.initScript.clear();
if (args.size() >= 3)
appData.pathToExe = args.at(2).toString();
if (appData.pathToExe.isEmpty())
appData.pathToExe = args.at(0).toString();
if (!appData.pathToExe.startsWith('/'))
appData.pathToExe = "/usr/bin/" + appData.pathToExe;
QFileInfo exe(appData.pathToExe);
if (!exe.isExecutable())
return fail(message, "Path doesn't point to an executable");
auto answer = m_parent->startApplicationRequest(appData, arguments);
if (answer == "ok")
QDBusConnection::sessionBus().send(message.createReply(answer));
else
fail(message, answer);
return;
} else {
QString exe;
if (args.size() >= 3)
exe = args.at(2).toString();
if (!exe.isEmpty() && existingAppData.pathToExe != exe) // reject.
return fail(message, "Exe path not accepted (try without)");
}
auto appData = m_parent->lookupApp(profileName, IsolationManager::OnlyExisting);
assert(appData.appId != -1);
appData.autoDelete = autoDelete;
auto answer = m_parent->startApplicationRequest(appData, arguments);
if (answer == "ok")
QDBusConnection::sessionBus().send(message.createReply(answer));
else
fail(message, answer);
}
QDBusVariant DBusConnection::listProfiles(bool verbose) const
{
auto profileInfos = m_parent->listProfiles();
std::sort(profileInfos.begin(), profileInfos.end(),
[](const IsolationManager::ProfileInfo &a,
const IsolationManager::ProfileInfo &b) {
return a.lastRun > b.lastRun;
});
QStringList answer;
for (const auto &profileInfo : profileInfos) {
QString profile = profileInfo.active ? "*" : " ";
if (verbose)
profile = profile % " " % QString::number(profileInfo.jailId);
profile = profile % " " % profileInfo.name;
answer.append(profile);
if (verbose && !profileInfo.exe.startsWith("/usr/bin/")
&& !profileInfo.exe.startsWith("/bin/")) {
// if the exe isn't in /usr/bin or /bin, show it in verbose mode
answer << QString(" -> ") % profileInfo.exe;
}
}
return QDBusVariant(answer);
}
void DBusConnection::details(const QString &profile, const QDBusMessage &message) const
{
if (profile.isEmpty())
QDBusConnection::sessionBus().send(message.createErrorReply(QDBusError::InvalidArgs, "Missing arg"));
QString path(profile);
if (!profile.startsWith("/"))
path = "/bin/" + profile;
auto appData = m_parent->lookupApp(path, IsolationManager::OnlyExisting);
if (appData.appId == -1) {
bool isNum;
const int profileId = profile.toLongLong(&isNum);
if (isNum) {
// the user-entered number may be a appId
for (const auto &profile : m_parent->listProfiles()) {
if (profileId == profile.jailId) {
appData = m_parent->lookupApp(profile.name, IsolationManager::OnlyExisting);
assert(appData.appId == profile.jailId);
break;
}
}
}
}
if (appData.appId == -1) {
QDBusConnection::sessionBus().send(message.createErrorReply(QDBusError::InvalidArgs, "unknown profile"));
return;
}
QBuffer data;
data.open(QIODevice::WriteOnly);
QXmlStreamWriter writer(&data);
writer.setAutoFormatting(true);
writer.writeStartDocument();
writer.writeStartElement("app");
writer.writeAttribute("name", appData.profileName);
writer.writeAttribute("id", QString::number(appData.appId));
writer.writeAttribute("exe", appData.pathToExe);
for (const auto &denied : std::as_const(appData.denied)) {
writer.writeStartElement("denied");
writer.writeCharacters(denied);
writer.writeEndElement();
}
for (const auto &allowed : std::as_const(appData.allowed)) {
writer.writeStartElement("allowed");
writer.writeCharacters(allowed);
writer.writeEndElement();
}
QString appStateFile = m_parent->stateFile(appData.appId);
if (QFile::exists(appStateFile)) {
QSettings appState(appStateFile, QSettings::IniFormat);
writer.writeStartElement("lastRun");
writer.writeAttribute("start", appState.value("start").toDateTime().toString(Qt::ISODate));
int pid = appState.value("pid").toInt();
if (pid && QFile::exists(QString("/proc/%1").arg(pid) )) {
writer.writeAttribute("pid", QString::number(pid));
}
writer.writeEndElement();
}
writer.writeEndElement();
QDBusConnection::sessionBus().send(message.createReply(QString::fromLatin1(data.buffer())));
}
void DBusConnection::setRights(const QString &profile, const QStringList &denied, const QStringList &allowed)
{
// this method is allowed to create the app if it does not exist.
QString path(profile);
if (!profile.startsWith("/"))
path = "/bin/" + profile;
auto appData = m_parent->lookupApp(path, IsolationManager::MaybeCreate);
appData.setAllowed(allowed);
appData.setDenied(denied);
QSettings entry(m_parent->dbDir().absoluteFilePath(appData.profileName + ".info"), QSettings::IniFormat);
entry.setValue("denied", appData.denied.join(' '));
entry.setValue("allowed", appData.allowed.join(' '));
}
void DBusConnection::fail(const QDBusMessage &message, const QString &errorMessage) const
{
QDBusConnection::sessionBus().send(message.createErrorReply(QDBusError::InvalidArgs, errorMessage));
}