feat(server): pin wildcard reply source address #85
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
|
- 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
|
options are not Linux's under other names. Build each direction's control
|
||||||
message explicitly rather than echoing a received one back.
|
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
|
- 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.
|
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"))]
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
|
||||||
V4 { address: std::net::Ipv4Addr },
|
V4 { address: std::net::Ipv4Addr },
|
||||||
/// The IPv6 address this request was addressed to, and the interface it
|
/// 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,
|
/// arrived on. The interface is part of the identity of a *scoped* address,
|
||||||
/// so it is carried rather than rediscovered.
|
/// 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"))]
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
|
||||||
V6 {
|
V6 {
|
||||||
address: std::net::Ipv6Addr,
|
address: std::net::Ipv6Addr,
|
||||||
|
|||||||
@@ -209,14 +209,13 @@ fn send_once(
|
|||||||
address,
|
address,
|
||||||
interface_index,
|
interface_index,
|
||||||
} => {
|
} => {
|
||||||
// For a send, `in6_pktinfo` names the source address and the
|
// For a send, `in6_pktinfo` names the source address and, where it
|
||||||
// interface to leave by — the interface being what makes a scoped
|
// is needed, the interface to leave by.
|
||||||
// address unambiguous.
|
|
||||||
let info = libc::in6_pktinfo {
|
let info = libc::in6_pktinfo {
|
||||||
ipi6_addr: libc::in6_addr {
|
ipi6_addr: libc::in6_addr {
|
||||||
s6_addr: address.octets(),
|
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
|
// The destination has to be stated in the same family the control
|
||||||
// message is: an IPv6 source with an `AF_INET` destination is a
|
// message is: an IPv6 source with an `AF_INET` destination is a
|
||||||
@@ -238,6 +237,28 @@ fn send_once(
|
|||||||
Ok(sent)
|
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.
|
/// The same endpoint expressed in IPv6, mapping an IPv4 peer.
|
||||||
///
|
///
|
||||||
/// A datagram sent to an IPv4-mapped destination from an `AF_INET6` socket goes
|
/// A datagram sent to an IPv4-mapped destination from an `AF_INET6` socket goes
|
||||||
@@ -316,3 +337,40 @@ impl ControlBuffer {
|
|||||||
&mut self.0
|
&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