Перейти к содержимому

Строгая проверка

[General] VerifyGame = "strict" — четвёртый уровень проверки установленной игры, появившийся с сервером 1.1.0, лаунчером 1.1.0 и протоколом v18. Три нижних уровня сравнивают установку BeamNG игрока со списком файлов самой игры, integrity.json — списком, который игрок может отредактировать вместе с файлами, которые в нём названы. strict сравнивает с эталонным манифестом: описанием чистой установки, которое вы генерируете один раз на версию игры и кладёте на сервер. Лаунчер забирает его, кэширует и сверяет с ним всю установку и пользовательскую папку игры перед каждым подключением и снова во время сессии.

Эта страница говорит, что проверка покрывает и что нет, как сгенерировать и разместить манифест и что видят игроки и авторы плагинов. Сам ключ описан в Конфигурации.

Проверка выполняется в лаунчере игрока, по эталонному манифесту, который назвал сервер. Игрок проходит, когда сходится всё перечисленное ниже; число несовпадений — это problems в отказе.

  • Каждый файл, который перечисляет эталон — его длина. lua/ и ui/ (код и интерфейс игры), всё под Bin64/ и BinLinux/ и файлы *.exe в корне игры дополнительно хешируются SHA-256 и сверяются с дайджестами эталона, так что правка с сохранённой длиной там ловится. Все остальные свободные файлы (art/, campaigns/, gameplay/, …) сравниваются только по размеру. Находки: missing, size, hash.
  • Каждый архив (*.zip) — его оглавление. Машины и уровни лежат внутри многогигабайтных zip-архивов, которые нельзя хешировать перед подключением, но каждый zip заканчивается списком своих записей с несжатыми размерами и CRC-32, и эталон хранит такой список для каждого архива чистой установки. Запись за записью: missing, extra, duplicate, size, crc; zip, чьё оглавление вообще не читается, — unreadable. Находка называет запись как /content/vehicles/pickup.zip!vehicles/pickup/pickup.jbeam.
  • Вся папка игры — любой файл, которого нет в эталоне, — unlisted, где бы он ни лежал, если он не подходит под один из изменчивых шаблонов манифеста (integrity.json, *.log, temp/, startup.ini, Bin64/*.pdb, shadercache/, desktop.ini, Thumbs.db). .dll под Bin64/ и .so под BinLinux/ шаблон не извиняет никогда. Если сам лаунчер установлен внутри папки игры, его собственные файлы (исполняемый файл, Launcher.cfg, logs/, cache/) исключаются, и отчёт говорит об этом (excluded=).
  • Пользовательская папка игры (current/) — папка, в которой игра монтирует свободные файлы поверх упакованного контента, так что vehicles/pickup/pickup.jbeam там подменяет упакованный. Разрешено только то, что игрок законно там хранит: деревья settings/, screenshots/, replays/, cache/, temp/, saves/, mods/ (включая mods/multiplayer/ самого клиентского мода), art/nodemp/, trackEditor/, flowgraphEditor/, roadArchitect/; *.log где угодно; сохранённые конфигурации машин с превью, vehicles/<model>/<name>.pc, .png, .jpg, ровно на этой глубине; заглушки main.materials.json, которые движок пишет сам на любой глубине под vehicles/ и art/; desktop.ini и Thumbs.db. Любой другой файл под vehicles/, levels/, lua/, ui/, art/, scripts/, gameplay/ или под папкой верхнего уровня, которая есть и в папке игры (campaigns/, …), — это overlay, названный как userfolder:vehicles/pickup/pickup.jbeam. Файлы в других папках верхнего уровня (multiplayer/, …) и свободные файлы прямо в current/ ничего не перекрывают и не учитываются. Папка в одном из этих деревьев, которую лаунчер не смог прочитать, — тоже находка (unreadable), а несуществующая пользовательская папка проваливает проверку с указанием пути.

Судится та пользовательская папка, которую использует игра: --user-path, если лаунчеру его передали, иначе startup.ini рядом с игрой ([filesystem] UserPath; относительное значение — относительно папки с ini), иначе BeamNG.Drive.ini в %LOCALAPPDATA%\BeamNG (userFolder), иначе %LOCALAPPDATA%\BeamNG\BeamNG.drive — всегда с добавленным current. Лаунчер пишет её в лог при каждом подключении: user folder judged by the strict check: ….

Отчёт, который отправляет лаунчер, называет манифест, с которым сверял (manifest=<id>;), что исключил как своё (excluded=1:NodeMP/cache/;), сколько папок не смог прочитать (skipped=1;) и до трёх примеров вида path (reason). Сервер сравнивает id с манифестом, который отдал сам: отчёт по другому манифесту — устаревший кэш, файл другого сервера — считается несовпадением независимо от его результата.

Измерено на BeamNG 0.39.4.0 с установкой, уже лежащей в файловом кэше: вся проверка занимает около секунды — 14 193 файла в папке игры, 173 оглавления архивов, 7 203 захешированных файла (около 1,9 ГБ) и обход пользовательской папки. Первая проверка после перезагрузки читает всё это с диска и занимает десятки секунд.

Проверка — это проверка честного клиента, а не граница безопасности. Где она останавливается:

  • Содержимое архивов. Архив описан только оглавлением — имя записи, несжатый размер, CRC-32 — а CRC-32 подделывается: запись можно отредактировать и четырьмя байтами вернуть её CRC. Хешировать содержимое каждого архива значило бы читать около 50 ГБ при каждом подключении, а такую проверку никто не запускал бы. Поэтому strict держит установку, изменённую так, как установки меняются обычно — моды, перекрытия в пользовательской папке, посторонние или отредактированные свободные файлы, перепакованный zip, — но не игрока, который намеренно правит запись игрового архива и подгоняет её CRC. Это принятый остаточный риск; вторым слоем остаются серверные правила вашего ресурса.
  • Сам лаунчер. Вся проверка выполняется на машине игрока, в лаунчере, который игрок мог бы пропатчить. Сервер судит отчёты; наблюдать установку он не может.
  • Свободные файлы вне кода и бинарников сравниваются только по размеру (их SHA-256 есть в манифесте, но хешировать ещё 2 ГБ перед каждым подключением не по средствам).
  • Перепроверки во время сессии — по событиям наблюдателя папок и по расписанию — не хешируют бинарники: библиотека, уже загруженная работающей игрой, не может изменить её поведение до следующего запуска, а следующий запуск снова проходит полную проверку на входе. Запрос от ресурса (player:verify("strict")) хеширует всё.
  • Эталон настолько хорош, насколько хороша установка, с которой он снят. Манифест, сгенерированный с изменённой установки, выдаёт это изменение за чистое. Генерируйте на чистой установке.
  • Список разрешённого в пользовательской папке — это список: файл, который движок пишет куда-то, чего в нём нет, будет отмечен как overlay. Диагностика на странице Устранение неполадок — способ найти такой случай.

Генератор встроен в бинарник сервера. Запускайте его на Windows-машине с чистой установкой BeamNG.drive той версии, на которой играют ваши игроки (сначала проверьте файлы в Steam и убедитесь, что в папку игры ничего не установлено), Windows-сборкой Node-Server из релизного архива. Игра должна быть установлена на той машине, где работает генератор, — серверу на Linux или в Docker читать нечего, поэтому хост на Linux генерирует файл на ПК с Windows (игра ваших игроков в любом случае программа для Windows) и копирует единственный файл на сервер; манифест переносим и не зависит от того, где сделан:

Terminal window
.\Node-Server.exe --gen-integrity "C:\Program Files (x86)\Steam\steamapps\common\BeamNG.drive"

Аргумент — корневая папка игры, та, в которой лежит integrity.json. Инструмент читает из этого файла только версию игры и номер сборки; его список он не копирует, потому что собственный список игры пропускает часть поставляемых файлов, а любое пропущенное сделало бы чистую установку непроходящей strict. Вместо этого он обходит всю установку, хеширует каждый свободный файл SHA-256, читает оглавление каждого архива, отбрасывает изменчивые пути и пишет один сжатый zstd JSON-файл:

reading the game install at C:\Program Files (x86)\Steam\steamapps\common\BeamNG.drive (every file is hashed; a few GB take about a minute) ...
integrity manifest written
game 0.39.4.0 (build 20972)
root files 14193 (whole install, 2656.7 MB hashed)
archives 173 (59594 entries)
size 6.4 MB json, 1.2 MB compressed
hash e326499d35bdef6f1d937a6648113b34225d37c5d0e07b4fc22cdee5db1273a6
file integrity\0.39.4.0.manifest
Put the file in the server's [General] IntegrityDir (default "integrity") and set VerifyGame = "strict".

Числа — для 0.39.4.0; прогон занял около 12 секунд на настольном компьютере с NVMe-диском. Строка hash — это id манифеста: SHA-256 байтов файла, 64 шестнадцатеричные цифры в нижнем регистре — тот дайджест, который для этого файла печатает Get-FileHash -Algorithm SHA256, в нижнем регистре. Всё, что идёт по сети, называет манифест этим id, и лаунчер кэширует его под этим именем. Два прогона по одной и той же установке дают побайтно одинаковые файлы с одним id, потому что метка generated внутри берётся из времени изменения integrity.json, а не из часов.

Опции: --out <file> пишет не в integrity/<game-version>.manifest под рабочим каталогом, а в другое место; --game-version <x.y.z.w> переопределяет версию, прочитанную из integrity.json. Обе принимают и написание --flag=value. Инструмент отказывается что-либо писать, когда в папке нет читаемого integrity.json (no readable integrity.json in … (is this the game's root folder?)), когда файл не читается, когда архив повреждён или когда вывод нельзя записать: integrity manifest NOT written: …, код выхода 1.

Файл имеет формат 2: ключ format внутри равен 2, каждый id и дайджест — SHA-256. Ранний черновик использовал XXH64 (формат 1); такой файл сервер отклоняет при запуске с integrity manifest integrity/0.39.4.0.manifest skipped: format 1 is not supported (this server reads format 2, SHA-256 ids and digests); regenerate with --gen-integrity.

Скопируйте файл в папку integrity/ рабочего каталога сервера — папки, из которой вы запускаете Node-Server, /opt/nodemp/integrity/ или C:\NodeMP\integrity\ в раскладке быстрого старта, — или в папку, которую называет [General] IntegrityDir. Под Docker рабочий каталог — /data, так что файл идёт в /data/integrity/ на томе данных хоста. Затем поставьте VerifyGame = "strict" (NODE_VERIFY_GAME=strict) и перезапустите: и настройка, и манифесты читаются при запуске.

Держите в папке ровно один .manifest. Рукопожатие не несёт версию игры игрока, так что сервер не может выбирать между манифестами; с двумя файлами каждое подключение отклоняется с This server has integrity manifests for 2 game versions (0.39.4.0, 0.39.3.0) and cannot tell which one you run. Ask the host to keep exactly one manifest in the server's integrity folder., а без файлов — с This server requires a strict check of your BeamNG install but has no integrity manifest to check it against. Ask the host to run `Node-Server --gen-integrity <gamedir>` and put the file in the server's integrity folder. Оба случая — ещё и ошибки в логе запуска. Здоровый запуск пишет по строке на манифест,

integrity manifest for game 0.39.4.0 (build 20972): 173 archives, 59594 entries, 14193 root files, 1.2 MB (38 chunks), hash e326499d…
strict game verification ON: reference manifest for game 0.39.4.0 (e326499d…)

Сервер на другом уровне всё равно загружает папку (1 integrity manifest loaded from "integrity" (available to Player:verify("strict"))): ресурс тогда может провести строгий аудит по запросу, никому не отказывая на входе.

Манифест описывает одну версию игры. Когда BeamNG обновляется, обновлённая установка игрока расходится со старым эталоном в бинарниках, коде и архивах и отклоняется с Game files do not match this server's reference (N problems). …. Сгенерируйте новый манифест с обновлённой чистой установки, замените старый файл (в папке — один файл), перезапустите. Лаунчеры замечают новый id, забирают новый манифест при следующем подключении и кэшируют его. Игрок, который игру ещё не обновил, отклоняется затем так же, с примерами вида /Bin64/BeamNG.drive.x64.exe (hash); обновление игры в Steam это исправляет.

На строгом сервере у подключения в лаунчере два лишних шага: Downloading the server's integrity manifest — один раз на манифест, около 1,2 МБ кусками по 32 КиБ, затем кэш в %LOCALAPPDATA%\com.nodemp.launcher\helper\cache\integrity\<id>.manifest — и Checking game files. Чистая установка затем подключается как на любом другом сервере; её лог говорит game files verified: 14193 files checked in 0.9s (strict), 7203 hashed. Несовпадение сервер отклоняет с Game files do not match this server's reference (3 problems). userfolder:vehicles/pickup/pickup.jbeam (overlay), …; лаунчер 1.1.0 показывает это своими словами, как Could not join · Your game files do not match this server's reference (3 problems) · vehicles/pickup/pickup.jbeam, с объяснением первой проблемы в панели под карточкой сервера. Если проверка провалилась позже по ходу сессии, сервер завершает её с Game files changed while you were playing and no longer match this server's reference (1 problem). …, что показывается как Session ended · Your game files changed while you were playing and no longer match this server's reference (1 problem) в игре.

Во время сессии лаунчер проверяет снова: в Windows — когда меняются файлы под content/, scripts/, lua/, ui/ установки или под vehicles/, levels/, lua/, ui/, art/ пользовательской папки (не чаще раза в минуту), и по расписанию каждые StrictRecheckMin минут из его Launcher.cfg (по умолчанию 10; см. Настройки). Перепроверка, которая ничего не нашла, не отправляется; та, которая нашла изменение, отправляется, и сервер завершает сессию текстом выше. Тексты отказов и диагностика, которую может запустить отклонённый игрок, — на странице Устранение неполадок и в Кодах ошибок.

К strict относятся три добавления в серверном API (ABI 1.12):

  • player:verify(level) просит лаунчер игрока выполнить проверку снова, сейчас, на уровне "size", "scripts", "full" или "strict". Возвращает true, когда запрос ушёл, или nil и "unknown player", "unsupported" (уровень недоступен — "strict" на сервере без ровно одного загруженного манифеста) или "pending" (один запрос на игрока за 60 с; проверка, которую выполняет каждое подключение, не считается, так что вызов из playerJoined уходит сразу). Строгий запрос от ресурса хеширует бинарники, в отличие от собственных перепроверок лаунчера.
  • playerVerifyReported срабатывает на каждый отчёт — на входе и на каждый в сессии — после вердикта сервера, с { level, outcome, problems, detail, manifest }: outcome — что решил сервер ("clean", "differs", "failed"), manifest — id, который назвал лаунчер ("" ниже strict). Отчёт differs или failed уже кикнул игрока, если VerifyGame не off; на сервере с off событие — единственное следствие, и так строится аудит без отказов.
  • player:setStrict(on) помечает сессию строгой на стороне сервера, что меняет одно правило ретрансляции: кадр Vehicle::Camera от этого игрока, нацеленный на машину, которой он не управляет, в которой не сидит и которая не его пешеходный аватар, не ретранслируется, так что строгий клиент не может следить камерой за чужими машинами. Отклонённый отчёт всё равно вызывает playerCameraChanged, а player:camera() тогда несёт denied = true. Клиенту это ничего не сообщает: клиентские правила включаются таблицей strict события session:config, описанной на странице Клиентские скрипты.

У нативных модулей те же три — player_verify, player_set_strict и уведомление playerVerifyReported — в C ABI.