From 4962bdbb80861734bf74cbb60ad8d33f596ba074 Mon Sep 17 00:00:00 2001 From: iphydf Date: Sat, 17 Jan 2026 03:59:52 +0000 Subject: [PATCH] test: Improve TCP simulation and add tests - `FakeTcpSocket` handles basic TCP state machine (SYN, ACK, RST, buffering). - `NetworkUniverse` handles TCP routing and loopback. - Add `TcpRelayChaining` test. - Add LogFilter to Simulation. --- .github/scripts/flags-gcc.sh | 3 +- .../scenarios/scenario_group_topic_test.c | 4 +- other/analysis/run-clang | 2 +- other/analysis/run-gcc | 4 +- testing/support/BUILD.bazel | 22 + testing/support/CMakeLists.txt | 2 + testing/support/doubles/fake_network_stack.cc | 4 +- testing/support/doubles/fake_network_stack.hh | 3 +- .../doubles/fake_network_stack_test.cc | 57 +++ .../support/doubles/fake_network_tcp_test.cc | 459 ++++++++++++++++++ testing/support/doubles/fake_sockets.cc | 213 +++++++- testing/support/doubles/fake_sockets.hh | 11 +- testing/support/doubles/fake_sockets_test.cc | 74 +++ testing/support/doubles/network_universe.cc | 159 +++++- testing/support/doubles/network_universe.hh | 17 +- .../support/doubles/network_universe_test.cc | 81 ++++ testing/support/public/simulation.hh | 54 +++ testing/support/simulation_test.cc | 76 +++ testing/support/src/simulation.cc | 129 ++++- testing/support/tox_network_test.cc | 178 +++++++ toxav/rtp_test.cc | 16 +- toxcore/BUILD.bazel | 4 - toxcore/crypto_core_test_util.cc | 7 + toxcore/crypto_core_test_util.hh | 2 + toxcore/net_crypto_test.cc | 2 +- toxcore/onion_client_test.cc | 13 +- toxcore/sort_test.cc | 8 +- 27 files changed, 1531 insertions(+), 73 deletions(-) create mode 100644 testing/support/doubles/fake_network_tcp_test.cc create mode 100644 testing/support/simulation_test.cc diff --git a/.github/scripts/flags-gcc.sh b/.github/scripts/flags-gcc.sh index 50a0531ac..df3ba21b3 100644 --- a/.github/scripts/flags-gcc.sh +++ b/.github/scripts/flags-gcc.sh @@ -22,7 +22,6 @@ add_flag -Wframe-larger-than=9000 add_flag -Wignored-attributes add_flag -Wignored-qualifiers add_flag -Winit-self -add_flag -Winline add_flag -Wlarger-than=530000 add_flag -Wmaybe-uninitialized add_flag -Wmemset-transposed-args @@ -45,6 +44,8 @@ add_flag -Wunused-value # Disable specific warning flags for both C and C++. +# Not important, and bothersome with lambdas in C++. +add_flag -Wno-inline # struct Foo foo = {0}; is a common idiom. add_flag -Wno-missing-field-initializers # Checked by clang, but gcc is warning when it's not necessary. diff --git a/auto_tests/scenarios/scenario_group_topic_test.c b/auto_tests/scenarios/scenario_group_topic_test.c index c6f3b5a62..cf2446165 100644 --- a/auto_tests/scenarios/scenario_group_topic_test.c +++ b/auto_tests/scenarios/scenario_group_topic_test.c @@ -305,6 +305,8 @@ int main(int argc, char *argv[]) return 0; } -#undef GROUP_NAME +#undef TOPIC2 +#undef TOPIC1 #undef GROUP_NAME_LEN +#undef GROUP_NAME #undef NUM_PEERS diff --git a/other/analysis/run-clang b/other/analysis/run-clang index bafcc6f21..bfcba0c81 100755 --- a/other/analysis/run-clang +++ b/other/analysis/run-clang @@ -10,7 +10,7 @@ run() { "${CPPFLAGS[@]}" \ "${LDFLAGS[@]}" \ "$@" \ - -std=c++17 \ + -std=c++20 \ -Werror \ -Weverything \ -Wno-alloca \ diff --git a/other/analysis/run-gcc b/other/analysis/run-gcc index c32529826..2fa4df48e 100755 --- a/other/analysis/run-gcc +++ b/other/analysis/run-gcc @@ -11,7 +11,7 @@ run() { "${CPPFLAGS[@]}" \ "${LDFLAGS[@]}" \ "$@" \ - -std=c++17 \ + -std=c++20 \ -fdiagnostics-color=always \ -Wall \ -Wextra \ @@ -20,6 +20,7 @@ run() { -Wno-aggressive-loop-optimizations \ -Wno-float-conversion \ -Wno-format-signedness \ + -Wno-inline \ -Wno-missing-field-initializers \ -Wno-nonnull-compare \ -Wno-padded \ @@ -46,7 +47,6 @@ run() { -Wignored-attributes \ -Wignored-qualifiers \ -Winit-self \ - -Winline \ -Wlarger-than=530000 \ -Wmaybe-uninitialized \ -Wmemset-transposed-args \ diff --git a/testing/support/BUILD.bazel b/testing/support/BUILD.bazel index f259b1871..8d1e1c67d 100644 --- a/testing/support/BUILD.bazel +++ b/testing/support/BUILD.bazel @@ -80,6 +80,17 @@ cc_test( ], ) +cc_test( + name = "fake_network_tcp_test", + srcs = ["doubles/fake_network_tcp_test.cc"], + deps = [ + ":support", + "@com_google_googletest//:gtest", + "@com_google_googletest//:gtest_main", + "@psocket", + ], +) + cc_test( name = "fake_network_stack_test", srcs = ["doubles/fake_network_stack_test.cc"], @@ -114,6 +125,16 @@ cc_test( ], ) +cc_test( + name = "simulation_test", + srcs = ["simulation_test.cc"], + deps = [ + ":support", + "@com_google_googletest//:gtest", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "tox_network_test", timeout = "long", @@ -121,6 +142,7 @@ cc_test( deps = [ ":support", "//c-toxcore/toxcore:attributes", + "//c-toxcore/toxcore:network", "//c-toxcore/toxcore:tox", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", diff --git a/testing/support/CMakeLists.txt b/testing/support/CMakeLists.txt index 6d1d471b4..35ee40491 100644 --- a/testing/support/CMakeLists.txt +++ b/testing/support/CMakeLists.txt @@ -69,6 +69,8 @@ if(TARGET GTest::gtest_main) support_test(fake_sockets_test doubles/fake_sockets_test.cc) support_test(fake_network_stack_test doubles/fake_network_stack_test.cc) + support_test(fake_network_udp_test doubles/fake_network_udp_test.cc) + support_test(fake_network_tcp_test doubles/fake_network_tcp_test.cc) support_test(network_universe_test doubles/network_universe_test.cc) support_test(bootstrap_scaling_test bootstrap_scaling_test.cc) support_test(tox_network_test tox_network_test.cc) diff --git a/testing/support/doubles/fake_network_stack.cc b/testing/support/doubles/fake_network_stack.cc index 6f60705ec..2fe046eeb 100644 --- a/testing/support/doubles/fake_network_stack.cc +++ b/testing/support/doubles/fake_network_stack.cc @@ -8,7 +8,7 @@ namespace tox::test { -static const Network_Funcs kVtable = { +static const Network_Funcs kNetworkVtable = { .close = [](void *_Nonnull obj, Socket sock) { return static_cast(obj)->close(sock); }, .accept = [](void *_Nonnull obj, @@ -102,7 +102,7 @@ FakeNetworkStack::FakeNetworkStack(NetworkUniverse &universe, const IP &node_ip) FakeNetworkStack::~FakeNetworkStack() = default; -struct Network FakeNetworkStack::c_network() { return Network{&kVtable, this}; } +struct Network FakeNetworkStack::c_network() { return Network{&kNetworkVtable, this}; } Socket FakeNetworkStack::socket(int domain, int type, int protocol) { diff --git a/testing/support/doubles/fake_network_stack.hh b/testing/support/doubles/fake_network_stack.hh index 30fdbed0b..6dcb9f8a5 100644 --- a/testing/support/doubles/fake_network_stack.hh +++ b/testing/support/doubles/fake_network_stack.hh @@ -45,14 +45,13 @@ public: struct Network c_network() override; // For testing/fuzzing introspection + FakeSocket *_Nullable get_sock(Socket sock); FakeUdpSocket *_Nullable get_udp_socket(Socket sock); std::vector get_bound_udp_sockets(); NetworkUniverse &universe() { return universe_; } private: - FakeSocket *_Nullable get_sock(Socket sock); - NetworkUniverse &universe_; std::map> sockets_; int next_fd_ = 100; diff --git a/testing/support/doubles/fake_network_stack_test.cc b/testing/support/doubles/fake_network_stack_test.cc index c171363cd..a6042a8dd 100644 --- a/testing/support/doubles/fake_network_stack_test.cc +++ b/testing/support/doubles/fake_network_stack_test.cc @@ -87,5 +87,62 @@ namespace { ASSERT_NE(net_socket_to_native(accepted), -1); } + TEST_F(FakeNetworkStackTest, LoopbackRedirection) + { + // 1. Create a stack with a specific IP (20.0.0.1) + FakeNetworkStack my_stack{universe, make_ip(0x14000001)}; + Socket sock = my_stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + IP_Port bind_addr; + ip_init(&bind_addr.ip, false); + bind_addr.ip.ip.v4.uint32 = net_htonl(0x14000001); + bind_addr.port = net_htons(12345); + ASSERT_EQ(my_stack.bind(sock, &bind_addr), 0); + ASSERT_EQ(my_stack.listen(sock, 5), 0); + + // 2. Connect to 127.0.0.1:12345 from the same stack + Socket client = my_stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + IP_Port connect_addr; + ip_init(&connect_addr.ip, false); + connect_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001); + connect_addr.port = net_htons(12345); + + // Should redirect to 20.0.0.1:12345 because 127.0.0.1 is not bound + ASSERT_EQ(my_stack.connect(client, &connect_addr), -1); + ASSERT_EQ(errno, EINPROGRESS); + + universe.process_events(0); // SYN + + Socket accepted = my_stack.accept(sock); + ASSERT_NE(net_socket_to_native(accepted), -1); + } + + TEST_F(FakeNetworkStackTest, ImplicitBindAvoidsCollision) + { + // Bind server to 33445 (default start of find_free_port) + Socket server = stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + IP_Port addr; + ip_init(&addr.ip, false); + addr.ip.ip.v4.uint32 = 0; + addr.port = net_htons(33445); + ASSERT_EQ(stack.bind(server, &addr), 0); + + // Create client and connect (implicit bind) + Socket client = stack.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + IP_Port server_addr; + ip_init(&server_addr.ip, false); + server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001); + server_addr.port = net_htons(33445); + + // Should find a free port (not 33445) + ASSERT_EQ(stack.connect(client, &server_addr), -1); + ASSERT_EQ(errno, EINPROGRESS); + + auto *client_obj = stack.get_sock(client); + ASSERT_NE(client_obj, nullptr); + ASSERT_NE(client_obj->local_port(), 33445); + } + } // namespace } // namespace tox::test diff --git a/testing/support/doubles/fake_network_tcp_test.cc b/testing/support/doubles/fake_network_tcp_test.cc new file mode 100644 index 000000000..4cfa3e916 --- /dev/null +++ b/testing/support/doubles/fake_network_tcp_test.cc @@ -0,0 +1,459 @@ +#include + +#include "fake_sockets.hh" +#include "network_universe.hh" + +namespace tox::test { +namespace { + + class FakeNetworkTcpTest : public ::testing::Test { + protected: + NetworkUniverse universe; + + IP make_ip(uint8_t a, uint8_t b, uint8_t c, uint8_t d) + { + IP ip; + ip_init(&ip, false); + ip.ip.v4.uint8[0] = a; + ip.ip.v4.uint8[1] = b; + ip.ip.v4.uint8[2] = c; + ip.ip.v4.uint8[3] = d; + return ip; + } + }; + + TEST_F(FakeNetworkTcpTest, MultipleConnectionsToSamePort) + { + universe.set_verbose(true); + IP server_ip = make_ip(10, 0, 0, 1); + uint16_t server_port = 12345; + + FakeTcpSocket server(universe); + server.set_ip(server_ip); + IP_Port server_addr{server_ip, net_htons(server_port)}; + ASSERT_EQ(server.bind(&server_addr), 0); + ASSERT_EQ(server.listen(5), 0); + + // Client 1 + IP client1_ip = make_ip(10, 0, 0, 2); + FakeTcpSocket client1(universe); + client1.set_ip(client1_ip); + client1.connect(&server_addr); + + // Client 2 (same IP as client 1, different port) + FakeTcpSocket client2(universe); + client2.set_ip(client1_ip); + client2.connect(&server_addr); + + // Handshake for both + // 1. SYNs + universe.process_events(0); + universe.process_events(0); + + // 2. SYN-ACKs + universe.process_events(0); + universe.process_events(0); + + // 3. ACKs + universe.process_events(0); + universe.process_events(0); + + auto accepted1 = server.accept(nullptr); + auto accepted2 = server.accept(nullptr); + + ASSERT_NE(accepted1, nullptr); + ASSERT_NE(accepted2, nullptr); + + EXPECT_EQ( + static_cast(accepted1.get())->state(), FakeTcpSocket::ESTABLISHED); + EXPECT_EQ( + static_cast(accepted2.get())->state(), FakeTcpSocket::ESTABLISHED); + + // Verify data isolation + const char *msg1 = "Message 1"; + const char *msg2 = "Message 2"; + + client1.send(reinterpret_cast(msg1), strlen(msg1)); + client2.send(reinterpret_cast(msg2), strlen(msg2)); + + universe.process_events(0); + universe.process_events(0); + + uint8_t buf[100]; + int len1 = accepted1->recv(buf, sizeof(buf)); + EXPECT_EQ(len1, strlen(msg1)); + int len2 = accepted2->recv(buf, sizeof(buf)); + EXPECT_EQ(len2, strlen(msg2)); + } + + TEST_F(FakeNetworkTcpTest, DuplicateSynCreatesDuplicateConnections) + { + universe.set_verbose(true); + IP server_ip = make_ip(10, 0, 0, 1); + uint16_t server_port = 12345; + + FakeTcpSocket server(universe); + server.set_ip(server_ip); + IP_Port server_addr{server_ip, net_htons(server_port)}; + server.bind(&server_addr); + server.listen(5); + + IP client_ip = make_ip(10, 0, 0, 2); + IP_Port client_addr{client_ip, net_htons(33445)}; + + Packet p{}; + p.from = client_addr; + p.to = server_addr; + p.is_tcp = true; + p.tcp_flags = 0x02; // SYN + p.seq = 100; + + universe.send_packet(p); + universe.send_packet(p); // Duplicate SYN + + universe.process_events(0); + universe.process_events(0); + + // Now send ACK from client + Packet ack{}; + ack.from = client_addr; + ack.to = server_addr; + ack.is_tcp = true; + ack.tcp_flags = 0x10; // ACK + ack.ack = 101; + + universe.send_packet(ack); + universe.process_events(0); + + auto accepted1 = server.accept(nullptr); + auto accepted2 = server.accept(nullptr); + + ASSERT_NE(accepted1, nullptr); + EXPECT_EQ(accepted2, nullptr); // This should pass now + } + + TEST_F(FakeNetworkTcpTest, PeerCloseClearsConnection) + { + universe.set_verbose(true); + IP server_ip = make_ip(10, 0, 0, 1); + uint16_t server_port = 12345; + + FakeTcpSocket server(universe); + server.set_ip(server_ip); + IP_Port server_addr{server_ip, net_htons(server_port)}; + server.bind(&server_addr); + server.listen(5); + + IP client_ip = make_ip(10, 0, 0, 2); + FakeTcpSocket client(universe); + client.set_ip(client_ip); + client.connect(&server_addr); + + // Handshake + universe.process_events(0); // SYN + universe.process_events(0); // SYN-ACK + universe.process_events(0); // ACK + + auto accepted = server.accept(nullptr); + ASSERT_NE(accepted, nullptr); + EXPECT_EQ( + static_cast(accepted.get())->state(), FakeTcpSocket::ESTABLISHED); + + // Client closes + client.close(); + universe.process_events(0); // Deliver RST/FIN + + // Server should no longer be ESTABLISHED + EXPECT_EQ(static_cast(accepted.get())->state(), FakeTcpSocket::CLOSED); + + // Now if client reconnects with same port + FakeTcpSocket client2(universe); + client2.set_ip(client_ip); + client2.connect(&server_addr); + universe.process_events(0); // Deliver SYN + + // Node 2 port 20002 should have: 1 LISTEN, 0 ESTABLISHED (old one gone), 1 SYN_RECEIVED + // (new one) Total targets should be 2. + } + + TEST_F(FakeNetworkTcpTest, DataNotProcessedByMultipleSockets) + { + universe.set_verbose(true); + IP server_ip = make_ip(10, 0, 0, 1); + uint16_t server_port = 12345; + + FakeTcpSocket server(universe); + server.set_ip(server_ip); + IP_Port server_addr{server_ip, net_htons(server_port)}; + server.bind(&server_addr); + server.listen(5); + + IP client_ip = make_ip(10, 0, 0, 2); + IP_Port client_addr{client_ip, net_htons(33445)}; + + // Manually create two "established" sockets on the same port for the same peer + // This simulates a bug where duplicate connections were allowed. + auto sock1 = FakeTcpSocket::create_connected(universe, client_addr, server_port); + sock1->set_ip(server_ip); + auto sock2 = FakeTcpSocket::create_connected(universe, client_addr, server_port); + sock2->set_ip(server_ip); + + universe.bind_tcp(server_ip, server_port, sock1.get()); + universe.bind_tcp(server_ip, server_port, sock2.get()); + + // Send data from client to server + Packet p{}; + p.from = client_addr; + p.to = server_addr; + p.is_tcp = true; + p.tcp_flags = 0x10; // ACK (Data) + const char *data = "Unique"; + p.data.assign(data, data + strlen(data)); + + universe.send_packet(p); + universe.process_events(0); + + // Only ONE of them should have received it, or at least they shouldn't BOTH have it + // in a way that suggests duplicate delivery. + EXPECT_TRUE((sock1->recv_buffer_size() == strlen(data)) + ^ (sock2->recv_buffer_size() == strlen(data))); + } + + TEST_F(FakeNetworkTcpTest, ConnectionCollision) + { + universe.set_verbose(true); + IP server_ip = make_ip(10, 0, 0, 1); + uint16_t server_port = 12345; + + FakeTcpSocket server(universe); + server.set_ip(server_ip); + IP_Port server_addr{server_ip, net_htons(server_port)}; + server.bind(&server_addr); + server.listen(5); + + IP client_ip = make_ip(10, 0, 0, 2); + + FakeTcpSocket client1(universe); + client1.set_ip(client_ip); + // Bind to specific port to force collision later + IP_Port client_bind_addr{client_ip, net_htons(33445)}; + client1.bind(&client_bind_addr); + client1.connect(&server_addr); + + // Handshake 1 + universe.process_events(0); // SYN + universe.process_events(0); // SYN-ACK + universe.process_events(0); // ACK + + auto accepted1 = server.accept(nullptr); + ASSERT_NE(accepted1, nullptr); + EXPECT_EQ( + static_cast(accepted1.get())->state(), FakeTcpSocket::ESTABLISHED); + + // Now client 1 "reconnects" (e.g. after a crash or timeout, but using same port) + FakeTcpSocket client2(universe); + client2.set_ip(client_ip); + client2.bind(&client_bind_addr); // Forced collision + client2.connect(&server_addr); + + // Deliver new SYN + universe.process_events(0); + + // server_addr port 12345 now has: + // 1. LISTEN socket + // 2. accepted1 (ESTABLISHED with 10.0.0.2:33445) + + // In our simplified simulation, the ESTABLISHED socket now handles the SYN by returning + // true (ignoring it). So no new connection is created. + auto accepted2 = server.accept(nullptr); + EXPECT_EQ(accepted2, nullptr); + + const char *msg1 = "Data 1"; + client1.send(reinterpret_cast(msg1), strlen(msg1)); + universe.process_events(0); + + // Data should still go to accepted1 + EXPECT_EQ(accepted1->recv_buffer_size(), strlen(msg1)); + } + + TEST_F(FakeNetworkTcpTest, LoopbackConnection) + { + universe.set_verbose(true); + IP node_ip = make_ip(10, 0, 0, 1); + uint16_t port = 12345; + + FakeTcpSocket server(universe); + server.set_ip(node_ip); + IP_Port listen_addr{node_ip, net_htons(port)}; + server.bind(&listen_addr); + server.listen(5); + + FakeTcpSocket client(universe); + client.set_ip(node_ip); + IP loopback_ip; + ip_init(&loopback_ip, false); + loopback_ip.ip.v4.uint32 = net_htonl(0x7F000001); + IP_Port server_loopback_addr{loopback_ip, net_htons(port)}; + + client.connect(&server_loopback_addr); + + // SYN (Client -> 127.0.0.1:12345) + universe.process_events(0); + + // SYN-ACK (Server -> Client) + universe.process_events(0); + + // ACK (Client -> Server) + universe.process_events(0); + + EXPECT_EQ(client.state(), FakeTcpSocket::ESTABLISHED); + auto accepted = server.accept(nullptr); + ASSERT_NE(accepted, nullptr); + EXPECT_EQ( + static_cast(accepted.get())->state(), FakeTcpSocket::ESTABLISHED); + + // Data Transfer + const char *msg = "Loopback"; + client.send(reinterpret_cast(msg), strlen(msg)); + universe.process_events(0); + + uint8_t buf[100]; + int len = accepted->recv(buf, sizeof(buf)); + ASSERT_EQ(len, strlen(msg)); + EXPECT_EQ(std::string(reinterpret_cast(buf), len), msg); + } + + TEST_F(FakeNetworkTcpTest, SimultaneousConnect) + { + universe.set_verbose(true); + IP ipA = make_ip(10, 0, 0, 1); + IP ipB = make_ip(10, 0, 0, 2); + uint16_t portA = 10001; + uint16_t portB = 10002; + + FakeTcpSocket sockA(universe); + sockA.set_ip(ipA); + IP_Port addrA{ipA, net_htons(portA)}; + sockA.bind(&addrA); + sockA.listen(5); + + FakeTcpSocket sockB(universe); + sockB.set_ip(ipB); + IP_Port addrB{ipB, net_htons(portB)}; + sockB.bind(&addrB); + sockB.listen(5); + + // A connects to B + sockA.connect(&addrB); + // B connects to A + sockB.connect(&addrA); + + // This is "simultaneous open" in TCP but here they are also LISTENing. + // Toxcore uses this pattern sometimes. + + universe.process_events(0); // SYN from A to B + universe.process_events(0); // SYN from B to A + + universe.process_events(0); // SYN-ACK from B to A (for A's SYN) + universe.process_events(0); // SYN-ACK from A to B (for B's SYN) + + universe.process_events(0); // ACK from A to B + universe.process_events(0); // ACK from B to A + + EXPECT_EQ(sockA.state(), FakeTcpSocket::ESTABLISHED); + EXPECT_EQ(sockB.state(), FakeTcpSocket::ESTABLISHED); + } + + TEST_F(FakeNetworkTcpTest, DataInHandshakeAck) + { + universe.set_verbose(true); + IP server_ip = make_ip(10, 0, 0, 1); + uint16_t server_port = 12345; + + FakeTcpSocket server(universe); + server.set_ip(server_ip); + IP_Port server_addr{server_ip, net_htons(server_port)}; + server.bind(&server_addr); + server.listen(5); + + IP client_ip = make_ip(10, 0, 0, 2); + IP_Port client_addr{client_ip, net_htons(33445)}; + + // 1. SYN + Packet syn{}; + syn.from = client_addr; + syn.to = server_addr; + syn.is_tcp = true; + syn.tcp_flags = 0x02; + universe.send_packet(syn); + universe.process_events(0); + + // 2. SYN-ACK (Server -> Client) + universe.process_events(0); + + // 3. ACK + Data (Client -> Server) + Packet ack{}; + ack.from = client_addr; + ack.to = server_addr; + ack.is_tcp = true; + ack.tcp_flags = 0x10; + const char *data = "HandshakeData"; + ack.data.assign(data, data + strlen(data)); + universe.send_packet(ack); + universe.process_events(0); + + auto accepted = server.accept(nullptr); + ASSERT_NE(accepted, nullptr); + EXPECT_EQ(accepted->recv_buffer_size(), strlen(data)); + } + + TEST_F(FakeNetworkTcpTest, LoopbackWithNodeIPMixed) + { + universe.set_verbose(true); + IP node_ip = make_ip(10, 0, 0, 1); + uint16_t port = 12345; + + FakeTcpSocket server(universe); + server.set_ip(node_ip); + IP_Port listen_addr{node_ip, net_htons(port)}; + server.bind(&listen_addr); + server.listen(5); + + FakeTcpSocket client(universe); + client.set_ip(node_ip); + IP loopback_ip; + ip_init(&loopback_ip, false); + loopback_ip.ip.v4.uint32 = net_htonl(0x7F000001); + IP_Port server_loopback_addr{loopback_ip, net_htons(port)}; + + // Client connects to 127.0.0.1 + client.connect(&server_loopback_addr); + + universe.process_events(0); // SYN (Client -> 127.0.0.1) + universe.process_events(0); // SYN-ACK (Server -> Client) + universe.process_events(0); // ACK (Client -> Server) + + EXPECT_EQ(client.state(), FakeTcpSocket::ESTABLISHED); + auto accepted = server.accept(nullptr); + ASSERT_NE(accepted, nullptr); + + // Now manually simulate a packet coming from the server's EXTERNAL IP to the client. + // This happens because the server socket is bound to node_ip, so its packets might + // be delivered as coming from node_ip even if the client connected to 127.0.0.1. + Packet p{}; + p.from = listen_addr; // node_ip:port + p.to.ip = node_ip; + p.to.port = net_htons(client.local_port()); + p.is_tcp = true; + p.tcp_flags = 0x10; // ACK + const char *msg = "MixedIP"; + p.data.assign(msg, msg + strlen(msg)); + + universe.send_packet(p); + universe.process_events(0); + + EXPECT_EQ(client.recv_buffer_size(), strlen(msg)); + } + +} +} diff --git a/testing/support/doubles/fake_sockets.cc b/testing/support/doubles/fake_sockets.cc index 47c012a13..628d044e5 100644 --- a/testing/support/doubles/fake_sockets.cc +++ b/testing/support/doubles/fake_sockets.cc @@ -118,6 +118,12 @@ int FakeUdpSocket::recv(uint8_t *_Nonnull buf, size_t len) return -1; } +size_t FakeUdpSocket::recv_buffer_size() +{ + std::lock_guard lock(mutex_); + return recv_queue_.size(); +} + int FakeUdpSocket::sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr) { std::lock_guard lock(mutex_); @@ -232,8 +238,8 @@ void FakeUdpSocket::set_recv_observer(RecvObserver observer) FakeTcpSocket::FakeTcpSocket(NetworkUniverse &universe) : FakeSocket(universe, SOCK_STREAM) - , remote_addr_{} { + ipport_reset(&remote_addr_); } FakeTcpSocket::~FakeTcpSocket() { close_impl(); } @@ -241,6 +247,17 @@ FakeTcpSocket::~FakeTcpSocket() { close_impl(); } int FakeTcpSocket::close() { std::lock_guard lock(mutex_); + if (state_ == ESTABLISHED || state_ == SYN_SENT || state_ == SYN_RECEIVED + || state_ == CLOSE_WAIT) { + // Send RST to peer + Packet p{}; + p.from.ip = ip_; + p.from.port = net_htons(local_port_); + p.to = remote_addr_; + p.is_tcp = true; + p.tcp_flags = 0x04; // RST + universe_.send_packet(p); + } close_impl(); return 0; } @@ -286,11 +303,22 @@ int FakeTcpSocket::listen(int backlog) int FakeTcpSocket::connect(const IP_Port *_Nonnull addr) { std::lock_guard lock(mutex_); + if (universe_.is_verbose()) { + Ip_Ntoa ip_str, dest_str; + net_ip_ntoa(&ip_, &ip_str); + net_ip_ntoa(&addr->ip, &dest_str); + std::cerr << "[FakeTcpSocket] connect from " << ip_str.buf << " to " << dest_str.buf << ":" + << net_ntohs(addr->port) << std::endl; + } + if (local_port_ == 0) { // Implicit bind uint16_t p = universe_.find_free_port(ip_); if (universe_.bind_tcp(ip_, p, this)) { local_port_ = p; + if (universe_.is_verbose()) { + std::cerr << "[FakeTcpSocket] implicit bind to port " << local_port_ << std::endl; + } } else { errno = EADDRINUSE; return -1; @@ -325,13 +353,16 @@ std::unique_ptr FakeTcpSocket::accept(IP_Port *_Nullable addr) return nullptr; } - if (pending_connections_.empty()) { + auto it = std::find_if(pending_connections_.begin(), pending_connections_.end(), + [](const std::unique_ptr &s) { return s->state() == ESTABLISHED; }); + + if (it == pending_connections_.end()) { errno = EWOULDBLOCK; return nullptr; } - auto client = std::move(pending_connections_.front()); - pending_connections_.pop_front(); + auto client = std::move(*it); + pending_connections_.erase(it); if (addr) { *addr = client->remote_addr(); @@ -343,7 +374,15 @@ int FakeTcpSocket::send(const uint8_t *_Nonnull buf, size_t len) { std::lock_guard lock(mutex_); if (state_ != ESTABLISHED) { - errno = ENOTCONN; + if (universe_.is_verbose()) { + std::cerr << "[FakeTcpSocket] send failed: state " << state_ << " port " << local_port_ + << std::endl; + } + if (state_ == SYN_SENT || state_ == SYN_RECEIVED) { + errno = EWOULDBLOCK; + } else { + errno = ENOTCONN; + } return -1; } @@ -375,6 +414,13 @@ int FakeTcpSocket::recv(uint8_t *_Nonnull buf, size_t len) } size_t actual = std::min(len, recv_buffer_.size()); + if (universe_.is_verbose() && actual > 0) { + char remote_ip_str[TOX_INET_ADDRSTRLEN]; + ip_parse_addr(&remote_addr_.ip, remote_ip_str, sizeof(remote_ip_str)); + std::cerr << "[FakeTcpSocket] Port " << local_port_ << " (Peer: " << remote_ip_str << ":" + << net_ntohs(remote_addr_.port) << ") recv requested " << len << " got " << actual + << " (remaining " << recv_buffer_.size() - actual << ")" << std::endl; + } for (size_t i = 0; i < actual; ++i) { buf[i] = recv_buffer_.front(); recv_buffer_.pop_front(); @@ -388,6 +434,22 @@ size_t FakeTcpSocket::recv_buffer_size() return recv_buffer_.size(); } +bool FakeTcpSocket::is_readable() +{ + std::lock_guard lock(mutex_); + if (state_ == LISTEN) { + return std::any_of(pending_connections_.begin(), pending_connections_.end(), + [](const std::unique_ptr &s) { return s->state() == ESTABLISHED; }); + } + return !recv_buffer_.empty() || state_ == CLOSED || state_ == CLOSE_WAIT; +} + +bool FakeTcpSocket::is_writable() +{ + std::lock_guard lock(mutex_); + return state_ == ESTABLISHED; +} + int FakeTcpSocket::sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr) { errno = EOPNOTSUPP; @@ -399,26 +461,82 @@ int FakeTcpSocket::recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull return -1; } -void FakeTcpSocket::handle_packet(const Packet &p) +int FakeTcpSocket::getsockopt( + int level, int optname, void *_Nonnull optval, size_t *_Nonnull optlen) +{ + if (universe_.is_verbose()) { + std::cerr << "[FakeTcpSocket] getsockopt level=" << level << " optname=" << optname + << " state=" << state_ << std::endl; + } + if (level == SOL_SOCKET && optname == SO_ERROR) { + int error = 0; + if (state_ == SYN_SENT || state_ == SYN_RECEIVED) { + error = EINPROGRESS; + } else if (state_ == CLOSED) { + error = ECONNREFUSED; + } + + if (*optlen >= sizeof(int)) { + *static_cast(optval) = error; + *optlen = sizeof(int); + } + if (universe_.is_verbose()) { + std::cerr << "[FakeTcpSocket] getsockopt SO_ERROR returning error=" << error + << std::endl; + } + return 0; + } + return 0; +} + +bool FakeTcpSocket::handle_packet(const Packet &p) { std::lock_guard lock(mutex_); if (universe_.is_verbose()) { - std::cerr << "Handle Packet: Port " << local_port_ << " Flags " - << static_cast(p.tcp_flags) << " State " << state_ << std::endl; + char remote_ip_str[TOX_INET_ADDRSTRLEN]; + ip_parse_addr(&remote_addr_.ip, remote_ip_str, sizeof(remote_ip_str)); + std::cerr << "Handle Packet: Port " << local_port_ << " (Peer: " << remote_ip_str << ":" + << net_ntohs(remote_addr_.port) << ") Flags " << TcpFlags{p.tcp_flags} + << " State " << state_ << " From " << net_ntohs(p.from.port) << std::endl; + } + + if (state_ != LISTEN) { + // Filter packets not from our peer + bool port_match = net_ntohs(p.from.port) == net_ntohs(remote_addr_.port); + bool ip_match = ip_equal(&p.from.ip, &remote_addr_.ip) + || (is_loopback(p.from.ip) && ip_equal(&remote_addr_.ip, &ip_)) + || (is_loopback(remote_addr_.ip) && ip_equal(&p.from.ip, &ip_)); + + if (!port_match || !ip_match) { + return false; + } + + if (p.tcp_flags & 0x04) { // RST + state_ = CLOSED; + if (local_port_ != 0) { + universe_.unbind_tcp(ip_, local_port_, this); + local_port_ = 0; + } + return true; + } } if (state_ == LISTEN) { if (p.tcp_flags & 0x02) { // SYN + // Check for duplicate SYN from same peer + for (const auto &pending : pending_connections_) { + if (ipport_equal(&p.from, &pending->remote_addr_)) { + return true; + } + } + // Create new socket for connection auto new_sock = std::make_unique(universe_); - // Bind to ephemeral? No, it's accepted on the same port but distinct 4-tuple. - // In our simplified model, the new socket is not bound to the global map - // until accepted? Or effectively bound to the 4-tuple. - // For now, let's just create it and queue it. new_sock->state_ = SYN_RECEIVED; new_sock->remote_addr_ = p.from; new_sock->local_port_ = local_port_; + new_sock->set_ip(ip_); // Inherit IP from listening socket new_sock->last_ack_ = p.seq + 1; new_sock->next_seq_ = 1000; // Random ISN @@ -435,13 +553,9 @@ void FakeTcpSocket::handle_packet(const Packet &p) universe_.send_packet(resp); - // In real TCP, we wait for ACK to move to ESTABLISHED and accept queue. - // Here we cheat and move to ESTABLISHED immediately or wait for ACK? - // Let's wait for ACK. - // But where do we store this half-open socket? - // For simplicity: auto-transition to ESTABLISHED and queue it. - new_sock->state_ = ESTABLISHED; + // Add to pending, but it's still SYN_RECEIVED pending_connections_.push_back(std::move(new_sock)); + return true; } } else if (state_ == SYN_SENT) { if ((p.tcp_flags & 0x12) == 0x12) { // SYN | ACK @@ -458,8 +572,31 @@ void FakeTcpSocket::handle_packet(const Packet &p) ack.seq = next_seq_; ack.ack = last_ack_; universe_.send_packet(ack); + return true; + } else if (p.tcp_flags & 0x02) { // SYN (Simultaneous Open) + state_ = SYN_RECEIVED; + last_ack_ = p.seq + 1; + + // Send SYN-ACK + Packet resp{}; + resp.from = p.to; + resp.to = p.from; + resp.is_tcp = true; + resp.tcp_flags = 0x12; // SYN | ACK + resp.seq = next_seq_++; + resp.ack = last_ack_; + universe_.send_packet(resp); + return true; } - } else if (state_ == ESTABLISHED) { + } else if (state_ == SYN_RECEIVED) { + if (p.tcp_flags & 0x10) { // ACK + state_ = ESTABLISHED; + } else { + return false; + } + } + + if (state_ == ESTABLISHED) { if (p.tcp_flags & 0x01) { // FIN state_ = CLOSE_WAIT; // Send ACK @@ -471,12 +608,23 @@ void FakeTcpSocket::handle_packet(const Packet &p) ack.seq = next_seq_; ack.ack = p.seq + 1; // Consume FIN universe_.send_packet(ack); - } else if (!p.data.empty()) { - recv_buffer_.insert(recv_buffer_.end(), p.data.begin(), p.data.end()); - last_ack_ += p.data.size(); - // Should send ACK? + return true; + } else { + if (!p.data.empty()) { + if (universe_.is_verbose()) { + char remote_ip_str[TOX_INET_ADDRSTRLEN]; + ip_parse_addr(&remote_addr_.ip, remote_ip_str, sizeof(remote_ip_str)); + std::cerr << "[FakeTcpSocket] Port " << local_port_ + << " (Peer: " << remote_ip_str << ":" << net_ntohs(remote_addr_.port) + << ") adding " << p.data.size() << " bytes to buffer (currently " + << recv_buffer_.size() << ")" << std::endl; + } + recv_buffer_.insert(recv_buffer_.end(), p.data.begin(), p.data.end()); + } + return true; } } + return false; } std::unique_ptr FakeTcpSocket::create_connected( @@ -489,4 +637,23 @@ std::unique_ptr FakeTcpSocket::create_connected( return s; } +std::ostream &operator<<(std::ostream &os, FakeTcpSocket::State state) +{ + switch (state) { + case FakeTcpSocket::CLOSED: + return os << "CLOSED"; + case FakeTcpSocket::LISTEN: + return os << "LISTEN"; + case FakeTcpSocket::SYN_SENT: + return os << "SYN_SENT"; + case FakeTcpSocket::SYN_RECEIVED: + return os << "SYN_RECEIVED"; + case FakeTcpSocket::ESTABLISHED: + return os << "ESTABLISHED"; + case FakeTcpSocket::CLOSE_WAIT: + return os << "CLOSE_WAIT"; + } + return os << "UNKNOWN(" << static_cast(state) << ")"; +} + } // namespace tox::test diff --git a/testing/support/doubles/fake_sockets.hh b/testing/support/doubles/fake_sockets.hh index 4eabb3e91..727bdccce 100644 --- a/testing/support/doubles/fake_sockets.hh +++ b/testing/support/doubles/fake_sockets.hh @@ -43,6 +43,8 @@ public: virtual int recv(uint8_t *_Nonnull buf, size_t len) = 0; virtual size_t recv_buffer_size() { return 0; } + virtual bool is_readable() { return recv_buffer_size() > 0; } + virtual bool is_writable() { return true; } virtual int sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr) = 0; virtual int recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr) = 0; @@ -85,6 +87,7 @@ public: int send(const uint8_t *_Nonnull buf, size_t len) override; int recv(uint8_t *_Nonnull buf, size_t len) override; + size_t recv_buffer_size() override; int sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr) override; int recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr) override; @@ -134,12 +137,16 @@ public: int send(const uint8_t *_Nonnull buf, size_t len) override; int recv(uint8_t *_Nonnull buf, size_t len) override; size_t recv_buffer_size() override; + bool is_readable() override; + bool is_writable() override; int sendto(const uint8_t *_Nonnull buf, size_t len, const IP_Port *_Nonnull addr) override; int recvfrom(uint8_t *_Nonnull buf, size_t len, IP_Port *_Nonnull addr) override; + int getsockopt(int level, int optname, void *_Nonnull optval, size_t *_Nonnull optlen) override; + // Internal events - void handle_packet(const Packet &p); + bool handle_packet(const Packet &p); State state() const { return state_; } const IP_Port &remote_addr() const { return remote_addr_; } @@ -161,6 +168,8 @@ private: uint32_t last_ack_ = 0; }; +std::ostream &operator<<(std::ostream &os, FakeTcpSocket::State state); + } // namespace tox::test #endif // C_TOXCORE_TESTING_SUPPORT_DOUBLES_FAKE_SOCKETS_H diff --git a/testing/support/doubles/fake_sockets_test.cc b/testing/support/doubles/fake_sockets_test.cc index a5814968f..93c289a9b 100644 --- a/testing/support/doubles/fake_sockets_test.cc +++ b/testing/support/doubles/fake_sockets_test.cc @@ -82,6 +82,46 @@ namespace { EXPECT_EQ(std::string(reinterpret_cast(recv_buf), 5), "Hello"); } + TEST_F(FakeTcpSocketTest, RecvBuffering) + { + IP_Port server_addr; + ip_init(&server_addr.ip, false); + server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001); + server_addr.port = net_htons(8082); + + server.bind(&server_addr); + server.listen(5); + client.connect(&server_addr); + + universe.process_events(0); // SYN + universe.process_events(0); // SYN-ACK + universe.process_events(0); // ACK + + auto accepted = server.accept(nullptr); + ASSERT_NE(accepted, nullptr); + + uint8_t msg1[] = "Part1"; + uint8_t msg2[] = "Part2"; + client.send(msg1, 5); + client.send(msg2, 5); + + universe.process_events(0); // Deliver Part1 + universe.process_events(0); // Deliver Part2 + + EXPECT_EQ(accepted->recv_buffer_size(), 10); + + uint8_t recv_buf[20]; + // Read partial + ASSERT_EQ(accepted->recv(recv_buf, 3), 3); + EXPECT_EQ(std::string(reinterpret_cast(recv_buf), 3), "Par"); + EXPECT_EQ(accepted->recv_buffer_size(), 7); + + // Read rest + ASSERT_EQ(accepted->recv(recv_buf, 7), 7); + EXPECT_EQ(std::string(reinterpret_cast(recv_buf), 7), "t1Part2"); + EXPECT_EQ(accepted->recv_buffer_size(), 0); + } + class FakeUdpSocketTest : public ::testing::Test { public: ~FakeUdpSocketTest() override; @@ -119,6 +159,40 @@ namespace { EXPECT_EQ(sender_addr.port, net_htons(client.local_port())); } + TEST_F(FakeUdpSocketTest, RecvBuffering) + { + IP_Port server_addr; + ip_init(&server_addr.ip, false); + server_addr.ip.ip.v4.uint32 = net_htonl(0x7F000001); + server_addr.port = net_htons(9001); + + server.bind(&server_addr); + + const char *msg1 = "Msg1"; + const char *msg2 = "Msg2"; + + client.sendto(reinterpret_cast(msg1), strlen(msg1), &server_addr); + client.sendto(reinterpret_cast(msg2), strlen(msg2), &server_addr); + + universe.process_events(0); // Deliver msg1 + universe.process_events(0); // Deliver msg2 + + EXPECT_EQ(server.recv_buffer_size(), 2); + + IP_Port sender; + uint8_t buf[10]; + + int len = server.recvfrom(buf, sizeof(buf), &sender); + ASSERT_EQ(len, 4); + EXPECT_EQ(std::string(reinterpret_cast(buf), len), "Msg1"); + EXPECT_EQ(server.recv_buffer_size(), 1); + + len = server.recvfrom(buf, sizeof(buf), &sender); + ASSERT_EQ(len, 4); + EXPECT_EQ(std::string(reinterpret_cast(buf), len), "Msg2"); + EXPECT_EQ(server.recv_buffer_size(), 0); + } + } // namespace } // namespace tox::test diff --git a/testing/support/doubles/network_universe.cc b/testing/support/doubles/network_universe.cc index 33fb554a1..63e8752a8 100644 --- a/testing/support/doubles/network_universe.cc +++ b/testing/support/doubles/network_universe.cc @@ -7,6 +7,30 @@ namespace tox::test { +std::ostream &operator<<(std::ostream &os, TcpFlags flags) +{ + bool first = true; + if (flags.value & 0x02) { + os << (first ? "" : "|") << "SYN"; + first = false; + } + if (flags.value & 0x10) { + os << (first ? "" : "|") << "ACK"; + first = false; + } + if (flags.value & 0x01) { + os << (first ? "" : "|") << "FIN"; + first = false; + } + if (flags.value & 0x04) { + os << (first ? "" : "|") << "RST"; + first = false; + } + if (first) + os << "NONE"; + return os << "(" << static_cast(flags.value) << ")"; +} + bool NetworkUniverse::IP_Port_Key::operator<(const IP_Port_Key &other) const { if (port != other.port) @@ -75,6 +99,22 @@ void NetworkUniverse::send_packet(Packet p) p.delivery_time += global_latency_ms_; std::lock_guard lock(mutex_); + p.sequence_number = next_packet_id_++; + + if (verbose_) { + Ip_Ntoa from_str, to_str; + net_ip_ntoa(&p.from.ip, &from_str); + net_ip_ntoa(&p.to.ip, &to_str); + std::cerr << "[NetworkUniverse] Enqueued packet #" << p.sequence_number << " from " + << from_str.buf << ":" << net_ntohs(p.from.port) << " to " << to_str.buf << ":" + << net_ntohs(p.to.port); + if (p.is_tcp) { + std::cerr << " (TCP Flags=" << TcpFlags{p.tcp_flags} << " Seq=" << p.seq + << " Ack=" << p.ack << ")"; + } + std::cerr << " with size " << p.data.size() << std::endl; + } + event_queue_.push(std::move(p)); } @@ -100,6 +140,23 @@ static IP extract_ipv4(const IP &ip) return ip4; } +bool is_loopback(const IP &ip) +{ + if (net_family_is_ipv4(ip.family)) { + return ip.ip.v4.uint32 == net_htonl(0x7F000001); + } + if (net_family_is_ipv6(ip.family)) { + const uint8_t *b = ip.ip.v6.uint8; + for (int i = 0; i < 15; ++i) { + if (b[i] != 0) { + return false; + } + } + return b[15] == 1; + } + return false; +} + void NetworkUniverse::process_events(uint64_t current_time_ms) { while (true) { @@ -110,28 +167,98 @@ void NetworkUniverse::process_events(uint64_t current_time_ms) { std::lock_guard lock(mutex_); + if (!event_queue_.empty()) { + const Packet &top = event_queue_.top(); + if (verbose_) { + std::cerr << "[NetworkUniverse] Peek packet: time=" << top.delivery_time + << " current=" << current_time_ms << " tcp=" << top.is_tcp + << std::endl; + } + } + if (!event_queue_.empty() && event_queue_.top().delivery_time <= current_time_ms) { p = event_queue_.top(); event_queue_.pop(); has_packet = true; - if (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); + if (verbose_) { + Ip_Ntoa from_str, to_str; + net_ip_ntoa(&p.from.ip, &from_str); + net_ip_ntoa(&p.to.ip, &to_str); + std::cerr << "[NetworkUniverse] Processing packet #" << p.sequence_number + << " from " << from_str.buf << ":" << net_ntohs(p.from.port) << " to " + << to_str.buf << ":" << net_ntohs(p.to.port) + << " (TCP=" << (p.is_tcp ? "true" : "false"); + if (p.is_tcp) { + std::cerr << " Flags=" << TcpFlags{p.tcp_flags} << " Seq=" << p.seq + << " Ack=" << p.ack; } - if (tcp_targets.empty() && is_ipv4_mapped(p.to.ip)) { - IP ip4 = extract_ipv4(p.to.ip); - auto range4 = tcp_bindings_.equal_range({ip4, net_ntohs(p.to.port)}); - for (auto it = range4.first; it != range4.second; ++it) { - tcp_targets.push_back(it->second); + std::cerr << " Size=" << p.data.size() << ")" << std::endl; + } + + IP target_ip = p.to.ip; + + if (p.is_tcp) { + if (is_loopback(target_ip) + && tcp_bindings_.count({target_ip, net_ntohs(p.to.port)}) == 0) { + if (verbose_) { + std::cerr << "[NetworkUniverse] Loopback packet to " + << static_cast(target_ip.ip.v4.uint8[3]) + << " redirected to " + << static_cast(p.from.ip.ip.v4.uint8[3]) << std::endl; + } + target_ip = p.from.ip; + } + + auto range = tcp_bindings_.equal_range({target_ip, net_ntohs(p.to.port)}); + FakeTcpSocket *listen_match = nullptr; + + for (auto it = range.first; it != range.second; ++it) { + FakeTcpSocket *s = it->second; + if (s->state() == FakeTcpSocket::LISTEN) { + listen_match = s; + } else { + const IP_Port &remote = s->remote_addr(); + if (net_ntohs(p.from.port) == net_ntohs(remote.port)) { + if (ip_equal(&p.from.ip, &remote.ip) + || (is_loopback(p.from.ip) && ip_equal(&remote.ip, &target_ip)) + || (is_loopback(remote.ip) + && ip_equal(&p.from.ip, &target_ip))) { + tcp_targets.push_back(s); + } + } + } + } + + if (listen_match && (p.tcp_flags & 0x02)) { + tcp_targets.push_back(listen_match); + } + + if (verbose_) { + std::cerr << "[NetworkUniverse] Routing TCP to " + << static_cast(target_ip.ip.v4.uint8[0]) << "." + << static_cast(target_ip.ip.v4.uint8[3]) << ":" + << net_ntohs(p.to.port) + << ". Targets found: " << tcp_targets.size() << std::endl; + } + if (tcp_targets.empty()) { + if (verbose_) { + std::cerr << "[NetworkUniverse] WARNING: No TCP targets for " + << static_cast(target_ip.ip.v4.uint8[0]) << "." + << static_cast(target_ip.ip.v4.uint8[3]) << ":" + << net_ntohs(p.to.port) << std::endl; } } } else { - if (udp_bindings_.count({p.to.ip, net_ntohs(p.to.port)})) { - udp_target = udp_bindings_[{p.to.ip, net_ntohs(p.to.port)}]; - } else if (is_ipv4_mapped(p.to.ip)) { - IP ip4 = extract_ipv4(p.to.ip); + if (is_loopback(target_ip) + && udp_bindings_.count({target_ip, net_ntohs(p.to.port)}) == 0) { + target_ip = p.from.ip; + } + + if (udp_bindings_.count({target_ip, net_ntohs(p.to.port)})) { + udp_target = udp_bindings_[{target_ip, net_ntohs(p.to.port)}]; + } else if (is_ipv4_mapped(target_ip)) { + IP ip4 = extract_ipv4(target_ip); if (udp_bindings_.count({ip4, net_ntohs(p.to.port)})) { udp_target = udp_bindings_[{ip4, net_ntohs(p.to.port)}]; } @@ -146,7 +273,9 @@ void NetworkUniverse::process_events(uint64_t current_time_ms) if (p.is_tcp) { for (auto *it : tcp_targets) { - it->handle_packet(p); + if (it->handle_packet(p)) { + break; + } } } else { if (udp_target) { @@ -170,7 +299,7 @@ uint16_t NetworkUniverse::find_free_port(IP ip, uint16_t start) { std::lock_guard lock(mutex_); for (uint16_t port = start; port < 65535; ++port) { - if (!udp_bindings_.count({ip, port})) + if (!udp_bindings_.count({ip, port}) && !tcp_bindings_.count({ip, port})) return port; } return 0; diff --git a/testing/support/doubles/network_universe.hh b/testing/support/doubles/network_universe.hh index 9fa1e1913..73c854666 100644 --- a/testing/support/doubles/network_universe.hh +++ b/testing/support/doubles/network_universe.hh @@ -17,11 +17,17 @@ namespace tox::test { class FakeUdpSocket; class FakeTcpSocket; +struct TcpFlags { + uint8_t value; +}; +std::ostream &operator<<(std::ostream &os, TcpFlags flags); + struct Packet { IP_Port from; IP_Port to; std::vector data; uint64_t delivery_time; + uint64_t sequence_number = 0; bool is_tcp = false; // TCP Simulation Fields @@ -29,9 +35,17 @@ struct Packet { uint32_t seq = 0; uint32_t ack = 0; - bool operator>(const Packet &other) const { return delivery_time > other.delivery_time; } + bool operator>(const Packet &other) const + { + if (delivery_time != other.delivery_time) { + return delivery_time > other.delivery_time; + } + return sequence_number > other.sequence_number; + } }; +bool is_loopback(const IP &ip); + /** * @brief The God Object for the network simulation. * Manages routing, latency, and connectivity. @@ -82,6 +96,7 @@ private: std::vector observers_; uint64_t global_latency_ms_ = 0; + uint64_t next_packet_id_ = 0; bool verbose_ = false; std::recursive_mutex mutex_; }; diff --git a/testing/support/doubles/network_universe_test.cc b/testing/support/doubles/network_universe_test.cc index 494f938b6..e29ebca6f 100644 --- a/testing/support/doubles/network_universe_test.cc +++ b/testing/support/doubles/network_universe_test.cc @@ -236,5 +236,86 @@ namespace { std::string(reinterpret_cast(buf), static_cast(len)), "Padding test"); } + TEST_F(NetworkUniverseTest, TcpRoutingSpecificity) + { + IP ip1{}; + ip_init(&ip1, false); + ip1.ip.v4.uint32 = net_htonl(0x0A000001); // 10.0.0.1 + + uint16_t port = 12345; + IP_Port local_addr{ip1, net_htons(port)}; + + FakeTcpSocket listen_sock(universe); + listen_sock.set_ip(ip1); + listen_sock.bind(&local_addr); + listen_sock.listen(5); + + IP remote_ip{}; + ip_init(&remote_ip, false); + remote_ip.ip.v4.uint32 = net_htonl(0x0A000002); // 10.0.0.2 + IP_Port remote_addr{remote_ip, net_htons(33445)}; + + auto established_sock = FakeTcpSocket::create_connected(universe, remote_addr, port); + established_sock->set_ip(ip1); + universe.bind_tcp(ip1, port, established_sock.get()); + + // Send a data packet from remote to local + Packet p{}; + p.from = remote_addr; + p.to = local_addr; + p.is_tcp = true; + p.tcp_flags = 0x10; // ACK (Data) + const char *data = "Specific"; + p.data.assign(data, data + strlen(data)); + + universe.send_packet(p); + universe.process_events(0); + + // established_sock should have received it + EXPECT_EQ(established_sock->recv_buffer_size(), strlen(data)); + + // listen_sock should NOT have received it (it doesn't have a buffer, but it shouldn't have + // been called) We can't easily check listen_sock wasn't called without mocks or checking + // logs, but we can check that it didn't create a new pending connection. + EXPECT_FALSE(listen_sock.is_readable()); + } + + TEST_F(NetworkUniverseTest, PacketOrdering) + { + IP ip1{}, ip2{}; + ip_init(&ip1, false); + ip1.ip.v4.uint32 = net_htonl(0x0A000001); + ip_init(&ip2, false); + ip2.ip.v4.uint32 = net_htonl(0x0A000002); + + uint16_t port = 33445; + IP_Port addr1{ip1, net_htons(port)}; + IP_Port addr2{ip2, net_htons(port)}; + + FakeUdpSocket sock1{universe}; + sock1.set_ip(ip1); + sock1.bind(&addr1); + + FakeUdpSocket sock2{universe}; + sock2.set_ip(ip2); + sock2.bind(&addr2); + + // Send 10 packets with the same delivery time (global latency = 0) + for (int i = 0; i < 10; ++i) { + uint8_t data = static_cast(i); + sock1.sendto(&data, 1, &addr2); + } + + universe.process_events(0); + + // They should be received in the exact order they were sent + for (int i = 0; i < 10; ++i) { + uint8_t buf[1]; + IP_Port from; + ASSERT_EQ(sock2.recvfrom(buf, 1, &from), 1); + EXPECT_EQ(buf[0], i) << "Packet " << i << " was delivered out of order"; + } + } + } // namespace } // namespace tox::test diff --git a/testing/support/public/simulation.hh b/testing/support/public/simulation.hh index 271143015..4d0269b41 100644 --- a/testing/support/public/simulation.hh +++ b/testing/support/public/simulation.hh @@ -16,6 +16,8 @@ #include #endif +#include + #include "../../../toxcore/attributes.h" #include "../../../toxcore/mem.h" #include "../../../toxcore/rng.h" @@ -32,6 +34,53 @@ namespace tox::test { class SimulatedNode; +struct LogMetadata { + Tox_Log_Level level; + const char *_Nonnull file; + uint32_t line; + const char *_Nonnull func; + const char *_Nonnull message; + uint32_t node_id; +}; + +using LogPredicate = std::function; + +struct LogFilter { + LogPredicate pred; + + LogFilter() = default; + explicit LogFilter(LogPredicate p) + : pred(std::move(p)) + { + } + + bool operator()(const LogMetadata &md) const { return !pred || pred(md); } +}; + +LogFilter operator&&(const LogFilter &lhs, const LogFilter &rhs); +LogFilter operator||(const LogFilter &lhs, const LogFilter &rhs); +LogFilter operator!(const LogFilter &target); + +namespace log_filter { + LogFilter level(Tox_Log_Level min_level); + + struct LevelPlaceholder { + LogFilter operator>(Tox_Log_Level rhs) const; + LogFilter operator>=(Tox_Log_Level rhs) const; + LogFilter operator<(Tox_Log_Level rhs) const; + LogFilter operator<=(Tox_Log_Level rhs) const; + LogFilter operator==(Tox_Log_Level rhs) const; + LogFilter operator!=(Tox_Log_Level rhs) const; + }; + + LevelPlaceholder level(); + + LogFilter file(std::string pattern); + LogFilter func(std::string pattern); + LogFilter message(std::string pattern); + LogFilter node(uint32_t id); +} // namespace log_filter + /** * @brief The Simulation World. * Holds the Clock and the Universe. @@ -47,6 +96,10 @@ public: void advance_time(uint64_t ms); void run_until(std::function condition, uint64_t timeout_ms = 5000); + // Logging + void set_log_filter(LogFilter filter); + const LogFilter &log_filter() const { return log_filter_; } + // Synchronization Barrier // These methods coordinate the lock-step execution of multiple Tox runners. @@ -110,6 +163,7 @@ public: private: std::unique_ptr clock_; std::unique_ptr net_; + LogFilter log_filter_; uint32_t node_count_ = 0; // Barrier State diff --git a/testing/support/simulation_test.cc b/testing/support/simulation_test.cc new file mode 100644 index 000000000..c70219bcb --- /dev/null +++ b/testing/support/simulation_test.cc @@ -0,0 +1,76 @@ +#include "public/simulation.hh" + +#include + +namespace tox::test { + +TEST(LogFilterTest, Operators) +{ + LogMetadata md1{TOX_LOG_LEVEL_INFO, "file1.c", 10, "func1", "message1", 1}; + LogMetadata md2{TOX_LOG_LEVEL_DEBUG, "file2.c", 20, "func2", "message2", 2}; + + auto f1 = log_filter::file("file1"); + auto f2 = log_filter::level(TOX_LOG_LEVEL_INFO); + + EXPECT_TRUE(f1(md1)); + EXPECT_FALSE(f1(md2)); + + EXPECT_TRUE(f2(md1)); + EXPECT_FALSE(f2(md2)); + + auto f_and = f1 && f2; + EXPECT_TRUE(f_and(md1)); + EXPECT_FALSE(f_and(md2)); + + auto f_or = f1 || log_filter::file("file2"); + EXPECT_TRUE(f_or(md1)); + EXPECT_TRUE(f_or(md2)); + + auto f_not = !f1; + EXPECT_FALSE(f_not(md1)); + EXPECT_TRUE(f_not(md2)); +} + +TEST(LogFilterTest, LevelComparison) +{ + LogMetadata md_trace{TOX_LOG_LEVEL_TRACE, "file.c", 1, "func", "msg", 1}; + LogMetadata md_debug{TOX_LOG_LEVEL_DEBUG, "file.c", 1, "func", "msg", 1}; + LogMetadata md_info{TOX_LOG_LEVEL_INFO, "file.c", 1, "func", "msg", 1}; + LogMetadata md_warn{TOX_LOG_LEVEL_WARNING, "file.c", 1, "func", "msg", 1}; + LogMetadata md_error{TOX_LOG_LEVEL_ERROR, "file.c", 1, "func", "msg", 1}; + + // level() > DEBUG + auto f_gt = log_filter::level() > TOX_LOG_LEVEL_DEBUG; + EXPECT_FALSE(f_gt(md_trace)); + EXPECT_FALSE(f_gt(md_debug)); + EXPECT_TRUE(f_gt(md_info)); + EXPECT_TRUE(f_gt(md_error)); + + // level() < INFO + auto f_lt = log_filter::level() < TOX_LOG_LEVEL_INFO; + EXPECT_TRUE(f_lt(md_trace)); + EXPECT_TRUE(f_lt(md_debug)); + EXPECT_FALSE(f_lt(md_info)); + EXPECT_FALSE(f_lt(md_error)); + + // level() == WARNING + auto f_eq = log_filter::level() == TOX_LOG_LEVEL_WARNING; + EXPECT_FALSE(f_eq(md_info)); + EXPECT_TRUE(f_eq(md_warn)); + EXPECT_FALSE(f_eq(md_error)); +} + +TEST(LogFilterTest, SimulationIntegration) +{ + Simulation sim; + sim.net().set_verbose(true); + + sim.set_log_filter(log_filter::level(TOX_LOG_LEVEL_ERROR) && log_filter::node(1)); + + auto node = sim.create_node(); + auto tox = node->create_tox(); + + SUCCEED(); +} + +} // namespace tox::test diff --git a/testing/support/src/simulation.cc b/testing/support/src/simulation.cc index 8a7568a1a..1d1fdea2f 100644 --- a/testing/support/src/simulation.cc +++ b/testing/support/src/simulation.cc @@ -7,6 +7,90 @@ namespace tox::test { +// --- LogFilter --- + +LogFilter operator&&(const LogFilter &lhs, const LogFilter &rhs) +{ + return LogFilter([=](const LogMetadata &md) { return lhs(md) && rhs(md); }); +} + +LogFilter operator||(const LogFilter &lhs, const LogFilter &rhs) +{ + return LogFilter([=](const LogMetadata &md) { return lhs(md) || rhs(md); }); +} + +LogFilter operator!(const LogFilter &target) +{ + return LogFilter([=](const LogMetadata &md) { return !target(md); }); +} + +namespace log_filter { + + LogFilter level(Tox_Log_Level min_level) + { + return LogFilter([=](const LogMetadata &md) { return md.level >= min_level; }); + } + + LevelPlaceholder level() { return {}; } + + LogFilter LevelPlaceholder::operator>(Tox_Log_Level rhs) const + { + return LogFilter([=](const LogMetadata &md) { return md.level > rhs; }); + } + + LogFilter LevelPlaceholder::operator>=(Tox_Log_Level rhs) const + { + return LogFilter([=](const LogMetadata &md) { return md.level >= rhs; }); + } + + LogFilter LevelPlaceholder::operator<(Tox_Log_Level rhs) const + { + return LogFilter([=](const LogMetadata &md) { return md.level < rhs; }); + } + + LogFilter LevelPlaceholder::operator<=(Tox_Log_Level rhs) const + { + return LogFilter([=](const LogMetadata &md) { return md.level <= rhs; }); + } + + LogFilter LevelPlaceholder::operator==(Tox_Log_Level rhs) const + { + return LogFilter([=](const LogMetadata &md) { return md.level == rhs; }); + } + + LogFilter LevelPlaceholder::operator!=(Tox_Log_Level rhs) const + { + return LogFilter([=](const LogMetadata &md) { return md.level != rhs; }); + } + + LogFilter file(std::string pattern) + { + return LogFilter([p = std::move(pattern)](const LogMetadata &md) { + return std::string(md.file).find(p) != std::string::npos; + }); + } + + LogFilter func(std::string pattern) + { + return LogFilter([p = std::move(pattern)](const LogMetadata &md) { + return std::string(md.func).find(p) != std::string::npos; + }); + } + + LogFilter message(std::string pattern) + { + return LogFilter([p = std::move(pattern)](const LogMetadata &md) { + return std::string(md.message).find(p) != std::string::npos; + }); + } + + LogFilter node(uint32_t id) + { + return LogFilter([=](const LogMetadata &md) { return md.node_id == id; }); + } + +} // namespace log_filter + // --- Simulation --- Simulation::Simulation() @@ -73,6 +157,8 @@ void Simulation::run_until(std::function condition, uint64_t timeout_ms) } } +void Simulation::set_log_filter(LogFilter filter) { log_filter_ = std::move(filter); } + Simulation::TickListenerId Simulation::register_tick_listener( std::function listener) { @@ -181,18 +267,41 @@ MemorySystem &SimulatedNode::memory() { return *memory_; } SimulatedNode::ToxPtr SimulatedNode::create_tox(const Tox_Options *_Nullable options) { - std::unique_ptr default_options( - nullptr, tox_options_free); + std::unique_ptr local_opts( + tox_options_new(nullptr), tox_options_free); + assert(local_opts != nullptr); - if (options == nullptr) { - default_options.reset(tox_options_new(nullptr)); - assert(default_options != nullptr); - tox_options_set_ipv6_enabled(default_options.get(), false); - tox_options_set_start_port(default_options.get(), 33445); - tox_options_set_end_port(default_options.get(), 55555); - options = default_options.get(); + if (options != nullptr) { + tox_options_copy(local_opts.get(), options); + } else { + tox_options_set_ipv6_enabled(local_opts.get(), false); + tox_options_set_start_port(local_opts.get(), 33445); + tox_options_set_end_port(local_opts.get(), 55555); } + tox_options_set_log_callback(local_opts.get(), + [](Tox *tox, Tox_Log_Level level, const char *file, uint32_t line, const char *func, + const char *message, void *user_data) { + SimulatedNode *node = static_cast(user_data); + uint32_t ip4 = net_ntohl(node->ip.ip.v4.uint32); + + LogMetadata md{level, file, line, func, message, ip4 & 0xFF}; + const auto &filter = node->simulation().log_filter(); + + bool allow = false; + if (filter.pred) { + allow = filter(md); + } else { + allow = node->simulation().net().is_verbose() && level >= TOX_LOG_LEVEL_TRACE; + } + + if (allow) { + std::cerr << "[Tox Log] [Node " << (ip4 & 0xFF) << "] " << file << ":" << line + << " (" << func << "): " << message << std::endl; + } + }); + tox_options_set_log_user_data(local_opts.get(), this); + Tox_Options_Testing opts_testing; Tox_System system; system.ns = &c_network; @@ -208,7 +317,7 @@ SimulatedNode::ToxPtr SimulatedNode::create_tox(const Tox_Options *_Nullable opt Tox_Err_New err; Tox_Err_New_Testing err_testing; - Tox *t = tox_new_testing(options, &err, &opts_testing, &err_testing); + Tox *t = tox_new_testing(local_opts.get(), &err, &opts_testing, &err_testing); if (!t) { std::cerr << "tox_new_testing failed: " << err << " (testing err: " << err_testing << ")" diff --git a/testing/support/tox_network_test.cc b/testing/support/tox_network_test.cc index 04c15e34d..e1ffdc32f 100644 --- a/testing/support/tox_network_test.cc +++ b/testing/support/tox_network_test.cc @@ -7,8 +7,11 @@ #include #include +#include +#include #include "../../toxcore/attributes.h" +#include "../../toxcore/network.h" namespace tox::test { namespace { @@ -221,5 +224,180 @@ namespace { EXPECT_EQ(ctx.count, num_friends); } + TEST(ToxNetworkTest, TcpRelayChaining) + { + constexpr bool kDebug = false; + + Simulation sim; + sim.net().set_verbose(false); + + if (kDebug) { + using namespace log_filter; + sim.set_log_filter(level(TOX_LOG_LEVEL_DEBUG) + || (level(TOX_LOG_LEVEL_TRACE) + && (file("TCP") || file("onion.c") || func("dht_isconnected")) + && !message("not sending repeated announce request"))); + } + + struct ToxOptionsDeleter { + void operator()(Tox_Options *opts) { tox_options_free(opts); } + }; + std::unique_ptr opts(tox_options_new(nullptr)); + tox_options_set_udp_enabled(opts.get(), false); + tox_options_set_ipv6_enabled(opts.get(), false); + tox_options_set_local_discovery_enabled(opts.get(), false); + + auto create = [&](const char *name, uint16_t port, bool udp_enabled = false) { + tox_options_set_tcp_port(opts.get(), port); + if (udp_enabled) { + tox_options_set_start_port(opts.get(), port); + tox_options_set_end_port(opts.get(), port); + } + tox_options_set_udp_enabled(opts.get(), udp_enabled); + auto node = sim.create_node(); + auto tox = node->create_tox(opts.get()); + if (!tox) { + std::cerr << "Failed to create node " << name << " on port " << port << std::endl; + std::abort(); + } + return std::make_pair(std::move(node), std::move(tox)); + }; + + // Servers (Enable UDP for relays so they can talk to each other) + auto [nodeA, toxA] = create("A", 20001, true); + auto [nodeB, toxB] = create("B", 20002, true); + + // Clients + auto [nodeC, toxC] = create("C", 0); + auto [nodeD, toxD] = create("D", 0); + auto [nodeE, toxE] = create("E", 0); + auto [nodeF, toxF] = create("F", 0); + + auto get_info = [](SimulatedNode &node, Tox *tox) { + uint8_t pk[TOX_PUBLIC_KEY_SIZE]; + tox_self_get_public_key(tox, pk); + uint8_t dht_id[TOX_PUBLIC_KEY_SIZE]; + tox_self_get_dht_id(tox, dht_id); + + char ip[TOX_INET_ADDRSTRLEN]; + ip_parse_addr(&node.ip, ip, sizeof(ip)); + uint16_t port = tox_self_get_tcp_port(tox, nullptr); + if (kDebug) { + std::cout << "Node Info: IP=" << ip << " Port=" << port << std::endl; + + auto to_hex = [](const uint8_t *data) { + std::stringstream ss; + ss << std::hex << std::setfill('0'); + for (int i = 0; i < TOX_PUBLIC_KEY_SIZE; ++i) + ss << std::setw(2) << static_cast(data[i]); + return ss.str(); + }; + + std::cout << "PK: " << to_hex(pk) << std::endl; + std::cout << "DHT ID: " << to_hex(dht_id) << std::endl; + } + + return std::make_tuple(std::vector(pk, pk + TOX_PUBLIC_KEY_SIZE), + std::vector(dht_id, dht_id + TOX_PUBLIC_KEY_SIZE), std::string(ip), port); + }; + + auto [pkA, dhtIdA, ipA, portA] = get_info(*nodeA, toxA.get()); + auto [pkB, dhtIdB, ipB, portB] = get_info(*nodeB, toxB.get()); + + // Helper to connect to a relay (bootstrap + add_tcp_relay) + auto connect_to_relay + = [](Tox *tox, const std::string &ip, uint16_t port, const std::vector &pk, + const std::vector &dht_id) { + Tox_Err_Bootstrap err_bs; + tox_bootstrap(tox, ip.c_str(), port, dht_id.data(), &err_bs); + if (err_bs != TOX_ERR_BOOTSTRAP_OK) { + std::cout << "tox_bootstrap failed with " << err_bs << " for " << ip << ":" + << port << std::endl; + } + Tox_Err_Bootstrap err_relay; + // Use dht_id for TCP relay as well, as server uses DHT key + tox_add_tcp_relay(tox, ip.c_str(), port, dht_id.data(), &err_relay); + if (err_relay != TOX_ERR_BOOTSTRAP_OK) { + std::cout << "tox_add_tcp_relay failed with " << err_relay << " for " << ip + << ":" << port << std::endl; + } + }; + + // Connect {C,D} -> A, {E,F} -> B + connect_to_relay(toxC.get(), ipA, portA, pkA, dhtIdA); + connect_to_relay(toxD.get(), ipA, portA, pkA, dhtIdA); + connect_to_relay(toxE.get(), ipB, portB, pkB, dhtIdB); + connect_to_relay(toxF.get(), ipB, portB, pkB, dhtIdB); + + // B -> A (Connect the two TCP relays, but only one initial link) + connect_to_relay(toxB.get(), ipA, portA, pkA, dhtIdA); + + // Connect C and F + uint8_t pkF[TOX_PUBLIC_KEY_SIZE]; + tox_self_get_public_key(toxF.get(), pkF); + uint8_t pkC[TOX_PUBLIC_KEY_SIZE]; + tox_self_get_public_key(toxC.get(), pkC); + + Tox_Err_Friend_Add err; + const uint32_t fC = tox_friend_add_norequest(toxC.get(), pkF, &err); + ASSERT_EQ(err, TOX_ERR_FRIEND_ADD_OK); + const uint32_t fF = tox_friend_add_norequest(toxF.get(), pkC, &err); + ASSERT_EQ(err, TOX_ERR_FRIEND_ADD_OK); + + struct Context { + bool received = false; + } ctx; + + tox_callback_friend_message(toxF.get(), + [](Tox *, uint32_t, Tox_Message_Type, const uint8_t *, size_t, void *user_data) { + static_cast(user_data)->received = true; + }); + + bool sent = false; + sim.run_until( + [&]() { + tox_iterate(toxA.get(), nullptr); + tox_iterate(toxB.get(), nullptr); + tox_iterate(toxC.get(), nullptr); + tox_iterate(toxD.get(), nullptr); + tox_iterate(toxE.get(), nullptr); + tox_iterate(toxF.get(), &ctx); + + Tox_Connection statusC = tox_friend_get_connection_status(toxC.get(), fC, nullptr); + Tox_Connection statusF = tox_friend_get_connection_status(toxF.get(), fF, nullptr); + + if (kDebug) { + static int loop_counter = 0; + if (loop_counter++ % 100 == 0) { + std::cout << "Conn Status: " + << "A=" << tox_self_get_connection_status(toxA.get()) << " " + << "B=" << tox_self_get_connection_status(toxB.get()) << " " + << "C=" << tox_self_get_connection_status(toxC.get()) << " " + << "D=" << tox_self_get_connection_status(toxC.get()) << " " + << "E=" << tox_self_get_connection_status(toxC.get()) << " " + << "F=" << tox_self_get_connection_status(toxF.get()) << " " + << " Friend Status C->F: " << statusC << ", F->C: " << statusF + << std::endl; + } + } + + if (!sent && statusC != TOX_CONNECTION_NONE) { + const uint8_t msg[] = "hello"; + Tox_Err_Friend_Send_Message send_err; + tox_friend_send_message( + toxC.get(), fC, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), &send_err); + if (kDebug) { + std::cout << "Message sent from C to F, err=" << send_err << std::endl; + } + sent = true; + } + + return ctx.received; + }, + 120000); + + EXPECT_TRUE(ctx.received); + } + } // namespace } // namespace tox::test diff --git a/toxav/rtp_test.cc b/toxav/rtp_test.cc index be0b8a3e5..b97f46b2f 100644 --- a/toxav/rtp_test.cc +++ b/toxav/rtp_test.cc @@ -408,7 +408,13 @@ TEST_F(RtpPublicTest, MoreInvalidPackets) // Get a valid packet to start with std::uint8_t data[] = "test"; rtp_send_data(log, session, data, sizeof(data), false); - std::vector valid_pkt = sd.sent_packets[0]; + ASSERT_FALSE(sd.sent_packets.empty()); + if (sd.sent_packets.empty()) + return; + const std::vector &src_pkt = sd.sent_packets[0]; + if (src_pkt.size() > 65536) + return; + std::vector valid_pkt(src_pkt.begin(), src_pkt.end()); sd.sent_packets.clear(); // 1. RTPHeader packet type and Tox protocol packet type do not agree @@ -444,7 +450,13 @@ TEST_F(RtpPublicTest, MoreInvalidPackets) nullptr, nullptr, nullptr, &sd, mock_m_cb); rtp_send_data(log, session_audio2, data, sizeof(data), false); - std::vector audio_pkt = sd.sent_packets[0]; + ASSERT_FALSE(sd.sent_packets.empty()); + if (sd.sent_packets.empty()) + return; + const std::vector &src_audio = sd.sent_packets[0]; + if (src_audio.size() > 65536) + return; + std::vector audio_pkt(src_audio.begin(), src_audio.end()); sd.sent_packets.clear(); std::vector bad_pkt_4 = audio_pkt; diff --git a/toxcore/BUILD.bazel b/toxcore/BUILD.bazel index feb3d034f..cde2f8846 100644 --- a/toxcore/BUILD.bazel +++ b/toxcore/BUILD.bazel @@ -804,10 +804,6 @@ cc_library( name = "TCP_server", srcs = ["TCP_server.c"], hdrs = ["TCP_server.h"], - copts = select({ - "//tools/config:linux": ["-DTCP_SERVER_USE_EPOLL=1"], - "//conditions:default": [], - }), visibility = [ "//c-toxcore/auto_tests:__pkg__", "//c-toxcore/other:__pkg__", diff --git a/toxcore/crypto_core_test_util.cc b/toxcore/crypto_core_test_util.cc index f41220132..b352fa7fe 100644 --- a/toxcore/crypto_core_test_util.cc +++ b/toxcore/crypto_core_test_util.cc @@ -14,6 +14,13 @@ PublicKey random_pk(const Random *_Nonnull rng) return pk; } +std::array random_sk(const Random *rng) +{ + std::array sk; + random_bytes(rng, sk.data(), sk.size()); + return sk; +} + std::ostream &operator<<(std::ostream &out, PublicKey const &pk) { out << '"'; diff --git a/toxcore/crypto_core_test_util.hh b/toxcore/crypto_core_test_util.hh index 0fda27c70..816d8cb45 100644 --- a/toxcore/crypto_core_test_util.hh +++ b/toxcore/crypto_core_test_util.hh @@ -56,4 +56,6 @@ std::ostream &operator<<(std::ostream &out, PublicKey const &pk); PublicKey random_pk(const Random *_Nonnull rng); +std::array random_sk(const Random *_Nonnull rng); + #endif // C_TOXCORE_TOXCORE_CRYPTO_CORE_TEST_UTIL_H diff --git a/toxcore/net_crypto_test.cc b/toxcore/net_crypto_test.cc index 6aa87b018..f69c28dac 100644 --- a/toxcore/net_crypto_test.cc +++ b/toxcore/net_crypto_test.cc @@ -519,7 +519,7 @@ TEST_F(NetCryptoTest, HandleRequestPacketOOB) ASSERT_EQ(len, plaintext_len + CRYPTO_MAC_SIZE); // 4. Inject the packet - tox::test::Packet p; + tox::test::Packet p{}; p.to = alice.get_ip_port(); p.data = malicious_packet; p.from = bob.get_ip_port(); diff --git a/toxcore/onion_client_test.cc b/toxcore/onion_client_test.cc index 026933cd4..7c27777b0 100644 --- a/toxcore/onion_client_test.cc +++ b/toxcore/onion_client_test.cc @@ -645,7 +645,7 @@ TEST_F(OnionClientTest, SharedKeyCacheUseAfterFreeRegression) const Memory *mem = &mem_struct; int total_decryption_failures = 0; - std::set> seen_announcements; + std::set> seen_announcements; auto observer = [&](OnionNode *node, const std::vector &data, const IP_Port &from) { @@ -674,8 +674,9 @@ TEST_F(OnionClientTest, SharedKeyCacheUseAfterFreeRegression) // The real PK (client_id) is in the payload at offset ONION_PING_ID_SIZE // (32). const std::uint8_t *real_pk = plain.data() + 32; - seen_announcements.insert( - std::vector(real_pk, real_pk + CRYPTO_PUBLIC_KEY_SIZE)); + std::array real_pk_arr; + std::memcpy(real_pk_arr.data(), real_pk, CRYPTO_PUBLIC_KEY_SIZE); + seen_announcements.insert(real_pk_arr); } else { total_decryption_failures++; #if 0 @@ -714,13 +715,15 @@ TEST_F(OnionClientTest, SharedKeyCacheUseAfterFreeRegression) ASSERT_NE(onion_connection_status(alice.get_onion_client()), ONION_CONNECTION_STATUS_NONE); const int kNumFriends = 2; - std::vector> friend_pks; + std::vector> friend_pks; for (int i = 0; i < kNumFriends; ++i) { std::uint8_t pk[CRYPTO_PUBLIC_KEY_SIZE], sk[CRYPTO_SECRET_KEY_SIZE]; crypto_new_keypair(alice.get_random(), pk, sk); onion_addfriend(alice.get_onion_client(), pk); - friend_pks.push_back(std::vector(pk, pk + CRYPTO_PUBLIC_KEY_SIZE)); + std::array pk_arr; + std::memcpy(pk_arr.data(), pk, CRYPTO_PUBLIC_KEY_SIZE); + friend_pks.push_back(pk_arr); } // Wait for announcements. diff --git a/toxcore/sort_test.cc b/toxcore/sort_test.cc index 9da884ecc..384f905be 100644 --- a/toxcore/sort_test.cc +++ b/toxcore/sort_test.cc @@ -34,7 +34,7 @@ TEST(MergeSort, BehavesLikeStdSort) // If vec was accidentally sorted, add another larger element that almost definitely makes // it not sorted. if (vec == sorted) { - int const largest = *std::prev(sorted.end()) + 1; + int const largest = sorted.empty() ? 0 : sorted.back() + 1; sorted.push_back(largest); vec.insert(vec.begin(), largest); } @@ -67,7 +67,11 @@ TEST(MergeSort, WorksWithNonTrivialTypes) if (vec == sorted) { std::string const largest = "larger than largest int"; sorted.push_back(largest); - vec.insert(vec.begin(), largest); + vec.push_back(largest); + // Swap the last two elements. Since i >= 1, vec has at least 2 elements now. + // This guarantees the vector is not sorted because 'largest' is now before the last + // element (which is smaller than 'largest'). + std::iter_swap(vec.end() - 2, vec.end() - 1); } ASSERT_NE(vec, sorted);