Files
ARMSX2/pcsx2/DEV9/LocalLinkAdapter.cpp
jpolo1224 069f8a44f3 Android 2.6.5.1: Local Link LAN play, async GS readback, pause/rotation/settings fixes
Crash and correctness
- Fix a crash when backgrounding the app mid-game: onPause flushed the Vulkan
  pipeline cache from the UI thread while the GS thread was creating pipelines
  into the same VkPipelineCache. Vulkan requires that handle to be externally
  synchronised, so this was a driver-level data race and crashed on Adreno and
  Xclipse alike. The flush now runs on the GS thread, posted via the CPU thread
  so it does not race the EE-owned MTGS ring.
- Fix an unbounded out-of-bounds vertex read in the GSRendererHW sprite-merge
  paving path: the inner loop advanced i instead of j, so j stayed loop-invariant
  and the scan walked past m_vertex->tail.
- Fix per-game settings being silently ignored: gamesettings/<serial>_<CRC>.ini
  loads into a higher-priority layer than anything the app writes, and saves made
  from the library never regenerated it, so any key already in that file
  overrode the user permanently. Only the category-Reset path rewrote it, which
  is why Reset appeared to be the only thing that worked.
- Fix screen rotation: the BIOS followed the launcher rotation instead of the
  renderer's (it has no GameInfo, and the tier was keyed on that), and the
  launcher stayed locked in a game's orientation after exit because the cleanup
  lived only inside stop()'s vmRunLoopActive-guarded branch, which loses a race
  against the VM thread's own finally. Rotation tier is now an explicit flag and
  the cleanup runs on every terminal path.
- Discard the Vulkan pipeline blob whenever the SPIR-V cache is discarded. It was
  validated only against the device header (vendor/device/pipelineCacheUUID),
  which is identical across an app update, so a SHADER_CACHE_VERSION bump kept
  every pipeline built from the old shaders and nothing pruned it.
- Make eeRecExitRequested atomic: it was a plain bool written from the JNI thread
  and read on the CPU thread.
- OpenGL: restore GL_PACK_ALIGNMENT after readback, add the missing memory
  barrier after the CAS dispatch, and initialise GLState::depth_mask to GL's
  actual default.
- DEV9: log the GetNetAdapter default: bail and the InitNet skip. Both returned
  silently, so a settings mistake surfaced as missing hardware three layers away.

Local Link (new)
- New DEV9 backend bridging emulated PS2 Ethernet between devices over
  authenticated local UDP, so games with a built-in LAN / System Link mode can
  play together. Ported from EmuCoreX (sashkinbro) with the wire format
  unchanged, so peers remain compatible across both forks.
- Network mode picker (Online / Host / Join), host address readout, auto-derived
  peer ids, generated room codes, hostname support alongside numeric IPv4, and a
  link to the supported-games list. Fully controller-navigable.

Performance
- Asynchronous hardware download mode (experimental, opt-in): non-blocking
  GPU->CPU readback so the EE thread no longer waits on the GS thread. Ported
  from EmuCoreX. Appending Asynchronous to GSHardwareDownloadMode makes the enum
  non-ordered, so the relational comparisons on it are replaced with
  IsHardwareDownloadReadbackEnabled / IsHardwareDownloadEEThreadRead.
- Affinity Control Mode (experimental, opt-in): EE/VU/GS priority orders plus a
  Performance Cores mode. Android otherwise leaves these threads unpinned.
- Raise the texture-replacement cache ceiling from 6 to 16 GB; RAM/2 remains the
  real limiter, so this only binds at 12 GB RAM and up.
- Low Latency frame pacing is no longer the default, with a one-time migration
  for installs that took the earlier flip.

Features
- Auto renderer resolves to Vulkan HW on Adreno.
- Auto Progressive Scan (per-game): holds Triangle+Cross through boot.
- OLED black as a modifier over any accent colour, including Custom and RGB.
- Optional system keyboard instead of the built-in on-screen one.

