improving martillo-maldito to server to protect also nodes (port, ip, etc..)
This commit is contained in:
46
src/cli.rs
46
src/cli.rs
@ -2,8 +2,11 @@ use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "martillo-maldito", about = "A simple iptables ban server")]
|
||||
pub enum Arguments {
|
||||
#[structopt(
|
||||
name = "martillo-maldito",
|
||||
about = "A simple iptables wrapper, including a ban server"
|
||||
)]
|
||||
pub enum Cli {
|
||||
#[structopt(about = "Initialize ban server")]
|
||||
BanServer {
|
||||
#[structopt(name = "Ssh auth log file", short = "f", long = "ssh-file")]
|
||||
@ -11,7 +14,18 @@ pub enum Arguments {
|
||||
#[structopt(name = "Iptables save file", short = "s", long = "iptables-save")]
|
||||
iptables_save: Option<PathBuf>,
|
||||
},
|
||||
#[structopt(about = "List all banned ips")]
|
||||
ListBannedIps,
|
||||
#[structopt(about = "List all secured ports")]
|
||||
ListSecuredPorts {
|
||||
#[structopt(name = "Docker", short = "d", long = "docker")]
|
||||
docker: bool,
|
||||
},
|
||||
#[structopt(about = "Map secured ports to allowed ips")]
|
||||
MapSecuredPortsAllowedIps {
|
||||
#[structopt(name = "Docker", short = "d", long = "docker")]
|
||||
docker: bool,
|
||||
},
|
||||
#[structopt(about = "Check if a port is secured")]
|
||||
IsPortSecured {
|
||||
#[structopt(name = "Port to check", short = "p", long = "port")]
|
||||
@ -19,60 +33,54 @@ pub enum Arguments {
|
||||
#[structopt(name = "Docker", short = "d", long = "docker")]
|
||||
docker: bool,
|
||||
},
|
||||
|
||||
#[structopt(about = "Ban port")]
|
||||
BanPort {
|
||||
SecurePort {
|
||||
#[structopt(name = "Port to ban", short = "p", long = "port")]
|
||||
port: u16,
|
||||
|
||||
#[structopt(name = "Docker", short = "d", long = "docker")]
|
||||
docker: bool,
|
||||
|
||||
#[structopt(name = "Position", short = "P", long = "position")]
|
||||
position: Option<i32>,
|
||||
},
|
||||
|
||||
#[structopt(about = "Unban port")]
|
||||
UnbanPort {
|
||||
UnsecurePort {
|
||||
#[structopt(name = "Port to unban", short = "p", long = "port")]
|
||||
port: u16,
|
||||
#[structopt(name = "Docker", short = "d", long = "docker")]
|
||||
docker: bool,
|
||||
},
|
||||
|
||||
#[structopt(about = "Allow ip and port")]
|
||||
AllowIpPort {
|
||||
AllowIpForPort {
|
||||
#[structopt(name = "Ip to allow", short = "i", long = "ip")]
|
||||
ip: String,
|
||||
|
||||
#[structopt(name = "Port to allow", short = "p", long = "port")]
|
||||
port: u16,
|
||||
#[structopt(name = "Docker", short = "d", long = "docker")]
|
||||
docker: bool,
|
||||
|
||||
#[structopt(name = "Position", short = "P", long = "position")]
|
||||
position: Option<i32>,
|
||||
},
|
||||
|
||||
#[structopt(about = "Allow port for only an ip")]
|
||||
OnlyIpPort {
|
||||
OnlyIpForPort {
|
||||
#[structopt(name = "Ip to allow", short = "i", long = "ip")]
|
||||
ip: String,
|
||||
|
||||
#[structopt(name = "Port to allow", short = "p", long = "port")]
|
||||
port: u16,
|
||||
#[structopt(name = "Docker", short = "d", long = "docker")]
|
||||
docker: bool,
|
||||
},
|
||||
|
||||
#[structopt(about = "Remove ip and port")]
|
||||
RemoveIpPort {
|
||||
#[structopt(about = "Removes an allow port for an ip")]
|
||||
RemoveAllowIpPort {
|
||||
#[structopt(name = "Ip to remove", short = "i", long = "ip")]
|
||||
ip: String,
|
||||
|
||||
#[structopt(name = "Port to remove", short = "p", long = "port")]
|
||||
port: u16,
|
||||
#[structopt(name = "Docker", short = "d", long = "docker")]
|
||||
docker: bool,
|
||||
},
|
||||
#[structopt(about = "Saves the IPTables configuration")]
|
||||
SaveIPTables {
|
||||
#[structopt(name = "Iptables save file", short = "s", long = "iptables-save")]
|
||||
iptables_save: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iptables::IPTables;
|
||||
use regex::Regex;
|
||||
|
||||
pub fn is_port_secured(port: u16, docker: bool) -> bool {
|
||||
let iptables = iptables::new(false).unwrap();
|
||||
@ -15,6 +18,22 @@ pub fn is_port_secured(port: u16, docker: bool) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn list_secured_ports(docker: bool) -> Vec<u16> {
|
||||
let iptables = iptables::new(false).unwrap();
|
||||
let rules = iptables.list("filter", &get_chain(docker));
|
||||
if rules.is_err() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let rgx = get_regex_for_port();
|
||||
rules
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|r| r.contains("-p tcp -m tcp --dport") && r.contains("-j DROP"))
|
||||
.map(|r| extract_port(&rgx, r).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn list_banned_ips() -> Vec<String> {
|
||||
let iptables = iptables::new(false).unwrap();
|
||||
let rules = iptables.list("filter", &get_chain(false));
|
||||
@ -22,66 +41,98 @@ pub fn list_banned_ips() -> Vec<String> {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// TODO: Remove after testing
|
||||
println!("{:?}", rules);
|
||||
|
||||
let rgx = get_regex_for_ip();
|
||||
rules
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|r| r.contains("--source") && r.contains("-j DROP"))
|
||||
.map(|r| r.to_string())
|
||||
.filter(|r| r.contains("-A INPUT") && r.contains("-j DROP") && r.contains("-s"))
|
||||
.map(|r| extract_ip(&rgx, r).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn ban_port(port: u16, docker: bool, position: Option<i32>) {
|
||||
pub fn map_secured_ports_allowed_ips(docker: bool) -> HashMap<u16, Vec<String>> {
|
||||
let mut result: HashMap<u16, Vec<String>> = HashMap::new();
|
||||
let secured_ports = list_secured_ports(docker);
|
||||
if secured_ports.is_empty() {
|
||||
return result;
|
||||
}
|
||||
|
||||
let iptables = iptables::new(false).unwrap();
|
||||
let rules = iptables.list("filter", &get_chain(docker));
|
||||
if rules.is_err() {
|
||||
return result;
|
||||
}
|
||||
|
||||
let rules = rules.unwrap();
|
||||
let rgx = get_regex_for_ip();
|
||||
for port in secured_ports {
|
||||
let ips = rules
|
||||
.iter()
|
||||
.filter(|r| {
|
||||
r.contains("-A INPUT -s")
|
||||
&& r.contains(&format!("-p tcp -m tcp --dport {} -j ACCEPT", port))
|
||||
})
|
||||
.map(|r| extract_ip(&rgx, r).unwrap())
|
||||
.collect();
|
||||
result.insert(port, ips);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn secure_port_rule(port: u16) -> String {
|
||||
format!("-p tcp --dport {} -j DROP", port)
|
||||
}
|
||||
|
||||
pub fn secure_port(port: u16, docker: bool, position: Option<i32>) {
|
||||
let iptables = iptables::new(false).unwrap();
|
||||
|
||||
let table = "filter";
|
||||
let chain = get_chain(docker);
|
||||
let rule = format!("-p tcp --dport {} -j DROP", port);
|
||||
let rule = secure_port_rule(port);
|
||||
if let Some(position) = position {
|
||||
insert_unique(&iptables, table, &chain, &rule, position)
|
||||
} else {
|
||||
append_unique(&iptables, table, &chain, &rule)
|
||||
}
|
||||
|
||||
println!("banned port {}", port);
|
||||
println!("Port {} secured", port);
|
||||
}
|
||||
|
||||
pub fn unban_port(port: u16, docker: bool) {
|
||||
pub fn unsecure_port(port: u16, docker: bool) {
|
||||
let iptables = iptables::new(false).unwrap();
|
||||
let _ = iptables.delete(
|
||||
"filter",
|
||||
&get_chain(docker),
|
||||
&format!("-p tcp --dport {} -j DROP", port),
|
||||
);
|
||||
let _ = iptables.delete("filter", &get_chain(docker), &secure_port_rule(port));
|
||||
|
||||
println!("unbanned port {}", port);
|
||||
println!("Port {} unsecured", port);
|
||||
}
|
||||
|
||||
pub fn allow_ip_port(ip: &str, port: u16, docker: bool, position: Option<i32>) {
|
||||
pub fn allow_ip_for_port_rule(port: u16, ip: &str) -> String {
|
||||
format!("-p tcp --dport {} -s {} -j ACCEPT", port, ip)
|
||||
}
|
||||
|
||||
pub fn allow_ip_for_port(ip: &str, port: u16, docker: bool, position: Option<i32>) {
|
||||
let iptables = iptables::new(false).unwrap();
|
||||
|
||||
let table = "filter";
|
||||
let chain = get_chain(docker);
|
||||
let rule = format!("-p tcp --dport {} -s {} -j ACCEPT", port, ip);
|
||||
let rule = allow_ip_for_port_rule(port, ip);
|
||||
if let Some(position) = position {
|
||||
insert_unique(&iptables, table, &chain, &rule, position)
|
||||
} else {
|
||||
append_unique(&iptables, table, &chain, &rule)
|
||||
}
|
||||
|
||||
println!("allowed {} to access {}", ip, port);
|
||||
println!("Allowed {} to access {}", ip, port);
|
||||
}
|
||||
|
||||
pub fn remove_ip_port(ip: &str, port: u16, docker: bool) {
|
||||
pub fn remove_allow_ip_for_port(ip: &str, port: u16, docker: bool) {
|
||||
let iptables = iptables::new(false).unwrap();
|
||||
let _ = iptables.delete(
|
||||
"filter",
|
||||
&get_chain(docker),
|
||||
&format!("-p tcp --dport {} -s {} -j ACCEPT", port, ip),
|
||||
&allow_ip_for_port_rule(port, ip),
|
||||
);
|
||||
println!("removed access {} to {}", ip, port);
|
||||
println!("Removed access of {} to {}", ip, port);
|
||||
}
|
||||
|
||||
fn get_chain(docker: bool) -> String {
|
||||
@ -99,3 +150,42 @@ fn append_unique(iptables: &IPTables, table: &str, chain: &str, rule: &str) {
|
||||
fn insert_unique(iptables: &IPTables, table: &str, chain: &str, rule: &str, position: i32) {
|
||||
let _ = iptables.insert_unique(table, chain, rule, position);
|
||||
}
|
||||
|
||||
fn extract_ip(regex: &Regex, input: &str) -> Option<String> {
|
||||
regex
|
||||
.captures(input)
|
||||
.and_then(|caps| caps.get(0).map(|m| m.as_str().to_string()))
|
||||
}
|
||||
|
||||
fn extract_port(regex: &Regex, input: &str) -> Option<u16> {
|
||||
regex
|
||||
.captures(input)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().parse::<u16>().unwrap()))
|
||||
}
|
||||
|
||||
fn get_regex_for_ip() -> Regex {
|
||||
Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b").unwrap()
|
||||
}
|
||||
|
||||
fn get_regex_for_port() -> Regex {
|
||||
Regex::new(r"--dport\s+(\d+)").unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn correct_extract_ip() {
|
||||
let regex = get_regex_for_ip();
|
||||
let input = "-A INPUT -s 81.69.255.132/32 -j DROP";
|
||||
assert_eq!(extract_ip(®ex, input), Some("81.69.255.132".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_match_extrac_ip() {
|
||||
let regex = get_regex_for_ip();
|
||||
let input = "-A INPUT -j DROP";
|
||||
assert_eq!(extract_ip(®ex, input), None);
|
||||
}
|
||||
}
|
||||
|
123
src/main.rs
123
src/main.rs
@ -3,53 +3,72 @@ pub mod iptables_save;
|
||||
pub mod iptables_wrapper;
|
||||
pub mod login_attempt;
|
||||
|
||||
use cli::Arguments;
|
||||
use cli::Cli;
|
||||
use iptables_wrapper::{
|
||||
allow_ip_port, ban_port, is_port_secured, list_banned_ips, remove_ip_port, unban_port,
|
||||
allow_ip_for_port, is_port_secured, list_banned_ips, list_secured_ports,
|
||||
map_secured_ports_allowed_ips, remove_allow_ip_for_port, secure_port, unsecure_port,
|
||||
};
|
||||
use linemux::MuxedLines;
|
||||
use login_attempt::LoginAttempt;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::thread::spawn;
|
||||
use std::{collections::HashMap, thread::sleep, time::Duration};
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let opts = Arguments::from_args();
|
||||
match opts {
|
||||
Arguments::BanServer {
|
||||
match Cli::from_args() {
|
||||
Cli::BanServer {
|
||||
ssh_auth_log,
|
||||
iptables_save,
|
||||
} => {
|
||||
let _ = start_ban_server(ssh_auth_log, iptables_save).await;
|
||||
}
|
||||
Arguments::ListBannedIps => {
|
||||
for ip in list_banned_ips() {
|
||||
println!("{}", ip);
|
||||
}
|
||||
Cli::ListBannedIps => {
|
||||
println!("{}", serde_json::to_string(&list_banned_ips()).unwrap());
|
||||
}
|
||||
Arguments::IsPortSecured { port, docker } => {
|
||||
let is_secured = is_port_secured(port, docker);
|
||||
println!("{}", is_secured);
|
||||
Cli::ListSecuredPorts { docker } => {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&list_secured_ports(docker)).unwrap()
|
||||
);
|
||||
}
|
||||
Arguments::BanPort {
|
||||
Cli::MapSecuredPortsAllowedIps { docker } => {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&map_secured_ports_allowed_ips(docker)).unwrap()
|
||||
);
|
||||
}
|
||||
Cli::IsPortSecured { port, docker } => {
|
||||
println!("{}", is_port_secured(port, docker));
|
||||
}
|
||||
Cli::SecurePort {
|
||||
port,
|
||||
docker,
|
||||
position,
|
||||
} => ban_port(port, docker, position),
|
||||
Arguments::UnbanPort { port, docker } => unban_port(port, docker),
|
||||
Arguments::AllowIpPort {
|
||||
} => secure_port(port, docker, position),
|
||||
Cli::UnsecurePort { port, docker } => unsecure_port(port, docker),
|
||||
Cli::AllowIpForPort {
|
||||
ip,
|
||||
port,
|
||||
docker,
|
||||
position,
|
||||
} => allow_ip_port(&ip, port, docker, position),
|
||||
Arguments::OnlyIpPort { ip, port, docker } => {
|
||||
allow_ip_port(&ip, port, docker, Some(1));
|
||||
ban_port(port, docker, Some(2));
|
||||
} => allow_ip_for_port(&ip, port, docker, position),
|
||||
Cli::OnlyIpForPort { ip, port, docker } => {
|
||||
allow_ip_for_port(&ip, port, docker, Some(1));
|
||||
secure_port(port, docker, Some(2));
|
||||
}
|
||||
Cli::RemoveAllowIpPort { ip, port, docker } => remove_allow_ip_for_port(&ip, port, docker),
|
||||
Cli::SaveIPTables { iptables_save } => {
|
||||
let path = if let Some(iptables_save) = iptables_save {
|
||||
iptables_save
|
||||
} else {
|
||||
PathBuf::from("/etc/iptables/rules.v4")
|
||||
};
|
||||
|
||||
iptables_save::save_iptables(&path);
|
||||
println!("Saved IPTables to {}", path.display());
|
||||
}
|
||||
Arguments::RemoveIpPort { ip, port, docker } => remove_ip_port(&ip, port, docker),
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,47 +84,51 @@ async fn start_ban_server(
|
||||
if let Some(iptables_save) = iptables_save {
|
||||
let seconds_iptables = Duration::from_secs(60);
|
||||
println!(
|
||||
"starting iptables-save, save every {} seconds",
|
||||
"Saving IPTables every {} seconds",
|
||||
seconds_iptables.as_secs()
|
||||
);
|
||||
thread::spawn(move || loop {
|
||||
|
||||
spawn(move || loop {
|
||||
sleep(seconds_iptables);
|
||||
iptables_save::save_iptables(&iptables_save);
|
||||
});
|
||||
}
|
||||
|
||||
println!("listening to changes over {}", ssh_auth_log.display());
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
if let Some(login_attempt) = LoginAttempt::capture(line.line()) {
|
||||
println!(
|
||||
"failed login attempt from {}@{}:{}",
|
||||
login_attempt.user, login_attempt.ip, login_attempt.port
|
||||
);
|
||||
match login_attempts.get_mut(&login_attempt.ip) {
|
||||
Some(count) => {
|
||||
*count += 1;
|
||||
println!("Listeging to changer over file: {}", ssh_auth_log.display());
|
||||
loop {
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
if let Some(login_attempt) = LoginAttempt::capture(line.line()) {
|
||||
println!(
|
||||
"Failed login attempt from {}@{}:{}",
|
||||
login_attempt.user, login_attempt.ip, login_attempt.port
|
||||
);
|
||||
|
||||
if *count == 3 {
|
||||
match iptables.append_unique(
|
||||
"filter",
|
||||
"INPUT",
|
||||
&format!("--source {} -j DROP", login_attempt.ip),
|
||||
) {
|
||||
Ok(_) => {
|
||||
println!("{} banned", login_attempt.ip);
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{} already banned", login_attempt.ip);
|
||||
match login_attempts.get_mut(&login_attempt.ip) {
|
||||
Some(count) => {
|
||||
*count += 1;
|
||||
|
||||
if *count == 3 {
|
||||
if iptables
|
||||
.append_unique(
|
||||
"filter",
|
||||
"INPUT",
|
||||
&format!("--source {} -j DROP", login_attempt.ip),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
println!("IP {} banned", login_attempt.ip);
|
||||
} else {
|
||||
println!("IP {} already banned", login_attempt.ip);
|
||||
}
|
||||
|
||||
login_attempts.remove(&login_attempt.ip);
|
||||
}
|
||||
login_attempts.remove(&login_attempt.ip);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
login_attempts.insert(login_attempt.ip.clone(), 1);
|
||||
None => {
|
||||
login_attempts.insert(login_attempt.ip.clone(), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
Reference in New Issue
Block a user