Skip to content

Releases: MafiaHub/MafiaNet

v0.13.0

Choose a tag to compare

@Segfaultd Segfaultd released this 29 Jul 15:07

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

v0.12.0

Choose a tag to compare

@Segfaultd Segfaultd released this 29 Jul 13:05

Batched datagram I/O (recvmmsg / sendmmsg)

On Linux the reliability layer now coalesces a tick's outgoing datagrams into a single sendmmsg, and drains the receive socket with a single recvmmsg per burst, instead of one sendto/recvfrom per packet.

There is nothing to configure. Batching is a platform capability, guarded by a plain #if defined(__linux__), always on where the syscalls exist. macOS, Windows and the BSDs compile the portable per-datagram paths. Delivery semantics are identical either way — the same datagrams arrive, in the same order, with the same reliability.

Impact

Measured on the 2560-message reliable-ordered burst in Tests/Integration/MmsgBatchLiveTests.cpp (Linux, Release, strace -c, median of 3 runs):

syscall per-datagram batched
sendto 2907 35
sendmmsg 0 58
recvfrom 2618 0
recvmmsg 0 85
total 5525 178

~31x fewer system calls. Up to 64 datagrams (MMSG_BATCH_MAX) coalesce per call. Counts vary a percent or two between runs, so the ratio is the result rather than the exact figures. At low packet rates the change is not measurable.

Runtime fallback

If recvmmsg/sendmmsg report ENOSYS — a seccomp profile, gVisor, user-mode emulation, or a kernel older than the syscall — the process latches the condition once and both paths revert to the portable per-datagram code for the rest of its life. Only ENOSYS latches; EPERM is excluded because a firewall rejecting a single destination reports it too. Verified end to end under a seccomp profile forcing errno 38: recvmmsg is attempted exactly once, sendmmsg never, and all traffic falls back cleanly with the full suite passing.

New API

RakNetSocket2::SendBatch — a virtual on the socket interface with a portable Send()-loop default and a sendmmsg override on Linux. Returns a datagram count (not a byte total), or a negative error only when nothing at all went out, mirroring sendmmsg(2). A datagram that fails on its own is dropped and the rest of the batch is still sent.

Testing

  • Tests/Unit/MmsgBatchTests.cpp — 58 cases over the partial-send resume state machine, the transient-vs-permanent errno split, missing-syscall detection, sockaddr decoding, and the recv-slot carry-over including a 500-pass fixed-seed stress proving no slot is leaked or double-freed.
  • Tests/Integration/MmsgBatchLiveTests.cpp — drives bursts far past the batch boundary; the rest of the suite sends about one datagram per tick and never fills a batch. Each message is its own checksum, so truncation, payload aliasing and reordering each fail a distinct assertion.
  • New linux-native CI job covering Debug and Release. The hermetic unit suite now runs exactly once — --repeat until-pass:3 is reserved for the integration suite, where it absorbs loopback timing misses instead of masking nondeterminism.

Verified on Linux Debug and Release (unit 94/94, integration 32/32), macOS and Windows CI green.

Known limitations

All testing is loopback inside a container — not a real NIC, MTU, loss or ICMP. ENOBUFS and partial-sendmmsg are exercised against fakes rather than provoked. No long-running soak. A staged rollout is recommended for the batched paths.

Full changelog: v0.11.0...v0.12.0

v0.11.0

Choose a tag to compare

@Segfaultd Segfaultd released this 14 Jul 08:56

Core / API

  • Range-based receive Peer::incoming() — drain the receive queue with a range-for; each iteration yields a fresh PacketPtr freed at end of scope, and pkt.id() returns the ID_TIMESTAMP-aware message identifier.
  • Startup builders Peer::server() / Peer::client() — a fluent chain folding SocketDescriptor + Startup + result check + SetMaximumIncomingConnections / Connect into one call. start() returns a move-only Result<Peer> whose error preserves the underlying StartupResult / ConnectionAttemptResult (tagged by PeerStage). Security stays opt-in (secure() / public_key()).
  • Serialization archives (mafianet/Archive.h) — one serialize(Ar&) member template describes a type's wire format for both directions; WriteArchive / ReadArchive adapt a BitStream, recursing into nested serialize() types and falling through to operator<< / operator>> for everything else.
  • Typed message dispatcher (mafianet/Dispatcher.h) — on<T>(handler) auto-assigns identifiers from ID_USER_PACKET_ENUM in registration order (documented wire contract; on<T>(id, handler) pins explicit ids), on(id, handler) covers system messages, and dispatch() skips ID_TIMESTAMP prefixes and hands handlers a deserialized T plus a Sender. encode() is the symmetric write path. Opt-in — the raw switch stays fully usable.
  • Typed Peer::send / Peer::broadcast — serialize-and-send a registered message in one call via the dispatcher registry, with overridable defaults (Priority::High, Reliability::ReliableOrdered, channel 0). Destination accepts SystemAddress or RakNetGUID; raw Send() untouched.

