fix(server): treat a mapped wildcard bind as wildcard
This commit is contained in:
3 files changed
+114
-3
No files matched your search
@@ -1,6 +1,11 @@
|
|||||||
//! Tokio UDP orchestration around [`ServerCore`](crate::ServerCore).
|
//! Tokio UDP orchestration around [`ServerCore`](crate::ServerCore).
|
||||||
|
|
||||||
use std::{future::Future, io, net::SocketAddr, time::Duration};
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
io,
|
||||||
|
net::{IpAddr, SocketAddr},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use socket2::SockRef;
|
use socket2::SockRef;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -104,7 +109,7 @@ impl Server {
|
|||||||
let addr = socket
|
let addr = socket
|
||||||
.local_addr()
|
.local_addr()
|
||||||
.map_err(|source| ServerRuntimeError::LocalAddr { source })?;
|
.map_err(|source| ServerRuntimeError::LocalAddr { source })?;
|
||||||
let select_reply_source = addr.ip().is_unspecified();
|
let select_reply_source = is_wildcard(addr);
|
||||||
|
|
||||||
if select_reply_source {
|
if select_reply_source {
|
||||||
if !socket_io::SUPPORTED {
|
if !socket_io::SUPPORTED {
|
||||||
@@ -243,6 +248,30 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a bound address names no particular local address, and so owes every
|
||||||
|
/// reply an explicitly selected source.
|
||||||
|
///
|
||||||
|
/// `0.0.0.0` and `[::]` are the obvious forms. The third is the IPv4-mapped
|
||||||
|
/// unspecified address, `[::ffff:0.0.0.0]`: Linux accepts it as an IPv4 wildcard
|
||||||
|
/// bind and reports it back verbatim from `getsockname`, so asking
|
||||||
|
/// `is_unspecified` alone would take a working wildcard listener for an explicit
|
||||||
|
/// one and answer its requests from whatever source the routing table picked.
|
||||||
|
/// (macOS normalizes that bind to `[::]` and never reaches the second test.)
|
||||||
|
///
|
||||||
|
/// A mapped address that names an actual host address — `[::ffff:127.0.0.1]` —
|
||||||
|
/// is explicit, and stays on the plain path.
|
||||||
|
fn is_wildcard(addr: SocketAddr) -> bool {
|
||||||
|
match addr.ip() {
|
||||||
|
IpAddr::V4(address) => address.is_unspecified(),
|
||||||
|
IpAddr::V6(address) => {
|
||||||
|
address.is_unspecified()
|
||||||
|
|| address
|
||||||
|
.to_ipv4_mapped()
|
||||||
|
.is_some_and(|mapped| mapped.is_unspecified())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a reply may still be sent after its traffic class could not be
|
/// Whether a reply may still be sent after its traffic class could not be
|
||||||
/// applied.
|
/// applied.
|
||||||
///
|
///
|
||||||
@@ -479,6 +508,32 @@ mod tests {
|
|||||||
ServerConfig::default().with_min_send_interval(Duration::ZERO)
|
ServerConfig::default().with_min_send_interval(Duration::ZERO)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which bound addresses owe their replies a selected source.
|
||||||
|
///
|
||||||
|
/// The IPv4-mapped unspecified address is the one that is not obvious, and
|
||||||
|
/// it is not hypothetical: Linux accepts `[::ffff:0.0.0.0]` as an IPv4
|
||||||
|
/// wildcard bind and reports it back unchanged, so missing it would leave a
|
||||||
|
/// working wildcard listener on the plain send path. A mapped address
|
||||||
|
/// naming a real host address is explicit and must stay there.
|
||||||
|
#[test]
|
||||||
|
fn a_bound_address_naming_no_particular_local_address_is_a_wildcard() {
|
||||||
|
for (addr, wildcard) in [
|
||||||
|
("0.0.0.0:2112", true),
|
||||||
|
("[::]:2112", true),
|
||||||
|
("[::ffff:0.0.0.0]:2112", true),
|
||||||
|
("127.0.0.1:2112", false),
|
||||||
|
("192.0.2.10:2112", false),
|
||||||
|
("[::1]:2112", false),
|
||||||
|
("[::ffff:127.0.0.1]:2112", false),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
is_wildcard(addr.parse().unwrap()),
|
||||||
|
wildcard,
|
||||||
|
"{addr} classified wrongly"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The rule that decides what an unappliable traffic class means, which no
|
/// The rule that decides what an unappliable traffic class means, which no
|
||||||
/// normal interface can reach: it needs a host that refuses the socket
|
/// normal interface can reach: it needs a host that refuses the socket
|
||||||
/// option, and the hosts these tests run on do not.
|
/// option, and the hosts these tests run on do not.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, IoSlice, IoSliceMut},
|
io::{self, IoSlice, IoSliceMut},
|
||||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6},
|
||||||
os::fd::AsRawFd,
|
os::fd::AsRawFd,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -218,6 +218,13 @@ fn send_once(
|
|||||||
},
|
},
|
||||||
ipi6_ifindex: interface_index,
|
ipi6_ifindex: interface_index,
|
||||||
};
|
};
|
||||||
|
// The destination has to be stated in the same family the control
|
||||||
|
// message is: an IPv6 source with an `AF_INET` destination is a
|
||||||
|
// rejected send, not a mixed-family one. The two can disagree —
|
||||||
|
// macOS reports an IPv4 peer for a listener bound to the IPv4-mapped
|
||||||
|
// wildcard while still delivering IPv6 packet info for it — and the
|
||||||
|
// peer identity the core sees stays exactly as it was received.
|
||||||
|
let destination = SockaddrStorage::from(as_ipv6_endpoint(peer));
|
||||||
sendmsg(
|
sendmsg(
|
||||||
socket.as_raw_fd(),
|
socket.as_raw_fd(),
|
||||||
&iov,
|
&iov,
|
||||||
@@ -231,6 +238,18 @@ fn send_once(
|
|||||||
Ok(sent)
|
Ok(sent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same endpoint expressed in IPv6, mapping an IPv4 peer.
|
||||||
|
///
|
||||||
|
/// A datagram sent to an IPv4-mapped destination from an `AF_INET6` socket goes
|
||||||
|
/// out as ordinary IPv4 to that address; this changes how the endpoint is
|
||||||
|
/// spelled for one `sendmsg`, not what reaches the client.
|
||||||
|
fn as_ipv6_endpoint(peer: SocketAddr) -> SocketAddrV6 {
|
||||||
|
match peer {
|
||||||
|
SocketAddr::V6(peer) => peer,
|
||||||
|
SocketAddr::V4(peer) => SocketAddrV6::new(peer.ip().to_ipv6_mapped(), peer.port(), 0, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `IP_PKTINFO` on a send reads only `ipi_spec_dst`, the source address, and
|
/// `IP_PKTINFO` on a send reads only `ipi_spec_dst`, the source address, and
|
||||||
/// `ipi_ifindex`. `ipi_addr` is a receive-only field and is left zero.
|
/// `ipi_ifindex`. `ipi_addr` is a receive-only field and is left zero.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -71,6 +71,43 @@ async fn a_wildcard_ipv4_listener_replies_from_the_requested_address() {
|
|||||||
.expect("the wildcard IPv4 source-selection test exceeded its bounded runtime");
|
.expect("the wildcard IPv4 source-selection test exceeded its bounded runtime");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn an_ipv4_mapped_wildcard_listener_replies_from_the_requested_address() {
|
||||||
|
// `[::ffff:0.0.0.0]` is a wildcard that does not look like one. Linux
|
||||||
|
// accepts it as an IPv4 wildcard bind and `getsockname` reports it back
|
||||||
|
// unchanged, so a listener that only recognized `0.0.0.0` and `[::]` would
|
||||||
|
// take this for an explicit address and answer from a routing-selected
|
||||||
|
// source. macOS normalizes the bind to `[::]`, where this is simply the
|
||||||
|
// IPv6 wildcard case again.
|
||||||
|
timeout(Duration::from_secs(5), async {
|
||||||
|
let server = match Server::bind("[::ffff:0.0.0.0]:0".parse().unwrap(), unthrottled()).await
|
||||||
|
{
|
||||||
|
Ok(server) => server,
|
||||||
|
// FreeBSD refuses a mapped bind under its default `IPV6_V6ONLY`.
|
||||||
|
Err(ServerRuntimeError::Bind { source, .. }) if is_family_unavailable(&source) => {
|
||||||
|
eprintln!("skipping IPv4-mapped wildcard test: the bind is unavailable here");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(error) => panic!("unexpected IPv4-mapped wildcard bind failure: {error}"),
|
||||||
|
};
|
||||||
|
let port = server.local_addr().unwrap().port();
|
||||||
|
let target = SocketAddr::from((secondary_ipv4_loopback(), port));
|
||||||
|
let running = spawn(server);
|
||||||
|
|
||||||
|
let mut client = connected_client(target).await;
|
||||||
|
assert_started(&mut client, target).await;
|
||||||
|
echo_once(&mut client).await;
|
||||||
|
assert!(matches!(
|
||||||
|
client.close().await.unwrap().as_slice(),
|
||||||
|
[ClientEvent::SessionClosed { .. }]
|
||||||
|
));
|
||||||
|
|
||||||
|
running.stop().await;
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("the IPv4-mapped wildcard test exceeded its bounded runtime");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn a_wildcard_ipv6_listener_replies_from_the_requested_address() {
|
async fn a_wildcard_ipv6_listener_replies_from_the_requested_address() {
|
||||||
timeout(Duration::from_secs(5), async {
|
timeout(Duration::from_secs(5), async {
|
||||||
|
|||||||
Reference in new issue
Block a user