Skip to content

Commit 037d09c

Browse files
committed
fix: private IPFS swarm nodes never peer, breaking shared storage
The pinned ipfs/go-ipfs:v0.10.0 image has a private-network (pnet) connection bug where swarm connections drop immediately after handshake, so org IPFS nodes never peer with each other. This causes "failed to bootstrap (no peers found)" and shared storage downloads to time out fetching content pinned only on another member's node. Bump to ipfs/kubo:v0.42.0, and since modern Kubo refuses to start in private-network mode with its default AutoConf/public bootstrap config, add a container-init.d script that disables it for private-mode stacks. mDNS auto-discovery, which worked locally, proved unreliable on native Linux Docker bridge networks - members' nodes never found each other there. Add an explicit peering step after first-time-setup that queries each member's real PeerID via its own API and calls swarm/peering/add against every other member, so nodes connect (and persistently reconnect) regardless of whether mDNS works in a given environment. Signed-off-by: Enrique Lacal <enrique.lacal@kaleido.io>
1 parent 42476ef commit 037d09c

4 files changed

Lines changed: 94 additions & 1 deletion

File tree

internal/constants/constants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323

2424
var StacksDir = checkHome()
2525
var FireFlyCoreImageName = "ghcr.io/hyperledger-firefly/firefly"
26-
var IPFSImageName = "ipfs/go-ipfs:v0.10.0"
26+
var IPFSImageName = "ipfs/kubo:v0.42.0"
2727
var PostgresImageName = "postgres"
2828
var PrometheusImageName = "prom/prometheus"
2929
var SandboxImageName = "ghcr.io/hyperledger-firefly/sandbox:latest"

internal/docker/docker_config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig {
167167
"LIBP2P_FORCE_PNET": "1",
168168
},
169169
)
170+
// Kubo's AutoConf/public-network defaults are incompatible with a
171+
// private swarm and prevent peering with other members unless
172+
// disabled via a container-init.d script - see ipfs_config.go.
173+
sharedStorage.Volumes = append(sharedStorage.Volumes, fmt.Sprintf("ipfs_init_%s:/container-init.d", member.ID))
174+
compose.Volumes[fmt.Sprintf("ipfs_init_%s", member.ID)] = struct{}{}
170175
} else {
171176
sharedStorage.Environment = s.EnvironmentVars
172177
}

internal/stacks/ipfs_config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,20 @@ func GenerateSwarmKey() (string, error) {
2929
hexKey := hex.EncodeToString(key)
3030
return "/key/swarm/psk/1.0.0/\n/base16/\n" + hexKey, nil
3131
}
32+
33+
// GenerateIPFSPrivateNetInitScript returns a container-init.d script that
34+
// disables Kubo's AutoConf/public-network defaults, which are incompatible
35+
// with a private swarm (swarm.key / LIBP2P_FORCE_PNET) and otherwise prevent
36+
// the daemon from starting or from ever peering with other members.
37+
func GenerateIPFSPrivateNetInitScript() string {
38+
return `#!/bin/sh
39+
ipfs config --json AutoConf.Enabled false
40+
ipfs config --json Bootstrap '[]'
41+
ipfs config --json DNS.Resolvers '{}'
42+
ipfs config --json Routing.DelegatedRouters '[]'
43+
ipfs config --json Ipns.DelegatedPublishers '[]'
44+
ipfs config Routing.Type dht
45+
ipfs config --json AutoTLS.Enabled false
46+
ipfs config --json Swarm.Transports.Network.Websocket false
47+
`
48+
}

internal/stacks/stack_manager.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"fmt"
2323
"net"
24+
"net/http"
2425
"os"
2526
"os/exec"
2627
"path"
@@ -505,6 +506,13 @@ func (s *StackManager) writeConfig(options *types.InitOptions) error {
505506
}
506507
}
507508

509+
if s.Stack.IPFSMode.Equals(types.IPFSModePrivate) {
510+
initScript := GenerateIPFSPrivateNetInitScript()
511+
if err := os.WriteFile(path.Join(s.Stack.InitDir, "config", "ipfs_privatenet_init.sh"), []byte(initScript), 0755); err != nil {
512+
return err
513+
}
514+
}
515+
508516
return nil
509517
}
510518

