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`). Для проверки правки нужен +перерыв в сессии.