net: битовый буфер переведён на MSB-first, как в движке

Настоящая причина отсутствия 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.
This commit is contained in:
Niko Marmeladkov 2026-10-12 11:52:28 +03:00
parent 50bb36304e
commit 12f94ae55a
3 changed files with 169 additions and 25 deletions

View file

@ -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]);
}
}

View file

@ -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<PacketHeader, NetErro
checksum_ok = expected == actual;
}
let reliable_state = r.read_short();
// reliable_state кодируется движком через WriteUBitLong(16) — MSB-первый.
let reliable_state = r.read_ubit_long(IN_RELIABLE_STATE_BITS) as u16;
let choked = (flags & PACKET_FLAG_CHOKED != 0).then(|| r.read_byte());
let challenge = (flags & PACKET_FLAG_CHALLENGE != 0).then(|| r.read_long());
@ -594,7 +597,7 @@ pub fn build_hello_packet(
.ok()
.and_then(|v| v.parse::<u16>().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![

View file

@ -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<<numbits)-1
```
Первым читается бит 7 байта. Значит и `WriteOneBit`, и `WriteUBitLong`
должны писать **MSB-first**.
**Почему это ломало всё.** При LSB-first:
- `WriteOneBit(true)` ставил бит в позицию 0 байта, а сервер читал позицию 7;
из-за этого бит «поток 0 несёт данные» попадал в поле «поток 1» →
сервер считал, что данные лежат в потоке File, а Normal пуст;
- ID сообщений (`net_SignonState`, `Net_SetConVar`) записывались
перевёрнутыми и сервер их не распознавал.
Итог: reliable-блок уходил, но не читался — ровно наблюдаемое поведение.
**Важное различие, подтверждённое движком:**
| Операция | Порядок |
|---|---|
| `ReadUBitLong` / `WriteUBitLong` / `ReadOneBit` / `WriteOneBit` | **MSB-first** |
| `ReadByte` / `ReadShort` / `ReadLong` | **побайтово little-endian** |
(для 8 бит разницы нет, для 16/32 — есть). Именно поэтому `reliable_state`
кодируется через `WriteUBitLong(16)`: сервер шлёт `InReliableState == 1`
байтами `00 01`, а не `01 00` — это и было видно в pcap.
Фикс: `crates/net/src/bitbuf.rs` переведён на MSB-first, `reliable_state`
переведён на `write_ubit_long(..., 16)`. Закреплено тестами
`bit_order_matches_source` и `reliable_block_layout_is_canonical`.
Побочный эффект: исправлены **все** многобитные поля на проводе (ID
сообщений, varint-длины, биты добивки), а не только подканал.