Skip to main content

pb_mapper_client/server/
mod.rs

1use crate::diagnostics::{Diagnostics, RecoveryFailure, RecoveryPhase};
2use crate::endpoint::{RecoveryWake, RelayEndpoint};
3use crate::relay::CarrierId;
4pub mod error;
5mod stream;
6
7use std::fmt::Debug;
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
10
11use snafu::ResultExt;
12use tokio::task::JoinSet;
13use tokio::time::MissedTickBehavior;
14use tokio_util::sync::CancellationToken;
15use tracing::instrument;
16
17use self::error::{
18    ControlIoTimeoutSnafu, DecodeRegisterRespSnafu, DecodeStreamReqSnafu, EncodePingMsgSnafu,
19    EncodeRegisterReqSnafu, EncodeStreamAckMsgSnafu, ReadRegisterRespSnafu, ReadStreamReqSnafu,
20    RegisterRespNotMatchSnafu, SendRegisterReqSnafu, WritePingMsgSnafu, WriteStreamAckMsgSnafu,
21};
22use self::stream::{StreamConnect, handle_stream_on_endpoint};
23use crate::addr::resolve_tunnel_ends;
24use crate::recovery::{RecoveryTiming, jitter};
25use pb_mapper_core::checksum::{Credential, get_process_credential};
26use pb_mapper_core::config::{
27    ResolvedAddrs, control_conn_pool_size, control_heartbeat_interval, control_heartbeat_tolerance,
28    control_io_timeout, control_suspect_grace, registration_probe_timeout,
29    registration_reject_backoff,
30};
31use pb_mapper_core::timeout::RetryBackoff;
32use pb_mapper_core::{
33    snafu_error_get_or_continue, snafu_error_get_or_return, snafu_error_get_or_return_ok,
34    snafu_error_handle,
35};
36use pb_mapper_protocol::command::{
37    CONTROL_PROTOCOL_V2, LocalServer, MessageSerializer, PbConnRequest, PbConnResponse,
38    PbConnStatusReq, PbConnStatusResp, PbServerRequest,
39};
40use pb_mapper_protocol::forward::StreamForward;
41use pb_mapper_protocol::secure::ClientHeaderSession;
42use pb_mapper_protocol::{MessageReader, MessageWriter};
43use uni_stream::addr::ToSocketAddrs;
44use uni_stream::stream::StreamProvider;
45
46fn get_ping_message(protocol_version: u16, seq: u64) -> error::Result<Vec<u8>> {
47    if protocol_version >= CONTROL_PROTOCOL_V2 {
48        PbServerRequest::PingV2 { seq }
49            .encode()
50            .context(EncodePingMsgSnafu)
51    } else {
52        PbServerRequest::Ping.encode().context(EncodePingMsgSnafu)
53    }
54}
55
56/// Why one attempt at holding a control connection ended, and therefore how the
57/// worker should treat it.
58///
59/// The three transport variants are interchangeable as far as the retry decision
60/// goes — they differ only in what they report — while the two rejection variants
61/// each mean something the transport ones do not:
62///
63/// * [`Status::Rejected`] is terminal, so the worker stops.
64/// * [`Status::RejectedRetryable`] is not, but the relay answered and refused, so
65///   the worker waits on the much slower [`registration_reject_backoff`] ladder
66///   instead of the transport one.
67#[derive(Debug)]
68enum Status {
69    ReadMsg,
70    SendPing,
71    ConnectRemote,
72    Resolve,
73    Timeout,
74    NetworkChanged,
75    Cancelled,
76    /// The relay refused the registration for a reason reconnecting cannot fix —
77    /// a namespace the credential does not own, a malformed service name. Retrying
78    /// would loop forever while the caller's `wait_ready` never resolves, so this
79    /// ends the worker instead.
80    Rejected(String),
81    /// The relay refused the registration for a condition that can clear on its
82    /// own — a full per-service connection quota, a namespace at its service
83    /// limit. Worth retrying, but slowly: the relay is reachable and answering,
84    /// and reconnecting hard only adds load to what has to drain.
85    RejectedRetryable(String),
86}
87
88impl Status {
89    fn rejected(error: pb_mapper_protocol::command::PbErrorResponse) -> Self {
90        let reason = format!("{}: {}", error.code, error.message);
91        if error.retryable {
92            Self::RejectedRetryable(reason)
93        } else {
94            Self::Rejected(reason)
95        }
96    }
97}
98
99/// The two retry ladders a control worker climbs, and the reason they are two.
100///
101/// A transport failure and a retryable rejection are not the same kind of
102/// problem, so they must not share a counter. Sharing one meant a reconnect —
103/// which happens on its own schedule — reset the wait a full quota was serving,
104/// leaving the worker hammering the relay with a request it had just refused.
105#[derive(Debug)]
106struct ControlBackoff {
107    /// Could not reach or hold the relay. Retry quickly; that is how the tunnel
108    /// comes back.
109    transport: RetryBackoff,
110    /// The relay answered and refused, for something that may clear. Retry
111    /// slowly; see [`registration_reject_backoff`].
112    reject: RetryBackoff,
113    timing: RecoveryTiming,
114}
115
116impl ControlBackoff {
117    fn new() -> Self {
118        let (reject_min, reject_max) = registration_reject_backoff();
119        Self {
120            transport: RetryBackoff::new(Duration::from_millis(100), Duration::from_secs(2)),
121            timing: RecoveryTiming::default(),
122            reject: RetryBackoff::new(reject_min, reject_max),
123        }
124    }
125
126    /// Clear both ladders. Called once a registration is established: it is the
127    /// only evidence that either condition has passed.
128    fn reset(&mut self) {
129        self.transport.reset();
130        self.reject.reset();
131    }
132}
133
134pub(crate) enum LocalControlWrite {
135    /// Boxed so it does not widen every ping and stream acknowledgement.
136    #[cfg(feature = "p2p")]
137    Direct(Box<pb_mapper_protocol::direct::DirectProviderMessage>),
138    Ping {
139        seq: u64,
140    },
141    StreamAck {
142        client_id: u32,
143        server_generation: u64,
144    },
145}
146
147#[derive(Debug, Clone, Copy)]
148struct ControlRegistration {
149    conn_id: u32,
150    generation: u64,
151    protocol_version: u16,
152    lease_ttl_ms: u64,
153}
154
155#[derive(Debug)]
156struct ControlLeaseState {
157    last_rx_at: Instant,
158    last_pong_at: Option<Instant>,
159}
160
161impl ControlLeaseState {
162    fn new() -> Self {
163        Self {
164            last_rx_at: Instant::now(),
165            last_pong_at: None,
166        }
167    }
168
169    fn record_rx(&mut self) {
170        self.last_rx_at = Instant::now();
171    }
172
173    fn record_pong(&mut self) {
174        let now = Instant::now();
175        self.last_rx_at = now;
176        self.last_pong_at = Some(now);
177    }
178
179    fn last_rx_age(&self) -> Duration {
180        self.last_rx_at.elapsed()
181    }
182}
183
184#[derive(Debug)]
185enum RegistrationProbeResult {
186    Present,
187    Missing,
188    Failed(String),
189}
190
191// Callback for notifying status changes to external systems
192pub type StatusCallback = Box<dyn Fn(&str) + Send + Sync>;
193
194/// Turns the control pool's per-worker statuses into the one status a caller sees.
195///
196/// A registration is served by [`control_conn_pool_size`] control connections,
197/// any one of which keeps the service reachable. Reporting only the first
198/// worker's view — which is what this code did — meant a caller was told
199/// `retrying` while the rest of the pool was registered and forwarding fine.
200///
201/// So the pool's status is the best of its workers, in this order:
202///
203/// * `connected` — at least one worker is registered.
204/// * `retrying` — none is, but at least one is still trying.
205/// * `failed` — every worker gave up permanently. Only then, because a caller
206///   awaiting readiness with no timeout must not be released while any worker
207///   could still bring the service up.
208///
209/// The reported status is recomputed on every worker transition and forwarded
210/// only when it actually changes, so a caller sees pool-level transitions rather
211/// than one line per worker.
212struct PoolStatus {
213    callback: StatusCallback,
214    workers: Mutex<PoolStatusState>,
215}
216
217/// What each worker last reported, plus what the pool last published.
218struct PoolStatusState {
219    workers: Vec<WorkerStatus>,
220    published: Option<WorkerStatus>,
221}
222
223/// One worker's latest view of its own control connection.
224#[derive(Clone, Debug, Eq, PartialEq)]
225enum WorkerStatus {
226    /// Connecting, or reconnecting after a retryable failure.
227    Retrying,
228    /// Registered: this worker alone makes the service reachable.
229    Connected,
230    /// Permanently rejected; this worker will not try again.
231    Failed(String),
232}
233
234impl PoolStatus {
235    fn new(callback: StatusCallback, pool_size: usize) -> Self {
236        Self {
237            callback,
238            workers: Mutex::new(PoolStatusState {
239                workers: vec![WorkerStatus::Retrying; pool_size],
240                published: None,
241            }),
242        }
243    }
244
245    /// Record `status` for `worker_index`, and publish the pool's status if the
246    /// aggregate changed.
247    fn report(&self, worker_index: usize, status: WorkerStatus) {
248        let next = {
249            let mut state = self
250                .workers
251                .lock()
252                .unwrap_or_else(|poisoned| poisoned.into_inner());
253            if let Some(slot) = state.workers.get_mut(worker_index) {
254                *slot = status;
255            }
256            let aggregate = Self::aggregate(&state.workers);
257            if state.published.as_ref() == Some(&aggregate) {
258                return;
259            }
260            state.published = Some(aggregate.clone());
261            aggregate
262        };
263        // Outside the lock: the callback runs caller code, which must not be able
264        // to deadlock a worker reporting its own transition.
265        match next {
266            WorkerStatus::Connected => (self.callback)("connected"),
267            WorkerStatus::Retrying => (self.callback)("retrying"),
268            WorkerStatus::Failed(reason) => (self.callback)(&format!("failed: {reason}")),
269        }
270    }
271
272    /// The best status among the workers. `Failed` needs every one of them.
273    ///
274    /// `Connected` short-circuits; `Retrying` cannot, because a later worker may
275    /// still be connected and outrank it.
276    fn aggregate(workers: &[WorkerStatus]) -> WorkerStatus {
277        let mut retrying = false;
278        let mut first_failure = None;
279        for status in workers {
280            match status {
281                WorkerStatus::Connected => return WorkerStatus::Connected,
282                WorkerStatus::Retrying => retrying = true,
283                WorkerStatus::Failed(reason) => {
284                    first_failure.get_or_insert(reason);
285                }
286            }
287        }
288        match first_failure {
289            // A pool that is out of workers reports the first permanent rejection
290            // it collected, which is the one that explains the others.
291            Some(reason) if !retrying => WorkerStatus::Failed(reason.clone()),
292            _ => WorkerStatus::Retrying,
293        }
294    }
295}
296
297/// How a local-server tunnel forwards: the codec, the transport, and whether
298/// its sockets keep alive.
299///
300/// Grouped rather than passed as three adjacent `bool`s, which is a
301/// transposition waiting to happen at a call site — and named fields make the
302/// call sites say which is which.
303#[derive(Clone, Copy, Debug)]
304pub struct ServerTunnelOptions {
305    pub need_codec: bool,
306    pub is_datagram: bool,
307    pub keep_alive: bool,
308    pub namespace: Option<u64>,
309    pub force_namespace: bool,
310}
311
312#[derive(Clone)]
313struct ServerCliRunConfig {
314    local_addr: ResolvedAddrs,
315    remote_addr: RelayEndpoint,
316    diagnostics: Diagnostics,
317    key: Arc<str>,
318    options: ServerTunnelOptions,
319    worker_index: usize,
320    credential: Credential,
321    idle_timeout: Option<Duration>,
322    #[cfg(feature = "p2p")]
323    direct: Option<Arc<crate::direct::Provider>>,
324}
325
326impl Debug for ServerCliRunConfig {
327    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        formatter
329            .debug_struct("ServerCliRunConfig")
330            .field("local_addr", &self.local_addr)
331            .field("remote_addr", &self.remote_addr)
332            .field("key", &self.key)
333            .field("options", &self.options)
334            .field("worker_index", &self.worker_index)
335            .field("credential_key_id", &self.credential.key_id())
336            .finish()
337    }
338}
339
340fn duration_to_millis(duration: Duration) -> u64 {
341    duration.as_millis().min(u128::from(u64::MAX)) as u64
342}
343
344fn new_client_instance_id(worker_index: usize) -> String {
345    let now_ms = SystemTime::now()
346        .duration_since(UNIX_EPOCH)
347        .map(duration_to_millis)
348        .unwrap_or_default();
349    format!("{}-{worker_index}-{now_ms}", std::process::id())
350}
351
352async fn probe_remote_registration(
353    remote_addr: ResolvedAddrs,
354    endpoint: RelayEndpoint,
355    key: Arc<str>,
356    registration: ControlRegistration,
357    namespace: Option<u64>,
358    credential: Credential,
359) -> RegistrationProbeResult {
360    let timeout = registration_probe_timeout();
361    let result = tokio::time::timeout(timeout, async {
362        let mut stream = endpoint
363            .connect_addresses(&remote_addr, credential, &mut None)
364            .await
365            .map_err(|e| format!("connect remote status stream failed: {e}"))?;
366        crate::client::status::get_status_with_credential(
367            &mut stream,
368            PbConnStatusReq::Service {
369                direct_query: None,
370                key: key.to_string(),
371            },
372            namespace,
373            &credential,
374        )
375        .await
376        .map_err(|e| {
377            format!(
378                "get service status failed: {}",
379                snafu::Report::from_error(e)
380            )
381        })
382    })
383    .await;
384
385    let status = match result {
386        Ok(Ok(status)) => status,
387        Ok(Err(reason)) => return RegistrationProbeResult::Failed(reason),
388        Err(_) => {
389            return RegistrationProbeResult::Failed(format!(
390                "status probe timed out after {timeout:?}"
391            ));
392        }
393    };
394
395    match status {
396        PbConnStatusResp::Service { connections, .. } => {
397            let present = connections.iter().any(|conn| {
398                conn.conn_id == registration.conn_id
399                    && conn.generation == registration.generation
400                    && conn.healthy
401            });
402            if present {
403                RegistrationProbeResult::Present
404            } else {
405                RegistrationProbeResult::Missing
406            }
407        }
408        PbConnStatusResp::Keys(keys) => {
409            if keys.iter().any(|candidate| candidate == key.as_ref()) {
410                RegistrationProbeResult::Present
411            } else {
412                RegistrationProbeResult::Missing
413            }
414        }
415        other => RegistrationProbeResult::Failed(format!(
416            "unexpected status response while probing registration: {other:?}"
417        )),
418    }
419}
420
421pub async fn run_server_side_cli<LocalStream, A>(
422    local_addr: A,
423    remote_addr: A,
424    key: Arc<str>,
425    options: ServerTunnelOptions,
426) where
427    LocalStream: StreamProvider + Send + 'static,
428    LocalStream::Item: StreamForward,
429    A: ToSocketAddrs,
430{
431    run_server_side_cli_with_callback::<LocalStream, A>(local_addr, remote_addr, key, options, None)
432        .await
433}
434
435pub async fn run_server_side_cli_with_pinned_credential<LocalStream, A>(
436    local_addr: A,
437    remote_addr: A,
438    key: Arc<str>,
439    options: ServerTunnelOptions,
440    status_callback: Option<StatusCallback>,
441    credential: Credential,
442) where
443    LocalStream: StreamProvider + Send + 'static,
444    LocalStream::Item: StreamForward,
445    A: ToSocketAddrs,
446{
447    let Some((local_addr, remote_addr)) = resolve_tunnel_ends(local_addr, remote_addr).await else {
448        return;
449    };
450    run_server_side_cli_pool::<LocalStream>(
451        local_addr,
452        remote_addr,
453        key,
454        options,
455        status_callback,
456        Some(credential),
457        CancellationToken::new(),
458    )
459    .await;
460}
461
462/// Same as [`run_server_side_cli_with_pinned_credential`], but the retry loop
463/// returns when `shutdown` is cancelled.
464///
465/// Takes both ends already resolved, because its caller — the SDK — resolves them
466/// itself in order to report a bad address as an error rather than a log line.
467pub async fn run_server_side_cli_with_shutdown<LocalStream>(
468    local_addr: ResolvedAddrs,
469    remote_addr: ResolvedAddrs,
470    key: Arc<str>,
471    options: ServerTunnelOptions,
472    status_callback: Option<StatusCallback>,
473    credential: Credential,
474    shutdown: CancellationToken,
475) where
476    LocalStream: StreamProvider + Send + 'static,
477    LocalStream::Item: StreamForward,
478{
479    run_server_side_cli_pool::<LocalStream>(
480        local_addr,
481        remote_addr,
482        key,
483        options,
484        status_callback,
485        Some(credential),
486        shutdown,
487    )
488    .await;
489}
490
491pub async fn run_server_side_cli_with_callback<LocalStream, A>(
492    local_addr: A,
493    remote_addr: A,
494    key: Arc<str>,
495    options: ServerTunnelOptions,
496    status_callback: Option<StatusCallback>,
497) where
498    LocalStream: StreamProvider + Send + 'static,
499    LocalStream::Item: StreamForward,
500    A: ToSocketAddrs,
501{
502    let Some((local_addr, remote_addr)) = resolve_tunnel_ends(local_addr, remote_addr).await else {
503        return;
504    };
505    run_server_side_cli_pool::<LocalStream>(
506        local_addr,
507        remote_addr,
508        key,
509        options,
510        status_callback,
511        None,
512        CancellationToken::new(),
513    )
514    .await;
515}
516
517async fn resolve_registration_credential(
518    pinned: Option<Credential>,
519    shutdown: &CancellationToken,
520) -> Option<Credential> {
521    if let Some(credential) = pinned {
522        return Some(credential);
523    }
524    let mut retry_backoff = RetryBackoff::default();
525    loop {
526        if shutdown.is_cancelled() {
527            return None;
528        }
529        match get_process_credential() {
530            Ok(credential) => return Some(credential),
531            Err(error) => {
532                tracing::error!("load registration credential failed: {error}");
533                tokio::select! {
534                    () = shutdown.cancelled() => return None,
535                    () = tokio::time::sleep(retry_backoff.next_delay()) => {}
536                }
537            }
538        }
539    }
540}
541
542async fn run_server_side_cli_pool<LocalStream>(
543    local_addr: ResolvedAddrs,
544    remote_addr: ResolvedAddrs,
545    key: Arc<str>,
546    options: ServerTunnelOptions,
547    status_callback: Option<StatusCallback>,
548    pinned_credential: Option<Credential>,
549    shutdown: CancellationToken,
550) where
551    LocalStream: StreamProvider + Send + 'static,
552    LocalStream::Item: StreamForward,
553{
554    run_server_side_cli_recovering::<LocalStream>(
555        local_addr,
556        RelayEndpoint::fixed(remote_addr),
557        key,
558        options,
559        status_callback,
560        pinned_credential,
561        shutdown,
562        Diagnostics::default(),
563        None,
564        false,
565    )
566    .await;
567}
568
569#[allow(clippy::too_many_arguments)]
570pub(crate) async fn run_server_side_cli_recovering<LocalStream>(
571    local_addr: ResolvedAddrs,
572    remote_addr: RelayEndpoint,
573    key: Arc<str>,
574    options: ServerTunnelOptions,
575    status_callback: Option<StatusCallback>,
576    pinned_credential: Option<Credential>,
577    shutdown: CancellationToken,
578    diagnostics: Diagnostics,
579    idle_timeout: Option<Duration>,
580    allow_direct: bool,
581) where
582    LocalStream: StreamProvider + Send + 'static,
583    LocalStream::Item: StreamForward,
584{
585    let Some(credential) = resolve_registration_credential(pinned_credential, &shutdown).await
586    else {
587        return;
588    };
589    remote_addr.start();
590    let pool_size = control_conn_pool_size().max(1);
591    tracing::info!(
592        event = "local_server_control_pool_starting",
593        key = %key,
594        pool_size,
595        "starting local server control connection pool"
596    );
597    #[cfg(feature = "p2p")]
598    let direct = allow_direct.then(|| {
599        crate::direct::Provider::spawn::<LocalStream>(
600            local_addr.clone(),
601            key.clone(),
602            options.is_datagram,
603            shutdown.clone(),
604            idle_timeout,
605            diagnostics.clone(),
606        )
607    });
608    #[cfg(not(feature = "p2p"))]
609    let _ = allow_direct;
610    let mut workers = JoinSet::new();
611    // Every worker reports here, and the pool's aggregate is what the caller is
612    // told: one healthy control connection is a reachable service, whichever
613    // worker holds it.
614    let pool_status =
615        status_callback.map(|callback| Arc::new(PoolStatus::new(callback, pool_size)));
616    for worker_index in 0..pool_size {
617        let worker_key = key.clone();
618        let worker_status = pool_status.clone();
619        let worker_shutdown = shutdown.clone();
620        let worker_local = local_addr.clone();
621        let worker_remote = remote_addr.clone();
622        let worker_diagnostics = diagnostics.clone();
623        #[cfg(feature = "p2p")]
624        let worker_direct = direct.clone();
625        workers.spawn(async move {
626            run_server_side_cli_worker::<LocalStream>(
627                worker_local,
628                worker_remote,
629                worker_key,
630                options,
631                worker_status,
632                worker_index,
633                credential,
634                worker_shutdown,
635                worker_diagnostics,
636                idle_timeout,
637                #[cfg(feature = "p2p")]
638                worker_direct,
639            )
640            .await;
641        });
642    }
643    while let Some(result) = workers.join_next().await {
644        if let Err(e) = result {
645            tracing::warn!(
646                event = "local_server_control_worker_join_failed",
647                error = %e,
648                "local server control worker join failed"
649            );
650        }
651    }
652    #[cfg(feature = "p2p")]
653    if let Some(direct) = direct {
654        direct.stop().await;
655    }
656}
657
658#[allow(clippy::too_many_arguments)]
659async fn run_server_side_cli_worker<LocalStream>(
660    local_addr: ResolvedAddrs,
661    remote_addr: RelayEndpoint,
662    key: Arc<str>,
663    options: ServerTunnelOptions,
664    pool_status: Option<Arc<PoolStatus>>,
665    worker_index: usize,
666    credential: Credential,
667    shutdown: CancellationToken,
668    diagnostics: Diagnostics,
669    idle_timeout: Option<Duration>,
670    #[cfg(feature = "p2p")] direct: Option<Arc<crate::direct::Provider>>,
671) where
672    LocalStream: StreamProvider + Send + 'static,
673    LocalStream::Item: StreamForward,
674{
675    let report = |status: WorkerStatus| {
676        if let Some(ref pool_status) = pool_status {
677            pool_status.report(worker_index, status);
678        }
679    };
680    let mut backoff = ControlBackoff::new();
681    let run_config = ServerCliRunConfig {
682        local_addr: local_addr.clone(),
683        remote_addr: remote_addr.clone(),
684        diagnostics: diagnostics.clone(),
685        key: key.clone(),
686        options,
687        worker_index,
688        credential,
689        idle_timeout,
690        #[cfg(feature = "p2p")]
691        direct: direct.clone(),
692    };
693    // Forwarded sessions outlive a single control connection, so the set lives
694    // here rather than inside the attempt: a reconnect is not a reason to drop
695    // streams that are still healthy. It is drained before this worker returns.
696    let mut stream_tasks = JoinSet::new();
697    let setup_slots = remote_addr.data_slots();
698    let mut wake = remote_addr.wake();
699    diagnostics.network_seen(remote_addr.network_generation());
700    'outer: loop {
701        if shutdown.is_cancelled() {
702            break 'outer;
703        }
704        wake.acknowledge();
705        diagnostics.attempt();
706        let mut carrier = None;
707        let status = if let Err(status) = run_server_side_cli_inner::<LocalStream>(
708            &mut backoff,
709            run_config.clone(),
710            &report,
711            shutdown.clone(),
712            &mut stream_tasks,
713            &setup_slots,
714            &mut wake,
715            &mut carrier,
716        )
717        .await
718        {
719            status
720        } else {
721            if shutdown.is_cancelled() {
722                break 'outer;
723            }
724            tracing::warn!(
725                event = "local_server_control_worker_finished",
726                key = %key,
727                worker_index,
728                "local server control worker finished without an error; reconnecting"
729            );
730            Status::ReadMsg
731        };
732        if matches!(status, Status::Cancelled) {
733            break 'outer;
734        }
735        // Every non-terminal outcome retries; only the ladder differs.
736        let failure = match &status {
737            Status::Resolve => RecoveryFailure::Dns,
738            Status::Timeout => RecoveryFailure::Timeout,
739            Status::Rejected(_) | Status::RejectedRetryable(_) => RecoveryFailure::Rejected,
740            Status::NetworkChanged => RecoveryFailure::NetworkChanged,
741            _ => RecoveryFailure::Transport,
742        };
743        if matches!(
744            failure,
745            RecoveryFailure::Transport | RecoveryFailure::Timeout | RecoveryFailure::Dns
746        ) {
747            remote_addr.transport_failed(carrier);
748        }
749        let (retry_interval, retry_count) = match status {
750            Status::Cancelled => break 'outer,
751            #[cfg(feature = "p2p")]
752            Status::Rejected(_)
753                if direct
754                    .as_ref()
755                    .is_some_and(|provider| provider.has_active()) =>
756            {
757                // Revocation closes the control plane, not endpoint-owned direct sessions.
758                (Duration::from_secs(30), 0)
759            }
760            Status::Rejected(reason) => {
761                diagnostics.failed(RecoveryFailure::Rejected, Duration::ZERO);
762                tracing::error!(
763                    event = "local_server_registration_rejected_permanently",
764                    key = %key,
765                    worker_index,
766                    reason = %reason,
767                    "pb server permanently rejected this registration; not reconnecting"
768                );
769                report(WorkerStatus::Failed(reason));
770                break 'outer;
771            }
772            Status::RejectedRetryable(ref reason) => {
773                let interval = backoff.reject.next_delay();
774                tracing::debug!(
775                    event = "local_server_registration_rejected_retryable",
776                    key = %key,
777                    worker_index,
778                    reason = %reason,
779                    retry_delay = ?interval,
780                    retry_count = backoff.reject.failures(),
781                    "pb server rejected this registration for a condition that may clear"
782                );
783                (interval, backoff.reject.failures())
784            }
785            Status::NetworkChanged => {
786                diagnostics.network_changed(remote_addr.network_generation());
787                backoff.transport.reset();
788                (Duration::ZERO, 0)
789            }
790            Status::ReadMsg
791            | Status::SendPing
792            | Status::ConnectRemote
793            | Status::Resolve
794            | Status::Timeout => (
795                jitter(backoff.transport.next_delay()),
796                backoff.transport.failures(),
797            ),
798        };
799        if diagnostics.failed(failure, retry_interval) {
800            tracing::info!(
801                event = "local_server_control_reconnect_scheduled",
802                key = %key,
803                worker_index,
804                local_addr = ?local_addr,
805                remote_addr = ?remote_addr,
806                status = ?status,
807                retry_delay = ?retry_interval,
808                retry_count,
809                "local server control connection will reconnect"
810            );
811        }
812        report(WorkerStatus::Retrying);
813
814        tokio::select! {
815            () = shutdown.cancelled() => break 'outer,
816            () = tokio::time::sleep(retry_interval) => {},
817            () = wake.changed(), if !matches!(failure, RecoveryFailure::Rejected) => {
818                diagnostics.network_changed(remote_addr.network_generation());
819                backoff.transport.reset();
820            }
821        }
822        if shutdown.is_cancelled() {
823            break 'outer;
824        }
825    }
826
827    // Cancellation is already signalled by the token the sessions hold; this only
828    // waits for them to observe it.
829    stream_tasks.shutdown().await;
830}
831
832// `backoff` is skipped: it is mutable retry state, and recording it would
833// print two ladders' internals on every span the worker enters.
834#[allow(clippy::too_many_arguments)]
835#[instrument(skip(backoff, report, shutdown, stream_tasks, setup_slots, wake, carrier))]
836async fn run_server_side_cli_inner<LocalStream: StreamProvider>(
837    backoff: &mut ControlBackoff,
838    config: ServerCliRunConfig,
839    report: &impl Fn(WorkerStatus),
840    shutdown: CancellationToken,
841    stream_tasks: &mut JoinSet<()>,
842    setup_slots: &Arc<tokio::sync::Semaphore>,
843    wake: &mut RecoveryWake,
844    carrier: &mut Option<CarrierId>,
845) -> std::result::Result<(), Status>
846where
847    LocalStream::Item: StreamForward,
848{
849    let ServerCliRunConfig {
850        local_addr,
851        remote_addr,
852        diagnostics,
853        key,
854        options:
855            ServerTunnelOptions {
856                need_codec,
857                is_datagram,
858                keep_alive,
859                namespace,
860                force_namespace,
861            },
862        worker_index,
863        credential,
864        idle_timeout,
865        #[cfg(feature = "p2p")]
866        direct,
867    } = config;
868    let control_permit = tokio::select! {
869        () = shutdown.cancelled() => return Err(Status::Cancelled),
870        () = wake.network_changed() => return Err(Status::NetworkChanged),
871        permit = remote_addr.control_permit() => permit,
872    };
873    let started = tokio::time::Instant::now();
874    let timeout = backoff.timing.timeout();
875    let deadline = started + timeout;
876    diagnostics.phase(RecoveryPhase::Resolving);
877    let addresses = tokio::select! {
878        () = shutdown.cancelled() => return Err(Status::Cancelled),
879        () = wake.network_changed() => return Err(Status::NetworkChanged),
880        result = tokio::time::timeout_at(deadline, remote_addr.addresses()) => match result {
881            Ok(Ok(addresses)) => addresses,
882            _ => return Err(Status::Resolve),
883        },
884    };
885    diagnostics.phase(RecoveryPhase::Connecting);
886    let mut manager_stream = tokio::select! {
887        () = shutdown.cancelled() => return Err(Status::Cancelled),
888        () = wake.network_changed() => return Err(Status::NetworkChanged),
889        result = tokio::time::timeout_at(deadline, remote_addr.connect_addresses(&addresses, credential, carrier)) => {
890            match result {
891                Ok(Ok(stream)) => stream,
892                Ok(Err(error)) => {
893                    tracing::debug!(event = "local_server_dial_failed", %error, %key, worker_index);
894                    if let crate::relay::ConnectError::Rejected { response } = error {
895                        remote_addr.protocol_succeeded();
896                        diagnostics.responded(started.elapsed());
897                        backoff.timing.record(started.elapsed());
898                        return Err(Status::rejected(response));
899                    }
900                    return Err(Status::ConnectRemote);
901                }
902                Err(_) => {
903                    backoff.timing.timed_out();
904                    tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "dial");
905                    return Err(Status::Timeout);
906                }
907            }
908        }
909    };
910    tracing::debug!(
911        event = "local_server_connected_remote",
912        key = %key,
913        worker_index,
914        local_addr = %local_addr,
915        remote_addr = %remote_addr,
916        need_codec,
917        is_datagram,
918        "local server connected to pb server"
919    );
920
921    snafu_error_handle!(manager_stream.configure(keep_alive));
922
923    let registered_addr = manager_stream
924        .relay_addr()
925        .map(ResolvedAddrs::from)
926        .unwrap_or(addresses);
927    diagnostics.phase(RecoveryPhase::Handshake);
928    // Start registration with a protocol-v2 first frame. The session is reused for all
929    // subsequent control messages on this TCP connection. The credential is pinned
930    // when the worker starts so a later process-key change cannot retarget reconnects.
931    let session = match ClientHeaderSession::new_v2(&credential) {
932        Ok(session) => session,
933        Err(error) => {
934            tracing::error!("create manager protocol-v2 session failed: {error}");
935            return Err(Status::ConnectRemote);
936        }
937    };
938    let heartbeat_interval = control_heartbeat_interval();
939    let heartbeat_tolerance = control_heartbeat_tolerance();
940    #[cfg(feature = "p2p")]
941    let direct_version = direct
942        .as_ref()
943        .map(|_| pb_mapper_protocol::direct::DIRECT_VERSION);
944    #[cfg(not(feature = "p2p"))]
945    let direct_version = None;
946    let request = match namespace {
947        Some(namespace) => PbConnRequest::RegisterScoped {
948            direct: direct_version,
949            key: key.to_string(),
950            namespace,
951            force_namespace,
952            need_codec,
953            is_datagram,
954            protocol_version: Some(CONTROL_PROTOCOL_V2),
955            client_instance_id: Some(new_client_instance_id(worker_index)),
956            heartbeat_interval_ms: Some(duration_to_millis(heartbeat_interval)),
957            heartbeat_tolerance_ms: Some(duration_to_millis(heartbeat_tolerance)),
958        },
959        None => PbConnRequest::Register {
960            direct: direct_version,
961            key: key.to_string(),
962            need_codec,
963            is_datagram,
964            protocol_version: Some(CONTROL_PROTOCOL_V2),
965            client_instance_id: Some(new_client_instance_id(worker_index)),
966            heartbeat_interval_ms: Some(duration_to_millis(heartbeat_interval)),
967            heartbeat_tolerance_ms: Some(duration_to_millis(heartbeat_tolerance)),
968        },
969    };
970    let msg = snafu_error_get_or_return_ok!(request.encode().context(EncodeRegisterReqSnafu));
971    tokio::select! {
972        () = shutdown.cancelled() => return Err(Status::Cancelled),
973        () = wake.network_changed() => return Err(Status::NetworkChanged),
974        result = tokio::time::timeout_at(deadline, session.write_initial(&mut manager_stream, &msg)) => {
975            match result {
976                Ok(result) => snafu_error_get_or_return_ok!(result.context(SendRegisterReqSnafu)),
977                Err(_) => {
978                    backoff.timing.timed_out();
979                    tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "write");
980                    return Err(Status::Timeout);
981                }
982            }
983        }
984    }
985    let (mut reader, mut writer) = manager_stream.into_split();
986    let mut msg_reader = match session.response_reader(&mut reader) {
987        Ok(reader) => reader,
988        Err(e) => {
989            tracing::error!("create manager header reader failed: {e}");
990            return Err(Status::ReadMsg);
991        }
992    };
993    // read register resp to indicate that register has finished
994    let (key, registration) = {
995        let msg = tokio::select! {
996            () = shutdown.cancelled() => return Err(Status::Cancelled),
997            () = wake.network_changed() => return Err(Status::NetworkChanged),
998            result = tokio::time::timeout_at(deadline, msg_reader.read_msg()) => {
999                match result {
1000                    Ok(result) => snafu_error_get_or_return_ok!(result.context(ReadRegisterRespSnafu)),
1001                    Err(_) => {
1002                        backoff.timing.timed_out();
1003                        tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "response");
1004                        return Err(Status::Timeout);
1005                    }
1006                }
1007            }
1008        };
1009        let resp = snafu_error_get_or_return_ok!(
1010            PbConnResponse::decode(msg).context(DecodeRegisterRespSnafu)
1011        );
1012        remote_addr.protocol_succeeded();
1013        diagnostics.responded(started.elapsed());
1014        backoff.timing.record(started.elapsed());
1015        let registration = match resp {
1016            PbConnResponse::RegisterV2 {
1017                conn_id,
1018                generation,
1019                lease_ttl_ms,
1020                ..
1021            } => ControlRegistration {
1022                conn_id,
1023                generation,
1024                protocol_version: CONTROL_PROTOCOL_V2,
1025                lease_ttl_ms,
1026            },
1027            PbConnResponse::Register(conn_id) => ControlRegistration {
1028                conn_id,
1029                generation: 0,
1030                protocol_version: 1,
1031                lease_ttl_ms: 0,
1032            },
1033            // A rejection is an answer, not a failure to get one, so it never
1034            // goes through `snafu_error_get_or_return_ok!`: that would report the
1035            // attempt as having "finished without an error" and retry it on the
1036            // transport ladder, which is how a refused registration turned into
1037            // thousands of reject lines a minute.
1038            PbConnResponse::Error(error) => {
1039                tracing::debug!(
1040                    event = "local_server_registration_rejected",
1041                    key = %key,
1042                    worker_index,
1043                    reason = %error.code,
1044                    retryable = error.retryable,
1045                    message = %error.message,
1046                    "pb server rejected service registration"
1047                );
1048                return Err(Status::rejected(error));
1049            }
1050            _ => snafu_error_get_or_return_ok!(RegisterRespNotMatchSnafu {}.fail()),
1051        };
1052        tracing::info!(
1053            event = "local_server_registered",
1054            key = %key,
1055            conn_id = %registration.conn_id,
1056            generation = registration.generation,
1057            protocol_version = registration.protocol_version,
1058            lease_ttl_ms = registration.lease_ttl_ms,
1059            worker_index,
1060            local_addr = %local_addr,
1061            remote_addr = %remote_addr,
1062            "local server registered with pb server"
1063        );
1064
1065        // This worker holds a registered control connection, which is on its own
1066        // enough to make the service reachable.
1067        report(WorkerStatus::Connected);
1068        (key, registration)
1069    };
1070
1071    drop(control_permit);
1072    diagnostics.succeeded(started.elapsed());
1073    remote_addr.protocol_succeeded();
1074    backoff.timing.record(started.elapsed());
1075    tracing::debug!(event = "local_server_setup_latency", elapsed_ms = duration_to_millis(started.elapsed()), next_timeout_ms = duration_to_millis(backoff.timing.timeout()), %key, worker_index);
1076    backoff.reset();
1077    let (write_tx, mut write_rx) = tokio::sync::mpsc::channel::<LocalControlWrite>(64);
1078    #[cfg(feature = "p2p")]
1079    if let Some(direct) = &direct {
1080        direct.connected(write_tx.clone());
1081    }
1082    let lease_state = Arc::new(tokio::sync::Mutex::new(ControlLeaseState::new()));
1083    let writer_key = key.clone();
1084    let writer_registration = registration;
1085    let mut writer_handle = tokio_util::task::AbortOnDropHandle::new(tokio::spawn(async move {
1086        let mut msg_writer = match session.continuation_writer(&mut writer) {
1087            Ok(writer) => writer,
1088            Err(e) => {
1089                tracing::error!("create manager header writer failed: {e}");
1090                return Err(Status::SendPing);
1091            }
1092        };
1093        loop {
1094            let Some(cmd) = write_rx.recv().await else {
1095                return Ok(());
1096            };
1097            match cmd {
1098                #[cfg(feature = "p2p")]
1099                LocalControlWrite::Direct(message) => {
1100                    let payload = PbServerRequest::Direct(*message)
1101                        .encode()
1102                        .map_err(|_| Status::SendPing)?;
1103                    tokio::time::timeout(control_io_timeout(), msg_writer.write_msg(&payload))
1104                        .await
1105                        .map_err(|_| Status::SendPing)?
1106                        .map_err(|_| Status::SendPing)?;
1107                }
1108                LocalControlWrite::Ping { seq } => {
1109                    snafu_error_get_or_return!(
1110                        handle_ping_interval(
1111                            &mut msg_writer,
1112                            writer_key.clone(),
1113                            writer_registration,
1114                            seq
1115                        )
1116                        .await,
1117                        "[send ping]",
1118                        Err(Status::SendPing)
1119                    );
1120                    tracing::debug!(
1121                        event = "local_server_heartbeat_sent",
1122                        key = %writer_key,
1123                        conn_id = %writer_registration.conn_id,
1124                        generation = writer_registration.generation,
1125                        protocol_version = writer_registration.protocol_version,
1126                        seq,
1127                        interval = ?control_heartbeat_interval(),
1128                        "local server heartbeat sent"
1129                    );
1130                }
1131                LocalControlWrite::StreamAck {
1132                    client_id,
1133                    server_generation,
1134                } => {
1135                    snafu_error_get_or_return!(
1136                        write_stream_ack(&mut msg_writer, client_id, server_generation).await,
1137                        "[send stream ack]",
1138                        Err(Status::SendPing)
1139                    );
1140                }
1141            }
1142        }
1143    }));
1144
1145    let heartbeat_interval = control_heartbeat_interval();
1146    let heartbeat_tolerance = control_heartbeat_tolerance();
1147    let suspect_grace = control_suspect_grace();
1148    let mut heartbeat = tokio::time::interval(heartbeat_interval);
1149    heartbeat.set_missed_tick_behavior(MissedTickBehavior::Delay);
1150    heartbeat.tick().await;
1151    let mut probes = JoinSet::new();
1152    let mut ping_seq = 0_u64;
1153    let mut control_deadline = tokio::time::Instant::now() + heartbeat_tolerance + suspect_grace;
1154
1155    let result = loop {
1156        tokio::select! {
1157            msg = msg_reader.read_msg() => {
1158                let msg = match msg.context(ReadStreamReqSnafu) {
1159                    Ok(msg) => msg,
1160                    Err(e) => {
1161                        tracing::error!(
1162                            event = "local_server_control_read_failed",
1163                            key = %key,
1164                            conn_id = %registration.conn_id,
1165                            generation = registration.generation,
1166                            error = %snafu::Report::from_error(e),
1167                            "local server control read failed"
1168                        );
1169                        break Err(Status::ReadMsg);
1170                    }
1171                };
1172                remote_addr.protocol_succeeded();
1173                diagnostics.heard();
1174                lease_state.lock().await.record_rx();
1175                control_deadline = tokio::time::Instant::now() + heartbeat_tolerance + suspect_grace;
1176                snafu_error_get_or_continue!(
1177                    handle_request::<LocalStream>(
1178                        msg,
1179                        StreamConnect {
1180                            local_addr: local_addr.clone(),
1181                            remote_addr: registered_addr.clone(),
1182                            keep_alive,
1183                            namespace,
1184                            credential,
1185                            idle_timeout,
1186                        },
1187                        remote_addr.clone(),
1188                        key.clone(),
1189                        registration.conn_id,
1190                        &write_tx,
1191                        lease_state.clone(),
1192                        stream_tasks,
1193                        &shutdown,
1194                        setup_slots,
1195                        #[cfg(feature = "p2p")]
1196                        direct.as_ref(),
1197                    )
1198                    .await
1199                );
1200            }
1201            Some(_) = stream_tasks.join_next() => {
1202                // Reap finished sessions so the set does not grow for the lifetime
1203                // of the registration. `handle_stream` logs its own failures.
1204            }
1205            result = &mut writer_handle => {
1206                break match result {
1207                    Ok(result) => result,
1208                    Err(e) => {
1209                        tracing::error!(
1210                            event = "local_server_control_writer_join_failed",
1211                            key = %key,
1212                            conn_id = %registration.conn_id,
1213                            generation = registration.generation,
1214                            error = %e,
1215                            "local server control writer task failed"
1216                        );
1217                        Err(Status::SendPing)
1218                    }
1219                };
1220            }
1221            _ = heartbeat.tick() => {
1222                ping_seq = ping_seq.wrapping_add(1);
1223                if write_tx.try_send(LocalControlWrite::Ping { seq: ping_seq }).is_err() {
1224                    break Err(Status::SendPing);
1225                }
1226
1227                let last_rx_age = lease_state.lock().await.last_rx_age();
1228                if registration.protocol_version >= CONTROL_PROTOCOL_V2
1229                    && last_rx_age >= heartbeat_tolerance
1230                    && probes.is_empty()
1231                {
1232                    tracing::warn!(
1233                        event = "local_server_lease_suspect",
1234                        key = %key,
1235                        conn_id = %registration.conn_id,
1236                        generation = registration.generation,
1237                        worker_index,
1238                        last_rx_age_ms = duration_to_millis(last_rx_age),
1239                        heartbeat_tolerance_ms = duration_to_millis(heartbeat_tolerance),
1240                        "local server control lease is suspect; probing remote registration"
1241                    );
1242                    let probe_key = key.clone();
1243                    let probe_remote = registered_addr.clone();
1244                    let probe_endpoint = remote_addr.clone();
1245                    probes.spawn(async move {
1246                        let _permit = probe_endpoint.control_permit().await;
1247                        probe_remote_registration(
1248                            probe_remote,
1249                            probe_endpoint.clone(),
1250                            probe_key,
1251                            registration,
1252                            namespace,
1253                            credential,
1254                        )
1255                        .await
1256                    });
1257                }
1258            }
1259            () = shutdown.cancelled() => {
1260                tracing::info!(
1261                    event = "local_server_control_cancelled",
1262                    key = %key,
1263                    conn_id = %registration.conn_id,
1264                    generation = registration.generation,
1265                    worker_index,
1266                    "local server control loop cancelled"
1267                );
1268                break Err(Status::Cancelled);
1269            }
1270            () = tokio::time::sleep_until(control_deadline) => {
1271                tracing::warn!(event = "local_server_control_unresponsive", %key, worker_index, "control socket received no reply past grace; reconnecting");
1272                break Err(Status::ReadMsg);
1273            }
1274            Some(probe_result) = probes.join_next() => {
1275                let probe_result = probe_result.unwrap_or_else(|error| RegistrationProbeResult::Failed(error.to_string()));
1276                let last_rx_age = lease_state.lock().await.last_rx_age();
1277                match probe_result {
1278                    RegistrationProbeResult::Present => {
1279                        tracing::debug!(
1280                            event = "local_server_registration_probe_ok",
1281                            key = %key,
1282                            conn_id = %registration.conn_id,
1283                            generation = registration.generation,
1284                            worker_index,
1285                            last_rx_age_ms = duration_to_millis(last_rx_age),
1286                            "remote registration still contains this control connection"
1287                        );
1288                    }
1289                    RegistrationProbeResult::Missing if last_rx_age >= heartbeat_tolerance => {
1290                        tracing::warn!(
1291                            event = "local_server_registration_missing",
1292                            key = %key,
1293                            conn_id = %registration.conn_id,
1294                            generation = registration.generation,
1295                            worker_index,
1296                            last_rx_age_ms = duration_to_millis(last_rx_age),
1297                            "remote registration no longer contains this control connection; reconnecting"
1298                        );
1299                        break Err(Status::ReadMsg);
1300                    }
1301                    RegistrationProbeResult::Missing => {
1302                        tracing::debug!(
1303                            event = "local_server_registration_missing_ignored_after_recent_activity",
1304                            key = %key,
1305                            conn_id = %registration.conn_id,
1306                            generation = registration.generation,
1307                            worker_index,
1308                            last_rx_age_ms = duration_to_millis(last_rx_age),
1309                            "remote registration probe was stale after recent control activity"
1310                        );
1311                    }
1312                    RegistrationProbeResult::Failed(reason)
1313                        if last_rx_age >= heartbeat_tolerance + suspect_grace =>
1314                    {
1315                        tracing::warn!(
1316                            event = "local_server_status_probe_failed",
1317                            key = %key,
1318                            conn_id = %registration.conn_id,
1319                            generation = registration.generation,
1320                            worker_index,
1321                            last_rx_age_ms = duration_to_millis(last_rx_age),
1322                            reason = %reason,
1323                            "registration probe failed past suspect grace; reconnecting"
1324                        );
1325                        break Err(Status::ReadMsg);
1326                    }
1327                    RegistrationProbeResult::Failed(reason) => {
1328                        tracing::warn!(
1329                            event = "local_server_status_probe_failed",
1330                            key = %key,
1331                            conn_id = %registration.conn_id,
1332                            generation = registration.generation,
1333                            worker_index,
1334                            last_rx_age_ms = duration_to_millis(last_rx_age),
1335                            reason = %reason,
1336                            "registration probe failed; waiting inside suspect grace"
1337                        );
1338                    }
1339                }
1340            }
1341        }
1342    };
1343    if !writer_handle.is_finished() {
1344        writer_handle.abort();
1345    }
1346    result
1347}
1348
1349#[instrument(skip(writer))]
1350async fn handle_ping_interval<T: MessageWriter>(
1351    writer: &mut T,
1352    _key: Arc<str>,
1353    registration: ControlRegistration,
1354    seq: u64,
1355) -> error::Result<()> {
1356    let ping_msg = get_ping_message(registration.protocol_version, seq)?;
1357    let timeout = control_io_timeout();
1358    match tokio::time::timeout(timeout, writer.write_msg(&ping_msg)).await {
1359        Ok(result) => result.context(WritePingMsgSnafu),
1360        Err(_) => ControlIoTimeoutSnafu {
1361            action: "write ping message",
1362            timeout,
1363        }
1364        .fail(),
1365    }
1366}
1367
1368#[instrument(skip_all, fields(%key, conn_id))]
1369#[allow(clippy::too_many_arguments)]
1370async fn handle_request<LocalStream: StreamProvider>(
1371    msg: &[u8],
1372    target: StreamConnect,
1373    relay_endpoint: RelayEndpoint,
1374    key: Arc<str>,
1375    conn_id: u32,
1376    write_tx: &tokio::sync::mpsc::Sender<LocalControlWrite>,
1377    lease_state: Arc<tokio::sync::Mutex<ControlLeaseState>>,
1378    stream_tasks: &mut JoinSet<()>,
1379    shutdown: &CancellationToken,
1380    setup_slots: &Arc<tokio::sync::Semaphore>,
1381    #[cfg(feature = "p2p")] direct: Option<&Arc<crate::direct::Provider>>,
1382) -> error::Result<()>
1383where
1384    LocalStream::Item: StreamForward,
1385{
1386    let req = LocalServer::decode(msg).context(DecodeStreamReqSnafu)?;
1387
1388    match req {
1389        LocalServer::Direct(event) => {
1390            #[cfg(feature = "p2p")]
1391            if let Some(direct) = direct {
1392                let mut event = event;
1393                // `remote_addr` is the relay address this registration is
1394                // connected to: the one to probe when the relay advertises
1395                // only its STUN port.
1396                if let (
1397                    pb_mapper_protocol::direct::DirectEvent::Offer { stun_addr, .. },
1398                    Some(relay),
1399                ) = (&mut *event, target.remote_addr.as_slice().first())
1400                {
1401                    *stun_addr = pb_mapper_protocol::direct::stun_target(*stun_addr, relay.ip());
1402                }
1403                direct.event(*event, write_tx.clone());
1404            }
1405            #[cfg(not(feature = "p2p"))]
1406            let _ = event;
1407        }
1408        LocalServer::Stream {
1409            client_id,
1410            server_generation,
1411        } => {
1412            tracing::debug!(
1413                event = "local_server_stream_request_received",
1414                key = %key,
1415                server_conn_id = %conn_id,
1416                client_conn_id = client_id,
1417                server_generation,
1418                "local server received stream request"
1419            );
1420            let Ok(permit) = setup_slots.clone().try_acquire_owned() else {
1421                tracing::warn!(event = "local_server_setup_saturated", %key, client_id, "stream setup capacity reached; relay can select another worker");
1422                return Ok(());
1423            };
1424            write_tx
1425                .try_send(LocalControlWrite::StreamAck {
1426                    client_id,
1427                    server_generation,
1428                })
1429                .map_err(|_| error::Error::ControlWriterClosed {
1430                    action: "stream ack message",
1431                })?;
1432            let key = key.clone();
1433            let stream_shutdown = shutdown.clone();
1434            // Tracked rather than detached: a cancelled registration must take its
1435            // in-flight forwarded sessions with it, so `stop()` returning means no
1436            // stream of this tunnel is still moving bytes.
1437            stream_tasks.spawn(async move {
1438                let forward = handle_stream_on_endpoint::<LocalStream>(
1439                    key,
1440                    client_id,
1441                    server_generation,
1442                    target,
1443                    permit,
1444                    relay_endpoint,
1445                );
1446                tokio::select! {
1447                    () = stream_shutdown.cancelled() => {}
1448                    result = forward => snafu_error_handle!(result),
1449                }
1450            });
1451        }
1452        // got pong response
1453        LocalServer::Pong => {
1454            lease_state.lock().await.record_pong();
1455            tracing::debug!(
1456                event = "local_server_pong_received",
1457                key = %key,
1458                server_conn_id = %conn_id,
1459                "local server received pong"
1460            );
1461        }
1462        LocalServer::PongV2 { seq } => {
1463            lease_state.lock().await.record_pong();
1464            tracing::debug!(
1465                event = "local_server_pong_received",
1466                key = %key,
1467                server_conn_id = %conn_id,
1468                seq,
1469                "local server received pong v2"
1470            );
1471        }
1472        LocalServer::Retire {
1473            reason,
1474            conn_id: retired_conn_id,
1475            server_generation,
1476        } => {
1477            tracing::warn!(
1478                event = "local_server_control_retired",
1479                key = %key,
1480                server_conn_id = %conn_id,
1481                retired_conn_id,
1482                server_generation,
1483                reason = %reason,
1484                "remote server retired this local control connection"
1485            );
1486        }
1487    }
1488    Ok(())
1489}
1490
1491async fn write_stream_ack<T: MessageWriter>(
1492    writer: &mut T,
1493    client_id: u32,
1494    server_generation: u64,
1495) -> error::Result<()> {
1496    let ack = PbServerRequest::StreamAck {
1497        client_id,
1498        server_generation,
1499    }
1500    .encode()
1501    .context(EncodeStreamAckMsgSnafu)?;
1502    let timeout = control_io_timeout();
1503    match tokio::time::timeout(timeout, writer.write_msg(&ack)).await {
1504        Ok(result) => result.context(WriteStreamAckMsgSnafu),
1505        Err(_) => ControlIoTimeoutSnafu {
1506            action: "write stream ack message",
1507            timeout,
1508        }
1509        .fail(),
1510    }
1511}
1512
1513#[cfg(test)]
1514mod pool_status_tests {
1515    use std::sync::Mutex as StdMutex;
1516
1517    use super::*;
1518
1519    /// Records what the pool published, in order.
1520    fn recording_pool(pool_size: usize) -> (PoolStatus, Arc<StdMutex<Vec<String>>>) {
1521        let seen = Arc::new(StdMutex::new(Vec::new()));
1522        let sink = seen.clone();
1523        let callback: StatusCallback = Box::new(move |status: &str| {
1524            sink.lock().unwrap().push(status.to_string());
1525        });
1526        (PoolStatus::new(callback, pool_size), seen)
1527    }
1528
1529    fn published(seen: &Arc<StdMutex<Vec<String>>>) -> Vec<String> {
1530        seen.lock().unwrap().clone()
1531    }
1532
1533    /// One connected worker is enough: the service is reachable through it, so
1534    /// the pool must not report the other worker's retry as the pool's state.
1535    #[test]
1536    fn one_connected_worker_makes_the_pool_connected() {
1537        let (pool, seen) = recording_pool(2);
1538        pool.report(0, WorkerStatus::Connected);
1539        pool.report(1, WorkerStatus::Retrying);
1540        assert_eq!(published(&seen), vec!["connected".to_string()]);
1541    }
1542
1543    /// The guarantee `wait_ready()` depends on: while any worker might still
1544    /// bring the service up, one worker's permanent rejection is not the pool's.
1545    #[test]
1546    fn a_single_failure_does_not_fail_the_pool() {
1547        let (pool, seen) = recording_pool(2);
1548        pool.report(0, WorkerStatus::Failed("service_transport_mismatch".into()));
1549        assert_eq!(published(&seen), vec!["retrying".to_string()]);
1550
1551        pool.report(1, WorkerStatus::Connected);
1552        assert_eq!(
1553            published(&seen),
1554            vec!["retrying".to_string(), "connected".to_string()]
1555        );
1556    }
1557
1558    /// Once every worker has given up, the pool reports failed and says why —
1559    /// otherwise a caller awaiting readiness with no timeout waits forever.
1560    #[test]
1561    fn the_pool_fails_only_when_every_worker_has() {
1562        let (pool, seen) = recording_pool(2);
1563        pool.report(0, WorkerStatus::Failed("service_transport_mismatch".into()));
1564        pool.report(1, WorkerStatus::Failed("namespace_access_denied".into()));
1565        assert_eq!(
1566            published(&seen),
1567            vec![
1568                "retrying".to_string(),
1569                "failed: service_transport_mismatch".to_string(),
1570            ],
1571            "the first permanent rejection is the one that explains the pool"
1572        );
1573    }
1574
1575    /// A worker that reconnects repeatedly must not turn into a stream of
1576    /// identical status lines for the caller.
1577    #[test]
1578    fn unchanged_aggregates_are_not_republished() {
1579        let (pool, seen) = recording_pool(2);
1580        pool.report(0, WorkerStatus::Connected);
1581        pool.report(1, WorkerStatus::Connected);
1582        pool.report(0, WorkerStatus::Connected);
1583        assert_eq!(published(&seen), vec!["connected".to_string()]);
1584    }
1585
1586    /// Losing the last connected worker has to be visible: the service is no
1587    /// longer reachable, and the caller's status must say so.
1588    #[test]
1589    fn losing_the_last_connection_publishes_retrying() {
1590        let (pool, seen) = recording_pool(1);
1591        pool.report(0, WorkerStatus::Connected);
1592        pool.report(0, WorkerStatus::Retrying);
1593        assert_eq!(
1594            published(&seen),
1595            vec!["connected".to_string(), "retrying".to_string()]
1596        );
1597    }
1598}
1599
1600#[cfg(test)]
1601mod shutdown_tests {
1602    use std::time::Duration;
1603
1604    use super::*;
1605    use uni_stream::stream::TcpStreamProvider;
1606
1607    #[tokio::test]
1608    async fn register_loop_stops_on_cancel() {
1609        let shutdown = CancellationToken::new();
1610        let token = shutdown.clone();
1611        let addr = ResolvedAddrs::from("127.0.0.1:1".parse::<std::net::SocketAddr>().unwrap());
1612        let task = tokio::spawn(async move {
1613            run_server_side_cli_with_shutdown::<TcpStreamProvider>(
1614                addr.clone(),
1615                addr,
1616                "k".into(),
1617                ServerTunnelOptions {
1618                    need_codec: false,
1619                    is_datagram: false,
1620                    keep_alive: false,
1621                    namespace: None,
1622                    force_namespace: false,
1623                },
1624                None,
1625                Credential::Admin(*b"0123456789abcdefghijklmnopqrstuv"),
1626                token,
1627            )
1628            .await;
1629        });
1630        tokio::time::sleep(Duration::from_millis(80)).await;
1631        shutdown.cancel();
1632        tokio::time::timeout(Duration::from_secs(3), task)
1633            .await
1634            .expect("register loop did not stop after cancel")
1635            .expect("join");
1636    }
1637}