Repository: libp2p/go-libp2p
Pull request #2774 (2024-04-18 18:45:05)

basichost: append certhash for webrtc addresses provided via address factory
This change is similar to how we append certhashes for webtransport. 


Patch:
diff --git a/core/event/identify.go b/core/event/identify.go
index 888572a2..ff149958 100644
--- a/core/event/identify.go
+++ b/core/event/identify.go
@@ -1,40 +1,11 @@
 package event
 
-import (
-	"github.com/libp2p/go-libp2p/core/network"
-	"github.com/libp2p/go-libp2p/core/peer"
-	"github.com/libp2p/go-libp2p/core/protocol"
-	"github.com/libp2p/go-libp2p/core/record"
-	"github.com/multiformats/go-multiaddr"
-)
+import "github.com/libp2p/go-libp2p/core/peer"
 
 // EvtPeerIdentificationCompleted is emitted when the initial identification round for a peer is completed.
 type EvtPeerIdentificationCompleted struct {
 	// Peer is the ID of the peer whose identification succeeded.
 	Peer peer.ID
-
-	// Conn is the connection we identified.
-	Conn network.Conn
-
-	// ListenAddrs is the list of addresses the peer is listening on.
-	ListenAddrs []multiaddr.Multiaddr
-
-	// Protocols is the list of protocols the peer advertised on this connection.
-	Protocols []protocol.ID
-
-	// SignedPeerRecord is the provided signed peer record of the peer. May be nil.
-	SignedPeerRecord *record.Envelope
-
-	// AgentVersion is like a UserAgent string in browsers, or client version in
-	// bittorrent includes the client name and client.
-	AgentVersion string
-
-	// ProtocolVersion is the protocolVersion field in the identify message
-	ProtocolVersion string
-
-	// ObservedAddr is the our side's connection address as observed by the
-	// peer. This is not verified, the peer could return anything here.
-	ObservedAddr multiaddr.Multiaddr
 }
 
 // EvtPeerIdentificationFailed is emitted when the initial identification round for a peer failed.
diff --git a/examples/README.md b/examples/README.md
index f9074de2..529122fe 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -2,21 +2,17 @@
 
 In this folder, you can find a variety of examples to help you get started in using go-libp2p. Every example as a specific purpose and some of each incorporate a full tutorial that you can follow through, helping you expand your knowledge about libp2p and p2p networks in general.
 
-Let us know if you find any issue or if you want to contribute and add a new tutorial, feel welcome to submit a PR, thank you!
+Let us know if you find any issue or if you want to contribute and add a new tutorial, feel welcome to submit a pr, thank you!
 
 ## Examples and Tutorials
 
 - [The libp2p 'host'](./libp2p-host)
 - [Building an http proxy with libp2p](./http-proxy)
 - [An echo host](./echo)
-- [Routed echo host](./routed-echo/)
 - [Multicodecs with protobufs](./multipro)
-- [Relay-based P2P Communication](./relay/)
 - [P2P chat application](./chat)
 - [P2P chat application w/ rendezvous peer discovery](./chat-with-rendezvous)
 - [P2P chat application with peer discovery using mdns](./chat-with-mdns)
