Skip to content

mdns: IPv6 peer addresses silently replaced with link-local source on LAN discovery #6474

@ohrensessel

Description

@ohrensessel

Disclaimer

This is AI generated. I stumbled upon the behavior in a small project I was doing and at least wanted to discuss if the proposed fix could be something for a PR. Thanks and all the best.

Summary

On an IPv6-only LAN, mDNS discovery returns peers at their link-local fe80:: address instead of the routable ULA (fd00::/8) or global address they announced. Link-local addresses without a zone ID cannot be dialled over QUIC, so every discovered peer immediately fails to connect.

Root cause

extract_discovered (in iface/query.rs) passes every announced address through _address_translation(address, &observed), where observed is derived from the UDP source address of the mDNS packet:

fn observed_address(&self) -> Multiaddr {
    let obs_ip = Protocol::from(self.remote_addr().ip());
    let obs_port = Protocol::Udp(self.remote_addr().port());
    Multiaddr::empty().with(obs_ip).with(obs_port)
}

On an IPv6 LAN the mDNS multicast source is always the sender's link-local address (fe80::). _address_translation then maps the peer's announced routable address (fd12::1/udp/4001) to the link-local source (fe80::abcd/udp/4001), discarding the announced address entirely.

This translation is correct for NAT traversal (where the "observed" external address genuinely differs from the "announced" private address), but harmful for LAN mDNS where there is no NAT and the announced address is the correct one to dial.

Reproduction

  1. Two hosts on an IPv6-only LAN, each with a routable ULA address (e.g. fd12::1 and fd12::2).
  2. Both announce their ULA address via mDNS.
  3. extract_discovered receives the announcement with source fe80::….
  4. _address_translation replaces fd12::1fe80::… and returns only the link-local address.
  5. Dialling fe80:: without a zone ID fails.

The IPv4 path is unaffected because private IPv4 LAN addresses (e.g. 192.168.x.x) pass through _address_translation differently.

Expected behaviour

The peer's announced address (e.g. /ip6/fd12::1/udp/4001/quic-v1/p2p/12D3Koo…) should be reachable as-is on a LAN — no translation is needed or correct. extract_discovered should yield the original announced address, either instead of or alongside the translation result.

Possible fix direction

Yield the announced address in addition to the translation so that callers receive the routable address the peer actually advertised:

peer.addresses().iter().flat_map(move |address| {
    let original = address.clone().with_p2p(pid).ok();
    let translated = _address_translation(address, &observed)
        .and_then(|a| a.with_p2p(pid).ok());
    [original, translated].into_iter().flatten()
        .map(move |addr| (pid, addr, new_expiration))
})

This is backward-compatible: the translated address is still returned for the NAT case, while the original is preserved for the LAN case.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions