Compare commits

..

7 commits

Author SHA1 Message Date
80b264331e wip 2026-05-29 17:40:33 +02:00
5dc61a0f12 feat: make OpaqueError FanctTrace more legible 2026-05-29 13:10:05 +02:00
0e2afd0772 feat: polish the OpaqueError get_span_trace function looks 2026-05-29 11:07:59 +02:00
1bce912a1a feat: polish logging if the server is unavailable 2026-05-29 10:42:24 +02:00
1229127101 feat: add better logging span to process_connection 2026-05-29 10:40:51 +02:00
ee7a05152e feat: cleaner logging
- less clutter with unusable `span` info (on my personal preference)
- less errors logged when being scanned (ex.: after status response
    don't log errors with ping packets, scanners usually close
    the connection)
- implemented more detailed OpaqueError fields
    - `tracing::Level`
    - error `kind`
2026-05-28 18:00:29 +02:00
efe080a9e7 feat: add localized tmux server 2026-05-28 17:59:28 +02:00
7 changed files with 182 additions and 80 deletions

View file

@ -1,3 +1,8 @@
# Development
TODO: fill this out
### Nix shell with tmux
In the nix flake there is a devshell setup with cargo and tmux.
When using tmux it uses a set tmux server path, so only this project uses it.
# Env variables: # Env variables:
```bash ```bash
# Address the server should bind to # Address the server should bind to

View file

@ -142,6 +142,11 @@
# Extra inputs can be added here; cargo and rustc are provided by default. # Extra inputs can be added here; cargo and rustc are provided by default.
packages = [ packages = [
pkgs.rust-analyzer pkgs.rust-analyzer
(pkgs.writers.writeBashBin "tmux" ''
exec ${lib.getExe pkgs.tmux} -S /tmp/tmux-kube_ingress "$@"
''
)
]; ];
}; };
}); });

View file

@ -12,11 +12,7 @@ use tracing::Instrument;
use crate::{ use crate::{
mc_server::{sanitize_addr, MinecraftAPI, MinecraftServerHandle, ServerDeploymentStatus}, mc_server::{sanitize_addr, MinecraftAPI, MinecraftServerHandle, ServerDeploymentStatus},
packets::{ packets::{clientbound::status::StatusTrait, SendPacket},
clientbound::status::StatusTrait,
serverbound::handshake::{self},
SendPacket,
},
OpaqueError, OpaqueError,
}; };
@ -99,17 +95,19 @@ impl MinecraftAPI<Server> for McApi {
let dep_name = match self.cache.query_dep_addr(&addr, &port).await { let dep_name = match self.cache.query_dep_addr(&addr, &port).await {
Some(x) => x, Some(x) => x,
None => { None => {
return Err(OpaqueError::create(&format!( return Err(OpaqueError::create_with_kind(
"Failed to find deployment name by addr" "Failed to find deployment name by addr",
))) "DepNameLookupFailed",
))
} }
}; };
let srv_name = match self.cache.query_srv_addr(&addr, &port).await { let srv_name = match self.cache.query_srv_addr(&addr, &port).await {
Some(x) => x, Some(x) => x,
None => { None => {
return Err(OpaqueError::create(&format!( return Err(OpaqueError::create_with_kind(
"Failed to find service name by addr" "Failed to find service name by addr",
))) "SrvNameLookupFailed",
))
} }
}; };
@ -191,7 +189,7 @@ impl MinecraftAPI<Server> for McApi {
drop(guard); drop(guard);
if let Err(err) = server.stop().await { if let Err(err) = server.stop().await {
tracing::error!( tracing::error!(
trace = err.get_span_trace(), trace = %err.print_span_trace(),
err = err.context, err = err.context,
msg = "failed to stop server" msg = "failed to stop server"
); );
@ -436,6 +434,7 @@ impl fmt::Debug for ServerDeploymentStatus {
Self::Starting => write!(f, "Starting"), Self::Starting => write!(f, "Starting"),
Self::PodOk => write!(f, "PodOk"), Self::PodOk => write!(f, "PodOk"),
Self::Offline => write!(f, "Offline"), Self::Offline => write!(f, "Offline"),
Self::Unavailable(s) => write!(f, "Unavailable ({s})"),
} }
} }
} }

