From 92b4784c2a266ad888047175bc4c24a40b520f05 Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Mon, 12 Oct 2026 14:05:49 +0300 Subject: [PATCH] =?UTF-8?q?net:=20InReliableState=3D0=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=BE=D0=BC=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D0=B5=20(=D0=BF=D0=BE=20=D1=8D=D1=82=D0=B0=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D1=83=20pcap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Захват настоящего GMod (ClassicBox, 8091 пакетов) показал: реальный клиент шлёт в первом netchan-пакете rel=0x0000, а мы ставили 1. Это поле означает «какие подканалы мы хотим получить», в начале оно нулевое. --- crates/net/src/netchan.rs | 8 ++++++-- docs/RECON.md | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/net/src/netchan.rs b/crates/net/src/netchan.rs index a492fe8..86d439c 100644 --- a/crates/net/src/netchan.rs +++ b/crates/net/src/netchan.rs @@ -586,11 +586,15 @@ pub fn build_hello_packet( w.write_u32(ack); w.write_byte(PACKET_FLAG_CHALLENGE | PACKET_FLAG_RELIABLE); // flags (патчится ниже) w.write_short(0); // checksum (патчится ниже) + // `InReliableState` — какие подканалы мы ХОТИМ получить от сервера. + // Настоящий клиент шлёт здесь 0 (мы ещё ничего не ждём), и только после + // приёма данных сервера бит подканала выставляется. Проверено pcap-захватом + // настоящего GMod: `seq=1 ack=0 flags=0x21 rel=0x0000`. let rel_state = std::env::var("SOURCE_RS_REL_STATE") .ok() .and_then(|v| v.parse::().ok()) - .unwrap_or(if messages.is_empty() { 0 } else { 1 }); - w.write_short(rel_state); // in_reliable_state (состояние принятых подканалов) + .unwrap_or(0); + w.write_short(rel_state); w.write_u32(challenge); if !messages.is_empty() && std::env::var_os("SOURCE_RS_UNRELIABLE_MSG").is_none() { diff --git a/docs/RECON.md b/docs/RECON.md index 95e7738..4cadc4a 100644 --- a/docs/RECON.md +++ b/docs/RECON.md @@ -536,3 +536,46 @@ IClientGameServerInternal::* таймаутятся). Установка GMod DS Вывод: для отладки клиента полезнее снимать трафик **настоящего** клиента — сервер тогда не нужен вовсе. + +### 13.9. Эталонный захват настоящего GMod (ClassicBox) — 8091 пакетов + +Снят pcap полной сессии настоящего клиента (`/tmp/opencode/real_client.pcap`). +Инструменты: `/tmp/opencode/pcap2.py` (разбор pcap, **заголовок 24 байта**, а не 16!) +и `/tmp/opencode/ncdec.py` (декод reliable-блока). + +**Рукопожатие полностью совпало с нашим** (побайтно, порядок полей): + +| пакет | что | наш клиент | +|---|---|---| +| C→S `q` | getchallenge + наш challenge | так же | +| C→S `k` | `18000000` протокол, `03000000` authProtocol=3, challengeNr, challenge, checksum, имя, пароль, `2026.04.29`, `f200`=242, SteamID, тикет | так же | +| S→C `B` | challenge + `"0000000000\0"` | так же | + +**Найденное расхождение — `InReliableState`:** + +| | первый netchan-пакет | +|---|---| +| настоящий клиент | `seq=1 ack=0 flags=0x21 csum=0x7568 rel=0x0000` (531 б) | +| наш клиент (было) | `seq=1 ack=0 flags=0x21 csum=0x58df rel=0x0001` (208 б) | + +`InReliableState` — это «какие подканалы мы **хотим получить** от сервера», а не +«сколько мы отправили». Настоящий клиент в первом пакете шлёт **0**, и бит +подканала выставляется только после приёма серверных данных. Мы же ставили 1 +(костыль `if messages.is_empty() {0} else {1}`). Исправлено на 0. + +Ещё наблюдаемые отличия (наш выбор, не ошибка): + +- настоящий клиент **сжимает** reliable-блок (LZSS, бит compressed=1) и шлёт + его несколькими фрагментами — 531 байт против наших 208. Сжатие необязательно + (в `ReadSubChannelData` бит сжатия проверяется, но 0 допустим); +- наш hello — один фрагмент, этого достаточно для `SetConVar`+`SignonState`. + +**Объём эталона:** 536 reliable-пакетов S→C и 878 C→C за сессию — полный +signon (server info, send tables, class info, string tables), снапшоты и +обычный игровой трафик. Это готовый фикстур для реализации разбора signon-блока +без живого сервера. + +**Ограничение повторной проверки:** когда в игре настоящий клиент того же +SteamID, наш вход отбивается `#GameUI_ServerRejectSteam` +(`k_EBeginAuthSessionResultDuplicateRequest`). Для проверки правки нужен +перерыв в сессии.