diff --git a/Cargo.toml b/Cargo.toml index 6c09c31e8..9fc2e059e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ default-members = ["quinn", "quinn-proto", "quinn-udp", "bench", "perf"] resolver = "2" [workspace.dependencies] +socket2 = "0.5" tracing = { version = "0.1.10", default-features = false, features = ["std"] } tracing-subscriber = { version = "0.3.0", default-features = false, features = ["env-filter", "fmt", "ansi", "time", "local-time"] } diff --git a/quinn-udp/Cargo.toml b/quinn-udp/Cargo.toml index 8ddc076b2..7a34943ce 100644 --- a/quinn-udp/Cargo.toml +++ b/quinn-udp/Cargo.toml @@ -20,7 +20,7 @@ log = ["tracing/log"] [dependencies] libc = "0.2.113" -socket2 = "0.5" +socket2 = { workspace = true } tracing = { workspace = true } [target.'cfg(windows)'.dependencies] diff --git a/quinn/Cargo.toml b/quinn/Cargo.toml index a2ca8a06d..643fc43dd 100644 --- a/quinn/Cargo.toml +++ b/quinn/Cargo.toml @@ -41,6 +41,7 @@ pin-project-lite = "0.2" proto = { package = "quinn-proto", path = "../quinn-proto", version = "0.11.2", default-features = false } rustls = { version = "0.23", default-features = false, features = ["ring", "std"], optional = true } smol = { version = "2", optional = true } +socket2 = { workspace = true } thiserror = "1.0.21" tracing = { workspace = true } tokio = { version = "1.28.1", features = ["sync"] } diff --git a/quinn/src/endpoint.rs b/quinn/src/endpoint.rs index 1f14c98a3..3dd373fb5 100644 --- a/quinn/src/endpoint.rs +++ b/quinn/src/endpoint.rs @@ -25,6 +25,7 @@ use proto::{ EndpointEvent, ServerConfig, }; use rustc_hash::FxHashMap; +use socket2::{Domain, Protocol, Socket, Type}; use tokio::sync::{futures::Notified, mpsc, Notify}; use tracing::{Instrument, Span}; use udp::{RecvMeta, BATCH_SIZE}; @@ -54,19 +55,32 @@ impl Endpoint { /// address like `0.0.0.0:0` or `[::]:0`, which allow communication with any reachable IPv4 or /// IPv6 address respectively from an OS-assigned port. /// - /// Platform defaults for dual-stack sockets vary. For example, any socket bound to a wildcard - /// IPv6 address on Windows will not by default be able to communicate with IPv4 - /// addresses. Portable applications should bind an address that matches the family they wish to - /// communicate within. + /// If an IPv6 address is provided, attempts to make the socket dual-stack so as to allow + /// communication with both IPv4 and IPv6 addresses. As such, calling `Endpoint::client` with + /// the address `[::]:0` is a reasonable default to maximize the ability to connect to other + /// address. For example: + /// + /// ``` + /// quinn::Endpoint::client((std::net::Ipv6Addr::UNSPECIFIED, 0).into()); + /// ``` + /// + /// Some environments may not allow creation of dual-stack sockets, in which case an IPv6 + /// client will only be able to connect to IPv6 servers. An IPv4 client is never dual-stack. #[cfg(feature = "ring")] pub fn client(addr: SocketAddr) -> io::Result { - let socket = std::net::UdpSocket::bind(addr)?; + let socket = Socket::new(Domain::for_address(addr), Type::DGRAM, Some(Protocol::UDP))?; + if addr.is_ipv6() { + if let Err(e) = socket.set_only_v6(false) { + tracing::debug!(%e, "unable to make socket dual-stack"); + } + } + socket.bind(&addr.into())?; let runtime = default_runtime() .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no async runtime found"))?; Self::new_with_abstract_socket( EndpointConfig::default(), None, - runtime.wrap_udp_socket(socket)?, + runtime.wrap_udp_socket(socket.into())?, runtime, ) } diff --git a/quinn/src/tests.rs b/quinn/src/tests.rs index 9d00fe312..90cff9e1a 100755 --- a/quinn/src/tests.rs +++ b/quinn/src/tests.rs @@ -406,7 +406,6 @@ fn echo_v4() { } #[test] -#[cfg(any(target_os = "linux", target_os = "macos"))] // Dual-stack sockets aren't the default anywhere else. fn echo_dualstack() { run_echo(EchoArgs { client_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),