use std::time::{Duration, Instant};
use tokio::io::AsyncWriteExt;
use crate::config::{Config, MtProtoProxy, default_dc_ips};
use crate::crypto::{ProtoTag, generate_client_handshake};
use crate::faketls;
use crate::outbound::OutboundConnector;
use crate::ws_client::{
connect_cf_worker_ws_for_dc_with_outbound, connect_cf_ws_for_dc_with_outbound,
};
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,
outbound: &OutboundConnector,
) -> ProbeStatus {
let start = Instant::now();
let (ws, _) = connect_cf_ws_for_dc_with_outbound(
2,
&[domain.to_string()],
false,
skip_tls,
timeout,
outbound,
)
.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_cf_worker(
domain: &str,
skip_tls: bool,
timeout: Duration,
outbound: &OutboundConnector,
) -> ProbeStatus {
let Some(dst) = default_dc_ips().get(&2).cloned() else {
return ProbeStatus::Fail("DC 2 default IP is missing".to_string());
};
let start = Instant::now();
let ws = connect_cf_worker_ws_for_dc_with_outbound(
domain, &dst, 2, false, skip_tls, timeout, outbound,
)
.await;
if ws.is_some() {
ProbeStatus::Ok(start.elapsed())
} else {
ProbeStatus::Fail(
"Worker WebSocket tunnel failed — check Worker code and domain".to_string(),
)
}
}
async fn probe_mtproto_proxy(
proxy: &MtProtoProxy,
timeout: Duration,
outbound: &OutboundConnector,
) -> 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 outbound.connect(&proxy.host, proxy.port, timeout).await {
Ok(s) => s,
Err(e) => return ProbeStatus::Fail(format!("TCP connect failed: {}", e)),
};
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 outbound = match config.outbound_connector() {
Ok(outbound) => outbound,
Err(e) => {
eprintln!("Invalid outbound proxy configuration: {e}");
return false;
}
};
run_check_with_outbound(config, &outbound).await
}
pub async fn run_check_with_outbound(config: &Config, outbound: &OutboundConnector) -> 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);
let cf_worker_domains = config.cf_worker_domains();
if config.cf_domains.is_empty()
&& cf_worker_domains.is_empty()
&& config.mtproto_proxies.is_empty()
{
println!();
println!(" Nothing to check.");
println!(" Configure --cf-domain, --cf-worker-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, outbound).await;
println!("[{}] {}", status.marker(), status.detail());
if !status.is_ok() {
all_ok = false;
}
}
}
if !cf_worker_domains.is_empty() {
println!();
println!("Cloudflare Worker domains (DC2 TCP tunnel probe):");
for domain in &cf_worker_domains {
print!(" {:40} ... ", domain);
let _ = std::io::Write::flush(&mut std::io::stdout());
let status = probe_cf_worker(&domain, skip_tls, cf_timeout, outbound).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, outbound).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
}