Makefile and agent start.
This commit is contained in:
191
agent/reporter/reporter.go
Normal file
191
agent/reporter/reporter.go
Normal file
@@ -0,0 +1,191 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user