144 lines
3.7 KiB
Go
144 lines
3.7 KiB
Go
|
|
package network
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bufio"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
type InterfaceStats struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
State string `json:"state"` // up, down, unknown
|
||
|
|
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"` // dhcp, static, loopback, manual, unknown
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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{}}
|
||
|
|
|
||
|
|
// Operational state
|
||
|
|
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 <gw> 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)
|
||
|
|
}
|