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