From 12f94ae55a99d462d35aefa5cd7cd9a6851ee00a Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Mon, 12 Oct 2026 11:52:28 +0300 Subject: [PATCH] =?UTF-8?q?net:=20=D0=B1=D0=B8=D1=82=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B1=D1=83=D1=84=D0=B5=D1=80=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B5=D0=B4=D1=91=D0=BD=20=D0=BD=D0=B0=20MSB-first?= =?UTF-8?q?,=20=D0=BA=D0=B0=D0=BA=20=D0=B2=20=D0=B4=D0=B2=D0=B8=D0=B6?= =?UTF-8?q?=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Настоящая причина отсутствия server info: bf_write писал биты LSB-первым, а Source читает/пишет MSB-первым (подтверждено дизассемблированием bf_read::ReadUBitLongNoInline в engine_srv.so). Из-за этого WriteOneBit(true) попадал в поле соседнего потока фрагментов, а ID сообщений записывались перевёрнутыми — сервер не распознавал net_SignonState и не выставлял m_bSendServerInfo. - write_one_bit/write_ubit_long/read_one_bit/read_ubit_long: MSB-first; - write_u32/write_short/read_long/read_short: побайтово little-endian (как ReadLong в движке); - reliable_state кодируется через WriteUBitLong(16); - тесты bit_order_matches_source и reliable_block_layout_is_canonical. --- crates/net/src/bitbuf.rs | 105 +++++++++++++++++++++++++++++--------- crates/net/src/netchan.rs | 43 +++++++++++++++- docs/RECON.md | 46 +++++++++++++++++ 3 files changed, 169 insertions(+), 25 deletions(-) diff --git a/crates/net/src/bitbuf.rs b/crates/net/src/bitbuf.rs index ad07fff..845433c 100644 --- a/crates/net/src/bitbuf.rs +++ b/crates/net/src/bitbuf.rs @@ -1,6 +1,14 @@ //! Битовые буферы в стиле Source (`bf_write`/`bf_read`). //! -//! Порядок битов — LSB-первый внутри каждого байта (как в движке). +//! Порядок битов — **MSB-первый внутри каждого байта**, как в движке. +//! Проверено дизассемблированием `bf_read::ReadUBitLongNoInline` в +//! `engine_srv.so`: значение собирается сдвигами `shr`/`shl` от старшего +//! бита, то есть первым читается бит 7 байта. +//! +//! Важное различие (тоже из движка): +//! - `ReadUBitLong`/`WriteUBitLong`/`ReadOneBit`/`WriteOneBit` — **MSB-first**; +//! - `ReadByte`/`ReadShort`/`ReadLong` — **побайтово, little-endian** +//! (для 8 бит обе схемы совпадают, для 16/32 — нет). /// Запись битов. #[derive(Debug, Default)] @@ -27,6 +35,7 @@ impl BitWriter { &self.buf } + /// Записывает один бит. MSB-первый: бит 7 байта идёт первым. pub fn write_one_bit(&mut self, bit: bool) { let byte_index = self.bit_len / 8; let bit_index = self.bit_len % 8; @@ -34,33 +43,53 @@ impl BitWriter { self.buf.push(0); } if bit { - self.buf[byte_index] |= 1 << bit_index; + self.buf[byte_index] |= 1 << (7 - bit_index); } self.bit_len += 1; } - /// Записывает `bits` младших битов значения (LSB-первый). - pub fn write_ubit_long(&mut self, mut value: u32, bits: usize) { - for _ in 0..bits { - self.write_one_bit(value & 1 != 0); - value >>= 1; + /// Записывает `bits` младших битов значения, MSB-первый + /// (аналог `bf_write::WriteUBitLong`). + pub fn write_ubit_long(&mut self, value: u32, bits: usize) { + let bits = bits.min(32); + for i in (0..bits).rev() { + self.write_one_bit(value >> i & 1 != 0); } } + /// `bf_write::WriteLong` — побайтово little-endian. pub fn write_long(&mut self, value: i32) { - self.write_ubit_long(value as u32, 32); + self.write_u32(value as u32); } + /// Побайтово little-endian (как `ReadLong` в движке). pub fn write_u32(&mut self, value: u32) { - self.write_ubit_long(value, 32); + for i in 0..4 { + self.write_byte((value >> (8 * i)) as u8); + } } + /// Побайтово little-endian (как `ReadShort` в движке). pub fn write_short(&mut self, value: u16) { - self.write_ubit_long(value as u32, 16); + self.write_byte(value as u8); + self.write_byte((value >> 8) as u8); } + /// Побайтово — 8 бит, для обоих порядков результат одинаков. pub fn write_byte(&mut self, value: u8) { - self.write_ubit_long(value as u32, 8); + if self.bit_len % 8 == 0 { + if self.buf.len() == self.bit_len / 8 { + self.buf.push(value); + } else { + let idx = self.bit_len / 8; + self.buf[idx] = value; + } + self.bit_len += 8; + } else { + for i in (0..8).rev() { + self.write_one_bit(value >> i & 1 != 0); + } + } } /// Записывает строку с нуль-терминатором (как `bf_write::WriteString`). @@ -111,38 +140,54 @@ impl<'a> BitReader<'a> { self.bits_left() / 8 } + /// Читает один бит. MSB-первый: бит 7 байта читается первым. pub fn read_one_bit(&mut self) -> bool { if self.bit_pos >= self.data.len() * 8 { self.bit_pos += 1; return false; } let byte = self.data[self.bit_pos / 8]; - let bit = (byte >> (self.bit_pos % 8)) & 1 != 0; + let bit = (byte >> (7 - (self.bit_pos % 8))) & 1 != 0; self.bit_pos += 1; bit } - /// Читает `bits` бит (LSB-первый) в u32. + /// Читает `bits` бит, MSB-первый (аналог `bf_read::ReadUBitLong`). pub fn read_ubit_long(&mut self, bits: usize) -> u32 { let mut value = 0u32; - for i in 0..bits.min(32) { - if self.read_one_bit() { - value |= 1 << i; - } + for _ in 0..bits.min(32) { + value = (value << 1) | self.read_one_bit() as u32; } value } + /// Побайтово little-endian (как `ReadLong` в движке). pub fn read_long(&mut self) -> u32 { - self.read_ubit_long(32) + let lo = self.read_byte() as u32; + let mid = self.read_byte() as u32; + let hi = self.read_byte() as u32; + let top = self.read_byte() as u32; + lo | (mid << 8) | (hi << 16) | (top << 24) } + /// Побайтово little-endian (как `ReadShort` в движке). pub fn read_short(&mut self) -> u16 { - self.read_ubit_long(16) as u16 + let lo = self.read_byte() as u16; + let hi = self.read_byte() as u16; + lo | (hi << 8) } pub fn read_byte(&mut self) -> u8 { - self.read_ubit_long(8) as u8 + if self.bit_pos % 8 == 0 && self.bit_pos / 8 < self.data.len() { + let byte = self.data[self.bit_pos / 8]; + self.bit_pos += 8; + return byte; + } + let mut value = 0u8; + for _ in 0..8 { + value = (value << 1) | self.read_one_bit() as u8; + } + value } /// Читает `VarInt32` (7 бит на байт, старший бит — продолжение). @@ -219,14 +264,28 @@ mod tests { #[test] fn bit_order_matches_source() { - // bf_write пишет младшие биты первыми: 0b1 в первом бите байта. + // WriteUBitLong/WriteOneBit — MSB-первый: первый бит попадает в бит 7. let mut w = BitWriter::new(); w.write_ubit_long(1, 1); w.write_ubit_long(0, 7); - assert_eq!(w.bytes(), &[0x01]); + assert_eq!(w.bytes(), &[0x80]); let mut w = BitWriter::new(); - w.write_ubit_long(0b10000000, 8); + w.write_ubit_long(0b1000_0000, 8); assert_eq!(w.bytes(), &[0x80]); + + // WriteLong/WriteShort — побайтово little-endian (как ReadLong). + let mut w = BitWriter::new(); + w.write_u32(1); + assert_eq!(w.bytes(), &[0x01, 0x00, 0x00, 0x00]); + let mut w = BitWriter::new(); + w.write_short(0x0140); + assert_eq!(w.bytes(), &[0x40, 0x01]); + + // WriteUBitLong(16) — MSB-первый, то есть «старший» байт идёт первым. + // Именно так сервер кодирует reliable_state (InReliableState == 1 -> 00 01). + let mut w = BitWriter::new(); + w.write_ubit_long(1, 16); + assert_eq!(w.bytes(), &[0x00, 0x01]); } } diff --git a/crates/net/src/netchan.rs b/crates/net/src/netchan.rs index 944959d..12380a3 100644 --- a/crates/net/src/netchan.rs +++ b/crates/net/src/netchan.rs @@ -31,6 +31,8 @@ pub const FRAGMENT_OFFSET_BITS: usize = 18; pub const MAX_FRAGMENTS_PER_PACKET_BITS: usize = 5; /// Биты максимального размера файла. pub const MAX_FILE_SIZE_BITS: usize = 30; +/// Ширина поля reliable_state в заголовке (`IN_RELIABLE_STATE_BITS`). +pub const IN_RELIABLE_STATE_BITS: usize = 16; /// Биты номера подканала: 3 (8 подканалов, как в Source 2013 / HL2). /// /// Проверено по эталонному разбору пакета: при 4 битах сервер читает @@ -157,7 +159,8 @@ pub fn parse_packet(data: &[u8], checksum: bool) -> Result().ok()) .unwrap_or(if messages.is_empty() { 0 } else { 1 }); - w.write_short(rel_state); // in_reliable_state (состояние принятых подканалов) + w.write_ubit_long(rel_state as u32, IN_RELIABLE_STATE_BITS); // in_reliable_state w.write_u32(challenge); if !messages.is_empty() && std::env::var_os("SOURCE_RS_UNRELIABLE_MSG").is_none() { @@ -728,6 +731,42 @@ impl ClientMessage { mod tests { use super::*; + /// Эталонный layout начала reliable-блока: после 17-байтного заголовка + /// идут `subchannel` (3 бита), затем по одному биту на каждый поток + /// фрагментов, затем бит single-block, бит сжатия и varint длины. + /// + /// Именно эти биты читает сервер (`CNetChan::ProcessPacket`), поэтому + /// layout зафиксирован тестом: при 4 битах подканала сервер читал + /// subchannel=1 и «0 фрагментов», теряя наши надёжные сообщения. + #[test] + fn reliable_block_layout_is_canonical() { + let messages = vec![ClientMessage::SignOnState { + state: signon::CONNECTED, + spawn_count: u32::MAX, + }]; + let packet = build_hello_packet(1, 0, 0x1234_5678, &messages); + assert!(packet.len() > 17, "есть reliable-блок"); + + // Заголовок: seq u32 | ack u32 | flags u8 | checksum u16 | + // reliable_state u16 | challenge u32 = 17 байт. + assert_eq!(&packet[..4], &[1, 0, 0, 0], "seq=1 (little-endian)"); + assert_eq!(packet[8] & PACKET_FLAG_RELIABLE, PACKET_FLAG_RELIABLE); + assert_eq!(packet[8] & PACKET_FLAG_CHALLENGE, PACKET_FLAG_CHALLENGE); + + // Бит 7 первого байта reliable-блока — младший бит индекса + // подканала; бит 4 — флаг «поток 0 несёт данные». + let first = packet[17]; + assert_eq!( + first >> 4, + 0b0001, + "ожидаем subchannel=0 (3 бита) и поток 0=1; получено {:04b} ({:#04x})", + first >> 4, + first + ); + assert_eq!(first >> 5, 0, "индекс подканала должен быть 0"); + assert_eq!((first >> 4) & 1, 1, "поток 0 (Normal) обязан нести данные"); + } + #[test] fn parse_own_packet() { let messages = vec![ diff --git a/docs/RECON.md b/docs/RECON.md index 0b9d9a2..400f5b9 100644 --- a/docs/RECON.md +++ b/docs/RECON.md @@ -394,3 +394,49 @@ payload[191]: 10 bc 01 c5 82 ... уже в `#GameUI_ServerRejectSteam` — это лимит Steam на частые подключения с одного аккаунта (`k_EBeginAuthSessionResultDuplicateRequest`), а не ошибка клиента. + +### 13.4. НАСТОЯЩАЯ ПРИЧИНА: порядок битов в `bf_write` был LSB-first + +Разбор §13.3 вскрыл более глубокий баг: **наш битовый буфер писал биты +LSB-первым, а движок — MSB-первым**. + +Подтверждено дизассемблированием GMod-сервера +(`bf_read::ReadUBitLongNoInline`, `engine_srv.so`): + +``` +206f8d: and $0x1f,%esi ; смещение бита внутри 32-битного слова +206f9d: shl %cl,%eax ; последнее слово сдвигаем влево +206fa1: shr %cl,%edx ; первое слово сдвигаем вправо +206fa7: or %edx,%eax ; значение собирается ОТ СТАРШЕГО бита +206fb4: and %edx,%eax ; маска (1<