Add fleasion support

Close #18
This commit is contained in:
komaruworld committed 2026-09-14 13:05:24 +03:00
1 parent ab4944a907
commit becaee71c0
20 files changed
+605 -19

No files matched your search

+1 -13
View File
@@ -57,19 +57,7 @@ Makefile.cmake
/cmake-build-release/
# Roblox payload
/rbx_bin/libroblox.so
/rbx_bin/libroblox.so.*
/rbx_bin/client_settings_android.json
/rbx_bin/assets/
/rbx_bin/android/
/rbx_bin/sober_apk/
/rbx_bin/versions/
/rbx_bin/.payload-update.*/
/rbx_bin/*.apk
/rbx_bin/*.so
/rbx_bin/*.so.*
!/rbx_bin/.gitkeep
/rbx_bin/
# Runtime state
/runtime/
+2
View File
@@ -268,6 +268,7 @@ add_library(mocktail_runtime STATIC
src/runtime/environment.cc
src/runtime/external_launch_broker.cc
src/runtime/frame_rate_policy.cc
src/runtime/fleasion.cc
src/runtime/game_mode.cc
src/runtime/game_session_coordinator.cc
src/runtime/graphics_launch_policy.cc
@@ -329,6 +330,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
endif()
target_link_libraries(mocktail_runtime PUBLIC Threads::Threads)
target_link_libraries(mocktail_runtime PRIVATE
OpenSSL::Crypto
CURL::libcurl
nlohmann_json::nlohmann_json
PkgConfig::GIO
+10
View File
@@ -75,6 +75,16 @@ audio:
input_device: default
integrations:
fleasion:
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
enabled: false
# String: match Fleasion's routing mode: env or hosts.
proxy_mode: env
# Integer: Fleasion's local HTTP proxy port in env mode.
proxy_port: 58443
# Optional absolute public CA path. Default: $XDG_CONFIG_HOME/Fleasion/proxy_ca/ca.crt
# or ~/.config/Fleasion/proxy_ca/ca.crt. Never select ca.key.
# ca_certificate: /home/user/.config/Fleasion/proxy_ca/ca.crt
discord_rpc:
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
# This never signs in to Discord and never reads an account token.
+28
View File
@@ -0,0 +1,28 @@
#ifndef MOCKTAIL_RUNTIME_FLEASION_H_
#define MOCKTAIL_RUNTIME_FLEASION_H_
#include <filesystem>
#include <string>
#include "runtime/runtime_config.h"
#include "runtime/runtime_paths.h"
namespace mocktail::runtime {
struct FleasionPreparation {
std::filesystem::path certificate;
std::filesystem::path base_bundle;
std::filesystem::path bundle;
std::string error;
explicit operator bool() const { return error.empty(); }
};
// Generates a private, replaceable trust bundle outside the managed payload.
// Reads certificates only; never reads Fleasion's private key or changes it.
FleasionPreparation PrepareFleasion(const RuntimeConfig& config,
const Environment& environment,
const RuntimePaths& paths);
} // namespace mocktail::runtime
#endif
+12
View File
@@ -119,6 +119,13 @@ class RuntimeConfig {
}
bool ca_bundle_valid() const { return ca_bundle_valid_; }
bool use_system_proxy() const { return use_system_proxy_; }
bool fleasion_enabled() const { return fleasion_enabled_; }
bool fleasion_valid() const { return fleasion_valid_; }
const std::string& fleasion_proxy_mode() const { return fleasion_proxy_mode_; }
int fleasion_proxy_port() const { return fleasion_proxy_port_; }
const std::optional<std::filesystem::path>& fleasion_ca_certificate() const {
return fleasion_ca_certificate_;
}
const DiscordRpcConfig& discord_rpc() const { return discord_rpc_; }
bool discord_rpc_valid() const { return discord_rpc_valid_; }
@@ -152,6 +159,11 @@ class RuntimeConfig {
std::string audio_input_device_ = "default";
bool audio_input_device_valid_ = true;
bool use_system_proxy_ = false;
bool fleasion_enabled_ = false;
bool fleasion_valid_ = true;
std::string fleasion_proxy_mode_ = "env";
int fleasion_proxy_port_ = 58443;
std::optional<std::filesystem::path> fleasion_ca_certificate_;
std::optional<NetworkProxyConfig> network_proxy_;
std::optional<std::filesystem::path> ca_bundle_;
bool ca_bundle_valid_ = true;
View File
Whitespace-only changes.
+21 -6
View File
@@ -8,16 +8,19 @@
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <iostream>
#include <limits.h>
#include <unordered_map>
#include <unistd.h>
#include <vector>
namespace libc_shim {
namespace {
std::unordered_map<std::string, std::string> g_path_mappings;
std::vector<std::string> g_ca_bundle_aliases;
bool g_installed = false;
std::atomic<GuestAllocator> g_guest_allocator{nullptr};
@@ -27,9 +30,15 @@ constexpr std::array<const char*, 2> kDefaultCaBundles = {
"/etc/ssl/cert.pem",
"/etc/ssl/certs/ca-certificates.crt",
};
constexpr std::array<const char*, 2> kAndroidCaBundlePaths = {
constexpr std::array<const char*, 8> kAndroidCaBundlePaths = {
"/data/user/0/com.roblox.client/files/exe/cacert.pem",
"/data/data/com.roblox.client/files/exe/cacert.pem",
"/data/user/0/com.roblox.client/files/exe/ssl/cacert.pem",
"/data/data/com.roblox.client/files/exe/ssl/cacert.pem",
"ssl/cacert.pem",
"content/ssl/cacert.pem",
"rbx_bin/assets/ssl/cacert.pem",
"rbx_bin/assets/content/ssl/cacert.pem",
};
bool IsEnabled(const char* name) {
@@ -192,17 +201,22 @@ HostCaBundleResolution ResolveHostCaBundle() {
}
HostCaBundleResolution ConfigureHostCaBundlePathMappings() {
for (const char* android_path : kAndroidCaBundlePaths) {
g_path_mappings.erase(android_path);
for (const auto& alias : g_ca_bundle_aliases) g_path_mappings.erase(alias);
g_ca_bundle_aliases.assign(kAndroidCaBundlePaths.begin(), kAndroidCaBundlePaths.end());
if (const char* content = GetEnvNonEmpty("MOCKTAIL_ASSET_PATH")) {
auto root = std::filesystem::path(content).lexically_normal();
if (root.filename().empty()) root = root.parent_path();
g_ca_bundle_aliases.push_back((root / "ssl/cacert.pem").string());
if (root.filename() == "content")
g_ca_bundle_aliases.push_back((root.parent_path() / "ssl/cacert.pem").string());
}
HostCaBundleResolution resolution = ResolveHostCaBundle();
if (!resolution.ok()) {
return resolution;
}
for (const char* android_path : kAndroidCaBundlePaths) {
RegisterPathMapping(android_path, resolution.host_path);
}
for (const auto& alias : g_ca_bundle_aliases)
RegisterPathMapping(alias, resolution.host_path);
return resolution;
}
@@ -308,6 +322,7 @@ void RegisterPathMapping(const std::string& android_prefix,
void ClearPathMappings() {
g_path_mappings.clear();
g_ca_bundle_aliases.clear();
}
} // namespace libc_shim
+33
View File
@@ -29,6 +29,7 @@
#include "runtime/environment.h"
#include "runtime/external_launch_broker.h"
#include "runtime/failure_dialog.h"
#include "runtime/fleasion.h"
#include "runtime/game_mode.h"
#include "runtime/graphics_launch_policy.h"
#include "runtime/memory_limit.h"
@@ -469,6 +470,38 @@ int main(int argc, char* argv[]) {
mocktail::runtime::LogProcessDiagnostics(
mocktail::runtime::ProcessDiagnosticStage::kStartup);
}
if (runtime_config.config.fleasion_enabled()) {
const auto fleasion = mocktail::runtime::PrepareFleasion(
runtime_config.config, environment, paths);
if (!fleasion ||
setenv("MOCKTAIL_CA_BUNDLE", fleasion.bundle.c_str(), 1) != 0 ||
setenv("MOCKTAIL_FLEASION_BASE_CA_BUNDLE", fleasion.base_bundle.c_str(), 1) != 0 ||
setenv("MOCKTAIL_FLEASION_GENERATED_BUNDLE", fleasion.bundle.c_str(), 1) != 0 ||
setenv("MOCKTAIL_FLEASION_CA_CERTIFICATE", fleasion.certificate.c_str(), 1) != 0) {
std::cerr << "[FATAL] Cannot prepare Fleasion: "
<< (!fleasion ? fleasion.error : "cannot export certificate paths") << '\n';
return EXIT_FAILURE;
}
if (const auto& proxy = runtime_config.config.network_proxy(); proxy &&
(setenv("MOCKTAIL_HTTP_PROXY_HOST", proxy->host.c_str(), 1) != 0 ||
setenv("MOCKTAIL_HTTP_PROXY_PORT", std::to_string(proxy->port).c_str(), 1) != 0 ||
setenv("MOCKTAIL_HTTP_PROXY_SCHEME", proxy->scheme.c_str(), 1) != 0)) {
std::cerr << "[FATAL] Cannot export Fleasion proxy\n";
return EXIT_FAILURE;
}
runtime_config = mocktail::runtime::LoadRuntimeConfig(environment, paths.config_file());
if (!runtime_config) {
std::cerr << "[FATAL] Cannot apply Fleasion: " << runtime_config.error << '\n';
return EXIT_FAILURE;
}
std::cout << " [fleasion] mode=" << runtime_config.config.fleasion_proxy_mode()
<< " certificate=" << fleasion.certificate
<< " trust_bundle=" << fleasion.bundle << '\n';
if (runtime_config.config.network_proxy()) {
std::cout << " [fleasion] proxy=" << mocktail::runtime::BuildNetworkProxyUrl(
*runtime_config.config.network_proxy()) << "; start Fleasion before Roblox\n";
}
}
if (config_bootstrap.created()) {
std::cout << " [runtime] created first-run configuration: "
<< paths.config_file() << '\n';
+4
View File
@@ -422,8 +422,12 @@ std::optional<Json> GetRobloxJson(std::string url) {
if (proxy.has_value()) {
proxy_url = BuildNetworkProxyUrl(*proxy);
curl_easy_setopt(curl, CURLOPT_PROXY, proxy_url.c_str());
curl_easy_setopt(curl, CURLOPT_NOPROXY, "");
}
}
if (const char* ca_bundle = std::getenv("MOCKTAIL_CA_BUNDLE")) {
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle);
}
const CURLcode status = curl_easy_perform(curl);
long http_status = 0;
if (status == CURLE_OK) {
+151
View File
@@ -0,0 +1,151 @@
#include "runtime/fleasion.h"
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509v3.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cerrno>
#include <fstream>
#include <memory>
namespace mocktail::runtime {
namespace {
using Bio = std::unique_ptr<BIO, decltype(&BIO_free)>;
using Certificate = std::unique_ptr<X509, decltype(&X509_free)>;
bool ReadFile(const std::filesystem::path& path, std::string* contents) {
std::error_code error;
if (!path.is_absolute() || !std::filesystem::is_regular_file(path, error))
return false;
const auto size = std::filesystem::file_size(path, error);
if (error || size == 0 || size > 4 * 1024 * 1024) return false;
std::ifstream input(path, std::ios::binary);
contents->resize(static_cast<std::size_t>(size));
return static_cast<bool>(input.read(contents->data(), contents->size()));
}
std::filesystem::path DefaultCertificate(const Environment& environment,
const RuntimePaths& paths) {
std::filesystem::path root = environment.GetOr("XDG_CONFIG_HOME", "");
if (!root.is_absolute()) root = paths.home() / ".config";
return root / "Fleasion/proxy_ca/ca.crt";
}
bool WriteBundle(const std::filesystem::path& destination,
const char* contents, std::size_t size) {
std::error_code error;
std::filesystem::create_directories(destination.parent_path(), error);
if (error || std::filesystem::is_symlink(destination.parent_path(), error))
return false;
std::string temporary = destination.string() + ".XXXXXX";
const int descriptor = mkstemp(temporary.data());
if (descriptor < 0) return false;
std::size_t offset = 0;
while (offset < size) {
const ssize_t count = write(descriptor, contents + offset, size - offset);
if (count < 0 && errno == EINTR) continue;
if (count <= 0) break;
offset += static_cast<std::size_t>(count);
}
bool ok = offset == size && fsync(descriptor) == 0;
if (close(descriptor) != 0) ok = false;
if (ok) ok = rename(temporary.c_str(), destination.c_str()) == 0;
if (!ok) unlink(temporary.c_str());
return ok;
}
} // namespace
FleasionPreparation PrepareFleasion(const RuntimeConfig& config,
const Environment& environment,
const RuntimePaths& paths) {
FleasionPreparation result;
if (!config.fleasion_enabled()) return result;
if (!config.fleasion_valid()) {
result.error = "invalid Fleasion configuration";
return result;
}
result.certificate = config.fleasion_ca_certificate().value_or(
DefaultCertificate(environment, paths));
result.bundle = paths.cache_root() / "fleasion/cacert.pem";
result.base_bundle = config.ca_bundle().value_or(std::filesystem::path{});
// Re-exec and updater canaries inherit the generated bundle. Rebuild from
// the original roots so a rotated Fleasion CA does not accumulate forever.
if (result.base_bundle == result.bundle ||
result.base_bundle == environment.GetOr("MOCKTAIL_FLEASION_GENERATED_BUNDLE", "")) {
result.base_bundle = environment.GetOr("MOCKTAIL_FLEASION_BASE_CA_BUNDLE", "");
}
if (result.base_bundle.empty()) {
for (const char* candidate : {"/etc/ssl/cert.pem", "/etc/ssl/certs/ca-certificates.crt"}) {
std::error_code error;
if (std::filesystem::is_regular_file(candidate, error)) {
result.base_bundle = candidate;
break;
}
}
}
std::error_code equivalent_error;
const bool certificate_is_output = std::filesystem::equivalent(
result.certificate, result.bundle, equivalent_error);
equivalent_error.clear();
const bool base_is_output = std::filesystem::equivalent(
result.base_bundle, result.bundle, equivalent_error);
if (result.certificate == result.bundle || result.base_bundle == result.bundle ||
certificate_is_output || base_is_output) {
result.error = "Fleasion source certificates must be outside its generated bundle";
return result;
}
std::string ca_pem;
if (!ReadFile(result.certificate, &ca_pem)) {
result.error = "cannot read Fleasion CA certificate at " + result.certificate.string() +
"; start Fleasion first or set integrations.fleasion.ca_certificate";
return result;
}
Bio ca_input(BIO_new_mem_buf(ca_pem.data(), static_cast<int>(ca_pem.size())), BIO_free);
Certificate ca(ca_input ? PEM_read_bio_X509(ca_input.get(), nullptr, nullptr, nullptr)
: nullptr, X509_free);
if (!ca || X509_check_ca(ca.get()) <= 0 ||
X509_cmp_current_time(X509_get0_notBefore(ca.get())) >= 0 ||
X509_cmp_current_time(X509_get0_notAfter(ca.get())) <= 0) {
ERR_clear_error();
result.error = "Fleasion CA must be a currently valid PEM CA certificate";
return result;
}
std::string roots;
if (!ReadFile(result.base_bundle, &roots)) {
result.error = "cannot read base CA bundle for Fleasion: " + result.base_bundle.string();
return result;
}
Bio input(BIO_new_mem_buf(roots.data(), static_cast<int>(roots.size())), BIO_free);
Bio output(BIO_new(BIO_s_mem()), BIO_free);
if (!input || !output) {
result.error = "cannot allocate Fleasion certificate bundle";
return result;
}
int count = 0;
for (;;) {
Certificate root(PEM_read_bio_X509(input.get(), nullptr, nullptr, nullptr), X509_free);
if (!root) break;
++count;
if (X509_cmp(root.get(), ca.get()) != 0 &&
PEM_write_bio_X509(output.get(), root.get()) != 1) {
result.error = "cannot encode base CA certificate";
return result;
}
}
ERR_clear_error();
if (count == 0 || PEM_write_bio_X509(output.get(), ca.get()) != 1) {
result.error = "base bundle must contain PEM certificates";
return result;
}
char* bytes = nullptr;
const long size = BIO_get_mem_data(output.get(), &bytes);
if (size <= 0 || !WriteBundle(result.bundle, bytes, static_cast<std::size_t>(size)))
result.error = "cannot write generated Fleasion CA bundle: " + result.bundle.string();
return result;
}
} // namespace mocktail::runtime
+29
View File
@@ -287,6 +287,35 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
config.use_system_proxy_ =
LegacyEnabled(environment, "MOCKTAIL_USE_SYSTEM_PROXY");
config.network_proxy_ = ReadNetworkProxy(environment);
config.fleasion_enabled_ = ReadBoolean(
environment, "MOCKTAIL_FLEASION_ENABLED", false, &config.fleasion_valid_);
config.fleasion_proxy_mode_ = environment.GetOr("MOCKTAIL_FLEASION_PROXY_MODE", "env");
const auto fleasion_proxy = ParseNetworkProxyConfig(
"127.0.0.1", environment.GetOr("MOCKTAIL_FLEASION_PROXY_PORT", "58443"));
if (!fleasion_proxy || (config.fleasion_proxy_mode_ != "env" &&
config.fleasion_proxy_mode_ != "hosts")) {
config.fleasion_valid_ = false;
} else {
config.fleasion_proxy_port_ = fleasion_proxy->port;
}
if (const auto certificate = environment.Get("MOCKTAIL_FLEASION_CA_CERTIFICATE")) {
config.fleasion_ca_certificate_ = *certificate;
if (certificate->empty() || !config.fleasion_ca_certificate_->is_absolute())
config.fleasion_valid_ = false;
}
if (config.fleasion_enabled_) {
if (config.use_system_proxy_) config.fleasion_valid_ = false;
if (config.fleasion_proxy_mode_ == "env" && fleasion_proxy) {
if (config.network_proxy_ &&
(config.network_proxy_->host != fleasion_proxy->host ||
config.network_proxy_->port != fleasion_proxy->port ||
config.network_proxy_->scheme != "http"))
config.fleasion_valid_ = false;
config.network_proxy_ = fleasion_proxy;
} else if (config.network_proxy_) {
config.fleasion_valid_ = false;
}
}
if (const std::optional<std::string> ca_bundle =
environment.Get("MOCKTAIL_CA_BUNDLE");
ca_bundle.has_value()) {
+10
View File
@@ -93,6 +93,16 @@ audio:
input_device: default
integrations:
fleasion:
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
enabled: false
# String: match Fleasion's routing mode: env or hosts.
proxy_mode: env
# Integer: Fleasion's local HTTP proxy port in env mode.
proxy_port: 58443
# Optional absolute public CA path. Default: $XDG_CONFIG_HOME/Fleasion/proxy_ca/ca.crt
# or ~/.config/Fleasion/proxy_ca/ca.crt. Never select ca.key.
# ca_certificate: /home/user/.config/Fleasion/proxy_ca/ca.crt
discord_rpc:
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
# This never signs in to Discord and never reads an account token.
+36
View File
@@ -188,6 +188,10 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
"network.proxy_host",
"network.proxy_port",
"network.ca_bundle",
"integrations.fleasion.enabled",
"integrations.fleasion.proxy_mode",
"integrations.fleasion.proxy_port",
"integrations.fleasion.ca_certificate",
"integrations.discord_rpc.enabled",
"integrations.discord_rpc.show_place_name",
"integrations.discord_rpc.show_elapsed_time",
@@ -483,6 +487,7 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
std::pair<std::string_view, std::string_view>(
"integrations.discord_rpc.enabled",
"MOCKTAIL_DISCORD_RPC_ENABLED"),
{"integrations.fleasion.enabled", "MOCKTAIL_FLEASION_ENABLED"},
{"integrations.discord_rpc.show_place_name",
"MOCKTAIL_DISCORD_RPC_SHOW_PLACE_NAME"},
{"integrations.discord_rpc.show_elapsed_time",
@@ -503,6 +508,15 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
}
(*environment)[std::string(variable)] = parsed ? "1" : "0";
}
for (const auto& [key, variable] : {
std::pair<std::string_view, std::string_view>(
"integrations.fleasion.proxy_mode", "MOCKTAIL_FLEASION_PROXY_MODE"),
{"integrations.fleasion.proxy_port", "MOCKTAIL_FLEASION_PROXY_PORT"},
{"integrations.fleasion.ca_certificate", "MOCKTAIL_FLEASION_CA_CERTIFICATE"},
}) {
if (const auto configured = value(key))
(*environment)[std::string(variable)] = *configured;
}
if (const auto application_id =
value("integrations.discord_rpc.application_id");
application_id.has_value()) {
@@ -751,6 +765,10 @@ RuntimeConfigLoadResult LoadRuntimeConfig(
result.error = "GameMode policy is invalid";
} else if (!result.config.performance().physics_worker_mode_valid) {
result.error = "physics worker policy is invalid";
} else if (!result.config.fleasion_valid()) {
result.error = "Fleasion configuration is invalid: use proxy_mode env or hosts, "
"a port from 1 to 65535, an absolute CA certificate path, and "
"no conflicting system/fixed proxy";
} else if (!result.config.ca_bundle_valid()) {
result.error = "CA bundle path is invalid";
} else if (!result.config.discord_rpc_valid()) {
@@ -761,6 +779,10 @@ RuntimeConfigLoadResult LoadRuntimeConfig(
bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
std::string* error) {
if (!config.fleasion_valid()) {
if (error != nullptr) *error = "cannot export invalid Fleasion configuration";
return false;
}
if (!config.device_profile_valid()) {
if (error != nullptr) {
*error = "cannot export an invalid device profile";
@@ -910,6 +932,20 @@ bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
if (!base_exported) {
return false;
}
if (!SetEnvironmentValue("MOCKTAIL_FLEASION_ENABLED",
config.fleasion_enabled() ? "1" : "0", error) ||
!SetEnvironmentValue("MOCKTAIL_FLEASION_PROXY_MODE",
config.fleasion_proxy_mode(), error) ||
!SetEnvironmentValue("MOCKTAIL_FLEASION_PROXY_PORT",
std::to_string(config.fleasion_proxy_port()), error))
return false;
if (config.fleasion_ca_certificate()) {
if (!SetEnvironmentValue("MOCKTAIL_FLEASION_CA_CERTIFICATE",
config.fleasion_ca_certificate()->string(), error))
return false;
} else if (!UnsetEnvironmentValue("MOCKTAIL_FLEASION_CA_CERTIFICATE", error)) {
return false;
}
if (config.ca_bundle().has_value()) {
if (!SetEnvironmentValue("MOCKTAIL_CA_BUNDLE",
config.ca_bundle()->string(), error)) {
+6
View File
@@ -118,6 +118,9 @@ HttpResponse PerformRequest(const HttpRequest& request,
curl_easy_setopt(handle, CURLOPT_WRITEDATA, &writer);
curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, WriteHeader);
curl_easy_setopt(handle, CURLOPT_HEADERDATA, &response.headers);
if (const char* ca_bundle = std::getenv("MOCKTAIL_CA_BUNDLE")) {
curl_easy_setopt(handle, CURLOPT_CAINFO, ca_bundle);
}
std::string proxy_url;
const char* proxy_host = std::getenv("MOCKTAIL_HTTP_PROXY_HOST");
const char* proxy_port = std::getenv("MOCKTAIL_HTTP_PROXY_PORT");
@@ -130,6 +133,9 @@ HttpResponse PerformRequest(const HttpRequest& request,
if (proxy.has_value()) {
proxy_url = runtime::BuildNetworkProxyUrl(*proxy);
curl_easy_setopt(handle, CURLOPT_PROXY, proxy_url.c_str());
// A configured application proxy must not be bypassed by an inherited
// NO_PROXY entry, including '*' from a desktop launcher.
curl_easy_setopt(handle, CURLOPT_NOPROXY, "");
}
}
if (headers != nullptr) {
+12
View File
@@ -16,6 +16,18 @@ target_link_libraries(runtime_config_test PRIVATE
GTest::gtest_main
)
add_executable(fleasion_probe fleasion_probe.cc)
target_link_libraries(fleasion_probe PRIVATE Mocktail::Services libc_shim)
mocktail_apply_compile_options(fleasion_probe)
find_program(MOCKTAIL_TEST_PYTHON python3)
find_program(MOCKTAIL_TEST_OPENSSL openssl)
if(MOCKTAIL_TEST_PYTHON AND MOCKTAIL_TEST_OPENSSL)
add_test(NAME FleasionIntegrationTest.ProxyAndTrust
COMMAND ${MOCKTAIL_TEST_PYTHON} ${CMAKE_CURRENT_SOURCE_DIR}/fleasion_integration_test.py
$<TARGET_FILE:fleasion_probe> ${MOCKTAIL_TEST_OPENSSL})
set_tests_properties(FleasionIntegrationTest.ProxyAndTrust PROPERTIES TIMEOUT 45)
endif()
add_executable(runtime_config_file_test runtime_config_file_test.cc)
target_link_libraries(runtime_config_file_test PRIVATE
Mocktail::Runtime
+141
View File
@@ -0,0 +1,141 @@
"""Exercise real CA composition, native path mapping, and HTTPS CONNECT routing."""
import os
from pathlib import Path
import socket
import ssl
import subprocess
import sys
import tempfile
import threading
probe, openssl = sys.argv[1:]
def command(*args):
subprocess.run(args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
with tempfile.TemporaryDirectory(prefix="mocktail-fleasion-test-") as directory:
root = Path(directory)
for name in ("public", "fleasion", "rotated"):
command(openssl, "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "2",
"-subj", f"/CN={name}", "-addext", "basicConstraints=critical,CA:TRUE",
"-keyout", str(root / f"{name}.key"), "-out", str(root / f"{name}.crt"))
command(openssl, "req", "-newkey", "rsa:2048", "-nodes", "-subj",
"/CN=assetdelivery.roblox.com", "-keyout", str(root / "leaf.key"),
"-out", str(root / "leaf.csr"))
(root / "leaf.ext").write_text("subjectAltName=DNS:assetdelivery.roblox.com\nbasicConstraints=CA:FALSE\n")
command(openssl, "x509", "-req", "-in", str(root / "leaf.csr"), "-CA",
str(root / "fleasion.crt"), "-CAkey", str(root / "fleasion.key"),
"-CAcreateserial", "-days", "1", "-extfile", str(root / "leaf.ext"),
"-out", str(root / "leaf.crt"))
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(root / "leaf.crt", root / "leaf.key")
intercepted = []
failures = []
listener = socket.socket()
listener.bind(("127.0.0.1", 0))
listener.listen()
listener.settimeout(5)
port = listener.getsockname()[1]
def serve():
try:
# Correct CA succeeds; the unrelated CA must fail TLS verification.
for attempt in range(2):
connection, _ = listener.accept()
with connection:
connection.settimeout(5)
connect = b""
while not connect.endswith(b"\r\n\r\n"):
data = connection.recv(4096)
if not data:
raise AssertionError("proxy connection closed before CONNECT")
connect += data
assert connect.startswith(b"CONNECT assetdelivery.roblox.com:443 "), connect
connection.sendall(b"HTTP/1.1 200 Connection established\r\n\r\n")
try:
with context.wrap_socket(connection, server_side=True) as tls:
request = tls.recv(8192)
assert request.startswith(b"GET /replacement "), request
intercepted.append(request)
body = b"fleasion-replaced-asset"
tls.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: " +
str(len(body)).encode() + b"\r\nConnection: close\r\n\r\n" + body)
except (ssl.SSLError, ConnectionError):
if attempt == 0:
raise
# The wrong CA may close with either a TLS alert or
# a TCP reset before the server finishes its handshake.
except BaseException as error:
failures.append(error)
thread = threading.Thread(target=serve, daemon=True)
thread.start()
env = {key: value for key, value in os.environ.items()
if not key.startswith(("MOCKTAIL_", "ROBLOX_"))}
for key in ("http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
env.pop(key, None)
env.update(HOME=str(root), XDG_CONFIG_HOME=str(root / "config"),
MOCKTAIL_CACHE_ROOT=str(root / "cache"), NO_PROXY="*", no_proxy="*")
certificate = root / "config/Fleasion/proxy_ca/ca.crt"
certificate.parent.mkdir(parents=True)
original = (root / "fleasion.crt").read_bytes()
certificate.write_bytes(original)
config = root / "config.yaml"
def configure(extra="", enabled="true", mode="env"):
config.write_text(f"version: 1\nnetwork:\n ca_bundle: {root / 'public.crt'}\n"
f"integrations:\n fleasion:\n enabled: {enabled}\n"
f" proxy_mode: {mode}\n proxy_port: {port}\n{extra}")
def run(url="prepare", success=True, environment=None):
result = subprocess.run([probe, str(config), url], env=environment or env,
capture_output=True, text=True, timeout=10)
assert (result.returncode == 0) == success, result.stdout + result.stderr
return result
configure()
response = run("https://assetdelivery.roblox.com/replacement")
assert "fleasion-replaced-asset" in response.stdout
assert certificate.read_bytes() == original
generated = root / "cache/fleasion/cacert.pem"
assert generated.stat().st_mode & 0o777 == 0o600
command(openssl, "verify", "-CAfile", str(generated), str(root / "public.crt"))
first_bundle = generated.read_bytes()
run() # Repeated startup must not duplicate trust roots.
assert generated.read_bytes() == first_bundle
certificate.write_bytes((root / "rotated.crt").read_bytes())
response = run("https://assetdelivery.roblox.com/replacement", success=False)
assert response.returncode == 10, response.stderr
thread.join(timeout=7)
listener.close()
assert not thread.is_alive() and not failures, failures
assert len(intercepted) == 1
assert original not in generated.read_bytes()
command(openssl, "verify", "-CAfile", str(generated), str(root / "rotated.crt"))
# Re-exec must compose from the original public roots, not old proxy CAs.
inherited = dict(env, MOCKTAIL_CA_BUNDLE=str(generated),
MOCKTAIL_FLEASION_GENERATED_BUNDLE=str(generated),
MOCKTAIL_FLEASION_BASE_CA_BUNDLE=str(root / "public.crt"))
certificate.write_bytes(original)
run(environment=inherited)
assert generated.read_bytes() == first_bundle
for bad in (b"not a certificate", (root / "leaf.crt").read_bytes()):
certificate.write_bytes(bad)
run(success=False)
assert generated.read_bytes() == first_bundle
certificate.unlink()
run(success=False)
configure(enabled="false")
run() # Disabled integration does not require Fleasion.
certificate.write_bytes(original)
configure(mode="hosts")
run()
configure(mode="invalid")
run(success=False)
configure(extra=" ca_certificate: relative.crt\n")
run(success=False)
print("Fleasion integration: proxy replacement, TLS rejection, CA rotation, "
"native aliases, and configuration checks passed")
+50
View File
@@ -0,0 +1,50 @@
#include "runtime/fleasion.h"
#include "runtime/runtime_config_file.h"
#include "services/http_client.h"
#include "libc_shim/libc_shim.h"
#include <cstdlib>
#include <iostream>
int main(int argc, char** argv) {
if (argc != 3) return 2;
const mocktail::runtime::ProcessEnvironment environment;
const auto paths = mocktail::runtime::RuntimePaths::FromEnvironment(environment);
const auto loaded = mocktail::runtime::LoadRuntimeConfig(environment, argv[1]);
if (!loaded) {
std::cerr << loaded.error << '\n';
return 3;
}
const auto prepared = mocktail::runtime::PrepareFleasion(loaded.config, environment, paths);
if (!prepared) {
std::cerr << prepared.error << '\n';
return 4;
}
std::string error;
if (!mocktail::runtime::ExportRuntimeConfigEnvironment(loaded.config, &error)) return 5;
if (!prepared.bundle.empty()) {
if (setenv("MOCKTAIL_CA_BUNDLE", prepared.bundle.c_str(), 1) != 0) return 6;
const auto mapped = libc_shim::ConfigureHostCaBundlePathMappings();
if (!mapped.ok()) return 7;
FILE* certificate = mocktail_fopen("ssl/cacert.pem", "r");
if (certificate == nullptr) return 8;
const int first = fgetc(certificate);
fclose(certificate);
if (first != '-') return 9;
}
if (std::string(argv[2]) == "prepare") {
std::cout << prepared.bundle.string() << '\n';
return 0;
}
mocktail::services::CurlHttpClient client;
mocktail::services::HttpRequest request;
request.url = argv[2];
request.timeout_ms = 3000;
const auto response = client.Get(request);
if (!response.transport_ok) {
std::cerr << response.error << '\n';
return 10;
}
std::cout << response.status_code << '\n' << response.body;
return response.status_code == 200 ? 0 : 11;
}
+9
View File
@@ -141,6 +141,7 @@ TEST_F(LibcShimTest, ValidCaBundleOverrideMapsAndroidAliases) {
static_cast<ssize_t>(sizeof(kTestCertificate) - 1));
ASSERT_EQ(close(fd), 0);
ASSERT_EQ(setenv("MOCKTAIL_CA_BUNDLE", temp_template, 1), 0);
ASSERT_EQ(setenv("MOCKTAIL_ASSET_PATH", "/payload/assets/content/", 1), 0);
RegisterPathMapping("/data/user/0/com.roblox.client", "/host/sober");
const HostCaBundleResolution resolution =
@@ -157,6 +158,14 @@ TEST_F(LibcShimTest, ValidCaBundleOverrideMapsAndroidAliases) {
temp_template);
EXPECT_EQ(TranslatePath("/data/user/0/com.roblox.client/files/other"),
"/host/sober/files/other");
for (const char* alias : {
"ssl/cacert.pem", "content/ssl/cacert.pem",
"rbx_bin/assets/ssl/cacert.pem", "rbx_bin/assets/content/ssl/cacert.pem",
"/payload/assets/ssl/cacert.pem", "/payload/assets/content/ssl/cacert.pem",
"/data/user/0/com.roblox.client/files/exe/ssl/cacert.pem",
"/data/data/com.roblox.client/files/exe/ssl/cacert.pem"}) {
EXPECT_EQ(TranslatePath(alias), temp_template);
}
FILE* mapped_file = mocktail_fopen(
"/data/user/0/com.roblox.client/files/exe/cacert.pem", "r");
ASSERT_NE(mapped_file, nullptr);
+48
View File
@@ -1040,6 +1040,54 @@ TEST(RuntimeConfigFileTest, RejectsMalformedYaml) {
EXPECT_NE(loaded.error.find("invalid YAML"), std::string::npos);
}
TEST(RuntimeConfigFileTest, FleasionRoutesThroughConfiguredLoopbackPort) {
TemporaryDirectory temporary;
const auto file = temporary.Write(
"integrations:\n fleasion:\n enabled: true\n proxy_mode: env\n"
" proxy_port: 59443\n ca_certificate: /tmp/fleasion-ca.crt\n");
auto loaded = LoadRuntimeConfig(MapEnvironment(), file);
ASSERT_TRUE(loaded) << loaded.error;
EXPECT_TRUE(loaded.config.fleasion_enabled());
ASSERT_TRUE(loaded.config.network_proxy());
EXPECT_EQ(loaded.config.network_proxy()->host, "127.0.0.1");
EXPECT_EQ(loaded.config.network_proxy()->port, 59443);
EXPECT_EQ(*loaded.config.fleasion_ca_certificate(), "/tmp/fleasion-ca.crt");
loaded = LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_FLEASION_ENABLED", "0"}}), file);
ASSERT_TRUE(loaded) << loaded.error;
EXPECT_FALSE(loaded.config.fleasion_enabled());
EXPECT_FALSE(loaded.config.network_proxy());
}
TEST(RuntimeConfigFileTest, FleasionRejectsInvalidConfigurationAndProxyConflicts) {
TemporaryDirectory temporary;
for (const char* setting : {"proxy_port: 0", "proxy_port: 65536", "proxy_port: 123x",
"proxy_mode: invalid", "ca_certificate: relative.pem"}) {
const auto file = temporary.Write(std::string(
"integrations:\n fleasion:\n enabled: true\n ") + setting + "\n");
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment(), file)) << setting;
}
const auto file = temporary.Write("integrations:\n fleasion:\n enabled: true\n");
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_USE_SYSTEM_PROXY", "1"}}), file));
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_HTTP_PROXY_HOST", "other"},
{"MOCKTAIL_HTTP_PROXY_PORT", "3128"}}), file));
}
TEST(RuntimeConfigFileTest, FleasionEnvironmentRoundTripsAndHostsModeDoesNotSetProxy) {
TemporaryDirectory temporary;
const auto file = temporary.Write("integrations:\n fleasion:\n enabled: true\n"
" proxy_mode: hosts\n");
const auto loaded = LoadRuntimeConfig(MapEnvironment(), file);
ASSERT_TRUE(loaded) << loaded.error;
EXPECT_FALSE(loaded.config.network_proxy());
std::string error;
ASSERT_TRUE(ExportRuntimeConfigEnvironment(loaded.config, &error)) << error;
const auto resolved = RuntimeConfig::FromEnvironment(ProcessEnvironment());
EXPECT_TRUE(resolved.fleasion_enabled());
EXPECT_TRUE(resolved.fleasion_valid());
EXPECT_EQ(resolved.fleasion_proxy_mode(), "hosts");
ASSERT_TRUE(ExportRuntimeConfigEnvironment(RuntimeConfig::FromEnvironment(MapEnvironment()), &error));
}
} // namespace
} // namespace runtime
} // namespace mocktail
+2
View File
@@ -71,6 +71,8 @@ TEST(RuntimeConfigTest, UsesSupportedDefaults) {
EXPECT_TRUE(config.audio_input_device_valid());
EXPECT_TRUE(config.microphone_enabled());
EXPECT_FALSE(config.use_system_proxy());
EXPECT_FALSE(config.fleasion_enabled());
EXPECT_TRUE(config.fleasion_valid());
EXPECT_FALSE(config.network_proxy().has_value());
EXPECT_FALSE(config.discord_rpc().enabled);
EXPECT_TRUE(config.discord_rpc().show_place_name);