48 private const RAKLIB_TPS = 100;
49 private const RAKLIB_TIME_PER_TICK = 1 / self::RAKLIB_TPS;
50 private const BLOCK_MESSAGE_SUPPRESSION_THRESHOLD = 2;
51 private const PACKET_ERROR_SUPPRESSION_THRESHOLD = 2;
53 protected int $receiveBytes = 0;
54 protected int $sendBytes = 0;
57 protected array $sessionsByAddress = [];
59 protected array $sessions = [];
63 protected string $name =
"";
65 protected int $packetLimit = 200;
67 protected bool $shutdown =
false;
69 protected int $ticks = 0;
72 protected array $block = [];
74 protected array $ipSec = [];
76 private int $blockedSinceLastUpdate = 0;
78 private int $packetErrorsSinceLastUpdate = 0;
81 protected array $rawPacketFilters = [];
83 public bool $portChecking =
false;
85 protected int $nextSessionId = 0;
92 protected int $serverId,
95 protected int $maxMtuSize,
100 private int $recvMaxSplitParts = ServerSession::DEFAULT_MAX_SPLIT_PART_COUNT,
101 private int $recvMaxConcurrentSplits = ServerSession::DEFAULT_MAX_CONCURRENT_SPLIT_COUNT,
102 private int $blockMessageSuppressionThreshold = self::BLOCK_MESSAGE_SUPPRESSION_THRESHOLD,
103 private int $packetErrorSuppressionThreshold = self::PACKET_ERROR_SUPPRESSION_THRESHOLD,
104 private bool $blockIpOnPacketErrors =
true
106 if($maxMtuSize < Session::MIN_MTU_SIZE){
107 throw new \InvalidArgumentException(
"MTU size must be at least " . Session::MIN_MTU_SIZE .
", got $maxMtuSize");
109 $this->socket->setBlocking(
false);
114 public function getPort() : int{
115 return $this->socket->getBindAddress()->getPort();
118 public function getMaxMtuSize() : int{
119 return $this->maxMtuSize;
122 public function getLogger() : \
Logger{
123 return $this->logger;
126 public function tickProcessor() : void{
127 $start = microtime(true);
134 $stream = !$this->shutdown;
135 for($i = 0; $i < 100 && $stream && !$this->shutdown; ++$i){
136 $stream = $this->eventSource->process($this);
140 for($i = 0; $i < 100 && $socket; ++$i){
141 $socket = $this->receivePacket();
143 }
while($stream || $socket);
147 $time = microtime(
true) - $start;
148 if($time < self::RAKLIB_TIME_PER_TICK){
149 @time_sleep_until(microtime(
true) + self::RAKLIB_TIME_PER_TICK - $time);
157 $this->shutdown = true;
159 while($this->eventSource->process($this)){
165 foreach($this->sessions as $session){
166 $session->initiateDisconnect(DisconnectReason::SERVER_SHUTDOWN);
169 while(count($this->sessions) > 0){
170 $this->tickProcessor();
173 $this->socket->close();
174 $this->logger->debug(
"Graceful shutdown complete");
177 private function tick() : void{
178 $time = microtime(true);
179 foreach($this->sessions as $session){
180 $session->update($time);
181 if($session->isFullyDisconnected()){
182 $this->removeSessionInternal($session);
188 if(!$this->shutdown and ($this->ticks % self::RAKLIB_TPS) === 0){
189 if($this->sendBytes > 0 or $this->receiveBytes > 0){
190 $this->eventListener->onBandwidthStatsUpdate($this->sendBytes, $this->receiveBytes);
191 $this->sendBytes = 0;
192 $this->receiveBytes = 0;
195 $packetErrorsWithoutMessage = $this->packetErrorsSinceLastUpdate - $this->packetErrorSuppressionThreshold;
196 if($packetErrorsWithoutMessage > 0){
197 $this->logger->warning(
"$packetErrorsWithoutMessage suppressed packet errors - RakLib may be under attack");
199 $this->packetErrorsSinceLastUpdate = 0;
201 $ipsBlockedWithoutMessage = $this->blockedSinceLastUpdate - $this->blockMessageSuppressionThreshold;
202 if($ipsBlockedWithoutMessage > 0){
203 $this->logger->warning(
"$ipsBlockedWithoutMessage more IP addresses were blocked - RakLib may be under attack");
205 $this->blockedSinceLastUpdate = 0;
207 if(count($this->block) > 0){
210 foreach($this->block as $address => $timeout){
211 if($timeout <= $now){
212 unset($this->block[$address]);
224 private function receivePacket() : bool{
226 $buffer = $this->socket->readPacket($addressIp, $addressPort);
227 }
catch(SocketException $e){
228 $error = $e->getCode();
229 if($error === SOCKET_ECONNRESET){
233 $this->logger->debug($e->getMessage());
236 if($buffer ===
null){
239 assert($addressIp !==
null,
"Can't be null if we got a buffer");
240 assert($addressPort !==
null,
"Can't be null if we got a buffer");
242 $len = strlen($buffer);
244 $this->receiveBytes += $len;
245 if(isset($this->block[$addressIp])){
249 if(isset($this->ipSec[$addressIp])){
250 if(++$this->ipSec[$addressIp] >= $this->packetLimit){
251 $this->blockAddress($addressIp);
255 $this->ipSec[$addressIp] = 1;
262 $address =
new InternetAddress($addressIp, $addressPort, $this->socket->getBindAddress()->getVersion());
264 $session = $this->getSessionByAddress($address);
265 if($session !==
null){
266 $header = ord($buffer[0]);
267 if(($header & Datagram::BITFLAG_VALID) !== 0){
268 if(($header & Datagram::BITFLAG_ACK) !== 0){
270 }elseif(($header & Datagram::BITFLAG_NAK) !== 0){
271 $packet =
new NACK();
273 $packet =
new Datagram();
275 $packet->decode(
new PacketSerializer($buffer));
277 $session->handlePacket($packet);
278 }
catch(PacketHandlingException $e){
279 $session->getLogger()->error(
"Error receiving packet: " . $e->getMessage());
280 $session->forciblyDisconnect($e->getDisconnectReason());
283 }elseif($session->isConnected()){
286 $this->logger->debug(
"Ignored unconnected packet from $address due to session already opened (0x" . bin2hex($buffer[0]) .
")");
291 if(!$this->shutdown){
292 if(!($handled = $this->unconnectedMessageHandler->handleRaw($buffer, $address))){
293 foreach($this->rawPacketFilters as $pattern){
294 if(preg_match($pattern, $buffer) > 0){
296 $this->eventListener->onRawPacketReceive($address->getIp(), $address->getPort(), $buffer);
303 $this->logger->debug(
"Ignored packet from $address due to no session opened (0x" . bin2hex($buffer[0]) .
")");
306 }
catch(BinaryDataException $e){
307 if($this->packetErrorsSinceLastUpdate < $this->packetErrorSuppressionThreshold){
308 $logFn =
function() use ($address, $e, $buffer) :
void{
309 $this->logger->debug(
"Packet from $address (" . strlen($buffer) .
" bytes): 0x" . bin2hex($buffer));
310 $this->logger->debug(get_class($e) .
": " . $e->getMessage() .
" in " . $e->getFile() .
" on line " . $e->getLine());
311 foreach($this->traceCleaner->getTrace(0, $e->getTrace()) as $line){
312 $this->logger->debug($line);
314 $this->logger->error(
"Bad packet from $address: " . $e->getMessage());
317 $this->logger->buffer($logFn);
322 $this->packetErrorsSinceLastUpdate++;
323 if($this->blockIpOnPacketErrors){
324 $this->blockAddress($address->getIp(), 5);
331 public function sendPacket(Packet $packet, InternetAddress $address) : void{
332 $out = new PacketSerializer();
333 $packet->encode($out);
335 $this->sendBytes += $this->socket->writePacket($out->getBuffer(), $address->getIp(), $address->getPort());
336 }
catch(SocketException $e){
337 $this->logger->debug($e->getMessage());
341 public function getEventListener() : ServerEventListener{
342 return $this->eventListener;
345 public function sendEncapsulated(
int $sessionId, EncapsulatedPacket $packet,
bool $immediate =
false) : void{
346 $session = $this->sessions[$sessionId] ?? null;
347 if($session !==
null and $session->isConnected()){
348 $session->addEncapsulatedToQueue($packet, $immediate);
352 public function sendRaw(
string $address,
int $port,
string $payload) : void{
354 $this->socket->writePacket($payload, $address, $port);
355 }
catch(SocketException $e){
356 $this->logger->debug($e->getMessage());
360 public function closeSession(
int $sessionId) : void{
361 if(isset($this->sessions[$sessionId])){
362 $this->sessions[$sessionId]->initiateDisconnect(DisconnectReason::SERVER_DISCONNECT);
366 public function setName(
string $name) : void{
370 public function setPortCheck(
bool $value) : void{
371 $this->portChecking = $value;
374 public function setPacketsPerTickLimit(
int $limit) : void{
375 $this->packetLimit = $limit;
378 public function blockAddress(
string $address,
int $timeout = 300) : void{
379 $final = time() + $timeout;
380 if(!isset($this->block[$address]) or $timeout === -1){
382 $final = PHP_INT_MAX;
384 if($this->blockedSinceLastUpdate < $this->blockMessageSuppressionThreshold){
387 $this->logger->notice(
"Blocked $address" . ($timeout === -1 ?
" forever" :
" for $timeout seconds"));
389 $this->block[$address] = $final;
390 $this->blockedSinceLastUpdate++;
391 }elseif($this->block[$address] < $final){
392 $this->block[$address] = $final;
396 public function unblockAddress(
string $address) : void{
397 unset($this->block[$address]);
398 $this->logger->debug(
"Unblocked $address");
401 public function addRawPacketFilter(
string $regex) : void{
402 $this->rawPacketFilters[] = $regex;
405 public function getSessionByAddress(InternetAddress $address) : ?ServerSession{
406 return $this->sessionsByAddress[$address->toString()] ?? null;
409 public function sessionExists(InternetAddress $address) : bool{
410 return isset($this->sessionsByAddress[$address->toString()]);
413 public function createSession(InternetAddress $address,
int $clientId,
int $mtuSize) : ServerSession{
414 $existingSession = $this->sessionsByAddress[$address->toString()] ?? null;
415 if($existingSession !==
null){
416 $existingSession->forciblyDisconnect(DisconnectReason::CLIENT_RECONNECT);
417 $this->removeSessionInternal($existingSession);
420 $this->checkSessions();
422 while(isset($this->sessions[$this->nextSessionId])){
423 $this->nextSessionId++;
424 $this->nextSessionId &= 0x7fffffff;
427 $session =
new ServerSession($this, $this->logger, clone $address, $clientId, $mtuSize, $this->nextSessionId, $this->recvMaxSplitParts, $this->recvMaxConcurrentSplits);
428 $this->sessionsByAddress[$address->toString()] = $session;
429 $this->sessions[$this->nextSessionId] = $session;
430 $this->logger->debug(
"Created session for $address with MTU size $mtuSize");
435 private function removeSessionInternal(ServerSession $session) : void{
436 unset($this->sessionsByAddress[$session->getAddress()->toString()], $this->sessions[$session->getInternalId()]);
439 public function openSession(ServerSession $session) : void{
440 $address = $session->getAddress();
441 $this->eventListener->onClientConnect($session->getInternalId(), $address->getIp(), $address->getPort(), $session->getID());
444 private function checkSessions() : void{
445 if(count($this->sessions) > 4096){
446 foreach($this->sessions as $sessionId => $session){
447 if($session->isTemporary()){
448 $this->removeSessionInternal($session);
449 if(count($this->sessions) <= 4096){
457 public function getName() : string{
461 public function getID() : int{
462 return $this->serverId;