diff --git a/src/protocol/OpenConnectionReply1.php b/src/protocol/OpenConnectionReply1.php index eaa227c..6dfaafa 100644 --- a/src/protocol/OpenConnectionReply1.php +++ b/src/protocol/OpenConnectionReply1.php @@ -16,17 +16,20 @@ namespace raklib\protocol; +use function is_int; + class OpenConnectionReply1 extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_OPEN_CONNECTION_REPLY_1; public int $serverID; - public bool $serverSecurity = false; + public bool $serverSecurity; + public ?int $cookie = null; public int $mtuSize; - public static function create(int $serverId, bool $serverSecurity, int $mtuSize) : self{ + public static function create(int $serverId, ?int $cookie, int $mtuSize) : self{ $result = new self; $result->serverID = $serverId; - $result->serverSecurity = $serverSecurity; + $result->cookie = $cookie; $result->mtuSize = $mtuSize; return $result; } @@ -34,7 +37,10 @@ public static function create(int $serverId, bool $serverSecurity, int $mtuSize) protected function encodePayload(PacketSerializer $out) : void{ $this->writeMagic($out); $out->putLong($this->serverID); - $out->putByte($this->serverSecurity ? 1 : 0); + $out->putByte($this->cookie !== null ? 1 : 0); + if ($this->cookie !== null) { + $out->putInt($this->cookie); + } $out->putShort($this->mtuSize); } @@ -42,6 +48,9 @@ protected function decodePayload(PacketSerializer $in) : void{ $this->readMagic($in); $this->serverID = $in->getLong(); $this->serverSecurity = $in->getByte() !== 0; + if ($this->serverSecurity) { + $this->cookie = $in->getInt(); + } $this->mtuSize = $in->getShort(); } } diff --git a/src/protocol/OpenConnectionRequest2.php b/src/protocol/OpenConnectionRequest2.php index bc22183..4cd1930 100644 --- a/src/protocol/OpenConnectionRequest2.php +++ b/src/protocol/OpenConnectionRequest2.php @@ -22,18 +22,29 @@ class OpenConnectionRequest2 extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_OPEN_CONNECTION_REQUEST_2; public int $clientID; + public ?int $cookie; + public bool $clientSupportsSecurity = false; // Always false for the vanilla client. public InternetAddress $serverAddress; public int $mtuSize; protected function encodePayload(PacketSerializer $out) : void{ $this->writeMagic($out); + if ($this->cookie !== null) { + $out->putInt($this->cookie); + $out->putBool($this->clientSupportsSecurity); + } $out->putAddress($this->serverAddress); $out->putShort($this->mtuSize); $out->putLong($this->clientID); } protected function decodePayload(PacketSerializer $in) : void{ + //$length = strlen($in->getRemaining()); // magic(16) + cookie(4) + clientSupportsSecurity(1) + serverAddress(??) + mtuSize(2) + clientID(8) $this->readMagic($in); + if ($this->cookie !== null) { + $this->cookie = $in->getInt(); + $this->clientSupportsSecurity = $in->getBool(); + } $this->serverAddress = $in->getAddress(); $this->mtuSize = $in->getShort(); $this->clientID = $in->getLong(); diff --git a/src/server/Server.php b/src/server/Server.php index 359ca1d..0d2fe5a 100644 --- a/src/server/Server.php +++ b/src/server/Server.php @@ -27,6 +27,7 @@ use raklib\protocol\NACK; use raklib\protocol\Packet; use raklib\protocol\PacketSerializer; +use raklib\utils\CookieCache; use raklib\utils\ExceptionTraceCleaner; use raklib\utils\InternetAddress; use function asort; @@ -77,6 +78,8 @@ class Server implements ServerInterface{ protected int $nextSessionId = 0; + public ?CookieCache $cookieCache = null; + /** * @phpstan-param positive-int $recvMaxSplitParts * @phpstan-param positive-int $recvMaxConcurrentSplits @@ -99,6 +102,9 @@ public function __construct( $this->socket->setBlocking(false); $this->unconnectedMessageHandler = new UnconnectedMessageHandler($this, $protocolAcceptor); + + // If you don't want to use security on the server, just delete this line. + $this->cookieCache = new CookieCache(); } public function getPort() : int{ @@ -113,6 +119,10 @@ public function getLogger() : \Logger{ return $this->logger; } + public function getCookieCache() : ?CookieCache { + return $this->cookieCache; + } + public function tickProcessor() : void{ $start = microtime(true); @@ -169,6 +179,9 @@ private function tick() : void{ foreach($this->sessions as $session){ $session->update($time); if($session->isFullyDisconnected()){ + if ($this->getCookieCache() instanceof CookieCache) { + $this->getCookieCache()->remove($session->getAddress()); + } $this->removeSessionInternal($session); } } diff --git a/src/server/UnconnectedMessageHandler.php b/src/server/UnconnectedMessageHandler.php index 2ab7bad..17b8f59 100644 --- a/src/server/UnconnectedMessageHandler.php +++ b/src/server/UnconnectedMessageHandler.php @@ -30,6 +30,7 @@ use raklib\protocol\UnconnectedPingOpenConnections; use raklib\protocol\UnconnectedPong; use raklib\utils\InternetAddress; +use raklib\utils\CookieCache; use function get_class; use function min; use function ord; @@ -81,10 +82,15 @@ private function handle(OfflineMessage $packet, InternetAddress $address) : bool $this->server->sendPacket(IncompatibleProtocolVersion::create($this->protocolAcceptor->getPrimaryVersion(), $this->server->getID()), $address); $this->server->getLogger()->notice("Refused connection from $address due to incompatible RakNet protocol version (version $packet->protocol)"); }else{ + $cookie = null; + if ($this->server->getCookieCache() instanceof CookieCache) { + $cookie = $this->server->getCookieCache()->add($address); + } //IP header size (20 bytes) + UDP header size (8 bytes) - $this->server->sendPacket(OpenConnectionReply1::create($this->server->getID(), false, $packet->mtuSize + 28), $address); + $this->server->sendPacket(OpenConnectionReply1::create($this->server->getID(), $cookie, $packet->mtuSize + 28), $address); } }elseif($packet instanceof OpenConnectionRequest2){ + // The client may not send such data even though serverSecurity is enabled, and if we try to decode this, we may encounter an error if($packet->serverAddress->getPort() === $this->server->getPort() or !$this->server->portChecking){ if($packet->mtuSize < Session::MIN_MTU_SIZE){ $this->server->getLogger()->debug("Not creating session for $address due to bad MTU size $packet->mtuSize"); @@ -97,6 +103,14 @@ private function handle(OfflineMessage $packet, InternetAddress $address) : bool $this->server->getLogger()->debug("Not creating session for $address due to session already opened"); return true; } + if ($this->server->getCookieCache() instanceof CookieCache) { // womp womp + if ($packet->cookie === null || !$this->server->getCookieCache()->check($address, $packet->cookie)) { + // Disconnect if OpenConnectionReply1 and the cookie in the OpenConnectionRequest2 packet do not match + $this->server->getLogger()->debug("Not creating session for $address due to session mismatched cookies"); + return true; + } + + } $mtuSize = min($packet->mtuSize, $this->server->getMaxMtuSize()); //Max size, do not allow creating large buffers to fill server memory $this->server->sendPacket(OpenConnectionReply2::create($this->server->getID(), $address, $mtuSize, false), $address); $this->server->createSession($address, $packet->clientID, $mtuSize); diff --git a/src/utils/CookieCache.php b/src/utils/CookieCache.php new file mode 100644 index 0000000..fb955c9 --- /dev/null +++ b/src/utils/CookieCache.php @@ -0,0 +1,59 @@ + + * + * RakLib is not affiliated with Jenkins Software LLC nor RakNet. + * + * RakLib is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +declare(strict_types=1); + +namespace raklib\utils; + +use raklib\utils\InternetAddress; +use pocketmine\utils\Limits; +use function random_int; + +final class CookieCache{ + + /** + * @var array $cookies + */ + private array $cookies = []; + + public function check(InternetAddress $address, int $cookie) : bool{ + $addressStr = $address->toString(); + + if (isset($this->cookies[$addressStr])) { + // If it checks the Cookie, it means that it is in the OpenConnectionRequest2 phase, and we can delete it from memory. + if ($this->cookies[$addressStr] === $cookie) { + unset($this->cookies[$addressStr]); + return true; + } + } // Is there any chance that this is something else? + return false; + } + + public function add(InternetAddress $address) : int{ + $cookie = $this->generate($address); + $this->cookies[$address->toString()] = $cookie; + return $cookie; + } + + public function remove(InternetAddress $address) : void{ + $addressStr = $address->toString(); + if (isset($this->cookies[$addressStr])) { + unset($this->cookies[$addressStr]); + } + } + + private function generate(InternetAddress $address) : int{ + return random_int(0, Limits::UINT32_MAX); + } +}