Skip to main content

layer_client/
proxy.rs

1//! MTProxy secret parsing, transport auto-selection, and TCP connect.
2//!
3//! | Secret prefix | Transport |
4//! |---|---|
5//! | 16 raw bytes | Obfuscated Abridged |
6//! | `0xDD` + 16 bytes | PaddedIntermediate |
7//! | `0xEE` + 16 bytes + domain | FakeTLS |
8
9use crate::{InvocationError, TransportKind};
10use tokio::net::TcpStream;
11
12/// Decoded MTProxy configuration extracted from a proxy link.
13#[derive(Clone, Debug)]
14pub struct MtProxyConfig {
15    /// Proxy server hostname or IP.
16    pub host: String,
17    /// Proxy server port.
18    pub port: u16,
19    /// Raw secret bytes.
20    pub secret: Vec<u8>,
21    /// Transport variant pass this as `config.transport`.
22    pub transport: TransportKind,
23}
24
25impl MtProxyConfig {
26    /// Open a TCP connection to the MTProxy host:port.
27    /// The proxy forwards traffic to Telegram; do NOT also connect to a DC addr.
28    pub async fn connect(&self) -> Result<TcpStream, InvocationError> {
29        let addr = format!("{}:{}", self.host, self.port);
30        tracing::debug!("[layer] MTProxy TCP connect → {addr}");
31        TcpStream::connect(&addr).await.map_err(InvocationError::Io)
32    }
33
34    /// Socket address string `"host:port"`.
35    pub fn addr(&self) -> String {
36        format!("{}:{}", self.host, self.port)
37    }
38}
39
40/// Parse a `tg://proxy?server=…&port=…&secret=…` or `https://t.me/proxy?…` link.
41pub fn parse_proxy_link(url: &str) -> Option<MtProxyConfig> {
42    let query = url
43        .strip_prefix("tg://proxy?")
44        .or_else(|| url.strip_prefix("https://t.me/proxy?"))?;
45
46    let mut server = None;
47    let mut port: Option<u16> = None;
48    let mut secret_hex = None;
49
50    for pair in query.split('&') {
51        if let Some((k, v)) = pair.split_once('=') {
52            match k {
53                "server" => server = Some(v.to_string()),
54                "port" => port = v.parse().ok(),
55                "secret" => secret_hex = Some(v.to_string()),
56                _ => {}
57            }
58        }
59    }
60
61    let host = server?;
62    let port = port?;
63    let secret = decode_secret_hex(&secret_hex?)?;
64    let transport = secret_to_transport(&secret);
65    Some(MtProxyConfig {
66        host,
67        port,
68        secret,
69        transport,
70    })
71}
72
73fn decode_secret_hex(s: &str) -> Option<Vec<u8>> {
74    if s.len() >= 32 && s.chars().all(|c| c.is_ascii_hexdigit()) {
75        let bytes: Option<Vec<u8>> = (0..s.len())
76            .step_by(2)
77            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
78            .collect();
79        if let Some(b) = bytes {
80            return Some(b);
81        }
82    }
83    use base64::Engine as _;
84    base64::engine::general_purpose::URL_SAFE_NO_PAD
85        .decode(s.trim_end_matches('='))
86        .ok()
87}
88
89/// Map secret prefix to the correct [`TransportKind`].
90pub fn secret_to_transport(secret: &[u8]) -> TransportKind {
91    match secret.first() {
92        Some(&0xDD) => {
93            let key = extract_key_bytes(secret, 1);
94            TransportKind::PaddedIntermediate { secret: key }
95        }
96        Some(&0xEE) => {
97            let key = extract_key_bytes(secret, 1);
98            let domain = if secret.len() > 17 {
99                String::from_utf8_lossy(&secret[17..]).into_owned()
100            } else {
101                String::new()
102            };
103            match key {
104                Some(k) => TransportKind::FakeTls { secret: k, domain },
105                None => TransportKind::Obfuscated { secret: None },
106            }
107        }
108        _ => {
109            let key = extract_key_bytes(secret, 0);
110            TransportKind::Obfuscated { secret: key }
111        }
112    }
113}
114
115fn extract_key_bytes(secret: &[u8], offset: usize) -> Option<[u8; 16]> {
116    let slice = secret.get(offset..offset + 16)?;
117    let mut arr = [0u8; 16];
118    arr.copy_from_slice(slice);
119    Some(arr)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn parse_plain_secret() {
128        let url = "tg://proxy?server=1.2.3.4&port=443&secret=deadbeefdeadbeefdeadbeefdeadbeef";
129        let cfg = parse_proxy_link(url).unwrap();
130        assert_eq!(cfg.host, "1.2.3.4");
131        assert_eq!(cfg.port, 443);
132        assert!(matches!(cfg.transport, TransportKind::Obfuscated { .. }));
133        assert_eq!(cfg.addr(), "1.2.3.4:443");
134    }
135
136    #[test]
137    fn parse_dd_secret() {
138        let url =
139            "tg://proxy?server=p.example.com&port=8888&secret=dddeadbeefdeadbeefdeadbeefdeadbeef";
140        let cfg = parse_proxy_link(url).unwrap();
141        assert!(matches!(
142            cfg.transport,
143            TransportKind::PaddedIntermediate { .. }
144        ));
145    }
146
147    #[test]
148    fn parse_ee_secret() {
149        let mut raw = vec![0xeeu8];
150        raw.extend_from_slice(&[0xabu8; 16]);
151        raw.extend_from_slice(b"example.com");
152        let hex: String = raw.iter().map(|b| format!("{b:02x}")).collect();
153        let url = format!("tg://proxy?server=p.example.com&port=443&secret={hex}");
154        let cfg = parse_proxy_link(&url).unwrap();
155        if let TransportKind::FakeTls { domain, .. } = &cfg.transport {
156            assert_eq!(domain, "example.com");
157        } else {
158            panic!("expected FakeTls");
159        }
160    }
161
162    #[test]
163    fn invalid_url_returns_none() {
164        assert!(parse_proxy_link("https://example.com").is_none());
165        assert!(parse_proxy_link("tg://proxy?server=x&port=443").is_none());
166    }
167}