From ea5d9be020fc720dae0f40f5a7e268b5ad61b8ff Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 15 Jul 2026 23:19:49 +0200 Subject: [PATCH 1/2] chore: bump go-libp2p for sorted confirmed addrs Pin the head commit of libp2p/go-libp2p#3526: AutoNAT V2's ConfirmedAddrs returned unsorted buckets, and removeNotInSource silently dropped webrtc-direct from the confirmed set. Switch to a master pseudo-version once the PR merges. --- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) 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/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= From 6a8ac637616d9844f4f9dd64f2c7c075c5edc5cc Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 15 Jul 2026 23:20:13 +0200 Subject: [PATCH 2/2] fix: keep browser transports in provider records Provider records sent to HTTP routers were narrowed to the addresses AutoNAT V2 confirmed reachable, which silently dropped the only two transports a browser can dial: the AutoTLS /tls/ws address and webrtc-direct. A publicly reachable node was invisible to browser and Helia clients that found it through a delegated router, even though ipfs id and the DHT both advertised those addresses. AutoNAT only ever sees listen addresses, so the AutoTLS address, which the AddrsFactory synthesizes afterwards, can never reach the confirmed set. webrtc-direct does get confirmed, but go-libp2p loses it again in getConfirmedAddrs, which feeds an unsorted slice to a scan that assumes sorted input; that one is fixed upstream in libp2p/go-libp2p#3526. Announce host.Addrs() instead, the same set identify sends to peers and the DHT already publishes, narrowed to globally routable addresses so loopback and LAN entries stay out of a public index. Nodes with no public address keep announcing what they have, so LAN-only setups pointing at a local router are unaffected. - core/node/libp2p/routingopt.go: drop the ConfirmedAddrs branch from httpRouterAddrFunc, filter host.Addrs() with manet.IsPublicAddr; AppendAnnounce is emitted exactly once and does not count toward the public-addr check - core/commands/swarm_addrs_autonat.go: take over the BasicHost compile-time assertion, now the only ConfirmedAddrs consumer Fixes #11369 --- core/commands/swarm_addrs_autonat.go | 8 ++ core/node/libp2p/routingopt.go | 78 ++++++------ core/node/libp2p/routingopt_test.go | 120 +++++++++++++----- docs/changelogs/vTBD.md | 29 +++++ .../delegated_routing_v1_http_client_test.go | 45 ++++++- 5 files changed, 210 insertions(+), 70 deletions(-) create mode 100644 docs/changelogs/vTBD.md 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/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) {