1use crate::diagnostics::Diagnostics;
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, PortMapping, 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 RegisterOptions {
242 idle_timeout: Some(idle_timeout),
243 ..Default::default()
244 },
245 )
246 .await
247 }
248
249 pub async fn register_with_options(
252 &self,
253 request: RegisterRequest,
254 options: RegisterOptions,
255 ) -> Result<Registration> {
256 if options.allow_direct && !cfg!(feature = "p2p") {
257 return Err(Error::invalid_config("built without p2p support"));
258 }
259 check_port_mapping(options.port_mapping)?;
260 if let Some(timeout) = options.idle_timeout
261 && (request.transport.is_datagram()
262 || tokio::time::Instant::now().checked_add(timeout).is_none())
263 {
264 return Err(Error::invalid_config(
265 "idle deadline requires TCP and a representable duration",
266 ));
267 }
268 self.register_inner(request, options).await
269 }
270
271 async fn register_inner(
272 &self,
273 request: RegisterRequest,
274 options: RegisterOptions,
275 ) -> Result<Registration> {
276 let RegisterOptions {
277 allow_direct,
278 idle_timeout,
279 port_mapping,
280 } = options;
281 if request.key.trim().is_empty() {
282 return Err(Error::invalid_config("service key is required"));
283 }
284 let tunnel = ServerTunnelOptions {
285 need_codec: request.codec,
286 is_datagram: request.transport.is_datagram(),
287 keep_alive: self.inner.keep_alive,
288 namespace: self.inner.namespace,
289 force_namespace: request.force_namespace,
290 };
291 let worker = self
292 .prepare_worker(&request.key, &request.local_addr)
293 .await?;
294 let credential = self.credential();
295 let port_mapping = worker.port_mapping(if allow_direct {
296 port_mapping
297 } else {
298 PortMapping::Off
299 });
300 let handle = match request.transport {
301 Transport::Tcp => worker.spawn(move |context| {
302 run_server_side_cli_recovering::<TcpStreamProvider>(
303 context.local_addr,
304 context.remote_addr,
305 context.key,
306 tunnel,
307 Some(context.status_callback),
308 Some(credential),
309 context.shutdown,
310 context.diagnostics,
311 idle_timeout,
312 allow_direct,
313 port_mapping,
314 )
315 }),
316 Transport::Udp => worker.spawn(move |context| {
317 run_server_side_cli_recovering::<UdpStreamProvider>(
318 context.local_addr,
319 context.remote_addr,
320 context.key,
321 tunnel,
322 Some(context.status_callback),
323 Some(credential),
324 context.shutdown,
325 context.diagnostics,
326 idle_timeout,
327 allow_direct,
328 port_mapping,
329 )
330 }),
331 };
332 Ok(Registration::new(handle, request.key))
333 }
334
335 pub async fn connect(&self, request: ConnectRequest) -> Result<Connection> {
343 self.connect_with_options(request, ConnectOptions::default())
344 .await
345 }
346
347 pub async fn connect_with_options(
349 &self,
350 request: ConnectRequest,
351 options: ConnectOptions,
352 ) -> Result<Connection> {
353 if options.path == PathPolicy::Auto && !cfg!(feature = "p2p") {
354 return Err(Error::invalid_config("built without p2p support"));
355 }
356 check_port_mapping(options.port_mapping)?;
357 let is_datagram = request.transport.is_datagram();
358 if request.key.trim().is_empty() {
359 return Err(Error::invalid_config("service key is required"));
360 }
361 let worker = self
362 .prepare_worker(&request.key, &request.local_addr)
363 .await?;
364 let credential = self.credential();
365 let keep_alive = self.inner.keep_alive;
366 let namespace = self.inner.namespace;
367 let port_mapping = worker.port_mapping(match options.path {
368 PathPolicy::Auto => options.port_mapping,
369 PathPolicy::RelayOnly => PortMapping::Off,
370 });
371 let handle = match request.transport {
372 Transport::Tcp => worker.spawn(move |context| {
373 run_client_side_cli_recovering_with_path::<TcpListenerProvider>(
374 context.local_addr,
375 context.remote_addr,
376 context.key,
377 keep_alive,
378 namespace,
379 Some(context.status_callback),
380 Some(credential),
381 context.shutdown,
382 context.diagnostics,
383 options.path,
384 is_datagram,
385 port_mapping,
386 )
387 }),
388 Transport::Udp => worker.spawn(move |context| {
389 run_client_side_cli_recovering_with_path::<UdpListenerProvider>(
390 context.local_addr,
391 context.remote_addr,
392 context.key,
393 keep_alive,
394 namespace,
395 Some(context.status_callback),
396 Some(credential),
397 context.shutdown,
398 context.diagnostics,
399 options.path,
400 is_datagram,
401 port_mapping,
402 )
403 }),
404 };
405 Ok(Connection::new(handle, request.key))
406 }
407
408 pub async fn list_keys(&self) -> Result<Vec<String>> {
410 match self.status_request(PbConnStatusReq::Keys).await? {
411 PbConnStatusResp::Keys(keys) => Ok(keys),
412 other => Err(Error::protocol(format!(
413 "expected keys status, got {other:?}"
414 ))),
415 }
416 }
417
418 pub async fn sessions(&self) -> Result<Vec<pb_mapper_protocol::direct::SessionObservation>> {
420 match self.status_request(PbConnStatusReq::Sessions).await? {
421 PbConnStatusResp::Sessions(sessions) => Ok(sessions),
422 other => Err(Error::protocol(format!(
423 "expected sessions status, got {other:?}"
424 ))),
425 }
426 }
427
428 pub async fn service_status(&self, key: impl Into<String>) -> Result<Vec<ServiceConnection>> {
429 let key = key.into();
430 match self
431 .status_request(PbConnStatusReq::Service {
432 direct_query: None,
433 key,
434 })
435 .await?
436 {
437 PbConnStatusResp::Service { connections, .. } => Ok(connections
438 .into_iter()
439 .map(ServiceConnection::from)
440 .collect()),
441 other => Err(Error::protocol(format!(
442 "expected service status, got {other:?}"
443 ))),
444 }
445 }
446
447 pub async fn remote_id(&self) -> Result<RemoteId> {
448 RemoteId::from_status(self.status_request(PbConnStatusReq::RemoteId).await?)
449 }
450
451 async fn prepare_worker(&self, key: &str, local_addr: &str) -> Result<TunnelWorker> {
456 if !self.inner.endpoint.validate() {
457 return Err(Error::invalid_config(
458 "relay must be host:port or an IP socket address",
459 ));
460 }
461 let local_addr = resolve(local_addr).await?;
462 let remote_addr = self.inner.endpoint.clone();
463 let (status_tx, status_rx) = watch::channel(TunnelStatus::Starting);
464 Ok(TunnelWorker {
465 local_addr,
466 remote_addr,
467 key: Arc::from(key),
468 shutdown: CancellationToken::new(),
469 status_tx,
470 status_rx,
471 diagnostics: Diagnostics::default(),
472 })
473 }
474
475 async fn status_request(&self, request: PbConnStatusReq) -> Result<PbConnStatusResp> {
476 let credential = self.credential();
477 let io_timeout = control_io_timeout().min(std::time::Duration::from_secs(5));
480 let endpoint = &self.inner.endpoint;
481 let ask = async |carrier: &mut Option<CarrierId>| {
482 let mut stream = endpoint
483 .connect(credential, carrier)
484 .await
485 .map_err(|error| Error::from_connect(error, self.inner.server.clone()))?;
486 get_status_with_credential(
487 &mut stream,
488 request.clone(),
489 self.inner.namespace,
490 &credential,
491 )
492 .await
493 .context(StatusSnafu)
494 };
495 let mut carrier = None;
498 let result = tokio::time::timeout(io_timeout, async {
499 let _permit = endpoint.control_permit().await;
500 match ask(&mut carrier).await {
501 Err(error)
506 if is_carrier_failure(&error)
507 && endpoint.transport() == super::RelayTransport::Quic =>
508 {
509 endpoint.carrier_failed(carrier);
510 ask(&mut carrier).await
511 }
512 result => result,
513 }
514 })
515 .await
516 .unwrap_or(Err(Error::TimedOut {
517 timeout: io_timeout,
518 }));
519 match &result {
520 Ok(_) => endpoint.protocol_succeeded(),
521 Err(error) if is_carrier_failure(error) => endpoint.carrier_failed(carrier),
523 Err(_) => {}
524 }
525 result
526 }
527}
528
529pub(crate) fn is_carrier_failure(error: &Error) -> bool {
532 match error {
533 Error::Remote { .. } => false,
534 Error::Status { source } => source.remote_retryable().is_none(),
535 _ => true,
536 }
537}
538
539fn check_port_mapping(requested: PortMapping) -> Result<()> {
543 if requested == PortMapping::Always && !cfg!(feature = "port-mapping") {
544 return Err(Error::invalid_config("built without port-mapping support"));
545 }
546 Ok(())
547}
548
549async fn resolve(addr: &str) -> Result<ResolvedAddrs> {
555 resolve_addrs_async(addr)
556 .await
557 .context(AddressSnafu { addr })
558}
559
560fn settle_stopped(tx: &watch::Sender<TunnelStatus>) {
566 tx.send_if_modified(|status| match status {
567 TunnelStatus::Failed(_) => false,
568 _ => {
569 *status = TunnelStatus::Stopped;
570 true
571 }
572 });
573}
574
575fn watch_callback(tx: watch::Sender<TunnelStatus>) -> StatusCallback {
580 Box::new(move |status: &str| {
581 let _ = tx.send(TunnelStatus::from_callback(status));
582 })
583}
584
585struct WorkerContext {
588 local_addr: ResolvedAddrs,
589 remote_addr: RelayEndpoint,
590 key: Arc<str>,
591 status_callback: StatusCallback,
592 shutdown: CancellationToken,
593 diagnostics: Diagnostics,
594}
595
596struct TunnelWorker {
603 local_addr: ResolvedAddrs,
604 remote_addr: RelayEndpoint,
605 key: Arc<str>,
606 shutdown: CancellationToken,
607 status_tx: watch::Sender<TunnelStatus>,
608 status_rx: watch::Receiver<TunnelStatus>,
609 diagnostics: Diagnostics,
610}
611
612impl TunnelWorker {
613 fn port_mapping(&self, mode: PortMapping) -> PortMapping {
616 let mode = if cfg!(feature = "port-mapping") {
617 mode
618 } else {
619 PortMapping::Off
620 };
621 if mode != PortMapping::Off {
622 self.diagnostics
623 .port_mapping_requested(mode, crate::network::NetworkHints::shared().generation());
624 }
625 mode
626 }
627
628 fn spawn<F, Fut>(self, start: F) -> LiveTunnel
629 where
630 F: FnOnce(WorkerContext) -> Fut + Send + 'static,
631 Fut: std::future::Future<Output = ()> + Send,
632 {
633 let Self {
634 local_addr,
635 remote_addr,
636 key,
637 shutdown,
638 status_tx,
639 status_rx,
640 diagnostics,
641 } = self;
642 let worker_shutdown = shutdown.clone();
643 let worker_diagnostics = diagnostics.clone();
644 let endpoint = remote_addr.clone();
645 let join = tokio::spawn(async move {
646 start(WorkerContext {
647 local_addr,
648 remote_addr,
649 key,
650 status_callback: watch_callback(status_tx.clone()),
651 shutdown: worker_shutdown,
652 diagnostics: worker_diagnostics.clone(),
653 })
654 .await;
655 worker_diagnostics.stop();
656 settle_stopped(&status_tx);
657 });
658 LiveTunnel::new(shutdown, join, status_rx, endpoint, diagnostics)
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn config_debug_redacts_credentials() {
668 let config = ClientConfig {
669 server: "localhost:7666".into(),
670 credential: "0123456789abcdefghijklmnopqrstuv".into(),
671 keep_alive: false,
672 namespace: None,
673 };
674 let debug = format!("{config:?}");
675 assert!(!debug.contains(&config.credential));
676 assert!(debug.contains("[redacted]"));
677 }
678
679 #[test]
680 fn admin_requires_administrator_credential() {
681 let admin = Client::from_credential(
682 "127.0.0.1:7666",
683 Credential::Admin(*b"0123456789abcdefghijklmnopqrstuv"),
684 false,
685 None,
686 );
687 assert!(admin.admin().is_ok());
688
689 let temporary = Client::from_credential(
690 "127.0.0.1:7666",
691 Credential::Temporary {
692 key_id: 1,
693 key: [0_u8; 32],
694 },
695 false,
696 None,
697 );
698 assert!(matches!(temporary.admin(), Err(Error::NotAdministrator)));
699 }
700}