feat(server): add multi-listener server set

`Server` answers on one address. A host that must serve several — an
explicit IPv4 and IPv6 pair, or two interface addresses — needs several
of them, and something has to own their construction, their shutdown and
each other's failures.

`ServerSet` is that owner, and deliberately not a protocol object: it
holds no session state, no tokens and no clock, and nothing it does is
visible on the wire. Each listener stays a complete `Server` with its own
socket, core, session table and tokens, so a token issued by one is
unknown at the others and every bound in the cloned `ServerConfig` —
`max_sessions` included — applies per listener rather than per process.

Construction binds every listener or none, preserving requested order and
recording the addresses actually bound. `run` moves each listener into
its own task, fans one shutdown future out over a watch channel, and
joins them all: a listener that fails, or that returns before shutdown
was asked for, shuts its siblings down and fails the set, because a
service configured for two families must not continue silently as one.

A genuine IPv6 listener sharing an explicit port with an IPv4 sibling is
created `IPV6_V6ONLY` before it is bound, since a dual-stack socket would
otherwise claim the IPv4 port too. That is the only pre-bind step, it
uses safe `socket2` APIs, and an IPv4-mapped address is read as the IPv4
listener it is — never as the IPv6 half of a pair. Standalone
`Server::bind` semantics are unchanged.
This commit is contained in:
Nils Andreas Svee committed 2026-08-14 16:04:46 +07:00
1 parent fabd698187
commit 09f0d6e02e
4 files changed
+953 -5

No files matched your search

+22 -5
View File
@@ -13,9 +13,24 @@
//! engine stays testable. Each reply it produces is an [`OutboundDatagram`]:
//! the packet and the raw traffic class it must be sent with. [`Server`] owns
//! one Tokio UDP listener and one core, and provides sequential receive, reply,
//! scheduled expiry maintenance and caller-controlled shutdown. The crate is
//! intentionally Tokio-native; there is no blocking or alternate-runtime
//! counterpart and no transport abstraction.
//! scheduled expiry maintenance and caller-controlled shutdown. [`ServerSet`]
//! owns one or more `Server`s as a single service: it binds them all or none,
//! runs each in its own task, fans one shutdown out to them and fails the group
//! if any listener does. The crate is intentionally Tokio-native; there is no
//! blocking or alternate-runtime counterpart and no transport abstraction.
//!
//! # One listener or several
//!
//! `Server` is the primitive and stays directly usable. `ServerSet` is the
//! service around it, and a set of one is an ordinary set rather than a special
//! case — the server CLI runs every invocation, single-bind included, through
//! one.
//!
//! Listeners in a set share nothing but the configuration they were cloned
//! from. Each has its own socket, core, session table and tokens, so a token
//! issued by one is unknown at the others, and every bound in
//! [`ServerConfig`] — `max_sessions` included — applies **per listener**, not
//! per process.
//!
//! # Rejection is silence
//!
@@ -60,8 +75,8 @@
//! per-session rate limiting, idle expiry, the maximum-duration
//! server-initiated close, the negotiated per-session reply traffic class,
//! server fill, the optional timestamp-allowance and DSCP capability
//! restrictions, wildcard reply-source selection and Tokio UDP orchestration.
//! Running several listeners in one process remains a separate slice.
//! restrictions, wildcard reply-source selection, Tokio UDP orchestration and
//! multi-listener supervision through [`ServerSet`].
//!
//! # Capability restrictions
//!
@@ -99,6 +114,7 @@ mod fill;
mod negotiate;
mod runtime;
mod session;
mod set;
mod socket_io;
mod socket_options;
mod token;
@@ -113,3 +129,4 @@ pub use config::{
pub use core::{OutboundDatagram, ServerCore};
pub use error::ServerError;
pub use runtime::{Server, ServerRuntimeError};
pub use set::{ServerSet, ServerSetError};