Skip to content

Fix issue #10 and a bunch of other stuff#11

Merged
Folleach merged 10 commits intoFolleach:mainfrom
Kaydax:fix-issue-10
Feb 27, 2026
Merged

Fix issue #10 and a bunch of other stuff#11
Folleach merged 10 commits intoFolleach:mainfrom
Kaydax:fix-issue-10

Conversation

@Kaydax
Copy link
Contributor

@Kaydax Kaydax commented Feb 26, 2026

Here is a bunch of changes that fix #10 and also fixes a random crash that happens. Also improve the code base to be way more robust.

I was sent down a rabbit hole because of the fact I forgot to up the file descriptor limit on my proxy server that sits between cloudflare and mineginx, thus causing connection limits

List of changes so far:

  • Fixed buffer expansion panic (todo!() → actual implementation)
  • Fixed off-by-one in buffer data length calculation
  • Fixed packet data length check after reading packet ID
  • Fixed string reader bounds check and UTF-8 panic
  • Added panic logging for spawned tasks
  • Replaced .unwrap() calls in main with error handling
  • Fixed TCP connections not sending FIN on close
  • Ensured forwarding tasks are awaited before cleanup
  • Fixed domain matching to handle trailing dots and case differences
  • Set global panic hook for crash diagnostics
  • Used socket2 for TCP listener with 1024 backlog and SO_REUSEADDR

@Kaydax Kaydax marked this pull request as ready for review February 26, 2026 04:23
Copilot AI review requested due to automatic review settings February 26, 2026 04:23
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses issue #10 (random connection/ping failures) by fixing multiple critical bugs and implementing architectural improvements to prevent connection stalls in the mineginx Minecraft proxy server.

Changes:

  • Fixed multiple critical bugs in packet parsing (buffer expansion panic, off-by-one errors, packet length calculations, string UTF-8 handling)
  • Replaced complex oneshot channel signaling with simpler TCP FIN-based coordination for bidirectional stream forwarding
  • Pre-resolved DNS at startup to prevent blocking on musl/scratch containers, with lazy resolution fallback for upstreams unavailable at startup
  • Upgraded TCP listener with socket2 for larger connection backlog (1024) and SO_REUSEADDR support
  • Improved error handling by replacing .unwrap() calls and adding global panic hook for diagnostics
  • Enhanced domain matching with case-insensitive comparison and trailing dot normalization

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
mineginx/src/stream.rs Simplified bidirectional TCP forwarding by removing oneshot channels and using TCP FIN for coordination; added comprehensive documentation
mineginx/src/main.rs Implemented DNS pre-resolution with lazy fallback, socket2 integration for robust listener setup, panic hook for diagnostics, and improved error handling
minecraft/src/serialization.rs Fixed critical bugs: implemented buffer expansion (was todo!()), corrected off-by-one in data_len(), added varint_size() for accurate packet length calculation, improved string reader with proper bounds checking and UTF-8 error handling
mineginx/Cargo.toml Added socket2 dependency for advanced socket configuration
Cargo.lock Updated dependencies including socket2 0.5.10 and transitive Windows dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -191,6 +241,12 @@ const CONFIG_FILE: &str = "./config/mineginx.yaml";
#[tokio::main(flavor = "multi_thread")]
async fn main() -> ExitCode {
SimpleLogger::new().init().unwrap();
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() call on logger initialization should be replaced with proper error handling. If logger initialization fails, the application should either handle it gracefully or exit with a clear error message. This is especially important given that the PR description mentions replacing .unwrap() calls with error handling.

Copilot uses AI. Check for mistakes.
value >>= 7;
}
}

Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new varint_size function calculates the encoded size of a VarInt, which is critical for the packet length fix. However, there are no tests verifying this function works correctly. Given that the minecraft crate has existing test infrastructure (tests/field_types.rs, tests/truncate_to_zero.rs), consider adding test cases that verify varint_size returns the correct values for edge cases like 0, 127, 128, -1, and maximum/minimum i32 values. This would help prevent regressions in this critical protocol parsing logic.

