use std::time::{Duration, Instant};
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use crate::config::{Config, MtProtoProxy};
use crate::crypto::{ProtoTag, generate_client_handshake};
use crate::faketls;
use crate::ws_client::connect_cf_ws_for_dc;
enum ProbeStatus {
Ok(Duration),
Fail(String),
}
impl ProbeStatus {
fn marker(&self) -> &'static str {
match self {
Self::Ok(_) => "OK ",
Self::Fail(_) => "FAIL",
}
}
fn detail(&self) -> String {
match self {
Self::Ok(d) => format!("{}ms", d.as_millis()),
Self::Fail(reason) => reason.clone(),
}
}
fn is_ok(&self) -> bool {
matches!(self, Self::Ok(_))
}
}
async fn probe_cf_domain(domain: &str, skip_tls: bool, timeout: Duration) -> ProbeStatus {
let start = Instant::now();
let (ws, _) = connect_cf_ws_for_dc(2, &[domain.to_string()], false, skip_tls, timeout).await;
if ws.is_some() {
ProbeStatus::Ok(start.elapsed())
} else {
ProbeStatus::Fail(
"WebSocket connection failed — check DNS records and Cloudflare settings".to_string(),
)
}
}
async fn probe_mtproto_proxy(proxy: &MtProtoProxy, timeout: Duration) -> ProbeStatus {
let secret = match hex::decode(&proxy.secret) {
Ok(b) => b,
Err(e) => return ProbeStatus::Fail(format!("invalid hex secret: {}", e)),
};
let is_faketls = secret.len() > 17 && secret[0] == 0xee;
let key_bytes: &[u8] = if secret.len() >= 17 && matches!(secret[0], 0xdd | 0xee) {
&secret[1..17]
} else {
&secret
};
let start = Instant::now();
let stream = match tokio::time::timeout(
timeout,
TcpStream::connect(format!("{}:{}", proxy.host, proxy.port)),
)
.await
{
Ok(Ok(s)) => s,
Ok(Err(e)) => return ProbeStatus::Fail(format!("TCP connect failed: {}", e)),
Err(_) => return ProbeStatus::Fail("TCP connect timed out".to_string()),
};
let _ = stream.set_nodelay(true);
let (handshake, _enc, _dec) =
generate_client_handshake(key_bytes, 2, ProtoTag::PaddedIntermediate);
let (mut reader, mut writer) = tokio::io::split(stream);
if is_faketls {
let hostname = match std::str::from_utf8(&secret[17..]) {
Ok(h) => h,
Err(_) => {
return ProbeStatus::Fail("FakeTLS secret contains non-UTF-8 hostname".to_string());
}
};
let mut client_hello = faketls::build_faketls_client_hello(hostname);
faketls::sign_faketls_client_hello(&mut client_hello, key_bytes);
if let Err(e) = writer.write_all(&client_hello).await {
return ProbeStatus::Fail(format!("send FakeTLS ClientHello: {}", e));
}
let drained =
tokio::time::timeout(timeout, faketls::drain_faketls_server_hello(&mut reader))
.await
.unwrap_or(false);
if !drained {
return ProbeStatus::Fail(
"FakeTLS server handshake failed or timed out — check secret and proxy address"
.to_string(),
);
}
} else {
if let Err(e) = writer.write_all(&handshake).await {
return ProbeStatus::Fail(format!("send MTProto handshake: {}", e));
}
}
ProbeStatus::Ok(start.elapsed())
}
fn proxy_kind(proxy: &MtProtoProxy) -> &'static str {
let first_byte = proxy
.secret
.get(..2)
.and_then(|s| u8::from_str_radix(s, 16).ok());
match first_byte {
Some(0xee) => "FakeTLS",
Some(0xdd) => "padded",
_ => "plain",
}
}
pub async fn run_check(config: &Config) -> bool {
let cf_timeout = Duration::from_secs(config.cf_connect_timeout);
let upstream_timeout = Duration::from_secs(config.upstream_connect_timeout);
let skip_tls = config.skip_tls_verify;
let sep = "=".repeat(60);
println!("{}", sep);
println!(" tg-ws-proxy connectivity check");
println!("{}", sep);
if config.cf_domains.is_empty() && config.mtproto_proxies.is_empty() {
println!();
println!(" Nothing to check.");
println!(" Configure --cf-domain and/or --mtproto-proxy and re-run.");
println!("{}", sep);
return true;
}
let mut all_ok = true;
if !config.cf_domains.is_empty() {
println!();
println!("Cloudflare proxy domains (DC2 WebSocket probe):");
for domain in &config.cf_domains {
print!(" {:40} ... ", format!("kws2.{}", domain));
let _ = std::io::Write::flush(&mut std::io::stdout());
let status = probe_cf_domain(domain, skip_tls, cf_timeout).await;
println!("[{}] {}", status.marker(), status.detail());
if !status.is_ok() {
all_ok = false;
}
}
}
if !config.mtproto_proxies.is_empty() {
println!();
println!("Upstream MTProto proxies:");
for proxy in &config.mtproto_proxies {
let label = format!("{}:{} [{}]", proxy.host, proxy.port, proxy_kind(proxy));
print!(" {:40} ... ", label);
let _ = std::io::Write::flush(&mut std::io::stdout());
let status = probe_mtproto_proxy(proxy, upstream_timeout).await;
println!("[{}] {}", status.marker(), status.detail());
if !status.is_ok() {
all_ok = false;
}
}
}
println!();
println!("{}", sep);
if all_ok {
println!(" Result: all checks passed");
} else {
println!(" Result: one or more checks FAILED");
}
println!("{}", sep);
all_ok
}