Skip to main content

futu_backend/
conn_ip.rs

1// 连接 IP 列表管理(platform CMD 1321 / broker CMD 20147)
2//
3// 对应 C++ f3clogin UpdateConnIpAfterLogin
4// 登录后发送 ConnIpReq,用于后续重连 / broker channel keepalive routing
5
6use futu_core::error::Result;
7
8use crate::auth::UserAttribution;
9use crate::conn::BackendConn;
10use crate::proto_internal::ft_conn_ip;
11
12/// CMD 1321 命令 ID
13pub const CMD_CONN_IP: u16 = CMD_CONN_IP_PLATFORM;
14
15/// Platform 通道 UpdateConnIp 命令号。
16pub const CMD_CONN_IP_PLATFORM: u16 = 1321;
17
18/// Broker 通道 UpdateConnIp 命令号。
19///
20/// Ref: FTLogin `channel_impl.h:kCmdUpdateConnIpBroker=20147` +
21/// `logger.cpp::UpdateConnIpAfterLogin()` 在 broker channel 登录成功后发送。
22pub const CMD_CONN_IP_BROKER: u16 = 20147;
23
24/// 连接点信息
25#[derive(Debug, Clone)]
26pub struct ConnPoint {
27    pub ip: String,
28    pub port: u16,
29    pub region: u32,
30    pub desc: String,
31    pub backup_port: Option<u16>,
32}
33
34/// 发送 CMD 1321 获取 IP 列表
35///
36/// 对应 C++ UpdateConnIpAfterLogin。`conn_identity` 字段**必须按
37/// user_attribution 派生**(`UserAttribution::to_conn_identity()`)——
38/// HK 账号 → 6,CN → 1,US → 2 等。v1.4.20 前硬编码成 1 → HK / US / SG /
39/// AU / JP 账号都会收到 `result_code=1 "conn_identify is invalid"`。
40/// v1.4.21 起按 attribution 动态设置,对齐 C++ F3CLogin Dart 示例里的
41/// `conn_identity: <N>`(`main.dart:383-386`)配置项。
42pub async fn fetch_conn_ip_list(
43    backend: &BackendConn,
44    user_id: u64,
45    device_id: &[u8],
46    attribution: UserAttribution,
47    client_ip: &str,
48) -> Result<Vec<ConnPoint>> {
49    use prost::Message;
50    let conn_identity = attribution.to_conn_identity();
51    let req = build_conn_ip_req(user_id, device_id, conn_identity, Some(client_ip));
52
53    let body = req.encode_to_vec();
54    tracing::info!(
55        user_id,
56        ?attribution,
57        conn_identity,
58        client_ip_present = !client_ip.is_empty(),
59        body_len = body.len(),
60        "sending CMD1321 ConnIpReq"
61    );
62
63    let resp = backend.request(CMD_CONN_IP_PLATFORM, body).await?;
64    parse_conn_ip_rsp(
65        CMD_CONN_IP_PLATFORM,
66        resp.body.as_ref(),
67        Some(conn_identity),
68    )
69}
70
71/// 发送 broker 通道的 UpdateConnIp 请求。
72///
73/// C++ `logger.cpp::OnRecvNormalLoginProtocol()` 在 broker `CMD1001` 登录成功、
74/// session key / sec_data 写入后调用 `UpdateConnIpAfterLogin()`,并用
75/// `kCmdUpdateConnIpBroker=20147` 发送同一个 `FTConnIP.ConnIpReq`。该命令在
76/// `FTChannelImpl::GetCmdConfigInfo()` 里是 no-encrypt/no-sec-data;Rust 的
77/// `nn_codec::should_skip_encryption(20147)` 必须同步保持 true。
78pub async fn send_broker_conn_ip_update(
79    backend: &BackendConn,
80    customer_id: u64,
81    device_id: &[u8],
82    conn_identity: u32,
83    client_ip: &str,
84) -> Result<Vec<ConnPoint>> {
85    use prost::Message;
86
87    let req = build_conn_ip_req(customer_id, device_id, conn_identity, Some(client_ip));
88    let body = req.encode_to_vec();
89    tracing::info!(
90        customer_id,
91        conn_identity,
92        client_ip_present = !client_ip.is_empty(),
93        body_len = body.len(),
94        "sending CMD20147 broker ConnIpReq"
95    );
96
97    let resp = backend.request(CMD_CONN_IP_BROKER, body).await?;
98    parse_conn_ip_rsp(CMD_CONN_IP_BROKER, resp.body.as_ref(), Some(conn_identity))
99}
100
101fn build_conn_ip_req(
102    user_id: u64,
103    device_id: &[u8],
104    conn_identity: u32,
105    client_ip: Option<&str>,
106) -> ft_conn_ip::ConnIpReq {
107    ft_conn_ip::ConnIpReq {
108        device_id: Some(device_id.to_vec()),
109        user_id: Some(user_id),
110        net_type: Some(3), // NET_TYPE_WIRE = 3 (有线网络)
111        conn_identity: Some(conn_identity),
112        client_feature: Some(ft_conn_ip::ClientFeature {
113            device_model: Some("pc".to_string()),
114            net_type: Some("wire".to_string()),
115            carrier: None,
116            // C++ `logger.cpp:1122-1125` always calls `set_client_ip(client_ip)`.
117            // Preserve field presence when login returned an empty string; some broker
118            // paths validate the echo shape, not just the visible value.
119            client_ip: client_ip.map(|ip| ip.to_string()),
120        }),
121    }
122}
123
124fn parse_conn_ip_rsp(
125    cmd_id: u16,
126    body: &[u8],
127    expected_conn_identity: Option<u32>,
128) -> Result<Vec<ConnPoint>> {
129    use prost::Message;
130
131    let rsp: ft_conn_ip::ConnIpRsp = Message::decode(body)?;
132
133    if rsp.result_code.unwrap_or(-1) != 0 {
134        tracing::warn!(
135            cmd_id,
136            result_code = rsp.result_code.unwrap_or(-1),
137            err_msg = ?rsp.err_msg,
138            "ConnIpRsp error"
139        );
140        return Ok(vec![]);
141    }
142    if let (Some(expected), Some(actual)) = (expected_conn_identity, rsp.conn_identity)
143        && expected != actual
144    {
145        tracing::warn!(cmd_id, expected, actual, "ConnIpRsp conn_identity mismatch");
146        return Ok(vec![]);
147    }
148
149    let mut points: Vec<ConnPoint> = rsp
150        .ip_list
151        .iter()
152        .filter_map(conn_point_from_item)
153        .collect();
154    points.sort_by(|a, b| a.desc.cmp(&b.desc));
155
156    tracing::info!(
157        cmd_id,
158        count = points.len(),
159        anti_ddos = ?rsp.anti_ddos_ip,
160        "ConnIpRsp: got {} IPs",
161        points.len()
162    );
163    for (i, p) in points.iter().enumerate() {
164        tracing::info!(
165            idx = i,
166            ip = %p.ip,
167            port = p.port,
168            region = p.region,
169            desc = %p.desc,
170            "  ConnIP[{}]",
171            i
172        );
173    }
174
175    Ok(points)
176}
177
178fn conn_point_from_item(item: &ft_conn_ip::ConnIpItem) -> Option<ConnPoint> {
179    // Ref: /Users/leaf/ai-lab/o-src/FTLogin/Src/ftlogin/channel/impl/logger.cpp:1216-1280
180    // C++ `ParseConnIpList` skips items missing ip/region/sc_desc/tc_desc/en_desc/
181    // enable_backup_port/backup_port, keeps port optional with ChannelAddress default 443,
182    // and sorts by English description after parsing.
183    let ip = item.ip.clone()?;
184    item.sc_desc.as_ref()?;
185    item.tc_desc.as_ref()?;
186    let region = item.region?;
187    let desc = item.en_desc.clone()?;
188    let enable_backup_port = item.enable_backup_port?;
189    let backup_port_raw = item.backup_port?;
190
191    Some(ConnPoint {
192        ip,
193        port: item.port.unwrap_or(443) as u16,
194        region,
195        desc,
196        backup_port: enable_backup_port.then_some(backup_port_raw as u16),
197    })
198}
199
200#[cfg(test)]
201mod tests;