mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Split the TUN outbound handler into host-side and mesh-side halves
handle_tun_outbound mixed two concerns: checking a packet from the host and answering it with ICMPv6, and getting the packet into the mesh. Move the host-side half to ipv6tun::outbound. It validates the IPv6 header, makes both Packet Too Big decisions, extracts the destination prefix, and sends the ICMPv6 replies. It reaches the mesh through a small Mesh trait that Node implements: resolve the prefix to a destination, with the session's path MTU when one is established, then send or queue the packet. The mesh-side half, send_tun_packet, stays in the session handler with the pending queue: it sends on an established session, queues behind one being set up, and otherwise admits, initiates or starts discovery. It reports what it did, and a full session table hands the packet back so the host side answers Destination Unreachable as before. The checks run in the same order with the same thresholds, and the send and session-initiation debug lines keep their log target. Node::handle_tun_outbound stays as the entry point, now a call into ipv6tun, so the receive loop and the tests that drive it are unchanged. The Node wrappers for the two ICMPv6 replies had no other caller and are removed.
This commit is contained in:
@@ -10,6 +10,7 @@ pub mod dns;
|
||||
pub mod icmp;
|
||||
pub mod icmp_rate_limit;
|
||||
pub mod ipv6_shim;
|
||||
pub(crate) mod outbound;
|
||||
pub mod tcp_mss;
|
||||
pub mod tun;
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
//! Host-side half of forwarding an IPv6 packet read from the TUN.
|
||||
//!
|
||||
//! This side checks the packet, answers the host with ICMPv6 when the mesh
|
||||
//! cannot take it, and turns the destination address into the 15-byte
|
||||
//! prefix the mesh resolves. What happens to the packet inside the mesh
|
||||
//! (session lookup, queueing while a session is set up, discovery) is the
|
||||
//! mesh's business, reached through the [`Mesh`] trait.
|
||||
//!
|
||||
//! The decisions are the synchronous functions [`admit`] and
|
||||
//! [`path_too_big`]; [`forward`] drives them against a [`Mesh`].
|
||||
|
||||
use super::icmp::{IcmpContext, effective_ipv6_mtu};
|
||||
use std::future::Future;
|
||||
|
||||
/// What the outbound path needs from the mesh side.
|
||||
pub(crate) trait Mesh {
|
||||
/// The mesh's handle on a resolved destination, passed back to `send`.
|
||||
type Dest;
|
||||
|
||||
/// Largest IPv6 packet, header included, the mesh carries on its
|
||||
/// narrowest transport.
|
||||
fn ipv6_mtu(&self) -> u16;
|
||||
|
||||
/// Resolve a destination from bytes 1-15 of its IPv6 address, with the
|
||||
/// session's current path MTU if a session to it is established.
|
||||
fn resolve(&mut self, prefix: &[u8; 15]) -> Option<Route<Self::Dest>>;
|
||||
|
||||
/// Send the packet on the destination's session, or hold it while one
|
||||
/// is set up.
|
||||
fn send(&mut self, dest: Self::Dest, packet: Vec<u8>) -> impl Future<Output = Outcome> + Send;
|
||||
|
||||
/// Borrow the sender of ICMPv6 replies to the host.
|
||||
fn icmp(&mut self) -> IcmpContext<'_>;
|
||||
}
|
||||
|
||||
/// A destination the mesh resolved from an address prefix.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Route<D> {
|
||||
/// The mesh's handle on the destination.
|
||||
pub dest: D,
|
||||
/// Current path MTU of an established session to it, `None` when no
|
||||
/// session is established or it has no path MTU state.
|
||||
pub path_mtu: Option<u16>,
|
||||
}
|
||||
|
||||
/// What the mesh did with a packet handed to [`Mesh::send`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum Outcome {
|
||||
/// Handed to the established session. A send error is logged by the
|
||||
/// mesh and not answered to the host.
|
||||
Sent,
|
||||
/// Held until the destination's session is established.
|
||||
Queued,
|
||||
/// Refused because the session table is full; the packet comes back so
|
||||
/// the host can be told the destination is unreachable.
|
||||
TableFull(Vec<u8>),
|
||||
}
|
||||
|
||||
/// The verdict on a packet read from the TUN, before the mesh is asked.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum Admit {
|
||||
/// Not an IPv6 packet, or shorter than its fixed header.
|
||||
Drop,
|
||||
/// Larger than the mesh carries; answer Packet Too Big with this MTU.
|
||||
TooBig(u32),
|
||||
/// Forward to the destination with this address prefix.
|
||||
Forward([u8; 15]),
|
||||
}
|
||||
|
||||
/// Check a packet from the TUN against the node-wide IPv6 MTU and extract
|
||||
/// its destination prefix.
|
||||
pub(crate) fn admit(packet: &[u8], ipv6_mtu: u16) -> Admit {
|
||||
if packet.len() < 40 || packet[0] >> 4 != 6 {
|
||||
return Admit::Drop;
|
||||
}
|
||||
|
||||
// Check if packet will fit after FIPS encapsulation
|
||||
let effective_mtu = ipv6_mtu as usize;
|
||||
if packet.len() > effective_mtu {
|
||||
return Admit::TooBig(effective_mtu as u32);
|
||||
}
|
||||
|
||||
// Extract destination FipsAddress prefix (IPv6 dest bytes 1-15)
|
||||
// IPv6 header: bytes 24-39 are dest addr, so prefix = bytes 25-39
|
||||
let mut prefix = [0u8; 15];
|
||||
prefix.copy_from_slice(&packet[25..40]);
|
||||
Admit::Forward(prefix)
|
||||
}
|
||||
|
||||
/// The Packet Too Big MTU for a packet of `len` bytes on a session whose
|
||||
/// path MTU is `path_mtu`, or `None` if it fits.
|
||||
///
|
||||
/// Applies only when the path is narrower than the node-wide `ipv6_mtu`,
|
||||
/// which [`admit`] has already enforced.
|
||||
pub(crate) fn path_too_big(len: usize, path_mtu: u16, ipv6_mtu: u16) -> Option<u32> {
|
||||
let path_ipv6_mtu = effective_ipv6_mtu(path_mtu) as usize;
|
||||
if path_ipv6_mtu < ipv6_mtu as usize && len > path_ipv6_mtu {
|
||||
Some(path_ipv6_mtu as u32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward one IPv6 packet read from the TUN into the mesh.
|
||||
///
|
||||
/// Packets that are not IPv6, too large for the node, too large for an
|
||||
/// established session's path, to an unknown destination, or refused for a
|
||||
/// full session table are answered or dropped here. The rest go to
|
||||
/// [`Mesh::send`].
|
||||
pub(crate) async fn forward<M: Mesh>(mesh: &mut M, packet: Vec<u8>) {
|
||||
let ipv6_mtu = mesh.ipv6_mtu();
|
||||
let prefix = match admit(&packet, ipv6_mtu) {
|
||||
Admit::Drop => return,
|
||||
Admit::TooBig(mtu) => {
|
||||
mesh.icmp().packet_too_big(&packet, mtu);
|
||||
return;
|
||||
}
|
||||
Admit::Forward(prefix) => prefix,
|
||||
};
|
||||
|
||||
let Some(route) = mesh.resolve(&prefix) else {
|
||||
mesh.icmp().dest_unreachable(&packet);
|
||||
return;
|
||||
};
|
||||
|
||||
// Check per-destination path MTU learned from MtuExceeded signals.
|
||||
// The first oversized packet is forwarded normally and triggers
|
||||
// the MtuExceeded signal; subsequent packets are caught here and
|
||||
// generate ICMPv6 Packet Too Big back to the application.
|
||||
if let Some(mtu) = route
|
||||
.path_mtu
|
||||
.and_then(|path_mtu| path_too_big(packet.len(), path_mtu, ipv6_mtu))
|
||||
{
|
||||
mesh.icmp().packet_too_big(&packet, mtu);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Outcome::TableFull(packet) = mesh.send(route.dest, packet).await {
|
||||
mesh.icmp().dest_unreachable(&packet);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ipv6tun::icmp_rate_limit::IcmpRateLimiter;
|
||||
use crate::ipv6tun::tun::TunTx;
|
||||
use std::net::Ipv6Addr;
|
||||
use std::sync::mpsc;
|
||||
|
||||
/// A mesh with one known destination prefix and a scripted send outcome.
|
||||
struct FakeMesh {
|
||||
ipv6_mtu: u16,
|
||||
known: [u8; 15],
|
||||
path_mtu: Option<u16>,
|
||||
refuse: bool,
|
||||
sent: Vec<Vec<u8>>,
|
||||
tun_tx: TunTx,
|
||||
limiter: IcmpRateLimiter,
|
||||
}
|
||||
|
||||
impl Mesh for FakeMesh {
|
||||
type Dest = ();
|
||||
|
||||
fn ipv6_mtu(&self) -> u16 {
|
||||
self.ipv6_mtu
|
||||
}
|
||||
|
||||
fn resolve(&mut self, prefix: &[u8; 15]) -> Option<Route<()>> {
|
||||
(*prefix == self.known).then_some(Route {
|
||||
dest: (),
|
||||
path_mtu: self.path_mtu,
|
||||
})
|
||||
}
|
||||
|
||||
fn send(&mut self, _dest: (), packet: Vec<u8>) -> impl Future<Output = Outcome> + Send {
|
||||
let outcome = if self.refuse {
|
||||
Outcome::TableFull(packet)
|
||||
} else {
|
||||
self.sent.push(packet);
|
||||
Outcome::Sent
|
||||
};
|
||||
async move { outcome }
|
||||
}
|
||||
|
||||
fn icmp(&mut self) -> IcmpContext<'_> {
|
||||
IcmpContext::new(
|
||||
Some(&self.tun_tx),
|
||||
"fd00::1".parse().unwrap(),
|
||||
&mut self.limiter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a fake mesh and the receiving end of its TUN channel.
|
||||
fn fake(ipv6_mtu: u16) -> (FakeMesh, mpsc::Receiver<Vec<u8>>) {
|
||||
let (tun_tx, rx) = mpsc::channel();
|
||||
let mesh = FakeMesh {
|
||||
ipv6_mtu,
|
||||
known: dest().octets()[1..16].try_into().unwrap(),
|
||||
path_mtu: None,
|
||||
refuse: false,
|
||||
sent: Vec::new(),
|
||||
tun_tx,
|
||||
limiter: IcmpRateLimiter::new(),
|
||||
};
|
||||
(mesh, rx)
|
||||
}
|
||||
|
||||
/// The destination the fake mesh knows.
|
||||
fn dest() -> Ipv6Addr {
|
||||
"fd12:3456:789a::2".parse().unwrap()
|
||||
}
|
||||
|
||||
/// An IPv6 UDP packet of `len` bytes from a host address to `dst`.
|
||||
fn packet(dst: Ipv6Addr, len: usize) -> Vec<u8> {
|
||||
let mut p = vec![0u8; len];
|
||||
p[0] = 0x60;
|
||||
p[4..6].copy_from_slice(&((len - 40) as u16).to_be_bytes());
|
||||
p[6] = 17;
|
||||
p[7] = 64;
|
||||
let src: Ipv6Addr = "fd00::5".parse().unwrap();
|
||||
p[8..24].copy_from_slice(&src.octets());
|
||||
p[24..40].copy_from_slice(&dst.octets());
|
||||
p
|
||||
}
|
||||
|
||||
/// The ICMPv6 type of a reply written to the TUN.
|
||||
fn icmp_type(reply: &[u8]) -> u8 {
|
||||
reply[40]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admit_drops_short_and_non_ipv6_packets() {
|
||||
assert_eq!(admit(&[0x60; 39], 1280), Admit::Drop);
|
||||
let mut p = packet(dest(), 60);
|
||||
p[0] = 0x45;
|
||||
assert_eq!(admit(&p, 1280), Admit::Drop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admit_reports_the_node_mtu_for_an_oversized_packet() {
|
||||
assert_eq!(admit(&packet(dest(), 1281), 1280), Admit::TooBig(1280));
|
||||
assert!(matches!(
|
||||
admit(&packet(dest(), 1280), 1280),
|
||||
Admit::Forward(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admit_extracts_destination_bytes_one_to_fifteen() {
|
||||
let expected: [u8; 15] = dest().octets()[1..16].try_into().unwrap();
|
||||
assert_eq!(admit(&packet(dest(), 60), 1280), Admit::Forward(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_too_big_applies_only_below_the_node_mtu() {
|
||||
let node = 1280;
|
||||
let narrow = effective_ipv6_mtu(1000);
|
||||
assert_eq!(
|
||||
path_too_big(narrow as usize + 1, 1000, node),
|
||||
Some(narrow as u32)
|
||||
);
|
||||
assert_eq!(path_too_big(narrow as usize, 1000, node), None);
|
||||
// A path at least as wide as the node MTU never answers.
|
||||
assert_eq!(path_too_big(1280, 1280 + 77, node), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forward_answers_unknown_destination_with_unreachable() {
|
||||
let (mut mesh, rx) = fake(1280);
|
||||
forward(&mut mesh, packet("fd99::1".parse().unwrap(), 60)).await;
|
||||
assert!(mesh.sent.is_empty());
|
||||
assert_eq!(icmp_type(&rx.try_recv().unwrap()), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forward_answers_oversized_for_session_path_with_packet_too_big() {
|
||||
let (mut mesh, rx) = fake(1280);
|
||||
mesh.path_mtu = Some(1000);
|
||||
forward(&mut mesh, packet(dest(), 1200)).await;
|
||||
assert!(mesh.sent.is_empty());
|
||||
assert_eq!(icmp_type(&rx.try_recv().unwrap()), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forward_sends_a_fitting_packet_without_a_reply() {
|
||||
let (mut mesh, rx) = fake(1280);
|
||||
mesh.path_mtu = Some(1400);
|
||||
forward(&mut mesh, packet(dest(), 1200)).await;
|
||||
assert_eq!(mesh.sent.len(), 1);
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forward_answers_a_full_session_table_with_unreachable() {
|
||||
let (mut mesh, rx) = fake(1280);
|
||||
mesh.refuse = true;
|
||||
forward(&mut mesh, packet(dest(), 60)).await;
|
||||
assert_eq!(icmp_type(&rx.try_recv().unwrap()), 1);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
use crate::NodeAddr;
|
||||
use crate::ipv6tun::icmp::IcmpContext;
|
||||
use crate::ipv6tun::outbound::{Mesh, Outcome, Route};
|
||||
use crate::node::handlers::mmp::format_throughput;
|
||||
use crate::node::rate_limit::Msg1Class;
|
||||
use crate::node::reject::{RejectReason, SessionReject};
|
||||
@@ -3062,63 +3063,44 @@ impl Node {
|
||||
|
||||
/// Handle an outbound IPv6 packet from the TUN reader.
|
||||
///
|
||||
/// Extracts the destination FipsAddress, looks up the NodeAddr and PublicKey
|
||||
/// from the identity cache, and either sends through an established session
|
||||
/// or initiates a new one (queuing the packet until established).
|
||||
/// The host-side checks and ICMPv6 replies are in
|
||||
/// [`crate::ipv6tun::outbound::forward`], which reaches back into the
|
||||
/// node through its [`Mesh`] impl: the destination prefix is resolved
|
||||
/// in the identity cache, and the packet is sent on an established
|
||||
/// session or queued while one is set up.
|
||||
///
|
||||
/// Also performs MTU checking: if the packet (plus FIPS overhead) exceeds
|
||||
/// the transport MTU, an ICMP Packet Too Big message is sent back to the
|
||||
/// source and the packet is dropped.
|
||||
pub(in crate::node) async fn handle_tun_outbound(&mut self, ipv6_packet: Vec<u8>) {
|
||||
// Validate IPv6 header
|
||||
if ipv6_packet.len() < 40 || ipv6_packet[0] >> 4 != 6 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if packet will fit after FIPS encapsulation
|
||||
let effective_mtu = self.effective_ipv6_mtu() as usize;
|
||||
if ipv6_packet.len() > effective_mtu {
|
||||
self.send_icmpv6_packet_too_big(&ipv6_packet, effective_mtu as u32);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract destination FipsAddress prefix (IPv6 dest bytes 1-15)
|
||||
// IPv6 header: bytes 24-39 are dest addr, so prefix = bytes 25-39
|
||||
let mut prefix = [0u8; 15];
|
||||
prefix.copy_from_slice(&ipv6_packet[25..40]);
|
||||
|
||||
// Look up in identity cache
|
||||
let (dest_addr, dest_pubkey) = match self.lookup_by_fips_prefix(&prefix) {
|
||||
Some((addr, pk)) => (addr, pk),
|
||||
None => {
|
||||
self.send_icmpv6_dest_unreachable(&ipv6_packet);
|
||||
return;
|
||||
}
|
||||
};
|
||||
crate::ipv6tun::outbound::forward(self, ipv6_packet).await;
|
||||
}
|
||||
|
||||
/// Send a TUN packet to a resolved destination, or queue it while the
|
||||
/// destination's session is set up.
|
||||
///
|
||||
/// Sends through an established session, queues behind one still being
|
||||
/// set up, and otherwise initiates a session and queues. With no route
|
||||
/// for the initiation it starts discovery and still queues. Refuses the
|
||||
/// packet, handing it back, when a new session would exceed the session
|
||||
/// table.
|
||||
async fn send_tun_packet(
|
||||
&mut self,
|
||||
dest_addr: NodeAddr,
|
||||
dest_pubkey: PublicKey,
|
||||
ipv6_packet: Vec<u8>,
|
||||
) -> Outcome {
|
||||
// Check for established session
|
||||
if let Some(entry) = self.sessions.get(&dest_addr) {
|
||||
if entry.is_established() {
|
||||
// Check per-destination path MTU learned from MtuExceeded signals.
|
||||
// The first oversized packet is forwarded normally and triggers
|
||||
// the MtuExceeded signal; subsequent packets are caught here and
|
||||
// generate ICMPv6 Packet Too Big back to the application.
|
||||
if let Some(mmp) = entry.mmp() {
|
||||
let path_mtu = mmp.path_mtu.current_mtu();
|
||||
let path_ipv6_mtu = crate::upper::icmp::effective_ipv6_mtu(path_mtu) as usize;
|
||||
if path_ipv6_mtu < effective_mtu && ipv6_packet.len() > path_ipv6_mtu {
|
||||
self.send_icmpv6_packet_too_big(&ipv6_packet, path_ipv6_mtu as u32);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Err(e) = self.send_ipv6_packet(&dest_addr, &ipv6_packet).await {
|
||||
debug!(dest = %self.peer_display_name(&dest_addr), error = %e, "Failed to send TUN packet via session");
|
||||
}
|
||||
return;
|
||||
return Outcome::Sent;
|
||||
}
|
||||
// Session exists but not yet established — queue the packet
|
||||
self.queue_pending_packet(dest_addr, ipv6_packet);
|
||||
return;
|
||||
return Outcome::Queued;
|
||||
}
|
||||
|
||||
// No session, so this one would grow the table. Answer the local
|
||||
@@ -3128,8 +3110,7 @@ impl Node {
|
||||
// queued packet, which is outbound traffic on a node already at its
|
||||
// limit.
|
||||
if !self.admit_new_session(&dest_addr) {
|
||||
self.send_icmpv6_dest_unreachable(&ipv6_packet);
|
||||
return;
|
||||
return Outcome::TableFull(ipv6_packet);
|
||||
}
|
||||
|
||||
// No session: initiate one and queue the packet.
|
||||
@@ -3139,9 +3120,10 @@ impl Node {
|
||||
debug!(dest = %self.peer_display_name(&dest_addr), error = %e, "Failed to initiate session, trying discovery");
|
||||
self.maybe_initiate_lookup(&dest_addr).await;
|
||||
self.queue_pending_packet(dest_addr, ipv6_packet);
|
||||
return;
|
||||
return Outcome::Queued;
|
||||
}
|
||||
self.queue_pending_packet(dest_addr, ipv6_packet);
|
||||
Outcome::Queued
|
||||
}
|
||||
|
||||
/// Borrow the host-facing ICMPv6 sender: the TUN channel, our address
|
||||
@@ -3155,19 +3137,6 @@ impl Node {
|
||||
)
|
||||
}
|
||||
|
||||
/// Send ICMPv6 Destination Unreachable back through TUN.
|
||||
pub(in crate::node) fn send_icmpv6_dest_unreachable(&mut self, original_packet: &[u8]) {
|
||||
self.host_icmp().dest_unreachable(original_packet);
|
||||
}
|
||||
|
||||
/// Send ICMPv6 Packet Too Big back through TUN.
|
||||
///
|
||||
/// Rate-limited per source address to prevent ICMP floods from
|
||||
/// misconfigured applications sending repeated oversized packets.
|
||||
pub(in crate::node) fn send_icmpv6_packet_too_big(&mut self, original_packet: &[u8], mtu: u32) {
|
||||
self.host_icmp().packet_too_big(original_packet, mtu);
|
||||
}
|
||||
|
||||
/// Queue a packet while waiting for session establishment.
|
||||
fn queue_pending_packet(&mut self, dest_addr: NodeAddr, packet: Vec<u8>) {
|
||||
// Reject if we already have too many pending destinations
|
||||
@@ -3235,3 +3204,36 @@ impl Node {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The mesh side of TUN outbound forwarding.
|
||||
impl Mesh for Node {
|
||||
type Dest = (NodeAddr, PublicKey);
|
||||
|
||||
fn ipv6_mtu(&self) -> u16 {
|
||||
self.effective_ipv6_mtu()
|
||||
}
|
||||
|
||||
fn resolve(&mut self, prefix: &[u8; 15]) -> Option<Route<Self::Dest>> {
|
||||
// Look up in identity cache
|
||||
let (dest_addr, dest_pubkey) = self.lookup_by_fips_prefix(prefix)?;
|
||||
let path_mtu = self
|
||||
.sessions
|
||||
.get(&dest_addr)
|
||||
.filter(|entry| entry.is_established())
|
||||
.and_then(|entry| entry.mmp())
|
||||
.map(|mmp| mmp.path_mtu.current_mtu());
|
||||
Some(Route {
|
||||
dest: (dest_addr, dest_pubkey),
|
||||
path_mtu,
|
||||
})
|
||||
}
|
||||
|
||||
fn send(&mut self, dest: Self::Dest, packet: Vec<u8>) -> impl Future<Output = Outcome> + Send {
|
||||
let (dest_addr, dest_pubkey) = dest;
|
||||
self.send_tun_packet(dest_addr, dest_pubkey, packet)
|
||||
}
|
||||
|
||||
fn icmp(&mut self) -> IcmpContext<'_> {
|
||||
self.host_icmp()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user