Skip to content
22 changes: 14 additions & 8 deletions src/action/start.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;

use clap::ArgMatches;
Expand All @@ -14,20 +15,25 @@ const RCON_PASSWORD_LENGTH: usize = 32;

/// Start lazymc.
pub fn invoke(matches: &ArgMatches) -> Result<(), ()> {
// Parse bind address
let bind_addr: SocketAddr = matches.get_one::<String>("bind").unwrap().parse().unwrap();
Comment thread
tW4r marked this conversation as resolved.
Outdated

// Load config
#[allow(unused_mut)]
let mut config = config::load(matches);
let mut configs = config::load(matches);

// Prepare RCON if enabled
#[cfg(feature = "rcon")]
prepare_rcon(&mut config);
for config in configs.iter_mut() {
// Prepare RCON if enabled
#[cfg(feature = "rcon")]
prepare_rcon(config);

// Rewrite server server.properties file
rewrite_server_properties(&config);
// Rewrite server server.properties file
rewrite_server_properties(config);
}

// Start server service
let config = Arc::new(config);
service::server::service(config)
let configs_arc = configs.into_iter().map(Arc::new).collect();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I don't think Arc<HashMap<_, Arc<Config>>> is desirable, as this is a very complex type.

I used Arc<Config> to make it easily sharable across async contexts with message passing.

I'll have to think about something that would be better here. I'm not sure right now. Maybe we need to create a custom type to manage these configurations for us rather than working with a hashmap directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a new type Router in src/router.rs to deal with this, what do you think?

service::server::service(bind_addr, configs_arc)
}

