package agent import ( "net" "os" "path/filepath" "testing" "time" ) // Sample /proc/net/dev with a realistic mix: physical NIC, loopback, docker // bridge + veth pair, WireGuard tunnel, a VLAN child and a bridge holding // the host address (vmbr-style stacked name). const sampleNetDev = `Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 1000 10 0 0 0 0 0 0 1000 10 0 0 0 0 0 0 eth0: 5000000 5000 0 0 0 0 0 0 3000000 4000 0 0 0 0 0 0 docker0: 999000 900 0 0 0 0 0 0 999000 900 0 0 0 0 0 0 veth1234: 888000 800 0 0 0 0 0 0 888000 800 0 0 0 0 0 0 wg0: 777000 700 0 0 0 0 0 0 777000 700 0 0 0 0 0 0 eth0.100: 666000 600 0 0 0 0 0 0 666000 600 0 0 0 0 0 0 vmbr0: 555000 500 0 0 0 0 0 0 555000 500 0 0 0 0 0 0 ` func TestCountedInterfacesNameLayers(t *testing.T) { // Empty sysfs root: layers 1+2 (names) must already exclude everything // but eth0. counted := countedInterfaces(parseNetDev(sampleNetDev), t.TempDir()) if len(counted) != 1 || counted[0].name != "eth0" { t.Fatalf("counted = %+v, want only eth0", counted) } if counted[0].rx != 5000000 || counted[0].tx != 3000000 { t.Fatalf("eth0 counters = %d/%d", counted[0].rx, counted[0].tx) } } func TestCountedElsewhereSysfsLayer(t *testing.T) { root := t.TempDir() mk := func(name string, files map[string]string, dirs ...string) { dev := filepath.Join(root, name) if err := os.MkdirAll(dev, 0o755); err != nil { t.Fatal(err) } for f, content := range files { if err := os.WriteFile(filepath.Join(dev, f), []byte(content), 0o644); err != nil { t.Fatal(err) } } for _, d := range dirs { if err := os.MkdirAll(filepath.Join(dev, d), 0o755); err != nil { t.Fatal(err) } } } // Plain NIC: no uevent devtype, hardware device link present. mk("eth0", map[string]string{"type": "1", "uevent": ""}, "device") // A user-named bridge: caught by DEVTYPE alone. mk("lan0", map[string]string{"type": "1", "uevent": "DEVTYPE=bridge\n"}) // A VM tap under a made-up name: caught as a bridge port. mk("vnet0", map[string]string{"type": "1", "uevent": ""}, "brport") // A user-named tunnel (type 768 = ipip, no hardware). mk("he-ipv6", map[string]string{"type": "768", "uevent": ""}) // Raw-IP LTE modem: tunnel link type (65534) but real hardware -> counts. mk("wwan0", map[string]string{"type": "65534", "uevent": ""}, "device") // macvlan child: has a lower_* link to its parent. mk("mac0", map[string]string{"type": "1", "uevent": ""}, "lower_eth0") want := map[string]bool{ "eth0": true, "lan0": false, "vnet0": false, "he-ipv6": false, "wwan0": true, "mac0": false, } for name, w := range want { if got := ifaceCounts(root, name); got != w { t.Errorf("ifaceCounts(%s) = %v, want %v", name, got, w) } } } func TestBootEpochDiscipline(t *testing.T) { base := bootEpoch("boot-1", []string{"eth0", "eth1"}) // Order must not matter (the set is sorted before hashing). if got := bootEpoch("boot-1", []string{"eth1", "eth0"}); got != base { t.Fatalf("epoch not order-stable: %s vs %s", got, base) } if len(base) != len("boot-1/")+16 { t.Fatalf("epoch shape wrong: %q", base) } // A set change is a new epoch (re-baseline), like the source. if got := bootEpoch("boot-1", []string{"eth0"}); got == base { t.Fatal("epoch unchanged after counted set change") } // A kernel reboot is a new epoch even with the same set. if got := bootEpoch("boot-2", []string{"eth0", "eth1"}); got == base { t.Fatal("epoch unchanged after reboot") } // Known vector: FNV-1a-64 of "eth0" (offset basis, prime per hash/fnv). if got := bootEpoch("b", []string{"eth0"}); got != "b/"+fnvEth0 { t.Fatalf("epoch digest = %s, want b/%s", got, fnvEth0) } } const fnvEth0 = "c41bf7607a25a404" // hash/fnv FNV-1a-64 over "eth0" func TestNetRateBaselineAndSaturation(t *testing.T) { c := &probeCollector{} t0 := time.Unix(1000, 0) // First sample only establishes the baseline. if rx, tx := c.netRate([]netCounters{{"eth0", 1000, 2000}}, t0); rx != 0 || tx != 0 { t.Fatalf("first sample rate = %d/%d, want 0/0", rx, tx) } // Second sample 10s later: +10000/+5000 bytes -> 1000/500 B/s. if rx, tx := c.netRate([]netCounters{{"eth0", 11000, 7000}}, t0.Add(10*time.Second)); rx != 1000 || tx != 500 { t.Fatalf("rate = %d/%d, want 1000/500", rx, tx) } // A counter going backwards (restart) saturates to 0, never wraps. if rx, tx := c.netRate([]netCounters{{"eth0", 5, 5}}, t0.Add(20*time.Second)); rx != 0 || tx != 0 { t.Fatalf("regressed rate = %d/%d, want 0/0", rx, tx) } // An interface joining later contributes nothing in its first sample. rx, _ := c.netRate([]netCounters{{"eth0", 105, 5}, {"eth1", 999999, 0}}, t0.Add(30*time.Second)) if rx != 10 { t.Fatalf("rate with joining iface = %d, want 10", rx) } } func TestMemUsedFreeSemantics(t *testing.T) { // free口径: used = total - available (cache not charged as used). total, used := memUsedFree(map[string]uint64{ "MemTotal": 1000, "MemAvailable": 400, "MemFree": 100, "Buffers": 50, "Cached": 200, }) if total != 1000 || used != 600 { t.Fatalf("got %d/%d, want 1000/600", total, used) } // No MemAvailable (old kernels): Free+Buffers+Cached fallback. _, used = memUsedFree(map[string]uint64{ "MemTotal": 1000, "MemFree": 100, "Buffers": 50, "Cached": 200, }) if used != 650 { t.Fatalf("fallback used = %d, want 650", used) } // LXCFS can report available > total: fall back to total - MemFree. _, used = memUsedFree(map[string]uint64{ "MemTotal": 1000, "MemAvailable": 5000, "MemFree": 250, }) if used != 750 { t.Fatalf("lxcfs used = %d, want 750", used) } // Swap: total - free, SwapCached NOT subtracted. st, su := swapUsedFree(map[string]uint64{"SwapTotal": 500, "SwapFree": 125}) if st != 500 || su != 375 { t.Fatalf("swap = %d/%d, want 500/375", st, su) } } func TestParseMountsDfSemantics(t *testing.T) { mounts := ` sysfs /sys sysfs rw 0 0 /dev/sda1 / ext4 rw 0 0 tmpfs /run tmpfs rw 0 0 /dev/sda1 /mnt/bind ext4 rw 0 0 /dev/sdb1 /data ext4 rw 0 0 /dev/sdb1 /data/bind ext4 rw 0 0 server:/export /mnt/nfs nfs rw 0 0 overlay /var/lib/docker overlay rw 0 0 tank/home /home zfs rw 0 0 tank/data /srv zfs rw 0 0 /dev/sdc1 /shadow ext4 rw 0 0 tmpfs /shadow tmpfs rw 0 0 ` got := realMountPoints(mounts) // / (sda1) and /data (sdb1) dedupe their binds; zfs dedupes by pool; // /shadow's real fs is shadowed by a later tmpfs so it drops out. want := []string{"/", "/data", "/home"} if len(got) != len(want) { t.Fatalf("mounts = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("mounts = %v, want %v", got, want) } } } func TestConnCountsSockstat(t *testing.T) { v4 := "sockets: used 100\nTCP: inuse 10 orphan 0 tw 5 alloc 20 mem 1\nUDP: inuse 7 mem 0\n" v6 := "TCP6: inuse 3\nUDP6: inuse 2\n" tcp, udp := connCounts(v4, v6) // tw counted once (v4 line serves both families): 10+5+3; udp 7+2. if tcp != 18 || udp != 9 { t.Fatalf("tcp/udp = %d/%d, want 18/9", tcp, udp) } } func TestCPUPercentDiff(t *testing.T) { c := &probeCollector{} if got := c.cpuPercent(&cpuSample{total: 1000, idle: 800}); got != 0 { t.Fatalf("first sample = %v, want 0", got) } // +1000 total, +600 idle -> 40% busy. got := c.cpuPercent(&cpuSample{total: 2000, idle: 1400}) if got < 39.9 || got > 40.1 { t.Fatalf("cpu = %v, want ~40", got) } } func parseIP(t *testing.T, s string) net.IP { t.Helper() ip := net.ParseIP(s) if ip == nil { t.Fatalf("bad test ip %q", s) } return ip } func TestIsPublicIP(t *testing.T) { cases := map[string]bool{ "8.8.8.8": true, "192.168.1.1": false, "100.64.0.1": false, // CGNAT "198.18.0.1": false, // TUN fake-IP range "192.0.0.9": false, "224.0.0.1": false, "2001:db8::1": true, "fd00::1": false, // ULA "fe80::1": false, "2606:4700::1": true, // public by range; WARP exclusion lives in lendsAddress "::1": false, } for s, want := range cases { ip := parseIP(t, s) if got := isPublicIP(ip); got != want { t.Errorf("isPublicIP(%s) = %v, want %v", s, got, want) } } }