/ core / node / libp2p / routingopt.go
routingopt.go
  1  package libp2p
  2  
  3  import (
  4  	"context"
  5  	"fmt"
  6  	"os"
  7  	"slices"
  8  	"strings"
  9  	"time"
 10  
 11  	"github.com/ipfs/boxo/autoconf"
 12  	"github.com/ipfs/go-datastore"
 13  	"github.com/ipfs/kubo/config"
 14  	irouting "github.com/ipfs/kubo/routing"
 15  	dht "github.com/libp2p/go-libp2p-kad-dht"
 16  	dual "github.com/libp2p/go-libp2p-kad-dht/dual"
 17  	record "github.com/libp2p/go-libp2p-record"
 18  	routinghelpers "github.com/libp2p/go-libp2p-routing-helpers"
 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 {
 27  	Ctx                           context.Context
 28  	Host                          host.Host
 29  	Datastore                     datastore.Batching
 30  	Validator                     record.Validator
 31  	BootstrapPeers                []peer.AddrInfo
 32  	OptimisticProvide             bool
 33  	OptimisticProvideJobsPoolSize int
 34  	LoopbackAddressesOnLanDHT     bool
 35  }
 36  
 37  type RoutingOption func(args RoutingOptionArgs) (routing.Routing, error)
 38  
 39  var noopRouter = routinghelpers.Null{}
 40  
 41  // EndpointSource tracks where a URL came from to determine appropriate capabilities
 42  type EndpointSource struct {
 43  	URL           string
 44  	SupportsRead  bool // came from DelegatedRoutersWithAutoConf (Read operations)
 45  	SupportsWrite bool // came from DelegatedPublishersWithAutoConf (Write operations)
 46  }
 47  
 48  // determineCapabilities determines endpoint capabilities based on URL path and source
 49  func determineCapabilities(endpoint EndpointSource) (string, autoconf.EndpointCapabilities, error) {
 50  	parsed, err := autoconf.DetermineKnownCapabilities(endpoint.URL, endpoint.SupportsRead, endpoint.SupportsWrite)
 51  	if err != nil {
 52  		log.Debugf("Skipping endpoint %q: %v", endpoint.URL, err)
 53  		return "", autoconf.EndpointCapabilities{}, nil // Return empty caps, not error
 54  	}
 55  
 56  	return parsed.BaseURL, parsed.Capabilities, nil
 57  }
 58  
 59  // collectAllEndpoints gathers URLs from both router and publisher sources
 60  func collectAllEndpoints(cfg *config.Config) []EndpointSource {
 61  	var endpoints []EndpointSource
 62  
 63  	// Get router URLs (Read operations)
 64  	var routerURLs []string
 65  	if envRouters := os.Getenv(config.EnvHTTPRouters); envRouters != "" {
 66  		// Use environment variable override if set (space or comma separated)
 67  		splitFunc := func(r rune) bool { return r == ',' || r == ' ' }
 68  		routerURLs = strings.FieldsFunc(envRouters, splitFunc)
 69  		log.Warnf("Using HTTP routers from %s environment variable instead of config/autoconf: %v", config.EnvHTTPRouters, routerURLs)
 70  	} else {
 71  		// Use delegated routers from autoconf
 72  		routerURLs = cfg.DelegatedRoutersWithAutoConf()
 73  		// No fallback - if autoconf doesn't provide endpoints, use empty list
 74  		// This exposes any autoconf issues rather than masking them with hardcoded defaults
 75  	}
 76  
 77  	// Add router URLs to collection
 78  	for _, url := range routerURLs {
 79  		endpoints = append(endpoints, EndpointSource{
 80  			URL:           url,
 81  			SupportsRead:  true,
 82  			SupportsWrite: false,
 83  		})
 84  	}
 85  
 86  	// Get publisher URLs (Write operations)
 87  	publisherURLs := cfg.DelegatedPublishersWithAutoConf()
 88  
 89  	// Add publisher URLs, merging with existing router URLs if they match
 90  	for _, url := range publisherURLs {
 91  		found := false
 92  		for i, existing := range endpoints {
 93  			if existing.URL == url {
 94  				endpoints[i].SupportsWrite = true
 95  				found = true
 96  				break
 97  			}
 98  		}
 99  		if !found {
100  			endpoints = append(endpoints, EndpointSource{
101  				URL:           url,
102  				SupportsRead:  false,
103  				SupportsWrite: true,
104  			})
105  		}
106  	}
107  
108  	return endpoints
109  }
110  
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  
115  	// Collect URLs from both router and publisher sources
116  	endpoints := collectAllEndpoints(cfg)
117  
118  	// Group endpoints by origin (base URL) and aggregate capabilities
119  	originCapabilities := make(map[string]autoconf.EndpointCapabilities)
120  	for _, endpoint := range endpoints {
121  		// Parse endpoint and determine capabilities based on source
122  		baseURL, capabilities, err := determineCapabilities(endpoint)
123  		if err != nil {
124  			return nil, fmt.Errorf("failed to parse endpoint %q: %w", endpoint.URL, err)
125  		}
126  
127  		// Aggregate capabilities for this origin
128  		existing := originCapabilities[baseURL]
129  		existing.Merge(capabilities)
130  		originCapabilities[baseURL] = existing
131  	}
132  
133  	// Create single HTTP router and composer per origin
134  	for baseURL, capabilities := range originCapabilities {
135  		// Construct HTTP router using base URL (without path)
136  		httpRouter, err := irouting.ConstructHTTPRouter(baseURL, cfg.Identity.PeerID, addrFunc, cfg.Identity.PrivKey, httpRetrievalEnabled)
137  		if err != nil {
138  			return nil, err
139  		}
140  
141  		// Configure router operations based on aggregated capabilities
142  		// https://specs.ipfs.tech/routing/http-routing-v1/
143  		composer := &irouting.Composer{
144  			GetValueRouter:      noopRouter, // Default disabled, enabled below based on capabilities
145  			PutValueRouter:      noopRouter, // Default disabled, enabled below based on capabilities
146  			ProvideRouter:       noopRouter, // we don't have spec for sending provides to /routing/v1 (revisit once https://github.com/ipfs/specs/pull/378 or similar is ratified)
147  			FindPeersRouter:     noopRouter, // Default disabled, enabled below based on capabilities
148  			FindProvidersRouter: noopRouter, // Default disabled, enabled below based on capabilities
149  		}
150  
151  		// Enable specific capabilities
152  		if capabilities.IPNSGet {
153  			composer.GetValueRouter = httpRouter // GET /routing/v1/ipns for IPNS resolution
154  		}
155  		if capabilities.IPNSPut {
156  			composer.PutValueRouter = httpRouter // PUT /routing/v1/ipns for IPNS publishing
157  		}
158  		if capabilities.Peers {
159  			composer.FindPeersRouter = httpRouter // GET /routing/v1/peers
160  		}
161  		if capabilities.Providers {
162  			composer.FindProvidersRouter = httpRouter // GET /routing/v1/providers
163  		}
164  
165  		// Handle special cases and backward compatibility
166  		if baseURL == config.CidContactRoutingURL {
167  			// Special-case: cid.contact only supports /routing/v1/providers/cid endpoint
168  			// Override any capabilities detected from URL path to ensure only providers is enabled
169  			// TODO: Consider moving this to configuration or removing once cid.contact adds more capabilities
170  			composer.GetValueRouter = noopRouter
171  			composer.PutValueRouter = noopRouter
172  			composer.ProvideRouter = noopRouter
173  			composer.FindPeersRouter = noopRouter
174  			composer.FindProvidersRouter = httpRouter // Only providers supported
175  		}
176  
177  		routers = append(routers, &routinghelpers.ParallelRouter{
178  			Router:                  composer,
179  			IgnoreError:             true,             // https://github.com/ipfs/kubo/pull/9475#discussion_r1042507387
180  			Timeout:                 15 * time.Second, // 5x server value from https://github.com/ipfs/kubo/pull/9475#discussion_r1042428529
181  			DoNotWaitForSearchValue: true,
182  			ExecuteAfter:            0,
183  		})
184  	}
185  	return routers, nil
186  }
187  
188  // ConstructDelegatedOnlyRouting returns routers used when Routing.Type is set to "delegated"
189  // This provides HTTP-only routing without DHT, using only delegated routers and IPNS publishers.
190  // Useful for environments where DHT connectivity is not available or desired
191  func ConstructDelegatedOnlyRouting(cfg *config.Config) RoutingOption {
192  	return func(args RoutingOptionArgs) (routing.Routing, error) {
193  		// Use only HTTP routers (includes both read and write capabilities) - no DHT
194  		var routers []*routinghelpers.ParallelRouter
195  
196  		// Add HTTP delegated routers (includes both router and publisher capabilities)
197  		addrFunc := httpRouterAddrFunc(args.Host, cfg.Addresses)
198  		httpRouters, err := constructDefaultHTTPRouters(cfg, addrFunc)
199  		if err != nil {
200  			return nil, err
201  		}
202  		routers = append(routers, httpRouters...)
203  
204  		// Validate that we have at least one router configured
205  		if len(routers) == 0 {
206  			return nil, fmt.Errorf("no delegated routers or publishers configured for 'delegated' routing mode")
207  		}
208  
209  		routing := routinghelpers.NewComposableParallel(routers)
210  		return routing, nil
211  	}
212  }
213  
214  // ConstructDefaultRouting returns routers used when Routing.Type is unset or set to "auto"
215  func ConstructDefaultRouting(cfg *config.Config, routingOpt RoutingOption) RoutingOption {
216  	return func(args RoutingOptionArgs) (routing.Routing, error) {
217  		// Defined routers will be queried in parallel (optimizing for response speed)
218  		// Different trade-offs can be made by setting Routing.Type = "custom" with own Routing.Routers
219  		var routers []*routinghelpers.ParallelRouter
220  
221  		dhtRouting, err := routingOpt(args)
222  		if err != nil {
223  			return nil, err
224  		}
225  		routers = append(routers, &routinghelpers.ParallelRouter{
226  			Router:                  dhtRouting,
227  			IgnoreError:             false,
228  			DoNotWaitForSearchValue: true,
229  			ExecuteAfter:            0,
230  		})
231  
232  		addrFunc := httpRouterAddrFunc(args.Host, cfg.Addresses)
233  		httpRouters, err := constructDefaultHTTPRouters(cfg, addrFunc)
234  		if err != nil {
235  			return nil, err
236  		}
237  
238  		routers = append(routers, httpRouters...)
239  
240  		routing := routinghelpers.NewComposableParallel(routers)
241  		return routing, nil
242  	}
243  }
244  
245  // constructDHTRouting is used when Routing.Type = "dht"
246  func constructDHTRouting(mode dht.ModeOpt) RoutingOption {
247  	return func(args RoutingOptionArgs) (routing.Routing, error) {
248  		dhtOpts := []dht.Option{
249  			dht.Concurrency(10),
250  			dht.Mode(mode),
251  			dht.Datastore(args.Datastore),
252  			dht.Validator(args.Validator),
253  		}
254  		if args.OptimisticProvide {
255  			dhtOpts = append(dhtOpts, dht.EnableOptimisticProvide())
256  		}
257  		if args.OptimisticProvideJobsPoolSize != 0 {
258  			dhtOpts = append(dhtOpts, dht.OptimisticProvideJobsPoolSize(args.OptimisticProvideJobsPoolSize))
259  		}
260  		wanOptions := []dht.Option{
261  			dht.BootstrapPeers(args.BootstrapPeers...),
262  		}
263  		lanOptions := []dht.Option{}
264  		if args.LoopbackAddressesOnLanDHT {
265  			lanOptions = append(lanOptions, dht.AddressFilter(nil))
266  		}
267  		return dual.New(
268  			args.Ctx, args.Host,
269  			dual.DHTOption(dhtOpts...),
270  			dual.WanDHTOption(wanOptions...),
271  			dual.LanDHTOption(lanOptions...),
272  		)
273  	}
274  }
275  
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,
283  				Host:           args.Host,
284  				Validator:      args.Validator,
285  				Datastore:      args.Datastore,
286  				Context:        args.Ctx,
287  			},
288  			&irouting.ExtraHTTPParams{
289  				PeerID:        peerID,
290  				AddrFunc:      addrFunc,
291  				PrivKeyB64:    privKey,
292  				HTTPRetrieval: httpRetrieval,
293  			},
294  		)
295  	}
296  }
297  
298  func constructNilRouting(_ RoutingOptionArgs) (routing.Routing, error) {
299  	return routinghelpers.Null{}, nil
300  }
301  
302  var (
303  	DHTOption       RoutingOption = constructDHTRouting(dht.ModeAuto)
304  	DHTClientOption               = constructDHTRouting(dht.ModeClient)
305  	DHTServerOption               = constructDHTRouting(dht.ModeServer)
306  	NilRouterOption               = constructNilRouting
307  )
308  
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 {
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  		}
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  		}
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  	}
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  }