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}