Squashed 'external/toxcore/c-toxcore/' changes from 1828c5356..c9cdae001

c9cdae001 fix(toxav): remove extra copy of video frame on encode
4f6d4546b test: Improve the fake network library.
a2581e700 refactor(toxcore): generate `Friend_Request` and `Dht_Nodes_Response`
2aaa11770 refactor(toxcore): use Tox_Memory in generated events
5c367452b test(toxcore): fix incorrect mutex in tox_scenario_get_time
8f92e710f perf: Add a timed limit of number of cookie requests.
695b6417a test: Add some more simulated network support.
815ae9ce9 test(toxcore): fix thread-safety in scenario framework
6d85c754e test(toxcore): add unit tests for net_crypto
9c22e79cc test(support): add SimulatedEnvironment for deterministic testing
f34fcb195 chore: Update windows Dockerfile to debian stable (trixie).
ece0e8980 fix(group_moderation): allow validating unsorted sanction list signatures
a4fa754d7 refactor: rename struct Packet to struct Net_Packet
d6f330f85 cleanup: Fix some warnings from coverity.
e206bffa2 fix(group_chats): fix sync packets reverting topics
0e4715598 test: Add new scenario testing framework.
668291f44 refactor(toxcore): decouple Network_Funcs from sockaddr via IP_Port
fc4396cef fix: potential division by zero in toxav and unsafe hex parsing
8e8b352ab refactor: Add nullable annotations to struct members.
7740bb421 refactor: decouple net_crypto from DHT
1936d4296 test: add benchmark for toxav audio and video
46bfdc2df fix: correct printf format specifiers for unsigned integers
REVERT: 1828c5356 fix(toxav): remove extra copy of video frame on encode

git-subtree-dir: external/toxcore/c-toxcore
git-subtree-split: c9cdae001341e701fca980c9bb9febfeb95d2902
This commit is contained in:
Green Sky
2026-01-11 14:42:31 +01:00
parent e95f2cbb1c
commit 565efa4f39
328 changed files with 19057 additions and 13982 deletions

View File

