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