use std::net::{IpAddr, SocketAddr};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Quiet = 0,
Fatal,
Error,
Info,
Verbose,
Debug1,
Debug2,
Debug3,
}
impl Level {
pub fn parse(s: &str) -> Option<Level> {
Some(match s.to_ascii_uppercase().as_str() {
"QUIET" => Level::Quiet,
"FATAL" => Level::Fatal,
"ERROR" => Level::Error,
"INFO" => Level::Info,
"VERBOSE" => Level::Verbose,
"DEBUG" | "DEBUG1" => Level::Debug1,
"DEBUG2" => Level::Debug2,
"DEBUG3" => Level::Debug3,
_ => return None,
})
}
fn from_u8(v: u8) -> Level {
[
Level::Quiet,
Level::Fatal,
Level::Error,
Level::Info,
Level::Verbose,
Level::Debug1,
Level::Debug2,
Level::Debug3,
][usize::from(v.min(7))]
}
}
static LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
static MAX_AUTH_TRIES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(6);
pub fn set_max_auth_tries(n: u32) {
MAX_AUTH_TRIES.store(n, Ordering::Relaxed);
}
pub fn failure_is_loud(c: &Conn, method: &str) -> bool {
!c.invalid_prefix().is_empty()
|| method == "password"
|| c.failures >= MAX_AUTH_TRIES.load(Ordering::Relaxed) / 2
}
static TO_STDERR: AtomicBool = AtomicBool::new(true);
pub fn facility(name: &str) -> Option<libc::c_int> {
Some(match name.to_ascii_uppercase().as_str() {
"DAEMON" => libc::LOG_DAEMON,
"USER" => libc::LOG_USER,
"AUTH" => libc::LOG_AUTH,
#[cfg(not(target_os = "solaris"))]
"AUTHPRIV" => libc::LOG_AUTHPRIV,
"LOCAL0" => libc::LOG_LOCAL0,
"LOCAL1" => libc::LOG_LOCAL1,
"LOCAL2" => libc::LOG_LOCAL2,
"LOCAL3" => libc::LOG_LOCAL3,
"LOCAL4" => libc::LOG_LOCAL4,
"LOCAL5" => libc::LOG_LOCAL5,
"LOCAL6" => libc::LOG_LOCAL6,
"LOCAL7" => libc::LOG_LOCAL7,
_ => return None,
})
}
pub fn init(level: Level, facility: libc::c_int, to_stderr: bool) {
LEVEL.store(level as u8, Ordering::Relaxed);
TO_STDERR.store(to_stderr, Ordering::Relaxed);
if !to_stderr {
unsafe { libc::openlog(c"sshd".as_ptr(), libc::LOG_PID | libc::LOG_NDELAY, facility) };
}
}
pub fn enabled(level: Level) -> bool {
level <= Level::from_u8(LEVEL.load(Ordering::Relaxed))
}
pub fn log(level: Level, msg: &str) {
if level == Level::Quiet || !enabled(level) {
return;
}
let msg: String = msg
.chars()
.map(|c| if c.is_control() { '?' } else { c })
.collect();
let stderr = TO_STDERR.load(Ordering::Relaxed);
let prefix = match level {
Level::Fatal if !stderr => "fatal: ",
Level::Error if !stderr => "error: ",
Level::Debug1 => "debug1: ",
Level::Debug2 => "debug2: ",
Level::Debug3 => "debug3: ",
_ => "",
};
if stderr {
eprintln!("{prefix}{msg}");
return;
}
let priority = match level {
Level::Fatal => libc::LOG_CRIT,
Level::Error => libc::LOG_ERR,
Level::Info | Level::Verbose => libc::LOG_INFO,
_ => libc::LOG_DEBUG,
};
if let Ok(line) = std::ffi::CString::new(format!("{prefix}{msg}")) {
unsafe { libc::syslog(priority, c"%s".as_ptr(), line.as_ptr()) };
}
}
pub fn info(msg: &str) {
log(Level::Info, msg);
}
pub fn verbose(msg: &str) {
log(Level::Verbose, msg);
}
pub fn error(msg: &str) {
log(Level::Error, msg);
}
pub fn addr_str(ip: IpAddr) -> String {
match ip {
IpAddr::V6(v6) => match v6.to_ipv4_mapped() {
Some(v4) => v4.to_string(),
None => v6.to_string(),
},
IpAddr::V4(v4) => v4.to_string(),
}
}
#[derive(Default)]
pub struct Conn {
ip: String,
port: u16,
user: Option<(String, bool)>,
authenticated: bool,
pub failures: u32,
said: Vec<&'static str>,
}
static CONN: Mutex<Option<Conn>> = Mutex::new(None);
pub fn with_conn<R>(f: impl FnOnce(&mut Conn) -> R) -> Option<R> {
CONN.lock().ok()?.as_mut().map(f)
}
impl Conn {
pub fn peer(&self) -> String {
format!("{} port {}", self.ip, self.port)
}
pub fn invalid_prefix(&self) -> &'static str {
match self.user {
Some((_, false)) => "invalid user ",
_ => "",
}
}
pub fn first(&mut self, key: &'static str) -> bool {
if self.said.contains(&key) {
return false;
}
self.said.push(key);
true
}
fn who(&self) -> String {
match &self.user {
Some((u, true)) if self.authenticated => format!("user {u} "),
Some((u, true)) => format!("authenticating user {u} "),
Some((u, false)) => format!("invalid user {u} "),
None => String::new(),
}
}
}
pub fn conn_begin(peer: SocketAddr, local: Option<SocketAddr>) {
let c = Conn {
ip: addr_str(peer.ip()),
port: peer.port(),
..Conn::default()
};
let (lip, lport) = local.map_or((String::new(), 0), |l| (addr_str(l.ip()), l.port()));
verbose(&format!(
"Connection from {} on {lip} port {lport} rdomain \"\"",
c.peer()
));
if let Ok(mut g) = CONN.lock() {
*g = Some(c);
}
}
pub fn conn_user(user: &str, valid: bool) {
with_conn(|c| {
if c.user.is_none() {
c.user = Some((user.to_string(), valid));
if !valid {
info(&format!("Invalid user {user} from {}", c.peer()));
}
}
});
}
pub fn conn_authenticated() {
with_conn(|c| c.authenticated = true);
}
pub enum End<'a> {
Closed,
PeerDisconnect { reason: u32, description: &'a str },
GraceTimeout,
AuthFailed,
NoMatch(String),
Error(String),
}
pub fn conn_end(end: End<'_>) {
let Some(c) = CONN.lock().ok().and_then(|mut g| g.take()) else {
return;
};
let preauth = if c.authenticated { "" } else { " [preauth]" };
let peer = c.peer();
let who = c.who();
match end {
End::GraceTimeout if !c.authenticated => {
log(
Level::Fatal,
&format!("Timeout before authentication for {peer}"),
);
}
End::AuthFailed if c.failures > 0 => {
let (user, inv) = match &c.user {
Some((u, valid)) => (u.as_str(), if *valid { "" } else { "invalid user " }),
None => ("", ""),
};
error(&format!(
"maximum authentication attempts exceeded for {inv}{user} from {peer} ssh2{preauth}"
));
info(&format!(
"Disconnecting {who}{peer}: Too many authentication failures{preauth}"
));
}
End::PeerDisconnect {
reason,
description,
} => {
info(&format!(
"Received disconnect from {peer}:{reason}: {description}{preauth}"
));
info(&format!("Disconnected from {who}{peer}{preauth}"));
}
End::NoMatch(m) => info(&format!("Unable to negotiate with {peer}: {m}{preauth}")),
End::Error(e) if !c.authenticated => {
info(&format!("Connection closed by {who}{peer}: {e}{preauth}"));
}
End::Error(e) => {
error(&e);
info(&format!("Disconnected from {who}{peer}"));
}
_ if c.authenticated => info(&format!("Disconnected from {who}{peer}")),
_ => info(&format!("Connection closed by {who}{peer}{preauth}")),
}
}