Files
kattila.status/agent/network/network.go
2026-04-17 19:23:04 +03:00

195 lines
4.5 KiB
Go

package network
import (
"bytes"
"encoding/json"
"fmt"
"os/exec"
"strconv"
"strings"
"kattila-agent/models"
)
type ipAddrInfo struct {
Local string `json:"local"`
PrefixLen int `json:"prefixlen"`
}
type ipInterface struct {
Ifname string `json:"ifname"`
Address string `json:"address"` // MAC address in newer iproute2
AddrInfo []ipAddrInfo `json:"addr_info"`
}
func getInterfaces() ([]models.Interface, error) {
cmd := exec.Command("ip", "-j", "a")
out, err := cmd.Output()
if err != nil {
return nil, err
}
var parsed []ipInterface
if err := json.Unmarshal(out, &parsed); err != nil {
return nil, err
}
var results []models.Interface
for _, itf := range parsed {
mac := itf.Address
var addrs []string
for _, info := range itf.AddrInfo {
addrs = append(addrs, fmt.Sprintf("%s/%d", info.Local, info.PrefixLen))
}
isVirtual := false
var vpnType *string
if strings.HasPrefix(itf.Ifname, "wg") || strings.HasPrefix(itf.Ifname, "tun") || strings.HasPrefix(itf.Ifname, "parvpn") || strings.HasPrefix(itf.Ifname, "home") || strings.HasPrefix(itf.Ifname, "tailscale") {
isVirtual = true
}
results = append(results, models.Interface{
Name: itf.Ifname,
MAC: mac,
Addresses: addrs,
IsVirtual: isVirtual,
VPNType: vpnType,
})
}
return results, nil
}
type ipRoute struct {
Dst string `json:"dst"`
Gateway string `json:"gateway"`
Via string `json:"via"` // Sometimes present instead of gateway
Dev string `json:"dev"`
}
func getRoutes() ([]models.Route, error) {
cmd := exec.Command("ip", "-j", "-4", "r")
out, err := cmd.Output()
if err != nil {
return nil, err
}
var parsed []ipRoute
if err := json.Unmarshal(out, &parsed); err != nil {
return nil, err
}
var results []models.Route
for _, r := range parsed {
via := r.Gateway
if via == "" {
via = r.Via
}
results = append(results, models.Route{
Dst: r.Dst,
Via: via,
Dev: r.Dev,
})
}
return results, nil
}
func getWgPeers() ([]models.WGPeer, error) {
cmd := exec.Command("wg", "show", "all", "dump")
out, err := cmd.Output()
if err != nil { // wg might fail or not be installed, ignore and return empty
return nil, nil
}
var peers []models.WGPeer
lines := bytes.Split(out, []byte("\n"))
for _, line := range lines {
line = bytes.TrimSpace(line)
if len(line) == 0 {
continue
}
cols := strings.Split(string(line), "\t")
// wg interface line: itf, privkey, pubkey, port, fwmark (5 cols)
// wg peer line: itf, pubkey, psk, endpoint, allowed-ips, handshake, rx, tx, keepalive (9 cols)
if len(cols) >= 8 {
itf := cols[0]
pubkey := cols[1]
endpoint := cols[3]
if endpoint == "(none)" {
endpoint = ""
}
allowedIpsRaw := cols[4]
if allowedIpsRaw == "(none)" {
allowedIpsRaw = ""
}
allowedIps := strings.Split(allowedIpsRaw, ",")
handshake, _ := strconv.ParseInt(cols[5], 10, 64)
rx, _ := strconv.ParseInt(cols[6], 10, 64)
tx, _ := strconv.ParseInt(cols[7], 10, 64)
peers = append(peers, models.WGPeer{
Interface: itf,
PublicKey: pubkey,
Endpoint: endpoint,
AllowedIPs: allowedIps,
LatestHandshake: handshake,
TransferRx: rx,
TransferTx: tx,
})
}
}
return peers, nil
}
func GatherSystemData() (models.SystemData, error) {
hostname, _ := exec.Command("hostname").Output()
// Simplified uptime extraction
uptimeBytes, _ := exec.Command("cat", "/proc/uptime").Output()
var uptimeSec int64
if len(uptimeBytes) > 0 {
parts := strings.Fields(string(uptimeBytes))
if len(parts) > 0 {
uptimeFloat, _ := strconv.ParseFloat(parts[0], 64)
uptimeSec = int64(uptimeFloat)
}
}
loadavgBytes, _ := exec.Command("cat", "/proc/loadavg").Output()
var loadavg []float64
if len(loadavgBytes) > 0 {
parts := strings.Fields(string(loadavgBytes))
if len(parts) >= 3 {
l1, _ := strconv.ParseFloat(parts[0], 64)
l2, _ := strconv.ParseFloat(parts[1], 64)
l3, _ := strconv.ParseFloat(parts[2], 64)
loadavg = []float64{l1, l2, l3}
}
}
intfs, err := getInterfaces()
if err != nil {
intfs = []models.Interface{}
}
routes, err := getRoutes()
if err != nil {
routes = []models.Route{}
}
wgPeers, err := getWgPeers()
if err != nil {
wgPeers = []models.WGPeer{}
}
return models.SystemData{
Hostname: strings.TrimSpace(string(hostname)),
UptimeSeconds: uptimeSec,
LoadAvg: loadavg,
Interfaces: intfs,
Routes: routes,
WGPeers: wgPeers,
}, nil
}