Skip to main content

ServerCore

Struct ServerCore 

Source
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 nothing

A 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: &params, 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

Source

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.

Source

pub fn config(&self) -> &ServerConfig

This core’s configuration.

Source

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.

Source

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.

Trait Implementations§

Source§

impl Debug for ServerCore

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.