// NLPacketCodec.cpp #include "NLPacketCodec.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace Overte { namespace { // Control bit masks for sequence number field constexpr uint32_t CONTROL_BIT_MASK = 0x80000000; // Bit 31 constexpr uint32_t RELIABLE_BIT_MASK = 0x40000000; // Bit 30 constexpr uint32_t MESSAGE_BIT_MASK = 0x20000000; // Bit 29 constexpr uint32_t OBFUSCATION_MASK = 0x18000000; // Bits 27-28 constexpr uint32_t SEQUENCE_NUMBER_MASK = 0x07FFFFFF; // Bits 0-26 } // anonymous namespace NLPacket::NLPacket(PacketType type, PacketVersion version, bool isReliable) : m_type(type) , m_version(version) , m_isReliable(isReliable) { // Determine header size (sourced packets have LocalID) m_isSourced = false; // Most client packets aren't sourced m_headerSize = m_isSourced ? SOURCED_HEADER_SIZE : BASE_HEADER_SIZE; // Reserve space for header m_data.resize(m_headerSize); writeHeader(); } void NLPacket::writeHeader() { size_t offset = 0; // Write sequence number and flags uint32_t seqAndFlags = m_sequenceNumber & SEQUENCE_NUMBER_MASK; if (m_isReliable) { seqAndFlags |= RELIABLE_BIT_MASK; } // Convert to network byte order uint32_t netSeqAndFlags = htonl(seqAndFlags); std::memcpy(m_data.data() + offset, &netSeqAndFlags, sizeof(uint32_t)); offset += sizeof(uint32_t); // Write packet type m_data[offset++] = static_cast(m_type); // Write version m_data[offset++] = m_version; // Write source ID if sourced if (m_isSourced) { // Source ID is written in LITTLE-ENDIAN (host byte order on x86) // Don't use htons() - the server expects it in little-endian! std::memcpy(m_data.data() + offset, &m_sourceID, sizeof(uint16_t)); offset += sizeof(uint16_t); } } void NLPacket::write(const void* data, size_t size) { const uint8_t* bytes = static_cast(data); m_data.insert(m_data.end(), bytes, bytes + size); } void NLPacket::writeUInt8(uint8_t value) { m_data.push_back(value); } void NLPacket::writeUInt16(uint16_t value) { uint16_t netValue = htons(value); write(&netValue, sizeof(netValue)); } void NLPacket::writeUInt32(uint32_t value) { uint32_t netValue = htonl(value); write(&netValue, sizeof(netValue)); } void NLPacket::writeUInt64(uint64_t value) { // Network byte order for 64-bit (big-endian) uint64_t netValue = ((value & 0xFF00000000000000ULL) >> 56) | ((value & 0x00FF000000000000ULL) >> 40) | ((value & 0x0000FF0000000000ULL) >> 24) | ((value & 0x000000FF00000000ULL) >> 8) | ((value & 0x00000000FF000000ULL) << 8) | ((value & 0x0000000000FF0000ULL) << 24) | ((value & 0x000000000000FF00ULL) << 40) | ((value & 0x00000000000000FFULL) << 56); write(&netValue, sizeof(netValue)); } void NLPacket::writeString(const std::string& str, bool nullTerminated) { write(str.data(), str.size()); if (nullTerminated) { writeUInt8(0); } } void NLPacket::setSequenceNumber(SequenceNumber seq) { m_sequenceNumber = seq & SEQUENCE_NUMBER_MASK; writeHeader(); } void NLPacket::setSourceID(LocalID id) { m_sourceID = id; m_isSourced = true; // Resize if needed to sourced header (no hash - we don't have HMAC auth) if (m_headerSize != SOURCED_HEADER_SIZE) { m_headerSize = SOURCED_HEADER_SIZE; m_data.resize(m_headerSize); } writeHeader(); } void NLPacket::writeVerificationHash(const uint8_t* connectionSecretUUID) { // HMAC-MD5 verification hash goes right after source ID // Packet structure for verified sourced packet: // [seq+flags(4)] [type(1)] [version(1)] [sourceID(2)] [hash(16)] [payload...] if (!m_isSourced) { std::cerr << "[NLPacket] Warning: Cannot write verification hash for non-sourced packet" << std::endl; return; } const size_t HASH_SIZE = 16; // MD5 produces 16 bytes const size_t HASH_OFFSET = SOURCED_HEADER_SIZE; // Hash goes right after source ID (offset 8) // The current packet structure is: [header(8)] [payload...] // We need to insert 16 bytes for the hash between header and payload // New structure will be: [header(8)] [hash(16)] [payload...] // Get the current payload (everything after the header) std::vector currentPayload(m_data.begin() + m_headerSize, m_data.end()); // Resize packet to make room for hash m_data.resize(m_headerSize + HASH_SIZE + currentPayload.size()); // Move payload to after the hash slot if (!currentPayload.empty()) { std::memcpy(m_data.data() + HASH_OFFSET + HASH_SIZE, currentPayload.data(), currentPayload.size()); } // Calculate HMAC-MD5 hash over the payload data unsigned char hash[HASH_SIZE]; unsigned int hashLen = HASH_SIZE; // Use HMAC_MD5 with connection secret UUID as key HMAC(EVP_md5(), connectionSecretUUID, 16, currentPayload.data(), currentPayload.size(), hash, &hashLen); // Write hash into the reserved slot std::memcpy(m_data.data() + HASH_OFFSET, hash, HASH_SIZE); } bool NLPacket::parseHeader(const uint8_t* data, size_t size, Header& header) { if (size < BASE_HEADER_SIZE) { return false; } size_t offset = 0; // Read sequence and flags uint32_t netSeqAndFlags; std::memcpy(&netSeqAndFlags, data + offset, sizeof(uint32_t)); header.sequenceAndFlags = ntohl(netSeqAndFlags); offset += sizeof(uint32_t); // Read packet type header.type = static_cast(data[offset++]); // Read version header.version = data[offset++]; // Read source ID if present (check if packet is sourced) if (size >= SOURCED_HEADER_SIZE) { uint16_t netSourceID; std::memcpy(&netSourceID, data + offset, sizeof(uint16_t)); header.sourceID = ntohs(netSourceID); } else { header.sourceID = NULL_LOCAL_ID; } return true; } PacketType NLPacket::getType(const uint8_t* data, size_t size) { if (size < sizeof(uint32_t) + 1) { return PacketType::Unknown; } return static_cast(data[sizeof(uint32_t)]); } namespace { // --- Helpers to parse Overte header enums to ensure exact version numbers --- std::string readFileToString(const std::string& path) { std::ifstream in(path); if (!in.is_open()) return {}; std::ostringstream ss; ss << in.rdbuf(); return ss.str(); } std::unordered_map parseEnumValues(const std::string& content, const std::string& enumName) { std::unordered_map values; std::string startToken = "enum class " + enumName; auto startPos = content.find(startToken); if (startPos == std::string::npos) return values; auto bracePos = content.find('{', startPos); if (bracePos == std::string::npos) return values; auto endPos = content.find("};", bracePos); if (endPos == std::string::npos) return values; std::string body = content.substr(bracePos + 1, endPos - bracePos - 1); int current = -1; std::istringstream lines(body); std::string line; while (std::getline(lines, line)) { // strip comments auto cpos = line.find("//"); if (cpos != std::string::npos) line = line.substr(0, cpos); // trim auto notspace = [](int ch){ return !std::isspace(ch); }; line.erase(line.begin(), std::find_if(line.begin(), line.end(), notspace)); line.erase(std::find_if(line.rbegin(), line.rend(), notspace).base(), line.end()); if (line.empty()) continue; // split by comma; a line may have trailing comma auto comma = line.find(','); std::string token = (comma == std::string::npos) ? line : line.substr(0, comma); // handle assignments auto eq = token.find('='); std::string name = token; if (eq != std::string::npos) { name = token.substr(0, eq); std::string val = token.substr(eq + 1); // trim name name.erase(name.begin(), std::find_if(name.begin(), name.end(), notspace)); name.erase(std::find_if(name.rbegin(), name.rend(), notspace).base(), name.end()); // trim val val.erase(val.begin(), std::find_if(val.begin(), val.end(), notspace)); val.erase(std::find_if(val.rbegin(), val.rend(), notspace).base(), val.end()); // numeric (no hex used in these enums) try { current = std::stoi(val); } catch (...) { continue; } } else { // trim name name.erase(name.begin(), std::find_if(name.begin(), name.end(), notspace)); name.erase(std::find_if(name.rbegin(), name.rend(), notspace).base(), name.end()); current = current + 1; } if (!name.empty()) values[name] = current; } return values; } int parsePacketTypeCount(const std::string& content) { // Count identifiers in PacketTypeEnum::Value until NUM_PACKET_TYPE auto pos = content.find("enum class Value : uint8_t"); if (pos == std::string::npos) return 106; // fallback auto brace = content.find('{', pos); if (brace == std::string::npos) return 106; auto end = content.find("NUM_PACKET_TYPE", brace); if (end == std::string::npos) return 106; std::string body = content.substr(brace + 1, end - brace - 1); int count = 0; std::istringstream lines(body); std::string line; while (std::getline(lines, line)) { auto cpos = line.find("//"); if (cpos != std::string::npos) line = line.substr(0, cpos); auto notspace = [](int ch){ return !std::isspace(ch); }; line.erase(line.begin(), std::find_if(line.begin(), line.end(), notspace)); line.erase(std::find_if(line.rbegin(), line.rend(), notspace).base(), line.end()); if (line.empty()) continue; if (line.find('=') != std::string::npos) { // handle explicit value lines (rare in this enum) count++; } else if (line.find(',') != std::string::npos) { count++; } } return count; // this should equal NUM_PACKET_TYPE } void ensureVersionTable(uint8_t& vAvatarRemoveAttachments, uint8_t& vAvatarTraitsAck, uint8_t& vEntityLastPacket, uint8_t& vEntityParticleSpin, uint8_t& vAssetBakingTextureMeta, uint8_t& vEntityScriptClientCallable, uint8_t& vEntityQueryCbor, uint8_t& vAvatarQueryConical, uint8_t& vDomainServerAddedNodeSocketTypes, uint8_t& vDomainListSocketTypes, uint8_t& vDomainListRequestSocketTypes, uint8_t& vDomainConnectionDeniedExtraInfo, uint8_t& vPingIncludeConnID, uint8_t& vIcePingSendPeerID, uint8_t& vAudioStopInjectors, int& numPacketTypes) { static bool inited = false; static uint8_t s_vAvatarRemoveAttachments, s_vAvatarTraitsAck, s_vEntityLastPacket, s_vEntityParticleSpin, s_vAssetBakingTextureMeta, s_vEntityScriptClientCallable, s_vEntityQueryCbor, s_vAvatarQueryConical, s_vDomainServerAddedNodeSocketTypes, s_vDomainListSocketTypes, s_vDomainListRequestSocketTypes, s_vDomainConnectionDeniedExtraInfo, s_vPingIncludeConnID, s_vIcePingSendPeerID, s_vAudioStopInjectors; static int s_numPacketTypes; if (!inited) { // Try multiple paths since we might be run from different directories std::string path = "third_party/overte-src/libraries/networking/src/udt/PacketHeaders.h"; auto content = readFileToString(path); if (content.empty()) { path = "../third_party/overte-src/libraries/networking/src/udt/PacketHeaders.h"; content = readFileToString(path); } if (!content.empty()) { auto avatar = parseEnumValues(content, "AvatarMixerPacketVersion"); auto entity = parseEnumValues(content, "EntityVersion"); auto asset = parseEnumValues(content, "AssetServerPacketVersion"); auto entScript = parseEnumValues(content, "EntityScriptCallMethodVersion"); auto entQuery = parseEnumValues(content, "EntityQueryPacketVersion"); auto avatarQuery = parseEnumValues(content, "AvatarQueryVersion"); auto domAdded = parseEnumValues(content, "DomainServerAddedNodeVersion"); auto domList = parseEnumValues(content, "DomainListVersion"); auto domListReq = parseEnumValues(content, "DomainListRequestVersion"); auto domDenied = parseEnumValues(content, "DomainConnectionDeniedVersion"); auto ping = parseEnumValues(content, "PingVersion"); auto icePing = parseEnumValues(content, "IcePingVersion"); auto audio = parseEnumValues(content, "AudioVersion"); s_vAvatarRemoveAttachments = static_cast(avatar["RemoveAttachments"]); s_vAvatarTraitsAck = static_cast(avatar["AvatarTraitsAck"]); s_vAvatarQueryConical = static_cast(avatarQuery["ConicalFrustums"]); // Entity LAST_PACKET_TYPE is number of entries - 1 before NUM_PACKET_TYPE // If parsing map failed to give LAST_PACKET_TYPE, derive from count of entries before NUM_PACKET_TYPE label. int entityCount = 0; { // Count entries until NUM_PACKET_TYPE in the EntityVersion enum body auto ep = content.find("enum class EntityVersion"); if (ep != std::string::npos) { auto eb = content.find('{', ep); auto ee = content.find("NUM_PACKET_TYPE", eb); if (eb != std::string::npos && ee != std::string::npos) { std::string body = content.substr(eb + 1, ee - eb - 1); std::istringstream ls(body); std::string l; while (std::getline(ls, l)) { auto cpos = l.find("//"); if (cpos != std::string::npos) l = l.substr(0, cpos); auto notspace = [](int ch){ return !std::isspace(ch); }; l.erase(l.begin(), std::find_if(l.begin(), l.end(), notspace)); l.erase(std::find_if(l.rbegin(), l.rend(), notspace).base(), l.end()); if (l.empty()) continue; if (l.find(',') != std::string::npos) entityCount++; } } } } s_vEntityLastPacket = entityCount > 0 ? static_cast(entityCount - 1) : 23; s_vEntityParticleSpin = static_cast(entity["ParticleSpin"]); s_vAssetBakingTextureMeta = static_cast(asset["BakingTextureMeta"]); s_vEntityScriptClientCallable = static_cast(entScript["ClientCallable"]); s_vEntityQueryCbor = static_cast(entQuery["CborData"]); s_vDomainServerAddedNodeSocketTypes = static_cast(domAdded["SocketTypes"]); s_vDomainListSocketTypes = static_cast(domList["SocketTypes"]); s_vDomainListRequestSocketTypes = static_cast(domListReq["SocketTypes"]); s_vDomainConnectionDeniedExtraInfo = static_cast(domDenied["IncludesExtraInfo"]); s_vPingIncludeConnID = static_cast(ping["IncludeConnectionID"]); s_vIcePingSendPeerID = static_cast(icePing["SendICEPeerID"]); s_vAudioStopInjectors = static_cast(audio["StopInjectors"]); s_numPacketTypes = parsePacketTypeCount(content); inited = true; } else { // Fallback values (best-known) s_vAvatarRemoveAttachments = 38; // conservative guess s_vAvatarTraitsAck = 43; // guess s_vEntityLastPacket = 99; // guess s_vAssetBakingTextureMeta = 22; s_vEntityScriptClientCallable = 19; s_vEntityQueryCbor = 24; s_vDomainServerAddedNodeSocketTypes = 19; s_vDomainListSocketTypes = 25; s_vDomainListRequestSocketTypes = 23; s_vDomainConnectionDeniedExtraInfo = 19; s_vPingIncludeConnID = 18; s_vIcePingSendPeerID = 18; s_vAudioStopInjectors = 24; s_numPacketTypes = 106; inited = true; } } vAvatarRemoveAttachments = s_vAvatarRemoveAttachments; vAvatarTraitsAck = s_vAvatarTraitsAck; vEntityLastPacket = s_vEntityLastPacket; vEntityParticleSpin = s_vEntityParticleSpin; vAssetBakingTextureMeta = s_vAssetBakingTextureMeta; vEntityScriptClientCallable = s_vEntityScriptClientCallable; vEntityQueryCbor = s_vEntityQueryCbor; vAvatarQueryConical = s_vAvatarQueryConical; vDomainServerAddedNodeSocketTypes = s_vDomainServerAddedNodeSocketTypes; vDomainListSocketTypes = s_vDomainListSocketTypes; vDomainListRequestSocketTypes = s_vDomainListRequestSocketTypes; vDomainConnectionDeniedExtraInfo = s_vDomainConnectionDeniedExtraInfo; vPingIncludeConnID = s_vPingIncludeConnID; vIcePingSendPeerID = s_vIcePingSendPeerID; vAudioStopInjectors = s_vAudioStopInjectors; numPacketTypes = s_numPacketTypes; } } // anonymous namespace uint8_t NLPacket::versionForPacketType(PacketType type) { uint8_t vAvatarRemoveAttachments, vAvatarTraitsAck, vEntityLastPacket, vEntityParticleSpin, vAssetBakingTextureMeta, vEntityScriptClientCallable, vEntityQueryCbor, vAvatarQueryConical, vDomainServerAddedNodeSocketTypes, vDomainListSocketTypes, vDomainListRequestSocketTypes, vDomainConnectionDeniedExtraInfo, vPingIncludeConnID, vIcePingSendPeerID, vAudioStopInjectors; int numPacketTypes = 106; ensureVersionTable(vAvatarRemoveAttachments, vAvatarTraitsAck, vEntityLastPacket, vEntityParticleSpin, vAssetBakingTextureMeta, vEntityScriptClientCallable, vEntityQueryCbor, vAvatarQueryConical, vDomainServerAddedNodeSocketTypes, vDomainListSocketTypes, vDomainListRequestSocketTypes, vDomainConnectionDeniedExtraInfo, vPingIncludeConnID, vIcePingSendPeerID, vAudioStopInjectors, numPacketTypes); // Based on Overte's PacketHeaders.cpp versionForPacketType() // Returns the protocol version for each packet type switch (type) { case PacketType::DomainConnectRequest: return PacketVersions::DomainConnectRequest_SocketTypes; case PacketType::DomainListRequest: return vDomainListRequestSocketTypes; case PacketType::DomainList: return vDomainListSocketTypes; case PacketType::Ping: return vPingIncludeConnID; case PacketType::DomainConnectionDenied: return vDomainConnectionDeniedExtraInfo; case PacketType::DomainConnectRequestPending: return 17; case PacketType::PingReply: return 17; case PacketType::ICEServerPeerInformation: case PacketType::ICEServerQuery: return 17; case PacketType::ICEServerHeartbeat: return 18; // ICE server heartbeat signing case PacketType::ICEServerHeartbeatACK: return 17; case PacketType::ICEServerHeartbeatDenied: return 17; case PacketType::ICEPing: return vIcePingSendPeerID; case PacketType::ICEPingReply: return 17; case PacketType::NodeIgnoreRequest: return 18; case PacketType::DomainServerAddedNode: return vDomainServerAddedNodeSocketTypes; case PacketType::EntityAdd: case PacketType::EntityClone: case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: return vEntityLastPacket; case PacketType::EntityQuery: return vEntityQueryCbor; case PacketType::EntityQueryInitialResultsComplete: return vEntityParticleSpin; case PacketType::AvatarQuery: return vAvatarQueryConical; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: return vAvatarRemoveAttachments; case PacketType::BulkAvatarTraitsAck: case PacketType::BulkAvatarTraits: return vAvatarTraitsAck; case PacketType::MessagesData: return 18; // TextOrBinaryData case PacketType::AssetMappingOperation: case PacketType::AssetMappingOperationReply: case PacketType::AssetGetInfo: case PacketType::AssetGet: case PacketType::AssetUpload: return vAssetBakingTextureMeta; case PacketType::EntityScriptCallMethod: return vEntityScriptClientCallable; case PacketType::DomainSettings: return 18; case PacketType::MixedAudio: case PacketType::SilentAudioFrame: case PacketType::InjectAudio: case PacketType::MicrophoneAudioNoEcho: case PacketType::MicrophoneAudioWithEcho: case PacketType::AudioStreamStats: case PacketType::StopInjector: return vAudioStopInjectors; // For other packet types, return a default version // In real Overte, each has a specific version default: return 22; // Default version for unspecified packets (matches Overte PacketHeaders.cpp) } } std::vector NLPacket::computeProtocolVersionSignature() { // Protocol signature extracted from official Overte client packet capture // This is the actual signature that the AUR overte-server-bin package expects static const std::vector signature = { 0xeb, 0x16, 0x00, 0xe7, 0x98, 0xdc, 0x5e, 0x03, 0xc7, 0x55, 0xa9, 0x68, 0xdc, 0x16, 0xb7, 0xfc }; return signature; } } // namespace Overte