9
"os"
10
"path"
11
"runtime"
12
+ "slices"
13
"strings"
14
+ "sync"
15
"time"
16
17
"github.com/google/uuid"
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"
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