telemt/src/proxy/tests/middle_relay_quota_reservat...

1066 lines
30 KiB
Rust

use super::*;
use crate::crypto::{AesCtr, SecureRandom};
use crate::stats::Stats;
use crate::stream::CryptoWriter;
use bytes::Bytes;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::task::{Context, Poll};
use tokio::io::AsyncWrite;
use tokio::task::JoinSet;
fn make_crypto_writer<W>(writer: W) -> CryptoWriter<W>
where
W: tokio::io::AsyncWrite + Unpin,
{
let key = [0u8; 32];
let iv = 0u128;
CryptoWriter::new(writer, AesCtr::new(&key, iv), 8 * 1024)
}
struct FailingWriter;
impl AsyncWrite for FailingWriter {
fn poll_write(
self: std::pin::Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &[u8],
) -> Poll<std::result::Result<usize, std::io::Error>> {
Poll::Ready(Err(std::io::Error::other("forced writer failure")))
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), std::io::Error>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), std::io::Error>> {
Poll::Ready(Ok(()))
}
}
struct FailAfterBudgetWriter {
remaining: usize,
written: usize,
}
impl FailAfterBudgetWriter {
fn new(remaining: usize) -> Self {
Self {
remaining,
written: 0,
}
}
}
impl AsyncWrite for FailAfterBudgetWriter {
fn poll_write(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::result::Result<usize, std::io::Error>> {
if self.remaining == 0 {
return Poll::Ready(Err(std::io::Error::other("forced short-write exhaustion")));
}
let n = self.remaining.min(buf.len());
self.remaining -= n;
self.written += n;
Poll::Ready(Ok(n))
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), std::io::Error>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), std::io::Error>> {
Poll::Ready(Ok(()))
}
}
#[tokio::test]
async fn positive_exact_quota_boundary_allows_last_frame_and_blocks_next() {
let stats = Stats::new();
let user = "quota-boundary-user";
let bytes_me2c = AtomicU64::new(0);
stats.add_user_octets_from(user, 5);
let mut writer_one = make_crypto_writer(tokio::io::sink());
let mut frame_buf_one = Vec::new();
let first = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[1, 2, 3]),
},
&mut writer_one,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf_one,
&stats,
user,
Some(8),
0,
&bytes_me2c,
7101,
false,
false,
)
.await;
assert!(first.is_ok(), "frame that reaches boundary must be allowed");
assert_eq!(stats.get_user_total_octets(user), 8);
let mut writer_two = make_crypto_writer(tokio::io::sink());
let mut frame_buf_two = Vec::new();
let second = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[9]),
},
&mut writer_two,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf_two,
&stats,
user,
Some(8),
0,
&bytes_me2c,
7102,
false,
false,
)
.await;
assert!(
matches!(second, Err(ProxyError::DataQuotaExceeded { .. })),
"frame after boundary must be rejected"
);
assert_eq!(stats.get_user_total_octets(user), 8);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 3);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn adversarial_parallel_reservation_stress_never_overshoots_quota_or_counters() {
let stats = Arc::new(Stats::new());
let user = "reservation-stress-user";
let quota_limit = 64u64;
let bytes_me2c = Arc::new(AtomicU64::new(0));
let mut tasks = JoinSet::new();
for idx in 0..256u64 {
let user_owned = user.to_string();
let stats_ref = Arc::clone(&stats);
let bytes_ref = Arc::clone(&bytes_me2c);
tasks.spawn(async move {
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[0xAB]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
stats_ref.as_ref(),
&user_owned,
Some(quota_limit),
0,
bytes_ref.as_ref(),
7200 + idx,
false,
false,
)
.await
});
}
let mut ok = 0usize;
let mut denied = 0usize;
while let Some(joined) = tasks.join_next().await {
match joined.expect("reservation stress task must not panic") {
Ok(_) => ok += 1,
Err(ProxyError::DataQuotaExceeded { .. }) => denied += 1,
Err(other) => panic!("unexpected error in stress case: {other:?}"),
}
}
let total = stats.get_user_total_octets(user);
assert_eq!(
total, quota_limit,
"quota must be exactly exhausted without overshoot"
);
assert_eq!(
bytes_me2c.load(Ordering::Relaxed),
total,
"ME->C forensic bytes must track committed quota usage"
);
assert_eq!(ok, quota_limit as usize, "exactly quota_limit tasks must succeed");
assert_eq!(
denied,
256usize - (quota_limit as usize),
"remaining tasks must be exactly denied without silently swallowing state"
);
}
#[tokio::test]
async fn light_fuzz_random_frame_sizes_preserve_quota_and_counter_consistency() {
let stats = Stats::new();
let user = "reservation-fuzz-user";
let quota_limit = 128u64;
let bytes_me2c = AtomicU64::new(0);
let mut seed = 0xC0FE_EE11_8899_2211u64;
for conn in 0..512u64 {
seed ^= seed << 7;
seed ^= seed >> 9;
seed ^= seed << 8;
let len = ((seed & 0x0f) + 1) as usize;
let payload = vec![0x5A; len];
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from(payload),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(quota_limit),
0,
&bytes_me2c,
7300 + conn,
false,
false,
)
.await;
if let Err(err) = result {
assert!(
matches!(err, ProxyError::DataQuotaExceeded { .. }),
"fuzz run produced unexpected error variant: {err:?}"
);
}
}
let total = stats.get_user_total_octets(user);
assert!(total <= quota_limit);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), total);
}
#[tokio::test]
async fn positive_soft_overshoot_allows_burst_inside_soft_cap_then_blocks() {
let stats = Stats::new();
let user = "soft-cap-boundary-user";
let bytes_me2c = AtomicU64::new(0);
let quota_limit = 10u64;
let overshoot = 3u64;
stats.add_user_octets_from(user, 10);
let mut writer_one = make_crypto_writer(tokio::io::sink());
let mut frame_buf_one = Vec::new();
let first = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[1, 2, 3]),
},
&mut writer_one,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf_one,
&stats,
user,
Some(quota_limit),
overshoot,
&bytes_me2c,
7401,
false,
false,
)
.await;
assert!(first.is_ok(), "soft-cap buffer should allow reaching limit+overshoot");
assert_eq!(stats.get_user_total_octets(user), 13);
let mut writer_two = make_crypto_writer(tokio::io::sink());
let mut frame_buf_two = Vec::new();
let second = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[9]),
},
&mut writer_two,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf_two,
&stats,
user,
Some(quota_limit),
overshoot,
&bytes_me2c,
7402,
false,
false,
)
.await;
assert!(matches!(second, Err(ProxyError::DataQuotaExceeded { .. })));
assert_eq!(stats.get_user_total_octets(user), 13);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn negative_soft_overshoot_rejects_when_payload_exceeds_remaining_soft_budget() {
let stats = Stats::new();
let user = "soft-cap-remaining-user";
let bytes_me2c = AtomicU64::new(0);
let quota_limit = 10u64;
let overshoot = 4u64;
stats.add_user_octets_from(user, 12);
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[1, 2, 3]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(quota_limit),
overshoot,
&bytes_me2c,
7501,
false,
false,
)
.await;
assert!(matches!(result, Err(ProxyError::DataQuotaExceeded { .. })));
assert_eq!(stats.get_user_total_octets(user), 12);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn negative_write_failure_rolls_back_reservation_under_soft_cap_mode() {
let stats = Stats::new();
let user = "soft-cap-rollback-user";
let bytes_me2c = AtomicU64::new(0);
let mut writer = make_crypto_writer(FailingWriter);
let mut frame_buf = Vec::new();
stats.add_user_octets_from(user, 9);
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[1, 2, 3]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(10),
8,
&bytes_me2c,
7601,
false,
false,
)
.await;
assert!(matches!(result, Err(ProxyError::Io(_))));
assert_eq!(stats.get_user_total_octets(user), 9);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn adversarial_parallel_soft_cap_stress_never_exceeds_soft_limit() {
let stats = Arc::new(Stats::new());
let user = "soft-cap-stress-user";
let quota_limit = 40u64;
let overshoot = 5u64;
let soft_limit = quota_limit + overshoot;
let bytes_me2c = Arc::new(AtomicU64::new(0));
let mut tasks = JoinSet::new();
for idx in 0..256u64 {
let user_owned = user.to_string();
let stats_ref = Arc::clone(&stats);
let bytes_ref = Arc::clone(&bytes_me2c);
tasks.spawn(async move {
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[0x42]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
stats_ref.as_ref(),
&user_owned,
Some(quota_limit),
overshoot,
bytes_ref.as_ref(),
7700 + idx,
false,
false,
)
.await
});
}
while let Some(joined) = tasks.join_next().await {
match joined.expect("soft-cap stress task must not panic") {
Ok(_) | Err(ProxyError::DataQuotaExceeded { .. }) => {}
Err(other) => panic!("unexpected error in soft-cap stress case: {other:?}"),
}
}
let total = stats.get_user_total_octets(user);
assert!(total <= soft_limit, "soft-cap stress must never overshoot soft limit");
assert_eq!(bytes_me2c.load(Ordering::Relaxed), total);
}
#[tokio::test]
async fn light_fuzz_soft_cap_matrix_keeps_counters_and_limits_consistent() {
let stats = Stats::new();
let user = "soft-cap-fuzz-user";
let bytes_me2c = AtomicU64::new(0);
let mut seed = 0x9E37_79B9_7F4A_7C15u64;
for conn in 0..1024u64 {
seed ^= seed << 7;
seed ^= seed >> 9;
seed ^= seed << 8;
let quota_limit = 32 + (seed & 0x3f);
let overshoot = seed.rotate_left(13) & 0x0f;
let len = ((seed >> 3) & 0x07) + 1;
let payload = vec![0xA5; len as usize];
let before = stats.get_user_total_octets(user);
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from(payload),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(quota_limit),
overshoot,
&bytes_me2c,
7800 + conn,
false,
false,
)
.await;
if let Err(ref err) = result {
assert!(
matches!(err, ProxyError::DataQuotaExceeded { .. }),
"soft-cap fuzz produced unexpected error variant: {err:?}"
);
}
let after = stats.get_user_total_octets(user);
let soft_limit = quota_limit.saturating_add(overshoot);
match result {
Ok(_) => {
assert_eq!(after, before.saturating_add(len));
assert!(after <= soft_limit, "accepted write must stay within active soft cap");
}
Err(_) => {
assert_eq!(after, before, "rejected write must not mutate quota state");
}
}
assert_eq!(
bytes_me2c.load(Ordering::Relaxed),
after,
"soft-cap fuzz must keep counters synchronized"
);
}
}
#[tokio::test]
async fn positive_no_quota_limit_accumulates_data_octets_exactly() {
let stats = Stats::new();
let user = "no-quota-user";
let bytes_me2c = AtomicU64::new(0);
let mut expected = 0u64;
for (idx, len) in [1usize, 2, 3, 5, 8, 13, 21].iter().copied().enumerate() {
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let payload = vec![0x41; len];
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from(payload),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
None,
0,
&bytes_me2c,
7900 + idx as u64,
false,
false,
)
.await;
assert!(result.is_ok());
expected += len as u64;
}
assert_eq!(stats.get_user_total_octets(user), expected);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), expected);
}
#[tokio::test]
async fn negative_zero_quota_rejects_non_empty_payload() {
let stats = Stats::new();
let user = "zero-quota-user";
let bytes_me2c = AtomicU64::new(0);
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[0xAA]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(0),
0,
&bytes_me2c,
8001,
false,
false,
)
.await;
assert!(matches!(result, Err(ProxyError::DataQuotaExceeded { .. })));
assert_eq!(stats.get_user_total_octets(user), 0);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn edge_zero_length_payload_with_zero_quota_is_fail_closed() {
let stats = Stats::new();
let user = "zero-len-zero-quota-user";
let bytes_me2c = AtomicU64::new(0);
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::new(),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(0),
0,
&bytes_me2c,
8002,
false,
false,
)
.await;
assert!(matches!(result, Err(ProxyError::DataQuotaExceeded { .. })));
assert_eq!(stats.get_user_total_octets(user), 0);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn positive_ack_response_does_not_touch_quota_counters() {
let stats = Stats::new();
let user = "ack-accounting-user";
let bytes_me2c = AtomicU64::new(11);
stats.add_user_octets_to(user, 23);
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
MeResponse::Ack(0x33445566),
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(24),
0,
&bytes_me2c,
8003,
true,
true,
)
.await;
assert!(result.is_ok());
assert_eq!(stats.get_user_total_octets(user), 23);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 11);
}
#[tokio::test]
async fn edge_close_response_is_accounting_noop() {
let stats = Stats::new();
let user = "close-accounting-user";
let bytes_me2c = AtomicU64::new(19);
stats.add_user_octets_to(user, 31);
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
MeResponse::Close,
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(40),
3,
&bytes_me2c,
8004,
false,
true,
)
.await;
assert!(result.is_ok());
assert_eq!(stats.get_user_total_octets(user), 31);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 19);
}
#[tokio::test]
async fn negative_preloaded_above_soft_cap_rejects_even_single_byte() {
let stats = Stats::new();
let user = "preloaded-over-soft-cap-user";
let bytes_me2c = AtomicU64::new(0);
let quota_limit = 20u64;
let overshoot = 2u64;
stats.add_user_octets_to(user, quota_limit + overshoot + 1);
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[1]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(quota_limit),
overshoot,
&bytes_me2c,
8005,
false,
false,
)
.await;
assert!(matches!(result, Err(ProxyError::DataQuotaExceeded { .. })));
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 0);
assert_eq!(stats.get_user_total_octets(user), quota_limit + overshoot + 1);
}
#[tokio::test]
async fn adversarial_fail_writer_path_never_desynchronizes_quota_accounting() {
let stats = Stats::new();
let user = "partial-write-rollback-user";
let bytes_me2c = AtomicU64::new(0);
let mut writer = make_crypto_writer(FailAfterBudgetWriter::new(7));
let mut frame_buf = Vec::new();
let payload_len = 16 * 1024u64;
let result = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from(vec![0x42; 16 * 1024]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(payload_len),
0,
&bytes_me2c,
8006,
false,
false,
)
.await;
let total_after = stats.get_user_total_octets(user);
let forensic_after = bytes_me2c.load(Ordering::Relaxed);
assert_eq!(forensic_after, total_after);
assert!(
total_after == 0 || total_after == payload_len,
"writer failure path must either roll back fully or commit exactly one payload"
);
// Regardless of whether I/O failure surfaced immediately or was deferred,
// accounting must remain fail-closed and prevent silent overshoot.
let mut writer_two = make_crypto_writer(tokio::io::sink());
let mut frame_buf_two = Vec::new();
let second = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[0x99]),
},
&mut writer_two,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf_two,
&stats,
user,
Some(payload_len),
0,
&bytes_me2c,
8007,
false,
false,
)
.await;
if total_after == payload_len {
assert!(matches!(second, Err(ProxyError::DataQuotaExceeded { .. })));
} else {
assert!(second.is_ok());
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn stress_parallel_oversized_frames_fail_closed_without_counter_leak() {
let stats = Arc::new(Stats::new());
let user = "parallel-fail-rollback-user";
let bytes_me2c = Arc::new(AtomicU64::new(0));
let mut tasks = JoinSet::new();
for idx in 0..256u64 {
let user_owned = user.to_string();
let stats_ref = Arc::clone(&stats);
let bytes_ref = Arc::clone(&bytes_me2c);
tasks.spawn(async move {
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from(vec![0xEE; 12 * 1024]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
stats_ref.as_ref(),
&user_owned,
Some(512),
0,
bytes_ref.as_ref(),
8100 + idx,
false,
false,
)
.await
});
}
while let Some(joined) = tasks.join_next().await {
let result = joined.expect("parallel fail writer task must not panic");
assert!(matches!(result, Err(ProxyError::DataQuotaExceeded { .. })));
}
assert_eq!(stats.get_user_total_octets(user), 0);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn integration_mixed_data_ack_close_sequence_preserves_data_only_accounting() {
let stats = Stats::new();
let user = "mixed-sequence-user";
let bytes_me2c = AtomicU64::new(0);
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let data_one = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[1, 2, 3]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(32),
0,
&bytes_me2c,
8201,
false,
false,
)
.await;
assert!(data_one.is_ok());
let ack = process_me_writer_response(
MeResponse::Ack(0x0102_0304),
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(32),
0,
&bytes_me2c,
8202,
true,
true,
)
.await;
assert!(ack.is_ok());
let data_two = process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[4, 5]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(32),
0,
&bytes_me2c,
8203,
false,
true,
)
.await;
assert!(data_two.is_ok());
let close = process_me_writer_response(
MeResponse::Close,
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(32),
0,
&bytes_me2c,
8204,
false,
true,
)
.await;
assert!(close.is_ok());
assert_eq!(stats.get_user_total_octets(user), 5);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 5);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn stress_parallel_multi_user_quota_isolation_no_cross_user_leakage() {
let stats = Arc::new(Stats::new());
let user_a = "quota-isolation-a";
let user_b = "quota-isolation-b";
let limit_a = 50u64;
let limit_b = 80u64;
let bytes_a = Arc::new(AtomicU64::new(0));
let bytes_b = Arc::new(AtomicU64::new(0));
let mut tasks = JoinSet::new();
for idx in 0..200u64 {
let stats_ref = Arc::clone(&stats);
let bytes_ref = Arc::clone(&bytes_a);
tasks.spawn(async move {
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[0xA1]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
stats_ref.as_ref(),
user_a,
Some(limit_a),
0,
bytes_ref.as_ref(),
8300 + idx,
false,
false,
)
.await
});
}
for idx in 0..220u64 {
let stats_ref = Arc::clone(&stats);
let bytes_ref = Arc::clone(&bytes_b);
tasks.spawn(async move {
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
process_me_writer_response(
MeResponse::Data {
flags: 0,
data: Bytes::from_static(&[0xB2]),
},
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
stats_ref.as_ref(),
user_b,
Some(limit_b),
0,
bytes_ref.as_ref(),
8500 + idx,
false,
false,
)
.await
});
}
while let Some(joined) = tasks.join_next().await {
let result = joined.expect("quota isolation task must not panic");
assert!(result.is_ok() || matches!(result, Err(ProxyError::DataQuotaExceeded { .. })));
}
assert_eq!(stats.get_user_total_octets(user_a), limit_a);
assert_eq!(stats.get_user_total_octets(user_b), limit_b);
assert_eq!(bytes_a.load(Ordering::Relaxed), limit_a);
assert_eq!(bytes_b.load(Ordering::Relaxed), limit_b);
}
#[tokio::test]
async fn light_fuzz_mixed_me_responses_preserve_quota_and_counter_invariants() {
let stats = Stats::new();
let user = "mixed-fuzz-user";
let bytes_me2c = AtomicU64::new(0);
let quota_limit = 96u64;
let mut seed = 0xDEAD_BEEF_2026_0323u64;
for idx in 0..2048u64 {
seed ^= seed << 7;
seed ^= seed >> 9;
seed ^= seed << 8;
let choice = (seed & 0x03) as u8;
let response = if choice == 0 {
MeResponse::Ack((seed >> 8) as u32)
} else if choice == 1 {
MeResponse::Close
} else {
let len = ((seed >> 16) & 0x07) as usize;
let mut payload = vec![0u8; len];
payload.fill((seed & 0xff) as u8);
MeResponse::Data {
flags: 0,
data: Bytes::from(payload),
}
};
let mut writer = make_crypto_writer(tokio::io::sink());
let mut frame_buf = Vec::new();
let result = process_me_writer_response(
response,
&mut writer,
ProtoTag::Intermediate,
&SecureRandom::new(),
&mut frame_buf,
&stats,
user,
Some(quota_limit),
0,
&bytes_me2c,
8800 + idx,
(idx & 1) == 0,
(idx & 2) == 0,
)
.await;
if let Err(err) = result {
assert!(
matches!(err, ProxyError::DataQuotaExceeded { .. }),
"mixed fuzz produced unexpected error variant: {err:?}"
);
}
let total = stats.get_user_total_octets(user);
assert!(
total <= quota_limit,
"mixed fuzz must keep usage at or below quota limit"
);
assert_eq!(bytes_me2c.load(Ordering::Relaxed), total);
}
}