@cryptotaxi247 / kubo / commits / 049256c22

fix(telemetry): improve vm/container detection (#10944)

* telemetry: use systemd-detect-virt for container/vm detection Current VM detection is not very accurate and systemd-detect-virt does exactly what's needed under a miriad of virtualization platforms. The downside is that we are running a system command which is uglier and might perhaps flip anti-viruses or something. * telemetry: improve vm/container detection with pure go replace systemd-detect-virt with file-based detection to avoid: - security risks from executing external binaries - unnecessary repeated detection (now cached with sync.Once) - missing detection on non-systemd systems removes false positives: - cpu hypervisor flag (indicates capability, not guest status) - generic dmi strings that match physical hardware - overlay filesystem check (used by immutable distros) Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com> Co-authored-by: Marcin Rataj <lidel@lidel.org>

Hector Sanjuan committed Sep 8, 2025 at 18:38 UTC 049256c22f5f0f5bcbece8c8e261e8b5ca55773d
1 file changed +119 -19
plugin/plugins/telemetry/telemetry.go
+119 -19
@@ -9,7 +9,9 @@ import (
9 "os"
10 "path"
11 "runtime"
12 + "slices"
13 "strings"
14 + "sync"
15 "time"
16
17 "github.com/google/uuid"
@@ -27,6 +29,14 @@ import (
29
30 var log = logging.Logger("telemetry")
31
32 +// Caching for virtualization detection - these values never change during process lifetime
33 +var (
34 + containerDetectionOnce sync.Once
35 + vmDetectionOnce sync.Once
36 + isContainerCached bool
37 + isVMCached bool
38 +)
39 +
40 const (
41 modeEnvVar = "IPFS_TELEMETRY"
42 uuidFilename = "telemetry_uuid"
@@ -476,45 +486,135 @@ func (p *telemetryPlugin) collectPlatformInfo() {
486 }
487
488 func isRunningInContainer() bool {
479 - // Check for Docker container
489 + containerDetectionOnce.Do(func() {
490 + isContainerCached = detectContainer()
491 + })
492 + return isContainerCached
493 +}
494 +
495 +func detectContainer() bool {
496 + // Docker creates /.dockerenv inside containers
497 if _, err := os.Stat("/.dockerenv"); err == nil {
498 return true
499 }
500
484 - // Check cgroup for container
485 - content, err := os.ReadFile("/proc/self/cgroup")
486 - if err == nil {
487 - if strings.Contains(string(content), "docker") || strings.Contains(string(content), "lxc") || strings.Contains(string(content), "/kubepods") {
488 - return true
489 - }
501 + // Kubernetes mounts service account tokens inside pods
502 + if _, err := os.Stat("/var/run/secrets/kubernetes.io"); err == nil {
503 + return true
504 }
505
492 - content, err = os.ReadFile("/proc/self/mountinfo")
493 - if err == nil {
506 + // systemd-nspawn creates this file inside containers
507 + if _, err := os.Stat("/run/systemd/container"); err == nil {
508 + return true
509 + }
510 +
511 + // Check if our process is running inside a container cgroup
512 + // Look for container-specific patterns in the cgroup path after "::/"
513 + if content, err := os.ReadFile("/proc/self/cgroup"); err == nil {
514 for line := range strings.Lines(string(content)) {
495 - if strings.Contains(line, "overlay") && strings.Contains(line, "/var/lib/containers/storage/overlay") {
515 + // cgroup lines format: "ID:subsystem:/path"
516 + // We want to check the path part after the last ":"
517 + parts := strings.SplitN(line, ":", 3)
518 + if len(parts) == 3 {
519 + cgroupPath := parts[2]
520 + // Check for container-specific paths
521 + containerIndicators := []string{
522 + "/docker/", // Docker containers
523 + "/containerd/", // containerd runtime
524 + "/cri-o/", // CRI-O runtime
525 + "/lxc/", // LXC containers
526 + "/podman/", // Podman containers
527 + "/kubepods/", // Kubernetes pods
528 + }
529 + for _, indicator := range containerIndicators {
530 + if strings.Contains(cgroupPath, indicator) {
531 + return true
532 + }
533 + }
534 + }
535 + }
536 + }
537 +
538 + // WSL is technically a container-like environment
539 + if runtime.GOOS == "linux" {
540 + if content, err := os.ReadFile("/proc/sys/kernel/osrelease"); err == nil {
541 + osrelease := strings.ToLower(string(content))
542 + if strings.Contains(osrelease, "microsoft") || strings.Contains(osrelease, "wsl") {
543 return true
544 }
545 }
546 }
547
501 - // Also check for systemd-nspawn
502 - if _, err := os.Stat("/run/systemd/container"); err == nil {
503 - return true
548 + // LXC sets container environment variable
549 + if content, err := os.ReadFile("/proc/1/environ"); err == nil {
550 + if strings.Contains(string(content), "container=lxc") {
551 + return true
552 + }
553 + }
554 +
555 + // Additional check: In containers, PID 1 is often not systemd/init
556 + if content, err := os.ReadFile("/proc/1/comm"); err == nil {
557 + pid1 := strings.TrimSpace(string(content))
558 + // Common container init processes
559 + containerInits := []string{"tini", "dumb-init", "s6-svscan", "runit"}
560 + if slices.Contains(containerInits, pid1) {
561 + return true
562 + }
563 }
564
565 return false
566 }
567
568 func isRunningInVM() bool {
510 - // Check for VM
511 - if _, err := os.Stat("/sys/hypervisor/uuid"); err == nil {
512 - return true
569 + vmDetectionOnce.Do(func() {
570 + isVMCached = detectVM()
571 + })
572 + return isVMCached
573 +}
574 +
575 +func detectVM() bool {
576 + // Check for VM-specific files and drivers that only exist inside VMs
577 + vmIndicators := []string{
578 + "/proc/xen", // Xen hypervisor guest
579 + "/sys/hypervisor/uuid", // KVM/Xen hypervisor guest
580 + "/dev/vboxguest", // VirtualBox guest additions
581 + "/sys/module/vmw_balloon", // VMware balloon driver (guest only)
582 + "/sys/module/hv_vmbus", // Hyper-V VM bus driver (guest only)
583 }
584
515 - // Check for other VM indicators
516 - if _, err := os.Stat("/dev/virt-0"); err == nil {
517 - return true
585 + for _, path := range vmIndicators {
586 + if _, err := os.Stat(path); err == nil {
587 + return true
588 + }
589 + }
590 +
591 + // Check DMI for VM vendors - these strings only appear inside VMs
592 + // DMI (Desktop Management Interface) is populated by the hypervisor
593 + dmiFiles := map[string][]string{
594 + "/sys/class/dmi/id/sys_vendor": {
595 + "qemu", "kvm", "vmware", "virtualbox", "xen",
596 + "parallels", // Parallels Desktop
597 + // Note: Removed "microsoft corporation" as it can match Surface devices
598 + },
599 + "/sys/class/dmi/id/product_name": {
600 + "virtualbox", "vmware", "kvm", "qemu",
601 + "hvm domu", // Xen HVM guest
602 + // Note: Removed generic "virtual machine" to avoid false positives
603 + },
604 + "/sys/class/dmi/id/chassis_vendor": {
605 + "qemu", "oracle", // Oracle for VirtualBox
606 + },
607 + }
608 +
609 + for path, signatures := range dmiFiles {
610 + if content, err := os.ReadFile(path); err == nil {
611 + contentStr := strings.ToLower(strings.TrimSpace(string(content)))
612 + for _, sig := range signatures {
613 + if strings.Contains(contentStr, sig) {
614 + return true
615 + }
616 + }
617 + }
618 }
619
620 return false