From 24ff2ac7fb730af63dab13844e3dd0a3f914952d Mon Sep 17 00:00:00 2001 From: Hector Sanjuan Date: Thu, 10 Jul 2025 09:51:01 +0200 Subject: [PATCH] checkpoint --- plugin/plugins/telemetry/telemetry.go | 610 +++++++++++++++----------- 1 file changed, 364 insertions(+), 246 deletions(-) diff --git a/plugin/plugins/telemetry/telemetry.go b/plugin/plugins/telemetry/telemetry.go index 0b9743dd1..fbc443e03 100644 --- a/plugin/plugins/telemetry/telemetry.go +++ b/plugin/plugins/telemetry/telemetry.go @@ -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)") }