use std::fmt::Display;
pub const TARGET: &str = "hypershunt::security";
pub fn auth_failure(
peer: impl Display,
method: impl Display,
path: &str,
host: &str,
) {
tracing::warn!(
target: TARGET,
peer = %peer, method = %method,
path, host,
"auth-failure"
);
}
pub fn auth_challenge(
peer: impl Display,
method: impl Display,
path: &str,
host: &str,
) {
tracing::info!(
target: TARGET,
peer = %peer, method = %method,
path, host,
"auth-challenge"
);
}
pub fn access_denied(
peer: impl Display,
method: impl Display,
status: u16,
path: &str,
host: &str,
) {
tracing::warn!(
target: TARGET,
peer = %peer, method = %method, status,
path, host,
"access-denied"
);
}
pub fn rate_limited(peer: impl Display, rule: impl Display, retry_after: u64) {
tracing::warn!(
target: TARGET,
peer = %peer, rule = %rule, retry_after,
"rate-limited"
);
}
pub fn access_denied_l4(peer: impl Display) {
tracing::warn!(
target: TARGET,
peer = %peer, proto = "tcp",
"access-denied"
);
}
pub fn bad_client_cert(peer: impl Display, reason: &'static str) {
tracing::warn!(
target: TARGET,
peer = %peer, reason = %reason,
"bad-client-cert"
);
}
pub fn client_cert_rejection(e: &std::io::Error) -> Option<&'static str> {
use rustls::{CertificateError, Error as TlsError};
let tls = e.get_ref()?.downcast_ref::<TlsError>()?;
match tls {
TlsError::NoCertificatesPresented => Some("no-cert"),
TlsError::InvalidCertificate(cert) => Some(match cert {
CertificateError::Expired => "expired",
CertificateError::Revoked => "revoked",
CertificateError::UnknownIssuer => "untrusted",
CertificateError::BadSignature => "bad-signature",
CertificateError::NotValidForName => "name-mismatch",
_ => "invalid",
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_client_cert_rejections() {
use rustls::{CertificateError, Error as TlsError};
let wrap = |e: TlsError| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
};
assert_eq!(
client_cert_rejection(&wrap(TlsError::NoCertificatesPresented)),
Some("no-cert")
);
assert_eq!(
client_cert_rejection(&wrap(TlsError::InvalidCertificate(
CertificateError::Revoked
))),
Some("revoked")
);
assert_eq!(
client_cert_rejection(&wrap(TlsError::InvalidCertificate(
CertificateError::UnknownIssuer
))),
Some("untrusted")
);
}
#[test]
fn rendered_line_matches_filter_and_escapes_injection() {
use std::io::Write;
use std::sync::{Arc, Mutex};
use tracing_subscriber::fmt::MakeWriter;
#[derive(Clone)]
struct Buf(Arc<Mutex<Vec<u8>>>);
impl Write for Buf {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for Buf {
type Writer = Buf;
fn make_writer(&'a self) -> Buf {
self.clone()
}
}
let buf = Buf(Arc::new(Mutex::new(Vec::new())));
let subscriber = tracing_subscriber::fmt()
.with_ansi(false)
.with_max_level(tracing::Level::INFO)
.with_writer(buf.clone())
.finish();
tracing::subscriber::with_default(subscriber, || {
auth_failure(
"1.2.3.4:5678",
"GET",
"/admin\nWARN forged peer=9.9.9.9",
"example.com",
);
});
let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
assert!(
out.contains(
"hypershunt::security: auth-failure peer=1.2.3.4:5678"
),
"missing fail2ban anchor in: {out}"
);
assert!(
!out.contains("\nWARN forged"),
"injection not escaped in: {out}"
);
assert!(out.contains("\\nWARN forged"), "expected escaped \\n");
}
#[test]
fn ignores_benign_handshake_errors() {
use rustls::Error as TlsError;
assert_eq!(
client_cert_rejection(&std::io::Error::from(
std::io::ErrorKind::UnexpectedEof
)),
None
);
let alert = std::io::Error::new(
std::io::ErrorKind::InvalidData,
TlsError::AlertReceived(rustls::AlertDescription::BadCertificate),
);
assert_eq!(client_cert_rejection(&alert), None);
}
}