Skip to main content

layer_client/
proxy.rs

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