package main import ( "os" "path/filepath" "strconv" "strings" ) // Split by direction of travel so each counter belongs to exactly one // direction: the transmitting interface owns the tx fields and the receiving one // the rx fields. Reading both sets off both interfaces double counts every drop. // // The two sides come from different places, and deliberately not from both at // once. dev_get_stats adds the core's own drop counts on top of whatever the // driver reports, so a sysfs field is the driver's counter plus the stack's and // overlaps any driver counter read beside it. // // Sending is the stack's story: what it refused, and what the qdisc discarded // because we outran it, neither of which the driver ever sees. var nicTxFields = []string{ "tx_errors", "tx_dropped", "tx_fifo_errors", "tx_carrier_errors", "tx_aborted_errors", "tx_window_errors", "collisions", } // Receiving is the hardware's, and is taken from the driver's own array: // sysfs flattens and overlaps it (crc errors fold into rx_errors, so summing // rx_errors alongside rx_crc_errors charges every bad frame twice). Named // individually, so nothing contains anything else in the list; the 82599 // exposes no jabber, fragment, illegal-byte or fault counters (docs/nics/x520/). var nicRxFields = []string{ "rx_crc_errors", "rx_missed_errors", "rx_length_errors", "rx_long_length_errors", "rx_short_length_errors", } func ifDriver(name string) (string, error) { link, err := os.Readlink("/sys/class/net/" + name + "/device/driver") if err != nil { return "", err } return filepath.Base(link), nil } func readUint(path string) (uint64, bool) { b, err := os.ReadFile(path) if err != nil { return 0, false } v, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64) if err != nil { return 0, false } return v, true } // Every counter here was read once before the test began, so one that stops // reading now is the interface going away underneath us. Stopping is the only // honest answer: a tester that quietly gives up counting errors goes on // reporting a clean link, which is the one lie it must never tell. func mustReadUint(path string) uint64 { v, ok := readUint(path) if !ok { panic("reading " + path) } return v } func sumFields(base string, fields []string) uint64 { var total uint64 for _, f := range fields { total += mustReadUint(base + "/statistics/" + f) } return total } // A read is an ioctl and a handful of sysfs attributes, each costing // microseconds of open and read, so it rides the sampler's goroutine and // clock rather than the one that has a frame to draw every sixteen // milliseconds; a link error lands on the panel as quickly as a lost frame. type nicCounters struct { txBase string rx *statReader raw uint64 } func newNICCounters(fd int, txName, rxName string) (*nicCounters, error) { rx, err := newStatReader(fd, rxName, nicRxFields) if err != nil { return nil, err } return &nicCounters{ txBase: "/sys/class/net/" + txName, rx: rx, }, nil } // Carrier is a property of the netdev rather than a counter of either side, and // is taken from the transmitting interface only: across both directions each // interface transmits exactly once, so every flap is counted once. func (c *nicCounters) read() uint64 { return sumFields(c.txBase, nicTxFields) + c.rx.sum() + mustReadUint(c.txBase+"/carrier_down_count") } // Nic counters run from boot and restart from zero whenever the driver resets // its statistics, so only forward motion is the delta: a raw difference would // charge a boot's worth of errors to the first collection and turn a reset // into a near-2^64 underflow. func (c *nicCounters) delta() uint64 { raw := c.read() var d uint64 if raw > c.raw { d = raw - c.raw } c.raw = raw return d }