Files
shadPS4/tests/network/test_net_p2p_tcp.cpp
T
georgemoralisandMetr1k 2a2f1003b0 Major net rewrite (#5236)
* p2p crypto schema

* adding p2p keyring

* p2p framing and testing

* fixups

* test fix

* missing definations

* implementation of host functions

* tcp/transport of p2p packets

* improvements in binding

* guest_net ,improvements and  test cases

* all mighty clang

* fixing macOS tests failures

* misc

* net_rewrite : net_errors , types and translation schemas

* updated fixed chunks transfer on windows

* bye bye old net

* small updates for the new net

* settings and tests

* net lib first draft

* allow more than 1 P2P instances on from same pc

* improved shadnet replies

* possible fix for jojo

* cleanup

* move net files to libaries/net

* fixed macOS tests

* mighty clang

* fixed cmakelists

* don't cosume inviation event on client (test code)

* fix macOS tests

* Large multi-file rework for Signaling and Matching2 network handling / optimization.  Consolidates Matching2-signaling and signaling to one process (signaling_handler)

General cleanup and consolidation of matching2 context management > nphandler

Removal of a lot

SIgnaling overall structure cleanup.

Matching2: added get and set roommemberdatainternal and the proper type / shadnet call / request / reply.

Consolidated signaling/matching2 typedefs to np_types2

Utilized Codex as a reviewer and for general cleanup.

* all mighty clang

* fixed member attributes

* fixed MAPPED address for matching signaling

* Fixed sceNpMatching2SignalingGetLocalNetInfo

* fixups on net core/library based on fw

* improved errno

* spilt p2p to net_p2p

* some cleanups

* fix for localinfo

* clang

* fix up

* cleanups

* cleanups

* added KernelFstat

* fixed fstat

* one more fix for fstat...

* cleanup old deprecated code

* cleanup

* General logging cleanup across NP changes.  Thanks Stephen!

* cleanup based on stephen's suggestions

* fixup

* fixing tests

* blah

* fixup

* improve memory pools

* cleaunp

* force https disable option

* clang

* @stephen's fixes

* added network field in settings and ported old network ones

---------

Co-authored-by: Metr1k <blackkn1ght1991@gmail.com>
2026-10-06 19:14:56 +03:00

396 lines
14 KiB
C++

// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// The STREAM_P2P TCP state machine: wire format, and two connections over a simulated link with
// loss, duplication, reordering and jitter on a virtual clock (so the tests are fast and
// deterministic).
#include <cstdlib>
#include <deque>
#include <random>
#include <string>
#include <queue>
#include <gtest/gtest.h>
#include "core/net/p2p_tcp.h"
using namespace Core::Net;
using namespace Core::Net::P2P;
using namespace std::chrono_literals;
namespace {
PseudoHeader Pseudo(u8 local, u8 remote) {
PseudoHeader p;
p.family = AF_INET;
p.local = {10, 0, 0, local};
p.remote = {10, 0, 0, remote};
return p;
}
std::vector<u8> RandomBytes(size_t n, u64 seed) {
std::mt19937_64 rng(seed);
std::vector<u8> v(n);
for (auto& b : v) {
b = static_cast<u8>(rng());
}
return v;
}
// ---------------------------------------------------------------------------------------------
// Wire format
// ---------------------------------------------------------------------------------------------
TEST(NetP2PTcpWire, SequenceArithmeticWraps) {
EXPECT_TRUE(SeqLt(0xFFFFFFF0u, 0x10u));
EXPECT_TRUE(SeqGt(0x10u, 0xFFFFFFF0u));
EXPECT_TRUE(SeqLe(5u, 5u));
EXPECT_FALSE(SeqLt(5u, 5u));
}
TEST(NetP2PTcpWire, BuildParseRoundTripWithChecksum) {
const u8 payload[] = {1, 2, 3, 4, 5};
const auto seg = BuildTcpSegment(Pseudo(1, 2), 1234, 80, 1000, 2000,
TcpFlag::Ack | TcpFlag::Psh, 4096, u16{1200}, payload);
const auto receiver = Pseudo(2, 1);
const auto parsed = ParseTcpSegment(seg, &receiver);
ASSERT_TRUE(parsed);
EXPECT_EQ(parsed->src_port, 1234);
EXPECT_EQ(parsed->dst_port, 80);
EXPECT_EQ(parsed->seq, 1000u);
EXPECT_EQ(parsed->ack, 2000u);
EXPECT_EQ(parsed->flags, TcpFlag::Ack | TcpFlag::Psh);
EXPECT_EQ(parsed->window, 4096);
EXPECT_EQ(parsed->mss, 1200);
EXPECT_EQ(std::vector<u8>(parsed->payload.begin(), parsed->payload.end()),
std::vector<u8>(std::begin(payload), std::end(payload)));
// A flipped bit, or the wrong pseudo-header, fails verification.
auto corrupt = seg;
corrupt.back() ^= 1;
EXPECT_FALSE(ParseTcpSegment(corrupt, &receiver));
const auto wrong = Pseudo(2, 9);
EXPECT_FALSE(ParseTcpSegment(seg, &wrong));
EXPECT_TRUE(ParseTcpSegment(corrupt)); // no pseudo-header: no verification
}
TEST(NetP2PTcpWire, KnownChecksum) {
// SYN 10.0.0.1:1234 -> 10.0.0.2:80, seq 1, window 1024, no options; checksum computed with
// an independent implementation (RFC 1071 one's complement sum, Python).
const auto seg =
BuildTcpSegment(Pseudo(1, 2), 1234, 80, 1, 0, TcpFlag::Syn, 1024, std::nullopt, {});
EXPECT_EQ((seg[16] << 8) | seg[17], 0x92BD);
}
TEST(NetP2PTcpWire, SkipsUnknownOptions) {
// SYN carrying MSS, SACK-permitted, timestamps, NOP and window scale, as Linux sends.
std::vector<u8> seg(40, 0);
seg[12] = 10 << 4;
seg[13] = TcpFlag::Syn;
const u8 options[] = {2, 4, 0x05, 0xB4, 4, 2, 8, 10, 0, 0, 0, 1, 0, 0, 0, 0, 1, 3, 3, 7};
std::copy(std::begin(options), std::end(options), seg.begin() + 20);
const auto parsed = ParseTcpSegment(seg);
ASSERT_TRUE(parsed);
EXPECT_EQ(parsed->mss, 1460);
EXPECT_TRUE(parsed->payload.empty());
}
TEST(NetP2PTcpWire, RejectsMalformed) {
std::vector<u8> seg(20, 0);
seg[12] = 4 << 4; // data offset below 20 bytes
EXPECT_FALSE(ParseTcpSegment(seg));
seg[12] = 15 << 4; // data offset beyond the segment
EXPECT_FALSE(ParseTcpSegment(seg));
EXPECT_FALSE(ParseTcpSegment(std::vector<u8>(19, 0)));
}
TEST(NetP2PTcpWire, ResetForUnknownPort) {
const auto p = Pseudo(1, 2);
const u8 data[10]{};
// With ACK: RST takes its sequence number from the ACK.
TcpSegment with_ack{5000, 80, 100, 777, TcpFlag::Ack, 100, std::nullopt, data};
auto rst = ParseTcpSegment(*BuildResetFor(p, with_ack));
EXPECT_EQ(rst->flags, TcpFlag::Rst);
EXPECT_EQ(rst->seq, 777u);
EXPECT_EQ(rst->src_port, 80);
EXPECT_EQ(rst->dst_port, 5000);
// Without ACK (a SYN): RST|ACK acknowledging what the segment occupied.
TcpSegment syn{5000, 80, 100, 0, TcpFlag::Syn, 100, std::nullopt, {}};
rst = ParseTcpSegment(*BuildResetFor(p, syn));
EXPECT_EQ(rst->flags, TcpFlag::Rst | TcpFlag::Ack);
EXPECT_EQ(rst->ack, 101u);
// Never answer a RST with a RST.
TcpSegment reset{5000, 80, 100, 0, TcpFlag::Rst, 0, std::nullopt, {}};
EXPECT_FALSE(BuildResetFor(p, reset));
}
// ---------------------------------------------------------------------------------------------
// Simulated link
// ---------------------------------------------------------------------------------------------
struct LinkParams {
double loss = 0.0;
double duplicate = 0.0;
std::chrono::milliseconds delay{20};
std::chrono::milliseconds jitter{0}; // jitter larger than packet spacing reorders
};
class Sim {
public:
Sim(u64 seed, LinkParams link) : rng_(seed), link_(link) {}
Clock::time_point now{};
void Transmit(int to, std::span<const u8> bytes) {
if (std::getenv("SHADPS4_NET_SIMLOG")) { // keep a trace, printed on failure
const auto seg = ParseTcpSegment(bytes);
char line[160];
std::snprintf(line, sizeof(line), "%8.3f %s flags=%02x seq=%u ack=%u len=%zu win=%u",
std::chrono::duration<double>(now.time_since_epoch()).count(),
to ? "A->B" : "B->A", seg->flags, seg->seq, seg->ack, seg->payload.size(),
seg->window);
log_.push_back(line);
if (log_.size() > 60) {
log_.pop_front();
}
}
std::uniform_real_distribution<double> u(0, 1);
const int copies = u(rng_) < link_.loss ? 0 : (u(rng_) < link_.duplicate ? 2 : 1);
for (int i = 0; i < copies; ++i) {
const auto jitter = link_.jitter.count()
? std::chrono::milliseconds(rng_() % link_.jitter.count())
: 0ms;
wire_.push({now + link_.delay + jitter, to, {bytes.begin(), bytes.end()}, order_++});
}
}
std::optional<Clock::time_point> NextDelivery() const {
return wire_.empty() ? std::nullopt : std::optional{wire_.top().deliver_at};
}
/// Pops the next packet due by `now`.
std::optional<std::pair<int, std::vector<u8>>> Pop() {
if (wire_.empty() || wire_.top().deliver_at > now) {
return std::nullopt;
}
auto p = wire_.top();
wire_.pop();
return std::pair{p.to, std::move(p.bytes)};
}
std::string Log() const {
std::string out;
for (const auto& line : log_) {
out += " " + line + "\n";
}
return out;
}
private:
struct Packet {
Clock::time_point deliver_at;
int to; // 0 = A, 1 = B
std::vector<u8> bytes;
u64 order;
bool operator>(const Packet& o) const {
return deliver_at != o.deliver_at ? deliver_at > o.deliver_at : order > o.order;
}
};
std::mt19937_64 rng_;
LinkParams link_;
std::priority_queue<Packet, std::vector<Packet>, std::greater<>> wire_;
u64 order_ = 0;
std::deque<std::string> log_;
};
struct TransferResult {
std::vector<u8> a_sent, b_sent, a_got, b_got;
TcpState a_state, b_state;
Error a_error, b_error;
bool b_created;
};
TransferResult RunTransfer(u64 seed, LinkParams link, size_t bytes, Clock::duration reader_stall,
std::string* log) {
Sim sim(seed, link);
TcpConfig config;
const auto a_pseudo = Pseudo(1, 2);
const auto b_pseudo = Pseudo(2, 1);
std::optional<TcpConnection> b;
// A's initial sequence number sits just below 2^32 so the transfer crosses the wrap.
TcpConnection a(config, a_pseudo, 5000, 6000, 0xfffff000u,
[&](std::span<const u8> s) { sim.Transmit(1, s); });
a.Connect(sim.now);
TransferResult r;
r.a_sent = RandomBytes(bytes, seed * 2 + 1);
r.b_sent = RandomBytes(bytes, seed * 2 + 2);
size_t a_off = 0, b_off = 0;
bool a_shut = false, b_shut = false;
const auto start = sim.now;
auto pump = [&](TcpConnection& c, const std::vector<u8>& out, size_t& off, std::vector<u8>& in,
bool& shut, bool may_read) {
while (off < out.size()) {
const auto w = c.Send(std::span(out).subspan(off), sim.now);
if (w.bytes == 0) {
break;
}
off += w.bytes;
}
if (off == out.size() && !shut &&
(c.State() == TcpState::Established || c.State() == TcpState::CloseWait)) {
c.Shutdown(sim.now);
shut = true;
}
if (may_read) {
u8 buf[4096];
for (;;) {
const auto got = c.Recv(buf, false, sim.now);
if (got.bytes == 0) {
break;
}
in.insert(in.end(), buf, buf + got.bytes);
}
}
};
const auto done = [](const TcpConnection& c) {
return c.State() == TcpState::Closed || c.State() == TcpState::TimeWait;
};
while (sim.now < start + 600s) {
pump(a, r.a_sent, a_off, r.a_got, a_shut, true);
if (b) {
pump(*b, r.b_sent, b_off, r.b_got, b_shut, sim.now - start >= reader_stall);
}
if (done(a) && b && done(*b) && r.a_got.size() == bytes && r.b_got.size() == bytes) {
break;
}
// Advance the virtual clock to the next packet or timer.
std::optional<Clock::time_point> next;
const auto consider = [&](std::optional<Clock::time_point> t) {
if (t && (!next || *t < *next)) {
next = t;
}
};
consider(sim.NextDelivery());
consider(a.NextDeadline());
if (b) {
consider(b->NextDeadline());
if (sim.now - start < reader_stall) {
consider(start + reader_stall);
}
}
if (!next) {
break;
}
sim.now = std::max(sim.now, *next);
while (auto packet = sim.Pop()) {
const auto [to, bytes_in] = std::move(*packet);
const auto& pseudo = to == 0 ? a_pseudo : b_pseudo;
const auto seg = ParseTcpSegment(bytes_in, &pseudo);
if (!seg) {
ADD_FAILURE() << "segment failed checksum verification";
continue;
}
TcpConnection* target = to == 0 ? &a : (b ? &*b : nullptr);
if (target && target->State() == TcpState::Closed) {
if (auto rst = BuildResetFor(pseudo, *seg)) {
sim.Transmit(to ^ 1, *rst);
}
continue;
}
if (target) {
target->OnSegment(*seg, sim.now);
} else if ((seg->flags & (TcpFlag::Syn | TcpFlag::Ack)) == TcpFlag::Syn) {
b.emplace(config, b_pseudo, 6000, 5000, 0x12345678u,
[&](std::span<const u8> s) { sim.Transmit(0, s); });
b->AcceptSyn(*seg, sim.now);
}
}
a.OnTimer(sim.now);
if (b) {
b->OnTimer(sim.now);
}
}
r.a_state = a.State();
r.a_error = a.PendingError();
r.b_created = b.has_value();
r.b_state = b ? b->State() : TcpState::Closed;
r.b_error = b ? b->PendingError() : Error::Ok;
if (log) {
*log = sim.Log();
}
return r;
}
struct Scenario {
const char* name;
LinkParams link;
Clock::duration reader_stall;
};
class NetP2PTcpSim : public ::testing::TestWithParam<Scenario> {};
TEST_P(NetP2PTcpSim, BidirectionalTransferAndClose) {
constexpr size_t kBytes = 300 * 1024;
for (u64 seed = 1; seed <= 20; ++seed) {
std::string log;
const auto r = RunTransfer(seed, GetParam().link, kBytes, GetParam().reader_stall, &log);
ASSERT_TRUE(r.b_created) << "seed " << seed;
ASSERT_TRUE(r.a_got == r.b_sent && r.b_got == r.a_sent)
<< "seed " << seed << ": a got " << r.a_got.size() << ", b got " << r.b_got.size()
<< " of " << kBytes << "; a " << ToString(r.a_state) << " b " << ToString(r.b_state)
<< "\n"
<< log << "(set SHADPS4_NET_SIMLOG=1 for a packet trace)";
EXPECT_EQ(r.a_error, Error::Ok) << "seed " << seed;
EXPECT_EQ(r.b_error, Error::Ok) << "seed " << seed;
}
}
INSTANTIATE_TEST_SUITE_P(
Links, NetP2PTcpSim,
::testing::Values(
Scenario{"Clean", {}, 0s}, Scenario{"Loss5", {.loss = 0.05}, 0s},
Scenario{"Loss20", {.loss = 0.20}, 0s},
Scenario{"ReorderDuplicate", {.duplicate = 0.05, .jitter = 30ms}, 0s},
Scenario{"Loss10ReorderDuplicate", {.loss = 0.10, .duplicate = 0.05, .jitter = 30ms}, 0s},
Scenario{"ZeroWindow", {.loss = 0.02}, 3s}),
[](const auto& info) { return std::string(info.param.name); });
TEST(NetP2PTcp, ConnectTimesOutOnDeadLink) {
Clock::time_point now{};
TcpConfig config;
int syns = 0;
TcpConnection a(config, Pseudo(1, 2), 5000, 6000, 1, [&](std::span<const u8>) { ++syns; });
a.Connect(now);
while (const auto next = a.NextDeadline()) {
now = *next;
a.OnTimer(now);
}
EXPECT_EQ(a.State(), TcpState::Closed);
EXPECT_EQ(a.PendingError(), Error::TimedOut);
EXPECT_EQ(syns, 1 + config.syn_retries);
EXPECT_GE(std::chrono::duration<double>(now.time_since_epoch()).count(), 60.0);
}
TEST(NetP2PTcp, AppIoBeforeAndAfterConnection) {
Clock::time_point now{};
TcpConnection a(TcpConfig{}, Pseudo(1, 2), 5000, 6000, 1, [](std::span<const u8>) {});
a.Connect(now);
const u8 byte = 1;
u8 buf[4];
EXPECT_EQ(a.Send({&byte, 1}, now).error, Error::WouldBlock); // still connecting
EXPECT_EQ(a.Recv(buf, false, now).error, Error::WouldBlock);
a.Abort();
EXPECT_EQ(a.State(), TcpState::Closed);
EXPECT_EQ(a.Send({&byte, 1}, now).error, Error::NotConn);
}
} // namespace