From 495e88989fae4fda809511b05245acf71fa109aa Mon Sep 17 00:00:00 2001 From: Markwin Date: Wed, 27 May 2026 19:23:02 +0200 Subject: [PATCH 1/7] Fix porttype decoding for NN_ADDRESS_CHECK natneg packet --- code/natneg/server/NNDriver.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/code/natneg/server/NNDriver.cpp b/code/natneg/server/NNDriver.cpp index a9d34f7a..bca9398f 100644 --- a/code/natneg/server/NNDriver.cpp +++ b/code/natneg/server/NNDriver.cpp @@ -121,6 +121,7 @@ namespace NN { break; case NN_ADDRESS_REPLY: case NN_NATIFY_REQUEST: + case NN_ADDRESS_CHECK: case NN_ERTTEST: case NN_INIT: case NN_INITACK: From 4cd40b180da0c5d827f40262c57e3450193e8ee9 Mon Sep 17 00:00:00 2001 From: Markwin Date: Wed, 27 May 2026 19:23:02 +0200 Subject: [PATCH 2/7] Add QR address to serverbrowsing service --- code/serverbrowsing/main.cpp | 19 ++++++++++++++++++- code/serverbrowsing/server/SBServer.cpp | 2 +- code/serverbrowsing/server/SBServer.h | 4 +++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/code/serverbrowsing/main.cpp b/code/serverbrowsing/main.cpp index 8f7bef9c..8b7bd852 100644 --- a/code/serverbrowsing/main.cpp +++ b/code/serverbrowsing/main.cpp @@ -20,12 +20,29 @@ int main() { uv_timer_init(uv_default_loop(), &tick_timer); OS::Init("serverbrowsing"); - g_gameserver = new SBServer(); char address_buff[256]; char port_buff[16]; size_t temp_env_sz = sizeof(address_buff); + OS::Address qr_address; + if(uv_os_getenv("OPENSPY_QR_BIND_ADDR", (char *)&address_buff, &temp_env_sz) != UV_ENOENT) { + temp_env_sz = sizeof(port_buff); + + uint16_t port = 27900; + if(uv_os_getenv("OPENSPY_QR_BIND_PORT", (char *)&port_buff, &temp_env_sz) != UV_ENOENT) { + port = atoi(port_buff); + } + std::string qr_addr = address_buff; + qr_address = OS::Address{qr_addr}; + qr_address.port = htons(port); + + } else { + OS::LogText(OS::ELogLevel_Warning, "Missing QR bind address environment variable"); + } + + g_gameserver = new SBServer(qr_address); + if(uv_os_getenv("OPENSPY_SBV1_BIND_ADDR", (char *)&address_buff, &temp_env_sz) != UV_ENOENT) { temp_env_sz = sizeof(port_buff); diff --git a/code/serverbrowsing/server/SBServer.cpp b/code/serverbrowsing/server/SBServer.cpp index 2894287c..04b4c862 100644 --- a/code/serverbrowsing/server/SBServer.cpp +++ b/code/serverbrowsing/server/SBServer.cpp @@ -2,7 +2,7 @@ #include "SBServer.h" #include "SBDriver.h" #include -SBServer::SBServer() : INetServer() { +SBServer::SBServer(const OS::Address& qr_address) : INetServer(), qr_address{qr_address} { uv_loop_set_data(uv_default_loop(), this); } SBServer::~SBServer() { diff --git a/code/serverbrowsing/server/SBServer.h b/code/serverbrowsing/server/SBServer.h index 3cafba35..cac4bbaf 100644 --- a/code/serverbrowsing/server/SBServer.h +++ b/code/serverbrowsing/server/SBServer.h @@ -6,13 +6,15 @@ class SBServer : public INetServer { public: - SBServer(); + SBServer(const OS::Address& qr_address); ~SBServer(); void tick(); void OnNewServer(MM::Server server); void OnUpdateServer(MM::Server server); void OnDeleteServer(MM::Server server); + + const OS::Address qr_address; private: }; #endif //_SBSERVER_H \ No newline at end of file From b4ee17963d900ab0bef7f3d54c7b871b8408f84d Mon Sep 17 00:00:00 2001 From: Markwin Date: Wed, 27 May 2026 19:23:02 +0200 Subject: [PATCH 3/7] Read instance_key in GetServer* functions --- code/serverbrowsing/tasks/GetServers.cpp | 5 ++++- code/serverbrowsing/tasks/tasks.h | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/code/serverbrowsing/tasks/GetServers.cpp b/code/serverbrowsing/tasks/GetServers.cpp index be2cf300..86ce96be 100644 --- a/code/serverbrowsing/tasks/GetServers.cpp +++ b/code/serverbrowsing/tasks/GetServers.cpp @@ -99,6 +99,7 @@ namespace MM { basic_lookup_keys.push_back("icmp_address"); basic_lookup_keys.push_back("allow_unsolicited_udp"); basic_lookup_keys.push_back("country"); + basic_lookup_keys.push_back("instance_key"); if (req == NULL) { return; @@ -613,7 +614,9 @@ namespace MM { case 7: server->kvFields["country"] = basic_keys_response->element[i]->str; break; - + case 8: + server->instance_key = (uint32_t)strtoul(basic_keys_response->element[i]->str, NULL, 10); + break; } } diff --git a/code/serverbrowsing/tasks/tasks.h b/code/serverbrowsing/tasks/tasks.h index bae98458..3216398b 100644 --- a/code/serverbrowsing/tasks/tasks.h +++ b/code/serverbrowsing/tasks/tasks.h @@ -46,8 +46,8 @@ namespace MM { std::string key; - - int id; + uint32_t instance_key; + int id; bool deleted; }; From dee421ca87a748672a369626d0492054996ef412 Mon Sep 17 00:00:00 2001 From: Markwin Date: Wed, 27 May 2026 19:23:02 +0200 Subject: [PATCH 4/7] Fix routing key for qr/sb client message --- code/qr/tasks/tasks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/qr/tasks/tasks.cpp b/code/qr/tasks/tasks.cpp index 89d24ed4..a721fea0 100644 --- a/code/qr/tasks/tasks.cpp +++ b/code/qr/tasks/tasks.cpp @@ -9,7 +9,7 @@ namespace MM { const char *mm_channel_exchange = "openspy.master"; - const char *mm_client_message_routingkey = "qr.message"; + const char *mm_client_message_routingkey = "client.message"; const char *mm_server_event_routingkey = "server.event"; const char *mm_server_client_acks_routingkey = "client-messages.acks"; From ca2d2c5070acfcc377223bebf457da96af546e24 Mon Sep 17 00:00:00 2001 From: Markwin Date: Wed, 27 May 2026 19:23:02 +0200 Subject: [PATCH 5/7] Fix client message sending in serverbrowsing service --- code/serverbrowsing/tasks/SubmitData.cpp | 54 ++++++++++++++---------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/code/serverbrowsing/tasks/SubmitData.cpp b/code/serverbrowsing/tasks/SubmitData.cpp index 3e407ac0..9a8356c3 100644 --- a/code/serverbrowsing/tasks/SubmitData.cpp +++ b/code/serverbrowsing/tasks/SubmitData.cpp @@ -1,12 +1,17 @@ #include +#include +#include #include "tasks.h" -#include +#include namespace MM { - bool PerformSubmitData(MMQueryRequest request, TaskThreadData *thread_data) { - std::string b64_string; - const char *base64; - std::ostringstream message; - std::string src_ip, dst_ip; + bool PerformSubmitData(MMQueryRequest request, TaskThreadData *thread_data) { + Server *server = GetServerByIP(thread_data, request.to, request.req.m_for_game, false, false); + if(server == NULL){ + OS::LogText(OS::ELogLevel_Warning, "[%s] No server at %s", + request.peer->getAddress().ToString().c_str(), request.to.ToString().c_str()); + return true; + } + #if HACKER_PATCH_MSG_FORCE_NATNEG_ONLY if(request.req.m_for_game.gameid == 1420 || request.req.m_for_game.gameid == 2999) { //only apply patch to flatout2pc / flatout2zp request.buffer.resetReadCursor(); @@ -22,28 +27,31 @@ namespace MM { #endif - base64 = OS::BinToBase64Str((uint8_t *)request.buffer.GetReadCursor(), request.buffer.readRemaining()); - b64_string = base64; - free((void *)base64); - - src_ip = request.from.ToString(true), dst_ip = request.to.ToString(true); - - - - message << "\\send_msg\\REMOVED\\" << src_ip << "\\" << - request.from.GetPort() << "\\" << - dst_ip << "\\" << - request.to.GetPort() << "\\" << - b64_string; - - b64_string = message.str(); - + const char *base64 = OS::BinToBase64Str((uint8_t *)request.buffer.GetReadCursor(), request.buffer.readRemaining()); OS::Address address = request.peer->getAddress(); - TaskShared::sendAMQPMessage(MM::mm_channel_exchange, MM::mm_client_message_routingkey, b64_string.c_str(), &address); + SBServer *sb_server = (SBServer *)uv_loop_get_data(uv_default_loop()); + json_t *json_obj = json_object(); + + json_object_set_new(json_obj, "to_address", json_string(request.to.ToString().c_str())); + json_object_set_new(json_obj, "driver_address", json_string(sb_server->qr_address.ToString().c_str())); + json_object_set_new(json_obj, "hostname", json_string(OS::g_hostName)); + json_object_set_new(json_obj, "version", json_integer(2)); + json_object_set_new(json_obj, "type", json_string("client_message")); + json_object_set_new(json_obj, "message", json_string(base64)); + json_object_set_new(json_obj, "instance_key", json_integer(server->instance_key)); + + char *json_data = json_dumps(json_obj, 0); + TaskShared::sendAMQPMessage(MM::mm_channel_exchange, MM::mm_client_message_routingkey, json_data, &address); + + if (json_data) + free((void *)json_data); + json_decref(json_obj); + free((void *)base64); if(request.peer) { request.peer->DecRef(); } + delete server; return true; } } \ No newline at end of file From 3f6da17c35169267e0b2cd523f5f61044a8129b7 Mon Sep 17 00:00:00 2001 From: Markwin Date: Wed, 27 May 2026 19:23:02 +0200 Subject: [PATCH 6/7] Add missing null terminator in client message --- code/serverbrowsing/tasks/SubmitData.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/code/serverbrowsing/tasks/SubmitData.cpp b/code/serverbrowsing/tasks/SubmitData.cpp index 9a8356c3..ec7759d7 100644 --- a/code/serverbrowsing/tasks/SubmitData.cpp +++ b/code/serverbrowsing/tasks/SubmitData.cpp @@ -12,6 +12,8 @@ namespace MM { return true; } + bool is_natneg_packet = memcmp(request.buffer.GetHead(), "\xFD\xFC\x1E\x66\x6A\xB2", 6) == 0; + #if HACKER_PATCH_MSG_FORCE_NATNEG_ONLY if(request.req.m_for_game.gameid == 1420 || request.req.m_for_game.gameid == 2999) { //only apply patch to flatout2pc / flatout2zp request.buffer.resetReadCursor(); @@ -19,7 +21,7 @@ namespace MM { OS::LogText(OS::ELogLevel_Warning, "[%s] Rejecting non-10 submit data (%d)", request.peer->getAddress().ToString().c_str(), request.buffer.readRemaining()); return true; } - if (memcmp(request.buffer.GetHead(), "\xFD\xFC\x1E\x66\x6A\xB2", 6) != 0) { + if (!is_natneg_packet) { OS::LogText(OS::ELogLevel_Warning, "[%s] Rejecting non-natneg submit data", request.peer->getAddress().ToString().c_str()); return true; } @@ -27,7 +29,22 @@ namespace MM { #endif - const char *base64 = OS::BinToBase64Str((uint8_t *)request.buffer.GetReadCursor(), request.buffer.readRemaining()); + auto data_buff = (uint8_t *)request.buffer.GetReadCursor(); + auto buffer_size = request.buffer.readRemaining(); + const char *base64; + if(!is_natneg_packet && buffer_size > 0 && data_buff[buffer_size-1] != '\0'){ + // missing null terminator in non-natneg packet + // GameSpy SDK is expecting null terminated string here when in unicode mode + uint8_t *data_buff2 = new uint8_t[buffer_size + 1]; + memcpy(data_buff2, data_buff, buffer_size); + data_buff2[buffer_size] = '\0'; + base64 = OS::BinToBase64Str(data_buff2, buffer_size + 1); + delete[] data_buff2; + }else{ + base64 = OS::BinToBase64Str(data_buff, buffer_size); + } + + OS::Address address = request.peer->getAddress(); SBServer *sb_server = (SBServer *)uv_loop_get_data(uv_default_loop()); json_t *json_obj = json_object(); From c15f607d037394193fcd61095ab04423e0c54b46 Mon Sep 17 00:00:00 2001 From: Markwin Date: Wed, 27 May 2026 19:23:02 +0200 Subject: [PATCH 7/7] Add ClientMessageResender for QR service --- code/qr/server/ClientMessageResender.cpp | 143 +++++++++++++++++++++++ code/qr/server/ClientMessageResender.h | 109 +++++++++++++++++ code/qr/server/QRServer.h | 4 +- code/qr/tasks/ClientMessageAck.cpp | 5 +- code/qr/tasks/Handle_QRMessage.cpp | 8 +- 5 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 code/qr/server/ClientMessageResender.cpp create mode 100644 code/qr/server/ClientMessageResender.h diff --git a/code/qr/server/ClientMessageResender.cpp b/code/qr/server/ClientMessageResender.cpp new file mode 100644 index 00000000..4ee4658e --- /dev/null +++ b/code/qr/server/ClientMessageResender.cpp @@ -0,0 +1,143 @@ +#include "ClientMessageResender.h" + + +#include +#include "QRDriver.h" + + +constexpr auto RESEND_INTERVAL_MS = 750; +constexpr auto RESEND_COUNT = 8; + + +namespace QR { + + ClientMessageResender::Task::~Task() { + uv_close((uv_handle_t *)timer_handle, +[](uv_handle_t *handle){ + delete (uv_timer_t *)handle; + }); + } + + ClientMessageResender::ClientMessageResender() : current_message_key{0} { + uv_mutex_init(&m_queue_mutex); + m_async_send_handle = new uv_async_t; + uv_async_init(uv_default_loop(), m_async_send_handle, +[](uv_async_t *handle){ + auto& thiz = *(ClientMessageResender *)uv_handle_get_data((uv_handle_t *)handle); + thiz.async_send_callback(); + }); + uv_handle_set_data((uv_handle_t *)m_async_send_handle, this); + } + + ClientMessageResender::~ClientMessageResender(){ + uv_close((uv_handle_t*)m_async_send_handle, +[](uv_handle_t *handle){ + delete (uv_async_t *)handle; + }); + uv_mutex_destroy(&m_queue_mutex); + } + + + void ClientMessageResender::put_message( + uint32_t instance_key, + OS::Buffer& message_buffer, + const OS::Address& to_address, + QR::Driver *driver + ){ + std::shared_ptr task = std::make_shared( + true, + to_address, + instance_key, + message_buffer, + driver, + this, + RESEND_COUNT + ); + + uv_mutex_lock(&m_queue_mutex); + m_task_queue.push(task); + uv_mutex_unlock(&m_queue_mutex); + uv_async_send(m_async_send_handle); + } + + void ClientMessageResender::remove_message(uint32_t message_key, const OS::Address *from_address){ + std::shared_ptr task = std::make_shared( + false, + message_key, + *from_address + ); + + uv_mutex_lock(&m_queue_mutex); + m_task_queue.push(task); + uv_mutex_unlock(&m_queue_mutex); + uv_async_send(m_async_send_handle); + } + + + // process queued add/remove tasks + void ClientMessageResender::async_send_callback(){ + uv_mutex_lock(&m_queue_mutex); + while(!m_task_queue.empty()){ + std::shared_ptr task = m_task_queue.front(); + m_task_queue.pop(); + uv_mutex_unlock(&m_queue_mutex); + + // cancel and remove existing task + auto it = m_active_tasks.find(task); + if(it != m_active_tasks.end()){ + m_active_tasks.erase(it); + } + + // add new task + if(task->add_task){ + task->message_key = current_message_key++; + + const std::shared_ptr& task2 = *m_active_tasks.emplace(task).first; + Task *task3 = (Task *)task2.get(); + task3->timer_handle = new uv_timer_t; + uv_timer_init(uv_default_loop(), task3->timer_handle); + uv_handle_set_data((uv_handle_t *)task3->timer_handle, (void *)&task2); + uv_timer_start( + task3->timer_handle, + +[](uv_timer_t *handle) { + auto& task = *(std::shared_ptr *)uv_handle_get_data((uv_handle_t *)handle); + Task *task2 = (Task *)task.get(); + task2->resender->timer_handler(&task); + }, + 0, + RESEND_INTERVAL_MS + ); + } + uv_mutex_lock(&m_queue_mutex); + } + uv_mutex_unlock(&m_queue_mutex); + } + + void ClientMessageResender::timer_handler(std::shared_ptr *task){ + WorkQueueData *work_data = new WorkQueueData{*task}; + work_data->uv_req.data = work_data; + + Task *task2 = (Task *)task->get(); + + uv_queue_work( + uv_default_loop(), + &work_data->uv_req, + +[](uv_work_t *req) { + WorkQueueData *work_data = (WorkQueueData *)req->data; + Task *task = (Task *)work_data->task.get(); + task->driver->send_client_message(2, task->address, task->instance_key, task->message_key, task->message_buffer); + }, + +[](uv_work_t *req, int status) { + WorkQueueData *work_data = (WorkQueueData *)req->data; + delete work_data; + } + ); + + + if(--task2->retries_left == 0){ + auto it = m_active_tasks.find(*task); + if(it != m_active_tasks.end()) { + m_active_tasks.erase(it); + } + } + } + + +} diff --git a/code/qr/server/ClientMessageResender.h b/code/qr/server/ClientMessageResender.h new file mode 100644 index 00000000..3ca1ec69 --- /dev/null +++ b/code/qr/server/ClientMessageResender.h @@ -0,0 +1,109 @@ +#ifndef _CLIENTMESSAGERESENDER_H +#define _CLIENTMESSAGERESENDER_H + + +#include +#include +#include +#include +#include +#include + + +namespace QR { + class Driver; + + class ClientMessageResender { + public: + ClientMessageResender(); + ~ClientMessageResender(); + void put_message( + uint32_t instance_key, + OS::Buffer& message_buffer, + const OS::Address& to_address, + QR::Driver *driver + ); + void remove_message(uint32_t message_key, const OS::Address *from_address); + private: + + struct TaskBasicData { + inline TaskBasicData( + bool add_task, + uint32_t message_key, + const OS::Address& address + ) : add_task{add_task}, + message_key{message_key}, + address{address} {} + inline TaskBasicData( + bool add_task, + const OS::Address& address + ) : add_task{add_task}, + address{address} {} + virtual ~TaskBasicData() = default; + + bool add_task; + uint32_t message_key; + OS::Address address; + }; + + struct Task : TaskBasicData { + inline Task( + bool add_task, + const OS::Address& address, + uint32_t instance_key, + OS::Buffer& message_buffer, + QR::Driver *driver, + ClientMessageResender *resender, + uint32_t retries_left + ) : TaskBasicData{add_task, address}, + instance_key{instance_key}, + message_buffer{message_buffer}, + driver{driver}, + resender{resender}, + retries_left{retries_left} {} + ~Task() override; + + uint32_t instance_key; + OS::Buffer message_buffer; + QR::Driver *driver; + ClientMessageResender *resender; + uint32_t retries_left; + uv_timer_t *timer_handle; + }; + + struct WorkQueueData { + std::shared_ptr task; + uv_work_t uv_req; + }; + + struct TaskSharedPtrHash { + inline size_t operator()(const std::shared_ptr& t) const noexcept { + auto ip_hash = std::hash()(t->address.ip); + auto port_hash = std::hash()(t->address.port); + auto message_key_hash = std::hash()(t->message_key); + auto final_hash = ip_hash ^ (port_hash << 1); + final_hash = final_hash ^ (message_key_hash << 1); + return final_hash; + } + }; + + struct TaskSharedPtrEqual { + bool operator()(const std::shared_ptr& lhs, const std::shared_ptr& rhs) const noexcept { + return lhs->address == rhs->address && lhs->message_key == rhs->message_key; + } + }; + + void async_send_callback(); + uv_async_t *m_async_send_handle; + uv_mutex_t m_queue_mutex; + std::queue> m_task_queue; + + // should be std::shared_ptr, but in C++11 there is no support + // for heterogenous .find() in std::unordered_set + std::unordered_set, TaskSharedPtrHash, TaskSharedPtrEqual> m_active_tasks; + uint32_t current_message_key; + void timer_handler(std::shared_ptr *task); + }; +} + +#endif //_CLIENTMESSAGERESENDER_H \ No newline at end of file diff --git a/code/qr/server/QRServer.h b/code/qr/server/QRServer.h index dea7dc22..afc8b12f 100644 --- a/code/qr/server/QRServer.h +++ b/code/qr/server/QRServer.h @@ -2,7 +2,7 @@ #define _QRSERVER_H #include #include - +#include "ClientMessageResender.h" #include #define MASTER_PORT 27900 namespace MM { @@ -17,6 +17,8 @@ namespace QR { ~Server(); void tick(); Driver *findDriverByAddress(OS::Address address); + + ClientMessageResender client_message_resender; private: }; } diff --git a/code/qr/tasks/ClientMessageAck.cpp b/code/qr/tasks/ClientMessageAck.cpp index ca7eaa1f..28846138 100644 --- a/code/qr/tasks/ClientMessageAck.cpp +++ b/code/qr/tasks/ClientMessageAck.cpp @@ -3,7 +3,7 @@ #include "../server/v2.h" #include #include - +#include #include namespace MM { @@ -38,6 +38,9 @@ namespace MM { if(request.callback) request.callback(response); + QR::Server *server = (QR::Server *)uv_loop_get_data(uv_default_loop()); + server->client_message_resender.remove_message(request.server.id, &request.from_address); + return true; } diff --git a/code/qr/tasks/Handle_QRMessage.cpp b/code/qr/tasks/Handle_QRMessage.cpp index 6a1f10a9..bd323b74 100644 --- a/code/qr/tasks/Handle_QRMessage.cpp +++ b/code/qr/tasks/Handle_QRMessage.cpp @@ -36,12 +36,6 @@ namespace MM { instance_key = json_integer_value(instance_key_json); } - int message_key = 0; - json_t *message_key_json = json_object_get(root, "identifier"); - if(message_key_json && json_is_integer(message_key_json)) { - message_key = json_integer_value(message_key_json); - } - OS::Buffer message_buffer; uint8_t *data_out; size_t data_len; @@ -72,7 +66,7 @@ namespace MM { const char *type_str = json_string_value(type); if(stricmp(type_str, "client_message") == 0) { if(version == 2) { - driver->send_client_message(version, to_address, instance_key, message_key, message_buffer); + server->client_message_resender.put_message(instance_key, message_buffer, to_address, driver); } } }