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 }