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 6 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
44 changes: 44 additions & 0 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 @@ -610,6 +612,10 @@
}
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)

Check warning on line 618 in cmd/ipfs/kubo/daemon.go

View check run for this annotation

Codecov / codecov/patch

cmd/ipfs/kubo/daemon.go#L615-L618

Added lines #L615 - L618 were not covered by tests
}
})
}
Expand Down Expand Up @@ -1052,3 +1058,41 @@
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

Check warning on line 1094 in cmd/ipfs/kubo/daemon.go

View check run for this annotation

Codecov / codecov/patch

cmd/ipfs/kubo/daemon.go#L1062-L1094

Added lines #L1062 - L1094 were not covered by tests
}
}
}()
}
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
170 changes: 163 additions & 7 deletions core/commands/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@
"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/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"
versionCompareNewFractionOptionName = "min-fraction"
)

var VersionCmd = &cmds.Command{
Expand All @@ -24,7 +31,8 @@
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 +138,151 @@
}),
},
}

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
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),
},
Type: VersionCheckOutput{},

Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
nd, err := cmdenv.GetNode(env)
if err != nil {
return err
}

Check warning on line 176 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L172-L176

Added lines #L172 - L176 were not covered by tests

if !nd.IsOnline {
return ErrNotOnline
}

Check warning on line 180 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L178-L180

Added lines #L178 - L180 were not covered by tests

newerFraction, _ := req.Options[versionCompareNewFractionOptionName].(float64)
output, err := DetectNewKuboVersion(nd, newerFraction)
if err != nil {
return err
}

Check warning on line 186 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L182-L186

Added lines #L182 - L186 were not covered by tests

if err := cmds.EmitOnce(res, output); err != nil {
return err
}
return nil

Check warning on line 191 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L188-L191

Added lines #L188 - L191 were not covered by tests
},
}

// 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)
}

Check warning on line 204 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L199-L204

Added lines #L199 - L204 were not covered by tests
// 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
}

Check warning on line 227 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L206-L227

Added lines #L206 - L227 were not covered by tests

// Ignore prerelases and development releases (-dev, -rcX)
if peerVersion.Metadata() != "" || peerVersion.Prerelease() != "" {
return
}

Check warning on line 232 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L230-L232

Added lines #L230 - L232 were not covered by tests

// 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
}

Check warning on line 244 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L235-L244

Added lines #L235 - L244 were not covered by tests
}

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

Check warning on line 253 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L247-L253

Added lines #L247 - L253 were not covered by tests
}

// 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")
}

Check warning on line 275 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L257-L275

Added lines #L257 - L275 were not covered by tests

// 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

Check warning on line 287 in core/commands/version.go

View check run for this annotation

Codecov / codecov/patch

core/commands/version.go#L278-L287

Added lines #L278 - L287 were not covered by tests
}
9 changes: 9 additions & 0 deletions docs/changelogs/v0.30.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@

- [Overview](#overview)
- [🔦 Highlights](#-highlights)
- [Automated `ipfs version check`](#automated-ipfs-version-check)
- [📝 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.

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
7 changes: 7 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading