fix(server): keep unmarkable listeners serving

This commit is contained in:
Nils Andreas Svee committed 2026-08-13 22:26:58 +07:00
1 parent 5420387846
commit 1830429ce5
3 files changed
+116 -26

No files matched your search

+81 -6
View File
@@ -44,6 +44,14 @@ pub struct Server {
/// queried at construction, in which case the send path asks again rather
/// than guessing a family.
listener_is_ipv4: Option<bool>,
/// Whether this server has ever successfully applied a nonzero traffic
/// class to the socket.
///
/// This is not a cache and never elides a call — the class is applied
/// before every send regardless. It is consulted only when applying one
/// *fails*, to tell "the socket may be carrying a marking of ours" from
/// "it cannot be carrying one".
socket_is_marked: bool,
}
impl Server {
@@ -65,16 +73,36 @@ impl Server {
core: ServerCore::new(config),
recv_buffer: vec![0; RECEIVE_BUFFER_LEN],
listener_is_ipv4,
socket_is_marked: false,
}
}
/// Applies one reply's raw traffic class to the listener socket.
/// Applies one reply's raw traffic class to the listener socket, and
/// reports whether that reply may be sent.
///
/// This runs before *every* send, and zero is applied as deliberately as
/// The class is applied before *every* send, and zero as deliberately as
/// any other value: a listener serves many sessions from one socket, so
/// skipping the call for an unmarked reply — an open reply, or a session
/// that negotiated nothing — would send it under whichever marking the
/// previous reply left behind.
///
/// When the option cannot be applied, [`may_send_unappliable`] decides. A
/// marking this server asked for and did not get must never be replaced by
/// silently sending under the previous one, so those replies are dropped;
/// but a host that refuses the option outright — some Windows builds do not
/// support `IP_TOS`, and a few targets have no safe setter at all — must
/// still be able to run a server, and it can, because a socket this server
/// has never marked has no marking of ours to clear.
fn prepare_reply_traffic_class(&mut self, traffic_class: u8) -> bool {
match self.apply_reply_traffic_class(traffic_class) {
Ok(()) => {
self.socket_is_marked = traffic_class != 0;
true
}
Err(_) => may_send_unappliable(traffic_class, self.socket_is_marked),
}
}
fn apply_reply_traffic_class(&self, traffic_class: u8) -> io::Result<()> {
let is_ipv4 = match self.listener_is_ipv4 {
Some(is_ipv4) => is_ipv4,
@@ -95,13 +123,16 @@ impl Server {
/// Graceful shutdown sends no session-close packets and leaves no hidden
/// receive task behind, even while a reply send is waiting for socket
/// writability. Per-packet send failures and short sends drop only that
/// reply, and so does a failure to apply that reply's traffic class —
/// reply, and so does a failure to apply a marking that reply needed —
/// preparing one packet is not a reason to stop serving every other
/// session, and the next reply makes its own independent attempt. Receive
/// failures and internal core failures terminate the loop with an error.
///
/// A reply whose class could not be applied is never sent anyway: sending it
/// would put it on the wire under the previous reply's marking.
/// A reply whose marking could not be applied is not sent instead: sending
/// it would put it on the wire under the previous reply's marking. An
/// *unmarked* reply on a socket this server has never marked is the one
/// exception, so a host that does not support the option at all still runs
/// a working server. See [`Server::prepare_reply_traffic_class`].
pub async fn run<F>(&mut self, shutdown: F) -> Result<(), ServerRuntimeError>
where
F: Future<Output = ()>,
@@ -126,7 +157,7 @@ impl Server {
}
if let Some(reply) = self.core.handle_datagram(peer, &self.recv_buffer[..len])? {
if self.apply_reply_traffic_class(reply.traffic_class()).is_err() {
if !self.prepare_reply_traffic_class(reply.traffic_class()) {
continue;
}
let send = self.socket.send_to(reply.bytes(), peer);
@@ -148,6 +179,21 @@ impl Server {
}
}
/// Whether a reply may still be sent after its traffic class could not be
/// applied.
///
/// Only an unmarked reply on a socket this server has never successfully marked
/// qualifies. Both halves matter: a reply that wanted a marking must not go out
/// without one, and once this server has put a nonzero class on the socket, a
/// failure to clear it means the socket may still be carrying it.
///
/// A socket handed to [`Server::from_socket`] pre-marked by its creator is
/// outside what this can know; explicit marking is the server's own to manage
/// from a fresh listener.
fn may_send_unappliable(traffic_class: u8, socket_is_marked: bool) -> bool {
traffic_class == 0 && !socket_is_marked
}
/// Failure to create or run a Tokio UDP server.
#[derive(Debug, Error)]
pub enum ServerRuntimeError {
@@ -323,4 +369,33 @@ mod tests {
fn unthrottled() -> ServerConfig {
ServerConfig::default().with_min_send_interval(Duration::ZERO)
}
/// The rule that decides what an unappliable traffic class means, which no
/// normal interface can reach: it needs a host that refuses the socket
/// option, and the hosts these tests run on do not.
#[test]
fn only_an_unmarked_reply_on_a_never_marked_socket_survives_an_apply_failure() {
for (traffic_class, socket_is_marked, sendable, why) in [
(
0,
false,
true,
"no marking wanted, and none of ours to clear",
),
(0, true, false, "our own marking may still be on the socket"),
(
0xb8,
false,
false,
"a marking that was wanted but not applied",
),
(0xb8, true, false, "and likewise over a marking already set"),
] {
assert_eq!(
may_send_unappliable(traffic_class, socket_is_marked),
sendable,
"{why}"
);
}
}
}