pub struct ServerCore { /* private fields */ }Expand description
The deterministic protocol and session engine of the server.
ServerCore owns admission, negotiation, the session table and reply
construction. It performs no I/O: a caller hands it a source endpoint and
the bytes of one received datagram, and gets back the bytes to send to that
endpoint, or nothing. Everything it does is a pure function of its state and
that input, except for drawing session tokens and sampling the clock an echo
reply’s timestamps come from — both of which are private injected seams, so
the whole engine stays deterministically testable.
The Tokio runtime wraps it directly:
recv_from -> core.handle_datagram(peer, packet) -> Some(reply) -> send_to
-> None -> send nothingA reply is an OutboundDatagram: the bytes to send plus the transport
policy that sending them requires. The core is where session policy lives, so
it hands that policy out with the packet rather than leaving the runtime to
rediscover it from the session table or from the packet it is about to send.
It is public as a low-level engine for a pre-1.0 crate, not as a promise of runtime independence. The server is intentionally Tokio-native; this is not a transport abstraction, and there is no blocking or alternate-runtime counterpart.
§Example
The core can be driven directly with a normally encoded open request. It returns a reply to send and records the new session without performing I/O:
use std::net::{Ipv4Addr, SocketAddr};
use irtt_proto::{decode_open_reply, encode_request, Params, RequestToEncode};
use irtt_server::{ServerConfig, ServerCore};
let peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 40_000));
let mut core = ServerCore::new(ServerConfig::default());
let params = Params::with_protocol_defaults();
let request = encode_request(
RequestToEncode::Open { params: ¶ms, no_test: false },
None,
)
.unwrap();
let reply = core.handle_datagram(peer, &request).unwrap().unwrap();
let open = decode_open_reply(reply.bytes(), None).unwrap();
assert_ne!(open.token, 0);
assert_eq!(core.session_count(), 1);§Scope
The core implements open handling, session creation and negotiation, normal echo processing, per-session rate limiting, idle expiry, maximum-duration close and client-initiated close. It decides the traffic class each reply is to be sent with; actually applying it to a socket is the runtime’s job. It also settles each session’s server fill and generates the payload bytes of every echo reply. Incoming authenticated requests enforce logical expiry exactly; the runtime also calls a private maintenance hook to reclaim idle sessions when no traffic arrives.
Implementations§
Source§impl ServerCore
impl ServerCore
Sourcepub fn new(config: ServerConfig) -> Self
pub fn new(config: ServerConfig) -> Self
Creates a server core drawing session tokens from the operating system’s random source and timestamps from the system clock.
Sourcepub fn config(&self) -> &ServerConfig
pub fn config(&self) -> &ServerConfig
This core’s configuration.
Sourcepub fn session_count(&self) -> usize
pub fn session_count(&self) -> usize
The number of live sessions.
This is the observable a caller genuinely needs — how much session state the server is holding against its bound. The sessions themselves stay crate-private until their shape settles.
Sourcepub fn handle_datagram(
&mut self,
peer: SocketAddr,
packet: &[u8],
) -> Result<Option<OutboundDatagram>, ServerError>
pub fn handle_datagram( &mut self, peer: SocketAddr, packet: &[u8], ) -> Result<Option<OutboundDatagram>, ServerError>
Handles one received datagram, returning the reply to send, if any.
A reply is an OutboundDatagram: the encoded packet and the raw
traffic class it must be sent with. Open and no-test replies are always
unmarked, and an echo reply — including one carrying the
maximum-duration close — takes its session’s negotiated class. The
runtime applies that class to the socket before sending; nothing here
touches a socket, and a class this core hands out is a request, not a
promise that the host can honor it.
Ok(None) means “answer nothing”. The protocol defines no error reply,
no reset and no NACK, so every rejection is a silent discard and a
client distinguishes “rejected” from “lost” only by timing out. A
datagram is discarded when it is structurally invalid, carries the reply
flag, disagrees with this server’s authentication configuration, fails
authentication, carries a malformed or out-of-range open parameter
payload, negotiates to parameters the server could not execute, would
exceed the session bound, is longer than the configured maximum packet
length, or names no live session from the endpoint that opened it. None
of that mutates any live session.
Ok(None) is not only a rejection, though. A close request that does
release its session is answered with nothing as well, because protocol
version 1 defines no acknowledgement for one — and so is an echo that
found its session’s rate allowance spent, which advances no statistic
but does keep the session alive.
Sessions whose idle deadline has passed are released before the request is dispatched, so a request may find the session it names already gone, and one request may release an unrelated session. Expiry is never signalled; a client sees only silence.
Authentication is checked before open parameters are decoded and before any session is looked up. That ordering is deliberate: a packet that fails authentication is discarded without parsing untrusted parameter data or consulting the session table, and no reply behavior may reveal which stage rejected it.
Every instant this reports is one the core sampled itself: a caller hands over bytes and an endpoint, not timings. The Tokio runtime, which does have transport metadata, reaches the same implementation through a crate-private entry point that may add a kernel-observed arrival time; this path behaves exactly as it does with none.
§Errors
Returns ServerError only for an internal failure — the random source
failing, no unique token being found within the retry budget, or a
server-generated reply failing to encode. Rejected input is never an
error.