feat: pre-play lifecycle timeouts, config keepalive, and favicon caching - #548
feat: pre-play lifecycle timeouts, config keepalive, and favicon caching#548carabistouflette wants to merge 3 commits into
Conversation
|
This pull request has conflicts with the base branch "master". Please resolve those so we can test out your changes. |
9c852db to
474f78d
Compare
|
Conflicts have been resolved! 🎉 |
474f78d to
48b16ed
Compare
…et kicks - get_raw_packet decodes the length VarInt with a manual 3-byte cap like vanilla Varint21FrameDecoder (rejects 'length wider than 21-bit' and zero-length frames) instead of an unbounded VarInt read; decode throughput measured at +10.5% (criterion, 256-packet stream, p < 0.05) - compressed packets declaring a size below the server threshold are rejected with PacketError::BelowThreshold before inflation, matching vanilla CompressionDecoder; the check only applies to nonzero (compressed) payloads - pre-play connections are kicked with multiplayer.disconnect.invalid_packet when packet processing fails instead of logging and continuing - play connections close when a packet fails to process (desynchronized stream) and unrecognized immediate packets log at debug level - adds packet_reader_framing criterion bench and vanilla-parity regression tests for all three framing checks
- login phase gets a 30-second deadline like vanilla ServerLoginPacketListenerImpl (MAX_TICKS_BEFORE_LOGIN = 600); expiring clients are kicked with multiplayer.disconnect.slow_login - configuration phase sends vanilla-timed keep-alives (challenge every 15 idle seconds, disconnect after 30 unanswered) using the existing common CKeepAlive/SKeepAlive packets; out-of-order answers are a timeout kick - favicon is loaded once into Server::favicon at startup (warn once when the configured path is unreadable), removing per-ping disk I/O from the status request path
- config keep-alive now matches vanilla ServerCommonPacketListenerImpl exactly: a challenge every 15 s since the last send, and disconnect.timeout at the next boundary (15 s after an unanswered challenge). The previous 30 s threshold lagged vanilla and landed 30-45 s after the challenge on the 15 s tick. - keep-alive decisions are pure tracker logic (KeepAliveDecision), unit tested against the vanilla boundaries including out-of-order answers - accepted answers smooth the round trip into a latency seeded into the play connection, like vanilla's CommonListenerCookie.latency() - the periodic challenge write and the timeout kick are bounded (1 s): a wedged socket closes the connection instead of stalling the read loop - favicon loading validates the PNG IHDR like vanilla PngInfo (64x64) and logs the vanilla 'Couldn't load server icon' error instead of serving arbitrary bytes
5dd8c52 to
13fc8f1
Compare
| connection_update = connection_updates_recv.recv() => { | ||
| IncomingEvent::ConnectionUpdate(connection_update) | ||
| } | ||
| _ = config_keepalive_tick.tick() => IncomingEvent::KeepAliveTick, |
There was a problem hiding this comment.
this tick can cancel get_raw_packet after some bytes was already read then remaining packet body is parsed as new frame anilla keeps incomplete frame bytes
| let mut connection = None; | ||
| // One-second ticks give the vanilla 15-second send and timeout boundaries | ||
| // one-second resolution on a single shared timer. | ||
| let mut config_keepalive_tick = interval(Duration::from_secs(1)); |
There was a problem hiding this comment.
vanilla checks config keepalive every server connection tick not with separate 1s timer timing is different here
| match decision { | ||
| KeepAliveDecision::None => {} | ||
| KeepAliveDecision::Send(challenge) => { | ||
| if timeout( |
There was a problem hiding this comment.
vanilla only queues this keepalive and has no 1s write timeout slow valid client can be closed too early
| .on_chunk_batch_received_by_client(packet.desired_chunks_per_tick); | ||
| } | ||
| ImmediatePlayPacket::Unknown(id) => log::info!("play packet id {id} is not known"), | ||
| ImmediatePlayPacket::Unknown(id) => log::debug!("play packet id {id} is not known"), |
There was a problem hiding this comment.
why is it now debug any reason for it?
| } | ||
|
|
||
| // Vanilla uses `Util.getMillis()` as the challenge id. | ||
| let challenge = SystemTime::now() |
There was a problem hiding this comment.
Util.getMillis() uses monotonic time, not unix time
| // Vanilla uses `Util.getMillis()` as the challenge id. | ||
| let challenge = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .expect("System time before UNIX EPOCH") |
There was a problem hiding this comment.
i mean hm would be an edge case where it could trigger so idk i dont lioe thaz
| Ok(ConnectionAction::none()) | ||
| } | ||
| config::S_KEEP_ALIVE => { | ||
| let packet = SKeepAlive::read_packet(data)?; |
There was a problem hiding this comment.
this new handler accepts keepalive with extra trailing bytes because payload end is not checked vanilla rejects it before handler
|
This pull request has conflicts with the base branch "master". Please resolve those so we can test out your changes. |
|
Conflicts have been resolved! 🎉 |
|
This pull request has conflicts with the base branch "master". Please resolve those so we can test out your changes. |
Type of change
Description
Implements the vanilla pre-play lifecycle:
ServerLoginPacketListenerImpl,MAX_TICKS_BEFORE_LOGIN = 600) is kicked withmultiplayer.disconnect.slow_login. The read loop wraps packet reads intimeout_atwhile the deadline is active.disconnect.timeoutkick at the next boundary — vanillaServerCommonPacketListenerImpl.keepConnectionAlivetiming. Out-of-order answers are a timeout kick, and accepted answers smooth the round-trip latency that is seeded into the play connection (vanillaCommonListenerCookie.latency()). Uses the existing commonCKeepAlive/SKeepAlivepackets — no new packet types.Server::faviconat startup (a single warning is logged when the configured path is unreadable), removing blocking filesystem I/O from the per-pinghandle_status_requestpath. The file is validated like vanillaPngInfo: it must be a PNG with a 64x64IHDR, otherwise it is rejected with the vanillaCouldn't load server iconerror.How this was tested
cargo test -p steel-login(22 tests) andcargo test -p steel-core --lib(2467 tests) pass;cargo clippy -r --all-targetsclean.keepalive_decisions_match_vanilla_boundariesandkeepalive_answer_smooths_latency_and_rejects_out_of_orderpin the vanilla keep-alive boundaries (15 s cadence, unanswered → timeout, out-of-order/duplicate answers rejected, latency smoothing);load_favicon_rejects_invalid_pngs_like_vanillacovers the favicon validation matrix.Screenshots / logs
N/A (server-log behavior only).
Checklist
Additional notes
pr4-preplay-lifecycleis stacked onpr3-read-path-hardening.