/// Prepare RCON.
Expand Down
8 changes: 8 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ pub fn app() -> Command {
)
.subcommand(Command::new("test").about("Test config")),
)
.arg(
Arg::new("bind")
.short('b')
.value_name("ADDRESS")
.default_value("0.0.0.0:25565")
.help("Address to bind to")
.num_args(1),
)
.arg(
Arg::new("config")
.short('c')
Expand Down
42 changes: 33 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,38 @@ pub const CONFIG_FILE: &str = "lazymc.toml";
/// Configuration version user should be using, or warning will be shown.
const CONFIG_VERSION: &str = "0.2.8";

/// Load config from file, based on CLI arguments.
///
/// Quits with an error message on failure.
pub fn load(matches: &ArgMatches) -> Config {
pub fn load(matches: &ArgMatches) -> Vec<Config> {
// Get config path, attempt to canonicalize
let mut path = PathBuf::from(matches.get_one::<String>("config").unwrap());
if let Ok(p) = path.canonicalize() {
path = p;
}

// Ensure configuration file exists
if !path.is_file() {
let paths: Vec<PathBuf> = if path.is_dir() {
path.read_dir()
.unwrap()
.filter_map(|entry| {
entry.ok().and_then(|entry| {
let path = entry.path();
if path.is_file() {
Some(path)
} else {
None
}
})
})
.collect()
} else if path.is_file() {
vec![path.clone()]
} else {
vec![]
};
Comment thread
tW4r marked this conversation as resolved.

// Ensure configuration file/directory exists
if paths.is_empty() {
quit_error_msg(
format!(
"Config file does not exist: {}",
"Config file/directory does not exist: {}",
path.to_str().unwrap_or("?")
),
ErrorHintsBuilder::default()
Expand All @@ -42,6 +59,13 @@ pub fn load(matches: &ArgMatches) -> Config {
);
}

paths.into_iter().map(load_file).collect()
}

/// Load config from file, based on CLI arguments.
///
/// Quits with an error message on failure.
pub fn load_file(path: PathBuf) -> Config {
// Load config
let config = match Config::load(path) {
Ok(config) => config,
Expand Down Expand Up @@ -135,8 +159,8 @@ impl Config {
#[serde(default)]
pub struct Public {
/// Public address.
#[serde(deserialize_with = "to_socket_addrs")]
pub address: SocketAddr,
// #[serde(deserialize_with = "to_socket_addrs")]
pub address: String, // SocketAddr,
Comment thread
tW4r marked this conversation as resolved.
Outdated

/// Minecraft protocol version name hint.
pub version: String,
Expand Down
9 changes: 0 additions & 9 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@ use tokio::net::TcpStream;

use crate::net;

/// Proxy the inbound stream to a target address.
pub async fn proxy(
Comment thread
timvisee marked this conversation as resolved.
inbound: TcpStream,
proxy_header: ProxyHeader,
addr_target: SocketAddr,
) -> Result<(), Box<dyn Error>> {
proxy_with_queue(inbound, proxy_header, addr_target, &[]).await
}

/// Proxy the inbound stream to a target address.
///
/// Send the queue to the target server before proxying.
Expand Down
122 changes: 46 additions & 76 deletions src/service/server.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;

Expand All @@ -8,7 +9,7 @@ use tokio::net::{TcpListener, TcpStream};
use crate::config::Config;
use crate::proto::client::Client;
use crate::proxy::{self, ProxyHeader};
use crate::server::{self, Server};
use crate::server::Server;
use crate::service;
use crate::status;
use crate::util::error::{quit_error, ErrorHints};
Expand All @@ -19,60 +20,68 @@ use crate::util::error::{quit_error, ErrorHints};
///
/// Spawns a tokio runtime to complete all work on.
#[tokio::main(flavor = "multi_thread")]
pub async fn service(config: Arc<Config>) -> Result<(), ()> {
// Load server state
let server = Arc::new(Server::default());

pub async fn service(bind_addr: SocketAddr, configs: Vec<Arc<Config>>) -> Result<(), ()> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I haven't inspected this in-depth right now. I'll try to do that later when the other changes have been made.

// Listen for new connections
let listener = TcpListener::bind(config.public.address)
.await
.map_err(|err| {
quit_error(
anyhow!(err).context("Failed to start proxy server"),
ErrorHints::default(),
);
})?;
let listener = TcpListener::bind(bind_addr).await.map_err(|err| {
quit_error(
anyhow!(err).context("Failed to start proxy server"),
ErrorHints::default(),
);
})?;

info!(
target: "lazymc",
"Proxying public {} to server {}",
config.public.address, config.server.address,
);
let mut servers = HashMap::new();

if config.lockout.enabled {
warn!(
for config in configs.iter() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

We should probably extract this (the logic for doing something for many configuration files) into a separate function we can invoke for each configuration.

This is now a bit messy (and I admit my code already was).

// Load server state
let server = Arc::new(Server::default());
servers.insert(
config.public.address.clone(),
(config.clone(), server.clone()),
);

info!(
target: "lazymc",
"Lockout mode is enabled, nobody will be able to connect through the proxy",
"Proxying public {} to server {}",
config.public.address, config.server.address,
);
}

// Spawn services: monitor, signal handler
tokio::spawn(service::monitor::service(config.clone(), server.clone()));
tokio::spawn(service::signal::service(config.clone(), server.clone()));
if config.lockout.enabled {
warn!(
target: "lazymc",
"Lockout mode is enabled, nobody will be able to connect through the proxy",
);
}

// Spawn services: monitor, signal handler
tokio::spawn(service::monitor::service(config.clone(), server.clone()));
tokio::spawn(service::signal::service(config.clone(), server.clone()));

// Initiate server start
if config.server.wake_on_start {
Server::start(config.clone(), server.clone(), None).await;
}

// Initiate server start
if config.server.wake_on_start {
Server::start(config.clone(), server.clone(), None).await;
// Spawn additional services: probe and ban manager
tokio::spawn(service::probe::service(config.clone(), server.clone()));
tokio::task::spawn_blocking({
let (config, server) = (config.clone(), server.clone());
|| service::file_watcher::service(config, server)
});
}

// Spawn additional services: probe and ban manager
tokio::spawn(service::probe::service(config.clone(), server.clone()));
tokio::task::spawn_blocking({
let (config, server) = (config.clone(), server.clone());
|| service::file_watcher::service(config, server)
});
let servers_arc = Arc::new(servers);
Comment thread
tW4r marked this conversation as resolved.
Outdated

// Route all incomming connections
while let Ok((inbound, _)) = listener.accept().await {
route(inbound, config.clone(), server.clone());
route(inbound, servers_arc.clone());
}

Ok(())
}

/// Route inbound TCP stream to correct service, spawning a new task.
#[inline]
fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
fn route(inbound: TcpStream, servers: Arc<HashMap<String, (Arc<Config>, Arc<Server>)>>) {
// Get user peer address
let peer = match inbound.peer_addr() {
Ok(peer) => peer,
Expand All @@ -82,29 +91,8 @@ fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
}
};

// Check ban state, just drop connection if enabled
let banned = server.is_banned_ip_blocking(&peer.ip());
if config.server.drop_banned_ips {
info!(target: "lazymc", "Connection from banned IP {}, dropping", peer.ip());
return;
}

// Route connection through proper channel
let should_proxy =
!banned && server.state() == server::State::Started && !config.lockout.enabled;
if should_proxy {
route_proxy(inbound, config)
} else {
route_status(inbound, config, server, peer)
}
}

/// Route inbound TCP stream to status server, spawning a new task.
#[inline]
fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>, peer: SocketAddr) {
// When server is not online, spawn a status server
let client = Client::new(peer);
let service = status::serve(client, inbound, config, server).map(|r| {
let service = status::serve(client, inbound, servers).map(|r| {
if let Err(err) = r {
warn!(target: "lazymc", "Failed to serve status: {:?}", err);
}
Expand All @@ -113,24 +101,6 @@ fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>, pe
tokio::spawn(service);
}

/// Route inbound TCP stream to proxy, spawning a new task.
#[inline]
fn route_proxy(inbound: TcpStream, config: Arc<Config>) {
Comment thread
timvisee marked this conversation as resolved.
// When server is online, proxy all
let service = proxy::proxy(
inbound,
ProxyHeader::Proxy.not_none(config.server.send_proxy_v2),
config.server.address,
)
.map(|r| {
if let Err(err) = r {
warn!(target: "lazymc", "Failed to proxy: {}", err);
}
});

tokio::spawn(service);
}

/// Route inbound TCP stream to proxy with queued data, spawning a new task.
#[inline]
pub fn route_proxy_queue(inbound: TcpStream, config: Arc<Config>, queue: BytesMut) {
Expand Down
Loading