From a6f49ae8ee512571bebff2f37aae980f80230efd Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Thu, 31 Mar 2022 19:12:36 -0300 Subject: [PATCH 01/10] feat(cmds): add version check command --- core/commands/commands_test.go | 1 + core/commands/version.go | 149 ++++++++++++++++++++++++- docs/examples/kubo-as-a-library/go.mod | 1 + docs/examples/kubo-as-a-library/go.sum | 2 + go.mod | 1 + go.sum | 2 + 6 files changed, 151 insertions(+), 5 deletions(-) diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index 38172fd66d9..018b6734e79 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -199,6 +199,7 @@ func TestCommands(t *testing.T) { "/swarm/resources", "/update", "/version", + "/version/check", "/version/deps", } diff --git a/core/commands/version.go b/core/commands/version.go index e404074fe75..541a49f9c30 100644 --- a/core/commands/version.go +++ b/core/commands/version.go @@ -5,17 +5,24 @@ import ( "fmt" "io" "runtime/debug" + "strings" version "github.com/ipfs/kubo" + "github.com/ipfs/kubo/core/commands/cmdenv" cmds "github.com/ipfs/go-ipfs-cmds" + + versioncmp "github.com/hashicorp/go-version" + "github.com/libp2p/go-libp2p-kad-dht/fullrt" + pstore "github.com/libp2p/go-libp2p/core/peerstore" ) const ( - versionNumberOptionName = "number" - versionCommitOptionName = "commit" - versionRepoOptionName = "repo" - versionAllOptionName = "all" + versionNumberOptionName = "number" + versionCommitOptionName = "commit" + versionRepoOptionName = "repo" + versionAllOptionName = "all" + versionCompareNewFractionOptionName = "--newer-fraction" ) var VersionCmd = &cmds.Command{ @@ -24,7 +31,8 @@ var VersionCmd = &cmds.Command{ ShortDescription: "Returns the current version of IPFS and exits.", }, Subcommands: map[string]*cmds.Command{ - "deps": depsVersionCommand, + "deps": depsVersionCommand, + "check": checkVersionCommand, }, Options: []cmds.Option{ @@ -130,3 +138,134 @@ Print out all dependencies and their versions.`, }), }, } + +type CheckOutput struct { + PeersCounted int + GreatestVersion string + OldVersion bool +} + +var checkVersionCommand = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "Checks IPFS version against network (online only).", + ShortDescription: ` +Checks node versions in our DHT to compare if we're running an older version.`, + }, + Options: []cmds.Option{ + cmds.FloatOption(versionCompareNewFractionOptionName, "f", "Fraction of peers with new version to generate update warning.").WithDefault(0.1), + }, + Type: CheckOutput{}, + + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + nd, err := cmdenv.GetNode(env) + if err != nil { + return err + } + + if !nd.IsOnline { + return ErrNotOnline + } + + if nd.DHT == nil { + return ErrNotDHT + } + + ourVersion, err := versioncmp.NewVersion(strings.Replace(version.CurrentVersionNumber, "-dev", "", -1)) + if err != nil { + return fmt.Errorf("could not parse our own version %s: %w", + version.CurrentVersionNumber, err) + } + + greatestVersionSeen := ourVersion + totalPeersCounted := 1 // Us (and to avoid division-by-zero edge case). + withGreaterVersion := 0 + + recordPeerVersion := func(agentVersion string) { + // We process the version as is it assembled in GetUserAgentVersion. + segments := strings.Split(agentVersion, "/") + if len(segments) < 2 { + return + } + if segments[0] != "kubo" { + return + } + versionNumber := segments[1] // As in our CurrentVersionNumber. + + // Ignore development releases. + if strings.Contains(versionNumber, "-dev") { + return + } + if strings.Contains(versionNumber, "-rc") { + return + } + + peerVersion, err := versioncmp.NewVersion(versionNumber) + if err != nil { + // Do not error on invalid remote versions, just ignore. + return + } + + // Valid peer version number. + totalPeersCounted += 1 + if ourVersion.LessThan(peerVersion) { + withGreaterVersion += 1 + } + if peerVersion.GreaterThan(greatestVersionSeen) { + greatestVersionSeen = peerVersion + } + } + + // Logic taken from `ipfs stats dht` command. + if nd.DHTClient != nd.DHT { + client, ok := nd.DHTClient.(*fullrt.FullRT) + if !ok { + return cmds.Errorf(cmds.ErrClient, "could not generate stats for the WAN DHT client type") + } + for _, p := range client.Stat() { + if ver, err := nd.Peerstore.Get(p, "AgentVersion"); err == nil { + recordPeerVersion(ver.(string)) + } else if err == pstore.ErrNotFound { + // ignore + } else { + // this is a bug, usually. + log.Errorw( + "failed to get agent version from peerstore", + "error", err, + ) + } + } + } else { + for _, pi := range nd.DHT.WAN.RoutingTable().GetPeerInfos() { + if ver, err := nd.Peerstore.Get(pi.Id, "AgentVersion"); err == nil { + recordPeerVersion(ver.(string)) + } else if err == pstore.ErrNotFound { + // ignore + } else { + // this is a bug, usually. + log.Errorw( + "failed to get agent version from peerstore", + "error", err, + ) + } + } + } + + newerFraction, _ := req.Options[versionCompareNewFractionOptionName].(float64) + if err := cmds.EmitOnce(res, CheckOutput{ + PeersCounted: totalPeersCounted, + GreatestVersion: greatestVersionSeen.String(), + OldVersion: (float64(withGreaterVersion) / float64(totalPeersCounted)) > newerFraction, + }); err != nil { + return err + } + return nil + }, + Encoders: cmds.EncoderMap{ + cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, checkOutput CheckOutput) error { + if checkOutput.OldVersion { + fmt.Fprintf(w, "⚠️WARNING: this Kubo node is running an outdated version compared to other peers, update to %s\n", checkOutput.GreatestVersion) + } + return nil + }), + }, +} diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 7f12b79367d..01e015b04e7 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -60,6 +60,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huin/goupnp v1.3.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 183aad046a6..3d98062103d 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -246,6 +246,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= diff --git a/go.mod b/go.mod index 2abdfacae10..cfc62f3d8b5 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-version v1.6.0 github.com/ipfs-shipyard/nopfs v0.0.12 github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c github.com/ipfs/boxo v0.20.0 diff --git a/go.sum b/go.sum index b02bced7a67..248db262acf 100644 --- a/go.sum +++ b/go.sum @@ -310,6 +310,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= From ad5cbffcaf0001b911f840d1728804e10645415a Mon Sep 17 00:00:00 2001 From: Matrix89 Date: Tue, 4 Jun 2024 23:39:20 +0200 Subject: [PATCH 02/10] feat(core): add periodic version check --- cmd/ipfs/kubo/daemon.go | 2 + core/commands/version.go | 112 ++++----------------------------- core/ver_checker.go | 130 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 100 deletions(-) create mode 100644 core/ver_checker.go diff --git a/cmd/ipfs/kubo/daemon.go b/cmd/ipfs/kubo/daemon.go index ab034b20a14..194d9796e9e 100644 --- a/cmd/ipfs/kubo/daemon.go +++ b/cmd/ipfs/kubo/daemon.go @@ -575,6 +575,8 @@ take effect. // start MFS pinning thread startPinMFS(daemonConfigPollInterval, cctx, &ipfsPinMFSNode{node}) + core.StartVersionChecker(node) + // The daemon is *finally* ready. fmt.Printf("Daemon is ready\n") notifyReady() diff --git a/core/commands/version.go b/core/commands/version.go index 541a49f9c30..a86373e28a8 100644 --- a/core/commands/version.go +++ b/core/commands/version.go @@ -5,16 +5,11 @@ import ( "fmt" "io" "runtime/debug" - "strings" + cmds "github.com/ipfs/go-ipfs-cmds" version "github.com/ipfs/kubo" + "github.com/ipfs/kubo/core" "github.com/ipfs/kubo/core/commands/cmdenv" - - cmds "github.com/ipfs/go-ipfs-cmds" - - versioncmp "github.com/hashicorp/go-version" - "github.com/libp2p/go-libp2p-kad-dht/fullrt" - pstore "github.com/libp2p/go-libp2p/core/peerstore" ) const ( @@ -139,12 +134,6 @@ Print out all dependencies and their versions.`, }, } -type CheckOutput struct { - PeersCounted int - GreatestVersion string - OldVersion bool -} - var checkVersionCommand = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "Checks IPFS version against network (online only).", @@ -154,13 +143,10 @@ Checks node versions in our DHT to compare if we're running an older version.`, Options: []cmds.Option{ cmds.FloatOption(versionCompareNewFractionOptionName, "f", "Fraction of peers with new version to generate update warning.").WithDefault(0.1), }, - Type: CheckOutput{}, + Type: core.VersionCheckOutput{}, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { nd, err := cmdenv.GetNode(env) - if err != nil { - return err - } if !nd.IsOnline { return ErrNotOnline @@ -170,100 +156,26 @@ Checks node versions in our DHT to compare if we're running an older version.`, return ErrNotDHT } - ourVersion, err := versioncmp.NewVersion(strings.Replace(version.CurrentVersionNumber, "-dev", "", -1)) if err != nil { - return fmt.Errorf("could not parse our own version %s: %w", - version.CurrentVersionNumber, err) + return err } - greatestVersionSeen := ourVersion - totalPeersCounted := 1 // Us (and to avoid division-by-zero edge case). - withGreaterVersion := 0 - - recordPeerVersion := func(agentVersion string) { - // We process the version as is it assembled in GetUserAgentVersion. - segments := strings.Split(agentVersion, "/") - if len(segments) < 2 { - return - } - if segments[0] != "kubo" { - return - } - versionNumber := segments[1] // As in our CurrentVersionNumber. - - // Ignore development releases. - if strings.Contains(versionNumber, "-dev") { - return - } - if strings.Contains(versionNumber, "-rc") { - return - } - - peerVersion, err := versioncmp.NewVersion(versionNumber) - if err != nil { - // Do not error on invalid remote versions, just ignore. - return - } - - // Valid peer version number. - totalPeersCounted += 1 - if ourVersion.LessThan(peerVersion) { - withGreaterVersion += 1 - } - if peerVersion.GreaterThan(greatestVersionSeen) { - greatestVersionSeen = peerVersion - } - } + newerFraction, _ := req.Options[versionCompareNewFractionOptionName].(float64) + output, err := core.CheckVersion(nd, newerFraction) - // Logic taken from `ipfs stats dht` command. - if nd.DHTClient != nd.DHT { - client, ok := nd.DHTClient.(*fullrt.FullRT) - if !ok { - return cmds.Errorf(cmds.ErrClient, "could not generate stats for the WAN DHT client type") - } - for _, p := range client.Stat() { - if ver, err := nd.Peerstore.Get(p, "AgentVersion"); err == nil { - recordPeerVersion(ver.(string)) - } else if err == pstore.ErrNotFound { - // ignore - } else { - // this is a bug, usually. - log.Errorw( - "failed to get agent version from peerstore", - "error", err, - ) - } - } - } else { - for _, pi := range nd.DHT.WAN.RoutingTable().GetPeerInfos() { - if ver, err := nd.Peerstore.Get(pi.Id, "AgentVersion"); err == nil { - recordPeerVersion(ver.(string)) - } else if err == pstore.ErrNotFound { - // ignore - } else { - // this is a bug, usually. - log.Errorw( - "failed to get agent version from peerstore", - "error", err, - ) - } - } + if err != nil { + return err } - newerFraction, _ := req.Options[versionCompareNewFractionOptionName].(float64) - if err := cmds.EmitOnce(res, CheckOutput{ - PeersCounted: totalPeersCounted, - GreatestVersion: greatestVersionSeen.String(), - OldVersion: (float64(withGreaterVersion) / float64(totalPeersCounted)) > newerFraction, - }); err != nil { + if err := cmds.EmitOnce(res, output); err != nil { return err } return nil }, Encoders: cmds.EncoderMap{ - cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, checkOutput CheckOutput) error { - if checkOutput.OldVersion { - fmt.Fprintf(w, "⚠️WARNING: this Kubo node is running an outdated version compared to other peers, update to %s\n", checkOutput.GreatestVersion) + cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, checkOutput core.VersionCheckOutput) error { + if checkOutput.IsOutdated { + fmt.Fprint(w, checkOutput.Msg) } return nil }), diff --git a/core/ver_checker.go b/core/ver_checker.go new file mode 100644 index 00000000000..4cc3b4897a9 --- /dev/null +++ b/core/ver_checker.go @@ -0,0 +1,130 @@ +package core + +import ( + "errors" + "fmt" + versioncmp "github.com/hashicorp/go-version" + version "github.com/ipfs/kubo" + "github.com/libp2p/go-libp2p-kad-dht/fullrt" + pstore "github.com/libp2p/go-libp2p/core/peerstore" + "strings" + "time" +) + +type VersionCheckOutput struct { + IsOutdated bool + Msg string +} + +func StartVersionChecker(nd *IpfsNode) { + ticker := time.NewTicker(time.Hour) + go func() { + for { + output, err := CheckVersion(nd, 0.1) + if err != nil { + log.Errorw("Failed to check version", "error", err) + } + if output.IsOutdated { + fmt.Println(output.Msg) + } + + select { + case <-nd.Process.Closing(): + return + case <-ticker.C: + continue + } + } + }() +} + +func CheckVersion(nd *IpfsNode, newerFraction float64) (VersionCheckOutput, error) { + ourVersion, err := versioncmp.NewVersion(strings.Replace(version.CurrentVersionNumber, "-dev", "", -1)) + if err != nil { + return VersionCheckOutput{}, fmt.Errorf("could not parse our own version %s: %w", + version.CurrentVersionNumber, err) + } + + greatestVersionSeen := ourVersion + totalPeersCounted := 1 // Us (and to avoid division-by-zero edge case). + withGreaterVersion := 0 + + recordPeerVersion := func(agentVersion string) { + // We process the version as is it assembled in GetUserAgentVersion. + segments := strings.Split(agentVersion, "/") + if len(segments) < 2 { + return + } + if segments[0] != "kubo" { + return + } + versionNumber := segments[1] // As in our CurrentVersionNumber. + + // Ignore development releases. + if strings.Contains(versionNumber, "-dev") { + return + } + if strings.Contains(versionNumber, "-rc") { + return + } + + peerVersion, err := versioncmp.NewVersion(versionNumber) + if err != nil { + // Do not error on invalid remote versions, just ignore. + return + } + + // Valid peer version number. + totalPeersCounted += 1 + if ourVersion.LessThan(peerVersion) { + withGreaterVersion += 1 + } + if peerVersion.GreaterThan(greatestVersionSeen) { + greatestVersionSeen = peerVersion + } + } + + // Logic taken from `ipfs stats dht` command. + if nd.DHTClient != nd.DHT { + client, ok := nd.DHTClient.(*fullrt.FullRT) + if !ok { + return VersionCheckOutput{}, errors.New("could not generate stats for the WAN DHT client type") + } + for _, p := range client.Stat() { + if ver, err := nd.Peerstore.Get(p, "AgentVersion"); err == nil { + recordPeerVersion(ver.(string)) + } else if errors.Is(err, pstore.ErrNotFound) { + // ignore + } else { + // this is a bug, usually. + log.Errorw( + "failed to get agent version from peerstore", + "error", err, + ) + } + } + } else { + for _, pi := range nd.DHT.WAN.RoutingTable().GetPeerInfos() { + if ver, err := nd.Peerstore.Get(pi.Id, "AgentVersion"); err == nil { + recordPeerVersion(ver.(string)) + } else if errors.Is(err, pstore.ErrNotFound) { + // ignore + } else { + // this is a bug, usually. + log.Errorw( + "failed to get agent version from peerstore", + "error", err, + ) + } + } + } + + if (float64(withGreaterVersion) / float64(totalPeersCounted)) > newerFraction { + return VersionCheckOutput{ + IsOutdated: true, + Msg: fmt.Sprintf("⚠️WARNING: this Kubo node is running an outdated version compared to other peers, update to %s\n", greatestVersionSeen.String()), + }, nil + } else { + return VersionCheckOutput{}, nil + } +} From cae64309b060386061638750c7292826825a39fd Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 20 Jun 2024 00:34:18 +0200 Subject: [PATCH 03/10] refactor: defensive version checks This refactor applies changes listed in https://github.com/ipfs/kubo/pull/10438#issuecomment-2178899255 namely - removes surface for panics on custom routing configurations - avoids running the check until one minute after node start, to allow for peerbook to populate - allow disabling version check via env. variable (because there will be ask for this anyway) - make RPC command JSON more useful by including information about the size of sampled kubo nodes - make message more friendly to less technical users --- cmd/ipfs/kubo/daemon.go | 46 ++++++++++- core/commands/version.go | 143 ++++++++++++++++++++++++++++------ core/ver_checker.go | 130 ------------------------------- docs/environment-variables.md | 7 ++ 4 files changed, 170 insertions(+), 156 deletions(-) delete mode 100644 core/ver_checker.go diff --git a/cmd/ipfs/kubo/daemon.go b/cmd/ipfs/kubo/daemon.go index 194d9796e9e..2c88be2ff83 100644 --- a/cmd/ipfs/kubo/daemon.go +++ b/cmd/ipfs/kubo/daemon.go @@ -1,9 +1,11 @@ package kubo import ( + "context" "errors" _ "expvar" "fmt" + "math" "net" "net/http" _ "net/http/pprof" @@ -575,8 +577,6 @@ take effect. // start MFS pinning thread startPinMFS(daemonConfigPollInterval, cctx, &ipfsPinMFSNode{node}) - core.StartVersionChecker(node) - // The daemon is *finally* ready. fmt.Printf("Daemon is ready\n") notifyReady() @@ -612,6 +612,10 @@ take effect. } if len(peers) == 0 { log.Error("failed to bootstrap (no peers found): consider updating Bootstrap or Peering section of your config") + } else { + // After 1 minute we should have enough peers + // to run informed version check + startVersionChecker(cctx.Context(), node) } }) } @@ -1054,3 +1058,41 @@ func printVersion() { fmt.Printf("System version: %s\n", runtime.GOARCH+"/"+runtime.GOOS) fmt.Printf("Golang version: %s\n", runtime.Version()) } + +func startVersionChecker(ctx context.Context, nd *core.IpfsNode) { + if os.Getenv("KUBO_VERSION_CHECK") == "false" { + return + } + ticker := time.NewTicker(time.Hour) + defer ticker.Stop() + go func() { + for { + o, err := commands.DetectNewKuboVersion(nd, commands.DefaultMinimalVersionFraction) + if err != nil { + // The version check is best-effort, and may fail in custom + // configurations that do not run standard WAN DHT. If it + // errors here, no point in spamming logs: og once and exit. + log.Errorw("initial version check failed, will not be run again", "error", err) + return + } + if o.UpdateAvailable { + newerPercent := fmt.Sprintf("%.0f%%", math.Round(float64(o.WithGreaterVersion)/float64(o.PeersSampled)*100)) + log.Errorf(` +⚠️ A NEW VERSION OF KUBO DETECTED + +This Kubo node is running an outdated version (%s). +%s of the sampled Kubo peers are running a higher version. +Visit https://github.com/ipfs/kubo/releases or https://dist.ipfs.tech/#kubo and update to version %s or later.`, + o.RunningVersion, newerPercent, o.GreatestVersion) + } + select { + case <-ctx.Done(): + return + case <-nd.Process.Closing(): + return + case <-ticker.C: + continue + } + } + }() +} diff --git a/core/commands/version.go b/core/commands/version.go index a86373e28a8..7a9d88ee606 100644 --- a/core/commands/version.go +++ b/core/commands/version.go @@ -5,11 +5,16 @@ import ( "fmt" "io" "runtime/debug" + "strings" + versioncmp "github.com/hashicorp/go-version" cmds "github.com/ipfs/go-ipfs-cmds" version "github.com/ipfs/kubo" "github.com/ipfs/kubo/core" "github.com/ipfs/kubo/core/commands/cmdenv" + "github.com/libp2p/go-libp2p-kad-dht/fullrt" + peer "github.com/libp2p/go-libp2p/core/peer" + pstore "github.com/libp2p/go-libp2p/core/peerstore" ) const ( @@ -17,7 +22,7 @@ const ( versionCommitOptionName = "commit" versionRepoOptionName = "repo" versionAllOptionName = "all" - versionCompareNewFractionOptionName = "--newer-fraction" + versionCompareNewFractionOptionName = "min-fraction" ) var VersionCmd = &cmds.Command{ @@ -134,35 +139,38 @@ Print out all dependencies and their versions.`, }, } +const DefaultMinimalVersionFraction = 0.05 // 5% + +type VersionCheckOutput struct { + UpdateAvailable bool + RunningVersion string + GreatestVersion string + PeersSampled int + WithGreaterVersion int +} + var checkVersionCommand = &cmds.Command{ Helptext: cmds.HelpText{ - Tagline: "Checks IPFS version against network (online only).", - ShortDescription: ` -Checks node versions in our DHT to compare if we're running an older version.`, + Tagline: "Checks Kubo version against connected peers.", + ShortDescription: "Uses the libp2p identify protocol to check the AgentVersion of connected peers and determine if running Kubo version is outdated. Peers with AgentVersion that does not start with 'kubo' are ignored.", }, Options: []cmds.Option{ - cmds.FloatOption(versionCompareNewFractionOptionName, "f", "Fraction of peers with new version to generate update warning.").WithDefault(0.1), + cmds.FloatOption(versionCompareNewFractionOptionName, "m", "Minimum fraction of sampled peers with the new Kubo version needed to trigger an update warning.").WithDefault(DefaultMinimalVersionFraction), }, - Type: core.VersionCheckOutput{}, + Type: VersionCheckOutput{}, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { nd, err := cmdenv.GetNode(env) + if err != nil { + return err + } if !nd.IsOnline { return ErrNotOnline } - if nd.DHT == nil { - return ErrNotDHT - } - - if err != nil { - return err - } - newerFraction, _ := req.Options[versionCompareNewFractionOptionName].(float64) - output, err := core.CheckVersion(nd, newerFraction) - + output, err := DetectNewKuboVersion(nd, newerFraction) if err != nil { return err } @@ -172,12 +180,99 @@ Checks node versions in our DHT to compare if we're running an older version.`, } return nil }, - Encoders: cmds.EncoderMap{ - cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, checkOutput core.VersionCheckOutput) error { - if checkOutput.IsOutdated { - fmt.Fprint(w, checkOutput.Msg) - } - return nil - }), - }, +} + +// DetectNewKuboVersion observers kubo version reported by other peers via +// libp2p identify protocol and notifies when threshold fraction of seen swarm +// is running updated Kubo. It is used by RPC and CLI at 'ipfs version check' +// and also periodically when 'ipfs daemon' is running. +func DetectNewKuboVersion(nd *core.IpfsNode, minFraction float64) (VersionCheckOutput, error) { + ourVersion, err := versioncmp.NewVersion(version.CurrentVersionNumber) + if err != nil { + return VersionCheckOutput{}, fmt.Errorf("could not parse our own version %q: %w", + version.CurrentVersionNumber, err) + } + // MAJOR.MINOR.PATCH without any suffix + ourVersion = ourVersion.Core() + + greatestVersionSeen := ourVersion + totalPeersSampled := 1 // Us (and to avoid division-by-zero edge case) + withGreaterVersion := 0 + + recordPeerVersion := func(agentVersion string) { + // We process the version as is it assembled in GetUserAgentVersion + segments := strings.Split(agentVersion, "/") + if len(segments) < 2 { + return + } + if segments[0] != "kubo" { + return + } + versionNumber := segments[1] // As in our CurrentVersionNumber + + peerVersion, err := versioncmp.NewVersion(versionNumber) + if err != nil { + // Do not error on invalid remote versions, just ignore + return + } + + // Ignore prerelases and development releases (-dev, -rcX) + if peerVersion.Metadata() != "" || peerVersion.Prerelease() != "" { + return + } + + // MAJOR.MINOR.PATCH without any suffix + peerVersion = peerVersion.Core() + + // Valid peer version number + totalPeersSampled += 1 + if ourVersion.LessThan(peerVersion) { + withGreaterVersion += 1 + } + if peerVersion.GreaterThan(greatestVersionSeen) { + greatestVersionSeen = peerVersion + } + } + + processPeerstoreEntry := func(id peer.ID) { + if v, err := nd.Peerstore.Get(id, "AgentVersion"); err == nil { + recordPeerVersion(v.(string)) + } else if errors.Is(err, pstore.ErrNotFound) { // ignore noop + } else { // a bug, usually. + log.Errorw("failed to get agent version from peerstore", "error", err) + } + } + + // Amino DHT client keeps information about previously seen peers + if nd.DHTClient != nd.DHT && nd.DHTClient != nil { + client, ok := nd.DHTClient.(*fullrt.FullRT) + if !ok { + return VersionCheckOutput{}, errors.New("could not perform version check due to missing or incompatible DHT configuration") + } + for _, p := range client.Stat() { + processPeerstoreEntry(p) + } + } else if nd.DHT != nil && nd.DHT.WAN != nil { + for _, pi := range nd.DHT.WAN.RoutingTable().GetPeerInfos() { + processPeerstoreEntry(pi.Id) + } + } else if nd.DHT != nil && nd.DHT.LAN != nil { + for _, pi := range nd.DHT.LAN.RoutingTable().GetPeerInfos() { + processPeerstoreEntry(pi.Id) + } + } else { + return VersionCheckOutput{}, errors.New("could not perform version check due to missing or incompatible DHT configuration") + } + + // UpdateAvailable flag is set only if minFraction was reached + greaterFraction := float64(withGreaterVersion) / float64(totalPeersSampled) + + // Gathered metric are returned every time + return VersionCheckOutput{ + UpdateAvailable: (greaterFraction >= minFraction), + RunningVersion: ourVersion.String(), + GreatestVersion: greatestVersionSeen.String(), + PeersSampled: totalPeersSampled, + WithGreaterVersion: withGreaterVersion, + }, nil } diff --git a/core/ver_checker.go b/core/ver_checker.go deleted file mode 100644 index 4cc3b4897a9..00000000000 --- a/core/ver_checker.go +++ /dev/null @@ -1,130 +0,0 @@ -package core - -import ( - "errors" - "fmt" - versioncmp "github.com/hashicorp/go-version" - version "github.com/ipfs/kubo" - "github.com/libp2p/go-libp2p-kad-dht/fullrt" - pstore "github.com/libp2p/go-libp2p/core/peerstore" - "strings" - "time" -) - -type VersionCheckOutput struct { - IsOutdated bool - Msg string -} - -func StartVersionChecker(nd *IpfsNode) { - ticker := time.NewTicker(time.Hour) - go func() { - for { - output, err := CheckVersion(nd, 0.1) - if err != nil { - log.Errorw("Failed to check version", "error", err) - } - if output.IsOutdated { - fmt.Println(output.Msg) - } - - select { - case <-nd.Process.Closing(): - return - case <-ticker.C: - continue - } - } - }() -} - -func CheckVersion(nd *IpfsNode, newerFraction float64) (VersionCheckOutput, error) { - ourVersion, err := versioncmp.NewVersion(strings.Replace(version.CurrentVersionNumber, "-dev", "", -1)) - if err != nil { - return VersionCheckOutput{}, fmt.Errorf("could not parse our own version %s: %w", - version.CurrentVersionNumber, err) - } - - greatestVersionSeen := ourVersion - totalPeersCounted := 1 // Us (and to avoid division-by-zero edge case). - withGreaterVersion := 0 - - recordPeerVersion := func(agentVersion string) { - // We process the version as is it assembled in GetUserAgentVersion. - segments := strings.Split(agentVersion, "/") - if len(segments) < 2 { - return - } - if segments[0] != "kubo" { - return - } - versionNumber := segments[1] // As in our CurrentVersionNumber. - - // Ignore development releases. - if strings.Contains(versionNumber, "-dev") { - return - } - if strings.Contains(versionNumber, "-rc") { - return - } - - peerVersion, err := versioncmp.NewVersion(versionNumber) - if err != nil { - // Do not error on invalid remote versions, just ignore. - return - } - - // Valid peer version number. - totalPeersCounted += 1 - if ourVersion.LessThan(peerVersion) { - withGreaterVersion += 1 - } - if peerVersion.GreaterThan(greatestVersionSeen) { - greatestVersionSeen = peerVersion - } - } - - // Logic taken from `ipfs stats dht` command. - if nd.DHTClient != nd.DHT { - client, ok := nd.DHTClient.(*fullrt.FullRT) - if !ok { - return VersionCheckOutput{}, errors.New("could not generate stats for the WAN DHT client type") - } - for _, p := range client.Stat() { - if ver, err := nd.Peerstore.Get(p, "AgentVersion"); err == nil { - recordPeerVersion(ver.(string)) - } else if errors.Is(err, pstore.ErrNotFound) { - // ignore - } else { - // this is a bug, usually. - log.Errorw( - "failed to get agent version from peerstore", - "error", err, - ) - } - } - } else { - for _, pi := range nd.DHT.WAN.RoutingTable().GetPeerInfos() { - if ver, err := nd.Peerstore.Get(pi.Id, "AgentVersion"); err == nil { - recordPeerVersion(ver.(string)) - } else if errors.Is(err, pstore.ErrNotFound) { - // ignore - } else { - // this is a bug, usually. - log.Errorw( - "failed to get agent version from peerstore", - "error", err, - ) - } - } - } - - if (float64(withGreaterVersion) / float64(totalPeersCounted)) > newerFraction { - return VersionCheckOutput{ - IsOutdated: true, - Msg: fmt.Sprintf("⚠️WARNING: this Kubo node is running an outdated version compared to other peers, update to %s\n", greatestVersionSeen.String()), - }, nil - } else { - return VersionCheckOutput{}, nil - } -} diff --git a/docs/environment-variables.md b/docs/environment-variables.md index f0f6b3f183a..aa6c49d3bf3 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -137,6 +137,13 @@ or more advanced tools like [mitmproxy](https://docs.mitmproxy.org/stable/#mitmp Disables the content-blocking subsystem. No denylists will be watched and no content will be blocked. +## `KUBO_VERSION_CHECK` + +Disables periodic `ipfs version check` run by `ipfs daemon` to log when +significant subset of seen Kubo peers run an updated version. + +Default: true + ## `LIBP2P_TCP_REUSEPORT` Kubo tries to reuse the same source port for all connections to improve NAT From e29ffe8b926716fc694c55e48f199c9f441bede1 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 20 Jun 2024 00:57:31 +0200 Subject: [PATCH 04/10] docs: version check release notes --- docs/changelogs/v0.30.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/changelogs/v0.30.md b/docs/changelogs/v0.30.md index e561566d90d..11d439eb1dc 100644 --- a/docs/changelogs/v0.30.md +++ b/docs/changelogs/v0.30.md @@ -6,6 +6,7 @@ - [Overview](#overview) - [🔦 Highlights](#-highlights) + - [Automated `ipfs version check`](#automated-ipfs-version-check) - [📝 Changelog](#-changelog) - [👨‍👩‍👧‍👦 Contributors](#-contributors) @@ -13,6 +14,14 @@ ### 🔦 Highlights +#### Automated `ipfs version check` + +Kubo now performs privacy-preserving version checks using the [libp2p identify protocol](https://github.com/libp2p/specs/blob/master/identify/README.md) on peers detected by the Amino DHT client. +If more than 5% of Kubo peers seen by your node are running a newer version, you will receive a log message notification. + +To disable automated checks, set `KUBO_VERSION_CHECK=false` in your environment. +For manual checks, refer to `ipfs version check --help` for details. + ### 📝 Changelog ### 👨‍👩‍👧‍👦 Contributors From ef968f41ad285f628cd4bbec263e9fa76fbc9690 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 20 Jun 2024 00:59:48 +0200 Subject: [PATCH 05/10] chore: mod tidy --- docs/examples/kubo-as-a-library/go.mod | 1 - docs/examples/kubo-as-a-library/go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 01e015b04e7..7f12b79367d 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -60,7 +60,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huin/goupnp v1.3.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 3d98062103d..183aad046a6 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -246,8 +246,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= From a65a90dc09e426ed61a48656ab8d203216b97547 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 20 Jun 2024 01:14:18 +0200 Subject: [PATCH 06/10] fix: TestCommandDocsWidth --- core/commands/version.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/commands/version.go b/core/commands/version.go index 7a9d88ee606..1bc600793b9 100644 --- a/core/commands/version.go +++ b/core/commands/version.go @@ -151,8 +151,18 @@ type VersionCheckOutput struct { var checkVersionCommand = &cmds.Command{ Helptext: cmds.HelpText{ - Tagline: "Checks Kubo version against connected peers.", - ShortDescription: "Uses the libp2p identify protocol to check the AgentVersion of connected peers and determine if running Kubo version is outdated. Peers with AgentVersion that does not start with 'kubo' are ignored.", + Tagline: "Checks Kubo version against connected peers.", + ShortDescription: ` +This command uses the libp2p identify protocol to check the 'AgentVersion' +of connected peers and see if the Kubo version we're running is outdated. + +Peers with an AgentVersion that doesn't start with 'kubo/' are ignored. +'UpdateAvailable' is set to true only if the 'min-fraction' criteria are met. + +The 'ipfs daemon' does the same check regularly and logs when a new version +is available. You can stop these regular checks by setting +KUBO_VERSION_CHECK=false in your environment. +`, }, Options: []cmds.Option{ cmds.FloatOption(versionCompareNewFractionOptionName, "m", "Minimum fraction of sampled peers with the new Kubo version needed to trigger an update warning.").WithDefault(DefaultMinimalVersionFraction), From 8678bf3c98a049aa6bbd34b3092967e2bbcfd335 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 24 Jul 2024 22:40:31 +0200 Subject: [PATCH 07/10] refactor: Version.SwarmCheckEnabled replacing env variable with proper configuration flag that allows user to enable/disable checks on init etc. --- cmd/ipfs/kubo/daemon.go | 13 ++++++++---- config/config.go | 1 + config/version.go | 14 +++++++++++++ core/commands/version.go | 31 +++++++++++++++++++---------- docs/changelogs/v0.30.md | 10 ++++++++-- docs/config.md | 37 +++++++++++++++++++++++++++++++++++ docs/environment-variables.md | 7 ------- 7 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 config/version.go diff --git a/cmd/ipfs/kubo/daemon.go b/cmd/ipfs/kubo/daemon.go index 2c88be2ff83..ecb6a01d816 100644 --- a/cmd/ipfs/kubo/daemon.go +++ b/cmd/ipfs/kubo/daemon.go @@ -615,7 +615,12 @@ take effect. } else { // After 1 minute we should have enough peers // to run informed version check - startVersionChecker(cctx.Context(), node) + startVersionChecker( + cctx.Context(), + node, + cfg.Version.SwarmCheckEnabled.WithDefault(true), + cfg.Version.SwarmCheckPercentThreshold.WithDefault(config.DefaultSwarmCheckPercentThreshold), + ) } }) } @@ -1059,15 +1064,15 @@ func printVersion() { fmt.Printf("Golang version: %s\n", runtime.Version()) } -func startVersionChecker(ctx context.Context, nd *core.IpfsNode) { - if os.Getenv("KUBO_VERSION_CHECK") == "false" { +func startVersionChecker(ctx context.Context, nd *core.IpfsNode, enabled bool, percentThreshold int64) { + if !enabled { return } ticker := time.NewTicker(time.Hour) defer ticker.Stop() go func() { for { - o, err := commands.DetectNewKuboVersion(nd, commands.DefaultMinimalVersionFraction) + o, err := commands.DetectNewKuboVersion(nd, percentThreshold) if err != nil { // The version check is best-effort, and may fail in custom // configurations that do not run standard WAN DHT. If it diff --git a/config/config.go b/config/config.go index 046c930be93..71365eb0b5e 100644 --- a/config/config.go +++ b/config/config.go @@ -37,6 +37,7 @@ type Config struct { Plugins Plugins Pinning Pinning Import Import + Version Version Internal Internal // experimental/unstable options } diff --git a/config/version.go b/config/version.go new file mode 100644 index 00000000000..8096107bb26 --- /dev/null +++ b/config/version.go @@ -0,0 +1,14 @@ +package config + +const DefaultSwarmCheckPercentThreshold = 5 + +// Version allows controling things like custom user agent and update checks. +type Version struct { + // Optional suffix to the AgentVersion presented by `ipfs id` and exposed + // via libp2p identify protocol. + AgentSuffix *OptionalString `json:",omitempty"` + + // Detect when to warn about new version when observed via libp2p identify + SwarmCheckEnabled Flag `json:",omitempty"` + SwarmCheckPercentThreshold *OptionalInteger `json:",omitempty"` +} diff --git a/core/commands/version.go b/core/commands/version.go index 1bc600793b9..22172688146 100644 --- a/core/commands/version.go +++ b/core/commands/version.go @@ -10,6 +10,7 @@ import ( versioncmp "github.com/hashicorp/go-version" cmds "github.com/ipfs/go-ipfs-cmds" version "github.com/ipfs/kubo" + "github.com/ipfs/kubo/config" "github.com/ipfs/kubo/core" "github.com/ipfs/kubo/core/commands/cmdenv" "github.com/libp2p/go-libp2p-kad-dht/fullrt" @@ -18,11 +19,11 @@ import ( ) const ( - versionNumberOptionName = "number" - versionCommitOptionName = "commit" - versionRepoOptionName = "repo" - versionAllOptionName = "all" - versionCompareNewFractionOptionName = "min-fraction" + versionNumberOptionName = "number" + versionCommitOptionName = "commit" + versionRepoOptionName = "repo" + versionAllOptionName = "all" + versionCheckThresholdOptionName = "min-percent" ) var VersionCmd = &cmds.Command{ @@ -161,11 +162,11 @@ Peers with an AgentVersion that doesn't start with 'kubo/' are ignored. The 'ipfs daemon' does the same check regularly and logs when a new version is available. You can stop these regular checks by setting -KUBO_VERSION_CHECK=false in your environment. +Version.SwarmCheckEnabled:false in the config. `, }, Options: []cmds.Option{ - cmds.FloatOption(versionCompareNewFractionOptionName, "m", "Minimum fraction of sampled peers with the new Kubo version needed to trigger an update warning.").WithDefault(DefaultMinimalVersionFraction), + cmds.IntOption(versionCheckThresholdOptionName, "t", "Percentage (1-100) of sampled peers with the new Kubo version needed to trigger an update warning.").WithDefault(config.DefaultSwarmCheckPercentThreshold), }, Type: VersionCheckOutput{}, @@ -179,8 +180,8 @@ KUBO_VERSION_CHECK=false in your environment. return ErrNotOnline } - newerFraction, _ := req.Options[versionCompareNewFractionOptionName].(float64) - output, err := DetectNewKuboVersion(nd, newerFraction) + minPercent, _ := req.Options[versionCheckThresholdOptionName].(int64) + output, err := DetectNewKuboVersion(nd, minPercent) if err != nil { return err } @@ -196,7 +197,7 @@ KUBO_VERSION_CHECK=false in your environment. // libp2p identify protocol and notifies when threshold fraction of seen swarm // is running updated Kubo. It is used by RPC and CLI at 'ipfs version check' // and also periodically when 'ipfs daemon' is running. -func DetectNewKuboVersion(nd *core.IpfsNode, minFraction float64) (VersionCheckOutput, error) { +func DetectNewKuboVersion(nd *core.IpfsNode, minPercent int64) (VersionCheckOutput, error) { ourVersion, err := versioncmp.NewVersion(version.CurrentVersionNumber) if err != nil { return VersionCheckOutput{}, fmt.Errorf("could not parse our own version %q: %w", @@ -274,6 +275,16 @@ func DetectNewKuboVersion(nd *core.IpfsNode, minFraction float64) (VersionCheckO return VersionCheckOutput{}, errors.New("could not perform version check due to missing or incompatible DHT configuration") } + if minPercent < 1 || minPercent > 100 { + if minPercent == 0 { + minPercent = config.DefaultSwarmCheckPercentThreshold + } else { + return VersionCheckOutput{}, errors.New("Version.SwarmCheckPercentThreshold must be between 1 and 100") + } + } + + minFraction := float64(minPercent) / 100.0 + // UpdateAvailable flag is set only if minFraction was reached greaterFraction := float64(withGreaterVersion) / float64(totalPeersSampled) diff --git a/docs/changelogs/v0.30.md b/docs/changelogs/v0.30.md index 11d439eb1dc..fbd3151ada1 100644 --- a/docs/changelogs/v0.30.md +++ b/docs/changelogs/v0.30.md @@ -19,8 +19,14 @@ Kubo now performs privacy-preserving version checks using the [libp2p identify protocol](https://github.com/libp2p/specs/blob/master/identify/README.md) on peers detected by the Amino DHT client. If more than 5% of Kubo peers seen by your node are running a newer version, you will receive a log message notification. -To disable automated checks, set `KUBO_VERSION_CHECK=false` in your environment. -For manual checks, refer to `ipfs version check --help` for details. +- For manual checks, refer to `ipfs version check --help` for details. +- To disable automated checks, set [`Version.SwarmCheckEnabled`](https://github.com/ipfs/kubo/blob/master/docs/config.md#versionswarmcheckenabled) to `false`. + +#### `Version.AgentSuffix` + +It is now possible to define optional agent version suffix presented in `ipfs id` and exposed via [libp2p identify protocol](https://github.com/libp2p/specs/blob/master/identify/README.md#agentversion). + +See [`Version.AgentSuffix`](https://github.com/ipfs/kubo/blob/master/docs/config.md#agentsuffix). ### 📝 Changelog diff --git a/docs/config.md b/docs/config.md index 130f724d5d2..35370d21c57 100644 --- a/docs/config.md +++ b/docs/config.md @@ -180,6 +180,10 @@ config file at runtime. - [`Import.UnixFSRawLeaves`](#importunixfsrawleaves) - [`Import.UnixFSChunker`](#importunixfschunker) - [`Import.HashFunction`](#importhashfunction) + - [`Version`](#version) + - [`Version.AgentSuffix`](#versionagentsuffix) + - [`Version.SwarmCheckEnabled`](#versionswarmcheckenabled) + - [`Version.SwarmCheckPercentThreshold`](#versionswarmcheckpercentthreshold) ## Profiles @@ -2435,3 +2439,36 @@ The default hash function. Commands affected: `ipfs add`, `ipfs block put`, `ipf Default: `sha2-256` Type: `optionalString` + +## `Version` + +Options to configure agent version announced to the swarm, and leveraging +other peers version for detecting when there is time to update. + +### `Version.AgentSuffix` + +Optional suffix to the AgentVersion presented by `ipfs id` and exposed via [libp2p identify protocol](https://github.com/libp2p/specs/blob/master/identify/README.md#agentversion). + +The value from config is appended to one passed via `ipfs daemon --agent-version-suffix`. + +Default: `""` (no suffix, or value from `ipfs daemon --agent-version-suffix=`) + +Type: `optionaliString` + +### `Version.SwarmCheckEnabled` + +Observe the AgentVersion of swarm peers and log warning when +`SwarmCheckPercentThreshold` of peers runs version higher than this node. + +Default: `true` + +Type: `flag` + +### `Version.SwarmCheckPercentThreshold` + +Control the percentage of `kubo/` peers running new version required to +trigger update warning. + +Default: `5` + +Type: `optionalInteger` (1-100) diff --git a/docs/environment-variables.md b/docs/environment-variables.md index aa6c49d3bf3..f0f6b3f183a 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -137,13 +137,6 @@ or more advanced tools like [mitmproxy](https://docs.mitmproxy.org/stable/#mitmp Disables the content-blocking subsystem. No denylists will be watched and no content will be blocked. -## `KUBO_VERSION_CHECK` - -Disables periodic `ipfs version check` run by `ipfs daemon` to log when -significant subset of seen Kubo peers run an updated version. - -Default: true - ## `LIBP2P_TCP_REUSEPORT` Kubo tries to reuse the same source port for all connections to improve NAT From cfd0c99800ab3c2b94a5bc6e8d5c83740e6aa10f Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 24 Jul 2024 22:57:17 +0200 Subject: [PATCH 08/10] refactor: Version.AgentSuffix wire up json config at Version.AgentSuffix to be applied when present, otherwise use --agent-version-suffix from ipfs daemon as before --- cmd/ipfs/kubo/daemon.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/ipfs/kubo/daemon.go b/cmd/ipfs/kubo/daemon.go index ecb6a01d816..6f0477eb18f 100644 --- a/cmd/ipfs/kubo/daemon.go +++ b/cmd/ipfs/kubo/daemon.go @@ -440,9 +440,11 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment return fmt.Errorf("unrecognized routing option: %s", routingOption) } - agentVersionSuffixString, _ := req.Options[agentVersionSuffix].(string) - if agentVersionSuffixString != "" { - version.SetUserAgentSuffix(agentVersionSuffixString) + // Set optional agent version suffix + versionSuffixFromCli, _ := req.Options[agentVersionSuffix].(string) + versionSuffix := cfg.Version.AgentSuffix.WithDefault(versionSuffixFromCli) + if versionSuffix != "" { + version.SetUserAgentSuffix(versionSuffix) } node, err := core.NewNode(req.Context, ncfg) From 40f61297ee4807bf76fc9225fa6c2d4b305b31b2 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 24 Jul 2024 23:06:39 +0200 Subject: [PATCH 09/10] test: Version.AgentSuffix --- test/sharness/t0026-id.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/sharness/t0026-id.sh b/test/sharness/t0026-id.sh index d4248c56295..992892a39a6 100755 --- a/test/sharness/t0026-id.sh +++ b/test/sharness/t0026-id.sh @@ -65,5 +65,16 @@ iptb stop test_kill_ipfs_daemon +# Version.AgentSuffix overrides --agent-version-suffix (local, offline) +test_expect_success "setting Version.AgentSuffix in config" ' + ipfs config Version.AgentSuffix json-config-suffix +' +test_launch_ipfs_daemon --agent-version-suffix=ignored-cli-suffix +test_expect_success "checking AgentVersion with suffix set via JSON config" ' + test_id_compute_agent json-config-suffix > expected-agent-version && + ipfs id -f "\n" > actual-agent-version && + test_cmp expected-agent-version actual-agent-version +' +test_kill_ipfs_daemon test_done From b4368149286a79079653fcce4d70f6a9428fa850 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 24 Jul 2024 23:26:11 +0200 Subject: [PATCH 10/10] docs: changelog about Version config --- docs/changelogs/v0.30.md | 8 +++++--- docs/config.md | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/changelogs/v0.30.md b/docs/changelogs/v0.30.md index fbd3151ada1..8457662bf84 100644 --- a/docs/changelogs/v0.30.md +++ b/docs/changelogs/v0.30.md @@ -7,6 +7,7 @@ - [Overview](#overview) - [🔦 Highlights](#-highlights) - [Automated `ipfs version check`](#automated-ipfs-version-check) + - [Version Suffix Configuration](#version-suffix-configuration) - [📝 Changelog](#-changelog) - [👨‍👩‍👧‍👦 Contributors](#-contributors) @@ -22,11 +23,12 @@ If more than 5% of Kubo peers seen by your node are running a newer version, you - For manual checks, refer to `ipfs version check --help` for details. - To disable automated checks, set [`Version.SwarmCheckEnabled`](https://github.com/ipfs/kubo/blob/master/docs/config.md#versionswarmcheckenabled) to `false`. -#### `Version.AgentSuffix` +#### Version Suffix Configuration -It is now possible to define optional agent version suffix presented in `ipfs id` and exposed via [libp2p identify protocol](https://github.com/libp2p/specs/blob/master/identify/README.md#agentversion). +Defining the optional agent version suffix is now simpler. The [`Version.AgentSuffix`](https://github.com/ipfs/kubo/blob/master/docs/config.md#agentsuffix) value from the Kubo config takes precedence over any value provided via `ipfs daemon --agent-version-suffix` (which is still supported). -See [`Version.AgentSuffix`](https://github.com/ipfs/kubo/blob/master/docs/config.md#agentsuffix). +> [!NOTE] +> Setting a custom version suffix helps with ecosystem analysis, such as Amino DHT reports published at https://stats.ipfs.network ### 📝 Changelog diff --git a/docs/config.md b/docs/config.md index 35370d21c57..c7355072d4b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2449,11 +2449,14 @@ other peers version for detecting when there is time to update. Optional suffix to the AgentVersion presented by `ipfs id` and exposed via [libp2p identify protocol](https://github.com/libp2p/specs/blob/master/identify/README.md#agentversion). -The value from config is appended to one passed via `ipfs daemon --agent-version-suffix`. +The value from config takes precedence over value passed via `ipfs daemon --agent-version-suffix`. + +> [!NOTE] +> Setting a custom version suffix helps with ecosystem analysis, such as Amino DHT reports published at https://stats.ipfs.network Default: `""` (no suffix, or value from `ipfs daemon --agent-version-suffix=`) -Type: `optionaliString` +Type: `optionalString` ### `Version.SwarmCheckEnabled`