Skip to main content

futu_core/
conn_ip.rs

1//! Internal shared API — C++-aligned ConnIP result policy and address catalog.
2//!
3//! Ref: `FTLogin/Src/ftlogin/channel/impl/logger.cpp:1156-1168`.
4//! C++ treats a missing `result_code` as failure and only
5//! `FTConnIP::ConnIpResultCode::CONN_IP_SUCC` (`0`) proceeds to list parsing.
6
7mod address_plan;
8
9pub use address_plan::{
10    CHANNEL_CONCURRENT_ADDRESS_LIMIT, CPP_CONCURRENCY_STAGE_DELAY, CPP_GUARANTEED_STAGE_DELAY,
11    CPP_PLATFORM_HARDCODED_BACKUP_PORT, CPP_PLATFORM_HARDCODED_PRIMARY_PORT, ChannelAddressAttempt,
12    ChannelAddressCandidate, ChannelAddressPlan, ChannelAddressPlanFacts,
13    channel_address_attempt_schedule_like_cpp, plan_channel_addresses_like_cpp,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ConnIpBackendReject {
18    result_code: i32,
19}
20
21impl ConnIpBackendReject {
22    pub const fn result_code(self) -> i32 {
23        self.result_code
24    }
25}
26
27pub fn ensure_conn_ip_backend_success(
28    result_code: Option<i32>,
29) -> std::result::Result<(), ConnIpBackendReject> {
30    match result_code {
31        Some(0) => Ok(()),
32        Some(result_code) => Err(ConnIpBackendReject { result_code }),
33        None => Err(ConnIpBackendReject { result_code: -1 }),
34    }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ConnIpAddressFacts {
39    pub ip: Option<String>,
40    pub port: Option<u32>,
41    pub region: Option<u32>,
42    pub simplified_description: Option<String>,
43    pub traditional_description: Option<String>,
44    pub english_description: Option<String>,
45    pub enable_backup_port: Option<bool>,
46    pub backup_port: Option<u32>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ConnIpAddress {
51    pub ip: String,
52    pub port: u16,
53    pub region: u32,
54    pub simplified_description: String,
55    pub traditional_description: String,
56    pub english_description: String,
57    pub enable_backup_port: bool,
58    pub backup_port: u16,
59    pub condition_flag: i32,
60}
61
62impl ConnIpAddress {
63    #[must_use]
64    pub fn endpoint(&self) -> String {
65        format!("{}:{}", self.ip, self.port)
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ConnIpSnapshotFacts {
71    pub expected_conn_identity: u32,
72    pub response_conn_identity: Option<u32>,
73    pub addresses: Vec<ConnIpAddressFacts>,
74    pub anti_ddos_ip: Option<String>,
75    pub condition_flag: Option<i32>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ConnIpSnapshot {
80    pub conn_identity: u32,
81    pub addresses: Vec<ConnIpAddress>,
82    pub anti_ddos_ip: Option<String>,
83    pub condition_flag: i32,
84}
85
86pub const CPP_MAX_PERSISTED_CONN_IP_ADDRESSES: usize = 100;
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct ConnIpCatalog {
90    pub snapshot: ConnIpSnapshot,
91    pub previous_endpoint: Option<String>,
92}
93
94impl ConnIpCatalog {
95    #[must_use]
96    pub fn new(snapshot: ConnIpSnapshot) -> Self {
97        Self {
98            snapshot,
99            previous_endpoint: None,
100        }
101    }
102
103    pub fn replace_snapshot_like_cpp(&mut self, snapshot: ConnIpSnapshot) {
104        self.snapshot = snapshot;
105    }
106
107    pub fn record_success_like_cpp(&mut self, connected_endpoint: &str) {
108        let anti_ddos_endpoint = self
109            .snapshot
110            .anti_ddos_ip
111            .as_ref()
112            .map(|ip| format!("{ip}:443"));
113        if anti_ddos_endpoint.as_deref() == Some(connected_endpoint) {
114            self.previous_endpoint = None;
115            return;
116        }
117
118        let canonical = self.snapshot.addresses.iter().find_map(|address| {
119            let primary = address.endpoint();
120            let backup = address
121                .enable_backup_port
122                .then(|| format!("{}:{}", address.ip, address.backup_port));
123            (primary == connected_endpoint || backup.as_deref() == Some(connected_endpoint))
124                .then_some(primary)
125        });
126        self.previous_endpoint = Some(canonical.unwrap_or_else(|| connected_endpoint.to_string()));
127    }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct RestoredConnIpCatalogFacts {
132    pub expected_conn_identity: u32,
133    pub snapshot: ConnIpSnapshot,
134    pub previous_endpoint: Option<String>,
135}
136
137/// Validate persisted address state using C++'s identity, size, and previous
138/// address backup-port gates.
139///
140/// Ref: `channel_address_manager.cpp:1004-1089`.
141#[must_use]
142pub fn restore_conn_ip_catalog_like_cpp(
143    facts: RestoredConnIpCatalogFacts,
144) -> Option<ConnIpCatalog> {
145    if facts.snapshot.conn_identity != facts.expected_conn_identity
146        || facts.snapshot.addresses.len() > CPP_MAX_PERSISTED_CONN_IP_ADDRESSES
147    {
148        return None;
149    }
150
151    let previous_endpoint = facts.previous_endpoint.and_then(|previous| {
152        facts
153            .snapshot
154            .addresses
155            .iter()
156            .any(|address| address.endpoint() == previous && address.enable_backup_port)
157            .then_some(previous)
158    });
159    Some(ConnIpCatalog {
160        snapshot: facts.snapshot,
161        previous_endpoint,
162    })
163}
164
165impl ConnIpSnapshot {
166    #[must_use]
167    pub fn primary_endpoints(&self) -> Vec<String> {
168        self.addresses.iter().map(ConnIpAddress::endpoint).collect()
169    }
170
171    /// Resolve the C++ login-timeout fallback for one primary endpoint.
172    ///
173    /// C++ only schedules the alternate port after the TCP connection has
174    /// succeeded but the login command times out. A disabled, zero, or equal
175    /// port is therefore not a usable fallback.
176    /// Ref: `logger.cpp:422-425,882-885`.
177    #[must_use]
178    pub fn backup_endpoint_for(&self, primary_endpoint: &str) -> Option<String> {
179        self.addresses.iter().find_map(|address| {
180            (address.endpoint() == primary_endpoint
181                && address.enable_backup_port
182                && address.backup_port != 0
183                && address.backup_port != address.port)
184                .then(|| format!("{}:{}", address.ip, address.backup_port))
185        })
186    }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum ConnIpSnapshotDecision {
191    Store(ConnIpSnapshot),
192    IgnoreIdentityMismatch { expected: u32, actual: u32 },
193    IgnoreEmptyCatalog,
194}
195
196/// Project one successful ConnIP response into the same address catalog that
197/// C++ gives `ChannelAddressManager::SetConnIpList`.
198///
199/// Ref:
200/// `f3c/FTlogin/Src/ftlogin/login/logger.cpp:2208-2275,2278-2337`
201/// and
202/// `f3c/FTlogin/Src/ftlogin/address/channel_address_manager.cpp:982-1002`.
203#[must_use]
204pub fn plan_conn_ip_snapshot_like_cpp(facts: ConnIpSnapshotFacts) -> ConnIpSnapshotDecision {
205    if let Some(actual) = facts.response_conn_identity
206        && actual != facts.expected_conn_identity
207    {
208        return ConnIpSnapshotDecision::IgnoreIdentityMismatch {
209            expected: facts.expected_conn_identity,
210            actual,
211        };
212    }
213
214    let condition_flag = facts.condition_flag.unwrap_or(0);
215    let mut addresses: Vec<_> = facts
216        .addresses
217        .into_iter()
218        .filter_map(|address| project_conn_ip_address_like_cpp(address, condition_flag))
219        .collect();
220    addresses.sort_by(|left, right| left.english_description.cmp(&right.english_description));
221    if addresses.is_empty() {
222        return ConnIpSnapshotDecision::IgnoreEmptyCatalog;
223    }
224
225    ConnIpSnapshotDecision::Store(ConnIpSnapshot {
226        conn_identity: facts.expected_conn_identity,
227        addresses,
228        anti_ddos_ip: facts.anti_ddos_ip.filter(|ip| !ip.is_empty()),
229        condition_flag,
230    })
231}
232
233fn project_conn_ip_address_like_cpp(
234    facts: ConnIpAddressFacts,
235    condition_flag: i32,
236) -> Option<ConnIpAddress> {
237    Some(ConnIpAddress {
238        ip: facts.ip?,
239        // C++ ChannelAddress defaults to 443 when ConnIpItem.port is absent.
240        // Ref: FTnet/channel/channel_address_manager.h:59.
241        port: facts.port.unwrap_or(443) as u16,
242        region: facts.region?,
243        simplified_description: facts.simplified_description?,
244        traditional_description: facts.traditional_description?,
245        english_description: facts.english_description?,
246        enable_backup_port: facts.enable_backup_port?,
247        backup_port: facts.backup_port? as u16,
248        condition_flag,
249    })
250}