View file

@ -58,12 +58,35 @@ async fn main() {
"Client connected" "Client connected"
); );
if let Err(e) = process_connection(socket, addr, api, config).await { if let Err(e) = process_connection(socket, addr, api, config).await {
tracing::error!( match e.level {
// addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()), tracing::Level::ERROR => tracing::error!(
trace = format!("{}", e.get_span_trace()), // addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()),
err = format!("{}", e.context), trace = %e.print_span_trace(),
"Client disconnected" err = format!("{}", e.context),
); "Client disconnected"
),
tracing::Level::WARN => tracing::warn!(
// addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()),
trace = %e.print_span_trace(),
err = format!("{}", e.context),
"Client disconnected"
),
tracing::Level::INFO => tracing::info!(
// addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()),
trace = %e.print_span_trace(),
err = format!("{}", e.context),
"Client disconnected"
),
_ => {
tracing::error!(
// addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()),
trace = %e.print_span_trace(),
err = format!("{}", e.context),
actual_level = ?e.level,
"Client disconnected (bad level)"
)
}
}
} else { } else {
tracing::debug!( tracing::debug!(
addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()), addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()),
@ -74,42 +97,55 @@ async fn main() {
} }
} }
#[tracing::instrument(level = "info", skip(api, client_stream, config))]
async fn process_connection<T: MinecraftServerHandle>( async fn process_connection<T: MinecraftServerHandle>(
mut client_stream: TcpStream, mut client_stream: TcpStream,
addr: SocketAddr, addr: SocketAddr,
api: impl MinecraftAPI<T>, api: impl MinecraftAPI<T>,
config: Config, config: Config,
) -> Result<(), OpaqueError> { ) -> Result<(), OpaqueError> {
let client_packet = Packet::parse(&mut client_stream).await?; // this is wrapper so that async doesnt mess up the span, and
// to make sure this doesn't propagate to later `handle_*`
#[tracing::instrument(level = "info", skip(client_stream, config))]
async fn first_packet(
client_stream: &mut TcpStream,
config: Config,
) -> Result<Option<packets::serverbound::handshake::Handshake>, OpaqueError> {
let client_packet = Packet::parse(client_stream).await?;
// --- Handshake --- // --- Handshake ---
let handshake; let handshake;
let next_server_state; let packet_id = client_packet.id.get_int();
let packet_id = client_packet.id.get_int(); if packet_id != 0 {
if packet_id != 0 { return Err(OpaqueError::create(&format!(
return Err(OpaqueError::create(&format!( "Client HANDSHAKE -> bad packet; id={packet_id} Disconnecting..."
"Client HANDSHAKE -> bad packet; id={packet_id} Disconnecting..." )));
))); }
handshake = packets::serverbound::handshake::Handshake::parse(client_packet)
.await
.ok_or_else(|| "Client HANDSHAKE -> malformed packet; Disconnecting...".to_string())?;
let filter = eval_boolean(&format!(
"addr=\"{}\";{}",
handshake.get_server_address(),
config.filter_conn
))
.map_err(|e| format!("filter error! err={:?}", e))?;
if filter {
// TODO: if the server just returns here, the client does not know it
// and sends a packet with the 122 WeirdID
tracing::trace!("filtered out the connection");
return Ok(None);
}
Ok(Some(handshake))
} }
handshake = packets::serverbound::handshake::Handshake::parse(client_packet) let handshake = match first_packet(&mut client_stream, config).await? {
.await Some(x) => x,
.ok_or_else(|| "Client HANDSHAKE -> malformed packet; Disconnecting...".to_string())?; // this is needed because of the filter
None => return Ok(()),
let filter = eval_boolean(&format!( };
"addr=\"{}\";{}",
handshake.get_server_address(),
config.filter_conn
))
.map_err(|e| format!("filter error! err={:?}", e))?;
if filter {
// TODO: if the server just returns here, the client does not know it
// and sends a packet with the 122 WeirdID
return Ok(());
}
next_server_state = handshake.get_next_state();
let next_server_state = handshake.get_next_state();
match next_server_state { match next_server_state {
packets::ProtocolState::Status => { packets::ProtocolState::Status => {
handle_status(&mut client_stream, &handshake, api).await?; handle_status(&mut client_stream, &handshake, api).await?;
@ -159,7 +195,6 @@ async fn handle_status<T: MinecraftServerHandle>(
}; };
let server_addr = handshake.get_server_address(); let server_addr = handshake.get_server_address();
let commit_hash: &'static str = env!("COMMIT_HASH");
let mut status_struct = StatusStructNew::create(); let mut status_struct = StatusStructNew::create();
status_struct.version.protocol = handshake.protocol_version.get_int(); status_struct.version.protocol = handshake.protocol_version.get_int();
@ -172,7 +207,7 @@ async fn handle_status<T: MinecraftServerHandle>(
{ {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
let span = tracing::span!(tracing::Level::WARN, "not_found", err = e.context); let span = tracing::span!(tracing::Level::INFO, "unavailable", err = e.get_kind());
status_struct.players.max = 0; status_struct.players.max = 0;
status_struct.players.online = 0; status_struct.players.online = 0;
status_struct.description.text = format!( status_struct.description.text = format!(
@ -184,22 +219,25 @@ async fn handle_status<T: MinecraftServerHandle>(
.await?; .await?;
// Recieve the ping packet, so the client does not send it again // Recieve the ping packet, so the client does not send it again
let _ping = Packet::parse(client_stream).instrument(span.clone()).await; let _ping = Packet::parse(client_stream)
.instrument(span.clone())
.await
.map_err(|err| {
tracing::debug!(
err = OpaqueError::from(err).context,
"failed to parse ping packet"
)
});
// Send a bad ping packet back, so the client shows *searching* icon // Send a bad ping packet back, so the client shows *searching* icon
let _pong = Packet::new(9, vec![0; 8]) let _pong = Packet::new(9, vec![0; 8])
.ok_or("failed to create empty pong packet?")? .ok_or("failed to create empty pong packet?")?
.send_packet(client_stream) .send_packet(client_stream)
.instrument(span.clone()) .instrument(span.clone())
.await .await
.map_err(|_| "failed to send pong packet")?; .map_err(|_| tracing::debug!("failed to send pong packet"));
let _guard = span.enter();
match _ping { let status = ServerDeploymentStatus::Unavailable(e.get_kind().to_string());
Ok(_) => tracing::info!("sent status with error"), tracing::info!(status = ?status, "status request");
Err(err) => {
tracing::error!(err = OpaqueError::from(err).context)
}
};
return Ok(()); return Ok(());
} }
}; };
@ -209,7 +247,6 @@ async fn handle_status<T: MinecraftServerHandle>(
.get_motd() .get_motd()
.unwrap_or("A minecraft server (proxy motd)".to_string()); .unwrap_or("A minecraft server (proxy motd)".to_string());
tracing::trace!(motd);
match status { match status {
ServerDeploymentStatus::Connectable(mut server_stream) => { ServerDeploymentStatus::Connectable(mut server_stream) => {
return server return server
@ -227,6 +264,7 @@ async fn handle_status<T: MinecraftServerHandle>(
status_struct.description.text = status_struct.description.text =
format!("{motd}\n§4Offline§r §oJoin to start!§r - {BYE_MESSAGE}"); format!("{motd}\n§4Offline§r §oJoin to start!§r - {BYE_MESSAGE}");
} }
ServerDeploymentStatus::Unavailable(_) => unreachable!(),
}; };
mc_server::complete_status_request(client_stream, status_struct).await?; mc_server::complete_status_request(client_stream, status_struct).await?;
@ -245,7 +283,8 @@ async fn handle_login<T: MinecraftServerHandle>(
&handshake.get_server_address(), &handshake.get_server_address(),
&handshake.server_port.get_value().to_string(), &handshake.server_port.get_value().to_string(),
) )
.await?; .await
.map_err(|e| e.set_level(tracing::Level::WARN))?;
let status = server.query_status().await?; let status = server.query_status().await?;
tracing::debug!(msg = "server status", status = ?status); tracing::debug!(msg = "server status", status = ?status);
@ -281,12 +320,20 @@ async fn handle_login<T: MinecraftServerHandle>(
.execute(client_stream, &mut server_stream) .execute(client_stream, &mut server_stream)
.await; .await;
tracing::debug!("data exchanged: tx: {} rx: {}", traffic.tx, traffic.rx); match traffic.error {
if let Some(e) = traffic.error { Some(e) => {
return Err(OpaqueError::create(&format!( tracing::warn!(
"failed to splice; err = {}", tx = traffic.tx,
e rx = traffic.rx,
))); err = ?e,
"connection splicing ended with error",
)
}
None => tracing::info!(
tx = traffic.tx,
rx = traffic.rx,
"connection splicing ended",
),
} }
} }
ServerDeploymentStatus::PodOk | ServerDeploymentStatus::Starting => { ServerDeploymentStatus::PodOk | ServerDeploymentStatus::Starting => {
@ -298,6 +345,7 @@ async fn handle_login<T: MinecraftServerHandle>(
.await?; .await?;
mc_server::send_disconnect(client_stream, format!("[\"\",{{\"text\":\"Okayy, §2starting§r the server!\n\n\"}},{{\"text\":\"{BYE_MESSAGE}\"}}]").as_str()).await?; mc_server::send_disconnect(client_stream, format!("[\"\",{{\"text\":\"Okayy, §2starting§r the server!\n\n\"}},{{\"text\":\"{BYE_MESSAGE}\"}}]").as_str()).await?;
} }
ServerDeploymentStatus::Unavailable(_) => unreachable!(),
} }
Ok(()) Ok(())
} }

View file

@ -139,6 +139,7 @@ pub enum ServerDeploymentStatus {
Starting, Starting,
PodOk, PodOk,
Offline, Offline,
Unavailable(String),
} }
pub fn sanitize_addr(addr: &str) -> &str { pub fn sanitize_addr(addr: &str) -> &str {

View file

@ -1,10 +1,16 @@
use std::{error::Error, fmt}; use std::{
error::Error,
fmt::{self, Debug},
};
use tracing::Level;
use tracing_error::SpanTrace; use tracing_error::SpanTrace;
#[derive(Debug)] #[derive(Debug)]
pub struct OpaqueError { pub struct OpaqueError {
span_trace: SpanTrace, span_trace: SpanTrace,
pub context: String, pub context: String,
pub level: Level,
kind: Option<String>,
} }
impl fmt::Display for OpaqueError { impl fmt::Display for OpaqueError {
@ -31,13 +37,23 @@ impl fmt::Display for OpaqueError {
} }
impl Error for OpaqueError {} impl Error for OpaqueError {}
impl OpaqueError { impl OpaqueError {
pub fn create(str: &str) -> OpaqueError { pub fn create(context: &str) -> OpaqueError {
Self { Self {
span_trace: SpanTrace::capture(), span_trace: SpanTrace::capture(),
context: str.to_string(), context: context.to_string(),
level: Level::ERROR,
kind: None,
} }
} }
pub fn get_span_trace(&self) -> String { pub fn create_with_kind(context: &str, kind: &str) -> OpaqueError {
Self {
span_trace: SpanTrace::capture(),
context: context.to_string(),
level: Level::ERROR,
kind: Some(kind.to_string()),
}
}
pub fn print_span_trace(&self) -> FancyTrace {
let mut vec: Vec<(&str, String)> = Vec::new(); let mut vec: Vec<(&str, String)> = Vec::new();
self.span_trace.with_spans(|metadata, _fields| { self.span_trace.with_spans(|metadata, _fields| {
@ -45,30 +61,50 @@ impl OpaqueError {
true true
}); });
let str = vec.iter().rfold(String::new(), |mut acc, x| { let str = vec.iter().rfold(String::new(), |mut acc, x| {
let first = acc.len() != 0; let first = acc.len() == 0;
if first { if !first {
acc.push_str(":"); // TODO: should this code be hardcoded?
acc.push_str("\x1b[2m:\x1b[0m");
} }
acc.push_str(x.0); acc.push_str(x.0);
acc.push_str("{"); if !x.1.is_empty() {
acc.push_str(&x.1); acc.push_str("{");
acc.push_str("}"); acc.push_str(&x.1);
acc.push_str("}");
}
acc acc
}); });
match String::from_utf8(strip_ansi_escapes::strip(str.clone())) {
Ok(x) => x, FancyTrace { str }
Err(_) => str, }
pub fn set_level(mut self, lvl: Level) -> Self {
self.level = lvl;
self
}
pub fn get_kind(&self) -> &str {
match self.kind.as_ref() {
Some(x) => &x,
None => &self.context,
} }
} }
} }
pub struct FancyTrace {
str: String,
}
impl fmt::Display for FancyTrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\x1b[2m{{{{\x1b[0m {} \x1b[2m}}}}\x1b[0m", self.str)
}
}
impl From<String> for OpaqueError { impl From<String> for OpaqueError {
fn from(value: String) -> Self { fn from(value: String) -> Self {
Self { Self {
span_trace: SpanTrace::capture(), span_trace: SpanTrace::capture(),
context: value, context: value,
level: Level::ERROR,
kind: None,
} }
} }
} }
@ -78,6 +114,8 @@ impl From<&str> for OpaqueError {
Self { Self {
span_trace: SpanTrace::capture(), span_trace: SpanTrace::capture(),
context: value.to_string(), context: value.to_string(),
level: Level::ERROR,
kind: None,
} }
} }
} }
@ -87,6 +125,8 @@ impl From<crate::packets::ParseErrorTrace> for OpaqueError {
Self { Self {
span_trace: value.context, span_trace: value.context,
context: format!("{:?}", value.inner), context: format!("{:?}", value.inner),
level: Level::ERROR,
kind: None,
} }
} }
} }

View file

@ -61,7 +61,7 @@ impl Packet {
pub async fn parse(stream: &mut TcpStream) -> Result<Packet, ParseErrorTrace> { pub async fn parse(stream: &mut TcpStream) -> Result<Packet, ParseErrorTrace> {
let length = VarInt::parse(stream) let length = VarInt::parse(stream)
.await .await
.ok_or(ParseError::IDParseError)?; .ok_or(ParseError::LengthParseError)?;
tracing::trace!(length = length.get_int()); tracing::trace!(length = length.get_int());
let id = match VarInt::parse(stream).await { let id = match VarInt::parse(stream).await {
@ -150,6 +150,7 @@ impl From<ParseError> for ParseErrorTrace {
} }
pub enum ParseError { pub enum ParseError {
LengthParseError,
IDParseError, IDParseError,
WeirdID, WeirdID,
LengthIsTooBig(i32), LengthIsTooBig(i32),
@ -162,7 +163,10 @@ pub enum ParseError {
impl fmt::Debug for ParseError { impl fmt::Debug for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::IDParseError => write!(f, "IDParseError: could not parse packet id"), Self::LengthParseError => {
write!(f, "LengthParseError: could not parse packet length VarInt")
}
Self::IDParseError => write!(f, "IDParseError: could not parse packet id VarInt"),
Self::WeirdID => write!(f, "WeirdID: weird packet id encountered: 122"), Self::WeirdID => write!(f, "WeirdID: weird packet id encountered: 122"),
Self::LengthIsTooBig(x) => { Self::LengthIsTooBig(x) => {
write!(f, "LengthIsTooBig: packet length is too big; len={x}") write!(f, "LengthIsTooBig: packet length is too big; len={x}")