Skip to content

v0.13.0

Latest

Choose a tag to compare

@Segfaultd Segfaultd released this 29 Jul 15:07
· 2 commits to master since this release

Voice: relay mode for RakVoice

SendFrame has always transmitted peer-to-peer, which a dedicated-server game cannot use — clients connect only to the server, never to each other. Relay mode lets clients send frames to a host that forwards them without decoding, so the server stays authoritative over who hears whom without paying for a codec: a hacked client cannot hear players it isn't allowed to, because it never receives their bytes.

Relay frames carry the talker's GUID, since the sender is now the relay rather than the speaker, plus a format-version byte:

[id][format version][origin guid][channel id][sequence][opus payload]

The version byte is a deliberate escape hatch — a future layout change is rejected by today's build instead of misparsed. Every offset derives from the one before it, so the writer and both readers cannot drift apart.

Alongside it:

  • Origin-keyed channels — frames are looked up by origin rather than packet->guid; otherwise every speaker arrives under the relay's GUID and collapses into one decoder.
  • Per-speaker outputSetPerSpeakerOutput / ReceiveFrameFrom pull one speaker's decoded PCM instead of the pre-mix, which is what makes 3D positioning possible above this layer.
  • Bounded relay state — concurrent speakers are capped (origins are attacker-influenced and each costs a decoder plus two rings), and idle relay channels are reaped, since OnClosedConnection never fires for peers of the host rather than of us.
  • RelayFrame validates centrally — origin against the transport-authenticated sender, frame size, packet id, recipient list — rather than trusting every host to remember the impersonation check.

Peer-to-peer behaviour is unchanged when relay mode is off.

Security and robustness

Five pre-existing remote-input bugs, found while auditing RakVoice's packet entry points. All are reachable by any connected peer and none require relay mode — they affect anyone running an earlier release with RakVoice attached:

  • OnVoiceData read out of bounds on a 1–2 byte ID_RAKVOICE_DATA packet: the header memcpy ran past the buffer and packet->length - headerSize underflowed to a huge unsigned value passed to opus_decode as the payload length.
  • OpenChannel called RakAssert on a remotely supplied sample rate. RakAssert is a real assert() in debug builds, so one malformed channel-open packet aborted a debug server.
  • OpenChannel used that sample rate without checking the read succeeded; a packet too short to carry it left the value indeterminate.
  • OnReceive dispatched on data[0] with no length check.
  • OnOpenChannelReply lacked the initialisation guard OnOpenChannelRequest has, so an unsolicited reply on an uninitialised instance opened a channel with bufferSizeBytes of 0 and allocated empty rings.

Two further latent bugs: the constructor never initialised zeroBufferedOutput or bufferedOutputCount, so Update() read indeterminate values on any attached-but-uninitialised instance; and CloseVoiceChannel sent ID_RAKVOICE_CLOSE_CHANNEL unconditionally, so a peer that never opened a channel still got one on disconnect.

Testing

23 new unit cases (Tests/Unit/RakVoiceRelayTests.cpp) over the wire layout, hostile relay input, the speaker cap and the channel-open paths. 149/149 pass.

⚠️ Breaking change

ID_RAKVOICE_RELAY_DATA is inserted after ID_RAKVOICE_DATA and shifts every subsequent message id — including ID_READY_EVENT_SET, the RPC4 and two-way-authentication ids, and ID_USER_PACKET_ENUM. Peers must be rebuilt together; a peer built against the old header misparses everything past that point.

Full changelog: v0.12.0...v0.13.0