Skip to content
API Documentation
BastionGuard™
UIClient.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™ Secure Connection
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 * GPL-3.0
5 */
6
7#include "bsc/ui/UIClient.hpp"
9#include "bsc/proto/Wire.hpp"
10
11#include <nlohmann/json.hpp>
12
13#include <atomic>
14#include <chrono>
15#include <condition_variable>
16#include <cstring>
17#include <deque>
18#include <errno.h>
19#include <mutex>
20#include <poll.h>
21#include <sys/socket.h>
22#include <sys/un.h>
23#include <thread>
24#include <unistd.h>
25#include <unordered_map>
26
27namespace bsc::ui {
28
29using json = nlohmann::json;
30
31namespace {
32
33// Serializza una Connection su un oggetto JSON.
35 j["protocol"] = c.protocol;
36 j["src_ip"] = c.src_ip;
37 j["src_port"] = c.src_port;
38 j["dst_ip"] = c.dst_ip;
39 j["dst_host"] = c.dst_host;
40 j["dst_port"] = c.dst_port;
41 j["user_id"] = c.user_id;
42 j["process_id"] = c.process_id;
43 j["process_path"] = c.process_path;
44 j["process_comm"] = c.process_comm;
45 j["process_cwd"] = c.process_cwd;
46 j["process_args"] = c.process_args;
47 j["process_env"] = c.process_env;
48 j["process_checksums"] = c.process_checksums;
49 json tree = json::array();
50 for (const auto& [name, pid] : c.process_tree) {
51 tree.push_back({{"name", name}, {"pid", pid}});
52 }
53 j["process_tree"] = std::move(tree);
54}
55
56// Helper di conversione del tipo operatore (serve solo per logging/rule).
58 if (s == "simple") return bsc::rule::OperatorType::Simple;
59 if (s == "regexp") return bsc::rule::OperatorType::Regexp;
60 if (s == "network") return bsc::rule::OperatorType::Network;
61 if (s == "range") return bsc::rule::OperatorType::Range;
62 if (s == "list") return bsc::rule::OperatorType::List;
63 if (s == "lists") return bsc::rule::OperatorType::Lists;
65}
66
69 op.type = op_type_from_string(j.value("type", std::string{"simple"}));
70 op.operand = j.value("operand", std::string{});
71 op.data = j.value("data", std::string{});
72 op.sensitive = j.value("sensitive", false);
73 if (j.contains("list") && j["list"].is_array()) {
74 for (const auto& item : j["list"]) op.list.push_back(operator_from_json(item));
75 }
76 return op;
77}
78
81 r.created = j.value("created", std::int64_t{0});
82 r.name = j.value("name", std::string{});
83 r.description = j.value("description", std::string{});
84 r.enabled = j.value("enabled", true);
85 r.precedence = j.value("precedence", false);
86 r.nolog = j.value("nolog", false);
87 r.action = j.value("action", std::string{"allow"});
88 r.duration = j.value("duration", std::string{"once"});
89 if (j.contains("operator") && j["operator"].is_object()) {
90 r.op = operator_from_json(j["operator"]);
91 }
92 return r;
93}
94
95} // namespace
96
98public:
99 explicit Impl(std::string uri)
100 : socket_path_(bsc::proto::resolve_socket_path(std::move(uri))) {}
101
102 ~Impl() { stop(); }
103
104 std::string socket_path_;
105
106 // Stato connessione
107 std::atomic_bool running_{false};
108 std::atomic_int fd_{-1}; // -1 se disconnesso
109 std::atomic_bool handshake_done_{false};
110 std::thread reader_thread_;
111
112 // Scrittore serializzato
113 std::mutex write_mutex_;
114
115 // ID auto-incrementale per i messaggi
116 std::atomic_uint64_t next_id_{1};
117
118 // Risposte pending a ask_rule (id -> promise-like)
119 struct PendingAsk {
120 std::mutex mu;
121 std::condition_variable cv;
122 bool ready{false};
124 };
125 std::mutex pending_mutex_;
126 std::unordered_map<std::uint64_t, std::shared_ptr<PendingAsk>> pending_asks_;
127
128 // Handler notifications
129 std::mutex handler_mutex_;
130 std::function<void(const NotificationEvent&)> notif_handler_;
131
132 // Coda alert da rispedire alla riconnessione
133 std::mutex alerts_mutex_;
134 std::deque<std::string> queued_alerts_; // JSON-encoded frame payloads
135
136 // ── Lifecycle ──────────────────────────────────────────────────────
137
138 void start() {
139 if (running_.exchange(true)) return;
140 reader_thread_ = std::thread([this]() { reconnect_loop(); });
141 }
142
143 void stop() {
144 running_ = false;
145 int fd = fd_.exchange(-1);
146 if (fd >= 0) {
147 ::shutdown(fd, SHUT_RDWR);
148 ::close(fd);
149 }
150 // Sblocca qualunque ask_rule bloccato
151 {
152 std::lock_guard<std::mutex> lk(pending_mutex_);
153 for (auto& [id, p] : pending_asks_) {
154 std::lock_guard<std::mutex> l2(p->mu);
155 p->ready = true;
156 p->reply = json::object(); // risposta vuota = timeout
157 p->cv.notify_all();
158 }
159 pending_asks_.clear();
160 }
161 if (reader_thread_.joinable()) reader_thread_.join();
162 }
163
164 bool is_connected() const noexcept {
165 return fd_.load() >= 0 && handshake_done_.load();
166 }
167
168 // ── Connessione ────────────────────────────────────────────────────
169
170 // Prova una sola connessione al socket Unix. Ritorna fd >=0 se ok.
171 int connect_once() const {
172 int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
173 if (fd < 0) return -1;
174 sockaddr_un addr{};
175 addr.sun_family = AF_UNIX;
176 if (socket_path_.size() >= sizeof(addr.sun_path)) { ::close(fd); return -1; }
177 std::memcpy(addr.sun_path, socket_path_.c_str(), socket_path_.size());
178 if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
179 ::close(fd);
180 return -1;
181 }
182 return fd;
183 }
184
186 auto& log = bsc::logging::Logger::instance();
187 bool previously_connected = false;
188 while (running_) {
189 int fd = connect_once();
190 if (fd < 0) {
191 if (previously_connected) {
192 log.warning("UI disconnessa, tento la riconnessione...");
193 previously_connected = false;
194 }
195 std::this_thread::sleep_for(std::chrono::seconds(1));
196 continue;
197 }
198
199 fd_ = fd;
200 handshake_done_ = false;
201
202 // Invia hello immediatamente
203 if (!send_hello()) {
204 ::close(fd);
205 fd_ = -1;
206 std::this_thread::sleep_for(std::chrono::seconds(1));
207 continue;
208 }
209
210 handshake_done_ = true;
211 previously_connected = true;
212 log.info("UI connessa su " + socket_path_);
214
215 // Loop di lettura finche' il socket non si chiude
217
218 // Disconnesso: pulisci
219 int old = fd_.exchange(-1);
220 if (old >= 0) ::close(old);
221 handshake_done_ = false;
222 }
223 }
224
225 // ── I/O di basso livello ───────────────────────────────────────────
226
227 // Scrive un frame completo. Usa write_mutex_ per serializzare.
228 bool write_frame(const std::string& payload) {
229 int fd = fd_.load();
230 if (fd < 0) return false;
231 std::vector<std::uint8_t> frame;
232 bsc::proto::frame_encode(payload, frame);
233 std::lock_guard<std::mutex> lk(write_mutex_);
234 std::size_t total = 0;
235 while (total < frame.size()) {
236 ssize_t n = ::send(fd, frame.data() + total, frame.size() - total, MSG_NOSIGNAL);
237 if (n < 0) {
238 if (errno == EINTR) continue;
239 return false;
240 }
241 total += static_cast<std::size_t>(n);
242 }
243 return true;
244 }
245
246 // Legge esattamente N byte o ritorna false se il socket si chiude.
247 bool read_exact(int fd, std::uint8_t* buf, std::size_t n) {
248 std::size_t got = 0;
249 while (got < n) {
250 ssize_t r = ::recv(fd, buf + got, n - got, 0);
251 if (r == 0) return false; // EOF
252 if (r < 0) {
253 if (errno == EINTR) continue;
254 return false;
255 }
256 got += static_cast<std::size_t>(r);
257 }
258 return true;
259 }
260
261 // Loop di lettura: smonta frame → smista per tipo.
263 auto& log = bsc::logging::Logger::instance();
264 int fd = fd_.load();
265 while (fd >= 0 && running_) {
266 std::uint8_t len_buf[4];
267 if (!read_exact(fd, len_buf, 4)) break;
268 const std::int64_t len = bsc::proto::frame_peek_length(len_buf, 4);
269 if (len < 0 || static_cast<std::size_t>(len) > bsc::proto::kMaxFrameSize) {
270 log.warning("UIClient: frame invalido, chiudo connessione");
271 break;
272 }
273 std::vector<std::uint8_t> payload(static_cast<std::size_t>(len));
274 if (!read_exact(fd, payload.data(), payload.size())) break;
275
276 try {
277 json j = json::parse(payload.begin(), payload.end());
279 } catch (const std::exception& e) {
280 log.warning(std::string("UIClient: JSON invalido: ") + e.what());
281 }
282 fd = fd_.load();
283 }
284 }
285
286 // Smista un messaggio ricevuto per tipo.
287 void dispatch_incoming(const json& j) {
288 const std::string type = j.value("type", std::string{});
289 if (type == bsc::proto::msg::kPong) {
290 // Nothing to do: e' una conferma di heartbeat.
291 return;
292 }
293 if (type == bsc::proto::msg::kAskRuleReply) {
294 const auto id = j.value("id", std::uint64_t{0});
295 std::shared_ptr<PendingAsk> p;
296 {
297 std::lock_guard<std::mutex> lk(pending_mutex_);
298 auto it = pending_asks_.find(id);
299 if (it == pending_asks_.end()) return;
300 p = it->second;
301 }
302 {
303 std::lock_guard<std::mutex> l2(p->mu);
304 p->reply = j;
305 p->ready = true;
306 }
307 p->cv.notify_all();
308 return;
309 }
310 if (type == bsc::proto::msg::kNotification) {
312 ev.id = j.value("id", std::uint64_t{0});
313 ev.type = j.value("kind", std::string{});
314 ev.data = j.value("data", std::string{});
315 if (j.contains("rules") && j["rules"].is_array()) {
316 for (const auto& r : j["rules"]) ev.rules.push_back(rule_from_json(r));
317 }
318 std::function<void(const NotificationEvent&)> cb;
319 {
320 std::lock_guard<std::mutex> lk(handler_mutex_);
321 cb = notif_handler_;
322 }
323 if (cb) cb(ev);
324 return;
325 }
327 "UIClient: messaggio di tipo sconosciuto: " + type);
328 }
329
330 // ── Invio messaggi ────────────────────────────────────────────────
331
332 bool send_hello() {
333 json j = {
334 {"type", bsc::proto::msg::kHello},
335 {"id", next_id_++},
336 {"daemon_version", "bsc-daemon-cpp"}
337 };
338 return write_frame(j.dump());
339 }
340
342 std::deque<std::string> q;
343 {
344 std::lock_guard<std::mutex> lk(alerts_mutex_);
345 q.swap(queued_alerts_);
346 }
347 for (const auto& payload : q) {
348 if (!write_frame(payload)) {
349 // Rimetti in coda quello che non siamo riusciti a spedire
350 std::lock_guard<std::mutex> lk(alerts_mutex_);
351 queued_alerts_.push_front(payload);
352 break;
353 }
354 }
355 }
356
357 // Invia l'alert se connesso, altrimenti lo accoda (fino a 128 alert).
358 bool send_or_queue_alert(const json& j) {
359 const std::string payload = j.dump();
360 if (is_connected() && write_frame(payload)) return true;
361 std::lock_guard<std::mutex> lk(alerts_mutex_);
362 if (queued_alerts_.size() >= 128) queued_alerts_.pop_front();
363 queued_alerts_.push_back(payload);
364 return true;
365 }
366};
367
368// ────────────────────────────────────────────────────────────────────────────
369// Facade UIClient
370// ────────────────────────────────────────────────────────────────────────────
371
372UIClient::UIClient(std::string socket_uri)
373 : impl_(std::make_unique<Impl>(std::move(socket_uri))) {}
374
375UIClient::~UIClient() = default;
376
377void UIClient::start() { impl_->start(); }
378void UIClient::stop() { impl_->stop(); }
379
380bool UIClient::is_connected() const noexcept {
381 return impl_->is_connected();
382}
383
384void UIClient::set_notification_handler(std::function<void(const NotificationEvent&)> cb) {
385 std::lock_guard<std::mutex> lk(impl_->handler_mutex_);
386 impl_->notif_handler_ = std::move(cb);
387}
388
389std::optional<bsc::rule::Rule> UIClient::ask_rule(const bsc::model::Connection& conn) {
390 if (!impl_->is_connected()) return std::nullopt;
391
392 const auto id = impl_->next_id_++;
393 auto pending = std::make_shared<Impl::PendingAsk>();
394 {
395 std::lock_guard<std::mutex> lk(impl_->pending_mutex_);
396 impl_->pending_asks_[id] = pending;
397 }
398
399 // Costruisci la richiesta
400 json j = {
402 {"id", id}
403 };
404 json cj;
405 connection_to_json(conn, cj);
406 j["connection"] = cj;
407
408 if (!impl_->write_frame(j.dump())) {
409 std::lock_guard<std::mutex> lk(impl_->pending_mutex_);
410 impl_->pending_asks_.erase(id);
411 return std::nullopt;
412 }
413
414 // Aspetta la risposta con timeout 30s (il popup ha countdown 15s,
415 // 30 da margine per il jitter).
416 std::unique_lock<std::mutex> lk(pending->mu);
417 const bool ok = pending->cv.wait_for(lk, std::chrono::seconds(30),
418 [&]() { return pending->ready; });
419
420 {
421 std::lock_guard<std::mutex> lk2(impl_->pending_mutex_);
422 impl_->pending_asks_.erase(id);
423 }
424
425 if (!ok) return std::nullopt; // timeout
426
427 // Estrai la rule dalla risposta
429 r.action = pending->reply.value("action", std::string{"deny"});
430 r.duration = pending->reply.value("duration", std::string{"once"});
431 r.name = pending->reply.value("rule_name", std::string{});
432 r.enabled = true;
433 return r;
434}
435
437 if (!impl_->is_connected()) return false;
438 json j = {
439 {"type", bsc::proto::msg::kPing},
440 {"id", impl_->next_id_++},
441 {"stats", {
442 {"daemon_version", "bsc-daemon-cpp"},
443 {"rules", stats.rules()},
444 {"connections", stats.connections()},
445 {"dns_responses", stats.dns_responses()},
446 {"ignored", stats.ignored()},
447 {"accepted", stats.accepted()},
448 {"dropped", stats.dropped()},
449 {"rule_hits", stats.rule_hits()},
450 {"rule_misses", stats.rule_misses()},
451 }}
452 };
453 return impl_->write_frame(j.dump());
454}
455
456bool UIClient::post_alert(const std::string& text) {
457 json j = {
458 {"type", bsc::proto::msg::kAlert},
459 {"id", impl_->next_id_++},
460 {"level", "info"},
461 {"what", "generic"},
462 {"text", text}
463 };
464 return impl_->send_or_queue_alert(j);
465}
466
468 const std::string& action,
469 const std::string& rule_name) {
470 json j = {
471 {"type", bsc::proto::msg::kAlert},
472 {"id", impl_->next_id_++},
473 {"level", action == "deny" ? "warning" : "info"},
474 {"what", "connection"},
475 };
476 json cj;
477 connection_to_json(conn, cj);
478 j["connection"] = cj;
479 if (!rule_name.empty()) {
480 j["rule"] = {{"name", rule_name}, {"action", action}};
481 }
482 return impl_->send_or_queue_alert(j);
483}
484
485} // namespace bsc::ui
void warning(const std::string &msg)
Definition Logger.cpp:22
static Logger & instance()
Definition Logger.cpp:11
std::unordered_map< std::uint64_t, std::shared_ptr< PendingAsk > > pending_asks_
Definition UIClient.cpp:126
std::atomic_bool running_
Definition UIClient.cpp:107
std::function< void(const NotificationEvent &)> notif_handler_
Definition UIClient.cpp:130
std::atomic_int fd_
Definition UIClient.cpp:108
std::deque< std::string > queued_alerts_
Definition UIClient.cpp:134
std::thread reader_thread_
Definition UIClient.cpp:110
bool write_frame(const std::string &payload)
Definition UIClient.cpp:228
std::atomic_uint64_t next_id_
Definition UIClient.cpp:116
bool is_connected() const noexcept
Definition UIClient.cpp:164
Impl(std::string uri)
Definition UIClient.cpp:99
std::atomic_bool handshake_done_
Definition UIClient.cpp:109
void dispatch_incoming(const json &j)
Definition UIClient.cpp:287
bool read_exact(int fd, std::uint8_t *buf, std::size_t n)
Definition UIClient.cpp:247
bool send_or_queue_alert(const json &j)
Definition UIClient.cpp:358
bool post_alert(const std::string &text)
Definition UIClient.cpp:456
void set_notification_handler(std::function< void(const NotificationEvent &)> cb)
Definition UIClient.cpp:384
std::unique_ptr< Impl > impl_
Definition UIClient.hpp:58
bool post_connection_alert(const bsc::model::Connection &conn, const std::string &action, const std::string &rule_name={})
Definition UIClient.cpp:467
bool ping(const bsc::stats::Statistics &stats)
Definition UIClient.cpp:436
UIClient(std::string socket_uri)
Definition UIClient.cpp:372
std::optional< bsc::rule::Rule > ask_rule(const bsc::model::Connection &conn)
Definition UIClient.cpp:389
bool is_connected() const noexcept
Definition UIClient.cpp:380
constexpr std::string_view kHello
Definition Wire.hpp:48
constexpr std::string_view kAskRuleReply
Definition Wire.hpp:52
constexpr std::string_view kPing
Definition Wire.hpp:49
constexpr std::string_view kAskRule
Definition Wire.hpp:51
constexpr std::string_view kPong
Definition Wire.hpp:50
constexpr std::string_view kNotification
Definition Wire.hpp:54
constexpr std::string_view kAlert
Definition Wire.hpp:53
constexpr std::size_t kMaxFrameSize
Definition Wire.hpp:89
std::int64_t frame_peek_length(const std::uint8_t *buf, std::size_t len)
Definition Wire.hpp:78
void frame_encode(std::string_view payload, std::vector< std::uint8_t > &out)
Definition Wire.hpp:66
OperatorType
Definition Rule.hpp:11
bsc::rule::Operator operator_from_json(const json &j)
Definition UIClient.cpp:67
bsc::rule::OperatorType op_type_from_string(const std::string &s)
Definition UIClient.cpp:57
void connection_to_json(const bsc::model::Connection &c, json &j)
Definition UIClient.cpp:34
bsc::rule::Rule rule_from_json(const json &j)
Definition UIClient.cpp:79
nlohmann::json json
Definition UIClient.cpp:29
Definition Wire.hpp:41
std::vector< std::pair< std::string, std::uint32_t > > process_tree
std::uint32_t process_id
std::vector< std::string > process_args
std::uint32_t dst_port
std::uint32_t src_port
std::map< std::string, std::string > process_checksums
std::map< std::string, std::string > process_env
std::uint32_t user_id
std::string operand
Definition Rule.hpp:15
std::string data
Definition Rule.hpp:16
OperatorType type
Definition Rule.hpp:14
std::vector< Operator > list
Definition Rule.hpp:18
std::vector< bsc::rule::Rule > rules
Definition UIClient.hpp:25
std::condition_variable cv
Definition UIClient.cpp:121