sourcesniffer is a low-level networking research framework implementing the Source Engine
netchannel protocol from scratch in Kotlin. It enables real-time decoding, inspection, and
manipulation of game traffic at the bit level. The project grew into a full engine-faithful
reimplementation of the entire network stack — from raw UDP frames to structured entity
state — paired with a live desktop UI built in Compose.
This entire project was developed and tested in local, controlled, and isolated environments
to research video game networking protocols and security. The project contains no code that
provides an unfair advantage in online play and cannot be used to monitor network
communications without explicit permission.
Almost all of the (outdated) Source Engine network message protocol is documented in Valve's
csgo-demoinfo.
Updated versions of the Source Engine protocol use asymmetric encryption, Perfect Forward
Secrecy (PFS), IP protection, and several real-time authentication checks that make this
class of research impossible on live infrastructure.
The video shows a Windows machine running Counter-Strike: Source (CS:S) streamed via Moonlight
(client) and Sunshine (host).
My MacBook Air (M1) is running the proxy server in IntelliJ, forwarding all network packets
to a local dedicated CS:S server running on the same Windows computer.
The game connects to the proxy; the proxy decodes all packets in real time and injects
SVC_FixAngle messages into every server-bound datagram, overwriting the client's
viewangles. This causes the client's camera to spin continuously without any code running on
the Windows machine.
It is also possible to inject packets into an ongoing session on the same LAN without a proxy,
using the passive sniffer path to infer sequence numbers. Using this technique I was able to
inject chat messages from my MacBook directly into my isolated test server. This produced
observable side effects — graphical glitches and sequence-ack desyncs — when spammed,
providing useful data about how the engine handles out-of-order reliable state.
Motivation
The goal was to understand the Source Engine netchannel at the deepest practical level —
not by reading documentation (little exists) but by observing live traffic, studying the
open-source SDK, and reconstructing every field through experimentation. The protocol
involves multiple overlapping encoding schemes (bit-aligned I/O, delta compression, varint
encoding, Snappy frames) and a reliable/unreliable multiplexing layer that must be
maintained with precise sequencing. Building a proxy that can intercept, decode, modify,
and re-forward traffic without breaking the session is a strong correctness test.
System Design
bitbuf/ — Source-style bit reader and writer; core I/O primitives
packetparser/ — Full netchannel state machine: headers, subchannels, fragmentation, compression
messages/ — ~40 protocol message implementations (SVC_* and CLC_*)
messagehandler/ — Dispatch by 6-bit message type ID; bidirectional (server ↔ client)
The netchannel sits immediately above UDP. Each datagram has a fixed header followed by an
optional reliable subchannel block and a stream of back-to-back variable-length messages.
Header fields (in order):
Sequence number (int32) — sender's outgoing packet counter
Sequence ack (int32) — last received sequence from the other side
Reliable state bitmask (uint8) — which subchannels are currently transferring
Optional choke count and challenge nonce depending on flags
Before the netchannel header, the engine wraps datagrams in one of two network-layer
envelopes: split packets (magic -2, reassembled from up to N fragments of 1400
bytes each) and compressed packets (magic -3, Snappy payload). Connectionless
queries carry magic 0xFFFFFFFF and are handled separately.
The flags byte has an undocumented dual use: the upper 3 bits encode a pad-bit count used
to byte-align the message stream tail, overlapping with the CHALLENGE flag region.
Correct reconstruction requires preserving this exactly.
Reliable Channel & Subchannels
Source's reliable layer sits inside the netchannel rather than in a separate TCP stream.
It operates over 8 subchannels, each carrying fragments of 256 bytes. Two independent
fragment streams exist: a general stream (index 0) and a file-transfer stream (index 1).
Payloads exceeding 1024 bytes are optionally Snappy-compressed before fragmentation.
Fragment size: 256 bytes (FRAGMENT_BITS = 8); max transfer: 64 MB
Reassembly via DataFragments accumulator; decompressed after all fragments arrive
Subchannel acknowledgement via the reliable state bitmask in every header
Every message implements a readFromBuffer / writeToBuffer /
process lifecycle. A captureBits mechanism snapshots the raw
payload bits on read so a message can be re-emitted verbatim without needing a
bit-perfect field encoder — critical for messages like SVC_PacketEntities
where re-encoding every delta field exactly is impractical.
Entity Decoding
SVC_PacketEntities is the densest part of the protocol. The server sends
compressed delta updates for every entity in the player's PVS each tick. Decoding requires
a fully initialised send-table schema, matching the engine's internal flat property list
exactly.
Send-table pipeline:
SVC_SendTable registers per-class prop hierarchies; SVC_ClassInfo
maps class IDs to data tables
Flat prop lists are built from the hierarchy: excludes resolved, collapsible tables
inlined, array element props expanded
Props sorted by priority with SPROP_CHANGES_OFTEN props promoted to the front,
matching engine behaviour
The rebuilt flat order can differ from the engine's internal list — on CSS x64,
DT_CSPlayer produces 644 props reconstructed vs 534 in the engine's
m_pPrecalc. Wrong order causes silent prop misalignment and corrupts
all entity state silently
Solution: preload an authoritative flat-prop dump captured from the engine at startup
(SendTableDumpLoader / ENGINE_FLAT sections); live
SVC_SendTable messages are still parsed for structure but the flat order
is overridden
Delta decode (SVC_PacketEntities):
Four entity record types per packet: EnterPVS, LeavePVS, DeltaEnt, PreserveEnt
Prop index encoded as a 7-bit continuation integer (ReadNextPropIndex,
matching CDeltaBitsReader)
Integer props: fixed-width or varint (ZigZag for signed); CSS x64 repurposes
SPROP_NORMAL bit (bit 5) as SPROP_VARINT for integers —
an undocumented divergence from the public SDK
Float props: bit-coord, bit-coord-MP (integral/low-precision), noscale, normal,
clamped range
Vector props: Z reconstructed from XY via sqrt(1 - x² - y²) with
sign bit when SPROP_NORMAL
String and array props supported; baseline bytes tracked via
instancebaseline string table
Lost-sync guard: on unknown classId or prop index overflow, m_bLostSync
halts decode rather than corrupting downstream state
Bit I/O Layer
The engine operates on a bit stream, not a byte stream. All reads and writes are
bit-aligned. The bitbuf/ package reimplements Source's bf_read
and bf_write with word-aligned I/O and masking:
readUBitLong(n) — arbitrary-width unsigned read with EXTRA_MASKS
boundary handling
Signed and unsigned varint32 (ZigZag encoding for signed)
readBitCoord, readBitCoordMP (integral and low-precision
variants) — used throughout entity props and sound origins
readBitNormal, readBitAngle, readBitVec
writeBitStream — opaque bit-range copy, the backbone of lossless
packet reconstruction
Packet Reconstruction
The proxy can forward a datagram via two distinct paths:
Verbatim reconstruction — copies the post-header bytes verbatim from
savedPostHeader, splicing injected messages at a defined insertion
point. Zero re-encoding risk; used when message fields cannot be round-tripped
exactly (e.g. SVC_PacketEntities delta bits)
Re-encode from messages — serialises parsed Message objects
back to wire format. Allows full field-level modification; used when the proxy
needs to rewrite message content rather than just inject alongside it
Choosing between paths requires knowing which messages are bit-sensitive.
SVC_SendTable, SVC_ClassInfo, string table messages, and
SVC_PacketEntities are all treated as verbatim-only. The
captureBits / writeSavedBits pattern on each Message
object makes this transparent to the caller.
Execution Modes
Passive sniffer: capture live traffic via Pcap4J with BPF port filter;
decode without forwarding
MITM proxy: bind on a local port; accept one client; maintain two UDP
sessions (client↔proxy, proxy↔server); parse and optionally rewrite both
directions in real time
Live Desktop UI
A Compose Desktop application provides real-time visibility into the decoded protocol state:
Radar view with per-entity position history trails, updated from decoded
entity origins each tick
Player table: entity ID, team, position, class, tracked prop values
Packet monitor: per-direction message type log with timestamps and sizes
Proxy target manager with persistent JSON favourites
Chat and console command injection panel (sends NET_StringCmd or
SVC_UserMessage into the live session)
Reactive state flows (EntityPropSession, EntityOriginSession,
LocalPlayerSession) bridge the JVM network thread to the Compose UI
thread without polling.
Protocol Injection Research
A core research goal was understanding which server-sent messages can be injected mid-session
without desynchronising the client, and which cannot. Several message types were studied:
SVC_FixAngle — overwrites client viewangles; relative mode (yaw-only)
vs absolute; engine only applies yaw for relative FixAngle — pitch and roll are silently
ignored, an undocumented engine quirk
SVC_PacketEntities delta injection — sending an invalid number of props instantly crashes the client
CLC_Move rewrite — modifying outgoing client commands (viewangles, buttons,
move vector) mid-stream while keeping tick and delta sequence fields consistent
The framework was also used to test how the engine handles structurally invalid messages
from a protocol-fuzzing perspective — malformed prop indices, oversized array counts, and
out-of-bounds reads in the message parsing path.
Cryptographic RNG Research
The Source Engine uses an MD5-based pseudo-random number generator (MD5_PseudoRandom)
seeded per client command for certain game mechanics. The crypto/ module
reimplements this RNG exactly — matching the engine's byte-level output — to study its
properties and period, and to verify that the command number field in
CLC_Move is the sole entropy source for those mechanics.
Technical Challenges
Reconstructing undocumented protocol structures entirely from observation and
experimentation — no authoritative spec exists for CSS netchannel internals
Bit-level reader/writer fidelity: a single off-by-one bit invalidates every
subsequent field in the datagram
Send-table flat prop ordering: rebuilt order diverges from engine order; requires
authoritative dump captured from the engine at runtime
CSS x64 integer encoding: SPROP_VARINT reuses the SPROP_NORMAL
bit position — undocumented; only discoverable through differential analysis of
decoded vs expected prop values
Flags byte dual encoding: the upper 3 bits serve double duty as pad-bit count and
challenge flag region; both must be preserved in reconstruction
Maintaining sequence/ack synchronisation while inserting or withholding messages
Reliable subchannel round-trip: receive path must track per-subchannel ack state to
avoid infinite retransmit from the server
Snappy framing: the engine uses a big-endian SNAP magic header
(0x534E4150); string-table blobs use a different little-endian variant —
two decompressors required
What I Learned
How a production real-time multiplayer network stack works at the bit level —
fragmentation, compression, reliable multiplexing, and delta state all in one UDP
channel
Reverse engineering workflows: differential analysis, live traffic observation,
SDK cross-referencing, and authoritative dump extraction
Protocol correctness under modification: small encoding errors produce silent
corruption rather than crashes, making testing difficult
The practical limits of MITM research: injecting into bit-sensitive messages
(entity deltas) without engine state is infeasible; cosmetic messages are much
more tractable
Designing systems that degrade gracefully under sync loss rather than corrupting
downstream state
How modern Source Engine versions hardened against this class of research —
making the security improvements (asymmetric encryption, PFS, authentication
checks) more concrete and easier to reason about
Current Status
Full netchannel header parsing and reconstruction (both directions)
Split packet reassembly and Snappy decompression
Complete message registry: all SVC_* and CLC_* types read and write
Entity delta decoding (SVC_PacketEntities) with authoritative flat lists
Send-table hierarchy build, prop sort, and engine-flat override
Reliable subchannel receive path; transmit path functional for injection
Live Compose Desktop UI: radar, player table, packet monitor, injection panel
Reactive state sessions bridging network thread to UI
Passive sniffer path exists but is disabled (entry point commented out)
Temp entity prop body decode not yet wired to state
Game event processing minimal; file transfer stream parsed but not completed
Several parameters still hard-coded; CLI configuration not yet built
Future Work
Full reliable channel transmit: retransmit scheduling, subchannel rotation
Temp entity prop decoding for state and visualisation
Game event decoding and display in UI
CLI configuration system (server IP, proxy port, feature flags)
Passive sniffer re-enable with clean entry point
Packet file record and replay for offline analysis
Improved packet visualisation and timeline tooling