Skip to main content

layer_client/
dc_migration.rs

1//! DC migration helpers.
2
3use layer_tl_types as tl;
4use std::sync::Mutex;
5
6use crate::errors::InvocationError;
7
8// Static DC address table.
9// Exposes a pub fn so migrate_to and tests can reference it without hardcoding strings.
10
11/// Return the statically known IPv4 address for a Telegram DC.
12///
13/// Used as a fallback when the DC is not yet in the session's dc_options table
14/// (i.e. first migration to a DC we haven't talked to before).
15///
16/// Source: https://core.telegram.org/mtproto/DC
17pub fn fallback_dc_addr(dc_id: i32) -> &'static str {
18    match dc_id {
19        1 => "149.154.175.53:443",
20        2 => "149.154.167.51:443",
21        3 => "149.154.175.100:443",
22        4 => "149.154.167.91:443",
23        5 => "91.108.56.130:443",
24        _ => "149.154.167.51:443", // DC2 as last resort
25    }
26}
27
28/// Build the initial DC options map from the static table.
29pub fn default_dc_addresses() -> Vec<(i32, String)> {
30    (1..=5)
31        .map(|id| (id, fallback_dc_addr(id).to_string()))
32        .collect()
33}
34
35// When operating on a non-home DC (e.g. downloading from DC4 while home is DC1),
36// the client must export its auth from home and import it on the target DC.
37// We track which DCs already have a copy to avoid redundant round-trips.
38
39/// State that must live inside `ClientInner` to track which DCs already have
40/// a copy of the account's authorization key.
41pub struct DcAuthTracker {
42    copied: Mutex<Vec<i32>>,
43}
44
45impl DcAuthTracker {
46    pub fn new() -> Self {
47        Self {
48            copied: Mutex::new(Vec::new()),
49        }
50    }
51
52    /// Check if we have already copied auth to `dc_id`.
53    pub fn has_copied(&self, dc_id: i32) -> bool {
54        self.copied.lock().unwrap().contains(&dc_id)
55    }
56
57    /// Mark `dc_id` as having received a copy of the auth.
58    pub fn mark_copied(&self, dc_id: i32) {
59        self.copied.lock().unwrap().push(dc_id);
60    }
61}
62
63impl Default for DcAuthTracker {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69/// Export the home-DC authorization and import it on `target_dc_id`.
70///
71/// This is a no-op if:
72/// - `target_dc_id == home_dc_id` (already home)
73/// - auth was already copied in this session (tracked by `DcAuthTracker`)
74///
75/// Ported from  `Client::copy_auth_to_dc`.
76///
77/// # Where to call this
78///
79/// Call from `invoke_on_dc(target_dc_id, req)` before sending the request,
80/// so that file downloads on foreign DCs work without manual setup:
81///
82/// ```rust,ignore
83/// pub async fn invoke_on_dc<R: RemoteCall>(
84/// &self,
85/// dc_id: i32,
86/// req: &R,
87/// ) -> Result<R::Return, InvocationError> {
88/// self.copy_auth_to_dc(dc_id).await?;
89/// // ... then call the DC-specific connection
90/// }
91/// ```
92pub async fn copy_auth_to_dc<F, Fut>(
93    home_dc_id: i32,
94    target_dc_id: i32,
95    tracker: &DcAuthTracker,
96    invoke_fn: F, // calls the home DC
97    invoke_on_dc_fn: impl Fn(i32, tl::functions::auth::ImportAuthorization) -> Fut,
98) -> Result<(), InvocationError>
99where
100    F: std::future::Future<
101            Output = Result<tl::enums::auth::ExportedAuthorization, InvocationError>,
102        >,
103    Fut: std::future::Future<Output = Result<tl::enums::auth::Authorization, InvocationError>>,
104{
105    if target_dc_id == home_dc_id {
106        return Ok(());
107    }
108    if tracker.has_copied(target_dc_id) {
109        return Ok(());
110    }
111
112    // Export from home DC
113    let tl::enums::auth::ExportedAuthorization::ExportedAuthorization(exported) = invoke_fn.await?;
114
115    // Import on target DC
116    invoke_on_dc_fn(
117        target_dc_id,
118        tl::functions::auth::ImportAuthorization {
119            id: exported.id,
120            bytes: exported.bytes,
121        },
122    )
123    .await?;
124
125    tracker.mark_copied(target_dc_id);
126    Ok(())
127}
128
129// migrate_to integration patch
130//
131// The following documents what migrate_to must be changed to use
132// fallback_dc_addr() instead of a hardcoded string.
133//
134// In lib.rs, replace:
135//
136// .unwrap_or_else(|| "149.154.167.51:443".to_string())
137//
138// With:
139//
140// .unwrap_or_else(|| crate::dc_migration::fallback_dc_addr(new_dc_id).to_string())
141//
142// And add auto-migration to rpc_call_raw:
143
144/// Patch description for `rpc_call_raw` in lib.rs.
145///
146/// Replace the existing loop body:
147/// ```rust,ignore
148/// // BEFORE: only FLOOD_WAIT handled:
149/// async fn rpc_call_raw<R: RemoteCall>(&self, req: &R) -> Result<Vec<u8>, InvocationError> {
150/// let mut fail_count   = NonZeroU32::new(1).unwrap();
151/// let mut slept_so_far = Duration::default();
152/// loop {
153///     match self.do_rpc_call(req).await {
154///         Ok(body) => return Ok(body),
155///         Err(e) => {
156///             let ctx = RetryContext { fail_count, slept_so_far, error: e };
157///             match self.inner.retry_policy.should_retry(&ctx) {
158///                 ControlFlow::Continue(delay) => { sleep(delay).await; slept_so_far += delay; fail_count = fail_count.saturating_add(1); }
159///                 ControlFlow::Break(())       => return Err(ctx.error),
160///             }
161///         }
162///     }
163/// }
164/// }
165///
166/// // AFTER: MIGRATE auto-handled, RetryLoop used:
167/// async fn rpc_call_raw<R: RemoteCall>(&self, req: &R) -> Result<Vec<u8>, InvocationError> {
168/// let mut rl = RetryLoop::new(Arc::clone(&self.inner.retry_policy));
169/// loop {
170///     match self.do_rpc_call(req).await {
171///         Ok(body) => return Ok(body),
172///         Err(e) if let Some(dc_id) = e.migrate_dc_id() => {
173///             // Telegram is redirecting us to a different DC.
174///             // Migrate transparently and retry: no error surfaces to caller.
175///             self.migrate_to(dc_id).await?;
176///         }
177///         Err(e) => rl.advance(e).await?,
178///     }
179/// }
180/// }
181/// ```
182///
183/// With this change, the manual MIGRATE checks in `bot_sign_in`,
184/// `request_login_code`, and `sign_in` can be deleted.
185pub const MIGRATE_PATCH_DESCRIPTION: &str = "see doc comment above";
186
187// Tests
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    // fallback_dc_addr
194
195    #[test]
196    fn known_dcs_return_correct_ips() {
197        assert_eq!(fallback_dc_addr(1), "149.154.175.53:443");
198        assert_eq!(fallback_dc_addr(2), "149.154.167.51:443");
199        assert_eq!(fallback_dc_addr(3), "149.154.175.100:443");
200        assert_eq!(fallback_dc_addr(4), "149.154.167.91:443");
201        assert_eq!(fallback_dc_addr(5), "91.108.56.130:443");
202    }
203
204    #[test]
205    fn unknown_dc_falls_back_to_dc2() {
206        assert_eq!(fallback_dc_addr(99), "149.154.167.51:443");
207    }
208
209    #[test]
210    fn default_dc_addresses_has_five_entries() {
211        let addrs = default_dc_addresses();
212        assert_eq!(addrs.len(), 5);
213        // DCs 1-5 are all present
214        for id in 1..=5_i32 {
215            assert!(addrs.iter().any(|(dc_id, _)| *dc_id == id));
216        }
217    }
218
219    // DcAuthTracker
220
221    #[test]
222    fn tracker_starts_empty() {
223        let t = DcAuthTracker::new();
224        assert!(!t.has_copied(2));
225        assert!(!t.has_copied(4));
226    }
227
228    #[test]
229    fn tracker_marks_and_checks() {
230        let t = DcAuthTracker::new();
231        t.mark_copied(4);
232        assert!(t.has_copied(4));
233        assert!(!t.has_copied(2));
234    }
235
236    #[test]
237    fn tracker_marks_multiple_dcs() {
238        let t = DcAuthTracker::new();
239        t.mark_copied(2);
240        t.mark_copied(4);
241        t.mark_copied(5);
242        assert!(t.has_copied(2));
243        assert!(t.has_copied(4));
244        assert!(t.has_copied(5));
245        assert!(!t.has_copied(1));
246        assert!(!t.has_copied(3));
247    }
248
249    // migrate_dc_id detection (also in retry.rs but sanity check here)
250
251    #[test]
252    fn rpc_error_migrate_detection_all_variants() {
253        use crate::errors::RpcError;
254
255        for name in &[
256            "PHONE_MIGRATE",
257            "NETWORK_MIGRATE",
258            "FILE_MIGRATE",
259            "USER_MIGRATE",
260        ] {
261            let e = RpcError {
262                code: 303,
263                name: name.to_string(),
264                value: Some(4),
265            };
266            assert_eq!(e.migrate_dc_id(), Some(4), "failed for {name}");
267        }
268    }
269
270    #[test]
271    fn invocation_error_migrate_dc_id_delegates() {
272        use crate::errors::{InvocationError, RpcError};
273        let e = InvocationError::Rpc(RpcError {
274            code: 303,
275            name: "PHONE_MIGRATE".into(),
276            value: Some(5),
277        });
278        assert_eq!(e.migrate_dc_id(), Some(5));
279    }
280}