package network import ( "bufio" "fmt" "os" "os/exec" "strconv" "strings" ) type InterfaceStats struct { Name string `json:"name"` State string `json:"state"` IPv4 string `json:"ipv4"` IPv4Mask string `json:"ipv4_mask"` IPv6 []string `json:"ipv6"` Gateway string `json:"gateway"` RxBytes uint64 `json:"rx_bytes"` TxBytes uint64 `json:"tx_bytes"` RxPackets uint64 `json:"rx_packets"` TxPackets uint64 `json:"tx_packets"` Mode string `json:"mode"` Type string `json:"type"` } // GetInterfaces returns all network interface names from /sys/class/net. func GetInterfaces() ([]string, error) { entries, err := os.ReadDir("/sys/class/net") if err != nil { return nil, fmt.Errorf("read /sys/class/net: %w", err) } names := make([]string, 0, len(entries)) for _, e := range entries { names = append(names, e.Name()) } return names, nil } // GetInterfaceStats returns current runtime state of an interface. func GetInterfaceStats(name string) (*InterfaceStats, error) { s := &InterfaceStats{Name: name, IPv6: []string{}} if raw, err := os.ReadFile("/sys/class/net/" + name + "/operstate"); err == nil { s.State = strings.TrimSpace(string(raw)) } else { s.State = "unknown" } // IP addresses if out, err := exec.Command("ip", "addr", "show", "dev", name).Output(); err == nil { parseIPAddr(string(out), s) } // Default gateway for this interface if out, err := exec.Command("ip", "route", "show", "dev", name).Output(); err == nil { parseRoute(string(out), s) } // Traffic stats from /proc/net/dev _ = parseNetDev(name, s) return s, nil } func parseIPAddr(output string, s *InterfaceStats) { for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "inet "): parts := strings.Fields(line) if len(parts) < 2 { continue } cidr := parts[1] if slash := strings.Index(cidr, "/"); slash >= 0 { s.IPv4 = cidr[:slash] if prefix, err := strconv.Atoi(cidr[slash+1:]); err == nil { s.IPv4Mask = prefixToMask(prefix) } } else { s.IPv4 = cidr } case strings.HasPrefix(line, "inet6 "): parts := strings.Fields(line) if len(parts) >= 2 { s.IPv6 = append(s.IPv6, parts[1]) } } } } func parseRoute(output string, s *InterfaceStats) { for _, line := range strings.Split(output, "\n") { fields := strings.Fields(strings.TrimSpace(line)) // "default via dev ..." if len(fields) >= 3 && fields[0] == "default" && fields[1] == "via" { s.Gateway = fields[2] return } } } func parseNetDev(name string, s *InterfaceStats) error { f, err := os.Open("/proc/net/dev") if err != nil { return err } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() colon := strings.Index(line, ":") if colon < 0 { continue } if strings.TrimSpace(line[:colon]) != name { continue } // Fields after colon: // rx: bytes packets errs drop fifo frame compressed multicast // tx: bytes packets errs drop fifo colls carrier compressed fields := strings.Fields(line[colon+1:]) if len(fields) >= 10 { s.RxBytes, _ = strconv.ParseUint(fields[0], 10, 64) s.RxPackets, _ = strconv.ParseUint(fields[1], 10, 64) s.TxBytes, _ = strconv.ParseUint(fields[8], 10, 64) s.TxPackets, _ = strconv.ParseUint(fields[9], 10, 64) } return nil } return scanner.Err() } func prefixToMask(prefix int) string { if prefix < 0 || prefix > 32 { return "" } mask := ^uint32(0) << uint(32-prefix) return fmt.Sprintf("%d.%d.%d.%d", (mask>>24)&0xFF, (mask>>16)&0xFF, (mask>>8)&0xFF, mask&0xFF) }