/* * 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 . * * BastionGuard™ is a trademark of Calogero Scarnà. * The BastionGuard™ name and branding are not licensed under the GPL. */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "block_page.hpp" #include "ca_installer.hpp" namespace asio = boost::asio; using tcp = asio::ip::tcp; namespace fs = std::filesystem; namespace TlsIntercept { static constexpr long CTX_CACHE_TTL_SEC = 30L * 24 * 3600; static inline void set_no_expiry(X509* cert) { ASN1_TIME* not_after = X509_getm_notAfter(cert); ASN1_GENERALIZEDTIME_set_string(not_after, "99991231235959Z"); } static inline void set_realistic_validity(X509* cert) { X509_gmtime_adj(X509_getm_notBefore(cert), -43200L); X509_gmtime_adj(X509_getm_notAfter(cert), 397L * 24 * 3600); } namespace detail { using Clock = std::chrono::steady_clock; static inline fs::path ca_dir() { const char* h = std::getenv("HOME"); return fs::path(h ? h : "/tmp") / ".local/share/BastionGuard/certs"; } static inline fs::path ca_cert_path() { return ca_dir() / "intercept-ca.crt.pem"; } static inline fs::path ca_key_path() { return ca_dir() / "intercept-ca.key.pem"; } static inline std::string drain_errors() { std::ostringstream o; unsigned long e; bool first = true; while ((e = ERR_get_error())) { char b[256]; ERR_error_string_n(e, b, sizeof(b)); if (!first) o << " | "; o << b; first = false; } return first ? "no error" : o.str(); } static inline EVP_PKEY* gen_rsa(int bits) { EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); if (!ctx) return nullptr; EVP_PKEY* k = nullptr; if (EVP_PKEY_keygen_init(ctx) > 0 && EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) > 0 && EVP_PKEY_keygen(ctx, &k) > 0) { EVP_PKEY_CTX_free(ctx); return k; } EVP_PKEY_CTX_free(ctx); if (k) EVP_PKEY_free(k); return nullptr; } static inline bool add_ext(X509* cert, X509V3_CTX* v3ctx, int nid, const char* val) { X509_EXTENSION* ext = X509V3_EXT_conf_nid(nullptr, v3ctx, nid, const_cast(val)); if (!ext) return false; bool ok = X509_add_ext(cert, ext, -1) == 1; X509_EXTENSION_free(ext); return ok; } static inline bool write_pem(const fs::path& path, std::function writer) { fs::create_directories(path.parent_path()); fs::path tmp = path.string() + ".tmp"; BIO* bio = BIO_new_file(tmp.c_str(), "w"); if (!bio) return false; bool ok = writer(bio) > 0; BIO_free(bio); if (!ok) { fs::remove(tmp); return false; } fs::permissions(tmp, fs::perms::owner_read | fs::perms::owner_write, fs::perm_options::replace); fs::rename(tmp, path); return true; } struct IntermediateCA { X509* cert = nullptr; EVP_PKEY* key = nullptr; ~IntermediateCA() { if (cert) X509_free(cert); if (key) EVP_PKEY_free(key); } }; static inline std::shared_ptr generate_ca() { std::cerr << _("[tls-intercept] generazione CA intermedia RSA 4096...\n"); EVP_PKEY* key = gen_rsa(4096); if (!key) { std::cerr << "[tls-intercept] CA keygen failed\n"; return nullptr; } X509* cert = X509_new(); if (!cert) { EVP_PKEY_free(key); return nullptr; } X509_set_version(cert, 2); unsigned char sbuf[20]; RAND_bytes(sbuf, sizeof(sbuf)); sbuf[0] &= 0x7F; BIGNUM* bn = BN_bin2bn(sbuf, sizeof(sbuf), nullptr); BN_to_ASN1_INTEGER(bn, X509_get_serialNumber(cert)); BN_free(bn); X509_gmtime_adj(X509_getm_notBefore(cert), -3600L); set_no_expiry(cert); X509_set_pubkey(cert, key); X509_NAME* name = X509_get_subject_name(cert); X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, reinterpret_cast("BastionGuard"), -1, -1, 0); X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast("BastionGuard Intercept CA"), -1, -1, 0); X509_set_issuer_name(cert, name); X509V3_CTX v3ctx; X509V3_set_ctx(&v3ctx, cert, cert, nullptr, nullptr, 0); add_ext(cert, &v3ctx, NID_basic_constraints, "critical,CA:TRUE,pathlen:0"); add_ext(cert, &v3ctx, NID_key_usage, "critical,keyCertSign,cRLSign"); add_ext(cert, &v3ctx, NID_subject_key_identifier, "hash"); add_ext(cert, &v3ctx, NID_authority_key_identifier, "keyid:always"); if (X509_sign(cert, key, EVP_sha256()) <= 0) { std::cerr << "[tls-intercept] CA sign failed: " << drain_errors() << "\n"; X509_free(cert); EVP_PKEY_free(key); return nullptr; } // Salva su disco if (!write_pem(ca_cert_path(), [&](BIO* b){ return PEM_write_bio_X509(b, cert); }) || !write_pem(ca_key_path(), [&](BIO* b){ return PEM_write_bio_PrivateKey(b, key, nullptr, nullptr, 0, nullptr, nullptr); })) { std::cerr << "[tls-intercept] CA disk write failed\n"; X509_free(cert); EVP_PKEY_free(key); return nullptr; } std::cerr << _("[tls-intercept] ✅ CA intermedia generata\n"); auto ca = std::make_shared(); ca->cert = cert; ca->key = key; return ca; } // ── Carica CA dal disco ─────────────────────────────────────────────────────── static inline std::shared_ptr load_ca() { FILE* fc = fopen(ca_cert_path().c_str(), "r"); FILE* fk = fopen(ca_key_path().c_str(), "r"); if (!fc || !fk) { if (fc) fclose(fc); if (fk) fclose(fk); return nullptr; } X509* cert = PEM_read_X509(fc, nullptr, nullptr, nullptr); EVP_PKEY* key = PEM_read_PrivateKey(fk, nullptr, nullptr, nullptr); fclose(fc); fclose(fk); if (!cert || !key) { if (cert) X509_free(cert); if (key) EVP_PKEY_free(key); return nullptr; } auto ca = std::make_shared(); ca->cert = cert; ca->key = key; return ca; } static inline bool ca_needs_rotation() { if (!fs::exists(ca_cert_path()) || !fs::exists(ca_key_path())) return true; FILE* f = fopen(ca_cert_path().c_str(), "r"); if (!f) return true; X509* cert = PEM_read_X509(f, nullptr, nullptr, nullptr); fclose(f); if (!cert) return true; X509_free(cert); return false; } static inline void install_generated_ca() { const fs::path system_cert = "/etc/BastionGuard/certs/BastionGuard-ca.crt.pem"; try { fs::create_directories(system_cert.parent_path()); fs::copy_file(ca_cert_path(), system_cert, fs::copy_options::overwrite_existing); fs::permissions(system_cert, fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::others_read, fs::perm_options::replace); std::cerr << _("[tls-intercept] CA copiata in /etc/BastionGuard/certs/\n"); } catch (const std::exception& e) { std::cerr << "[tls-intercept] CA copy to /etc failed: " << e.what() << "\n"; } CaInstaller::ensure_trusted(); } static std::shared_ptr g_ca; static std::mutex g_ca_mutex; static inline std::shared_ptr get_ca() { std::lock_guard lock(g_ca_mutex); if (g_ca) return g_ca; if (!ca_needs_rotation()) { g_ca = load_ca(); if (g_ca) { std::cerr << _("[tls-intercept] CA caricata dal disco\n"); install_generated_ca(); return g_ca; } } g_ca = generate_ca(); if (g_ca) install_generated_ca(); return g_ca; } static inline std::shared_ptr make_ssl_ctx_for_host(const std::string& host, const std::shared_ptr& ca) { EVP_PKEY* leaf_key = gen_rsa(2048); if (!leaf_key) return nullptr; X509* cert = X509_new(); if (!cert) { EVP_PKEY_free(leaf_key); return nullptr; } X509_set_version(cert, 2); unsigned char sbuf[20]; RAND_bytes(sbuf, sizeof(sbuf)); sbuf[0] &= 0x7F; BIGNUM* bn = BN_bin2bn(sbuf, sizeof(sbuf), nullptr); BN_to_ASN1_INTEGER(bn, X509_get_serialNumber(cert)); BN_free(bn); set_realistic_validity(cert); X509_set_pubkey(cert, leaf_key); X509_NAME* subject = X509_get_subject_name(cert); X509_NAME_add_entry_by_txt(subject, "CN", MBSTRING_ASC, reinterpret_cast(host.c_str()), -1, -1, 0); X509_set_issuer_name(cert, X509_get_subject_name(ca->cert)); X509V3_CTX v3ctx; X509V3_set_ctx(&v3ctx, ca->cert, cert, nullptr, nullptr, 0); add_ext(cert, &v3ctx, NID_basic_constraints, "critical,CA:FALSE"); add_ext(cert, &v3ctx, NID_key_usage, "critical,digitalSignature,keyEncipherment"); add_ext(cert, &v3ctx, NID_ext_key_usage, "serverAuth"); add_ext(cert, &v3ctx, NID_subject_key_identifier, "hash"); add_ext(cert, &v3ctx, NID_authority_key_identifier, "keyid:always"); std::string san = "DNS:" + host; if (host.rfind("www.", 0) == 0) san += ",DNS:" + host.substr(4); else san += ",DNS:www." + host; add_ext(cert, &v3ctx, NID_subject_alt_name, san.c_str()); if (X509_sign(cert, ca->key, EVP_sha256()) <= 0) { std::cerr << "[tls-intercept] leaf sign failed: " << drain_errors() << "\n"; X509_free(cert); EVP_PKEY_free(leaf_key); return nullptr; } auto ssl_ctx = std::make_shared(asio::ssl::context::tls_server); SSL_CTX* native = ssl_ctx->native_handle(); SSL_CTX_set_min_proto_version(native, TLS1_2_VERSION); SSL_CTX_set_options(native, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | SSL_OP_CIPHER_SERVER_PREFERENCE); SSL_CTX_set_mode(native, SSL_MODE_AUTO_RETRY); SSL_CTX_set_cipher_list(native, "HIGH:!aNULL:!MD5"); #ifdef TLS1_3_VERSION SSL_CTX_set_ciphersuites(native, "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"); #endif bool ok = SSL_CTX_use_certificate(native, cert) > 0 && SSL_CTX_use_PrivateKey(native, leaf_key) > 0 && SSL_CTX_check_private_key(native) > 0; if (ok) SSL_CTX_add_extra_chain_cert(native, X509_dup(ca->cert)); X509_free(cert); EVP_PKEY_free(leaf_key); if (!ok) { std::cerr << "[tls-intercept] SSL_CTX setup failed: " << drain_errors() << "\n"; return nullptr; } char buf[256]; std::snprintf(buf, sizeof(buf), _("[tls-intercept] leaf cert pronto per: %s\n"), host.c_str()); std::cerr << buf; return ssl_ctx; } struct CacheEntry { std::shared_ptr ctx; Clock::time_point expires_at; }; static std::shared_mutex g_ctx_mutex; static std::unordered_map g_ctx_cache; static inline void evict_expired() { const auto now = Clock::now(); for (auto it = g_ctx_cache.begin(); it != g_ctx_cache.end(); ) it = (it->second.expires_at <= now) ? g_ctx_cache.erase(it) : ++it; } static inline std::shared_ptr get_or_create_ctx(const std::string& host) { { std::shared_lock lock(g_ctx_mutex); auto it = g_ctx_cache.find(host); if (it != g_ctx_cache.end() && it->second.expires_at > Clock::now()) return it->second.ctx; } auto ca = get_ca(); if (!ca) return nullptr; auto new_ctx = make_ssl_ctx_for_host(host, ca); if (!new_ctx) return nullptr; { std::unique_lock lock(g_ctx_mutex); evict_expired(); g_ctx_cache[host] = { new_ctx, Clock::now() + std::chrono::seconds(CTX_CACHE_TTL_SEC) }; } return new_ctx; } static inline std::string make_tls_response(const std::string& host, const std::string& type) { const std::string full = BlockPage::make(host, type); const auto sep = full.find("\r\n\r\n"); const std::string body = (sep != std::string::npos) ? full.substr(sep + 4) : full; std::ostringstream oss; oss << "HTTP/1.1 200 OK\r\n" << "Content-Type: text/html; charset=UTF-8\r\n" << "Content-Length: " << body.size() << "\r\n" << "Cache-Control: no-store, no-cache\r\n" << "X-Frame-Options: DENY\r\n" << "X-BastionGuard: intercepted\r\n" << "Connection: close\r\n\r\n" << body; return oss.str(); } } // namespace detail static inline std::string extract_path_from_request(const std::string& raw) { // Cerca la prima riga (fino a \r\n o \n) const auto eol = raw.find('\n'); const std::string line = (eol != std::string::npos) ? raw.substr(0, eol) : raw; const auto s1 = line.find(' '); if (s1 == std::string::npos) return "/"; const auto s2 = line.find(' ', s1 + 1); const std::string path = (s2 != std::string::npos) ? line.substr(s1 + 1, s2 - s1 - 1) : line.substr(s1 + 1); if (!path.empty() && path.back() == '\r') return path.substr(0, path.size() - 1); return path.empty() ? "/" : path; } static inline std::shared_ptr make_stealth_upstream_ctx(const std::string& host) { auto ctx = std::make_shared(asio::ssl::context::tls_client); SSL_CTX* native = ctx->native_handle(); // Disabilita protocolli obsoleti e opzioni che non usa Chrome SSL_CTX_set_options(native, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_COMPRESSION); SSL_CTX_set_min_proto_version(native, TLS1_2_VERSION); SSL_CTX_set_cipher_list(native, "TLS_AES_128_GCM_SHA256:" "TLS_AES_256_GCM_SHA384:" "TLS_CHACHA20_POLY1305_SHA256:" "ECDHE-ECDSA-AES128-GCM-SHA256:" "ECDHE-RSA-AES128-GCM-SHA256:" "ECDHE-ECDSA-AES256-GCM-SHA384:" "ECDHE-RSA-AES256-GCM-SHA384:" "ECDHE-ECDSA-CHACHA20-POLY1305:" "ECDHE-RSA-CHACHA20-POLY1305:" "ECDHE-RSA-AES128-SHA:" "ECDHE-RSA-AES256-SHA:" "AES128-GCM-SHA256:" "AES256-GCM-SHA384:" "AES128-SHA:" "AES256-SHA"); #ifdef TLS1_3_VERSION SSL_CTX_set_ciphersuites(native, "TLS_AES_128_GCM_SHA256:" "TLS_AES_256_GCM_SHA384:" "TLS_CHACHA20_POLY1305_SHA256"); #endif // Il proxy trasparente sotto usa HTTP/1.1 byte-for-byte. Non negoziare // h2 upstream: inviare una request HTTP/1.1 su una connessione ALPN h2 // corromperebbe il flusso. static const unsigned char alpn[] = "\x08http/1.1"; SSL_CTX_set_alpn_protos(native, alpn, sizeof(alpn) - 1); SSL_CTX_set_default_verify_paths(native); SSL_CTX_set_verify(native, SSL_VERIFY_PEER, nullptr); (void)host; return ctx; } // ----------------------------------------------------------------------------- // Fetch Metadata / transaction passthrough // // Una navigazione "sicura" non e' sempre una pagina top-level. 3-D Secure, // SCA, OTP e diversi flow bancari usano iframe, fetch/XHR o form POST verso // ACS/issuer. Trasformare queste richieste in una nuova navigazione CEF perde // POST body, cookie e contesto della transazione. // // Regola generica: // * top-level GET/HEAD document -> comportamento BastionGuard tradizionale // (apri SecureBrowser e mostra block page) // * iframe/subresource -> inoltro HTTPS trasparente e verificato // * qualunque metodo stateful -> inoltro HTTPS trasparente e verificato // (POST/PUT/PATCH/DELETE, anche popup) // // In questo modo non servono eccezioni per "3ds4.*", "acs.*", ecc. // ----------------------------------------------------------------------------- static inline std::string lower_ascii(std::string v) { std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return v; } static inline std::string trim_ascii(std::string v) { while (!v.empty() && std::isspace(static_cast(v.front()))) v.erase(v.begin()); while (!v.empty() && std::isspace(static_cast(v.back()))) v.pop_back(); return v; } static inline std::string request_method_from_raw(const std::string& raw) { const auto eol = raw.find("\r\n"); const std::string line = raw.substr(0, eol); const auto sp = line.find(' '); if (sp == std::string::npos) return {}; return lower_ascii(line.substr(0, sp)); } static inline std::string header_value_ci(const std::string& raw, const std::string& wanted) { const std::string wanted_l = lower_ascii(wanted); const auto first_eol = raw.find("\r\n"); if (first_eol == std::string::npos) return {}; std::size_t pos = first_eol + 2; while (pos < raw.size()) { const auto eol = raw.find("\r\n", pos); if (eol == std::string::npos || eol == pos) break; const auto colon = raw.find(':', pos); if (colon != std::string::npos && colon < eol) { std::string name = lower_ascii(raw.substr(pos, colon - pos)); if (name == wanted_l) return trim_ascii(raw.substr(colon + 1, eol - colon - 1)); } pos = eol + 2; } return {}; } static inline bool is_stateful_method(const std::string& method_l) { return !(method_l == "get" || method_l == "head" || method_l == "options"); } static inline bool should_passthrough_transaction(const std::string& raw) { const std::string method = request_method_from_raw(raw); if (method.empty()) return false; // fail verso il comportamento storico // Mai trasformare una POST/PUT/PATCH/DELETE in una GET dentro un altro // browser: perderemmo i dati della transazione. if (is_stateful_method(method)) return true; const std::string dest = lower_ascii(header_value_ci(raw, "sec-fetch-dest")); if (dest == "iframe" || dest == "frame" || dest == "embed" || dest == "object" || dest == "empty" || dest == "script" || dest == "style" || dest == "image" || dest == "font" || dest == "audio" || dest == "video" || dest == "track" || dest == "worker" || dest == "sharedworker" || dest == "serviceworker" || dest == "manifest") { return true; } const std::string mode = lower_ascii(header_value_ci(raw, "sec-fetch-mode")); if (mode == "cors" || mode == "no-cors" || mode == "same-origin" || mode == "websocket") { return true; } const std::string purpose = lower_ascii(header_value_ci(raw, "purpose")); const std::string sec_purpose = lower_ascii(header_value_ci(raw, "sec-purpose")); if (purpose.find("prefetch") != std::string::npos || sec_purpose.find("prefetch") != std::string::npos) { return true; } // dest=document + mode=navigate, oppure header Fetch Metadata assenti: // conserva il comportamento top-level esistente. return false; } static inline bool parse_content_length(const std::string& raw, std::size_t& out_len) { const std::string v = trim_ascii(header_value_ci(raw, "content-length")); if (v.empty()) return false; errno = 0; char* end = nullptr; const unsigned long long n = std::strtoull(v.c_str(), &end, 10); if (errno != 0 || end == v.c_str() || (end && *end != '\0') || n > std::numeric_limits::max()) { return false; } out_len = static_cast(n); return true; } static inline bool is_chunked_request(const std::string& raw) { const std::string te = lower_ascii(header_value_ci(raw, "transfer-encoding")); return te.find("chunked") != std::string::npos; } static inline bool read_more(asio::ssl::stream& tls, std::string& raw, std::size_t min_total, std::size_t max_total) { std::array buf{}; while (raw.size() < min_total) { if (raw.size() >= max_total) return false; boost::system::error_code ec; const std::size_t room = std::min(buf.size(), max_total - raw.size()); const std::size_t n = tls.read_some(asio::buffer(buf.data(), room), ec); if (n) raw.append(buf.data(), n); if (ec) return false; } return true; } static inline bool read_complete_chunked(asio::ssl::stream& tls, std::string& raw, std::size_t body_start, std::size_t max_total) { std::size_t cur = body_start; std::array buf{}; auto ensure_line = [&](std::size_t from, std::size_t& eol) -> bool { for (;;) { eol = raw.find("\r\n", from); if (eol != std::string::npos) return true; if (raw.size() >= max_total) return false; boost::system::error_code ec; const std::size_t room = std::min(buf.size(), max_total - raw.size()); const std::size_t n = tls.read_some(asio::buffer(buf.data(), room), ec); if (n) raw.append(buf.data(), n); if (ec) return false; } }; for (;;) { std::size_t eol = 0; if (!ensure_line(cur, eol)) return false; std::string size_line = raw.substr(cur, eol - cur); const auto semi = size_line.find(';'); if (semi != std::string::npos) size_line.erase(semi); size_line = trim_ascii(size_line); if (size_line.empty()) return false; errno = 0; char* end = nullptr; const unsigned long long chunk = std::strtoull(size_line.c_str(), &end, 16); if (errno != 0 || end == size_line.c_str() || (end && *end != '\0') || chunk > max_total) { return false; } cur = eol + 2; if (chunk == 0) { // Trailer section: zero o piu' header, terminati da una riga vuota. for (;;) { if (!ensure_line(cur, eol)) return false; if (eol == cur) return true; cur = eol + 2; } } const std::size_t need = cur + static_cast(chunk) + 2; if (need < cur || need > max_total) return false; if (!read_more(tls, raw, need, max_total)) return false; if (raw.compare(cur + static_cast(chunk), 2, "\r\n") != 0) return false; cur = need; } } static inline bool read_complete_http1_request(asio::ssl::stream& tls, std::string& raw) { static constexpr std::size_t MAX_REQUEST = 32u * 1024u * 1024u; const auto hdr_end_pos = raw.find("\r\n\r\n"); if (hdr_end_pos == std::string::npos) return false; const std::size_t body_start = hdr_end_pos + 4; std::size_t content_len = 0; if (parse_content_length(raw, content_len)) { if (content_len > MAX_REQUEST || body_start > MAX_REQUEST - content_len) return false; return read_more(tls, raw, body_start + content_len, MAX_REQUEST); } if (is_chunked_request(raw)) return read_complete_chunked(tls, raw, body_start, MAX_REQUEST); return true; } static inline std::string force_connection_close(const std::string& raw) { const auto hdr_end = raw.find("\r\n\r\n"); if (hdr_end == std::string::npos) return raw; const std::string headers = raw.substr(0, hdr_end); const std::string body = raw.substr(hdr_end + 4); std::istringstream in(headers); std::ostringstream out; std::string line; bool first = true; while (std::getline(in, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); if (first) { out << line << "\r\n"; first = false; continue; } const auto colon = line.find(':'); const std::string name = colon == std::string::npos ? std::string{} : lower_ascii(trim_ascii(line.substr(0, colon))); if (name == "connection" || name == "proxy-connection" || name == "keep-alive") continue; out << line << "\r\n"; } out << "Connection: close\r\n\r\n"; out << body; return out.str(); } static inline bool forward_transaction_https(asio::ssl::stream& client_tls, const std::string& host, std::string raw_request) { if (!read_complete_http1_request(client_tls, raw_request)) { std::cerr << "[tls-intercept] transaction request incomplete/too large for " << host << "\n"; return false; } raw_request = force_connection_close(raw_request); try { asio::io_context io; tcp::resolver resolver(io); auto endpoints = resolver.resolve(host, "443"); tcp::socket upstream_socket(io); asio::connect(upstream_socket, endpoints); auto upstream_ctx = make_stealth_upstream_ctx(host); asio::ssl::stream upstream(std::move(upstream_socket), *upstream_ctx); SSL* ssl = upstream.native_handle(); if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) { std::cerr << "[tls-intercept] SNI setup failed for " << host << "\n"; return false; } X509_VERIFY_PARAM* param = SSL_get0_param(ssl); X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); if (X509_VERIFY_PARAM_set1_host(param, host.c_str(), 0) != 1) { std::cerr << "[tls-intercept] hostname verification setup failed for " << host << "\n"; return false; } upstream.set_verify_mode(asio::ssl::verify_peer); upstream.handshake(asio::ssl::stream_base::client); asio::write(upstream, asio::buffer(raw_request)); std::array buf{}; for (;;) { boost::system::error_code rec; const std::size_t n = upstream.read_some(asio::buffer(buf), rec); if (n) { boost::system::error_code wec; asio::write(client_tls, asio::buffer(buf.data(), n), wec); if (wec) return false; } if (rec) { if (rec != asio::error::eof && rec != asio::ssl::error::stream_truncated) { std::cerr << "[tls-intercept] upstream read failed for " << host << ": " << rec.message() << "\n"; } break; } } boost::system::error_code sd_ec; upstream.shutdown(sd_ec); return true; } catch (const std::exception& e) { std::cerr << "[tls-intercept] transaction passthrough failed for " << host << ": " << e.what() << "\n"; return false; } } static inline void send_passthrough_failure(asio::ssl::stream& tls) { static constexpr char body[] = "BastionGuard: upstream secure transaction failed\n"; std::ostringstream oss; oss << "HTTP/1.1 502 Bad Gateway\r\n" << "Content-Type: text/plain; charset=UTF-8\r\n" << "Content-Length: " << (sizeof(body) - 1) << "\r\n" << "Cache-Control: no-store\r\n" << "Connection: close\r\n\r\n" << body; const std::string resp = oss.str(); boost::system::error_code ec; asio::write(tls, asio::buffer(resp), ec); } static inline void do_intercept(tcp::socket sock, const std::string& host, const std::string& type, std::function on_url_ready = nullptr) { std::cerr << "[tls-intercept] intercepting " << host << "\n"; auto ssl_ctx = detail::get_or_create_ctx(host); if (!ssl_ctx) { std::cerr << "[tls-intercept] ctx setup failed for " << host << "\n"; boost::system::error_code ignore; sock.close(ignore); return; } try { asio::ssl::stream tls(std::move(sock), *ssl_ctx); boost::system::error_code hs_ec; tls.handshake(asio::ssl::stream_base::server, hs_ec); if (hs_ec) { std::cerr << "[tls-intercept] handshake failed for " << host << ": " << hs_ec.message() << "\n"; boost::system::error_code ignore; tls.lowest_layer().close(ignore); return; } std::cerr << "[tls-intercept] handshake ok for " << host << "\n"; asio::streambuf rbuf; boost::system::error_code read_ec; asio::read_until(tls, rbuf, "\r\n\r\n", read_ec); if (read_ec) { std::cerr << "[tls-intercept] request header read failed for " << host << ": " << read_ec.message() << "\n"; boost::system::error_code ignore; tls.lowest_layer().close(ignore); return; } const std::string raw{ asio::buffers_begin(rbuf.data()), asio::buffers_end(rbuf.data()) }; if (should_passthrough_transaction(raw)) { const std::string method = request_method_from_raw(raw); const std::string dest = lower_ascii(header_value_ci(raw, "sec-fetch-dest")); std::cerr << "[tls-intercept] iframe/stateful passthrough: host=" << host << " method=" << method << " dest=" << dest << "\n"; if (!forward_transaction_https(tls, host, raw)) send_passthrough_failure(tls); boost::system::error_code sd_ec; tls.shutdown(sd_ec); boost::system::error_code ignore; tls.lowest_layer().close(ignore); return; } // Solo una vera navigazione top-level GET/HEAD viene trasformata in // apertura SecureBrowser. Il callback NON viene chiamato per iframe, // fetch/XHR o POST, quindi nessuna sessione/gate fittizia viene creata. if (on_url_ready) { const std::string path = extract_path_from_request(raw); const std::string full_url = "https://" + host + path; std::cerr << "[tls-intercept] top-level secure URL: " << full_url << "\n"; on_url_ready(full_url); } const std::string resp = detail::make_tls_response(host, type); boost::system::error_code write_ec; asio::write(tls, asio::buffer(resp), write_ec); if (write_ec) std::cerr << "[tls-intercept] write error for " << host << ": " << write_ec.message() << "\n"; else std::cerr << "[tls-intercept] page served for " << host << "\n"; boost::system::error_code sd_ec; tls.shutdown(sd_ec); boost::system::error_code ignore; tls.lowest_layer().close(ignore); } catch (const std::exception& e) { std::cerr << "[tls-intercept] exception for " << host << ": " << e.what() << "\n"; boost::system::error_code ignore; sock.close(ignore); } } } // namespace TlsIntercept