Disconnect banned IPs based on server banned-ips.json file
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -446,6 +446,7 @@ checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@@ -468,6 +469,17 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
|
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@@ -508,11 +520,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
|
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg 1.0.1",
|
"autocfg 1.0.1",
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -639,6 +655,7 @@ dependencies = [
|
|||||||
"rand 0.8.4",
|
"rand 0.8.4",
|
||||||
"rcon",
|
"rcon",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"shlex",
|
"shlex",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@@ -38,12 +38,13 @@ colored = "2.0"
|
|||||||
derive_builder = "0.10"
|
derive_builder = "0.10"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
flate2 = { version = "1.0", default-features = false, features = ["default"] }
|
flate2 = { version = "1.0", default-features = false, features = ["default"] }
|
||||||
futures = { version = "0.3", default-features = false }
|
futures = { version = "0.3", default-features = false, features = ["executor"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "a14b40e" }
|
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "a14b40e" }
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
|
serde_json = "1.0"
|
||||||
shlex = "1.1"
|
shlex = "1.1"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal", "sync"] }
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal", "sync"] }
|
||||||
|
@@ -43,6 +43,9 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
|
|||||||
#start_timeout = 300
|
#start_timeout = 300
|
||||||
#stop_timeout = 150
|
#stop_timeout = 150
|
||||||
|
|
||||||
|
# Block banned IPs as listed in banned-ips.json in server directory.
|
||||||
|
#block_banned_ips = true
|
||||||
|
|
||||||
[time]
|
[time]
|
||||||
# Sleep after number of seconds.
|
# Sleep after number of seconds.
|
||||||
#sleep_after = 60
|
#sleep_after = 60
|
||||||
|
@@ -180,6 +180,10 @@ pub struct Server {
|
|||||||
/// Server stopping timeout. Force kill server process if it takes longer.
|
/// Server stopping timeout. Force kill server process if it takes longer.
|
||||||
#[serde(default = "u32_150")]
|
#[serde(default = "u32_150")]
|
||||||
pub stop_timeout: u32,
|
pub stop_timeout: u32,
|
||||||
|
|
||||||
|
/// Block banned IPs as listed in banned-ips.json in server directory.
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub block_banned_ips: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Time configuration.
|
/// Time configuration.
|
||||||
@@ -451,3 +455,7 @@ fn u32_300() -> u32 {
|
|||||||
fn u32_150() -> u32 {
|
fn u32_150() -> u32 {
|
||||||
300
|
300
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bool_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
37
src/mc/ban.rs
Normal file
37
src/mc/ban.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// File name.
|
||||||
|
pub const FILE: &str = "banned-ips.json";
|
||||||
|
|
||||||
|
/// A banned IP entry.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct BannedIp {
|
||||||
|
/// Banned IP.
|
||||||
|
pub ip: IpAddr,
|
||||||
|
|
||||||
|
/// Ban creation time.
|
||||||
|
pub created: String,
|
||||||
|
|
||||||
|
/// Ban source.
|
||||||
|
pub source: String,
|
||||||
|
|
||||||
|
/// Ban expiry time.
|
||||||
|
pub expires: String,
|
||||||
|
|
||||||
|
/// Ban reason.
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load banned IPs from file.
|
||||||
|
pub fn load(path: &Path) -> Result<Vec<BannedIp>, Box<dyn Error>> {
|
||||||
|
// Load file contents
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
// Parse contents
|
||||||
|
Ok(serde_json::from_str(&contents)?)
|
||||||
|
}
|
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod ban;
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
pub mod rcon;
|
pub mod rcon;
|
||||||
pub mod server_properties;
|
pub mod server_properties;
|
||||||
|
105
src/server.rs
105
src/server.rs
@@ -1,3 +1,4 @@
|
|||||||
|
use std::net::IpAddr;
|
||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -12,6 +13,7 @@ use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
|
|||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::mc::ban::BannedIp;
|
||||||
use crate::os;
|
use crate::os;
|
||||||
|
|
||||||
/// Server cooldown after the process quit.
|
/// Server cooldown after the process quit.
|
||||||
@@ -25,45 +27,6 @@ const SERVER_QUIT_COOLDOWN: Duration = Duration::from_millis(2500);
|
|||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
const RCON_COOLDOWN: Duration = Duration::from_secs(15);
|
const RCON_COOLDOWN: Duration = Duration::from_secs(15);
|
||||||
|
|
||||||
/// Server state.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
||||||
pub enum State {
|
|
||||||
/// Server is stopped.
|
|
||||||
Stopped,
|
|
||||||
|
|
||||||
/// Server is starting.
|
|
||||||
Starting,
|
|
||||||
|
|
||||||
/// Server is online and responding.
|
|
||||||
Started,
|
|
||||||
|
|
||||||
/// Server is stopping.
|
|
||||||
Stopping,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
/// From u8, panics if invalid.
|
|
||||||
pub fn from_u8(state: u8) -> Self {
|
|
||||||
match state {
|
|
||||||
0 => Self::Stopped,
|
|
||||||
1 => Self::Starting,
|
|
||||||
2 => Self::Started,
|
|
||||||
3 => Self::Stopping,
|
|
||||||
_ => panic!("invalid State u8"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// To u8.
|
|
||||||
pub fn to_u8(self) -> u8 {
|
|
||||||
match self {
|
|
||||||
Self::Stopped => 0,
|
|
||||||
Self::Starting => 1,
|
|
||||||
Self::Started => 2,
|
|
||||||
Self::Stopping => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shared server state.
|
/// Shared server state.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
@@ -102,6 +65,9 @@ pub struct Server {
|
|||||||
/// Used as starting/stopping timeout.
|
/// Used as starting/stopping timeout.
|
||||||
kill_at: RwLock<Option<Instant>>,
|
kill_at: RwLock<Option<Instant>>,
|
||||||
|
|
||||||
|
/// List of banned IPs.
|
||||||
|
banned_ips: RwLock<Vec<BannedIp>>,
|
||||||
|
|
||||||
/// Lock for exclusive RCON operations.
|
/// Lock for exclusive RCON operations.
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
rcon_lock: Semaphore,
|
rcon_lock: Semaphore,
|
||||||
@@ -345,6 +311,27 @@ impl Server {
|
|||||||
.filter(|d| *d > 0)
|
.filter(|d| *d > 0)
|
||||||
.map(|d| Instant::now() + Duration::from_secs(d as u64));
|
.map(|d| Instant::now() + Duration::from_secs(d as u64));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check whether the given IP is banned.
|
||||||
|
///
|
||||||
|
/// This uses the latest known `banned-ips.json` contents if known.
|
||||||
|
/// If this feature is disabled, this will always return false.
|
||||||
|
pub async fn is_banned_ip(&self, ip: &IpAddr) -> bool {
|
||||||
|
self.banned_ips.read().await.iter().any(|i| &i.ip == ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the given IP is banned.
|
||||||
|
///
|
||||||
|
/// This uses the latest known `banned-ips.json` contents if known.
|
||||||
|
/// If this feature is disabled, this will always return false.
|
||||||
|
pub fn is_banned_ip_blocking(&self, ip: &IpAddr) -> bool {
|
||||||
|
futures::executor::block_on(async { self.is_banned_ip(ip).await })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the list of banned IPs.
|
||||||
|
pub async fn set_banned_ips(&self, ips: Vec<BannedIp>) {
|
||||||
|
*self.banned_ips.write().await = ips;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Server {
|
impl Default for Server {
|
||||||
@@ -360,6 +347,7 @@ impl Default for Server {
|
|||||||
last_active: Default::default(),
|
last_active: Default::default(),
|
||||||
keep_online_until: Default::default(),
|
keep_online_until: Default::default(),
|
||||||
kill_at: Default::default(),
|
kill_at: Default::default(),
|
||||||
|
banned_ips: Default::default(),
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
rcon_lock: Semaphore::new(1),
|
rcon_lock: Semaphore::new(1),
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
@@ -368,6 +356,45 @@ impl Default for Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Server state.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum State {
|
||||||
|
/// Server is stopped.
|
||||||
|
Stopped,
|
||||||
|
|
||||||
|
/// Server is starting.
|
||||||
|
Starting,
|
||||||
|
|
||||||
|
/// Server is online and responding.
|
||||||
|
Started,
|
||||||
|
|
||||||
|
/// Server is stopping.
|
||||||
|
Stopping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
/// From u8, panics if invalid.
|
||||||
|
pub fn from_u8(state: u8) -> Self {
|
||||||
|
match state {
|
||||||
|
0 => Self::Stopped,
|
||||||
|
1 => Self::Starting,
|
||||||
|
2 => Self::Started,
|
||||||
|
3 => Self::Stopping,
|
||||||
|
_ => panic!("invalid State u8"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To u8.
|
||||||
|
pub fn to_u8(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Stopped => 0,
|
||||||
|
Self::Starting => 1,
|
||||||
|
Self::Started => 2,
|
||||||
|
Self::Stopping => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Invoke server command, store PID and wait for it to quit.
|
/// Invoke server command, store PID and wait for it to quit.
|
||||||
pub async fn invoke_server_cmd(
|
pub async fn invoke_server_cmd(
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
|
@@ -6,6 +6,7 @@ use futures::FutureExt;
|
|||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::mc::ban::{self, BannedIp};
|
||||||
use crate::proto::client::Client;
|
use crate::proto::client::Client;
|
||||||
use crate::proxy;
|
use crate::proxy;
|
||||||
use crate::server::{self, Server};
|
use crate::server::{self, Server};
|
||||||
@@ -23,6 +24,9 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
|
|||||||
// Load server state
|
// Load server state
|
||||||
let server = Arc::new(Server::default());
|
let server = Arc::new(Server::default());
|
||||||
|
|
||||||
|
// Load banned IPs
|
||||||
|
server.set_banned_ips(load_banned_ips(&config)).await;
|
||||||
|
|
||||||
// Listen for new connections
|
// Listen for new connections
|
||||||
let listener = TcpListener::bind(config.public.address)
|
let listener = TcpListener::bind(config.public.address)
|
||||||
.await
|
.await
|
||||||
@@ -66,6 +70,12 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
|
|||||||
/// Route inbound TCP stream to correct service, spawning a new task.
|
/// Route inbound TCP stream to correct service, spawning a new task.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
||||||
|
// Check ban
|
||||||
|
if !check_ban(&inbound, &server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route connection through proper channel
|
||||||
let should_proxy = server.state() == server::State::Started && !config.lockout.enabled;
|
let should_proxy = server.state() == server::State::Started && !config.lockout.enabled;
|
||||||
if should_proxy {
|
if should_proxy {
|
||||||
route_proxy(inbound, config)
|
route_proxy(inbound, config)
|
||||||
@@ -74,6 +84,29 @@ fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check whether user IP is banned.
|
||||||
|
///
|
||||||
|
/// Returns `true` if user is still allowed to connect.
|
||||||
|
fn check_ban(inbound: &TcpStream, server: &Server) -> bool {
|
||||||
|
// Get user peer address
|
||||||
|
let peer = match inbound.peer_addr() {
|
||||||
|
Ok(peer) => peer,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(target: "lazymc", "Connection from unknown peer, disconnecting: {}", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if user is banned
|
||||||
|
let is_banned = server.is_banned_ip_blocking(&peer.ip());
|
||||||
|
if is_banned {
|
||||||
|
warn!(target: "lazymc", "Connection from banned IP {}, disconnecting", peer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Route inbound TCP stream to status server, spawning a new task.
|
/// Route inbound TCP stream to status server, spawning a new task.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
||||||
@@ -123,3 +156,41 @@ pub fn route_proxy_address_queue(inbound: TcpStream, addr: SocketAddr, queue: By
|
|||||||
|
|
||||||
tokio::spawn(service);
|
tokio::spawn(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load banned IPs if IP banning is enabled.
|
||||||
|
///
|
||||||
|
/// If disabled or on error, an empty list is returned.
|
||||||
|
fn load_banned_ips(config: &Config) -> Vec<BannedIp> {
|
||||||
|
// Blocking banned IPs must be enabled
|
||||||
|
if !config.server.block_banned_ips {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure server directory is set, it must exist
|
||||||
|
let dir = match &config.server.directory {
|
||||||
|
Some(dir) => dir,
|
||||||
|
None => {
|
||||||
|
warn!(target: "lazymc", "Not blocking banned IPs, server directory not configured, unable to find {} file", ban::FILE);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine file path, ensure it exists
|
||||||
|
let path = dir.join(crate::mc::ban::FILE);
|
||||||
|
if !path.is_file() {
|
||||||
|
warn!(target: "lazymc", "Not blocking banned IPs, {} file does not exist", ban::FILE);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load banned IPs
|
||||||
|
let banned_ips = match ban::load(&path) {
|
||||||
|
Ok(ips) => ips,
|
||||||
|
Err(err) => {
|
||||||
|
// TODO: quit here, require user to disable feature as security feature?
|
||||||
|
error!(target: "lazymc", "Failed to load banned IPs from {}: {}", ban::FILE, err);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
banned_ips
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user