1use crate::diagnostics::{Diagnostics, RecoveryPhase};
9use crate::endpoint::RelayEndpoint;
10use crate::relay::CarrierId;
11use std::sync::{Arc, RwLock};
12
13use pb_mapper_core::checksum::{Credential, parse_credential};
14use pb_mapper_core::config::{ResolvedAddrs, control_io_timeout, resolve_addrs_async};
15use pb_mapper_protocol::command::{PbConnStatusReq, PbConnStatusResp};
16use tokio::sync::watch;
17use tokio_util::sync::CancellationToken;
18use uni_stream::stream::{
19 TcpListenerProvider, TcpStreamProvider, UdpListenerProvider, UdpStreamProvider,
20};
21
22use snafu::ResultExt;
23
24use super::Error;
25use super::admin::Admin;
26use super::error::{AddressSnafu, Result, StatusSnafu};
27use super::handle::{Connection, LiveTunnel, Registration};
28use super::types::{RemoteId, ServiceConnection, Transport, TunnelStatus};
29use super::{ConnectOptions, PathPolicy, RegisterOptions};
30use crate::client::run_client_side_cli_recovering_with_path;
31use crate::client::status::get_status_with_credential;
32use crate::server::{ServerTunnelOptions, StatusCallback, run_server_side_cli_recovering};
33
34#[derive(Clone)]
36pub struct ClientConfig {
37 pub server: String,
39 pub credential: String,
41 pub keep_alive: bool,
42 pub namespace: Option<u64>,
45}
46
47impl std::fmt::Debug for ClientConfig {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("ClientConfig")
50 .field("server", &self.server)
51 .field("credential", &"[redacted]")
52 .field("keep_alive", &self.keep_alive)
53 .field("namespace", &self.namespace)
54 .finish()
55 }
56}
57
58#[derive(Clone, Debug)]
60pub struct RegisterRequest {
61 pub key: String,
62 pub local_addr: String,
63 pub transport: Transport,
64 pub codec: bool,
65 pub force_namespace: bool,
66}
67
68#[derive(Clone, Debug)]
70pub struct ConnectRequest {
71 pub key: String,
72 pub local_addr: String,
73 pub transport: Transport,
74}
75
76pub(crate) struct ClientInner {
77 pub(crate) server: String,
78 pub(crate) endpoint: RelayEndpoint,
79 pub(crate) credential: RwLock<Credential>,
80 pub(crate) keep_alive: bool,
81 pub(crate) namespace: Option<u64>,
82}
83
84#[derive(Clone)]
89pub struct Client {
90 pub(crate) inner: Arc<ClientInner>,
91}
92
93impl Client {
94 pub fn new(config: ClientConfig) -> Result<Self> {
95 if config.server.trim().is_empty() {
96 return Err(Error::invalid_config("server address is required"));
97 }
98 if config.credential.trim().is_empty() {
99 return Err(Error::invalid_config("credential is required"));
100 }
101 let credential =
102 parse_credential(config.credential.trim()).map_err(Error::invalid_config)?;
103 Ok(Self::from_credential(
104 config.server,
105 credential,
106 config.keep_alive,
107 config.namespace,
108 ))
109 }
110
111 pub fn from_credential(
113 server: impl Into<String>,
114 credential: Credential,
115 keep_alive: bool,
116 namespace: Option<u64>,
117 ) -> Self {
118 let server = server.into();
119 let endpoint = RelayEndpoint::shared(&server);
120 Self {
121 inner: Arc::new(ClientInner {
122 server,
123 endpoint,
124 credential: RwLock::new(credential),
125 keep_alive,
126 namespace,
127 }),
128 }
129 }
130
131 pub fn with_relay_transport(&self, transport: super::RelayTransport) -> Result<Self> {
134 if transport == self.relay_transport() {
135 return Ok(self.clone());
136 }
137 let endpoint = self
138 .inner
139 .endpoint
140 .with_transport(transport)
141 .map_err(|error| Error::invalid_config(error.to_string()))?;
142 Ok(Self {
143 inner: Arc::new(ClientInner {
144 server: self.inner.server.clone(),
145 endpoint,
146 credential: RwLock::new(self.credential()),
147 keep_alive: self.inner.keep_alive,
148 namespace: self.inner.namespace,
149 }),
150 })
151 }
152 pub fn relay_transport(&self) -> super::RelayTransport {
153 self.inner.endpoint.transport()
154 }
155 pub async fn relay_diagnostics(&self) -> super::RelayDiagnostics {
158 self.inner.endpoint.relay_diagnostics().await
159 }
160
161 pub async fn relay_quic_port(&self) -> Result<Option<u16>> {
166 let timeout = control_io_timeout().min(std::time::Duration::from_secs(5));
167 tokio::time::timeout(timeout, self.inner.endpoint.quic_port(self.credential()))
168 .await
169 .unwrap_or(Err(
170 std::io::Error::from(std::io::ErrorKind::TimedOut).into()
171 ))
172 .map_err(|error| Error::from_connect(error, self.inner.server.clone()))
173 }
174
175 pub fn notify_network_change(&self) {
177 self.inner.endpoint.notify_network_change();
178 }
179
180 pub fn server(&self) -> &str {
181 &self.inner.server
182 }
183
184 pub fn namespace(&self) -> Option<u64> {
185 self.inner.namespace
186 }
187
188 pub(crate) fn credential(&self) -> Credential {
189 *self
190 .inner
191 .credential
192 .read()
193 .unwrap_or_else(|poisoned| poisoned.into_inner())
194 }
195
196 pub fn admin(&self) -> Result<Admin> {
198 if !self.credential().is_admin() {
199 return Err(Error::NotAdministrator);
200 }
201 Ok(Admin {
202 inner: Arc::clone(&self.inner),
203 })
204 }
205
206 pub async fn register(&self, request: RegisterRequest) -> Result<Registration> {
213 self.register_with_options(request, RegisterOptions::default())
214 .await
215 }
216
217 pub async fn register_with_idle_timeout(
224 &self,
225 request: RegisterRequest,
226 idle_timeout: std::time::Duration,
227 ) -> Result<Registration> {
228 if request.transport.is_datagram() {
229 return Err(Error::invalid_config(
230 "a registration idle deadline requires TCP",
231 ));
232 }
233 if tokio::time::Instant::now()
234 .checked_add(idle_timeout)
235 .is_none()
236 {
237 return Err(Error::invalid_config("idle timeout exceeds timer range"));
238 }
239 self.register_inner(
240 request,
241 Some(idle_timeout),
242 RegisterOptions::default().allow_direct,
243 )
244 .await
245 }
246
247 pub async fn register_with_options(
250 &self,
251 request: RegisterRequest,
252 options: RegisterOptions,
253 ) -> Result<Registration> {
254 if options.allow_direct && !cfg!(feature = "p2p") {
255 return Err(Error::invalid_config("built without p2p support"));
256 }
257 if let Some(timeout) = options.idle_timeout
258 && (request.transport.is_datagram()
259 || tokio::time::Instant::now().checked_add(timeout).is_none())
260 {
261 return Err(Error::invalid_config(
262 "idle deadline requires TCP and a representable duration",
263 ));
264 }
265 self.register_inner(request, options.idle_timeout, options.allow_direct)
266 .await
267 }
268
269 async fn register_inner(
270 &self,
271 request: RegisterRequest,
272 idle_timeout: Option<std::time::Duration>,
273 allow_direct: bool,
274 ) -> Result<Registration> {
275 if request.key.trim().is_empty() {
276 return Err(Error::invalid_config("service key is required"));
277 }
278 let options = ServerTunnelOptions {
279 need_codec: request.codec,
280 is_datagram: request.transport.is_datagram(),
281 keep_alive: self.inner.keep_alive,
282 namespace: self.inner.namespace,
283 force_namespace: request.force_namespace,
284 };
285 let worker = self
286 .prepare_worker(&request.key, &request.local_addr)
287 .await?;
288 let credential = self.credential();
289 let handle = match request.transport {
290 Transport::Tcp => worker.spawn(move |context| {
291 run_server_side_cli_recovering::<TcpStreamProvider>(
292 context.local_addr,
293 context.remote_addr,
294 context.key,
295 options,
296 Some(context.status_callback),
297 Some(credential),
298 context.shutdown,
299 context.diagnostics,
300 idle_timeout,
301 allow_direct,
302 )
303 }),
304 Transport::Udp => worker.spawn(move |context| {
305 run_server_side_cli_recovering::<UdpStreamProvider>(
306 context.local_addr,
307 context.remote_addr,
308 context.key,
309 options,
310 Some(context.status_callback),
311 Some(credential),
312 context.shutdown,
313 context.diagnostics,
314 idle_timeout,
315 allow_direct,
316 )
317 }),
318 };
319 Ok(Registration::new(handle, request.key))
320 }
321
322 pub async fn connect(&self, request: ConnectRequest) -> Result<Connection> {
330 self.connect_with_options(request, ConnectOptions::default())
331 .await
332 }
333
334 pub async fn connect_with_options(
336 &self,
337 request: ConnectRequest,
338 options: ConnectOptions,
339 ) -> Result<Connection> {
340 if options.path == PathPolicy::Auto && !cfg!(feature = "p2p") {
341 return Err(Error::invalid_config("built without p2p support"));
342 }
343 let is_datagram = request.transport.is_datagram();
344 if request.key.trim().is_empty() {
345 return Err(Error::invalid_config("service key is required"));
346 }
347 let worker = self
348 .prepare_worker(&request.key, &request.local_addr)
349 .await?;
350 let credential = self.credential();
351 let keep_alive = self.inner.keep_alive;
352 let namespace = self.inner.namespace;
353 let handle = match request.transport {
354 Transport::Tcp => worker.spawn(move |context| {
355 run_client_side_cli_recovering_with_path::<TcpListenerProvider>(
356 context.local_addr,
357 context.remote_addr,
358 context.key,
359 keep_alive,
360 namespace,
361 Some(context.status_callback),
362 Some(credential),
363 context.shutdown,
364 context.diagnostics,
365 options.path,
366 is_datagram,
367 )
368 }),
369 Transport::Udp => worker.spawn(move |context| {
370 run_client_side_cli_recovering_with_path::<UdpListenerProvider>(
371 context.local_addr,
372 context.remote_addr,
373 context.key,
374 keep_alive,
375 namespace,
376 Some(context.status_callback),
377 Some(credential),
378 context.shutdown,
379 context.diagnostics,
380 options.path,
381 is_datagram,
382 )
383 }),
384 };
385 Ok(Connection::new(handle, request.key))
386 }
387
388 pub async fn list_keys(&self) -> Result<Vec<String>> {
390 match self.status_request(PbConnStatusReq::Keys).await? {
391 PbConnStatusResp::Keys(keys) => Ok(keys),
392 other => Err(Error::protocol(format!(
393 "expected keys status, got {other:?}"
394 ))),
395 }
396 }
397
398 pub async fn sessions(&self) -> Result<Vec<pb_mapper_protocol::direct::SessionObservation>> {
400 match self.status_request(PbConnStatusReq::Sessions).await? {
401 PbConnStatusResp::Sessions(sessions) => Ok(sessions),
402 other => Err(Error::protocol(format!(
403 "expected sessions status, got {other:?}"
404 ))),
405 }
406 }
407
408 pub async fn service_status(&self, key: impl Into<String>) -> Result<Vec<ServiceConnection>> {
409 let key = key.into();
410 match self
411 .status_request(PbConnStatusReq::Service {
412 direct_query: None,
413 key,
414 })
415 .await?
416 {
417 PbConnStatusResp::Service { connections, .. } => Ok(connections
418 .into_iter()
419 .map(ServiceConnection::from)
420 .collect()),
421 other => Err(Error::protocol(format!(
422 "expected service status, got {other:?}"
423 ))),
424 }
425 }
426
427 pub async fn remote_id(&self) -> Result<RemoteId> {
428 RemoteId::from_status(self.status_request(PbConnStatusReq::RemoteId).await?)
429 }
430
431 async fn prepare_worker(&self, key: &str, local_addr: &str) -> Result<TunnelWorker> {
436 if !self.inner.endpoint.validate() {
437 return Err(Error::invalid_config(
438 "relay must be host:port or an IP socket address",
439 ));
440 }
441 let local_addr = resolve(local_addr).await?;
442 let remote_addr = self.inner.endpoint.clone();
443 let (status_tx, status_rx) = watch::channel(TunnelStatus::Starting);
444 Ok(TunnelWorker {
445 local_addr,
446 remote_addr,
447 key: Arc::from(key),
448 shutdown: CancellationToken::new(),
449 status_tx,
450 status_rx,
451 diagnostics: Diagnostics::default(),
452 })
453 }
454
455 async fn status_request(&self, request: PbConnStatusReq) -> Result<PbConnStatusResp> {
456 let credential = self.credential();
457 let io_timeout = control_io_timeout().min(std::time::Duration::from_secs(5));
460 let endpoint = &self.inner.endpoint;
461 let ask = async |carrier: &mut Option<CarrierId>| {
462 let mut stream = endpoint
463 .connect(credential, carrier)
464 .await
465 .map_err(|error| Error::from_connect(error, self.inner.server.clone()))?;
466 get_status_with_credential(
467 &mut stream,
468 request.clone(),
469 self.inner.namespace,
470 &credential,
471 )
472 .await
473 .context(StatusSnafu)
474 };
475 let mut carrier = None;
478 let result = tokio::time::timeout(io_timeout, async {
479 let _permit = endpoint.control_permit().await;
480 match ask(&mut carrier).await {
481 Err(error)
486 if is_carrier_failure(&error)
487 && endpoint.transport() == super::RelayTransport::Quic =>
488 {
489 endpoint.carrier_failed(carrier);
490 ask(&mut carrier).await
491 }
492 result => result,
493 }
494 })
495 .await
496 .unwrap_or(Err(Error::TimedOut {
497 timeout: io_timeout,
498 }));
499 match &result {
500 Ok(_) => endpoint.protocol_succeeded(),
501 Err(error) if is_carrier_failure(error) => endpoint.carrier_failed(carrier),
503 Err(_) => {}
504 }
505 result
506 }
507}
508
509pub(crate) fn is_carrier_failure(error: &Error) -> bool {
512 match error {
513 Error::Remote { .. } => false,
514 Error::Status { source } => source.remote_retryable().is_none(),
515 _ => true,
516 }
517}
518
519async fn resolve(addr: &str) -> Result<ResolvedAddrs> {
525 resolve_addrs_async(addr)
526 .await
527 .context(AddressSnafu { addr })
528}
529
530fn settle_stopped(tx: &watch::Sender<TunnelStatus>) {
536 tx.send_if_modified(|status| match status {
537 TunnelStatus::Failed(_) => false,
538 _ => {
539 *status = TunnelStatus::Stopped;
540 true
541 }
542 });
543}
544
545fn watch_callback(tx: watch::Sender<TunnelStatus>) -> StatusCallback {
550 Box::new(move |status: &str| {
551 let _ = tx.send(TunnelStatus::from_callback(status));
552 })
553}
554
555struct WorkerContext {
558 local_addr: ResolvedAddrs,
559 remote_addr: RelayEndpoint,
560 key: Arc<str>,
561 status_callback: StatusCallback,
562 shutdown: CancellationToken,
563 diagnostics: Diagnostics,
564}
565
566struct TunnelWorker {
573 local_addr: ResolvedAddrs,
574 remote_addr: RelayEndpoint,
575 key: Arc<str>,
576 shutdown: CancellationToken,
577 status_tx: watch::Sender<TunnelStatus>,
578 status_rx: watch::Receiver<TunnelStatus>,
579 diagnostics: Diagnostics,
580}
581
582impl TunnelWorker {
583 fn spawn<F, Fut>(self, start: F) -> LiveTunnel
584 where
585 F: FnOnce(WorkerContext) -> Fut + Send + 'static,
586 Fut: std::future::Future<Output = ()> + Send,
587 {
588 let Self {
589 local_addr,
590 remote_addr,
591 key,
592 shutdown,
593 status_tx,
594 status_rx,
595 diagnostics,
596 } = self;
597 let worker_shutdown = shutdown.clone();
598 let worker_diagnostics = diagnostics.clone();
599 let endpoint = remote_addr.clone();
600 let join = tokio::spawn(async move {
601 start(WorkerContext {
602 local_addr,
603 remote_addr,
604 key,
605 status_callback: watch_callback(status_tx.clone()),
606 shutdown: worker_shutdown,
607 diagnostics: worker_diagnostics.clone(),
608 })
609 .await;
610 worker_diagnostics.phase(RecoveryPhase::Stopped);
611 settle_stopped(&status_tx);
612 });
613 LiveTunnel::new(shutdown, join, status_rx, endpoint, diagnostics)
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
622 fn config_debug_redacts_credentials() {
623 let config = ClientConfig {
624 server: "localhost:7666".into(),
625 credential: "0123456789abcdefghijklmnopqrstuv".into(),
626 keep_alive: false,
627 namespace: None,
628 };
629 let debug = format!("{config:?}");
630 assert!(!debug.contains(&config.credential));
631 assert!(debug.contains("[redacted]"));
632 }
633
634 #[test]
635 fn admin_requires_administrator_credential() {
636 let admin = Client::from_credential(
637 "127.0.0.1:7666",
638 Credential::Admin(*b"0123456789abcdefghijklmnopqrstuv"),
639 false,
640 None,
641 );
642 assert!(admin.admin().is_ok());
643
644 let temporary = Client::from_credential(
645 "127.0.0.1:7666",
646 Credential::Temporary {
647 key_id: 1,
648 key: [0_u8; 32],
649 },
650 false,
651 None,
652 );
653 assert!(matches!(temporary.admin(), Err(Error::NotAdministrator)));
654 }
655}