Skip to main content

hyperactor/
channel.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! One-way, multi-process, typed communication channels. These are used
10//! to send messages between mailboxes residing in different processes.
11
12use core::net::SocketAddr;
13use std::fmt;
14use std::future::Future;
15use std::net::IpAddr;
16use std::net::Ipv6Addr;
17#[cfg(target_os = "linux")]
18use std::os::linux::net::SocketAddrExt;
19use std::os::unix::io::FromRawFd;
20use std::os::unix::io::RawFd;
21use std::panic::Location;
22use std::pin::Pin;
23use std::str::FromStr;
24use std::sync::Arc;
25use std::sync::Mutex;
26use std::sync::atomic::AtomicU8;
27use std::sync::atomic::AtomicUsize;
28use std::sync::atomic::Ordering;
29use std::task::Context;
30use std::task::Poll;
31
32use async_trait::async_trait;
33use enum_as_inner::EnumAsInner;
34use futures::task::AtomicWaker;
35use hyperactor_config::attrs::AttrValue;
36use serde::Deserialize;
37use serde::Serialize;
38use tokio::sync::mpsc;
39use tokio::sync::watch;
40use tokio::time::Instant;
41use tokio_util::sync::CancellationToken;
42
43use crate as hyperactor;
44use crate::RemoteMessage;
45pub(crate) mod local;
46pub(crate) mod net;
47
48// Public TLS API for HTTP services (mesh admin, TUI, etc.). The
49// implementation lives in `net` but we re-export here to keep `net`'s
50// internal types out of the public API surface.
51pub use net::ServerError;
52pub use net::try_tls_acceptor_with_pem_bundle;
53pub use net::try_tls_connector;
54pub use net::try_tls_pem_bundle;
55
56/// Duplex channel API: a single connection carries messages in both directions.
57pub mod duplex {
58    pub use super::net::duplex::DuplexClient;
59    pub use super::net::duplex::DuplexRx;
60    pub use super::net::duplex::DuplexServer;
61    pub use super::net::duplex::DuplexTx;
62    pub use super::net::duplex::dial;
63    pub use super::net::duplex::serve;
64}
65
66/// The type of error that can occur on channel operations.
67#[derive(thiserror::Error, Debug)]
68pub enum ChannelError {
69    /// An operation was attempted on a closed channel.
70    #[error("channel closed")]
71    Closed,
72
73    /// An error occurred during send.
74    #[error("send: {0}")]
75    Send(#[source] anyhow::Error),
76
77    /// A network client error.
78    #[error(transparent)]
79    Client(#[from] net::ClientError),
80
81    /// The address was not valid.
82    #[error("invalid address {0:?}")]
83    InvalidAddress(String),
84
85    /// A serving error was encountered.
86    #[error(transparent)]
87    Server(#[from] net::ServerError),
88
89    /// A bincode encoding error occurred.
90    #[error(transparent)]
91    BincodeEncode(#[from] bincode::error::EncodeError),
92
93    /// A bincode decoding error occurred.
94    #[error(transparent)]
95    BincodeDecode(#[from] bincode::error::DecodeError),
96
97    /// Data encoding errors.
98    #[error(transparent)]
99    Data(#[from] wirevalue::Error),
100
101    /// Some other error.
102    #[error(transparent)]
103    Other(#[from] anyhow::Error),
104
105    /// An operation timeout occurred.
106    #[error("operation timed out after {0:?}")]
107    Timeout(std::time::Duration),
108}
109
110/// Structured context for a send error.
111#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
112pub enum SendErrorReason {
113    /// The serialized frame exceeded the configured channel frame limit.
114    #[error(
115        "rejecting oversize frame: len={len} > max={max}. \
116        ack will not arrive before timeout; increase CODEC_MAX_FRAME_LENGTH to allow."
117    )]
118    OversizedFrame {
119        /// The serialized frame length.
120        len: usize,
121
122        /// The configured frame limit.
123        max: usize,
124    },
125
126    /// Other human-readable context.
127    #[error("{0}")]
128    Other(String),
129}
130
131/// An error that occurred during send. Returns the message that failed to send.
132#[derive(thiserror::Error, Debug)]
133#[error("{error} for reason {reason:?}")]
134pub struct SendError<M: RemoteMessage> {
135    /// Inner channel error
136    #[source]
137    pub error: ChannelError,
138    /// Message that couldn't be sent
139    pub message: M,
140    /// Reason that message couldn't be sent, if any.
141    pub reason: Option<SendErrorReason>,
142}
143
144#[repr(u8)]
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146enum CompletionStatus {
147    Pending = 0,
148    Accepted = 1,
149    Rejected = 2,
150}
151
152impl CompletionStatus {
153    fn from_u8(value: u8) -> Self {
154        match value {
155            value if value == Self::Pending as u8 => Self::Pending,
156            value if value == Self::Accepted as u8 => Self::Accepted,
157            value if value == Self::Rejected as u8 => Self::Rejected,
158            _ => panic!("invalid completion state"),
159        }
160    }
161}
162
163struct CompletionState<M: RemoteMessage> {
164    state: AtomicU8,
165    waker: AtomicWaker,
166    rejected: Mutex<Option<Box<SendError<M>>>>,
167}
168
169pub(crate) struct CompletionSender<M: RemoteMessage> {
170    inner: Arc<CompletionState<M>>,
171}
172
173/// Future that resolves when a posted message is accepted or rejected.
174pub struct CompletionReceipt<M: RemoteMessage> {
175    inner: Arc<CompletionState<M>>,
176}
177
178impl<M: RemoteMessage> CompletionSender<M> {
179    fn pair() -> (Self, CompletionReceipt<M>) {
180        let inner = Arc::new(CompletionState {
181            state: AtomicU8::new(CompletionStatus::Pending as u8),
182            waker: AtomicWaker::new(),
183            rejected: Mutex::new(None),
184        });
185
186        (
187            Self {
188                inner: Arc::clone(&inner),
189            },
190            CompletionReceipt { inner },
191        )
192    }
193
194    fn accept(self) {
195        self.inner
196            .state
197            .store(CompletionStatus::Accepted as u8, Ordering::Release);
198        self.inner.waker.wake();
199    }
200
201    fn reject(self, error: SendError<M>) {
202        *self.inner.rejected.lock().unwrap() = Some(Box::new(error));
203        self.inner
204            .state
205            .store(CompletionStatus::Rejected as u8, Ordering::Release);
206        self.inner.waker.wake();
207    }
208}
209
210impl<M: RemoteMessage> Drop for CompletionSender<M> {
211    fn drop(&mut self) {
212        if CompletionStatus::from_u8(self.inner.state.load(Ordering::Acquire))
213            == CompletionStatus::Pending
214        {
215            self.inner
216                .state
217                .store(CompletionStatus::Accepted as u8, Ordering::Release);
218            self.inner.waker.wake();
219        }
220    }
221}
222
223impl<M: RemoteMessage> Future for CompletionReceipt<M> {
224    type Output = Result<(), SendError<M>>;
225
226    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
227        match self.poll_ready() {
228            Poll::Ready(result) => Poll::Ready(result),
229            Poll::Pending => {
230                self.inner.waker.register(cx.waker());
231                self.poll_ready()
232            }
233        }
234    }
235}
236
237impl<M: RemoteMessage> CompletionReceipt<M> {
238    fn poll_ready(&self) -> Poll<Result<(), SendError<M>>> {
239        match CompletionStatus::from_u8(self.inner.state.load(Ordering::Acquire)) {
240            CompletionStatus::Accepted => Poll::Ready(Ok(())),
241            CompletionStatus::Rejected => {
242                let error = self
243                    .inner
244                    .rejected
245                    .lock()
246                    .unwrap()
247                    .take()
248                    .expect("rejected completion should store send error");
249                Poll::Ready(Err(*error))
250            }
251            CompletionStatus::Pending => Poll::Pending,
252        }
253    }
254}
255
256/// Shared completion counter for sinks that need to wake flush waiters.
257pub(crate) struct CompletionTracker {
258    completed: Arc<AtomicUsize>,
259    completed_notify: Arc<tokio::sync::Notify>,
260}
261
262impl CompletionTracker {
263    pub(crate) fn new(
264        completed: Arc<AtomicUsize>,
265        completed_notify: Arc<tokio::sync::Notify>,
266    ) -> Self {
267        Self {
268            completed,
269            completed_notify,
270        }
271    }
272
273    fn complete(&self) {
274        self.completed.fetch_add(1, Ordering::Relaxed);
275        self.completed_notify.notify_waiters();
276    }
277}
278
279enum CompletionSinkInner<M: RemoteMessage> {
280    Ignore,
281    Receipt(CompletionSender<M>),
282    OnReject(Box<dyn FnOnce(SendError<M>) + Send + Sync>),
283    Tracked {
284        tracker: CompletionTracker,
285        on_reject: Box<dyn FnOnce(SendError<M>) + Send + Sync>,
286    },
287}
288
289/// Sink for the terminal outcome of a posted message.
290pub struct CompletionSink<M: RemoteMessage>(CompletionSinkInner<M>);
291
292impl<M: RemoteMessage> CompletionSink<M> {
293    /// Ignore the message completion.
294    pub fn ignore() -> Self {
295        Self(CompletionSinkInner::Ignore)
296    }
297
298    /// Invoke `f` only when the channel rejects the message.
299    pub fn on_reject(f: impl FnOnce(SendError<M>) + Send + Sync + 'static) -> Self {
300        Self(CompletionSinkInner::OnReject(Box::new(f)))
301    }
302
303    /// Return a completion sink and a receipt that observes its terminal outcome.
304    pub fn receipt() -> (Self, CompletionReceipt<M>) {
305        let (sender, receipt) = CompletionSender::pair();
306        (Self(CompletionSinkInner::Receipt(sender)), receipt)
307    }
308
309    /// Track every completion and invoke `on_reject` only for rejected messages.
310    pub(crate) fn tracked(
311        tracker: CompletionTracker,
312        on_reject: impl FnOnce(SendError<M>) + Send + Sync + 'static,
313    ) -> Self {
314        Self(CompletionSinkInner::Tracked {
315            tracker,
316            on_reject: Box::new(on_reject),
317        })
318    }
319
320    /// Adapt rejected send errors for a wrapped message type.
321    pub fn contramap_rejected<N: RemoteMessage>(
322        self,
323        f: impl FnOnce(SendError<N>) -> Option<SendError<M>> + Send + Sync + 'static,
324    ) -> CompletionSink<N> {
325        match self.0 {
326            CompletionSinkInner::Ignore => CompletionSink::ignore(),
327            CompletionSinkInner::Receipt(sender) => CompletionSink::on_reject(move |error| {
328                if let Some(error) = f(error) {
329                    sender.reject(error);
330                } else {
331                    sender.accept();
332                }
333            }),
334            CompletionSinkInner::OnReject(on_reject) => CompletionSink::on_reject(move |error| {
335                if let Some(error) = f(error) {
336                    on_reject(error);
337                }
338            }),
339            CompletionSinkInner::Tracked { tracker, on_reject } => {
340                CompletionSink::tracked(tracker, move |error| {
341                    if let Some(error) = f(error) {
342                        on_reject(error);
343                    }
344                })
345            }
346        }
347    }
348
349    /// Report that the channel accepted the message.
350    pub fn accept(self) {
351        match self.0 {
352            CompletionSinkInner::Ignore => {}
353            CompletionSinkInner::Receipt(sender) => sender.accept(),
354            CompletionSinkInner::OnReject(_) => {}
355            CompletionSinkInner::Tracked { tracker, .. } => tracker.complete(),
356        }
357    }
358
359    /// Report that the channel rejected the message.
360    pub fn reject(self, error: SendError<M>) {
361        match self.0 {
362            CompletionSinkInner::Ignore => {}
363            CompletionSinkInner::Receipt(sender) => sender.reject(error),
364            CompletionSinkInner::OnReject(on_reject) => on_reject(error),
365            CompletionSinkInner::Tracked { tracker, on_reject } => {
366                on_reject(error);
367                tracker.complete();
368            }
369        }
370    }
371}
372
373impl<M: RemoteMessage> From<SendError<M>> for ChannelError {
374    fn from(error: SendError<M>) -> Self {
375        error.error
376    }
377}
378
379/// Reason a [`TxStatus`] transitioned to `Closed`. Callers should branch on
380/// the typed variants for cases they care about (e.g. cache-eviction logic in
381/// `DialMailboxRouter` keys on `SequenceMismatch`); everything else falls
382/// into `Other` and is for display/logging only.
383#[derive(Debug, Clone, PartialEq)]
384pub enum CloseReason {
385    /// The peer rejected our session because our sequence number did not
386    /// match what the peer's dispatcher expected — the K8s "out-of-sequence
387    /// message, expected seq 0, got N" case where the peer GC'd the
388    /// `SessionId` while we still hold an `Outbox.next_seq` past 0.
389    /// Re-dialing produces a fresh session that the peer accepts.
390    SequenceMismatch(String),
391    /// The peer rejected a frame whose length exceeded
392    /// `config::CODEC_MAX_FRAME_LENGTH`. Re-dialing will not help — the
393    /// message itself is the problem.
394    OversizedFrame {
395        /// Actual frame length in bytes.
396        size: usize,
397        /// `CODEC_MAX_FRAME_LENGTH` at the time of rejection.
398        max: usize,
399    },
400    /// Any close reason the transport hasn't classified further. The string
401    /// is for display/logging only — do not parse it. If a caller needs to
402    /// branch on a sub-case, lift it into its own variant on this enum.
403    Other(String),
404}
405
406impl fmt::Display for CloseReason {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        match self {
409            Self::SequenceMismatch(s) => write!(f, "stale session: {}", s),
410            Self::OversizedFrame { size, max } => {
411                write!(f, "oversized frame: len={size} > max={max}")
412            }
413            Self::Other(s) => f.write_str(s),
414        }
415    }
416}
417
418/// The possible states of a `Tx`.
419#[derive(Debug, Clone, PartialEq, EnumAsInner)]
420pub enum TxStatus {
421    /// The tx is good.
422    Active,
423    /// The tx cannot be used for message delivery.
424    Closed(CloseReason),
425}
426
427/// The transmit end of an M-typed channel.
428#[async_trait]
429pub trait Tx<M: RemoteMessage> {
430    /// Post a message and report its terminal outcome to `completion`.
431    ///
432    /// Users should use the `try_post`, and `post` variants directly.
433    fn do_post(&self, message: M, completion: CompletionSink<M>);
434
435    /// Enqueue a `message` on the local end of the channel and return a receipt
436    /// that resolves when the message is accepted or rejected.
437    fn try_post(&self, message: M) -> CompletionReceipt<M> {
438        let (completion, receipt) = CompletionSink::receipt();
439        self.do_post(message, completion);
440        receipt
441    }
442
443    /// Enqueue a message to be sent on the channel.
444    #[hyperactor::instrument_infallible]
445    fn post(&self, message: M) {
446        self.do_post(message, CompletionSink::ignore());
447    }
448
449    /// Send a message synchronously, returning when the message has
450    /// been delivered to the remote end of the channel.
451    async fn send(&self, message: M) -> Result<(), SendError<M>> {
452        self.try_post(message).await
453    }
454
455    /// The channel address to which this Tx is sending.
456    fn addr(&self) -> ChannelAddr;
457
458    /// A means to monitor the health of a `Tx`.
459    fn status(&self) -> &watch::Receiver<TxStatus>;
460}
461
462/// The receive end of an M-typed channel.
463#[async_trait]
464pub trait Rx<M: RemoteMessage> {
465    /// Receive the next message from the channel. If the channel returns
466    /// an error it is considered broken and should be discarded.
467    async fn recv(&mut self) -> Result<M, ChannelError>;
468
469    /// The channel address from which this Rx is receiving.
470    fn addr(&self) -> ChannelAddr;
471
472    /// Gracefully shut down the channel receiver, flushing any pending
473    /// acks before returning. Implementations must ensure all pending
474    /// acks are sent before this method returns.
475    async fn join(self)
476    where
477        Self: Sized;
478}
479
480/// The hostname to use for TLS connections.
481#[derive(
482    Clone,
483    Debug,
484    PartialEq,
485    Eq,
486    Hash,
487    Serialize,
488    Deserialize,
489    strum::EnumIter,
490    strum::Display,
491    strum::EnumString
492)]
493pub enum TcpMode {
494    /// Use localhost/loopback for the connection.
495    Localhost,
496    /// Use a non-loopback address resolved from the host domain name.
497    Hostname,
498}
499
500/// The hostname to use for TLS connections.
501#[derive(
502    Clone,
503    Debug,
504    PartialEq,
505    Eq,
506    Hash,
507    Serialize,
508    Deserialize,
509    strum::EnumIter,
510    strum::Display,
511    strum::EnumString
512)]
513pub enum TlsMode {
514    /// Use IpV6 address for TLS connections.
515    IpV6,
516    /// Use host domain name for TLS connections.
517    Hostname,
518    // TODO: consider adding IpV4 support.
519}
520
521/// Address format for TLS channels.
522#[derive(
523    Clone,
524    Debug,
525    PartialEq,
526    Eq,
527    Hash,
528    Serialize,
529    Deserialize,
530    Ord,
531    PartialOrd
532)]
533pub struct TlsAddr {
534    /// The hostname to connect to.
535    pub hostname: Hostname,
536    /// The port to connect to.
537    pub port: Port,
538}
539
540impl TlsAddr {
541    /// Creates a new TLS address with a normalized hostname.
542    pub fn new(hostname: impl Into<Hostname>, port: Port) -> Self {
543        Self {
544            hostname: normalize_host(&hostname.into()),
545            port,
546        }
547    }
548
549    /// Returns the port number for this address.
550    pub fn port(&self) -> Port {
551        self.port
552    }
553
554    /// Returns the hostname for this address.
555    pub fn hostname(&self) -> &str {
556        &self.hostname
557    }
558}
559
560impl FromStr for TlsAddr {
561    type Err = anyhow::Error;
562
563    fn from_str(addr: &str) -> Result<Self, Self::Err> {
564        let (hostname, port_str) = addr
565            .rsplit_once(':')
566            .ok_or_else(|| anyhow::anyhow!("invalid TLS address: {}", addr))?;
567        let port = port_str
568            .parse()
569            .map_err(|_| anyhow::anyhow!("invalid TLS address port: {}", port_str))?;
570        Ok(Self::new(hostname, port))
571    }
572}
573
574impl fmt::Display for TlsAddr {
575    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576        write!(f, "{}:{}", self.hostname, self.port)
577    }
578}
579
580/// Types of channel transports.
581#[derive(
582    Clone,
583    Debug,
584    PartialEq,
585    Eq,
586    Hash,
587    Serialize,
588    Deserialize,
589    typeuri::Named
590)]
591pub enum ChannelTransport {
592    /// Transport over a TCP connection.
593    Tcp(TcpMode),
594
595    /// Transport over a TCP connection with TLS support within Meta
596    MetaTls(TlsMode),
597
598    /// Transport over a TCP connection with configurable TLS support
599    Tls,
600
601    /// Transport over a QUIC connection with configurable TLS support.
602    Quic,
603
604    /// Transport over a QUIC connection with TLS support within Meta.
605    MetaQuic(TlsMode),
606
607    /// Local transports use a process-local registry and private Unix socket
608    /// pairs.
609    Local,
610
611    /// Transport over unix domain socket.
612    Unix,
613}
614
615impl fmt::Display for ChannelTransport {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        match self {
618            Self::Tcp(mode) => write!(f, "tcp({:?})", mode),
619            Self::MetaTls(mode) => write!(f, "metatls({:?})", mode),
620            Self::Tls => write!(f, "tls"),
621            Self::Quic => write!(f, "quic"),
622            Self::MetaQuic(mode) => write!(f, "metaquic({:?})", mode),
623            Self::Local => write!(f, "local"),
624            Self::Unix => write!(f, "unix"),
625        }
626    }
627}
628
629impl FromStr for ChannelTransport {
630    type Err = anyhow::Error;
631
632    fn from_str(s: &str) -> Result<Self, Self::Err> {
633        match s {
634            // Default to TcpMode::Hostname, if the mode isn't set
635            "tcp" => Ok(ChannelTransport::Tcp(TcpMode::Hostname)),
636            s if s.starts_with("tcp(") => {
637                let inner = &s["tcp(".len()..s.len() - 1];
638                let mode = inner.parse()?;
639                Ok(ChannelTransport::Tcp(mode))
640            }
641            "local" => Ok(ChannelTransport::Local),
642            "unix" => Ok(ChannelTransport::Unix),
643            "tls" => Ok(ChannelTransport::Tls),
644            "quic" => Ok(ChannelTransport::Quic),
645            s if s.starts_with("metatls(") && s.ends_with(")") => {
646                let inner = &s["metatls(".len()..s.len() - 1];
647                let mode = inner.parse()?;
648                Ok(ChannelTransport::MetaTls(mode))
649            }
650            s if s.starts_with("metaquic(") && s.ends_with(")") => {
651                let inner = &s["metaquic(".len()..s.len() - 1];
652                let mode = inner.parse()?;
653                Ok(ChannelTransport::MetaQuic(mode))
654            }
655            unknown => Err(anyhow::anyhow!("unknown channel transport: {}", unknown)),
656        }
657    }
658}
659
660impl ChannelTransport {
661    /// All known channel transports.
662    pub fn all() -> [ChannelTransport; 3] {
663        [
664            // TODO: @rusch add back once figuring out unspecified override for OSS CI
665            // ChannelTransport::Tcp(TcpMode::Localhost),
666            ChannelTransport::Tcp(TcpMode::Hostname),
667            ChannelTransport::Local,
668            ChannelTransport::Unix,
669            // Tls requires certificate configuration, tested separately in tls::tests
670            // TODO add MetaTls (T208303369)
671        ]
672    }
673
674    /// Return an "any" address for this transport.
675    pub fn any(&self) -> ChannelAddr {
676        ChannelAddr::any(self.clone())
677    }
678
679    /// Returns true if this transport type represents a remote channel.
680    pub fn is_remote(&self) -> bool {
681        match self {
682            ChannelTransport::Tcp(_) => true,
683            ChannelTransport::MetaTls(_) => true,
684            ChannelTransport::Tls => true,
685            ChannelTransport::Quic => true,
686            ChannelTransport::MetaQuic(_) => true,
687            ChannelTransport::Local => false,
688            ChannelTransport::Unix => false,
689        }
690    }
691
692    /// Returns true if this transport is served by the `net` module
693    /// (i.e., a kernel-level socket: TCP, Unix, or a TLS variant
694    /// thereof). The only non-net transport is the in-process
695    /// [`Local`](ChannelTransport::Local) channel.
696    pub fn is_net(&self) -> bool {
697        match self {
698            ChannelTransport::Tcp(_) => true,
699            ChannelTransport::MetaTls(_) => true,
700            ChannelTransport::Tls => true,
701            ChannelTransport::Quic => false,
702            ChannelTransport::MetaQuic(_) => false,
703            ChannelTransport::Unix => true,
704            ChannelTransport::Local => false,
705        }
706    }
707
708    /// Returns true if this transport uses TLS encryption.
709    pub fn is_tls(&self) -> bool {
710        matches!(self, ChannelTransport::Tls | ChannelTransport::MetaTls(_))
711    }
712
713    /// Returns true if this transport can carry the duplex byte-stream
714    /// protocol (see [`crate::channel::net::duplex`]). This is a
715    /// distinct predicate from [`is_net`](Self::is_net): the in-process
716    /// [`Local`](ChannelTransport::Local) transport is not a kernel
717    /// socket (so `is_net` is false) yet is still served over the net
718    /// stack and carries duplex.
719    pub fn supports_duplex(&self) -> bool {
720        match self {
721            ChannelTransport::Tcp(_) => true,
722            ChannelTransport::MetaTls(_) => true,
723            ChannelTransport::Tls => true,
724            // Quic actually supports duplex byte streams, but they are not yet tested.
725            ChannelTransport::Quic => false,
726            ChannelTransport::MetaQuic(_) => false,
727            ChannelTransport::Unix => true,
728            ChannelTransport::Local => true,
729        }
730    }
731}
732
733impl AttrValue for ChannelTransport {
734    fn display(&self) -> String {
735        self.to_string()
736    }
737
738    fn parse(s: &str) -> Result<Self, anyhow::Error> {
739        s.parse()
740    }
741}
742
743/// Specifies how to bind a channel server.
744#[derive(
745    Clone,
746    Debug,
747    PartialEq,
748    Eq,
749    Hash,
750    Serialize,
751    Deserialize,
752    typeuri::Named
753)]
754pub enum BindSpec {
755    /// Bind to any available address for the given transport.
756    Any(ChannelTransport),
757
758    /// Bind to a specific channel address.
759    Addr(ChannelAddr),
760}
761
762impl BindSpec {
763    /// Return an "any" address for this bind spec.
764    pub fn binding_addr(&self) -> ChannelAddr {
765        match self {
766            BindSpec::Any(transport) => ChannelAddr::any(transport.clone()),
767            BindSpec::Addr(addr) => addr.clone(),
768        }
769    }
770}
771
772impl From<ChannelTransport> for BindSpec {
773    fn from(transport: ChannelTransport) -> Self {
774        BindSpec::Any(transport)
775    }
776}
777
778impl From<ChannelAddr> for BindSpec {
779    fn from(addr: ChannelAddr) -> Self {
780        BindSpec::Addr(addr)
781    }
782}
783
784impl fmt::Display for BindSpec {
785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
786        match self {
787            Self::Any(transport) => write!(f, "{}", transport),
788            Self::Addr(addr) => write!(f, "{}", addr),
789        }
790    }
791}
792
793impl FromStr for BindSpec {
794    type Err = anyhow::Error;
795
796    fn from_str(s: &str) -> Result<Self, Self::Err> {
797        if let Ok(transport) = ChannelTransport::from_str(s) {
798            Ok(BindSpec::Any(transport))
799        } else if let Ok(addr) = ChannelAddr::from_zmq_url(s) {
800            Ok(BindSpec::Addr(addr))
801        } else if let Ok(addr) = ChannelAddr::from_str(s) {
802            Ok(BindSpec::Addr(addr))
803        } else {
804            Err(anyhow::anyhow!("invalid bind spec: {}", s))
805        }
806    }
807}
808
809impl AttrValue for BindSpec {
810    fn display(&self) -> String {
811        self.to_string()
812    }
813
814    fn parse(s: &str) -> Result<Self, anyhow::Error> {
815        Self::from_str(s)
816    }
817}
818
819/// The type of (TCP) hostnames.
820pub type Hostname = String;
821
822/// The type of (TCP) ports.
823pub type Port = u16;
824
825/// The type of a channel address, used to multiplex different underlying
826/// channel implementations. ChannelAddrs also have a concrete syntax:
827/// the address type (e.g., "tcp" or "local"), followed by ":", and an address
828/// parseable to that type. For example:
829///
830/// - `tcp:127.0.0.1:1234` - localhost port 1234 over TCP
831/// - `tcp:192.168.0.1:1111` - 192.168.0.1 port 1111 over TCP
832/// - `quic:example.com:1234` - example.com port 1234 over QUIC
833/// - `local:123` - the (in-process) local port 123
834/// - `unix:/some/path` - the Unix socket at `/some/path`
835///
836/// Both local and TCP ports 0 are reserved to indicate "any available
837/// port" when serving.
838///
839/// ```
840/// # use hyperactor::channel::ChannelAddr;
841/// let addr: ChannelAddr = "tcp:127.0.0.1:1234".parse().unwrap();
842/// let ChannelAddr::Tcp(socket_addr) = addr else {
843///     panic!()
844/// };
845/// assert_eq!(socket_addr.port(), 1234);
846/// assert_eq!(socket_addr.is_ipv4(), true);
847/// ```
848#[derive(
849    Clone,
850    Debug,
851    PartialEq,
852    Eq,
853    Ord,
854    PartialOrd,
855    Serialize,
856    Deserialize,
857    Hash,
858    typeuri::Named
859)]
860pub enum ChannelAddr {
861    /// A socket address used to establish TCP channels. Supports
862    /// both  IPv4 and IPv6 address / port pairs.
863    Tcp(SocketAddr),
864
865    /// An address to establish TCP channels with TLS support within Meta.
866    /// Uses TlsAddr with hostname and port.
867    MetaTls(TlsAddr),
868
869    /// An address to establish TCP channels with configurable TLS support.
870    /// Uses TlsAddr with hostname and port.
871    Tls(TlsAddr),
872
873    /// An address to establish QUIC channels with configurable TLS support.
874    /// Uses TlsAddr with hostname and port.
875    Quic(TlsAddr),
876
877    /// An address to establish QUIC channels with TLS support within Meta.
878    /// Uses TlsAddr with hostname and port.
879    MetaQuic(TlsAddr),
880
881    /// Local addresses are registered in-process and given an integral
882    /// index.
883    Local(u64),
884
885    /// A unix domain socket address. Supports both absolute path names as
886    ///  well as "abstract" names per https://manpages.debian.org/unstable/manpages/unix.7.en.html#Abstract_sockets
887    Unix(net::unix::SocketAddr),
888
889    /// A pair of addresses, one for the client and one for the server:
890    ///   - The client should dial to the `dial_to` address.
891    ///   - The server should bind to the `bind_to` address.
892    ///
893    /// The user is responsible for ensuring the traffic to the `dial_to` address
894    /// is routed to the `bind_to` address.
895    ///
896    /// This is useful for scenarios where the network is configured in a way,
897    /// that the bound address is not directly accessible from the client.
898    ///
899    /// For example, in AWS, the client could be provided with the public IP
900    /// address, yet the server is bound to a private IP address or simply
901    /// INADDR_ANY. Traffic to the public IP address is mapped to the private
902    /// IP address through network address translation (NAT).
903    ///
904    /// `Alias` is serve-side syntax. [`serve`] consumes it by binding to
905    /// `bind_to` and advertising `dial_to`; identity-bearing values such as
906    /// proc addresses, actor addresses, host references, and routing keys
907    /// should store only `dial_to`. Dial helpers canonicalize aliases the same
908    /// way.
909    Alias {
910        /// The address to which the client should dial to.
911        dial_to: Box<ChannelAddr>,
912        /// The address to which the server should bind to.
913        bind_to: Box<ChannelAddr>,
914    },
915}
916
917impl From<SocketAddr> for ChannelAddr {
918    fn from(value: SocketAddr) -> Self {
919        Self::Tcp(value)
920    }
921}
922
923impl From<net::unix::SocketAddr> for ChannelAddr {
924    fn from(value: net::unix::SocketAddr) -> Self {
925        Self::Unix(value)
926    }
927}
928
929impl From<std::os::unix::net::SocketAddr> for ChannelAddr {
930    fn from(value: std::os::unix::net::SocketAddr) -> Self {
931        Self::Unix(net::unix::SocketAddr::new(value))
932    }
933}
934
935impl From<tokio::net::unix::SocketAddr> for ChannelAddr {
936    fn from(value: tokio::net::unix::SocketAddr) -> Self {
937        std::os::unix::net::SocketAddr::from(value).into()
938    }
939}
940
941fn is_routable_address(addr: &IpAddr) -> bool {
942    !addr.is_loopback()
943        && match addr {
944            IpAddr::V6(v6) => !v6.is_unicast_link_local(),
945            IpAddr::V4(v4) => !v4.is_link_local(),
946        }
947}
948
949/// Return the first non-loopback, non-link-local address from a list.
950fn find_routable_address(addresses: &[IpAddr]) -> Option<IpAddr> {
951    addresses
952        .iter()
953        .find(|addr| is_routable_address(addr))
954        .cloned()
955}
956
957impl ChannelAddr {
958    /// Returns the "any" address for the given transport.
959    ///
960    /// For [`TcpMode::Hostname`], this resolves the host name to a
961    /// non-loopback, non-link-local address with port zero.
962    ///
963    /// # Panics
964    ///
965    /// Panics if host name resolution fails or produces no routable address.
966    pub fn any(transport: ChannelTransport) -> Self {
967        match transport {
968            ChannelTransport::Tcp(mode) => {
969                let ip = match mode {
970                    TcpMode::Localhost => IpAddr::V6(Ipv6Addr::LOCALHOST),
971                    TcpMode::Hostname => {
972                        let hostname = hostname::get().expect("failed to retrieve hostname");
973                        let hostname = hostname.to_str().expect("hostname is not valid UTF-8");
974                        // TODO: Avoid using DNS directly once we figure out a good extensibility story here
975                        let addresses =
976                            dns_lookup::lookup_host(hostname).expect("failed to resolve hostname");
977                        find_routable_address(&addresses).expect(
978                            "hostname resolved to no non-loopback, non-link-local address; configure an explicit transport address",
979                        )
980                    }
981                };
982                Self::Tcp(SocketAddr::new(ip, 0))
983            }
984            ChannelTransport::MetaTls(mode) => {
985                let host_address = match mode {
986                    TlsMode::Hostname => hostname::get()
987                        .ok()
988                        .and_then(|hostname| hostname.to_str().map(|s| s.to_string()))
989                        .unwrap_or("unknown_host".to_string()),
990                    TlsMode::IpV6 => {
991                        get_host_ipv6_address().expect("failed to retrieve ipv6 address")
992                    }
993                };
994                Self::MetaTls(TlsAddr::new(host_address, 0))
995            }
996            ChannelTransport::MetaQuic(mode) => {
997                let host_address = match mode {
998                    TlsMode::Hostname => hostname::get()
999                        .ok()
1000                        .and_then(|hostname| hostname.to_str().map(|s| s.to_string()))
1001                        .unwrap_or("unknown_host".to_string()),
1002                    TlsMode::IpV6 => {
1003                        get_host_ipv6_address().expect("failed to retrieve ipv6 address")
1004                    }
1005                };
1006                Self::MetaQuic(TlsAddr::new(host_address, 0))
1007            }
1008            ChannelTransport::Local => Self::Local(0),
1009            ChannelTransport::Tls => {
1010                let host_address = hostname::get()
1011                    .ok()
1012                    .and_then(|hostname| hostname.to_str().map(|s| s.to_string()))
1013                    .unwrap_or("localhost".to_string());
1014                Self::Tls(TlsAddr::new(host_address, 0))
1015            }
1016            ChannelTransport::Quic => {
1017                let host_address = hostname::get()
1018                    .ok()
1019                    .and_then(|hostname| hostname.to_str().map(|s| s.to_string()))
1020                    .unwrap_or("localhost".to_string());
1021                Self::Quic(TlsAddr::new(host_address, 0))
1022            }
1023            // This works because the file will be deleted but we know we have a unique file by this point.
1024            ChannelTransport::Unix => Self::Unix(net::unix::SocketAddr::from_str("").unwrap()),
1025        }
1026    }
1027
1028    /// The transport used by this address.
1029    pub fn transport(&self) -> ChannelTransport {
1030        match self {
1031            Self::Tcp(addr) => {
1032                if addr.ip().is_loopback() {
1033                    ChannelTransport::Tcp(TcpMode::Localhost)
1034                } else {
1035                    ChannelTransport::Tcp(TcpMode::Hostname)
1036                }
1037            }
1038            Self::MetaTls(addr) => match addr.hostname.parse::<IpAddr>() {
1039                Ok(IpAddr::V6(_)) => ChannelTransport::MetaTls(TlsMode::IpV6),
1040                Ok(IpAddr::V4(_)) => ChannelTransport::MetaTls(TlsMode::Hostname),
1041                Err(_) => ChannelTransport::MetaTls(TlsMode::Hostname),
1042            },
1043            Self::Tls(_) => ChannelTransport::Tls,
1044            Self::Quic(_) => ChannelTransport::Quic,
1045            Self::MetaQuic(addr) => match addr.hostname.parse::<IpAddr>() {
1046                Ok(IpAddr::V6(_)) => ChannelTransport::MetaQuic(TlsMode::IpV6),
1047                Ok(IpAddr::V4(_)) => ChannelTransport::MetaQuic(TlsMode::Hostname),
1048                Err(_) => ChannelTransport::MetaQuic(TlsMode::Hostname),
1049            },
1050            Self::Local(_) => ChannelTransport::Local,
1051            Self::Unix(_) => ChannelTransport::Unix,
1052            // bind_to's transport is what is actually used in communication.
1053            // Therefore we use its transport to represent the Alias.
1054            Self::Alias { bind_to, .. } => bind_to.transport(),
1055        }
1056    }
1057}
1058
1059#[cfg(fbcode_build)]
1060fn get_host_ipv6_address() -> anyhow::Result<String> {
1061    crate::meta::host_ip::host_ipv6_address()
1062}
1063
1064#[cfg(not(fbcode_build))]
1065fn get_host_ipv6_address() -> anyhow::Result<String> {
1066    Ok(local_ip_address::local_ipv6()?.to_string())
1067}
1068
1069impl fmt::Display for ChannelAddr {
1070    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1071        match self {
1072            Self::Tcp(addr) => write!(f, "tcp:{}", addr),
1073            Self::MetaTls(addr) => write!(f, "metatls:{}", addr),
1074            Self::Tls(addr) => write!(f, "tls:{}", addr),
1075            Self::Quic(addr) => write!(f, "quic:{}", addr),
1076            Self::MetaQuic(addr) => write!(f, "metaquic:{}", addr),
1077            Self::Local(index) => write!(f, "local:{}", index),
1078            Self::Unix(addr) => write!(f, "unix:{}", addr),
1079            Self::Alias { dial_to, bind_to } => {
1080                write!(f, "alias:dial_to={};bind_to={}", dial_to, bind_to)
1081            }
1082        }
1083    }
1084}
1085
1086impl FromStr for ChannelAddr {
1087    type Err = anyhow::Error;
1088
1089    fn from_str(addr: &str) -> Result<Self, Self::Err> {
1090        match addr.split_once('!').or_else(|| addr.split_once(':')) {
1091            Some(("local", rest)) => rest
1092                .parse::<u64>()
1093                .map(Self::Local)
1094                .map_err(anyhow::Error::from),
1095            Some(("tcp", rest)) => rest
1096                .parse::<SocketAddr>()
1097                .map(Self::Tcp)
1098                .map_err(anyhow::Error::from),
1099            Some(("metatls", rest)) => net::meta::parse(rest).map_err(|e| e.into()),
1100            Some(("tls", rest)) => net::tls::parse(rest).map_err(|e| e.into()),
1101            Some(("quic", rest)) => TlsAddr::from_str(rest).map(Self::Quic),
1102            Some(("metaquic", rest)) => TlsAddr::from_str(rest).map(Self::MetaQuic),
1103            Some(("unix", rest)) => Ok(Self::Unix(net::unix::SocketAddr::from_str(rest)?)),
1104            Some(("alias", _)) => Err(anyhow::anyhow!(
1105                "detect possible alias address, but we currently do not support \
1106                parsing alias' string representation since we only want to \
1107                support parsing its zmq url format."
1108            )),
1109            Some((r#type, _)) => Err(anyhow::anyhow!("no such channel type: {type}")),
1110            None => Err(anyhow::anyhow!("no channel type specified")),
1111        }
1112    }
1113}
1114
1115/// Normalize a host string. If the host is an IP address, parse and
1116/// re-format it to produce a canonical string representation.
1117pub(crate) fn normalize_host(host: &str) -> String {
1118    // Strip URI-style brackets (e.g., "[::1]") because IpAddr::from_str
1119    // rejects them — it only accepts bare addresses.
1120    let host_clean = host
1121        .strip_prefix('[')
1122        .and_then(|h| h.strip_suffix(']'))
1123        .unwrap_or(host);
1124
1125    if let Ok(ip_addr) = host_clean.parse::<IpAddr>() {
1126        ip_addr.to_string()
1127    } else {
1128        host.to_string()
1129    }
1130}
1131
1132impl ChannelAddr {
1133    /// Return the canonical address that remote peers should dial.
1134    ///
1135    /// For regular addresses this is the address itself. For aliases, this
1136    /// recursively consumes the alias and returns its `dial_to` address. Use
1137    /// this before storing an address in identity-bearing state; aliases are
1138    /// intended as input to [`serve`].
1139    pub fn into_dial_addr(self) -> Self {
1140        match self {
1141            Self::Alias { dial_to, .. } => (*dial_to).into_dial_addr(),
1142            addr => addr,
1143        }
1144    }
1145
1146    /// Parse ZMQ-style URL format: scheme://address
1147    /// Supports:
1148    /// - tcp://hostname:port or tcp://*:port (wildcard binding)
1149    /// - inproc://endpoint-name (equivalent to local)
1150    /// - ipc://path (equivalent to unix)
1151    /// - metatls://hostname:port or metatls://*:port
1152    /// - quic://hostname:port or quic://*:port
1153    /// - metaquic://hostname:port or metaquic://*:port
1154    /// - Alias format: dial_to_url@bind_to_url (e.g., tcp://host:port@tcp://host:port)
1155    ///   Note: Alias format is currently only supported for TCP addresses
1156    ///
1157    /// Alias format is meant for serving. Callers that will dial or store the
1158    /// result as an identity should canonicalize it with
1159    /// [`ChannelAddr::into_dial_addr`].
1160    pub fn from_zmq_url(address: &str) -> Result<Self, anyhow::Error> {
1161        let (addr, _listener) = Self::from_zmq_url_with_listener(address)?;
1162        Ok(addr)
1163    }
1164
1165    /// Parse ZMQ-style URL format, with support for pre-opened file descriptors.
1166    ///
1167    /// When the port portion of a URL is `fdNNN` (e.g. `tcp://myhost:fd5`),
1168    /// the file descriptor is adopted as a pre-bound `TcpListener`. The
1169    /// returned `ChannelAddr` will contain the real port that the fd is bound
1170    /// to, and the `Option<TcpListener>` will be `Some`.
1171    ///
1172    /// # Safety
1173    /// When using the `fd` syntax, the caller must ensure the file descriptor
1174    /// is a valid, bound TCP socket that is not used elsewhere. The socket
1175    /// does not need to be in a listening state — `listen()` will be called
1176    /// automatically.
1177    pub fn from_zmq_url_with_listener(
1178        address: &str,
1179    ) -> Result<(Self, Option<std::net::TcpListener>), anyhow::Error> {
1180        // Check for Alias format: dial_to_url@bind_to_url
1181        // The @ character separates two valid ZMQ URLs.
1182        if let Some(at_pos) = address
1183            .find('@')
1184            .filter(|&pos| address[..pos].starts_with("tcp://"))
1185        {
1186            let dial_to_str = &address[..at_pos];
1187            let bind_to_str = &address[at_pos + 1..];
1188
1189            // Validate that both addresses use TCP scheme
1190            if !dial_to_str.starts_with("tcp://") {
1191                return Err(anyhow::anyhow!(
1192                    "alias format is only supported for TCP addresses, got dial_to: {}",
1193                    dial_to_str
1194                ));
1195            }
1196            if !bind_to_str.starts_with("tcp://") {
1197                return Err(anyhow::anyhow!(
1198                    "alias format is only supported for TCP addresses, got bind_to: {}",
1199                    bind_to_str
1200                ));
1201            }
1202
1203            let dial_to = Self::from_zmq_url(dial_to_str)?;
1204            let bind_to = Self::from_zmq_url(bind_to_str)?;
1205
1206            return Ok((
1207                Self::Alias {
1208                    dial_to: Box::new(dial_to),
1209                    bind_to: Box::new(bind_to),
1210                },
1211                None,
1212            ));
1213        }
1214
1215        // Try ZMQ-style URL format first (scheme://...)
1216        let (scheme, address) = address.split_once("://").ok_or_else(|| {
1217            anyhow::anyhow!("address must be in url form scheme://endppoint {}", address)
1218        })?;
1219
1220        match scheme {
1221            "tcp" => {
1222                let (host, port, listener) = Self::parse_host_port_or_fd(address)?;
1223                let socket_addr = if host == "*" {
1224                    SocketAddr::new("::".parse().unwrap(), port)
1225                } else {
1226                    Self::resolve_hostname_to_socket_addr(host, port)?
1227                };
1228                Ok((Self::Tcp(socket_addr), listener))
1229            }
1230            "inproc" => {
1231                let port = address.parse::<u64>().map_err(|_| {
1232                    anyhow::anyhow!("inproc endpoint must be a valid port number: {}", address)
1233                })?;
1234                Ok((Self::Local(port), None))
1235            }
1236            "ipc" => Ok((Self::Unix(net::unix::SocketAddr::from_str(address)?), None)),
1237            "metatls" | "tls" | "quic" | "metaquic" => {
1238                let (host, port, listener) = Self::parse_host_port_or_fd(address)?;
1239                let hostname = if host == "*" {
1240                    std::net::Ipv6Addr::UNSPECIFIED.to_string()
1241                } else {
1242                    host.to_string()
1243                };
1244                let addr = match scheme {
1245                    "metatls" => Self::MetaTls(TlsAddr::new(hostname, port)),
1246                    "metaquic" => Self::MetaQuic(TlsAddr::new(hostname, port)),
1247                    "quic" => Self::Quic(TlsAddr::new(hostname, port)),
1248                    _ => Self::Tls(TlsAddr::new(hostname, port)),
1249                };
1250                Ok((addr, listener))
1251            }
1252            scheme => Err(anyhow::anyhow!("unsupported ZMQ scheme: {}", scheme)),
1253        }
1254    }
1255
1256    /// Parse host:port where the port may be either a numeric port or `fdNNN`
1257    /// referencing a pre-opened file descriptor. Returns (host, resolved_port, optional_listener).
1258    fn parse_host_port_or_fd(
1259        address: &str,
1260    ) -> Result<(&str, u16, Option<std::net::TcpListener>), anyhow::Error> {
1261        let (host, port_str) = address
1262            .rsplit_once(':')
1263            .ok_or_else(|| anyhow::anyhow!("invalid address format: {}", address))?;
1264
1265        if let Some(fd_str) = port_str.strip_prefix("fd") {
1266            let fd_num: RawFd = fd_str
1267                .parse()
1268                .map_err(|_| anyhow::anyhow!("invalid file descriptor number: {}", port_str))?;
1269            // Ensure the socket is in listening state. This is a no-op if
1270            // listen() was already called, and required if only bind() was done.
1271            // Safety: fd_num is valid and we are about to take ownership of it.
1272            let borrowed = unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd_num) };
1273            nix::sys::socket::listen(&borrowed, nix::sys::socket::Backlog::new(128)?)?;
1274            // Safety: caller guarantees the fd is a valid bound TCP socket.
1275            let std_listener = unsafe { std::net::TcpListener::from_raw_fd(fd_num) };
1276            let local_addr = std_listener.local_addr()?;
1277            Ok((host, local_addr.port(), Some(std_listener)))
1278        } else {
1279            let port: u16 = port_str
1280                .parse()
1281                .map_err(|_| anyhow::anyhow!("invalid port: {}", port_str))?;
1282            Ok((host, port, None))
1283        }
1284    }
1285
1286    /// Render as a ZMQ-style URL, the inverse of [`from_zmq_url`](Self::from_zmq_url).
1287    pub fn to_zmq_url(&self) -> String {
1288        match self {
1289            Self::Tcp(addr) => format!("tcp://{}", addr),
1290            Self::MetaTls(addr) => format!("metatls://{}:{}", addr.hostname, addr.port),
1291            Self::Tls(addr) => format!("tls://{}:{}", addr.hostname, addr.port),
1292            Self::Quic(addr) => format!("quic://{}:{}", addr.hostname, addr.port),
1293            Self::MetaQuic(addr) => format!("metaquic://{}:{}", addr.hostname, addr.port),
1294            Self::Local(index) => format!("inproc://{}", index),
1295            Self::Unix(addr) => format!("ipc://{}", addr),
1296            Self::Alias { dial_to, bind_to } => {
1297                format!("{}@{}", dial_to.to_zmq_url(), bind_to.to_zmq_url())
1298            }
1299        }
1300    }
1301
1302    /// Resolve hostname to SocketAddr, handling both IP addresses and hostnames
1303    fn resolve_hostname_to_socket_addr(host: &str, port: u16) -> Result<SocketAddr, anyhow::Error> {
1304        // Handle IPv6 addresses in brackets by stripping the brackets
1305        let host_clean = if host.starts_with('[') && host.ends_with(']') {
1306            &host[1..host.len() - 1]
1307        } else {
1308            host
1309        };
1310
1311        // First try to parse as an IP address directly
1312        if let Ok(ip_addr) = host_clean.parse::<IpAddr>() {
1313            return Ok(SocketAddr::new(ip_addr, port));
1314        }
1315
1316        // If not an IP, try hostname resolution
1317        use std::net::ToSocketAddrs;
1318        let mut addrs = (host_clean, port)
1319            .to_socket_addrs()
1320            .map_err(|e| anyhow::anyhow!("failed to resolve hostname '{}': {}", host_clean, e))?;
1321
1322        addrs
1323            .next()
1324            .ok_or_else(|| anyhow::anyhow!("no addresses found for hostname '{}'", host_clean))
1325    }
1326}
1327
1328/// Universal channel transmitter. Manages the link state, reconnections,
1329/// etc. on top of a [`net::Link`].
1330pub struct ChannelTx<M: RemoteMessage> {
1331    sender: mpsc::UnboundedSender<(M, CompletionSink<M>, Instant)>,
1332    dest: ChannelAddr,
1333    status: watch::Receiver<TxStatus>,
1334}
1335
1336impl<M: RemoteMessage> fmt::Debug for ChannelTx<M> {
1337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1338        f.debug_struct("ChannelTx")
1339            .field("addr", &self.addr())
1340            .finish()
1341    }
1342}
1343
1344#[async_trait]
1345impl<M: RemoteMessage> Tx<M> for ChannelTx<M> {
1346    // HOT PATH: Be mindful of performance when making changes here.
1347    // To test how performance is affected by a change, run the RPC benchmarks in
1348    // `monarch/python/benches/`.
1349    fn do_post(&self, message: M, completion: CompletionSink<M>) {
1350        tracing::trace!(
1351            name = "post",
1352            dest = %self.dest,
1353            "sending message"
1354        );
1355
1356        if let Err(mpsc::error::SendError((message, completion, _))) =
1357            self.sender.send((message, completion, Instant::now()))
1358        {
1359            let reason = self
1360                .status
1361                .borrow()
1362                .as_closed()
1363                .map(|r| SendErrorReason::Other(r.to_string()));
1364            completion.reject(SendError {
1365                error: ChannelError::Closed,
1366                message,
1367                reason,
1368            });
1369        }
1370    }
1371
1372    fn addr(&self) -> ChannelAddr {
1373        self.dest.clone()
1374    }
1375
1376    fn status(&self) -> &watch::Receiver<TxStatus> {
1377        &self.status
1378    }
1379}
1380
1381/// Universal channel receiver.
1382pub struct ChannelRx<M: RemoteMessage> {
1383    receiver: mpsc::Receiver<M>,
1384    dest: ChannelAddr,
1385    server: net::ServerHandle,
1386}
1387
1388impl<M: RemoteMessage> fmt::Debug for ChannelRx<M> {
1389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1390        f.debug_struct("ChannelRx")
1391            .field("addr", &self.addr())
1392            .finish()
1393    }
1394}
1395
1396impl<M: RemoteMessage> ChannelRx<M> {
1397    /// Stop the channel server, tagging the log with what triggered shutdown.
1398    fn stop(&self, trigger: &str) {
1399        self.server.stop(&format!(
1400            "ChannelRx {trigger}; channel address: {}",
1401            self.dest
1402        ));
1403    }
1404}
1405
1406#[async_trait]
1407impl<M: RemoteMessage> Rx<M> for ChannelRx<M> {
1408    async fn recv(&mut self) -> Result<M, ChannelError> {
1409        tracing::trace!(
1410            name = "recv",
1411            dest = %self.dest,
1412            "receiving message"
1413        );
1414        self.receiver.recv().await.ok_or(ChannelError::Closed)
1415    }
1416
1417    fn addr(&self) -> ChannelAddr {
1418        self.dest.clone()
1419    }
1420
1421    /// Gracefully shut down the channel server, waiting for pending
1422    /// acks to be flushed before returning.
1423    async fn join(mut self) {
1424        self.stop("joined");
1425        let _ = (&mut self.server).await;
1426        // Drop will call stop() again which is harmless (token already cancelled).
1427    }
1428}
1429
1430impl<M: RemoteMessage> Drop for ChannelRx<M> {
1431    fn drop(&mut self) {
1432        self.stop("dropped");
1433    }
1434}
1435
1436/// Dial the provided address, returning the corresponding Tx, or error
1437/// if the channel cannot be established. The underlying connection is
1438/// dropped whenever the returned Tx is dropped.
1439#[allow(clippy::result_large_err)] // TODO: Consider reducing the size of `ChannelError`.
1440#[track_caller]
1441pub fn dial<M: RemoteMessage>(addr: ChannelAddr) -> Result<ChannelTx<M>, ChannelError> {
1442    let addr = addr.into_dial_addr();
1443    tracing::debug!(name = "dial", caller = %Location::caller(), %addr, "dialing channel {}", addr);
1444    Ok(net::spawn::<M>(net::link(
1445        addr,
1446        net::SessionId::random(),
1447        0,
1448        net::ProtocolKind::Simplex,
1449    )?))
1450}
1451
1452/// Channels that may deliver messages out of send order.
1453pub mod unordered {
1454    use super::*;
1455
1456    /// Dial with out-of-order delivery and N parallel streams.
1457    ///
1458    /// Opens N links sharing a single `SessionId` (distinct
1459    /// `stream_id` in `1..=num_streams` so the server routes them
1460    /// through the multi-stream receive path). Frames are
1461    /// load-balanced across streams via a shared MPMC work queue —
1462    /// idle writers pull next.
1463    ///
1464    /// # Semantics (how this differs from [`super::dial`])
1465    ///
1466    /// Multi-stream trades several of [`super::dial`]'s delivery
1467    /// guarantees for aggregate bandwidth. Use [`super::dial`] when
1468    /// any of these matter:
1469    ///
1470    /// - **Ordering.** Messages are delivered to the receiver in
1471    ///   *arrival order across streams*, not send order. Two messages
1472    ///   posted back to back on the sender may reach the receiver out
1473    ///   of order when they're carried on different streams.
1474    ///   [`super::dial`] is strictly in-order.
1475    ///
1476    /// - **Retransmission on reconnect.** If a writer's connection
1477    ///   drops after the bytes of a message have been written but
1478    ///   before the peer acks, that message is **not retransmitted**
1479    ///   on the new connection. It may be silently lost.
1480    ///   [`super::dial`] re-sends all unacked messages on reconnect.
1481    ///
1482    /// - **Delivery timeouts.** No delivery timeout is enforced. On
1483    ///   sustained peer outage, writers reconnect indefinitely
1484    ///   (backoff capped at 5s); senders block in `send().await` with
1485    ///   no bound. [`super::dial`] fails unacked sends after
1486    ///   `MESSAGE_DELIVERY_TIMEOUT`.
1487    ///
1488    /// - **`Tx::send` return semantics.** On session shutdown,
1489    ///   messages still in the unacked buffer have their
1490    ///   `return_channel` dropped. Per the [`Tx::send`] contract,
1491    ///   this makes `send().await` return `Ok(())` for messages that
1492    ///   may never have been delivered — i.e. a "success" return does
1493    ///   not actually confirm delivery here. [`super::dial`] delivers
1494    ///   a structured `SendError` instead.
1495    ///
1496    /// # Ack semantics
1497    ///
1498    /// Receivers ack a cumulative watermark: the highest `N` such
1499    /// that all of `0..=N` have been observed across all streams.
1500    /// Acks may stall behind a single missing seq on a slow stream.
1501    #[track_caller]
1502    pub fn dial<M: RemoteMessage>(
1503        addr: ChannelAddr,
1504        num_streams: usize,
1505    ) -> Result<ChannelTx<M>, ChannelError> {
1506        assert!(num_streams > 0);
1507        let addr = addr.into_dial_addr();
1508        let session_id = net::SessionId::random();
1509        let links: Vec<net::NetLink> = (1..=num_streams)
1510            .map(|i| {
1511                net::link(
1512                    addr.clone(),
1513                    session_id,
1514                    i as u8,
1515                    net::ProtocolKind::Simplex,
1516                )
1517            })
1518            .collect::<Result<_, _>>()?;
1519        Ok(net::spawn_unordered::<M>(links))
1520    }
1521
1522    /// Serve a receiver that accepts unordered senders.
1523    ///
1524    /// The network server already routes multi-stream sessions by
1525    /// `SessionId`, so unordered serving uses the same listener as the
1526    /// ordered channel API.
1527    #[track_caller]
1528    pub fn serve<M: RemoteMessage>(
1529        addr: ChannelAddr,
1530    ) -> Result<(ChannelAddr, ChannelRx<M>), ChannelError> {
1531        super::serve(addr)
1532    }
1533
1534    /// Serve with an optional pre-opened listener.
1535    ///
1536    /// Pre-opened listeners are only supported for TCP-based transports,
1537    /// matching [`super::serve_with_listener`].
1538    #[track_caller]
1539    pub fn serve_with_listener<M: RemoteMessage>(
1540        addr: ChannelAddr,
1541        listener: Option<std::net::TcpListener>,
1542    ) -> Result<(ChannelAddr, ChannelRx<M>), ChannelError> {
1543        super::serve_with_listener(addr, listener)
1544    }
1545}
1546
1547/// Serve on the provided channel address. The server is turned down
1548/// when the returned Rx is dropped.
1549#[track_caller]
1550pub fn serve<M: RemoteMessage>(
1551    addr: ChannelAddr,
1552) -> Result<(ChannelAddr, ChannelRx<M>), ChannelError> {
1553    serve_with_listener(addr, None)
1554}
1555
1556/// Serve on the provided channel address, optionally using a pre-opened TCP listener.
1557/// When `listener` is `Some`, the provided listener is used instead of binding a new socket.
1558/// The server is turned down when the returned Rx is dropped.
1559#[track_caller]
1560pub fn serve_with_listener<M: RemoteMessage>(
1561    addr: ChannelAddr,
1562    listener: Option<std::net::TcpListener>,
1563) -> Result<(ChannelAddr, ChannelRx<M>), ChannelError> {
1564    let caller = Location::caller();
1565    serve_inner(addr, listener).map(|(addr, rx)| {
1566        tracing::debug!(
1567            name = "serve",
1568            %addr,
1569            %caller,
1570        );
1571        (addr, rx)
1572    })
1573}
1574
1575/// Serve a muxed listener on `addr`. Simplex clients (dialed via
1576/// [`channel::dial`](dial)) deliver into the bundled [`ChannelRx<M>`];
1577/// duplex clients (dialed via [`channel::duplex::dial`](net::duplex::dial))
1578/// populate the bundled [`DuplexServer<In, Out>`]. Only net transports
1579/// are supported; the caller picks transports that implement both
1580/// protocol styles.
1581///
1582/// The returned [`MuxServer`] owns both halves and a shared
1583/// [`MuxShutdown`]. Dropping or stopping any of these tears down the
1584/// listener and the other half together — see [`MuxServer`] for the
1585/// full lifecycle contract.
1586#[track_caller]
1587pub fn serve_mux<M: RemoteMessage, In: RemoteMessage, Out: RemoteMessage>(
1588    addr: ChannelAddr,
1589    prebound_listener: Option<std::net::TcpListener>,
1590) -> Result<MuxServer<M, In, Out>, ChannelError> {
1591    if !addr.transport().is_net() {
1592        return Err(ChannelError::InvalidAddress(format!(
1593            "serve_mux requires a net transport; got {}",
1594            addr
1595        )));
1596    }
1597    let parts = net::mux::serve::<M, In, Out>(addr, prebound_listener)?;
1598    Ok(MuxServer {
1599        addr: parts.addr,
1600        simplex: parts.simplex,
1601        duplex: parts.duplex,
1602        shutdown: MuxShutdown {
1603            join_handle: parts.join_handle,
1604            cancel: parts.cancel,
1605        },
1606    })
1607}
1608
1609/// A muxed server bundling a simplex receiver, a duplex accept
1610/// server, and the shared shutdown signal that ties them together.
1611///
1612/// All three components share one underlying listener. Lifecycle:
1613///
1614/// - Dropping the [`MuxServer`] cancels the shared shutdown and
1615///   tears down the listener and any in-flight sessions.
1616/// - [`MuxServer::stop`] does the same explicitly.
1617/// - [`MuxServer::split`] hands out the address, simplex half,
1618///   duplex half, and a [`MuxShutdown`] separately. After splitting,
1619///   dropping the simplex half, the duplex half, or the
1620///   [`MuxShutdown`] guard cancels the shared shutdown. The address
1621///   is a plain value and does not own any resources.
1622///
1623/// The simplex half is a [`ChannelRx<M>`] you `recv()` on; the duplex
1624/// half is a [`DuplexServer<In, Out>`] you `accept()` on. Neither is
1625/// `join()`-able — there is no separate per-half task to await.
1626pub struct MuxServer<M: RemoteMessage, In: RemoteMessage, Out: RemoteMessage> {
1627    addr: ChannelAddr,
1628    simplex: ChannelRx<M>,
1629    duplex: net::duplex::DuplexServer<In, Out>,
1630    shutdown: MuxShutdown,
1631}
1632
1633impl<M: RemoteMessage, In: RemoteMessage, Out: RemoteMessage> MuxServer<M, In, Out> {
1634    /// The address the muxed listener is bound to.
1635    pub fn addr(&self) -> &ChannelAddr {
1636        &self.addr
1637    }
1638
1639    /// Borrow the simplex receiver.
1640    pub fn simplex_mut(&mut self) -> &mut ChannelRx<M> {
1641        &mut self.simplex
1642    }
1643
1644    /// Borrow the duplex accept server.
1645    pub fn duplex_mut(&mut self) -> &mut net::duplex::DuplexServer<In, Out> {
1646        &mut self.duplex
1647    }
1648
1649    /// Cancel the shared shutdown and tear down both halves.
1650    pub fn stop(&self, reason: &str) {
1651        self.shutdown.stop(reason);
1652    }
1653
1654    /// Move the bound address, simplex half, duplex half, and a
1655    /// [`MuxShutdown`] guard out of this wrapper. After splitting,
1656    /// dropping the simplex half, the duplex half, or the
1657    /// [`MuxShutdown`] guard cancels the shared shutdown and tears
1658    /// the rest down. The address is a plain value and does not own
1659    /// any resources.
1660    pub fn split(
1661        self,
1662    ) -> (
1663        ChannelAddr,
1664        ChannelRx<M>,
1665        net::duplex::DuplexServer<In, Out>,
1666        MuxShutdown,
1667    ) {
1668        (self.addr, self.simplex, self.duplex, self.shutdown)
1669    }
1670
1671    /// Wire up handlers for both halves, spawn a background task that
1672    /// owns the orderly shutdown (duplex drain → simplex pump → listener),
1673    /// and return a [`MailboxServerHandle`](crate::mailbox::MailboxServerHandle).
1674    /// Calling `.stop()` on the handle drives the drain.
1675    ///
1676    /// `simplex_handler` consumes the simplex receiver and produces
1677    /// the simplex pump's `MailboxServerHandle` (typically by calling
1678    /// [`MailboxServer::serve`](crate::mailbox::MailboxServer::serve)
1679    /// on a forwarder). `duplex_handler` receives the
1680    /// [`DuplexServer`](net::duplex::DuplexServer) and a stop signal,
1681    /// and returns the future driving the duplex pump.
1682    ///
1683    /// **Shutdown ordering.** On stop, the coordinator awaits
1684    /// `duplex_task` first so the duplex handler can drive its own
1685    /// internal drain (e.g., the host's per-connection forwarder
1686    /// pumps stop and drop their `AttachSender`s, which lets
1687    /// `send_connected` flush queued outbound naturally as
1688    /// `SendLoopError::AppClosed`, before the handler's terminal
1689    /// `duplex_server.stop` signals listener shutdown and `join`
1690    /// waits for it). Stopping the simplex pump and listener happens
1691    /// after the duplex drain to avoid cascading cancellation through
1692    /// `dispatch_duplex_stream`'s `select!` while the duplex side
1693    /// still has queued sends.
1694    ///
1695    /// Mirrors [`MailboxServer::serve`](crate::mailbox::MailboxServer::serve)'s
1696    /// shape: take the work to do, return a `MailboxServerHandle`.
1697    pub fn serve<SH, DH, DF>(
1698        self,
1699        simplex_handler: SH,
1700        duplex_handler: DH,
1701    ) -> crate::mailbox::MailboxServerHandle
1702    where
1703        SH: FnOnce(ChannelRx<M>) -> crate::mailbox::MailboxServerHandle,
1704        DH: FnOnce(net::duplex::DuplexServer<In, Out>, tokio::sync::watch::Receiver<bool>) -> DF,
1705        DF: std::future::Future<Output = ()> + Send + 'static,
1706    {
1707        let (stopped_tx, mut stopped_rx) = tokio::sync::watch::channel(false);
1708        let duplex_stop = stopped_rx.clone();
1709        let simplex_handle = simplex_handler(self.simplex);
1710        let duplex_task = tokio::spawn(duplex_handler(self.duplex, duplex_stop));
1711        let shutdown = self.shutdown;
1712        let join_handle = tokio::spawn(async move {
1713            // Pend forever if `stopped_tx` is silently dropped (caller
1714            // discarded the handle without `stop()`). Mirrors the
1715            // existing `MailboxServer::serve` behavior of holding the
1716            // server open absent an explicit stop signal — otherwise
1717            // we'd tear down the mux as soon as the handle drops.
1718            let ok = stopped_rx.wait_for(|stopped| *stopped).await.is_ok();
1719            if !ok {
1720                std::future::pending::<()>().await;
1721            }
1722            const REASON: &str = "MuxServer shutdown";
1723            // 1. Wait for the duplex handler to complete its own drain.
1724            //    The handler already saw `duplex_stop` (a clone of our
1725            //    `stopped_rx`) at the same instant we did, so it is
1726            //    already winding down. Awaiting before any cancel fires
1727            //    preserves the natural app-closed path through
1728            //    `send_connected` so queued outbound is not abandoned.
1729            let _ = duplex_task.await;
1730            // 2. The duplex handler's drop fires `cancel_token`, which
1731            //    cascades to simplex per-session cancels and closes
1732            //    `simplex_rx`; the simplex pump then exits naturally
1733            //    via its `rx.recv()` returning `Closed`. We only need
1734            //    to await it. Calling `simplex_handle.stop` here would
1735            //    race the natural exit and panic on the watch send if
1736            //    the pump's receiver has already dropped.
1737            let _ = simplex_handle.await;
1738            // 3. Backstop: cancel the listener explicitly (idempotent
1739            //    if already cancelled), then await the accept-loop's
1740            //    full drain.
1741            shutdown.stop(REASON);
1742            let _ = shutdown.await;
1743            Ok::<(), crate::mailbox::MailboxServerError>(())
1744        });
1745        crate::mailbox::MailboxServerHandle::from_parts(join_handle, stopped_tx)
1746    }
1747}
1748
1749/// Awaitable shutdown handle for a [`MuxServer`]. Wraps the muxed
1750/// listener's accept-loop [`JoinHandle`](tokio::task::JoinHandle); the
1751/// caller signals teardown with [`stop`](Self::stop) and then `.await`s
1752/// the handle to confirm the listener task has fully exited. Drop
1753/// cancels the shared signal as a backstop, so a forgotten handle does
1754/// not leak the listener.
1755///
1756/// Mirrors the [`MailboxServerHandle`](crate::mailbox::MailboxServerHandle)
1757/// shape: signal stop, then await the handle.
1758pub struct MuxShutdown {
1759    join_handle: tokio::task::JoinHandle<Result<(), net::ServerError>>,
1760    cancel: CancellationToken,
1761}
1762
1763impl MuxShutdown {
1764    /// Signal the muxed listener to stop accepting new connections and
1765    /// tear down. The caller should subsequently `.await` the handle
1766    /// to confirm shutdown.
1767    pub fn stop(&self, reason: &str) {
1768        tracing::info!(
1769            name = "MuxServerStatus",
1770            status = "Stop::Sent",
1771            reason,
1772            "muxed frontend stop signalled",
1773        );
1774        self.cancel.cancel();
1775    }
1776
1777    /// Resolve when the shared shutdown has been cancelled (without
1778    /// awaiting the listener task itself). Useful for outer tasks that
1779    /// drive per-half pumps and need to wake on teardown.
1780    pub async fn cancelled(&self) {
1781        self.cancel.cancelled().await;
1782    }
1783}
1784
1785impl std::future::Future for MuxShutdown {
1786    type Output =
1787        <tokio::task::JoinHandle<Result<(), net::ServerError>> as std::future::Future>::Output;
1788
1789    fn poll(
1790        mut self: std::pin::Pin<&mut Self>,
1791        cx: &mut std::task::Context<'_>,
1792    ) -> std::task::Poll<Self::Output> {
1793        // `JoinHandle` is `Unpin`, so we can re-pin a mutable borrow of
1794        // it without unsafe pin projection. `MuxShutdown` is `Unpin` by
1795        // virtue of its `Unpin` fields, which lets us reach through the
1796        // outer `Pin`.
1797        std::pin::Pin::new(&mut self.join_handle).poll(cx)
1798    }
1799}
1800
1801impl Drop for MuxShutdown {
1802    fn drop(&mut self) {
1803        self.cancel.cancel();
1804    }
1805}
1806
1807fn serve_inner<M: RemoteMessage>(
1808    addr: ChannelAddr,
1809    listener: Option<std::net::TcpListener>,
1810) -> Result<(ChannelAddr, ChannelRx<M>), ChannelError> {
1811    match addr {
1812        ChannelAddr::Unix(_) => {
1813            assert!(
1814                listener.is_none(),
1815                "pre-opened listener not supported for Unix transport"
1816            );
1817            let (addr, rx) = net::server::serve::<M>(addr, listener)?;
1818            Ok((addr, rx))
1819        }
1820        ChannelAddr::Tcp(_)
1821        | ChannelAddr::Local(_)
1822        | ChannelAddr::Tls(_)
1823        | ChannelAddr::MetaTls(_)
1824        | ChannelAddr::Quic(_)
1825        | ChannelAddr::MetaQuic(_)
1826        // The `Alias` variant binds on its `bind_to` address but advertises
1827        // `dial_to`; `listen_with_prebound` resolves this, so it routes through
1828        // the same net serve path as the other TCP-based transports.
1829        | ChannelAddr::Alias { .. } => {
1830            let (addr, rx) = net::server::serve::<M>(addr, listener)?;
1831            Ok((addr, rx))
1832        }
1833    }
1834}
1835
1836/// Serve on the local address. The server is turned down
1837/// when the returned Rx is dropped.
1838pub fn serve_local<M: RemoteMessage>() -> (ChannelAddr, ChannelRx<M>) {
1839    serve::<M>(ChannelAddr::Local(0)).expect("fresh local stream port must bind")
1840}
1841
1842/// Reserve a local channel address that can be served later.
1843///
1844/// Local channels are backed by a process-local port registry, so reserving a
1845/// concrete address is a synchronous allocation that does not bind an OS
1846/// listener. Gateways use this to have a stable advertised local location
1847/// immediately, including when the process-wide gateway is initialized from a
1848/// [`std::sync::OnceLock`]. Serving is a separate step that binds the reserved
1849/// port to a receiver.
1850///
1851/// Network transports do not have an equivalent reservation API here: their
1852/// concrete addresses come from binding sockets and starting the corresponding
1853/// channel server.
1854pub fn reserve_local_addr() -> ChannelAddr {
1855    ChannelAddr::Local(local::reserve())
1856}
1857
1858#[cfg(test)]
1859mod tests {
1860    use std::assert_matches;
1861    use std::collections::HashSet;
1862    use std::net::IpAddr;
1863    use std::net::Ipv4Addr;
1864    use std::net::Ipv6Addr;
1865    use std::time::Duration;
1866
1867    use rand::RngExt as _;
1868    use rand::distr::Uniform;
1869    use tokio::task::JoinSet;
1870
1871    use super::net::*;
1872    use super::*;
1873    #[test]
1874    fn test_channel_addr() {
1875        let cases_ok = vec![
1876            (
1877                "tcp<DELIM>[::1]:1234",
1878                ChannelAddr::Tcp(SocketAddr::new(
1879                    IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
1880                    1234,
1881                )),
1882            ),
1883            (
1884                "tcp<DELIM>127.0.0.1:8080",
1885                ChannelAddr::Tcp(SocketAddr::new(
1886                    IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
1887                    8080,
1888                )),
1889            ),
1890            (
1891                "quic<DELIM>example.com:443",
1892                ChannelAddr::Quic(TlsAddr::new("example.com", 443)),
1893            ),
1894            (
1895                "metaquic<DELIM>example.com:443",
1896                ChannelAddr::MetaQuic(TlsAddr::new("example.com", 443)),
1897            ),
1898            #[cfg(target_os = "linux")]
1899            ("local<DELIM>123", ChannelAddr::Local(123)),
1900            (
1901                "unix<DELIM>@yolo",
1902                ChannelAddr::Unix(
1903                    unix::SocketAddr::from_abstract_name("yolo")
1904                        .expect("can't make socket from abstract name"),
1905                ),
1906            ),
1907            (
1908                "unix<DELIM>/cool/socket-path",
1909                ChannelAddr::Unix(
1910                    unix::SocketAddr::from_pathname("/cool/socket-path")
1911                        .expect("can't make socket from path"),
1912                ),
1913            ),
1914        ];
1915
1916        for (raw, parsed) in cases_ok {
1917            for delim in ["!", ":"] {
1918                let raw = raw.replace("<DELIM>", delim);
1919                assert_eq!(raw.parse::<ChannelAddr>().unwrap(), parsed);
1920            }
1921        }
1922
1923        let cases_err = vec![
1924            ("tcp:abcdef..123124", "invalid socket address syntax"),
1925            ("xxx:foo", "no such channel type: xxx"),
1926            ("127.0.0.1", "no channel type specified"),
1927            ("local:abc", "invalid digit found in string"),
1928        ];
1929
1930        for (raw, error) in cases_err {
1931            let Err(err) = raw.parse::<ChannelAddr>() else {
1932                panic!("expected error parsing: {}", raw)
1933            };
1934            assert_eq!(format!("{}", err), error);
1935        }
1936    }
1937
1938    #[test]
1939    fn test_zmq_style_channel_addr() {
1940        // Test TCP addresses
1941        assert_eq!(
1942            ChannelAddr::from_zmq_url("tcp://127.0.0.1:8080").unwrap(),
1943            ChannelAddr::Tcp("127.0.0.1:8080".parse().unwrap())
1944        );
1945
1946        // Test TCP wildcard binding
1947        assert_eq!(
1948            ChannelAddr::from_zmq_url("tcp://*:5555").unwrap(),
1949            ChannelAddr::Tcp("[::]:5555".parse().unwrap())
1950        );
1951
1952        // Test inproc (maps to local with numeric endpoint)
1953        assert_eq!(
1954            ChannelAddr::from_zmq_url("inproc://12345").unwrap(),
1955            ChannelAddr::Local(12345)
1956        );
1957
1958        // Test ipc (maps to unix)
1959        assert_eq!(
1960            ChannelAddr::from_zmq_url("ipc:///tmp/my-socket").unwrap(),
1961            ChannelAddr::Unix(unix::SocketAddr::from_pathname("/tmp/my-socket").unwrap())
1962        );
1963
1964        // Test metatls with hostname
1965        assert_eq!(
1966            ChannelAddr::from_zmq_url("metatls://example.com:443").unwrap(),
1967            ChannelAddr::MetaTls(TlsAddr::new("example.com", 443))
1968        );
1969
1970        // Test metatls with IP address (should be normalized)
1971        assert_eq!(
1972            ChannelAddr::from_zmq_url("metatls://192.168.1.1:443").unwrap(),
1973            ChannelAddr::MetaTls(TlsAddr::new("192.168.1.1", 443))
1974        );
1975
1976        // Test quic with hostname
1977        assert_eq!(
1978            ChannelAddr::from_zmq_url("quic://example.com:443").unwrap(),
1979            ChannelAddr::Quic(TlsAddr::new("example.com", 443))
1980        );
1981
1982        // Test quic wildcard binding
1983        assert_eq!(
1984            ChannelAddr::from_zmq_url("quic://*:8443").unwrap(),
1985            ChannelAddr::Quic(TlsAddr::new("::", 8443))
1986        );
1987
1988        // Test metaquic with hostname
1989        assert_eq!(
1990            ChannelAddr::from_zmq_url("metaquic://example.com:443").unwrap(),
1991            ChannelAddr::MetaQuic(TlsAddr::new("example.com", 443))
1992        );
1993
1994        // Test metaquic wildcard binding
1995        assert_eq!(
1996            ChannelAddr::from_zmq_url("metaquic://*:8443").unwrap(),
1997            ChannelAddr::MetaQuic(TlsAddr::new("::", 8443))
1998        );
1999
2000        // Test metatls with wildcard (should use IPv6 unspecified address)
2001        assert_eq!(
2002            ChannelAddr::from_zmq_url("metatls://*:8443").unwrap(),
2003            ChannelAddr::MetaTls(TlsAddr::new("::", 8443))
2004        );
2005
2006        // Test TCP hostname resolution (should resolve hostname to IP)
2007        // Note: This test may fail in environments without proper DNS resolution
2008        // We test that it at least doesn't fail to parse
2009        let tcp_hostname_result = ChannelAddr::from_zmq_url("tcp://localhost:8080");
2010        assert!(tcp_hostname_result.is_ok());
2011
2012        // Test IPv6 address
2013        assert_eq!(
2014            ChannelAddr::from_zmq_url("tcp://[::1]:1234").unwrap(),
2015            ChannelAddr::Tcp("[::1]:1234".parse().unwrap())
2016        );
2017
2018        // Test error cases
2019        assert!(ChannelAddr::from_zmq_url("invalid://scheme").is_err());
2020        assert!(ChannelAddr::from_zmq_url("tcp://invalid-port").is_err());
2021        assert!(ChannelAddr::from_zmq_url("metatls://no-port").is_err());
2022        assert!(ChannelAddr::from_zmq_url("inproc://not-a-number").is_err());
2023
2024        // IPv6 normalization: leading zeros are stripped
2025        assert_eq!(
2026            ChannelAddr::from_zmq_url("metatls://2a03:83e4:5000:c000:56d7:00cf:75ce:144a:443")
2027                .unwrap(),
2028            ChannelAddr::MetaTls(TlsAddr::new("2a03:83e4:5000:c000:56d7:cf:75ce:144a", 443))
2029        );
2030
2031        // Short and long forms of the same IPv6 produce equal ChannelAddr values
2032        assert_eq!(
2033            ChannelAddr::from_zmq_url("metatls://2a03:83e4:5000:c000:56d7:00cf:75ce:144a:443")
2034                .unwrap(),
2035            ChannelAddr::from_zmq_url("metatls://2a03:83e4:5000:c000:56d7:cf:75ce:144a:443")
2036                .unwrap(),
2037        );
2038
2039        // Bracketed IPv6 is normalized
2040        assert_eq!(
2041            ChannelAddr::from_zmq_url("metatls://[::1]:443").unwrap(),
2042            ChannelAddr::MetaTls(TlsAddr::new("::1", 443))
2043        );
2044
2045        // Same tests for tls://
2046        assert_eq!(
2047            ChannelAddr::from_zmq_url("tls://2a03:83e4:5000:c000:56d7:00cf:75ce:144a:443").unwrap(),
2048            ChannelAddr::Tls(TlsAddr::new("2a03:83e4:5000:c000:56d7:cf:75ce:144a", 443))
2049        );
2050        assert_eq!(
2051            ChannelAddr::from_zmq_url("tls://2a03:83e4:5000:c000:56d7:00cf:75ce:144a:443").unwrap(),
2052            ChannelAddr::from_zmq_url("tls://2a03:83e4:5000:c000:56d7:cf:75ce:144a:443").unwrap(),
2053        );
2054        assert_eq!(
2055            ChannelAddr::from_zmq_url("tls://[::1]:443").unwrap(),
2056            ChannelAddr::Tls(TlsAddr::new("::1", 443))
2057        );
2058    }
2059
2060    #[tokio::test]
2061    async fn test_reserved_local_addr_can_be_served() {
2062        let addr = reserve_local_addr();
2063        assert!(dial::<u64>(addr.clone()).is_err());
2064
2065        let (bound_addr, mut rx) = serve::<u64>(addr.clone()).unwrap();
2066        assert_eq!(bound_addr, addr);
2067
2068        let tx = dial::<u64>(addr.clone()).unwrap();
2069        tx.post(123);
2070        assert_eq!(rx.recv().await.unwrap(), 123);
2071        rx.join().await;
2072
2073        let (rebound_addr, _rx) = serve::<u64>(addr.clone()).unwrap();
2074        assert_eq!(rebound_addr, addr);
2075    }
2076
2077    #[test]
2078    fn test_normalize_host() {
2079        // Plain IPv4 passes through
2080        assert_eq!(normalize_host("192.168.1.1"), "192.168.1.1");
2081
2082        // Plain hostname passes through
2083        assert_eq!(normalize_host("example.com"), "example.com");
2084
2085        // IPv6 with leading zeros gets normalized
2086        assert_eq!(
2087            normalize_host("2a03:83e4:5000:c000:56d7:00cf:75ce:144a"),
2088            "2a03:83e4:5000:c000:56d7:cf:75ce:144a"
2089        );
2090
2091        // Bracketed IPv6 is stripped and normalized
2092        assert_eq!(normalize_host("[::1]"), "::1");
2093
2094        // Without bracket stripping, IpAddr::from_str rejects bracketed
2095        // addresses. This demonstrates that the bracket stripping in
2096        // normalize_host is necessary.
2097        assert!("[::1]".parse::<IpAddr>().is_err());
2098    }
2099
2100    #[test]
2101    fn test_find_routable_address_skips_loopback() {
2102        let addresses = [
2103            "127.0.1.1".parse::<IpAddr>().unwrap(),
2104            "169.254.1.1".parse::<IpAddr>().unwrap(),
2105            "::1".parse::<IpAddr>().unwrap(),
2106            "fe80::1".parse::<IpAddr>().unwrap(),
2107            "192.0.2.10".parse::<IpAddr>().unwrap(),
2108        ];
2109
2110        assert_eq!(
2111            find_routable_address(&addresses),
2112            Some("192.0.2.10".parse::<IpAddr>().unwrap())
2113        );
2114    }
2115
2116    #[test]
2117    fn test_find_routable_address_rejects_unroutable_addresses() {
2118        let addresses = [
2119            "127.0.1.1".parse::<IpAddr>().unwrap(),
2120            "169.254.1.1".parse::<IpAddr>().unwrap(),
2121            "::1".parse::<IpAddr>().unwrap(),
2122            "fe80::1".parse::<IpAddr>().unwrap(),
2123        ];
2124
2125        assert_eq!(find_routable_address(&addresses), None);
2126    }
2127
2128    #[test]
2129    fn test_zmq_style_alias_channel_addr() {
2130        // Test Alias format: dial_to_url@bind_to_url
2131        // The format is: dial_to_url@bind_to_url where both are valid ZMQ URLs
2132        // Note: Alias format is only supported for TCP addresses
2133
2134        // Test Alias with tcp on both sides
2135        let alias_addr = ChannelAddr::from_zmq_url("tcp://127.0.0.1:9000@tcp://[::]:8800").unwrap();
2136        match alias_addr {
2137            ChannelAddr::Alias { dial_to, bind_to } => {
2138                assert_eq!(
2139                    *dial_to,
2140                    ChannelAddr::Tcp("127.0.0.1:9000".parse().unwrap())
2141                );
2142                assert_eq!(*bind_to, ChannelAddr::Tcp("[::]:8800".parse().unwrap()));
2143            }
2144            _ => panic!("Expected Alias"),
2145        }
2146
2147        // Non-tcp left side: alias branch is skipped, parsed as regular address.
2148        // metatls:// with garbage host is not an alias.
2149        let non_alias = ChannelAddr::from_zmq_url("metatls://example.com:443@tcp://127.0.0.1:8080");
2150        assert!(
2151            !matches!(non_alias, Ok(ChannelAddr::Alias { .. })),
2152            "non-tcp left side must not produce Alias"
2153        );
2154
2155        // Test error: alias with non-tcp bind_to (not supported)
2156        assert!(
2157            ChannelAddr::from_zmq_url("tcp://127.0.0.1:8080@metatls://example.com:443").is_err()
2158        );
2159
2160        // Test error: invalid scheme falls through to scheme parsing, errors there
2161        assert!(ChannelAddr::from_zmq_url("invalid://scheme@tcp://127.0.0.1:8080").is_err());
2162
2163        // Test error: invalid bind_to URL in Alias
2164        assert!(ChannelAddr::from_zmq_url("tcp://127.0.0.1:8080@invalid://scheme").is_err());
2165
2166        // Test error: missing port in dial_to
2167        assert!(ChannelAddr::from_zmq_url("tcp://host@tcp://127.0.0.1:8080").is_err());
2168
2169        // Test error: missing port in bind_to
2170        assert!(ChannelAddr::from_zmq_url("tcp://127.0.0.1:8080@tcp://example.com").is_err());
2171    }
2172
2173    #[tokio::test]
2174    async fn test_multiple_connections() {
2175        for addr in ChannelTransport::all().map(ChannelAddr::any) {
2176            let (listen_addr, mut rx) = crate::channel::serve::<u64>(addr).unwrap();
2177
2178            let mut sends: JoinSet<()> = JoinSet::new();
2179            for message in 0u64..100u64 {
2180                let addr = listen_addr.clone();
2181                sends.spawn(async move {
2182                    let tx = dial::<u64>(addr).unwrap();
2183                    tx.post(message);
2184                });
2185            }
2186
2187            let mut received: HashSet<u64> = HashSet::new();
2188            while received.len() < 100 {
2189                received.insert(rx.recv().await.unwrap());
2190            }
2191
2192            for message in 0u64..100u64 {
2193                assert!(received.contains(&message));
2194            }
2195
2196            loop {
2197                match sends.join_next().await {
2198                    Some(Ok(())) => (),
2199                    Some(Err(err)) => panic!("{}", err),
2200                    None => break,
2201                }
2202            }
2203        }
2204    }
2205
2206    #[tokio::test]
2207    async fn test_server_close() {
2208        for addr in ChannelTransport::all().map(ChannelAddr::any) {
2209            if net::is_net_addr(&addr) {
2210                // Net has store-and-forward semantics. We don't expect failures
2211                // on closure.
2212                continue;
2213            }
2214
2215            let (listen_addr, rx) = crate::channel::serve::<u64>(addr).unwrap();
2216
2217            let tx = dial::<u64>(listen_addr).unwrap();
2218            tx.post(123);
2219            drop(rx);
2220
2221            // New transmits should fail... but there is buffering, etc.,
2222            // which can cause the failure to be delayed. We give it
2223            // a deadline, but it can still technically fail -- the test
2224            // should be considered a kind of integration test.
2225            let start = tokio::time::Instant::now();
2226
2227            let result = loop {
2228                let result = tx.try_post(123).await;
2229
2230                if result.is_err() || start.elapsed() > Duration::from_secs(10) {
2231                    break result;
2232                }
2233            };
2234            assert_matches!(
2235                result,
2236                Err(SendError {
2237                    error: ChannelError::Closed,
2238                    message: 123,
2239                    reason: None
2240                })
2241            );
2242        }
2243    }
2244
2245    fn addrs() -> Vec<ChannelAddr> {
2246        let rng = rand::rng();
2247        let uniform = Uniform::new_inclusive('a', 'z').unwrap();
2248        vec![
2249            "tcp:[::1]:0".parse().unwrap(),
2250            "local:0".parse().unwrap(),
2251            #[cfg(target_os = "linux")]
2252            "unix:".parse().unwrap(),
2253            #[cfg(target_os = "linux")]
2254            format!(
2255                "unix:@{}",
2256                rng.sample_iter(uniform).take(10).collect::<String>()
2257            )
2258            .parse()
2259            .unwrap(),
2260        ]
2261    }
2262
2263    #[test]
2264    fn test_bind_spec_from_str() {
2265        // Test parsing ChannelTransport strings -> BindSpec::Any
2266        assert_eq!(
2267            BindSpec::from_str("tcp").unwrap(),
2268            BindSpec::Any(ChannelTransport::Tcp(TcpMode::Hostname))
2269        );
2270        assert_eq!(
2271            BindSpec::from_str("metatls(Hostname)").unwrap(),
2272            BindSpec::Any(ChannelTransport::MetaTls(TlsMode::Hostname))
2273        );
2274
2275        // Test parsing ChannelAddr strings -> BindSpec::Addr
2276        assert_eq!(
2277            BindSpec::from_str("tcp:127.0.0.1:8080").unwrap(),
2278            BindSpec::Addr(ChannelAddr::Tcp("127.0.0.1:8080".parse().unwrap()))
2279        );
2280
2281        // Test parsing ZMQ URL format -> BindSpec::Addr
2282        assert_eq!(
2283            BindSpec::from_str("tcp://127.0.0.1:9000").unwrap(),
2284            BindSpec::Addr(ChannelAddr::Tcp("127.0.0.1:9000".parse().unwrap()))
2285        );
2286        assert_eq!(
2287            BindSpec::from_str("tcp://127.0.0.1:9000@tcp://[::1]:7200").unwrap(),
2288            BindSpec::Addr(
2289                ChannelAddr::from_zmq_url("tcp://127.0.0.1:9000@tcp://[::1]:7200").unwrap()
2290            )
2291        );
2292
2293        // Test error cases
2294        assert!(BindSpec::from_str("invalid_spec").is_err());
2295        assert!(BindSpec::from_str("unknown://scheme").is_err());
2296        assert!(BindSpec::from_str("").is_err());
2297    }
2298
2299    #[tokio::test]
2300    // TODO: OSS: called `Result::unwrap()` on an `Err` value: Server(Listen(Tcp([::1]:0), Os { code: 99, kind: AddrNotAvailable, message: "Cannot assign requested address" }))
2301    #[cfg_attr(not(fbcode_build), ignore)]
2302    async fn test_dial_serve() {
2303        for addr in addrs() {
2304            let (listen_addr, mut rx) = crate::channel::serve::<i32>(addr).unwrap();
2305            let tx = crate::channel::dial(listen_addr).unwrap();
2306            tx.post(123);
2307            assert_eq!(rx.recv().await.unwrap(), 123);
2308        }
2309    }
2310
2311    #[tokio::test]
2312    // TODO: OSS: called `Result::unwrap()` on an `Err` value: Server(Listen(Tcp([::1]:0), Os { code: 99, kind: AddrNotAvailable, message: "Cannot assign requested address" }))
2313    #[cfg_attr(not(fbcode_build), ignore)]
2314    async fn test_serve_alias_advertises_dial_to() {
2315        // Reserve an ephemeral port, then release it so the alias can bind to
2316        // it via `bind_to`.
2317        let probe = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2318        let port = probe.local_addr().unwrap().port();
2319        drop(probe);
2320
2321        // `dial_to` advertises a reachable loopback address; `bind_to` listens
2322        // on the wildcard interface (the case that matters where the dial
2323        // address cannot be bound directly, e.g. behind NAT).
2324        let alias =
2325            ChannelAddr::from_zmq_url(&format!("tcp://127.0.0.1:{port}@tcp://0.0.0.0:{port}"))
2326                .unwrap();
2327        assert_matches!(alias, ChannelAddr::Alias { .. });
2328
2329        let (listen_addr, mut rx) = crate::channel::serve::<i32>(alias).unwrap();
2330
2331        // Serving an alias consumes it: the advertised address is the plain
2332        // `dial_to`. Remote peers independently construct this same `Tcp`
2333        // address from the dial string, so the proc namespace derived from it
2334        // must match. If serving left the address an `Alias`, that match would
2335        // fail and messages would not route to the server.
2336        assert_eq!(
2337            listen_addr,
2338            ChannelAddr::Tcp(format!("127.0.0.1:{port}").parse().unwrap()),
2339            "serving an alias must advertise dial_to, not the alias itself"
2340        );
2341
2342        let tx = crate::channel::dial(listen_addr).unwrap();
2343        tx.post(123);
2344        assert_eq!(rx.recv().await.unwrap(), 123);
2345    }
2346
2347    #[tokio::test]
2348    // TODO: OSS: called `Result::unwrap()` on an `Err` value: Server(Listen(Tcp([::1]:0), Os { code: 99, kind: AddrNotAvailable, message: "Cannot assign requested address" }))
2349    #[cfg_attr(not(fbcode_build), ignore)]
2350    async fn test_send() {
2351        let config = hyperactor_config::global::lock();
2352
2353        // Use temporary config for this test
2354        let _guard1 = config.override_key(
2355            crate::config::MESSAGE_DELIVERY_TIMEOUT,
2356            Duration::from_secs(1),
2357        );
2358        let _guard2 = config.override_key(crate::config::MESSAGE_ACK_EVERY_N_MESSAGES, 1);
2359        for addr in addrs() {
2360            let (listen_addr, mut rx) = crate::channel::serve::<i32>(addr).unwrap();
2361            let tx = crate::channel::dial(listen_addr).unwrap();
2362            tx.send(123).await.unwrap();
2363            assert_eq!(rx.recv().await.unwrap(), 123);
2364
2365            drop(rx);
2366            assert_matches!(
2367                tx.send(123).await.unwrap_err(),
2368                SendError {
2369                    error: ChannelError::Closed,
2370                    message: 123,
2371                    ..
2372                }
2373            );
2374        }
2375    }
2376
2377    #[test]
2378    fn test_find_routable_address_skips_link_local_ipv6() {
2379        let link_local_v6: IpAddr = "fe80::1".parse().unwrap();
2380        let routable_v6: IpAddr = "2001:db8::1".parse().unwrap();
2381        let addrs = vec![link_local_v6, routable_v6];
2382        assert_eq!(find_routable_address(&addrs), Some(routable_v6));
2383    }
2384
2385    #[test]
2386    fn test_find_routable_address_skips_link_local_ipv4() {
2387        let link_local_v4: IpAddr = "169.254.1.1".parse().unwrap();
2388        let routable_v4: IpAddr = "192.168.1.1".parse().unwrap();
2389        let addrs = vec![link_local_v4, routable_v4];
2390        assert_eq!(find_routable_address(&addrs), Some(routable_v4));
2391    }
2392
2393    #[test]
2394    fn test_find_routable_address_returns_none_when_all_link_local() {
2395        let link_local_v6: IpAddr = "fe80::1".parse().unwrap();
2396        let link_local_v4: IpAddr = "169.254.1.1".parse().unwrap();
2397        let addrs = vec![link_local_v6, link_local_v4];
2398        assert_eq!(find_routable_address(&addrs), None);
2399    }
2400
2401    #[test]
2402    fn test_find_routable_address_mixed() {
2403        let link_local_v6: IpAddr = "fe80::1".parse().unwrap();
2404        let link_local_v4: IpAddr = "169.254.0.1".parse().unwrap();
2405        let routable_v4: IpAddr = "10.0.0.1".parse().unwrap();
2406        let routable_v6: IpAddr = "2001:db8::2".parse().unwrap();
2407
2408        // First routable address in list order should be returned.
2409        let addrs = vec![link_local_v6, link_local_v4, routable_v4, routable_v6];
2410        assert_eq!(find_routable_address(&addrs), Some(routable_v4));
2411    }
2412}