From 975726c675c41d12190923e2944dd1409496726b Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 17:30:02 -0500 Subject: [PATCH 01/10] Attempt to fix #10 --- minecraft/src/serialization.rs | 34 ++++++++++++++------- mineginx/src/main.rs | 18 ++++++++++-- mineginx/src/stream.rs | 54 ++++++++++++---------------------- 3 files changed, 58 insertions(+), 48 deletions(-) diff --git a/minecraft/src/serialization.rs b/minecraft/src/serialization.rs index 2a5a0b3..ed10977 100644 --- a/minecraft/src/serialization.rs +++ b/minecraft/src/serialization.rs @@ -9,6 +9,19 @@ use crate::{ const SEGMENT_BITS: u32 = 0x7F; const CONTINUE_BIT: u32 = 0x80; +/// Returns the number of bytes a VarInt value occupies when encoded. +fn varint_size(value: i32) -> usize { + let mut value = value as u32; + let mut size = 0; + loop { + size += 1; + if (value & !SEGMENT_BITS) == 0 { + return size; + } + value >>= 7; + } +} + #[derive(Debug, PartialEq)] pub enum ReadingError { Insufficient, @@ -93,7 +106,7 @@ impl MinecraftStream { } pub fn data_len(&self) -> usize { - self.free - self.position + 1 + self.free - self.position } pub fn take_buffer(&mut self) -> Vec { @@ -160,8 +173,11 @@ impl MinecraftStream { where T: PacketDeserializer, { - if signature.length > self.data_len() { - match &self.fill_buffer_from_source(signature.length).await { + // signature.length includes the packet_id VarInt which was already consumed + // in read_signature, so subtract its encoded size to get the actual data length + let data_needed = signature.length.saturating_sub(varint_size(signature.packet_id)); + if data_needed > self.data_len() { + match &self.fill_buffer_from_source(data_needed).await { Ok(_) => {} Err(_) => return Err(ReadingError::Closed), }; @@ -199,10 +215,6 @@ impl MinecraftStream { T::read(self) } - fn remain_len(&self) -> usize { - self.buffer.len() - self.position - } - fn copy_buffer_to_start(&mut self) { let data_len = self.free - self.position; self.buffer.copy_within(self.position..self.free, 0); @@ -211,7 +223,8 @@ impl MinecraftStream { } fn expand_buffer(&mut self) { - todo!() + let new_len = self.buffer.len() * 2; + self.buffer.resize(new_len, 0); } async fn fill_buffer_from_source(&mut self, required: usize) -> Result<(), ()> { @@ -295,16 +308,15 @@ impl FieldReader for String { fn read( stream: &mut MinecraftStream, ) -> Result { - // todo: there is a bug - read_field changes position of the stream, but below can happen reading error if packet doesn't fully read let length = stream.read_field::()? as usize; - if length > stream.remain_len() { + if length > stream.data_len() { return Err(ReadingError::Insufficient); } let mut vec: Vec = vec![0; length]; vec.copy_from_slice(&stream.buffer[stream.position..stream.position + length]); stream.position += length; - Ok(String::from_utf8(vec).unwrap()) + String::from_utf8(vec).map_err(|_| ReadingError::Invalid) } } diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index a56c75e..a7484dc 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -191,6 +191,12 @@ const CONFIG_FILE: &str = "./config/mineginx.yaml"; #[tokio::main(flavor = "multi_thread")] async fn main() -> ExitCode { SimpleLogger::new().init().unwrap(); + + std::panic::set_hook(Box::new(|panic_info| { + eprintln!("PANIC: {}", panic_info); + log::error!("panic occurred: {}", panic_info); + })); + let mut args = env::args(); if args.any(|x| &x == "-t") { return match check_config().await { @@ -213,14 +219,22 @@ async fn main() -> ExitCode { continue; } info!("listening {}", &server.listen); - let listener = TcpListener::bind(&server.listen).await.unwrap(); + let listener = match TcpListener::bind(&server.listen).await { + Ok(l) => l, + Err(e) => { + error!("failed to bind {}: {e}", &server.listen); + return ExitCode::from(3); + } + }; let conf = config.clone(); let task = tokio::spawn(async move { handle_address(&listener, conf).await; }); listening.insert(server.listen.to_string(), ListeningAddress(task)); } - tokio::signal::ctrl_c().await.unwrap(); + if let Err(e) = tokio::signal::ctrl_c().await { + error!("failed to listen for ctrl_c signal: {e}"); + } info!("shutdown"); ExitCode::from(0) } diff --git a/mineginx/src/stream.rs b/mineginx/src/stream.rs index c3fa842..c25ac50 100644 --- a/mineginx/src/stream.rs +++ b/mineginx/src/stream.rs @@ -1,7 +1,7 @@ use tokio::{ task::JoinHandle, sync::oneshot::{ - Sender, Receiver, error::TryRecvError + Sender, Receiver }, net::tcp::{ OwnedReadHalf, OwnedWriteHalf @@ -11,50 +11,34 @@ use tokio::{ pub fn forward_stream( close: Sender<()>, - close_by_other: Receiver<()>, + mut close_by_other: Receiver<()>, mut reader: OwnedReadHalf, mut writer: OwnedWriteHalf, buffer_size: usize) -> JoinHandle<()> { tokio::spawn(async move { let mut buf = vec![0; buffer_size]; - let mut close = Some(close); - let mut close_by_other = Some(close_by_other); - let mut closed = false; loop { - if let Some(mut receiver) = close_by_other.take() { - match receiver.try_recv() { - Err(e ) => closed |= e == TryRecvError::Closed, - Ok(_) => closed = true - } - } - if closed { - return; - } - let res = reader.read(&mut buf).await; - match res { - Ok(size) => { - if size == 0 { - if let Some(sender) = close.take() { - closed = true; - _ = sender.send(()); - } - } - let writed = writer.write_all(&buf[..size]).await; - match writed { - Ok(_) => { }, - Err(_) => { - if let Some(sender) = close.take() { - _ = sender.send(()) + tokio::select! { + _ = &mut close_by_other => { + return; + }, + res = reader.read(&mut buf) => { + match res { + Ok(0) => { + _ = close.send(()); + return; + }, + Ok(size) => { + if writer.write_all(&buf[..size]).await.is_err() { + _ = close.send(()); + return; } + }, + Err(_) => { + _ = close.send(()); return; } } - }, - Err(_) => { - if let Some(sender) = close.take() { - _ = sender.send(()); - } - return; } } } From b3f1d71a8ba27036464cf2098b3c408ecbc922d0 Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 18:24:37 -0500 Subject: [PATCH 02/10] Attempt to fix endless pinging --- mineginx/src/main.rs | 13 +++++++++---- mineginx/src/stream.rs | 20 +++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index a7484dc..8c2bb02 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -97,18 +97,23 @@ async fn handle_client(mut client: TcpStream, config: Arc) { let (upstream_reader, upstream_writer) = upstream.into_split(); let (client_close_sender, client_close_receiver) = oneshot::channel::<()>(); let (upstream_close_sender, upstream_close_receiver) = oneshot::channel::<()>(); - forward_stream( + let buf_size = upstream_server.buffer_size.map(|b| b as usize).unwrap_or(2048); + let h1 = forward_stream( client_close_sender, upstream_close_receiver, client_reader, upstream_writer, - if let Some(buffer_size) = upstream_server.buffer_size { buffer_size as usize } else { 2048 }); - forward_stream( + buf_size); + let h2 = forward_stream( upstream_close_sender, client_close_receiver, upstream_reader, client_writer, - if let Some(buffer_size) = upstream_server.buffer_size { buffer_size as usize } else { 2048 }); + buf_size); + // Wait for both forwarding tasks to finish so TCP connections are + // properly shut down before this handler exits + let _ = tokio::join!(h1, h2); + info!("connection closed (domain: {}, upstream: {})", &domain, upstream_server.proxy_pass); } async fn handle_address(listener: &TcpListener, config: Arc) { diff --git a/mineginx/src/stream.rs b/mineginx/src/stream.rs index c25ac50..9cd9ee6 100644 --- a/mineginx/src/stream.rs +++ b/mineginx/src/stream.rs @@ -20,27 +20,25 @@ pub fn forward_stream( loop { tokio::select! { _ = &mut close_by_other => { - return; + break; }, res = reader.read(&mut buf) => { match res { - Ok(0) => { - _ = close.send(()); - return; - }, + Ok(0) | Err(_) => break, Ok(size) => { if writer.write_all(&buf[..size]).await.is_err() { - _ = close.send(()); - return; + break; } - }, - Err(_) => { - _ = close.send(()); - return; } } } } } + // Signal the other forwarding task to stop (dropping the sender + // causes the receiver to resolve, which triggers its close_by_other branch) + drop(close); + // Shut down write half so the remote end receives FIN immediately + // instead of waiting for its own keepalive/timeout to detect the dead connection + _ = writer.shutdown().await; }) } From 708ba88b59eb4bd56b17d772f1287ad3b650e3c2 Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 19:26:27 -0500 Subject: [PATCH 03/10] Try to fix multiple connections for single client --- mineginx/src/main.rs | 33 ++++++++++++----------------- mineginx/src/stream.rs | 47 +++++++++++++++++------------------------- 2 files changed, 32 insertions(+), 48 deletions(-) diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index 8c2bb02..74fcd6a 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -5,8 +5,8 @@ use config::{MinecraftServerDescription, MineginxConfig}; use log::{error, info, warn}; use minecraft::{packets::{HandshakeC2SPacket, MinecraftPacket}, serialization::{truncate_to_zero, MinecraftStream}}; use simple_logger::SimpleLogger; -use tokio::{io::AsyncWriteExt, net::{TcpListener, TcpStream}, sync::oneshot, task::JoinHandle, time::timeout}; -use stream::forward_stream; +use tokio::{io::AsyncWriteExt, net::{TcpListener, TcpStream}, task::JoinHandle, time::timeout}; +use stream::forward_half; mod stream; mod config; @@ -95,24 +95,17 @@ async fn handle_client(mut client: TcpStream, config: Arc) { let (client_reader, client_writer) = client.into_split(); let (upstream_reader, upstream_writer) = upstream.into_split(); - let (client_close_sender, client_close_receiver) = oneshot::channel::<()>(); - let (upstream_close_sender, upstream_close_receiver) = oneshot::channel::<()>(); - let buf_size = upstream_server.buffer_size.map(|b| b as usize).unwrap_or(2048); - let h1 = forward_stream( - client_close_sender, - upstream_close_receiver, - client_reader, - upstream_writer, - buf_size); - let h2 = forward_stream( - upstream_close_sender, - client_close_receiver, - upstream_reader, - client_writer, - buf_size); - // Wait for both forwarding tasks to finish so TCP connections are - // properly shut down before this handler exits - let _ = tokio::join!(h1, h2); + let buf_size = upstream_server.buffer_size.map(|b| b as usize).unwrap_or(8192); + + // Each direction gets its own spawned task so the tokio scheduler + // can freely interleave them with the accept loop and other connections. + // When one direction reads EOF it calls shutdown() on its writer, + // sending FIN to the remote — the opposite task then naturally reads + // EOF from its side and terminates. No explicit signaling needed. + let c2s = forward_half(client_reader, upstream_writer, buf_size); + let s2c = forward_half(upstream_reader, client_writer, buf_size); + + let _ = tokio::join!(c2s, s2c); info!("connection closed (domain: {}, upstream: {})", &domain, upstream_server.proxy_pass); } diff --git a/mineginx/src/stream.rs b/mineginx/src/stream.rs index 9cd9ee6..e2c4bab 100644 --- a/mineginx/src/stream.rs +++ b/mineginx/src/stream.rs @@ -1,44 +1,35 @@ use tokio::{ task::JoinHandle, - sync::oneshot::{ - Sender, Receiver - }, - net::tcp::{ - OwnedReadHalf, OwnedWriteHalf - }, - io::{AsyncReadExt, AsyncWriteExt} + net::tcp::{OwnedReadHalf, OwnedWriteHalf}, + io::{AsyncReadExt, AsyncWriteExt}, }; -pub fn forward_stream( - close: Sender<()>, - mut close_by_other: Receiver<()>, +/// Forwards data from `reader` to `writer` until EOF or error, +/// then shuts down the writer (sends TCP FIN to the remote end). +/// +/// Each direction of a proxied connection gets its own spawned task +/// so the tokio scheduler can interleave them with the accept loop +/// and other connections freely. +pub fn forward_half( mut reader: OwnedReadHalf, mut writer: OwnedWriteHalf, - buffer_size: usize) -> JoinHandle<()> { + buffer_size: usize, +) -> JoinHandle<()> { tokio::spawn(async move { let mut buf = vec![0; buffer_size]; loop { - tokio::select! { - _ = &mut close_by_other => { - break; - }, - res = reader.read(&mut buf) => { - match res { - Ok(0) | Err(_) => break, - Ok(size) => { - if writer.write_all(&buf[..size]).await.is_err() { - break; - } - } + match reader.read(&mut buf).await { + Ok(0) | Err(_) => break, + Ok(n) => { + if writer.write_all(&buf[..n]).await.is_err() { + break; } } } } - // Signal the other forwarding task to stop (dropping the sender - // causes the receiver to resolve, which triggers its close_by_other branch) - drop(close); - // Shut down write half so the remote end receives FIN immediately - // instead of waiting for its own keepalive/timeout to detect the dead connection + // Shut down the write half so the remote end receives FIN. + // The other forwarding task (opposite direction) will then + // naturally read EOF and terminate on its own — no signaling needed. _ = writer.shutdown().await; }) } From 17a15da1a1ab67eba63dc92d96612d99eb1f4cc2 Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 20:47:48 -0500 Subject: [PATCH 04/10] Ugh --- mineginx/src/main.rs | 86 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index 74fcd6a..d1afd14 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -1,5 +1,5 @@ use std::{ - borrow::BorrowMut, collections::HashMap, env, fs::{self}, io::ErrorKind, path::Path, process::ExitCode, sync::Arc, time::Duration + borrow::BorrowMut, collections::HashMap, env, fs::{self}, io::ErrorKind, net::{SocketAddr, ToSocketAddrs}, path::Path, process::ExitCode, sync::Arc, time::Duration }; use config::{MinecraftServerDescription, MineginxConfig}; use log::{error, info, warn}; @@ -11,11 +11,26 @@ use stream::forward_half; mod stream; mod config; -fn find_upstream(domain: &String, config: Arc) -> Option { +/// Holds upstream info with a pre-resolved socket address so that +/// per-connection connects skip DNS entirely. On musl + scratch containers, +/// getaddrinfo() is synchronous and blocks the tokio blocking threadpool, +/// which can stall the accept loop and block all new connections/pings. +#[derive(Clone)] +struct ResolvedUpstream { + proxy_pass: String, + addr: SocketAddr, + buffer_size: Option, +} + +fn find_upstream(domain: &str, config: &MineginxConfig, resolved: &HashMap) -> Option { + // Strip trailing dot from client domain — Minecraft clients send FQDN + // (e.g. "mc.kaydax.xyz.") but configs typically use bare names + let domain = domain.trim_end_matches('.'); for x in &config.servers { for server_name in &x.server_names { - if server_name == domain { - return Some(x.clone()); + let cfg_name = server_name.trim_end_matches('.'); + if cfg_name.eq_ignore_ascii_case(domain) { + return resolved.get(&x.proxy_pass).cloned(); } } } @@ -31,7 +46,7 @@ async fn read_handshake_packet(client: &mut MinecraftStream<&mut TcpStream>) -> Ok(handshake) } -async fn handle_client(mut client: TcpStream, config: Arc) { +async fn handle_client(mut client: TcpStream, config: Arc, resolved: Arc>) { if let Err(e) = client.set_nodelay(true) { error!("failed to set no_delay for client: {}", e); return; @@ -56,7 +71,7 @@ async fn handle_client(mut client: TcpStream, config: Arc) { }; let domain = truncate_to_zero(&handshake.domain).to_string(); - let upstream_server = match find_upstream(&domain, config.clone()) { + let upstream = match find_upstream(&domain, &config, &resolved) { Some(x) => x, None => { warn!("there is no upstream for domain {:#?}", &domain); @@ -64,16 +79,17 @@ async fn handle_client(mut client: TcpStream, config: Arc) { } }; - info!("new connection (protocol_version: {}, domain: {}, upstream: {})", &handshake.protocol_version, &domain, upstream_server.proxy_pass); + info!("new connection (protocol_version: {}, domain: {}, upstream: {})", &handshake.protocol_version, &domain, upstream.proxy_pass); - let mut upstream = match TcpStream::connect(&upstream_server.proxy_pass).await { + // Connect using the pre-resolved SocketAddr — no DNS lookup at connection time + let mut upstream_conn = match TcpStream::connect(upstream.addr).await { Ok(x) => x, Err(e) => { - error!("failed to connect upstream: {}, {e}", &upstream_server.proxy_pass); + error!("failed to connect upstream: {}, {e}", &upstream.proxy_pass); return; } }; - if let Err(e) = upstream.set_nodelay(true) { + if let Err(e) = upstream_conn.set_nodelay(true) { error!("failed to set no_delay for upstream: {}", e); return; } @@ -81,12 +97,12 @@ async fn handle_client(mut client: TcpStream, config: Arc) { Some(v) => v, None => return }; - match upstream.write_all(&packet[0..packet.len()]).await { + match upstream_conn.write_all(&packet[0..packet.len()]).await { Ok(_) => { }, Err(_) => return }; // flush unread buffer to the upstream - match upstream.write_all(&minecraft.take_buffer()).await { + match upstream_conn.write_all(&minecraft.take_buffer()).await { Ok(_) => {}, Err(_) => { return; @@ -94,8 +110,8 @@ async fn handle_client(mut client: TcpStream, config: Arc) { } let (client_reader, client_writer) = client.into_split(); - let (upstream_reader, upstream_writer) = upstream.into_split(); - let buf_size = upstream_server.buffer_size.map(|b| b as usize).unwrap_or(8192); + let (upstream_reader, upstream_writer) = upstream_conn.into_split(); + let buf_size = upstream.buffer_size.map(|b| b as usize).unwrap_or(8192); // Each direction gets its own spawned task so the tokio scheduler // can freely interleave them with the accept loop and other connections. @@ -106,10 +122,10 @@ async fn handle_client(mut client: TcpStream, config: Arc) { let s2c = forward_half(upstream_reader, client_writer, buf_size); let _ = tokio::join!(c2s, s2c); - info!("connection closed (domain: {}, upstream: {})", &domain, upstream_server.proxy_pass); + info!("connection closed (domain: {}, upstream: {})", &domain, upstream.proxy_pass); } -async fn handle_address(listener: &TcpListener, config: Arc) { +async fn handle_address(listener: &TcpListener, config: Arc, resolved: Arc>) { loop { let (socket, _address) = match listener.accept().await { Ok(x) => x, @@ -119,8 +135,9 @@ async fn handle_address(listener: &TcpListener, config: Arc) { } }; let conf = config.clone(); + let res = resolved.clone(); tokio::spawn(async move { - handle_client(socket, conf).await; + handle_client(socket, conf, res).await; }); } } @@ -211,6 +228,38 @@ async fn main() -> ExitCode { return ExitCode::from(2); } }; + + // Pre-resolve all upstream DNS at startup so per-connection connects + // use raw SocketAddr and never touch DNS. On musl (used in scratch + // containers), getaddrinfo() is synchronous and blocks tokio's + // blocking threadpool, which can stall the accept loop. + let mut resolved = HashMap::::new(); + for server in &config.servers { + if resolved.contains_key(&server.proxy_pass) { + continue; + } + let addr = match server.proxy_pass.to_socket_addrs() { + Ok(mut addrs) => match addrs.next() { + Some(a) => a, + None => { + error!("failed to resolve upstream '{}': no addresses returned", &server.proxy_pass); + return ExitCode::from(4); + } + }, + Err(e) => { + error!("failed to resolve upstream '{}': {e}", &server.proxy_pass); + return ExitCode::from(4); + } + }; + info!("resolved upstream {} -> {}", &server.proxy_pass, addr); + resolved.insert(server.proxy_pass.clone(), ResolvedUpstream { + proxy_pass: server.proxy_pass.clone(), + addr, + buffer_size: server.buffer_size, + }); + } + let resolved = Arc::new(resolved); + let mut listening = HashMap::::new(); for server in &config.servers { if listening.contains_key(&server.listen) { @@ -225,8 +274,9 @@ async fn main() -> ExitCode { } }; let conf = config.clone(); + let res = resolved.clone(); let task = tokio::spawn(async move { - handle_address(&listener, conf).await; + handle_address(&listener, conf, res).await; }); listening.insert(server.listen.to_string(), ListeningAddress(task)); } From 4e4b5d9fa148e063ef8f95652a81767a9754896f Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 20:53:25 -0500 Subject: [PATCH 05/10] Fix DNS resolver cache --- mineginx/src/main.rs | 86 ++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index d1afd14..88f7222 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -5,7 +5,7 @@ use config::{MinecraftServerDescription, MineginxConfig}; use log::{error, info, warn}; use minecraft::{packets::{HandshakeC2SPacket, MinecraftPacket}, serialization::{truncate_to_zero, MinecraftStream}}; use simple_logger::SimpleLogger; -use tokio::{io::AsyncWriteExt, net::{TcpListener, TcpStream}, task::JoinHandle, time::timeout}; +use tokio::{io::AsyncWriteExt, net::{TcpListener, TcpStream}, sync::RwLock, task::JoinHandle, time::timeout}; use stream::forward_half; mod stream; @@ -22,15 +22,21 @@ struct ResolvedUpstream { buffer_size: Option, } -fn find_upstream(domain: &str, config: &MineginxConfig, resolved: &HashMap) -> Option { - // Strip trailing dot from client domain — Minecraft clients send FQDN - // (e.g. "mc.kaydax.xyz.") but configs typically use bare names +/// Try to resolve an upstream hostname to a SocketAddr. +fn try_resolve(proxy_pass: &str) -> Option { + match proxy_pass.to_socket_addrs() { + Ok(mut addrs) => addrs.next(), + Err(_) => None, + } +} + +fn find_upstream_config<'a>(domain: &str, config: &'a MineginxConfig) -> Option<&'a MinecraftServerDescription> { let domain = domain.trim_end_matches('.'); for x in &config.servers { for server_name in &x.server_names { let cfg_name = server_name.trim_end_matches('.'); if cfg_name.eq_ignore_ascii_case(domain) { - return resolved.get(&x.proxy_pass).cloned(); + return Some(x); } } } @@ -46,7 +52,7 @@ async fn read_handshake_packet(client: &mut MinecraftStream<&mut TcpStream>) -> Ok(handshake) } -async fn handle_client(mut client: TcpStream, config: Arc, resolved: Arc>) { +async fn handle_client(mut client: TcpStream, config: Arc, resolved: Arc>>) { if let Err(e) = client.set_nodelay(true) { error!("failed to set no_delay for client: {}", e); return; @@ -71,14 +77,44 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol }; let domain = truncate_to_zero(&handshake.domain).to_string(); - let upstream = match find_upstream(&domain, &config, &resolved) { - Some(x) => x, + let server_desc = match find_upstream_config(&domain, &config) { + Some(x) => x.clone(), None => { warn!("there is no upstream for domain {:#?}", &domain); return; } }; + // Look up the pre-resolved address, or try resolving on-the-fly + // for upstreams that weren't available at startup + let upstream = { + let cache = resolved.read().await; + cache.get(&server_desc.proxy_pass).cloned() + }; + let upstream = match upstream { + Some(u) => u, + None => { + // Upstream wasn't resolved at startup — try now (container may have come online) + match try_resolve(&server_desc.proxy_pass) { + Some(addr) => { + let entry = ResolvedUpstream { + proxy_pass: server_desc.proxy_pass.clone(), + addr, + buffer_size: server_desc.buffer_size, + }; + info!("lazily resolved upstream {} -> {}", &server_desc.proxy_pass, addr); + let mut cache = resolved.write().await; + cache.insert(server_desc.proxy_pass.clone(), entry.clone()); + entry + }, + None => { + error!("failed to resolve upstream '{}' for domain {:#?}", &server_desc.proxy_pass, &domain); + return; + } + } + } + }; + info!("new connection (protocol_version: {}, domain: {}, upstream: {})", &handshake.protocol_version, &domain, upstream.proxy_pass); // Connect using the pre-resolved SocketAddr — no DNS lookup at connection time @@ -125,7 +161,7 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol info!("connection closed (domain: {}, upstream: {})", &domain, upstream.proxy_pass); } -async fn handle_address(listener: &TcpListener, config: Arc, resolved: Arc>) { +async fn handle_address(listener: &TcpListener, config: Arc, resolved: Arc>>) { loop { let (socket, _address) = match listener.accept().await { Ok(x) => x, @@ -233,32 +269,28 @@ async fn main() -> ExitCode { // use raw SocketAddr and never touch DNS. On musl (used in scratch // containers), getaddrinfo() is synchronous and blocks tokio's // blocking threadpool, which can stall the accept loop. + // Upstreams that fail to resolve are skipped — they'll be resolved + // lazily when a client first connects to them. let mut resolved = HashMap::::new(); for server in &config.servers { if resolved.contains_key(&server.proxy_pass) { continue; } - let addr = match server.proxy_pass.to_socket_addrs() { - Ok(mut addrs) => match addrs.next() { - Some(a) => a, - None => { - error!("failed to resolve upstream '{}': no addresses returned", &server.proxy_pass); - return ExitCode::from(4); - } + match try_resolve(&server.proxy_pass) { + Some(addr) => { + info!("resolved upstream {} -> {}", &server.proxy_pass, addr); + resolved.insert(server.proxy_pass.clone(), ResolvedUpstream { + proxy_pass: server.proxy_pass.clone(), + addr, + buffer_size: server.buffer_size, + }); }, - Err(e) => { - error!("failed to resolve upstream '{}': {e}", &server.proxy_pass); - return ExitCode::from(4); + None => { + warn!("upstream '{}' not available yet, will resolve on first connection", &server.proxy_pass); } - }; - info!("resolved upstream {} -> {}", &server.proxy_pass, addr); - resolved.insert(server.proxy_pass.clone(), ResolvedUpstream { - proxy_pass: server.proxy_pass.clone(), - addr, - buffer_size: server.buffer_size, - }); + } } - let resolved = Arc::new(resolved); + let resolved = Arc::new(RwLock::new(resolved)); let mut listening = HashMap::::new(); for server in &config.servers { From 519dd2b1b6c22b118659bec6a6b2d4fd8ed18420 Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 21:22:52 -0500 Subject: [PATCH 06/10] Temp add debug logging --- mineginx/src/main.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index 88f7222..9e64f45 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -1,8 +1,8 @@ use std::{ - borrow::BorrowMut, collections::HashMap, env, fs::{self}, io::ErrorKind, net::{SocketAddr, ToSocketAddrs}, path::Path, process::ExitCode, sync::Arc, time::Duration + borrow::BorrowMut, collections::HashMap, env, fs::{self}, io::ErrorKind, net::SocketAddr, path::Path, process::ExitCode, sync::Arc, time::Duration }; use config::{MinecraftServerDescription, MineginxConfig}; -use log::{error, info, warn}; +use log::{debug, error, info, warn}; use minecraft::{packets::{HandshakeC2SPacket, MinecraftPacket}, serialization::{truncate_to_zero, MinecraftStream}}; use simple_logger::SimpleLogger; use tokio::{io::AsyncWriteExt, net::{TcpListener, TcpStream}, sync::RwLock, task::JoinHandle, time::timeout}; @@ -22,11 +22,12 @@ struct ResolvedUpstream { buffer_size: Option, } -/// Try to resolve an upstream hostname to a SocketAddr. -fn try_resolve(proxy_pass: &str) -> Option { - match proxy_pass.to_socket_addrs() { - Ok(mut addrs) => addrs.next(), - Err(_) => None, +/// Try to resolve an upstream hostname to a SocketAddr asynchronously. +/// Uses a 2-second timeout to avoid blocking startup or connections. +async fn try_resolve(proxy_pass: &str) -> Option { + match timeout(Duration::from_secs(2), tokio::net::lookup_host(proxy_pass)).await { + Ok(Ok(mut addrs)) => addrs.next(), + _ => None, } } @@ -53,11 +54,14 @@ async fn read_handshake_packet(client: &mut MinecraftStream<&mut TcpStream>) -> } async fn handle_client(mut client: TcpStream, config: Arc, resolved: Arc>>) { + let peer = client.peer_addr().ok(); + debug!("accepted connection from {:?}", peer); if let Err(e) = client.set_nodelay(true) { error!("failed to set no_delay for client: {}", e); return; } let mut minecraft = MinecraftStream::new(client.borrow_mut(), 4096); + debug!("reading handshake from {:?}", peer); let timeout_future = Duration::from_millis(config.handshake_timeout_ms.unwrap_or(10_000)); let handshake_result = timeout(timeout_future, read_handshake_packet(&mut minecraft)).await; let handshake = match handshake_result { @@ -76,6 +80,7 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol } }; + debug!("handshake complete from {:?}, domain: {}", peer, &handshake.domain); let domain = truncate_to_zero(&handshake.domain).to_string(); let server_desc = match find_upstream_config(&domain, &config) { Some(x) => x.clone(), @@ -95,7 +100,7 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol Some(u) => u, None => { // Upstream wasn't resolved at startup — try now (container may have come online) - match try_resolve(&server_desc.proxy_pass) { + match try_resolve(&server_desc.proxy_pass).await { Some(addr) => { let entry = ResolvedUpstream { proxy_pass: server_desc.proxy_pass.clone(), @@ -276,7 +281,7 @@ async fn main() -> ExitCode { if resolved.contains_key(&server.proxy_pass) { continue; } - match try_resolve(&server.proxy_pass) { + match try_resolve(&server.proxy_pass).await { Some(addr) => { info!("resolved upstream {} -> {}", &server.proxy_pass, addr); resolved.insert(server.proxy_pass.clone(), ResolvedUpstream { From 18ab27621c509eea912979bc127f626c8bef2580 Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 21:29:21 -0500 Subject: [PATCH 07/10] More logging --- mineginx/src/main.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index 9e64f45..47016be 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -168,8 +168,12 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol async fn handle_address(listener: &TcpListener, config: Arc, resolved: Arc>>) { loop { + debug!("accept loop waiting for connection"); let (socket, _address) = match listener.accept().await { - Ok(x) => x, + Ok(x) => { + debug!("accept() returned a connection"); + x + }, Err(e) => { error!("failed to accept client: {e}"); continue; @@ -180,6 +184,7 @@ async fn handle_address(listener: &TcpListener, config: Arc, res tokio::spawn(async move { handle_client(socket, conf, res).await; }); + debug!("spawned handler, looping back to accept"); } } From b0f1a2caf46b174b4108945fdd431c242293b9a0 Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 21:37:21 -0500 Subject: [PATCH 08/10] Ugh 2 --- mineginx/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index 47016be..e5cf28a 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -92,10 +92,14 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol // Look up the pre-resolved address, or try resolving on-the-fly // for upstreams that weren't available at startup + debug!("looking up upstream for {} from {:?}", &server_desc.proxy_pass, peer); let upstream = { + debug!("acquiring read lock from {:?}", peer); let cache = resolved.read().await; + debug!("got read lock from {:?}", peer); cache.get(&server_desc.proxy_pass).cloned() }; + debug!("released read lock from {:?}, found: {}", peer, upstream.is_some()); let upstream = match upstream { Some(u) => u, None => { From 6cc450ba1dc0151686c315a41fe80dd8f0936e15 Mon Sep 17 00:00:00 2001 From: Kaydax Date: Wed, 25 Feb 2026 23:23:27 -0500 Subject: [PATCH 09/10] The rest --- Cargo.lock | 104 ++++++++++++++++++++++++++++++++++++++----- mineginx/Cargo.toml | 1 + mineginx/src/main.rs | 60 ++++++++++++++++--------- 3 files changed, 133 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd9da7e..8d7112d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,7 @@ dependencies = [ "serde", "serde_yaml", "simple_logger", + "socket2 0.5.10", "tokio", "uuid", ] @@ -345,6 +346,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.2" @@ -411,7 +422,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -516,13 +527,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -534,6 +554,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -541,58 +577,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/mineginx/Cargo.toml b/mineginx/Cargo.toml index 731b302..d941759 100644 --- a/mineginx/Cargo.toml +++ b/mineginx/Cargo.toml @@ -16,3 +16,4 @@ tokio = { version = "1.49.0", features = ["full"] } uuid = { version = "1.20.0", features = ["v4"] } log = { version = "0.4" } simple_logger = { version = "5.1.0" } +socket2 = "0.5" diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index e5cf28a..067a785 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -2,9 +2,10 @@ use std::{ borrow::BorrowMut, collections::HashMap, env, fs::{self}, io::ErrorKind, net::SocketAddr, path::Path, process::ExitCode, sync::Arc, time::Duration }; use config::{MinecraftServerDescription, MineginxConfig}; -use log::{debug, error, info, warn}; +use log::{error, info, warn}; use minecraft::{packets::{HandshakeC2SPacket, MinecraftPacket}, serialization::{truncate_to_zero, MinecraftStream}}; use simple_logger::SimpleLogger; +use socket2::{Socket, Domain, Type}; use tokio::{io::AsyncWriteExt, net::{TcpListener, TcpStream}, sync::RwLock, task::JoinHandle, time::timeout}; use stream::forward_half; @@ -54,21 +55,16 @@ async fn read_handshake_packet(client: &mut MinecraftStream<&mut TcpStream>) -> } async fn handle_client(mut client: TcpStream, config: Arc, resolved: Arc>>) { - let peer = client.peer_addr().ok(); - debug!("accepted connection from {:?}", peer); if let Err(e) = client.set_nodelay(true) { error!("failed to set no_delay for client: {}", e); return; } let mut minecraft = MinecraftStream::new(client.borrow_mut(), 4096); - debug!("reading handshake from {:?}", peer); let timeout_future = Duration::from_millis(config.handshake_timeout_ms.unwrap_or(10_000)); let handshake_result = timeout(timeout_future, read_handshake_packet(&mut minecraft)).await; let handshake = match handshake_result { Ok(result) => match result { - Ok(handshake) => { - handshake - } + Ok(handshake) => handshake, Err(_) => { error!("handshake failed for someone"); return; @@ -79,8 +75,6 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol return; } }; - - debug!("handshake complete from {:?}, domain: {}", peer, &handshake.domain); let domain = truncate_to_zero(&handshake.domain).to_string(); let server_desc = match find_upstream_config(&domain, &config) { Some(x) => x.clone(), @@ -92,14 +86,10 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol // Look up the pre-resolved address, or try resolving on-the-fly // for upstreams that weren't available at startup - debug!("looking up upstream for {} from {:?}", &server_desc.proxy_pass, peer); let upstream = { - debug!("acquiring read lock from {:?}", peer); let cache = resolved.read().await; - debug!("got read lock from {:?}", peer); cache.get(&server_desc.proxy_pass).cloned() }; - debug!("released read lock from {:?}, found: {}", peer, upstream.is_some()); let upstream = match upstream { Some(u) => u, None => { @@ -172,12 +162,8 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol async fn handle_address(listener: &TcpListener, config: Arc, resolved: Arc>>) { loop { - debug!("accept loop waiting for connection"); - let (socket, _address) = match listener.accept().await { - Ok(x) => { - debug!("accept() returned a connection"); - x - }, + let (socket, _addr) = match listener.accept().await { + Ok(x) => x, Err(e) => { error!("failed to accept client: {e}"); continue; @@ -188,7 +174,6 @@ async fn handle_address(listener: &TcpListener, config: Arc, res tokio::spawn(async move { handle_client(socket, conf, res).await; }); - debug!("spawned handler, looping back to accept"); } } @@ -312,10 +297,41 @@ async fn main() -> ExitCode { continue; } info!("listening {}", &server.listen); - let listener = match TcpListener::bind(&server.listen).await { + // Use socket2 to create a socket with a large backlog (1024) to prevent + // connection resets when multiple clients try to connect simultaneously + let addr: SocketAddr = match server.listen.parse() { + Ok(a) => a, + Err(e) => { + error!("failed to parse listen address {}: {e}", &server.listen); + return ExitCode::from(3); + } + }; + let socket = match Socket::new(Domain::for_address(addr), Type::STREAM, None) { + Ok(s) => s, + Err(e) => { + error!("failed to create socket for {}: {e}", &server.listen); + return ExitCode::from(3); + } + }; + // Allow address reuse to avoid "address already in use" on restart + if let Err(e) = socket.set_reuse_address(true) { + error!("failed to set SO_REUSEADDR for {}: {e}", &server.listen); + return ExitCode::from(3); + } + if let Err(e) = socket.bind(&addr.into()) { + error!("failed to bind {}: {e}", &server.listen); + return ExitCode::from(3); + } + // Set backlog to 1024 to handle bursts of connections + if let Err(e) = socket.listen(1024) { + error!("failed to listen on {}: {e}", &server.listen); + return ExitCode::from(3); + } + socket.set_nonblocking(true).unwrap(); + let listener = match TcpListener::from_std(socket.into()) { Ok(l) => l, Err(e) => { - error!("failed to bind {}: {e}", &server.listen); + error!("failed to create tokio listener for {}: {e}", &server.listen); return ExitCode::from(3); } }; From 1530c5f21273f11adfbed2e5d281924eea37a9d3 Mon Sep 17 00:00:00 2001 From: Kaydax Date: Thu, 26 Feb 2026 16:18:54 -0500 Subject: [PATCH 10/10] Remove DNS cache, do suggested changes --- mineginx/src/main.rs | 113 +++++------------------------------------ mineginx/src/stream.rs | 9 +--- 2 files changed, 14 insertions(+), 108 deletions(-) diff --git a/mineginx/src/main.rs b/mineginx/src/main.rs index 067a785..9aef6d2 100644 --- a/mineginx/src/main.rs +++ b/mineginx/src/main.rs @@ -6,32 +6,12 @@ use log::{error, info, warn}; use minecraft::{packets::{HandshakeC2SPacket, MinecraftPacket}, serialization::{truncate_to_zero, MinecraftStream}}; use simple_logger::SimpleLogger; use socket2::{Socket, Domain, Type}; -use tokio::{io::AsyncWriteExt, net::{TcpListener, TcpStream}, sync::RwLock, task::JoinHandle, time::timeout}; +use tokio::{io::AsyncWriteExt, net::{TcpListener, TcpStream}, task::JoinHandle, time::timeout}; use stream::forward_half; mod stream; mod config; -/// Holds upstream info with a pre-resolved socket address so that -/// per-connection connects skip DNS entirely. On musl + scratch containers, -/// getaddrinfo() is synchronous and blocks the tokio blocking threadpool, -/// which can stall the accept loop and block all new connections/pings. -#[derive(Clone)] -struct ResolvedUpstream { - proxy_pass: String, - addr: SocketAddr, - buffer_size: Option, -} - -/// Try to resolve an upstream hostname to a SocketAddr asynchronously. -/// Uses a 2-second timeout to avoid blocking startup or connections. -async fn try_resolve(proxy_pass: &str) -> Option { - match timeout(Duration::from_secs(2), tokio::net::lookup_host(proxy_pass)).await { - Ok(Ok(mut addrs)) => addrs.next(), - _ => None, - } -} - fn find_upstream_config<'a>(domain: &str, config: &'a MineginxConfig) -> Option<&'a MinecraftServerDescription> { let domain = domain.trim_end_matches('.'); for x in &config.servers { @@ -54,7 +34,7 @@ async fn read_handshake_packet(client: &mut MinecraftStream<&mut TcpStream>) -> Ok(handshake) } -async fn handle_client(mut client: TcpStream, config: Arc, resolved: Arc>>) { +async fn handle_client(mut client: TcpStream, config: Arc) { if let Err(e) = client.set_nodelay(true) { error!("failed to set no_delay for client: {}", e); return; @@ -84,43 +64,12 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol } }; - // Look up the pre-resolved address, or try resolving on-the-fly - // for upstreams that weren't available at startup - let upstream = { - let cache = resolved.read().await; - cache.get(&server_desc.proxy_pass).cloned() - }; - let upstream = match upstream { - Some(u) => u, - None => { - // Upstream wasn't resolved at startup — try now (container may have come online) - match try_resolve(&server_desc.proxy_pass).await { - Some(addr) => { - let entry = ResolvedUpstream { - proxy_pass: server_desc.proxy_pass.clone(), - addr, - buffer_size: server_desc.buffer_size, - }; - info!("lazily resolved upstream {} -> {}", &server_desc.proxy_pass, addr); - let mut cache = resolved.write().await; - cache.insert(server_desc.proxy_pass.clone(), entry.clone()); - entry - }, - None => { - error!("failed to resolve upstream '{}' for domain {:#?}", &server_desc.proxy_pass, &domain); - return; - } - } - } - }; + info!("new connection (protocol_version: {}, domain: {}, upstream: {})", &handshake.protocol_version, &domain, &server_desc.proxy_pass); - info!("new connection (protocol_version: {}, domain: {}, upstream: {})", &handshake.protocol_version, &domain, upstream.proxy_pass); - - // Connect using the pre-resolved SocketAddr — no DNS lookup at connection time - let mut upstream_conn = match TcpStream::connect(upstream.addr).await { + let mut upstream_conn = match TcpStream::connect(&server_desc.proxy_pass).await { Ok(x) => x, Err(e) => { - error!("failed to connect upstream: {}, {e}", &upstream.proxy_pass); + error!("failed to connect upstream: {}, {e}", &server_desc.proxy_pass); return; } }; @@ -136,7 +85,6 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol Ok(_) => { }, Err(_) => return }; - // flush unread buffer to the upstream match upstream_conn.write_all(&minecraft.take_buffer()).await { Ok(_) => {}, Err(_) => { @@ -146,21 +94,18 @@ async fn handle_client(mut client: TcpStream, config: Arc, resol let (client_reader, client_writer) = client.into_split(); let (upstream_reader, upstream_writer) = upstream_conn.into_split(); - let buf_size = upstream.buffer_size.map(|b| b as usize).unwrap_or(8192); + let buf_size = server_desc.buffer_size.map(|b| b as usize).unwrap_or(8192); - // Each direction gets its own spawned task so the tokio scheduler - // can freely interleave them with the accept loop and other connections. - // When one direction reads EOF it calls shutdown() on its writer, - // sending FIN to the remote — the opposite task then naturally reads - // EOF from its side and terminates. No explicit signaling needed. + // Each direction is a separate task. When one side hits EOF, + // it shuts down its writer (sends FIN), causing the other to EOF too. let c2s = forward_half(client_reader, upstream_writer, buf_size); let s2c = forward_half(upstream_reader, client_writer, buf_size); let _ = tokio::join!(c2s, s2c); - info!("connection closed (domain: {}, upstream: {})", &domain, upstream.proxy_pass); + info!("connection closed (domain: {}, upstream: {})", &domain, &server_desc.proxy_pass); } -async fn handle_address(listener: &TcpListener, config: Arc, resolved: Arc>>) { +async fn handle_address(listener: &TcpListener, config: Arc) { loop { let (socket, _addr) = match listener.accept().await { Ok(x) => x, @@ -170,9 +115,8 @@ async fn handle_address(listener: &TcpListener, config: Arc, res } }; let conf = config.clone(); - let res = resolved.clone(); tokio::spawn(async move { - handle_client(socket, conf, res).await; + handle_client(socket, conf).await; }); } } @@ -264,41 +208,12 @@ async fn main() -> ExitCode { } }; - // Pre-resolve all upstream DNS at startup so per-connection connects - // use raw SocketAddr and never touch DNS. On musl (used in scratch - // containers), getaddrinfo() is synchronous and blocks tokio's - // blocking threadpool, which can stall the accept loop. - // Upstreams that fail to resolve are skipped — they'll be resolved - // lazily when a client first connects to them. - let mut resolved = HashMap::::new(); - for server in &config.servers { - if resolved.contains_key(&server.proxy_pass) { - continue; - } - match try_resolve(&server.proxy_pass).await { - Some(addr) => { - info!("resolved upstream {} -> {}", &server.proxy_pass, addr); - resolved.insert(server.proxy_pass.clone(), ResolvedUpstream { - proxy_pass: server.proxy_pass.clone(), - addr, - buffer_size: server.buffer_size, - }); - }, - None => { - warn!("upstream '{}' not available yet, will resolve on first connection", &server.proxy_pass); - } - } - } - let resolved = Arc::new(RwLock::new(resolved)); - let mut listening = HashMap::::new(); for server in &config.servers { if listening.contains_key(&server.listen) { continue; } info!("listening {}", &server.listen); - // Use socket2 to create a socket with a large backlog (1024) to prevent - // connection resets when multiple clients try to connect simultaneously let addr: SocketAddr = match server.listen.parse() { Ok(a) => a, Err(e) => { @@ -313,7 +228,6 @@ async fn main() -> ExitCode { return ExitCode::from(3); } }; - // Allow address reuse to avoid "address already in use" on restart if let Err(e) = socket.set_reuse_address(true) { error!("failed to set SO_REUSEADDR for {}: {e}", &server.listen); return ExitCode::from(3); @@ -322,7 +236,7 @@ async fn main() -> ExitCode { error!("failed to bind {}: {e}", &server.listen); return ExitCode::from(3); } - // Set backlog to 1024 to handle bursts of connections + if let Err(e) = socket.listen(1024) { error!("failed to listen on {}: {e}", &server.listen); return ExitCode::from(3); @@ -336,9 +250,8 @@ async fn main() -> ExitCode { } }; let conf = config.clone(); - let res = resolved.clone(); let task = tokio::spawn(async move { - handle_address(&listener, conf, res).await; + handle_address(&listener, conf).await; }); listening.insert(server.listen.to_string(), ListeningAddress(task)); } diff --git a/mineginx/src/stream.rs b/mineginx/src/stream.rs index e2c4bab..2b2eb69 100644 --- a/mineginx/src/stream.rs +++ b/mineginx/src/stream.rs @@ -5,11 +5,7 @@ use tokio::{ }; /// Forwards data from `reader` to `writer` until EOF or error, -/// then shuts down the writer (sends TCP FIN to the remote end). -/// -/// Each direction of a proxied connection gets its own spawned task -/// so the tokio scheduler can interleave them with the accept loop -/// and other connections freely. +/// then shuts down the writer (sends TCP FIN). pub fn forward_half( mut reader: OwnedReadHalf, mut writer: OwnedWriteHalf, @@ -27,9 +23,6 @@ pub fn forward_half( } } } - // Shut down the write half so the remote end receives FIN. - // The other forwarding task (opposite direction) will then - // naturally read EOF and terminate on its own — no signaling needed. _ = writer.shutdown().await; }) }