commit 8808bfcea2aaa625775628939cc3e35078543ec2
Author: Zack Newman <zack@philomathiclife.com>
Date: Sat, 15 Feb 2025 13:04:00 -0700
init
Diffstat:
A | .gitignore | | | 2 | ++ |
A | Cargo.toml | | | 20 | ++++++++++++++++++++ |
A | LICENSE-APACHE | | | 177 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LICENSE-MIT | | | 20 | ++++++++++++++++++++ |
A | README.md | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/lib.rs | | | 532 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
6 files changed, 818 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+Cargo.lock
+target/**
diff --git a/Cargo.toml b/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+authors = ["Zack Newman <zack@philomathiclife.com>"]
+categories = ["asynchronous", "network-programming"]
+description = "Dual-stack TCP listener based on tokio."
+documentation = "https://docs.rs/webauthn_rp/latest/tokio_dual_stack/"
+edition = "2021"
+keywords = ["ip", "listener", "tcp", "tokio"]
+license = "MIT OR Apache-2.0"
+name = "tokio_dual_stack"
+readme = "README.md"
+repository = "https://git.philomathiclife.com/repos/tokio_dual_stack/"
+rust-version = "1.84.0"
+version = "0.1.0"
+
+[dependencies]
+pin-project-lite = { version = "0.2.16", default-features = false }
+tokio = { version = "1.43.0", default-features = false, features = ["net"] }
+
+[dev-dependencies]
+tokio = { version = "1.43.0", default-features = false, features = ["macros", "net", "rt"] }
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/LICENSE-MIT b/LICENSE-MIT
@@ -0,0 +1,20 @@
+Copyright © 2025 Zack Newman
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+“Software”), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
@@ -0,0 +1,67 @@
+# `tokio_dual_stack`
+
+[<img alt="git" src="https://git.philomathiclife.com/badges/tokio_dual_stack.svg" height="20">](https://git.philomathiclife.com/tokio_dual_stack/log.html)
+[<img alt="crates.io" src="https://img.shields.io/crates/v/tokio_dual_stack.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/tokio_dual_stack)
+[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-tokio_dual_stack-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/tokio_dual_stack/latest/tokio_dual_stack/)
+
+`tokio_dual_stack` is a library that adds a "dual-stack"
+[`TcpListener`](https://docs.rs/tokio/latest/tokio/net/struct.TcpListener.html).
+
+## Why is this useful?
+
+Only certain platforms offer the ability for one socket to handle both IPv6 and IPv4 requests
+(e.g., OpenBSD does not). For the platforms that do, it is often dependent on runtime configuration
+(e.g., [`IPV6_V6ONLY`](https://www.man7.org/linux/man-pages/man7/ipv6.7.html)). Additionally those platforms
+that support it often require the "wildcard" IPv6 address to be used (i.e., `::`) which has the unfortunate
+consequence of preventing other services from using the same protocol port.
+
+There are a few ways to work around this issue. One is to deploy the same service twice: one that uses
+an IPv6 socket and the other that uses an IPv4 socket. This can complicate deployments (e.g., the application
+may not have been written with the expectation that multiple deployments could be running at the same time) in
+addition to using more resources. Another is for the application to manually handle each socket (e.g.,
+[`select`](https://docs.rs/tokio/latest/tokio/macro.select.html)/[`join`](https://docs.rs/tokio/latest/tokio/macro.join.html)
+each `TcpListener::accept`).
+
+`DualStackTcpListener` chooses an implementation similar to what the equivalent `select` would do while
+also ensuring that one socket does not "starve" another by ensuring each socket is fairly given an opportunity
+to `TcpListener::accept` a connection. This has the nice benefit of having a similar API to what a single
+`TcpListener` would have as well as having similar performance to a socket that does handle both IPv6 and
+IPv4 requests.
+
+## Minimum Supported Rust Version (MSRV)
+
+This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that
+update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV
+will be updated.
+
+MSRV changes will correspond to a SemVer patch version bump pre-`1.0.0`; otherwise a minor version bump.
+
+## SemVer Policy
+
+* All on-by-default features of this library are covered by SemVer
+* MSRV is considered exempt from SemVer as noted above
+
+## License
+
+Licensed under either of
+
+* Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0))
+* MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT))
+
+at your option.
+
+## Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you,
+as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
+
+Before any PR is sent, `cargo clippy` and `cargo t` should be run. Additionally
+`RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be built.
+
+### Status
+
+This package is actively maintained and will conform to the latest version of
+[`tokio`](https://crates.io/crates/tokio).
+
+The crate is only tested on `x86_64-unknown-linux-gnu` and `x86_64-unknown-openbsd` targets, but it should work
+on most platforms.
diff --git a/src/lib.rs b/src/lib.rs
@@ -0,0 +1,532 @@
+//! [![git]](https://git.philomathiclife.com/tokio_dual_stack/log.html) [![crates-io]](https://crates.io/crates/tokio_dual_stack) [![docs-rs]](crate)
+//!
+//! [git]: https://git.philomathiclife.com/git_badge.svg
+//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
+//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
+//!
+//! `tokio_dual_stack` is a library that adds a "dual-stack" [`TcpListener`].
+//!
+//! ## Why is this useful?
+//!
+//! Only certain platforms offer the ability for one socket to handle both IPv6 and IPv4 requests
+//! (e.g., OpenBSD does not). For the platforms that do, it is often dependent on runtime configuration
+//! (e.g., [`IPV6_V6ONLY`](https://www.man7.org/linux/man-pages/man7/ipv6.7.html)). Additionally those platforms
+//! that support it often require the "wildcard" IPv6 address to be used (i.e., `::`) which has the unfortunate
+//! consequence of preventing other services from using the same protocol port.
+//!
+//! There are a few ways to work around this issue. One is to deploy the same service twice: one that uses
+//! an IPv6 socket and the other that uses an IPv4 socket. This can complicate deployments (e.g., the application
+//! may not have been written with the expectation that multiple deployments could be running at the same time) in
+//! addition to using more resources. Another is for the application to manually handle each socket (e.g.,
+//! [`select`](https://docs.rs/tokio/latest/tokio/macro.select.html)/[`join`](https://docs.rs/tokio/latest/tokio/macro.join.html)
+//! each [`TcpListener::accept`]).
+//!
+//! [`DualStackTcpListener`] chooses an implementation similar to what the equivalent `select` would do while
+//! also ensuring that one socket does not "starve" another by ensuring each socket is fairly given an opportunity
+//! to `TcpListener::accept` a connection. This has the nice benefit of having a similar API to what a single
+//! `TcpListener` would have as well as having similar performance to a socket that does handle both IPv6 and
+//! IPv4 requests.
+#![deny(
+ unknown_lints,
+ future_incompatible,
+ let_underscore,
+ missing_docs,
+ nonstandard_style,
+ refining_impl_trait,
+ rust_2018_compatibility,
+ rust_2018_idioms,
+ rust_2021_compatibility,
+ rust_2024_compatibility,
+ unsafe_code,
+ unused,
+ warnings,
+ clippy::all,
+ clippy::cargo,
+ clippy::complexity,
+ clippy::correctness,
+ clippy::nursery,
+ clippy::pedantic,
+ clippy::perf,
+ clippy::restriction,
+ clippy::style,
+ clippy::suspicious
+)]
+#![expect(
+ clippy::arbitrary_source_item_ordering,
+ clippy::blanket_clippy_restriction_lints,
+ clippy::implicit_return,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+use core::{
+ future::Future,
+ net::{SocketAddr, SocketAddrV4, SocketAddrV6},
+ pin::Pin,
+ sync::atomic::{AtomicBool, Ordering},
+ task::{Context, Poll},
+};
+use pin_project_lite::pin_project;
+use std::io::{Error, ErrorKind, Result};
+use tokio::net::{self, TcpListener, TcpSocket, TcpStream, ToSocketAddrs};
+/// Prevents [`Sealed`] from being publicly implementable.
+mod private {
+ /// Marker trait to prevent [`super::Tcp`] from being publicly implementable.
+ pub trait Sealed {}
+}
+use private::Sealed;
+/// TCP "listener".
+///
+/// This `trait` is sealed and cannot be implemented for types outside of `tokio_dual_stack`.
+///
+/// This exists primarily as a way to define type constructors or polymorphic functions
+/// that can user either a [`TcpListener`] or [`DualStackTcpListener`].
+///
+/// # Examples
+///
+/// ```no_run
+/// # use core::convert::Infallible;
+/// # use tokio_dual_stack::Tcp;
+/// async fn main_loop<T: Tcp>(listener: T) -> Infallible {
+/// loop {
+/// match listener.accept().await {
+/// Ok((_, socket)) => println!("Client socket: {socket}"),
+/// Err(e) => println!("TCP connection failure: {e}"),
+/// }
+/// }
+/// }
+/// ```
+pub trait Tcp: Sealed + Sized {
+ /// Creates a new TCP listener, which will be bound to the specified address(es).
+ ///
+ /// The returned listener is ready for accepting connections.
+ ///
+ /// Binding with a port number of 0 will request that the OS assigns a port to this listener.
+ /// The port allocated can be queried via the `local_addr` method.
+ ///
+ /// The address type can be any implementor of the [`ToSocketAddrs`] trait. If `addr` yields
+ /// multiple addresses, bind will be attempted with each of the addresses until one succeeds
+ /// and returns the listener. If none of the addresses succeed in creating a listener, the
+ /// error returned from the last attempt (the last address) is returned.
+ ///
+ /// This function sets the `SO_REUSEADDR` option on the socket.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
+ /// # use std::io::Result;
+ /// # use tokio_dual_stack::{DualStackTcpListener, Tcp as _};
+ /// #[tokio::main(flavor = "current_thread")]
+ /// async fn main() -> Result<()> {
+ /// let listener = DualStackTcpListener::bind(
+ /// [
+ /// SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 8080, 0, 0)),
+ /// SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080)),
+ /// ]
+ /// .as_slice(),
+ /// )
+ /// .await?;
+ /// Ok(())
+ /// }
+ /// ```
+ fn bind<A: ToSocketAddrs>(addr: A) -> impl Future<Output = Result<Self>>;
+ /// Accepts a new incoming connection from this listener.
+ ///
+ /// This function will yield once a new TCP connection is established. When established,
+ /// the corresponding `TcpStream` and the remote peer’s address will be returned.
+ ///
+ /// # Cancel safety
+ ///
+ /// This method is cancel safe. If the method is used as the event in a
+ /// [`tokio::select!`](https://docs.rs/tokio/latest/tokio/macro.select.html)
+ /// statement and some other branch completes first, then it is guaranteed that no new
+ /// connections were accepted by this method.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
+ /// # use std::io::Result;
+ /// # use tokio_dual_stack::{DualStackTcpListener, Tcp as _};
+ /// #[tokio::main(flavor = "current_thread")]
+ /// async fn main() -> Result<()> {
+ /// match DualStackTcpListener::bind(
+ /// [
+ /// SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 8080, 0, 0)),
+ /// SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080)),
+ /// ]
+ /// .as_slice(),
+ /// )
+ /// .await?.accept().await {
+ /// Ok((_, addr)) => println!("new client: {addr}"),
+ /// Err(e) => println!("couldn't get client: {e}"),
+ /// }
+ /// Ok(())
+ /// }
+ /// ```
+ fn accept(&self) -> impl Future<Output = Result<(TcpStream, SocketAddr)>> + Send + Sync;
+ /// Polls to accept a new incoming connection to this listener.
+ ///
+ /// If there is no connection to accept, `Poll::Pending` is returned and the current task will be notified by
+ /// a waker. Note that on multiple calls to `poll_accept`, only the `Waker` from the `Context` passed to the
+ /// most recent call is scheduled to receive a wakeup.
+ fn poll_accept(&self, cx: &mut Context<'_>) -> Poll<Result<(TcpStream, SocketAddr)>>;
+}
+impl Sealed for TcpListener {}
+impl Tcp for TcpListener {
+ #[expect(
+ clippy::future_not_send,
+ reason = "TcpListener::bind Future is not send"
+ )]
+ #[inline]
+ fn bind<A: ToSocketAddrs>(addr: A) -> impl Future<Output = Result<Self>> {
+ Self::bind(addr)
+ }
+ #[inline]
+ fn accept(&self) -> impl Future<Output = Result<(TcpStream, SocketAddr)>> + Send + Sync {
+ self.accept()
+ }
+ #[inline]
+ fn poll_accept(&self, cx: &mut Context<'_>) -> Poll<Result<(TcpStream, SocketAddr)>> {
+ self.poll_accept(cx)
+ }
+}
+/// "Dual-stack" TCP listener.
+///
+/// IPv6 and IPv4 TCP listener.
+#[derive(Debug)]
+pub struct DualStackTcpListener {
+ /// IPv6 TCP listener.
+ ip6: TcpListener,
+ /// IPv4 TCP listener.
+ ip4: TcpListener,
+ /// `true` iff [`Self::ip6::accept`] should be `poll`ed first; otherwise [`Self::ip4::accept`] is `poll`ed
+ /// first.
+ ///
+ /// This exists to prevent one IP version from "starving" another. Each time [`Self::accept`] or
+ /// [`Self::poll_accept`] is called, it's overwritten with the opposite `bool`.
+ ///
+ /// Note we could make this a `core::cell::Cell`; but for maximal flexibility and consistency with `TcpListener`,
+ /// we use an `AtomicBool`. This among other things means `DualStackTcpListener` will implement `Sync`.
+ ip6_first: AtomicBool,
+}
+impl DualStackTcpListener {
+ /// Creates `Self` using the [`TcpListener`]s returned from [`TcpSocket::listen`].
+ ///
+ /// [`Self::bind`] is useful when the behavior of [`TcpListener::bind`] is sufficient; however if the underlying
+ /// `TcpSocket`s need to be configured differently, then one must call this function instead.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`TcpSocket::local_addr`] does for either socket, the underlying sockets use the same IP version,
+ /// or [`TcpSocket::listen`] errors for either socket.
+ ///
+ /// Note on Windows-based platforms `TcpSocket::local_addr` will error if [`TcpSocket::bind`] was not called.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
+ /// # use std::io::Result;
+ /// # use tokio_dual_stack::DualStackTcpListener;
+ /// # use tokio::net::TcpSocket;
+ /// #[tokio::main(flavor = "current_thread")]
+ /// async fn main() -> Result<()> {
+ /// let ip6 = TcpSocket::new_v6()?;
+ /// ip6.bind(SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 8080, 0, 0)))?;
+ /// let ip4 = TcpSocket::new_v4()?;
+ /// ip4.bind(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080)))?;
+ /// let listener = DualStackTcpListener::from_sockets((ip6, 1024), (ip4, 1024))?;
+ /// Ok(())
+ /// }
+ /// ```
+ #[inline]
+ pub fn from_sockets(
+ (socket_1, backlog_1): (TcpSocket, u32),
+ (socket_2, backlog_2): (TcpSocket, u32),
+ ) -> Result<Self> {
+ socket_1.local_addr().and_then(|sock| {
+ socket_2.local_addr().and_then(|sock_2| {
+ if sock.is_ipv6() {
+ if sock_2.is_ipv4() {
+ socket_1.listen(backlog_1).and_then(|ip6| {
+ socket_2.listen(backlog_2).map(|ip4| Self {
+ ip6,
+ ip4,
+ ip6_first: AtomicBool::new(true),
+ })
+ })
+ } else {
+ Err(Error::new(
+ ErrorKind::InvalidData,
+ "TcpSockets are the same IP version",
+ ))
+ }
+ } else if sock_2.is_ipv6() {
+ socket_1.listen(backlog_1).and_then(|ip4| {
+ socket_2.listen(backlog_2).map(|ip6| Self {
+ ip6,
+ ip4,
+ ip6_first: AtomicBool::new(true),
+ })
+ })
+ } else {
+ Err(Error::new(
+ ErrorKind::InvalidData,
+ "TcpSockets are the same IP version",
+ ))
+ }
+ })
+ })
+ }
+ /// Returns the local address of each socket that the listeners are bound to.
+ ///
+ /// This can be useful, for example, when binding to port 0 to figure out which port was actually bound.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`TcpListener::local_addr`] does for either listener.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
+ /// # use std::io::Result;
+ /// # use tokio_dual_stack::{DualStackTcpListener, Tcp as _};
+ /// #[tokio::main(flavor = "current_thread")]
+ /// async fn main() -> Result<()> {
+ /// let ip6 = SocketAddrV6::new(Ipv6Addr::LOCALHOST, 8080, 0, 0);
+ /// let ip4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080);
+ /// assert_eq!(
+ /// DualStackTcpListener::bind([SocketAddr::V6(ip6), SocketAddr::V4(ip4)].as_slice())
+ /// .await?
+ /// .local_addr()?,
+ /// (ip6, ip4)
+ /// );
+ /// Ok(())
+ /// }
+ /// ```
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[inline]
+ pub fn local_addr(&self) -> Result<(SocketAddrV6, SocketAddrV4)> {
+ self.ip6.local_addr().and_then(|ip6| {
+ self.ip4.local_addr().map(|ip4| {
+ (
+ if let SocketAddr::V6(sock6) = ip6 {
+ sock6
+ } else {
+ unreachable!("there is a bug in DualStackTcpListener::bind")
+ },
+ if let SocketAddr::V4(sock4) = ip4 {
+ sock4
+ } else {
+ unreachable!("there is a bug in DualStackTcpListener::bind")
+ },
+ )
+ })
+ })
+ }
+ /// Sets the value for the `IP_TTL` option on both sockets.
+ ///
+ /// This value sets the time-to-live field that is used in every packet sent from each socket.
+ /// `ttl_ip6` is the `IP_TTL` value for the IPv6 socket and `ttl_ip4` is the `IP_TTL` value for the
+ /// IPv4 socket.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`TcpListener::set_ttl`] does for either listener.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
+ /// # use std::io::Result;
+ /// # use tokio_dual_stack::{DualStackTcpListener, Tcp as _};
+ /// #[tokio::main(flavor = "current_thread")]
+ /// async fn main() -> Result<()> {
+ /// DualStackTcpListener::bind(
+ /// [
+ /// SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 8080, 0, 0)),
+ /// SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080)),
+ /// ]
+ /// .as_slice(),
+ /// )
+ /// .await?.set_ttl(100, 100).expect("could not set TTL");
+ /// Ok(())
+ /// }
+ /// ```
+ #[inline]
+ pub fn set_ttl(&self, ttl_ip6: u32, ttl_ip4: u32) -> Result<()> {
+ self.ip6
+ .set_ttl(ttl_ip6)
+ .and_then(|()| self.ip4.set_ttl(ttl_ip4))
+ }
+ /// Gets the values of the `IP_TTL` option for both sockets.
+ ///
+ /// The first `u32` represents the `IP_TTL` value for the IPv6 socket and the second `u32` is the
+ /// `IP_TTL` value for the IPv4 socket. For more information about this option, see [`Self::set_ttl`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`TcpListener::ttl`] does for either listener.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
+ /// # use std::io::Result;
+ /// # use tokio_dual_stack::{DualStackTcpListener, Tcp as _};
+ /// #[tokio::main(flavor = "current_thread")]
+ /// async fn main() -> Result<()> {
+ /// let listener = DualStackTcpListener::bind(
+ /// [
+ /// SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 8080, 0, 0)),
+ /// SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080)),
+ /// ]
+ /// .as_slice(),
+ /// )
+ /// .await?;
+ /// listener.set_ttl(100, 100).expect("could not set TTL");
+ /// assert_eq!(listener.ttl()?, (100, 100));
+ /// Ok(())
+ /// }
+ /// ```
+ #[inline]
+ pub fn ttl(&self) -> Result<(u32, u32)> {
+ self.ip6
+ .ttl()
+ .and_then(|ip6| self.ip4.ttl().map(|ip4| (ip6, ip4)))
+ }
+}
+pin_project! {
+ /// `Future` returned by [`DualStackTcpListener::accept]`.
+ struct AcceptFut<
+ F: Future<Output = Result<(TcpStream, SocketAddr)>>,
+ F2: Future<Output = Result<(TcpStream, SocketAddr)>>,
+ > {
+ // Accept future for one `TcpListener`.
+ #[pin]
+ fut_1: F,
+ // Accept future for the other `TcpListener`.
+ #[pin]
+ fut_2: F2,
+ }
+}
+impl<
+ F: Future<Output = Result<(TcpStream, SocketAddr)>>,
+ F2: Future<Output = Result<(TcpStream, SocketAddr)>>,
+ > Future for AcceptFut<F, F2>
+{
+ type Output = Result<(TcpStream, SocketAddr)>;
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+ let this = self.project();
+ match this.fut_1.poll(cx) {
+ Poll::Ready(res) => Poll::Ready(res),
+ Poll::Pending => this.fut_2.poll(cx),
+ }
+ }
+}
+impl Sealed for DualStackTcpListener {}
+impl Tcp for DualStackTcpListener {
+ #[expect(
+ clippy::future_not_send,
+ reason = "TcpListener::bind Future is not send"
+ )]
+ #[inline]
+ async fn bind<A: ToSocketAddrs>(addr: A) -> Result<Self> {
+ match net::lookup_host(addr).await {
+ Ok(socks) => {
+ let mut last_err = None;
+ let mut ip6_opt = None;
+ let mut ip4_opt = None;
+ for sock in socks {
+ match ip6_opt {
+ None => match ip4_opt {
+ None => {
+ let is_ip6 = sock.is_ipv6();
+ match TcpListener::bind(sock).await {
+ Ok(ip) => {
+ if is_ip6 {
+ ip6_opt = Some(ip);
+ } else {
+ ip4_opt = Some(ip);
+ }
+ }
+ Err(err) => last_err = Some(err),
+ };
+ }
+ Some(ip4) => {
+ if sock.is_ipv6() {
+ match TcpListener::bind(sock).await {
+ Ok(ip6) => {
+ return Ok(Self {
+ ip6,
+ ip4,
+ ip6_first: AtomicBool::new(true),
+ })
+ }
+ Err(err) => last_err = Some(err),
+ };
+ }
+ ip4_opt = Some(ip4);
+ }
+ },
+ Some(ip6) => {
+ if sock.is_ipv4() {
+ match TcpListener::bind(sock).await {
+ Ok(ip4) => {
+ return Ok(Self {
+ ip6,
+ ip4,
+ ip6_first: AtomicBool::new(true),
+ })
+ }
+ Err(err) => last_err = Some(err),
+ };
+ }
+ ip6_opt = Some(ip6);
+ }
+ }
+ }
+ Err(last_err.unwrap_or_else(|| {
+ Error::new(
+ ErrorKind::InvalidInput,
+ "could not resolve to an IPv6 and IPv4 address",
+ )
+ }))
+ }
+ Err(err) => Err(err),
+ }
+ }
+ #[inline]
+ fn accept(&self) -> impl Future<Output = Result<(TcpStream, SocketAddr)>> + Send + Sync {
+ // The correctness of code does not depend on `self.ip6_first`; therefore
+ // we elect for the most performant `Ordering`.
+ if self.ip6_first.swap(false, Ordering::Relaxed) {
+ AcceptFut {
+ fut_1: self.ip6.accept(),
+ fut_2: self.ip4.accept(),
+ }
+ } else {
+ // The correctness of code does not depend on `self.ip6_first`; therefore
+ // we elect for the most performant `Ordering`.
+ self.ip6_first.store(true, Ordering::Relaxed);
+ AcceptFut {
+ fut_1: self.ip4.accept(),
+ fut_2: self.ip6.accept(),
+ }
+ }
+ }
+ #[inline]
+ fn poll_accept(&self, cx: &mut Context<'_>) -> Poll<Result<(TcpStream, SocketAddr)>> {
+ // The correctness of code does not depend on `self.ip6_first`; therefore
+ // we elect for the most performant `Ordering`.
+ if self.ip6_first.swap(false, Ordering::Relaxed) {
+ self.ip6.poll_accept(cx)
+ } else {
+ // The correctness of code does not depend on `self.ip6_first`; therefore
+ // we elect for the most performant `Ordering`.
+ self.ip6_first.store(true, Ordering::Relaxed);
+ self.ip4.poll_accept(cx)
+ }
+ }
+}