Skip to content

Commit 049256c

Browse files
hsanjuangammazerolidel
authored
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 <[email protected]> Co-authored-by: Marcin Rataj <[email protected]>
1 parent 906ce80 commit 049256c

File tree

1 file changed

+119
-19
lines changed

1 file changed

+119
-19
lines changed

plugin/plugins/telemetry/telemetry.go

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"os"
1010
"path"
1111
"runtime"
12+
"slices"
1213
"strings"
14+
"sync"
1315
"time"
1416

1517
"github.com/google/uuid"
@@ -27,6 +29,14 @@ import (
2729

2830
var log = logging.Logger("telemetry")
2931

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+
3040
const (
3141
modeEnvVar = "IPFS_TELEMETRY"
3242
uuidFilename = "telemetry_uuid"
@@ -476,45 +486,135 @@ func (p *telemetryPlugin) collectPlatformInfo() {
476486
}
477487

478488
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
480497
if _, err := os.Stat("/.dockerenv"); err == nil {
481498
return true
482499
}
483500

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
490504
}
491505

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 {
494514
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") {
496543
return true
497544
}
498545
}
499546
}
500547

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+
}
504563
}
505564

506565
return false
507566
}
508567

509568
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)
513583
}
514584

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+
}
518618
}
519619

520620
return false

0 commit comments

Comments
 (0)