diff --git a/g3proxy/src/auth/cache.rs b/g3proxy/src/auth/cache.rs new file mode 100644 index 00000000..f32bdbec --- /dev/null +++ b/g3proxy/src/auth/cache.rs @@ -0,0 +1,91 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap}; +use std::num::NonZeroUsize; +use std::time::Duration; + +use foldhash::fast::FixedState; +use lru::LruCache; +use tokio::time::Instant; + +use g3_types::metrics::NodeName; + +thread_local! { + static CACHE: RefCell> = const { + RefCell::new(HashMap::with_hasher(FixedState::with_seed(0))) + }; +} + +#[derive(Default)] +struct UserLocalCache { + password_map: BTreeMap, +} + +struct GroupLocalCache { + user_map: LruCache, +} + +impl Default for GroupLocalCache { + fn default() -> Self { + GroupLocalCache::new(crate::config::auth::group::DEFAULT_CACHE_USER_COUNT) + } +} + +impl GroupLocalCache { + fn new(user_count: NonZeroUsize) -> Self { + GroupLocalCache { + user_map: LruCache::with_hasher(user_count, FixedState::with_seed(0)), + } + } +} + +pub(super) fn has_valid_password(group: &NodeName, username: &str, password: &str) -> bool { + CACHE.with(|cache| { + let mut cache = cache.borrow_mut(); + let group = cache + .entry(group.clone()) + .or_insert_with(GroupLocalCache::default); + + let Some(user) = group.user_map.get_mut(username) else { + return false; + }; + + let Some((p, t)) = user.password_map.remove_entry(password) else { + return false; + }; + + if t > Instant::now() { + user.password_map.insert(p, t); + true + } else { + false + } + }) +} + +pub(super) fn save_user_password( + group: &NodeName, + user_count: NonZeroUsize, + username: String, + password: String, + expire_time: Duration, +) { + CACHE.with(|cache| { + let mut cache = cache.borrow_mut(); + let group = cache + .entry(group.clone()) + .and_modify(|g| g.user_map.resize(user_count)) + .or_insert_with(|| GroupLocalCache::new(user_count)); + + let user = group + .user_map + .get_or_insert_mut(username, UserLocalCache::default); + + user.password_map + .insert(password, Instant::now() + expire_time); + }) +} diff --git a/g3proxy/src/auth/group/ldap/pool/mod.rs b/g3proxy/src/auth/group/ldap/pool/mod.rs index 5e4b756b..a1e0ffbe 100644 --- a/g3proxy/src/auth/group/ldap/pool/mod.rs +++ b/g3proxy/src/auth/group/ldap/pool/mod.rs @@ -11,7 +11,7 @@ use tokio::sync::{mpsc, oneshot}; use g3_types::auth::UserAuthError; -use crate::config::auth::LdapUserGroupConfig; +use crate::config::auth::{LdapUserGroupConfig, UserGroupConfig}; mod connect; use connect::LdapConnector; @@ -51,6 +51,14 @@ impl LdapAuthPoolHandle { username: &str, password: &str, ) -> Result<(), UserAuthError> { + if crate::auth::cache::has_valid_password( + self.config.basic_config().name(), + username, + password, + ) { + return Ok(()); + } + let (sender, receiver) = oneshot::channel(); let req = LdapAuthRequest { username: username.to_string(), @@ -65,7 +73,16 @@ impl LdapAuthPoolHandle { let _ = self.req_sender.send(req).await; match tokio::time::timeout(self.config.queue_wait_timeout, receiver).await { - Ok(Ok(Some(_))) => Ok(()), + Ok(Ok(Some((username, password)))) => { + crate::auth::cache::save_user_password( + self.config.basic_config().name(), + self.config.cache_user_count, + username, + password, + self.config.cache_expire_time, + ); + Ok(()) + } Ok(Ok(None)) => Err(UserAuthError::TokenNotMatch), Ok(Err(_)) => Err(UserAuthError::RemoteError), Err(_) => Err(UserAuthError::RemoteTimeout), diff --git a/g3proxy/src/auth/mod.rs b/g3proxy/src/auth/mod.rs index 4449002d..5b4d8ba4 100644 --- a/g3proxy/src/auth/mod.rs +++ b/g3proxy/src/auth/mod.rs @@ -10,6 +10,8 @@ pub(crate) use ops::reload; mod registry; pub(crate) use registry::{get_all_groups, get_names, get_or_insert_default}; +mod cache; + mod site; pub(crate) use site::UserSite; use site::UserSites; diff --git a/g3proxy/src/config/auth/group/ldap.rs b/g3proxy/src/config/auth/group/ldap.rs index f7b07ad0..636e2948 100644 --- a/g3proxy/src/config/auth/group/ldap.rs +++ b/g3proxy/src/config/auth/group/ldap.rs @@ -3,6 +3,7 @@ * Copyright 2026 G3-OSS developers. */ +use std::num::NonZeroUsize; use std::sync::Arc; use std::time::Duration; @@ -34,6 +35,8 @@ pub(crate) struct LdapUserGroupConfig { pub(crate) connection_pool: ConnectionPoolConfig, pub(crate) queue_channel_size: usize, pub(crate) queue_wait_timeout: Duration, + pub(crate) cache_user_count: NonZeroUsize, + pub(crate) cache_expire_time: Duration, } impl LdapUserGroupConfig { @@ -53,6 +56,8 @@ impl LdapUserGroupConfig { connection_pool: ConnectionPoolConfig::new(1024, 8), queue_channel_size: 64, queue_wait_timeout: Duration::from_secs(4), + cache_user_count: super::DEFAULT_CACHE_USER_COUNT, + cache_expire_time: super::DEFAULT_CACHE_EXPIRE_TIME, } } @@ -169,6 +174,15 @@ impl LdapUserGroupConfig { .context(format!("invalid humanize duration value for key {k}"))?; Ok(()) } + "cache_user_count" => { + self.cache_user_count = g3_yaml::value::as_nonzero_usize(v)?; + Ok(()) + } + "cache_expire_time" => { + self.cache_expire_time = g3_yaml::humanize::as_duration(v) + .context(format!("invalid humanize duration value for key {k}"))?; + Ok(()) + } _ => self.basic.set(k, v), } } diff --git a/g3proxy/src/config/auth/group/mod.rs b/g3proxy/src/config/auth/group/mod.rs index fbfd2995..93d2a9ee 100644 --- a/g3proxy/src/config/auth/group/mod.rs +++ b/g3proxy/src/config/auth/group/mod.rs @@ -3,6 +3,9 @@ * Copyright 2026 G3-OSS developers. */ +use std::num::NonZeroUsize; +use std::time::Duration; + use g3_macros::AnyConfig; use g3_types::metrics::NodeName; use g3_yaml::YamlDocPosition; @@ -16,6 +19,9 @@ pub(crate) use facts::FactsUserGroupConfig; mod ldap; pub(crate) use ldap::LdapUserGroupConfig; +pub(crate) const DEFAULT_CACHE_USER_COUNT: NonZeroUsize = NonZeroUsize::new(128).unwrap(); +const DEFAULT_CACHE_EXPIRE_TIME: Duration = Duration::from_secs(300); + pub(crate) trait UserGroupConfig { fn basic_config(&self) -> &BasicUserGroupConfig; diff --git a/sphinx/g3proxy/configuration/auth/group/ldap.rst b/sphinx/g3proxy/configuration/auth/group/ldap.rst index 219593ac..29cd0180 100644 --- a/sphinx/g3proxy/configuration/auth/group/ldap.rst +++ b/sphinx/g3proxy/configuration/auth/group/ldap.rst @@ -126,3 +126,21 @@ queue_wait_timeout Set the timeout value when auth with the LDAP server for a client request. **default**: 4s + +cache_user_count +---------------- + +**optional**, **type**: usize + +Set how many users will be LRU cached in thread local storage. + +**default**: 128 + +cache_expire_time +----------------- + +**optional**, **type**: :ref:`humanize duration ` + +Set the expire time for valid passwords in the thread local LRU cache. + +**default**: 5min