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
- 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>.
- On host A, bootstrap/dial B with that exact multiaddr:
/ip4/192.168.0.105/tcp/9002/p2p/<B-peer-id>
- 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.
Title
Cross-device dial ignores the multiaddr TCP port — dials a random port →
Connection refused, errno=111Environment
dart_libp2p: 1.0.3 (latest on pub.dev; no newer release exists)192.168.0.104/192.168.0.105/ip4/.../tcp/.../p2p/<id>)Summary
When dialing a peer by an explicit, correct multiaddr,
BasicHost.connect/Swarmdials a different (random) TCP port than the one in the multiaddr,so the connection is refused. The multiaddr we supply is correct — a standalone
Multiaddrparse proves it — so the wrong port is introduced inside thelibrary between our
addAddrs/bootstrap and the actualtcp_transport.dial.Reproduction
9002) with a fixed peerid (stable ed25519 seed). It advertises
/ip4/192.168.0.105/tcp/9002/p2p/<B-peer-id>./ip4/192.168.0.105/tcp/9002/p2p/<B-peer-id>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:
So our input carries
tcp: 9002. The library dials41982.Where the wrong port is introduced (source trace)
tcp_transport.dartreadsvalueForProtocol('tcp')from the multiaddr it isgiven; if that were
0it would throwArgumentError. 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.dialPeerreads candidate addresses from_peerstore.addrBook.addrs(peerId).AddressFilter.filterReachableonlyfilters by IP type — it never touches the port.
BasicHost.connectapplies_addrsFactory(pi.addrs)to the peer addrs beforestoring them. The default factory (
defaultAddrsFactory/_defaultAddrsFactoryInternal) only filters loopback / unspecified IPs anddoes 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
addAddrsand thetcp_transport.dialcall. Thesame-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 closinginside the library), soexplicit 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/BasicHostthat produces the dial candidate list. The candidate port shouldequal the port in the bootstrap multiaddr; instead a random/ephemeral port is
substituted.