fix(server): pin the reply source, not the route
This commit is contained in:
3 files changed
+72
-6
No files matched your search
@@ -100,6 +100,12 @@ address fails invisibly, as loss.
|
||||
- The receive and send structures are not interchangeable, and the BSD IPv4
|
||||
options are not Linux's under other names. Build each direction's control
|
||||
message explicitly rather than echoing a received one back.
|
||||
- **Pin the source address, not the route.** What the protocol requires is the
|
||||
address a reply leaves from; which interface carries it is the routing table's
|
||||
business. Naming the arrival interface on a send would lose replies on a host
|
||||
whose path back to the peer leaves by another one. The single exception is a
|
||||
link-local source, which is not unique across interfaces and must name its
|
||||
own — every other source, IPv4 included, goes out with no interface set.
|
||||
- A send failure is per-packet loss, exactly as an ordinary `send_to` failure
|
||||
is. It never terminates the listener and never rolls back core state.
|
||||
|
||||
|
||||
@@ -48,8 +48,10 @@ pub(crate) enum ReplySource {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
|
||||
V4 { address: std::net::Ipv4Addr },
|
||||
/// The IPv6 address this request was addressed to, and the interface it
|
||||
/// arrived on. The interface is part of the identity of a scoped address,
|
||||
/// so it is carried rather than rediscovered.
|
||||
/// arrived on. The interface is part of the identity of a *scoped* address,
|
||||
/// so it is carried rather than rediscovered — and it is used on the send
|
||||
/// only where the source address is one, so that a reply is never forced
|
||||
/// out of an interface the route to the peer does not use.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
|
||||
V6 {
|
||||
address: std::net::Ipv6Addr,
|
||||
|
||||
@@ -209,14 +209,13 @@ fn send_once(
|
||||
address,
|
||||
interface_index,
|
||||
} => {
|
||||
// For a send, `in6_pktinfo` names the source address and the
|
||||
// interface to leave by — the interface being what makes a scoped
|
||||
// address unambiguous.
|
||||
// For a send, `in6_pktinfo` names the source address and, where it
|
||||
// is needed, the interface to leave by.
|
||||
let info = libc::in6_pktinfo {
|
||||
ipi6_addr: libc::in6_addr {
|
||||
s6_addr: address.octets(),
|
||||
},
|
||||
ipi6_ifindex: interface_index,
|
||||
ipi6_ifindex: egress_interface_index(address, 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
|
||||
@@ -238,6 +237,28 @@ fn send_once(
|
||||
Ok(sent)
|
||||
}
|
||||
|
||||
/// The interface a reply must leave by, which is usually none of our business.
|
||||
///
|
||||
/// A link-local source address is not unique across a host's interfaces, so a
|
||||
/// reply carrying one has to name the interface it belongs to. Every other
|
||||
/// source — global, unique-local, loopback, IPv4-mapped — identifies itself
|
||||
/// without help.
|
||||
///
|
||||
/// Pinning the arrival interface for those would only add a way for a reply to
|
||||
/// fail: on a host whose route back to the peer leaves by a different interface
|
||||
/// than the request arrived on, forcing the ingress interface can make the send
|
||||
/// fail outright, losing a reply whose source address was perfectly valid. What
|
||||
/// the protocol requires is the source *address*; the route to the peer is the
|
||||
/// routing table's business. The IPv4 path makes the same choice for the same
|
||||
/// reason.
|
||||
fn egress_interface_index(address: Ipv6Addr, interface_index: u32) -> u32 {
|
||||
if address.is_unicast_link_local() {
|
||||
interface_index
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// The same endpoint expressed in IPv6, mapping an IPv4 peer.
|
||||
///
|
||||
/// A datagram sent to an IPv4-mapped destination from an `AF_INET6` socket goes
|
||||
@@ -316,3 +337,40 @@ impl ControlBuffer {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Which reply sources need their interface named, which no normal
|
||||
/// interface can show: it takes a host whose route back to a peer leaves by
|
||||
/// a different interface than the request arrived on, and a test host has
|
||||
/// one loopback.
|
||||
#[test]
|
||||
fn only_a_link_local_source_pins_the_egress_interface() {
|
||||
const ARRIVED_ON: u32 = 7;
|
||||
|
||||
for (address, expected, why) in [
|
||||
(
|
||||
"fe80::1",
|
||||
ARRIVED_ON,
|
||||
"a link-local source needs its link named",
|
||||
),
|
||||
(
|
||||
"fe80::7858:baff:fe91:183d",
|
||||
ARRIVED_ON,
|
||||
"and so does any other one",
|
||||
),
|
||||
("2001:db8::1", 0, "a global source identifies itself"),
|
||||
("fd00::1", 0, "so does a unique-local one"),
|
||||
("::1", 0, "and loopback"),
|
||||
("::ffff:127.0.0.2", 0, "and an IPv4-mapped destination"),
|
||||
] {
|
||||
assert_eq!(
|
||||
egress_interface_index(address.parse().unwrap(), ARRIVED_ON),
|
||||
expected,
|
||||
"{address}: {why}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user