192 lines
4.7 KiB
Go
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
|
|
}
|