mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-08 19:02:59 +00:00
Compare commits
4 Commits
5.33.1
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
fc072b05d6 | |||
2589fcb31d | |||
8d255a6512 | |||
41789bc67a |
@ -37,7 +37,7 @@
|
||||
"pocketmine/bedrock-data": "~4.0.0+bedrock-1.21.60",
|
||||
"pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50",
|
||||
"pocketmine/bedrock-protocol": "~36.0.0+bedrock-1.21.60",
|
||||
"pocketmine/binaryutils": "^0.2.1",
|
||||
"pocketmine/binaryutils": "dev-experimental/read-ops-accounting as 0.2.6",
|
||||
"pocketmine/callback-validator": "^1.0.2",
|
||||
"pocketmine/color": "^0.3.0",
|
||||
"pocketmine/errorhandler": "^0.7.0",
|
||||
|
33
composer.lock
generated
33
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "bef9decc40d9f5bd82e1de2d151bd99f",
|
||||
"content-hash": "415cb668882a32039066467bb308421f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/json-comment",
|
||||
@ -302,16 +302,16 @@
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/binaryutils",
|
||||
"version": "0.2.6",
|
||||
"version": "dev-experimental/read-ops-accounting",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pmmp/BinaryUtils.git",
|
||||
"reference": "ccfc1899b859d45814ea3592e20ebec4cb731c84"
|
||||
"reference": "8cfa34c9d5aae11886a4142c172cff05f1e87ee2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/ccfc1899b859d45814ea3592e20ebec4cb731c84",
|
||||
"reference": "ccfc1899b859d45814ea3592e20ebec4cb731c84",
|
||||
"url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/8cfa34c9d5aae11886a4142c172cff05f1e87ee2",
|
||||
"reference": "8cfa34c9d5aae11886a4142c172cff05f1e87ee2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -320,9 +320,9 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "~1.10.3",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.0.0",
|
||||
"phpstan/phpstan": "2.1.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.0",
|
||||
"phpunit/phpunit": "^9.5 || ^10.0 || ^11.0"
|
||||
},
|
||||
"type": "library",
|
||||
@ -338,9 +338,9 @@
|
||||
"description": "Classes and methods for conveniently handling binary data",
|
||||
"support": {
|
||||
"issues": "https://github.com/pmmp/BinaryUtils/issues",
|
||||
"source": "https://github.com/pmmp/BinaryUtils/tree/0.2.6"
|
||||
"source": "https://github.com/pmmp/BinaryUtils/tree/experimental/read-ops-accounting"
|
||||
},
|
||||
"time": "2024-03-04T15:04:17+00:00"
|
||||
"time": "2025-03-11T11:50:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/callback-validator",
|
||||
@ -2930,9 +2930,18 @@
|
||||
"time": "2024-03-03T12:36:25+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"aliases": [
|
||||
{
|
||||
"package": "pocketmine/binaryutils",
|
||||
"version": "dev-experimental/read-ops-accounting",
|
||||
"alias": "0.2.6",
|
||||
"alias_normalized": "0.2.6.0"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {
|
||||
"pocketmine/binaryutils": 20
|
||||
},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
|
@ -83,6 +83,29 @@ network:
|
||||
#DO NOT DISABLE THIS unless you understand the risks involved.
|
||||
enable-encryption: true
|
||||
|
||||
#EXPERIMENTAL! Limit packet read operations per network session.
|
||||
#This is intended to stop exploitation of packets with arrays in them.
|
||||
#Note that enabling this system may cause players to be unexpectedly kicked, or it may fail to stop attackers.
|
||||
#As of March 2025, the system is still in development and subject to change.
|
||||
packet-read-ops-limit:
|
||||
#What to do when a session's read ops budget is depleted.
|
||||
#Supported actions are "none", "warn" and "kick".
|
||||
deplete-action: warn
|
||||
|
||||
#How many backlog ticks to budget for. 200 allows for a 10-second network lag spike, or a small number of complex
|
||||
#packets.
|
||||
session-budget-ticks: 200
|
||||
|
||||
#How much to top up each session's read operations budget per tick. Recommended to set this to about 2x the
|
||||
#average number of read operations per tick per session.
|
||||
#Exceeding this value won't cause any action to be taken. However, consistently exceeding it will cause the
|
||||
#session's budget to be depleted, resulting in action being taken.
|
||||
session-budget-per-tick: 100
|
||||
|
||||
#Whether to collect stats for debugging. This might impact performance.
|
||||
#See NetworkSession::dumpDecodeCostStats() if you want to visualize the stats yourself.
|
||||
collect-stats: false
|
||||
|
||||
debug:
|
||||
#If > 1, it will show debug messages in the console
|
||||
level: 1
|
||||
|
@ -90,6 +90,11 @@ final class YmlServerProperties{
|
||||
public const NETWORK_COMPRESSION_LEVEL = 'network.compression-level';
|
||||
public const NETWORK_ENABLE_ENCRYPTION = 'network.enable-encryption';
|
||||
public const NETWORK_MAX_MTU_SIZE = 'network.max-mtu-size';
|
||||
public const NETWORK_PACKET_READ_OPS_LIMIT = 'network.packet-read-ops-limit';
|
||||
public const NETWORK_PACKET_READ_OPS_LIMIT_COLLECT_STATS = 'network.packet-read-ops-limit.collect-stats';
|
||||
public const NETWORK_PACKET_READ_OPS_LIMIT_DEPLETE_ACTION = 'network.packet-read-ops-limit.deplete-action';
|
||||
public const NETWORK_PACKET_READ_OPS_LIMIT_SESSION_BUDGET_PER_TICK = 'network.packet-read-ops-limit.session-budget-per-tick';
|
||||
public const NETWORK_PACKET_READ_OPS_LIMIT_SESSION_BUDGET_TICKS = 'network.packet-read-ops-limit.session-budget-ticks';
|
||||
public const NETWORK_UPNP_FORWARDING = 'network.upnp-forwarding';
|
||||
public const PLAYER = 'player';
|
||||
public const PLAYER_SAVE_PLAYER_DATA = 'player.save-player-data';
|
||||
|
@ -113,10 +113,11 @@ use pocketmine\utils\BinaryDataException;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\io\GlobalItemDataHandlers;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\World;
|
||||
use pocketmine\YmlServerProperties;
|
||||
use pocketmine\YmlServerProperties as Yml;
|
||||
use function array_map;
|
||||
use function base64_encode;
|
||||
use function bin2hex;
|
||||
@ -126,6 +127,9 @@ use function implode;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function json_encode;
|
||||
use function max;
|
||||
use function microtime;
|
||||
use function min;
|
||||
use function ord;
|
||||
use function random_bytes;
|
||||
use function str_split;
|
||||
@ -136,6 +140,7 @@ use function substr;
|
||||
use function time;
|
||||
use function ucfirst;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use const PHP_INT_MAX;
|
||||
|
||||
class NetworkSession{
|
||||
private const INCOMING_PACKET_BATCH_PER_TICK = 2; //usually max 1 per tick, but transactions arrive separately
|
||||
@ -144,9 +149,17 @@ class NetworkSession{
|
||||
private const INCOMING_GAME_PACKETS_PER_TICK = 2;
|
||||
private const INCOMING_GAME_PACKETS_BUFFER_TICKS = 100;
|
||||
|
||||
private const READ_OPS_PER_TICK = 100; //subject to change
|
||||
private const READ_OPS_BUFFER_TICKS = 200; //subject to change
|
||||
|
||||
private PacketRateLimiter $packetBatchLimiter;
|
||||
private PacketRateLimiter $gamePacketLimiter;
|
||||
|
||||
private PacketRateLimiter $readOpsLimiter;
|
||||
private PacketRateLimiterAction $readOpsLimiterAction;
|
||||
|
||||
private bool $readOpsStats;
|
||||
|
||||
private \PrefixedLogger $logger;
|
||||
private ?Player $player = null;
|
||||
private ?PlayerInfo $info = null;
|
||||
@ -194,6 +207,28 @@ class NetworkSession{
|
||||
*/
|
||||
private ObjectSet $disposeHooks;
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
* @phpstan-var array<string, int>
|
||||
*/
|
||||
private array $readOpsPerPacketMin = [];
|
||||
/**
|
||||
* @var int[]
|
||||
* @phpstan-var array<string, int>
|
||||
*/
|
||||
private array $readOpsPerPacketMax = [];
|
||||
/**
|
||||
* @var int[]
|
||||
* @phpstan-var array<string, int>
|
||||
*/
|
||||
private array $readOpsPerPacketTotal = [];
|
||||
/**
|
||||
* @var int[]
|
||||
* @phpstan-var array<string, int>
|
||||
*/
|
||||
private array $receivedPacketCounts = [];
|
||||
private int $totalReadOpsUsed = 0;
|
||||
|
||||
public function __construct(
|
||||
private Server $server,
|
||||
private NetworkSessionManager $manager,
|
||||
@ -216,6 +251,15 @@ class NetworkSession{
|
||||
$this->packetBatchLimiter = new PacketRateLimiter("Packet Batches", self::INCOMING_PACKET_BATCH_PER_TICK, self::INCOMING_PACKET_BATCH_BUFFER_TICKS);
|
||||
$this->gamePacketLimiter = new PacketRateLimiter("Game Packets", self::INCOMING_GAME_PACKETS_PER_TICK, self::INCOMING_GAME_PACKETS_BUFFER_TICKS);
|
||||
|
||||
$serverConfigGroup = $this->server->getConfigGroup();
|
||||
$this->readOpsLimiter = new PacketRateLimiter(
|
||||
"Packet Read Operations",
|
||||
$serverConfigGroup->getPropertyInt(Yml::NETWORK_PACKET_READ_OPS_LIMIT_SESSION_BUDGET_PER_TICK, self::READ_OPS_PER_TICK),
|
||||
$serverConfigGroup->getPropertyInt(Yml::NETWORK_PACKET_READ_OPS_LIMIT_SESSION_BUDGET_TICKS, self::READ_OPS_BUFFER_TICKS),
|
||||
);
|
||||
$this->readOpsLimiterAction = PacketRateLimiterAction::from($serverConfigGroup->getPropertyString(Yml::NETWORK_PACKET_READ_OPS_LIMIT_DEPLETE_ACTION, PacketRateLimiterAction::WARN->value));
|
||||
$this->readOpsStats = $serverConfigGroup->getPropertyBool(Yml::NETWORK_PACKET_READ_OPS_LIMIT_COLLECT_STATS, false);
|
||||
|
||||
$this->setHandler(new SessionStartPacketHandler(
|
||||
$this,
|
||||
$this->onSessionStartSuccess(...)
|
||||
@ -450,7 +494,36 @@ class NetworkSession{
|
||||
try{
|
||||
$stream = PacketSerializer::decoder($buffer, 0);
|
||||
try{
|
||||
$packet->decode($stream);
|
||||
if($this->readOpsLimiterAction === PacketRateLimiterAction::NONE){
|
||||
$packet->decode($stream);
|
||||
}else{
|
||||
$this->readOpsLimiter->update();
|
||||
$stream->setReadOpsLimit(
|
||||
$this->readOpsLimiterAction === PacketRateLimiterAction::KICK ?
|
||||
$this->readOpsLimiter->getBudget() :
|
||||
PHP_INT_MAX //don't bail out of decoding if we're only warning
|
||||
);
|
||||
|
||||
$packet->decode($stream);
|
||||
$readOps = $stream->getReadOps();
|
||||
//If we exceeded the budget, a PacketDecodeException has already been thrown if we're in KICK
|
||||
//mode, so we can assume we're in WARN mode here
|
||||
if($this->readOpsLimiter->getBudget() < $readOps){
|
||||
$this->getLogger()->warning("Decoding " . $packet->getName() . " exceeded read ops budget! $readOps > " . $this->readOpsLimiter->getBudget());
|
||||
$this->readOpsLimiter->reset();
|
||||
}else{
|
||||
$this->readOpsLimiter->decrement($readOps);
|
||||
}
|
||||
|
||||
if($this->readOpsStats){
|
||||
$this->totalReadOpsUsed += $readOps;
|
||||
$key = $packet->getName();
|
||||
$this->readOpsPerPacketTotal[$key] = ($this->readOpsPerPacketTotal[$key] ?? 0) + $readOps;
|
||||
$this->readOpsPerPacketMin[$key] = min($this->readOpsPerPacketMin[$key] ?? PHP_INT_MAX, $readOps);
|
||||
$this->readOpsPerPacketMax[$key] = max($this->readOpsPerPacketMax[$key] ?? 0, $readOps);
|
||||
$this->receivedPacketCounts[$key] = ($this->receivedPacketCounts[$key] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}catch(PacketDecodeException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
@ -483,6 +556,30 @@ class NetworkSession{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]|float[]
|
||||
* @phpstan-return array<string, int|float|array<string, int|float>>
|
||||
*/
|
||||
public function dumpDecodeCostStats() : array{
|
||||
if(!$this->readOpsStats){
|
||||
throw new \LogicException("Not collecting stats for this session");
|
||||
}
|
||||
$sessionTime = microtime(true) - $this->connectTime;
|
||||
$packetDecodeAverages = [];
|
||||
$packetCostsPerSecondAverages = [];
|
||||
foreach(Utils::stringifyKeys($this->readOpsPerPacketTotal) as $packet => $total){
|
||||
$packetDecodeAverages[$packet] = $total / $this->receivedPacketCounts[$packet];
|
||||
$packetCostsPerSecondAverages[$packet] = $total / $sessionTime;
|
||||
}
|
||||
return [
|
||||
"readOpsAvgPerSecondTotal" => $this->totalReadOpsUsed / $sessionTime,
|
||||
"readOpsAvgPerPacketPerSecond" => $packetCostsPerSecondAverages,
|
||||
"readOpsAvgPerPacket" => $packetDecodeAverages,
|
||||
"readOpsMinPerPacket" => $this->readOpsPerPacketMin,
|
||||
"readOpsMaxPerPacket" => $this->readOpsPerPacketMax
|
||||
];
|
||||
}
|
||||
|
||||
public function handleAckReceipt(int $receiptId) : void{
|
||||
if(!$this->connected){
|
||||
return;
|
||||
@ -857,7 +954,7 @@ class NetworkSession{
|
||||
}
|
||||
$this->logger->debug("Xbox Live authenticated: " . ($this->authenticated ? "YES" : "NO"));
|
||||
|
||||
$checkXUID = $this->server->getConfigGroup()->getPropertyBool(YmlServerProperties::PLAYER_VERIFY_XUID, true);
|
||||
$checkXUID = $this->server->getConfigGroup()->getPropertyBool(Yml::PLAYER_VERIFY_XUID, true);
|
||||
$myXUID = $this->info instanceof XboxLivePlayerInfo ? $this->info->getXuid() : "";
|
||||
$kickForXUIDMismatch = function(string $xuid) use ($checkXUID, $myXUID) : bool{
|
||||
if($checkXUID && $myXUID !== $xuid){
|
||||
|
@ -62,6 +62,14 @@ final class PacketRateLimiter{
|
||||
$this->budget -= $amount;
|
||||
}
|
||||
|
||||
public function getBudget() : int{
|
||||
return $this->budget;
|
||||
}
|
||||
|
||||
public function reset() : void{
|
||||
$this->budget = $this->maxBudget;
|
||||
}
|
||||
|
||||
public function update() : void{
|
||||
$nowNs = hrtime(true);
|
||||
$timeSinceLastUpdateNs = $nowNs - $this->lastUpdateTimeNs;
|
||||
|
30
src/network/mcpe/PacketRateLimiterAction.php
Normal file
30
src/network/mcpe/PacketRateLimiterAction.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
enum PacketRateLimiterAction : string{
|
||||
case NONE = "none";
|
||||
case WARN = "warn";
|
||||
case KICK = "kick";
|
||||
}
|
Reference in New Issue
Block a user