1pub mod error;
2pub mod status;
3mod stream;
4
5use std::fmt::Debug;
6use std::sync::Arc;
7use std::time::Duration;
8
9use snafu::ResultExt;
10use tokio::sync::{Mutex, Semaphore};
11use tokio::task::JoinSet;
12use tokio::time::Instant;
13use tokio_util::sync::CancellationToken;
14use uni_stream::udp::set_custom_timeout;
15
16use self::error::{AcceptLocalStreamSnafu, BindLocalListenerSnafu};
17use self::status::{get_status_scoped, get_status_with_credential};
18use self::stream::{StreamSetup, handle_local_stream};
19use crate::addr::{resolve_all, resolve_tunnel_ends};
20use crate::recovery::{RecoveryTiming, jitter};
21use pb_mapper_core::checksum::{Credential, get_process_credential};
22use pb_mapper_core::config::ResolvedAddrs;
23use pb_mapper_core::config::{
24 StatusOp, client_health_check_interval, client_health_check_timeout,
25 client_health_failure_threshold,
26};
27use pb_mapper_core::timeout::RetryBackoff;
28use pb_mapper_protocol::command::{PbConnStatusReq, PbConnStatusResp};
29use pb_mapper_protocol::forward::StreamForward;
30use uni_stream::addr::ToSocketAddrs;
31use uni_stream::stream::{ListenerProvider, StreamAccept};
32
33pub type ClientStatusCallback = Box<dyn Fn(&str) + Send + Sync>;
35
36async fn resolve_ends_or_fail<A: ToSocketAddrs>(
42 local_addr: A,
43 remote_addr: A,
44 status_callback: Option<&ClientStatusCallback>,
45) -> Option<(ResolvedAddrs, ResolvedAddrs)> {
46 let resolved = resolve_tunnel_ends(local_addr, remote_addr).await;
47 if let (None, Some(callback)) = (&resolved, status_callback) {
48 callback("failed");
49 }
50 resolved
51}
52
53pub async fn run_client_side_cli<LocalListener: ListenerProvider, A: ToSocketAddrs>(
54 local_addr: A,
55 remote_addr: A,
56 key: Arc<str>,
57 keep_alive: bool,
58) where
59 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
60{
61 run_client_side_cli_with_callback::<LocalListener, A>(
62 local_addr,
63 remote_addr,
64 key,
65 keep_alive,
66 None,
67 )
68 .await
69}
70
71pub async fn run_client_side_cli_with_callback<LocalListener: ListenerProvider, A: ToSocketAddrs>(
72 local_addr: A,
73 remote_addr: A,
74 key: Arc<str>,
75 keep_alive: bool,
76 status_callback: Option<ClientStatusCallback>,
77) where
78 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
79{
80 run_client_side_cli_with_callback_scoped::<LocalListener, A>(
81 local_addr,
82 remote_addr,
83 key,
84 keep_alive,
85 None,
86 status_callback,
87 None,
88 )
89 .await
90}
91
92pub async fn run_client_side_cli_with_pinned_credential<
93 LocalListener: ListenerProvider,
94 A: ToSocketAddrs,
95>(
96 local_addr: A,
97 remote_addr: A,
98 key: Arc<str>,
99 keep_alive: bool,
100 status_callback: Option<ClientStatusCallback>,
101 credential: pb_mapper_core::checksum::Credential,
102) where
103 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
104{
105 run_client_side_cli_with_callback_scoped::<LocalListener, A>(
106 local_addr,
107 remote_addr,
108 key,
109 keep_alive,
110 None,
111 status_callback,
112 Some(credential),
113 )
114 .await
115}
116
117pub async fn run_client_side_cli_with_callback_scoped<
118 LocalListener: ListenerProvider,
119 A: ToSocketAddrs,
120>(
121 local_addr: A,
122 remote_addr: A,
123 key: Arc<str>,
124 keep_alive: bool,
125 namespace: Option<u64>,
126 status_callback: Option<ClientStatusCallback>,
127 pinned_credential: Option<pb_mapper_core::checksum::Credential>,
128) where
129 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
130{
131 let Some((local_addr, remote_addr)) =
132 resolve_ends_or_fail(local_addr, remote_addr, status_callback.as_ref()).await
133 else {
134 return;
135 };
136 run_client_side_cli_loop::<LocalListener>(
137 local_addr,
138 remote_addr,
139 key,
140 keep_alive,
141 namespace,
142 status_callback,
143 pinned_credential,
144 CancellationToken::new(),
145 )
146 .await
147}
148
149#[allow(clippy::too_many_arguments)]
155pub async fn run_client_side_cli_with_shutdown<LocalListener: ListenerProvider>(
156 local_addr: ResolvedAddrs,
157 remote_addr: ResolvedAddrs,
158 key: Arc<str>,
159 keep_alive: bool,
160 namespace: Option<u64>,
161 status_callback: Option<ClientStatusCallback>,
162 pinned_credential: Option<pb_mapper_core::checksum::Credential>,
163 shutdown: CancellationToken,
164) where
165 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
166{
167 run_client_side_cli_loop::<LocalListener>(
168 local_addr,
169 remote_addr,
170 key,
171 keep_alive,
172 namespace,
173 status_callback,
174 pinned_credential,
175 shutdown,
176 )
177 .await
178}
179
180#[allow(clippy::too_many_arguments)]
181async fn run_client_side_cli_loop<LocalListener: ListenerProvider>(
182 local_addr: ResolvedAddrs,
183 remote_addr: ResolvedAddrs,
184 key: Arc<str>,
185 keep_alive: bool,
186 namespace: Option<u64>,
187 status_callback: Option<ClientStatusCallback>,
188 pinned_credential: Option<pb_mapper_core::checksum::Credential>,
189 shutdown: CancellationToken,
190) where
191 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
192{
193 set_custom_timeout(Duration::from_secs(120));
194
195 let credential = match pinned_credential {
196 Some(credential) => credential,
197 None => match get_process_credential() {
198 Ok(credential) => credential,
199 Err(e) => {
200 tracing::error!("load client credential failed: {e}");
201 if let Some(ref callback) = status_callback {
202 callback("failed");
203 }
204 return;
205 }
206 },
207 };
208
209 let mut retry_backoff = RetryBackoff::new(Duration::from_millis(100), Duration::from_secs(2));
210 let mut stream_tasks = JoinSet::new();
211 let setup_slots = Arc::new(Semaphore::new(64));
212 let timing = Arc::new(Mutex::new(RecoveryTiming::default()));
213
214 'outer: loop {
215 let listener = tokio::select! {
218 () = shutdown.cancelled() => break,
219 result = LocalListener::bind(local_addr.as_slice()) => match result.context(BindLocalListenerSnafu) {
220 Ok(listener) => listener,
221 Err(error) => {
222 tracing::warn!(event = "client_local_bind_failed", %key, %local_addr, %error);
223 if let Some(callback) = &status_callback { callback("retrying"); }
224 tokio::select! {
225 () = shutdown.cancelled() => break,
226 () = tokio::time::sleep(jitter(retry_backoff.next_delay())) => {}
227 }
228 continue;
229 }
230 }
231 };
232 tracing::info!(event = "client_local_listener_bound", %key, %local_addr, "local listener bound; checking remote service");
233 let mut probes = JoinSet::new();
234 let mut next_probe = Instant::now();
235 let mut last_success = None;
236 let mut connected = false;
237 let mut consecutive_health_failures = 0usize;
238 let (stream_event_tx, mut stream_event_rx) = tokio::sync::mpsc::channel(1);
239
240 loop {
241 tokio::select! {
242 () = shutdown.cancelled() => break 'outer,
243 accepted = async {
244 let permit = setup_slots.clone().acquire_owned().await;
247 (permit, listener.accept().await)
248 } => {
249 let (Ok(permit), accepted) = accepted else { break 'outer; };
250 let (stream, _) = match accepted.context(AcceptLocalStreamSnafu) {
251 Ok(accepted) => accepted,
252 Err(error) => {
253 tracing::warn!(event = "client_local_accept_failed", %key, %local_addr, %error);
254 break;
255 }
256 };
257 let stream_key = key.clone();
258 let stream_remote = remote_addr.clone();
259 let stream_shutdown = shutdown.clone();
260 let event_tx = stream_event_tx.clone();
261 let setup = StreamSetup { permit, timing: timing.clone(), events: event_tx.clone() };
262 stream_tasks.spawn(async move {
263 let result = tokio::select! {
264 () = stream_shutdown.cancelled() => return,
265 result = handle_local_stream(stream, stream_key, stream_remote, keep_alive, namespace, credential, setup) => result,
266 };
267 if let Err(error) = result {
268 tracing::warn!(event = "client_local_stream_failed_before_forward", reason = %snafu::Report::from_error(error));
269 let _ = event_tx.try_send(false);
270 }
271 });
272 }
273 () = tokio::time::sleep_until(next_probe), if probes.is_empty() => {
274 let probe_remote = remote_addr.clone();
275 let probe_key = key.clone();
276 let budget = timing.lock().await.timeout().min(client_health_check_timeout());
277 probes.spawn(async move {
278 let started = Instant::now();
279 let result = probe_remote_key(&probe_remote, &probe_key, namespace, credential, budget).await;
280 (started, started.elapsed(), result)
281 });
282 }
283 Some(result) = probes.join_next() => {
284 let (started, elapsed, result) = match result {
285 Ok(result) => result,
286 Err(error) => (Instant::now(), Duration::ZERO, Err(ProbeFailure::transient(error.to_string()))),
287 };
288 match result {
289 Ok(()) => {
290 timing.lock().await.record(elapsed);
291 last_success = Some(Instant::now());
292 consecutive_health_failures = 0;
293 retry_backoff.reset();
294 next_probe = Instant::now() + client_health_check_interval();
295 if !connected {
296 connected = true;
297 if let Some(callback) = &status_callback { callback("connected"); }
298 }
299 }
300 Err(failure) if failure.permanent => {
301 if let Some(callback) = &status_callback { callback(&format!("failed: {failure}")); }
302 break 'outer;
303 }
304 Err(_) if last_success.is_some_and(|success| success > started) => {
305 next_probe = Instant::now() + client_health_check_interval();
307 }
308 Err(failure) => {
309 timing.lock().await.timed_out();
310 consecutive_health_failures = consecutive_health_failures.saturating_add(1);
311 if !connected || consecutive_health_failures >= client_health_failure_threshold() {
312 connected = false;
313 if let Some(callback) = &status_callback { callback("retrying"); }
314 }
315 let delay = jitter(retry_backoff.next_delay());
316 next_probe = Instant::now() + delay;
317 tracing::warn!(event = "client_remote_probe_failed", %key, reason = %failure, consecutive_health_failures, retry_delay = ?delay, "remote probe failed; listener remains active");
318 }
319 }
320 }
321 Some(success) = stream_event_rx.recv() => {
322 if success {
323 last_success = Some(Instant::now());
324 consecutive_health_failures = 0;
325 retry_backoff.reset();
326 if !connected {
327 connected = true;
328 if let Some(callback) = &status_callback { callback("connected"); }
329 }
330 } else if probes.is_empty() {
331 next_probe = next_probe.min(Instant::now() + Duration::from_millis(100));
334 }
335 }
336 Some(_) = stream_tasks.join_next() => {}
337 }
338 }
339 drop(probes);
340 if let Some(callback) = &status_callback {
341 callback("retrying");
342 }
343 tokio::select! {
344 () = shutdown.cancelled() => break,
345 () = tokio::time::sleep(jitter(retry_backoff.next_delay())) => {}
346 }
347 }
348
349 stream_tasks.shutdown().await;
352}
353
354#[derive(Debug)]
361struct ProbeFailure {
362 reason: String,
363 permanent: bool,
364}
365
366impl ProbeFailure {
367 fn transient(reason: impl Into<String>) -> Self {
370 Self {
371 reason: reason.into(),
372 permanent: false,
373 }
374 }
375
376 fn from_status_error(context: &str, error: crate::client::error::Error) -> Self {
379 let permanent = error.remote_retryable() == Some(false);
380 Self {
381 reason: format!("{context}: {}", snafu::Report::from_error(error)),
382 permanent,
383 }
384 }
385}
386
387impl std::fmt::Display for ProbeFailure {
388 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 formatter.write_str(&self.reason)
390 }
391}
392
393async fn probe_remote_key(
394 remote_addr: &ResolvedAddrs,
395 key: &str,
396 namespace: Option<u64>,
397 credential: Credential,
398 timeout: Duration,
399) -> std::result::Result<(), ProbeFailure> {
400 match tokio::time::timeout(
401 timeout,
402 probe_remote_key_once(remote_addr, key, namespace, credential),
403 )
404 .await
405 {
406 Ok(result) => result,
407 Err(_) => Err(ProbeFailure::transient(format!(
408 "remote key probe timed out after {timeout:?}"
409 ))),
410 }
411}
412
413async fn probe_remote_key_once(
414 remote_addr: &ResolvedAddrs,
415 key: &str,
416 namespace: Option<u64>,
417 credential: Credential,
418) -> std::result::Result<(), ProbeFailure> {
419 match fetch_remote_status(
420 remote_addr,
421 PbConnStatusReq::Service {
422 key: key.to_string(),
423 },
424 namespace,
425 credential,
426 )
427 .await
428 {
429 Ok(PbConnStatusResp::Service { connections, .. }) => {
430 if connections.iter().any(|conn| conn.healthy) {
431 return Ok(());
432 }
433 return Err(ProbeFailure::transient(format!(
434 "client key `{key}` has no healthy remote server connections"
435 )));
436 }
437 Ok(status_resp) => {
438 return Err(ProbeFailure::transient(format!(
439 "expected service status response, got {status_resp:?}"
440 )));
441 }
442 Err(failure) if failure.permanent => return Err(failure),
445 Err(service_failure) => {
446 tracing::debug!(
447 event = "client_remote_service_probe_failed",
448 key = %key,
449 remote_addr = %remote_addr,
450 reason = %service_failure,
451 "service status probe failed; falling back to key status"
452 );
453 }
454 }
455
456 let status_resp =
457 fetch_remote_status(remote_addr, PbConnStatusReq::Keys, namespace, credential).await?;
458 let PbConnStatusResp::Keys(keys) = status_resp else {
459 return Err(ProbeFailure::transient(format!(
460 "expected keys status response, got {status_resp:?}"
461 )));
462 };
463 if keys.iter().any(|candidate| candidate == key) {
464 Ok(())
465 } else {
466 Err(ProbeFailure::transient(format!(
467 "client key `{key}` is not registered on remote server; valid keys: {keys:?}"
468 )))
469 }
470}
471
472async fn fetch_remote_status(
473 remote_addr: &ResolvedAddrs,
474 req: PbConnStatusReq,
475 namespace: Option<u64>,
476 credential: Credential,
477) -> std::result::Result<PbConnStatusResp, ProbeFailure> {
478 let mut stream = crate::addr::connect_tcp(remote_addr)
479 .await
480 .map_err(|error| {
481 ProbeFailure::transient(format!("connect remote stream failed: {error}"))
482 })?;
483 get_status_with_credential(&mut stream, req, namespace, &credential)
484 .await
485 .map_err(|error| ProbeFailure::from_status_error("get status failed", error))
486}
487
488pub async fn handle_status_cli_scoped<A: ToSocketAddrs>(
489 op: StatusOp,
490 addr: A,
491 namespace: Option<u64>,
492) -> Result<(), Box<dyn std::error::Error>> {
493 match op {
494 StatusOp::RemoteId => show_status_scoped(addr, PbConnStatusReq::RemoteId, namespace).await,
495 StatusOp::Keys => show_status_scoped(addr, PbConnStatusReq::Keys, namespace).await,
496 }
497}
498
499pub async fn show_status_scoped<A: ToSocketAddrs>(
500 remote_addr: A,
501 req: PbConnStatusReq,
502 namespace: Option<u64>,
503) -> Result<(), Box<dyn std::error::Error>> {
504 let remote_addr = resolve_all(remote_addr).await?;
505 let mut stream = crate::addr::connect_tcp(&remote_addr)
506 .await
507 .map_err(|error| format!("get status stream: {error}"))?;
508 let status = get_status_scoped(&mut stream, req, namespace).await?;
509 let status = serde_json::to_string_pretty(&status)?;
510 println!("Status:{status}");
511 Ok(())
512}