Skip to content

feat: carry a disconnect reason in ID_DISCONNECTION_NOTIFICATION / CloseConnection#24

Merged
Segfaultd merged 4 commits into
masterfrom
feat/disconnect-reason
Jun 3, 2026
Merged

feat: carry a disconnect reason in ID_DISCONNECTION_NOTIFICATION / CloseConnection#24
Segfaultd merged 4 commits into
masterfrom
feat/disconnect-reason

Conversation

@Segfaultd
Copy link
Copy Markdown
Member

@Segfaultd Segfaultd commented Jun 3, 2026

Closes #23.

Summary

Adds an optional reasonData payload to RakPeerInterface::CloseConnection() so a graceful disconnect can tell the remote peer why it was dropped, instead of racing a separate application RPC against the disconnect.

virtual void CloseConnection(const AddressOrGUID target, bool sendDisconnectionNotification,
                             unsigned char orderingChannel = 0,
                             PacketPriority disconnectionNotificationPriority = LOW_PRIORITY,
                             const MafiaNet::BitStream *reasonData = nullptr) = 0;

The reason rides right after the 1-byte ID_DISCONNECTION_NOTIFICATION ID; the receiver reads it from packet->data+1 (length packet->length-1), exactly like any other message body. It's a flexible superset — send a single enum byte if you want minimal overhead, or an enum + custom string for "Kicked: <reason>":

BitStream bs; bs.Write((uint8_t)KickReason::Kicked);
RakString("Cheating in match #4821").Serialize(&bs);
peer->CloseConnection(guid, true, 0, LOW_PRIORITY, &bs);

The non-obvious part

The issue assumed the payload would flow straight through, but it would not have: when the notification arrives over the network, the raw reliability-layer buffer is freed, and the user-facing packet is re-synthesized fresh as a bare 1-byte message after ACKs flush. So the reason bytes are stashed on RemoteSystemStruct on receipt and copied into the synthesized packet. The buffer is freed via a single ClearDisconnectReason() helper on delivery's teardown, on slot reuse, and at shutdown (plus a one-time zero-init at allocation).

The notification is sent RELIABLE_ORDERED (unchanged), so the reason is ordered relative to prior messages — eliminating the race the downstream workaround had.

Scope / compatibility

  • Only graceful disconnects carry a reason. Locally-synthesized notifications (ID_CONNECTION_LOST, timeout/dead-connection) stay payload-less, so consumers must tolerate a zero-length body.
  • Wire-backward-compatible in both directions: peers that only read data[0] are unaffected; an old sender yields a bare 1-byte notification.
  • RakPeer is the only RakPeerInterface implementor, so the signature change touches nothing else.

Tests

Adds DisconnectReasonTest (registered in the suite) over loopback:

  • Case 1: server kicks client with an enum + RakString; asserts the notification carries the payload and round-trips exactly.
  • Case 2: server kicks with no reason (default nullptr); asserts a bare 1-byte notification (packet->length == 1).

Verification:

  • New test + existing disconnect/churn tests (PeerConnectDisconnectTest, PeerConnectDisconnectWithCancelPendingTest, DroppedConnectionConvertTest, EightPeerTest, ManyClientsOneServerBlockingTest) all pass.
  • Clean under AddressSanitizer (exercises stash → deliver → slot-reuse → shutdown teardown).
  • Full sample tree builds with no errors.

Note: this is a fork-divergent wire addition relative to upstream SLikeNet; worth a changelog line at the next release cut.

Summary by CodeRabbit

  • New Features

    • Connections can now include optional reason payloads when closing; disconnection notifications deliver and preserve these payloads for client-side handling.
  • Tests

    • Added test suite for disconnect reason payload functionality, covering scenarios with and without reason data.

