@@ -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+
569644func (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