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 }