From 76cf61a1f633eaeee2008a1f23635fafb36b2944 Mon Sep 17 00:00:00 2001 From: kalou Date: Mon, 7 Sep 2026 09:12:28 +0200 Subject: [PATCH 1/2] fix(bootstrap): skip backup peer list when no bootstrap peers configured bootstrapRound no longer consults the backup peer list when cfg.BootstrapPeers() returns empty. The backup list exists only as a recovery mechanism for when configured bootstrap peers are down (#8856); with no configured peers there is nothing to recover from, so dialing stale backup peers persisted from previous runs is skipped. This lets a caller fully disable bootstrap dialing by setting an empty peer list (for example a local-only/offline node with Routing.Type=none), and works with runtime overrides such as `ipfs daemon --routing=none` that do not change the config file. Nodes that configure explicit Bootstrap peers are unaffected. Closes kubo issue #11452 (companion kubo PR to follow). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 + bootstrap/bootstrap.go | 20 ++++++--- bootstrap/bootstrap_test.go | 84 +++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f1403511..17be34bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ The following emojis are used to highlight certain changes: ### Fixed - `gateway`: `X-Ipfs-Path` values no longer carry bytes that are invalid in an HTTP field value (Section 5.5 of RFC 9110). The header used to echo raw UnixFS file names, so non-ASCII paths arrived garbled or broke strict clients; when the header is enabled, it is now omitted for such paths, which only the percent-encoded `Ipfs-Uri` can carry. [#1209](https://github.com/ipfs/boxo/pull/1209) +- ✨ `bootstrap`: `bootstrapRound` no longer consults the backup peer list when no bootstrap peers are configured. The backup list exists only as a recovery mechanism for when configured bootstrap peers are down (see #8856); with no configured peers there is nothing to recover from, so dialing stale backup peers persisted from previous runs is skipped. This lets a caller fully disable bootstrap dialing by setting an empty peer list (for example a local-only/offline node with `Routing.Type=none`), and works with runtime overrides such as `ipfs daemon --routing=none` that do not change the config file. Nodes that configure explicit `Bootstrap` peers are unaffected. [#1213](https://github.com/ipfs/boxo/pull/1213) + ### Security diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 9b7ed0b9b..b74aa6788 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -299,11 +299,21 @@ func bootstrapRound(ctx context.Context, host host.Host, cfg BootstrapConfig) er // Retrieving them here makes sure we remain observant of changes to client configuration. peers := cfg.BootstrapPeers() - if len(peers) > 0 { - numToDial -= int(peersConnect(ctx, host, peers, numToDial, true)) - if numToDial <= 0 { - return nil - } + if len(peers) == 0 { + // No bootstrap peers are configured. The backup peer list exists only as + // a recovery mechanism for when configured bootstrap peers are down + // (see #8856). With no configured peers there is nothing to recover + // from, so we skip the backup list entirely. This lets a caller fully + // disable bootstrap dialing by setting an empty peer list (for example + // a local-only/offline node with Routing.Type=none), and avoids dialing + // stale backup peers persisted from previous runs. + log.Debugf("%s bootstrap skipped -- no bootstrap peers configured", id) + return nil + } + + numToDial -= int(peersConnect(ctx, host, peers, numToDial, true)) + if numToDial <= 0 { + return nil } if cfg.loadBackupBootstrapPeers == nil { diff --git a/bootstrap/bootstrap_test.go b/bootstrap/bootstrap_test.go index c0b71e5ee..581d95080 100644 --- a/bootstrap/bootstrap_test.go +++ b/bootstrap/bootstrap_test.go @@ -254,3 +254,87 @@ func TestHasCircuitProtocol(t *testing.T) { }) } } + +// TestBootstrapRoundSkipsBackupWhenNoBootstrapPeers verifies that bootstrapRound +// does not consult the backup peer list when no bootstrap peers are configured. +// The backup list exists only as a recovery mechanism for when configured +// bootstrap peers are down; with no configured peers there is nothing to +// recover from, so dialing stale backup peers from previous runs would be +// unwanted network traffic. See kubo issue #11452. +func TestBootstrapRoundSkipsBackupWhenNoBootstrapPeers(t *testing.T) { + backupCalled := false + loadFunc := func(_ context.Context) []peer.AddrInfo { + backupCalled = true + return nil + } + saveFunc := func(_ context.Context, _ []peer.AddrInfo) {} + + bootCfg := BootstrapConfigWithPeers(nil, WithBackupPeers(loadFunc, saveFunc)) + bootCfg.MinPeerThreshold = 2 + + priv, pub, err := crypto.GenerateEd25519Key(rand.Reader) + if err != nil { + t.Fatal(err) + } + peerID, err := peer.IDFromPublicKey(pub) + if err != nil { + t.Fatal(err) + } + p2pHost, err := libp2p.New(libp2p.Identity(priv)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = p2pHost.Close() }) + + _ = peerID + if err := bootstrapRound(context.Background(), p2pHost, bootCfg); err != nil { + t.Fatalf("bootstrapRound returned error: %v", err) + } + if backupCalled { + t.Fatal("bootstrapRound consulted the backup peer list despite no bootstrap peers being configured") + } +} + +// TestBootstrapRoundDialsBackupWhenBootstrapPeersPresent confirms the backup +// list is still consulted when configured bootstrap peers fail to connect, +// preserving the recovery mechanism from #8856. +func TestBootstrapRoundDialsBackupWhenBootstrapPeersPresent(t *testing.T) { + backupCalled := false + loadFunc := func(_ context.Context) []peer.AddrInfo { + backupCalled = true + return nil + } + saveFunc := func(_ context.Context, _ []peer.AddrInfo) {} + + // A fake, undialable bootstrap peer so peersConnect attempts and fails + // rather than short-circuiting on an empty list. + fakeID, err := test.RandPeerID() + if err != nil { + t.Fatal(err) + } + fakePeer := peer.AddrInfo{ID: fakeID} + bootCfg := BootstrapConfigWithPeers([]peer.AddrInfo{fakePeer}, WithBackupPeers(loadFunc, saveFunc)) + bootCfg.MinPeerThreshold = 2 + // Keep the round snappy: the dial will fail fast against a random peer ID. + bootCfg.ConnectionTimeout = 500 * time.Millisecond + + priv, pub, err := crypto.GenerateEd25519Key(rand.Reader) + if err != nil { + t.Fatal(err) + } + peerID, err := peer.IDFromPublicKey(pub) + if err != nil { + t.Fatal(err) + } + p2pHost, err := libp2p.New(libp2p.Identity(priv)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = p2pHost.Close() }) + + _ = peerID + _ = bootstrapRound(context.Background(), p2pHost, bootCfg) + if !backupCalled { + t.Fatal("bootstrapRound did not consult the backup peer list despite configured bootstrap peers failing to connect") + } +} From df6852a09b58d46b064682bedd21509fcbf3d395 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 7 Sep 2026 22:19:06 +0200 Subject: [PATCH 2/2] chore(bootstrap): review follow-ups Shorten the empty-list comment and the changelog entry, point the #8856 and #11452 references at ipfs/kubo, drop the unused peerID in the new tests and use t.Context(). --- CHANGELOG.md | 3 +-- bootstrap/bootstrap.go | 10 +++------- bootstrap/bootstrap_test.go | 22 ++++++---------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17be34bba..5b5bcd655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,8 +29,7 @@ The following emojis are used to highlight certain changes: ### Fixed - `gateway`: `X-Ipfs-Path` values no longer carry bytes that are invalid in an HTTP field value (Section 5.5 of RFC 9110). The header used to echo raw UnixFS file names, so non-ASCII paths arrived garbled or broke strict clients; when the header is enabled, it is now omitted for such paths, which only the percent-encoded `Ipfs-Uri` can carry. [#1209](https://github.com/ipfs/boxo/pull/1209) -- ✨ `bootstrap`: `bootstrapRound` no longer consults the backup peer list when no bootstrap peers are configured. The backup list exists only as a recovery mechanism for when configured bootstrap peers are down (see #8856); with no configured peers there is nothing to recover from, so dialing stale backup peers persisted from previous runs is skipped. This lets a caller fully disable bootstrap dialing by setting an empty peer list (for example a local-only/offline node with `Routing.Type=none`), and works with runtime overrides such as `ipfs daemon --routing=none` that do not change the config file. Nodes that configure explicit `Bootstrap` peers are unaffected. [#1213](https://github.com/ipfs/boxo/pull/1213) - +- `bootstrap`: the saved backup peer list is no longer dialed when no bootstrap peers are configured. [#1213](https://github.com/ipfs/boxo/pull/1213) ### Security diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index b74aa6788..51ce70778 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -300,13 +300,9 @@ func bootstrapRound(ctx context.Context, host host.Host, cfg BootstrapConfig) er peers := cfg.BootstrapPeers() if len(peers) == 0 { - // No bootstrap peers are configured. The backup peer list exists only as - // a recovery mechanism for when configured bootstrap peers are down - // (see #8856). With no configured peers there is nothing to recover - // from, so we skip the backup list entirely. This lets a caller fully - // disable bootstrap dialing by setting an empty peer list (for example - // a local-only/offline node with Routing.Type=none), and avoids dialing - // stale backup peers persisted from previous runs. + // The backup list is a fallback for when configured bootstrap peers + // are unreachable (ipfs/kubo#8856). With no configured peers there is + // nothing to fall back from. log.Debugf("%s bootstrap skipped -- no bootstrap peers configured", id) return nil } diff --git a/bootstrap/bootstrap_test.go b/bootstrap/bootstrap_test.go index 581d95080..411a312e5 100644 --- a/bootstrap/bootstrap_test.go +++ b/bootstrap/bootstrap_test.go @@ -260,7 +260,7 @@ func TestHasCircuitProtocol(t *testing.T) { // The backup list exists only as a recovery mechanism for when configured // bootstrap peers are down; with no configured peers there is nothing to // recover from, so dialing stale backup peers from previous runs would be -// unwanted network traffic. See kubo issue #11452. +// unwanted network traffic. See ipfs/kubo#11452. func TestBootstrapRoundSkipsBackupWhenNoBootstrapPeers(t *testing.T) { backupCalled := false loadFunc := func(_ context.Context) []peer.AddrInfo { @@ -272,11 +272,7 @@ func TestBootstrapRoundSkipsBackupWhenNoBootstrapPeers(t *testing.T) { bootCfg := BootstrapConfigWithPeers(nil, WithBackupPeers(loadFunc, saveFunc)) bootCfg.MinPeerThreshold = 2 - priv, pub, err := crypto.GenerateEd25519Key(rand.Reader) - if err != nil { - t.Fatal(err) - } - peerID, err := peer.IDFromPublicKey(pub) + priv, _, err := crypto.GenerateEd25519Key(rand.Reader) if err != nil { t.Fatal(err) } @@ -286,8 +282,7 @@ func TestBootstrapRoundSkipsBackupWhenNoBootstrapPeers(t *testing.T) { } t.Cleanup(func() { _ = p2pHost.Close() }) - _ = peerID - if err := bootstrapRound(context.Background(), p2pHost, bootCfg); err != nil { + if err := bootstrapRound(t.Context(), p2pHost, bootCfg); err != nil { t.Fatalf("bootstrapRound returned error: %v", err) } if backupCalled { @@ -297,7 +292,7 @@ func TestBootstrapRoundSkipsBackupWhenNoBootstrapPeers(t *testing.T) { // TestBootstrapRoundDialsBackupWhenBootstrapPeersPresent confirms the backup // list is still consulted when configured bootstrap peers fail to connect, -// preserving the recovery mechanism from #8856. +// preserving the recovery mechanism from ipfs/kubo#8856. func TestBootstrapRoundDialsBackupWhenBootstrapPeersPresent(t *testing.T) { backupCalled := false loadFunc := func(_ context.Context) []peer.AddrInfo { @@ -318,11 +313,7 @@ func TestBootstrapRoundDialsBackupWhenBootstrapPeersPresent(t *testing.T) { // Keep the round snappy: the dial will fail fast against a random peer ID. bootCfg.ConnectionTimeout = 500 * time.Millisecond - priv, pub, err := crypto.GenerateEd25519Key(rand.Reader) - if err != nil { - t.Fatal(err) - } - peerID, err := peer.IDFromPublicKey(pub) + priv, _, err := crypto.GenerateEd25519Key(rand.Reader) if err != nil { t.Fatal(err) } @@ -332,8 +323,7 @@ func TestBootstrapRoundDialsBackupWhenBootstrapPeersPresent(t *testing.T) { } t.Cleanup(func() { _ = p2pHost.Close() }) - _ = peerID - _ = bootstrapRound(context.Background(), p2pHost, bootCfg) + _ = bootstrapRound(t.Context(), p2pHost, bootCfg) if !backupCalled { t.Fatal("bootstrapRound did not consult the backup peer list despite configured bootstrap peers failing to connect") }