Skip to main content

pb_mapper_client/client/
mod.rs

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
33// Callback for notifying status changes to external systems
34pub type ClientStatusCallback = Box<dyn Fn(&str) + Send + Sync>;
35
36/// Resolve both ends, reporting a bad address through `status_callback`.
37///
38/// The callback is the only channel a caller has here — these entry points return
39/// `()` — so an address that never resolves has to settle the tunnel as failed
40/// rather than just leaving a log line behind.
41async 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/// Same as [`run_client_side_cli_with_callback_scoped`], but the retry loop
150/// returns when `shutdown` is cancelled.
151///
152/// Takes both ends already resolved, because its caller — the SDK — resolves them
153/// itself in order to report a bad address as an error rather than a log line.
154#[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        // Listener lifetime follows the local endpoint, not a remote health
216        // snapshot. Only a local bind/accept error requires replacing it.
217        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                    // Backpressure stays in the listener backlog while setup is
245                    // saturated. Established forwarding releases its permit.
246                    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                            // Actual traffic completed after this probe began.
306                            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                        // Coalesce failures and rate limit probes independently
332                        // of the number of callers arriving during an outage.
333                        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    // Wait for the forwarding sessions to observe the cancellation, so returning
350    // from here means no stream of this tunnel is still moving bytes.
351    stream_tasks.shutdown().await;
352}
353
354/// Why a remote probe did not confirm the service, and whether waiting could
355/// change that.
356///
357/// The distinction is what keeps the retry loop from spinning: a service that has
358/// not registered yet may still appear, but a namespace the credential does not
359/// own never will.
360#[derive(Debug)]
361struct ProbeFailure {
362    reason: String,
363    permanent: bool,
364}
365
366impl ProbeFailure {
367    /// A failure worth retrying: a transport error, or a service that is simply
368    /// not registered yet.
369    fn transient(reason: impl Into<String>) -> Self {
370        Self {
371            reason: reason.into(),
372            permanent: false,
373        }
374    }
375
376    /// A status failure classified by the relay's own `retryable` verdict, which
377    /// is the only party that knows whether the refusal is final.
378    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        // A refusal the relay calls final applies to the keys probe just as much,
443        // so there is nothing to fall back to.
444        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}