From 9a9e90a32c21402ca336caf63d12625dccc258b3 Mon Sep 17 00:00:00 2001 From: Arjen <18398758+Origami74@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:10:53 +0200 Subject: [PATCH] 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 .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. --- CHANGELOG.md | 8 ++++ src/upper/tun.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b6137..90def49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `.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 diff --git a/src/upper/tun.rs b/src/upper/tun.rs index e9dddd6..a12fd53 100644 --- a/src/upper/tun.rs +++ b/src/upper/tun.rs @@ -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 { + 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::>(); + let (outbound_tx, mut outbound_rx) = tokio::sync::mpsc::channel::>(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::>(); + let (outbound_tx, mut outbound_rx) = tokio::sync::mpsc::channel::>(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) //