fix(server): mark a mapped listener with the IPv4 option

This commit is contained in:
Nils Andreas Svee committed 2026-08-14 11:49:05 +07:00
1 parent 2d88939ed4
commit aed7eef454
4 files changed
+95 -5

No files matched your search

+4 -2
View File
@@ -12,7 +12,9 @@ use thiserror::Error;
use tokio::{net::UdpSocket, time::MissedTickBehavior};
use crate::{
socket_io, socket_options::set_reply_traffic_class, ServerConfig, ServerCore, ServerError,
socket_io,
socket_options::{marks_with_ipv4_option, set_reply_traffic_class},
ServerConfig, ServerCore, ServerError,
};
/// Fixed transport receive capacity.
@@ -123,7 +125,7 @@ impl Server {
socket,
core: ServerCore::new(config),
recv_buffer: vec![0; RECEIVE_BUFFER_LEN],
listener_is_ipv4: addr.is_ipv4(),
listener_is_ipv4: marks_with_ipv4_option(addr),
select_reply_source,
socket_is_marked: false,
})
+64 -1
View File
@@ -10,10 +10,40 @@
//! there is no raw `setsockopt` or per-packet control message anywhere in the
//! server.
use std::io;
use std::{
io,
net::{IpAddr, SocketAddr},
};
use socket2::SockRef;
/// Whether this listener's replies are marked with the IPv4 TOS option rather
/// than the IPv6 Traffic Class one.
///
/// The listener's own bound family answers this, not the peer's — one socket,
/// one answer. The exception is an IPv4-mapped bound address, where the socket
/// is `AF_INET6` but every packet it emits is IPv4, and the platforms disagree
/// about it:
///
/// - **Linux** binds such a socket to the IPv4 side. `IPV6_TCLASS` does not
/// reach the packets it sends — their TOS stays zero and a negotiated marking
/// is silently lost — while `IP_TOS` on that same socket marks them. So a
/// mapped bind is read as the IPv4 listener it is.
/// - **macOS** rejects `IP_TOS` on an `AF_INET6` socket with `EINVAL` whatever
/// it is bound to, and marks nothing through either option. Reading a mapped
/// bind as IPv4 there would turn an unmarked reply into a *dropped* one, since
/// an unappliable marking stops the send — worse than the marking simply not
/// arriving. It keeps the IPv6 option and its existing behavior. (The mapped
/// *wildcard* never reaches this at all: macOS normalizes that bind to `[::]`.)
/// - **FreeBSD** refuses a mapped bind under its default `IPV6_V6ONLY`, so the
/// case does not arise.
pub(crate) fn marks_with_ipv4_option(addr: SocketAddr) -> bool {
match addr.ip() {
IpAddr::V4(_) => true,
IpAddr::V6(address) => cfg!(target_os = "linux") && address.to_ipv4_mapped().is_some(),
}
}
/// Applies `traffic_class` to a socket bound in the given family.
///
/// `is_ipv4` is the *listener's* family, not the peer's.
@@ -119,3 +149,36 @@ fn unsupported_traffic_class(_traffic_class: u32, option: &'static str) -> io::R
format!("{option} are unsupported on this target"),
))
}
#[cfg(test)]
mod tests {
use super::*;
/// Which listeners mark with the IPv4 option.
///
/// The mapped forms are the point: such a socket is `AF_INET6` but sends
/// IPv4, and `IPV6_TCLASS` does not reach those packets. Linux marks them
/// through `IP_TOS`; the platforms that would reject that keep the IPv6
/// option, because an unappliable marking stops the send and a dropped
/// reply is worse than an unmarked one.
#[test]
fn a_mapped_listener_marks_with_the_ipv4_option_where_that_option_works() {
let mapped = cfg!(target_os = "linux");
for (addr, is_ipv4) in [
("0.0.0.0:2112", true),
("127.0.0.1:2112", true),
("[::ffff:0.0.0.0]:2112", mapped),
("[::ffff:127.0.0.1]:2112", mapped),
("[::]:2112", false),
("[::1]:2112", false),
("[2001:db8::1]:2112", false),
] {
assert_eq!(
marks_with_ipv4_option(addr.parse().unwrap()),
is_ipv4,
"{addr} would mark with the wrong option"
);
}
}
}