Skip to content

Commit 4bfec00

Browse files
authored
Merge pull request #359 from dan-r/fix-ipfs-private-network-peering
fix: private IPFS swarm nodes never peer, breaking shared storage
2 parents 42476ef + 41405ff commit 4bfec00

4 files changed

Lines changed: 109 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: 86 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,73 @@ 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+
624+
// swarm/peering/add only registers the peer with Kubo's background
625+
// reconnect service - it returns success even when the peer is
626+
// unreachable, so it does not confirm a connection was made.
627+
peeringURL := fmt.Sprintf("http://127.0.0.1:%d/api/v0/swarm/peering/add?arg=%s", member.ExposedIPFSApiPort, addr)
628+
if err := core.RequestWithRetry(s.ctx, http.MethodPost, peeringURL, nil, nil); err != nil {
629+
return fmt.Errorf("failed to peer IPFS node %s with %s: %w", member.ID, other.ID, err)
630+
}
631+
632+
// swarm/connect dials synchronously and errors if the connection
633+
// fails, so retrying it confirms the nodes are actually connected
634+
// rather than just registered to reconnect in the background.
635+
connectURL := fmt.Sprintf("http://127.0.0.1:%d/api/v0/swarm/connect?arg=%s", member.ExposedIPFSApiPort, addr)
636+
if err := core.RequestWithRetry(s.ctx, http.MethodPost, connectURL, nil, nil); err != nil {
637+
return fmt.Errorf("failed to connect IPFS node %s to %s: %w", member.ID, other.ID, err)
638+
}
639+
}
640+
}
641+
return nil
642+
}
643+
569644
func (s *StackManager) createMember(id string, index int, options *types.InitOptions, external bool) (*types.Organization, error) {
570645
serviceBase := options.ServicesBasePort + (index * 100)
571646
ptmBase := options.PtmBasePort + (index * 10)
@@ -908,6 +983,10 @@ func (s *StackManager) runFirstTimeSetup(options *types.StartOptions) (messages
908983
return messages, err
909984
}
910985

986+
if err := s.copyIPFSInitScriptToVolumes(); err != nil {
987+
return messages, err
988+
}
989+
911990
pullOptions := &types.PullOptions{
912991
Retries: 2,
913992
}
@@ -1035,6 +1114,13 @@ func (s *StackManager) runFirstTimeSetup(options *types.StartOptions) (messages
10351114
return messages, err
10361115
}
10371116

1117+
// Peer the IPFS nodes on the finalized containers, after the config
1118+
// restart above, so peering is applied to the containers that will keep
1119+
// running rather than relying on it surviving the restart.
1120+
if err := s.peerIPFSNodes(); err != nil {
1121+
return messages, err
1122+
}
1123+
10381124
if s.Stack.MultipartyEnabled {
10391125
if s.Stack.ContractAddress == "" {
10401126
s.Log.Info("registering FireFly identities")

0 commit comments

Comments
 (0)