Skip to main content

ferogram_connect/
transport_obfuscated.rs

1/*
2 * Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
3 * https://github.com/ankit-chaubey
4 *
5 * Project: ferogram
6 * Website: https://ferogram.dev
7 *
8 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
9 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
10 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
11 * This file may not be copied, modified, or distributed except according
12 * to those terms.
13 */
14
15pub use ferogram_crypto::ObfuscatedCipher;
16
17use crate::ConnectError;
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio::net::TcpStream;
20
21/// Framing mode for `ObfuscatedStream`.
22///
23/// * `Abridged` - Obfuscated2 over Abridged framing (`0xEFEFEFEF` nonce tag).
24///   Used for plain and `0x??` MTProxy secrets.
25/// * `PaddedIntermediate` - Obfuscated2 over Padded Intermediate framing
26///   (`0xDDDDDDDD` nonce tag).  Required for `0xDD` MTProxy secrets.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum ObfuscatedFraming {
29    Abridged,
30    PaddedIntermediate,
31}
32
33pub struct ObfuscatedStream {
34    stream: TcpStream,
35    cipher: ObfuscatedCipher,
36    framing: ObfuscatedFraming,
37}
38
39impl ObfuscatedStream {
40    /// Connect using Abridged framing (plain MTProxy secret, no 0xDD prefix).
41    pub async fn connect(
42        addr: &str,
43        proxy_secret: Option<&[u8; 16]>,
44        dc_id: i16,
45    ) -> Result<Self, ConnectError> {
46        let stream = TcpStream::connect(addr).await?;
47        Self::handshake(stream, proxy_secret, dc_id, ObfuscatedFraming::Abridged).await
48    }
49
50    /// Connect using Padded Intermediate framing (0xDD MTProxy secret).
51    pub async fn connect_padded(
52        addr: &str,
53        proxy_secret: Option<&[u8; 16]>,
54        dc_id: i16,
55    ) -> Result<Self, ConnectError> {
56        let stream = TcpStream::connect(addr).await?;
57        Self::handshake(
58            stream,
59            proxy_secret,
60            dc_id,
61            ObfuscatedFraming::PaddedIntermediate,
62        )
63        .await
64    }
65
66    async fn handshake(
67        mut stream: TcpStream,
68        proxy_secret: Option<&[u8; 16]>,
69        dc_id: i16,
70        framing: ObfuscatedFraming,
71    ) -> Result<Self, ConnectError> {
72        let framing_byte = match framing {
73            ObfuscatedFraming::Abridged => 0xef,
74            ObfuscatedFraming::PaddedIntermediate => 0xdd,
75        };
76        let secret = proxy_secret.map(|s| s.as_ref());
77        let (nonce, cipher) = ferogram_crypto::build_obfuscated_init(framing_byte, dc_id, secret);
78        stream.write_all(&nonce).await?;
79        Ok(Self {
80            stream,
81            cipher,
82            framing,
83        })
84    }
85
86    /// Encrypt and send one frame using whichever [`ObfuscatedFraming`] this
87    /// stream was set up with.
88    pub async fn send(&mut self, data: &[u8]) -> Result<(), ConnectError> {
89        match self.framing {
90            ObfuscatedFraming::Abridged => {
91                debug_assert_eq!(
92                    data.len() % 4,
93                    0,
94                    "obfuscated send: payload must be 4-byte aligned"
95                );
96                let words = data.len() / 4;
97                let mut frame = if words < 0x7f {
98                    let mut v = Vec::with_capacity(1 + data.len());
99                    v.push(words as u8);
100                    v
101                } else {
102                    let mut v = Vec::with_capacity(4 + data.len());
103                    v.extend_from_slice(&[
104                        0x7f,
105                        (words & 0xff) as u8,
106                        ((words >> 8) & 0xff) as u8,
107                        ((words >> 16) & 0xff) as u8,
108                    ]);
109                    v
110                };
111                frame.extend_from_slice(data);
112                self.cipher.encrypt(&mut frame);
113                self.stream.write_all(&frame).await?;
114            }
115            ObfuscatedFraming::PaddedIntermediate => {
116                // Padded intermediate framing: 4-byte LE length of
117                // (payload + random 0-15 padding), then payload, then padding.
118                let mut pad_len_buf = [0u8; 1];
119                ferogram_crypto::fill_random(&mut pad_len_buf);
120                let pad_len = (pad_len_buf[0] & 0x0f) as usize;
121                let total_payload = data.len() + pad_len;
122                let mut frame = Vec::with_capacity(4 + total_payload);
123                frame.extend_from_slice(&(total_payload as u32).to_le_bytes());
124                frame.extend_from_slice(data);
125                let mut pad = vec![0u8; pad_len];
126                ferogram_crypto::fill_random(&mut pad);
127                frame.extend_from_slice(&pad);
128                self.cipher.encrypt(&mut frame);
129                self.stream.write_all(&frame).await?;
130            }
131        }
132        Ok(())
133    }
134
135    /// Read and decrypt one frame. A header word count above `0x7f` (in
136    /// Abridged framing) means the server sent a 4-byte transport error
137    /// code instead of a real frame; that's surfaced as an `Io` error rather
138    /// than parsed as a frame length.
139    pub async fn recv(&mut self) -> Result<Vec<u8>, ConnectError> {
140        match self.framing {
141            ObfuscatedFraming::Abridged => {
142                let mut h = [0u8; 1];
143                self.stream.read_exact(&mut h).await?;
144                self.cipher.decrypt(&mut h);
145
146                let words = if h[0] < 0x7f {
147                    h[0] as usize
148                } else if h[0] == 0x7f {
149                    let mut b = [0u8; 3];
150                    self.stream.read_exact(&mut b).await?;
151                    self.cipher.decrypt(&mut b);
152                    b[0] as usize | (b[1] as usize) << 8 | (b[2] as usize) << 16
153                } else {
154                    let mut rest = [0u8; 3];
155                    self.stream.read_exact(&mut rest).await?;
156                    self.cipher.decrypt(&mut rest);
157                    let code = i32::from_le_bytes([h[0], rest[0], rest[1], rest[2]]);
158                    return Err(ConnectError::Io(std::io::Error::new(
159                        std::io::ErrorKind::ConnectionRefused,
160                        format!("transport error from server: {code}"),
161                    )));
162                };
163
164                let mut buf = vec![0u8; words * 4];
165                self.stream.read_exact(&mut buf).await?;
166                self.cipher.decrypt(&mut buf);
167
168                if buf.len() == 4 {
169                    let code = i32::from_le_bytes(buf[..4].try_into().unwrap());
170                    if code < 0 {
171                        return Err(ConnectError::Io(std::io::Error::new(
172                            std::io::ErrorKind::ConnectionRefused,
173                            format!("transport error from server: {code}"),
174                        )));
175                    }
176                }
177
178                Ok(buf)
179            }
180            ObfuscatedFraming::PaddedIntermediate => {
181                let mut len_buf = [0u8; 4];
182                self.stream.read_exact(&mut len_buf).await?;
183                self.cipher.decrypt(&mut len_buf);
184                let total_len = i32::from_le_bytes(len_buf);
185                if total_len < 0 {
186                    return Err(ConnectError::Io(std::io::Error::new(
187                        std::io::ErrorKind::ConnectionRefused,
188                        format!("transport error from server: {total_len}"),
189                    )));
190                }
191                let mut buf = vec![0u8; total_len as usize];
192                self.stream.read_exact(&mut buf).await?;
193                self.cipher.decrypt(&mut buf);
194                if buf.len() >= 24 {
195                    let pad = (buf.len() - 24) % 16;
196                    buf.truncate(buf.len() - pad);
197                }
198                Ok(buf)
199            }
200        }
201    }
202}