Game compatibility
- Everybody's Golf 4 / Hot Shots Golf Fore! hwDownloadMode across all regions
  (PR #421, XDarkFallenX).
- Delta Force: Black Hawk Down (PR #401, XDarkFallenX).
- Reduced input latency and input handling improvements (PR #403, Splaser).

RetroAchievements
- Inject the client version from a build-time secret kept out of public source,
  with a stock-PCSX2 fallback for secret-less builds, so third parties cannot
  copy the client identity. Covers the iOS token too.
2026-07-25 00:48:56 -04:00

707 lines
21 KiB
C++

// SPDX-FileCopyrightText: 2026 EmuCoreX contributors
// SPDX-License-Identifier: GPL-3.0+
#include "LocalLinkAdapter.h"
#include "DEV9.h"
#include "common/Console.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstring>
#include <random>
#ifdef _WIN32
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#include <cerrno>
#include <fcntl.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
namespace
{
constexpr u32 LOCAL_LINK_MAGIC = 0x45434c58; // "ECLX" on the wire after htonl().
constexpr u8 LOCAL_LINK_VERSION = 1;
constexpr u8 MESSAGE_HELLO = 1;
constexpr u8 MESSAGE_HELLO_ACK = 2;
constexpr u8 MESSAGE_DATA = 3;
#pragma pack(push, 1)
struct WireHeader
{
u32 magic;
u8 version;
u8 type;
u16 header_size;
u32 peer_id;
u32 sequence;
u32 frame_id;
u16 fragment_index;
u16 fragment_count;
u16 payload_size;
u16 reserved;
u64 session_nonce;
u64 auth_tag;
};
#pragma pack(pop)
static_assert(sizeof(WireHeader) == 44);
u64 RotateLeft(u64 value, int bits)
{
return (value << bits) | (value >> (64 - bits));
}
u64 HostToNetwork64(u64 value)
{
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
return value;
#else
return (static_cast<u64>(htonl(static_cast<u32>(value))) << 32) | htonl(static_cast<u32>(value >> 32));
#endif
}
u64 NetworkToHost64(u64 value)
{
return HostToNetwork64(value);
}
u64 Load64Le(const u8* bytes)
{
u64 value = 0;
for (int i = 7; i >= 0; --i)
value = (value << 8) | bytes[i];
return value;
}
void SipRound(u64& v0, u64& v1, u64& v2, u64& v3)
{
v0 += v1;
v1 = RotateLeft(v1, 13);
v1 ^= v0;
v0 = RotateLeft(v0, 32);
v2 += v3;
v3 = RotateLeft(v3, 16);
v3 ^= v2;
v0 += v3;
v3 = RotateLeft(v3, 21);
v3 ^= v0;
v2 += v1;
v1 = RotateLeft(v1, 17);
v1 ^= v2;
v2 = RotateLeft(v2, 32);
}
u64 SipHash24(const u8* data, std::size_t size, u64 key0, u64 key1)
{
u64 v0 = 0x736f6d6570736575ULL ^ key0;
u64 v1 = 0x646f72616e646f6dULL ^ key1;
u64 v2 = 0x6c7967656e657261ULL ^ key0;
u64 v3 = 0x7465646279746573ULL ^ key1;
const u8* cursor = data;
const u8* end = data + (size & ~static_cast<std::size_t>(7));
while (cursor != end)
{
const u64 word = Load64Le(cursor);
v3 ^= word;
SipRound(v0, v1, v2, v3);
SipRound(v0, v1, v2, v3);
v0 ^= word;
cursor += 8;
}
u64 tail = static_cast<u64>(size) << 56;
for (std::size_t i = 0; i < (size & 7); ++i)
tail |= static_cast<u64>(cursor[i]) << (8 * i);
v3 ^= tail;
SipRound(v0, v1, v2, v3);
SipRound(v0, v1, v2, v3);
v0 ^= tail;
v2 ^= 0xff;
for (int i = 0; i < 4; ++i)
SipRound(v0, v1, v2, v3);
return v0 ^ v1 ^ v2 ^ v3;
}
} // namespace
LocalLinkAdapter::LocalLinkAdapter()
: NetAdapter()
{
if (!EmuConfig.DEV9.EthEnable || EmuConfig.DEV9.LocalLinkRoomCode.size() < 4 ||
EmuConfig.DEV9.LocalLinkRoomCode.size() > 12)
{
Console.Error("DEV9: Local Link requires Ethernet and a 4-12 character room code");
return;
}
m_host = EmuConfig.DEV9.LocalLinkHost;
m_port = static_cast<u16>(std::clamp<u32>(EmuConfig.DEV9.LocalLinkPort, 1024, 65535));
m_peer_id = m_host ? 1u : std::clamp<u32>(EmuConfig.DEV9.LocalLinkPeerId, 2, 65533);
if (m_host)
{
std::random_device random;
const u64 nonce = (static_cast<u64>(random()) << 32) | random();
m_session_nonce.store(nonce != 0 ? nonce : 1, std::memory_order_relaxed);
}
m_auth_key0 = DeriveKey(EmuConfig.DEV9.LocalLinkRoomCode, 0x9e3779b97f4a7c15ULL);
m_auth_key1 = DeriveKey(EmuConfig.DEV9.LocalLinkRoomCode, 0xd1b54a32d192ed03ULL);
if (!OpenSocket() || !ConfigureEndpoint())
{
close();
return;
}
PacketReader::MAC_Address mac = defaultMAC;
mac.bytes[4] = static_cast<u8>((m_peer_id >> 8) & 0xff);
mac.bytes[5] = static_cast<u8>(m_peer_id & 0xff);
SetMACAddress(&mac);
// Use the full peer id in a /16 so different identities never collapse to
// the same DHCP lease. Skip 192.0.2.1, which belongs to the internal server.
u32 host_part = m_peer_id;
if (host_part >= 513)
++host_part;
const PacketReader::IP::IP_Address ps2_ip{{{192, 0,
static_cast<u8>((host_part >> 8) & 0xff), static_cast<u8>(host_part & 0xff)}}};
const PacketReader::IP::IP_Address subnet{{{255, 255, 0, 0}}};
const PacketReader::IP::IP_Address gateway = internalIP;
InitInternalServer(nullptr, true, ps2_ip, subnet, gateway);
m_initialized.store(true, std::memory_order_release);
SendHelloIfNeeded(true);
Console.WriteLn("DEV9: Local Link %s ready on port %u as peer %u",
m_host ? "host" : "client", m_port, m_peer_id);
}
LocalLinkAdapter::~LocalLinkAdapter()
{
close();
}
bool LocalLinkAdapter::blocks()
{
return false;
}
bool LocalLinkAdapter::isInitialised()
{
return m_initialized.load(std::memory_order_acquire);
}
bool LocalLinkAdapter::OpenSocket()
{
#ifdef _WIN32
WSADATA data{};
if (WSAStartup(MAKEWORD(2, 2), &data) != 0)
return false;
m_wsa_started = true;
m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (m_socket == INVALID_SOCKET)
return false;
u_long non_blocking = 1;
if (ioctlsocket(m_socket, FIONBIO, &non_blocking) != 0)
return false;
#else
m_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (m_socket < 0)
return false;
const int flags = fcntl(m_socket, F_GETFL, 0);
if (flags < 0 || fcntl(m_socket, F_SETFL, flags | O_NONBLOCK) < 0)
return false;
#endif
const int reuse = 1;
setsockopt(m_socket, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&reuse), sizeof(reuse));
return true;
}
bool LocalLinkAdapter::ConfigureEndpoint()
{
sockaddr_in local{};
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(m_host ? m_port : 0);
if (bind(m_socket, reinterpret_cast<const sockaddr*>(&local), sizeof(local)) != 0)
{
Console.Error("DEV9: Local Link failed to bind UDP port %u", m_host ? m_port : 0);
return false;
}
if (m_host)
return true;
m_host_endpoint.sin_family = AF_INET;
m_host_endpoint.sin_port = htons(m_port);
const std::string& host = EmuConfig.DEV9.LocalLinkAddress;
if (host.empty())
{
Console.Error("DEV9: Local Link join mode needs the host device's address");
return false;
}
// Numeric IPv4 FIRST, and deliberately so: a LAN address or a VPN address (Tailscale's
// 100.x.y.z, ZeroTier, WireGuard) is the overwhelmingly common case, and this path must never
// touch DNS. ConfigureEndpoint runs from the adapter's constructor via GetNetAdapter() while
// the VM is booting on the CPU thread, so a blocking resolve here stalls game start.
if (inet_pton(AF_INET, host.c_str(), &m_host_endpoint.sin_addr) == 1)
return true;
// Not a literal, so treat it as a hostname — a dynamic-DNS name for a port-forwarded host, or
// a VPN's own DNS name. One blocking lookup, once, at boot. There is no portable timeout for
// getaddrinfo (DNS_Server.cpp sidesteps that by resolving on its own thread), so a name that
// does not resolve costs the platform's DNS timeout before we give up — at which point DEV9
// fails safe: GetNetAdapter deletes the adapter and InitNet clears EthEnable.
addrinfo hints{};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
#ifdef AI_ADDRCONFIG
hints.ai_flags = AI_ADDRCONFIG;
#endif
addrinfo* results = nullptr;
if (getaddrinfo(host.c_str(), nullptr, &hints, &results) != 0 || results == nullptr)
{
if (results != nullptr)
freeaddrinfo(results);
Console.Error("DEV9: Local Link could not resolve host '%s' (use the numeric IPv4 address "
"shown on the host device if this keeps failing)",
host.c_str());
return false;
}
m_host_endpoint.sin_addr = reinterpret_cast<const sockaddr_in*>(results->ai_addr)->sin_addr;
freeaddrinfo(results);
char resolved[INET_ADDRSTRLEN] = {};
inet_ntop(AF_INET, &m_host_endpoint.sin_addr, resolved, sizeof(resolved));
Console.WriteLn("DEV9: Local Link resolved host '%s' to %s", host.c_str(), resolved);
return true;
}
bool LocalLinkAdapter::recv(NetPacket* pkt)
{
if (NetAdapter::recv(pkt))
return true;
if (!m_initialized.load(std::memory_order_acquire) || m_closed.load(std::memory_order_acquire))
return false;
SendHelloIfNeeded();
PurgeExpiredState();
for (int i = 0; i < 16; ++i)
{
bool had_datagram = false;
if (ReceiveDatagram(pkt, &had_datagram))
return true;
if (!had_datagram)
break;
}
return false;
}
bool LocalLinkAdapter::send(NetPacket* pkt)
{
if (NetAdapter::send(pkt))
return true;
if (!m_initialized.load(std::memory_order_acquire) || m_closed.load(std::memory_order_acquire) ||
pkt == nullptr || pkt->size <= 0 || pkt->size > 1514)
return false;
InspectSend(pkt);
if (!m_host && m_session_nonce.load(std::memory_order_acquire) == 0)
return true;
if (!m_host)
return SendFrameFragments(*pkt, m_host_endpoint);
bool sent = false;
std::lock_guard lock(m_peer_mutex);
for (const Peer& peer : m_peers)
sent = SendFrameFragments(*pkt, peer.endpoint) || sent;
return sent || m_peers.empty();
}
void LocalLinkAdapter::reloadSettings()
{
// Local Link endpoint, identity and room authentication changes require a
// complete adapter restart. ReconfigureLiveNet() handles that comparison.
}
void LocalLinkAdapter::close()
{
if (m_closed.exchange(true, std::memory_order_acq_rel))
return;
m_initialized.store(false, std::memory_order_release);
std::lock_guard socket_lock(m_socket_mutex);
#ifdef _WIN32
if (m_socket != INVALID_SOCKET)
{
closesocket(m_socket);
m_socket = INVALID_SOCKET;
}
if (m_wsa_started)
{
WSACleanup();
m_wsa_started = false;
}
#else
if (m_socket >= 0)
{
::close(m_socket);
m_socket = -1;
}
#endif
}
void LocalLinkAdapter::SendHelloIfNeeded(bool force)
{
if (m_host || !m_initialized.load(std::memory_order_acquire))
return;
const auto now = std::chrono::steady_clock::now();
if (!force && now - m_last_hello < std::chrono::seconds(1))
return;
m_last_hello = now;
SendControl(MESSAGE_HELLO, m_host_endpoint);
}
void LocalLinkAdapter::SendControl(u8 type, const sockaddr_in& endpoint)
{
SendDatagram(type, m_peer_id, 0, 0, 1, nullptr, 0, endpoint);
}
bool LocalLinkAdapter::SendFrameFragments(const NetPacket& pkt, const sockaddr_in& endpoint)
{
const u32 frame_id = m_frame_id.fetch_add(1, std::memory_order_relaxed);
const u16 count = static_cast<u16>((pkt.size + MAX_FRAGMENT_PAYLOAD - 1) / MAX_FRAGMENT_PAYLOAD);
bool sent = true;
for (u16 index = 0; index < count; ++index)
{
const std::size_t offset = index * MAX_FRAGMENT_PAYLOAD;
const u16 size = static_cast<u16>(std::min<std::size_t>(MAX_FRAGMENT_PAYLOAD, pkt.size - offset));
sent = SendDatagram(MESSAGE_DATA, m_peer_id, frame_id, index, count,
pkt.buffer + offset, size, endpoint) && sent;
}
return sent;
}
bool LocalLinkAdapter::SendDatagram(u8 type, u32 source_peer, u32 frame_id, u16 fragment_index,
u16 fragment_count, const void* payload, u16 payload_size, const sockaddr_in& endpoint)
{
std::array<u8, sizeof(WireHeader) + MAX_FRAGMENT_PAYLOAD> datagram{};
WireHeader header{};
header.magic = htonl(LOCAL_LINK_MAGIC);
header.version = LOCAL_LINK_VERSION;
header.type = type;
header.header_size = htons(sizeof(WireHeader));
header.peer_id = htonl(source_peer);
header.sequence = htonl(m_send_sequence.fetch_add(1, std::memory_order_relaxed));
header.frame_id = htonl(frame_id);
header.fragment_index = htons(fragment_index);
header.fragment_count = htons(fragment_count);
header.payload_size = htons(payload_size);
header.session_nonce = HostToNetwork64(m_session_nonce.load(std::memory_order_acquire));
header.auth_tag = 0;
std::memcpy(datagram.data(), &header, sizeof(header));
if (payload_size != 0)
std::memcpy(datagram.data() + sizeof(header), payload, payload_size);
header.auth_tag = HostToNetwork64(Authenticate(datagram.data(), sizeof(header),
datagram.data() + sizeof(header), payload_size, m_auth_key0, m_auth_key1));
std::memcpy(datagram.data(), &header, sizeof(header));
std::lock_guard socket_lock(m_socket_mutex);
if (m_closed.load(std::memory_order_acquire))
return false;
const int result = sendto(m_socket, reinterpret_cast<const char*>(datagram.data()),
static_cast<int>(sizeof(header) + payload_size), 0,
reinterpret_cast<const sockaddr*>(&endpoint), sizeof(endpoint));
return result == static_cast<int>(sizeof(header) + payload_size);
}
bool LocalLinkAdapter::ReceiveDatagram(NetPacket* pkt, bool* had_datagram)
{
*had_datagram = false;
std::array<u8, sizeof(WireHeader) + MAX_FRAGMENT_PAYLOAD> datagram{};
sockaddr_in source{};
#ifdef _WIN32
int source_size = sizeof(source);
#else
socklen_t source_size = sizeof(source);
#endif
int size;
{
std::lock_guard socket_lock(m_socket_mutex);
if (m_closed.load(std::memory_order_acquire))
return false;
size = recvfrom(m_socket, reinterpret_cast<char*>(datagram.data()),
static_cast<int>(datagram.size()), 0, reinterpret_cast<sockaddr*>(&source), &source_size);
}
if (size >= 0)
*had_datagram = true;
if (size < static_cast<int>(sizeof(WireHeader)))
return false;
WireHeader header{};
std::memcpy(&header, datagram.data(), sizeof(header));
const u64 received_tag = NetworkToHost64(header.auth_tag);
header.auth_tag = 0;
std::memcpy(datagram.data(), &header, sizeof(header));
const u16 header_size = ntohs(header.header_size);
const u16 payload_size = ntohs(header.payload_size);
if (ntohl(header.magic) != LOCAL_LINK_MAGIC || header.version != LOCAL_LINK_VERSION ||
header_size != sizeof(WireHeader) || payload_size > MAX_FRAGMENT_PAYLOAD ||
size != static_cast<int>(sizeof(WireHeader) + payload_size) ||
received_tag != Authenticate(datagram.data(), sizeof(WireHeader),
datagram.data() + sizeof(WireHeader), payload_size, m_auth_key0, m_auth_key1))
{
return false;
}
const u32 source_peer = ntohl(header.peer_id);
const u32 sequence = ntohl(header.sequence);
const u64 session_nonce = NetworkToHost64(header.session_nonce);
if (source_peer == 0 || source_peer == m_peer_id)
return false;
header.auth_tag = HostToNetwork64(received_tag);
std::memcpy(datagram.data(), &header, sizeof(header));
if (header.type == MESSAGE_HELLO)
{
if (m_host && RegisterPeer(source_peer, sequence, source))
SendControl(MESSAGE_HELLO_ACK, source);
return false;
}
if (header.type == MESSAGE_HELLO_ACK)
{
if (!m_host && SameEndpoint(source, m_host_endpoint) && session_nonce != 0)
{
const u64 previous = m_session_nonce.exchange(session_nonce, std::memory_order_acq_rel);
if (previous != 0 && previous != session_nonce)
{
m_remote_peers.clear();
m_reassembly.clear();
}
}
return false;
}
if (header.type != MESSAGE_DATA)
return false;
if (session_nonce == 0 || session_nonce != m_session_nonce.load(std::memory_order_acquire))
return false;
if (m_host)
{
std::lock_guard lock(m_peer_mutex);
Peer* peer = FindPeer(source_peer, source);
if (peer == nullptr || !AcceptSequence(*peer, sequence))
return false;
peer->last_seen = std::chrono::steady_clock::now();
RelayDatagram(datagram.data(), size, source);
}
else if (!SameEndpoint(source, m_host_endpoint))
{
return false;
}
else
{
auto remote_it = m_remote_peers.find(source_peer);
if (remote_it == m_remote_peers.end())
{
if (m_remote_peers.size() >= MAX_PEERS + 1)
return false;
remote_it = m_remote_peers.emplace(source_peer, Peer{}).first;
}
Peer& remote = remote_it->second;
if (remote.id == 0)
{
remote.id = source_peer;
remote.endpoint = source;
}
if (!AcceptSequence(remote, sequence))
return false;
remote.last_seen = std::chrono::steady_clock::now();
}
const u16 fragment_index = ntohs(header.fragment_index);
const u16 fragment_count = ntohs(header.fragment_count);
if (fragment_count == 0 || fragment_count > 2 || fragment_index >= fragment_count)
return false;
const ReassemblyKey key{source_peer, ntohl(header.frame_id)};
auto frame_it = m_reassembly.find(key);
if (frame_it == m_reassembly.end())
{
if (m_reassembly.size() >= 64)
return false;
frame_it = m_reassembly.emplace(key, Reassembly{}).first;
}
Reassembly& frame = frame_it->second;
if (frame.fragment_count == 0)
{
frame.fragment_count = fragment_count;
frame.created = std::chrono::steady_clock::now();
}
if (frame.fragment_count != fragment_count)
{
m_reassembly.erase(key);
return false;
}
const std::size_t offset = fragment_index * MAX_FRAGMENT_PAYLOAD;
if (offset + payload_size > frame.data.size())
{
m_reassembly.erase(key);
return false;
}
std::memcpy(frame.data.data() + offset, datagram.data() + sizeof(WireHeader), payload_size);
frame.sizes[fragment_index] = payload_size;
frame.received[fragment_index] = true;
for (u16 i = 0; i < fragment_count; ++i)
{
if (!frame.received[i])
return false;
}
const int frame_size = (fragment_count - 1) * MAX_FRAGMENT_PAYLOAD + frame.sizes[fragment_count - 1];
std::memcpy(pkt->buffer, frame.data.data(), frame_size);
m_reassembly.erase(key);
if (!VerifyLocalLinkPacket(pkt, frame_size))
return false;
InspectRecv(pkt);
return true;
}
void LocalLinkAdapter::RelayDatagram(const void* data, std::size_t size, const sockaddr_in& source)
{
std::lock_guard socket_lock(m_socket_mutex);
if (m_closed.load(std::memory_order_acquire))
return;
for (const Peer& peer : m_peers)
{
if (!SameEndpoint(peer.endpoint, source))
sendto(m_socket, reinterpret_cast<const char*>(data), static_cast<int>(size), 0,
reinterpret_cast<const sockaddr*>(&peer.endpoint), sizeof(peer.endpoint));
}
}
LocalLinkAdapter::Peer* LocalLinkAdapter::FindPeer(u32 id, const sockaddr_in& endpoint)
{
const auto it = std::find_if(m_peers.begin(), m_peers.end(), [&](const Peer& peer) {
return peer.id == id && SameEndpoint(peer.endpoint, endpoint);
});
return it == m_peers.end() ? nullptr : &*it;
}
bool LocalLinkAdapter::RegisterPeer(u32 id, u32 hello_sequence, const sockaddr_in& endpoint)
{
if (id <= 1)
return false;
std::lock_guard lock(m_peer_mutex);
if (Peer* peer = FindPeer(id, endpoint))
{
if (hello_sequence <= peer->highest_sequence)
{
peer->highest_sequence = 0;
peer->replay_window = 0;
}
peer->last_seen = std::chrono::steady_clock::now();
return true;
}
const auto same_id = std::find_if(m_peers.begin(), m_peers.end(), [&](const Peer& peer) { return peer.id == id; });
if (same_id != m_peers.end())
{
if (same_id->endpoint.sin_addr.s_addr != endpoint.sin_addr.s_addr)
{
Console.Error("DEV9: Local Link rejected duplicate peer id %u", id);
return false;
}
same_id->endpoint = endpoint;
same_id->highest_sequence = 0;
same_id->replay_window = 0;
same_id->last_seen = std::chrono::steady_clock::now();
return true;
}
if (m_peers.size() >= MAX_PEERS)
return false;
m_peers.push_back(Peer{endpoint, id, std::chrono::steady_clock::now(), 0, 0});
Console.WriteLn("DEV9: Local Link peer %u joined", id);
return true;
}
bool LocalLinkAdapter::VerifyLocalLinkPacket(NetPacket* pkt, int read_size)
{
const PacketReader::MAC_Address& destination = *reinterpret_cast<PacketReader::MAC_Address*>(&pkt->buffer[0]);
const PacketReader::MAC_Address& source = *reinterpret_cast<PacketReader::MAC_Address*>(&pkt->buffer[6]);
if (destination != ps2MAC && destination != broadcastMAC && (destination.bytes[0] & 0x01) == 0)
return false;
if (source == ps2MAC)
return false;
pkt->size = read_size;
return true;
}
bool LocalLinkAdapter::AcceptSequence(Peer& peer, u32 sequence)
{
if (sequence > peer.highest_sequence)
{
const u32 shift = sequence - peer.highest_sequence;
peer.replay_window = shift >= 64 ? 1 : ((peer.replay_window << shift) | 1);
peer.highest_sequence = sequence;
return true;
}
const u32 delta = peer.highest_sequence - sequence;
if (delta >= 64 || ((peer.replay_window >> delta) & 1) != 0)
return false;
peer.replay_window |= 1ULL << delta;
return true;
}
void LocalLinkAdapter::PurgeExpiredState()
{
const auto now = std::chrono::steady_clock::now();
for (auto it = m_reassembly.begin(); it != m_reassembly.end();)
{
if (now - it->second.created > std::chrono::seconds(2))
it = m_reassembly.erase(it);
else
++it;
}
if (!m_host)
return;
std::lock_guard lock(m_peer_mutex);
m_peers.erase(std::remove_if(m_peers.begin(), m_peers.end(), [&](const Peer& peer) {
return now - peer.last_seen > std::chrono::seconds(10);
}), m_peers.end());
}
bool LocalLinkAdapter::SameEndpoint(const sockaddr_in& lhs, const sockaddr_in& rhs)
{
return lhs.sin_family == rhs.sin_family && lhs.sin_port == rhs.sin_port &&
lhs.sin_addr.s_addr == rhs.sin_addr.s_addr;
}
u64 LocalLinkAdapter::DeriveKey(const std::string& room_code, u64 salt)
{
u64 hash = 1469598103934665603ULL ^ salt;
for (const unsigned char value : room_code)
{
hash ^= value;
hash *= 1099511628211ULL;
}
return hash;
}
u64 LocalLinkAdapter::Authenticate(const void* header, std::size_t header_size,
const void* payload, std::size_t payload_size, u64 key0, u64 key1)
{
std::array<u8, sizeof(WireHeader) + MAX_FRAGMENT_PAYLOAD> bytes{};
std::memcpy(bytes.data(), header, header_size);
if (payload_size != 0)
std::memcpy(bytes.data() + header_size, payload, payload_size);
return SipHash24(bytes.data(), header_size + payload_size, key0, key1);
}