@@ -15,6 +15,7 @@ sh_test(
"-Wno-boolean-return",
"-Wno-callback-names",
"-Wno-enum-from-int",
"-Wno-tagged-union",
"+RTS",
"-N4",
"-RTS",

View File

@@ -38,3 +38,5 @@ if(BUILD_MISC_TESTS)
target_link_libraries(Messenger_test PRIVATE Threads::Threads)
endif()
endif()
add_subdirectory(support)

View File

@@ -108,7 +108,7 @@ int main(int argc, char *argv[])
m = new_messenger(mono_time, mem, os_random(), os_network(), &options, &err);
if (!m) {
fprintf(stderr, "Failed to allocate messenger datastructure: %d\n", err);
fprintf(stderr, "Failed to allocate messenger datastructure: %u\n", err);
exit(0);
}

View File

@@ -1,28 +1,6 @@
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test")
load("@rules_cc//cc:defs.bzl", "cc_binary")
load("@rules_fuzzing//fuzzing:cc_defs.bzl", "cc_fuzz_test")
cc_library(
name = "fuzz_support",
srcs = [
"func_conversion.hh",
"fuzz_support.cc",
],
hdrs = ["fuzz_support.hh"],
visibility = ["//c-toxcore:__subpackages__"],
deps = [
"//c-toxcore/toxcore:crypto_core",
"//c-toxcore/toxcore:network",
"//c-toxcore/toxcore:tox",
],
)
cc_library(
name = "fuzz_tox",
hdrs = ["fuzz_tox.hh"],
visibility = ["//c-toxcore:__subpackages__"],
deps = [":fuzz_support"],
)
cc_fuzz_test(
name = "bootstrap_fuzz_test",
size = "small",
@@ -30,8 +8,7 @@ cc_fuzz_test(
copts = ["-UNDEBUG"],
corpus = ["//tools/toktok-fuzzer/corpus:bootstrap_fuzz_test"],
deps = [
":fuzz_support",
":fuzz_tox",
"//c-toxcore/testing/support",
"//c-toxcore/toxcore:tox",
"//c-toxcore/toxcore:tox_dispatch",
"//c-toxcore/toxcore:tox_events",
@@ -46,8 +23,7 @@ cc_fuzz_test(
corpus = ["//tools/toktok-fuzzer/corpus:e2e_fuzz_test"],
data = ["//tools/toktok-fuzzer/init:e2e_fuzz_test.dat"],
deps = [
":fuzz_support",
":fuzz_tox",
"//c-toxcore/testing/support",
"//c-toxcore/toxcore:crypto_core",
"//c-toxcore/toxcore:tox",
"//c-toxcore/toxcore:tox_dispatch",
@@ -62,7 +38,7 @@ cc_fuzz_test(
copts = ["-UNDEBUG"],
corpus = ["//tools/toktok-fuzzer/corpus:toxsave_fuzz_test"],
deps = [
":fuzz_support",
"//c-toxcore/testing/support",
"//c-toxcore/toxcore:tox",
],
)
@@ -72,7 +48,7 @@ cc_binary(
srcs = ["protodump.cc"],
copts = ["-UNDEBUG"],
deps = [
":fuzz_support",
"//c-toxcore/testing/support",
"//c-toxcore/toxcore:tox",
"//c-toxcore/toxcore:tox_dispatch",
"//c-toxcore/toxcore:tox_events",
@@ -90,38 +66,3 @@ genrule(
tags = ["manual"],
tools = [":protodump"],
)
# bazel test --config=asan-libfuzzer //c-toxcore/testing/fuzzing:protodump_reduce_test
cc_test(
name = "protodump_reduce_test",
size = "small",
srcs = ["protodump_reduce.cc"],
args = ["$(location :e2e_fuzz_test_init.dat)"],
copts = ["-UNDEBUG"],
data = [":e2e_fuzz_test_init.dat"],
tags = ["manual"],
deps = [
":fuzz_support",
":fuzz_tox",
"//c-toxcore/toxcore:crypto_core",
"//c-toxcore/toxcore:tox",
"//c-toxcore/toxcore:tox_dispatch",
"//c-toxcore/toxcore:tox_events",
"@rules_fuzzing//fuzzing:cc_engine",
],
)
cc_fuzz_test(
name = "protodump_reduce",
size = "small",
srcs = ["protodump_reduce.cc"],
copts = ["-UNDEBUG"],
deps = [
":fuzz_support",
":fuzz_tox",
"//c-toxcore/toxcore:crypto_core",
"//c-toxcore/toxcore:tox",
"//c-toxcore/toxcore:tox_dispatch",
"//c-toxcore/toxcore:tox_events",
],
)

View File

@@ -1,5 +1,4 @@
# Override network and random functions
add_library(fuzz_support func_conversion.hh fuzz_support.cc fuzz_support.hh)
set(LIBFUZZER_LINKER_FLAGS)
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
@@ -12,9 +11,9 @@ function(fuzz_test target source_dir)
set(CORPUS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/toktok-fuzzer/corpus/${target}_fuzz_test)
file(GLOB CORPUS "${CORPUS_DIR}/*")
add_executable(${target}_fuzz_test ${source_dir}/${target}_fuzz_test.cc)
target_link_libraries(${target}_fuzz_test PRIVATE fuzz_support test_util toxcore_fuzz ${LIBFUZZER_LINKER_FLAGS})
target_link_libraries(${target}_fuzz_test PRIVATE support test_util toxcore_fuzz ${LIBFUZZER_LINKER_FLAGS})
if(CORPUS)
add_test(NAME ${target}_fuzz COMMAND ${CROSSCOMPILING_EMULATOR} ${target}_fuzz_test -max_total_time=10 ${CORPUS})
add_test(NAME ${target}_fuzz COMMAND ${target}_fuzz_test -max_total_time=10 ${CORPUS})
set_property(TEST ${target}_fuzz PROPERTY ENVIRONMENT "LLVM_PROFILE_FILE=${target}.profraw;srcdir=${CMAKE_CURRENT_SOURCE_DIR}")
endif()
endfunction()

View File

@@ -1,14 +1,26 @@
#include <cassert>
#include <cstdio>
#include <functional>
#include <memory>
#include "../../toxcore/tox.h"
#include "../../toxcore/tox_dispatch.h"
#include "../../toxcore/tox_events.h"
#include "fuzz_support.hh"
#include "fuzz_tox.hh"
#include "../support/public/fuzz_data.hh"
#include "../support/public/fuzz_helpers.hh"
#include "../support/public/simulated_environment.hh"
namespace {
using tox::test::configure_fuzz_memory_source;
using tox::test::configure_fuzz_packet_source;
using tox::test::FakeClock;
using tox::test::Fuzz_Data;
using tox::test::SimulatedEnvironment;
template <typename T>
using Ptr = std::unique_ptr<T, void (*)(T *)>;
void setup_callbacks(Tox_Dispatch *dispatch)
{
tox_events_callback_conference_connected(
@@ -97,10 +109,15 @@ void setup_callbacks(Tox_Dispatch *dispatch)
void TestBootstrap(Fuzz_Data &input)
{
// Null system for regularly working memory allocations needed in
// tox_events_equal.
Null_System null_sys;
Fuzz_System sys(input);
SimulatedEnvironment env;
env.fake_clock().advance(1000000000); // Match legacy behavior
auto node = env.create_node(33445);
configure_fuzz_memory_source(env.fake_memory(), input);
configure_fuzz_packet_source(*node->endpoint, input);
// Create a second null system for tox_events_equal check
SimulatedEnvironment null_env;
auto null_node = null_env.create_node(0); // Port 0 (unbound/irrelevant)
Ptr<Tox_Options> opts(tox_options_new(nullptr), tox_options_free);
assert(opts != nullptr);
@@ -110,8 +127,26 @@ void TestBootstrap(Fuzz_Data &input)
const char *message, void *user_data) {
// Log to stdout.
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf("[tox1] %c %s:%u(%s): %s\n", tox_log_level_name(level), file, line,
func, message);
// Approximate level name mapping
char level_char = '?';
switch (level) {
case TOX_LOG_LEVEL_TRACE:
level_char = 'T';
break;
case TOX_LOG_LEVEL_DEBUG:
level_char = 'D';
break;
case TOX_LOG_LEVEL_INFO:
level_char = 'I';
break;
case TOX_LOG_LEVEL_WARNING:
level_char = 'W';
break;
case TOX_LOG_LEVEL_ERROR:
level_char = 'E';
break;
}
std::printf("[tox1] %c %s:%u(%s): %s\n", level_char, file, line, func, message);
}
});
@@ -134,7 +169,7 @@ void TestBootstrap(Fuzz_Data &input)
}
Tox_Options_Testing tox_options_testing;
tox_options_testing.operating_system = sys.sys.get();
tox_options_testing.operating_system = &node->system;
Tox_Err_New error_new;
Tox_Err_New_Testing error_new_testing;
@@ -165,15 +200,17 @@ void TestBootstrap(Fuzz_Data &input)
while (!input.empty()) {
Tox_Err_Events_Iterate error_iterate;
Tox_Events *events = tox_events_iterate(tox, true, &error_iterate);
assert(tox_events_equal(null_sys.sys.get(), events, events));
assert(tox_events_equal(&null_node->system, events, events));
tox_dispatch_invoke(dispatch, events, tox);
tox_events_free(events);
// Move the clock forward a decent amount so all the time-based checks
// trigger more quickly.
sys.clock += 200;
// If no input was consumed, something went wrong.
assert(input_size != input.size());
env.advance_time(200);
// If no input was consumed, stop.
if (input_size == input.size()) {
break;
}
input_size = input.size();
}

View File

@@ -5,17 +5,29 @@
#include <cassert>
#include <cstdio>
#include <functional>
#include <vector>
#include "../../toxcore/crypto_core.h"
#include "../../toxcore/tox.h"
#include "../../toxcore/tox_dispatch.h"
#include "../../toxcore/tox_events.h"
#include "fuzz_support.hh"
#include "fuzz_tox.hh"
#include "../support/public/fuzz_data.hh"
#include "../support/public/fuzz_helpers.hh"
#include "../support/public/simulated_environment.hh"
namespace {
using tox::test::configure_fuzz_memory_source;
using tox::test::configure_fuzz_packet_source;
using tox::test::configure_fuzz_random_source;
using tox::test::FakeClock;
using tox::test::Fuzz_Data;
using tox::test::SimulatedEnvironment;
template <typename T>
using Ptr = std::unique_ptr<T, void (*)(T *)>;
void setup_callbacks(Tox_Dispatch *dispatch)
{
tox_events_callback_conference_connected(
@@ -131,9 +143,16 @@ void setup_callbacks(Tox_Dispatch *dispatch)
void TestEndToEnd(Fuzz_Data &input)
{
Fuzz_System sys(input);
// Used for places where we want all allocations to succeed.
Null_System null_sys;
SimulatedEnvironment env;
env.fake_clock().advance(1000000000); // Match legacy behavior
auto node = env.create_node(33445);
configure_fuzz_memory_source(env.fake_memory(), input);
configure_fuzz_packet_source(*node->endpoint, input);
configure_fuzz_random_source(env.fake_random(), input);
// Null system replacement for event comparison
SimulatedEnvironment null_env;
auto null_node = null_env.create_node(0);
Ptr<Tox_Options> opts(tox_options_new(nullptr), tox_options_free);
assert(opts != nullptr);
@@ -144,13 +163,31 @@ void TestEndToEnd(Fuzz_Data &input)
const char *message, void *user_data) {
// Log to stdout.
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf("[tox1] %c %s:%u(%s): %s\n", tox_log_level_name(level), file, line,
func, message);
// Approximate level name mapping
char level_char = '?';
switch (level) {
case TOX_LOG_LEVEL_TRACE:
level_char = 'T';
break;
case TOX_LOG_LEVEL_DEBUG:
level_char = 'D';
break;
case TOX_LOG_LEVEL_INFO:
level_char = 'I';
break;
case TOX_LOG_LEVEL_WARNING:
level_char = 'W';
break;
case TOX_LOG_LEVEL_ERROR:
level_char = 'E';
break;
}
std::printf("[tox1] %c %s:%u(%s): %s\n", level_char, file, line, func, message);
}
});
Tox_Options_Testing tox_options_testing;
tox_options_testing.operating_system = sys.sys.get();
tox_options_testing.operating_system = &node->system;
Tox_Err_New error_new;
Tox_Err_New_Testing error_new_testing;
@@ -171,15 +208,18 @@ void TestEndToEnd(Fuzz_Data &input)
assert(dispatch != nullptr);
setup_callbacks(dispatch);
// MIN_ITERATION_INTERVAL = 20
const uint8_t MIN_ITERATION_INTERVAL = 20;
while (!input.empty()) {
Tox_Err_Events_Iterate error_iterate;
Tox_Events *events = tox_events_iterate(tox, true, &error_iterate);
assert(tox_events_equal(null_sys.sys.get(), events, events));
assert(tox_events_equal(&null_node->system, events, events));
tox_dispatch_invoke(dispatch, events, tox);
tox_events_free(events);
// Move the clock forward a decent amount so all the time-based checks
// trigger more quickly.
sys.clock += std::max(System::MIN_ITERATION_INTERVAL, random_u08(sys.rng.get()));
uint32_t rand_val = env.fake_random().uniform(256);
env.fake_clock().advance(std::max<uint64_t>(MIN_ITERATION_INTERVAL, rand_val));
}
tox_dispatch_free(dispatch);

View File

@@ -1,69 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2022-2025 The TokTok team.
*/
#ifndef C_TOXCORE_TESTING_FUZZING_FUNC_CONVERSION_H
#define C_TOXCORE_TESTING_FUZZING_FUNC_CONVERSION_H
namespace detail {
template <typename F, F f>
struct func_conversion {
private:
template <typename R, typename... Args>
using func_pointer = R (*)(Args...);
template <typename From>
struct static_caster {
From obj;
template <typename To>
operator To() const
{
return static_cast<To>(obj);
}
};
public:
template <typename R, typename Arg, typename... Args>
constexpr operator func_pointer<R, Arg, Args...>()
{
return [](Arg obj, auto... args) { return f(static_caster<Arg>{obj}, args...); };
}
};
template <typename F>
struct make_funptr;
template <typename T, typename R, typename... Args>
struct make_funptr<R (T::*)(Args...) const> {
using type = R (*)(Args...);
};
/** @brief Turn a memfunptr type into a plain funptr type.
*
* Not needed in C++20, because we can pass the lambda itself as template
* argument, but in C++17, we need to do an early conversion.
*/
template <typename F>
using make_funptr_t = typename make_funptr<F>::type;
}
/** @brief Turn a C++ lambda into a C function pointer with `void*` param.
*
* Takes a lambda function with any pointer type as first parameter and turns it
* into a C function pointer with `void*` as the first parameter. Internally, it
* `static_cast`s that `void*` to the lambda's parameter type, avoiding a bunch
* of casts inside the lambdas.
*
* This works on any type `T` that can be `static_cast` to `U`, not just `void*`
* to `U*`, but the common case for C callbacks is `void*`.
*/
template <typename F>
static constexpr auto operator!(F f)
{
return detail::func_conversion<detail::make_funptr_t<decltype(&F::operator())>, f>{};
}
#endif // C_TOXCORE_TESTING_FUZZING_FUNC_CONVERSION_H

View File

@@ -1,479 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2021-2025 The TokTok team.
*/
#include "fuzz_support.hh"
#ifdef _WIN32
#include <winsock2.h>
// Comment line here to avoid reordering by source code formatters.
#include <windows.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#include <sys/socket.h>
#endif
#include <algorithm>
#include <cassert>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstring>
#include <memory>
#include "../../toxcore/crypto_core.h"
#include "../../toxcore/network.h"
#include "../../toxcore/tox_memory_impl.h"
#include "../../toxcore/tox_private.h"
#include "../../toxcore/tox_random_impl.h"
#include "func_conversion.hh"
// TODO(iphydf): Put this somewhere shared.
struct Network_Addr {
struct sockaddr_storage addr;
size_t size;
};
System::System(std::unique_ptr<Tox_System> in_sys, std::unique_ptr<Tox_Memory> in_mem,
std::unique_ptr<Network> in_ns, std::unique_ptr<Tox_Random> in_rng)
: sys(std::move(in_sys))
, mem(std::move(in_mem))
, ns(std::move(in_ns))
, rng(std::move(in_rng))
{
}
System::System(System &&) = default;
System::~System() { }
static int recv_common(Fuzz_Data &input, uint8_t *buf, size_t buf_len)
{
if (input.size() < 2) {
errno = ENOMEM;
return -1;
}
CONSUME_OR_ABORT(const uint8_t *fuzz_len_bytes, input, 2);
const std::size_t fuzz_len = (fuzz_len_bytes[0] << 8) | fuzz_len_bytes[1];
if (fuzz_len == 0xffff) {
errno = EWOULDBLOCK;
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf("recvfrom: no data for tox1\n");
}
return -1;
}
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf(
"recvfrom: %zu (%02x, %02x) for tox1\n", fuzz_len, input.data()[-2], input.data()[-1]);
}
const size_t res = std::min(buf_len, std::min(fuzz_len, input.size()));
CONSUME_OR_ABORT(const uint8_t *data, input, res);
std::copy(data, data + res, buf);
return res;
}
static void *report_alloc(const char *name, const char *func, std::size_t size, void *ptr)
{
if (Fuzz_Data::FUZZ_DEBUG) {
printf("%s: %s(%zu): %s\n", name, func, size, ptr == nullptr ? "false" : "true");
}
return ptr;
}
template <typename F, F Func, typename... Args>
static void *alloc_common(const char *func, std::size_t size, Fuzz_Data &data, Args... args)
{
CONSUME1_OR_RETURN_VAL(
const bool, want_alloc, data, report_alloc("tox1", func, size, Func(args...)));
if (!want_alloc) {
return nullptr;
}
return report_alloc("tox1", func, size, Func(args...));
}
static constexpr Tox_Memory_Funcs fuzz_memory_funcs = {
/* .malloc = */
![](Fuzz_System *self, uint32_t size) {
return alloc_common<decltype(std::malloc), std::malloc>("malloc", size, self->data, size);
},
/* .realloc = */
![](Fuzz_System *self, void *ptr, uint32_t size) {
return alloc_common<decltype(std::realloc), std::realloc>(
"realloc", size, self->data, ptr, size);
},
/* .dealloc = */
![](Fuzz_System *self, void *ptr) { std::free(ptr); },
};
static constexpr Network_Funcs fuzz_network_funcs = {
/* .close = */ ![](Fuzz_System *self, Socket sock) { return 0; },
/* .accept = */ ![](Fuzz_System *self, Socket sock) { return Socket{1337}; },
/* .bind = */ ![](Fuzz_System *self, Socket sock, const Network_Addr *addr) { return 0; },
/* .listen = */ ![](Fuzz_System *self, Socket sock, int backlog) { return 0; },
/* .connect = */ ![](Fuzz_System *self, Socket sock, const Network_Addr *addr) { return 0; },
/* .recvbuf = */
![](Fuzz_System *self, Socket sock) {
assert(sock.value == 42 || sock.value == 1337);
const size_t count = random_u16(self->rng.get());
return static_cast<int>(std::min(count, self->data.size()));
},
/* .recv = */
![](Fuzz_System *self, Socket sock, uint8_t *buf, size_t len) {
assert(sock.value == 42 || sock.value == 1337);
// Receive data from the fuzzer.
return recv_common(self->data, buf, len);
},
/* .recvfrom = */
![](Fuzz_System *self, Socket sock, uint8_t *buf, size_t len, Network_Addr *addr) {
assert(sock.value == 42 || sock.value == 1337);
addr->addr = sockaddr_storage{};
// Dummy Addr
addr->addr.ss_family = AF_INET;
// We want an AF_INET address with dummy values
sockaddr_in *addr_in = reinterpret_cast<sockaddr_in *>(&addr->addr);
addr_in->sin_port = htons(33446);
addr_in->sin_addr.s_addr = htonl(0x7f000002); // 127.0.0.2
addr->size = sizeof(struct sockaddr);
return recv_common(self->data, buf, len);
},
/* .send = */
![](Fuzz_System *self, Socket sock, const uint8_t *buf, size_t len) {
assert(sock.value == 42 || sock.value == 1337);
// Always succeed.
return static_cast<int>(len);
},
/* .sendto = */
![](Fuzz_System *self, Socket sock, const uint8_t *buf, size_t len, const Network_Addr *addr) {
assert(sock.value == 42 || sock.value == 1337);
// Always succeed.
return static_cast<int>(len);
},
/* .socket = */ ![](Fuzz_System *self, int domain, int type, int proto) { return Socket{42}; },
/* .socket_nonblock = */ ![](Fuzz_System *self, Socket sock, bool nonblock) { return 0; },
/* .getsockopt = */
![](Fuzz_System *self, Socket sock, int level, int optname, void *optval, size_t *optlen) {
std::memset(optval, 0, *optlen);
return 0;
},
/* .setsockopt = */
![](Fuzz_System *self, Socket sock, int level, int optname, const void *optval, size_t optlen) {
return 0;
},
};
static constexpr Tox_Random_Funcs fuzz_random_funcs = {
/* .bytes_callback = */
![](Fuzz_System *self, uint8_t *bytes, size_t length) {
// Initialize the buffer with zeros in case there's no randomness left.
std::fill_n(bytes, length, 0);
// For integers, we copy bytes directly, because we want to control the
// exact values.
if (length == sizeof(uint8_t) || length == sizeof(uint16_t) || length == sizeof(uint32_t)
|| length == sizeof(uint64_t)) {
CONSUME_OR_RETURN(const uint8_t *data, self->data, length);
std::copy(data, data + length, bytes);
if (Fuzz_Data::FUZZ_DEBUG) {
if (length == 1) {
std::printf("rng: %d (0x%02x)\n", bytes[0], bytes[0]);
} else {
std::printf("rng: %02x..%02x[%zu]\n", bytes[0], bytes[length - 1], length);
}
}
return;
}
// For nonces and keys, we fill the buffer with the same 1-2 bytes
// repeated. We only need these to be different enough to not often be
// the same.
assert(length == 24 || length == 32);
// We must cover the case of having only 1 byte left in the input. In
// that case, we will use the same byte for all the bytes in the output.
const size_t chunk_size = std::max(self->data.size(), static_cast<std::size_t>(2));
CONSUME_OR_RETURN(const uint8_t *chunk, self->data, chunk_size);
if (chunk_size == 2) {
std::fill_n(bytes, length / 2, chunk[0]);
std::fill_n(bytes + length / 2, length / 2, chunk[1]);
} else {
std::fill_n(bytes, length, chunk[0]);
}
if (Fuzz_Data::FUZZ_DEBUG) {
if (length == 1) {
std::printf("rng: %d (0x%02x)\n", bytes[0], bytes[0]);
} else {
std::printf("rng: %02x..%02x[%zu]\n", bytes[0], bytes[length - 1], length);
}
}
},
/* .uniform_callback = */
![](Fuzz_System *self, uint32_t upper_bound) {
uint32_t randnum = 0;
if (upper_bound > 0) {
self->rng->funcs->bytes_callback(
self, reinterpret_cast<uint8_t *>(&randnum), sizeof(randnum));
randnum %= upper_bound;
}
return randnum;
},
};
Fuzz_System::Fuzz_System(Fuzz_Data &input)
: System{
std::make_unique<Tox_System>(),
std::make_unique<Tox_Memory>(Tox_Memory{&fuzz_memory_funcs, this}),
std::make_unique<Network>(Network{&fuzz_network_funcs, this}),
std::make_unique<Tox_Random>(Tox_Random{&fuzz_random_funcs, this}),
}
, data(input)
{
sys->mono_time_callback = [](void *self) { return static_cast<Fuzz_System *>(self)->clock; };
sys->mono_time_user_data = this;
sys->mem = mem.get();
sys->ns = ns.get();
sys->rng = rng.get();
}
static constexpr Tox_Memory_Funcs null_memory_funcs = {
/* .malloc = */
![](Null_System *self, uint32_t size) { return std::malloc(size); },
/* .realloc = */
![](Null_System *self, void *ptr, uint32_t size) { return std::realloc(ptr, size); },
/* .dealloc = */
![](Null_System *self, void *ptr) { std::free(ptr); },
};
static constexpr Network_Funcs null_network_funcs = {
/* .close = */ ![](Null_System *self, Socket sock) { return 0; },
/* .accept = */ ![](Null_System *self, Socket sock) { return Socket{1337}; },
/* .bind = */ ![](Null_System *self, Socket sock, const Network_Addr *addr) { return 0; },
/* .listen = */ ![](Null_System *self, Socket sock, int backlog) { return 0; },
/* .connect = */ ![](Null_System *self, Socket sock, const Network_Addr *addr) { return 0; },
/* .recvbuf = */ ![](Null_System *self, Socket sock) { return 0; },
/* .recv = */
![](Null_System *self, Socket sock, uint8_t *buf, size_t len) {
// Always fail.
errno = ENOMEM;
return -1;
},
/* .recvfrom = */
![](Null_System *self, Socket sock, uint8_t *buf, size_t len, Network_Addr *addr) {
// Always fail.
errno = ENOMEM;
return -1;
},
/* .send = */
![](Null_System *self, Socket sock, const uint8_t *buf, size_t len) {
// Always succeed.
return static_cast<int>(len);
},
/* .sendto = */
![](Null_System *self, Socket sock, const uint8_t *buf, size_t len, const Network_Addr *addr) {
// Always succeed.
return static_cast<int>(len);
},
/* .socket = */ ![](Null_System *self, int domain, int type, int proto) { return Socket{42}; },
/* .socket_nonblock = */ ![](Null_System *self, Socket sock, bool nonblock) { return 0; },
/* .getsockopt = */
![](Null_System *self, Socket sock, int level, int optname, void *optval, size_t *optlen) {
std::memset(optval, 0, *optlen);
return 0;
},
/* .setsockopt = */
![](Null_System *self, Socket sock, int level, int optname, const void *optval, size_t optlen) {
return 0;
},
};
static uint64_t simple_rng(uint64_t &seed)
{
// https://nuclear.llnl.gov/CNP/rng/rngman/node4.html
seed = 2862933555777941757LL * seed + 3037000493LL;
return seed;
}
static constexpr Tox_Random_Funcs null_random_funcs = {
/* .bytes_callback = */
![](Null_System *self, uint8_t *bytes, size_t length) {
for (size_t i = 0; i < length; ++i) {
bytes[i] = simple_rng(self->seed) & 0xff;
}
},
/* .uniform_callback = */
![](Null_System *self, uint32_t upper_bound) {
return static_cast<uint32_t>(simple_rng(self->seed)) % upper_bound;
},
};
Null_System::Null_System()
: System{
std::make_unique<Tox_System>(),
std::make_unique<Tox_Memory>(Tox_Memory{&null_memory_funcs, this}),
std::make_unique<Network>(Network{&null_network_funcs, this}),
std::make_unique<Tox_Random>(Tox_Random{&null_random_funcs, this}),
}
{
sys->mono_time_callback = [](void *self) { return static_cast<Null_System *>(self)->clock; };
sys->mono_time_user_data = this;
sys->mem = mem.get();
sys->ns = ns.get();
sys->rng = rng.get();
}
static uint16_t get_port(const Network_Addr *addr)
{
if (addr->addr.ss_family == AF_INET6) {
return reinterpret_cast<const sockaddr_in6 *>(&addr->addr)->sin6_port;
} else {
assert(addr->addr.ss_family == AF_INET);
return reinterpret_cast<const sockaddr_in *>(&addr->addr)->sin_port;
}
}
static constexpr Tox_Memory_Funcs record_memory_funcs = {
/* .malloc = */
![](Record_System *self, uint32_t size) {
self->push(true);
return report_alloc(self->name_, "malloc", size, std::malloc(size));
},
/* .realloc = */
![](Record_System *self, void *ptr, uint32_t size) {
self->push(true);
return report_alloc(self->name_, "realloc", size, std::realloc(ptr, size));
},
/* .dealloc = */
![](Record_System *self, void *ptr) { std::free(ptr); },
};
static constexpr Network_Funcs record_network_funcs = {
/* .close = */ ![](Record_System *self, Socket sock) { return 0; },
/* .accept = */ ![](Record_System *self, Socket sock) { return Socket{2}; },
/* .bind = */
![](Record_System *self, Socket sock, const Network_Addr *addr) {
const uint16_t port = get_port(addr);
if (self->global_.bound.find(port) != self->global_.bound.end()) {
errno = EADDRINUSE;
return -1;
}
self->global_.bound.emplace(port, self);
self->port = port;
return 0;
},
/* .listen = */ ![](Record_System *self, Socket sock, int backlog) { return 0; },
/* .connect = */ ![](Record_System *self, Socket sock, const Network_Addr *addr) { return 0; },
/* .recvbuf = */ ![](Record_System *self, Socket sock) { return 0; },
/* .recv = */
![](Record_System *self, Socket sock, uint8_t *buf, size_t len) {
// Always fail.
errno = ENOMEM;
return -1;
},
/* .recvfrom = */
![](Record_System *self, Socket sock, uint8_t *buf, size_t len, Network_Addr *addr) {
assert(sock.value == 42);
if (self->recvq.empty()) {
self->push("\xff\xff");
errno = EWOULDBLOCK;
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf("%s: recvfrom: no data\n", self->name_);
}
return -1;
}
const auto [from, packet] = std::move(self->recvq.front());
self->recvq.pop_front();
const size_t recvlen = std::min(len, packet.size());
std::copy(packet.begin(), packet.end(), buf);
addr->addr = sockaddr_storage{};
// Dummy Addr
addr->addr.ss_family = AF_INET;
// We want an AF_INET address with dummy values
sockaddr_in *addr_in = reinterpret_cast<sockaddr_in *>(&addr->addr);
addr_in->sin_port = from;
addr_in->sin_addr.s_addr = htonl(0x7f000002); // 127.0.0.2
addr->size = sizeof(struct sockaddr);
assert(recvlen > 0 && recvlen <= INT_MAX);
self->push(uint8_t(recvlen >> 8));
self->push(uint8_t(recvlen & 0xff));
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf("%s: recvfrom: %zu (%02x, %02x)\n", self->name_, recvlen,
self->recording().end()[-2], self->recording().end()[-1]);
}
self->push(buf, recvlen);
return static_cast<int>(recvlen);
},
/* .send = */
![](Record_System *self, Socket sock, const uint8_t *buf, size_t len) {
// Always succeed.
return static_cast<int>(len);
},
/* .sendto = */
![](Record_System *self, Socket sock, const uint8_t *buf, size_t len,
const Network_Addr *addr) {
assert(sock.value == 42);
auto backend = self->global_.bound.find(get_port(addr));
assert(backend != self->global_.bound.end());
backend->second->receive(self->port, buf, len);
return static_cast<int>(len);
},
/* .socket = */
![](Record_System *self, int domain, int type, int proto) { return Socket{42}; },
/* .socket_nonblock = */ ![](Record_System *self, Socket sock, bool nonblock) { return 0; },
/* .getsockopt = */
![](Record_System *self, Socket sock, int level, int optname, void *optval, size_t *optlen) {
std::memset(optval, 0, *optlen);
return 0;
},
/* .setsockopt = */
![](Record_System *self, Socket sock, int level, int optname, const void *optval,
size_t optlen) { return 0; },
};
static constexpr Tox_Random_Funcs record_random_funcs = {
/* .bytes_callback = */
![](Record_System *self, uint8_t *bytes, size_t length) {
for (size_t i = 0; i < length; ++i) {
bytes[i] = simple_rng(self->seed_) & 0xff;
self->push(bytes[i]);
}
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf(
"%s: rng: %02x..%02x[%zu]\n", self->name_, bytes[0], bytes[length - 1], length);
}
},
/* .uniform_callback = */
fuzz_random_funcs.uniform_callback,
};
Record_System::Record_System(Global &global, uint64_t seed, const char *name)
: System{
std::make_unique<Tox_System>(),
std::make_unique<Tox_Memory>(Tox_Memory{&record_memory_funcs, this}),
std::make_unique<Network>(Network{&record_network_funcs, this}),
std::make_unique<Random>(Random{&record_random_funcs, this}),
}
, global_(global)
, seed_(seed)
, name_(name)
{
sys->mono_time_callback = [](void *self) { return static_cast<Record_System *>(self)->clock; };
sys->mono_time_user_data = this;
sys->mem = mem.get();
sys->ns = ns.get();
sys->rng = rng.get();
}
void Record_System::receive(uint16_t send_port, const uint8_t *buf, size_t len)
{
assert(port != 0);
recvq.emplace_back(send_port, std::vector<uint8_t>{buf, buf + len});
}

View File

@@ -1,407 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2021-2025 The TokTok team.
*/
#ifndef C_TOXCORE_TESTING_FUZZING_FUZZ_SUPPORT_H
#define C_TOXCORE_TESTING_FUZZING_FUZZ_SUPPORT_H
#include <array>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <memory>
#include <utility>
#include <vector>
#include "../../toxcore/tox_private.h"
struct Fuzz_Data {
static constexpr bool FUZZ_DEBUG = false;
static constexpr std::size_t TRACE_TRAP = -1; // 579;
private:
const uint8_t *data_;
const uint8_t *base_;
std::size_t size_;
public:
Fuzz_Data(const uint8_t *input_data, std::size_t input_size)
: data_(input_data)
, base_(input_data)
, size_(input_size)
{
}
Fuzz_Data &operator=(const Fuzz_Data &rhs) = delete;
Fuzz_Data(const Fuzz_Data &rhs) = delete;
struct Consumer {
const char *func;
Fuzz_Data &fd;
operator bool()
{
// Special case because memcpy causes UB for bool (which can't be
// anything other than 0 or 1).
const bool val = fd.data_[0];
if (FUZZ_DEBUG) {
std::printf("consume@%zu(%s): bool %s\n", fd.pos(), func, val ? "true" : "false");
}
++fd.data_;
--fd.size_;
return val;
}
template <typename T>
operator T()
{
const uint8_t *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}; }
std::size_t size() const { return size_; }
std::size_t pos() const { return data_ - base_; }
const uint8_t *data() const { return data_; }
bool empty() const { return size_ == 0; }
const uint8_t *consume(const char *func, std::size_t count)
{
const uint8_t *val = data_;
if (FUZZ_DEBUG) {
if (pos() == TRACE_TRAP) {
__asm__("int $3");
}
if (count == 1) {
std::printf("consume@%zu(%s): %d (0x%02x)\n", pos(), func, val[0], val[0]);
} else if (count != 0) {
std::printf("consume@%zu(%s): %02x..%02x[%zu]\n", pos(), func, val[0],
val[count - 1], count);
}
}
data_ += count;
size_ -= count;
return val;
}
};
/** @brief Consumes 1 byte of the fuzzer input or returns if no data available.
*
* This advances the fuzzer input data by 1 byte and consumes that byte in the
* declaration.
*
* @example
* @code
* CONSUME1_OR_RETURN(const uint8_t, one_byte, input);
* @endcode
*/
#define CONSUME1_OR_RETURN(TYPE, NAME, INPUT) \
if (INPUT.size() < sizeof(TYPE)) { \
return; \
} \
TYPE NAME = INPUT.consume1(__func__)
/** @brief Consumes 1 byte of the fuzzer input or returns a value if no data
* available.
*
* This advances the fuzzer input data by 1 byte and consumes that byte in the
* declaration.
*
* @example
* @code
* CONSUME1_OR_RETURN_VAL(const uint8_t one_byte, input, nullptr);
* @endcode
*/
#define CONSUME1_OR_RETURN_VAL(TYPE, NAME, INPUT, VAL) \
if (INPUT.size() < sizeof(TYPE)) { \
return VAL; \
} \
TYPE NAME = INPUT.consume1(__func__)
/** @brief Consumes SIZE bytes of the fuzzer input or returns if not enough data available.
*
* This advances the fuzzer input data by SIZE byte and consumes those bytes in
* the declaration. If less than SIZE bytes are available in the fuzzer input,
* this macro returns from the enclosing function.
*
* @example
* @code
* CONSUME_OR_RETURN(const uint8_t *ten_bytes, input, 10);
* @endcode
*/
#define CONSUME_OR_RETURN(DECL, INPUT, SIZE) \
if (INPUT.size() < SIZE) { \
return; \
} \
DECL = INPUT.consume(__func__, SIZE)
#define CONSUME_OR_RETURN_VAL(DECL, INPUT, SIZE, VAL) \
if (INPUT.size() < SIZE) { \
return VAL; \
} \
DECL = INPUT.consume(__func__, SIZE)
#define CONSUME_OR_ABORT(DECL, INPUT, SIZE) \
if (INPUT.size() < SIZE) { \
abort(); \
} \
DECL = INPUT.consume(__func__, SIZE)
using Fuzz_Target = void (*)(Fuzz_Data &input);
template <Fuzz_Target... Args>
struct Fuzz_Target_Selector;
template <Fuzz_Target Arg, Fuzz_Target... Args>
struct Fuzz_Target_Selector<Arg, Args...> {
static void select(uint8_t selector, Fuzz_Data &input)
{
if (selector == sizeof...(Args)) {
return Arg(input);
}
return Fuzz_Target_Selector<Args...>::select(selector, input);
}
};
template <>
struct Fuzz_Target_Selector<> {
static void select(uint8_t selector, Fuzz_Data &input)
{
// The selector selected no function, so we do nothing and rely on the
// fuzzer to come up with a better selector.
}
};
template <Fuzz_Target... Args>
void fuzz_select_target(const uint8_t *data, std::size_t size)
{
Fuzz_Data input{data, size};
CONSUME1_OR_RETURN(const uint8_t, selector, input);
return Fuzz_Target_Selector<Args...>::select(selector, input);
}
struct Tox_Memory;
struct Network;
struct Tox_Random;
struct System {
/** @brief Deterministic system clock for this instance.
*
* Different instances can evolve independently. The time is initialised
* with a large number, because otherwise many zero-initialised "empty"
* friends inside toxcore will be "not timed out" for a long time, messing
* up some logic. Tox moderately depends on the clock being fairly high up
* (not close to 0).
*
* We make it a nice large round number so we can recognise it when debugging.
*/
uint64_t clock = 1000000000;
std::unique_ptr<Tox_System> sys;
std::unique_ptr<Tox_Memory> mem;
std::unique_ptr<Network> ns;
std::unique_ptr<Tox_Random> rng;
System(std::unique_ptr<Tox_System> sys, std::unique_ptr<Tox_Memory> mem,
std::unique_ptr<Network> ns, std::unique_ptr<Tox_Random> rng);
System(System &&);
// Not inline because sizeof of the above 2 structs is not known everywhere.
~System();
/**
* During bootstrap, move the time forward a decent amount, because friend
* finding and bootstrapping takes significant (around 10 seconds) wall
* clock time that should be advanced more quickly in the test.
*/
static constexpr uint8_t BOOTSTRAP_ITERATION_INTERVAL = 200;
/**
* Less than BOOTSTRAP_ITERATION_INTERVAL because otherwise we'll spam
* onion announce packets.
*/
static constexpr uint8_t MESSAGE_ITERATION_INTERVAL = 20;
/**
* Move the clock forward at least 20ms so at least some amount of
* time passes on each iteration.
*/
static constexpr uint8_t MIN_ITERATION_INTERVAL = 20;
};
/**
* A Tox_System implementation that consumes fuzzer input to produce network
* inputs and random numbers. Once it runs out of fuzzer input, network receive
* functions return no more data and the random numbers are always zero.
*/
struct Fuzz_System : System {
Fuzz_Data &data;
explicit Fuzz_System(Fuzz_Data &input);
};
/**
* A Tox_System implementation that consumes no fuzzer input but still has a
* working and deterministic RNG. Network receive functions always fail, send
* always succeeds.
*/
struct Null_System : System {
uint64_t seed = 4; // chosen by fair dice roll. guaranteed to be random.
Null_System();
};
template <typename V>
class int_map {
public:
struct iterator {
std::pair<uint16_t, V> pair;
bool operator==(const iterator &rhs) const { return pair.first == rhs.pair.first; }
bool operator!=(const iterator &rhs) const { return pair.first != rhs.pair.first; }
std::pair<uint16_t, V> operator*() const { return pair; }
const std::pair<uint16_t, V> *operator->() const { return &pair; }
};
int_map() = default;
~int_map() = default;
iterator find(uint16_t key) const
{
if (!values[key]) {
return end();
}
return {{key, values[key]}};
}
iterator end() const { return {{static_cast<uint16_t>(values.size()), nullptr}}; }
void emplace(uint16_t key, V value) { values[key] = value; }
private:
std::array<V, UINT16_MAX> values;
};
/**
* A Tox_System implementation that records all I/O but does not actually
* perform any real I/O. Everything inside this system is hermetic in-process
* and fully deterministic.
*
* Note: take care not to initialise two systems with the same seed, since
* that's the only thing distinguishing the system's behaviour. Two toxes
* initialised with the same seed will be identical (same keys, etc.).
*/
struct Record_System : System {
static constexpr bool FUZZ_DEBUG = Fuzz_Data::FUZZ_DEBUG;
/** @brief State shared between all tox instances. */
struct Global {
/** @brief Bound UDP ports and their system instance.
*
* This implements an in-process network where instances can send
* packets to other instances by inserting them into the receiver's
* recvq using the receive function.
*
* We need to keep track of ports associated with recv queues because
* toxcore sends packets to itself sometimes when doing onion routing
* with only 2 nodes in the network.
*/
int_map<Record_System *> bound;
};
Global &global_;
uint64_t seed_; //!< Current PRNG state.
const char *name_; //!< Tox system name ("tox1"/"tox2") for logging.
std::deque<std::pair<uint16_t, std::vector<uint8_t>>> recvq;
uint16_t port = 0; //!< Sending port for this system instance.
Record_System(Global &global, uint64_t seed, const char *name);
Record_System(const Record_System &) = delete;
Record_System operator=(const Record_System &) = delete;
/** @brief Deposit a network packet in this instance's recvq.
*/
void receive(uint16_t send_port, const uint8_t *buf, size_t len);
void push(bool byte)
{
if (FUZZ_DEBUG) {
if (recording_.size() == Fuzz_Data::TRACE_TRAP) {
__asm__("int $3");
}
std::printf(
"%s: produce@%zu(bool %s)\n", name_, recording_.size(), byte ? "true" : "false");
}
recording_.push_back(byte);
}
void push(uint8_t byte)
{
if (FUZZ_DEBUG) {
if (recording_.size() == Fuzz_Data::TRACE_TRAP) {
__asm__("int $3");
}
std::printf("%s: produce@%zu(%u (0x%02x))\n", name_, recording_.size(), byte, byte);
}
recording_.push_back(byte);
}
void push(const uint8_t *bytes, std::size_t size)
{
if (FUZZ_DEBUG) {
if (recording_.size() == Fuzz_Data::TRACE_TRAP) {
__asm__("int $3");
}
std::printf("%s: produce@%zu(%02x..%02x[%zu])\n", name_, recording_.size(), bytes[0],
bytes[size - 1], size);
}
recording_.insert(recording_.end(), bytes, bytes + size);
}
template <std::size_t N>
void push(const char (&bytes)[N])
{
push(reinterpret_cast<const uint8_t *>(bytes), N - 1);
}
const std::vector<uint8_t> &recording() const { return recording_; }
std::vector<uint8_t> take_recording() const { return std::move(recording_); }
private:
std::vector<uint8_t> recording_;
};
/** @brief Enable debug logging.
*
* This should not be enabled in fuzzer code while fuzzing, as console I/O slows
* everything down drastically. It's useful while developing the fuzzer and the
* protodump program.
*/
extern const bool FUZZ_DEBUG;
inline constexpr char tox_log_level_name(Tox_Log_Level level)
{
switch (level) {
case TOX_LOG_LEVEL_TRACE:
return 'T';
case TOX_LOG_LEVEL_DEBUG:
return 'D';
case TOX_LOG_LEVEL_INFO:
return 'I';
case TOX_LOG_LEVEL_WARNING:
return 'W';
case TOX_LOG_LEVEL_ERROR:
return 'E';
}
return '?';
}
#endif // C_TOXCORE_TESTING_FUZZING_FUZZ_SUPPORT_H

View File

@@ -1,17 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2022-2025 The TokTok team.
*/
#ifndef C_TOXCORE_TESTING_FUZZING_FUZZ_TOX_H
#define C_TOXCORE_TESTING_FUZZING_FUZZ_TOX_H
#include <memory>
#include "../../toxcore/network.h"
constexpr uint16_t SIZE_IP_PORT = SIZE_IP6 + sizeof(uint16_t);
template <typename T>
using Ptr = std::unique_ptr<T, void (*)(T *)>;
#endif // C_TOXCORE_TESTING_FUZZING_FUZZ_TOX_H

View File

@@ -26,23 +26,41 @@
#include <cstring>
#include <fstream>
#include <memory>
#include <vector>
#include "../../toxcore/tox.h"
#include "../../toxcore/tox_dispatch.h"
#include "../../toxcore/tox_events.h"
#include "../../toxcore/tox_private.h"
#include "fuzz_support.hh"
#include "../support/public/simulated_environment.hh"
namespace {
/** @brief Number of messages to exchange between tox1 and tox2.
*
* The higher this number, the more room we give the fuzzer to mutate the
* exchange into something more interesting. If it's too high, the fuzzer will
* be slow.
*/
using tox::test::FakeClock;
using tox::test::Packet;
using tox::test::ScopedToxSystem;
using tox::test::SimulatedEnvironment;
constexpr uint32_t MESSAGE_COUNT = 5;
class Recorder {
public:
std::vector<uint8_t> data;
void push(bool val) { data.push_back(val); }
void push(uint8_t val) { data.push_back(val); }
void push(const uint8_t *bytes, size_t size) { data.insert(data.end(), bytes, bytes + size); }
// Format: 2 bytes length (big-endian), then data.
void push_packet(const uint8_t *bytes, size_t size)
{
assert(size <= 65535);
push(static_cast<uint8_t>(size >> 8));
push(static_cast<uint8_t>(size & 0xFF));
push(bytes, size);
}
};
struct State {
Tox *tox;
uint32_t done;
@@ -91,7 +109,6 @@ void setup_callbacks(Tox_Dispatch *dispatch)
tox_events_callback_friend_connection_status(
dispatch, [](const Tox_Event_Friend_Connection_Status *event, void *user_data) {
State *state = static_cast<State *>(user_data);
// OK: friend came online.
const uint32_t friend_number
= tox_event_friend_connection_status_get_friend_number(event);
assert(friend_number == 0);
@@ -163,9 +180,7 @@ void setup_callbacks(Tox_Dispatch *dispatch)
assert(!tox_event_friend_typing_get_typing(event));
});
tox_events_callback_self_connection_status(
dispatch, [](const Tox_Event_Self_Connection_Status *event, void *user_data) {
// OK: we got connected.
});
dispatch, [](const Tox_Event_Self_Connection_Status *event, void *user_data) {});
}
void dump(std::vector<uint8_t> recording, const char *filename)
@@ -177,69 +192,80 @@ void dump(std::vector<uint8_t> recording, const char *filename)
void RecordBootstrap(const char *init, const char *bootstrap)
{
auto global = std::make_unique<Record_System::Global>();
SimulatedEnvironment env1;
SimulatedEnvironment env2;
// Set deterministic seeds.
std::minstd_rand rng1(4);
env1.fake_random().set_entropy_source([&](uint8_t *out, size_t count) {
std::uniform_int_distribution<uint16_t> dist(0, 255);
for (size_t i = 0; i < count; ++i)
out[i] = static_cast<uint8_t>(dist(rng1));
});
std::minstd_rand rng2(5);
env2.fake_random().set_entropy_source([&](uint8_t *out, size_t count) {
std::uniform_int_distribution<uint16_t> dist(0, 255);
for (size_t i = 0; i < count; ++i)
out[i] = static_cast<uint8_t>(dist(rng2));
});
Recorder recorder1;
Recorder recorder2;
env1.fake_memory().set_observer([&](bool success) { recorder1.push(success); });
env1.fake_random().set_observer(
[&](const uint8_t *data, size_t count) { recorder1.push(data, count); });
auto node1 = env1.create_node(33445);
auto node2 = env2.create_node(33446);
// Record received packets.
node1->endpoint->set_recv_observer([&](const std::vector<uint8_t> &data, const IP_Port &from) {
recorder1.push_packet(data.data(), data.size());
});
// Bridge the two simulated networks.
env1.simulation().net().add_observer(
[&](const Packet &p) { env2.simulation().net().send_packet(p); });
env2.simulation().net().add_observer(
[&](const Packet &p) { env1.simulation().net().send_packet(p); });
Tox_Options *opts = tox_options_new(nullptr);
assert(opts != nullptr);
tox_options_set_local_discovery_enabled(opts, false);
tox_options_set_log_callback(opts,
[](Tox *tox, Tox_Log_Level level, const char *file, uint32_t line, const char *func,
const char *message, void *user_data) {
// Log to stdout.
std::printf("[%s] %c %s:%d(%s): %s\n", static_cast<Record_System *>(user_data)->name_,
tox_log_level_name(level), file, line, func, message);
});
Tox_Options_Testing opts_test1;
opts_test1.operating_system = &node1->system;
Tox_Err_New error_new;
Tox_Err_New_Testing error_new_testing;
Tox_Options_Testing tox_options_testing;
auto sys1 = std::make_unique<Record_System>(*global, 4, "tox1"); // fair dice roll
tox_options_set_log_user_data(opts, sys1.get());
tox_options_testing.operating_system = sys1->sys.get();
Tox *tox1 = tox_new_testing(opts, &error_new, &tox_options_testing, &error_new_testing);
Tox *tox1 = tox_new_testing(opts, &error_new, &opts_test1, &error_new_testing);
assert(tox1 != nullptr);
assert(error_new == TOX_ERR_NEW_OK);
assert(error_new_testing == TOX_ERR_NEW_TESTING_OK);
std::array<uint8_t, TOX_ADDRESS_SIZE> address1;
tox_self_get_address(tox1, address1.data());
std::array<uint8_t, TOX_PUBLIC_KEY_SIZE> pk1;
tox_self_get_public_key(tox1, pk1.data());
std::array<uint8_t, TOX_PUBLIC_KEY_SIZE> dht_key1;
tox_self_get_dht_id(tox1, dht_key1.data());
auto sys2 = std::make_unique<Record_System>(*global, 5, "tox2"); // unfair dice roll
tox_options_set_log_user_data(opts, sys2.get());
tox_options_testing.operating_system = sys2->sys.get();
Tox *tox2 = tox_new_testing(opts, &error_new, &tox_options_testing, &error_new_testing);
Tox_Options_Testing opts_test2;
opts_test2.operating_system = &node2->system;
Tox *tox2 = tox_new_testing(opts, &error_new, &opts_test2, &error_new_testing);
assert(tox2 != nullptr);
assert(error_new == TOX_ERR_NEW_OK);
assert(error_new_testing == TOX_ERR_NEW_TESTING_OK);
std::array<uint8_t, TOX_ADDRESS_SIZE> address2;
tox_self_get_address(tox2, address2.data());
std::array<uint8_t, TOX_PUBLIC_KEY_SIZE> pk2;
tox_self_get_public_key(tox2, pk2.data());
std::array<uint8_t, TOX_PUBLIC_KEY_SIZE> dht_key2;
tox_self_get_dht_id(tox2, dht_key2.data());
assert(address1 != address2);
assert(pk1 != pk2);
assert(dht_key1 != dht_key2);
tox_options_free(opts);
const uint16_t port = tox_self_get_udp_port(tox1, nullptr);
std::array<uint8_t, TOX_ADDRESS_SIZE> address1;
tox_self_get_address(tox1, address1.data());
std::array<uint8_t, TOX_PUBLIC_KEY_SIZE> dht_key1;
tox_self_get_dht_id(tox1, dht_key1.data());
const bool udp_success = tox_bootstrap(tox2, "127.0.0.2", port, dht_key1.data(), nullptr);
// Bootstrap tox2 to tox1.
const bool udp_success = tox_bootstrap(tox2, "127.0.0.1", 33445, dht_key1.data(), nullptr);
assert(udp_success);
tox_events_init(tox1);
tox_events_init(tox2);
Tox_Dispatch *dispatch = tox_dispatch_new(nullptr);
assert(dispatch != nullptr);
setup_callbacks(dispatch);
State state1 = {tox1, 0};
@@ -250,35 +276,26 @@ void RecordBootstrap(const char *init, const char *bootstrap)
Tox_Events *events;
events = tox_events_iterate(tox1, true, &error_iterate);
assert(tox_events_equal(sys1->sys.get(), events, events));
tox_dispatch_invoke(dispatch, events, &state1);
tox_events_free(events);
events = tox_events_iterate(tox2, true, &error_iterate);
assert(tox_events_equal(sys2->sys.get(), events, events));
tox_dispatch_invoke(dispatch, events, &state2);
tox_events_free(events);
// Move the clock forward a decent amount so all the time-based checks
// trigger more quickly.
sys1->clock += clock_increment;
sys2->clock += clock_increment;
// Record the clock increment.
env1.fake_clock().advance(clock_increment);
recorder1.push(clock_increment);
if (Fuzz_Data::FUZZ_DEBUG) {
printf("tox1: rng: %d (for clock)\n", clock_increment);
printf("tox2: rng: %d (for clock)\n", clock_increment);
}
sys1->push(clock_increment);
sys2->push(clock_increment);
env2.fake_clock().advance(clock_increment);
env1.simulation().net().process_events(env1.fake_clock().current_time_ms());
env2.simulation().net().process_events(env2.fake_clock().current_time_ms());
};
while (tox_self_get_connection_status(tox1) == TOX_CONNECTION_NONE
|| tox_self_get_connection_status(tox2) == TOX_CONNECTION_NONE) {
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf("tox1: %d, tox2: %d\n", tox_self_get_connection_status(tox1),
tox_self_get_connection_status(tox2));
}
iterate(System::BOOTSTRAP_ITERATION_INTERVAL);
iterate(200);
}
std::printf("toxes are online\n");
@@ -289,27 +306,18 @@ void RecordBootstrap(const char *init, const char *bootstrap)
while (tox_friend_get_connection_status(tox2, friend_number, nullptr) == TOX_CONNECTION_NONE
|| tox_friend_get_connection_status(tox1, 0, nullptr) == TOX_CONNECTION_NONE) {
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf("tox1: %d, tox2: %d, tox1 -> tox2: %d, tox2 -> tox1: %d\n",
tox_self_get_connection_status(tox1), tox_self_get_connection_status(tox2),
tox_friend_get_connection_status(tox1, 0, nullptr),
tox_friend_get_connection_status(tox2, 0, nullptr));
}
iterate(System::BOOTSTRAP_ITERATION_INTERVAL);
iterate(200);
}
std::printf("tox clients connected\n");
dump(sys1->take_recording(), init);
dump(recorder1.data, init);
// Clear the recorder.
recorder1.data.clear();
while (state1.done < MESSAGE_COUNT && state2.done < MESSAGE_COUNT) {
if (Fuzz_Data::FUZZ_DEBUG) {
std::printf("tox1: %d, tox2: %d, tox1 -> tox2: %d, tox2 -> tox1: %d\n",
tox_self_get_connection_status(tox1), tox_self_get_connection_status(tox2),
tox_friend_get_connection_status(tox1, 0, nullptr),
tox_friend_get_connection_status(tox2, 0, nullptr));
}
iterate(System::MESSAGE_ITERATION_INTERVAL);
iterate(20);
}
std::printf("test complete\n");
@@ -318,7 +326,7 @@ void RecordBootstrap(const char *init, const char *bootstrap)
tox_kill(tox2);
tox_kill(tox1);
dump(sys1->recording(), bootstrap);
dump(recorder1.data, bootstrap);
}
}

