Skip to main content

ferogram_connect/
connection.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 std::sync::Arc;
16use std::time::Duration;
17
18use socket2::TcpKeepalive;
19use tokio::io::AsyncWriteExt;
20use tokio::net::TcpStream;
21
22use ferogram_mtproto::{EncryptedSession, Session, authentication as auth};
23use ferogram_tl_types as tl;
24
25use crate::error::ConnectError;
26use crate::frame::{recv_frame_plain, send_frame};
27use crate::pfs::decode_bind_response;
28use crate::transport::recv_raw_frame;
29use crate::transport_kind::TransportKind;
30
31pub const PING_DELAY_SECS: u64 = 60;
32pub const NO_PING_DISCONNECT: i32 = 75;
33
34const TCP_KEEPALIVE_IDLE_SECS: u64 = 10;
35const TCP_KEEPALIVE_INTERVAL_SECS: u64 = 5;
36#[cfg(not(target_os = "windows"))]
37const TCP_KEEPALIVE_PROBES: u32 = 3;
38
39/// How framing bytes are sent/received on a connection.
40///
41/// `Obfuscated` carries an `Arc<Mutex<ObfuscatedCipher>>` so the same cipher
42/// state is shared (safely) between the writer task (TX / `encrypt`) and the
43/// reader task (RX / `decrypt`).  The two directions are separate AES-CTR
44/// instances inside `ObfuscatedCipher`, so locking is only needed to prevent
45/// concurrent mutation of the struct, not to serialise TX vs RX.
46#[derive(Clone)]
47pub enum FrameKind {
48    Abridged,
49    Intermediate,
50    Full {
51        send_seqno: Arc<std::sync::atomic::AtomicU32>,
52        recv_seqno: Arc<std::sync::atomic::AtomicU32>,
53    },
54    /// Obfuscated2 over Abridged framing.
55    Obfuscated {
56        cipher: std::sync::Arc<tokio::sync::Mutex<ferogram_crypto::ObfuscatedCipher>>,
57    },
58    /// Obfuscated2 over Intermediate+padding framing (`0xDD` MTProxy).
59    PaddedIntermediate {
60        cipher: std::sync::Arc<tokio::sync::Mutex<ferogram_crypto::ObfuscatedCipher>>,
61    },
62    /// FakeTLS framing (`0xEE` MTProxy). Same Obfuscated2/PaddedIntermediate
63    /// transport as `PaddedIntermediate`, carried inside a decoy TLS
64    /// handshake + TLS record byte-stream framing. `cipher` is the *real*
65    /// data cipher (a fresh Obfuscated2 keypair generated after the decoy
66    /// handshake completes) -- never derived from the ClientHello HMAC.
67    FakeTls {
68        cipher: std::sync::Arc<tokio::sync::Mutex<ferogram_crypto::ObfuscatedCipher>>,
69        /// Raw wire bytes not yet unwrapped from TLS record framing (may
70        /// hold a partial trailing record between reads).
71        tls_raw_pending: std::sync::Arc<tokio::sync::Mutex<Vec<u8>>>,
72        /// Decrypted PaddedIntermediate-stream bytes extracted from
73        /// complete TLS records but not yet consumed by frame peeling.
74        decoded_pending: std::sync::Arc<tokio::sync::Mutex<Vec<u8>>>,
75    },
76}
77
78/// A single server-provided salt with its validity window.
79///
80#[derive(Clone, Debug)]
81pub struct FutureSalt {
82    pub valid_since: i32,
83    /// Stored as `u32` because Telegram sends validity windows that extend
84    /// past 2038 (e.g. valid_until ≈ 2_751_656_413, year 2057).  Those values
85    /// overflow `i32` and wrap negative, making every salt look expired when
86    /// compared against the current server time with a signed comparison.
87    pub valid_until: u32,
88    pub salt: i64,
89}
90
91/// Delay (seconds) before a salt is considered usable after its `valid_since`.
92///
93pub const SALT_USE_DELAY: i32 = 60;
94
95pub struct Connection {
96    pub stream: TcpStream,
97    pub enc: EncryptedSession,
98    pub frame_kind: FrameKind,
99    /// When PFS is active, the permanent auth key (stored in session).
100    /// `enc` holds the temp key; this field holds the perm key so
101    /// `auth_key_bytes()` returns the right value to persist.
102    pub perm_auth_key: Option<[u8; 256]>,
103}
104
105impl Connection {
106    /// Open a TCP stream, optionally via SOCKS5, and apply transport init bytes.
107    async fn open_stream(
108        addr: &str,
109        socks5: Option<&crate::socks5::Socks5Config>,
110        transport: &TransportKind,
111        dc_id: i16,
112    ) -> Result<(TcpStream, FrameKind), ConnectError> {
113        let stream = match socks5 {
114            Some(proxy) => proxy.connect(addr).await?,
115            None => {
116                let stream = TcpStream::connect(addr).await.map_err(ConnectError::Io)?;
117                stream.set_nodelay(true).ok();
118                {
119                    let sock = socket2::SockRef::from(&stream);
120                    let keepalive = TcpKeepalive::new()
121                        .with_time(Duration::from_secs(TCP_KEEPALIVE_IDLE_SECS))
122                        .with_interval(Duration::from_secs(TCP_KEEPALIVE_INTERVAL_SECS));
123                    #[cfg(not(target_os = "windows"))]
124                    let keepalive = keepalive.with_retries(TCP_KEEPALIVE_PROBES);
125                    sock.set_tcp_keepalive(&keepalive).ok();
126                }
127                stream
128            }
129        };
130        Self::apply_transport_init(stream, transport, dc_id).await
131    }
132
133    /// Open a stream routed through an MTProxy (connects to proxy host:port,
134    /// not to the Telegram DC address).
135    async fn open_stream_mtproxy(
136        mtproxy: &crate::proxy::MtProxyConfig,
137        dc_id: i16,
138    ) -> Result<(TcpStream, FrameKind), ConnectError> {
139        let stream = mtproxy.connect().await?;
140        stream.set_nodelay(true).ok();
141        Self::apply_transport_init(stream, &mtproxy.transport, dc_id).await
142    }
143
144    async fn apply_transport_init(
145        mut stream: TcpStream,
146        transport: &TransportKind,
147        dc_id: i16,
148    ) -> Result<(TcpStream, FrameKind), ConnectError> {
149        match transport {
150            TransportKind::Abridged => {
151                stream.write_all(&[0xef]).await?;
152                Ok((stream, FrameKind::Abridged))
153            }
154            TransportKind::Intermediate => {
155                stream.write_all(&[0xee, 0xee, 0xee, 0xee]).await?;
156                Ok((stream, FrameKind::Intermediate))
157            }
158            TransportKind::Full => {
159                // Full transport has no init byte.
160                Ok((
161                    stream,
162                    FrameKind::Full {
163                        send_seqno: Arc::new(std::sync::atomic::AtomicU32::new(0)),
164                        recv_seqno: Arc::new(std::sync::atomic::AtomicU32::new(0)),
165                    },
166                ))
167            }
168            TransportKind::Obfuscated { secret } => {
169                let proxy_secret = secret.as_ref().map(|s| s.as_ref());
170                let (nonce, cipher) =
171                    ferogram_crypto::build_obfuscated_init(0xef, dc_id, proxy_secret);
172                stream.write_all(&nonce).await?;
173                let cipher_arc = std::sync::Arc::new(tokio::sync::Mutex::new(cipher));
174                Ok((stream, FrameKind::Obfuscated { cipher: cipher_arc }))
175            }
176            TransportKind::PaddedIntermediate { secret } => {
177                let proxy_secret = secret.as_ref().map(|s| s.as_ref());
178                let (nonce, cipher) =
179                    ferogram_crypto::build_obfuscated_init(0xdd, dc_id, proxy_secret);
180                stream.write_all(&nonce).await?;
181                let cipher_arc = std::sync::Arc::new(tokio::sync::Mutex::new(cipher));
182                Ok((stream, FrameKind::PaddedIntermediate { cipher: cipher_arc }))
183            }
184            TransportKind::FakeTls { secret, domain } => {
185                // Real MTProxy FakeTLS is the *same* Obfuscated2/PaddedIntermediate
186                // transport used by `dd` secrets, carried inside a decoy TLS 1.3
187                // handshake + TLS record byte-stream framing so the traffic looks
188                // like ordinary HTTPS to DPI. The ClientHello HMAC below only
189                // proves we know `secret` to the proxy -- it is NOT the data
190                // cipher. The real data cipher is a fresh Obfuscated2 nonce
191                // generated *after* this decoy handshake completes (see below).
192                let domain_bytes = domain.as_bytes();
193                let mut session_id = [0u8; 32];
194                ferogram_crypto::fill_random(&mut session_id);
195
196                // GREASE values (RFC 8701): reserved cipher/extension/group/
197                // version IDs of the form 0x?A?A. A real FakeTLS/DPI-aware
198                // proxy expects to see these -- a ClientHello that omits them
199                // (and omits key_share/signature_algorithms entirely, as the
200                // previous minimal hello did) doesn't parse as a plausible
201                // TLS 1.3 ClientHello and gets dropped before any reply is
202                // sent, which is exactly the "early eof" symptom.
203                let mut grease_seed = [0u8; 4];
204                ferogram_crypto::fill_random(&mut grease_seed);
205                let grease_u16 = |b: u8| -> u16 {
206                    let v = (b & 0xf0) | 0x0a;
207                    ((v as u16) << 8) | v as u16
208                };
209                let grease_cipher = grease_u16(grease_seed[0]);
210                let grease_group = grease_u16(grease_seed[1]);
211                let grease_version = grease_u16(grease_seed[2]);
212                let grease_ext = grease_u16(grease_seed[3]);
213
214                // Build ClientHello body (random placeholder = zeros)
215                let mut cipher_suites = Vec::new();
216                cipher_suites.extend_from_slice(&grease_cipher.to_be_bytes());
217                for c in [
218                    0x1301u16, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030, 0xcca9, 0xcca8,
219                    0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035,
220                ] {
221                    cipher_suites.extend_from_slice(&c.to_be_bytes());
222                }
223                let compression: &[u8] = &[0x01, 0x00];
224
225                let sni_name_len = domain_bytes.len() as u16;
226                let sni_list_len = sni_name_len + 3;
227                let sni_ext_len = sni_list_len + 2;
228                let mut sni_ext = Vec::new();
229                sni_ext.extend_from_slice(&[0x00, 0x00]);
230                sni_ext.extend_from_slice(&sni_ext_len.to_be_bytes());
231                sni_ext.extend_from_slice(&sni_list_len.to_be_bytes());
232                sni_ext.push(0x00);
233                sni_ext.extend_from_slice(&sni_name_len.to_be_bytes());
234                sni_ext.extend_from_slice(domain_bytes);
235
236                let sup_grp: [u8; 12] = [
237                    0x00,
238                    0x0a,
239                    0x00,
240                    0x08,
241                    0x00,
242                    0x06,
243                    (grease_group >> 8) as u8,
244                    grease_group as u8,
245                    0x00,
246                    0x1d,
247                    0x00,
248                    0x17,
249                ];
250                let ec_point_fmt: &[u8] = &[0x00, 0x0b, 0x00, 0x02, 0x01, 0x00];
251                let sig_algs: &[u8] = &[
252                    0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05,
253                    0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01,
254                ];
255                let alpn: &[u8] = &[
256                    0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, b'h', b'2', 0x08, b'h', b't', b't',
257                    b'p', b'/', b'1', b'.', b'1',
258                ];
259                let ems: &[u8] = &[0x00, 0x17, 0x00, 0x00];
260                let sess_tick: &[u8] = &[0x00, 0x23, 0x00, 0x00];
261                let sup_ver: Vec<u8> = {
262                    let mut v = vec![0x00, 0x2b, 0x00, 0x05, 0x04];
263                    v.extend_from_slice(&grease_version.to_be_bytes());
264                    v.extend_from_slice(&[0x03, 0x04]);
265                    v
266                };
267                let psk_modes: &[u8] = &[0x00, 0x2d, 0x00, 0x02, 0x01, 0x01];
268                let mut key_share_pub = [0u8; 32];
269                ferogram_crypto::fill_random(&mut key_share_pub);
270                let key_share: Vec<u8> = {
271                    let mut v = vec![0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20];
272                    v.extend_from_slice(&key_share_pub);
273                    v
274                };
275                let reneg_info: &[u8] = &[0xff, 0x01, 0x00, 0x01, 0x00];
276                let grease_ext_entry: [u8; 6] = [
277                    (grease_ext >> 8) as u8,
278                    grease_ext as u8,
279                    0x00,
280                    0x02,
281                    0x00,
282                    0x00,
283                ];
284
285                let mut ext_body = Vec::new();
286                ext_body.extend_from_slice(&sni_ext);
287                ext_body.extend_from_slice(&sup_grp);
288                ext_body.extend_from_slice(ec_point_fmt);
289                ext_body.extend_from_slice(sig_algs);
290                ext_body.extend_from_slice(alpn);
291                ext_body.extend_from_slice(ems);
292                ext_body.extend_from_slice(sess_tick);
293                ext_body.extend_from_slice(&sup_ver);
294                ext_body.extend_from_slice(psk_modes);
295                ext_body.extend_from_slice(&key_share);
296                ext_body.extend_from_slice(reneg_info);
297                ext_body.extend_from_slice(&grease_ext_entry);
298
299                // Pad the whole ClientHello up to a size typical of a real
300                // browser hello (avoids a suspiciously short record, which
301                // is itself a signal a FakeTLS-detecting DPI box looks for).
302                const TARGET_HELLO_LEN: usize = 517;
303                let prefix_len = 2 /*version*/ + 32 /*random*/ + 1 /*sid len*/
304                    + session_id.len() + 2 /*cs len*/ + cipher_suites.len()
305                    + compression.len() + 2 /*ext len field*/;
306                let unpadded_total = prefix_len + ext_body.len();
307                if unpadded_total < TARGET_HELLO_LEN {
308                    let pad_needed = TARGET_HELLO_LEN - unpadded_total - 4; // minus padding ext header
309                    ext_body.extend_from_slice(&[0x00, 0x15]);
310                    ext_body.extend_from_slice(&(pad_needed as u16).to_be_bytes());
311                    ext_body.extend(std::iter::repeat_n(0u8, pad_needed));
312                }
313
314                let mut extensions = Vec::new();
315                extensions.extend_from_slice(&(ext_body.len() as u16).to_be_bytes());
316                extensions.extend_from_slice(&ext_body);
317
318                let mut hello_body = Vec::new();
319                hello_body.extend_from_slice(&[0x03, 0x03]);
320                hello_body.extend_from_slice(&[0u8; 32]); // random placeholder, filled below
321                hello_body.push(session_id.len() as u8);
322                hello_body.extend_from_slice(&session_id);
323                hello_body.extend_from_slice(&(cipher_suites.len() as u16).to_be_bytes());
324                hello_body.extend_from_slice(&cipher_suites);
325                hello_body.extend_from_slice(compression);
326                hello_body.extend_from_slice(&extensions);
327
328                let hs_len = hello_body.len() as u32;
329                let mut handshake = vec![
330                    0x01,
331                    ((hs_len >> 16) & 0xff) as u8,
332                    ((hs_len >> 8) & 0xff) as u8,
333                    (hs_len & 0xff) as u8,
334                ];
335                handshake.extend_from_slice(&hello_body);
336
337                let rec_len = handshake.len() as u16;
338                let mut record = Vec::new();
339                record.push(0x16);
340                record.extend_from_slice(&[0x03, 0x01]);
341                record.extend_from_slice(&rec_len.to_be_bytes());
342                record.extend_from_slice(&handshake);
343
344                // random field lives at: TLS-rec(5) + HS-hdr(4) + version(2) = offset 11
345                const CLIENT_RANDOM_OFFSET: usize = 5 + 4 + 2;
346                // Digest is HMAC-SHA256(secret, record-with-random-zeroed); the
347                // random field is still all-zero at this point.
348                let client_digest = ferogram_crypto::fake_tls_client_digest(secret, &record);
349                record[CLIENT_RANDOM_OFFSET..CLIENT_RANDOM_OFFSET + 32]
350                    .copy_from_slice(&client_digest);
351                stream.write_all(&record).await?;
352
353                // Real MTProxy FakeTLS servers reply with exactly three TLS
354                // records: Handshake (ServerHello), ChangeCipherSpec, and one
355                // Application Data record (padding to round out the
356                // digest-covered blob). All three are concatenated (headers
357                // included) to verify the server's digest.
358                let hello_rec = crate::tls_record::read_one_record(&mut stream)
359                    .await
360                    .map_err(ConnectError::Io)?;
361                if hello_rec.rec_type != crate::tls_record::RECORD_HANDSHAKE {
362                    return Err(ConnectError::Other(format!(
363                        "FakeTLS: expected ServerHello (Handshake) record, got type 0x{:02x}",
364                        hello_rec.rec_type
365                    )));
366                }
367                let ccs_rec = crate::tls_record::read_one_record(&mut stream)
368                    .await
369                    .map_err(ConnectError::Io)?;
370                if ccs_rec.rec_type != crate::tls_record::RECORD_CHANGE_CIPHER_SPEC {
371                    return Err(ConnectError::Other(format!(
372                        "FakeTLS: expected ChangeCipherSpec record, got type 0x{:02x}",
373                        ccs_rec.rec_type
374                    )));
375                }
376                let app_rec = crate::tls_record::read_one_record(&mut stream)
377                    .await
378                    .map_err(ConnectError::Io)?;
379                if app_rec.rec_type != crate::tls_record::RECORD_APPLICATION_DATA {
380                    return Err(ConnectError::Other(format!(
381                        "FakeTLS: expected Application Data record, got type 0x{:02x}",
382                        app_rec.rec_type
383                    )));
384                }
385
386                let mut packet = Vec::with_capacity(
387                    hello_rec.bytes.len() + ccs_rec.bytes.len() + app_rec.bytes.len(),
388                );
389                packet.extend_from_slice(&hello_rec.bytes);
390                packet.extend_from_slice(&ccs_rec.bytes);
391                packet.extend_from_slice(&app_rec.bytes);
392
393                // server random/digest lives at the same offset as the client's:
394                // TLS-rec(5) + HS-hdr(4) + version(2) = 11, within the ServerHello record.
395                const SERVER_DIGEST_OFFSET: usize = 11;
396                if packet.len() < SERVER_DIGEST_OFFSET + 32 {
397                    return Err(ConnectError::Other("FakeTLS: ServerHello too short".into()));
398                }
399                let mut server_digest = [0u8; 32];
400                server_digest
401                    .copy_from_slice(&packet[SERVER_DIGEST_OFFSET..SERVER_DIGEST_OFFSET + 32]);
402                packet[SERVER_DIGEST_OFFSET..SERVER_DIGEST_OFFSET + 32].fill(0);
403
404                if !ferogram_crypto::fake_tls_verify_server_digest(
405                    secret,
406                    &client_digest,
407                    &packet,
408                    &server_digest,
409                ) {
410                    return Err(ConnectError::Other(
411                        "FakeTLS: server digest verification failed (wrong secret, wrong \
412                         domain, or the proxy does not speak FakeTLS)"
413                            .into(),
414                    ));
415                }
416
417                // Decoy handshake verified. Now do the *real* handshake: a
418                // fresh Obfuscated2/PaddedIntermediate nonce, sent through the
419                // TLS record wrapper exactly like the plain `dd` connection
420                // start, prefixed once by the leading ChangeCipherSpec decoy
421                // record several proxies require.
422                let (nonce, cipher) =
423                    ferogram_crypto::build_obfuscated_init(0xdd, dc_id, Some(secret.as_ref()));
424
425                let mut first_write = Vec::new();
426                first_write.extend_from_slice(&crate::tls_record::change_cipher_spec_record());
427                crate::tls_record::wrap_application_data(&nonce, &mut first_write);
428                stream.write_all(&first_write).await?;
429
430                let cipher_arc = std::sync::Arc::new(tokio::sync::Mutex::new(cipher));
431                Ok((
432                    stream,
433                    FrameKind::FakeTls {
434                        cipher: cipher_arc,
435                        tls_raw_pending: std::sync::Arc::new(tokio::sync::Mutex::new(Vec::new())),
436                        decoded_pending: std::sync::Arc::new(tokio::sync::Mutex::new(Vec::new())),
437                    },
438                ))
439            }
440            TransportKind::Http => {
441                // HTTP transport is handled in dc_pool - fall back to Abridged framing.
442                stream.write_all(&[0xef]).await?;
443                Ok((stream, FrameKind::Abridged))
444            }
445        }
446    }
447
448    /// Open a TCP stream and apply transport framing, returning the stream and FrameKind.
449    ///
450    /// Used by `ferogram-mtsender` for the connect-with-key path where DH is not needed
451    /// (the auth key is already known). Socket options and transport init are handled here.
452    pub async fn open_stream_pub(
453        addr: &str,
454        dc_id: i16,
455        transport: &TransportKind,
456        socks5: Option<&crate::socks5::Socks5Config>,
457        mtproxy: Option<&crate::proxy::MtProxyConfig>,
458    ) -> Result<(TcpStream, FrameKind), ConnectError> {
459        if let Some(mp) = mtproxy {
460            Self::open_stream_mtproxy(mp, dc_id).await
461        } else {
462            Self::open_stream(addr, socks5, transport, dc_id).await
463        }
464    }
465
466    /// Open a fresh connection and run the full unauthenticated DH key
467    /// exchange to produce a brand new permanent auth key. Use
468    /// [`Self::connect_with_key`] instead once an auth key already exists
469    /// for the DC, since redoing DH on every reconnect is wasted work.
470    pub async fn connect_raw(
471        addr: &str,
472        socks5: Option<&crate::socks5::Socks5Config>,
473        mtproxy: Option<&crate::proxy::MtProxyConfig>,
474        transport: &TransportKind,
475        dc_id: i16,
476    ) -> Result<Self, ConnectError> {
477        let t_label = match transport {
478            TransportKind::Abridged => "Abridged",
479            TransportKind::Obfuscated { .. } => "Obfuscated",
480            TransportKind::PaddedIntermediate { .. } => "PaddedIntermediate",
481            TransportKind::Http => "Http",
482            TransportKind::Intermediate => "Intermediate",
483            TransportKind::Full => "Full",
484            TransportKind::FakeTls { .. } => "FakeTls",
485        };
486        tracing::debug!("[ferogram::connect] starting DH handshake with {addr} via {t_label}");
487
488        let addr2 = addr.to_string();
489        let socks5_c = socks5.cloned();
490        let mtproxy_c = mtproxy.cloned();
491        let transport_c = transport.clone();
492
493        let fut = async move {
494            let (mut stream, frame_kind) = if let Some(ref mp) = mtproxy_c {
495                Self::open_stream_mtproxy(mp, dc_id).await?
496            } else {
497                Self::open_stream(&addr2, socks5_c.as_ref(), &transport_c, dc_id).await?
498            };
499
500            let mut plain = Session::new();
501
502            let (req1, s1) = auth::step1().map_err(|e| ConnectError::other(e.to_string()))?;
503            send_frame(
504                &mut stream,
505                &plain.pack(&req1).to_plaintext_bytes(),
506                &frame_kind,
507            )
508            .await?;
509            let res_pq: tl::enums::ResPq = recv_frame_plain(&mut stream, &frame_kind).await?;
510
511            let (req2, s2) = auth::step2(s1, res_pq, dc_id as i32)
512                .map_err(|e| ConnectError::other(e.to_string()))?;
513            send_frame(
514                &mut stream,
515                &plain.pack(&req2).to_plaintext_bytes(),
516                &frame_kind,
517            )
518            .await?;
519            let dh: tl::enums::ServerDhParams = recv_frame_plain(&mut stream, &frame_kind).await?;
520
521            let (req3, s3) = auth::step3(s2, dh).map_err(|e| ConnectError::other(e.to_string()))?;
522            send_frame(
523                &mut stream,
524                &plain.pack(&req3).to_plaintext_bytes(),
525                &frame_kind,
526            )
527            .await?;
528            let ans: tl::enums::SetClientDhParamsAnswer =
529                recv_frame_plain(&mut stream, &frame_kind).await?;
530
531            // Retry loop for dh_gen_retry (up to 5 attempts).
532            let done = {
533                let mut result =
534                    auth::finish(s3, ans).map_err(|e| ConnectError::other(e.to_string()))?;
535                let mut attempts = 0u8;
536                loop {
537                    match result {
538                        auth::FinishResult::Done(d) => break d,
539                        auth::FinishResult::Retry {
540                            retry_id,
541                            dh_params,
542                            nonce,
543                            server_nonce,
544                            new_nonce,
545                        } => {
546                            attempts += 1;
547                            if attempts >= 5 {
548                                return Err(ConnectError::other(
549                                    "dh_gen_retry exceeded 5 attempts",
550                                ));
551                            }
552                            let (req_retry, s3_retry) = auth::retry_step3(
553                                &dh_params,
554                                nonce,
555                                server_nonce,
556                                new_nonce,
557                                retry_id,
558                            )
559                            .map_err(|e| ConnectError::other(e.to_string()))?;
560                            send_frame(
561                                &mut stream,
562                                &plain.pack(&req_retry).to_plaintext_bytes(),
563                                &frame_kind,
564                            )
565                            .await?;
566                            let ans_retry: tl::enums::SetClientDhParamsAnswer =
567                                recv_frame_plain(&mut stream, &frame_kind).await?;
568                            result = auth::finish(s3_retry, ans_retry)
569                                .map_err(|e| ConnectError::other(e.to_string()))?;
570                        }
571                    }
572                }
573            };
574            tracing::debug!("[ferogram::connect] DH handshake complete, auth key established");
575
576            Ok::<Self, ConnectError>(Self {
577                stream,
578                enc: EncryptedSession::new(done.auth_key, done.first_salt, done.time_offset),
579                frame_kind,
580                perm_auth_key: None, // connect_raw produces the perm key itself
581            })
582        };
583
584        tokio::time::timeout(Duration::from_secs(15), fut)
585            .await
586            .map_err(|_| {
587                ConnectError::other(format!("DH handshake with {addr} timed out after 15 s"))
588            })?
589    }
590
591    /// Open a connection using an `auth_key` already negotiated for this DC,
592    /// skipping the DH handshake entirely. If `pfs` is set, also binds a
593    /// temporary key for this session via `auth.bindTempAuthKey`, falling
594    /// back to the permanent key if the bind fails.
595    #[allow(clippy::too_many_arguments)]
596    pub async fn connect_with_key(
597        addr: &str,
598        auth_key: [u8; 256],
599        first_salt: i64,
600        time_offset: i32,
601        socks5: Option<&crate::socks5::Socks5Config>,
602        mtproxy: Option<&crate::proxy::MtProxyConfig>,
603        transport: &TransportKind,
604        dc_id: i16,
605        pfs: bool,
606    ) -> Result<Self, ConnectError> {
607        let addr2 = addr.to_string();
608        let socks5_c = socks5.cloned();
609        let mtproxy_c = mtproxy.cloned();
610        let transport_c = transport.clone();
611
612        let fut = async move {
613            let (mut stream, frame_kind) = if let Some(ref mp) = mtproxy_c {
614                Self::open_stream_mtproxy(mp, dc_id).await?
615            } else {
616                Self::open_stream(&addr2, socks5_c.as_ref(), &transport_c, dc_id).await?
617            };
618            if pfs {
619                tracing::debug!("[ferogram::connect] PFS: binding temporary key for DC{dc_id}");
620                match Self::do_pfs_bind(&mut stream, &frame_kind, &auth_key, dc_id).await {
621                    Ok(temp_enc) => {
622                        tracing::debug!(
623                            "[ferogram::connect] PFS: temporary key bound for DC{dc_id}"
624                        );
625                        return Ok(Self {
626                            stream,
627                            enc: temp_enc,
628                            frame_kind,
629                            perm_auth_key: Some(auth_key),
630                        });
631                    }
632                    Err(e) => {
633                        tracing::warn!(
634                            "[ferogram::connect] PFS bind failed for DC{dc_id} ({e}); falling back to permanent key"
635                        );
636                        // Graceful fallback: reconnect because DH frames left the stream dirty.
637                        // Return error and let the caller handle retry without PFS.
638                        return Err(e);
639                    }
640                }
641            }
642            Ok::<Self, ConnectError>(Self {
643                stream,
644                enc: EncryptedSession::new(auth_key, first_salt, time_offset),
645                frame_kind,
646                perm_auth_key: None,
647            })
648        };
649
650        tokio::time::timeout(Duration::from_secs(30), fut)
651            .await
652            .map_err(|_| {
653                ConnectError::Io(std::io::Error::new(
654                    std::io::ErrorKind::TimedOut,
655                    format!("connect_with_key to {addr} timed out after 30 s"),
656                ))
657            })?
658    }
659
660    /// Perform a fresh temp-key DH on an already-open stream, then
661    /// send `auth.bindTempAuthKey` encrypted with the temp key.
662    /// Returns an `EncryptedSession` keyed with the bound temp key.
663    async fn do_pfs_bind(
664        stream: &mut TcpStream,
665        frame_kind: &FrameKind,
666        perm_auth_key: &[u8; 256],
667        dc_id: i16,
668    ) -> Result<EncryptedSession, ConnectError> {
669        use ferogram_mtproto::{
670            auth_key_id_from_key, encrypt_bind_inner, gen_msg_id, new_seen_msg_ids,
671            serialize_bind_temp_auth_key,
672        };
673        const TEMP_EXPIRES: i32 = 86_400; // 24 h
674
675        // temp-key DH
676        let mut plain = Session::new();
677
678        let (req1, s1) = auth::step1().map_err(|e| ConnectError::other(e.to_string()))?;
679        send_frame(stream, &plain.pack(&req1).to_plaintext_bytes(), frame_kind).await?;
680        let res_pq: tl::enums::ResPq = recv_frame_plain(stream, frame_kind).await?;
681
682        let (req2, s2) = ferogram_mtproto::step2_temp(s1, res_pq, dc_id as i32, TEMP_EXPIRES)
683            .map_err(|e| ConnectError::other(e.to_string()))?;
684        send_frame(stream, &plain.pack(&req2).to_plaintext_bytes(), frame_kind).await?;
685        let dh: tl::enums::ServerDhParams = recv_frame_plain(stream, frame_kind).await?;
686
687        let (req3, s3) = auth::step3(s2, dh).map_err(|e| ConnectError::other(e.to_string()))?;
688        send_frame(stream, &plain.pack(&req3).to_plaintext_bytes(), frame_kind).await?;
689        let ans: tl::enums::SetClientDhParamsAnswer = recv_frame_plain(stream, frame_kind).await?;
690
691        let done = {
692            let mut result =
693                auth::finish(s3, ans).map_err(|e| ConnectError::other(e.to_string()))?;
694            let mut attempts = 0u8;
695            loop {
696                match result {
697                    ferogram_mtproto::FinishResult::Done(d) => break d,
698                    ferogram_mtproto::FinishResult::Retry {
699                        retry_id,
700                        dh_params,
701                        nonce,
702                        server_nonce,
703                        new_nonce,
704                    } => {
705                        attempts += 1;
706                        if attempts >= 5 {
707                            return Err(ConnectError::other(
708                                "PFS temp DH retry exceeded 5 attempts",
709                            ));
710                        }
711                        let (rr, s3r) = ferogram_mtproto::retry_step3(
712                            &dh_params,
713                            nonce,
714                            server_nonce,
715                            new_nonce,
716                            retry_id,
717                        )
718                        .map_err(|e| ConnectError::other(e.to_string()))?;
719                        send_frame(stream, &plain.pack(&rr).to_plaintext_bytes(), frame_kind)
720                            .await?;
721                        let ar: tl::enums::SetClientDhParamsAnswer =
722                            recv_frame_plain(stream, frame_kind).await?;
723                        result = auth::finish(s3r, ar)
724                            .map_err(|e| ConnectError::other(e.to_string()))?;
725                    }
726                }
727            }
728        };
729
730        let temp_key = done.auth_key;
731        let temp_salt = done.first_salt;
732        let temp_offset = done.time_offset;
733
734        // build bindTempAuthKey body
735        let temp_key_id = auth_key_id_from_key(&temp_key);
736        let perm_key_id = auth_key_id_from_key(perm_auth_key);
737
738        let mut nonce_buf = [0u8; 8];
739        ferogram_crypto::fill_random(&mut nonce_buf);
740        let nonce = i64::from_le_bytes(nonce_buf);
741
742        let server_now = std::time::SystemTime::now()
743            .duration_since(std::time::UNIX_EPOCH)
744            .unwrap()
745            .as_secs() as i32
746            + temp_offset;
747        let expires_at = server_now + TEMP_EXPIRES;
748
749        let seen = new_seen_msg_ids();
750        let mut temp_enc = EncryptedSession::with_seen(temp_key, temp_salt, temp_offset, seen);
751        let temp_session_id = temp_enc.session_id();
752
753        let msg_id = gen_msg_id();
754        let enc_msg = encrypt_bind_inner(
755            perm_auth_key,
756            msg_id,
757            nonce,
758            temp_key_id,
759            perm_key_id,
760            temp_session_id,
761            expires_at,
762        );
763        let bind_body = serialize_bind_temp_auth_key(perm_key_id, nonce, expires_at, &enc_msg);
764
765        // send encrypted bind request
766        let wire = temp_enc.pack_body_at_msg_id(&bind_body, msg_id);
767        send_frame(stream, &wire, frame_kind).await?;
768
769        // Receive and verify response.
770        // The server may send informational frames first (msgs_ack, new_session_created)
771        // before the actual rpc_result{boolTrue}, so we loop up to 5 frames.
772        for attempt in 0u8..5 {
773            let mut raw = recv_raw_frame(stream, frame_kind).await?;
774            let decrypted = temp_enc
775                .unpack(&mut raw)
776                .map_err(|e| ConnectError::other(format!("PFS bind decrypt: {e:?}")))?;
777            match decode_bind_response(&decrypted.body) {
778                Ok(()) => {
779                    // bindTempAuthKey succeeds under the temp key; keep the session
780                    // sequence as-is so subsequent RPCs continue from the same MTProto
781                    // message stream.
782                    return Ok(temp_enc);
783                }
784                Err(ref e) if e == "__need_more__" => {
785                    tracing::debug!(
786                        "[ferogram::connect] PFS (DC{dc_id}): got informational frame on attempt {attempt}, reading next"
787                    );
788                    continue;
789                }
790                Err(reason) => {
791                    tracing::error!(
792                        "[ferogram::connect] PFS bind rejected by server for DC{dc_id}: {reason}"
793                    );
794                    return Err(ConnectError::other(format!(
795                        "auth.bindTempAuthKey: {reason}"
796                    )));
797                }
798            }
799        }
800        Err(ConnectError::other(
801            "auth.bindTempAuthKey: no boolTrue after 5 frames",
802        ))
803    }
804
805    /// The permanent auth key, for persisting to the session. Under PFS this
806    /// is `perm_auth_key`, not the short-lived temp key the connection is
807    /// actually encrypted with.
808    pub fn auth_key_bytes(&self) -> [u8; 256] {
809        // When PFS is active, perm_auth_key is the key to persist in the session.
810        // enc.auth_key_bytes() would return the short-lived temp key instead.
811        self.perm_auth_key
812            .unwrap_or_else(|| self.enc.auth_key_bytes())
813    }
814
815    /// Open a TCP connection, negotiate transport framing, and complete the MTProto DH handshake.
816    ///
817    /// Returns `(stream, frame_kind, session)` as owned values. The caller is responsible for
818    /// setting up reader/writer tasks. This is the single authoritative connection path;
819    /// `ferogram-mtsender` delegates here instead of reimplementing the DH sequence.
820    pub async fn connect_to_dc(
821        addr: &str,
822        dc_id: i16,
823        transport: &TransportKind,
824        socks5: Option<&crate::socks5::Socks5Config>,
825        mtproxy: Option<&crate::proxy::MtProxyConfig>,
826    ) -> Result<(TcpStream, FrameKind, EncryptedSession), ConnectError> {
827        let conn = Self::connect_raw(addr, socks5, mtproxy, transport, dc_id).await?;
828        Ok((conn.stream, conn.frame_kind, conn.enc))
829    }
830}
831
832/// Free-function wrapper around [`Connection::connect_to_dc`].
833///
834/// Opens a TCP connection, negotiates transport framing, and completes the
835/// MTProto DH handshake. Returns `(stream, frame_kind, session)`.
836pub async fn connect_to_dc(
837    addr: &str,
838    dc_id: i16,
839    transport: &TransportKind,
840    socks5: Option<&crate::socks5::Socks5Config>,
841    mtproxy: Option<&crate::proxy::MtProxyConfig>,
842) -> Result<(TcpStream, FrameKind, EncryptedSession), ConnectError> {
843    Connection::connect_to_dc(addr, dc_id, transport, socks5, mtproxy).await
844}