Skip to main content

pb_mapper_core/
config.rs

1use std::net::{AddrParseError, SocketAddr};
2use std::sync::{Arc, Once};
3use std::time::Duration;
4
5use clap::ValueEnum;
6use snafu::ResultExt;
7use tracing_subscriber::layer::SubscriberExt;
8use tracing_subscriber::{EnvFilter, Layer, fmt};
9
10use crate::error::{CfgPbServerEnvNotExistSnafu, Result};
11
12#[derive(ValueEnum, Debug, Clone, Copy)]
13pub enum StatusOp {
14    /// Get active remote connection IDs.
15    RemoteId,
16    /// Get registered service keys.
17    Keys,
18    /// Get direct-session observations reported by endpoints in this namespace.
19    Sessions,
20}
21
22/// Every address a name resolved to, in the order the resolver returned them.
23///
24/// Resolution used to end in `.next()`, which threw away everything after the
25/// first candidate. That is enough for an `ip:port` literal, but a hostname with
26/// several A/AAAA records — a relay behind round-robin DNS, or a dual-stack host
27/// whose first record is an unreachable IPv6 address — resolved to one address
28/// and then failed against it, with the working candidates never tried.
29///
30/// The dial loops below this were always able to try every address: both
31/// `each_addr` and the `uni-stream` providers iterate a `ToSocketAddrs` and only
32/// report the last error once every candidate has failed. They just never
33/// received more than one. So this type is the whole of the fix: carry the list
34/// to them, and they do the rest.
35///
36/// Guaranteed non-empty, so a caller never has to handle a resolution that
37/// succeeded with nothing in it. Cheap to clone — the addresses are shared, not
38/// copied — because every tunnel worker and every retry needs its own handle.
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct ResolvedAddrs {
41    /// Non-empty by construction; see [`ResolvedAddrs::new`].
42    addrs: Arc<[SocketAddr]>,
43}
44
45impl ResolvedAddrs {
46    /// Build from resolver output, rejecting an empty result.
47    ///
48    /// `name` is what was resolved, and `parse_error` the failure from parsing it
49    /// as a literal `ip:port`, so an empty resolution reports the same error a
50    /// malformed address would.
51    fn new(name: &str, addrs: Vec<SocketAddr>, parse_error: AddrParseError) -> Result<Self> {
52        if addrs.is_empty() {
53            return Err(crate::error::Error::CfgParseSockAddr {
54                string: name.to_string(),
55                source: parse_error,
56            });
57        }
58        Ok(Self {
59            addrs: Arc::from(addrs),
60        })
61    }
62
63    /// Build from candidates already resolved elsewhere, rejecting an empty list.
64    ///
65    /// For callers that resolve through another address trait — the tunnel
66    /// internals take a generic `ToSocketAddrs` at their public boundary — and
67    /// need the result in this non-empty form.
68    #[must_use]
69    pub fn from_candidates(addrs: Vec<SocketAddr>) -> Option<Self> {
70        if addrs.is_empty() {
71            return None;
72        }
73        Some(Self {
74            addrs: Arc::from(addrs),
75        })
76    }
77
78    /// The candidates, for handing to `each_addr` or a `ToSocketAddrs` bound.
79    ///
80    /// `&[SocketAddr]` is what both address traits in play accept, and it is
81    /// `Copy`, which the `A: ToSocketAddrs + Copy` bounds on the tunnel internals
82    /// require. A `Vec` is neither.
83    #[inline]
84    #[must_use]
85    pub fn as_slice(&self) -> &[SocketAddr] {
86        &self.addrs
87    }
88
89    /// The first candidate.
90    ///
91    /// For the places that genuinely need one address rather than a list: a
92    /// `SocketAddr` field on a status record, a preflight probe, a log line. Not
93    /// for dialling — that is what [`Self::as_slice`] is for.
94    #[inline]
95    #[must_use]
96    pub fn primary(&self) -> SocketAddr {
97        // Non-empty by construction, so indexing cannot panic. `[0]` rather than
98        // `first().unwrap()`: the invariant is the reason, and `unwrap` is denied.
99        self.addrs[0]
100    }
101}
102
103impl std::fmt::Display for ResolvedAddrs {
104    /// Renders the primary address, with a count when candidates were dropped.
105    ///
106    /// Log lines pass these through `%`, and a bare Debug dump of a one-element
107    /// list reads worse than the address itself.
108    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        write!(formatter, "{}", self.primary())?;
110        match self.addrs.len() {
111            1 => Ok(()),
112            more => write!(formatter, " (+{} more)", more - 1),
113        }
114    }
115}
116
117impl From<SocketAddr> for ResolvedAddrs {
118    fn from(addr: SocketAddr) -> Self {
119        Self {
120            addrs: Arc::from(vec![addr]),
121        }
122    }
123}
124
125/// Resolve `addr` to every candidate it names.
126///
127/// A literal `ip:port` short-circuits. Otherwise `localhost:` goes to the system
128/// resolver — the custom DNS path does not answer for it — and any other hostname
129/// tries the configured DNS servers first, falling back to the system resolver.
130#[inline]
131pub fn resolve_addrs(addr: &str) -> Result<ResolvedAddrs> {
132    // The literal case returns here; otherwise the parse error is kept as the
133    // failure reported below, so a caller sees what it asked for rather than a
134    // resolver's internal complaint.
135    let parse_error = match addr.parse::<SocketAddr>() {
136        Ok(socket_addr) => return Ok(ResolvedAddrs::from(socket_addr)),
137        Err(error) => error,
138    };
139
140    let system = || match std::net::ToSocketAddrs::to_socket_addrs(addr) {
141        Ok(addrs) => addrs.collect(),
142        Err(_) => Vec::new(),
143    };
144
145    let addrs = if addr.starts_with("localhost:") {
146        system()
147    } else {
148        match crate::addr::get_socket_addrs(addr) {
149            Ok(addrs) if !addrs.is_empty() => addrs,
150            _ => system(),
151        }
152    };
153    ResolvedAddrs::new(addr, addrs, parse_error)
154}
155
156/// Async counterpart of [`resolve_addrs`], for Tokio contexts.
157pub async fn resolve_addrs_async(addr: &str) -> Result<ResolvedAddrs> {
158    let parse_error = match addr.parse::<SocketAddr>() {
159        Ok(socket_addr) => return Ok(ResolvedAddrs::from(socket_addr)),
160        Err(error) => error,
161    };
162
163    async fn system(addr: &str) -> Vec<SocketAddr> {
164        match tokio::net::lookup_host(addr).await {
165            Ok(addrs) => addrs.collect(),
166            Err(_) => Vec::new(),
167        }
168    }
169
170    let addrs = if addr.starts_with("localhost:") {
171        system(addr).await
172    } else {
173        match crate::addr::get_socket_addrs_async(addr).await {
174            Ok(addrs) if !addrs.is_empty() => addrs,
175            _ => system(addr).await,
176        }
177    };
178    ResolvedAddrs::new(addr, addrs, parse_error)
179}
180
181const PB_MAPPER_SERVER: &str = "PB_MAPPER_SERVER";
182
183/// Env to control whether the keep-alive option of TCP is enabled
184pub const PB_MAPPER_KEEP_ALIVE: &str = "PB_MAPPER_KEEP_ALIVE";
185pub const PB_MAPPER_CONTROL_IO_TIMEOUT: &str = "PB_MAPPER_CONTROL_IO_TIMEOUT";
186pub const PB_MAPPER_STREAM_ACK_TIMEOUT: &str = "PB_MAPPER_STREAM_ACK_TIMEOUT";
187pub const PB_MAPPER_STREAM_READY_TIMEOUT: &str = "PB_MAPPER_STREAM_READY_TIMEOUT";
188pub const PB_MAPPER_STREAM_RECOVERY_TIMEOUT: &str = "PB_MAPPER_STREAM_RECOVERY_TIMEOUT";
189pub const PB_MAPPER_CONTROL_CONN_POOL_SIZE: &str = "PB_MAPPER_CONTROL_CONN_POOL_SIZE";
190pub const PB_MAPPER_CONTROL_HEARTBEAT_INTERVAL: &str = "PB_MAPPER_CONTROL_HEARTBEAT_INTERVAL";
191pub const PB_MAPPER_CONTROL_HEARTBEAT_TOLERANCE: &str = "PB_MAPPER_CONTROL_HEARTBEAT_TOLERANCE";
192pub const PB_MAPPER_CONTROL_SUSPECT_GRACE: &str = "PB_MAPPER_CONTROL_SUSPECT_GRACE";
193pub const PB_MAPPER_REGISTRATION_PROBE_TIMEOUT: &str = "PB_MAPPER_REGISTRATION_PROBE_TIMEOUT";
194pub const PB_MAPPER_REGISTRATION_REJECT_BACKOFF_MIN: &str =
195    "PB_MAPPER_REGISTRATION_REJECT_BACKOFF_MIN";
196pub const PB_MAPPER_REGISTRATION_REJECT_BACKOFF_MAX: &str =
197    "PB_MAPPER_REGISTRATION_REJECT_BACKOFF_MAX";
198pub const PB_MAPPER_SERVER_LEASE_TIMEOUT: &str = "PB_MAPPER_SERVER_LEASE_TIMEOUT";
199pub const PB_MAPPER_SERVER_LEASE_SWEEP_INTERVAL: &str = "PB_MAPPER_SERVER_LEASE_SWEEP_INTERVAL";
200pub const PB_MAPPER_CLIENT_HEALTH_CHECK_INTERVAL: &str = "PB_MAPPER_CLIENT_HEALTH_CHECK_INTERVAL";
201pub const PB_MAPPER_CLIENT_HEALTH_CHECK_TIMEOUT: &str = "PB_MAPPER_CLIENT_HEALTH_CHECK_TIMEOUT";
202pub const PB_MAPPER_CLIENT_HEALTH_FAILURE_THRESHOLD: &str =
203    "PB_MAPPER_CLIENT_HEALTH_FAILURE_THRESHOLD";
204pub const PB_MAPPER_LOG_FORMAT: &str = "PB_MAPPER_LOG_FORMAT";
205const DEFAULT_CONTROL_IO_TIMEOUT: Duration = Duration::from_secs(30);
206const DEFAULT_STREAM_ACK_TIMEOUT: Duration = Duration::from_millis(300);
207const DEFAULT_STREAM_READY_TIMEOUT: Duration = Duration::from_secs(1);
208const DEFAULT_STREAM_RECOVERY_TIMEOUT: Duration = Duration::from_secs(2);
209const DEFAULT_CONTROL_CONN_POOL_SIZE: usize = 2;
210const DEFAULT_CONTROL_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(2);
211const DEFAULT_CONTROL_HEARTBEAT_TOLERANCE: Duration = Duration::from_secs(6);
212const DEFAULT_CONTROL_SUSPECT_GRACE: Duration = Duration::from_secs(2);
213const DEFAULT_REGISTRATION_PROBE_TIMEOUT: Duration = Duration::from_secs(1);
214const DEFAULT_REGISTRATION_REJECT_BACKOFF_MIN: Duration = Duration::from_secs(5);
215const DEFAULT_REGISTRATION_REJECT_BACKOFF_MAX: Duration = Duration::from_secs(80);
216const DEFAULT_SERVER_LEASE_TIMEOUT: Duration = Duration::from_secs(15);
217const DEFAULT_SERVER_LEASE_SWEEP_INTERVAL: Duration = Duration::from_secs(5);
218const DEFAULT_CLIENT_HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(15);
219const DEFAULT_CLIENT_HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
220const DEFAULT_CLIENT_HEALTH_FAILURE_THRESHOLD: usize = 3;
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum LogFormat {
224    Pretty,
225    Compact,
226    Json,
227}
228
229pub fn parse_log_format(value: &str) -> LogFormat {
230    match value.trim().to_ascii_lowercase().as_str() {
231        "compact" => LogFormat::Compact,
232        "json" => LogFormat::Json,
233        _ => LogFormat::Pretty,
234    }
235}
236
237fn log_format_from_env() -> LogFormat {
238    std::env::var(PB_MAPPER_LOG_FORMAT)
239        .ok()
240        .map(|value| parse_log_format(&value))
241        .unwrap_or(LogFormat::Pretty)
242}
243
244fn default_env_filter() -> EnvFilter {
245    EnvFilter::builder()
246        .with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
247        .from_env_lossy()
248}
249
250pub fn parse_duration(value: &str) -> Option<Duration> {
251    let value = value.trim();
252    if value.is_empty() {
253        return None;
254    }
255    if let Some(raw) = value.strip_suffix("ms") {
256        return raw.trim().parse::<u64>().ok().map(Duration::from_millis);
257    }
258    if let Some(raw) = value.strip_suffix('s') {
259        return raw.trim().parse::<u64>().ok().map(Duration::from_secs);
260    }
261    if let Some(raw) = value.strip_suffix('m') {
262        return raw
263            .trim()
264            .parse::<u64>()
265            .ok()
266            .and_then(|minutes| minutes.checked_mul(60))
267            .map(Duration::from_secs);
268    }
269    if let Some(raw) = value.strip_suffix('h') {
270        return raw
271            .trim()
272            .parse::<u64>()
273            .ok()
274            .and_then(|hours| hours.checked_mul(60 * 60))
275            .map(Duration::from_secs);
276    }
277    value.parse::<u64>().ok().map(Duration::from_secs)
278}
279
280pub fn duration_from_env(name: &str, default: Duration) -> Duration {
281    std::env::var(name)
282        .ok()
283        .and_then(|value| parse_duration(&value))
284        .unwrap_or(default)
285}
286
287/// Read a duration that has to be positive, falling back to `default` on a zero.
288///
289/// Zero is never a usable value for these settings: a period of zero panics
290/// `tokio::time::interval`, and a zero minimum trips [`RetryBackoff::new`]'s
291/// assertion. Clamping a zero up to a millisecond avoids the panic, but trades it
292/// for a hot loop — a rejected registration retried a thousand times a second, or
293/// a full registration scan queued into the routing loop every millisecond. The
294/// default is the only value that is both safe and useful, so a zero selects it
295/// and says so once.
296///
297/// [`RetryBackoff::new`]: crate::timeout::RetryBackoff::new
298fn positive_duration_from_env(name: &str, default: Duration) -> Duration {
299    let value = duration_from_env(name, default);
300    if value.is_zero() {
301        tracing::warn!(
302            event = "config_zero_duration_ignored",
303            variable = name,
304            default = ?default,
305            "ignoring a zero duration and using the default instead"
306        );
307        return default;
308    }
309    value
310}
311
312pub fn control_io_timeout() -> Duration {
313    duration_from_env(PB_MAPPER_CONTROL_IO_TIMEOUT, DEFAULT_CONTROL_IO_TIMEOUT)
314}
315
316pub fn stream_ack_timeout() -> Duration {
317    duration_from_env(PB_MAPPER_STREAM_ACK_TIMEOUT, DEFAULT_STREAM_ACK_TIMEOUT)
318}
319
320pub fn stream_ready_timeout() -> Duration {
321    duration_from_env(PB_MAPPER_STREAM_READY_TIMEOUT, DEFAULT_STREAM_READY_TIMEOUT)
322}
323
324pub fn stream_recovery_timeout() -> Duration {
325    duration_from_env(
326        PB_MAPPER_STREAM_RECOVERY_TIMEOUT,
327        DEFAULT_STREAM_RECOVERY_TIMEOUT,
328    )
329}
330
331pub fn control_conn_pool_size() -> usize {
332    std::env::var(PB_MAPPER_CONTROL_CONN_POOL_SIZE)
333        .ok()
334        .and_then(|value| value.trim().parse::<usize>().ok())
335        .filter(|size| *size > 0)
336        .map(|size| size.min(16))
337        .unwrap_or(DEFAULT_CONTROL_CONN_POOL_SIZE)
338}
339
340pub fn control_heartbeat_interval() -> Duration {
341    duration_from_env(
342        PB_MAPPER_CONTROL_HEARTBEAT_INTERVAL,
343        DEFAULT_CONTROL_HEARTBEAT_INTERVAL,
344    )
345}
346
347pub fn control_heartbeat_tolerance() -> Duration {
348    duration_from_env(
349        PB_MAPPER_CONTROL_HEARTBEAT_TOLERANCE,
350        DEFAULT_CONTROL_HEARTBEAT_TOLERANCE,
351    )
352}
353
354pub fn control_suspect_grace() -> Duration {
355    duration_from_env(
356        PB_MAPPER_CONTROL_SUSPECT_GRACE,
357        DEFAULT_CONTROL_SUSPECT_GRACE,
358    )
359}
360
361pub fn registration_probe_timeout() -> Duration {
362    duration_from_env(
363        PB_MAPPER_REGISTRATION_PROBE_TIMEOUT,
364        DEFAULT_REGISTRATION_PROBE_TIMEOUT,
365    )
366}
367
368/// The retry ladder for a registration the relay rejected but marked retryable.
369///
370/// Separate from the transport ladder, and far slower. A transport failure means
371/// the relay could not be reached, so retrying quickly is how the tunnel comes
372/// back. A retryable rejection is the opposite: the relay answered, and refused —
373/// a per-service connection quota that is full, a namespace at its service limit.
374/// Reconnecting a few times a second adds load to the very condition that has to
375/// clear, and it produced thousands of identical reject lines a minute in
376/// production. So the ladder starts at 5s and settles at 80s.
377///
378/// Both ends come back together because they are only valid as a pair: a zero
379/// falls back to its default, and the maximum is then raised to the minimum,
380/// rather than letting an environment typo reach [`RetryBackoff::new`]'s
381/// assertions, which would abort the process at start-up.
382///
383/// Raising the maximum, but defaulting a zero minimum, because the two failures
384/// are different. An inverted range is a legible request — wait exactly this long
385/// — and collapsing it to a fixed delay honours it. A zero minimum is not: it
386/// asks for no wait at all, and since [`RetryBackoff`] caps its multiplier at
387/// 1024, a millisecond minimum would never climb past about a second no matter
388/// how high the maximum, hammering the very quota the ladder exists to let clear.
389///
390/// [`RetryBackoff`]: crate::timeout::RetryBackoff
391/// [`RetryBackoff::new`]: crate::timeout::RetryBackoff::new
392pub fn registration_reject_backoff() -> (Duration, Duration) {
393    let min = positive_duration_from_env(
394        PB_MAPPER_REGISTRATION_REJECT_BACKOFF_MIN,
395        DEFAULT_REGISTRATION_REJECT_BACKOFF_MIN,
396    );
397    let max = positive_duration_from_env(
398        PB_MAPPER_REGISTRATION_REJECT_BACKOFF_MAX,
399        DEFAULT_REGISTRATION_REJECT_BACKOFF_MAX,
400    )
401    .max(min);
402    (min, max)
403}
404
405pub fn server_lease_timeout() -> Duration {
406    duration_from_env(PB_MAPPER_SERVER_LEASE_TIMEOUT, DEFAULT_SERVER_LEASE_TIMEOUT)
407}
408
409/// How often the relay looks for registrations that stopped renewing their lease.
410///
411/// A third of [`server_lease_timeout`] by default, which bounds two things at
412/// once: how long an expired registration can keep attracting subscribers, and —
413/// because the sweep needs two passes to retire one — how much grace a live
414/// connection gets to notice its own idle timeout first.
415///
416/// A zero falls back to the default: `tokio::time::interval` panics on a zero
417/// period, and each tick queues a scan over every registration through the single
418/// routing loop, so sweeping as fast as the timer allows would starve the traffic
419/// the sweep exists to protect.
420pub fn server_lease_sweep_interval() -> Duration {
421    positive_duration_from_env(
422        PB_MAPPER_SERVER_LEASE_SWEEP_INTERVAL,
423        DEFAULT_SERVER_LEASE_SWEEP_INTERVAL,
424    )
425}
426
427pub fn client_health_check_interval() -> Duration {
428    duration_from_env(
429        PB_MAPPER_CLIENT_HEALTH_CHECK_INTERVAL,
430        DEFAULT_CLIENT_HEALTH_CHECK_INTERVAL,
431    )
432}
433
434pub fn client_health_check_timeout() -> Duration {
435    duration_from_env(
436        PB_MAPPER_CLIENT_HEALTH_CHECK_TIMEOUT,
437        DEFAULT_CLIENT_HEALTH_CHECK_TIMEOUT,
438    )
439}
440
441pub fn client_health_failure_threshold() -> usize {
442    std::env::var(PB_MAPPER_CLIENT_HEALTH_FAILURE_THRESHOLD)
443        .ok()
444        .and_then(|value| value.trim().parse::<usize>().ok())
445        .filter(|threshold| *threshold > 0)
446        .map(|threshold| threshold.min(100))
447        .unwrap_or(DEFAULT_CLIENT_HEALTH_FAILURE_THRESHOLD)
448}
449
450/// Whether the environment asks for TCP keep-alive, from `PB_MAPPER_KEEP_ALIVE`.
451///
452/// A default for a process to read once at startup — not something the tunnels
453/// consult. Keep-alive is a per-tunnel parameter because one process can run
454/// many tunnels that disagree about it, which is why this is a function and not
455/// the `LazyLock<bool>` it used to be: that froze on the first tunnel to touch
456/// a socket and left every later one, including the UI's own toggle, unable to
457/// change it.
458pub fn keep_alive_from_env() -> bool {
459    match std::env::var(PB_MAPPER_KEEP_ALIVE) {
460        // The documented spelling is `ON`; the previous check was `is_ok()`,
461        // which turned keep-alive on for any value at all — `OFF` included.
462        Ok(value) => matches!(
463            value.trim().to_ascii_lowercase().as_str(),
464            "on" | "1" | "true" | "yes"
465        ),
466        Err(_) => false,
467    }
468}
469
470/// The relay address as configured: `addr` if given, else `PB_MAPPER_SERVER`.
471///
472/// Unresolved on purpose. A caller that hands the address to something which
473/// resolves it itself — the SDK client, for one — should pass the name along
474/// rather than resolve it here and stringify the result, which both duplicates
475/// the lookup and narrows a multi-address name down to one entry on the way.
476pub fn pb_mapper_server_addr(addr: Option<&str>) -> Result<String> {
477    match addr {
478        Some(addr) => Ok(addr.to_string()),
479        None => std::env::var(PB_MAPPER_SERVER).context(CfgPbServerEnvNotExistSnafu),
480    }
481}
482
483/// Every address the relay resolves to: `addr` if given, else `PB_MAPPER_SERVER`.
484#[inline]
485pub fn resolve_pb_mapper_server(addr: Option<&str>) -> Result<ResolvedAddrs> {
486    match addr {
487        Some(addr) => resolve_addrs(addr),
488        None => {
489            let addr = std::env::var(PB_MAPPER_SERVER).context(CfgPbServerEnvNotExistSnafu)?;
490            resolve_addrs(&addr)
491        }
492    }
493}
494
495/// Async counterpart of [`resolve_pb_mapper_server`], for Tokio contexts.
496pub async fn resolve_pb_mapper_server_async(addr: Option<&str>) -> Result<ResolvedAddrs> {
497    match addr {
498        Some(addr) => resolve_addrs_async(addr).await,
499        None => {
500            let addr = std::env::var(PB_MAPPER_SERVER).context(CfgPbServerEnvNotExistSnafu)?;
501            resolve_addrs_async(&addr).await
502        }
503    }
504}
505
506pub fn init_tracing() {
507    static INIT_TRACING: Once = Once::new();
508    INIT_TRACING.call_once(|| {
509        let result = match log_format_from_env() {
510            LogFormat::Pretty => {
511                let subscriber = tracing_subscriber::registry().with(
512                    fmt::layer()
513                        .pretty()
514                        .with_writer(std::io::stdout)
515                        .with_filter(default_env_filter()),
516                );
517                tracing::subscriber::set_global_default(subscriber)
518            }
519            LogFormat::Compact => {
520                let subscriber = tracing_subscriber::registry().with(
521                    fmt::layer()
522                        .compact()
523                        .with_writer(std::io::stdout)
524                        .with_filter(default_env_filter()),
525                );
526                tracing::subscriber::set_global_default(subscriber)
527            }
528            LogFormat::Json => {
529                let subscriber = tracing_subscriber::registry().with(
530                    fmt::layer()
531                        .json()
532                        .flatten_event(true)
533                        .with_writer(std::io::stdout)
534                        .with_filter(default_env_filter()),
535                );
536                tracing::subscriber::set_global_default(subscriber)
537            }
538        };
539
540        if let Err(e) = result {
541            eprintln!("failed to initialize tracing subscriber: {e}");
542        }
543    });
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn parse_log_format_accepts_supported_values() {
552        assert_eq!(parse_log_format("pretty"), LogFormat::Pretty);
553        assert_eq!(parse_log_format("compact"), LogFormat::Compact);
554        assert_eq!(parse_log_format("json"), LogFormat::Json);
555        assert_eq!(parse_log_format(" JSON "), LogFormat::Json);
556        assert_eq!(parse_log_format("unknown"), LogFormat::Pretty);
557    }
558
559    /// The point of [`ResolvedAddrs`]: a name with several records keeps them
560    /// all, so the dial loops can try each one. Collapsing to the first is what
561    /// made a multi-record relay unreachable whenever its first address was.
562    #[test]
563    fn resolved_addrs_keeps_every_candidate() {
564        let first: SocketAddr = "127.0.0.1:7666".parse().expect("literal");
565        let second: SocketAddr = "[::1]:7666".parse().expect("literal");
566        let addrs = ResolvedAddrs::from_candidates(vec![first, second]).expect("non-empty");
567
568        assert_eq!(addrs.as_slice(), [first, second]);
569        assert_eq!(addrs.primary(), first, "order is the resolver's order");
570    }
571
572    /// An empty candidate list is not a resolution: nothing could be dialled, so
573    /// it has to be rejected here rather than surface as a connect failure
574    /// against an address the caller never gave.
575    #[test]
576    fn resolved_addrs_rejects_an_empty_candidate_list() {
577        assert!(ResolvedAddrs::from_candidates(Vec::new()).is_none());
578    }
579
580    /// The rendering has to name one address — it goes into connect errors and
581    /// log fields — while still admitting that others were available.
582    #[test]
583    fn resolved_addrs_renders_the_primary_and_the_rest_as_a_count() {
584        let single: SocketAddr = "127.0.0.1:7666".parse().expect("literal");
585        assert_eq!(ResolvedAddrs::from(single).to_string(), "127.0.0.1:7666");
586
587        let second: SocketAddr = "[::1]:7666".parse().expect("literal");
588        let both = ResolvedAddrs::from_candidates(vec![single, second]).expect("non-empty");
589        assert_eq!(both.to_string(), "127.0.0.1:7666 (+1 more)");
590    }
591
592    /// A literal address needs no resolver, and must survive verbatim.
593    #[test]
594    fn resolve_addrs_passes_a_literal_through() {
595        let addrs = resolve_addrs("127.0.0.1:7666").expect("a literal always resolves");
596        assert_eq!(
597            addrs.as_slice(),
598            ["127.0.0.1:7666".parse().expect("literal")]
599        );
600    }
601
602    /// `localhost` is the everyday multi-record name: it resolves to a v4 and a
603    /// v6 loopback on most hosts, and both must reach the dial loop.
604    #[test]
605    fn resolve_addrs_keeps_every_localhost_record() {
606        let addrs = resolve_addrs("localhost:7666").expect("localhost always resolves");
607        assert!(
608            addrs.as_slice().iter().all(|addr| addr.ip().is_loopback()),
609            "localhost must resolve to loopback only, got {:?}",
610            addrs.as_slice()
611        );
612    }
613
614    /// A name that resolves to nothing is an error, not an empty success.
615    ///
616    /// Tested with a missing port rather than an unresolvable host: a resolver
617    /// that answers every query with a wildcard address — WSL's NAT DNS, among
618    /// others — makes "this host does not exist" untestable, while no resolver
619    /// invents a port.
620    #[test]
621    fn resolve_addrs_fails_when_nothing_can_be_resolved() {
622        assert!(resolve_addrs("127.0.0.1").is_err(), "no port");
623        assert!(resolve_addrs("localhost").is_err(), "no port");
624    }
625
626    /// The async path is the one the tunnels use, and has to agree with the
627    /// blocking one on a literal.
628    #[tokio::test]
629    async fn resolve_addrs_async_matches_the_blocking_path_on_a_literal() {
630        let expected = resolve_addrs("127.0.0.1:7666").expect("literal");
631        let actual = resolve_addrs_async("127.0.0.1:7666")
632            .await
633            .expect("literal");
634        assert_eq!(actual, expected);
635    }
636}