mirror of
https://github.com/telemt/telemt.git
synced 2026-11-01 13:21:58 +03:00
WEB: bounded opt-in wildcard host routing added
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
fn wildcard_source(selector: &str, origin: std::net::SocketAddr) -> String {
|
||||
source(selector, origin).replace("[web]", "[web]\nallow_wildcard_hosts = true")
|
||||
+ "\n[web.limits]\nmemory_envelope_bytes = 1610612736\n"
|
||||
}
|
||||
|
||||
fn capability(config: &ProxyConfig, host: &str) -> String {
|
||||
// Independent reference derivation uses the client wire contract, not the cache.
|
||||
use hmac::{Hmac, Mac};
|
||||
let mut mac =
|
||||
Hmac::<Sha256>::new_from_slice(&hex::decode("000102030405060708090a0b0c0d0e0f").unwrap())
|
||||
.unwrap();
|
||||
mac.update(b"tdesktop-web-proxy-bridge-v2\n");
|
||||
mac.update(host.as_bytes());
|
||||
mac.update(b"\nrelay");
|
||||
let encoded =
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
|
||||
assert!(config.web.runtime.as_ref().unwrap().route(host).is_some());
|
||||
encoded
|
||||
}
|
||||
|
||||
async fn bridge(listener: &TcpListener, runtime: &Arc<WebProcessRuntime>, host: &str) -> String {
|
||||
let cap = capability(&runtime.active_generation().config(), host);
|
||||
let response = request(
|
||||
listener,
|
||||
runtime,
|
||||
raw_request("GET", &format!("/relay/?bridge={cap}"), host, "", &[]),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
response.starts_with(b"HTTP/1.1 200"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
std::str::from_utf8(split_response(&response).1)
|
||||
.unwrap()
|
||||
.split_once("bootstrap=\"")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once('"')
|
||||
.unwrap()
|
||||
.0
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wildcard_cold_containment_canonical_host_and_positive_cache_in_all_modes() {
|
||||
for mode in ["off", "shadow", "enforce"] {
|
||||
let site = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let text = wildcard_source(
|
||||
"hosts = [\"*.example.com\", \"exact.example.com\"]",
|
||||
site.local_addr().unwrap(),
|
||||
)
|
||||
.replace(
|
||||
"enabled = true",
|
||||
&format!("enabled = true\ndecoy_fasttrack_mode = \"{mode}\""),
|
||||
);
|
||||
let generation = generation(1, &text);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(generation.clone())));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let host = "a.deep.example.com";
|
||||
let cap = capability(&generation.config(), host);
|
||||
let encoded = cap
|
||||
.bytes()
|
||||
.map(|byte| format!("%{byte:02X}"))
|
||||
.collect::<String>();
|
||||
for (path, headers) in [
|
||||
(format!("/wrong/{cap}"), String::new()),
|
||||
(format!("/wrong/{encoded}"), String::new()),
|
||||
("/wrong".to_string(), format!("Cookie: opaque={cap}\r\n")),
|
||||
("/relay/".to_string(), format!("User-Agent: {encoded}\r\n")),
|
||||
] {
|
||||
assert_private(
|
||||
&request(
|
||||
&listener,
|
||||
&runtime,
|
||||
raw_request("GET", &path, host, &headers, &[]),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
assert_eq!(runtime.wildcard.as_ref().unwrap().test_stats().0, 0);
|
||||
let before = runtime.wildcard.as_ref().unwrap().test_stats().2;
|
||||
let response = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
raw_request(
|
||||
"GET",
|
||||
&format!("/relay/?bridge={cap}"),
|
||||
"A.DEEP.EXAMPLE.COM:443",
|
||||
"",
|
||||
&[],
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(response.starts_with(b"HTTP/1.1 200"));
|
||||
let cache = runtime.wildcard.as_ref().unwrap();
|
||||
assert_eq!(cache.test_stats().0, 1);
|
||||
assert_eq!(cache.test_stats().2, before + 1);
|
||||
bridge(&listener, &runtime, host).await;
|
||||
assert_eq!(cache.test_stats().2, before + 1);
|
||||
for foreign in ["b.example.com", "exact.example.com"] {
|
||||
assert_private(
|
||||
&request(
|
||||
&listener,
|
||||
&runtime,
|
||||
raw_request("GET", &format!("/wrong/{cap}"), foreign, "", &[]),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
for invalid in [
|
||||
"example.com",
|
||||
"127.0.0.1",
|
||||
"a.example.com:0443",
|
||||
"a.example.com:80",
|
||||
"a..example.com",
|
||||
"a.example.com.",
|
||||
"a.example.com\r\nHost: b.example.com",
|
||||
] {
|
||||
assert!(
|
||||
request(
|
||||
&listener,
|
||||
&runtime,
|
||||
raw_request("GET", "/relay/", invalid, "", &[])
|
||||
)
|
||||
.await
|
||||
.starts_with(b"HTTP/1.1 404")
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(20), site.accept())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
stop(runtime, &[generation]).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wildcard_budget_exhaustion_does_not_break_warm_bootstraps_or_authenticated_carriers() {
|
||||
let text = wildcard_source(
|
||||
"host = \"*.example.com\"",
|
||||
"127.0.0.1:18081".parse().unwrap(),
|
||||
) + "\n[web.limits.wildcard]\nmax_profiles_per_request = 1\ncold_hmac_burst = 1\ncold_hmac_per_minute = 1\n";
|
||||
let generation = generation(1, &text);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(generation.clone())));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let bootstrap = bridge(&listener, &runtime, HOSTS[0]).await;
|
||||
let session = create(&runtime, HOSTS[0], &bootstrap).unwrap();
|
||||
let cap = capability(&generation.config(), HOSTS[1]);
|
||||
assert!(
|
||||
request(
|
||||
&listener,
|
||||
&runtime,
|
||||
raw_request("GET", &format!("/relay/?bridge={cap}"), HOSTS[1], "", &[])
|
||||
)
|
||||
.await
|
||||
.starts_with(b"HTTP/1.1 503")
|
||||
);
|
||||
bridge(&listener, &runtime, HOSTS[0]).await;
|
||||
let auth = format!(
|
||||
"Authorization: Bearer {session}\r\nContent-Type: application/octet-stream\r\nX-Up-Seq: 1\r\n"
|
||||
);
|
||||
let response = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
raw_request(
|
||||
"POST",
|
||||
"/relay/api/v1/up",
|
||||
HOSTS[0],
|
||||
&auth,
|
||||
&frame::encode(FrameType::Pong, 0, &[]),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
response.starts_with(b"HTTP/1.1 204"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
assert_private(
|
||||
&request(
|
||||
&listener,
|
||||
&runtime,
|
||||
raw_request(
|
||||
"POST",
|
||||
"/relay/api/v1/up",
|
||||
HOSTS[1],
|
||||
&auth,
|
||||
&frame::encode(FrameType::Pong, 0, &[]),
|
||||
),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
assert_eq!(runtime.wildcard.as_ref().unwrap().test_stats().2, 1);
|
||||
stop(runtime, &[generation]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wildcard_cache_eviction_never_resets_host_quotas_and_recovery_uses_actual_host() {
|
||||
let text = wildcard_source(
|
||||
"host = \"*.example.com\"",
|
||||
"127.0.0.1:18081".parse().unwrap(),
|
||||
) + "\n[web.limits.wildcard]\ncache_hosts = 1\n";
|
||||
let generation = generation(1, &text);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(generation.clone())));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let a = create(
|
||||
&runtime,
|
||||
HOSTS[0],
|
||||
&bridge(&listener, &runtime, HOSTS[0]).await,
|
||||
)
|
||||
.unwrap();
|
||||
create(
|
||||
&runtime,
|
||||
HOSTS[1],
|
||||
&bridge(&listener, &runtime, HOSTS[1]).await,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(runtime.wildcard.as_ref().unwrap().test_stats().0, 1);
|
||||
assert_eq!(
|
||||
create(
|
||||
&runtime,
|
||||
HOSTS[0],
|
||||
&bridge(&listener, &runtime, HOSTS[0]).await
|
||||
),
|
||||
Err(ManagerError::Limit)
|
||||
);
|
||||
let cap = capability(&generation.config(), HOSTS[0]);
|
||||
let response = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
raw_request(
|
||||
"GET",
|
||||
&format!("/relay/?bridge={cap}"),
|
||||
HOSTS[0],
|
||||
&format!(
|
||||
"Accept: application/vnd.telemt.web-recovery+json\r\nAuthorization: Bearer {a}\r\n"
|
||||
),
|
||||
&[],
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
response.starts_with(b"HTTP/1.1 200"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
let doc: serde_json::Value = serde_json::from_slice(split_response(&response).1).unwrap();
|
||||
assert!(doc["bootstrap"].as_str().is_some());
|
||||
stop(runtime, &[generation]).await;
|
||||
}
|
||||
|
||||
// Same-generation replacement and public decoy compatibility are independent regressions.
|
||||
#[path = "wildcard/decoy.rs"]
|
||||
mod decoy;
|
||||
#[path = "wildcard/reload.rs"]
|
||||
mod reload;
|
||||
|
||||
// Matched loopback measurements are opt-in and never run in the normal suite.
|
||||
#[path = "wildcard/benchmarks.rs"]
|
||||
mod benchmarks;
|
||||
|
||||
// Upgrade authentication must remain independent of cold-host admission pressure.
|
||||
#[path = "wildcard/websocket.rs"]
|
||||
mod websocket;
|
||||
Reference in New Issue
Block a user