From 03b1ea766aa5821ebc18eee86506317bd91a7d32 Mon Sep 17 00:00:00 2001 From: Dylan T Date: Fri, 4 Dec 2020 21:21:25 +0000 Subject: [PATCH] Added a DedicatedQueryNetworkInterface to ensure Query functionality when RakLibInterface is disabled (#3942) --- src/Server.php | 10 +- .../query/DedicatedQueryNetworkInterface.php | 163 ++++++++++++++++++ 2 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 src/network/query/DedicatedQueryNetworkInterface.php diff --git a/src/Server.php b/src/Server.php index 0e2084762..c1be895c9 100644 --- a/src/Server.php +++ b/src/Server.php @@ -60,6 +60,7 @@ use pocketmine\network\mcpe\raklib\RakLibInterface; use pocketmine\network\Network; use pocketmine\network\query\QueryHandler; use pocketmine\network\query\QueryInfo; +use pocketmine\network\query\DedicatedQueryNetworkInterface; use pocketmine\network\upnp\UPnP; use pocketmine\permission\BanList; use pocketmine\permission\DefaultPermissions; @@ -1037,10 +1038,15 @@ class Server{ $this->enablePlugins(PluginEnableOrder::POSTWORLD()); - $this->network->registerInterface(new RakLibInterface($this)); + $useQuery = $this->configGroup->getConfigBool("enable-query", true); + if(!$this->network->registerInterface(new RakLibInterface($this)) && $useQuery){ + //RakLib would normally handle the transport for Query packets + //if it's not registered we need to make sure Query still works + $this->network->registerInterface(new DedicatedQueryNetworkInterface($this->getIp(), $this->getPort(), new \PrefixedLogger($this->logger, "Dedicated Query Interface"))); + } $this->logger->info($this->getLanguage()->translateString("pocketmine.server.networkStart", [$this->getIp(), $this->getPort()])); - if($this->configGroup->getConfigBool("enable-query", true)){ + if($useQuery){ $this->network->registerRawPacketHandler(new QueryHandler($this)); } diff --git a/src/network/query/DedicatedQueryNetworkInterface.php b/src/network/query/DedicatedQueryNetworkInterface.php new file mode 100644 index 000000000..5a083a9e3 --- /dev/null +++ b/src/network/query/DedicatedQueryNetworkInterface.php @@ -0,0 +1,163 @@ + timeout time + * @phpstan-var array + */ + private $blockedIps = []; + /** @var string[] */ + private $rawPacketPatterns = []; + + public function __construct(string $ip, int $port, \Logger $logger){ + $this->ip = $ip; + $this->port = $port; + $this->logger = $logger; + + $socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + if($socket === false){ + throw new \RuntimeException("Failed to create socket"); + } + $this->socket = $socket; + } + + public function start() : void{ + if(!@socket_bind($this->socket, $this->ip, $this->port)){ + $error = socket_last_error($this->socket); + if($error === SOCKET_EADDRINUSE){ //platform error messages aren't consistent + throw new \RuntimeException("Failed to bind socket: Something else is already running on $this->ip $this->port", $error); + } + throw new \RuntimeException("Failed to bind to $this->ip $this->port: " . trim(socket_strerror($error)), $error); + } + socket_set_nonblock($this->socket); + $this->logger->info("Running on $this->ip $this->port"); + } + + public function setName(string $name) : void{ + //NOOP + } + + public function tick() : void{ + $r = [$this->socket]; + $w = null; + $e = null; + if(@socket_select($r, $w, $e, 0, 0) === 1){ + $address = ""; + $port = 0; + $buffer = ""; + while(true){ + $bytes = @socket_recvfrom($this->socket, $buffer, 65535, 0, $address, $port); + if($bytes !== false){ + if(isset($this->blockedIps[$address]) && $this->blockedIps[$address] > time()){ + $this->logger->debug("Dropped packet from banned address $address"); + continue; + } + foreach($this->rawPacketPatterns as $pattern){ + if(preg_match($pattern, $buffer) === 1){ + $this->network->processRawPacket($this, $address, $port, $buffer); + break; + } + } + }else{ + $errno = socket_last_error($this->socket); + if($errno === SOCKET_EWOULDBLOCK){ + break; + } + if($errno !== SOCKET_ECONNRESET){ //remote peer disappeared unexpectedly, this might spam like crazy so we don't log it + $this->logger->debug("Failed to recv (errno $errno): " . trim(socket_strerror($errno))); + } + } + } + } + } + + public function blockAddress(string $address, int $timeout = 300) : void{ + $this->blockedIps[$address] = $timeout > 0 ? time() + $timeout : PHP_INT_MAX; + } + + public function unblockAddress(string $address) : void{ + unset($this->blockedIps[$address]); + } + + public function setNetwork(Network $network) : void{ + $this->network = $network; + } + + public function sendRawPacket(string $address, int $port, string $payload) : void{ + if(@socket_sendto($this->socket, $payload, strlen($payload), 0, $address, $port) === false){ + $errno = socket_last_error($this->socket); + throw new \RuntimeException("Failed to send to $address $port (errno $errno): " . trim(socket_strerror($errno)), $errno); + } + } + + public function addRawPacketFilter(string $regex) : void{ + $this->rawPacketPatterns[] = $regex; + } + + public function shutdown() : void{ + @socket_close($this->socket); + } +}