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        crate::sdk::PortMapping::Off,
566    )
567    .await;
568}
569
570#[allow(clippy::too_many_arguments)]
571pub(crate) async fn run_server_side_cli_recovering<LocalStream>(
572    local_addr: ResolvedAddrs,
573    remote_addr: RelayEndpoint,
574    key: Arc<str>,
575    options: ServerTunnelOptions,
576    status_callback: Option<StatusCallback>,
577    pinned_credential: Option<Credential>,
578    shutdown: CancellationToken,
579    diagnostics: Diagnostics,
580    idle_timeout: Option<Duration>,
581    allow_direct: bool,
582    port_mapping: crate::sdk::PortMapping,
583) where
584    LocalStream: StreamProvider + Send + 'static,
585    LocalStream::Item: StreamForward,
586{
587    let Some(credential) = resolve_registration_credential(pinned_credential, &shutdown).await
588    else {
589        return;
590    };
591    remote_addr.start();
592    let pool_size = control_conn_pool_size().max(1);
593    tracing::info!(
594        event = "local_server_control_pool_starting",
595        key = %key,
596        pool_size,
597        "starting local server control connection pool"
598    );
599    #[cfg(feature = "p2p")]
600    let direct = allow_direct.then(|| {
601        crate::direct::Provider::spawn::<LocalStream>(
602            local_addr.clone(),
603            key.clone(),
604            options.is_datagram,
605            shutdown.clone(),
606            idle_timeout,
607            diagnostics.clone(),
608            port_mapping,
609        )
610    });
611    #[cfg(not(feature = "p2p"))]
612    let _ = (allow_direct, port_mapping);
613    let mut workers = JoinSet::new();
614    // Every worker reports here, and the pool's aggregate is what the caller is
615    // told: one healthy control connection is a reachable service, whichever
616    // worker holds it.
617    let pool_status =
618        status_callback.map(|callback| Arc::new(PoolStatus::new(callback, pool_size)));
619    for worker_index in 0..pool_size {
620        let worker_key = key.clone();
621        let worker_status = pool_status.clone();
622        let worker_shutdown = shutdown.clone();
623        let worker_local = local_addr.clone();
624        let worker_remote = remote_addr.clone();
625        let worker_diagnostics = diagnostics.clone();
626        #[cfg(feature = "p2p")]
627        let worker_direct = direct.clone();
628        workers.spawn(async move {
629            run_server_side_cli_worker::<LocalStream>(
630                worker_local,
631                worker_remote,
632                worker_key,
633                options,
634                worker_status,
635                worker_index,
636                credential,
637                worker_shutdown,
638                worker_diagnostics,
639                idle_timeout,
640                #[cfg(feature = "p2p")]
641                worker_direct,
642            )
643            .await;
644        });
645    }
646    while let Some(result) = workers.join_next().await {
647        if let Err(e) = result {
648            tracing::warn!(
649                event = "local_server_control_worker_join_failed",
650                error = %e,
651                "local server control worker join failed"
652            );
653        }
654    }
655    #[cfg(feature = "p2p")]
656    if let Some(direct) = direct {
657        direct.stop().await;
658    }
659}
660
661#[allow(clippy::too_many_arguments)]
662async fn run_server_side_cli_worker<LocalStream>(
663    local_addr: ResolvedAddrs,
664    remote_addr: RelayEndpoint,
665    key: Arc<str>,
666    options: ServerTunnelOptions,
667    pool_status: Option<Arc<PoolStatus>>,
668    worker_index: usize,
669    credential: Credential,
670    shutdown: CancellationToken,
671    diagnostics: Diagnostics,
672    idle_timeout: Option<Duration>,
673    #[cfg(feature = "p2p")] direct: Option<Arc<crate::direct::Provider>>,
674) where
675    LocalStream: StreamProvider + Send + 'static,
676    LocalStream::Item: StreamForward,
677{
678    let report = |status: WorkerStatus| {
679        if let Some(ref pool_status) = pool_status {
680            pool_status.report(worker_index, status);
681        }
682    };
683    let mut backoff = ControlBackoff::new();
684    let run_config = ServerCliRunConfig {
685        local_addr: local_addr.clone(),
686        remote_addr: remote_addr.clone(),
687        diagnostics: diagnostics.clone(),
688        key: key.clone(),
689        options,
690        worker_index,
691        credential,
692        idle_timeout,
693        #[cfg(feature = "p2p")]
694        direct: direct.clone(),
695    };
696    // Forwarded sessions outlive a single control connection, so the set lives
697    // here rather than inside the attempt: a reconnect is not a reason to drop
698    // streams that are still healthy. It is drained before this worker returns.
699    let mut stream_tasks = JoinSet::new();
700    let setup_slots = remote_addr.data_slots();
701    let mut wake = remote_addr.wake();
702    diagnostics.network_seen(remote_addr.network_generation());
703    'outer: loop {
704        if shutdown.is_cancelled() {
705            break 'outer;
706        }
707        wake.acknowledge();
708        diagnostics.attempt();
709        let mut carrier = None;
710        let status = if let Err(status) = run_server_side_cli_inner::<LocalStream>(
711            &mut backoff,
712            run_config.clone(),
713            &report,
714            shutdown.clone(),
715            &mut stream_tasks,
716            &setup_slots,
717            &mut wake,
718            &mut carrier,
719        )
720        .await
721        {
722            status
723        } else {
724            if shutdown.is_cancelled() {
725                break 'outer;
726            }
727            tracing::warn!(
728                event = "local_server_control_worker_finished",
729                key = %key,
730                worker_index,
731                "local server control worker finished without an error; reconnecting"
732            );
733            Status::ReadMsg
734        };
735        if matches!(status, Status::Cancelled) {
736            break 'outer;
737        }
738        // Every non-terminal outcome retries; only the ladder differs.
739        let failure = match &status {
740            Status::Resolve => RecoveryFailure::Dns,
741            Status::Timeout => RecoveryFailure::Timeout,
742            Status::Rejected(_) | Status::RejectedRetryable(_) => RecoveryFailure::Rejected,
743            Status::NetworkChanged => RecoveryFailure::NetworkChanged,
744            _ => RecoveryFailure::Transport,
745        };
746        if matches!(
747            failure,
748            RecoveryFailure::Transport | RecoveryFailure::Timeout | RecoveryFailure::Dns
749        ) {
750            remote_addr.transport_failed(carrier);
751        }
752        let (retry_interval, retry_count) = match status {
753            Status::Cancelled => break 'outer,
754            #[cfg(feature = "p2p")]
755            Status::Rejected(_)
756                if direct
757                    .as_ref()
758                    .is_some_and(|provider| provider.has_active()) =>
759            {
760                // Revocation closes the control plane, not endpoint-owned direct sessions.
761                (Duration::from_secs(30), 0)
762            }
763            Status::Rejected(reason) => {
764                diagnostics.failed(RecoveryFailure::Rejected, Duration::ZERO);
765                tracing::error!(
766                    event = "local_server_registration_rejected_permanently",
767                    key = %key,
768                    worker_index,
769                    reason = %reason,
770                    "pb server permanently rejected this registration; not reconnecting"
771                );
772                report(WorkerStatus::Failed(reason));
773                break 'outer;
774            }
775            Status::RejectedRetryable(ref reason) => {
776                let interval = backoff.reject.next_delay();
777                tracing::debug!(
778                    event = "local_server_registration_rejected_retryable",
779                    key = %key,
780                    worker_index,
781                    reason = %reason,
782                    retry_delay = ?interval,
783                    retry_count = backoff.reject.failures(),
784                    "pb server rejected this registration for a condition that may clear"
785                );
786                (interval, backoff.reject.failures())
787            }
788            Status::NetworkChanged => {
789                diagnostics.network_changed(remote_addr.network_generation());
790                backoff.transport.reset();
791                (Duration::ZERO, 0)
792            }
793            Status::ReadMsg
794            | Status::SendPing
795            | Status::ConnectRemote
796            | Status::Resolve
797            | Status::Timeout => (
798                jitter(backoff.transport.next_delay()),
799                backoff.transport.failures(),
800            ),
801        };
802        if diagnostics.failed(failure, retry_interval) {
803            tracing::info!(
804                event = "local_server_control_reconnect_scheduled",
805                key = %key,
806                worker_index,
807                local_addr = ?local_addr,
808                remote_addr = ?remote_addr,
809                status = ?status,
810                retry_delay = ?retry_interval,
811                retry_count,
812                "local server control connection will reconnect"
813            );
814        }
815        report(WorkerStatus::Retrying);
816
817        tokio::select! {
818            () = shutdown.cancelled() => break 'outer,
819            () = tokio::time::sleep(retry_interval) => {},
820            woke = wake.changed(), if !matches!(failure, RecoveryFailure::Rejected) => {
821                if let crate::endpoint::Wake::Network(generation) = woke {
822                    diagnostics.network_changed(generation);
823                }
824                backoff.transport.reset();
825            }
826        }
827        if shutdown.is_cancelled() {
828            break 'outer;
829        }
830    }
831
832    // Cancellation is already signalled by the token the sessions hold; this only
833    // waits for them to observe it.
834    stream_tasks.shutdown().await;
835}
836
837// `backoff` is skipped: it is mutable retry state, and recording it would
838// print two ladders' internals on every span the worker enters.
839#[allow(clippy::too_many_arguments)]
840#[instrument(skip(backoff, report, shutdown, stream_tasks, setup_slots, wake, carrier))]
841async fn run_server_side_cli_inner<LocalStream: StreamProvider>(
842    backoff: &mut ControlBackoff,
843    config: ServerCliRunConfig,
844    report: &impl Fn(WorkerStatus),
845    shutdown: CancellationToken,
846    stream_tasks: &mut JoinSet<()>,
847    setup_slots: &Arc<tokio::sync::Semaphore>,
848    wake: &mut RecoveryWake,
849    carrier: &mut Option<CarrierId>,
850) -> std::result::Result<(), Status>
851where
852    LocalStream::Item: StreamForward,
853{
854    let ServerCliRunConfig {
855        local_addr,
856        remote_addr,
857        diagnostics,
858        key,
859        options:
860            ServerTunnelOptions {
861                need_codec,
862                is_datagram,
863                keep_alive,
864                namespace,
865                force_namespace,
866            },
867        worker_index,
868        credential,
869        idle_timeout,
870        #[cfg(feature = "p2p")]
871        direct,
872    } = config;
873    let control_permit = tokio::select! {
874        () = shutdown.cancelled() => return Err(Status::Cancelled),
875        () = wake.network_changed() => return Err(Status::NetworkChanged),
876        permit = remote_addr.control_permit() => permit,
877    };
878    let started = tokio::time::Instant::now();
879    let timeout = backoff.timing.timeout();
880    let deadline = started + timeout;
881    diagnostics.phase(RecoveryPhase::Resolving);
882    let addresses = tokio::select! {
883        () = shutdown.cancelled() => return Err(Status::Cancelled),
884        () = wake.network_changed() => return Err(Status::NetworkChanged),
885        result = tokio::time::timeout_at(deadline, remote_addr.addresses()) => match result {
886            Ok(Ok(addresses)) => addresses,
887            _ => return Err(Status::Resolve),
888        },
889    };
890    diagnostics.phase(RecoveryPhase::Connecting);
891    let mut manager_stream = tokio::select! {
892        () = shutdown.cancelled() => return Err(Status::Cancelled),
893        () = wake.network_changed() => return Err(Status::NetworkChanged),
894        result = tokio::time::timeout_at(deadline, remote_addr.connect_addresses(&addresses, credential, carrier)) => {
895            match result {
896                Ok(Ok(stream)) => stream,
897                Ok(Err(error)) => {
898                    tracing::debug!(event = "local_server_dial_failed", %error, %key, worker_index);
899                    if let crate::relay::ConnectError::Rejected { response } = error {
900                        remote_addr.protocol_succeeded();
901                        diagnostics.responded(started.elapsed());
902                        backoff.timing.record(started.elapsed());
903                        return Err(Status::rejected(response));
904                    }
905                    return Err(Status::ConnectRemote);
906                }
907                Err(_) => {
908                    backoff.timing.timed_out();
909                    tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "dial");
910                    return Err(Status::Timeout);
911                }
912            }
913        }
914    };
915    tracing::debug!(
916        event = "local_server_connected_remote",
917        key = %key,
918        worker_index,
919        local_addr = %local_addr,
920        remote_addr = %remote_addr,
921        need_codec,
922        is_datagram,
923        "local server connected to pb server"
924    );
925
926    snafu_error_handle!(manager_stream.configure(keep_alive));
927
928    let registered_addr = manager_stream
929        .relay_addr()
930        .map(ResolvedAddrs::from)
931        .unwrap_or(addresses);
932    diagnostics.phase(RecoveryPhase::Handshake);
933    // Start registration with a protocol-v2 first frame. The session is reused for all
934    // subsequent control messages on this TCP connection. The credential is pinned
935    // when the worker starts so a later process-key change cannot retarget reconnects.
936    let session = match ClientHeaderSession::new_v2(&credential) {
937        Ok(session) => session,
938        Err(error) => {
939            tracing::error!("create manager protocol-v2 session failed: {error}");
940            return Err(Status::ConnectRemote);
941        }
942    };
943    let heartbeat_interval = control_heartbeat_interval();
944    let heartbeat_tolerance = control_heartbeat_tolerance();
945    #[cfg(feature = "p2p")]
946    let direct_version = direct
947        .as_ref()
948        .map(|_| pb_mapper_protocol::direct::DIRECT_VERSION);
949    #[cfg(not(feature = "p2p"))]
950    let direct_version = None;
951    let request = match namespace {
952        Some(namespace) => PbConnRequest::RegisterScoped {
953            direct: direct_version,
954            key: key.to_string(),
955            namespace,
956            force_namespace,
957            need_codec,
958            is_datagram,
959            protocol_version: Some(CONTROL_PROTOCOL_V2),
960            client_instance_id: Some(new_client_instance_id(worker_index)),
961            heartbeat_interval_ms: Some(duration_to_millis(heartbeat_interval)),
962            heartbeat_tolerance_ms: Some(duration_to_millis(heartbeat_tolerance)),
963        },
964        None => PbConnRequest::Register {
965            direct: direct_version,
966            key: key.to_string(),
967            need_codec,
968            is_datagram,
969            protocol_version: Some(CONTROL_PROTOCOL_V2),
970            client_instance_id: Some(new_client_instance_id(worker_index)),
971            heartbeat_interval_ms: Some(duration_to_millis(heartbeat_interval)),
972            heartbeat_tolerance_ms: Some(duration_to_millis(heartbeat_tolerance)),
973        },
974    };
975    let msg = snafu_error_get_or_return_ok!(request.encode().context(EncodeRegisterReqSnafu));
976    tokio::select! {
977        () = shutdown.cancelled() => return Err(Status::Cancelled),
978        () = wake.network_changed() => return Err(Status::NetworkChanged),
979        result = tokio::time::timeout_at(deadline, session.write_initial(&mut manager_stream, &msg)) => {
980            match result {
981                Ok(result) => snafu_error_get_or_return_ok!(result.context(SendRegisterReqSnafu)),
982                Err(_) => {
983                    backoff.timing.timed_out();
984                    tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "write");
985                    return Err(Status::Timeout);
986                }
987            }
988        }
989    }
990    let (mut reader, mut writer) = manager_stream.into_split();
991    let mut msg_reader = match session.response_reader(&mut reader) {
992        Ok(reader) => reader,
993        Err(e) => {
994            tracing::error!("create manager header reader failed: {e}");
995            return Err(Status::ReadMsg);
996        }
997    };
998    // read register resp to indicate that register has finished
999    let (key, registration) = {
1000        let msg = tokio::select! {
1001            () = shutdown.cancelled() => return Err(Status::Cancelled),
1002            () = wake.network_changed() => return Err(Status::NetworkChanged),
1003            result = tokio::time::timeout_at(deadline, msg_reader.read_msg()) => {
1004                match result {
1005                    Ok(result) => snafu_error_get_or_return_ok!(result.context(ReadRegisterRespSnafu)),
1006                    Err(_) => {
1007                        backoff.timing.timed_out();
1008                        tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "response");
1009                        return Err(Status::Timeout);
1010                    }
1011                }
1012            }
1013        };
1014        let resp = snafu_error_get_or_return_ok!(
1015            PbConnResponse::decode(msg).context(DecodeRegisterRespSnafu)
1016        );
1017        remote_addr.protocol_succeeded();
1018        diagnostics.responded(started.elapsed());
1019        backoff.timing.record(started.elapsed());
1020        let registration = match resp {
1021            PbConnResponse::RegisterV2 {
1022                conn_id,
1023                generation,
1024                lease_ttl_ms,
1025                ..
1026            } => ControlRegistration {
1027                conn_id,
1028                generation,
1029                protocol_version: CONTROL_PROTOCOL_V2,
1030                lease_ttl_ms,
1031            },
1032            PbConnResponse::Register(conn_id) => ControlRegistration {
1033                conn_id,
1034                generation: 0,
1035                protocol_version: 1,
1036                lease_ttl_ms: 0,
1037            },
1038            // A rejection is an answer, not a failure to get one, so it never
1039            // goes through `snafu_error_get_or_return_ok!`: that would report the
1040            // attempt as having "finished without an error" and retry it on the
1041            // transport ladder, which is how a refused registration turned into
1042            // thousands of reject lines a minute.
1043            PbConnResponse::Error(error) => {
1044                tracing::debug!(
1045                    event = "local_server_registration_rejected",
1046                    key = %key,
1047                    worker_index,
1048                    reason = %error.code,
1049                    retryable = error.retryable,
1050                    message = %error.message,
1051                    "pb server rejected service registration"
1052                );
1053                return Err(Status::rejected(error));
1054            }
1055            _ => snafu_error_get_or_return_ok!(RegisterRespNotMatchSnafu {}.fail()),
1056        };
1057        tracing::info!(
1058            event = "local_server_registered",
1059            key = %key,
1060            conn_id = %registration.conn_id,
1061            generation = registration.generation,
1062            protocol_version = registration.protocol_version,
1063            lease_ttl_ms = registration.lease_ttl_ms,
1064            worker_index,
1065            local_addr = %local_addr,
1066            remote_addr = %remote_addr,
1067            "local server registered with pb server"
1068        );
1069
1070        // This worker holds a registered control connection, which is on its own
1071        // enough to make the service reachable.
1072        report(WorkerStatus::Connected);
1073        (key, registration)
1074    };
1075
1076    drop(control_permit);
1077    diagnostics.succeeded(started.elapsed());
1078    remote_addr.protocol_succeeded();
1079    backoff.timing.record(started.elapsed());
1080    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);
1081    backoff.reset();
1082    let (write_tx, mut write_rx) = tokio::sync::mpsc::channel::<LocalControlWrite>(64);
1083    #[cfg(feature = "p2p")]
1084    if let Some(direct) = &direct {
1085        direct.connected(write_tx.clone());
1086    }
1087    let lease_state = Arc::new(tokio::sync::Mutex::new(ControlLeaseState::new()));
1088    let writer_key = key.clone();
1089    let writer_registration = registration;
1090    let mut writer_handle = tokio_util::task::AbortOnDropHandle::new(tokio::spawn(async move {
1091        let mut msg_writer = match session.continuation_writer(&mut writer) {
1092            Ok(writer) => writer,
1093            Err(e) => {
1094                tracing::error!("create manager header writer failed: {e}");
1095                return Err(Status::SendPing);
1096            }
1097        };
1098        loop {
1099            let Some(cmd) = write_rx.recv().await else {
1100                return Ok(());
1101            };
1102            match cmd {
1103                #[cfg(feature = "p2p")]
1104                LocalControlWrite::Direct(message) => {
1105                    let payload = PbServerRequest::Direct(*message)
1106                        .encode()
1107                        .map_err(|_| Status::SendPing)?;
1108                    tokio::time::timeout(control_io_timeout(), msg_writer.write_msg(&payload))
1109                        .await
1110                        .map_err(|_| Status::SendPing)?
1111                        .map_err(|_| Status::SendPing)?;
1112                }
1113                LocalControlWrite::Ping { seq } => {
1114                    snafu_error_get_or_return!(
1115                        handle_ping_interval(
1116                            &mut msg_writer,
1117                            writer_key.clone(),
1118                            writer_registration,
1119                            seq
1120                        )
1121                        .await,
1122                        "[send ping]",
1123                        Err(Status::SendPing)
1124                    );
1125                    tracing::debug!(
1126                        event = "local_server_heartbeat_sent",
1127                        key = %writer_key,
1128                        conn_id = %writer_registration.conn_id,
1129                        generation = writer_registration.generation,
1130                        protocol_version = writer_registration.protocol_version,
1131                        seq,
1132                        interval = ?control_heartbeat_interval(),
1133                        "local server heartbeat sent"
1134                    );
1135                }
1136                LocalControlWrite::StreamAck {
1137                    client_id,
1138                    server_generation,
1139                } => {
1140                    snafu_error_get_or_return!(
1141                        write_stream_ack(&mut msg_writer, client_id, server_generation).await,
1142                        "[send stream ack]",
1143                        Err(Status::SendPing)
1144                    );
1145                }
1146            }
1147        }
1148    }));
1149
1150    let heartbeat_interval = control_heartbeat_interval();
1151    let heartbeat_tolerance = control_heartbeat_tolerance();
1152    let suspect_grace = control_suspect_grace();
1153    let mut heartbeat = tokio::time::interval(heartbeat_interval);
1154    heartbeat.set_missed_tick_behavior(MissedTickBehavior::Delay);
1155    heartbeat.tick().await;
1156    let mut probes = JoinSet::new();
1157    let mut ping_seq = 0_u64;
1158    let mut control_deadline = tokio::time::Instant::now() + heartbeat_tolerance + suspect_grace;
1159
1160    let result = loop {
1161        tokio::select! {
1162            msg = msg_reader.read_msg() => {
1163                let msg = match msg.context(ReadStreamReqSnafu) {
1164                    Ok(msg) => msg,
1165                    Err(e) => {
1166                        tracing::error!(
1167                            event = "local_server_control_read_failed",
1168                            key = %key,
1169                            conn_id = %registration.conn_id,
1170                            generation = registration.generation,
1171                            error = %snafu::Report::from_error(e),
1172                            "local server control read failed"
1173                        );
1174                        break Err(Status::ReadMsg);
1175                    }
1176                };
1177                remote_addr.protocol_succeeded();
1178                diagnostics.heard();
1179                lease_state.lock().await.record_rx();
1180                control_deadline = tokio::time::Instant::now() + heartbeat_tolerance + suspect_grace;
1181                snafu_error_get_or_continue!(
1182                    handle_request::<LocalStream>(
1183                        msg,
1184                        StreamConnect {
1185                            local_addr: local_addr.clone(),
1186                            remote_addr: registered_addr.clone(),
1187                            keep_alive,
1188                            namespace,
1189                            credential,
1190                            idle_timeout,
1191                        },
1192                        remote_addr.clone(),
1193                        key.clone(),
1194                        registration.conn_id,
1195                        &write_tx,
1196                        lease_state.clone(),
1197                        stream_tasks,
1198                        &shutdown,
1199                        setup_slots,
1200                        #[cfg(feature = "p2p")]
1201                        direct.as_ref(),
1202                    )
1203                    .await
1204                );
1205            }
1206            Some(_) = stream_tasks.join_next() => {
1207                // Reap finished sessions so the set does not grow for the lifetime
1208                // of the registration. `handle_stream` logs its own failures.
1209            }
1210            result = &mut writer_handle => {
1211                break match result {
1212                    Ok(result) => result,
1213                    Err(e) => {
1214                        tracing::error!(
1215                            event = "local_server_control_writer_join_failed",
1216                            key = %key,
1217                            conn_id = %registration.conn_id,
1218                            generation = registration.generation,
1219                            error = %e,
1220                            "local server control writer task failed"
1221                        );
1222                        Err(Status::SendPing)
1223                    }
1224                };
1225            }
1226            _ = heartbeat.tick() => {
1227                ping_seq = ping_seq.wrapping_add(1);
1228                if write_tx.try_send(LocalControlWrite::Ping { seq: ping_seq }).is_err() {
1229                    break Err(Status::SendPing);
1230                }
1231
1232                let last_rx_age = lease_state.lock().await.last_rx_age();
1233                if registration.protocol_version >= CONTROL_PROTOCOL_V2
1234                    && last_rx_age >= heartbeat_tolerance
1235                    && probes.is_empty()
1236                {
1237                    tracing::warn!(
1238                        event = "local_server_lease_suspect",
1239                        key = %key,
1240                        conn_id = %registration.conn_id,
1241                        generation = registration.generation,
1242                        worker_index,
1243                        last_rx_age_ms = duration_to_millis(last_rx_age),
1244                        heartbeat_tolerance_ms = duration_to_millis(heartbeat_tolerance),
1245                        "local server control lease is suspect; probing remote registration"
1246                    );
1247                    let probe_key = key.clone();
1248                    let probe_remote = registered_addr.clone();
1249                    let probe_endpoint = remote_addr.clone();
1250                    probes.spawn(async move {
1251                        let _permit = probe_endpoint.control_permit().await;
1252                        probe_remote_registration(
1253                            probe_remote,
1254                            probe_endpoint.clone(),
1255                            probe_key,
1256                            registration,
1257                            namespace,
1258                            credential,
1259                        )
1260                        .await
1261                    });
1262                }
1263            }
1264            () = shutdown.cancelled() => {
1265                tracing::info!(
1266                    event = "local_server_control_cancelled",
1267                    key = %key,
1268                    conn_id = %registration.conn_id,
1269                    generation = registration.generation,
1270                    worker_index,
1271                    "local server control loop cancelled"
1272                );
1273                break Err(Status::Cancelled);
1274            }
1275            () = tokio::time::sleep_until(control_deadline) => {
1276                tracing::warn!(event = "local_server_control_unresponsive", %key, worker_index, "control socket received no reply past grace; reconnecting");
1277                break Err(Status::ReadMsg);
1278            }
1279            Some(probe_result) = probes.join_next() => {
1280                let probe_result = probe_result.unwrap_or_else(|error| RegistrationProbeResult::Failed(error.to_string()));
1281                let last_rx_age = lease_state.lock().await.last_rx_age();
1282                match probe_result {
1283                    RegistrationProbeResult::Present => {
1284                        tracing::debug!(
1285                            event = "local_server_registration_probe_ok",
1286                            key = %key,
1287                            conn_id = %registration.conn_id,
1288                            generation = registration.generation,
1289                            worker_index,
1290                            last_rx_age_ms = duration_to_millis(last_rx_age),
1291                            "remote registration still contains this control connection"
1292                        );
1293                    }
1294                    RegistrationProbeResult::Missing if last_rx_age >= heartbeat_tolerance => {
1295                        tracing::warn!(
1296                            event = "local_server_registration_missing",
1297                            key = %key,
1298                            conn_id = %registration.conn_id,
1299                            generation = registration.generation,
1300                            worker_index,
1301                            last_rx_age_ms = duration_to_millis(last_rx_age),
1302                            "remote registration no longer contains this control connection; reconnecting"
1303                        );
1304                        break Err(Status::ReadMsg);
1305                    }
1306                    RegistrationProbeResult::Missing => {
1307                        tracing::debug!(
1308                            event = "local_server_registration_missing_ignored_after_recent_activity",
1309                            key = %key,
1310                            conn_id = %registration.conn_id,
1311                            generation = registration.generation,
1312                            worker_index,
1313                            last_rx_age_ms = duration_to_millis(last_rx_age),
1314                            "remote registration probe was stale after recent control activity"
1315                        );
1316                    }
1317                    RegistrationProbeResult::Failed(reason)
1318                        if last_rx_age >= heartbeat_tolerance + suspect_grace =>
1319                    {
1320                        tracing::warn!(
1321                            event = "local_server_status_probe_failed",
1322                            key = %key,
1323                            conn_id = %registration.conn_id,
1324                            generation = registration.generation,
1325                            worker_index,
1326                            last_rx_age_ms = duration_to_millis(last_rx_age),
1327                            reason = %reason,
1328                            "registration probe failed past suspect grace; reconnecting"
1329                        );
1330                        break Err(Status::ReadMsg);
1331                    }
1332                    RegistrationProbeResult::Failed(reason) => {
1333                        tracing::warn!(
1334                            event = "local_server_status_probe_failed",
1335                            key = %key,
1336                            conn_id = %registration.conn_id,
1337                            generation = registration.generation,
1338                            worker_index,
1339                            last_rx_age_ms = duration_to_millis(last_rx_age),
1340                            reason = %reason,
1341                            "registration probe failed; waiting inside suspect grace"
1342                        );
1343                    }
1344                }
1345            }
1346        }
1347    };
1348    if !writer_handle.is_finished() {
1349        writer_handle.abort();
1350    }
1351    result
1352}
1353
1354#[instrument(skip(writer))]
1355async fn handle_ping_interval<T: MessageWriter>(
1356    writer: &mut T,
1357    _key: Arc<str>,
1358    registration: ControlRegistration,
1359    seq: u64,
1360) -> error::Result<()> {
1361    let ping_msg = get_ping_message(registration.protocol_version, seq)?;
1362    let timeout = control_io_timeout();
1363    match tokio::time::timeout(timeout, writer.write_msg(&ping_msg)).await {
1364        Ok(result) => result.context(WritePingMsgSnafu),
1365        Err(_) => ControlIoTimeoutSnafu {
1366            action: "write ping message",
1367            timeout,
1368        }
1369        .fail(),
1370    }
1371}
1372
1373#[instrument(skip_all, fields(%key, conn_id))]
1374#[allow(clippy::too_many_arguments)]
1375async fn handle_request<LocalStream: StreamProvider>(
1376    msg: &[u8],
1377    target: StreamConnect,
1378    relay_endpoint: RelayEndpoint,
1379    key: Arc<str>,
1380    conn_id: u32,
1381    write_tx: &tokio::sync::mpsc::Sender<LocalControlWrite>,
1382    lease_state: Arc<tokio::sync::Mutex<ControlLeaseState>>,
1383    stream_tasks: &mut JoinSet<()>,
1384    shutdown: &CancellationToken,
1385    setup_slots: &Arc<tokio::sync::Semaphore>,
1386    #[cfg(feature = "p2p")] direct: Option<&Arc<crate::direct::Provider>>,
1387) -> error::Result<()>
1388where
1389    LocalStream::Item: StreamForward,
1390{
1391    let req = LocalServer::decode(msg).context(DecodeStreamReqSnafu)?;
1392
1393    match req {
1394        LocalServer::Direct(event) => {
1395            #[cfg(feature = "p2p")]
1396            if let Some(direct) = direct {
1397                let mut event = event;
1398                // `remote_addr` is the relay address this registration is
1399                // connected to: the one to probe when the relay advertises
1400                // only its STUN port.
1401                if let (
1402                    pb_mapper_protocol::direct::DirectEvent::Offer { stun_addr, .. },
1403                    Some(relay),
1404                ) = (&mut *event, target.remote_addr.as_slice().first())
1405                {
1406                    *stun_addr = pb_mapper_protocol::direct::stun_target(*stun_addr, relay.ip());
1407                }
1408                direct.event(*event, write_tx.clone());
1409            }
1410            #[cfg(not(feature = "p2p"))]
1411            let _ = event;
1412        }
1413        LocalServer::Stream {
1414            client_id,
1415            server_generation,
1416        } => {
1417            tracing::debug!(
1418                event = "local_server_stream_request_received",
1419                key = %key,
1420                server_conn_id = %conn_id,
1421                client_conn_id = client_id,
1422                server_generation,
1423                "local server received stream request"
1424            );
1425            let Ok(permit) = setup_slots.clone().try_acquire_owned() else {
1426                tracing::warn!(event = "local_server_setup_saturated", %key, client_id, "stream setup capacity reached; relay can select another worker");
1427                return Ok(());
1428            };
1429            write_tx
1430                .try_send(LocalControlWrite::StreamAck {
1431                    client_id,
1432                    server_generation,
1433                })
1434                .map_err(|_| error::Error::ControlWriterClosed {
1435                    action: "stream ack message",
1436                })?;
1437            let key = key.clone();
1438            let stream_shutdown = shutdown.clone();
1439            // Tracked rather than detached: a cancelled registration must take its
1440            // in-flight forwarded sessions with it, so `stop()` returning means no
1441            // stream of this tunnel is still moving bytes.
1442            stream_tasks.spawn(async move {
1443                let forward = handle_stream_on_endpoint::<LocalStream>(
1444                    key,
1445                    client_id,
1446                    server_generation,
1447                    target,
1448                    permit,
1449                    relay_endpoint,
1450                );
1451                tokio::select! {
1452                    () = stream_shutdown.cancelled() => {}
1453                    result = forward => snafu_error_handle!(result),
1454                }
1455            });
1456        }
1457        // got pong response
1458        LocalServer::Pong => {
1459            lease_state.lock().await.record_pong();
1460            tracing::debug!(
1461                event = "local_server_pong_received",
1462                key = %key,
1463                server_conn_id = %conn_id,
1464                "local server received pong"
1465            );
1466        }
1467        LocalServer::PongV2 { seq } => {
1468            lease_state.lock().await.record_pong();
1469            tracing::debug!(
1470                event = "local_server_pong_received",
1471                key = %key,
1472                server_conn_id = %conn_id,
1473                seq,
1474                "local server received pong v2"
1475            );
1476        }
1477        LocalServer::Retire {
1478            reason,
1479            conn_id: retired_conn_id,
1480            server_generation,
1481        } => {
1482            tracing::warn!(
1483                event = "local_server_control_retired",
1484                key = %key,
1485                server_conn_id = %conn_id,
1486                retired_conn_id,
1487                server_generation,
1488                reason = %reason,
1489                "remote server retired this local control connection"
1490            );
1491        }
1492    }
1493    Ok(())
1494}
1495
1496async fn write_stream_ack<T: MessageWriter>(
1497    writer: &mut T,
1498    client_id: u32,
1499    server_generation: u64,
1500) -> error::Result<()> {
1501    let ack = PbServerRequest::StreamAck {
1502        client_id,
1503        server_generation,
1504    }
1505    .encode()
1506    .context(EncodeStreamAckMsgSnafu)?;
1507    let timeout = control_io_timeout();
1508    match tokio::time::timeout(timeout, writer.write_msg(&ack)).await {
1509        Ok(result) => result.context(WriteStreamAckMsgSnafu),
1510        Err(_) => ControlIoTimeoutSnafu {
1511            action: "write stream ack message",
1512            timeout,
1513        }
1514        .fail(),
1515    }
1516}
1517
1518#[cfg(test)]
1519mod pool_status_tests {
1520    use std::sync::Mutex as StdMutex;
1521
1522    use super::*;
1523
1524    /// Records what the pool published, in order.
1525    fn recording_pool(pool_size: usize) -> (PoolStatus, Arc<StdMutex<Vec<String>>>) {
1526        let seen = Arc::new(StdMutex::new(Vec::new()));
1527        let sink = seen.clone();
1528        let callback: StatusCallback = Box::new(move |status: &str| {
1529            sink.lock().unwrap().push(status.to_string());
1530        });
1531        (PoolStatus::new(callback, pool_size), seen)
1532    }
1533
1534    fn published(seen: &Arc<StdMutex<Vec<String>>>) -> Vec<String> {
1535        seen.lock().unwrap().clone()
1536    }
1537
1538    /// One connected worker is enough: the service is reachable through it, so
1539    /// the pool must not report the other worker's retry as the pool's state.
1540    #[test]
1541    fn one_connected_worker_makes_the_pool_connected() {
1542        let (pool, seen) = recording_pool(2);
1543        pool.report(0, WorkerStatus::Connected);
1544        pool.report(1, WorkerStatus::Retrying);
1545        assert_eq!(published(&seen), vec!["connected".to_string()]);
1546    }
1547
1548    /// The guarantee `wait_ready()` depends on: while any worker might still
1549    /// bring the service up, one worker's permanent rejection is not the pool's.
1550    #[test]
1551    fn a_single_failure_does_not_fail_the_pool() {
1552        let (pool, seen) = recording_pool(2);
1553        pool.report(0, WorkerStatus::Failed("service_transport_mismatch".into()));
1554        assert_eq!(published(&seen), vec!["retrying".to_string()]);
1555
1556        pool.report(1, WorkerStatus::Connected);
1557        assert_eq!(
1558            published(&seen),
1559            vec!["retrying".to_string(), "connected".to_string()]
1560        );
1561    }
1562
1563    /// Once every worker has given up, the pool reports failed and says why —
1564    /// otherwise a caller awaiting readiness with no timeout waits forever.
1565    #[test]
1566    fn the_pool_fails_only_when_every_worker_has() {
1567        let (pool, seen) = recording_pool(2);
1568        pool.report(0, WorkerStatus::Failed("service_transport_mismatch".into()));
1569        pool.report(1, WorkerStatus::Failed("namespace_access_denied".into()));
1570        assert_eq!(
1571            published(&seen),
1572            vec![
1573                "retrying".to_string(),
1574                "failed: service_transport_mismatch".to_string(),
1575            ],
1576            "the first permanent rejection is the one that explains the pool"
1577        );
1578    }
1579
1580    /// A worker that reconnects repeatedly must not turn into a stream of
1581    /// identical status lines for the caller.
1582    #[test]
1583    fn unchanged_aggregates_are_not_republished() {
1584        let (pool, seen) = recording_pool(2);
1585        pool.report(0, WorkerStatus::Connected);
1586        pool.report(1, WorkerStatus::Connected);
1587        pool.report(0, WorkerStatus::Connected);
1588        assert_eq!(published(&seen), vec!["connected".to_string()]);
1589    }
1590
1591    /// Losing the last connected worker has to be visible: the service is no
1592    /// longer reachable, and the caller's status must say so.
1593    #[test]
1594    fn losing_the_last_connection_publishes_retrying() {
1595        let (pool, seen) = recording_pool(1);
1596        pool.report(0, WorkerStatus::Connected);
1597        pool.report(0, WorkerStatus::Retrying);
1598        assert_eq!(
1599            published(&seen),
1600            vec!["connected".to_string(), "retrying".to_string()]
1601        );
1602    }
1603}
1604
1605#[cfg(test)]
1606mod shutdown_tests {
1607    use std::time::Duration;
1608
1609    use super::*;
1610    use uni_stream::stream::TcpStreamProvider;
1611
1612    #[tokio::test]
1613    async fn register_loop_stops_on_cancel() {
1614        let shutdown = CancellationToken::new();
1615        let token = shutdown.clone();
1616        let addr = ResolvedAddrs::from("127.0.0.1:1".parse::<std::net::SocketAddr>().unwrap());
1617        let task = tokio::spawn(async move {
1618            run_server_side_cli_with_shutdown::<TcpStreamProvider>(
1619                addr.clone(),
1620                addr,
1621                "k".into(),
1622                ServerTunnelOptions {
1623                    need_codec: false,
1624                    is_datagram: false,
1625                    keep_alive: false,
1626                    namespace: None,
1627                    force_namespace: false,
1628                },
1629                None,
1630                Credential::Admin(*b"0123456789abcdefghijklmnopqrstuv"),
1631                token,
1632            )
1633            .await;
1634        });
1635        tokio::time::sleep(Duration::from_millis(80)).await;
1636        shutdown.cancel();
1637        tokio::time::timeout(Duration::from_secs(3), task)
1638            .await
1639            .expect("register loop did not stop after cancel")
1640            .expect("join");
1641    }
1642}