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 }