7#include <nlohmann/json.hpp>
12#define _(text) gettext(text)
16namespace fs = std::filesystem;
17using json = nlohmann::json;
22 for (
char& ch : s) ch =
static_cast<char>(std::tolower(
static_cast<unsigned char>(ch)));
28 return s.find(
"thread") != std::string::npos ||
29 s.find(
"socket") != std::string::npos ||
30 s.find(
"web content") != std::string::npos ||
31 s.find(
"renderer") != std::string::npos ||
32 s.find(
"utility") != std::string::npos ||
33 s.find(
"chrome_") != std::string::npos ||
34 s.find(
"isolated web") != std::string::npos ||
37 s ==
"privileged cont";
58 if (conn.
user_id == 0xffffffffu) conn.
user_id =
static_cast<std::uint32_t
>(p.
uid);
68 if (v.empty())
return def;
70 if (v.ends_with(
"ms"))
return std::chrono::milliseconds(std::stoll(v.substr(0, v.size() - 2)));
71 if (v.ends_with(
"s"))
return std::chrono::seconds(std::stoll(v.substr(0, v.size() - 1)));
72 if (v.ends_with(
"m"))
return std::chrono::minutes(std::stoll(v.substr(0, v.size() - 1)));
79 const auto aliases = std::filesystem::path(
config_.
rules.
path).parent_path() /
"network_aliases.json";
104 auto scan_interval = std::chrono::seconds(2);
105 auto heartbeat_interval = std::chrono::seconds(10);
106 auto rules_interval = std::chrono::seconds(3);
110 if (fs::exists(tasks_file)) {
112 std::ifstream in(tasks_file);
115 if (j.contains(
"tasks") && j[
"tasks"].is_array()) {
116 for (
const auto&
task : j[
"tasks"]) {
117 const auto name =
task.value(
"name", std::string{});
118 const auto interval =
parse_interval(
task.value(
"interval", std::string{
"5s"}), std::chrono::seconds(5));
119 if (name ==
"scan") {
120 scan_interval = std::chrono::duration_cast<std::chrono::seconds>(interval);
121 }
else if (name ==
"heartbeat") {
122 heartbeat_interval = std::chrono::duration_cast<std::chrono::seconds>(interval);
123 }
else if (name ==
"rules_reload") {
124 rules_interval = std::chrono::duration_cast<std::chrono::seconds>(interval);
126 else if (name ==
"firewall_reload") firewall_interval = interval;
130 log.warning(
"tasks.json non valido, uso gli intervalli di default");
139 const auto proc_method = [&]() {
141 for (
auto& ch : method) ch =
static_cast<char>(std::tolower(
static_cast<unsigned char>(ch)));
145 if (proc_method ==
"netlink" || proc_method ==
"proc" || proc_method ==
"audit" || proc_method ==
"ebpf") {
147 log.info(
"proc event monitor attivo via netlink connector");
148 if (proc_method ==
"audit" || proc_method ==
"ebpf") {
155 if (proc_method ==
"ebpf") {
157 ? std::string{
"/usr/lib/bastionguard-sc/bsc_ebpf.o"}
160 log.info(
"eBPF monitor attivo: tracepoint inet_sock_set_state");
161 ui_.
post_alert(
"eBPF monitor attivo — intercettazione a livello kernel");
163 ui_.
post_alert(
"eBPF non disponibile — fallback NFQUEUE+/proc");
170 log.warning(
"proc event monitor non disponibile, resto in modalità on-demand /proc");
183 if (!desktop_allows_activity()) {
184 bsc::netfilter::Decision d;
185 d.verdict = bsc::netfilter::Verdict::Accept;
190 if (conn.user_id == 0xffffffffu && pkt.
uid != 0xffffffffu) conn.
user_id = pkt.
uid;
195 for (
int i = 0; i < 4 && !has_process_identity(conn); ++i) {
197 std::this_thread::sleep_for(std::chrono::milliseconds(15 + 10 * i));
207 ui_.
post_alert(
"NFQUEUE attiva: intercettazione pacchetti in tempo reale");
210 log.warning(
"NFQUEUE non disponibile: uso polling /proc come fallback");
211 ui_.
post_alert(
"NFQUEUE non disponibile: fallback su /proc snapshot");
215 log.info(
"bsc-daemon-cpp started");
216 ui_.post_alert(
"bsc-daemon-cpp avviato");
217 ui_.post_alert(
"runtime: proc_monitor_method=" + config_.proc_monitor_method +
", queue_num=" + std::to_string(config_.fw_options.queue_num));
218 while (running_) std::this_thread::sleep_for(std::chrono::milliseconds(250));
219 ui_.post_alert(
"bsc-daemon-cpp arrestato");
222 std::this_thread::sleep_for(std::chrono::milliseconds(200));
224 firewall_.remove_nfqueue_rule();
229 log.info(
"bsc-daemon-cpp stopped");
240 if (previous == active)
return;
249 net_.invalidate_cache();
250 log.info(
"Desktop attivo: monitoraggio connessioni riattivato");
256 net_.discard_pending_events();
257 log.info(
"Desktop inattivo/bloccato: sospendo scansione e popup connessioni");
263 fs::file_time_type latest{};
264 if (!fs::exists(
config_.rules.path, ec))
return false;
265 for (
const auto& entry : fs::directory_iterator(
config_.rules.path, ec)) {
266 if (!entry.is_regular_file() || entry.path().extension() !=
".json")
continue;
267 latest = std::max(latest, fs::last_write_time(entry.path(), ec));
277 const auto aliases = std::filesystem::path(
config_.rules.path).parent_path() /
"network_aliases.json";
278 rules_.load_aliases(aliases.string());
284 if (
config_.fw_options.config_path.empty()) {
289 const std::string message =
290 std::string{
_(
"Reload firewall fallito: ")} +
291 config_.fw_options.config_path;
294 ui_.post_alert(message);
300 log.info(
"notification: " + ev.
type);
303 if (ev.
type ==
"reload_fw_rules") {
305 }
else if (ev.
type ==
"stop") {
307 }
else if (ev.
type ==
"enable_interception") {
308 config_.intercept_unknown =
true;
309 }
else if (ev.
type ==
"disable_interception") {
310 config_.intercept_unknown =
false;
311 }
else if (ev.
type ==
"desktop_active") {
313 }
else if (ev.
type ==
"desktop_idle") {
315 }
else if (ev.
type ==
"enable_firewall") {
317 tasks_.every(
"firewall_reload",
319 [
this]() { reload_firewall(); });
320 }
else if (ev.
type ==
"disable_firewall") {
321 log.warning(
"disable firewall: sospendo i reload periodici");
322 tasks_.stop_task(
"firewall_reload");
323 }
else if (ev.
type ==
"task_stop") {
325 auto j = json::parse(ev.
data);
326 const auto name = j.value(
"Name", std::string{});
327 if (!name.empty())
tasks_.stop_task(name);
328 }
catch (...) { log.warning(
"task_stop con JSON non valido"); }
329 }
else if (ev.
type ==
"task_start") {
331 auto j = json::parse(ev.
data);
332 const auto name = j.value(
"Name", std::string{});
333 const auto interval =
parse_interval(j.value(
"Interval", std::string{
"5s"}), std::chrono::seconds(5));
335 else if (name ==
"heartbeat")
tasks_.every(
"heartbeat", interval, [
this]() {
heartbeat(); });
336 else if (name ==
"rules_reload")
tasks_.every(
"rules_reload", interval, [
this]() {
reload_rules(); });
337 else if (name ==
"firewall_reload")
tasks_.every(
"firewall_reload", interval, [
this]() {
reload_firewall(); });
338 }
catch (...) { log.warning(
"task_start con JSON non valido"); }
339 }
else if (ev.
type ==
"log_level") {
341 auto lvl = std::stoi(ev.
data);
342 config_.log_level = lvl <= 0 ? 0 : lvl >= 3 ? 3 : lvl;
344 }
catch (...) { log.warning(
"log_level non valido"); }
345 }
else if (ev.
type ==
"change_rule" || ev.
type ==
"enable_rule" || ev.
type ==
"disable_rule") {
346 for (
auto r : ev.
rules) {
347 if (ev.
type ==
"disable_rule") r.enabled =
false;
348 if (ev.
type ==
"enable_rule") r.enabled =
true;
352 }
else if (ev.
type ==
"delete_rule") {
353 for (
const auto& r : ev.
rules) {
355 fs::remove(fs::path(
config_.rules.path) / (r.name +
".json"), ec);
358 }
else if (ev.
type ==
"change_config") {
360 auto j = json::parse(ev.
data);
361 if (j.contains(
"DefaultAction"))
config_.default_action = j[
"DefaultAction"].get<std::string>();
362 if (j.contains(
"DefaultDuration"))
config_.default_duration = j[
"DefaultDuration"].get<std::string>();
363 if (j.contains(
"Firewall"))
config_.firewall = j[
"Firewall"].get<std::string>();
364 if (j.contains(
"ProcMonitorMethod"))
config_.proc_monitor_method = j[
"ProcMonitorMethod"].get<std::string>();
365 if (j.contains(
"EBPFObjPath"))
config_.ebpf_obj_path = j[
"EBPFObjPath"].get<std::string>();
366 if (j.contains(
"InterceptUnknown"))
config_.intercept_unknown = j[
"InterceptUnknown"].get<
bool>();
367 if (j.contains(
"LogUTC"))
config_.log_utc = j[
"LogUTC"].get<
bool>();
368 if (j.contains(
"LogMicro"))
config_.log_micro = j[
"LogMicro"].get<
bool>();
369 if (j.contains(
"LogLevel")) {
370 config_.log_level = j[
"LogLevel"].get<
int>();
376 const auto target =
config_.source_path.empty()
377 ? std::string{
"/etc/bastionguard-secure-connectiond/default-config.json"}
380 }
catch (...) { log.warning(
"change_config con JSON non valido"); }
392 if (
auto p =
proc_.cached(pid,
false)) {
393 log.debug(
"exec pid=" + std::to_string(p->pid) +
" path=" + p->path);
396 proc_.note_exit(pid);
397 log.debug(
"exit pid=" + std::to_string(pid) +
" code=" + std::to_string(ev.
exit_code));
402 if (decision.
duration !=
"always")
return;
404 persisted.
created = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
405 if (persisted.
name.empty()) {
406 std::ostringstream name;
407 name <<
"autogen-" << persisted.
action <<
'-' << conn.
process_id <<
'-' << conn.
dst_port <<
'-' << std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
408 persisted.
name = name.str();
410 persisted.
description =
"Auto-generated from UI decision";
413 persisted.
nolog =
false;
418 proc_op.
operand =
"process.path";
421 proc_op.
operand =
"process.name";
424 proc_op.
operand =
"process.id";
437 persisted.
op.
list = {proc_op, host_op, port_op};
441 ui_.post_alert(
"Regola persistente salvata: " + persisted.
name);
447 const auto now = std::chrono::steady_clock::now();
452 if (now - it->second.second > kRecentFlowTtl) it =
recent_flows_.erase(it);
459 const auto now = std::chrono::steady_clock::now();
462 if (now - it->second.second > kRecentFlowTtl) {
466 const auto& cached = it->second.first;
467 const bool same_direct = conn.
protocol == cached.protocol && conn.
src_ip == cached.src_ip && conn.
src_port == cached.src_port && conn.
dst_ip == cached.dst_ip && conn.
dst_port == cached.dst_port;
468 const bool same_reverse = conn.
protocol == cached.protocol && conn.
src_ip == cached.dst_ip && conn.
src_port == cached.dst_port && conn.
dst_ip == cached.src_ip && conn.
dst_port == cached.src_port;
469 if (same_direct || same_reverse) {
488 std::vector<bsc::model::ProcessInfo> candidates;
489 if (conn.
user_id != 0xffffffffu) candidates =
proc_.recent_candidates(conn.
user_id, max_age, 8);
490 else candidates =
proc_.recent_candidates_any(max_age, 4);
491 if (candidates.empty())
return;
493 const auto& best = candidates.front();
494 bool unambiguous = candidates.size() == 1;
497 for (std::size_t i = 1; i < std::min<std::size_t>(candidates.size(), 3); ++i) {
498 if (!same_process_signature(best, candidates[i])) {
504 if (!unambiguous)
return;
505 merge_process_info(conn, best);
512 return proc_key +
"|" + host +
"|" + std::to_string(c.
dst_port);
515std::optional<DaemonService::CachedVerdict>
518 const auto now = std::chrono::steady_clock::now();
522 if (it->second.expires_at != std::chrono::steady_clock::time_point{} &&
523 now >= it->second.expires_at) {
533 if (r.duration !=
"always")
return;
534 CachedVerdict cv{r.action, r.duration, std::chrono::steady_clock::time_point{}};
558 if (!has_process_identity(conn)) {
561 if (!has_process_identity(conn)) {
566 merge_process_info(conn, *p);
570 log.warning(
"Connessione senza processo risolto: uid=" + std::to_string(conn.
user_id) +
579 std::string rule_name;
580 std::string duration =
config_.default_duration;
581 bool asked_ui =
false;
587 action = cached->action;
588 duration = cached->duration;
592 else if (
auto matched =
rules_.find_match(conn)) {
593 action = matched->action;
594 duration = matched->duration;
595 rule_name = matched->name;
602 else if (
config_.intercept_unknown && realtime && allow_blocking_ask) {
604 log.debug(
"ask_rule gia' in volo, applico default_action=" +
606 action =
config_.default_action;
612 ":" + std::to_string(conn.
dst_port));
613 auto asked =
ui_.ask_rule(conn);
617 action = asked->action.empty() ?
config_.default_action : asked->action;
618 duration = asked->duration.empty() ?
config_.default_duration : asked->duration;
619 rule_name = asked->name;
620 asked->duration = duration;
621 asked->action = action;
622 log.info(
"AskRule risposta: action=" + action +
", duration=" + duration);
626 log.warning(
"AskRule senza risposta, uso default_action=" +
628 action =
config_.default_action;
632 action =
config_.default_action;
636 stats_.record(conn, action, rule_name);
642 log.info(
"PostAlert UI: action=" + action +
643 (rule_name.empty() ? std::string{} :
", rule=" + rule_name));
644 ui_.post_connection_alert(conn, action, rule_name);
647 std::ostringstream oss;
649 if (!rule_name.empty()) oss <<
" [" << rule_name <<
']';
661 "scan sospesa: desktop inattivo o UI non connessa");
663 net_.discard_pending_events();
668 for (
auto conn :
net_.list_connections()) {
669 if ((conn.dst_ip ==
"0.0.0.0" || conn.dst_ip ==
"::") && conn.dst_port == 0)
continue;
670 const auto key = conn.key();
673 if (
seen_.contains(key))
continue;
bool install_nfqueue_rule(std::uint16_t queue_num, bool queue_bypass=true) const
void remove_nfqueue_rule() const
bool reload(const std::string &config_path) const
void warning(const std::string &msg)
void set_microseconds(bool enabled)
void info(const std::string &msg)
static Logger & instance()
void set_utc(bool enabled)
void set_level(Level level)
bool enrich_connection(bsc::model::Connection &conn) const
bool start_ebpf(const std::string &obj_path="/usr/lib/bastionguard-sc/bsc_ebpf.o")
bool open(std::uint16_t queue_id, Handler handler)
bool start(Handler handler)
void load_aliases(const std::string &path)
void load_directory(const std::string &dir)
static std::chrono::milliseconds parse_interval(const std::string &v, std::chrono::milliseconds def=std::chrono::seconds(5))
bsc::stats::Statistics stats_
bsc::netlink::ProcEventMonitor proc_events_
std::mutex verdict_cache_mutex_
void enrich_from_recent_processes(bsc::model::Connection &conn, std::chrono::milliseconds max_age=std::chrono::milliseconds(5000))
bsc::netfilter::Decision decide_connection(bsc::model::Connection conn, bool realtime, bool allow_blocking_ask=true)
static std::string verdict_key(const bsc::model::Connection &c)
bool rules_changed() const
std::atomic_bool is_asking_
void verdict_store(const bsc::model::Connection &c, const bsc::rule::Rule &r)
void set_desktop_active(bool active)
bsc::net::SocketSnapshot net_
bool desktop_allows_activity() const
bsc::net::DnsResolver dns_
std::mutex rules_reload_mutex_
void handle_notification(const bsc::ui::NotificationEvent &ev)
bsc::config::Config config_
std::unordered_map< std::string, CachedVerdict > verdict_cache_
bsc::task::TaskRunner tasks_
DaemonService(bsc::config::Config config)
bsc::fw::Firewall firewall_
bsc::netfilter::NFQueue nfqueue_
void remember_connection(const bsc::model::Connection &conn)
void handle_proc_event(const bsc::netlink::ProcEvent &ev)
std::atomic_bool running_
std::filesystem::file_time_type rules_mtime_
std::set< std::string > seen_
std::atomic_bool desktop_active_
std::optional< CachedVerdict > verdict_lookup(const bsc::model::Connection &c) const
std::atomic_bool inactive_notice_logged_
void persist_learned_rule(const bsc::model::Connection &conn, const bsc::rule::Rule &decision)
std::unordered_map< std::string, std::pair< bsc::model::Connection, std::chrono::steady_clock::time_point > > recent_flows_
void enrich_from_recent_flows(bsc::model::Connection &conn)
void every(const std::string &name, std::chrono::milliseconds interval, std::function< void()> fn)
bool post_alert(const std::string &text)
void set_notification_handler(std::function< void(const NotificationEvent &)> cb)
bool save_file(const Config &cfg, const std::string &path)
constexpr auto kRecentFlowTtl
void merge_process_info(bsc::model::Connection &conn, const bsc::model::ProcessInfo &p)
std::string lower_copy(std::string s)
bool has_process_identity(const bsc::model::Connection &c)
bool looks_like_browser_thread_comm(const std::string &comm)
bool same_process_signature(const bsc::model::ProcessInfo &a, const bsc::model::ProcessInfo &b)
std::string monitor_interval
std::string proc_monitor_method
std::string ebpf_obj_path
std::vector< std::pair< std::string, std::uint32_t > > process_tree
std::vector< std::string > process_args
std::map< std::string, std::string > process_checksums
std::map< std::string, std::string > process_env
std::map< std::string, std::string > env
std::map< std::string, std::string > checksums
std::vector< std::pair< std::string, std::uint32_t > > process_tree
std::vector< std::string > args
bsc::model::Connection connection
std::uint32_t parent_tgid
std::vector< Operator > list
std::vector< bsc::rule::Rule > rules