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}