diff --git a/include/rex/system/xnet_transport.h b/include/rex/system/xnet_transport.h new file mode 100644 index 0000000..f927181 --- /dev/null +++ b/include/rex/system/xnet_transport.h @@ -0,0 +1,161 @@ +/** + * netcode-revival: relay transport for the game's own netcode. + * + * The game's RakNet sockets talk to peers at "virtual" addresses + * (10.80.0.x, assigned per machine by the XNet layer). Real internet peers + * are not reachable at those addresses, so this transport routes their UDP + * through the ghost relay: a datagram to a virtual address is wrapped in a + * relay Game payload and sent OUTBOUND to the relay, which forwards it to + * the destination member's real socket (the server is a pure relay / NAT + * stabilizer). Inbound Game payloads are injected into the game's sockets + * via XSocket::QueuePacket, and XNetQosLookup discovers sessions by asking + * the relay who else is in the room. + * + * The wire structs below are minimal copies of the main repo's + * skate3_native_ghost_proto.h (deliberately kept dependency-free and + * one-way: the SDK must not include game-side headers). If they ever + * diverge, the relay's version check refuses the connection rather than + * corrupting traffic. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace rex { +namespace system { + +class XSocket; + +// Header of every relay datagram ("GHST" magic, little-endian type + size). +// Must match kGhostMagic / PacketHeader in skate3_native_ghost_proto.h. +struct XNetRelayHeader { + uint32_t magic; // "GHST" little-endian + uint8_t type; + uint16_t payload_size; +}; + +// kTypeJoin payload. Must match JoinPayload. +struct XNetRelayJoin { + uint8_t version; + uint8_t room_len; + char room[8]; + uint8_t name_len; + char name[16]; + uint32_t virtual_ip; // host-order 10.80.0.x +}; + +// kTypeGame payload. Must match GamePayload. +struct XNetRelayGame { + uint32_t dst_virtual_ip; // recipient (host order) or 0 = all room members + uint32_t src_virtual_ip; // filled by the relay on forward (host order) + uint16_t data_len; + uint8_t data[1400]; +}; + +// kTypeSessionReply payload. Must match SessionReplyPayload. +struct XNetRelaySessionEntry { + uint32_t session_id; + uint32_t virtual_ip; // host order + uint8_t name_len; + char name[16]; +}; +struct XNetRelaySessionReply { + uint8_t count; + XNetRelaySessionEntry list[32]; +}; + +// One discovered session (a member of our room). +struct XNetSession { + uint32_t session_id; + uint32_t virtual_ip; // host order + char name[16]; + uint8_t name_len; +}; + +class XNetTransport { + public: + static constexpr uint8_t kVersion = 5; + static constexpr uint32_t kMagic = 0x54484753u; // "GHST" LE + enum Type : uint8_t { + kJoin = 1, + kHello = 2, + kGame = 10, + kSessionQuery = 11, + kSessionReply = 12, + }; + + static XNetTransport& Instance(); + + // Configure the relay (host[:port], port) and the room to join. Idempotent; + // a changed relay/room reconnects. Called by the game-side ghost client + // when it (re)joins a room, so the game's netcode follows the same room. + void SetRelay(const char* host, uint16_t port, const char* room); + + // True once the transport has successfully joined the relay. + bool Active() const; + + // True when ip (host order) is in the virtual 10.80.0.x range. + static bool IsVirtualIp(uint32_t host_order_ip); + + // Route one datagram to a virtual destination via the relay. Returns the + // datagram length on accept, -1 when the transport is not active or the + // destination is not a virtual address. + int SendGame(const void* buf, uint32_t len, uint32_t dst_virtual_ip_host_order); + + // The local machine's virtual IP (host order) - mirrors XNetGetTitleXnAddr. + static uint32_t LocalVirtualIp(); + + // --- discovery (used by XNetQosLookup) ------------------------------------ + // Send a session query for our room; the reply lands asynchronously and is + // surfaced by Sessions() once WaitForReply returns. + void QuerySessions(); + // Block up to timeout_ms for a fresh session reply to arrive. + void WaitForReply(int timeout_ms); + std::vector Sessions() const; + + // --- receive injection ----------------------------------------------------- + // A UDP socket the game expects inbound netcode datagrams on. Delivered + // Game payloads are pushed into every registered socket via QueuePacket. + void RegisterSocket(XSocket* s); + void UnregisterSocket(XSocket* s); + + ~XNetTransport(); + + private: + XNetTransport() = default; + XNetTransport(const XNetTransport&) = delete; + XNetTransport& operator=(const XNetTransport&) = delete; + + void Reconnect(); + void StopLocked(); + void Run(); + void HandlePacket(const uint8_t* data, int size); + + int64_t sock_ = -1; // native UDP socket to the relay (platform handle) + // Resolved relay endpoint as raw bytes (sockaddr_in is 16 bytes on POSIX + // and Windows); cast back in the .cpp. Kept byte-form so this public + // header stays platform-independent. + uint8_t relay_addr_[16] = {}; + bool relay_resolved_ = false; + std::thread thread_; + bool running_ = false; + + mutable std::mutex mu_; + std::string host_; + uint16_t port_ = 0; + std::string room_; + bool active_ = false; + bool joined_ = false; + uint64_t reply_seq_ = 0; // bumped by every SessionReply + std::vector sessions_; + std::unordered_set sockets_; +}; + +} // namespace system +} // namespace rex \ No newline at end of file diff --git a/src/kernel/xam/xam_net.cpp b/src/kernel/xam/xam_net.cpp index f19fd2a..b365eac 100644 --- a/src/kernel/xam/xam_net.cpp +++ b/src/kernel/xam/xam_net.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -160,7 +161,8 @@ const XnetIdentity& XnetLocalIdentity() { seed = seed * 6364136223846793005ULL + 1442695040888963407ULL; id.ab_enet[i] = uint8_t(seed >> 24); } - id.virtual_ip = (10u << 24) | (80u << 16) | (0u << 8) | id.machine_id; + // Single source of truth with the relay transport (same 10.80.0.x). + id.virtual_ip = rex::system::XNetTransport::LocalVirtualIp(); return id; }(); return s_identity; @@ -632,9 +634,10 @@ u32 NetDll_XNetQosListen_entry(u32 caller, mapped_void id, mapped_void data, u32 return 0; } -// netcode-revival: session search. Phase 0 reports zero discovered sessions -// (the relay registry lands in Phase 1) but returns success and signals the -// completion event so the game's search finishes instead of hanging. +// netcode-revival: session search. Phase 1: ask the relay who else is in our +// room and hand the discovered sessions to the game, then signal the +// completion event. With no relay configured the search returns empty (the +// game's flow completes cleanly). u32 NetDll_XNetQosLookup_entry(u32 caller, u32 cxna, mapped_void apxna, mapped_void apxnk, u32 cxnk, mapped_void apxnaResults, mapped_u32 apxnqosResults, mapped_void axnqosInfoResults, u32 dwTimeoutMs, u32 hEvent, @@ -645,12 +648,47 @@ u32 NetDll_XNetQosLookup_entry(u32 caller, u32 cxna, mapped_void apxna, mapped_v (void)cxnk; (void)apxnaResults; (void)axnqosInfoResults; - (void)dwTimeoutMs; (void)dwFlags; + + using rex::system::XNetTransport; + + std::vector sessions; + if (XNetTransport::Instance().Active()) { + // Ask the relay, wait up to ~500ms for the reply, then report members. + XNetTransport::Instance().QuerySessions(); + XNetTransport::Instance().WaitForReply(500); + sessions = XNetTransport::Instance().Sessions(); + } + + // Up to 8 discovered sessions (the game's XNQOS addresses array is 8). + auto qos_guest = REX_KERNEL_MEMORY()->SystemHeapAlloc(sizeof(XNQOS) + sizeof(XNQOSINFO) * 7); + auto qos = REX_KERNEL_MEMORY()->TranslateVirtual(qos_guest); + qos->count = qos->count_pending = 0; + uint32_t n = 0; + for (const auto& s : sessions) { + if (n >= 8) { + break; + } + if (!XNetTransport::IsVirtualIp(s.virtual_ip) || s.virtual_ip == XNetTransport::LocalVirtualIp()) { + continue; // only real peers, not ourselves + } + // Each session's QoS info references the host's XNADDR (built from the + // discovered virtual IP) as its QoS data. + XNQOSINFO& info = qos->info[n]; + info.flags = 1; // XNET_QOS_FLAG_NONE equivalent - address present + info.probes_xmit = 1; + info.probes_recv = 1; + info.data_len = sizeof(XNADDR); + auto xnaddr_guest = REX_KERNEL_MEMORY()->SystemHeapAlloc(sizeof(XNADDR)); + auto xnaddr = REX_KERNEL_MEMORY()->TranslateVirtual(xnaddr_guest); + std::memset(xnaddr, 0, sizeof(XNADDR)); + xnaddr->ina.s_addr = htonl(s.virtual_ip); + info.data_ptr = xnaddr_guest; + ++n; + } + qos->count = qos->count_pending = n; + if (apxnqosResults) { - auto qos_guest = REX_KERNEL_MEMORY()->SystemHeapAlloc(sizeof(XNQOS)); - auto qos = REX_KERNEL_MEMORY()->TranslateVirtual(qos_guest); - qos->count = qos->count_pending = 0; *apxnqosResults = qos_guest; } if (hEvent) { diff --git a/src/system/CMakeLists.txt b/src/system/CMakeLists.txt index d01f0ed..e507099 100644 --- a/src/system/CMakeLists.txt +++ b/src/system/CMakeLists.txt @@ -14,6 +14,7 @@ set(REXSYSTEM_SOURCES xnotifylistener.cpp xenumerator.cpp xsocket.cpp + xnet_transport.cpp xsymboliclink.cpp xobject.cpp xmodule.cpp diff --git a/src/system/xnet_transport.cpp b/src/system/xnet_transport.cpp new file mode 100644 index 0000000..000d596 --- /dev/null +++ b/src/system/xnet_transport.cpp @@ -0,0 +1,361 @@ +/** + * netcode-revival: relay transport for the game's own netcode. + * See xnet_transport.h for the design. + */ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#if REX_PLATFORM_WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +namespace rex { +namespace system { + +namespace { + +// Virtual address range: 10.80.0.0/16 (host order prefix 0x0A500000). +constexpr uint32_t kVirtualPrefix = 0x0A500000u; + +int64_t SocketCreate() { +#if REX_PLATFORM_WIN32 + return int64_t(::socket(AF_INET, SOCK_DGRAM, 0)); +#else + return int64_t(::socket(AF_INET, SOCK_DGRAM, 0)); +#endif +} + +void SocketClose(int64_t s) { +#if REX_PLATFORM_WIN32 + ::closesocket(SOCKET(s)); +#else + ::close(int(s)); +#endif +} + +bool SocketSendTo(int64_t s, const sockaddr_in* to, const void* buf, int size) { +#if REX_PLATFORM_WIN32 + const int n = ::sendto(SOCKET(s), reinterpret_cast(buf), size, 0, + reinterpret_cast(to), sizeof(*to)); +#else + const int n = ::sendto(int(s), buf, size, 0, + reinterpret_cast(to), sizeof(*to)); +#endif + return n == size; +} + +// Resolve "host" to a sockaddr_in (UDP), defaulting the port. Returns true +// on success. +bool Resolve(const std::string& host, uint16_t port, sockaddr_in* out) { + addrinfo hints = {}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + addrinfo* res = nullptr; + const std::string port_str = std::to_string(port); + if (getaddrinfo(host.c_str(), port_str.c_str(), &hints, &res) != 0 || + res == nullptr) { + if (res != nullptr) { + freeaddrinfo(res); + } + return false; + } + *out = *reinterpret_cast(res->ai_addr); + out->sin_port = htons(port); + freeaddrinfo(res); + return true; +} + +} // namespace + +XNetTransport& XNetTransport::Instance() { + static XNetTransport s_instance; + return s_instance; +} + +XNetTransport::~XNetTransport() { + std::lock_guard lock(mu_); + StopLocked(); +} + +uint32_t XNetTransport::LocalVirtualIp() { + // Mirrors the XNet identity: machine id from the signed-in profile XUID, + // mapped onto 10.80.0.x. Stable across runs on one box. + uint64_t seed = 0; + if (xam::UserProfile* profile = REX_KERNEL_STATE()->user_profile()) { + seed = profile->xuid(); + } + if (seed == 0) { + seed = 0x52454B4E45544CULL; + } + const uint8_t machine_id = uint8_t((seed ^ (seed >> 32)) & 0xFEu) + 1; + return kVirtualPrefix | machine_id; +} + +bool XNetTransport::IsVirtualIp(uint32_t host_order_ip) { + return (host_order_ip & 0xFFFF0000u) == kVirtualPrefix; +} + +bool XNetTransport::Active() const { + std::lock_guard lock(mu_); + return active_; +} + +void XNetTransport::SetRelay(const char* host, uint16_t port, const char* room) { + { + std::lock_guard lock(mu_); + const std::string h = host ? host : ""; + const std::string r = room ? room : ""; + if (h == host_ && port == port_ && r == room_ && active_) { + return; // unchanged and already connected + } + host_ = h; + port_ = port; + room_ = r; + StopLocked(); + } + Reconnect(); +} + +void XNetTransport::StopLocked() { + if (thread_.joinable()) { + running_ = false; + thread_.join(); + thread_ = {}; + } + if (sock_ != -1) { + SocketClose(sock_); + sock_ = -1; + } + active_ = false; + joined_ = false; + sessions_.clear(); +} + +void XNetTransport::Reconnect() { + std::lock_guard lock(mu_); + if (host_.empty() || port_ == 0) { + return; + } + sockaddr_in relay_addr = {}; + if (!Resolve(host_, port_, &relay_addr)) { + return; + } + std::memcpy(relay_addr_, &relay_addr, sizeof(relay_addr)); + relay_resolved_ = true; + sock_ = SocketCreate(); + if (sock_ == -1) { + return; + } + // Join the room so the relay routes our game traffic among its members. + XNetRelayJoin jp = {}; + jp.version = kVersion; + jp.virtual_ip = LocalVirtualIp(); + const uint32_t room_len = + std::min(room_.size(), sizeof(jp.room)); + std::memcpy(jp.room, room_.data(), room_len); + jp.room_len = uint8_t(room_len); + const char name[] = "skate3"; + const uint32_t name_len = std::min(sizeof(name) - 1, sizeof(jp.name)); + std::memcpy(jp.name, name, name_len); + jp.name_len = uint8_t(name_len); + uint8_t pkt[7 + sizeof(jp)] = {}; + XNetRelayHeader* h = reinterpret_cast(pkt); + h->magic = kMagic; + h->type = kJoin; + h->payload_size = uint16_t(sizeof(jp)); + std::memcpy(pkt + 7, &jp, sizeof(jp)); + SocketSendTo(sock_, &relay_addr, pkt, sizeof(pkt)); + + running_ = true; + thread_ = std::thread(&XNetTransport::Run, this); +} + +int XNetTransport::SendGame(const void* buf, uint32_t len, uint32_t dst_virtual_ip_host_order) { + if (len == 0 || len > sizeof(XNetRelayGame::data)) { + return -1; + } + if (!IsVirtualIp(dst_virtual_ip_host_order) && dst_virtual_ip_host_order != 0) { + return -1; + } + std::lock_guard lock(mu_); + if (!active_ || sock_ == -1 || !relay_resolved_) { + return -1; + } + const sockaddr_in* relay = reinterpret_cast(relay_addr_); + XNetRelayGame gp = {}; + gp.dst_virtual_ip = dst_virtual_ip_host_order; + gp.data_len = uint16_t(len); + std::memcpy(gp.data, buf, len); + uint8_t pkt[7 + sizeof(gp)] = {}; + XNetRelayHeader* h = reinterpret_cast(pkt); + h->magic = kMagic; + h->type = kGame; + h->payload_size = uint16_t(sizeof(gp)); + std::memcpy(pkt + 7, &gp, sizeof(gp)); + if (!SocketSendTo(sock_, relay, pkt, sizeof(pkt))) { + return -1; + } + return int(len); +} + +void XNetTransport::QuerySessions() { + std::lock_guard lock(mu_); + if (!active_ || sock_ == -1 || !relay_resolved_) { + return; + } + const sockaddr_in* relay = reinterpret_cast(relay_addr_); + uint8_t pkt[7] = {}; + XNetRelayHeader* h = reinterpret_cast(pkt); + h->magic = kMagic; + h->type = kSessionQuery; + h->payload_size = 0; + SocketSendTo(sock_, relay, pkt, sizeof(pkt)); +} + +void XNetTransport::WaitForReply(int timeout_ms) { + const uint64_t target = [this] { + std::lock_guard lock(mu_); + return reply_seq_; + }(); + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(timeout_ms); + while (std::chrono::steady_clock::now() < deadline) { + std::lock_guard lock(mu_); + if (reply_seq_ > target) { + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } +} + +std::vector XNetTransport::Sessions() const { + std::lock_guard lock(mu_); + return sessions_; +} + +void XNetTransport::RegisterSocket(XSocket* s) { + if (s == nullptr) { + return; + } + std::lock_guard lock(mu_); + sockets_.insert(s); +} + +void XNetTransport::UnregisterSocket(XSocket* s) { + std::lock_guard lock(mu_); + sockets_.erase(s); +} + +void XNetTransport::Run() { + while (true) { + { + std::lock_guard lock(mu_); + if (!running_) { + return; + } + } + uint8_t buf[7 + sizeof(XNetRelayGame)]; + sockaddr_in from = {}; + socklen_t from_len = sizeof(from); +#if REX_PLATFORM_WIN32 + int n = ::recvfrom(SOCKET(sock_), reinterpret_cast(buf), sizeof(buf), 0, + reinterpret_cast(&from), &from_len); +#else + int n = ::recvfrom(int(sock_), buf, sizeof(buf), 0, + reinterpret_cast(&from), &from_len); +#endif + if (n < 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + continue; + } + if (n < 7) { + continue; + } + HandlePacket(buf, n); + } +} + +void XNetTransport::HandlePacket(const uint8_t* data, int size) { + const XNetRelayHeader* h = reinterpret_cast(data); + if (h->magic != kMagic || h->payload_size > 3200 || + size < int(sizeof(*h) + h->payload_size)) { + return; + } + const uint8_t* payload = data + sizeof(*h); + + switch (h->type) { + case kHello: { + std::lock_guard lock(mu_); + // session_id != 0 means accepted (0 = refused). + active_ = payload[0] != 0; + joined_ = true; + break; + } + case kGame: { + if (h->payload_size < sizeof(XNetRelayGame)) { + return; + } + XNetRelayGame gp = {}; + std::memcpy(&gp, payload, sizeof(gp)); + if (gp.data_len > sizeof(gp.data)) { + return; + } + std::vector targets; + { + std::lock_guard lock(mu_); + for (XSocket* s : sockets_) { + targets.push_back(s); + } + } + for (XSocket* s : targets) { + // src_virtual_ip is host order; QueuePacket's src is host order too. + s->QueuePacket(gp.src_virtual_ip, 0, gp.data, gp.data_len); + } + break; + } + case kSessionReply: { + if (h->payload_size < sizeof(XNetRelaySessionReply)) { + return; + } + XNetRelaySessionReply sr = {}; + std::memcpy(&sr, payload, sizeof(sr)); + std::vector fresh; + const uint8_t count = std::min(sr.count, 32); + for (uint8_t i = 0; i < count; ++i) { + XNetSession s; + s.session_id = sr.list[i].session_id; + s.virtual_ip = sr.list[i].virtual_ip; + s.name_len = std::min(sr.list[i].name_len, sizeof(s.name)); + std::memcpy(s.name, sr.list[i].name, s.name_len); + fresh.push_back(s); + } + std::lock_guard lock(mu_); + sessions_ = std::move(fresh); + ++reply_seq_; + break; + } + default: + break; // kTypeRooms / Pose / Ping / Pong: not this transport's business. + } +} + +} // namespace system +} // namespace rex \ No newline at end of file diff --git a/src/system/xsocket.cpp b/src/system/xsocket.cpp index 67753bf..9ef4c3d 100644 --- a/src/system/xsocket.cpp +++ b/src/system/xsocket.cpp @@ -16,6 +16,7 @@ #include #include // #include +#include #include @@ -65,10 +66,15 @@ X_STATUS XSocket::Initialize(AddressFamily af, Type type, Protocol proto) { return X_STATUS_UNSUCCESSFUL; } + // netcode-revival: relayed game traffic is injected into registered + // sockets; the game's RakNet netcode socket is one of them. + XNetTransport::Instance().RegisterSocket(this); + return X_STATUS_SUCCESS; } X_STATUS XSocket::Close() { + XNetTransport::Instance().UnregisterSocket(this); int ret = rex::net::socket_close(native_handle_); if (ret != 0) { return X_STATUS_UNSUCCESSFUL; @@ -171,28 +177,29 @@ int XSocket::Recv(uint8_t* buf, uint32_t buf_len, uint32_t flags) { int XSocket::RecvFrom(uint8_t* buf, uint32_t buf_len, uint32_t flags, N_XSOCKADDR_IN* from, uint32_t* from_len) { - // Pop from secure packets first - // TODO(DrChat): Enable when I commit XNet - /* + // netcode-revival: relayed game datagrams (from the XNetTransport receive + // thread) are queued here and surface ahead of any native traffic. { std::lock_guard lock(incoming_packet_mutex_); - if (incoming_packets_.size()) { - packet* pkt = (packet*)incoming_packets_.front(); - int data_len = pkt->data_len; - std::memcpy(buf, pkt->data, std::min((uint32_t)pkt->data_len, buf_len)); - - from->sin_family = 2; - from->sin_addr = pkt->src_ip; - from->sin_port = pkt->src_port; - + if (!incoming_packets_.empty()) { + packet* pkt = reinterpret_cast(incoming_packets_.front()); incoming_packets_.pop(); - uint8_t* pkt_ui8 = (uint8_t*)pkt; - delete[] pkt_ui8; - - return data_len; + const uint32_t data_len = pkt->data_len; + const uint32_t n = std::min(data_len, buf_len); + std::memcpy(buf, pkt->data, n); + if (from) { + from->sin_family = 2; + from->sin_addr = pkt->src_ip; + from->sin_port = pkt->src_port; + std::memset(from->x_sin_zero, 0, sizeof(from->x_sin_zero)); + } + if (from_len) { + *from_len = sizeof(sockaddr_in); + } + delete[] reinterpret_cast(pkt); + return int(n); } } - */ sockaddr_in nfrom; socklen_t nfromlen = sizeof(sockaddr_in); @@ -218,16 +225,12 @@ int XSocket::Send(const uint8_t* buf, uint32_t buf_len, uint32_t flags) { int XSocket::SendTo(uint8_t* buf, uint32_t buf_len, uint32_t flags, N_XSOCKADDR_IN* to, uint32_t to_len) { - // Send 2 copies of the packet: One to XNet (for network security) and an - // unencrypted copy for other Xenia hosts. - // TODO(DrChat): Enable when I commit XNet. - /* - auto xam = kernel_state()->GetKernelModule("xam.xex"); - auto xnet = xam->xnet(); - if (xnet) { - xnet->SendPacket(this, to, buf, buf_len); + // netcode-revival: a datagram addressed to a virtual 10.80.0.x peer is + // routed through the relay (the game's RakNet netcode), not sent raw to + // an unreachable address. + if (to && XNetTransport::IsVirtualIp(uint32_t(to->sin_addr))) { + return XNetTransport::Instance().SendGame(buf, buf_len, uint32_t(to->sin_addr)); } - */ sockaddr_in nto; if (to) {