diff --git a/Cargo.lock b/Cargo.lock index fe306e47e..444741711 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -200,7 +200,7 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror 1.0.69", @@ -675,7 +675,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -1148,7 +1148,7 @@ checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ "asn1-rs", "displaydoc", - "nom", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", @@ -1213,7 +1213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9313f104b590510b46fc01c0a324fc76505c13871454d3c48490468d04c8d395" dependencies = [ "libc", - "nom", + "nom 7.1.3", ] [[package]] @@ -2271,6 +2271,18 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "haproxy-protocol" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61fc527a2f089b57ebc09301b6371bbbff4ce7b547306c17dfa55766655bec6" +dependencies = [ + "hex", + "nom 8.0.0", + "tokio", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3140,6 +3152,7 @@ dependencies = [ "filetime", "futures", "futures-util", + "haproxy-protocol", "hyper 1.6.0", "hyper-util", "kanidm_build_profiles", @@ -3311,7 +3324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" dependencies = [ "bytes", - "nom", + "nom 7.1.3", ] [[package]] @@ -3343,7 +3356,7 @@ dependencies = [ "base64 0.21.7", "bytes", "lber", - "nom", + "nom 7.1.3", "peg", "serde", "thiserror 1.0.69", @@ -3675,6 +3688,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nonempty" version = "0.8.1" @@ -4873,7 +4895,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -5364,7 +5386,7 @@ checksum = "34285eaade87ba166c4f17c0ae1e35d52659507db81888beae277e962b9e5a02" dependencies = [ "base64 0.21.7", "base64urlsafedata", - "nom", + "nom 7.1.3", "openssl", "serde", "serde_cbor_2", @@ -6341,7 +6363,7 @@ dependencies = [ "bitflags 1.3.2", "futures", "hex", - "nom", + "nom 7.1.3", "num-derive", "num-traits", "openssl", @@ -6386,7 +6408,7 @@ dependencies = [ "compact_jwt", "der-parser", "hex", - "nom", + "nom 7.1.3", "openssl", "rand 0.8.5", "rand_chacha 0.3.1", @@ -6888,7 +6910,7 @@ dependencies = [ "data-encoding", "der-parser", "lazy_static", - "nom", + "nom 7.1.3", "oid-registry", "rusticata-macros", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 400184969..b3cd6f960 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,6 +177,7 @@ fs4 = "^0.12.0" futures = "^0.3.31" futures-util = { version = "^0.3.30", features = ["sink"] } gix = { version = "0.64.0", default-features = false } +haproxy-protocol = { version = "0.0.1" } hashbrown = { version = "0.14.3", features = ["serde", "inline-more", "ahash"] } hex = "^0.4.3" http = "1.2.0" diff --git a/server/core/Cargo.toml b/server/core/Cargo.toml index 1c753cb44..f5be5b56a 100644 --- a/server/core/Cargo.toml +++ b/server/core/Cargo.toml @@ -34,6 +34,7 @@ cron = { workspace = true } filetime = { workspace = true } futures = { workspace = true } futures-util = { workspace = true } +haproxy-protocol = { workspace = true, features = ["tokio"] } hyper = { workspace = true } hyper-util = { workspace = true } kanidm_proto = { workspace = true } diff --git a/server/core/src/config.rs b/server/core/src/config.rs index 5ff4b9830..9e28a5189 100644 --- a/server/core/src/config.rs +++ b/server/core/src/config.rs @@ -109,7 +109,7 @@ pub enum LdapAddressInfo { } impl LdapAddressInfo { - pub fn proxy_v2(&self) -> bool { + pub fn is_proxy_v2(&self) -> bool { matches!(self, Self::ProxyV2) } } @@ -138,7 +138,7 @@ impl HttpAddressInfo { matches!(self, Self::XForwardFor) } - pub fn proxy_v2(&self) -> bool { + pub fn is_proxy_v2(&self) -> bool { matches!(self, Self::ProxyV2) } } diff --git a/server/core/src/https/extractors/mod.rs b/server/core/src/https/extractors/mod.rs index 4d3fd686f..f5ee76c2e 100644 --- a/server/core/src/https/extractors/mod.rs +++ b/server/core/src/https/extractors/mod.rs @@ -5,7 +5,6 @@ use axum::{ http::{ header::HeaderName, header::AUTHORIZATION as AUTHORISATION, request::Parts, StatusCode, }, - serve::IncomingStream, RequestPartsExt, }; @@ -40,7 +39,8 @@ impl FromRequestParts<ServerState> for TrustedClientIp { state: &ServerState, ) -> Result<Self, Self::Rejection> { let ConnectInfo(ClientConnInfo { - addr, + connection_addr: _, + client_addr, client_cert: _, }) = parts .extract::<ConnectInfo<ClientConnInfo>>() @@ -75,10 +75,13 @@ impl FromRequestParts<ServerState> for TrustedClientIp { ) })? } else { - addr.ip() + client_addr.ip() } } else { - addr.ip() + // This can either be the client_addr == connection_addr if there are + // no ip address trust sources, or this is the value as reported by haproxy + // proxy header. + client_addr.ip() }; Ok(TrustedClientIp(ip_addr)) @@ -97,7 +100,11 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation { parts: &mut Parts, state: &ServerState, ) -> Result<Self, Self::Rejection> { - let ConnectInfo(ClientConnInfo { addr, client_cert }) = parts + let ConnectInfo(ClientConnInfo { + connection_addr: _, + client_addr, + client_cert, + }) = parts .extract::<ConnectInfo<ClientConnInfo>>() .await .map_err(|_| { @@ -130,10 +137,10 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation { ) })? } else { - addr.ip() + client_addr.ip() } } else { - addr.ip() + client_addr.ip() }; let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) { @@ -201,30 +208,30 @@ impl FromRequestParts<ServerState> for DomainInfo { #[derive(Debug, Clone)] pub struct ClientConnInfo { - pub addr: SocketAddr, + /// This is the address that is *connected* to kanidm right now + /// for this operation. + #[allow(dead_code)] + pub connection_addr: SocketAddr, + /// This is the client address as reported by a remote IP source + /// such as x-forward-for or proxy-hdr + pub client_addr: SocketAddr, // Only set if the certificate is VALID pub client_cert: Option<ClientCertInfo>, } +// This is the normal way that our extractors get the ip info impl Connected<ClientConnInfo> for ClientConnInfo { fn connect_info(target: ClientConnInfo) -> Self { target } } +// This is only used for plaintext http - in other words, integration tests only. impl Connected<SocketAddr> for ClientConnInfo { - fn connect_info(addr: SocketAddr) -> Self { + fn connect_info(connection_addr: SocketAddr) -> Self { ClientConnInfo { - addr, - client_cert: None, - } - } -} - -impl Connected<IncomingStream<'_>> for ClientConnInfo { - fn connect_info(target: IncomingStream<'_>) -> Self { - ClientConnInfo { - addr: target.remote_addr(), + client_addr: connection_addr.clone(), + connection_addr, client_cert: None, } } diff --git a/server/core/src/https/mod.rs b/server/core/src/https/mod.rs index c0302c911..ed431e5cc 100644 --- a/server/core/src/https/mod.rs +++ b/server/core/src/https/mod.rs @@ -19,7 +19,6 @@ use self::javascript::*; use crate::actors::{QueryServerReadV1, QueryServerWriteV1}; use crate::config::{Configuration, ServerRole}; use crate::CoreAction; - use axum::{ body::Body, extract::connect_info::IntoMakeServiceWithConnectInfo, @@ -29,21 +28,23 @@ use axum::{ routing::*, Router, }; - use axum_extra::extract::cookie::CookieJar; use compact_jwt::{error::JwtError, JwsCompact, JwsHs256Signer, JwsVerifier}; use futures::pin_mut; +use haproxy_protocol::{ProxyHdrV2, RemoteAddress}; use hyper::body::Incoming; use hyper_util::rt::{TokioExecutor, TokioIo}; +use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate}; use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID}; use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor}; use openssl::ssl::{Ssl, SslAcceptor}; - -use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate}; - use serde::de::DeserializeOwned; use sketching::*; use std::fmt::Write; +use std::io::ErrorKind; +use std::path::PathBuf; +use std::pin::Pin; +use std::{net::SocketAddr, str::FromStr}; use tokio::{ net::{TcpListener, TcpStream}, sync::broadcast, @@ -56,11 +57,6 @@ use tower_http::{services::ServeDir, trace::TraceLayer}; use url::Url; use uuid::Uuid; -use std::io::ErrorKind; -use std::path::PathBuf; -use std::pin::Pin; -use std::{net::SocketAddr, str::FromStr}; - #[derive(Clone)] pub struct ServerState { pub(crate) status_ref: &'static StatusActor, @@ -212,6 +208,7 @@ pub async fn create_https_server( })?; let trust_x_forward_for = config.http_client_address_info.is_x_forward_for(); + let enable_haproxy_hdr = config.http_client_address_info.is_proxy_v2(); let origin = Url::parse(&config.origin) // Should be impossible! @@ -337,6 +334,7 @@ pub async fn create_https_server( rx, server_message_tx, tls_acceptor_reload_rx, + enable_haproxy_hdr, ))) } None => Ok(task::spawn(server_loop_plaintext(addr, app, rx))), @@ -350,6 +348,7 @@ async fn server_loop( mut rx: broadcast::Receiver<CoreAction>, server_message_tx: broadcast::Sender<CoreAction>, mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>, + enable_haproxy_hdr: bool, ) { pin_mut!(listener); @@ -365,7 +364,7 @@ async fn server_loop( Ok((stream, addr)) => { let tls_acceptor = tls_acceptor.clone(); let app = app.clone(); - task::spawn(handle_conn(tls_acceptor, stream, app, addr)); + task::spawn(handle_conn(tls_acceptor, stream, app, addr, enable_haproxy_hdr)); } Err(err) => { error!("Web server exited with {:?}", err); @@ -415,8 +414,36 @@ pub(crate) async fn handle_conn( acceptor: SslAcceptor, stream: TcpStream, mut app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>, - addr: SocketAddr, + connection_addr: SocketAddr, + enable_haproxy_hdr: bool, ) -> Result<(), std::io::Error> { + let (stream, client_addr) = if enable_haproxy_hdr { + match ProxyHdrV2::parse_from_read(stream).await { + Ok((stream, hdr)) => { + let remote_socket_addr = match hdr.to_remote_addr() { + RemoteAddress::Local => { + debug!("haproxy check - will not contain client data"); + return Err(std::io::Error::from(ErrorKind::ConnectionAborted)); + } + RemoteAddress::TcpV4 { src, dst: _ } => SocketAddr::from(src), + RemoteAddress::TcpV6 { src, dst: _ } => SocketAddr::from(src), + remote_addr => { + error!(?remote_addr, "remote address in proxy header is invalid"); + return Err(std::io::Error::from(ErrorKind::ConnectionAborted)); + } + }; + + (stream, remote_socket_addr) + } + Err(err) => { + error!(?err, "Unable to process proxy v2 header"); + return Err(std::io::Error::from(ErrorKind::ConnectionAborted)); + } + } + } else { + (stream, connection_addr.clone()) + }; + let ssl = Ssl::new(acceptor.context()).map_err(|e| { error!("Failed to create TLS context: {:?}", e); std::io::Error::from(ErrorKind::ConnectionAborted) @@ -459,7 +486,11 @@ pub(crate) async fn handle_conn( None }; - let client_conn_info = ClientConnInfo { addr, client_cert }; + let client_conn_info = ClientConnInfo { + connection_addr, + client_addr, + client_cert, + }; debug!(?client_conn_info); diff --git a/server/core/src/ldaps.rs b/server/core/src/ldaps.rs index ca57a7e1b..22ca7fce5 100644 --- a/server/core/src/ldaps.rs +++ b/server/core/src/ldaps.rs @@ -2,12 +2,13 @@ use crate::actors::QueryServerReadV1; use crate::CoreAction; use futures_util::sink::SinkExt; use futures_util::stream::StreamExt; +use haproxy_protocol::{ProxyHdrV2, RemoteAddress}; use kanidmd_lib::idm::ldap::{LdapBoundToken, LdapResponseState}; use kanidmd_lib::prelude::*; use ldap3_proto::proto::LdapMsg; use ldap3_proto::LdapCodec; use openssl::ssl::{Ssl, SslAcceptor}; -use std::net; +use std::net::SocketAddr; use std::pin::Pin; use std::str::FromStr; use tokio::io::{AsyncRead, AsyncWrite}; @@ -33,7 +34,7 @@ impl LdapSession { #[instrument(name = "ldap-request", skip(client_address, qe_r_ref))] async fn client_process_msg( uat: Option<LdapBoundToken>, - client_address: net::SocketAddr, + client_address: SocketAddr, protomsg: LdapMsg, qe_r_ref: &'static QueryServerReadV1, ) -> Option<LdapResponseState> { @@ -50,7 +51,8 @@ async fn client_process_msg( async fn client_process<STREAM>( stream: STREAM, - client_address: net::SocketAddr, + client_address: SocketAddr, + connection_address: SocketAddr, qe_r_ref: &'static QueryServerReadV1, ) where STREAM: AsyncRead + AsyncWrite, @@ -67,6 +69,8 @@ async fn client_process<STREAM>( let uat = session.uat.clone(); let caddr = client_address; + debug!(?client_address, ?connection_address); + match client_process_msg(uat, caddr, protomsg, qe_r_ref).await { // I'd really have liked to have put this near the [LdapResponseState::Bind] but due // to the handing of `audit` it isn't possible due to borrows, etc. @@ -112,28 +116,61 @@ async fn client_process<STREAM>( } async fn client_tls_accept( - tcpstream: TcpStream, + stream: TcpStream, tls_acceptor: SslAcceptor, - client_socket_addr: net::SocketAddr, + connection_addr: SocketAddr, qe_r_ref: &'static QueryServerReadV1, + enable_haproxy_hdr: bool, ) { + let (stream, client_addr) = if enable_haproxy_hdr { + match ProxyHdrV2::parse_from_read(stream).await { + Ok((stream, hdr)) => { + let remote_socket_addr = match hdr.to_remote_addr() { + RemoteAddress::Local => { + debug!("haproxy check - will not contain client data"); + return; + } + RemoteAddress::TcpV4 { src, dst: _ } => SocketAddr::from(src), + RemoteAddress::TcpV6 { src, dst: _ } => SocketAddr::from(src), + remote_addr => { + error!(?remote_addr, "remote address in proxy header is invalid"); + return; + } + }; + + (stream, remote_socket_addr) + } + Err(err) => { + error!(?err, "Unable to process proxy v2 header"); + return; + } + } + } else { + (stream, connection_addr.clone()) + }; + // Start the event // From the parameters we need to create an SslContext. let mut tlsstream = match Ssl::new(tls_acceptor.context()) - .and_then(|tls_obj| SslStream::new(tls_obj, tcpstream)) + .and_then(|tls_obj| SslStream::new(tls_obj, stream)) { Ok(ta) => ta, Err(err) => { - error!(?err, %client_socket_addr, "LDAP TLS setup error"); + error!(?err, %client_addr, %connection_addr, "LDAP TLS setup error"); return; } }; if let Err(err) = SslStream::accept(Pin::new(&mut tlsstream)).await { - error!(?err, %client_socket_addr, "LDAP TLS accept error"); + error!(?err, %client_addr, %connection_addr, "LDAP TLS accept error"); return; }; - tokio::spawn(client_process(tlsstream, client_socket_addr, qe_r_ref)); + tokio::spawn(client_process( + tlsstream, + client_addr, + connection_addr, + qe_r_ref, + )); } /// TLS LDAP Listener, hands off to [client_tls_accept] @@ -143,6 +180,7 @@ async fn ldap_tls_acceptor( qe_r_ref: &'static QueryServerReadV1, mut rx: broadcast::Receiver<CoreAction>, mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>, + enable_haproxy_hdr: bool, ) { loop { tokio::select! { @@ -155,7 +193,7 @@ async fn ldap_tls_acceptor( match accept_result { Ok((tcpstream, client_socket_addr)) => { let clone_tls_acceptor = tls_acceptor.clone(); - tokio::spawn(client_tls_accept(tcpstream, clone_tls_acceptor, client_socket_addr, qe_r_ref)); + tokio::spawn(client_tls_accept(tcpstream, clone_tls_acceptor, client_socket_addr, qe_r_ref, enable_haproxy_hdr)); } Err(err) => { warn!(?err, "LDAP acceptor error, continuing"); @@ -187,7 +225,7 @@ async fn ldap_plaintext_acceptor( accept_result = listener.accept() => { match accept_result { Ok((tcpstream, client_socket_addr)) => { - tokio::spawn(client_process(tcpstream, client_socket_addr, qe_r_ref)); + tokio::spawn(client_process(tcpstream, client_socket_addr.clone(), client_socket_addr, qe_r_ref)); } Err(e) => { error!("LDAP acceptor error, continuing -> {:?}", e); @@ -205,6 +243,7 @@ pub(crate) async fn create_ldap_server( qe_r_ref: &'static QueryServerReadV1, rx: broadcast::Receiver<CoreAction>, tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>, + enable_haproxy_hdr: bool, ) -> Result<tokio::task::JoinHandle<()>, ()> { if address.starts_with(":::") { // takes :::xxxx to xxxx @@ -212,7 +251,7 @@ pub(crate) async fn create_ldap_server( error!("Address '{}' looks like an attempt to wildcard bind with IPv6 on port {} - please try using ldapbindaddress = '[::]:{}'", address, port, port); }; - let addr = net::SocketAddr::from_str(address).map_err(|e| { + let addr = SocketAddr::from_str(address).map_err(|e| { error!("Could not parse LDAP server address {} -> {:?}", address, e); })?; @@ -233,6 +272,7 @@ pub(crate) async fn create_ldap_server( qe_r_ref, rx, tls_acceptor_reload_rx, + enable_haproxy_hdr, )) } None => tokio::spawn(ldap_plaintext_acceptor(listener, qe_r_ref, rx)), diff --git a/server/core/src/lib.rs b/server/core/src/lib.rs index 1117f446a..18af12044 100644 --- a/server/core/src/lib.rs +++ b/server/core/src/lib.rs @@ -1087,6 +1087,7 @@ pub async fn create_server_core( server_read_ref, broadcast_tx.subscribe(), ldap_tls_acceptor_reload_rx, + config.ldap_client_address_info.is_proxy_v2(), ) .await?; Some(h)