kolibri-net
Reusable Rust core of the Qlyra messaging protocol — a hand-rolled binary
framing over a persistent TLS TCP socket, with MessagePack payloads and LZ4/Zstd
compression. Extracted from the Flutter client (lib/core/transport/ +
lib/core/protocol/) so the same protocol implementation can be shared across
Flutter (via flutter_rust_bridge), Python (via PyO3/maturin), and any other
host over a C ABI.
Status
| Phase | Scope | State |
|---|---|---|
| 1 | Protocol core: packet codec, framing, compression, opcodes | ✅ done |
| 2 | Async transport: tokio TCP + TLS (rustls), seq/opcode dispatcher | ✅ done |
| 3 | Session state machine: handshake, ping keepalive, reconnect+backoff | ✅ done |
| 4 | FFI: Python (pyo3/maturin) ✅ · Dart (flutter_rust_bridge) planned |
🚧 |
| 5 | Swap Dart transport behind a flag, live-compare, remove | planned |
Proxy: ClientConfig::proxy takes a ProxyConfig (HTTP CONNECT or SOCKS5, with
optional user/pass), applied to the main socket, media uploads, and ws2 calls.
Parse one from a url with ProxyConfig::parse("http://user:pass@host:port")
(http / socks5 / socks5h).
Not yet ported: VPN-bypass (Android). Production TLS uses the bundled Mozilla
root store — swap for rustls-platform-verifier to match Dart's OS trust store.
Wire format (10-byte big-endian header)
[0] ver protocol version (u8, = 10)
[1] cmd 0 request/push · 1 ok · 2 not_found · 3 error
[2..4] seq sequence number (u16 BE)
[4..6] opcode operation code (u16 BE)
[6..10] packedLen high byte = compression flag, low 24 bits = payload length
[10..] payload MessagePack, LZ4-block / LZ4-frame / Zstd (sniffed by magic)
Payloads under 32 bytes are sent uncompressed. Outgoing compression is LZ4
frame; incoming is sniffed by magic number (Zstd 28 B5 2F FD, LZ4 frame
04 22 4D 18, otherwise LZ4 block).
Design
The core is I/O-free and representation-agnostic: encode/decode work on raw
MessagePack bytes so a Dart Map, a Python dict, and a Rust struct all come
from the same Packet.payload. PacketReceiver de-frames the raw TLS byte
stream into complete packets.
use kolibri_net::{encode, decode, protocol::opcodes};
let wire = encode(opcodes::MSG_SEND, &msgpack_bytes, seq);
let packet = decode(&wire)?;
let value = packet.value()?; // rmpv::Value
Build & test
cargo test # 14 protocol vectors + 5 transport + 6 session + 1 backoff
cargo clippy --all-targets
The transport feature (async client, on by default) can be disabled to build
the pure protocol codec with no tokio/rustls dependency:
cargo build --no-default-features
Transport usage
use kolibri_net::{Client, ClientConfig, protocol::opcodes};
let client = Client::connect(ClientConfig::new("host.example", 443)).await?;
let mut pushes = client.subscribe(); // broadcast of server pushes
let resp = client.request(opcodes::CHATS_LIST, &msgpack_bytes).await?;
Session usage (handshake + keepalive + reconnect)
use kolibri_net::{Session, SessionConfig, ClientConfig, HandshakeConfig, UserAgent};
let config = SessionConfig::new(
ClientConfig::new("host.example", 443),
HandshakeConfig { /* device values from the host */ },
);
let session = Session::new(config);
let info = session.connect().await?; // connect + sessionInit → Online
let resp = session.request(opcodes::AUTH_REQUEST, &msgpack_bytes).await?;
The host (Flutter, Python, …) supplies device values for the handshake; the wire shape and the connect → handshake → ping → reconnect sequence live in Rust.