checkpoint

This commit is contained in:
Hector Sanjuan 2025-07-10 09:51:01 +02:00
parent c7f29514b0
commit 24ff2ac7fb

View File

@ -3,40 +3,71 @@ package telemetry
import (
"errors"
"fmt"
"io"
"os"
"path"
"strings"
"syscall"
"time"
"github.com/google/uuid"
logging "github.com/ipfs/go-log/v2"
"github.com/ipfs/kubo/config"
"github.com/ipfs/kubo/core"
"github.com/ipfs/kubo/plugin"
"github.com/ipfs/kubo/repo"
golibp2p "github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/pnet"
"go.uber.org/fx"
"golang.org/x/term"
)
var log = logging.Logger("telemetry")
const (
envVar = "IPFS_TELEMETRY_PLUGIN_MODE"
filename = "telemetry_mode"
modeEnvVar = "IPFS_TELEMETRY_PLUGIN_MODE"
uuidFilename = "telemetry_uuid"
endpoint = "http://127.0.0.1:8083"
)
type pluginMode int
const (
modeAsk pluginMode = iota
modeInfo pluginMode = iota
modeOptIn
modeOptOut
)
type LogEvent struct {
UUID string `json:"uuid"`
AgentVersion string `json:"agent_version"`
PrivateNetwork bool `json:"private_network"`
BootstrappersCustom bool `json:"bootstrappers_custom"`
RepoSizeBucket int `json:"repo_size_bucket"`
UptimeBucket int `json:"uptime_bucket"`
ReproviderStrategy string `json:"reprovider_strategy"`
RoutingType string `json:"routing_type"`
RoutingAcceleratedDHTClient bool `json:"routing_accelerated_dht_client"`
RoutingDelegatedCount int `json:"routing_delegated_count"`
AutoNATServiceMode string `json:"autonat_service_mode"`
AutoNATPubliclyDiallable bool `json:"autonat_publicly_diallable"`
SwarmEnableHolePunching bool `json:"swarm_enable_hole_punching"`
SwarmRelayAddresses bool `json:"swarm_relay_addresses"`
SwarmIPv4PublicAddresses bool `json:"swarm_ipv4_public_addresses"`
SwarmIPv6PublicAddresses bool `json:"swarm_ipv6_public_addresses"`
AutoTLSAutoWSS bool `json:"auto_tls_auto_wss"`
AutoTLSDomainSuffixCustom bool `json:"auto_tls_domain_suffix_custom"`
DiscoveryMDNSEnabled bool `json:"discovery_mdns_enabled"`
PlatformOS string `json:"platform_os"`
PlatformArch string `json:"platform_arch"`
PlatformContainerized bool `json:"platform_containerized"`
}
// Plugins sets the list of plugins to be loaded.
var Plugins = []plugin.Plugin{
&telemetryPlugin{},
@ -44,9 +75,11 @@ var Plugins = []plugin.Plugin{
// telemetryPlugin is an FX plugin for Kubo that detects if the node is running on a private network.
type telemetryPlugin struct {
filename string
mode pluginMode
pnet bool
uuidFilename string
mode pluginMode
node *core.IpfsNode
event *LogEvent
}
func (p *telemetryPlugin) Name() string {
@ -73,144 +106,187 @@ func (p *telemetryPlugin) Init(env *plugin.Environment) error {
}
}
if !isDaemon || isOffline {
log.Debug("telemetry mode: optout (offline/one-shot)")
p.mode = modeOptOut
return nil
}
p.event = &LogEvent{}
repoPath := env.Repo
p.filename = path.Join(repoPath, filename)
p.uuidFilename = path.Join(repoPath, uuidFilename)
v := os.Getenv(envVar)
v := os.Getenv(modeEnvVar)
if v != "" {
log.Debug("mode set from env-var")
} else { // try with file
b, err := os.ReadFile(p.filename)
if err == nil {
v = string(b)
v = strings.TrimSpace(v)
log.Debug("mode set from file")
} else if cfg := env.Config; cfg != nil { // try with config
fmt.Printf("cfg not nil: %+v", cfg)
pcfg, ok := cfg.(map[string]interface{})
} else if cfg := env.Config; cfg != nil { // try with config
pcfg, ok := cfg.(map[string]interface{})
if ok {
pmode, ok := pcfg["Mode"].(string)
if ok {
pmode, ok := pcfg["Mode"].(string)
if ok {
v = pmode
log.Debug("mode set from config")
}
v = pmode
log.Debug("mode set from config")
}
}
}
log.Debug("telemetry mode: ", v)
switch v {
case "optin":
p.mode = modeOptIn
case "optout":
p.mode = modeOptOut
return nil
case "info":
p.mode = modeInfo
default:
p.mode = modeAsk
p.mode = modeOptIn
}
// loadUUID might switch to modeInfo when generating a new uuid
if err := p.loadUUID(); err != nil {
p.mode = modeOptOut
return nil
}
return nil
}
func (p *telemetryPlugin) setupTelemetry(cfg *config.Config, repo repo.Repo) {
// if we have no host, then we won't be able
// to send telemetry anyways.
if p.mode == modeOptOut {
return
}
func (p *telemetryPlugin) loadUUID() error {
// Generate or read our UUID from disk
b, err := os.ReadFile(p.uuidFilename)
if err == nil {
v := string(b)
v = strings.TrimSpace(v)
uid, err := uuid.Parse(v)
if err != nil {
log.Errorf("cannot parse telemetry uuid: %s", err)
return err
}
p.event.UUID = uid.String()
return err
} else if os.IsNotExist(err) {
uid, err := uuid.NewRandom()
if err != nil {
log.Errorf("cannot generate telemetry uuid: %s", err)
return err
}
p.event.UUID = uid.String()
p.mode = modeInfo
// Check whether we have a standard setup.
standardSetup := true
if pnet.ForcePrivateNetwork {
standardSetup = false
} else if key, _ := repo.SwarmKey(); key != nil {
standardSetup = false
} else if !hasDefaultBootstrapPeers(cfg) {
standardSetup = false
}
// in a standard setup we don't ask, assume opt-in. This does not
// change the status-quo as tracking already exist via
// bootstrappers/peerlog. We are just officializing it under a
// separate telemetry-protocol.
if standardSetup {
p.mode = modeOptIn
return
}
// on non-standard setups, we ask
// user gave permission already
if p.mode == modeOptIn {
p.pnet = true
return
}
// ask and send if allowed.
if p.pnetPromptWithTimeout(15 * time.Second) {
p.pnet = true
return
// Write the UUID to disk
if err := os.WriteFile(p.uuidFilename, []byte(p.event.UUID), 0600); err != nil {
log.Errorf("cannot write telemetry uuid: %s", err)
return err
}
} else {
log.Errorf("error reading telemetry uuid from disk: %s", err)
return err
}
return nil
}
// ParamsIn are the params for the decorator. Includes golibp2p.Option so that
// it is called before initializing a Host.
type ParamsIn struct {
fx.In
Repo repo.Repo
Cfg *config.Config
Opts [][]golibp2p.Option `group:"libp2p"`
}
// func (p *telemetryPlugin) setupTelemetry(cfg *config.Config, repo repo.Repo) {
// // noop on opt out
// if p.mode == modeOptOut {
// return
// }
// ParamsOut includes the modified Opts from the decorator.
type ParamsOut struct {
fx.Out
Opts [][]golibp2p.Option `group:"libp2p"`
}
// // We should have a UUID
// if len(p.event.UUID) == 0 {
// return
// }
// Decorator hijacks the initialization process. Params ensures that we are
// called before the libp2p Host is initialized and starts listening, as we declare that we want to return a Libp2p Option (even if we don't).
func (p *telemetryPlugin) Decorator(in ParamsIn) (out ParamsOut) {
log.Debug("telemetry decorator executed")
p.setupTelemetry(in.Cfg, in.Repo)
// out.Opts = append(in.Opts, []golibp2p.Option{golibp2p.UserAgent("my ass")})
out.Opts = in.Opts
return
}
// if p.mode == modeInfo {
// p.showInfo()
// }
type TelemetryIn struct {
fx.In
// // Check whether we have a standard setup.
// standardSetup := true
// if pnet.ForcePrivateNetwork {
// standardSetup = false
// } else if key, _ := repo.SwarmKey(); key != nil {
// standardSetup = false
// } else if !hasDefaultBootstrapPeers(cfg) {
// standardSetup = false
// }
Host host.Host `optional:"true"`
}
// // in a standard setup we don't ask, assume opt-in. This does not
// // change the status-quo as tracking already exist via
// // bootstrappers/peerlog. We are just officializing it under a
// // separate telemetry-protocol.
// if standardSetup {
// p.mode = modeOptIn
// return
// }
func (p *telemetryPlugin) Telemetry(in TelemetryIn) {
if p.mode == modeOptOut || in.Host == nil {
return
}
// // on non-standard setups, we ask
if p.pnet {
sendPnetTelemetry(in.Host)
return
}
sendTelemetry(in.Host)
}
// // user gave permission already
// if p.mode == modeOptIn {
// p.pnet = true
// return
// }
func (p *telemetryPlugin) Options(info core.FXNodeInfo) ([]fx.Option, error) {
if p.mode == modeOptOut {
return info.FXOptions, nil
}
// // ask and send if allowed.
// if p.pnetPromptWithTimeout(15 * time.Second) {
// p.pnet = true
// return
// }
// }
opts := append(
info.FXOptions,
fx.Decorate(p.Decorator), // runs pre Host creation
fx.Invoke(p.Telemetry), // runs post Host creation
)
return opts, nil
}
// // ParamsIn are the params for the decorator. Includes golibp2p.Option so that
// // it is called before initializing a Host.
// type ParamsIn struct {
// fx.In
// Repo repo.Repo
// Cfg *config.Config
// Opts [][]golibp2p.Option `group:"libp2p"`
// }
// // ParamsOut includes the modified Opts from the decorator.
// type ParamsOut struct {
// fx.Out
// Opts [][]golibp2p.Option `group:"libp2p"`
// }
// // Decorator hijacks the initialization process. Params ensures that we are
// // called before the libp2p Host is initialized and starts listening, as we declare that we want to return a Libp2p Option (even if we don't).
// func (p *telemetryPlugin) Decorator(in ParamsIn) (out ParamsOut) {
// log.Debug("telemetry decorator executed")
// p.setupTelemetry(in.Cfg, in.Repo)
// // out.Opts = append(in.Opts, []golibp2p.Option{golibp2p.UserAgent("my ass")})
// out.Opts = in.Opts
// return
// }
// type TelemetryIn struct {
// fx.In
// Host host.Host `optional:"true"`
// }
// func (p *telemetryPlugin) Telemetry(in TelemetryIn) {
// if p.mode == modeOptOut || in.Host == nil {
// return
// }
// if p.pnet {
// sendPnetTelemetry(in.Host)
// return
// }
// sendTelemetry(in.Host)
// }
// func (p *telemetryPlugin) Options(info core.FXNodeInfo) ([]fx.Option, error) {
// if p.mode == modeOptOut {
// return info.FXOptions, nil
// }
// opts := append(
// info.FXOptions,
// fx.Decorate(p.Decorator), // runs pre Host creation
// fx.Invoke(p.Telemetry), // runs post Host creation
// )
// return opts, nil
// }
func hasDefaultBootstrapPeers(cfg *config.Config) bool {
defaultPeers := config.DefaultBootstrapAddresses
@ -230,141 +306,183 @@ func hasDefaultBootstrapPeers(cfg *config.Config) bool {
return true
}
func (p *telemetryPlugin) pnetPromptWithTimeout(timeout time.Duration) bool {
fmt.Print(`
func (p *telemetryPlugin) showInfo() {
fmt.Printf(`
*********************************************
*********************************************
ATTENTION: IT SEEMS YOU ARE RUNNING KUBO ON:
** Telemetry enabled **
* A PRIVATE NETWORK, or using
* NON-STANDARD configuration (custom bootstrappers, amino DHT protocols)
Kubo will send anonymized information to the developers:
- Why? The developers want to better understand the usage of different
features.
- What? Anonymized information only. You can inspect it with
'GOLOG_LOG_LEVEL="telemetry=debug"
- When? Soon after daemon's boot, or every 24h.
- How? An HTTP request to %s.
The Kubo team is interested in learning more about how IPFS is used in private
networks. For example, we would like to understand if features like "private
networks" are used or can be removed in future releases.
To opt-out, CTRL-C now and:
* Set %s to "optput", or
* Run 'ipfs config Plugins.Plugins.telemetry.Config.Mode optout'
* This message will be shown only once.
Would you like to OPT-IN to send some anonymized telemetry to help us understand
your usage? If you OPT-IN:
* A stable but temporary peer ID will be generated
* The peer will bootstrap to our public bootstrappers
* Anonymized metrics will be sent via the Telemetry protocol on every boot
* No IP logging will happen
* The temporary peer will disconnect aftewards
* Telemetry can be controlled any time by setting:
* Setting ` + envVar + ` to:
* "ask" : Asks on every daemon start (default)
* "optin": Sends telemetry on every daemon start.
* "optout": Does not send telemetry and disables this message.
* Creating $IPFS_HOME/` + filename + ` with the "ask", "optin", "optout" contents per the above.
Your telemetry UUID is: %s
IF YOU WOULD LIKE TO OPT-IN to telemetry, PRESS "Y".
IF YOU WOULD LIKE TO OPT-OUT to telemetry, PRESS "N".
(boot will continue in 15s)
Your answer (Y/N): `)
pr, pw, err := os.Pipe()
if err != nil {
log.Error(err)
return false
}
// We want to read a single key press without waiting
// for the user to press enter.
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return false
}
// nolint:errcheck
go io.CopyN(pw, os.Stdin, 1)
// Pipes support ReadDeadlines, so it seems nicer to use that
// than signaling over a channel.
err = pr.SetReadDeadline(time.Now().Add(timeout))
if err != nil {
// nolint:errcheck
term.Restore(int(os.Stdin.Fd()), oldState)
return false
}
// Attempt to read one byte from the pipe
b := make([]byte, 1)
_, err = pr.Read(b)
// nolint:errcheck
term.Restore(int(os.Stdin.Fd()), oldState)
// We timed out.
// The following section is the only way to
// achieve waiting for user input until a time-out happens.
//
// Read() is a blocking operation and there is NO WAY to cancel it.
// We cannot close Stdin. We cannot write to Stdin. We can continue on
// timeout but the reader on Stdin will remain blocked and we will
// at least leak a goroutine until some input comes it.
//
// So the only way out of this is to Exec() ourselves again and
// replace our running copy with a new one.
// TODO: Test with supported architectures.
if errors.Is(err, os.ErrDeadlineExceeded) {
fmt.Printf("(Timed-out. Answer: Ask later)\n\n\n")
time.Sleep(2 * time.Second)
exec, err := os.Executable()
if err != nil {
return false
}
// make sure we do not re-execute the checks
os.Setenv(envVar, "optout")
env := os.Environ()
// js/wasm doesn't have this. I hope reasonable architectures
// have this.
err = syscall.Exec(exec, os.Args, env)
if err != nil {
log.Error(err)
}
return false
}
// We didn't timeout. We errored in some other way or we actually
// got user input.
input := string(b[0])
fmt.Println(input)
// Close pipes.
pr.Close()
pw.Close()
switch input {
case "y", "Y":
err = os.WriteFile(p.filename, []byte("optin"), 0600)
if err != nil {
log.Errorf("error saving telemetry preferences: %s", err)
}
return true
case "n", "N":
err = os.WriteFile(p.filename, []byte("optout"), 0600)
if err != nil {
log.Errorf("error saving telemetry preferences: %s", err)
}
return false
default:
return false
}
`, endpoint, modeEnvVar, p.event.UUID)
}
func sendTelemetry(h host.Host) {
fmt.Println("Sending Telemetry (TODO)", h.ID())
func (p *telemetryPlugin) Start(n *core.IpfsNode) error {
p.node = n
if p.mode == modeOptOut {
return nil
}
// We should have a UUID
if len(p.event.UUID) == 0 {
return nil
}
if p.mode == modeInfo {
p.showInfo()
}
time.AfterFunc(time.Second, func() {
sendTelemetry()
})
return errors.New("does this crash it?")
}
func sendPnetTelemetry(h host.Host) {
fmt.Println("Sending pnet Telemetry (TODO)", h.ID())
// func (p *telemetryPlugin) pnetPromptWithTimeout(timeout time.Duration) bool {
// fmt.Print(`
// *********************************************
// *********************************************
// ATTENTION: IT SEEMS YOU ARE RUNNING KUBO ON:
// * A PRIVATE NETWORK, or using
// * NON-STANDARD configuration (custom bootstrappers, amino DHT protocols)
// The Kubo team is interested in learning more about how IPFS is used in private
// networks. For example, we would like to understand if features like "private
// networks" are used or can be removed in future releases.
// Would you like to OPT-IN to send some anonymized telemetry to help us understand
// your usage? If you OPT-IN:
// * A stable but temporary peer ID will be generated
// * The peer will bootstrap to our public bootstrappers
// * Anonymized metrics will be sent via the Telemetry protocol on every boot
// * No IP logging will happen
// * The temporary peer will disconnect aftewards
// * Telemetry can be controlled any time by setting:
// * Setting ` + envVar + ` to:
// * "ask" : Asks on every daemon start (default)
// * "optin": Sends telemetry on every daemon start.
// * "optout": Does not send telemetry and disables this message.
// * Creating $IPFS_HOME/` + filename + ` with the "ask", "optin", "optout" contents per the above.
// IF YOU WOULD LIKE TO OPT-IN to telemetry, PRESS "Y".
// IF YOU WOULD LIKE TO OPT-OUT to telemetry, PRESS "N".
// (boot will continue in 15s)
// Your answer (Y/N): `)
// pr, pw, err := os.Pipe()
// if err != nil {
// log.Error(err)
// return false
// }
// // We want to read a single key press without waiting
// // for the user to press enter.
// oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
// if err != nil {
// return false
// }
// // nolint:errcheck
// go io.CopyN(pw, os.Stdin, 1)
// // Pipes support ReadDeadlines, so it seems nicer to use that
// // than signaling over a channel.
// err = pr.SetReadDeadline(time.Now().Add(timeout))
// if err != nil {
// // nolint:errcheck
// term.Restore(int(os.Stdin.Fd()), oldState)
// return false
// }
// // Attempt to read one byte from the pipe
// b := make([]byte, 1)
// _, err = pr.Read(b)
// // nolint:errcheck
// term.Restore(int(os.Stdin.Fd()), oldState)
// // We timed out.
// // The following section is the only way to
// // achieve waiting for user input until a time-out happens.
// //
// // Read() is a blocking operation and there is NO WAY to cancel it.
// // We cannot close Stdin. We cannot write to Stdin. We can continue on
// // timeout but the reader on Stdin will remain blocked and we will
// // at least leak a goroutine until some input comes it.
// //
// // So the only way out of this is to Exec() ourselves again and
// // replace our running copy with a new one.
// // TODO: Test with supported architectures.
// if errors.Is(err, os.ErrDeadlineExceeded) {
// fmt.Printf("(Timed-out. Answer: Ask later)\n\n\n")
// time.Sleep(2 * time.Second)
// exec, err := os.Executable()
// if err != nil {
// return false
// }
// // make sure we do not re-execute the checks
// os.Setenv(envVar, "optout")
// env := os.Environ()
// // js/wasm doesn't have this. I hope reasonable architectures
// // have this.
// err = syscall.Exec(exec, os.Args, env)
// if err != nil {
// log.Error(err)
// }
// return false
// }
// // We didn't timeout. We errored in some other way or we actually
// // got user input.
// input := string(b[0])
// fmt.Println(input)
// // Close pipes.
// pr.Close()
// pw.Close()
// switch input {
// case "y", "Y":
// err = os.WriteFile(p.filename, []byte("optin"), 0600)
// if err != nil {
// log.Errorf("error saving telemetry preferences: %s", err)
// }
// return true
// case "n", "N":
// err = os.WriteFile(p.filename, []byte("optout"), 0600)
// if err != nil {
// log.Errorf("error saving telemetry preferences: %s", err)
// }
// return false
// default:
// return false
// }
// }
func sendTelemetry() {
fmt.Println("Sending Telemetry (TODO)")
}