Skip to main content

dynomite/net/
listener.rs

1//! Dual-stack listener helpers.
2//!
3//! Each `listen:` / `dyn_listen:` directive binds a single socket:
4//! `0.0.0.0:port` opens an IPv4-only socket, `[::]:port` opens an
5//! IPv6 socket whose `IPV6_V6ONLY` flag matches the platform
6//! default (on Linux, the `/proc/sys/net/ipv6/bindv6only` knob,
7//! usually `0`).
8//!
9//! The Stage 9 Rust wiring uses [`socket2::Socket`] to open the
10//! socket explicitly so the engine can:
11//!
12//! * bind to a single address family when the YAML specified a
13//!   concrete address (`192.0.2.1:8102`, `[::1]:8102`),
14//! * bind to a v6 wildcard with `IPV6_V6ONLY=0` when the YAML
15//!   specified `[::]:port`, accepting both v4 and v6 clients on
16//!   one listener (matching most platforms' default),
17//! * bind to a v4 wildcard when the YAML specified `0.0.0.0:port`.
18//!
19//! Callers that want strict-v6 behavior pass
20//! [`BindOptions::v6_only`].
21//!
22//! # Examples
23//!
24//! ```
25//! use dynomite::net::listener::{bind_dual_stack, BindOptions};
26//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
27//! let addr: std::net::SocketAddr = "127.0.0.1:0".parse().unwrap();
28//! let listener = bind_dual_stack(addr, BindOptions::default()).unwrap();
29//! assert!(listener.local_addr().unwrap().ip().is_loopback());
30//! # });
31//! ```
32
33use std::io;
34use std::net::SocketAddr;
35
36use socket2::{Domain, Protocol, Socket, Type};
37use tokio::net::TcpListener;
38
39/// Configuration knobs for [`bind_dual_stack`].
40#[derive(Copy, Clone, Debug, Default)]
41pub struct BindOptions {
42    /// When the bind address is a v6 wildcard (`[::]`), set the
43    /// `IPV6_V6ONLY` flag instead of accepting v4-mapped clients.
44    /// The default (`false`) accepts both families, matching the
45    /// platform default on Linux.
46    pub v6_only: bool,
47    /// `SO_REUSEADDR`. Defaults to `true`.
48    pub reuseaddr: bool,
49    /// TCP listen backlog. Defaults to `1024`. The configured pool
50    /// `backlog` knob (Stage 4) feeds this field at startup.
51    pub backlog: i32,
52}
53
54impl BindOptions {
55    /// Build options with `v6_only = true` and the other knobs at
56    /// their defaults.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use dynomite::net::listener::BindOptions;
62    /// assert!(BindOptions::v6_only_strict().v6_only);
63    /// ```
64    #[must_use]
65    pub fn v6_only_strict() -> Self {
66        Self {
67            v6_only: true,
68            ..Self::default_filled()
69        }
70    }
71
72    fn default_filled() -> Self {
73        Self {
74            v6_only: false,
75            reuseaddr: true,
76            backlog: 1024,
77        }
78    }
79}
80
81/// Bind a TCP listener using dual-stack semantics.
82///
83/// When `addr` is a v6 wildcard (`::`) and `opts.v6_only` is
84/// `false` (the default), the listener accepts both v4 and v6
85/// clients via v4-mapped addresses on platforms that support it
86/// (Linux, macOS, *BSD).
87///
88/// # Errors
89///
90/// Returns the underlying `io::Error` from `socket(2)`,
91/// `setsockopt(2)`, `bind(2)`, or `listen(2)`.
92///
93/// # Examples
94///
95/// ```
96/// use dynomite::net::listener::{bind_dual_stack, BindOptions};
97/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
98/// let addr: std::net::SocketAddr = "[::1]:0".parse().unwrap();
99/// let l = bind_dual_stack(addr, BindOptions::default()).unwrap();
100/// assert!(l.local_addr().unwrap().is_ipv6());
101/// # });
102/// ```
103pub fn bind_dual_stack(addr: SocketAddr, opts: BindOptions) -> io::Result<TcpListener> {
104    let opts = if opts.backlog == 0 {
105        BindOptions {
106            backlog: 1024,
107            ..opts
108        }
109    } else {
110        opts
111    };
112
113    let domain = if addr.is_ipv6() {
114        Domain::IPV6
115    } else {
116        Domain::IPV4
117    };
118    let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
119    socket.set_nonblocking(true)?;
120    if opts.reuseaddr {
121        socket.set_reuse_address(true)?;
122    }
123    if addr.is_ipv6() {
124        // The default on most platforms accepts both v4 and v6
125        // clients when bound to `[::]`. The caller can flip
126        // `v6_only_strict` to opt out.
127        socket.set_only_v6(opts.v6_only)?;
128    }
129    socket.bind(&addr.into())?;
130    socket.listen(opts.backlog)?;
131    let std_listener: std::net::TcpListener = socket.into();
132    TcpListener::from_std(std_listener)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use std::net::Ipv4Addr;
139
140    #[tokio::test]
141    async fn bind_v4_loopback() {
142        let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0);
143        let l = bind_dual_stack(addr, BindOptions::default()).unwrap();
144        assert!(l.local_addr().unwrap().is_ipv4());
145    }
146}