Skip to content

Networking: derive RPC identifiers from a salted compile-time hash - #252

Open
Segfaultd wants to merge 1 commit into
developfrom
feat/rpc-identifier-hashing
Open

Networking: derive RPC identifiers from a salted compile-time hash#252
Segfaultd wants to merge 1 commit into
developfrom
feat/rpc-identifier-hashing

Conversation

@Segfaultd

Copy link
Copy Markdown
Member

Summary

RPC identifiers were plaintext on two surfaces. RPC4 keys its slots by, and transmits, the identifier as a string — Signal/Call write it with WriteCompressed(RakString) (RPC4Plugin.cpp:433, :308/340/355), and the receiver reads it back as a RakString and does a string-keyed lookup. So every RPC packet carried a readable name like Framework::ChatMessage on the wire, and the same literals sat in the compiled binary. With no transport encryption in the current build, that hands a passive sniffer — and anyone running strings on a mod — the entire RPC surface.

This PR replaces each readable identifier with a salted compile-time hash, so the binary and the wire carry only an opaque, per-build token.

Approach (idea "A" from the design discussion — inspired by GTA/FiveM native hashing)

New header networking/rpc/rpc_identifier.h:

  • FW_RPC_IDENTIFIER("Framework::ChatMessage") → a 64-bit FNV-1a of the name, salted by FW_RPC_IDENTIFIER_SALT, rendered as a stable 16-char lowercase-hex token.
  • The token backs a plain const char*, so RPC4's string-keyed API is untouched — registration, dispatch, and the Payload concept are unchanged.
  • The readable name is consumed only inside a template argument (mandatory constant evaluation), so it is not referenced at runtime and does not ship in release binaries — only the hex token does.

Converted every framework RPC identifier: ClientIdentity, ChatMessage, ServerResources, ResourceRefresh, ResourceStop, VoiceSettings, VoiceSpeakerRange, VoicePreference, EmitScriptEvent, plus the replication ForceState / SetOwner RPCs. Source keeps the readable names for developers.

Per-build rotation

The salt makes the mapping per-build: bump FW_RPC_IDENTIFIER_SALT, or set -DFW_RPC_IDENTIFIER_SALT=<uint64> in release CI, to re-derive every identifier — a name table lifted from one build is worthless against the next. Client and server must share the salt (they exchange the derived token), so it defaults to a fixed constant that keeps local dev builds deterministic and interoperable; CI can inject a per-release value at tag time.

Compatibility

  • Wire-format change → MAJOR bump. Client and server must be built together with the same salt (already required via the build token; the salt rides along the same release boundary).
  • No API change for game code: payload structs still declare kIdentifier, just via the macro. Projects that define their own RPCs keep working unchanged (and can opt in to the macro).

Scope / non-goals

  • Not obfuscation of the transport itself — the right fix for wire confidentiality of payloads (and identifiers) is enabling transport encryption (LIBCAT_SECURITY is currently 0), reported separately. Identifier hashing narrows the surface and adds per-build rotation; it is not a substitute for encryption, and does not make identifiers secret at runtime.
  • Out of scope here: EmitScriptEvent's script-defined event name (application data, still plaintext) and entity type ids (already sent as an unsalted CRC32; a candidate for the same treatment as a follow-up). The TwoWayAuthentication build-challenge identifier is a separate auth-plugin mechanism, left untouched.

Testing

Added a rpc_identifier unit module (code/tests/modules/rpc_identifier_ut.h, wired into framework_ut.cpp) asserting the properties the RPC layer relies on:

  • token is an opaque 16-char hex string that embeds neither Framework nor the payload name,
  • distinct payloads get distinct identifiers,
  • the derivation is deterministic for a given name (both peers must agree),
  • the token is exactly the hex of HashIdentifier(name),
  • the build salt actually participates (salted hash differs from plain FNV-1a; salt is non-zero).

The machinery and all converted payloads type-check cleanly (clang++ -std=c++20 -fsyntax-only, including compile-time static_asserts of the same properties). I could not run the full FrameworkTests binary locally (unrelated toolchain issue building the tree on my machine: AppleClang + bundled fmt), so please confirm the new module is green in CI.

RPC4 keys slots by, and transmits, the RPC identifier as a plain string:
Signal/Call write it with WriteCompressed(RakString), so every RPC packet
carried a readable name like "Framework::ChatMessage" on the wire, and the
same literals sat in the compiled binary. With no transport encryption in
place, that hands a passive observer (and anyone running strings on a mod)
the full RPC surface.

Replace each readable identifier with FW_RPC_IDENTIFIER(name): a 64-bit
FNV-1a of the name, salted by FW_RPC_IDENTIFIER_SALT, rendered as a stable
16-char hex token. The readable name is consumed only in constant
evaluation, so it no longer reaches the binary or the wire -- both now
carry the opaque token. RPC4's const char* API is untouched (the token
backs a const char*), so registration and dispatch are unchanged.

The salt makes the mapping per-build: bump FW_RPC_IDENTIFIER_SALT (or set
-DFW_RPC_IDENTIFIER_SALT=<uint64> in release CI) to re-derive every
identifier, so a name table lifted from one build is worthless against the
next. Client and server must share the salt, so a change is netcode-
breaking (MAJOR); the default keeps local dev builds deterministic.

Source keeps the readable names for developers. Adds a rpc_identifier unit
module covering opacity, distinctness, determinism, and salt inclusion.
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Segfaultd, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 997cd1f7-8897-4ef7-bd46-fab6b1a93a84

📥 Commits

Reviewing files that changed from the base of the PR and between 7d552a2 and b5c289f.

📒 Files selected for processing (11)
  • code/framework/src/integrations/shared/rpc/emit_script_event.h
  • code/framework/src/networking/replication/replication_manager.cpp
  • code/framework/src/networking/rpc/chat_message.h
  • code/framework/src/networking/rpc/client_identity.h
  • code/framework/src/networking/rpc/resource_refresh.h
  • code/framework/src/networking/rpc/rpc.h
  • code/framework/src/networking/rpc/rpc_identifier.h
  • code/framework/src/networking/rpc/server_resources.h
  • code/framework/src/networking/rpc/voice_settings.h
  • code/tests/framework_ut.cpp
  • code/tests/modules/rpc_identifier_ut.h

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants