feat: add connection filters; and config struct;

also move sanitize_addr to mc_server module
This commit is contained in:
Tamipes 2025-12-14 12:56:21 +01:00
parent 3dcf2f03a8
commit 822330ef87
7 changed files with 94 additions and 41 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ target
result result
.direnv .direnv
DEBUG.sh

10
Cargo.lock generated
View file

@ -377,6 +377,15 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "evalexpr"
version = "13.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25929004897f2bbab309121a60400d36992f6d911d09baa6c172f6cc55706601"
dependencies = [
"regex",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "5.4.1" version = "5.4.1"
@ -947,6 +956,7 @@ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"either", "either",
"evalexpr",
"futures", "futures",
"k8s-openapi", "k8s-openapi",
"kube", "kube",

View file

@ -40,3 +40,4 @@ either = "1.6.1"
nix = { version= "0.30.1", features = [ "zerocopy"] } nix = { version= "0.30.1", features = [ "zerocopy"] }
tokio-splice2 = "0.3.2" tokio-splice2 = "0.3.2"
strip-ansi-escapes = "0.2.1" strip-ansi-escapes = "0.2.1"
evalexpr = { version = "13.1.0", features = ["regex"] }

View file

@ -1,2 +1,6 @@
# Env variables: # Env variables:
- BIND_ADDR(default: 0.0.0.0:25565): the address the server should bind to - BIND_ADDR(default: 0.0.0.0:25565): the address the server should bind to
- FILTER_CONN(default: '(addr == "10.100.0.1")'): the filter appplied with [evalexpr](https://docs.rs/evalexpr/latest/evalexpr/)
The "context" has the addr variable populated with the address of part of the
handshake packet. Custom filter can be specified, and drop any
connections which match the filter.

View file

@ -11,7 +11,7 @@ use tokio::task::JoinHandle;
use tracing::Instrument; use tracing::Instrument;
use crate::{ use crate::{
mc_server::{MinecraftAPI, MinecraftServerHandle, ServerDeploymentStatus}, mc_server::{sanitize_addr, MinecraftAPI, MinecraftServerHandle, ServerDeploymentStatus},
packets::{ packets::{
clientbound::status::StatusTrait, clientbound::status::StatusTrait,
serverbound::handshake::{self}, serverbound::handshake::{self},
@ -430,34 +430,3 @@ impl From<kube::Error> for OpaqueError {
OpaqueError::create(value.to_string().as_str()) OpaqueError::create(value.to_string().as_str())
} }
} }
fn terminate_at_null(str: &str) -> &str {
match str.split('\0').next() {
Some(x) => x,
None => str,
}
}
fn sanitize_addr(addr: &str) -> &str {
// Thanks to a buggy minecraft, when the client sends a join
// from a SRV DNS record, it will not use the address typed
// in the game, but use the address redicted *to* by the
// DNS record as the address for joining, plus a trailing "."
//
// For example:
// server.example.com (_minecraft._tcp.server.example.com)
// (the typed address) I (the DNS SRV record which gets read)
// V
// 5 25565 server.example.com
// I (the response for the DNS SRV query)
// V
// server.example.com.
// (the address used in the protocol)
let addr = addr.trim_end_matches(".");
// Modded minecraft clients send null terminated strings,
// after which they have extra data. This just removes them
// from the addr lookup
let addr = terminate_at_null(addr);
addr
}

View file

@ -1,3 +1,4 @@
use evalexpr::*;
use std::env; use std::env;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::time::Duration; use std::time::Duration;
@ -38,23 +39,22 @@ async fn main() {
let api = kube_cache::McApi::create().unwrap(); let api = kube_cache::McApi::create().unwrap();
tracing::info!("initialized kube api"); tracing::info!("initialized kube api");
let addr = match env::var("BIND_ADDR") { let config: Config = Default::default();
Ok(x) => x,
Err(_) => "0.0.0.0:25565".to_string(), let listener = TcpListener::bind(config.bind_addr.clone()).await.unwrap();
}; tracing::info!(bind_addr = config.bind_addr, "started tcp server");
let listener = TcpListener::bind(addr.clone()).await.unwrap();
tracing::info!(addr, "started tcp server");
loop { loop {
let (socket, addr) = listener.accept().await.unwrap(); let (socket, addr) = listener.accept().await.unwrap();
let api = api.clone(); let api = api.clone();
let config = config.clone();
tokio::spawn(async move { tokio::spawn(async move {
tracing::debug!( tracing::debug!(
addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()), addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()),
"Client connected" "Client connected"
); );
if let Err(e) = process_connection(socket, addr, api).await { if let Err(e) = process_connection(socket, addr, api, config).await {
tracing::error!( tracing::error!(
// addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()), // addr = format!("{}:{}", addr.ip().to_string(), addr.port().to_string()),
trace = format!("{}", e.get_span_trace()), trace = format!("{}", e.get_span_trace()),
@ -71,11 +71,12 @@ async fn main() {
} }
} }
#[tracing::instrument(level = "info", skip(api, client_stream))] #[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,
) -> Result<(), OpaqueError> { ) -> Result<(), OpaqueError> {
let client_packet = Packet::parse(&mut client_stream).await?; let client_packet = Packet::parse(&mut client_stream).await?;
@ -92,6 +93,18 @@ async fn process_connection<T: MinecraftServerHandle>(
.await .await
.ok_or_else(|| "Client HANDSHAKE -> malformed packet; Disconnecting...".to_string())?; .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
return Ok(());
}
next_server_state = handshake.get_next_state(); next_server_state = handshake.get_next_state();
match next_server_state { match next_server_state {
@ -256,3 +269,27 @@ async fn handle_login<T: MinecraftServerHandle>(
} }
Ok(()) Ok(())
} }
#[derive(Clone)]
struct Config {
pub filter_conn: String,
pub bind_addr: String,
}
impl Default for Config {
fn default() -> Self {
let filter_conn = match env::var("FILTER_CONN") {
Ok(x) => x,
Err(_) => "(addr == \"10.100.0.1\")".to_string(),
};
let bind_addr = match env::var("BIND_ADDR") {
Ok(x) => x,
Err(_) => "0.0.0.0:25565".to_string(),
};
Self {
filter_conn,
bind_addr,
}
}
}

View file

@ -3,7 +3,7 @@ use tokio::{io::AsyncWriteExt, net::TcpStream};
use crate::{ use crate::{
packets::{ packets::{
clientbound::status::{StatusStructNew, StatusTrait}, clientbound::status::{StatusStructNew, StatusTrait},
serverbound::handshake::{self, Handshake}, serverbound::handshake::Handshake,
Packet, SendPacket, Packet, SendPacket,
}, },
OpaqueError, OpaqueError,
@ -137,3 +137,34 @@ pub enum ServerDeploymentStatus {
PodOk, PodOk,
Offline, Offline,
} }
pub fn sanitize_addr(addr: &str) -> &str {
// Thanks to a buggy minecraft, when the client sends a join
// from a SRV DNS record, it will not use the address typed
// in the game, but use the address redicted *to* by the
// DNS record as the address for joining, plus a trailing "."
//
// For example:
// server.example.com (_minecraft._tcp.server.example.com)
// (the typed address) I (the DNS SRV record which gets read)
// V
// 5 25565 server.example.com
// I (the response for the DNS SRV query)
// V
// server.example.com.
// (the address used in the protocol)
let addr = addr.trim_end_matches(".");
// Modded minecraft clients send null terminated strings,
// after which they have extra data. This just removes them
// from the addr lookup
let addr = terminate_at_null(addr);
addr
}
fn terminate_at_null(str: &str) -> &str {
match str.split('\0').next() {
Some(x) => x,
None => str,
}
}