Skip to content

Commit bc2e0c7

Browse files
committed
Merge remote-tracking branch 'origin/stable' into major-next
2 parents ed32ddb + 5aff21f commit bc2e0c7

File tree

7 files changed

+165
-36
lines changed

7 files changed

+165
-36
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @pmmp/server-developers

src/generic/DisconnectReason.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ final class DisconnectReason{
2222
public const PEER_TIMEOUT = 2;
2323
public const CLIENT_RECONNECT = 3;
2424
public const SERVER_SHUTDOWN = 4; //TODO: do we really need a separate reason for this in addition to SERVER_DISCONNECT?
25+
public const SPLIT_PACKET_TOO_LARGE = 5;
26+
public const SPLIT_PACKET_TOO_MANY_CONCURRENT = 6;
27+
public const SPLIT_PACKET_INVALID_PART_INDEX = 7;
28+
public const SPLIT_PACKET_INCONSISTENT_HEADER = 8;
2529

2630
public static function toString(int $reason) : string{
2731
return match($reason){
@@ -30,6 +34,10 @@ public static function toString(int $reason) : string{
3034
self::PEER_TIMEOUT => "timeout",
3135
self::CLIENT_RECONNECT => "new session established on same address and port",
3236
self::SERVER_SHUTDOWN => "server shutdown",
37+
self::SPLIT_PACKET_TOO_LARGE => "received packet split into more parts than allowed",
38+
self::SPLIT_PACKET_TOO_MANY_CONCURRENT => "too many received split packets being reassembled at once",
39+
self::SPLIT_PACKET_INVALID_PART_INDEX => "invalid split packet part index",
40+
self::SPLIT_PACKET_INCONSISTENT_HEADER => "received split packet header inconsistent with previous fragments",
3341
default => "Unknown reason $reason"
3442
};
3543
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of RakLib.
5+
* Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/RakLib>
6+
*
7+
* RakLib is not affiliated with Jenkins Software LLC nor RakNet.
8+
*
9+
* RakLib is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU General Public License as published by
11+
* the Free Software Foundation, either version 3 of the License, or
12+
* (at your option) any later version.
13+
*/
14+
15+
declare(strict_types=1);
16+
17+
namespace raklib\generic;
18+
19+
class PacketHandlingException extends \RuntimeException{
20+
21+
/** @phpstan-var DisconnectReason::* */
22+
private int $disconnectReason;
23+
24+
/**
25+
* @phpstan-param DisconnectReason::* $disconnectReason
26+
*/
27+
public function __construct(string $message, int $disconnectReason, int $code = 0, ?\Throwable $previous = null){
28+
$this->disconnectReason = $disconnectReason;
29+
parent::__construct($message, $code, $previous);
30+
}
31+
32+
/**
33+
* @phpstan-return DisconnectReason::*
34+
*/
35+
public function getDisconnectReason() : int{
36+
return $this->disconnectReason;
37+
}
38+
}

src/generic/ReceiveReliabilityLayer.php

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,33 +84,36 @@ private function handleEncapsulatedPacketRoute(EncapsulatedPacket $pk) : void{
8484

8585
/**
8686
* Processes a split part of an encapsulated packet.
87+
* If an error occurs (limit exceeded, inconsistent header, etc.) a PacketHandlingException is thrown.
88+
*
89+
* An error processing a split packet means we can't fulfill reliability promises, which at best would lead to some
90+
* packets not arriving, and in the worst case (reliable-ordered) cause no more packets to be processed.
91+
* Therefore, the owning session MUST disconnect the peer if this exception is thrown.
8792
*
8893
* @return null|EncapsulatedPacket Reassembled packet if we have all the parts, null otherwise.
94+
* @throws PacketHandlingException if there was a problem with processing the split packet.
8995
*/
9096
private function handleSplit(EncapsulatedPacket $packet) : ?EncapsulatedPacket{
9197
if($packet->splitInfo === null){
9298
return $packet;
9399
}
94100
$totalParts = $packet->splitInfo->getTotalPartCount();
95101
$partIndex = $packet->splitInfo->getPartIndex();
96-
if(
97-
$totalParts >= $this->maxSplitPacketPartCount or $totalParts < 0 or
98-
$partIndex >= $totalParts or $partIndex < 0
99-
){
100-
$this->logger->debug("Invalid split packet part, too many parts or invalid split index (part index $partIndex, part count $totalParts)");
101-
return null;
102+
if($totalParts >= $this->maxSplitPacketPartCount || $totalParts < 0){
103+
throw new PacketHandlingException("Invalid split packet part count ($totalParts)", DisconnectReason::SPLIT_PACKET_TOO_LARGE);
104+
}
105+
if($partIndex >= $totalParts || $partIndex < 0){
106+
throw new PacketHandlingException("Invalid split packet part index (part index $partIndex, part count $totalParts)", DisconnectReason::SPLIT_PACKET_INVALID_PART_INDEX);
102107
}
103108

104109
$splitId = $packet->splitInfo->getId();
105110
if(!isset($this->splitPackets[$splitId])){
106111
if(count($this->splitPackets) >= $this->maxConcurrentSplitPackets){
107-
$this->logger->debug("Ignored split packet part because reached concurrent split packet limit of $this->maxConcurrentSplitPackets");
108-
return null;
112+
throw new PacketHandlingException("Exceeded concurrent split packet reassembly limit of $this->maxConcurrentSplitPackets", DisconnectReason::SPLIT_PACKET_TOO_MANY_CONCURRENT);
109113
}
110114
$this->splitPackets[$splitId] = array_fill(0, $totalParts, null);
111115
}elseif(count($this->splitPackets[$splitId]) !== $totalParts){
112-
$this->logger->debug("Wrong split count $totalParts for split packet $splitId, expected " . count($this->splitPackets[$splitId]));
113-
return null;
116+
throw new PacketHandlingException("Wrong split count $totalParts for split packet $splitId, expected " . count($this->splitPackets[$splitId]), DisconnectReason::SPLIT_PACKET_INCONSISTENT_HEADER);
114117
}
115118

116119
$this->splitPackets[$splitId][$partIndex] = $packet;
@@ -142,6 +145,9 @@ private function handleSplit(EncapsulatedPacket $packet) : ?EncapsulatedPacket{
142145
return $pk;
143146
}
144147

148+
/**
149+
* @throws PacketHandlingException
150+
*/
145151
private function handleEncapsulatedPacket(EncapsulatedPacket $packet) : void{
146152
if($packet->messageIndex !== null){
147153
//check for duplicates or out of range
@@ -209,6 +215,9 @@ private function handleEncapsulatedPacket(EncapsulatedPacket $packet) : void{
209215
}
210216
}
211217

218+
/**
219+
* @throws PacketHandlingException
220+
*/
212221
public function onDatagram(Datagram $packet) : void{
213222
if($packet->seqNumber < $this->windowStart or $packet->seqNumber > $this->windowEnd or isset($this->ACKQueue[$packet->seqNumber])){
214223
$this->logger->debug("Received duplicate or out-of-window packet (sequence number $packet->seqNumber, window " . $this->windowStart . "-" . $this->windowEnd . ")");

src/generic/SendReliabilityLayer.php

Lines changed: 85 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,21 @@
2323
use raklib\protocol\PacketReliability;
2424
use raklib\protocol\SplitPacketInfo;
2525
use function array_fill;
26+
use function array_push;
27+
use function assert;
2628
use function count;
29+
use function microtime;
2730
use function str_split;
2831
use function strlen;
29-
use function time;
3032

3133
final class SendReliabilityLayer{
3234
private const DATAGRAM_MTU_OVERHEAD = 36 + Datagram::HEADER_SIZE; //IP header (20 bytes) + UDP header (8 bytes) + RakNet weird (8 bytes) = 36
3335
private const MIN_POSSIBLE_PACKET_SIZE_LIMIT = Session::MIN_MTU_SIZE - self::DATAGRAM_MTU_OVERHEAD;
36+
/**
37+
* Delay in seconds before an unacked packet is retransmitted.
38+
* TODO: Replace this with dynamic calculation based on roundtrip times (that's a complex task for another time)
39+
*/
40+
private const UNACKED_RETRANSMIT_DELAY = 2.0;
3441

3542
/** @var EncapsulatedPacket[] */
3643
private array $sendQueue = [];
@@ -41,12 +48,23 @@ final class SendReliabilityLayer{
4148

4249
private int $messageIndex = 0;
4350

51+
private int $reliableWindowStart;
52+
private int $reliableWindowEnd;
53+
/**
54+
* @var bool[] message index => acked
55+
* @phpstan-var array<int, bool>
56+
*/
57+
private array $reliableWindow = [];
58+
4459
/** @var int[] */
4560
private array $sendOrderedIndex;
4661
/** @var int[] */
4762
private array $sendSequencedIndex;
4863

49-
/** @var ReliableCacheEntry[] */
64+
/** @var EncapsulatedPacket[] */
65+
private array $reliableBacklog = [];
66+
67+
/** @var EncapsulatedPacket[] */
5068
private array $resendQueue = [];
5169

5270
/** @var ReliableCacheEntry[] */
@@ -60,18 +78,22 @@ final class SendReliabilityLayer{
6078

6179
/**
6280
* @phpstan-param int<Session::MIN_MTU_SIZE, max> $mtuSize
63-
* @phpstan-param \Closure(Datagram) : void $sendDatagramCallback
64-
* @phpstan-param \Closure(int) : void $onACK
81+
* @phpstan-param \Closure(Datagram) : void $sendDatagramCallback
82+
* @phpstan-param \Closure(int) : void $onACK
6583
*/
6684
public function __construct(
6785
private int $mtuSize,
6886
private \Closure $sendDatagramCallback,
69-
private \Closure $onACK
87+
private \Closure $onACK,
88+
private int $reliableWindowSize = 512,
7089
){
7190
$this->sendOrderedIndex = array_fill(0, PacketReliability::MAX_ORDER_CHANNELS, 0);
7291
$this->sendSequencedIndex = array_fill(0, PacketReliability::MAX_ORDER_CHANNELS, 0);
7392

7493
$this->maxDatagramPayloadSize = $this->mtuSize - self::DATAGRAM_MTU_OVERHEAD;
94+
95+
$this->reliableWindowStart = 0;
96+
$this->reliableWindowEnd = $this->reliableWindowSize;
7597
}
7698

7799
/**
@@ -102,6 +124,19 @@ public function sendQueue() : void{
102124
}
103125

104126
private function addToQueue(EncapsulatedPacket $pk, bool $immediate) : void{
127+
if(PacketReliability::isReliable($pk->reliability)){
128+
if($pk->messageIndex === null || $pk->messageIndex < $this->reliableWindowStart){
129+
throw new \InvalidArgumentException("Cannot send a reliable packet with message index less than the window start ($pk->messageIndex < $this->reliableWindowStart)");
130+
}
131+
if($pk->messageIndex >= $this->reliableWindowEnd){
132+
//If we send this now, the client's reliable window may overflow, causing the packet to need redelivery
133+
$this->reliableBacklog[$pk->messageIndex] = $pk;
134+
return;
135+
}
136+
137+
$this->reliableWindow[$pk->messageIndex] = false;
138+
}
139+
105140
if($pk->identifierACK !== null and $pk->messageIndex !== null){
106141
$this->needACK[$pk->identifierACK][$pk->messageIndex] = $pk->messageIndex;
107142
}
@@ -152,6 +187,7 @@ public function addEncapsulatedToQueue(EncapsulatedPacket $packet, bool $immedia
152187
$pk->splitInfo = new SplitPacketInfo($splitID, $count, $bufferCount);
153188
$pk->reliability = $packet->reliability;
154189
$pk->buffer = $buffer;
190+
$pk->identifierACK = $packet->identifierACK;
155191

156192
if($pk->reliability->isReliable()){
157193
$pk->messageIndex = $this->messageIndex++;
@@ -171,11 +207,26 @@ public function addEncapsulatedToQueue(EncapsulatedPacket $packet, bool $immedia
171207
}
172208
}
173209

210+
private function updateReliableWindow() : void{
211+
while(
212+
isset($this->reliableWindow[$this->reliableWindowStart]) && //this messageIndex has been used
213+
$this->reliableWindow[$this->reliableWindowStart] === true //we received an ack for this messageIndex
214+
){
215+
unset($this->reliableWindow[$this->reliableWindowStart]);
216+
$this->reliableWindowStart++;
217+
$this->reliableWindowEnd++;
218+
}
219+
}
220+
174221
public function onACK(ACK $packet) : void{
175222
foreach($packet->packets as $seq){
176223
if(isset($this->reliableCache[$seq])){
177224
foreach($this->reliableCache[$seq]->getPackets() as $pk){
178-
if($pk->identifierACK !== null and $pk->messageIndex !== null){
225+
assert($pk->messageIndex !== null && $pk->messageIndex >= $this->reliableWindowStart && $pk->messageIndex < $this->reliableWindowEnd);
226+
$this->reliableWindow[$pk->messageIndex] = true;
227+
$this->updateReliableWindow();
228+
229+
if($pk->identifierACK !== null){
179230
unset($this->needACK[$pk->identifierACK][$pk->messageIndex]);
180231
if(count($this->needACK[$pk->identifierACK]) === 0){
181232
unset($this->needACK[$pk->identifierACK]);
@@ -191,8 +242,9 @@ public function onACK(ACK $packet) : void{
191242
public function onNACK(NACK $packet) : void{
192243
foreach($packet->packets as $seq){
193244
if(isset($this->reliableCache[$seq])){
194-
//TODO: group resends if the resulting datagram is below the MTU
195-
$this->resendQueue[] = $this->reliableCache[$seq];
245+
foreach($this->reliableCache[$seq]->getPackets() as $pk){
246+
$this->resendQueue[] = $pk;
247+
}
196248
unset($this->reliableCache[$seq]);
197249
}
198250
}
@@ -201,34 +253,42 @@ public function onNACK(NACK $packet) : void{
201253
public function needsUpdate() : bool{
202254
return (
203255
count($this->sendQueue) !== 0 or
256+
count($this->reliableBacklog) !== 0 or
204257
count($this->resendQueue) !== 0 or
205258
count($this->reliableCache) !== 0
206259
);
207260
}
208261

209262
public function update() : void{
210-
if(count($this->resendQueue) > 0){
211-
$limit = 16;
212-
foreach($this->resendQueue as $k => $pk){
213-
$this->sendDatagram($pk->getPackets());
214-
unset($this->resendQueue[$k]);
215-
216-
if(--$limit <= 0){
217-
break;
218-
}
263+
$retransmitOlderThan = microtime(true) - self::UNACKED_RETRANSMIT_DELAY;
264+
foreach($this->reliableCache as $seq => $pk){
265+
if($pk->getTimestamp() < $retransmitOlderThan){
266+
//behave as if a NACK was received
267+
array_push($this->resendQueue, ...$pk->getPackets());
268+
unset($this->reliableCache[$seq]);
269+
}else{
270+
break;
219271
}
272+
}
220273

221-
if(count($this->resendQueue) > ReceiveReliabilityLayer::$WINDOW_SIZE){
222-
$this->resendQueue = [];
274+
if(count($this->resendQueue) > 0){
275+
foreach($this->resendQueue as $pk){
276+
//resends should always be within the reliable window
277+
$this->addToQueue($pk, false);
223278
}
279+
$this->resendQueue = [];
224280
}
225281

226-
foreach($this->reliableCache as $seq => $pk){
227-
if($pk->getTimestamp() < (time() - 8)){
228-
$this->resendQueue[] = $pk;
229-
unset($this->reliableCache[$seq]);
230-
}else{
231-
break;
282+
if(count($this->reliableBacklog) > 0){
283+
foreach($this->reliableBacklog as $k => $pk){
284+
assert($pk->messageIndex !== null && $pk->messageIndex >= $this->reliableWindowStart);
285+
if($pk->messageIndex >= $this->reliableWindowEnd){
286+
//we can't send this packet yet, the client's reliable window will drop it
287+
break;
288+
}
289+
290+
$this->addToQueue($pk, false);
291+
unset($this->reliableBacklog[$k]);
232292
}
233293
}
234294

src/generic/Session.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ protected function getRakNetTimeMS() : int{
155155
return intdiv(hrtime(true), 1_000_000);
156156
}
157157

158+
public function getLogger() : \Logger{
159+
return $this->logger;
160+
}
161+
158162
public function getAddress() : InternetAddress{
159163
return $this->address;
160164
}
@@ -275,6 +279,9 @@ private function handlePong(int $sendPingTime, int $sendPongTime) : void{
275279
}
276280
}
277281

282+
/**
283+
* @throws PacketHandlingException
284+
*/
278285
public function handlePacket(Packet $packet) : void{
279286
$this->isActive = true;
280287
$this->lastUpdate = microtime(true);

src/server/Server.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use raklib\generic\DisconnectReason;
2121
use raklib\generic\Session;
2222
use raklib\generic\SocketException;
23+
use raklib\generic\PacketHandlingException;
2324
use raklib\protocol\ACK;
2425
use raklib\protocol\Datagram;
2526
use raklib\protocol\EncapsulatedPacket;
@@ -247,7 +248,12 @@ private function receivePacket() : bool{
247248
$packet = new Datagram();
248249
}
249250
$packet->decode(new PacketSerializer($buffer));
250-
$session->handlePacket($packet);
251+
try{
252+
$session->handlePacket($packet);
253+
}catch(PacketHandlingException $e){
254+
$session->getLogger()->error("Error receiving packet: " . $e->getMessage());
255+
$session->forciblyDisconnect($e->getDisconnectReason());
256+
}
251257
return true;
252258
}elseif($session->isConnected()){
253259
//allows unconnected packets if the session is stuck in DISCONNECTING state, useful if the client

0 commit comments

Comments
 (0)