use std::{
fmt,
io::{self, IoSliceMut},
net::{SocketAddr, SocketAddrV4, SocketAddrV6},
pin::Pin,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
task::{Context, Poll},
};
use quinn_udp::{RecvMeta, Transmit};
use tokio::io::ReadBuf;
use tracing::debug;
use super::{AsyncUdpSocket, UdpPollHelper, UdpPoller, UdpSender};
pub struct DualStackSocket {
v4: Option<Arc<tokio::net::UdpSocket>>,
v6: Option<Arc<tokio::net::UdpSocket>>,
v4_addr: Option<SocketAddr>,
v6_addr: Option<SocketAddr>,
poll_v4_first: AtomicBool,
}
impl fmt::Debug for DualStackSocket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DualStackSocket")
.field("v4", &self.v4_addr)
.field("v6", &self.v6_addr)
.finish()
}
}
impl DualStackSocket {
pub fn new(
v4: Option<tokio::net::UdpSocket>,
v6: Option<tokio::net::UdpSocket>,
) -> io::Result<Self> {
if v4.is_none() && v6.is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"at least one socket (IPv4 or IPv6) must be provided",
));
}
let v4_addr = v4.as_ref().and_then(|s| s.local_addr().ok());
let v6_addr = v6.as_ref().and_then(|s| s.local_addr().ok());
Ok(Self {
v4: v4.map(Arc::new),
v6: v6.map(Arc::new),
v4_addr,
v6_addr,
poll_v4_first: AtomicBool::new(false),
})
}
pub fn local_addr_v4(&self) -> Option<SocketAddr> {
self.v4_addr
}
pub fn local_addr_v6(&self) -> Option<SocketAddr> {
self.v6_addr
}
pub fn local_addrs(&self) -> (Option<SocketAddr>, Option<SocketAddr>) {
(self.v4_addr, self.v6_addr)
}
pub fn is_dual(&self) -> bool {
self.v4.is_some() && self.v6.is_some()
}
fn convert_dest(dest: SocketAddr, socket_is_v6: bool) -> io::Result<SocketAddr> {
match dest {
SocketAddr::V4(v4) if socket_is_v6 => {
Ok(SocketAddr::V6(to_mapped_v6(v4)))
}
SocketAddr::V6(v6) if !socket_is_v6 => {
if let Some(v4) = v6.ip().to_ipv4_mapped() {
Ok(SocketAddr::new(v4.into(), v6.port()))
} else {
Err(io::Error::new(
io::ErrorKind::AddrNotAvailable,
"cannot send native IPv6 address through IPv4 socket",
))
}
}
other => Ok(other),
}
}
fn poll_recv_one(
socket: &tokio::net::UdpSocket,
socket_is_v4: bool,
cx: &mut Context<'_>,
buf: &mut IoSliceMut<'_>,
meta: &mut RecvMeta,
) -> Poll<io::Result<()>> {
let mut read_buf = ReadBuf::new(buf);
let addr = match socket.poll_recv_from(cx, &mut read_buf) {
Poll::Ready(Ok(addr)) => addr,
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
};
let len = read_buf.filled().len();
let mapped_addr = if socket_is_v4 {
match addr {
SocketAddr::V4(v4) => SocketAddr::V6(to_mapped_v6(v4)),
other => other,
}
} else {
addr
};
*meta = RecvMeta {
len,
stride: len,
addr: mapped_addr,
ecn: None,
dst_ip: None,
};
Poll::Ready(Ok(()))
}
}
impl AsyncUdpSocket for DualStackSocket {
fn create_sender(&self) -> Pin<Box<dyn UdpSender>> {
Box::pin(DualStackUdpSender {
v4: self.v4.clone(),
v6: self.v6.clone(),
v4_writable: self.v4.as_ref().map(make_socket_poller),
v6_writable: self.v6.as_ref().map(make_socket_poller),
})
}
fn poll_recv(
&self,
cx: &mut Context,
bufs: &mut [IoSliceMut<'_>],
meta: &mut [RecvMeta],
) -> Poll<io::Result<usize>> {
if bufs.is_empty() || meta.is_empty() {
return Poll::Ready(Ok(0));
}
let v4_first = self.poll_v4_first.fetch_xor(true, Ordering::Relaxed);
let (first_socket, first_is_v4, second_socket, second_is_v4) = if v4_first {
(&self.v4, true, &self.v6, false)
} else {
(&self.v6, false, &self.v4, true)
};
if let Some(socket) = first_socket {
match Self::poll_recv_one(socket, first_is_v4, cx, &mut bufs[0], &mut meta[0]) {
Poll::Ready(Ok(())) => return Poll::Ready(Ok(1)),
Poll::Ready(Err(e)) => {
debug!(
"recv error on {} socket: {}",
if first_is_v4 { "IPv4" } else { "IPv6" },
e
);
}
Poll::Pending => {} }
}
if let Some(socket) = second_socket {
match Self::poll_recv_one(socket, second_is_v4, cx, &mut bufs[0], &mut meta[0]) {
Poll::Ready(Ok(())) => return Poll::Ready(Ok(1)),
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => {} }
}
Poll::Pending
}
fn local_addr(&self) -> io::Result<SocketAddr> {
if let Some(addr) = self.v6_addr {
return Ok(addr);
}
if let Some(addr) = self.v4_addr {
return Ok(addr);
}
Err(io::Error::new(
io::ErrorKind::NotConnected,
"no socket bound",
))
}
fn may_fragment(&self) -> bool {
let v4_frag = self
.v4
.as_ref()
.map(|_| true) .unwrap_or(true);
let v6_frag = self.v6.as_ref().map(|_| true).unwrap_or(true);
v4_frag || v6_frag
}
}
fn make_socket_poller(socket: &Arc<tokio::net::UdpSocket>) -> Pin<Box<dyn UdpPoller>> {
let socket = Arc::clone(socket);
Box::pin(UdpPollHelper::new(move || {
let socket = Arc::clone(&socket);
async move { socket.writable().await }
}))
}
#[derive(Debug)]
struct DualStackUdpSender {
v4: Option<Arc<tokio::net::UdpSocket>>,
v6: Option<Arc<tokio::net::UdpSocket>>,
v4_writable: Option<Pin<Box<dyn UdpPoller>>>,
v6_writable: Option<Pin<Box<dyn UdpPoller>>>,
}
impl DualStackUdpSender {
fn select_family(&self, dest: &SocketAddr) -> Option<bool> {
let has_v4 = self.v4.is_some();
let has_v6 = self.v6.is_some();
match dest {
SocketAddr::V4(_) => {
if has_v4 {
Some(false)
} else {
has_v6.then_some(true)
}
}
SocketAddr::V6(addr) if addr.ip().to_ipv4_mapped().is_some() => {
if has_v4 {
Some(false)
} else {
has_v6.then_some(true)
}
}
SocketAddr::V6(_) => {
if has_v6 {
Some(true)
} else {
has_v4.then_some(false)
}
}
}
}
}
impl UdpSender for DualStackUdpSender {
fn poll_send(
mut self: Pin<&mut Self>,
transmit: &Transmit,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
let socket_is_v6 = self.select_family(&transmit.destination).ok_or_else(|| {
io::Error::new(
io::ErrorKind::AddrNotAvailable,
"no socket available for destination address family",
)
})?;
loop {
let (socket, poller) = if socket_is_v6 {
(
self.v6.as_ref().cloned().ok_or_else(|| {
io::Error::new(io::ErrorKind::AddrNotAvailable, "IPv6 socket unavailable")
})?,
self.v6_writable.as_mut().ok_or_else(|| {
io::Error::new(io::ErrorKind::AddrNotAvailable, "IPv6 poller unavailable")
})?,
)
} else {
(
self.v4.as_ref().cloned().ok_or_else(|| {
io::Error::new(io::ErrorKind::AddrNotAvailable, "IPv4 socket unavailable")
})?,
self.v4_writable.as_mut().ok_or_else(|| {
io::Error::new(io::ErrorKind::AddrNotAvailable, "IPv4 poller unavailable")
})?,
)
};
match poller.as_mut().poll_writable(cx) {
Poll::Ready(Ok(())) => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
let dest = DualStackSocket::convert_dest(transmit.destination, socket_is_v6)?;
match socket.try_send_to(transmit.contents, dest) {
Ok(_) => return Poll::Ready(Ok(())),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue,
Err(e) => return Poll::Ready(Err(e)),
}
}
}
}
fn to_mapped_v6(v4: SocketAddrV4) -> SocketAddrV6 {
SocketAddrV6::new(v4.ip().to_ipv6_mapped(), v4.port(), 0, 0)
}
#[cfg(feature = "network-discovery")]
pub fn create_dual_stack_sockets(
port: u16,
) -> io::Result<(Option<std::net::UdpSocket>, Option<std::net::UdpSocket>)> {
use tracing::info;
let mut v6_result = None;
let mut v4_result = None;
let mut actual_port = port;
match create_v6_socket(port) {
Ok(socket) => {
if port == 0 {
actual_port = socket.local_addr().map(|a| a.port()).unwrap_or(0);
}
v6_result = Some(socket);
}
Err(e) => {
debug!("IPv6 socket creation failed: {e}");
}
}
match create_v4_socket(actual_port) {
Ok(socket) => {
v4_result = Some(socket);
}
Err(e) if actual_port != 0 && port == 0 => {
debug!("IPv4 bind to port {actual_port} failed ({e}), trying OS-assigned");
match create_v4_socket(0) {
Ok(socket) => {
v4_result = Some(socket);
}
Err(e2) => {
debug!("IPv4 socket creation failed: {e2}");
}
}
}
Err(e) => {
debug!("IPv4 socket creation failed: {e}");
}
}
if v4_result.is_none() && v6_result.is_none() {
return Err(io::Error::new(
io::ErrorKind::AddrNotAvailable,
"failed to bind both IPv4 and IPv6 sockets",
));
}
let v4_desc = v4_result
.as_ref()
.and_then(|s| s.local_addr().ok())
.map(|a| a.to_string())
.unwrap_or_else(|| "none".to_string());
let v6_desc = v6_result
.as_ref()
.and_then(|s| s.local_addr().ok())
.map(|a| a.to_string())
.unwrap_or_else(|| "none".to_string());
info!("Dual-stack sockets: IPv4={v4_desc}, IPv6={v6_desc}");
Ok((v4_result, v6_result))
}
#[cfg(feature = "network-discovery")]
fn create_v6_socket(port: u16) -> io::Result<std::net::UdpSocket> {
use socket2::{Domain, Protocol, Socket, Type};
let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
socket.set_only_v6(true)?;
socket.set_nonblocking(true)?;
let buffer_size = crate::config::buffer_defaults::PLATFORM_DEFAULT;
let _ = socket.set_send_buffer_size(buffer_size);
let _ = socket.set_recv_buffer_size(buffer_size);
let addr = SocketAddrV6::new(std::net::Ipv6Addr::UNSPECIFIED, port, 0, 0);
socket.bind(&socket2::SockAddr::from(addr))?;
Ok(socket.into())
}
#[cfg(feature = "network-discovery")]
fn create_v4_socket(port: u16) -> io::Result<std::net::UdpSocket> {
use socket2::{Domain, Protocol, Socket, Type};
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
socket.set_nonblocking(true)?;
let buffer_size = crate::config::buffer_defaults::PLATFORM_DEFAULT;
let _ = socket.set_send_buffer_size(buffer_size);
let _ = socket.set_recv_buffer_size(buffer_size);
let addr = SocketAddrV4::new(std::net::Ipv4Addr::UNSPECIFIED, port);
socket.bind(&socket2::SockAddr::from(addr))?;
Ok(socket.into())
}
#[cfg(not(feature = "network-discovery"))]
pub fn create_dual_stack_sockets(
port: u16,
) -> io::Result<(Option<std::net::UdpSocket>, Option<std::net::UdpSocket>)> {
let v6_addr: SocketAddr = format!("[::]:{port}")
.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad address: {e}")))?;
let v4_addr: SocketAddr = format!("0.0.0.0:{port}")
.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad address: {e}")))?;
let v6 = std::net::UdpSocket::bind(v6_addr).ok();
let v4 = std::net::UdpSocket::bind(v4_addr).ok();
if v4.is_none() && v6.is_none() {
return Err(io::Error::new(
io::ErrorKind::AddrNotAvailable,
"failed to bind both IPv4 and IPv6 sockets",
));
}
Ok((v4, v6))
}
pub fn wrap_dual_stack(
v4: Option<std::net::UdpSocket>,
v6: Option<std::net::UdpSocket>,
) -> io::Result<DualStackSocket> {
let v4_tokio = match v4 {
Some(s) => {
s.set_nonblocking(true)?;
Some(tokio::net::UdpSocket::from_std(s)?)
}
None => None,
};
let v6_tokio = match v6 {
Some(s) => {
s.set_nonblocking(true)?;
Some(tokio::net::UdpSocket::from_std(s)?)
}
None => None,
};
DualStackSocket::new(v4_tokio, v6_tokio)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn test_to_mapped_v6() {
let v4 = SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 1), 9000);
let mapped = to_mapped_v6(v4);
assert_eq!(mapped.port(), 9000);
assert!(mapped.ip().to_ipv4_mapped().is_some());
assert_eq!(
mapped.ip().to_ipv4_mapped().unwrap(),
Ipv4Addr::new(192, 168, 1, 1)
);
}
#[tokio::test]
async fn test_dual_stack_socket_creation() {
let v4 = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let v6 = tokio::net::UdpSocket::bind("[::1]:0").await.unwrap();
let ds = DualStackSocket::new(Some(v4), Some(v6)).unwrap();
assert!(ds.is_dual());
assert!(ds.local_addr_v4().is_some());
assert!(ds.local_addr_v6().is_some());
let addr = ds.local_addr().unwrap();
assert!(addr.is_ipv6());
}
#[tokio::test]
async fn test_v4_only_fallback() {
let v4 = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let ds = DualStackSocket::new(Some(v4), None).unwrap();
assert!(!ds.is_dual());
assert!(ds.local_addr_v4().is_some());
assert!(ds.local_addr_v6().is_none());
let addr = ds.local_addr().unwrap();
assert!(addr.is_ipv4());
}
#[tokio::test]
async fn test_v6_only_fallback() {
let v6 = tokio::net::UdpSocket::bind("[::1]:0").await.unwrap();
let ds = DualStackSocket::new(None, Some(v6)).unwrap();
assert!(!ds.is_dual());
assert!(ds.local_addr_v4().is_none());
assert!(ds.local_addr_v6().is_some());
let addr = ds.local_addr().unwrap();
assert!(addr.is_ipv6());
}
#[test]
fn test_no_socket_fails() {
let result = DualStackSocket::new(None, None);
assert!(result.is_err());
}
#[tokio::test]
async fn test_send_routing_ipv4_mapped() {
let receiver = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
receiver.set_nonblocking(true).unwrap();
let recv_port = receiver.local_addr().unwrap().port();
let v4 = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
v4.writable().await.unwrap();
let v6 = tokio::net::UdpSocket::bind("[::]:0").await.unwrap();
v6.writable().await.unwrap();
let ds = DualStackSocket::new(Some(v4), Some(v6)).unwrap();
let v4_port = ds.local_addr_v4().unwrap().port();
let v6_port = ds.local_addr_v6().unwrap().port();
assert_ne!(
v4_port, v6_port,
"test requires distinct v4/v6 sender ports to discriminate egress socket"
);
let mapped_dest: SocketAddr = format!("[::ffff:127.0.0.1]:{recv_port}").parse().unwrap();
let transmit = Transmit {
destination: mapped_dest,
ecn: None,
contents: b"hello-v4-mapped",
segment_size: None,
src_ip: None,
};
let mut sender = ds.create_sender();
std::future::poll_fn(|cx| sender.as_mut().poll_send(&transmit, cx))
.await
.unwrap();
let mut buf = [0u8; 64];
let mut received = false;
for _ in 0..50 {
match receiver.recv_from(&mut buf) {
Ok((len, sender_addr)) => {
assert_eq!(&buf[..len], b"hello-v4-mapped");
assert_eq!(
sender_addr.port(),
v4_port,
"reply must egress the v4 socket (port {v4_port}), saw port {} (v6 socket is {v6_port})",
sender_addr.port()
);
received = true;
break;
}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(e) => panic!("recv error: {e}"),
}
}
assert!(received, "v4 receiver should get the IPv4-mapped datagram");
}
#[tokio::test]
async fn test_send_routing_native_v6() {
let receiver = std::net::UdpSocket::bind("[::1]:0").unwrap();
receiver.set_nonblocking(true).unwrap();
let recv_port = receiver.local_addr().unwrap().port();
let v4 = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
v4.writable().await.unwrap();
let v6 = tokio::net::UdpSocket::bind("[::1]:0").await.unwrap();
v6.writable().await.unwrap();
let ds = DualStackSocket::new(Some(v4), Some(v6)).unwrap();
let dest: SocketAddr = format!("[::1]:{recv_port}").parse().unwrap();
let transmit = Transmit {
destination: dest,
ecn: None,
contents: b"hello-v6",
segment_size: None,
src_ip: None,
};
let mut sender = ds.create_sender();
std::future::poll_fn(|cx| sender.as_mut().poll_send(&transmit, cx))
.await
.unwrap();
let mut buf = [0u8; 64];
let mut received = false;
for _ in 0..50 {
match receiver.recv_from(&mut buf) {
Ok((len, _)) => {
assert_eq!(&buf[..len], b"hello-v6");
received = true;
break;
}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(e) => panic!("recv error: {e}"),
}
}
assert!(received, "v6 receiver should get the native v6 datagram");
}
#[cfg(feature = "network-discovery")]
#[test]
fn test_create_dual_stack_sockets_port_zero() {
let (v4, v6) = create_dual_stack_sockets(0).unwrap();
assert!(v4.is_some() || v6.is_some());
if let Some(ref s) = v4 {
assert!(s.local_addr().unwrap().is_ipv4());
assert_ne!(s.local_addr().unwrap().port(), 0);
}
if let Some(ref s) = v6 {
assert!(s.local_addr().unwrap().is_ipv6());
assert_ne!(s.local_addr().unwrap().port(), 0);
}
}
#[tokio::test]
async fn test_recv_v4_address_mapping() {
let v4 = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let v4_port = v4.local_addr().unwrap().port();
let v6 = tokio::net::UdpSocket::bind("[::1]:0").await.unwrap();
let ds = DualStackSocket::new(Some(v4), Some(v6)).unwrap();
let sender = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
sender
.send_to(b"from-v4", format!("127.0.0.1:{v4_port}"))
.unwrap();
let mut buf_data = [0u8; 256];
let mut bufs = [IoSliceMut::new(&mut buf_data)];
let mut meta = [RecvMeta::default()];
let ds_arc = Arc::new(ds);
let result = tokio::time::timeout(
std::time::Duration::from_secs(2),
wait_for_recv(ds_arc.clone(), &mut bufs, &mut meta),
)
.await;
assert!(result.is_ok(), "should receive within timeout");
let n = result.unwrap();
assert_eq!(n, 1);
assert_eq!(&buf_data[..meta[0].len], b"from-v4");
let source = meta[0].addr;
assert!(
source.is_ipv6(),
"source should be IPv6 (mapped), got {source}"
);
if let SocketAddr::V6(v6_addr) = source {
assert!(
v6_addr.ip().to_ipv4_mapped().is_some(),
"should be IPv4-mapped, got {v6_addr}"
);
}
}
async fn wait_for_recv(
socket: Arc<DualStackSocket>,
bufs: &mut [IoSliceMut<'_>],
meta: &mut [RecvMeta],
) -> usize {
std::future::poll_fn(|cx| socket.poll_recv(cx, bufs, meta))
.await
.unwrap()
}
#[tokio::test]
async fn reply_to_v4_mapped_peer_egresses_from_the_v4_socket() {
let v4 = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
let listener_v4_port = v4.local_addr().unwrap().port();
let v6 = std::net::UdpSocket::bind("[::]:0").unwrap();
let listener_v6_port = v6.local_addr().unwrap().port();
assert_ne!(
listener_v4_port, listener_v6_port,
"test requires distinct v4/v6 listener ports to discriminate egress socket"
);
let dual = wrap_dual_stack(Some(v4), Some(v6)).expect("wrap dual-stack");
let peer = std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
peer.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
let peer_port = peer.local_addr().unwrap().port();
peer.send_to(b"dial", (Ipv4Addr::LOCALHOST, listener_v4_port))
.unwrap();
let mut buf = [0u8; 512];
let mut bufs = [IoSliceMut::new(&mut buf)];
let mut meta = [RecvMeta::default()];
let n = tokio::time::timeout(
std::time::Duration::from_secs(5),
std::future::poll_fn(|cx| dual.poll_recv(cx, &mut bufs, &mut meta)),
)
.await
.expect("listener should receive the dial")
.expect("poll_recv should succeed");
assert_eq!(n, 1);
let learned_peer = meta[0].addr;
assert!(
matches!(learned_peer, SocketAddr::V6(a) if a.ip().to_ipv4_mapped().is_some()),
"peer learned on the v4 socket should be reported IPv4-mapped, got {learned_peer}"
);
assert_eq!(learned_peer.port(), peer_port);
let transmit = Transmit {
destination: learned_peer,
ecn: None,
contents: b"reply",
segment_size: None,
src_ip: None,
};
let mut sender = dual.create_sender();
tokio::time::timeout(
std::time::Duration::from_secs(5),
std::future::poll_fn(|cx| sender.as_mut().poll_send(&transmit, cx)),
)
.await
.expect("send should not stall")
.expect("send should succeed");
let mut reply = [0u8; 512];
let (len, src) = peer
.recv_from(&mut reply)
.expect("native-IPv4 peer must receive the reply; a v6 egress black-holes it");
assert_eq!(&reply[..len], b"reply");
assert_eq!(
src,
SocketAddr::from((Ipv4Addr::LOCALHOST, listener_v4_port)),
"reply must egress the v4 socket (port {listener_v4_port}), saw port {} \
(v6 socket is {listener_v6_port}); check select_family's IPv4-mapped arm",
src.port()
);
}
#[tokio::test]
async fn inbound_ipv4_connection_delivers_server_to_client_data() {
use crate::config::{ClientConfig, EndpointConfig, ServerConfig};
use crate::high_level::{Endpoint, TokioRuntime};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let v4 = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
let listener_v4_port = v4.local_addr().unwrap().port();
let v6 = std::net::UdpSocket::bind("[::]:0").unwrap();
let listener_v6_port = v6.local_addr().unwrap().port();
assert_ne!(
listener_v4_port, listener_v6_port,
"test requires distinct v4/v6 listener ports to discriminate egress socket"
);
let _ = listener_v6_port;
let dual: Arc<dyn AsyncUdpSocket> =
Arc::new(wrap_dual_stack(Some(v4), Some(v6)).expect("wrap dual-stack"));
assert!(
dual.local_addr().unwrap().is_ipv6(),
"true dual-stack listener must report an IPv6 local_addr"
);
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()])
.expect("self-signed cert");
let cert_der = CertificateDer::from(cert.cert);
let key_der = PrivateKeyDer::Pkcs8(cert.signing_key.serialize_der().into());
let chain = vec![cert_der];
let server_cfg =
ServerConfig::with_single_cert(chain.clone(), key_der).expect("server config");
let server_ep = Endpoint::new_with_abstract_socket(
EndpointConfig::default(),
Some(server_cfg),
dual,
Arc::new(TokioRuntime),
)
.expect("dual-stack server endpoint");
let server = tokio::spawn(async move {
let incoming = server_ep.accept().await.expect("incoming connection");
let conn = incoming.await.expect("server handshake");
let (mut send, mut recv) = conn.accept_bi().await.expect("server accept_bi");
let got = recv.read_to_end(64).await.expect("server read");
assert_eq!(got, b"ping");
send.write_all(b"pong").await.expect("server write");
send.finish().expect("server finish");
conn.closed().await;
});
let mut roots = rustls::RootCertStore::empty();
for c in chain {
roots.add(c).expect("add server cert to roots");
}
let client_cfg =
ClientConfig::with_root_certificates(Arc::new(roots)).expect("client config");
let mut client_ep =
Endpoint::client((Ipv4Addr::LOCALHOST, 0).into()).expect("client endpoint");
client_ep.set_default_client_config(client_cfg);
let conn = tokio::time::timeout(
std::time::Duration::from_secs(10),
client_ep
.connect((Ipv4Addr::LOCALHOST, listener_v4_port).into(), "localhost")
.expect("start connect"),
)
.await
.expect("handshake must not stall")
.expect("client connected");
let (mut send, mut recv) = conn.open_bi().await.expect("client open_bi");
send.write_all(b"ping").await.expect("client write");
send.finish().expect("client finish");
let echoed = tokio::time::timeout(std::time::Duration::from_secs(10), recv.read_to_end(64))
.await
.expect("server->client data must arrive on an inbound dual-stack connection")
.expect("client read");
assert_eq!(
echoed, b"pong",
"server reply must reach the IPv4 dialer intact"
);
conn.close(0u32.into(), b"done");
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), server).await;
}
#[tokio::test]
async fn dual_stack_peers_exchange_data_over_an_ipv4_dial() {
use crate::config::{ClientConfig, EndpointConfig, ServerConfig};
use crate::high_level::{Endpoint, TokioRuntime};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let make_dual = || {
let v4 = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind v4");
let v4_port = v4.local_addr().unwrap().port();
let v6 = std::net::UdpSocket::bind("[::]:0").expect("bind v6");
let v6_port = v6.local_addr().unwrap().port();
assert_ne!(
v4_port, v6_port,
"test requires distinct v4/v6 ports to discriminate egress socket"
);
let socket: Arc<dyn AsyncUdpSocket> =
Arc::new(wrap_dual_stack(Some(v4), Some(v6)).expect("wrap dual-stack"));
(socket, v4_port)
};
let (server_socket, server_v4_port) = make_dual();
let (client_socket, _client_v4_port) = make_dual();
assert!(server_socket.local_addr().unwrap().is_ipv6());
assert!(client_socket.local_addr().unwrap().is_ipv6());
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()])
.expect("self-signed cert");
let cert_der = CertificateDer::from(cert.cert);
let key_der = PrivateKeyDer::Pkcs8(cert.signing_key.serialize_der().into());
let chain = vec![cert_der];
let server_cfg =
ServerConfig::with_single_cert(chain.clone(), key_der).expect("server config");
let server_ep = Endpoint::new_with_abstract_socket(
EndpointConfig::default(),
Some(server_cfg),
server_socket,
Arc::new(TokioRuntime),
)
.expect("dual-stack server endpoint");
let server = tokio::spawn(async move {
let incoming = server_ep.accept().await.expect("incoming connection");
let conn = incoming.await.expect("server handshake");
let (mut send, mut recv) = conn.accept_bi().await.expect("server accept_bi");
let got = recv.read_to_end(64).await.expect("server read");
assert_eq!(got, b"ping");
send.write_all(b"pong").await.expect("server write");
send.finish().expect("server finish");
conn.closed().await;
});
let mut roots = rustls::RootCertStore::empty();
for c in chain {
roots.add(c).expect("add server cert to roots");
}
let client_cfg =
ClientConfig::with_root_certificates(Arc::new(roots)).expect("client config");
let mut client_ep = Endpoint::new_with_abstract_socket(
EndpointConfig::default(),
None,
client_socket,
Arc::new(TokioRuntime),
)
.expect("dual-stack client endpoint");
client_ep.set_default_client_config(client_cfg);
let conn = tokio::time::timeout(
std::time::Duration::from_secs(10),
client_ep
.connect((Ipv4Addr::LOCALHOST, server_v4_port).into(), "localhost")
.expect("start connect"),
)
.await
.expect("handshake must not stall")
.expect("client connected");
let (mut send, mut recv) = conn.open_bi().await.expect("client open_bi");
send.write_all(b"ping").await.expect("client write");
send.finish().expect("client finish");
let echoed = tokio::time::timeout(std::time::Duration::from_secs(10), recv.read_to_end(64))
.await
.expect("server->client data must arrive between two dual-stack peers")
.expect("client read");
assert_eq!(echoed, b"pong");
conn.close(0u32.into(), b"done");
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), server).await;
}
}