mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-10-28 12:38:43 +01:00
* 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>
476 lines
16 KiB
C++
476 lines
16 KiB
C++
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
// Linux only needs root or CAP_NET_ADMIN, otherwise every test is skipped.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#ifdef __linux__
|
|
|
|
#include <arpa/inet.h>
|
|
#include <fcntl.h>
|
|
#include <linux/if.h>
|
|
#include <linux/if_tun.h>
|
|
#include <netinet/in.h>
|
|
#include <poll.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
|
|
#include <atomic>
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <random>
|
|
#include <thread>
|
|
|
|
#include "core/net/p2p_tcp.h"
|
|
|
|
using namespace Core::Net;
|
|
using namespace Core::Net::P2P;
|
|
using namespace std::chrono_literals;
|
|
|
|
namespace {
|
|
|
|
constexpr const char* KernelIp = "10.77.0.1"; // the TUN interface address (kernel TCP)
|
|
constexpr const char* OurIp = "10.77.0.2"; // the address our TCP answers for
|
|
|
|
std::vector<u8> RandomBytes(size_t n, u32 seed) {
|
|
std::mt19937 rng(seed);
|
|
std::vector<u8> v(n);
|
|
for (auto& b : v) {
|
|
b = static_cast<u8>(rng());
|
|
}
|
|
return v;
|
|
}
|
|
|
|
u16 IpChecksum(const u8* header, size_t size) {
|
|
u32 sum = 0;
|
|
for (size_t i = 0; i < size; i += 2) {
|
|
sum += (header[i] << 8) | header[i + 1];
|
|
}
|
|
while (sum >> 16) {
|
|
sum = (sum & 0xffff) + (sum >> 16);
|
|
}
|
|
return static_cast<u16>(~sum);
|
|
}
|
|
|
|
/// Kernel socket helpers with a generous timeout so a broken test fails instead of hanging.
|
|
int KernelSocket() {
|
|
const int s = socket(AF_INET, SOCK_STREAM, 0);
|
|
timeval tv{120, 0};
|
|
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
|
return s;
|
|
}
|
|
|
|
sockaddr_in Address(const char* ip, u16 port) {
|
|
sockaddr_in addr{};
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(port);
|
|
inet_pton(AF_INET, ip, &addr.sin_addr);
|
|
return addr;
|
|
}
|
|
|
|
/// Our side of the network: owns TcpConnections keyed by (local port, remote port), accepts
|
|
/// SYNs on listening ports and resets everything else, like the P2P transport does.
|
|
class TunHost {
|
|
public:
|
|
explicit TunHost(int tun) : tun_(tun) {
|
|
inet_pton(AF_INET, OurIp, pseudo_.local.data());
|
|
inet_pton(AF_INET, KernelIp, pseudo_.remote.data());
|
|
}
|
|
|
|
double loss = 0.0;
|
|
std::map<std::pair<u16, u16>, std::unique_ptr<TcpConnection>> connections;
|
|
std::map<u16, bool> listeners;
|
|
|
|
TcpConnection& Connect(u16 local_port, u16 remote_port) {
|
|
auto& c = connections[{local_port, remote_port}];
|
|
c = std::make_unique<TcpConnection>(config_, pseudo_, local_port, remote_port, NextIss(),
|
|
[this](std::span<const u8> s) { EmitToKernel(s); });
|
|
c->Connect(Clock::now());
|
|
return *c;
|
|
}
|
|
|
|
TcpConnection* First() {
|
|
return connections.empty() ? nullptr : connections.begin()->second.get();
|
|
}
|
|
|
|
/// Runs the bridge until `step()` returns true or the time limit passes.
|
|
bool Run(const std::function<bool()>& step, std::chrono::seconds limit) {
|
|
const auto end = Clock::now() + limit;
|
|
u8 buf[65536];
|
|
while (Clock::now() < end) {
|
|
if (step()) {
|
|
return true;
|
|
}
|
|
auto next = Clock::now() + 5ms;
|
|
for (auto& [_, c] : connections) {
|
|
if (const auto d = c->NextDeadline(); d && *d < next) {
|
|
next = *d;
|
|
}
|
|
}
|
|
const auto wait = std::chrono::ceil<std::chrono::milliseconds>(
|
|
std::max<Clock::duration>(next - Clock::now(), 0ms));
|
|
pollfd pfd{tun_, POLLIN, 0};
|
|
poll(&pfd, 1, static_cast<int>(wait.count()));
|
|
for (;;) {
|
|
const auto n = read(tun_, buf, sizeof(buf));
|
|
if (n <= 0) {
|
|
break;
|
|
}
|
|
OnPacket({buf, static_cast<size_t>(n)});
|
|
}
|
|
const auto now = Clock::now();
|
|
for (auto& [_, c] : connections) {
|
|
if (const auto d = c->NextDeadline(); d && *d <= now) {
|
|
c->OnTimer(now);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int checksum_failures = 0;
|
|
|
|
private:
|
|
bool Drop() {
|
|
return loss > 0 && std::uniform_real_distribution<double>(0, 1)(rng_) < loss;
|
|
}
|
|
|
|
u32 NextIss() {
|
|
const u32 iss = next_iss_;
|
|
next_iss_ += 0x01000193;
|
|
return iss;
|
|
}
|
|
|
|
/// Wraps one of our TCP segments in IPv4 and hands it to the kernel.
|
|
void EmitToKernel(std::span<const u8> segment) {
|
|
if (Drop()) {
|
|
return;
|
|
}
|
|
std::vector<u8> packet(20 + segment.size());
|
|
packet[0] = 0x45;
|
|
packet[2] = static_cast<u8>(packet.size() >> 8);
|
|
packet[3] = static_cast<u8>(packet.size());
|
|
packet[6] = 0x40; // don't fragment
|
|
packet[8] = 64;
|
|
packet[9] = IPPROTO_TCP;
|
|
inet_pton(AF_INET, OurIp, packet.data() + 12);
|
|
inet_pton(AF_INET, KernelIp, packet.data() + 16);
|
|
const u16 checksum = IpChecksum(packet.data(), 20);
|
|
packet[10] = static_cast<u8>(checksum >> 8);
|
|
packet[11] = static_cast<u8>(checksum);
|
|
std::copy(segment.begin(), segment.end(), packet.begin() + 20);
|
|
[[maybe_unused]] const auto r = write(tun_, packet.data(), packet.size());
|
|
}
|
|
|
|
void OnPacket(std::span<const u8> packet) {
|
|
if (packet.size() < 20 || (packet[0] >> 4) != 4 || packet[9] != IPPROTO_TCP) {
|
|
return;
|
|
}
|
|
const size_t ihl = (packet[0] & 0x0f) * 4;
|
|
const size_t total = (packet[2] << 8) | packet[3];
|
|
if (total > packet.size() || ihl > total || Drop()) {
|
|
return;
|
|
}
|
|
// Verifying the kernel's checksum against our pseudo-header also proves ours agree.
|
|
const auto seg = ParseTcpSegment(packet.subspan(ihl, total - ihl), &pseudo_);
|
|
if (!seg) {
|
|
++checksum_failures;
|
|
return;
|
|
}
|
|
const auto key = std::pair{seg->dst_port, seg->src_port};
|
|
if (const auto it = connections.find(key);
|
|
it != connections.end() && it->second->State() != TcpState::Closed) {
|
|
it->second->OnSegment(*seg, Clock::now());
|
|
return;
|
|
}
|
|
if ((seg->flags & (TcpFlag::Syn | TcpFlag::Ack | TcpFlag::Rst)) == TcpFlag::Syn &&
|
|
listeners.contains(seg->dst_port)) {
|
|
auto& c = connections[key];
|
|
c = std::make_unique<TcpConnection>(config_, pseudo_, seg->dst_port, seg->src_port,
|
|
NextIss(),
|
|
[this](std::span<const u8> s) { EmitToKernel(s); });
|
|
c->AcceptSyn(*seg, Clock::now());
|
|
return;
|
|
}
|
|
if (const auto rst = BuildResetFor(pseudo_, *seg)) {
|
|
EmitToKernel(*rst);
|
|
}
|
|
}
|
|
|
|
int tun_;
|
|
TcpConfig config_;
|
|
PseudoHeader pseudo_;
|
|
std::mt19937 rng_{42};
|
|
u32 next_iss_ = 0xfffffc00u; // exercise sequence wraparound
|
|
};
|
|
|
|
class NetP2PTcpKernelInterop : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
tun_ = open("/dev/net/tun", O_RDWR | O_NONBLOCK);
|
|
if (tun_ < 0) {
|
|
GTEST_SKIP() << "no /dev/net/tun access (needs root or CAP_NET_ADMIN)";
|
|
}
|
|
ifreq ifr{};
|
|
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
|
|
std::strncpy(ifr.ifr_name, "shadps4tun0", IFNAMSIZ - 1);
|
|
if (ioctl(tun_, TUNSETIFF, &ifr) < 0) {
|
|
GTEST_SKIP() << "cannot create TUN device: " << std::strerror(errno);
|
|
}
|
|
const int s = socket(AF_INET, SOCK_DGRAM, 0);
|
|
auto* addr = reinterpret_cast<sockaddr_in*>(&ifr.ifr_addr);
|
|
addr->sin_family = AF_INET;
|
|
inet_pton(AF_INET, KernelIp, &addr->sin_addr);
|
|
const bool ok = ioctl(s, SIOCSIFADDR, &ifr) == 0 &&
|
|
(inet_pton(AF_INET, "255.255.255.0", &addr->sin_addr),
|
|
ioctl(s, SIOCSIFNETMASK, &ifr) == 0) &&
|
|
ioctl(s, SIOCGIFFLAGS, &ifr) == 0 &&
|
|
(ifr.ifr_flags |= IFF_UP | IFF_RUNNING, ioctl(s, SIOCSIFFLAGS, &ifr) == 0);
|
|
close(s);
|
|
if (!ok) {
|
|
GTEST_SKIP() << "cannot configure TUN device: " << std::strerror(errno);
|
|
}
|
|
std::this_thread::sleep_for(100ms);
|
|
host_ = std::make_unique<TunHost>(tun_);
|
|
}
|
|
|
|
void TearDown() override {
|
|
host_.reset();
|
|
if (tun_ >= 0) {
|
|
close(tun_); // the non-persistent interface disappears with its fd
|
|
}
|
|
}
|
|
|
|
static bool Finished(const TcpConnection* c) {
|
|
return c && (c->State() == TcpState::Closed || c->State() == TcpState::TimeWait);
|
|
}
|
|
|
|
/// The kernel connects to us; we echo; the kernel verifies and half-closes.
|
|
void KernelClientToOurEcho(double loss, size_t bytes) {
|
|
host_->loss = loss;
|
|
host_->listeners[7000] = true;
|
|
const auto data = RandomBytes(bytes, 1);
|
|
std::atomic<bool> kernel_done{false};
|
|
std::vector<u8> kernel_got;
|
|
std::string kernel_error;
|
|
|
|
std::thread kernel([&] {
|
|
const int s = KernelSocket();
|
|
const auto addr = Address(OurIp, 7000);
|
|
if (connect(s, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) != 0) {
|
|
kernel_error = std::string("connect: ") + std::strerror(errno);
|
|
kernel_done = true;
|
|
return;
|
|
}
|
|
std::thread writer([&] {
|
|
for (size_t off = 0; off < data.size();) {
|
|
const auto n = send(s, data.data() + off, data.size() - off, 0);
|
|
if (n <= 0) {
|
|
break;
|
|
}
|
|
off += static_cast<size_t>(n);
|
|
}
|
|
shutdown(s, SHUT_WR);
|
|
});
|
|
u8 buf[16384];
|
|
for (;;) {
|
|
const auto n = recv(s, buf, sizeof(buf), 0);
|
|
if (n < 0) {
|
|
kernel_error = std::string("recv: ") + std::strerror(errno);
|
|
}
|
|
if (n <= 0) {
|
|
break;
|
|
}
|
|
kernel_got.insert(kernel_got.end(), buf, buf + n);
|
|
}
|
|
writer.join();
|
|
close(s);
|
|
kernel_done = true;
|
|
});
|
|
|
|
// Our echo: send back whatever arrives, close after the kernel's FIN.
|
|
std::vector<u8> pending;
|
|
bool shut = false;
|
|
host_->Run(
|
|
[&] {
|
|
if (auto* c = host_->First()) {
|
|
u8 buf[8192];
|
|
for (;;) {
|
|
const auto r = c->Recv(buf, false, Clock::now());
|
|
if (r.bytes == 0) {
|
|
break;
|
|
}
|
|
pending.insert(pending.end(), buf, buf + r.bytes);
|
|
}
|
|
while (!pending.empty()) {
|
|
const auto w = c->Send(pending, Clock::now());
|
|
if (w.bytes == 0) {
|
|
break;
|
|
}
|
|
pending.erase(pending.begin(), pending.begin() + w.bytes);
|
|
}
|
|
if (c->ReceivedFin() && pending.empty() && !shut) {
|
|
c->Shutdown(Clock::now());
|
|
shut = true;
|
|
}
|
|
}
|
|
return kernel_done.load();
|
|
},
|
|
300s);
|
|
kernel.join();
|
|
host_->Run([&] { return Finished(host_->First()); }, 20s); // final ACK exchange
|
|
|
|
const auto* c = host_->First();
|
|
EXPECT_TRUE(kernel_error.empty()) << kernel_error;
|
|
EXPECT_TRUE(kernel_got == data)
|
|
<< "kernel received " << kernel_got.size() << " of " << data.size();
|
|
ASSERT_TRUE(c);
|
|
EXPECT_EQ(c->PendingError(), Error::Ok);
|
|
EXPECT_TRUE(Finished(c)) << ToString(c->State());
|
|
EXPECT_EQ(host_->checksum_failures, 0);
|
|
}
|
|
|
|
/// We connect to a kernel echo server, send, verify the echo and close first.
|
|
void OurClientToKernelEcho(double loss, size_t bytes, int kernel_rcvbuf,
|
|
std::chrono::milliseconds kernel_stall) {
|
|
host_->loss = loss;
|
|
const int ls = socket(AF_INET, SOCK_STREAM, 0);
|
|
int one = 1;
|
|
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
|
if (kernel_rcvbuf) {
|
|
setsockopt(ls, SOL_SOCKET, SO_RCVBUF, &kernel_rcvbuf, sizeof(kernel_rcvbuf));
|
|
}
|
|
const auto addr = Address(KernelIp, 7001);
|
|
ASSERT_EQ(bind(ls, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)), 0)
|
|
<< std::strerror(errno);
|
|
listen(ls, 4);
|
|
|
|
std::atomic<bool> kernel_done{false};
|
|
std::thread kernel([&] {
|
|
const int s = accept(ls, nullptr, nullptr);
|
|
timeval tv{120, 0};
|
|
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
std::this_thread::sleep_for(kernel_stall); // let our sender hit a zero window
|
|
u8 buf[16384];
|
|
for (;;) {
|
|
const auto n = recv(s, buf, sizeof(buf), 0);
|
|
if (n <= 0) {
|
|
break;
|
|
}
|
|
for (ssize_t off = 0; off < n;) {
|
|
const auto w = send(s, buf + off, n - off, 0);
|
|
if (w <= 0) {
|
|
break;
|
|
}
|
|
off += w;
|
|
}
|
|
}
|
|
close(s);
|
|
kernel_done = true;
|
|
});
|
|
|
|
auto& c = host_->Connect(40001, 7001);
|
|
const auto data = RandomBytes(bytes, 2);
|
|
std::vector<u8> got;
|
|
size_t off = 0;
|
|
bool shut = false;
|
|
host_->Run(
|
|
[&] {
|
|
while (off < data.size()) {
|
|
const auto w = c.Send(std::span(data).subspan(off), Clock::now());
|
|
if (w.bytes == 0) {
|
|
break;
|
|
}
|
|
off += w.bytes;
|
|
}
|
|
if (off == data.size() && !shut && c.State() == TcpState::Established) {
|
|
c.Shutdown(Clock::now());
|
|
shut = true;
|
|
}
|
|
u8 buf[8192];
|
|
for (;;) {
|
|
const auto r = c.Recv(buf, false, Clock::now());
|
|
if (r.bytes == 0) {
|
|
break;
|
|
}
|
|
got.insert(got.end(), buf, buf + r.bytes);
|
|
}
|
|
return (c.ReceivedFin() && kernel_done && Finished(&c)) ||
|
|
c.PendingError() != Error::Ok;
|
|
},
|
|
300s);
|
|
if (!kernel_done) {
|
|
shutdown(ls, SHUT_RDWR);
|
|
c.Abort();
|
|
}
|
|
kernel.join();
|
|
close(ls);
|
|
|
|
EXPECT_TRUE(got == data) << "received " << got.size() << " of " << data.size();
|
|
EXPECT_EQ(c.PendingError(), Error::Ok);
|
|
EXPECT_EQ(host_->checksum_failures, 0);
|
|
}
|
|
|
|
int tun_ = -1;
|
|
std::unique_ptr<TunHost> host_;
|
|
};
|
|
|
|
TEST_F(NetP2PTcpKernelInterop, KernelClientToOurServer) {
|
|
KernelClientToOurEcho(0.0, 1 << 20);
|
|
}
|
|
|
|
TEST_F(NetP2PTcpKernelInterop, KernelClientToOurServerWithLoss) {
|
|
KernelClientToOurEcho(0.05, 256 << 10);
|
|
}
|
|
|
|
TEST_F(NetP2PTcpKernelInterop, OurClientToKernelServer) {
|
|
OurClientToKernelEcho(0.0, 1 << 20, 0, 0ms);
|
|
}
|
|
|
|
TEST_F(NetP2PTcpKernelInterop, OurClientToKernelServerWithLoss) {
|
|
OurClientToKernelEcho(0.05, 256 << 10, 0, 0ms);
|
|
}
|
|
|
|
TEST_F(NetP2PTcpKernelInterop, ZeroWindowProbing) {
|
|
// The kernel reads nothing for 3 s with a tiny receive buffer: our persist timer has to
|
|
// probe the closed window until it reopens.
|
|
OurClientToKernelEcho(0.0, 256 << 10, 4096, 3000ms);
|
|
}
|
|
|
|
TEST_F(NetP2PTcpKernelInterop, OurConnectToClosedPortIsRefused) {
|
|
auto& c = host_->Connect(40002, 7009); // nothing listens on 7009
|
|
host_->Run([&] { return c.State() == TcpState::Closed; }, 20s);
|
|
EXPECT_EQ(c.PendingError(), Error::ConnRefused);
|
|
}
|
|
|
|
TEST_F(NetP2PTcpKernelInterop, KernelConnectToOurClosedPortIsRefused) {
|
|
std::atomic<int> result{0};
|
|
std::atomic<bool> done{false};
|
|
std::thread kernel([&] {
|
|
const int s = KernelSocket();
|
|
const auto addr = Address(OurIp, 7010);
|
|
result =
|
|
connect(s, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) == 0 ? 0 : errno;
|
|
close(s);
|
|
done = true;
|
|
});
|
|
host_->Run([&] { return done.load(); }, 20s);
|
|
kernel.join();
|
|
EXPECT_EQ(result, ECONNREFUSED); // we answered the SYN with a RST
|
|
}
|
|
|
|
} // namespace
|
|
|
|
#else
|
|
|
|
TEST(NetP2PTcpKernelInterop, Unsupported) {
|
|
GTEST_SKIP() << "the TUN interop tests run on Linux only";
|
|
}
|
|
|
|
#endif
|