Build

  • RakVoice is built into the core library — header now mafianet/RakVoice.h; Opus and RNNoise are fetched and linked into the core automatically, no separate extension build.

Testing / CI

  • Full GoogleTest migration — all 29 legacy tests ported and the Samples/Tests TestInterface harness deleted. Hermetic UnitTests and loopback IntegrationTests under Tests/, one process per test via CTest; MAFIANET_BUILD_TESTS builds everything test-related (requires MAFIANET_BUILD_STATIC). CI runs ctest with JUnit artifacts on all platforms.

Full changelog: v0.10.0...v0.11.0

v0.10.0

Choose a tag to compare

@Segfaultd Segfaultd released this 15 Jun 14:51

MafiaNet v0.10.0

Core / API

  • Umbrella header mafianet/mafianet.h — single include aggregating the core public headers (RakPeerInterface, types, message IDs, PacketPriority, BitStream, GetTime, Statistics). Additive; encryption headers intentionally omitted (security stays opt-in).
  • Canonical type aliases (mafianet/aliases.h) — PeerInterface, Guid, Statistics, UnassignedGuid over the legacy RakNet names. using aliases, fully interoperable; legacy names untouched.
  • RAII handles Peer & PacketPtr (mafianet/PeerHandle.h) — own a RakPeerInterface / received Packet and clean up on scope exit. ChatExample client rewritten to use them.
  • Thread-safe GUID value accessors (mafianet/guid_util.h) — MafiaNet::to_string(const RakNetGUID&) (owns its buffer) and connected_address(...) returning std::optional<SystemAddress>.

Spatial

  • PointGridSectorizer — uniform point grid with O(1) RemoveEntry/MoveEntry (per-entry hash + swap-remove, early-out on same-cell moves), upsert add/move, duplicate-free GetEntries, edge-cell clamping. GridSectorizer left untouched.

⚠️ Breaking changes

  • Scoped enum classes — global PacketPriority / PacketReliability C enums removed in favour of scoped MafiaNet::Priority / MafiaNet::Reliability. Enumerator order and wire field preserved; update call sites (HIGH_PRIORITYMafiaNet::Priority::High, RELIABLE_ORDEREDMafiaNet::Reliability::ReliableOrdered). NUMBER_OF_* sentinels are now constexpr counts in MafiaNet.
  • Removed non-thread-safe RakNetGUID::ToString(void) (shared static buffer) — use MafiaNet::to_string(g).c_str().

Bug fix

  • PeerHandle no longer dereferences a moved-from Peer in receive(); corrected header copyright.

Testing

  • Added PointGridSectorizerTest, PeerHandleTest, GuidUtilTest; hardened DisconnectReasonTest against CI scheduler starvation.

Full changelog: v0.9.0...v0.10.0

v0.9.0

Choose a tag to compare

@Segfaultd Segfaultd released this 08 Jun 10:36

