1use futu_core::{
7 conn_ip::{
8 ConnIpAddressFacts, ConnIpSnapshotDecision, ConnIpSnapshotFacts,
9 ensure_conn_ip_backend_success, plan_conn_ip_snapshot_like_cpp,
10 },
11 error::Result,
12 log_redact::endpoint_log_fingerprint,
13};
14
15pub use futu_core::conn_ip::ConnIpSnapshot;
16
17mod store;
18pub use store::{
19 ConnIpCatalogStore, ConnIpCatalogStoreError, load_conn_ip_catalog, save_conn_ip_catalog,
20};
21
22use crate::auth::{UserAttribution, redact::uid_log_fingerprint};
23use crate::command_runtime::execute_connection_discovery;
24use crate::conn::BackendConn;
25use crate::proto_internal::ft_conn_ip;
26use futu_command_spec::{
27 CMD_CORE_PULL_CONN_IP as CMD_CONN_IP_PLATFORM,
28 CMD_FTLOGIN_UPDATE_CONN_IP_BROKER as CMD_CONN_IP_BROKER, ConnectionDiscoveryOperation,
29};
30
31pub async fn fetch_conn_ip_list(
40 backend: &BackendConn,
41 user_id: u64,
42 device_id: &[u8],
43 attribution: UserAttribution,
44 client_ip: &str,
45) -> Result<Option<ConnIpSnapshot>> {
46 use prost::Message;
47 let conn_identity = attribution.to_conn_identity();
48 let req = build_conn_ip_req(user_id, device_id, conn_identity, Some(client_ip));
49
50 let body = req.encode_to_vec();
51 tracing::info!(
52 user_id_fp = %uid_log_fingerprint(user_id),
53 ?attribution,
54 conn_identity,
55 client_ip_present = !client_ip.is_empty(),
56 body_len = body.len(),
57 "sending CMD1321 ConnIpReq"
58 );
59
60 let resp = execute_connection_discovery(
61 backend,
62 ConnectionDiscoveryOperation::PlatformConnIp,
63 body.into(),
64 )
65 .await?;
66 parse_conn_ip_rsp(CMD_CONN_IP_PLATFORM, resp.body.as_ref(), conn_identity)
67}
68
69pub async fn send_broker_conn_ip_update(
77 backend: &BackendConn,
78 customer_id: u64,
79 device_id: &[u8],
80 conn_identity: u32,
81 client_ip: &str,
82) -> Result<Option<ConnIpSnapshot>> {
83 use prost::Message;
84
85 let req = build_conn_ip_req(customer_id, device_id, conn_identity, Some(client_ip));
86 let body = req.encode_to_vec();
87 tracing::info!(
88 customer_id_fp = %uid_log_fingerprint(customer_id),
89 conn_identity,
90 client_ip_present = !client_ip.is_empty(),
91 body_len = body.len(),
92 "sending CMD20147 broker ConnIpReq"
93 );
94
95 let resp = execute_connection_discovery(
96 backend,
97 ConnectionDiscoveryOperation::BrokerConnIp,
98 body.into(),
99 )
100 .await?;
101 parse_conn_ip_rsp(CMD_CONN_IP_BROKER, resp.body.as_ref(), conn_identity)
102}
103
104fn build_conn_ip_req(
105 user_id: u64,
106 device_id: &[u8],
107 conn_identity: u32,
108 client_ip: Option<&str>,
109) -> ft_conn_ip::ConnIpReq {
110 ft_conn_ip::ConnIpReq {
114 device_id: Some(device_id.to_vec()),
115 user_id: Some(user_id),
116 net_type: Some(3), conn_identity: Some(conn_identity),
118 client_feature: Some(ft_conn_ip::ClientFeature {
119 device_model: Some(crate::auth::device_type().to_string()),
120 net_type: Some("LAN".to_string()),
121 carrier: Some(String::new()),
122 client_ip: client_ip.map(|ip| ip.to_string()),
126 }),
127 }
128}
129
130fn parse_conn_ip_rsp(
131 cmd_id: u16,
132 body: &[u8],
133 expected_conn_identity: u32,
134) -> Result<Option<ConnIpSnapshot>> {
135 use prost::Message;
136
137 let rsp: ft_conn_ip::ConnIpRsp = Message::decode(body)?;
138
139 if let Err(err) = ensure_conn_ip_backend_success(rsp.result_code) {
140 tracing::warn!(
141 cmd_id,
142 result_code = err.result_code(),
143 err_msg = ?rsp.err_msg,
144 "ConnIpRsp error"
145 );
146 return Ok(None);
147 }
148
149 let decision = plan_conn_ip_snapshot_like_cpp(ConnIpSnapshotFacts {
150 expected_conn_identity,
151 response_conn_identity: rsp.conn_identity,
152 addresses: rsp.ip_list.iter().map(conn_ip_address_facts).collect(),
153 anti_ddos_ip: rsp.anti_ddos_ip,
154 condition_flag: rsp.condition_flag,
155 });
156 match decision {
157 ConnIpSnapshotDecision::Store(snapshot) => {
158 tracing::info!(
159 cmd_id,
160 conn_identity = snapshot.conn_identity,
161 count = snapshot.addresses.len(),
162 anti_ddos_present = snapshot.anti_ddos_ip.is_some(),
163 condition_flag = snapshot.condition_flag,
164 "ConnIpRsp catalog accepted"
165 );
166 for (index, address) in snapshot.addresses.iter().enumerate() {
167 tracing::debug!(
168 index,
169 endpoint_fp = %endpoint_log_fingerprint(&address.endpoint()),
170 region = address.region,
171 english_description = %address.english_description,
172 enable_backup_port = address.enable_backup_port,
173 backup_port = address.backup_port,
174 "ConnIP address"
175 );
176 }
177 Ok(Some(snapshot))
178 }
179 ConnIpSnapshotDecision::IgnoreIdentityMismatch { expected, actual } => {
180 tracing::warn!(cmd_id, expected, actual, "ConnIpRsp conn_identity mismatch");
181 Ok(None)
182 }
183 ConnIpSnapshotDecision::IgnoreEmptyCatalog => {
184 tracing::warn!(
185 cmd_id,
186 expected_conn_identity,
187 "ConnIpRsp empty catalog ignored"
188 );
189 Ok(None)
190 }
191 }
192}
193
194fn conn_ip_address_facts(item: &ft_conn_ip::ConnIpItem) -> ConnIpAddressFacts {
195 ConnIpAddressFacts {
196 ip: item.ip.clone(),
197 port: item.port,
198 region: item.region,
199 simplified_description: item.sc_desc.clone(),
200 traditional_description: item.tc_desc.clone(),
201 english_description: item.en_desc.clone(),
202 enable_backup_port: item.enable_backup_port,
203 backup_port: item.backup_port,
204 }
205}
206
207#[cfg(test)]
208mod store_tests;
209#[cfg(test)]
210mod tests;