diff --git a/core/commands/swarm_addrs_autonat.go b/core/commands/swarm_addrs_autonat.go index ad4fed7c606..0f02ddc1514 100644 --- a/core/commands/swarm_addrs_autonat.go +++ b/core/commands/swarm_addrs_autonat.go @@ -8,6 +8,7 @@ import ( cmdenv "github.com/ipfs/kubo/core/commands/cmdenv" "github.com/ipfs/kubo/core/node/libp2p" "github.com/libp2p/go-libp2p/core/network" + basichost "github.com/libp2p/go-libp2p/p2p/host/basic" ma "github.com/multiformats/go-multiaddr" ) @@ -21,6 +22,13 @@ type confirmedAddrsHost interface { ConfirmedAddrs() (reachable, unreachable, unknown []ma.Multiaddr) } +// Compile-time check: BasicHost must satisfy confirmedAddrsHost. +// ConfirmedAddrs is not part of the core host.Host interface and is marked +// experimental in go-libp2p. If BasicHost ever drops or changes this method, +// this assertion fails at build time instead of the command silently +// reporting nothing. +var _ confirmedAddrsHost = (*basichost.BasicHost)(nil) + // autoNATResult represents the AutoNAT reachability information. type autoNATResult struct { Reachability string `json:"reachability"` diff --git a/core/node/libp2p/routingopt.go b/core/node/libp2p/routingopt.go index 1d5d6c23220..8922e0e234a 100644 --- a/core/node/libp2p/routingopt.go +++ b/core/node/libp2p/routingopt.go @@ -19,8 +19,8 @@ import ( host "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" routing "github.com/libp2p/go-libp2p/core/routing" - basichost "github.com/libp2p/go-libp2p/p2p/host/basic" ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" ) type RoutingOptionArgs struct { @@ -320,58 +320,64 @@ var ( NilRouterOption = constructNilRouting ) -// confirmedAddrsHost matches libp2p hosts that support AutoNAT V2 address confirmation. -type confirmedAddrsHost interface { - ConfirmedAddrs() (reachable, unreachable, unknown []ma.Multiaddr) -} - -// Compile-time check: BasicHost must satisfy confirmedAddrsHost. -// ConfirmedAddrs is not part of the core host.Host interface and is marked -// experimental in go-libp2p. If BasicHost ever drops or changes this method, -// this assertion will fail at build time. In that case, update -// httpRouterAddrFunc (this file) and the swarm autonat command -// (core/commands/swarm_addrs_autonat.go) which both type-assert to this -// interface. -var _ confirmedAddrsHost = (*basichost.BasicHost)(nil) - // httpRouterAddrFunc returns a function that resolves provider addresses for // HTTP routers at provide-time. // // Resolution logic: // - If Announce is set, use it as a static override (no dynamic resolution). -// - Otherwise, prefer AutoNAT V2 confirmed reachable addresses when available, -// falling back to host.Addrs() which resolves 0.0.0.0/:: Swarm binds to -// concrete interface addresses and applies the libp2p AddrsFactory -// (Addresses.NoAnnounce CIDR filters and Swarm.AddrFilters). -// - AppendAnnounce addresses are always appended. +// - Otherwise announce host.Addrs(), the same set identify sends to peers: +// 0.0.0.0/:: Swarm binds resolved to concrete interface addresses, the +// libp2p AddrsFactory applied (Addresses.NoAnnounce, and the AutoTLS +// /tls/ws address, which only exists here), and certhashes attached. +// - Narrowed to globally routable addresses when the node has any, matching +// the DHT provide path (selfAddrsFunc in core/node/provider.go). A node +// with no public address keeps announcing what it has, so LAN-only setups +// pointing at a local router still publish something dialable. +// - AppendAnnounce addresses are always appended, exactly once, and are +// exempt from both NoAnnounce and the public-address narrowing: an +// explicit operator entry wins. +// +// Do not narrow this to AutoNAT V2 confirmed reachable addresses. AutoNAT only +// ever sees listen addresses, so an address the AddrsFactory synthesizes (the +// AutoTLS /tls/ws one) can never be confirmed, and browser clients that reach +// us through a delegated router are left with nothing they can dial. +// See https://github.com/ipfs/kubo/issues/11369. func httpRouterAddrFunc(h host.Host, cfgAddrs config.Addresses) func() []ma.Multiaddr { appendAddrs := parseMultiaddrs(cfgAddrs.AppendAnnounce) // If Announce is explicitly set, use it as a static override. if len(cfgAddrs.Announce) > 0 { - staticAddrs := slices.Concat(parseMultiaddrs(cfgAddrs.Announce), appendAddrs) + announceAddrs := parseMultiaddrs(cfgAddrs.Announce) + // Skip AppendAnnounce entries already listed in Announce, + // mirroring makeAddrsFactory (addrs.go). + extra := ma.FilterAddrs(appendAddrs, func(a ma.Multiaddr) bool { + return !ma.Contains(announceAddrs, a) + }) + staticAddrs := slices.Concat(announceAddrs, extra) return func() []ma.Multiaddr { return staticAddrs } } - ch, hasConfirmed := h.(confirmedAddrsHost) return func() []ma.Multiaddr { - if hasConfirmed { - reachable, _, _ := ch.ConfirmedAddrs() - if len(reachable) > 0 { - if len(appendAddrs) == 0 { - return reachable - } - return slices.Concat(reachable, appendAddrs) - } + addrs := h.Addrs() + if len(appendAddrs) > 0 { + // The AddrsFactory (makeAddrsFactory in addrs.go) already injects + // AppendAnnounce into h.Addrs(). Take those out so the re-append + // below emits them exactly once, and so a public AppendAnnounce + // entry cannot trip the public-addr guard on a node whose own + // addresses are all private. + addrs = ma.FilterAddrs(addrs, func(a ma.Multiaddr) bool { + return !ma.Contains(appendAddrs, a) + }) + } + // Delegated routers are public indexes, so keep loopback and LAN + // addresses out of the record whenever we have a routable one. + if public := ma.FilterAddrs(addrs, manet.IsPublicAddr); len(public) > 0 { + addrs = public } - // Fallback: host.Addrs() resolves wildcard binds (0.0.0.0, ::) to - // concrete interface addresses and applies the libp2p AddrsFactory, - // which is where Addresses.NoAnnounce CIDR filtering happens. - hostAddrs := h.Addrs() if len(appendAddrs) == 0 { - return hostAddrs + return addrs } - return slices.Concat(hostAddrs, appendAddrs) + return slices.Concat(addrs, appendAddrs) } } diff --git a/core/node/libp2p/routingopt_test.go b/core/node/libp2p/routingopt_test.go index 2681e6714d9..182ef960e9b 100644 --- a/core/node/libp2p/routingopt_test.go +++ b/core/node/libp2p/routingopt_test.go @@ -206,14 +206,19 @@ func TestEndpointCapabilitiesReadWriteLogic(t *testing.T) { } // stubHost is a minimal host.Host stub for testing httpRouterAddrFunc. -// reachable mocks ConfirmedAddrs (AutoNAT V2 result); hostAddrs mocks -// Addrs(), which in a real host returns wildcard-resolved interface -// addresses after the AddrsFactory filters (NoAnnounce/AddrFilters). +// hostAddrs mocks Addrs(), which in a real host returns wildcard-resolved +// interface addresses after the AddrsFactory ran (NoAnnounce filtering, the +// AutoTLS /tls/ws address, certhashes). type stubHost struct { - reachable []ma.Multiaddr hostAddrs []ma.Multiaddr + reachable []ma.Multiaddr } +// ConfirmedAddrs mocks the AutoNAT V2 confirmed set. httpRouterAddrFunc must +// not consult it: AutoNAT only sees listen addresses, so the AddrsFactory- +// synthesized AutoTLS /tls/ws address can never appear in it, and a record +// narrowed to this set carries nothing a browser can dial. +// See https://github.com/ipfs/kubo/issues/11369. func (h *stubHost) ConfirmedAddrs() (reachable, unreachable, unknown []ma.Multiaddr) { return h.reachable, nil, nil } @@ -237,77 +242,126 @@ func (h *stubHost) ConnManager() connmgr.ConnManager { panic("unused") } func (h *stubHost) EventBus() event.Bus { panic("unused") } func TestHttpRouterAddrFunc(t *testing.T) { - // hostAddrs simulates what host.Addrs() returns in a running daemon: - // wildcard Swarm binds resolved to concrete interfaces, with the - // libp2p AddrsFactory (NoAnnounce/AddrFilters) already applied. - resolvedAddrs := []string{ - "/ip4/192.168.1.10/tcp/4001", - "/ip4/192.168.1.10/udp/4001/quic-v1", - } + // A publicly reachable node with every transport kubo can announce. + // This is what host.Addrs() returns in a running daemon: wildcard Swarm + // binds resolved to concrete interfaces (so the LAN address shows up + // next to the public one), AddrsFactory applied, certhashes attached. + const ( + autoTLSWS = "/dns4/1-2-3-4.k51qzi5uqu5dlwbcbb2u3ptbnhs5lpj4nc3xbhrgqrkgo3l0h5xgv9k1nn9k1.libp2p.direct/tcp/4001/tls/ws" + webRTCDirect = "/ip4/1.2.3.4/udp/4001/webrtc-direct/certhash/uEiDDq4_xKlbjmeSaEwB7UUcH_TS1z2WLwkGuOHZgKZgUSw" + webTransport = "/ip4/1.2.3.4/udp/4001/quic-v1/webtransport/certhash/uEiDDq4_xKlbjmeSaEwB7UUcH_TS1z2WLwkGuOHZgKZgUSw" + publicTCP = "/ip4/1.2.3.4/tcp/4001" + publicQUIC = "/ip4/1.2.3.4/udp/4001/quic-v1" + lanTCP = "/ip4/192.168.1.10/tcp/4001" + lanQUIC = "/ip4/192.168.1.10/udp/4001/quic-v1" + loopbackTCP = "/ip4/127.0.0.1/tcp/4001" + ) + publicNodeAddrs := []string{autoTLSWS, webRTCDirect, webTransport, publicTCP, publicQUIC, lanTCP, loopbackTCP} + lanOnlyAddrs := []string{lanTCP, lanQUIC} tests := []struct { name string - reachable []string // autonat confirmed addrs (nil = none) hostAddrs []string // host.Addrs() output (nil = none) + reachable []string // AutoNAT V2 confirmed set; must NOT influence the result cfg config.Addresses want []string }{ { - name: "prefers autonat confirmed reachable addrs over host.Addrs fallback", - reachable: []string{"/ip4/1.2.3.4/tcp/4001", "/ip4/1.2.3.4/udp/4001/quic-v1"}, - hostAddrs: resolvedAddrs, + // Regression test for https://github.com/ipfs/kubo/issues/11369: + // the record must be built from host.Addrs(), never from + // ConfirmedAddrs. reachable carries the narrowed set AutoNAT + // reports on such a node (no AutoTLS, no webrtc-direct); the + // browser-dialable addrs from hostAddrs must win. + name: "browser-dialable transports reach the record on a public node", + hostAddrs: publicNodeAddrs, + reachable: []string{publicTCP, publicQUIC, webTransport}, cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"}}, - want: []string{"/ip4/1.2.3.4/tcp/4001", "/ip4/1.2.3.4/udp/4001/quic-v1"}, + want: []string{autoTLSWS, webRTCDirect, webTransport, publicTCP, publicQUIC}, }, { - name: "falls back to host.Addrs when autonat has no confirmed addrs", - hostAddrs: resolvedAddrs, + name: "LAN-only node announces what it has", + hostAddrs: lanOnlyAddrs, cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"}}, - want: resolvedAddrs, + want: lanOnlyAddrs, + }, + { + name: "empty host.Addrs announces nothing", + hostAddrs: nil, + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}}, + want: nil, }, { - name: "Announce overrides autonat and host.Addrs", - reachable: []string{"/ip4/1.2.3.4/tcp/4001"}, - hostAddrs: resolvedAddrs, + name: "empty host.Addrs with AppendAnnounce announces AppendAnnounce", + hostAddrs: nil, + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, AppendAnnounce: []string{"/ip4/10.0.0.1/tcp/4001"}}, + want: []string{"/ip4/10.0.0.1/tcp/4001"}, + }, + { + name: "Announce overrides host.Addrs", + hostAddrs: publicNodeAddrs, cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, Announce: []string{"/ip4/5.6.7.8/tcp/4001"}}, want: []string{"/ip4/5.6.7.8/tcp/4001"}, }, { - name: "AppendAnnounce added to autonat addrs", - reachable: []string{"/ip4/1.2.3.4/tcp/4001"}, - hostAddrs: resolvedAddrs, + // AppendAnnounce is an explicit operator override, so it survives + // the public-address filter even when it is a LAN address. + name: "AppendAnnounce added to host.Addrs", + hostAddrs: []string{publicTCP, lanTCP}, cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, AppendAnnounce: []string{"/ip4/10.0.0.1/tcp/4001"}}, - want: []string{"/ip4/1.2.3.4/tcp/4001", "/ip4/10.0.0.1/tcp/4001"}, + want: []string{publicTCP, "/ip4/10.0.0.1/tcp/4001"}, }, { - name: "AppendAnnounce added to host.Addrs fallback", - hostAddrs: resolvedAddrs, + name: "AppendAnnounce added to LAN-only fallback", + hostAddrs: lanOnlyAddrs, cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, AppendAnnounce: []string{"/ip4/10.0.0.1/tcp/4001"}}, - want: append(append([]string{}, resolvedAddrs...), "/ip4/10.0.0.1/tcp/4001"), + want: append(append([]string{}, lanOnlyAddrs...), "/ip4/10.0.0.1/tcp/4001"), + }, + { + // The AddrsFactory injects AppendAnnounce into h.Addrs(), so the + // same addr arrives twice: once inside hostAddrs and once via the + // explicit re-append. The record must carry it exactly once. + name: "AppendAnnounce present in host.Addrs is not duplicated", + hostAddrs: []string{publicTCP, "/ip4/5.6.7.8/tcp/4001"}, + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, AppendAnnounce: []string{"/ip4/5.6.7.8/tcp/4001"}}, + want: []string{publicTCP, "/ip4/5.6.7.8/tcp/4001"}, + }, + { + // A public AppendAnnounce echoed back through h.Addrs() must not + // count as "the node has a public address" and evict the real + // LAN addresses of an otherwise private node. + name: "public AppendAnnounce does not evict LAN-only fallback addrs", + hostAddrs: append([]string{"/ip4/5.6.7.8/tcp/4001"}, lanOnlyAddrs...), + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, AppendAnnounce: []string{"/ip4/5.6.7.8/tcp/4001"}}, + want: append(append([]string{}, lanOnlyAddrs...), "/ip4/5.6.7.8/tcp/4001"), }, { // NoAnnounce (including server profile CIDR ranges) is applied by the // libp2p AddrsFactory before host.Addrs() returns, so httpRouterAddrFunc - // itself performs no filtering on the fallback. + // never sees the filtered addresses. name: "NoAnnounce filtering happens upstream in host.Addrs", - hostAddrs: []string{"/ip4/192.168.1.10/tcp/4001"}, // already filtered by addrFactory + hostAddrs: []string{lanTCP}, // already filtered by addrFactory cfg: config.Addresses{ Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, NoAnnounce: []string{"/ip4/127.0.0.0/ipcidr/8"}, }, - want: []string{"/ip4/192.168.1.10/tcp/4001"}, + want: []string{lanTCP}, }, { name: "AppendAnnounce added to Announce", cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, Announce: []string{"/ip4/5.6.7.8/tcp/4001"}, AppendAnnounce: []string{"/ip4/10.0.0.1/tcp/4001"}}, want: []string{"/ip4/5.6.7.8/tcp/4001", "/ip4/10.0.0.1/tcp/4001"}, }, + { + name: "AppendAnnounce overlapping Announce is not duplicated", + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, Announce: []string{"/ip4/5.6.7.8/tcp/4001"}, AppendAnnounce: []string{"/ip4/5.6.7.8/tcp/4001", "/ip4/10.0.0.1/tcp/4001"}}, + want: []string{"/ip4/5.6.7.8/tcp/4001", "/ip4/10.0.0.1/tcp/4001"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := &stubHost{ - reachable: parseMultiaddrs(tt.reachable), hostAddrs: parseMultiaddrs(tt.hostAddrs), + reachable: parseMultiaddrs(tt.reachable), } fn := httpRouterAddrFunc(h, tt.cfg) assert.Equal(t, parseMultiaddrs(tt.want), fn()) diff --git a/docs/changelogs/vTBD.md b/docs/changelogs/vTBD.md new file mode 100644 index 00000000000..9ce82083e11 --- /dev/null +++ b/docs/changelogs/vTBD.md @@ -0,0 +1,29 @@ +# Kubo changelog vTBD + + + +This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. + +- [vTBD](#vtbd) + +## vTBD + +- [Overview](#overview) +- [๐Ÿ”ฆ Highlights](#-highlights) + - [๐ŸŒ Browsers can dial you again through delegated routers](#-browsers-can-dial-you-again-through-delegated-routers) +- [๐Ÿ“ Changelog](#-changelog) +- [๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors](#-contributors) + +### Overview + +### ๐Ÿ”ฆ Highlights + +#### ๐ŸŒ Browsers can dial you again through delegated routers + +A publicly reachable node with [AutoTLS](https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls) or `webrtc-direct` enabled was leaving those addresses out of the provider records it sent to HTTP routers. The record carried only `tcp`, `quic-v1`, and `webtransport`, none of which a browser can dial, so browser and [Helia](https://helia.io/) clients that found the node through a delegated router had no way to connect to it, even though the node was listening and `ipfs id` showed the addresses. + +Provider records sent to HTTP routers now carry the same addresses the node announces everywhere else, including the AutoTLS `/tls/ws` and `webrtc-direct` ones, matching what the DHT already published. Loopback and LAN addresses stay out of the record when the node has a public address. See [#11369](https://github.com/ipfs/kubo/issues/11369). + +### ๐Ÿ“ Changelog + +### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 5196bfdddb7..80646cad609 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -9,7 +9,7 @@ replace github.com/ipfs/kubo => ./../../.. require ( github.com/ipfs/boxo v0.41.1-0.20260707130650-177b35c8ef09 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 - github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014 + github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646 github.com/multiformats/go-multiaddr v0.16.1 ) diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 09f3bb1945d..c72f55bfe63 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -411,8 +411,8 @@ github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZ github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014 h1:RYkdiaWH3aPT7m98atoCw+MD2YJOHfUi16ZaXaP3yGg= -github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= +github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646 h1:B7ql/sbL+R6vpJuQeophg8juH1tGshyTx4xKokpeSRE= +github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= diff --git a/go.mod b/go.mod index 73409f11599..2bd3ad453e3 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/jbenet/go-temp-err-catcher v0.1.0 github.com/julienschmidt/httprouter v1.3.0 github.com/libp2p/go-doh-resolver v0.5.0 - github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014 // TODO: switch to a tagged release once one ships past v0.48.0 + github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646 // TODO: switch to a tagged release once one ships past v0.48.0 github.com/libp2p/go-libp2p-http v0.5.0 github.com/libp2p/go-libp2p-kad-dht v0.41.0 github.com/libp2p/go-libp2p-kbucket v0.8.0 diff --git a/go.sum b/go.sum index 138bdb034fa..d9f9713b7dc 100644 --- a/go.sum +++ b/go.sum @@ -494,8 +494,8 @@ github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZ github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014 h1:RYkdiaWH3aPT7m98atoCw+MD2YJOHfUi16ZaXaP3yGg= -github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= +github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646 h1:B7ql/sbL+R6vpJuQeophg8juH1tGshyTx4xKokpeSRE= +github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= diff --git a/test/cli/delegated_routing_v1_http_client_test.go b/test/cli/delegated_routing_v1_http_client_test.go index cfa347565f7..340b5a1b069 100644 --- a/test/cli/delegated_routing_v1_http_client_test.go +++ b/test/cli/delegated_routing_v1_http_client_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "slices" "strings" "sync" "testing" @@ -272,7 +273,49 @@ func TestHTTPDelegatedRoutingProviderAddrs(t *testing.T) { addrs := getAddrs() require.NotEmpty(t, addrs, "provider record should contain addresses") - assert.Contains(t, addrs, "/ip4/5.6.7.8/tcp/4001", "AppendAnnounce address should be present") + // Exactly once: the AddrsFactory injects AppendAnnounce into + // host.Addrs() and httpRouterAddrFunc re-appends it, so a dedup bug + // shows up as a duplicate entry here. + count := 0 + for _, a := range addrs { + if a == "/ip4/5.6.7.8/tcp/4001" { + count++ + } + } + assert.Equal(t, 1, count, "AppendAnnounce address should appear exactly once, got: %v", addrs) + }) + + t.Run("provider records keep browser-dialable transports", func(t *testing.T) { + t.Parallel() + srv, getAddrs := captureProviderAddrs(t) + + // Browsers can only dial webrtc-direct and secure WebSockets, so + // dropping those from the record leaves a browser client with nothing + // to connect to. See https://github.com/ipfs/kubo/issues/11369. + // + // The node here is loopback-only, so it never gets an AutoNAT V2 + // confirmation and cannot reproduce #11369 on its own. What this pins + // is that a webrtc-direct address, certhash and all, survives the trip + // into the PUT payload. + node := harness.NewT(t).NewNode().Init() + node.SetIPFSConfig("Addresses.Swarm", []string{ + "/ip4/127.0.0.1/tcp/0", + "/ip4/127.0.0.1/udp/0/webrtc-direct", + }) + node.SetIPFSConfig("Routing", customRoutingConf(srv.URL)) + node.StartDaemon() + defer node.StopDaemon() + + cidStr := node.IPFSAddStr(time.Now().String()) + node.IPFS("routing", "provide", cidStr) + + addrs := getAddrs() + require.NotEmpty(t, addrs, "provider record should contain addresses") + idx := slices.IndexFunc(addrs, func(a string) bool { + return strings.Contains(a, "/webrtc-direct/") + }) + require.NotEqual(t, -1, idx, "webrtc-direct address missing from provider record: %v", addrs) + assert.Contains(t, addrs[idx], "/certhash/", "webrtc-direct address is undialable without a certhash: %s", addrs[idx]) }) t.Run("provider records resolve 0.0.0.0 Swarm bind to interface addresses", func(t *testing.T) { diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 04a7f0d2b33..6d7af3cfe7f 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -183,7 +183,7 @@ require ( github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect - github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014 // indirect + github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-libp2p-kad-dht v0.41.0 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 9e82d4f6e0c..38d7992a2ef 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -426,8 +426,8 @@ github.com/libp2p/go-doh-resolver v0.5.0 h1:4h7plVVW+XTS+oUBw2+8KfoM1jF6w8XmO7+s github.com/libp2p/go-doh-resolver v0.5.0/go.mod h1:aPDxfiD2hNURgd13+hfo29z9IC22fv30ee5iM31RzxU= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014 h1:RYkdiaWH3aPT7m98atoCw+MD2YJOHfUi16ZaXaP3yGg= -github.com/libp2p/go-libp2p v0.48.1-0.20260708062241-95be6665b014/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= +github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646 h1:B7ql/sbL+R6vpJuQeophg8juH1tGshyTx4xKokpeSRE= +github.com/libp2p/go-libp2p v0.48.1-0.20260715204039-42aa212c5646/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-kad-dht v0.41.0 h1:sDigz5SgV20Crj8ItJmJpEAM+eJrzC/SaiBweg0gDRw=