View File

@@ -1,213 +0,0 @@
#include <cassert>
#include <cstdio>
#include "../../toxcore/crypto_core.h"
#include "../../toxcore/tox.h"
#include "../../toxcore/tox_dispatch.h"
#include "../../toxcore/tox_events.h"
#include "../../toxcore/tox_private.h"
#include "fuzz_support.hh"
#include "fuzz_tox.hh"
namespace {
constexpr bool PROTODUMP_DEBUG = Fuzz_Data::FUZZ_DEBUG;
void setup_callbacks(Tox_Dispatch *dispatch)
{
tox_events_callback_conference_connected(
dispatch, [](const Tox_Event_Conference_Connected *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_conference_connected(
dispatch, [](const Tox_Event_Conference_Connected *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_conference_invite(
dispatch, [](const Tox_Event_Conference_Invite *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_conference_message(
dispatch, [](const Tox_Event_Conference_Message *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_conference_peer_list_changed(
dispatch, [](const Tox_Event_Conference_Peer_List_Changed *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_conference_peer_name(
dispatch, [](const Tox_Event_Conference_Peer_Name *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_conference_title(dispatch,
[](const Tox_Event_Conference_Title *event, void *user_data) { assert(event == nullptr); });
tox_events_callback_file_chunk_request(
dispatch, [](const Tox_Event_File_Chunk_Request *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_file_recv(dispatch,
[](const Tox_Event_File_Recv *event, void *user_data) { assert(event == nullptr); });
tox_events_callback_file_recv_chunk(dispatch,
[](const Tox_Event_File_Recv_Chunk *event, void *user_data) { assert(event == nullptr); });
tox_events_callback_file_recv_control(
dispatch, [](const Tox_Event_File_Recv_Control *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_friend_connection_status(
dispatch, [](const Tox_Event_Friend_Connection_Status *event, void *user_data) {
Tox *tox = static_cast<Tox *>(user_data);
// OK: friend came online.
const uint32_t friend_number
= tox_event_friend_connection_status_get_friend_number(event);
assert(friend_number == 0);
const uint8_t message = 'A';
Tox_Err_Friend_Send_Message err;
tox_friend_send_message(tox, friend_number, TOX_MESSAGE_TYPE_NORMAL, &message, 1, &err);
assert(err == TOX_ERR_FRIEND_SEND_MESSAGE_OK);
});
tox_events_callback_friend_lossless_packet(
dispatch, [](const Tox_Event_Friend_Lossless_Packet *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_friend_lossy_packet(
dispatch, [](const Tox_Event_Friend_Lossy_Packet *event, void *user_data) {
assert(event == nullptr);
});
tox_events_callback_friend_message(
dispatch, [](const Tox_Event_Friend_Message *event, void *user_data) {
Tox *tox = static_cast<Tox *>(user_data);
const uint32_t friend_number = tox_event_friend_message_get_friend_number(event);
assert(friend_number == 0);
const uint32_t message_length = tox_event_friend_message_get_message_length(event);
assert(message_length == 1);
const uint8_t *message = tox_event_friend_message_get_message(event);
const uint8_t reply = message[0] + 1;
Tox_Err_Friend_Send_Message err;
tox_friend_send_message(tox, friend_number, TOX_MESSAGE_TYPE_NORMAL, &reply, 1, &err);
assert(err == TOX_ERR_FRIEND_SEND_MESSAGE_OK);
});
tox_events_callback_friend_name(
dispatch, [](const Tox_Event_Friend_Name *event, void *user_data) {
const uint32_t friend_number = tox_event_friend_name_get_friend_number(event);
assert(friend_number == 0);
});
tox_events_callback_friend_read_receipt(
dispatch, [](const Tox_Event_Friend_Read_Receipt *event, void *user_data) {
const uint32_t friend_number = tox_event_friend_read_receipt_get_friend_number(event);
assert(friend_number == 0);
const uint32_t message_id = tox_event_friend_read_receipt_get_message_id(event);
uint32_t *done = static_cast<uint32_t *>(user_data);
*done = std::max(*done, message_id);
});
tox_events_callback_friend_request(
dispatch, [](const Tox_Event_Friend_Request *event, void *user_data) {
Tox *tox = static_cast<Tox *>(user_data);
Tox_Err_Friend_Add err;
tox_friend_add_norequest(tox, tox_event_friend_request_get_public_key(event), &err);
});
tox_events_callback_friend_status(
dispatch, [](const Tox_Event_Friend_Status *event, void *user_data) {
const uint32_t friend_number = tox_event_friend_status_get_friend_number(event);
assert(friend_number == 0);
});
tox_events_callback_friend_status_message(
dispatch, [](const Tox_Event_Friend_Status_Message *event, void *user_data) {
const uint32_t friend_number = tox_event_friend_status_message_get_friend_number(event);
assert(friend_number == 0);
});
tox_events_callback_friend_typing(
dispatch, [](const Tox_Event_Friend_Typing *event, void *user_data) {
const uint32_t friend_number = tox_event_friend_typing_get_friend_number(event);
assert(friend_number == 0);
assert(!tox_event_friend_typing_get_typing(event));
});
tox_events_callback_self_connection_status(
dispatch, [](const Tox_Event_Self_Connection_Status *event, void *user_data) {
// OK: we got connected.
});
}
void TestEndToEnd(Fuzz_Data &input)
{
/**
* Whether to abort the program if a friend connection can be established.
*
* This is useful to make the fuzzer produce minimal startup data so the
* interesting part of the fuzzer (the part that comes after the friend
* connection is established) can run sooner and thus more frequently.
*/
const bool PROTODUMP_REDUCE = getenv("PROTODUMP_REDUCE") != nullptr;
Fuzz_System sys(input);
Ptr<Tox_Options> opts(tox_options_new(nullptr), tox_options_free);
assert(opts != nullptr);
tox_options_set_local_discovery_enabled(opts.get(), false);
Tox_Options_Testing tox_options_testing;
tox_options_testing.operating_system = sys.sys.get();
tox_options_set_log_callback(opts.get(),
[](Tox *tox, Tox_Log_Level level, const char *file, uint32_t line, const char *func,
const char *message, void *user_data) {
// Log to stdout.
if (PROTODUMP_DEBUG) {
std::printf("[tox1] %c %s:%d(%s): %s\n", tox_log_level_name(level), file, line,
func, message);
}
});
Tox_Err_New error_new;
Tox_Err_New_Testing error_new_testing;
Tox *tox = tox_new_testing(opts.get(), &error_new, &tox_options_testing, &error_new_testing);
if (tox == nullptr) {
// It might fail, because some I/O happens in tox_new, and the fuzzer
// might do things that make that I/O fail.
return;
}
assert(error_new == TOX_ERR_NEW_OK);
assert(error_new_testing == TOX_ERR_NEW_TESTING_OK);
tox_events_init(tox);
Tox_Dispatch *dispatch = tox_dispatch_new(nullptr);
assert(dispatch != nullptr);
setup_callbacks(dispatch);
while (!input.empty()) {
Tox_Err_Events_Iterate error_iterate;
Tox_Events *events = tox_events_iterate(tox, true, &error_iterate);
tox_events_equal(tox_get_system(tox), events, events); // TODO(iphydf): assert?
tox_dispatch_invoke(dispatch, events, tox);
tox_events_free(events);
const uint8_t clock_increment = random_u08(sys.rng.get());
if (PROTODUMP_DEBUG) {
printf("clock increment: %d\n", clock_increment);
}
sys.clock += std::max(System::MIN_ITERATION_INTERVAL, clock_increment);
}
if (PROTODUMP_REDUCE) {
assert(tox_friend_get_connection_status(tox, 0, nullptr) != 2);
} else {
printf("friend: %d\n", tox_friend_get_connection_status(tox, 0, nullptr));
printf("self: %d\n", tox_self_get_connection_status(tox));
assert(tox_friend_get_connection_status(tox, 0, nullptr) == 2);
assert(input.empty());
}
tox_dispatch_free(dispatch);
tox_kill(tox);
}
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
Fuzz_Data input{data, size};
TestEndToEnd(input);
return 0; // Non-zero return values are reserved for future use.
}

View File

@@ -4,10 +4,14 @@
#include "../../toxcore/tox.h"
#include "../../toxcore/tox_private.h"
#include "fuzz_support.hh"
#include "../support/public/fuzz_data.hh"
#include "../support/public/simulated_environment.hh"
namespace {
using tox::test::Fuzz_Data;
using tox::test::SimulatedEnvironment;
void TestSaveDataLoading(Fuzz_Data &input)
{
Tox_Err_Options_New error_options;
@@ -27,10 +31,12 @@ void TestSaveDataLoading(Fuzz_Data &input)
tox_options_set_savedata_type(tox_options, TOX_SAVEDATA_TYPE_TOX_SAVE);
Tox_Options_Testing tox_options_testing;
Null_System sys;
tox_options_testing.operating_system = sys.sys.get();
SimulatedEnvironment env;
auto node = env.create_node(33445);
tox_options_testing.operating_system = &node->system;
Tox *tox = tox_new_testing(tox_options, nullptr, &tox_options_testing, nullptr);
Tox_Err_New_Testing err_testing;
Tox *tox = tox_new_testing(tox_options, nullptr, &tox_options_testing, &err_testing);
tox_options_free(tox_options);
if (tox == nullptr) {
// Tox save was invalid, we're finished here

111
testing/support/BUILD.bazel Normal file
View File

@@ -0,0 +1,111 @@
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")
cc_library(
name = "support",
srcs = [
"doubles/fake_network_stack.cc",
"doubles/fake_sockets.cc",
"doubles/network_universe.cc",
"src/clock.cc",
"src/environment.cc",
"src/fake_clock.cc",
"src/fake_memory.cc",
"src/fake_random.cc",
"src/fuzz_helpers.cc",
"src/memory.cc",
"src/network.cc",
"src/random.cc",
"src/simulated_environment.cc",
"src/simulation.cc",
"src/tox_network.cc",
],
hdrs = [
"doubles/fake_clock.hh",
"doubles/fake_memory.hh",
"doubles/fake_network_stack.hh",
"doubles/fake_random.hh",
"doubles/fake_sockets.hh",
"doubles/network_universe.hh",
"public/clock.hh",
"public/environment.hh",
"public/fuzz_data.hh",
"public/fuzz_helpers.hh",
"public/memory.hh",
"public/network.hh",
"public/random.hh",
"public/simulated_environment.hh",
"public/simulation.hh",
"public/tox_network.hh",
],
copts = select({
"//tools/config:windows": ["/wd4200"], # Zero-sized array in struct/union
"//conditions:default": [],
}),
visibility = ["//visibility:public"],
deps = [
"//c-toxcore/toxcore:mem",
"//c-toxcore/toxcore:network",
"//c-toxcore/toxcore:tox",
"//c-toxcore/toxcore:tox_memory",
"//c-toxcore/toxcore:tox_options",
"//c-toxcore/toxcore:tox_random",
"@psocket",
],
)
cc_test(
name = "fake_sockets_test",
srcs = ["doubles/fake_sockets_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"],
deps = [
":support",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
"@psocket",
],
)
cc_test(
name = "network_universe_test",
srcs = ["doubles/network_universe_test.cc"],
deps = [
":support",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
"@psocket",
],
)
cc_test(
name = "bootstrap_scaling_test",
srcs = ["bootstrap_scaling_test.cc"],
deps = [
":support",
"//c-toxcore/toxcore:network",
"//c-toxcore/toxcore:tox",
"@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:tox",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)

View File

@@ -0,0 +1,72 @@
if(NOT UNITTEST)
return()
endif()
set(support_SOURCES
doubles/fake_network_stack.cc
doubles/fake_sockets.cc
doubles/network_universe.cc
src/clock.cc
src/environment.cc
src/fake_clock.cc
src/fake_memory.cc
src/fake_random.cc
src/fuzz_helpers.cc
src/memory.cc
src/network.cc
src/random.cc
src/simulated_environment.cc
src/simulation.cc
src/tox_network.cc
doubles/fake_clock.hh
doubles/fake_memory.hh
doubles/fake_network_stack.hh
doubles/fake_random.hh
doubles/fake_sockets.hh
doubles/network_universe.hh
public/clock.hh
public/environment.hh
public/fuzz_data.hh
public/fuzz_helpers.hh
public/memory.hh
public/network.hh
public/random.hh
public/simulated_environment.hh
public/simulation.hh
public/tox_network.hh
)
add_library(support STATIC ${support_SOURCES})
if(TARGET toxcore_static)
target_link_libraries(support PRIVATE toxcore_static)
else()
target_link_libraries(support PRIVATE toxcore_shared)
endif()
if(TARGET pthreads4w::pthreads4w)
target_link_libraries(support PUBLIC pthreads4w::pthreads4w)
elseif(TARGET PThreads4W::PThreads4W)
target_link_libraries(support PUBLIC PThreads4W::PThreads4W)
elseif(TARGET Threads::Threads)
target_link_libraries(support PUBLIC Threads::Threads)
endif()
if(TARGET GTest::gtest_main)
function(support_test target source)
add_executable(${target} ${source})
target_link_libraries(${target} PRIVATE support GTest::gtest_main)
if(TARGET toxcore_static)
target_link_libraries(${target} PRIVATE toxcore_static)
else()
target_link_libraries(${target} PRIVATE toxcore_shared)
endif()
add_test(NAME ${target} COMMAND ${target})
endfunction()
support_test(fake_sockets_test doubles/fake_sockets_test.cc)
support_test(fake_network_stack_test doubles/fake_network_stack_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)
endif()

View File

@@ -0,0 +1,109 @@
// clang-format off
#include "public/simulation.hh"
// clang-format on
#include <gtest/gtest.h>
#include <iostream>
#include <memory>
#include <vector>
#include "../../toxcore/network.h"
#include "../../toxcore/tox.h"
namespace tox::test {
namespace {
class BootstrapScalingTest : public ::testing::Test {
protected:
Simulation sim;
};
TEST_F(BootstrapScalingTest, TwentyNodes)
{
sim.net().set_verbose(false);
const int num_nodes = 20;
std::vector<std::unique_ptr<SimulatedNode>> nodes;
std::vector<SimulatedNode::ToxPtr> toxes;
std::cerr << "[Test] Creating " << num_nodes << " nodes..." << std::endl;
// Create all nodes and tox instances
for (int i = 0; i < num_nodes; ++i) {
auto node = sim.create_node();
auto tox = node->create_tox();
ASSERT_NE(tox, nullptr) << "Failed to create Tox instance " << i;
nodes.push_back(std::move(node));
toxes.push_back(std::move(tox));
}
// Node 0 is the bootstrap target
uint8_t bootstrap_dht_id[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(toxes[0].get(), bootstrap_dht_id);
FakeUdpSocket *main_sock = nodes[0]->get_primary_socket();
ASSERT_NE(main_sock, nullptr) << "Node 0 has no primary socket";
uint16_t bootstrap_port = main_sock->local_port();
char bootstrap_ip[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&nodes[0]->ip, bootstrap_ip, sizeof(bootstrap_ip));
std::cerr << "[Test] Bootstrapping to Node 0 at " << bootstrap_ip << ":" << bootstrap_port
<< std::endl;
size_t total_packets = 0;
sim.net().add_observer([&](const Packet &) { total_packets++; });
// Everyone else bootstraps to Node 0
for (int i = 1; i < num_nodes; ++i) {
Tox_Err_Bootstrap err;
bool success = tox_bootstrap(
toxes[i].get(), bootstrap_ip, bootstrap_port, bootstrap_dht_id, &err);
ASSERT_TRUE(success) << "Bootstrap call failed for node " << i << ": " << err;
}
// Run simulation until everyone is connected or timeout
const uint64_t virtual_timeout_ms = 300000;
bool all_connected = false;
sim.run_until(
[&]() {
all_connected = true;
size_t connected_count = 0;
for (int i = 0; i < num_nodes; ++i) {
tox_iterate(toxes[i].get(), nullptr);
if (tox_self_get_connection_status(toxes[i].get()) != TOX_CONNECTION_NONE) {
connected_count++;
} else {
all_connected = false;
}
}
static uint64_t last_print = 0;
if (sim.clock().current_time_ms() - last_print > 5000) {
std::cerr << "[Test] DHT connected: " << connected_count << "/" << num_nodes
<< " at " << sim.clock().current_time_ms()
<< "ms. Total packets: " << total_packets << std::endl;
last_print = sim.clock().current_time_ms();
}
return all_connected;
},
virtual_timeout_ms);
if (!all_connected) {
std::cerr << "[Test] Bootstrap failed. Status of nodes:" << std::endl;
for (int i = 0; i < num_nodes; ++i) {
Tox_Connection status = tox_self_get_connection_status(toxes[i].get());
if (status == TOX_CONNECTION_NONE) {
std::cerr << " Node " << i << " is NOT connected." << std::endl;
}
}
}
EXPECT_TRUE(all_connected)
<< "Only " << (num_nodes - (all_connected ? 0 : 1)) << " nodes connected? Check logs.";
}
} // namespace
} // namespace tox::test

View File

@@ -0,0 +1,23 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_CLOCK_H
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_CLOCK_H
#include "../public/clock.hh"
namespace tox::test {
class FakeClock : public ClockSystem {
public:
explicit FakeClock(uint64_t start_time_ms = 1000);
uint64_t current_time_ms() const override;
uint64_t current_time_s() const override;
void advance(uint64_t ms);
private:
uint64_t now_ms_;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_CLOCK_H

View File

@@ -0,0 +1,51 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_MEMORY_H
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_MEMORY_H
#include <functional>
#include "../public/memory.hh"
// Forward declaration
struct Tox_Memory;
namespace tox::test {
class FakeMemory : public MemorySystem {
public:
using FailureInjector = std::function<bool(size_t size)>; // Return true to fail
using Observer = std::function<void(bool success)>;
FakeMemory();
~FakeMemory() override;
void *malloc(size_t size) override;
void *realloc(void *ptr, size_t size) override;
void free(void *ptr) override;
// Configure failure injection
void set_failure_injector(FailureInjector injector);
// Configure observer
void set_observer(Observer observer);
// Get the C-compatible struct
struct Tox_Memory get_c_memory();
private:
struct Header {
size_t size;
size_t magic;
};
static constexpr size_t kMagic = 0xDEADC0DE;
static constexpr size_t kFreeMagic = 0xBAADF00D;
size_t current_allocation_ = 0;
size_t max_allocation_ = 0;
FailureInjector failure_injector_;
Observer observer_;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_MEMORY_H

View File

@@ -0,0 +1,299 @@
#include "fake_network_stack.hh"
#include <cerrno>
#include <cstring>
#include <iostream>
#include "../../../toxcore/mem.h"
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); },
.listen
= [](void *obj, Socket sock,
int backlog) { return static_cast<FakeNetworkStack *>(obj)->listen(sock, backlog); },
.connect =
[](void *obj, Socket sock, const IP_Port *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,
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) {
return static_cast<FakeNetworkStack *>(obj)->recvfrom(sock, buf, len, addr);
},
.send = [](void *obj, Socket sock, const uint8_t *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) {
return static_cast<FakeNetworkStack *>(obj)->sendto(sock, buf, len, addr);
},
.socket
= [](void *obj, int domain, int type,
int proto) { return static_cast<FakeNetworkStack *>(obj)->socket(domain, type, proto); },
.socket_nonblock =
[](void *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) {
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) {
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) {
FakeNetworkStack *self = static_cast<FakeNetworkStack *>(obj);
if (self->universe().is_verbose()) {
std::cerr << "[FakeNetworkStack] getaddrinfo for " << address << std::endl;
}
if (strcmp(address, "127.0.0.1") == 0 || strcmp(address, "localhost") == 0) {
*addrs = static_cast<IP_Port *>(mem_alloc(mem, sizeof(IP_Port)));
memset(&(*addrs)->ip, 0, sizeof(IP));
ip_init(&(*addrs)->ip, false);
(*addrs)->ip.ip.v4.uint32 = net_htonl(0x7F000001);
(*addrs)->port = 0;
return 1;
}
IP ip;
if (addr_parse_ip(address, &ip)) {
*addrs = static_cast<IP_Port *>(mem_alloc(mem, sizeof(IP_Port)));
(*addrs)->ip = ip;
(*addrs)->port = 0;
if (self->universe().is_verbose()) {
std::cerr << "[FakeNetworkStack] resolved " << address << std::endl;
}
return 1;
}
return 0;
},
.freeaddrinfo =
[](void *obj, const Memory *mem, IP_Port *addrs) {
mem_delete(mem, addrs);
return 0;
},
};
FakeNetworkStack::FakeNetworkStack(NetworkUniverse &universe, const IP &node_ip)
: universe_(universe)
, node_ip_(node_ip)
{
}
FakeNetworkStack::~FakeNetworkStack() = default;
struct Network FakeNetworkStack::get_c_network() { return Network{&kVtable, this}; }
Socket FakeNetworkStack::socket(int domain, int type, int protocol)
{
std::lock_guard<std::mutex> lock(mutex_);
int fd = next_fd_++;
std::unique_ptr<FakeSocket> sock;
if (type == SOCK_DGRAM) {
if (universe_.is_verbose()) {
std::cerr << "[FakeNetworkStack] create UDP socket fd=" << fd << std::endl;
}
sock = std::make_unique<FakeUdpSocket>(universe_);
} else if (type == SOCK_STREAM) {
if (universe_.is_verbose()) {
std::cerr << "[FakeNetworkStack] create TCP socket fd=" << fd << std::endl;
}
sock = std::make_unique<FakeTcpSocket>(universe_);
} else {
// Unknown type
return net_socket_from_native(-1);
}
sockets_[fd] = std::move(sock);
sockets_[fd]->set_ip(node_ip_);
return net_socket_from_native(fd);
}
FakeSocket *FakeNetworkStack::get_sock(Socket sock)
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = sockets_.find(net_socket_to_native(sock));
if (it != sockets_.end()) {
return it->second.get();
}
return nullptr;
}
int FakeNetworkStack::close(Socket sock)
{
std::lock_guard<std::mutex> lock(mutex_);
int fd = net_socket_to_native(sock);
auto it = sockets_.find(fd);
if (it == sockets_.end()) {
errno = EBADF;
return -1;
}
it->second->close();
sockets_.erase(it);
return 0;
}
// Delegate all others
int FakeNetworkStack::bind(Socket sock, const IP_Port *addr)
{
if (auto *s = get_sock(sock)) {
int ret = s->bind(addr);
if (universe_.is_verbose() && ret == 0) {
char ip_str[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&s->ip_address(), ip_str, sizeof(ip_str));
std::cerr << "[FakeNetworkStack] bound socket to " << ip_str << ":" << s->local_port()
<< std::endl;
}
return ret;
}
errno = EBADF;
return -1;
}
int FakeNetworkStack::connect(Socket sock, const IP_Port *addr)
{
if (auto *s = get_sock(sock))
return s->connect(addr);
errno = EBADF;
return -1;
}
int FakeNetworkStack::listen(Socket sock, int backlog)
{
if (auto *s = get_sock(sock))
return s->listen(backlog);
errno = EBADF;
return -1;
}
Socket FakeNetworkStack::accept(Socket sock)
{
// This requires creating a new FD
IP_Port addr;
std::unique_ptr<FakeSocket> new_sock_obj;
{
auto *s = get_sock(sock);
if (!s) {
errno = EBADF;
return net_socket_from_native(-1);
}
new_sock_obj = s->accept(&addr);
}
if (!new_sock_obj) {
// errno set by accept
return net_socket_from_native(-1);
}
std::lock_guard<std::mutex> lock(mutex_);
int fd = next_fd_++;
sockets_[fd] = std::move(new_sock_obj);
return net_socket_from_native(fd);
}
int FakeNetworkStack::send(Socket sock, const uint8_t *buf, size_t len)
{
if (auto *s = get_sock(sock))
return s->send(buf, len);
errno = EBADF;
return -1;
}
int FakeNetworkStack::recv(Socket sock, uint8_t *buf, size_t len)
{
if (auto *s = get_sock(sock))
return s->recv(buf, len);
errno = EBADF;
return -1;
}
int FakeNetworkStack::recvbuf(Socket sock)
{
if (auto *s = get_sock(sock))
return s->recv_buffer_size();
errno = EBADF;
return -1;
}
int FakeNetworkStack::sendto(Socket sock, const uint8_t *buf, size_t len, const IP_Port *addr)
{
if (auto *s = get_sock(sock))
return s->sendto(buf, len, addr);
errno = EBADF;
return -1;
}
int FakeNetworkStack::recvfrom(Socket sock, uint8_t *buf, size_t len, IP_Port *addr)
{
if (auto *s = get_sock(sock))
return s->recvfrom(buf, len, addr);
errno = EBADF;
return -1;
}
int FakeNetworkStack::socket_nonblock(Socket sock, bool nonblock)
{
if (auto *s = get_sock(sock))
return s->socket_nonblock(nonblock);
errno = EBADF;
return -1;
}
int FakeNetworkStack::getsockopt(Socket sock, int level, int optname, void *optval, size_t *optlen)
{
if (auto *s = get_sock(sock))
return s->getsockopt(level, optname, optval, optlen);
errno = EBADF;
return -1;
}
int FakeNetworkStack::setsockopt(
Socket sock, int level, int optname, const void *optval, size_t optlen)
{
if (auto *s = get_sock(sock))
return s->setsockopt(level, optname, optval, optlen);
errno = EBADF;
return -1;
}
FakeUdpSocket *FakeNetworkStack::get_udp_socket(Socket sock)
{
if (auto *s = get_sock(sock)) {
if (s->type() == SOCK_DGRAM) {
return static_cast<FakeUdpSocket *>(s);
}
}
return nullptr;
}
std::vector<FakeUdpSocket *> FakeNetworkStack::get_bound_udp_sockets()
{
std::lock_guard<std::mutex> lock(mutex_);
std::vector<FakeUdpSocket *> result;
for (const auto &pair : sockets_) {
FakeSocket *s = pair.second.get();
if (s->type() == SOCK_DGRAM && s->local_port() != 0) {
result.push_back(static_cast<FakeUdpSocket *>(s));
}
}
return result;
}
} // namespace tox::test

View File

@@ -0,0 +1,56 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_NETWORK_STACK_H
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_NETWORK_STACK_H
#include <map>
#include <mutex>
#include "../public/network.hh"
#include "fake_sockets.hh"
#include "network_universe.hh"
namespace tox::test {
class FakeNetworkStack : public NetworkSystem {
public:
explicit FakeNetworkStack(NetworkUniverse &universe, const IP &node_ip);
~FakeNetworkStack() override;
// NetworkSystem Implementation
Socket socket(int domain, int type, int protocol) override;
int bind(Socket sock, const IP_Port *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 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 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;
struct Network get_c_network();
// For testing/fuzzing introspection
FakeUdpSocket *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;
IP node_ip_;
std::mutex mutex_;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_NETWORK_STACK_H

View File

@@ -0,0 +1,91 @@
#include "fake_network_stack.hh"
#include <gtest/gtest.h>
#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 FakeNetworkStackTest : public ::testing::Test {
public:
FakeNetworkStackTest()
: stack{universe, make_ip(0x7F000001)}
{
}
~FakeNetworkStackTest() override;
protected:
NetworkUniverse universe;
FakeNetworkStack stack;
};
FakeNetworkStackTest::~FakeNetworkStackTest() = default;
TEST_F(FakeNetworkStackTest, SocketCreationAndLifecycle)
{
Socket udp_sock = stack.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
ASSERT_NE(net_socket_to_native(udp_sock), -1);
// Check introspection
ASSERT_NE(stack.get_udp_socket(udp_sock), nullptr);
// Bind
IP_Port addr;
ip_init(&addr.ip, false);
addr.ip.ip.v4.uint32 = 0;
addr.port = net_htons(9002);
ASSERT_EQ(stack.bind(udp_sock, &addr), 0);
// Check introspection again
auto sockets = stack.get_bound_udp_sockets();
ASSERT_EQ(sockets.size(), 1);
EXPECT_EQ(sockets[0]->local_port(), 9002);
ASSERT_EQ(stack.close(udp_sock), 0);
ASSERT_EQ(stack.get_bound_udp_sockets().size(), 0);
}
TEST_F(FakeNetworkStackTest, TcpSocketThroughStack)
{
Socket tcp_sock = stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ASSERT_NE(net_socket_to_native(tcp_sock), -1);
IP_Port addr;
ip_init(&addr.ip, false);
addr.ip.ip.v4.uint32 = 0;
addr.port = net_htons(9003);
ASSERT_EQ(stack.bind(tcp_sock, &addr), 0);
ASSERT_EQ(stack.listen(tcp_sock, 5), 0);
// Connect from another stack
FakeNetworkStack client_stack{universe, make_ip(0x7F000002)};
Socket client_sock = 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); // Localhost
server_addr.port = net_htons(9003);
ASSERT_EQ(client_stack.connect(client_sock, &server_addr), -1);
ASSERT_EQ(errno, EINPROGRESS);
universe.process_events(0); // SYN
universe.process_events(0); // SYN-ACK
universe.process_events(0); // ACK
Socket accepted = stack.accept(tcp_sock);
ASSERT_NE(net_socket_to_native(accepted), -1);
}
} // namespace
} // namespace tox::test

View File

@@ -0,0 +1,45 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_RANDOM_H
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_RANDOM_H
#include <functional>
#include <random>
#include "../public/random.hh"
// Forward declaration
struct Tox_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)>;
explicit FakeRandom(uint64_t seed);
uint32_t uniform(uint32_t upper_bound) override;
void bytes(uint8_t *out, size_t count) override;
/**
* @brief Set a custom entropy source.
* If set, this function will be called to generate random bytes instead of the PRNG.
*/
void set_entropy_source(EntropySource source);
/**
* @brief Set an observer to record generated bytes.
*/
void set_observer(Observer observer);
struct Tox_Random get_c_random();
private:
std::minstd_rand rng_;
EntropySource entropy_source_;
Observer observer_;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_RANDOM_H

View File

@@ -0,0 +1,485 @@
#include "fake_sockets.hh"
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <iostream>
#include "network_universe.hh"
namespace tox::test {
// --- FakeSocket ---
FakeSocket::FakeSocket(NetworkUniverse &universe, int type)
: universe_(universe)
, type_(type)
{
ip_init(&ip_, false);
ip_.ip.v4.uint32 = net_htonl(0x7F000001);
}
FakeSocket::~FakeSocket() = default;
int FakeSocket::close()
{
// Override in subclasses to unbind
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::socket_nonblock(bool nonblock)
{
nonblocking_ = nonblock;
return 0;
}
// --- FakeUdpSocket ---
FakeUdpSocket::FakeUdpSocket(NetworkUniverse &universe)
: FakeSocket(universe, SOCK_DGRAM)
{
}
FakeUdpSocket::~FakeUdpSocket() { close_impl(); }
int FakeUdpSocket::close()
{
std::lock_guard<std::mutex> lock(mutex_);
close_impl();
return 0;
}
void FakeUdpSocket::close_impl()
{
if (local_port_ != 0) {
universe_.unbind_udp(ip_, local_port_);
local_port_ = 0;
}
}
int FakeUdpSocket::bind(const IP_Port *addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (local_port_ != 0)
return -1; // Already bound
uint16_t port = addr->port;
if (port == 0) {
port = universe_.find_free_port(ip_);
} else {
port = net_ntohs(port);
}
if (universe_.bind_udp(ip_, port, this)) {
local_port_ = port;
return 0;
}
errno = EADDRINUSE;
return -1;
}
int FakeUdpSocket::connect(const IP_Port *addr)
{
// UDP connect just sets default dest.
// Not strictly needed for toxcore UDP but good for completeness.
return 0;
}
int FakeUdpSocket::listen(int backlog)
{
errno = EOPNOTSUPP;
return -1;
}
std::unique_ptr<FakeSocket> FakeUdpSocket::accept(IP_Port *addr)
{
errno = EOPNOTSUPP;
return nullptr;
}
int FakeUdpSocket::send(const uint8_t *buf, size_t len)
{
errno = EDESTADDRREQ;
return -1;
}
int FakeUdpSocket::recv(uint8_t *buf, size_t len)
{
errno = EOPNOTSUPP;
return -1;
}
int FakeUdpSocket::sendto(const uint8_t *buf, size_t len, const IP_Port *addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (local_port_ == 0) {
// Implicit bind
uint16_t p = universe_.find_free_port(ip_);
if (universe_.bind_udp(ip_, p, this)) {
local_port_ = p;
} else {
errno = EADDRINUSE;
return -1;
}
}
Packet p{};
// Source
p.from.ip = ip_;
p.from.port = net_htons(local_port_);
p.to = *addr;
p.data.assign(buf, buf + len);
p.is_tcp = false;
universe_.send_packet(p);
if (universe_.is_verbose()) {
uint32_t tip4 = net_ntohl(addr->ip.ip.v4.uint32);
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;
}
return len;
}
int FakeUdpSocket::recvfrom(uint8_t *buf, size_t len, IP_Port *addr)
{
RecvObserver observer_copy;
std::vector<uint8_t> data_copy;
IP_Port from_copy;
size_t copy_len = 0;
{
std::lock_guard<std::mutex> lock(mutex_);
if (recv_queue_.empty() && packet_source_) {
// NOTE: We call packet_source_ with lock held.
// Be careful not to call back into socket methods from packet_source_.
std::vector<uint8_t> data;
IP_Port from;
if (packet_source_(data, from)) {
recv_queue_.push_back({std::move(data), from});
}
}
if (recv_queue_.empty()) {
errno = EWOULDBLOCK;
return -1;
}
auto &p = recv_queue_.front();
copy_len = std::min(len, p.data.size());
std::memcpy(buf, p.data.data(), copy_len);
*addr = p.from;
if (recv_observer_) {
observer_copy = recv_observer_;
data_copy = p.data;
from_copy = p.from;
}
recv_queue_.pop_front();
}
if (observer_copy) {
observer_copy(data_copy, from_copy);
}
if (universe_.is_verbose()) {
std::cerr << "[FakeUdpSocket] recv " << copy_len << " bytes at port " << local_port_
<< " from port " << net_ntohs(addr->port) << std::endl;
}
return copy_len;
}
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);
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;
}
recv_queue_.push_back({std::move(data), from});
}
void FakeUdpSocket::set_packet_source(PacketSource source)
{
std::lock_guard<std::mutex> lock(mutex_);
packet_source_ = std::move(source);
}
void FakeUdpSocket::set_recv_observer(RecvObserver observer)
{
std::lock_guard<std::mutex> lock(mutex_);
recv_observer_ = std::move(observer);
}
// --- FakeTcpSocket ---
FakeTcpSocket::FakeTcpSocket(NetworkUniverse &universe)
: FakeSocket(universe, SOCK_STREAM)
, remote_addr_{}
{
}
FakeTcpSocket::~FakeTcpSocket() { close_impl(); }
int FakeTcpSocket::close()
{
std::lock_guard<std::mutex> lock(mutex_);
close_impl();
return 0;
}
void FakeTcpSocket::close_impl()
{
if (local_port_ != 0) {
universe_.unbind_tcp(ip_, local_port_, this);
local_port_ = 0;
}
state_ = CLOSED;
}
int FakeTcpSocket::bind(const IP_Port *addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (local_port_ != 0)
return -1;
uint16_t port = addr->port;
if (port == 0) {
port = universe_.find_free_port(ip_);
} else {
port = net_ntohs(port);
}
if (universe_.bind_tcp(ip_, port, this)) {
local_port_ = port;
return 0;
}
errno = EADDRINUSE;
return -1;
}
int FakeTcpSocket::listen(int backlog)
{
std::lock_guard<std::mutex> lock(mutex_);
state_ = LISTEN;
backlog_ = backlog;
return 0;
}
int FakeTcpSocket::connect(const IP_Port *addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (local_port_ == 0) {
// Implicit bind
uint16_t p = universe_.find_free_port(ip_);
if (universe_.bind_tcp(ip_, p, this)) {
local_port_ = p;
} else {
errno = EADDRINUSE;
return -1;
}
}
remote_addr_ = *addr;
state_ = SYN_SENT;
Packet p{};
p.from.ip = ip_;
p.from.port = net_htons(local_port_);
p.to = *addr;
p.is_tcp = true;
p.tcp_flags = 0x02; // SYN
p.seq = next_seq_;
universe_.send_packet(p);
// Non-blocking connect not fully simulated (we return 0 but state is SYN_SENT).
// Real connect() blocks or returns EINPROGRESS.
// For simplicity, we assume the test will pump events until connected.
errno = EINPROGRESS;
return -1;
}
std::unique_ptr<FakeSocket> FakeTcpSocket::accept(IP_Port *addr)
{
std::lock_guard<std::mutex> lock(mutex_);
if (state_ != LISTEN) {
errno = EINVAL;
return nullptr;
}
if (pending_connections_.empty()) {
errno = EWOULDBLOCK;
return nullptr;
}
auto client = std::move(pending_connections_.front());
pending_connections_.pop_front();
if (addr) {
*addr = client->remote_addr();
}
return client;
}
int FakeTcpSocket::send(const uint8_t *buf, size_t len)
{
std::lock_guard<std::mutex> lock(mutex_);
if (state_ != ESTABLISHED) {
errno = ENOTCONN;
return -1;
}
// Wrap as TCP packet
Packet p{};
// Source
p.from.ip = ip_;
p.from.port = net_htons(local_port_);
p.to = remote_addr_;
p.data.assign(buf, buf + len);
p.is_tcp = true;
p.tcp_flags = 0x10; // ACK (Data packets usually have ACK)
p.seq = next_seq_;
p.ack = last_ack_;
next_seq_ += len;
universe_.send_packet(p);
return len;
}
int FakeTcpSocket::recv(uint8_t *buf, size_t len)
{
std::lock_guard<std::mutex> lock(mutex_);
if (recv_buffer_.empty()) {
if (state_ == CLOSED || state_ == CLOSE_WAIT)
return 0; // EOF
errno = EWOULDBLOCK;
return -1;
}
size_t actual = std::min(len, recv_buffer_.size());
for (size_t i = 0; i < actual; ++i) {
buf[i] = recv_buffer_.front();
recv_buffer_.pop_front();
}
return actual;
}
size_t FakeTcpSocket::recv_buffer_size()
{
std::lock_guard<std::mutex> lock(mutex_);
return recv_buffer_.size();
}
int FakeTcpSocket::sendto(const uint8_t *buf, size_t len, const IP_Port *addr)
{
errno = EOPNOTSUPP;
return -1;
}
int FakeTcpSocket::recvfrom(uint8_t *buf, size_t len, IP_Port *addr)
{
errno = EOPNOTSUPP;
return -1;
}
void 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;
}
if (state_ == LISTEN) {
if (p.tcp_flags & 0x02) { // SYN
// 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->last_ack_ = p.seq + 1;
new_sock->next_seq_ = 1000; // Random ISN
universe_.bind_tcp(ip_, local_port_, new_sock.get());
// 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 = new_sock->next_seq_++;
resp.ack = new_sock->last_ack_;
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;
pending_connections_.push_back(std::move(new_sock));
}
} else if (state_ == SYN_SENT) {
if ((p.tcp_flags & 0x12) == 0x12) { // SYN | ACK
state_ = ESTABLISHED;
last_ack_ = p.seq + 1;
next_seq_++; // Consumer SYN
// Send ACK
Packet ack{};
ack.from = p.to;
ack.to = p.from;
ack.is_tcp = true;
ack.tcp_flags = 0x10; // ACK
ack.seq = next_seq_;
ack.ack = last_ack_;
universe_.send_packet(ack);
}
} else if (state_ == ESTABLISHED) {
if (p.tcp_flags & 0x01) { // FIN
state_ = CLOSE_WAIT;
// Send ACK
Packet ack{};
ack.from = p.to;
ack.to = p.from;
ack.is_tcp = true;
ack.tcp_flags = 0x10; // ACK
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?
}
}
}
std::unique_ptr<FakeTcpSocket> FakeTcpSocket::create_connected(
NetworkUniverse &universe, const IP_Port &remote, uint16_t local_port)
{
auto s = std::make_unique<FakeTcpSocket>(universe);
s->state_ = ESTABLISHED;
s->remote_addr_ = remote;
s->local_port_ = local_port;
return s;
}
} // namespace tox::test

View File

@@ -0,0 +1,165 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_SOCKETS_H
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_SOCKETS_H
#include <atomic>
#include <cstdint>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <vector>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <winsock2.h>
#else
#include <sys/socket.h>
#endif
#include "../../../toxcore/network.h"
namespace tox::test {
class NetworkUniverse;
struct Packet;
/**
* @brief Abstract base class for all fake sockets.
*/
class FakeSocket {
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 listen(int backlog) = 0;
virtual std::unique_ptr<FakeSocket> accept(IP_Port *addr) = 0;
virtual int send(const uint8_t *buf, size_t len) = 0;
virtual int recv(uint8_t *buf, size_t len) = 0;
virtual size_t recv_buffer_size() { return 0; }
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 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 socket_nonblock(bool nonblock);
virtual int close();
bool is_nonblocking() const { return nonblocking_; }
int type() const { return type_; }
uint16_t local_port() const { return local_port_; }
const IP &ip_address() const { return ip_; }
void set_ip(const IP &ip) { ip_ = ip; }
protected:
NetworkUniverse &universe_;
int type_;
bool nonblocking_ = false;
uint16_t local_port_ = 0;
IP ip_;
std::mutex mutex_; // Use regular mutex (recursive_mutex not fully supported in MinGW)
};
/**
* @brief Implements UDP logic.
*/
class FakeUdpSocket : public FakeSocket {
public:
explicit FakeUdpSocket(NetworkUniverse &universe);
~FakeUdpSocket() override;
int bind(const IP_Port *addr) override;
int connect(const IP_Port *addr) override;
int listen(int backlog) override;
std::unique_ptr<FakeSocket> accept(IP_Port *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 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;
// Called by Universe to deliver a packet
void push_packet(std::vector<uint8_t> data, IP_Port from);
using PacketSource = std::function<bool(std::vector<uint8_t> &data, IP_Port &from)>;
void set_packet_source(PacketSource source);
using RecvObserver = std::function<void(const std::vector<uint8_t> &data, const IP_Port &from)>;
void set_recv_observer(RecvObserver observer);
private:
void close_impl();
struct PendingPacket {
std::vector<uint8_t> data;
IP_Port from;
};
std::deque<PendingPacket> recv_queue_;
PacketSource packet_source_;
RecvObserver recv_observer_;
};
/**
* @brief Implements TCP logic (Simplified for simulation).
*
* Supports:
* - 3-way handshake simulation (instant or delayed)
* - Stream buffering
* - Connection states
*/
class FakeTcpSocket : public FakeSocket {
public:
enum State { CLOSED, LISTEN, SYN_SENT, SYN_RECEIVED, ESTABLISHED, CLOSE_WAIT };
explicit FakeTcpSocket(NetworkUniverse &universe);
~FakeTcpSocket() override;
int bind(const IP_Port *addr) override;
int connect(const IP_Port *addr) override;
int listen(int backlog) override;
std::unique_ptr<FakeSocket> accept(IP_Port *addr) override;
int close() override;
int send(const uint8_t *buf, size_t len) override;
int recv(uint8_t *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;
// Internal events
void handle_packet(const Packet &p);
State state() const { return state_; }
const IP_Port &remote_addr() const { return remote_addr_; }
// Factory for accepting connections
static std::unique_ptr<FakeTcpSocket> create_connected(
NetworkUniverse &universe, const IP_Port &remote, uint16_t local_port);
private:
void close_impl();
State state_ = CLOSED;
IP_Port remote_addr_;
std::deque<uint8_t> recv_buffer_;
std::deque<std::unique_ptr<FakeTcpSocket>> pending_connections_;
int backlog_ = 0;
uint32_t next_seq_ = 0;
uint32_t last_ack_ = 0;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_SOCKETS_H

View File

@@ -0,0 +1,125 @@
#include "fake_sockets.hh"
#include <gtest/gtest.h>
#include "network_universe.hh"
namespace tox::test {
namespace {
class FakeTcpSocketTest : public ::testing::Test {
public:
~FakeTcpSocketTest() override;
protected:
NetworkUniverse universe;
FakeTcpSocket server{universe};
FakeTcpSocket client{universe};
};
FakeTcpSocketTest::~FakeTcpSocketTest() = default;
TEST_F(FakeTcpSocketTest, ConnectAndAccept)
{
IP_Port server_addr;
ip_init(&server_addr.ip, false);
server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
server_addr.port = net_htons(8080);
ASSERT_EQ(server.bind(&server_addr), 0);
ASSERT_EQ(server.listen(5), 0);
// Client connects
ASSERT_EQ(client.connect(&server_addr), -1);
ASSERT_EQ(errno, EINPROGRESS);
// Process events (Client SYN -> Server)
universe.process_events(0);
// Server accepts (SYN-ACK -> Client)
universe.process_events(0);
// Client receives SYN-ACK, sends ACK (ACK -> Server)
universe.process_events(0);
// Server receives ACK, connection established
IP_Port client_addr;
auto accepted = server.accept(&client_addr);
ASSERT_NE(accepted, nullptr);
auto *accepted_tcp = static_cast<FakeTcpSocket *>(accepted.get());
EXPECT_EQ(accepted_tcp->state(), FakeTcpSocket::ESTABLISHED);
EXPECT_EQ(client.state(), FakeTcpSocket::ESTABLISHED);
}
TEST_F(FakeTcpSocketTest, DataTransfer)
{
IP_Port server_addr;
ip_init(&server_addr.ip, false);
server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
server_addr.port = net_htons(8081);
server.bind(&server_addr);
server.listen(5);
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);
// Send data Client -> Server
uint8_t send_buf[] = "Hello";
ASSERT_EQ(client.send(send_buf, 5), 5);
universe.process_events(0); // Data packet
uint8_t recv_buf[10];
ASSERT_EQ(accepted->recv(recv_buf, 10), 5);
EXPECT_EQ(std::string(reinterpret_cast<char *>(recv_buf), 5), "Hello");
}
class FakeUdpSocketTest : public ::testing::Test {
public:
~FakeUdpSocketTest() override;
protected:
NetworkUniverse universe;
FakeUdpSocket server{universe};
FakeUdpSocket client{universe};
};
FakeUdpSocketTest::~FakeUdpSocketTest() = default;
TEST_F(FakeUdpSocketTest, BindAndSendTo)
{
IP_Port server_addr;
ip_init(&server_addr.ip, false);
server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
server_addr.port = net_htons(9000);
ASSERT_EQ(server.bind(&server_addr), 0);
const char *message = "UDP Packet";
ASSERT_EQ(client.sendto(
reinterpret_cast<const uint8_t *>(message), strlen(message), &server_addr),
strlen(message));
universe.process_events(0);
IP_Port sender_addr;
uint8_t recv_buf[100];
int len = server.recvfrom(recv_buf, sizeof(recv_buf), &sender_addr);
ASSERT_GT(len, 0);
EXPECT_EQ(std::string(reinterpret_cast<char *>(recv_buf), len), message);
EXPECT_EQ(sender_addr.port, net_htons(client.local_port()));
}
} // namespace
} // namespace tox::test
// end of file

View File

@@ -0,0 +1,145 @@
#include "network_universe.hh"
#include <cstring>
#include <iostream>
#include "fake_sockets.hh"
namespace tox::test {
bool NetworkUniverse::IP_Port_Key::operator<(const IP_Port_Key &other) const
{
if (port != other.port)
return port < other.port;
if (ip.family.value != other.ip.family.value)
return ip.family.value < other.ip.family.value;
if (net_family_is_ipv4(ip.family)) {
return ip.ip.v4.uint32 < other.ip.ip.v4.uint32;
}
return std::memcmp(&ip.ip.v6, &other.ip.ip.v6, sizeof(ip.ip.v6)) < 0;
}
NetworkUniverse::NetworkUniverse() { }
NetworkUniverse::~NetworkUniverse() { }
bool NetworkUniverse::bind_udp(IP ip, uint16_t port, FakeUdpSocket *socket)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
IP_Port_Key key{ip, port};
if (udp_bindings_.count(key))
return false;
udp_bindings_[key] = socket;
return true;
}
void NetworkUniverse::unbind_udp(IP ip, uint16_t port)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
udp_bindings_.erase({ip, port});
}
bool NetworkUniverse::bind_tcp(IP ip, uint16_t port, FakeTcpSocket *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)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
auto range = tcp_bindings_.equal_range({ip, port});
for (auto it = range.first; it != range.second; ++it) {
if (it->second == socket) {
tcp_bindings_.erase(it);
break;
}
}
}
void NetworkUniverse::send_packet(Packet p)
{
// Apply filters
for (const auto &filter : filters_) {
if (!filter(p))
return;
}
// Notify observers
for (const auto &observer : observers_) {
observer(p);
}
p.delivery_time += global_latency_ms_;
std::lock_guard<std::recursive_mutex> lock(mutex_);
event_queue_.push(std::move(p));
}
void NetworkUniverse::process_events(uint64_t current_time_ms)
{
while (true) {
Packet p;
std::vector<FakeTcpSocket *> tcp_targets;
FakeUdpSocket *udp_target = nullptr;
bool has_packet = false;
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
if (!event_queue_.empty() && event_queue_.top().delivery_time <= current_time_ms) {
p = event_queue_.top();
event_queue_.pop();
has_packet = true;
if (p.is_tcp) {
auto range = tcp_bindings_.equal_range({p.to.ip, net_ntohs(p.to.port)});
for (auto it = range.first; it != range.second; ++it) {
tcp_targets.push_back(it->second);
}
} 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 (!has_packet) {
break;
}
if (p.is_tcp) {
for (auto *it : tcp_targets) {
it->handle_packet(p);
}
} else {
if (udp_target) {
udp_target->push_packet(std::move(p.data), p.from);
}
}
}
}
void NetworkUniverse::set_latency(uint64_t ms) { global_latency_ms_ = ms; }
void NetworkUniverse::set_verbose(bool verbose) { verbose_ = verbose; }
bool NetworkUniverse::is_verbose() const { return verbose_; }
void NetworkUniverse::add_filter(PacketFilter filter) { filters_.push_back(std::move(filter)); }
void NetworkUniverse::add_observer(PacketSink sink) { observers_.push_back(std::move(sink)); }
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}))
return port;
}
return 0;
}
} // namespace tox::test

View File

@@ -0,0 +1,90 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_DOUBLES_NETWORK_UNIVERSE_H
#define C_TOXCORE_TESTING_SUPPORT_DOUBLES_NETWORK_UNIVERSE_H
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <queue>
#include <vector>
#include "../../../toxcore/network.h"
namespace tox::test {
class FakeUdpSocket;
class FakeTcpSocket;
struct Packet {
IP_Port from;
IP_Port to;
std::vector<uint8_t> data;
uint64_t delivery_time;
bool is_tcp = false;
// TCP Simulation Fields
uint8_t tcp_flags = 0; // 0x01=FIN, 0x02=SYN, 0x10=ACK
uint32_t seq = 0;
uint32_t ack = 0;
bool operator>(const Packet &other) const { return delivery_time > other.delivery_time; }
};
/**
* @brief The God Object for the network simulation.
* Manages routing, latency, and connectivity.
*/
class NetworkUniverse {
public:
using PacketFilter = std::function<bool(Packet &)>;
using PacketSink = std::function<void(const Packet &)>;
NetworkUniverse();
~NetworkUniverse();
// Registration
// Returns true if binding succeeded
bool bind_udp(IP ip, uint16_t port, FakeUdpSocket *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);
// Routing
void send_packet(Packet p);
// Simulation
void process_events(uint64_t current_time_ms);
void set_latency(uint64_t ms);
void set_verbose(bool verbose);
bool is_verbose() const;
void add_filter(PacketFilter filter);
void add_observer(PacketSink sink);
// Helpers
// Finds a free port starting from 'start'
uint16_t find_free_port(IP ip, uint16_t start = 33445);
struct IP_Port_Key {
IP ip;
uint16_t port;
bool operator<(const IP_Port_Key &other) const;
};
private:
std::map<IP_Port_Key, FakeUdpSocket *> udp_bindings_;
std::multimap<IP_Port_Key, FakeTcpSocket *> tcp_bindings_;
std::priority_queue<Packet, std::vector<Packet>, std::greater<Packet>> event_queue_;
std::vector<PacketFilter> filters_;
std::vector<PacketSink> observers_;
uint64_t global_latency_ms_ = 0;
bool verbose_ = false;
std::recursive_mutex mutex_;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_DOUBLES_NETWORK_UNIVERSE_H

View File

@@ -0,0 +1,240 @@
#include "network_universe.hh"
#include <gtest/gtest.h>
#include "fake_sockets.hh"
namespace tox::test {
namespace {
class NetworkUniverseTest : public ::testing::Test {
public:
~NetworkUniverseTest() override;
protected:
NetworkUniverse universe;
FakeUdpSocket s1{universe};
FakeUdpSocket s2{universe};
};
NetworkUniverseTest::~NetworkUniverseTest() = default;
TEST_F(NetworkUniverseTest, LatencySimulation)
{
universe.set_latency(100);
IP_Port s2_addr;
ip_init(&s2_addr.ip, false);
s2_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
s2_addr.port = net_htons(9004);
s2.bind(&s2_addr);
uint8_t data[] = "Ping";
s1.sendto(data, 4, &s2_addr);
// Time 0: packet sent but delivery time is 100
universe.process_events(0);
IP_Port from;
uint8_t buf[10];
ASSERT_EQ(s2.recvfrom(buf, 10, &from), -1);
// Time 50: still not delivered
universe.process_events(50);
ASSERT_EQ(s2.recvfrom(buf, 10, &from), -1);
// Time 100: delivered
universe.process_events(100);
ASSERT_EQ(s2.recvfrom(buf, 10, &from), 4);
}
TEST_F(NetworkUniverseTest, RoutesBasedOnIpAndPort)
{
IP ip1{}, ip2{};
ip_init(&ip1, false);
ip1.ip.v4.uint32 = net_htonl(0x01010101);
ip_init(&ip2, false);
ip2.ip.v4.uint32 = net_htonl(0x02020202);
uint16_t port = 33445;
FakeUdpSocket sock1{universe};
FakeUdpSocket sock2{universe};
sock1.set_ip(ip1);
sock2.set_ip(ip2);
IP_Port addr1{ip1, net_htons(port)};
IP_Port addr2{ip2, net_htons(port)};
ASSERT_EQ(sock1.bind(&addr1), 0);
ASSERT_EQ(sock2.bind(&addr2), 0);
const char *msg1 = "To IP 1";
const char *msg2 = "To IP 2";
FakeUdpSocket sender{universe};
sender.sendto(reinterpret_cast<const uint8_t *>(msg1), strlen(msg1), &addr1);
sender.sendto(reinterpret_cast<const uint8_t *>(msg2), strlen(msg2), &addr2);
universe.process_events(0);
uint8_t buf[100];
IP_Port from;
int len1 = sock1.recvfrom(buf, sizeof(buf), &from);
ASSERT_GT(len1, 0);
EXPECT_EQ(std::string(reinterpret_cast<char *>(buf), static_cast<size_t>(len1)), msg1);
int len2 = sock2.recvfrom(buf, sizeof(buf), &from);
ASSERT_GT(len2, 0);
EXPECT_EQ(std::string(reinterpret_cast<char *>(buf), static_cast<size_t>(len2)), msg2);
}
TEST_F(NetworkUniverseTest, FindFreePortIsIpSpecific)
{
IP ip1{}, ip2{};
ip_init(&ip1, false);
ip1.ip.v4.uint32 = net_htonl(0x01010101);
ip_init(&ip2, false);
ip2.ip.v4.uint32 = net_htonl(0x02020202);
FakeUdpSocket sock1{universe};
sock1.set_ip(ip1);
uint16_t port = 33445;
IP_Port addr1{ip1, net_htons(port)};
ASSERT_EQ(sock1.bind(&addr1), 0);
// Port 33445 should be busy for ip1, but free for ip2
EXPECT_EQ(universe.find_free_port(ip1, port), port + 1);
EXPECT_EQ(universe.find_free_port(ip2, port), port);
}
TEST_F(NetworkUniverseTest, IpPortKeyEqualityRobustness)
{
IP ip1{}, ip2{};
ip_init(&ip1, false);
ip1.ip.v4.uint32 = net_htonl(0x7F000001);
ip_init(&ip2, false);
ip2.ip.v4.uint32 = net_htonl(0x7F000001);
uint16_t port = 12345;
// Force different garbage in the union padding for IPv4
// The union is 16 bytes. IP4 is 4 bytes. Trailing 12 bytes are unused.
memset(ip1.ip.v6.uint8 + 4, 0x11, 12);
memset(ip2.ip.v6.uint8 + 4, 0x22, 12);
NetworkUniverse::IP_Port_Key key1{ip1, port};
NetworkUniverse::IP_Port_Key key2{ip2, port};
// They should be considered equal (neither is less than the other)
EXPECT_FALSE(key1 < key2);
EXPECT_FALSE(key2 < key1);
// Now try with different IPv4 but same garbage
ip2.ip.v4.uint32 = net_htonl(0x7F000002);
memset(ip2.ip.v6.uint8 + 4, 0x11, 12); // same garbage as ip1
NetworkUniverse::IP_Port_Key key3{ip2, port};
EXPECT_TRUE(key1 < key3 || key3 < key1);
}
TEST_F(NetworkUniverseTest, IPv4v6Distinction)
{
IP ip1{}, ip2{};
ip_init(&ip1, false); // IPv4
ip1.ip.v4.uint32 = net_htonl(0x01020304);
ip_init(&ip2, true); // IPv6
// Set IPv6 bytes to match IPv4 bytes at the beginning
memset(ip2.ip.v6.uint8, 0, 16);
ip2.ip.v6.uint32[0] = net_htonl(0x01020304);
uint16_t port = 12345;
NetworkUniverse::IP_Port_Key key1{ip1, port};
NetworkUniverse::IP_Port_Key key2{ip2, port};
// Different families must be different even if underlying bytes happen to match
EXPECT_TRUE(key1 < key2 || key2 < key1);
}
TEST_F(NetworkUniverseTest, ManyNodes)
{
const int num_nodes = 5000;
struct NodeInfo {
std::unique_ptr<FakeUdpSocket> sock;
IP_Port addr;
};
std::vector<NodeInfo> nodes;
nodes.reserve(num_nodes);
for (int i = 0; i < num_nodes; ++i) {
auto sock = std::make_unique<FakeUdpSocket>(universe);
IP ip{};
ip_init(&ip, false);
ip.ip.v4.uint32 = net_htonl(0x0A000000 + i); // 10.0.x.y
sock->set_ip(ip);
IP_Port addr{ip, net_htons(33445)};
ASSERT_EQ(sock->bind(&addr), 0);
nodes.push_back({std::move(sock), addr});
}
const int num_messages = 100;
// Send messages from first num_messages to last num_messages nodes
for (int i = 0; i < num_messages; ++i) {
const char *msg = "Stress test";
nodes[i].sock->sendto(reinterpret_cast<const uint8_t *>(msg), strlen(msg),
&nodes[num_nodes - 1 - i].addr);
}
universe.process_events(0);
for (int i = 0; i < num_messages; ++i) {
uint8_t buf[100];
IP_Port from;
int len = nodes[num_nodes - 1 - i].sock->recvfrom(buf, sizeof(buf), &from);
ASSERT_GT(len, 0);
EXPECT_EQ(std::string(reinterpret_cast<char *>(buf), static_cast<size_t>(len)),
"Stress test");
EXPECT_TRUE(ip_equal(&from.ip, &nodes[i].addr.ip));
}
}
TEST_F(NetworkUniverseTest, IpPadding)
{
IP ip1{};
ip_init(&ip1, false);
ip1.ip.v4.uint32 = net_htonl(0x7F000001);
FakeUdpSocket sock{universe};
sock.set_ip(ip1);
IP_Port bind_addr{ip1, net_htons(12345)};
ASSERT_EQ(sock.bind(&bind_addr), 0);
// Create an address with garbage in the padding
IP_Port target_addr;
memset(&target_addr, 0xAA, sizeof(target_addr));
ip_init(&target_addr.ip, false);
target_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001);
target_addr.port = net_htons(12345);
FakeUdpSocket sender{universe};
const char *msg = "Padding test";
sender.sendto(reinterpret_cast<const uint8_t *>(msg), strlen(msg), &target_addr);
universe.process_events(0);
uint8_t buf[100];
IP_Port from;
int len = sock.recvfrom(buf, sizeof(buf), &from);
// If this fails, it means NetworkUniverse is not robust against padding garbage
ASSERT_GT(len, 0);
EXPECT_EQ(
std::string(reinterpret_cast<char *>(buf), static_cast<size_t>(len)), "Padding test");
}
} // namespace
} // namespace tox::test

View File

@@ -0,0 +1,28 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_CLOCK_H
#define C_TOXCORE_TESTING_SUPPORT_CLOCK_H
#include <cstdint>
namespace tox::test {
/**
* @brief Abstraction over the system's monotonic clock.
*/
class ClockSystem {
public:
virtual ~ClockSystem();
/**
* @brief Returns current monotonic time in milliseconds.
*/
virtual uint64_t current_time_ms() const = 0;
/**
* @brief Returns current monotonic time in seconds.
*/
virtual uint64_t current_time_s() const = 0;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_CLOCK_H

View File

@@ -0,0 +1,47 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_ENVIRONMENT_H
#define C_TOXCORE_TESTING_SUPPORT_ENVIRONMENT_H
#include <memory>
namespace tox::test {
class NetworkSystem;
class ClockSystem;
class RandomSystem;
class MemorySystem;
/**
* @brief Service locator for system resources in tests.
*
* This interface allows tests to access system resources (Network, Time, RNG,
* memory) in a way that can be swapped between real implementations (for
* integration tests) and simulated ones (for unit/fuzz tests).
*/
class Environment {
public:
virtual ~Environment();
/**
* @brief Access the network subsystem.
*/
virtual NetworkSystem &network() = 0;
/**
* @brief Access the monotonic clock and timer subsystem.
*/
virtual ClockSystem &clock() = 0;
/**
* @brief Access the random number generator.
*/
virtual RandomSystem &random() = 0;
/**
* @brief Access the memory allocator.
*/
virtual MemorySystem &memory() = 0;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_ENVIRONMENT_H

View File

@@ -0,0 +1,183 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_PUBLIC_FUZZ_DATA_H
#define C_TOXCORE_TESTING_SUPPORT_PUBLIC_FUZZ_DATA_H
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
namespace tox::test {
struct Fuzz_Data {
static constexpr bool FUZZ_DEBUG = false;
static constexpr std::size_t TRACE_TRAP = -1;
private:
const uint8_t *data_;
const uint8_t *base_;
std::size_t size_;
public:
Fuzz_Data(const uint8_t *input_data, std::size_t input_size)
: data_(input_data)
, base_(input_data)
, size_(input_size)
{
}
Fuzz_Data &operator=(const Fuzz_Data &rhs) = delete;
Fuzz_Data(const Fuzz_Data &rhs) = delete;
struct Consumer {
const char *func;
Fuzz_Data &fd;
operator bool()
{
if (fd.empty())
return false;
const bool val = fd.data_[0];
if (FUZZ_DEBUG) {
std::printf("consume@%zu(%s): bool %s\n", fd.pos(), func, val ? "true" : "false");
}
++fd.data_;
--fd.size_;
return val;
}
template <typename T>
operator T()
{
if (sizeof(T) > fd.size())
return T{};
const uint8_t *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}; }
template <typename T>
T consume_integral()
{
return consume1("consume_integral");
}
template <typename T>
T consume_integral_in_range(T min, T max)
{
if (min >= max)
return min;
T val = consume_integral<T>();
return min + (val % (max - min + 1));
}
std::size_t remaining_bytes() const { return size(); }
std::vector<uint8_t> consume_bytes(std::size_t count)
{
if (count == 0 || count > size_)
return {};
const uint8_t *start = consume("consume_bytes", count);
if (!start)
return {};
return std::vector<uint8_t>(start, start + count);
}
std::vector<uint8_t> consume_remaining_bytes()
{
if (empty())
return {};
std::size_t count = size();
const uint8_t *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_; }
bool empty() const { return size_ == 0; }
const uint8_t *consume(const char *func, std::size_t count)
{
if (count > size_)
return nullptr;
const uint8_t *val = data_;
if (FUZZ_DEBUG) {
if (count == 1) {
std::printf("consume@%zu(%s): %d (0x%02x)\n", pos(), func, val[0], val[0]);
} else if (count != 0) {
std::printf("consume@%zu(%s): %02x..%02x[%zu]\n", pos(), func, val[0],
val[count - 1], count);
}
}
data_ += count;
size_ -= count;
return val;
}
};
#define CONSUME1_OR_RETURN(TYPE, NAME, INPUT) \
if ((INPUT).size() < sizeof(TYPE)) { \
return; \
} \
TYPE NAME = (INPUT).consume1(__func__)
#define CONSUME1_OR_RETURN_VAL(TYPE, NAME, INPUT, VAL) \
if ((INPUT).size() < sizeof(TYPE)) { \
return (VAL); \
} \
TYPE NAME = (INPUT).consume1(__func__)
#define CONSUME_OR_RETURN(DECL, INPUT, SIZE) \
if ((INPUT).size() < (SIZE)) { \
return; \
} \
DECL = (INPUT).consume(__func__, (SIZE))
#define CONSUME_OR_RETURN_VAL(DECL, INPUT, SIZE, VAL) \
if ((INPUT).size() < (SIZE)) { \
return (VAL); \
} \
DECL = (INPUT).consume(__func__, (SIZE))
using Fuzz_Target = void (*)(Fuzz_Data &input);
template <Fuzz_Target... Args>
struct Fuzz_Target_Selector;
template <Fuzz_Target Arg, Fuzz_Target... Args>
struct Fuzz_Target_Selector<Arg, Args...> {
static void select(uint8_t selector, Fuzz_Data &input)
{
if (selector == sizeof...(Args)) {
return Arg(input);
}
return Fuzz_Target_Selector<Args...>::select(selector, input);
}
};
template <>
struct Fuzz_Target_Selector<> {
static void select(uint8_t selector, Fuzz_Data &input)
{
// The selector selected no function, so we do nothing and rely on the
// fuzzer to come up with a better selector.
}
};
template <Fuzz_Target... Args>
void fuzz_select_target(const uint8_t *data, std::size_t size)
{
Fuzz_Data input{data, size};
CONSUME1_OR_RETURN(const uint8_t, selector, input);
return Fuzz_Target_Selector<Args...>::select(selector, input);
}
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_PUBLIC_FUZZ_DATA_H

View File

@@ -0,0 +1,23 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_PUBLIC_FUZZ_HELPERS_H
#define C_TOXCORE_TESTING_SUPPORT_PUBLIC_FUZZ_HELPERS_H
#include "../doubles/fake_memory.hh"
#include "../doubles/fake_random.hh"
#include "../doubles/fake_sockets.hh"
#include "fuzz_data.hh"
namespace tox::test {
// Configures the socket to pull packets from the Fuzz_Data stream
// mimicking the legacy Fuzz_System behavior (2-byte length prefix).
void configure_fuzz_packet_source(FakeUdpSocket &socket, Fuzz_Data &input);
// Configures memory allocator to consume failure decisions from Fuzz_Data.
void configure_fuzz_memory_source(FakeMemory &memory, Fuzz_Data &input);
// Configures random generator to consume bytes from Fuzz_Data.
void configure_fuzz_random_source(FakeRandom &random, Fuzz_Data &input);
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_PUBLIC_FUZZ_HELPERS_H

View File

@@ -0,0 +1,23 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_PUBLIC_MEMORY_H
#define C_TOXCORE_TESTING_SUPPORT_PUBLIC_MEMORY_H
#include <cstddef>
#include <cstdint>
namespace tox::test {
/**
* @brief Abstraction over the memory allocator.
*/
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;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_PUBLIC_MEMORY_H

View File

@@ -0,0 +1,51 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_NETWORK_H
#define C_TOXCORE_TESTING_SUPPORT_NETWORK_H
#include <cstdint>
#include <vector>
#include "../../../toxcore/network.h"
namespace tox::test {
/**
* @brief Abstraction over the network subsystem (sockets).
*/
class NetworkSystem {
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 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;
// 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 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)
= 0;
};
/**
* @brief Helper to create an IPv4 IP struct from a host-byte-order address.
*/
IP make_ip(uint32_t ipv4);
/**
* @brief Helper to create a unique node IP in the 10.x.y.z range.
*/
IP make_node_ip(uint32_t node_id);
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_NETWORK_H

View File

@@ -0,0 +1,22 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_RANDOM_H
#define C_TOXCORE_TESTING_SUPPORT_RANDOM_H
#include <cstdint>
#include <vector>
namespace tox::test {
/**
* @brief Abstraction over the random number generator.
*/
class RandomSystem {
public:
virtual ~RandomSystem();
virtual uint32_t uniform(uint32_t upper_bound) = 0;
virtual void bytes(uint8_t *out, size_t count) = 0;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_RANDOM_H

View File

@@ -0,0 +1,78 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_SIMULATED_ENVIRONMENT_H
#define C_TOXCORE_TESTING_SUPPORT_SIMULATED_ENVIRONMENT_H
#include <memory>
#include <vector>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <winsock2.h>
#else
#include <netinet/in.h>
#include <sys/socket.h>
#endif
#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_random.hh"
#include "environment.hh"
#include "simulation.hh"
namespace tox::test {
struct ScopedToxSystem {
// The underlying node in the simulation
std::unique_ptr<SimulatedNode> node;
// Direct access to primary socket (for fuzzer injection)
FakeUdpSocket *endpoint;
// C structs
struct Network c_network;
struct Tox_Random c_random;
struct Tox_Memory c_memory;
// The main struct passed to tox_new
Tox_System system;
};
class SimulatedEnvironment : public Environment {
public:
SimulatedEnvironment();
~SimulatedEnvironment() override;
NetworkSystem &network() override;
ClockSystem &clock() override;
RandomSystem &random() override;
MemorySystem &memory() override;
FakeClock &fake_clock();
FakeRandom &fake_random();
FakeMemory &fake_memory();
Simulation &simulation() { return *sim_; }
/**
* @brief Creates a new virtual node in the simulation bound to the specified port.
*/
std::unique_ptr<ScopedToxSystem> create_node(uint16_t port);
void advance_time(uint64_t ms);
private:
std::unique_ptr<Simulation> sim_;
// Global instances for Environment interface compliance.
std::unique_ptr<FakeRandom> global_random_;
std::unique_ptr<FakeMemory> global_memory_;
// For network(), we can't return a per-node stack.
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_SIMULATED_ENVIRONMENT_H

View File

@@ -0,0 +1,112 @@
#ifndef C_TOXCORE_TESTING_SUPPORT_SIMULATION_H
#define C_TOXCORE_TESTING_SUPPORT_SIMULATION_H
#include <functional>
#include <memory>
#include <vector>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <winsock2.h>
#else
#include <netinet/in.h>
#include <sys/socket.h>
#endif
#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"
#include "../doubles/fake_random.hh"
#include "../doubles/network_universe.hh"
#include "environment.hh"
namespace tox::test {
class SimulatedNode;
/**
* @brief The Simulation World.
* Holds the Clock and the Universe.
*/
class Simulation {
public:
Simulation();
~Simulation();
// Time Control
void advance_time(uint64_t ms);
void run_until(std::function<bool()> condition, uint64_t timeout_ms = 5000);
// Global Access
FakeClock &clock() { return *clock_; }
NetworkUniverse &net() { return *net_; }
// Node Factory
std::unique_ptr<SimulatedNode> create_node();
private:
std::unique_ptr<FakeClock> clock_;
std::unique_ptr<NetworkUniverse> net_;
uint32_t node_count_ = 0;
};
/**
* @brief Represents a single node in the simulation.
* Implements the Environment interface for dependency injection.
*/
class SimulatedNode : public Environment {
public:
explicit SimulatedNode(Simulation &sim, uint32_t node_id);
~SimulatedNode() override;
// Environment Interface
NetworkSystem &network() override;
ClockSystem &clock() override;
RandomSystem &random() override;
MemorySystem &memory() override;
// Direct Access to Fakes
FakeNetworkStack &fake_network() { return *network_; }
FakeRandom &fake_random() { return *random_; }
FakeMemory &fake_memory() { return *memory_; }
// Tox Creation Helper
// 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); }
};
using ToxPtr = std::unique_ptr<Tox, ToxDeleter>;
ToxPtr create_tox(const Tox_Options *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(); }
// For fuzzing compatibility (exposes first bound UDP socket as "endpoint")
FakeUdpSocket *get_primary_socket();
private:
Simulation &sim_;
std::unique_ptr<FakeNetworkStack> network_;
std::unique_ptr<FakeRandom> random_;
std::unique_ptr<FakeMemory> memory_;
// 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 IP ip;
};
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_SIMULATION_H

View File

@@ -0,0 +1,82 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2026 The TokTok team.
*/
#ifndef C_TOXCORE_TESTING_SUPPORT_TOX_NETWORK_H
#define C_TOXCORE_TESTING_SUPPORT_TOX_NETWORK_H
#include <vector>
#include "simulation.hh"
namespace tox::test {
struct ConnectedFriend {
std::unique_ptr<SimulatedNode> node;
SimulatedNode::ToxPtr tox;
uint32_t friend_number;
ConnectedFriend(std::unique_ptr<SimulatedNode> node_in, SimulatedNode::ToxPtr tox_in,
uint32_t friend_number_in)
: node(std::move(node_in))
, tox(std::move(tox_in))
, friend_number(friend_number_in)
{
}
ConnectedFriend(ConnectedFriend &&) = default;
ConnectedFriend &operator=(ConnectedFriend &&) = default;
~ConnectedFriend();
};
/**
* @brief Sets up a network of connected Tox instances.
*
* This function creates num_friends Tox instances, adds them as friends to main_tox,
* and bootstraps them to main_node. It then runs the simulation until all friends
* are connected to main_tox.
*
* @param sim The simulation to run.
* @param main_tox The main Tox instance to which friends will connect.
* @param main_node The simulated node hosting the main Tox instance.
* @param num_friends The number of friends to create and connect.
* @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);
/**
* @brief Connects two existing Tox instances as friends.
*
* This function adds each Tox instance as a friend to the other, bootstraps
* them to each other, and runs the simulation until they are connected.
*
* @param sim The simulation to run.
* @param node1 The simulated node hosting the first Tox instance.
* @param tox1 The first Tox instance.
* @param node2 The simulated node hosting the second Tox instance.
* @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);
/**
* @brief Sets up a group and has all friends join it.
*
* This function creates a new public group on main_tox, invites all friends in the
* provided vector, and runs the simulation until all friends have joined and
* main_tox sees all of them.
*
* @param sim The simulation to run.
* @param main_tox The main Tox instance that creates the group.
* @param friends The friends to invite to the group.
* @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);
} // namespace tox::test
#endif // C_TOXCORE_TESTING_SUPPORT_TOX_NETWORK_H

View File

@@ -0,0 +1,7 @@
#include "../public/clock.hh"
namespace tox::test {
ClockSystem::~ClockSystem() = default;
} // namespace tox::test

View File

@@ -0,0 +1,7 @@
#include "../public/environment.hh"
namespace tox::test {
Environment::~Environment() = default;
} // namespace tox::test

View File

@@ -0,0 +1,16 @@
#include "../doubles/fake_clock.hh"
namespace tox::test {
FakeClock::FakeClock(uint64_t start_time_ms)
: now_ms_(start_time_ms)
{
}
uint64_t FakeClock::current_time_ms() const { return now_ms_; }
uint64_t FakeClock::current_time_s() const { return now_ms_ / 1000; }
void FakeClock::advance(uint64_t ms) { now_ms_ += ms; }
} // namespace tox::test

View File

@@ -0,0 +1,144 @@
#include "../doubles/fake_memory.hh"
#include <cstdlib>
#include <iostream>
#include <new>
#include "../../../toxcore/tox_memory_impl.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); },
.realloc_callback
= [](void *obj, void *ptr,
uint32_t size) { return static_cast<FakeMemory *>(obj)->realloc(ptr, size); },
.dealloc_callback = [](void *obj, void *ptr) { static_cast<FakeMemory *>(obj)->free(ptr); },
};
// --- Implementation ---
FakeMemory::FakeMemory() = default;
FakeMemory::~FakeMemory() = default;
void *FakeMemory::malloc(size_t size)
{
bool fail = failure_injector_ && failure_injector_(size);
if (observer_) {
observer_(!fail);
}
if (fail) {
return nullptr;
}
void *ptr = std::malloc(size + sizeof(Header));
if (!ptr) {
return nullptr;
}
Header *header = static_cast<Header *>(ptr);
header->size = size;
header->magic = kMagic;
current_allocation_ += size;
if (current_allocation_ > max_allocation_) {
max_allocation_ = current_allocation_;
}
void *res = header + 1;
// std::cerr << "[FakeMemory] malloc(" << size << ") -> " << res << " (header=" << header << ")"
// << std::endl;
return res;
}
void *FakeMemory::realloc(void *ptr, size_t size)
{
if (!ptr) {
return malloc(size);
}
Header *old_header = static_cast<Header *>(ptr) - 1;
if (old_header->magic != kMagic) {
if (old_header->magic == kFreeMagic) {
std::cerr << "[FakeMemory] realloc: Double realloc/free detected at " << ptr
<< " (header=" << old_header << ")" << std::endl;
} else {
std::cerr << "[FakeMemory] realloc: Invalid pointer (wrong magic 0x" << std::hex
<< old_header->magic << ") at " << ptr << " (header=" << old_header << ")"
<< std::endl;
}
std::abort();
}
bool fail = failure_injector_ && failure_injector_(size);
if (observer_) {
observer_(!fail);
}
if (fail) {
// If realloc fails, original block is left untouched.
return nullptr;
}
size_t old_size = old_header->size;
void *new_ptr = std::realloc(old_header, size + sizeof(Header));
if (!new_ptr) {
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_;
}
header->size = size;
header->magic = kMagic;
void *res = header + 1;
// std::cerr << "[FakeMemory] realloc(" << ptr << ", " << size << ") -> " << res << " (header="
// << header << ")" << std::endl;
return res;
}
void FakeMemory::free(void *ptr)
{
if (!ptr) {
return;
}
Header *header = static_cast<Header *>(ptr) - 1;
if (header->magic != kMagic) {
if (header->magic == kFreeMagic) {
std::cerr << "[FakeMemory] free: Double free detected at " << ptr
<< " (header=" << header << ")" << std::endl;
} else {
std::cerr << "[FakeMemory] free: Invalid pointer (wrong magic 0x" << std::hex
<< header->magic << ") at " << ptr << " (header=" << header << ")"
<< std::endl;
}
std::abort();
}
size_t size = header->size;
current_allocation_ -= size;
header->magic = kFreeMagic; // Mark as free
std::free(header);
}
void FakeMemory::set_failure_injector(FailureInjector injector)
{
failure_injector_ = std::move(injector);
}
void FakeMemory::set_observer(Observer observer) { observer_ = std::move(observer); }
struct Tox_Memory FakeMemory::get_c_memory() { return Tox_Memory{&kFakeMemoryVtable, this}; }
} // namespace tox::test

View File

@@ -0,0 +1,63 @@
#include "../doubles/fake_random.hh"
#include <algorithm>
#include "../../../toxcore/tox_random_impl.h"
namespace tox::test {
// --- Trampolines for Tox_Random_Funcs ---
static const Tox_Random_Funcs kFakeRandomVtable = {
.bytes_callback
= [](void *obj, uint8_t *bytes,
uint32_t length) { static_cast<FakeRandom *>(obj)->bytes(bytes, length); },
.uniform_callback
= [](void *obj,
uint32_t upper_bound) { return static_cast<FakeRandom *>(obj)->uniform(upper_bound); },
};
FakeRandom::FakeRandom(uint64_t seed)
: rng_(seed)
{
}
void FakeRandom::set_entropy_source(EntropySource source) { entropy_source_ = std::move(source); }
void FakeRandom::set_observer(Observer observer) { observer_ = std::move(observer); }
uint32_t FakeRandom::uniform(uint32_t upper_bound)
{
if (upper_bound == 0) {
return 0;
}
// If we are recording (observer set) or replaying (entropy source set),
// we want consistency. Legacy behavior derived uniform from bytes.
if (entropy_source_ || observer_) {
uint32_t raw_val = 0;
bytes(reinterpret_cast<uint8_t *>(&raw_val), sizeof(raw_val));
return raw_val % upper_bound;
}
std::uniform_int_distribution<uint32_t> dist(0, upper_bound - 1);
return dist(rng_);
}
void FakeRandom::bytes(uint8_t *out, size_t count)
{
if (entropy_source_) {
entropy_source_(out, count);
} else {
std::uniform_int_distribution<uint16_t> dist(0, 255);
std::generate_n(out, count, [&]() { return static_cast<uint8_t>(dist(rng_)); });
}
if (observer_) {
observer_(out, count);
}
}
struct Tox_Random FakeRandom::get_c_random() { return Tox_Random{&kFakeRandomVtable, this}; }
} // namespace tox::test

View File

@@ -0,0 +1,96 @@
#include "../public/fuzz_helpers.hh"
#include <algorithm>
#include <cstring>
#include <vector>
namespace tox::test {
void configure_fuzz_packet_source(FakeUdpSocket &socket, Fuzz_Data &input)
{
socket.set_packet_source([&input](std::vector<uint8_t> &data, IP_Port &from) -> bool {
if (input.size() < 2)
return false;
const uint8_t *len_bytes = input.consume("recv_len", 2);
if (!len_bytes)
return false;
uint16_t len = (len_bytes[0] << 8) | len_bytes[1];
if (len == 0xffff) {
// EWOULDBLOCK simulation. Return false implies "no packet".
return false;
}
size_t actual_len = std::min(static_cast<size_t>(len), input.size());
const uint8_t *payload = input.consume("recv_payload", actual_len);
if (!payload && actual_len > 0)
return false;
data.assign(payload, payload + actual_len);
// Dummy address (legacy Fuzz_System used 127.0.0.2:33446)
ip_init(&from.ip, false); // IPv4
from.ip.ip.v4.uint32 = net_htonl(0x7F000002); // 127.0.0.2
from.port = net_htons(33446);
return true;
});
}
void configure_fuzz_memory_source(FakeMemory &memory, Fuzz_Data &input)
{
memory.set_failure_injector([&input](size_t size) -> bool {
if (input.size() < 1) {
// Legacy behavior: if input runs out, allocation succeeds.
return false;
}
const uint8_t *b = input.consume("malloc_decision", 1);
bool succeed = (b && *b);
return !succeed; // Return true to fail
});
}
void configure_fuzz_random_source(FakeRandom &random, Fuzz_Data &input)
{
random.set_entropy_source([&input](uint8_t *out, size_t count) {
// Initialize with zeros in case of underflow
std::memset(out, 0, count);
// For small types (keys, nonces), behave like legacy fuzz support
if (count == 1 || count == 2 || count == 4 || count == 8) {
if (input.size() >= count) {
const uint8_t *bytes = input.consume("rnd_bytes", count);
std::memcpy(out, bytes, count);
}
return;
}
// For large chunks, repeat available data
if (count == 24 || count == 32) {
size_t available = std::min(input.size(), static_cast<size_t>(2));
if (available > 0) {
const uint8_t *chunk = input.consume("rnd_chunk", available);
if (available == 2) {
std::memset(out, chunk[0], count / 2);
std::memset(out + count / 2, chunk[1], count / 2);
} else {
std::memset(out, chunk[0], count);
}
}
return;
}
// Fallback for other sizes: consume as much as possible
size_t taken = std::min(input.size(), count);
if (taken > 0) {
const uint8_t *bytes = input.consume("rnd_generic", taken);
std::memcpy(out, bytes, taken);
}
});
}
} // namespace tox::test

View File

@@ -0,0 +1,7 @@
#include "../public/memory.hh"
namespace tox::test {
MemorySystem::~MemorySystem() = default;
} // namespace tox::test

View File

@@ -0,0 +1,21 @@
#include "../public/network.hh"
namespace tox::test {
NetworkSystem::~NetworkSystem() = default;
IP make_ip(uint32_t ipv4)
{
IP ip;
ip_init(&ip, false);
ip.ip.v4.uint32 = net_htonl(ipv4);
return ip;
}
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));
}
} // namespace tox::test

View File

@@ -0,0 +1,7 @@
#include "../public/random.hh"
namespace tox::test {
RandomSystem::~RandomSystem() = default;
} // namespace tox::test

View File

@@ -0,0 +1,76 @@
#include "../public/simulated_environment.hh"
#include <iostream>
namespace tox::test {
SimulatedEnvironment::SimulatedEnvironment()
: sim_(std::make_unique<Simulation>())
, global_random_(std::make_unique<FakeRandom>(12345))
, global_memory_(std::make_unique<FakeMemory>())
{
}
SimulatedEnvironment::~SimulatedEnvironment() = default;
NetworkSystem &SimulatedEnvironment::network()
{
// Return a dummy stack for interface compliance; real networking is per-node.
static FakeNetworkStack *dummy = nullptr;
if (!dummy) {
IP dummy_ip;
ip_init(&dummy_ip, false);
dummy = new FakeNetworkStack(sim_->net(), dummy_ip);
}
return *dummy;
}
ClockSystem &SimulatedEnvironment::clock() { return sim_->clock(); }
RandomSystem &SimulatedEnvironment::random() { return *global_random_; }
MemorySystem &SimulatedEnvironment::memory() { return *global_memory_; }
FakeClock &SimulatedEnvironment::fake_clock() { return sim_->clock(); }
FakeRandom &SimulatedEnvironment::fake_random() { return *global_random_; }
FakeMemory &SimulatedEnvironment::fake_memory() { return *global_memory_; }
std::unique_ptr<ScopedToxSystem> SimulatedEnvironment::create_node(uint16_t port)
{
auto scoped = std::make_unique<ScopedToxSystem>();
scoped->node = sim_->create_node();
// Bind port
if (port != 0) {
Socket s = scoped->node->fake_network().socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
IP_Port addr;
ip_init(&addr.ip, false);
addr.ip.ip.v4.uint32 = 0;
addr.port = net_htons(port);
scoped->node->fake_network().bind(s, &addr);
}
// Get Primary Endpoint for Fuzzer
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();
// Use Node's Network
scoped->c_network = scoped->node->get_c_network();
// Setup System
scoped->system.mem = &scoped->c_memory;
scoped->system.ns = &scoped->c_network;
scoped->system.rng = &scoped->c_random;
scoped->system.mono_time_user_data = &sim_->clock();
scoped->system.mono_time_callback = [](void *user_data) -> uint64_t {
return static_cast<FakeClock *>(user_data)->current_time_ms();
};
return scoped;
}
void SimulatedEnvironment::advance_time(uint64_t ms) { sim_->advance_time(ms); }
} // namespace tox::test

View File

@@ -0,0 +1,113 @@
#include "../public/simulation.hh"
#include <cassert>
#include <iostream>
namespace tox::test {
// --- Simulation ---
Simulation::Simulation()
: clock_(std::make_unique<FakeClock>())
, net_(std::make_unique<NetworkUniverse>())
{
}
Simulation::~Simulation() = default;
void Simulation::advance_time(uint64_t ms)
{
clock_->advance(ms);
net_->process_events(clock_->current_time_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) {
break;
}
advance_time(10); // 10ms ticks
}
}
std::unique_ptr<SimulatedNode> Simulation::create_node()
{
auto node = std::make_unique<SimulatedNode>(*this, ++node_count_);
if (net_->is_verbose()) {
uint32_t ip4 = net_ntohl(node->ip.ip.v4.uint32);
std::cerr << "[Simulation] Created node " << node_count_ << " with IP "
<< ((ip4 >> 24) & 0xFF) << "." << ((ip4 >> 16) & 0xFF) << "."
<< ((ip4 >> 8) & 0xFF) << "." << (ip4 & 0xFF) << std::endl;
}
return node;
}
// --- SimulatedNode ---
SimulatedNode::SimulatedNode(Simulation &sim, uint32_t node_id)
: sim_(sim)
, 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())
, ip(make_node_ip(node_id))
{
}
SimulatedNode::~SimulatedNode() = default;
NetworkSystem &SimulatedNode::network() { return *network_; }
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)
{
std::unique_ptr<Tox_Options, decltype(&tox_options_free)> default_options(
nullptr, tox_options_free);
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();
}
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 {
return static_cast<FakeClock *>(user_data)->current_time_ms();
};
system.mono_time_user_data = &sim_.clock();
opts_testing.operating_system = &system;
Tox_Err_New err;
Tox_Err_New_Testing err_testing;
Tox *t = tox_new_testing(options, &err, &opts_testing, &err_testing);
if (!t) {
return nullptr;
}
return ToxPtr(t);
}
FakeUdpSocket *SimulatedNode::get_primary_socket()
{
auto sockets = network_->get_bound_udp_sockets();
if (sockets.empty())
return nullptr;
return sockets.front(); // Return the first one bound
}
} // namespace tox::test

View File

@@ -0,0 +1,330 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2026 The TokTok team.
*/
#include "../public/tox_network.hh"
#include <cstring>
#include <iostream>
#include "../../../toxcore/network.h"
#include "../../../toxcore/tox.h"
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> friends;
friends.reserve(num_friends);
uint8_t main_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(main_tox, main_pk);
uint8_t main_dht_id[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(main_tox, main_dht_id);
FakeUdpSocket *main_socket = main_node.get_primary_socket();
if (!main_socket) {
return {};
}
uint16_t main_port = main_socket->local_port();
char main_ip_str[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&main_node.ip, main_ip_str, sizeof(main_ip_str));
uint8_t prev_dht_id[TOX_PUBLIC_KEY_SIZE];
memcpy(prev_dht_id, main_dht_id, TOX_PUBLIC_KEY_SIZE);
char prev_ip_str[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&main_node.ip, prev_ip_str, sizeof(prev_ip_str));
uint16_t prev_port = main_port;
for (int i = 0; i < num_friends; ++i) {
auto node = sim.create_node();
auto tox = node->create_tox(options);
if (!tox) {
return {};
}
uint8_t friend_pk[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox.get(), 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);
}
// 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();
if (!node_socket) {
return {};
}
prev_port = node_socket->local_port();
friends.push_back({std::move(node), std::move(tox), 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);
}
// 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
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;
}
}
}
last_print = sim.clock().current_time_ms();
}
return all_connected;
},
300000); // 5 minutes simulation time for 100 nodes to converge
return friends;
}
bool connect_friends(
Simulation &sim, SimulatedNode &node1, Tox *tox1, SimulatedNode &node2, Tox *tox2)
{
uint8_t pk1[TOX_PUBLIC_KEY_SIZE];
uint8_t pk2[TOX_PUBLIC_KEY_SIZE];
tox_self_get_public_key(tox1, pk1);
tox_self_get_public_key(tox2, pk2);
Tox_Err_Friend_Add err_add;
uint32_t f1 = tox_friend_add_norequest(tox1, pk2, &err_add);
if (f1 == UINT32_MAX || err_add != TOX_ERR_FRIEND_ADD_OK) {
return false;
}
uint32_t f2 = tox_friend_add_norequest(tox2, pk1, &err_add);
if (f2 == UINT32_MAX || err_add != TOX_ERR_FRIEND_ADD_OK) {
return false;
}
uint8_t dht_id1[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(tox1, dht_id1);
char ip1_str[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&node1.ip, ip1_str, sizeof(ip1_str));
FakeUdpSocket *s1 = node1.get_primary_socket();
if (!s1) {
return false;
}
uint16_t port1 = s1->local_port();
uint8_t dht_id2[TOX_PUBLIC_KEY_SIZE];
tox_self_get_dht_id(tox2, dht_id2);
char ip2_str[TOX_INET_ADDRSTRLEN];
ip_parse_addr(&node2.ip, ip2_str, sizeof(ip2_str));
FakeUdpSocket *s2 = node2.get_primary_socket();
if (!s2) {
return false;
}
uint16_t port2 = s2->local_port();
tox_bootstrap(tox1, ip2_str, port2, dht_id2, nullptr);
tox_bootstrap(tox2, ip1_str, port1, dht_id1, nullptr);
sim.run_until(
[&]() {
tox_iterate(tox1, nullptr);
tox_iterate(tox2, nullptr);
return tox_friend_get_connection_status(tox1, f1, nullptr) != TOX_CONNECTION_NONE
&& tox_friend_get_connection_status(tox2, f2, nullptr) != TOX_CONNECTION_NONE;
},
60000);
return tox_friend_get_connection_status(tox1, f1, nullptr) != TOX_CONNECTION_NONE
&& tox_friend_get_connection_status(tox2, f2, nullptr) != TOX_CONNECTION_NONE;
}
uint32_t setup_connected_group(
Simulation &sim, Tox *main_tox, const std::vector<ConnectedFriend> &friends)
{
struct NodeGroupState {
uint32_t peer_count = 0;
uint32_t group_number = UINT32_MAX;
};
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_Err_Group_New err_new;
main_state.group_number = tox_group_new(main_tox, TOX_GROUP_PRIVACY_STATE_PUBLIC,
reinterpret_cast<const uint8_t *>("test"), 4, reinterpret_cast<const uint8_t *>("main"), 4,
&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());
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++;
});
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
size_t accepted_count = 0;
for (size_t k = 0; k < invites_sent; ++k) {
if (friend_states[k]->group_number != UINT32_MAX) {
accepted_count++;
}
}
while (invites_sent < friends.size() && (invites_sent - accepted_count) < 5) {
Tox_Err_Group_Invite_Friend err_invite;
if (tox_group_invite_friend(main_tox, main_state.group_number,
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
}
}
bool all_see_all = true;
if (main_state.peer_count < friends.size()) {
all_see_all = false;
}
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;
}
}
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++;
for (const auto &fs : friend_states) {
if (fs->group_number != UINT32_MAX) {
joined++;
if (fs->peer_count >= friends.size())
fully_connected++;
}
}
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;
}
return false;
},
300000); // 5 minutes
return success ? main_state.group_number : UINT32_MAX;
}
} // namespace tox::test

View File

@@ -0,0 +1,226 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2026 The TokTok team.
*/
#include "public/tox_network.hh"
#include <gtest/gtest.h>
namespace tox::test {
namespace {
TEST(ToxNetworkTest, SetupConnectedFriends)
{
Simulation sim;
sim.net().set_latency(5);
auto main_node = sim.create_node();
auto main_tox = main_node->create_tox();
ASSERT_NE(main_tox, nullptr);
const int num_friends = 3;
auto friends = setup_connected_friends(sim, main_tox.get(), *main_node, num_friends);
ASSERT_EQ(friends.size(), num_friends);
for (const auto &f : friends) {
EXPECT_NE(tox_friend_get_connection_status(main_tox.get(), f.friend_number, nullptr),
TOX_CONNECTION_NONE);
}
// Verify they can actually communicate
struct Context {
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++;
});
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);
}
sim.run_until([&]() {
tox_iterate(main_tox.get(), &ctx);
for (auto &f : friends) {
tox_iterate(f.tox.get(), nullptr);
}
return ctx.count == num_friends;
});
EXPECT_EQ(ctx.count, num_friends);
}
TEST(ToxNetworkTest, Setup50ConnectedFriends)
{
Simulation sim;
sim.net().set_latency(5);
auto main_node = sim.create_node();
auto main_tox = main_node->create_tox();
ASSERT_NE(main_tox, nullptr);
const int num_friends = 50;
auto friends = setup_connected_friends(sim, main_tox.get(), *main_node, num_friends);
ASSERT_EQ(friends.size(), num_friends);
struct Context {
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++;
});
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);
}
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);
EXPECT_EQ(ctx.count, num_friends);
}
TEST(ToxNetworkTest, ConnectFriends)
{
Simulation sim;
sim.net().set_latency(5);
auto node1 = sim.create_node();
auto tox1 = node1->create_tox();
auto node2 = sim.create_node();
auto tox2 = node2->create_tox();
ASSERT_NE(tox1, nullptr);
ASSERT_NE(tox2, nullptr);
ASSERT_TRUE(connect_friends(sim, *node1, tox1.get(), *node2, tox2.get()));
EXPECT_NE(tox_friend_get_connection_status(tox1.get(), 0, nullptr), TOX_CONNECTION_NONE);
EXPECT_NE(tox_friend_get_connection_status(tox2.get(), 0, nullptr), TOX_CONNECTION_NONE);
// Verify communication
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;
});
const uint8_t msg[] = "hello";
tox_friend_send_message(tox1.get(), 0, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), nullptr);
sim.run_until([&]() {
tox_iterate(tox1.get(), nullptr);
tox_iterate(tox2.get(), &received);
return received;
});
EXPECT_TRUE(received);
}
TEST(ToxNetworkTest, SetupConnectedGroup)
{
Simulation sim;
sim.net().set_latency(5);
auto main_node = sim.create_node();
auto main_tox = main_node->create_tox();
ASSERT_NE(main_tox, nullptr);
const int num_friends = 15;
auto friends = setup_connected_friends(sim, main_tox.get(), *main_node, num_friends);
ASSERT_EQ(friends.size(), num_friends);
uint32_t group_number = setup_connected_group(sim, main_tox.get(), friends);
EXPECT_NE(group_number, UINT32_MAX);
// Verify we can send a group message
struct Context {
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++; });
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);
}
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);
EXPECT_EQ(ctx.count, num_friends);
}
TEST(ToxNetworkTest, Setup50ConnectedGroup)
{
Simulation sim;
sim.net().set_latency(5);
auto main_node = sim.create_node();
auto main_tox = main_node->create_tox();
ASSERT_NE(main_tox, nullptr);
const int num_friends = 50;
auto friends = setup_connected_friends(sim, main_tox.get(), *main_node, num_friends);
ASSERT_EQ(friends.size(), num_friends);
uint32_t group_number = setup_connected_group(sim, main_tox.get(), friends);
EXPECT_NE(group_number, UINT32_MAX);
struct Context {
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++; });
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);
}
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);
EXPECT_EQ(ctx.count, num_friends);
}
} // namespace
} // namespace tox::test