mirror of https://github.com/telemt/telemt.git
Enhance UserConnectionReservation: add runtime handle for cross-thread IP cleanup and implement tests for user expiration and connection limits
This commit is contained in:
parent
0284b9f9e3
commit
2c06288b40
|
|
@ -31,16 +31,19 @@ struct UserConnectionReservation {
|
||||||
user: String,
|
user: String,
|
||||||
ip: IpAddr,
|
ip: IpAddr,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
runtime_handle: Option<tokio::runtime::Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserConnectionReservation {
|
impl UserConnectionReservation {
|
||||||
fn new(stats: Arc<Stats>, ip_tracker: Arc<UserIpTracker>, user: String, ip: IpAddr) -> Self {
|
fn new(stats: Arc<Stats>, ip_tracker: Arc<UserIpTracker>, user: String, ip: IpAddr) -> Self {
|
||||||
|
let runtime_handle = tokio::runtime::Handle::try_current().ok();
|
||||||
Self {
|
Self {
|
||||||
stats,
|
stats,
|
||||||
ip_tracker,
|
ip_tracker,
|
||||||
user,
|
user,
|
||||||
ip,
|
ip,
|
||||||
active: true,
|
active: true,
|
||||||
|
runtime_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +65,15 @@ impl Drop for UserConnectionReservation {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
self.stats.decrement_user_curr_connects(&self.user);
|
self.stats.decrement_user_curr_connects(&self.user);
|
||||||
|
|
||||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
if let Some(handle) = &self.runtime_handle {
|
||||||
|
let ip_tracker = self.ip_tracker.clone();
|
||||||
|
let user = self.user.clone();
|
||||||
|
let ip = self.ip;
|
||||||
|
let handle = handle.clone();
|
||||||
|
handle.spawn(async move {
|
||||||
|
ip_tracker.remove_ip(&user, ip).await;
|
||||||
|
});
|
||||||
|
} else if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||||
let ip_tracker = self.ip_tracker.clone();
|
let ip_tracker = self.ip_tracker.clone();
|
||||||
let user = self.user.clone();
|
let user = self.user.clone();
|
||||||
let ip = self.ip;
|
let ip = self.ip;
|
||||||
|
|
|
||||||
|
|
@ -1888,6 +1888,227 @@ async fn quota_rejection_does_not_reserve_ip_or_trigger_rollback() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn expired_user_rejection_does_not_reserve_ip_or_increment_curr_connects() {
|
||||||
|
let mut config = ProxyConfig::default();
|
||||||
|
config
|
||||||
|
.access
|
||||||
|
.user_expirations
|
||||||
|
.insert("user".to_string(), chrono::Utc::now() - chrono::Duration::seconds(1));
|
||||||
|
|
||||||
|
let stats = Stats::new();
|
||||||
|
let ip_tracker = UserIpTracker::new();
|
||||||
|
let peer_addr: SocketAddr = "203.0.113.212:50002".parse().unwrap();
|
||||||
|
|
||||||
|
let result = RunningClientHandler::check_user_limits_static(
|
||||||
|
"user",
|
||||||
|
&config,
|
||||||
|
&stats,
|
||||||
|
peer_addr,
|
||||||
|
&ip_tracker,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(ProxyError::UserExpired { user }) if user == "user"
|
||||||
|
));
|
||||||
|
assert_eq!(stats.get_user_curr_connects("user"), 0);
|
||||||
|
assert_eq!(ip_tracker.get_active_ip_count("user").await, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn same_ip_second_reservation_succeeds_under_unique_ip_limit_one() {
|
||||||
|
let user = "same-ip-unique-limit-user";
|
||||||
|
let peer_addr: SocketAddr = "198.51.100.248:50010".parse().unwrap();
|
||||||
|
|
||||||
|
let mut config = ProxyConfig::default();
|
||||||
|
config
|
||||||
|
.access
|
||||||
|
.user_max_tcp_conns
|
||||||
|
.insert(user.to_string(), 8);
|
||||||
|
|
||||||
|
let stats = Arc::new(Stats::new());
|
||||||
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
ip_tracker.set_user_limit(user, 1).await;
|
||||||
|
|
||||||
|
let first = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
|
user,
|
||||||
|
&config,
|
||||||
|
stats.clone(),
|
||||||
|
peer_addr,
|
||||||
|
ip_tracker.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("first reservation must succeed");
|
||||||
|
let second = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
|
user,
|
||||||
|
&config,
|
||||||
|
stats.clone(),
|
||||||
|
peer_addr,
|
||||||
|
ip_tracker.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("second reservation from same IP must succeed under unique-ip limit=1");
|
||||||
|
|
||||||
|
assert_eq!(stats.get_user_curr_connects(user), 2);
|
||||||
|
assert_eq!(ip_tracker.get_active_ip_count(user).await, 1);
|
||||||
|
|
||||||
|
first.release().await;
|
||||||
|
second.release().await;
|
||||||
|
assert_eq!(stats.get_user_curr_connects(user), 0);
|
||||||
|
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn second_distinct_ip_is_rejected_under_unique_ip_limit_one() {
|
||||||
|
let user = "distinct-ip-unique-limit-user";
|
||||||
|
let peer1: SocketAddr = "198.51.100.249:50011".parse().unwrap();
|
||||||
|
let peer2: SocketAddr = "198.51.100.250:50012".parse().unwrap();
|
||||||
|
|
||||||
|
let mut config = ProxyConfig::default();
|
||||||
|
config
|
||||||
|
.access
|
||||||
|
.user_max_tcp_conns
|
||||||
|
.insert(user.to_string(), 8);
|
||||||
|
|
||||||
|
let stats = Arc::new(Stats::new());
|
||||||
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
ip_tracker.set_user_limit(user, 1).await;
|
||||||
|
|
||||||
|
let first = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
|
user,
|
||||||
|
&config,
|
||||||
|
stats.clone(),
|
||||||
|
peer1,
|
||||||
|
ip_tracker.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("first reservation must succeed");
|
||||||
|
|
||||||
|
let second = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
|
user,
|
||||||
|
&config,
|
||||||
|
stats.clone(),
|
||||||
|
peer2,
|
||||||
|
ip_tracker.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
second,
|
||||||
|
Err(ProxyError::ConnectionLimitExceeded { user }) if user == "distinct-ip-unique-limit-user"
|
||||||
|
));
|
||||||
|
assert_eq!(stats.get_user_curr_connects(user), 1);
|
||||||
|
assert_eq!(ip_tracker.get_active_ip_count(user).await, 1);
|
||||||
|
|
||||||
|
first.release().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cross_thread_drop_uses_captured_runtime_for_ip_cleanup() {
|
||||||
|
let user = "cross-thread-drop-user";
|
||||||
|
let peer_addr: SocketAddr = "198.51.100.251:50013".parse().unwrap();
|
||||||
|
|
||||||
|
let mut config = ProxyConfig::default();
|
||||||
|
config
|
||||||
|
.access
|
||||||
|
.user_max_tcp_conns
|
||||||
|
.insert(user.to_string(), 8);
|
||||||
|
|
||||||
|
let stats = Arc::new(Stats::new());
|
||||||
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
|
||||||
|
let reservation = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
|
user,
|
||||||
|
&config,
|
||||||
|
stats.clone(),
|
||||||
|
peer_addr,
|
||||||
|
ip_tracker.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("reservation acquisition must succeed");
|
||||||
|
|
||||||
|
assert_eq!(stats.get_user_curr_connects(user), 1);
|
||||||
|
assert_eq!(ip_tracker.get_active_ip_count(user).await, 1);
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
drop(reservation);
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.expect("drop thread must not panic");
|
||||||
|
|
||||||
|
tokio::time::timeout(Duration::from_secs(1), async {
|
||||||
|
loop {
|
||||||
|
if stats.get_user_curr_connects(user) == 0
|
||||||
|
&& ip_tracker.get_active_ip_count(user).await == 0
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("cross-thread drop must still converge to zero user and IP footprint");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn immediate_reacquire_after_cross_thread_drop_succeeds() {
|
||||||
|
let user = "cross-thread-reacquire-user";
|
||||||
|
let peer_addr: SocketAddr = "198.51.100.252:50014".parse().unwrap();
|
||||||
|
|
||||||
|
let mut config = ProxyConfig::default();
|
||||||
|
config
|
||||||
|
.access
|
||||||
|
.user_max_tcp_conns
|
||||||
|
.insert(user.to_string(), 1);
|
||||||
|
|
||||||
|
let stats = Arc::new(Stats::new());
|
||||||
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
|
||||||
|
let reservation = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
|
user,
|
||||||
|
&config,
|
||||||
|
stats.clone(),
|
||||||
|
peer_addr,
|
||||||
|
ip_tracker.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("initial reservation must succeed");
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
drop(reservation);
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.expect("drop thread must not panic");
|
||||||
|
|
||||||
|
tokio::time::timeout(Duration::from_secs(1), async {
|
||||||
|
loop {
|
||||||
|
if stats.get_user_curr_connects(user) == 0
|
||||||
|
&& ip_tracker.get_active_ip_count(user).await == 0
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("cross-thread cleanup must settle before reacquire check");
|
||||||
|
|
||||||
|
let reacquire = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
|
user,
|
||||||
|
&config,
|
||||||
|
stats,
|
||||||
|
peer_addr,
|
||||||
|
ip_tracker,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
reacquire.is_ok(),
|
||||||
|
"reacquire must succeed after cross-thread drop cleanup"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn concurrent_limit_rejections_from_mixed_ips_leave_no_ip_footprint() {
|
async fn concurrent_limit_rejections_from_mixed_ips_leave_no_ip_footprint() {
|
||||||
const PARALLEL_IPS: usize = 64;
|
const PARALLEL_IPS: usize = 64;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue