diff --git a/crates/net/src/netchan.rs b/crates/net/src/netchan.rs index 064c401..9c26f53 100644 --- a/crates/net/src/netchan.rs +++ b/crates/net/src/netchan.rs @@ -46,12 +46,12 @@ pub fn subchannel_bits() -> usize { /// Число потоков фрагментов (Normal, File). pub const MAX_STREAMS: usize = 2; -// Флаги пакета. +// Флаги пакета (`PacketFlag` в Source.Common/Networking/IConnectionlessPacketHandler.cs). pub const PACKET_FLAG_RELIABLE: u8 = 1 << 0; pub const PACKET_FLAG_COMPRESSED: u8 = 1 << 1; pub const PACKET_FLAG_ENCRYPTED: u8 = 1 << 2; -pub const PACKET_FLAG_CHOKED: u8 = 1 << 3; -/// GMod-дополнение: в пакете присутствует challenge (0x20, не 0x10). +pub const PACKET_FLAG_SPLIT: u8 = 1 << 3; +pub const PACKET_FLAG_CHOKED: u8 = 1 << 4; pub const PACKET_FLAG_CHALLENGE: u8 = 1 << 5; /// Net-сообщения (нумерация GMod). @@ -765,6 +765,46 @@ mod tests { let _ = parse_messages(&packet, header.payload_bit); } + /// Флаги `PacketFlag` — по спецификации Source.NET: + /// Reliable=1, Compressed=2, Encrypted=4, Split=8, Choked=16, Challenge=32. + /// + /// Раньше `CHOKED` был объявлен как `1<<3`, то есть фактически `Split`. + /// Из-за этого в пакетах сервера с выставленным битом 3 читался лишний + /// байт «choked», и `reliable_state` с `challenge` уезжали на 8 бит. + #[test] + fn packet_flags_match_specification() { + assert_eq!(PACKET_FLAG_RELIABLE, 0x01); + assert_eq!(PACKET_FLAG_COMPRESSED, 0x02); + assert_eq!(PACKET_FLAG_ENCRYPTED, 0x04); + assert_eq!(PACKET_FLAG_SPLIT, 0x08); + assert_eq!(PACKET_FLAG_CHOKED, 0x10); + assert_eq!(PACKET_FLAG_CHALLENGE, 0x20); + } + + /// Заголовок с флагом Choked: между reliable_state и challenge + /// вставляется байт «сколько пакетов пропущено». + #[test] + fn header_with_choked_flag_has_extra_byte() { + let mut packet = Vec::new(); + packet.extend_from_slice(&7u32.to_le_bytes()); // sequence + packet.extend_from_slice(&3u32.to_le_bytes()); // ack + packet.push(PACKET_FLAG_CHOKED); + packet.extend_from_slice(&0u16.to_le_bytes()); // checksum, патчится ниже + packet.extend_from_slice(&0x1234u16.to_le_bytes()); // reliable state + packet.push(4); // choked + // Далее — payload; challenge не выставлен. + let csum = fold_crc32(&packet[11..]); + packet[9..11].copy_from_slice(&csum.to_le_bytes()); + + let header = parse_packet(&packet, true).expect("заголовок"); + assert_eq!(header.sequence, 7); + assert_eq!(header.ack, 3); + assert_eq!(header.flags & PACKET_FLAG_CHOKED, PACKET_FLAG_CHOKED); + assert_eq!(header.reliable_state, 0x1234, "reliable_state не сдвинут"); + assert_eq!(header.choked, Some(4)); + assert!(header.checksum_ok); + } + #[test] fn checksum_detects_corruption() { let packet = build_hello_packet(5, 0, 42, &[]); diff --git a/docs/RECON.md b/docs/RECON.md index f6c746a..fafcc33 100644 --- a/docs/RECON.md +++ b/docs/RECON.md @@ -663,3 +663,49 @@ Map: gm_bigcity_improved_rp / Players…» — значения сходятся **Статус сетевого слоя:** заголовок пакета, реассемблер фрагментов, connect, hello и `SV_ServerInfo` — по эталону подтверждены. Осталось разобрать multi-fragment переносы (send tables, string tables), сжатие и снапшоты. + +### 13.12. Source.NET — источник истины, а не pcap (урок процесса) + +Замечание «у нас есть исходники Source.NET, зачем мы реверсем» — справедливое, +и оно указывает на потерянное время. Правильный порядок: **сначала читать +`Source.Common/Networking/Protocol.cs` и `Source.Engine/NetChannel.cs`**, +и лишь потом сверять с эталонным pcap. + +Что дало чтение исходников за секунды (после часа перебора по захвату): + +```csharp +// Source.Common/Networking/Protocol.cs +FRAGMENT_BITS = 12; FRAGMENT_SIZE = 1 << 12; // 4096, а НЕ 254 +MAX_FILE_SIZE_BITS = 30; // startFragment = 30-12 = 18 бит +NETMSG_TYPE_BITS = 6; MAX_PAYLOAD = 288000; +MAX_ROUTABLE_PAYLOAD = 1260; + +// Source.Common/Networking/IConnectionlessPacketHandler.cs +public enum PacketFlag { + Reliable = 1<<0, Compressed = 1<<1, Encrypted = 1<<2, + Split = 1<<3, Choked = 1<<4, Challenge = 1<<5 +} +``` + +**Найденный баг.** У нас было `PACKET_FLAG_CHOKED = 1<<3` — это на самом деле +`Split`. Значит в пакетах сервера с выставленным битом 3 мы читали лишний байт +«choked», и `reliable_state` вместе с `challenge` уезжали на 8 бит — заголовок +такого пакета разбирался неверно целиком. Исправлено на `Choked = 1<<4` + +`Split = 1<<3`, закрыто тестами `packet_flags_match_specification` и +`header_with_choked_flag_has_extra_byte`. + +**Про порядок бит.** `Source.Common/Bitbuffers/bf_read.cs:752` +(`ReadUBitLong`) — `dw1 = word >> startBit; dw2 = word << (32 - startBit)` — +это в точности тот алгоритм, что я дизассемблировал из `engine_srv.so`: +значения собираются **LSB-first**. Подтверждено дважды. + +**Про сжатие в single-block.** Source.NET читает бит сжатия перед длиной, +но эталон однозначно показывает 23-битный заголовок (4+2+1+VarInt32) без него. +Похоже, GMod сигналит сжатие флагом пакета, а не фрагмента; наша правка +опирается на измерение, источник тут расходится. + +**Что уже совпадало с исходником** (значит, брали правильно): +`FRAGMENT_SIZE = 1<<12 = 4096`, `FRAGMENT_OFFSET_BITS = 18`, +`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)`.