Skip to content

ConnectResponse sent to joiner's private address instead of observed external address #2141

@sanity

Description

@sanity

Summary

When a peer behind NAT joins the network, the gateway correctly observes their external address from the UDP socket, but sends the ConnectResponse to the joiner's self-reported private address (e.g., 127.0.0.1) instead of the observed external address. This causes the join handshake to never complete, leaving the peer without a ring location and unable to participate in DHT operations.

Impact

  • Peers behind NAT cannot complete the join process
  • own_location stays unassigned (-1)
  • All PUT operations are cached locally with warning: "Not forwarding PUT – own ring location not assigned yet"
  • GET/Subscribe operations fail because peer can't route through DHT
  • River and other Freenet applications are completely non-functional for users behind NAT

Root Cause

File: crates/core/src/operations/connect.rs, line 824

When handling a ConnectRequest, the relay correctly updates the joiner's address with the observed external address at line 282:

// Line 282 - CORRECT: Updates joiner address with observed external address
self.request.joiner.peer.addr = joiner_addr;

And uses the updated address when sending ObservedAddress at lines 784-792:

// Lines 784-792 - CORRECT: Uses updated target from actions
if let Some((target, address)) = actions.observed_address {
    let msg = ConnectMsg::ObservedAddress { ... target: target.clone(), ... };
    network_bridge.send(&target.peer, ...).await?;  // Sends to correct address
}

But when sending ConnectResponse, it uses the original from parameter which still has the private address:

// Lines 820-830 - BUG: Uses original 'from' instead of updated joiner
if let Some(response) = actions.accept_response {
    let response_msg = ConnectMsg::Response {
        id: self.id,
        sender: env.self_location().clone(),
        target: from.clone(),  // <-- BUG: from.peer.addr is still 127.0.0.1
        payload: response,
    };

Evidence from Logs

Gateway logs show it trying to send ConnectResponse to 127.0.0.1:43227:

Sending outbound message to peer, tx: 01KAWDVAY8..., 
  msg_type: Message {ConnectResponse { sender: v6MWKgqHiBMNcGtG, target: v6MWKgqKEpQJPprk, ... }}, 
  target_peer: v6MWKgqKEpQJPprk

WARN: No existing outbound connection, establishing connection first, 
  id: 01KAWDVAY8..., target: v6MWKgqKEpQJPprk

NodeEvent::ConnectPeer received, tx: 01KAWDVAY8..., 
  remote: v6MWKgqKEpQJPprk, 
  remote_addr: 127.0.0.1:43227   <-- WRONG ADDRESS

OutboundFailed { transaction: 01KAWDVAY8..., peer: v6MWKgqKEpQJPprk, 
  error: TransportError("failed while establishing connection, reason: max connection attempts reached") }

Meanwhile, ObservedAddress was sent to the correct address and arrived successfully:

# Gateway sends to correct address:
Sending outbound message to peer, tx: 01KAWDVAY8..., 
  msg_type: Message {ObservedAddress { target: v6MWKgqKEpQJPprk (@ 0.409...), address: 136.62.52.28:43227 }}
Message successfully sent to peer connection

# Local peer receives it:
Received inbound message from peer - processing, tx: 01KAWDVAY8..., 
  msg_type: Message {ObservedAddress { target: v6MWKgqKEpQJPprk (@ 0.409...), address: 136.62.52.28:43227 }}

Suggested Fix

Line 824 should use the updated joiner address from actions.observed_address instead of the original from:

if let Some(response) = actions.accept_response {
    // Use updated joiner address if available, otherwise fall back to from
    let response_target = actions.observed_address
        .as_ref()
        .map(|(target, _)| target.clone())
        .unwrap_or_else(|| from.clone());

    let response_msg = ConnectMsg::Response {
        id: self.id,
        sender: env.self_location().clone(),
        target: response_target,  // Uses correct external address
        payload: response,
    };
    return Ok(store_operation_state_with_msg(&mut self, Some(response_msg)));
}

Alternatively, add a response_target field to RelayActions that's always set to the correct joiner address.

Why Tests Don't Catch This

  1. Tests run on localhost where all addresses are 127.0.0.1 - packets to loopback still arrive
  2. The existing test relay_emits_observed_address_for_private_joiner validates that ObservedAddress uses the correct address, but doesn't test ConnectResponse
  3. Test network configurations often pre-set locations via location: Some(...), bypassing the need for the join handshake to complete

Test Case Needed

Add a test that verifies ConnectResponse is sent to the observed external address, not the joiner's self-reported address:

#[test]
fn connect_response_uses_observed_address_not_private() {
    // Setup: joiner with private address 127.0.0.1:5050
    // Gateway observes external address 203.0.113.10:5050
    // Verify: ConnectResponse target has 203.0.113.10:5050, not 127.0.0.1:5050
}

Reproduction Steps

  1. Run a Freenet peer behind NAT (or with a private IP that differs from external)
  2. Configure it to connect to a public gateway
  3. Observe in logs:
    • Peer sends ConnectRequest with private address in joiner field
    • Gateway sends ObservedAddress with correct external address (peer receives this)
    • Gateway sends ConnectResponse to private address (peer never receives this)
    • Peer logs show: "Not forwarding PUT – own ring location not assigned yet"

Related Files

  • crates/core/src/operations/connect.rs - Main bug location (line 824)
  • crates/core/src/operations/connect.rs:277-289 - Where joiner address is correctly updated
  • crates/core/src/node/network_bridge/p2p_protoc.rs:333-335 - Where observed_addr is set from UDP socket

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-networkingArea: Networking, ring protocol, peer discoveryE-mediumExperience needed to fix/implement: Medium / intermediateP-highHigh priorityT-bugType: Something is broken

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions