195 lines
4.5 KiB
Go
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
|
|
}
|