diff --git a/pkg/bzz/address.go b/pkg/bzz/address.go index 1a4021e9eff..8c96094a0b7 100644 --- a/pkg/bzz/address.go +++ b/pkg/bzz/address.go @@ -83,10 +83,9 @@ func ParseAddress(underlay, overlay, signature, nonce []byte, validateOverlay bo return nil, fmt.Errorf("deserialize underlays: %w: %w", ErrInvalidAddress, err) } - if len(multiUnderlays) == 0 { - // no underlays sent - return nil, ErrInvalidAddress - } + // Empty underlays are allowed for inbound-only peers (e.g., browsers, WebRTC, strict NAT) + // that cannot be dialed back. These peers can still use protocols over existing connections + // but won't participate in Kademlia topology or hive gossip. ethAddress, err := crypto.NewEthereumAddress(*recoveredPK) if err != nil { @@ -144,10 +143,7 @@ func AreUnderlaysEqual(a, b []ma.Multiaddr) bool { } func (a *Address) MarshalJSON() ([]byte, error) { - if len(a.Underlays) == 0 { - return nil, fmt.Errorf("no underlays for %s", a.Overlay) - } - + // Empty underlays are allowed for inbound-only peers that cannot be dialed back // select the underlay address for backward compatibility var underlay string if v := SelectBestAdvertisedAddress(a.Underlays, nil); v != nil { diff --git a/pkg/bzz/address_test.go b/pkg/bzz/address_test.go index 0c50b08de64..c8ee1aeb342 100644 --- a/pkg/bzz/address_test.go +++ b/pkg/bzz/address_test.go @@ -5,6 +5,7 @@ package bzz_test import ( + "bytes" "testing" "github.com/ethereum/go-ethereum/common" @@ -301,3 +302,252 @@ func TestAreUnderlaysEqual(t *testing.T) { }) } } + +func TestParseAddress(t *testing.T) { + t.Parallel() + + const networkID uint64 = 10 + nonce := common.HexToHash("0x5").Bytes() + + // Generate test key pair and overlay address + privateKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + t.Fatal(err) + } + + overlay, err := crypto.NewOverlayAddress(privateKey.PublicKey, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + signer := crypto.NewDefaultSigner(privateKey) + + t.Run("single underlay - valid address", func(t *testing.T) { + t.Parallel() + + underlay := mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634") + + addr, err := bzz.NewAddress(signer, []multiaddr.Multiaddr{underlay}, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + // Parse the address with overlay validation + parsed, err := bzz.ParseAddress(addr.Underlays[0].Bytes(), overlay.Bytes(), addr.Signature, nonce, true, networkID) + if err != nil { + t.Fatalf("ParseAddress failed: %v", err) + } + + if !parsed.Equal(addr) { + t.Errorf("parsed address not equal to original: got %v, want %v", parsed, addr) + } + + if !parsed.Overlay.Equal(overlay) { + t.Errorf("overlay mismatch: got %v, want %v", parsed.Overlay, overlay) + } + + if len(parsed.Underlays) != 1 { + t.Errorf("expected 1 underlay, got %d", len(parsed.Underlays)) + } + }) + + t.Run("multiple underlays - valid address", func(t *testing.T) { + t.Parallel() + + underlays := []multiaddr.Multiaddr{ + mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634"), + mustNewMultiaddr(t, "/ip4/192.168.1.100/tcp/1634"), + mustNewMultiaddr(t, "/ip6/::1/tcp/1634"), + } + + addr, err := bzz.NewAddress(signer, underlays, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + serialized := bzz.SerializeUnderlays(underlays) + parsed, err := bzz.ParseAddress(serialized, overlay.Bytes(), addr.Signature, nonce, true, networkID) + if err != nil { + t.Fatalf("ParseAddress failed: %v", err) + } + + if !parsed.Overlay.Equal(overlay) { + t.Errorf("overlay mismatch: got %v, want %v", parsed.Overlay, overlay) + } + + if len(parsed.Underlays) != 3 { + t.Errorf("expected 3 underlays, got %d", len(parsed.Underlays)) + } + + if !bzz.AreUnderlaysEqual(parsed.Underlays, underlays) { + t.Errorf("underlays not equal: got %v, want %v", parsed.Underlays, underlays) + } + }) + + t.Run("empty underlays - inbound-only peer", func(t *testing.T) { + t.Parallel() + + // Create address with empty underlays (for inbound-only peers like browsers) + emptyUnderlays := []multiaddr.Multiaddr{} + addr, err := bzz.NewAddress(signer, emptyUnderlays, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + serialized := bzz.SerializeUnderlays(emptyUnderlays) + parsed, err := bzz.ParseAddress(serialized, overlay.Bytes(), addr.Signature, nonce, true, networkID) + if err != nil { + t.Fatalf("ParseAddress failed for empty underlays: %v", err) + } + + if !parsed.Overlay.Equal(overlay) { + t.Errorf("overlay mismatch: got %v, want %v", parsed.Overlay, overlay) + } + + if len(parsed.Underlays) != 0 { + t.Errorf("expected 0 underlays for inbound-only peer, got %d", len(parsed.Underlays)) + } + }) + + t.Run("without overlay validation", func(t *testing.T) { + t.Parallel() + + underlay := mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634") + + addr, err := bzz.NewAddress(signer, []multiaddr.Multiaddr{underlay}, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + // Parse without overlay validation + parsed, err := bzz.ParseAddress(addr.Underlays[0].Bytes(), overlay.Bytes(), addr.Signature, nonce, false, networkID) + if err != nil { + t.Fatalf("ParseAddress failed: %v", err) + } + + if !parsed.Overlay.Equal(overlay) { + t.Errorf("overlay mismatch: got %v, want %v", parsed.Overlay, overlay) + } + }) + + t.Run("invalid signature", func(t *testing.T) { + t.Parallel() + + underlay := mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634") + invalidSignature := make([]byte, 65) // All zeros - invalid signature + + _, err := bzz.ParseAddress(underlay.Bytes(), overlay.Bytes(), invalidSignature, nonce, true, networkID) + if err == nil { + t.Error("expected error for invalid signature, got nil") + } + }) + + t.Run("tampered signature", func(t *testing.T) { + t.Parallel() + + underlay := mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634") + + addr, err := bzz.NewAddress(signer, []multiaddr.Multiaddr{underlay}, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + // Tamper with signature + tamperedSig := make([]byte, len(addr.Signature)) + copy(tamperedSig, addr.Signature) + tamperedSig[0] ^= 0xFF // Flip bits + + _, err = bzz.ParseAddress(addr.Underlays[0].Bytes(), overlay.Bytes(), tamperedSig, nonce, true, networkID) + if err == nil { + t.Error("expected error for tampered signature, got nil") + } + }) + + t.Run("mismatched overlay with validation", func(t *testing.T) { + t.Parallel() + + underlay := mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634") + + addr, err := bzz.NewAddress(signer, []multiaddr.Multiaddr{underlay}, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + // Use a different overlay + wrongOverlay := make([]byte, len(overlay.Bytes())) + copy(wrongOverlay, overlay.Bytes()) + wrongOverlay[0] ^= 0xFF // Flip bits + + _, err = bzz.ParseAddress(addr.Underlays[0].Bytes(), wrongOverlay, addr.Signature, nonce, true, networkID) + if err == nil { + t.Error("expected error for mismatched overlay, got nil") + } + }) + + t.Run("wrong network ID", func(t *testing.T) { + t.Parallel() + + underlay := mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634") + + addr, err := bzz.NewAddress(signer, []multiaddr.Multiaddr{underlay}, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + // Try to parse with different network ID + wrongNetworkID := networkID + 1 + _, err = bzz.ParseAddress(addr.Underlays[0].Bytes(), overlay.Bytes(), addr.Signature, nonce, true, wrongNetworkID) + if err == nil { + t.Error("expected error for wrong network ID, got nil") + } + }) + + t.Run("invalid underlay bytes", func(t *testing.T) { + t.Parallel() + + invalidUnderlay := []byte{0xFF, 0xFF, 0xFF} // Invalid multiaddr bytes + + // We need a valid signature for this test, so create one with valid data first + underlay := mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634") + addr, err := bzz.NewAddress(signer, []multiaddr.Multiaddr{underlay}, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + // Now try to parse with invalid underlay bytes + _, err = bzz.ParseAddress(invalidUnderlay, overlay.Bytes(), addr.Signature, nonce, false, networkID) + if err == nil { + t.Error("expected error for invalid underlay bytes, got nil") + } + }) + + t.Run("ethereum address extraction", func(t *testing.T) { + t.Parallel() + + underlay := mustNewMultiaddr(t, "/ip4/127.0.0.1/tcp/1634") + + addr, err := bzz.NewAddress(signer, []multiaddr.Multiaddr{underlay}, overlay, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + parsed, err := bzz.ParseAddress(addr.Underlays[0].Bytes(), overlay.Bytes(), addr.Signature, nonce, true, networkID) + if err != nil { + t.Fatalf("ParseAddress failed: %v", err) + } + + if len(parsed.EthereumAddress) == 0 { + t.Error("ethereum address not extracted") + } + + // Verify ethereum address matches the one from the private key + expectedEthAddr, err := crypto.NewEthereumAddress(privateKey.PublicKey) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(parsed.EthereumAddress, expectedEthAddr) { + t.Errorf("ethereum address mismatch: got %x, want %x", parsed.EthereumAddress, expectedEthAddr) + } + }) +} diff --git a/pkg/p2p/libp2p/connections_test.go b/pkg/p2p/libp2p/connections_test.go index 27ba4b7549d..481469e81fa 100644 --- a/pkg/p2p/libp2p/connections_test.go +++ b/pkg/p2p/libp2p/connections_test.go @@ -1394,6 +1394,21 @@ func checkAddressbook(t *testing.T, ab addressbook.Getter, overlay swarm.Address } } +// containsAtLeastOne checks if stored contains at least one address from advertised +func containsAtLeastOne(stored, advertised []ma.Multiaddr) bool { + if len(stored) == 0 { + return false + } + for _, s := range stored { + for _, a := range advertised { + if s.Equal(a) { + return true + } + } + } + return false +} + type notifiee struct { connected cFunc disconnected dFunc @@ -1477,6 +1492,155 @@ type ( reachableFunc func(swarm.Address, p2p.ReachabilityStatus) ) +// TestAddressbookPersistence verifies addressbook persistence behavior based on underlay presence +func TestAddressbookPersistence(t *testing.T) { + t.Parallel() + + t.Run("with underlays - persisted", func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + // Create addressbooks for both services + ab1, ab2 := addressbook.New(mock.NewStateStore()), addressbook.New(mock.NewStateStore()) + + // s1: Full node with underlays (dialable) + s1, overlay1 := newService(t, 1, libp2pServiceOpts{ + Addressbook: ab1, + libp2pOpts: libp2p.Options{ + FullNode: true, + }, + }) + + // s2: Full node with underlays (dialable) + s2, overlay2 := newService(t, 1, libp2pServiceOpts{ + Addressbook: ab2, + libp2pOpts: libp2p.Options{ + FullNode: true, + }, + }) + + s1Addrs := serviceUnderlayAddress(t, s1) + + bzzAddr, err := s2.Connect(ctx, s1Addrs) + if err != nil { + t.Fatal(err) + } + + // Verify connection established in both directions + expectPeers(t, s2, overlay1) + expectPeersEventually(t, s1, overlay2) + + // Verify both peers are persisted in addressbooks with underlays + addr1, err := ab2.Get(overlay1) + if err != nil { + t.Fatal(err) + } + if !containsAtLeastOne(addr1.Underlays, s1Addrs) { + t.Fatalf("expected at least one of s1's addresses %v in addressbook, got %v", s1Addrs, addr1.Underlays) + } + + s2Addrs := serviceUnderlayAddress(t, s2) + addr2, err := ab1.Get(overlay2) + if err != nil { + t.Fatal(err) + } + if !containsAtLeastOne(addr2.Underlays, s2Addrs) { + t.Fatalf("expected at least one of s2's addresses %v in addressbook, got %v", s2Addrs, addr2.Underlays) + } + + if err := s2.Disconnect(bzzAddr.Overlay, testDisconnectMsg); err != nil { + t.Fatal(err) + } + + expectPeers(t, s2) + expectPeersEventually(t, s1) + + // Verify addressbook entries persist after disconnect + addr1, err = ab2.Get(overlay1) + if err != nil { + t.Fatal(err) + } + if !containsAtLeastOne(addr1.Underlays, s1Addrs) { + t.Fatal("expected s1's addresses to persist in addressbook after disconnect") + } + + addr2, err = ab1.Get(overlay2) + if err != nil { + t.Fatal(err) + } + if !containsAtLeastOne(addr2.Underlays, s2Addrs) { + t.Fatal("expected s2's addresses to persist in addressbook after disconnect") + } + }) + + t.Run("with empty underlays - not persisted", func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + ab1 := addressbook.New(mock.NewStateStore()) + ab2 := addressbook.New(mock.NewStateStore()) + + s1, overlay1 := newService(t, 1, libp2pServiceOpts{ + Addressbook: ab1, + libp2pOpts: libp2p.Options{ + FullNode: true, + }, + }) + + s2, overlay2 := newService(t, 1, libp2pServiceOpts{ + Addressbook: ab2, + emptyUnderlays: true, + libp2pOpts: libp2p.Options{ + FullNode: true, + }, + }) + + s1Addrs := serviceUnderlayAddress(t, s1) + + s2Addrs := serviceUnderlayAddress(t, s2) + if len(s2Addrs) != 0 { + t.Fatalf("expected s2 to have 0 underlay addresses, got %d", len(s2Addrs)) + } + + _, err := s2.Connect(ctx, s1Addrs) + if err != nil { + t.Fatal(err) + } + + expectPeers(t, s2, overlay1) + expectPeersEventually(t, s1, overlay2) + + // Verify s1 (has underlays) is persisted in s2's addressbook + var addr *bzz.Address + err = spinlock.Wait(time.Second, func() bool { + var getErr error + addr, getErr = ab2.Get(overlay1) + return getErr == nil && containsAtLeastOne(addr.Underlays, s1Addrs) + }) + if err != nil { + addr, _ = ab2.Get(overlay1) + if addr == nil { + t.Fatal("s1 not found in s2's addressbook") + } + t.Fatalf("expected at least one of s1's addresses %v in addressbook, got %v", s1Addrs, addr.Underlays) + } + + // Wait to ensure any incorrect persistence would have occurred + _ = spinlock.Wait(500*time.Millisecond, func() bool { + _, _ = ab1.Get(overlay2) + return false + }) + + // Verify s2 (empty underlays) is NOT persisted in s1's addressbook + addr2, err := ab1.Get(overlay2) + if err == nil { + t.Fatalf("expected s2 (inbound-only peer with empty underlays) not to be persisted in s1's addressbook, but got: %+v with %d underlays", addr2, len(addr2.Underlays)) + } + }) +} + var ( noopCf = func(context.Context, p2p.Peer, bool) error { return nil } noopDf = func(p2p.Peer) {} diff --git a/pkg/p2p/libp2p/export_test.go b/pkg/p2p/libp2p/export_test.go index c85a85b54ae..ce398600088 100644 --- a/pkg/p2p/libp2p/export_test.go +++ b/pkg/p2p/libp2p/export_test.go @@ -53,6 +53,10 @@ func SetAutoTLSCertManager(o *Options, m autoTLSCertManager) { o.autoTLSCertManager = m } +func SetHostFactory(o *Options, factory func(...libp2pm.Option) (host.Host, error)) { + o.hostFactory = factory +} + type AutoTLSCertManager = autoTLSCertManager var NewCompositeAddressResolver = newCompositeAddressResolver diff --git a/pkg/p2p/libp2p/internal/handshake/handshake_test.go b/pkg/p2p/libp2p/internal/handshake/handshake_test.go index d455e6e8520..f5ff7b6973d 100644 --- a/pkg/p2p/libp2p/internal/handshake/handshake_test.go +++ b/pkg/p2p/libp2p/internal/handshake/handshake_test.go @@ -725,3 +725,147 @@ func (a *AdvertisableAddresserMock) Resolve(observedAddress ma.Multiaddr) (ma.Mu return observedAddress, nil } + +// TestHandshakeWithEmptyUnderlays verifies that the handshake can successfully +// parse a peer address with empty underlays (e.g., inbound-only peers like +// browsers or peers behind strict NAT that cannot be dialed back). +func TestHandshakeWithEmptyUnderlays(t *testing.T) { + t.Parallel() + + logger := log.Noop + networkID := uint64(3) + + // Node 1 has normal underlay addresses + node1ma, err := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/1634/p2p/16Uiu2HAkx8ULY8cTXhdVAcMmLcH9AsTKz6uBQ7DPLKRjMLgBVYkA") + if err != nil { + t.Fatal(err) + } + + node1AddrInfo, err := libp2ppeer.AddrInfoFromP2pAddr(node1ma) + if err != nil { + t.Fatal(err) + } + + // Create private keys and signers + privateKey1, err := crypto.GenerateSecp256k1Key() + if err != nil { + t.Fatal(err) + } + privateKey2, err := crypto.GenerateSecp256k1Key() + if err != nil { + t.Fatal(err) + } + + nonce := common.HexToHash("0x1").Bytes() + + signer1 := crypto.NewDefaultSigner(privateKey1) + signer2 := crypto.NewDefaultSigner(privateKey2) + + // Create overlays + overlay1, err := crypto.NewOverlayAddress(privateKey1.PublicKey, networkID, nonce) + if err != nil { + t.Fatal(err) + } + overlay2, err := crypto.NewOverlayAddress(privateKey2.PublicKey, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + // Node 2: Inbound-only peer with EMPTY underlays (simulates browser/WebRTC peer) + node2BzzAddress, err := bzz.NewAddress(signer2, []ma.Multiaddr{}, overlay2, networkID, nonce) + if err != nil { + t.Fatal(err) + } + + // Verify node2 has empty underlays + if len(node2BzzAddress.Underlays) != 0 { + t.Fatalf("expected node2 to have 0 underlays, got %d", len(node2BzzAddress.Underlays)) + } + + // Create handshake service for node1 + handshakeService1, err := handshake.New( + signer1, + &AdvertisableAddresserMock{}, + overlay1, + networkID, + true, + nonce, + nil, + "node1", + true, + node1AddrInfo.ID, + logger, + ) + if err != nil { + t.Fatal(err) + } + + // Create mock stream following the existing test pattern + var buffer1, buffer2 bytes.Buffer + stream1 := mock.NewStream(&buffer1, &buffer2) + stream2 := mock.NewStream(&buffer2, &buffer1) + defer stream1.Close() + defer stream2.Close() + + // Serialize empty underlays + emptyUnderlaysBinary := bzz.SerializeUnderlays([]ma.Multiaddr{}) + + // Pre-write the SynAck message with empty underlays (like the existing "Handshake - OK" test) + w, r := protobuf.NewWriterAndReader(stream2) + if err := w.WriteMsg(&pb.SynAck{ + Syn: &pb.Syn{ + ObservedUnderlay: bzz.SerializeUnderlays([]ma.Multiaddr{node1ma}), + }, + Ack: &pb.Ack{ + Address: &pb.BzzAddress{ + Underlay: emptyUnderlaysBinary, // Node 2 advertises EMPTY underlays + Overlay: node2BzzAddress.Overlay.Bytes(), + Signature: node2BzzAddress.Signature, + }, + NetworkID: networkID, + FullNode: true, + Nonce: nonce, + WelcomeMessage: "inbound-only-peer", + }, + }); err != nil { + t.Fatal(err) + } + + // Node 1 initiates handshake and receives node2's empty underlays + info1, err := handshakeService1.Handshake( + context.Background(), + stream1, + []ma.Multiaddr{}, // Pass empty underlays as we don't know node2's address + ) + + if err != nil { + t.Fatalf("handshake failed: %v", err) + } + + // Read the Syn that node1 sent + var syn pb.Syn + if err := r.ReadMsg(&syn); err != nil { + t.Fatal(err) + } + + // Read the Ack that node1 sent back + var ack pb.Ack + if err := r.ReadMsg(&ack); err != nil { + t.Fatal(err) + } + + // Verify node1 successfully received node2's address with EMPTY underlays + if len(info1.BzzAddress.Underlays) != 0 { + t.Errorf("expected to receive 0 underlays, got %d", len(info1.BzzAddress.Underlays)) + } + + // Verify overlay address matches + if !info1.BzzAddress.Overlay.Equal(overlay2) { + t.Errorf("received wrong overlay: got %v, want %v", info1.BzzAddress.Overlay, overlay2) + } + + // Node 2 is a full node + if !info1.FullNode { + t.Error("expected peer to be a full node") + } +} diff --git a/pkg/p2p/libp2p/libp2p.go b/pkg/p2p/libp2p/libp2p.go index 367490d9dc0..268cf7e1ed6 100644 --- a/pkg/p2p/libp2p/libp2p.go +++ b/pkg/p2p/libp2p/libp2p.go @@ -706,7 +706,9 @@ func (s *Service) handleIncoming(stream network.Stream) { return } - if i.FullNode { + // Only persist peers with underlays to addressbook. Inbound-only peers (empty underlays) + // cannot be dialed back, so there's no point in storing them for reconnection. + if i.FullNode && len(i.BzzAddress.Underlays) > 0 { err = s.addressbook.Put(i.BzzAddress.Overlay, *i.BzzAddress) if err != nil { s.logger.Debug("stream handler: addressbook put error", "peer_id", peerID, "error", err) @@ -806,6 +808,11 @@ func (s *Service) notifyReacherConnected(overlay swarm.Address, peerID libp2ppee peerAddrs := s.host.Peerstore().Addrs(peerID) bestAddr := bzz.SelectBestAdvertisedAddress(peerAddrs, nil) + if bestAddr == nil { + s.logger.Debug("skipping reacher notification for inbound-only peer", "peer_id", peerID, "overlay", overlay) + return + } + s.logger.Debug("selected reacher address", "peer_id", peerID, "selected_addr", bestAddr.String(), "advertised_count", len(peerAddrs)) underlay, err := buildFullMA(bestAddr, peerID) @@ -1128,7 +1135,9 @@ func (s *Service) Connect(ctx context.Context, addrs []ma.Multiaddr) (address *b return nil, p2p.ErrPeerNotFound } - if i.FullNode { + // Only persist peers with underlays to addressbook. Inbound-only peers (empty underlays) + // cannot be dialed back, so there's no point in storing them for reconnection. + if i.FullNode && len(i.BzzAddress.Underlays) > 0 { err = s.addressbook.Put(overlay, *i.BzzAddress) if err != nil { _ = s.Disconnect(overlay, "failed storing peer in addressbook") diff --git a/pkg/p2p/libp2p/libp2p_test.go b/pkg/p2p/libp2p/libp2p_test.go index ede9cbbcb79..0256ca8b5ec 100644 --- a/pkg/p2p/libp2p/libp2p_test.go +++ b/pkg/p2p/libp2p/libp2p_test.go @@ -23,6 +23,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/topology/lightnode" "github.com/ethersphere/bee/v2/pkg/util/testutil" + libp2pm "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" "github.com/multiformats/go-multiaddr" ) @@ -35,6 +37,7 @@ type libp2pServiceOpts struct { lightNodes *lightnode.Container notifier p2p.PickyNotifier autoTLSCertManager libp2p.AutoTLSCertManager + emptyUnderlays bool // If true, create a service that advertises no underlay addresses } // newService constructs a new libp2p service. @@ -82,6 +85,16 @@ func newService(t *testing.T, networkID uint64, o libp2pServiceOpts) (s *libp2p. opts := o.libp2pOpts opts.Nonce = nonce + // If emptyUnderlays is set, use a custom host factory that creates a host with no listen addresses + // This simulates an inbound-only peer (browser, WebRTC connection, strict NAT) + if o.emptyUnderlays { + libp2p.SetHostFactory(&opts, func(hostOpts ...libp2pm.Option) (host.Host, error) { + // Add NoListenAddrs option to prevent the host from listening on any addresses + hostOpts = append(hostOpts, libp2pm.NoListenAddrs) + return libp2pm.New(hostOpts...) + }) + } + if o.autoTLSCertManager != nil { libp2p.SetAutoTLSCertManager(&opts, o.autoTLSCertManager) }