API — sentry-protocol
Reference for sentry-protocol: protocol schema DSL, compiler, VM, DoS guards and CLI
API — sentry-protocol
sentry-protocol describes and validates non-HTTP protocols on non-standard ports (F9). A YAML schema describes framing, dispatch by header, field reads and constraints; the crate compiles it into an instruction table executed by a small register VM — zero regex on the default path (the one regex an schema may declare is compiled at load, linear-time). The crate is pure: no I/O, no async, no dependency on other Sentry crates (#![forbid(unsafe_code)]); the host (daemon/edge) owns file loading, hot-reload watchers and converts Violations into pipeline signals.
Schema model — module schema
ProtocolSchema
pub struct ProtocolSchema {
pub id: String,
pub mode: Mode,
pub transport: Transport,
pub on_message: Option<OnMessage>,
pub types: BTreeMap<String, TypeDef>,
pub policies: BTreeMap<String, Policy>,
pub messages: BTreeMap<String, MessageDef>,
}ProtocolSchema is the root (deny_unknown_fields — a wrong key is a load error). The id becomes the label on metrics and signals.
Transport and Mode
pub struct Transport {
pub protocol: TransportKind, // tcp | udp | ws
pub ports: Vec<u16>,
pub flags: BTreeMap<String, String>,
}Transport declares the port(s) the schema guards — the host wires by port. TransportKind accepts tcp, udp and ws. Mode sets the posture: shadow (default) only signals and logs; enforce authorizes the host to close the connection on the first violation.
types — read macros defined per protocol
The crate only ships universal atoms; protocol-specific formats (variable-length integers, custom-prefixed strings) are defined in the schema itself, as a TypeDef, in three forms:
- One-liner sugar (
SugarRead):{prefix/fixed/terminator, endian, decode, max_len, mask_ok, range_ok, len_ok, regex, in/not_in, req, charsets}— desugared into steps at load. - Step body: a register mini-machine with infix expressions (
acc: "(b0 and 0x38) >> 3"; word opsand/or/xor/shl/shr/add/sub/mul/not+ symbolic sugar& | ^ << >> + - *), I/Oread/decode/peek,while min!(expr, cap):with a compile-time-provable bound,if <expr>:with a forward-only block, the statement macroscheck_mask!/check_range!/check_len!andreturn <reg>. - Composite: a list of sequential reads into named sub-fields.
Type-in-type nesting is capped at 8 levels (MAX_TYPE_DEPTH).
messages and policies
messages is the dispatch table: when is mandatory (the union of whens is an implicit allowlist — overlap is a load error), after: [msg] (sequencing), policy (cited severity), keepalive {cadence, rate_limit} and validate (per-field constraint one-liners). policies names severities: default is mandatory; each policy has a weight and on_repeat {count, window, escalate} for repeat escalation.
Compiler — module compile
Compiled
pub struct Compiled {
pub protocols: Vec<CompiledProtocol>,
/* by_port: HashMap<u16, usize> */
}
pub fn add(&mut self, schema: ProtocolSchema, opts: &CompileOptions) -> Result<(), ProtocolError>;
pub fn for_port(&self, port: u16) -> Option<&CompiledProtocol>;Compiled is the compiled set, swappable as a single Arc per reload. add compiles one schema and registers its ports (the first schema claiming a port wins); for_port is the host lookup. CompileOptions injects datasets for not_in ops.
CompiledProtocol, FrameSpec and MessageProgram
Each CompiledProtocol carries the id, mode, ports, preamble (the on_message.run pipeline), per-message programs, the dispatch table (when by header becomes a HashMap; the rest is linear) and the named policies (PolicySpec: weight + on_repeat window). MessageProgram holds the label, the name hash (after bookkeeping), the policy, prerequisites and the straight-line Instr program. Custom macros desugar into the same Vec<Instr> — inlining, zero runtime cost.
When the schema uses check_len!, the compiler captures the framing into FrameSpec (offset, prefix, endian, what the length covers, cap) — the host uses it to split the TCP stream into frames before calling the VM. The entry point is compile_schema.
VM — module engine
ProtocolEngine
pub struct ProtocolEngine { /* ArcSwap<Compiled> */ }
pub fn new(compiled: Compiled) -> Self;
pub fn swap(&self, compiled: Compiled); // hot reload: 1 pointer swap
pub fn current(&self) -> Arc<Compiled>;
pub fn feed(&self, proto: &CompiledProtocol, state: &mut ConnectionState, bytes: &[u8]) -> Result<FrameInfo, Vec<Violation>>;ProtocolEngine wraps the set in an ArcSwap: a reload is one atomic swap with zero downtime — in-flight frames finish on the previous program. feed validates one complete frame (framing is the host's job) and returns a FrameInfo or the violation list (always ≥ 1). feed_on is the free function for hosts holding their own compiled snapshot.
ConnectionState
Per-connection state, lock-free, owned by the connection's task (ConnectionState): a message seen-set (validates after), keepalive windows and per-policy repeat counters (validate on_repeat). is_stale lets the host prune dead connections.
Guards against DoS
Hard budgets enforced at compile time and runtime:
- 16 registers per protocol (
MAX_REGS); op/atom names are reserved. - 512 instructions per compiled message (
MAX_INSTRS_PER_MESSAGE). whilerequires a compile-time-provable bound (min!(expr, cap)); iterations clamp;ifis a forward-only branch — termination is guaranteed.- Type nesting ≤ 8; size caps on every prefixed/terminated read (
max_len,FrameSpec.max).
Errors — module error
ProtocolError covers the whole cycle, each variant naming the schema at fault: Yaml (invalid document), Schema (violates DSL rules), Parse (malformed one-liner or step), Compile (duplicate when, unknown type/macro, register misuse) and MissingDataset (a not_in dataset not provided by the host).
Expressions and instructions
expr is the Pratt parser for the infix expressions (negative literals fold at compile time); instr defines the VM instructions over Value (Bytes/Str/Int). Benchmarks: a compiled frame runs ~0.68 µs vs a ~20.8 µs regex baseline (~30×); read_until with memchr 27 ns vs 1.10 µs scalar (~41×).
The simd feature
sentry-protocol has an optional simd feature (Cargo.toml): terminator scans via memchr and UTF-8 validation via simdutf8 — pure-Rust crates with scalar fallbacks. On by default for the daemon; turn it off to force the scalar path. The simd_vs_scalar comparison bench runs in the same executable with always-present dev-deps.
CLI
sentry protocol validate [--dir <path>] # compiles every *.protocol.yaml in dir
sentry protocol list [--dir <path>] # id, ports, mode, messages
sentry protocol check <schema> --hex <bytes> # test real bytes against the schemaImplemented in protocol_cmd.rs: compile_dir loads and compiles the directory (all-or-nothing per file). The hot-reload watcher (notify, debounce + coalescing + safety poll) and the [protocol] config (enabled, dir, max_schemas) live in the daemon; sentry config validate also compiles the schemas.
Example
An excerpt of the game-relay.protocol.yaml fixture — a legacy binary protocol with a big-endian length prefix, schema-defined types and an escalating policy:
transport:
protocol: tcp
ports: [14901]
on_message:
run: "check_len! | parse_header!"
types:
LPStr: {prefix: u16, decode: utf8, max_len: 1024}
VLInt:
body:
- b0: "read u8"
- n: "(b0 and 0x38) >> 3"
- acc: "b0 and 0x03"
- while min!(n, 4):
- bi: "read u8"
- check_mask!(bi, 0xC0, 0x40)
- acc: "(acc << 6) or (bi and 0x3F)"
- if sign:
- acc: "-acc"
- return acc
policies:
default: {weight: 20}
unknown_header:
weight: 35
on_repeat: {count: 3, window: 60s, escalate: true}
messages:
sso_ticket_event:
when: {header: 400}
validate:
- sso_ticket: LPStr >2 <64 regex 'GAME-[a-zA-Z0-9]+-3324'
ping_event:
when: {header: 4096}
keepalive: {cadence: 30s, rate_limit: 2/60s}The chat-relay.protocol.yaml fixture shows the same DSL over WebSocket with a JSON payload — no custom macros needed, validate ops apply to the decoded text fields.