Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 15 additions & 5 deletions bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
84 changes: 84 additions & 0 deletions bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}