Skip to content
API Documentation
BastionGuard™
bastionguard_cef.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
21#include <boost/asio.hpp>
22#include <boost/asio/signal_set.hpp>
23#include <boost/asio/thread_pool.hpp>
24#include <boost/asio/ssl.hpp>
25
26#include <include/cef_app.h>
27#include <include/cef_command_line.h>
28#include <glib/gi18n.h>
29#include <libsoup/soup.h>
30#include <glib.h>
31
32#include <algorithm>
33#include <array>
34#include <atomic>
35#include <chrono>
36#include <cerrno>
37#include <cstdlib>
38#include <cstring>
39#include <functional>
40#include <iostream>
41#include <memory>
42#include <mutex>
43#include <sstream>
44#include <string>
45#include <string_view>
46#include <thread>
47#include <unordered_map>
48#include <unordered_set>
49#include <vector>
50#include <cctype>
51#include <fstream>
52#include <unistd.h>
53#include <sys/stat.h>
54#include <sys/wait.h>
55
56#include "bank_filter.hpp"
57#include "proxy_config.hpp"
58#include "block_page.hpp"
59#include "Resource.hpp"
60#include "SecureBrowser.hpp"
62#include "ca_installer.hpp"
63#include "tls_intercept.hpp"
64#include "SettingsStore.hpp"
65
66#ifndef LOCALEDIR
67# define LOCALEDIR "/usr/share/BastionGuard/locale"
68#endif
69#ifndef BG_WARNING_HTTP_PORT
70# define BG_WARNING_HTTP_PORT 81
71#endif
72#ifndef BG_WARNING_HTTPS_PORT
73# define BG_WARNING_HTTPS_PORT 444
74#endif
75#ifndef BG_WARNING_ADDR
76# define BG_WARNING_ADDR "127.0.0.2"
77#endif
78
79static constexpr auto SESSION_TTL = std::chrono::minutes(30);
80
81static const unsigned int IO_THREADS =
82 std::max(2u, std::thread::hardware_concurrency());
83
84namespace asio = boost::asio;
85using tcp = asio::ip::tcp;
86using Clock = std::chrono::steady_clock;
87
88static asio::io_context g_io{static_cast<int>(IO_THREADS)};
89
90static std::string root_domain(const std::string& host) {
91 const auto dots = std::count(host.begin(), host.end(), '.');
92 if (dots <= 1) return host;
93 const auto p1 = host.rfind('.');
94 const auto p2 = host.rfind('.', p1 - 1);
95 return host.substr(p2 + 1);
96}
97
99 Clock::time_point opened_at;
100 std::string type;
101 pid_t child_pid = -1;
102};
103
104static std::mutex g_session_mutex;
105static std::unordered_map<std::string, SessionEntry> g_sessions;
106
107// Definita più avanti (gate anti-doppia-apertura della sandbox). La
108// dichiariamo qui perché alla scadenza del TTL di sessione dobbiamo anche
109// liberare il gate, così una nuova visita al sito dopo i 30 minuti può
110// riaprire legittimamente la sandbox.
111static void sandbox_close(const std::string& root);
112static void sandbox_bind_pid(const std::string& root, pid_t pid);
113static void sandbox_close_if_pid(const std::string& root, pid_t pid);
114
115static bool session_active(const std::string& root, std::string& out_type) {
116 bool expired = false;
117 {
118 std::lock_guard<std::mutex> lock(g_session_mutex);
119 auto it = g_sessions.find(root);
120 if (it == g_sessions.end()) return false;
121 if (Clock::now() - it->second.opened_at > SESSION_TTL) {
122 g_sessions.erase(it);
123 expired = true;
124 } else {
125 out_type = it->second.type;
126 return true;
127 }
128 }
129 // Sessione scaduta: libera anche il gate sandbox (fuori dal lock di
130 // sessione per evitare di tenere due mutex insieme).
131 if (expired) sandbox_close(root);
132 return false;
133}
134
135// Apertura atomica usata dopo che il MITM ha verificato che la richiesta e'
136// davvero una navigazione top-level. Gli iframe/POST non creano sessioni.
137static bool session_try_open(const std::string& root, const std::string& type) {
138 std::lock_guard<std::mutex> lock(g_session_mutex);
139 auto [it, inserted] = g_sessions.emplace(
140 root, SessionEntry{Clock::now(), type, -1});
141 return inserted;
142}
143
144static void session_bind_pid(const std::string& root, pid_t pid) {
145 std::lock_guard<std::mutex> lock(g_session_mutex);
146 auto it = g_sessions.find(root);
147 if (it != g_sessions.end())
148 it->second.child_pid = pid;
149}
150
151static void session_close(const std::string& root) {
152 bool removed = false;
153 {
154 std::lock_guard<std::mutex> lock(g_session_mutex);
155 removed = g_sessions.erase(root) != 0;
156 }
157 if (removed) {
158 char msg[256];
159 std::snprintf(msg, sizeof(msg),
160 _("[bastionguard] session closed for root: %s — browser proxy restored\n"),
161 root.c_str());
162 std::cerr << msg;
163 }
164}
165
166static void session_close_if_pid(const std::string& root, pid_t pid) {
167 bool removed = false;
168 {
169 std::lock_guard<std::mutex> lock(g_session_mutex);
170 auto it = g_sessions.find(root);
171 if (it != g_sessions.end() && it->second.child_pid == pid) {
172 g_sessions.erase(it);
173 removed = true;
174 }
175 }
176 if (removed) {
177 char msg[320];
178 std::snprintf(msg, sizeof(msg),
179 _("[bastionguard] secure browser pid %ld closed for root: %s — browser proxy restored\n"),
180 static_cast<long>(pid), root.c_str());
181 std::cerr << msg;
182 }
183}
184
185struct TunnelSession : std::enable_shared_from_this<TunnelSession> {
186 tcp::socket client_;
187 tcp::socket remote_;
188 std::array<char, 64 * 1024> cbuf_{};
189 std::array<char, 64 * 1024> rbuf_{};
190
191 TunnelSession(tcp::socket c, tcp::socket r)
192 : client_(std::move(c)), remote_(std::move(r)) {}
193
194 void start() { pump_c2r(); pump_r2c(); }
195
196private:
197 void pump_c2r() {
198 auto self = shared_from_this();
199 client_.async_read_some(asio::buffer(cbuf_),
200 [this, self](boost::system::error_code ec, std::size_t n) {
201 if (ec || n == 0) { shutdown(); return; }
202 asio::async_write(remote_, asio::buffer(cbuf_.data(), n),
203 [this, self](boost::system::error_code wec, std::size_t) {
204 if (wec) { shutdown(); return; }
205 pump_c2r();
206 });
207 });
208 }
209
210 void pump_r2c() {
211 auto self = shared_from_this();
212 remote_.async_read_some(asio::buffer(rbuf_),
213 [this, self](boost::system::error_code ec, std::size_t n) {
214 if (ec || n == 0) { shutdown(); return; }
215 asio::async_write(client_, asio::buffer(rbuf_.data(), n),
216 [this, self](boost::system::error_code wec, std::size_t) {
217 if (wec) { shutdown(); return; }
218 pump_r2c();
219 });
220 });
221 }
222
223 void shutdown() {
224 if (closed_.exchange(true)) return;
225 boost::system::error_code ignore;
226 client_.close(ignore);
227 remote_.close(ignore);
228 }
229
230 std::atomic<bool> closed_{false};
231};
232
233static void launch_sandbox(const std::string& url,
234 bool is_bank,
235 const std::string& root) {
236 const std::string binary = is_bank ? "/usr/bin/BastionGuard-bankopener"
237 : "/usr/bin/BastionGuard-secure";
238
239 // Mantieni il processo reale come figlio del proxy. In questo modo il
240 // proxy conosce il PID della sandbox e può liberare sessione/gate appena
241 // l'utente chiude il Secure Browser. waitpid viene eseguito in un thread
242 // dedicato, quindi nessun io-thread asio viene bloccato.
243 pid_t pid = fork();
244 if (pid < 0) {
245 std::cerr << _("[bastionguard] fork() fallita per launch_sandbox\n");
246 sandbox_close(root);
247 session_close(root);
248 return;
249 }
250
251 if (pid == 0) {
252 setenv("BG_SANDBOX", "1", 1);
253 execl(binary.c_str(), binary.c_str(), url.c_str(), (char*)nullptr);
254 _exit(127);
255 }
256
257 // Associa il PID alla generazione corrente della sessione/gate PRIMA di
258 // avviare il watcher. Le varianti *_if_pid evitano che un vecchio processo
259 // possa chiudere per errore una sessione nuova dello stesso dominio.
260 session_bind_pid(root, pid);
261 sandbox_bind_pid(root, pid);
262
263 {
264 char msg[384];
265 std::snprintf(msg, sizeof(msg),
266 _("[bastionguard] secure browser started: pid=%ld root=%s binary=%s\n"),
267 static_cast<long>(pid), root.c_str(), binary.c_str());
268 std::cerr << msg;
269 }
270
271 std::thread([pid, root]() {
272 int status = 0;
273 pid_t rc;
274 do {
275 rc = waitpid(pid, &status, 0);
276 } while (rc < 0 && errno == EINTR);
277
278 if (rc < 0) {
279 const int wait_err = errno;
280 char msg[320];
281 std::snprintf(msg, sizeof(msg),
282 _("[bastionguard] waitpid(%ld) failed for root %s: %s; uso TTL fallback\n"),
283 static_cast<long>(pid), root.c_str(), std::strerror(wait_err));
284 std::cerr << msg;
285 // Non liberare alla cieca: se non siamo riusciti a osservare la
286 // morte del figlio, il vecchio TTL resta il fallback sicuro.
287 return;
288 }
289
290 // Prima libera il gate e poi la sessione. Se Firefox ritenta proprio
291 // in questa piccola finestra, vede ancora la sessione attiva e non può
292 // creare un doppio browser. Subito dopo la sessione viene rimossa e il
293 // tentativo successivo riapre normalmente la sandbox.
294 sandbox_close_if_pid(root, pid);
295 session_close_if_pid(root, pid);
296 }).detach();
297}
298
300 bool is_connect = false;
301 bool is_http_plain = false;
302 std::string host;
303 std::string port;
304 std::string method;
305 std::string path;
306 std::string raw_headers;
307};
308
309static bool parse_request(const std::string& first_line, ParsedRequest& out) {
310 std::istringstream ss(first_line);
311 std::string method, target, ver;
312 ss >> method >> target >> ver;
313 if (method.empty() || target.empty()) return false;
314
315 out.method = method;
316
317 if (method == "CONNECT") {
318 const auto p = target.find(':');
319 if (p == std::string::npos) return false;
320 out.host = target.substr(0, p);
321 out.port = target.substr(p + 1);
322 out.is_connect = true;
323 return !out.host.empty() && !out.port.empty();
324 }
325
326 if (target.size() > 7 && target.substr(0, 7) == "http://") {
327 std::string rest = target.substr(7);
328 const auto slash = rest.find('/');
329 const std::string authority = slash != std::string::npos ? rest.substr(0, slash) : rest;
330 out.path = slash != std::string::npos ? rest.substr(slash) : "/";
331
332 const auto colon = authority.rfind(':');
333 if (colon != std::string::npos) {
334 out.host = authority.substr(0, colon);
335 out.port = authority.substr(colon + 1);
336 } else {
337 out.host = authority;
338 out.port = "80";
339 }
340
341 out.is_http_plain = true;
342 return !out.host.empty();
343 }
344
345 return false;
346}
347
348static void async_tunnel(tcp::socket sock, const std::string& host, const std::string& port) {
349 auto resolver = std::make_shared<tcp::resolver>(g_io);
350 auto psock = std::make_shared<tcp::socket>(std::move(sock));
351
352 resolver->async_resolve(host, port,
353 [resolver, psock](boost::system::error_code ec, tcp::resolver::results_type eps) mutable {
354 if (ec) {
355 static constexpr char bad[] =
356 "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
357 asio::async_write(*psock, asio::buffer(bad, sizeof(bad) - 1), [psock](auto, auto) {});
358 return;
359 }
360
361 auto remote = std::make_shared<tcp::socket>(g_io);
362 asio::async_connect(*remote, eps,
363 [remote, psock](boost::system::error_code ec, const tcp::endpoint&) mutable {
364 if (ec) {
365 static constexpr char bad[] =
366 "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
367 asio::async_write(*psock, asio::buffer(bad, sizeof(bad) - 1), [psock](auto, auto) {});
368 return;
369 }
370
371 static constexpr char ok[] =
372 "HTTP/1.1 200 Connection Established\r\n\r\n";
373
374 auto ok_buf = std::make_shared<std::string>(ok);
375 asio::async_write(*psock, asio::buffer(*ok_buf),
376 [ok_buf, psock, remote](boost::system::error_code ec, std::size_t) mutable {
377 if (ec) return;
378 auto session = std::make_shared<TunnelSession>(std::move(*psock), std::move(*remote));
379 session->start();
380 });
381 });
382 });
383}
384
385static void async_forward_http(tcp::socket sock,
386 const ParsedRequest& req,
387 const std::string& full_request)
388{
389 std::string rewritten = req.method + " " + req.path + " HTTP/1.1\r\n";
390
391 static const std::vector<std::string> strip_headers = {
392 "via:",
393 "x-forwarded-for:",
394 "x-forwarded-host:",
395 "x-forwarded-proto:",
396 "x-real-ip:",
397 "forwarded:",
398 "proxy-connection:",
399 "proxy-authorization:",
400 "proxy-authenticate:",
401 };
402
403 auto header_should_strip = [](const std::string& line) -> bool {
404 std::string lower = line;
405 for (auto& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
406 for (const auto& h : strip_headers) {
407 if (lower.rfind(h, 0) == 0) return true;
408 }
409 return false;
410 };
411
412 const auto first_crlf = full_request.find("\r\n");
413 if (first_crlf != std::string::npos) {
414 std::istringstream ss(full_request.substr(first_crlf + 2));
415 std::string hdr_line;
416 while (std::getline(ss, hdr_line)) {
417 if (!hdr_line.empty() && hdr_line.back() == '\r')
418 hdr_line.pop_back();
419 if (hdr_line.empty()) {
420 rewritten += "\r\n";
421 break;
422 }
423 if (!header_should_strip(hdr_line))
424 rewritten += hdr_line + "\r\n";
425 }
426 } else {
427 rewritten += "\r\n";
428 }
429
430 auto resolver = std::make_shared<tcp::resolver>(g_io);
431 auto psock = std::make_shared<tcp::socket>(std::move(sock));
432 auto req_buf = std::make_shared<std::string>(std::move(rewritten));
433
434 resolver->async_resolve(req.host, req.port,
435 [resolver, psock, req_buf](boost::system::error_code ec, tcp::resolver::results_type eps) mutable {
436 if (ec) {
437 static constexpr char bad[] =
438 "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
439 asio::async_write(*psock, asio::buffer(bad, sizeof(bad) - 1), [psock](auto, auto) {});
440 return;
441 }
442
443 auto remote = std::make_shared<tcp::socket>(g_io);
444 asio::async_connect(*remote, eps,
445 [remote, psock, req_buf](boost::system::error_code ec, const tcp::endpoint&) mutable {
446 if (ec) {
447 static constexpr char bad[] =
448 "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
449 asio::async_write(*psock, asio::buffer(bad, sizeof(bad) - 1), [psock](auto, auto) {});
450 return;
451 }
452
453 asio::async_write(*remote, asio::buffer(*req_buf),
454 [remote, psock, req_buf](boost::system::error_code ec, std::size_t) mutable {
455 if (ec) return;
456 auto session = std::make_shared<TunnelSession>(std::move(*psock), std::move(*remote));
457 session->start();
458 });
459 });
460 });
461}
462
463static std::string warning_url_for(const std::string& host, const std::string& type) {
464 return "https://bastionguard.eu/secure.html?host=" + host + "&type=" + type;
465}
466
467static std::string local_warning_url_for(const std::string& host, const std::string& type) {
468 return std::string("http://") + BG_WARNING_ADDR + ":"
469 + std::to_string(BG_WARNING_HTTP_PORT)
470 + "/" + host + "?type=" + type;
471}
472
473
474static std::mutex g_notify_mutex;
475static std::unordered_set<std::string> g_notify_open;
476
477static void open_notify_window(const std::string& root,
478 const std::string& host,
479 const std::string& type,
480 int argc,
481 char** argv)
482{
483 {
484 std::lock_guard<std::mutex> lock(g_notify_mutex);
485 if (g_notify_open.count(root)) return;
486 g_notify_open.insert(root);
487 }
488
489 const std::string url = warning_url_for(host, type);
490
491 std::thread([root, url, argc, argv]() {
492 try {
493 SecureBrowser::open_notify(url, argc, argv);
494 } catch (const std::exception& e) {
495 char buf[512];
496 std::snprintf(buf, sizeof(buf),
497 _("[bastionguard] open_notify exception: %s\n"), e.what());
498 std::cerr << buf;
499 } catch (...) {
500 std::cerr << _("[bastionguard] open_notify unknown exception\n");
501 }
502 std::lock_guard<std::mutex> lock(g_notify_mutex);
503 g_notify_open.erase(root);
504 }).detach();
505}
506
507
508static std::mutex g_sandbox_mutex;
509static std::unordered_set<std::string> g_sandbox_open;
510static std::unordered_map<std::string, pid_t> g_sandbox_pid;
511
512static bool sandbox_try_open(const std::string& root) {
513 std::lock_guard<std::mutex> lock(g_sandbox_mutex);
514 const bool inserted = g_sandbox_open.insert(root).second;
515 if (inserted)
516 g_sandbox_pid[root] = -1;
517 return inserted;
518}
519
520static void sandbox_bind_pid(const std::string& root, pid_t pid) {
521 std::lock_guard<std::mutex> lock(g_sandbox_mutex);
522 if (g_sandbox_open.count(root))
523 g_sandbox_pid[root] = pid;
524}
525
526static void sandbox_close_if_pid(const std::string& root, pid_t pid) {
527 std::lock_guard<std::mutex> lock(g_sandbox_mutex);
528 auto it = g_sandbox_pid.find(root);
529 if (it != g_sandbox_pid.end() && it->second == pid) {
530 g_sandbox_pid.erase(it);
531 g_sandbox_open.erase(root);
532 }
533}
534
535static void sandbox_close(const std::string& root) {
536 std::lock_guard<std::mutex> lock(g_sandbox_mutex);
537 g_sandbox_pid.erase(root);
538 g_sandbox_open.erase(root);
539}
540
541static void block_active_session(std::shared_ptr<tcp::socket> sock,
542 const ParsedRequest& req,
543 const std::string& type)
544{
545 if (req.is_connect) {
546 static constexpr char ok200[] =
547 "HTTP/1.1 200 Connection Established\r\n\r\n";
548
549 const std::string intercept_host = req.host;
550 const std::string intercept_type = type;
551
552 auto ok_buf = std::make_shared<std::string>(ok200);
553 asio::async_write(*sock, asio::buffer(*ok_buf),
554 [sock, ok_buf, intercept_host, intercept_type](
555 boost::system::error_code ec, std::size_t) mutable {
556 if (ec) return;
557 std::thread([s = std::move(*sock),
558 host = intercept_host,
559 t = intercept_type]() mutable {
560 // Nessun on_url_ready: la sandbox è già aperta.
561 TlsIntercept::do_intercept(std::move(s), host, t);
562 }).detach();
563 });
564 } else {
565 const std::string loc = warning_url_for(req.host, type);
566 auto redir = std::make_shared<std::string>(
567 "HTTP/1.1 302 Found\r\n"
568 "Cache-Control: no-store\r\n"
569 "Pragma: no-cache\r\n"
570 "Location: " + loc + "\r\n"
571 "Connection: close\r\n\r\n");
572 asio::async_write(*sock, asio::buffer(*redir), [sock, redir](auto, auto) {});
573 }
574}
575
576static void block_secure_request(std::shared_ptr<tcp::socket> sock,
577 const ParsedRequest& req,
578 const std::string& type,
579 const std::string& root,
580 int argc, char** argv)
581{
582 if (req.is_connect) {
583 static constexpr char ok200[] =
584 "HTTP/1.1 200 Connection Established\r\n\r\n";
585
586 const std::string intercept_host = req.host;
587 const std::string intercept_type = type;
588 const bool is_bank_type = (type == "bank");
589
590 auto ok_buf = std::make_shared<std::string>(ok200);
591 asio::async_write(*sock, asio::buffer(*ok_buf),
592 [sock, ok_buf, intercept_host, intercept_type, is_bank_type, root, argc, argv](
593 boost::system::error_code ec, std::size_t) mutable {
594 if (ec) return;
595
596 std::thread([s = std::move(*sock),
597 host = intercept_host,
598 t = intercept_type,
599 bank = is_bank_type,
600 r = root,
601 argc, argv]() mutable {
602 auto on_url_ready = [bank, r, t, host, argc, argv](const std::string& full_url) {
603 // Questo callback viene invocato SOLO da do_intercept
604 // per top-level GET/HEAD. Iframe/fetch/POST vengono
605 // inoltrati in-place e non devono aprire una sandbox.
606 if (!session_try_open(r, t)) return;
607
608 open_notify_window(r, host, t, argc, argv);
609
610 if (!sandbox_try_open(r)) {
611 // Stato incoerente/stale: non lasciare una sessione
612 // attiva senza un browser reale.
613 session_close(r);
614 return;
615 }
616
617 launch_sandbox(full_url, bank, r);
618 };
619 TlsIntercept::do_intercept(std::move(s), host, t,
620 std::move(on_url_ready));
621 }).detach();
622 });
623 } else {
624 const std::string full_url = "https://" + req.host + req.path;
625 if (session_try_open(root, type)) {
626 open_notify_window(root, req.host, type, argc, argv);
627 if (sandbox_try_open(root)) {
628 std::thread([full_url, type, root]() {
629 launch_sandbox(full_url, type == "bank", root);
630 }).detach();
631 } else {
632 session_close(root);
633 }
634 }
635
636 const std::string loc = warning_url_for(req.host, type);
637 auto redir = std::make_shared<std::string>(
638 "HTTP/1.1 302 Found\r\n"
639 "Cache-Control: no-store\r\n"
640 "Pragma: no-cache\r\n"
641 "Location: " + loc + "\r\n"
642 "Connection: close\r\n\r\n");
643 asio::async_write(*sock, asio::buffer(*redir), [sock, redir](auto, auto) {});
644 }
645}
646
647static void handle_client(tcp::socket client, int argc, char** argv) {
648 auto sbuf = std::make_shared<asio::streambuf>();
649 auto sock = std::make_shared<tcp::socket>(std::move(client));
650
651 asio::async_read_until(*sock, *sbuf, "\r\n\r\n",
652 [sock, sbuf, argc, argv](boost::system::error_code ec, std::size_t) mutable {
653 if (ec) return;
654
655 std::string full_request{
656 asio::buffers_begin(sbuf->data()),
657 asio::buffers_end(sbuf->data())
658 };
659
660 std::istringstream is(full_request);
661 std::string first_line;
662 std::getline(is, first_line);
663 if (!first_line.empty() && first_line.back() == '\r')
664 first_line.pop_back();
665
666 ParsedRequest req;
667 if (!parse_request(first_line, req)) {
668 static constexpr char resp[] =
669 "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
670 asio::async_write(*sock, asio::buffer(resp, sizeof(resp) - 1), [sock](auto, auto) {});
671 return;
672 }
673
674 // -----------------------------------------------------------------
675 // FAST-PATH: traffico LAN/loopback non passa MAI dal MITM CEF.
676 //
677 // Anche se il browser è configurato col proxy del sistema, per:
678 // - 127.0.0.0/8, 10.0.0.0/8, 172.16/12, 192.168/16,
679 // 169.254/16, 100.64/10, ::1, fe80::/10, fc00::/7
680 // - hostname *.local, *.lan, *.intranet, *.internal, ...
681 // - hostname brevi senza punti (router, nas, printer)
682 // facciamo tunnel diretto e basta. Niente lookup banca/payment,
683 // niente notify, niente block: il traffico interno alla LAN
684 // resta affare dell'utente, BastionGuard non lo vede.
685 // -----------------------------------------------------------------
687 {
688 char dbg[256];
689 std::snprintf(dbg, sizeof(dbg),
690 "[bastionguard] LAN/loopback bypass: %s\n",
691 req.host.c_str());
692 std::cerr << dbg;
693 }
694 if (req.is_connect)
695 async_tunnel(std::move(*sock), req.host, req.port);
696 else
697 async_forward_http(std::move(*sock), req, full_request);
698 return;
699 }
700
701 const std::string root = root_domain(req.host);
702 const bool bypass = domain_filter().is_proxy_bypass(req.host);
703 const bool secure = domain_filter().is_secure(req.host);
704 const bool bank = domain_filter().is_bank(req.host);
705
706 {
707 char dbg[256];
708 std::snprintf(dbg, sizeof(dbg),
709 "[bastionguard] request host=%s bypass=%d intercept=%d bank=%d method=%s\n",
710 req.host.c_str(),
711 static_cast<int>(bypass),
712 static_cast<int>(secure),
713 static_cast<int>(bank),
714 req.method.c_str());
715 std::cerr << dbg;
716 }
717
718 if (bypass) {
719 {
720 char dbg[256];
721 std::snprintf(dbg, sizeof(dbg),
722 "[bastionguard] PROXY-BYPASS (user configured): %s\n",
723 req.host.c_str());
724 std::cerr << dbg;
725 }
726 if (req.is_connect)
727 async_tunnel(std::move(*sock), req.host, req.port);
728 else
729 async_forward_http(std::move(*sock), req, full_request);
730 return;
731 }
732
733 std::string active_type;
734 const bool has_active_session = session_active(root, active_type);
735 if (!secure) {
736 {
737 char dbg[256];
738 std::snprintf(dbg, sizeof(dbg),
739 "[bastionguard] TUNNEL-ONLY (no MITM): %s\n",
740 req.host.c_str());
741 std::cerr << dbg;
742 }
743 if (req.is_connect)
744 async_tunnel(std::move(*sock), req.host, req.port);
745 else
746 async_forward_http(std::move(*sock), req, full_request);
747 return;
748 }
749 if (has_active_session) {
750 char msg[256];
751 std::snprintf(msg, sizeof(msg),
752 _("[bastionguard] secure domain already active, blocking again: %s (session: %s)\n"),
753 req.host.c_str(), root.c_str());
754 std::cerr << msg;
755 block_active_session(sock, req, active_type);
756 return;
757 }
758 const std::string type = bank ? "bank" : "payment";
759
760 {
761 char msg[256];
762 std::snprintf(msg, sizeof(msg),
763 _("[bastionguard] %s intercepted: %s (root: %s)\n"),
764 type.c_str(), req.host.c_str(), root.c_str());
765 std::cerr << msg;
766 }
767
768 // Non aprire ancora sessione/gate: per HTTPS dobbiamo prima
769 // vedere i Fetch Metadata dentro TLS e distinguere top-level da
770 // iframe/POST. Solo la vera navigazione top-level apre CEF.
771 block_secure_request(sock, req, type, root, argc, argv);
772 });
773}
774
775
776static void do_accept(tcp::acceptor& acceptor, int argc, char** argv) {
777 acceptor.async_accept(g_io,
778 [&acceptor, argc, argv](boost::system::error_code ec, tcp::socket client) {
779 if (ec == asio::error::operation_aborted) return;
780
781 if (!ec) {
782 handle_client(std::move(client), argc, argv);
783 } else {
784 char buf[256];
785 std::snprintf(buf, sizeof(buf), _("[cef] accept error: %s\n"), ec.message().c_str());
786 std::cerr << buf;
787 }
788
789 do_accept(acceptor, argc, argv);
790 });
791}
792
793static bool running_confined();
794
795class BastionApp : public CefApp, public CefBrowserProcessHandler {
796public:
798
799 CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override {
800 return this;
801 }
802
803 void OnBeforeCommandLineProcessing(const CefString&, CefRefPtr<CefCommandLine> cmd) override {
804 cmd->AppendSwitch("incognito");
805 cmd->AppendSwitch("disable-extensions");
806 cmd->AppendSwitch("no-first-run");
807 cmd->AppendSwitch("no-default-browser-check");
808 cmd->AppendSwitch("disable-gpu");
809 cmd->AppendSwitch("disable-gpu-compositing");
810 cmd->AppendSwitch("disable-software-rasterizer");
811 cmd->AppendSwitch("disable-gpu-process-crash-limit");
812 cmd->AppendSwitch("in-process-gpu");
813 cmd->AppendSwitch("disable-accelerated-2d-canvas");
814 cmd->AppendSwitch("disable-accelerated-video-decode");
815 cmd->AppendSwitch("disable-webgl");
816
817 // ANTI-CRASH IN SANDBOX: settings.no_sandbox copre il processo
818 // browser, ma i sottoprocessi (zygote, renderer, gpu, utility)
819 // leggono i flag dalla command line. In ambiente confinato dobbiamo
820 // propagare esplicitamente --no-sandbox a tutti, altrimenti la zygote
821 // tenta comunque clone(CLONE_NEWUSER) e aborta.
822 if (no_sandbox_) {
823 if (!cmd->HasSwitch("no-sandbox"))
824 cmd->AppendSwitch("no-sandbox");
825 if (!cmd->HasSwitch("disable-setuid-sandbox"))
826 cmd->AppendSwitch("disable-setuid-sandbox");
827 if (!cmd->HasSwitch("disable-namespace-sandbox"))
828 cmd->AppendSwitch("disable-namespace-sandbox");
829 }
830 }
831
832 // Impostato dal processo browser prima di CefInitialize. I sottoprocessi
833 // ereditano il flag via command line (--no-sandbox), quindi qui basta
834 // riflettere la decisione presa in main().
835 void set_no_sandbox(bool v) { no_sandbox_ = v; }
836
838
839private:
840 bool no_sandbox_ = false;
841};
842
843static bool parse_args(int argc, char** argv, std::string& listen_host, int& listen_port) {
844 for (int i = 1; i < argc; ++i) {
845 const std::string_view a = argv[i];
846
847 if (a.starts_with("--type=") || a.starts_with("--lang=") ||
848 a.starts_with("--user-data-dir=") || a.starts_with("--enable-") ||
849 a.starts_with("--disable-") || a.starts_with("--service-") ||
850 a.starts_with("--utility-") || a.starts_with("--ozone-") ||
851 a.starts_with("--render-node-") || a.starts_with("--change-stack-") ||
852 a.starts_with("--no-zygote"))
853 continue;
854
855 if (a == "--listen" && i + 1 < argc) {
856 listen_host = argv[++i];
857 } else if (a == "--port" && i + 1 < argc) {
858 try {
859 const int p = std::stoi(argv[++i]);
860 if (p < 1 || p > 65535) throw std::out_of_range("port");
861 listen_port = p;
862 } catch (...) {
863 std::cerr << _("[args] invalid --port value\n");
864 return false;
865 }
866 }
867 }
868
869 return true;
870}
871
872
873// ---------------------------------------------------------------------------
874// Rilevazione ambiente sandbox/confinato.
875//
876// Il sandbox interno di Chromium/CEF (no_sandbox=false) richiede o il binario
877// chrome-sandbox SUID-root, oppure i namespace utente non privilegiati
878// (kernel.unprivileged_userns_clone=1). Quando bastionguard_cef gira già
879// dentro un confinamento (Flatpak, Snap, AppImage/bubblewrap, container, o
880// avviato con BG_SANDBOX=1 dal launch_sandbox() qui sopra), il sandbox
881// ANNIDATO non può inizializzarsi: clone(CLONE_NEWUSER) fallisce con EPERM
882// e la zygote di Chromium aborta → CRASH dell'intero processo.
883//
884// In questi casi dobbiamo forzare no_sandbox=true. Questo è esattamente ciò
885// che fa già SecureBrowser.cpp (settings.no_sandbox = true).
886// ---------------------------------------------------------------------------
887static bool running_confined() {
888 // 1) Flag esplicito impostato da launch_sandbox() / dal wrapper.
889 if (std::getenv("BG_SANDBOX")) return true;
890 if (std::getenv("BG_NO_CEF_SANDBOX")) return true;
891
892 // 2) Flatpak / Snap.
893 if (std::getenv("FLATPAK_ID")) return true;
894 if (std::getenv("SNAP")) return true;
895 if (access("/.flatpak-info", F_OK) == 0) return true;
896
897 // 3) Container generico (Docker/Podman/systemd-nspawn ecc.).
898 if (access("/run/.containerenv", F_OK) == 0) return true;
899 {
900 std::ifstream cg("/proc/1/cgroup");
901 std::string line;
902 while (std::getline(cg, line)) {
903 if (line.find("docker") != std::string::npos ||
904 line.find("libpod") != std::string::npos ||
905 line.find("/lxc") != std::string::npos) {
906 return true;
907 }
908 }
909 }
910
911 // 4) Namespace utente non privilegiati disabilitati nel kernel:
912 // senza di essi il sandbox di Chromium non parte (a meno del SUID
913 // helper, che in molte installazioni/portali non è disponibile).
914 {
915 std::ifstream f("/proc/sys/kernel/unprivileged_userns_clone");
916 if (f.good()) {
917 int v = 1;
918 f >> v;
919 if (v == 0) return true;
920 }
921 }
922
923 // 5) Il SUID sandbox helper di CEF non è presente/eseguibile: senza di
924 // esso e senza userns, il sandbox non può attivarsi.
925 // (Percorsi tipici di installazione CEF di BastionGuard.)
926 {
927 static const char* helpers[] = {
928 "/usr/share/BastionGuard/cef/chrome-sandbox",
929 "/usr/lib/BastionGuard/chrome-sandbox",
930 };
931 bool any_helper = false;
932 for (const char* h : helpers) {
933 struct stat st{};
934 if (stat(h, &st) == 0) {
935 any_helper = true;
936 // Deve essere SUID-root per funzionare.
937 if ((st.st_mode & S_ISUID) && st.st_uid == 0) {
938 any_helper = true;
939 }
940 }
941 }
942 // Se nessun helper SUID valido e nessun userns, meglio disattivare.
943 if (!any_helper) {
944 std::ifstream f("/proc/sys/kernel/unprivileged_userns_clone");
945 int v = 1;
946 if (f.good()) f >> v;
947 if (v == 0) return true;
948 }
949 }
950
951 return false;
952}
953
954
955int main(int argc, char** argv) {
957 std::cerr << "[bastionguard-cef] CEF disabilitato su questa build o distribuzione.\n";
958 return 78;
959 }
960
962
963 bindtextdomain("bastionguard", LOCALEDIR);
964 bind_textdomain_codeset("bastionguard", "UTF-8");
965 textdomain("bastionguard");
966
967 CefMainArgs main_args(argc, argv);
968 CefRefPtr<CefApp> app = new BastionApp();
969
970 const int exit_code = CefExecuteProcess(main_args, app, nullptr);
971 if (exit_code >= 0) return exit_code;
972
973 std::string listen_host = "127.0.0.1";
974 int listen_port = 3130;
975 if (!parse_args(argc, argv, listen_host, listen_port)) return 2;
976
977 CefSettings settings;
978
979 // ANTI-CRASH IN SANDBOX: il sandbox interno di Chromium non può
980 // annidarsi dentro un altro confinamento. Se siamo confinati, lo
981 // disabilitiamo per evitare l'abort della zygote (vedi running_confined()).
982 // La decisione è preventiva: CefInitialize non si può ri-eseguire nello
983 // stesso processo, quindi NON tentiamo un retry in-process (inefficace);
984 // ci affidiamo alla rilevazione a monte, come fa SecureBrowser.
985 const bool confined = running_confined();
986 settings.no_sandbox = confined;
987 settings.multi_threaded_message_loop = true;
988 settings.windowless_rendering_enabled = false;
989
990 if (confined) {
991 std::cerr << _("[cef] ambiente confinato rilevato → sandbox CEF disabilitato\n");
992 }
993
994 if (!CefInitialize(main_args, settings, app, nullptr)) {
995 std::cerr << _("[cef] CefInitialize failed\n");
996 return 1;
997 }
998
999 std::cerr << (settings.no_sandbox
1000 ? _("[cef] initialized (sandbox OFF)\n")
1001 : _("[cef] initialized (sandbox ON)\n"));
1002 CaInstaller::ensure_trusted();
1003
1004 domain_filter().load();
1005 LocalWarningServer warning_server(
1009 warning_server.start();
1010
1011 ProxyConfig::apply({ listen_host, listen_port });
1012
1013 struct Guard {
1015 ~Guard() {
1016 ProxyConfig::restore();
1017 ws.stop();
1018 }
1019 } guard{warning_server};
1020
1021 std::vector<std::thread> io_threads;
1022 io_threads.reserve(IO_THREADS);
1023
1024 {
1025 char buf[128];
1026 std::snprintf(buf, sizeof(buf), _("[cef] starting %u I/O threads\n"), IO_THREADS);
1027 std::cerr << buf;
1028 }
1029
1030 asio::signal_set sig(g_io, SIGINT, SIGTERM);
1031 sig.async_wait([&](const boost::system::error_code&, int) {
1032 std::cerr << _("[cef] signal received, exiting\n");
1033 ProxyConfig::restore();
1034 guard.ws.stop();
1035 g_io.stop();
1036 });
1037
1038 asio::signal_set sighup(g_io, SIGHUP);
1039 std::function<void()> arm_sighup = [&]() {
1040 sighup.async_wait([&](const boost::system::error_code& ec, int) {
1041 if (ec) return;
1042 std::cerr << _("[filter] SIGHUP: reloading domain lists\n");
1043 domain_filter().load();
1044 arm_sighup();
1045 });
1046 };
1047 arm_sighup();
1048
1049 tcp::acceptor acceptor(g_io);
1050 {
1051 boost::system::error_code ec;
1052 tcp::endpoint ep(asio::ip::make_address(listen_host),
1053 static_cast<unsigned short>(listen_port));
1054
1055 acceptor.open(ep.protocol(), ec);
1056 if (!ec) acceptor.set_option(asio::socket_base::reuse_address(true), ec);
1057 if (!ec) acceptor.bind(ep, ec);
1058 if (!ec) acceptor.listen(asio::socket_base::max_listen_connections, ec);
1059
1060 if (ec) {
1061 char buf[256];
1062 std::snprintf(buf, sizeof(buf), _("[cef] acceptor error: %s\n"), ec.message().c_str());
1063 std::cerr << buf;
1064 ProxyConfig::restore();
1065 warning_server.stop();
1066 CefShutdown();
1067 return 1;
1068 }
1069 }
1070
1071 {
1072 char buf[256];
1073 std::snprintf(buf, sizeof(buf), _("[cef] backend proxy listening on %s:%d\n"),
1074 listen_host.c_str(), listen_port);
1075 std::cerr << buf;
1076 }
1077
1078 do_accept(acceptor, argc, argv);
1079
1080 for (unsigned int i = 1; i < IO_THREADS; ++i)
1081 io_threads.emplace_back([]() { g_io.run(); });
1082
1083 g_io.run();
1084
1085 for (auto& t : io_threads)
1086 if (t.joinable()) t.join();
1087
1088 CefShutdown();
1089 return 0;
1090}
#define _(s)
int main(int argc, char *argv[])
DomainFilter & domain_filter()
static bool running_confined()
static bool parse_args(int argc, char **argv, std::string &listen_host, int &listen_port)
static bool session_try_open(const std::string &root, const std::string &type)
static bool sandbox_try_open(const std::string &root)
static void block_active_session(std::shared_ptr< tcp::socket > sock, const ParsedRequest &req, const std::string &type)
std::chrono::steady_clock Clock
static std::unordered_map< std::string, SessionEntry > g_sessions
static void launch_sandbox(const std::string &url, bool is_bank, const std::string &root)
static void async_tunnel(tcp::socket sock, const std::string &host, const std::string &port)
static std::mutex g_session_mutex
static std::unordered_set< std::string > g_notify_open
static void do_accept(tcp::acceptor &acceptor, int argc, char **argv)
asio::ip::tcp tcp
#define BG_WARNING_HTTP_PORT
static void session_close_if_pid(const std::string &root, pid_t pid)
static void async_forward_http(tcp::socket sock, const ParsedRequest &req, const std::string &full_request)
static std::string local_warning_url_for(const std::string &host, const std::string &type)
#define BG_WARNING_ADDR
static bool session_active(const std::string &root, std::string &out_type)
#define BG_WARNING_HTTPS_PORT
static void open_notify_window(const std::string &root, const std::string &host, const std::string &type, int argc, char **argv)
static void session_close(const std::string &root)
static void sandbox_bind_pid(const std::string &root, pid_t pid)
static std::string root_domain(const std::string &host)
static void sandbox_close(const std::string &root)
#define LOCALEDIR
static void block_secure_request(std::shared_ptr< tcp::socket > sock, const ParsedRequest &req, const std::string &type, const std::string &root, int argc, char **argv)
static const unsigned int IO_THREADS
static std::mutex g_sandbox_mutex
static void handle_client(tcp::socket client, int argc, char **argv)
static std::string warning_url_for(const std::string &host, const std::string &type)
static bool parse_request(const std::string &first_line, ParsedRequest &out)
static std::unordered_map< std::string, pid_t > g_sandbox_pid
static std::mutex g_notify_mutex
static std::unordered_set< std::string > g_sandbox_open
static void session_bind_pid(const std::string &root, pid_t pid)
static void sandbox_close_if_pid(const std::string &root, pid_t pid)
static asio::io_context g_io
static constexpr auto SESSION_TTL
IMPLEMENT_REFCOUNTING(BastionApp)
CefRefPtr< CefBrowserProcessHandler > GetBrowserProcessHandler() override
void OnBeforeCommandLineProcessing(const CefString &, CefRefPtr< CefCommandLine > cmd) override
void set_no_sandbox(bool v)
bool is_secure(const std::string &host) const
bool is_bank(const std::string &host) const
static bool is_local_network(const std::string &host_raw)
bool is_proxy_bypass(const std::string &host) const
static void open_notify(const std::string &url, int argc, char **argv)
static void apply_language()
#define S_ISUID
std::string raw_headers
Clock::time_point opened_at
std::atomic< bool > closed_
std::array< char, 64 *1024 > rbuf_
std::array< char, 64 *1024 > cbuf_
TunnelSession(tcp::socket c, tcp::socket r)