kubo/core/node/libp2p/routingopt.go
Hector Sanjuan b5d73695ba
feat: opt-in http retrieval client (#10772)
* Feat: http retrieval as experimental feature

This introduces the http-retrieval capability as an experimental feature.

It can be enabled in the configuration `Experimental.HTTPRetrieval.Enabled = true`.

Documentation and changelog to be added later.

* refactor: HTTPRetrieval.Enabled as Flag

* docs(config): HTTPRetrieval section

* refactor: reusable MockHTTPContentRouter

* feat: HTTPRetrieval.TLSInsecureSkipVerify

allows self-signed certificates in tests

* feat(config): HTTPRetrieval.MaxBlockSize

* test: end-to-end HTTPRetrieval.Enabled

this spawns two http services on localhost:
1. HTTP router that returns HTTP provider when /routing/v1/providers/cid  i queried
2. HTTP provider that returns a block when /ipfs/cid is queried
3. Configures Kubo to use (1) instead of cid.contact

this seems to work (running test with DEBUG=true shows (1) was queried
for the test CID and returned multiaddr of (2), but Kubo never requested
test CID block from (2) – needs investigation

* fix: enable /routing/v1/peers for non-cid.contact

we artificially limited every delegated routing endpoint because of
cid.contact being limited to one endpoint

* feat: Routing.DelegatedRouters

make it easy to override the hardcoded implicit HTTP routeur URL
without having to set the entire custom Router.Routers and
Router.Methods

(http_retrieval_client_test.go still needs to be fixed in future commit)

* test: flag remaining work

* docs: review feedback

* refactor: providerQueryMgr with bitswapNetworks

this fixes two regressions:

(1) introduced in https://github.com/ipfs/kubo/issues/10717
    where we only used bitswapLib2p query manager
    (this is why E2E did not act on http provider)

(2) introduced in https://github.com/ipfs/kubo/pull/10765
    where it was not possible to set binary peerID in IgnoreProviders
    (we changed to []string)

* refactor: Bitswap.Libp2pEnabled

replaces Bitswap.Enabled with Bitswap.Libp2pEnabled
adds tests that confirm it is possible to disable libp2p bitswap fully
and only keep http in client mode

also, removes the need for passing empty blockstore in client-only mode

* docs: changelog

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
2025-05-06 19:06:40 +02:00

204 lines
7.0 KiB
Go

package libp2p
import (
"context"
"os"
"time"
"github.com/ipfs/go-datastore"
"github.com/ipfs/kubo/config"
irouting "github.com/ipfs/kubo/routing"
dht "github.com/libp2p/go-libp2p-kad-dht"
dual "github.com/libp2p/go-libp2p-kad-dht/dual"
record "github.com/libp2p/go-libp2p-record"
routinghelpers "github.com/libp2p/go-libp2p-routing-helpers"
host "github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
routing "github.com/libp2p/go-libp2p/core/routing"
)
type RoutingOptionArgs struct {
Ctx context.Context
Host host.Host
Datastore datastore.Batching
Validator record.Validator
BootstrapPeers []peer.AddrInfo
OptimisticProvide bool
OptimisticProvideJobsPoolSize int
LoopbackAddressesOnLanDHT bool
}
type RoutingOption func(args RoutingOptionArgs) (routing.Routing, error)
var noopRouter = routinghelpers.Null{}
func constructDefaultHTTPRouters(cfg *config.Config) ([]*routinghelpers.ParallelRouter, error) {
var routers []*routinghelpers.ParallelRouter
httpRetrievalEnabled := cfg.HTTPRetrieval.Enabled.WithDefault(config.DefaultHTTPRetrievalEnabled)
// Use config.DefaultHTTPRouters if custom override was sent via config.EnvHTTPRouters
// or if user did not set any preference in cfg.Routing.DelegatedRouters
var httpRouterEndpoints []string
if os.Getenv(config.EnvHTTPRouters) != "" || len(cfg.Routing.DelegatedRouters) == 0 {
httpRouterEndpoints = config.DefaultHTTPRouters
} else {
httpRouterEndpoints = cfg.Routing.DelegatedRouters
}
// Append HTTP routers for additional speed
for _, endpoint := range httpRouterEndpoints {
httpRouter, err := irouting.ConstructHTTPRouter(endpoint, cfg.Identity.PeerID, httpAddrsFromConfig(cfg.Addresses), cfg.Identity.PrivKey, httpRetrievalEnabled)
if err != nil {
return nil, err
}
// Mapping router to /routing/v1/* endpoints
// https://specs.ipfs.tech/routing/http-routing-v1/
r := &irouting.Composer{
GetValueRouter: httpRouter, // GET /routing/v1/ipns
PutValueRouter: httpRouter, // PUT /routing/v1/ipns
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)
FindPeersRouter: httpRouter, // /routing/v1/peers
FindProvidersRouter: httpRouter, // /routing/v1/providers
}
if endpoint == config.CidContactRoutingURL {
// Special-case: cid.contact only supports /routing/v1/providers/cid
// we disable other endpoints to avoid sending requests that always fail
r.GetValueRouter = noopRouter
r.PutValueRouter = noopRouter
r.ProvideRouter = noopRouter
r.FindPeersRouter = noopRouter
}
routers = append(routers, &routinghelpers.ParallelRouter{
Router: r,
IgnoreError: true, // https://github.com/ipfs/kubo/pull/9475#discussion_r1042507387
Timeout: 15 * time.Second, // 5x server value from https://github.com/ipfs/kubo/pull/9475#discussion_r1042428529
DoNotWaitForSearchValue: true,
ExecuteAfter: 0,
})
}
return routers, nil
}
// ConstructDefaultRouting returns routers used when Routing.Type is unset or set to "auto"
func ConstructDefaultRouting(cfg *config.Config, routingOpt RoutingOption) RoutingOption {
return func(args RoutingOptionArgs) (routing.Routing, error) {
// Defined routers will be queried in parallel (optimizing for response speed)
// Different trade-offs can be made by setting Routing.Type = "custom" with own Routing.Routers
var routers []*routinghelpers.ParallelRouter
dhtRouting, err := routingOpt(args)
if err != nil {
return nil, err
}
routers = append(routers, &routinghelpers.ParallelRouter{
Router: dhtRouting,
IgnoreError: false,
DoNotWaitForSearchValue: true,
ExecuteAfter: 0,
})
httpRouters, err := constructDefaultHTTPRouters(cfg)
if err != nil {
return nil, err
}
routers = append(routers, httpRouters...)
routing := routinghelpers.NewComposableParallel(routers)
return routing, nil
}
}
// constructDHTRouting is used when Routing.Type = "dht"
func constructDHTRouting(mode dht.ModeOpt) RoutingOption {
return func(args RoutingOptionArgs) (routing.Routing, error) {
dhtOpts := []dht.Option{
dht.Concurrency(10),
dht.Mode(mode),
dht.Datastore(args.Datastore),
dht.Validator(args.Validator),
}
if args.OptimisticProvide {
dhtOpts = append(dhtOpts, dht.EnableOptimisticProvide())
}
if args.OptimisticProvideJobsPoolSize != 0 {
dhtOpts = append(dhtOpts, dht.OptimisticProvideJobsPoolSize(args.OptimisticProvideJobsPoolSize))
}
wanOptions := []dht.Option{
dht.BootstrapPeers(args.BootstrapPeers...),
}
lanOptions := []dht.Option{}
if args.LoopbackAddressesOnLanDHT {
lanOptions = append(lanOptions, dht.AddressFilter(nil))
}
return dual.New(
args.Ctx, args.Host,
dual.DHTOption(dhtOpts...),
dual.WanDHTOption(wanOptions...),
dual.LanDHTOption(lanOptions...),
)
}
}
// ConstructDelegatedRouting is used when Routing.Type = "custom"
func ConstructDelegatedRouting(routers config.Routers, methods config.Methods, peerID string, addrs config.Addresses, privKey string, httpRetrieval bool) RoutingOption {
return func(args RoutingOptionArgs) (routing.Routing, error) {
return irouting.Parse(routers, methods,
&irouting.ExtraDHTParams{
BootstrapPeers: args.BootstrapPeers,
Host: args.Host,
Validator: args.Validator,
Datastore: args.Datastore,
Context: args.Ctx,
},
&irouting.ExtraHTTPParams{
PeerID: peerID,
Addrs: httpAddrsFromConfig(addrs),
PrivKeyB64: privKey,
HTTPRetrieval: httpRetrieval,
},
)
}
}
func constructNilRouting(_ RoutingOptionArgs) (routing.Routing, error) {
return routinghelpers.Null{}, nil
}
var (
DHTOption RoutingOption = constructDHTRouting(dht.ModeAuto)
DHTClientOption = constructDHTRouting(dht.ModeClient)
DHTServerOption = constructDHTRouting(dht.ModeServer)
NilRouterOption = constructNilRouting
)
// httpAddrsFromConfig creates a list of addresses from the provided configuration to be used by HTTP delegated routers.
func httpAddrsFromConfig(cfgAddrs config.Addresses) []string {
// Swarm addrs are announced by default
addrs := cfgAddrs.Swarm
// if Announce addrs are specified - override Swarm
if len(cfgAddrs.Announce) > 0 {
addrs = cfgAddrs.Announce
} else if len(cfgAddrs.NoAnnounce) > 0 {
// if Announce adds are not specified - filter Swarm addrs with NoAnnounce list
maddrs := map[string]struct{}{}
for _, addr := range addrs {
maddrs[addr] = struct{}{}
}
for _, addr := range cfgAddrs.NoAnnounce {
delete(maddrs, addr)
}
addrs = make([]string, 0, len(maddrs))
for k := range maddrs {
addrs = append(addrs, k)
}
}
// append AppendAnnounce addrs to the result list
if len(cfgAddrs.AppendAnnounce) > 0 {
addrs = append(addrs, cfgAddrs.AppendAnnounce...)
}
return addrs
}