…ON (#23)

Add an optional reasonData BitStream to CloseConnection() so a graceful
disconnect can tell the remote peer *why* it was dropped. The payload is
appended right after the 1-byte ID_DISCONNECTION_NOTIFICATION ID, so the
receiver reads it from packet->data+1 (length packet->length-1). Appending
bytes after the ID is wire-backward-compatible.

On the receive side the raw reliability-layer buffer is freed before the
user-facing notification is synthesized, so the reason bytes are stashed on
the RemoteSystemStruct when the notification arrives and copied into the
delivered packet once outstanding ACKs are flushed. The stash buffer is
freed/cleared on delivery, on slot reuse, and on every teardown path via
ClearDisconnectReason().

Only graceful disconnects carry a reason; locally-synthesized notifications
(ID_CONNECTION_LOST, timeout/dead-connection) stay payload-less, so consumers
must tolerate a zero-length body.

Adds DisconnectReasonTest covering both the reason-carrying and reasonless
(bare 1-byte) cases over loopback. Verified clean under AddressSanitizer.

Closes #23
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 20 minutes. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0a932806-c477-4405-a695-4961e956cb65

📥 Commits

Reviewing files that changed from the base of the PR and between 5da7708 and 787b403.

📒 Files selected for processing (4)
  • Samples/Tests/DisconnectReasonTest.cpp
  • Source/src/RakPeer.cpp
  • docs/advanced/debugging-disconnects.rst
  • docs/basics/connecting.rst

Walkthrough

This PR implements optional disconnect-reason payloads on ID_DISCONNECTION_NOTIFICATION messages. The caller supplies a BitStream payload pointer to CloseConnection, which flows through the send path to NotifyAndFlagForShutdown and is appended after the message ID. The receiver extracts and stashes trailing bytes; later user-facing notifications for graceful disconnects include that stashed reason. Storage and lifecycle are managed throughout allocation, reuse, shutdown, and cleanup paths.

Changes

Disconnect Reason Feature

Layer / File(s) Summary
API Contract Definition
Source/include/mafianet/peerinterface.h, Source/include/mafianet/peer.h
CloseConnection and internal methods (NotifyAndFlagForShutdown, CloseConnectionInternal2) gain optional reasonData parameter. ClearDisconnectReason helper is declared. Wire format appends payload after the 1-byte message ID.
Remote System State Storage
Source/include/mafianet/peer.h
RemoteSystemStruct adds disconnectReasonData pointer and disconnectReasonLength to store received disconnect reason, with lifecycle documentation.
Send-Side Implementation
Source/src/RakPeer.cpp
CloseConnection threads reasonData through CloseConnectionInternal2 to NotifyAndFlagForShutdown, which appends payload bytes immediately after writing ID_DISCONNECTION_NOTIFICATION to the outbound BitStream.
Receive and Synthetic Notification
Source/src/RakPeer.cpp
Network loop extracts trailing bytes from inbound ID_DISCONNECTION_NOTIFICATION and copies into remoteSystem->disconnectReasonData. Later synthesis of user-facing notifications for graceful DISCONNECT_ON_NO_ACK includes stashed reason payload after the message ID.
State Lifecycle Management
Source/src/RakPeer.cpp
ClearDisconnectReason() helper frees owned memory and resets fields. Remote-system slots zero-initialize reason fields on creation, clear on reuse, clear on shutdown, and clear on connection closure.
Test Suite and Integration
Samples/Tests/DisconnectReasonTest.h, Samples/Tests/DisconnectReasonTest.cpp, Samples/Tests/IncludeAllTests.h, Samples/Tests/Tests.cpp
DisconnectReasonTest class validates disconnect with serialized reason code/string (case 1) and disconnect without payload (case 2). Helper ConnectAndGetClientGuid establishes loopback connections. Tests registered in harness.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 A reason now rides along the wire,
No more chasing messages in the fire!
Disconnect with style, tell them why,
No silent drops, just a proper goodbye! 🎩✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately and concisely summarizes the main change: adding optional disconnect reason payload to ID_DISCONNECTION_NOTIFICATION via CloseConnection parameter.
Linked Issues check ✅ Passed All coding requirements from issue #23 are met: API extended with reasonData parameter, payload appended after message ID, backward compatibility preserved, graceful-only restriction enforced, safe lifecycle handling implemented, and comprehensive tests provided.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #23: API signatures, internal implementation, test infrastructure, and test coverage for disconnect reasons—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/disconnect-reason

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Source/src/RakPeer.cpp (1)

1640-1673: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Don't coerce unresolved closes onto slot 0.

If CloseConnection() is called before Startup(), after Shutdown(), or for a target that no longer resolves, GetIndexFromSystemAddress() returns -1, Line 1660 rewrites that to 0, and Line 1669 immediately reads remoteSystemList[0]. That is a crash when remoteSystemList == 0 / maximumNumberOfPeers == 0, and when the array does exist it can still pick the wrong peer's socket and systemIndex.

Proposed fix
 void RakPeer::CloseConnection( const AddressOrGUID target, bool sendDisconnectionNotification, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority, const MafiaNet::BitStream *reasonData )
 {
+	if (remoteSystemList == 0 || endThreads == true)
+		return;
+
 	const SystemAddress address = (target.systemAddress == UNASSIGNED_SYSTEM_ADDRESS) ? GetSystemAddressFromGuid(target.rakNetGuid) : target.systemAddress;
-	int remoteSystemListIndex = GetIndexFromSystemAddress(address);
-	// fallback to index 0 (i.e. preserve old behavior for now)
-	// `#med` - review this whole design here
-	if (remoteSystemListIndex == -1) {
-		remoteSystemListIndex = 0;
-	}
+	int remoteSystemListIndex =
+		target.rakNetGuid != UNASSIGNED_RAKNET_GUID
+			? GetIndexFromGuid(target.rakNetGuid)
+			: GetIndexFromSystemAddress(address);
+	if (remoteSystemListIndex == -1)
+		return;
 
 	RakNetSocket2 *closeSocket = remoteSystemList[remoteSystemListIndex].rakNetSocket;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/src/RakPeer.cpp` around lines 1640 - 1673, CloseConnection currently
coerces a -1 result from GetIndexFromSystemAddress into 0 and then
unconditionally reads remoteSystemList[remoteSystemListIndex], which can crash
or target the wrong peer; change the logic to NOT rewrite -1 to 0 and to only
dereference remoteSystemList when remoteSystemListIndex != -1: if
GetIndexFromSystemAddress returns -1, attempt to select a closeSocket from
socketList (if any) but do not read remoteSystemList[0] or otherwise assume a
valid systemIndex; if no socket is available, return early; when
remoteSystemListIndex is valid use
remoteSystemList[remoteSystemListIndex].rakNetSocket as before and then call
CloseConnectionInternal2 with the chosen socket.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Source/src/RakPeer.cpp`:
- Around line 1640-1673: CloseConnection currently coerces a -1 result from
GetIndexFromSystemAddress into 0 and then unconditionally reads
remoteSystemList[remoteSystemListIndex], which can crash or target the wrong
peer; change the logic to NOT rewrite -1 to 0 and to only dereference
remoteSystemList when remoteSystemListIndex != -1: if GetIndexFromSystemAddress
returns -1, attempt to select a closeSocket from socketList (if any) but do not
read remoteSystemList[0] or otherwise assume a valid systemIndex; if no socket
is available, return early; when remoteSystemListIndex is valid use
remoteSystemList[remoteSystemListIndex].rakNetSocket as before and then call
CloseConnectionInternal2 with the chosen socket.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9466dab-eb27-4dad-8b32-122f1ac4e05f

📥 Commits

Reviewing files that changed from the base of the PR and between 0d2a630 and 5da7708.

📒 Files selected for processing (7)
  • Samples/Tests/DisconnectReasonTest.cpp
  • Samples/Tests/DisconnectReasonTest.h
  • Samples/Tests/IncludeAllTests.h
  • Samples/Tests/Tests.cpp
  • Source/include/mafianet/peer.h
  • Source/include/mafianet/peerinterface.h
  • Source/src/RakPeer.cpp

Segfaultd added 3 commits June 3, 2026 20:12
Add a 'Disconnect with a reason' section to the connecting guide showing how
to attach a reason BitStream to CloseConnection() and read it back from the
notification packet, plus a cross-referencing note in the disconnect-debugging
handler example. The CloseConnection() API reference picks up the reasonData
parameter from the updated header doc comment.
Add Case 3 to DisconnectReasonTest exercising the
GetNumberOfBytesUsed() > 0 guard in NotifyAndFlagForShutdown: a
non-null but empty reason BitStream must still yield a bare 1-byte
ID_DISCONNECTION_NOTIFICATION, distinct from the nullptr path.
GetIndexFromSystemAddress returns -1 when the target isn't in the
remote system list. The old code rewrote that to 0 and then read
remoteSystemList[0].rakNetSocket unconditionally, which targets an
unrelated peer's slot (or crashes if the list is unallocated).

Resolve the socket without assuming a valid index: use the slot's
socket only when the index is valid, otherwise fall back to the
primary socket and bail out if none exists. Guard the downstream
systemIndex assignment so it no longer casts -1.
@Segfaultd Segfaultd merged commit 1a785a2 into master Jun 3, 2026
4 checks passed
@Segfaultd Segfaultd deleted the feat/disconnect-reason branch June 3, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: carry a disconnect reason in ID_DISCONNECTION_NOTIFICATION / CloseConnection

1 participant