142 lines
4.7 KiB
Rust
142 lines
4.7 KiB
Rust
use std::{net::SocketAddr, sync::Arc, thread, time::Duration};
|
|
|
|
use wgcloak::{
|
|
config::{self, LimitsConfig},
|
|
transport::{AdmissionController, AdmissionError},
|
|
};
|
|
|
|
fn limits(
|
|
max_sessions: usize,
|
|
max_sessions_per_ip: usize,
|
|
max_pending_handshakes: usize,
|
|
max_auth_attempts_per_ip: usize,
|
|
auth_attempt_window_secs: u64,
|
|
) -> LimitsConfig {
|
|
LimitsConfig {
|
|
max_sessions,
|
|
max_sessions_per_ip,
|
|
max_pending_handshakes,
|
|
max_auth_attempts_per_ip,
|
|
auth_attempt_window_secs,
|
|
handshake_timeout_ms: 1_000,
|
|
}
|
|
}
|
|
|
|
fn address(ip: &str) -> SocketAddr {
|
|
format!("{ip}:40000").parse().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn pending_limit_and_failed_handshake_release_reservation() {
|
|
let controller = AdmissionController::new(&limits(8, 4, 1, 100, 60));
|
|
let first = controller.begin(address("192.0.2.1")).unwrap();
|
|
assert_eq!(controller.pending_handshakes(), 1);
|
|
assert!(matches!(
|
|
controller.begin(address("192.0.2.2")),
|
|
Err(AdmissionError::Busy)
|
|
));
|
|
|
|
// Dropping the pending guard models any failed or timed-out handshake.
|
|
drop(first);
|
|
assert_eq!(controller.pending_handshakes(), 0);
|
|
let second = controller.begin(address("192.0.2.2")).unwrap();
|
|
drop(second);
|
|
assert_eq!(controller.pending_handshakes(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn active_and_per_ip_limits_release_on_close_or_drop() {
|
|
let controller = AdmissionController::new(&limits(2, 1, 2, 100, 60));
|
|
let first = controller.begin(address("192.0.2.1")).unwrap().promote();
|
|
assert_eq!(controller.active_sessions(), 1);
|
|
assert!(matches!(
|
|
controller.begin(address("192.0.2.1")),
|
|
Err(AdmissionError::Busy)
|
|
));
|
|
|
|
let second = controller.begin(address("192.0.2.2")).unwrap().promote();
|
|
assert_eq!(controller.active_sessions(), 2);
|
|
assert!(matches!(
|
|
controller.begin(address("192.0.2.3")),
|
|
Err(AdmissionError::Busy)
|
|
));
|
|
|
|
drop(first);
|
|
assert_eq!(controller.active_sessions(), 1);
|
|
drop(second);
|
|
assert_eq!(controller.active_sessions(), 0);
|
|
assert!(controller.begin(address("192.0.2.3")).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn concurrent_admissions_never_exceed_global_or_per_ip_capacity() {
|
|
let controller = AdmissionController::new(&limits(3, 3, 32, 100, 60));
|
|
let barrier = Arc::new(std::sync::Barrier::new(33));
|
|
let (sender, receiver) = std::sync::mpsc::channel();
|
|
let mut workers = Vec::new();
|
|
for _ in 0..32 {
|
|
let controller = controller.clone();
|
|
let barrier = barrier.clone();
|
|
let sender = sender.clone();
|
|
workers.push(thread::spawn(move || {
|
|
barrier.wait();
|
|
let permit = controller
|
|
.begin(address("192.0.2.10"))
|
|
.ok()
|
|
.map(|pending| pending.promote());
|
|
barrier.wait();
|
|
sender.send(permit).unwrap();
|
|
}));
|
|
}
|
|
barrier.wait();
|
|
barrier.wait();
|
|
drop(sender);
|
|
let permits: Vec<_> = receiver.into_iter().flatten().collect();
|
|
for worker in workers {
|
|
worker.join().unwrap();
|
|
}
|
|
assert_eq!(permits.len(), 3);
|
|
assert_eq!(controller.active_sessions(), 3);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn authentication_attempt_limit_is_per_ip_and_expires() {
|
|
let controller = AdmissionController::new(&limits(8, 4, 8, 2, 1));
|
|
for _ in 0..2 {
|
|
drop(controller.begin(address("192.0.2.1")).unwrap());
|
|
}
|
|
assert!(matches!(
|
|
controller.begin(address("192.0.2.1")),
|
|
Err(AdmissionError::RateLimited)
|
|
));
|
|
assert!(controller.begin(address("192.0.2.2")).is_ok());
|
|
|
|
tokio::time::sleep(Duration::from_millis(1_050)).await;
|
|
assert!(controller.begin(address("192.0.2.1")).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn server_limits_have_safe_defaults_and_reject_out_of_bounds_values() {
|
|
let defaults = LimitsConfig::default();
|
|
assert_eq!(defaults.max_sessions, 1024);
|
|
assert_eq!(defaults.max_sessions_per_ip, 32);
|
|
assert_eq!(defaults.max_pending_handshakes, 256);
|
|
assert_eq!(defaults.max_auth_attempts_per_ip, 20);
|
|
assert_eq!(defaults.auth_attempt_window_secs, 60);
|
|
assert_eq!(defaults.handshake_timeout_ms, 3_000);
|
|
|
|
let valid = "[server]\nlisten='127.0.0.1'\n[backend]\ntarget='127.0.0.1:51820'\n[[auth.clients]]\nid='one'\nsecret='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\n";
|
|
assert!(config::Config::from_str(valid).unwrap().validate().is_ok());
|
|
|
|
let invalid = format!("{valid}\n[limits]\nmax_auth_attempts_per_ip=0\n");
|
|
assert!(config::Config::from_str(&invalid)
|
|
.unwrap()
|
|
.validate()
|
|
.is_err());
|
|
let too_long = format!("{valid}\n[limits]\nhandshake_timeout_ms=300001\n");
|
|
assert!(config::Config::from_str(&too_long)
|
|
.unwrap()
|
|
.validate()
|
|
.is_err());
|
|
}
|