fix(relay): preserve forwarded packets in secure relay paths (#2618)

Forward remote packets without starting a relay session at the
intermediate hop. Encrypting forwarded Noise handshake replies with
that hop's session corrupts the end-to-end handshake and causes
credential connectivity tests to time out.

Disable automatic P2P in the admin/shared/credential test so direct
connections cannot mask broken forwarding. Cover unchanged forwarding
of handshake requests, replies, and encrypted data packets.
This commit is contained in:
KKRainbow authored and GitHub committed 2026-09-29 22:52:10 +08:00
1 parent ff3921ce68
commit 3da349f4ee
2 files changed
+83

No files matched your search

+77
View File
@@ -229,6 +229,15 @@ impl RelayPeerMap {
dst_peer_id: PeerId,
policy: NextHopPolicy,
) -> Result<(), Error> {
// Forwarded packets belong to the original sender's end-to-end session.
// Do not establish another session or encrypt them at this hop.
if msg
.peer_manager_header()
.is_some_and(|hdr| hdr.from_peer_id.get() != self.my_peer_id)
{
return self.send_via_next_hop(msg, dst_peer_id, policy).await;
}
let now = Instant::now();
self.states.entry(dst_peer_id).or_default().last_active_at = now;
@@ -865,6 +874,74 @@ mod tests {
(Arc::new(context), public.as_bytes().to_vec())
}
#[derive(Default)]
struct ForwardedPacketTransport {
packets: StdMutex<Vec<(ZCPacket, PeerId, NextHopPolicy)>>,
}
#[async_trait::async_trait]
impl RelayRouteTransport for ForwardedPacketTransport {
async fn get_route_peer_info(&self, _peer_id: PeerId) -> Option<RoutePeerInfo> {
None
}
async fn send_msg_to_next_hop(
&self,
msg: ZCPacket,
dst_peer_id: PeerId,
policy: NextHopPolicy,
) -> Result<(), Error> {
self.packets
.lock()
.unwrap()
.push((msg, dst_peer_id, policy));
Ok(())
}
}
#[tokio::test]
async fn secure_relay_forwards_remote_packets_without_changing_payload() {
let (context, _) = relay_test_context(2);
let transport = Arc::new(ForwardedPacketTransport::default());
let relay = RelayPeerMap::new(
transport.clone(),
context,
2,
Arc::new(PeerSessionStore::new()),
);
for packet_type in [
PacketType::RelayHandshake,
PacketType::RelayHandshakeAck,
PacketType::Data,
] {
let mut packet = ZCPacket::new_with_payload(b"end-to-end payload");
packet.fill_peer_manager_hdr(1, 3, packet_type as u8);
if matches!(packet_type, PacketType::Data) {
packet
.mut_peer_manager_header()
.unwrap()
.set_encrypted(true);
}
let expected = packet.clone();
relay
.send_msg(packet, 3, NextHopPolicy::LeastCost)
.await
.unwrap();
let mut packets = transport.packets.lock().unwrap();
assert_eq!(
packets.len(),
1,
"forwarded packet was held for a local handshake"
);
let (forwarded, destination, policy) = packets.pop().unwrap();
assert_eq!(forwarded.into_bytes(), expected.into_bytes());
assert_eq!(destination, 3);
assert!(matches!(policy, NextHopPolicy::LeastCost));
}
}
#[tokio::test]
async fn latency_first_handshake_returns_ack_over_least_cost_route() {
let (context_a, pubkey_a) = relay_test_context(1);