Skip to main content

ferogram_connect/
transport_intermediate.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2//
3// ferogram: async Telegram MTProto client in Rust
4// https://github.com/ankit-chaubey/ferogram
5//
6// Licensed under either the MIT License or the Apache License 2.0.
7// See the LICENSE-MIT or LICENSE-APACHE file in this repository:
8// https://github.com/ankit-chaubey/ferogram
9//
10// Feel free to use, modify, and share this code.
11// Please keep this notice when redistributing.
12
13use crate::ConnectError;
14use tokio::io::{AsyncReadExt, AsyncWriteExt};
15use tokio::net::TcpStream;
16
17// Intermediate
18
19/// [MTProto Intermediate] transport framing.
20///
21/// Init byte: `0xeeeeeeee` (4 bytes).  Each message is prefixed with its
22/// 4-byte little-endian byte length.
23///
24/// [MTProto Intermediate]: https://core.telegram.org/mtproto/mtproto-transports#intermediate
25pub struct IntermediateTransport {
26    stream: TcpStream,
27    init_sent: bool,
28}
29
30impl IntermediateTransport {
31    /// Connect and send the 4-byte init header.
32    pub async fn connect(addr: &str) -> Result<Self, ConnectError> {
33        let stream = TcpStream::connect(addr).await?;
34        Ok(Self {
35            stream,
36            init_sent: false,
37        })
38    }
39
40    /// Wrap an existing stream (the init byte will be sent on first [`send`]).
41    pub fn from_stream(stream: TcpStream) -> Self {
42        Self {
43            stream,
44            init_sent: false,
45        }
46    }
47
48    /// Send a message with Intermediate framing.
49    pub async fn send(&mut self, data: &[u8]) -> Result<(), ConnectError> {
50        if !self.init_sent {
51            self.stream.write_all(&[0xee, 0xee, 0xee, 0xee]).await?;
52            self.init_sent = true;
53        }
54        let len = (data.len() as u32).to_le_bytes();
55        self.stream.write_all(&len).await?;
56        self.stream.write_all(data).await?;
57        Ok(())
58    }
59
60    /// Receive the next Intermediate-framed message.
61    pub async fn recv(&mut self) -> Result<Vec<u8>, ConnectError> {
62        let mut len_buf = [0u8; 4];
63        self.stream.read_exact(&mut len_buf).await?;
64        let raw = i32::from_le_bytes(len_buf);
65        if raw < 0 {
66            return Err(ConnectError::Io(std::io::Error::new(
67                std::io::ErrorKind::ConnectionRefused,
68                format!("transport error: {raw}"),
69            )));
70        }
71        let len = raw as usize;
72        let mut buf = vec![0u8; len];
73        self.stream.read_exact(&mut buf).await?;
74        Ok(buf)
75    }
76
77    pub fn into_inner(self) -> TcpStream {
78        self.stream
79    }
80}
81
82// Padded Intermediate
83
84/// [MTProto Padded Intermediate] transport framing.
85///
86/// Init tag: `0xdddddddd` (4 bytes).  Each message is sent as:
87/// `[4-byte LE length of (payload + random padding)][payload][0-15 random bytes]`
88///
89/// This is the correct framing for `0xDD` MTProxy secrets.
90///
91/// [MTProto Padded Intermediate]: https://core.telegram.org/mtproto/mtproto-transports#padded-intermediate
92pub struct PaddedIntermediateTransport {
93    stream: TcpStream,
94    init_sent: bool,
95}
96
97impl PaddedIntermediateTransport {
98    /// Connect to `addr` and lazily send the `0xDDDDDDDD` init tag on first [`send`].
99    pub async fn connect(addr: &str) -> Result<Self, ConnectError> {
100        let stream = TcpStream::connect(addr).await?;
101        Ok(Self {
102            stream,
103            init_sent: false,
104        })
105    }
106
107    /// Wrap an existing stream (the init tag will be sent on first [`send`]).
108    pub fn from_stream(stream: TcpStream) -> Self {
109        Self {
110            stream,
111            init_sent: false,
112        }
113    }
114
115    /// Send a message with Padded Intermediate framing.
116    ///
117    /// Frame layout: `[total_len: u32 LE][data][random_pad: 0-15 bytes]`
118    /// where `total_len = data.len() + pad_len`.
119    pub async fn send(&mut self, data: &[u8]) -> Result<(), ConnectError> {
120        if !self.init_sent {
121            self.stream.write_all(&[0xdd, 0xdd, 0xdd, 0xdd]).await?;
122            self.init_sent = true;
123        }
124        let mut pad_len_buf = [0u8; 1];
125        ferogram_crypto::fill_random(&mut pad_len_buf);
126        let pad_len = (pad_len_buf[0] & 0x0f) as usize;
127        let total_len = (data.len() + pad_len) as u32;
128        self.stream.write_all(&total_len.to_le_bytes()).await?;
129        self.stream.write_all(data).await?;
130        if pad_len > 0 {
131            let mut pad = vec![0u8; pad_len];
132            ferogram_crypto::fill_random(&mut pad);
133            self.stream.write_all(&pad).await?;
134        }
135        Ok(())
136    }
137
138    /// Receive the next Padded Intermediate message, stripping the random padding.
139    pub async fn recv(&mut self) -> Result<Vec<u8>, ConnectError> {
140        let mut len_buf = [0u8; 4];
141        self.stream.read_exact(&mut len_buf).await?;
142        let raw = i32::from_le_bytes(len_buf);
143        if raw < 0 {
144            return Err(ConnectError::Io(std::io::Error::new(
145                std::io::ErrorKind::ConnectionRefused,
146                format!("transport error: {raw}"),
147            )));
148        }
149        let total_len = raw as usize;
150        let mut buf = vec![0u8; total_len];
151        self.stream.read_exact(&mut buf).await?;
152        // Strip up to 15 bytes of random padding.
153        // The MTProto payload is at minimum 24 bytes (32-byte minimum decrypted frame).
154        if buf.len() >= 24 {
155            let pad = (buf.len() - 24) % 16;
156            buf.truncate(buf.len() - pad);
157        }
158        Ok(buf)
159    }
160
161    pub fn into_inner(self) -> TcpStream {
162        self.stream
163    }
164}
165
166// Full
167
168/// [MTProto Full] transport framing.
169///
170/// Extends Intermediate with:
171/// * 4-byte little-endian **sequence number** (auto-incremented per message).
172/// * 4-byte **CRC-32** at the end of each packet covering
173///   `[len][seq_no][payload]`.
174///
175/// No init byte is sent; the full format is detected by the absence of
176/// `0xef` / `0xee` in the first byte.
177///
178/// [MTProto Full]: https://core.telegram.org/mtproto/mtproto-transports#full
179pub struct FullTransport {
180    stream: TcpStream,
181    send_seqno: u32,
182    recv_seqno: u32,
183}
184
185impl FullTransport {
186    pub async fn connect(addr: &str) -> Result<Self, ConnectError> {
187        let stream = TcpStream::connect(addr).await?;
188        Ok(Self {
189            stream,
190            send_seqno: 0,
191            recv_seqno: 0,
192        })
193    }
194
195    pub fn from_stream(stream: TcpStream) -> Self {
196        Self {
197            stream,
198            send_seqno: 0,
199            recv_seqno: 0,
200        }
201    }
202
203    /// Send a message with Full framing (length + seqno + payload + crc32).
204    pub async fn send(&mut self, data: &[u8]) -> Result<(), ConnectError> {
205        let total_len = (data.len() + 12) as u32; // len field + seqno + payload + crc
206        let seq = self.send_seqno;
207        self.send_seqno = self.send_seqno.wrapping_add(1);
208
209        let mut packet = Vec::with_capacity(total_len as usize);
210        packet.extend_from_slice(&total_len.to_le_bytes());
211        packet.extend_from_slice(&seq.to_le_bytes());
212        packet.extend_from_slice(data);
213
214        let crc = crate::crc32_ieee(&packet);
215        packet.extend_from_slice(&crc.to_le_bytes());
216
217        self.stream.write_all(&packet).await?;
218        Ok(())
219    }
220
221    /// Receive the next Full-framed message; validates the CRC-32.
222    pub async fn recv(&mut self) -> Result<Vec<u8>, ConnectError> {
223        let mut len_buf = [0u8; 4];
224        self.stream.read_exact(&mut len_buf).await?;
225        // Negative value = transport-level error code from Telegram.
226        let raw = i32::from_le_bytes(len_buf);
227        if raw < 0 {
228            return Err(ConnectError::TransportCode(raw));
229        }
230        let total_len = raw as usize;
231        if total_len < 12 {
232            return Err(ConnectError::Other(
233                "Full transport: packet too short".into(),
234            ));
235        }
236        let mut rest = vec![0u8; total_len - 4];
237        self.stream.read_exact(&mut rest).await?;
238
239        // Verify CRC
240        let (body, crc_bytes) = rest.split_at(rest.len() - 4);
241        let expected_crc = u32::from_le_bytes(crc_bytes.try_into().unwrap());
242        let mut check_input = len_buf.to_vec();
243        check_input.extend_from_slice(body);
244        let actual_crc = crate::crc32_ieee(&check_input);
245        if actual_crc != expected_crc {
246            return Err(ConnectError::Other(format!(
247                "Full transport: CRC mismatch (got {actual_crc:#010x}, expected {expected_crc:#010x})"
248            )));
249        }
250
251        // seq_no is the first 4 bytes of `body`
252        let recv_seq = u32::from_le_bytes(body[..4].try_into().unwrap());
253        if recv_seq != self.recv_seqno {
254            return Err(ConnectError::Other(format!(
255                "Full transport: seq_no mismatch (got {recv_seq}, expected {})",
256                self.recv_seqno
257            )));
258        }
259        self.recv_seqno = self.recv_seqno.wrapping_add(1);
260
261        Ok(body[4..].to_vec())
262    }
263
264    pub fn into_inner(self) -> TcpStream {
265        self.stream
266    }
267}