Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: periodic version check and json config #10438

Merged
merged 10 commits into from
Jul 24, 2024
Merged
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
57 changes: 54 additions & 3 deletions cmd/ipfs/kubo/daemon.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package kubo

import (
"context"
"errors"
_ "expvar"
"fmt"
"math"
"net"
"net/http"
_ "net/http/pprof"
Expand Down Expand Up @@ -438,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)
Expand Down Expand Up @@ -610,6 +614,15 @@ 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,
cfg.Version.SwarmCheckEnabled.WithDefault(true),
cfg.Version.SwarmCheckPercentThreshold.WithDefault(config.DefaultSwarmCheckPercentThreshold),
)
}
})
}
Expand Down Expand Up @@ -1052,3 +1065,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, enabled bool, percentThreshold int64) {
if !enabled {
return
}
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
go func() {
for {
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
// 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
}
}
}()
}
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Config struct {
Plugins Plugins
Pinning Pinning
Import Import
Version Version

Internal Internal // experimental/unstable options
}
Expand Down
14 changes: 14 additions & 0 deletions config/version.go
Original file line number Diff line number Diff line change
@@ -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"`
}
1 change: 1 addition & 0 deletions core/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func TestCommands(t *testing.T) {
"/swarm/resources",
"/update",
"/version",
"/version/check",
"/version/deps",
}

Expand Down
181 changes: 174 additions & 7 deletions core/commands/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@ import (
"fmt"
"io"
"runtime/debug"
"strings"

version "github.com/ipfs/kubo"

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"
peer "github.com/libp2p/go-libp2p/core/peer"
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"
versionCheckThresholdOptionName = "min-percent"
)

var VersionCmd = &cmds.Command{
Expand All @@ -24,7 +32,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{
Expand Down Expand Up @@ -130,3 +139,161 @@ 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 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
Version.SwarmCheckEnabled:false in the config.
`,
},
Options: []cmds.Option{
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{},

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
}

minPercent, _ := req.Options[versionCheckThresholdOptionName].(int64)
output, err := DetectNewKuboVersion(nd, minPercent)
if err != nil {
return err
}

if err := cmds.EmitOnce(res, output); err != nil {
return err
}
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, 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",
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")
}

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)

// Gathered metric are returned every time
return VersionCheckOutput{
UpdateAvailable: (greaterFraction >= minFraction),
RunningVersion: ourVersion.String(),
GreatestVersion: greatestVersionSeen.String(),
PeersSampled: totalPeersSampled,
WithGreaterVersion: withGreaterVersion,
}, nil
}
17 changes: 17 additions & 0 deletions docs/changelogs/v0.30.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,30 @@

- [Overview](#overview)
- [🔦 Highlights](#-highlights)
- [Automated `ipfs version check`](#automated-ipfs-version-check)
- [Version Suffix Configuration](#version-suffix-configuration)
- [📝 Changelog](#-changelog)
- [👨‍👩‍👧‍👦 Contributors](#-contributors)

### Overview

### 🔦 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.

- 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 Suffix Configuration

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).

> [!NOTE]
> Setting a custom version suffix helps with ecosystem analysis, such as Amino DHT reports published at https://stats.ipfs.network

### 📝 Changelog

### 👨‍👩‍👧‍👦 Contributors
Loading
Loading