use std::collections::HashMap;
use std::net::UdpSocket;
use clap::Parser;
pub fn default_dc_ips() -> HashMap<u32, String> {
[
(1, "149.154.175.50"),
(2, "149.154.167.51"),
(3, "149.154.175.100"),
(4, "149.154.167.91"),
(5, "149.154.171.5"),
(203, "91.105.192.100"),
]
.iter()
.map(|(k, v)| (*k, v.to_string()))
.collect()
}
pub fn default_dc_overrides() -> HashMap<u32, u32> {
[(203, 2)].iter().copied().collect()
}
#[derive(Clone, Debug)]
pub struct MtProtoProxy {
pub host: String,
pub port: u16,
pub secret: String,
}
fn parse_mtproto_proxy(s: &str) -> Result<MtProtoProxy, String> {
let parts: Vec<&str> = s.rsplitn(3, ':').collect();
if parts.len() != 3 {
return Err(format!("expected HOST:PORT:SECRET, got {:?}", s));
}
let secret = parts[0].to_string();
let port: u16 = parts[1]
.parse()
.map_err(|_| format!("invalid port {:?}", parts[1]))?;
let host = parts[2].to_string();
hex::decode(&secret).map_err(|_| format!("invalid hex secret {:?}", secret))?;
Ok(MtProtoProxy { host, port, secret })
}
fn parse_dc_ip(s: &str) -> Result<(u32, String), String> {
let (dc_s, ip_s) = s
.split_once(':')
.ok_or_else(|| format!("expected DC:IP, got {s:?}"))?;
let dc: u32 = dc_s
.parse()
.map_err(|_| format!("invalid DC number {dc_s:?}"))?;
let _: std::net::IpAddr = ip_s
.parse()
.map_err(|_| format!("invalid IP address {ip_s:?}"))?;
Ok((dc, ip_s.to_string()))
}
#[derive(Parser, Clone, Debug)]
#[command(
name = "tg-ws-proxy",
about = "Telegram MTProto WebSocket Bridge Proxy",
long_about = "Local MTProto proxy that tunnels Telegram Desktop traffic \
through WebSocket connections to Telegram DCs.\n\
Useful on networks where raw TCP to Telegram is blocked."
)]
pub struct Config {
#[arg(long, default_value = "1443", env = "TG_PORT")]
pub port: u16,
#[arg(long, default_value = "127.0.0.1", env = "TG_HOST")]
pub host: String,
#[arg(long, env = "TG_SECRET")]
pub secret: Option<String>,
#[arg(long = "dc-ip", value_name = "DC:IP", value_parser = parse_dc_ip)]
pub dc_ip: Vec<(u32, String)>,
#[arg(long = "buf-kb", default_value = "256", env = "TG_BUF_KB")]
pub buf_kb: usize,
#[arg(long = "pool-size", default_value = "4", env = "TG_POOL_SIZE")]
pub pool_size: usize,
#[arg(long = "max-connections", env = "TG_MAX_CONNECTIONS")]
pub max_connections: Option<usize>,
#[arg(short, long, env = "TG_VERBOSE")]
pub verbose: bool,
#[arg(long = "danger-accept-invalid-certs", env = "TG_SKIP_TLS_VERIFY")]
pub skip_tls_verify: bool,
#[arg(short = 'q', long, env = "TG_QUIET")]
pub quiet: bool,
#[arg(long = "log-file", value_name = "PATH", env = "TG_LOG_FILE")]
pub log_file: Option<String>,
#[arg(
long = "mtproto-proxy",
value_name = "HOST:PORT:SECRET",
value_parser = parse_mtproto_proxy,
value_delimiter = ',',
env = "TG_MTPROTO_PROXY"
)]
pub mtproto_proxies: Vec<MtProtoProxy>,
#[arg(long = "link-ip", env = "TG_LINK_IP")]
pub link_ip: Option<String>,
#[arg(long = "cf-domain", value_name = "DOMAIN", value_delimiter = ',', env = "TG_CF_DOMAIN")]
pub cf_domains: Vec<String>,
#[arg(long = "cf-priority", env = "TG_CF_PRIORITY")]
pub cf_priority: bool,
#[arg(long = "ws-connect-timeout", default_value = "10", env = "TG_WS_CONNECT_TIMEOUT")]
pub ws_connect_timeout: u64,
#[arg(long = "ws-fail-probe-timeout", default_value = "2", env = "TG_WS_FAIL_PROBE_TIMEOUT")]
pub ws_fail_probe_timeout: u64,
#[arg(long = "ws-fail-cooldown", default_value = "30", env = "TG_WS_FAIL_COOLDOWN")]
pub ws_fail_cooldown: u64,
#[arg(long = "ws-redirect-cooldown", default_value = "300", env = "TG_WS_REDIRECT_COOLDOWN")]
pub ws_redirect_cooldown: u64,
#[arg(long = "handshake-timeout", default_value = "10", env = "TG_HANDSHAKE_TIMEOUT")]
pub handshake_timeout: u64,
#[arg(long = "tcp-fallback-timeout", default_value = "10", env = "TG_TCP_FALLBACK_TIMEOUT")]
pub tcp_fallback_timeout: u64,
#[arg(long = "upstream-connect-timeout", default_value = "5", env = "TG_UPSTREAM_CONNECT_TIMEOUT")]
pub upstream_connect_timeout: u64,
#[arg(long = "upstream-fail-cooldown", default_value = "60", env = "TG_UPSTREAM_FAIL_COOLDOWN")]
pub upstream_fail_cooldown: u64,
#[arg(long = "cf-connect-timeout", default_value = "10", env = "TG_CF_CONNECT_TIMEOUT")]
pub cf_connect_timeout: u64,
#[arg(long = "cf-fail-cooldown", default_value = "60", env = "TG_CF_FAIL_COOLDOWN")]
pub cf_fail_cooldown: u64,
#[arg(long = "pool-max-age", default_value = "55", env = "TG_POOL_MAX_AGE")]
pub pool_max_age: u64,
}
impl Config {
pub fn from_args() -> Self {
let mut cfg = Self::parse();
if cfg.secret.is_none() {
let bytes: [u8; 16] = rand::random();
cfg.secret = Some(hex::encode(bytes));
}
if cfg.dc_ip.is_empty() && cfg.cf_domains.is_empty() {
cfg.dc_ip = vec![
(2, "149.154.167.220".to_string()),
(4, "149.154.167.220".to_string()),
];
}
cfg
}
pub fn secret_bytes(&self) -> Vec<u8> {
hex::decode(self.secret.as_deref().unwrap_or("")).expect("secret must be valid hex")
}
pub fn dc_redirects(&self) -> HashMap<u32, String> {
self.dc_ip.iter().cloned().collect()
}
pub fn link_host(&self) -> String {
if let Some(ref ip) = self.link_ip {
return ip.clone();
}
let bind_is_local = matches!(self.host.as_str(), "0.0.0.0" | "::" | "127.0.0.1" | "::1");
if bind_is_local {
if let Some(lan_ip) = detect_lan_ip() {
return lan_ip;
}
}
self.host.clone()
}
#[allow(dead_code)]
pub fn buf_bytes(&self) -> usize {
self.buf_kb * 1024
}
}
fn detect_lan_ip() -> Option<String> {
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
socket.connect("8.8.8.8:80").ok()?;
let local_addr = socket.local_addr().ok()?;
let ip = local_addr.ip();
if let std::net::IpAddr::V4(v4) = ip {
if !v4.is_loopback() && !v4.is_link_local() && !v4.is_unspecified() {
return Some(v4.to_string());
}
}
None
}