@@ -566,6 +574,61 @@ func (s *StackManager) copyDataExchangeConfigToVolumes() error {
566574
return nil
567575
}
568576

577+
func (s *StackManager) copyIPFSInitScriptToVolumes() error {
578+
if !s.Stack.IPFSMode.Equals(types.IPFSModePrivate) {
579+
return nil
580+
}
581+
configDir := filepath.Join(s.Stack.RuntimeDir, "config")
582+
scriptPath := path.Join(configDir, "ipfs_privatenet_init.sh")
583+
for _, member := range s.Stack.Members {
584+
volumeName := fmt.Sprintf("%s_ipfs_init_%s", s.Stack.Name, member.ID)
585+
if err := docker.CopyFileToVolume(s.ctx, volumeName, scriptPath, "/privatenet-init.sh"); err != nil {
586+
return err
587+
}
588+
}
589+
return nil
590+
}
591+
592+
// peerIPFSNodes explicitly peers every private-mode IPFS node with every
593+
// other member's node. mDNS auto-discovery has proven unreliable across
594+
// different Docker networking environments (it connected nodes on Docker
595+
// Desktop but not on a native Linux Docker bridge network, such as GitHub
596+
// Actions runners use), so without this, members' IPFS nodes may never
597+
// connect to each other and shared storage downloads will hang/time out.
598+
func (s *StackManager) peerIPFSNodes() error {
599+
if !s.Stack.IPFSMode.Equals(types.IPFSModePrivate) || len(s.Stack.Members) < 2 {
600+
return nil
601+
}
602+
603+
type ipfsIDResponse struct {
604+
ID string `json:"ID"`
605+
}
606+
607+
peerIDs := make(map[string]string, len(s.Stack.Members))
608+
for _, member := range s.Stack.Members {
609+
var idResp ipfsIDResponse
610+
url := fmt.Sprintf("http://127.0.0.1:%d/api/v0/id", member.ExposedIPFSApiPort)
611+
if err := core.RequestWithRetry(s.ctx, http.MethodPost, url, nil, &idResp); err != nil {
612+
return fmt.Errorf("failed to get IPFS peer ID for member %s: %w", member.ID, err)
613+
}
614+
peerIDs[member.ID] = idResp.ID
615+
}
616+
617+
for _, member := range s.Stack.Members {
618+
for _, other := range s.Stack.Members {
619+
if member.ID == other.ID {
620+
continue
621+
}
622+
addr := fmt.Sprintf("/dns4/ipfs_%s/tcp/4001/p2p/%s", other.ID, peerIDs[other.ID])
623+
url := fmt.Sprintf("http://127.0.0.1:%d/api/v0/swarm/peering/add?arg=%s", member.ExposedIPFSApiPort, addr)
624+
if err := core.RequestWithRetry(s.ctx, http.MethodPost, url, nil, nil); err != nil {
625+
return fmt.Errorf("failed to peer IPFS node %s with %s: %w", member.ID, other.ID, err)
626+
}
627+
}
628+
}
629+
return nil
630+
}
631+
569632
func (s *StackManager) createMember(id string, index int, options *types.InitOptions, external bool) (*types.Organization, error) {
570633
serviceBase := options.ServicesBasePort + (index * 100)
571634
ptmBase := options.PtmBasePort + (index * 10)
@@ -908,6 +971,10 @@ func (s *StackManager) runFirstTimeSetup(options *types.StartOptions) (messages
908971
return messages, err
909972
}
910973

974+
if err := s.copyIPFSInitScriptToVolumes(); err != nil {
975+
return messages, err
976+
}
977+
911978
pullOptions := &types.PullOptions{
912979
Retries: 2,
913980
}
@@ -919,6 +986,10 @@ func (s *StackManager) runFirstTimeSetup(options *types.StartOptions) (messages
919986
return messages, err
920987
}
921988

989+
if err := s.peerIPFSNodes(); err != nil {
990+
return messages, err
991+
}
992+
922993
for i, tp := range s.tokenProviders {
923994
if !s.Stack.DisableTokenFactories {
924995
result, err := tp.DeploySmartContracts(i)

0 commit comments

Comments
 (0)