218 lines
5.4 KiB
C++
218 lines
5.4 KiB
C++
/*
|
|
* BastionGuard™
|
|
* Copyright (C) 2025–2026 Calogero Scarnà
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* BastionGuard™ is a trademark of Calogero Scarnà.
|
|
* The BastionGuard™ name and branding are not licensed under the GPL.
|
|
*/
|
|
|
|
#include "../SafeProcess.hpp"
|
|
#include "dnsmasq_backend.hpp"
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
#include <algorithm>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <atomic>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace BastionGuard {
|
|
|
|
static std::mutex g_mutex;
|
|
static std::atomic<bool> g_loaded{false};
|
|
static std::vector<std::string> g_blacklist;
|
|
static std::vector<std::string> g_whitelist;
|
|
|
|
DnsmasqBackend::DnsmasqBackend(const std::string& bl,
|
|
const std::string& wl,
|
|
const std::string& user)
|
|
: blacklist_path_(bl),
|
|
whitelist_path_(wl),
|
|
dnsmasq_user_(user)
|
|
{
|
|
}
|
|
|
|
|
|
|
|
static void load_list_file(const std::string& path,
|
|
std::vector<std::string>& out,
|
|
bool is_blacklist)
|
|
{
|
|
out.clear();
|
|
|
|
if (!fs::exists(path)) return;
|
|
|
|
std::ifstream ifs(path);
|
|
std::string line;
|
|
|
|
while (std::getline(ifs, line)) {
|
|
if (line.empty()) continue;
|
|
|
|
if (is_blacklist) {
|
|
auto a = line.find("address=/");
|
|
if (a != std::string::npos) {
|
|
auto b = line.find('/', a + 9);
|
|
if (b != std::string::npos)
|
|
out.push_back(line.substr(a + 9, b - (a + 9)));
|
|
}
|
|
} else {
|
|
out.push_back(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ensure_loaded(const std::string& bl, const std::string& wl)
|
|
{
|
|
if (g_loaded.load()) return;
|
|
|
|
std::lock_guard<std::mutex> lock(g_mutex);
|
|
if (g_loaded) return;
|
|
|
|
load_list_file(bl, g_blacklist, true);
|
|
load_list_file(wl, g_whitelist, false);
|
|
|
|
g_loaded = true;
|
|
}
|
|
|
|
|
|
bool DnsmasqBackend::atomic_write(const std::string& path,
|
|
const std::string& content)
|
|
{
|
|
try {
|
|
fs::path p(path);
|
|
fs::path tmp = p.string() + ".tmp";
|
|
|
|
{
|
|
std::ofstream o(tmp);
|
|
if (!o) return false;
|
|
o << content;
|
|
}
|
|
fs::rename(tmp, p);
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
std::string DnsmasqBackend::make_dnsmasq_entries(
|
|
const std::vector<std::string>& domains)
|
|
{
|
|
std::ostringstream oss;
|
|
for (auto& d : domains)
|
|
oss << "address=/" << d << "/127.0.0.1\n";
|
|
return oss.str();
|
|
}
|
|
|
|
|
|
bool DnsmasqBackend::add_to_blacklist(const std::string& domain)
|
|
{
|
|
std::thread([this, domain] {
|
|
ensure_loaded(blacklist_path_, whitelist_path_);
|
|
|
|
std::lock_guard<std::mutex> lock(g_mutex);
|
|
if (std::find(g_blacklist.begin(), g_blacklist.end(), domain)
|
|
!= g_blacklist.end())
|
|
return;
|
|
|
|
g_blacklist.push_back(domain);
|
|
|
|
write_blacklist(g_blacklist);
|
|
reload_dnsmasq();
|
|
}).detach();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DnsmasqBackend::remove_from_blacklist(const std::string& domain)
|
|
{
|
|
std::thread([this, domain] {
|
|
ensure_loaded(blacklist_path_, whitelist_path_);
|
|
|
|
std::lock_guard<std::mutex> lock(g_mutex);
|
|
|
|
g_blacklist.erase(
|
|
std::remove(g_blacklist.begin(), g_blacklist.end(), domain),
|
|
g_blacklist.end());
|
|
|
|
write_blacklist(g_blacklist);
|
|
reload_dnsmasq();
|
|
}).detach();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DnsmasqBackend::add_to_whitelist(const std::string& domain)
|
|
{
|
|
std::thread([this, domain] {
|
|
ensure_loaded(blacklist_path_, whitelist_path_);
|
|
|
|
std::lock_guard<std::mutex> lock(g_mutex);
|
|
if (std::find(g_whitelist.begin(), g_whitelist.end(), domain)
|
|
!= g_whitelist.end())
|
|
return;
|
|
|
|
g_whitelist.push_back(domain);
|
|
|
|
write_whitelist(g_whitelist);
|
|
}).detach();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DnsmasqBackend::remove_from_whitelist(const std::string& domain)
|
|
{
|
|
std::thread([this, domain] {
|
|
ensure_loaded(blacklist_path_, whitelist_path_);
|
|
|
|
std::lock_guard<std::mutex> lock(g_mutex);
|
|
|
|
g_whitelist.erase(
|
|
std::remove(g_whitelist.begin(), g_whitelist.end(), domain),
|
|
g_whitelist.end());
|
|
|
|
write_whitelist(g_whitelist);
|
|
}).detach();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool DnsmasqBackend::write_blacklist(
|
|
const std::vector<std::string>& domains)
|
|
{
|
|
return atomic_write(blacklist_path_, make_dnsmasq_entries(domains));
|
|
}
|
|
|
|
bool DnsmasqBackend::write_whitelist(
|
|
const std::vector<std::string>& domains)
|
|
{
|
|
std::ostringstream oss;
|
|
for (auto& d : domains)
|
|
oss << d << "\n";
|
|
return atomic_write(whitelist_path_, oss.str());
|
|
}
|
|
|
|
|
|
bool DnsmasqBackend::reload_dnsmasq()
|
|
{
|
|
int st = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--system", "try-reload-or-restart", "dnsmasq.service"}).exit_code);
|
|
return WIFEXITED(st) && WEXITSTATUS(st) == 0;
|
|
}
|
|
|
|
} // namespace BastionGuard
|