Merge commit '9b36dd9d9952851d842c2f3bc6fadb0f9e4d8fa7'

This commit is contained in:
Green Sky
2026-02-01 14:26:52 +01:00
274 changed files with 11891 additions and 4292 deletions

View File

@@ -1,4 +1,5 @@
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
load("//hs-tokstyle/tools:tokstyle.bzl", "tokstyle_c_test")
CIMPLE_FILES = [
"//c-toxcore/toxav:cimple_files",
@@ -9,13 +10,16 @@ CIMPLE_FILES = [
sh_test(
name = "cimple_test",
size = "small",
size = "medium",
srcs = ["//hs-tokstyle/tools:check-cimple"],
args = ["$(locations %s)" % f for f in CIMPLE_FILES] + [
"-Wno-boolean-return",
"-Wno-callback-names",
"-Wno-enum-from-int",
"-Wno-nullability",
"-Wno-ownership-decls",
"-Wno-tagged-union",
"-Wno-type-check",
"+RTS",
"-N4",
"-RTS",
@@ -27,37 +31,30 @@ sh_test(
],
)
sh_test(
tokstyle_c_test(
name = "c_test",
size = "small",
srcs = ["//hs-tokstyle/tools:check-c"],
size = "medium",
srcs = CIMPLE_FILES,
args = [
"--cc=$(CC)",
"-Iexternal/libsodium/include",
"-Iexternal/libvpx",
"-Iexternal/opus/include",
"-Ihs-tokstyle/include",
] + ["$(locations %s)" % f for f in CIMPLE_FILES] + [
"+RTS",
"-N4",
"-RTS",
],
data = CIMPLE_FILES + [
"//hs-tokstyle:headers",
"@libsodium//:headers",
"@libvpx//:headers",
"@opus//:headers",
"-Wno-borrow-check",
"-Wno-callback-discipline",
"-Wno-strict-typedef",
],
tags = [
"haskell",
"no-cross",
],
toolchains = ["@rules_cc//cc:current_cc_toolchain"],
deps = [
"//hs-tokstyle:headers",
"@libsodium",
"@libvpx",
"@opus",
],
)
sh_test(
name = "cimplefmt_test",
size = "small",
size = "medium",
srcs = ["//hs-cimple/tools:cimplefmt"],
args = ["--reparse"] + ["$(locations %s)" % f for f in CIMPLE_FILES],
data = CIMPLE_FILES,

View File

@@ -9,11 +9,11 @@ else()
target_link_libraries(misc_tools PRIVATE toxcore_shared)
endif()
if(TARGET pthreads4w::pthreads4w)
target_link_libraries(misc_tools PRIVATE pthreads4w::pthreads4w)
target_link_libraries(misc_tools PUBLIC pthreads4w::pthreads4w)
elseif(TARGET PThreads4W::PThreads4W)
target_link_libraries(misc_tools PRIVATE PThreads4W::PThreads4W)
target_link_libraries(misc_tools PUBLIC PThreads4W::PThreads4W)
elseif(TARGET Threads::Threads)
target_link_libraries(misc_tools PRIVATE Threads::Threads)
target_link_libraries(misc_tools PUBLIC Threads::Threads)
endif()
################################################################################
@@ -30,13 +30,7 @@ if(BUILD_MISC_TESTS)
else()
target_link_libraries(Messenger_test PRIVATE toxcore_shared)
endif()
if(TARGET pthreads4w::pthreads4w)
target_link_libraries(Messenger_test PRIVATE pthreads4w::pthreads4w)
elseif(TARGET PThreads4W::PThreads4W)
target_link_libraries(Messenger_test PRIVATE PThreads4W::PThreads4W)
elseif(TARGET Threads::Threads)
target_link_libraries(Messenger_test PRIVATE Threads::Threads)
endif()
endif()
add_subdirectory(bench)
add_subdirectory(support)

View File

@@ -102,14 +102,27 @@ int main(int argc, char *argv[])
exit(0);
}
Messenger_Options options = {0};
Messenger_Options options = {nullptr};
options.ipv6enabled = ipv6enabled;
Logger *logger = logger_new(mem);
if (logger == nullptr) {
fputs("Failed to allocate logger datastructure\n", stderr);
mono_time_free(mem, mono_time);
exit(1);
}
options.log = logger;
Messenger_Error err;
m = new_messenger(mono_time, mem, os_random(), os_network(), &options, &err);
if (!m) {
fprintf(stderr, "Failed to allocate messenger datastructure: %u\n", err);
exit(0);
logger_kill(logger);
mono_time_free(mem, mono_time);
exit(1);
}
if (argc == argvoffset + 4) {
@@ -117,6 +130,9 @@ int main(int argc, char *argv[])
if (port_conv <= 0 || port_conv > UINT16_MAX) {
printf("Failed to convert \"%s\" into a valid port. Exiting...\n", argv[argvoffset + 2]);
kill_messenger(m);
logger_kill(logger);
mono_time_free(mem, mono_time);
exit(1);
}
@@ -131,6 +147,9 @@ int main(int argc, char *argv[])
if (!res) {
printf("Failed to convert \"%s\" into an IP address. Exiting...\n", argv[argvoffset + 1]);
kill_messenger(m);
logger_kill(logger);
mono_time_free(mem, mono_time);
exit(1);
}
}
@@ -157,6 +176,9 @@ int main(int argc, char *argv[])
printf("\nEnter the address of the friend you wish to add (38 bytes HEX format):\n");
if (!fgets(temp_hex_id, sizeof(temp_hex_id), stdin)) {
kill_messenger(m);
logger_kill(logger);
mono_time_free(mem, mono_time);
exit(0);
}
@@ -186,6 +208,8 @@ int main(int argc, char *argv[])
if (file == nullptr) {
printf("Failed to open file %s\n", filename);
kill_messenger(m);
logger_kill(logger);
mono_time_free(mem, mono_time);
return 1;
}
@@ -195,6 +219,8 @@ int main(int argc, char *argv[])
fputs("Failed to allocate memory\n", stderr);
fclose(file);
kill_messenger(m);
logger_kill(logger);
mono_time_free(mem, mono_time);
return 1;
}
@@ -205,6 +231,8 @@ int main(int argc, char *argv[])
free(buffer);
fclose(file);
kill_messenger(m);
logger_kill(logger);
mono_time_free(mem, mono_time);
return 1;
}

View File

@@ -0,0 +1,24 @@
load("@rules_cc//cc:defs.bzl", "cc_binary")
cc_binary(
name = "tox_messenger_bench",
testonly = True,
srcs = ["tox_messenger_bench.cc"],
deps = [
"//c-toxcore/testing/support",
"//c-toxcore/toxcore:network",
"//c-toxcore/toxcore:tox",
"@benchmark",
],
)
cc_binary(
name = "tox_friends_scaling_bench",
testonly = True,
srcs = ["tox_friends_scaling_bench.cc"],
deps = [
"//c-toxcore/testing/support",
"//c-toxcore/toxcore:tox",
"@benchmark",
],
)

View File

@@ -0,0 +1,21 @@
if(NOT UNITTEST)
return()
endif()
find_package(benchmark QUIET)
if(benchmark_FOUND)
add_executable(tox_messenger_bench tox_messenger_bench.cc)
target_link_libraries(tox_messenger_bench PRIVATE
toxcore_static
support
benchmark::benchmark
)
add_executable(tox_friends_scaling_bench tox_friends_scaling_bench.cc)
target_link_libraries(tox_friends_scaling_bench PRIVATE
toxcore_static
support
benchmark::benchmark
)
endif()

View File

@@ -0,0 +1,479 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2026 The TokTok team.
*/
#include <benchmark/benchmark.h>
#include <iostream>
#include <memory>
#include <vector>
#include "../../testing/support/public/simulation.hh"
#include "../../testing/support/public/tox_network.hh"
#include "../../toxcore/tox.h"
namespace {
using tox::test::ConnectedFriend;
using tox::test::setup_connected_friends;
using tox::test::SimulatedNode;
using tox::test::Simulation;
// --- Helper Contexts ---
struct GroupContext {
uint32_t peer_count = 0;
uint32_t group_number = UINT32_MAX;
};
// --- Fixtures ---
class ToxIterateScalingFixture : public benchmark::Fixture {
public:
void SetUp(benchmark::State &state) override
{
// Explicitly clear members to handle fixture reuse or non-empty initial state.
// Order matters: dependent objects first.
friend_toxes.clear();
friend_nodes.clear();
main_tox.reset();
main_node.reset();
sim.reset();
int num_friends = state.range(0);
sim = std::make_unique<Simulation>();
main_node = sim->create_node();
main_tox = main_node->create_tox();
for (int i = 0; i < num_friends; ++i) {
auto node = sim->create_node();
auto tox = node->create_tox();
uint8_t friend_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox.get(), friend_pk);
Tox_Err_Friend_Add err;
tox_friend_add_norequest(main_tox.get(), friend_pk, &err);
friend_nodes.push_back(std::move(node));
friend_toxes.push_back(std::move(tox));
}
}
protected:
std::unique_ptr<Simulation> sim;
std::unique_ptr<SimulatedNode> main_node;
SimulatedNode::ToxPtr main_tox;
std::vector<std::unique_ptr<SimulatedNode>> friend_nodes;
std::vector<SimulatedNode::ToxPtr> friend_toxes;
};
// --- Contexts for Shared State Benchmarks ---
class ToxOnlineDisconnectedScalingFixture : public benchmark::Fixture {
static constexpr bool kVerbose = false;
public:
void SetUp(benchmark::State &state) override
{
// Explicitly clear members to handle fixture reuse or non-empty initial state.
// Order matters: dependent objects first (Tox depends on Node).
main_tox.reset();
bootstrap_tox.reset();
main_node.reset();
bootstrap_node.reset();
sim.reset();
int num_friends = state.range(0);
sim = std::make_unique<Simulation>();
sim->net().set_latency(1); // Low latency to encourage traffic
sim->net().set_verbose(kVerbose);
// Create a bootstrap node to ensure we are "online" on the DHT
bootstrap_node = sim->create_node();
auto log_cb = [](Tox *tox, Tox_Log_Level level, const char *file, uint32_t line,
const char *func, const char *message, void *user_data) {
if (kVerbose) {
std::cerr << "Log: " << file << ":" << line << " (" << func << ") " << message
<< std::endl;
}
};
auto opts_bs = std::unique_ptr<Tox_Options, decltype(&tox_options_free)>(
tox_options_new(nullptr), tox_options_free);
assert(opts_bs);
tox_options_set_local_discovery_enabled(opts_bs.get(), false);
tox_options_set_ipv6_enabled(opts_bs.get(), false);
tox_options_set_log_callback(opts_bs.get(), log_cb);
bootstrap_tox = bootstrap_node->create_tox(opts_bs.get());
uint8_t bootstrap_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(bootstrap_tox.get(), bootstrap_pk);
uint16_t bootstrap_port = bootstrap_node->get_primary_socket()->local_port();
main_node = sim->create_node();
auto opts = std::unique_ptr<Tox_Options, decltype(&tox_options_free)>(
tox_options_new(nullptr), tox_options_free);
assert(opts);
// Disable local discovery to force DHT usage
tox_options_set_local_discovery_enabled(opts.get(), false);
tox_options_set_ipv6_enabled(opts.get(), false);
tox_options_set_log_callback(opts.get(), log_cb);
main_tox = main_node->create_tox(opts.get());
// Bootstrap to the network (Mutual bootstrap to ensure connectivity)
Ip_Ntoa bs_ip_str_buf;
const char *bs_ip_str = net_ip_ntoa(&bootstrap_node->ip, &bs_ip_str_buf);
Tox_Err_Bootstrap bs_err;
tox_bootstrap(main_tox.get(), bs_ip_str, bootstrap_port, bootstrap_pk, &bs_err);
if (bs_err != TOX_ERR_BOOTSTRAP_OK) {
std::cerr << "bootstrapping failed: " << bs_err << "\n";
std::abort();
}
// Run until we are connected to the DHT
sim->run_until(
[&]() {
tox_iterate(main_tox.get(), nullptr);
tox_iterate(bootstrap_tox.get(), nullptr);
return tox_self_get_connection_status(main_tox.get()) != TOX_CONNECTION_NONE;
},
15000);
if (tox_self_get_connection_status(main_tox.get()) == TOX_CONNECTION_NONE) {
std::cerr << "WARNING: Failed to connect to DHT in SetUp (timeout 30s)\n";
std::abort();
}
for (int i = 0; i < num_friends; ++i) {
// Add friend but don't create a node for them -> they are offline
uint8_t friend_pk[TOX_PUBLIC_KEY_SIZE];
// Just generate a random PK
main_node->fake_random().bytes(friend_pk, TOX_PUBLIC_KEY_SIZE);
Tox_Err_Friend_Add err;
tox_friend_add_norequest(main_tox.get(), friend_pk, &err);
}
}
protected:
std::unique_ptr<Simulation> sim;
std::unique_ptr<SimulatedNode> main_node;
SimulatedNode::ToxPtr main_tox;
std::unique_ptr<SimulatedNode> bootstrap_node;
SimulatedNode::ToxPtr bootstrap_tox;
};
BENCHMARK_DEFINE_F(ToxOnlineDisconnectedScalingFixture, Iterate)(benchmark::State &state)
{
if (tox_self_get_connection_status(main_tox.get()) == TOX_CONNECTION_NONE) {
state.SkipWithError("not connected to DHT");
}
for (auto _ : state) {
tox_iterate(main_tox.get(), nullptr);
tox_iterate(bootstrap_tox.get(), nullptr);
uint32_t interval = tox_iteration_interval(main_tox.get());
uint32_t interval_bs = tox_iteration_interval(bootstrap_tox.get());
sim->advance_time(std::min(interval, interval_bs));
}
state.counters["mem_current"]
= benchmark::Counter(static_cast<double>(main_node->fake_memory().current_allocation()),
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
}
BENCHMARK_REGISTER_F(ToxOnlineDisconnectedScalingFixture, Iterate)
->Arg(0)
->Arg(10)
->Arg(100)
->Arg(1000)
->Arg(2000);
struct ConnectedContext {
std::unique_ptr<Simulation> sim;
std::unique_ptr<SimulatedNode> main_node;
SimulatedNode::ToxPtr main_tox;
std::vector<ConnectedFriend> friends;
int num_friends = -1;
void Setup(int n)
{
if (num_friends == n)
return;
// Destruction order is critical
friends.clear();
main_tox.reset();
main_node.reset();
sim.reset();
sim = std::make_unique<Simulation>();
sim->net().set_latency(5);
main_node = sim->create_node();
main_tox = main_node->create_tox();
num_friends = n;
if (n > 0) {
friends = setup_connected_friends(*sim, main_tox.get(), *main_node, num_friends);
}
}
~ConnectedContext();
};
ConnectedContext::~ConnectedContext() = default;
struct GroupScalingContext {
static constexpr bool verbose = false;
std::unique_ptr<Simulation> sim;
std::unique_ptr<SimulatedNode> main_node;
SimulatedNode::ToxPtr main_tox;
GroupContext main_ctx;
std::vector<ConnectedFriend> friends;
int num_peers = -1;
void Setup(int peers)
{
if (num_peers == peers)
return;
// Destruction order is critical
friends.clear();
main_ctx = GroupContext();
main_tox.reset();
main_node.reset();
sim.reset();
sim = std::make_unique<Simulation>();
sim->net().set_latency(5);
main_node = sim->create_node();
auto opts = std::unique_ptr<Tox_Options, decltype(&tox_options_free)>(
tox_options_new(nullptr), tox_options_free);
tox_options_set_ipv6_enabled(opts.get(), false);
tox_options_set_local_discovery_enabled(opts.get(), false);
main_tox = main_node->create_tox(opts.get());
num_peers = peers;
// Setup Group Callbacks
tox_callback_group_peer_join(
main_tox.get(), [](Tox *, uint32_t, uint32_t, void *user_data) {
static_cast<GroupContext *>(user_data)->peer_count++;
});
tox_callback_group_peer_exit(main_tox.get(),
[](Tox *, uint32_t, uint32_t, Tox_Group_Exit_Type, const uint8_t *, size_t,
const uint8_t *, size_t,
void *user_data) { static_cast<GroupContext *>(user_data)->peer_count--; });
Tox_Err_Group_New err_new;
main_ctx.group_number = tox_group_new(main_tox.get(), TOX_GROUP_PRIVACY_STATE_PUBLIC,
reinterpret_cast<const uint8_t *>("test"), 4, reinterpret_cast<const uint8_t *>("main"),
4, &err_new);
if (num_peers > 0) {
// Setup Friends
auto opts_friends = std::unique_ptr<Tox_Options, decltype(&tox_options_free)>(
tox_options_new(nullptr), tox_options_free);
tox_options_set_ipv6_enabled(opts_friends.get(), false);
tox_options_set_local_discovery_enabled(opts_friends.get(), false);
friends = setup_connected_friends(
*sim, main_tox.get(), *main_node, num_peers, opts_friends.get());
// Invite Friends
for (const auto &f : friends) {
tox_group_invite_friend(
main_tox.get(), main_ctx.group_number, f.friend_number, nullptr);
}
// Wait for Joins
std::vector<uint32_t> peer_group_numbers(num_peers, UINT32_MAX);
sim->run_until(
[&]() {
tox_iterate(main_tox.get(), &main_ctx);
// Poll events
for (size_t i = 0; i < friends.size(); ++i) {
auto batches = friends[i].runner->poll_events();
for (const auto &batch : batches) {
size_t size = tox_events_get_size(batch.get());
for (size_t k = 0; k < size; ++k) {
const Tox_Event *e = tox_events_get(batch.get(), k);
if (tox_event_get_type(e) == TOX_EVENT_GROUP_INVITE) {
auto *ev = tox_event_get_group_invite(e);
uint32_t friend_number
= tox_event_group_invite_get_friend_number(ev);
const uint8_t *data
= tox_event_group_invite_get_invite_data(ev);
size_t len = tox_event_group_invite_get_invite_data_length(ev);
std::vector<uint8_t> invite_data(data, data + len);
friends[i].runner->execute([=](Tox *tox) {
tox_group_invite_accept(tox, friend_number,
invite_data.data(), invite_data.size(),
reinterpret_cast<const uint8_t *>("peer"), 4, nullptr,
0, nullptr);
});
} else if (tox_event_get_type(e) == TOX_EVENT_GROUP_SELF_JOIN) {
auto *ev = tox_event_get_group_self_join(e);
peer_group_numbers[i]
= tox_event_group_self_join_get_group_number(ev);
}
}
}
}
bool all_joined = true;
for (auto gn : peer_group_numbers)
if (gn == UINT32_MAX)
all_joined = false;
return all_joined;
},
60000);
// Wait for Convergence
sim->run_until(
[&]() {
tox_iterate(main_tox.get(), &main_ctx);
if (main_ctx.peer_count >= static_cast<uint32_t>(num_peers))
return true;
static uint64_t last_print = 0;
if (verbose && sim->clock().current_time_ms() - last_print > 1000) {
std::cerr << "Peers joined: " << main_ctx.peer_count << "/" << num_peers
<< std::endl;
last_print = sim->clock().current_time_ms();
}
return false;
},
120000);
}
}
~GroupScalingContext();
};
GroupScalingContext::~GroupScalingContext() = default;
// --- Benchmark Definitions ---
BENCHMARK_DEFINE_F(ToxIterateScalingFixture, Iterate)(benchmark::State &state)
{
for (auto _ : state) {
tox_iterate(main_tox.get(), nullptr);
}
state.counters["mem_current"]
= benchmark::Counter(static_cast<double>(main_node->fake_memory().current_allocation()),
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
state.counters["mem_max"]
= benchmark::Counter(static_cast<double>(main_node->fake_memory().max_allocation()),
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
}
BENCHMARK_REGISTER_F(ToxIterateScalingFixture, Iterate)
->Arg(0)
->Arg(10)
->Arg(100)
->Arg(200)
->Arg(300);
void RunConnectedScaling(benchmark::State &state, ConnectedContext &ctx)
{
ctx.Setup(state.range(0));
for (auto _ : state) {
tox_iterate(ctx.main_tox.get(), nullptr);
}
state.counters["mem_current"]
= benchmark::Counter(static_cast<double>(ctx.main_node->fake_memory().current_allocation()),
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
state.counters["mem_max"]
= benchmark::Counter(static_cast<double>(ctx.main_node->fake_memory().max_allocation()),
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
}
void RunGroupScaling(benchmark::State &state, GroupScalingContext &ctx)
{
ctx.Setup(state.range(0));
for (auto _ : state) {
tox_iterate(ctx.main_tox.get(), &ctx.main_ctx);
}
state.counters["mem_current"]
= benchmark::Counter(static_cast<double>(ctx.main_node->fake_memory().current_allocation()),
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
state.counters["mem_max"]
= benchmark::Counter(static_cast<double>(ctx.main_node->fake_memory().max_allocation()),
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
state.counters["peers"] = benchmark::Counter(
static_cast<double>(ctx.main_ctx.peer_count + 1), benchmark::Counter::kDefaults);
}
/**
* @brief Benchmark the time and CPU required to discover and connect to many friends.
*
* This stresses the Onion Client's discovery mechanism (shared key caching)
* and the DHT's shared key cache efficiency.
*/
static void BM_MassDiscovery(benchmark::State &state)
{
const int num_friends = state.range(0);
for (auto _ : state) {
Simulation sim;
// Set a realistic latency to ensure packets are in flight and DHT/Onion logic
// has to run multiple iterations.
sim.net().set_latency(10);
auto alice_node = sim.create_node();
auto alice_tox = alice_node->create_tox();
// setup_connected_friends runs the simulation until all friends are connected.
auto friends = setup_connected_friends(sim, alice_tox.get(), *alice_node, num_friends);
benchmark::DoNotOptimize(friends);
}
}
BENCHMARK(BM_MassDiscovery)
->Arg(50)
->Arg(100)
->Arg(200)
->Unit(benchmark::kMillisecond)
->Iterations(5);
} // namespace
int main(int argc, char **argv)
{
::benchmark::Initialize(&argc, argv);
if (::benchmark::ReportUnrecognizedArguments(argc, argv)) {
return 1;
}
ConnectedContext connected_ctx;
benchmark::RegisterBenchmark("ToxConnectedScalingFixture/IterateConnected",
[&](benchmark::State &st) { RunConnectedScaling(st, connected_ctx); })
->Arg(0)
->Arg(10)
->Arg(20)
->Arg(50);
GroupScalingContext group_ctx;
benchmark::RegisterBenchmark("ToxGroupScalingFixture/IterateGroup",
[&](benchmark::State &st) { RunGroupScaling(st, group_ctx); })
->Arg(0)
->Arg(10)
->Arg(20)
->Arg(50);
::benchmark::RunSpecifiedBenchmarks();
::benchmark::Shutdown();
return 0;
}

View File

@@ -0,0 +1,221 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2026 The TokTok team.
*/
#include <benchmark/benchmark.h>
#include <iostream>
#include "../../testing/support/public/simulation.hh"
#include "../../toxcore/network.h"
#include "../../toxcore/tox.h"
namespace {
using tox::test::Simulation;
struct Context {
size_t count = 0;
};
void BM_ToxMessengerThroughput(benchmark::State &state)
{
Simulation sim;
sim.net().set_latency(5);
auto node1 = sim.create_node();
auto node2 = sim.create_node();
auto opts1 = std::unique_ptr<Tox_Options, decltype(&tox_options_free)>(
tox_options_new(nullptr), tox_options_free);
tox_options_set_log_user_data(opts1.get(), const_cast<char *>("Tox1"));
tox_options_set_ipv6_enabled(opts1.get(), false);
tox_options_set_local_discovery_enabled(opts1.get(), false);
auto opts2 = std::unique_ptr<Tox_Options, decltype(&tox_options_free)>(
tox_options_new(nullptr), tox_options_free);
tox_options_set_log_user_data(opts2.get(), const_cast<char *>("Tox2"));
tox_options_set_ipv6_enabled(opts2.get(), false);
tox_options_set_local_discovery_enabled(opts2.get(), false);
auto tox1 = node1->create_tox(opts1.get());
auto tox2 = node2->create_tox(opts2.get());
if (!tox1 || !tox2) {
state.SkipWithError("Failed to create Tox instances");
return;
}
uint8_t tox1_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox1.get(), tox1_pk);
uint8_t tox2_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox2.get(), tox2_pk);
uint8_t tox1_dht_id[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(tox1.get(), tox1_dht_id);
uint8_t tox2_dht_id[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(tox2.get(), tox2_dht_id);
Tox_Err_Friend_Add friend_add_err;
uint32_t f1 = tox_friend_add_norequest(tox1.get(), tox2_pk, &friend_add_err);
uint32_t f2 = tox_friend_add_norequest(tox2.get(), tox1_pk, &friend_add_err);
uint16_t port1 = node1->get_primary_socket()->local_port();
uint16_t port2 = node2->get_primary_socket()->local_port();
char ip1[TOX_INET6_ADDRSTRLEN];
ip_parse_addr(&node1->ip, ip1, sizeof(ip1));
char ip2[TOX_INET6_ADDRSTRLEN];
ip_parse_addr(&node2->ip, ip2, sizeof(ip2));
tox_bootstrap(tox2.get(), ip1, port1, tox1_dht_id, nullptr);
tox_bootstrap(tox1.get(), ip2, port2, tox2_dht_id, nullptr);
bool connected = false;
sim.run_until(
[&]() {
tox_iterate(tox1.get(), nullptr);
tox_iterate(tox2.get(), nullptr);
sim.advance_time(90); // +10ms from run_until = 100ms
connected
= (tox_friend_get_connection_status(tox1.get(), f1, nullptr) != TOX_CONNECTION_NONE
&& tox_friend_get_connection_status(tox2.get(), f2, nullptr)
!= TOX_CONNECTION_NONE);
return connected;
},
60000);
if (!connected) {
state.SkipWithError("Failed to connect toxes within 60s");
return;
}
const uint8_t msg[] = "benchmark message";
const size_t msg_len = sizeof(msg);
Context ctx;
tox_callback_friend_message(tox2.get(),
[](Tox *, uint32_t, Tox_Message_Type, const uint8_t *, size_t, void *user_data) {
static_cast<Context *>(user_data)->count++;
});
for (auto _ : state) {
tox_friend_send_message(tox1.get(), f1, TOX_MESSAGE_TYPE_NORMAL, msg, msg_len, nullptr);
for (int i = 0; i < 5; ++i) {
sim.advance_time(1);
tox_iterate(tox1.get(), nullptr);
tox_iterate(tox2.get(), &ctx);
}
}
state.counters["messages_received"]
= benchmark::Counter(static_cast<double>(ctx.count), benchmark::Counter::kAvgThreads);
}
BENCHMARK(BM_ToxMessengerThroughput);
void BM_ToxMessengerBidirectional(benchmark::State &state)
{
Simulation sim;
sim.net().set_latency(5);
auto node1 = sim.create_node();
auto node2 = sim.create_node();
auto opts1 = std::unique_ptr<Tox_Options, decltype(&tox_options_free)>(
tox_options_new(nullptr), tox_options_free);
tox_options_set_log_user_data(opts1.get(), const_cast<char *>("Tox1"));
tox_options_set_ipv6_enabled(opts1.get(), false);
tox_options_set_local_discovery_enabled(opts1.get(), false);
auto opts2 = std::unique_ptr<Tox_Options, decltype(&tox_options_free)>(
tox_options_new(nullptr), tox_options_free);
tox_options_set_log_user_data(opts2.get(), const_cast<char *>("Tox2"));
tox_options_set_ipv6_enabled(opts2.get(), false);
tox_options_set_local_discovery_enabled(opts2.get(), false);
auto tox1 = node1->create_tox(opts1.get());
auto tox2 = node2->create_tox(opts2.get());
if (!tox1 || !tox2) {
state.SkipWithError("Failed to create Tox instances");
return;
}
uint8_t tox1_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox1.get(), tox1_pk);
uint8_t tox2_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox2.get(), tox2_pk);
uint8_t tox1_dht_id[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(tox1.get(), tox1_dht_id);
uint8_t tox2_dht_id[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(tox2.get(), tox2_dht_id);
Tox_Err_Friend_Add friend_add_err;
uint32_t f1 = tox_friend_add_norequest(tox1.get(), tox2_pk, &friend_add_err);
uint32_t f2 = tox_friend_add_norequest(tox2.get(), tox1_pk, &friend_add_err);
uint16_t port1 = node1->get_primary_socket()->local_port();
uint16_t port2 = node2->get_primary_socket()->local_port();
char ip1[TOX_INET6_ADDRSTRLEN];
ip_parse_addr(&node1->ip, ip1, sizeof(ip1));
char ip2[TOX_INET6_ADDRSTRLEN];
ip_parse_addr(&node2->ip, ip2, sizeof(ip2));
tox_bootstrap(tox2.get(), ip1, port1, tox1_dht_id, nullptr);
tox_bootstrap(tox1.get(), ip2, port2, tox2_dht_id, nullptr);
bool connected = false;
sim.run_until(
[&]() {
tox_iterate(tox1.get(), nullptr);
tox_iterate(tox2.get(), nullptr);
sim.advance_time(90); // +10ms from run_until = 100ms
connected
= (tox_friend_get_connection_status(tox1.get(), f1, nullptr) != TOX_CONNECTION_NONE
&& tox_friend_get_connection_status(tox2.get(), f2, nullptr)
!= TOX_CONNECTION_NONE);
return connected;
},
60000);
if (!connected) {
state.SkipWithError("Failed to connect toxes within 60s");
return;
}
const uint8_t msg[] = "benchmark message";
const size_t msg_len = sizeof(msg);
Context ctx1, ctx2;
tox_callback_friend_message(tox1.get(),
[](Tox *, uint32_t, Tox_Message_Type, const uint8_t *, size_t, void *user_data) {
static_cast<Context *>(user_data)->count++;
});
tox_callback_friend_message(tox2.get(),
[](Tox *, uint32_t, Tox_Message_Type, const uint8_t *, size_t, void *user_data) {
static_cast<Context *>(user_data)->count++;
});
for (auto _ : state) {
tox_friend_send_message(tox1.get(), f1, TOX_MESSAGE_TYPE_NORMAL, msg, msg_len, nullptr);
tox_friend_send_message(tox2.get(), f2, TOX_MESSAGE_TYPE_NORMAL, msg, msg_len, nullptr);
for (int i = 0; i < 5; ++i) {
sim.advance_time(1);
tox_iterate(tox1.get(), &ctx1);
tox_iterate(tox2.get(), &ctx2);
}
}
state.counters["messages_received"] = benchmark::Counter(
static_cast<double>(ctx1.count + ctx2.count), benchmark::Counter::kAvgThreads);
}
BENCHMARK(BM_ToxMessengerBidirectional);
} // namespace
BENCHMARK_MAIN();

View File

@@ -18,6 +18,7 @@ cc_library(
"src/simulated_environment.cc",
"src/simulation.cc",
"src/tox_network.cc",
"src/tox_runner.cc",
],
hdrs = [
"doubles/fake_clock.hh",
@@ -31,11 +32,13 @@ cc_library(
"public/fuzz_data.hh",
"public/fuzz_helpers.hh",
"public/memory.hh",
"public/mpsc_queue.hh",
"public/network.hh",
"public/random.hh",
"public/simulated_environment.hh",
"public/simulation.hh",
"public/tox_network.hh",
"public/tox_runner.hh",
],
copts = select({
"//tools/config:windows": ["/wd4200"], # Zero-sized array in struct/union
@@ -43,12 +46,14 @@ cc_library(
}),
visibility = ["//visibility:public"],
deps = [
"//c-toxcore/toxcore:attributes",
"//c-toxcore/toxcore:mem",
"//c-toxcore/toxcore:net",
"//c-toxcore/toxcore:network",
"//c-toxcore/toxcore:rng",
"//c-toxcore/toxcore:tox",
"//c-toxcore/toxcore:tox_memory",
"//c-toxcore/toxcore:tox_events",
"//c-toxcore/toxcore:tox_options",
"//c-toxcore/toxcore:tox_random",
"@psocket",
],
)
@@ -64,6 +69,28 @@ cc_test(
],
)
cc_test(
name = "fake_network_udp_test",
srcs = ["doubles/fake_network_udp_test.cc"],
deps = [
":support",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
"@psocket",
],
)
cc_test(
name = "fake_network_tcp_test",
srcs = ["doubles/fake_network_tcp_test.cc"],
deps = [
":support",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
"@psocket",
],
)
cc_test(
name = "fake_network_stack_test",
srcs = ["doubles/fake_network_stack_test.cc"],
@@ -98,12 +125,24 @@ cc_test(
],
)
cc_test(
name = "simulation_test",
srcs = ["simulation_test.cc"],
deps = [
":support",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_test(
name = "tox_network_test",
timeout = "long",
srcs = ["tox_network_test.cc"],
deps = [
":support",
"//c-toxcore/toxcore:attributes",
"//c-toxcore/toxcore:network",
"//c-toxcore/toxcore:tox",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",

View File

@@ -18,6 +18,7 @@ set(support_SOURCES
src/simulated_environment.cc
src/simulation.cc
src/tox_network.cc
src/tox_runner.cc
doubles/fake_clock.hh
doubles/fake_memory.hh
doubles/fake_network_stack.hh
@@ -29,11 +30,13 @@ set(support_SOURCES
public/fuzz_data.hh
public/fuzz_helpers.hh
public/memory.hh
public/mpsc_queue.hh
public/network.hh
public/random.hh
public/simulated_environment.hh
public/simulation.hh
public/tox_network.hh
public/tox_runner.hh
)
add_library(support STATIC ${support_SOURCES})
@@ -66,7 +69,10 @@ if(TARGET GTest::gtest_main)
support_test(fake_sockets_test doubles/fake_sockets_test.cc)
support_test(fake_network_stack_test doubles/fake_network_stack_test.cc)
support_test(fake_network_udp_test doubles/fake_network_udp_test.cc)
support_test(fake_network_tcp_test doubles/fake_network_tcp_test.cc)
support_test(network_universe_test doubles/network_universe_test.cc)
support_test(bootstrap_scaling_test bootstrap_scaling_test.cc)
support_test(tox_network_test tox_network_test.cc)
# TODO(iphydf): Re-enable once we migrate TCP server to ev.
#support_test(tox_network_test tox_network_test.cc)
endif()

View File

@@ -1,6 +1,9 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_CLOCK_H
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_CLOCK_H
#include <atomic>
#include <cstdint>
#include "../public/clock.hh"
namespace tox::test {
@@ -15,7 +18,7 @@ public:
void advance(uint64_t ms);
private:
uint64_t now_ms_;
std::atomic<uint64_t> now_ms_;
};
} // namespace tox::test

View File

@@ -1,12 +1,13 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_MEMORY_H
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_MEMORY_H
#include <atomic>
#include <functional>
#include "../public/memory.hh"
// Forward declaration
struct Tox_Memory;
struct Memory;
namespace tox::test {
@@ -18,9 +19,9 @@ public:
FakeMemory();
~FakeMemory() override;
void *malloc(size_t size) override;
void *realloc(void *ptr, size_t size) override;
void free(void *ptr) override;
void *_Nullable malloc(size_t size) override;
void *_Nullable realloc(void *_Nullable ptr, size_t size) override;
void free(void *_Nullable ptr) override;
// Configure failure injection
void set_failure_injector(FailureInjector injector);
@@ -28,10 +29,18 @@ public:
// Configure observer
void set_observer(Observer observer);
// Get the C-compatible struct
struct Tox_Memory get_c_memory();
/**
* @brief Returns C-compatible Memory struct.
*/
struct Memory c_memory() override;
size_t current_allocation() const;
size_t max_allocation() const;
private:
void on_allocation(size_t size);
void on_deallocation(size_t size);
struct Header {
size_t size;
size_t magic;
@@ -39,8 +48,8 @@ private:
static constexpr size_t kMagic = 0xDEADC0DE;
static constexpr size_t kFreeMagic = 0xBAADF00D;
size_t current_allocation_ = 0;
size_t max_allocation_ = 0;
std::atomic<size_t> current_allocation_{0};
std::atomic<size_t> max_allocation_{0};
FailureInjector failure_injector_;
Observer observer_;

View File

@@ -8,55 +8,60 @@
namespace tox::test {
static const Network_Funcs kVtable = {
.close
= [](void *obj, Socket sock) { return static_cast<FakeNetworkStack *>(obj)->close(sock); },
.accept
= [](void *obj, Socket sock) { return static_cast<FakeNetworkStack *>(obj)->accept(sock); },
.bind
= [](void *obj, Socket sock,
const IP_Port *addr) { return static_cast<FakeNetworkStack *>(obj)->bind(sock, addr); },
static const Network_Funcs kNetworkVtable = {
.close = [](void *_Nonnull obj,
Socket sock) { return static_cast<FakeNetworkStack *>(obj)->close(sock); },
.accept = [](void *_Nonnull obj,
Socket sock) { return static_cast<FakeNetworkStack *>(obj)->accept(sock); },
.bind =
[](void *_Nonnull obj, Socket sock, const IP_Port *_Nonnull addr) {
return static_cast<FakeNetworkStack *>(obj)->bind(sock, addr);
},
.listen
= [](void *obj, Socket sock,
= [](void *_Nonnull obj, Socket sock,
int backlog) { return static_cast<FakeNetworkStack *>(obj)->listen(sock, backlog); },
.connect =
[](void *obj, Socket sock, const IP_Port *addr) {
[](void *_Nonnull obj, Socket sock, const IP_Port *_Nonnull addr) {
return static_cast<FakeNetworkStack *>(obj)->connect(sock, addr);
},
.recvbuf
= [](void *obj, Socket sock) { return static_cast<FakeNetworkStack *>(obj)->recvbuf(sock); },
.recv = [](void *obj, Socket sock, uint8_t *buf,
.recvbuf = [](void *_Nonnull obj,
Socket sock) { return static_cast<FakeNetworkStack *>(obj)->recvbuf(sock); },
.recv = [](void *_Nonnull obj, Socket sock, uint8_t *_Nonnull buf,
size_t len) { return static_cast<FakeNetworkStack *>(obj)->recv(sock, buf, len); },
.recvfrom =
[](void *obj, Socket sock, uint8_t *buf, size_t len, IP_Port *addr) {
[](void *_Nonnull obj, Socket sock, uint8_t *_Nonnull buf, size_t len,
IP_Port *_Nonnull addr) {
return static_cast<FakeNetworkStack *>(obj)->recvfrom(sock, buf, len, addr);
},
.send = [](void *obj, Socket sock, const uint8_t *buf,
.send = [](void *_Nonnull obj, Socket sock, const uint8_t *_Nonnull buf,
size_t len) { return static_cast<FakeNetworkStack *>(obj)->send(sock, buf, len); },
.sendto =
[](void *obj, Socket sock, const uint8_t *buf, size_t len, const IP_Port *addr) {
[](void *_Nonnull obj, Socket sock, const uint8_t *_Nonnull buf, size_t len,
const IP_Port *_Nonnull addr) {
return static_cast<FakeNetworkStack *>(obj)->sendto(sock, buf, len, addr);
},
.socket
= [](void *obj, int domain, int type,
= [](void *_Nonnull obj, int domain, int type,
int proto) { return static_cast<FakeNetworkStack *>(obj)->socket(domain, type, proto); },
.socket_nonblock =
[](void *obj, Socket sock, bool nonblock) {
[](void *_Nonnull obj, Socket sock, bool nonblock) {
return static_cast<FakeNetworkStack *>(obj)->socket_nonblock(sock, nonblock);
},
.getsockopt =
[](void *obj, Socket sock, int level, int optname, void *optval, size_t *optlen) {
[](void *_Nonnull obj, Socket sock, int level, int optname, void *_Nonnull optval,
size_t *_Nonnull optlen) {
return static_cast<FakeNetworkStack *>(obj)->getsockopt(
sock, level, optname, optval, optlen);
},
.setsockopt =
[](void *obj, Socket sock, int level, int optname, const void *optval, size_t optlen) {
[](void *_Nonnull obj, Socket sock, int level, int optname, const void *_Nonnull optval,
size_t optlen) {
return static_cast<FakeNetworkStack *>(obj)->setsockopt(
sock, level, optname, optval, optlen);
},
.getaddrinfo =
[](void *obj, const Memory *mem, const char *address, int family, int protocol,
IP_Port **addrs) {
[](void *_Nonnull obj, const Memory *_Nonnull mem, const char *_Nonnull address, int family,
int protocol, IP_Port *_Nullable *_Nonnull addrs) {
FakeNetworkStack *self = static_cast<FakeNetworkStack *>(obj);
if (self->universe().is_verbose()) {
std::cerr << "[FakeNetworkStack] getaddrinfo for " << address << std::endl;
@@ -83,7 +88,7 @@ static const Network_Funcs kVtable = {
return 0;
},
.freeaddrinfo =
[](void *obj, const Memory *mem, IP_Port *addrs) {
[](void *_Nonnull obj, const Memory *_Nonnull mem, IP_Port *_Nullable addrs) {
mem_delete(mem, addrs);
return 0;
},
@@ -97,7 +102,7 @@ FakeNetworkStack::FakeNetworkStack(NetworkUniverse &universe, const IP &node_ip)
FakeNetworkStack::~FakeNetworkStack() = default;
struct Network FakeNetworkStack::get_c_network() { return Network{&kVtable, this}; }
struct Network FakeNetworkStack::c_network() { return Network{&kNetworkVtable, this}; }
Socket FakeNetworkStack::socket(int domain, int type, int protocol)
{

View File

@@ -2,8 +2,11 @@
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_NETWORK_STACK_H
#include <map>
#include <memory>
#include <mutex>
#include <vector>
#include "../../../toxcore/net.h"
#include "../public/network.hh"
#include "fake_sockets.hh"
#include "network_universe.hh"
@@ -17,33 +20,38 @@ public:
// NetworkSystem Implementation
Socket socket(int domain, int type, int protocol) override;
int bind(Socket sock, const IP_Port *addr) override;
int bind(Socket sock, const IP_Port *_Nonnull addr) override;
int close(Socket sock) override;
int sendto(Socket sock, const uint8_t *buf, size_t len, const IP_Port *addr) override;
int recvfrom(Socket sock, uint8_t *buf, size_t len, IP_Port *addr) override;
int sendto(Socket sock, const uint8_t *_Nonnull buf, size_t len,
const IP_Port *_Nonnull addr) override;
int recvfrom(Socket sock, uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr) override;
int listen(Socket sock, int backlog) override;
Socket accept(Socket sock) override;
int connect(Socket sock, const IP_Port *addr) override;
int send(Socket sock, const uint8_t *buf, size_t len) override;
int recv(Socket sock, uint8_t *buf, size_t len) override;
int connect(Socket sock, const IP_Port *_Nonnull addr) override;
int send(Socket sock, const uint8_t *_Nonnull buf, size_t len) override;
int recv(Socket sock, uint8_t *_Nonnull buf, size_t len) override;
int recvbuf(Socket sock) override;
int socket_nonblock(Socket sock, bool nonblock) override;
int getsockopt(Socket sock, int level, int optname, void *optval, size_t *optlen) override;
int setsockopt(Socket sock, int level, int optname, const void *optval, size_t optlen) override;
int getsockopt(Socket sock, int level, int optname, void *_Nonnull optval,
size_t *_Nonnull optlen) override;
int setsockopt(
Socket sock, int level, int optname, const void *_Nonnull optval, size_t optlen) override;
struct Network get_c_network();
/**
* @brief Returns C-compatible Network struct.
*/
struct Network c_network() override;
// For testing/fuzzing introspection
FakeUdpSocket *get_udp_socket(Socket sock);
FakeSocket *_Nullable get_sock(Socket sock);
FakeUdpSocket *_Nullable get_udp_socket(Socket sock);
std::vector<FakeUdpSocket *> get_bound_udp_sockets();
NetworkUniverse &universe() { return universe_; }
private:
FakeSocket *get_sock(Socket sock);
NetworkUniverse &universe_;
std::map<int, std::unique_ptr<FakeSocket>> sockets_;
int next_fd_ = 100;

View File

@@ -87,5 +87,62 @@ namespace {
ASSERT_NE(net_socket_to_native(accepted), -1);
}
TEST_F(FakeNetworkStackTest, LoopbackRedirection)
{
// 1. Create a stack with a specific IP (20.0.0.1)
FakeNetworkStack my_stack{universe, make_ip(0x14000001)};
Socket sock = my_stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
IP_Port bind_addr;
ip_init(&bind_addr.ip, false);
bind_addr.ip.ip.v4.uint32 = net_htonl(0x14000001);
bind_addr.port = net_htons(12345);
ASSERT_EQ(my_stack.bind(sock, &bind_addr), 0);
ASSERT_EQ(my_stack.listen(sock, 5), 0);
// 2. Connect to 127.0.0.1:12345 from the same stack
Socket client = my_stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
IP_Port connect_addr;
ip_init(&connect_addr.ip, false);
connect_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
connect_addr.port = net_htons(12345);
// Should redirect to 20.0.0.1:12345 because 127.0.0.1 is not bound
ASSERT_EQ(my_stack.connect(client, &connect_addr), -1);
ASSERT_EQ(errno, EINPROGRESS);
universe.process_events(0); // SYN
Socket accepted = my_stack.accept(sock);
ASSERT_NE(net_socket_to_native(accepted), -1);
}
TEST_F(FakeNetworkStackTest, ImplicitBindAvoidsCollision)
{
// Bind server to 33445 (default start of find_free_port)
Socket server = stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
IP_Port addr;
ip_init(&addr.ip, false);
addr.ip.ip.v4.uint32 = 0;
addr.port = net_htons(33445);
ASSERT_EQ(stack.bind(server, &addr), 0);
// Create client and connect (implicit bind)
Socket client = stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
IP_Port server_addr;
ip_init(&server_addr.ip, false);
server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
server_addr.port = net_htons(33445);
// Should find a free port (not 33445)
ASSERT_EQ(stack.connect(client, &server_addr), -1);
ASSERT_EQ(errno, EINPROGRESS);
auto *client_obj = stack.get_sock(client);
ASSERT_NE(client_obj, nullptr);
ASSERT_NE(client_obj->local_port(), 33445);
}
} // namespace
} // namespace tox::test

View File

@@ -0,0 +1,459 @@
#include <gtest/gtest.h>
#include "fake_sockets.hh"
#include "network_universe.hh"
namespace tox::test {
namespace {
class FakeNetworkTcpTest : public ::testing::Test {
protected:
NetworkUniverse universe;
IP make_ip(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
IP ip;
ip_init(&ip, false);
ip.ip.v4.uint8[0] = a;
ip.ip.v4.uint8[1] = b;
ip.ip.v4.uint8[2] = c;
ip.ip.v4.uint8[3] = d;
return ip;
}
};
TEST_F(FakeNetworkTcpTest, MultipleConnectionsToSamePort)
{
universe.set_verbose(true);
IP server_ip = make_ip(10, 0, 0, 1);
uint16_t server_port = 12345;
FakeTcpSocket server(universe);
server.set_ip(server_ip);
IP_Port server_addr{server_ip, net_htons(server_port)};
ASSERT_EQ(server.bind(&server_addr), 0);
ASSERT_EQ(server.listen(5), 0);
// Client 1
IP client1_ip = make_ip(10, 0, 0, 2);
FakeTcpSocket client1(universe);
client1.set_ip(client1_ip);
client1.connect(&server_addr);
// Client 2 (same IP as client 1, different port)
FakeTcpSocket client2(universe);
client2.set_ip(client1_ip);
client2.connect(&server_addr);
// Handshake for both
// 1. SYNs
universe.process_events(0);
universe.process_events(0);
// 2. SYN-ACKs
universe.process_events(0);
universe.process_events(0);
// 3. ACKs
universe.process_events(0);
universe.process_events(0);
auto accepted1 = server.accept(nullptr);
auto accepted2 = server.accept(nullptr);
ASSERT_NE(accepted1, nullptr);
ASSERT_NE(accepted2, nullptr);
EXPECT_EQ(
static_cast<FakeTcpSocket *>(accepted1.get())->state(), FakeTcpSocket::ESTABLISHED);
EXPECT_EQ(
static_cast<FakeTcpSocket *>(accepted2.get())->state(), FakeTcpSocket::ESTABLISHED);
// Verify data isolation
const char *msg1 = "Message 1";
const char *msg2 = "Message 2";
client1.send(reinterpret_cast<const uint8_t *>(msg1), strlen(msg1));
client2.send(reinterpret_cast<const uint8_t *>(msg2), strlen(msg2));
universe.process_events(0);
universe.process_events(0);
uint8_t buf[100];
int len1 = accepted1->recv(buf, sizeof(buf));
EXPECT_EQ(len1, strlen(msg1));
int len2 = accepted2->recv(buf, sizeof(buf));
EXPECT_EQ(len2, strlen(msg2));
}
TEST_F(FakeNetworkTcpTest, DuplicateSynCreatesDuplicateConnections)
{
universe.set_verbose(true);
IP server_ip = make_ip(10, 0, 0, 1);
uint16_t server_port = 12345;
FakeTcpSocket server(universe);
server.set_ip(server_ip);
IP_Port server_addr{server_ip, net_htons(server_port)};
server.bind(&server_addr);
server.listen(5);
IP client_ip = make_ip(10, 0, 0, 2);
IP_Port client_addr{client_ip, net_htons(33445)};
Packet p{};
p.from = client_addr;
p.to = server_addr;
p.is_tcp = true;
p.tcp_flags = 0x02; // SYN
p.seq = 100;
universe.send_packet(p);
universe.send_packet(p); // Duplicate SYN
universe.process_events(0);
universe.process_events(0);
// Now send ACK from client
Packet ack{};
ack.from = client_addr;
ack.to = server_addr;
ack.is_tcp = true;
ack.tcp_flags = 0x10; // ACK
ack.ack = 101;
universe.send_packet(ack);
universe.process_events(0);
auto accepted1 = server.accept(nullptr);
auto accepted2 = server.accept(nullptr);
ASSERT_NE(accepted1, nullptr);
EXPECT_EQ(accepted2, nullptr); // This should pass now
}
TEST_F(FakeNetworkTcpTest, PeerCloseClearsConnection)
{
universe.set_verbose(true);
IP server_ip = make_ip(10, 0, 0, 1);
uint16_t server_port = 12345;
FakeTcpSocket server(universe);
server.set_ip(server_ip);
IP_Port server_addr{server_ip, net_htons(server_port)};
server.bind(&server_addr);
server.listen(5);
IP client_ip = make_ip(10, 0, 0, 2);
FakeTcpSocket client(universe);
client.set_ip(client_ip);
client.connect(&server_addr);
// Handshake
universe.process_events(0); // SYN
universe.process_events(0); // SYN-ACK
universe.process_events(0); // ACK
auto accepted = server.accept(nullptr);
ASSERT_NE(accepted, nullptr);
EXPECT_EQ(
static_cast<FakeTcpSocket *>(accepted.get())->state(), FakeTcpSocket::ESTABLISHED);
// Client closes
client.close();
universe.process_events(0); // Deliver RST/FIN
// Server should no longer be ESTABLISHED
EXPECT_EQ(static_cast<FakeTcpSocket *>(accepted.get())->state(), FakeTcpSocket::CLOSED);
// Now if client reconnects with same port
FakeTcpSocket client2(universe);
client2.set_ip(client_ip);
client2.connect(&server_addr);
universe.process_events(0); // Deliver SYN
// Node 2 port 20002 should have: 1 LISTEN, 0 ESTABLISHED (old one gone), 1 SYN_RECEIVED
// (new one) Total targets should be 2.
}
TEST_F(FakeNetworkTcpTest, DataNotProcessedByMultipleSockets)
{
universe.set_verbose(true);
IP server_ip = make_ip(10, 0, 0, 1);
uint16_t server_port = 12345;
FakeTcpSocket server(universe);
server.set_ip(server_ip);
IP_Port server_addr{server_ip, net_htons(server_port)};
server.bind(&server_addr);
server.listen(5);
IP client_ip = make_ip(10, 0, 0, 2);
IP_Port client_addr{client_ip, net_htons(33445)};
// Manually create two "established" sockets on the same port for the same peer
// This simulates a bug where duplicate connections were allowed.
auto sock1 = FakeTcpSocket::create_connected(universe, client_addr, server_port);
sock1->set_ip(server_ip);
auto sock2 = FakeTcpSocket::create_connected(universe, client_addr, server_port);
sock2->set_ip(server_ip);
universe.bind_tcp(server_ip, server_port, sock1.get());
universe.bind_tcp(server_ip, server_port, sock2.get());
// Send data from client to server
Packet p{};
p.from = client_addr;
p.to = server_addr;
p.is_tcp = true;
p.tcp_flags = 0x10; // ACK (Data)
const char *data = "Unique";
p.data.assign(data, data + strlen(data));
universe.send_packet(p);
universe.process_events(0);
// Only ONE of them should have received it, or at least they shouldn't BOTH have it
// in a way that suggests duplicate delivery.
EXPECT_TRUE((sock1->recv_buffer_size() == strlen(data))
^ (sock2->recv_buffer_size() == strlen(data)));
}
TEST_F(FakeNetworkTcpTest, ConnectionCollision)
{
universe.set_verbose(true);
IP server_ip = make_ip(10, 0, 0, 1);
uint16_t server_port = 12345;
FakeTcpSocket server(universe);
server.set_ip(server_ip);
IP_Port server_addr{server_ip, net_htons(server_port)};
server.bind(&server_addr);
server.listen(5);
IP client_ip = make_ip(10, 0, 0, 2);
FakeTcpSocket client1(universe);
client1.set_ip(client_ip);
// Bind to specific port to force collision later
IP_Port client_bind_addr{client_ip, net_htons(33445)};
client1.bind(&client_bind_addr);
client1.connect(&server_addr);
// Handshake 1
universe.process_events(0); // SYN
universe.process_events(0); // SYN-ACK
universe.process_events(0); // ACK
auto accepted1 = server.accept(nullptr);
ASSERT_NE(accepted1, nullptr);
EXPECT_EQ(
static_cast<FakeTcpSocket *>(accepted1.get())->state(), FakeTcpSocket::ESTABLISHED);
// Now client 1 "reconnects" (e.g. after a crash or timeout, but using same port)
FakeTcpSocket client2(universe);
client2.set_ip(client_ip);
client2.bind(&client_bind_addr); // Forced collision
client2.connect(&server_addr);
// Deliver new SYN
universe.process_events(0);
// server_addr port 12345 now has:
// 1. LISTEN socket
// 2. accepted1 (ESTABLISHED with 10.0.0.2:33445)
// In our simplified simulation, the ESTABLISHED socket now handles the SYN by returning
// true (ignoring it). So no new connection is created.
auto accepted2 = server.accept(nullptr);
EXPECT_EQ(accepted2, nullptr);
const char *msg1 = "Data 1";
client1.send(reinterpret_cast<const uint8_t *>(msg1), strlen(msg1));
universe.process_events(0);
// Data should still go to accepted1
EXPECT_EQ(accepted1->recv_buffer_size(), strlen(msg1));
}
TEST_F(FakeNetworkTcpTest, LoopbackConnection)
{
universe.set_verbose(true);
IP node_ip = make_ip(10, 0, 0, 1);
uint16_t port = 12345;
FakeTcpSocket server(universe);
server.set_ip(node_ip);
IP_Port listen_addr{node_ip, net_htons(port)};
server.bind(&listen_addr);
server.listen(5);
FakeTcpSocket client(universe);
client.set_ip(node_ip);
IP loopback_ip;
ip_init(&loopback_ip, false);
loopback_ip.ip.v4.uint32 = net_htonl(0x7F000001);
IP_Port server_loopback_addr{loopback_ip, net_htons(port)};
client.connect(&server_loopback_addr);
// SYN (Client -> 127.0.0.1:12345)
universe.process_events(0);
// SYN-ACK (Server -> Client)
universe.process_events(0);
// ACK (Client -> Server)
universe.process_events(0);
EXPECT_EQ(client.state(), FakeTcpSocket::ESTABLISHED);
auto accepted = server.accept(nullptr);
ASSERT_NE(accepted, nullptr);
EXPECT_EQ(
static_cast<FakeTcpSocket *>(accepted.get())->state(), FakeTcpSocket::ESTABLISHED);
// Data Transfer
const char *msg = "Loopback";
client.send(reinterpret_cast<const uint8_t *>(msg), strlen(msg));
universe.process_events(0);
uint8_t buf[100];
int len = accepted->recv(buf, sizeof(buf));
ASSERT_EQ(len, strlen(msg));
EXPECT_EQ(std::string(reinterpret_cast<char *>(buf), len), msg);
}
TEST_F(FakeNetworkTcpTest, SimultaneousConnect)
{
universe.set_verbose(true);
IP ipA = make_ip(10, 0, 0, 1);
IP ipB = make_ip(10, 0, 0, 2);
uint16_t portA = 10001;
uint16_t portB = 10002;
FakeTcpSocket sockA(universe);
sockA.set_ip(ipA);
IP_Port addrA{ipA, net_htons(portA)};
sockA.bind(&addrA);
sockA.listen(5);
FakeTcpSocket sockB(universe);
sockB.set_ip(ipB);
IP_Port addrB{ipB, net_htons(portB)};
sockB.bind(&addrB);
sockB.listen(5);
// A connects to B
sockA.connect(&addrB);
// B connects to A
sockB.connect(&addrA);
// This is "simultaneous open" in TCP but here they are also LISTENing.
// Toxcore uses this pattern sometimes.
universe.process_events(0); // SYN from A to B
universe.process_events(0); // SYN from B to A
universe.process_events(0); // SYN-ACK from B to A (for A's SYN)
universe.process_events(0); // SYN-ACK from A to B (for B's SYN)
universe.process_events(0); // ACK from A to B
universe.process_events(0); // ACK from B to A
EXPECT_EQ(sockA.state(), FakeTcpSocket::ESTABLISHED);
EXPECT_EQ(sockB.state(), FakeTcpSocket::ESTABLISHED);
}
TEST_F(FakeNetworkTcpTest, DataInHandshakeAck)
{
universe.set_verbose(true);
IP server_ip = make_ip(10, 0, 0, 1);
uint16_t server_port = 12345;
FakeTcpSocket server(universe);
server.set_ip(server_ip);
IP_Port server_addr{server_ip, net_htons(server_port)};
server.bind(&server_addr);
server.listen(5);
IP client_ip = make_ip(10, 0, 0, 2);
IP_Port client_addr{client_ip, net_htons(33445)};
// 1. SYN
Packet syn{};
syn.from = client_addr;
syn.to = server_addr;
syn.is_tcp = true;
syn.tcp_flags = 0x02;
universe.send_packet(syn);
universe.process_events(0);
// 2. SYN-ACK (Server -> Client)
universe.process_events(0);
// 3. ACK + Data (Client -> Server)
Packet ack{};
ack.from = client_addr;
ack.to = server_addr;
ack.is_tcp = true;
ack.tcp_flags = 0x10;
const char *data = "HandshakeData";
ack.data.assign(data, data + strlen(data));
universe.send_packet(ack);
universe.process_events(0);
auto accepted = server.accept(nullptr);
ASSERT_NE(accepted, nullptr);
EXPECT_EQ(accepted->recv_buffer_size(), strlen(data));
}
TEST_F(FakeNetworkTcpTest, LoopbackWithNodeIPMixed)
{
universe.set_verbose(true);
IP node_ip = make_ip(10, 0, 0, 1);
uint16_t port = 12345;
FakeTcpSocket server(universe);
server.set_ip(node_ip);
IP_Port listen_addr{node_ip, net_htons(port)};
server.bind(&listen_addr);
server.listen(5);
FakeTcpSocket client(universe);
client.set_ip(node_ip);
IP loopback_ip;
ip_init(&loopback_ip, false);
loopback_ip.ip.v4.uint32 = net_htonl(0x7F000001);
IP_Port server_loopback_addr{loopback_ip, net_htons(port)};
// Client connects to 127.0.0.1
client.connect(&server_loopback_addr);
universe.process_events(0); // SYN (Client -> 127.0.0.1)
universe.process_events(0); // SYN-ACK (Server -> Client)
universe.process_events(0); // ACK (Client -> Server)
EXPECT_EQ(client.state(), FakeTcpSocket::ESTABLISHED);
auto accepted = server.accept(nullptr);
ASSERT_NE(accepted, nullptr);
// Now manually simulate a packet coming from the server's EXTERNAL IP to the client.
// This happens because the server socket is bound to node_ip, so its packets might
// be delivered as coming from node_ip even if the client connected to 127.0.0.1.
Packet p{};
p.from = listen_addr; // node_ip:port
p.to.ip = node_ip;
p.to.port = net_htons(client.local_port());
p.is_tcp = true;
p.tcp_flags = 0x10; // ACK
const char *msg = "MixedIP";
p.data.assign(msg, msg + strlen(msg));
universe.send_packet(p);
universe.process_events(0);
EXPECT_EQ(client.recv_buffer_size(), strlen(msg));
}
}
}

View File

@@ -0,0 +1,75 @@
#include <gtest/gtest.h>
#include "fake_network_stack.hh"
#include "network_universe.hh"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <winsock2.h>
#else
#include <netinet/in.h>
#include <sys/socket.h>
#endif
namespace tox::test {
namespace {
class FakeNetworkUdpTest : public ::testing::Test {
public:
FakeNetworkUdpTest()
: ip1(make_ip(0x0A000001)) // 10.0.0.1
, ip2(make_ip(0x0A000002)) // 10.0.0.2
, stack1{universe, ip1}
, stack2{universe, ip2}
{
}
protected:
NetworkUniverse universe;
IP ip1, ip2;
FakeNetworkStack stack1;
FakeNetworkStack stack2;
};
TEST_F(FakeNetworkUdpTest, UdpExchange)
{
Socket sock1 = stack1.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
Socket sock2 = stack2.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
IP_Port addr1;
addr1.ip = ip1;
addr1.port = net_htons(1234);
ASSERT_EQ(stack1.bind(sock1, &addr1), 0);
IP_Port addr2;
addr2.ip = ip2;
addr2.port = net_htons(5678);
ASSERT_EQ(stack2.bind(sock2, &addr2), 0);
const char *msg = "Hello UDP";
size_t msg_len = strlen(msg) + 1;
// Send from 1 to 2
ASSERT_EQ(stack1.sendto(sock1, reinterpret_cast<const uint8_t *>(msg), msg_len, &addr2),
static_cast<int>(msg_len));
// Delivery
universe.process_events(10); // With some time offset
// Receive at 2
uint8_t buffer[1024];
IP_Port from_addr;
int recv_len = stack2.recvfrom(sock2, buffer, sizeof(buffer), &from_addr);
ASSERT_EQ(recv_len, static_cast<int>(msg_len));
EXPECT_STREQ(reinterpret_cast<const char *>(buffer), msg);
EXPECT_EQ(net_ntohl(from_addr.ip.ip.v4.uint32), net_ntohl(ip1.ip.v4.uint32));
EXPECT_EQ(net_ntohs(from_addr.port), 1234);
stack1.close(sock1);
stack2.close(sock2);
}
} // namespace
} // namespace tox::test

View File

@@ -7,19 +7,19 @@
#include "../public/random.hh"
// Forward declaration
struct Tox_Random;
struct Random;
namespace tox::test {
class FakeRandom : public RandomSystem {
public:
using EntropySource = std::function<void(uint8_t *out, size_t count)>;
using Observer = std::function<void(const uint8_t *data, size_t count)>;
using EntropySource = std::function<void(uint8_t *_Nonnull out, size_t count)>;
using Observer = std::function<void(const uint8_t *_Nonnull data, size_t count)>;
explicit FakeRandom(uint64_t seed);
uint32_t uniform(uint32_t upper_bound) override;
void bytes(uint8_t *out, size_t count) override;
void bytes(uint8_t *_Nonnull out, size_t count) override;
/**
* @brief Set a custom entropy source.
@@ -32,7 +32,10 @@ public:
*/
void set_observer(Observer observer);
struct Tox_Random get_c_random();
/**
* @brief Returns C-compatible Random struct.
*/
struct Random c_random() override;
private:
std::minstd_rand rng_;

View File

@@ -3,7 +3,11 @@
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <mutex>
#include <vector>
#include "network_universe.hh"
@@ -27,8 +31,14 @@ int FakeSocket::close()
return 0;
}
int FakeSocket::getsockopt(int level, int optname, void *optval, size_t *optlen) { return 0; }
int FakeSocket::setsockopt(int level, int optname, const void *optval, size_t optlen) { return 0; }
int FakeSocket::getsockopt(int level, int optname, void *_Nonnull optval, size_t *_Nonnull optlen)
{
return 0;
}
int FakeSocket::setsockopt(int level, int optname, const void *_Nonnull optval, size_t optlen)
{
return 0;
}
int FakeSocket::socket_nonblock(bool nonblock)
{
nonblocking_ = nonblock;
@@ -59,7 +69,7 @@ void FakeUdpSocket::close_impl()
}
}
int FakeUdpSocket::bind(const IP_Port *addr)
int FakeUdpSocket::bind(const IP_Port *_Nonnull addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (local_port_ != 0)
@@ -80,7 +90,7 @@ int FakeUdpSocket::bind(const IP_Port *addr)
return -1;
}
int FakeUdpSocket::connect(const IP_Port *addr)
int FakeUdpSocket::connect(const IP_Port *_Nonnull addr)
{
// UDP connect just sets default dest.
// Not strictly needed for toxcore UDP but good for completeness.
@@ -92,23 +102,29 @@ int FakeUdpSocket::listen(int backlog)
errno = EOPNOTSUPP;
return -1;
}
std::unique_ptr<FakeSocket> FakeUdpSocket::accept(IP_Port *addr)
std::unique_ptr<FakeSocket> FakeUdpSocket::accept(IP_Port *_Nullable addr)
{
errno = EOPNOTSUPP;
return nullptr;
}
int FakeUdpSocket::send(const uint8_t *buf, size_t len)
int FakeUdpSocket::send(const uint8_t *_Nonnull buf, size_t len)
{
errno = EDESTADDRREQ;
return -1;
}
int FakeUdpSocket::recv(uint8_t *buf, size_t len)
int FakeUdpSocket::recv(uint8_t *_Nonnull buf, size_t len)
{
errno = EOPNOTSUPP;
return -1;
}
int FakeUdpSocket::sendto(const uint8_t *buf, size_t len, const IP_Port *addr)
size_t FakeUdpSocket::recv_buffer_size()
{
std::lock_guard<std::mutex> lock(mutex_);
return recv_queue_.size();
}
int FakeUdpSocket::sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (local_port_ == 0) {
@@ -132,16 +148,15 @@ int FakeUdpSocket::sendto(const uint8_t *buf, size_t len, const IP_Port *addr)
universe_.send_packet(p);
if (universe_.is_verbose()) {
uint32_t tip4 = net_ntohl(addr->ip.ip.v4.uint32);
Ip_Ntoa ip_str;
net_ip_ntoa(&addr->ip, &ip_str);
std::cerr << "[FakeUdpSocket] sent " << len << " bytes from port " << local_port_ << " to "
<< ((tip4 >> 24) & 0xFF) << "." << ((tip4 >> 16) & 0xFF) << "."
<< ((tip4 >> 8) & 0xFF) << "." << (tip4 & 0xFF) << ":" << net_ntohs(addr->port)
<< std::endl;
<< ip_str.buf << ":" << net_ntohs(addr->port) << std::endl;
}
return len;
}
int FakeUdpSocket::recvfrom(uint8_t *buf, size_t len, IP_Port *addr)
int FakeUdpSocket::recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr)
{
RecvObserver observer_copy;
std::vector<uint8_t> data_copy;
@@ -196,15 +211,13 @@ void FakeUdpSocket::push_packet(std::vector<uint8_t> data, IP_Port from)
{
std::lock_guard<std::mutex> lock(mutex_);
if (universe_.is_verbose()) {
uint32_t fip4 = net_ntohl(from.ip.ip.v4.uint32);
Ip_Ntoa local_ip_str, from_ip_str;
net_ip_ntoa(&ip_, &local_ip_str);
net_ip_ntoa(&from.ip, &from_ip_str);
std::cerr << "[FakeUdpSocket] push " << data.size() << " bytes into queue for "
<< ((ip_.ip.v4.uint32 >> 24) & 0xFF)
<< "." // ip_ is in network order from net_htonl
<< ((ip_.ip.v4.uint32 >> 16) & 0xFF) << "." << ((ip_.ip.v4.uint32 >> 8) & 0xFF)
<< "." << (ip_.ip.v4.uint32 & 0xFF) << ":" << local_port_ << " from "
<< ((fip4 >> 24) & 0xFF) << "." << ((fip4 >> 16) & 0xFF) << "."
<< ((fip4 >> 8) & 0xFF) << "." << (fip4 & 0xFF) << ":" << net_ntohs(from.port)
<< std::endl;
<< local_ip_str.buf << ":" << local_port_ << " from " << from_ip_str.buf << ":"
<< net_ntohs(from.port) << std::endl;
}
recv_queue_.push_back({std::move(data), from});
}
@@ -225,8 +238,8 @@ void FakeUdpSocket::set_recv_observer(RecvObserver observer)
FakeTcpSocket::FakeTcpSocket(NetworkUniverse &universe)
: FakeSocket(universe, SOCK_STREAM)
, remote_addr_{}
{
ipport_reset(&remote_addr_);
}
FakeTcpSocket::~FakeTcpSocket() { close_impl(); }
@@ -234,6 +247,17 @@ FakeTcpSocket::~FakeTcpSocket() { close_impl(); }
int FakeTcpSocket::close()
{
std::lock_guard<std::mutex> lock(mutex_);
if (state_ == ESTABLISHED || state_ == SYN_SENT || state_ == SYN_RECEIVED
|| state_ == CLOSE_WAIT) {
// Send RST to peer
Packet p{};
p.from.ip = ip_;
p.from.port = net_htons(local_port_);
p.to = remote_addr_;
p.is_tcp = true;
p.tcp_flags = 0x04; // RST
universe_.send_packet(p);
}
close_impl();
return 0;
}
@@ -247,7 +271,7 @@ void FakeTcpSocket::close_impl()
state_ = CLOSED;
}
int FakeTcpSocket::bind(const IP_Port *addr)
int FakeTcpSocket::bind(const IP_Port *_Nonnull addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (local_port_ != 0)
@@ -276,14 +300,25 @@ int FakeTcpSocket::listen(int backlog)
return 0;
}
int FakeTcpSocket::connect(const IP_Port *addr)
int FakeTcpSocket::connect(const IP_Port *_Nonnull addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (universe_.is_verbose()) {
Ip_Ntoa ip_str, dest_str;
net_ip_ntoa(&ip_, &ip_str);
net_ip_ntoa(&addr->ip, &dest_str);
std::cerr << "[FakeTcpSocket] connect from " << ip_str.buf << " to " << dest_str.buf << ":"
<< net_ntohs(addr->port) << std::endl;
}
if (local_port_ == 0) {
// Implicit bind
uint16_t p = universe_.find_free_port(ip_);
if (universe_.bind_tcp(ip_, p, this)) {
local_port_ = p;
if (universe_.is_verbose()) {
std::cerr << "[FakeTcpSocket] implicit bind to port " << local_port_ << std::endl;
}
} else {
errno = EADDRINUSE;
return -1;
@@ -310,7 +345,7 @@ int FakeTcpSocket::connect(const IP_Port *addr)
return -1;
}
std::unique_ptr<FakeSocket> FakeTcpSocket::accept(IP_Port *addr)
std::unique_ptr<FakeSocket> FakeTcpSocket::accept(IP_Port *_Nullable addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (state_ != LISTEN) {
@@ -318,13 +353,16 @@ std::unique_ptr<FakeSocket> FakeTcpSocket::accept(IP_Port *addr)
return nullptr;
}
if (pending_connections_.empty()) {
auto it = std::find_if(pending_connections_.begin(), pending_connections_.end(),
[](const std::unique_ptr<FakeTcpSocket> &s) { return s->state() == ESTABLISHED; });
if (it == pending_connections_.end()) {
errno = EWOULDBLOCK;
return nullptr;
}
auto client = std::move(pending_connections_.front());
pending_connections_.pop_front();
auto client = std::move(*it);
pending_connections_.erase(it);
if (addr) {
*addr = client->remote_addr();
@@ -332,11 +370,19 @@ std::unique_ptr<FakeSocket> FakeTcpSocket::accept(IP_Port *addr)
return client;
}
int FakeTcpSocket::send(const uint8_t *buf, size_t len)
int FakeTcpSocket::send(const uint8_t *_Nonnull buf, size_t len)
{
std::lock_guard<std::mutex> lock(mutex_);
if (state_ != ESTABLISHED) {
errno = ENOTCONN;
if (universe_.is_verbose()) {
std::cerr << "[FakeTcpSocket] send failed: state " << state_ << " port " << local_port_
<< std::endl;
}
if (state_ == SYN_SENT || state_ == SYN_RECEIVED) {
errno = EWOULDBLOCK;
} else {
errno = ENOTCONN;
}
return -1;
}
@@ -357,7 +403,7 @@ int FakeTcpSocket::send(const uint8_t *buf, size_t len)
return len;
}
int FakeTcpSocket::recv(uint8_t *buf, size_t len)
int FakeTcpSocket::recv(uint8_t *_Nonnull buf, size_t len)
{
std::lock_guard<std::mutex> lock(mutex_);
if (recv_buffer_.empty()) {
@@ -368,6 +414,13 @@ int FakeTcpSocket::recv(uint8_t *buf, size_t len)
}
size_t actual = std::min(len, recv_buffer_.size());
if (universe_.is_verbose() && actual > 0) {
char remote_ip_str[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&remote_addr_.ip, remote_ip_str, sizeof(remote_ip_str));
std::cerr << "[FakeTcpSocket] Port " << local_port_ << " (Peer: " << remote_ip_str << ":"
<< net_ntohs(remote_addr_.port) << ") recv requested " << len << " got " << actual
<< " (remaining " << recv_buffer_.size() - actual << ")" << std::endl;
}
for (size_t i = 0; i < actual; ++i) {
buf[i] = recv_buffer_.front();
recv_buffer_.pop_front();
@@ -381,37 +434,109 @@ size_t FakeTcpSocket::recv_buffer_size()
return recv_buffer_.size();
}
int FakeTcpSocket::sendto(const uint8_t *buf, size_t len, const IP_Port *addr)
bool FakeTcpSocket::is_readable()
{
std::lock_guard<std::mutex> lock(mutex_);
if (state_ == LISTEN) {
return std::any_of(pending_connections_.begin(), pending_connections_.end(),
[](const std::unique_ptr<FakeTcpSocket> &s) { return s->state() == ESTABLISHED; });
}
return !recv_buffer_.empty() || state_ == CLOSED || state_ == CLOSE_WAIT;
}
bool FakeTcpSocket::is_writable()
{
std::lock_guard<std::mutex> lock(mutex_);
return state_ == ESTABLISHED;
}
int FakeTcpSocket::sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr)
{
errno = EOPNOTSUPP;
return -1;
}
int FakeTcpSocket::recvfrom(uint8_t *buf, size_t len, IP_Port *addr)
int FakeTcpSocket::recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr)
{
errno = EOPNOTSUPP;
return -1;
}
void FakeTcpSocket::handle_packet(const Packet &p)
int FakeTcpSocket::getsockopt(
int level, int optname, void *_Nonnull optval, size_t *_Nonnull optlen)
{
if (universe_.is_verbose()) {
std::cerr << "[FakeTcpSocket] getsockopt level=" << level << " optname=" << optname
<< " state=" << state_ << std::endl;
}
if (level == SOL_SOCKET && optname == SO_ERROR) {
int error = 0;
if (state_ == SYN_SENT || state_ == SYN_RECEIVED) {
error = EINPROGRESS;
} else if (state_ == CLOSED) {
error = ECONNREFUSED;
}
if (*optlen >= sizeof(int)) {
*static_cast<int *>(optval) = error;
*optlen = sizeof(int);
}
if (universe_.is_verbose()) {
std::cerr << "[FakeTcpSocket] getsockopt SO_ERROR returning error=" << error
<< std::endl;
}
return 0;
}
return 0;
}
bool FakeTcpSocket::handle_packet(const Packet &p)
{
std::lock_guard<std::mutex> lock(mutex_);
if (universe_.is_verbose()) {
std::cerr << "Handle Packet: Port " << local_port_ << " Flags "
<< static_cast<int>(p.tcp_flags) << " State " << state_ << std::endl;
char remote_ip_str[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&remote_addr_.ip, remote_ip_str, sizeof(remote_ip_str));
std::cerr << "Handle Packet: Port " << local_port_ << " (Peer: " << remote_ip_str << ":"
<< net_ntohs(remote_addr_.port) << ") Flags " << TcpFlags{p.tcp_flags}
<< " State " << state_ << " From " << net_ntohs(p.from.port) << std::endl;
}
if (state_ != LISTEN) {
// Filter packets not from our peer
bool port_match = net_ntohs(p.from.port) == net_ntohs(remote_addr_.port);
bool ip_match = ip_equal(&p.from.ip, &remote_addr_.ip)
|| (is_loopback(p.from.ip) && ip_equal(&remote_addr_.ip, &ip_))
|| (is_loopback(remote_addr_.ip) && ip_equal(&p.from.ip, &ip_));
if (!port_match || !ip_match) {
return false;
}
if (p.tcp_flags & 0x04) { // RST
state_ = CLOSED;
if (local_port_ != 0) {
universe_.unbind_tcp(ip_, local_port_, this);
local_port_ = 0;
}
return true;
}
}
if (state_ == LISTEN) {
if (p.tcp_flags & 0x02) { // SYN
// Check for duplicate SYN from same peer
for (const auto &pending : pending_connections_) {
if (ipport_equal(&p.from, &pending->remote_addr_)) {
return true;
}
}
// Create new socket for connection
auto new_sock = std::make_unique<FakeTcpSocket>(universe_);
// Bind to ephemeral? No, it's accepted on the same port but distinct 4-tuple.
// In our simplified model, the new socket is not bound to the global map
// until accepted? Or effectively bound to the 4-tuple.
// For now, let's just create it and queue it.
new_sock->state_ = SYN_RECEIVED;
new_sock->remote_addr_ = p.from;
new_sock->local_port_ = local_port_;
new_sock->set_ip(ip_); // Inherit IP from listening socket
new_sock->last_ack_ = p.seq + 1;
new_sock->next_seq_ = 1000; // Random ISN
@@ -428,13 +553,9 @@ void FakeTcpSocket::handle_packet(const Packet &p)
universe_.send_packet(resp);
// In real TCP, we wait for ACK to move to ESTABLISHED and accept queue.
// Here we cheat and move to ESTABLISHED immediately or wait for ACK?
// Let's wait for ACK.
// But where do we store this half-open socket?
// For simplicity: auto-transition to ESTABLISHED and queue it.
new_sock->state_ = ESTABLISHED;
// Add to pending, but it's still SYN_RECEIVED
pending_connections_.push_back(std::move(new_sock));
return true;
}
} else if (state_ == SYN_SENT) {
if ((p.tcp_flags & 0x12) == 0x12) { // SYN | ACK
@@ -451,8 +572,31 @@ void FakeTcpSocket::handle_packet(const Packet &p)
ack.seq = next_seq_;
ack.ack = last_ack_;
universe_.send_packet(ack);
return true;
} else if (p.tcp_flags & 0x02) { // SYN (Simultaneous Open)
state_ = SYN_RECEIVED;
last_ack_ = p.seq + 1;
// Send SYN-ACK
Packet resp{};
resp.from = p.to;
resp.to = p.from;
resp.is_tcp = true;
resp.tcp_flags = 0x12; // SYN | ACK
resp.seq = next_seq_++;
resp.ack = last_ack_;
universe_.send_packet(resp);
return true;
}
} else if (state_ == ESTABLISHED) {
} else if (state_ == SYN_RECEIVED) {
if (p.tcp_flags & 0x10) { // ACK
state_ = ESTABLISHED;
} else {
return false;
}
}
if (state_ == ESTABLISHED) {
if (p.tcp_flags & 0x01) { // FIN
state_ = CLOSE_WAIT;
// Send ACK
@@ -464,12 +608,23 @@ void FakeTcpSocket::handle_packet(const Packet &p)
ack.seq = next_seq_;
ack.ack = p.seq + 1; // Consume FIN
universe_.send_packet(ack);
} else if (!p.data.empty()) {
recv_buffer_.insert(recv_buffer_.end(), p.data.begin(), p.data.end());
last_ack_ += p.data.size();
// Should send ACK?
return true;
} else {
if (!p.data.empty()) {
if (universe_.is_verbose()) {
char remote_ip_str[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&remote_addr_.ip, remote_ip_str, sizeof(remote_ip_str));
std::cerr << "[FakeTcpSocket] Port " << local_port_
<< " (Peer: " << remote_ip_str << ":" << net_ntohs(remote_addr_.port)
<< ") adding " << p.data.size() << " bytes to buffer (currently "
<< recv_buffer_.size() << ")" << std::endl;
}
recv_buffer_.insert(recv_buffer_.end(), p.data.begin(), p.data.end());
}
return true;
}
}
return false;
}
std::unique_ptr<FakeTcpSocket> FakeTcpSocket::create_connected(
@@ -482,4 +637,23 @@ std::unique_ptr<FakeTcpSocket> FakeTcpSocket::create_connected(
return s;
}
std::ostream &operator<<(std::ostream &os, FakeTcpSocket::State state)
{
switch (state) {
case FakeTcpSocket::CLOSED:
return os << "CLOSED";
case FakeTcpSocket::LISTEN:
return os << "LISTEN";
case FakeTcpSocket::SYN_SENT:
return os << "SYN_SENT";
case FakeTcpSocket::SYN_RECEIVED:
return os << "SYN_RECEIVED";
case FakeTcpSocket::ESTABLISHED:
return os << "ESTABLISHED";
case FakeTcpSocket::CLOSE_WAIT:
return os << "CLOSE_WAIT";
}
return os << "UNKNOWN(" << static_cast<int>(state) << ")";
}
} // namespace tox::test

View File

@@ -18,6 +18,7 @@
#include <sys/socket.h>
#endif
#include "../../../toxcore/attributes.h"
#include "../../../toxcore/network.h"
namespace tox::test {
@@ -33,21 +34,23 @@ public:
FakeSocket(NetworkUniverse &universe, int type);
virtual ~FakeSocket();
virtual int bind(const IP_Port *addr) = 0;
virtual int connect(const IP_Port *addr) = 0;
virtual int bind(const IP_Port *_Nonnull addr) = 0;
virtual int connect(const IP_Port *_Nonnull addr) = 0;
virtual int listen(int backlog) = 0;
virtual std::unique_ptr<FakeSocket> accept(IP_Port *addr) = 0;
virtual std::unique_ptr<FakeSocket> accept(IP_Port *_Nullable addr) = 0;
virtual int send(const uint8_t *buf, size_t len) = 0;
virtual int recv(uint8_t *buf, size_t len) = 0;
virtual int send(const uint8_t *_Nonnull buf, size_t len) = 0;
virtual int recv(uint8_t *_Nonnull buf, size_t len) = 0;
virtual size_t recv_buffer_size() { return 0; }
virtual bool is_readable() { return recv_buffer_size() > 0; }
virtual bool is_writable() { return true; }
virtual int sendto(const uint8_t *buf, size_t len, const IP_Port *addr) = 0;
virtual int recvfrom(uint8_t *buf, size_t len, IP_Port *addr) = 0;
virtual int sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr) = 0;
virtual int recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr) = 0;
virtual int getsockopt(int level, int optname, void *optval, size_t *optlen);
virtual int setsockopt(int level, int optname, const void *optval, size_t optlen);
virtual int getsockopt(int level, int optname, void *_Nonnull optval, size_t *_Nonnull optlen);
virtual int setsockopt(int level, int optname, const void *_Nonnull optval, size_t optlen);
virtual int socket_nonblock(bool nonblock);
virtual int close();
@@ -76,17 +79,18 @@ public:
explicit FakeUdpSocket(NetworkUniverse &universe);
~FakeUdpSocket() override;
int bind(const IP_Port *addr) override;
int connect(const IP_Port *addr) override;
int bind(const IP_Port *_Nonnull addr) override;
int connect(const IP_Port *_Nonnull addr) override;
int listen(int backlog) override;
std::unique_ptr<FakeSocket> accept(IP_Port *addr) override;
std::unique_ptr<FakeSocket> accept(IP_Port *_Nullable addr) override;
int close() override;
int send(const uint8_t *buf, size_t len) override;
int recv(uint8_t *buf, size_t len) override;
int send(const uint8_t *_Nonnull buf, size_t len) override;
int recv(uint8_t *_Nonnull buf, size_t len) override;
size_t recv_buffer_size() override;
int sendto(const uint8_t *buf, size_t len, const IP_Port *addr) override;
int recvfrom(uint8_t *buf, size_t len, IP_Port *addr) override;
int sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr) override;
int recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr) override;
// Called by Universe to deliver a packet
void push_packet(std::vector<uint8_t> data, IP_Port from);
@@ -124,21 +128,25 @@ public:
explicit FakeTcpSocket(NetworkUniverse &universe);
~FakeTcpSocket() override;
int bind(const IP_Port *addr) override;
int connect(const IP_Port *addr) override;
int bind(const IP_Port *_Nonnull addr) override;
int connect(const IP_Port *_Nonnull addr) override;
int listen(int backlog) override;
std::unique_ptr<FakeSocket> accept(IP_Port *addr) override;
std::unique_ptr<FakeSocket> accept(IP_Port *_Nullable addr) override;
int close() override;
int send(const uint8_t *buf, size_t len) override;
int recv(uint8_t *buf, size_t len) override;
int send(const uint8_t *_Nonnull buf, size_t len) override;
int recv(uint8_t *_Nonnull buf, size_t len) override;
size_t recv_buffer_size() override;
bool is_readable() override;
bool is_writable() override;
int sendto(const uint8_t *buf, size_t len, const IP_Port *addr) override;
int recvfrom(uint8_t *buf, size_t len, IP_Port *addr) override;
int sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr) override;
int recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr) override;
int getsockopt(int level, int optname, void *_Nonnull optval, size_t *_Nonnull optlen) override;
// Internal events
void handle_packet(const Packet &p);
bool handle_packet(const Packet &p);
State state() const { return state_; }
const IP_Port &remote_addr() const { return remote_addr_; }
@@ -160,6 +168,8 @@ private:
uint32_t last_ack_ = 0;
};
std::ostream &operator<<(std::ostream &os, FakeTcpSocket::State state);
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_SOCKETS_H

View File

@@ -82,6 +82,46 @@ namespace {
EXPECT_EQ(std::string(reinterpret_cast<char *>(recv_buf), 5), "Hello");
}
TEST_F(FakeTcpSocketTest, RecvBuffering)
{
IP_Port server_addr;
ip_init(&server_addr.ip, false);
server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
server_addr.port = net_htons(8082);
server.bind(&server_addr);
server.listen(5);
client.connect(&server_addr);
universe.process_events(0); // SYN
universe.process_events(0); // SYN-ACK
universe.process_events(0); // ACK
auto accepted = server.accept(nullptr);
ASSERT_NE(accepted, nullptr);
uint8_t msg1[] = "Part1";
uint8_t msg2[] = "Part2";
client.send(msg1, 5);
client.send(msg2, 5);
universe.process_events(0); // Deliver Part1
universe.process_events(0); // Deliver Part2
EXPECT_EQ(accepted->recv_buffer_size(), 10);
uint8_t recv_buf[20];
// Read partial
ASSERT_EQ(accepted->recv(recv_buf, 3), 3);
EXPECT_EQ(std::string(reinterpret_cast<char *>(recv_buf), 3), "Par");
EXPECT_EQ(accepted->recv_buffer_size(), 7);
// Read rest
ASSERT_EQ(accepted->recv(recv_buf, 7), 7);
EXPECT_EQ(std::string(reinterpret_cast<char *>(recv_buf), 7), "t1Part2");
EXPECT_EQ(accepted->recv_buffer_size(), 0);
}
class FakeUdpSocketTest : public ::testing::Test {
public:
~FakeUdpSocketTest() override;
@@ -119,6 +159,40 @@ namespace {
EXPECT_EQ(sender_addr.port, net_htons(client.local_port()));
}
TEST_F(FakeUdpSocketTest, RecvBuffering)
{
IP_Port server_addr;
ip_init(&server_addr.ip, false);
server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
server_addr.port = net_htons(9001);
server.bind(&server_addr);
const char *msg1 = "Msg1";
const char *msg2 = "Msg2";
client.sendto(reinterpret_cast<const uint8_t *>(msg1), strlen(msg1), &server_addr);
client.sendto(reinterpret_cast<const uint8_t *>(msg2), strlen(msg2), &server_addr);
universe.process_events(0); // Deliver msg1
universe.process_events(0); // Deliver msg2
EXPECT_EQ(server.recv_buffer_size(), 2);
IP_Port sender;
uint8_t buf[10];
int len = server.recvfrom(buf, sizeof(buf), &sender);
ASSERT_EQ(len, 4);
EXPECT_EQ(std::string(reinterpret_cast<char *>(buf), len), "Msg1");
EXPECT_EQ(server.recv_buffer_size(), 1);
len = server.recvfrom(buf, sizeof(buf), &sender);
ASSERT_EQ(len, 4);
EXPECT_EQ(std::string(reinterpret_cast<char *>(buf), len), "Msg2");
EXPECT_EQ(server.recv_buffer_size(), 0);
}
} // namespace
} // namespace tox::test

View File

@@ -7,6 +7,30 @@
namespace tox::test {
std::ostream &operator<<(std::ostream &os, TcpFlags flags)
{
bool first = true;
if (flags.value & 0x02) {
os << (first ? "" : "|") << "SYN";
first = false;
}
if (flags.value & 0x10) {
os << (first ? "" : "|") << "ACK";
first = false;
}
if (flags.value & 0x01) {
os << (first ? "" : "|") << "FIN";
first = false;
}
if (flags.value & 0x04) {
os << (first ? "" : "|") << "RST";
first = false;
}
if (first)
os << "NONE";
return os << "(" << static_cast<int>(flags.value) << ")";
}
bool NetworkUniverse::IP_Port_Key::operator<(const IP_Port_Key &other) const
{
if (port != other.port)
@@ -24,7 +48,7 @@ bool NetworkUniverse::IP_Port_Key::operator<(const IP_Port_Key &other) const
NetworkUniverse::NetworkUniverse() { }
NetworkUniverse::~NetworkUniverse() { }
bool NetworkUniverse::bind_udp(IP ip, uint16_t port, FakeUdpSocket *socket)
bool NetworkUniverse::bind_udp(IP ip, uint16_t port, FakeUdpSocket *_Nonnull socket)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
IP_Port_Key key{ip, port};
@@ -40,14 +64,14 @@ void NetworkUniverse::unbind_udp(IP ip, uint16_t port)
udp_bindings_.erase({ip, port});
}
bool NetworkUniverse::bind_tcp(IP ip, uint16_t port, FakeTcpSocket *socket)
bool NetworkUniverse::bind_tcp(IP ip, uint16_t port, FakeTcpSocket *_Nonnull socket)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
tcp_bindings_.insert({{ip, port}, socket});
return true;
}
void NetworkUniverse::unbind_tcp(IP ip, uint16_t port, FakeTcpSocket *socket)
void NetworkUniverse::unbind_tcp(IP ip, uint16_t port, FakeTcpSocket *_Nonnull socket)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
auto range = tcp_bindings_.equal_range({ip, port});
@@ -75,9 +99,64 @@ void NetworkUniverse::send_packet(Packet p)
p.delivery_time += global_latency_ms_;
std::lock_guard<std::recursive_mutex> lock(mutex_);
p.sequence_number = next_packet_id_++;
if (verbose_) {
Ip_Ntoa from_str, to_str;
net_ip_ntoa(&p.from.ip, &from_str);
net_ip_ntoa(&p.to.ip, &to_str);
std::cerr << "[NetworkUniverse] Enqueued packet #" << p.sequence_number << " from "
<< from_str.buf << ":" << net_ntohs(p.from.port) << " to " << to_str.buf << ":"
<< net_ntohs(p.to.port);
if (p.is_tcp) {
std::cerr << " (TCP Flags=" << TcpFlags{p.tcp_flags} << " Seq=" << p.seq
<< " Ack=" << p.ack << ")";
}
std::cerr << " with size " << p.data.size() << std::endl;
}
event_queue_.push(std::move(p));
}
static bool is_ipv4_mapped(const IP &ip)
{
if (!net_family_is_ipv6(ip.family))
return false;
const uint8_t *b = ip.ip.v6.uint8;
for (int i = 0; i < 10; ++i)
if (b[i] != 0)
return false;
if (b[10] != 0xFF || b[11] != 0xFF)
return false;
return true;
}
static IP extract_ipv4(const IP &ip)
{
IP ip4;
ip_init(&ip4, false);
const uint8_t *b = ip.ip.v6.uint8;
std::memcpy(ip4.ip.v4.uint8, b + 12, 4);
return ip4;
}
bool is_loopback(const IP &ip)
{
if (net_family_is_ipv4(ip.family)) {
return ip.ip.v4.uint32 == net_htonl(0x7F000001);
}
if (net_family_is_ipv6(ip.family)) {
const uint8_t *b = ip.ip.v6.uint8;
for (int i = 0; i < 15; ++i) {
if (b[i] != 0) {
return false;
}
}
return b[15] == 1;
}
return false;
}
void NetworkUniverse::process_events(uint64_t current_time_ms)
{
while (true) {
@@ -88,19 +167,101 @@ void NetworkUniverse::process_events(uint64_t current_time_ms)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
if (!event_queue_.empty()) {
const Packet &top = event_queue_.top();
if (verbose_) {
std::cerr << "[NetworkUniverse] Peek packet: time=" << top.delivery_time
<< " current=" << current_time_ms << " tcp=" << top.is_tcp
<< std::endl;
}
}
if (!event_queue_.empty() && event_queue_.top().delivery_time <= current_time_ms) {
p = event_queue_.top();
event_queue_.pop();
has_packet = true;
if (verbose_) {
Ip_Ntoa from_str, to_str;
net_ip_ntoa(&p.from.ip, &from_str);
net_ip_ntoa(&p.to.ip, &to_str);
std::cerr << "[NetworkUniverse] Processing packet #" << p.sequence_number
<< " from " << from_str.buf << ":" << net_ntohs(p.from.port) << " to "
<< to_str.buf << ":" << net_ntohs(p.to.port)
<< " (TCP=" << (p.is_tcp ? "true" : "false");
if (p.is_tcp) {
std::cerr << " Flags=" << TcpFlags{p.tcp_flags} << " Seq=" << p.seq
<< " Ack=" << p.ack;
}
std::cerr << " Size=" << p.data.size() << ")" << std::endl;
}
IP target_ip = p.to.ip;
if (p.is_tcp) {
auto range = tcp_bindings_.equal_range({p.to.ip, net_ntohs(p.to.port)});
if (is_loopback(target_ip)
&& tcp_bindings_.count({target_ip, net_ntohs(p.to.port)}) == 0) {
if (verbose_) {
std::cerr << "[NetworkUniverse] Loopback packet to "
<< static_cast<int>(target_ip.ip.v4.uint8[3])
<< " redirected to "
<< static_cast<int>(p.from.ip.ip.v4.uint8[3]) << std::endl;
}
target_ip = p.from.ip;
}
auto range = tcp_bindings_.equal_range({target_ip, net_ntohs(p.to.port)});
FakeTcpSocket *listen_match = nullptr;
for (auto it = range.first; it != range.second; ++it) {
tcp_targets.push_back(it->second);
FakeTcpSocket *s = it->second;
if (s->state() == FakeTcpSocket::LISTEN) {
listen_match = s;
} else {
const IP_Port &remote = s->remote_addr();
if (net_ntohs(p.from.port) == net_ntohs(remote.port)) {
if (ip_equal(&p.from.ip, &remote.ip)
|| (is_loopback(p.from.ip) && ip_equal(&remote.ip, &target_ip))
|| (is_loopback(remote.ip)
&& ip_equal(&p.from.ip, &target_ip))) {
tcp_targets.push_back(s);
}
}
}
}
if (listen_match && (p.tcp_flags & 0x02)) {
tcp_targets.push_back(listen_match);
}
if (verbose_) {
std::cerr << "[NetworkUniverse] Routing TCP to "
<< static_cast<int>(target_ip.ip.v4.uint8[0]) << "."
<< static_cast<int>(target_ip.ip.v4.uint8[3]) << ":"
<< net_ntohs(p.to.port)
<< ". Targets found: " << tcp_targets.size() << std::endl;
}
if (tcp_targets.empty()) {
if (verbose_) {
std::cerr << "[NetworkUniverse] WARNING: No TCP targets for "
<< static_cast<int>(target_ip.ip.v4.uint8[0]) << "."
<< static_cast<int>(target_ip.ip.v4.uint8[3]) << ":"
<< net_ntohs(p.to.port) << std::endl;
}
}
} else {
if (udp_bindings_.count({p.to.ip, net_ntohs(p.to.port)})) {
udp_target = udp_bindings_[{p.to.ip, net_ntohs(p.to.port)}];
if (is_loopback(target_ip)
&& udp_bindings_.count({target_ip, net_ntohs(p.to.port)}) == 0) {
target_ip = p.from.ip;
}
if (udp_bindings_.count({target_ip, net_ntohs(p.to.port)})) {
udp_target = udp_bindings_[{target_ip, net_ntohs(p.to.port)}];
} else if (is_ipv4_mapped(target_ip)) {
IP ip4 = extract_ipv4(target_ip);
if (udp_bindings_.count({ip4, net_ntohs(p.to.port)})) {
udp_target = udp_bindings_[{ip4, net_ntohs(p.to.port)}];
}
}
}
}
@@ -112,7 +273,9 @@ void NetworkUniverse::process_events(uint64_t current_time_ms)
if (p.is_tcp) {
for (auto *it : tcp_targets) {
it->handle_packet(p);
if (it->handle_packet(p)) {
break;
}
}
} else {
if (udp_target) {
@@ -136,7 +299,7 @@ uint16_t NetworkUniverse::find_free_port(IP ip, uint16_t start)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
for (uint16_t port = start; port < 65535; ++port) {
if (!udp_bindings_.count({ip, port}))
if (!udp_bindings_.count({ip, port}) && !tcp_bindings_.count({ip, port}))
return port;
}
return 0;

View File

@@ -9,6 +9,7 @@
#include <queue>
#include <vector>
#include "../../../toxcore/attributes.h"
#include "../../../toxcore/network.h"
namespace tox::test {
@@ -16,11 +17,17 @@ namespace tox::test {
class FakeUdpSocket;
class FakeTcpSocket;
struct TcpFlags {
uint8_t value;
};
std::ostream &operator<<(std::ostream &os, TcpFlags flags);
struct Packet {
IP_Port from;
IP_Port to;
std::vector<uint8_t> data;
uint64_t delivery_time;
uint64_t sequence_number = 0;
bool is_tcp = false;
// TCP Simulation Fields
@@ -28,9 +35,17 @@ struct Packet {
uint32_t seq = 0;
uint32_t ack = 0;
bool operator>(const Packet &other) const { return delivery_time > other.delivery_time; }
bool operator>(const Packet &other) const
{
if (delivery_time != other.delivery_time) {
return delivery_time > other.delivery_time;
}
return sequence_number > other.sequence_number;
}
};
bool is_loopback(const IP &ip);
/**
* @brief The God Object for the network simulation.
* Manages routing, latency, and connectivity.
@@ -45,11 +60,11 @@ public:
// Registration
// Returns true if binding succeeded
bool bind_udp(IP ip, uint16_t port, FakeUdpSocket *socket);
bool bind_udp(IP ip, uint16_t port, FakeUdpSocket *_Nonnull socket);
void unbind_udp(IP ip, uint16_t port);
bool bind_tcp(IP ip, uint16_t port, FakeTcpSocket *socket);
void unbind_tcp(IP ip, uint16_t port, FakeTcpSocket *socket);
bool bind_tcp(IP ip, uint16_t port, FakeTcpSocket *_Nonnull socket);
void unbind_tcp(IP ip, uint16_t port, FakeTcpSocket *_Nonnull socket);
// Routing
void send_packet(Packet p);
@@ -81,6 +96,7 @@ private:
std::vector<PacketSink> observers_;
uint64_t global_latency_ms_ = 0;
uint64_t next_packet_id_ = 0;
bool verbose_ = false;
std::recursive_mutex mutex_;
};

View File

@@ -236,5 +236,86 @@ namespace {
std::string(reinterpret_cast<char *>(buf), static_cast<size_t>(len)), "Padding test");
}
TEST_F(NetworkUniverseTest, TcpRoutingSpecificity)
{
IP ip1{};
ip_init(&ip1, false);
ip1.ip.v4.uint32 = net_htonl(0x0A000001); // 10.0.0.1
uint16_t port = 12345;
IP_Port local_addr{ip1, net_htons(port)};
FakeTcpSocket listen_sock(universe);
listen_sock.set_ip(ip1);
listen_sock.bind(&local_addr);
listen_sock.listen(5);
IP remote_ip{};
ip_init(&remote_ip, false);
remote_ip.ip.v4.uint32 = net_htonl(0x0A000002); // 10.0.0.2
IP_Port remote_addr{remote_ip, net_htons(33445)};
auto established_sock = FakeTcpSocket::create_connected(universe, remote_addr, port);
established_sock->set_ip(ip1);
universe.bind_tcp(ip1, port, established_sock.get());
// Send a data packet from remote to local
Packet p{};
p.from = remote_addr;
p.to = local_addr;
p.is_tcp = true;
p.tcp_flags = 0x10; // ACK (Data)
const char *data = "Specific";
p.data.assign(data, data + strlen(data));
universe.send_packet(p);
universe.process_events(0);
// established_sock should have received it
EXPECT_EQ(established_sock->recv_buffer_size(), strlen(data));
// listen_sock should NOT have received it (it doesn't have a buffer, but it shouldn't have
// been called) We can't easily check listen_sock wasn't called without mocks or checking
// logs, but we can check that it didn't create a new pending connection.
EXPECT_FALSE(listen_sock.is_readable());
}
TEST_F(NetworkUniverseTest, PacketOrdering)
{
IP ip1{}, ip2{};
ip_init(&ip1, false);
ip1.ip.v4.uint32 = net_htonl(0x0A000001);
ip_init(&ip2, false);
ip2.ip.v4.uint32 = net_htonl(0x0A000002);
uint16_t port = 33445;
IP_Port addr1{ip1, net_htons(port)};
IP_Port addr2{ip2, net_htons(port)};
FakeUdpSocket sock1{universe};
sock1.set_ip(ip1);
sock1.bind(&addr1);
FakeUdpSocket sock2{universe};
sock2.set_ip(ip2);
sock2.bind(&addr2);
// Send 10 packets with the same delivery time (global latency = 0)
for (int i = 0; i < 10; ++i) {
uint8_t data = static_cast<uint8_t>(i);
sock1.sendto(&data, 1, &addr2);
}
universe.process_events(0);
// They should be received in the exact order they were sent
for (int i = 0; i < 10; ++i) {
uint8_t buf[1];
IP_Port from;
ASSERT_EQ(sock2.recvfrom(buf, 1, &from), 1);
EXPECT_EQ(buf[0], i) << "Packet " << i << " was delivered out of order";
}
}
} // namespace
} // namespace tox::test

View File

@@ -7,6 +7,8 @@
#include <cstring>
#include <vector>
#include "../../../toxcore/attributes.h"
namespace tox::test {
struct Fuzz_Data {
@@ -14,12 +16,12 @@ struct Fuzz_Data {
static constexpr std::size_t TRACE_TRAP = -1;
private:
const uint8_t *data_;
const uint8_t *base_;
const uint8_t *_Nonnull data_;
const uint8_t *_Nonnull base_;
std::size_t size_;
public:
Fuzz_Data(const uint8_t *input_data, std::size_t input_size)
Fuzz_Data(const uint8_t *_Nonnull input_data, std::size_t input_size)
: data_(input_data)
, base_(input_data)
, size_(input_size)
@@ -30,7 +32,7 @@ public:
Fuzz_Data(const Fuzz_Data &rhs) = delete;
struct Consumer {
const char *func;
const char *_Nonnull func;
Fuzz_Data &fd;
operator bool()
@@ -51,14 +53,14 @@ public:
{
if (sizeof(T) > fd.size())
return T{};
const uint8_t *bytes = fd.consume(func, sizeof(T));
const uint8_t *_Nonnull bytes = fd.consume(func, sizeof(T));
T val;
std::memcpy(&val, bytes, sizeof(T));
return val;
}
};
Consumer consume1(const char *func) { return Consumer{func, *this}; }
Consumer consume1(const char *_Nonnull func) { return Consumer{func, *this}; }
template <typename T>
T consume_integral()
@@ -81,7 +83,7 @@ public:
{
if (count == 0 || count > size_)
return {};
const uint8_t *start = consume("consume_bytes", count);
const uint8_t *_Nullable start = consume("consume_bytes", count);
if (!start)
return {};
return std::vector<uint8_t>(start, start + count);
@@ -92,20 +94,20 @@ public:
if (empty())
return {};
std::size_t count = size();
const uint8_t *start = consume("consume_remaining_bytes", count);
const uint8_t *_Nonnull start = consume("consume_remaining_bytes", count);
return std::vector<uint8_t>(start, start + count);
}
std::size_t size() const { return size_; }
std::size_t pos() const { return data_ - base_; }
const uint8_t *data() const { return data_; }
const uint8_t *_Nonnull data() const { return data_; }
bool empty() const { return size_ == 0; }
const uint8_t *consume(const char *func, std::size_t count)
const uint8_t *_Nullable consume(const char *_Nonnull func, std::size_t count)
{
if (count > size_)
return nullptr;
const uint8_t *val = data_;
const uint8_t *_Nonnull val = data_;
if (FUZZ_DEBUG) {
if (count == 1) {
std::printf("consume@%zu(%s): %d (0x%02x)\n", pos(), func, val[0], val[0]);
@@ -170,7 +172,7 @@ struct Fuzz_Target_Selector<> {
};
template <Fuzz_Target... Args>
void fuzz_select_target(const uint8_t *data, std::size_t size)
void fuzz_select_target(const uint8_t *_Nonnull data, std::size_t size)
{
Fuzz_Data input{data, size};

View File

@@ -4,6 +4,11 @@
#include <cstddef>
#include <cstdint>
#include "../../../toxcore/attributes.h"
// Forward declaration
struct Memory;
namespace tox::test {
/**
@@ -13,9 +18,14 @@ class MemorySystem {
public:
virtual ~MemorySystem();
virtual void *malloc(size_t size) = 0;
virtual void *realloc(void *ptr, size_t size) = 0;
virtual void free(void *ptr) = 0;
virtual void *_Nullable malloc(size_t size) = 0;
virtual void *_Nullable realloc(void *_Nullable ptr, size_t size) = 0;
virtual void free(void *_Nullable ptr) = 0;
/**
* @brief Returns C-compatible Memory struct.
*/
virtual struct Memory c_memory() = 0;
};
} // namespace tox::test

View File

@@ -0,0 +1,83 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2026 The TokTok team.
*/
#ifndef C_TOXCORE_TESTING_SUPPORT_MPSC_QUEUE_H
#define C_TOXCORE_TESTING_SUPPORT_MPSC_QUEUE_H
#include <condition_variable>
#include <deque>
#include <mutex>
namespace tox::test {
/**
* @brief Multiple Producer, Single Consumer Queue.
*
* This queue implementation provides thread-safe access for multiple producers
* pushing items and a single consumer popping items. It uses a `std::mutex`
* and `std::condition_variable` for synchronization.
*
* @tparam T The type of elements stored in the queue.
*/
template <typename T>
class MpscQueue {
public:
MpscQueue() = default;
~MpscQueue() = default;
// Disable copy/move to prevent accidental sharing/slicing issues
MpscQueue(const MpscQueue &) = delete;
MpscQueue &operator=(const MpscQueue &) = delete;
/**
* @brief Pushes a value onto the queue.
* Thread-safe (Multiple Producers).
*/
void push(T value)
{
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push_back(std::move(value));
}
cv_.notify_one();
}
/**
* @brief Pops a value from the queue, blocking if empty.
* Thread-safe (Single Consumer).
*/
T pop()
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return !queue_.empty(); });
T value = std::move(queue_.front());
queue_.pop_front();
return value;
}
/**
* @brief Tries to pop a value from the queue without blocking.
* Thread-safe (Single Consumer).
*
* @param out Reference to store the popped value.
* @return true if a value was popped, false if the queue was empty.
*/
bool try_pop(T &out)
{
std::lock_guard<std::mutex> lock(mutex_);
if (queue_.empty())
return false;
out = std::move(queue_.front());
queue_.pop_front();
return true;
}
private:
std::deque<T> queue_;
std::mutex mutex_;
std::condition_variable cv_;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_MPSC_QUEUE_H

View File

@@ -4,6 +4,8 @@
#include <cstdint>
#include <vector>
#include "../../../toxcore/attributes.h"
#include "../../../toxcore/net.h"
#include "../../../toxcore/network.h"
namespace tox::test {
@@ -16,24 +18,35 @@ public:
virtual ~NetworkSystem();
virtual Socket socket(int domain, int type, int protocol) = 0;
virtual int bind(Socket sock, const IP_Port *addr) = 0;
virtual int bind(Socket sock, const IP_Port *_Nonnull addr) = 0;
virtual int close(Socket sock) = 0;
virtual int sendto(Socket sock, const uint8_t *buf, size_t len, const IP_Port *addr) = 0;
virtual int recvfrom(Socket sock, uint8_t *buf, size_t len, IP_Port *addr) = 0;
virtual int sendto(
Socket sock, const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr)
= 0;
virtual int recvfrom(Socket sock, uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr)
= 0;
// TCP Support
virtual int listen(Socket sock, int backlog) = 0;
virtual Socket accept(Socket sock) = 0;
virtual int connect(Socket sock, const IP_Port *addr) = 0;
virtual int send(Socket sock, const uint8_t *buf, size_t len) = 0;
virtual int recv(Socket sock, uint8_t *buf, size_t len) = 0;
virtual int connect(Socket sock, const IP_Port *_Nonnull addr) = 0;
virtual int send(Socket sock, const uint8_t *_Nonnull buf, size_t len) = 0;
virtual int recv(Socket sock, uint8_t *_Nonnull buf, size_t len) = 0;
virtual int recvbuf(Socket sock) = 0;
// Auxiliary
virtual int socket_nonblock(Socket sock, bool nonblock) = 0;
virtual int getsockopt(Socket sock, int level, int optname, void *optval, size_t *optlen) = 0;
virtual int setsockopt(Socket sock, int level, int optname, const void *optval, size_t optlen)
virtual int getsockopt(
Socket sock, int level, int optname, void *_Nonnull optval, size_t *_Nonnull optlen)
= 0;
virtual int setsockopt(
Socket sock, int level, int optname, const void *_Nonnull optval, size_t optlen)
= 0;
/**
* @brief Returns C-compatible Network struct.
*/
virtual struct Network c_network() = 0;
};
/**

View File

@@ -4,6 +4,11 @@
#include <cstdint>
#include <vector>
#include "../../../toxcore/attributes.h"
// Forward declaration
struct Random;
namespace tox::test {
/**
@@ -14,7 +19,12 @@ public:
virtual ~RandomSystem();
virtual uint32_t uniform(uint32_t upper_bound) = 0;
virtual void bytes(uint8_t *out, size_t count) = 0;
virtual void bytes(uint8_t *_Nonnull out, size_t count) = 0;
/**
* @brief Returns C-compatible Random struct.
*/
virtual struct Random c_random() = 0;
};
} // namespace tox::test

View File

@@ -13,9 +13,10 @@
#include <sys/socket.h>
#endif
#include "../../../toxcore/tox_memory_impl.h"
#include "../../../toxcore/attributes.h"
#include "../../../toxcore/mem.h"
#include "../../../toxcore/rng.h"
#include "../../../toxcore/tox_private.h"
#include "../../../toxcore/tox_random_impl.h"
#include "../doubles/fake_clock.hh"
#include "../doubles/fake_memory.hh"
#include "../doubles/fake_random.hh"
@@ -29,12 +30,12 @@ struct ScopedToxSystem {
std::unique_ptr<SimulatedNode> node;
// Direct access to primary socket (for fuzzer injection)
FakeUdpSocket *endpoint;
FakeUdpSocket *_Nullable endpoint;
// C structs
struct Network c_network;
struct Tox_Random c_random;
struct Tox_Memory c_memory;
struct Random c_random;
struct Memory c_memory;
// The main struct passed to tox_new
Tox_System system;

View File

@@ -1,6 +1,8 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_SIMULATION_H
#define C_TOXCORE_TESTING_SUPPORT_SIMULATION_H
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>
#include <vector>
@@ -14,10 +16,13 @@
#include <sys/socket.h>
#endif
#include <string>
#include "../../../toxcore/attributes.h"
#include "../../../toxcore/mem.h"
#include "../../../toxcore/rng.h"
#include "../../../toxcore/tox.h"
#include "../../../toxcore/tox_memory_impl.h"
#include "../../../toxcore/tox_private.h"
#include "../../../toxcore/tox_random_impl.h"
#include "../doubles/fake_clock.hh"
#include "../doubles/fake_memory.hh"
#include "../doubles/fake_network_stack.hh"
@@ -29,12 +34,61 @@ namespace tox::test {
class SimulatedNode;
struct LogMetadata {
Tox_Log_Level level;
const char *_Nonnull file;
uint32_t line;
const char *_Nonnull func;
const char *_Nonnull message;
uint32_t node_id;
};
using LogPredicate = std::function<bool(const LogMetadata &)>;
struct LogFilter {
LogPredicate pred;
LogFilter() = default;
explicit LogFilter(LogPredicate p)
: pred(std::move(p))
{
}
bool operator()(const LogMetadata &md) const { return !pred || pred(md); }
};
LogFilter operator&&(const LogFilter &lhs, const LogFilter &rhs);
LogFilter operator||(const LogFilter &lhs, const LogFilter &rhs);
LogFilter operator!(const LogFilter &target);
namespace log_filter {
LogFilter level(Tox_Log_Level min_level);
struct LevelPlaceholder {
LogFilter operator>(Tox_Log_Level rhs) const;
LogFilter operator>=(Tox_Log_Level rhs) const;
LogFilter operator<(Tox_Log_Level rhs) const;
LogFilter operator<=(Tox_Log_Level rhs) const;
LogFilter operator==(Tox_Log_Level rhs) const;
LogFilter operator!=(Tox_Log_Level rhs) const;
};
LevelPlaceholder level();
LogFilter file(std::string pattern);
LogFilter func(std::string pattern);
LogFilter message(std::string pattern);
LogFilter node(uint32_t id);
} // namespace log_filter
/**
* @brief The Simulation World.
* Holds the Clock and the Universe.
*/
class Simulation {
public:
static constexpr uint32_t kDefaultTickIntervalMs = 50;
Simulation();
~Simulation();
@@ -42,9 +96,66 @@ public:
void advance_time(uint64_t ms);
void run_until(std::function<bool()> condition, uint64_t timeout_ms = 5000);
// Logging
void set_log_filter(LogFilter filter);
const LogFilter &log_filter() const { return log_filter_; }
// Synchronization Barrier
// These methods coordinate the lock-step execution of multiple Tox runners.
/**
* @brief Registers a new runner with the simulation barrier.
* @return The current generation ID of the simulation.
*/
uint64_t register_runner();
/**
* @brief Unregisters a runner from the simulation barrier.
*
* This ensures the simulation does not block waiting for a terminated runner.
*/
void unregister_runner();
using TickListenerId = int;
/**
* @brief Registers a callback to be invoked when a new simulation tick starts.
*
* @param listener The function to call with the new generation ID.
* @return An ID handle for unregistering the listener.
*/
TickListenerId register_tick_listener(std::function<void(uint64_t)> listener);
/**
* @brief Unregisters a tick listener.
*/
void unregister_tick_listener(TickListenerId id);
/**
* @brief Blocks until the simulation advances to the next tick.
*
* Called by runner threads to wait for the global clock to advance.
*
* @param last_gen The generation ID of the last processed tick.
* @param stop_token Atomic flag to signal termination while waiting.
* @param timeout_ms Maximum time to wait for the tick.
* @return The new generation ID, or `last_gen` on timeout/stop.
*/
uint64_t wait_for_tick(
uint64_t last_gen, const std::atomic<bool> &stop_token, uint64_t timeout_ms = 10);
/**
* @brief Signals that a runner has completed its work for the current tick.
*
* @param next_delay_ms The requested delay until the next tick (from `tox_iteration_interval`).
*/
void tick_complete(uint32_t next_delay_ms = kDefaultTickIntervalMs);
// Global Access
FakeClock &clock() { return *clock_; }
const FakeClock &clock() const { return *clock_; }
NetworkUniverse &net() { return *net_; }
const NetworkUniverse &net() const { return *net_; }
// Node Factory
std::unique_ptr<SimulatedNode> create_node();
@@ -52,7 +163,23 @@ public:
private:
std::unique_ptr<FakeClock> clock_;
std::unique_ptr<NetworkUniverse> net_;
LogFilter log_filter_;
uint32_t node_count_ = 0;
// Barrier State
std::mutex barrier_mutex_;
std::condition_variable barrier_cv_;
uint64_t current_generation_ = 0;
int registered_runners_ = 0;
std::atomic<int> active_runners_{0};
std::atomic<uint32_t> next_step_min_{kDefaultTickIntervalMs};
struct TickListener {
TickListenerId id;
std::function<void(uint64_t)> callback;
};
std::vector<TickListener> tick_listeners_;
TickListenerId next_listener_id_ = 0;
};
/**
@@ -79,19 +206,16 @@ public:
// Returns a configured Tox instance bound to this node's environment.
// The user owns the Tox instance.
struct ToxDeleter {
void operator()(Tox *t) const { tox_kill(t); }
void operator()(Tox *_Nonnull t) const { tox_kill(t); }
};
using ToxPtr = std::unique_ptr<Tox, ToxDeleter>;
ToxPtr create_tox(const Tox_Options *options = nullptr);
ToxPtr create_tox(const Tox_Options *_Nullable options = nullptr);
// Helper to get C structs for manual injection
struct Network get_c_network() { return network_->get_c_network(); }
struct Tox_Random get_c_random() { return random_->get_c_random(); }
struct Tox_Memory get_c_memory() { return memory_->get_c_memory(); }
Simulation &simulation() { return sim_; }
// For fuzzing compatibility (exposes first bound UDP socket as "endpoint")
FakeUdpSocket *get_primary_socket();
FakeUdpSocket *_Nullable get_primary_socket();
private:
Simulation &sim_;
@@ -102,8 +226,8 @@ private:
// C-compatible views (must stay valid for the lifetime of Tox)
public:
struct Network c_network;
struct Tox_Random c_random;
struct Tox_Memory c_memory;
struct Random c_random;
struct Memory c_memory;
struct IP ip;
};

View File

@@ -5,21 +5,25 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_TOX_NETWORK_H
#define C_TOXCORE_TESTING_SUPPORT_TOX_NETWORK_H
#include <memory>
#include <utility>
#include <vector>
#include "../../../toxcore/attributes.h"
#include "simulation.hh"
#include "tox_runner.hh"
namespace tox::test {
struct ConnectedFriend {
std::unique_ptr<SimulatedNode> node;
SimulatedNode::ToxPtr tox;
std::unique_ptr<ToxRunner> runner;
uint32_t friend_number;
ConnectedFriend(std::unique_ptr<SimulatedNode> node_in, SimulatedNode::ToxPtr tox_in,
ConnectedFriend(std::unique_ptr<SimulatedNode> node_in, std::unique_ptr<ToxRunner> runner_in,
uint32_t friend_number_in)
: node(std::move(node_in))
, tox(std::move(tox_in))
, runner(std::move(runner_in))
, friend_number(friend_number_in)
{
}
@@ -43,8 +47,9 @@ struct ConnectedFriend {
* @param options Optional Tox_Options to use for the friend Tox instances.
* @return A vector of ConnectedFriend structures, each representing a friend.
*/
std::vector<ConnectedFriend> setup_connected_friends(Simulation &sim, Tox *main_tox,
SimulatedNode &main_node, int num_friends, const Tox_Options *options = nullptr);
std::vector<ConnectedFriend> setup_connected_friends(Simulation &sim, Tox *_Nonnull main_tox,
SimulatedNode &main_node, int num_friends, const Tox_Options *_Nullable options = nullptr,
bool verbose = false);
/**
* @brief Connects two existing Tox instances as friends.
@@ -59,8 +64,8 @@ std::vector<ConnectedFriend> setup_connected_friends(Simulation &sim, Tox *main_
* @param tox2 The second Tox instance.
* @return True if connected successfully, false otherwise.
*/
bool connect_friends(
Simulation &sim, SimulatedNode &node1, Tox *tox1, SimulatedNode &node2, Tox *tox2);
bool connect_friends(Simulation &sim, SimulatedNode &node1, Tox *_Nonnull tox1,
SimulatedNode &node2, Tox *_Nonnull tox2);
/**
* @brief Sets up a group and has all friends join it.
@@ -75,7 +80,7 @@ bool connect_friends(
* @return The group number on the main Tox instance, or UINT32_MAX on failure.
*/
uint32_t setup_connected_group(
Simulation &sim, Tox *main_tox, const std::vector<ConnectedFriend> &friends);
Simulation &sim, Tox *_Nonnull main_tox, const std::vector<ConnectedFriend> &friends);
} // namespace tox::test

View File

@@ -0,0 +1,137 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2026 The TokTok team.
*/
#ifndef C_TOXCORE_TESTING_SUPPORT_TOX_RUNNER_H
#define C_TOXCORE_TESTING_SUPPORT_TOX_RUNNER_H
#include <atomic>
#include <functional>
#include <future>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
#include "../../../toxcore/attributes.h"
#include "../../../toxcore/tox_events.h"
#include "mpsc_queue.hh"
#include "simulation.hh"
namespace tox::test {
class ToxRunner {
public:
explicit ToxRunner(SimulatedNode &node, const Tox_Options *_Nullable options = nullptr);
~ToxRunner();
ToxRunner(const ToxRunner &) = delete;
ToxRunner &operator=(const ToxRunner &) = delete;
struct ToxEventsDeleter {
void operator()(Tox_Events *_Nonnull e) const { tox_events_free(e); }
};
using ToxEventsPtr = std::unique_ptr<Tox_Events, ToxEventsDeleter>;
/**
* @brief Schedules a task for execution on the runner's thread.
*
* This method is thread-safe and non-blocking. The task is queued and will
* be executed during the runner's event loop cycle.
*
* @param task The function to execute, taking a raw Tox pointer.
*/
void execute(std::function<void(Tox *_Nonnull)> task);
/**
* @brief Executes a task on the runner's thread and waits for the result.
*
* This method blocks the calling thread until the task has been executed
* by the runner. It automatically handles return value propagation and
* exception safety (though exceptions are not currently propagated).
*
* @tparam Func The type of the callable object.
* @param func The callable to execute, taking a raw Tox pointer.
* @return The result of the callable execution.
*/
template <typename Func>
auto invoke(Func &&func) -> std::invoke_result_t<Func, Tox *_Nonnull>
{
using R = std::invoke_result_t<Func, Tox *_Nonnull>;
auto promise = std::make_shared<std::promise<R>>();
auto future = promise->get_future();
execute([p = promise, f = std::forward<Func>(func)](Tox *_Nonnull tox) {
if constexpr (std::is_void_v<R>) {
f(tox);
p->set_value();
} else {
p->set_value(f(tox));
}
});
return future.get();
}
/**
* @brief Retrieves all accumulated Tox event batches.
*
* Returns a vector of unique pointers to Tox_Events structures that have
* been collected by the runner since the last call. Ownership is transferred
* to the caller. This method is thread-safe.
*
* @return A vector of Tox_Events pointers.
*/
std::vector<ToxEventsPtr> poll_events();
/**
* @brief Accesses the underlying Tox instance directly.
*
* @warning Thread-Safety Violation: This method provides unsafe access to the
* Tox instance. It should ONLY be used when the runner thread is known to be
* idle (e.g., before the loop starts) or for accessing constant/read-only properties.
* For all other operations, use `execute` or `invoke`.
*/
Tox *_Nullable unsafe_tox() { return tox_.get(); }
/**
* @brief Temporarily stops the runner from participating in the simulation.
*
* Unregisters the runner and its tick listener from the simulation.
* While paused, the runner will not call tox_iterate.
*/
void pause();
/**
* @brief Resumes the runner's participation in the simulation.
*/
void resume();
/**
* @brief Returns true if the runner is currently active.
*/
bool is_active() const { return active_; }
private:
void loop();
SimulatedNode::ToxPtr tox_;
std::thread thread_;
std::atomic<bool> active_{true};
struct Message {
enum Type { Task, Tick, Stop } type;
std::function<void(Tox *_Nonnull)> task;
uint64_t generation = 0;
};
MpscQueue<Message> queue_;
MpscQueue<ToxEventsPtr> events_queue_;
Simulation::TickListenerId tick_listener_id_ = -1;
SimulatedNode &node_;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_TOX_RUNNER_H

View File

@@ -0,0 +1,76 @@
#include "public/simulation.hh"
#include <gtest/gtest.h>
namespace tox::test {
TEST(LogFilterTest, Operators)
{
LogMetadata md1{TOX_LOG_LEVEL_INFO, "file1.c", 10, "func1", "message1", 1};
LogMetadata md2{TOX_LOG_LEVEL_DEBUG, "file2.c", 20, "func2", "message2", 2};
auto f1 = log_filter::file("file1");
auto f2 = log_filter::level(TOX_LOG_LEVEL_INFO);
EXPECT_TRUE(f1(md1));
EXPECT_FALSE(f1(md2));
EXPECT_TRUE(f2(md1));
EXPECT_FALSE(f2(md2));
auto f_and = f1 && f2;
EXPECT_TRUE(f_and(md1));
EXPECT_FALSE(f_and(md2));
auto f_or = f1 || log_filter::file("file2");
EXPECT_TRUE(f_or(md1));
EXPECT_TRUE(f_or(md2));
auto f_not = !f1;
EXPECT_FALSE(f_not(md1));
EXPECT_TRUE(f_not(md2));
}
TEST(LogFilterTest, LevelComparison)
{
LogMetadata md_trace{TOX_LOG_LEVEL_TRACE, "file.c", 1, "func", "msg", 1};
LogMetadata md_debug{TOX_LOG_LEVEL_DEBUG, "file.c", 1, "func", "msg", 1};
LogMetadata md_info{TOX_LOG_LEVEL_INFO, "file.c", 1, "func", "msg", 1};
LogMetadata md_warn{TOX_LOG_LEVEL_WARNING, "file.c", 1, "func", "msg", 1};
LogMetadata md_error{TOX_LOG_LEVEL_ERROR, "file.c", 1, "func", "msg", 1};
// level() > DEBUG
auto f_gt = log_filter::level() > TOX_LOG_LEVEL_DEBUG;
EXPECT_FALSE(f_gt(md_trace));
EXPECT_FALSE(f_gt(md_debug));
EXPECT_TRUE(f_gt(md_info));
EXPECT_TRUE(f_gt(md_error));
// level() < INFO
auto f_lt = log_filter::level() < TOX_LOG_LEVEL_INFO;
EXPECT_TRUE(f_lt(md_trace));
EXPECT_TRUE(f_lt(md_debug));
EXPECT_FALSE(f_lt(md_info));
EXPECT_FALSE(f_lt(md_error));
// level() == WARNING
auto f_eq = log_filter::level() == TOX_LOG_LEVEL_WARNING;
EXPECT_FALSE(f_eq(md_info));
EXPECT_TRUE(f_eq(md_warn));
EXPECT_FALSE(f_eq(md_error));
}
TEST(LogFilterTest, SimulationIntegration)
{
Simulation sim;
sim.net().set_verbose(true);
sim.set_log_filter(log_filter::level(TOX_LOG_LEVEL_ERROR) && log_filter::node(1));
auto node = sim.create_node();
auto tox = node->create_tox();
SUCCEED();
}
} // namespace tox::test

View File

@@ -4,19 +4,20 @@
#include <iostream>
#include <new>
#include "../../../toxcore/tox_memory_impl.h"
#include "../../../toxcore/mem.h"
namespace tox::test {
// --- Trampolines ---
static const Tox_Memory_Funcs kFakeMemoryVtable = {
.malloc_callback
= [](void *obj, uint32_t size) { return static_cast<FakeMemory *>(obj)->malloc(size); },
static const Memory_Funcs kFakeMemoryVtable = {
.malloc_callback = [](void *_Nonnull obj,
uint32_t size) { return static_cast<FakeMemory *>(obj)->malloc(size); },
.realloc_callback
= [](void *obj, void *ptr,
= [](void *_Nonnull obj, void *_Nullable ptr,
uint32_t size) { return static_cast<FakeMemory *>(obj)->realloc(ptr, size); },
.dealloc_callback = [](void *obj, void *ptr) { static_cast<FakeMemory *>(obj)->free(ptr); },
.dealloc_callback
= [](void *_Nonnull obj, void *_Nullable ptr) { static_cast<FakeMemory *>(obj)->free(ptr); },
};
// --- Implementation ---
@@ -24,7 +25,7 @@ static const Tox_Memory_Funcs kFakeMemoryVtable = {
FakeMemory::FakeMemory() = default;
FakeMemory::~FakeMemory() = default;
void *FakeMemory::malloc(size_t size)
void *_Nullable FakeMemory::malloc(size_t size)
{
bool fail = failure_injector_ && failure_injector_(size);
@@ -45,18 +46,12 @@ void *FakeMemory::malloc(size_t size)
header->size = size;
header->magic = kMagic;
current_allocation_ += size;
if (current_allocation_ > max_allocation_) {
max_allocation_ = current_allocation_;
}
on_allocation(size);
void *res = header + 1;
// std::cerr << "[FakeMemory] malloc(" << size << ") -> " << res << " (header=" << header << ")"
// << std::endl;
return res;
return header + 1;
}
void *FakeMemory::realloc(void *ptr, size_t size)
void *_Nullable FakeMemory::realloc(void *_Nullable ptr, size_t size)
{
if (!ptr) {
return malloc(size);
@@ -82,7 +77,6 @@ void *FakeMemory::realloc(void *ptr, size_t size)
}
if (fail) {
// If realloc fails, original block is left untouched.
return nullptr;
}
@@ -92,22 +86,17 @@ void *FakeMemory::realloc(void *ptr, size_t size)
return nullptr;
}
Header *header = static_cast<Header *>(new_ptr);
current_allocation_ -= old_size;
current_allocation_ += size;
if (current_allocation_ > max_allocation_) {
max_allocation_ = current_allocation_;
}
on_deallocation(old_size);
on_allocation(size);
Header *header = static_cast<Header *>(new_ptr);
header->size = size;
header->magic = kMagic;
void *res = header + 1;
// std::cerr << "[FakeMemory] realloc(" << ptr << ", " << size << ") -> " << res << " (header="
// << header << ")" << std::endl;
return res;
return header + 1;
}
void FakeMemory::free(void *ptr)
void FakeMemory::free(void *_Nullable ptr)
{
if (!ptr) {
return;
@@ -127,7 +116,7 @@ void FakeMemory::free(void *ptr)
}
size_t size = header->size;
current_allocation_ -= size;
on_deallocation(size);
header->magic = kFreeMagic; // Mark as free
std::free(header);
}
@@ -139,6 +128,19 @@ void FakeMemory::set_failure_injector(FailureInjector injector)
void FakeMemory::set_observer(Observer observer) { observer_ = std::move(observer); }
struct Tox_Memory FakeMemory::get_c_memory() { return Tox_Memory{&kFakeMemoryVtable, this}; }
struct Memory FakeMemory::c_memory() { return Memory{&kFakeMemoryVtable, this}; }
size_t FakeMemory::current_allocation() const { return current_allocation_.load(); }
size_t FakeMemory::max_allocation() const { return max_allocation_.load(); }
void FakeMemory::on_allocation(size_t size)
{
size_t current = current_allocation_.fetch_add(size) + size;
size_t max = max_allocation_.load(std::memory_order_relaxed);
while (current > max && !max_allocation_.compare_exchange_weak(max, current)) { }
}
void FakeMemory::on_deallocation(size_t size) { current_allocation_.fetch_sub(size); }
} // namespace tox::test

View File

@@ -2,18 +2,18 @@
#include <algorithm>
#include "../../../toxcore/tox_random_impl.h"
#include "../../../toxcore/rng.h"
namespace tox::test {
// --- Trampolines for Tox_Random_Funcs ---
// --- Trampolines for Random_Funcs ---
static const Tox_Random_Funcs kFakeRandomVtable = {
static const Random_Funcs kFakeRandomVtable = {
.bytes_callback
= [](void *obj, uint8_t *bytes,
= [](void *_Nonnull obj, uint8_t *_Nonnull bytes,
uint32_t length) { static_cast<FakeRandom *>(obj)->bytes(bytes, length); },
.uniform_callback
= [](void *obj,
= [](void *_Nonnull obj,
uint32_t upper_bound) { return static_cast<FakeRandom *>(obj)->uniform(upper_bound); },
};
@@ -44,7 +44,7 @@ uint32_t FakeRandom::uniform(uint32_t upper_bound)
return dist(rng_);
}
void FakeRandom::bytes(uint8_t *out, size_t count)
void FakeRandom::bytes(uint8_t *_Nonnull out, size_t count)
{
if (entropy_source_) {
entropy_source_(out, count);
@@ -58,6 +58,6 @@ void FakeRandom::bytes(uint8_t *out, size_t count)
}
}
struct Tox_Random FakeRandom::get_c_random() { return Tox_Random{&kFakeRandomVtable, this}; }
struct Random FakeRandom::c_random() { return Random{&kFakeRandomVtable, this}; }
} // namespace tox::test

View File

@@ -56,7 +56,7 @@ void configure_fuzz_memory_source(FakeMemory &memory, Fuzz_Data &input)
void configure_fuzz_random_source(FakeRandom &random, Fuzz_Data &input)
{
random.set_entropy_source([&input](uint8_t *out, size_t count) {
random.set_entropy_source([&input](uint8_t *_Nonnull out, size_t count) {
// Initialize with zeros in case of underflow
std::memset(out, 0, count);

View File

@@ -14,8 +14,8 @@ IP make_ip(uint32_t ipv4)
IP make_node_ip(uint32_t node_id)
{
// Use 10.x.y.z range: 10. (id >> 16) . (id >> 8) . (id & 0xFF)
return make_ip(0x0A000000 | (node_id & 0x00FFFFFF));
// Use 20.x.y.z range: 20. (id >> 16) . (id >> 8) . (id & 0xFF)
return make_ip(0x14000000 | (node_id & 0x00FFFFFF));
}
} // namespace tox::test

View File

@@ -52,11 +52,11 @@ std::unique_ptr<ScopedToxSystem> SimulatedEnvironment::create_node(uint16_t port
scoped->endpoint = scoped->node->get_primary_socket();
// Use global Random and Memory for legacy compatibility.
scoped->c_random = global_random_->get_c_random();
scoped->c_memory = global_memory_->get_c_memory();
scoped->c_random = global_random_->c_random();
scoped->c_memory = global_memory_->c_memory();
// Use Node's Network
scoped->c_network = scoped->node->get_c_network();
scoped->c_network = scoped->node->c_network;
// Setup System
scoped->system.mem = &scoped->c_memory;
@@ -64,7 +64,7 @@ std::unique_ptr<ScopedToxSystem> SimulatedEnvironment::create_node(uint16_t port
scoped->system.rng = &scoped->c_random;
scoped->system.mono_time_user_data = &sim_->clock();
scoped->system.mono_time_callback = [](void *user_data) -> uint64_t {
scoped->system.mono_time_callback = [](void *_Nullable user_data) -> uint64_t {
return static_cast<FakeClock *>(user_data)->current_time_ms();
};

View File

@@ -1,10 +1,96 @@
#include "../public/simulation.hh"
#include <cassert>
#include <chrono>
#include <iostream>
#include <thread>
namespace tox::test {
// --- LogFilter ---
LogFilter operator&&(const LogFilter &lhs, const LogFilter &rhs)
{
return LogFilter([=](const LogMetadata &md) { return lhs(md) && rhs(md); });
}
LogFilter operator||(const LogFilter &lhs, const LogFilter &rhs)
{
return LogFilter([=](const LogMetadata &md) { return lhs(md) || rhs(md); });
}
LogFilter operator!(const LogFilter &target)
{
return LogFilter([=](const LogMetadata &md) { return !target(md); });
}
namespace log_filter {
LogFilter level(Tox_Log_Level min_level)
{
return LogFilter([=](const LogMetadata &md) { return md.level >= min_level; });
}
LevelPlaceholder level() { return {}; }
LogFilter LevelPlaceholder::operator>(Tox_Log_Level rhs) const
{
return LogFilter([=](const LogMetadata &md) { return md.level > rhs; });
}
LogFilter LevelPlaceholder::operator>=(Tox_Log_Level rhs) const
{
return LogFilter([=](const LogMetadata &md) { return md.level >= rhs; });
}
LogFilter LevelPlaceholder::operator<(Tox_Log_Level rhs) const
{
return LogFilter([=](const LogMetadata &md) { return md.level < rhs; });
}
LogFilter LevelPlaceholder::operator<=(Tox_Log_Level rhs) const
{
return LogFilter([=](const LogMetadata &md) { return md.level <= rhs; });
}
LogFilter LevelPlaceholder::operator==(Tox_Log_Level rhs) const
{
return LogFilter([=](const LogMetadata &md) { return md.level == rhs; });
}
LogFilter LevelPlaceholder::operator!=(Tox_Log_Level rhs) const
{
return LogFilter([=](const LogMetadata &md) { return md.level != rhs; });
}
LogFilter file(std::string pattern)
{
return LogFilter([p = std::move(pattern)](const LogMetadata &md) {
return std::string(md.file).find(p) != std::string::npos;
});
}
LogFilter func(std::string pattern)
{
return LogFilter([p = std::move(pattern)](const LogMetadata &md) {
return std::string(md.func).find(p) != std::string::npos;
});
}
LogFilter message(std::string pattern)
{
return LogFilter([p = std::move(pattern)](const LogMetadata &md) {
return std::string(md.message).find(p) != std::string::npos;
});
}
LogFilter node(uint32_t id)
{
return LogFilter([=](const LogMetadata &md) { return md.node_id == id; });
}
} // namespace log_filter
// --- Simulation ---
Simulation::Simulation()
@@ -24,11 +110,125 @@ void Simulation::advance_time(uint64_t ms)
void Simulation::run_until(std::function<bool()> condition, uint64_t timeout_ms)
{
uint64_t start_time = clock_->current_time_ms();
while (!condition()) {
if (clock_->current_time_ms() - start_time > timeout_ms) {
// Initial check
if (condition())
return;
while (true) {
if (clock_->current_time_ms() - start_time >= timeout_ms) {
break;
}
advance_time(10); // 10ms ticks
// 1. Advance Global Time
// Determine the time step based on the minimum requested delay from all runners
// during the previous tick. We default to kDefaultTickIntervalMs if no specific request was
// made. The `exchange` operation resets the minimum accumulator for the current tick.
uint32_t step = next_step_min_.exchange(kDefaultTickIntervalMs);
advance_time(step);
// 2. Start Barrier (Signal Runners)
// Notify all registered runners that time has advanced and they should proceed
// with their next iteration.
{
std::lock_guard<std::mutex> lock(barrier_mutex_);
current_generation_++;
// Initialize the countdown of active runners for this tick.
active_runners_.store(registered_runners_);
for (const auto &l : tick_listeners_) {
l.callback(current_generation_);
}
}
barrier_cv_.notify_all();
// 3. End Barrier (Wait for Completion)
// Block until all active runners have reported completion via `tick_complete()`.
{
std::unique_lock<std::mutex> lock(barrier_mutex_);
// We use a lambda predicate to handle spurious wakeups.
// The wait finishes when `active_runners_` reaches zero.
barrier_cv_.wait(lock, [this] { return active_runners_.load() == 0; });
}
// 4. Check condition
if (condition())
return;
}
}
void Simulation::set_log_filter(LogFilter filter) { log_filter_ = std::move(filter); }
Simulation::TickListenerId Simulation::register_tick_listener(
std::function<void(uint64_t)> listener)
{
std::lock_guard<std::mutex> lock(barrier_mutex_);
TickListenerId id = next_listener_id_++;
tick_listeners_.push_back({id, std::move(listener)});
return id;
}
void Simulation::unregister_tick_listener(TickListenerId id)
{
std::lock_guard<std::mutex> lock(barrier_mutex_);
for (auto it = tick_listeners_.begin(); it != tick_listeners_.end(); ++it) {
if (it->id == id) {
tick_listeners_.erase(it);
break;
}
}
}
uint64_t Simulation::register_runner()
{
std::lock_guard<std::mutex> lock(barrier_mutex_);
registered_runners_++;
return current_generation_;
}
void Simulation::unregister_runner()
{
std::lock_guard<std::mutex> lock(barrier_mutex_);
registered_runners_--;
// If we are currently running a tick (active_runners > 0), we need to decrement it
// because this runner will not be calling tick_complete()
if (active_runners_.load() > 0) {
if (active_runners_.fetch_sub(1) == 1) {
barrier_cv_.notify_all();
}
}
}
uint64_t Simulation::wait_for_tick(
uint64_t last_gen, const std::atomic<bool> &stop_token, uint64_t timeout_ms)
{
std::unique_lock<std::mutex> lock(barrier_mutex_);
// Wait until generation increases (new tick started) OR we are stopped OR timeout
bool result = barrier_cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms),
[&] { return current_generation_ > last_gen || stop_token; });
if (stop_token)
return last_gen;
if (!result)
return last_gen; // Timeout
return current_generation_;
}
void Simulation::tick_complete(uint32_t next_delay_ms)
{
// Atomic min reduction
uint32_t current = next_step_min_.load(std::memory_order_relaxed);
while (
next_delay_ms < current && !next_step_min_.compare_exchange_weak(current, next_delay_ms)) {
// If exchange failed, current was updated to actual value, so loop checks again
}
// We don't need the mutex to decrement the atomic
if (active_runners_.fetch_sub(1) == 1) {
// Last runner to finish: notify main thread
std::lock_guard<std::mutex> lock(barrier_mutex_);
barrier_cv_.notify_all();
}
}
@@ -51,9 +251,9 @@ SimulatedNode::SimulatedNode(Simulation &sim, uint32_t node_id)
, network_(std::make_unique<FakeNetworkStack>(sim.net(), make_node_ip(node_id)))
, random_(std::make_unique<FakeRandom>(12345 + node_id)) // Unique seed
, memory_(std::make_unique<FakeMemory>())
, c_network(network_->get_c_network())
, c_random(random_->get_c_random())
, c_memory(memory_->get_c_memory())
, c_network(network_->c_network())
, c_random(random_->c_random())
, c_memory(memory_->c_memory())
, ip(make_node_ip(node_id))
{
}
@@ -65,26 +265,49 @@ ClockSystem &SimulatedNode::clock() { return sim_.clock(); }
RandomSystem &SimulatedNode::random() { return *random_; }
MemorySystem &SimulatedNode::memory() { return *memory_; }
SimulatedNode::ToxPtr SimulatedNode::create_tox(const Tox_Options *options)
SimulatedNode::ToxPtr SimulatedNode::create_tox(const Tox_Options *_Nullable options)
{
std::unique_ptr<Tox_Options, decltype(&tox_options_free)> default_options(
nullptr, tox_options_free);
std::unique_ptr<Tox_Options, decltype(&tox_options_free)> local_opts(
tox_options_new(nullptr), tox_options_free);
assert(local_opts != nullptr);
if (options == nullptr) {
default_options.reset(tox_options_new(nullptr));
assert(default_options != nullptr);
tox_options_set_ipv6_enabled(default_options.get(), false);
tox_options_set_start_port(default_options.get(), 33445);
tox_options_set_end_port(default_options.get(), 55555);
options = default_options.get();
if (options != nullptr) {
tox_options_copy(local_opts.get(), options);
} else {
tox_options_set_ipv6_enabled(local_opts.get(), false);
tox_options_set_start_port(local_opts.get(), 33445);
tox_options_set_end_port(local_opts.get(), 55555);
}
tox_options_set_log_callback(local_opts.get(),
[](Tox *tox, Tox_Log_Level level, const char *file, uint32_t line, const char *func,
const char *message, void *user_data) {
SimulatedNode *node = static_cast<SimulatedNode *>(user_data);
uint32_t ip4 = net_ntohl(node->ip.ip.v4.uint32);
LogMetadata md{level, file, line, func, message, ip4 & 0xFF};
const auto &filter = node->simulation().log_filter();
bool allow = false;
if (filter.pred) {
allow = filter(md);
} else {
allow = node->simulation().net().is_verbose() && level >= TOX_LOG_LEVEL_TRACE;
}
if (allow) {
std::cerr << "[Tox Log] [Node " << (ip4 & 0xFF) << "] " << file << ":" << line
<< " (" << func << "): " << message << std::endl;
}
});
tox_options_set_log_user_data(local_opts.get(), this);
Tox_Options_Testing opts_testing;
Tox_System system;
system.ns = &c_network;
system.rng = &c_random;
system.mem = &c_memory;
system.mono_time_callback = [](void *user_data) -> uint64_t {
system.mono_time_callback = [](void *_Nullable user_data) -> uint64_t {
return static_cast<FakeClock *>(user_data)->current_time_ms();
};
system.mono_time_user_data = &sim_.clock();
@@ -94,15 +317,17 @@ SimulatedNode::ToxPtr SimulatedNode::create_tox(const Tox_Options *options)
Tox_Err_New err;
Tox_Err_New_Testing err_testing;
Tox *t = tox_new_testing(options, &err, &opts_testing, &err_testing);
Tox *t = tox_new_testing(local_opts.get(), &err, &opts_testing, &err_testing);
if (!t) {
std::cerr << "tox_new_testing failed: " << err << " (testing err: " << err_testing << ")"
<< std::endl;
return nullptr;
}
return ToxPtr(t);
}
FakeUdpSocket *SimulatedNode::get_primary_socket()
FakeUdpSocket *_Nullable SimulatedNode::get_primary_socket()
{
auto sockets = network_->get_bound_udp_sockets();
if (sockets.empty())

View File

@@ -5,17 +5,21 @@
#include "../public/tox_network.hh"
#include <cstring>
#include <future>
#include <iostream>
#include <vector>
#include "../../../toxcore/network.h"
#include "../../../toxcore/tox.h"
#include "../../../toxcore/tox_events.h"
#include "../public/tox_runner.hh"
namespace tox::test {
ConnectedFriend::~ConnectedFriend() = default;
std::vector<ConnectedFriend> setup_connected_friends(Simulation &sim, Tox *main_tox,
SimulatedNode &main_node, int num_friends, const Tox_Options *options)
std::vector<ConnectedFriend> setup_connected_friends(Simulation &sim, Tox *_Nonnull main_tox,
SimulatedNode &main_node, int num_friends, const Tox_Options *_Nullable options, bool verbose)
{
std::vector<ConnectedFriend> friends;
friends.reserve(num_friends);
@@ -42,108 +46,115 @@ std::vector<ConnectedFriend> setup_connected_friends(Simulation &sim, Tox *main_
for (int i = 0; i < num_friends; ++i) {
auto node = sim.create_node();
auto tox = node->create_tox(options);
if (!tox) {
return {};
}
auto runner = std::make_unique<ToxRunner>(*node, options);
uint8_t friend_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox.get(), friend_pk);
runner->invoke([&](Tox *_Nonnull tox) { tox_self_get_public_key(tox, friend_pk); });
Tox_Err_Friend_Add err;
uint32_t fn = tox_friend_add_norequest(main_tox, friend_pk, &err);
if (fn == UINT32_MAX || err != TOX_ERR_FRIEND_ADD_OK) {
return {};
}
if (tox_friend_add_norequest(tox.get(), main_pk, &err) == UINT32_MAX
|| err != TOX_ERR_FRIEND_ADD_OK) {
return {};
}
// Bootstrap to the main node AND the PREVIOUS node in the chain
tox_bootstrap(tox.get(), main_ip_str, main_port, main_dht_id, nullptr);
if (i > 0) {
tox_bootstrap(tox.get(), prev_ip_str, prev_port, prev_dht_id, nullptr);
}
// Execute add friend and bootstrap on runner
runner->execute([=](Tox *_Nonnull tox) {
tox_friend_add_norequest(tox, main_pk, nullptr);
tox_bootstrap(tox, main_ip_str, main_port, main_dht_id, nullptr);
if (i > 0) {
tox_bootstrap(tox, prev_ip_str, prev_port, prev_dht_id, nullptr);
}
});
// Retrieve previous node's DHT ID and update IP for the next iteration.
// We use invoke to safely fetch data from the runner thread.
runner->invoke([&](Tox *_Nonnull tox) { tox_self_get_dht_id(tox, prev_dht_id); });
// Update prev for next node
tox_self_get_dht_id(tox.get(), prev_dht_id);
ip_parse_addr(&node->ip, prev_ip_str, sizeof(prev_ip_str));
FakeUdpSocket *node_socket = node->get_primary_socket();
FakeUdpSocket *_Nullable node_socket = node->get_primary_socket();
if (!node_socket) {
return {};
}
prev_port = node_socket->local_port();
friends.push_back({std::move(node), std::move(tox), fn});
friends.push_back({std::move(node), std::move(runner), fn});
// Run simulation to let DHT stabilize
sim.run_until(
[&]() {
tox_iterate(main_tox, nullptr);
for (auto &f : friends) {
tox_iterate(f.tox.get(), nullptr);
}
return false;
},
200);
// Run the simulation periodically to allow the DHT to stabilize incrementally
// as we add nodes, rather than waiting until the end.
if (friends.size() % 10 == 0) {
sim.run_until([&]() { return false; }, 20);
}
}
// Optional: Bootstrap main_tox to the last node to complete the circle
if (!friends.empty()) {
tox_bootstrap(main_tox, prev_ip_str, prev_port, prev_dht_id, nullptr);
}
// Run simulation until all are connected
std::vector<bool> friends_connected(friends.size(), false);
sim.run_until(
[&]() {
bool all_connected = true;
int connected_count = 0;
tox_iterate(main_tox, nullptr);
for (auto &f : friends) {
tox_iterate(f.tox.get(), nullptr);
if (tox_friend_get_connection_status(main_tox, f.friend_number, nullptr)
!= TOX_CONNECTION_NONE
&& tox_friend_get_connection_status(f.tox.get(), 0, nullptr)
!= TOX_CONNECTION_NONE) {
connected_count++;
} else {
all_connected = false;
}
}
static uint64_t last_print = 0;
if (sim.clock().current_time_ms() - last_print > 1000) {
std::cerr << "[setup_connected_friends] Friends connected: " << connected_count
<< "/" << friends.size() << " (time: " << sim.clock().current_time_ms()
<< "ms)" << std::endl;
if (connected_count < static_cast<int>(friends.size())
&& sim.clock().current_time_ms() > 10000) {
for (size_t i = 0; i < friends.size(); ++i) {
auto s1 = tox_friend_get_connection_status(
main_tox, friends[i].friend_number, nullptr);
auto s2
= tox_friend_get_connection_status(friends[i].tox.get(), 0, nullptr);
if (s1 == TOX_CONNECTION_NONE || s2 == TOX_CONNECTION_NONE) {
std::cerr << " Friend " << i << " not connected (Main->F: " << s1
<< ", F->Main: " << s2 << ")" << std::endl;
// Check connection status
int connected_count = 0;
for (size_t i = 0; i < friends.size(); ++i) {
// Check if main sees friend
bool main_sees_friend
= tox_friend_get_connection_status(main_tox, friends[i].friend_number, nullptr)
!= TOX_CONNECTION_NONE;
// Check if friend sees main by polling events from the runner
auto batches = friends[i].runner->poll_events();
for (const auto &batch : batches) {
size_t size = tox_events_get_size(batch.get());
for (size_t k = 0; k < size; ++k) {
const Tox_Event *e = tox_events_get(batch.get(), k);
if (tox_event_get_type(e) == TOX_EVENT_FRIEND_CONNECTION_STATUS) {
auto *ev = tox_event_get_friend_connection_status(e);
if (tox_event_friend_connection_status_get_connection_status(ev)
!= TOX_CONNECTION_NONE) {
friends_connected[i] = true;
} else {
friends_connected[i] = false;
}
}
}
}
if (main_sees_friend && friends_connected[i]) {
connected_count++;
}
}
if (connected_count == static_cast<int>(friends.size())) {
return true;
}
static uint64_t last_print = 0;
if (verbose && sim.clock().current_time_ms() - last_print > 1000) {
std::cerr << "[setup_connected_friends] Friends connected: " << connected_count
<< "/" << friends.size() << " (time: " << sim.clock().current_time_ms()
<< "ms)" << std::endl;
last_print = sim.clock().current_time_ms();
}
return all_connected;
return false;
},
300000); // 5 minutes simulation time for 100 nodes to converge
300000);
return friends;
}
bool connect_friends(
Simulation &sim, SimulatedNode &node1, Tox *tox1, SimulatedNode &node2, Tox *tox2)
bool connect_friends(Simulation &sim, SimulatedNode &node1, Tox *_Nonnull tox1,
SimulatedNode &node2, Tox *_Nonnull tox2)
{
// This helper function assumes the Tox instances are running in the current thread
// (e.g., standard unit test) or that the caller is handling thread safety if they
// are part of a runner. It uses direct tox_iterate calls.
uint8_t pk1[TOX_PUBLIC_KEY_SIZE];
uint8_t pk2[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox1, pk1);
@@ -199,7 +210,7 @@ bool connect_friends(
}
uint32_t setup_connected_group(
Simulation &sim, Tox *main_tox, const std::vector<ConnectedFriend> &friends)
Simulation &sim, Tox *_Nonnull main_tox, const std::vector<ConnectedFriend> &friends)
{
struct NodeGroupState {
uint32_t peer_count = 0;
@@ -207,9 +218,10 @@ uint32_t setup_connected_group(
};
NodeGroupState main_state;
tox_callback_group_peer_join(main_tox, [](Tox *, uint32_t, uint32_t, void *user_data) {
static_cast<NodeGroupState *>(user_data)->peer_count++;
});
tox_callback_group_peer_join(
main_tox, [](Tox *_Nonnull, uint32_t, uint32_t, void *_Nullable user_data) {
static_cast<NodeGroupState *>(user_data)->peer_count++;
});
Tox_Err_Group_New err_new;
main_state.group_number = tox_group_new(main_tox, TOX_GROUP_PRIVACY_STATE_PUBLIC,
@@ -217,52 +229,26 @@ uint32_t setup_connected_group(
&err_new);
if (main_state.group_number == UINT32_MAX || err_new != TOX_ERR_GROUP_NEW_OK) {
std::cerr << "tox_group_new failed with error: " << err_new << std::endl;
return UINT32_MAX;
}
std::vector<std::unique_ptr<NodeGroupState>> friend_states;
friend_states.reserve(friends.size());
// Friend states tracked via events
std::vector<NodeGroupState> friend_states(friends.size());
for (size_t i = 0; i < friends.size(); ++i) {
auto state = std::make_unique<NodeGroupState>();
tox_callback_group_peer_join(
friends[i].tox.get(), [](Tox *, uint32_t, uint32_t, void *user_data) {
static_cast<NodeGroupState *>(user_data)->peer_count++;
});
// Main tox sends invites; friends accept via events polled from their runners.
tox_callback_group_invite(friends[i].tox.get(),
[](Tox *tox, uint32_t friend_number, const uint8_t *invite_data,
size_t invite_data_length, const uint8_t *, size_t, void *user_data) {
NodeGroupState *ng_state = static_cast<NodeGroupState *>(user_data);
Tox_Err_Group_Invite_Accept err_accept;
ng_state->group_number
= tox_group_invite_accept(tox, friend_number, invite_data, invite_data_length,
reinterpret_cast<const uint8_t *>("peer"), 4, nullptr, 0, &err_accept);
if (ng_state->group_number == UINT32_MAX
|| err_accept != TOX_ERR_GROUP_INVITE_ACCEPT_OK) {
ng_state->group_number = UINT32_MAX;
}
});
friend_states.push_back(std::move(state));
}
// Run until all have joined and see everyone
bool success = false;
uint64_t last_print = 0;
size_t invites_sent = 0;
sim.run_until(
[&]() {
tox_iterate(main_tox, &main_state);
// Throttle invites: keep max 5 pending
// Throttle invites
size_t accepted_count = 0;
for (size_t k = 0; k < invites_sent; ++k) {
if (friend_states[k]->group_number != UINT32_MAX) {
for (const auto &fs : friend_states) {
if (fs.group_number != UINT32_MAX)
accepted_count++;
}
}
while (invites_sent < friends.size() && (invites_sent - accepted_count) < 5) {
@@ -271,58 +257,58 @@ uint32_t setup_connected_group(
friends[invites_sent].friend_number, &err_invite)) {
invites_sent++;
} else {
if (err_invite != TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND) {
std::cerr << "Invite failed for friend " << invites_sent << ": "
<< err_invite << std::endl;
}
break; // Stop trying to send for this tick if we failed
break;
}
}
bool all_see_all = true;
if (main_state.peer_count < friends.size()) {
all_see_all = false;
}
// Process friend events
for (size_t i = 0; i < friends.size(); ++i) {
tox_iterate(friends[i].tox.get(), friend_states[i].get());
if (friend_states[i]->group_number == UINT32_MAX
|| friend_states[i]->peer_count < friends.size()) {
all_see_all = false;
}
}
auto batches = friends[i].runner->poll_events();
for (const auto &batch : batches) {
size_t size = tox_events_get_size(batch.get());
for (size_t k = 0; k < size; ++k) {
const Tox_Event *e = tox_events_get(batch.get(), k);
Tox_Event_Type type = tox_event_get_type(e);
if ((sim.clock().current_time_ms() - last_print) % 5000 == 0) {
int joined = 0;
int fully_connected = 0;
if (main_state.group_number != UINT32_MAX)
joined++;
if (main_state.peer_count >= friends.size())
fully_connected++;
if (type == TOX_EVENT_GROUP_INVITE) {
auto *ev = tox_event_get_group_invite(e);
uint32_t friend_number = tox_event_group_invite_get_friend_number(ev);
const uint8_t *data = tox_event_group_invite_get_invite_data(ev);
size_t len = tox_event_group_invite_get_invite_data_length(ev);
for (const auto &fs : friend_states) {
if (fs->group_number != UINT32_MAX) {
joined++;
if (fs->peer_count >= friends.size())
fully_connected++;
// Accept invite on runner thread.
// We must copy data because the event structure will be freed.
std::vector<uint8_t> invite_data(data, data + len);
friends[i].runner->execute([=](Tox *_Nonnull tox) {
Tox_Err_Group_Invite_Accept err;
tox_group_invite_accept(tox, friend_number, invite_data.data(),
invite_data.size(), reinterpret_cast<const uint8_t *>("peer"),
4, nullptr, 0, &err);
});
} else if (type == TOX_EVENT_GROUP_PEER_JOIN) {
friend_states[i].peer_count++;
} else if (type == TOX_EVENT_GROUP_SELF_JOIN) {
auto *ev = tox_event_get_group_self_join(e);
friend_states[i].group_number
= tox_event_group_self_join_get_group_number(ev);
}
}
}
std::cerr << "[setup_connected_group] Main peer count: " << main_state.peer_count
<< "/" << friends.size() << ", Nodes joined: " << joined << "/"
<< (friends.size() + 1) << ", fully connected: " << fully_connected << "/"
<< (friends.size() + 1) << " (time: " << sim.clock().current_time_ms()
<< "ms)" << std::endl;
last_print = sim.clock().current_time_ms();
}
if (all_see_all) {
success = true;
return true;
if (main_state.peer_count < friends.size())
return false;
for (const auto &fs : friend_states) {
if (fs.group_number == UINT32_MAX || fs.peer_count < friends.size())
return false;
}
return false;
success = true;
return true;
},
300000); // 5 minutes
300000);
return success ? main_state.group_number : UINT32_MAX;
}

View File

@@ -0,0 +1,125 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2026 The TokTok team.
*/
#include "../public/tox_runner.hh"
namespace tox::test {
ToxRunner::ToxRunner(SimulatedNode &node, const Tox_Options *_Nullable options)
: tox_(node.create_tox(options))
, node_(node)
{
if (tox_) {
tox_events_init(tox_.get());
}
node_.simulation().register_runner();
tick_listener_id_ = node_.simulation().register_tick_listener([this](uint64_t gen) {
Message msg;
msg.type = Message::Tick;
msg.generation = gen;
queue_.push(std::move(msg));
});
thread_ = std::thread([this] { loop(); });
}
ToxRunner::~ToxRunner()
{
// Unregister first to prevent new ticks and update simulation counters
node_.simulation().unregister_tick_listener(tick_listener_id_);
node_.simulation().unregister_runner();
Message msg;
msg.type = Message::Stop;
queue_.push(std::move(msg));
if (thread_.joinable()) {
thread_.join();
}
}
void ToxRunner::execute(std::function<void(Tox *_Nonnull)> task)
{
Message msg;
msg.type = Message::Task;
msg.task = std::move(task);
queue_.push(std::move(msg));
}
std::vector<ToxRunner::ToxEventsPtr> ToxRunner::poll_events()
{
std::vector<ToxEventsPtr> ret;
ToxEventsPtr ptr;
while (events_queue_.try_pop(ptr)) {
ret.push_back(std::move(ptr));
ptr = nullptr; // Reset ptr to avoid use-after-move warning, although try_pop overwrites
// it.
}
return ret;
}
void ToxRunner::pause()
{
if (!active_.exchange(false)) {
return;
}
node_.simulation().unregister_tick_listener(tick_listener_id_);
node_.simulation().unregister_runner();
tick_listener_id_ = -1;
}
void ToxRunner::resume()
{
if (active_.exchange(true)) {
return;
}
node_.simulation().register_runner();
tick_listener_id_ = node_.simulation().register_tick_listener([this](uint64_t gen) {
Message msg;
msg.type = Message::Tick;
msg.generation = gen;
queue_.push(std::move(msg));
});
}
void ToxRunner::loop()
{
while (true) {
Message msg = queue_.pop(); // Blocking wait
switch (msg.type) {
case Message::Stop:
return;
case Message::Task:
if (msg.task && tox_) {
msg.task(tox_.get());
}
break;
case Message::Tick: {
if (!tox_) {
node_.simulation().tick_complete();
break;
}
// Run Tox Events
Tox_Err_Events_Iterate err;
Tox_Events *events = tox_events_iterate(tox_.get(), false, &err);
if (events) {
events_queue_.push(ToxEventsPtr(events));
}
uint32_t interval = tox_iteration_interval(tox_.get());
node_.simulation().tick_complete(interval);
break;
}
}
}
}
} // namespace tox::test

View File

@@ -6,6 +6,13 @@
#include <gtest/gtest.h>
#include <atomic>
#include <iomanip>
#include <sstream>
#include "../../toxcore/attributes.h"
#include "../../toxcore/network.h"
namespace tox::test {
namespace {
@@ -22,6 +29,8 @@ namespace {
ASSERT_EQ(friends.size(), num_friends);
// Verification of connection status is done inside setup_connected_friends now,
// but we can double check main_tox's view.
for (const auto &f : friends) {
EXPECT_NE(tox_friend_get_connection_status(main_tox.get(), f.friend_number, nullptr),
TOX_CONNECTION_NONE);
@@ -29,25 +38,22 @@ namespace {
// Verify they can actually communicate
struct Context {
int count = 0;
std::atomic<int> count{0};
} ctx;
tox_callback_friend_message(main_tox.get(),
[](Tox *, uint32_t, Tox_Message_Type, const uint8_t *, size_t, void *user_data) {
static_cast<Context *>(user_data)->count++;
});
[](Tox *_Nonnull, uint32_t, Tox_Message_Type, const uint8_t *_Nonnull, size_t,
void *_Nullable user_data) { static_cast<Context *>(user_data)->count++; });
for (const auto &f : friends) {
const uint8_t msg[] = "hello";
tox_friend_send_message(
f.tox.get(), 0, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), nullptr);
f.runner->execute([](Tox *tox) {
const uint8_t msg[] = "hello";
tox_friend_send_message(tox, 0, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), nullptr);
});
}
sim.run_until([&]() {
tox_iterate(main_tox.get(), &ctx);
for (auto &f : friends) {
tox_iterate(f.tox.get(), nullptr);
}
return ctx.count == num_friends;
});
@@ -68,26 +74,23 @@ namespace {
ASSERT_EQ(friends.size(), num_friends);
struct Context {
int count = 0;
std::atomic<int> count{0};
} ctx;
tox_callback_friend_message(main_tox.get(),
[](Tox *, uint32_t, Tox_Message_Type, const uint8_t *, size_t, void *user_data) {
static_cast<Context *>(user_data)->count++;
});
[](Tox *_Nonnull, uint32_t, Tox_Message_Type, const uint8_t *_Nonnull, size_t,
void *_Nullable user_data) { static_cast<Context *>(user_data)->count++; });
for (const auto &f : friends) {
const uint8_t msg[] = "hello";
tox_friend_send_message(
f.tox.get(), 0, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), nullptr);
f.runner->execute([](Tox *tox) {
const uint8_t msg[] = "hello";
tox_friend_send_message(tox, 0, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), nullptr);
});
}
sim.run_until(
[&]() {
tox_iterate(main_tox.get(), &ctx);
for (auto &f : friends) {
tox_iterate(f.tox.get(), nullptr);
}
return ctx.count == num_friends;
},
60000);
@@ -113,10 +116,11 @@ namespace {
EXPECT_NE(tox_friend_get_connection_status(tox2.get(), 0, nullptr), TOX_CONNECTION_NONE);
// Verify communication
bool received = false;
std::atomic<bool> received{false};
tox_callback_friend_message(tox2.get(),
[](Tox *, uint32_t, Tox_Message_Type, const uint8_t *, size_t, void *user_data) {
*static_cast<bool *>(user_data) = true;
[](Tox *_Nonnull, uint32_t, Tox_Message_Type, const uint8_t *_Nonnull, size_t,
void *_Nullable user_data) {
*static_cast<std::atomic<bool> *>(user_data) = true;
});
const uint8_t msg[] = "hello";
@@ -125,7 +129,7 @@ namespace {
sim.run_until([&]() {
tox_iterate(tox1.get(), nullptr);
tox_iterate(tox2.get(), &received);
return received;
return received.load();
});
EXPECT_TRUE(received);
@@ -148,28 +152,27 @@ namespace {
// Verify we can send a group message
struct Context {
int count = 0;
std::atomic<int> count{0};
} ctx;
tox_callback_group_message(main_tox.get(),
[](Tox *, uint32_t, uint32_t, Tox_Message_Type, const uint8_t *, size_t, uint32_t,
void *user_data) { static_cast<Context *>(user_data)->count++; });
[](Tox *_Nonnull, uint32_t, uint32_t, Tox_Message_Type, const uint8_t *_Nonnull, size_t,
uint32_t,
void *_Nullable user_data) { static_cast<Context *>(user_data)->count++; });
for (const auto &f : friends) {
const uint8_t msg[] = "hello";
uint32_t f_gn = 0; // It should be 0 since it's the first group.
Tox_Err_Group_Send_Message err_send;
tox_group_send_message(
f.tox.get(), f_gn, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), &err_send);
EXPECT_EQ(err_send, TOX_ERR_GROUP_SEND_MESSAGE_OK);
f.runner->execute([](Tox *tox) {
const uint8_t msg[] = "hello";
uint32_t f_gn = 0; // First group
Tox_Err_Group_Send_Message err_send;
tox_group_send_message(
tox, f_gn, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), &err_send);
});
}
sim.run_until(
[&]() {
tox_iterate(main_tox.get(), &ctx);
for (auto &f : friends) {
tox_iterate(f.tox.get(), nullptr);
}
return ctx.count == num_friends;
},
10000);
@@ -193,28 +196,27 @@ namespace {
EXPECT_NE(group_number, UINT32_MAX);
struct Context {
int count = 0;
std::atomic<int> count{0};
} ctx;
tox_callback_group_message(main_tox.get(),
[](Tox *, uint32_t, uint32_t, Tox_Message_Type, const uint8_t *, size_t, uint32_t,
void *user_data) { static_cast<Context *>(user_data)->count++; });
[](Tox *_Nonnull, uint32_t, uint32_t, Tox_Message_Type, const uint8_t *_Nonnull, size_t,
uint32_t,
void *_Nullable user_data) { static_cast<Context *>(user_data)->count++; });
for (const auto &f : friends) {
const uint8_t msg[] = "hello";
uint32_t f_gn = 0;
Tox_Err_Group_Send_Message err_send;
tox_group_send_message(
f.tox.get(), f_gn, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), &err_send);
EXPECT_EQ(err_send, TOX_ERR_GROUP_SEND_MESSAGE_OK);
f.runner->execute([](Tox *tox) {
const uint8_t msg[] = "hello";
uint32_t f_gn = 0;
Tox_Err_Group_Send_Message err_send;
tox_group_send_message(
tox, f_gn, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), &err_send);
});
}
sim.run_until(
[&]() {
tox_iterate(main_tox.get(), &ctx);
for (auto &f : friends) {
tox_iterate(f.tox.get(), nullptr);
}
return ctx.count == num_friends;
},
120000);
@@ -222,5 +224,180 @@ namespace {
EXPECT_EQ(ctx.count, num_friends);
}
TEST(ToxNetworkTest, TcpRelayChaining)
{
constexpr bool kDebug = false;
Simulation sim;
sim.net().set_verbose(false);
if (kDebug) {
using namespace log_filter;
sim.set_log_filter(level(TOX_LOG_LEVEL_DEBUG)
|| (level(TOX_LOG_LEVEL_TRACE)
&& (file("TCP") || file("onion.c") || func("dht_isconnected"))
&& !message("not sending repeated announce request")));
}
struct ToxOptionsDeleter {
void operator()(Tox_Options *opts) { tox_options_free(opts); }
};
std::unique_ptr<Tox_Options, ToxOptionsDeleter> opts(tox_options_new(nullptr));
tox_options_set_udp_enabled(opts.get(), false);
tox_options_set_ipv6_enabled(opts.get(), false);
tox_options_set_local_discovery_enabled(opts.get(), false);
auto create = [&](const char *name, uint16_t port, bool udp_enabled = false) {
tox_options_set_tcp_port(opts.get(), port);
if (udp_enabled) {
tox_options_set_start_port(opts.get(), port);
tox_options_set_end_port(opts.get(), port);
}
tox_options_set_udp_enabled(opts.get(), udp_enabled);
auto node = sim.create_node();
auto tox = node->create_tox(opts.get());
if (!tox) {
std::cerr << "Failed to create node " << name << " on port " << port << std::endl;
std::abort();
}
return std::make_pair(std::move(node), std::move(tox));
};
// Servers (Enable UDP for relays so they can talk to each other)
auto [nodeA, toxA] = create("A", 20001, true);
auto [nodeB, toxB] = create("B", 20002, true);
// Clients
auto [nodeC, toxC] = create("C", 0);
auto [nodeD, toxD] = create("D", 0);
auto [nodeE, toxE] = create("E", 0);
auto [nodeF, toxF] = create("F", 0);
auto get_info = [](SimulatedNode &node, Tox *tox) {
uint8_t pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox, pk);
uint8_t dht_id[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(tox, dht_id);
char ip[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&node.ip, ip, sizeof(ip));
uint16_t port = tox_self_get_tcp_port(tox, nullptr);
if (kDebug) {
std::cout << "Node Info: IP=" << ip << " Port=" << port << std::endl;
auto to_hex = [](const uint8_t *data) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (int i = 0; i < TOX_PUBLIC_KEY_SIZE; ++i)
ss << std::setw(2) << static_cast<int>(data[i]);
return ss.str();
};
std::cout << "PK: " << to_hex(pk) << std::endl;
std::cout << "DHT ID: " << to_hex(dht_id) << std::endl;
}
return std::make_tuple(std::vector<uint8_t>(pk, pk + TOX_PUBLIC_KEY_SIZE),
std::vector<uint8_t>(dht_id, dht_id + TOX_PUBLIC_KEY_SIZE), std::string(ip), port);
};
auto [pkA, dhtIdA, ipA, portA] = get_info(*nodeA, toxA.get());
auto [pkB, dhtIdB, ipB, portB] = get_info(*nodeB, toxB.get());
// Helper to connect to a relay (bootstrap + add_tcp_relay)
auto connect_to_relay
= [](Tox *tox, const std::string &ip, uint16_t port, const std::vector<uint8_t> &pk,
const std::vector<uint8_t> &dht_id) {
Tox_Err_Bootstrap err_bs;
tox_bootstrap(tox, ip.c_str(), port, dht_id.data(), &err_bs);
if (err_bs != TOX_ERR_BOOTSTRAP_OK) {
std::cout << "tox_bootstrap failed with " << err_bs << " for " << ip << ":"
<< port << std::endl;
}
Tox_Err_Bootstrap err_relay;
// Use dht_id for TCP relay as well, as server uses DHT key
tox_add_tcp_relay(tox, ip.c_str(), port, dht_id.data(), &err_relay);
if (err_relay != TOX_ERR_BOOTSTRAP_OK) {
std::cout << "tox_add_tcp_relay failed with " << err_relay << " for " << ip
<< ":" << port << std::endl;
}
};
// Connect {C,D} -> A, {E,F} -> B
connect_to_relay(toxC.get(), ipA, portA, pkA, dhtIdA);
connect_to_relay(toxD.get(), ipA, portA, pkA, dhtIdA);
connect_to_relay(toxE.get(), ipB, portB, pkB, dhtIdB);
connect_to_relay(toxF.get(), ipB, portB, pkB, dhtIdB);
// B -> A (Connect the two TCP relays, but only one initial link)
connect_to_relay(toxB.get(), ipA, portA, pkA, dhtIdA);
// Connect C and F
uint8_t pkF[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(toxF.get(), pkF);
uint8_t pkC[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(toxC.get(), pkC);
Tox_Err_Friend_Add err;
const uint32_t fC = tox_friend_add_norequest(toxC.get(), pkF, &err);
ASSERT_EQ(err, TOX_ERR_FRIEND_ADD_OK);
const uint32_t fF = tox_friend_add_norequest(toxF.get(), pkC, &err);
ASSERT_EQ(err, TOX_ERR_FRIEND_ADD_OK);
struct Context {
bool received = false;
} ctx;
tox_callback_friend_message(toxF.get(),
[](Tox *, uint32_t, Tox_Message_Type, const uint8_t *, size_t, void *user_data) {
static_cast<Context *>(user_data)->received = true;
});
bool sent = false;
sim.run_until(
[&]() {
tox_iterate(toxA.get(), nullptr);
tox_iterate(toxB.get(), nullptr);
tox_iterate(toxC.get(), nullptr);
tox_iterate(toxD.get(), nullptr);
tox_iterate(toxE.get(), nullptr);
tox_iterate(toxF.get(), &ctx);
Tox_Connection statusC = tox_friend_get_connection_status(toxC.get(), fC, nullptr);
Tox_Connection statusF = tox_friend_get_connection_status(toxF.get(), fF, nullptr);
if (kDebug) {
static int loop_counter = 0;
if (loop_counter++ % 100 == 0) {
std::cout << "Conn Status: "
<< "A=" << tox_self_get_connection_status(toxA.get()) << " "
<< "B=" << tox_self_get_connection_status(toxB.get()) << " "
<< "C=" << tox_self_get_connection_status(toxC.get()) << " "
<< "D=" << tox_self_get_connection_status(toxC.get()) << " "
<< "E=" << tox_self_get_connection_status(toxC.get()) << " "
<< "F=" << tox_self_get_connection_status(toxF.get()) << " "
<< " Friend Status C->F: " << statusC << ", F->C: " << statusF
<< std::endl;
}
}
if (!sent && statusC != TOX_CONNECTION_NONE) {
const uint8_t msg[] = "hello";
Tox_Err_Friend_Send_Message send_err;
tox_friend_send_message(
toxC.get(), fC, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), &send_err);
if (kDebug) {
std::cout << "Message sent from C to F, err=" << send_err << std::endl;
}
sent = true;
}
return ctx.received;
},
120000);
EXPECT_TRUE(ctx.received);
}
} // namespace
} // namespace tox::test