Core

  • Strong-typed PeerGuid (#25). A new enum class PeerGuid : uint64_t names a peer's RakNetGUID value distinctly from NetworkID (an object id), so the two can no longer be passed interchangeably in a uint64_t-typed signature — removing a class of silent "passed the wrong id" bugs in ReplicaManager3 glue and void(uint64_t) callbacks.
    • Convert with MafiaNet::ToPeerGuid() / MafiaNet::ToGuid(); compare against the UNASSIGNED_PEER_GUID sentinel.
    • As a trivially-copyable 8-byte scoped enum it serializes byte-identically through BitStream (and therefore VariableDeltaSerializer) to the raw uint64_t it replaces — fully wire-compatible, no netcode/protocol bump.
    • Purely additive; no behavioural change.

Full Changelog: v0.8.0...v0.9.0

v0.8.0

Choose a tag to compare

@Segfaultd Segfaultd released this 03 Jun 18:47

Core

  • Optional disconnect reason on graceful disconnects. CloseConnection gains a final optional const BitStream *reasonData argument whose bytes are appended right after the ID_DISCONNECTION_NOTIFICATION message ID, so the remote peer can learn why it was dropped (e.g. a kick/ban enum plus a custom string). The receiver reads it like any other message body — packet->data + 1 for packet->length - 1 bytes. Only graceful disconnects carry a reason; locally-synthesized notifications (ID_CONNECTION_LOST and the timeout/dead-connection path) stay payload-less, so consumers must tolerate a zero-length body. Wire-backward-compatible: peers that only inspect data[0] are unaffected.

Bug fix

  • RakPeer::CloseConnection no longer coerces an unresolved target index (-1 from GetIndexFromSystemAddress) to 0 and then reads remoteSystemList[0] — which targeted an unrelated peer's slot or crashed when the list was unallocated. The close socket is now resolved without assuming a valid slot index.

Testing

  • Added DisconnectReasonTest covering reason round-trip, the nullptr default, and the empty-but-non-null BitStream guard.

Full Changelog: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@Segfaultd Segfaultd released this 03 Jun 09:35

Virtual Worlds (Dimensions) for ReplicaManager3

This release adds runtime per-player dimension scoping on top of ReplicaManager3 — the SA-MP `SetPlayerVirtualWorld` / FiveM routing-bucket model for instanced interiors such as apartments.

Highlights

  • New lightweight per-entity / per-observer `VirtualWorldId` tag (distinct from the heavyweight RM3 `WorldId`). Players only see entities sharing their virtual world (or the `VIRTUAL_WORLD_GLOBAL` sentinel), switchable on the fly with no reconnect, while staying on the same connection and RM3 world.
  • Derive entities from the new `VirtualWorldReplica3` base (`mafianet/VirtualWorldReplica3.h`) — no per-object filtering code.
  • `Connection_RM3`: `Get/SetVirtualWorld` (the observer's dimension).
  • `ReplicaManager3`: `GetConnectionsInVirtualWorld` / `GetGuidsInVirtualWorld` (recipient-filter helpers for scoping chat/RPC/raw sends) and `SetPlayerVirtualWorld`.
  • The filter is applied only by the authority for an (entity, connection) pair, so a downloaded copy never despawns the entity at its owner.

Docs & tests

  • New "Virtual Worlds (Dimensions)" plugin guide; expanded contributing guide (how to test networked features end-to-end, the RM3 authority model, multi-peer gotchas).
  • New `VirtualWorldTest` (deterministic) and a self-contained `Samples/VirtualWorld` demo/smoke test.

See `mafianet/VirtualWorld.h` and `Samples/VirtualWorld` to get started.

Full changelog: v0.6.1...v0.7.0

v0.6.1

Choose a tag to compare

@Segfaultd Segfaultd released this 02 Jun 18:48

ReplicaManager3: const-qualify GetReplicaAtIndex

GetReplicaAtIndex was the only one of the four ReplicaManager3 read accessors that was non-const, even though its sibling GetConnectionAtIndex (same pattern, returns an internal pointer by index) is const. This forced const methods on derived managers to const_cast away constness just to iterate replicas.

Replica3 *GetReplicaAtIndex(unsigned index, WorldId worldId=0) const;

The method only reads the world's replica list and returns an existing pointer — no mutation — so the const qualifier is accurate. The returned Replica3* stays non-const, consistent with GetConnectionAtIndex returning a non-const Connection_RM3* from a const method.

Compatibility: Source-compatible — adding const to a read accessor doesn't break existing non-const call sites, and lets const iterators drop their const_cast. No ABI-sensitive layout change.

Closes #8 (via #9).

Full Changelog: v0.6.0...v0.6.1

v0.6.0

Choose a tag to compare

@Segfaultd Segfaultd released this 02 Jun 17:44

RPC4 handlers now carry user context

RegisterFunction, RegisterSlot, RegisterBlockingFunction and the RPC4GlobalRegistration handler constructors now take an opaque void *context that is passed back to the handler on every invocation. This removes the need for file-static global pointers to route an RPC back to an object instance — each registration carries its own context, so the same handler can serve multiple object instances under one identifier. The void* approach keeps RPC4 free of external dependencies. (#4, #5)

Bug fixes

  • RakPeer::CloseConnection no longer dereferences a null rakNetSocket during connection teardown (a pre-existing crash in release builds, where the assertion is compiled out); it now falls back to the primary socket.

Testing

  • Added RPC4ContextTest covering slot, nonblocking, and blocking handler context.
  • Quarantined the flaky ManyClientsOneServerDeallocateBlockingTest under CI pending a pre-existing teardown-race fix (#7).

⚠️ Breaking changes

  • RPC4 handler signatures gained a trailing void *context parameter, and the registration / global-registration functions take a context argument. There are no compatibility overloads — update your handlers and registration calls (pass nullptr when no context is needed).

Full changelog: v0.5.1...v0.6.0

v0.5.1

Choose a tag to compare

@Segfaultd Segfaultd released this 31 May 20:22

Plugins

  • Added DirectoryDeltaTransfer::AddFile(const char *filePath, const char *fileName) to queue a single file for upload, complementing the recursive AddUploadsFromSubdirectory. It forwards to the existing FileList::AddFile overload, making the fork self-sufficient for downstream consumers (MafiaHub Framework) that depend on this helper.

Full Changelog: v0.5.0...v0.5.1