Suggested change
#[cfg(test)]
mod tests {
use super::varint_size;
#[test]
fn test_varint_size_edge_cases() {
// Basic small values
assert_eq!(varint_size(0), 1);
assert_eq!(varint_size(1), 1);
assert_eq!(varint_size(127), 1);
// Boundary where an extra byte is needed
assert_eq!(varint_size(128), 2);
// Negative values and extremes of i32 range
assert_eq!(varint_size(-1), 5);
assert_eq!(varint_size(i32::MAX), 5);
assert_eq!(varint_size(i32::MIN), 5);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines 225 to 228
fn expand_buffer(&mut self) {
todo!()
let new_len = self.buffer.len() * 2;
self.buffer.resize(new_len, 0);
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffer expansion logic was changed from todo!() to actual implementation, and the data_len() calculation had an off-by-one error fixed. Given that the minecraft crate has existing test infrastructure, consider adding tests that verify buffer expansion works correctly when the buffer fills up, and that data_len() returns accurate values in various states (empty buffer, partial data, full buffer). These are critical bugs that were fixed and should have regression tests.

Copilot uses AI. Check for mistakes.
}
let packet = match MinecraftPacket::make_raw(0, &handshake) {
Some(v) => v,
None => return
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return without error logging is inconsistent with the error handling pattern used elsewhere in this function (e.g., lines 123, 128). Consider logging an error message before returning to aid debugging, especially since packet serialization failures could indicate protocol issues.

Suggested change
None => return
None => {
error!("failed to serialize handshake packet for upstream: {}", upstream.proxy_pass);
return;
}

Copilot uses AI. Check for mistakes.
match upstream.write_all(&packet[0..packet.len()]).await {
match upstream_conn.write_all(&packet[0..packet.len()]).await {
Ok(_) => { },
Err(_) => return
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return without error logging is inconsistent with the error handling pattern used elsewhere in this function. Consider logging an error message to indicate that writing the handshake packet to the upstream failed, which would aid in diagnosing connection issues.

Suggested change
Err(_) => return
Err(e) => {
error!("failed to write handshake packet to upstream '{}': {}", upstream.proxy_pass, e);
return;
}

Copilot uses AI. Check for mistakes.
error!("failed to listen on {}: {e}", &server.listen);
return ExitCode::from(3);
}
socket.set_nonblocking(true).unwrap();
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() call on set_nonblocking(true) should be replaced with proper error handling like the other socket operations in this section. This is inconsistent with the surrounding error handling pattern where all socket operations return errors via ExitCode::from(3).

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +107
cache.insert(server_desc.proxy_pass.clone(), entry.clone());
entry
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition in the lazy DNS resolution logic. If multiple clients connect simultaneously to the same unresolved upstream, they will all attempt to resolve it concurrently (line 97). While each will successfully insert their result into the cache (line 106), this results in unnecessary duplicate DNS lookups. Consider using a more sophisticated caching strategy like checking again after acquiring the write lock, or using a Mutex around the resolution attempt itself to ensure only one resolution happens per upstream.

Suggested change
cache.insert(server_desc.proxy_pass.clone(), entry.clone());
entry
// Re-check the cache under the write lock in case another task
// has already resolved and inserted this upstream.
if let Some(existing) = cache.get(&server_desc.proxy_pass).cloned() {
existing
} else {
cache.insert(server_desc.proxy_pass.clone(), entry.clone());
entry
}

Copilot uses AI. Check for mistakes.
match upstream.write_all(&minecraft.take_buffer()).await {
match upstream_conn.write_all(&minecraft.take_buffer()).await {
Ok(_) => {},
Err(_) => {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return without error logging is inconsistent with the error handling pattern used elsewhere in this function. Consider logging an error message to indicate that flushing the unread buffer to the upstream failed.

Suggested change
Err(_) => {
Err(e) => {
error!("failed to flush unread buffer to upstream: {}", e);

Copilot uses AI. Check for mistakes.
None => return
};
match upstream.write_all(&packet[0..packet.len()]).await {
match upstream_conn.write_all(&packet[0..packet.len()]).await {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slice &packet[0..packet.len()] is redundant and can be simplified to &packet since it represents the entire slice. This is a minor style improvement for code readability.

Suggested change
match upstream_conn.write_all(&packet[0..packet.len()]).await {
match upstream_conn.write_all(&packet).await {

Copilot uses AI. Check for mistakes.
Copy link
Owner

@Folleach Folleach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that is so many changes you've made! Hello!

Most of these improvements are spot on and really make sense.
I grateful how you rewrote forward_stream function, it was awfuuuul, haha. I wrote it one evening after falling down a rabbit hole and told myself: "Will rewrite this someday", and now you've simplified it so much. Thanks a lot!

I've had a quick look at the code, and there are a few bits I'd like to tweak. If you could make those changes, that'd be awesome. Or I can handle it myself later when I have some free time.
Either way, this is a great improvements to the code base!

@Kaydax
Copy link
Contributor Author

Kaydax commented Feb 26, 2026

@Folleach I removed the DNS cache as it really was only there as a test to see if DNS resolution under docker was causing some weird issue with the pinging problems I was having. I also removed all the redundant comments and simplified the huge comment

@Kaydax Kaydax requested a review from Folleach February 26, 2026 21:22
@Folleach Folleach merged commit ec3ee18 into Folleach:main Feb 27, 2026
1 check passed
@Folleach
Copy link
Owner

Looks great.
Now it is available on Docker Hub at the tag v0.1.6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Randomly cannot ping or connect to server

3 participants