Skip to main content

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