-- [P2P chat using pubsub](./pubsub)
 - [A chapter based approach to building a libp2p application](./ipfs-camp-2019/) _Created for [IPFS Camp 2019](https://github.com/ipfs/camp/tree/master/CORE_AND_ELECTIVE_COURSES/CORE_COURSE_B)_
-- [View metrics using Prometheus and Grafana](./metrics-and-dashboards)
 
 For js-libp2p examples, check https://github.com/libp2p/js-libp2p-examples
diff --git a/examples/chat-with-rendezvous/chat.go b/examples/chat-with-rendezvous/chat.go
index a2ddb897..5fc8d4e3 100644
--- a/examples/chat-with-rendezvous/chat.go
+++ b/examples/chat-with-rendezvous/chat.go
@@ -6,7 +6,7 @@ import (
 	"flag"
 	"fmt"
 	"os"
-	"time"
+	"sync"
 
 	"github.com/libp2p/go-libp2p"
 	"github.com/libp2p/go-libp2p/core/network"
@@ -114,12 +114,7 @@ func main() {
 	// DHT, so that the bootstrapping node of the DHT can go down without
 	// inhibiting future peer discovery.
 	ctx := context.Background()
-	bootstrapPeers := make([]peer.AddrInfo, len(config.BootstrapPeers))
-	for i, addr := range config.BootstrapPeers {
-		peerinfo, _ := peer.AddrInfoFromP2pAddr(addr)
-		bootstrapPeers[i] = *peerinfo
-	}
-	kademliaDHT, err := dht.New(ctx, host, dht.BootstrapPeers(bootstrapPeers...))
+	kademliaDHT, err := dht.New(ctx, host)
 	if err != nil {
 		panic(err)
 	}
@@ -131,8 +126,22 @@ func main() {
 		panic(err)
 	}
 
-	// Wait a bit to let bootstrapping finish (really bootstrap should block until it's ready, but that isn't the case yet.)
-	time.Sleep(1 * time.Second)
+	// Let's connect to the bootstrap nodes first. They will tell us about the
+	// other nodes in the network.
+	var wg sync.WaitGroup
+	for _, peerAddr := range config.BootstrapPeers {
+		peerinfo, _ := peer.AddrInfoFromP2pAddr(peerAddr)
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			if err := host.Connect(ctx, *peerinfo); err != nil {
+				logger.Warning(err)
+			} else {
+				logger.Info("Connection established with bootstrap node:", *peerinfo)
+			}
+		}()
+	}
+	wg.Wait()
 
 	// We use a rendezvous point "meet me here" to announce our location.
 	// This is like telling your friends to meet you at the Eiffel Tower.
diff --git a/examples/pubsub/README.md b/examples/pubsub/README.md
index a41419ac..050b33f6 100644
--- a/examples/pubsub/README.md
+++ b/examples/pubsub/README.md
@@ -3,5 +3,4 @@
 This directory contains example projects that use [go-libp2p-pubsub](https://github.com/libp2p/go-libp2p-pubsub),
 the Go implementation of libp2p's [Publish / Subscribe system](https://docs.libp2p.io/concepts/publish-subscribe).
 
-- The [chat room example](./chat) covers the basics of using the PubSub API to build a peer-to-peer chat application.
-- The [pubsub rendezvous example](./basic-chat-with-rendezvous) allows multiple peers to chat among each other using go-libp2p-pubsub using rendezvous names.
+The [chat room example](./chat) covers the basics of using the PubSub API to build a peer-to-peer chat application.
diff --git a/examples/relay/README.md b/examples/relay/README.md
deleted file mode 100644
index a4bd5c50..00000000
--- a/examples/relay/README.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Relay-based P2P Communication Example
-
-## Overview
-This project demonstrates the setup of a relay-based peer-to-peer communication using the libp2p library in Go. It features creating unreachable libp2p hosts and facilitating their communication through a relay node.
-
-## Features
-- Creation of two "unreachable" libp2p hosts.
-- Setup of a relay node to enable communication between these hosts.
-
-## Usage
-
-Run the program 
-   ```bash
-   go run . 
-   ```
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 74dd50cb..2c231af2 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
 	github.com/flynn/noise v1.1.0
 	github.com/google/gopacket v1.1.19
-	github.com/gorilla/websocket v1.5.0
+	github.com/gorilla/websocket v1.5.1
 	github.com/hashicorp/golang-lru/arc/v2 v2.0.5
 	github.com/hashicorp/golang-lru/v2 v2.0.5
 	github.com/ipfs/go-cid v0.4.1
diff --git a/go.sum b/go.sum
index 155ca30f..5d1648e6 100644
--- a/go.sum
+++ b/go.sum
@@ -133,8 +133,8 @@ github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
 github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
 github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
diff --git a/libp2p.go b/libp2p.go
index 1440648d..9bb36b17 100644
--- a/libp2p.go
+++ b/libp2p.go
@@ -48,7 +48,7 @@ func ChainOptions(opts ...Option) Option {
 // - If no peerstore is provided, the host is initialized with an empty
 // peerstore.
 //
-// To stop/shutdown the returned libp2p node, the user needs to call `Close` on the returned Host.
+// To stop/shutdown the returned libp2p node, the user needs to cancel the passed context and call `Close` on the returned Host.
 func New(opts ...Option) (host.Host, error) {
 	return NewWithoutDefaults(append(opts, FallbackDefaults)...)
 }
diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go
index 12329391..367fca05 100644
--- a/p2p/host/basic/basic_host.go
+++ b/p2p/host/basic/basic_host.go
@@ -27,6 +27,7 @@ import (
 	"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
 	"github.com/libp2p/go-libp2p/p2p/protocol/identify"
 	"github.com/libp2p/go-libp2p/p2p/protocol/ping"
+	libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
 	libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
 	"github.com/prometheus/client_golang/prometheus"
 
@@ -786,7 +787,9 @@ func (h *BasicHost) Addrs() []ma.Multiaddr {
 	copy(addrs, addrsOld)
 
 	for i, addr := range addrs {
-		if ok, n := libp2pwebtransport.IsWebtransportMultiaddr(addr); ok && n == 0 {
+		wtOK, wtN := libp2pwebtransport.IsWebtransportMultiaddr(addr)
+		webrtcOK, webrtcN := libp2pwebrtc.IsWebRTCDirectMultiaddr(addr)
+		if (wtOK && wtN == 0) || (webrtcOK && webrtcN == 0) {
 			t := s.TransportForListening(addr)
 			tpt, ok := t.(addCertHasher)
 			if !ok {
@@ -794,7 +797,7 @@ func (h *BasicHost) Addrs() []ma.Multiaddr {
 			}
 			addrWithCerthash, added := tpt.AddCertHashes(addr)
 			if !added {
-				log.Debug("Couldn't add certhashes to webtransport multiaddr because we aren't listening on webtransport")
+				log.Debugf("Couldn't add certhashes to multiaddr: %s", addr)
 				continue
 			}
 			addrs[i] = addrWithCerthash
diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go
index 9b0fc178..fbd8a64f 100644
--- a/p2p/http/libp2phttp.go
+++ b/p2p/http/libp2phttp.go
@@ -30,7 +30,6 @@ import (
 var log = logging.Logger("libp2phttp")
 
 const ProtocolIDForMultistreamSelect = "/http/1.1"
-const WellKnownProtocols = "/.well-known/libp2p/protocols"
 const peerMetadataLimit = 8 << 10 // 8KB
 const peerMetadataLRUSize = 256   // How many different peer's metadata to keep in our LRU cache
 
@@ -42,7 +41,7 @@ type ProtocolMeta struct {
 
 type PeerMeta map[protocol.ID]ProtocolMeta
 
-// WellKnownHandler is an http.Handler that serves the well-known resource
+// WellKnownHandler is an http.Handler that serves the .well-known/libp2p resource
 type WellKnownHandler struct {
 	wellknownMapMu   sync.Mutex
 	wellKnownMapping PeerMeta
@@ -138,7 +137,7 @@ type Host struct {
 	// `http.Transport` on first use.
 	DefaultClientRoundTripper *http.Transport
 
-	// WellKnownHandler is the http handler for the well-known
+	// WellKnownHandler is the http handler for the `.well-known/libp2p`
 	// resource. It is responsible for sharing this node's protocol metadata
 	// with other nodes. Users only care about this if they set their own
 	// ServeMux with pre-existing routes. By default, new protocols are added
@@ -271,7 +270,7 @@ func (h *Host) Serve() error {
 	}
 
 	h.serveMuxInit()
-	h.ServeMux.Handle(WellKnownProtocols, &h.WellKnownHandler)
+	h.ServeMux.Handle("/.well-known/libp2p", &h.WellKnownHandler)
 
 	h.httpTransportInit()
 
@@ -305,7 +304,7 @@ func (h *Host) Serve() error {
 		h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, h.StreamHost.Addrs()...)
 
 		go func() {
-			errCh <- http.Serve(listener, connectionCloseHeaderMiddleware(h.ServeMux))
+			errCh <- http.Serve(listener, h.ServeMux)
 		}()
 	}
 
@@ -353,7 +352,7 @@ func (h *Host) Close() error {
 }
 
 // SetHTTPHandler sets the HTTP handler for a given protocol. Automatically
-// manages the well-known resource mapping.
+// manages the .well-known/libp2p mapping.
 // http.StripPrefix is called on the handler, so the handler will be unaware of
 // its prefix path.
 func (h *Host) SetHTTPHandler(p protocol.ID, handler http.Handler) {
@@ -361,7 +360,7 @@ func (h *Host) SetHTTPHandler(p protocol.ID, handler http.Handler) {
 }
 
 // SetHTTPHandlerAtPath sets the HTTP handler for a given protocol using the
-// given path. Automatically manages the well-known resource mapping.
+// given path. Automatically manages the .well-known/libp2p mapping.
 // http.StripPrefix is called on the handler, so the handler will be unaware of
 // its prefix path.
 func (h *Host) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Handler) {
@@ -423,9 +422,6 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error)
 		return nil, err
 	}
 
-	// Write connection: close header to ensure the stream is closed after the response
-	r.Header.Add("connection", "close")
-
 	go func() {
 		defer s.CloseWrite()
 		r.Write(s)
@@ -747,7 +743,7 @@ func (h *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server pe
 		return meta, nil
 	}
 
-	req, err := http.NewRequest("GET", WellKnownProtocols, nil)
+	req, err := http.NewRequest("GET", "/.well-known/libp2p", nil)
 	if err != nil {
 		return nil, err
 	}
@@ -820,11 +816,3 @@ func (h *Host) RemovePeerMetadata(server peer.ID) {
 	}
 	h.peerMetadata.Remove(server)
 }
-
-func connectionCloseHeaderMiddleware(next http.Handler) http.Handler {
-	// Sets connection: close. It's preferable to not reuse streams for HTTP.
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.Header().Set("Connection", "close")
-		next.ServeHTTP(w, r)
-	})
-}
diff --git a/p2p/protocol/identify/id.go b/p2p/protocol/identify/id.go
index 4bb4a592..6c07dbdc 100644
--- a/p2p/protocol/identify/id.go
+++ b/p2p/protocol/identify/id.go
@@ -400,6 +400,8 @@ func (ids *idService) IdentifyWait(c network.Conn) <-chan struct{} {
 			ids.emitters.evtPeerIdentificationFailed.Emit(event.EvtPeerIdentificationFailed{Peer: c.RemotePeer(), Reason: err})
 			return
 		}
+
+		ids.emitters.evtPeerIdentificationCompleted.Emit(event.EvtPeerIdentificationCompleted{Peer: c.RemotePeer()})
 	}()
 
 	return e.IdentifyWaitChan
@@ -709,16 +711,8 @@ func (ids *idService) consumeMessage(mes *pb.Identify, c network.Conn, isPush bo
 		})
 	}
 
-	obsAddr, err := ma.NewMultiaddrBytes(mes.GetObservedAddr())
-	if err != nil {
-		log.Debugf("error parsing received observed addr for %s: %s", c, err)
-		obsAddr = nil
-	}
-
-	if obsAddr != nil {
-		// TODO refactor this to use the emitted events instead of having this func call explicitly.
-		ids.observedAddrs.Record(c, obsAddr)
-	}
+	// mes.ObservedAddr
+	ids.consumeObservedAddress(mes.GetObservedAddr(), c)
 
 	// mes.ListenAddrs
 	laddrs := mes.GetListenAddrs()
@@ -769,7 +763,6 @@ func (ids *idService) consumeMessage(mes *pb.Identify, c network.Conn, isPush bo
 		signedAddrs, err := ids.consumeSignedPeerRecord(c.RemotePeer(), signedPeerRecord)
 		if err != nil {
 			log.Debugf("failed to consume signed peer record: %s", err)
-			signedPeerRecord = nil
 		} else {
 			addrs = signedAddrs
 		}
@@ -793,18 +786,6 @@ func (ids *idService) consumeMessage(mes *pb.Identify, c network.Conn, isPush bo
 
 	// get the key from the other side. we may not have it (no-auth transport)
 	ids.consumeReceivedPubKey(c, mes.PublicKey)
-
-	ids.emitters.evtPeerIdentificationCompleted.Emit(event.EvtPeerIdentificationCompleted{
-		Peer:             c.RemotePeer(),
-		Conn:             c,
-		ListenAddrs:      lmaddrs,
-		Protocols:        mesProtocols,
-		SignedPeerRecord: signedPeerRecord,
-		ObservedAddr:     obsAddr,
-		ProtocolVersion:  pv,
-		AgentVersion:     av,
-	})
-
 }
 
 func (ids *idService) consumeSignedPeerRecord(p peer.ID, signedPeerRecord *record.Envelope) ([]ma.Multiaddr, error) {
@@ -938,6 +919,20 @@ func HasConsistentTransport(a ma.Multiaddr, green []ma.Multiaddr) bool {
 	return false
 }
 
+func (ids *idService) consumeObservedAddress(observed []byte, c network.Conn) {
+	if observed == nil {
+		return
+	}
+
+	maddr, err := ma.NewMultiaddrBytes(observed)
+	if err != nil {
+		log.Debugf("error parsing received observed addr for %s: %s", c, err)
+		return
+	}
+
+	ids.observedAddrs.Record(c, maddr)
+}
+
 // addConnWithLock assuems caller holds the connsMu lock
 func (ids *idService) addConnWithLock(c network.Conn) {
 	_, found := ids.conns[c]
diff --git a/p2p/transport/webrtc/transport.go b/p2p/transport/webrtc/transport.go
index ae7ae339..b04753ec 100644
--- a/p2p/transport/webrtc/transport.go
+++ b/p2p/transport/webrtc/transport.go
@@ -40,7 +40,6 @@ import (
 	"github.com/libp2p/go-msgio"
 
 	ma "github.com/multiformats/go-multiaddr"
-	mafmt "github.com/multiformats/go-multiaddr-fmt"
 	manet "github.com/multiformats/go-multiaddr/net"
 	"github.com/multiformats/go-multihash"
 
@@ -48,8 +47,6 @@ import (
 	"github.com/pion/webrtc/v3"
 )
 
-var dialMatcher = mafmt.And(mafmt.UDP, mafmt.Base(ma.P_WEBRTC_DIRECT), mafmt.Base(ma.P_CERTHASH))
-
 var webrtcComponent *ma.Component
 
 func init() {
@@ -179,7 +176,8 @@ func (t *WebRTCTransport) Proxy() bool {
 }
 
 func (t *WebRTCTransport) CanDial(addr ma.Multiaddr) bool {
-	return dialMatcher.Matches(addr)
+	isValid, n := IsWebRTCDirectMultiaddr(addr)
+	return isValid && n > 0
 }
 
 // Listen returns a listener for addr.
@@ -514,6 +512,24 @@ func (t *WebRTCTransport) noiseHandshake(ctx context.Context, pc *webrtc.PeerCon
 	return secureConn.RemotePublicKey(), nil
 }
 
+func (t *WebRTCTransport) AddCertHashes(addr ma.Multiaddr) (ma.Multiaddr, bool) {
+	listenerFingerprint, err := t.getCertificateFingerprint()
+	if err != nil {
+		return nil, false
+	}
+
+	encodedLocalFingerprint, err := encodeDTLSFingerprint(listenerFingerprint)
+	if err != nil {
+		return nil, false
+	}
+
+	certComp, err := ma.NewComponent(ma.ProtocolWithCode(ma.P_CERTHASH).Name, encodedLocalFingerprint)
+	if err != nil {
+		return nil, false
+	}
+	return addr.Encapsulate(certComp), true
+}
+
 type netConnWrapper struct {
 	*stream
 }
@@ -601,3 +617,35 @@ func newWebRTCConnection(settings webrtc.SettingEngine, config webrtc.Configurat
 		IncomingDataChannels: incomingDataChannels,
 	}, nil
 }
+
+// IsWebRTCDirectMultiaddr returns whether addr is a /webrtc-direct multiaddr with the count of certhashes
+// in addr
+func IsWebRTCDirectMultiaddr(addr ma.Multiaddr) (bool, int) {
+	var foundUDP, foundWebRTC bool
+	certHashCount := 0
+	ma.ForEach(addr, func(c ma.Component) bool {
+		if !foundUDP {
+			if c.Protocol().Code == ma.P_UDP {
+				foundUDP = true
+			}
+			return true
+		}
+		if !foundWebRTC && foundUDP {
+			// protocol after udp must be webrtc-direct
+			if c.Protocol().Code != ma.P_WEBRTC_DIRECT {
+				return false
+			}
+			foundWebRTC = true
+			return true
+		}
+		if foundWebRTC {
+			if c.Protocol().Code == ma.P_CERTHASH {
+				certHashCount++
+			} else {
+				return false
+			}
+		}
+		return true
+	})
+	return foundUDP && foundWebRTC, certHashCount
+}


Test patch:
diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go
index ca87fe40..2e90f8cc 100644
--- a/p2p/http/example_test.go
+++ b/p2p/http/example_test.go
@@ -339,11 +339,11 @@ func ExampleWellKnownHandler() {
 	}
 
 	defer listener.Close()
-	// Serve the well-known resource. Note, this is handled automatically if you use the libp2phttp.Host.
+	// Serve `.well-known/libp2p`. Note, this is handled automatically if you use the libp2phttp.Host.
 	go http.Serve(listener, &h)
 
-	// Get the well-known resource
-	resp, err := http.Get("http://" + listener.Addr().String() + libp2phttp.WellKnownProtocols)
+	// Get the `.well-known/libp2p` resource
+	resp, err := http.Get("http://" + listener.Addr().String() + "/.well-known/libp2p")
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go
index 46c33029..57dd0cc8 100644
--- a/p2p/http/libp2phttp_test.go
+++ b/p2p/http/libp2phttp_test.go
@@ -68,84 +68,6 @@ func TestHTTPOverStreams(t *testing.T) {
 	require.Equal(t, "hello", string(body))
 }
 
-func TestHTTPOverStreamsSendsConnectionClose(t *testing.T) {
-	serverHost, err := libp2p.New(
-		libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"),
-	)
-	require.NoError(t, err)
-
-	httpHost := libp2phttp.Host{StreamHost: serverHost}
-
-	connectionHeaderVal := make(chan string, 1)
-	httpHost.SetHTTPHandlerAtPath("/hello", "/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.Write([]byte("hello"))
-		connectionHeaderVal <- r.Header.Get("Connection")
-	}))
-
-	// Start server
-	go httpHost.Serve()
-	defer httpHost.Close()
-
-	// run client
-	clientHost, err := libp2p.New(libp2p.NoListenAddrs)
-	require.NoError(t, err)
-	clientHost.Connect(context.Background(), peer.AddrInfo{
-		ID:    serverHost.ID(),
-		Addrs: serverHost.Addrs(),
-	})
-	clientHttpHost := libp2phttp.Host{StreamHost: clientHost}
-	rt, err := clientHttpHost.NewConstrainedRoundTripper(peer.AddrInfo{ID: serverHost.ID()})
-	require.NoError(t, err)
-	client := &http.Client{Transport: rt}
-	_, err = client.Get("/")
-	require.NoError(t, err)
-
-	select {
-	case val := <-connectionHeaderVal:
-		require.Equal(t, "close", strings.ToLower(val))
-	case <-time.After(5 * time.Second):
-		t.Fatal("timeout waiting for connection header")
-	}
-}
-
-func TestHTTPOverStreamsReturnsConnectionClose(t *testing.T) {
-	serverHost, err := libp2p.New(
-		libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"),
-	)
-	require.NoError(t, err)
-
-	httpHost := libp2phttp.Host{StreamHost: serverHost}
-
-	httpHost.SetHTTPHandlerAtPath("/hello", "/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.Write([]byte("hello"))
-	}))
-
-	// Start server
-	go httpHost.Serve()
-	defer httpHost.Close()
-
-	// Start client
-	clientHost, err := libp2p.New(libp2p.NoListenAddrs)
-	require.NoError(t, err)
-	clientHost.Connect(context.Background(), peer.AddrInfo{
-		ID:    serverHost.ID(),
-		Addrs: serverHost.Addrs(),
-	})
-
-	s, err := clientHost.NewStream(context.Background(), serverHost.ID(), libp2phttp.ProtocolIDForMultistreamSelect)
-	require.NoError(t, err)
-	_, err = s.Write([]byte("GET / HTTP/1.1\r\nHost: \r\n\r\n"))
-	require.NoError(t, err)
-
-	out := make([]byte, 1024)
-	n, err := s.Read(out)
-	if err != io.EOF {
-		require.NoError(t, err)
-	}
-
-	require.Contains(t, strings.ToLower(string(out[:n])), "connection: close")
-}
-
 func TestRoundTrippers(t *testing.T) {
 	serverHost, err := libp2p.New(
 		libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"),
@@ -283,7 +205,7 @@ func TestRoundTrippers(t *testing.T) {
 				})
 			}
 
-			// Read the well-known resource
+			// Read the .well-known/libp2p resource
 			wk, err := rt.(libp2phttp.PeerMetadataGetter).GetPeerMetadata()
 			require.NoError(t, err)
 
@@ -297,7 +219,7 @@ func TestRoundTrippers(t *testing.T) {
 func TestPlainOldHTTPServer(t *testing.T) {
 	mux := http.NewServeMux()
 	wk := libp2phttp.WellKnownHandler{}
-	mux.Handle(libp2phttp.WellKnownProtocols, &wk)
+	mux.Handle("/.well-known/libp2p", &wk)
 
 	mux.Handle("/ping/", httpping.Ping{})
 	wk.AddProtocolMeta(httpping.PingProtocolID, libp2phttp.ProtocolMeta{Path: "/ping/"})
@@ -348,7 +270,7 @@ func TestPlainOldHTTPServer(t *testing.T) {
 			},
 			getWellKnown: func(t *testing.T) (libp2phttp.PeerMeta, error) {
 				client := http.Client{}
-				resp, err := client.Get("http://" + l.Addr().String() + libp2phttp.WellKnownProtocols)
+				resp, err := client.Get("http://" + l.Addr().String() + "/.well-known/libp2p")
 				require.NoError(t, err)
 
 				b, err := io.ReadAll(resp.Body)
diff --git a/p2p/protocol/identify/id_test.go b/p2p/protocol/identify/id_test.go
index 2352abef..0f920356 100644
--- a/p2p/protocol/identify/id_test.go
+++ b/p2p/protocol/identify/id_test.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"slices"
 	"sync"
 	"testing"
 	"time"
@@ -201,47 +200,12 @@ func TestIDService(t *testing.T) {
 
 	// test that we received the "identify completed" event.
 	select {
-	case evtAny := <-sub.Out():
-		assertCorrectEvtPeerIdentificationCompleted(t, evtAny, h2)
+	case <-sub.Out():
 	case <-time.After(3 * time.Second):
 		t.Fatalf("expected EvtPeerIdentificationCompleted event within 10 seconds; none received")
 	}
 }
 
-func assertCorrectEvtPeerIdentificationCompleted(t *testing.T, evtAny interface{}, other host.Host) {
-	t.Helper()
-	evt := evtAny.(event.EvtPeerIdentificationCompleted)
-	require.NotNil(t, evt.Conn)
-	require.Equal(t, other.ID(), evt.Peer)
-
-	require.Equal(t, len(other.Addrs()), len(evt.ListenAddrs))
-	if len(other.Addrs()) == len(evt.ListenAddrs) {
-		otherAddrsStrings := make([]string, len(other.Addrs()))
-		evtAddrStrings := make([]string, len(evt.ListenAddrs))
-		for i, a := range other.Addrs() {
-			otherAddrsStrings[i] = a.String()
-			evtAddrStrings[i] = evt.ListenAddrs[i].String()
-		}
-		slices.Sort(otherAddrsStrings)
-		slices.Sort(evtAddrStrings)
-		require.Equal(t, otherAddrsStrings, evtAddrStrings)
-	}
-
-	otherProtos := other.Mux().Protocols()
-	slices.Sort(otherProtos)
-	evtProtos := evt.Protocols
-	slices.Sort(evtProtos)
-	require.Equal(t, otherProtos, evtProtos)
-	idFromSignedRecord, err := peer.IDFromPublicKey(evt.SignedPeerRecord.PublicKey)
-	require.NoError(t, err)
-	require.Equal(t, other.ID(), idFromSignedRecord)
-	require.Equal(t, peer.PeerRecordEnvelopePayloadType, evt.SignedPeerRecord.PayloadType)
-	var peerRecord peer.PeerRecord
-	evt.SignedPeerRecord.TypedRecord(&peerRecord)
-	require.Equal(t, other.ID(), peerRecord.PeerID)
-	require.Equal(t, other.Addrs(), peerRecord.Addrs)
-}
-
 func TestProtoMatching(t *testing.T) {
 	tcp1, _ := ma.NewMultiaddr("/ip4/1.2.3.4/tcp/1234")
 	tcp2, _ := ma.NewMultiaddr("/ip4/1.2.3.4/tcp/2345")
@@ -701,8 +665,7 @@ func TestLargeIdentifyMessage(t *testing.T) {
 
 	// test that we received the "identify completed" event.
 	select {
-	case evtAny := <-sub.Out():
-		assertCorrectEvtPeerIdentificationCompleted(t, evtAny, h2)
+	case <-sub.Out():
 	case <-time.After(3 * time.Second):
 		t.Fatalf("expected EvtPeerIdentificationCompleted event within 3 seconds; none received")
 	}
diff --git a/p2p/test/basichost/basic_host_test.go b/p2p/test/basichost/basic_host_test.go
index 98d8cf45..e6cd7ea9 100644
--- a/p2p/test/basichost/basic_host_test.go
+++ b/p2p/test/basichost/basic_host_test.go
@@ -3,6 +3,7 @@ package basichost
 import (
 	"context"
 	"fmt"
+	"strings"
 	"testing"
 	"time"
 
@@ -12,6 +13,8 @@ import (
 	"github.com/libp2p/go-libp2p/core/peerstore"
 	"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client"
 	"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
+	libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
+	libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
 	ma "github.com/multiformats/go-multiaddr"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -158,3 +161,41 @@ func TestNewStreamTransientConnection(t *testing.T) {
 	<-done
 	<-done
 }
+
+func TestAddrFactorCertHashAppend(t *testing.T) {
+	wtAddr := "/ip4/1.2.3.4/udp/1/quic-v1/webtransport"
+	webrtcAddr := "/ip4/1.2.3.4/udp/2/webrtc-direct"
+	addrsFactory := func(addrs []ma.Multiaddr) []ma.Multiaddr {
+		return append(addrs,
+			ma.StringCast(wtAddr),
+			ma.StringCast(webrtcAddr),
+		)
+	}
+	h, err := libp2p.New(
+		libp2p.AddrsFactory(addrsFactory),
+		libp2p.Transport(libp2pwebrtc.New),
+		libp2p.Transport(libp2pwebtransport.New),
+		libp2p.ListenAddrStrings(
+			"/ip4/0.0.0.0/udp/0/quic-v1/webtransport",
+			"/ip4/0.0.0.0/udp/0/webrtc-direct",
+		),
+	)
+	require.NoError(t, err)
+	require.Eventually(t, func() bool {
+		addrs := h.Addrs()
+		var hasWebRTC, hasWebTransport bool
+		for _, addr := range addrs {
+			if strings.HasPrefix(addr.String(), webrtcAddr) {
+				if _, err := addr.ValueForProtocol(ma.P_CERTHASH); err == nil {
+					hasWebRTC = true
+				}
+			}
+			if strings.HasPrefix(addr.String(), wtAddr) {
+				if _, err := addr.ValueForProtocol(ma.P_CERTHASH); err == nil {
+					hasWebTransport = true
+				}
+			}
+		}
+		return hasWebRTC && hasWebTransport
+	}, 5*time.Second, 100*time.Millisecond)
+}
diff --git a/p2p/transport/webrtc/transport_test.go b/p2p/transport/webrtc/transport_test.go
index e5ef3ca1..a3054a82 100644
--- a/p2p/transport/webrtc/transport_test.go
+++ b/p2p/transport/webrtc/transport_test.go
@@ -40,6 +40,58 @@ func getTransport(t *testing.T, opts ...Option) (*WebRTCTransport, peer.ID) {
 	return transport, peerID
 }
 
+func TestIsWebRTCDirectMultiaddr(t *testing.T) {
+	invalid := []string{
+		"/ip4/1.2.3.4/tcp/10/",
+		"/ip6/1::3/udp/100/quic-v1/",
+		"/ip4/1.2.3.4/udp/1/quic-v1/webrtc-direct",
+	}
+
+	valid := []struct {
+		addr  string
+		count int
+	}{
+		{
+			addr:  "/ip4/1.2.3.4/udp/1234/webrtc-direct",
+			count: 0,
+		},
+		{
+			addr:  "/dns/test.test/udp/1234/webrtc-direct",
+			count: 0,
+		},
+		{
+			addr:  "/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7YFrV4VZ3hpEKTd_zg",
+			count: 1,
+		},
+		{
+			addr:  "/ip6/0:0:0:0:0:0:0:1/udp/1234/webrtc-direct/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7YFrV4VZ3hpEKTd_zg",
+			count: 1,
+		},
+		{
+			addr:  "/dns/test.test/udp/1234/webrtc-direct/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7YFrV4VZ3hpEKTd_zg",
+			count: 1,
+		},
+		{
+			addr:  "/dns/test.test/udp/1234/webrtc-direct/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7YFrV4VZ3hpEKTd_zg/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7ZGrV4VZ3hpEKTd_zg",
+			count: 2,
+		},
+	}
+
+	for _, addr := range invalid {
+		a := ma.StringCast(addr)
+		isValid, n := IsWebRTCDirectMultiaddr(a)
+		require.Equal(t, 0, n)
+		require.False(t, isValid)
+	}
+
+	for _, tc := range valid {
+		a := ma.StringCast(tc.addr)
+		isValid, n := IsWebRTCDirectMultiaddr(a)
+		require.Equal(t, tc.count, n)
+		require.True(t, isValid)
+	}
+}
+
 func TestTransportWebRTC_CanDial(t *testing.T) {
 	tr, _ := getTransport(t)
 	invalid := []string{
@@ -65,6 +117,21 @@ func TestTransportWebRTC_CanDial(t *testing.T) {
 	}
 }
 
+func TestTransportAddCertHasher(t *testing.T) {
+	tr, _ := getTransport(t)
+	addrs := []string{
+		"/ip4/1.2.3.4/udp/1/webrtc-direct",
+		"/ip6/1::3/udp/2/webrtc-direct",
+	}
+	for _, a := range addrs {
+		addr, added := tr.AddCertHashes(ma.StringCast(a))
+		require.True(t, added)
+		_, err := addr.ValueForProtocol(ma.P_CERTHASH)
+		require.NoError(t, err)
+		require.True(t, strings.HasPrefix(addr.String(), a))
+	}
+}
+
 func TestTransportWebRTC_ListenFailsOnNonWebRTCMultiaddr(t *testing.T) {
 	tr, _ := getTransport(t)
 	testAddrs := []string{