@cryptotaxi247 / kubo / commits / 9c4e2683e

fix: correct provider addresses for custom HTTP routing (#11234)

* fix: resolve provider addresses dynamically for HTTP routing HTTP routing provider records now use addresses resolved at provide-time instead of static config values captured at daemon startup. This fixes nodes with default 0.0.0.0 swarm addresses sending unresolved addresses to HTTP routers. - routing/delegated.go: add AddrFunc to ExtraHTTPParams, use WithProviderInfoFunc when set - core/node/libp2p/routingopt.go: add httpRouterAddrFunc that prefers AutoNAT V2 confirmed reachable addrs, falls back to static config; Addresses.Announce is a full static override, AppendAnnounce is appended to dynamic/fallback results - bump boxo to ipfs/boxo#1115 (WithProviderInfoFunc support) Fixes #11213 * refactor: remove redundant static addrs from HTTP routing Now that AddrFunc is always set by all callers, the static Addrs field and httpAddrsFromConfig are dead weight. - routing/delegated.go: remove Addrs from ExtraHTTPParams and addrs param from ConstructHTTPRouter, remove unused createAddrInfo - core/node/libp2p/routingopt.go: remove httpAddrsFromConfig, drop redundant httpAddrsFromConfig calls from all callers - core/node/libp2p/routingopt_test.go: remove TestHttpAddrsFromConfig (covered by TestHttpRouterAddrFunc), convert to table-driven tests * fix: always append AppendAnnounce addrs in HTTP routing AppendAnnounce was silently dropped when Announce was set, breaking the documented behavior that AppendAnnounce is always appended. * refactor: add compile-time assertion for confirmedAddrsHost * fix: harden parseMultiaddrs and prevent static addr slice aliasing - log error on invalid multiaddr strings instead of silently dropping - return cloned slice in static Announce path to prevent caller mutation * perf: reduce allocations in httpRouterAddrFunc closure - precompute fallback result (both parts immutable, no per-call concat) - cache type assertion at construction time - skip concat when AppendAnnounce is empty - return static Announce slice directly (boxo already copies) * chore: update boxo to main with ipfs/boxo#1115 merged * docs: clarify changelog entry for custom HTTP routing provider addresses

Marcin Rataj committed Mar 18, 2026 at 01:24 UTC 9c4e2683e00995533eb374771fd574ec5b4a002f
12 files changed +312 -90
core/node/libp2p/routing.go
+2 -1
@@ -122,7 +122,8 @@ func BaseRouting(cfg *config.Config) any {
122
123 // we want to also use the default HTTP routers, so wrap the FullRT client
124 // in a parallel router that calls them in parallel
125 - httpRouters, err := constructDefaultHTTPRouters(cfg)
125 + addrFunc := httpRouterAddrFunc(in.Host, cfg.Addresses)
126 + httpRouters, err := constructDefaultHTTPRouters(cfg, addrFunc)
127 if err != nil {
128 return out, err
129 }
core/node/libp2p/routingopt.go
+78 -24
@@ -4,6 +4,7 @@ import (
4 "context"
5 "fmt"
6 "os"
7 + "slices"
8 "strings"
9 "time"
10
@@ -18,6 +19,8 @@ import (
19 host "github.com/libp2p/go-libp2p/core/host"
20 "github.com/libp2p/go-libp2p/core/peer"
21 routing "github.com/libp2p/go-libp2p/core/routing"
22 + basichost "github.com/libp2p/go-libp2p/p2p/host/basic"
23 + ma "github.com/multiformats/go-multiaddr"
24 )
25
26 type RoutingOptionArgs struct {
@@ -105,7 +108,7 @@ func collectAllEndpoints(cfg *config.Config) []EndpointSource {
108 return endpoints
109 }
110
108 -func constructDefaultHTTPRouters(cfg *config.Config) ([]*routinghelpers.ParallelRouter, error) {
111 +func constructDefaultHTTPRouters(cfg *config.Config, addrFunc func() []ma.Multiaddr) ([]*routinghelpers.ParallelRouter, error) {
112 var routers []*routinghelpers.ParallelRouter
113 httpRetrievalEnabled := cfg.HTTPRetrieval.Enabled.WithDefault(config.DefaultHTTPRetrievalEnabled)
114
@@ -130,7 +133,7 @@ func constructDefaultHTTPRouters(cfg *config.Config) ([]*routinghelpers.Parallel
133 // Create single HTTP router and composer per origin
134 for baseURL, capabilities := range originCapabilities {
135 // Construct HTTP router using base URL (without path)
133 - httpRouter, err := irouting.ConstructHTTPRouter(baseURL, cfg.Identity.PeerID, httpAddrsFromConfig(cfg.Addresses), cfg.Identity.PrivKey, httpRetrievalEnabled)
136 + httpRouter, err := irouting.ConstructHTTPRouter(baseURL, cfg.Identity.PeerID, addrFunc, cfg.Identity.PrivKey, httpRetrievalEnabled)
137 if err != nil {
138 return nil, err
139 }
@@ -191,7 +194,8 @@ func ConstructDelegatedOnlyRouting(cfg *config.Config) RoutingOption {
194 var routers []*routinghelpers.ParallelRouter
195
196 // Add HTTP delegated routers (includes both router and publisher capabilities)
194 - httpRouters, err := constructDefaultHTTPRouters(cfg)
197 + addrFunc := httpRouterAddrFunc(args.Host, cfg.Addresses)
198 + httpRouters, err := constructDefaultHTTPRouters(cfg, addrFunc)
199 if err != nil {
200 return nil, err
201 }
@@ -225,7 +229,8 @@ func ConstructDefaultRouting(cfg *config.Config, routingOpt RoutingOption) Routi
229 ExecuteAfter: 0,
230 })
231
228 - httpRouters, err := constructDefaultHTTPRouters(cfg)
232 + addrFunc := httpRouterAddrFunc(args.Host, cfg.Addresses)
233 + httpRouters, err := constructDefaultHTTPRouters(cfg, addrFunc)
234 if err != nil {
235 return nil, err
236 }
@@ -271,6 +276,7 @@ func constructDHTRouting(mode dht.ModeOpt) RoutingOption {
276 // ConstructDelegatedRouting is used when Routing.Type = "custom"
277 func ConstructDelegatedRouting(routers config.Routers, methods config.Methods, peerID string, addrs config.Addresses, privKey string, httpRetrieval bool) RoutingOption {
278 return func(args RoutingOptionArgs) (routing.Routing, error) {
279 + addrFunc := httpRouterAddrFunc(args.Host, addrs)
280 return irouting.Parse(routers, methods,
281 &irouting.ExtraDHTParams{
282 BootstrapPeers: args.BootstrapPeers,
@@ -281,7 +287,7 @@ func ConstructDelegatedRouting(routers config.Routers, methods config.Methods, p
287 },
288 &irouting.ExtraHTTPParams{
289 PeerID: peerID,
284 - Addrs: httpAddrsFromConfig(addrs),
290 + AddrFunc: addrFunc,
291 PrivKeyB64: privKey,
292 HTTPRetrieval: httpRetrieval,
293 },
@@ -300,30 +306,78 @@ var (
306 NilRouterOption = constructNilRouting
307 )
308
303 -// httpAddrsFromConfig creates a list of addresses from the provided configuration to be used by HTTP delegated routers.
304 -func httpAddrsFromConfig(cfgAddrs config.Addresses) []string {
305 - // Swarm addrs are announced by default
306 - addrs := cfgAddrs.Swarm
307 - // if Announce addrs are specified - override Swarm
309 +// confirmedAddrsHost matches libp2p hosts that support AutoNAT V2 address confirmation.
310 +type confirmedAddrsHost interface {
311 + ConfirmedAddrs() (reachable, unreachable, unknown []ma.Multiaddr)
312 +}
313 +
314 +// Compile-time check: BasicHost must satisfy confirmedAddrsHost.
315 +// ConfirmedAddrs is not part of the core host.Host interface and is marked
316 +// experimental in go-libp2p. If BasicHost ever drops or changes this method,
317 +// this assertion will fail at build time. In that case, update
318 +// httpRouterAddrFunc (this file) and the swarm autonat command
319 +// (core/commands/swarm_addrs_autonat.go) which both type-assert to this
320 +// interface.
321 +var _ confirmedAddrsHost = (*basichost.BasicHost)(nil)
322 +
323 +// httpRouterAddrFunc returns a function that resolves provider addresses for
324 +// HTTP routers at provide-time.
325 +//
326 +// Resolution logic:
327 +// - If Announce is set, use it as a static override (no dynamic resolution).
328 +// - Otherwise, prefer AutoNAT V2 confirmed reachable addresses when available,
329 +// falling back to static Swarm addresses (filtered by NoAnnounce).
330 +// - AppendAnnounce addresses are always appended.
331 +func httpRouterAddrFunc(h host.Host, cfgAddrs config.Addresses) func() []ma.Multiaddr {
332 + appendAddrs := parseMultiaddrs(cfgAddrs.AppendAnnounce)
333 +
334 + // If Announce is explicitly set, use it as a static override.
335 if len(cfgAddrs.Announce) > 0 {
309 - addrs = cfgAddrs.Announce
310 - } else if len(cfgAddrs.NoAnnounce) > 0 {
311 - // if Announce adds are not specified - filter Swarm addrs with NoAnnounce list
312 - maddrs := map[string]struct{}{}
313 - for _, addr := range addrs {
314 - maddrs[addr] = struct{}{}
336 + staticAddrs := slices.Concat(parseMultiaddrs(cfgAddrs.Announce), appendAddrs)
337 + return func() []ma.Multiaddr { return staticAddrs }
338 + }
339 +
340 + // Precompute fallback: Swarm minus NoAnnounce plus AppendAnnounce.
341 + fallbackStrs := cfgAddrs.Swarm
342 + if len(cfgAddrs.NoAnnounce) > 0 {
343 + noAnnounce := map[string]struct{}{}
344 + for _, a := range cfgAddrs.NoAnnounce {
345 + noAnnounce[a] = struct{}{}
346 }
316 - for _, addr := range cfgAddrs.NoAnnounce {
317 - delete(maddrs, addr)
347 + filtered := make([]string, 0, len(fallbackStrs))
348 + for _, a := range fallbackStrs {
349 + if _, skip := noAnnounce[a]; !skip {
350 + filtered = append(filtered, a)
351 + }
352 }
319 - addrs = make([]string, 0, len(maddrs))
320 - for k := range maddrs {
321 - addrs = append(addrs, k)
353 + fallbackStrs = filtered
354 + }
355 + fallbackResult := slices.Concat(parseMultiaddrs(fallbackStrs), appendAddrs)
356 +
357 + ch, hasConfirmed := h.(confirmedAddrsHost)
358 + return func() []ma.Multiaddr {
359 + if hasConfirmed {
360 + reachable, _, _ := ch.ConfirmedAddrs()
361 + if len(reachable) > 0 {
362 + if len(appendAddrs) == 0 {
363 + return reachable
364 + }
365 + return slices.Concat(reachable, appendAddrs)
366 + }
367 }
368 + return fallbackResult
369 }
324 - // append AppendAnnounce addrs to the result list
325 - if len(cfgAddrs.AppendAnnounce) > 0 {
326 - addrs = append(addrs, cfgAddrs.AppendAnnounce...)
370 +}
371 +
372 +func parseMultiaddrs(strs []string) []ma.Multiaddr {
373 + addrs := make([]ma.Multiaddr, 0, len(strs))
374 + for _, s := range strs {
375 + a, err := ma.NewMultiaddr(s)
376 + if err != nil {
377 + log.Errorf("ignoring invalid multiaddr %q: %s", s, err)
378 + continue
379 + }
380 + addrs = append(addrs, a)
381 }
382 return addrs
383 }
core/node/libp2p/routingopt_test.go
+92 -26
@@ -1,40 +1,22 @@
1 package libp2p
2
3 import (
4 + "context"
5 "testing"
6
7 "github.com/ipfs/boxo/autoconf"
8 config "github.com/ipfs/kubo/config"
9 + "github.com/libp2p/go-libp2p/core/connmgr"
10 + "github.com/libp2p/go-libp2p/core/event"
11 + "github.com/libp2p/go-libp2p/core/network"
12 + "github.com/libp2p/go-libp2p/core/peer"
13 + "github.com/libp2p/go-libp2p/core/peerstore"
14 + "github.com/libp2p/go-libp2p/core/protocol"
15 + ma "github.com/multiformats/go-multiaddr"
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18 )
19
12 -func TestHttpAddrsFromConfig(t *testing.T) {
13 - require.Equal(t, []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"},
14 - httpAddrsFromConfig(config.Addresses{
15 - Swarm: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"},
16 - }), "Swarm addrs should be taken by default")
17 -
18 - require.Equal(t, []string{"/ip4/192.168.0.1/tcp/4001"},
19 - httpAddrsFromConfig(config.Addresses{
20 - Swarm: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"},
21 - Announce: []string{"/ip4/192.168.0.1/tcp/4001"},
22 - }), "Announce addrs should override Swarm if specified")
23 -
24 - require.Equal(t, []string{"/ip4/0.0.0.0/udp/4001/quic-v1"},
25 - httpAddrsFromConfig(config.Addresses{
26 - Swarm: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"},
27 - NoAnnounce: []string{"/ip4/0.0.0.0/tcp/4001"},
28 - }), "Swarm addrs should not contain NoAnnounce addrs")
29 -
30 - require.Equal(t, []string{"/ip4/192.168.0.1/tcp/4001", "/ip4/192.168.0.2/tcp/4001"},
31 - httpAddrsFromConfig(config.Addresses{
32 - Swarm: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"},
33 - Announce: []string{"/ip4/192.168.0.1/tcp/4001"},
34 - AppendAnnounce: []string{"/ip4/192.168.0.2/tcp/4001"},
35 - }), "AppendAnnounce addrs should be included if specified")
36 -}
37 -
20 func TestDetermineCapabilities(t *testing.T) {
21 tests := []struct {
22 name string
@@ -222,3 +204,87 @@ func TestEndpointCapabilitiesReadWriteLogic(t *testing.T) {
204 assert.False(t, capabilities.IPNSPut)
205 })
206 }
207 +
208 +// stubHost is a minimal host.Host stub for testing httpRouterAddrFunc.
209 +// Only the methods checked via type assertion (confirmedAddrsHost) matter;
210 +// all other methods panic if called.
211 +type stubHost struct {
212 + reachable []ma.Multiaddr
213 +}
214 +
215 +func (h *stubHost) ConfirmedAddrs() (reachable, unreachable, unknown []ma.Multiaddr) {
216 + return h.reachable, nil, nil
217 +}
218 +
219 +func (h *stubHost) ID() peer.ID { panic("unused") }
220 +func (h *stubHost) Addrs() []ma.Multiaddr { panic("unused") }
221 +func (h *stubHost) Peerstore() peerstore.Peerstore { panic("unused") }
222 +func (h *stubHost) Network() network.Network { panic("unused") }
223 +func (h *stubHost) Mux() protocol.Switch { panic("unused") }
224 +func (h *stubHost) Connect(context.Context, peer.AddrInfo) error { panic("unused") }
225 +func (h *stubHost) SetStreamHandler(protocol.ID, network.StreamHandler) { panic("unused") }
226 +func (h *stubHost) SetStreamHandlerMatch(protocol.ID, func(protocol.ID) bool, network.StreamHandler) {
227 + panic("unused")
228 +}
229 +func (h *stubHost) RemoveStreamHandler(protocol.ID) { panic("unused") }
230 +func (h *stubHost) NewStream(context.Context, peer.ID, ...protocol.ID) (network.Stream, error) {
231 + panic("unused")
232 +}
233 +func (h *stubHost) Close() error { panic("unused") }
234 +func (h *stubHost) ConnManager() connmgr.ConnManager { panic("unused") }
235 +func (h *stubHost) EventBus() event.Bus { panic("unused") }
236 +
237 +func TestHttpRouterAddrFunc(t *testing.T) {
238 + tests := []struct {
239 + name string
240 + reachable []string // autonat confirmed addrs (nil = none)
241 + cfg config.Addresses
242 + want []string
243 + }{
244 + {
245 + name: "prefers autonat confirmed reachable addrs over swarm fallback",
246 + reachable: []string{"/ip4/1.2.3.4/tcp/4001", "/ip4/1.2.3.4/udp/4001/quic-v1"},
247 + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"}},
248 + want: []string{"/ip4/1.2.3.4/tcp/4001", "/ip4/1.2.3.4/udp/4001/quic-v1"},
249 + },
250 + {
251 + name: "falls back to swarm when autonat has no confirmed addrs",
252 + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}},
253 + want: []string{"/ip4/0.0.0.0/tcp/4001"},
254 + },
255 + {
256 + name: "Announce overrides autonat and swarm",
257 + reachable: []string{"/ip4/1.2.3.4/tcp/4001"},
258 + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, Announce: []string{"/ip4/5.6.7.8/tcp/4001"}},
259 + want: []string{"/ip4/5.6.7.8/tcp/4001"},
260 + },
261 + {
262 + name: "AppendAnnounce added to autonat addrs",
263 + reachable: []string{"/ip4/1.2.3.4/tcp/4001"},
264 + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, AppendAnnounce: []string{"/ip4/10.0.0.1/tcp/4001"}},
265 + want: []string{"/ip4/1.2.3.4/tcp/4001", "/ip4/10.0.0.1/tcp/4001"},
266 + },
267 + {
268 + name: "AppendAnnounce added to swarm fallback",
269 + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001"}, AppendAnnounce: []string{"/ip4/10.0.0.1/tcp/4001"}},
270 + want: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/10.0.0.1/tcp/4001"},
271 + },
272 + {
273 + name: "NoAnnounce filters swarm fallback",
274 + cfg: config.Addresses{Swarm: []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"}, NoAnnounce: []string{"/ip4/0.0.0.0/tcp/4001"}},
275 + want: []string{"/ip4/0.0.0.0/udp/4001/quic-v1"},
276 + },
277 + {
278 + name: "AppendAnnounce added to Announce",
279 + 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"}},
280 + want: []string{"/ip4/5.6.7.8/tcp/4001", "/ip4/10.0.0.1/tcp/4001"},
281 + },
282 + }
283 + for _, tt := range tests {
284 + t.Run(tt.name, func(t *testing.T) {
285 + h := &stubHost{reachable: parseMultiaddrs(tt.reachable)}
286 + fn := httpRouterAddrFunc(h, tt.cfg)
287 + assert.Equal(t, parseMultiaddrs(tt.want), fn())
288 + })
289 + }
290 +}
docs/changelogs/v0.41.md
+5
@@ -11,6 +11,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
11 - [Overview](#overview)
12 - [🔦 Highlights](#-highlights)
13 - [🖥️ WebUI Improvements](#-webui-improvements)
14 + - [🔧 Correct provider addresses for custom HTTP routing](#-correct-provider-addresses-for-custom-http-routing)
15 - [📦️ Dependency updates](#-dependency-updates)
16 - [📝 Changelog](#-changelog)
17 - [👨‍👩‍👧‍👦 Contributors](#-contributors)
@@ -29,6 +30,10 @@ The Peers screen now resolves IPv6 addresses to geographic locations, and the ge
30
31 Peer locations load faster thanks to UX optimizations in the underlying ipfs-geoip library.
32
33 +#### 🔧 Correct provider addresses for custom HTTP routing
34 +
35 +Nodes using custom routing (`Routing.Type=custom`) with [IPIP-526](https://github.com/ipfs/specs/pull/526) could end up publishing unresolved `0.0.0.0` addresses in provider records. Addresses are now resolved at provide-time, and when AutoNAT V2 has confirmed publicly reachable addresses, those are preferred automatically. See [#11213](https://github.com/ipfs/kubo/issues/11213).
36 +
37 #### 📦️ Dependency updates
38
39 - update `ipfs-webui` to [v4.12.0](https://github.com/ipfs/ipfs-webui/releases/tag/v4.12.0)
docs/examples/kubo-as-a-library/go.mod
+1 -1
@@ -7,7 +7,7 @@ go 1.25.0
7 replace github.com/ipfs/kubo => ./../../..
8
9 require (
10 - github.com/ipfs/boxo v0.37.0
10 + github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422
11 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
12 github.com/libp2p/go-libp2p v0.47.0
13 github.com/multiformats/go-multiaddr v0.16.1
docs/examples/kubo-as-a-library/go.sum
+2 -2
@@ -346,8 +346,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd
346 github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU=
347 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
348 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
349 -github.com/ipfs/boxo v0.37.0 h1:2E3mZvydMI2t5IkAgtkmZ3sGsld0oS7o3I+xyzDk6uI=
350 -github.com/ipfs/boxo v0.37.0/go.mod h1:8yyiRn54F2CsW13n0zwXEPrVsZix/gFj9SYIRYMZ6KE=
349 +github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422 h1:yY3ot/DU1bqTzHDBARACM76Tbx9s4xzcRbzifG1e/es=
350 +github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422/go.mod h1:8yyiRn54F2CsW13n0zwXEPrVsZix/gFj9SYIRYMZ6KE=
351 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
352 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
353 github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk=
go.mod
+1 -1
@@ -21,7 +21,7 @@ require (
21 github.com/hashicorp/go-version v1.8.0
22 github.com/ipfs-shipyard/nopfs v0.0.14
23 github.com/ipfs-shipyard/nopfs/ipfs v0.25.0
24 - github.com/ipfs/boxo v0.37.0
24 + github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422
25 github.com/ipfs/go-block-format v0.2.3
26 github.com/ipfs/go-cid v0.6.0
27 github.com/ipfs/go-cidutil v0.1.1
go.sum
+2 -2
@@ -386,8 +386,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd
386 github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU=
387 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
388 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
389 -github.com/ipfs/boxo v0.37.0 h1:2E3mZvydMI2t5IkAgtkmZ3sGsld0oS7o3I+xyzDk6uI=
390 -github.com/ipfs/boxo v0.37.0/go.mod h1:8yyiRn54F2CsW13n0zwXEPrVsZix/gFj9SYIRYMZ6KE=
389 +github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422 h1:yY3ot/DU1bqTzHDBARACM76Tbx9s4xzcRbzifG1e/es=
390 +github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422/go.mod h1:8yyiRn54F2CsW13n0zwXEPrVsZix/gFj9SYIRYMZ6KE=
391 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
392 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
393 github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk=
routing/delegated.go
+15 -30
@@ -160,12 +160,12 @@ func parse(visited map[string]bool,
160
161 type ExtraHTTPParams struct {
162 PeerID string
163 - Addrs []string
163 + AddrFunc func() []ma.Multiaddr // dynamic address resolver for provider records
164 PrivKeyB64 string
165 HTTPRetrieval bool
166 }
167
168 -func ConstructHTTPRouter(endpoint string, peerID string, addrs []string, privKey string, httpRetrieval bool) (routing.Routing, error) {
168 +func ConstructHTTPRouter(endpoint string, peerID string, addrFunc func() []ma.Multiaddr, privKey string, httpRetrieval bool) (routing.Routing, error) {
169 return httpRoutingFromConfig(
170 config.Router{
171 Type: "http",
@@ -175,7 +175,7 @@ func ConstructHTTPRouter(endpoint string, peerID string, addrs []string, privKey
175 },
176 &ExtraHTTPParams{
177 PeerID: peerID,
178 - Addrs: addrs,
178 + AddrFunc: addrFunc,
179 PrivKeyB64: privKey,
180 HTTPRetrieval: httpRetrieval,
181 },
@@ -226,21 +226,28 @@ func httpRoutingFromConfig(conf config.Router, extraHTTP *ExtraHTTPParams) (rout
226 return nil, err
227 }
228
229 - addrInfo, err := createAddrInfo(extraHTTP.PeerID, extraHTTP.Addrs)
229 + protocols := config.DefaultHTTPRoutersFilterProtocols
230 + if extraHTTP.HTTPRetrieval {
231 + protocols = append(protocols, "transport-ipfs-gateway-http")
232 + }
233 +
234 + peerID, err := peer.Decode(extraHTTP.PeerID)
235 if err != nil {
236 return nil, err
237 }
238
234 - protocols := config.DefaultHTTPRoutersFilterProtocols
235 - if extraHTTP.HTTPRetrieval {
236 - protocols = append(protocols, "transport-ipfs-gateway-http")
239 + var providerInfoOpt drclient.Option
240 + if extraHTTP.AddrFunc != nil {
241 + providerInfoOpt = drclient.WithProviderInfoFunc(peerID, extraHTTP.AddrFunc)
242 + } else {
243 + providerInfoOpt = drclient.WithProviderInfo(peerID, nil)
244 }
245
246 cli, err := drclient.New(
247 params.Endpoint,
248 drclient.WithHTTPClient(delegateHTTPClient),
249 drclient.WithIdentity(key),
243 - drclient.WithProviderInfo(addrInfo.ID, addrInfo.Addrs),
250 + providerInfoOpt,
251 drclient.WithUserAgent(version.GetUserAgentVersion()),
252 drclient.WithProtocolFilter(protocols),
253 drclient.WithStreamResultsRequired(), // https://specs.ipfs.tech/routing/http-routing-v1/#streaming
@@ -278,28 +285,6 @@ func decodePrivKey(keyB64 string) (ic.PrivKey, error) {
285 return ic.UnmarshalPrivateKey(pk)
286 }
287
281 -func createAddrInfo(peerID string, addrs []string) (peer.AddrInfo, error) {
282 - pID, err := peer.Decode(peerID)
283 - if err != nil {
284 - return peer.AddrInfo{}, err
285 - }
286 -
287 - var mas []ma.Multiaddr
288 - for _, a := range addrs {
289 - m, err := ma.NewMultiaddr(a)
290 - if err != nil {
291 - return peer.AddrInfo{}, err
292 - }
293 -
294 - mas = append(mas, m)
295 - }
296 -
297 - return peer.AddrInfo{
298 - ID: pID,
299 - Addrs: mas,
300 - }, nil
301 -}
302 -
288 type ExtraDHTParams struct {
289 BootstrapPeers []peer.AddrInfo
290 Host host.Host
test/cli/delegated_routing_v1_http_client_test.go
+111
@@ -1,14 +1,20 @@
1 package cli
2
3 import (
4 + "encoding/json"
5 + "io"
6 "net/http"
7 "net/http/httptest"
8 + "strings"
9 + "sync"
10 "testing"
11 + "time"
12
13 "github.com/ipfs/kubo/config"
14 "github.com/ipfs/kubo/test/cli/harness"
15 . "github.com/ipfs/kubo/test/cli/testutils"
16 "github.com/stretchr/testify/assert"
17 + "github.com/stretchr/testify/require"
18 )
19
20 func TestHTTPDelegatedRouting(t *testing.T) {
@@ -164,3 +170,108 @@ func TestHTTPDelegatedRouting(t *testing.T) {
170 assert.Contains(t, resp.Body, "routing_http_client_length_count")
171 })
172 }
173 +
174 +// TestHTTPDelegatedRoutingProviderAddrs verifies that provider records sent to
175 +// HTTP routers contain the expected addresses based on Addresses configuration.
176 +// See https://github.com/ipfs/kubo/issues/11213
177 +func TestHTTPDelegatedRoutingProviderAddrs(t *testing.T) {
178 + t.Parallel()
179 +
180 + // captureProviderAddrs returns a mock server and a function to retrieve captured addresses.
181 + captureProviderAddrs := func(t *testing.T) (*httptest.Server, func() []string) {
182 + t.Helper()
183 + var mu sync.Mutex
184 + var capturedAddrs []string
185 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
186 + if (r.Method == http.MethodPut || r.Method == http.MethodPost) &&
187 + strings.HasPrefix(r.URL.Path, "/routing/v1/providers") {
188 + body, _ := io.ReadAll(r.Body)
189 + var envelope struct {
190 + Providers []struct {
191 + Payload json.RawMessage `json:"Payload"`
192 + } `json:"Providers"`
193 + }
194 + if json.Unmarshal(body, &envelope) == nil {
195 + for _, prov := range envelope.Providers {
196 + var payload struct {
197 + Addrs []string `json:"Addrs"`
198 + }
199 + if json.Unmarshal(prov.Payload, &payload) == nil && len(payload.Addrs) > 0 {
200 + mu.Lock()
201 + capturedAddrs = payload.Addrs
202 + mu.Unlock()
203 + }
204 + }
205 + }
206 + w.WriteHeader(http.StatusOK)
207 + return
208 + }
209 + if strings.HasPrefix(r.URL.Path, "/routing/v1/") {
210 + w.WriteHeader(http.StatusOK)
211 + return
212 + }
213 + w.WriteHeader(http.StatusNotFound)
214 + }))
215 + t.Cleanup(srv.Close)
216 + return srv, func() []string {
217 + mu.Lock()
218 + defer mu.Unlock()
219 + return capturedAddrs
220 + }
221 + }
222 +
223 + customRoutingConf := func(endpoint string) map[string]any {
224 + return map[string]any{
225 + "Type": "custom",
226 + "Methods": map[string]any{
227 + "provide": map[string]any{"RouterName": "TestRouter"},
228 + "find-providers": map[string]any{"RouterName": "TestRouter"},
229 + "find-peers": map[string]any{"RouterName": "TestRouter"},
230 + "get-ipns": map[string]any{"RouterName": "TestRouter"},
231 + "put-ipns": map[string]any{"RouterName": "TestRouter"},
232 + },
233 + "Routers": map[string]any{
234 + "TestRouter": map[string]any{
235 + "Type": "http",
236 + "Parameters": map[string]any{"Endpoint": endpoint},
237 + },
238 + },
239 + }
240 + }
241 +
242 + t.Run("provider records respect user-provided Addresses.Announce override", func(t *testing.T) {
243 + t.Parallel()
244 + srv, getAddrs := captureProviderAddrs(t)
245 +
246 + node := harness.NewT(t).NewNode().Init()
247 + node.SetIPFSConfig("Addresses.Announce", []string{"/ip4/1.2.3.4/tcp/4001"})
248 + node.SetIPFSConfig("Routing", customRoutingConf(srv.URL))
249 + node.StartDaemon()
250 + defer node.StopDaemon()
251 +
252 + cidStr := node.IPFSAddStr(time.Now().String())
253 + node.IPFS("routing", "provide", cidStr)
254 +
255 + addrs := getAddrs()
256 + require.NotEmpty(t, addrs, "provider record should contain addresses")
257 + assert.Equal(t, []string{"/ip4/1.2.3.4/tcp/4001"}, addrs)
258 + })
259 +
260 + t.Run("provider records respect user-provided Addresses.AppendAnnounce", func(t *testing.T) {
261 + t.Parallel()
262 + srv, getAddrs := captureProviderAddrs(t)
263 +
264 + node := harness.NewT(t).NewNode().Init()
265 + node.SetIPFSConfig("Addresses.AppendAnnounce", []string{"/ip4/5.6.7.8/tcp/4001"})
266 + node.SetIPFSConfig("Routing", customRoutingConf(srv.URL))
267 + node.StartDaemon()
268 + defer node.StopDaemon()
269 +
270 + cidStr := node.IPFSAddStr(time.Now().String())
271 + node.IPFS("routing", "provide", cidStr)
272 +
273 + addrs := getAddrs()
274 + require.NotEmpty(t, addrs, "provider record should contain addresses")
275 + assert.Contains(t, addrs, "/ip4/5.6.7.8/tcp/4001", "AppendAnnounce address should be present")
276 + })
277 +}
test/dependencies/go.mod
+1 -1
@@ -135,7 +135,7 @@ require (
135 github.com/huin/goupnp v1.3.0 // indirect
136 github.com/inconshreveable/mousetrap v1.1.0 // indirect
137 github.com/ipfs/bbloom v0.0.4 // indirect
138 - github.com/ipfs/boxo v0.37.0 // indirect
138 + github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422 // indirect
139 github.com/ipfs/go-bitfield v1.1.0 // indirect
140 github.com/ipfs/go-block-format v0.2.3 // indirect
141 github.com/ipfs/go-cid v0.6.0 // indirect
test/dependencies/go.sum
+2 -2
@@ -448,8 +448,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
448 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
449 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
450 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
451 -github.com/ipfs/boxo v0.37.0 h1:2E3mZvydMI2t5IkAgtkmZ3sGsld0oS7o3I+xyzDk6uI=
452 -github.com/ipfs/boxo v0.37.0/go.mod h1:8yyiRn54F2CsW13n0zwXEPrVsZix/gFj9SYIRYMZ6KE=
451 +github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422 h1:yY3ot/DU1bqTzHDBARACM76Tbx9s4xzcRbzifG1e/es=
452 +github.com/ipfs/boxo v0.37.1-0.20260317235537-851246983422/go.mod h1:8yyiRn54F2CsW13n0zwXEPrVsZix/gFj9SYIRYMZ6KE=
453 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
454 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
455 github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk=