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

192 lines
4.7 KiB
Go

package reporter
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
"time"
"kattila-agent/config"
"kattila-agent/models"
"kattila-agent/network"
"kattila-agent/security"
)
var tickCounter int64 = 0
func StartLoop() {
doReport() // run immediately
ticker := time.NewTicker(30 * time.Second)
go func() {
for range ticker.C {
doReport()
}
}()
}
func doReport() {
data, err := network.GatherSystemData()
if err != nil {
log.Printf("reporter: gather error: %v", err)
return
}
tickCounter++
now := time.Now().Unix()
report := models.Report{
Version: 1,
Tick: tickCounter,
Type: "report",
Nonce: security.GenerateNonce(),
Timestamp: now,
AgentID: config.Cfg.AgentID,
AgentVersion: 1,
FleetID: security.FleetID(),
Data: data,
}
report.HMAC = security.SignPayload(report.Data)
err = pushToManager(report)
if err != nil {
log.Printf("reporter: direct push failed (%v). Attempting relay scan...", err)
tryRelay(report, data)
}
}
func pushToManager(report models.Report) error {
body, _ := json.Marshal(report)
url := strings.TrimRight(config.Cfg.ManagerURL, "/") + "/status/updates"
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("bad status code %d: %s", resp.StatusCode, respBody)
}
log.Printf("reporter: Report successfully sent to Manager (tick %d)", report.Tick)
return nil
}
func HandleRelay(body []byte) error {
var envelope models.RelayEnvelope
if err := json.Unmarshal(body, &envelope); err != nil {
return err
}
for _, id := range envelope.RelayPath {
if id == config.Cfg.AgentID {
log.Println("reporter: Dropped relay request: routing loop detected")
return errors.New("routing loop detected")
}
}
envelope.RelayPath = append(envelope.RelayPath, config.Cfg.AgentID)
if len(envelope.RelayPath) > 3 {
return errors.New("relay hop limit exceeded")
}
envelopeBody, _ := json.Marshal(envelope)
url := strings.TrimRight(config.Cfg.ManagerURL, "/") + "/status/updates"
req, err := http.NewRequest("POST", url, bytes.NewBuffer(envelopeBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("reporter: Manager unreachable during relay forward, hopping further...")
data, err := network.GatherSystemData()
if err == nil {
return tryRelayEnvelope(envelope, data)
}
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status from manager: %d", resp.StatusCode)
}
log.Printf("reporter: Successfully relayed message for %s", envelope.Payload.AgentID)
return nil
}
func tryRelay(report models.Report, localData models.SystemData) {
env := models.RelayEnvelope{
RelayPath: []string{config.Cfg.AgentID},
Payload: report,
}
err := tryRelayEnvelope(env, localData)
if err != nil {
log.Printf("reporter: Exhausted all relays, couldn't push report: %v", err)
}
}
func tryRelayEnvelope(env models.RelayEnvelope, data models.SystemData) error {
for _, wg := range data.WGPeers {
for _, allowedRaw := range wg.AllowedIPs {
ip, _, err := net.ParseCIDR(allowedRaw)
if err != nil {
ip = net.ParseIP(allowedRaw)
}
if ip != nil {
ipTarget := ip.String()
if pingPeer(ipTarget) {
log.Printf("reporter: Found relay peer at %s, forwarding...", ipTarget)
err := pushToRelay(ipTarget, env)
if err == nil {
return nil
}
log.Printf("reporter: Failed to push to relay %s: %v", ipTarget, err)
}
}
}
}
return errors.New("no working relays found")
}
func pingPeer(ip string) bool {
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://%s:%s/status/peer", ip, config.Cfg.AgentPort))
if err == nil {
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
return false
}
func pushToRelay(ip string, env models.RelayEnvelope) error {
body, _ := json.Marshal(env)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Post(fmt.Sprintf("http://%s:%s/status/relay", ip, config.Cfg.AgentPort), "application/json", bytes.NewBuffer(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("relay rejected forwarding attempt with %d", resp.StatusCode)
}
return nil
}