mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 11:26:56 +02:00
net: translate native UDP socket options and microsecond timeouts
This commit is contained in:
+84
-12
@@ -992,6 +992,7 @@ static int ConvertHostSocketError(int error) {
|
|||||||
case WSAENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
case WSAENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||||
case WSAENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
case WSAENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||||
case WSAENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
case WSAENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||||
|
case WSAENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
|
||||||
case WSAEOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
case WSAEOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||||
case WSAEPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
case WSAEPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||||
case WSAESHUTDOWN: posix_error = Posix::POSIX_ESHUTDOWN; break;
|
case WSAESHUTDOWN: posix_error = Posix::POSIX_ESHUTDOWN; break;
|
||||||
@@ -1028,6 +1029,7 @@ static int ConvertHostSocketError(int error) {
|
|||||||
case ENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
case ENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||||
case ENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
case ENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||||
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||||
|
case ENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
|
||||||
case EOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
case EOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||||
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
|
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
|
||||||
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||||
@@ -1066,6 +1068,26 @@ static int ConvertSocketOptionLevel(int level) {
|
|||||||
return (level == 0xffff ? SOL_SOCKET : level);
|
return (level == 0xffff ? SOL_SOCKET : level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int ConvertSocketOptionName(int level, int option) {
|
||||||
|
if (level == 0xffff) {
|
||||||
|
switch (option) {
|
||||||
|
case 0x0020: return SO_BROADCAST;
|
||||||
|
case 0x1001: return SO_SNDBUF;
|
||||||
|
case 0x1002: return SO_RCVBUF;
|
||||||
|
case 0x1007: return SO_ERROR;
|
||||||
|
case 0x1105: return SO_SNDTIMEO;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
} else if (level == 6 && option == 1) {
|
||||||
|
return TCP_NODELAY;
|
||||||
|
}
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return option;
|
||||||
|
#else
|
||||||
|
return -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
|
static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
|
||||||
switch (option) {
|
switch (option) {
|
||||||
case 0x0020: return &endpoint.broadcast;
|
case 0x0020: return &endpoint.broadcast;
|
||||||
@@ -2149,21 +2171,41 @@ int KYTY_SYSV_ABI Getsockopt(int s, int level, int optname, void* optval, uint32
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guest socket options: SOL_SOCKET=0xffff, SO_ERROR=0x1007.
|
|
||||||
const bool socket_error = (level == 0xffff && optname == 0x1007);
|
const bool socket_error = (level == 0xffff && optname == 0x1007);
|
||||||
#if !defined(_WIN32)
|
const bool send_timeout = (level == 0xffff && optname == 0x1105);
|
||||||
if (!socket_error) {
|
optname = ConvertSocketOptionName(level, optname);
|
||||||
|
if (optname < 0) {
|
||||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||||
}
|
}
|
||||||
|
#if defined(_WIN32)
|
||||||
|
DWORD timeout {};
|
||||||
|
#else
|
||||||
|
timeval timeout {};
|
||||||
#endif
|
#endif
|
||||||
if (socket_error) {
|
|
||||||
optname = SO_ERROR;
|
|
||||||
}
|
|
||||||
SocketLength len = static_cast<SocketLength>(*optlen);
|
SocketLength len = static_cast<SocketLength>(*optlen);
|
||||||
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(optval),
|
void* value = optval;
|
||||||
|
if (send_timeout) {
|
||||||
|
if (*optlen < sizeof(int)) {
|
||||||
|
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||||
|
}
|
||||||
|
value = &timeout;
|
||||||
|
len = sizeof(timeout);
|
||||||
|
}
|
||||||
|
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(value),
|
||||||
&len) != 0) {
|
&len) != 0) {
|
||||||
return SetHostSocketError();
|
return SetHostSocketError();
|
||||||
}
|
}
|
||||||
|
if (send_timeout) {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const int64_t usec = static_cast<int64_t>(timeout) * 1000;
|
||||||
|
#else
|
||||||
|
const int64_t usec = static_cast<int64_t>(timeout.tv_sec) * 1'000'000 + timeout.tv_usec;
|
||||||
|
#endif
|
||||||
|
const int guest_timeout =
|
||||||
|
static_cast<int>(std::min<int64_t>(usec, std::numeric_limits<int>::max()));
|
||||||
|
std::memcpy(optval, &guest_timeout, sizeof(guest_timeout));
|
||||||
|
len = sizeof(guest_timeout);
|
||||||
|
}
|
||||||
if (socket_error && len >= static_cast<SocketLength>(sizeof(int))) {
|
if (socket_error && len >= static_cast<SocketLength>(sizeof(int))) {
|
||||||
auto* error = static_cast<int*>(optval);
|
auto* error = static_cast<int*>(optval);
|
||||||
*error = ConvertHostSocketError(*error);
|
*error = ConvertHostSocketError(*error);
|
||||||
@@ -2215,6 +2257,18 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
if (level == 0xffff && optname == 0x10000) {
|
||||||
|
if (optlen != sizeof(int)) {
|
||||||
|
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||||
|
}
|
||||||
|
int enabled = 0;
|
||||||
|
std::memcpy(&enabled, optval, sizeof(enabled));
|
||||||
|
// Native sends preserve 255.255.255.255 without changing broadcast permission.
|
||||||
|
return enabled != 0 ? 0 : SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
constexpr int ORBIS_SO_NBIO = 0x1200;
|
constexpr int ORBIS_SO_NBIO = 0x1200;
|
||||||
if (ConvertSocketOptionLevel(level) == SOL_SOCKET && optname == ORBIS_SO_NBIO &&
|
if (ConvertSocketOptionLevel(level) == SOL_SOCKET && optname == ORBIS_SO_NBIO &&
|
||||||
optlen >= sizeof(int)) {
|
optlen >= sizeof(int)) {
|
||||||
@@ -2230,14 +2284,32 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
|||||||
#endif
|
#endif
|
||||||
return failed ? SetHostSocketError() : 0;
|
return failed ? SetHostSocketError() : 0;
|
||||||
}
|
}
|
||||||
#if !defined(_WIN32)
|
const bool send_timeout = (level == 0xffff && optname == 0x1105);
|
||||||
// Guest TCP options: IPPROTO_TCP=6, TCP_NODELAY=1.
|
optname = ConvertSocketOptionName(level, optname);
|
||||||
if (level != 6 || optname != 1) {
|
if (optname < 0) {
|
||||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||||
}
|
}
|
||||||
level = IPPROTO_TCP;
|
#if defined(_WIN32)
|
||||||
optname = TCP_NODELAY;
|
DWORD timeout {};
|
||||||
|
#else
|
||||||
|
timeval timeout {};
|
||||||
#endif
|
#endif
|
||||||
|
if (send_timeout) {
|
||||||
|
if (optlen != sizeof(int)) {
|
||||||
|
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||||
|
}
|
||||||
|
int usec = 0;
|
||||||
|
std::memcpy(&usec, optval, sizeof(usec));
|
||||||
|
usec = std::max(usec, 0);
|
||||||
|
#if defined(_WIN32)
|
||||||
|
timeout = static_cast<DWORD>((static_cast<int64_t>(usec) + 999) / 1000);
|
||||||
|
#else
|
||||||
|
timeout.tv_sec = usec / 1'000'000;
|
||||||
|
timeout.tv_usec = usec % 1'000'000;
|
||||||
|
#endif
|
||||||
|
optval = &timeout;
|
||||||
|
optlen = sizeof(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
if (::setsockopt(socket, ConvertSocketOptionLevel(level), optname,
|
if (::setsockopt(socket, ConvertSocketOptionLevel(level), optname,
|
||||||
static_cast<const char*>(optval), static_cast<SocketLength>(optlen)) != 0) {
|
static_cast<const char*>(optval), static_cast<SocketLength>(optlen)) != 0) {
|
||||||
|
|||||||
@@ -34,6 +34,11 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Libs::LibKernelApr {
|
namespace Libs::LibKernelApr {
|
||||||
void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
|
void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
|
||||||
}
|
}
|
||||||
@@ -1065,6 +1070,8 @@ void CheckSocketWakeup() {
|
|||||||
{"OXXX4mUk3uk", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
{"OXXX4mUk3uk", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||||
const auto *getsockopt_symbol = symbols.Find(
|
const auto *getsockopt_symbol = symbols.Find(
|
||||||
{"xphrZusl78E", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
{"xphrZusl78E", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||||
|
const auto *setsockopt_symbol = symbols.Find(
|
||||||
|
{"2mKX2Spso7I", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||||
const auto *send_symbol = symbols.Find(
|
const auto *send_symbol = symbols.Find(
|
||||||
{"beRjXBn-z+o", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
{"beRjXBn-z+o", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||||
const auto *sendto_symbol = symbols.Find(
|
const auto *sendto_symbol = symbols.Find(
|
||||||
@@ -1075,11 +1082,12 @@ void CheckSocketWakeup() {
|
|||||||
{"304ooNZxWDY", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
{"304ooNZxWDY", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||||
const auto *errno_symbol = symbols.Find(
|
const auto *errno_symbol = symbols.Find(
|
||||||
{"HQOwnfMGipQ", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
{"HQOwnfMGipQ", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||||
Check(connect_symbol && getsockopt_symbol && send_symbol && sendto_symbol &&
|
Check(connect_symbol && getsockopt_symbol && setsockopt_symbol && send_symbol && sendto_symbol &&
|
||||||
recv_symbol && recvfrom_symbol && errno_symbol,
|
recv_symbol && recvfrom_symbol && errno_symbol,
|
||||||
"Net socket and errno exports resolve with the guest ABI versions");
|
"Net socket and errno exports resolve with the guest ABI versions");
|
||||||
using Connect = int (KYTY_SYSV_ABI *)(int, const void *, uint32_t);
|
using Connect = int (KYTY_SYSV_ABI *)(int, const void *, uint32_t);
|
||||||
using Getsockopt = int (KYTY_SYSV_ABI *)(int, int, int, void *, uint32_t *);
|
using Getsockopt = int (KYTY_SYSV_ABI *)(int, int, int, void *, uint32_t *);
|
||||||
|
using Setsockopt = int (KYTY_SYSV_ABI *)(int, int, int, const void *, uint32_t);
|
||||||
using Send = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int);
|
using Send = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int);
|
||||||
using Sendto = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int, const void *, uint32_t);
|
using Sendto = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int, const void *, uint32_t);
|
||||||
using Recv = int (KYTY_SYSV_ABI *)(int, void *, size_t, int);
|
using Recv = int (KYTY_SYSV_ABI *)(int, void *, size_t, int);
|
||||||
@@ -1087,6 +1095,7 @@ void CheckSocketWakeup() {
|
|||||||
using Errno = int *(KYTY_SYSV_ABI *)();
|
using Errno = int *(KYTY_SYSV_ABI *)();
|
||||||
const auto net_connect = reinterpret_cast<Connect>(connect_symbol->vaddr);
|
const auto net_connect = reinterpret_cast<Connect>(connect_symbol->vaddr);
|
||||||
const auto net_getsockopt = reinterpret_cast<Getsockopt>(getsockopt_symbol->vaddr);
|
const auto net_getsockopt = reinterpret_cast<Getsockopt>(getsockopt_symbol->vaddr);
|
||||||
|
const auto net_setsockopt = reinterpret_cast<Setsockopt>(setsockopt_symbol->vaddr);
|
||||||
const auto net_send = reinterpret_cast<Send>(send_symbol->vaddr);
|
const auto net_send = reinterpret_cast<Send>(send_symbol->vaddr);
|
||||||
const auto net_sendto = reinterpret_cast<Sendto>(sendto_symbol->vaddr);
|
const auto net_sendto = reinterpret_cast<Sendto>(sendto_symbol->vaddr);
|
||||||
const auto net_recv = reinterpret_cast<Recv>(recv_symbol->vaddr);
|
const auto net_recv = reinterpret_cast<Recv>(recv_symbol->vaddr);
|
||||||
@@ -1157,6 +1166,86 @@ void CheckSocketWakeup() {
|
|||||||
Net::Getsockname(datagram_writer, expected_peer.data(), &expected_peer_size) == 0 &&
|
Net::Getsockname(datagram_writer, expected_peer.data(), &expected_peer_size) == 0 &&
|
||||||
Net::Setsockopt(datagram, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0,
|
Net::Setsockopt(datagram, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0,
|
||||||
"create nonblocking loopback datagrams for Net ABI verification");
|
"create nonblocking loopback datagrams for Net ABI verification");
|
||||||
|
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||||
|
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
|
||||||
|
for (const int option : {0x1001, 0x1002}) {
|
||||||
|
constexpr int requested = 16384;
|
||||||
|
int actual = 0;
|
||||||
|
uint32_t size = sizeof(actual);
|
||||||
|
Check(net_setsockopt(datagram, 0xffff, option, &requested, sizeof(requested)) == 0 &&
|
||||||
|
net_getsockopt(datagram, 0xffff, option, &actual, &size) == 0 &&
|
||||||
|
actual >= requested && size == sizeof(actual),
|
||||||
|
"Net UDP send and receive buffers accept guest option numbers");
|
||||||
|
}
|
||||||
|
for (const int value : {1, 0}) {
|
||||||
|
int actual = -1;
|
||||||
|
uint32_t size = sizeof(actual);
|
||||||
|
Check(net_setsockopt(datagram, 0xffff, 0x20, &value, sizeof(value)) == 0 &&
|
||||||
|
net_getsockopt(datagram, 0xffff, 0x20, &actual, &size) == 0 &&
|
||||||
|
actual == value && size == sizeof(actual),
|
||||||
|
"Net UDP broadcast option can be enabled and disabled");
|
||||||
|
}
|
||||||
|
int timeout = 0;
|
||||||
|
int *timeout_value = &timeout;
|
||||||
|
#if defined(__linux__)
|
||||||
|
int broadcast = -1;
|
||||||
|
uint32_t broadcast_size = sizeof(broadcast);
|
||||||
|
Check(net_setsockopt(datagram, 0xffff, 0x10000, &enabled, sizeof(enabled)) == 0 &&
|
||||||
|
net_getsockopt(datagram, 0xffff, 0x20, &broadcast, &broadcast_size) == 0 &&
|
||||||
|
broadcast == 0,
|
||||||
|
"preserving the all-ones destination does not enable broadcast permission");
|
||||||
|
const long page_size = sysconf(_SC_PAGESIZE);
|
||||||
|
Check(page_size > 0, "get host page size for socket timeout boundary");
|
||||||
|
void *timeout_pages = mmap(nullptr, page_size * 2, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||||
|
Check(timeout_pages != MAP_FAILED &&
|
||||||
|
mprotect(static_cast<char *>(timeout_pages) + page_size, page_size,
|
||||||
|
PROT_NONE) == 0,
|
||||||
|
"guard memory after the four-byte socket timeout");
|
||||||
|
timeout_value = reinterpret_cast<int *>(static_cast<char *>(timeout_pages) +
|
||||||
|
page_size - sizeof(int));
|
||||||
|
#endif
|
||||||
|
for (const int value : {1500000, 0, -1}) {
|
||||||
|
*timeout_value = value;
|
||||||
|
Check(net_setsockopt(datagram, 0xffff, 0x1105, timeout_value, sizeof(int)) == 0,
|
||||||
|
"Net send timeout reads a four-byte microsecond value");
|
||||||
|
*timeout_value = -2;
|
||||||
|
uint32_t size = sizeof(int);
|
||||||
|
Check(net_getsockopt(datagram, 0xffff, 0x1105, timeout_value, &size) == 0 &&
|
||||||
|
*timeout_value == std::max(value, 0) && size == sizeof(int),
|
||||||
|
"Net send timeout returns four-byte microseconds and disables nonpositive values");
|
||||||
|
}
|
||||||
|
#if defined(__linux__)
|
||||||
|
Check(munmap(timeout_pages, page_size * 2) == 0, "free socket timeout guard pages");
|
||||||
|
#endif
|
||||||
|
Check(*net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||||
|
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
||||||
|
"successful Net socket option calls preserve both guest errno values");
|
||||||
|
Check(net_setsockopt(datagram, 0xffff, 0x1105, nullptr, sizeof(timeout)) ==
|
||||||
|
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT &&
|
||||||
|
net_getsockopt(datagram, 0xffff, 0x1105, &timeout, nullptr) ==
|
||||||
|
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT,
|
||||||
|
"Net socket options reject null value and length pointers");
|
||||||
|
uint32_t short_size = sizeof(timeout) - 1;
|
||||||
|
Check(net_setsockopt(datagram, 0xffff, 0x1105, &timeout, short_size) ==
|
||||||
|
Libs::Network::NET_ERROR_EINVAL && *net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||||
|
net_getsockopt(datagram, 0xffff, 0x1105, &timeout, &short_size) ==
|
||||||
|
Libs::Network::NET_ERROR_EINVAL && *net_errno == Libs::Posix::POSIX_EINVAL,
|
||||||
|
"Net send timeout rejects undersized values");
|
||||||
|
uint32_t option_size = sizeof(timeout);
|
||||||
|
Check(net_setsockopt(datagram, 0xffff, 0x7fffffff, &timeout, option_size) ==
|
||||||
|
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||||
|
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT &&
|
||||||
|
net_getsockopt(datagram, 0xffff, 0x7fffffff, &timeout, &option_size) ==
|
||||||
|
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||||
|
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT,
|
||||||
|
"Net unknown socket options return protocol-option errors");
|
||||||
|
#if defined(__linux__)
|
||||||
|
Check(net_setsockopt(datagram, 0xffff, 0x1007, &timeout, option_size) ==
|
||||||
|
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||||
|
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT,
|
||||||
|
"Net native protocol-option errors retain their guest error code");
|
||||||
|
#endif
|
||||||
received.fill(0);
|
received.fill(0);
|
||||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||||
Check(net_sendto(datagram_writer, payload, sizeof(payload), 0,
|
Check(net_sendto(datagram_writer, payload, sizeof(payload), 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user