mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
fix(tun): loop back self-addressed packets for local delivery
A packet destined for our own mesh address reached the TUN reader on macOS (utun egresses self-traffic into the daemon despite the lo0 host route) and was pushed onto the mesh outbound path, where it was dropped for lack of a session/route to self. Hairpin self-addressed packets back to the TUN writer instead, so ping6 and connections to our own <npub>.fips address are delivered locally. On Linux the kernel already loops self-traffic via `lo` before it reaches the TUN, so the branch never fires there; the check is kept unconditional as a daemon-level delivery invariant and to keep it covered by Linux CI.
This commit is contained in:
@@ -307,6 +307,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Packets addressed to a node's own mesh address are now delivered
|
||||
locally instead of being dropped. On macOS the point-to-point `utun`
|
||||
interface egresses self-addressed traffic into the daemon, which
|
||||
previously pushed it onto the mesh outbound path where it was dropped
|
||||
for lack of a route to self; such packets are now hairpinned back to
|
||||
the TUN for inbound delivery, so `ping6` and connections to a node's
|
||||
own `<npub>.fips` address work. Linux was unaffected (the kernel
|
||||
already loops self-traffic via `lo`).
|
||||
- The Tor transport now increments its `connect_refused` statistic (the
|
||||
"Refused" line in fipstop) when a SOCKS5 connection is actively
|
||||
refused, instead of recording every connect failure as a generic
|
||||
|
||||
@@ -704,6 +704,27 @@ fn handle_tun_packet(
|
||||
|
||||
// Check if destination is a FIPS address (fd::/8 prefix)
|
||||
if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
|
||||
// Loopback: a packet to our own mesh address must be delivered
|
||||
// locally, not pushed into the mesh (we have no session/route to
|
||||
// ourselves, so it would just be dropped). Hairpin it back to the TUN
|
||||
// writer for inbound delivery.
|
||||
//
|
||||
// Platform note: in practice this branch is reached only on macOS.
|
||||
// macOS point-to-point `utun` interfaces egress self-addressed traffic
|
||||
// down the tunnel into this reader, so the daemon has to loop it back
|
||||
// itself. On Linux the kernel routes traffic to our own bound
|
||||
// addresses via `lo` before it ever reaches the TUN, so this branch
|
||||
// never fires there. The check is kept unconditional anyway, both as a
|
||||
// platform-independent self-delivery invariant and so the path stays
|
||||
// exercised by the Linux-only CI unit tests.
|
||||
if packet[24..40] == *our_addr.as_bytes() {
|
||||
trace!(name = %name, "Hairpinning self-addressed packet back to TUN (loopback)");
|
||||
if tun_tx.send(packet.to_vec()).is_err() {
|
||||
return false; // Channel closed, shutdown
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Per-destination clamp: if discovery has learned a smaller path
|
||||
// MTU for this destination, tighten the ceiling for this flow.
|
||||
let effective_max_mss = per_flow_max_mss(path_mtu_lookup, &packet[24..40], max_mss);
|
||||
@@ -1520,6 +1541,88 @@ mod tests {
|
||||
assert_eq!(per_flow_max_mss(&lookup, b.as_bytes(), 1360), 1315);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// handle_tun_packet — self-addressed loopback hairpin
|
||||
//
|
||||
// A packet destined for our own mesh address must be delivered back to
|
||||
// the local stack via the TUN writer, never pushed into the mesh (there
|
||||
// is no session/route to ourselves). On macOS the kernel egresses such
|
||||
// self-traffic down the utun into the reader, so the daemon has to loop
|
||||
// it back itself.
|
||||
// ========================================================================
|
||||
|
||||
/// Build a minimal 40-byte IPv6 packet (no upper-layer payload) addressed
|
||||
/// to `dst`, sourced from a distinct fips address.
|
||||
fn ipv6_packet_to(dst: &FipsAddress) -> Vec<u8> {
|
||||
let mut pkt = vec![0u8; 40];
|
||||
pkt[0] = 0x60; // version 6
|
||||
pkt[6] = 59; // next header = No Next Header (skips MSS clamp)
|
||||
pkt[7] = 64; // hop limit
|
||||
pkt[8] = crate::identity::FIPS_ADDRESS_PREFIX; // src in fd::/8
|
||||
pkt[24..40].copy_from_slice(dst.as_bytes()); // dst
|
||||
pkt
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_addressed_packet_is_hairpinned_to_tun() {
|
||||
let our_addr = fips_addr_with_node_byte(0x55);
|
||||
let (tun_tx, tun_rx) = mpsc::channel::<Vec<u8>>();
|
||||
let (outbound_tx, mut outbound_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(4);
|
||||
let lookup = empty_lookup();
|
||||
let mut pkt = ipv6_packet_to(&our_addr);
|
||||
|
||||
assert!(handle_tun_packet(
|
||||
&mut pkt,
|
||||
1360,
|
||||
"test0",
|
||||
our_addr,
|
||||
&tun_tx,
|
||||
&outbound_tx,
|
||||
&lookup,
|
||||
));
|
||||
|
||||
// Delivered locally via the TUN writer...
|
||||
let looped = tun_rx
|
||||
.try_recv()
|
||||
.expect("self-addressed packet should be hairpinned to the TUN");
|
||||
assert_eq!(&looped[24..40], our_addr.as_bytes());
|
||||
// ...and never handed to the mesh.
|
||||
assert!(
|
||||
outbound_rx.try_recv().is_err(),
|
||||
"self-addressed packet must not be pushed into the mesh"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_fips_packet_goes_to_mesh() {
|
||||
let our_addr = fips_addr_with_node_byte(0x55);
|
||||
let peer = fips_addr_with_node_byte(0x66);
|
||||
let (tun_tx, tun_rx) = mpsc::channel::<Vec<u8>>();
|
||||
let (outbound_tx, mut outbound_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(4);
|
||||
let lookup = empty_lookup();
|
||||
let mut pkt = ipv6_packet_to(&peer);
|
||||
|
||||
assert!(handle_tun_packet(
|
||||
&mut pkt,
|
||||
1360,
|
||||
"test0",
|
||||
our_addr,
|
||||
&tun_tx,
|
||||
&outbound_tx,
|
||||
&lookup,
|
||||
));
|
||||
|
||||
// A non-self fips destination is routed into the mesh, not looped back.
|
||||
assert!(
|
||||
outbound_rx.try_recv().is_ok(),
|
||||
"non-self fips destination should be sent to the mesh"
|
||||
);
|
||||
assert!(
|
||||
tun_rx.try_recv().is_err(),
|
||||
"non-self fips destination must not be hairpinned"
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// macOS utun packet-info header (AF_INET6 4-byte big-endian prefix)
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user