Skip to content

Cross-device dial ignores the multiaddr TCP port — dials a random port → Connection refused, errno=111 #14

Description

@cloudabe

Title

Cross-device dial ignores the multiaddr TCP port — dials a random port → Connection refused, errno=111

Environment

  • dart_libp2p: 1.0.3 (latest on pub.dev; no newer release exists)
  • Dart/Flutter: Flutter 3.44 / Dart 3.12
  • Devices: two real Android phones on the same Wi-Fi
    • SM A566B (Android 16), SM N975F (Android 12)
    • LAN IPs 192.168.0.104 / 192.168.0.105
  • Transport: TCP (/ip4/.../tcp/.../p2p/<id>)

Summary

When dialing a peer by an explicit, correct multiaddr, BasicHost.connect /
Swarm dials a different (random) TCP port than the one in the multiaddr,
so the connection is refused. The multiaddr we supply is correct — a standalone
Multiaddr parse proves it — so the wrong port is introduced inside the
library between our addAddrs/bootstrap and the actual tcp_transport.dial.

Reproduction

  1. Start host B listening on a fixed TCP port (e.g. 9002) with a fixed peer
    id (stable ed25519 seed). It advertises
    /ip4/192.168.0.105/tcp/9002/p2p/<B-peer-id>.
  2. On host A, bootstrap/dial B with that exact multiaddr:
    /ip4/192.168.0.105/tcp/9002/p2p/<B-peer-id>
  3. Observe the dial attempt.

Expected: TCP connection to 192.168.0.105:9002.
Actual: connection attempt to 192.168.0.105:41982 (a different,
apparently random port) → Connection refused, errno=111.

Evidence the input is correct

A standalone parse of the exact multiaddr we pass:

final ma = Multiaddr('/ip4/192.168.0.105/tcp/9002/p2p/12D3KooW...');
print(ma.valueForProtocol('tcp')); // -> 9002

So our input carries tcp: 9002. The library dials 41982.

Where the wrong port is introduced (source trace)

  • tcp_transport.dart reads valueForProtocol('tcp') from the multiaddr it is
    given; if that were 0 it would throw ArgumentError. It does not throw,
    meaning the multiaddr reaching the transport already has the wrong (valid) port
    41982. So the corruption happens before the transport.
  • Swarm.dialPeer reads candidate addresses from
    _peerstore.addrBook.addrs(peerId). AddressFilter.filterReachable only
    filters by IP type — it never touches the port.
  • BasicHost.connect applies _addrsFactory(pi.addrs) to the peer addrs before
    storing them. The default factory (defaultAddrsFactory /
    _defaultAddrsFactoryInternal) only filters loopback / unspecified IPs and
    does not rewrite ports — so the factory is not the culprit either.

Net: the port is rewritten somewhere in the swarm/identify/address-book path
between our bootstrap addAddrs and the tcp_transport.dial call. The
same-process (loopback) "mesh" unit test passes only because both hosts resolve
the real port from in-process host state, not the cross-process multiaddr.

Impact

Cross-device / cross-process libp2p mesh is unusable on real networks with
explicit bootstrap multiaddrs. mDNS discovery is also broken on these Android
devices (Bad state: Cannot add event after closing inside the library), so
explicit multiaddr dialing is the only viable path — and it fails for the same
port reason.

Suggested fix area (for maintainer)

Inspect the address-book / Identify observed-addr handling in Swarm /
BasicHost that produces the dial candidate list. The candidate port should
equal the port in the bootstrap multiaddr; instead a random/ephemeral port is
substituted.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions