Skip to main content

ferogram_mtsender/
sender.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 ferogram_connect::FrameKind;
16use ferogram_mtproto::{
17    EncryptedSession, SeenMsgIds, Session, authentication as auth, new_seen_msg_ids, step2_temp,
18};
19use ferogram_tl_types as tl;
20use ferogram_tl_types::{Cursor, Deserializable, RemoteCall};
21use tokio::io::AsyncReadExt;
22use tokio::net::TcpStream;
23
24use crate::errors::InvocationError;
25use crate::pool::{build_msgs_ack_body, build_msgs_ack_ping_body};
26use ferogram_connect::TransportKind;
27
28/// A single encrypted connection to one Telegram DC.
29/// Un-acked server msg_ids to accumulate before eagerly flushing a `msgs_ack` frame.
30const PENDING_ACKS_THRESHOLD: usize = 10;
31
32/// `PingDelayDisconnect` interval for worker connections (in GetFile chunks).
33/// Keeps the socket alive within Telegram's 75-second idle-disconnect window.
34const PING_EVERY_N_CHUNKS: u32 = 5;
35
36pub struct DcConnection {
37    stream: TcpStream,
38    enc: EncryptedSession,
39    pending_acks: Vec<i64>,
40    call_count: u32,
41    /// Active framing kind for this connection.
42    frame_kind: FrameKind,
43    /// Persistent dedup ring that outlives individual EncryptedSessions.
44    #[allow(dead_code)]
45    seen_msg_ids: SeenMsgIds,
46}
47
48impl DcConnection {
49    /// Races the default transport set (see `default_transport_race`).
50    /// Use `connect_fastest_with` to pass a custom race.
51    #[tracing::instrument(skip(socks5), fields(addr = %addr, dc_id = dc_id))]
52    pub async fn connect_fastest(
53        addr: &str,
54        socks5: Option<&ferogram_connect::Socks5Config>,
55        dc_id: i16,
56    ) -> Result<(Self, String), InvocationError> {
57        let race = ferogram_connect::default_transport_race();
58        Self::connect_fastest_with(addr, socks5, dc_id, &race).await
59    }
60
61    /// Races the given transports in parallel, each after its stagger
62    /// delay, and returns whichever finishes DH first. Others are cancelled.
63    #[tracing::instrument(skip(socks5, race), fields(addr = %addr, dc_id = dc_id))]
64    pub async fn connect_fastest_with(
65        addr: &str,
66        socks5: Option<&ferogram_connect::Socks5Config>,
67        dc_id: i16,
68        race: &[ferogram_connect::RaceLeg],
69    ) -> Result<(Self, String), InvocationError> {
70        use tokio::task::JoinSet;
71        let addr = addr.to_owned();
72        let socks5 = socks5.cloned();
73        tracing::debug!(
74            "[ferogram::sender] probing {addr} with {} transports in parallel: {:?}",
75            race.len(),
76            race.iter().map(|l| &l.transport).collect::<Vec<_>>()
77        );
78        let mut set: JoinSet<Result<(DcConnection, String), InvocationError>> = JoinSet::new();
79
80        for leg in race {
81            let a = addr.clone();
82            let s = socks5.clone();
83            let transport = leg.transport.clone();
84            let stagger = leg.stagger;
85            let label = format!("{transport:?}");
86            set.spawn(async move {
87                if !stagger.is_zero() {
88                    tokio::time::sleep(stagger).await;
89                }
90                Ok((
91                    DcConnection::connect_raw(&a, s.as_ref(), None, &transport, dc_id).await?,
92                    label,
93                ))
94            });
95        }
96
97        let mut last_err = InvocationError::Deserialize("connect_fastest: no candidates".into());
98        while let Some(outcome) = set.join_next().await {
99            match outcome {
100                Ok(Ok((conn, label))) => {
101                    set.abort_all();
102                    return Ok((conn, label));
103                }
104                Ok(Err(e)) => {
105                    last_err = e;
106                }
107                Err(e) if e.is_cancelled() => {}
108                Err(_) => {}
109            }
110        }
111        Err(last_err)
112    }
113
114    /// Connect and perform full DH handshake, optionally via `mtproxy`.
115    #[tracing::instrument(skip(socks5, mtproxy, transport), fields(addr = %addr, dc_id = dc_id))]
116    pub async fn connect_raw(
117        addr: &str,
118        socks5: Option<&ferogram_connect::Socks5Config>,
119        mtproxy: Option<&ferogram_connect::MtProxyConfig>,
120        transport: &TransportKind,
121        dc_id: i16,
122    ) -> Result<Self, InvocationError> {
123        tracing::debug!("[ferogram::sender] connecting to {addr} with known auth key");
124        let (stream, frame_kind, enc) =
125            ferogram_connect::connect_to_dc(addr, dc_id, transport, socks5, mtproxy).await?;
126
127        tracing::debug!("[ferogram::sender] DH complete, auth key established for {addr}");
128        let seen = new_seen_msg_ids();
129        Ok(Self {
130            stream,
131            frame_kind,
132            enc: EncryptedSession::with_seen(
133                enc.auth_key_bytes(),
134                enc.salt,
135                enc.time_offset,
136                seen.clone(),
137            ),
138            pending_acks: Vec::new(),
139            call_count: 0,
140            seen_msg_ids: seen,
141        })
142    }
143
144    /// Connect with an already-known auth key (no DH needed).
145    /// If `pfs` is true, performs a temp-key DH bind before any RPCs.
146    #[allow(clippy::too_many_arguments)]
147    pub async fn connect_with_key(
148        addr: &str,
149        auth_key: [u8; 256],
150        first_salt: i64,
151        time_offset: i32,
152        socks5: Option<&ferogram_connect::Socks5Config>,
153        mtproxy: Option<&ferogram_connect::MtProxyConfig>,
154        transport: &TransportKind,
155        dc_id: i16,
156        pfs: bool,
157    ) -> Result<Self, InvocationError> {
158        // ferogram-connect owns TCP open + keepalive + transport init.
159        let (mut stream, mut frame_kind) =
160            ferogram_connect::Connection::open_stream_pub(addr, dc_id, transport, socks5, mtproxy)
161                .await?;
162
163        if pfs {
164            tracing::debug!("[ferogram::sender] PFS: binding temporary key for DC{dc_id}");
165            match Self::do_pool_pfs_bind(&mut stream, &mut frame_kind, &auth_key, dc_id).await {
166                Ok(temp_enc) => {
167                    tracing::debug!("[ferogram::sender] PFS: temporary key bound for DC{dc_id}");
168                    return Ok(Self {
169                        stream,
170                        frame_kind,
171                        enc: temp_enc,
172                        pending_acks: Vec::new(),
173                        call_count: 0,
174                        seen_msg_ids: new_seen_msg_ids(),
175                    });
176                }
177                Err(e) => {
178                    tracing::warn!(
179                        "[ferogram::sender] PFS bind failed for DC{dc_id} ({e}); using permanent key"
180                    );
181                    return Err(e);
182                }
183            }
184        }
185
186        let seen = new_seen_msg_ids();
187        Ok(Self {
188            stream,
189            frame_kind,
190            enc: EncryptedSession::with_seen(auth_key, first_salt, time_offset, seen.clone()),
191            pending_acks: Vec::new(),
192            call_count: 0,
193            seen_msg_ids: seen,
194        })
195    }
196
197    /// Temp-key DH handshake + auth.bindTempAuthKey on an existing stream.
198    async fn do_pool_pfs_bind(
199        stream: &mut tokio::net::TcpStream,
200        kind: &mut FrameKind,
201        perm_auth_key: &[u8; 256],
202        dc_id: i16,
203    ) -> Result<EncryptedSession, InvocationError> {
204        use ferogram_mtproto::{
205            auth_key_id_from_key, encrypt_bind_inner, gen_msg_id, new_seen_msg_ids,
206            serialize_bind_temp_auth_key,
207        };
208        const TEMP_EXPIRES: i32 = 86_400; // 24 h
209
210        // temp-key DH
211        let mut plain = Session::new();
212
213        let (req1, s1) = auth::step1().map_err(|e| InvocationError::Deserialize(e.to_string()))?;
214        Self::send_plain_frame(stream, &plain.pack(&req1).to_plaintext_bytes(), kind).await?;
215        let res_pq: tl::enums::ResPq = Self::recv_plain_frame(stream, kind).await?;
216
217        let (req2, s2) = step2_temp(s1, res_pq, dc_id as i32, TEMP_EXPIRES)
218            .map_err(|e| InvocationError::Deserialize(e.to_string()))?;
219        Self::send_plain_frame(stream, &plain.pack(&req2).to_plaintext_bytes(), kind).await?;
220        let dh: tl::enums::ServerDhParams = Self::recv_plain_frame(stream, kind).await?;
221
222        let (req3, s3) =
223            auth::step3(s2, dh).map_err(|e| InvocationError::Deserialize(e.to_string()))?;
224        Self::send_plain_frame(stream, &plain.pack(&req3).to_plaintext_bytes(), kind).await?;
225        let ans: tl::enums::SetClientDhParamsAnswer = Self::recv_plain_frame(stream, kind).await?;
226
227        let done = {
228            let mut result =
229                auth::finish(s3, ans).map_err(|e| InvocationError::Deserialize(e.to_string()))?;
230            let mut attempts = 0u8;
231            loop {
232                match result {
233                    ferogram_mtproto::FinishResult::Done(d) => break d,
234                    ferogram_mtproto::FinishResult::Retry {
235                        retry_id,
236                        dh_params,
237                        nonce,
238                        server_nonce,
239                        new_nonce,
240                    } => {
241                        attempts += 1;
242                        if attempts >= 5 {
243                            return Err(InvocationError::Deserialize(
244                                "PFS pool temp DH retry exceeded 5".into(),
245                            ));
246                        }
247                        let (rr, s3r) = ferogram_mtproto::retry_step3(
248                            &dh_params,
249                            nonce,
250                            server_nonce,
251                            new_nonce,
252                            retry_id,
253                        )
254                        .map_err(|e| InvocationError::Deserialize(e.to_string()))?;
255                        Self::send_plain_frame(stream, &plain.pack(&rr).to_plaintext_bytes(), kind)
256                            .await?;
257                        let ar: tl::enums::SetClientDhParamsAnswer =
258                            Self::recv_plain_frame(stream, kind).await?;
259                        result = auth::finish(s3r, ar)
260                            .map_err(|e| InvocationError::Deserialize(e.to_string()))?;
261                    }
262                }
263            }
264        };
265
266        let temp_key = done.auth_key;
267        let temp_salt = done.first_salt;
268        let temp_offset = done.time_offset;
269
270        // build bindTempAuthKey body
271        let temp_key_id = auth_key_id_from_key(&temp_key);
272        let perm_key_id = auth_key_id_from_key(perm_auth_key);
273
274        let mut nonce_buf = [0u8; 8];
275        ferogram_crypto::fill_random(&mut nonce_buf);
276        let nonce = i64::from_le_bytes(nonce_buf);
277
278        let server_now = std::time::SystemTime::now()
279            .duration_since(std::time::UNIX_EPOCH)
280            .expect("system clock is before UNIX epoch")
281            .as_secs() as i32
282            + temp_offset;
283        let expires_at = server_now + TEMP_EXPIRES;
284
285        let seen = new_seen_msg_ids();
286        let mut temp_enc = EncryptedSession::with_seen(temp_key, temp_salt, temp_offset, seen);
287        let temp_session_id = temp_enc.session_id();
288
289        let msg_id = gen_msg_id();
290        let enc_msg = encrypt_bind_inner(
291            perm_auth_key,
292            msg_id,
293            nonce,
294            temp_key_id,
295            perm_key_id,
296            temp_session_id,
297            expires_at,
298        );
299        let bind_body = serialize_bind_temp_auth_key(perm_key_id, nonce, expires_at, &enc_msg);
300
301        // send encrypted bind request
302        let wire = temp_enc.pack_body_at_msg_id(&bind_body, msg_id);
303        Self::send_abridged(stream, &wire, kind).await?;
304
305        // Receive and verify response.
306        // The server may send informational frames first (msgs_ack, new_session_created)
307        // before the actual rpc_result{boolTrue}, so we loop up to 5 frames.
308        for attempt in 0u8..5 {
309            let mut raw = Self::recv_abridged(stream, kind).await?;
310            let decrypted = temp_enc.unpack(&mut raw).map_err(|e| {
311                InvocationError::Deserialize(format!("PFS pool bind decrypt: {e:?}"))
312            })?;
313            match ferogram_connect::decode_bind_response(&decrypted.body) {
314                Ok(()) => {
315                    // bindTempAuthKey succeeds under the temp key; keep the session
316                    // sequence as-is so subsequent RPCs continue from the same MTProto
317                    // message stream.
318                    return Ok(temp_enc);
319                }
320                Err(ref e) if e == "__need_more__" => {
321                    tracing::debug!(
322                        "[ferogram::sender] PFS (DC{dc_id}): got informational frame on attempt {attempt}, reading next"
323                    );
324                    continue;
325                }
326                Err(reason) => {
327                    tracing::error!(
328                        "[ferogram::sender] PFS bind rejected by server for DC{dc_id}: {reason}"
329                    );
330                    return Err(InvocationError::Deserialize(format!(
331                        "auth.bindTempAuthKey (pool): {reason}"
332                    )));
333                }
334            }
335        }
336        Err(InvocationError::Deserialize(
337            "auth.bindTempAuthKey (pool): no boolTrue after 5 frames".into(),
338        ))
339    }
340
341    /// The auth key this connection is currently encrypted with. Unlike
342    /// [`crate::MtpSender::auth_key_bytes`], there's no separate permanent
343    /// key tracked here, so under PFS this is the temporary key, not one
344    /// safe to persist to the session.
345    pub fn auth_key_bytes(&self) -> [u8; 256] {
346        self.enc.auth_key_bytes()
347    }
348    /// The server salt this connection started with.
349    pub fn first_salt(&self) -> i64 {
350        self.enc.salt
351    }
352    /// Clock offset (seconds) between this client and the server.
353    pub fn time_offset(&self) -> i32 {
354        self.enc.time_offset
355    }
356
357    /// Decompose this connection into its raw parts so it can be handed off
358    /// to [`crate::sender_task::spawn_sender_task`], graduating it from a
359    /// single-request-at-a-time `DcConnection` into a pipelined background
360    /// sender task that supports multiple concurrent in-flight requests.
361    ///
362    /// `DcPool` uses this once a connection has finished its setup (DH, PFS
363    /// bind, initConnection) as a plain `DcConnection`. ferogram's transfer
364    /// workers (`Client::open_worker_sender`) use the same pattern to enable
365    /// request pipelining on upload/download connections.
366    pub fn into_parts(self) -> (TcpStream, FrameKind, EncryptedSession) {
367        (self.stream, self.frame_kind, self.enc)
368    }
369
370    /// Send `req` and block until its matching `rpc_result` comes back,
371    /// discarding or handling anything else that arrives in between (server
372    /// pushes, the periodic keepalive ping, salt/session-reset retries).
373    /// One request at a time; `DcPool` graduates connections that need
374    /// pipelined concurrent requests into a background sender task instead
375    /// of using this directly.
376    #[tracing::instrument(skip(self, req), fields(method = std::any::type_name::<R>()))]
377    pub async fn rpc_call<R: RemoteCall>(&mut self, req: &R) -> Result<Vec<u8>, InvocationError> {
378        let _t0 = std::time::Instant::now();
379        // Periodic PingDelayDisconnect: sent before the request to piggyback on
380        // the same TCP write window.  Keeps the socket alive across the download.
381        self.call_count += 1;
382        if self.call_count.is_multiple_of(PING_EVERY_N_CHUNKS) {
383            let ping_id = self.call_count as i64;
384            let ping_body = build_msgs_ack_ping_body(ping_id);
385            // PingDelayDisconnect is content-related (returns Pong): must use odd seq_no.
386            let (ping_wire, _) = self.enc.pack_body_with_msg_id(&ping_body, true);
387            // This ping is fire-and-forget. The Pong response is a content-related
388            // server message and must be acknowledged. If the RPC result arrives before
389            // the Pong, the Pong's msg_id is never added to pending_acks. On idle
390            // connections (no subsequent RPCs) the un-acked Pong will eventually cause
391            // Telegram to close the connection. A dedicated always-running reader task
392            // that drains and acks all server messages would fix this permanently; for
393            // now the next rpc_call iteration receives and acks the Pong via pending_acks.
394            let _ = Self::send_abridged(&mut self.stream, &ping_wire, &mut self.frame_kind).await;
395        }
396
397        // Flush pending acks.
398        if !self.pending_acks.is_empty() {
399            let ack_body = build_msgs_ack_body(&self.pending_acks);
400            let (ack_wire, _) = self.enc.pack_body_with_msg_id(&ack_body, false);
401            let _ = Self::send_abridged(&mut self.stream, &ack_wire, &mut self.frame_kind).await;
402            self.pending_acks.clear();
403        }
404
405        // Track sent msg_id to verify rpc_result.req_msg_id and discard stale responses.
406        let (wire, mut sent_msg_id) = self.enc.pack_with_msg_id(req);
407        Self::send_abridged(&mut self.stream, &wire, &mut self.frame_kind).await?;
408        let mut salt_retries = 0u8;
409        let mut session_resets = 0u8;
410        loop {
411            let mut raw = Self::recv_abridged(&mut self.stream, &mut self.frame_kind).await?;
412            let msg = self
413                .enc
414                .unpack(&mut raw)
415                .map_err(|e| InvocationError::Deserialize(e.to_string()))?;
416            // Track every received msg_id for acknowledgement.
417            self.pending_acks.push(msg.msg_id);
418            if self.pending_acks.len() >= PENDING_ACKS_THRESHOLD {
419                // Eager flush: too many un-acked messages  - Telegram will close the
420                // connection if we don't ack within its window.
421                let ack_body = build_msgs_ack_body(&self.pending_acks);
422                let (ack_wire, _) = self.enc.pack_body_with_msg_id(&ack_body, false);
423                let _ =
424                    Self::send_abridged(&mut self.stream, &ack_wire, &mut self.frame_kind).await;
425                self.pending_acks.clear();
426            }
427            // Salt is updated only on explicit bad_server_salt, not on every message.
428            if msg.body.len() < 4 {
429                return Ok(msg.body);
430            }
431            let mut need_resend = false;
432            let mut need_session_reset = false;
433            let mut bad_msg_code: Option<u32> = None;
434            let mut bad_msg_server_id: Option<i64> = None;
435            // Process all flags before returning: containers may carry
436            // new_session_created + rpc_result together.
437            let scan_result = Self::scan_body(
438                &msg.body,
439                &mut self.enc.salt,
440                &mut need_resend,
441                &mut need_session_reset,
442                &mut bad_msg_code,
443                &mut bad_msg_server_id,
444                Some(sent_msg_id),
445                msg.msg_id,
446            )?;
447            // new_session_created requires seq_no reset to 0.
448            if need_session_reset {
449                session_resets += 1;
450                if session_resets > 2 {
451                    return Err(InvocationError::Deserialize(
452                        "new_session_created: exceeded 2 resets".into(),
453                    ));
454                }
455                if !self.pending_acks.is_empty() {
456                    let ack_body = build_msgs_ack_body(&self.pending_acks);
457                    let (ack_wire, _) = self.enc.pack_body_with_msg_id(&ack_body, false);
458                    let _ = Self::send_abridged(&mut self.stream, &ack_wire, &mut self.frame_kind)
459                        .await;
460                    self.pending_acks.clear();
461                }
462                // Keep the current session sequence. new_session_created updates the
463                // server salt and may require resending stale requests, but it does
464                // not require zeroing the local MTProto seq counter.
465                if scan_result.is_none() {
466                    // No result yet; resend using the current MTProto sequence.
467                    tracing::debug!(
468                        "[ferogram::sender] new_session_created: resending request (attempt {session_resets}/2)"
469                    );
470                    let (wire, new_id) = self.enc.pack_with_msg_id(req);
471                    sent_msg_id = new_id;
472                    Self::send_abridged(&mut self.stream, &wire, &mut self.frame_kind).await?;
473                }
474                // If scan_result.is_some(), the result arrived in the same container
475                // as new_session_created; session has been reset for future calls,
476                // fall through to return the result.
477            } else if need_resend {
478                // Apply seq_no / time corrections from bad_msg_notification.
479                match bad_msg_code {
480                    Some(16) | Some(17) => {
481                        if let Some(srv_id) = bad_msg_server_id {
482                            self.enc.correct_time_offset(srv_id);
483                        }
484                        // Do not call undo_seq_no here. Reusing the same seq_no on a
485                        // retry violates MTProto monotonicity; the server may reject
486                        // with code 32. Let the next pack_with_msg_id assign the next
487                        // available odd seq_no for the resent message.
488                    }
489                    Some(32) | Some(33) => {
490                        // correct_seq_no does a full session reset (new session_id,
491                        // seq_no=0) instead of magic +/- offsets.
492                        self.enc
493                            .correct_seq_no(bad_msg_code.expect("matched Some arm"));
494                    }
495                    _ => {
496                        // bad_server_salt or bad_msg code 48
497                        self.enc.undo_seq_no();
498                    }
499                }
500                salt_retries += 1;
501                if salt_retries >= 5 {
502                    return Err(InvocationError::Deserialize(
503                        "bad_server_salt/bad_msg: exceeded 5 retries".into(),
504                    ));
505                }
506                tracing::debug!(
507                    "[ferogram::sender] resending transfer request after bad_msg correction (code={bad_msg_code:?}, attempt {salt_retries}/5)"
508                );
509                if !self.pending_acks.is_empty() {
510                    let ack_body = build_msgs_ack_body(&self.pending_acks);
511                    let (ack_wire, _) = self.enc.pack_body_with_msg_id(&ack_body, false);
512                    let _ = Self::send_abridged(&mut self.stream, &ack_wire, &mut self.frame_kind)
513                        .await;
514                    self.pending_acks.clear();
515                }
516                let (wire, new_id) = self.enc.pack_with_msg_id(req);
517                sent_msg_id = new_id;
518                Self::send_abridged(&mut self.stream, &wire, &mut self.frame_kind).await?;
519            }
520            if let Some(result) = scan_result {
521                crate::metrics_shim::counter!("ferogram.rpc_calls_total", "result" => "ok")
522                    .increment(1);
523                crate::metrics_shim::histogram!("ferogram.rpc_latency_ms")
524                    .record(_t0.elapsed().as_millis() as f64);
525                return Ok(result);
526            }
527        }
528    }
529    ///
530    /// Returns `Ok(Some(bytes))` when rpc_result is found.
531    /// Returns `Ok(None)` for informational messages (continue reading).
532    /// Returns `Err` for rpc_error or parse failures.
533    ///
534    /// Output flags:
535    /// - `need_resend`: set for bad_server_salt / bad_msg_notification (codes 16/17/32/33/48)
536    /// - `need_session_reset`: set for new_session_created (seq_no must reset to 0)
537    /// - `bad_msg_code`: error_code from bad_msg_notification for caller to apply correction
538    /// - `bad_msg_server_id`: server msg_id for time-offset correction (codes 16/17)
539    /// - `server_msg_id`: outer frame msg_id for time-offset correction (codes 16/17).
540    ///   Must be msg.msg_id from the caller, not bad_msg_id (client clock, not server's).
541    #[allow(clippy::too_many_arguments)]
542    fn scan_body(
543        body: &[u8],
544        salt: &mut i64,
545        need_resend: &mut bool,
546        need_session_reset: &mut bool,
547        bad_msg_code: &mut Option<u32>,
548        bad_msg_server_id: &mut Option<i64>,
549        sent_msg_id: Option<i64>,
550        server_msg_id: i64,
551    ) -> Result<Option<Vec<u8>>, InvocationError> {
552        if body.len() < 4 {
553            return Ok(None);
554        }
555        let cid = u32::from_le_bytes(body[..4].try_into().expect("body.len() >= 4 checked above"));
556        match cid {
557            0xf35c6d01 /* rpc_result: CID(4) + req_msg_id(8) + result */ => {
558                if body.len() >= 12
559                    && let Some(expected) = sent_msg_id {
560                        let resp_id = i64::from_le_bytes(body[4..12].try_into().expect("body.len() >= 12 checked above"));
561                        if resp_id != expected {
562                            tracing::debug!(
563                                "[ferogram::sender] rpc_result msg_id mismatch (got {resp_id:#018x}, want {expected:#018x}); skipping this frame"
564                            );
565                            return Ok(None);
566                        }
567                    }
568                let inner = if body.len() >= 12 { &body[12..] } else { body };
569                // Inner body may itself be gzip_packed (e.g. help.Config inside rpc_result).
570                if inner.len() >= 4
571                    && u32::from_le_bytes(inner[..4].try_into().expect("inner.len() >= 4 checked above")) == 0x3072cfa1
572                {
573                    let mut dummy_salt = *salt;
574                    let mut nr = false; let mut nsr = false;
575                    let mut bc = None; let mut bsi = None;
576                    if let Some(r) = Self::scan_body(inner, &mut dummy_salt, &mut nr, &mut nsr, &mut bc, &mut bsi, None, server_msg_id)? {
577                        return Ok(Some(r));
578                    }
579                    // Unwrap the gzip directly and return the decompressed bytes.
580                    if let Some(compressed) = ferogram_connect::tl_read_bytes(&inner[4..])
581                        && let Ok(out) = ferogram_connect::gz_inflate(&compressed)
582                    {
583                        return Ok(Some(out));
584                    }
585                    return Ok(None);
586                }
587                if inner.len() >= 8
588                    && u32::from_le_bytes(inner[..4].try_into().expect("inner.len() >= 8 checked above")) == 0x2144ca19
589                {
590                    let code = i32::from_le_bytes(inner[4..8].try_into().expect("inner.len() >= 8 checked above"));
591                    let message = ferogram_connect::tl_read_string(&inner[8..]).unwrap_or_default();
592                    return Err(InvocationError::Rpc(
593                        crate::errors::RpcError::from_telegram(code, &message),
594                    ));
595                }
596                Ok(Some(inner.to_vec()))
597            }
598            0x2144ca19 /* rpc_error */ => {
599                if body.len() < 8 {
600                    return Err(InvocationError::Deserialize("rpc_error short".into()));
601                }
602                let code = i32::from_le_bytes(body[4..8].try_into().expect("body.len() >= 8 checked above"));
603                let message = ferogram_connect::tl_read_string(&body[8..]).unwrap_or_default();
604                Err(InvocationError::Rpc(crate::errors::RpcError::from_telegram(code, &message)))
605            }
606            0xedab447b /* bad_server_salt */ => {
607                // bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long
608                if body.len() >= 28 {
609                    let bad_msg_id = i64::from_le_bytes(body[4..12].try_into().expect("body.len() >= 28 checked above"));
610                    let new_salt   = i64::from_le_bytes(body[20..28].try_into().expect("body.len() >= 28 checked above"));
611                    // Only apply new salt when bad_msg_id matches our sent request;
612                    // stale frames from prior requests must not corrupt the current salt.
613                    if sent_msg_id.is_none_or(|id| id == bad_msg_id) {
614                        *salt = new_salt;
615                        *need_resend = true;
616                    }
617                }
618                Ok(None)
619            }
620            0x9ec20908 /* new_session_created */ => {
621                // new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long
622                // Signal need_session_reset so the caller resets seq_no before resending.
623                if body.len() >= 28 {
624                    let first_msg_id = i64::from_le_bytes(body[4..12].try_into().expect("body.len() >= 28 checked above"));
625                    let unique_id    = i64::from_le_bytes(body[12..20].try_into().expect("body.len() >= 28 checked above"));
626                    let server_salt  = i64::from_le_bytes(body[20..28].try_into().expect("body.len() >= 28 checked above"));
627                    tracing::debug!(
628                        unique_id = format_args!("{unique_id:#018x}"),
629                        first_msg_id,
630                        salt = server_salt,
631                        "[ferogram::sender] new_session_created: server opened fresh session"
632                    );
633                    *salt = server_salt;
634                    // Only reset if the pending request predates the server's new session.
635                    // If sent_msg_id == first_msg_id (fresh worker conn on first send),
636                    // the server will reply with our current session_id. Unconditionally
637                    // calling reset_session() here changes the id, causing the response
638                    // decrypt to fail with session_id mismatch.
639                    if sent_msg_id.is_some_and(|id| id < first_msg_id) {
640                        *need_session_reset = true;
641                    }
642                }
643                Ok(None)
644            }
645            0xa7eff811 /* bad_msg_notification */ => {
646                // bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int
647                //
648                // TL layout: body[4..12]=bad_msg_id, body[12..16]=bad_msg_seqno,
649                // body[16..20]=error_code. Previous code read [12..16] as error_code
650                // (bad_msg_seqno), so error matching always compared the wrong field.
651                if body.len() >= 20 {
652                    let bad_msg_id  = i64::from_le_bytes(body[4..12].try_into().expect("body.len() >= 20 checked above"));
653                    // body[12..16] = bad_msg_seqno, not used for recovery.
654                    let error_code  = u32::from_le_bytes(body[16..20].try_into().expect("body.len() >= 20 checked above"));
655                    tracing::debug!(
656                        bad_msg_id = format_args!("{bad_msg_id:#018x}"),
657                        error_code,
658                        "[ferogram::sender] bad_msg_notification received"
659                    );
660                    match error_code {
661                        16 | 17 => {
662                            // msg_id too low/high: time-offset correction needed.
663                            // server_msg_id upper 32 bits = server Unix timestamp.
664                            // bad_msg_id carries the client's clock, not the server's.
665                            *bad_msg_code = Some(error_code);
666                            *bad_msg_server_id = Some(server_msg_id);
667                            *need_resend = sent_msg_id.is_none_or(|id| id == bad_msg_id);
668                        }
669                        32 | 33 => {
670                            // seq_no wrong.
671                            *bad_msg_code = Some(error_code);
672                            *need_resend = sent_msg_id.is_none_or(|id| id == bad_msg_id);
673                        }
674                        48 => {
675                            // bad_msg code 48 = incorrect server salt. Per spec, this
676                            // arrives together with a bad_server_salt frame in the same
677                            // container that carries the new salt. If bad_server_salt was
678                            // already processed, *salt is updated and the resend uses the
679                            // correct value. If not (partial container), resend once
680                            // conservatively; the retry loop's 5-attempt cap prevents a loop.
681                            *need_resend = sent_msg_id.is_none_or(|id| id == bad_msg_id);
682                            tracing::debug!(
683                                "[ferogram::sender] bad_msg code 48 (wrong server salt): will resend with updated salt"
684                            );
685                        }
686                        _ => {
687                            // Unknown code; resend to avoid the loop stalling.
688                            *need_resend = sent_msg_id.is_none_or(|id| id == bad_msg_id);
689                        }
690                    }
691                }
692                Ok(None)
693            }
694            0x347773c5 /* pong */ => {
695                // Pong is returned for both internal PingDelayDisconnect (fire-and-forget)
696                // and user-invoked Ping (which has a pending invoke future waiting).
697                // pong layout: CID(4) + msg_id(8) + ping_id(8)
698                // pong.msg_id is the msg_id of the original ping request.
699                // Route back to the caller when it matches the pending sent_msg_id.
700                if body.len() >= 12
701                    && let Some(expected) = sent_msg_id
702                {
703                    let pong_req_id = i64::from_le_bytes(body[4..12].try_into().expect("body.len() >= 12 for pong"));
704                    if pong_req_id == expected {
705                        return Ok(Some(body.to_vec()));
706                    }
707                }
708                // Internal keepalive pong - discard.
709                Ok(None)
710            }
711            0x73f1f8dc /* msg_container */ => {
712                if body.len() < 8 {
713                    return Ok(None);
714                }
715                let count = u32::from_le_bytes(body[4..8].try_into().expect("body.len() >= 8 for msg_container")) as usize;
716                let mut pos = 8usize;
717                // Do not early-return: containers may bundle new_session_created + rpc_result
718                // together; all items must be processed so session/salt flags are observed.
719                let mut found: Option<Vec<u8>> = None;
720                for _ in 0..count {
721                    if pos + 16 > body.len() { break; }
722                    let inner_bytes =
723                        u32::from_le_bytes(body[pos + 12..pos + 16].try_into().expect("pos+16 <= body.len() checked above")) as usize;
724                    pos += 16;
725                    if pos + inner_bytes > body.len() { break; }
726                    let inner = &body[pos..pos + inner_bytes];
727                    pos += inner_bytes;
728                    if found.is_none() {
729                        if let Some(r) = Self::scan_body(inner, salt, need_resend,
730                            need_session_reset, bad_msg_code, bad_msg_server_id, sent_msg_id,
731                            server_msg_id)?
732                        {
733                            found = Some(r);
734                            // Do NOT return  - continue processing remaining items so that
735                            // session/salt flags from co-arriving messages are observed.
736                        }
737                    } else {
738                        // Result already captured; still process remaining items for
739                        // side-effect flags (salt, session reset, bad_msg). Pass
740                        // sent_msg_id so the req_msg_id guard still filters stale
741                        // rpc_results. Passing None would bypass the guard and allow
742                        // a stale response to overwrite `found` on the next iteration.
743                        let _ = Self::scan_body(inner, salt, need_resend, need_session_reset,
744                                                bad_msg_code, bad_msg_server_id, sent_msg_id,
745                                                server_msg_id)?;
746                    }
747                }
748                Ok(found)
749            }
750            0x3072cfa1 /* gzip_packed */ => {
751                // Decompress and recurse: server wraps large responses in gzip_packed.
752                if let Some(compressed) = ferogram_connect::tl_read_bytes(&body[4..])
753                    && let Ok(decompressed) = ferogram_connect::gz_inflate(&compressed)
754                    && !decompressed.is_empty()
755                {
756                    return Self::scan_body(
757                        &decompressed, salt,
758                        need_resend, need_session_reset,
759                        bad_msg_code, bad_msg_server_id,
760                        sent_msg_id,
761                        server_msg_id,
762                    );
763                }
764                Ok(None)
765            }
766            _ => Ok(None),
767        }
768    }
769
770    /// Like `rpc_call` but accepts any `Serializable` type (not just `RemoteCall`).
771    pub async fn rpc_call_serializable<S: ferogram_tl_types::Serializable>(
772        &mut self,
773        req: &S,
774    ) -> Result<Vec<u8>, InvocationError> {
775        if !self.pending_acks.is_empty() {
776            let ack_body = build_msgs_ack_body(&self.pending_acks);
777            let (ack_wire, _) = self.enc.pack_body_with_msg_id(&ack_body, false);
778            let _ = Self::send_abridged(&mut self.stream, &ack_wire, &mut self.frame_kind).await;
779            self.pending_acks.clear();
780        }
781        let (wire, mut sent_msg_id) = self.enc.pack_serializable_with_msg_id(req);
782        Self::send_abridged(&mut self.stream, &wire, &mut self.frame_kind).await?;
783        let mut salt_retries = 0u8;
784        let mut session_resets = 0u8;
785        loop {
786            let mut raw = Self::recv_abridged(&mut self.stream, &mut self.frame_kind).await?;
787            let msg = self
788                .enc
789                .unpack(&mut raw)
790                .map_err(|e| InvocationError::Deserialize(e.to_string()))?;
791            self.pending_acks.push(msg.msg_id);
792            if self.pending_acks.len() >= PENDING_ACKS_THRESHOLD {
793                let ack_body = build_msgs_ack_body(&self.pending_acks);
794                let (ack_wire, _) = self.enc.pack_body_with_msg_id(&ack_body, false);
795                let _ =
796                    Self::send_abridged(&mut self.stream, &ack_wire, &mut self.frame_kind).await;
797                self.pending_acks.clear();
798            }
799            // Salt updated only on explicit bad_server_salt, not on every message.
800            if msg.body.len() < 4 {
801                return Ok(msg.body);
802            }
803            let mut need_resend = false;
804            let mut need_session_reset = false;
805            let mut bad_msg_code: Option<u32> = None;
806            let mut bad_msg_server_id: Option<i64> = None;
807            // Save result before handling flags; apply all before returning.
808            let scan_result = Self::scan_body(
809                &msg.body,
810                &mut self.enc.salt,
811                &mut need_resend,
812                &mut need_session_reset,
813                &mut bad_msg_code,
814                &mut bad_msg_server_id,
815                Some(sent_msg_id),
816                msg.msg_id,
817            )?;
818            if need_session_reset {
819                session_resets += 1;
820                if session_resets > 2 {
821                    return Err(InvocationError::Deserialize(
822                        "new_session_created (serializable): exceeded 2 resets".into(),
823                    ));
824                }
825                if !self.pending_acks.is_empty() {
826                    let ack_body = build_msgs_ack_body(&self.pending_acks);
827                    let (ack_wire, _) = self.enc.pack_body_with_msg_id(&ack_body, false);
828                    let _ = Self::send_abridged(&mut self.stream, &ack_wire, &mut self.frame_kind)
829                        .await;
830                    self.pending_acks.clear();
831                }
832                if scan_result.is_none() {
833                    let (wire, new_id) = self.enc.pack_serializable_with_msg_id(req);
834                    sent_msg_id = new_id;
835                    Self::send_abridged(&mut self.stream, &wire, &mut self.frame_kind).await?;
836                }
837            } else if need_resend {
838                match bad_msg_code {
839                    Some(16) | Some(17) => {
840                        if let Some(srv_id) = bad_msg_server_id {
841                            self.enc.correct_time_offset(srv_id);
842                        }
843                        // Do not call undo_seq_no (see rpc_call for explanation).
844                    }
845                    Some(32) | Some(33) => {
846                        self.enc
847                            .correct_seq_no(bad_msg_code.expect("matched Some arm"));
848                    }
849                    _ => {
850                        self.enc.undo_seq_no();
851                    }
852                }
853                salt_retries += 1;
854                if salt_retries >= 5 {
855                    return Err(InvocationError::Deserialize(
856                        "bad_server_salt (serializable): exceeded 5 retries".into(),
857                    ));
858                }
859                tracing::debug!(
860                    "[ferogram::sender] resending serializable request after bad_msg correction (code={bad_msg_code:?}, attempt {salt_retries}/5)"
861                );
862                if !self.pending_acks.is_empty() {
863                    let ack_body = build_msgs_ack_body(&self.pending_acks);
864                    let (ack_wire, _) = self.enc.pack_body_with_msg_id(&ack_body, false);
865                    let _ = Self::send_abridged(&mut self.stream, &ack_wire, &mut self.frame_kind)
866                        .await;
867                    self.pending_acks.clear();
868                }
869                let (wire, new_id) = self.enc.pack_serializable_with_msg_id(req);
870                sent_msg_id = new_id;
871                Self::send_abridged(&mut self.stream, &wire, &mut self.frame_kind).await?;
872            }
873            if let Some(result) = scan_result {
874                return Ok(result);
875            }
876        }
877    }
878
879    /// Send pre-serialized raw bytes and receive the raw response.
880    /// Used by CDN download connections (no MTProto encryption layer).
881    pub async fn rpc_call_raw(&mut self, body: &[u8]) -> Result<Vec<u8>, InvocationError> {
882        Self::send_abridged(&mut self.stream, body, &mut self.frame_kind).await?;
883        Self::recv_abridged(&mut self.stream, &mut self.frame_kind).await
884    }
885
886    /// Send a framed message using the active FrameKind.
887    /// All transport variants (Abridged, Intermediate, Full, Obfuscated, …) are handled.
888    async fn send_abridged(
889        stream: &mut TcpStream,
890        data: &[u8],
891        kind: &mut FrameKind,
892    ) -> Result<(), InvocationError> {
893        use tokio::io::AsyncWriteExt as _;
894        match kind {
895            FrameKind::Abridged => {
896                let words = data.len() / 4;
897                let mut frame = if words < 0x7f {
898                    let mut v = Vec::with_capacity(1 + data.len());
899                    v.push(words as u8);
900                    v
901                } else {
902                    let mut v = Vec::with_capacity(4 + data.len());
903                    v.extend_from_slice(&[
904                        0x7f,
905                        (words & 0xff) as u8,
906                        ((words >> 8) & 0xff) as u8,
907                        ((words >> 16) & 0xff) as u8,
908                    ]);
909                    v
910                };
911                frame.extend_from_slice(data);
912                stream.write_all(&frame).await?;
913            }
914            FrameKind::Intermediate => {
915                let mut frame = Vec::with_capacity(4 + data.len());
916                frame.extend_from_slice(&(data.len() as u32).to_le_bytes());
917                frame.extend_from_slice(data);
918                stream.write_all(&frame).await?;
919            }
920            FrameKind::Full { send_seqno, .. } => {
921                let seq = send_seqno.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
922                let total_len = (data.len() as u32) + 12;
923                let mut packet = Vec::with_capacity(total_len as usize);
924                packet.extend_from_slice(&total_len.to_le_bytes());
925                packet.extend_from_slice(&seq.to_le_bytes());
926                packet.extend_from_slice(data);
927                let crc = ferogram_connect::crc32_ieee(&packet);
928                packet.extend_from_slice(&crc.to_le_bytes());
929                stream.write_all(&packet).await?;
930            }
931            FrameKind::Obfuscated { cipher } => {
932                let words = data.len() / 4;
933                let mut frame = if words < 0x7f {
934                    let mut v = Vec::with_capacity(1 + data.len());
935                    v.push(words as u8);
936                    v
937                } else {
938                    let mut v = Vec::with_capacity(4 + data.len());
939                    v.extend_from_slice(&[
940                        0x7f,
941                        (words & 0xff) as u8,
942                        ((words >> 8) & 0xff) as u8,
943                        ((words >> 16) & 0xff) as u8,
944                    ]);
945                    v
946                };
947                frame.extend_from_slice(data);
948                cipher.lock().await.encrypt(&mut frame);
949                stream.write_all(&frame).await?;
950            }
951            FrameKind::PaddedIntermediate { cipher } => {
952                let mut pad_len_buf = [0u8; 1];
953                ferogram_crypto::fill_random(&mut pad_len_buf);
954                let pad_len = (pad_len_buf[0] & 0x0f) as usize;
955                let total_payload = data.len() + pad_len;
956                let mut frame = Vec::with_capacity(4 + total_payload);
957                frame.extend_from_slice(&(total_payload as u32).to_le_bytes());
958                frame.extend_from_slice(data);
959                let mut pad = vec![0u8; pad_len];
960                ferogram_crypto::fill_random(&mut pad);
961                frame.extend_from_slice(&pad);
962                cipher.lock().await.encrypt(&mut frame);
963                stream.write_all(&frame).await?;
964            }
965            FrameKind::FakeTls { cipher, .. } => {
966                // Same PaddedIntermediate framing as `dd`, then wrapped in
967                // TLS Application Data records (see ferogram-connect's
968                // frame.rs / mtp_sender.rs for the matching read side and
969                // the handshake that establishes `cipher`). The leading
970                // ChangeCipherSpec decoy is sent once, during the
971                // handshake, not here.
972                let mut pad_len_buf = [0u8; 1];
973                ferogram_crypto::fill_random(&mut pad_len_buf);
974                let pad_len = (pad_len_buf[0] & 0x0f) as usize;
975                let total_payload = data.len() + pad_len;
976                let mut frame = Vec::with_capacity(4 + total_payload);
977                frame.extend_from_slice(&(total_payload as u32).to_le_bytes());
978                frame.extend_from_slice(data);
979                let mut pad = vec![0u8; pad_len];
980                ferogram_crypto::fill_random(&mut pad);
981                frame.extend_from_slice(&pad);
982                cipher.lock().await.encrypt(&mut frame);
983
984                let mut wire = Vec::new();
985                ferogram_connect::tls_record::wrap_application_data(&frame, &mut wire);
986                stream.write_all(&wire).await?;
987            }
988        }
989        Ok(())
990    }
991
992    /// Receive a framed message using the active FrameKind (with 60-second timeout).
993    async fn recv_abridged(
994        stream: &mut TcpStream,
995        kind: &mut FrameKind,
996    ) -> Result<Vec<u8>, InvocationError> {
997        use tokio::time::{Duration, timeout};
998        const RECV_TIMEOUT: Duration = Duration::from_secs(60);
999
1000        macro_rules! tread {
1001            ($buf:expr) => {
1002                timeout(RECV_TIMEOUT, stream.read_exact($buf))
1003                    .await
1004                    .map_err(|_| {
1005                        InvocationError::Io(std::io::Error::new(
1006                            std::io::ErrorKind::TimedOut,
1007                            "transfer recv: timeout (60 s)",
1008                        ))
1009                    })??
1010            };
1011        }
1012
1013        match kind {
1014            FrameKind::Abridged => {
1015                let mut h = [0u8; 1];
1016                tread!(&mut h);
1017                let words = if h[0] == 0x7f {
1018                    let mut b = [0u8; 3];
1019                    tread!(&mut b);
1020                    let w = b[0] as usize | (b[1] as usize) << 8 | (b[2] as usize) << 16;
1021                    if w == 1 {
1022                        let mut code_buf = [0u8; 4];
1023                        tread!(&mut code_buf);
1024                        let code = i32::from_le_bytes(code_buf);
1025                        return Err(InvocationError::Rpc(
1026                            crate::errors::RpcError::from_telegram(code, "transport error"),
1027                        ));
1028                    }
1029                    w
1030                } else {
1031                    h[0] as usize
1032                };
1033                let mut buf = vec![0u8; words * 4];
1034                tread!(&mut buf);
1035                if buf.len() == 4 {
1036                    let code = i32::from_le_bytes(buf[..4].try_into().unwrap());
1037                    if code < 0 {
1038                        return Err(InvocationError::Rpc(
1039                            crate::errors::RpcError::from_telegram(code, "transport error"),
1040                        ));
1041                    }
1042                }
1043                Ok(buf)
1044            }
1045            FrameKind::Intermediate => {
1046                let mut len_buf = [0u8; 4];
1047                tread!(&mut len_buf);
1048                let len_i32 = i32::from_le_bytes(len_buf);
1049                if len_i32 < 0 {
1050                    return Err(InvocationError::Rpc(
1051                        crate::errors::RpcError::from_telegram(len_i32, "transport error"),
1052                    ));
1053                }
1054                let mut buf = vec![0u8; len_i32 as usize];
1055                tread!(&mut buf);
1056                Ok(buf)
1057            }
1058            FrameKind::Full { recv_seqno, .. } => {
1059                let mut len_buf = [0u8; 4];
1060                tread!(&mut len_buf);
1061                let total_len_i32 = i32::from_le_bytes(len_buf);
1062                if total_len_i32 < 0 {
1063                    return Err(InvocationError::Rpc(
1064                        crate::errors::RpcError::from_telegram(total_len_i32, "transport error"),
1065                    ));
1066                }
1067                let total_len = total_len_i32 as usize;
1068                if total_len < 12 {
1069                    return Err(InvocationError::Deserialize(
1070                        "Full transport: packet too short".into(),
1071                    ));
1072                }
1073                let mut rest = vec![0u8; total_len - 4];
1074                tread!(&mut rest);
1075                let (body, crc_bytes) = rest.split_at(rest.len() - 4);
1076                let expected_crc = u32::from_le_bytes(crc_bytes.try_into().unwrap());
1077                let mut check_input = Vec::with_capacity(4 + body.len());
1078                check_input.extend_from_slice(&len_buf);
1079                check_input.extend_from_slice(body);
1080                let actual_crc = ferogram_connect::crc32_ieee(&check_input);
1081                if actual_crc != expected_crc {
1082                    return Err(InvocationError::Deserialize(format!(
1083                        "Full transport: CRC mismatch (got {actual_crc:#010x}, expected {expected_crc:#010x})"
1084                    )));
1085                }
1086                let recv_seq = u32::from_le_bytes(body[..4].try_into().unwrap());
1087                let expected_seq = recv_seqno.load(std::sync::atomic::Ordering::Relaxed);
1088                if recv_seq != expected_seq {
1089                    return Err(InvocationError::Deserialize(format!(
1090                        "Full transport: seqno mismatch (got {recv_seq}, expected {expected_seq})"
1091                    )));
1092                }
1093                recv_seqno.store(
1094                    expected_seq.wrapping_add(1),
1095                    std::sync::atomic::Ordering::Relaxed,
1096                );
1097                Ok(body[4..].to_vec())
1098            }
1099            FrameKind::Obfuscated { cipher } => {
1100                let mut h = [0u8; 1];
1101                tread!(&mut h);
1102                cipher.lock().await.decrypt(&mut h);
1103                let words = if h[0] == 0x7f {
1104                    let mut b = [0u8; 3];
1105                    tread!(&mut b);
1106                    cipher.lock().await.decrypt(&mut b);
1107                    let w = b[0] as usize | (b[1] as usize) << 8 | (b[2] as usize) << 16;
1108                    if w == 1 {
1109                        let mut code_buf = [0u8; 4];
1110                        tread!(&mut code_buf);
1111                        cipher.lock().await.decrypt(&mut code_buf);
1112                        let code = i32::from_le_bytes(code_buf);
1113                        return Err(InvocationError::Rpc(
1114                            crate::errors::RpcError::from_telegram(code, "transport error"),
1115                        ));
1116                    }
1117                    w
1118                } else {
1119                    h[0] as usize
1120                };
1121                let mut buf = vec![0u8; words * 4];
1122                tread!(&mut buf);
1123                cipher.lock().await.decrypt(&mut buf);
1124                if buf.len() == 4 {
1125                    let code = i32::from_le_bytes(buf[..4].try_into().unwrap());
1126                    if code < 0 {
1127                        return Err(InvocationError::Rpc(
1128                            crate::errors::RpcError::from_telegram(code, "transport error"),
1129                        ));
1130                    }
1131                }
1132                Ok(buf)
1133            }
1134            FrameKind::PaddedIntermediate { cipher } => {
1135                let mut len_buf = [0u8; 4];
1136                tread!(&mut len_buf);
1137                cipher.lock().await.decrypt(&mut len_buf);
1138                let total_len = i32::from_le_bytes(len_buf);
1139                if total_len < 0 {
1140                    return Err(InvocationError::Rpc(
1141                        crate::errors::RpcError::from_telegram(total_len, "transport error"),
1142                    ));
1143                }
1144                let mut buf = vec![0u8; total_len as usize];
1145                tread!(&mut buf);
1146                cipher.lock().await.decrypt(&mut buf);
1147                if buf.len() >= 24 {
1148                    let pad = (buf.len() - 24) % 16;
1149                    buf.truncate(buf.len() - pad);
1150                }
1151                Ok(buf)
1152            }
1153            FrameKind::FakeTls {
1154                cipher,
1155                decoded_pending,
1156                ..
1157            } => {
1158                async fn timed(
1159                    fut: impl std::future::Future<Output = Result<(), ferogram_connect::ConnectError>>,
1160                ) -> Result<(), InvocationError> {
1161                    timeout(RECV_TIMEOUT, fut)
1162                        .await
1163                        .map_err(|_| {
1164                            InvocationError::Io(std::io::Error::new(
1165                                std::io::ErrorKind::TimedOut,
1166                                "transfer recv: timeout (60 s)",
1167                            ))
1168                        })?
1169                        .map_err(InvocationError::from)
1170                }
1171
1172                let mut len_buf = [0u8; 4];
1173                timed(ferogram_connect::faketls_read_exact(
1174                    stream,
1175                    cipher,
1176                    decoded_pending,
1177                    &mut len_buf,
1178                ))
1179                .await?;
1180                let total_len = i32::from_le_bytes(len_buf);
1181                if total_len < 0 {
1182                    return Err(InvocationError::Rpc(
1183                        crate::errors::RpcError::from_telegram(total_len, "transport error"),
1184                    ));
1185                }
1186                let mut buf = vec![0u8; total_len as usize];
1187                timed(ferogram_connect::faketls_read_exact(
1188                    stream,
1189                    cipher,
1190                    decoded_pending,
1191                    &mut buf,
1192                ))
1193                .await?;
1194                if buf.len() >= 24 {
1195                    let pad = (buf.len() - 24) % 16;
1196                    buf.truncate(buf.len() - pad);
1197                }
1198                Ok(buf)
1199            }
1200        }
1201    }
1202
1203    /// Send a plaintext (DH handshake) frame, padding to 4-byte alignment for
1204    /// abridged-family transports. Full and Intermediate don't need padding.
1205    async fn send_plain_frame(
1206        stream: &mut TcpStream,
1207        data: &[u8],
1208        kind: &mut FrameKind,
1209    ) -> Result<(), InvocationError> {
1210        // Abridged/Obfuscated use word-count (len/4); must be 4-byte aligned.
1211        // Full and Intermediate carry the exact byte length so no padding needed.
1212        let needs_align = matches!(kind, FrameKind::Abridged | FrameKind::Obfuscated { .. });
1213        if needs_align && !data.len().is_multiple_of(4) {
1214            let mut padded = data.to_vec();
1215            let pad = 4 - (data.len() % 4);
1216            padded.resize(data.len() + pad, 0);
1217            Self::send_abridged(stream, &padded, kind).await
1218        } else {
1219            Self::send_abridged(stream, data, kind).await
1220        }
1221    }
1222
1223    async fn recv_plain_frame<T: Deserializable>(
1224        stream: &mut TcpStream,
1225        kind: &mut FrameKind,
1226    ) -> Result<T, InvocationError> {
1227        let raw = Self::recv_abridged(stream, kind).await?;
1228        if raw.len() == 4 {
1229            let code = i32::from_le_bytes(raw[..4].try_into().unwrap());
1230            if code < 0 {
1231                return Err(InvocationError::Deserialize(format!(
1232                    "server transport error during DH: code {code}"
1233                )));
1234            }
1235        }
1236        if raw.len() < 20 {
1237            return Err(InvocationError::Deserialize("plain frame too short".into()));
1238        }
1239        if u64::from_le_bytes(raw[..8].try_into().unwrap()) != 0 {
1240            return Err(InvocationError::Deserialize(
1241                "expected auth_key_id=0 in plaintext".into(),
1242            ));
1243        }
1244        let body_len = u32::from_le_bytes(raw[16..20].try_into().unwrap()) as usize;
1245        if raw.len() < 20 + body_len {
1246            return Err(InvocationError::Deserialize(format!(
1247                "plain frame truncated: have {} bytes, need {}",
1248                raw.len(),
1249                20 + body_len
1250            )));
1251        }
1252        let mut cur = Cursor::from_slice(&raw[20..20 + body_len]);
1253        T::deserialize(&mut cur).map_err(Into::into)
1254    }
1255}