diff --git a/config/gateway.go b/config/gateway.go index 4070c218355..3495caede94 100644 --- a/config/gateway.go +++ b/config/gateway.go @@ -8,7 +8,7 @@ const ( DefaultInlineDNSLink = false DefaultDeserializedResponses = true DefaultDisableHTMLErrors = false - DefaultExposeRoutingAPI = false + DefaultExposeRoutingAPI = true DefaultDiagnosticServiceURL = "https://check.ipfs.network" // Gateway limit defaults from boxo diff --git a/core/corehttp/routing.go b/core/corehttp/routing.go index 9a2591d32be..239f8737bec 100644 --- a/core/corehttp/routing.go +++ b/core/corehttp/routing.go @@ -2,6 +2,8 @@ package corehttp import ( "context" + "errors" + "fmt" "net" "net/http" "time" @@ -13,6 +15,9 @@ import ( "github.com/ipfs/boxo/routing/http/types/iter" cid "github.com/ipfs/go-cid" core "github.com/ipfs/kubo/core" + dht "github.com/libp2p/go-libp2p-kad-dht" + "github.com/libp2p/go-libp2p-kad-dht/dual" + "github.com/libp2p/go-libp2p-kad-dht/fullrt" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" ) @@ -96,6 +101,60 @@ func (r *contentRouter) PutIPNS(ctx context.Context, name ipns.Name, record *ipn return r.n.Routing.PutValue(ctx, string(name.RoutingKey()), raw) } +func (r *contentRouter) GetClosestPeers(ctx context.Context, key cid.Cid) (iter.ResultIter[*types.PeerRecord], error) { + // Per the spec, if the peer ID is empty, we should use self. + if key == cid.Undef { + return nil, errors.New("GetClosestPeers key is undefined") + } + + keyStr := string(key.Hash()) + var peers []peer.ID + var err error + + if r.n.DHTClient == nil { + return nil, fmt.Errorf("GetClosestPeers not supported: DHT is not available") + } + + switch dhtClient := r.n.DHTClient.(type) { + case *dual.DHT: + // Only use WAN DHT for public HTTP Routing API. + // LAN DHT contains private network peers that should not be exposed publicly. + if dhtClient.WAN == nil { + return nil, fmt.Errorf("GetClosestPeers not supported: WAN DHT is not available") + } + peers, err = dhtClient.WAN.GetClosestPeers(ctx, keyStr) + case *fullrt.FullRT: + peers, err = dhtClient.GetClosestPeers(ctx, keyStr) + case *dht.IpfsDHT: + peers, err = dhtClient.GetClosestPeers(ctx, keyStr) + default: + return nil, fmt.Errorf("GetClosestPeers not supported for DHT type %T", r.n.DHTClient) + } + + if err != nil { + return nil, err + } + + // We have some DHT-closest peers. Find addresses for them. + // The addresses should be in the peerstore. + records := make([]*types.PeerRecord, 0, len(peers)) + for _, p := range peers { + addrs := r.n.Peerstore.Addrs(p) + rAddrs := make([]types.Multiaddr, len(addrs)) + for i, addr := range addrs { + rAddrs[i] = types.Multiaddr{Multiaddr: addr} + } + record := types.PeerRecord{ + ID: &p, + Schema: types.SchemaPeer, + Addrs: rAddrs, + } + records = append(records, &record) + } + + return iter.ToResultIter(iter.FromSlice(records)), nil +} + type peerChanIter struct { ch <-chan peer.AddrInfo cancel context.CancelFunc diff --git a/docs/changelogs/v0.39.md b/docs/changelogs/v0.39.md index e34fc1d705e..054e6c07eb2 100644 --- a/docs/changelogs/v0.39.md +++ b/docs/changelogs/v0.39.md @@ -168,6 +168,10 @@ The `go-ipfs` name was deprecated in 2022 and renamed to `kubo`. Starting with t All users should migrate to the `kubo` name in their scripts and configurations. +#### Routing V1 HTTP API now exposed by default + +The [Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/) is now exposed by default at `http://127.0.0.1:8080/routing/v1`. This allows light clients in browsers to use Kubo Gateway as a delegated routing backend instead of running a full DHT client. Support for [IPIP-476: Delegated Routing DHT Closest Peers API](https://github.com/ipfs/specs/pull/476) is included. Can be disabled via [`Gateway.ExposeRoutingAPI`](https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewayexposeroutingapi). + ### 📦️ Important dependency updates - update `go-libp2p` to [v0.45.0](https://github.com/libp2p/go-libp2p/releases/tag/v0.45.0) (incl. [v0.44.0](https://github.com/libp2p/go-libp2p/releases/tag/v0.44.0)) with self-healing UPnP port mappings and go-log/slog interop fixes diff --git a/docs/config.md b/docs/config.md index 0352bd84540..19335d658cf 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1128,7 +1128,7 @@ Kubo will filter out routing results which are not actionable, for example, all graphsync providers will be skipped. If you need a generic pass-through, see standalone router implementation named [someguy](https://github.com/ipfs/someguy). -Default: `false` +Default: `true` Type: `flag` diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index dc4c7467972..aca8b87af4e 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -7,7 +7,7 @@ go 1.25 replace github.com/ipfs/kubo => ./../../.. require ( - github.com/ipfs/boxo v0.35.2 + github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 github.com/libp2p/go-libp2p v0.45.0 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 349831625e9..1d334a51235 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -291,8 +291,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.35.2 h1:0QZJJh6qrak28abENOi5OA8NjBnZM4p52SxeuIDqNf8= -github.com/ipfs/boxo v0.35.2/go.mod h1:bZn02OFWwJtY8dDW9XLHaki59EC5o+TGDECXEbe1w8U= +github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263 h1:7sSi4euS5Rb+RwQZOXrd/fURpC9kgbESD4DPykaLy0I= +github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263/go.mod h1:bZn02OFWwJtY8dDW9XLHaki59EC5o+TGDECXEbe1w8U= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk= diff --git a/go.mod b/go.mod index ce175f14d5d..f7a73a475ce 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/ipfs-shipyard/nopfs v0.0.14 github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 - github.com/ipfs/boxo v0.35.2 + github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263 github.com/ipfs/go-block-format v0.2.3 github.com/ipfs/go-cid v0.5.0 github.com/ipfs/go-cidutil v0.1.0 diff --git a/go.sum b/go.sum index 9a11f2db8c1..2f1b7e9bc1a 100644 --- a/go.sum +++ b/go.sum @@ -358,8 +358,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.35.2 h1:0QZJJh6qrak28abENOi5OA8NjBnZM4p52SxeuIDqNf8= -github.com/ipfs/boxo v0.35.2/go.mod h1:bZn02OFWwJtY8dDW9XLHaki59EC5o+TGDECXEbe1w8U= +github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263 h1:7sSi4euS5Rb+RwQZOXrd/fURpC9kgbESD4DPykaLy0I= +github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263/go.mod h1:bZn02OFWwJtY8dDW9XLHaki59EC5o+TGDECXEbe1w8U= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk= diff --git a/test/cli/delegated_routing_v1_http_server_test.go b/test/cli/delegated_routing_v1_http_server_test.go index 9d10637a851..b3fdcba053c 100644 --- a/test/cli/delegated_routing_v1_http_server_test.go +++ b/test/cli/delegated_routing_v1_http_server_test.go @@ -2,9 +2,13 @@ package cli import ( "context" + "encoding/json" + "strings" "testing" + "time" "github.com/google/uuid" + "github.com/ipfs/boxo/autoconf" "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/routing/http/client" "github.com/ipfs/boxo/routing/http/types" @@ -14,8 +18,14 @@ import ( "github.com/ipfs/kubo/test/cli/harness" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +// swarmPeersOutput is used to parse the JSON output of 'ipfs swarm peers --enc=json' +type swarmPeersOutput struct { + Peers []struct{} `json:"Peers"` +} + func TestRoutingV1Server(t *testing.T) { t.Parallel() @@ -143,4 +153,132 @@ func TestRoutingV1Server(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "/ipfs/"+cidStr, value.String()) }) + + t.Run("GetClosestPeers returns error when DHT is disabled", func(t *testing.T) { + t.Parallel() + + // Test various routing types that don't support DHT + routingTypes := []string{"none", "delegated", "custom"} + for _, routingType := range routingTypes { + t.Run("routing_type="+routingType, func(t *testing.T) { + t.Parallel() + + // Create node with specified routing type (DHT disabled) + node := harness.NewT(t).NewNode().Init() + node.UpdateConfig(func(cfg *config.Config) { + cfg.Gateway.ExposeRoutingAPI = config.True + cfg.Routing.Type = config.NewOptionalString(routingType) + + // For custom routing type, we need to provide minimal valid config + // otherwise daemon startup will fail + if routingType == "custom" { + // Configure a minimal HTTP router (no DHT) + cfg.Routing.Routers = map[string]config.RouterParser{ + "http-only": { + Router: config.Router{ + Type: config.RouterTypeHTTP, + Parameters: config.HTTPRouterParams{ + Endpoint: "https://delegated-ipfs.dev", + }, + }, + }, + } + cfg.Routing.Methods = map[config.MethodName]config.Method{ + config.MethodNameProvide: {RouterName: "http-only"}, + config.MethodNameFindProviders: {RouterName: "http-only"}, + config.MethodNameFindPeers: {RouterName: "http-only"}, + config.MethodNameGetIPNS: {RouterName: "http-only"}, + config.MethodNamePutIPNS: {RouterName: "http-only"}, + } + } + + // For delegated routing type, ensure we have at least one HTTP router + // to avoid daemon startup failure + if routingType == "delegated" { + // Use a minimal delegated router configuration + cfg.Routing.DelegatedRouters = []string{"https://delegated-ipfs.dev"} + // Delegated routing doesn't support providing, must be disabled + cfg.Provide.Enabled = config.False + } + }) + node.StartDaemon() + + c, err := client.New(node.GatewayURL()) + require.NoError(t, err) + + // Try to get closest peers - should fail gracefully with an error + testCid, err := cid.Decode("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") + require.NoError(t, err) + + _, err = c.GetClosestPeers(context.Background(), testCid) + require.Error(t, err) + // All these routing types should indicate DHT is not available + // The exact error message may vary based on implementation details + errStr := err.Error() + assert.True(t, + strings.Contains(errStr, "not supported") || + strings.Contains(errStr, "not available") || + strings.Contains(errStr, "500"), + "Expected error indicating DHT not available for routing type %s, got: %s", routingType, errStr) + }) + } + }) + + t.Run("GetClosestPeers returns peers for self", func(t *testing.T) { + t.Parallel() + + routingTypes := []string{"auto", "autoclient", "dht", "dhtclient"} + for _, routingType := range routingTypes { + t.Run("routing_type="+routingType, func(t *testing.T) { + t.Parallel() + + // Single node with DHT and real bootstrap peers + node := harness.NewT(t).NewNode().Init() + node.UpdateConfig(func(cfg *config.Config) { + cfg.Gateway.ExposeRoutingAPI = config.True + cfg.Routing.Type = config.NewOptionalString(routingType) + // Set real bootstrap peers from boxo/autoconf + cfg.Bootstrap = autoconf.FallbackBootstrapPeers + }) + node.StartDaemon() + + // Wait for node to connect to bootstrap peers and populate WAN DHT routing table + minPeers := len(autoconf.FallbackBootstrapPeers) + require.EventuallyWithT(t, func(t *assert.CollectT) { + res := node.RunIPFS("swarm", "peers", "--enc=json") + var output swarmPeersOutput + err := json.Unmarshal(res.Stdout.Bytes(), &output) + assert.NoError(t, err) + peerCount := len(output.Peers) + // Wait until we have at least minPeers connected + assert.GreaterOrEqual(t, peerCount, minPeers, + "waiting for at least %d bootstrap peers, currently have %d", minPeers, peerCount) + }, 30*time.Second, time.Second) + + c, err := client.New(node.GatewayURL()) + require.NoError(t, err) + + // Query for closest peers to our own peer ID + key := peer.ToCid(node.PeerID()) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + resultsIter, err := c.GetClosestPeers(ctx, key) + require.NoError(t, err) + + records, err := iter.ReadAllResults(resultsIter) + require.NoError(t, err) + + // Verify we got some peers back from WAN DHT + assert.NotEmpty(t, records, "should return some peers close to own peerid") + + // Verify structure of returned records + for _, record := range records { + assert.Equal(t, types.SchemaPeer, record.Schema) + assert.NotNil(t, record.ID) + assert.NotEmpty(t, record.Addrs, "peer record should have addresses") + } + }) + } + }) } diff --git a/test/cli/testutils/httprouting/mock_http_content_router.go b/test/cli/testutils/httprouting/mock_http_content_router.go index 8f6f3102387..19394005ea1 100644 --- a/test/cli/testutils/httprouting/mock_http_content_router.go +++ b/test/cli/testutils/httprouting/mock_http_content_router.go @@ -19,13 +19,14 @@ import ( // (https://specs.ipfs.tech/routing/http-routing-v1/) server implementation // based on github.com/ipfs/boxo/routing/http/server type MockHTTPContentRouter struct { - m sync.Mutex - provideBitswapCalls int - findProvidersCalls int - findPeersCalls int - providers map[cid.Cid][]types.Record - peers map[peer.ID][]*types.PeerRecord - Debug bool + m sync.Mutex + provideBitswapCalls int + findProvidersCalls int + findPeersCalls int + getClosestPeersCalls int + providers map[cid.Cid][]types.Record + peers map[peer.ID][]*types.PeerRecord + Debug bool } func (r *MockHTTPContentRouter) FindProviders(ctx context.Context, key cid.Cid, limit int) (iter.ResultIter[types.Record], error) { @@ -115,3 +116,30 @@ func (r *MockHTTPContentRouter) AddProvider(key cid.Cid, record types.Record) { r.peers[*pid] = append(r.peers[*pid], peerRecord) } } + +func (r *MockHTTPContentRouter) GetClosestPeers(ctx context.Context, key cid.Cid) (iter.ResultIter[*types.PeerRecord], error) { + r.m.Lock() + defer r.m.Unlock() + r.getClosestPeersCalls++ + + if r.peers == nil { + r.peers = make(map[peer.ID][]*types.PeerRecord) + } + pid, err := peer.FromCid(key) + if err != nil { + return iter.FromSlice([]iter.Result[*types.PeerRecord]{}), nil + } + records, found := r.peers[pid] + if !found { + return iter.FromSlice([]iter.Result[*types.PeerRecord]{}), nil + } + + results := make([]iter.Result[*types.PeerRecord], len(records)) + for i, rec := range records { + results[i] = iter.Result[*types.PeerRecord]{Val: rec} + if r.Debug { + fmt.Printf("MockHTTPContentRouter.GetPeers(%s) result: %+v\n", pid.String(), rec) + } + } + return iter.FromSlice(results), nil +} diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 5a98d97bcd9..68c5a99c642 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -136,7 +136,7 @@ require ( github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/bbloom v0.0.4 // indirect - github.com/ipfs/boxo v0.35.2 // indirect + github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263 // indirect github.com/ipfs/go-bitfield v1.1.0 // indirect github.com/ipfs/go-block-format v0.2.3 // indirect github.com/ipfs/go-cid v0.5.0 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 070acd3dfdd..005747af87a 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -334,8 +334,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.35.2 h1:0QZJJh6qrak28abENOi5OA8NjBnZM4p52SxeuIDqNf8= -github.com/ipfs/boxo v0.35.2/go.mod h1:bZn02OFWwJtY8dDW9XLHaki59EC5o+TGDECXEbe1w8U= +github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263 h1:7sSi4euS5Rb+RwQZOXrd/fURpC9kgbESD4DPykaLy0I= +github.com/ipfs/boxo v0.35.3-0.20251118170232-e71f50ea2263/go.mod h1:bZn02OFWwJtY8dDW9XLHaki59EC5o+TGDECXEbe1w8U= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk=