diff --git a/crates/net/src/netchan.rs b/crates/net/src/netchan.rs index 9c26f53..4ab7ab9 100644 --- a/crates/net/src/netchan.rs +++ b/crates/net/src/netchan.rs @@ -380,10 +380,17 @@ impl NetChan { } if start_fragment + num_fragments > stream.num_fragments { - return Err(NetError::Protocol(format!( - "фрагмент вне диапазона: {start_fragment}+{num_fragments}>{}", - stream.num_fragments - ))); + // Source.NET (ReadSubChannelData) просто возвращает управление: + // это штатная ситуация для опоздавшего/дублирующего фрагмента. + // Ошибкой считать нельзя — иначе один старый фрагмент уронит + // разбор всех остальных сообщений пакета. + tracing::trace!( + start_fragment, + num_fragments, + num_fragments_total = stream.num_fragments, + "фрагмент вне диапазона, ждём повтор" + ); + return Ok(()); } if start_fragment + num_fragments == stream.num_fragments { // Последний фрагмент — укорачиваем до реального размера. @@ -393,10 +400,17 @@ impl NetChan { } } if length == 0 || offset + length > stream.bytes { - return Err(NetError::Protocol(format!( - "некорректный фрагмент: offset={offset} length={length} bytes={}", - stream.bytes - ))); + // Source.NET делает data.Return() и уходит: передача отбрасывается + // целиком, ждём её с начала. Ошибка здесь означала бы потерю всех + // прочих сообщений пакета. + tracing::trace!( + offset, + length, + bytes = stream.bytes, + "некорректный фрагмент, передача отброшена" + ); + stream.reset(); + return Ok(()); } for i in 0..length { diff --git a/docs/RECON.md b/docs/RECON.md index fafcc33..5e31dd0 100644 --- a/docs/RECON.md +++ b/docs/RECON.md @@ -709,3 +709,48 @@ public enum PacketFlag { `MAX_FRAGMENTS_PER_PACKET_BITS = 5`, `NETMSG_TYPE_BITS = 6`, `IN_RELIABLE_STATE_BITS = 16`, `SubChannel.MAX_BITS = 4` (совпало с дизасмом), порядок `net.SetConVar` → `net.SignonState(CONNECTED, -1)`. + +### 13.13. Полнота эталонного захвата: send tables в нём отсутствуют + +Проверка непрерывности `seq` в `real_client.pcap`: + +| сторона | netchan-пакетов | пропусков | потеряно | +|---|---|---|---| +| сервер | 2718 | 164 | **2576** | +| клиент | 3204 | 82 | 201 | + +Основная потеря — в начале сессии (пакеты #63…#130, по 30+ подряд), то есть +именно там, где сервер шлёт send tables, baselines и string tables. + +**Вывод:** отсутствие send tables в эталоне — дефект захвата (трафик шёл через +SOCKS-прокси), а не дефект парсера. Проверено: собранные 582 блока разбираются +до конца, первый блок (`#34`, 305 байт) содержит `SV_Print` (баннер ClassicBox) +и `SV_ServerInfo` (proto=24, count=2, ded=1, maxcl=48, tick=0.030303, +`dir=garrysmod`, `map=gm_bigcity_improved_rp`) — ровно тот блок, что лежит в +фикстуре `tests/fixtures/classicbox_signon_block.bin`. + +Многофрагментных (multi-fragment) передач в читаемом виде в захвате нет: все +собранные блоки меньше `FRAGMENT_SIZE`. Значит дальнейшую верификацию этой ветки +проводить не на чем — но её раскладка теперь известна из исходника и совпадает с +нашей реализацией. + +### 13.14. Устойчивость разбора фрагментов + +`ReadSubChannelData` в Source.NET при «неожиданном» фрагменте (за пределами +диапазона, нулевой длине, выходе за `bytes`) **не считает это ошибкой**: делает +`data.Return()` и выходит, передача будет прислана заново. Наш код возвращал +`NetError`, что сбрасывало разбор всех остальных сообщений пакета — достаточно +одного опоздавшего фрагмента, чтобы потерять весь signon-блок. Исправлено на +отбрасывание передачи (`stream.reset()`) с `trace`-логом. + +### 13.15. Распределение флагов в эталоне + +| флаги | пакетов | состав | +|---|---|---| +| 0x20 / 0xa0 / 0x60 | 2187 | Challenge (+ биты добивки 0x40/0x80) | +| 0x21 / 0x61 / 0xa1 | 527 | Reliable\|Challenge (+ добивка) | +| 0xb0, 0xa7, 0xbf, 0xd3 | 4 | единичные Split/Choked/Compressed/Encrypted | + +То есть правка `CHOKED = 1<<4` затрагивала 3 пакета из 2718 — редко, но каждый +из них раньше давал рассинхрон на 8 бит. Биты 6–7 (0x40/0x80) — добивка, их +значения игнорируются.