236 lines
8.5 KiB
Go
236 lines
8.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|